From 68c967f9bbc388a3a4605a13d440389929e2561b Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Wed, 3 Jun 2026 19:40:36 +0200 Subject: [PATCH] =?UTF-8?q?fix(email):=20recover=20stuck=20queue=20?= =?UTF-8?q?=E2=80=94=20reinit=20transporter=20on=20config=20save=20+=20man?= =?UTF-8?q?ual=20flush=20ignores=20retry=20cap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two gaps left emails stuck 'pending' after (re)configuring SMTP: 1. Saving the email config never re-initialised the transporter. The queue processor only re-inits when its cached transporter is null, so a changed SMTP account had no effect until a backend restart. Now call initializeTransporter(true) after save (it self-catches; invalid config just leaves it null, surfaced via the Test-email button). 2. The manual 'send now' flush (ignoreSchedule) still enforced retry_count<3, so emails that failed 3× while SMTP was broken could never be retried from the UI. Move the retry-cap (and schedule gate) to automatic runs only; a manual flush forces a retry of every pending email. --- backend/src/routes/adminEmail.js | 10 ++++++++++ backend/src/services/emailProcessor.js | 11 ++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/backend/src/routes/adminEmail.js b/backend/src/routes/adminEmail.js index acc37156..d880b884 100644 --- a/backend/src/routes/adminEmail.js +++ b/backend/src/routes/adminEmail.js @@ -94,6 +94,16 @@ router.post('/config', [ await db('email_configs').insert(configData); } + // Refresh the cached transporter so the new SMTP settings take effect + // immediately. Without this, a previously-initialised transporter stays + // cached (the queue processor only re-inits when it's null), so changing + // the email account had no effect until a backend restart — emails kept + // failing against the old/empty config. initializeTransporter catches its + // own errors and returns null, so this never throws; an invalid config + // simply leaves the transporter null (surfaced via the Test-email button). + const { initializeTransporter } = require('../services/emailProcessor'); + await initializeTransporter(true); + // Log activity await logActivity('email_config_updated', { smtp_host, from_email }, diff --git a/backend/src/services/emailProcessor.js b/backend/src/services/emailProcessor.js index 6b6308d3..0e9b2412 100644 --- a/backend/src/services/emailProcessor.js +++ b/backend/src/services/emailProcessor.js @@ -774,13 +774,18 @@ async function processEmailQueue({ ignoreSchedule = false, limit = 10 } = {}) { // queue split-payment emails relative to the event date. const now = new Date(); const query = db('email_queue') - .where('status', 'pending') - .where('retry_count', '<', 3); + .where('status', 'pending'); if (!ignoreSchedule) { - query.andWhere(function() { + // Automatic runs: respect the retry cap (don't hammer a failing + // address) AND the schedule (business-hours floor / future send). + query.where('retry_count', '<', 3).andWhere(function() { this.whereNull('scheduled_at').orWhere('scheduled_at', '<=', now); }); } + // A manual "send now" (ignoreSchedule) deliberately bypasses BOTH the + // schedule and the retry cap: the admin is forcing a retry, typically + // right after fixing SMTP. Without this, emails that failed 3× during + // an SMTP outage are stuck "pending" forever with no way to resend. pendingEmails = await query .orderBy('scheduled_at', 'asc') .orderBy('created_at', 'asc')