8.7 KiB
8.7 KiB
Gmail Push Revisit — Quick Reference
New Config Fields
// 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()
// 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()
// src/automation/gmail.ts
private async pullSubscriptionMessages(): Promise<void>
- Uses
@google-cloud/pubsubSDK - Pulls up to
pubsub_max_messages - Calls
handlePushNotification()for each message - Acknowledges messages after processing
buildWatchErrorHint()
// 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()
// 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()
// Add:
if (this.pullSubscriptionTimer) {
clearInterval(this.pullSubscriptionTimer);
this.pullSubscriptionTimer = undefined;
}
Doctor Check Enhancement
// 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
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
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
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
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
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)
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)
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)
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)
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
# 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
{
"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
- ✅ Config schema fields
- ✅
buildWatchErrorHint()method (standalone) - ✅
setupPullSubscription()method (requires new dependency) - ✅
pullSubscriptionMessages()method - ✅ Update
connect()to call pull setup and checkdisable_push - ✅ Update
disconnect()to clear pull timer - ✅ Enhance
checkGmaildoctor check - ✅ Write unit tests
- ✅ Update documentation
- ✅ Manual integration testing