Files
flynn/docs/plans/2026-02-13-gmail-quick-reference.md

323 lines
8.7 KiB
Markdown

# Gmail Push Revisit — Quick Reference
## New Config Fields
```typescript
// src/config/schema.ts (line ~179)
const gmailSchema = z.object({
// ... existing fields ...
pubsub_subscription_id: z.string().optional(),
pubsub_pull_interval: z.string().default('60s'),
pubsub_max_messages: z.number().min(1).max(100).default(10),
disable_push: z.boolean().default(false),
});
```
## New Methods
### setupPullSubscription()
```typescript
// src/automation/gmail.ts
private async setupPullSubscription(): Promise<void>
```
- Parses `pubsub_subscription_id` (shorthand or full path)
- Validates format
- Starts interval timer with `pubsub_pull_interval`
### pullSubscriptionMessages()
```typescript
// src/automation/gmail.ts
private async pullSubscriptionMessages(): Promise<void>
```
- Uses `@google-cloud/pubsub` SDK
- Pulls up to `pubsub_max_messages`
- Calls `handlePushNotification()` for each message
- Acknowledges messages after processing
### buildWatchErrorHint()
```typescript
// src/automation/gmail.ts
private buildWatchErrorHint(errMsg: string): string
```
- Matches error patterns ("Invalid topicName", "Permission denied", "Not found")
- Returns actionable hints with gcloud commands
## Modified Methods
### connect()
```typescript
// Changes:
if (!this.config.disable_push) {
await this.setupWatch();
} else {
console.log('Push disabled');
}
await this.setupPullSubscription();
console.log(`Connected (push=${...}, pull=${...}, poll_interval=${...})`);
```
### disconnect()
```typescript
// Add:
if (this.pullSubscriptionTimer) {
clearInterval(this.pullSubscriptionTimer);
this.pullSubscriptionTimer = undefined;
}
```
## Doctor Check Enhancement
```typescript
// src/cli/doctor.ts (line ~246)
const checkGmail: Check = async (ctx) => {
// ... existing validation ...
// NEW: Mode detection
const modes: string[] = [];
const warnings: string[] = [];
if (gmail.pubsub_topic && !gmail.disable_push) {
modes.push('push');
if (ctx.config.server?.tailscale_only && !ctx.config.server?.tailscale?.funnel) {
warnings.push('push requires public endpoint or Tailscale funnel');
}
}
if (gmail.pubsub_subscription_id) {
modes.push('pull');
}
if (modes.length === 0) {
modes.push('poll');
} else {
modes.push('poll-fallback');
}
const modeStr = modes.join(' + ');
const detail = warnings.length > 0
? `${modeStr} (⚠️ ${warnings.join(', ')})`
: `${modeStr}${gmail.output.channel}/${gmail.output.peer}`;
return { status: warnings.length > 0 ? 'warn' : 'pass', label: 'Gmail configured', detail };
};
```
## Test Scenarios
### Pull Subscription
```typescript
it('should set up pull subscription with full path', async () => {
const config = {
...baseConfig,
pubsub_subscription_id: 'projects/test-project/subscriptions/gmail-pull',
};
const watcher = new GmailWatcher(config, channelLookup);
await watcher.connect();
expect(watcher['subscriptionPath']).toBe('projects/test-project/subscriptions/gmail-pull');
expect(watcher['pullSubscriptionTimer']).toBeDefined();
});
```
### Shorthand Auto-Prefix
```typescript
it('should auto-prefix subscription with project_id', async () => {
const config = {
...baseConfig,
pubsub_subscription_id: 'gmail-pull',
};
const watcher = new GmailWatcher(config, channelLookup);
await watcher.connect();
expect(watcher['subscriptionPath']).toBe('projects/test-project/subscriptions/gmail-pull');
});
```
### Disable Push
```typescript
it('should skip watch when disable_push is true', async () => {
const config = {
...baseConfig,
pubsub_topic: 'gmail-push',
disable_push: true,
};
const watcher = new GmailWatcher(config, channelLookup);
await watcher.connect();
const mockWatch = vi.mocked(google.gmail().users.watch);
expect(mockWatch).not.toHaveBeenCalled();
});
```
### Error Hints
```typescript
it('should return topic hint for Invalid topicName error', () => {
const watcher = new GmailWatcher(baseConfig, channelLookup);
const hint = watcher['buildWatchErrorHint']('Invalid topicName');
expect(hint).toContain('Set automation.gmail.pubsub_topic');
expect(hint).toContain('projects/test-project/topics/gmail-push');
});
```
### Doctor Modes
```typescript
it('should detect hybrid mode', async () => {
const config = {
...baseConfig,
automation: {
gmail: {
enabled: true,
pubsub_topic: 'gmail-push',
pubsub_subscription_id: 'gmail-pull',
output: { channel: 'telegram', peer: '123' },
},
},
};
const result = await checkGmail({ config });
expect(result.status).toBe('pass');
expect(result.detail).toContain('push + pull + poll-fallback');
});
```
## Deployment Pattern Config Examples
### Public Endpoint (Push Only)
```yaml
automation:
gmail:
enabled: true
credentials_file: ~/.config/flynn/gmail-credentials.json
pubsub_topic: projects/my-project/topics/gmail-push
output:
channel: telegram
peer: "123456789"
```
### Tailscale Funnel (Push Only)
```yaml
server:
tailscale:
serve: true
funnel: true
automation:
gmail:
enabled: true
credentials_file: ~/.config/flynn/gmail-credentials.json
pubsub_topic: gmail-push # Shorthand OK
output:
channel: telegram
peer: "123456789"
```
### Private Network (Pull Only)
```yaml
automation:
gmail:
enabled: true
credentials_file: ~/.config/flynn/gmail-credentials.json
pubsub_subscription_id: projects/my-project/subscriptions/gmail-pull
pubsub_pull_interval: "60s"
output:
channel: telegram
peer: "123456789"
```
### Hybrid (Recommended)
```yaml
automation:
gmail:
enabled: true
credentials_file: ~/.config/flynn/gmail-credentials.json
pubsub_topic: gmail-push
pubsub_subscription_id: gmail-pull
pubsub_pull_interval: "60s"
poll_interval: "300s"
output:
channel: telegram
peer: "123456789"
```
## GCP Setup Checklist
- [ ] Create project: `gcloud projects create my-flynn-project`
- [ ] Enable APIs: `gcloud services enable gmail.googleapis.com pubsub.googleapis.com`
- [ ] Create topic: `gcloud pubsub topics create gmail-push`
- [ ] Grant IAM: `gcloud pubsub topics add-iam-policy-binding ... --member=serviceAccount:gmail-api-push@system.gserviceaccount.com --role=roles/pubsub.publisher`
- [ ] Create OAuth credentials (Desktop app) via Cloud Console
- [ ] Download credentials JSON to `~/.config/flynn/gmail-credentials.json`
- [ ] Create push subscription (if using push): `gcloud pubsub subscriptions create gmail-push-sub --topic=gmail-push --push-endpoint=https://flynn.example.com/gmail/push`
- [ ] Create pull subscription (if using pull): `gcloud pubsub subscriptions create gmail-pull-sub --topic=gmail-push`
- [ ] Authenticate: `flynn gmail-auth`
- [ ] Verify: `flynn doctor`
## Debugging Commands
```bash
# Check if topic exists
gcloud pubsub topics list --filter="name:gmail-push"
# Check subscriptions
gcloud pubsub subscriptions list --filter="topic:gmail-push"
# Test pull subscription
gcloud pubsub subscriptions pull gmail-pull-sub --limit=5
# Check IAM
gcloud pubsub topics get-iam-policy projects/my-project/topics/gmail-push
# Verify Flynn config
flynn doctor
# Check Flynn logs
journalctl -u flynn -f
```
## Error Message Mapping
| Error | Hint |
|-------|------|
| `Invalid topicName` | Set `pubsub_topic` to `projects/<project>/topics/<topic>`, ensure topic exists, check subscription configuration |
| `Permission denied` / `Forbidden` | Grant Gmail API publish permission: `gcloud pubsub topics add-iam-policy-binding ...` |
| `Not found` / `404` | Create topic: `gcloud pubsub topics create gmail-push` |
| Other | Generic hint about pull subscription fallback |
## Dependencies
```json
{
"dependencies": {
"@google-cloud/pubsub": "^4.0.0"
}
}
```
## Files Modified Summary
| File | Lines Changed | New Code | Description |
|------|---------------|----------|-------------|
| `src/config/schema.ts` | +8 | 4 fields | Config schema additions |
| `src/automation/gmail.ts` | +120 | 3 methods | Pull support, error hints |
| `src/cli/doctor.ts` | +40 | Mode detection | Enhanced validation |
| `package.json` | +1 | Dependency | `@google-cloud/pubsub` |
| `README.md` | +150 | Full rewrite | Deployment patterns, setup |
| `config/default.yaml` | +15 | Comments | Example configs |
## Implementation Order
1. ✅ Config schema fields
2.`buildWatchErrorHint()` method (standalone)
3.`setupPullSubscription()` method (requires new dependency)
4.`pullSubscriptionMessages()` method
5. ✅ Update `connect()` to call pull setup and check `disable_push`
6. ✅ Update `disconnect()` to clear pull timer
7. ✅ Enhance `checkGmail` doctor check
8. ✅ Write unit tests
9. ✅ Update documentation
10. ✅ Manual integration testing