# CRM Step 10 — Integration: IMAP/SMTP Email ## Context Read `.claude/crm-build-plan.md` for full context and IMPORTANT NOTES. Steps 01–09 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` ```python 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). ```python 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` ```sql 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: ```python 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