diff --git a/CHANGELOG.md b/CHANGELOG.md index 05d5bc0..003d448 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 2026-06-29 — App-email: calcom + nextcloud (tls_mode=plaintext :2526 + nextcloud split-From) +- **nextcloud** — `smtp_mapping` with `tls_mode: plaintext` (controller injects port 2526, the plaintext-only + listener) + **split From** (`from_var=MAIL_FROM_ADDRESS` + `from_domain_var=MAIL_DOMAIN` → nextcloud@felhom.eu). + Compose references the injected `${SMTP_*}`/`${MAIL_*}`. Live-confirmed: real password-reset delivered via + plaintext :2526 (Symfony Mailer never attempted STARTTLS). +- **calcom** — `smtp_mapping` with `tls_mode: plaintext` (EMAIL_SERVER_HOST/PORT, EMAIL_FROM=calcom@felhom.eu). + **Plus three pre-existing template fixes** (calcom never deployed before — the image pin was invalid): + (1) image `v4.8.7`→`v4.6.9` (the pinned tag has no published image); (2) added required `DATABASE_DIRECT_URL` + (Prisma `migrate deploy` fails without it → incomplete schema → 500s); (3) healthcheck `/api/health`→ + `/api/auth/providers` (the old path 404s in v4.x → container stayed unhealthy → Traefik wouldn't route). +- Both apps point at the controller's `:2526` plaintext-only listener because their SMTP clients + opportunistically STARTTLS-upgrade and can't skip the self-signed cert — the listener simply doesn't offer + STARTTLS, so they stay plaintext (accepted on the single-tenant app bridge). + ## 2026-06-29 — App-email rollout: gitea + rallly (calcom/nextcloud/immich = findings) - **gitea 1.23.4** — added `smtp_mapping` (STARTTLS via `GITEA__mailer__PROTOCOL=smtp+starttls` + `FORCE_TRUST_SERVER_CERT=true` to trust the shim's self-signed cert; single `GITEA__mailer__FROM`). Compose diff --git a/REPORT.md b/REPORT.md index 72becac..8dec0e7 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,71 +1,39 @@ -# REPORT — App-email rollout: gitea + rallly wired (calcom/nextcloud/immich = findings) +# REPORT — App-email gap closure: calcom + nextcloud wired (plaintext :2526 + split-From) **Date:** 2026-06-29 -**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. +**Task:** Wire cal.com + nextcloud now that the controller (v0.89.0) has the `:2526` plaintext-only listener +(Gap 1) and `tls_mode`/`from_domain_var` mapping fields (Gap 2). Catalog-only; live send-tests. -## 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. +## Mappings added +- **nextcloud** (`tls_mode: plaintext`, split From): `host_var=SMTP_HOST`, `port_var=SMTP_PORT`, + `from_var=MAIL_FROM_ADDRESS`, `from_domain_var=MAIL_DOMAIN`, `from_local=nextcloud`; no `security_var` + (`SMTP_SECURE` stays empty = no TLS). Compose references the injected `${SMTP_*}` + `${MAIL_*}`. + **Live send confirmed:** real password-reset → Symfony Mailer **plaintext to :2526** (no STARTTLS) → hub → + Resend, From `nextcloud@felhom.eu` (split From), shim `hub=200`. nextcloud applied the env on redeploy + (`occ` shows `mail_smtpport=2526`, `mail_from_address=nextcloud`, `mail_domain=felhom.eu`) — every-boot + `getenv`, **no occ step needed (§8)**. +- **cal.com** (`tls_mode: plaintext`): `host_var=EMAIL_SERVER_HOST`, `port_var=EMAIL_SERVER_PORT`, + `from_var=EMAIL_FROM`, `from_name_var=EMAIL_FROM_NAME`, `from_local=calcom`; secure inferred from the port + (2526 → plaintext). Compose references the injected `${EMAIL_*}`. Injected env verified in-container + (`EMAIL_SERVER_PORT=2526`, `EMAIL_FROM=calcom@felhom.eu`). **Live send confirmed:** a real signup (201 + "Created user") → Nodemailer **plaintext to :2526** (no STARTTLS) → hub → Resend, From `calcom@felhom.eu`, + shim `hub=200`. -## Per-app outcome +## Pre-existing cal.com template bugs fixed (never caught — bad pin meant calcom never deployed) +1. **Image pin `calcom/cal.com:v4.8.7` doesn't exist** as a published image (manifest unknown) → `v4.6.9` + (nearest valid 4.x; same `EMAIL_SERVER_*` behaviour). +2. **Missing `DATABASE_DIRECT_URL`** — cal.com's Prisma schema declares `directUrl=env(DATABASE_DIRECT_URL)`; + without it `prisma migrate deploy` fails (P1012) on startup → incomplete schema → `/api/health` 500s. Added + (same direct Postgres URL). +3. **Healthcheck path `/api/health` is 404 in v4.x** → container stayed unhealthy → Traefik wouldn't route it. + Changed to `/api/auth/providers` (stable 200) in both the docker healthcheck and the `.felhom.yml` probe. -| 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 | +## Validation +- All four edited files YAML-validated; field names match the controller's `stacks.SMTPMapping` tags. +- The `:2526` plaintext-no-STARTTLS mechanism is proven end-to-end by the nextcloud real send; cal.com uses the + identical mechanism (Nodemailer) with its env verified. -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). +## Notes +- **immich still unwired by decision** (Gap 3 — no SMTP env; needs config-file injection). +- Other email-capable apps (ghost, outline, grafana, …) now wire as `tls_mode: plaintext` with no per-app TLS + determination. No secrets in any committed file. diff --git a/templates/calcom/docker-compose.yml b/templates/calcom/docker-compose.yml index 2b99220..581b0b7 100644 --- a/templates/calcom/docker-compose.yml +++ b/templates/calcom/docker-compose.yml @@ -41,7 +41,8 @@ services: limits: memory: 768M healthcheck: - test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/api/auth/providers"] + # GET (-O /dev/null), not --spider (HEAD): the Next.js API route rejects HEAD, so --spider flaps unhealthy. + test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://localhost:3000/api/auth/providers"] interval: 30s timeout: 5s retries: 3