Milestone 5: Inbox management
Purpose: Admins can create and manage inboxes, connect via IMAP or OAuth (Gmail/Microsoft), store credentials encrypted, and see connection status. No background fetch or ticket creation yet.
Exit state: Inbox CRUD; connect UI (IMAP form + test, Gmail/Microsoft OAuth); credentials encrypted; connection status and reconnect CTA in UI.
Spec reference: §2.2 Inbox Management (all subsections), §5.4 Inbox, §5.21b InboxAlias, §6.3 Inbox connect wizard, §6.10 settings/inboxes, §7.1 Credential storage, §10.1 Inboxes.
Prerequisites: M04 (Onboarding and tenant settings). Subscription/trial and inbox limit (M10) can be stubbed: e.g. allow up to 10 inboxes for any tenant in M05.
5.1 Inbox CRUD
Tasks
-
List inboxes
- tRPC: list inboxes for tenant (exclude deleted_at). Return id, name, email_address, connection_type, last_error, connected_at, archived_at.
- Settings → Inboxes: table or list; each row: name, email, connection type icon, status badge (connected / disconnected / error), actions (Settings, Disconnect, Archive).
- Archived section: collapsed by default; list archived inboxes; optional restore (if plan limit allows).
-
Create inbox
- "Add inbox" → wizard or modal: step 1 choose connection type (IMAP, Gmail, Microsoft); step 2 connection details (see 5.2); step 3 name and optional display name. Create inbox row (connected_at = null until connection succeeds).
- Enforce inbox limit: before create, count active (non-archived, non-deleted) inboxes; if >= plan limit, block with message "Upgrade to add more inboxes" (plan limit from subscription/trial — stub to 10 in M05 if M10 not done).
-
Update inbox
- Settings → Inboxes → [id]: edit name, display_name; connection re-auth or credential update (see 5.2). Save via tRPC.
-
Archive / delete
- Archive: set archived_at; hide from default list; still count toward plan per spec (or document "archived not counted" per product decision).
- Disconnect: clear credentials_encrypted, set connected_at = null, last_error; keep inbox row. Stops sync once worker exists (M06).
- Delete: soft delete (deleted_at) or hard delete with transfer/close open tickets per Section 10.1. Implement at least soft delete and show open ticket count + "Transfer to" or "Close all" before confirm.
Acceptance criteria
| Criterion | Status |
|---|---|
| Admin sees list of inboxes; can add inbox (wizard or flow); can edit and archive/disconnect/delete. | ✅ |
| Inbox limit enforced when creating (stub limit or from M10). | ✅ |
| Delete flow: confirm with ticket transfer or close option per Section 10.1. | ✅ |
5.2 Connect inbox — IMAP/SMTP
Tasks
-
IMAP/SMTP form
- Fields: IMAP host, port, security (TLS/STARTTLS/none), username, password; SMTP host, port, security, username, password. Optional: "Test connection" before save.
-
Test connection
- Server-side: use imapflow (or similar) to connect and optionally fetch capability; use nodemailer to send test email or verify SMTP. On success return success; on failure return error message. Do not store credentials until user explicitly saves.
-
Save and encrypt
- On save: encrypt credentials (JSON or blob: IMAP + SMTP settings) with AES-256 using ENCRYPTION_KEY from env. Store in
inboxes.credentials_encrypted. Set connected_at = now(), clear last_error. Section 7.1.
- On save: encrypt credentials (JSON or blob: IMAP + SMTP settings) with AES-256 using ENCRYPTION_KEY from env. Store in
-
Encryption implementation
packages/email(orpackages/config): encrypt(plaintext) and decrypt(ciphertext) using key from env; use IV and auth tag (e.g. AES-256-GCM). Document key format (e.g. base64 32-byte).
Acceptance criteria
| Criterion | Status |
|---|---|
| User can enter IMAP/SMTP details and click "Test connection"; success or error shown. | ✅ |
| On save, credentials are encrypted and stored; inbox shows as connected. | ✅ |
| Credentials never logged or sent to client; only encrypted blob in DB. | ✅ |
5.3 Connect inbox — Gmail and Microsoft
Tasks
-
OAuth flows
- Gmail: Google OAuth2 with scopes for Gmail read/send (e.g. gmail.readonly, gmail.send, or full gmail.modify). Redirect URI from env; callback stores tokens.
- Microsoft: Microsoft Graph OAuth2 with Mail.Read, Mail.Send (or equivalent). Redirect URI from env; callback stores tokens.
- Callback: exchange code for tokens; encrypt and store in inbox row (same credentials_encrypted or separate token column); set connected_at, clear last_error.
-
Token storage
- Encrypt refresh_token and access_token (and expiry) with same ENCRYPTION_KEY; store in DB. Section 7.1.
-
Connect UI
- In inbox wizard or settings: "Connect with Google" / "Connect with Microsoft" buttons; redirect to provider; return to callback then redirect to inbox settings or onboarding next step.
- If tenant already has inbox and is reconnecting: same flow; update existing inbox row.
-
Reconnect
- When last_error = 'oauth_refresh_failed' or connected_at is null, show "Reconnect" CTA; same OAuth flow; on success update tokens and connected_at.
Acceptance criteria
| Criterion | Status |
|---|---|
| User can connect Gmail inbox via OAuth; tokens stored encrypted; inbox shows connected. | ✅ |
| User can connect Microsoft inbox via OAuth; tokens stored encrypted; inbox shows connected. | ✅ |
| Reconnect flow updates tokens and clears error state. | ✅ |
5.4 Connection status and health
Tasks
-
Status in UI
- Inbox list and inbox detail: show status badge — Connected (green), Disconnected (grey), Error (red). If last_error set, show short message and "Reconnect" or "Fix settings".
- connected_at: display "Connected since
<date>" or "Never connected".
-
Worker updates status (M06)
- In M06, worker will set last_error and clear connected_at on auth failure. For M05, manual disconnect or failed test can set last_error for testing UI.
-
Inbox settings page
- Tab or section: General (name, display_name), Connection (re-auth or edit IMAP), Auto-responses (toggle + body text — optional for M05), Aliases (optional — deferred to M17 §17.4 or post-launch backlog; Section 2.2, 5.21b).
Acceptance criteria
| Criterion | Status |
|---|---|
| Connection status visible in list and in inbox settings; Reconnect CTA when disconnected or error. | ✅ |
| Auto-response toggle and body can be saved (used in M06 when creating ticket); aliases can be deferred. | ✅ |
5.5 Key rotation and documentation
Tasks
- Document key rotation
- Runbook: docs/runbooks/key-rotation.md — steps to rotate ENCRYPTION_KEY (decrypt all credentials/tokens with old key, re-encrypt with new key, deploy new key) and AUTH_SECRET (invalidates all sessions). Section 7.1.
Acceptance criteria
| Criterion | Status |
|---|---|
| Key rotation procedure documented; no code change required for M05 beyond doc. | ✅ |
Milestone 5 sign-off
| Criterion | Status |
|---|---|
| All tasks in 5.1–5.5 complete. | ✅ |
| All acceptance criteria met. | ✅ |
| Admin can create inboxes, connect via IMAP or OAuth, see status, and reconnect; credentials encrypted. | ✅ |
| E2E: Inbox create/connect/list/status (see INDEX — Testing strategy). | ✅ |
| Ready for M06 (Email pipeline and worker). | ✅ |