Add automation action Send email
## 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
C9 Automation "Send email": a new automation Action that sends from the org's connected Gmail when its When fires, with templated fields. Delivers PRD R11.
## Requirements (verbatim from the PRD)
### R11 — Automation action "Send email" (A6, Q9)
A new automation Action sends an email from the org's connected Gmail when its When fires, with templated fields from the trigger/event context.
## Approach (from the settled architecture)
- `ActionDescriptor(type="email.send")` in `apps/api/app/automation_actions.py` (near :102); params from/to/cc/subject/body templates; render on **both** event and schedule runs (schedule context: automation name, fired_at, org) — extends `apps/api/app/automation_fire.py:369-393`. Calls `tenant_gmail.send(..., automation_run_id=<run>)` (S7).
- Templating (§9.11): plain `{field}` substitution over a whitelist (`automation.name`, `fired_at`, `org.name`; event runs add the trigger's existing fields via `trigger_template.render`), no expression evaluation; every rendered header passes `mail_message._header_text` (CR/LF stripped, length-capped); recipients re-parsed as addresses after rendering.
- Console: the action's form in the automation editor per `docs/standards/ui-style-guide.md` and `docs/standards/ui-copy-standards.md`.
Pinned seams (build against these exactly; other children build against them too):
- S7 tenant send (C7 → C8/C9): `TenantOutboundEmail(to: list[str], cc: list[str] = [], subject: str, text: str, html: str | None = None)` (1–50 recipients total); `tenant_gmail.send(session, *, org_id: UUID, connection_id: UUID | None, message: TenantOutboundEmail, actor: Actor, task_id: UUID | None = None, automation_run_id: UUID | None = None) -> SendResult`; `SendResult(send_id: UUID, gmail_message_id: str, from_address: str)`. `connection_id=None` = the org's only send-capable connection, else `AmbiguousConnection` (→ 409). Errors: `NoConnection` (→ 409), `PlanOff` (→ 403), `GmailRefused(.status)` (→ 502). C7 creates `tenant_email_sends` and writes it inside `send`; C8/C9 only pass `task_id`/`automation_run_id`. Both builders feed `mail_message.build(sender, to: list[str], cc: list[str], ...)`.
Credentials & reach:
| Oracle | Credential / identity / environment | Status |
|---|---|---|
| AC10 (non-operator 403), AC17 tenant side | `org/console/dev-orgb-login` (org B admin) | **verified** — `credential_get` ok (its recorded base_url is legacy; use `https://app.dev.jaah.ai`) |
| AC14/AC15/AC16/AC17(a) live (AC16 needs a connected Gmail too) | Jaah Google OAuth client (gmail.send) + a test Workspace SA with DWD | **deferred** — operator: "Build now, verify later" (I4) |
| AC5, AC18, unit/integration suites | worktree only | **verified** — nothing beyond the worktree |
Out of scope:
- Do NOT change `tenant_gmail.send` or the connection model (C7); do NOT add the agent MCP tool (C8).
- No expression evaluation in templates.
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:
- **AC16 (R11) Automation action.** → verify: create an automation (When: schedule in 1 min → Action: Send email) in that org; the email arrives with the templated fields filled. (deferred — needs a connected Gmail; operator "Build now, verify later" (I4). Substitute now: fire-engine tests for a schedule run and an event run with a stubbed `tenant_gmail.send` asserting the rendered subject/body/recipients and `automation_run_id` on the send; the action is creatable in the dev console as `org/console/dev-orgb-login` and a fire with no connection records the `NoConnection` failure on the run.)
- Header injection: a subject carrying `\r\nBcc: x@y` yields one Subject header and no Bcc (test).
- 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).
Outcome
Built the Send email automation action (schedule + event, templated, injection-safe recipients) with console form and wizard fix; quick gate + 463 API/196 web tests green; review lane clean; PR #6757 queued in merge lane. Live Gmail check deferred (I4); draft #9458 filed for send source ids. Memory: none applied.
Artifacts
Activity
- Draft
- Ready
- Runner started
- Runner ended (done)