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

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.goGap 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.goGap 2: SMTPMapping gains TLSMode (yaml:"tls_mode") + FromDomainVar (yaml:"from_domain_var").
  • internal/stacks/mailenv.gosmtpEnv 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.gomail_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_SplitFromfrom_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.