diff --git a/controller/README.md b/controller/README.md index 5e009df..2e1881a 100644 --- a/controller/README.md +++ b/controller/README.md @@ -2101,6 +2101,32 @@ Response format: `{"ok": true/false, "data": ..., "error": "...", "message": ".. --- +## App-email relay (`internal/mailrelay/`) + +Gives deployed apps outbound email (password resets, invites, confirmations) through one managed path — +**app → in-process SMTP shim → hub → Resend** — with the Resend key staying **hub-side** (never on the box). +Architecture **Shape 1**: the shim runs in-process inside the controller, reusing the existing hub client. +Implements `felhom.eu/documentation/audits/SPIKE-smtp-app-relay-2026-06-28.md`. + +- **The shim (`internal/mailrelay/`)** is a `go-smtp` server with two listeners — `:2525` plaintext+STARTTLS + and `:2465` implicit-TLS (self-signed cert at boot). It advertises AUTH PLAIN+LOGIN and **accepts any + credentials, ignoring them** (apps send none; some require the offer). `Data` reads the raw message, enforces + the **From-header** domain allowlist (reject `5xx` **before** any hub call), then forwards the **raw MIME** to + the hub `POST /api/v1/mail` with the controller's hub Bearer key — **single-shot** (no retry, no spool in v1). + The hub HTTP status maps to an SMTP reply (2xx→250, 4xx→451, 5xx→554). Listeners bind to the app Docker + network only (the controller container joins `traefik-public`); apps reach the shim by `felhom-controller`. +- **Lifecycle** (`lifecycle.go`): the shim starts/stops at runtime to match the global app-email toggle (no + controller restart). Wired in `main.go`, gated on a configured hub + the `mail_relay` kill-switch. +- **Toggles + injection:** a **global** toggle (`settings.AppEmail`, Settings page) and a **per-app** toggle + (`AppConfig.EmailEnabled`, on the app's config page, shown only for apps with an `smtp_mapping`). When both are + on and the app declares `.felhom.yml` **`smtp_mapping`**, `stackEnv` injects the relay env at compose time + (host=shim, port=2525, security/from per the mapping, From=`@felhom.eu`) — derived each compose, never + persisted to `app.yaml`. Config knobs: `mail_relay` (listeners, `shim_host`, `from_domains`, kill-switch). +- **What the box never holds:** the Resend key, or any durable mail queue. v2 (deferred) = a separate + `felhom-smtp-shim` container + accept-and-spool retry + a `Resend-Idempotency-Key`. + +--- + ## Build & Deploy ### Build