Repository navigation
Download
| Windows, x64 | hMailServer-6.3.8-x64.exe: the installer, Authenticode-signed by Progressive Robot Ltd. It installs a new server, or upgrades an existing one in place: read the notes below and back up the database and the data folder first. SHA-256 4b9b9c3102cbed743f6c826289c54cec25c395d02208e55cdd3219187fbf8654. |
| Windows on ARM64 (preview) | hMailServer-6.3.8-arm64.exe, signed the same way. A preview: it has not been run on ARM64 hardware. A new installation is set up against Microsoft SQL Server or PostgreSQL, since SQL Server Compact has no ARM64 build (WindowsOnArm.md). SHA-256 e5ce357b9ca4e013529898c5208d43895fec3a397b1a4defa4cd905ee9b7f27a. |
| Linux | The .deb and .rpm packages for each distribution and the two AppImages, under Assets below, in Packages; hmailserver-linux-6.3.8-SHA256SUMS.txt lists them. |
| Container image | registry.gitlab.com/hmailserver/hmailserver:6.3.8, for linux/amd64 and linux/arm64, pushed by this release's pipeline after the release is published. :6.3 and :latest are moved to it too, unless a later version has either already. |
| Checking a download | Each file the release pipeline made is signed with Sigstore, its bundle beside it as <file>.cosign.bundle, and hmailserver-6.3.8.intoto.sigstore.json is the release's SLSA build provenance (hmailserver-6.3.8.intoto.jsonl is its signed envelope alone, as one line of JSON); Verification, at the end of these notes, gives the identity to check them against (SECURITY.md, "Verifying a release"). |
hMailServer 6.3.8
Everything that landed on master after the v6.3.7 tag, from 17 October 2026 (compare v6.3.7 with
v6.3.8). This is Progressive Robot's fork of
hMailServer. 6.3.8 is a security release: it fixes vulnerabilities the project found in its own code, each a work item
with a CVE request through GitLab's CNA, and the four narrower cases of work item 78 that 6.3.7's notes left open.
Between them they reach back to 6.0.0's first beta: upgrade every server running 6.3.7 or earlier. It was first
decided, while releases pause, because the published Helm charts of 6.3.6 and 6.3.7 did not start a server,
which only a release puts right, and that reason stands beside the fixes; it also puts right the container image, which
could not reach a MariaDB database, and the Windows installer, which ran over an installation of the original
hMailServer's 5.7, and it carries the finished work master holds (Why there is a 6.3.8, below). The schema stays at
6108, as 6.3.6 and 6.3.7 have it: 6.3.8 adds no upgrade step. What changes on upgrade without a switch follows the
Security section.
What it fixes, and for whom. Each is in its section below, with what changed and how it is tested. The
vulnerabilities first; their work items are made public, and their advisories published, once 6.3.8 is on the update
feed:
- The database layer's test for the marker that lets a statement's error be ignored, and the log's and the trace's
redaction of a statement's literals, read a literal as each engine does
(work item 100; Security). - The auto-ban saves the IP range it adds under a name of its own, so that it bans the address
(work item 102; Security). - A sign-in's name is matched to its account as the per-name lockout and the compromised-account hold count it
(work item 104; Security). - The web services listener (CardDAV, CalDAV, Autodiscover) refuses an address an auto-ban or an IP range bans
(work item 105; Security). - A failed sign-in at the web services listener through a proxy is no longer charged to the proxy's address
(work item 106; Security). - A failed account sign-in at the REST listener through a reverse proxy on the same machine is no longer charged to
127.0.0.1 (work item 107; Security). - The REST API, and the reader of a report mailed to a domain's report address, read a JSON body in time and memory
that follow its bytes (work item 110;
work item 111; Security). TlsMode=verify-fullon MySQL and MariaDB checks the database server's certificate against the CA fileTlsCaFile
names (work item 112; Security). Its entry corrects
what 6.3.7's notes said of a namedTlsCaFileas the mitigation for CVE-2026-107819, MariaDB Connector/C's own flaw.
On Linux the client is the distribution's, which should be MariaDB Connector/C 3.4.11 or later; where it is older and
not patched, name the CA inTlsCaFile(DatabaseTls.md).- The database client's
TlsMode=verify-fullcheck of a database server on this machine, on MySQL and MariaDB
(work item 113; Security). - On Windows the database clients check the database server's certificate against the root certificate file named, or
refuse its name (work item 114; Security). - On Windows the files the server hands to OpenSSL by name are read from the file configured, or refused
(work item 115; Security). - On Windows the bundled MySQL and MariaDB client, MariaDB Connector/C, loads its authentication plugins from the
installation's own plugin folder, or from none
(work item 116; Security). - On Windows, setup makes or hardens the program folder,
BinandControlPanelbefore it copies a file or runs a
program of the installation, and refuses a folder or a link on the way to them, or a program, library or script in
them, that another account controls - the four narrower cases of work item 78 that 6.3.7's notes listed as still
open (work item 78; Security). - Published with it: the advisory for a flaw in the auto-ban on MySQL and MariaDB that 6.3.4 fixed
(work item 103).
Each was found by the hMailServer project (Progressive Robot Ltd). The other fixes and changes an administrator meets:
- The Helm chart starts a server (Downloads and the release, Correctness). Installed into a cluster, the charts
of 6.3.6 and 6.3.7 never started a pod: the image names its user, which Kubernetes cannot hold to the chart's
runAsNonRoot, and once given the user by number the server stopped at a configuration volume that hid the image's
settings file. 6.3.8's chart runs the pod as the image's user by number, 999:999, fills new volumes in an init
container, and secures a new installation at its first start, as the packaged systemd unit does; so does the Compose
file. The pod setsnet.ipv4.ip_unprivileged_port_startto 0 in its own security context, so that the server binds
the mail ports under containerd 1.7 and CRI-O too. A release of the 6.3.6 or 6.3.7 chart is moved withhelm upgrade --reset-then-reuse-valuesand thenkubectl delete pod(Before you upgrade, below). - The container image reaches a MariaDB database (Downloads and the release). The images of 6.3.7 and earlier
carried PostgreSQL's client alone, so a container toldHM_DB_TYPE=MySQLstopped at its first start; 6.3.8's
carries MariaDB Connector/C (libmariadb3), and the release's pipeline starts each platform's pushed image beside
PostgreSQL and beside MariaDB and asks each for SMTP, the REST API and the schema it laid out. The chart passes a
database port only when one is set, so a chart toldMySQLno longer dials MariaDB on PostgreSQL's port. Found by
the project's own audit of its listings. The image's entrypoint also secures a new installation by itself, whatever
the container's command, and takes the database and administrator passwords out of the environment before it runs
any program (Security). - The Windows installer refuses an installation of the original hMailServer's 5.7 (Correctness). Both projects'
installers register ashMailServer_is1, so the installers of 6.3.7 and earlier took an installation of the
original's 5.7 builds of August and September 2026 (schema 5709 to 5711, which this fork's upgrade chain has no step
from) for their own and replaced its programs. 6.3.8's setup asks first, before a page is shown or anything is
installed or stopped, refuses such an installation with a message that says nothing has been changed, and, silent,
ends with exit code 1 and writes the reason to its log (/LOG) and tohMailServer-setup-refused.txtin%TEMP%.
An installation of this fork, or of the original up to 5.6, is upgraded as before. The installers of 6.3.7 and
earlier do not look: never run one over the original's 5.7. - A host added to
AcmeDomainsno longer waits for the renewal, and a check can be asked for
(work item 98,ThiagoES84; Correctness,
Administration, Control Panel). The hourly check never looked at a certificate's names, so an added host waited
for the renewal, some sixty days. A certificate that does not name every host the list holds is now due at once, a
save ofAcmeDomainsin the running server starts the check (a valuehmailserver --set-settingstores is read at
the next reinitialise or start), the certificate is issued once the CA has validated every host on the list, and
GET /api/v1/acmeandPOST /api/v1/acme/run(COMUtilities.GetAcmeStatusandUtilities.RunAcmeCheck, the
Control Deck's Certificates page) give the certificate's state and ask for a check, one at a time, with the CA's
rate limits and a pause after a failed attempt kept whatever starts it. The certificate a CA sends is installed only
when it is for the request's key, names every host asked for and has not expired. - An event after an IMAP move, for scripts
(work item 109,ThiagoES84; Administration).
OnIMAPMessageMovedis called once for each message an IMAPMOVEorUID MOVEmoved between two folders of the
account signed in, after the command has done all of its work and off the IMAP thread. Off as shipped:
IMAPMessageMovedEventsswitches it on. - Rspamd learns from the Junk folder, beside SpamAssassin
(work item 108,ThiagoES84; Administration). A
message a user moves into Junk is taught as spam and one moved out as ham to the filterMoveLearningBackendnames:
SpamAssassin, the default, taught as before, or Rspamd through its controller, for the classifier
RspamdClassifiernames. Nothing is sent to Rspamd until it is chosen and its controller and classifier are set
(JunkFolderLearning.md). - A REST request body meant to be JSON must be one JSON object (Correctness). The server answers 400, saying
why, before it reads one that is not, names a member twice or passes the bounds the entry gives, and every member
reader reads the one parse, so that a key spelt inside a value is no longer read as the member it is not. - The database client's lines say what it checks (Correctness):
--check-databaseand PostgreSQL's connection
line name the root certificate libpq is handed and what it checks. - On Linux a file's name outside ASCII is one name, UTF-8, whatever
LANGsays (Correctness). It was two files,
or a file and an exception, by the call that opened it, and aLANGnaming a locale the machine does not have
stopped the server at its start. spamd'sUser:header now carries a name outside ASCII, and the legacy Blowfish
scheme refuses a secret it cannot carry, saying whose it was, where it wrote an empty one. A server that ran under a
LANGthat is not UTF-8 renames its folders first (Before you upgrade, below). - On Alpine, log lines and other text holding a character outside ASCII are written (Correctness), where each
came out empty. - A stop of the servers is no longer reported as errors of their own (Correctness). Work that outlasted a stop -
a reinitialise among them - met the stop's interruption and wrote it to the error log as a failure (HM5136 and the
other codes the entry names), which also ran an installed OnError script; each now writes a line to the debug log
and ends as the stop asked, and a database connection such a call held goes back to the pool. A session or a task
that went on after a stop no longer reaches for what the stop let go of, which on Linux ended the process. - The Linux server reaches clamd over its Unix socket (Administration):
ClamAVHosttakes the socket's
absolute path (ClamAV.md). - The command-line clients (Correctness): the PowerShell module says why a call got no answer under Windows
PowerShell 5.1, andSet-HmOpenPgpKey -Pathsends the key there;hmctlreads a file that begins with a byte
order mark - a configuration document,--from-json,--password-fileand--passphrase-file- and refuses a
body holdingNaNorInfinitybefore it is sent. - The Control Panel (Control Panel): the Logging page names the destination a new installation has as Files on
disk; the Anti-spam page opens and saves on a server whose refuse threshold is at or below its mark threshold,
saying what that pair does, and the Anti-spam overview reads the quarantine switch; the ACME page and the Setup
checklist readAcmeDomainsas the server does. - Who made what. The packages, the image, the chart, the snap and the command-line clients say that they are
Progressive Robot's fork of hMailServer and credit the original's author, Martin Knafve; his copyright notices are
back on the original's files; the tree is REUSE-compliant, with a CI job that holds it; the third parties whose
work the tree carries are credited under their licences - Feather's icons (Cole Bemis), the Contributor Covenant,
commonmark.js (John MacFarlane), GitLab's secret detection rules and Microsoft's Fluent icons in the README's
screenshots; and two pieces of third-party code that stated no licence - the installer's service routines and the
tools' tab-order helper - are written anew (Correctness, Downloads and the release, Build and tests). - New ways to install (Downloads and the release): an Ansible collection,
progressiverobot.hmailserver
(Ansible.md); a Kubernetes operator for Operator Lifecycle Manager; and a container on
Proxmox VE with one command on the host (Proxmox.md), whose scripts have not yet run
on a Proxmox VE host (Known issues, below). - The release's files (Downloads and the release): the release carries
hmailserver-6.3.8.intoto.jsonl, its
SLSA build provenance as one line of JSON besidehmailserver-6.3.8.intoto.sigstore.json; the provenance is held to
the pipeline that publishes it and to what GitLab certified of the run; and the documents say its SLSA level, Build
L1 and no higher.
Reported from outside and answered besides: on-demand ACME issuance (work item 98), Rspamd learning from the Junk
folder (work item 108) and an event after an IMAP move (work item 109), each asked for by ThiagoES84. Thank you.
Why there is a 6.3.8. While releases pause, a release is made for a confirmed vulnerability, and other defects
ship with the next one (SECURITY.md, "What gets a release"). 6.3.8 was first decided on 24 October 2026
under the owner's delegation for another reason: the published charts of 6.3.6 and 6.3.7 start no server, and neither
the chart nor the image of a release can be put right but by a release. The project's own work since found the
vulnerabilities above, each with a CVE request, and 6.3.8 is made as a security release (SECURITY.md,
"How a security release is made"), the chart's reason standing beside them. One of its fixes, work item 100's, was
public on GitLab from 19 October 2026 and on master from 20 October 2026, a week before it was judged a
vulnerability, as SECURITY.md says; the others were pushed with the release. Work item 108 and work item 109, asked
for on 29 October 2026, were put into it on 30 October 2026, each off until it is switched on. Like every release
during the pause it is cut from master with the finished work master holds then, and so it carries everything that
landed after 6.3.7: the work above, the channels' packages and texts, and the documents.
Signing, and who installs this release by the live update. Every file is signed with Sigstore as this release's
own pipeline at the group's path, https://gitlab.com/hmailserver/hmailserver//.gitlab-ci.yml@refs/tags/v6.3.8,
issuer https://gitlab.com, with the project's id, 86730559, in the certificate, as 6.3.6 and 6.3.7 were. Every
server from 6.3.4 on has that signer compiled in and requires the id for it. The update is applied by the version
already installed, so:
- From 6.3.7 or 6.3.6 the live update installs 6.3.8, with the running version's own file as its rollback image.
A Windows service running as LocalSystem installs it itself; one running asNT SERVICE\hMailServerdownloads and
verifies it, and an administrator installs it with Install update on the Server status page of the Control Panel
on the server: one Windows prompt, and the installer is checked again, elevated, before it runs. On Linux the
installed version's update helper applies it, for the generic.deband.rpmof either architecture and for both
AppImages; an ARM64 Windows server finds the ARM64 installer by name. A server installed from one of 6.3.6's or
6.3.7's per-distribution packages is offered the generic package, which apt refuses on Debian 12 and dnf on
Enterprise Linux 9: upgrade it with its package manager and its own release's package of 6.3.8. No live update is
known to have installed a release signed at the group's path when these notes were written. - From 6.3.4 or 6.3.5 what 6.3.7's notes said of those two versions still holds: a Windows service running as
LocalSystem installs 6.3.8 by its live update, from 6.3.5 with 6.3.5's file as its rollback image, which is no way
back once the upgrade's schema steps have run, and from 6.3.4 without one; one running asNT SERVICE\hMailServer
leaves the install to an administrator, whose Install update on those versions cannot do it, so install 6.3.8 by
hand, once; on Linux install it with apt, dnf or zypper rather than by the live update, whose apply step on 6.3.4 and
6.3.5 is the one 6.3.6 fixed, or turn the apply off first withsystemctl disable --now hmailserver-update.path. - A server whose
UpdateSigningIdentity,UpdateSigningIssuerandUpdateSourceRepositorystill name the path the
project began at refuses every release signed after the project moved, 6.3.8 included: clear the three first, as
UpdateFeed.md says. - From 6.2.28 to 6.3.3, and from 6.3.3's AArch64 packages or AppImages: by hand, once, after the backup
Upgrading.md describes (UpdateFeed.md, "A server
from 6.2.28 to 6.3.3"). Older than 6.2.28: run the installer, as always; the upgrade chain runs from every
earlier version of this fork, and from the original hMailServer up to 5.6 and its 5.7 development builds up to
schema 5708, and not from the original's 5.7 releases, which 6.3.8's installer refuses
(Migration.md, "From the original hMailServer", says how such a server's mail moves). - The container image,
registry.gitlab.com/hmailserver/hmailserver:6.3.8, also tagged6.3andlatest, is
pushed from this release's own pipeline once the release is published, by a job started by hand, as 6.3.6's and
6.3.7's were: in the container the update check names the image to pull and installs nothing
(ContainerImage.md).
By hand on Windows: download hMailServer-6.3.8-x64.exe from this release, check that Get-AuthenticodeSignature
reports it Valid and signed by CN=Progressive Robot Ltd, and run it as an administrator, keeping the service
account on the installer's Service account page. On a machine that has not seen the file before, Windows
SmartScreen may say that it "prevented an unrecognized app from starting": More info names the publisher,
Progressive Robot Ltd, and with the signature checked, Run anyway installs it.
Back up first; no schema step. Take the backup Upgrading.md describes - the
database and the data directory, each with its own tools - before installing. 6.3.8 requires schema 6108, as 6.3.7
and 6.3.6 do, and adds no upgrade step, so an upgrade from either changes no table, and the live update's rollback,
should 6.3.8 not start, meets no newer schema. From 6.3.5 or older the upgrade runs 6.3.6's three steps to 6108,
which are one-way (6.3.6's notes; from 6.3.3 or older 6.3.4's steps come first), and the backup is the only way back.
The .NET 10 Desktop Runtime. The installers carry 10.0.13, as 6.3.7's did, and install it only on a machine that
has no .NET 10 Desktop Runtime at all: an upgraded server keeps the runtime it had. Bring it up to date through
Microsoft Update, or with Microsoft's installer (https://dotnet.microsoft.com/download/dotnet/10.0);
dotnet --list-runtimes lists what a machine has (Upgrading.md, "The .NET 10
runtime").
Before you upgrade. What an upgraded server does differently with no setting turned is in What changes on upgrade
without a switch, below. A server upgraded from 6.3.6 or earlier meets as well what 6.3.7's section says under its
Before you upgrade and in its list, and one from 6.3.5 or earlier what 6.3.6's list says. The changes most likely to
need something done:
- A release of the Helm chart of 6.3.6 or 6.3.7 (its pod never started):
helm repo update,helm upgrade <release> hmailserver/hmailserver --reset-then-reuse-values(Helm 3.14 or later), thenkubectl delete pod <release>-hmailserver-0, since the StatefulSet replaces no pod while one is not Running and Ready. With
--reuse-valuesinstead the release keeps the old chart'sfsGroup1000 anddatabase.port5432; the pod still
runs as 999. In a cluster whose policy refuses every sysctl, setunprivilegedPortsSysctl: false. - A Compose deployment made from an earlier Compose file: keep its
HM_REST_CERTIFICATE_FILE,
HM_REST_PRIVATE_KEY_FILEand./certsmount in the new file, the key readable by 999, or its REST API, and with
it the Control Deck, the webmail andhmctl, does not start (ContainerImage.md,
"Updates"). AcmeDomainslisting a host the certificate lacks: the first check orders a certificate for every host on the
list. A host the CA cannot validate - it does not resolve to the server, or its port 80 is not reached - fails the
whole order, at most three times an hour, and holds the renewal for the other hosts with it: take such a host off
the list before upgrading.TlsMode=verify-fullon MySQL or MariaDB through MariaDB Connector/C withoutTlsCaFile, to a server on this
machine (127.0.0.1,::1,localhost, an emptyServer=,.for a named pipe on Windows, or aServer=list
that holds one; Connector/C is the client the Windows installer bundles and the one the Linux server loads,
libmariadb.so.3): 6.3.8 refuses the connection with HM7535. Name inTlsCaFilea CA that issues nothing but the
database's certificates, the one that issued its certificate (DatabaseTls.md), or
setTlsMode=require, before upgrading.- On Windows, a
TlsCaFile(withTlsMode=verify-full) or aPostgreSQLSslRootCertwhose path holds characters
outside ASCII: 6.3.8 may refuse the name with HM7534, and then does not attempt the connection to the database.
Give the file, and the folders above it, an ASCII name before upgrading
(DatabaseTls.md). - On Windows, a TLS certificate or private key file,
dh2048.pem, a client-certificate CA file,
MessageStoreObjectCaFile,OAuth2PublicKeyFileorAcmeCertificateDirectory, whose path holds characters outside
ASCII: 6.3.8 may refuse the name with HM7536, and then what the file is for does not work - a port whose
certificate or key it names does not start. Give each such file, and the folders above it, an ASCII name before
upgrading. - A Linux server that ran under a
LANGthat is not UTF-8, with a data directory, a domain or account folder,
LogFolder,TlsCaFileor a TLS certificate or key file named outside ASCII: 6.3.8 names files in UTF-8 whatever
LANGsays, so rename each to its UTF-8 name before upgrading, for example with
convmv -f iso-8859-1 -t utf-8 -r --notest <folder>. Under a UTF-8LANG, the packages', nothing changes. - IP ranges of your own that allow some addresses none of SMTP, POP3 and IMAP, with the web services listener on:
such a range now closes that listener to those addresses as well - ACME's http-01 validation when this listener
answers it, MTA-STS, CalDAV, CardDAV, Autodiscover, autoconfiguration and the Web Key Directory - so check that the
certificate authority, the sending MTAs and these services' clients do not come from one. - A REST client of your own that sends a body that is not one JSON object, names a member twice, holds an escaped
U+0000 or passes the bounds (100,000 values, 32 levels deep), or that writes a member whose absence is the wider
setting as another type than its route reads - a quoted flag, an array for a text: the server answers it with 400
(REST request bodies, in the list below). - A mail client that signs in with another spelling of the account's address than the stored one, in more than
the case of the letters A to Z, or with white space at either end: it may no longer sign in. Correct the name in the
client (Sign-in names, in the list below). - Installing with Chocolatey: its install now passes the installer
/LOG, and the log records setup's command
line, so give the administrator password inHM_ADMIN_PASSWORD, set in the elevated prompt, rather than with
/adminpasswordin--install-arguments(PackageManagers.md). - On Windows, a program folder,
BinorControlPanel, or a folder or a link on the way to them, that another
account than SYSTEM, the Administrators and TrustedInstaller controls: setup refuses it with exit code 7, naming
theicaclsthat deals with it, and asks for a restart of Windows before it runs again
(ServiceAccount.md). - Anything that counts the error log or relies on an OnError script during a stop: a stop's interruption is no
longer written there; an event script's call that meets a stop fails with "The server is stopping. The request was
not completed." and its script error goes to the debug log. - On Windows, an installation of the original hMailServer's 5.7 is refused by 6.3.8's setup before it changes
anything; move its mail over IMAP instead.
What 6.3.8 fixes of what 6.3.7 left open, and what it does not. The four narrower cases of work item 78 that
6.3.7's notes listed as still open are fixed in this release
(work item 78, above). The original hMailServer's 5.7
releases are still not on the upgrade chain: 6.3.8's installer refuses an installation of them, and its mail moves
over IMAP (Migration.md, "From the original hMailServer").
Known issues.
- The ARM64 installer has never run on ARM64 hardware. It is cross-compiled on the project's x64 Windows runner and
remains a preview (WindowsOnArm.md); the AArch64 Linux files are cross-compiled,
and installed - the generic.deband.rpmstarted as well - in the release's pipeline under emulation: no AArch64
machine has run them. If you install either, please say how it went, working or not, in
an issue. - The channels are published to by jobs started by hand. The Ubuntu PPA, Fedora Copr, Docker Hub, Chocolatey,
PyPI and the PowerShell Gallery are sent 6.3.8 from this release, verified against its bundles, by a job started by
hand onmasteronce the release is published; a Chocolatey version is listed once its moderators have approved
it. The openSUSE Build Service's job runs once its package holds this tree's new_service, which a maintainer puts
in after the release is published, since 6.3.7's tarball lacks a file it takes; until then it keeps 6.3.7. A job
started the same way uploads the snaps to the Snap Store'sedgeandcandidatechannels,stablebeing promoted
by hand; 6.3.8's are the first there, since 6.3.7's snaps could not read their configuration and were never uploaded
(Channels.md, Snap.md). The Ansible collection is not on
Ansible Galaxy yet (Ansible.md): the collection in the tree pins 6.3.7's.debfiles,
and installs that release unless it is given another's SHA-256, until the collection built for Galaxy pins 6.3.8's.
The operator's pull request to OperatorHub.io is made by hand. - The Proxmox VE scripts have not run on a Proxmox VE host. None was available: the container's creation is shown
by a dry run and by stand-ins of Proxmox VE's commands, and the install ran in a container made from Proxmox VE's own
debian-13-standardtemplate (Proxmox.md, "What has run"). If you run them, please
say how it went in an issue. - The image's OpenSSL is Ubuntu 24.04's 3.0.13, which has neither the hybrid post-quantum key exchange groups
(OpenSSL 3.5) nor Argon2id (3.2): its server logs HM5720 at its start and takes the classical groups, makes with
scrypt the hashes it would make with Argon2id, andGET /api/v1/statusnow saysfalseforargon2idand
post_quantum_key_exchange, from the library it runs with (ContainerImage.md,
"What is not shown yet"). - An AppImage on Fedora, Enterprise Linux or openSUSE runs on the host's glibc and cannot require its extra
character-set converters: installglibc-gconv-extra(Fedora, EL) orglibc-gconv-modules-extra(openSUSE) by
hand; the server says when they are missing.
Verification. hMailServer.exe 6.3.8.47, built from this release's source by build\build.ps1 -Configuration Release with the toolchain RELEASE.md step 8 names, has the SHA-256
6baa2c2348cea4f83cbffae33e0f02f60b1ee843c9315852eb95d2a0ecd9e881, the same from clean and from a second checkout at
another path (ReproducibleBuilds.md).
Security
-
Fixed:
TlsMode=verify-fullon MySQL and MariaDB checks the database server's certificate against the file
TlsCaFilenames when that name holds a character outside ASCII, where the client was given no CA file at all.
In 6.3.4 to 6.3.7 such a name - on Linux any character outside ASCII, on Windows one above U+00FF - reached the
MySQL client as no CA file while certificate verification stayed on, so that the certificate was checked against
the system's trust store in place of the named CA, and with some versions of MariaDB Connector/C less than that:
on a connection the client takes as one to this machine, 3.4.1 and later checked neither the certificate's issuer
nor its name; and over the network, with 3.4.2 to 3.4.10 a certificate whose issuer the client could not find was
taken unchecked (MariaDB's CONC-853, fixed in Connector/C 3.4.11), and with 3.4.1 to 3.4.9 such a connection was
open to CVE-2026-107819. The Windows
builds of 6.3.4 to 6.3.6 bundle Connector/C 3.4.9; on Linux the distribution's client is used. The settings line of
--check-databasenamed the configured file all the same, and the application log and the diagnostics said the
certificate had been checked. So 6.3.7's notes, which called a namedTlsCaFilethe mitigation for CVE-2026-107819,
did not hold for such a name. On Linux with no locale set, and on Alpine, such a name failed the connection instead.
From 6.3.6 PostgreSQL's root certificate was handed to libpq, in UTF-8 on Linux and in the code page on Windows (in
6.3.4 and 6.3.5 its whole connection string went through the same conversion, which 6.3.6 fixed), and SQL Server
reads no file. Now the name is handed to the
client in the encoding it opens the file in - on Linux UTF-8, on Windows the ANSI code page - and a name that cannot
be written so is refused before anything is attempted, with HM7534 (new) in the error log, and never handed over as
nothing.DALConnection::FileNameBytesis the one conversion, which PostgreSQL's root certificate goes through
too. On Windows the authentication-plugin folder the bundled client is handed is always one that only
administrators control, and never empty:Bin\plugin, in the code page it loads a plugin in, or by the folder's
short name (MySQLConnection::PluginDirectoryBytes), and where neither can be written, a folder under the Windows
system folder that holds no plugin, so that an account whose plugin is only inBin\pluginis refused with the
client's own error; where even that cannot be written, the connection is refused before anything is attempted,
with HM7537 (new) in the error log (MySQLConnection::PluginFolderOption).
hmailserver/docs/DatabaseTls.mdsays what the client is given, with HM7537 in its table, no
longer asks for the path in ASCII, and asks on Linux for MariaDB Connector/C 3.4.11 or later, and
hmailserver/docs/ThirdPartyBinaries.mdsays which plugin folder the client is handed. connection-string-tests'
MySQLConnectionTester, which ClassTester runs on Windows, holds the name's bytes for both clients, the refusals,
the plugin folder's name and the folder handed where that name is refused.
build/check-mysql-plugin-folder.py, run by the checkers job with its self-test, holds that the connection hands
the client that folder and no other: the option is set in one place, from the folder
MySQLConnection::PluginFolderOptiongave, andPluginDirectoryBytesis called byPluginFolderOptionand the
tester alone.
Found by the project's own review. -
Changed: a command run as root on Linux examines and opens the folders it will work in by the names the server
uses for them, through one function. The store commands, the object-store copies,--sample-domain,
--apply-profile,--revert-profileand--apply-environment, run as root, decide whether to become the data
directory's owner or to go on as root by examining the data directory, the log, temporary, event and database folders
and the configuration that names them (RootCommand,docs/ServiceAccount.md). In 6.3.6 and 6.3.7 they examined
those folders by the Latin-1 bytes of their names, while the server reaches them by the UTF-8 ones, and their own
file operations in the log, temporary, event and database folders went by the same bytes, so that for a name with a
character outside ASCII the commands missed the folder the server uses. Now the examination and
the commands' own file operations both go by the name the server uses, through one function
(FileUtilities::NarrowPath, below), so that they stay on the same folder. -
Fixed:
TlsMode=verify-fullon MySQL and MariaDB is refused withoutTlsCaFileon a connection the client takes
as one to this machine, where neither the database server's certificate's issuer nor its name was checked. In
6.3.4 to 6.3.7, with MariaDB Connector/C 3.4.1 or later as the client - the one this server bundles on Windows, and
the one the Linux packages suggest and the server loads - such a connection, to a database server on this machine by
a loopback address,localhost, a Unix socket or a named pipe, or through aServer=list that holds such a server,
was made with neither the certificate's issuer nor its name checked, while the settings line of--check-database
said it had been checked against the system's trust store, and the application log and the diagnostics that it had
been checked. It is now refused before anything is attempted, with HM7535 (new) in the error log, which says to name
the CA that issued the database server's certificate inTlsCaFile, or to setTlsMode=require. The lines that
say what was checked now say what the client checks (MySQLConnection::VerifyFullCheck). Connector/C 3.3 and 3.4.0
and Oracle's MySQL client were not affected.hmailserver/docs/DatabaseTls.mdsays what each client checks, with
HM7535 in its table. connection-string-tests' MySQLConnectionTester, which ClassTester runs on Windows, holds which
servers the client takes as local, the refusal and each line. Found by the project's own review. -
Fixed: on Windows a root certificate's name whose bytes in the ANSI code page outside ASCII also read as UTF-8 is
refused, where the database's client could check the server's certificate against another file than the one
named. In 6.2.25 to 6.3.7 on Windows the filePostgreSQLSslRootCertnames (from 6.2.25) or, with
TlsMode=verify-full,TlsCaFile(from 6.3.4) was handed to libpq - andTlsCaFileto Oracle's MySQL client where
one is put in place of the bundled MariaDB Connector/C - by its bytes in the code page (up to 6.3.5, and to Oracle's
client up to 6.3.7, by its Latin-1 bytes), which the client's OpenSSL could open as a file at another name. Such a
name is now refused before anything is attempted, with HM7534 in the error log, whichever client is loaded, as one
the code page cannot carry is:TlsCaFilebyDatabaseTls::FromConfiguration,PostgreSQLSslRootCertby
PGConnection::Connect, where 6.3.6 and 6.3.7 refused only a character the code page lacks. The bundled MariaDB
Connector/C and the Linux builds were not affected, and a Windows whose code page is UTF-8 is handed the name in
UTF-8 (6.3.6 and 6.3.7 refused every PostgreSQL root certificate name there).DALConnection::FileNameBytessays
so, and connection-string-tests'
PGConnectionTester, which ClassTester runs on Windows, holds the refusal. Found by the project's own review. -
Fixed: on Windows the files the server hands to OpenSSL by name are read from the file configured, or refused.
The certificate and private key files of the TLS certificates - the REST API's, the metrics listener's and the web
services' among them -dh2048.pem, a TCP/IP port's client-certificate CA file,MessageStoreObjectCaFileand the
files inAcmeCertificateDirectorywere handed to OpenSSL in an encoding in which a name with some characters
outside ASCII could name another file than the one configured, in every release up to and including 6.3.7. Each name
now goes through one conversion,FileUtilities::OpenSslPath, which holds to the rule the database's CA files
follow (FileUtilities::FileNameBytes, whichDALConnection::FileNameBytesnow calls), and a name it cannot hand
over as it is named is refused, with HM7536 (new) in the error log, together with what the file was for: a port
whose certificate or key it names does not start, a client-certificate CA file verifies no certificate, the object
store's CA file allows no request, the public key verifies no token,dh2048.pemis left out as a missing one is,
and the ACME client reads and writes nothing there and orders no certificate. A name in ASCII is handed over as
before, and on Linux every name in UTF-8, the bytesFileUtilities::NarrowPathgives every other call there (under
Correctness), which OpenSSL opens as they are. The Control Panel holds the same rule where it reads these files on
the server's own computer: the SSL certificates page shows such a certificate or key file as Name refused, at
Critical, and the setup checklist's items on the TLS private keys, the client-certificate CA files and
OAuth2PublicKeyFilesay the server does not read it (error 7536), since they read the file by its wide name, which
works whatever the name.OAuth2PublicKeyFilewas handed over in UTF-8 up to 6.3.7, so a name outside the code page
that worked then is refused now.build/check-openssl-file-names.py, run by the checkers job with its self-test,
holds every call in the server that opens a file through OpenSSL by its name to a nameOpenSslPathgave.
DatabaseTls.md says which names are refused and what to do, and Acme.md, ObjectStorage.md and
RelocatingAnInstallation.md say so beside the settings. Found by the project's own review. -
Fixed: the container image secures a new installation by itself, whatever the container's command, as the
packaged systemd unit's and OpenRC service's first starts do. Its entrypoint now runshmailserver --apply-new-install-defaults --if-markedlast, before the command: a database the entrypoint's--create-database
made a moment before gets a self-signed certificate, STARTTLS on 25, 587, 110 and 143, AUTH on 25 a honeypot and a
password taken from another address only over TLS, and at every other start the step opens nothing; a failure (run as
root, a database not reached) is said and forgiven, the server starting all the same, as the unit'sExecStartPre=-
forgives it. Until now only the Compose file's and the Helm chart's commands ran it, so adocker runof the image
with its own command took passwords in the clear on every port, and--create-databasetold such a container's
administrator to secure it by hand - and told the Compose file's and the chart's the same, whose next step did it. The
entrypoint gives its--create-databaseHM_ENTRYPOINT_SECURES_NEW_DATABASE=1, which makes it say that the
entrypoint secures the database next; run anywhere else, it says what it said before. The Compose file and the chart
keep their own step, for an image published before, whose entrypoint does not run it; with this image it finds no
marker and does nothing. SecureDefaults.md, ContainerImage.md and CommandLine.md say so. -
Fixed: the redaction of a statement's literals in the log and the trace, and the test for the marker that lets a
statement's error be ignored, now read a literal as each engine does.SqlLiteralScanTesterholds both to each
engine's reading, inside the server on Windows (ClassTester) and inconnection-string-testson Linux. Found by
the project's own review. -
Changed: the container image's two passwords no longer reach the server's environment, nor that of anything it
starts. The entrypoint readHM_DB_PASSWORDandHM_ADMIN_PASSWORD(or their_FILEforms, whose contents it
exported into the plain variables), used them, and then ran the server - the container's PID 1 - with its whole
environment, so that the server's verbs it ran, the server itself and every program the server starts, 7-Zip, the
auto-ban hook, event scripts, a virus scanner and the update helper among them, held both. A program the server
starts holds no capability, and its/proc/<pid>/environshowed them to any program running as the image's user
while it ran (6.3.7's image: the auto-ban hook the server runs at its start held both, plain or from a_FILE). The
server's own/proc/1/environis kept from the image's user only where the kernel grants the binary its file
capability, as it does under Docker's defaults. Underno_new_privs, which the Helm chart's
allowPrivilegeEscalation: falsesets, as Docker's--security-opt no-new-privilegesdoes, the kernel never grants
it, and any program running as the image's user can read the server's own environment for as long as the server
runs: under the chart's own security context 6.3.7's server held both passwords there, read from their_FILE
secrets, for its whole life. Root in the container reads none of these environments, since it lacks
CAP_SYS_PTRACE. The server reads neither password from its environment on Linux. Both now leave the environment,
in whichever form they came, before the entrypoint runs any program - thecatthat reads a_FILEamong them,
which held the password read before it - and stay its own shell variables for the[Database]section and
--set-admin-password's standard input. The settings' variables (HM_SETTING_*,HM_PROPERTY_*, their_FILE
forms and the fourHM_REST_*), which the entrypoint's own--apply-environmentstill sees and stores in the
database before the server starts, and which the running server does not read, leave it too when the command is the
server itself (hmailserverwith nothing but--foregroundand--config, the image's own command); any other
command keeps them, since one that runs--apply-environmentwould otherwise give back every setting they name, and
the entrypoint cannot read a shell's command line. So the Compose file's and the Helm chart's commands, which are
shell command lines (--apply-new-install-defaultsand then the server), still pass the settings' variables to the
server and to what it starts - under the chart's security context, to its own/proc/1/environtoo - and a setting
that is a secret belongs in a_FILEthere, as their examples give it.HM_INSTALL_KINDstays, for the update
check, and any other variable reaches the command as it was given, as in 6.3.7, but forHM_DB_TYPE, which reaches
it in the entrypoint's own spelling (PostgreSQL,MySQLorMSSQL) when given another it accepts, and two of the
entrypoint's own names: a variable namedINIreaches it holding the configuration file's path, and one named
default_portthe database's default port whenHM_DB_TYPEis given. Hardening, not a flaw: a program running as
the image's user can readhMailServer.ini, which holds the database password, all the same.
build/test-docker-entrypoint.shholds it, with 6.3.7's entrypoint as its negative control
(hmailserver/docs/ContainerImage.md). -
Changed: the container image's
HMAILSERVER_RECOVERY_PASSPHRASEno longer reaches the server's environment when
the container runs the image's own command.
The variable is read by one verb,hmailserver --decrypt-store, and by nothing the running server does; given in a
container's environment it stayed in the server's, and that of every program the server starts, for the server's
whole life, which underno_new_privs- the Helm chart'sallowPrivilegeEscalation: false- any program running
as the image's user could read from/proc/1/environ. The entrypoint now takes it out of the environment with the
two passwords, before it runs any program of its own, and gives it back to the command unless the command is the
server itself, as it keeps the settings' variables: a container whose command is--decrypt-storehas it, and the
Compose file's and the chart's shell command lines still pass it to the server they start, so it belongs only in
the environment of a container started to run the verb. ContainerImage.md and EncryptionAtRest.md say so, and
build/test-docker-entrypoint.shholds it, with a case of its own for--decrypt-storeas the command. -
Fixed: a run of failed logons under a user name could fail to ban the address, when the auto-ban could not save
the range it is written as under a name of its own. The IP range a ban is written as is named
Auto-ban: <user name>, with(1)or
another suffix after it when a range of that name exists, in a column of 100 characters that no two ranges may
share. Up to 6.3.7 the suffix went after the whole user name and the name was cut to 100 characters only after the
check for a name in use, so that for a long user name a suffix could be cut and a later ban for the user name could
take a name in use: the second ban for it or a later one. Such a ban was refused and the address was not banned
(reported as HM6099 from 6.2.19) - and its counted failures were cleared all the same, so it was given a whole new
set of attempts. On SQL Server the check for a name in use compared the name in the database's code page, so it
could also miss a name in use. A ban for a user name holding a NUL could fail too. Now the
user name (or the spam trap) ends before a NUL and is shortened so that the name, its suffix included, fits the
column, never in the middle of a character, and the check asks about the name exactly as it will be stored,
compared as the database compares it; a range refused even so - a name another server sharing the database wrote
in between - is written again once under a GUID name before HM6099 is reported; and an address's counted failures
are cleared only once its range is saved, so while it cannot be each later failed logon within
MaxInvalidLogonAttemptsWithintries again (withAutoBanMinutes0, which writes no range, as before). Sessions
from one address that were open before it was banned, and that each cross the threshold, write no second range
while the ban in force lasts at least as long as a new one would. A ban for a long user name is named with the
user name cut, and the Auto-ban page shows it so; a range name over 100 characters given over COM or the REST API
is stored as its first 100, as before, and now reads back as stored, and one holding a NUL given over COM as the
part before it, while over the REST API a body that carries one, as an escaped U+0000, is answered 400 before the
route reads it; andPOST /api/v1/iprangesreads the name in UTF-8, asPUTdoes, where it stored a name with
characters outside ASCII as other characters, one for each of its bytes.
AccountLogonRangeNameTesterholds the names (inside the server on Windows, with a half against the database, and
inautoban-widen-testson Linux), and the regression fixturesSecurity.AutoBanRangeNameand
Infrastructure.DatabaseFailureHandlingthe bans (AutoBan.md). Found by the
hMailServer project (Progressive Robot Ltd). -
Fixed: an address an auto-ban or an administrator's IP range banned was not refused by the web services
listener, so it could go on guessing passwords at the CardDAV, CalDAV and Autodiscover sign-in. The listener,
off by default (WebServicesHttpPortandWebServicesHttpsPort0), asked the country policy at most when it
accepted a connection (6.3.3 nothing at all). A failed sign-in there counted towards the auto-ban as on every other protocol, and the
ban it wrote refused the address at the SMTP, POP3, IMAP and ManageSieve listeners, but not at this one.
Now the listener asks, once a connection and before the TLS handshake, for the IP range that decides for the
address, and closes the connection with no answer when that range allows none of SMTP, POP3 and IMAP - an
auto-ban's, or an administrator's that allows nothing - or when the ranges cannot be read; no range lets the
address in. A range that allows any of the three does not refuse, since the listener also serves MTA-STS
policies, ACME challenges, the Web Key Directory and autoconfiguration to the world, and a server that lets the
Internet in for SMTP alone must still serve them. Loopback and the addresses inWebServicesTrustedProxiesare
never refused by a range: behind such a proxy the listener sees the proxy's address for every client, and a ban
that held it would refuse every client behind it; through a proxy the per-name lockout is what limits the guesses.
Every failed sign-in on the listener still closes its connection, so one opened before the ban has one guess at
most after it. An administrator's range that allows none of the three now closes this listener to the addresses it
holds as well - the ACME http-01 validation when this listener answers it, MTA-STS, CalDAV, CardDAV, Autodiscover,
autoconfiguration and the Web Key Directory - so before upgrading a server whose own ranges allow some addresses
nothing (an Internet range that allows nothing because mail comes through a relay or a VPN, say), check that the
certificate authority, the sending MTAs and the clients of these services do not come from one; each such refusal
is said in the application log, naming the range, once an hour for each address while fewer than 1,024 are
recorded.WebServicesRangeTesterholds the rule (inside the server on Windows, and inautoban-widen-testson
Linux), and the regression fixtureSecurity.WebServicesBanthe listener, from the machine's own address that is
not loopback, with the ranges unreadable among its cases (AutoBan.md). Found by
the hMailServer project (Progressive Robot Ltd). -
Fixed: a failed CardDAV, CalDAV or Autodiscover sign-in through a proxy in front of the web services listener
counted towards an auto-ban of the proxy's own address. The listener sees a TLS-terminating proxy's
address for every client behind it (X-Forwarded-Foris not believed), and a failed sign-in through it was
counted towards the auto-ban against that address, so a ban written for that address could shut the proxy, and
every client behind it, out of the SMTP, POP3, IMAP and ManageSieve listeners. Since
6.3.3 for CardDAV and 6.3.4 for CalDAV and Autodiscover. A failed sign-in from loopback or a
WebServicesTrustedProxiesentry - the peers this listener takesX-Forwarded-Protofrom and never refuses by a
range - is now counted against the name alone (AccountLockoutThreshold, off by default, which is then the limit
on the guesses through a proxy), never towards a ban of the address; a failure from any other address counts as
before; the comments on Autodiscover's sign-in and onWebServicesServer::IsForwardersay so, where the first
still said that each failure was charged to the proxy's address. The Control Panel's note under the trusted
proxies says what a listed address is spared, and that only proxies belong there, in English and in the 17
languages.Security.WebServicesBanholds it: three failed sign-ins from loopback and through a trusted proxy
write no ban and SMTP still greets each, and from loopback they lock the name
(AutoBan.md). Found by the hMailServer project (Progressive Robot Ltd). -
Fixed: an account's failed sign-in at the REST listener through a reverse proxy on the same machine counted
towards an auto-ban of 127.0.0.1. Every browser behind such a proxy reaches the listener from loopback, the one
peer whoseX-Forwarded-Protoit believes. The listener's own accounting of a refused credential spared 127.0.0.1
and ::1, but an account's failed HTTP Basic sign-in - on any route,POST /api/v1/session, the webmail's and the portal's
sign-in, among them - had already been counted towards the auto-ban against the connection's address by the logon
the mail protocols share, so a ban could be written that refused 127.0.0.1 at the SMTP, POP3, IMAP and ManageSieve
listeners, while the REST listener, which asks no range, went on answering. Since 6.2.28, where accounts
first signed in there. A failed account sign-in from loopback is now counted against the name alone
(AccountLockoutThreshold, off by default, which is the limit on the guesses at one account through such a
proxy, as it already was, since the REST listener never refused 127.0.0.1 or ::1), never towards a ban of the address, and
noAuto-ban: logon failureline is written for it; a failure from any other address counts as before. The
listener's own accounting - of the administrator password, API keys, passkeys and the codes and passwords it
checks itself - now spares all of 127.0.0.0/8, the peers it takesX-Forwarded-Protofrom, where it spared
127.0.0.1 and ::1 alone. On 6.3.2 to 6.3.7,AutoBanNeverBanwith127.0.0.1, ::1avoids it; 6.2.28 to 6.3.1
have noAutoBanNeverBan, and there only the auto-ban on failed logons switched off (AutoBanOnLogonFailure
false, orMaxInvalidLogonAttempts0) orAutoBanMinutes0, which disconnects without writing a range, avoids
it, each for every address. The regression fixtureSecurity.RestApiLoopbackBanholds it: three failed sign-ins atGET /api/v1/meand at
POST /api/v1/sessionfrom loopback write no ban and no such line, SMTP and IMAP still greet loopback, and the
name is locked; failed account sign-ins, a wrong administrator password and a wrong API key from 127.0.0.2, and on
the Windows suite over ::1, are charged to nothing and the listener still answers; on the Windows suite, three
from another address are still charged to it
(AutoBan.md, DevicesAndSessions.md).
Found by the hMailServer project (Progressive Robot Ltd). -
Fixed: a sign-in took for an account every spelling of its name that the database's own comparison took for it,
so the per-name lockout and the compromised-account hold could be got round with another spelling of the name.
The per-name lockout (AccountLockoutThreshold, from 6.2.22-pre2) and the hold the compromised-account detection
puts on an account (from 6.3.4) are asked of the name the client gives - trimmed, in lower case, with a domain
alias or the default domain applied - before the password is checked. The account the name then signed in to was
whichever one the database found under it, and the databases find an account under spellings that key counts
apart: they differ by database and collation. Each such
spelling had a lockout threshold of its own, so the lockout limited the guesses at an account only up to the
number of its spellings, and on some databases not at all; and an account held for review could
be signed into under another spelling of its name with its password. Now a name signs in to an account only when
the account's stored address is that name, compared in lower case after the alias and the default domain, by the
function the lockout's key is made with: any other spelling names no account, fails as a name that does not
exist fails, with the same reply and no password verified, and is counted as a failure; the country policy asked
before the password finds the account by that rule too, so such a spelling meets its domain's policy, as a name
that does not exist does, where it had met the account's own and a refusal there was counted as nothing. The
same rule finds the account for every sign-in by a name a client or another party gives: the password sign-ins
of SMTP, IMAP, POP3,
ManageSieve, the DAV services, the webmail, the REST API and COM, the IMAP master user's authorisation identity,
SCRAM-SHA-256, the name of an OAuth2 bearer token, a client certificate and a Kerberos ticket (for an account linked
to a directory user, the directory user name and domain the account holds, where the database's comparison had
chosen among the linked accounts), and the address an OpenID Connect or SAML identity provider signs a person in
with (for which no mailbox is made either). A name that holds a NUL names no account and is refused before anything
is looked up with it: up to 6.3.7 such a name reached the account and domain lookups, where MySQL, MariaDB and
PostgreSQL failed the statement and the server reported a database error for each. A password
sign-in also asks the hold, the country policy and, for the account that authenticated, the lockout of the
account's own address once the password has found it; when the IMAP master user signs in as another account, the
master user's own hold, country policy and lockout are asked of its own address too, besides the other account's
hold and country policy, where up to 6.3.7 they were asked only of the name it gave, so that a held or locked
master user opens no mailbox under any spelling of its name; and theaccount.lockedwebhook is raised only for a
locked name that is an account's own. A name with white space at either end no longer signs in, whatever the
domain aliases hold (it
is still counted against the account, or as a name of no account where a domain alias's own stored name ends in
white space; the server now takes the white space off an alias's name when COM or the Control Panel saves one, as
the REST API did, where up to 6.3.7 a name under such an alias could be counted apart from the account; and the
default domain is saved and read without white space at either end, where up to 6.3.7 COM and the REST API saved it
as given, and with white space after it a bare local part could be counted apart from the account), nor does one
that
differs from the stored address in the case of a letter the C library does not lower in the C locale: with glibc
every letter but A to Z; on Windows Latin-1's own capitals (U+00C0 to U+00DE) and some letters from U+0100 up -
title-case letters, letters added in recent versions of Unicode and any outside the Basic Multilingual Plane among
them - while it lowers most of the others; and with musl (Alpine) hardly any, since it lowers nearly every letter,
so that there such a spelling signs in to the account and is counted under the
account's own key.LogonNameTesterholds the rule
(inside the server on Windows, and insign-in-budget-testson Linux), and the regression fixtures
Security.LogonNameVariants(over IMAP, over the web services' HTTP Basic, which carries a name as UTF-8, and over
SCRAM-SHA-256 on SMTP, IMAP and POP3, and a name holding a NUL over IMAP LOGIN and in a request for a password
reset, an alias whose stored name ends in a space, a default domain saved with white space after it, an
account's own country policy and a locked IMAP master user) and
API.RestApiCompromisedAccount(a held account, over IMAP and over SMTP AUTH PLAIN, which carries a name as UTF-8
too, and a held IMAP master user) andSecurity.CountryBlocking(the IMAP master user's own country policy) the
sign-ins
(CompromisedAccounts.md). Found by the hMailServer project (Progressive
Robot Ltd). -
Fixed: a mailbox's recovery address could be sent more password-reset links than the per-address limit allows.
POST /api/v1/password-reset, which takes no credential, allows each address one request a minute and six an
hour, which is what keeps a link from being mailed at somebody over and over. From 6.3.4 that limit was counted
under the address as it was given, and the mailbox was then found under another reading of it: some other forms of
the address each had a limit of their own, though each was looked up as the address itself; each sent the mailbox's
link. Now the limit counts the address as a sign-in compares
a name with an account's address - in lower case, with the alias and the default domain applied and the white space
at either end taken off - so every spelling that names a mailbox shares its one limit, with no lookup made for the
count; a request under the address itself is counted as before; the address is lowered once, and what is looked up
is what was counted; and an address that holds a NUL character, which names no mailbox, is counted against nothing
and looks nothing up, because a body that carries one, as an escaped U+0000, is now answered 400 before the route
reads it, as every REST body that carries one is. The regression fixtures
Security.LogonNameVariants.APasswordResetForAnAddressHoldingANulSendsNoMoreThanOneForTheAddressand
Security.LogonNameVariants.APasswordResetUnderADomainAliasOrTheDefaultDomainSharesTheAddressBudgethold it
(AccountRecovery.md). Found by the hMailServer project (Progressive Robot
Ltd). -
Fixed: the REST API, and the reader of a report mailed to a domain's report address, read a JSON body in time and
memory that follow its bytes. From 6.3.2 the REST API could take far more time and memory to read a JSON body than
the body's size, in the service that also serves SMTP, IMAP and POP3, and an account's own password was enough to send
one. From 6.3.4 the same was true of the reply to a password-protected message. The parse now moves each value into
its place rather than copying it; it keeps the names an object has already seen in an ordered set; and a body of more
than 100,000 values, an object of more than 10,000 members, or more than 32 levels is answered 400 at the first value,
member or level past the bound, before any route reads a member. The reply to a password-protected message is now held
to 2 MB, room for its one armoured message, and is read by that one parse rather than a second. A TLS-RPT report
mailed to a domain's report address is now read through the bounded parse too, within bounds of its own. The
RestApiRequestBodiesfixtures hold the bounds, with the account's password and on the
reply with no credential, andJsonValueTesterand theDomainReportstester hold the parse. Found by the
hMailServer project (Progressive Robot Ltd). -
Fixed: on Windows, setup makes a new program folder, and a
Binand aControlPanelthat are not there, with
permissions of their own from the moment they exist (work item
78, CVE-2026-107571, R1). The last step of
PrepareToInstall, before Inno Setup makes a folder or copies a file, makes the program folder, and every folder
above it that is not there, each by its one name relative to the handle of the folder above it (NtCreateFile), so
that no link put on the way since setup looked is followed and nothing is made where one leads, with a security
descriptor: SYSTEM and the Administrators full control,BUILTIN\UsersRead & execute where every user could read a
folder made there, nothing inherited, the Administrators the owner. It makes exactly the names below the deepest
folder it found and holds, never deciding what is there by a path again: it reads that folder again through the
handle that holds it before it makes anything in it, and holds each folder the moment it is made and reads through
the two handles where it is, so that a name somebody else takes in the meantime, a folder or a link, and a folder
held above it made a link in the meantime, are refused rather than taken. In a program folder it makes, it makes
Bin, andControlPanelwhen the Control Panel is installed, the same way, so that a cancel or a failure during
the copy removes them with the folders setup made - the program folder stays only when Inno Setup's rollback leaves
in it the files it never uninstalls, ofAddonsandEvents, the Administrators' with setup's permissions and
nothing else in it; in a program folder that is there it makes them the same way when they are not there, and when
they are there and held nothing when setup first looked, before anything changed (R2). An entry the folder above
hands down with generic rights counts as the rights they mean for a folder, as the server counts it, so that
C:\Program Files\hMailServer, where Program Files hands Users(GR,GE)down, gets its Users entry;
InstallerPackaging.csandbuild/check-installer-scripts.pyhold every mask setup tests an entry's rights against
to the generic rights that mean one of its own. A program folder typed on the directory page with a name that ends in
a dot or a space, which Windows drops from a path, or with.or..in it, is refused before anything is looked
at, with the path Windows reads - Inno Setup takes such a path as typed and installs by the path Windows reads, so
that setup would make the folder the name names, which no path reaches, and Inno Setup make and install into its own
with what the folder above hands down - and setup makes no folder whose name ends in a dot or a space. In 6.3.7 Inno Setup made the folder with the permissions the folder
above hands down, andicaclsreplaced them just after, and made a missingBinorControlPanelwith what the
program folder handed down. The uninstaller is given each folder setup made (section_dirs.iss) and removes it when
it is empty, and those still empty are removed when setup is cancelled or fails before its files: each recorded only
once it was found where it was named, with where that is, and removed only while it is still that folder, read
through a handle opened on its path as itself while the folders above it are still held; a folder found made
elsewhere is left as it is. An emptyBinorControlPanelthat was there, which setup removes and makes again, is
removed too when setup is cancelled or fails before its files: such an installation does not put back the empty
BinorControlPanelthat was there. An empty folder on the program folder's own level that was there, which
setup's last look removes (R2), is made again by setup at once, and a cancel or a failure leaves it in place.
InstallerPackaging.cs,build/check-installer-scripts.pyand the installer smoke
test, which reads each folder's permissions the moment it appears, hold it
(ServiceAccount.md, "Before the files"). Found by the hMailServer project
(Progressive Robot Ltd). -
Fixed: on Windows, setup hardens a program folder,
BinandControlPanelthat are already there before it
copies a file or runs a program of the installation, and does not install into one it cannot harden (work item
78, CVE-2026-107571, R2): a server installed into a
folder made beforehand, an upgrade, a repair, and the Control Panel alone, whatever the folder holds. A program
folder that is the root of a volume, which a mount point or a junction to a volume's root leads to, is refused,
since its permissions are the volume's and are handed down to what is made there, the uninstaller among it: the
refusal says to install into a folder on that volume; so is aBinor aControlPanelthat leads to the root of a
volume. The program folder is left as it is only when it is the earlier installation's and itsData,Logs,
Temp,DatabaseorEventsstill inherit from it, as an installation before 6.3.6 leaves them, which
/ProtectFoldershardens after the files, and no other account may change it - an inherited entry or a group of
every user's own that lets another account write it counts whatever it holds, one emptied of all but empty
folders among them, so that setup hardens it and refuses until a restart, and an entry of its own for a named
account is said; itsBinandControlPanelare hardened or made before them all the same. Whether it is left so
is decided once, before anything changes, and the restart rule goes by the same answer: asked again before the
files, an answer that differs - a data folder made, renamed or removed since setup looked - ends the installation
as a folder changed while setup prepared it, exit code 7. Each folder is read through the handle that holds it, and read again
once hardened. An entry of its own that lets a group of every user change it is taken away - each group the server
counts as one,CONSOLE LOGON,NETWORK,BATCH,SERVICE,ANONYMOUS LOGON, the two groups of application
packages,This Organization,Local account,Authentication authority asserted identity,ALL SERVICESand a
domain'sDomain UsersandDomain Guestsamong them, which/ProtectFoldersand itsCHECKlines count too; one
for another account is kept and said (R4), but not on a folder whose permissions another account could change, as
below. A folder of the
program that is there and that another account could change before this installation - one it owns, one an entry
lets it change the permissions or the owner of (EveryoneFull control on a folder made beforehand, say), or one
that holds anything but empty folders and that it may write, by an entry setup's hardening takes away, an inherited
one or a group of every user's own - a named account's own, which setup keeps and says, is not counted, and so does
not refuse every run - or one that holds a program, a library, a script or a settings file whose inherited entry,
kept from where it was made and not handed down by the folder it is in, lets another account change it, or a folder
moved so intoBin,ControlPanelor a folder that holds what is run, which keeps its entries as a file does and
hands them down to every file in it - keeps that
right in a handle a program the account started then, whatever setup does to the permissions after. Setup hardens
each before it stops the service - one whose permissions or owner another account could change rebuilt as the
earlier installation's folder another account owned is, since through that right the account could have written an
entry of the folder's own for itself, which setup cannot tell from one an administrator gave: every entry of its own
that lets another account write is dropped and named in the setup log - and refuses the installation, exit code 7,
until Windows has restarted, which
closes every such handle, and records that in a volatile key ofHKEY_LOCAL_MACHINE,
SOFTWARE\hMailServerSetupRestart, which Windows does not keep past a restart, by{app}and by where the program
folder,BinandControlPanelare, read through the handles that hold them; setup run again before it, by that
name or another for the same folder - a junction or a mount point to it, a short name, a second drive letter - is
refused by the record, and a refusal whose remedy ends in a restart is recorded the same way. A data folder of the
program folder,Data,Logs,TemporDatabase, and the folder a data folder's link leads to, is held to a
rule of its own before anything changes, since/ProtectFolderskeeps a data folder's own entries and its owner
after the files: its owner SYSTEM, the Administrators, TrustedInstaller or the service, and no entry of its own
that lets another account but the service write it; one that is not is refused, exit code 7, withicacls /setowner *S-1-5-32-544or/remove:gand a restart of Windows, but for the earlier installation's that another account
owns, which setup takes before a restart as it takesBin, its permissions rebuilt with the service's entries kept
and none for Users; and the files of theDatabaseare held to the rule of a settings file by their owners and
their own entries. A folder another account owns is taken so only when it is the earlier installation's, whose
folders an administrator's own account may own: it is given to the Administrators withtakeown /A, and its
entries that give no right to write are kept, the service's Read & execute onBinamong them, as
/ProtectFolderskeeps them. Any other such folder is refused, since its owner may hold it open with the right to
change its permissions: the refusal says to remove or rename it, or to install elsewhere. So that nothing another
account opened while it could write a folder decides where the files go, aBinor aControlPanelthat held
nothing when setup first looked, before anything changed, is removed and made again by setup whoever could write
it, one that holds something is refused when it has been made a link since, read through the handle that holds it,
before it is hardened, the program folder,BinandControlPanelare read again through their handles for a link
just before setup's last look at what is in them, an empty folder inBin,ControlPaneland the folders that
hold what is run, and, on the program folder's own level, an empty folder setup installs into or makes again - one
of those five, or a data folder when the server is installed - is removed before the files through a handle read as
that folder, the last made again at once by setup - with the list of entries it had when it inherited nothing, so
that an earlier installation's privateLogsandTempstay private and a cancel leaves them in place, and
otherwise with what the program folder hands down now and the entries of its own it had, so that an entry it kept,
still marked inherited, from a folder it was moved in from is not carried over onto it and the files copied into
it - and another empty folder there, which nothing installs into or runs from, is left and said, a folder
hardened in place keeps a file of setup's until the files are in, so that none can be made a junction, and after
the files, before setup runs anything of the installation, the three are read again through the handles that hold
them and looked through for a link or an empty folder setup did not leave - only what was found as setup left it,
each folder opened as itself where the one above it says - which is named and not removed; anything not as setup
left it ends the installation with exit code 12, runs nothing of it - the Control Panel the last page offers
included - sets the service not to start, until setup run again with the folders as it leaves them sets it to start
again as it did - Automatic, Automatic (Delayed Start) or Manual, the start type it read and recorded first; a
service that was Disabled already, as a standby kept from starting is, is left so, and one an administrator has set
to start otherwise since is left as they set it - and keeps setup from running again before Windows has restarted. A refusal after setup stopped the service
that found a folder changed sets the service not to start too, andhMailServer.Updaterputs no earlier version
back over such folders, on 12 or on that 7. A folder setup cannot harden ends the installation with exit code 7 and
what to do. In 6.3.7 a server installation into a folder that was there, and every upgrade, had the three hardened
only at the end of setup, by/ProtectFolders, and a folder another account owned, which only an installation of
the Control Panel alone hardened before its files, was left with the entries of SYSTEM, the Administrators and
Users alone. Found by the hMailServer project (Progressive Robot Ltd). -
Fixed:
hMailServer.exe /ProtectFolderssays a key or certificate file that a group of every user may read by
an entry it inherits as inheriting it (work item 78,
R4). A file inData\Certificates,Data\DKIMorData\ACMEthat inherited such a group's Read from an entry of
the data folder's own, which the line on the data folder names, was said to have permissions of its own, as a
file moved there keeps them, withicacls "<file>" /resetas the remedy, which changes nothing; it is now said
as inheriting the entry from the folder whose own entry it is, with theicacls "<folder>" /remove:gthat takes
it away there, and an entry of the file's own as before. A key file the configuration names in one of the
program's own folders is said the same way, and--report-onlyno longer says one as outside the program's own
folders: it says a key file whose own entry lets such a group read it, in those three folders or named by the
configuration in one of the program's own, as/ProtectFoldersdoes, and what a file inherits by the line on its
folder, which says who may read the folder now. Since the release after 6.3.7 counts This Organization,
Local account, Authentication authority asserted identity, ALL SERVICES and a domain's Domain Users and Domain
Guests among the groups of every user, both lines could be written of a data folder an administrator had given one
of them. Found by the hMailServer project (Progressive Robot Ltd). -
Fixed: on Windows, setup refuses a junction, a symbolic link or a mount point on the way to the program folder,
BinorControlPanelthat anybody but SYSTEM, the Administrators and TrustedInstaller controls, and a folder on
the way that another account may rename or empty (work item
78, CVE-2026-107571, R3). Every folder from the root of
the volume down is held open as itself, not shared for deleting, from the page before the installation to the end of
setup, and a link is read through the handle that holds it: refused when another account owns it, has a right on it
that changes it or what it leads to, or may remove it from the folder it is in: every right the server's
OnlyAdministratorsControlcounts on a link. A link an administrator made is refused as well when the rights on it,
those it inherits among them, let another account change it. The refusal says how to make the link
the administrators' alone (icacls <link> /L /setowner *S-1-5-32-544, then/L /inheritance:r /grant:rwith SYSTEM
and the Administrators full control and Users Read & execute) and to restart Windows before setup runs again; setup
then goes through it and on from where it leads, a mount point to a volume's root included, and a relative
symbolic link taken as Windows takes it: from the root of its volume when its name starts with a separator, from
the folder it is in otherwise, and refused when its name goes above that root. A folder on the way that
is not a link, the root of its volume included, is held to the rule the live update holds the folders above the
program folder to: owned by SYSTEM, the Administrators or TrustedInstaller, and nobody else may rename it, remove
what is in it or change who may; the refusal names the account and theicaclsthat takes the right away, and says
to restart Windows before setup runs again, which setup keeps it from doing until then. Refused too, before anything
changes: a program folder on a share or a network drive; a program folder,BinorControlPanelon a volume that
keeps no permissions (FAT, exFAT), or whose kind cannot be told, each asked of its own volume; and walks of the
three that disagree, as when a folder or a link is made while setup looks. The three are looked at, changing
nothing, before setup stops the service, so that a refusal leaves a running server running; a refusal after setup
stopped it says so, and how to start it again, also when an earlier click on the page found the stop slow. The
refusal isPrepareToInstall's, so a silent installation ends with exit code 7. In 6.3.7 setup installed through
any link and said in its log that it had not hardened it, and refused no folder above the program folder: it read
the one a new program folder would be made in only to warn, and/ProtectFoldersnamed one others may rename on a
CHECKline after the files. Found by the hMailServer project (Progressive Robot Ltd). -
Fixed: on Windows, setup does not install where the service and the administrators would run a program, a library
or a script another account controls, and says what an administrator is to look at in a program folder that is
already there (work item 78, CVE-2026-107571, R4).
Everything inBinandControlPanelat any depth, on the program folder's own level, and inAddons,Events,
DBScripts,WebAdminandLanguagesat any depth - where the service runs the event script, the administrators
the tools, and DBSetup the SQL - and inMySQL, which holds the bundled MySQL's programs, is looked at before
setup stops the service and again once the folders are
hardened, each item but a link, which is known by what the folder's listing says of it, through a handle opened on
it as itself: a link there - on the program folder's own level one only the administrators control is taken for
Data,Logs,TempandDatabasealone, where it leads then held to the rules of the program folder's own way,
every link and every folder above it, its volume and the folder being there, and any other there is refused
whoever controls it,Addons,Events,DBScripts,WebAdminandLanguages, which setup installs into,
MySQLand the uninstaller'sunins000.datamong them - or a program, a
library, a script (in those five folders,MySQLand on the program folder's own level), a file that says what
they run or where (Bin's
hMailServer.INI, the REST API's key storehMailServerApiKeys.ini, which may also be the service's, with the
Modify/GrantServiceAccessgives it, a.configor a.jsonbeside a program, and the uninstaller's
unins000.dat) or a folder whose owner is not SYSTEM, the Administrators or TrustedInstaller, or cannot be read, or
an entry of whose own lets another account change it, ends the installation with exit code 7, each named with the
icaclsthat deals with it and with a restart of Windows before setup runs again - as the live update's check of an
installation refuses them inBinandControlPanel. Where a data folder's link leads is held as well to what
/GrantServiceAccessrefuses after the files - a drive's root, the Windows folder, a Program Files folder, the
profiles folder,ProgramData\Microsoft,ProgramDataitself or a folder that holds it, and the program folder
outside its data folders,Binabove all - and aDatabaselink's target, and every file in it, to the rule of a
settings file: its owner SYSTEM, the Administrators, TrustedInstaller or the service, and no group of every user
able to change it, since the built-in database there names the program the service runs to scan for viruses. The
folder a data folder's link leads to is held to the rule of a data folder too (R2).Data,LogsandTemp, which
the service writes in, are not looked inside, andDatabaseonly for its files. Another file such an account owns,
such as a note, and an entry of a folder's own that lets a named account or group change it, which setup keeps on a
folder the administrators own as/ProtectFoldersdoes, are said in the setup log with theicaclsthat deals with
each, and an attended installation shows them in one box. Setup replaces the files it installs, and leaves one it
does not install as it is. In 6.3.7 setup looked at none of it before the files; after them/ProtectFoldersheld
every folder, program and library inBinandControlPanel, andBin\hMailServer.INI, to the live update's rule,
and said the first it found on aCHECKline. Found by the hMailServer project (Progressive Robot Ltd).
What changes on upgrade without a switch
- ACME: a host the certificate lacks is ordered at the first check. A server whose
AcmeDomainslists a host its
certificate does not name orders a certificate for every host at the first check after the upgrade, where it waited
for the renewal; if the CA cannot validate that host, the attempt fails from then on, at most three times an hour
(no order within twenty minutes of a failed one), and since each order names every host, no certificate is issued
and the one the other hosts use is not renewed while it stays on the list. A name no certificate can carry - a
wildcard, an IP address, a name past ASCII not in itsxn--form, one holding a character no host name can - is left
out of the check and the order and named in the application log at each check, where it failed every order. A CA's
rateLimitedanswer holds every order until itsRetry-After(an hour without one, at most seven days). An ACME URL
that is nothttpsis refused unless it is to this machine. The certificate a CA sends is installed only when it is
a PEM certificate for the request's key, naming every host asked for and not expired; otherwise the certificate in
use stays, and the log says why. Installing one reinitialises the server, as before. (Correctness, "a host added to
AcmeDomains") - A stop is no longer an error. Work that outlasted a stop of the servers - a reinitialise among them - writes a
line to the debug log where it wrote HM5136 or one of the other codes the entries name to the error log, which also
ran an installed OnError script; the error log of a server that reinitialises may be quieter.
An event script's call that meets a stop fails with "The server is stopping. The request was not completed.", its
script error goes to the debug log, and the event ends there;Scripting.Reload,CheckSyntaxandEnabledmet by
a stop keep the scripts loaded before. A database connection such a call held goes back to the pool, where on SQL
Server Compact every later statement waited a minute and failed (HM5180). (Correctness, "a session whose
command outlasted a stop" and "work that outlasted a stop") - With the servers stopped, or after a stop that could not end a thread: a delivery going on after the stop
defers its recipients (4.3.0) and is retried after the start with the try not counted; a message being finalised
goes on without a SpamAssassin verdict; an external account'sDownloadNowsucceeds and the account is fetched when
the servers start; a clear of the delivery queue reports HM4210 and clears nothing; a change made through COM, a
message saved into an IMAP folder among them, succeeds where it was answered with an error after being made; and a
session that lived through a stop gives back its own folder subscription and nobody else's. (Correctness, "an
IMAP session that ended after a stop" and "a task or a session that a stop of the servers could not end") - On Windows, a stop of the REST API's or the web services' HTTP listener gives the connections it closed five
seconds to drain, where it gave about six and a quarter. (Correctness) - On Windows, setup refuses an installation of the original hMailServer's 5.7 (5.7.0 and its pre-release builds
2788 and 2843, 5.7.1 and 5.7.2) before it changes anything; silent, it ends with exit code 1, and writes the reason
to its log with/LOGand tohMailServer-setup-refused.txtin its%TEMP%, which a later setup that is not
refused deletes. Installations of this fork, and of the original up to 5.6 and its 5.7 development builds up to
schema 5708, are upgraded as before. (Correctness) - On Windows, setup and the uninstaller find and stop the service through routines written anew from Microsoft's
documentation: the service's name reaches Windows through its Unicode functions, the Service Control Manager is
opened withSC_MANAGER_CONNECTalone; where Windows will not open it, setup, or the uninstaller, says so with
Windows' reason in its log and in a message, and a stop that Windows refuses is written to the setup log with
Windows' reason. (Correctness, "the Windows installer finds, reads and stops") - On Windows, setup makes or hardens the program folder,
BinandControlPanelbefore it copies a file or runs a
program of the installation. A folder, a link on the way to them, or a program, library, script or settings file
in them, that another account than SYSTEM, the Administrators and TrustedInstaller controls, and a program folder
on a share or on a volume that keeps no permissions, end the installation with exit code 7, each named with the
icaclsthat deals with it and, where it says so, a restart of Windows before setup runs again; anything not as
setup left it after the files ends it with exit code 12 and sets the service not to start until setup runs again.
An entry of a folder's own that lets a named account or group change it is kept and said in the setup log.
hMailServer.exe /ProtectFolderssays a key or certificate file that inherits a group of every user's Read as
inheriting it, andhMailServer.Updaterputs no earlier version back over folders setup found changed. (Security,
work item 78's entries) - The log's redaction of literals: the slow-query line and the trace's
db.statementread a literal as the
configured database does, so a line may differ from before. (Security) - The auto-ban's ranges: a ban for a long user name is named with the user name cut so that the name and its
suffix fit the column, and the Auto-ban page shows it so; a range name over 100 characters given over COM or the
REST API is stored as its first 100, as before, and now reads back as stored; andPOST /api/v1/iprangesreads a
name in UTF-8, asPUTdoes. (Security, "a run of failed logons") - Sign-in names: a name signs in to an account only when it is the account's stored address, compared in lower
case, as the C library lowers it in the C locale (with glibc, A to Z alone), after a domain alias and the default
domain; any other spelling the database would have found the account under, and a name with
white space at either end, fails as a name that does not exist fails and is counted as a failure, for every
protocol and for an OpenID Connect or SAML sign-in (Before you upgrade, above). The IMAP master user's own hold,
country policy and lockout are asked of its own address as well; theaccount.lockedwebhook is raised only for a
locked name that is an account's own; and COM and the Control Panel save a domain alias's name, and COM and the
REST API the default domain, without white space at either end. (Security, "a sign-in took for an account") - Password reset:
POST /api/v1/password-resetcounts its per-address limit under the address as a sign-in
compares it, so every spelling that names a mailbox shares its one limit. (Security, "a mailbox's recovery
address") - The web services listener and IP ranges: with the listener on, an address whose IP range allows none of SMTP,
POP3 and IMAP - an auto-ban's, or an administrator's that allows nothing - or one met while the ranges cannot be
read, is closed with no answer before the TLS handshake; loopback and the addresses inWebServicesTrustedProxies
are never refused by a range. Such an administrator's range now closes this listener to the addresses it holds -
ACME's http-01 validation when this listener answers it, MTA-STS, CalDAV, CardDAV, Autodiscover, autoconfiguration
and the Web Key Directory among them - and each refusal is said in the application log, naming the range, once an
hour for each address. (Security, "an address an auto-ban or an administrator's IP range banned") - Failed sign-ins through a proxy: a failed sign-in at the web services listener from loopback or an address in
WebServicesTrustedProxies, and a failed account sign-in at the REST listener from loopback, is counted against
the name alone (AccountLockoutThreshold, off by default, is then the limit on the guesses at one account through
the proxy), never towards a ban of the address, and the REST listener writes noAuto-ban: logon failureline for
it; a failure from any other address counts as before. The REST listener's own accounting spares all of
127.0.0.0/8, where it spared 127.0.0.1 and ::1. (Security, "a failed CardDAV, CalDAV or Autodiscover sign-in
through a proxy" and "an account's failed sign-in at the REST listener") - REST request bodies: a body meant to be JSON, on every route under
/api/and/scim/that takes one, must be
one JSON object within the bounds the entries give - no text after the value, no member named twice - or it is
answered 400 saying why (on/scim/, RFC 7644'sinvalidSyntax), before the route reads a member; every member
reader reads the one parse, and a member whose absence is the wider setting, written as another type than its
route reads, is refused too, where it was read as absent (the entry lists them); the reply to a password-protected
message is held to 2 MB, room for its one armoured message; and a TLS-RPT report mailed to a domain's report
address is read through the bounded parse, within bounds of its own. (Correctness, "a REST request body meant to
be JSON"; Security, "the REST API, and the reader of a report") - The database client's TLS:
TlsMode=verify-fullon MySQL and MariaDB through MariaDB Connector/C without
TlsCaFileis refused with HM7535 (new) on a connection to this machine (Before you upgrade, above);
TlsCaFile, and on WindowsPostgreSQLSslRootCert, reach the client as named, and a name the client cannot be
handed as named is refused before anything is attempted, with HM7534 (new) (Before you upgrade, above); and the
lines that say what was checked ---check-database's settings line, the application log's and the diagnostics'
connection line - say what the client checks, for PostgreSQL the root certificate libpq is handed.
(Security, the entries onverify-fulland on a root certificate's name; Correctness, "--check-database
names the root certificate" and "PostgreSQL's connection line") - On Windows, the files the server hands to OpenSSL by name - the TLS certificates' certificate and private key
files,dh2048.pem, a TCP/IP port's client-certificate CA file,MessageStoreObjectCaFile,OAuth2PublicKeyFile
and the files inAcmeCertificateDirectory- are read from the file configured, and a name the server cannot hand
over as it is named is refused with HM7536 (new) in the error log, with what the file was for: a port whose
certificate or key it names does not start, a client-certificate CA file verifies no certificate, the object
store's CA file allows no request, the public key verifies no token,dh2048.pemis left out as a missing one is,
and the ACME client reads and writes nothing there and orders no certificate (Before you upgrade, above).
OAuth2PublicKeyFilewas handed over in UTF-8 up to 6.3.7, so a name outside the code page that worked then is
refused now. A name in ASCII is handed over as before; on Linux every name goes over in UTF-8, as every other file
name there does (On Linux, file names outside ASCII, below). The Control Panel's SSL certificates page shows a
certificate or key file whose name is refused as Name refused, at Critical, and the setup checklist says that
the server does not read it (error 7536). (Security, "on Windows the files the server hands to OpenSSL by name") - On Linux, file names outside ASCII are UTF-8 whatever
LANGsays. A server that ran under aLANGthat is not
UTF-8 named such files in that locale's bytes and no longer finds them: rename each folder to its UTF-8 name before
upgrading (Before you upgrade, above). ALANG,LC_ALLorLC_CTYPEnaming a locale the machine does not have
no longer stops the server at its start; the values--set-setting,--apply-profile,--accountand--domain
take, and--config, are read as UTF-8 and refused with exit 2 when they are not; and whathmailserverprints to
a terminal is UTF-8 throughout. (Correctness, "on Linux a file's name outside ASCII") - spamd's
User:header carries a name outside ASCII, where it was left out and the message scanned, or the
lesson told, as spamd's default user: an address that arrived as SMTPUTF8 bytes goes as those bytes, any other name
as UTF-8, and on Windows a name within Latin-1 as before; a name the header cannot carry is sent as no header with
HM5514 (new). (Correctness, "spamd'sUser:header") - The legacy Blowfish scheme refuses a secret it cannot carry (HM7278, new), where it wrote an empty one: a
backup's XML leaves such a secret out and names it in the error log and the backup log (HM7280, new), to be entered
again after a restore; COM's database calls answer an error for such a password; and on Linux a text within
Latin-1 is written as Windows writes it. (Correctness, "the legacy Blowfish scheme") - On Alpine, a log line or other formatted text holding a character outside ASCII is written, where it came out
empty; on every Linux build a formatted text longer than 65,535 characters is made whole, where it came out empty.
(Correctness, "on Alpine, every log line") - Argon2id where the OpenSSL the server runs with has none:
GET /api/v1/status'sargon2idand
post_quantum_key_exchangesay what the library the server runs with does; HM5606 and HM5607 name that library;
and an app password aMinimumAcceptedHashAlgorithmof 5 raises to Argon2id is made with scrypt and said once
(HM5607), where it was an empty hash the store refused. (Correctness, "GET /api/v1/statussaid" and "the
reports that a server has no Argon2id") - Moves between IMAP folders: with
SpamAssassinLearnOnMoveon, the lessons of aCOPYor aMOVEare handed on
after the command, aMOVE's after its expunge, and the plain copy spamd is sent is made when the work queue runs
the lesson; spamd is sent the same bytes and taught as before.OnIMAPMessageMoved(IMAPMessageMovedEvents) and
Rspamd's learning (MoveLearningBackend) are off until they are switched on. (Administration,
"OnIMAPMessageMoved" and "Rspamd as the filter that learns from the Junk folder") - The container image: it carries MariaDB Connector/C (
libmariadb3, withmariadb-commonandmysql-common),
soHM_DB_TYPE=MySQLreaches a MariaDB server; itshmailserveraccount is made as 999:999, the numbers it had.
Its entrypoint runshmailserver --apply-new-install-defaults --if-markedbefore the command, which secures a
database its--create-databasemade and opens nothing at any other start;HM_DB_PASSWORD,HM_ADMIN_PASSWORD,
their_FILEforms andHMAILSERVER_RECOVERY_PASSPHRASEleave the environment before the entrypoint runs any
program, and when the command is the server itself the settings' variables (HM_SETTING_*,HM_PROPERTY_*, their
_FILEforms and the fourHM_REST_*) leave it too, so that the server, and what it starts, no longer has them.
The Compose file's and the Helm chart's commands are shell command lines and still pass them to the server, so a
setting that is a secret belongs in a_FILEvariable there, as their examples give it.
(Downloads and the release, "the container image reaches a MariaDB database"; Security, "the container image
secures a new installation by itself" and the two entries on the image's environment) - On Linux,
hmailserver --create-databaseasks first whether the schema's version table is there, so a new
installation, a container image's first start among them, no longer logs HM5033 as its first error; a database
that cannot answer that question is still HM5033 and is refused, and one that holds the table with no version
that can be read is refused too, where the create script ran before. (Correctness, "every new installation's
create step logged HM5033") - The Helm chart runs the pod as 999:999 with
fsGroup999 (it was 1000), whichever of the three its values leave
out; an init container puts the image'shMailServer.inion an empty configuration volume and makes the data
volume'stemp,eventsanddatabase; the server's container runshmailserver --apply-new-install-defaults --if-markedbefore the server, so a new installation is secured at its first start;database.portis empty by
default, the type's own port; and the pod setsnet.ipv4.ip_unprivileged_port_startto 0 in its own
securityContext.sysctlsunlessunprivilegedPortsSysctlis false, a release upgraded with--reuse-valuesfrom
a chart that does not name it included. The Compose file runs the same step, and gives its REST listener the pair a
new installation makes; a pair in./certsmust be readable by 999. (Downloads and the release, "the Helm chart
starts a server"; Correctness, "the Helm chart's server binds the mail ports") - The Control Panel: the Logging page shows a destination never chosen (0) as Files on disk, and a save that
leaves the list alone writes 0 back; the Anti-spam page shows a refuse threshold at or below the mark threshold with
a Warning and saves it, where it opened in error and saved nothing; the Anti-spam overview reads the quarantine
switch for its verdict; the ACME page and the Setup checklist leave out the names the server leaves out, and say
so. (Control Panel) - The PowerShell module and
hmctl: the module's error for no answer gives the system's reason, and an answer
that is not JSON is printed as text;hmctl config diff,config apply,--from-json,--password-fileand
--passphrase-filetake a file with a byte order mark, where they refused it or took the mark for the password's
first character; andhmctlrefuses a body holdingNaNorInfinity,--set name=NaNsending the stringNaN.
(Correctness, the module's two entries; "a REST request body meant to be JSON") - The Linux packages name their maintainer as Christopher Holloway chris.holloway@progressiverobot.com, where
they gave another address, and their summaries and descriptions, the snap's and the image's labels say Progressive
Robot's fork. (Downloads and the release) - The release carries
hmailserver-6.3.8.intoto.jsonlbeside the provenance's Sigstore bundle, 161 links in all.
(Downloads and the release)
Correctness
-
Fixed: on Alpine, every log line and other formatted text holding a character outside ASCII was empty.
String::Formaton Linux made its text with the C library'svswprintf, and musl's writes each wide character
through the C locale's multibyte form, which the server runs in and which carries nothing above U+007F but the
characters0xDF80to0xDFFF, so a format holding any other such character - in its text or in a%sor%c
argument - failed whole and gave nothing; glibc's copies wide characters as they are. Every line of the default log
is made byFormat: on Alpine no line held a character outside ASCII and a line that would have was not written -
an IMAPLOGINwith such a name lost itsRECEIVEDline, and the auto-ban's
Auto-ban: logon failure ... for <name>, which fail2ban reads, was never written for such a name, though the server
counted the failure - and protocol replies, REST messages and the other formatted text holding such a character were
empty too.platform/hm_platform.hnow makes the text itself, as glibc'svswprintfmakes it, for every value a
wchar_tholds - the format's text,%sand%ccopied with their flags, width and precision, a narrow string
through the C library's own conversion as before, the numbers by itssnprintf, which writes ASCII - and changes no
locale; a locale of the call's own would not have done, since musl'sC.UTF-8refuses0xDF80to0xDFFF, of
which a Boost.Filesystem path'swstring()is made there for a name holding a byte above0x7F. musl'svswprintf
has no%peither, and ends the process on a null%s, where glibc's writes(null). The layer writes a null%s
as(null), as glibc's does, and makes a%pwith the C library'ssnprintf, as it makes the numbers, so that a
null pointer is0on musl, where glibc writes(nil); no format of the Linux build uses%p. With glibc 2.37 and
later the text is whatvswprintfmade, but for one thing: a text longer than 65,535 characters, which the old
measure in a buffer that doubled up to 64 KB gave as nothing, is now made whole, as on Windows (2.36 and older,
Debian 12's and Enterprise Linux 9's, also refused a%cofWEOF, which the layer writes, as 2.37 does). It costs
more there: a short format, a log line's among them, takes 1.4 to 2.0 times as long as glibc'svswprintftook
(0.04 to 0.27 microseconds more, clang -O3), and a long text less; on musl every format measured takes less time
than before. A new tester program,string-format-tests, whichlinux-build, the package builds, the APKBUILD's
check(),debian/rulesand the RPM spec's%checkrun, asks for U+00E0, U+0130, U+20AC, U+1F600,0xDF80to
0xDFFF, the other surrogates, U+FFFE, U+10FFFF and the values above it through%s,%cand the format's own
text, holds the formatter to the C library's ownvswprintfover a table of formats, and logs lines holding those
characters through the default format, the event log and the backup log and reads them back.
Alpine.md, "What musl changed", says so. And
LogonNameVariants.OverHttpBasicAVariantSpellingFailsAndIsCountedAsAFailureexpects of the dotted capital I what
the server's C library does, asLogonName.hstates the rule: on a musl server that spelling is the account's own
name and signs in, and elsewhere it fails as a name that does not exist fails. And both of the layer's format
rewriters stop at a format's end: one that ended inside a conversion - a%at its end, or flags, a width or a
length with nothing after them - made them step past its terminating NUL and read on into what lay after it, on
glibc and musl alike, which AddressSanitizer reported of the new tester. -
Fixed:
Unicode::ToANSIgives on Linux the bytes it gives on Windows: each character up to U+00FF as its byte,
and nothing at all for a text with a character above it. Up to 6.3.7 Linux ranwcstombsin the C locale, which
the server never changes, and gave nothing for any character outside ASCII, where Windows'wcstombs_sin its own C
locale gives Latin-1 (measured with MSVC 19.51 on 30 October 2026: U+00E0 is e0, U+0080 80, U+00FF ff, and U+0130 or
U+20AC anywhere in the text empties it). What reached it on Linux: the legacy Blowfish scheme, spamd'sUser:
header andFormatter::FormatAsAnsi, whose exception texts name files (the entries below), and a 503's text from
the CRL endpoint, which is ASCII. unicode-tests (new, run by linux-build's jobs, the APKBUILD's check() on musl and
the source packages' builds - debian/rules' TESTERS and the spec's loop, whichbuild/check-source-tarball.pyholds
to CMakeLists.txt) runs FormatterTester, which ClassTester runs on Windows, over each case, withLANG=C.UTF-8and
unset. -
Fixed:
Formatter::FormatAsAnsiwith five arguments puts each in its place. It passed the first, the second and
the fifth to the three-argumentFormat, so that{0}|{1}|{2}|{3}|{4}gaveone|two|five|{3}|{4}. Nothing called
it with five; FormatterTester now holds every count of arguments. -
Fixed: the legacy Blowfish scheme refuses a secret it cannot carry, and its writers say whose it was and why,
where it was written as an empty secret without a word.LegacyBlowfish::EncryptToStringtakes a text as
Unicode::ToANSI's bytes, which for a text with a character above U+00FF are none, and encrypted those: the value
written read back as no secret at all - on Linux for any character outside ASCII. It now refuses such a text, HM7278
(new) in the error log, and answers nothing, as it does for one longer than 248 characters (HM7277). A backup's XML,
which carries secrets in that scheme, says in the error log and the backup log, in one line, which secret it was
written without and which of the two limits refused it - a character above U+00FF, or more characters than the
scheme holds, as an external account's OAuth refresh token often has (HM7280, new;LegacyBlowfish::Refusal,
Crypt::BackupRefusal): an external account's password and OAuth token, a route's relayer password, a certificate's
private key password - so that it is entered again after a restore (Crypt::EnCryptForXml). A person export, whose
file carries no credential, encrypts none and reports nothing to the backup log (Backup::BOLeaveOutCredentials).
IniFileSettings::SetPasswordleaves the database password in hMailServer.ini, and the running process's copy, as
they were, with HM7279 (new) naming the limit, where it wrote an empty one, and answers false; COM's
Database.CreateInternalDatabase,CreateExternalDatabaseandSetDefaultDatabaseanswer an error for it, where
they answered success, and the last two refuse a password the scheme would refuse before anything is created, when
the machine's own store is not used. COM'sUtilities.BlowfishEncryptanswers an error for such a text; and
Crypt::ProtectSecret's fall-back to the scheme on Windows answers empty with HM7278, which its callers already
read as not stored. A text within Latin-1 is written on Linux as Windows writes it, and a value Windows wrote reads
back on Linux as the text it was: legacy-blowfish-tests' table of what 6.3.4 wrote on Windows now holds on both
platforms, where at the commit before this Linux wrote dbd2989fb2ae94d1, an empty text, for each of its four texts
outside ASCII; its LegacyBlowfishTester, which ClassTester runs on Windows, holds each refusal's reason and HM7280's
words; file-utilities-tests holds that a password above U+00FF and a token of 249 characters are each left out of a
backup with one HM7280 naming whose it was and why, and no HM7277 or HM7278, where at the commit before this change
each was reported a second time, by HM7278 or HM7277; and the regression suite'sBlowfishRefusesATextItCannotCarry
holds COM's error and HM7278 on Windows. -
Fixed: spamd's
User:header carries a name outside ASCII, where it was left out without a word and the message
was scanned, or the lesson told, as spamd's default user.SpamAssassinUser, and a recipient's address with
SpamAssassinUserFromRecipient=1, went throughUnicode::ToANSI, which gave nothing for a name with a character
outside ASCII on Linux and above U+00FF on Windows, and an empty user sends no header. spamd reads the header's
bytes as they are and refuses a request whoseUser:holds a control character (got_user_headerin spamd,
PROTOCOL: the meaning of the name is spamd's own). Now an address held as the bytes it arrived in - SMTPUTF8's -
goes as those bytes, any other name as UTF-8, and on Windows a name within Latin-1 as its Latin-1 bytes, as before
(SpamAssassinClient::UserBytes); a name the header cannot carry, a control character or half of a surrogate pair,
is sent as no header with HM5514 (new) in the error log. unicode-tests' SpamAssassinUserTester, which ClassTester
runs on Windows, holds each, where at the commit before this the first name outside ASCII went as nothing.
README.md's lines on the two settings say so. -
Fixed: on Linux a file's name outside ASCII is one name to every call that opens the file, UTF-8, whatever
LANG
says. Boost.Filesystem converted a wide name with the locale the process started in: UTF-8 under a UTF-8LANGon
glibc, and an uncaught exception (boost::filesystem::path codecvt to string) withLANGunset - as the container
image leaves it - and on Alpine whateverLANGsays, since libstdc++ on musl has no UTF-8 locale.File::Openand
the other narrow calls -stat,open,link,rename,access,chmod,statvfs, libstdc++'s streams, the
listeners' and ACME's certificate and key files, the client-certificate CA, OpenSSL'sBIO_new_file,execvp's
command line, working directory and output file - were handedAnsiString(String)'s Latin-1 bytes, '?' above U+00FF.
So one name outside ASCII was two files, or a file and an exception. Now Boost.Filesystem is given a UTF-8 conversion
of its own beforemain(FileUtilities::UseUtf8FileNames, Boost's own facet, the same on glibc and musl), every
narrow call the bytes ofFileUtilities::NarrowPath, UTF-8, besideIniFile,DataProtector, OAuth's key file and
libpq's root certificate, which wrote UTF-8 already, and a name the system gives as bytes is read as UTF-8
(FileUtilities::WidePath):--configand--if-marked, refused with exit 2 when they are not UTF-8,
/proc/self/exe, from which the program folder is made, a directory ofPATHwhere 7-Zip is looked for,APPIMAGE,
the image an update replaces, andAPPDIR, where the AppImage runtime mounts that image underTMPDIRand from where
the update helper is copied - each of the two refusing the update, with the reason, when it is not UTF-8, where under
aTMPDIRoutside ASCII the helper's name was its UTF-8 encoded a second time, the copy found nothing (HM6891) and
the image could not update itself - andRUNTIME_DIRECTORY, where the update's request is put, which systemd names in
ASCII for the packaged unit. Functions that held a file's name narrow take it as it is:MimeBody'sLoadFromFile,
LoadHeaderFromFileandSaveAllToFileandMessageStore::LoadMime, so that a message under a folder named above
U+00FF, which was written, is read - by IMAP's FETCH of a part or of the structure, the rules, scripts, signatures and
the indexer, the virus scan's pass over each attachment, attachment stripping and a Sieve script's header edits -
where it was looked for under '?', and iMIP's look for an invitation, which handedLoadMimethe name's UTF-8 to be
encoded again and read nothing under any name outside ASCII; and DKIM's signer, which looked for a key file under such
a folder with '?' in its name and signed nothing (HM5306). On Windows these narrowings put '?' for a character outside
the code page, and they take the name there too. The crash record is written under aLogFolderoutside ASCII, where
its name was in no directory there is; the update helper is made executable by its name, and a failure to is refused
(HM6891), wherechmodnamed another file and nothing was said; and the update's request names the package, the
rollback image, the outcome and the log in UTF-8, as the helper is given the Updates directory, where under a
DataFolderoutside ASCII the helper found them outside it and refused the update. The values--set-settingtakes,
on the command line or from standard input, and--apply-profile,--accountand--domainare read as UTF-8 too
and refused with exit 2 when they are not, where each byte was widened to a character and a value outside ASCII was
stored, or looked for, as other text - for a path, its UTF-8 encoded a second time, the name of no file.
--set-admin-passwordstill widens its bytes, which is safe: the hash is made overUnicode::ToUtf8's bytes, which
for UTF-8 widened so are the bytes typed, the ones a sign-in's UTF-8 gives. Whathmailserverprints to a terminal is
UTF-8 throughout, as--check-database's and--apply-environment's lines were, whereplatform/main.cppprinted
some paths in UTF-8 and every other text, the configuration file's name among them, in Latin-1 with '?' above U+00FF.
That covers the[Directories]folders,TlsCaFile, the domain and account folders of the data directory and the
messages in them, the backup's and the archive's files, DKIM key files, the logs and the crash record, the message
store's links and stamps, the GeoIP database, the update's Sigstore bundles, helper and request, the stored-secrets
key and the files the REST API serves and keeps. On Windows the other narrow calls were and are given the code page's
bytes, and the wide ones the name. And aLANG,LC_ALLorLC_CTYPEnaming a locale the machine does not have -
LC_CTYPE=UTF-8from a Mac's SSH session, whichsudokeeps, or a container'sLANGwith no such locale generated -
changes nothing, and no longer stops the server: up to 6.3.7 its start ended at the first name Boost.Filesystem
converted (exit 134,locale::facet::_S_create_c_locale name not valid), Boost asking the C++ library for the locale
named there, which glibc's refuses for one it does not have; Boost.Filesystem now converts with a UTF-8 conversion of
its own and never asks for that locale, and the one answer Boost 1.90'spath::imbuestill makes from it, after the
conversion is in place, is caught. Upgrading a Linux server that ran under aLANGthat is not UTF-8, a Latin-1
locale say: it named its files in that locale's bytes - Boost.Filesystem by the locale,File::Openin Latin-1 - and
from this release names them in UTF-8 whateverLANGsays, so that a data directory, a domain or account folder,
LogFolderorTlsCaFilenamed outside ASCII under such a locale is not found after the upgrade, and new folders are
made beside the old ones: rename each to its UTF-8 name before upgrading, for example withconvmv -f iso-8859-1 -t utf-8 -r --notest <folder>. Under a UTF-8LANG, the packages', a folder named outside ASCII was Boost's, in UTF-8,
and is found as it was. file-utilities-tests holds it (FileUtilitiesTester::TestFileNameEncodingand
MessageFileNameTester, which ClassTester runs on Windows too): a file named with U+00E0, U+0130 and U+20AC, made by
File::Open, is found byFileUtilities::Exists, Boost.Filesystem andstatand listed under its name, and a
message under folders named in CJK, Cyrillic, a character beyond U+FFFF, a combining accent and Latin-1 is read by
MimeBody,LoadMimeandMessageDataand written back bySaveAllToFile, withLANG=C.UTF-8and unset; before
this changeNarrowPath's stand-in,AnsiString(String), gave 63 e0 2d 3f 2d 3f for the name's first six characters,
and at the commit before this change the message under the folder named in CJK was not read by
MimeBody::LoadFromFile; and the regression suite'sSigning.TestSigningWithAKeyFileNamedOutsideLatin1signs with a
key file in a folder named in CJK and Cyrillic. file-utilities-tests also runs itself again under
LC_ALL=xx_YY.UTF-8, which reachesmainand converts a name to UTF-8, and names the AppImage's update helper from
anAPPDIRin UTF-8 and refuses one that is not, in a build with the live update; with the exception left uncaught
the run again ended by signal 6 beforemain, and withAPPDIRwidened as before the helper's name was other
characters, on glibc under eachLANG. Measured on 31 October 2026 withhmailserver --check-databasebuilt in the
CI image, underLC_ALL=xx_YY.UTF-8, against MariaDB 13.0.2 and MySQL 8.4.11 (each by its image's digest): at the
commit before this change all 24 cases ended with exit 134, and with it each connected or was refused as under
LANG=C.UTF-8. -
**Fixed:
--check-databasenames the root certificate libpq is handed for PostgreSQL, where it namedTlsCaFile
These notes are cut here for GitHub's limit. The whole notes are on the release on GitLab, https://gitlab.com/hmailserver/hmailserver/-/releases/v6.3.8, and attached here as release-notes-v6.3.8.md.