Emails from certain providers not being received

Issue Description

When sent an email from some providers I don’t get anything in my inbox even though it’s shown as sent on their end.

Expected Behavior

Email accepted by server and delivered to my inbox

Actual Behavior

Email does not arrive and the logs are not showing much, however Claude thinks this is the cause:

Every single Spamhaus DBL lookup in this log returns the identical result, regardless of domain

I checked all 424 queries to *.dbl.spamhaus.org across 272 distinct domains — including massive, obviously-legitimate senders like hotmail.co.uk, docusign.net, and kajabi.com. Every one of them comes back 127.255.255.254.

That code isn’t Spamhaus saying “this domain is spam” — it’s Spamhaus’s error response for queries arriving via a public DNS resolver. Since April 2024 Spamhaus blocks their free DNSBL mirrors from resolvers like 8.8.8.8 or 1.1.1.1 (and most VPS providers’ default resolvers) to stop abuse of the free tier. Stalwart is treating that error/refusal as “yes, this domain is listed” and scoring every message accordingly. multi.uribl.com shows the same pattern (346/388 non-null results all 127.0.0.1).

Reproduction Steps

  1. Get sent an email by an outlook or Hotmail email address
  2. Never get the email

Stalwart Version

v0.16.x

Installation Method

Binary (Linux)

Database Backend

RocksDB

Blob Storage

RocksDB

Search Engine

Internal

Directory Backend

Internal

I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.

on

I have searched this support forum (open and closed topics) and confirm this is not a duplicate.

on

I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.

on

This confirms the diagnosis: Stalwart is almost certainly configured to use Cloudflare, Quad9, Google, or another public DNS resolver, and Spamhaus’s free DNSBL zones return 127.255.255.254 to public resolvers as an anti-abuse block (not a real listing). If Stalwart’s DNSBL logic treats any 127.x.x.x A-record response as a match, this will cause every message to be scored as blocklisted on every domain, resulting in silent rejection.

The fix is to switch the DNS resolver to your own recursive/authoritative-forwarding resolver (e.g. an unbound/bind instance you run, or your hosting provider’s non-shared resolver) rather than a public one like Cloudflare/Google/Quad9, since the docs explicitly warn that most public DNSBL services reject open-resolver queries.

To fix:

  1. Go to Settings → Network → DNS → DNS Resolver in the WebUI (or the DnsResolver singleton via the API/CLI).
  2. Change the variant from Cloudflare/Google/Quad9/System (if your system resolver forwards to a public one) to Custom, pointing at a resolver you control that isn’t shared/rate-limited by Spamhaus (a local recursive resolver like Unbound is recommended).
  3. Once queries originate from a resolver with a unique, non-abused IP, Spamhaus DBL and other RBLs should return correct results instead of the 127.255.255.254/127.0.0.1 abuse-block codes.

You can verify by manually querying, e.g., dig hotmail.co.uk.dbl.spamhaus.org from the mail server itself using the same resolver Stalwart is configured to use, and confirming whether it returns the error code even for clean domains.

Documentation: DNS Resolver | Stalwart

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong.

Setting a different DNS Provider or even using my internal unbound setup has not worked.

Stalwart already decodes the Spamhaus refusal codes: 127.255.255.254 maps to a “blocked, open resolver” tag with a weight of 0.0, and the 127.0.0.1 you see from multi.uribl.com maps to a “query refused” tag, also 0.0. Neither contributes anything to the score, so they are not why mail is going missing.

What the uniform result does tell you is real: your resolver is blocked by Spamhaus, so you are getting no signal from those lists at all. A local unbound still shows this if it forwards to a public resolver rather than recursing from the root, and it shows it regardless if your server’s own egress IP falls in a range Spamhaus blocks for free-tier use. Worth fixing for the sake of the signal, but it will not change delivery either way.

More to the point, on a near-default config the reject and discard thresholds are both 0, which means disabled, so the worst the spam filter can do is file into Junk. A message that never appears anywhere is not a spam-score outcome. Can you pull the full log for one failing message keyed on the sender’s IP rather than just the DNSBL lines? What matters is whether there is an inbound session from a Microsoft address at all (smtp.connect-start, smtp.mail-from, smtp.rcpt-to), and if there is, whether it ends in message-ingest.ham, message-ingest.duplicate or a rejection. If there is no session at all then the problem is upstream of Stalwart and no amount of filter tuning will touch it.

Pulled the session data, Microsoft’s connecting IPs across all of today’s traffic:

  • Several inbound SMTP sessions from *.protection.outlook.com today
  • None of them ever issued MAIL FROM

Every session follows the same shape: connect → EHLO → SPF check (passes) → STARTTLS → TLS handshake completes successfully → immediate QUIT. No exceptions in the full day’s log. So there’s no rejection or discard happening on our end.

Microsoft’s servers are also repeatedly requesting /.well-known/mta-sts.txt (roughly every 10–20 min, same time window as the failed sessions), and that request 404s:

curl -I https://mta-sts.my-domain.co.uk/.well-known/mta-sts.txt
HTTP/1.1 404 Not Found

So the domain has an MTA-STS DNS record pointing at it, but there’s no policy file for them to actually fetch.

I tried setting the MTA-STS policy in the WebUI (Testing mode, 7-day max lifetime) and saving, but the 404 persists after save. I have automatic DNS management setup with Porkbun.

Could be it’s not actually persisting, or there’s a separate step to actually publish/serve it. Any chance you know where that’s supposed to land, or whether there’s a step past the WebUI form?

Total Outlook EHLO sessions:
4945

Of those IPs, how many ever appear in a smtp.mail-from line:
0

Logs:

SMTP session traces:

2026-07-28T23:57:40Z DEBUG SMTP connection ended (smtp.connection-end) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.32.74, remotePort = 46452, elapsed = 222ms
2026-07-28T23:58:39Z DEBUG SMTP connection started (smtp.connection-start) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.20.48, remotePort = 23126
2026-07-28T23:58:39Z INFO SMTP EHLO command (smtp.ehlo) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.20.48, remotePort = 23126, domain = “CH5PR02CU005.outbound.protection.outlook.c o m”
2026-07-28T23:58:39Z INFO SPF EHLO check passed (smtp.spf-ehlo-pass) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.20.48, remotePort = 23126, domain = “CH5PR02CU005.outbound.protection.outlook.c o m”, result = SPF check passed (spf.pass), elapsed = 149ms
2026-07-28T23:58:40Z DEBUG SMTP STARTTLS command (smtp.start-tls) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.20.48, remotePort = 23126
2026-07-28T23:58:40Z INFO TLS handshake (tls.handshake) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.20.48, remotePort = 23126, listenerId = “smtp”, version = “TLSv1_3”, details = “TLS13_AES_256_GCM_SHA384”
2026-07-28T23:58:40Z DEBUG SMTP QUIT command (smtp.quit) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.20.48, remotePort = 23126
2026-07-28T23:58:40Z DEBUG SMTP connection ended (smtp.connection-end) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.20.48, remotePort = 23126, elapsed = 777ms
2026-07-28T23:58:40Z DEBUG SMTP connection started (smtp.connection-start) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.33.21, remotePort = 45993
2026-07-28T23:58:40Z INFO SMTP EHLO command (smtp.ehlo) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.33.21, remotePort = 45993, domain = “AM0PR83CU005.outbound.protection.outlook.c o m”
2026-07-28T23:58:40Z INFO SPF EHLO check passed (smtp.spf-ehlo-pass) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.33.21, remotePort = 45993, domain = “AM0PR83CU005.outbound.protection.outlook.c o m”, result = SPF check passed (spf.pass), elapsed = 0ms
2026-07-28T23:58:41Z DEBUG SMTP STARTTLS command (smtp.start-tls) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.33.21, remotePort = 45993
2026-07-28T23:58:41Z INFO TLS handshake (tls.handshake) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.33.21, remotePort = 45993, listenerId = “smtp”, version = “TLSv1_3”, details = “TLS13_AES_256_GCM_SHA384”
2026-07-28T23:58:41Z DEBUG SMTP QUIT command (smtp.quit) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.33.21, remotePort = 45993
2026-07-28T23:58:41Z DEBUG SMTP connection ended (smtp.connection-end) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.33.21, remotePort = 45993, elapsed = 154ms
2026-07-28T23:58:46Z DEBUG SMTP connection started (smtp.connection-start) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.14.81, remotePort = 18248
2026-07-28T23:58:46Z INFO SMTP EHLO command (smtp.ehlo) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.14.81, remotePort = 18248, domain = “SA9PR02CU001.outbound.protection.outlook.c o m”
2026-07-28T23:58:46Z INFO SPF EHLO check passed (smtp.spf-ehlo-pass) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.14.81, remotePort = 18248, domain = “SA9PR02CU001.outbound.protection.outlook.c o m”, result = SPF check passed (spf.pass), elapsed = 58ms
2026-07-28T23:58:46Z DEBUG SMTP STARTTLS command (smtp.start-tls) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.14.81, remotePort = 18248
2026-07-28T23:58:46Z INFO TLS handshake (tls.handshake) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.14.81, remotePort = 18248, listenerId = “smtp”, version = “TLSv1_3”, details = “TLS13_AES_256_GCM_SHA384”
2026-07-28T23:58:46Z DEBUG SMTP QUIT command (smtp.quit) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.14.81, remotePort = 18248
2026-07-28T23:58:46Z DEBUG SMTP connection ended (smtp.connection-end) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.14.81, remotePort = 18248, elapsed = 783ms
2026-07-28T23:59:02Z DEBUG SMTP connection started (smtp.connection-start) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.32.3, remotePort = 19098
2026-07-28T23:59:02Z INFO SMTP EHLO command (smtp.ehlo) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.32.3, remotePort = 19098, domain = “DB3PR0202CU003.outbound.protection.outlook.c o m”
2026-07-28T23:59:02Z INFO SPF EHLO check passed (smtp.spf-ehlo-pass) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.32.3, remotePort = 19098, domain = “DB3PR0202CU003.outbound.protection.outlook.c o m”, result = SPF check passed (spf.pass), elapsed = 0ms
2026-07-28T23:59:02Z DEBUG SMTP STARTTLS command (smtp.start-tls) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.32.3, remotePort = 19098
2026-07-28T23:59:03Z INFO TLS handshake (tls.handshake) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.32.3, remotePort = 19098, listenerId = “smtp”, version = “TLSv1_3”, details = “TLS13_AES_256_GCM_SHA384”
2026-07-28T23:59:03Z DEBUG SMTP QUIT command (smtp.quit) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.32.3, remotePort = 19098
2026-07-28T23:59:03Z DEBUG SMTP connection ended (smtp.connection-end) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.32.3, remotePort = 19098, elapsed = 220ms
2026-07-28T23:59:04Z DEBUG SMTP connection started (smtp.connection-start) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.32.26, remotePort = 11170
2026-07-28T23:59:04Z INFO SMTP EHLO command (smtp.ehlo) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.32.26, remotePort = 11170, domain = “DU2PR03CU002.outbound.protection.outlook.c o m”
2026-07-28T23:59:04Z INFO SPF EHLO check passed (smtp.spf-ehlo-pass) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.32.26, remotePort = 11170, domain = “DU2PR03CU002.outbound.protection.outlook.c o m”, result = SPF check passed (spf.pass), elapsed = 62ms
2026-07-28T23:59:04Z DEBUG SMTP STARTTLS command (smtp.start-tls) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.32.26, remotePort = 11170
2026-07-28T23:59:04Z INFO TLS handshake (tls.handshake) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.32.26, remotePort = 11170, listenerId = “smtp”, version = “TLSv1_3”, details = “TLS13_AES_256_GCM_SHA384”
2026-07-28T23:59:04Z DEBUG SMTP QUIT command (smtp.quit) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.32.26, remotePort = 11170
2026-07-28T23:59:04Z DEBUG SMTP connection ended (smtp.connection-end) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.32.26, remotePort = 11170, elapsed = 286ms
2026-07-28T23:59:41Z DEBUG SMTP connection started (smtp.connection-start) listenerId = “smtp”, localPort = 25, remoteIp = 52.103.32.40, remotePort = 35418
2026-07

MTA-STS:

2026-07-28T22:53:54Z DEBUG HTTP request URL (http.request-url) listenerId = “https”, localPort = 443, remoteIp = 40.93.64.24, remotePort = 15094, url = “/.well-known/mta-sts.txt”
2026-07-28T23:12:08Z DEBUG HTTP request URL (http.request-url) listenerId = “https”, localPort = 443, remoteIp = 40.93.64.8, remotePort = 14847, url = “/.well-known/mta-sts.txt”
2026-07-28T23:21:10Z DEBUG HTTP request URL (http.request-url) listenerId = “https”, localPort = 443, remoteIp = 40.93.64.24, remotePort = 12826, url = “/.well-known/mta-sts.txt”
2026-07-28T23:21:40Z DEBUG HTTP request URL (http.request-url) listenerId = “https”, localPort = 443, remoteIp = 40.93.65.3, remotePort = 58000, url = “/.well-known/mta-sts.txt”
2026-07-28T23:21:58Z DEBUG HTTP request URL (http.request-url) listenerId = “https”, localPort = 443, remoteIp = 40.93.69.24, remotePort = 12244, url = “/.well-known/mta-sts.txt”
2026-07-28T23:23:35Z DEBUG HTTP request URL (http.request-url) listenerId = “https”, localPort = 443, remoteIp = 40.93.76.72, remotePort = 11012, url = “/.well-known/mta-sts.txt”
2026-07-28T23:33:55Z DEBUG HTTP request URL (http.request-url) listenerId = “https”, localPort = 443, remoteIp = 40.93.65.3, remotePort = 10980, url = “/.well-known/mta-sts.txt”
2026-07-28T23:36:11Z DEBUG HTTP request URL (http.request-url) listenerId = “https”, localPort = 443, remoteIp = 40.93.64.0, remotePort = 12093, url = “/.well-known/mta-sts.txt”
2026-07-28T23:37:32Z DEBUG HTTP request URL (http.request-url) listenerId = “https”, localPort = 443, remoteIp = 40.93.81.72, remotePort = 12089, url = “/.well-known/mta-sts.txt”
2026-07-28T23:39:03Z DEBUG HTTP request URL (http.request-url) listenerId = “https”, localPort = 443, remoteIp = 40.93.69.24, remotePort = 11411, url = “/.well-known/mta-sts.txt”
2026-07-28T23:41:41Z DEBUG HTTP request URL (http.request-url) listenerId = “https”, localPort = 443, remoteIp = 40.93.65.24, remotePort = 15401, url = “/.well-known/mta-sts.txt”
2026-07-28T23:51:41Z DEBUG HTTP request URL (http.request-url) listenerId = “https”, localPort = 443, remoteIp = 40.93.64.0, remotePort = 12997, url = “/.well-known/mta-sts.txt”
2026-07-28T23:56:13Z DEBUG HTTP request URL (http.request-url) listenerId = “https”, localPort = 443, remoteIp = 40.93.76.72, remotePort = 11614, url = “/.well-known/mta-sts.txt”

The 404 first, since that one has a definite answer. The route ignores the Host header entirely, so reaching it on mta-sts.yourdomain isn’t the issue; it returns 404 only when the running config has no policy in it. And saving in the WebUI persists the setting without reloading the running config: nothing in the settings write path triggers a config rebuild. Run the ReloadSettings action, no restart needed, and re-curl it. If it still 404s, check that the MTA-STS mode isn’t set to Disable and that your mail exchangers list isn’t empty, since an empty MX list also yields no policy.

Two related things: We only emit the mta-sts CNAME and the _mta-sts TXT while a policy actually exists, so if the DNS records page currently shows no MTA-STS rows then the record at Porkbun is a leftover from earlier, which is exactly why Microsoft keeps fetching and failing. And the TXT id is a hash of the policy contents, so it has to be republished whenever the policy changes.

On the aborted sessions: that’s Microsoft giving up after the handshake, not us rejecting anything. We never refuse a TLS handshake over a missing certificate, we substitute a fallback and log it, so a name mismatch shows up as tls.certificate-not-found or tls.multiple-certificates-available rather than as a failure. Could you grep for those three tls events in the same window, and run openssl s_client -starttls smtp -connect yourmx:25 -servername yourmx to see which SANs are actually presented? Also worth checking whether you have TLSA records on _25._tcp.yourmx and whether their hash still matches the current certificate. Stale TLSA after a certificate renewal fits your symptom better than anything else I can think of: Exchange Online enforces DANE on outbound, and it would connect, STARTTLS and then abandon exactly as you’re seeing, with nothing on our side to log.