# Email System

The email system covers **outbound campaigns** (SMTP) and **inbound business mail** (IMAP), unified under CRM linkage and tracking.

## Goals

- Send campaign and operational email from Anwal-owned domains
- Sync inbound replies into the Business Inbox
- Associate messages with companies, people, and leads
- Track opens and clicks without exposing internal IDs
- Trigger n8n workflows on meaningful email events

## Outbound architecture

```mermaid
sequenceDiagram
  participant App as Growth Platform
  participant Queue as Send Queue
  participant SMTP as SMTP Server
  participant Recipient as Recipient

  App->>Queue: Enqueue campaign_sends
  Queue->>SMTP: Send MIME message
  SMTP->>Recipient: Deliver
  App->>App: Log messageId and status
```

### SMTP configuration

All SMTP settings come from environment variables — see [10-environment-variables.md](./10-environment-variables.md):

- `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASS`, `SMTP_FROM`
- `EMAIL_SEND_BATCH_SIZE` (default `20`)
- `EMAIL_SEND_DELAY_MS` (default `1000`)
- `EMAIL_TRACKING_ENABLED` (default `true`)

### Message composition

- MIME multipart: `text/plain` + `text/html`
- Embedded tracking pixel URL for opens (Phase 7)
- Wrapped links for click tracking (Phase 7)
- `List-Unsubscribe` header when applicable
- `Message-ID` stored for thread matching with IMAP

### Sending modes

| Mode | Use case |
|------|----------|
| Campaign batch | Many recipients from segment |
| Single send | BD one-off from lead/company record |
| System | Password reset, notifications (if needed) |

## Task 010 implementation (completed)

### Services

- `src/server/services/email-transport.service.ts`
  - `createTransporter()`
  - `verifySmtpConnection()`
  - `sendEmail()`
- `src/server/services/email-render.service.ts`
  - Builds personalization payload from campaign + recipient
  - Renders subject/html/text using `src/lib/template.ts`
  - Injects open pixel and tracked links when tracking is enabled
- `src/server/services/email-sending.service.ts`
  - `sendCampaignRecipient()`
  - `sendCampaignBatch()` (synchronous, sequential, non-blocking per failure)
  - `sendTestEmail()`

### Tracking implementation

- Open tracking route: `GET /api/email-tracking/open/:trackingId`
  - Returns transparent `1x1` pixel
  - Marks `EmailMessage`/`CampaignRecipient` as opened when applicable
- Click tracking route: `GET /api/email-tracking/click/:trackingId?url=...`
  - Validates redirect target (`http/https` only)
  - Marks `EmailMessage`/`CampaignRecipient` as clicked when applicable
  - Redirects to the original URL

### Test utilities

- SMTP verify API: `GET /api/email/verify-smtp`
- Test send API: `POST /api/email/test-send`
- Protected UI page: `/email-test`

### Current limitation

- Campaign sending currently runs synchronously from API calls in batches.
- Queue/worker-based background sending remains a future enhancement.

### Rate limiting and deliverability

- Throttle sends per minute (configurable; Redis optional)
- Retry transient SMTP failures with backoff
- Record bounces and mark contact points invalid

## Inbound architecture (IMAP)

- Periodic or manual sync job pulls new messages
- **Mailbox source:** saved `Mailbox` records (Settings) are preferred; `.env` `IMAP_*` is only used when `source=env` is passed explicitly
- **Cron** (`POST /api/cron/sync-mailboxes`) syncs **all** active mailboxes with complete IMAP credentials
- **Inbox UI** sync respects mailbox selector: one mailbox or all (`POST /api/inbox/sync` with `mailboxId` or `all: true`)
- Dedupe by `Message-ID` and folder UID
- Match threads: `In-Reply-To`, `References`, subject normalization (scoped by `mailboxId` when set)
- Link to existing `campaign_sends` when reply detected
- `EmailThread` stores `mailboxId`, `mailboxEmail`, `mailboxName` for multi-mailbox filtering

## Task 011 implementation (Business Inbox foundation)

### Services

- `src/server/services/imap.service.ts`
  - `createImapClient()`
  - `verifyImapConnection()`
  - `fetchRecentEmails()`
  - `fetchEmailByUid()`
- `src/server/services/inbox.service.ts`
  - `syncInbox()`
  - `linkEmailToExistingRecords()`
  - `createEmailReplyFromParsed()`
  - `listInbox()`
  - `getInboxEmail()`
  - `updateInboxEmailStatus()`

### Linking strategy

- Match existing `EmailMessage` by `providerMessageId`, `inReplyTo`, `references`, or `trackingId` hints.
- Match CRM contacts by sender email (`Person.email`, `ContactPoint.normalizedValue/contactValue`).
- Resolve company via matched person/contact/campaign recipient.
- Create `EmailReply` and avoid duplicates using message metadata (`internetMessageId`, sender, receive timestamp/subject).
- Create `Activity` (`email_replied`) when a reply is linked to campaign/company/person.

### Reply classification (Task 012)

After inbox sync, replies can be classified via the AI provider layer:

- **Service:** `src/server/services/ai-classification.service.ts`
- **APIs:** `/api/ai/classify-email-reply`, `/api/ai/classify-email-replies/bulk`, `/api/ai/classify-preview`, `/api/ai/status`
- **Storage:** `ai_classifications` (`sourceType: email_reply`) linked from `email_replies.classificationId`
- **Status updates:** `classified`, `spam`, or `ignored` based on intent/action
- **Activities:** `ai_classified` when linked to campaign/company/person
- **Lead creation:** explicit via `/api/leads/from-email-reply` or inbox **تحويل إلى Lead** (Task 013)

Default provider is **fallback** (keyword rules, no API key). Set `AI_PROVIDER=openai` and `OPENAI_API_KEY` for LLM classification.

### Current scope note

- No n8n automation on classify yet (Task 019).

## Business Inbox UX

See [04-ui-pages.md](./04-ui-pages.md) — `/inbox`, `/ai/status`.

Actions: sync inbox, classify reply (single/bulk), review AI labels, manual status (new/ignored/spam). Lead creation from reply — Task 013.

## Security

- Credentials only in env
- Sanitize HTML bodies before render (XSS)
- Do not log full email bodies in production info logs

## Related documents

- [06-campaign-system.md](./06-campaign-system.md)
- Email tracking — Task 017 (see roadmap Phase 7)
- [08-ai-classification.md](./08-ai-classification.md)
- [09-n8n-integration.md](./09-n8n-integration.md)

## Implementation tasks

| Area | Task |
|------|------|
| Templates | task-008 |
| Sending engine | task-010 (completed) |
| Inbox UI + basic sync | task-011 (completed) |
| IMAP sync expansion | task-016 |
| Tracking | task-010 baseline (task-017 advanced) |
| Reply classification | task-012 (completed) |

---

## Templates (Task 008 — implemented)

Email templates are reusable message blueprints used later by Campaigns.

### Template fields

- `name`, `subject`
- `htmlContent`, `textContent` (optional)
- `variables` (auto-extracted)
- `language` (`ar` | `en` | `mixed`)
- `status` (`draft` | `active` | `archived`)
- Optional: `description`, `category`

### Variables

Syntax: `{{variable_name}}`

Allowed variables:

- `{{person_name}}`
- `{{company_name}}`
- `{{sector}}`
- `{{city}}`
- `{{recommended_service}}`
- `{{sender_name}}`
- `{{sender_email}}`
- `{{job_title}}`
- `{{service_interest}}`

Rules:

- Variables are extracted from **subject + htmlContent + textContent**.
- Invalid variables are rejected at create/update time.
- Missing variables render as empty string during preview/render.

Implementation helpers: `src/lib/template.ts` (`extractTemplateVariables`, `renderTemplate`, `validateTemplateVariables`).
