# 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 ``` - 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 ``` - 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//topics/`, 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