# 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
