- Same build as pre5, republished with its supply-chain assets. pre5 went out carrying only the installer: this repository has immutable releases enabled, so the SBOM and signing workflows could not attach anything after publication. That is fixed — assets are now attached while the release is still a draft — but a tag used by an immutable release can never be reused, so the corrected build had to come back under a new one. The code is identical to pre5 (build 30, commit bf0d38a); what is new is the SPDX and CycloneDX SBOMs and a cosign bundle for every asset, the installer included.
- pre4 was withdrawn. Its installer could not create its own database: two SQL statements were written on consecutive lines, and the script parser splits commands on a blank line, so SQL Server Compact — what a default install uses — rejected the pair as one command. The install succeeded, the service started, and it listened on nothing. Nothing local caught it, because this project's test database is upgraded out of band and no test had ever taken the create path a fresh install takes; the installer smoke test found it after publication. There is now a check-db-scripts step that builds a throwaway database from the real create script, and it is part of the release checklist. The same defect was found and fixed in the 6020 upgrade step, which only ever shipped in pre4.
- The v6.2.22-pre4 tag still exists (tag deletion is blocked by a repository rule) but has no release attached; do not build from it.
Sixteen commits on from pre3.
Upgrading from pre3 requires DBUpdater — the schema moves from 6019 to 6022 in three steps.
The theme is standards catching up with reality: DMARCbis and the RFCs around it were published while this fork was being written, certificate lifetimes are falling on a schedule, and several things this server did were correct when they were written and are not any more.
DMARC, brought up to DMARCbis
- The DNS tree walk (RFC 9989 §4.10) replaces the Public Suffix List for deciding whether two names belong to the same organization — which is what DMARC's default relaxed alignment compares. This is not tidying: where a sender relies on tree-walk semantics, a list-based answer is a wrong policy decision rather than a soft failure, in both directions. The list is kept as the
DmarcTreeWalkEnabled=0 escape hatch and as the answer whenever a lookup fails transiently — a resolver outage must not silently turn relaxed alignment into strict. - Bounded at eight queries per domain and cached for five minutes, because DMARC needs an organizational domain for the author domain and every identifier aligned against it: uncached, one message could cost two dozen lookups.
- The np= tag (RFC 9989 §5.5.4) — the policy for subdomains that do not exist. p= covers the domain and sp= its subdomains, and between them they left the cheapest forgery there is uncovered: a phisher does not need a subdomain delegated to them, only a From header that says accounts.thebank.test, and no domain owner can publish a record for a name they have never heard of.
- The whole tag turns on NXDOMAIN versus NODATA, so the resolver now reports the RCODE. A subdomain holding only an MX or a TXT record exists, and treating those as absent would apply
np=reject to real parts of a sender's own estate. - Aggregate reports name the selector. RFC 7489 has a <selector> inside auth_results/dkim and it was never emitted, so a domain owner reading their own report could see that something of theirs signed a message but not which key — the one thing they need during a rotation or after a compromise. The SPF <scope> is now emitted for the same reason.
Algorithms that stopped being safe
- DKIM rsa-sha1 is refused (RFC 8301). This server both signed and verified with it. A DKIM signature is an assertion of identity, DMARC alignment is built on it, and SHA-1 chosen-prefix collisions have been practical since 2020 — a forgeable signature is worse than an absent one, because it carries the signer's domain past an aligned DMARC check. A domain configured to sign with it is now signed rsa-sha256 instead and told so.
DkimAcceptSha1=1 restores both halves for a correspondent who has not moved.- DKIM keys below 1024 bits are refused (RFC 8301 §3.2). Nothing checked, so a domain publishing a 512-bit key — several did for years, because a smaller key fits more comfortably in a TXT record — had its signatures verified as normal. A 512-bit key is factorable in hours on rented hardware.
- SPF void-lookup limit (RFC 7208 §4.6.4). The ten-term limit bounds what a policy can demand; it does not bound what a policy can waste. Ten terms naming hosts that do not exist cost a full resolution each for a record that can never match. Capped at two, SpfVoidLookupLimit to change or disable.
Certificates, before the lifetimes drop
- The ACME renewal window is now a fraction of the certificate's lifetime.
- Thirty days was right while certificates lasted ninety and stops being right the moment they do not — Let's Encrypt defaults to 64 days from February 2027, the maximum falls to 100 days in March 2027 and to 47 in March 2029. A fixed 30-day window against a 47-day certificate starts renewing seventeen days after issuance and never stops, which is how an operator gets rate-limited by their own CA. Renewal now begins two thirds through whatever lifetime the certificate has, with a one-day floor.
- ARI (RFC 9773). The CA's renewalInfo endpoint is consulted hourly and its suggested window wins when offered, so a CA can pull renewals forward during a mass revocation. A point is chosen inside the window rather than at its start — renewing at start moves the thundering herd rather than removing it — derived from the certificate's own identifier so it is stable across the hourly re-checks. Whatever the CA says is clamped to a day before expiry: ARI is advisory and the certificate is not. A CA without ARI, a failed request or an unreadable answer all mean "decide for yourself", never "do not renew".
Mail that used to be accepted and then bounced
- A full mailbox is refused during the SMTP conversation with 452 4.2.2.
- Quotas were checked only at delivery, so a message for an over-quota mailbox was accepted and then answered with a non-delivery report addressed to the envelope sender — which the traffic that fills mailboxes fastest has forged.
- That report went from this server to somebody who sent nothing: backscatter, with this server's reputation paying for the forgery. Temporary rather than permanent, so a legitimate sender's mail waits in its queue.
RejectFullMailboxAtRcpt=0 restores the old behaviour.- Quota warnings (schema 6022). Nothing told an account holder their mailbox was filling up, so the first they heard of it was mail no longer arriving.
- QuotaWarningPercent (default 90, 0 disables) sends one notice — crossing a threshold is an event, not a state, so nothing has to be remembered and emptying the mailbox and filling it again warns again.
Asked for
- Per-domain outbound relay (schema 6021) — discussion #31. Each domain can have its own relay host, port, credentials and transport security, matched by the sending domain. Deliberately a different question from a route: a route is about where mail is going, this is about where it leaves from.
- Both can apply, so the order is fixed and tested — route, then the sending domain's relay, then the server-wide relayer. An empty relay host means the domain has no opinion, which is what every existing domain has.
Operations
- A queryable message trace (schema 6020). "What happened to the message Jane sent at 14:20" used to mean grepping several log files and hoping the relevant one had not rotated. Off by default, because it records who corresponds with whom.
- Archive retention. ArchiveDir is a raw copy of every message that passes through the server and nothing ever removed anything from it. ArchiveRetentionDays prunes it; 0 by default, because an archive is usually kept for a legal reason and a server that started deleting from one on upgrade would be destroying what it was told to keep. Only ever removes .eml files, and does not follow symlinks or junctions.
- Per-domain metric labels. Every counter was server-wide, which is what stopped metrics becoming reporting. MetricsPerDomainEnabled adds a domain= label to message counters. Off by default — every label value is a separate time series — and the label set is bounded by construction: only domains this server hosts are ever labelled, so no amount of mail from strangers can create series.
Corrections to the record
- THREAD (RFC 5256) was listed as not implemented and has been implemented all along — both algorithms, the capability, UID THREAD, thirteen tests. The roadmap row was written from an assumption and is corrected rather than the work repeated.
- The per-domain metrics row claimed every counter was global; protocol=, queue= and build-info labels already existed.
Known limitations
- The DMARCbis 2.0 aggregate-report namespace (RFC 9990) is not implemented; reports are still the RFC 7489 form, which every consumer understands.
- Per-account metric labels are not implemented and are not planned in this shape: accounts are unbounded in a way domains are not.
- Archive retention has no per-domain scope, index, immutability or legal hold.
- This release has not been installed by the maintainer on a production server. The installer is smoke-tested on a throwaway CI runner only.