Redesign every system email template
## 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