Skip to content
hMailServer 6.3.8 is the current release, signed 2 November 2026. Windows and Linux, and an in-place upgrade from 5.3 or later, but for the original hMailServer's 5.7.Download 6.3.8 Documentation FAQ
  • 0 Votes
    1 Posts
    31 Views
    chrisC
    Who this affects: servers on 6.3.7 or 6.3.8 that deliver mail directly to recipients' mail servers. 6.3.6 may be affected too. Mail sent through an SMTP relay or a route isn't affected. What you'll see: messages to some domains stay in the delivery queue and are retried later, even though your DNS server is working. If that goes on long enough, the sender gets a bounce. We've seen it with paypal.com, seznam.cz, kpnmail.nl and xs4all.nl, and it isn't limited to those. With application logging on, the log says the DNSSEC lookup of the TLSA records "could not be completed". The cause: a bug in our DNSSEC checking. Some signed domains prove that a name doesn't exist in a valid way that our code misreads as incomplete. The server then can't tell whether the domain requires DANE, so it holds the message rather than risk sending it insecurely. What to do: the fix is in testing for 6.3.9, which has no date yet. If you have mail stuck now: In the Control Panel, open Transport security, then the Encryption tab, and find DANE & DNSSEC. Untick "Honour recipient DANE/TLSA records when sending", then choose Save changes. Open the delivery queue and choose Deliver all now. This turns off DANE checks for all your outgoing mail, so tick it again once you're on 6.3.9. Use the Control Panel rather than editing hMailServer.ini, because an ini edit is hard to undo. If you run on Linux, which has no Control Panel, post here and we'll help you with this step. If nearly all your outgoing mail is stuck, the cause is more likely a DNS server or firewall that drops DNSSEC data or blocks DNS over TCP. The same steps get mail moving while you look at that. If you're seeing something else, please post the log line here with addresses and passwords removed.
  • 0 Votes
    1 Posts
    42 Views
    chrisC
    hMailServer 6.3.8, Progressive Robot's fork of hMailServer, was published on 2 November 2026. It 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 everything that landed on master after 6.3.7. The schema stays at 6108: from 6.3.6 or 6.3.7 the upgrade changes no table. Who is affected, and what to do Every server running 6.3.7 or earlier: upgrade. "What 6.3.8 fixes" lists the vulnerabilities, each with its work item and CVE request; "Upgrading" says how each version upgrades. Read "Before you upgrade" first. Some fixes change what an upgraded server does, above all for TlsMode=verify-full without TlsCaFile to a MySQL or MariaDB server on the same machine, for file names outside ASCII, and for sign-in names spelt otherwise than the stored address. Anyone who installed the Helm chart of 6.3.6 or 6.3.7: its pod never started. Move the release to 6.3.8's chart as "Before you upgrade" says. Anyone running the container image against MariaDB: the images of 6.3.7 and earlier could not reach it; 6.3.8's carries MariaDB's client library, and the release's pipeline starts each platform's image beside MariaDB and asks it for SMTP, the REST API and the schema it laid out. Windows, on the original hMailServer's 5.7 - 5.7.0 and its pre-release builds 2788 and 2843, 5.7.1 or 5.7.2: this fork's installers of 6.3.7 and earlier must never be run over it; 6.3.8's refuses it before it changes anything. Move such a server's mail over IMAP instead (Migration.md, "From the original hMailServer"). Before you upgrade The 6.3.8 section of CHANGELOG.md, "What changes on upgrade without a switch", lists what an upgraded server does differently with no setting turned. These are the most likely to need something done: Back up the database and the data directory, each with its own tools (Upgrading.md). 6.3.8 adds no schema step; from 6.3.5 or older the upgrade runs 6.3.6's three one-way steps to 6108, and the backup is the only way back. From 6.3.6 or earlier, 6.3.7's "Before you upgrade" and its list hold as well, and from 6.3.5 or earlier 6.3.6's list. A Helm release of the 6.3.6 or 6.3.7 chart: helm repo update, then helm upgrade <release> hmailserver/hmailserver --reset-then-reuse-values (Helm 3.14 or later), then kubectl delete pod <release>-hmailserver-0, since the StatefulSet never replaces the stuck pod by itself; --reuse-values instead keeps the old chart's fsGroup 1000 and database.port 5432. Where a cluster's policy refuses every sysctl, set unprivilegedPortsSysctl: false. A Compose deployment made from an earlier Compose file: keep its HM_REST_CERTIFICATE_FILE, HM_REST_PRIVATE_KEY_FILE and ./certs mount in the new file, the key readable by 999, or its REST API does not start. ACME: AcmeDomains listing a host the certificate lacks. The first check orders a certificate for every host on the list; a host the CA cannot validate fails the whole order, at most three times an hour, and holds the renewal for the other hosts: take it off the list first. TlsMode=verify-full on MySQL or MariaDB without TlsCaFile, to a database server on this machine: through MariaDB Connector/C, the client the Windows installer bundles and the Linux server loads, 6.3.8 refuses the connection with HM7535. Name in TlsCaFile a CA that issues nothing but the database's certificates, the one that issued its certificate, or set TlsMode=require, before upgrading (DatabaseTls.md). On Windows, a file whose path holds characters outside ASCII - the database's TlsCaFile or PostgreSQLSslRootCert, a TLS certificate or private key file, dh2048.pem, a client-certificate CA file, MessageStoreObjectCaFile, OAuth2PublicKeyFile or AcmeCertificateDirectory: 6.3.8 may refuse its name, with HM7534 or HM7536, and then the database is not reached, or what the file is for does not work. Give each such file, and the folders above it, an ASCII name before upgrading. A Linux server that ran under a LANG that is not UTF-8, with files or folders named outside ASCII: 6.3.8 names files in UTF-8 whatever LANG says, so rename each to its UTF-8 name before upgrading, for example with convmv -f iso-8859-1 -t utf-8 -r --notest <folder>. 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 too, and with it ACME's http-01 validation there, 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 writes a member whose absence is the wider setting as another type than its route reads: the server answers it with 400. 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. Installing with Chocolatey: give the administrator password in HM_ADMIN_PASSWORD, set in the elevated prompt, not with /adminpassword in --install-arguments: the install now passes the installer /LOG, and the log records setup's command line. On Windows, a program folder, Bin or ControlPanel, 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 the icacls that deals with it, and asks for a restart of Windows before it runs again (ServiceAccount.md). Monitoring that counts the error log, or an OnError script, during a stop: a stop's interruption is now a line in the debug log, and an event script's call that meets a stop fails with "The server is stopping. The request was not completed." The .NET 10 Desktop Runtime is installed only where none is, so an upgraded server keeps the one it had: bring it up to date through Microsoft Update. What 6.3.8 fixes The vulnerabilities first, each found by the hMailServer project (Progressive Robot Ltd), each work item public now and each with a CVE request through GitLab's CNA; the 6.3.8 section of CHANGELOG.md, linked above, gives each, with how it is tested. 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, CVE requested). The auto-ban saves the IP range it adds under a name of its own, so that it bans the address (work item 102, CVE requested). 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, CVE requested). The web services listener (CardDAV, CalDAV, Autodiscover) refuses an address an auto-ban or an IP range bans (work item 105, CVE requested). A failed sign-in at the web services listener through a proxy is no longer charged to the proxy's address (work item 106, CVE requested). 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, CVE requested). 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, CVE requested; work item 111, CVE requested). TlsMode=verify-full on MySQL and MariaDB checks the database server's certificate against the CA file TlsCaFile names (work item 112, CVE requested). Its entry corrects what 6.3.7's notes said of a named TlsCaFile as the mitigation for CVE-2026-107819, MariaDB Connector/C's own flaw. On Linux the client is the distribution's, which should be Connector/C 3.4.11 or later; where it is older and not patched, name the CA in TlsCaFile. The database client's TlsMode=verify-full check of a database server on this machine, on MySQL and MariaDB (work item 113, CVE requested). On Windows the database clients check the database server's certificate against the root certificate file named, or refuse its name (work item 114, CVE requested). On Windows the files the server hands to OpenSSL by name are read from the file configured, or refused (work item 115, CVE requested). 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, CVE requested). On Windows, setup makes or hardens the program folder, Bin and ControlPanel before 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, each with a request of its own: a new folder (CVE requested), a folder already there (CVE requested), a link on the way to them (CVE requested), and a program, library or script in them (CVE requested). Published with it: the advisory for a flaw in the auto-ban on MySQL and MariaDB that 6.3.4 fixed (work item 103, CVE requested). The other fixes and changes an administrator meets: The Helm chart starts a server. The charts of 6.3.6 and 6.3.7 stopped at "container has runAsNonRoot and image has non-numeric user (hmailserver)", and, given the user by number, at an empty 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 container image reaches a MariaDB database. It carries MariaDB Connector/C now, and the release's pipeline starts each platform's image beside PostgreSQL and beside MariaDB and asks each for SMTP, the REST API and the schema it laid out; a chart told MySQL no longer dials MariaDB on PostgreSQL's port. Found by the project's own audit of its listings. Its 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. The Windows installer refuses an installation of the original hMailServer's 5.7. Both projects' installers register as hMailServer_is1, so those of 6.3.7 and earlier took 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 refuses such an installation before it changes anything; silent, it ends with exit code 1 and writes the reason to its log and to hMailServer-setup-refused.txt in %TEMP%. 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. A host added to AcmeDomains no longer waits for the renewal, and a check can be asked for (work item 98, ThiagoES84). A certificate that does not name every host on the list is now due at once, and GET /api/v1/acme, POST /api/v1/acme/run, COM and the Control Deck's Certificates page give its state and ask for a check, with the CA's rate limits kept (Acme.md). An event after an IMAP move, for scripts (work item 109, ThiagoES84). OnIMAPMessageMoved is called once for each message an IMAP MOVE or UID MOVE moved between two folders of the account signed in, after the command and off the IMAP thread. Off as shipped: IMAPMessageMovedEvents switches it on. Rspamd learns from the Junk folder, beside SpamAssassin (work item 108, ThiagoES84). A message a user moves into Junk is taught as spam, and one moved out as ham, to the filter MoveLearningBackend names, SpamAssassin by default; nothing is sent to Rspamd until it is chosen and its controller and classifier are set (JunkFolderLearning.md). A stop of the servers is no longer reported as errors of their own. Work that outlasted a stop wrote its interruption to the error log as a failure and ran an installed OnError script; it now writes a line to the debug log and ends as the stop asked, a database connection it held goes back to the pool, and on Linux a session or a task that went on after a stop no longer ends the process. Also: a REST request body meant to be JSON must be one JSON object; on Linux a file's name outside ASCII is one name, UTF-8, whatever LANG says; the Linux server reaches clamd over its Unix socket; and the command-line clients and the Control Panel have fixes of their own. New ways to install: an Ansible collection, a Kubernetes operator for Operator Lifecycle Manager, and a container on Proxmox VE (Proxmox.md), whose scripts have not yet run on a Proxmox VE host. 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. Upgrading By the live update, from 6.3.7 or 6.3.6. 6.3.8 is signed with Sigstore as its own pipeline at the project's group path, with the project's id, 86730559, in the certificate, a signer every server from 6.3.4 on has compiled in; the running version's own file is its rollback image. A Windows service running as LocalSystem installs it itself; for one running as NT SERVICE\hMailServer an administrator presses Install update on the Control Panel's Server status page, on the server, and Windows asks once. On Linux the installed version's update helper applies it to the generic .deb and .rpm and both AppImages. From 6.3.4 or 6.3.5, as 6.3.7's notes said: a service running as LocalSystem installs 6.3.8 by its live update; for one running as NT SERVICE\hMailServer, download hMailServer-6.3.8-x64.exe, check that Get-AuthenticodeSignature says Valid for CN=Progressive Robot Ltd, and run it as an administrator, once, keeping the service account on the installer's Service account page (SmartScreen's More info names Progressive Robot Ltd, and Run anyway installs it). On Linux, install it with apt, dnf or zypper rather than by the live update, or turn the live update's apply off first with systemctl disable --now hmailserver-update.path. A server 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 release's own package of 6.3.8. A server whose UpdateSigningIdentity, UpdateSigningIssuer and UpdateSourceRepository still name the path the project began at refuses 6.3.8: clear the three first (UpdateFeed.md). By hand, from 6.2.28 to 6.3.3, or on Linux from 6.3.3's AArch64 packages or AppImages: those versions trust only the GitHub workflow that signed the releases up to 6.3.3, so run hMailServer-6.3.8-x64.exe over the installation, install the package with apt or dnf, or replace the AppImage, once; keep UpdateAutoDownload off there until then. 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. The container image, registry.gitlab.com/hmailserver/hmailserver:6.3.8, also tagged 6.3 and latest, for linux/amd64 and linux/arm64. In the container the update check names the image to pull and installs nothing (ContainerImage.md). What the release carries 62 files, each with its Sigstore bundle, <file>.cosign.bundle, beside it: the x64 and ARM64 Windows installers, Authenticode-signed; the generic .deb and .rpm and each supported distribution's own package, for x86-64 and AArch64; both AppImages, and beside them the source of the libraries they carry and of those their runtime links; the source tarball; the Helm chart; the PowerShell module; an SBOM for each compiled file and the tarball, and the source tree's two; and hmailserver-linux-6.3.8-SHA256SUMS.txt. Beside them, hmailserver-6.3.8.intoto.sigstore.json is SLSA v1 build provenance that names each of them by its SHA-256, and hmailserver-6.3.8.intoto.jsonl the same as one line of JSON. https://updates.progressiverobot.com/download/ leads to the newest stable release's page, and /download/hMailServer-x64.exe, /download/hmailserver_amd64.deb, /download/hmailserver.x86_64.rpm and the rest of the download links to that file of it. How to check a download With cosign and OpenSSL, with the file and its bundle beside you: cosign verify-blob --bundle hMailServer-6.3.8-x64.exe.cosign.bundle \ --certificate-identity 'https://gitlab.com/hmailserver/hmailserver//.gitlab-ci.yml@refs/tags/v6.3.8' \ --certificate-oidc-issuer https://gitlab.com hMailServer-6.3.8-x64.exe jq -r .verificationMaterial.certificate.rawBytes hMailServer-6.3.8-x64.exe.cosign.bundle \ | base64 -d | openssl x509 -inform DER -noout -text | grep -A1 1.3.6.1.4.1.57264.1.15 The first must say Verified OK, and the second print ..86730559, the project's own id, which cosign does not check. sha256sum -c --ignore-missing hmailserver-linux-6.3.8-SHA256SUMS.txt must say OK for each Linux file; on Windows, Get-AuthenticodeSignature on either installer must answer Valid, signed by CN=Progressive Robot Ltd, and the x64 installer's published SHA-256 is 4b9b9c3102cbed743f6c826289c54cec25c395d02208e55cdd3219187fbf8654. SECURITY.md, "Verifying a release", gives the other checks - the tag's signature and a file against the build provenance among them - and ContainerImage.md the image's and the chart's. How it was checked The tag's pipeline (https://gitlab.com/hmailserver/hmailserver/-/pipelines/3016341592) ran every gate before anything was published: the Windows suite, in two halves on the project's two Windows benches, 4,569 tests, 4,489 passed, 0 failed, 80 skipped; the Linux suite on one of the project's Linux runners, 2,749 tests, 2,374 passed, 0 failed, 375 skipped; the rebuild of the x86-64 server, .deb and .rpm byte for byte; the install of every Linux package in a clean container of its release, by that release's own package manager, the AArch64 ones under emulation, but for the generic x86-64 .rpm, which is built on Ubuntu and was installed on Fedora 44 with rpm --nodeps, its dependencies not resolved; the start of the generic x86-64 .deb and of the generic AArch64 .deb and .rpm against a database; the REUSE check; the static analysis, Semgrep and the dependency scan. It then checked every signature with the server's own verifier, hmailserver --verify-update, built from the same commit. The signed installer went through the installer smoke test: an upgrade from 6.3.7 passed 105 checks and a new installation 65, none failed. The published chart, installed from the project's Helm repository into a one-node cluster, started its pod with 6.3.8's image and answered SMTP with STARTTLS and the REST API over TLS. 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 .deb and .rpm started 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 sent 6.3.8 by jobs started by hand once the release is published: the Ubuntu PPA, Fedora Copr, Docker Hub, Chocolatey (listed once its moderators approve it), PyPI and the PowerShell Gallery; the openSUSE Build Service once its package holds this release's new _service, until then keeping 6.3.7; and the snaps on the Snap Store's edge and candidate channels, stable being promoted by hand. The Ansible collection is not on Ansible Galaxy yet, and the one in the tree installs 6.3.7 unless it is given another release's SHA-256; the operator's pull request to OperatorHub.io is made by hand (Channels.md). The Proxmox VE scripts have not run on a Proxmox VE host, only as a dry run, with stand-ins of its commands, and in a container made from its own debian-13-standard template. If you run them, please say how it went in an issue. The image's OpenSSL, Ubuntu 24.04's 3.0.13, has neither the hybrid post-quantum key exchange groups nor Argon2id: its server logs HM5720 and takes the classical groups and scrypt, and GET /api/v1/status says false for both. An AppImage on Fedora, Enterprise Linux or openSUSE runs on the host's glibc and cannot require its extra character-set converters: install glibc-gconv-extra (Fedora, EL) or glibc-gconv-modules-extra (openSUSE) by hand; the server says when they are missing. While releases pause 6.3.6 was the last release planned by date, by the owner's decision of 28 September 2026; the releases since are security releases, each cut from master with whatever finished work it holds then, until the maintainers say otherwise in SECURITY.md, "While releases pause". A confirmed vulnerability still ships: a critical or high one as soon as its fix has passed the gates, and none held past 90 days from its acknowledgement. 6.3.8 was first decided on 24 October 2026, under the owner's delegation, because the published Helm charts of 6.3.6 and 6.3.7 start no server, which only a release can put right; the project's own work since found the vulnerabilities above, and 6.3.8 is made as a security release, that 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, longer than the day SECURITY.md allows; the others were pushed with the release. The 6.3 line receives security fixes until 30 September 2027. To hear of a release, switch the server's update check on (UpdateCheckEnabled), or follow the releases' feed. Reporting a vulnerability Not in a thread here, and not in a public issue. Report it privately, as SECURITY.md says: a confidential issue on GitLab - check that it is still marked confidential before you submit it, and choose the Security template - or an email to contact-project+hmailserver-hmailserver-86730559-issue-@incoming.gitlab.com, which GitLab files as a confidential issue, so no GitLab account is needed. Release notes, checksums and downloads · Mirror on GitHub · Newest stable release
  • 0 Votes
    1 Posts
    321 Views
    chrisC
    This release is superseded by hMailServer 6.3.8, a security release published on 2 November 2026. Upgrade every server running 6.3.7 or earlier, and read its announcement before upgrading. hMailServer 6.3.7 was published on 17 October 2026. It is a security release: it fixes the security flaws listed under Security below, in every earlier version, among them a way for any account that could sign in to a Windows computer to replace the programs of an hMailServer installed at the root of a drive, the DKIM signing of mail from outside in a hosted domain's name, and any sender's way past the rule loop limit; and its Windows installers carry the .NET Desktop Runtime 10.0.13, and its x64 installer MariaDB Connector/C 3.4.11. The schema stays at 6108: from 6.3.6 the upgrade changes no table. It is the first release since 6.3.6 and the first security release made while releases pause, and it carries more than fixes: the work that was under way on 13 October 2026, which the owner decided that day to finish and fold into one batch, and, by his decision of 15 October 2026, the work already done for a later release - the first step of the Control Deck's and the webmail's rebuild planned for 6.4: the framework decision, the threat model of the web surface and the authorisation matrix with its check, with no product code. Who is affected, and what to do Every server running 6.3.6 or earlier: upgrade. The security section below says why. Before upgrading, read "Before you upgrade". Some changes alter what an upgraded server does without anything being switched on - above all for accounts that send as another hosted domain's addresses, for rules and Sieve scripts that rewrite who a message is from, for an account given the serveradmin role with a list of domains that administers the whole server over COM, and for a database account that signs in with MySQL Enterprise's Windows authentication. On 6.3.6: the live update installs 6.3.7, with the exceptions under "Upgrading". On a Windows server whose service runs as NT SERVICE\hMailServer, an administrator presses Install update on the Control Panel's Server status page, on the server, and Windows asks once. On 6.3.4 or 6.3.5: the live update installs 6.3.7 too, with the exceptions under "Upgrading". On 6.2.28 to 6.3.3, or on a Linux server from 6.3.3's AArch64 packages or AppImages: upgrade by hand, once. Those versions trust only the GitHub workflow that signed the releases up to 6.3.3. Older than 6.2.28: run the installer, as always; the upgrade chain runs from every earlier hMailServer version. Before you upgrade Back up the database and the data directory, each with its own tools (Upgrading.md). 6.3.7 adds no schema step, so the live update's rollback to 6.3.6, should 6.3.7 not start, meets no newer schema. From 6.3.5 or older the upgrade runs 6.3.6's three one-way steps to 6108, 6.3.6's announcement applies, and the backup is the only way back; from 6.3.3 or older 6.3.4's steps come first, and 6.3.4's notes apply as well. Accounts that send as another hosted domain. An authenticated submission is now signed as another domain hosted here only when its account may send as the address its From names - its own, an alias pointing at it, a mailbox that granted it the post right, an address it proved, or a masked address of its own. Otherwise the mail is still accepted and sent, unsigned as that domain. Give such an account the alias or the grant before upgrading. Rules and Sieve scripts that rewrite who a message is from. A domain's or an account's rule may no longer set From, Sender, Resent-From, Resent-Sender or DKIM-Signature, or the loop count, and its Reply goes only from an address its owner may send as; a Sieve script may no longer add or delete those fields, Auto-Submitted or the loop count, and its notify goes from the account's own address unless its :from reaches the account. One stored before the upgrade still runs, without that action or edit, and the application log names it. To set From on a domain's mail, make the rule a server rule with a condition on the domain. RuleLoopLimit 0 now stops out-of-office replies, as it stopped every other generated message. The serveradmin role with a list of domains. An account given the serveradmin role with a list of domains is, over COM as over REST, confined to those domains and no longer the server's administrator: give the role with no list to an account that is to administer the whole server, a script's or a Control Panel's account among them. Event scripts that use a transaction. A script that goes on in a transaction after Database.ExecuteSQL has failed is refused every statement after it once the database has ended the transaction, and Database.CommitTransaction then fails: call Database.RollbackTransaction and run the whole transaction again, or stop. The x64 installer's MySQL and MariaDB client is MariaDB Connector/C 3.4.11, built from MariaDB's source, with seven authentication plugins where 6.3.6's had nine. A database account that signs in with MySQL Enterprise's Windows authentication can no longer sign in, and the server then cannot open its database. No such setup is known to the project; ThirdPartyBinaries.md says what one would have to do before upgrading. The .NET 10 Desktop Runtime is not replaced. The Control Panel and the setup tools run on it; the mail server itself does not. The installers carry 10.0.13, Microsoft's security release of 13 October 2026, but install it only where no .NET 10 Desktop Runtime is installed at all: an upgraded server keeps the one it had - 10.0.12 where 6.3.4, 6.3.5 or 6.3.6 installed it - against which Microsoft published advisories that day, several of them rated High; whether hMailServer's own tools reach the code they concern has not been established. Bring it to 10.0.13 or later through Microsoft Update (on Windows Server, WSUS or the Microsoft Update Catalog, or Automatic Updates where the server has been opted in), or with Microsoft's installer from https://dotnet.microsoft.com/download/dotnet/10.0; dotnet --list-runtimes lists what a machine has. Windows: the program folder, Bin and ControlPanel. At every installation and upgrade /ProtectFolders now hardens the three: they keep no entry that lets another user change them, a file's own entry aside, which a CHECK line names, and under Program Files the first run makes their inherited entries their own. An installation at the root of a drive is hardened by the upgrade itself; to have it hardened before the upgrade runs anything from it, follow the steps "Before upgrading an installation at a drive's root" in ServiceAccount.md first. Reinstall the programs from the release into a new folder that only the administrators may change, not over the installation, where accounts that are not administrators could sign in to the computer while it was installed so, where anybody but SYSTEM, the Administrators or TrustedInstaller owned one of the three folders, a file directly in the program folder, or Events, WebAdmin, DBScripts, Addons or Languages or anything in them or in Bin or ControlPanel, or where one of the three had an entry of its own that let anybody else change it, or one the steps could not take away: setup leaves in place every file it does not ship, and puts the event script in Events, which the service runs, and the scripts in Addons only where none is there. The documentation says how: nothing run from the old folder, its uninstaller included; the mail, the built-in database and the settings file copied into the new folder before setup runs, since setup run into a folder without a settings file sets up a new, empty server; the settings file - its [Directories] and administrator password - and the event script read first; and setup given the new folder with /DIR=, since over an installation it installs into the folder it finds, once a 32-bit installation's uninstall entry, which overrides /DIR=, is taken away. Sign-in and relaying. SMTP, POP3 and ManageSieve refuse a SASL PLAIN sign-in that asks to act as another account. With SRS on, mail to an SRS address this server handed out is held to the relay rules unless it is a bounce. The anti-virus action Remove all attachments and deliver the text (Delete infected attachments only in 6.3.6's Control Panel) now delivers a message that is one attachment as the note alone, and deletes a message the server cannot read, telling its recipients. Forwards of mail from outside. A forward, a redirect or a rule's forward of mail from outside whose From names the forwarding domain's own organisation, and that did not pass DMARC for it when it arrived, keeps the envelope sender it came with, with no SRS address or rewritten sender; a copy of it with an empty envelope sender is not sent to an outside address under this server's host name where that name is in the same organisation; a copy whose own envelope sender is at this server's host name, or at a hosted domain or a subdomain of one, in the organisation its From names is not sent to an outside address at all - the message stays in the mailbox, and the application log names each recipient - nor is such a message sent where an alias or a distribution list expands to an outside address; and a managed list does not distribute a posting whose From names the list domain's organisation unless it passed. No pass is recorded for a message the DMARC check did not judge - with ASDMARCEnabled off, from a sender on the anti-spam white list or an IP range with spam protection off, larger than antispammaxsize (1024 KB as shipped), already scored by the tests before it at the higher of the spam thresholds, imported from a file or downloaded - nor for a From whose domain publishes no DMARC record. Security Each of these was found by the project's own review, and each is fixed in 6.3.7, unless it says otherwise. Each links its work item, which gives the details, and the Security section of CHANGELOG.md says what changed. CVE-2026-107571, work item #78 - on Windows, where hMailServer was installed in a folder that took its permissions from the root of a drive, any account that could sign in to the computer could replace the programs the service runs; every version from 6.0.0 to 6.3.6. The default folder under Program Files was never affected. Four narrower cases are not fixed in 6.3.7 (Known issues, below). CVE-2026-107567, work item #84 - mail from outside whose From named a domain hosted here was signed with that domain's DKIM key, and so was every forward and copy of it, so that it passed DMARC for that domain elsewhere; and an authenticated submission was signed as whatever hosted domain its From named, which 6.3.7 does only where the account may send as that address; every version from 6.0.0 to 6.3.6 that signs. An upgrade stops the signing; only rotating a domain's key ends the signatures already made. CVE-2026-114478 and CVE-2026-114474, work item #90 and work item #91 - a forward, a redirect, a list's copy or an alias's copy of mail from outside, a forward of a message downloaded from an external account among them, was given an envelope sender, or for a null one was sent under a host name, in the organisation of a domain its From names, so that it could pass that domain's DMARC policy through SPF; every version from 6.0.0 to 6.3.6. 6.3.7 does so only when the message passed DMARC for it on arrival. CVE-2026-107568, work item #82 - an account's Sieve notify was sent from whatever its :from said, and signed as that address's domain where the server signs for it; 6.2.22 to 6.3.6. CVE-2026-107566, work item #83 - an account's Sieve editheader could rewrite From, Sender or DKIM-Signature ahead of a redirect, which the server then sent and signed in that name; 6.2.22 to 6.3.6. A domain's or an account's rule had the same gap, in every version - a Reply from an address its owner may not send as, a Set header value of From, Sender, the Resent- pair or DKIM-Signature - and is held the same way, under work items #82 and #83. CVE-2026-107569, work item #81 - any sender could lift the rule loop limit off a message, an account's own Sieve script or rule could reset it, and a Sieve notify did not consult it, so that a loop of generated mail could run without bound; every version from 6.0.0 to 6.3.6. CVE-2026-114489, work item #86 - on a server with several tenants, a credential confined to named domains - a domain administrator, an account or API key given another role that manages accounts in named domains, or a full-scope API key issued for named domains - could rename a domain, create a domain alias, set on a domain's or an account's rule an action that reaches the server's own addresses, a configured route's stored credentials or a function of the event script, and point its domain's relay host at the server's own network, over the REST and the COM APIs; every version from 6.0.0 to 6.3.6. CVE-2026-114475, work item #89 - such a credential, a full-scope API key issued for named domains or the serveradmin role given with a list of domains, could rewrite over the REST API a custom role that accounts in other domains hold, and create custom roles or delete one that no account holds; 6.3.4 to 6.3.6. CVE-2026-114477, work item #96 - a credential confined to named domains that may read their configuration, such as a domain administrator, the auditor role given a list of domains or an API key issued for named domains, could read the server-wide metric history over the REST API; 6.2.25 to 6.3.6. CVE-2026-114490, work item #87 - over COM, an account given the serveradmin role with a list of domains was the server's administrator, its list aside, and one given domainadmin with a list that leaves out its own domain administered that domain's ordinary accounts, where the REST API refused both; 6.3.4 to 6.3.6, on Windows. CVE-2026-114476, work item #88 - over the REST API, a credential that holds manage_roles in named domains could take a role away from an account in those domains whose admin level is the server administrator's, and so hand that account the server administrator's authority; 6.3.4 to 6.3.6. CVE-2026-114470, work item #93 - a page on another site could make a browser that held a REST API credential from HTTP Basic send a state-changing request to the server, a cross-site request forgery; every version from 6.0.0 to 6.3.6. CVE-2026-114472, work item #97 - the ACME HTTP-01 challenge responder served one connection at a time, so idle clients could make a certificate's issuance or renewal fail; every version from 6.0.0 to 6.3.6. It now serves up to 64 connections at once and closes one past that; a client that holds 64 idle connections open through a whole validation can still make it fail (the work item says what remains). CVE-2026-114479, work item #92 - with the anti-virus action that removes attachments (AVAction 1: Delete infected attachments only in 6.3.6's Control Panel, Remove all attachments and deliver the text in 6.3.7's), a message whose one part is an attachment, and an S/MIME message signed as one part, were delivered as they arrived, with what the scanner had found in them; every version from 6.0.0 to 6.3.6. The default action, which deletes the message, was never affected. CVE-2026-114473, work item #94 - with SRS on, which it is not by default, mail to an SRS address this server had handed out was not held to the relay rules; 6.2.1 to 6.3.6. Found by upstream hMailServer's review of its own SRS (upstream PR 649). CVE-2026-114471, work item #95 - with SMTP logging on, which it is not by default, the SMTP log wrote some credentials in the clear - an AUTH PLAIN sent before EHLO or not in its one exact form, the bearer token of AUTH XOAUTH2 and AUTH OAUTHBEARER, and the lines of SCRAM-SHA-256, OAUTHBEARER and GSSAPI; every version from 6.0.0 to 6.3.6, each mechanism from the version that brought it. Found by upstream hMailServer (upstream issue 654). The same release fixes or hardens more without a CVE request: the ARC seals this server makes attest its own SPF result, are made by the domain that relayed the message and name the domains their results are for; the stored-secrets key is no longer left in memory freed without being written over, nor a mailbox key encrypted at rest open past its idle minutes (both from 6.3.4); and, found by upstream hMailServer in code this project shares with it, an SRS address or a BATV tag almost a whole cycle old is no longer taken for fresh (upstream PR 649), and a SASL PLAIN client that asks SMTP, POP3 or ManageSieve to act as another account is refused, where it was signed in as itself (upstream issue 652). The Windows installers carry the .NET Desktop Runtime 10.0.13 (Microsoft's security release of 13 October 2026, where 6.3.4 to 6.3.6 carried 10.0.12), and the x64 installer MariaDB Connector/C 3.4.11, the first release of Connector/C with the fix of CVE-2026-107819, a flaw of the Connector/C 3.4.9 that 6.2.3 to 6.3.6 carried; the ARM64 installer carries no MySQL or MariaDB client. Upgrade every server running 6.3.6 or earlier. Five of these fixes, those of work items 78 and 81 to 84, and their descriptions in CHANGELOG.md, were public in the project's repository before this release - on GitLab from 6 and 7 October 2026, and on master from 7 and 9 October 2026. That was the owner's decision, and an exception: the rule, which holds again after 6.3.7, is that a security fix becomes public about a day before the release that ships it (SECURITY.md, "What gets a release"). Upgrading By the live update, from 6.3.6. Every file of 6.3.7 is signed with Sigstore as this release's own pipeline at the project's group path, https://gitlab.com/hmailserver/hmailserver//.gitlab-ci.yml@refs/tags/v6.3.7, with the project's id, 86730559, in the certificate; every server from 6.3.4 on has that signer compiled in and requires the id for it. From 6.3.6 the live update installs 6.3.7, with 6.3.6's own file as its rollback image. On Windows a service running as LocalSystem installs it itself; one running as NT SERVICE\hMailServer downloads 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 6.3.6's own update helper applies it, for the generic .deb and .rpm of either architecture and for both AppImages; an ARM64 Windows server finds the ARM64 installer by name. No live update had installed a release signed at the group's path when this was written. From 6.3.4 or 6.3.5, what 6.3.6's notes said of those two versions still holds, since the update is applied by the version already installed. A Windows service running as LocalSystem installs 6.3.7 by its live update, from 6.3.5 with 6.3.5's file as its rollback image and from 6.3.4 without one; that image is no way back once the upgrade's schema steps have run, and the backup taken before the upgrade is. One running as NT SERVICE\hMailServer downloads and verifies it, then leaves the install to an administrator (HM7463), whose Install update on 6.3.4 and 6.3.5 cannot do it: download hMailServer-6.3.7-x64.exe from the release, check that Get-AuthenticodeSignature says Valid for CN=Progressive Robot Ltd, and run it as an administrator, keeping the service account on the installer's Service account page - once, since every release after it installs with one click. 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. On Linux, install 6.3.7 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 (CVE-2026-104658), or turn the apply off until then with systemctl disable --now hmailserver-update.path. Three settings left from 6.3.5's by-hand route. A server whose UpdateSigningIdentity, UpdateSigningIssuer and UpdateSourceRepository still name the path the project began at refuses 6.3.7. Clear the three first: on 6.3.4 and 6.3.5 in the Control Panel, with DELETE /api/v1/settings/ini/<name> or on Linux with hmailserver --set-setting <name>=, not in the file, which those versions put back; on 6.3.6, which reads them from hMailServer.ini alone, by deleting their lines from [Settings] with an editor run as an administrator, or on Linux with sudo hmailserver --set-setting <name>=, and restarting the service. By hand, from 6.2.28 to 6.3.3, and from 6.3.3's AArch64 packages or AppImages. On Windows run hMailServer-6.3.7-x64.exe over the existing installation; on Linux install the package with apt or dnf, or replace the AppImage with 6.3.7's. Those versions refuse a release signed on GitLab, and one pointed at this project's feed with UpdateFeedUrl downloads the installer at every check and refuses it: keep UpdateAutoDownload off there until then. A server from one of 6.3.6's per-distribution packages is offered the generic package by its live update, which apt refuses on Debian 12 and dnf on Enterprise Linux 9. There, upgrade with the package manager and your release's own package of 6.3.7. The container image, registry.gitlab.com/hmailserver/hmailserver:6.3.7, also tagged 6.3 and latest. In the container the update check names the image to pull and installs nothing (ContainerImage.md). What else changes on upgrade The 6.3.7 section of CHANGELOG.md lists everything under "What changes on upgrade without a switch". The rest, in short: Database errors: on MySQL, MariaDB, SQL Server and SQL Server Compact an insert that forgives a duplicate key forgives that alone, and any other failure is reported (HM5032), where it was taken as done with no row written; on MySQL and MariaDB a lost connection is reconnected and the statement run again, so the statement after a database restart, a failover or wait_timeout no longer fails. An error log that was quiet may now name failures that were hidden. IMAP: COPY and MOVE by message number read their set in the client's numbering, and a set that names a message another session has just expunged is refused, NO [EXPUNGEISSUED], for the client to send again; a COPY or MOVE that fails part way leaves the destination as it was; SEARCH, SORT and THREAD ask a criterion named again beside itself once; FETCH BODY[N.MIME], and BODY[N] of a multipart, are the stored octets. POP3: classic Outlook for Windows, Outlook 2016 to 2024, downloads its mail again: the server accepts the UTF8 it sends after the login, until the session's first STAT, LIST, UIDL, RETR, TOP, DELE or RSET. DNS: a DNS lookup that fails is a failure, not a name with no addresses (upstream PR 640). The HELO and PTR tests no longer score a name they could not look up, a DNS blacklist and SURBL no longer read a failed lookup as "not listed", and delivery whose mail servers' lookup failed tells the sender that the DNS query failed (4.4.4) and retries. Anti-spam: a link's host that is not ASCII is read by UTS 46, and an HTML part's character references as a browser reads them, for the SURBL test and the sender lists' URL entries; and SpamAssassin is sent a collected message's own Return-Path, so its SPF and envelope rules may score such mail differently. Large messages: a message over 80 MB goes through the rules, the forwards, a bounce and the spam headers by its header alone, where it went through them as if it had no header; the rule conditions that need the body do not match, each with a line in the application log. Stops: setup, the live update's silent install and the uninstaller wait ShutdownDrainSeconds plus 90 seconds for the service's stop, where they waited plus 30; and on Windows a stop no longer waits past a work queue's ten seconds for a thread held by a host or a database that has stopped answering. Rules: a rule's Run function is refused when it is saved unless it names a plain function, for a rule of every owner, over COM and REST. The quarantine: a quarantined copy's file name carries where the message came from, its SPF result and the relaying domain (<GUID>.spf-fail.outside.eml, .relay-<id>); a tool that reads the quarantine folder by name meets the longer names. The Control Panel: Updates is a page of its own under Maintenance; the sidebar has Compliance and Virus scanning groups; API & monitoring is Monitoring & alerts; the PROXY protocol and XCLIENT are a card on Incoming relays; and Ctrl+K takes every old title and route to its page. A Control Panel signed in to a server on another computer shows and saves the server's hMailServer.ini [Settings] values. Reported from outside and fixed besides: the Control Panel's IP ranges page, which could show no list of ranges at all (work item #79, ThiagoES84); mail to a DNSSEC-signed domain whose MX answer came compressed, live.com among them, which DANE enforcement deferred (work item #80, ThiagoES84); and classic Outlook's POP3 downloads, which stopped at the UTF8 it sends after the login (work item #85, ciprianbalus). Thank you. Also new The IMAP and POP3 listeners take the client's address from a PROXY protocol header, as the SMTP listener does, each from the peers on a list of its own; both are off by default. The first units of the active-active work, which is still not built (ActiveActive.md). The first step of the Control Deck's and the webmail's rebuild, with no product code: ControlDeckFramework.md, WebThreatModel.md and WebAuthorisationMatrix.md, the matrix written from the code by build/generate-auth-matrix.py and held to it in CI. What the release carries 62 files, each with its Sigstore bundle, <file>.cosign.bundle, beside it: Windows: hMailServer-6.3.7-x64.exe, and hMailServer-6.3.7-arm64.exe for Windows on ARM, both Authenticode-signed. The ARM64 installer is a preview: it is cross-compiled on the project's x64 Windows runner and has never run on ARM64 hardware. SQL Server Compact has no ARM64 build, so a new ARM64 installation is set up against SQL Server or PostgreSQL (WindowsOnArm.md). Linux, x86-64 and AArch64: the generic hmailserver_6.3.7_amd64.deb, hmailserver_6.3.7_arm64.deb, hmailserver-6.3.7-1.x86_64.rpm and hmailserver-6.3.7-1.aarch64.rpm, the names an installed server's update check asks for; and each Debian 12 and 13, Ubuntu 24.04, Fedora 44 and 43, Enterprise Linux 10 and 9, openSUSE Leap 16.0 and Tumbleweed release's own package for both architectures. The AArch64 files are cross-compiled and tested under emulation: no AArch64 machine has run them. The AppImages, hMailServer-6.3.7-x86_64.AppImage and hMailServer-6.3.7-aarch64.AppImage, and beside them the source of the libraries they carry and of those their runtime links. The source tarball, hmailserver-6.3.7.tar.gz; the Helm chart, hmailserver-chart-6.3.7.tgz; and the PowerShell module, HmailServer.psd1 and HmailServer.psm1, Authenticode-signed. An SBOM for each compiled file and the tarball, <file>.cdx.json, and the source tree's two; hmailserver-linux-6.3.7-SHA256SUMS.txt, the SHA-256 of every Linux file; and, beside the 62, hmailserver-6.3.7.intoto.sigstore.json, SLSA v1 build provenance that names each of them by its SHA-256. https://updates.progressiverobot.com/download/ leads to the newest stable release's page, and /download/hMailServer-x64.exe, /download/hmailserver_amd64.deb, /download/hmailserver.x86_64.rpm and the rest of the download links to that file of it. How to check a download With cosign and OpenSSL, with the file and its bundle beside you: cosign verify-blob --bundle hMailServer-6.3.7-x64.exe.cosign.bundle \ --certificate-identity 'https://gitlab.com/hmailserver/hmailserver//.gitlab-ci.yml@refs/tags/v6.3.7' \ --certificate-oidc-issuer https://gitlab.com hMailServer-6.3.7-x64.exe jq -r .verificationMaterial.certificate.rawBytes hMailServer-6.3.7-x64.exe.cosign.bundle \ | base64 -d | openssl x509 -inform DER -noout -text | grep -A1 1.3.6.1.4.1.57264.1.15 The first must say Verified OK, and the second print ..86730559, the project's own id, which cosign does not check: the identity is a path, and a path could pass to another project were the group or the project renamed. Each file but a <file>.cdx.json SBOM also has its bundle a second time as <file>.sigstore.json; either name verifies the same way. To check that a file is one this release's pipeline built, against its build provenance: cosign verify-blob-attestation --new-bundle-format \ --bundle hmailserver-6.3.7.intoto.sigstore.json --type slsaprovenance1 \ --certificate-identity 'https://gitlab.com/hmailserver/hmailserver//.gitlab-ci.yml@refs/tags/v6.3.7' \ --certificate-oidc-issuer https://gitlab.com hmailserver_6.3.7_amd64.deb It says Verified OK only when the file's SHA-256 is one the provenance names. Then: sha256sum -c --ignore-missing hmailserver-linux-6.3.7-SHA256SUMS.txt must say OK for each Linux file you downloaded. On Windows, Get-AuthenticodeSignature on either installer must answer Valid, with a signer certificate for CN=Progressive Robot Ltd. The x64 installer's published SHA-256 is dcae96bd75d6cae8e5a910d15940567067539ed99db5aeed40a815added4d85f. In a clone, git -c gpg.ssh.allowedSignersFile=.gitlab/allowed_signers verify-tag v6.3.7 must report a good signature by the one key that file lists. For the image, ContainerImage.md, "Checking an image", gives the cosign verify command and the check of the project's id. The release's own notes, under Verification, give each file's SHA-256 and the certificate the pipeline was given. SECURITY.md, "Verifying a release", has the rest. How it was checked The tag's pipeline (https://gitlab.com/hmailserver/hmailserver/-/pipelines/2958958332) ran every gate before anything was published: the Windows suite, in two halves on the project's two Windows benches, 4,446 tests, 4,366 passed, 0 failed, 80 skipped; the Linux suite on one of the project's Linux runners, 2,652 tests, 2,284 passed, 0 failed, 368 skipped; the rebuild of the x86-64 server, .deb and .rpm byte for byte; the install of every Linux package in a clean container of its release, by that release's own package manager, the AArch64 ones under emulation, but for the generic x86-64 .rpm, which is built on Ubuntu and was installed on Fedora 44 with rpm --nodeps, its dependencies not resolved; the generic x86-64 .deb configured and started against a database; the static analysis, Semgrep and the dependency scan. It then checked every signature with the server's own verifier, hmailserver --verify-update, built from the same commit. The signed installer went through the installer smoke test: an upgrade from 6.3.6 passed 98 checks and a new installation 64, none failed. Known issues Four narrower cases of work item #78 are not fixed in 6.3.7, by the owner's decision, and stay open: setup hardens a program folder before it copies anything into it only when setup makes the folder, and then just after making it; a folder that is already there is hardened in place, before the copy, only for an installation of the Control Panel alone, while for a server installation, and at every upgrade, the program folder, Bin and ControlPanel are hardened by /ProtectFolders at the end of setup; a link or a junction in the program folder's path is named in setup's log as not hardened, and not refused; and the files in Bin and ControlPanel are not listed by their owners. None of them reaches an installation in a folder only the administrators may change - the default under Program Files is one - unless a link that another account controls is on its path. Before upgrading an installation at the root of a drive, follow ServiceAccount.md's steps (Before you upgrade, above). The ARM64 installer has never run on ARM64 hardware, and no AArch64 machine has run the AArch64 Linux files. If you install either, please say how it went, working or not, in an issue. No channel publishes it yet. The apt and dnf repositories, Docker Hub, Quay.io, the AUR, the Ubuntu PPA, Copr, the openSUSE Build Service, Chocolatey, PyPI and the PowerShell Gallery publish from master once their keys and accounts exist; none is live as this release is made (Channels.md). The Snap Store's name is registered, and the job that would have put 6.3.7 on its edge and candidate channels once the release was published failed on 18 October 2026, on a tool missing from its image, with the two snaps built and checked: 6.3.7 is not on the Snap Store (Snap.md). An AppImage on Fedora, Enterprise Linux or openSUSE runs on the host's glibc and cannot require its extra character-set converters: install glibc-gconv-extra (Fedora, EL) or glibc-gconv-modules-extra (openSUSE) by hand; the server says when they are missing. An upgraded server keeps the .NET 10 Desktop Runtime it had (Before you upgrade, above). While releases pause 6.3.6 was the last release planned by date, by the owner's decision of 28 September 2026; the releases since are security releases, each cut from master with whatever finished work master holds then, until the maintainers say otherwise in SECURITY.md, "While releases pause". A confirmed vulnerability still ships in a release: a critical or high one as soon as its fix has passed the gates, and none held past 90 days from its acknowledgement. Other defects are fixed on master and ship with the next security release, or when releases resume. The 6.3 line receives security fixes until 30 September 2027; before that date the maintainers write in SECURITY.md whether support continues, and if nothing has been written, the 6.3 line is not supported from 1 October 2027. Nothing is back-ported: an installation of 6.3.6, 6.2 or anything older is upgraded in place to the newest release. To hear of a security release, switch the server's update check on (UpdateCheckEnabled), or follow the releases' feed. Reporting a vulnerability Not in a thread here, and not in a public issue. Report it privately, as SECURITY.md says: a confidential issue on GitLab - check that it is still marked confidential before you submit it, and choose the Security template - or an email to contact-project+hmailserver-hmailserver-86730559-issue-@incoming.gitlab.com, which GitLab files as a confidential issue, so no GitLab account is needed. Release notes, checksums and downloads · Mirror on GitHub · Newest stable release
  • 0 Votes
    1 Posts
    196 Views
    chrisC
    This release is superseded by hMailServer 6.3.7, published on 17 October 2026, which fixes security flaws in every earlier version, 6.3.6 included. Read its announcement before upgrading. hMailServer 6.3.6 was published on 6 October 2026. It fixes the security flaws the project's own reviews of the code found, those listed under Security below with their CVE identifiers among them, and takes OpenSSL 4.0.3 into the Windows installers. It also fixes what 6.3.5's release and the first live updates from 6.3.4 found, replaces with the project's own code the last code in the server whose licence a distribution would have had to settle, and carries every file its pipeline makes, the ARM64 Windows installer (a preview) and the AArch64 Linux packages among them. The schema moves from 6105 to 6108 in three one-way steps. 6.3.6 is the last release for the time being: after it the project makes security releases only (below). Who is affected, and what to do Every server running 6.3.5 or earlier: upgrade. The security section below says why. Before upgrading, read "Before you upgrade". Some changes alter what an upgraded server does without anything being switched on - above all for a server whose DNS resolver strips DNSSEC, for external accounts, for scripts that use COM, for programs that read a Windows server's logs or mail as an ordinary account, and for administrators who run the store commands or install by script. On 6.3.4 or 6.3.5: the live update installs 6.3.6, with the exceptions under "Upgrading". On 6.2.28 to 6.3.3, or on a Linux server from an AArch64 package or an AppImage: upgrade by hand, once. Those versions trust only the GitHub workflow that signed the releases up to 6.3.3. Older than 6.2.28: run the installer, as always; the upgrade chain runs from every earlier hMailServer version. Before you upgrade Back up the database and the data directory. The three schema steps are one-way: 6106 gives external accounts' days to keep messages one meaning (below); 6107 finishes the greylisting key - a sender whose old row it deletes is greylisted once more, and on a large hm_greylisting_triplets it keys the old rows before its transaction, committing as it goes; 6108 adds the never-ban list by reverse name and each SMTP listener's choice of what it does with AUTH. From 6.3.3 or older the upgrade runs 6.3.4's steps first, and 6.3.4's notes apply. Mail can stop flowing if your DNS resolver strips DNSSEC. With DANE on, as it is by default, an MX or TLSA lookup that does not come back whole - no answer, SERVFAIL, a signed zone's answer without its signatures or without its proof that no records exist - no longer counts as "unsigned": the host is not used, and a delivery with no host left is deferred with 4.7.0. A server whose resolver cannot answer DNSSEC queries, or behind a firewall that drops DNS answers longer than 512 octets, now defers its mail to most domains, and says so in each deferral and in the application log ("DNSSEC: the root zone's keys did not validate"). Make the resolver answer DNSSEC queries (any current public or ISP resolver does), or set DaneEnforcementEnabled to 0. A private name under a signed public domain, answered by an internal DNS server, needs a route. Outbound TLS defers more. A host that the destination's TLSA records or MTA-STS policy require TLS of, and that offers no STARTTLS, is deferred with 450 4.7.0 where the message bounced; a host with only DANE-TA records whose certificate does not meet them is deferred where it was delivered to as if no record were published. External accounts: days to keep messages means one thing now. -1 deletes each message on the remote server once collected, 0 leaves every message there, N deletes it N days after collection - for POP3 and IMAP alike. From 6.2.25 to 6.3.5 the IMAP fetcher read 0 and -1 the other way round, and the Control Panel, the Deck, the webmail and the REST API described that reversed meaning. The upgrade keeps every account doing what it did or stops it deleting. Afterwards, look at your IMAP external accounts (one that was emptying its remote INBOX now shows -1 and goes on until you set 0) and your POP3 accounts at -1 (they go on deleting until you set 0). A script that writes the value, or an OnExternalAccountDownload script that answers 1 for an IMAP account, follows the one meaning from now on. A webmail or Deck page left open across the upgrade has a change with a negative number refused (409, reload the page) until it is reloaded. A script outside the service that uses COM, on a server with an administrator password. Utilities.BlowfishEncrypt, BlowfishDecrypt, ResolveMXRecords, GetMailServer, IsLocalHost, CheckSieveSyntax, EvaluateSieveScript and CriteriaMatch answer only a caller that has signed in: create hMailServer.Application, call Authenticate, and use that object's Utilities. A message or fetch account the script creates itself works only while the same program holds an Application it signed in on; or make the message with the new Application.CreateMessage() after Authenticate. Attachments.Add and Attachment.SaveAs are the server administrator's alone, and mail a script signed in as an account sends must be from an address that account may send as. Event scripts in the service, the Control Panel and the shipped DecryptBlowfish.vbs need no change. Windows: the data, log, temp, database and event folders and hMailServer.INI become private to SYSTEM, the Administrators and the service, at every installation and upgrade, whatever account the service runs as; on a large message store the first run takes minutes. A script, a backup program or a monitoring agent that read the logs or the message store as an ordinary account, or an administrator's own program not run as administrator, needs an entry of its own (icacls "<folder>" /grant "<account>:(OI)(CI)RX"), which later runs keep. The Control Panel not run as administrator says Could not read the file on its live log page and its message-store check. An editor that saves hMailServer.INI by replacing the file gives it Bin's permissions again: run hMailServer.exe /ProtectFolders as an administrator after it (ServiceAccount.md, "What the other users of the computer may read"). The administrator's commands. hMailServer.exe's command lines (all but the service's own) no longer write to the error log or the Application log: what they did and why they failed is on their own output, so a script that read the error log after one reads the output instead. The store commands (--compress-store, --encrypt-store, --recount-store, --measure-store and the rest) and the object-store copies stop, naming it, when the data folder holds a junction, mount point or symbolic link that the administrators do not alone control: a junction made in the data folder, or at a drive's root, is refused until it is given permissions only the administrators may change (ServiceAccount.md, "What an administrator's command writes"). The grant Setup makes again at each upgrade of a service running as NT SERVICE\hMailServer goes through a junction on the way to Data, Logs or Database on the same terms, where it refused every one, and still refuses one the administrators do not alone control, naming who may change it, which fails the upgrade: give such a junction to the administrators first. On Linux such a command run as root first becomes the service's account, and refuses (exit 2), naming why, when the way to the data directory is not root's and its owner's alone; over a data directory root owns it runs on as root only where no other account could have put a link where it goes. Installing by script. The installer now exits 10 when a step after its files did not complete and the database is as the earlier version left it, and 11 once its database step has changed the database; it exited 0. /GrantServiceAccess that cannot read the database ends in 7 unless an administrator confirmed. The live update's trust is read from hMailServer.ini alone. UpdateSigningIdentity, UpdateSigningIssuer, UpdateSourceRepository, UpdateTrustRootsFile and UpdateLogPublicKeyFile are read from the file's [Settings] section, and COM and the REST API (409) refuse a change to them. Most servers set none of the five. One that set a signer of its own over REST, the Deck or the Control Panel puts it in the file and restarts the service; where several servers share a database, put it in every server's file before the first is upgraded. Three settings left from 6.3.5's by-hand route. A server whose UpdateSigningIdentity, UpdateSigningIssuer and UpdateSourceRepository still name the project's first path refuses 6.3.6. Clear the three first, in the Control Panel, with DELETE /api/v1/settings/ini/<name> or on Linux with hmailserver --set-setting <name>=, not in the file, which 6.3.4 and 6.3.5 put back. Behind a reverse proxy. The REST listener's loopback port now answers only localhost, an IP address, the server's host name and the host of each configured outside address of the webmail, and refuses any other Host with 421. If your proxy forwards the browser's own Host, set that name as the webmail's outside address (for example confidential_base_url) or as the server's host name before upgrading. The proxy must pass the Control Deck's page through unchanged. Port 25. An upgraded server's SMTP listeners stay On and still take AUTH on port 25, so nobody is banned on the morning of the upgrade. The setup checklist's new item No password on port 25 says so: move clients that sign in on 25 to 587, then set port 25 to Honeypot or Off, on which the first AUTH bans the address. A new installation's port 25 is a Honeypot. URI block lists. The SURBL test now asks each list about every domain a message links to, up to 200 - up to 256 DNS queries per list for a message with many links, where it was 16 - and a message whose links name more than 200 domains scores the new setting SurblTooManyDomainsScore, 5 by default, which is the stock mark threshold. A list's error answers are never a listing: a list of your own that answers 127.0.0.1 for a listing no longer tags anything - have it answer another address, such as 127.0.0.2. A sender list's URL entry written as a reversed address (1.2.0.192 for 192.0.2.1) no longer matches: write the address itself. ManageSieve (off by default) is held to deadlines: a command line within 30 seconds, sign-in within 60, a literal 300. Before signing in an address has one connection served at a time, and a connection that runs out of time before signing in counts as a failed logon for the auto-ban (never from the loopback address). A signed-in session ends after ten idle lines in a row or two minutes of waiting in all, and an account has at most four sessions. IP addresses written in text are read the same way on Windows as on Linux: four decimal parts, or IPv6 as RFC 4291 writes it. A value kept in an older form (192.168.1, 010.0.0.1, an address with a port) is read the new way from the first start: an API key's source restriction so written refuses the key from every address, a REST, web-services or SNMP bind address so written leaves that listener stopped, and an AutoBanNeverBan entry so written protects nothing (HM6524). Webmail S/MIME. A correspondent's certificate is kept for encrypting replies only when its chain is trusted, under the message's From address, and a different one only when the reader chooses Keep this certificate. Certificates kept before the upgrade stay: review them on the webmail's Security page. Windows, account retention. From 6.2.25 to 6.3.5 an account saved through a webmail's password or out-of-office plugin could have its retention overwritten: after upgrading, check the retention of the accounts whose holders use such a plugin. The packaging Compose file runs PostgreSQL 18. If you take the new docker-compose.yml, move the PostgreSQL 16 data once, with a dump and a load (ContainerImage.md, "PostgreSQL 16 to 18, in the Compose file"); a deployment that keeps its own copy of the file keeps PostgreSQL 16. A monitor that matches "The response carried no HTTP headers." needs the new sentence, which says who ended the connection, after how many bytes and seconds. Security Each of these was found by the project's own review, and each is fixed in 6.3.6 unless it says otherwise. Each links its work item, which gives the details, and the Security section of CHANGELOG.md says what changed; that file, as tagged for 6.3.6, names only the two Blowfish identifiers, and says "CVE requested" for the others; for the string replacement, the Sieve filter, the JSON reader and the Linux commands' error reports it gives other affected versions, and for the header unfolding, the SPF expansion and the Linux network waits it leaves out a way in, and there the entries here follow the corrections filed on their CVE requests after the tag was made. CVE-2026-103011 (work item #49) - a heap buffer overflow in the encryption routine of the legacy Blowfish code of stored secrets, inherited from the original hMailServer, in every version up to 6.3.5. 6.3.6 replaces that code with the project's own. CVE-2026-103010 (work item #49) - a heap buffer overflow in the decryption routine of the same code, in every version up to 6.3.3, fixed in 6.3.4, whose notes did not say so. CVE-2026-104660 (work item #59) - on Windows, COM objects that carry no credential - a message, its attachments or a fetch account that a program creates itself, and the objects an Application handed out once a later Authenticate on it failed - were not refused what only a signed-in caller may do, so with an administrator password set a local Windows user with no hMailServer credential could queue mail, read and write files as the service's account, and attach a fetch account to any account; every version up to 6.3.5. CVE-2026-104658 (work item #58) - on Linux, the live update's apply step, which runs as root, took the unit to stop and, for an AppImage, the verifying program from the request the service account writes, and once it had started the server again it wrote its log and its outcome by names through which a symbolic link that account planted could lead them elsewhere; 6.3.4 and 6.3.5. CVE-2026-107578 (work item #71) - the commands an administrator runs elevated on Windows wrote by path into folders the service's account can change, and on Linux the store commands run as root followed links that account planted and every command run as root wrote its error reports into that account's log directory by name, so a program already running as the service's account could lead an administrator's process to a file of its choosing; on Windows 6.3.4 and 6.3.5, on Linux every version from 6.3.0 to 6.3.5. CVE-2026-107573 (work item #77) - on Windows, the installer left the Data, Logs, Temp, Database and Events folders and hMailServer.INI with the permissions Program Files hands down, so every account that could sign in to the computer could read the mail, the logs, the event scripts and the database's password, which any account on the machine can unseal; every version from 6.0.0 to 6.3.5. The installer now makes them private to SYSTEM, the Administrators and the service, and on Linux the AppImage makes its folders 0700. CVE-2026-104659 (work item #57) - the REST administration listener's loopback port answered a request whatever its Host, so a web page could reach the console through a browser on the same machine, and a wrong administrator password over REST from the loopback address was not slowed down; earlier versions. Such a request is now refused with 421, and a wrong password costs a growing delay of up to ten seconds. CVE-2026-103647 (work item #52) - the webmail handed the browser an attachment it had decrypted itself (S/MIME or OpenPGP) under the type the message named; 6.3.2 to 6.3.5. It is now always a file the browser only saves. CVE-2026-104704 (work item #61) - outbound DANE used DANE-EE records alone, so a host whose validated TLSA records were of another kind was delivered to as if it published none; every version up to 6.3.5. DANE-TA is now verified too. CVE-2026-107584 (work item #64) - outbound DANE read a DNS lookup that did not come back whole as "unsigned", so whoever could disturb a recipient domain's DNS could take it out of DANE; every version up to 6.3.5. CVE-2026-107586 (work item #62) - the REST listener's table of browser sessions dropped its least recently used session whoever held it, so one account holder signing in over and over signed out every webmail tab and other browser on the server that sat idle for a minute; 6.2.28 to 6.3.5, and the Control Deck with them from 6.3.0. CVE-2026-107585 (work item #63) - the pending halves of single sign-on and passkey sign-ins, which anybody may start, were held the same way, so a client asking for sign-in starts a few times a second made other people's OpenID Connect, SAML and passkey sign-ins fail; 6.3.4 and 6.3.5. The browser now carries a pending sign-in, sealed. CVE-2026-107587 (work item #66) - the webmail kept a stranger's S/MIME certificate for encrypting replies without a trust check and under an address the certificate chose; 6.3.2 to 6.3.5. CVE-2026-107583 (work item #65) - the webmail's view of a message's HTML put its embedded images back in time growing with the square of the references, so one message could hold a REST worker for minutes; 6.3.2 to 6.3.5. CVE-2026-107582 (work item #67) - the server's string replacement took time growing with the square of what it replaced. A message with an HTML part and no text part, full of entities, held the REST worker that listed its folder, or an IMAP thread, for minutes (the message list from 6.3.3, the page of a message that expires from 6.3.4, IMAP PREVIEW from 6.2.22); and the DMARC test, on by default, verifying a received message's DKIM signature whose tags held a great many spaces, held the thread that accepts the message for seconds to tens of seconds before the server answered, with nobody opening anything, so a stream of such messages stopped the server accepting mail; every version from 6.0.0 up to 6.3.5. CVE-2026-107581 (work item #68) - parsing one IMAP FETCH from a signed-in account could hold an IMAP worker thread, shared with SMTP and POP3, for minutes and longer, and the sections a FETCH named of one message were all read and held before any was sent; every version from 6.0.0 up to 6.3.5. CVE-2026-107579 (work item #70) - with bounce or complaint processing on, or a list the list manager runs, a returned report begun by very many blank lines held a delivery thread for a minute and more; 6.3.4 and 6.3.5. CVE-2026-107580 (work item #69) - a header field with very many line breaks, read as text, held the REST worker or IMAP thread that read it for minutes, and the DMARC test, on by default, reads each received message's From that way, so such a From held the thread that accepts the message for seconds with nobody opening anything; reading a message's header from its file took seconds for a large header at each read; every version from 6.0.0 up to 6.3.5. CVE-2026-107577 (work item #72) - removing a MIME header parameter whose value is empty and directly followed by a ; never ended, holding a worker thread at full load until the service was restarted - reached from one received message when the attachment blocker (not on by default) renames a blocked attachment, or when a filename is set over the REST API - and decoding many encoded words of an encoding other than base64 or quoted-printable, removing a parameter of many continuations, or deleting many header fields of one name took time growing with the square of their number; every version from 6.0.0 up to 6.3.5. CVE-2026-107576 (work item #73) - verifying a received message's DKIM or ARC signatures took time growing with the square of its header, so a message with very many fields, or a field folded over very many lines, held a thread that serves delivery and SMTP for tens of seconds; every version from 6.0.0 up to 6.3.5. CVE-2026-107575 (work item #74) - expanding a macro in a sender's own SPF record into a name too long to look up took time growing with the square of its length, so the SPF check - the SPF test's, or the DMARC test's, which is on by default - took several seconds; 6.3.4 and 6.3.5. CVE-2026-107574 (work item #76) - the JSON reader took time growing with the square of an object's members. It reads the REST API's bodies and, where domain reports are on, a TLS report of up to 16 MB that anyone may mail in, on the delivery queue: a 4 MB report took about 300 seconds, and a few such reports stopped all delivery for over an hour each; 6.3.2 to 6.3.5 (the REST API's bodies from 6.3.2, the reports from 6.3.4). CVE-2026-107572 (work item #75) - an account holder's own Sieve filter could hold the delivery threads every account shares: a :matches test backtracked over every length of every *, and deleteheader erased its fields one at a time; 6.2.22 to 6.3.5. CVE-2026-103649 (work item #53) - on Linux, some network waits had no timeout - the MTA-STS policy fetch, the DANE TLSA query, the OAuth2 token request, the ACME client and its http-01 responder, and the ManageSieve and metrics listeners, and in 6.3.0 to 6.3.2 the server's own DNS resolver; 6.3.0 to 6.3.5. The same release fixes or hardens more, without a CVE request: more code that took time out of proportion to its input, in the calendar and IMAP LIST among others; calendar messages delivered by mail now change a stored event only from a sender the server verified (6.3.4 and 6.3.5 did not check); the Control Deck and the webmail are served under stricter page policies; attachments, files sent as links and contact photos keep their declared type only when no browser runs or renders it; ManageSieve serves its connections side by side and holds each to deadlines; a mailbox key and the stored-secrets key no longer stay whole in memory freed without being written over, as they could in 6.3.4 and 6.3.5; and more, listed in the release notes. The Windows installers carry OpenSSL 4.0.3 (CVE-2026-84783 and others) and libpq from PostgreSQL 18.6, and event scripts on Linux run in QuickJS 2026-06-04 (CVE-2025-62494, CVE-2025-62493, CVE-2025-62492). Upgrade every server running 6.3.5 or earlier. Upgrading By the live update, from 6.3.4 or 6.3.5. Every file of 6.3.6 is signed as this release's pipeline at the project's group path, https://gitlab.com/hmailserver/hmailserver//.gitlab-ci.yml@refs/tags/v6.3.6, with the project's id, 86730559, in the certificate. Every server from 6.3.4 on has that signer compiled in, so one installed from the x64 installer or the release's x86-64 .deb or .rpm is built to install 6.3.6 by its live update: from 6.3.5 with 6.3.5's own file as its rollback image, and from 6.3.4, which carries no Sigstore bundle, without one. No live update had installed a release signed at the group's path when this was written. On the Control Panel's Status page: Check for updates, Download update, Install update. The update is applied by the version already installed, so what 6.3.4 and 6.3.5 do still holds for it. On Windows, a service running as NT SERVICE\hMailServer downloads and verifies the installer, then leaves the install to an administrator (HM7463) - download hMailServer-6.3.6-x64.exe from the release, check that Get-AuthenticodeSignature says Valid for CN=Progressive Robot Ltd, and run it as an administrator, keeping the service account on the installer's Service account page. From 6.3.6 on, Install update in the Control Panel on the server installs the next release with one Windows prompt. On Linux, install 6.3.6 with your package manager rather than by the live update. The live update to 6.3.6 is applied by the apply step of the version it replaces, the one fixed above. Use apt, dnf or zypper, or turn the apply off until then with systemctl disable --now hmailserver-update.path (the check and the download go on). From 6.3.6 its own helper applies the next release. A backup destination inside /var/lib/hmailserver fails the backup an unattended apply takes on 6.3.4 and 6.3.5 (HM5014). On Fedora and Enterprise Linux, every .rpm requires glibc's extra character-set converters, glibc-gconv-extra, which dnf installs with it; a server without them whose dnf has no repository enabled (an unregistered RHEL, say) has the update refused until glibc-gconv-extra is installed. On openSUSE they are recommended (glibc-gconv-modules-extra), and zypper installs them unless solver.onlyRequires is on. Without them the server converts no text in BIG5, ISO-2022-JP, Windows-1251 or ISO-8859-2, and from 6.3.6 it says so at its start. By hand, from 6.2.28 to 6.3.3, and from 6.3.3's AArch64 packages or AppImages. On Windows run hMailServer-6.3.6-x64.exe over the existing installation; on Linux install the package with apt or dnf, or replace the AppImage with 6.3.6's. Until then keep UpdateAutoDownload off on such a server, or it downloads the installer at every check and refuses it. A server from one of 6.3.6's per-distribution packages is offered the next release's generic package by its live update, which apt refuses on Debian 12 and dnf on Enterprise Linux 9. There, upgrade with the package manager and your release's own package of the next version. What else changes on upgrade The 6.3.6 section of CHANGELOG.md lists everything under "What changes on upgrade without a switch". The rest, in short: Spam checks: the MX-record test and the SURBL test cut host names by the Public Suffix List compiled into the server, so a sender at a public suffix itself with no MX (github.io, co.uk) now fails the MX test; the SURBL test reads a link's host as a browser does; tlds.txt is removed. Backups leave out the live update's Updates folder, and a destination inside the data directory works; up to 6.3.5 it failed every backup. A message deleted while a backup copies the data directory no longer fails the backup. A full restore of messages, which failed on every installation made new with 6.3.4 or 6.3.5 and left the mail in the temp folder alone, copies the archive's store in first and removes what the archive does not hold only after. It keeps this installation's own self-signed certificate and key when the archive's key is not this installation's, so a mail client that accepted the other server's certificate asks again. If a restore failed this way, the archive is as it was: upgrade, restore again from it, then delete the staged copy its HM5014 line names. On Linux a full restore from 6.3.0 on also deleted the key the stored secrets are sealed under, which no archive carries (from 6.3.4 the server then says HM7495 at each start) - import the stored-secrets key from its export (Backup.md, "How a restore replaces the data directory"). More web requests go through HttpProxy when it is set: the OAuth2 token, the MTA-STS policy, ACME, brand logos, Web Key Directory, Web Push, one-click unsubscribe and an inbound webhook route held to public addresses. On Windows a listener whose port another program holds now fails to bind and says so. Calendars: a cancellation of some occurrences of a series takes those out of a guest's calendar and leaves the rest. The live update keeps its outcome after the start, tries a failed scheduled check again once ten minutes later, and keeps a verified installer across a restart instead of fetching it again. What the auto-ban put below the server (the Linux firewall hook, AutoBanCommand) is lifted after a stop too, and Windows' uninstaller removes the auto-ban's firewall rules. On Linux the backup and event logs are UTF-8, converted in place the first time; Windows keeps UTF-16LE. The .deb and .rpm require ca-certificates. Sign-in limits: an account, or the administrator, holds at most fifty REST browser sessions, and a pending single sign-on or passkey sign-in is carried by the browser, sealed, so no number of starts can push anybody's out. Text in the Control Panel can be selected and copied on every page and dialogue (work item 48); where the keyboard is shows in one violet in the Control Panel, the Deck and the webmail. An error reported where the settings hold no scripting switch (UseScriptServer - a database without that row) is logged and fires no event; in every version up to 6.3.5 it reported the missing setting again and again until the reporting thread's stack ran out, taking the service with it. Scripting off runs no script engine. The project's address is gitlab.com/hmailserver/hmailserver in the Control Panel's links, security.txt, the systemd units and the packages. Also new A never-ban list by reverse name (work item #56) - spare a provider whose outbound hosts share a reverse name from every auto-ban, checked forward and back. Port 25 takes no password: each SMTP listener's AUTH is On, Off or Honeypot (work item #54). An auto-ban can be widened to its reverse-name neighbours in the same /24, off by default (work item #55). A HELO name or a sender's domain that is not a name can be refused, two settings, off by default (work item #51). One-click install of an update from the Control Panel on a Windows server whose service is not an administrator. What the release carries 62 files, each with its Sigstore bundle, <file>.cosign.bundle, beside it: Windows: hMailServer-6.3.6-x64.exe, and hMailServer-6.3.6-arm64.exe for Windows on ARM, both Authenticode-signed. The ARM64 installer is a preview: it is cross-compiled on the project's x64 Windows runner and has never run on ARM64 hardware. SQL Server Compact has no ARM64 build, so a new ARM64 installation is set up against SQL Server or PostgreSQL, and a silent one is given its database on the command line (WindowsOnArm.md). If you install it on an ARM64 machine, please say how it went, working or not, in an issue - the project has no ARM64 machine of its own. Linux, x86-64 and AArch64: the generic hmailserver_6.3.6_amd64.deb, hmailserver_6.3.6_arm64.deb, hmailserver-6.3.6-1.x86_64.rpm and hmailserver-6.3.6-1.aarch64.rpm, the names an installed server's update check asks for; and each Debian 12 and 13, Ubuntu 24.04, Fedora 44 and 43, Enterprise Linux 10 and 9, openSUSE Leap 16.0 and Tumbleweed release's own package for both architectures. The AArch64 files are cross-compiled and tested under emulation: no AArch64 machine has run them. The AppImages, hMailServer-6.3.6-x86_64.AppImage and hMailServer-6.3.6-aarch64.AppImage, with the project's own AppImage runtime, and beside them the source of the libraries they carry and of those their runtime links, as their licences ask. The source tarball, hmailserver-6.3.6.tar.gz, reproducible and with nothing non-free in it; the Helm chart, hmailserver-chart-6.3.6.tgz; and the PowerShell module, HmailServer.psd1 and HmailServer.psm1, Authenticode-signed. An SBOM for each compiled file and the tarball, <file>.cdx.json, and the source tree's two; hmailserver-linux-6.3.6-SHA256SUMS.txt, the SHA-256 of every Linux file; and hmailserver-6.3.6.intoto.sigstore.json, SLSA v1 build provenance that names every file by its SHA-256. The container image, the first at GitLab's registry: registry.gitlab.com/hmailserver/hmailserver:6.3.6, also tagged 6.3 and latest, one index over linux/amd64 and linux/arm64, each built from this release's .deb of its architecture and signed with cosign as this release's pipeline, with its SBOM attested to it. In the container the update check names the image to pull and installs nothing (ContainerImage.md). Also in the tree, not yet published anywhere: a Nix flake of the project's own with a NixOS module, an APKBUILD and an OpenRC service for Alpine Linux, on whose musl the server now builds and runs, and a snap. They are built without the live update and name the command that updates them; none is in a distribution's archive or store yet. https://updates.progressiverobot.com/download/ leads to the newest stable release's page, and /download/hMailServer-x64.exe, /download/hMailServer-arm64.exe, /download/hmailserver_amd64.deb, /download/hmailserver_arm64.deb, /download/hmailserver.x86_64.rpm, /download/hmailserver.aarch64.rpm, /download/hMailServer-x86_64.AppImage and /download/hMailServer-aarch64.AppImage to that file of it; add .cosign.bundle for its bundle. How to check a download With cosign and OpenSSL, with the file and its bundle beside you: cosign verify-blob --bundle hMailServer-6.3.6-x64.exe.cosign.bundle \ --certificate-identity 'https://gitlab.com/hmailserver/hmailserver//.gitlab-ci.yml@refs/tags/v6.3.6' \ --certificate-oidc-issuer https://gitlab.com hMailServer-6.3.6-x64.exe jq -r .verificationMaterial.certificate.rawBytes hMailServer-6.3.6-x64.exe.cosign.bundle \ | base64 -d | openssl x509 -inform DER -noout -text | grep -A1 1.3.6.1.4.1.57264.1.15 The first must say Verified OK, and the second print ..86730559, the project's own id, which cosign does not check: the identity is a path, and a path could pass to another project were the group or the project renamed. 6.3.5, signed before the move, names https://gitlab.com/Progressiverobot/hmailserver//.gitlab-ci.yml@refs/tags/v6.3.5 instead. If your cosign has the --new-bundle-format option and does not use it by default, add it. Each file but a compiled file's SBOM also has its bundle a second time as <file>.sigstore.json; either name verifies the same way. To check that a file is one this release's pipeline built, against its build provenance: cosign verify-blob-attestation --new-bundle-format \ --bundle hmailserver-6.3.6.intoto.sigstore.json --type slsaprovenance1 \ --certificate-identity 'https://gitlab.com/hmailserver/hmailserver//.gitlab-ci.yml@refs/tags/v6.3.6' \ --certificate-oidc-issuer https://gitlab.com hmailserver_6.3.6_amd64.deb It says Verified OK only when the file's SHA-256 is one the provenance names. Then: sha256sum -c --ignore-missing hmailserver-linux-6.3.6-SHA256SUMS.txt must say OK for each Linux file you downloaded. On Windows, Get-AuthenticodeSignature on either installer must answer Valid, with a signer certificate for CN=Progressive Robot Ltd. The x64 installer's published SHA-256 is 2f673913e5f127d688090e98f6001ca567cd45560d452aebc74421b5533cfe53. In a clone, git -c gpg.ssh.allowedSignersFile=.gitlab/allowed_signers verify-tag v6.3.6 must report a good signature by the one key that file lists. For the image, ContainerImage.md, "Checking an image", gives the cosign verify command and the check of the project's id. The release's own notes, under Verification, give each file's SHA-256 and the certificate the pipeline was given. SECURITY.md, "Verifying a release", "Build provenance" and "Software bills of materials", has the rest. How it was checked The tag's pipeline (https://gitlab.com/hmailserver/hmailserver/-/pipelines/2914690858) ran every gate before anything was published: the Windows suite on the project's Windows runner, 4,110 tests, 4,031 passed, 0 failed, 78 skipped and 1 inconclusive; the Linux suite on the project's Linux runner, 2,409 tests, 2,066 passed, 0 failed, 343 skipped; the rebuild of the x86-64 .deb and .rpm byte for byte; the install and start of every Linux package in a clean container of its release, the AArch64 ones under emulation; the static analysis, Semgrep and the dependency scan. It then checked every signature with the server's own verifier. The signed installer went through the installer smoke test: an upgrade from 6.3.5 passed 93 checks and a new installation 59, none failed. Known issues The ARM64 installer has never run on ARM64 hardware (above), and no ARM64 Windows server has a live update to 6.3.6, since no release before it carried the ARM64 installer; from 6.3.6 one finds its own installer by name, and an x64 server is never offered it. No channel publishes it yet. The apt and dnf repositories, Docker Hub, Quay.io, the AUR, the Ubuntu PPA, Copr, the openSUSE Build Service, Chocolatey, the Snap Store, PyPI and the PowerShell Gallery publish from master once their keys and accounts exist; none is live as this release is made (Channels.md). An AppImage on Fedora, Enterprise Linux or openSUSE runs on the host's glibc and cannot require its extra character-set converters: install glibc-gconv-extra (Fedora, EL) or glibc-gconv-modules-extra (openSUSE) by hand; the server says when they are missing. Releases pause after this one 6.3.6 is the last release for the time being, by the owner's decision of 28 September 2026, and no feature release follows it until the maintainers say otherwise in SECURITY.md, "While releases pause". A confirmed vulnerability still ships, in a release of its own that is 6.3.6 with fixes - 6.3.7, 6.3.8 and so on: a critical or high one as soon as its fix has passed the gates, and none held past 90 days. Other defects are fixed on master and ship with the next security release, or when releases resume. The 6.3 line receives security fixes until 30 September 2027; before that date the maintainers write in SECURITY.md whether support continues, and if nothing is written, it ends then. There are still no maintenance branches: an installation of 6.3.5, 6.2 or anything older is upgraded in place to the newest release. To hear of a security release, switch the server's update check on (UpdateCheckEnabled), or follow the releases' feed. Reporting a vulnerability Not in a thread here, and not in a public issue. Report it privately, as SECURITY.md says: a confidential issue on GitLab - check that it is still marked confidential before you submit it, and choose the Security template - or an email to contact-project+hmailserver-hmailserver-86730559-issue-@incoming.gitlab.com, which GitLab files as a confidential issue, so no GitLab account is needed. Release notes, checksums and downloads · Newest stable release
  • 0 Votes
    1 Posts
    89 Views
    chrisC
    This release is superseded by hMailServer 6.3.6, published on 6 October 2026, which fixes security flaws in every earlier version, 6.3.5 included. Read its announcement before upgrading. hMailServer 6.3.5 was published on 29 September 2026. It is the first release the project's GitLab CI made from its signed tag. The tag's pipeline built the Windows installer after the whole Windows suite had passed, signed it with Authenticode, signed every file of the release with Sigstore, and checked every signature with the server's own verifier before anything was published. A 6.3.4 server trusts that signer, and a 6.3.4 server has installed 6.3.5 by its own live update. The release also fixes two defects 6.3.4 has, and lets an administrator review the quarantine by the sender's domain and the HELO name. The schema moves from 6104 to 6105 in one step. Who is affected Every server on 6.3.4. Two of its defects are fixed: A mail flow rule with a Readable text or Sensitive content found condition, which 6.3.4 added, read a message's HTML part in time that could rise with the square of its size. 400,000 &amp; - two million characters - held the delivery for about a minute, and on a server with such a rule anybody could send one. Each entity is now unwrapped in one pass, with the same result. A server with no such rule was not affected. An invitation whose calendar part carries parameters in its Content-Type and has no .ics name - the shape Outlook sends - was never marked as an invitation, so the webmail showed no card for it. It now shows the card with Accept, Maybe and Decline. A server older than 6.3.4 gets everything 6.3.4 brought as well, including the fix for CVE-2026-100741. Read the 6.3.4 announcement first: all of its "Before you upgrade" applies. Before you upgrade Back up the database and the data directory first. The schema goes from 6104 to 6105 in one one-way step, which adds a column to the quarantine's table, hm_quarantine.quarantinehelo. From 6.3.3 or older the upgrade runs 6.3.4's steps first, and 6.3.4's notes apply: on MySQL and MariaDB, step 6102 rebuilds every table as utf8mb4, and at the first start the [Settings] section of hMailServer.ini moves into the database. A server from 6.2.28 to 6.3.3 cannot install 6.3.5 by its live update. It trusts only the GitHub workflow that signed the releases up to 6.3.3, and it refuses a file the GitLab pipeline signed: "The certificate names https://gitlab.com/Progressiverobot/hmailserver//.gitlab-ci.yml@refs/tags/v..., not this repository's release workflow." Nothing is installed and the server runs on. What to do: Install 6.3.4 or 6.3.5 by hand, once. On Windows run hMailServer-6.3.5-x64.exe over the existing installation; on Linux install the .deb or the .rpm with apt or dnf. After that the server reads the new feed without being told and trusts both signers. Until then, keep UpdateAutoDownload off. With it on, such a server downloads the whole installer, 84 MB, at every check and refuses it every time, since the verification comes after the download. Its update check works only once it is pointed at the new feed: under [Settings] in hMailServer.ini, UpdateFeedUrl=https://updates.progressiverobot.com/releases.json, then restart the service. On Windows only, there is a second way: three settings that make the old server trust the GitLab signer. They cost the rollback image, must be cleared afterwards in the Control Panel or over REST (not in the file), and must never name the group path hmailserver/hmailserver. UpdateFeed.md, "Having the live update make that upgrade anyway", gives the values and the costs. Installing by hand is simpler. The live update from 6.3.4 A 6.3.4 server reads https://updates.progressiverobot.com without being told, and trusts the identity the GitLab pipeline signs with; that verifier is unchanged in 6.3.5. On the Control Panel's Status page: Check for updates, Download update, then Install update; or POST /api/v1/update/install over REST, or Status.InstallUpdate over COM. With UpdateAutoDownload on and an update window open, the server does it by itself. It was tried on 29 September 2026 on a clean Windows Server 2025, against the published release and the live feed: A service running as LocalSystem found 6.3.5, downloaded and verified it, took its backup and installed it. The schema went from 6104 to 6105, mail was unavailable for about 24 seconds, and the domain, the account, its messages and the settings were intact. A server upgraded to 6.3.4 that kept LocalSystem runs this way. A service running as NT SERVICE\hMailServer does not install it itself. That is how a new 6.3.4 installation runs the service. Such a server found, downloaded and verified 6.3.5, then logged HM7463 and left the install to an administrator. Download the installer from the release, check that Get-AuthenticodeSignature says Valid, and run it as an administrator - not the copy the server downloaded, which is in a folder the service can write. There is no rollback image for 6.3.4. Before it installs, the server fetches and verifies the installer of the version it runs, to put it back if the new one does not start. 6.3.4 carries no Sigstore bundle, so that copy cannot be verified, and the application log says "Update: no rollback image for hMailServer 6.3.4 could be had (release 6.3.4 carries no x64 installer with a Sigstore bundle.); if 6.3.5 does not start, it will need a manual reinstall." Take the backup first. On Linux the live update was not part of that test. A server installed from the release's own amd64 .deb or x86-64 .rpm is offered the same file of 6.3.5. A server installed from a .deb or .rpm the CI builds for another distribution release is offered the release's file too, whose dependencies its distribution may not meet; apt or dnf then refuses it and the server stays as it was. One installed from an AArch64 package, an AppImage or the ARM64 installer is told that the release carries nothing for it to download. The test found three defects in code 6.3.4 and 6.3.5 share, to be fixed in 6.3.6: the outcome of an update is dropped by the check that runs one second after start-up, so Status.UpdateApplyOutcome is empty though the log has it; a first check on a new machine can fail with "The response carried no HTTP headers", which hides the real cause (on the test machine, probably a slow first certificate revocation check); and a service that cannot install downloads the installer again after every restart. What changes on upgrade without a switch The 6.3.5 section of CHANGELOG.md lists five. The Control Panel's sidebar is in a new order. Welcome and Dashboard come first, then a new group, Set up this server: the Setup checklist (it was External setup, under Monitoring & troubleshooting), Setting profiles and DNS records. TLS & certificates is now Certificates & encryption, and Access & abuse protection is Sign-in & access. Connections & protocols has a new Webmail page. Ctrl+K still finds the checklist as External setup. A page's nav-<key> UI Automation id is unchanged; the navgroup- and navrail- ids of the new and renamed groups follow their new headings, so a UI test that finds a group by its old id must be changed. The quarantine shows the HELO name, and groups. The Control Panel's Quarantine page and the Control Deck's Quarantine view have a HELO column and group held messages by the sender's domain unless told otherwise. Messages held before the upgrade have no HELO name recorded. The Deck now asks before a delete with a Yes or No of its own whose default is No; it used the browser's confirm(), whose default is OK. It offers whitelist and blacklist only to a credential holding manage_settings, and a held message's headers and preview only to one holding view_mailbox, as the server requires. The webmail's keyboard. The message list is one Tab stop, and its keys reach what a row holds. Enter on a button in the mail view presses the button; it used to open the message under the list's cursor. A letter typed into the formatted signature is text, where c opened a new message. Escape closes every modal dialogue as its Cancel does. The compose window's Plain is Remove formatting. Invitations. GET /api/v1/me/messages/{id} answers "invitation": true for the Outlook-shaped invitation above, where it answered false. What a script sees. GET /api/v1/quarantine gives each message three more fields - helo, sender_domain and helo_domain - and four routes are new. Over COM, Quarantine gains Bulk and QuarantinedMessage gains HeloName, SenderDomain, HeloDomain, ReadHeaders and ReadPreview, added to the existing interfaces; no existing member or dispid changes. A program built against 6.3.5's type library that calls one of them needs a 6.3.5 server. Also in this release Reviewing the quarantine: grouping by the sender's domain, by the HELO name's domain or not at all; release, delete, whitelist then release, and blacklist then delete, on one message, a group or the checked ones, with a dry run first; and a held message's headers and a plain-text preview read without releasing it. The Control Deck does the same as the Control Panel (SenderListsAndDigests.md). Every database setting that should have an editor has one in the Control Panel: 137 that needed one and had none, on new tabs and on the new Webmail page. 103 of them are reached over the REST API alone, which is off as shipped (RestApiPort 0); with it off, each such tab says so. The Control Panel and the webmail, audited in both themes, by keyboard and, for the webmail, with NVDA in English and German: contrast raised where it fell short, focus rings restored, and 82 texts that were English on a translated webmail page are now in all 23 languages. The packages for Debian, Ubuntu, Fedora, RHEL and openSUSE, for x86-64 and AArch64, the AppImages and both Windows installers are built on GitLab CI, on the project's own runners. Only the files listed below are in the release; the others are CI job artifacts, kept for 7 to 30 days, not signed and not offered by the update feed. Reproducible Linux packages: the server, the .deb and the .rpm are built a second time from a copy of the tree at another path, and the release waits until all three are byte for byte the same. How it was checked Every gate of the tag's pipeline passed: the Windows suite on the project's Windows runner, 3,828 tests, 3,753 passed, 0 failed, 74 skipped and 1 inconclusive; the Linux suite on the project's Linux runner, 2,213 tests, 1,892 passed, 0 failed, 321 skipped; linux-repro, which rebuilt the .deb and the .rpm byte for byte; the static analysis, Semgrep and the dependency scan. The signed installer then went through the installer smoke test on a clean Windows Server 2025: an upgrade from 6.3.4 passed 93 checks and a new installation 59, none failed. No ARM64 Windows installer in this release The pipeline has a job that builds the ARM64 installer, and it may fail without failing the pipeline. In 6.3.5 it fails: its toolchain check looks for Visual Studio's ARM64 C++ runtime by a path that finds it in no installation, so it stops on the Windows runner, which has the ARM64 tools, ATL and SDK libraries, and prints a command that changes nothing. The check is corrected for the next release. Even then, the ARM64 installer is a CI job artifact only, not signed and not in the release, and the ARM64 build has never been run on ARM64 hardware (WindowsOnArm.md). What 6.3.5 does not carry No ARM64 Windows installer (above). No AArch64 .deb or .rpm, no AppImage and no SLSA provenance. The pipeline builds the AArch64 packages and the AppImages as CI job artifacts only, and the provenance generator the releases up to 6.3.3 used is GitHub's. 6.3.3's AArch64 packages and AppImages are still the newest published. 6.3.6 is to carry all three. No container image. registry.gitlab.com/progressiverobot/hmailserver is empty: the pipeline pushes and signs the image only once the project has moved to the group hmailserver and the owner has switched it on. No apt or dnf repository yet: the job that makes them publishes nothing until the repository's signing key exists (PackageManagers.md). Not in winget or Chocolatey yet: the first submission to each is made by hand, and has not been made. Downloads, and how to check them The release carries nine files, each with its Sigstore bundle, <file>.cosign.bundle, beside it: hMailServer-6.3.5-x64.exe, the Windows x64 installer; hmailserver_6.3.5_amd64.deb for Ubuntu 26.04 and 24.04, and hmailserver-6.3.5-1.x86_64.rpm for Fedora 43 and 44 and Enterprise Linux 10. Their dependencies are 6.3.4's: the .deb asks for glibc 2.38 or later, so it does not install on Debian 12, and the .rpm does not install on Enterprise Linux 9. An SBOM for each of those three, <file>.cdx.json (CycloneDX 1.6), whose subject is that file by its SHA-256 and SHA-512; and the source tree's SBOMs, hmailserver.spdx.json and hmailserver.cyclonedx.json. hmailserver-linux-6.3.5-SHA256SUMS.txt, the SHA-256 of the two Linux packages. There is no SSH-signed sums file this time: the pipeline cannot hold the key that signs the release tags, so every file is signed with Sigstore instead. https://updates.progressiverobot.com/download/hMailServer-x64.exe, /download/hmailserver_amd64.deb and /download/hmailserver.x86_64.rpm always lead to that file of the newest stable release, never a pre-release's; add .cosign.bundle for its bundle. https://updates.progressiverobot.com/download/ leads to that release's page. To check a download with cosign, with the file and its bundle beside you: cosign verify-blob --bundle hMailServer-6.3.5-x64.exe.cosign.bundle \ --certificate-identity 'https://gitlab.com/Progressiverobot/hmailserver//.gitlab-ci.yml@refs/tags/v6.3.5' \ --certificate-oidc-issuer https://gitlab.com hMailServer-6.3.5-x64.exe It must succeed (exit status 0). The identity names the tag, so a file signed for another release does not pass. If your cosign has the --new-bundle-format option and does not use it by default, add it: the release's own signing job passes it wherever cosign offers it. On Linux, a server from 6.3.4 on makes the same check its live update makes: hmailserver --verify-update hmailserver_6.3.5_amd64.deb with hmailserver_6.3.5_amd64.deb.cosign.bundle beside the file. It takes any file of the release; the release's pipeline ran it on every one before publishing. It answers hmailserver_6.3.5_amd64.deb verifies: signed by https://gitlab.com/Progressiverobot/hmailserver//.gitlab-ci.yml@refs/tags/v6.3.5, recorded by the transparency log at ... and exits 0, and exits 1 for a file that does not verify. It does not check which release the file belongs to, so read the identity it prints: it must end in @refs/tags/v6.3.5. Then: sha256sum -c hmailserver-linux-6.3.5-SHA256SUMS.txt must say OK for each package (add --ignore-missing when you downloaded only one). On Windows, Get-AuthenticodeSignature .\hMailServer-6.3.5-x64.exe must answer Valid, with a signer certificate for CN=Progressive Robot Ltd. In a clone, git -c gpg.ssh.allowedSignersFile=.gitlab/allowed_signers verify-tag v6.3.5 must say Good "git" signature for [email protected]. The release's own notes, under Verification, give each file's SHA-256 and the certificate the pipeline was given. SECURITY.md, "Verifying a release" and "Software bills of materials", has the rest, including how to check that an SBOM describes your file. Reporting a vulnerability Not in a thread here, and not in a public issue. Report it privately, as SECURITY.md says: a confidential issue on GitLab - check that it is still marked confidential before you submit it, and choose the Security template - or an email to contact-project+hmailserver-hmailserver-86730559-issue-@incoming.gitlab.com, which GitLab files as a confidential issue, so no GitLab account is needed. Release notes, checksums and downloads · Newest stable release
  • 0 Votes
    1 Posts
    178 Views
    ProgressiverobotP
    Every documentation topic on this forum, by the category it sits in. Each one is written from the project's wiki and the operator documents at the release tag, for hMailServer 6.3.4, and links the pages it was written from. The long tail is in the wiki and the operator documents; the public documentation is the same material as one long page. A question that none of these answers belongs in the category it is about, as a new topic; Read this before you post says what a support request needs. Running hMailServer General support Read this before you post: what a support request needs Where to post what: a map of the forum categories Diagnosing stalled mail: a sender that times out, and a message that never leaves Reading the logs: levels, files, the Logs page, following a connection, and fail2ban Backup and restore: what is in the archive, what is not, and restoring one mailbox Settings: what is not stock, when a save takes effect, and the three levels The Control Panel on Windows: every page, what it writes, and what applies when IMAP and POP3 on the server: capabilities, switches, shared mailboxes, search limits Troubleshooting by symptom: no mail in, no mail out, a service that will not start Every port hMailServer listens on or connects to, and the firewall rules to match Installation, migration & upgrades Upgrading to 6.3.4: any 5.3 or later goes straight there Moving to new hardware, and why Windows to Linux is a rebuild Installing on Windows: wizard pages, service account, profile and the first hour Choosing a database, creating it, encrypting the link and leaving the built-in one Your first domain and mailbox, and the DNS records your domain needs Bringing mailboxes in: IMAP mirror, Google, Microsoft 365, mbox, Maildir and PST Unattended installs, winget and Chocolatey, and configuration from the environment Warm standby and high availability: what a second machine can and cannot do Message storage: compression, de-duplication, object storage and encryption at rest The update check and the live update: feed, verification, window and rollback Upgrading from 5.x, 6.2 or 6.3: the 24 changes without a switch, and rolling back Linux hMailServer on Linux: what you get and what you do not First install on Linux: the order of operations, Ubuntu to Enterprise Linux The hmailserver command line on Linux: every verb, who runs it, the files, the unit In a container or on Kubernetes: the image, Compose, Helm and the environment Databases on Linux: PostgreSQL or MariaDB, the client, TLS and the upgrade floor Webmail, portal & Control Deck The webmail portal at /portal The Control Deck: browser administration at / Calendars, contacts, the directory and shared address books over CalDAV and CardDAV Signing in: passwords, second factor, passkeys, single sign-on, recovery, devices The webmail in depth: search, offline mail, expiring messages, send-as and recovery Connecting a mail client: IMAP, POP3, SMTP submission, app passwords and the ports TLS, certificates & deliverability Let's Encrypt certificates are automatic: the built-in ACME v2 client Being trusted by the servers you send to TLS on the listeners: your own certificate, versions, ciphers and client certificates Requiring TLS: MTA-STS and TLS-RPT, DANE with ACME, and remote domain policies DKIM rotation, DMARC and TLS reports, complaints, bounces and the reputation watch Outbound shaping and IP warm-up: one slow destination must not stall the queue End-to-end encryption: S/MIME authority, OpenPGP and WKD, zero access, sealed mail Anti-spam & message filtering The anti-spam stack end to end: score, greylisting, SPF, DKIM, DMARC, DNSBL, SURBL Allow and block lists, digests, spam traps, country blocking and impersonation Viruses and dangerous attachments: ClamAV, custom scanners, the sandbox, sealed mail Mail flow rules, the outbound content policy and what a domain does to a message Guides & tutorials Quotas, retention and the archive: how much mail the server keeps, and for how long Administrator roles end to end: the 27 permissions, giving a role, where each works How the service is built and what runs out first: pools, connections, large folders IP ranges, auto-ban, lockout, tarpits, sending limits, send-as and password storage Behind a gateway, load balancer or reverse proxy: relays, PROXY, XCLIENT and HTTPS Hardening a public mail server: secure defaults, checklist, sealed secrets and roles Compliance: eDiscovery, legal holds, personal data, erasure and the audit trail How email works, and what hMailServer is and is not Relocating an installation to another drive, and moving or renaming a mailbox Automation & integration Scripting & the COM API Event scripts: every event and its arguments, the limits, and the 6.3.4 security fix The COM API on Windows: object model, Save() since 6.3.4, roles and the Import Tool REST API & integrations The REST API: turning it on, credentials, roles, one-time codes, OpenAPI and examples Active Directory and LDAP sign-in, directory synchronisation and OAuth2 bearer tokens hmctl and the PowerShell module: shell administration and accounts from a CSV file Integrating applications: the send API, mail in to a webhook, and outbound webhooks SCIM provisioning from Entra ID and Okta, and single sign-on for the organisation Rules, Sieve & routing Rules and Sieve: what runs when, vacation replies and the Run function action Routes, relays and smart hosts, OAuth2 relaying, restricted delivery and recall Aliases, distribution lists, membership queries, mailing lists and public folders Journaling and dual delivery: a compliance copy, and what a journal rule does not see Monitoring, metrics & logs Monitoring and health: Prometheus metrics, health probes, Grafana, OTLP and SNMPv3 Alerts, the audit trail, syslog and the journal, and per-domain reports Releases The current release, its announcement and the release history are in Announcements & releases.
  • 0 Votes
    1 Posts
    242 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) moves the Control Panel's Audit trail page, with journaling and eDiscovery, into a new Compliance group, and the alerts' settings, the outbound webhooks and the SNMP agent onto its Monitoring & alerts page (API & monitoring until 6.3.6; 6.3.5 and 6.3.6 had them on Logging); Ctrl+K takes an old title or route to its page. It also fixes the SMTP log, whose lines the syslog sink copies, which wrote some credentials in the clear with SMTP logging on, in every version from 6.0.0 to 6.3.6 (work item #95). See the 6.3.7 announcement. This topic covers four things 6.3.4 adds for being told what the server is doing and who changed it: the alerts, the audit trail, the syslog sink (or the systemd journal on Linux) and the per-domain reports, with a paragraph on following one connection live. The one thing to know first: after the upgrade to 6.3.4 the audit trail and the alerts are on, with seven of the eleven conditions enabled, but nothing is mailed until AlertRecipient and AlertSenderAddress are set. What 6.3.4 switches on without a switch From the CHANGELOG's list of what changes on upgrade: the audit trail is on (schema 6043), and keeps every row until a retention is set. Compromised-account detection starts in watch mode (schema 6071): it raises account.compromised but holds nothing until compromise_detection_mode is set to enforce. The reputation watch, which raises reputation.listed, is on in a new installation and off after an upgrade. Alerts A condition is a row in hm_alertrules (on or off, threshold, mail or webhook or both, cool-down, at once or in the digest); an event is a row in hm_alertevents. Delivery is AlertTask, once a minute, and nothing else sends, so an alert about the delivery queue is never itself submitted into the stuck queue. Condition On as shipped Raised by Threshold backup.failed yes A backup that failed, and a scheduled backup that could not run. Sent at once — certificate.expiring yes The notAfter of every configured certificate, read once a minute 7 days disk.low no The free-space warning band, and again below the floor; cleared on recovery — queue.stalled no A work queue whose every thread has been in the same task past AsyncQueueStallThreshold — autoban.storm no Auto-bans in the last hour 25 minidump.written no A crash dump newer than the last one recorded, in the log directory — complaints.rate.high yes A hosted domain's complaint rate over the feedback loops' seven-day window; inert while complaint processing is off, and a domain that sent fewer than 100 messages is not judged 1,000 per million dkim.rotation.stalled yes A rotation whose next key's record has not been seen for the threshold, or a running rotation whose last move failed 7 days account.compromised yes Compromised-account detection at submission, one event per account per hour; in watch mode the detail says what enforcing would have done — account.password-reset yes A password reset completed through an account's confirmed recovery address; no cool-down and no digest — reputation.listed yes The reputation watch's stored rows; sent at once, and a check that was refused or timed out does not clear it — The four that are off are noisy on a healthy server. The settings. All nine are rows of hm_settings, none an hMailServer.ini key. They are in the server group of GET/PUT /api/v1/settings, on the Deck's Settings page, and on Windows over COM through Settings. The CHANGELOG and AlertsAndAuditTrail.md spell some in their hm_settings form (AlertRecipient, AlertSenderAddress and so on); the REST keys are these. REST key Default Meaning alerts_enabled on The master switch alert_recipient empty Where alerts and the digest go; a rule can name an address of its own instead alert_sender_address empty What they are sent from; a rule with no sender sends nothing alert_digest_enabled on Whether rules marked for the digest wait for it alert_digest_hour 7 The hour, UTC, the digest goes out; a digest with nothing in it is not sent alert_max_per_hour 20 Past this many single alerts in an hour, everything goes into the digest alert_webhook_max_attempts 5 Webhook attempts before an event is dead-lettered (1 to 20) audit_trail_enabled on Whether administrative changes are recorded audit_retention_days 0 Days of trail to keep; 0 keeps everything Nothing may flap. A state condition already raised is not raised again; an occurrence inside the rule's cool-down is recorded but held for the digest; and the digest is one message listing what fired. Webhooks. A rule whose actions is 2 (webhook) or 3 (mail and webhook) POSTs one JSON object per event (condition, state, severity, time, time_utc, summary, detail, server) with three headers: X-hMailServer-Timestamp, X-hMailServer-Signature: sha256=<hex> and X-hMailServer-Event. It is signed under the rule's shared secret, as every webhook the server sends is; the topic on integrating applications shows how to verify it. A rule that calls a webhook is refused without an address and without a secret; webhook_secret is write-only and the listing says only webhook_secret_set. Only https, or plain http to a loopback address, is used. An attempt that does not get a 2xx is retried on the next pass, then after 1, 5 and 15 minutes, which is the default five attempts, and 60 minutes apart after that if the maximum is raised. After the last attempt the event is dead-lettered: kept, no longer retried, and reported once in the error log as HM6543. Route Does Permission GET /api/v1/alerts/rules Every condition and its rule view_configuration PUT /api/v1/alerts/rules/{condition} Change one, or create one for a condition nothing ships; a field left out keeps its value manage_settings GET /api/v1/alerts/events What has fired, newest first, with the webhook's state view_configuration POST /api/v1/alerts/test Raise a condition through the path a real one takes: {"condition":"disk.low","summary":"...","occurrence":false} manage_settings POST /api/v1/alerts/run Run one pass now manage_server All five are refused to every API key. The administrator password reaches them, and so does an account whose role holds the permission and reaches every domain. Neither the Control Panel nor the Deck has a page for the rules, and hmctl and the PowerShell module have no alerts or audit verbs; hmctl api reaches the routes. The audit trail One row per administrative change made over COM, REST or the Deck, in hm_audit: when (audittime, UTC); who (auditactor: Administrator over COM, administrator or key:<id> over REST, and an account signed in with its own password is its address over COM and account:<address> over REST and the Deck); the interface and, except over COM, the peer address; the object's type and name; the action (created, updated or deleted, also unlocked, cleared, moved and move failed, the last for a mailbox move that failed and could not be put back); the detail (the columns the change carried, or for a setting Name: old -> new, ending [role: <name>] under a role); and the two hashes of the chain. Both kinds of setting are recorded: the hm_settings values and the [Settings] keys (in hm_inisettings since schema 6042), with the value they had. A key set for the first time reads SmtpTarpitCount: (default) -> 5; returned to its default it reads SmtpTarpitCount: 5 -> (default), with the action deleted. A write of the value a setting already has records nothing. What is never recorded as a value. A password, key, token, secret, hash, salt or passphrase is recorded as (changed), decided by the name of the column or setting, matched as a substring, so MetricsServerAuthToken and PasswordPepper are among them. Fifteen keys whose names only look like secrets, the seven PasswordPolicy* keys among them, are recorded with their values. The server's own writes (delivery, retention, greylisting, the index) are never recorded. Verifying the chain. Each row carries the SHA-256 of the row before it and of itself. GET /api/v1/audit/verify answers {"intact","rows_checked","first_broken_id","reason"}, and the reason tells a row that was edited from one that was removed. This makes tampering detectable, not impossible: whoever can write the table can recompute the whole chain. Rows hashed the older way, which only a build from before the 6.3.4 release can have written, did not cover a character above U+00FF; they still verify, with that weakness. Reading it. GET /api/v1/audit?actor=&object_type=&action=&since=&until=&limit=&offset= answers newest first; actor is a substring, so key: finds every change made with an API key, and limit is 1 to 1,000, default 100. It needs the administrator password, or an account whose role holds view_audit (the auditor role) and is not confined to named domains. Every API key is refused, with 401 rather than 403, so that it learns nothing from asking. Putting a change back. POST /api/v1/audit/{id}/revert writes a setting's old value back through the same setter, so the put-back is itself a row in the trail. It needs manage_settings and is refused with 409 for a row that is not a setting's change, a secret's row, a value the trail cut at 120 characters, and a setting this version no longer has. Where to see it. The Deck's Audit trail view has the filters, paging, Verify the chain and Put back. On Windows the Control Panel has the same page, over COM, at Monitoring & troubleshooting → Audit trail (Compliance → Audit trail since 6.3.7; Ctrl+K takes the old route to it). Switching recording off is itself recorded. The trail, the rules and the events are not in the built-in backup. What it holds for compliance. A download of a discovery export is recorded on the trail alone, so with the trail off it is recorded nowhere. An erasure is recorded with the address it erased wherever it deletes from recorded tables (aliases, distribution-list recipients and, with the archive included, the archive index), and audit_retention_days is how those rows age out. The compliance topic covers eDiscovery and the erasure. What it does not record. Reads; what a restore replaces (the request is one row, restore started); creating or revoking a REST API key; releasing or deleting a quarantined message; creating, renaming or deleting a public folder; the [Security], [Directories] and [Database] sections of hMailServer.ini; and a change nobody made through an interface. A change to the twelve syslog settings over COM, from the Control Panel's Logging page for one, is recorded in 6.3.4: the settings property setters carry an audit scope of their own. The wiki's syslog section, 16.2a, still describes the earlier state. Syslog and the journal (since 6.3.4) Every entry the server logs can also be sent as RFC 5424 to a syslog collector over UDP, TCP or TLS. It is a copy: the files under LogFolder are written as before. On Windows the settings are the syslog (RFC 5424) card on the Control Panel's Logging page; on Linux they are keys of PUT /api/v1/settings/logging and the Control Deck, and hmctl settings get logging prints them. They take effect when the sink starts, at the next service start or Reinitialize, not when they are saved. hm_settings name REST key Default Meaning SyslogEnabled syslog_enabled off The whole feature SyslogHost syslog_host empty Empty is not "off": on Linux under systemd it selects the journal; on Windows nothing is sent, which the server says once in the application log SyslogPort syslog_port 514 514 for UDP and plain TCP; 6514 is the registered port for TLS SyslogTransport syslog_transport 0 (UDP) 0 UDP, 1 TCP, 2 TCP over TLS; the REST key takes udp, tcp or tls SyslogFacility syslog_facility 2 (mail) 0 to 23; 16 to 23 are local0 to local7 SyslogMinimumSeverity syslog_minimum_severity 6 (informational) Entries less severe than this are not sent SyslogLogTypes syslog_log_smtp, syslog_log_pop3, syslog_log_imap, syslog_log_application, syslog_log_tcpip, syslog_log_debug 86 One bit mask: SMTP, POP3, application and IMAP on; TCP/IP and debug off Errors are sent whatever the mask says. A message looks like this: <22>1 2026-09-15T14:21:09.284+01:00 mail.example.com hMailServer 4812 SMTPD [hmailserver@32473 thread="7412" session="41" client="203.0.113.9"] <BOM>RECEIVED: MAIL FROM:<[email protected]> The message id is the log category (SMTPD and so on) or, on an error line carrying an hMailServer error number, that number, so msgid == HM5165 is a usable filter at the collector. The structured data under hmailserver@32473 carries thread, and session and client on a conversation line. UDP truncates each message to 1024 bytes, cutting the text and never the header; TCP and TLS frame each message with its length and truncate nothing below 65000 bytes. TLS verifies the collector's certificate against the machine's trust store and the configured host name, so set the host to the name in the certificate, not an IP address; no client certificate is offered. A collector that is down costs log lines, not mail. Entries go to a background thread over a queue of 4096 that drops the oldest when full; a stream that cannot connect backs off from one second to a minute; and the application log gets one line when an outage starts and one when it ends, with the number dropped. Nothing is retried from disk. Each step of a stream transport (the connection, the TLS handshake, one entry written) is given ten seconds, so a collector that accepts the connection and then goes quiet is a failure like a refused one, and a reinitialise or a service stop waits for that step at most. For rsyslog, in /etc/rsyslog.d/30-hmailserver.conf (UDP for the default transport, TCP for SyslogTransport = 1; the stop keeps the lines out of /var/log/mail.log too, so drop it if you want both): module(load="imudp") input(type="imudp" port="514") module(load="imtcp") input(type="imtcp" port="514") if ($app-name == "hMailServer") then { action(type="omfile" file="/var/log/hmailserver.log" template="RSYSLOG_SyslogProtocol23Format") stop } Without RSYSLOG_SyslogProtocol23Format rsyslog reformats to the older RFC 3164 shape and the structured data is lost. For TLS (SyslogTransport = 2 module(load="imtcp" StreamDriver.Name="gtls" StreamDriver.Mode="1" StreamDriver.AuthMode="anon") input(type="imtcp" port="6514") global(DefaultNetstreamDriverCAFile="/etc/ssl/certs/ca.pem" DefaultNetstreamDriverCertFile="/etc/rsyslog.d/collector-cert.pem" DefaultNetstreamDriverKeyFile="/etc/rsyslog.d/collector-key.pem") A self-signed collector certificate has to be installed as trusted on the hMailServer machine, or the handshake is refused and nothing is sent. The journal, on Linux only, needs no collector. Where the server runs as the packaged hmailserver.service, switch the sink on and leave the collector host empty: $ hmctl settings set logging --set syslog_enabled=true $ sudo systemctl restart hmailserver $ journalctl -u hmailserver -f Beside the standard fields the journal gets HMAILSERVER_CATEGORY, HMAILSERVER_MSGID, HMAILSERVER_THREAD, HMAILSERVER_SESSION and HMAILSERVER_CLIENT: $ journalctl -u hmailserver HMAILSERVER_SESSION=41 $ journalctl -u hmailserver HMAILSERVER_MSGID=HM5165 If a collector host is configured the journal is not used. The start of the sink is itself logged, as Syslog: sending RFC 5424 log entries to <host>:<port> over UDP, facility 2. or Syslog: writing log entries to the systemd journal.; a server that logs neither has the feature off. Not sent: the AWStats log, the backup log and the event-script log. Per-domain reports (since 6.3.4) GET /api/v1/reports is an index of the sections, the state of their three sources and what this server cannot answer. A report drawn while its source was off is empty, which is not the same statement as "nothing happened"; each section's note says which it is. GET /api/v1/reports/{section} answers one section, the Deck's Reports view draws the summary, and hmctl report prints a section or saves it as CSV. There is no Control Panel page. Section Counted from Says traffic the message trace Per day and per local domain: incoming, outgoing, failed_incoming, failed_outgoing failures the message trace Per local domain and SMTP status code senders, recipients the message trace The local addresses that sent or received the most, with their failures mailboxes hm_messages The largest mailboxes as they are now: a size, not a history volume the metric history, and the daily per-domain sample Processed, delivered, deferred, bounced, spam and viruses per day, with by_domain rows of spam and viruses storage the same The store's size per day and growth_bytes, with by_domain rows of each domain's size summary all three sources Every section the credential may see, with an omitted list and the reason quota the store Every mailbox with a limit and at least one message, nearest its limit first, as a share of the limit: a share now, not a history countries the metric history and the country counters What a country policy refused, per day; the split by country is since the server last started, not per day The last two are described in QuotaPolicy.md and CountryBlocking.md, not in Reports.md. Parameters. from and to as YYYY-MM-DD, both inclusive, defaulting to the last 30 days and never more than 366; domain, one hosted domain (a domain not hosted here is a 404); top, 1 to 200, default 10; format=csv, refused for summary. In CSV a cell starting with =, +, - or @ is prefixed with an apostrophe. Three things decide whether a report means anything. The message trace is off by default (MessageTraceEnabled=0), and while it is off the four sections counted from it are empty. Processed, delivered, deferred and bounced cannot be given per domain, because those counters carry no domain; spam, viruses and storage can, since schema 6062 added the daily sample in hm_domainsamples, which starts the day it was first taken. And only hosted domains are counted. Authorisation. The administrator password reads everything; every other credential needs both view_configuration and view_message_trace. domainadmin and auditor hold both; helpdesk is refused. A key restricted to named domains sees only those and is refused 403 for another. On volume and storage Reports.md contradicts itself: its Authorisation section refuses them to such a key outright, while its section What cannot be answered, and what it would take (and, for volume, its table of sections under The route) gives a credential restricted to domains the per-domain rows, as the wiki's section 16.6a does. Test both with your own restricted key before a script relies on either. The cost. One report makes at most three grouped queries; the trace read is capped at 100 000 grouped rows and the answer says truncated when it reaches the cap. hmctl report sections hmctl report traffic --from 2026-09-01 --to 2026-09-15 --domain example.com Following one connection live Since 6.3.4 the Deck's Logs page lists the sessions open now and those recently ended, and Follow shows one session's lines as they are written, from an in-memory ring of the last 4,096 log entries that no restart keeps; it needs view_logs. Reading the logs covers following a connection in full, and the log files themselves. Where to read more Documents at v6.3.4: AlertsAndAuditTrail.md, Syslog.md, Reports.md and FollowConnection.md Wiki: Monitoring and Health, sections 16.2a, 16.6a, 16.7a and 16.7b The public documentation: 16.2 Logs on disk On this forum: Monitoring and health for the metrics, the probes, the dashboard and the SNMP agent; Administrator roles for the permissions named here. See also The Control Deck and hMailServer on Linux: what you get and what you do not
  • 0 Votes
    1 Posts
    160 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.6 (6 October 2026) publishes the first container image, registry.gitlab.com/hmailserver/hmailserver:6.3.6, and a monitor that matches "The response carried no HTTP headers." needs the new sentence, which says who ended the connection, after how many bytes and seconds. See the 6.3.6 announcement. hMailServer 6.3.7 (17 October 2026) publishes registry.gitlab.com/hmailserver/hmailserver:6.3.7, tagged 6.3 and latest until 6.3.8; renames the Control Panel page that holds the metrics listener's settings from API & monitoring to Monitoring & alerts, with the alerts' settings, the outbound webhooks and the SNMP agent on it (Ctrl+K takes the old title to it); and refuses GET /api/v1/metrics/history, and the same history over COM, to a credential confined to named domains, such as a domain administrator, the auditor role given a list of domains or an API key issued for named domains. With IMAP and POP3 now taking the client's address from a PROXY protocol header as SMTP does, the High Availability Runbook says that a balancer's /readyz check must not carry that header: the metrics listener reads none and answers a request behind one 404. See the 6.3.7 announcement. hMailServer 6.3.8 (2 November 2026) publishes registry.gitlab.com/hmailserver/hmailserver:6.3.8, also tagged 6.3 and latest. See the 6.3.8 announcement. This topic covers what hMailServer 6.3.4 offers a monitoring system: the metrics listener's Prometheus exposition and health probes, the repository's Grafana dashboard, the OpenTelemetry push, the server's own metric history, the new SNMPv3 agent and, briefly, the daily block-list check of the server's own addresses. The one thing to know first: the metrics listener is off until you give it a port, and once it is on, the health check to configure is /readyz, because it is proved by a real database round trip rather than by a flag. The metrics listener One small HTTP listener serves both the exposition and the probes. Its seven settings are [Settings] keys, set on the Control Panel's Monitoring & troubleshooting → API & monitoring page (Monitoring & alerts since 6.3.7; Windows only), with PUT /api/v1/settings/ini/{name}, with hmctl ini set, or, on a Linux server whose REST listener is not yet on, with hmailserver --set-setting NAME=VALUE; the topic on settings explains where they are stored and when a save applies. All seven are taken when the listener starts, the three credential settings included, so a saved change waits for a reinitialise (POST /api/v1/server/reinitialize, or Apply now in the Control Panel or the Deck) or a service restart. A token that has been saved and not applied leaves /metrics answering as it did before. Setting Default Effect When it is wrong MetricsServerPort 0 0 means the listener is never created No port, no probes, no metrics, and no complaint anywhere MetricsServerBindAddress 127.0.0.1 An IP literal only, parsed with inet_pton: 0.0.0.0, 127.0.0.1, :: or a specific IPv6 address A host name or localhost is rejected: the log says MetricsServer: Invalid bind address, the listener does not start and the probes go with it MetricsServerAuthToken empty Sent by the scraper as Authorization: Bearer <token> Absent on a non-loopback bind: /metrics answers 503, not 401, and the body names the settings that would open it. The probes keep answering MetricsServerAuthUsername and MetricsServerAuthPassword empty HTTP Basic; with both schemes configured, either satisfies the check As above MetricsServerCertificateFile and MetricsServerPrivateKeyFile empty Both set makes the whole port HTTPS, probes included Set but unusable: the listener still serves the probes in the clear and /metrics answers 503 :: serves both address families; a specific IPv6 literal serves IPv6 only. "Loopback" means the whole of 127.0.0.0/8 and ::1. The probes are answered before any branch that can refuse. A Kubernetes httpGet probe, an ELB or ALB health check and a Docker HEALTHCHECK have nowhere to keep a bearer token. Refused credentials on /metrics are counted in hmailserver_metrics_unauthorized_requests_total rather than logged. Nothing on the request path touches the database or the disk. A refresher thread probes the database every 5 s (every 30 s after a failure), refreshes the delivery-queue aggregates at most every 10 s and only when a scrape has asked for them, and re-reads the certificate files every 5 minutes. One exception since 6.3.4: the two message-store gauges are cached for an hour and recomputed by whichever caller finds them stale, so about once an hour a scrape can run select sum(messagesize), count(*) from hm_messages on the listener thread itself: on a large store, a slow answer to that scrape and to any probe queued behind it. It is a single accept loop serving one connection at a time. A request's header read is bounded at 5 s and a response write at 15 s. For a load balancer's check the High Availability Runbook gives an interval of 5 s and a timeout of 2 s; never point a sub-second health check at it. The three health probes No credential in any configuration; HTTP/1.0 responses that close the connection. Path 200 when Body Use it for /livez Always, if the listener answered; never 503 alive Liveness restarts. It checks nothing on purpose: a database outage must not get a healthy process killed /readyz State is Running and the database has proved within the last 20 s that it can answer; 503 otherwise ready, or not ready: <reason> Load-balancer and VIP routing /healthz The same conditions {"status":"ok\|unavailable","state":"...","database":"up\|down","uptime_seconds":N} Dashboards and human debugging Readiness is four conditions in order, each with its own reason: not ready: server not in running state; not ready: database not connected; not ready: database did not answer the last readiness probe; and not ready: database readiness probe has not completed recently. The last exists because a probe against a database that has stopped answering can hang for as long as DBConnectionAcquireTimeout allows, 60 s by default. Since 6.3.4 a server in maintenance mode answers 503 not ready: draining: POST /api/v1/server/drain enters it, GET reports {"draining","since","sessions","deliveries","drained"}, and DELETE leaves it. Start-up is a refused connection, not a 503: the listener is brought up only after the state has gone to Running. Shutdown is a 503 first, then a refusal: the state goes to Stopping before anything is torn down, so /readyz reports 503 while ShutdownDrainSeconds runs. The High Availability Runbook builds its active/passive design on this: both nodes have the listener on, with MetricsServerBindAddress=0.0.0.0 and a MetricsServerAuthToken, and the VIP is gated strictly on /readyz. The passive node's service is stopped, so its /readyz is refused and it is never routed to. Since the probes need no credential, a missing token shows as missing dashboards, not as a failed failover. Every metric family Thirty-eight metric families in 6.3.4 (thirty in 6.3.3). Counters are monotonic since server start; a restart resets them, which is what hmailserver_start_time_seconds is for. Mail flow Metric Type Meaning, and the trap in it hmailserver_processed_messages_total counter One per delivery pass started, not per message received: a message deferred three times and then delivered counts four hmailserver_messages_delivered_total counter Delivery passes that completed with no failed recipient hmailserver_messages_deferred_total counter Passes rescheduled: a temporary failure, greylisting, or a message the virus scanner could not examine (AVFailAction=1) hmailserver_messages_bounced_total counter Delivery-failure notifications queued to the sender; one can cover several recipients hmailserver_delivery_queue_messages gauge Rows in hm_messages in the delivering state hmailserver_delivery_queue_oldest_message_age_seconds gauge Age of the oldest queued message; 0 when the queue is empty. Alert on this, not on depth hmailserver_spam_messages_total, hmailserver_viruses_removed_total counter Messages classified as spam; viruses removed during delivery preprocessing hmailserver_store_bytes, hmailserver_store_messages gauge Since 6.3.4. The size and the number of the messages in hm_messages, recomputed at most once an hour hmailserver_domain_messages_received_total{domain}, hmailserver_domain_messages_sent_total{domain} counter Only present when MetricsPerDomainEnabled=1; only hosted domains are ever labelled Added in 6.3.4. hmailserver_country_blocked_total and hmailserver_country_blocked_by_country_total{country} count connections and sign-ins refused by a country policy, and hmailserver_spam_trap_hits_total counts hits on a declared spam trap. Three gauges cover the feedback loops' seven-day window: hmailserver_domain_complaints{domain}, hmailserver_domain_complaint_window_messages{domain} and hmailserver_domain_complaint_rate{domain}. They are present only when MetricsPerDomainEnabled is set and complaint processing is on. The rate gauge is a ratio, not parts per million: Complaints.md alerts on hmailserver_domain_complaint_rate > 0.001. Parts per million is the unit of the complaints.rate.high alert rule's threshold, so an expression written in parts per million against the gauge would never fire. Sessions, authentication and TLS Metric Type Meaning hmailserver_sessions{protocol} gauge Concurrent sessions; protocol is smtp, imap or pop3 hmailserver_auth_success_total, hmailserver_auth_failures_total counter A rising failure rate against a flat success rate is a password-guessing run hmailserver_tls_handshakes_total, hmailserver_tls_handshake_failures_total counter The mail protocols only; a failed handshake on the metrics port is never counted here hmailserver_tls_certificate_expiry_seconds{certificate} gauge notAfter as a Unix timestamp per configured certificate, re-read every 5 minutes; at most 128 series. Absent entirely when no certificate is configured. With no certificate row, the ACME certificate is labelled ACME (automatic) hmailserver_metrics_unauthorized_requests_total counter Always 0 when no credential is configured The server and the database Metric Type Meaning hmailserver_start_time_seconds gauge Uptime is time() - hmailserver_start_time_seconds hmailserver_build_info{version,architecture,database_schema_version} gauge Always 1; the information is in the labels hmailserver_state{state} gauge One series per state (unknown, stopped, starting, running, stopping and, since 6.3.4, draining), exactly one of which is 1 hmailserver_workqueue_depth{queue}, hmailserver_workqueue_blocking_tasks_waiting{queue} gauge Seven fixed queue values: Server queue, Maintenance queue, Asynchronous task queue, Name lookup queue, IOCPQueue, SMTP delivery queue, External fetch queue. Asynchronous task queue sends the final 250, so depth there is mail waiting to be acknowledged hmailserver_messagestore_missing_files gauge Always 0 unless MessageStoreConsistencyCheck=1 hmailserver_command_processing_seconds, hmailserver_db_query_seconds histogram Bucket bounds from 0.0001 s to 1.0 s, and from 0.001 s to 10.0 s; both emit the full _bucket, _sum and _count set hmailserver_database_connected gauge 1 when the database answered a real round trip recently hmailserver_database_probe_success_timestamp_seconds, hmailserver_database_probe_age_seconds gauge When it last answered, and how long ago on the server's own clock; /readyz reports 503 once the age passes 20 s hmailserver_db_connections{state} gauge busy and available; available pinned at zero is saturation hmailserver_db_slow_queries_total counter Statements slower than SlowQueryLogMilliseconds; 0 while that setting is 0 Scraping it scrape_configs: - job_name: hmailserver scrape_interval: 30s scrape_timeout: 10s metrics_path: /metrics authorization: credentials: "<the value of MetricsServerAuthToken>" static_configs: - targets: ["mail.example.com:9100"] Use scheme: https when the certificate pair is set, and keep scrape_timeout well under scrape_interval. The wiki gives nine alerting rules against these metrics, among them hmailserver_database_connected == 0 for 2m, hmailserver_delivery_queue_oldest_message_age_seconds > 3600 for 15m and hmailserver_tls_certificate_expiry_seconds - time() < 14 * 86400. The Grafana dashboard The repository ships hmailserver/docs/grafana-dashboard.json. Import it as it is and pick your Prometheus data source; the instance variable, from label_values(hmailserver_build_info, instance), selects the server. Its uid is hmailserver-overview; it refreshes every 1m and opens on now-24h. Its panels array has 19 entries: four row headers, five stat panels and ten time series. Every rate is over [5m] and every query is filtered by instance=~"$instance". Health: server state (hmailserver_state == 1), database, uptime, missing message files, and TLS certificate expiry, amber below 21 days and red below 7. Mail flow: delivery outcomes (delivered, deferred and bounced, stacked), queue depth with the oldest message's age on a right-hand axis, messages processed with spam and viruses removed, and active sessions by protocol. Authentication and TLS: authentication outcomes and TLS handshakes, as rates. Latency and database: command processing and database query latency (p50, p95 and p99 by histogram_quantile over the two histograms' _bucket series), the connection pool by state, and slow queries. What the dashboard does not draw: the two database probe gauges, the two work-queue gauges, hmailserver_metrics_unauthorized_requests_total, the per-domain counters, the two store gauges and the six families added in 6.3.4. hmailserver_build_info is read by the instance variable and by no panel. The metric history inside the server The server keeps a short history of its own, so the Control Panel's dashboard can show 24 hours, 7 days and 30 days without Prometheus. MetricsHistoryDays (default 7; 0 switches it off) is the days kept in hm_metricsamples: one row per metric per minute, plus a sample at service start. Fourteen metrics are stored in 6.3.3, named as the exposition's without the hmailserver_ prefix and with the label folded in, such as sessions_smtp and processed_messages_total. 6.3.4 adds store_bytes and store_messages, whose history begins the day the server first samples them, and country_blocked_total and spam_trap_hits_total. Read it with GET /api/v1/metrics/history?metric=…&range=24h|7d|30d, or on Windows over COM with Utilities.GetMetricHistory. It has no labels and no histograms and is not a replacement for Prometheus, but it survives a restart. Pushing to an OpenTelemetry collector Three OTLP/HTTP signals, each a no-op until its own endpoint is set. The pushed metric names are the Prometheus names verbatim. Setting Default Effect OtelMetricsEndpoint empty OTLP metrics; the default path is /v1/metrics OtelMetricsInterval 60 Seconds between exports, clamped to 5–3600 OtelEndpoint empty OTLP traces only: one span per delivery attempt, joined to the trace named by the message's own traceparent; default path /v1/traces OtelLogsEndpoint empty OTLP logs: the same lines, categories and mask the log files get; default path /v1/logs What is pushed: the twelve counters that predate 6.3.4 and, since 6.3.4, hmailserver_spam_trap_hits_total, sessions as one gauge with a protocol attribute, hmailserver_messagestore_missing_files, and the per-domain counters when they are enabled. The histograms, the database and queue gauges and certificate expiry are scrape-only. On Linux, in every release from 6.3.0 to 6.3.3, a collector that accepted the connection and never answered held the exporter, and the stop that waits for it, for good; 6.3.4 fixes that. The SNMPv3 agent (since 6.3.4) For a monitoring system that speaks SNMP and not Prometheus or OpenTelemetry, 6.3.4 adds a read-only agent answering the mail MIB (RFC 2789, MTA-MIB) and the standard SNMP groups from the same counters /metrics serves; an object /metrics has no counter for is answered noSuchObject, not made up. It speaks SNMPv3 with the User-based Security Model for one user at authPriv only (HMAC-SHA-256 and AES-128), and SNMPv2c as an option that stays off even when the agent is on. SNMPv1 is not answered, a Set is answered noAccess, and nothing is sent unasked: no traps, no informs. It is off as shipped, and an empty allow list answers nobody. Setting (hm_settings) REST key Default Meaning SnmpEnabled snmp_enabled off The whole agent; off, no socket is opened SnmpPort snmp_port 161 The UDP port SnmpBindAddress snmp_bind_address empty, every address 127.0.0.1 for a collector on the same machine; otherwise an address on a management network SnmpAllowedAddresses snmp_allowed_addresses empty Addresses and CIDR ranges, at most 64. Name the managers one by one: UDP does not prove a source address SnmpV3User snmp_v3_user empty The one SNMPv3 user SnmpV3AuthPassword, SnmpV3PrivPassword snmp_v3_auth_password, snmp_v3_priv_password (write-only) empty 8 to 64 ASCII characters each SnmpV2cEnabled, SnmpV2cCommunity snmp_v2c_enabled, snmp_v2c_community (write-only) off, empty The community crosses the network in the clear; an answer is never more than eight times its request SnmpContact, SnmpLocation snmp_contact, snmp_location empty sysContact and sysLocation They are on /api/v1/settings (the server group), on the Control Deck's Settings page under Server and in hmctl settings. There is no COM property for any of them, and in 6.3.4 no Control Panel page, so Windows and Linux are configured the same way; since 6.3.7 they are the SNMP tab of the Control Panel's Monitoring & alerts page on Windows, which 6.3.5 and 6.3.6 had on Logging. They can also be given as HM_PROPERTY_SnmpEnabled and so on in the environment, which reaches the database only when hmailserver --apply-environment is run (hMailServer.exe --apply-environment on Windows): the service does not read the environment itself. Each setting is read when the agent starts, at the next service start or Reinitialize. The two passwords and the community are stored protected, never read back, and not carried by a backup: enter them again after a restore. Snmp.md's example keeps every password off the command line. On the server, for a manager at 192.0.2.10: $ umask 077 $ cat > snmp.json {"snmp_allowed_addresses": "192.0.2.10", "snmp_v3_user": "monitor", "snmp_v3_auth_password": "...", "snmp_v3_priv_password": "...", "snmp_contact": "[email protected]", "snmp_location": "Rack 4, London", "snmp_enabled": true} ^D $ hmctl settings set --from-json snmp.json $ rm snmp.json $ hmctl api POST /api/v1/server/reinitialize On the manager, with Net-SNMP 5.9 or later, defVersion 3, the user (defSecurityName), both passphrases, defSecurityLevel authPriv, defAuthType SHA-256 and defPrivType AES go in ~/.snmp/snmp.conf, readable by you alone (chmod 600); snmpwalk mail.example.com 1.3.6.1.2.1.28 then walks the mail MIB. What it serves. In the MTA-MIB mtaGroupTable has one row per protocol the server runs: 1 SMTP, 2 POP3, 3 IMAP. What has a counter is answered: mtaStoredMessages is the delivery queue's depth, mtaTransmittedMessages is hmailserver_messages_delivered_total, mtaGroupOldestMessageStored is the oldest queued message's age, and mtaGroupInboundAssociations is hmailserver_sessions for the protocol. mtaReceivedMessages is noSuchObject, because no counter of messages received exists, and so are the volume, recipient and conversion columns. Sixteen scalars at 1.3.6.1.4.1.32473.1.2.<n>.0 carry /metrics series directly, among them hmailserver_state as a number at 14 (3 is running, 5 draining) and the two delivery-queue gauges at 15 and 16. The enterprise number is a placeholder. 1.3.6.1.4.1.32473 is the number RFC 5612 reserves for documentation and examples; it is not registered to this project with IANA. When a number is registered, sysObjectID, the scalars and the engine ID's prefix move to it in one release, and a manager configured for these OIDs will need them changed. There is no MIB module file yet, for the same reason. When it does not start. Error 7420: the port could not be bound, because another program holds it (the operating system's own SNMP service, on Windows or Linux, listens on 161 when it is installed) or, on Linux outside the packaged unit, the process lacks CAP_NET_BIND_SERVICE; the server runs on without the agent. 7421: the engine's seed or boot count in hm_settings could not be read or written. 7422: an exception left the agent's thread; a Reinitialize starts it again. On Linux the packaged hmailserver.service has CAP_NET_BIND_SERVICE for port 25, which is what lets it bind 161. No container image of 6.3.4 is published; a container built from the repository's Dockerfile publishes the port as UDP (-p 161:161/udp) and needs a host name of its own, since two started with one fixed hostname are one SNMP engine. The daily block-list check, briefly Since 6.3.4 the server looks the addresses it sends from up on DNS block lists once a day and raises the reputation.listed alert when one is listed. The watch is on in a new installation and off after an upgrade, and is read at GET /api/v1/reputation; the topic on DKIM rotation, reports and the reputation watch has the lists, the addresses checked and what a failed lookup means. Where to read more Wiki: Monitoring and Health and High Availability Runbook Documents at v6.3.4: Snmp.md, HighAvailabilityRunbook.md, ReputationWatch.md, Complaints.md and the dashboard itself, grafana-dashboard.json The public documentation: 16.3 Prometheus metrics and health probes On this forum: Alerts, the audit trail, syslog and the journal, and per-domain reports, Reading the logs, and Warm standby and high availability for the pair the probes gate. See also The Control Deck and hMailServer on Linux: what you get and what you do not
  • 0 Votes
    1 Posts
    124 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) regroups the Control Panel's sidebar: the Journaling and dual delivery page is Journaling & dual delivery, in the new Compliance group rather than under Monitoring & troubleshooting as the table below has it, and Ctrl+K takes the old title to it. See the 6.3.7 announcement. A journal rule sends a copy of a domain's mail, or of one account's, to an address. Exchange calls it journaling and sends an envelope report with the message attached; Google calls it dual delivery and sends the message itself. In hMailServer they are one mechanism with two shapes of copy, new in 6.3.4 (27 September 2026, schema 6104; the rules themselves arrived with schema 6075). This topic covers what a rule says, the two shapes, when a rule is evaluated, how it avoids looping, where it is edited, and what it deliberately does not do. The one thing to know first: a journal rule covers mail that comes in through the SMTP session, a fetch account or the send API, and nothing this server queues itself. A compliance archive built on a journal rule therefore has known gaps, and they are listed exactly below. Nothing is journalled until a rule exists. With an empty table the whole of this costs one comparison per accepted message. What a rule says Field Meaning scope domain or account: whose mail the rule is about target The domain name, or the account address. It must be a domain this server hosts, or an account in one direction inbound (a message with a local recipient the rule covers), outbound (a message from an authenticated sender it covers), or both journal_address Where the copy goes. One address, local or anywhere else, checked as an address before the row is written. It may be a mailbox in the very domain the rule journals; it may not be the account an account rule names mode report or copy enabled A rule that is off copies nothing and keeps everything it said note Why the rule exists. At most 255 characters The rules are stored in hm_journalrules. The row holds the domain id and, for an account rule, the account id, so nothing breaks when a domain or an account is renamed under it. A domain's rules go with the domain when it is deleted, and a rule naming an account goes with the account. A rule has no conditions. It covers all of a domain's or an account's mail in the direction it names. There is no "only messages over 10 MB" and no "only from these senders"; a mail flow rule is the place for that. The two shapes report is the Exchange journal shape: a multipart/mixed message whose first part is the envelope report and whose second part is the original message attached as message/rfc822, written through byte for byte. The report is four fixed fields, one per line, in one order: Sender: [email protected] Subject: Quarterly figures Message-Id: <[email protected]> Recipient: [email protected] Recipient: [email protected] The envelope is the point. A message's To and Cc headers are what the sender typed; the Recipient: lines are where the message actually went, blind copies and expanded lists included. A message with no envelope sender - a bounce - writes Sender: <>. copy is the message itself, as it was accepted, with one header line (X-hMailServer-Journal) in front of it and nothing else changed: not the body, not its transfer encoding, not its signature. The file is written byte for byte rather than through the MIME writer. What a migration or a second mail system wants is the mail, not a report about it; that is what copy is for. Both shapes are queued through the ordinary delivery queue, so a journal address somewhere else is delivered, retried and bounced as any other mail is, and a local one lands in a mailbox. The envelope sender of a journal message is the journalled domain's postmaster, not the person who wrote the message. A journal address that stops answering is the administrator's problem, and bouncing it to the sender would tell somebody outside this server that their mail is being copied. When a rule is evaluated Once per accepted message, in the SMTP session, after the row is saved and the message file is settled - the same place, and the same recipient list, the on-disk archive decision is made from. Not at delivery: the deliverer runs again for every retry and for every route a message splits into, and a compliance archive that gained a second copy because a remote host was busy would be worse than useless. One report or copy per matching rule per message, not per recipient. A domain rule that matches a message addressed to three accounts of the domain sends one report, with all three on it. There are three points of entry where rules are evaluated: The SMTP session. The outbound half judges an authenticated sender; this is what a desktop or phone client uses. A fetch account. Mail a mailbox collects from another provider by POP3 or IMAP is journalled once per message, at the same point in that path. A download has no authenticated sender, so it is the inbound half of a rule that speaks for it, and it has no SMTP session, so its lines go to the application log. The send API, POST /api/v1/send, which is new in 6.3.4 and is journalled in it (CHANGELOG lists the fix in its 6.3.4 section). The key's sender counts as the sender an outbound rule judges when that sender is an account here. What a journal rule does not see A message the server queues itself is queued past all three points and is not journalled in either direction, even when it is addressed to a mailbox a rule covers: Not journalled Note A message the webmail queues directly POST /api/v1/me/messages A send-later draft A message written by an event script A mail flow rule's forward, copy or reply An automatic reply A Sieve redirect An account's forwarding A delivery report Two consequences follow, and both matter to anyone relying on this for compliance: The outbound half covers SMTP submission and the send API, and not the browser. Mail a person sends from the webmail is not journalled by an outbound rule. The inbound half covers mail from outside this server, and not what the server sends on to a mailbox of its own. Two further limits of the same kind: A message that was refused is not journalled. A message rejected at RCPT TO or by a rule never existed as far as this is concerned. A message held for review is journalled, because it was accepted. Not looping There is one guard, and it takes nobody's word for anything. Every journal message this server writes carries X-hMailServer-Journal, and its value is the rule's id, a full stop, and a mark: 128 random bits the server process chose when it first needed one. The evaluation reads that header before it does anything else and stops only when the mark is this process's own. A journal message that comes back into this server - relayed by the far end, forwarded by the journal mailbox's owner - is never journalled again, and the guard stops the copy, not the delivery: the mailbox still gets its mail. A message carrying the header with any other value is journalled exactly as it would have been without it, and the attempt is noted in the log. The header's name is in the published document, so a guard that honoured the header as written would let anybody under an outbound compliance rule keep a message out of the archive by typing one line into their mail client. An archive that the people it is about can opt out of is not an archive. Three properties of the mark to understand: It is per process, and nothing is stored for it. A restart forgets the marks it wrote, so one of this server's own reports that returns after a restart is journalled once more, and the report that makes carries a mark the running process does know, so it stops there. A loop cannot run away; the price of keeping no secret on disk is one duplicate. It is not a signature over the message. It travels in the messages it marks, so whoever can read this server's journal mail - the journal address itself, and anything relaying to it - could quote a mark back in a message of their own and keep that one message out of the archive. If that relay is a worry, journal to a local mailbox, or over an authenticated route. There is no second guard, and no rule is skipped for where its journal address points. Journalling a domain into a mailbox of that domain works. example.com to [email protected] sends one report per message into that mailbox and nothing cascades, because a journal message is written straight into the delivery queue and never passes an evaluation point. It used to be refused at run time, silently: the rule matched, the copy was dropped with a line in the log, and every surface went on showing the rule as enabled. That guard went. The one arrangement refused is an account journalled into itself, and it is refused when the rule is written rather than found out later: it would put every message in the mailbox twice and archive nothing anywhere. Where it is edited Journal rules are the server administrator's. A rule says that somebody's mail is copied to an address outside their sight, so a credential confined to named domains is refused even though a rule names one. Surface How Platform REST GET/POST /api/v1/journal-rules, PUT/DELETE /api/v1/journal-rules/{id} Windows and Linux The Control Deck The Journaling view: every rule, a switch on each row, and a form to add one Windows and Linux The Control Panel The Journaling and dual delivery page, under Monitoring & troubleshooting (since 6.3.7 Journaling & dual delivery, in the Compliance group; Ctrl+K takes the old title to it), over three Utilities methods (GetJournalRules, SaveJournalRule, DeleteJournalRule) Windows only hmctl hmctl journalrule list\|create\|update\|delete A client over the REST API PowerShell module Get-HmJournalRule, New-HmJournalRule, Set-HmJournalRule, Remove-HmJournalRule Over the REST API Over REST, reading needs view_configuration and writing needs manage_server, so the auditor role reads the rules and writes none. Every surface goes through one function on the server, Journaling::SaveRule, so the three small vocabularies (domain/account, inbound/outbound/both, report/copy) mean the same thing whichever you use, and a refusal reads the same sentence. Writing, changing or removing a rule is on the audit trail. Setting up a compliance copy for one domain: Decide the journal address. A mailbox in the same domain, such as [email protected], is allowed; an address elsewhere is delivered through the ordinary queue. Create one rule with scope domain, target the domain name, direction both, mode report and the journal_address. Over SMTP - from a mail client signed in to an account, or from outside, and not from the webmail - send one message to two recipients of the domain, one of them as a blind copy, and open what arrives at the journal address: one report for the message, with a Recipient: line for each envelope recipient. Then send one message from the webmail and confirm that it is not journalled, so that whoever owns the archive knows the gap is there. A rule is refused before it is written for an unhosted target, a word outside the three vocabularies, no journal address, one that is not an address, one carrying a line break, and an account journalled into itself. Journaling beside three other features Sealed mail. The journal keeps the bytes verbatim, so a message that was encrypted end to end still decrypts from the copy - but nobody reviewing the journal can read its content. Restricted delivery. A journal rule's copy of a restricted account's mail goes where the rule says, because the copy is the administrator's and not the account's forward. Rules and Sieve. A rule's forward, copy or reply and a Sieve redirect are among the messages the server queues itself, so they are not journalled. What it does not do It is not the archive. ArchiveDir and ArchiveDomains keep files on this server; a journal rule sends mail elsewhere. Neither replaces the other, and turning one on does not turn the other off. It is not eDiscovery. A case searches the mailboxes, their deleted items and the archive on this server, and a journal copy has left it. A journal address that is a mailbox here is searched like any other custodian. It is not an account's forwarding. A forward is the account holder's, and can deliver instead of the mailbox. A journal copy is the administrator's, and never changes what the mailbox gets. It is not a mail flow rule's forward or copy action. Those have conditions, run per delivery attempt, and can be edited by the account. It is not retention. Once a journal message leaves the queue it belongs to whatever receives it. This server keeps no second copy, expires nothing and withdraws nothing; deleting a rule stops the next copy and does not reach the ones already sent. It is not encryption or redaction. A journal address outside your control sees everything the recipient sees. Where to read more The document at the 6.3.4 tag: Journaling.md, with Discovery.md, SendApi.md, SealedMail.md and RestrictedDelivery.md Wiki: Routes and Relays (section 14.2d) and Compliance and eDiscovery On this forum: the 6.3.4 release announcement; Rules and Sieve for the forwards and redirects a journal rule does not see; restricted delivery; the send API; compliance, eDiscovery and legal holds; and the archive
  • 0 Votes
    1 Posts
    127 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) signs an authenticated submission as another hosted domain only when the account may send as the address its From names - its own, an alias pointing at it, a mailbox that granted it the post right, an address it proved, or a masked address of its own; otherwise the mail is sent unsigned as that domain, so give such an account the alias or the grant before upgrading. Mail from outside whose From names a hosted domain is no longer signed as it, nor is an alias's or a distribution list's copy of it to an outside address; where such a message did not pass DMARC for the organisation its From names when it arrived, a copy whose envelope sender is at this server's host name, or at a hosted domain or a subdomain of one, in that organisation is not sent where an alias or a distribution list expands to an outside address, a managed list does not distribute a posting whose From names the list domain's organisation, and a distribution list's bounce address is not given to it; a managed list's copies of a posting from outside are not signed as the list's domain when an address of the list domain's organisation is in its From fields. Creating a domain alias is now the server administrator's: a credential confined to named domains is refused it, over REST and over COM. See the 6.3.7 announcement. There are three ways of making one address reach somewhere other than a mailbox of the same name - an alias, a distribution list and a route - and one way of giving several people the same folder. This topic covers all of them as they are in 6.3.4 (27 September 2026, schema 6104): aliases, domain aliases and plus addressing; distribution lists, who may post to them and how moderation works; the membership query, which lets a list define its own members; the list manager, which turns a distribution list into a mailing list people join and leave themselves; and public folders with their permissions. The membership query and the list manager are new in 6.3.4; the distribution list itself is in every release. The one thing to know first: the address is resolved in one function (RecipientParser::CheckDeliveryPossibility), in a fixed order, and an account always wins. The order an address is resolved in At RCPT TO the server works through this, and stops at the first answer: Domain aliases rewrite the domain. Plus addressing trims the local part. Account? If found, deliver to the mailbox. Alias? If found, follow it and start over, up to 25 hops. Distribution list? If found, expand to the members, or to the moderator. Route? If found, send to the route's target server. The domain's postmaster address, the catch-all: when nothing above matched, the resolution starts again with that address, which is why a mistyped address is delivered there rather than refused. If it is empty, 550 Unknown user. Since 6.3.4 one question is asked before all of these: is the address, or its domain, routed to a webhook? A webhook route that only posts takes the address's mail instead of its mailbox, alias or list - a list whose address is routed never sees the message, nor applies its posting rules to it. Route the addresses you mean rather than a list's whole domain; mail in to a webhook has the routes themselves. Aliases An alias is a second address that delivers to an existing target. In the Control Panel: Accounts & domains → Domains, select the domain, then the Aliases panel. The Control Panel and COM are Windows-only. On Linux, where there is neither, the Control Deck is the administration people click through, and it is the front end of the REST API, which both platforms have: What REST route A domain's aliases GET and POST /api/v1/domains/{domain}/aliases; DELETE /api/v1/aliases/{address} A domain's other names GET and POST /api/v1/domains/{domain}/domain-aliases; DELETE on .../{name} beneath it A domain's lists GET and POST /api/v1/domains/{domain}/lists; PUT and DELETE /api/v1/lists/{address} A domain's plus addressing PUT /api/v1/domains/{domain} (plus_addressing_enabled, plus_addressing_character) The Control Panel paths and COM properties below are for Windows. Field COM property Meaning Alias address Alias.Name The address people send to. Must be in this domain Target Alias.Value May be anything: another account here, another alias, a distribution list, an address covered by a route, or an address at a different server Active Alias.Active Unticked: RCPT TO is answered 550 Alias is not active. - not passed on to the catch-all The target replaces the address and resolution starts again, so an alias may point at an alias, which may point at a list. Beyond 25 hops the answer is 550 Mail server configuration error. Too many recursive forwards. An alias is only consulted when no account has that address. Creating sales@ as an account after sales@ exists as an alias silently changes where the mail goes. An alias to an external address is treated as inbound mail, not as relaying, so [email protected] → [email protected] works for external senders without opening a relay. Since 6.3.4 three more kinds of second address exist, and they are the person's own rather than the administrator's: the alias a mailbox move leaves (renaming or moving a mailbox keeps the old address as an alias of the new one by default); masked addresses (schema 6067), which the person makes in the webmail for one shop or sign-up and which, switched off, are refused as an address that never existed; and verified send-as (schema 6066), which receives nothing here - it is a From address, not an alias. Domain aliases. Accounts & domains → Domains → Properties → Names. The rewrite is total: [email protected] becomes [email protected] before anything else is looked at. You do not create per-address aliases for an alias domain; the sender address is rewritten too, which is what lets a list in membership mode recognise a member who posts from the alias domain; and the alias domain needs its own MX record. Since 6.3.7 adding one is the server administrator's: a credential confined to named domains - such as a domain administrator, or an API key issued for its domains - is refused, over REST and over COM, because a domain alias decides what names the server accepts mail for. Plus addressing is off by default and enabled per domain on Accounts & domains → Domains → Properties → Limits (default character +). With it on, [email protected] resolves to [email protected], and the full original address is still in the To: header. It applies to the recipient address only, after domain aliases and before the account, alias and list lookups. When off, alice+invoices@ is an unknown address. Distribution lists A distribution list delivers one message to many recipients. In the Control Panel: the Distribution lists panel of the domain, then Properties and Members. Mode Stored value Who may post Refusal Public - anyone can send 0 Anyone at all - Membership - only list members can send 1 Any address on the member list, after domain aliases are applied to both sides 550 Not authorized sender. Announcements only 2 Only the address in Require sender address 550 Not authorized owner. Anyone in the domain can send 3 Any sender whose domain is the list's own 550 Not authorized domain. (on any mode) Require SMTP authentication flag An unauthenticated session is refused first 550 SMTP authentication required. "Anyone in the domain" is a claim, not a proof. MAIL FROM is free text, so an outsider can assert a local address. Tick Require SMTP authentication with that mode or the list is open to the Internet. A fifth mode, 4, "Anyone with a server account can send", existed only in the type library and behaved as Public; put_Mode now refuses the value and the option is gone from the dialog. One more check runs for every mode, Public included: the poster must be allowed to reach every recipient of the list, so a list containing a members-only list the poster is not in is refused as a whole. Moderation. Set a Moderator address and a posting the rules above would refuse is accepted with 250 and forwarded to the moderator instead, stamped X-hMailServer-Moderation. The moderator approves it by resending it to the list from an authenticated session; that authentication is the security boundary and is unconditional. Three failures are numbered in the log: 6360 (the moderator was removed between the check and the expansion), 6361 (the moderator address does not resolve; the recipient is refused with 550) and 6363 (the list is its own moderator). Bounce address. Empty means bounces go to the poster. Set, it becomes the envelope sender of every copy, so a dead subscriber's bounce reaches the list owner, and an external member's server evaluates SPF against the list's own domain. A bounce address pointing at the list itself is ignored, with error 6362. Headers. An unmanaged list adds List-Id, List-Post (the literal NO for an announcements-only list), List-Help, List-Owner and a mailto:-only List-Unsubscribe, before DKIM signing. Nothing is added when the message already carries a List-Id, or when the list or owner address is not US-ASCII. A list that defines itself: the membership query, since 6.3.4 Schema 6074. A list can carry a membership query: one short text that says who its members are, answered against the server's accounts at the moment a message is sent to it. [email protected] with the query domain reaches every active account of the domain on the day, including the person who joined this morning and not the one switched off last night. Query Members (empty) A static list: the addresses under Recipients and nothing else domain Every active account of the list's own domain domain:other.example Every active account of that domain, which this server has to host pattern:sales-*@example.com Every active account whose address matches the wildcard; * is any run, ? exactly one, case ignored everyone Every active account of every active domain on the server It is a union with the list's own recipients, never a replacement. An address that is both is delivered to once. It answers accounts only - not aliases, other lists or outside addresses - and never a disabled account or an account of a disabled domain. It is resolved once per SMTP transaction, not once per RCPT TO, and nothing is stored. Who may set which form. A domain administrator, or an API key with a domain scope, may set domain or a pattern: in a domain it holds. everyone, domain: naming another tenant's domain, and a pattern whose domain part is itself a wildcard are the server administrator's. The same rule governs reading the answer: GET /api/v1/lists/{address}/members answers 403 to a confined credential when the query reaches past its domains. A query is judged when it is saved. domain: naming a domain this server does not host, a pattern: with no @, or any text that is none of the five forms is refused, by the same sentence over COM, over REST and in the Control Panel. "Answers nobody" and "could not be answered" are different. A query whose named domain has been deleted is not an error at delivery: the message goes to the list's rows and code 6980 is logged. But when the accounts cannot be read - the database locked by a backup - the recipient is refused 451 4.3.2 and the sender retries, with code 6982, and the members route answers 503. It is set as Membership query in the Control Panel's list dialog and the Control Deck, membership over REST, DistributionList.Membership over COM, and hmctl list update [email protected] --set membership=domain. The Deck's list editor shows Delivers to now, each address marked Kept by hand or From the query. The list manager: a mailing list, since 6.3.4 Schema 6092. A distribution list with the list manager switched on is one people join and leave themselves. Nothing changes for a list until it is switched on: every list an upgrade finds is unmanaged. On the list: Managed in the Control Panel's list dialog, managed in the Deck's list editor and on PUT /api/v1/lists/{address}, or DistributionList.Managed over COM. The links. The confirmation page, each member's page, the owner's approval page, the subscribe form and the archive are served by the REST listener, which is off until RestApiPort is set (turning the REST API on). Make it reachable over HTTPS, then set list_base_url (ListBaseUrl), such as https://lists.example.com. Empty, the default, offers no link at all. Only an https:// base gives RFC 8058's one-click. DKIM on the list's domain. The copies are signed as that domain after the list's fields are in place, so the signature covers List-Unsubscribe and List-Unsubscribe-Post. With DKIM off for the domain the copies go unsigned and the providers do not offer the one-click. Address What it does [email protected] Post to the list [email protected] Ask to join, and confirm by replying [email protected] Ask to leave: the member is sent a link and a token, and nothing more happens until they follow it [email protected] The help text [email protected] The person who runs the list [email protected] Delivery reports; with VERP, [email protected] None of them is an account; an account, an alias or a list of exactly one of those names is the administrator's and wins over the manager. Joining. subscribe_policy is 0 closed (the default: the administrator adds members), 1 confirm (the address asked for is sent a confirmation with a token) or 2 moderated (after the address confirms, the owner approves on a page, signed in to the webmail as the owner's own account, or on the Deck; a reply by mail approves nothing). The list answers at most twenty requests an hour from one requester and two hundred an hour in all. Delivery. Each member chooses each message, one digest a day, or none. With digest on, one digest goes at digest_hour (7 by default, the server's local time), at most 100 messages and 10 MB. With archive at 1 (members) or 2 (public), every distributed message is kept and shown by month, as text only, never as HTML. The rows of hm_listarchive are not in the backup. What a member's copy carries, over an https:// base with the archive on: List-Id: <team.example.com> List-Post: <mailto:[email protected]> (NO for an announcement list) List-Help: <mailto:[email protected]?subject=help> List-Subscribe: <mailto:[email protected]?subject=subscribe>, <https://lists.example.com/lists/[email protected]/subscribe> List-Unsubscribe: <mailto:[email protected]?subject=unsubscribe>, <https://lists.example.com/lists/unsubscribe?t=...> List-Unsubscribe-Post: List-Unsubscribe=One-Click List-Owner: <mailto:[email protected]> List-Archive: <https://lists.example.com/lists/[email protected]/archive> Precedence: list X-Loop: [email protected] X-hMailServer-List-Member: <a tag, see Bounces> Bounces. A 5.x.x is a hard bounce, a 4.x.x a soft one; a delayed warning counts for nothing. Anybody can write to the bounce address, so a report is believed only when it carries the member's tag, X-hMailServer-List-Member. Setting REST key Default ListHardBouncesToUnsubscribe list_hard_bounces_to_unsubscribe 1 ListSoftBouncesToUnsubscribe list_soft_bounces_to_unsubscribe 5 ListBounceWindowDays list_bounce_window_days 30 A member who reaches either count is set to none - paused, never unsubscribed - and the owner is told once. Public folders Accounts & domains → Public folders, in the Control Panel on Windows, creates shared IMAP folders; public folders have no REST route yet, so Linux has no administrator's page for them. On either platform a client holding the a right manages a folder's permissions over IMAP. Public folders live in their own namespace, #Public, and sub-folders are made by typing the whole path with the hierarchy delimiter, . by default: #Public.Team.Archive. #[email protected] is a different namespace: one account's own folders, shared. Each permission entry grants RFC 4314 rights to an account, a group or Anyone (every authenticated account). Right Letter What it allows Lookup l The folder appears in LIST/LSUB. Without it the folder does not exist as far as that user is concerned Read messages r SELECT and STATUS Keep seen/unseen state s Set or clear \Seen Set flags w Flags other than \Seen and \Deleted Insert i APPEND and COPY into the folder Post p Send mail to the folder's submission address Create sub-folders k CREATE beneath it Delete folder x DELETE Delete messages t Set or clear \Deleted Expunge e EXPUNGE Administer a SETACL, DELETEACL, GETACL, LISTRIGHTS An owner has every right implicitly. For anyone else the server walks up from the folder to the nearest ancestor that has any ACL rows, then uses the row for the account, else for a group the account is in, else for Anyone. The nearest ancestor with any rows wins, and rows are not merged. Adding one permission to a sub-folder removes everyone else's access to it. \Seen is shared in a shared folder, not per user. Grant lrs and delegates share one read/unread state; grant lr and they leave no trace. Turning ACL enforcement off opens public folders only. It does not extend to #Users, which stops existing entirely. What it does not do The list manager does not rewrite From for DMARC. A posting from a domain that publishes p=reject reaches outside members with a From that fails DMARC when the list changed the message, and Gmail and Yahoo may refuse it. No posting from the web, no list password, no approval by reply, no NNTP, no IMAP access to an archive, no weekly digest, and one owner address per list. A membership query has no directory-attribute form and no group: form, and no query can name another list's query. The list manager's fixture is not linked into the Linux test suite: MailingLists.md says its thirty-four tests are of the Windows suite. Where to read more Wiki: Aliases, Lists and Public Folders, Mailing Lists, Mail In to a Webhook Documents at the 6.3.4 tag: DistributionLists.md, MailingLists.md The public documentation: chapter 13 On this forum: Being trusted by the servers you send to for DKIM and DMARC, the 6.3.4 release announcement, the webmail in depth for masked addresses and send-as, and IMAP on the server for shared mailboxes
  • 0 Votes
    1 Posts
    128 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) holds a credential confined to named domains - such as a domain administrator, or an API key issued for its domains - to a public address when it changes a domain's relay host, so it cannot point the server's delivery at the server's own network, and makes a domain's or an account's rule's Send using route and Bind to address actions the server administrator's to set; an account given the serveradmin role with a list of domains is, over COM as over REST, confined to those domains, so a script's account that edits the routes or the relayer needs the role with no list. See the 6.3.7 announcement. There are four ways this server can decide where an outbound message goes, and they are tried in a fixed order. Almost every "it went to the wrong server" question is answered by knowing that order, so this topic starts with it. It then covers the smart host and its failover, per-domain relays, routes and what they do to an unknown recipient, sending an OAuth2 token to a relay and what 6.3.4 changed about it, the COM change that is in effect only after Save(), and three things new in 6.3.4 (27 September 2026, schema 6104) that are not about where mail goes: remote domain policies (one paragraph here; the fuller treatment is Requiring TLS: MTA-STS, DANE and remote domain policies) and restricted delivery, which decide whether mail may go, and message recall, which withdraws unread copies already delivered on this server. The one thing to know first: a route is a statement about the destination and beats everything; a per-domain relay is a statement about the sender; the server-wide relayer is the fallback for mail nothing else has an opinion about. The order of decision When a message is ready to leave, its recipients are grouped by domain and the server asks: Did a global rule apply the Send using route action? Then every recipient goes to that route's target host and nothing else is consulted. Is there a route whose domain matches the recipient domain? Wildcards are allowed. The route's host, port, connection security and credentials are used. Does the sender's local domain have a relay host of its own? That domain's relay host, port, security and credentials. Port 0 means 25. Is a server-wide SMTP relayer configured? The relayer's host, port, security and credentials. Port 0 means 25. Otherwise, an ordinary MX lookup for the recipient domain, then A or AAAA for each MX host. Targets that turn out to be identical are merged: a message for four domains that all relay through one smart host opens one connection, not four. A bounce never picks up a per-domain relay, because a bounce has no envelope sender, so there is no sending domain to consult. Since 6.3.4 one question comes before all of these for inbound mail: an address or a hosted domain routed to a webhook has each message posted to an application, instead of being delivered (deliver off) or as well (deliver on). That is a different thing from the routes below, which send a domain's mail to another mail server; mail in to a webhook has the webhook routes. Sending through a smart host On Windows, configure it in the Control Panel at Mail flow & delivery → Delivery of e-mail → Relayer. The Control Panel and the COM API are Windows-only; over REST, which Linux has too, the relayer's settings are keys of PUT /api/v1/settings (smtp_relayer_password is write-only). Setting COM name Default When it is wrong SMTP relayer SMTPRelayer empty: MX lookup A host that does not resolve gives the bounce text "The host specified as SMTP relay server could not be found" with 4.3.5, a temporary failure, so the message is retried Port SMTPRelayerPort 25 Looks like a connection timeout in the SMTP log Connection security SMTPRelayerConnectionSecurity None Required against a host without STARTTLS defers every message Relayer requires authentication SMTPRelayerRequiresAuthentication off Off against a host that requires it gives 530/535 and the mail stays queued Number of delivery retries SMTPNoOfTries 4 0 means one attempt and then a bounce Minutes between retries SMTPMinutesBetweenTry 60 Outbound delivery security SMTPConnectionSecurity STARTTLS (optional) Applies to MX-resolved delivery, not to the relayer Several smart hosts. Separate the names with a vertical bar: smtp1.example.net|smtp2.example.net Each name is resolved in turn and every address it resolves to is appended, giving one ordered candidate list tried top to bottom. A single name with several A records already gives that failover. All hosts in the list share the port, security and credentials. A permanent 5xx rejection is a real delivery failure and the next host is not tried: trying it would deliver the message twice if the first server had in fact accepted it. Relaying everything through an ISP: Open Mail flow & delivery → Delivery of e-mail → Relayer. SMTP relayer: smtp.isp.example. Port: 587. Connection security: STARTTLS (required) - required, not optional, so a downgrade cannot send your credentials in clear. Tick Relayer requires authentication and fill in the user name and password. Save changes, send one message, and read Monitoring & troubleshooting → Live logs. The line that proves the relayer was used is: "SMTPDeliverer - Message 41: Relaying to host smtp.isp.example." If the message went to an MX host instead, the relayer field is empty, or a route or a per-domain relay took precedence. Per-domain relay. Accounts & domains → Domains → Properties → Relay sets where mail from that domain leaves through. It is for a server hosting several independent domains where each has bought its own delivery provider. An empty host means the domain has no opinion. Since 6.3.7 a credential confined to named domains - such as a domain administrator, or an API key issued for its domains - may set the host to a public address only, over REST and over COM; a host on the server's own network is the server administrator's to set, and one the server administrator set stays the domain administrator's to save, since only a change of the host is judged. Routes A route overrides delivery for a specific recipient domain. Mail flow & delivery → Routes: type the domain and target host, press Add, then Properties for everything else. Over REST the routes are /api/v1/routes. Field Default Note Domain name - Wildcards are matched, so *.partner.example works. The first matching route in name order wins; a wildcard broader than you meant silently captures other domains Target SMTP host, port -, 25 A name or a literal IP address; the host1\|host2 failover applies here too Number of delivery retries 3 Replaces SMTPNoOfTries. The delivery path applies it as tries − 1, so a route set to 3 schedules 2 further attempts Minutes between retries 10 Where one message has recipients on several routes, the largest count and the largest interval win Deliver to all addresses on Off: only the addresses listed are routed. Off with an incomplete list is the single most common route mistake Connection security None None, SSL/TLS, STARTTLS optional, STARTTLS required Treat sender domain as local off Turn it on only for a domain you actually control Treat recipient domain as local off On, unauthenticated senders may send to it from the Internet range What a route does to an unknown recipient: Route setting Domain has a catch-all? Address in the route's list? Result Deliver to all addresses - - Routed Specific addresses only no no 550 Recipient not in route list. Specific addresses only yes no Delivered to the catch-all, not routed Specific addresses only yes yes Routed; the catch-all does not intercept it Routes also make ETRN work: when a remote site issues ETRN <domain> and a route matches, the messages held for it are released for immediate delivery. OAuth2 to a relay A route or the SMTP relay whose host is on OutboundOAuth2Hosts (default smtp.office365.com) is sent AUTH XOAUTH2 with the server's own token: a client-credentials token fetched from OutboundOAuth2TokenUrl with OutboundOAuth2ClientId, OutboundOAuth2ClientSecret and OutboundOAuth2Scope (default https://outlook.office365.com/.default), or OutboundOAuth2FixedToken used verbatim instead of fetching. The default host list is inert on its own: with OutboundOAuth2TokenUrl and OutboundOAuth2FixedToken both empty, no bearer is set and the delivery proceeds with LOGIN and the password, with no log line. Since 6.3.4 the token is sent over TLS only. A route or the SMTP relay whose host is on OutboundOAuth2Hosts and whose connection security is none - or STARTTLS optional against a server that does not offer it - used to send AUTH XOAUTH2 with the token in the clear. Such a delivery now sends nothing in its place, fails with a temporary 450 4.7.0, and the error log says to set the security to TLS or STARTTLS required (HM5912); the message waits for the retries the route allows. Exchange Online offers STARTTLS on 587, so a route set up as Microsoft documents it is not affected. Also since 6.3.4, a relay that refuses the login no longer gets the token into the bounce, which now reads hMailServer sent: AUTH XOAUTH2 <Token removed>; the logs write AUTH XOAUTH2 ***. These are [Settings] keys, and since 6.3.4 the database is the settings store: a line edited in the file changes nothing, the stored value is used and named as HM5804, and the line is put back where the service can write the file (when a saved setting takes effect). Change them in the Control Panel, with PUT /api/v1/settings/ini/{name} or with hmctl ini set. A COM change is in effect only after Save() Since 6.3.4 a COM change to a route or an incoming relay is in effect only after the object's Save(), and a property set and never saved changes nothing; the list of objects this applies to does not name the server-wide SMTP relayer's settings. COM is Windows-only, so this concerns scripts on Windows: the COM API topic has the whole list, what an object read before the save shows, and error 7390. Remote domain policies, in one paragraph Since 6.3.4 (schema 6045). A route says where mail for a domain goes; a remote domain policy says what this server will and will not do when it talks to one domain it does not host, however that domain's server was found, and it applies to a routed delivery exactly as to an MX one. One record per name or pattern (bank.example, *.example, *), the most specific active one governing, holds the outbound TLS the domain demands (none, encrypted, verified or dane; a delivery that cannot meet it is deferred with 4.7.0, never sent in the clear), TLS required of its senders (530 5.7.0), a largest message (5.3.4), connection and rate ceilings (deferred with 4.4.5), whether automatic replies and forwards may go there, a recipient callout for a domain this server is a backup MX for, and gateway encryption to the partner's own key. It is server-administrator only, and GET /api/v1/remote-domains/effective?domain=mail.bank.example answers which record a delivery will use. Restricted delivery, since 6.3.4 Schema 6084. Accounts, or a whole domain, that may only exchange mail with named domains: a school whose pupils write to the school and to nobody else. It is not a route: it decides whether mail may go, not where. The list is the domain's: Domain.RestrictedDomains over COM, restricted_domains on GET/PUT /api/v1/domains/{domain}, the Restricted delivery tab of the Control Panel's domain dialog, the same group in the Deck's domain editor. At most 2,000 characters, names separated by commas or spaces, ASCII only, an internationalised name in its xn--... form. Written as Covers partner.example that domain, and nothing beneath it .example example and everything beneath it, anchored at a label boundary An account follows its domain unless it says otherwise (Account.RestrictedDelivery, restricted_delivery on the account routes): Value Word Meaning 0 domain Follows the domain. The default 1 restricted Restricted even when the domain names nothing; the account is then confined to its own domain 2 exempt May exchange mail with anyone An empty list, the default, means nothing runs, so an upgraded server delivers exactly as it did. Always allowed: the account's own domain under its own name or any alias domain, the domain's postmaster, the null sender MAIL FROM:<>, and mail this server writes itself. Other domains this server hosts are not allowed unless named. It is enforced in three places: Inbound, at RCPT TO. A restricted recipient whose envelope sender is not allowed is answered 550 5.7.1 <address>: this mailbox accepts mail from named domains only., per recipient, so one refused RCPT does not end the transaction. Every session is judged, authenticated or not. Outbound, at RCPT TO. A restricted submitting account is answered 550 5.7.1 <address>: this account may send to named domains only. for a recipient outside. The webmail's POST /api/v1/me/messages answers 400 with the sentence and queues nothing. At local delivery, for a recipient reached by expansion: a distribution list member, an alias target, a catch-all. The restricted recipient is dropped, and a non-delivery report goes to the sender only when the sender is local. A restricted account's forward - the account forward, an account rule's forward, a Sieve redirect - to a domain not allowed is not made, and the message is still delivered to the mailbox. Its out-of-office reply is not sent to a sender who is not allowed. Every refusal is a DEBUG log line; nothing goes to the error log, since a refusal is the feature working. Message recall within the server, since 6.3.4 Schema 6085. An unread message sent to accounts on this server can be withdrawn by its sender. Nothing does this across the Internet: a message that has left for another server is that server's. No recall notice reaches the recipients; the Sent copy's own mark is the record. What is recorded. A message accepted from one of the server's own accounts - an authenticated SMTP submission, credited to the account that signed in, or the webmail's send - gets one row in hm_recall per recipient that resolved to a local account. Nothing is recorded for: mail from outside, and mail this server writes itself; an unauthenticated submission from a local IP range in a local account's name, because a recall keyed on a claim would let anybody delete mail from other people's mailboxes; a message with a Message-ID longer than 255 characters, and a message that expires; the 201st and later recipient rows of one sender in a rolling day. The recall. In the webmail, Recall in the menu of a message in the Sent folder. For a program, POST /api/v1/me/recall with {"message_id": "<the Message-ID>", "sent_id": n}; it is refused with 403 on an app password. Each recipient is judged on their own: Outcome Meaning read Some live copy of theirs carries \Seen. Nothing was done: whoever has read it keeps it recalled No copy was read; every unread copy has been deleted the way the webmail deletes a message gone No copy of theirs is there any more: deleted by the reader, never delivered, or still in the queue failed The database did not answer; a later call decides them again The answer also counts elsewhere, the To and Cc addresses whose domain this server does not host, and says in sent_marked whether the Sent copy carries the keyword $Recalled. Read is \Seen and nothing more: a POP3 client, or an IMAP client that peeks, has the message and the recall cannot know. RecallDays (recall_days on PUT /api/v1/settings) is 14 by default, 1 to 365; a domain or an account may hold a shorter window, never a longer one. There is no COM property and no Control Panel page. What it does not do A route does not decide whether mail may go, and restricted delivery and a remote domain policy do not decide where. A per-policy SMTP port or credential is a route's job. Restricted delivery does not check the From header, does not filter what is already in the mailbox, and does not gate a journal rule's copy or an alias or list in the domain that reaches outside. Recall does not reach another server, does not touch the queue, and does not recall a message submitted without authentication. A 5xx from one smart host is not retried on the next. Where to read more Wiki: Routes and Relays, Settings Reference for the OutboundOAuth2* keys, APIs Reference, Diagnosing Stalled Mail Documents at the 6.3.4 tag: RemoteDomains.md, RestrictedDelivery.md, MessageRecall.md, Upgrading.md The public documentation: chapter 14, Routes and relays On this forum: Being trusted by the servers you send to, the 6.3.4 release announcement, requiring TLS and remote domain policies, and diagnosing stalled mail for a message that will not leave the queue
  • 0 Votes
    1 Posts
    133 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) holds a rule and a Sieve script to their owner: a domain's or an account's rule may no longer set From, Sender, Resent-From, Resent-Sender, DKIM-Signature or the loop count, and its Reply goes only from an address its owner may send as; a Sieve script may no longer add or delete those fields, Auto-Submitted or the loop count, and its notify goes from the account's own address unless its :from reaches the account. One stored before the upgrade still runs without that action or edit, and the application log names it. A loop count that is not digits alone, or is above the limit, reads as the limit, and RuleLoopLimit 0 now stops out-of-office replies too. See the 6.3.7 announcement. hMailServer filters mail with two systems that both run during delivery and do not know about each other. Rules are the server's own engine: criteria and actions, edited in the Control Panel, and the only one of the two that can run server-wide. Sieve (RFC 5228) is the standard mail filtering language, owned by the mailbox user and edited from a mail client over ManageSieve or on the Control Panel. This topic covers the order they run in, rules and the Run function action, Sieve and vacation, the out-of-office policy, and what both see on sealed mail. It describes 6.3.4 (27 September 2026, schema 6104). The one thing to know first: they run at different points, they see the message at different stages, and when they disagree about where a message goes, Sieve wins. The order they run in The filtering pipeline for one inbound message: During the SMTP conversation, while the sender is still connected. The pre-transmission spam tests (SPF, DNSBL, HELO, greylisting) run at MAIL FROM and RCPT TO; the script event OnSMTPData runs at the DATA command, before any of the body is sent; once the body is in come the post-transmission tests (SURBL, DKIM, ARC, SpamAssassin). A message over the delete threshold is refused, with 550 when the verdict came before the body and with 554 after it; with quarantine on, one judged after the body is held in quarantine instead and the sender answered 250. Otherwise the script event OnAcceptMessage runs and the message is queued. On the delivery thread, once per message and on every delivery attempt: OnDeliveryStart, blocked attachments and the virus scan, the global rules, OnDeliverMessage, then DKIM signing and mirroring (first attempt only). The message is split into local and external recipients. External recipients go to relays, routes and MX delivery. For each local recipient, on that recipient's own copy: per-account spam overrides, the account auto-reply, the account rules, account forwarding, trace headers, the Sieve script, the domain-wide auto-reply (only if the account said nothing), Sieve reject then discard, Sieve flags, the folder decision, and the save to the mailbox. Because It follows that Spam scoring happens during the SMTP conversation A rule cannot make a message spam; it can only read the flag, or a header the filter wrote Virus scanning happens before global rules A rule never sees an infected message Global rules run on the shared message, before the split Delete e-mail in a global rule destroys the message for every recipient; in an account rule, only that recipient's copy Global rules re-run on every delivery attempt Forward, Create copy and Reply can fire more than once for a deferred message. Write Delivery attempts equals 1 to prevent it Sieve runs after account rules A Sieve fileinto overrides the folder a rule chose. A header a rule set, or a copy it queued, has already happened and Sieve cannot undo it What a rule is Rules in the Control Panel sidebar is the server-wide set ("Global rules"); each account has its own on the Rules tab of its account dialog. A rule is a name, an enabled flag, a match mode - Match ALL criteria (AND) or Match ANY criterion (OR) - an ordered list of criteria and an ordered list of actions. Criterion What is compared Note From, To, CC The From:, To: and Cc: headers Not the envelope. Mail that reached you by Bcc, an alias or a list has no To: naming you Body The plain-text body concatenated with the HTML body A phrase inside an attachment is not matched Message size The file's size in bytes 25 MB is 26214400 Recipient list Every SMTP envelope recipient, joined with ; The field that sees a Bcc, an alias expansion or a list member Delivery attempts The retry count plus one The first attempt is 1 Custom header The named header's value Name it without the colon Match types. equals, does not equal, contains and does not contain are case-insensitive. is less than and is greater than parse both sides as numbers, and a non-numeric string parses as 0. matches wildcard and matches regex require the whole value to match; a regex is also case-sensitive, so to find a word anywhere write .*invoice.*, and (?i).*invoice.* to ignore case. does not equal and does not contain are true when the field is empty: Subject does not contain [ok] -> Delete e-mail deletes every message that has no Subject at all. Add a positive criterion under match mode ALL when the action is Delete. Action What it does Delete e-mail Global: the message is dropped and Message will be deleted. Action triggered by a global rule (<name>) is logged. Account: only this recipient's copy Forward e-mail Queues a second message to the named address, loop-count guarded Reply Queues a new message to the envelope sender, stamped Auto-Submitted; never answers auto-submitted mail Move to IMAP folder Happens at the end of local delivery. INBOX.Archive; #Public.Name for a public folder, subject to the insert permission; %YEAR%, %MONTH% and %DAY% come from the server's local date. A missing folder is created Run script function Calls a function in the event script - next section Stop rule processing Stops the rule set after this rule's remaining actions have run Set header value Rewrites the message file; %MACRO_ORIGINAL_HEADER% is the header's current value Create copy Queues a duplicate carrying X-CopyRule: <rule name> Send using route, Bind to address Force a named route, or the local address the connection leaves from. Absent from an account's rules The Run function action, since 6.3.4 The action the Control Panel calls Run script function calls the named function in the event script as <function>(HMAILSERVER_MESSAGE), then reloads the message from disk, so a script that edits and saves is seen by the rest of the rule set. It needs scripting enabled. Since 6.3.4 the name must be a plain identifier: ASCII letters, digits, _ and $, not starting with a digit. On Windows, in either script language (VBScript or JScript), any other name is refused as HM5018 each time the rule fires. Before 6.3.4 the name was written into the call unescaped, in either language, so whoever may edit a rule, a domain's own administrator included, could run code in the server's process: that is CVE-2026-100741, in every release from 6.0.0 to 6.3.3, and event scripts and the 6.3.4 security fix has the flaw and what to do until a server is upgraded. On Linux the action works since 6.3.4, where event scripts are JavaScript. The Linux engine looks a handler up by name instead of building a call, so such a name is never evaluated and finds no function: nothing is run, and the ERROR log has a Script Error line, not HM5018. Since 6.3.7 such a name is refused when the rule is saved, over COM and REST, for a rule of every owner; and the action on a domain's or an account's rule is the server administrator's to set, since it reaches a function of the server's event script: a credential confined to named domains - such as a domain administrator, or an API key issued for its domains - is refused it. Evaluation: a matching rule does not stop the set For each active rule, in order, the criteria are evaluated under the match mode and, if they matched, every action runs. Only a Stop rule processing action ends the set, so make that the last action of every rule that should be terminal. A rule with no criteria runs no actions, in either match mode. A catch-all rule needs a criterion that is always true. If the message file cannot be loaded, the whole set is skipped and the message is delivered unfiltered, with HM6040 in the log, rather than run against an empty message. When several choose a folder, a Sieve fileinto beats an account rule's folder, which beats a global rule's. The regular-expression breaker. A regex runs against a value the sender wrote, so it has a budget of 250 ms per evaluation. A pattern that will not compile is reported as HM6042; one that takes 250 ms or more as HM6043; either is suspended for 300 seconds, keyed on the pattern text, so a corrected pattern takes effect on the next message. A suspended pattern does not match. The same breaker covers Sieve's :regex, and suspension is shared. Loop protection. Every generated message carries X-hMailServer-LoopCount. Forward, Reply and Create copy check it against RuleLoopLimit (COM Settings.RuleLoopLimit; Mail flow & delivery → Delivery of e-mail → Rules, since 6.3.7 a card on the Rules page; default 5); at or above it nothing is generated and HM5064, HM5065, HM5201 or HM4404 is logged. An Auto-Submitted header stops Reply and bounce reports only; Forward deliberately ignores it. What else 6.3.4 changed for rules Three collections instead of two. A domain's rules (Domain.Rules, /api/v1/domains/{name}/rules) run after the server's. Account rules are on /api/v1/accounts/{address}/rules. Five more actions: Add a disclaimer, Reject with a text, Require TLS, Quarantine and Warn the sender. A legal hold keeps, among the account's deleted items, what an account rule's Delete or Reject, or a Sieve discard, reject or redirect that keeps no local copy, dropped. It does not reach the server's or a domain's rules: a Delete there drops the message for everybody, held accounts included. A domain's rules do not always act on the whole message. Mail flow rules and what a domain does to a message says when they act on all of it and when on the domain's own recipients only, what each new action does, and two 6.3.4 fixes to what a rule queues. Sieve: where a script lives and how it becomes active Each account has one active script, plus any number of named scripts. They are files, not database rows, under [Directories] DataFolder: <DataFolder>\ Sieve\ _global\ administrator-placed scripts, for include :global example.com\ alice\ active.sieve THE ACTIVE SCRIPT - this is what delivery runs vacation.responses who has already had a vacation reply, and until when duplicate.seen identifiers the duplicate test has seen scripts\ holiday.sieve named scripts, from ManageSieve PUTSCRIPT Four routes write that same active.sieve, and all four check the syntax first: Route Where Control Panel (Windows) Account dialog, Sieve tab COM (Windows) Account.SieveScript; no Save() is needed REST GET and PUT /api/v1/me/filters, as the account itself; 256 KB cap; an empty script removes the filter ManageSieve PUTSCRIPT then SETACTIVE Deactivating a script also clears that account's vacation-response records. ManageSieve is off by default: ManageSieveServerPort=0 means the listener is never created. Set the port (the wiki's example is 4190) and ManageSieveServerBindAddress on the ManageSieve (RFC 5804) card of the Control Panel's API & monitoring page on Windows (since 6.3.7 a ManageSieve tab of the Protocols page; Ctrl+K takes the old title to it), or on either platform with PUT /api/v1/settings/ini/{name} or hmctl ini set. Since 6.3.4 both wait for a reinitialise, not a restart: Apply now in the Control Panel or the Deck, or POST /api/v1/server/reinitialize. A script is at most 1 MB. With no certificate, STARTTLS is not advertised at all; with no certificate and no range policy, keep the listener on 127.0.0.1. What this server's Sieve accepts and refuses require accepts fileinto, envelope, copy, relational, subaddress, vacation, vacation-seconds, imap4flags, body, mailbox, regex, ihave, environment, date, index, spamtest, spamtestplus, duplicate, editheader, variables, reject, ereject, include and enotify, and the comparators comparator-i;ascii-casemap, comparator-i;octet and comparator-i;ascii-numeric. What matters in practice: fileinto :copy is refused at upload. This server stores exactly one local copy. Use :copy on redirect. redirect without :copy cancels the implicit keep, and the message leaves the mailbox entirely. imap4flags stores only the five system flags, \Seen \Answered \Flagged \Deleted \Draft. A keyword is accepted, logged, and stored nowhere. spamtest reads the server's internal flag, never a header a sender could forge: "0" when the message was not classified, "10" when it was. Any threshold from 1 to 10 behaves identically. duplicate keys on Message-ID over 7 days by default, and its store fails open, because a wrong "duplicate" destroys mail. reject and ereject send a non-delivery report with status 5.7.1 to whoever the envelope names. Against forged senders use discard. editheader refuses Received and Return-Path, and since 6.3.7 From, Sender, Resent-From, Resent-Sender, DKIM-Signature, Auto-Submitted and X-hMailServer-LoopCount, at upload and at delivery, where a script stored before the upgrade still runs without the edit; enotify accepts mailto: only, and since 6.3.7 a notify goes from the account's own address unless its :from is the account's own address or an active alias pointing at it; include nests at most three levels. The everyday script: require ["fileinto", "mailbox", "spamtest", "relational", "comparator-i;ascii-numeric"]; if spamtest :value "ge" :comparator "i;ascii-numeric" "5" { fileinto :create "INBOX.Junk"; stop; } if header :contains "List-Id" "announce.example.com" { fileinto :create "INBOX.Lists.announce"; stop; } Vacation, and the out-of-office policy require ["vacation"]; vacation :days 7 :subject "Out of the office until 22 September" :addresses ["[email protected]", "[email protected]"] "I am away until 22 September. For anything urgent please write to [email protected]."; :addresses is not decoration: a reply is sent only when one of the user's own addresses appears in the envelope or the headers, and that list is what "the user's own addresses" means. :days defaults to 7; vacation-seconds gives :seconds, and :seconds 0 means "reply every time". The window is per sender, in vacation.responses, and that store fails closed: if it cannot be read, no reply is sent. The account's Auto-reply tab and the domain's Out of office tab are the server's own reply, separate from Sieve. At most one reply answers any message. The account's own, when it is on and inside its dates, comes first; a domain's reply answers only for accounts that have none. If both an account-level auto-reply and a Sieve vacation are configured, the account-level one wins. Account Domain General text VacationSubject, VacationMessage VacationSubject, VacationMessage Text for colleagues VacationInternalSubject, VacationInternalMessage the same two Colleagues only VacationMessageInternalOnly leave the general text empty Strangers get the domain's text - VacationExternalOverride A sender is internal when the envelope sender resolves to an account hosted here or to an active alias. That is whatever the submitting client said, so an internal text must never carry anything that cannot be shown to a stranger. Each sender is answered once per absence; a window - VacationMessageDays on the account, VacationDays on the domain, 0 to 3650 - answers the same sender again after that many days. The memory is the table hm_autoreplies (schema 6060), so since 6.3.4 a restart no longer repeats every reply. Never answered: a null return path; Auto-Submitted other than no; Precedence: bulk, list or junk; anything with a List-* header; a sender that set X-Auto-Response-Suppress; postmaster and mailer-daemon; ourselves; and marked spam when VacationMessageAbortSpamFlagged says so; and since 6.3.7 a message whose X-hMailServer-LoopCount has reached RuleLoopLimit, so RuleLoopLimit 0 stops out-of-office replies as it stops every other generated message. Every reply goes out with a null return path and Auto-Submitted: auto-replied. What rules and Sieve see on sealed mail A message encrypted end to end (S/MIME or PGP/MIME) is opaque: the server holds no key to it. How the server decides that, and what the rest of the server does with such a message, is in end-to-end encryption and sealed mail. For rules and Sieve: A rule's body criterion on an opaque message does not match, whichever way it is phrased, and one line per rule per message in the log says so. Under AND, a rule with a body criterion does not fire; under OR, the other criteria are still evaluated. Before 6.3.4, does not contain matched every encrypted message. A multipart/signed message is read as it always was. To catch encrypted mail, test the Content-Type header for multipart/encrypted or application/pkcs7-mime with a custom-header criterion. Sieve's evaluation is unchanged; a log line is new. An encrypted message has no text part, so body :contains "x" is false and not body :contains "x" is true, and the delivery writes a log line saying so, once per message and recipient. body :raw is the undecoded body exactly as it arrived, so if body :raw :contains "BEGIN PGP MESSAGE" is the honest way a script can recognise a sealed message. What it does not do Sieve cannot run server-wide, force a route or bind an outbound address. A rule's Delete e-mail is silent. The sender was already answered 250. To tell the sender, use Sieve reject, or since 6.3.4 the Reject with a text action. A journal rule does not see a rule's forward, copy or reply, or a Sieve redirect: the server queues those itself. Where to read more Wiki: Rules and Sieve, The COM API and Scripting Documents at the 6.3.4 tag: OutOfOffice.md, SealedMail.md, MailFlowRules.md, EventScripts.md The public documentation: chapter 12 and the rules cookbook On this forum: the 6.3.4 release announcement, the Linux guide, mail flow rules and domain rules, event scripts, and when a saved setting takes effect for the ManageSieve keys
  • 0 Votes
    1 Posts
    131 Views
    ProgressiverobotP
    An organisation that keeps its people in an identity provider wants two things of its mail server: mailboxes that follow the directory, and a sign-in that goes through it. Since 6.3.4 hMailServer has both. SCIM 2.0 at /scim/v2 lets Microsoft Entra ID or Okta make, rename, suspend and remove accounts, with their aliases and distribution lists. Single sign-on, by OpenID Connect or SAML 2.0, opens the webmail at /portal and the Control Deck at /. The one thing to know first: they answer different questions and take different credentials. SCIM answers to an API key whose role holds provision and to nothing else. Single sign-on opens the two pages only: IMAP, POP3 and SMTP still take the account's password and its application passwords. Passkeys and account recovery are covered in the topic on signing in. How the pieces fit Piece Direction What it does LDAP directory authentication This server asks the directory Checks a password when somebody signs in SCIM provisioning The identity provider calls this server Says what changed, as it changes Single sign-on The browser goes to the identity provider and back Signs a person in to the webmail or the Control Deck oidc_auto_provision, saml_auto_provision In passing, at sign-in Makes a mailbox for whoever arrives with a verified address, and never touches it again SCIM is the deliberate version of auto-provisioning. The two work together: SCIM keeps the mailboxes, and single sign-on is how the people it made sign in to them. Where the settings are. The single sign-on settings are the REST API's and the Control Deck's. There is no COM property and no Control Panel page for them, on Windows or on Linux. SCIM: the key and the permission A SCIM request is a request of the REST listener. RestApiPort has to be on and the listener reachable from the identity provider's service over TLS, with a certificate it trusts (RestApiCertificateFile, or the ACME certificate). A body is at most 64 KB, and the key's requests count against the listener's budget of 200 in ten seconds per credential, beyond which the answer is 429 with Retry-After. The key's role must hold provision. The built-in serveradmin role holds it, so every key issued with "scope":"full" provisions; no other built-in role does, so a read-only key is answered 403. The way to issue a key that provisions and does nothing else is a custom role: curl -u Administrator:your-admin-password -H "Content-Type: application/json" \ -d '{"name":"entra-provisioning","description":"Microsoft Entra ID","permissions":"provision"}' \ https://mail.example.com:8443/api/v1/roles curl -u Administrator:your-admin-password -H "Content-Type: application/json" \ -d '{"label":"Entra provisioning","role":"entra-provisioning"}' \ https://mail.example.com:8443/api/v1/apikeys The second request, POST /api/v1/apikeys, answers the hmapi_... token once, and that token is the identity provider's secret. provision is needed for SCIM's reads as well as its writes. Both requests present the administrator password, which works while no second factor is enrolled on it. With one enrolled, sign in once at POST /api/v1/session with the code in X-hMailServer-OTP, and send both requests with the hmailsession cookie and X-Requested-With: hMailServer: since 6.3.4 a one-time code is taken once, so a second request carrying the same code within the same half minute is refused with 401. Every other credential is refused. The administrator password, an account's password and a browser session are answered 401 on /scim/, in SCIM's shape, with WWW-Authenticate: Bearer. A key confined to named domains ("domains":"example.com") provisions those domains and learns nothing about the rest: listings are filtered, an account elsewhere is the same 404 as one that is not there, and an address elsewhere is refused 400 invalidValue. A key may not change or remove an account whose authority it does not take in, and is answered 403. A key whose role holds provision alone takes in no administrator at all: not a domain administrator, not a help desk, not an auditor. A full key confined to named domains takes in an administrator of those domains and nobody else. A full key issued for every domain takes in every account. SCIM: the routes and the mapping Route What it does GET /scim/v2/ServiceProviderConfig What this server supports: patch, filter (200 at most), changePassword and etag; not bulk and not sort GET /scim/v2/Schemas, GET /scim/v2/ResourceTypes The User and Group schemas, listing only what this server keeps; the two resource types GET, POST /scim/v2/Users The accounts: listed and filtered, or one made GET, PUT, PATCH, DELETE /scim/v2/Users/{id} One account: read, replaced, changed, removed GET, POST /scim/v2/Groups The distribution lists: listed and filtered, or one made GET, PUT, PATCH, DELETE /scim/v2/Groups/{id} One list: read, replaced, changed, removed Every answer under /scim/ is application/scim+json, the refusals included. A User is an account. SCIM This server id, userName The account's address. The id is the address, so a rename changes the id. name.givenName, name.familyName The first and last names, sixty characters each displayName The two names composed. Written alone to an account that already has a name, it is accepted and not kept. active The account's switch. false suspends it and deletes nothing. emails The address, type work and primary, and every alias whose value is the address, type other password Write-only. Judged by the password policy, the reuse history and, when it is on, the breach check. externalId The identity provider's own id for the person, at most 255 characters (schema 6096) meta.created and meta.lastModified are not answered, because the tables keep no such times. A Group is a distribution list: id and displayName are the list's address, and members are its members. The sharing groups at /api/v1/groups are folder-permission principals and are not SCIM groups. Attributes the schemas have and this server does not keep, such as title, phoneNumbers and the enterprise extension's department and manager, are accepted and not kept. A misspelt attribute or path is refused by name. SCIM: what each operation does Making a user. POST /scim/v2/Users needs a userName at a domain this server hosts and the key reaches. A body with no password, which is what Entra sends, makes the account with thirty-two random bytes for one, which nobody is ever told. An address something already has is 409 uniqueness. Filters. These and no others: userName eq, userName co, userName sw, externalId eq, emails.value eq and active eq. Any other filter is 400 invalidFilter. count is at most 200, and sortBy and sortOrder are ignored. Groups filter on displayName eq only. Changing. PUT takes the body a POST takes, and an attribute it leaves out is left as it is. PATCH takes a PatchOp, with op in any case, as Entra capitalises it. If-Match is honoured on PUT, PATCH and DELETE; a version other than the current one is 412. A change is judged whole before anything is written. Suspending. active false refuses every sign-in from the next attempt, refuses mail to the account, ends its browser sessions and drops its live IMAP, POP3 and SMTP connections. The mailbox, the aliases and the lists are untouched. Renaming. A new userName renames the account through the mailbox move. It is refused 409 while the mailbox is in use. The id changes, the old id is 404 from then on, and the old address is kept as an alias until a later request that sends emails leaves it out. Every alias of the account is part of emails, including one an administrator made. An identity provider that replaces emails removes the aliases it does not list. Where aliases are managed on this server and not in the directory, do not map emails beyond the work address. Removing. DELETE /scim/v2/Users/{id} removes the account and its mailbox, every alias emails answered for it, and its row in every distribution list. It answers 204. Nothing of it can be put back but from a backup. Entra sends active false straight away and the DELETE thirty days after a user is soft-deleted, so the suspension is the safe first step. Five diagnostics in the error log, HM7220 to HM7224, name what a SCIM write left behind when it failed part way. Every SCIM write is on the audit trail, each row naming key:<id>. Setting up Microsoft Entra ID for SCIM Issue the key as above and note the token. In the Entra admin centre, Enterprise applications > New application > Create your own application > Integrate any other application you don't find in the gallery (Non-gallery). Provisioning > Automatic. Tenant URL: https://<host>:<port>/scim/v2, where the port is RestApiPort. Secret token: the hmapi_... token. Test connection asks for a user that does not exist and expects an empty list, which is what this server answers. Mappings > Provision Microsoft Entra ID Users: map userName from userPrincipalName where that is the mailbox address, or from mail where it is not. emails[type eq "work"].value must be the same address as userName. Keep active, name.givenName, name.familyName, displayName and externalId, and delete the mappings this server does not keep. Mappings > Provision Microsoft Entra ID Groups, for mail-enabled groups that should be distribution lists: map displayName from mail, keep members, and delete the mapping of externalId. Assign the users and groups, and switch Provisioning Status on. Setting up Okta for SCIM Issue the key as above. Applications > Create App Integration > SAML 2.0 or SWA, then on General enable SCIM provisioning. Provisioning > Integration: SCIM connector base URL https://<host>:<port>/scim/v2; Unique identifier field for users userName; Supported provisioning actions: Import New Users and Profile Updates, Push New Users, Push Profile Updates, Push Groups; Authentication Mode HTTP Header, and the token in Authorization. Provisioning > To App: enable Create Users, Update User Attributes and Deactivate Users. Push Groups sends a group under its Okta name as displayName, and here that is the list's address, so a push of Engineering is 400 invalidValue. Name each pushed group with the list's address, or leave Push Groups off. Okta sends emails with the primary address alone, so an alias made on this server is removed by Okta's next update. Scim.md gives no steps for Google Workspace: at the time it was written, a custom application there could not be pointed at a SCIM endpoint. Single sign-on with OpenID Connect Register this server at the issuer as an authorization-code client with PKCE, with the redirect URI <oidc_redirect_base_url>/portal/oidc/callback. Setting What it is oidc_issuer For example https://login.example.com. The discovery document is read from <issuer>/.well-known/openid-configuration and cached for an hour. Empty offers no single sign-on at all. oidc_client_id The client id this server is registered under oidc_client_secret Its secret, stored protected and never emitted. Empty for a public client. oidc_scopes openid email by default oidc_redirect_base_url The address the webmail is reached at from outside. Empty falls back to import_oauth_redirect_base_url. oidc_auto_provision Whether a verified address with no mailbox here gets one. Off. oidc_administrators The addresses that open the Control Deck as the administrator. Empty means single sign-on reaches the webmail only. The flow is GET /portal/oidc/start[?deck=1] to the issuer and GET /portal/oidc/callback back. The callback verifies the id_token: its signature against the issuer's published keys (RS256 and ES256 only), iss, aud, exp, iat and nbf with a minute of skew, and the nonce. It then takes email, and only when email_verified says the issuer verified it. With oidc_issuer set, both sign-in pages draw a Sign in with button naming the issuer's host. What they draw is decided by GET /api/v1/signin-methods, which needs no credential and says nothing about who exists here. With SAML set up it answers saml: true and saml_label, and both pages draw a second button. The callback does not make the session. It mints a one-use ticket, good for two minutes, and the page hands it to POST /api/v1/session, which is where every browser session is made. The ticket is bound to the browser by an hmsso cookie, and POST /api/v1/session refuses a ticket that arrives without it. Mapping. The verified address is looked up as a mailbox. With auto-provisioning off, the default, a sign-in with no mailbox is refused. With it on, a mailbox is made, but only at a domain this server already has, and with a random password nobody is told. For the Control Deck, the address must be in oidc_administrators. An issuer that says somebody is an administrator is not obeyed. Single sign-on with SAML 2.0 For identity providers that speak only SAML 2.0: ADFS above all, and an Entra ID or Okta application set up the SAML way. It ends in the same ticket, the same POST /api/v1/session and the same mapping rules. Setting What it is saml_idp_entity_id The identity provider's entity ID. Empty, the default, offers no SAML sign-in at all. saml_idp_sso_url Its sign-in URL for the HTTP-Redirect binding saml_idp_certificate Its signing certificate in PEM, or two while it rolls its key over. Write-only. saml_sp_entity_id This server's entity ID. Empty means <base URL>/saml. saml_email_attribute The attribute the address is read from when the NameID is not an email address saml_require_signed_assertion Whether the Assertion must carry its own signature. On by default. saml_auto_provision, saml_administrators As the OpenID Connect settings of the same names saml_button_label The words on the button: Sign in with SAML by default The base URL is OpenID Connect's: oidc_redirect_base_url, falling back to import_oauth_redirect_base_url. From it follow the entity ID <base URL>/saml, the ACS URL <base URL>/portal/saml/acs and the metadata at <base URL>/saml/metadata. Identity provider What to enter there What to copy back Microsoft Entra ID Identifier (Entity ID) <base URL>/saml; Reply URL <base URL>/portal/saml/acs; token encryption off Login URL to saml_idp_sso_url; Microsoft Entra Identifier to saml_idp_entity_id; Certificate (Base64) to saml_idp_certificate Okta Single sign-on URL <base URL>/portal/saml/acs; Audience URI (SP Entity ID) <base URL>/saml; Name ID format EmailAddress; Assertion Encryption Unencrypted Identity Provider Single Sign-On URL to saml_idp_sso_url; Identity Provider Issuer to saml_idp_entity_id; X.509 Certificate to saml_idp_certificate ADFS A claims-aware relying party trust, imported from <base URL>/saml/metadata or entered by hand; E-Mail-Addresses sent as E-Mail Address https://<adfs host>/adfs/ls/ as saml_idp_sso_url; the Federation Service Identifier as saml_idp_entity_id; the Token-signing certificate as saml_idp_certificate The flow. GET /portal/saml/start[?deck=1] to the identity provider, with an AuthnRequest, and the hmsaml cookie POST /portal/saml/acs back from it, with the Response GET /portal/saml/finish on from the ACS, where the cookie is asked for GET /saml/metadata this service provider, for the identity provider The ACS takes a form of at most 256 KB; a larger body is answered 413. The signature is verified against the certificates an administrator stored and never a key the message carries. The signature method must be rsa-sha256 or ecdsa-sha256, and SHA-1 is refused. A Response is taken only when it answers a request this server issued, not yet answered, with the same RelayState. The checks refuse signature wrapping, a replayed Response or assertion, a Response nobody asked for and an encrypted assertion; comment injection in the NameID gains nothing, because the address read is the whole of what was signed. A saml_idp_certificate that cannot be read stops every SAML sign-in. The settings refuse one, so it comes only from a database edited by hand; HM7320 in the error log then names the setting to paste again. A SAML identity provider says nothing like email_verified. The attribute the address is read from must be one the directory controls, such as mail or the user principal name, and never one a person can set themselves. This server cannot check that. What it does not do SCIM: no bulk, no sorting and no POST .search (/Bulk, /Me and POST .search answer 501); no enterprise user extension; no groups but distribution lists; not a SCIM client. externalId is shown by nothing but SCIM and COM's Account.ExternalId, which is Windows-only. Single sign-on, both kinds: no logout in either direction, so an account disabled at the identity provider keeps a session it already started until that session's own ceiling ends it. One OpenID Connect issuer and one SAML identity provider. Nothing is enforced at IMAP, POP3 and SMTP. OpenID Connect: no claims mapping. The email claim and nothing else. SAML: no encrypted assertions, no signed requests, no metadata fetch, no artifact binding and no sign-in started by the identity provider. Where to read more The operator documents: Scim.md, PasskeysAndSingleSignOn.md and Roles.md. The wiki: The REST API and APIs Reference. The public documentation: chapter 24, The REST API and chapter 36, The Control Deck and the webmail portal. On this forum: passkeys, recovery and the rest of sign-in are in Signing in: passwords, second factor, passkeys, single sign-on; LDAP sign-in and directory synchronisation are in Active Directory and LDAP sign-in; The Control Deck; the 6.3.4 announcement; and the REST API guide.
  • 0 Votes
    1 Posts
    122 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) refuses a state-changing REST request that carries a credential when the browser making it reports it as cross-site (Sec-Fetch-Site, or an Origin that is not this server's), so a page on another site cannot post to these routes with a credential, whether a key or the administrator password the browser holds; a program or script that sends no such header, as the curl examples here do not, is unaffected. See the 6.3.7 announcement. hMailServer 6.3.8 (2 November 2026) answers 400, saying why, before a route reads a member, to a body that is not one JSON object, names a member twice, holds an escaped U+0000 or passes 100,000 values or 32 levels, so correct a program that sends such a body to these routes; and it raises account.locked only for a locked name that is an account's own. See the 6.3.8 announcement. Since 6.3.4 hMailServer has three ways for another program to work with the server's mail: a send API, through which an application sends as one sender and asks afterwards what became of each recipient; mail in to a webhook, where the mail for an address or a whole domain is posted to a URL; and outbound webhooks, which tell a program what the server's mail did. Nothing is on until a key, a route or a subscription is made. The one thing to know first: each of the three takes a different credential, and the difference is deliberate. Feature Routes Who may use it The send API /api/v1/send An API key that names a sender and whose role holds send_through_api. Not the administrator password, not an account. Mail in to a webhook /api/v1/inbound-webhooks view_configuration to read, manage_inbound_webhooks to change. Meant for an API key's role, confined to the application's domain. Outbound webhooks /api/v1/webhooks The administrator password, or an account whose role holds the permission. No API key reaches these routes; the refusal is 401. All three are made and managed over the REST listener, so RestApiPort has to be on; outbound webhook subscriptions can also be managed over COM, on Windows. The send API: the key POST /api/v1/me/messages is the webmail's send and stays the webmail's. The send API is for programs. Route What it does POST /api/v1/send Sends a message as the key's sender. 202 {queue_id, message_id, recipients, recorded} GET /api/v1/send The key's own sends, newest first, paged GET /api/v1/send/{queue_id} One of them, with the state of each address its body named A key sends when two things are true of it: its role holds send_through_api, and it names a sender. Either alone sends nothing. Three fields are set when the key is made, on POST /api/v1/apikeys with the administrator password or on the Control Deck's API keys page. Field What it is send_from The one sender the key may use: an address of a domain this server hosts, or *@domain for any address of that domain. Empty, the default, means the key cannot send. send_per_minute The key's own ceiling on messages a minute. 0, the default, sets none. send_per_day The key's own ceiling on messages in any 24 hours. 0 sets none, which only a key whose sender is an account's address may have. The built-in serveradmin role holds the permission, so a key made with "scope": "full" holds it. No other built-in role does. A custom role is how a key that sends and does nothing else is made: POST /api/v1/roles {"name": "mailer", "description": "sends mail", "permissions": "send_through_api"} POST /api/v1/apikeys {"label": "shop confirmations", "role": "mailer", "send_from": "[email protected]", "send_per_day": 5000} The route refuses with 400, among other things, a send_from at a domain this server does not host (a domain alias does not count), at a domain that is switched off, on a read-only key, or outside the domains a key is confined to. A send_from that is *@domain, or an address that is no account's, needs a send_per_day above 0, because such a sender has no account's sending quota to hold it. A key's sender cannot be changed afterwards: a new sender is a new key. What *@domain grants. To the server, such a key is every mailbox of that domain sending as itself. It can post to a members-only list as any member and send as the domain administrator's mailbox. The display name and reply_to are the application's own and are not checked against anything; what the key vouches for is the address. On Windows, the desktop Control Panel's key page does not show these three fields and cannot set them, so a key made there cannot send. The Control Deck and the route do. The send API: sending and asking afterwards curl https://mail.example.com:8443/api/v1/send \ -H "Authorization: Bearer hmapi_0123…" \ -H "Content-Type: application/json" \ -d '{"to":["[email protected]"],"subject":"Your order","text":"Thank you."}' Member Rule from Optional when the key's sender is an address, required when it is *@domain. "address" or "Name <address>". to, cc, bcc Arrays of mailboxes; at least one recipient between them. Bcc goes to the envelope and nowhere else. reply_to Optional. subject Required; it may be empty. text, html At least one. Both makes a multipart/alternative. headers Optional: X- fields other than the server's own X-hMailServer- fields, List-Unsubscribe, List-Unsubscribe-Post, In-Reply-To and References. Any other field is 400. attachments Optional: [{filename, content_type, data}], data in base64, at most 20 and twelve megabytes together. A line break is refused with 400 in every value that becomes a header, and a member the route does not take (body for text, for example) is 400 and named, not dropped. What the server does to the message. It is written and checked by the same code as the webmail's send. Every recipient goes through what RCPT TO asks of an authenticated sender, with the server's recipient ceiling (MaxSMTPRecipientsInBatch) and the suppression list. The domain's signature and disclaimer are added, the rules that block are asked (403, policy_rejected), and the message is held to the maximum message size (413). It is DKIM-signed at delivery by the domain's key. The envelope sender is the From address, so a bounce comes back to it. A Received field names the server, the send API and the key's label, never the key's id. No Sent copy is kept anywhere. When the sender is an account's address, that account's sending quota is consumed as an SMTP submission would consume it. Ceiling Answer send_per_minute 429, Retry-After: 60 send_per_day 429, Retry-After: 3600 The sender account's sending quota 429, Retry-After one twenty-fourth of the account's period The REST request budget, 200 requests in 10 seconds 429, Retry-After: 10 The recipient ceiling, the size 400, 413 A message is counted only once every other check has passed, and when a later ceiling refuses, every earlier one is given back. A refused message is never a counted one. The answer to a message that is taken is 202: {"queue_id": 4711, "message_id": "<…@mail.example.com>", "recipients": 2, "recorded": true} queue_id is the one to ask about afterwards with GET /api/v1/send/4711. The answer names only the addresses the body named, each with a state. State Meaning queued In the queue and not yet tried deferred An attempt failed and will be retried delivered The trace's delivered event bounced Refused for good, or the retries ran out refused The suppression list stopped it at delivery unknown Nothing names the address; the answer says why It needs the message trace, and the trace is off by default (MessageTraceEnabled), because it records who corresponds with whom. With it off, a send reads queued or deferred while it is in the queue and unknown once the queue has let it go; trace in the answer is false and a trace_note says so. Turn the trace on, and choose its retention, before relying on the answer. The record of a send is kept for MessageTraceRetentionDays, thirty by default. Only a send this key made is answered; any other id is the same 404. The backup does not carry the records. Mail in to a webhook: a route SendGrid and Mailgun call this inbound parse. A route is made over REST or on the Control Deck's Inbound webhooks view. There is no COM member and no Control Panel page, on either platform. POST /api/v1/inbound-webhooks { "address": "[email protected]", "url": "https://helpdesk.example/hooks/mail", "secret": "a-long-random-string" } Field What it is Default address [email protected], or example.com for every address of the domain. The domain must be one this server hosts. required url https:// to the endpoint; plain http:// only to this machine. Printable ASCII only. required secret Write-only. Every request is signed under it and it is never returned. required deliver false: the route alone receives the mail. true: what the address reaches without the route receives it as well. false on_failure keep or bounce keep format form (multipart/form-data in SendGrid's field names) or json form attachments include, names or none include include_raw Also send the message as it arrived false max_size_kb A larger message is not posted. At most 81920. 10240 enabled A route switched off is as though it were not there for new mail true A route decides that the address exists. RCPT TO is accepted for a routed address whether or not an account, an alias or a list has it, and a route is asked before any of them. With deliver off, the route takes the mail instead of whatever has the address, so a list whose address a route takes never sees the message. Route a list's domain with care. An address route wins over its domain's, and a recipient with a plus tag ([email protected]) is found by the address as written, then by the address before the tag, then by the domain, with the tag kept in what is posted. What arrives is a signed POST: POST /hooks/mail HTTP/1.0 Content-Type: multipart/form-data; boundary=hMailServer-4193-... X-hMailServer-Timestamp: 1789000042 X-hMailServer-Signature: sha256=<hex> X-hMailServer-Event: message.inbound X-hMailServer-Delivery: 4193 X-hMailServer-Attempt: 1 X-hMailServer-Delivery is the same on every attempt. The form format carries SendGrid's fields (headers, from, to, cc, subject, text, html, envelope, sender_ip, SPF, dkim, charsets, attachments, attachment-info, attachment1 onward) with a few of this server's own beside them (spam, dmarc, route, queue_id, attachments_left_out). Mailgun's field names are not sent. The json format is this server's own shape. The SPF, dkim and dmarc results are the ones this server wrote into its own Authentication-Results field, and only while AuthenticationResultsEnabled is on. Retries and what is kept. The message is taken after the global rules, the virus scan and the sandbox have had it, and nothing is posted from the delivery thread. Any 2xx delivers. Anything else, a redirect included, is a failure. The retries are the outbound webhooks': WebhookMaxAttempts attempts (5 by default), with waits of 0, 1, 5, 15 and then 60 minutes. When they run out: keep: the message is kept and never swept. It can be read with GET /api/v1/inbound-webhooks/deliveries/{id}/message, tried again with POST /api/v1/inbound-webhooks/deliveries/{id}/retry or removed with DELETE /api/v1/inbound-webhooks/deliveries/{id}. The error log says so once, as HM7401. bounce: the message is returned to its sender with a delivery-status report (4.4.7), without repeating what the endpoint answered. HM7402 records the return. Where the sender cannot be told, the message is kept instead. A message larger than the route's ceiling is not posted at all: it is returned at once (5.3.4) or kept at once (HM7403). Delivered and returned rows are removed after WebhookRetentionDays (7 by default), and the rows survive a restart. Where an endpoint may be depends on who made the route. The server's administrator may name an address only this server can reach, but never a link-local or cloud metadata address (169.254.0.0/16, fe80::/10 and the clouds' own metadata addresses). Every other credential, an API key of any scope or role included, may name a public address only. Both are judged when the route is saved and again at every connection. No redirect is followed and HttpProxy is never used. Who may. Only serveradmin holds manage_inbound_webhooks as shipped, since routing a domain's mail to a URL is as much as reading all of it. A reader without it is answered a route's URL as its scheme and host alone, so put a token in the secret, not in the URL. Reading a kept message needs view_mailbox as well. POST /api/v1/inbound-webhooks/run makes one pass now and needs a credential that reaches every domain. Outbound webhooks An outbound webhook does not carry the message. No body, no attachments, no header block. It is a notification. The one piece of content it can carry is the subject, and only where the domain's webhook_subjects and the subscription's include_subject are both on; both are off by default, and even then a subject travels on message.received and message.quarantined only. Event Raised when message.received The message has been saved and the server has taken responsibility for it, once per local recipient message.delivered A recipient, local or remote, has been delivered to message.bounced A non-delivery report has been written and queued for the sender, once per failed recipient message.quarantined The held copy and its row exist account.locked Repeated authentication failures have locked a name A delivery attempt that failed and will be retried is not a bounce, and no event says deferred. POST /api/v1/webhooks { "name": "ticketing", "url": "https://tickets.example/hooks/mail", "secret": "a-long-random-string", "events": "message.bounced,account.locked", "domains": "example.com", "max_per_minute": 120 } An empty events, domains or addresses list means every one. A subscription confined to a send API key's sender ("addresses": "[email protected]") hears message.delivered and message.bounced for the recipients outside this server, with the message trace on or off. Plain http off this machine is refused when the subscription is made. The secret is write-only, protected at rest, and an empty secret in a change keeps the one that is set. Permissions. Reading needs view_configuration; deleting needs manage_settings; creating, changing, testing and running a pass need manage_server, because whoever can do those can make the server connect to a host of their choosing. A role confined to named domains is refused all eight routes. The queue. The delivery pass runs once a minute, attempts at most 200 rows and runs for at most 60 seconds. A delivery succeeds on any 2xx; a request that has taken more than ten seconds is a failure. After WebhookMaxAttempts the row is dead-lettered and the error log says so once as HM6971. Past a subscription's ceiling per minute, events are dropped and counted as dropped, not queued. Setting REST key Default WebhooksEnabled webhooks_enabled on WebhookMaxAttempts webhook_max_attempts 5 WebhookMaxPerMinute webhook_max_per_minute 60 WebhookRetentionDays webhook_retention_days 7 WebhooksEnabled silences the outbound webhooks and not the inbound routes. From a shell: hmctl webhook create --set name=ticketing --set url=https://tickets.example/hook \ --set-string secret="$SECRET" --set events=message.bounced --set domains=example.com hmctl webhooks test --event message.bounced --domain example.com hmctl webhooks run hmctl webhooks deliveries --state dead-lettered webhooks test raises a real event through the path a real one takes and says how many subscriptions took it, so 0 is an answer. In PowerShell the functions are Get-HmWebhook, New-HmWebhook, Set-HmWebhook, Remove-HmWebhook, Get-HmWebhookDelivery, Invoke-HmWebhookDelivery and Test-HmWebhook. Over COM, on Windows, Utilities.GetWebhookSubscriptions(name) and Utilities.SaveWebhookSubscription(json) take and answer the same JSON documents. Verifying a signature One recipe signs all of them: the outbound webhooks, the inbound routes and the webhook of an alert rule. The signature is HMAC-SHA256 over the timestamp, a full stop, and the raw request body, under the shared secret, as lower-case hex, in X-hMailServer-Signature. Verify it against the bytes received, before parsing them. import hmac, hashlib, time def verify(secret, headers, raw_body): timestamp = headers["X-hMailServer-Timestamp"] signature = headers["X-hMailServer-Signature"] # Refuse anything older than five minutes, or the signature can be replayed. if abs(time.time() - int(timestamp)) > 300: return False expected = hmac.new(secret.encode(), (timestamp + ".").encode() + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest("sha256=" + expected, signature) Compare in constant time. Check the timestamp: a signature with no freshness check can be replayed. De-duplicate on X-hMailServer-Delivery together with X-hMailServer-Event. A retry is the same body with a fresh timestamp and so a fresh signature, and an inbound and an outbound delivery can carry the same number. To rotate a secret, PUT the new one and accept either signature at the receiver while the change is made. There is no overlap window in the server. The documents also give the recipe in JavaScript. An alert's webhook carries three of the headers: X-hMailServer-Timestamp, X-hMailServer-Signature and X-hMailServer-Event. What they do not do The send API has no templates, no substitutions, no scheduled send and no per-recipient ceiling of the key's own, and gives no state without the trace once a message has left the queue. An inbound route makes one post per route per message, not one per recipient. It has no setting of its own and no per-route retry ladder. An outbound webhook does not carry the message and has no per-subscription retry policy. Neither kind of webhook is in the configuration backup. Export the routes with GET /api/v1/inbound-webhooks and the subscriptions with hmctl webhook list; the secrets are in no read. One pass at a time is per process, not per database, for both kinds of webhook. Where to read more The operator documents: SendApi.md, InboundWebhooks.md, Webhooks.md and, for the alerts' webhook, AlertsAndAuditTrail.md. The wiki: Mail In to a Webhook and The REST API. The public documentation: chapter 24, The REST API. On this forum: The Control Deck, the 6.3.4 announcement, the REST API guide and hmctl and the PowerShell module; turning the message trace on is in Troubleshooting by symptom, and the alert rules whose webhook is signed the same way are in Alerts, the audit trail, syslog and the journal.
  • 0 Votes
    1 Posts
    114 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.6 (6 October 2026) carries the PowerShell module, HmailServer.psd1 and HmailServer.psm1, Authenticode-signed, as files of the release, and Linux packages for x86-64 and AArch64; hmctl as a PyPI package and the module on the PowerShell Gallery are built but not yet published. See the 6.3.6 announcement. hMailServer 6.3.7 (17 October 2026) carries the module the same way, and since 18 October 2026 the repository and its releases are also mirrored on GitHub, issues and merge requests staying on GitLab. 6.3.7's REST API refuses a state-changing request that carries a credential when a browser reports it as cross-site, which hmctl and the module, sending no such header, never meet, and it refuses a key restricted to named domains a domain's rename, a domain alias and a custom role's creation, change or delete. See the 6.3.7 announcement. Since 21 October 2026 hmctl is on PyPI as hmailserver-cli and the module on the PowerShell Gallery as HmailServer, each at 6.3.7. hMailServer 6.3.8 (2 November 2026) carries the module as files of the release, as before, and both are sent 6.3.8 by a job started by hand once the release is published. 6.3.8's server answers 400 to a body that is not one JSON object or names a member twice, so a body given to hmctl api or --from-json must be one JSON object that names each member once, and 6.3.8's hmctl refuses a body holding NaN or Infinity before sending it. See the 6.3.8 announcement. hMailServer 6.3.4 has two command-line clients: hmctl for any shell, and the HmailServer module for PowerShell. They share one vocabulary and one API, so both administer a Windows server, a Linux server or a server on another machine, and neither needs COM or the Control Panel. This guide covers where each comes from, the credentials they take, the verbs, the import of accounts from a CSV file with its dry run, the configuration as one document, and what neither client does. The one thing to know first: both speak the REST API, so the REST listener has to be on before either can do anything, and neither client can turn it on. Neither is a replacement for the administration programs. The Windows Control Panel is the place to configure a server on Windows, and the browser Control Deck is that place on Linux. A command line is for what a command line is better at: making fifty accounts from a spreadsheet, reading one setting in a script, watching the delivery queue, or doing the same thing to twenty servers. Where the clients come from File What it is hmailserver/source/Tools/Cli/hmctl One Python 3 file with no dependencies beyond the standard library. hmailserver/source/Tools/Cli/HmailServer.psm1 The PowerShell module: Get-HmDomain, New-HmAccount, Set-HmSetting and the rest, answering objects rather than text. build/check-cli.py Runs both against a recorded server in CI and asserts what each one sent, so the two cannot drift apart. On Linux, the packages install hmctl as /usr/bin/hmctl. The 6.3.4 packages are x86-64 only, and the changelog records Python 3 as one of the .deb's two Recommends, the other being 7-Zip. On Windows, the wiki says the installer carries neither client: take them from the repository directory hmailserver/source/Tools/Cli/. Both clients are in the repository since 6.3.4. build/hmconfig.ps1 is a different tool and stays. It exports the whole configuration as one JSON document and applies it to another machine over COM, on Windows. The two clients here are per-object verbs over HTTP. Before anything else: the listener On Linux, store the port on the server itself and restart the service. Since 6.3.4 the database is the settings store, so editing the [Settings] section of hMailServer.ini does not turn the listener on once the server has stored the key. sudo hmailserver --set-setting RestApiPort=8080 sudo systemctl restart hmailserver On Windows, the Control Panel does this over COM. On either platform, TLS is required unless the listener is bound to the loopback, so give it a certificate the client trusts if it is to be reached from another machine. The REST API guide has the listener's settings, the reinitialise they wait for and the three ways it refuses to start, and the hmailserver command line on Linux has the checks --set-setting makes and how it takes a secret. Credentials Either the administrator password, sent as HTTP Basic with the user name Administrator, or an API key, sent as a bearer token. An API key is the better answer for anything unattended: it can be read-only, it can be restricted to one domain, and it can be revoked without changing the administrator password. export HMAIL_URL=https://mail.example.com:8045 export HMAIL_API_KEY="$(cat /etc/hmailserver/api.key)" hmctl domain list Connect-HmServer -Url https://mail.example.com:8045 -ApiKey (Get-Content .\api.key) Get-HmDomain hmctl reads HMAIL_URL, HMAIL_API_KEY, HMAIL_PASSWORD and HMAIL_PASSWORD_FILE, and takes --url, --api-key, --password and --password-file before the verb or after it. With no credential at all it asks for one, but only when there is a console to answer. In a scheduled task or a pipeline it says what is missing and stops. --insecure is refused for anything but the loopback. A self-signed certificate on localhost is the ordinary case and nothing is on the wire. An administrator password sent to a remote host over a connection nobody verified is what the refusal prevents, and the remedy is a certificate the client trusts. Some commands take the administrator password and never an API key, because no key reaches the routes behind them: hmctl apikey, hmctl ini, hmctl config changed, hmctl webhook and hmctl webhooks. The command synopsis names no option for a one-time code. Since 6.3.4 a one-time code is taken once, and the changelog's advice to a script that sent the administrator's password and a code with every request is a session or an API key; the REST API guide has the detail. hmctl hmctl [--url URL] [--api-key KEY | --password PASSWORD | --password-file FILE] [--insecure] [--json] <resource> <verb> [arguments] The resources, grouped. CommandLine.md has a row for each. Area Resources Notes Domains and what is in them domain, account, alias, list, group, fetchaccount, app-password, device, mailbox An account is listed and created under --domain, and read and written at its address, which is how the API is shaped. mailbox renames or moves one. Server-wide objects rule, route, port, iprange, certificate, journalrule list, create, update, delete Mail in motion queue, quarantine queue has list, retry, delete; quarantine has list, release, delete Settings settings, ini, config A settings group whole or one setting; the keys that were the [Settings] section; the configuration as one document Deliverability dkim, dmarc, suppression, bounce, bounces, complaint, complaints, spamtrap, traphits, reputation, openpgp reputation exits 1 when an address is listed and when a list could not be asked Integrations apikey, webhook, webhooks All three with the administrator password only, never an API key Operations log, backup, report, sample-domain, match match says what a rule criterion would decide, without a rule Bulk accounts import and export, a CSV file, with a dry run Anything else api A method and a path: the escape hatch, for a route newer than the client A few whole commands: hmctl domain create --set name=example.com --set active=true hmctl account create --domain example.com --set [email protected] --set password=... hmctl account list --domain example.com hmctl settings get antispam use_spf hmctl settings set antispam --set use_spf=true --set spam_mark_threshold=5 hmctl device [email protected] list hmctl queue list --json | jq '.[] | select(.retry_count > 3)' hmctl match contains viagra "cheap viagra here" hmctl report traffic --from 2026-09-01 --to 2026-09-15 --domain example.com hmctl api POST /api/v1/server/reinitialize How values are read. --set name=value reads true, false, null, numbers and anything starting with { or [ as JSON, and everything else as a string. --set-string keeps a value that looks like one of those a string. --from-json FILE takes a whole object, and - reads it from standard input. Output and exit codes. Output is a table by default and the server's own JSON with --json. Exit code Meaning 0 Success 1 An error 2 A usage mistake 3 The server refused the credential 4 What was named does not exist A script can therefore tell "no such account" from "wrong password" without reading the text. The PowerShell module Import-Module .\hmailserver\source\Tools\Cli\HmailServer.psm1 Connect-HmServer -Url https://localhost:8045 -Insecure Get-HmDomain | Where-Object active -eq $false Get-HmAccount -Domain example.com | Sort-Object max_size_mb -Descending | Select-Object -First 10 New-HmAccount -Domain example.com -Address [email protected] -Password (Read-Host -AsSecureString) Set-HmAccount -Address [email protected] -Property @{ max_size_mb = 500 } Set-HmSetting -Group antispam -Property @{ use_spf = $true } Get-HmQueue | Where-Object retry_count -gt 3 | Start-HmQueueDelivery Get-HmChangedSetting | Format-Table store, name, value, default Every function answers objects, so Where-Object, Sort-Object and Export-Csv work on them, and nothing in the module formats anything. Everything that changes the server supports -WhatIf and -Confirm, and deleting a domain or an account asks before it acts unless told not to. A domain name or an address completes from the server once Connect-HmServer has run. Invoke-HmApi -Method GET -Path /api/v1/... is the escape hatch, and is what every other function is built on. Accounts from a CSV file hmctl accounts import new-people.csv --dry-run hmctl accounts import new-people.csv hmctl accounts export example.com accounts.csv Import-HmAccount -Path .\new-people.csv -WhatIf Import-HmAccount -Path .\new-people.csv Export-HmAccount -Domain example.com -Path .\accounts.csv The file is UTF-8 (in Excel, CSV UTF-8) and its header row names the columns. These are the columns, and there are no others. Column Required Notes address yes Created under the address's own domain. password yes Sent as text, so digits alone are still a password. Never trimmed: white space at either end is refused. Judged by the server's password policy and, when it is switched on, the breach check. first_name At most 60 characters, counted as UTF-16 code units. last_name As first_name. active true, yes, on or 1; false, no, off or 0. Default true. max_size_mb A whole number of megabytes; 0 is no limit, and is the default. admin_level user, domain or server. Default user. address,password,first_name,last_name,active,max_size_mb,admin_level [email protected],S0me-Long-Passphrase,Ann,Adams,true,500,user [email protected],An0ther-Long-Passphrase,Bob,"Brown, Jr",true,,domain The columns may come in any order and their names in any case. Every cell but the password is trimmed, and an empty cell is not sent, so the server's default applies. A value with a comma in it goes in quotes. A row with more cells than the header has columns is refused rather than shifted along. What is refused before anything is sent. Each row becomes one POST /api/v1/domains/<domain>/accounts, and that route refuses a field it does not know by name. So a column that is not in the table is refused before anything is sent, a name column included. The clients also refuse, in the words the server would use: a row with no password; an active that is not one of the eight words; a max_size_mb that is not a whole number; an admin_level that is not one of the three; a name past its 60 characters; a password with a space or a tab at its start or end, which is the one row the clients refuse and the server would take. What a dry run cannot foresee. The password policy, the domain's account limit and an address the server does not accept are judged by the server alone. They are refused per row when the import runs, and listed with the address and the server's reason; hmctl adds the line number. An address that already exists is left alone and counted, never overwritten. An import that reset a password without saying so would lock people out with one command. --dry-run makes no request that changes anything and prints what would happen, line by line. The exit code is 1 when any row was refused. Who may make an administrator. admin_level is decided by who is asking. The administrator password, or a key issued for every domain, may make a server administrator. A key restricted to named domains may make users and domain administrators, and a row asking it for server is refused with a 403. The export writes the same columns less password, because the server does not have the passwords to give. Add a password column and change the addresses, and the exported file imports into another domain or another server as the same accounts: names, sizes, levels and whether each is active. The configuration as one document hmctl config changed # every setting that is not the shipped default hmctl config export server.json # the whole configuration, as a document hmctl config diff server.json # what would change; exit 1 if anything would hmctl config apply server.json # the plan, and nothing else hmctl config apply server.json --force # the plan, applied The document holds the settings groups, the hMailServer.ini keys the server exposes, the domains with their accounts, aliases and distribution lists, the groups, the rules, the routes, the listeners and the IP ranges. It is sorted throughout and carries nothing the server allocated, so two exports of an unchanged server are the same bytes. apply is a plan unless --force is added, and a deletion needs --allow-delete on top of that. Without it, an entry the server has and the document does not is left alone and counted. Not carried Why Passwords The server does not give them out. An account in the document that does not exist on the server is refused rather than created with a password nobody chose; add a password field for the accounts to be created. The secrets among the ini keys Listed as (set) or empty, and left out of the document. hmctl ini get <name> reads one by name. Certificates and the directories They are files and paths on one machine's disk. The journaling rules An address chosen for one server is almost never the right one for another. hmctl journalrule edits them. What lives under an account Application passwords, fetch accounts and folder permissions are the account holder's. The selector and key files of a domain whose DKIM key rotation is on The rotation moves them on its own schedule. Settings the environment manages In a container the value is the deployment's. The server refuses a change to such a setting with 409. Remote domain policies and alert rules The wiki's own addition: they have routes of their own that config does not read. What this server has changed from stock. hmctl config changed, or Get-HmChangedSetting in PowerShell, lists every setting whose value is not the one this version ships with, with the shipped value beside it. Settings: what is not stock has the route behind it, the other places to read the same list, what "default" means and who may see it. $ hmctl config changed STORE NAME VALUE DEFAULT ini LogLevel 5 9 ini SMTPDMaxTimeout 600 1800 ini OAuth2HmacSecret (set) ini DnsServer 10.0.0.1 (forced from [SettingsOverride]) database smtpnoofretries 6 4 5 of 391 settings are not what this version ships with. A secret is named and never shown. A value that the file's [SettingsOverride] section is forcing says (forced from [SettingsOverride]), a value the environment sets says (set by the environment), and a name the server holds a value for and ships no default for says (none: this server does not read it). A difference is not an exit code here, unlike config diff, because every real server has changed something. Reports and country blocking from a shell hmctl report sections hmctl report summary --from 2026-09-01 --to 2026-09-15 hmctl report senders --domain example.com --top 25 hmctl report mailboxes --csv mailboxes.csv hmctl report traffic --csv - --csv fetches the server's own CSV with format=csv instead of building one from the JSON. Per-domain reports has the sections, their sources and parameters, and who may read them. Run hmctl report sections first. It says which sources are recording and what this server cannot answer. The four sections counted from the message trace are empty when the trace is switched off, which is the default, and that is not the same statement as "nothing happened". Without --csv, the section's own note is printed above the table. CountryBlocking.md shows the country policy, which the topic on allow and block lists and country blocking explains, set from both clients: hmctl settings get settings geoip_database_status hmctl settings set --set geoip_database_file=/var/lib/GeoIP/GeoLite2-Country.mmdb hmctl settings set --set country_policy_smtp=block:CN,RU hmctl domain update example.com --set active=true --set country_policy=allow:GB,IE Set-HmSetting -Property @{ geoip_database_file = '/var/lib/GeoIP/GeoLite2-Country.mmdb' country_policy_smtp = 'block:CN,RU' } (Get-HmSetting).geoip_database_status Set-HmDomain -Name example.com -Property @{ active = $true; country_policy = 'allow:GB,IE' } geoip_database_status is read-only. A settings change is all or nothing, so a policy the grammar refuses leaves every other key in the same request unapplied. What neither client does Neither reads or writes the message store. Mail is IMAP's business. Neither is a backup tool of its own. hmctl backup start, hmctl backup restore and hmctl backup passphrase ask the server; the archive is written, read and restored by the server. Neither sets the administrator password. hmailserver --set-admin-password on the server does that, on Windows hMailServer.exe --set-admin-password, with the password in HM_ADMIN_PASSWORD or on standard input and never as an argument. On Windows the Control Panel does it too, but only where the service may write hMailServer.ini, as under LocalSystem: under NT SERVICE\hMailServer, a new 6.3.4 installation's account, its Administrator password card is refused with a sentence. Neither makes or carries the stored-secrets key. That is done on the server itself, never over the API. Neither can turn the REST listener on, since both need it. Neither sets what the environment sets in a container. No container image of 6.3.4 is published. Neither invents a field. A field that was not given is one the server decides. hmctl has no pending verb. SettingsApply.md notes that GET /api/v1/server/settings/pending is one request; hmctl api takes a method and a path. Where to read more The operator document: CommandLine.md, with a row for every resource and sections on DKIM rotation, bounces, complaints, DMARC reports, OpenPGP keys, the mailbox move and setting profiles. The wiki page: Command Line. For the examples above: ChangedFromStock.md, Reports.md and CountryBlocking.md. The public documentation: chapter 24, The REST API, which both clients speak, and 35.10, Administering a Linux server. On this forum: the Linux guide, The Control Deck, the 6.3.4 announcement, the REST API guide and, for what hmctl webhook manages, the send API and webhooks.
  • 0 Votes
    1 Posts
    133 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) changes how the listener judges some requests. A state-changing request that carries a credential, HTTP Basic included, is refused when the browser reports it as cross-site (Sec-Fetch-Site, or an Origin that is not this server's); scripts (the curl examples below among them), hmctl and the PowerShell module send no such header and are unaffected. A credential confined to named domains (a domain administrator, a key issued for named domains, or serveradmin given with a list of domains) is refused a domain's rename, a domain alias, a custom role's creation, change or delete, the bind_to_address, send_using_route and script_function actions on a domain's or an account's rule, a relay host changed to an address that is not public, and GET /api/v1/metrics/history; and a role is given or taken away only by a credential that holds manage_roles in the account's domain and takes in what the account holds now and afterwards, so such a credential cannot take a role away from an account whose admin level is the server administrator's. The OpenAPI document describes each path once, with every method it answers. In the Control Panel, API & monitoring is Monitoring & alerts and REST API keys is under Sign-in & access; Ctrl+K takes every old title to its page. See the 6.3.7 announcement. hMailServer 6.3.8 (2 November 2026) reads a JSON body in time and memory that follow its bytes (work item 110), and answers 400, saying why, before a route reads a member, to a body that is not one JSON object, names a member twice, holds an escaped U+0000 or passes 100,000 values or 32 levels, and to a member whose absence is the wider setting written as another type than its route reads, such as domains, allowed_from, role or expires on POST /api/v1/apikeys as anything but a string or null: correct a client of your own that sends such a body. An account's failed sign-in from loopback, as through a reverse proxy on the same machine, is counted against the name alone, AccountLockoutThreshold (off by default) being the limit, and never towards a ban of the address (work item 107); the listener's own accounting spares all of 127.0.0.0/8. See the 6.3.8 announcement. The REST API is the alternative to COM for automating hMailServer, and the only administration interface that works from a machine that is not running Windows. This guide covers how to turn the listener on, the three credentials it takes and the roles that narrow them, how one-time codes behave since 6.3.4, the OpenAPI document the server emits, and a set of curl examples that use only routes the project's own documents name. The one thing to know first: the listener is off as shipped (RestApiPort=0), and since 6.3.4 RestApiPort is a setting stored in the database. Editing the [Settings] section of hMailServer.ini does not turn the API on once the server has stored the key. Turning the listener on Four settings shape the listener. Setting Shipped value What it is RestApiPort 0 The port. 0 means there is no listener. RestApiBindAddress 127.0.0.1 The address it binds. TLS is required unless this is 127.0.0.1, localhost or ::1. RestApiCertificateFile empty The certificate, in PEM. Left empty, the listener falls back to the ACME certificate. RestApiPrivateKeyFile empty Its private key, in PEM. Since 6.3.4 the database is the settings store (schema 6042) and the [Settings] section of hMailServer.ini is its copy. A key the server has never stored is taken in from the file at the next start, so on a fresh installation an edit made before the first start still works; once the key is stored, the database's value is the one used. Settings: what is not stock, when a save takes effect covers the store itself, why an edited line is put back (HM5804) and [SettingsOverride]. Where the stored value is changed depends on the platform. Platform How RestApiPort is set Windows The Control Panel page Monitoring & troubleshooting → API & monitoring, or Settings.SetIniSetting over COM. The Control Panel and the COM API are Windows-only. Linux hmailserver --set-setting on the server itself, then a restart of the service. Either, once the API answers PUT /api/v1/settings/ini/{name} with the administrator password. sudo hmailserver --set-setting RestApiPort=8443 sudo systemctl restart hmailserver --set-setting checks what it is given: a port must be a number from 0 to 65535 and RestApiBindAddress an IP address or localhost. On Linux this matters most, because there the REST API is the administration interface. The packaged /etc/hmailserver/hMailServer.ini ships RestApiPort=0 and RestApiBindAddress=127.0.0.1 under [Settings], so the first start stores both, and changing those lines afterwards turns nothing on. The one part of the file that still decides a stored key is a [SettingsOverride] section, applied over the store at every start and reported as HM5807 each time; the wiki gives it as the route where neither the Control Panel nor COM is available. The new value waits for a reinitialise. Since 6.3.4 most settings apply when they are saved, but RestApiPort, RestApiBindAddress, RestApiCertificateFile and RestApiPrivateKeyFile are among the 28 keys marked reinitialize, because the listener binds and its TLS context is built when the servers start. They take effect at Apply now in the Control Panel (Windows) or the Control Deck, at Application.Reinitialize over COM (Windows), at POST /api/v1/server/reinitialize, or at a restart of the service. The listener can refuse to start, and each refusal is written once to the application log. What is wrong Log line Fix The administrator password is empty RestApi: Refusing to start - the administrator password is not set. Set an administrator password The bind address is not loopback and there is no certificate RestApi: Refusing to start - TLS certificate is required unless bound to 127.0.0.1 or ::1. Set RestApiCertificateFile and RestApiPrivateKeyFile. Set both files, enable ACME, or bind loopback The certificate cannot be loaded, or its key does not match RestApi: Refusing to start - the shared TLS configuration could not be applied to the configured certificate., preceded by HM5113 naming the certificate or key file Correct the PEM paths and permissions Nothing RestApi: Listening on 127.0.0.1:8443 (http, loopback only). or ... (https). None needed The Listening on line is the only proof that the listener is up. The loopback exemption is a string comparison against 127.0.0.1, localhost and ::1; 127.0.0.2 and ::ffff:127.0.0.1 do not qualify. The TLS configuration is the one the mail protocols use (cipher list, protocol switches, TlsKeyExchangeGroups), with a TLS 1.2 minimum applied on top, so opening an older protocol for a mail client does not open it for the management API. The three credentials Credential Header Reaches Administrator password Authorization: Basic with the user name Administrator (case-insensitive) Every administrative route. It is never read-only and never confined to domains. API key Authorization: Bearer hmapi_<64 hex> What its scope, role and domains allow, less the routes listed below. An account's own Authorization: Basic with the address and the mailbox password or an app password, or the hmailsession cookie /api/v1/me and /api/v1/session; since 6.3.4 also the administrative routes a role it holds allows. The two Basic forms are told apart before either password is tried: any user name but administrator is treated as a mailbox address and goes down the path an IMAP logon takes, with the same lockout and auto-ban. An account is never tested against the administrator password. API keys are the credential for scripts. A key is read-only unless it is made with "scope":"full", expires after 90 days unless the request names an expires (there is no "never expires"), and may be narrowed to named domains and to a source address, range or CIDR. Only the SHA-256 of a key is stored, in hMailServerApiKeys.ini beside hMailServer.ini, and that file is read again at every authentication attempt, so removing a key's section revokes it on the next request. # Administrator password only: a key can never make or revoke keys curl -u Administrator:your-admin-password \ -H "Content-Type: application/json" \ -d '{"label":"backup monitor","scope":"readonly","expires":"2027-01-01 00:00:00"}' \ http://127.0.0.1:8443/api/v1/apikeys {"id":"3f9a1c4b5d6e7f80","label":"backup monitor","scope":"readonly","domains":"", "expires":"2027-01-01 00:00:00","allowed_from":"","key":"hmapi_7c1f...c92a"} key is the clear text. It is returned once and cannot be recovered, so store it then. GET /api/v1/apikeys lists the keys with an expired flag and never a hash; DELETE /api/v1/apikeys/<id> revokes one. The create request takes label (required, at most 64 characters), scope, domains, expires and allowed_from, and since 6.3.4 role and the send API's send_from, send_per_minute and send_per_day. What no API key reaches. Whatever its scope or role, a key is answered 401 (not 403, so that it learns nothing) on the key store /api/v1/apikeys and on /api/v1/settings/ini, and since 6.3.4 also on /api/v1/audit, /api/v1/alerts, /api/v1/webhooks, the compromised-account review, /api/v1/settings/changed, /api/v1/server/settings/pending, an account's export, the address erasure and the second factor's enrolment. In the other direction, since 6.3.4 a key is the only credential POST /api/v1/send takes (a key naming a sender) and the only one /scim/v2 takes (a key whose role holds provision). Roles, since 6.3.4 A role is a named set of permissions, and an assignment gives an account a role in named domains (schema 6047). Nothing changes on upgrade: an account with no assignment holds what its admin level has always given it, and a key without a role is what it always was: Scope=full is serveradmin and Scope=readonly is auditor. A key is given a role with role on POST /api/v1/apikeys; an account is given one at /api/v1/role-assignments by a credential holding manage_roles. What a refusal says. A route a role may not call answers 403 naming the role and the permission, for example the role 'helpdesk' does not hold the permission 'manage_settings'. An address in a domain the role does not reach answers the role 'helpdesk' does not reach that domain whether or not the account exists. A request authenticated with an app password never carries the account's role. Administrator roles end to end has the built-in roles and their permissions, giving a role on each surface, custom roles, and the two stores, /api/v1/apikeys and /api/v1/settings/ini, that answer to the administrator password alone, not even to an account holding serveradmin. One-time codes When a second factor is enrolled on the administrator credential, the code travels in an extra header: curl -u Administrator:your-admin-password \ -H "X-hMailServer-OTP: 123456" \ http://127.0.0.1:8443/api/v1/status Without it the answer is the one 401 on this listener that says what is missing: HTTP/1.1 401 Unauthorized WWW-Authenticate: Basic realm="hMailServer" X-hMailServer-OTP: required Cache-Control: no-store {"error":"authentication failed","second_factor":"required"} A missing code is not counted towards the auto-ban; a wrong code is. Since 6.3.4 a one-time code is taken once, over COM and over REST, for the administrator and for an account alike. A code the server has accepted is refused if it comes again. The consequence for automation is direct: a script that sent the administrator's password and a code with every request now has only its first request in each half minute accepted. The others are refused with 401 and are not counted as wrong codes, so such a script does not lock the administrator out. The changelog names the two remedies: sign in once with POST /api/v1/session and use the hmailsession cookie from there, or use an API key. # Sign in once; the password and the code travel on this request alone curl -c cookies.txt -u Administrator:your-admin-password \ -H "X-hMailServer-OTP: 123456" \ -X POST http://127.0.0.1:8443/api/v1/session # Reads carry the cookie curl -b cookies.txt http://127.0.0.1:8443/api/v1/status # Anything that changes something also carries the CSRF header curl -b cookies.txt -X POST -H "X-Requested-With: hMailServer" \ http://127.0.0.1:8443/api/v1/queue/81234/retry # Sign out curl -b cookies.txt -X DELETE -H "X-Requested-With: hMailServer" \ http://127.0.0.1:8443/api/v1/session The session rules are enforced by the server: 30 minutes idle and 12 hours from sign-in until a session policy sets others (since 6.3.4, session_idle_seconds and session_lifetime_seconds), at most 1000 sessions, and every session dropped when the listener stops. A cookie-authenticated request that is not GET or HEAD without X-Requested-With: hMailServer is refused with 403. Somebody who is asked for a code twice within half a minute, to sign in to the Control Deck and then to register a passkey for example, waits for the authenticator's next code. Two further changes in 6.3.4 belong here. A wrong code is counted against a budget of five in fifteen minutes, after which no code is taken for fifteen minutes; on the administrator's Basic sign-in the answer is then 429 with code second_factor_locked and Retry-After. And for an account, one of its one-time recovery codes, in X-hMailServer-Recovery-Code, is taken wherever the authenticator's code is asked for. The administrator signs in at POST /api/v1/session with HTTP Basic, and with the code in X-hMailServer-OTP when a second factor is enrolled: AccountRecovery.md names the header for this route, the 6.3.4 changelog gives the session as the remedy above, and the Control Deck signs in that way. The wiki's credentials table, which lists /api/v1/session among the routes the administrator password does not reach, is out of date on this. How a request is judged The server tries Bearer, then Basic, then the session cookie. It then applies the CSRF check for cookie-authenticated writes (and, since 6.3.7, refuses any state-changing request that carries a credential when the browser reports it as cross-site, by Sec-Fetch-Site or an Origin that is not this server's) and the rate budget of 200 requests in a fixed 10-second window per credential, turns the method and path into a route kind, and decides on that kind in one place before any handler runs. Read-only is decided by route kind, not by HTTP method. A read-only key on a mutating route is answered 403 with this api key is read-only. A key with a domain list is refused server-wide resources outright: the queue, the quarantine, IP ranges, certificates, rules, logs, the backup and the settings among them. On a route that names a domain or an address outside its list the answer is 403 with this api key is not permitted for that domain. Routing is exact. Method and path must match after the query string is removed, a trailing slash is a different path, and an id in a queue, quarantine, IP-range, archive or /api/v1/me/... path must be 1 to 18 decimal digits and greater than zero, or the answer is 404. The OpenAPI document GET /api/v1/openapi.json answers the API's description to the administrator password or to any key, a read-only one included. The wiki's advice is that this document is the count to trust: in 6.3.4 it names 441 paths and 621 path-and-method operations, 413 and 585 of them under /api/, against 146 and 219 in 6.3.3. The rest are /scim/v2, the sign-in, reset and reading pages, the list manager's pages and the static catalogues and scripts. The Control Deck draws its settings groups from this document, and since 6.3.4 the document gives the ini-backed rows of the settings groups an x-applies mark saying when a save takes effect. The APIs Reference describes it as a static OpenAPI 3.0.3 document and records where its text is not accurate: the summary of /api/v1/status mentions uptime, and the handler returns no uptime field; the text for /api/v1/settings/ini still describes the file as the store, where since 6.3.4 the value goes to the database first. Settings saved and not yet running Since 6.3.4 a key marked reinitialize or restart whose stored value is not the value the server is running on is pending. The list is recomputed at every request. Route Answer GET /api/v1/server/settings/pending [{name, stored, running, applies, secret}]; a secret's two values are (set) or empty GET /api/v1/settings/ini The keys as {name, value, secret}, an applies map and a pending array of names GET, PUT, DELETE /api/v1/settings/ini/{name} One key, with applies and pending POST /api/v1/server/reinitialize 202 before it happens; {"reinitializing": true, "drained": true} when it ended a drain All but the last answer to the administrator password alone. A DELETE is now the only way to return a stored key to its default, because deleting a line from the file no longer does. A reinitialise restarts the REST listener with the other services, so it ends a Control Deck session; poll /api/v1/status until it answers again. Worked examples These use the loopback, plain-HTTP shape that RestApiPort=8443 and RestApiBindAddress=127.0.0.1 give; put the port that was stored in place of 8443. With a certificate configured, change http:// to https://. They present the administrator password on every request, which works while no second factor is enrolled; with one enrolled, use the session above or an API key. # Status: state is 0 Unknown, 1 Stopped, 2 Starting, 3 Running, 4 Stopping curl -u Administrator:pw http://127.0.0.1:8443/api/v1/status # Domains and accounts curl -u Administrator:pw http://127.0.0.1:8443/api/v1/domains curl -u Administrator:pw http://127.0.0.1:8443/api/v1/domains/example.com/accounts curl -u Administrator:pw -H "Content-Type: application/json" \ -d '{"address":"[email protected]","password":"S0me-Long-Passphrase"}' \ http://127.0.0.1:8443/api/v1/domains/example.com/accounts curl -u Administrator:pw -X DELETE \ http://127.0.0.1:8443/api/v1/accounts/[email protected] # The delivery queue curl -u Administrator:pw http://127.0.0.1:8443/api/v1/queue curl -u Administrator:pw -X POST http://127.0.0.1:8443/api/v1/queue/81234/retry curl -u Administrator:pw -X DELETE http://127.0.0.1:8443/api/v1/queue/81234 # Logs and the backup curl -u Administrator:pw "http://127.0.0.1:8443/api/v1/logs/hmailserver_2026-09-08.log?lines=50" curl -u Administrator:pw -X POST http://127.0.0.1:8443/api/v1/backup curl -u Administrator:pw http://127.0.0.1:8443/api/v1/backup # With a key curl -H "Authorization: Bearer hmapi_7c1f...c92a" http://127.0.0.1:8443/api/v1/status # Store another port: it waits for a reinitialise, and the pending list shows both values curl -u Administrator:pw -X PUT -H "Content-Type: application/json" \ -d '{"value":"8444"}' http://127.0.0.1:8443/api/v1/settings/ini/RestApiPort curl -u Administrator:pw http://127.0.0.1:8443/api/v1/server/settings/pending # Put the running port back; a value put back before anything applied it is no longer pending curl -u Administrator:pw -X PUT -H "Content-Type: application/json" \ -d '{"value":"8443"}' http://127.0.0.1:8443/api/v1/settings/ini/RestApiPort Creating an account answers {"address":"[email protected]","created":true}, or 400 for a missing field, 404 {"error":"domain not found"} or 409 {"error":"account already exists"}. In 6.3.4 the create route takes address and password and the other columns of the CSV import as well (first_name, last_name, active, max_size_mb and admin_level), and refuses a field it does not know by name (CommandLine.md). The older statement in the wiki and the APIs Reference, that only address and password are read, no longer holds. For a log, lines defaults to 200 and is capped at 2000. Limits and status codes Limit Value What the client sees Request size 64 KB, head and body 413, connection closed Large requests 16 MB and 300 s on POST /api/v1/me/messages and POST /api/v1/me/drafts; a few other routes are granted more than 64 KB as well, among them PUT /api/v1/me/settings and the SAML ACS (256 KB) 413 above that Request deadline 30 s, the TLS handshake included Connection closed with no response Connection lifetime 300 s, and 1000 requests Connection closed Concurrent connections 64 Connection closed Rate 200 requests per 10 s per credential 429 with Retry-After: 10 Auto-ban Shared with SMTP, IMAP and POP3; loopback exempt The address is refused at accept for 5 minutes A 401 is deliberately the same for no credential, a wrong password and an unknown, expired or out-of-network key; the application log says which. A request with Transfer-Encoding is answered 411, because the server does not de-chunk. An API response is application/json with Cache-Control: no-store, with exceptions: the static pages and the attachment download serve their own media types, a report asked for with format=csv is text/csv, and every answer under /scim/ is application/scim+json. Where to read more The wiki: The REST API and, for a row per route, APIs Reference section 4. The operator documents: Roles.md and SettingsApply.md. The public documentation: chapter 24, The REST API, with API keys and the OpenAPI document. On this forum: The Control Deck, which is the browser front end for this API; the Linux guide; the 6.3.4 announcement; hmctl and the PowerShell module; the send API and webhooks; and SCIM provisioning and single sign-on.
  • 0 Votes
    1 Posts
    121 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) changes three things this guide relies on. A Control Panel signed in to a server on another computer no longer reads or writes its own computer's hMailServer.ini for the [LDAP] values, which it cannot read from the server, where it showed that file or the defaults as the server's; the sidebar is regrouped, and Ctrl+K takes every old page title to its page. With SMTP logging on, 6.3.6 and every earlier version that offered them wrote the bearer token of AUTH XOAUTH2 and AUTH OAUTHBEARER and the lines of SCRAM-SHA-256, OAUTHBEARER and GSSAPI into the SMTP log in the clear; 6.3.7 hides every line of a SASL exchange but the user name of AUTH LOGIN and the authentication identity of AUTH PLAIN. An account given the serveradmin role with a list of domains is, over COM as over REST, confined to those domains and no longer the server's administrator, so a script that runs the directory synchronisation's preview and apply as such an account needs the role given with no list. See the 6.3.7 announcement. hMailServer 6.3.8 (2 November 2026) signs a name in to an account only when it is the account's stored address, compared in lower case after a domain alias and the default domain, for the name an OAuth2 bearer token carries as for a password (work item 104), so a name that differs from the stored address in more than the case of the letters A to Z, or has white space at either end, may no longer sign in. On Windows an OAuth2PublicKeyFile whose path holds characters outside ASCII may be refused with HM7536 (one outside the code page, which worked up to 6.3.7, is refused), and then the key verifies no token: give the file, and the folders above it, an ASCII name before upgrading. See the 6.3.8 announcement. This topic covers three ways an organisation's directory or identity provider vouches for somebody on hMailServer 6.3.4's mail protocols: checking a password against Active Directory or another LDAP directory, creating mailboxes from that directory, and accepting OAuth2 bearer tokens. Know this first: 6.3.4 keeps settings in the database, but the [LDAP] section stays in hMailServer.ini on each server, with no REST route and no environment name; the server re-reads it within two seconds of a change, with no restart. The OAuth2 keys are ordinary [Settings] keys, stored like any other. Three mechanisms, three questions Mechanism The question it answers Control Panel page (Windows) LDAP directory authentication Is this the password of the directory user the account is linked to? Access & abuse protection → Directory authentication (LDAP) Directory synchronisation Which mailboxes should exist, according to the directory? Access & abuse protection → Directory synchronisation OAuth2 bearer tokens Did the identity provider sign this token, and which mailbox does it name? Access & abuse protection → Authentication All three are off by default, and an upgrade switches none of them on: the [LDAP] section is absent until you add it, and OAuth2Enabled defaults to 0. Where the [LDAP] section lives Windows. The Control Panel's page writes the hMailServer.INI of its own machine. Connected to another machine, its Save is disabled, although "the connection test still works with the values typed here". Work on the server, over Remote Desktop. Linux. No Control Panel and no COM. The file is /etc/hmailserver/hMailServer.ini, 0640 root:hmailserver: sudoedit /etc/hmailserver/hMailServer.ini. Not REST or hmctl. The REST ini routes and hmctl ini set handle [Settings] keys only. Per server. On a warm standby each node has its own [LDAP]; keep the two alike by hand. ServicePassword is a line of that file. It is not among the [Settings] secrets 6.3.4 seals under the stored-secrets key; the Control Panel's field for it is write-only. The sign-in keys Key Default What it does Enabled 0 The master switch Server empty The directory host; required Port 0 0 means 636 for LDAPS, 389 otherwise Security 2 2 LDAPS, 1 StartTLS, 0 plain; anything else is LDAPS VerifyCertificate 1 Check the directory's certificate AllowUnprotectedPassword 0 Permit a simple bind over a plain connection BindMethod 0 0 simple; 1 Negotiate (Kerberos or NTLM through Windows SSPI) SearchBase empty Required unless UserDnTemplate is set or the bind is Negotiate UserSearchFilter (&(objectCategory=person)(objectClass=user)(sAMAccountName=%u)) How the user's entry is found UserDnTemplate empty Bind directly, without searching ServiceUsername, ServiceDomain, ServicePassword empty What the search binds with TimeoutSeconds 10 Bounds every directory operation; 1 to 90 FallbackToWindowsLogon 0 Also try Windows LogonUser; Windows only LDAPS with VerifyCertificate=1 is the default and the recommended configuration, proven end to end against a Windows Server 2025 domain controller. Server must be the name in the directory's certificate: the same setup pointed at the controller's IP address, with the right password, is refused, and HM5920 reports a failure "unrelated to the credentials supplied". On Windows, put the issuing CA in LocalMachine\Root; when this was proven the service ran as LocalSystem, and a CurrentUser\Root import served it nothing. The bind needs no extra privilege under NT SERVICE\hMailServer, which a new 6.3.4 installation runs as. On Linux the certificate must chain to the system trust store. StartTLS (Security=1) connects on 389 and upgrades before anything is sent. Plain LDAP is labelled Unprotected LDAP - cleartext (lab networks only): there a simple bind is refused (HM5921) rather than sent, unless AllowUnprotectedPassword is 1. Simple or Negotiate. A simple bind sends the password to the directory, so it belongs on LDAPS or StartTLS. Negotiate, Windows only, never sends it; it worked on 389 against a controller that had no LDAPS certificate yet, and suits a mail server not joined to the domain. Search or template. A search binds with the service credentials, finds the user under SearchBase with UserSearchFilter, then binds as the user. It asks for two entries to see ambiguity: more than one match is a configuration fault and authenticates nobody. UserDnTemplate skips the search, so no service credential need be stored at all; with a simple bind, that is the configuration the feature was proven with. Linking an account to the directory The directory is asked only about linked accounts; others keep their own password. The webmail's sign-in shares the IMAP and POP3 logon, so it takes the directory password too. Control Panel: the account dialog's Windows directory tab, with the domain (ADDomain) and user name (ADUsername); Browse Active Directory searches the forest to fill them. Since 6.3.4 the tab has that name; it was called Directory, and the new Directory entry tab is the organisation's address book, unrelated to Active Directory. REST: PUT /api/v1/accounts/{address} with ad_enabled, ad_domain and ad_username. COM (Windows): IsAD, ADDomain and ADUsername. What a link changes. The account's stored password is no fallback: in the proof run, a linked account's local password was refused. Linked accounts bypass local hashing, so they are exempt from the minimum-hash policy and from password expiry, and cannot sign in with SCRAM-SHA-256; their clients use PLAIN or LOGIN over TLS. App passwords work for them: the directory holds the password, but an app password is local, which is what makes a second factor possible for a directory account at all. The person cannot change the password in the webmail (POST /api/v1/me/password answers 409), and Forgotten your password? cannot reset it, because this server does not hold it. Without LDAP, on Windows, a linked account is checked by LogonUser, which needs the mail server's host joined to the domain. On a host that is not joined, Windows answers every domain name, real or misspelt, as a wrong password, so the account can never sign in; HM5911 says so, naming the domain and the computer. An empty domain, . or the computer's own name means a local Windows account. Never leave the domain empty by accident: with [LDAP] Enabled off, an account linked with an empty domain and the user name administrator would accept the server's local Administrator password. FallbackToWindowsLogon never applies to a wrong password: a directory's rejection deliberately does not fall through to LogonUser, because that would double every failed attempt and trip a lockout policy on a single wrong password. Leave it off on a host that is not domain-joined. Setting it up on Windows For LDAPS or StartTLS, put the directory's issuing CA in LocalMachine\Root on the mail server. On the mail server, open Directory authentication (LDAP). Switch directory authentication on (Enabled); until then a linked account is checked by Windows LogonUser, not with these settings. Fill in the server, the security, the bind, and either the search or UserDnTemplate; the first card asks Is this configuration complete? Test the connection with a directory user name, its AD domain and its password. It tries the values typed on the page. Save. The server re-reads [LDAP] within two seconds. Link one account and sign in with the right password and a wrong one. Over POP3 the proof run got +OK Mailbox locked and ready and -ERR, and the log showed the bind and the rejection separately. Link the others by hand, or with directory synchronisation. Setting it up on Linux The only directory path is an LDAP simple bind over LDAPS or StartTLS, proven against OpenLDAP's slapd. Make sure the directory's certificate chains to the system trust store, and use the name it carries. Add the section with sudoedit /etc/hmailserver/hMailServer.ini, filling in the angle brackets. Without a UserSearchFilter line the search uses Active Directory's filter, (&(objectCategory=person)(objectClass=user)(sAMAccountName=%u)), which finds nobody in another directory such as OpenLDAP; there, write a filter that matches its user entries, with %u for the name: [LDAP] Enabled=1 Server=<the host name the directory's certificate carries> Security=2 VerifyCertificate=1 BindMethod=0 SearchBase=<the DN the users are under> UserSearchFilter=<that filter, unless the directory is Active Directory> ServiceUsername=<the search account> ServicePassword=<its password> Two seconds later the server has it. Link an account over REST (ad_enabled, ad_domain, ad_username) and sign in with the right password and a wrong one. Linux has no Negotiate (BindMethod=1 is refused as a directory failure, not a wrong password: HM6420, reported once), no LogonUser (a linked account while [LDAP] Enabled is off is refused with HM6421, which names the settings to set; FallbackToWindowsLogon refuses with HM6404), and no GSSAPI bind. When the directory cannot answer So that an outage does not read as users mistyping, infrastructure failures are reported apart from credential rejections, throttled per error and endpoint, and TimeoutSeconds bounds the wait. Code Meaning HM5920 The directory could not be used (a certificate not matching Server, say). Reported at High: unrelated to the credentials, and every account on that directory is affected HM5921 A simple bind would have sent the password unprotected, so the logon was refused first HM5911 Windows LogonUser: the host is not joined to the domain; at most once a minute per problem Directory synchronisation: preview, then apply Synchronisation reads the directory, works out which mailboxes should exist, and only when asked creates and updates them; it was verified end to end against a Windows Server 2025 domain controller. The planner writes nothing and the applier decides nothing, so a preview cannot promise what the apply would not do. Per-domain opt-in. A domain takes part only if its Active Directory domain name is set (ADDomainName in the domain dialog, ad_domain_name over REST), so a search base one level too high cannot provision into unrelated hosted domains. Nothing is ever deleted, account or message. The most it does is clear Active, only when asked, and never on a truncated or empty enumeration or for a domain in which nothing was seen. No password. A made account is marked IsAD and signs in through the directory from the start. No existing account claimed. An account that is not linked, or is linked to another AD domain, is reported, not changed. Paged. Active Directory answers a search beyond MaxPageSize (1000) with the first page and a success status, so the enumeration pages; SyncMaxUsers is the ceiling. [LDAP] key Default SyncFilter (&(objectClass=user)(objectCategory=person)(mail=*)(!(userAccountControl:1.2.840.113556.1.4.803:=2))) SyncMailAttribute mail; proxyAddresses works, preferring the capitalised SMTP: primary SyncUsernameAttribute, SyncDisplayNameAttribute sAMAccountName, displayName SyncMaxUsers 5000; at most 100000 SyncScheduleMinutes 0, off; 15 at least, 10080 (seven days) at most On Windows: Get authentication working first. Synchronisation binds as the service account, honouring BindMethod, so Negotiate works where there is no LDAPS certificate. Set the Active Directory domain name on each mail domain that takes part. Open Directory synchronisation, check the selection keys, choose the domain and whether absent accounts are marked inactive, and run Preview. It changes nothing. Read the report, including the existing accounts it reports and leaves alone. Apply. The button stays disabled until a preview of the same options has run. Behind the page are Settings.PreviewDirectorySync(DomainName, DisableMissing) and Settings.ApplyDirectorySync, for a server administrator and callable from a script. From a Control Panel on another machine the six selection boxes are read-only and, up to 6.3.6, show their defaults; since 6.3.7 the Control Panel no longer reads its own computer's file for the [LDAP] values, which it cannot read from the server, so it no longer shows them as the server's. Preview and Apply still run inside the server. The unattended schedule (SyncScheduleMinutes) runs at start and then every so many minutes. It creates and updates but never disables: unattended is where nobody reads the report that would show a mailbox about to be taken away. The schedule is one of the tasks the server registers as it starts, and only if SyncScheduleMinutes is set then, so restart the service once after first setting it. On Linux, with no COM, there is no preview or apply, and whether the schedule runs there is not documented: treat it as untested. OAuth2 bearer tokens on IMAP, POP3 and SMTP With OAuth2Enabled=1, SMTP, IMAP and POP3 offer XOAUTH2 and OAUTHBEARER (RFC 7628), by default only over TLS (OAuth2RequireTLS=1). The server verifies the token's signature itself, takes the login name from the claim OAuth2UsernameClaim names (email), and signs in to that mailbox; a user= the client asserts must match. The mailbox must already exist: a token never makes one. Key Default What it does OAuth2JwksUrl empty The provider's JWK Set; the token's kid picks the key. https, or plain http to loopback only OAuth2JwksCacheSeconds 3600 How long the set is kept OAuth2PublicKeyFile empty A PEM RSA or EC public key; the fallback OAuth2HmacSecret empty The HS256 key OAuth2AllowedAlgorithms RS256 HS256, RS256 and ES256 recognised; none never OAuth2Issuer, OAuth2Audience empty Expected iss and aud; empty turns the check off OAuth2IntrospectionUrl empty RFC 7662 endpoint asked whether a verified token is still active OAuth2IntrospectionClientId, OAuth2IntrospectionClientSecret empty Who this server introspects as, over HTTP Basic OAuth2IntrospectionCacheSeconds 300 Verdict reuse, never past the token's exp OAuth2IntrospectionFailOpen 0 1 accepts a token the endpoint cannot judge, logging each An unknown kid, which is what a rotation looks like, fetches the set again at once, at most once a minute; a failed fetch keeps the keys held. HttpProxy, when set, carries these fetches. Where to set them. Each applies when saved: on the Control Panel's Authentication page, with PUT /api/v1/settings/ini/{name} and the administrator password, with hmctl ini set, or with COM Settings.SetIniSetting. Not in hMailServer.ini: since 6.3.4 the database holds them, and a line edited in the file is not used; Settings: when a save takes effect explains the store. Choose the key source: OAuth2JwksUrl, so a rotation needs no copied file; or OAuth2PublicKeyFile; or OAuth2HmacSecret with HS256 allowed. Set OAuth2Issuer and OAuth2Audience to what the provider writes into its tokens. Left empty, neither is checked: any token the provider's keys signed is accepted, whatever client it was issued to. Optionally set introspection, so a revoked token stops working before it expires. Only then set OAuth2Enabled to 1. On Windows, read the OAuth2 / XOAUTH2 item of the Control Panel's External setup list. Why that order. Switched on without usable key material, the mechanisms are still advertised, every token fails, and failed bearer sign-ins feed the auto-ban, so a correct client can have its address banned. On Linux before the REST listener is up, hmailserver --set-setting stores a key; a secret is given by name alone and typed at a prompt. A value stored this way reaches the running server at its next save, reinitialise or restart. Set the issuer and audience too, as step 2 says, putting your provider's values in place of these examples: sudo hmailserver --set-setting OAuth2JwksUrl=https://idp.example.com/keys sudo hmailserver --set-setting OAuth2Issuer=https://idp.example.com sudo hmailserver --set-setting OAuth2Audience=hmailserver sudo hmailserver --set-setting OAuth2Enabled=1 sudo systemctl restart hmailserver Since 6.3.4: a bearer token meets the domain's and the account's country policy, as a password does (a stolen token used to sign in from a refused country); an SMTP session signed in with a token can send more than one message (the second used to be refused with 550 "Login credentials no longer valid"); and OAuth2HmacSecret and OAuth2IntrospectionClientSecret are sealed at rest once the installation has a stored-secrets key, which an upgrade does not make until an administrator runs --create-stored-secrets-key. Tested how. The regression fixtures play the identity provider on the loopback, to make it misbehave: RS256 and ES256, the JWK Set and its rotation, introspection and fail-open. A real provider's tokens are not yet part of them. This is inbound only. The OutboundOAuth2 keys and FetchOAuth2Hosts are the server's own token for relaying through and collecting from Microsoft 365; Routes, relays and smart hosts covers relaying with it. Where these end: SCIM, single sign-on and Kerberos The LDAP backend authenticates; SCIM provisions. Synchronisation reads the directory; SCIM is the other direction, Entra ID or Okta calling /scim/v2 under an API key whose role holds provision, and it removes accounts, which synchronisation never does. Single sign-on, OpenID Connect or SAML 2.0, opens the webmail and the Control Deck only, ending in POST /api/v1/session; the protocols' bearer tokens are a separate, older mechanism with the OAuth2 settings above, and single sign-on does not reach mail clients. On Windows, SASL GSSAPI (GssapiEnabled) lets a mail client sign in with a Kerberos ticket, mapped to the account linked to that directory user. SCIM provisioning and single sign-on sets those up; Signing in and Connecting a mail client show the person's side. What it does not do No Negotiate, Windows logon or GSSAPI bind on Linux. hMailServer on Linux lists the rest. No LDAP server here, and the webmail's organisation directory is typed in, not imported from Active Directory. Where to read more Wiki: Settings Reference sections 9.2 and 11, Capabilities and Configuration, Control Panel Reference, Installing on Linux, Security Hardening, Sign-in and Identity, Architecture Documents: SettingsApply.md, ServiceAccount.md, Scim.md, PasskeysAndSingleSignOn.md, TestLab.md The Roadmap rows on LDAP, which record how each part was proven On this forum: the stored-secrets key, auto-ban and the warm standby The public documentation's Creating accounts from Active Directory and Modern authentication Report a security flaw in any of this by SECURITY.md's routes: a confidential GitLab issue, or email to contact-project+hmailserver-hmailserver-86730559-issue-@incoming.gitlab.com, which GitLab files as one.
  • 0 Votes
    1 Posts
    138 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) confines an account given the serveradmin role with a list of domains to those domains over COM as over REST, so it is no longer the server's administrator: give the role with no list to an account that is to administer the whole server, a script's or a Control Panel's account among them. Over COM, one given domainadmin with a list that leaves out its own domain no longer administers that domain's ordinary accounts; 6.3.4 to 6.3.6 allowed both where REST refused them. A role is given or taken away only by a credential that holds manage_roles in the account's domain and takes in what the account holds now and afterwards, and a credential confined to named domains is refused over COM, as over REST, a domain's rename, a domain alias, a relay host changed to an address that is not public and an action on a domain's or an account's rule that reaches one of the server's own addresses, a configured route or a function of the event script. Rule.Save and RuleAction.Save refuse, on a domain's or an account's rule, a Set header value of From, Sender, Resent-From, Resent-Sender or DKIM-Signature and a Reply from an address the owner may not send as, and on a rule of every owner a Set header value of the loop count; one stored before the upgrade is taken without that action, and the application log names it. For a rule of every owner they refuse a Run function that is not a plain function, which one stored before still meets when the rule fires, as since 6.3.4. Utilities.CheckSieveSyntax refuses a Sieve script that adds or deletes From, Sender, Resent-From, Resent-Sender, DKIM-Signature, Auto-Submitted or the loop count. Once the database has ended a Database object's transaction, every later statement is refused and Database.CommitTransaction fails: call Database.RollbackTransaction and run the whole transaction again, or stop. See the 6.3.7 announcement. hMailServer 6.3.8 (2 November 2026) signs a name in over COM, as at every other sign-in, only when it is the account's stored address, compared in lower case after a domain alias and the default domain: a mailbox address spelt otherwise, in more than the case of the letters A to Z, or with white space at either end, may no longer sign in; it fails as a name that does not exist fails, and is counted as a failure. See the 6.3.8 announcement. This topic covers the COM API of hMailServer 6.3.4: what a script connects to and how it authenticates, the object model and the shape every collection shares, working PowerShell examples, the change 6.3.4 makes to when a configuration change takes effect, what 6.3.4 adds to the object model, administrator roles over COM and why API keys are not there, the changed-from-stock document, the COM surfaces of distribution lists and the directory, and the Import Tool. The one thing to know first is that the COM API is Windows-only, and that an object you add to a collection, or an item of one you change, is not stored until you call its Save(); a few writes need none, among them an account's SieveScript and Settings.SetIniSetting, and an address saved or deleted through Route.Addresses needs no Save() of the route. Since 6.3.4 the running server's routes, relays and other shared configuration objects also wait for that Save(); up to 6.3.3 they changed the moment a property was set. What you are connecting to hMailServer exposes a full COM API. Almost anything the Control Panel can do, a script can do, because the Control Panel does its work through COM. The exceptions are the pages that edit a file on the server itself: REST API keys (described under roles below), the two directory pages' [LDAP] section, the sending limits on Server limits & expert settings, the [Database] rows of the Performance page, and the event script; and Discovery, which reads the REST listener because eDiscovery has no COM interface. The COM API and the Control Panel are both Windows-only: a Linux server is administered over REST, hmctl and the Control Deck (hMailServer on Linux: what you get and what you do not). One type library, hMailServer, with 94 coclasses in 6.3.3 and 100 in 6.3.4. The root is Application, ProgID hMailServer.Application. Every interface is a dual IDispatch interface, so late binding works without registering the type library. CreateObject, New-Object -ComObject and C# dynamic all work on a machine where only the server is installed. Early binding depends on vtable order, so new members are always appended, never inserted. An interop assembly built against 6.3.3 keeps working; recompile it to reach the new members, and on a hand-copied installation register the server again, elevated, so that the new interfaces are known to COM. Late-bound scripts need nothing. The installer's admintools component exists only to register the type library, so that scripts on another machine can administer this one. # Local $app = New-Object -ComObject 'hMailServer.Application' # Remote, over DCOM, against a machine where admintools is registered $app = [Activator]::CreateInstance([Type]::GetTypeFromProgID('hMailServer.Application', 'mail01')) Authenticating $app = New-Object -ComObject 'hMailServer.Application' $account = $app.Authenticate('Administrator', 'your-admin-password') if (-not $account) { throw 'Authentication failed.' } Authenticate returns an Account object, or $null on failure. Sign in as Gets Administrator with [Security] AdministratorPassword from hMailServer.INI server administrator A mailbox address whose AdminLevel is 2 server administrator A mailbox address whose AdminLevel is 1 domain administrator: their own domain only A mailbox address that holds a role (since 6.3.4) what the role's permissions reach, in the domains it reaches An empty administrator password is an open door. If the administrator password is empty, and no administrator TOTP secret is enrolled, every COM session starts already authenticated as a server administrator. This is the historical behaviour. A second factor changes the call you must make. With Settings.EnrolAdministratorTOTP() used, Authenticate refuses the administrator outright and AuthenticateWithCode is the only way in: if ($app.AdministratorTOTPEnabled) { # readable BEFORE authenticating $app.AuthenticateWithCode('Administrator', $password, $code) | Out-Null } else { $app.Authenticate('Administrator', $password) | Out-Null } Three things about that sign-in change in 6.3.4: Change, since 6.3.4 What a script sees A one-time code is taken once a TOTP code the server has accepted is refused if it comes again, for the administrator and for an account alike; the refusal is not counted as a wrong code A wrong code is counted AuthenticateWithCode checks the password first, and a wrong code after a right password counts against a budget of five in fifteen minutes, then no code is taken for fifteen An app password is not a COM logon Authenticate and AuthenticateWithCode answer an app password as they answer a wrong password; it still opens the mailbox over IMAP, POP3 and SMTP How a refusal reaches you. If you are not entitled to an object, every member returns an error HRESULT: a terminating exception in PowerShell, a trappable error in VBScript, a COMException in C#. Up to 6.3.3, seventy-two COM classes answered a refusal as a bare Exception from HRESULT: 0x800403E9. Since 6.3.4 a refusal arrives with the server's own sentence. The object model From Application What is there Domains Domain, and under each Accounts, Aliases, DistributionLists, DomainAliases and, since 6.3.4, Rules Account (under Accounts) Address, Password, Active, MaxSize, Size, AdminLevel; Messages, IMAPFolders, Rules, FetchAccounts, AppPasswords; SieveScript, TOTPEnabled Settings the server-wide properties and the sub-objects AntiSpam (which owns Quarantine), AntiVirus, Backup, Cache, Directories, Groups, IncomingRelays, Logging, MessageIndexing, PublicFolders, Routes, Scripting, SecurityRanges, ServerMessages, SSLCertificates, TCPIPPorts; since 6.3.4, RemoteDomainPolicies Rules the global rules Status UndeliveredMessages, StartTime, ProcessedMessages, RemovedViruses, RemovedSpamMessages, SessionCount Utilities helpers such as CheckSieveSyntax, CriteriaMatch, MakeDependent, PerformMaintenance, ImportMessageFromFileToIMAPFolder Database ExecuteSQL, ExecuteSQLWithReturn, RequiresUpgrade, IsConnected, DatabaseType BackupManager StartBackup, LoadBackup GlobalObjects DeliveryQueue, MessageTrace, Languages Links Domain(DBID), Account(DBID), Alias(DBID), DistributionList(DBID) Diagnostics DiagnosticResults, each a DiagnosticResult Every collection offers the same shape: Count, Item(index), ItemByDBID(id), Add(), Refresh(), Delete(index) and DeleteByDBID(id), with ItemByName or ItemByAddress where a name exists. Items have Save() and Delete(). Add() does not persist. It returns a new, unsaved object. Set the properties, then call Save(). A script that forgets Save() looks as though it worked and changes nothing. In PowerShell, keep the object in a variable rather than chaining. Working examples $app = New-Object -ComObject 'hMailServer.Application' $app.Authenticate('Administrator', 'your-admin-password') | Out-Null # Add a domain $domain = $app.Domains.Add() $domain.Name = 'example.com' $domain.Active = $true $domain.Save() # Add an account $account = $domain.Accounts.Add() $account.Address = '[email protected]' $account.Password = 'a-strong-password' $account.Active = $true $account.MaxSize = 1000 # MB, 0 = unlimited $account.Save() # Report on every mailbox foreach ($d in $app.Domains) { foreach ($a in $d.Accounts) { '{0,-40} {1,8} MB used' -f $a.Address, [math]::Round($a.Size, 1) # Size is already in MB } } Three units, three objects. Account.Size is megabytes. Dividing it by 1MB again, which older examples do, prints 0 for every mailbox. Message.Size is kilobytes, and the Message size rule criterion is bytes. Find one account without walking every domain: $domain = $app.Domains.ItemByName('example.com') $account = $domain.Accounts.ItemByAddress('[email protected]') Send a message from a script. Message is a creatable coclass in its own right, and Save() on a new one queues it for delivery: $m = New-Object -ComObject 'hMailServer.Message' $m.FromAddress = '[email protected]' $m.From = 'Postmaster <[email protected]>' $m.AddRecipient('Alice', '[email protected]') $m.Subject = 'Scheduled maintenance tonight' $m.Body = 'The server will restart at 22:00.' $m.Save() The queue, and a backup: $app.Status.UndeliveredMessages # tab-separated text, one row per message $app.GlobalObjects.DeliveryQueue.ResetDeliveryTime() $app.GlobalObjects.DeliveryQueue.StartDelivery() $b = $app.Settings.Backup $b.BackupDomains = $true; $b.BackupSettings = $true; $b.BackupMessages = $true $b.Destination = 'D:\backups' $app.BackupManager.StartBackup() The [Settings] keys are reachable generically: $app.Settings.IniSettingNames # every key the server knows $app.Settings.GetIniSetting('ManageSieveServerPort') $app.Settings.SetIniSetting('ManageSieveServerPort', '4190') Up to 6.3.3 a change this way usually needed a restart. Since 6.3.4 the keys are kept in the database, and Settings.IniSettingApplies(name) says whether a save applies a key at once, at a reinitialise or at a restart; Settings.PendingIniSettings lists the saved ones the server does not run on yet. Settings: when a save takes effect, and the three levels has the counts and what becomes of the file's copy. What changed in 6.3.4: a shared object takes effect at Save() Some collections are not loaded for your COM session alone: the running server reads the very same items while it works. They are the routes and their addresses, the incoming relays, the DNS black lists, the SURBL servers, the remote domain policies, the server messages, the blocked attachments, the SSL certificates and the groups. Up to 6.3.3, setting a property on one of those items wrote the live object every SMTP, IMAP and delivery thread was reading, which let a session read a string while it was being freed. Since 6.3.4: The first property you set makes a copy, held by your COM object, and every later set writes the copy. Reading a property back from the same object reads what you set. Nothing is in effect until Save(). Save() writes the copy to the database and the server reads the collection again. A property set and never saved changes nothing at all. Up to 6.3.3 it changed the running server until the next restart while the database kept the old value. A script that relied on that must now call Save(). An object read before the save keeps what it read. After a Save(), read the item again from its collection (ItemByDBID, ItemByName) to see the saved values. A route's addresses. A change to an existing address waits for that address's Save(), like every item here. An address saved or deleted through Route.Addresses reaches the running route at once, without the route's own Save(), as it always did. A RouteAddresses list read before the route's save is the old route's list, though an address saved or deleted through it still reaches the running route. A save the database refuses leaves the running item as it was, and your object keeps the refused values, to be corrected and saved again. A remote domain policy is the exception: it is read back as the database holds it. A save whose re-read fails still answers success, because the save succeeded, and is reported as error HM7390: the change is in the database and not yet in effect, until a reinitialise, a restart, or another save. The Control Panel reads each item fresh, sets, saves and reads again, so it is not affected. The other collections were never shared this way and behave as before: TCP/IP ports, security ranges, the white list, the greylisting lists, blocked senders, sender list entries, and everything loaded for the COM object itself (domains, accounts, aliases, lists, rules, fetch accounts, app passwords, folder permissions). What 6.3.4 adds to the object model Six new interfaces make 100 coclasses, and the IDL has 1,884 HRESULT declarations against 6.3.3's 1,375. The wiki page lists every member; this is the shape of it. Interface Added in 6.3.4 Application Roles, CurrentRole, CurrentPermissions, RoleAssignments, AssignRole, ResetPassword, UnlockAccount Settings RemoteDomainPolicies; GetChangedFromDefault, GetSettingDefault; IniSettingApplies, IniSettingAppliesAll, PendingIniSettings; the profiles (GetProfiles, ApplyProfile, RevertProfile and the rest) Domain Rules; the quota policy; the domain transforms and disclaimer; SenderList; the DKIM rotation members; SpamTraps Account MoveTo; GetDevices, RevokeDevice; RoleName, RoleDomains, RolePermissions; the nine Directory properties; ExportArchive; Kind (0 a person, 1 a meeting room, 2 a piece of equipment) Utilities among others GetAuditTrail, VerifyAuditTrail, RevertAuditedChange, GetSetupChecklist, MoveMailbox, RunDkimRotation DistributionList Membership, and the list manager's members BackupManager, BackupSettings LoadEncryptedBackup, RestoreMailbox, RestoreMessage; EncryptionPassphrase (write-only), EncryptionPassphraseSet New RemoteDomainPolicies and RemoteDomainPolicy; SenderListEntries and SenderListEntry; GreyListingBypassEntries and GreyListingBypassEntry eServerState gains hStateDraining (5). The rule criteria gain eFTAttachmentName (9), eFTSensitiveContent (10), eFTAttachmentType (11), eFTExternalRecipient (12) and eFTContent (13), and the rule actions eRAAddDisclaimer (11), eRAReject (12), eRARequireTLS (13), eRAQuarantine (14) and eRAWarnSender (15). Roles over COM, and why API keys are not Since 6.3.4, authority is a role: serveradmin, domainadmin, helpdesk, auditor or a custom one, given to an account for named domains. Nothing changes on the day you upgrade: an account with no assignment holds exactly what its AdminLevel has always given it. An account with a role signs in with its own address and password, and COM asks the same question REST asks. Application.CurrentRole and CurrentPermissions say what the session holds. Administrator roles end to end has the permissions, the domains a role reaches and the Control Panel pages each role sees. Giving a role over COM is Application.AssignRole("[email protected]", "helpdesk", "example.test"), with an empty role name to take it away. Account.RoleName, RoleDomains and RolePermissions read it back. Custom roles are made over REST (POST /api/v1/roles), and COM has no way to delete a role at all. A help desk and an auditor over COM. A help desk's script has Application.ResetPassword, UnlockAccount and GlobalObjects.MessageTrace; COM's settings objects answer only the server administrator, so an auditor reads the configuration through the Deck or REST. Refusals that do not give anything away. Application.ResetPassword and UnlockAccount answer No such account. alike for an account that does not exist and for one the role does not reach. Settings.ClearLogonFailureList answers the server administrator alone. The question is asked where the write is made, not where the object was reached. Account.Save, Account.Delete, Account.MoveTo, Application.ResetPassword, the app passwords, and every write to what is in an account (FetchAccount.Save, Rule.Save, IMAPFolder.Save, Message.Save and the rest) each ask that the credential reach the account and take in its authority. An event script's objects carry no credential and are left as they were. API keys are not a COM object. The API key store has never been COM-visible. The key store (/api/v1/apikeys) answers to the administrator password and to nothing else, and a key is given a role with POST /api/v1/apikeys and "role":"helpdesk". The Control Panel's REST API keys page does not go through COM either: it edits hMailServerApiKeys.ini directly. What this server has changed from stock Since 6.3.4, Settings.GetChangedFromDefault answers every setting whose value is not the one this version ships with, with the shipped value beside it: the same document, byte for byte, as GET /api/v1/settings/changed. Only the administrator password may read it; an API key of any scope and an account holding an administrative role are both refused. The settings topic says what each entry carries. Distribution lists and the directory A list that defines itself. Since 6.3.4, DistributionList.Membership is a string property, read and written like the moderator and bounce addresses, holding a membership query whose accounts are added to the list's own recipients. A domain administrator over COM may set domain, a pattern: in a domain it holds, or nothing; the server administrator may set any form, and a query the server cannot answer is refused when it is saved. Aliases, distribution lists and membership queries has the five forms and how a query is answered. The directory. Since 6.3.4 an account has Account.DirectoryTitle, .DirectoryDepartment, .DirectoryPhone, .DirectoryMobile, .DirectoryOffice, .DirectoryOptOut, .DirectoryNote, .DirectoryPhoto (base64) and .DirectoryPhotoType (read-only). The row is read at the first get and written by Account.Save() with the rest. A change of the opt-out through Account.Save() is recorded on the audit trail as directory-opt-out-changed. The Import Tool The Import Tool is a Windows program (Start menu > hMailServer > Import Tool) that works through the COM API: it brings mbox files into an account, handing each message to the server through Utilities.ImportMessageFromFileToIMAPFolder, reads a Maildir with its flags, and creates accounts in bulk from a text file. Bringing mailboxes in covers each of its jobs. The breach check applies to a COM script. It is off by default. Where it has been switched on, it runs whenever a password is set through COM Account.Password, the Import Tool's included, and a bulk operation makes one request per account: run a large import against a mirror, or with the check off. What it does not do The COM API and the Control Panel do not exist on Linux, and there is no Import Tool on Linux. The Import Tool does not read PST files. The file goes into the server's import directory, and REST, hmctl or the Control Deck start the job. COM has no API keys and cannot delete a role. These features have no COM surface at all and are REST only: SCIM provisioning, SAML and OpenID Connect sign-in, passkeys, the S/MIME authority, the send API, migrations, IP warm-up, eDiscovery, mail in to a webhook and mailbox encryption at rest, among others the wiki lists. Where to read more The COM API and Scripting, sections 23.1 to 23.5, and APIs Reference, sections 8 and 9. Roles.md, ChangedFromStock.md, DistributionLists.md and Directory.md. Migration.md for the Import Tool, and Upgrading.md for what an upgrader's scripts now see. The public documentation's COM API chapter and its Import Tool section. On this forum: Upgrading to 6.3.4, the 6.3.4 announcement, event scripts, and the REST API for what COM does not reach.
  • 0 Votes
    1 Posts
    133 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) refuses a rule's Run function action when the rule is saved unless it names a plain function, for a rule of every owner, over COM and REST, and setting one on a domain's or an account's rule is held to the server administrator alone, where up to 6.3.6 a credential confined to named domains, a domain administrator among them, could set it. An event script that goes on in a transaction after Database.ExecuteSQL has failed is refused every statement after it once the database has ended the transaction, and Database.CommitTransaction then fails: call Database.RollbackTransaction and run the whole transaction again, or stop. A ScriptTimeout above 4,294,967 seconds is taken as that, where it ended every Windows event script at once, and the VBScript calls of OnClientValidatePassword, OnExternalAccountDownload and OnDeliveryFailed are quoted as every other call is. See the 6.3.7 announcement. hMailServer 6.3.8 (2 November 2026) adds a sixteenth event, OnIMAPMessageMoved, called once for each message an IMAP MOVE or UID MOVE moved between two folders of the account signed in, after the command and off the IMAP thread, with the account and thirteen values, in VBScript and JScript on Windows and JavaScript on Linux; it is off as shipped, and IMAPMessageMovedEvents (COM Settings.Scripting.IMAPMessageMovedEvents, imap_message_moved_events in PUT /api/v1/settings/scripting) switches it on (EventScripts.md). Work that outlasts a stop of the servers now writes its interruption to the debug log, not the ERROR log, so OnError is not called for it, and an event script's call that meets a stop fails with "The server is stopping. The request was not completed.", its script error going to the debug log. See the 6.3.8 announcement. This topic covers event scripts in hMailServer 6.3.4: the languages, switching scripting on, the fifteen events, the objects, the limits and errors, and the 6.3.4 security fix. If you run a Windows server older than 6.3.4 with event scripting on, that fix is the one thing to know first: a value written into a JScript handler's call, a password among them, could run code inside the server, and so could the name in a rule's Run function action, in either language. The section on CVE-2026-100741 below says what to do until the server is upgraded. What an event script is An event script is one file, EventHandlers.vbs or EventHandlers.js, in the server's Events directory, holding functions named after the events they handle. When scripting is on, the server calls the function for each event as it happens, hands it the objects the event is about (the client, the message, the account) and reads back what the function decided through the Result object. A rule's Run function action calls a function of the same file by name, and it too needs scripting on. Scripting is off as shipped, and with it off no handler is called. The file is loaded all the same: at start, at every reload, at Reload() and at POST /api/v1/settings/scripting/reload. Loading a file runs its code outside functions, once to compile it and once more for each of the fifteen handlers looked for, so keep the top level to declarations. A top level that throws, or runs past the timeout, is reported as a load that failed (HM5016, HM5710 or HM5021), whether scripting is on or not. A handler runs inside the server, on the thread that is doing the work, as the service's account: LocalSystem on a Windows installation that began before 6.3.4, NT SERVICE\hMailServer on a new one from 6.3.4, the hmailserver user on Linux. Everything behind a slow script waits, so try a script first on a server not carrying your mail. Which language runs where Language Windows Linux Engine File VBScript runs refused at load Windows Active Scripting (vbscript.dll) EventHandlers.vbs JScript runs refused at load Windows Active Scripting (jscript.dll), ECMAScript 3 EventHandlers.js JavaScript refused at load runs QuickJS 2025-04-26, built into the server, ES2023 EventHandlers.js Up to 6.3.3 a Linux server had no script engine and refused any script; since 6.3.4 it runs JavaScript in QuickJS, with the same fifteen events, function names and objects. Windows keeps VBScript and JScript unchanged. The language is one setting: Settings.Scripting.Language over COM (Windows only), "language" in PUT /api/v1/settings/scripting over REST, and the Language (VBScript, JScript or JavaScript) field on the Control Panel's Advanced page. Since 6.3.4 it accepts all three words on both platforms, in any case, and refuses any other word without storing anything. It is seeded VBScript on every platform, so a Linux server switching scripting on sets JavaScript first. Storing the language loads nothing. The script in force goes on running, in the language it was loaded in, until the next load. A language the build does not run is refused when the script is loaded: one line in the ERROR log, HM5016 at a start or HM5710 at a reload (the earlier script then stays in force), naming the language to use, once per load. While scripting is switched off this refusal is not reported, so switch scripting on and reload before reading the log for it. GET /api/v1/status says which languages a build runs, under build.script_languages: ["VBScript","JScript"] on Windows, ["JavaScript"] on Linux. Switching it on On Windows there are two Control Panel pages under Maintenance: Advanced, whose Scripting tab turns the engine on and chooses the language, and Event scripts, which edits the file. Save & reload writes a .bak first, then reloads and checks the syntax. Over COM the switch is Settings.Scripting.Enabled; Settings.Scripting.Reload() re-reads the file and CheckSyntax() returns the error text, where empty means clean. There is one file and no include mechanism. On Linux, from the command line: Set the language: hmctl settings set scripting --set-string language=JavaScript Write the file as the service's user: sudo -u hmailserver tee /var/lib/hmailserver/events/EventHandlers.js <<'EOF' function OnAcceptMessage(client, message) { if (message.Subject.includes('[URGENT]') && !client.Authenticated) { Result.Value = 2; Result.Message = 'Urgent mail comes from people who have logged on.'; } } EOF Switch scripting on: hmctl settings set scripting --set enabled=true Confirm which file the server reads: hmctl settings get scripting current_script_file Load it with POST /api/v1/settings/scripting/reload. POST /api/v1/settings/scripting/check answers {"result":""} when the file compiles and the error, with its line, when it does not. The fifteen events Every handler is optional. When the script is loaded the server checks which of these names the file declares and fires only those. Names are matched exactly: OnErrorLog is not a handler, the name is OnError. Handler Fires when Called with What Result.Value does OnClientConnect(client) any TCP connection is accepted, before a greeting the client 1 closes the connection at once OnHELO(client) the SMTP client sent HELO or EHLO the client 1 answers 554 5.7.1 Rejected; 2 answers 554 5.7.1 with Result.Message; 3 answers 453 4.7.0 with Result.Message OnClientLogon(client) an SMTP, POP3 or IMAP client finished a logon attempt, successful or not the client; client.Authenticated says how the logon went nothing: the logon has been decided OnClientValidatePassword(account, password) a password is being checked, before the built-in check the account and the password typed 0 lets the client in; 1 turns it away; 2 (as given) leaves it to the stored password OnSMTPData(client, message) the DATA command arrived, before the body the client and the message as OnHELO OnAcceptMessage(client, message) the body has been received and is about to be queued the client and the message as OnHELO OnRecipientUnknown(client, message) a RCPT TO named an address this server does not host the client and the message nothing OnTooManyInvalidCommands(client, message) an SMTP client sent too many invalid commands the client and the message nothing OnDeliveryStart(message) the first step of a delivery attempt, before virus scanning the message 1 deletes the message OnDeliverMessage(message) after virus scanning and global rules, before the recipient split the message 1 deletes the message OnDeliveryFailed(message, recipient, error) once per failed recipient; may fire several times per message the message, the recipient, the error text nothing OnExternalAccountDownload(fetchAccount, message, uid) a message was fetched from an external POP3 or IMAP account the fetch account, the message or null, its UID on the remote server 1 deletes it on the remote server now; 2 after Result.Parameter days; 3 never OnBackupCompleted() a backup finished nothing nothing OnBackupFailed(reason) a backup failed the reason nothing OnError(severity, code, source, description) anything is written to the ERROR log the error, as the ERROR log has it nothing OnError's severity is 1 Critical, 2 High, 3 Medium, 4 Low, and its code is the HM<nnnn> number without the prefix. OnClientValidatePassword setting 0 is a full authentication bypass for that account, on every protocol. A handler that bridges to another directory should fail closed: set 1 when the lookup errors, never 0. The objects a handler is given The objects an event has are also globals of the same names, HMAILSERVER_CLIENT, HMAILSERVER_MESSAGE, HMAILSERVER_ACCOUNT, HMAILSERVER_FETCHACCOUNT, Result and EventLog, and the parameter and the global are the same object. An object an event does not have is null as a parameter; as a global it may not be defined at all (the message of an external download that failed), and reading an undefined global throws a ReferenceError, so a handler reads its parameter. Do not use Result in the seven events that are notifications. EventScripts.md says it is not defined at all in OnClientLogon, OnRecipientUnknown, OnTooManyInvalidCommands, OnDeliveryFailed and OnError; the public documentation adds that the server makes no Result for OnBackupCompleted and OnBackupFailed. Object Members On JavaScript (Linux) Client Port, IPAddress, Username, HELO, Authenticated, EncryptedConnection, CipherVersion, CipherName, CipherBits, SessionID the same, all read-only Message ID, Filename, Subject, From, FromAddress, To, CC, Date, Body, HTMLBody, Charset, Recipients, AddRecipient(name, address), ClearRecipients(), HeaderValue(name), HasBodyType(type), Flag(flag), Size (KB), State, DeliveryAttempt, UID, InternalDate, Copy(folderId), RefreshContent(), Save(), Attachments, Headers Attachments and Headers are not there; SetHeaderValue(name, value) and SetFlag(flag, on) write; InternalDate is text Result Value, Message, Parameter Value and Parameter take a whole Number only EventLog Write(text) the same FetchAccount the COM FetchAccount readable, not assignable; Password, Save(), DownloadNow() and Delete() are not there Account the COM Account, freshly loaded the properties that describe the account are readable, not assignable; Password, the collections and every method are not there Save() is what writes a change to disk. Changes to the subject, the body and the headers are written to the message file by Save(), on both platforms; a handler that does not call it changes nothing on disk. Do not call it in OnSMTPData: the public documentation says the message is still being processed there and saving corrupts it, and that only the character set, the file name, the envelope sender and the recipients are reliable at that point. Change a message in OnAcceptMessage. Result is read after the handler returns. On JavaScript a string ('1' too), a boolean, null, undefined, NaN, a fraction or an object assigned to Value or Parameter throws a TypeError, and the event goes on with the Result it had. So in OnClientValidatePassword, Result.Value = decisions[user] for a user the table does not have leaves 2, the stored password. On Windows, COM turns a boolean, undefined, a fraction and a string of digits into numbers, and undefined into 0: the same line in JScript lets that client in. EventLog.Write is the debugger. It appends a line to hmailserver_events.log in the log directory, and there is no breakpoint. A rule's Run function A rule's Run script function action calls the named function as <function>(HMAILSERVER_MESSAGE), then reloads the message from disk, so anything the function saves is seen by the rest of the rule set. Since 6.3.4 it works on Linux, where the function is a JavaScript one in EventHandlers.js. Since 6.3.4 the action names the function alone, as a plain identifier: ASCII letters, digits, _ and $, not starting with a digit. StampReceivedDate qualifies; StampReceivedDate(x) does not. On Windows, in VBScript or JScript, any other name is refused as HM5018 each time the rule matches and the function is not run. On Linux such a name is never evaluated and finds no function: nothing is run, and the ERROR log has a Script Error line, not HM5018. A ported handler JScript and JavaScript share the file name, so a JScript file carried from Windows to Linux is the file the JavaScript engine is given. EventScripts.md's example starts from VBScript on Windows: Sub OnAcceptMessage(oClient, oMessage) If InStr(oMessage.HeaderValue("X-Spam-Flag"), "YES") > 0 And Not oClient.Authenticated Then oMessage.HeaderValue("X-Quarantine") = "yes" oMessage.Subject = "[SPAM] " & oMessage.Subject oMessage.Save EventLog.Write("Tagged spam from " & oClient.IPAddress) End If End Sub The same handler for a Linux server, in EventHandlers.js: function OnAcceptMessage(client, message) { if (message.HeaderValue('X-Spam-Flag').includes('YES') && !client.Authenticated) { message.SetHeaderValue('X-Quarantine', 'yes'); message.Subject = `[SPAM] ${message.Subject}`; message.Save(); EventLog.Write(`Tagged spam from ${client.IPAddress}`); } } What changes in a port: Parameterised properties have no assignment. message.HeaderValue('X') = 'v' becomes message.SetHeaderValue('X', 'v'), and message.Flag(n) = true becomes message.SetFlag(n, true). The reading forms are unchanged. Names are case-sensitive and spelled as the IDL spells them. JScript found message.FileName; JavaScript needs message.Filename. No CreateObject, no new ActiveXObject. Both exist on JavaScript only to throw a TypeError that says so. null, not Nothing, for an object an event does not have. The limits A fresh engine for every event, on Windows and Linux alike: the whole file is parsed and run, the handler is called, and the engine is freed before the event goes on, so nothing a script makes outlives the event. No file system, no network, no processes on JavaScript. QuickJS keeps these in quickjs-libc.c, and that file is not in the tree. No module loader is installed, so import() is refused and an import statement is a syntax error; there is no print and no console. What a handler can change is what its objects change: a message's file through Save(), a copy into a folder through Copy(), the event log through EventLog.Write. On Windows, ScriptAllowedObjects decides what a script may create with CreateObject or new ActiveXObject. Its shipped value is *, every class. Empty means no class beyond the server's own objects, and a comma-separated list of ProgIDs means only those. Up to 6.3.3 it applied at a service restart; since 6.3.4 a save applies it. It has nothing to apply to on Linux. ScriptTimeout is in seconds, with 0 for no limit. It is kept in the settings store: set it with PUT /api/v1/settings/ini/ScriptTimeout, COM Settings.SetIniSetting or the Control Panel, not by editing hMailServer.ini. Limit Platform Value When a handler reaches it ScriptTimeout Windows 60 seconds by default HM5019; the interrupt cannot release a handler blocked inside a COM call ScriptTimeout Linux 60 seconds by default HM7341; the event goes on with whatever Result the handler had set Memory Linux 64 MB for each run, a constant HM7342; the event goes on Stack Linux 512 KB of the calling thread's stack a runaway recursion throws an InternalError The Linux timeout does not stop one call into a built-in that loops in the engine's C code without polling, such as includes or indexOf on an object whose length is huge: do not call an array method on an object whose length the handler has not checked. Errors, and where they are written What Where A script that does not compile, or whose top level throws HM5016 at start (nothing loaded), HM5710 at a reload (the earlier script stays in force) A top level that runs past the timeout HM5021, and the load refused An exception a handler does not catch one Script Error line in the ERROR log; the event goes on The load is all or nothing on every engine, so a typo in a reload never switches a working OnClientValidatePassword off. The 6.3.4 security fix: CVE-2026-100741 Up to 6.3.3 a password could run code inside the server. On Windows, with event scripting on, the language JScript and an OnClientValidatePassword handler in EventHandlers.js, any client that could reach a sign-in and name an existing, active account could have JScript of its own run inside hMailServer.exe, with the rights of the service's account, before it had authenticated. The server built each call as source text and escaped the apostrophe but not the backslash before it. With ScriptAllowedObjects at its shipped *, that script could create any COM object, WScript.Shell among them. Path Who could use it Open in The password given to OnClientValidatePassword any client that can reach a sign-in JScript The UID given to OnExternalAccountDownload a remote POP3 server JScript The error text and failed recipient given to OnDeliveryFailed a remote mail server JScript The name in a rule's Run function action whoever may edit a rule, a domain's own administrator included JScript and VBScript Which servers are affected. The flaw is CVE-2026-100741; the record was not yet public when this was written. The flaw is in every release from 6.0.0 to 6.3.3: the escaping was checked at every one of those tags (GitLab issue 27). Treat any server older than 6.3.4 with event scripting on and JScript as its language as affected. A rule's Run function action wrote its function name into the call unescaped, in either language, so VBScript servers too. What 6.3.4 changes. Every JScript value is escaped in one helper, ScriptServer::EscapeForJScriptLiteral: every backslash first, then both quotes, CR, LF, U+2028, U+2029 and NUL; OnBackupFailed's reason, which neither language escaped and the table above does not list, is escaped in both. A rule's Run function name must be a plain identifier. On Linux the handler is called with values as values, so nothing is quoted into source text. A JScript handler now receives every value exactly as it was, backslashes included; one that undid the old half-escaping by hand must stop doing so. Until you can upgrade, switch event scripting off. That is the only measure that closes every path, in either language. A server that cannot do without its scripts can narrow what is open: Take the OnClientValidatePassword, OnExternalAccountDownload and OnDeliveryFailed handlers out of a JScript EventHandlers.js and reload the scripts. Check the reload: one that fails (HM5710 in the ERROR log) keeps the earlier script in force, those handlers included. In either language, let nobody but the server administrator make or edit a rule with a Run function action. Roles.md lets an account's rules be written over COM by the server administrator, an administrator of its domain, or the account itself, so "whoever may edit a rule" is wider than administrators. Narrow ScriptAllowedObjects from *, which limits what an injected script can create. The key exists from 6.2.25, and before 6.3.4 it applies only after a service restart. Moving a handler to VBScript closes the password, UID and error-text paths and leaves the rule path open. Before you upgrade, check your rules' Run function actions and put the function's bare name in any that does not qualify. After it, an HM5018 in the ERROR log (on Linux, a Script Error line naming the function) means one was missed. Upgrading to 6.3.4 covers the upgrade itself. Reporting a vulnerability goes through a confidential GitLab issue or the Service Desk address contact-project+hmailserver-hmailserver-86730559-issue-@incoming.gitlab.com, and not through a public issue or this forum. What it does not do OnClientValidatePassword is not consulted for SCRAM, which has no password to give it. Where to read more EventScripts.md: the member-by-member contract of every object, and the engine. The COM API and Scripting, sections 23.6 to 23.10. Rules and Sieve for the Run script function action, and Security Hardening, section 17.11. The public documentation's event scripts chapter. On this forum: hMailServer on Linux: what you get and what you do not, the 6.3.4 security announcement, the COM API on Windows and rules and the Run function action.
  • 0 Votes
    1 Posts
    107 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) changes the anti-virus action that removes attachments (Remove all attachments and deliver the text in 6.3.7's Control Panel, Delete infected attachments only in 6.3.6's): an S/MIME message signed as one part, which is opaque but not encrypted, is now replaced by the action's note when the scanner finds something in it, where every earlier version delivered it as it arrived, and a message the server cannot read is deleted, its recipients told; an encrypted message is still delivered as it is (work item 92). See the 6.3.7 announcement. In hMailServer 6.3.8 (2 November 2026) the web services listener refuses an address an auto-ban or an IP range bans (work item 105), so an IP range of your own that allows some addresses none of SMTP, POP3 and IMAP now closes that listener to them, and with it the Web Key Directory: check that the clients that look keys up there do not come from such a range. See the 6.3.8 announcement. This topic is about encrypting the message rather than the connection, so that the person it is for can read it and nobody on the way can, this server included. S/MIME in the webmail has been there since 6.3.2; everything else here is new in 6.3.4. The design rule running through all of it is that this server holds nothing that decrypts a person's mail: keys are made in the browser and kept wrapped under a passphrase or password the server never sees. The one thing to know before you allow any of it is the cost of that rule: nothing on this server can recover mail encrypted to a key that was lost. There is no escrow. Which one you want You want Use S/MIME inside the organisation without buying a certificate per person A domain's S/MIME authority Correspondents who use GnuPG, Thunderbird, Proton or Mailvelope OpenPGP in the webmail, and the Web Key Directory To write privately to somebody who has no key at all A password-protected message A mailbox the administrator cannot read Zero access Everything to one partner organisation encrypted, whatever the sender's client did Gateway encryption, on a remote domain policy A stolen disk or backup to hold nothing readable, while the mailbox works as before Mailbox encryption at rest, which the server can read and so is not in this topic All of the browser cryptography needs a page opened over HTTPS: a browser withholds crypto.subtle from any other, and the page says so instead of offering Sign and Encrypt. S/MIME with your own authority Since 6.3.4 (schema 6095). A domain may have its own S/MIME authority: a root certificate the domain owns, which certifies the keys its accounts make in the webmail, publishes them for colleagues to look up, and says what it has revoked in a signed CRL. Nothing is issued until an administrator creates one. Who holds what. The person's private key never leaves the browser. The webmail makes the key pair, builds a PKCS#10 request, wraps the private key under the account password, and sends only the request. The authority's own key is on the server, because it signs there, and it is protected by the platform's secret store: Where the authority's key opens Windows DPAPI with the machine's key. It opens on this machine and nowhere else, and on this machine to any process that asks Linux The stored-secret key file, <DataFolder>/.hmailserver-secret-key (AES-256-GCM, mode 0600). It opens wherever that file is It never falls back to the legacy scheme. When the store is unavailable the authority is not created (503). Creating it. In the Control Deck, open the domain and find S/MIME authority, or POST /api/v1/domains/{domain}/smime-ca with {"key": "p256"} or "rsa3072". The root is self-signed, CA:TRUE with pathlen:0, valid ten years, with a critical name constraint permitting the domain's own addresses and nothing else. P-256 is the default. The Control Panel shows nothing of it, and there is no COM surface. Enrolment. A person finds Get a certificate from {domain} under Settings → Security → S/MIME. They pick the key type. RSA 2048 is the default and the one to choose. A P-256 certificate from this page signs, and neither this webmail nor a colleague's can encrypt to it. The page makes the key pair and the request, and asks for the account password that wraps the private key before anything is sent. POST /api/v1/me/smime/enroll is made from the account's own sign-in. An app password and an administrator's support session are refused with 403. The server checks the request. It must name exactly one address, and that address must be the account's own or an active alias of its own domain that points at it. The certificate is issued pending. When it arrives in the account's own store it is confirmed, and only then is it answered to colleagues and the address's earlier certificates revoked as superseded. The limits are five enrolments a day (429) and ten valid certificates at once (409). A person with two devices ticks Keep my other certificates for this address on the second. What is issued. Valid one year. With smime_authority_base_url empty, which is the default, a certificate carries no CRL distribution point. What that costs: Outlook and Thunderbird cannot fetch the CRL from the certificate, so a revocation reaches them only when they have been given the CRL another way. This webmail does not use the CRL at all; it reads the authority's own record. Publication and trust. The root and the CRL are served over plain HTTP by the web services listener (WebServicesHttpPort), at /smime-ca/{domain}.pem, .cer and .crl. A signature verifies in Outlook, Thunderbird, Apple Mail or a phone only once the root is installed on the clients, by group policy, a device-management profile or by hand. This webmail trusts the domain's authority by itself. A colleague's certificate is looked up with GET /api/v1/me/directory/{address}/smime, which the webmail asks on its own when Encrypt is ticked. Moving and restoring. The authority is in the configuration backup, but its key opens only where its envelope opens. Restored anywhere else, the Deck shows a warning, enrolment is refused with 503, its CRL is not served, and the authority must be rotated there. Rotation makes a new root and retires the old one without deleting it, and clients must install the new root beside the old. What it is not. Not Active Directory Certificate Services. No OCSP. No key escrow: a forgotten password that the key was wrapped under leaves the mail encrypted to that key unreadable, by anybody, for ever. No elliptic-curve encryption in the webmail. One address per certificate. No automatic renewal: a person enrols again before the year is out. OpenPGP: the half in the directory and the half in the browser The directory The page Where it runs On the server In the person's browser What it holds Public keys only Own keys, the secret half encrypted under a passphrase, and correspondents' public keys What cryptography it does None at all All of it: signing, encrypting, decrypting, verifying, in PGP/MIME Table and schema hm_pgpkeys, schema 6050 hm_pgpring, schema 6080; the trust columns 6081 State as shipped Three switches, all off portal_enabled on In neither half does the server sign, verify, encrypt or decrypt a message. It makes an OpenPGP message itself in only two places further down, a delivery to a zero-access mailbox and gateway encryption to a partner's key; there it encrypts, never signs or decrypts, and verifies only the binding of the subkey it encrypts to. The directory publishes keys at a URL derived from the address, so that anybody who has the address can find the key. <hash> is the local part, lower-cased, SHA-1 hashed and z-base-32 encoded. Both methods are served: Method The key advanced https://openpgpkey.<domain>/.well-known/openpgpkey/<domain>/hu/<hash> direct https://<domain>/.well-known/openpgpkey/hu/<hash> Three switches have to be on before one key is served: The server: OpenPGPWebKeyDirectoryEnabled, set with hmctl settings set openpgp --set web_key_directory_enabled=true or PUT /api/v1/settings/openpgp. The domain: openpgp_publish on the domain. The account: openpgp_publish on the account. This one is the person's own, through PUT /api/v1/me/openpgp/publish. The web services listener has to exist (WebServicesHttpsPort), and the advanced method, which clients try first, needs an address record for openpgpkey.<domain> and a certificate valid for it. Keys are served over HTTPS only. Every refusal is the same 404, whether the domain is unknown, a switch is off, or the account has no key, so that the directory cannot be used to ask whether somebody has an account. Put a key in with hmctl openpgp store [email protected] ann.asc --publish, and check it as a correspondent's client will: gpg --locate-keys --auto-key-locate clear,wkd [email protected] An upload carrying a secret-key packet is refused; so are a version 3 key, a key with no user id or no self-signature, a key past OpenPGPKeyMaximumBytes (65536), and a key whose user ids do not carry the account's own address. An account may hold OpenPGPKeysPerAccount keys (8). A signature on an uploaded key is not verified cryptographically: what makes a key in this directory the right one is that its owner or an administrator put it there. The desktop Control Panel has nothing for the directory yet. The page is /portal-openpgp.js, written on the Web Crypto API with no library vendored. Keys. Curve25519 (the default) or RSA 3072 or 4096, made in the browser. The secret key is stored as OpenPGP writes one, under the passphrase, so the bytes the server holds are bytes it cannot open, and the same bytes import into GnuPG. A revocation certificate is made at the same moment and saved to the device. Autocrypt Level 1, both ways. Finding a key. GET /api/v1/me/openpgp/lookup?address= asks the correspondent's own Web Key Directory (lookup_wkd, on) and then a keyserver (lookup_keyserver, off by default; keyserver_url is https://keys.openpgp.org). Nothing is stored by the lookup. Trust made visible. A different key arriving for an address the ring already holds a key for is stored as a change. Until the person accepts it, the page does not encrypt to it. A key can be marked verified in person, and the mark is never inherited by a key that replaces it. Who may write. Every write to the ring is refused to an app password and to an administrator's support session. Not implemented, and refused by name: version 5 and 6 keys and AEAD; DSA and ElGamal keys and the NIST and Brainpool curves; smartcards and hardware tokens; the web of trust; a draft or a scheduled send that is to be sealed. Curve25519 needs Chrome 137, Firefox 130 or Safari 17 or later. Both directions are proved against GnuPG 2.4 by build/check-portal-openpgp.js, which GitLab CI runs on every push and every merge request. A password-protected message (schema 6082) is for the correspondent with no key. The page encrypts the text and attachments to a password as one OpenPGP message, and what goes out by SMTP is a short notice with a link to /portal/protected/<token>, where the recipient's browser decrypts it. The link is the whole access control. It needs portal_enabled on and a public address for the links (the confidential_base_url chain the messages that expire use), and is offered only over HTTPS. A message lives protected_message_days (30 by default). The ceilings are 25 MB a message and 250 MB an account. It does not promise that the recipient cannot copy what they read. Zero-access mailboxes Since 6.3.4 (schema 6083). A mailbox this server holds and cannot read. An administrator allows it server-wide once (zero_access_allowed, under /api/v1/settings/openpgp, off by default); the person then switches it on for their own account on the webmail's Security page. From then on every message delivered to the account is encrypted to the account's own OpenPGP key - after the spam and virus filters, the rules and the Sieve script have had the text - and stored as PGP/MIME with the headers in the clear. An administrator sees the switch read-only everywhere. What it costs. The webmail refuses to switch until the recovery key has been typed back and the sentence about a lost password has been ticked. Cost What it means The administrator cannot read the mail The message source and the account export answer 403 Neither the administrator nor the server can search it The index and IMAP SEARCH read the headers, never the text. A word in the subject is found; a word in the body is not Nobody can recover it A forgotten password without the recovery key is the mail lost, for ever IMAP and POP see ciphertext Valid PGP/MIME that a client holding the key opens. Until then the account is a webmail account The plaintext was on the disk From acceptance until the seal. The account's copy is overwritten with zeros; whether the bytes are gone is the filesystem's decision Only what is delivered is sealed, and the Sent copy A draft, and anything a client uploads with IMAP APPEND, is stored as it is A journal rule sees the plaintext Journaling runs at acceptance, before delivery The filters have read it A forward goes on in the clear. A rule that files into a public folder files the plaintext there. Quarantined mail is held in the clear Switching off decrypts nothing Mail that arrived while it was on stays encrypted to the key The headers the server kept. From, To, Cc, Date, Subject, Message-ID, the Received trace and the rest stay in the clear, so that the listing, the threading and the spam folder work without the key. The server adds X-hMailServer-Zero-Access: sealed on delivery. The key, in two sealed copies. hm_zeroaccess holds two copies of the secret key, both sealed in the browser: one under a passphrase the page derives from the account password with Argon2id, three passes over 64 MiB in four lanes. It was measured at 1.6 seconds in Node 24 on the development machine; a phone is expected to take several seconds, and it was not measured on one; one under the recovery key: 30 random octets shown once as eight groups of six base32 characters. A key that carries an expiry is refused at the switch-on. When a seal fails, the delivery is refused rather than stored in the clear: the sender is told with 4.3.0 and the error log carries HM7070. Passwords. A change through the webmail carries the key sealed again under the new password, and the server refuses a change without it (409, zero_access_rewrap). A password an administrator sets cannot re-wrap; at the next sign-in the page asks for the previous password or the recovery key. It is not encryption at rest. Mailbox encryption at rest stores mail under a key the server holds while the person is signed in. The two are never on one mailbox: each switch is refused where the other is on. Not built: a local bridge for IMAP or POP clients; search over the decrypted text in the browser; encryption of mail already in the mailbox when the switch is thrown. What the server does with a sealed message A sealed message is one signed or encrypted end to end, which must leave this server byte for byte as it arrived. An opaque one is a sealed message the server cannot read at all. A multipart/signed message is sealed but not opaque: its text is in the clear. Opaque is decided from the body, never from the label, or every check that stands aside for an encrypted message would be one header away from being switched off by anyone sending mail. The body must be S/MIME whose one part begins a CMS object (enveloped-data, authenticated-enveloped-data or signed-data), or PGP/MIME of exactly two parts: Version: 1, then one armoured OpenPGP message. None of what follows is a setting. Part of the server What it does with an opaque message Blocked-attachment list Never rewrites it. One application log line says the list was not applied. In 6.3.3 and earlier, blocking *.asc or *.p7m destroyed the ciphertext without a log line Virus scanner Scans it, and logs that its content COULD NOT BE EXAMINED for viruses. It is not treated as a scanner failure, so AVFailAction=1 does not hold it. If mail the server cannot scan must not be delivered, use a rule or a Sieve script on body :raw Delete attachments action (Remove all attachments and deliver the text since 6.3.7) Never strips an encrypted message. Since 6.3.7 an S/MIME message signed as one part - application/pkcs7-mime holding signed-data, opaque but not encrypted - is replaced by the action's note when the scanner finds something in it, where it was delivered as it arrived (work item 92). Set the action to delete the message if such mail must not be delivered Rules A body criterion does not match, whichever way it is phrased, with one log line per rule per message: under AND the rule does not fire; under OR its other criteria still count. To catch encrypted mail, test Content-Type for multipart/encrypted or application/pkcs7-mime with a custom-header criterion Sieve The evaluation is unchanged, as RFC 5173 requires; a log line is new. Use body :raw to recognise such a message Search and the index Only the header is indexed Content spam scoring Sees nothing in it, so encrypted spam is scored more leniently. SPF, DKIM, DMARC and DNSBL are unaffected A message only labelled encrypted, whose parts are readable, is blocked, stripped, filtered and searched like any other. In SealedMail.md's words: a message that arrives encrypted is not scanned, and your mail client, and you, are the last check. Gateway encryption, briefly Since 6.3.4 (schema 6094). On a remote domain policy, Encrypt mail to this domain at this server encrypts every message to one partner, here, to the partner's own gateway key. It encrypts at the server, as a delivery to a zero-access mailbox does, and the server holds no private key for it. The Subject is not hidden. When a message cannot be encrypted the default is to refuse it with 5.7.10. It does not sign, look keys up, or decrypt replies. Where to read more Operator documents at the v6.3.4 tag: SmimeAuthority.md, OpenPGP.md, ZeroAccess.md, SealedMail.md and RemoteDomains.md. Wiki: Encryption for People and Encryption and Certificates. Public documentation: 36.6 The portal: what a user gets. On this forum: The webmail portal at /portal, The Control Deck, Moving to new hardware before you carry an authority's key to another machine, and the rest of this category.