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

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/pubsub SDK
  • 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"
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

  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