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 starts from what you want to do; the stalled-mail diagnosis guide is a page; the status palette is held apart for colour-blind eyes by a test; "Advanced & scripting" is "Advanced".
- The data directory synchronizer walks the public folders (upstream #601), and the importer refuses a file under
#Publicthe database does not reference rather than guessing where it belongs.
Databases
- TLS to PostgreSQL and MS SQL is a line in hMailServer.ini:
PostgreSQLSslModeandPostgreSQLSslRootCertunder[Database],ConnectionStringOptionsappended to the OLE DB connection string. An unknown sslmode refuses the connection with the reason rather than falling back to "prefer". - A plaintext MySQL is an ini choice rather than a silent downgrade (upstream #559):
AllowUnencryptedConnection=1lets the client fall back from TLS; the default still refuses a server without it. - The schema enforces its own parent-child relationships (schema 6030): seventeen FOREIGN KEYs with ON DELETE CASCADE, so a crash mid-delete can no longer leave rows behind.
Platform and hardening
- OpenSSL 4.0.2 (a security patch release: eleven CVEs, the most severe Moderate) and Boost 1.92.0, built with the tree's own scripts.
- The fourteen open CodeQL C++ findings are dealt with, each on its merits: a byte buffer copied rather than reinterpreted, the Windows 2000 address workaround retired, the SPF address copy a memcpy, the Apple profile served over HTTPS only and its handler no longer named in a way the private-data heuristic reads as a phone number; and the ten
boost::asioTLS-settings findings dismissed as false positives with the reason on record - the query wants everyset_optionscall reachable from a context's constructor to carry the protocol flags as a constant, and Boost's own constructor makes such a call withno_compression, so no program built against a real Boost can satisfy it (verified with the query's own predicates against this tree). Every context is nonetheless built with one constant floor - SSLv2, SSLv3, TLS 1.0 and TLS 1.1 off - and the administrator's protocol toggles act on the mail listeners and the mail client afterwards. The static-analysis backlog is triaged: a repeatable/analyzebuild, 632 findings read, the defect-class ones in the project's own code fixed - among them a service-dependency buffer sized in bytes for a function that counts characters - and the rest named with the reason they stay. - A Release build that keeps its assertions (
build.ps1 -Asserts) is the dynamic-analysis build the suite runs against before a release; a violated assertion is reported as HM6364 naming the expression, file and line. It is never the binary that ships. - A timed fuzz run is a release step, and the C++ tree is spaces, checked. The fuzz harness had not built since 13 August (the MIME parser had gained two dependencies the harness shim did not supply, and the harness's MSVC-environment check was fooled by Git for Windows'
link.exe); it builds again, and this release's run is below.
Build, scanning and supply chain
- The C++ server is compiled on a GitHub-hosted runner on every push and pull request, with OpenSSL, Boost and libpq built from pinned, digest-verified sources and cached - and the weekly C++ CodeQL analysis runs, which closes the second known limitation of 6.2.24.
- The Scorecard result is published to the OpenSSF API, so the badge and the viewer it links to answer; the Code Quality page grades the C# tree Excellent; NuGet lock files hold every restore.
- Twelve upstream ports in all (#550, #552, #554, #559, #566, #580, #592, #601, #602, #603 and two smaller ones).
Upgrading from 6.2.24
The database schema moves from 6025 to 6030 in five steps, one way. DBUpdater runs them in order and resumes from wherever a partial upgrade stopped. There is no downgrade.
| Step | What it does |
|---|---|
| 6025 → 6026 | One hm_settings row for the default special-use folders switch. |
| 6026 → 6027 | Two retention columns, on domains and accounts. Both 0, no policy. |
| 6027 → 6028 | Creates hm_metricsamples. Empty; the sampler fills it a row per metric per minute from the first start. |
| 6028 → 6029 | Creates hm_archiveindex. Empty; only copies made from now on are indexed. (The release check found this step's four statements written as one command, which SQL Server Compact and MySQL refuse; they are four commands in this release. Nobody ran the step before 6.2.25, since 6.2.24 was schema 6025.) |
| 6029 → 6030 | Adds seventeen FOREIGN KEY constraints and removes the orphan rows they would refuse (the create script had them in a spelling Compact Edition lacks; the release check caught that too, and a fresh Compact Edition install builds a complete schema 6030 database) - rows that name an account, domain, list, route or fetch account that no longer exists, which only a crash between a parent's delete and its children's could have left. On a large database this step reads every child table once; plan for it the way you would for an index build. |
Behaviour that changes on upgrade, without a switch:
- The Apple
.mobileconfigprofile is served over HTTPS only. It tells a device which servers to trust with the user's password, and a profile fetched over plain HTTP can be rewritten on the way by anyone on the path. A TLS-terminating proxy in front of the web-services listener says so withX-Forwarded-Proto: https; without that, a plain-HTTP request is answered with a 301 to the same URL on theWebServicesHttpsPortlistener when one is configured, and with a 403 that says why when none is. The Thunderbird autoconfig and Outlook autodiscover documents are unchanged. - A Message-ID is no longer added to relayed mail, only to submissions (authenticated, or from a local domain on a range that does not require authentication). Nothing in the server needed the header to be there; a downstream filter that counts on every message having one will now see the ones that arrived without.
- With IMAP ACL enforcement off, a rule or a delivery into a public folder is allowed - as every IMAP command already allowed it. It used to be refused for want of ACL rows nobody was enforcing.
- The four HTTPS clients - the ACME client, the OAuth2 token client, the MTA-STS policy fetch and the introspection/JWKS client - require TLS 1.2 or later whatever the mail protocol toggles allow. Mail delivery and the listeners follow the toggles as before.
- The AUTH command and the SASL responses that follow it accept lines of up to 12288 octets on SMTP and POP3 (they were 510 and 500). Every other line keeps its limit.
- IMAP sequence numbers are stable within a session (upstream #602). A client that relied on the old behaviour - a message expunged by another session shifting its numbers at once - was relying on a violation of RFC 3501.
Account.DeleteMessagesempties a designated special-use folder and keeps it, the way it has always kept the inbox; IMAP DELETE andIMAPFolder.Deletestill delete one.- A DANE TLSA line that cannot be computed after an ACME issuance is logged as such rather than being silently absent; the certificate is deployed first either way.
New settings, every one of them off or inert until you set it (all in hMailServer.ini unless named otherwise; the Control Panel carries each): DeliveryHardLinks, SpamAssassinLearnOnMove, SpamAssassinUser, SpamAssassinUserFromRecipient, SmtpAuthenticatedSenderCheck, LogonTarpitSeconds, SmtpTarpitCount, SmtpTarpitDelaySeconds, PasswordHashIterations, PasswordHashMemoryKB, PasswordHashTimeCost, OAuth2JwksUrl and its cache seconds, OAuth2IntrospectionUrl and its client credentials, cache seconds and fail-open switch, ArchiveDomains, PostgreSQLSslMode, PostgreSQLSslRootCert, ConnectionStringOptions, AllowUnencryptedConnection, [Security] AdministratorTotpSecret (written by enrolment, cleared to recover), Settings.CreateDefaultSpecialUseFoldersEnabled, Domain.MessageRetentionDays, Account.MessageRetentionDays, FetchAccount.ServerType. Three ship on: OutboundPipelining, OutboundChunking (each used only when the remote advertises the extension) and BackupVerifyRestore. MetricsHistoryDays defaults to 7 and ScriptAllowedObjects to *, which is what earlier versions did without a setting.
COM clients. The type library gained members (Utilities.SearchArchive, SetArchiveHold, GetMetricHistory, SampleMetricsNow, RunMessageRetention; Settings.EnrolAdministratorTOTP and DisableAdministratorTOTP; Application.AdministratorTOTPEnabled; COMAuthentication.AuthenticateWithCode; Domain and Account MessageRetentionDays; Diagnostics.AssertionsEnabled). Clients compiled against the 6.2.24 type library keep working; recompile to reach the new members. FetchAccount.ServerType was always there; 1 now means something.
Known limitations
- Verified by reading, not on a live server: the PostgreSQL and MS SQL TLS keys, and
AllowUnencryptedConnectionfor MySQL. The regression bench runs SQL Server Compact. - IMAP external accounts collect the INBOX only. Other folders are not mirrored; a message that leaves the remote INBOX by other means is forgotten, not deleted locally.
- Statement timeouts were built for PostgreSQL and MySQL and have been exercised only against SQL Server Compact (carried from 6.2.24).
- SCRAM-SHA-1 is not offered (#49, with the reason); clients without SCRAM-SHA-256 use PLAIN or LOGIN over TLS (carried from 6.2.24).
- The fuzz harnesses cover the MIME parser and its codecs. The IMAP command parser, the SPF evaluator and the Sieve parser are exercised by the regression suite, not by a fuzzer.
The second known limitation of 6.2.24 - the weekly C++ CodeQL analysis not running - is resolved: it runs on a GitHub-hosted runner, and the code-scanning page is clear.
Downloads and verification
| Asset | What it is |
|---|---|
hMailServer-6.2.25-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.25
Verify an asset with cosign:
cosign verify-blob --bundle hMailServer-6.2.25-x64.exe.cosign.bundle \
--certificate-identity-regexp '^https://github\.com/Progressiverobot/hmailserver/' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
hMailServer-6.2.25-x64.exe
The server binary is reproducible: two clean Release builds of this tag produce a byte-identical hMailServer.exe, SHA-256 4bfae5c7d9721cc4b2408f2512723369c0156ab445848620b854357a439b3345 (three clean builds, one before and one after the assertion build, all identical), 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: 2001 tests, 1994 passed, 0 failed, the 7 explicit skips, 34 minutes. The same suite on the assertion build (build.ps1 -Asserts, every HM_ASSERT live and reported): 2001 tests, 1994 passed, 0 failed, the 7 explicit skips, no HM6364. Control Panel unit suite: 667/667. Fuzzing on this source (libFuzzer with ASan, LLVM 22.1.8, harnesses rebuilt from this tree): mime_message_fuzzer 265,770 executions, mime_header_fuzzer 274,986, mime_decode_fuzzer 4,793,507, thirty minutes each, no crash, no hang, no out-of-memory, no assertion.