The database-unavailable fix
The most consequential change here is not a timeout. It is that a recipient lookup which fails because the database did not answer now returns 451 rather than 550.
The two cases were indistinguishable inside the server: a lookup that failed and a lookup that found nothing were reported identically. So a database briefly locked by a backup told the sending server that a perfectly valid mailbox did not exist, and the mail was bounced instead of retried. That is mail loss with a delivery receipt.
It is implemented as a thread-local marker set at the two points where the connection pool gives up, read at both RCPT TO decision points. One detail worth recording because it is easy to get wrong: the marker is set after the pool lock is released and before ReportError, because the error handler can re-enter the pool on the same thread and deadlock against it.
This is also what made it safe to turn the pool deadline on: DBConnectionAcquireTimeout now defaults to 60 seconds rather than 0. It shipped disabled in 6.2.17 precisely because timing out an acquisition turned a slow database into a bounce. It no longer does.
Bounded waits (#23, #24)
Every remaining wait that could occupy a thread from a shared pool now has a ceiling, and the log names which one was hit.
- Outbound delivery sessions have an absolute ceiling (ClientSessionCeiling, 30 minutes) that is separate from the idle timeout. The idle timeout is re-armed on every byte received, so a peer dribbling one byte at a time held a delivery thread indefinitely — the idle timeout never fired because the peer was never idle.
- ClamAV on the delivery path (#23) is bounded and reports a timeout rather than holding the thread.
- DNS queries, event scripts and external scanner processes (#24) are bounded by DNSQueryTimeout (10s), ScriptTimeout (60s) and ExternalProcessTimeout (300s).
- Work queue saturation is reported on a schedule and names the task holding each thread, with its session and peer IP.
- Pre-authentication IMAP command buffering is capped at 11 MB. An unauthenticated peer could previously buffer without limit.
- Backup restore validates the source archive before deleting the target.
- It deleted first, so a corrupt archive destroyed the data it was restoring over.
Static analysis
First /analyze pass. The findings worth naming:
- GetExecutableName and two MySQL path helpers allocated a byte count where a character count was needed — a buffer overrun on long paths.
- Logger::WriteLogFile shadowed fileExists, so rotation tested an uninitialised value and could corrupt the log it was rotating.
- Every read of the virus-scanner counter is now interlocked, and the compare-and-swap uses the value it compared against rather than a fresh non-atomic read.
Roadmap
Roadmap.md is rewritten after a comparison against Postfix + Dovecot, Stalwart, iRedMail, Mail-in-a-Box and Exchange Online.
It states plainly what this fork is ahead on, because a list of gaps misleads on its own: in-process MTA-STS (Postfix still has none native), DANE with in-process DNSSEC validation, ARC sealing, IMAP4rev2 advertised by default (Dovecot ships it compile-time experimental and off), SCRAM-SHA-256-PLUS with channel binding, and observability in the open tier that Stalwart charges for.
Then the gaps that actually cost users — full-text search, shared and delegated mailboxes, Sieve depth, per-account send limits, message trace — each with a note on what is already in the tree that a fix would reuse.
It adds a section on graphs and visual data, including three accessibility defects in the current Control Panel dashboard, and reasoned refusals for JMAP, CalDAV, webmail, active/active clustering, ActiveSync/EWS and mobile push — with the reason in each case rather than a bare no.
Also flagged as time-sensitive: hMailServer can only present a username and password when relaying out through, or collecting from, Microsoft 365. Basic auth for SMTP AUTH there is scheduled off by default at the end of December 2026, so OAuth 2.0 client support (XOAUTH2) is now dated work, not speculative.
Documentation
DiagnosingStalledMail.md gains the delivery half and the corrected DBConnectionAcquireTimeout default.
Verification
1049 regression tests pass against this exact binary, up from 1038. New coverage for recipient verification (4), IMAP sequence sets (12) and aborted transfers (2).
The delivery ceiling was verified against a deliberately hostile peer rig — the kind that dribbles bytes to defeat an idle timeout. That rig is what caught a bad_weak_ptr crash from calling shared_from_this() in a constructor; the timer is now armed from Start().
Still open
Discussion #18 — the root cause is found, fixed and bounded, and the tracking issue for it is closed, but it has not been confirmed against the original reporter's Postfix/PMG setup. That confirmation is the one thing still missing. If you are affected, the per-stage timings described in the troubleshooting guide will name the culprit in a single log line; please post it on the discussion.
Correction (12 Aug 2026)
A follow-up audit of the source tree found that ARC sealing is narrower than described above and in the roadmap. Arc::Seal has a single caller, sitting after every early return in DKIMSigner::Sign, so a message is sealed only when the sender's domain is hosted on this server and has DKIM enabled and signing succeeds. Relayed third-party mail — the case ARC exists for — is never sealed.
Until that is fixed this should not be presented as an advantage over verify-only implementations, and the roadmap now says so. The same audit turned up fifteen other defects, all listed there.