Listmonk SMTP Setup (and Mautic's Mailer DSN) with JustEmails
Wiring Listmonk SMTP and Mautic to a JustEmails mailbox — the settings block that isn't in config.toml, the four numbers that stop the throttling, and the list sizes this honestly suits.
Wiring Listmonk SMTP and Mautic to a JustEmails mailbox — the settings block that isn't in config.toml, the four numbers that stop the throttling, and the list sizes this honestly suits.
11:40 PM on a Tuesday. Fresh Listmonk SMTP settings pointed at a JustEmails mailbox. I hit Start on the campaign — 3,400 subscribers on a side-project newsletter I'd been growing for about a year — and went to make tea. Came back four minutes later to a campaign sitting at paused, fewer than a hundred messages delivered, and one log line repeating itself down the terminal:
error sending message: dial tcp: i/o timeout
max errors exceeded; campaign paused
Credentials were fine. Relay was fine. Listmonk's defaults had opened ten simultaneous SMTP connections and started firing at roughly ten messages a second — a completely reasonable default if you own the mail server, and a great way to get throttled if you don't.
Two more failed sends before I worked out the fix was four numbers on a settings page.
So here's the whole thing: pointing Listmonk and Mautic at a JustEmails mailbox, the concurrency knobs nobody documents together, and the part most integration guides skip — which lists this is appropriate for. Because it isn't all of them. Not even close.
Self-hosted Listmonk (or Mautic) handling subscribers, segments, templates and campaign scheduling, with the actual delivery handed to an authenticated JustEmails mailbox over SMTP on port 587, signed with DKIM.
Split of duties, roughly:
| Job | Owner |
|---|---|
| Subscriber lists, segments, double opt-in | Listmonk / Mautic |
| Template rendering, merge tags, scheduling | Listmonk / Mautic |
| SMTP auth, TLS, DKIM signing, outbound reputation | JustEmails |
| Bounce and complaint handling | Shared — and this is where setups break |
By the end: Listmonk campaigns landing with spf=pass dkim=pass dmarc=pass, a send rate that trips nothing, and a clear answer on whether your list belongs here at all.
Read this part before you open a config file, because it decides whether the build is worth doing at all.
Every number later in this post is chosen to fit inside 500 a day and 100 recipients an hour. If your list needs more than that, the last section tells you where to send it instead — and it might save you an evening.
dkim=none twenty minutes after adding a domain is propagation, not misconfiguration.nc -zv smtp.justemails.app 587. Most VPS providers block 25 by default and a handful quietly filter 587 too, which is the one that stings because every guide tells you 587 is the safe port. The breakdown of 25, 465, 587 and 993 covers which TLS mode each one wants.Make a mailbox that does nothing but send campaigns. Not the one you read mail in.
newsletter@mail.yourdomain.com
Two reasons, and the second is the one people learn the hard way. Credentials in a config file on an internet-facing app host are credentials you'll want to rotate without locking yourself out of your own inbox. And bulk sending and your business correspondence build separate reputations — a rough campaign shouldn't put your invoices in spam.
Mailboxes are unlimited on the single $49/year plan, so the dedicated one costs nothing. Domains are unlimited too, which is why the subdomain approach is free rather than an upsell. If you haven't split sending off yet, the dedicated sending subdomain writeup covers the DNS side.
One opinion, and plenty of people run it the other way: don't send campaigns from noreply@. Someone will reply to your newsletter, that reply is the highest-signal message you'll get all month, and noreply@ throws it in a bin. We've argued that case at length.
This trips up almost everyone, so before the settings themselves: on Listmonk v2 and later, SMTP configuration is not in config.toml. That file holds the app address and the Postgres connection. Everything else lives in the admin UI under Settings → SMTP, stored in the database.
A lot of still-indexed guides predate that move. You'll edit a [[smtp]] block, restart, and watch nothing change — which sends you debugging the relay instead of the file you're looking at.
Moving SMTP config out of the file and into Postgres was the right call, I think, and a documentation disaster at the same time. Years of stale tutorials, most of them still outranking the real docs. That one still irritates me.
In Settings → SMTP, enable the first block and fill in:
| Field | Value |
|---|---|
| Host | smtp.justemails.app |
| Port | 587 |
| Auth protocol | plain (try login if auth fails on a password you know is right) |
| Username | newsletter@mail.yourdomain.com — the full address |
| Password | your mailbox password |
| TLS | STARTTLS |
| Skip TLS verification | off |
| HELO hostname | mail.yourdomain.com |
| Max connections | 3 |
| Retries | 2 |
| Idle timeout | 15s |
| Wait timeout | 5s |
Four of those deserve a sentence.
Username is the full address. I have typed newsletter where newsletter@mail.yourdomain.com belonged more times than I'd like on the record, and the auth failure reads identically either way.
STARTTLS on 587, SSL/TLS on 465 — and here it's STARTTLS. Port 587 opens in the clear and upgrades; 465 is wrapped from the first byte. JustEmails publishes 587 only, so SSL/TLS isn't an alternative setting in this dropdown, it's just the wrong one: pick it while pointing at 587 and the handshake hangs until timeout with no useful error.
Leave "skip TLS verification" off. A hundred forum threads recommend it for certificate errors, and it "fixes" them by not checking certificates — which turns an encrypted connection into a costume. A cert error means a missing CA bundle. Fix that instead.
Max connections is the number that mattered in my 11:40 PM story. Default is 10. On a shared relay, 3 is plenty.
Now Listmonk's Settings → Performance, which is a separate page from SMTP and is why people tune one and not the other.
| Setting | Default | Use |
|---|---|---|
| Concurrency | 10 | 3 |
| Message rate | 10 | 2 |
| Batch size | 1000 | 1000 |
| Max error threshold | 1000 | 100 |
Concurrency is how many workers push messages at once; message rate is messages per second per worker. Multiply them — stock settings aim at roughly 100 messages a second. On a relay you share, that's not ambitious, it's a denial-of-service impression.
At 3 × 2 you're at about six a second, which is a sane burst rate for the connection limits. It is not your throughput. Throughput is set by the account ceilings, and the sliding window is where you encode them:
Enable sliding window: on
Duration: 1h
Max messages: 80
Eighty an hour sits under the 100-distinct-recipients-an-hour cap with a little room for retries. Listmonk gives you one window, not two, so the daily ceiling is a scheduling decision rather than a setting: 80 an hour across a six-hour sending window is 480 messages a day, which fits under the 500-a-day account cap and leaves a margin for the mail you actually needed to send that day. Run the same 80 an hour around the clock and you're asking for 1,920 a day — the account cap stops you about a quarter of the way in, mid-campaign, with a partial send and no clean record of who got a copy.
So the number worth internalising: a 3,400-subscriber campaign is not a ten-minute job here. At 480 a day it lands over roughly seven days. That's the honest pace, and it's why the list-size question at the end of this post matters more than any setting on this page.
Per-second limits protect the relay from a burst. The sliding window protects you from yourself over a weekend of enthusiastic scheduling.
Lower the max error threshold to 100 too. The default of 1000 means a campaign will hammer away through a thousand consecutive failures before pausing, and there is no scenario where failure 400 was going to work out.
Mautic 5 moved to Symfony Mailer, which means one DSN string instead of the six separate keys Mautic 4 used. In config/local.php:
'mailer_dsn' => 'smtp://news%40mail.yourdomain.com:YOUR_PASSWORD@smtp.justemails.app:587',
'mailer_from_email' => 'news@mail.yourdomain.com',
'mailer_from_name' => 'Acme Weekly',
Percent-encode the username. A DSN is a URI. The @ in an email-address username has to be %40 or the parser splits on the wrong one and tries to resolve a host that doesn't exist. Same for any #, :, /, ? or @ in the password. A DSN that looks completely correct and fails instantly with a host error is nearly always this.
Scheme smtp:// on port 587 negotiates STARTTLS on its own, which is what you want here. Don't reach for smtps:// — that scheme means implicit TLS on 465, and JustEmails publishes no implicit-TLS port, so the DSN would resolve correctly and then stall on a socket nothing is listening to.
On Mautic 4 the same thing is six keys — mailer_transport, mailer_host, mailer_port, mailer_user, mailer_password, mailer_encryption set to tls — and no encoding needed, since they're separate fields rather than one URI. The Configuration → Email Settings UI writes either version for you, but it caches aggressively, so run php bin/console cache:clear after any change. I lost most of an evening to that once — rewriting a DSN that had been correct the whole time, over and over, because Mautic kept handing me back the old one. Clear the cache first. Then doubt your config.
Don't send immediately. In Configuration → Email Settings turn the queue on, and in config/local.php:
'messenger_dsn_email' => 'doctrine://default',
Then the cron entries that actually move mail:
*/5 * * * * php /path/to/mautic/bin/console messenger:consume email --time-limit=280
*/15 6-11 * * * php /path/to/mautic/bin/console mautic:broadcasts:send --limit=20
0 * * * * php /path/to/mautic/bin/console mautic:campaigns:trigger
messenger:consume drains queued mail — on Mautic 4 it was mautic:emails:send against a file spool. The --time-limit stops the worker running forever and leaking memory.
--limit on the broadcast command is your real throttle, and it has to be sized against two ceilings rather than one. Twenty messages every fifteen minutes is four runs an hour, so 80 an hour — under the 100-distinct-recipients-an-hour cap, and the same figure the sliding window in Step 3 uses. Restricting that cron to a six-hour window (the 6-11 hours field) makes it 24 runs a day, so 480 messages, under the 500-a-day account cap.
Leave it as */15 * * * * around the clock and the same --limit=20 asks for 1,920 a day. The relay will accept the first few hundred and then start refusing, Mautic will retry, and you'll be reading bounce logs trying to work out which subscribers actually received the thing. Size the cron so it cannot breach either ceiling, and the ceilings never become your problem.
Immediate sending skips all of that and hands Mautic's web process the job of talking SMTP during a page request. Works fine until a campaign. Then it doesn't.
Here's the part I'd want told to me.
JustEmails is transactional-first, and the send caps are the yardstick, not the API allowance. 500 messages a day per account once you're past the first week, 20 a day before that, and 100 distinct recipients an hour. The included transactional allowance — 1,000 API emails a month on the $49/year plan, stackable in 10,000/month tiers at $25/year each — tells you the same story from the other direction. This isn't a bulk marketing tier in disguise: transactional infrastructure optimises for per-message reliability, bulk infrastructure optimises for throughput and per-campaign reputation isolation.
Which makes the sizing arithmetic simple. A weekly send has seven days to clear, and 500 a day is the wall:
| List | Weekly send | Needs per day, spread over the week | Verdict |
|---|---|---|---|
| 500 | 500 | ~72 | Comfortable. About an hour of paced sending a day, and most of the daily allowance is still free for ordinary mail |
| 2,500 | 2,500 | ~360 | Possible, but only paced across all seven days — and it consumes most of the 500-a-day allowance, so nothing else heavy can share the account |
| 8,000 | 8,000 | ~1,150 | Exceeds the daily cap. Not possible on this account — 7 days at 500 is 3,500 messages, full stop |
| 50,000 | 50,000 | ~7,150 | Wrong tool. Not close |
The hard line falls at 3,500 a week — seven days times the 500-a-day cap — and you want to be well under it, because everything else the account sends counts against the same number: password resets, invoices, the receipt emails your app fires. A list you can mail comfortably is one whose weekly send is a fraction of that, not a match for it.
And the opt-in standard matters more than the number. Confirmed, double opt-in subscribers who asked for your specific newsletter. Not a conference badge scan. Not a list you bought. Not "everyone who ever created an account." A 400-person purchased list will damage a sending domain faster than a clean 40,000-person one ever could, and no relay saves you from that.
So the contrarian bit, and I'd rather say it than have you find out in a month: if your weekly send is anywhere near 3,500, or you want to mail more often than weekly, run Listmonk in front of Amazon SES instead. Listmonk stays your list manager — it's very good at that, and its per-message cost is zero. SES is built for the throughput shape you're in. Sending this advice from an email hosting company's blog is a little odd, but recommending the wrong tool and collecting the complaints later is worse.
Two more things, whatever size you land on:
List-Unsubscribe with one-click support. Listmonk and Mautic both emit the header, and Gmail and Yahoo both require it above modest volumes now. Read the current rules rather than assuming you already know them — the one-click unsubscribe rules changed in a way that caught a lot of senders out.Campaign pauses partway with max errors exceeded
Concurrency and max connections, in that order. Step 3. The relay isn't refusing you outright — per-connection limits produce intermittent failures that pile up against the error threshold until Listmonk gives up.
dial tcp: i/o timeout on every single message
Nothing to do with config. The host is blocking outbound. Run the nc -zv check from Prerequisites.
Mautic: Unable to connect with TCP, or a host that doesn't exist
An unencoded @ in the DSN username. %40. Every time.
Auth fails on a password you're certain is correct
Confirm the username is the full address, not the local part. Then switch auth protocol from plain to login. Still failing? Check the password for a # — depending on how it landed in the config file, everything after that character may have been read as a comment.
Everything sends, but Gmail files it under Promotions or worse
Not an SMTP problem. Check a test send's raw headers for spf=pass dkim=pass dmarc=pass first. If all three pass, it's send shape: a young domain that sent nine messages last week and four hundred this morning looks exactly like a domain that just changed hands. Warm up over a few weeks, most engaged subscribers first.
They moved. On Listmonk v2 and later, config.toml only holds the app address and the Postgres connection; every SMTP field lives in Settings → SMTP in the admin UI and is stored in the database. Half the guides still online predate that change, which is why people edit a TOML block, restart, and watch nothing happen. If you want it in version control, export the settings JSON from the API rather than editing the file.
Start at concurrency 3, message rate 2, max connections 3, and leave batch size at 1000. The stock values are concurrency 10 and message rate 10, which assume you own the mail server. On any shared relay those defaults trip connection limits, the errors accumulate against the max error threshold, and the campaign pauses itself partway through. Slower finishes; a paused campaign doesn't.
Percent-encode it. The DSN is a URI, so the @ in the username has to become %40 or the parser reads the wrong host — smtp://news%40yourdomain.com:password@smtp.justemails.app:587. Same rule for any #, :, /, ? or @ in the password. A DSN that looks right but fails instantly with a host-resolution error is almost always an unencoded character.
No, and I wouldn't pretend otherwise. The account ceiling is 500 messages a day once a domain is past its first fortnight — 20 a day in that domain's first week, 200 a day in the second — with distinct recipients capped at 100 an hour, so even a settled account clears only about 3,500 messages across a full week. The included transactional allowance is transactional-shaped too: 1,000 API emails a month on the $49/year plan, with stackable 10,000/month tiers at $25/year each. That suits a confirmed opt-in list in the low thousands, mailed weekly and paced across the week. A 50,000-subscriber weekly send is 200,000 messages a month and belongs on a bulk sending backend built for it, with Listmonk still running list management in front.
Unlimited custom domain email hosting for $49/year flat — unlimited domains, unlimited mailboxes, 10 GB storage, full IMAP/SMTP. Built for agencies, freelancers, and anyone managing email across more than one domain.