---
title: "Email AI-CRM developer guide"
description: "Local multi-mailbox env, migrations, cron poll, CRM SMTP smoke test, and production cron"
locale: "en"
---
# Email AI-CRM developer guide

Set up the Email AI-CRM module on a **local or self-hosted** Ring clone: configure channel secrets (or legacy IMAP_*), apply JSONB migrations when using Postgres, then verify ingest and draft send from the admin CRM UI (`/admin/crm/*`).

## 1. Local environment

Prefer **`ring-config.emailCrm.channels`** + channel secrets. Copy the **EMAIL CRM** block from `env.local.template`:

```bash
# .env.local — multi-mailbox (recommended)
CRM_CHANNEL_PRIMARY_PASSWORD=
# CRM_CHANNEL_PRIMARY_SMTP_PASSWORD=

ANTHROPIC_API_KEY=

CRON_SECRET=generate-a-long-random-string
EMAIL_AUTO_SEND_ENABLED=false
WEBHOOK_EMAIL_SECRET=              # optional: webhook ingest instead of IMAP
EMAIL_CRM_PERSISTENCE=memory       # quick start without DB tables

# Auth OTP stays separate (lib/mailer.ts) — do not reuse for CRM replies:
# EMAIL_MODE=ethereal
# or SMTP_HOST / SMTP_USER / SMTP_PASSWORD for noreply@

# Legacy single-mailbox fallback (only when emailCrm.channels empty):
# IMAP_HOST=mail.example.com
# IMAP_PORT=993
# IMAP_TLS=true
# IMAP_USER=info@example.com
# IMAP_PASSWORD=
# SMTP_HOST=mail.example.com
# SMTP_PORT=587
# SMTP_USER=info@example.com
# SMTP_PASSWORD=
```

Remove `EMAIL_CRM_PERSISTENCE=memory` and apply migrations below so threads and drafts persist across restarts.

Auth login mail uses `SMTP_*` / `lib/mailer.ts`. CRM draft send uses `EmailSenderService` + `CRM_CHANNEL_*`. See [Ring Mailer](/docs/features/ring-mailer.md).

## 2. Database migrations

When `DATABASE_URL` points at your dev Postgres:

```bash
psql "$DATABASE_URL" -f data/migrations/009_email_crm_jsonb.sql
psql "$DATABASE_URL" -f data/migrations/010_email_crm_tasks_jsonb.sql
psql "$DATABASE_URL" -c "\dt email_*"
```

`001_email_crm_schema.sql` is **deprecated**. Always apply **009 + 010** only.

## 3. Trigger ingest (local)

**Option A — cron poll** (matches production):

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

**Option B — webhook** (no IMAP required for the test):

```bash
BODY='{"messageId":"<test@local>","from":"you@example.com","to":"info@example.com","subject":"Hi","bodyText":"Hello"}'
SIG=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_EMAIL_SECRET" | awk '{print $2}')

curl -X POST "http://localhost:3000/api/webhooks/email/inbound" \
  -H "Content-Type: application/json" \
  -H "X-Email-Webhook-Signature: $SIG" \
  -d "$BODY"
```

## 4. CRM SMTP smoke test

1. Ensure channel passwords resolve (`GET /api/admin/email/channels` as admin → `hasImapPassword` / `hasSmtpPassword`).
2. Send a test message to the channel inbox (or use the webhook).
3. Run cron `poll`.
4. Open **Admin → CRM Inbox** (`/admin/crm/inbox`) — optional channel filter.
5. Open **CRM Drafts** — approve and send.
6. Confirm reply in the sender mailbox and an outbound row in `email_messages`.

Only then set `EMAIL_AUTO_SEND_ENABLED=true`.

## 5. Admin UI checklist

| Page | URL | Expect |
|------|-----|--------|
| Inbox | `/{locale}/admin/crm/inbox` | Live threads; channel filter; channel status chips |
| Thread | `/{locale}/admin/crm/inbox/` | Message timeline |
| Drafts | `/{locale}/admin/crm/drafts` | Approve / reject / send |
| Contacts | `/{locale}/admin/crm/contacts` | CRM from processor |
| Analytics | `/{locale}/admin/crm/analytics` | Cost from `email_api_usage` |
| Tasks | `/{locale}/admin/crm/tasks` | Auto-created follow-ups |
| Orders | `/{locale}/admin/crm/orders` | Project orders desk (Owner Project Lab) |

All CRM pages use `CrmAdminShell` (horizontal tabs; no right rail). APIs remain `/api/admin/email/*`.

## 6. Production cron

Schedule the same poll endpoint on your deployed `BASE_URL` every 1–5 minutes:

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

Optional analytics snapshot: `GET $BASE_URL/api/cron/email-analytics` with the same Bearer token.

Ops: `data/migrations/EMAIL-CRM-OPS.md`.

## Troubleshooting

| Symptom | Check |
|---------|--------|
| Empty inbox | Migrations applied (or `EMAIL_CRM_PERSISTENCE=memory`)? Cron `poll` returning `processed > 0`? |
| Channel status red | `CRM_CHANNEL_PRIMARY_PASSWORD` set? `ring-config.emailCrm.enabled` true? |
| No AI draft | `ANTHROPIC_API_KEY` set? Security pipeline blocked? Check server logs. |
| Duplicate processing | Should not occur — `email_messages` Message-ID dedup |
| Draft send fails | Channel SMTP password / host (`EmailSenderService`), not Auth `SMTP_*` |
| Auto-send silent | `EMAIL_AUTO_SEND_ENABLED=true` and confidence rules met |
| 401 on cron | `CRON_SECRET` matches `Authorization: Bearer` header |
| Jsonb / circular require | Types in `features/email-crm/types/*`; no CJS `require()` of Jsonb repos |

## Related documentation

  
- [features/email-ai-crm](/docs/features/email-ai-crm.md) — Prerequisite: feature overview and Auth vs CRM SMTP rules.

  
- [api/email-ai-crm](/docs/api/email-ai-crm.md) — Deep-dive: channels route and cron action contracts.

  
- [architecture/email-ai-crm](/docs/architecture/email-ai-crm.md) — Deep-dive: EmailProcessor and persistence layout.

  
- [features/ring-mailer](/docs/features/ring-mailer.md) — See-also: Auth SMTP and cleanup-email-tokens.

  
- [getting-started/migrations](/docs/getting-started/migrations.md) — Same-workflow: kingdom migration order and run-migration.sh.
