Prima Get an API key

Module kit / Messaging, email and SMS

Platform Email Transport (Resend / SMTP)

messaging-email-sms/email-transport

Kind backend
Domain messaging-email-sms
Maturity prod
Reusable extract
Stack fastapi-0.118, sqlalchemy-2.0-sync, pydantic-2.12, pydantic-settings-2.11, redis-6.4, resend-2.7-sdk-unreachable, smtplib-stdlib
Depends on _shared/fastapi-project-layout, _shared/auth-dependency-pattern, _shared/workspace-scoping
Frontends messaging-email-sms/ui/email-template-editor
Reference implementation
  • alchemy_labs/backend/services/email_service.py
  • alchemy_labs/backend/api/routers/mail_router.py
  • alchemy_labs/backend/core/config.py
  • alchemy_labs/backend/api/dependencies.py
  • alchemy_labs/backend/tests/test_email_redirect.py
  • alchemy_labs/backend/tests/test_email_transport_funnel.py
Measured on alchemy_labs main 62ba4fc41009b454e1c033e5036c7e8577db2061 2026-09-12
Version 1

Purpose

_send_email is the one place platform code sends a transactional message, so flipping one config value (EMAIL_TRANSPORT) moves every mail at once instead of only the call sites someone remembered to rewire (email_service.py 1313 to 1322). The invariant that makes it trustworthy is narrower than its own name suggests, and this recipe states it plainly rather than inventing a cleaner story: as of the measured commit, _send_via_resend no longer calls Resend at all - it delegates unconditionally to the SMTP leaf (email_service.py 1612 to 1616), so every message this funnel sends leaves over Infomaniak regardless of what EMAIL_TRANSPORT is set to. The genuinely load-bearing guarantees are: exactly one transport leaf ever touches the network per call, the EMAIL_REDIRECT_ALL_TO rehearsal valve is applied inside every leaf so a cutover rehearsal cannot leak to a client inbox, and a send failure returns False rather than raising, so a bad mailbox password degrades a login flow instead of 500ing it.

When to use

Read it before touching _send_email, _send_via_smtp_transport, _send_via_resend, apply_redirect, redirect_target, selected_transport, or POST /api/mail/send; before changing what counts as a permanent vs. transient SMTP failure; and when someone asks "why didn't this email go through Resend", "is the redirect still on", "why did this bounce instead of retrying", or "can a client site send mail without an SMTP socket". Trigger phrases: "EMAIL_TRANSPORT", "EMAIL_REDIRECT_ALL_TO", "mail relay", "transport funnel", "ADR-0023", "ADR-0019", "Infomaniak", "hermes@prima.li".

Not this recipe: EmailService.send_via_smtp (email_service.py 1682 onward), a per-workspace customer relay - DB-configured per tenant (a clinic's HIN gateway), carries PHI headers, and raises on failure instead of returning False; the module's own docstring draws this line explicitly (1 to 19). Also not this recipe: the fifty-plus send_* template methods (send_magic_link_email, send_auto_topup_receipt, etc.) that call into _send_email - those are message content, this recipe is the transport underneath all of them. Also not this recipe: the eight files in KNOWN_BYPASSES (test_email_transport_funnel.py 45 to 54) that still call _send_via_resend directly and therefore never touch this funnel at all - see Gotchas.

Stack assumptions

At 62ba4fc: fastapi==0.118.2, synchronous handlers, pydantic==2.12.0 / pydantic-settings==2.11.0 for Settings (core/config.py), redis==6.4.0 for the mail relay's rate limiter, resend==2.7.0 still imported (email_service.py 33 to 38, try/except ImportError guarded) but its send path is dead code, stdlib smtplib/ssl/socket for the SMTP leaf. No httpx or requests import in this file. Foundations assumed: S4 (fastapi-project-layout) for mount_router and the surface-gate mounting pattern the mail relay uses (app_fastapi.py 846); auth-dependency-pattern for ActiveUser/WorkspaceID (api/dependencies.py 566, 779); workspace-scoping for how WorkspaceID resolves a tenant (_get_workspace_id, api/dependencies.py 680 to 703).

Environment, all in core/config.py: EMAIL_TRANSPORT (458, default "smtp", validated to exactly "resend" or "smtp" at 493 to 508 - an unrecognised value raises at config load, never silently ignored, ADR-0019); EMAIL_SMTP_HOST (462, default mail.infomaniak.com), EMAIL_SMTP_PORT (463, default 587), EMAIL_SMTP_USERNAME (464), EMAIL_SMTP_PASSWORD (465, SecretStr), EMAIL_SMTP_USE_SSL (469, default False - STARTTLS on 587 rather than implicit TLS on 465), EMAIL_SMTP_TIMEOUT_SECONDS (470, default 20); RESEND_API_KEY (437, SecretStr, validated to start with re_ only in ENVIRONMENT=production with email verification enabled, 331 to 351); EMAIL_FROM_ADDRESS (438, default prima@labs.alchemy.zuerich), EMAIL_FROM_NAME (439, default Alchemy Zürich); EMAIL_REDIRECT_ALL_TO (491, default None, test/rehearsal only). Fail behaviour: validate_email_smtp_credentials (510 to 566) throws at boot when SMTP is selected (declared explicitly, or defaulted) and credentials are missing, UNLESS the transport was only defaulted (not declared) and ENVIRONMENT is not production - then it falls back to resend and logs a warning (556 to 565); _send_email's own two leaves never raise, they log and return False (module docstring, 1319 to 1322).

Data model

None. This recipe holds no table and writes no row - it is pure transport (config in, an SMTP/HTTP call out, a bool back). The only persistent things adjacent to it are the email_templates table (models/client_onboarding_model.EmailTemplate, read by EmailService.get_template*) and the Redis-backed rate-limit counters the mail relay writes (mailrelay:{workspace_id}:{window}, mail_router.py 117), neither owned by this recipe.

API surface

One HTTP route; everything else below is the funnel and its two leaves, called directly, not routed.

RouteAuthTenancyBodyCodesNotes
POST /api/mail/send (mail_router.py 143 to 190, mounted at app_fastapi.py 846 with prefix="/api/mail", surfaces=LABS_SURFACES)ActiveUser (API-key or session, api/dependencies.py 566) carrying scope mail, gateway-scope-locked: a key with "mail" in its scopes is refused with 403 on any path not starting /api/mail/ (_GATEWAY_SCOPES, api/dependencies.py 93 to 96, checked 98 to 104)WorkspaceID (779), resolved by _get_workspace_id (680 to 703): pre-validated request.state.workspace_id, else X-Workspace-ID header re-checked against the user, else a single-workspace fallback, else told to bootstrap oneMailSendRequest (66 to 74): to: EmailStr (single recipient, no CC/BCC by design), subject: str (1 to 200 chars), html: Optional[str] (≤100 000 chars), text: Optional[str] (≤100 000 chars, wrapped in <pre> and HTML-escaped when html is absent, _escape_html 82 to 87), from_name: Optional[str] (≤80 chars, cosmetic display name only)200 {"sent": true, "to": <email>}; 400 when neither html nor text is given (155 to 159); 429 over the per-workspace rate limit; 502 when the transport reports failure (182 to 188); 503 when the rate limiter itself is unreachable (135 to 140)Rate-limited fail-closed at 30/hour and 200/day per workspace (RATE_LIMIT_PER_HOUR/RATE_LIMIT_PER_DAY, 59 to 60), Redis-backed sliding counters (_check_and_count, 90 to 140); every accepted send is logged with workspace, acting user, recipient and outcome (177 to 180)

The three Python entrypoints the route (and every send_* template method) calls, the actual surface a rebuild reimplements:

FunctionNever raisesReturns
_send_email(to_email, subject, html_content, attachments=None, from_name=None) (email_service.py 1305 to 1332)yesbool
_send_via_smtp_transport(...) (1334 to 1571)yesbool
_send_via_resend(...) (1573 to 1670)yesbool - but see Gotchas: it now only ever calls _send_via_smtp_transport

Application logic

_send_email (1305 to 1332), the whole funnel: reads selected_transport() (48 to 61 - returns TRANSPORT_SMTP only when settings.EMAIL_TRANSPORT == "smtp" literally, else TRANSPORT_RESEND, read via getattr so a mocked settings object in a test behaves like an unset variable); calls _send_via_smtp_transport on the SMTP branch, _send_via_resend otherwise. Nothing else happens at this level - no retry, no redirect application, no logging beyond what the two leaves do themselves.

_send_via_smtp_transport (1334 to 1571), in order:

  1. apply_redirect(to_email, subject) (1362, defined 91 to 121) - see Application logic's own subsection below; called FIRST, before anything is built.
  2. Reads EMAIL_SMTP_HOST/PORT/USERNAME/PASSWORD/USE_SSL/EMAIL_SMTP_TIMEOUT_SECONDS off settings (1376 to 1385); missing host/username/password logs an ERROR naming exactly which is missing and returns False (1389 to 1397) - mirrors _send_via_resend's "not configured" guard.
  3. Builds a MIME message: MIMEMultipart("alternative") with one HTML part (1405 to 1406), wrapped in MIMEMultipart("mixed") with base64-decoded attachment parts when attachments is given (1408 to 1432); sets Subject, RFC-2047-safe From via formataddr (1437, so a non-ASCII display name like "Alchemy Zürich" is encoded rather than sent raw), To, Date, a Message-ID scoped to the from-address's domain (1440), and Auto-Submitted: auto-generated (1443, so a magic link is never auto-replied to).
  4. Sends over up to 3 attempts (for attempt in range(3), 1449) via smtplib.SMTP_SSL when EMAIL_SMTP_USE_SSL else smtplib.SMTP with starttls() and no opportunistic downgrade (1451 to 1462); server.login then server.sendmail; success logs and returns True (1465 to 1469) on the first attempt that works.
  5. Failure classification, exactly as the docstring states (1350 to 1360) and the except clauses implement it:
    • SMTPAuthenticationError → PERMANENT, logged with the server's code and reply, returns False immediately, not retried (1473 to 1482).
    • SMTPNotSupportedError (STARTTLS unavailable) → PERMANENT, returns False, never falls back to an unencrypted session (1487 to 1496).
    • SMTPConnectError / SMTPServerDisconnected / SMTPHeloError → transient, retried with 0.5 * 2**attempt backoff (0.5s, 1s) up to 2 more attempts (1501 to 1512).
    • SMTPSenderRefused / SMTPRecipientsRefused / SMTPDataError / SMTPResponseException → classified by reply code via _smtp_failure_detail (189 to 216): a 5xx is a hard bounce at submission, PERMANENT, returns False without retrying (1521 to 1531); anything else (4xx, or no code at all) is transient and retried with the same backoff (1532 to 1541).
    • socket.timeout / TimeoutError / ConnectionError / socket.gaierror / ssl.SSLError / OSError → transient, same backoff (1546 to 1557). Ordered last among typed clauses deliberately - every smtplib exception subclasses OSError.
    • Anything else → caught, logged, loop breaks without retry (1560 to 1562).
    • After 3 exhausted attempts: one final ERROR log naming host, recipient, sender, subject and the last error, then return False (1566 to 1571).

_send_via_resend (1573 to 1670): the docstring (1585 to 1611) states the policy directly - since 2026-09-03 (commit 98fd579, "email: never Resend - Swiss sending only, unconditionally") this method return self._send_via_smtp_transport(...) at line 1612 to 1615, UNCONDITIONALLY, before apply_redirect is even called at this level (redirect application is left to the SMTP leaf it delegates to, so it is not skipped, only applied once instead of twice). Everything from line 1617 onward - the Resend-specific apply_redirect call, the RESEND_API_KEY check, the resend.Emails.send call with its own 3-attempt exponential-backoff retry (1643 to 1666) and _is_transient_email_error classification (1672 to 1680, string-matched against timeout/connection/temporarily/temporary/rate limit/429/500/502/503/504) - is unreachable. It is kept, per the commit message, "only until the call-site ledger in test_email_transport_funnel.py empties".

apply_redirect(to_email, subject) (91 to 121): reads redirect_target() (80 to 88, EMAIL_REDIRECT_ALL_TO stripped, empty/whitespace reads as unset); when unset, returns its arguments unchanged so every call site can call it unconditionally. When set: a subject already starting with "[→ " is treated as already-diverted and only the recipient is swapped (idempotent, 115 to 116); otherwise logs a WARNING naming the true recipient and the redirect target, and returns (target, f"[→ {to_email}] {subject}") - the true recipient survives in the subject rather than being dropped, so one rehearsal inbox proves both that the mail sent and who it would have reached. Called at the transport leaves (_send_via_smtp_transport 1362, and inside the still-live opening of _send_via_resend before its own dead code, plus _send_via_resend_with_attachment and send_via_smtp per test_email_redirect.py's LEAVES list, 83 to 88), never at _send_email itself - the docstring (102 to 108) explains why: several callers (commerce/fulfillment_service, appointment_invite, send_workspace_email) reach a leaf directly and bypass the funnel, so hooking only the funnel would miss exactly the traffic a rehearsal most needs kept off a client inbox.

log_transport_selection() (124 to 179): called from the app lifespan, never from a request. States the transport choice and its reason at boot in plain language (ADR-0019): warns loudly if a redirect is active (132 to 139); for SMTP, logs host/port/mode/username/sender and warns if a live RESEND_API_KEY is still present and inert (144 to 162); for Resend, warns if SMTP credentials are configured but unused (166 to 173) and states whether RESEND_API_KEY is actually set (174 to 179). Never logs a credential value, only presence.

Invariants

  • Exactly one transport leaf ever puts a message on the wire per _send_email call: the funnel branches once, on selected_transport(), and neither branch calls the other's send primitive a second time (_send_email, 1324 to 1332).
  • _send_email and both leaves never raise: every failure path ends in a logged return False, never an uncaught exception reaching a route handler (module docstring's stated contract, 1319 to 1322; enforced by the exhaustive except chain in _send_via_smtp_transport, 1473 to 1571, and the try/except Exception wrapping _send_via_resend's own body, 1623 to 1670).
  • EMAIL_REDIRECT_ALL_TO, when set, diverts every message and preserves the true recipient in the subject rather than dropping it - proven at each of the four transport leaves independently, not once at the funnel (test_email_redirect.py's test_every_transport_leaf_applies_the_redirect, parametrized over LEAVES, 83 to 111).
  • An unrecognised EMAIL_TRANSPORT value can never boot the process (validate_email_transport, config.py 493 to 508) - no silent fallback to whichever transport happens to be configured.
  • A hard bounce (SMTP 5xx at submission, or SMTPAuthenticationError, or SMTPNotSupportedError) is never retried; every other SMTP failure gets up to 3 attempts with exponential backoff (_send_via_smtp_transport's classification, Application logic above; no dedicated unit test in the two files this recipe is measured on isolates each branch - see Gotchas).
  • As currently shipped, EMAIL_TRANSPORT=resend selects nothing different at the network layer: _send_via_resend forwards unconditionally to the SMTP leaf (email_service.py 1612 to 1616). The config value still gates _send_email's branch and is still validated and logged, but both branches converge on the same wire behaviour.

Frontend

The module that would render this:

messaging-email-sms/ui/email-template-editor   the surface that authors the HTML a send carries
                                                 (EmailTemplate rows via get_template/create_template/
                                                 update_template in this same service file, 221 to 339) -
                                                 not written yet in this repo; nothing in prima-modules
                                                 confirms its existence or state, cited here only because
                                                 frontend_modules on a backend recipe may not be empty

At 62ba4fc, nothing in alchemy_labs's own frontend calls _send_email, _send_via_smtp_transport or POST /api/mail/send directly (mail_router.py's only documented caller is a client site's Cloudflare Worker over plain HTTPS, module docstring 1 to 21) - this recipe has no first-party UI of its own. Minimum admin/debug surface: GET /health reports whether EMAIL_REDIRECT_ALL_TO is active (per config.py's own comment, 483 to 486 - the endpoint itself is out of this recipe's file scope, cited here only because the comment promises it); the boot log line from log_transport_selection() states the selected transport, sender, and whether Resend is configured, every process start.

Security & tenancy

Auth dependency: ActiveUser (api/dependencies.py 566, Depends(get_current_active_user)), session or API key. Tenancy: WorkspaceID (779, Depends(_get_workspace_id)), resolved from pre-validated request state, an X-Workspace-ID header re-checked against the user, or a single-workspace fallback - never inferred silently across a multi-workspace user (680 to 703). Scope lock: a key carrying "mail" in its declared scopes is refused with 403 on every path outside /api/mail/ (_GATEWAY_SCOPES["mail"] = "/api/mail/", 93 to 96, enforced 98 to 104) - the router's own docstring states the threat model outright: the credential lives in a client's Cloudflare Worker environment, "assume it leaks", and this scope means a leak reaches the relay and nothing else (mail_router.py 23 to 39).

Fail direction: EMAIL_TRANSPORT closed at config load (an unrecognised value throws, never guessed, 493 to 508); SMTP credentials closed in production (throws at boot rather than serving with no mail path, 547 to 554); the mail relay's rate limiter fail-CLOSED - a Redis outage refuses the send with 503 rather than waving traffic through unmetered, because an unmetered relay is "the failure mode that gets a sending domain blocklisted" (_check_and_count's own docstring, mail_router.py 93 to 100, 135 to 140); a transport-reported failure surfaces as 502, never a false 200 (182 to 188).

Secrets at rest: EMAIL_SMTP_PASSWORD and RESEND_API_KEY are both SecretStr (config.py 437, 465); the SMTP leaf reads the credential value only at the point of server.login (1379 to 1383) and never logs it - every log line in _send_via_smtp_transport's failure paths prints host, port, username, recipient, subject and the server's own reply code, never the password. No caller-controlled sender: From is always the platform identity (EMAIL_FROM_ADDRESS/EMAIL_FROM_NAME), from_name on POST /api/mail/send is display-only and cannot change the envelope address or domain (mail_router.py 71 to 74, "It cannot be used to impersonate a domain"). No CC/BCC and one recipient per call, by design, so a leaked relay key is "a poor spam cannon" (36).

Attacker-controlled inputs on POST /api/mail/send, all behind ActiveUser + the mail scope: to (EmailStr-typed), subject (≤200 chars), html/text (≤100 000 chars each, text HTML-escaped before use, _escape_html 82 to 87 - a caller cannot inject markup through the plain-text path), from_name (≤80 chars, display only).

Tests

Run 2026-09-12 from alchemy_labs/backend with the repo's own .venv, no database, no live SMTP or Resend:

.venv/bin/python -m pytest tests/test_email_redirect.py tests/test_email_transport_funnel.py \
    -q --no-cov -o addopts="" -p no:cacheprovider

prints 12 passed (36 Pydantic-deprecation warnings, unrelated to this recipe - core/billing_config.py's own Field(..., env=...) usage). grep -c "^def test_" tests/test_email_redirect.py tests/test_email_transport_funnel.py prints 6 and 3; test_email_redirect.py carries one @pytest.mark.parametrize("leaf", LEAVES) over 4 leaves, taking its collected total from 6 defs to 9 test cases, plus the funnel file's 3, for 12.

Hermetic: both files patch or monkeypatch everything that would touch a network - test_email_redirect.py patches apply_redirect itself to raise (_Reached, 79 to 111) so a leaf call is proven to reach the hook without any message actually being built or sent; test_email_transport_funnel.py does static source scanning (_offenders(), 59 to 74), no runtime send at all.

Known-answer vectors: apply_redirect("client@example.ch", "Subject") with no redirect set returns the arguments unchanged (test_passthrough_when_unset); with EMAIL_REDIRECT_ALL_TO set to a rehearsal address (the test's own TARGET fixture), apply_redirect("someone@example.com", "New submission") returns (TARGET, "[→ someone@example.com] New submission") exactly (test_diverts_and_keeps_the_true_recipient); a whitespace-only EMAIL_REDIRECT_ALL_TO reads as unset (test_blank_value_is_not_a_redirect); applying it twice is idempotent (test_is_idempotent); an empty recipient is left alone rather than having a target invented for it (test_empty_recipient_is_left_alone).

Regression guard: test_no_new_transport_bypasses fails the build the moment any file outside services/email_service.py and tests/ adds a new direct _send_via_resend(...) or _send_via_resend_with_attachment(...) call - the known set in KNOWN_BYPASSES (8 files, 45 to 54) may only shrink; test_ledger_has_no_stale_entries fails the build if a listed bypass was actually fixed and the entry was not deleted, so the ledger cannot silently become fiction; test_funnel_still_exists is a guard on the guard - if _send_email is ever renamed, this test says so explicitly rather than the other two silently passing for the wrong reason.

What this recipe's two test files do NOT cover, stated plainly: no test exercises _send_via_smtp_transport's own retry/backoff or permanent-vs-transient classification (the smtplib exception branches, email_service.py 1449 to 1571) - that logic is read from source and the module's own inline comments in this recipe, not proven by a known-answer test in the files this recipe was scoped to; no test exercises POST /api/mail/send itself (rate limiting, scope enforcement, the 400/429/502/503 paths) - no test_mail_router.py or equivalent exists in this repo at the measured commit (find backend -iname "*mail_router*" -path "*test*" returns nothing).

Deploy & ops

Config keys per app: EMAIL_TRANSPORT, EMAIL_SMTP_HOST, EMAIL_SMTP_PORT, EMAIL_SMTP_USERNAME, EMAIL_SMTP_PASSWORD, EMAIL_SMTP_USE_SSL, EMAIL_SMTP_TIMEOUT_SECONDS, RESEND_API_KEY, EMAIL_FROM_ADDRESS, EMAIL_FROM_NAME, EMAIL_REDIRECT_ALL_TO (all cited under Stack assumptions). No migration: this recipe adds no table. Mounting: mount_router(app, mail_router.router, prefix="/api/mail", tags=["Mail Relay"], surfaces=LABS_SURFACES) (app_fastapi.py 846), alongside the platform's ordinary session-gated routers, not surface_exempt.

No scheduler job: unlike the auto-topup engine this recipe borrowed its measured-detail bar from, nothing here is polled - every send is triggered inline, by a request or a background task.

Argus-style checks (not found as a dedicated script in this repo at the measured commit, stated as the equivalent manual check): GET /health's report of whether EMAIL_REDIRECT_ALL_TO is active is the go/no-go signal before any DNS cutover, per config.py's own comment (483 to 486); the boot log line from log_transport_selection() is the fastest way to confirm which transport a given process actually selected without reading config by hand. Rollback: stateless - reverting the code or the config value changes behaviour on the next process boot with nothing to unwind.

Gotchas

  • 2026-09-12 (measured at 62ba4fc, email_service.py 1612 to 1616, commit 98fd579 dated 2026-09-03): EMAIL_TRANSPORT=resend does not send through Resend. _send_via_resend returns self._send_via_smtp_transport(...) unconditionally as its first statement; the Resend SDK call, its own retry loop and _is_transient_email_error classification (1617 to 1670) are dead code, reachable by no code path. The commit message states this is deliberate policy ("Swiss sending only, unconditionally... regardless of what EMAIL_TRANSPORT is set to") kept "only until the call-site ledger in test_email_transport_funnel.py empties." A rebuild that assumes the config value genuinely branches to two live transports will misdescribe production behaviour.
  • 2026-09-12 (measured at 62ba4fc, test_email_transport_funnel.py 45 to 54): eight files bypass _send_email entirely and call _send_via_resend (now the SMTP leaf, per the gotcha above) or _send_via_resend_with_attachment directly, so EMAIL_TRANSPORT never governed their choice of transport in the first place: api/routers/admin_router.py (/api/admin/notify, named in both files' own docstrings as the still-open bug), api/routers/user_router.py, services/notification_service.py, services/content_review_service.py, services/appointment_invite.py, services/commerce/fulfillment_service.py, services/accounting/invoice_service.py, services/spark/welcome.py. This recipe's funnel and redirect guarantees do not extend to these call sites; apply_redirect is proven to run inside _send_via_resend itself (Tests, above) so the redirect valve still catches them, but the funnel's single-choke-point property does not.
  • 2026-09-12 (measured at 62ba4fc): _send_via_resend_with_attachment (email_service.py 2678, not read in full for this recipe - out of the three named source files) is cited by both test files (KNOWN_BYPASSES's invoice_service entry, and LEAVES in test_email_redirect.py) but its body was not part of this recipe's cited source files; a rebuild needing its exact behaviour should read it directly rather than infer it from this recipe.
  • Genuine gap, stated rather than guessed: no test in the two files this recipe was scoped to isolates _send_via_smtp_transport's permanent-vs-transient branch behaviour (an SMTPAuthenticationError really stopping after one attempt, a ConnectionError really retrying three times with the stated backoff). The classification in Application logic above is read from the source code and its own inline comments, which are specific and dated, but it is not backed by a known-answer test the way the redirect and funnel-bypass guarantees are.
  • Genuine gap: maturity: prod here is inferred from mounting posture and boot-time validation (see the frontmatter comment), not from a deploy log, a running /health response, or traffic metrics - this repo checkout has none of those to read.

Reference implementation

Paths relative to the repo root; all at main 62ba4fc41009b454e1c033e5036c7e8577db2061 unless noted.

  • alchemy_labs/backend/services/email_service.py (2805 lines): module docstring, including the ADR-0023 transport note and the explicit boundary with send_via_smtp (1 to 20); selected_transport (48 to 61); transport_is_configured (64 to 77); redirect_target (80 to 88); apply_redirect (91 to 121); log_transport_selection (124 to 179); _decode_smtp_reply (182 to 186); _smtp_failure_detail (189 to 216); EmailService.get_template/get_template_by_name/list_templates/render_template/create_template/update_template/delete_template (221 to 339, template CRUD, cited for context, not this recipe's subject); _send_email (1305 to 1332); _send_via_smtp_transport (1334 to 1571); _send_via_resend (1573 to 1670, live opening 1573 to 1616, dead body 1617 to 1670); _is_transient_email_error (1672 to 1680); send_via_smtp (1682 onward, the per-workspace relay, out of scope, cited only at its boundary); email_service = EmailService() singleton (2806).
  • alchemy_labs/backend/api/routers/mail_router.py (190 lines, whole file in scope): module docstring, including the Worker→HTTPS→SMTP diagram and the security posture list (1 to 44); MailSendRequest/MailSendResponse (66 to 80); _escape_html (82 to 87); _check_and_count (90 to 140); send_mail (143 to 190).
  • alchemy_labs/backend/core/config.py: RESEND_API_KEY (437) and its validator (331 to 351); EMAIL_FROM_ADDRESS/EMAIL_FROM_NAME (438 to 439); the ADR-0023 comment block and EMAIL_TRANSPORT (443 to 458); EMAIL_SMTP_* (460 to 470); the EMAIL_REDIRECT_ALL_TO comment block and field (472 to 491); validate_email_transport (493 to 508); validate_email_smtp_credentials (510 to 566).
  • alchemy_labs/backend/api/dependencies.py: _GATEWAY_SCOPES including the mail entry and its comment (66 to 96), the enforcement loop (98 to 104); ActiveUser (566); _get_workspace_id (680 to 703); WorkspaceID (779).
  • alchemy_labs/backend/app_fastapi.py: the mail relay's mount (846).
  • Tests as listed under Tests: alchemy_labs/backend/tests/test_email_redirect.py (111 lines, whole file); alchemy_labs/backend/tests/test_email_transport_funnel.py (108 lines, whole file).

Deviations of the reference from an ideal implementation, all dated 2026-09-12: EMAIL_TRANSPORT=resend is a config value with no live effect on the wire (Gotchas); eight call sites still bypass the funnel by design-debt, tracked but not fixed (Gotchas); the Resend SDK integration, including its own retry and error classification, is unreachable dead code kept for an unfinished migration; no dedicated test exists for POST /api/mail/send or for the SMTP leaf's failure-classification branches in the two files this recipe was scoped to read.

Acceptance checks

Run from alchemy_labs/backend at 62ba4fc (or an app built to this recipe), no database and no live network required. Expected observations are exact, as measured 2026-09-12.

  1. .venv/bin/python -m pytest tests/test_email_redirect.py tests/test_email_transport_funnel.py -q --no-cov -o addopts="" -p no:cacheprovider prints 12 passed.
  2. grep -c "^def test_" tests/test_email_redirect.py tests/test_email_transport_funnel.py prints 6 and 3; grep -c "@pytest.mark.parametrize" tests/test_email_redirect.py prints 1 (the leaf parametrization over 4 LEAVES entries, which is what takes the file from 6 defs to 9 collected cases).
  3. grep -n "return self._send_via_smtp_transport" services/email_service.py | head -1 shows line 1612, inside _send_via_resend, confirming the Resend path is short-circuited before its own apply_redirect/RESEND_API_KEY logic runs.
  4. grep -n "^def selected_transport" services/email_service.py shows line 48; grep -n "TRANSPORT_SMTP if getattr" services/email_service.py shows one match, confirming the funnel's branch is a positive test for SMTP, not a value match on "resend".
  5. grep -c "\"mail\":" api/dependencies.py prints 1, confirming exactly one gateway-scope entry names the mail relay's prefix.
  6. grep -n "mount_router(app, mail_router.router" app_fastapi.py shows line 846 with surfaces=LABS_SURFACES, not surface_exempt(...).
  7. find . -iname "*mail_router*" -path "*test*" (run from alchemy_labs/backend) prints nothing, confirming the stated test gap under Tests.

Content synced from the prima-modules repository at 0df2ec8 on 2026-09-14. The recipe file is the source; this page is a rendering of it.