// services/mailer.js // Thin nodemailer wrapper for stage-change email notifications (Production // module). SMTP credentials come from .env (SMTP_HOST/PORT/SECURE/USER/ // PASSWORD/FROM) — there is no SMTP config in the Admin UI. Until SMTP_HOST // is set, sendMail() is a safe no-op (logs and resolves) so the rest of the // notification pipeline (and the request that triggered it) never breaks // because email isn't configured yet. 'use strict'; const nodemailer = require('nodemailer'); let _transporter = null; function isConfigured() { return !!process.env.SMTP_HOST; } function getTransporter() { if (_transporter) return _transporter; _transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST, port: parseInt(process.env.SMTP_PORT) || 587, secure: String(process.env.SMTP_SECURE).toLowerCase() === 'true', auth: process.env.SMTP_USER ? { user: process.env.SMTP_USER, pass: process.env.SMTP_PASSWORD } : undefined, }); return _transporter; } const mailLog = () => require('./mailLogStore'); // Never throws — logs and resolves so a mail failure can't break the caller's // request. Returns true if actually sent, false if skipped/failed. Every // outcome (sent/failed/skipped) is also recorded to the Mail Log (see // services/mailLogStore.js, viewer at /mail-logs) so delivery can be // confirmed without digging through console output. async function sendMail({ to, subject, html, text }) { const recipients = Array.isArray(to) ? to.filter(Boolean) : [to].filter(Boolean); if (!recipients.length) return false; if (!isConfigured()) { console.log(`[MAILER] SMTP not configured — skipping email "${subject}" to ${recipients.join(', ')}`); await mailLog().record({ to: recipients, subject, status: 'SKIPPED', error: 'SMTP not configured' }); return false; } try { const info = await getTransporter().sendMail({ from: process.env.SMTP_FROM || process.env.SMTP_USER, to: recipients.join(', '), subject, html, text: text || undefined, }); console.log(`[MAILER] sent "${subject}" to ${recipients.join(', ')} (messageId: ${info.messageId})`); await mailLog().record({ to: recipients, subject, status: 'SENT', messageId: info.messageId }); return true; } catch (e) { console.error(`[MAILER] send FAILED for "${subject}" to ${recipients.join(', ')}:`, e.message); await mailLog().record({ to: recipients, subject, status: 'FAILED', error: e.message }); return false; } } // Checks the SMTP connection/credentials without sending anything — surfaces // the real error (bad host, auth rejected, etc.) rather than a generic // send failure. async function verifyConnection() { if (!isConfigured()) return { ok: false, message: 'SMTP_HOST is not set in .env' }; try { await getTransporter().verify(); return { ok: true }; } catch (e) { return { ok: false, message: e.message }; } } module.exports = { sendMail, isConfigured, verifyConnection };