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 }; }