---
title: "Email AI-CRM"
description: "Multi-mailbox AI inbox — IMAP via ring-config.emailCrm.channels, JSONB persistence, per-channel SMTP replies, admin CRM under /admin/crm"
locale: "en"
---
# Email AI-CRM

> **Info**
> Use **Founder** / **Developer** tabs in the docs sidebar to filter this page. Shared sections below apply to both audiences.

Ring ships an **AI-assisted email CRM** for public contact mailboxes (for example `info@ringdom.org`). Inbound mail is classified, threaded, and answered with Claude drafts; platform admins review and send from **`/admin/crm/*`**. Channels live in **`ring-config.json` → `emailCrm.channels[]`**; secrets use **`CRM_CHANNEL__PASSWORD`**. CRM replies use **per-channel SMTP** via `EmailSenderService` — **not** Auth OTP transport (`lib/mailer.ts` / `SMTP_*`).

  
- **[Architecture deep dive](/docs/architecture/email-ai-crm.md)** — EmailProcessor, multi-mailbox IMAP, JSONB collections, Auth vs CRM SMTP planes.

  
- **[API reference](/docs/api/email-ai-crm.md)** — Admin routes (including channels), cron actions, inbound webhook.

  
- **[Developer setup guide](/docs/examples/email-ai-crm.md)** — Local env, migrations, channel secrets, production cron.

  
- **[Ring Mailer (auth SMTP)](/docs/features/ring-mailer.md)** — OTP / magic-link mail — separate from CRM channel SMTP.

  
- **[Owner Project Lab](/docs/features/owner-project-lab.md)** — Project orders + shared order_lab chat under the same CRM shell.

## Capability overview

| Layer | Responsibility |
|-------|----------------|
| **Channels** | `ring-config.emailCrm.channels[]` + `loadCrmChannels()` — IMAP/SMTP host, user, mailbox per channel |
| **Ingestion** | `POST /api/cron/email-processor` (`action: poll`) or `POST /api/webhooks/email/inbound` |
| **Processor** | `features/email-crm/pipeline/email-processor.ts` — parse, security, AI, CRM, drafts (constructed per cron action) |
| **Persistence** | JSONB collections via `jsonb-collection.ts` → `db()`; types in `features/email-crm/types/*` |
| **Outbound** | `features/email-crm/pipeline/smtp/email-sender.ts` — per-channel CRM SMTP (`In-Reply-To` / `References`) |
| **Admin UI** | `/admin/crm/inbox` (channel filter), drafts, contacts, analytics, tasks — plus `/admin/crm/orders` |
| **Admin API** | `/api/admin/email/*` including `GET /api/admin/email/channels` |

```mermaid
sequenceDiagram
    participant M as Channel mailbox
    participant C as Cron poll
    participant P as EmailProcessor
    participant DB as db() JSONB
    participant A as Admin CRM UI
    participant S as EmailSenderService

    M->>C: UNSEEN per channel
    C->>P: pollInboundBatch
    P->>P: Security + intent + sentiment
    P->>DB: messages, threads, contacts, drafts, tasks
    A->>DB: GET threads?sourceChannel=
    A->>A: Review draft, approve
    A->>S: Send via channel SMTP
    S-->>M: Re: reply
```

### For founders

## Why this matters for your clone

Public contact inboxes (`info@`, `support@`, `hello@`) are where trust is won or lost. Email AI-CRM gives operators a single admin surface to triage inbound mail, filter by mailbox channel, review AI drafts before send, and track follow-up tasks — without hiring a full support desk on day one.

  
  
  

### Business rules

- **Message-ID dedup** — the same RFC `Message-ID` is never processed twice.
- **Auto-send off by default** — set `EMAIL_AUTO_SEND_ENABLED=true` only after a CRM SMTP smoke test passes.
- **Cron poll in serverless** — schedule `poll` every 1–5 minutes; `EMAIL_PROCESSOR_AUTOSTART` is for long-lived Node workers only.
- **Admin-only** — all `/api/admin/email/*` routes require a platform admin session.
- **Auth mail ≠ CRM mail** — OTP/magic links use `SMTP_*` / `lib/mailer.ts`; CRM replies use channel SMTP (`CRM_CHANNEL_*`).

### Admin pages (CRM shell)

| Route | Purpose |
|-------|---------|
| `/admin/crm/inbox` | Thread list with **sourceChannel** filter + read-only channel status |
| `/admin/crm/inbox/[id]` | Thread detail + messages |
| `/admin/crm/drafts` | Pending AI drafts — approve / reject / send |
| `/admin/crm/contacts` | CRM contacts registry |
| `/admin/crm/analytics` | Volume, intent/sentiment, API cost |
| `/admin/crm/tasks` | Follow-ups and escalations |
| `/admin/crm/orders` | Project / custom orders desk |

Navigation: **Admin → CRM** horizontal tabs. Right rail is hidden (`showRightRail={false}`).

Legacy `/admin/email-*` page URLs are a **clean break** — use `/admin/crm/*`. APIs and cron ids are unchanged.

### Operator setup

Enable `emailCrm` in `ring-config.json` with at least one channel (host, user, mailbox). Set `CRM_CHANNEL_PRIMARY_PASSWORD` (and optional `CRM_CHANNEL_PRIMARY_SMTP_PASSWORD`) in secrets. Keep Auth `SMTP_*` pointed at your login mailbox (often a different host).

Apply JSONB migrations (`009_email_crm_jsonb.sql` + `010_email_crm_tasks_jsonb.sql`). For a UI-only test without tables, set `EMAIL_CRM_PERSISTENCE=memory`.

Schedule `POST /api/cron/email-processor` with `{"action":"poll"}` and `Authorization: Bearer $CRON_SECRET` every 1–5 minutes.

Open **Admin → CRM Inbox**, confirm channel status chips load, approve a draft, and verify the reply left via the **CRM** host (not Auth noreply).

### For developers

## Auth SMTP vs CRM SMTP (do not merge)

| Plane | Config | Code | Use |
|-------|--------|------|-----|
| **Auth** | `SMTP_*` / `EMAIL_MODE` | `lib/mailer.ts` → `sendMail()` | OTP, magic link, password reset |
| **CRM** | `emailCrm.channels[].smtp` + `CRM_CHANNEL__PASSWORD` | `EmailSenderService.sendReply()` | Approved inbox replies |

ring-platform.org prod Auth host is documented in `data/migrations/RING-MAILER-OPS.md` (`mail.subiworx.com`). CRM primary channel uses `mail.ringdom.org` / `info@ringdom.org` in `ring-config.json`.

Legacy fallback: when `emailCrm.channels` is empty, `IMAP_*` / `SMTP_*` still feed a single mailbox via `loadCrmChannels()`.

### Processing pipeline

1. **Parse** — headers, body, attachments, thread keys.
2. **Security** — four-layer prompt-injection defense (`features/email-crm/pipeline/security/`).
3. **AI** — Haiku intent + sentiment; Sonnet response generation.
4. **CRM** — contact/thread upsert with `sourceChannel`; optional auto-tasks.
5. **Draft** — pending queue unless auto-send criteria are met.
6. **Notify** — `EmailNotificationService` on draft, task, security block, urgent mail.

### Code layout

| Path | Role |
|------|------|
| `features/email-crm/pipeline/imap/config.ts` | `loadCrmChannels()`, `validateCrmChannels()` |
| `features/email-crm/pipeline/email-processor.ts` | Orchestrator; polls each enabled channel |
| `features/email-crm/pipeline/smtp/email-sender.ts` | Per-channel CRM SMTP (not `lib/mailer`) |
| `features/email-crm/types/{contact,task,draft}.ts` | Pure types — breaks Jsonb circular require |
| `features/email-crm/repositories/jsonb-*-repository.ts` | `import type` from types; static ESM only |
| `features/email-crm/pipeline/crm/*`, `features/email-crm/pipeline/drafts/*` | `import { Jsonb*Repository }` (no CJS `require`) |
| `features/email-crm/lib/jsonb-collection.ts` | `readDoc` / `upsertDoc` / `queryDocs` → `db()` |
| `app/api/cron/email-processor/route.ts` | Cron: `getEmailProcessor()` **per action** |
| `app/api/admin/email/channels/route.ts` | Read-only channel status (no secrets) |
| `app/[locale]/admin/crm/inbox/page.tsx` | `sourceChannel` query filter |

### Persistence — Jsonb circular fix (shipped)

Types live in `features/email-crm/types/*` with **no runtime imports**. Repositories use `import type`; services use static ESM `import { Jsonb*Repository }`. Do **not** reintroduce CJS `require()` of Jsonb repos (that cycle broke Next server bundles).

### Active JSONB collections (migrations 009 + 010)

| Collection | Document id | Indexed fields |
|------------|-------------|----------------|
| `email_threads` | RFC thread root / Message-ID | `status`, `fromEmail`, `lastMessageAt`, `sourceChannel` |
| `email_contacts` | `contact_<sha256(email)>` | `email`, `type` |
| `email_messages` | RFC Message-ID | `threadId` |
| `email_drafts` | `draft_` | `threadId`, `status` |
| `email_tasks` | `task_` | `threadId`, `status`, `dueDate` |
| `email_api_usage` | `req__` | `timestamp`, `operation`, `emailId` |

### Cron actions

`POST /api/cron/email-processor` — `poll` (default), `status`, `mark-overdue-tasks`, optional `start` / `stop` when `EMAIL_PROCESSOR_ALLOW_HTTP_START=true`. Fail-closed on `CRON_SECRET`. Processor instance is created **inside** each action branch (not always at module top).

## Schema-only tables (not wired in app)

The legacy migration `001_email_crm_schema.sql` defines **`email_analytics`**, **`email_knowledge_base`**, and **`email_security_events`**. **No TypeScript module reads or writes these tables today.** Production uses JSONB migrations `009` + `010` only.

## Local configuration

Prefer **multi-mailbox** secrets (canonical comments in `env.local.template`):

{`# ring-config.emailCrm.channels[].id = "primary"
CRM_CHANNEL_PRIMARY_PASSWORD=
# CRM_CHANNEL_PRIMARY_SMTP_PASSWORD=   # optional override

ANTHROPIC_API_KEY=
CRON_SECRET=generate-a-long-random-string
EMAIL_AUTO_SEND_ENABLED=false
EMAIL_CRM_PERSISTENCE=memory
WEBHOOK_EMAIL_SECRET=

# Legacy single-mailbox fallback (only when emailCrm.channels is empty):
# IMAP_HOST=mail.example.com
# IMAP_USER=info@example.com
# IMAP_PASSWORD=
# SMTP_HOST=mail.example.com
# SMTP_USER=info@example.com
# SMTP_PASSWORD=`}

| Variable | Purpose |
|----------|---------|
| `CRM_CHANNEL__PASSWORD` | IMAP (and SMTP if no SMTP override) for channel `id` |
| `CRM_CHANNEL__SMTP_PASSWORD` | Optional dedicated SMTP password |
| `ANTHROPIC_API_KEY` | Intent, sentiment, draft generation |
| `CRON_SECRET` | Bearer for `/api/cron/email-processor` |
| `EMAIL_AUTO_SEND_ENABLED` | Keep `false` until CRM SMTP smoke test passes |
| `WEBHOOK_EMAIL_SECRET` | HMAC for inbound webhook (optional) |
| `EMAIL_CRM_PERSISTENCE` | `memory` for UI-only tests; omit for Postgres JSONB |
| `SMTP_*` | **Auth** Ring Mailer only — not CRM replies when channels are configured |

## Production deployment

Store channel secrets in the cluster secret manager, apply migrations `009` + `010`, and schedule:

{`curl -sS -X POST "$BASE_URL/api/cron/email-processor" \\
  -H "Authorization: Bearer ${CRON_SECRET}" \\
  -H "Content-Type: application/json" \\
  -d '{"action":"poll"}'`}

Ops notes: `data/migrations/EMAIL-CRM-OPS.md`. Auth SMTP split: `data/migrations/RING-MAILER-OPS.md`.

## Related documentation

  
- [architecture/email-ai-crm](/docs/architecture/email-ai-crm.md) — Deep-dive: ingest diagram, jsonb-collection → db(), Auth vs CRM SMTP planes.

  
- [api/email-ai-crm](/docs/api/email-ai-crm.md) — Next-step: channels status route, sourceChannel query, cron and webhook contracts.

  
- [examples/email-ai-crm](/docs/examples/email-ai-crm.md) — Same-workflow: local smoke test from env through draft send.

  
- [features/ring-mailer](/docs/features/ring-mailer.md) — Depends-on: Auth OTP/magic SMTP stays on lib/mailer — do not reuse for CRM replies.

  
- [features/owner-project-lab](/docs/features/owner-project-lab.md) — See-also: project orders desk under the same CRM shell.

  
- [features/authentication](/docs/features/authentication.md) — See-also: vitals onboarding and login mail (separate from CRM).
