Skip to content

Pre-release: hMailServer 6.2.22-pre3 (build 28)

Pre-release
Pre-release

Choose a tag to compare

@RainbowWizard-coder RainbowWizard-coder released this 18 Oct 05:19
· 3102 commits to master since this release

Re-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
    20000000000 row, 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. IsStrongPassword has
    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 be test.
    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 BINARY on a composite section returned an empty literal — the
    whole message, a multipart part and a message/rfc822 part all came back as
    nothing, and BINARY.SIZE agreed 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). EXPIRE is answered NEVER,
    which is this server's actual retention policy rather than a placeholder.
    LOGIN-DELAY declares 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).