From 9c0166ddddc684ddae887ceac4a40b5d0a572a0a Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Mon, 29 Jun 2026 08:59:19 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20README=20=E2=80=94=20app-email=20relay?= =?UTF-8?q?=20(mailrelay)=20feature=20section?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- controller/README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) 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