f10d74f993
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4.8 KiB
4.8 KiB
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 listenerPlainNoTLSAddr(default:2526), plaintext, withTLSConfigleft nil so go-smtp does NOT advertise STARTTLS.:2525/:2465unchanged.Start/Close/addr-accessor updated for the third socket.internal/stacks/metadata.go— Gap 2:SMTPMappinggainsTLSMode(yaml:"tls_mode") +FromDomainVar(yaml:"from_domain_var").internal/stacks/mailenv.go—smtpEnvnow picks the port viashimPortForTLSMode(""/starttls→2525;plaintext→2526;implicit-tls→2465) instead of hardcoded 2525; split-From injection whenFromDomainVaris 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— passPlainNoTLSAddrto 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. CompanionTestShimPortForTLSMode— plaintext and starttls must map to DIFFERENT ports (fails if the switch is dropped).TestSMTPEnv_SplitFrom—from_domain_varset → two keys (MAIL_FROM_ADDRESS=nextcloud+MAIL_DOMAIN=felhom.eu), port 2526. Companion — a mapping withoutfrom_domain_varyields the singlevaultwarden@felhom.eu.TestServer_PlainNoTLSListener_NoSTARTTLS—:2526server hasTLSConfig==nil& a real EHLO shows it does not advertise STARTTLS while:2525does.
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 FromMAIL_FROM_ADDRESS=nextcloud+MAIL_DOMAIN=felhom.eu. §8 env-consumption: nextcloud applied it on redeploy (occ config:system:getshowsmail_smtphost=felhom-controller mail_smtpport=2526 mail_smtpsecure= mail_from_address=nextcloud mail_domain=felhom.eu) — the official image reads these viagetenv()every boot, no occ step needed. A real password-reset relayed: shimrelayed (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: shimrelayed (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.7doesn't exist as a published image →v4.6.9; (2) missingDATABASE_DIRECT_URL(Prismamigrate deployfails → incomplete schema → 500s); (3) healthcheck path/api/healthis 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_FILEJSON); 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.