Redesign every system email template

Task 9430 · Queued · Coder · created · updated

## Original request (verbatim) > Migrate the emails system from GMail API to AWS SES. Use all the best practices. The quality of communication is of highest priority. Spare no time or expense. > > [jaah.ai](http://jaah.ai) MX is currently at GMail and will remain so, but we can carve out whatever we need Epic context (BA PRD Task 9352, Architecture Task 9353) — settled: all system mail (send + receive) moves to SES on `mail.jaah.ai` (dev: `mail.dev.jaah.ai`, dev account, sends/receives real mail) with suppression, operator-only delivery log, RFC 8058 unsubscribe, DMARC monitoring, operator-approved template redesign; tenant Gmail (agent send, automation action, receive) is built in this epic behind one plan feature. All system AWS calls use the hand-signed SigV4 seam (`apps/api/app/aws/common.py` `signed_call`), no boto3. SES events and inbound mail are PULLED from SQS by system crons (precedent `apps/api/app/alarm_relay.py`, `apps/api/app/aws/sqs.py`); the only new public path is unsubscribe on the hooks edge. ## Scope of this child C6 Template redesign: rebuild the email layout and text part, rewrite copy for every system email type, a screenshot harness, and the operator mock-up approval gate. Delivers PRD R7 and the content side of AC2. ## Requirements (verbatim from the PRD) ### R7 — Redesigned templates (A3, Q11) Every system email type is redesigned (layout + copy) in the console's brand: renders correctly in Gmail (web, Android, iOS), Outlook (web + desktop), Apple Mail; light and dark mode; 390px phone and desktop; accessible (real text, alt text, ≥4.5:1 contrast, semantic structure, `lang`); text part is a readable equivalent. Copy follows `docs/standards/ui-copy-standards.md`. Operator-edited templates (*Settings > Integrations > Email templates*) render inside the new layout. **Gate:** rendered mock-ups of every email type are approved by the operator (AskUserQuestion) before the redesign merges. Q11: **Match console brand; operator approves mock-ups before build ships.** ## Approach (from the settled architecture) - Rebuild `_layout` (`apps/api/app/email_templates.py:605`) and `_text_part` (`:666`): 600 px max table layout, inline styles, `lang`, `role=presentation`, `color-scheme` + `supported-color-schemes` meta with explicit dark palette, bullet-proof button, preheader, alt text, no images required to read. Keep the S6 footer slot (`unsubscribe_url`) C5 added. - The api owns the email palette (`email_templates.py` tokens, today's `_INK` family), derived once from the console theme's values and named as such in a comment (apps never cross-import). - Copy rewritten per `docs/standards/ui-copy-standards.md`; operator-edited fields still slot in (`email_template_store.active_fields`). - Screenshot harness (Playwright: `apps/api` render → HTML files → `apps/web/e2e`-style runner or a script under `tools/`): every type, light/dark × 390/640 px, Chromium + WebKit, axe contrast. - **Gate:** render every type to PNG and ask the operator to approve through AskUserQuestion before the PR is enqueued; record the answer in the PR body and report. (This is the operator-approval gate PRD R7 mandates; it is the one question this child raises regardless of autonomous mode.) Pinned seams (build against these exactly; other children build against them too): - S6 layout (C5 ↔ C6): `_layout(..., unsubscribe_url: str | None)`; text part ends with `Unsubscribe: <url>` when set. Credentials & reach: | Oracle | Credential / identity / environment | Status | |---|---|---| | AC2 deliverability | mail-tester.com, no account (free tier) | **verified (reach)** — `https://www.mail-tester.com` answered 301→site; the check runs after the SES production-access row | | AC12 client matrix (Litmus / Email on Acid) | none | **deferred** — operator chose self-run screenshots (I3) | | SES production access (sandbox exit) — gates AC2, AC3, AC11, AC13 and C11 | AWS approval of `PutAccountDetails` in jaah-dev and jaah-main (filed by C1) | **deferred** — reason: AWS reviews it; both accounts probed `ProductionAccessEnabled=false`. Owner C1 files on day one; dependent oracles run after approval | | AC6, terraform plan, SES state, production-access filing (C1, C11) | `me/jaah/system/bootstrap-admin` (management 155534476372) assuming `OrganizationAccountAccessRole` in jaah-dev 905905585302 and jaah-main 366008913850 | **verified** — `credential_get` ok; read-only probe from this node: `sts get-caller-identity`, `sesv2 get-account`, `route53 list-hosted-zones` in both accounts answered | | AC4 (local), AC7, AC9, AC10, AC13 (after sandbox exit), AC17(b) in the dev console | `me/jaah/debug/main/operator-login` (operator, `https://app.dev.jaah.ai`) | **verified** — `credential_get` ok; `https://app.dev.jaah.ai` answered 200 from this node | | AC5, AC18, unit/integration suites | worktree only | **verified** — nothing beyond the worktree | Out of scope: - Do NOT add tracking pixels, link rewriting or required images. - Do NOT change which mail is essential/optional or the unsubscribe mechanics (C5), or the send pipeline (C2a). - Do NOT import from `apps/web` into `apps/api`. Epic out-of-scope (PRD §7, verbatim): - MUST NOT change the apex `jaah.ai` MX, SPF or `_dmarc.jaah.ai` record, nor any Workspace mailbox, alias or routing. - MUST NOT add SES Virtual Deliverability Manager, BIMI/VMC, or a dedicated IP / dedicated IP pool. - MUST NOT request Google's restricted `gmail.readonly` (or any restricted Gmail scope) via OAuth; no CASA assessment. - MUST NOT add open/click tracking pixels or link rewriting to system mail. - MUST NOT let tenant Gmail replace system mail for an org's members (Q9 option not chosen). - MUST NOT expose the delivery log or suppression list to tenant admins or users (operator only, Q7). - MUST NOT relax the verified-address rule or invitation budgets for system mail (AS6). - MUST NOT add boto3/botocore to `apps/api/requirements.txt` unless the Architect records why the hand-signed SigV4 seam (`apps/api/app/aws/`) cannot serve SES (the repo guards its absence). - MUST NOT put a tenant Gmail credential, OAuth token or service-account key on any node or in a runner's environment. - MUST NOT change Slack/Telegram delivery, the support chat, or site enquiries (no email today). Done means: Verifier-recipient mechanism (Planner decision — the operator test send goes only to the caller's own verified login address, `routes/email_templates.py:318-331`, plus the four §9.1 simulator addresses; the verified-address rule AS6 is NOT relaxed, and no privileged account ever gets a shared or public inbox): - Loopback `probe@<mail_domain>`: a **non-privileged verifier user** — an ordinary member invited into org B by its admin (`org/console/dev-orgb-login`, a non-operator org), never superadmin, never in the operator org — whose login email is `probe@mail.dev.jaah.ai`. Its mail lands as a raw object under `inbound/` in the inbound bucket (readable with `me/jaah/system/bootstrap-admin`; after C3, also under the operator's Unmatched emails); the verification link is taken from there. Sending to an address on the verified SES domain works inside the SES sandbox. Use it for user-triggered mail (mention, password reset, invitation, billing notice). - Every template type / mail-tester.com: render each type's raw MIME with the api's own code path in the **local `./dc` api container, built from the commit deployed to that environment, using built-in template content** (a script that calls `email_templates` + `mail_message.build` exactly as the outbox does, with the S2 tags) — never by exec into a deployed task or reading a deployed DB (Jaah is operated only through its UI) — and send it with `aws sesv2 send-email --content Raw` from the environment's identity and configuration set via `me/jaah/system/bootstrap-admin`. mail-tester needs SES production access (it is an unverified recipient). No account's login email ever points at a mail-tester address. Operator-edited templates are covered separately through the console's own preview / operator test send, not the raw path. The free tier caps tests per day — spread runs across days; if a paid credit pack is needed, that is a vendor purchase: ask once with header `Human act`. - The verifier user is deleted at the end of the run that created it (Done-means item: verify it no longer exists in the org's members list). - **AC12 (R7) Design quality.** → verify: the operator-approved mock-ups are recorded in the epic (AskUserQuestion answer); every type rendered (Litmus/Email on Acid or equivalent screenshots) in Gmail web/Android/iOS, Outlook desktop/web, Apple Mail, light + dark, 390px + desktop: no clipped or truncated text, no broken images, no unreadable dark-mode inversion, contrast ≥4.5:1, matches the approved mock-up. An operator-edited template renders inside the layout. (Client matrix deferred — operator chose self-run screenshots (I3): Playwright Chromium + WebKit, light/dark, 390 + 640 px, axe contrast with zero violations; Outlook desktop, Gmail apps and Apple Mail proper are not proven — follow-up F3.) Tightened: every screenshot in the report must be legible and on-brand — a layout that passes axe but looks broken, cramped or off-brand fails. - **AC2 (R1, R7) Deliverability score.** → verify: send the operator test email for every template type to a mail-tester.com address; every one scores **10/10**. (This child owns AC2 on dev (§9.15) via the raw render-and-send path above — the score measures the same MIME the outbox sends: run it if SES production access in jaah-dev is granted by the time the build is ready; otherwise mark it deferred — reason: AWS has not approved sandbox exit — and C11 runs it on dev and prod. Always now: html/text parts pass a lint for mail-tester's content checks — text part present and equivalent, no missing alt, no broken links — and a loopback send of every type to `probe@mail.dev.jaah.ai` passes SPF/DKIM/DMARC.) - The text part of every type is a readable equivalent of the HTML (test asserts every link and the key facts appear in text). - Quick gate `./dc check quick` green and changed-area tests green; `python3 tools/check_context.py` green (any doc this child adds/edits is routed and single-homed).

Activity

  • Draft
  • Ready