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 C9E8853B89883B02CF0BB3E5FBCA33E87CC6A6E8DD0666CE61B101F2EB95638B. The Linux binaries keep the same promise by a different mechanism, described in RELEASE.md.
Fuzzed. Three libFuzzer targets over the MIME parser and its character handling, thirty minutes each on the release source: mime_message_fuzzer 241,817 executions, mime_header_fuzzer 261,618, mime_decode_fuzzer 7,792,919 - 8,296,354 in all, with no crash, no hang, no leak and no violated assertion. Required this release rather than carried over, because Mime.cpp, CodePages.cpp, Charset.cpp, ByteBuffer.cpp, StringParser.cpp and Unicode.cpp all changed.
And the suite ran against the Linux server. hmailserver/test/LinuxRegressionTests is a net10.0 project that compiles 260 of the suite's own files in place - not copies of them - under a REST-backed fixture layer standing where COM stands on Windows, and runs them against the Linux server on PostgreSQL: 756 passed, 0 failed, 649 skipped of 1,405, in 40 minutes. A test that passes there is the same test that passes here; no fixture is edited and no assertion is weakened. CI runs it on every push, against the binary the same workflow has just built.
The Linux packages install on a clean machine and the installed server answers.
Upgrading
No schema change: 6031, as 6.2.28. The installer's database upgrade runs as usual and has nothing to do on a 6.2.28 database.
Windows. Run the installer over the existing installation, as always. Nothing this release adds is on by default: the REST API needs RestApiPort, which is what the Control Deck and the portal are served on, and a server upgraded without touching its settings behaves exactly as 6.2.28 did.
Linux, first installation. apt install ./hmailserver_6.3.0_amd64.deb or dnf install ./hmailserver-6.3.0.x86_64.rpm, then hmailserver --create-database, hmailserver --set-admin-password, systemctl enable --now hmailserver. The configuration is /etc/hmailserver/hMailServer.ini, which carries a paragraph on every key it sets. The wiki's Installing on Linux page is the walk-through.
Moving a Windows installation to Linux is not a supported migration and this release does not claim it is. The database schema is the same and the message store's on-disk format is the same on both, so the pieces are there; what is missing is a tested path, and the roadmap says so rather than this page implying otherwise.
Known and unfixed
- The installer is not Authenticode-signed. This project has no code-signing certificate, so Windows shows an unknown publisher and SmartScreen warns before it will run the installer, as it has for every release of this fork. What is signed is Sigstore: every asset on the release carries a
.cosign.bundlebeside it, andcosign verify-blobagainst that bundle is the check that means something here. It is also why the server's own updater refuses this project's releases whenUpdateRequireAuthenticode=1; the default is 0 and the Sigstore verification cannot be turned off. - Per-domain DKIM cannot be configured on Linux. As the API section says: there is a read route and no write route, and a
PUTto it answers 404. On Windows the Control Panel sets it; on Linux nothing does. A domain that must sign its outbound mail with DKIM is not one to run on this platform today. - The Linux regression run still skips, and the skips are the API's gaps rather than the server's. A fixture that reaches something no route can answer is ignored with a reason naming what is missing -
hMailServer.iniitself, the administrator password, the event-handler scripting, the anti-spam address lists, the account groups, the incoming relays, an account's or the administrator's second factor, the directory-sync preview, the live log - rather than being weakened until it passes. Those are covered on Windows, where the suite drives COM, and the count above is what remains.