Repository navigation
Pre-release: hMailServer 6.2.22-pre3 (build 28)
Pre-releaseRe-published on GitHub on 18 October 2026 from the GitLab release v6.2.22-pre3 of 2026-08-19. Each file below was checked against the SHA-256 that GitLab's package registry records for it before it was uploaded here.
Re-published on GitLab on 24 September 2026 from the GitHub release of 19 August 2026 (2026-08-19 23:43:33 UTC). Issue, pull request and discussion numbers below are GitHub's.
6.2.22-pre3 (build 28)
A third pre-release of 6.2.22, fifteen commits on from pre2. Still a
pre-release: it is gated but not yet run in anger anywhere.
Upgrading from pre2 requires DBUpdater — the database schema moves from
6015 to 6019 in four steps.
The theme of this one is authentication. The roadmap has said for months that
per-account two-factor authentication was impossible here and named the reason;
that chain is now complete, and three of the four schema steps below exist to
support it.
Two-factor authentication, and the chain that had to come first
- App passwords (schema 6016). A per-account credential, revocable on its
own, that authenticates over IMAP, POP3 and SMTP alongside the account's own
password. It is a prerequisite rather than a convenience: a mail client has
nowhere to type a code, so an account required to present one could not be
opened by any client at all.
The secret exists once — 20 symbols from a 30-character alphabet, about 98
bits, from OpenSSL's CSPRNG with rejection sampling. Deliberately not
PasswordGenerator::Generate, which returns the first twelve hex characters
of a GUID: 48 bits, for a credential typed into a client once and then left
for years.
App passwords are tried only after the account's own password has failed —
checking five of them first would add half a second of Argon2id to every
ordinary logon — and on a server that has never issued one, a cached existence
flag answers without touching the database at all. - Per-account 2FA (schema 6017). TOTP has been in this product for years and
protected exactly one thing: the admin tool's own logon, implemented in C# in
the Control Panel and checked after the server had already accepted the
password. The server had no idea the feature existed.
It does now. Once a secret is enrolled, the account password stops being a
mailbox credential and an app password becomes the only one, which is
precisely what app passwords were built to make survivable.
RFC 6238 with the parameters authenticator apps actually implement, and the
RFC's published vectors are pinned in the server's self-tests — including the
20000000000row, which also proves the counter stays 64-bit past 2038. The
regression fixture then computes its codes with .NET's own HMACSHA1 rather
than anything the server owns, because an implementation that agrees only with
itself will agree with itself about the wrong answer. - A password policy the server actually enforces.
IsStrongPasswordhas
existed for years, carries a hard-coded deny-list, is reachable over COM — and
nothing in the server has ever called it. Every mailbox on every installation
could betest.
Five settings now decide, plus one rule that is always on: a password may not
contain the account name. Enforced where a password is chosen and nowhere
that verifies or re-hashes an existing one, because a policy applied to
verification locks people out of mailboxes they can open today. - Password expiry and history (schema 6019). Shipped as a pair, because
expiry without history teaches people to alternate between two passwords —
worse than not expiring at all.
Expiry is the half with a caveat, and it is written into the setting itself:
this server has no self-service password change, so an expired password
can only be reset by an administrator. Existing app passwords keep working,
Active Directory accounts are exempt, and an unreadable stamp is never treated
as expired — the failure that would otherwise lock out every account on any
server that upgrades.
Anti-spam
- Admin-reviewable quarantine (schema 6018). Messages the server would
otherwise have refused are held in a store an administrator can list, read,
release or delete, with a review queue in the Control Panel.
The design turns on one fact: refusing happens during the SMTP conversation
with a 550, so the message is never accepted and there is nothing to review.
Quarantining answers 250 — the sender believes it was delivered and will
not retry, which is what makes a false positive recoverable without bouncing
to a return path that is probably forged, and also means this store is the
only place that message now exists.
Post-transmission verdicts only, because a verdict reached before DATA has no
message to hold. A quarantine that fails to store falls through to refusing:
accepting mail that was not actually stored would turn a spam refusal into
silent deletion. Releasing re-enters at the delivery queue rather than at
SMTP, so the filters that quarantined it cannot undo the administrator's
decision. Off by default.
IMAP and POP3
FETCH BINARYon a composite section returned an empty literal — the
whole message, a multipart part and amessage/rfc822part all came back as
nothing, andBINARY.SIZEagreed with them, so the answer was internally
consistent and completely wrong. Shipped as a documented limitation in pre2
and fixed here. BINARY now also announces content with literal8 (~{n}) as
RFC 3516 requires, because decoded bytes may contain NUL and a plain literal
cannot carry one.- POP3 EXPIRE and LOGIN-DELAY (RFC 2449).
EXPIREis answeredNEVER,
which is this server's actual retention policy rather than a placeholder.
LOGIN-DELAYdeclares and enforces a minimum interval between logins,
refusing an early one with-ERR [LOGIN-DELAY]— the code a conforming client
backs off from, rather than a reply indistinguishable from a wrong password.
Off by default.
OAuth2
- ES256 tokens now verify. The obstacle was a format mismatch rather than a
missing algorithm: JWS carries an ECDSA signature as the raw R||S pair while
OpenSSL verifies X9.62 DER. The transcode is pinned against OpenSSL's own
encoder, case by case, because one checked by round-tripping through its own
decoder proves only that it agrees with itself.
The configured public key is now also required to be the kind the algorithm
names — an EC key against RS256, or RSA against ES256, is refused on the key
type rather than surfacing as a signature failure. And implementing an
algorithm does not enable it: a valid ES256 token is still refused while the
allow-list omits it.
The test suite stopped answering to somebody else's DNS
Not a server change, but the reason four gate runs failed for reasons unrelated
to the code under test, and worth recording because it cost most of a night.
Ten tests resolved live third-party names. Each red gate exposed a fresh
set — six, then one, then two, then four — and the timing was the tell rather
than anything in the messages: three DKIM verification tests took 24-35 seconds
each instead of one, because a DKIM key that cannot be fetched is a temporary
error, so the message is accepted and a test asserting it is rejected fails
while reporting nothing whatsoever about DKIM.
Ten are now served from a local zone. In three cases the assertions got
stronger rather than merely faster: two fixtures used to compare the server's
answer against a lookup the test process made of the same name — two live
lookups that merely had to agree with each other — and now check a value the
fixture chose.
A separate defect underneath cost 653 failures in one run: an error provoked by
a test that has already failed goes on to fail every test after it. Teardown
now prints the ERROR log with the failure that caused it and clears it until it
stays clear, and drains the delivery queue as well — a queued message is a
delivery that has not happened yet, and the error it eventually raises belongs
to a test that has not started.
The resolver turned out to be the real answer. Making a fast resolver the
machine's primary took the suite straight to green; as a secondary it made
things worse, because Windows falls through only when the first server does not
answer at all, never merely because it is slow.
Provenance
Gated at 1700/1700 on the stamped binary (build 28, commit 5f7098f), with 630/630 Control Panel tests.
SBOMs and cosign signatures are attached by CI after publication; the installer
is smoke-tested by CI against this tag. The installer is deliberately not run on
the build machine.
Assets
| File | Size | SHA-256 |
|---|---|---|
hMailServer-6.2.22-x64.exe |
76,783,126 | 24e7e6c83ffbbd852703d629257418c756072053acd2d7c7ee5ff4995fe0f014 |
hMailServer-6.2.22-x64.exe.cosign.bundle |
10,839 | a5b4787fac85bc2c00bc2a0226b4113a18e8f8ad1807c639880483cc27c56813 |
hmailserver.cyclonedx.json |
89,889 | f4cf722aaac5c21b5e682fcb18f3c2064a08b0daea5b0a1e1133453d5e767966 |
hmailserver.spdx.json |
151,498 | 027d9005c57116aca92ef7b648fb63987b4111553da151b2f5c394b1cb17e435 |
These are the files published with the GitHub release, byte for byte, re-hosted in this project's package registry. The hashes above were computed from them on 24 September 2026. Each .cosign.bundle / .sigstore.json is the Sigstore bundle GitHub Actions made when the release was signed; all of them were verified again before re-publishing, against the former GitHub repository's workflow identity (https://github.com/Progressiverobot/hmailserver/..., issuer https://token.actions.githubusercontent.com), which is the identity to give cosign verify-blob for these files. The Windows installer of this release was not Authenticode-signed (Authenticode signing began with 6.3.1).