docs: REPORT + CHANGELOG — gitea/rallly send-tested live; findings + template fixes

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-29 11:06:57 +02:00
parent 32c730f749
commit 42bf76f540
2 changed files with 72 additions and 38 deletions
+7 -2
View File
@@ -5,8 +5,13 @@
`FORCE_TRUST_SERVER_CERT=true` to trust the shim's self-signed cert; single `GITEA__mailer__FROM`). Compose
references the injected `GITEA__mailer__*` keys; env applied every boot.
- **rallly** — added `smtp_mapping` (Nodemailer STARTTLS, `SMTP_SECURE=false` + `SMTP_REJECT_UNAUTHORIZED=false`
to accept the self-signed cert; single `NOREPLY_EMAIL`). **Also fixed a broken image pin:** `3.12.1` does not
exist (3.x stops at 3.11.x) → corrected to `3.11.2` so the template is deployable.
to accept the self-signed cert; single `NOREPLY_EMAIL`). **Also fixed three pre-existing template bugs** that
made rallly undeployable (never caught because the bad pin never ran): (1) image pin `3.12.1` doesn't exist →
`3.11.2`; (2) healthcheck used `wget`, absent from the rallly image (exit 127) → container unhealthy →
**Traefik wouldn't route it** → replaced with a Node http check; (3) added required `SUPPORT_EMAIL` + a valid
`NOREPLY_EMAIL` default (rallly refuses to boot without them).
- **Both gitea and rallly send-tested live** end-to-end (app → shim → hub → Resend): gitea password-reset
(From `gitea@felhom.eu`) and rallly registration code (From `rallly@felhom.eu`) both delivered.
- **NOT wired — reported as findings** (`felhom.eu/documentation/audits/FINDING-app-email-rollout-2026-06-29.md`):
- **cal.com v4.8.7** — hard-codes TLS `rejectUnauthorized:true` with no override; opportunistic STARTTLS
against the self-signed shim fails. Needs a non-STARTTLS-advertising plaintext listener (mechanism change).
+65 -36
View File
@@ -1,42 +1,71 @@
# REPORT — App-email `smtp_mapping` for Vaultwarden + Mealie
# REPORT — App-email rollout: gitea + rallly wired (calcom/nextcloud/immich = findings)
**Date:** 2026-06-29
**Task:** SMTP app-relay (catalog leg). Add `smtp_mapping` to the two spike-proven apps so deployed apps can
send outbound email via the managed path (app → in-controller shim → hub → Resend). Implements
`felhom.eu/documentation/audits/SPIKE-smtp-app-relay-2026-06-28.md` §7.
**Task:** Roll out the shipped app-email relay (controller v0.88.0 shim + `smtp_mapping`; hub v0.18.0 passthrough)
to calcom, rallly, gitea, nextcloud (+ investigate immich). Catalog-only; no controller/hub code change.
## Files modified
- `templates/vaultwarden/.felhom.yml` — added `smtp_mapping` (STARTTLS; SMTP_SECURITY=starttls,
SMTP_FROM=vaultwarden@felhom.eu, SMTP_FROM_NAME, + SMTP_ACCEPT_INVALID_CERTS/HOSTNAMES). SMTP_USERNAME/
SMTP_PASSWORD intentionally unset.
- `templates/vaultwarden/docker-compose.yml` — reference the injected `${SMTP_*}` keys (harmless defaults).
- `templates/mealie/.felhom.yml` — added `smtp_mapping` (plaintext; SMTP_AUTH_STRATEGY=NONE on :2525,
SMTP_FROM_EMAIL=mealie@felhom.eu, SMTP_FROM_NAME).
- `templates/mealie/docker-compose.yml` — reference the injected `${SMTP_*}` keys (harmless defaults).
- `README.md` — documented the `smtp_mapping` pattern (field table + STARTTLS-vs-plaintext guidance) so further
apps are easy adds.
- `CHANGELOG.md` — entry.
## Baselines (verified live)
- felhom-controller **v0.88.0** + hub **v0.18.0** live (relay mechanism shipped). Global app-email toggle ON on
guest 9201. Guest RAM **12 GB** (raised from 2 GB) — confirmed before deploying heavier apps.
## How it works
When app-email is on (global toggle + per-app toggle), the controller injects at deploy/redeploy:
`SMTP_HOST` = the on-box shim, `SMTP_PORT` = 2525, From = `<app>@felhom.eu`, the app's security value, plus the
mapping's fixed `extra` vars. The values are derived from settings each compose — never persisted to `app.yaml`.
An empty `SMTP_HOST` (toggle off) leaves the app's mail disabled.
## Per-app outcome
## Validation
- Both `.felhom.yml` and both `docker-compose.yml` YAML-validated OK.
- Field names match the controller's `stacks.SMTPMapping` struct tags exactly (covered by the controller's
`TestMetadata_SMTPMappingParse` and the Vaultwarden/Mealie env-injection tests).
- Git sync picks up the change within ~15 min (or via "Sablonok frissítése").
- The end-to-end relay path these mappings feed (app → on-box shim → hub → Resend) was live-validated on
guest 9201 / hub v0.18.0 on 2026-06-29 with a Vaultwarden-shaped message (From `vaultwarden@felhom.eu`,
inline CID image) → Resend `250 queued`; see `felhom-controller/REPORT.md`. Deploying the Vaultwarden app and
using its admin "send test email" is the recommended final smoke for the app's own SMTP client (spike-proven).
| App | Wired? | Shape | Self-signed handling | From | Env consumed on redeploy | Live send-test |
|-----|--------|-------|----------------------|------|--------------------------|----------------|
| **gitea 1.23.4** | ✅ | STARTTLS (`smtp+starttls`) | `FORCE_TRUST_SERVER_CERT=true` | single `GITEA__mailer__FROM` | **Yes** (env→app.ini every boot) | ✅ **arrived** — password-reset email, From `gitea@felhom.eu` |
| **rallly 3.11.2** | ✅ | STARTTLS (`SMTP_SECURE=false`) | `SMTP_REJECT_UNAUTHORIZED=false` | single `NOREPLY_EMAIL` | **Yes** (read at send time) | ✅ **arrived** — registration code, From `rallly@felhom.eu` |
| **cal.com v4.8.7** | ❌ finding | — | **none** (hard-coded `rejectUnauthorized:true`) | single `EMAIL_FROM` | n/a | not wired |
| **nextcloud 31.0.14** | ❌ finding | — | **none** + **split From** | `MAIL_FROM_ADDRESS`+`MAIL_DOMAIN` | n/a | not wired |
| **immich v2.5.5** | ❌ finding | — | `ignoreCert` (config-file/UI only) | single (config field) | n/a | not wired |
## Notes / deviation
- **Mealie uses plaintext (NONE), not STARTTLS.** Mealie has no accept-invalid-cert option, so STARTTLS to the
self-signed shim would fail cert verification; the spike validated Mealie via plaintext on :2525 (the shim is
Docker-network-only, never published). This is the spike-faithful choice (spike §7 mealie table = NONE),
deviating from the prompt's Part-4 "TLS for mealie" wording.
- **Scope = these two proven apps only.** Further apps (immich, nextcloud, …) are a follow-on: each just needs
its own `smtp_mapping` block + the matching compose `${VAR}` lines.
Findings detail: `felhom.eu/documentation/audits/FINDING-app-email-rollout-2026-06-29.md`.
### gitea 1.23.4 — WIRED, send confirmed
- **Mapping** (`smtp_mapping`): `host_var=GITEA__mailer__SMTP_ADDR`, `port_var=GITEA__mailer__SMTP_PORT`,
`security_var=GITEA__mailer__PROTOCOL` (`smtp+starttls`), `from_var=GITEA__mailer__FROM`, `from_local=gitea`,
`extra={GITEA__mailer__ENABLED:true, GITEA__mailer__FORCE_TRUST_SERVER_CERT:true}`.
- **Verified live:** toggle ON → controller injected all six `GITEA__mailer__*` env vars (confirmed in the
running container) → gitea wrote the correct `[mailer]` section in app.ini (env→ini every boot). Completed
the install (admin = the test inbox) and triggered a **password-reset** (forgot-password): the shim logged
`relayed (from="gitea@felhom.eu" rcpts=1 bytes=2195 hub=200)`, the hub logged `/api/v1/mail: relayed`. Go
`net/smtp` STARTTLS to the self-signed shim, trusting it via FORCE_TRUST_SERVER_CERT — works.
### rallly 3.11.2 — WIRED, send confirmed (+ 2 pre-existing template bugs fixed)
- **Mapping:** `host_var=SMTP_HOST`, `port_var=SMTP_PORT`, `security_var=SMTP_SECURE` (`false`),
`from_var=NOREPLY_EMAIL`, `from_name_var=NOREPLY_EMAIL_NAME`, `from_local=rallly`,
`extra={SMTP_REJECT_UNAUTHORIZED:false}`.
- **Verified live:** toggle ON → injected env confirmed (`SMTP_HOST=felhom-controller`,
`NOREPLY_EMAIL=rallly@felhom.eu`, `SMTP_SECURE=false`, `SMTP_REJECT_UNAUTHORIZED=false`). Drove the **real
registration** flow in the browser → rallly's Nodemailer sent the verification code: shim logged
`relayed (from="rallly@felhom.eu" rcpts=1 bytes=12755 hub=200)`, hub logged `/api/v1/mail: relayed`.
Nodemailer opportunistic STARTTLS to the self-signed shim, accepted via reject-unauthorized=false — works.
- **Side-fixes (pre-existing, undeployable template):**
1. Image pin `lukevella/rallly:3.12.1` **does not exist** (3.x stops at 3.11.x) → corrected to **3.11.2**.
2. Healthcheck used `wget`, which the rallly image **does not ship** (exit 127) → container marked unhealthy
**Traefik refused to publish a route** (the "URL nem elérhető" warning). Replaced with a Node http
check. After the fix rallly is healthy and Traefik routes it. (Diagnosis credit: the operator spotted the
unhealthy↔no-route correlation.)
3. rallly **requires** `SUPPORT_EMAIL` + a valid `NOREPLY_EMAIL` at boot (refuses to start otherwise) — added
valid defaults so it boots with app-email off; the relay overrides `NOREPLY_EMAIL` when on.
### immich investigation (Task §6)
immich v2.5.5 has **no SMTP environment variables**. Email is system config (admin UI → DB) or a JSON file via
`IMMICH_CONFIG_FILE` (which supports `ignoreCert:true` for the self-signed shim). It does **not** fit the
env-injection `smtp_mapping` model. **Left unwired** — needs a future config-file-injection mechanism or manual
admin-UI setup. See the finding doc.
## Observations / mechanism gaps (for follow-on)
- **Gap 1 — self-signed STARTTLS vs opportunistic-upgrade clients with no skip-verify** (cal.com; nextcloud's
TLS half). The shim advertises STARTTLS on :2525; Nodemailer/Symfony clients auto-upgrade and validate the
cert. Apps without a skip flag fail. **Fix:** a plaintext-only listener that does NOT advertise STARTTLS
(controller change) — the highest-coverage unblock.
- **Gap 2 — split From** (nextcloud `MAIL_FROM_ADDRESS`+`MAIL_DOMAIN`): the single `from_var` can't express it.
**Fix:** add an optional `from_domain_var`.
- **Gap 3 — no-SMTP-env apps** (immich): needs config-file injection (a separate, larger capability).
- An app that needs SMTP only at first-install was NOT found among these — gitea and nextcloud both re-apply
every boot; rallly reads at send time.
- **Remaining email-capable apps still unwired** (future passes): ghost, outline, grafana, docmost, bookstack,
n8n, paperless-ngx, … — each needs the same per-app determination, and the opportunistic-STARTTLS ones need
Gap-1's plaintext listener first.
No secrets in any committed file; the Resend key stays hub-side (out-of-band).