mono/packages/ui/docs/gmail.md
2026-03-21 20:18:25 +01:00

5.0 KiB

Gmail / IMAP Integration

Developer reference for connecting user mailboxes via IMAP to harvest contacts.


Overview

Users can connect any IMAP mailbox (Gmail, Outlook, etc.) from the Profile → Integrations tab. Credentials are stored encrypted within user_secrets.settings.mailboxes in Supabase — same row-level security as API keys.

Phase 1 (current): Store credentials, test connection.
Phase 2 (future): Harvest sender/recipient contacts from mailbox, import into contact groups.
Phase 3 (future): OAuth2 flow for Gmail (no App Password needed).


Gmail App Password Setup (Required for Phase 1)

Standard Gmail passwords won't work over IMAP if 2FA is enabled. Users must generate an App Password:

  1. Go to myaccount.google.com/security
  2. Enable 2-Step Verification if not already on
  3. Go to App passwords → Select app: Mail, device: Other (custom name)
  4. Copy the generated 16-character password

IMAP settings for Gmail:

  • Host: imap.gmail.com
  • Port: 993
  • TLS: true (IMAPS)
  • Auth: plain (user + App Password)

Credential Storage Schema

Mailboxes are stored in user_secrets.settings.mailboxes as a JSON array:

interface MailboxCredential {
  id: string;           // uuid, generated on save
  label: string;        // user-facing name e.g. "Work Gmail"
  host: string;         // imap.gmail.com
  port: number;         // 993
  tls: boolean;         // always true for Gmail
  user: string;         // email@gmail.com
  password: string;     // App Password (stored as-is, protected by Supabase RLS)
  status?: 'ok' | 'error' | 'pending';
  lastTestedAt?: string; // ISO datetime
  lastError?: string;
}

No plaintext encryption beyond Supabase's column-level storage. The password is never returned from the API — only has_password: boolean and user are exposed.


API Routes

All routes require Authorization: Bearer <jwt> header.

GET /api/contacts/mailboxes

List connected mailboxes for the current user. Password is masked.

Response:

[
  {
    "id": "uuid",
    "label": "Work Gmail",
    "host": "imap.gmail.com",
    "port": 993,
    "tls": true,
    "user": "user@gmail.com",
    "has_password": true,
    "status": "ok",
    "lastTestedAt": "2026-03-06T12:00:00Z"
  }
]

POST /api/contacts/mailboxes

Save or update a mailbox credential.

Body:

{
  "id": "optional-uuid-for-update",
  "label": "Work Gmail",
  "host": "imap.gmail.com",
  "port": 993,
  "tls": true,
  "user": "user@gmail.com",
  "password": "abcd efgh ijkl mnop"
}

Response: Masked mailbox object (same as GET item).

DELETE /api/contacts/mailboxes/:id

Remove a mailbox by ID.

Response: { "ok": true }

POST /api/contacts/mailboxes/:id/test

Test an IMAP connection using saved credentials. Does not modify stored data but updates status and lastTestedAt.

Response:

{ "ok": true }
// or
{ "ok": false, "error": "Invalid credentials" }

Server Implementation

Located in server/src/products/contacts/:

  • imap-handler.ts — business logic using imapflow
  • imap-routes.ts — route definitions (Hono/Zod OpenAPI)
  • index.ts — routes registered in ContactsProduct.initializeRoutes()

imapflow connection pattern

import { ImapFlow } from 'imapflow';

const client = new ImapFlow({
  host: creds.host,
  port: creds.port,
  secure: creds.tls,
  auth: { user: creds.user, pass: creds.password },
  logger: false,
});
await client.connect();
await client.logout();

Environment Variables

No additional env vars required for Phase 1 (credentials are per-user in Supabase).

Future OAuth2 vars (Phase 3):

# Google OAuth2 for Gmail IMAP access
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-client-secret
GOOGLE_REDIRECT_URI=https://your-domain.com/api/auth/google/callback

Security Notes

  • Passwords are stored in user_secrets which has Supabase Row Level Security allowing only the owner to read their own row
  • Server API never returns the password field — only has_password: true
  • Test connection is server-side only; credentials are never sent to the browser after initial save
  • In future Phase 3, App Password flow is replaced by OAuth2 refresh tokens (no password stored at all)

Frontend

Component: src/components/GmailIntegrations.tsx
Client module: src/modules/contacts/client-mailboxes.ts
Profile tab: Profile.tsx/profile/integrations


Roadmap

Phase Description Status
1 Store IMAP credentials, test connection Current
2 Harvest contacts from sent/received emails 🔜 Planned
3 OAuth2 for Gmail (no App Password) 🔜 Planned
4 Scheduled background sync, dedup contacts 🔜 Planned