// ── SMTP — the baseline mail transport ───────────────────────────────────── // // ENGAGEMENT.md decision 4: Gmail OAuth2 is removed, SMTP is the baseline. This // is the only registered transport, and it is deliberately plain SMTP rather than // anything provider-shaped — a relay (Mailgun, SES, Postmark), a self-hosted MTA // and Gmail-with-an-app-password are all reachable through these five fields, so // one transport covers all three postures §7.1 Q5 asks to document. // // **No defaults for host, port, user or sender.** §3.2 rule 1: a transport with // no operator configuration is unconfigured, never pointed at somewhere we chose. // `secure` gets a default because it is a protocol choice, not a destination — and // even that is only a form default, not a fallback applied to a stored blank. // // **`secure` is the field operators get wrong**, so its help text says which port // each setting means: `secure: true` is implicit TLS on 465, `secure: false` is // plaintext-then-STARTTLS on 587 (which nodemailer upgrades automatically). The // combination that silently fails is 587 with secure on — the handshake hangs // rather than erroring cleanly — which is exactly why "Send test" is the real // verification path now (§1.2a consequence 2). const nodemailer = require('nodemailer') const registry = require('./index') const CREDENTIAL_FIELDS = [ { key: 'host', label: 'SMTP host', kind: 'text', required: true, placeholder: 'smtp.example.com', help: 'Your relay or mail server. No default — nothing is sent until you set this.', }, { key: 'port', label: 'Port', kind: 'number', required: true, default: 587, help: '587 for STARTTLS (most relays), 465 for implicit TLS, 25 for an unauthenticated local MTA.', }, { key: 'secure', label: 'Implicit TLS', kind: 'boolean', required: false, default: false, help: 'On for port 465. Leave off for 587 — the connection still upgrades to TLS via STARTTLS.', }, { key: 'user', label: 'Username', kind: 'text', required: false, help: 'Leave blank for an unauthenticated local relay.', }, { key: 'password', label: 'Password / API key', kind: 'secret', required: false, help: 'Stored encrypted and never returned. For Gmail this is an app password, not the account password.', }, ] // Authentication is optional (a local MTA on port 25 needs none), so the only // hard requirement is a destination. A username without a password is not // "complete" — that combination authenticates as nobody and fails at the server. function isComplete(credential) { const c = credential || {} if (!c.host || !Number(c.port)) return false if (c.user && !c.password) return false return true } function build(credential) { const c = credential || {} const options = { host: String(c.host), port: Number(c.port), secure: Boolean(c.secure), } if (c.user) options.auth = { user: String(c.user), pass: String(c.password || '') } return nodemailer.createTransport(options) } registry.registerMailTransport({ id: 'smtp', label: 'SMTP', help: 'Any SMTP relay or mail server. See the operator guide for the three supported postures.', credentialFields: CREDENTIAL_FIELDS, isComplete, build, }) module.exports = { CREDENTIAL_FIELDS, isComplete, build }