Files
bellsystems-cp/.claude/crm-step-10.md

3.5 KiB
Raw Blame History

CRM Step 10 — Integration: IMAP/SMTP Email

Context

Read .claude/crm-build-plan.md for full context and IMPORTANT NOTES. Steps 0109 must be complete.

Task

Integrate the company email mailbox so that:

  1. Emails from/to a customer's email addresses appear in their Comms tab
  2. New emails can be composed and sent from the console
  3. A background sync runs periodically to pull new emails

Backend changes

1. Add email settings to backend/config.py

imap_host: str = ""
imap_port: int = 993
imap_username: str = ""
imap_password: str = ""
imap_use_ssl: bool = True
smtp_host: str = ""
smtp_port: int = 587
smtp_username: str = ""
smtp_password: str = ""
smtp_use_tls: bool = True
email_sync_interval_minutes: int = 15

2. Create backend/crm/email_sync.py

Using standard library imaplib and email (no new deps).

async def sync_emails():
    """
    Connect to IMAP. Search UNSEEN or since last sync date.
    For each email:
      - Parse from/to/subject/body (text/plain preferred, fallback to stripped HTML)
      - Check if from-address or to-address matches any customer contact (search crm_customers)
      - If match found: create crm_comms_log entry with type=email, ext_message_id=message-id header
      - Skip if ext_message_id already exists in crm_comms_log (dedup)
    Store last sync time in a simple SQLite table crm_sync_state:
      CREATE TABLE IF NOT EXISTS crm_sync_state (key TEXT PRIMARY KEY, value TEXT)
    """

async def send_email(to: str, subject: str, body: str, cc: List[str] = []) -> str:
    """
    Send email via SMTP. Returns message-id.
    After sending, create a crm_comms_log entry: type=email, direction=outbound.
    """

3. Add SQLite table to backend/mqtt/database.py

CREATE TABLE IF NOT EXISTS crm_sync_state (
  key   TEXT PRIMARY KEY,
  value TEXT
);

4. Add email endpoints to backend/crm/router.py

POST /api/crm/email/send Body: { customer_id, to, subject, body, cc (optional) } → calls send_email(...), links to customer in comms_log

POST /api/crm/email/sync → manually trigger sync_emails() (for testing / on-demand) → returns count of new emails found

5. Add background sync to backend/main.py

In the startup event, add a periodic task:

async def email_sync_loop():
    while True:
        await asyncio.sleep(settings.email_sync_interval_minutes * 60)
        try:
            from crm.email_sync import sync_emails
            await sync_emails()
        except Exception as e:
            print(f"[EMAIL SYNC] Error: {e}")

asyncio.create_task(email_sync_loop())

Only start if settings.imap_host is set (non-empty).

Frontend changes

Update Comms tab in CustomerDetail.jsx

  • Email entries show: from/to, subject, body (truncated with expand)
  • "Compose Email" button → modal with: to (pre-filled from customer primary email), subject, body (textarea), CC
  • On send: POST /api/crm/email/send, add new entry to comms list

Update InboxPage.jsx

  • Add "Sync Now" button → POST /api/crm/email/sync, show result count toast

Notes

  • imaplib is synchronous — wrap in asyncio.run_in_executor(None, sync_fn) for the async context
  • For HTML emails: strip tags with a simple regex or html.parser — no need for an HTML renderer
  • Email body matching: compare email From/To headers against ALL customer contacts where type=email
  • Don't sync attachments yet — just text content. Attachment handling can be a future step.
  • If imap_host is empty string, the sync loop doesn't start and the send endpoint returns 503