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

103 lines
3.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`
```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