Files
felhom-controller/REPORT.md
T
2026-06-29 13:56:51 +02:00

64 lines
4.8 KiB
Markdown

# REPORT — App-email gap closure: plaintext listener + split-From, v0.89.0
**Task:** Close the two relay gaps from `FINDING-app-email-rollout-2026-06-29.md` so opportunistic-STARTTLS
clients (cal.com, nextcloud) can use the relay. Controller mechanism + two catalog mappings + live send-tests.
**Locked posture:** plaintext on the single-tenant app Docker bridge is accepted (no TLS/CA work).
## Baselines (verified)
- felhom-controller `main` @ `6692a2f`, **v0.88.0 → v0.89.0**. app-catalog touched. Hub untouched (N/A).
## Files changed (controller)
- `internal/mailrelay/server.go`**Gap 1:** added a third listener `PlainNoTLSAddr` (default `:2526`),
plaintext, with `TLSConfig` left **nil** so go-smtp does NOT advertise STARTTLS. `:2525`/`:2465` unchanged.
`Start`/`Close`/addr-accessor updated for the third socket.
- `internal/stacks/metadata.go`**Gap 2:** `SMTPMapping` gains `TLSMode` (`yaml:"tls_mode"`) +
`FromDomainVar` (`yaml:"from_domain_var"`).
- `internal/stacks/mailenv.go``smtpEnv` now picks the port via `shimPortForTLSMode` (`""`/`starttls`→2525;
`plaintext`→2526; `implicit-tls`→2465) instead of hardcoded 2525; split-From injection when `FromDomainVar`
is set (`FromVar=<local>` + `FromDomainVar=<domain>`), else the existing `<local>@<domain>`.
- `internal/config/config.go``mail_relay.plain_no_tls_listen` (default `:2526`). `cmd/controller/main.go`
pass `PlainNoTLSAddr` to the lifecycle Options.
## Green gate (local)
`go build ./... && go vet ./... && go test ./...`**PASS** (16 packages, 0 failures).
- `TestSMTPEnv_PortByTLSMode` — plaintext→2526, starttls/empty→2525, implicit-tls→2465. **Companion**
`TestShimPortForTLSMode` — plaintext and starttls must map to DIFFERENT ports (fails if the switch is dropped).
- `TestSMTPEnv_SplitFrom``from_domain_var` set → two keys (`MAIL_FROM_ADDRESS=nextcloud`+`MAIL_DOMAIN=felhom.eu`),
port 2526. **Companion** — a mapping without `from_domain_var` yields the single `vaultwarden@felhom.eu`.
- `TestServer_PlainNoTLSListener_NoSTARTTLS``:2526` server has `TLSConfig==nil` & a real EHLO shows it does
**not** advertise STARTTLS while `:2525` does.
## Deployed v0.89.0 + listener
Built `:0.89.0` on 180, deployed to guest 9201 (golden/bootstrap). `docker ps`: `…:0.89.0 Up (healthy)`. Shim
started **three** listeners: `plaintext+STARTTLS on :2525`, `implicit-TLS on :2465`, and the new
`plaintext-only (no STARTTLS) listener on :2526`.
## Live send-tests
- **nextcloud (Symfony Mailer) — Scenario B PROVEN:** toggle ON injected `SMTP_HOST=felhom-controller`,
`SMTP_PORT=2526`, `SMTP_SECURE=` (empty), and the **split From** `MAIL_FROM_ADDRESS=nextcloud` +
`MAIL_DOMAIN=felhom.eu`. **§8 env-consumption:** nextcloud applied it on redeploy (`occ config:system:get`
shows `mail_smtphost=felhom-controller mail_smtpport=2526 mail_smtpsecure= mail_from_address=nextcloud
mail_domain=felhom.eu`) — the official image reads these via `getenv()` every boot, **no occ step needed**. A
real password-reset relayed: shim `relayed (from="nextcloud@felhom.eu" rcpts=1 bytes=20430 hub=200)`. The
Symfony client sent **plaintext to :2526 and never attempted STARTTLS** → delivered. (Gap 1 + Gap 2 proven
end-to-end with a real opportunistic-STARTTLS client.)
- **cal.com (Nodemailer) — Scenario A PROVEN:** toggle ON injected `EMAIL_SERVER_HOST=felhom-controller`,
`EMAIL_SERVER_PORT=2526`, `EMAIL_FROM=calcom@felhom.eu` (verified in-container). A real signup (HTTP 201
"Created user") triggered the verification email: shim `relayed (from="calcom@felhom.eu" rcpts=1 bytes=10260
hub=200)`. The Nodemailer client sent **plaintext to :2526 and never attempted STARTTLS** → delivered.
(calcom needed three pre-existing template fixes first — see Observations.)
- **No regression (Scenario C):** gitea redeployed under v0.89.0 still injects `SMTP_PORT=2525` +
`smtp+starttls`; a real gitea forgot-password relayed `(from="gitea@felhom.eu" hub=200)` via the unchanged
:2525 STARTTLS path.
## Observations
- **Three pre-existing cal.com template bugs** surfaced (never caught — the bad image pin meant calcom never
deployed): (1) image `v4.8.7` doesn't exist as a published image → `v4.6.9`; (2) missing `DATABASE_DIRECT_URL`
(Prisma `migrate deploy` fails → incomplete schema → 500s); (3) healthcheck path `/api/health` is 404 in
v4.x → `/api/auth/providers`. All fixed in the catalog.
- **immich (Gap 3) remains unwired by decision** — it has no SMTP env (admin-UI/DB or `IMMICH_CONFIG_FILE`
JSON); the next email piece is a config-file render+mount mechanism.
- **Other email-capable apps now wire trivially** as `tls_mode: plaintext` (ghost, outline, grafana, …) — no
per-app TLS determination needed under the accepted posture.
- No secrets in any committed file; the Resend key stays hub-side.