323 lines
8.7 KiB
Markdown
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
|