The send worker used to write "SMTP send returned false (see hub logs)" on every failure, forcing the operator to SSH into the box to find the actual cause. Now we capture the real reason and surface it in the UI. Three changes: 1. lib/email.js exposes getLastError() — a side-channel for the most recent nodemailer error message, cleared at the start of every sendEmail call. Legacy "if (await sendEmail(...))" callers stay on the false-return contract; only the campaign worker reads the side-channel for detailed error capture. 2. The worker now retries each recipient up to 3 times (initial + 2 retries with 2s/5s backoff). Most "Unexpected socket close"-style transient Mailjet errors recover on the second attempt. We observed exactly this case for Myriam Bergevin in cmp-20260522-2d4605 — a single socket close interrupted 1 of 202 sends; auto-retry would have caught it. retry_count is now stored on the recipient. 3. POST /campaigns/:id/recipients/:row/retry resets a single failed row back to pending and re-fires the worker. Surfaced in the detail-page table as a small 🔁 button next to the error text on any row with status=failed. Useful when auto-retry exhausted its 3 attempts on a one-off transient. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| data | ||
| lib | ||
| preview | ||
| public | ||
| scripts | ||
| templates | ||
| .env.example | ||
| docker-compose.yml | ||
| package-lock.json | ||
| package.json | ||
| server.js | ||