Platform ·DarDev Team · 5 min read

Hybrid mail sender: Listmonk for lists, direct SMTP for automation

DarDev finished its hybrid mail roadmap: Listmonk keeps lists and subscribe forms; sync-worker sends daily digest and batch jobs via Stalwart SMTP with warm-up limits.

Architecture diagram of Listmonk list layer feeding sync-worker SMTP to Stalwart

DarDev completed its hybrid mail sender roadmap in June 2026: Listmonk remains the system of record for lists, subscribers, public subscribe forms, and manual UI campaigns, while sync-worker sends automated traffic—daily digest, drip jobs, programmatic batch sends—through direct SMTP to Stalwart with warm-up rate limits. Status tracker data/mail-roadmap-status.json marks complete: true; validation lives in scripts/mail/validate-mail-stack.sh.

The decision was never to replace Listmonk with another marketing suite. It was to stop forcing the Listmonk campaign engine to carry cron-driven batch jobs that need hours or days of throttled delivery. Both paths exit Stalwart on send.dardev.net. See inside-unified-mailer-stack for compose topology and why-send-dardev-subdomain for From-address rules.

What each layer owns

  • Listmonk — subscriber CRUD, double opt-in forms at mail.dardev.net, CRM webhook sync, templates, A/B tests, operator-initiated campaigns
  • sync-worker — Twenty staff sync, daily digest builder, mail-sender module, POST /api/mail/campaign job API
  • Stalwart — SMTP submission, outbound queue, dardev-warmup throttles, bounce handling

Listmonk still talks to Stalwart SMTP when an operator clicks send in the admin UI. sync-worker bypasses the campaign cursor machinery for automation: it reads list members via Listmonk API or database credentials, renders HTML, and submits messages one-by-one or in paced batches through nodemailer to stalwart:465 as news@send.dardev.net.

Milestone summary (M0 through M4)

M0 established baseline: MAIL_SEND_PROFILE=warmup documented, configure-production.sh applying Listmonk concurrency limits (3/3/1), Stalwart warmup.ndjson throttles at roughly 450 messages per day global and 50 per hour per recipient domain, and DAILY_SEND_MODE env wired in compose.

M1 shipped the reusable mail-sender.js module with mail-rate.js helpers, unit tests in sync-worker/test/, and SMTP auth from the worker network to Stalwart. sendToList({ listmonk, listId, subject, html, from, tags, delayMs, dryRun }) returns sent, failed, skipped, and errors arrays for observability.

M2 moved the internal daily digest to DAILY_SEND_MODE=direct. Cron at 08:00 Africa/Tunis calls POST /api/daily/send; staff list internal:staff receives news@send.dardev.net without creating a new Listmonk campaign row or polling campaign status for fifteen minutes. Dry-run completes in seconds; live send for roughly ten staff finishes well under five minutes.

M3 added programmatic campaign jobs: POST /api/mail/campaign with listId, subject, html, optional rate and dryRun; GET /api/mail/campaign/:jobId for progress. Jobs persist so a worker restart can resume; operators cancel runaway jobs without touching Listmonk subscriber data.

M4 ran full-stack sign-off: validate-mail-stack.sh, e2e-mail-stack.mjs, news subscribe test, Twenty webhook reachability, staff bidirectional sync, and confirmation that manual Listmonk UI campaigns still work for marketing operators who prefer the visual editor.

When to use which path

  • Product marketing campaign with template preview — Listmonk UI
  • Public subscribe form on news.dardev.net — Listmonk API; CRM webhook adds opted-in rows
  • Daily internal digest to staff — sync-worker direct SMTP
  • Services or Hesabi drip over days at warm-up pace — sync-worker campaign job API
  • One urgent manual blast to an opted-in list — Listmonk UI still valid if volume fits throttle

Goals unchanged after completion

Single sender identity news@send.dardev.net for DarDev company mail, warm-up toward roughly ten thousand messages per month, and support for large lists held in Listmonk but delivered over multi-day worker jobs while Stalwart queues excess. Per-product outbound—send.hesabi.tn first via manifest mail.fromDomain—extends the same pattern without collapsing everything into one From domain.

Multi-provider gate M20 adds smtp-external connector for solutions that must relay through a third party; DarDev company stack stays on self-hosted Stalwart documented in self-hosted-email-vs-saas.

Validation commands operators run

  1. wsl bash scripts/mail/test-milestone.sh M0 — baseline throttles and SMTP auth
  2. cd sync-worker && npm test — mail-sender and mail-rate unit tests
  3. DRY_RUN=1 wsl bash scripts/mail/send-daily.sh — digest recipient preview
  4. wsl bash scripts/mail/validate-mail-stack.sh — full regression before deploy
  5. wsl bash scripts/mail/e2e-mail-stack.mjs — subscribe and webhook paths

Rollback plan

If direct send fails in production, set DAILY_SEND_MODE=campaign and use recover-daily-send.sh for the digest path. If a worker job misbehaves, pause jobs and fall back to a manual Listmonk campaign for urgent communication. If Stalwart throttles are too tight, adjust stalwart/production/warmup.ndjson and re-apply—never disable warm-up entirely before reputation is established.

What we are not doing post-M4: replacing Listmonk with Keila or Mautic unless a future regression forces reconsideration; batch automation via Listmonk TX; or disabling Stalwart throttles for convenience.

Operator workflow after completion

Marketing operators still log into mail.dardev.net for template edits and manual sends. Platform engineers own sync-worker env vars—DAILY_SEND_MODE, MAIL_JOB_SECRET, LISTMONK_DB_HOST—and cron on dardev-vps. When a new automated drip ships, the change lands in sync-worker code and scripts/mail/, not a Listmonk database migration. That separation keeps subscriber data stable while send logic evolves.

Before each release touching mail-sender.js, run npm test locally and validate-mail-stack.sh on the VPS staging path. Regression on M2 daily digest blocks the deploy: staff expect the 08:00 Africa/Tunis message even when product campaigns pause.

Is the hybrid sender roadmap still in progress?

No. data/mail-roadmap-status.json shows complete: true as of 2026-06-06. M0–M4 validation suites are the ongoing guardrail.

Does Listmonk still send anything?

Yes. Operators use Listmonk UI for product marketing campaigns. Only automated cron and long-running batch jobs moved to sync-worker direct SMTP.

How does the daily digest send now?

DAILY_SEND_MODE=direct: sync-worker builds HTML and paces SMTP to Stalwart as news@send.dardev.net. No Listmonk campaign row required.

Can a 100k list send in one hour?

Not during warm-up. Listmonk holds data; sync-worker drips at MAIL_SEND_DELAY_MS and Stalwart throttle limits over hours or days.

Where is troubleshooting documented?

docs/runbooks/TROUBLESHOOTING.md and scripts/mail/README.md include pool timeout and direct-versus-campaign decision trees.

Get company news

Releases and announcements — confirm from your inbox.

Subscribe to updates