Releases: Progressiverobot/hmailserver
Release list
hMailServer 6.3.1
hMailServer 6.3.1
This is the first release of this project to carry an Authenticode signature. The Windows elevation prompt names Progressive Robot Ltd instead of Unknown publisher, and an enterprise policy that refuses unsigned binaries outright stops being a wall.
Nothing in the server changes. The compiled server differs from 6.3.0 by its version stamp and one comment line; everything else in this release is the path a release travels - the workflows that sign and verify it, found wanting while preparing to use the certificate for the first time - and three documentation corrections.
The installer is signed
The installer is signed with Azure Artifact Signing, against a certificate profile issued to Progressive Robot Ltd after Microsoft's identity validation. The account, the validation and the profile were arranged on 10 September 2026, after 6.3.0 had shipped. A published release here is immutable, so 6.3.0 could not be signed retroactively and never will be.
What it changes. The elevation prompt reads the publisher's name. And for administrators on managed estates - a great many of this server's users - a policy that refuses unsigned binaries no longer refuses this one.
What it does not change, said plainly: SmartScreen still warns. Microsoft's own comparison puts a signed installer in the same row as an unsigned one, flagged as unrecognised until reputation accumulates, and reputation attaches to a file that does not change - which a new 80 MB installer every few weeks never is. An EV certificate would not help; Microsoft removed EV's SmartScreen bypass in 2024. Expect the same Windows protected your PC screen, now with the publisher's real name on it.
Only the Windows installer is Authenticode-signed. There is no Authenticode for a .deb, an .rpm or an AppImage. Sigstore is unchanged and remains the check that means something for every asset on the release: each carries a .cosign.bundle beside it, and cosign verify-blob against that bundle verifies it.
One setting becomes usable. UpdateRequireAuthenticode=1 has existed since 6.2.28 and, until now, made the server's own update path refuse every release of this project, because none carried a signature: the check is WinVerifyTrust on the downloaded installer, and an unsigned file is refused with the file carries no Authenticode signature. From 6.3.1 the installer it downloads carries one. The check is Windows-only: on Linux there is no Authenticode to verify, the server says so rather than reporting a pass, and updating is the package manager's job.
The release path
Releases here are immutable, so each of these could have put out a permanently wrong release.
- The Authenticode gate was checking the wrong four of the six settings it guards. It asked for the three Azure secrets and
ARTIFACT_SIGNING_ACCOUNT- the one input the signing action marks optional - and never asked aboutARTIFACT_SIGNING_ENDPOINTorARTIFACT_SIGNING_PROFILE, the two it marks required. Five half-configured states got through, each a state somebody passes through while filling in three repository variables one at a time; the action throws on an empty endpoint, and the cosign job declares a dependency on this one, so a release that tripped it would have got no Authenticode signature and no Sigstore bundle on any of its twenty assets. There are three outcomes now: none of the six set is the ordinary case and stays a silent no-op, all six signs, and anything in between stops and names what is missing while the release can still be fixed. The six values, and the tag, reach the shell through the environment rather than being pasted into the script text, because an expression pasted into a script is executed as syntax if it holds a quote. - The release is verified as it is attached, not as it was signed. The old check looked only at the job's own copies. A draft stays mutable afterwards, and four things upload over it with
--clobber: a re-run of the Linux publish job, the SBOM workflow, a second Authenticode run, and the hand-recovery commands the Linux job prints into its own log. The workflow now ends by re-downloading the release exactly as it stands and verifying every asset against its attached bundle, in both directions - an asset with no bundle, and a bundle whose asset has gone. - What a release must carry is asserted before a certificate is spent on it. The only completeness check was "did we sign at least one thing", and the SBOMs were named nowhere but in comments - which is how 6.2.22-pre4 came to be published with an installer and no SBOM at all. Both SBOMs are a hard failure now, in the Authenticode job rather than the signing job, so an incomplete release stops before a code-signing certificate has been spent on it. The Linux sums file and the four package names stay warnings, deliberately: a Linux packaging failure must not hold back the Windows installer every server in the field is waiting for.
- An RFC3161 countersignature is required, and is checked. These certificates live about seventy-two hours.
Get-AuthenticodeSignatureanswers Valid for a signature carrying no timestamp for as long as the certificate has not expired, which it never has seconds after signing - so an untimestamped installer would look perfect on the day and stop verifying on every machine in the field three days later, on a release that cannot be corrected. The countersignature is proved before the release asset is replaced, so a timestamping failure costs a re-dispatch and nothing else. - Verification can no longer pass vacuously. A glob that matched nothing used to verify nothing and succeed. Both verification steps now refuse to pass having checked zero assets, and compare what they verified against what the signing step reported.
- Runs are serialised per tag. Two dispatches for one tag could interleave, and one run's bundle could end up describing bytes the other had already replaced. They queue now, and are not cancelled: a run stopped between signing and uploading would leave assets with no signature at all.
- The SBOM workflow refuses to overwrite an SBOM that has already been signed, because cosign signs the bytes attached when it runs and Syft writes a fresh document identifier every time, so a re-upload after signing leaves a signature that verifies against nothing. It also proves the tree it describes is the tag's own commit before Syft runs - a bill of materials describing a tree the release was not built from is the one thing an SBOM must never do.
- The installer smoke test can read a draft release, which is the only kind it is ever pointed at. It asked for
contents: read, and GitHub shows an unpublished release only to a token with push access, so every dispatch against a draft answered "release not found" - which made the documented order, green before publication, impossible rather than merely awkward. It asks forcontents: writenow, and writes nothing. - The Linux package-install check runs under
sudo, like every other command in its step. The package makes/etc/hmailserver0750 root:hmailserveron purpose, because the configuration holds credentials, so a barestatanswered "Permission denied" and underset -etook the job with it. That is why 6.3.0's tagged run attached no Linux packages and they had to be attached by hand.
All of the above was verified by executing the real run bodies against the live 6.3.0 asset list with gh and cosign stubbed - the twenty-asset release, a re-run over already-attached bundles, a Windows-only release, a pre-release tag, the hand-recovery shape, a release with no SBOMs, an orphaned bundle, an asset excluded from signing by design, and an empty download - and the gate across fifteen configurations, including whitespace-only values and five hostile ones, none of which executed anything. The Authenticode path itself was rehearsed end to end on a throwaway copy of the 6.3.0 installer before this tag was cut: a valid, countersigned signature naming Progressive Robot Ltd, attached to nothing.
Documentation
- The README offers the Linux packages on its download line, where a reader looks for a download, rather than 140 lines further on in the Linux section. The headline of 6.3.0 was that this server runs on Linux at all.
- Two files named 6.2.29, a version that does not exist and never will: the
[Directories]header comment in the server source, and the relocating guide's note about when a relative path began to resolve against the program folder. Both features shipped in 6.3.0 and both now say so. Comment and prose only. - The release checklist gives
ARTIFACT_SIGNING_ENDPOINTas a full URI rather than a bare host, which is the form the signing action documents and one of the two values that used to pass the old gate and then fail inside the action, and it says the six settings are required together.
Known, and not fixed here
The orphan sweep in the 6029 to 6030 upgrade step runs children before parents. Before that step adds seventeen foreign keys, it deletes rows whose parent is gone. Three of the parent tables are themselves pruned by those deletes, and each one's children are cleaned earlier, against a parent that has not yet lost its rows - so pruning an orphaned account, fetch account or distribution list re-orphans rows nothing revisits, and the constraint that follows is refused. It affects only an upgrade from a schema below 6030 on a database that already holds orphaned rows; a database without them upgrades correctly, a fresh installation is never on this path, and when it does fire it fails loudly with the engine's own words and rolls back rather than doing anything quietly. It is in all four database backends. No installation has reported hitting it. The fix is a reordering of seventeen delete statements and is held for 6.3.2, because a change to a database upgrade script belongs in a...
Build inputs 1: third-party binaries the build fetches
Third-party binaries the build fetches instead of carrying in git. Each is the exact file that was committed until 11 September 2026, recorded by SHA-256 in hmailserver/docs/third-party-binaries.json, which is also where its provenance, licence and use are written down. build/get-installer-binaries.ps1 downloads these, verifies them against that manifest, and places them where the installer script expects them; the same script gathers the MSVC runtime from the build machine's Visual Studio. A release rather than a URL at the vendor, because two of the three have no stable first-party download left: Microsoft withdrew SQL Server Compact 4.0 SP1 from the Download Center, and 7-Zip's standalone console ships only inside a .7z archive that needs 7-Zip to open.
| Asset | SHA-256 | What it is |
|---|---|---|
7za-19.00.exe |
8117e40ee7f824f63373a4f5625bb62749f69159d0c449b3ce2f35aad3b83549 |
7-Zip 19.00 standalone console, Igor Pavlov, LGPL-2.1-or-later; from https://www.7-zip.org/a/7z1900-extra.7z |
mariadb-connector-c-3.4.9-win64-client.zip |
a23e96857e7e02fd26c9317638655c4d3da3f11d0ac2f40ae310842a5169cc8d |
MariaDB Connector/C 3.4.9 client (libmysql.dll) and its ten authentication plugins, MariaDB Corporation AB, LGPL-2.1-or-later; the per-file hashes are in the manifest |
SSCERuntime_x64-ENU.msi |
b2d2aba2102d0fa8915421b1e0b850f7c538983eed06e89c358cc2a0c578f958 |
Microsoft SQL Server Compact 4.0 SP1 x64 runtime (MSI built 2012-04-06), redistributable under its licence; the installer's built-in database option |
This release is immutable, like every release here: a changed or added input goes into build-inputs-2.
hMailServer 6.3.0
hMailServer 6.3.0
hMailServer runs on Linux. That is the whole of why this is 6.3 and not 6.2.29: a mail server that has been Windows-only since 2002 now builds, installs, runs and delivers mail on x86-64 and AArch64 Linux, from the same source tree, with the Windows build unchanged in what it compiles and how it behaves.
Two other things arrived with it, because a Linux server has no Control Panel and no COM: the REST API gained a large write surface, and the browser drives most of it.
The server runs on Linux
One tree, two platforms. Every core translation unit compiles on Linux, counted one file at a time by build/linux-tu-census.sh rather than estimated. The census is a step of the build job: it runs under clang on x86-64 and on AArch64 - a native runner, not a cross-build - and the job fails when a single file stops compiling. A third job builds and links the whole core with GCC on x86-64, which is what stops the port quietly becoming clang-only. The Windows build is the same MSVC project it was; nothing in this release changes what it compiles or how it behaves.
Installed the way a Linux service is installed. A .deb and an .rpm for both architectures, a PKGBUILD for Arch, and an AppImage: the binary, the schema scripts, a systemd unit that runs the server as its own user, a logrotate rule, the configuration under /etc/hmailserver, and the Control Deck. hmailserver --create-database builds the schema, --upgrade-database moves it forward on a package upgrade, --set-admin-password sets the credential, and --check-config reads the configuration back before anything starts.
Proven, not assumed. Against PostgreSQL 18 and MariaDB 11.8: the schema is created, a domain and an account are made over the API, a message is submitted over SMTP, delivered, and read back over IMAP. Directory authentication runs against a real slapd over StartTLS and LDAPS, including a refused bind and a self-signed certificate. Stored secrets are AES-256-GCM under a key file the installation owns, which is what DPAPI does on Windows. A crash is recorded before the process dies, as it is on Windows.
Reproducible on the runner image of the day. No absolute path reaches the binary and the clock every tool would otherwise stamp is the commit's own date, so the same commit built twice on the hosted runners gave byte-identical binaries for x86-64 clang, x86-64 GCC and AArch64 clang. What is not pinned is the image: ubuntu-latest moves, and a compiler or a Boost that moved between two runs changes the binary legitimately. The Windows claim under Verification is the stronger one, and RELEASE.md says why.
The REST API writes what only COM could write
PUT /api/v1/settings and its anti-spam and logging groups: 108 settings written, each through the same setter the Control Panel calls, applied only when every key in the request is accepted. The three groups describe 113 keys in all - five of the logging group's are read-only facts about where the log is written rather than settings, and the relayer password is written and never read back. Global rules with their criteria and actions. SMTP routes with their address lists. Aliases. Accounts, including the password and the administrator level. Certificates, and the listeners that bind them. And POST /api/v1/server/reinitialize, which is what the Control Panel's Reinitialize does, so a new listener takes effect without stopping the process.
Every one of them is refused to a read-only key, refused to a key issued for other domains, and scoped to its own domain where a domain owns it. The OpenAPI document describes all of it, and is now valid JSON - it never was before this release.
What it still does not write, said plainly: a domain's own properties beyond its active flag and its postmaster. DKIM signing above all - there is a read route and no write route - and with it the per-domain size limits, the domain signature and a per-domain relay host. On Windows the Control Panel sets those. On Linux nothing does yet, so a domain that must sign its outbound mail with DKIM is not one to run on this platform today.
The Control Deck writes, and signs in properly
The administration page at / gained editors for most of what the API now writes. Its settings forms are generated from the server's own OpenAPI document, so every setting the API accepts appears with its type, its permitted words, whether it is read-only, and whether it takes effect on restart. Rules, routes, certificates and listeners are created, edited and deleted; an account is created and deleted under a domain; and a button restarts the services in place. Three write surfaces the API has are not on the page yet: a domain, an alias, and changing an account that already exists.
It no longer keeps the administrator password. POST /api/v1/session exchanges it once for an HttpOnly, SameSite=Strict session cookie that ends when the administrator password changes; a write must carry X-Requested-With; an API key cannot mint a session.
Webmail
The self-service portal at /portal is a webmail now rather than a page that reads mail. It is still served by the same listener as the API and still comes out of the binary rather than out of files beside it, so it is there on a machine that has nothing else, and it fetches no font, no image and no stylesheet from anywhere. The markup and its script are two responses, /portal and /portal.js, precisely so that the page's Content-Security-Policy can allow no inline script at all.
It has addresses. #/f/<id>, #/m/<id>, #/search, #/compose: a folder and a message can be linked to, Back and Forward work, a reload lands where the reader was, and a half-written reply survives one.
It looks like the Control Deck, which is to say like the Control Panel - the same visual language, applied to a page that had none.
It notices new mail. GET /api/v1/me/changes hashes the per-folder message and unseen counts into an opaque token and answers changed against the one you hand back. The page polls it every six seconds, stops dead while the tab is hidden, and puts the reader's cursor back by message id rather than by row.
It manages folders. POST, PUT and DELETE /api/v1/me/folders create, rename and delete, judging a name exactly as IMAP's CREATE, RENAME and DELETE judge it and refusing in IMAP's own sentences. The page does not offer delete for the inbox or for a folder the server has designated for a special use: that route refuses those deliberately, because a page has no undo.
Inline images work, and the way they work is the interesting part. Attachment entries carry content_type and content_id. The obvious design - point the <img> at the attachment download route - cannot work, and that was measured rather than assumed: the message body is rendered in a frame sandboxed without allow-same-origin, so its document has an opaque origin, so the browser counts its subresource requests as cross-site, so the SameSite=Strict session cookie never travels and the route answers 401. The only thing that would fix that is the attribute the frame exists to withhold. So the page fetches the attachment itself, with its own credentials, and hands the frame the bytes as a data: URL. The frame's policy names no URL at all, and remote images stay off. What decides whether something is inlined is the server's own Content-Type on the download rather than the message's claim, so the types the download route deliberately neuters - HTML, SVG, XML, script, all served as application/octet-stream - are never inlined and stay in the attachment list.
And its script is executed in CI. build/check-portal-script.py extracts the page and the script out of the C++ literals and runs the script against a small DOM and a stubbed API: 88 checks. What stood there before was a test that asserted certain substrings appeared.
Under the surface
- A message with a non-ASCII folder name is stored correctly on Linux. The modified UTF-7 encoding IMAP uses was broken in both directions there: the conversion handed iconv the string's terminator, which its UTF-7 decoder refuses, and never made the flush call that ends a stateful encoding.
- A command line with a non-ASCII byte arrives whole.
CStdStringconverted narrow to wide through the C locale on Linux, which stops at the first byte above 127 - so an IMAPSEARCH CHARSET UTF-8and aMAIL FROMunder SMTPUTF8 were cut short. - A TLS key-exchange group list that OpenSSL rejects is reported once, not once for every listener and every outbound delivery. On the OpenSSL that Debian and Ubuntu ship, which is older than the ML-KEM hybrids this server prefers, that was hundreds of medium errors an hour for a condition the fallback already handled.
- The administrator's user name is no longer translated in the Control Panel's sign-in box, which made a fresh installation in Chinese, German or Swedish refuse the credential it had just asked for. (#156, #177)
- The installation-paths diagnostic lists every directory the server was configured with and whether it exists. (#158)
Verification
The full regression suite ran on the stamped binary: 2,175 tests, 2,166 passed, 0 failed, 9 skipped (2 ignored, 7 explicit stress runs). hMailServer.exe 6.3.0.39, SHA-256 C9E8853B89883B02CF0BB3E5FBCA33E87CC6A6E8DD0666CE61B101F2EB95638B; installer hMailServer-6.3.0-x64.exe, SHA-256 D9F1217375DD70D523FFB27DBDCD668DD525EDC98EDF64F00B4F69D070434FCB.
The assertion build, first. The same source compiled with every HM_ASSERT kept - a violated one is reported as HM6364 in the error log rather than compiled out - and the whole suite run on it: 2,175 tests, 0 failed, the same 9 skipped, and no assertion violated anywhere in the run.
Reproducible. Two clean Release builds of the stamped source produced a byte-identical hMailServer.exe, SHA-256...
hMailServer 6.2.28
hMailServer 6.2.28
The largest release of this fork so far, and three of its four headline features are things hMailServer has never had: the server can update itself, an account can use its own mailbox in a browser, and the Control Panel speaks seventeen languages. Underneath them is a real HTTP/1.1 server, which is what made the other two possible. Everything new here is off until it is turned on.
The server can update itself
A release is noticed, verified and applied - by a click, or inside a window you choose. UpdateCheckEnabled=0 is the default and nothing happens at all until it is set: no request, no identifier, no configuration, no counts. Turned on, a scheduled task reads this project's release feed every UpdateCheckHours (24 by default) and the Status page reports what it found. Fetching an installer and applying it are two further, equally opt-in steps.
What is applied is what this project signed. The installer is downloaded with the release's Sigstore bundle and verified before it is run: the certificate chains to Fulcio, the identity and issuer are this project's release workflow, the signature covers the file, and the entry is in the public transparency log. UpdateTrustRootsFile, UpdateSigningIdentity, UpdateSigningIssuer, UpdateSourceRepository and UpdateLogPublicKeyFile point that at a private Sigstore instance instead. This project's releases are not Authenticode-signed, so UpdateRequireAuthenticode=1 refuses every one of them; the Sigstore check is not optional and cannot be turned off.
The apply survives its own failure. hMailServer.Updater.exe runs the installer with the service stopped, waits UpdateServiceWaitSeconds (180) for it to come back, and reinstalls the previous version if it does not; the outcome is reported at the next start. The database upgrade authenticates with a single-use token, revoked at every service start, rather than with the administrator password. UpdateBackupBeforeApply=1 means an unattended apply does not happen at all without a configured backup destination and a backup that succeeded.
It works from behind a proxy. HttpProxy=host:port sends every web request this server makes as a client - the feed, its downloads, JWKS, token introspection - through a forward proxy: CONNECT for https with the TLS handshake inside the tunnel and the same certificate verification as a direct connection, the absolute URL for plain http. A proxy that refuses is reported by its own name and status line. No proxy credentials.
Only from 6.2.28 onwards. The version you are reading about is the first to carry any of this, so the upgrade to it is manual for everybody, as every upgrade has been. Its own Updates card in the Control Panel is where the next one is turned on.
An account can use its mailbox in a browser
/portal on the REST listener, and /api/v1/me behind it. It answers to an account's own credentials and to nothing else: no administrator password, no API key. Signing in exchanges the password for a session cookie the server keeps, so the password is sent once. A second factor is asked for where the account has one.
The page reads and writes the mailbox it belongs to: folders with their counts, a folder's messages with a cursor the keyboard moves, a message shown as it was sent - an HTML message inside a frame with an origin of its own, where nothing can reach the page around it - threads, flags, move, delete, marking in bulk, search over the account's own mail, attachments downloaded decoded under a type a browser will not execute, and shared and public folders alongside its own. It sends: a text or HTML message with attachments added the way the COM API adds one, every address put through the same checks a submitted message meets, drafts kept in the Drafts folder and replaced rather than duplicated, reply, reply-all, forward and Bcc.
It also carries what a person would otherwise ask an administrator for: the account's own quarantine, with release and delete; its name, forwarding, signature and vacation reply; its active Sieve script; and a password change - which is why the Control Panel's password-expiry text now says an expired password is renewed by an administrator or by the person themselves if they hold an app password.
A real HTTP server underneath
The REST API and the web services (autoconfiguration, autodiscover, the mobileconfig profile, CalDAV and CardDAV redirects) ran on a single-threaded HTTP/1.0 loop written for a handful of administrative requests. They now run on HttpServer: HTTP/1.1 on Boost.Asio with its own io_context and four workers, keep-alive, chunked bodies, header and body limits, and separate header and body deadlines. Every API answer carries Cache-Control: no-store. This is what a browser talking to the portal actually needs, and it is why the portal exists.
IMAP COMPRESS=DEFLATE (RFC 4978)
A session compresses both directions on request, so headers, bodies and the FETCH traffic of a large mailbox go over the wire deflated. It is advertised until compression is on and refused afterwards, as the RFC requires, and STARTTLS is refused once a session is compressed.
The Control Panel in seventeen languages
Czech, Danish, German, Spanish, Finnish, French, Italian, Japanese, Norwegian bokmål, Dutch, Polish, Brazilian Portuguese, Russian, Swedish, Turkish, Ukrainian and Simplified Chinese, beside English: every page, dialog, message, hint and verdict sentence - 3,376 texts, not a subset. The language is chosen in the connect card and remembered per user. Three checkers keep it honest in CI: a new English caption fails the build until it is marked and translated in all seventeen, the catalogues are checked against each other for the server's own words, numbers and page names, and every [Settings] key the server reads is checked to have an editor.
Administration in the browser
The Control Deck - the page the REST listener serves at / - reads the server's settings, the log files with any file's last 200 or 2,000 lines, the certificates by name and file, and the global rules with their criteria and actions in evaluation order. Writing is still the desktop Control Panel's and COM's; the roadmap row says so. The page is served as the bytes on disk with a Content-Security-Policy that allows its own inline script and style and nothing else, nosniff and no-referrer.
Fixed
A masked password was typed backwards from the second character (#156). In the Control Panel with the password hidden, 12345678 became 18765432; revealed text was fine. The masked box rebuilt its mask on every change and then restored the caret to the index it had read before the keystroke - correct on the ordinary keyboard path, wrong on an IME commit and on some layouts, which is what the reporter had. Every password box in the Control Panel now places the caret from the change itself.
Saving the SSL/TLS page dropped the post-quantum key exchange groups; saving the Scripting page took every COM object from every script. Two of the settings editors added for this release shipped without the server's absent-key default, and saving a page writes every field, so a blank was written where the server's own list belonged. Both now carry it, and both captions say what an empty value really does.
Five defects in the live update, found by reading it back against the tree before it had ever shipped: an unvalidated release tag and installer name from the feed, the apply token readable by any account on the machine, old downloads never swept, and the token not revoked at start.
The certificate renewal arithmetic that makes a 47-day certificate lifetime a non-event is pinned by tests rather than assumed.
Ten defects in the portal page, found the same way: attachments carried on forward and edit-draft, reply-all addressing, Bcc, thread indentation, the message list's cursor after a refresh, and a sign-in prompt instead of silence when a session expires.
Also in it
- The four open code-scanning alerts closed on their merits, and the C# tree's LINQ notes rewritten rather than dismissed.
- The repository's own documents and all 51 wiki pages read against the tree and corrected: routes, settings keys, schema and release stamps, two false paragraphs about the listeners, and two pages that did not render.
- A roadmap section for Linux and AArch64, written from a measurement of the tree - 603 translation units through one ATL precompiled header, no CMake anywhere, but 466 of 1,237 server sources with no Windows token at all - rather than from hope.
Upgrading
No schema change: 6031, as 6.2.27. The installer's database upgrade runs as usual and has nothing to do on a 6.2.27 database.
Everything this release adds is off by default. UpdateCheckEnabled, RestApiPort (which the portal and the Control Deck need), IMAPCompressionEnabled and HttpProxy are all opt-in, and a server upgraded to 6.2.28 without touching its settings behaves exactly as it did on 6.2.27.
Verification
The full regression suite ran on the stamped binary: 2,127 tests, 2,119 passed, 0 failed, 8 skipped (7 explicit stress tests, 1 ignored where STARTTLS is not offered on the plain IMAP port of this bench). hMailServer.exe 6.2.28.38, SHA-256 9E89C1E3ABF4DD36560B0B5F3515BDA3DAB8B96F00EF35DA5376A9B60176BD15; installer hMailServer-6.2.28-x64.exe, SHA-256 FCC791855E9C1C4F3B2D3B8592B4D9CCBAC000DA866BBF8617561415078D5054. The database scripts build a 6031 database from the create script with all 65 schema probes passing, and the upgrade chain is contiguous and forward-only.
Known and unfixed
- The Control Deck reads but does not write; a browser session for the administrator is still owed, so it holds the administrator password in
sessionStoragewhile it is open. - The regression suite runs on Windows only. The Linux and AArch64 section of the roadmap says what that wou...
hMailServer 6.2.27
hMailServer 6.2.27
Two roadmap items that had been open since the roadmap was written, and one small schema step: the validating resolver now proves a missing DS record missing rather than assuming it, and an external IMAP account can mirror every folder of a remote mailbox - the migration route the project never had. The 6.2.26 notes and the 6.2.25 notes still describe everything else.
Security
A missing DS is proved missing, not assumed (RFC 4035 §5.2, RFC 5155 §8). The DNSSEC validator that guards DANE and the SPF, DKIM and DMARC lookups treated a DS query answered with nothing as an unsigned delegation - which is exactly what an attacker who strips the DS from the answer on the way shows a resolver, and a signed zone quietly became an unsigned one, DANE and validated TXT gone with it. The resolver now keeps the authority section of a negative answer and requires the parent's proof there: an NSEC at the delegation name with NS set and DS clear, an NSEC3 whose hashed owner matches with the same bits, or an Opt-Out NSEC3 covering the hash, each signed by the parent's key. A proof that fails to verify, has expired, claims a DS exists, or belongs to another name is no proof, and a delegation without one under a signed parent is Bogus - blocked, as a forged chain is - rather than Insecure. An unsigned parent still makes everything below it Insecure, as it should, and a resolver that cannot be reached still degrades to Insecure rather than lying. Twelve tests run the resolver against a chain the suite signs itself, through the new Diagnostics.DnssecChainStatus.
Mail flow
An external IMAP account can mirror every folder (FetchAccount.MirrorFolders, schema 6031; in the Control Panel, Mirror every folder on an IMAP external account). With it on, the server lists the remote mailboxes and collects each into the local folder of the same name: every message byte for byte, its \Seen \Flagged \Answered \Draft \Deleted flags, its internal date, the remote hierarchy delimiter mapped to the local one. Nothing is delivered - no header, rule, anti-spam or anti-virus touches a copy - because it is mail the other server had already accepted. Each folder keeps its own record of what has been collected, so a second poll takes only what is new, and days to keep messages 0 makes it a move, folder by folder. This is the imapsync shape: the way to bring a mailbox from another IMAP server, or from a hosted service that is switching Basic authentication off, into this one.
The Import Tool reads a Maildir. A second button takes a Maildir - the directory with cur, new and tmp - and imports the INBOX and every Maildir++ folder beside it, with the flags the file names carry (;2, and !2, accepted as well as :2,, since a colon cannot be in a Windows file name) and line endings made CRLF.
Migration.md is the guide the roadmap asked for: from another IMAP server, from mbox, from a Maildir, from Outlook (through IMAP - PST is deliberately not parsed, and the page says why), from the upstream hMailServer in place, and accounts in bulk; what each route keeps and what it does not.
A message imported into a folder a client has open appears at once. The Import Tool's import told nobody that the folder had changed, so a session with the folder's message list cached saw the copy only after a restart. The folder is now refreshed, as it is after a delivery.
Also in it
- Four roadmap rows brought up to date after 6.2.26, and the create-script check and the DBUpdater's message record what #114 taught.
Upgrading
Schema 6030 → 6031: one column on hm_fetchaccounts, added by the installer's database upgrade on every backend; the SQL Server Compact verification that 6.2.25 got wrong was fixed in 6.2.26 and is proved on a fresh database before every release. From 6.2.25 or 6.2.26: run the installer. From earlier: the 6.2.25 and 6.2.24 notes describe what changes on the way.
Verification
Three clean Release builds of the stamped source produced a byte-identical hMailServer.exe 6.2.27.37 (SHA-256 8CD745C49657A2E02989A8A86E9290C0AA49D317466B860C3749C757C129410F). The full regression suite ran on the assertion-enabled build of this source and again on the shipping binary: 2024 tests, 2017 passed, 0 failed, the 7 explicit skips, both times. The Control Panel's tests passed. hMailServer-6.2.27-x64.exe is 77,237,885 bytes, SHA-256 F60D3C30F27AF9EDC18BEE295DA231C666E826C1436E2214593B33499227C1A6; its Sigstore bundle and the two SBOMs are attached below, signed by this repository's sign-release workflow.
hMailServer 6.2.26
hMailServer 6.2.26
The release to take if you run the built-in SQL Server Compact database: the 6.2.25 installer reported every upgrade of one as failed, after the upgrade had in fact succeeded, and the check that gave the false verdict took the service down with it (#114). Nothing else in the server changes; the 6.2.25 notes still describe everything it does.
The fix
Every SQL Server Compact upgrade through schema 6030 was reported as failed after it had succeeded (#114). After each upgrade script, the database updater proves the schema changed by running a probe statement, and reads a failed probe as a missing object. The four probes for schema 6030, the foreign keys, were written with case when exists (subquery) in the SET expression - valid on SQL Server, MySQL and PostgreSQL, and on SQL Server Compact an access violation inside the OLE DB provider, on a correct database with every constraint present. The server caught the fault and reported it as HM10045 Unknown error, the updater declared that Upgrade6029to6030MSSQLCE.sql had not created fk_hm_accounts_domain and blamed an [IGNORE-ERRORS] marker the statement does not carry, and the installer said the database could not be upgraded. Two seconds after answering the probe, the server's crash oracle - which treats a memory-safety fault as fatal - ended the service, and service recovery started it again.
Reproduced from a Compact Edition database created at schema 6011 and upgraded with the shipped scripts: it reaches 6030 with all seventeen foreign keys, and the probe then faults the provider. The probes now read update hm_dbversion set value = value / (value - value) where not exists (select 1 from information_schema.table_constraints where constraint_name = '...' and constraint_type = 'FOREIGN KEY'): with the constraint present no row matches and nothing is evaluated; with it absent the one row matches and the division by zero fails the statement on every backend, leaving hm_dbversion untouched. The updater's message no longer asserts a cause it cannot see; it gives the backend's own words and says how to read them.
So that a probe the backend cannot run is caught before a release: build/check-db-scripts.ps1 now executes every probe against the database it builds from the create script, through the provider the server uses, with a negative control that must fail; and a regression fixture runs them through the same COM path the updater takes, against the bench database, which is Compact Edition. Both fail on the 6.2.25 statement.
If the 6.2.25 installer failed on your database, it is at schema 6030 with its foreign keys in place, and this installer finds nothing left to upgrade. If you restored a backup from before, the whole chain runs and the verification passes.
Also in it
- Two roadmap rows closed on what 6.2.25 did (the fuzz run and the assertion-build gate a release records, and the accessibility audit), and the native-coverage rows now say why they are blocked rather than merely open.
Upgrading
From 6.2.25: run the installer. From 6.2.24 or earlier: the 6.2.25 notes and, before them, the 6.2.24 notes describe what changes on the way; everything there still applies.
Verification
Two clean Release builds of the stamped tree produced the same hMailServer.exe byte for byte (SHA-256 89E72B5B143FD0B9AFD20A4A7848AB1F746E58582BF015238147B74F352A5329); the full regression suite ran on the assertion build (every HM_ASSERT live: 2004 tests, 1997 passed, 0 failed, the 7 explicit skips, no assertion violated) and again on the shipping binary (2004, 1997 passed, 0 failed, 7 skipped), with the three new probe tests in both runs; the Control Panel's own tests passed; the database checks ran all 64 schema probes against a freshly created database with the negative control failing as it must. Installer hMailServer-6.2.26-x64.exe SHA-256 BFB1E5D606D3FB18B2BBBE26A8704A29033C807E293988265F47E75891905F1E; the SBOM and the cosign signatures attached to this release were produced and verified by the workflows after the tag.
hMailServer 6.2.25
hMailServer 6.2.25
Two days after 6.2.24, and the release to take if you use the built-in ACME client: 6.2.24 ends the service the moment a certificate has been issued, and at the start of every renewal (#93). It also carries the sixty commits that landed in those two days: IMAP fetching of external accounts, message retention, an archive index with legal hold, a week of metric history, SASL EXTERNAL, scrypt, tarpits, a server-enforced second factor on the administrator credential, the REST API reaching what was COM-only, pipelined and chunked delivery, OpenSSL 4.0.2, and a code-scanning page with nothing on it.
If you are on 6.2.21 or a 6.2.22/6.2.23 pre-release, read the 6.2.24 notes first; everything there still applies, and the upgrade section below starts from 6.2.24.
The fix to upgrade for
ACME issuance and renewal ended the process (#93). Two calls in the ACME client handed the OpenSSL DLL a FILE* opened by the server's own C runtime: the one that computes the DANE TLSA line logged straight after a certificate has been issued, and the one that re-reads the existing private key at the start of every renewal (the default, AcmeReuseKey). On Windows the OpenSSL DLL does not read such a FILE* itself; it routes every stdio call back into the executable through an OPENSSL_Applink export the executable has to provide, and this executable never has. OpenSSL's answer to the missing export is not an error return: it writes "OPENSSL_Uplink(...): no OPENSSL_Applink" to the Windows Application log under the source "OpenSSL" and calls TerminateProcess. That is the whole picture the reporter saw - an OpenSSL event with a message that looks blank, a 7031 from the service control manager in the same second, no crash dump, and no "ACME (automatic)" certificate record because the TLSA line ran before the deployment.
Both calls now go through OpenSSL's own file I/O, as every other file the ACME client touches already did. The deployment runs before the TLSA line, so nothing optional stands between a renewal and the restart it exists for. A class test in the server's self-test computes the TLSA of a fixture certificate the way the old code would have, so the crash cannot come back unnoticed.
If 6.2.24 issued you a certificate before it died, the files under Data\ACME are valid, and 6.2.25 deploys them at its first ACME check after start-up: a certificate pair on disk that no "ACME (automatic)" record names is deployed once, the way a fresh issuance is - the record, the assignment to TLS ports that have none, the restart. The application log says "issued but never deployed" when that happens.
Found on the way, and fixed in the same release: two restarts of the servers at once - one asked for over COM, one started by an ACME deployment or a backup restore - tore down and rebuilt the same queues under each other and could end in an access violation. Restarts now run one after the other.
What is in it since 6.2.24
Mail flow
- External accounts over IMAP.
FetchAccount.ServerType1 collects a remote INBOX once by UID - LOGIN or XOAUTH2, STARTTLS or TLS as the account says - and leaves it on the server unlessDaysToKeepMessagessays otherwise. Everything after the download is the POP3 fetcher's code. The INBOX only; folders are not mirrored. - Message retention (schema 6027).
Domain.MessageRetentionDaysandAccount.MessageRetentionDaysremove delivered mail older than the policy, in every folder, through the same deletion an EXPUNGE performs, by stored creation time and never by the sender's Date header. Off everywhere until a number is set. - The archive has an index (schema 6029): one row per copy, searched over COM and
GET /api/v1/archive, a legal hold that survives the retention sweep and an address erasure.ArchiveDomainsscopes the archive to the domains it is kept for. - Outbound delivery pipelines the envelope and sends the message as one BDAT chunk when the remote advertises PIPELINING and CHUNKING (
OutboundPipelining,OutboundChunking, both on; either can be turned off). A binary message is now relayed as it is to a remote that accepts BINARYMIME. - Delivery hard links (
DeliveryHardLinks, off): a message to several local recipients can be one file with a name in each folder, and every rewrite of a message file is a temporary file renamed into place. - SEARCH BODY and TEXT read the text-bearing attachments, and the full-text index tokenises them, so a phrase that lives only inside an attached text, HTML or CSV file is found - the same with the index on and off.
- ManageSieve advertises
envelope, which had shipped without its capability line. - A Message-ID is added only for submissions (upstream #552): the client authenticated, or sending as one of our domains from a range that does not require it to. Relayed mail keeps its headers as they are, so SpamAssassin's MISSING_MID and a rule looking for the header see the truth.
- A stranded fetch account (upstream #603): a fetch that threw, or a database error recording the next try, left the account locked and silently skipped until a restart. The task owns the lock now.
IMAP
- Every session numbers its selected folder for itself (upstream #602). Another session's expunge no longer renumbers a client's messages under it; a message found gone is answered
NO [EXPUNGEISSUED], and QRESYNC sessions hearVANISHED. - Sessions stop sharing unguarded state (upstream #580, #566): one lock on the connection, serialised lazy collections, snapshots for cross-thread readers.
- One strand per connection. A write from another thread - a notification to an idling IMAPS session - can no longer run inside the SSL engine while that connection's own read is mid-decrypt.
- Default special-use folders at account creation (
CreateDefaultSpecialUseFoldersEnabled, off; schema 6026): Drafts, Sent, Trash and Junk with their RFC 6154 designation stored on the row. - One authorisation choke point. Every "may this account do this to that folder" is one decision, delivery and rules included, and a CI guard fails any second decision-maker.
Security and authentication
- SASL EXTERNAL. A client certificate that verified against the port's CA logs on as the mailbox it names - through the addresses the certificate carries and nothing else - on SMTP, IMAP and POP3. Found on the way: the AUTH line limit was 500 octets on POP3 and 510 on SMTP, so a real RS256 bearer token had never fitted; it is 12288 now, RFC 5034's figure.
- OAuth2: the provider's JWK Set as the signing-key source (
OAuth2JwksUrl, rotation handled) and token introspection for revocation (OAuth2IntrospectionUrl). Both off unless configured. - scrypt (
PreferredHashAlgorithm=7, OWASP parameters), and schemes compared by strength rather than by number, so a preference for scrypt leaves Argon2id accounts alone and a minimum of either accepts both. - The password-hash work factor is a setting (
PasswordHashIterations,PasswordHashMemoryKB,PasswordHashTimeCost; upstream #554), and a hash derived under a cheaper one is re-derived at the next logon. Upward only. - Tarpitting on failed logons and on stranger recipients (
LogonTarpitSeconds;SmtpTarpitCountandSmtpTarpitDelaySeconds; all off), as a pause on the connection's own timer, never a thread asleep - which is why the two old COM properties were stubs for years. - The administrator's second factor is the server's now. With a TOTP secret enrolled (
Settings.EnrolAdministratorTOTP), COM needsAuthenticateWithCodeand the REST API needs anX-hMailServer-OTPheader; a stolen administrator password is no longer a complete credential. - An authenticated session may only send as an address its account owns or has been granted (
SmtpAuthenticatedSenderCheck, off), and the post right on a mailbox's INBOX is the Send-As grant. - Event scripts can create only the COM classes
ScriptAllowedObjectsnames. The default is*, which is what every earlier version did; set it to a list andCreateObjectof anything else fails inside the script with error 429. - The Apple configuration profile is served over HTTPS only (see the upgrade section).
Anti-spam and anti-virus
- spamd is told whose preferences to apply (
SpamAssassinUser,SpamAssassinUserFromRecipient; off), and moving a message into the Junk folder teaches SpamAssassin it is spam, out of it teaches ham (SpamAssassinLearnOnMove, off) through spamc's TELL. - The ClamAV connection test asks PING and VERSION first, names the daemon it reached, and streams its EICAR sample from memory - the button had failed on every Windows with real-time protection on, because Defender removed the sample file before clamd saw it.
Administration
- The REST API reaches what was COM-only: IP ranges, distribution lists, certificates (never the private-key password), DKIM, the global rules, the logs, the backup and a settings snapshot, each decided at the same authorisation choke point. Writing settings, rules, certificates and DKIM stays COM-only, on purpose.
- The server remembers its metrics (schema 6028): one row per metric per minute, kept a week (
MetricsHistoryDays), read back over COM andGET /api/v1/metrics/history, shown on the dashboard for a day, a week or a month. - Every backup proves its mail can be got back out (
BackupVerifyRestore, on): the archive's message store is extracted to a scratch directory through the same code a restore runs, before the backup is called complete. - Control Panel: follows Windows High Contrast for every control; every static caption carries an Alt-key mnemonic, checked in CI; page titles are headings and status text is a live region; the Welcome page star...
hMailServer 6.2.24
hMailServer 6.2.24
The first stable release since 6.2.21. It is the code of 6.2.23 Alpha 2, cut the same day, plus NuGet lock files for the .NET tools and the CI changes that hold every restore to them. Nothing that ships changed between the two; the alpha's regression gate stands, and this release ran the full gate again on its own stamped binary. Schema 6025, unchanged since Alpha 1.
If you are on 6.2.21, everything the two pre-release lines introduced arrives at once. The section after next is written for you.
What is in it since 6.2.21
Two pre-release lines, 6.2.22 (pre1 to pre6) and 6.2.23 (Alpha 1 and 2), carried the work. Their notes are the detailed record - 6.2.22 pre-releases, Alpha 1, Alpha 2 - and this is the shape of it:
- Mail that was silently lost is not any more. A delegated APPEND, COPY or MOVE filed bytes under the wrong account; a MOVE destroyed the only readable copy. Thunderbird's every Sent copy was refused once UTF8=ACCEPT was enabled (#53). A kept-open log was never flushed (#33). The ACME client had never worked against real Let's Encrypt (#34).
- Anti-spam bypasses closed. A sender could steer a per-account spam override into un-marking their own mail by forging a header; a sender could choose which address the DNSBL, SPF and HELO tests ran against through the HELO literal in the Received header; RSET before EHLO or STARTTLS without a fresh EHLO opened a transaction with the HELO host empty, skipping the HELO-host test and the script events.
- Features. Shared and delegated IMAP mailboxes, full-text search, RFC 3464 machine-readable bounces, BINARYMIME, per-account spam thresholds, a sender blacklist, domain-wide out-of-office, distribution-list moderation, Windows Event Log integration, OTLP metrics and logs, a reverse-proxy PROXY-protocol front for SMTP, an external HTTP filter hook, eleven IMAP extensions (APPENDLIMIT, LITERAL-, LIST-STATUS, UNAUTHENTICATE, QUOTA, PREVIEW, MULTIAPPEND, REPLACE, SAVEDATE, METADATA, BINARY), Microsoft 365 XOAUTH2 in both directions, app passwords, TOTP for the Control Panel, a quarantine, password history, message trace, per-domain outbound relay, a disk-space floor, and a substantial Control Panel overhaul.
- Hardening. Control Flow Guard on the server; a reproducible build (two clean builds give the same bytes); signed release tags; SBOMs and Sigstore bundles on every release; a COM vtable layout repaired to be binary-compatible with 6.2.21; an unattended install that fails its database step now exits non-zero; an unattended upgrade can no longer hang on a password dialog.
Upgrading from 6.2.21
The database schema moves from 6011 to 6025 in fourteen steps, one way. DBUpdater runs them in order and resumes from wherever a partial upgrade stopped. There is no downgrade: an older server refuses a newer database rather than misreading it, so take a database backup and a data-directory backup before you install, with your backend's own tools. Rollback is only as good as that snapshot.
Two of the steps are worth planning for:
| Step | What it does |
|---|---|
| 6024 → 6025 | Rewrites hm_messages.messageflags from tinyint to smallint on MS SQL, SQL Server Compact and MySQL/MariaDB - a table-rewriting operation on the largest table you have, holding locks for its duration. Size the maintenance window by your message count. PostgreSQL is unaffected (already smallint). |
| 6022 → 6023 | Creates the full-text index tables. Empty and inert until you enable indexing; costs nothing on upgrade. |
Behaviour that changes on upgrade, without a switch:
- A client that sends
MAIL FROMorAUTHstraight after the STARTTLS handshake, without sayingEHLOagain, now receives503 Bad sequence of commands. RFC 3207 has required the secondEHLOall along and every mainstream client sends it; a home-grown submission script that skips it will find out here. - For mail arriving through an incoming relay or fetched from an external account, the anti-spam tests run against the address the relay actually observed, not the first bracketed value in the Received header (which was the client's HELO literal). If you tuned scores around the old behaviour, expect verdicts to move.
- Mail is refused with a temporary error when the data volume falls below 100 MB free (
MinimumFreeDiskSpaceMB, Control Panel: Server settings). Set it to 0 to restore the old behaviour of writing until the disk is full. - Critical and high-severity errors also go to the Windows Event Log (source
hMailServer, throttled). A service account that cannot create the event source logs a single line saying so and continues. - Existing per-account out-of-office replies now honour the RFC 3834 suppressions (no reply to bulk, list, auto-submitted or bounce mail) that previously applied only to the domain-wide one.
- A bulk prune of old
hm_imapexpungedrows runs once at first start (IMAPExpungeRetentionRecords, default 5000; set 0 to defer). - Early-bound COM clients compiled against a 6.2.22 pre-release interop assembly must be recompiled against this release's type library; the pre-release layout was the broken one. Late-bound scripts (VBScript, PowerShell, PHP) are unaffected, and so is anything compiled against 6.2.21.
Everything else keeps its previous default. The full list of the 23 new INI settings, all shipped off or inert, is in the Alpha 1 notes and in README.md.
What changed since Alpha 2
- NuGet lock files beside all nine .NET tool projects, and every restore in CI held to them (
--locked-mode,RestoreLockedMode), so a package bump without its lock-file update fails the build rather than building against a graph nobody reviewed. This was the last OpenSSF Scorecard finding that code could fix; the other four are dismissed on the security page with the reason on each, and eight required status checks were added to the master branch ruleset. - The version stamp. Nothing compiled into the server, the tools or the installer changed otherwise.
Known limitations
The Alpha 1 and Alpha 2 lists still apply. The three worth repeating for a stable release:
- Statement timeouts were built for PostgreSQL and MySQL and have been exercised only against SQL Server Compact, the bench backend.
- The weekly C++ CodeQL analysis is not running: it targets a self-hosted runner that is no longer registered. The C# analysis runs on every push and pull request.
- SCRAM-SHA-1 is not offered (#49, with the reason); clients without SCRAM-SHA-256 use PLAIN or LOGIN over TLS.
Downloads and verification
| Asset | What it is |
|---|---|
hMailServer-6.2.24-x64.exe |
The installer (Inno Setup). Not Authenticode-signed. |
*.spdx.json, *.cdx.json |
SBOMs, SPDX and CycloneDX, covering the .NET and native dependencies. |
*.cosign.bundle |
A Sigstore bundle per asset, keyless, bound to this repository's workflow identity. |
Verify the tag, from any clone:
git -c gpg.ssh.allowedSignersFile=.github/allowed_signers verify-tag v6.2.24
Verify an asset with cosign:
cosign verify-blob --bundle hMailServer-6.2.24-x64.exe.cosign.bundle \
--certificate-identity-regexp '^https://github\.com/Progressiverobot/hmailserver/' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
hMailServer-6.2.24-x64.exe
The server binary is reproducible: two clean Release builds of this tag produce a byte-identical hMailServer.exe, SHA-256 64406163b1a3a8a865e34d74883b5e2a05a5a1c476b500b26745cde67d8a989b, with the v145 toolset (MSVC 14.51), Windows SDK 10.0.26100 and the library layout in README.md. The regression suite on that binary: 1838/1838, nothing skipped, 31 minutes. Control Panel unit suite: 645/645.
hMailServer 6.2.23 Alpha 2
hMailServer 6.2.23 Alpha 2
Pre-release. Not for production. 30 commits since v6.2.23-alpha1; 1,873 files touched, of which 1,825 are the licence-header pass from 22 August and 72 are the formatting commit, leaving roughly 80 files of actual change (+1,600 / −200 lines). The database schema is unchanged at 6025, the COM interfaces are unchanged, and no checked-in binary changed. Everything in Alpha 1's notes - the schema move from 6.2.21, the one-way upgrade, the known limitations - still applies to an installation coming from 6.2.21 or earlier.
What this release is
A small, deliberate release: one reported defect fixed, seven fixes taken from the original project - which has become active again after years - and the hardening that was written down as missing. It exists because a Thunderbird user could not save a single Sent copy, and because the upstream comparison turned up two ways a sender could steer the anti-spam tests. Alpha rather than beta because the STARTTLS change below alters what a non-conforming client sees, and that deserves a cycle in the open before it is called stable.
Fixed: Thunderbird's Sent copies were refused (#53)
Once a client enables UTF8=ACCEPT, RFC 6855 lets it send an APPEND's message as UTF8 (~{n} with the closing ) following the octets. Thunderbird 128 and later does this for every Sent copy. The server's command parser counts parentheses across the whole line before it tokenizes anything, saw one ( too many, and refused the line with BAD APPEND Command requires at least 2 parameter before reading a byte - so mail went out over SMTP and no Sent copy was ever stored, on every message, with only a per-machine client setting as a workaround.
The wrapper is now recognised and stripped ahead of the parser, and the ) is taken from the text that follows the octets. It works for the non-synchronizing ~{n+} form and for later messages of a MULTIAPPEND. A wrapper that is never closed is refused and nothing is stored - the ) is protocol, not message, and a server that guessed where the message ended would be worse than one that refused. Five fixtures, including the exact line from the report; six of their assertions fail against the previous binary.
Two anti-spam bypasses, ported from upstream
The original project has resumed development (seventy-one commits since this fork last compared itself against it). Every one was read against the function that does the same job here; the full record, verdict by verdict, is in .github/upstream-sync. Two of the ports matter for security:
The sender could choose which address the IP-based spam tests ran against
For mail arriving through an incoming relay or fetched from an external account, the server parses the topmost Received header to find the originating address. It took the first bracketed value after from - which is the HELO literal the client chose to present - rather than the address the receiving server actually observed, which comes last. And a header whose host name was not a valid domain name (my_pc, say) was skipped entirely, so parsing fell through into headers the sender wrote, or found no address at all and quietly skipped DNSBL, SPF and the HELO-host test.
The parser now takes the last observed address before by, ignores values marked as HELO-supplied, and keeps a header without a usable host name. Two assert(0) on sender-reachable input are gone with it. Upstream's own test vectors were taken along.
This changes which address is tested for relayed and fetched mail. If you run behind an incoming relay and have tuned scores around the old behaviour, expect verdicts to move - towards the address that actually connected.
RSET before EHLO, or STARTTLS without a fresh EHLO, opened a transaction with no greeting
RSET is valid before EHLO and used to move the session into the transaction state on its own; and the TLS handshake discarded the greeting as RFC 3207 requires but left the state machine where it was. Either way MAIL FROM was accepted with the HELO host empty, which skipped the HELO-host spam test and the OnHELO/OnEHLO script events for that session.
The greeting state is now only left by an actual EHLO or HELO. A client that sends MAIL FROM straight after the TLS handshake without saying EHLO again now receives 503 Bad sequence of commands - which is what RFC 3207 section 4.2 has required of it all along, and what every mainstream client does. If you have a home-grown submission script that skips the second EHLO, this is the release that tells you.
Hardening
- Control Flow Guard is on (#45).
hMailServer.exeis compiled and linked with/guard:cfin both configurations, so an indirect call through a corrupted function pointer or vtable - the natural target of a memory-safety defect in the SMTP, IMAP, POP3 or MIME parsers - terminates the process instead of transferring control.dumpbin /headersshowsGuardin the DLL characteristics. The full regression gate on the CFG build ran in 31 minutes against about 55 for the previous release's gates on the same machine - a faster run, not a slower one, so the cost is below the suite's run-to-run variance. - The release tag is signed (#46).
v6.2.23-alpha2is the first annotated, SSH-signed tag; every earlier one was a lightweight ref with nothing to verify. The signing workflow now refuses to sign a single asset for a tag that is lightweight or does not verify against the allow list in the repository. Verification is in the last section. - An unattended install that fails its database step now fails. The installer discarded the result of the post-install tasks, so a failed database create or upgrade showed a suppressible message box and setup still exited 0. It raises now; scripted deployments can tell.
- COM
IMAPFolder.Deletereports a refused deletion instead of answering S_OK while the row stayed behind.
Other fixes
- A reverse-DNS answer whose owner name carries the trailing dot (
1.0.0.127.in-addr.arpa.) no longer fails the resolver's equality filter and empties the PTR result, which had been failing open in the HELO and reverse-DNS spam tests on affected hosts. - The script host dereferenced a null engine when engine creation had failed (a missing or policy-blocked VBScript engine) and a script reload asked whether a procedure existed.
- The IMAP notification client dereferenced the current folder on the notifying session's thread while the owning session could be closing it.
- Two CodeQL alerts on the mbox importer's buffer handling, and the warning-level findings the managed quality scan raised in the test suite and the header script.
Dependencies
Ten NuGet updates (Azure.Core 1.62, MSAL 4.88, NLog 6.2, OpenTelemetry 1.18, NUnit3TestAdapter 6.3, xunit.runner.visualstudio 4.0) and three CodeQL action updates, both from Dependabot. GitHub's automatic dependency submission - red on every push since it was enabled, because it restores Windows-targeting projects on a Linux runner - now completes, so the dependency graph carries the transitive packages. Getting it there also meant bumping the headers of ten Visual Studio 2005/2008-era solutions (legacy tools and test harnesses) that the .NET 10 solution parser refuses outright. The native dependencies are unchanged: OpenSSL 4.0.1, Boost 1.91, PostgreSQL 18.3, MariaDB Connector/C 3.4.9.
Repository and process
hmailserver/docs/RegressionEnvironment.md- how to set a machine up so the 1,800-test suite runs, which CONTRIBUTING.md had admitted was unpublished (#48).- A Developer Certificate of Origin check on pull requests (#47), and the C# tree brought to
dotnet formatclean with the verify job restored (#44). - Two CI-only changes landed after the regression gate had run and are the only things in this range that did: the editorconfig-checker version pinned beside its action (the checker's 4.0.0 release renamed its download and every run since 3 September had failed before checking a file), and the ten solution headers above. Neither is compiled into anything shipped, which is why the gate stands.
- Issue #49 (SCRAM-SHA-1) is closed as not planned, with the reason: the stored PBKDF2-SHA256 output is the SCRAM-SHA-256 salted password, and SHA-1 would need a second stored verifier per account - a change to what is stored, not to the transport.
- Ten further upstream changes are recorded as roadmap rows with their size and what they wait for, the largest being upstream's way of building the C++ server on GitHub-hosted runners, which this fork lacks.
Known limitations
Everything listed under Alpha 1 still stands. New or newly written down:
- The weekly C++ CodeQL analysis has not run since late August. It targets a self-hosted runner that is no longer registered, and is cancelled after 24 hours in the queue each Monday. The C# analysis runs on every push. Restoring the runner, or porting the hosted build, is the roadmap's top build item.
- SCRAM-SHA-1 is not offered, for the reason above; clients without SCRAM-SHA-256 use PLAIN or LOGIN over TLS.
- Received-header parsing is stricter about host names but not about brackets. An address given without brackets (
(HELO host) (203.0.113.99)) is still not recognised, as before.
Downloads and verification
| Asset | What it is |
|---|---|
hMailServer-6.2.23-alpha2-x64.exe |
The installer (Inno Setup). Not Authenticode-signed. |
*.spdx.json, *.cdx.json |
SBOMs, SPDX and CycloneDX, covering the .NET and native dependencies. |
*.cosign.bundle |
A Sigstore bundle per asset, keyless, bound to this repository's workflow identity. |
Verify the tag, from any clone:
git -c gpg.ssh.allowedSignersFile=.github/al...
hMailServer 6.2.23 Alpha 1
hMailServer 6.2.23 Alpha 1
Pre-release. Not for production. 83 commits, 345 files, roughly +27,000 / −4,100 lines since v6.2.22-pre6. The database schema moves from 6022 to 6025 in three steps — and if you are coming from 6.2.21, the last stable release, it moves from 6011 to 6025 in fourteen, picking up everything the 6.2.22 pre-release line introduced (app passwords, TOTP, the quarantine, password history, message trace, per-domain relay) on the way. One of the new steps rewrites a column on the largest table in the database.
What this release is
A large feature release cut as an alpha, staying in the 6.2 line — the alpha marker carries the caution, not the version number. It adds reverse-proxy client-address handling in front of SMTP, an external HTTP filtering hook, shared/delegated IMAP mailboxes, a full-text search index, RFC 3464 machine-readable bounces, RFC 3030 BINARYMIME, per-account spam thresholds, a sender blacklist, domain-wide out-of-office, distribution-list moderation, Windows Event Log integration, two more OTLP signals, IPv6 on the management listeners, a disk-space floor, and a substantial Control Panel overhaul. It also fixes one path that was silently losing mail.
What "alpha" means here, in plain terms
- Do not put this on a production mail server. Seventy-two of the commits in this range landed on a single day, and the adversarial pre-release review of that work found six defects — one that filed delegated mail under the wrong mailbox and, on
MOVE, destroyed the only readable copy; a COM interface layout that would have silently broken early-bound clients; a checked-in file that broke the build on any machine but the one it came from; and an unattended upgrade that would have hung forever at an invisible password dialog. All six are fixed and each fix carries a test that fails against the code before it — but a feature found to be broken hours before a cut has not had time to be wrong in a second way. - One feature is known to be untested on the backends it was built for. Statement timeouts were built for PostgreSQL and MySQL and have only been exercised against SQL Server Compact, the bench backend. This is written down in
Roadmap.mdrather than glossed. - If you install it, install it on a test system with mail you can afford to lose, and read Known limitations first.
The database upgrade is one-way
Upgrading.md states this as the one genuine sharp edge in the process, and it applies here with more force than usual because at least three schema steps land at once:
There is no downgrade path... The schema upgrade is one-way: an older server will refuse to run against a newer
dbversionrather than misinterpret it, which is the correct behaviour but means "just reinstall the old version" does not work on its own. To roll back you need the pre-upgrade database backup.
So: take a database backup and a data-directory backup before you install, with your backend's own tools. Rollback is only as good as the snapshot you took first, and if you restore an older database against a newer data directory, any mail that arrived in between will exist on disk with no metadata row and will be invisible.
Breaking changes and upgrade notes
1. Schema 6022 → 6025, in three registered steps
REQUIRED_DB_VERSION is now 6025. The chain is 76 registered steps for MySQL/MariaDB and MS SQL Server (reaching back to schema 0), and 49 for PostgreSQL and the internal SQL Server Compact database (reaching back to 5001). Fresh installations are stamped 6025 by the CreateTables scripts. An installation on 6.2.21 (schema 6011) upgrades through every intermediate step automatically; DBUpdater resumes from whatever version the database reports, so a retry after a partial upgrade continues rather than restarting.
| Step | What it does |
|---|---|
| 6022 → 6023 | hm_messageindexterms (full-text posting list) and hm_messageindexstate (backfill cursor). Both empty until full-text search is enabled. |
| 6023 → 6024 | Six hm_domains columns for the domain-wide out-of-office reply; new hm_blocked_senders table. |
| 6024 → 6025 | hm_messages.messageflags widened tinyint → smallint; three hm_accounts columns for per-account spam settings; two hm_distributionlists columns for moderation and per-list bounce address. |
2. The 6024 → 6025 step alters the largest table you have
ALTER TABLE hm_messages ALTER COLUMN messageflags smallint (MS SQL, SQL CE) and alter table hm_messages modify column messageflags smallint unsigned (MySQL/MariaDB) are table-rewriting operations on the message metadata table. On an installation with millions of message rows this can take a long time and will hold locks for the duration. Plan a maintenance window sized to your hm_messages row count, and do not interrupt it.
PostgreSQL is unaffected by this one — its messageflags was already smallint, so the 6024 → 6025 script for PostgreSQL adds columns only.
The widening exists because FlagSeen through FlagSpam had taken all eight bits a tinyint can hold, and RFC 3030 BINARYMIME needs a ninth.
3. Unattended upgrades used to be able to hang forever; from this release they cannot
If you script installs with /VERYSILENT, read this one. DBSetupQuick forwarded the administrator password to DBUpdater on the create path but never on the upgrade path, and DBUpdater authenticates before it can move the schema — so on any installation with a non-empty administrator password, a silent upgrade fell through to a modal password dialog nobody was there to answer, and the installer waited on it indefinitely. The defect was latent because DBUpdater exits before authenticating when the schema does not move; a release that moves the schema is exactly what arms it, and this is the first release since the fix's own introduction that moves it for every existing installation.
The password is now forwarded on both paths, and the shared authenticator fails instead of prompting under /silent, so the worst case is an exit code the installer reports out loud rather than a wait with no end. Pass the password with /adminpassword=... as before.
4. COM vtable layout repaired — recompile anything early-bound against a 6.2.22 pre-release
Five COM properties added during the 6.2.22 pre-release line and this branch were declared mid-interface instead of appended. A dual interface's vtable follows declaration order, not id(), so four interfaces — AntiSpam, Account, Application, GlobalObjects — had every slot beneath the insertion silently shifted while their IIDs stayed the same. Late-bound scripts (VBScript, PowerShell, PHP COM — the way virtually everyone drives hMailServer) were never affected, because dispatch by id() was always correct.
This release moves all five declarations to the end of their interfaces, restoring exact binary compatibility with v6.2.21, the last stable release. The cost lands in one narrow place: a client early-bound against a 6.2.22 pre-release interop assembly was compiled against the shifted layout and must be recompiled against this release's type library. Since 6.2.22 shipped only as pre-releases, this is the last cheap moment the repair will ever have; the alternative was carrying the silent 6.2.21 break forever.
5. New default behaviour: mail is refused when the disk gets low
MinimumFreeDiskSpaceMB defaults to 100. Below that floor on the volume holding the message store, new mail is refused with a temporary failure — 452 4.3.1 at MAIL FROM, NO [UNAVAILABLE] at IMAP APPEND — and the external POP3 fetcher pauses its whole sweep rather than downloading onto a full disk. DiskSpaceWarningThresholdMB defaults to 1024 and writes one application-log line when the volume crosses into the warning band.
Set MinimumFreeDiskSpaceMB=0 to switch the floor off entirely and keep the previous behaviour. If you deliberately run a server with under 100 MB free, you must set this or mail will start being deferred on upgrade.
6. New default behaviour: hMailServer writes to the Windows Application event log
WindowsEventLogEnabled defaults to 1, at WindowsEventLogLevel=2 (Critical + High). Errors are forwarded from ErrorManager — the one chokepoint every error passes through — to the Windows Application log under the source name hMailServer.
A healthy server writes zero events. Curated ids give the conditions operators alert on their own number: 2010 database unavailable, 2011 listener would not start, 2012 crash, 2013 backup failed, 2014 brute-force protection impaired, 2015 disk floor reached. Unrecognised codes fall into a severity catch-all (2000–2003). A throttle of five per id per ten minutes bounds a flood, and the first event after a throttled window says how many it swallowed. Event id 0 remains reserved for the pre-existing log-write-failure event.
Two operational notes. First, if you have log-collection tooling that alerts on anything new in the Application log, it will now see hMailServer; set WindowsEventLogEnabled=0 to keep the previous silence. Second, registering the event source is a privileged HKLM write no prior version made. The service performs it under LocalSystem, where it succeeds; a service reconfigured to run under a lesser account will fail the registration silently and events simply do not appear.
Message-file caveat, stated rather than discovered: hMailServer has no message DLL. The sink best-effort registers .NET's EventLogMessages.dll as its EventMessageFile. Registered, Event Viewer shows the text cleanly; unregistered, the entry carries the "description cannot be found" boilerplate with the full text underneath — ugly, but complete.