f10d74f993
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
64 lines
4.8 KiB
Markdown
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.
|