Skip to content

Correspondence (mail-intake) module

Parked — not currently shipped

The Correspondence staff UI (apps/web/src/app/dashboard/correspondence/) has been removed, and its API ops routes + worker handlers are parked under archive/ (they were already quarantined out of the build). This note is retained as the reference for reviving the module onto the greenfield tenant spine; the source paths below now live under archive/.

The Correspondence product is a deep integration of the standalone aima-mvp app (Next.js 16 + Supabase) into the monorepo. The Next.js/Supabase/Vercel-cron shell was dropped; the business logic was preserved and re-homed onto the monorepo stack (Fastify + pg-boss + Prisma/Postgres + MinIO/S3 + @breezycorp/* shared packages).

Pipeline

Gmail INBOX (has:attachment)
  └─ worker: correspondence.gmail-poll (cron)
       sync → GmailIntakeQueue → claim one → download attachment
       → IntakeService.createDocumentFromAttachment (store PDF in MinIO, create CorrespondenceDocument)
       → archive Gmail message → enqueue correspondence.run-ocr
  └─ worker: correspondence.run-ocr
       OcrClientsService.runForDocument:
         Vision/pdf-parse OCR → Claude client-row extraction → per-page PDF split
         → Odoo D3/D4 client + contact match → persist ocrClientsItems (JSON)
  └─ worker: correspondence.dispatch-poll (cron)
       find completed docs past grace window → honour dispatch_mode
       (item_ready | document_complete) → DispatchService sends one Gmail email
       per clean item (split PDF attached) → stamp dispatched_at

Where things live

  • packages/correspondence (@breezycorp/correspondence)
    • logic/ — pure: classification rules (+ Claude fallback), entity regex, LLM entity extraction, Odoo match helpers, mail id/subject parsing, ocr-clients review rules.
    • ocr/ — Vision + pdf-parse OCR orchestration, barcode/AI PDF splitting, Claude client-row extraction, the pure runOcrClientsPipelineOnPdfBuffer.
    • integrations/ — Gmail client + intake helpers, Odoo JSON-RPC + matching, dispatch email builders.
    • ports/StoragePort + S3StoragePort (MinIO; keys under correspondence/<drid>/… in the shared S3_BUCKET).
    • services/ — Prisma-backed IntakeService, OcrClientsService, GmailQueueService, DispatchService, SettingsService.
  • apps/worker/src/handlers/correspondence/gmail-poll, run-ocr-clients, dispatch-poll; schedules registered in apps/worker/src/schedules/index.ts (env crons CORRESPONDENCE_GMAIL_POLL_CRON / CORRESPONDENCE_DISPATCH_POLL_CRON).
  • apps/api/src/routes/ops/correspondence/ — staff list/detail/run-ocr/ source-url/settings, gated by REVIEW_CORRESPONDENCE / MANAGE_CORRESPONDENCE_CONFIG.
  • apps/web/src/app/dashboard/correspondence/ — staff dashboard (list + run-OCR). A full review screen (PDF viewer + per-item manual match/defer/dispatch) and a settings UI are the next UI increment.
  • packages/db — models CorrespondenceDocument, CorrespondenceEntity, GmailIntakeQueue, CorrespondenceSetting; migration …_add_correspondence_module.

Reused vs. kept-raw

  • Text Claude calls go through @breezycorp/ai-claude; image/vision Claude calls (PDF-boundary detection) keep the raw @anthropic-ai/sdk (the shared client is text-only).
  • Vision OCR keeps aima's GCS async-batch path (@google-cloud/vision + @google-cloud/storage) — it doesn't fit the synchronous @breezycorp/documentsVisionOcrAdapter, so it was not forced onto it.
  • Dispatch keeps the Gmail API (thread/reply fidelity) rather than @breezycorp/notifications SMTP.

Config

Reuses S3_* (MinIO), the active AI gateway's key (OPENROUTER_API_KEY by default; ANTHROPIC_API_KEY when AI_GATEWAY=anthropic), and GOOGLE_VISION_CREDENTIALS_JSON. Correspondence-only env (see .env.example): GMAIL_*, ODOO_* (matching is optional — ODOO_MATCH_ENABLED), DISPATCH_TO_EMAIL, DISPATCH_FROM_NAME, DISPATCH_GRACE_SECONDS, ENTITY_EXTRACTION_USE_LLM, and the two cron vars.

Dev verification (no cloud creds)

With OCR_PROVIDER=mock / CLAUDE_PROVIDER=mock: pnpm db:seed creates two demo documents (one REVIEW/completed with two extracted items, one fresh INTAKE). The dashboard at /dashboard/correspondence lists them; "Run OCR" enqueues correspondence.run-ocr. Flip dispatch_mode via PATCH /ops/correspondence/settings; the dispatch poll sends to Mailpit (:8025) when DISPATCH_TO_EMAIL is set.

Notable port decisions

  • The flattened document_entities table was ported as CorrespondenceEntity; the normalized universal/legal/invoice variant was deferred (matches aima's ENTITY_EXTRACTION_USE_LLM=false default).
  • odoo-match-helpers: a stale aima test asserted ambiguous for a lone mid-band candidate; the shipped code returns matched. The port preserves the live behaviour and aligns the test.

Internal use only — BreezyCorp