From 7f2d854529fb8f476c48ff2a5468b11e1f61500f Mon Sep 17 00:00:00 2001 From: ionous Date: Wed, 10 Jun 2026 01:02:36 -0700 Subject: [PATCH 1/2] add support for a simple email block list - added isDeadLetter() helper - exposed the DEAD_LETTERS env var to docker - removed logging from inside emailer, so that caller can choose what to write to the log --- app/emailer.js | 23 ++++++++++------------- app/endpoints/manage_event.js | 16 ++++++++++++---- app/test/manage_test.js | 12 ++++++++++++ docker-compose.yml | 1 + 4 files changed, 35 insertions(+), 17 deletions(-) diff --git a/app/emailer.js b/app/emailer.js index c4dbcd04b..325172e0e 100644 --- a/app/emailer.js +++ b/app/emailer.js @@ -17,20 +17,17 @@ module.exports = { Promise.reject("smtp not configured.") : transporter.verify().then(_ => config.smtp.host || "???"); }, - // returns a promise after sending the email and logging the trailing arguments + // returns an empty promise after sending the email. // see https://nodemailer.com/message/ - sendMail(email, ...logArgs) { - return transporter.sendMail(email).then(info => { - const date = dt.getNow().toString(); - const logMessage = `Sent email ${date}:\n` + JSON.stringify(logArgs, null, " "); - console.log(logMessage); - return Promise.resolve(true); // don't log to the file for now; conflicts with php/node paths - // tbd: would it be better to log to console only, and configure docker with "local" - // it does compression, and auto rotation. - // https://docs.docker.com/config/containers/logging/configure/ - // const logFile = config.email.logfile; - // return !logFile ? Promise.resolve(true) : - // fsp.writeFile(config.email.logfile, logMessage+"\n", {flag: 'a'}); + sendMail(email) { + return transporter.sendMail(email).then(_ => { + return Promise.resolve(true); }); + }, + + // returns true if the passed email address was on the block list. + isDeadLetter(address) { + const parts = (process.env.DEAD_LETTERS || "").split(";").filter(Boolean); + return parts.some(p => address.endsWith(p)); } }; diff --git a/app/endpoints/manage_event.js b/app/endpoints/manage_event.js index 343757747..0e39ccf50 100644 --- a/app/endpoints/manage_event.js +++ b/app/endpoints/manage_event.js @@ -135,8 +135,10 @@ function updateEvent(evt, values, statusList) { // promises a sent email // evt is a CalEvent; dailies is an array of CalDaily. function sendConfirmationEmail(evt, dailies) { + const isDeadLetter = emailer.isDeadLetter(evt.email); + const url = config.site.url('addevent', `edit-${evt.id}-${evt.password}`); - const subject = `Shift2Bikes Secret URL for ${evt.title}`; + const subject = !isDeadLetter ? `Shift2Bikes Secret URL for ${evt.title}` : `Shift2Bikes Blocked ${evt.title}`; console.debug("sending confirmation for", url); const dates = dailies @@ -161,16 +163,22 @@ function sendConfirmationEmail(evt, dailies) { return emailer.sendMail({ subject, text: body, - to: { + to: !isDeadLetter ? { name: evt.name, address: evt.email, - }, + } : config.email.moderator, from: config.email.sender, replyTo: config.email.support, bcc: config.email.moderator, // backup copy for debugging and/or moderating // html // attachments - }, evt.name, evt.email, evt.title, url); + }).then(_ => { + // log after sending: + const date = dt.getNow().toString(); + const header = JSON.stringify([evt.name, evt.email, evt.title, url], null, " "); + const logMessage = [date, header, body].join("\n"); + console.log(!isDeadLetter ? "Sent email" : "** Blocked ** email", logMessage); + }); } // read json into a javascript object. diff --git a/app/test/manage_test.js b/app/test/manage_test.js index 6d1b88650..0a57cb549 100644 --- a/app/test/manage_test.js +++ b/app/test/manage_test.js @@ -26,6 +26,7 @@ const { CalDaily } = require("../models/calDaily"); const { describe, it, beforeEach, afterEach } = require("node:test"); const assert = require("node:assert/strict"); const request = require('supertest'); +const emailer = require("../emailer"); // const manage_api = '/api/manage_event.php'; @@ -69,6 +70,17 @@ describe("managing events", () => { // console.log(res.body); }); }); + it("tests dead letters", () => { + process.env.DEAD_LETTERS = "example.com"; + assert.equal(emailer.isDeadLetter("bob@example.com"), true); + assert.equal(emailer.isDeadLetter("bob@example.net"), false); + assert.equal(emailer.isDeadLetter("bob@example.org"), false); + process.env.DEAD_LETTERS = "example.net;.org"; + assert.equal(emailer.isDeadLetter("bob@example.com"), false); + assert.equal(emailer.isDeadLetter("bob@example.net"), true); + assert.equal(emailer.isDeadLetter("bob@shifty.org"), true); + delete process.env.DEAD_LETTERS; + }); it("fail creation when missing required fields", () => { // each time substitute a field value that should fail const pairs = [ diff --git a/docker-compose.yml b/docker-compose.yml index a44c2b458..fe58f56a6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -75,6 +75,7 @@ services: - SMTP_HOST - SMTP_USER - SMTP_PASS + - DEAD_LETTERS # by default the node image will run "node" # https://github.com/nodejs/docker-node/blob/main/docker-entrypoint.sh # override to run npm start; which auto installs from package-lock.json From 8a508fcb4f760892ea1eb3fe519d3c0f4d17c314 Mon Sep 17 00:00:00 2001 From: ionous Date: Wed, 10 Jun 2026 01:03:13 -0700 Subject: [PATCH 2/2] remove body logging too --- app/endpoints/manage_event.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/endpoints/manage_event.js b/app/endpoints/manage_event.js index 0e39ccf50..312004132 100644 --- a/app/endpoints/manage_event.js +++ b/app/endpoints/manage_event.js @@ -159,7 +159,6 @@ function sendConfirmationEmail(evt, dailies) { help: config.site.helpPage(), support: support.address || support, // a string or object }); - console.debug("confirmation email body:\n", body); return emailer.sendMail({ subject, text: body,