From 86079adb9a615dfc7e4868817ef41b2d3f142c11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Wed, 12 Aug 2026 23:01:17 +0000 Subject: [PATCH] mail transport is configured in the app, not in .env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MAIL_TRANSPORT was a fallback left from the old registration flow that sent confirmation mail. That flow is gone; the variable outlived it. It was never the primary source anyway. getTransport reads server_config ('server-settings' → smtp) first, which already backs a full UI at Settings → Server → SMTP and its API in api/server-settings/smtp.ts, supporting resend, smtp and mailhog. The env var only answered when that was absent — which is a second source of truth for something the owner can already set, with the failure mode that a stale URL in .env silently answers for a server whose settings row is simply empty. Removed from transport.ts, .env.example and the setup script's Environment section, which no longer asks for it. setup-old/ still mentions it; that is the archive and is left alone. Also split the try. It wrapped the read AND the transport construction and swallowed both, so three different problems produced one message. Unreachable database, nothing configured, and stored settings that do not build a transport now say different things, because the fix for each is different and this message is all the caller ever sees. The two consumers — queue/engine.ts and auth/forgot-password.ts — now raise until SMTP is set in the UI, which is the honest answer rather than a regression. Not typechecked: node_modules is empty here and installs are frozen. transport.ts parses under `bun build --no-bundle`; the setup script was run and no longer prompts for or writes the variable. Co-Authored-By: Claude Opus 5 (1M context) --- .env.example | 1 - scripts/setup/officer-setup.sh | 6 --- scripts/setup/officer-setup/lib/env.sh | 1 - src/workspaces/emailer/src/transport.ts | 57 +++++++++++++------------ 4 files changed, 29 insertions(+), 36 deletions(-) diff --git a/.env.example b/.env.example index 0d05e034..238bcef0 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,6 @@ PORT=9000 JWT_SECRET="" POSTGRES_URL="postgres://postgres:password@localhost:5432/officer" -MAIL_TRANSPORT="smtp://localhost:1025" PUBLIC_URL=http://localhost:9000 # Guards (CORS origin checks, rate limits, password-strength rules) are ON unless this is set to diff --git a/scripts/setup/officer-setup.sh b/scripts/setup/officer-setup.sh index 2e81d2da..9bc6e246 100755 --- a/scripts/setup/officer-setup.sh +++ b/scripts/setup/officer-setup.sh @@ -464,7 +464,6 @@ if ! skip; then ENV_VAULT_STORE_KEY="$(env_get VAULT_STORE_KEY)" ENV_PORT="$(env_get PORT)" ENV_PUBLIC_URL="$(env_get PUBLIC_URL)" - ENV_MAIL_TRANSPORT="$(env_get MAIL_TRANSPORT)" ENV_DISCORD_WEBHOOK="$(env_get DISCORD_BUG_REPORT_WEBHOOK)" ENV_BROWSER_RELAY_PORT="$(env_get BROWSER_RELAY_PORT)" @@ -522,11 +521,6 @@ if ! skip; then echo "" echo " ALLOW_ANY_ORIGIN=${ENV_ALLOW_ANY_ORIGIN} — ${ORIGIN_WHY}" - echo "" - ENV_MAIL_TRANSPORT="${ENV_MAIL_TRANSPORT:-}" - read -rp " Mail transport, blank for none [${ENV_MAIL_TRANSPORT}]: " REPLY_MAIL || true - ENV_MAIL_TRANSPORT="${REPLY_MAIL:-$ENV_MAIL_TRANSPORT}" - echo "" echo " to write:" echo " PORT=${ENV_PORT} BROWSER_RELAY_PORT=${ENV_BROWSER_RELAY_PORT}" diff --git a/scripts/setup/officer-setup/lib/env.sh b/scripts/setup/officer-setup/lib/env.sh index acbd05b0..f10ad210 100644 --- a/scripts/setup/officer-setup/lib/env.sh +++ b/scripts/setup/officer-setup/lib/env.sh @@ -111,7 +111,6 @@ HOME_DIR="${USER_HOME}" # tailnet; written explicitly here so the machine's actual situation decides it. ALLOW_ANY_ORIGIN="${ENV_ALLOW_ANY_ORIGIN}" -MAIL_TRANSPORT="${ENV_MAIL_TRANSPORT}" DISCORD_BUG_REPORT_WEBHOOK="${ENV_DISCORD_WEBHOOK}" ENVF diff --git a/src/workspaces/emailer/src/transport.ts b/src/workspaces/emailer/src/transport.ts index 04e2d571..ff9cebb8 100644 --- a/src/workspaces/emailer/src/transport.ts +++ b/src/workspaces/emailer/src/transport.ts @@ -1,7 +1,13 @@ import { createTransport, type Transporter } from 'nodemailer'; import { readServerSettings } from 'officerdb'; -const { MAIL_TRANSPORT } = process.env; +// Configured from Settings → Server → SMTP, and from nowhere else. +// +// There was a MAIL_TRANSPORT fallback here until 2026-08-12, left from the old registration flow that +// sent confirmation mail. It outlived that flow and became a second source of truth for something the +// owner can already set in the UI — with the failure mode that a stale URL in .env silently answered +// for a server whose settings row was simply empty. Absent settings now raise, which is the honest +// answer and points at the screen that fixes it. type SmtpConfig = { provider: 'resend' | 'smtp' | 'mailhog'; @@ -32,36 +38,31 @@ type ResendTransport = { type: 'resend'; apiKey: string; from: string }; export type TransportResult = SmtpTransport | ResendTransport; export async function getTransport(): Promise { + // Only the read is guarded. Three outcomes that used to be one — the database is unreachable, nothing + // is configured, and the stored settings do not build a transport — say different things now, because + // the fix for each is different and the caller only ever sees this message. + let settings: Record; try { - const settings = await readServerSettings(); - const smtp: SmtpConfig | undefined = settings.smtp as SmtpConfig | undefined; - if (smtp) { - const from = `${smtp.fromName} <${smtp.fromEmail}>`; - - if (smtp.provider === 'resend') { - return { type: 'resend', apiKey: smtp.apiKey ?? '', from }; - } - - const hash = JSON.stringify(smtp); - if (cachedTransport && cachedConfigHash === hash) { - return { type: 'smtp', transport: cachedTransport, from }; - } - const url = buildTransportUrl(smtp); - cachedTransport = createTransport(url); - cachedConfigHash = hash; - return { type: 'smtp', transport: cachedTransport, from }; - } - } catch { - // Fall through to env var + settings = await readServerSettings(); + } catch (ex) { + throw new Error(`Could not read the SMTP settings: ${ex instanceof Error ? ex.message : String(ex)}`); } - if (MAIL_TRANSPORT) { - if (!cachedTransport || cachedConfigHash !== 'env') { - cachedTransport = createTransport(MAIL_TRANSPORT); - cachedConfigHash = 'env'; - } - return { type: 'smtp', transport: cachedTransport, from: null }; + const smtp: SmtpConfig | undefined = settings.smtp as SmtpConfig | undefined; + if (!smtp) throw new Error('No email transport configured — set one in Settings → Server → SMTP'); + + const from = `${smtp.fromName} <${smtp.fromEmail}>`; + + if (smtp.provider === 'resend') { + return { type: 'resend', apiKey: smtp.apiKey ?? '', from }; } - throw new Error('No email transport configured'); + const hash = JSON.stringify(smtp); + if (cachedTransport && cachedConfigHash === hash) { + return { type: 'smtp', transport: cachedTransport, from }; + } + const url = buildTransportUrl(smtp); + cachedTransport = createTransport(url); + cachedConfigHash = hash; + return { type: 'smtp', transport: cachedTransport, from }; }