Module kit / Messaging, email and SMS
Workspace Custom SMTP Relay
messaging-email-sms/workspace-smtp-relay
| Kind | backend |
|---|---|
| Domain | messaging-email-sms |
| Maturity | prod |
| Reusable | extract |
| Stack | fastapi-0.118.2, sqlalchemy-2.0-sync, postgres, cryptography-46-fernet, python-smtplib |
| Depends on | _shared/fastapi-project-layout, _shared/workspace-scoping, _shared/error-envelope |
| Frontends | messaging-email-sms/ui/email-template-editor |
| Reference implementation |
|
| Measured on | alchemy_labs main 62ba4fc41009b454e1c033e5036c7e8577db2061 2026-09-12 |
| Version | 1 |
Purpose
A workspace can route its outbound mail through its own SMTP server (a clinic's HIN relay such as smtp.mail.hin.ch) instead of Alchemy's platform transport, configured once at PUT /api/workspaces/{workspace_id}/smtp and read back by every send that goes through send_workspace_email (email_service.py 1826-1862). The invariant that makes it trustworthy is a deliberate contract split: the platform funnel (_send_email/_send_via_smtp_transport/_send_via_resend, all elsewhere in the same file) always returns a bool and never raises, while this per-workspace relay (send_via_smtp, 1682-1824) always raises on failure, so a caller that cannot afford a silently-lost send - the secure medical-form submit, which stores nothing (public_forms_router._send_secure_submission, 386-425) - can pass raise_on_error=True and get an exception instead of a swallowed False.
When to use
Read before touching send_via_smtp, send_workspace_email, get_smtp_config/save_smtp_config/delete_smtp_config, or the GET/PUT/DELETE/POST .../smtp* routes; before changing what a workspace's own relay does to non-medical mail (form-creator notifications, messaging_service campaign steps); and when someone asks "why did this clinic's mail go out from HIN instead of Infomaniak", "why does the SMTP test button say ok: false", "does the workspace relay affect the whole workspace or only secure forms", or "why does HIN reject mail from: in lowercase". Trigger phrases: "custom SMTP", "HIN relay", "workspace SMTP", "smtp.mail.hin.ch", "X-HIN-Encrypt", "PHI headers", "secure_recipient", "subject_flag", "uppercase SMTP verbs", "send_via_smtp raises".
Not this recipe: the PLATFORM transport chosen by EMAIL_TRANSPORT (Resend vs Infomaniak - selected_transport/_send_via_smtp_transport/_send_via_resend, email_service.py 44-1671), a different config source (env vars, not the database) with a different contract (bool, never raises), a neighbouring recipe; EMAIL_REDIRECT_ALL_TO/apply_redirect (91-121), the rehearsal-diversion hook this recipe's leaf also calls into but does not own; the secure-medical-form submit pipeline itself (the no-storage intake, the PHI-safe HTML rendering in smtp_config.format_submission_html) beyond the one call it makes into send_workspace_email; the inbound IMAP mailbox (services/service_desk/inbox_config.py, whose own docstring says "mirroring services.smtp_config") - same Fernet-encryption pattern, opposite direction, a separate recipe; the EmailTemplate system (get_template/render_template) that composes the HTML this recipe transports, not what generates it.
Stack assumptions
At 62ba4fc: fastapi==0.118.2, synchronous handlers on a sqlalchemy.orm.Session (SQLAlchemy==2.0.43), cryptography==46.0.0 for Fernet, psycopg2-binary==2.9.10, stdlib smtplib/ssl/email for the wire - no third-party SMTP client. Foundations assumed: S4 (fastapi-project-layout) for the app and router mount; S6 (workspace-scoping) for IsolationService.verify_workspace_access gating every /smtp* route; S7 (error-envelope) for the HTTPException shapes the routes raise and the plain ValueError send_via_smtp raises on an incomplete config.
No migration: the config lives in the pre-existing workspace_settings.settings JSON column (baseline_prod_schema_001.py 2229-2238, json NOT NULL, unique on workspace_id via uq_workspace_settings_workspace_id, 3060-3063), read and written with raw sqlalchemy.text() rather than an ORM model - smtp_config.py's own module docstring says so explicitly (1-14).
Environment this recipe's own code reads: none directly. No EMAIL_SMTP_* variable gates this path; the config comes entirely from the database row. It reuses one platform credential: CALENDAR_ENCRYPTION_KEY, read by calendar_integration_service._get_or_create_key (78-112) - sourced from the environment in production and REQUIRED there (ValueError if ENVIRONMENT != development and the variable is unset, 108-112), or degrading to a generated file-based key (.calendar_encryption_key) in development (95-107). Fail behaviour: get_smtp_config/save_smtp_config/delete_smtp_config never validate that the relay is reachable, they only read/write the encrypted blob; send_via_smtp raises ValueError("SMTP config incomplete (host/from_address required)") when those two fields are missing (1754-1755) and otherwise lets any smtplib/ssl/socket exception propagate uncaught - no retry, no classification, unlike the platform transport's three-attempt backoff (contrast _send_via_smtp_transport, 1334-1571, which this method's own docstring calls out by name, 1344-1348).
Data model
No dedicated table. One JSON block inside the pre-existing workspace_settings row, one per workspace:
workspace_settings.settings = {
..., -- other settings blocks untouched (read-merge-write, smtp_config.py 73-91)
"smtp": {
"host": string,
"port": integer, -- default 587 (smtp_config.py 80, email_service.py 1749)
"username": string | null,
"from_address": string | null, -- falls back to username if unset (email_service.py 1753)
"encryption": "starttls" | "ssl" | "none", -- default "starttls"
"subject_flag": string | null, -- HIN encryption trigger keyword, e.g. "[secure]"
"secure_recipient": string | null, -- ALL secure medical forms in the workspace, set once, not per form
"password_encrypted": string | null -- Fernet ciphertext; written only when a new password is supplied
}
}
workspace_settings.settings is json NOT NULL with no shape enforced at the database level (baseline_prod_schema_001.py 2234) and no CHECK constraints - every field above is enforced only by smtp_config.py's own read/write functions (_load_settings 27-40, get_smtp_config 43-70, save_smtp_config 73-107, delete_smtp_config 110-119).
What is NOT stored anywhere in this recipe: the plaintext password - only password_encrypted is written (88-89), and get_smtp_config decrypts it into a password key only when called with with_password=True (67-69); the API's own GET /smtp route never passes that flag (workspace_router.py 681), so the plaintext never crosses the network in a response. The content of a secure medical-form submission - _send_secure_submission's docstring states "store NOTHING" (public_forms_router.py 386-391); the relay itself, and the practice's own inbox, is the only place that answer ever lands.
API surface
| Route | Auth | Tenancy | Codes | Notes |
|---|---|---|---|---|
GET /api/workspaces/{workspace_id}/smtp (workspace_router.py 670-682) | get_current_user | verify_workspace_access(user_id, workspace_id) - any active member | 200 {configured:false} or the config (no password); 403 no access | |
PUT /api/workspaces/{workspace_id}/smtp (685-702), body SmtpConfigPut (655-664) | get_current_user | verify_workspace_access(..., required_role="admin") | 200 the saved config; 403 no access/insufficient role | "Mutating the relay is money-bearing" (inline comment, 693-697); a PUT that omits password keeps the stored one (save_smtp_config 88-89) |
DELETE /api/workspaces/{workspace_id}/smtp (705-720) | get_current_user | verify_workspace_access(...) - member | 200 {configured:false}; 403 no access | |
POST /api/workspaces/{workspace_id}/smtp/test (723-763), body SmtpTestRequest (666-667) | get_current_user | verify_workspace_access(...) - member | 200 {ok:true, to} or {ok:false, error} - no 5xx: the route itself catches send_via_smtp's raise (761-763) | Merges a stored password when the body omits one (744-747); runs send_via_smtp in a threadpool |
The two Python entrypoints this recipe is actually about, not routes but the surface a rebuild reimplements:
| Function | Raises on failure | Returns | Notes |
|---|---|---|---|
send_via_smtp(smtp_config, to_email, subject, html_content, from_name=None, attachments=None) (1682-1824) | yes, always | True on success | smtp_config is a plain dict {host, port, username, password, from_address, encryption}, not the ORM/DB shape directly |
send_workspace_email(db, workspace_id, to_email, subject, html_content, *, raise_on_error=False) (1826-1862) | only if raise_on_error=True and a relay is configured | True/False | Looks up the workspace's config itself; falls through to the platform _send_email funnel when none is configured |
Application logic
- Configure:
PUT /smtp→save_smtp_config(73-107) does a read-merge-write of thesmtpkey insidesettings- every other top-level key insettingsis preserved (78, 90) - normalizesport/encryptiondefaults, and only overwritespassword_encryptedwhen the request body'spasswordis non-empty (86-89); commits (106). - Take over: any call site that sends through
send_workspace_emailfirst callsget_smtp_config(db, workspace_id, with_password=True)(1846-1851). Ifsmtp.hostis set, EVERY such send for that workspace goes tosend_via_smtpfrom then on - not scoped to secure/medical forms, not opt-in per message (module docstring 1836-1843). There is no per-call override to force the platform transport once a relay is configured. - Send (
send_via_smtp, 1682-1824): applies the platform's rehearsal redirect first (apply_redirect, 1705) - the one leaf a rehearsal environment could otherwise miss, per the inline comment (1700-1703); builds a MIME message structurally identical to the platform transport (MIMEMultipart, optional attachment part); forces UPPERCASE SMTP verbs forMAIL/RCPT/DATAvia the_UpperSMTP/_UpperSMTP_SSLsmtplib.SMTPsubclasses (1720-1746), because "HIN'ssmtp-proxyis case-sensitive on SMTP verbs (non-RFC-5321)" and rejects smtplib's default lowercasemail from:/rcpt to:with501 invalid MAIL arguments- verified directly againstsmtp.mail.hin.chper the inline comment (1716-1719); stamps three PHI/priority headers unconditionally,X-HIN-Encrypt: true,X-Contains-PHI: true,X-Priority: 2(1801-1804, "Best-effort signal headers honoured by HIN / Swiss healthcare gateways" - not encryption Labs performs itself); connects overSSLorSTARTTLS/none per theencryptionfield (1806-1821); raisesValueErrorifhost/from_addressis missing (1754-1755) and otherwise lets anysmtplib/sslexception surface uncaught - no retry loop, no failure classification, unlike the platform leaf right above it in the same file. - Two callers of
send_workspace_email, three call sites, one direct bypass:_send_secure_submission(public_forms_router.py386-425) calls it withraise_on_error=Trueinside atry/exceptthat turns any exception intoHTTPException(502)(412-425) so the caller never reports success for a lost submission; the form-creator notification (800-803) and the AI-form-creator notification (1227-1230) both call it with the defaultraise_on_error=False, so a relay failure there is only alogger.warning(804-805, 1231-1232), the submission itself still succeeds.messaging_service.send_step(280-355) is a FOURTH, separate call site that reachessend_via_smtpdirectly rather than throughsend_workspace_email- it fetchesget_smtp_configitself (312-313), prefers the workspace relay whensmtp.hostis set, and wraps the raise in its owntry/exceptto keep the campaign step best-effort (322-341).
Invariants
- A workspace's own relay, once
smtp.hostis set, takes over ALL of that workspace's mail sent throughsend_workspace_email- not scoped to secure/medical forms, and there is no per-message opt-out (send_workspace_emaildocstring, 1836-1843). send_via_smtpraises on failure; every platform transport leaf in the same file returns a bool and never raises (module docstring 16-19;_send_via_smtp_transport's own docstring names the contrast explicitly, 1344-1348).- The plaintext password is returned by
get_smtp_configonly when a caller explicitly passeswith_password=True(43-46, 67-69); theGET /smtproute never does (workspace_router.py681). - The rehearsal redirect (
apply_redirect) runs insidesend_via_smtpitself (1705), not only at the platform funnel - proven bytest_email_redirect.py, wheresend_via_smtpis one of the four transportLEAVESthe parametrized test asserts calls it before anything reaches the wire (83-89, 102-111). - SMTP verbs are forced uppercase for every send through this relay unconditionally, not only when the configured host is HIN's (
_UpperSMTP, 1720-1746) - the source measures this againstsmtp.mail.hin.chspecifically (1716-1719) and generalizes it to the whole method; a relay that is itself strictly RFC-5321-lowercase-only is not something this code has been observed against. - A
PUT /smtpthat omitspasswordkeeps the previously storedpassword_encrypteduntouched (save_smtp_config86-89) - the field is write-only and additive, never blanked by omission.
Frontend
The frontmatter's frontend_modules names messaging-email-sms/ui/email-template-editor, not yet a written recipe in this domain (zero recipes existed in messaging-email-sms before this one) - it is the template-composition surface whose HTML output this recipe transports, not the screen that configures the relay itself. Stated plainly because the two are easy to conflate: the actual settings surface, also not yet its own written frontend recipe, is WorkspaceEmailSettings.tsx (frontend/src/pages/settings/WorkspaceEmailSettings.tsx, whole file, 34 lines) rendering CustomSmtpCard.tsx (frontend/src/components/settings/CustomSmtpCard.tsx, 232 lines): a form posting SmtpConfigPut's exact fields to the three mutating routes above, a write-only password input that shows a "gespeichert – leer lassen, um zu behalten" placeholder once has_password is true (168-176), and a "Testnachricht senden" button hitting POST /smtp/test (222-224, onTest 85-100). Reached from Forms.tsx via a "Sicherer E-Mail-Versand (HIN)" link (frontend/src/pages/Forms.tsx 426-433) and mounted at /settings/email behind PrivateRoute in App.tsx (288-295) - not gated behind any feature flag.
Minimum admin/debug surface (no dedicated route exists at this commit): SELECT workspace_id, settings->'smtp'->>'host' AS host, settings->'smtp'->>'secure_recipient' AS secure_recipient FROM workspace_settings WHERE settings ? 'smtp' lists every workspace with a relay configured; the log lines "Secure form send failed (template %s): %s: %s" (public_forms_router.py 421), "Workspace SMTP send failed (%s): %s: %s" (email_service.py 1859), and "messaging: SMTP send failed to %s: %s" (messaging_service.py 340) are the three places a relay failure surfaces in the logs.
Security & tenancy
Auth dependency: get_current_user (JWT bearer, httpOnly cookie, or X-API-Key, api/dependencies.py 187 onward) on every /smtp* route. Tenancy: IsolationService.verify_workspace_access(user_id, workspace_id, required_role=None) (services/workspace/isolation.py 109-154) checks WorkspaceUser membership and an is_active row, then a role-hierarchy comparison (viewer < member < admin < owner, 144-153) when a required_role is given. GET/DELETE/POST .../smtp/test require only membership; PUT requires required_role="admin" (workspace_router.py 699) - the inline comment states why: "Mutating the relay is money-bearing" (693-697), the same reasoning applied to the read/delete/test endpoints being ordinary-member-accessible: "managing your own workspace's email/payment relay requires workspace ACCESS, not a separate admin-only permission" (comments at 693, 711, 731).
Fail direction: 403 on failed verify_workspace_access on every route; send_via_smtp fails closed on an incomplete config (ValueError, 1754-1755) rather than attempting a connection with a blank host. There is no server-side validation of the host/port/encryption VALUES beyond presence: a workspace admin can point host at any hostname:port they choose and the backend will open a real TCP/TLS connection and attempt server.login/sendmail against it (1748-1821) - the only boundary is workspace-admin auth on the PUT, there is no allowlist of relay hosts and no check that the target is an actual mail submission port.
Secrets at rest: the password is Fernet-encrypted with CALENDAR_ENCRYPTION_KEY, the SAME cipher instance the calendar OAuth-token integration owns (calendar_integration_service.py 70, encrypt_token/decrypt_token 137-143) - chosen specifically so no new credential is required (smtp_config.py module docstring, 4-6). It is decrypted server-side only at the three call sites that pass with_password=True: workspace_router.test_workspace_smtp (745-747), email_service.send_workspace_email (1848), and messaging_service.send_step (313). PHI: the secure-medical-form path never persists clean_data anywhere, only emails it through this relay (_send_secure_submission docstring, public_forms_router.py 386-391); the X-HIN-Encrypt/X-Contains-PHI headers are signals TO the relay, not encryption Labs performs itself (1801-1804 comment). Rate limits: none dedicated to /smtp*; /smtp/test is reachable by any active workspace member with no throttling beyond ordinary session auth.
Attacker-controlled inputs: workspace_id is a path parameter bound by verify_workspace_access on every route, so it cannot select another tenant's config. Every SmtpConfigPut field (host, port, username, password, from_address, encryption, subject_flag, secure_recipient) is a free-form string a workspace ADMIN supplies about their own relay - the realistic "attacker" this section is about is an admin of a compromised or malicious workspace using the relay config as an internal-network probe or an SSRF-shaped connect-anywhere primitive, not an anonymous caller (every route requires authentication).
Tests
No dedicated test file for smtp_config.py, send_via_smtp, or send_workspace_email's bool-vs-raise contract exists under backend/tests/ at 62ba4fc - find backend/tests -iname "*smtp*" returns only backend/tests/integration/test_smtp_capture.py. send_via_smtp is exercised exactly once, and only for the rehearsal-redirect hook: test_email_redirect.py's LEAVES list (83-89) includes "send_via_smtp", and the parametrized test_every_transport_leaf_applies_the_redirect (102-111) proves apply_redirect runs before anything reaches the wire by patching it to raise and asserting the raise propagates through send_via_smtp({"host": "smtp.invalid"}, "[email protected]", "S", "<p>x</p>") (99). It does not open a socket, exercise _UpperSMTP's verb override, or assert on the PHI headers.
test_smtp_capture.py (whole file, 324 lines) is a real in-process SMTP server that captures the PLATFORM transport's bytes on the wire (STARTTLS, envelope sender, headers, body) - a strong template for what a send_via_smtp-specific version of the same test would look like - but its two tests (test_prima_magic_link_on_the_wire, test_labs_magic_link_is_unchanged_on_the_wire) both go through send_magic_link_email → _send_via_smtp_transport, never through send_via_smtp or _UpperSMTP; the file's own docstring states it "does NOT prove anything about real Infomaniak" (15-16), and it says nothing about HIN at all.
Run 2026-09-12, from alchemy_labs/backend with the .venv there:
python -m pytest tests/test_email_redirect.py -q --no-cov
prints 9 passed (five named tests plus the four-way @pytest.mark.parametrize("leaf", LEAVES) case, one per transport leaf), including the parametrized send_via_smtp case. Hermetic: no socket is opened, apply_redirect is patched before any leaf runs. Known-answer vectors: none exist for this recipe specifically - no test asserts on _UpperSMTP's literal wire bytes (MAIL FROM: vs mail from:), on the X-HIN-Encrypt/X-Contains-PHI/X-Priority header values, or on save_smtp_config preserving a sibling settings key across a read-merge-write.
What a new app adds and this branch lacks: a test_smtp_capture.py-style hermetic proof that _UpperSMTP.mail/.rcpt/.data actually put uppercase verbs on the wire against a captive server; a test for send_workspace_email's two contracts (raise_on_error=True propagates the underlying exception; raise_on_error=False swallows it and returns False); a test that save_smtp_config leaves an unrelated key already present in settings untouched (the read-merge-write claim in the module docstring is currently unverified by any test); a test that PUT /smtp without a password field genuinely keeps the stored one rather than merely not overwriting it in that one code path.
Deploy & ops
Config keys: CALENDAR_ENCRYPTION_KEY (shared with the calendar-integration module; required in production, optional/file-backed in development - calendar_integration_service.py 78-112). No dedicated env var for this recipe; nothing here reads EMAIL_SMTP_*. Migrations: none - the table and column (workspace_settings.settings, json NOT NULL) predate this feature in baseline_prod_schema_001.py (2229-2238); the feature itself shipped by adding code, not schema, per smtp_config.py's own docstring ("no migration", 4). First shipped fdd410e "feat(forms): per-workspace custom SMTP + secure medical-form mode (HIN)", dated 2026-06-16 (git log --follow), with a routing follow-up in b66025d. Scheduler: none - every send is synchronous, run in a FastAPI threadpool from the route (run_in_threadpool, workspace_router.py 760; public_forms_router.py 414).
Argus-style checks: none dedicated exist in this repo for this seam; the closest is the admin query under Frontend (workspaces with settings ? 'smtp') combined with the three log lines listed there. Rollback: nothing deployable beyond the code itself - a workspace's smtp key is removed only by an explicit DELETE /smtp call (delete_smtp_config, 110-119), never by a code rollback, so reverting this recipe's code does not need to unwind any stored configuration.
Gotchas
- 2026-09-12 (measured at 62ba4fc):
_send_via_resend(1573-1670) is dead code below an unconditional earlyreturn self._send_via_smtp_transport(...)at line 1612 - the Resend SDK call beneath it (1617-1670) is unreachable. Not this recipe's subject (that's the platform transport), noted here only because it lives in the same file and a naive read of "what happens when I call_send_via_resend" is misleading without it. - 2026-09-12 (measured at 62ba4fc,
calendar_integration_service.py90-112): in adevelopmentenvironment with noCALENDAR_ENCRYPTION_KEYset, a.calendar_encryption_keyfile is silently generated on first use. Because this is the SAME key that encrypts a workspace's SMTP password, any process that constructsCalendarService()(and thereforeemail_service, which importscalendar_serviceat module load -smtp_config.py22) before that file exists will create it; losing that file makes every storedpassword_encryptedvalue permanently undecryptable. Production fails closed instead (ValueErrorat boot), so this is a development/staging-only trap. - 2026-09-12 (measured at 62ba4fc): the uppercase-SMTP-verb override (
_UpperSMTP, 1720-1746) is applied to everysend_via_smtpcall regardless of the configured host - it is not conditional onhostcontaininghin.chor similar. The source comment frames it as measured specifically againstsmtp.mail.hin.ch(1716-1719); whether it is harmless, neutral, or a problem against a strict-lowercase RFC-5321 relay has not been measured in this repo. - 2026-09-12 (measured at 62ba4fc):
send_via_smtp's own docstring says it is "Blocking; run in a threadpool" (1698) - every call site in this repo does (run_in_threadpoolinworkspace_router.py760 andpublic_forms_router.py414;messaging_service.send_stepis itself already off the event loop per its own surrounding code). A future direct call from anasync defroute body without that wrapper would block the event loop for the duration of a real SMTP round trip (up to a ~25s timeout, 1808/1814), with nothing in this file guarding against it.
Reference implementation
Paths relative to the repo root; all at main 62ba4fc41009b454e1c033e5036c7e8577db2061 unless noted.
alchemy_labs/backend/services/email_service.py(2806 lines): module docstring and the transport split (1-20);_decode_smtp_reply/_smtp_failure_detail(182-216, shared helpers, used by the platform leaf, not this recipe's own send path);_send_email(1305-1332, the platform funnelsend_workspace_emailfalls back to);_send_via_smtp_transport(1334-1571, the PLATFORM transport, contrasted throughout this recipe but not its subject);send_via_smtp(1682-1824);send_workspace_email(1826-1862).alchemy_labs/backend/services/smtp_config.py(158 lines, whole file in scope): module docstring (1-14);_load_settings(27-40);get_smtp_config(43-70);save_smtp_config(73-107);delete_smtp_config(110-119);format_submission_html(122-157, the secure-form HTML renderer this recipe'ssend_via_smtptransports, not otherwise part of this recipe).alchemy_labs/backend/api/routers/workspace_router.py:SmtpConfigPut/SmtpTestRequest(655-667);get_workspace_smtp(670-682);put_workspace_smtp(685-702);delete_workspace_smtp(705-720);test_workspace_smtp(723-763).alchemy_labs/backend/api/routers/public_forms_router.py:_send_secure_submission(386-425, theraise_on_error=Truecaller); the form-creator notification call (788-805); the AI-submission notification call (1199-1232).alchemy_labs/backend/services/messaging_service.py: the workspace-relay preference and directsend_via_smtpcall insidesend_step(280-355, specifically 307-341).alchemy_labs/backend/services/calendar_integration_service.py:_get_or_create_key(78-112);encrypt_token/decrypt_token(137-143) - the shared Fernet cipher this recipe's password encryption reuses.alchemy_labs/backend/services/service_desk/inbox_config.py: not read in full for this recipe, cited only because its own docstring namessmtp_config.pyas the pattern it mirrors for inbound IMAP - a neighbouring recipe, not this one.alchemy_labs/backend/migrations/versions/baseline_prod_schema_001.py: theworkspace_settingstable this recipe stores into, already present at baseline (2229-2238, constraints 2880-2883, 3060-3063, index 3641-3643) - no migration was added for this feature.alchemy_labs/frontend/src/pages/settings/WorkspaceEmailSettings.tsx(34 lines, whole file in scope).alchemy_labs/frontend/src/components/settings/CustomSmtpCard.tsx(232 lines, whole file in scope).alchemy_labs/frontend/src/pages/Forms.tsx: the "Sicherer E-Mail-Versand (HIN)" entry point (426-433).alchemy_labs/frontend/src/App.tsx: the/settings/emailroute (288-295) and its lazy import (95).- Tests:
alchemy_labs/backend/tests/test_email_redirect.py(112 lines, whole file in scope, the only test that touchessend_via_smtp);alchemy_labs/backend/tests/integration/test_smtp_capture.py(324 lines, cited for contrast - it tests the platform transport, not this recipe).
Deviations of the reference from an ideal implementation, all dated 2026-09-12: no test exists for _UpperSMTP's wire behaviour, the PHI headers, or send_workspace_email's raise/no-raise contract (Tests, above); the SMTP relay host/port fields accept any value with no allowlist, no reachability check, and no SSRF-shaped guard beyond workspace-admin auth (Security & tenancy, above); save_smtp_config's read-merge-write claim is asserted only in a comment, not in a test; the encryption key can be silently file-generated in development (Gotchas, above).
Acceptance checks
Run from alchemy_labs/backend at 62ba4fc with its own .venv active. Expected observations are exact, as measured 2026-09-12.
python -m pytest tests/test_email_redirect.py -q --no-covprints9 passed.grep -n "def send_via_smtp" services/email_service.pyprints line1682;grep -n "def send_workspace_email" services/email_service.pyprints line1826.grep -n "class _UpperSMTP" services/email_service.pyprints two lines,1720(class _UpperSMTP(smtplib.SMTP):) and1745(class _UpperSMTP_SSL(smtplib.SMTP_SSL, _UpperSMTP):), confirming the non-RFC uppercase-verb override lives insidesend_via_smtp, not the platform transport.grep -n "X-HIN-Encrypt\|X-Contains-PHI" services/email_service.pyprints two lines,1802and1803, both insidesend_via_smtp(no occurrence inside_send_via_smtp_transport, the platform leaf).grep -n "smtp\"" ../backend/migrations/versions/baseline_prod_schema_001.pyfinds no match - confirms thesmtpblock lives only inside the pre-existingworkspace_settings.settingsJSON, never in its own migrated column.grep -n "send_workspace_email" api/routers/public_forms_router.pyprints exactly three lines -415,800,1227- confirming the three call sites named in Application logic step 4 (the secure-form submit, the form-creator notification, the AI-submission notification).git log --follow --oneline -- services/smtp_config.py | tail -1printsfdd410e feat(forms): per-workspace custom SMTP + secure medical-form mode (HIN), confirming the feature shipped 2026-06-16 and predates this measurement by three months, consistent withmaturity: prod.
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.