← back to Butlr
wire nodemailer Purelymail SMTP transport for password-reset email
c3e3751889185e48d6d1038741a05ddcb45ec194 · 2026-05-19 12:50:18 -0700 · SteveStudio2
Files touched
M lib/reset-delivery.jsM package-lock.jsonM package.json
Diff
commit c3e3751889185e48d6d1038741a05ddcb45ec194
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date: Tue May 19 12:50:18 2026 -0700
wire nodemailer Purelymail SMTP transport for password-reset email
---
lib/reset-delivery.js | 50 ++++++++++++++++++++++++++++++++++++++------------
package-lock.json | 10 ++++++++++
package.json | 1 +
3 files changed, 49 insertions(+), 12 deletions(-)
diff --git a/lib/reset-delivery.js b/lib/reset-delivery.js
index dd7fa9b..566def1 100644
--- a/lib/reset-delivery.js
+++ b/lib/reset-delivery.js
@@ -3,12 +3,13 @@
// ── WHY THIS IS PLUGGABLE ──────────────────────────────────────────────
// As of this build Butlr accounts (lib/users.js) are keyed by EMAIL ONLY —
// there is no phone number on the user record, and signup only collects an
-// email. Butlr also has NO email-sending infrastructure wired (no
-// nodemailer, no SMTP, no transactional-email API key in .env). It DOES
-// have a working, DNC-gated Twilio SMS sender (lib/twilio.sendSms).
+// email. EMAIL is therefore the natural reset channel, and it is now wired:
+// the 'smtp' transport below sends via nodemailer over Purelymail SMTP
+// (mailbox noreply@agentabrams.com). Butlr also has a working, DNC-gated
+// Twilio SMS sender (lib/twilio.sendSms) kept as an alternate channel.
//
-// So neither channel is turnkey:
-// - EMAIL — the natural channel (accounts are email-keyed) but no sender.
+// Channel notes:
+// - EMAIL — wired (smtp transport); the default and recommended channel.
// - SMS — a working sender exists, but no phone is stored on accounts.
//
// This module therefore exposes ONE function, deliverResetLink(), and picks
@@ -41,8 +42,11 @@ function log(...a) { console.log('[reset-delivery]', new Date().toISOString(), .
// ── EMAIL ──────────────────────────────────────────────────────────────
// Transport is selected by RESET_EMAIL_TRANSPORT:
// 'log' (default) — prints the link to the server log (dev / not-yet-wired)
-// 'smtp' / 'api' — placeholder; throws so a misconfig fails loud, not
-// silently. Wire nodemailer or a provider SDK here.
+// 'smtp' — sends via nodemailer over SMTP. Needs RESET_SMTP_HOST,
+// RESET_SMTP_PORT, RESET_SMTP_USER, RESET_SMTP_PASS and
+// RESET_EMAIL_FROM. Wired for Purelymail SMTP
+// (smtp.purelymail.com:465, mailbox noreply@agentabrams.com).
+// 'api' — not implemented; throws loud rather than silently drop.
async function sendEmail({ to, subject, text }) {
const transport = (process.env.RESET_EMAIL_TRANSPORT || 'log').toLowerCase();
if (transport === 'log') {
@@ -50,12 +54,34 @@ async function sendEmail({ to, subject, text }) {
log(` ${text.split('\n').join('\n ')}`);
return { ok: true, transport: 'log' };
}
- // A real transport is requested but not implemented in this build.
- // Fail loud rather than silently dropping a password-reset email.
+ if (transport === 'smtp') {
+ const host = process.env.RESET_SMTP_HOST;
+ const user = process.env.RESET_SMTP_USER;
+ const pass = process.env.RESET_SMTP_PASS;
+ if (!host || !user || !pass) {
+ // Fail loud — never silently drop a password-reset email.
+ throw new Error(
+ 'RESET_EMAIL_TRANSPORT=smtp but RESET_SMTP_HOST / RESET_SMTP_USER / ' +
+ 'RESET_SMTP_PASS are not all set.'
+ );
+ }
+ const port = parseInt(process.env.RESET_SMTP_PORT, 10) || 465;
+ const nodemailer = require('nodemailer');
+ const tx = nodemailer.createTransport({
+ host,
+ port,
+ secure: port === 465, // 465 = implicit TLS; 587 = STARTTLS
+ auth: { user, pass }
+ });
+ const from = process.env.RESET_EMAIL_FROM || user;
+ const info = await tx.sendMail({ from, to, subject, text });
+ log(`EMAIL[smtp] sent to=${to} id=${info.messageId}`);
+ return { ok: true, transport: 'smtp' };
+ }
+ // 'api' or any unknown value — not implemented; fail loud.
throw new Error(
- `RESET_EMAIL_TRANSPORT=${transport} requested but no email transport is ` +
- `wired in lib/reset-delivery.js. Add nodemailer/provider SDK here, or ` +
- `set RESET_EMAIL_TRANSPORT=log for dev.`
+ `RESET_EMAIL_TRANSPORT=${transport} requested but only 'log' and 'smtp' ` +
+ `are implemented in lib/reset-delivery.js.`
);
}
diff --git a/package-lock.json b/package-lock.json
index 32b1444..ce73062 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,7 @@
"helmet": "^7.1.0",
"morgan": "^1.10.0",
"multer": "^2.1.1",
+ "nodemailer": "^8.0.7",
"ws": "^8.20.1"
},
"engines": {
@@ -758,6 +759,15 @@
"node": ">= 0.6"
}
},
+ "node_modules/nodemailer": {
+ "version": "8.0.7",
+ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.7.tgz",
+ "integrity": "sha512-pkjE4mkBzQjdJT4/UmlKl3pX0rC9fZmjh7c6C9o7lv66Ac6w9WCnzPzhbPNxwZAzlF4mdq4CSWB5+FbK6FWCow==",
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
diff --git a/package.json b/package.json
index f4792d8..3c34764 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
"helmet": "^7.1.0",
"morgan": "^1.10.0",
"multer": "^2.1.1",
+ "nodemailer": "^8.0.7",
"ws": "^8.20.1"
},
"engines": {
← 2606c4d Add password-reset test suite and wire into npm test
·
back to Butlr
·
Add external call-trigger API for first-party sites (NPH) f709144 →