hMailServer by Progressive Robot

hMailServer 6.3 Documentation

The complete guide to running your own mail server on Windows or Linux — from first install to hardened production. Written for people who have never run a mail server before, and thorough enough for people who have.

  • Version 6.3.8
  • Windows 10 1607+/11 & Server 2016+, x64 (ARM64 as a preview) · Linux x86-64 and AArch64
  • AGPL-3.0-or-later — free, including commercially
  • SMTP · IMAP · POP3

Part IGetting started

1. What hMailServer is, and what it is not

hMailServer is a mail server — the software that owns an email domain. When someone sends a message to [email protected], their mail server looks up where yourcompany.com receives mail and delivers it to whatever is listening there. If you run hMailServer, that is your machine.

It does three jobs:

Job Protocol What it means
Accept mail from the world SMTP Other mail servers connect and hand you messages for your users
Let your users read mail IMAP and POP3 Outlook, Thunderbird, Apple Mail and phones connect to fetch messages
Let your users send mail SMTP submission Your users hand you messages and you deliver them onward

What hMailServer is not:

  • It is not a full groupware suite, though it now carries the parts of one that ride on open standards. There is a browser inbox now — the self-service portal at /portal, served by the REST listener out of the binary itself (chapter 36), and since 6.3.0 it is a real webmail: folders, search, compose, drafts, flags and inline images. Since 6.3.3 it keeps the account’s address book, served over CardDAV, and since 6.3.4 its calendars and tasks, served over CalDAV. Most users will still read their mail in Outlook, Thunderbird, Apple Mail or a phone, and a separate webmail package such as Roundcube pointed at hMailServer’s IMAP remains a perfectly reasonable choice.
  • It is not a spam filter you can point at someone else’s mail. It filters mail it receives, for domains it hosts.
  • It is not Exchange. There is no Teams integration, no ActiveSync and no MAPI. The calendars 6.3.4 added, shared calendars and rooms among them, are served over CalDAV and in the webmail, not over Exchange’s protocols. It is email — done properly — and what a mail server needs beside it.

Who should run it: a small business that wants its own mail on its own server; a developer who needs a real SMTP/IMAP endpoint for testing; a hosting provider running mail for many domains; anyone who would rather not pay per mailbox forever.

Who should not: if nobody in your organisation is comfortable editing DNS records and watching a log file occasionally, a hosted mailbox provider will serve you better. Running a mail server is not hard, but it is not zero-maintenance either.

About this fork

Progressive Robot Ltd forked hMailServer in 2026 and maintains the 6 line. The original project, Martin Knafve’s, has resumed development, and its forum at hmailserver.com is online. This fork brings the 6 line up to current standards:

  • On Windows, built with Visual Studio 2026, OpenSSL 4.0.3 (since 6.3.6; 4.0.2 up to 6.3.5) and Boost 1.92 — 64-bit only
  • Runs on Linux since 6.3.0 — x86-64 and AArch64 from the same source tree (from 6.3.6 it is packaged for both again, as the releases up to 6.3.3 were; 6.3.4 and 6.3.5 were packaged for x86-64 only), built with clang or GCC, taking OpenSSL and Boost from the distribution rather than from those pins (chapter 35)
  • TLS 1.2/1.3, modern password hashing (PBKDF2-HMAC-SHA256, optional Argon2id or scrypt)
  • MTA-STS, DANE with DNSSEC validation, ARC, DMARC, Ed25519 DKIM
  • Automatic Let’s Encrypt certificates built in — no manual renewal
  • A modern .NET 10 administration application replacing the old WinForms tool
  • A REST API that writes as well as reads, a browser Control Deck, a built-in webmail portal and Prometheus metrics (chapters 24 and 36)
  • Automated regression tests on every release — at 6.3.8, 4,569 of them, run in the release tag’s own pipeline in two halves on the project’s two Windows benches: 4,489 passed, 0 failed, 80 skipped — plus the Linux suite on one of the project’s Linux runners: 2,749 tests, 2,374 passed, 0 failed, 375 skipped

Almost everything new is additive and off by default, and an existing hMailServer installation upgrades without behaving differently until you turn something on. The standing exception is outbound transport security: honouring recipients’ MTA-STS policies and DANE records, with DNSSEC validation, is on from the first start (MtaStsEnabled, DaneEnforcementEnabled and DnssecValidationEnabled all default to 1), and each can be switched off. 6.3.4 is the larger exception: twenty-four things change on the upgrade without a switch — the settings move into the database, SPF follows RFC 7208, the audit trail and alerts are on — and §18.5 lists the ones an administrator meets first. A new installation of 6.3.4, unlike an upgrade, also starts secure: a self-signed certificate, STARTTLS offered on ports 25, 587, 110 and 143, TLS required before a password is taken from the Internet IP range (mail from other servers on port 25 is still taken with or without TLS), and on Windows the service running as NT SERVICE\hMailServer rather than LocalSystem unless the installer is told otherwise.


2. How email actually works

Skip this chapter if you already know it. If you do not, the next eight chapters will make far more sense with it.

Delivering a message

You send mail to [email protected]. Here is what happens:

  Your mail client                 Your server              Example.com's server
  ───────────────                  ───────────               ────────────────────
        │                               │                             │
        │──── SMTP submission ─────────▶│                             │
        │     (port 587, you log in)    │                             │
        │                               │                             │
        │                        DNS: "who receives                   │
        │                         mail for example.com?"              │
        │                        Answer: the MX record                │
        │                               │                             │
        │                               │──── SMTP delivery ─────────▶│
        │                               │     (port 25, no login)     │
        │                               │                             │
        │                               │                    stored in Alice's
        │                               │                        mailbox
        │                               │                             │
        │                               │                             │◀── IMAP ─── Alice

Two facts do most of the explaining:

  1. Port 25 has no password. Any server on the internet may connect to your port 25 and offer you mail for your own domains. That is how email works — it has to be open, or nobody could write to you. This is also why spam exists, and why chapters 8, 10 and 17 matter.
  2. Your users use a different door. Port 587 (submission) requires a login. Never let an unauthenticated stranger send mail through you to the outside world — that is an open relay, and it will get your server blacklisted within hours. hMailServer refuses this by default. On a new installation of 6.3.4 (not an upgrade) the login must also wait for TLS: from another machine, AUTH is not offered before STARTTLS and is refused with 530 if it is sent, so set mail clients to STARTTLS (chapter 7). Mail from other servers on port 25 is taken with or without TLS, as before.

The three things that make mail arrive

A message you send is judged before it is delivered. Three DNS records decide whether it lands in the inbox or the spam folder — or is rejected outright:

  • SPF — a list of the servers allowed to send mail for your domain.
  • DKIM — a cryptographic signature proving the message really came from you and was not altered.
  • DMARC — a policy saying what to do with a message that passes neither SPF nor DKIM for the domain in its From address, and where to send reports.

Chapter 8 sets all three up. Without them your mail will go to spam. This is the single most common reason a new mail server “doesn’t work”.

More: How email works, and what hMailServer is and is not on the forum.


3. Before you install: the checklist

Work through this before you download anything. Skipping it is the usual cause of a painful first day.

3.1 A machine to run it on

  • Windows: Windows 10/11 (Windows 10 needs version 1607 or later), or Windows Server 2016 or newer. 64-bit (x64) only: setup refuses to start on anything older or on 32-bit Windows. For Windows on ARM, 6.3.8 publishes an installer of its own as a preview: it has never run on ARM64 hardware, and SQL Server Compact has no ARM64 build, so a new installation there is set up against SQL Server or PostgreSQL (§4.1). Since 6.3.4 a new installation runs the service as the virtual account NT SERVICE\hMailServer, which may write only in the program folder’s own Data, Logs, Temp and Database folders and in the folders you approve for it when the installer asks (or later, when Grant access on Maintenance → Server limits & expert settings, card Windows service account, asks). Any other folder on this machine that the service writes, a backup folder above all, or a data or log folder kept somewhere else, has to be a folder of its own on a fixed local drive (not a drive’s root), outside Program Files, the Windows folder and the user profiles folder: a folder anywhere else in the program folder is never granted, approved or not. If SQL Server on the same machine is to be reached with Windows authentication, give NT SERVICE\hMailServer a login there first (§17.3).
  • Linux: since 6.3.0, x86-64 or AArch64 with systemd — from the .deb, the .rpm, the PKGBUILD or the AppImage. 64-bit only. 6.3.8 is published for both architectures: a .deb or an .rpm of 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, the generic .deb and .rpm (the .deb is Ubuntu 26.04’s build), and the two AppImages; the AArch64 files are cross-compiled and tested under emulation, since no AArch64 machine has run them. Install your release’s own package (§35.2). Chapter 35 covers it end to end, including what a Linux server does not have: no Control Panel, no COM, event scripts in JavaScript only (since 6.3.4; none before). Per-domain DKIM signing is set over the REST API or on the Control Deck (§24.15).
  • Minimum 2 GB RAM and 20 GB free disk. Mail accumulates; size the disk for how much mail you will keep, not how much you have today.
  • The machine must be on all the time. A mail server that is asleep is a mail server that is losing mail.

3.2 A domain name

You need a domain you control — yourcompany.com — and the ability to edit its DNS records. If your domain is with a registrar like Namecheap, Cloudflare, GoDaddy or 123-Reg, you have this.

3.3 A static public IP address — and port 25 open

This is the step that stops most home installations.

  • Your server needs a static public IP. A dynamic home IP will work badly and be widely blacklisted.
  • Port 25 inbound must reach your server. Most consumer ISPs block it. Business connections and cloud/VPS providers usually allow it, sometimes on request.
  • Port 25 outbound must work too. Many cloud providers (notably Azure and AWS) block outbound 25 by default and require you to ask.

Check before you commit. If your ISP blocks port 25, you cannot receive mail directly. You can still run hMailServer and relay outbound mail through a smart host (chapter 14), but inbound mail will not reach you.

3.4 Reverse DNS (PTR)

Your IP address must resolve backwards to your mail server’s name. If mail.example.com is 203.0.113.10, then 203.0.113.10 must resolve back to mail.example.com.

Only your ISP or hosting provider can set this — it is not in your domain’s DNS. Ask them. Many large providers (Google, Microsoft) reject mail from IPs with no matching PTR record, so this is not optional.

3.5 A database

hMailServer stores accounts, domains, message metadata and settings in a database; since 6.3.4 that includes the [Settings] keys, of which hMailServer.INI keeps only a copy. The messages themselves are files on disk. You have four choices:

Database Good for Notes
Built-in (SQL Server Compact) Getting started; small installations Installed automatically, nothing to configure. The default.
MySQL / MariaDB Most production use Client is bundled on Windows — works with MySQL 8 and MariaDB out of the box. Since 6.3.4 every table is utf8mb4, so the database server must be MySQL 5.7.9 or MariaDB 10.2.2 or later
Microsoft SQL Server Windows shops that already run it Including Express edition
PostgreSQL Larger installations libpq bundled

On Windows, if in doubt, take the built-in database. You can migrate later, and for anything up to a few dozen mailboxes it is entirely adequate.

On Linux the choice is narrower — two of the four. PostgreSQL is reached through libpq, which the package depends on. MySQL/MariaDB is reached through libmariadb.so.3, loaded at run time, so the client package has to be installed separately: libmariadb3 on Debian and Ubuntu, mariadb-connector-c on Fedora and RHEL, mariadb-libs on Arch. Microsoft SQL Server and the built-in SQL Server Compact database are refused by name, with an explanation in the error log, because both are reached through Windows-only interfaces. See chapter 35.

More: Choosing a database, creating it, encrypting the link and leaving the built-in one on the forum.

3.6 Decide your host name

Pick the name your mail server will be known by — conventionally mail.yourcompany.com. Write it down. It is used in your MX record, your PTR record, your TLS certificate and your server’s SMTP greeting, and they should all agree.

Pre-flight checklist

  • Windows machine (x64, build 14393 or newer) or Linux machine (x86-64 or AArch64), always on
  • Domain name with DNS access
  • Static public IP
  • Port 25 open inbound and outbound
  • PTR record requested from your provider
  • Host name chosen (mail.yourcompany.com)
  • Administrator access to the machine — an administrator account on Windows, root or sudo on Linux
  • Database chosen (§3.5): on Linux, PostgreSQL or MySQL/MariaDB; a MySQL server must be 5.7.9 or later, a MariaDB server 10.2.2 or later
  • On Windows, the service account planned for: a new installation runs as NT SERVICE\hMailServer, so any folder it must write other than the program folder’s own Data, Logs, Temp and Database (a backup folder above all), or SQL Server on the same machine reached with Windows authentication, is arranged first (§3.1)
  • Somewhere to keep the exported stored-secrets key (since 6.3.4), with the database backups but stored apart from them: without it, a database restored on another machine opens none of the second factors and secret settings the key seals

4. Installing hMailServer

4.1 Download

Get the Windows installer for the current release, hMailServer-6.3.8-x64.exe, at updates.progressiverobot.com/download/hMailServer-x64.exe. That link always leads to the installer of the newest stable release, never a pre-release’s, and updates.progressiverobot.com/download/ leads to that release’s page, with its notes and every file. For Windows on ARM, hMailServer-6.3.8-arm64.exe is at updates.progressiverobot.com/download/hMailServer-arm64.exe: a preview, cross-compiled on the project’s x64 runner, Authenticode-signed like the x64 one, and never run on ARM64 hardware. SQL Server Compact has no ARM64 build, so a new installation there is set up against SQL Server or PostgreSQL, and a silent one is given its database on the command line. If you install it on an ARM64 machine, the project would like to hear how it went, in an issue. Every release is on the project’s GitLab releases page.

Each release also carries SBOM files (hmailserver.spdx.json, hmailserver.cyclonedx.json) — the source tree’s inventory of its components, made by Syft with the native libraries it cannot see added, for anyone who needs to audit their supply chain. From 6.3.4 every installer and package has one of its own, beside it and named after it (hMailServer-6.3.8-x64.exe.cdx.json, CycloneDX 1.6), whose subject is that file by its SHA-256 and SHA-512: the installer’s lists OpenSSL, Boost, libpq and zlib at the versions the server is built with and every third-party file the installer packs. You do not need them to install.

The Linux packages are on the same release. For 6.3.8 that is x86-64 and AArch64: the generic hmailserver_6.3.8_amd64.deb, hmailserver_6.3.8_arm64.deb, hmailserver-6.3.8-1.x86_64.rpm and hmailserver-6.3.8-1.aarch64.rpm, the names an installed server’s update check asks for; 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 (§35.2); the two AppImages, with the source of the libraries they carry; the source tarball, hmailserver-6.3.8.tar.gz; and the Helm chart. Each compiled file and the tarball has its SBOM, hmailserver-linux-6.3.8-SHA256SUMS.txt gives the SHA-256 of every Linux file, every file of the release has its Sigstore bundle beside it, named after it with .cosign.bundle, and hmailserver-6.3.8.intoto.sigstore.json, the release’s SLSA build provenance, names every file by its SHA-256, and hmailserver-6.3.8.intoto.jsonl is the same provenance as one line of JSON. The AArch64 files are cross-compiled on the project’s x86-64 runner and tested under emulation; no AArch64 machine has run them. From 6.3.6 the release’s pipeline also makes a container image, registry.gitlab.com/hmailserver/hmailserver, for linux/amd64 and linux/arm64, built from the release’s .debs and signed with cosign as that pipeline; it is pushed by a job started by hand once the release is published, and until then the registry holds no image of that release. Installing and first-run setup on Linux are chapter 35; the rest of this chapter is the Windows installer.

The Windows installer is Authenticode-signed from 6.3.1. It is signed with Azure Artifact Signing, against a certificate profile issued to Progressive Robot Ltd after Microsoft’s identity validation, and countersigned to RFC 3161. The elevation prompt names the publisher instead of reading Unknown publisher, and an enterprise policy that refuses unsigned binaries no longer refuses this one.

SmartScreen still warns. Microsoft puts a signed installer in the same row as an unsigned one, flagged unrecognised until reputation accumulates, and reputation attaches to a file that does not change — which a new 80 MB installer every few weeks never is. An EV certificate would not help; Microsoft removed EV’s SmartScreen bypass in 2024. Expect the same Windows protected your PC screen, now carrying the real publisher name. Releases before 6.3.1 are unsigned and always will be: a published release is immutable, so 6.3.0 could not be signed after the fact.

Only the Windows installer carries Authenticode — there is no such thing for a .deb, an .rpm or an AppImage. From 6.3.5 every file of a release is signed with Sigstore by the project’s GitLab CI pipeline for the release’s tag, and checked with the server’s own verifier before the release is published: each file has <file>.cosign.bundle beside it. 6.3.8’s name the identity https://gitlab.com/hmailserver/hmailserver//.gitlab-ci.yml@refs/tags/v6.3.8, the project’s path in the group hmailserver, with the project’s id, 86730559, in the certificate, and the issuer https://gitlab.com. With cosign, and the file and its bundle beside you, this must succeed:

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

On Linux, a server from 6.3.4 on makes the same check its live update makes: hmailserver --verify-update <file>, with the bundle beside the file; read the identity it prints, which must end in @refs/tags/v6.3.8. sha256sum -c hmailserver-linux-6.3.8-SHA256SUMS.txt --ignore-missing checks the Linux files, and Get-AuthenticodeSignature .\hMailServer-6.3.8-x64.exe, or the -arm64.exe, should answer Valid, signed by CN=Progressive Robot Ltd. The release’s SLSA provenance is checked with cosign verify-blob-attestation against the same identity (the security policy’s Build provenance). A server from 6.3.4 on trusts the pipeline’s Sigstore identity above, with the project’s id, so its live update can install 6.3.8 (§18.2); a server from 6.2.28 to 6.3.3 trusts only the GitHub release workflow and is upgraded by hand. 6.3.5 was signed on 29 September 2026, before the project moved to gitlab.com/hmailserver/hmailserver, where the old addresses redirect, so its bundles name the project’s former path, https://gitlab.com/Progressiverobot/hmailserver//.gitlab-ci.yml@refs/tags/v6.3.5, with the same issuer; a server from 6.3.4 on trusts both paths. 6.3.4 itself carries no Sigstore bundle: it was built and Authenticode-signed by hand, after GitHub suspended the project’s account on 18 September 2026 and before the GitLab pipeline existed, so no server’s live update installs 6.3.4, and it is installed by hand: on Windows by running the installer (chapter 18), on Linux by installing the package (chapter 35).

6.3.4’s own check is hmailserver-6.3.4-SHA256SUMS.txt, which names every file of the release but itself and its signature with its SHA-256, and hmailserver-6.3.4-SHA256SUMS.txt.sig, an SSH signature of that list made with the key that signs the release tags, the one key in the repository’s .gitlab/allowed_signers. With OpenSSH 8.1 or later, in a clone of the repository and with the downloads beside you:

ssh-keygen -Y verify -f .gitlab/allowed_signers -I [email protected] -n file -s hmailserver-6.3.4-SHA256SUMS.txt.sig < hmailserver-6.3.4-SHA256SUMS.txt
sha256sum -c hmailserver-6.3.4-SHA256SUMS.txt

The first must say Good "file" signature for [email protected], and the second OK for every file (--ignore-missing when you downloaded only some). The installer’s Authenticode signature is checked as before:

Get-AuthenticodeSignature .\hMailServer-6.3.4-x64.exe   # Status Valid, signed by CN=Progressive Robot Ltd

The project’s security policy, Verifying a release, has these commands, and under Software bills of materials how to check that an SBOM describes your file.

For 6.3.3 and the releases before it back to 6.2.19, Sigstore is the check that means something for every asset on the release: each carries a .cosign.bundle beside it, signed by the GitHub release workflow of the time.

cosign verify-blob --bundle hMailServer-6.3.3-x64.exe.cosign.bundle \
  --certificate-identity-regexp '^https://github\.com/Progressiverobot/hmailserver/' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  hMailServer-6.3.3-x64.exe

Two SHA-256 values, deliberately. From 6.3.1 the release notes give the Windows installer’s hash twice — as built, and as published. The signing job replaces the installer asset with the signed one before cosign runs, so the bytes on the release are not the bytes the build produced. The published value is the one to check a download against, and it is those same published bytes the cosign bundle above is signed over. 6.3.1 is the first release for which the two differ. 6.3.4, signed by hand with no signing job and no cosign bundle, is checked against hmailserver-6.3.4-SHA256SUMS.txt instead (above). From 6.3.5 the notes give the installer’s published SHA-256 only, the value its bundle is signed over.

4.2 Run the installer

Right-click the installer and choose Run as administrator.

Work through the wizard — there is no welcome page; it opens straight on the licence:

1. Licence agreement. hMailServer is AGPL-3.0-or-later. In short: you may use it freely, including commercially; if you modify it and offer it to others over a network, you must publish your changes.

The licence agreement — the installer's first page
The licence agreement — the installer’s first page

2. Destination folder. The default C:\Program Files\hMailServer is fine.

Select Destination Location
Select Destination Location

3. Components. This page matters, so read it:

Select Components
Select Components
Component Take it? What it does
Server Yes The mail server itself. Everything else is optional.
Control Panel Yes The administration application. Without it you have no GUI.
Remote administration support Only if scripting from another machine Registers the hMailServer COM type library on a machine that is not the server, so that scripts and other COM clients there can administer a remote hMailServer. The Control Panel does not need it — it connects to remote servers by itself.

Note for upgraders: “Administrative tools” in older versions bundled the classic Administrator and a PHP web interface. Both are gone (see chapter 25). The component now does only what it always really did — register the COM type library for scripts.

4. Database type. Choose Use the built-in database engine (SQL Server Compact) unless you decided otherwise in §3.5. An upgrade never shows this page: the database type already in hMailServer.INI is kept.

Select database server type
Select database server type

5. Start Menu folder. Where the shortcuts go. The default is fine.

Select Start Menu Folder
Select Start Menu Folder

6. Administrator password. hMailServer asks you to set a password for the administration interface. This is not an email password — it is the master password for the whole server: at least five characters, entered twice. Since 6.3.4 the installer has the server store it as a PBKDF2 hash (hMailServer.exe --set-admin-password), where it used to write an unsalted MD5 digest, and hands it to the database tools in its environment (HM_ADMIN_PASSWORD), never on their command lines; before 6.3.4 a password with a space, a double quote or backslashes before one failed the database step. An unattended installation takes the password with /adminpassword= or, from 6.3.4, in HM_ADMIN_PASSWORD, set in an elevated prompt; a password with a double quote must go in the variable, because Inno Setup drops every double quote from its command line, and anybody listing processes can read /adminpassword= while setup runs; setup refuses to start when either gives one shorter than five characters. The page is skipped when the server already has a password, as on an upgrade, which keeps it.

Setting the administration password
Setting the administration password

Use a long, unique password and store it in a password manager. Anyone with it owns every mailbox on the server.

Do not leave it unset. An unattended installation given neither /adminpassword= nor HM_ADMIN_PASSWORD sets no password, and while there is none every COM session starts authenticated as a server administrator and the REST API refuses to start. Set one before the server can be reached from anywhere but this machine: from an elevated prompt in the Bin folder, hMailServer.exe --set-admin-password, with the password on standard input or in HM_ADMIN_PASSWORD, never as an argument, then restart the service (Restart-Service hMailServer): a running service keeps the password it loaded, here none, until it is restarted or reinitialised.

7. Server profile (new in 6.3.4; shown on a new installation only, one with no database yet). What kind of server this is: none, a small office server, one behind a filtering gateway, an outbound relay or a high-volume sender. A profile sets about thirty settings, each with its reason, and keeps what it replaced so that it can be put back later from the Control Panel’s Set up this server → Setting profiles page or the Control Deck. None keeps the settings this version ships with. A silent installation takes /profile=office, gateway, relay, sender or none.

8. Service account (new in 6.3.4). On a new installation the default, and the recommendation, is NT SERVICE\hMailServer, a virtual account of the service’s own that reaches only the folders setup grants it; the other choice is LocalSystem, as earlier versions ran it (§17.3). An upgrade keeps the account the service has unless you choose otherwise here, and a silent installation takes /serviceaccount=virtual, localsystem or keep. If the database is SQL Server on this machine reached with Windows authentication, give NT SERVICE\hMailServer a login there before the service moves to it (CREATE LOGIN [NT SERVICE\hMailServer] FROM WINDOWS, and a user in the hMailServer database with db_owner); the page says so.

9. Ready to install. A summary of everything you chose. Click Install. The installer copies files, registers the hMailServer Windows service, creates the database and starts the service. If the .NET 10 Desktop Runtime is missing it is installed silently — the Control Panel and the database tools both run on it. Since 6.3.4 a database the installer creates is made secure before anyone configures it — a self-signed certificate for the machine, STARTTLS offered on 25, 587, 110 and 143, and TLS required for authentication from the Internet IP range — and the installer makes the stored-secrets key that seals the second factors and the [Settings] secrets. That key is in this machine’s hMailServer.INI alone, sealed under its DPAPI, and never in the database, so export it when setup asks, from an elevated prompt in the Bin folder (.\hMailServer.exe --export-stored-secrets-key D:\keys\hmailserver-stored-secrets.key), and keep the file with the database backups, stored apart from them: a database-level backup (a dump, a SQL Server backup, a copy of the .sdf) restored without it opens no second factor and no [Settings] secret. An upgrade makes no key and is not given these secure defaults.

Ready to Install — the pre-install summary
Ready to Install — the pre-install summary

More: Installing on Windows: wizard pages, service account, profile and the first hour and Unattended installs, winget and Chocolatey, and configuration from the environment on the forum.

4.3 Confirm it is running

Open Services (services.msc) and find hMailServer. It should say Running, startup type Automatic, logging on as NT SERVICE\hMailServer on a new 6.3.4 installation that kept the installer’s default (as Local System if setup could not move it, which it then says), and on an upgrade as the account it had, Local System unless that was changed (the screenshot, from an earlier version, shows Local System).

The Services console showing hMailServer running
The Services console showing hMailServer running

Or from PowerShell:

Get-Service hMailServer
Status   Name          DisplayName
------   ----          -----------
Running  hMailServer   hMailServer

On Linux the service is a systemd unit of the same name:

systemctl status hmailserver
journalctl -u hmailserver -f
hmailserver --check-config

--check-config reads the configuration, reports what it says and exits without opening a listener or touching the database: it prints the configuration path, the directories and the database type it made of the file, and Database type: 0 means the Type key did not take. Run it as the service’s user (sudo -u hmailserver hmailserver --check-config). It does not check [Settings]: since 6.3.4 a [Settings] key edited into the file after the first start is reported in the error log as HM5804 and not used, and is changed over the REST API or with sudo hmailserver --set-setting instead. Chapter 35 has the whole command set.

Since 6.3.4 the server also judges its own first hour, in twelve items, thirteen from 6.3.6: a profile, a domain, an account, TLS on the submission port, passwords only over TLS, SPF, DKIM, DMARC, a backup destination, a second factor for the administrator, the stored-secrets key, the service account and, from 6.3.6, no password on port 25 (§18.5), each done, action needed, cannot tell or not needed, with a sentence saying what it found. It heads the Control Panel’s Set up this server → Setup checklist page and is the Control Deck’s Checklist view.

4.4 Open the firewall

The installer does not open firewall ports for you. Until you do, nothing can reach your server. Run this in an elevated PowerShell:

New-NetFirewallRule -DisplayName "hMailServer SMTP"        -Direction Inbound -Protocol TCP -LocalPort 25  -Action Allow
New-NetFirewallRule -DisplayName "hMailServer Submission"  -Direction Inbound -Protocol TCP -LocalPort 587 -Action Allow
New-NetFirewallRule -DisplayName "hMailServer IMAP"        -Direction Inbound -Protocol TCP -LocalPort 143 -Action Allow
New-NetFirewallRule -DisplayName "hMailServer IMAPS"       -Direction Inbound -Protocol TCP -LocalPort 993 -Action Allow
New-NetFirewallRule -DisplayName "hMailServer POP3"        -Direction Inbound -Protocol TCP -LocalPort 110 -Action Allow
New-NetFirewallRule -DisplayName "hMailServer POP3S"       -Direction Inbound -Protocol TCP -LocalPort 995 -Action Allow

On Linux, with firewalld:

firewall-cmd --permanent --add-port={25,110,143,465,587,993,995}/tcp
firewall-cmd --reload

or with ufw:

ufw allow 25,110,143,465,587,993,995/tcp

The systemd unit grants CAP_NET_BIND_SERVICE ambiently, so the service can bind 25, 110, 143, 465, 587, 993 and 995 without ever running as root.

Your router or cloud firewall needs the same ports forwarded to this machine.

The TLS ports in these lists, 993 and 995 (and 465 in the Linux ones), are there because you will want them, but a new installation listens only on 25, 587, 110 and 143, the four listeners the database is created with (since 6.3.4 with STARTTLS offered on a self-signed certificate). Add the implicit-TLS ports once you have a certificate: on Windows on Connections & protocols → TCP/IP ports, on Linux over REST (/api/v1/ports) or in the Control Deck (chapter 9).

More: Every port hMailServer listens on or connects to, and the firewall rules to match on the forum.


5. The Control Panel: your first look

The hMailServer Control Panel (hMailCP.exe) is the Windows way to administer the server. It is a .NET 10 desktop application that talks to the server over the COM API — which means it can administer a server on another machine just as easily as the local one.

It is one route of three. The Control Deck — the administration page the REST listener serves at /, in any browser — now covers most of the same ground (chapter 36), and the REST API itself (chapter 24) is there for scripts. On Linux there is no Control Panel and no COM at all, so those two are administration, beside the server’s own command-line options and, since 6.3.4, hmctl, a command-line client over the same API (chapter 35).

Launch it from the Start menu.

5.1 Connecting

The connection screen
The connection screen
Field What to enter
Server localhost for the machine you are sitting at, or the remote server’s name/IP
User name Administrator
Password The administration password you set during installation

If you have enabled two-factor authentication (§17.4), you are asked for a six-digit code next.

To change the administrator password later, use Sign-in & access → Administrative access, Password tab. On a server whose service runs as NT SERVICE\hMailServer, as a new 6.3.4 installation’s does, the tab’s Administrator password card refuses the change with a sentence, because the file the password is kept in is read-only to the service. Set it instead from an elevated prompt beside hMailServer.exe: put the new password in the HM_ADMIN_PASSWORD environment variable (never on the command line), run hMailServer.exe --set-admin-password, and restart the service, which goes on with the old password until then (§17.3).

5.2 The Welcome page

The Welcome page
The Welcome page, captured from 6.2.10: the older sidebar, and only the six area tiles that today sit below the twelve task rows

The Welcome page opens with twelve starting points under What do you want to do?, written as outcomes (“Mail is stuck or slow”, “Add a domain or a mailbox”, “Stop the spam”, “Renew or install a certificate” and eight more), each one click from the page that does it. Below them, Or browse by area has six tiles for the areas you will visit most. It is the fastest route to “add a domain” when you are starting out, but it is not where you land: connecting opens the Dashboard, and Welcome is a click away in the sidebar.

Since 6.3.4 the Welcome page also offers a sample domain: one button makes a throwaway example.test with three mailboxes, a list of the three, a rule and eight delivered messages, so that every page has something in it while you learn, and then turns into a Remove button. The three mailboxes share one password, made when the sample is and shown once, so write it down. They are ordinary mailboxes that can sign in and relay, so remove the sample before the server takes real mail. A button in the page’s header starts a seven-stop tour of the pages a new server needs (Domains, TCP/IP ports, SSL certificates, then the Dashboard); it points and explains, and never acts.

5.3 Finding your way around

The left sidebar is the navigation tree: 66 pages in nine groups in 6.3.5, one more than in 6.3.4 (starred below), never more than two levels deep and grouped by what you are trying to do rather than by where the server keeps things. It is organised in the order you will need it:

Welcome                         ← what do you want to do?
Dashboard                       ← live counters and charts
Set up this server              ← Setup checklist, Setting profiles, DNS records
Accounts & domains              ← Domains, Groups, Public folders, Administrator roles
Mail flow & delivery            ← Delivery of e-mail, Routes, Remote domains,
  Incoming relays, Rules, Server messages, Server sendout
Connections & protocols         ← TCP/IP ports, Protocols,
  Web services & autoconfiguration, Webmail*, DNS resolver
Certificates & encryption       ← SSL certificates, Certificates (ACME),
  Transport encryption overview, SSL/TLS, Transport security
Spam & virus filtering          ← Spam filtering overview, Quarantine,
  Anti-spam settings, SURBL servers, DNS blacklists, White list,
  Blocked senders, Allow and block list, Greylisting white list,
  Greylisting bypass list, Domain white and black list,
  Virus scanning overview, Anti-virus settings, Blocked attachments
Sign-in & access                ← Authentication, Administrative access, IP ranges,
  Auto-ban, Directory authentication (LDAP), Directory synchronisation
Monitoring & troubleshooting    ← is mail flowing, and if not, where has it stopped?
  Server status, Delivery queue, Live logs, Logging, Diagnosing stalled mail,
  Message trace, Diagnostics, MX query, Suppression list, Audit trail,
  Journaling and dual delivery, Discovery, API & monitoring, REST API keys
Maintenance                     ← Backup & restore, Performance, Advanced, Event scripts,
  Server limits & expert settings
About

* new in 6.3.5. The Setup checklist was External setup, Certificates & encryption
  was TLS & certificates, and Sign-in & access was Access & abuse protection;
  each old name is still an alias the Ctrl+K palette finds.

Once you are connected, press Ctrl+K at any time for the command palette — since 6.2.14 it searches settings as well as pages, indexed by label and INI key, so typing log level or LogDeleteDays takes you to the setting itself. Start typing a page name and jump straight to it, or say what you want to do (“stop spam”, “block an IP”). It also knows the old navigation paths, so a path from an old forum post or your own notes still leads somewhere: Settings > Security > Auto-ban & SSL/TLS opens Auto-ban.

The window remembers its size, position and maximised state between sessions, and both light and dark themes are fully supported (it follows your Windows setting until you choose a theme yourself, and follows High Contrast as well). Since 6.3.4 a command bar runs across the top of the window: a search field that opens the same Ctrl+K palette, a status pill saying whether the connection is up, the theme toggle, and the administrator’s menu, which holds who is signed in where, the server version, the language, the theme and About. Below 1,000 pixels of window width the sidebar folds to a rail of one icon per group.

Also since 6.3.4, every page has a help button, and F1 does the same: a pane opens beside the page with what the operator documentation says about it, built into the Control Panel, so it works on a server with no route to the Internet. And a save applies: under every field kept in the settings store a line says when a new value takes effect — Applies on save (211 of the keys), After reinitialize (28: a listener’s port or bind address, a TLS context, a work queue’s size) or After a restart (2). A bar at the foot of the window counts the saved settings still waiting, with Apply now: a reinitialise over COM, which stops and starts every one of the server’s services in place, but not the Windows service itself, and needs no elevation prompt. It asks first, and says so when the server is draining, because a reinitialise ends a drain. Connected to a 6.3.3 server, the Control Panel marks every such field After a restart and never says a save took effect.

Since 6.2.28 it speaks seventeen languages beside English — Czech, Danish, German, Spanish, Finnish, French, Italian, Japanese, Norwegian bokmål, Dutch, Polish, Brazilian Portuguese, Russian, Swedish, Turkish, Ukrainian and Simplified Chinese. Every page, dialog, message, hint and verdict sentence is translated: 4,196 texts in 6.3.4, not a subset. The language is chosen on the sign-in card or, since 6.3.4, in the command bar’s administrator menu; the Control Panel restarts in it, and the choice is remembered per Windows user.

5.4 The Dashboard

The Dashboard
The Dashboard, captured from 6.2.10: today it adds a Live / 24 hours / 7 days / 30 days range selector above the charts and a needs attention card below them

Live view of the server, refreshed every two seconds: uptime, messages processed, messages in the queue, spam blocked and viruses removed, with charts of throughput and of SMTP, POP3 and IMAP sessions over the last three minutes, 24 hours, 7 days or 30 days (the three longer ranges read the history the server keeps, MetricsHistoryDays days of it, 7 by default). Below them a needs attention card runs the same checks as the Setup checklist page — since 6.3.4 headed by the first-hour checklist, and since 6.3.5 numbered in the same order — and links each outstanding item to the page that owns it. This is the page to leave open on a second monitor; the state of the Windows service itself is on Monitoring & troubleshooting → Server status.

More: The Control Panel on Windows: every page, what it writes, and what applies when on the forum.


6. Your first domain and mailbox

This is the chapter where you get working email.

Just learning? Make a sample domain first. Since 6.3.4 one button, on the Control Panel’s Welcome page or the Control Deck’s Checklist view, or one command (hmctl sample-domain create, or hmailserver --sample-domain create on Linux) makes a throwaway example.test with three mailboxes, a distribution list, one rule and eight messages, so every page has something in it. The three mailboxes share one password, which the server makes when it creates the sample and shows once, in its answer; it is stored nowhere, so write it down. The same button, now Remove, or hmctl sample-domain remove takes all of it away and says what it removed. Remove it before the server takes real mail: its mailboxes are ordinary accounts that can sign in and relay like any other. Then come back here for your own domain.

6.1 Add your domain

Go to Accounts & domains → Domains, type the domain name into the box at the bottom of the domain list and click Add domain. Then select the domain and click Properties to open the domain dialog.

The Domains page
The Domains page

In the domain dialog:

The domain dialog
The domain dialog
Field Value
Domain name yourcompany.com — the part after the @, with no www and no mail.
Domain enabled Ticked
Maximum domain size (MB) (Limits tab) 0 for unlimited, or a size in MB to cap the whole domain
Postmaster address Leave it empty. This field is the domain’s catch-all, not the postmaster mailbox: mail to any address in the domain that matches no account, alias, distribution list or route is redirected to it, so every typo and every dictionary-attack recipient is accepted and stored instead of being refused with 550 Unknown user. The [email protected] mailbox that RFC 5321 requires is an account, which you create in 6.2.

Click Save.

A common mistake: the domain is yourcompany.com, not mail.yourcompany.com. mail.yourcompany.com is the name of the machine; yourcompany.com is the domain whose email you are hosting. Addresses look like [email protected].

6.2 Add a mailbox

Select your domain, type the address and a password into the boxes under its account list and click Create account. Then click Edit (or double-click the account) to open the account dialog, where the rest of these fields are.

Field Value
Address [email protected]
Password Click Generate strong password in the account dialog (Generate beside the password box when you create the account) — it uses a cryptographic RNG and copies to your clipboard
Quota (MB) Mailbox quota in MB. 0 = unlimited
Account enabled Ticked
Administration level Normal user for a normal mailbox. Domain administrator manages its own domain; only give Server administrator to yourself
The account dialog
The account dialog

The account dialog also has tabs for Auto-reply (out-of-office), Forwarding, External (fetch mail from elsewhere by POP3 or IMAP), Rules, Sieve and Windows directory (Active Directory linking; the tab was called Directory in 6.3.3). In 6.3.4 it has fifteen tabs in all.

The Auto-reply tab — out-of-office messages
The Auto-reply tab — out-of-office messages
The Forwarding tab
The Forwarding tab

Create at least these two:

6.3 Test it locally, before touching DNS

Prove the server works before you involve the internet. From PowerShell on the server:

Send-MailMessage -SmtpServer localhost -Port 25 `
   -From "[email protected]" -To "[email protected]" `
   -Subject "First test" -Body "If you can read this, hMailServer is working."

Send-MailMessage is deprecated but perfectly good for a one-off test.

Then check Monitoring & troubleshooting → Delivery queue (should be empty — the message was delivered instantly) and Monitoring & troubleshooting → Live logs.

The live log
The live log

Now connect a mail client (chapter 7) and read the message. If it is there, your server works. Everything from here is about the outside world being able to reach it.

More: Your first domain and mailbox, and the DNS records your domain needs on the forum.

6.4 Creating accounts from Active Directory

New in 6.2.19. An account has always been able to authenticate against Active Directory (the account dialog’s Windows directory tab); now the directory can also be the source of accounts. Directory sync reads an LDAP directory and creates or updates the mailboxes it says should exist — from its Control Panel page, Sign-in & access → Directory synchronisation, on an optional unattended schedule, or from a script (Settings.PreviewDirectorySync / ApplyDirectorySync).

It is built around a preview. Planning and applying are one piece of code — the planner decides and writes nothing, the applier writes and decides nothing — so a preview cannot describe an action the apply would not take, and the Apply button stays disabled until a preview of the same options has been run.

What it refuses to do is as important as what it does:

  • A domain takes part only if its Active Directory domain name is set — provisioning is opt-in per domain, so a search base pointed one level too high cannot provision into unrelated hosted domains.
  • Nothing is ever deleted. The most it will do is clear Active, and only when asked — never on a truncated or empty enumeration, where an account that still exists is indistinguishable from one that was removed.
  • An account that is not directory-linked, or is linked to a different AD domain, is reported rather than taken over.

Since 6.3.4 an organisation whose people are kept in Microsoft Entra ID or Okta can let the identity provider do this instead, over SCIM 2.0 at /scim/v2 on the REST listener: it makes, renames, suspends and removes accounts, with their aliases and distribution lists, as people join, change and leave. It needs an API key whose role holds the provision permission, and unlike directory sync it does delete: a DELETE removes the account and its mailbox.

More: Active Directory and LDAP sign-in, directory synchronisation and OAuth2 bearer tokens and SCIM provisioning from Entra ID and Okta on the forum.


7. Connecting a mail client

7.1 The settings your users need

Setting Value
Incoming server (IMAP) mail.yourcompany.com, port 993, SSL/TLS
Incoming server (POP3) mail.yourcompany.com, port 995, SSL/TLS
Outgoing server (SMTP) mail.yourcompany.com, port 587, STARTTLS
User name The full email address — [email protected], not you
Password The account’s password, or an app password once the account has a second factor (TOTP) enrolled: from then on its own password is refused in every mail client, and app passwords are its only mail credential
Authentication Normal password (or SCRAM-SHA-256, see §17.2; IMAP offers it only once Allow SASL PLAIN authentication is on, on the IMAP tab of Connections & protocols → Protocols)

Two mistakes cover most failed client setups: entering the user name without the domain part, and using port 25 for sending. Users send on 587.

Ports 993, 995 and 465 do not exist until you create them: a new database has only SMTP 25 and 587, POP3 110 and IMAP 143. Create the TLS ports on Connections & protocols → TCP/IP ports (on Linux, in the Control Deck or over the REST API) and give each a certificate (chapter 9) before you hand these settings out; a port added or changed is listened on from the next reinitialise (POST /api/v1/server/reinitialize, or systemctl reload on Linux) or service start, not from the save. Since 6.3.4 a new installation, and not an upgraded server, starts with a self-signed certificate, STARTTLS offered on those four ports and TLS required for authentication from the Internet IP range: from any other machine a client set to no encryption is refused its password, so set it to STARTTLS, or to SSL/TLS on 993 and 995 once they exist. On a server whose database was made before 6.3.4 the four ports stay as they were, seeded with no encryption, so also set 587 to STARTTLS (required) on the same page and give it a certificate before telling users to send on it. Mail clients warn about the self-signed certificate until you replace it or ACME fetches one (§9.1).

IMAP or POP3?

  • IMAP keeps mail on the server; every device sees the same mailbox, folders and read/unread state. Use IMAP.
  • POP3 downloads mail to one device and (usually) deletes it from the server. Only use it if you have a specific reason.

7.2 Automatic client configuration

hMailServer can configure clients for your users automatically, so they type only their address and password.

Enable it on Connections & protocols → Web services & autoconfiguration in the Control Panel, or set the keys below over REST (PUT /api/v1/settings/ini/{name}) or, on Linux, which has no Control Panel, with sudo hmailserver --set-setting NAME=VALUE. Since 6.3.4 they are kept in the database, not in hMailServer.INI: a line edited in the file is ignored in favour of the stored value, named in the error log as HM5804 and, where the service may write the file, put back.

WebServicesHttpPort=80
WebServicesHttpsPort=443
AutoconfigEnabled=1

AutoconfigEnabled is already 1 by default and applies when saved: giving the web services listener a port is what turns autoconfiguration on. The two ports wait for a reinitialise (Apply now in the Control Panel’s pending bar, or POST /api/v1/server/reinitialize), and the HTTPS listener stays off until it has a certificate, from ACME or in WebServicesCertificateFile and WebServicesPrivateKeyFile. The answer names AutoconfigClientHost, or the server’s host name when that is empty; with neither set, the endpoints answer 404.

Then add DNS records pointing autoconfig.yourcompany.com and autodiscover.yourcompany.com at your server. Thunderbird and Outlook will find their own settings.

More: Connecting a mail client: IMAP, POP3, SMTP submission, app passwords and the ports on the forum.


8. Making the internet trust you: DNS

This is the most important chapter in this document. A perfectly configured mail server with wrong DNS delivers nothing but spam-foldered mail.

Add these records at your DNS provider. Replace yourcompany.com and 203.0.113.10 with your own.

8.1 A record — where the server is

mail.yourcompany.com.    IN  A       203.0.113.10

8.2 MX record — where mail goes

yourcompany.com.         IN  MX  10  mail.yourcompany.com.

The 10 is a priority; with one server the number is arbitrary. An MX record must point at a host name, never at an IP address.

8.3 PTR record — proving the IP is yours

10.113.0.203.in-addr.arpa.  IN  PTR  mail.yourcompany.com.

You cannot set this yourself — ask your ISP or hosting provider. Without it, Google and Microsoft will reject or spam-folder your mail.

8.4 SPF — who may send as you

yourcompany.com.         IN  TXT     "v=spf1 mx a:mail.yourcompany.com ~all"

This says: “the servers in my MX records, and this host, may send my mail; treat everything else as suspicious.”

  • -all — hard fail, reject anything else. Correct once you are sure.
  • ~all — soft fail, mark as suspicious. Start here: it is what the Control Panel suggests, and it is safer while you are still testing.

If you also send through another service (a newsletter platform, say), include it:

"v=spf1 mx include:sendgrid.net -all"

8.5 DKIM — signing your mail

DKIM adds a cryptographic signature proving the message came from you unaltered.

hMailServer generates the key for you. In the Control Panel, open your domain’s DKIM tab and click Generate key pair. It creates an RSA-2048 key, saves the private key, fills in the path, and shows you the exact DNS record to publish — with a Copy DNS value button.

The domain's DKIM tab
The domain’s DKIM tab

You will get something like:

dkim._domainkey.yourcompany.com.  IN  TXT  "v=DKIM1; k=rsa; p=MIIBIjANBgkq..."
The DKIM tab after generating a key — the DNS record ready to copy
The DKIM tab after generating a key — the DNS record ready to copy

Publish that TXT record. Signing is off until you switch it on: once the record resolves, tick Enable DKIM signing on the same tab and save the domain, and hMailServer signs every outgoing message from that domain. Do not tick it before the record is live: every message would then fail DKIM at the receiver.

hMailServer also supports Ed25519 DKIM (RFC 8463) — shorter keys, modern crypto — alongside RSA.

Since 6.3.4 the DKIM tab can also rotate the key for you (Key rotation; also on the Control Deck and over the REST API). It is off on every domain until you switch it on. When the key is older than the interval (180 days by default), or when you click Rotate now, it generates a new key under a dated selector such as s20260915 and shows its TXT record; checks the record from the server’s own resolver every fifteen minutes; moves signing to the new key only once the record has carried exactly that key for the propagation period (72 hours by default); keeps the old selector for a retention period (7 days by default) for mail still on its way; and then lists the old record as one you may remove. It never edits DNS and does not switch signing on: publishing and removing the records stays yours.

8.6 DMARC — the policy

_dmarc.yourcompany.com.  IN  TXT  "v=DMARC1; p=none; rua=mailto:[email protected]"

Start with p=none: “don’t change how you treat my mail, but send me reports.” Read the reports for a few weeks, confirm all your legitimate mail passes SPF and DKIM, then tighten:

p=quarantine     ← failures go to spam
p=reject         ← failures are refused outright

Do not start at p=reject. If something you had forgotten about sends mail as your domain, you will silently lose it.

Since 6.3.4 the server can read the reports for you. Name the rua= address as the domain’s report mailbox (the domain’s Reports received tab, or report_mailbox over REST), and the DMARC aggregate and TLS-RPT reports that arrive there, gzip, zip or plain, are read into the database and charted on the Control Deck’s DMARC page: who sends as the domain, from where, and with what result. The mailbox must be in a domain this server hosts. Nothing is read until you name one, and nothing read there acts on any mail.

More: DKIM rotation, DMARC and TLS reports on the forum.

8.7 MTA-STS — enforcing encryption inbound

MTA-STS tells other servers that mail to you must be encrypted, preventing an attacker from stripping TLS. hMailServer can host the policy itself. Since 6.3.4 these keys are kept in the database, and the [Settings] section of hMailServer.ini is only its copy: a line edited there is ignored in favour of the stored value, reported as HM5804 and, where the service may write the file, put back (chapter 21). Set them on Connections & protocols → Web services & autoconfiguration (the Listener and MTA-STS policy hosting cards), over REST (PUT /api/v1/settings/ini/{name}) or COM, or on Linux with sudo hmailserver --set-setting or hmctl ini set; the Control Deck has no editor for them:

WebServicesHttpsPort=443
MtaStsHostingEnabled=1
MtaStsPolicyMode=testing

The HTTPS listener starts only once it has a certificate, and it must be valid for mta-sts.yourcompany.com: its own (WebServicesCertificateFile and WebServicesPrivateKeyFile), or an ACME one with that name in AcmeDomains. A changed port or certificate file takes effect at a reinitialise (Apply now); MtaStsHostingEnabled and MtaStsPolicyMode apply when saved. Start with MtaStsPolicyMode=testing: it gives you TLS-RPT reports without losing mail while you find out whether every sender can reach you over TLS (publish _smtp._tls.yourcompany.com. IN TXT "v=TLSRPTv1; rua=mailto:[email protected]" to receive them at the report mailbox of §8.6). Move to enforce once they can.

Point mta-sts.yourcompany.com at your server and publish:

_mta-sts.yourcompany.com.  IN  TXT  "v=STSv1; id=20260101000000Z"

More: Requiring TLS: MTA-STS and TLS-RPT, DANE with ACME, and remote domain policies on the forum.

8.8 Checking your work

Give DNS an hour to propagate, then verify. hMailServer has an MX query tool built in (Monitoring & troubleshooting → MX query):

The MX query tool
The MX query tool

Also use these external checkers — they will tell you what the rest of the internet sees:

Aim for 10/10 on mail-tester before you consider yourself live.

DNS summary

Record Name Value Set by
A mail.yourcompany.com Your IP You
MX yourcompany.com mail.yourcompany.com You
PTR Your IP mail.yourcompany.com Your ISP
TXT (SPF) yourcompany.com v=spf1 mx a:mail.yourcompany.com ~all You
TXT (DKIM) dkim._domainkey.yourcompany.com From the Control Panel You
TXT (DMARC) _dmarc.yourcompany.com v=DMARC1; p=none; rua=... You

Part IIRunning it properly

9. Encryption and certificates

Mail must be encrypted in transit. You need a TLS certificate for mail.yourcompany.com.

Since 6.3.4 a new installation is secured before anyone configures it: a database the installation created is given a self-signed certificate for the machine (listed as Self-signed (made at installation)), STARTTLS (optional) on ports 25, 587, 110 and 143 with that certificate, and TLS required for authentication from the Internet IP range. An upgraded or attached database keeps its listeners as they were. Mail clients warn about a self-signed certificate, so replacing it (§9.1 or §9.2) is still the first step.

Since 6.3.4 the connection to the database is encrypted too: MySQL and MariaDB use TLS whenever the database server offers it, without checking its certificate (PostgreSQL’s client already did), and SQL Server’s whole session is encrypted through MSOLEDBSQL 18 or SQL Native Client, but through SQLOLEDB only the login unless TlsMode is require or verify-full. Where the database is on another machine, set TlsMode=verify-full and TlsCaFile in the [Database] section of hMailServer.INI, which stays in the file and is read when the service starts (chapter 21). verify-full is TLS or no connection: the database server’s certificate must chain to the CA in TlsCaFile (SQL Server reads no file and checks against the Windows trust store) and name the host or address in Server=. So give the database server such a certificate first, and try the change with --check-database, which makes one connection and changes nothing, before restarting the service.

9.1 The easy way: automatic Let’s Encrypt certificates

hMailServer has an ACME v2 client built in. It obtains a free certificate, installs it, assigns it to your TLS ports, renews it before expiry and reloads it — with no restart and no scheduled task. It assigns the certificate to every TLS port that has none and, since 6.3.4, to every port that still holds the self-signed certificate a new installation made; a port that names a certificate somebody chose is left alone.

Open Certificates & encryption → Certificates (ACME):

Automatic certificates
Automatic certificates

Or set the same keys over COM (Settings.SetIniSetting), over REST (PUT /api/v1/settings/ini/{name}) or, on Linux, with hmailserver --set-setting NAME=VALUE. Since 6.3.4 the database holds them and the [Settings] section of hMailServer.INI is only its copy: a line edited in the file is ignored in favour of the stored value and reported as HM5804. AcmeEnabled takes effect at the next reinitialise (Apply now on the Control Panel’s pending bar); the other four below apply when saved. The keys:

AcmeEnabled=1
[email protected]
AcmeDomains=mail.yourcompany.com,mta-sts.yourcompany.com,autoconfig.yourcompany.com
AcmeHttpPort=80
AcmeReuseKey=1

Requirements: port 80 must be reachable from the internet (that is how Let’s Encrypt verifies you own the name), and each name in AcmeDomains must already resolve to this server.

AcmeReuseKey=1 matters if you use DANE. Keeping the same private key across renewals keeps your published TLSA records valid. Leave it on.

Certificates are stored in the ACME folder of the data directory (Data\ACME on a default Windows installation), or in AcmeCertificateDirectory when that is set.

More: Let’s Encrypt certificates are automatic: the built-in ACME v2 client on the forum.

9.2 The manual way

If you have a certificate from another CA, go to Certificates & encryption → SSL certificates, add it (PEM certificate and private key, and the key’s passphrase if it is encrypted), then assign it to your TLS ports under Connections & protocols → TCP/IP ports, where the change takes effect at the next reinitialise or service start. The certificate file must hold the leaf certificate followed by the issuer chain. A TLS port with no certificate does not fall back to plaintext: it fails to start (HM5113).

SSL certificates
SSL certificates

The port editor is where the certificate meets the port — protocol, bind address, connection security and the certificate to serve:

The TCP/IP port editor
The TCP/IP port editor

9.3 TLS settings

Certificates & encryption → SSL/TLS controls protocol versions and cipher suites, for the server’s own listeners and for the connections it makes when delivering. The defaults — TLS 1.2 and 1.3 only — are correct. Do not re-enable TLS 1.0 or 1.1 unless a specific ancient client forces you to, and then plan to replace that client.

More: TLS on the listeners: your own certificate, versions, ciphers and client certificates on the forum.

9.4 Outbound transport security

These protect mail you send:

MtaStsEnabled=1            ; honour recipients' MTA-STS policies
DaneEnforcementEnabled=1   ; honour recipients' DANE/TLSA records
DnssecValidationEnabled=1  ; validate DNSSEC for DANE and SPF/DKIM/DMARC lookups

All three are on by default. With DANE enforcement, a recipient whose DNSSEC chain is bogus will not receive your mail rather than receive it unencrypted — which is the point.

Configure them under Certificates & encryption → Transport security.

Transport security
Transport security

Since 6.3.4 you can also demand TLS for one remote domain that publishes no policy of its own, such as a partner or a bank, under Mail flow & delivery → Remote domains: encrypted, verified (the certificate must chain to a trusted root and name the host) or dane. Such a policy can only raise what MTA-STS or DANE require, and a delivery it cannot meet is deferred with 4.7.0, never sent in the clear.

You can also send daily TLS-RPT reports to recipient domains: give a sender address on the Transport security page’s TLS reporting (TLS-RPT) card, where it applies when saved. Until one is set, the statistics are collected and nothing is sent. The key:

[email protected]

More: Requiring TLS: MTA-STS and TLS-RPT, DANE with ACME, and remote domain policies on the forum.


10. Stopping spam

hMailServer filters in layers. Each one is cheap and catches a different kind of junk.

Open Spam & virus filtering → Anti-spam settings:

Anti-spam settings
Anti-spam settings

SURBL servers checks the links inside a message body against URL blocklists:

SURBL servers
SURBL servers

10.1 How scoring works

Each test that matches adds a score. Two thresholds decide the outcome:

  • Spam mark threshold — at or above this, the message is delivered but tagged (X-hMailServer-Spam: YES and X-hMailServer-Reason-* headers added, and the subject prefixed with [SPAM] if you switch that on) so client-side rules can file it.
  • Spam delete threshold — at or above this, the message is refused during the SMTP conversation, so the sending server is told. With quarantine on (the General tab’s Quarantine instead of refusing card), a message judged after its body has arrived is held for review on Spam & virus filtering → Quarantine instead.

Sensible starting values: mark at 5, delete at 20 (the shipped defaults). Set delete to 0 to switch refusing off entirely while you are learning what your mail looks like.

Start by tagging, not deleting. Run for a fortnight with deletion off and read what got tagged. Only then turn on deletion, and set the threshold well above anything legitimate has scored.

10.2 The layers

Test What it does Cost
SPF Checks the sender’s SPF record (RFC 7208 since 6.3.4) Up to 10 DNS-querying terms
DNS blacklists (DNSBL) Is the sending IP a known spam source? One DNS lookup per list
SURBL Are the links in the body on a blocklist? One DNS lookup per URL
Greylisting Temporarily reject unknown senders; real servers retry, spambots do not Free, but delays first mail
DMARC Evaluates the sender’s DMARC policy; the only test on by default The policy lookup, plus SPF and DKIM evaluation
SpamAssassin Full content analysis, hundreds of rules External service

SPF follows RFC 7208 since 6.3.4, uses the server’s own resolver settings, and works on Linux, where every check had been TempError. By default only a hard Fail scores, as before. A client on 127.0.0.0/8 or ::1 no longer passes before any policy is read, so a content filter on the same machine that re-injects mail should authenticate, or connect from an IP range with anti-spam off.

DNS blacklists (Spam & virus filtering → DNS blacklists) are the best effort-to-reward ratio. zen.spamhaus.org alone removes most spam. A new installation lists zen.spamhaus.org and bl.spamcop.net but leaves both inactive: tick Active on the ones you want. Public mirrors of the large lists rate-limit or refuse queries sent through a public DNS service, so if a list suddenly stops matching anything, check its operator’s terms first.

DNS blacklists
DNS blacklists

Greylisting is remarkably effective and costs nothing, but delays the first message from every new sender until the sending server retries after the initial delay, 30 minutes by default. Warn your users, or leave it off for a customer-facing domain. It is switched on, and its delays are set, on the Greylisting tab of Spam & virus filtering → Anti-spam settings. Since 6.3.4 the same tab can make a sender that retries too early wait longer (GreyListingSlideOnEarlyRetry) and refuse one that keeps doing so with a 550 and a lock (GreyListingAbuseThreshold). Both are off by default; the threshold counts attempts, so a server delivering many queued messages to one recipient at once can reach it.

Also since 6.3.4, and each off until you set it: SPF’s SoftFail, TempError and PermError can carry scores of their own, and a TempError can be deferred with 451 4.4.3; a null sender that names a second recipient can be refused, and the connecting address banned; spam-trap addresses refuse exactly as an unknown address does, ban the connecting address and, when the sender’s SPF passes, block the sender; impersonation protection, per domain, flags or holds mail from outside that borrows a colleague’s name or a look-alike of a domain you host; and country blocking judges connections and sign-ins against a MaxMind-format country database you supply, since the server ships none.

A setting profile, new in 6.3.4, switches several layers on at once. Choose one on Set up this server → Setting profiles, which shows the difference before anything is written, or, on a new installation, on the installer’s Server profile page, the seventh, before the service account page (§4.2). The office profile (a small office server) turns on SPF, the MX check, DKIM verification and greylisting, prefixes a marked message’s subject with [SPAM], and holds a message that reaches the delete threshold after DATA in the quarantine rather than refusing it.

More: The anti-spam stack end to end on the forum.

10.3 SpamAssassin

For serious content filtering, connect SpamAssassin. hMailServer talks to spamd over TCP.

Switch it on and set the host and port on the SpamAssassin tab of Spam & virus filtering → Anti-spam settings (127.0.0.1 and port 783 are the shipped defaults), set the score SpamAssassin contributes, and press Test SpamAssassin connection before you leave the page.

Installing SpamAssassin on Windows is covered in the repository’s README — briefly, it is Strawberry Perl plus Mail::SpamAssassin from CPAN, with spamd run as a Windows service. On Linux, spamd is your distribution’s own package. hMailServer reaches it over TCP either way, so it can equally well live on another machine.

Three settings added in 6.2.25, all off by default, on the same tab. Since 6.3.4 they are kept in the database like every other [Settings] key: change them on that tab, over REST or COM, or on Linux with hmailserver --set-setting NAME=VALUE, not by editing hMailServer.ini (chapter 21). Saved on that tab or over REST or COM, they apply at once; a value stored with --set-setting while the service runs is taken at its next save, reinitialise or restart. SpamAssassinLearnOnMove works only with a spamd started with --allow-tell:

SpamAssassinUser=                ; whose spamd preferences to apply (empty = none)
SpamAssassinUserFromRecipient=0  ; 1 uses the recipient address as that user (one-recipient messages only)
SpamAssassinLearnOnMove=0        ; a move into Junk teaches spam, out of it teaches ham

An external filter hook

Since 6.2.24 hMailServer can hand a message to an external HTTP filter — rspamd and the like — and fold the verdict into the same score SPF, DKIM and DMARC land in. A "reject" or "soft reject" verdict is worth FilterHookRejectScore. The settings are on the SpamAssassin tab too, on the card External filtering engine, and are kept in the database like the three above.

FilterHookUrl=                ; empty = off
FilterHookTimeoutSeconds=10
FilterHookRejectScore=100
FilterHookMaxMessageSizeKB=10240
FilterHookFailClosed=0        ; 0 = a filter that cannot be reached does not block mail

Two honest limits. The hook does not speak HTTPS to the engine, so the engine belongs on this machine or on a trusted network. And it cannot express a hard 5xx at DATA independently of the thresholds — its verdict is a score, not a veto. FilterHookFailClosed=0 is the default deliberately: a filter outage should not stop your mail.

10.4 White lists

Two separate lists, for two different jobs:

  • Spam & virus filtering → White list — senders and IPs exempt from spam checks entirely.
  • Spam & virus filtering → Greylisting white list — sending servers, by IP address, exempt from greylisting only, still spam-checked. Use this for services whose mail you need immediately.
The spam white list
The spam white list
The greylisting white list
The greylisting white list

6.3.4 adds three lists to the same group, all empty by default. Spam & virus filtering → Greylisting bypass list lets a named domain or HELO suffix past greylisting only when the message passes every check the entry requires (SPF, DKIM or DMARC, at least one) for that name, a narrower privilege than bypassing greylisting on every SPF pass; every spam test still runs. Spam & virus filtering → Domain white and black list decides about a sending organisation by its envelope domain or HELO suffix rather than its IP address: black is refused at MAIL FROM, white skips greylisting and is not refused by the score, though every test still runs. Spam & virus filtering → Allow and block list is the server’s list of senders every recipient wants or does not, read at delivery after the spam tests; a domain’s own list and each person’s safe and blocked senders come before it, and the nearest wins. An allowed sender’s mail is delivered unmarked; a blocked sender’s is held in the quarantine, or marked when the quarantine is off, and never deleted.

More: Allow and block lists, digests, spam traps, country blocking and impersonation on the forum.


11. Stopping viruses

Open Spam & virus filtering → Anti-virus settings:

Anti-virus settings
Anti-virus settings

11.1 ClamAV

ClamAV is free, open source, and the usual choice. Install it, run freshclam to download signatures, and run clamd listening on TCP 3310.

The connection test changed in 6.2.25. It now asks PING and VERSION first and names the daemon it reached, and it streams its EICAR sample from memory instead of writing it to a file. The old test failed on every Windows with real-time protection enabled, because Defender removed the sample before clamd ever saw it. If you tested this on an earlier build and concluded ClamAV was broken, test it again.

In hMailServer, on the ClamAV tab of that page, enable ClamAV, point it at localhost:3310 and press Test ClamAV connection. Then open Spam & virus filtering → Virus scanning overview, which says whether an infected message would actually be caught: a scanner switched on with a wrong host does not refuse mail, and when every enabled scanner errors, the message is delivered as if it had been scanned and found clean. To hold such a message and try again instead, set AVFailAction to 1 on the General tab (card When a scanner cannot run); as shipped it is retried every 15 minutes and returned to its sender after 16 holds.

Action on virus found:

  • Delete entire e-mail — the message is destroyed and delivered to nobody. The default, and recommended.
  • Delete infected attachments only — the message is rebuilt, not edited: its first text part is kept with a notice added, the subject is prefixed with “Virus found”, and everything else is discarded, so an HTML version, inline images and every clean attachment go with the infected one. A message encrypted end to end (S/MIME or PGP/MIME) is never stripped: it is delivered exactly as it arrived and the application log says so, so only Delete entire e-mail stops it.

Do not configure a bounce for virus mail: leave Notify sender off. The sender address on a virus is virtually always forged, so a bounce just sends junk to an innocent third party — “backscatter”, and a good way to get blacklisted. Both notification switches are off by default for exactly this reason.

Since 6.3.4 the Anti-virus settings page also has a Sandbox tab: a message from outside whose attachment is an executable, a script, a shortcut, an archive, a disk image, an office document or a PDF can be held until a sandbox service you run or subscribe to has opened the file, and quarantined (or returned to its sender while the quarantine is off) if the verdict is malicious. It is off until you set SandboxSubmitUrl and SandboxVerdictUrl and switch SandboxEnabled on, and no vendor adapter is shipped. A message encrypted end to end (S/MIME or PGP/MIME) is scanned only as ciphertext, so a clean result says nothing about its content, and the application log says it could not be examined.

11.2 Blocked attachments

Separate from virus scanning, and worth using regardless: Spam & virus filtering → Blocked attachments lists file-name wildcards, and it acts only while attachment blocking is switched on, on the General tab of Anti-virus settings (AntiVirus.EnableAttachmentBlocking, off as shipped; the office and gateway profiles on Set up this server → Setting profiles switch it on). A matching attachment is not refused or deleted: .txt is appended to its name and its content is replaced by the ATTACHMENT_REMOVED server message, which names the original file, so the message still arrives and the recipient can see what was removed.

Blocked attachments
Blocked attachments

A sound starting list: *.exe, *.scr, *.pif, *.bat, *.cmd, *.com, *.vbs, *.js, *.jse, *.wsf, *.wsh, *.hta, *.jar, *.msi, *.msp, *.cpl, *.lnk, *.reg, *.ps1, *.iso, *.img. No legitimate correspondent emails you an .exe.

The list matches the declared file name only, ignoring case: it does not look inside archives or see a file whose extension lies. Since 6.3.4 a mail flow rule can do more than rename: one with an Attachment name condition can quarantine the message or reject it with a text, and the outbound content policy adds an Attachment type condition read from the file’s first bytes, so a renamed executable is still an executable (chapter 12). An encrypted message is never rewritten by the list, since its ciphertext counts as one attachment.

More: Viruses and dangerous attachments on the forum.


12. Sorting mail automatically: rules and Sieve

Two independent systems, both running during delivery.

12.1 hMailServer rules

Mail flow & delivery → Rules (on screen, Global rules) holds the server-wide rules. Since 6.3.4 a domain can have rules of its own, on the same page with the domain chosen at the top; they run after the server’s and act only on that domain’s mail. Each account also has its own Rules tab in its account dialog.

Rules
Rules

A rule is criteria plus actions. Criteria test the From, To and CC headers, the subject, the body, the message size, the envelope recipient list, the number of delivery attempts or any header you name; since 6.3.4 also an attachment’s name, and the four conditions of the outbound content policy: sensitive content (a card number, an IBAN or an account number), an attachment’s type read from its first bytes, external recipients, and readable text. Actions move to a folder, forward, reply, delete, create a copy, set a header, run a script function or stop rule processing, and, in server and domain rules only, send using a route or bind to an address; since 6.3.4 also add a disclaimer, reject with a text, require TLS, quarantine, or warn the sender. No rule action sets a flag: in Sieve, imap4flags does that.

A Run script function action (the release notes call it the Run function action) calls a function of the event script by name, with the message. The function goes in the script on Maintenance → Event scripts, and scripting must be switched on (Maintenance → Advanced, Scripting tab). Since 6.3.4 the name must be a plain identifier: ASCII letters, digits, _ and $, not starting with a digit. On Windows any other name is refused as HM5018 each time the rule matches and nothing is run; on Linux, where event scripts are JavaScript and the action works since 6.3.4, such a name finds no function and the ERROR log has a Script Error line. Every release from 6.0.0 to 6.3.3 wrote the name into the script’s call unescaped, in VBScript and JScript alike (CVE-2026-100741), so whoever could edit a rule, a domain’s own administrator included, could run code of their own in the service. Until a server older than 6.3.4 is upgraded, switch event scripting off. One that cannot do without its scripts should take the OnClientValidatePassword, OnExternalAccountDownload and OnDeliveryFailed handlers out of a JScript EventHandlers.js and reload the scripts, and, in either language, let nobody but the server administrator make or edit a rule with a Run script function action.

Useful examples:

  • Move anything with [SPAM] in the subject to a Junk folder.
  • Forward everything for sales@ to a distribution list.
  • Delete messages over 50 MB before they fill a mailbox. The size is counted in bytes, so the value is 52428800, not 50. A Delete is silent, because the sender has already been answered 250; to have the sender told, use Reject with a text in a server rule (since 6.3.4, answered 550 5.7.1 before the message is accepted), or set Maximum message size on the SMTP tab of Connections & protocols → Protocols.

12.2 Sieve (RFC 5228)

hMailServer also implements Sieve, the standard mail filtering language — portable across mail servers, and what most modern clients expect.

Each account can have an active Sieve script, edited on the account’s Sieve tab:

require ["fileinto"];

if header :contains "subject" "[SPAM]" {
    fileinto "Junk";
} elsif address :domain "from" "newsletters.example.com" {
    fileinto "Newsletters";
} else {
    keep;
}
The account's Sieve tab
The account’s Sieve tab

Supported: keep, fileinto, discard, redirect, and implicit keep; tests header, address, exists, size, allof/anyof/not; match types :is, :contains, :matches. Two extensions arrived in 6.2.19 and 6.2.21:

  • imap4flags (since 6.2.19) — setflag, addflag, removeflag and the :flags tag on fileinto/keep, applied to the stored message. The five system flags (\Seen \Answered \Flagged \Deleted \Draft) can be stored; a custom keyword is noted in the application log rather than dropped silently.
  • body (RFC 5173, since 6.2.21) — filters can read the message itself, not just its headers, with all three transforms: :text (the decoded text of every text part — transfer encoding and charset are decoded before matching, so a rule keeps working when a sender’s client base64-encodes the part), :content "type" (decoded parts of the named MIME types), and :raw (the body exactly as it arrived).

Sixteen more have arrived since, and the capability line advertises twenty-four extensions in all:

RFC Extension What it adds
5490 mailbox fileinto :create — file into a folder, creating it if it is missing
5463 ihave Capability probing as a test, so one script serves servers with different feature sets
5183 environment Which server, where, and at what phase the script is running
5260 date, index The date/currentdate tests, and :index/:last for repeated header fields. Two capability names, by that RFC’s own registration
3685 / 5235 spamtest, spamtestplus The server’s own spam verdict as a graded value. virustest is deliberately absent — infected mail never reaches a script
7352 duplicate Has a message with this identifier been seen before. The seen-store fails open
5293 editheader addheader and deleteheader. Received and Return-Path are protected
5229 variables set, ${} expansion, match variables from :matches, and the string test
5429 reject, ereject Both refuse by report: scripts run after the SMTP transaction has accepted the message, so the RFC’s DSN fallback is the only honest spelling
6609 include include, return, global. Personal scripts are the account’s own, global ones the administrator’s. Nesting capped at three levels
5435 / 5436 enotify Notifications go out with a null return path and Auto-Submitted: auto-notified; an auto-submitted message is never notified about
— regex draft-ietf-sieve-regex, which never became an RFC. Implemented because Dovecot’s Pigeonhole implements it and clients offer it
5228 envelope Tests the SMTP envelope rather than the headers. Implemented earlier, advertised since 6.2.25, once a test proved the test command itself

Also advertised, and present at 6.2.21: copy (RFC 3894), relational (RFC 5231), subaddress (RFC 5233), vacation (RFC 5230) and vacation-seconds (RFC 6131). The comparators are i;ascii-casemap, i;octet and i;ascii-numeric.

Anything this server does not implement is refused when the script is uploaded, with an error naming the construct (since 6.2.21) — a script never parses as valid and then silently does nothing.

To let users manage their own scripts from their mail client, enable ManageSieve (RFC 5804). It is off as shipped (ManageSieveServerPort is 0). Since 6.3.4 its two keys, below, are kept in the database, and their lines in hMailServer.INI are only a copy: a line edited in the file is ignored, reported as HM5804 and, where the service may write the file, put back. Set them on the ManageSieve (RFC 5804) card of Monitoring & troubleshooting → API & monitoring, over REST (PUT /api/v1/settings/ini/{name}) or COM, or on Linux with sudo hmailserver --set-setting ManageSieveServerPort=4190. Both take effect at a reinitialise (Apply now in the Control Panel or the Control Deck, or POST /api/v1/server/reinitialize), without a service restart. The values:

ManageSieveServerPort=4190
ManageSieveServerBindAddress=127.0.0.1

ManageSieve’s only SASL mechanism is PLAIN, but it is no longer offered blindly. The listener advertises STARTTLS whenever a TLS certificate is configured on an IMAP, POP3 or SMTP port, and where the connecting IP range requires TLS for authentication it advertises an empty SASL list and refuses AUTHENTICATE with NO (ENCRYPT-NEEDED) until STARTTLS has been issued. Bind it to 127.0.0.1 anyway unless a certificate is available to it.

For a cookbook of worked rule recipes — and what to do when a rule refuses to match — see chapter 27.

More: Rules and Sieve: what runs when, vacation replies and the Run function action on the forum.


13. Aliases, distribution lists and public folders

13.1 Aliases

An alias is a second address that delivers to a target: usually an existing mailbox, though the target may also be another alias, a distribution list or an address at another server. info@ → sales@ costs no extra mailbox and no extra password.

Since 6.3.4 two more kinds of second address exist. Renaming a mailbox or moving it to another domain (Move or rename… on the account’s General tab) keeps the old address as an alias of the new one by default, so mail to it goes on arriving. And users can make masked addresses for themselves in the webmail (Settings, Masked addresses): each delivers to its owner until switched off, and then mail to it is refused rather than handed to the catch-all. ReaderMaskedAddressesMax (20 by default) caps how many each person may make, and 0 removes the page.

Domain aliases work at the domain level: mail to yourcompany.net can be delivered to the accounts of yourcompany.com.

13.2 Distribution lists

A distribution list delivers one message to many recipients — [email protected]. Since 6.3.4 an address, or a whole domain, can be routed to a webhook, and a route is asked before any account, alias or list: a route that only posts takes the address’s mail in place of its mailbox, alias or list, so a list whose address or domain is routed never sees the message and applies none of its posting rules to it. Route a list’s domain with care.

The distribution-list dialog
The distribution-list dialog

You control who may post: anyone, only list members, only the one address in Require sender address (announcements only), or anyone whose address is in the list’s own domain; Require SMTP authentication can be added to any of these. Set this deliberately. A publicly postable list is a spam relay in waiting, and anyone in the domain only checks the address the sender claims: tick Require SMTP authentication with it, or the list is open to the internet.

Since 6.3.4 the distribution-list dialog also takes a Membership query: domain (the list’s own domain), domain:other.example (another domain this server hosts), pattern:sales-*@example.com or everyone. It is answered against the server’s active accounts each time a message is sent and added to the addresses under Recipients, so everyone@ stays current with nobody editing it. With Managed ticked, a list gets the list manager: each member is sent a copy of their own that carries a way to leave, a member whose address bounces (by default one hard bounce, or five soft ones within thirty days) is paused and the owner told, and the manager’s other fields decide who may join by themselves (nobody until the subscribe policy is changed from closed, the default), whether an archive is kept and whether a daily digest is offered. Its web pages need the REST listener and a ListBaseUrl, and nothing changes for a list until it is switched on. The list does not rewrite From: a posting from an outside domain that publishes p=reject reaches outside members failing DMARC once the list has changed it, and Gmail and Yahoo may refuse it, so know where a list’s postings come from before you tick Managed.

13.3 Public folders

Accounts & domains → Public folders creates shared IMAP folders visible to multiple users — a shared archive, or a team inbox.

Public folders
Public folders

Permissions (select a folder and press Permissions) are granted to one account, to a group from Accounts & domains → Groups, or to Anyone (every authenticated account on the server), with the IMAP ACL rights of RFC 4314: lookup, read, insert, delete, administer and the rest. Grant Lookup with anything else: without it the folder does not exist for that user. A sub-folder given any entry of its own stops inheriting its parent’s entries, which are not merged with it.

More: Aliases, distribution lists, membership queries, mailing lists and public folders on the forum.


14. Routes and relays

14.1 Sending through a smart host

If your ISP blocks outbound port 25, or your IP has poor reputation, relay outbound mail through a smart host — your ISP’s server, or a service such as SendGrid, Mailgun or Amazon SES.

Configure it in Mail flow & delivery → Delivery of e-mail, on the Relayer tab — the SMTP relayer host, port, connection security and credentials. When the relay takes a user name and password, set the connection security to STARTTLS (required), or SSL/TLS if the relay expects it, not STARTTLS (optional), so that a downgrade cannot send your credentials in clear.

Changed in 6.3.4. The same tab’s OAuth2 for the relay (Microsoft 365) card signs in to the relay with a token instead of a password, and that token now goes over TLS only. A delivery that would present it (through the relayer, or a route, to a host on OutboundOAuth2Hosts, smtp.office365.com by default) with connection security None, or STARTTLS (optional) against a server that does not offer STARTTLS, no longer sends the token in the clear: it fails with a temporary 450 4.7.0 and waits for its retries, and the error log’s HM5912 says to set SSL/TLS or STARTTLS (required). Exchange Online offers STARTTLS on port 587, so a relay set up as Microsoft documents it is not affected.

The relayer field accepts several hosts separated by | — smtp1.example.com|smtp2.example.com — and delivery fails over to the next when one cannot be reached. This has always worked; it just went undocumented until 6.2.14. Every host in the list uses the port, connection security and credentials configured beside the field, so they must all accept the same sign-in; and a permanent (5xx) rejection is final and is not tried on the next host, which could otherwise deliver the message twice.

Delivery of e-mail
Delivery of e-mail

This is the standard fix for a home or small-office installation that can receive mail but cannot send it.

14.2 Routes

A route overrides delivery for a specific domain — “mail for partner.example.com goes to this specific server, not wherever DNS says.”

Routes
Routes

Used for internal domains, split delivery during a migration, or a partner with a private link.

New in 6.3.4. Mail flow & delivery → Remote domains, beside Mail flow & delivery → Routes, holds a policy for one remote domain or a pattern: the TLS this server demands in each direction (a delivery that cannot meet it is deferred, never sent in the clear), size, connection and rate limits, whether automatic replies and forwards may go there, and a recipient check with the primary for a domain this server is a backup MX for. A route says where mail for a domain goes and a policy says what this server will do when it gets there, so a policy applies to a routed delivery exactly as to an MX one; a saved policy is in force for the next message.

14.3 Incoming relays

Mail flow & delivery → Incoming relays lists servers that legitimately forward mail to you — a front-end spam filter, for instance. hMailServer then reads the original sender IP from the Received headers instead of blaming your own filter for everything. Each entry is a name and an IP range (lower and upper address): keep the range to the gateway’s own addresses, because a machine inside it can insert a forged Received header and choose which address is judged.

Changed in 6.2.24. The anti-spam tests now run against the address the relay actually observed, rather than the first bracketed value in the Received header — which was the client’s own HELO literal, and therefore chosen by the sender. It is the correct behaviour, and it moves scores: if you tuned thresholds around the old one, re-check them after upgrading.

Changed in 6.3.4. A listed relay changes two more things. A remote domain policy’s requirement of TLS on mail from a domain is not applied to a connection from the relay, because the hop that policy is about happened before the gateway, so require TLS on the gateway itself. And a domain’s external-sender tag treats mail a listed relay hands over as coming from inside, so behind a filtering gateway listed here nothing is tagged.

Incoming relays
Incoming relays

More: Routes, relays and smart hosts, OAuth2 relaying, restricted delivery and recall and Behind a gateway, load balancer or reverse proxy on the forum.


15. Backup and restore

15.1 What must be backed up

What Where Why
Messages The data directory (default C:\Program Files\hMailServer\Data); since 6.3.4, when MessageStoreObjectService names a store (s3 or azure), also the S3-compatible bucket or Azure Blob container that MessageStoreObjectBucket names The mail itself. A message held only in the object store is not in the data directory: the built-in backup with Back up messages reads it into the archive, and a copy of the data directory does not
Database Your database server, or Database\hMailServer.sdf for the built-in Accounts, domains, settings
hMailServer.INI Bin\hMailServer.INI The database connection, the directories, the administrator password and PasswordPepper, none of which the built-in backup carries; since 6.3.4 the [Settings] keys live in the database and the file holds a copy. Since 6.3.4 it also holds the stored-secrets key (the Windows installer makes one on a new installation; an upgraded one has none until --create-stored-secrets-key). A database backup restored without that key brings back every second factor and [Settings] secret unopenable, and on Windows a copy of this file is no copy of the key, because its line is sealed under the machine’s DPAPI: export it once with hMailServer.exe --export-stored-secrets-key FILE, elevated, and keep that file apart from the database backups
Certificates and DKIM keys Wherever you put them; Data\ACME for ACME Recreating these is painful

On Linux the same four live at /var/lib/hmailserver (messages, and the ACME directory under it), your database server, /etc/hmailserver/hMailServer.ini, and wherever you put certificates and DKIM keys. One extra file matters there and has no Windows counterpart: /var/lib/hmailserver/.hmailserver-secret-key, the 32-byte key every stored secret is encrypted under. Back it up with the database — a database restored without it has lost every stored password. Since 6.3.4 it also opens the stored-secrets key line in hMailServer.ini, once there is one (sudo hmailserver --create-stored-secrets-key makes it), so keep the key file and the configuration together, or export the key with sudo hmailserver --export-stored-secrets-key FILE.

15.2 The built-in backup

Maintenance → Backup & restore backs up the database and settings, on a schedule if you like. The schedule is four settings, not an external task. Since 6.3.4 they are kept in the database, not read from hMailServer.INI: set them on that page, over REST (PUT /api/v1/settings/ini/{name}) or COM, or with hmctl ini set, and each applies when it is saved. On Linux sudo hmailserver --set-setting NAME=VALUE stores one as well, but a value it stores while the service runs takes effect only at the service’s next save, a reinitialise or a restart, so reload the service afterwards (sudo systemctl reload hmailserver, a reinitialise). A line edited into the file is reported as HM5804 and, where the service may write the file, put back. The keys and their defaults:

ScheduledBackupTime=              ; 24-hour local HH:MM for a daily run; wins over the interval
ScheduledBackupIntervalMinutes=0  ; or run every N minutes (0 = off)
ScheduledBackupKeepCount=0        ; keep this many archives (0 = keep all)
ScheduledBackupMaxAgeDays=0       ; and/or delete archives older than N days (0 = off)

Since 6.3.4 a new Windows installation runs the service as NT SERVICE\hMailServer, which can write only the folders it has been granted: the program folder’s own Data, Logs, Temp and Database. A backup destination anywhere else on the machine must be approved first, with Grant access on the Windows service account card of Maintenance → Server limits & expert settings, or hMailServer.exe --confirm /GrantServiceAccess from an elevated prompt. Some folders are never granted, approved or not: a drive’s root, a folder in the program folder outside those four, anything else in a Program Files folder, the Windows folder or the user profiles folder, and a folder reached through a junction or link. Move such a destination to a folder of its own, for example D:\Backups\hMailServer. A destination on a network share is reached as the computer account (DOMAIN\COMPUTER$), which needs the right on the share and its folder.

Since 6.2.25 a backup verifies itself. With BackupVerifyRestore=1 — the default — the archive’s message store is extracted to a scratch directory through the same code a restore runs, before the backup is reported complete. Set it to 0 only for a store your temporary volume cannot hold.

Since 6.3.4 the archive also carries what an account keeps beyond its mail — the address book, webmail preferences, scheduled sends, file links, S/MIME keys, calendars, password history and message labels — and a restore puts it back. An archive written by 6.3.3 or earlier holds none of it, and a whole restore from one deletes it with the accounts it replaces.

Since 6.3.4 an archive can be encrypted. Set a passphrase (12 to 256 printable ASCII characters) under Encryption on Maintenance → Backup & restore and every archive is written as 7-Zip AES-256 with its headers encrypted; the passphrase is never written into the archive, a restore asks for it, and if it is lost, every archive written with it is lost, so keep it apart from the backups. An encrypted backup of messages needs Compress destination files on and room for the message store in the temp directory; an archive written without a passphrase holds the server’s stored secrets and every account’s second factor in the clear.

Backup and restore
Backup and restore

The built-in backup does not copy your messages by default. Tick Back up messages to include them, always together with Back up domains: without it the messages are not copied and the run fails. Or back up the data directory separately. Many people discover this at the worst possible moment.

With Compress destination files off, every run overwrites the same DataBackup folder, so you keep one generation of mail whatever the retention settings say.

15.3 Restoring

  1. Install the same version of hMailServer, and stop its service before you restore anything into it.
  2. Restore the data directory.
  3. Restore the database.
  4. Restore hMailServer.INI onto the machine that wrote it. On a different Windows machine do not copy it over wholesale: its database password and stored-secrets key line are sealed under the old machine’s DPAPI and open nowhere else. Point the new installation’s own [Database] connection at the restored database, and copy PasswordPepper across if one was set.
  5. Since 6.3.4, if the database came from an installation with a stored-secrets key and this machine does not hold that key, import the exported key before the service starts: hMailServer.exe --import-stored-secrets-key FILE (elevated) or sudo hmailserver --import-stored-secrets-key FILE. The import refuses while the machine holds another key, and a new Windows installation made one of its own, so first remove that installation’s [Security] StoredSecretsKey line from hMailServer.INI. Without the database’s key the service reports HM7495, every sealed second factor refuses its codes and the [Settings] secrets read as not set.
  6. Start the service and check Monitoring & troubleshooting → Server status.

On a new server, enter again what the restore does not bring in a form that opens there: each domain’s own relay password and, since 6.3.4, the backup passphrase, the SNMP agent’s passwords and community, the object store’s secret key or SAS and the sandbox’s bearer token. No archive carries them, and on Windows a copy of the database brings them sealed under the old machine’s DPAPI. Set the passphrase first: until it is set, a server restored from an archive writes its backups unencrypted, with its stored secrets and every account’s second factor in the clear, and on Windows one restored from a copied database fails every backup.

Since 6.3.4 one mailbox, or one message, can come back without a whole restore, from an archive in the backup destination taken with domains and messages: Restore a mailbox and Restore one message on Maintenance → Backup & restore, or the Control Deck, REST, COM and hmctl. Merge adds each archived message the mailbox lacks; Replace empties the account’s folders first, so mail that arrived since the backup is gone.

A caveat about secrets, and it differs by platform. With ProtectStoredSecretsWithDPAPI=1 (the default), stored passwords — the database password, route/fetch/relay passwords — are protected by the platform’s own secret store. Since 6.3.4 an account’s second factor and the [Settings] secrets, which earlier releases kept as written, are sealed under the installation’s stored-secrets key once it has one, and a copy of the database opens them only where that key is held or has been imported (§15.1, §15.3).

On Windows the platform’s store is machine-scoped DPAPI, and the secrets cannot be decrypted on a different machine. Restoring a copy of the database onto new hardware means re-entering them. The built-in backup and restore does not: it carries route, fetch-account and certificate-key passwords in a portable encoding and protects them again on the new machine (the per-domain relay password is in no archive, so re-enter that either way). If you would rather have portable database copies, set ProtectStoredSecretsWithDPAPI=0 to keep the older portable encoding — weaker at rest, but movable. Since 6.3.4 that setting at 0 also stops the second factors and [Settings] secrets being sealed: they are stored as written.

On Linux the same setting selects a key file instead: <DataFolder>/.hmailserver-secret-key, 32 random bytes at mode 0600, written once the first time a secret is stored and never rewritten; each secret is AES-256-GCM under it and stored as LINUX1:<base64>. The key travels with the data directory, so a file-level restore that carries Data/ carries the secrets with it — which the Windows store cannot offer. The built-in archive leaves the key file out, and carries the passwords and second factors it holds in a portable form instead; a domain’s S/MIME authority key travels as its envelope and must be rotated where it does not open. The file is refused if any account but its owner can read it, and if it cannot be made or read the secret is not stored at all rather than falling back to something weaker (HM6410 to HM6413, HM6416).

A Passwordencryption=6 value written by a Windows installation is a DPAPI blob that nothing on Linux can open. It is recognised as one and reported as HM6414, and the password has to be re-entered in plain text.

More: Backup and restore: what is in the archive, what is not, and restoring one mailbox on the forum.


16. Monitoring and health

16.1 The live view

  • Monitoring & troubleshooting → Server status — the server’s version, state and uptime, the database, the session and message counters, and a list of configuration warnings (a missing host name, an open relay, an auto-banned 127.0.0.1) worth reading first.
  • Monitoring & troubleshooting → Delivery queue — messages waiting to go out, each with its next try and try count. The age of the oldest message is your earliest warning, more than the queue’s size: a deep queue that drains is a busy server, but a message that has waited for hours is mail a destination is not getting.
  • Monitoring & troubleshooting → Live logs — a real-time, colour-coded log tail, shown only when the Control Panel runs on the server itself.
  • Monitoring & troubleshooting → Audit trail — since 6.3.4, and on as shipped: every administrative change, who made it, over which interface and from where, with a setting’s old and new value and Put back to restore the old one.
Server status
Server status
The delivery queue
The delivery queue

16.2 Logs on disk

Logs live in the Logs directory — /var/log/hmailserver on Linux. Monitoring & troubleshooting → Logging controls what is written. Since 6.3.4 LogDeleteDays and JsonLogging are stored in the database, as nearly every [Settings] key is, and both apply when saved; on Linux set them as §16.3 describes, not by editing hMailServer.ini.

Logging settings
Logging settings
  • Turn debug logging on only while diagnosing something — it is verbose and it will fill your disk.
  • LogDeleteDays=30 deletes the log files named hmailserver_*.log or ERROR_hmailserver_*.log that nothing has written to for 30 days, and nothing else in the directory. The default, 0, keeps everything for ever, which with debug logging on is the usual way a log volume fills. On Linux the packages install a logrotate rule for the same files: use one or the other, not both (§35.11).
  • JsonLogging=1 writes JSON lines, for ingestion into Elasticsearch, Loki or Splunk. It is overridden while the log format is NCSA, with no error.
  • Since 6.3.4 every entry can also be copied to a syslog collector (RFC 5424, over UDP, TCP or TLS) or, on Linux under systemd with no collector named, to the journal. It is off as shipped; switch it on with the syslog (RFC 5424) card on the Logging page or, on Linux, with PUT /api/v1/settings/logging or the Control Deck’s Settings view. These settings are read at the next start or reinitialise, not when saved.

More: Reading the logs: levels, files, the Logs page, following a connection, and fail2ban on the forum.

16.3 Prometheus metrics and health probes

One small HTTP listener serves the metrics and three health probes. It is off until it has a port (MetricsServerPort is 0 as shipped). The bind address must be an IP address: a host name or localhost is refused, the application log says MetricsServer: Invalid bind address, and neither the metrics nor the probes start.

MetricsServerPort=9100
MetricsServerBindAddress=127.0.0.1
  • /metrics — Prometheus format: messages processed, spam and virus counts, TLS handshake success/failure, authentication success/failure, sessions per protocol, uptime, database pool state, delivery-queue depth and the age of its oldest message, delivery outcomes (hmailserver_messages_delivered_total, _deferred_total, _bounced_total), per-command latency and certificate expiry. Since 6.3.4 also the message store’s size and message count, country blocking, complaint rates and spam-trap hits: 38 metric families, against 30 in 6.3.3.
  • /livez — is the process alive?
  • /readyz — 200 when the server is running and the database has answered within the last 20 seconds; 503 otherwise, including while it drains for maintenance or before a stop. While the server starts nothing is listening yet, so a probe is refused rather than answered 503.
  • /healthz — JSON summary of status, server state and database.

The /readyz and /livez shapes are deliberately Kubernetes-compatible, so a load balancer or orchestrator can use them directly.

The listener takes authentication and TLS as of 6.2.25. Bound to anything but loopback, /metrics needs a credential: without one it answers 503. The three probes answer without a credential in every configuration. With a certificate and a key set, the whole port is HTTPS, probes included:

MetricsServerAuthToken=        ; bearer token for /metrics
MetricsServerAuthUsername=     ; or HTTP Basic
MetricsServerAuthPassword=
MetricsServerCertificateFile=  ; PEM
MetricsServerPrivateKeyFile=
MetricsPerDomainEnabled=0      ; per-domain series as well as server totals

And the server remembers its metrics. One row per metric per minute is kept for MetricsHistoryDays (7) in hm_metricsamples — the table schema 6028 added — readable over COM, over GET /api/v1/metrics/history, on the dashboard for a day, a week or a month, and since 6.3.4 in the Control Deck’s Metric history view. OTLP export is separate and off by default: OtelMetricsEndpoint, OtelLogsEndpoint and OtelMetricsInterval (60).

Since 6.3.4 the server raises alerts of its own. Seven of its eleven conditions are on as shipped, a failed backup and a certificate within 7 days of expiry among them, but nothing is mailed until a recipient and a sender address are set (alert_recipient and alert_sender_address under /api/v1/settings, or the Control Deck’s Settings view). The rules themselves are on /api/v1/alerts/rules; there is no Control Panel page for them. A read-only SNMPv3 agent, also new, answers from the same counters as /metrics; it is off as shipped and answers no manager until one is listed in SnmpAllowedAddresses.

Monitoring & troubleshooting → Live logs is not the only live view any more either: the Control Deck reads the log files over /api/v1/logs, a named file’s last 200 or 2,000 lines at a time (chapter 36). Since 6.3.4 its Logs page also lists the connections open now and the last ones that ended, and Follow shows every line logged for one of them as it is written (GET /api/v1/logs/sessions). The server keeps the last 4,096 log entries in memory for this; nothing survives a restart.

Configure the listener, the history and the OTLP export under Monitoring & troubleshooting → API & monitoring on Windows. On Linux there is no Control Panel: set these keys with sudo hmailserver --set-setting NAME=VALUE, with hmctl ini set NAME VALUE (installed with the packages), or over the REST API (PUT /api/v1/settings/ini/{name}, with the administrator password); the Control Deck has no editor for these keys. Since 6.3.4 the database holds them on both platforms, and a line edited in hMailServer.ini is not applied and is reported as HM5804 (chapter 21). The listener’s port, bind address, credentials and certificate files wait for a reinitialise (Apply now in the Control Panel, sudo systemctl reload hmailserver on Linux); the history, per-domain and OTLP keys apply when saved. A value stored with --set-setting while the service runs reaches it at the next save made on the server, a reinitialise or a restart; nothing watches the table.

API and monitoring
API and monitoring

More: Monitoring and health: Prometheus metrics, health probes, Grafana, OTLP and SNMPv3 and Alerts, the audit trail, syslog and the journal, and per-domain reports on the forum.

16.4 Diagnostics

Monitoring & troubleshooting → Diagnostics runs ten tests against the running server, among them outbound port 25 (or the smart host), your MX records and a connection to each MX host, the backup directory, where message files are stored, the installation’s paths, an auto-banned 127.0.0.1 and any ERROR log file, and reports each in plain language, with the last message-store consistency scan below. Fill in the local domain and test domain boxes first: three of the tests are skipped silently when they are empty. Run this first when something breaks. Since 6.3.4 the Control Deck has a Diagnostics view that runs the same tests from a browser.

Diagnostics
Diagnostics

17. Security hardening

The settings in this chapter’s code blocks are [Settings] keys. Since 6.3.4 the database holds them: set each on its Control Panel page, over COM (Settings.SetIniSetting), over REST (PUT /api/v1/settings/ini/{name}) or, on Linux, with sudo hmailserver --set-setting NAME=VALUE (a secret such as SRSSecret with the name alone, typed at the prompt). A line edited in hMailServer.INI is ignored in favour of the stored value, reported as HM5804 and, where the service may write the file, put back (chapter 21). Every key in these blocks applies when it is saved in the Control Panel, over COM or over REST, with no restart; a value stored with --set-setting while the service runs is in force only once the service next saves a setting, reinitialises or restarts (sudo systemctl restart hmailserver).

17.1 The essentials

  • Never become an open relay. hMailServer’s defaults are safe; the danger is a careless IP range. Check Sign-in & access → IP ranges and make sure no external range is allowed to relay without authentication.
IP ranges
IP ranges
  • Auto-ban blocks an IP after repeated failed logins. On by default — leave it on. Tune it under Sign-in & access → Auto-ban.
Auto-ban and TLS
Auto-ban and TLS

A 6.2.10-era capture, when auto-ban was the third tab of the SSL/TLS page. Auto-ban is a page of its own now; the protocol versions stayed on Certificates & encryption → SSL/TLS.

  • Require TLS. Disable plaintext logins on ports reachable from the internet: set Require TLS for auth (Require SSL/TLS when authenticating in the range’s dialog) on the Internet IP range, and AUTH is not offered on a connection from that range until it has started TLS. A new installation of 6.3.4 starts that way: the installer, or on Linux the unit’s first start after --create-database, gives the database it created a self-signed certificate, STARTTLS on 25, 587, 110 and 143 and Require TLS for auth on the Internet range. An upgraded server is never changed; the setup checklist’s Passwords only over TLS item says what is left. Mail clients warn about the self-signed certificate until it is replaced (chapter 9).
  • Strong passwords everywhere. Use the generator in the account dialog.

17.2 Modern authentication

hMailServer supports SCRAM-SHA-256 and SCRAM-SHA-256-PLUS (with channel binding) across IMAP, POP3 and SMTP submission — the password never crosses the wire, even inside TLS. Also supported: OAuth2 bearer tokens (XOAUTH2 and OAUTHBEARER, RFC 7628) for integration with an identity provider.

Password storage defaults to PBKDF2-HMAC-SHA256, with Argon2id available. Existing weaker hashes upgrade transparently the next time the user logs in. You can enforce a floor; the scheme and the floor are both set on Sign-in & access → Authentication, card Password storage:

MinimumAcceptedHashAlgorithm=4   ; 4 = PBKDF2; refuse a logon whose stored hash is weaker. 0 = off (the default)

scrypt joined the list in 6.2.25. PreferredHashAlgorithm now takes 3 (SHA256), 4 (PBKDF2, the default), 5 (Argon2id) or 7 (scrypt); anything else is refused at read time and reported as HM5528 while the server falls back to 4. If you ask for Argon2id on a build whose OpenSSL has no Argon2id KDF, that is reported as HM5607 and scrypt is used instead. The work factor is settable, and only ever re-derived upward at the next logon:

PasswordHashIterations=0      ; PBKDF2 iterations. 0 = the built-in 210,000
PasswordHashMemoryKB=0        ; Argon2id memory. 0 = the built-in 19,456 KiB
PasswordHashTimeCost=0        ; Argon2id passes. 0 = the built-in 2

Also since 6.2.24 and 6.2.25: SASL EXTERNAL (RFC 4422 Appendix A) — a client certificate verified against the port’s CA logs on as the mailbox its addresses name, on SMTP, IMAP and POP3; app passwords, so a device gets a credential of its own; and OAuth2 hardening — OAuth2JwksUrl makes the provider’s JWK Set the signing-key source, and OAuth2IntrospectionUrl adds token introspection so a revoked token stops working. Both are off unless configured.

Since 6.3.4 the webmail and the Control Deck also take passkeys and single sign-on through an OpenID Connect or SAML 2.0 identity provider, and a new password can be checked against the published corpus of breached passwords (off by default; the Password tab of Sign-in & access → Administrative access). None of these reaches IMAP, POP3 or SMTP, which still take the password or an app password and the protections in §17.5.

SCRAM-SHA-1 is not offered, deliberately. A client that cannot do SCRAM-SHA-256 uses PLAIN or LOGIN over TLS.

17.3 Run as a least-privilege account

Since 6.3.4 a new installation runs the service as NT SERVICE\hMailServer, the service’s own virtual account: no password, and it reaches only the folders setup grants it. Before 6.3.4 every installation ran as LocalSystem, the most privileged account on the machine, and an upgrade keeps the account the service has. The installer’s Service account page offers the other one either way, and a silent installation takes /serviceaccount=virtual, /serviceaccount=localsystem or /serviceaccount=keep. To move an upgraded service later, run the installer again, use Move to NT SERVICE\hMailServer on Maintenance → Server limits & expert settings (card Windows service account), or, from an elevated Command Prompt in the Bin folder, run hMailServer.exe --confirm /GrantServiceAccess, then, only if it ended in 0, hMailServer.exe --service-account virtual /Register; whichever way you choose, the service runs as the new account once it is restarted. Move to LocalSystem on the same card, or hMailServer.exe --service-account localsystem /Register and a restart, goes back. Before the move, give SQL Server reached with Windows authentication on the same machine a login for the account (CREATE LOGIN [NT SERVICE\hMailServer] FROM WINDOWS and a db_owner user), and keep certificate and key files inside Data\Certificates or in a folder you approve.

hMailServer.exe /GrantServiceAccess grants the account the program folder’s Data, Logs, Temp and Database folders and the folders an administrator approved, and nothing the configuration merely names. Under the virtual account hMailServer.INI is read-only to the service, which works on with the database’s values, and three things only LocalSystem could do are refused with a sentence: the live update’s apply (an administrator runs the verified installer instead), the auto-ban’s firewall rules and Utilities.MakeDependent. Because the file is read-only to it, a change of the administrator password over COM or in the Control Panel is refused as well: run hMailServer.exe --set-admin-password from an elevated prompt (it reads the password from HM_ADMIN_PASSWORD or standard input, never from its command line) and restart the service. ServiceAccountName and ServiceAccountPassword are retired: /Register takes the account from --service-account alone.

From 6.3.6 Setup also runs hMailServer.exe /ProtectFolders at every installation and upgrade, whatever account the service runs as: the program folder’s Data, Logs, Temp, Database and Events and Bin\hMailServer.INI stop inheriting what the folder above hands down — BUILTIN\Users’ Read above all, which let any account that could sign in to the computer read every message, the logs, the event scripts and the database’s password — and keep SYSTEM and the Administrators, the service’s grant, every deny entry and every entry of their own. On a large message store the first run takes minutes. A folder the configuration names outside the program folder, the backup and PST import folders and a key file with permissions of its own are checked and not changed: a CHECK line in the setup log names who may read each and the icacls that closes it. An account that is not an administrator and read the logs or the message store — a script, a backup program, a monitoring agent — is given an entry of its own (icacls "<folder>" /grant "<account>:(OI)(CI)RX"), which later runs keep; an editor that saves hMailServer.INI by replacing the file gives it Bin’s permissions again, so run /ProtectFolders after it.

On Linux these two settings do nothing. The packaged systemd unit already runs the server as its own hmailserver user, never as root, and declares the rest of the sandbox itself — ProtectSystem=strict with ReadWritePaths limited to /var/lib/hmailserver and /var/log/hmailserver, NoNewPrivileges, a capability bounding set of nothing but CAP_NET_BIND_SERVICE, a private /tmp, and a system-call filter. Change any of it with systemctl edit hmailserver, because a package upgrade replaces the unit file and a drop-in survives it. Chapter 35 has the detail.

17.4 Two-factor authentication for administrators

The Control Panel offers TOTP two-factor authentication of two kinds, both on the Two-factor tab of Sign-in & access → Administrative access. Use the card Second factor on the administrator credential (recommended): it shows a scannable QR code; scan it with Microsoft Authenticator, Google Authenticator, Authy or 1Password, confirm with a code, and every subsequent administrator logon requires one, over COM and the REST API as well (below). The card Two-factor for this Control Panel only, whose setup dialog the Connect screen’s Set up two-factor authentication… link also opens, is older and weaker: the Control Panel asks for its code only after the server has accepted the password, so it does nothing for the REST API or for scripts.

Two-factor authentication setup — QR code, setup key and verification
Two-factor authentication setup — QR code, setup key and verification (the Control-Panel-only kind)

Turning the Control-Panel-only kind on or off requires the Control Panel to run as administrator on Windows — its secret is stored under HKLM.

Since 6.2.25 the recommended second factor is the server’s, not the Control Panel’s. Enrol it with Settings.EnrolAdministratorTOTP (and clear it with Settings.DisableAdministratorTOTP); Application.AdministratorTOTPEnabled reports whether it is on. Once enrolled, every administrative route asks for the code: a COM client authenticates with Application.AuthenticateWithCode, and the REST API wants the six digits in an X-hMailServer-OTP header. That closes the gap where a script could bypass a check the GUI was making on its own. Since 6.3.4 a code the server has accepted is refused if it comes again, over COM and REST alike, so a script that sent the password and a code with every REST request signs in once instead (POST /api/v1/session, then the hmailsession cookie) or uses an API key; otherwise only its first request in each half minute is accepted (chapter 24). The secret itself lives in [Security] AdministratorTotpSecret in hMailServer.INI; deleting that line with an elevated editor and restarting the service is how you recover a locked-out administrator credential. Under NT SERVICE\hMailServer, a new 6.3.4 installation’s account, the file is read-only to the service, so enrolling or removing the second factor over COM, REST or the Control Panel is refused (409 over REST): move the service to LocalSystem for the enrolment and back (§17.3).

Since 6.3.4 the administrator can also enrol a passkey for the Control Deck, which stands in for the one-time code there, and administration can be delegated by role rather than by sharing the administrator password: four built-in roles, serveradmin, domainadmin, helpdesk (find an account, reset its password, unlock it, read where its mail went) and auditor (read everything, change nothing), and roles of your own, assigned to accounts in named domains on Accounts & domains → Administrator roles. An account with no assignment keeps exactly the authority its admin level always gave it.

17.5 Rate limiting

MaxSubmissionsPerIPPerMinute=30        ; cap MAIL FROM per source IP per minute
MaxOutboundPerDestinationPerMinute=60  ; cap outbound per destination domain per minute

The first refuses excess submissions with 421 — it contains a compromised account. The second defers rather than bounces, which keeps you the right side of large providers’ rate limits. Both are 0, off, until you set them; the values above are a starting point, not the defaults.

Tarpits arrived in 6.2.25 and account lockout in 6.2.24, all off by default. The tarpits are implemented as a pause on the connection’s own timer, never a sleeping thread — which is why the two old COM properties of the same name were stubs for years.

LogonTarpitSeconds=0          ; delay before a failed logon is refused: this × the failures so far on the connection, capped at 30 s
SmtpTarpitCount=0             ; recipients an unauthenticated SMTP session may name before...
SmtpTarpitDelaySeconds=0      ; ...each further RCPT TO waits this long, capped at 30 s
Pop3LoginDelaySeconds=0
AccountLockoutThreshold=0     ; failed logons for one user name before that name is locked (0 = never)
AccountLockoutWindowMinutes=30
AccountLockoutMinutes=30

A password policy, applied when a password is set:

PasswordPolicyMinimumLength=0
PasswordPolicyRequireMixedCase=0
PasswordPolicyRequireDigit=0
PasswordPolicyRequireNonAlphanumeric=0
PasswordPolicyRejectCommon=0
PasswordPolicyHistoryCount=0      ; refuse re-use of the last N (needs schema 6019)
PasswordPolicyMaximumAgeDays=0

And if you run event scripts, ScriptAllowedObjects restricts which COM classes a script may create. The default is *, which is what every earlier version did; set it to a list and CreateObject of anything else fails inside the script with error 429. At * a script can create WScript.Shell and run any command the service’s account may, which is what let a script injected through CVE-2026-100741 run commands on the machine (a narrower list only limits what it can reach): in every release from 6.0.0 to 6.3.3 a value handed to a JScript server’s OnClientValidatePassword, OnExternalAccountDownload or OnDeliveryFailed handler could carry script past the escaping, and a rule’s Run function action wrote its function name into the call unescaped, in either language. A Windows server with event scripting on should be on 6.3.4 or later (§28.2, chapter 25). The key is about COM, which Linux does not have: event scripts there run in JavaScript since 6.3.4, with no file, process or network access (§28.2), so this key has no effect on Linux.

More: IP ranges, auto-ban, lockout, tarpits, sending limits, send-as and password storage on the forum.

17.6 SRS and BATV

If you forward mail, enable SRS, which rewrites the envelope sender when you forward so that forwarded mail still passes SPF at the next hop:

SRSEnabled=1
SRSSecret=<a long random string you never change>

SRSSecret must be stable. Changing it invalidates outstanding SRS addresses, which remain valid for 21 days — so bounces for recently forwarded mail will be lost.

BATV (prvs) signs your envelope sender so you can recognise and drop backscatter. It needs BATVEnabled=1 and a BATVSecret of its own, a different long random string: with the secret empty the switch reads enabled and nothing is signed or checked. SRS and BATV are both on Certificates & encryption → Transport security, card Forwarded mail & bounce protection (SRS / BATV).

17.7 Advanced hardening

Maintenance → Server limits & expert settings (titled Advanced hardening in the build shown below) collects the remaining controls — DPAPI secret protection, connection limits, message-store fsync, and consistency checking — and the Windows service account card, whose three buttons since 6.3.4 grant the account its folders and move the service between accounts (§17.3). Its [Settings] rows apply when they are saved, not at a service restart as that build said.

Advanced hardening
Advanced hardening

Since 6.3.4 each account’s second factor and the [Settings] secrets (SRSSecret, BATVSecret and the OAuth2 and metrics credentials among them) are sealed under the installation’s stored-secrets key once it has one; ProtectStoredSecretsWithDPAPI=0 keeps them as written. The Windows installer makes the key on a new installation. An upgraded installation, and the Linux packages, seal nothing until an administrator runs hMailServer.exe --create-stored-secrets-key from an elevated prompt (sudo hmailserver --create-stored-secrets-key on Linux), which the setup checklist offers, and restarts the service. Export the key at once with --export-stored-secrets-key FILE and keep the file with the database backups but stored apart from them: a database-level backup (a dump, or a copy of the database file) restored without it opens no second factor and no [Settings] secret.

More: Hardening a public mail server: secure defaults, checklist, sealed secrets and roles on the forum.


Part IIIMaintenance

18. Upgrading hMailServer

18.1 How upgrades work

hMailServer upgrades in place. The installer stops the service, replaces the program files, upgrades the database schema if needed, and restarts. Your mail, accounts, domains and settings are preserved.

Two things change during an upgrade:

  1. Program files in C:\Program Files\hMailServer\Bin are replaced. On Linux the package manager replaces /usr/bin/hmailserver and /usr/share/hmailserver, and the post-install step runs hmailserver --upgrade-database (chapter 35).
  2. The database schema is upgraded by DBUpdater, which the installer runs automatically. Current schema version: 6108 (6.3.6 to 6.3.8; 6105 in 6.3.5, 6104 in 6.3.4, 6040 in 6.3.3, 6038 in 6.3.2, 6031 in 6.2.27 to 6.3.1, 6030 in 6.2.25 and 6.2.26, 6025 in 6.2.24, 6011 in 6.2.19 to 6.2.21, 6005 in 6.2.10 to 6.2.18, and 6001 to 6005 in the 6.0 to 6.2 releases before them).

Your data directory (the messages) is left alone. From 6.3.4 the [Settings] section of hMailServer.INI is taken into the database at the first start and is the database’s copy from then on (§18.5), and an administrator password still stored in [Security] as MD5 or salted SHA-256 is written again as PBKDF2 at the first successful administrator sign-in (§18.5).

18.2 The upgrade path

The database upgrade chain is continuous from every earlier release of this fork, and of the original hMailServer up to 5.6, on MySQL, MS SQL, PostgreSQL and SQL CE; on MySQL and MariaDB, 6.3.4 and later need MySQL 5.7.9 or MariaDB 10.2.2 or later and the ALTER privilege on the database (below). You do not need to step through intermediate versions:

  5.3.x ┐
  5.4.x ┤
  5.5.x ┼──▶  6.3.8    (DBUpdater walks every intermediate schema step for you)
  5.6.x ┤
  6.0.x ┤
  6.1.x ┤
  6.2.x ┤
  6.3.x ┘

Not the original’s 5.7 builds of August and September 2026. They went on from schema 5708 by steps of their own (5709 to 5711), which this fork’s chain does not have; the table below says what to do instead.

Coming from Path Notes
6.3.7 The live update, or run the 6.3.8 installer No schema step: 6.3.8 needs 6108, as 6.3.7 does, so the upgrade changes no table. The live update keeps 6.3.7’s own file as its rollback image (§18.6); a service that runs as NT SERVICE\hMailServer downloads and verifies it, and an administrator installs it with Install update in the Control Panel on the server, with one Windows prompt. On Linux 6.3.7’s own update helper applies it to the generic .deb and .rpm and to both AppImages; a server installed from one of 6.3.7’s per-release packages is upgraded with its package manager and its release’s own package of 6.3.8 (§35.13). A server whose UpdateSigningIdentity, UpdateSigningIssuer and UpdateSourceRepository still name the project’s former path refuses 6.3.8: clear the three first (below)
6.3.6 The live update, or run the 6.3.8 installer No schema step: 6.3.8 needs 6108, as 6.3.6 does, so the upgrade changes no table. The live update keeps 6.3.6’s own file as its rollback image (§18.6); a service that runs as NT SERVICE\hMailServer downloads and verifies it, and an administrator installs it with Install update in the Control Panel on the server, with one Windows prompt. On Linux 6.3.6’s own update helper applies it to the generic .deb and .rpm and to both AppImages; a server installed from one of 6.3.6’s per-release packages is upgraded with its package manager and its release’s own package of 6.3.8 (§35.13). A server whose UpdateSigningIdentity, UpdateSigningIssuer and UpdateSourceRepository still name the project’s former path refuses 6.3.8: clear the three first (below)
6.3.5 The live update, or run the 6.3.8 installer A 6.3.5 database is at 6105 and is walked forward to 6108 in three one-way steps (below): 6106 changes no column, 6107 finishes the greylisting key and deletes the rows in which an address had been cut to 200 characters, and 6108 adds the never-ban list by reverse name and each SMTP listener’s choice of what it does with AUTH, On for every port it finds. The live update keeps 6.3.5’s own file as its rollback image, which is no way back once these steps have run (§18.6); a service that runs as NT SERVICE\hMailServer verifies the download and leaves the install to an administrator, and on Linux a backup destination inside /var/lib/hmailserver stops the unattended apply until it is moved (§35.13). A server whose UpdateSigningIdentity, UpdateSigningIssuer and UpdateSourceRepository still name the project’s former path refuses 6.3.8: clear the three first (below)
6.3.4 The live update, or run the 6.3.8 installer A 6.3.4 database is at 6104 and is walked forward to 6108 in four steps: 6.3.5’s one, which adds a column to the quarantine’s table, then 6.3.6’s three. The live update installs 6.3.8 without a rollback image, since 6.3.4 carries no Sigstore bundle (§18.6); otherwise as from 6.3.5
6.3.0 – 6.3.3 Run the 6.3.8 installer, by hand A 6.3.3 database is at 6040 and is walked forward to 6108 in sixty-eight steps, a 6.3.2 one (6038) in seventy, and a 6.3.0 or 6.3.1 one (6031) in seventy-seven; on MySQL and MariaDB step 6102 rebuilds every table (below)
6.2.x Run the 6.3.8 installer Drop-in. A 6.2.27 or 6.2.28 database is at 6031 and is walked forward to 6108 — 6.3.2’s seven steps, 6.3.3’s two, 6.3.4’s sixty-four, 6.3.5’s one and 6.3.6’s three, in place; any earlier 6.2 is walked all the way, automatically, as always
6.0 / 6.1 Run the 6.3.8 installer Schema upgraded automatically
5.6 Run the 6.3.8 installer Schema upgraded automatically, as is a database of the original’s 5.7 development builds up to schema 5708. Read §18.5 — the GUI has changed
5.3 – 5.5 Run the 6.3.8 installer As above. Very old installs: back up first and test the restore
The original project’s releases up to 5.6 Run the 6.3.8 installer This fork extended the original’s schema from 5708 by its own steps, so a database of the original up to 5.6 (5601) is walked forward like this fork’s own
The original’s 5.7 builds of August and September 2026 (5.7.0, 5.7.1, 5.7.2) Not the installer: move the mail over IMAP The original went on from 5708 by steps of its own (5709 to 5711), which this fork’s chain does not have. The installers of 6.3.7 and earlier take such an installation for their own and replace its programs, so never run one of them over it; 6.3.8’s setup refuses it before it changes anything. Set this server up beside it and move the mail over IMAP (Migration.md)

6.3.0 and 6.3.1 made no schema change — a 6.2.28 database was already at 6031 and their installers found nothing to do. 6.3.2 moved the schema to 6038 in seven steps (contacts, account preferences, scheduled sends and snoozes, files sent as links, message keywords, S/MIME keys, SURBL expected results), and 6.3.3 needs 6040: 6039 widens a domain’s relay-password column so a DPAPI envelope fits, and 6040 adds the CardDAV tables. Every step upgrades in place and DBUpdater runs whichever ones your database still needs. Nothing 6.3.0 added is on by default: the REST API, the Control Deck and the portal all need RestApiPort set, and up to 6.3.3 a server upgraded without touching its settings behaved as 6.2.28 did, apart from the delivery fix and the rebuilt webmail that 6.3.3 brought to everyone. An upgrade to 6.3.4 changes more (below). 6.3.1’s Windows installer was the first this project Authenticode-signed (§4.1).

6.3.4 needs 6104, sixty-four steps from 6.3.3’s 6040. Two of them take time on a large database (below), and 6.3.4 is the release that changes some things without a switch: the settings move into the database, and §18.5 lists the rest. The last step, 6104, lets the server record who made each fetch account from then on, so that one a person makes in the webmail is fetched only from a public address, unless ReaderFetchPrivateHosts (off by default) is on, and one made without the server’s authority is never given the server’s own OAuth2 token.

6.3.5 needs 6105, one step from 6.3.4’s 6104, on all four backends: it adds hm_quarantine.quarantinehelo, the HELO name a held message arrived with, which stays empty for every message held before the upgrade. From 6.3.3 or older the upgrade runs 6.3.4’s sixty-four steps first, and what this section says about them applies. What 6.3.5 changes without a switch is in §18.5.

6.3.6 needs 6108, three steps from 6.3.5’s 6105, on all four backends, each one way, so take the backup first. 6106 changes no column: it gives every external account’s days to keep messages the one meaning both fetchers now read (-1 deletes a message on the remote server once it has been collected, 0 leaves every message there, n deletes it n days after collection), translating what is stored so that every account goes on doing what it did or stops deleting, and none starts (§18.5). 6107 finishes the greylisting key that schema 6072 began: it keys every triplet written before 6072 in the server, deletes the rows in which an address had been cut to the old columns’ 200 characters, so such a sender is greylisted once more, and makes the key unique; on a large hm_greylisting_triplets the keying runs before the step’s transaction, committed as it goes, so an upgrade stopped part-way continues where it stopped. 6108 adds the never-ban list by reverse name and, for each SMTP listener, what it does with AUTH, written On for every port an upgrade finds, so no listener answers differently on the morning of the upgrade. From 6.3.4 the upgrade runs 6.3.5’s step first, and from 6.3.3 or older 6.3.4’s sixty-four, and what this section says about them applies. What 6.3.6 changes without a switch is in §18.5.

6.3.7 needs 6108 too, as 6.3.6 does: it adds no schema step, so an upgrade from 6.3.6 changes no table, and 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 steps first, and what this section says about them applies. What 6.3.7 changes without a switch is in its section of the CHANGELOG (§18.5).

6.3.8 needs 6108 too, as 6.3.6 and 6.3.7 do: it adds no schema step, so an upgrade from either changes no table, and the live update’s rollback, should 6.3.8 not start, meets no newer schema. What 6.3.8 changes without a switch is in its section of the CHANGELOG (§18.5).

Upgrading from anything older than 6.3.1? Go straight to 6.3.8. 6.3.2 could not upgrade a database whose schema was older than its own: its guard for a refused database covered the whole Settings object, which DBUpdater reads before it runs the first script, so every upgrade from 5.x or 6.2.x stopped with The server has not loaded its configuration and left the database untouched. 6.3.3 and later upgrade such a database in one go, and nothing in a database was changed by a failed 6.3.2 attempt.

On 6.3.1 or 6.3.2 and using the Control Panel’s live update? The first real update, 6.3.1 to 6.3.2, failed with the installer’s exit code 5 because the Control Panel that started it kept its own files open. 6.3.3 fixes that, but the helper that runs a live update is the one already installed, so update by hand once: close the Control Panel, run the installer.

A 6.3.4, 6.3.5, 6.3.6 or 6.3.7 server can install 6.3.8 by its live update. 6.3.8 is signed with Sigstore by the project’s GitLab pipeline at its path in the group hmailserver, which every server from 6.3.4 on trusts (§4.1). On Windows, a service that runs as LocalSystem installs it itself; one that runs as NT SERVICE\hMailServer, as a new installation from 6.3.4 on does, downloads and verifies it, reports HM7463 and leaves the install to an administrator: on 6.3.6 or 6.3.7, Install update in the Control Panel on the server installs it with one Windows prompt; on 6.3.4 or 6.3.5 the administrator runs the installer from the release, not the copy the server downloaded, once, and from then on Install update installs the next release. A 6.3.7 or 6.3.6 server keeps its own file as its rollback image, and a 6.3.5 server 6.3.5’s, which is no way back once the upgrade’s schema steps have run, since 6.3.5 refuses a database they have moved to 6108 (§18.6); there is none for 6.3.4, which carries no Sigstore bundle. From 6.3.5 or 6.3.4 the backup taken before the upgrade is the way back, so take it first. The update is applied by the version already installed, so two things of 6.3.4 and 6.3.5 still hold for it: on Linux a backup destination inside /var/lib/hmailserver fails the backup before an unattended apply (HM5014), and the apply is refused until the destination is moved; and a server whose UpdateSigningIdentity, UpdateSigningIssuer and UpdateSourceRepository still name the project’s former path, set so that an older server could install 6.3.5, trusts that path alone and refuses 6.3.8. 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 6.3.4 and 6.3.5 put back; on 6.3.6 or 6.3.7, which read 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. On Linux, install 6.3.8 on 6.3.4 or 6.3.5 with the package manager rather than by the live update: the update is applied by the apply step of the version it replaces, which is among what 6.3.6’s security fixes replace (chapter 25), and until a server’s helper is that of 6.3.6 or later, systemctl disable --now hmailserver-update.path turns the apply off while the check and the download go on. A server from 6.2.28 to 6.3.3 trusts only the GitHub signer and is upgraded by hand: run hMailServer-6.3.8-x64.exe; on Linux, install the package with apt or dnf, as for one installed from 6.3.3’s AArch64 packages or AppImages, which 6.3.6 to 6.3.8 carry again (§35.2). A server from 6.2.28 to 6.3.3 also needs one line before its update check works again (§18.7).

Four steps in the 6011 → 6031 chain need a maintenance window on a large database. 6012 → 6013 and 6014 → 6015 each add a column to hm_messages and then update every message row to fill it, so they take longer the larger the store. 6024 → 6025 rewrites hm_messages.messageflags from tinyint to smallint — a table rewrite on your largest table, holding locks, on MS SQL, SQL Server Compact and MySQL/MariaDB; PostgreSQL is unaffected. 6029 → 6030 adds seventeen FOREIGN KEY constraints with ON DELETE CASCADE, deleting the orphan rows they would otherwise refuse, and reads every child table once. Plan all four like an index build. After them, 6.3.2’s 6035 → 6036 adds a column with a default to hm_messages on all four backends; how long that takes depends on the backend and its version, so on a large store time it on a copy first.

Two steps in 6.3.4’s part of the chain take time too. 6101 adds two columns to hm_messages, the table with a row for every stored message: the account an authenticated session submitted a message as, and the domains that passed DMARC and SPF when it arrived. On a large store it is not instant on every backend. On MySQL and MariaDB, 6102 rebuilds every table as utf8mb4 under utf8mb4_general_ci, InnoDB and ROW_FORMAT=DYNAMIC, so that an emoji or a CJK Extension B character is stored instead of refused. It takes as long as copying the database, with free disk space of the order of the largest table; it needs MySQL 5.7.9 or MariaDB 10.2.2 or later, as 6041 and 6101 before it do, so on an older server the upgrade stops at 6041 with error 1071 and changes nothing; it needs the ALTER privilege on the database, not only on its tables; and two rows the old collation told apart and the new one does not stop it with error 1062, which names the index and the value — remove or rename one and run the upgrade again, since every statement of the step can run twice. SQL Server, SQL Server Compact and PostgreSQL only move the version at 6102. One more 6.3.4 step, 6072, widens the greylisting table’s two address columns and indexes it, which takes time only where that table is very large.

A known defect in the 6029 → 6030 step, unfixed at 6.3.1 and fixed in 6.3.2. That step’s orphan sweep runs children before parents. Three of the parent tables are themselves pruned by those deletes, and each one’s children are cleaned earlier, against a parent that has not yet lost its rows — so pruning an orphaned account, fetch account or distribution list re-orphans rows nothing revisits, and the foreign-key constraint that follows is refused.

The scope, stated fully. It fires only on an upgrade from a schema below 6030 on a database that already holds orphaned rows; a database without them upgrades correctly, and a fresh installation is never on this path. It affects all four database backends. When it does fire it fails loudly, in the database engine’s own words, and rolls back rather than doing anything quietly. No installation has reported hitting it. The fix is a reordering of seventeen delete statements; it shipped in 6.3.2, together with a gate that executes a schema upgrade on a seeded database on every push — which is why it could not ship earlier.

This chapter’s readers cross that step. A 6.2.19 to 6.2.21 installation sits at schema 6011, below 6030. Take the backup at §18.3 step 1 before you start. If the upgrade does stop here it has rolled back, so the database is where it was; the error names the constraint it could not add, and the orphaned rows behind it have to go before the upgrade will pass.

The upgrade is one-way: an older server refuses a newer database rather than misreading it. That is what makes §18.3 step 1 non-negotiable.

On Linux

A first installation is the package, the [Database] section of the configuration file and three commands; there is no wizard:

sudo apt install ./hmailserver_6.3.8_amd64.deb     # Ubuntu 26.04; any other release: its own .deb, or with dnf its own .rpm (§35.2)
sudoedit /etc/hmailserver/hMailServer.ini          # [Database]: Type, Server, Database, Username, Password, Port; and, before the first start, [Settings] RestApiPort and RestApiBindAddress
sudo hmailserver --set-admin-password              # as root: the file is root-owned, and this writes it
sudo -u hmailserver hmailserver --create-database
sudo systemctl enable --now hmailserver

A package upgrade replaces the binary, runs hmailserver --upgrade-database as the service user from its post-install step when a database is configured, and restarts the unit if it was running. A failed schema step is reported but does not fail the package, so read what the upgrade printed; sudo -u hmailserver hmailserver --upgrade-database is safe to run again by hand. Chapter 35 is the full walk-through.

Moving a Windows installation to Linux is not a supported migration, and the 6.3 releases do not claim it is. The database schema is the same on both and the message store’s on-disk format is the same on both, so the pieces are there — what is missing is a tested path. Treat a move as a new installation plus a mailbox-level migration (§33.8), not as a restore.

18.3 Before you upgrade

  1. Back up. Database, data directory and hMailServer.INI. See chapter 15.
  2. Write down two numbers. The server version, on the Control Panel’s About page (before 6.2, Help → About in the classic Administrator), and the schema version, which Monitoring & troubleshooting → Server status shows; when the server will not start, select value from hm_dbversion reads it. If you need support, those are the first two questions.
  3. Pick a quiet time. From 6.2.24 or later the service is down for a minute or two, except on a very large store, where 6036 and 6101 add columns to hm_messages; from 6.2.21 or earlier on a large store it is longer, and on MySQL or MariaDB step 6102 takes as long as copying the database (§18.2). Senders will retry, so nothing is lost, but users will notice.
  4. Coming from 6.3.3 or earlier, restart the service once after your last change to a [Settings] key. The first start of 6.3.4 or later takes a line changed after the old server last started — in the file, or on a 6.3.3 Control Panel running on the server, which writes the file directly — for an edit, reports it as HM5804 and puts it back. If HM5804 appears after the upgrade, set the named keys again in the Control Panel, or on Linux with hmailserver --set-setting.

18.4 Doing it

  1. Download hMailServer-6.3.8-x64.exe from updates.progressiverobot.com/download/hMailServer-x64.exe, which leads to the newest stable release’s installer, and check it (§4.1).
  2. Run it as administrator.
  3. The installer detects the existing installation and offers to upgrade. Accept.
  4. Keep your existing database settings when asked.
  5. On the Service account page (new in 6.3.4), keeping the account the service runs as is the default. The other choice moves a LocalSystem service to NT SERVICE\hMailServer, or a service under any other account back to LocalSystem (§17.3). A move is made after the database is upgraded, and the installer asks before it grants the service a folder or key file the configuration names outside the program folder. With SQL Server reached through Windows authentication, give the virtual account a login there before you move to it.
  6. Let it finish, then confirm the service is running and that the ERROR log (ERROR_hmailserver_<date>.log in the Logs folder) has nothing from the time of the upgrade; an empty ERROR log is the signal you want. HM5804 there names a [Settings] line that was put back (§18.3).
  7. Open the Control Panel and check About shows 6.3.8.
  8. Send a test message in and out.

From 6.3.4 a stop drains first. That serves the upgrades from 6.3.4 on, 6.3.4, 6.3.5, 6.3.6 or 6.3.7 to 6.3.8 among them; an upgrade from 6.3.3 or older stops the service as it always did. The SMTP, IMAP, POP3 and ManageSieve listeners close at the socket, so senders and mail clients retry, and the delivery queue pauses, while the webmail, CalDAV and CardDAV, the REST API and the metrics listener stay up and /readyz answers 503. ShutdownDrainSeconds is how long the stop then waits for what is in flight; at its default of 0 nothing is waited for, so set it on a production server (30 is a reasonable figure). Where something other than this installer replaces the binaries, UpgradeOnStart=1 under [Database] in hMailServer.ini (a file key, off by default) has the service apply the schema steps itself as it starts; it backs nothing up first, and belongs on one node at a time.

18.5 Things that changed, and what to do about them

Coming from 6.3.7, what changes on the upgrade to 6.3.8 without a switch is in 6.3.8’s section of the CHANGELOG, under What changes on upgrade without a switch, and its Before you upgrade names the changes most likely to need something done (chapter 25); coming from 6.3.6, 6.3.7’s section, below it, adds what 6.3.7 changed. From 6.3.5 or older, what follows applies as well.

Coming from 6.3.5, forty-six things change on the upgrade to 6.3.6 without a switch, listed first in 6.3.6’s section of the CHANGELOG and in chapter 25. Read before upgrading: mail can stop flowing if your resolver strips DNSSEC. With DaneEnforcementEnabled and DnssecValidationEnabled on, as they are by default, an MX or TLSA lookup that cannot tell — no answer, SERVFAIL, a signed zone’s answer without its signatures or 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; 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, is delivered to through a route. A host that the destination’s TLSA records or MTA-STS policy require TLS of, and that offers no STARTTLS, is deferred where the message bounced at the first attempt; 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.

Then what asks something of you. External accounts: days to keep messages means one thing for POP3 and IMAP from 6.3.6 (-1 deletes a message once collected, 0 keeps every message, n deletes it after n days; 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), and schema step 6106 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), and change a script that writes the value, or an OnExternalAccountDownload script that answers 1 for an IMAP account, to the one meaning (chapter 29, FetchAccount). Scripts outside the service that use COM, on a server with an administrator password: the eight helpers of Utilities that do work, and a Message or FetchAccount the program creates itself, answer only a program that has signed in on an Application it still holds; Attachments.Add and Attachment.SaveAs answer the server administrator alone; and mail a program signed in as an account queues must be from an address that account may send as (chapters 30 and 31). Windows file permissions: Setup runs hMailServer.exe /ProtectFolders at every installation and upgrade, whatever account the service runs as, so the Data, Logs, Temp, Database and Events folders and hMailServer.INI stop inheriting BUILTIN\Users’ Read and keep SYSTEM, the Administrators and the service (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 needs an entry of its own (icacls "<folder>" /grant "<account>:(OI)(CI)RX"), which later runs keep, and a Control Panel not run as administrator says Could not read the file on its live log page (§17.3). An editor that saves hMailServer.INI by replacing the file gives it Bin’s permissions again: run hMailServer.exe /ProtectFolders after it. The administrator’s commands: every command line of hMailServer.exe but the service’s own writes nothing to the error log or the Application log and says what it did on its own output, so a script that read the error log after /Register or --upgrade-database reads the output instead; the store commands 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; on Linux such a command run as root first becomes the service’s account. Installing by script: the installer 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, where 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, and COM and REST refuse a change to the five settings (chapter 21, Self-update), so a signer of your own goes in the file, in every server’s file where several share a database; and a server whose UpdateSigningIdentity, UpdateSigningIssuer and UpdateSourceRepository still name the project’s former path refuses 6.3.6 until the three are cleared (§18.2). Behind a reverse proxy: the REST listener’s plain loopback port answers only a request whose Host is localhost, an IP address, the server’s host name or the host of an address configured as the one the webmail is reached at from outside, and refuses any other with 421; a proxy that forwards the browser’s own Host needs that name set first, and must pass the Control Deck’s page through unchanged (§24.2). Port 25: every SMTP listener stays On and still takes AUTH, so nobody is banned on the morning of the upgrade; the setup checklist’s new item No password on port 25 says so until you move clients that sign in on 25 to 587 and set port 25 to Honeypot or Off, on which the first AUTH of a session bans the address (a new installation’s port 25 is a Honeypot). URI block lists: the SURBL test 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; a list’s error answers are never a listing, so a list of your own that answers 127.0.0.1 for a listing is made to answer another address, such as 127.0.0.2; and a sender list’s URL entry written as a reversed address is written as the address itself. ManageSieve (off by default) is held to deadlines, serves one connection per address before sign-in, and ends a signed-in session after ten idle lines in a row or two minutes of waiting in all. 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 — so a value kept in an older form (192.168.1, a leading zero read as octal, 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; review the certificates kept before the upgrade 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; check the retention of the accounts whose holders use such a plugin. The packaging Compose file runs PostgreSQL 18: a deployment that takes the new docker-compose.yml moves its PostgreSQL 16 data once, with a dump and a load. A monitor that matches “The response carried no HTTP headers.” needs the new sentence, which says who ended the connection.

The rest need nothing: the MX-record test and the SURBL test cut a host name by the Public Suffix List, so a sender at a public suffix itself with no MX (github.io, co.uk) now fails the MX-record test, and the SURBL test reads a link as a browser does; backups leave out the live update’s folder, and a destination inside the data directory works; tlds.txt is removed and error 4335 retired; the project’s address is gitlab.com/hmailserver/hmailserver; the live update keeps its outcome and a verified installer, and tries a failed check again once; on Linux the backup and event logs are UTF-8; more web requests go through HttpProxy when it is set; a calendar message delivered by mail changes a stored event only from a sender the server verified; 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; what the auto-ban put below the server is lifted after a stop too; the Control Panel’s text can be selected and copied; event scripts on Linux run in QuickJS 2026-06-04; and scripting off runs no script engine.

Coming from 6.3.4, five things change on the upgrade to 6.3.5 without a switch, listed first in 6.3.5’s section of the CHANGELOG and in chapter 25: the Control Panel’s sidebar is in a new order, with a new Set up this server group and two groups renamed (§5.3); the quarantine shows the HELO name and groups by the sender’s domain; the webmail’s message list is one Tab stop, and Enter, Escape and the formatted signature’s keys do what they say; an invitation sent the way Outlook sends one is shown as one; and GET /api/v1/quarantine gives three more fields, with new members over COM. A UI test that finds a sidebar group by its old navgroup- or navrail- id must be changed.

Coming from 6.3.3 or earlier, twenty-four things change on the upgrade to 6.3.4 without a switch. The full list is the first part of 6.3.4’s section in the CHANGELOG; these are the ones an administrator meets first:

  • The database is the settings store, and [Settings] in hMailServer.ini is its copy (chapter 21). The section is taken into the database at the first start. A line edited after the old server last started, including one edited to prepare the upgrade, is taken for an edit and put back (HM5804 in the error log), so make such a change after upgrading. From then on change settings in the Control Panel, over COM or REST, or with hmailserver --set-setting on Linux — not in the file, where an edited line is put back and deleting a line no longer returns a setting to its default.
  • Most saved settings take effect at once: of the file’s keys, 211 at once, 28 at a reinitialise and 2 at a restart, and every interface lists the ones still waiting.
  • SPF is RFC 7208. 127.0.0.0/8 and ::1 no longer pass before any policy is read, so a content filter that re-injects mail should authenticate, or connect from an IP range with anti-spam off.
  • The audit trail and alerts are on. Nothing is mailed until AlertRecipient and AlertSenderAddress are set. Compromised-account detection starts in watch mode and holds nothing until it is set to enforce.
  • A destination that asks the server to slow down is paused for OutboundBackoffMinutes (2, doubling to OutboundBackoffMaxMinutes, 60); 0 turns it off.
  • IMAP answers as the RFCs say where Dovecot’s imaptest found it did not: a mailbox that does not exist is refused NO rather than BAD, every body fetch without .PEEK sets \Seen, SORT and THREAD order messages as the RFCs do, and an APPEND stores the literal byte for byte.
  • The database connection is encrypted whenever the database server offers TLS: MySQL and MariaDB without checking the certificate, SQL Server through MSOLEDBSQL 18 or SQL Native Client for the whole session, and through SQLOLEDB only the login unless TlsMode is require or verify-full; PostgreSQL is unchanged ([Database] TlsMode, chapter 21).
  • The administrator password is hashed again at the first successful administrator sign-in; a browser session made under the old hash ends once. The installer now writes it as PBKDF2 itself (§4.2).
  • A password stored before 6.3.4 with a character above U+00FF is refused, and the application log names the account: its hash was made over NUL bytes on Windows or question marks on Linux, so any other password of that form opened it. Set such a password again, and soon. ASCII and Latin-1 passwords sign in as before.
  • A one-time code is taken once. A script that sends the administrator’s password and a code with every REST request should sign in once (POST /api/v1/session) or use an API key; otherwise only its first request in each half minute is accepted.
  • The outbound relay sends its OAuth2 token over TLS only. A route or the SMTP relay whose host is on OutboundOAuth2Hosts and whose connection security is none fails its deliveries with a temporary 450 4.7.0 and HM5912 until it is set to TLS or STARTTLS required.
  • A domain named like one of the server’s own directories — protected, quarantine, files and the rest — is refused for a new domain or a rename; one that already exists is named as HM7597 at every start: rename it.
  • A COM change to a route, an incoming relay, a DNS black list, a certificate or another shared configuration object takes effect at Save(). A script that set one and relied on it without saving must now save.
  • A rule’s Run function action names a plain function. A name that is not ASCII letters, digits, _ and $, not starting with a digit, is refused as HM5018 each time the rule fires, in VBScript and JScript alike; on Linux such a name runs nothing and the ERROR log has a Script Error line. On a JScript server every value a handler is given now arrives exactly as it was, backslashes included.
  • A restore lists the archive before it extracts anything and refuses an unsafe one whole; mail labelled ISO-8859-1 is decoded as windows-1252.
  • The service account. An upgrade keeps the account the service runs as; only a new installation defaults to NT SERVICE\hMailServer (§17.3).
  • The listeners and the secrets. Only a new installation is given a self-signed certificate, STARTTLS on 25, 587, 110 and 143 and TLS required for authentication from the Internet IP range, and, from the Windows installer, a stored-secrets key. An upgrade keeps its listeners as they were, and nothing is sealed until an administrator runs --create-stored-secrets-key, which the setup checklist offers. Export the key at once with --export-stored-secrets-key, import it on every other server that shares the database, restart each server, since the next start seals what is stored as written, and keep the file with the database backups but apart from them: a database backup restored without it opens none of the sealed secrets.

Coming from 5.x or early 6.x, the administration GUI is different.

The classic hMailServer Administrator (hMailAdmin.exe) was retired in 6.2 and its source removed in 6.2.10. It is replaced by the Control Panel (hMailCP.exe), which does everything the old tool did — domains, accounts, aliases, distribution lists, routes, rules, IP ranges, TCP/IP ports and SSL bindings, server settings, status, queue, logs, backup, certificates, scripts and public folders — plus a live dashboard, complete settings coverage and optional two-factor authentication.

  • The navigation no longer mirrors the old Administrator’s tree: its pages sit in eight task-based groups (chapter 20). A path from old notes still finds its page in the Ctrl+K palette, so Settings > Advanced > IP ranges opens Sign-in & access → IP ranges.
  • Your TOTP secret carries over — the Control Panel uses the same one.
  • The Control Panel needs the .NET 10 Desktop Runtime, which the installer installs silently if missing.
  • If that runtime install fails, you have no mail server, not just no GUI. DBSetup, DBSetupQuick and DBUpdater are .NET applications too, the upgrade has to move the schema, and the server refuses to start against an old one. Recovery: install the .NET 10 Desktop Runtime (x64) by hand from https://dotnet.microsoft.com/download/dotnet/10.0, run DBUpdater.exe from the installation’s Bin folder, then start the service. Since 6.3.4 hMailServer.exe --upgrade-database, run from Bin with the service stopped, walks the same chain without .NET or the administrator password, and exits 0 when it is done.

The PHP WebAdmin is gone. It stored the administrator password in plaintext in a PHP session and needed DCOM opened up for the web server account. It was 2008-era code. If you used it to administer the server remotely, use the Control Panel instead — it connects to a remote host directly, which is both simpler and safer.

“Administrative tools” means something narrower now. With both old front-ends gone, the component’s only job is registering the COM type library so scripts on another machine can administer this server. If you do not script remotely, you do not need it.

18.6 Rolling back

If an upgrade goes wrong:

  1. Uninstall 6.3.8.
  2. Restore the database from your backup — this is the essential step after an upgrade from 6.3.5 or older, because the schema was upgraded and an older server will refuse a newer schema; an upgrade from 6.3.6 or 6.3.7 changed no table, so either meets no newer schema (§18.2).
  3. Going back to 6.3.3 or older, put the pre-upgrade hMailServer.INI back beside it if the newer version had sealed the stored secrets: the older version’s installer would otherwise hand Windows the sealed service-account password, and the service would not start. Without such a copy, set ServiceAccountPassword (and any other of those secrets you use) again with the older version’s Control Panel once it runs, and run hMailServer.exe /Register again. An older build ignores the [SettingsStore] and [SettingsOverride] sections, so move a value only [SettingsOverride] supplied into [Settings].
  4. Install your previous version.

The data directory does not normally need restoring: an upgrade does not rewrite message files. Mail that arrived after the backup is on disk with no row in the restored database, and is not seen.

This is why §18.3 step 1 is not optional. Without a database backup there is no rollback from an upgrade that ran schema steps, as every upgrade from 6.3.5 or older does.

A live update from 6.3.4 has no automatic rollback. Before it installs, the updater fetches the installer of the version that is running and verifies it against its Sigstore bundle, so that it can put that version back if the new one does not start. 6.3.4 has no bundle, so a 6.3.4 server installs 6.3.5, 6.3.6, 6.3.7 or 6.3.8 without one: the application log says no rollback image for hMailServer 6.3.4 could be had, and if the new version does not start, the update’s outcome says no rollback image was available, and the way back is the list above, with 6.3.4’s installer run by hand. 6.3.5, 6.3.6 and 6.3.7 carry a bundle, so a live update from any of them to 6.3.8 fetches the running version’s as its rollback image. From 6.3.5 that image is no way back once the upgrade’s schema steps have run: 6.3.5’s updater runs it whenever the service is not running again in time, without looking at the database, and 6.3.5 refuses a database the steps have moved to 6108, so the backup taken before the upgrade is the way back (the list above).

More: Upgrading from 5.x, 6.2 or 6.3: the 24 changes without a switch, and rolling back on the forum.

18.7 Staying up to date

  • Watch the downloads page: every release is published there with its notes.
  • Or let the server watch. Since 6.2.28 it can check for releases itself. Nothing happens at all until you turn it on — no request, no identifier, no counts: UpdateCheckEnabled=0 is the default. Switch it on from the Updates card of Monitoring & troubleshooting → API & monitoring (since 6.3.4 a save there applies at once) and the release feed is read at startup and every UpdateCheckHours (24) and reported on Monitoring & troubleshooting → Server status. Fetching (UpdateAutoDownload) and applying are two further opt-in steps, and UpdateBackupBeforeApply=1 means an unattended apply does not happen without a configured backup destination and a backup that succeeded.
  • What is applied is verified against the release’s Sigstore bundle — chain to Fulcio, this project’s release-workflow identity and issuer, the signature over the file, and inclusion in the transparency log — and that check cannot be turned off. If the service does not come back within UpdateServiceWaitSeconds (180), hMailServer.Updater.exe reinstalls the previous version, when it could fetch and verify that version’s installer before the install; on 6.3.4, which has no bundle, it cannot (§18.6).
  • 6.3.4 is the exception: it carries no Sigstore bundle, so no live update installs it — a server of any version that is offered it says it cannot be verified and downloads nothing. Install it by hand (§18.4). A server from 6.2.28 to 6.3.3 still looks for releases at the project’s former GitHub repository, github.com/Progressiverobot/hmailserver, which has answered 404 since 18 September 2026: add UpdateFeedUrl=https://updates.progressiverobot.com/releases.json under [Settings] in its hMailServer.ini and restart the service, and its check works again. 6.3.4 reads that feed by itself, and trusts both GitHub’s signer, for the releases up to 6.3.3, and this project’s GitLab pipeline, which signs the releases from 6.3.5 on: 6.3.5 under the project’s former path, https://gitlab.com/Progressiverobot/hmailserver//.gitlab-ci.yml@refs/tags/v6.3.5, and the releases from 6.3.6 under the group path the project moved to on 29 September 2026, https://gitlab.com/hmailserver/hmailserver//.gitlab-ci.yml@refs/tags/v<version>; a server from 6.3.4 on trusts both paths. A server left on 6.3.3 or older trusts only GitHub’s, so its live update refuses every release from 6.3.5 on: install the current release by hand, once, and its live update takes the releases after that; until then keep UpdateAutoDownload off on such a server. Use the UpdateFeedUrl value exactly: without .json it names a directory and the check reports The update feed answered HTTP 302. On 6.3.4 and later the line can go, though leaving it changes nothing: clear it in the Control Panel, with DELETE /api/v1/settings/ini/UpdateFeedUrl, or on Linux with hmailserver --set-setting UpdateFeedUrl=, not in the file, where a changed line is put back (HM5804).
  • Under NT SERVICE\hMailServer, the account a new 6.3.4 installation runs as, the apply is refused with a sentence: the installer takes an administrator, who downloads it from the release page, checks its Authenticode signature and runs it (§17.3).
  • Authenticode is an extra check, and from 6.3.1 it can pass. UpdateRequireAuthenticode=1 makes the server demand a Windows-trusted Authenticode signature on the downloaded installer as well as the Sigstore bundle. It has existed since 6.2.28 and, until 6.3.1, refused every release of this project, because none carried one. It is off by default, and it is Windows-only: on Linux there is no Authenticode to check, and the server reports that rather than passing. Up to 6.3.3 updating a Linux server is the package manager’s job; from 6.3.4 its live update applies the package through a root helper (§35.13).
  • Every release lists exactly what changed and why, and carries SBOMs. Release assets are signed with Sigstore from 6.2.19 to 6.3.3 and again from 6.3.5, by the project’s GitLab pipeline; 6.3.4 carries an Authenticode-signed installer, an SBOM beside each installer and package, and SHA-256 sums signed with the release tags’ SSH key (§4.1), and no bundle.
  • Security fixes are called out explicitly in the release notes.
  • Read the roadmap — Roadmap.md in the repository states what is planned, what is deliberately refused, and why.

More: The update check and the live update: feed, verification, window and rollback on the forum.


19. Troubleshooting

19.1 Start here, always

  1. Monitoring & troubleshooting → Diagnostics — the built-in self-test.
  2. Monitoring & troubleshooting → Live logs — watch what happens as you reproduce the problem. It reads the log folder, so it works only in a Control Panel running on the server itself.
  3. Monitoring & troubleshooting → Delivery queue — is mail stuck, and what is the error?

Chapter 32 is the companion lookup reference: what individual database, DNS and SMTP error messages mean, and what to do about each.

19.2 “I can’t receive mail from outside”

Work through in order:

Check How
Is the service running? Get-Service hMailServer
Is port 25 open on Windows Firewall? §4.4
Is port 25 forwarded by your router/cloud firewall? Your firewall’s own configuration
Does your ISP block port 25? Ask them. Very common on consumer lines
Is your MX record right? Monitoring & troubleshooting → MX query, or mxtoolbox
Can the world reach you? From outside your network: Test-NetConnection mail.yourcompany.com -Port 25
Is the domain in hMailServer and Active? Accounts & domains → Domains

19.3 “I can’t send mail”

Symptom Likely cause
Client says “relay denied” Client is not authenticating, or is using port 25 instead of 587
Mail sits in the queue Outbound port 25 blocked, or DNS resolution failing. Use a smart host (§14.1)
Remote server rejects you Missing PTR, missing SPF, or your IP is blacklisted
Mail to one destination waits in the queue, and the log says Delivery deferred by this server's own shaping, not by a remote server. Since 6.3.4, a destination that answers with a 421, or with one of the providers’ rate-limit replies, is given a pause: deliveries to it alone wait for OutboundBackoffMinutes (2 by default, doubling to OutboundBackoffMaxMinutes, 60), and a wait this server chose does not spend the message’s retries until the message has been queued for longer than its retry schedule spans, so a destination that never stops asking still ends in a bounce. Through a smart host, the relay is that one destination. It is on after an upgrade; OutboundBackoffMinutes set to 0 turns it off
A new 6.3.4 installation refuses a client’s sign-in from another machine: 530 A SSL/TLS-connection is required for authentication. The secure defaults of a new installation: the Internet IP range requires TLS for authentication, and ports 25, 587, 110 and 143 offer STARTTLS with a self-signed certificate. Set the client to STARTTLS on 587, and do not clear the range’s requirement to suit an old client. Clients warn about the self-signed certificate until you replace it with an ACME or your own certificate (chapter 9). This computer’s own clients, and an upgraded server, are not affected

More: Troubleshooting by symptom: no mail in, no mail out, a service that will not start on the forum.

19.4 “My mail goes to spam”

Almost always DNS. In order of impact:

  1. PTR record — ask your ISP. Missing PTR is the single biggest cause.
  2. SPF — publish it, and make sure it lists your actual sending server.
  3. DKIM — generate the key on the DKIM tab of the domain’s Properties (Accounts & domains → Domains), which prints the exact TXT record to publish; Set up this server → DNS records checks it against live DNS.
  4. DMARC — publish it, even as p=none.
  5. Blacklists — check your IP at mxtoolbox. New IPs sometimes arrive pre-tainted; most lists have a delisting form.
  6. Test it — https://www.mail-tester.com. Fix everything it flags.

19.5 “The Control Panel won’t connect”

Check Notes
Is the service running? The Control Panel talks to the running service
Right password? The administration password, not a mailbox password. If it is lost, set a new one on the server: hMailServer.exe --set-admin-password from an elevated prompt, with the password in HM_ADMIN_PASSWORD or on standard input and never as an argument (a pipe from Windows PowerShell 5.1 carries only ASCII and turns every other character into ?, so use the variable for a password with any other character), or sudo hmailserver --set-admin-password on Linux. The running service keeps the old one until it is restarted or reinitialised. After an upgrade to 6.3.4, an administrator password stored with a character above U+00FF is refused until it is set again, and the application log says so
Connecting remotely? The COM API must be reachable and DCOM permitted
A change of the administrator password is refused with a sentence about the file Since 6.3.4 a new installation runs the service as NT SERVICE\hMailServer, to which hMailServer.INI is read-only, so the Control Panel cannot write the password there. Set it with hMailServer.exe --set-admin-password from an elevated prompt, then restart the service. Enrolling the administrator’s second factor is refused the same way: move the service to LocalSystem for the enrolment and back
Just restarted the service? The Control Panel reconnects itself; give it a few seconds

19.6 “Database too old” / “Authentication failed” after install

Both were real defects in 6.2.4 and are fixed in 6.2.5 and later. If you see either on a fresh install, you are running an old build — upgrade to 6.3.8.

19.7 Where the files are

What Default location
Program C:\Program Files\hMailServer\Bin
Configuration C:\Program Files\hMailServer\Bin\hMailServer.INI
Messages C:\Program Files\hMailServer\Data
Logs C:\Program Files\hMailServer\Logs
Built-in database C:\Program Files\hMailServer\Database
ACME certificates C:\Program Files\hMailServer\Data\ACME

On Linux the packaged layout is:

What Default location
Program /usr/bin/hmailserver, with /usr/share/hmailserver (including DBScripts); since 6.3.4 also /usr/bin/hmctl, the command-line client over the REST API
Configuration /etc/hmailserver/hMailServer.ini (mode 0640, root:hmailserver)
Messages /var/lib/hmailserver
Logs /var/log/hmailserver
Temporary files /var/lib/hmailserver/temp
Event scripts (JavaScript, since 6.3.4) /var/lib/hmailserver/events/EventHandlers.js
Machine secret key /var/lib/hmailserver/.hmailserver-secret-key (mode 0600). Since 6.3.4 it also seals the stored-secrets key, once one has been made, which is kept in hMailServer.ini ([Security] StoredSecretsKey): back the two up together, or export the key with sudo hmailserver --export-stored-secrets-key FILE

The server looks for hMailServer.ini beside its own executable first and falls back to the packaged path; --config <file> overrides both. Chapter 35 covers the rest.

Since 6.3.4 the database is the store for the [Settings] keys, and the file’s [Settings] section is a copy of it. Change a setting in the Control Panel, over REST (PUT /api/v1/settings/ini/{name}) or COM (Settings.SetIniSetting), with hmctl ini set, or on Linux with sudo hmailserver --set-setting NAME=VALUE (the Control Deck edits only a few of these keys), not by editing the file: a line edited there is ignored in favour of the stored value, reported in the error log as HM5804 and, where the service may write the file, put back. The file keeps its other sections, [Database], [Directories] and [Security] among them, and PasswordPepper, the one setting still set in it. A new 6.3.4 installation on Windows runs the service as NT SERVICE\hMailServer, to which hMailServer.INI is read-only.

19.8 Getting help

When reporting a problem, include your version (the Control Panel shows it; on Linux, hmailserver --version), the platform and the database backend, the relevant log extract and the ERROR_hmailserver_<date>.log for the same window, and what you have already checked. Remove passwords, keys and real email addresses from anything you paste: an issue is public. More: Read this before you post: what a support request needs on the forum.


Part IVReference

20. Every Control Panel page

This is the Windows desktop application, page by page. The Control Deck — the administration page the REST listener serves at / — covers a large part of the same ground in a browser, and is the only graphical administration a Linux server has; see chapter 36 for what it does. At 6.3.5 a Deck view reaches 498 of the 508 properties this program writes, and a few things remain this program’s alone, among them editing the event scripts, which are a file on the server’s disk, and the Active Directory pickers.

6.3.5 has 66 pages: the ten marked (new in 6.3.4) arrived in 6.3.4, and the Webmail page, marked (new in 6.3.5), in 6.3.5. 6.3.5 renamed one page (External setup is the Setup checklist) and two groups (TLS & certificates is Certificates & encryption, Access & abuse protection is Sign-in & access), moved the Setup checklist, Setting profiles and DNS records into a new group, Set up this server, and put each group’s pages in the order a new administrator needs them. The table follows the sidebar: Welcome and Dashboard, then the nine groups — Set up this server (3 pages), Accounts & domains (4), Mail flow & delivery (7), Connections & protocols (5), Certificates & encryption (5), Spam & virus filtering (14), Sign-in & access (6), Monitoring & troubleshooting (14) and Maintenance (5) — then About. A path from the old tree, such as “Settings > Maintenance > Performance”, still opens its page from the Ctrl+K palette; “Settings > Security > Auto-ban & SSL/TLS”, the one page split in two, opens Auto-ban. The old names External setup, TLS & certificates and Access & abuse protection are aliases the palette finds too. Since 6.3.4 an account that signs in with an administrator role sees only the pages its role may use (a help-desk role, Welcome and About); the server refuses the rest whatever a page shows.

Where it writes decides when a change applies. What the table calls COM is stored in the database. Since 6.3.4 so are the [Settings] keys (schema 6042), with the [Settings] section of hMailServer.INI kept as a copy: the Control Panel writes them over COM only, even on the server itself, and a line edited in the file instead is reported as HM5804 and, where the service may write the file, put back. Under each such field a line says when it applies: Applies on save (211 keys), After reinitialize (28: a listener’s port or bind address, a TLS context, a work queue’s size), applied by Apply now on the pending bar at the foot of the window, which stops and starts every service in place, with no restart of the Windows service and no elevation, or After a restart (2). [Database], [Directories], [LDAP], [SendingLimits], [SendingLimitsOverrides] and hMailServerApiKeys.ini stay in their files. Connected to a 6.3.3 server, the Control Panel marks every such field After a restart, as that server needs.

Page What it is for Where it writes When a change applies
Welcome Twelve starting points written as outcomes, each one click from the page that does it, then six area tiles. Since 6.3.4 also a button that makes a throwaway sample domain, example.test, and removes it again Actions only (the sample domain) At once
Dashboard Live counters and charts for processed mail, spam, viruses and sessions — Live, 24 hours, 7 days or 30 days — and a “needs attention” summary of what still needs doing outside the server. Connecting opens this page; the state of the service is on Server status — —
Set up this server → Setup checklist Everything this server needs done outside it — DNS records, key files, trusted lists — and the state of each, headed since 6.3.4 by the first-hour checklist the server judges for itself. Since 6.3.5 its items are numbered in the order a new installation meets them, the first that needs action marked as the next step, and it heads the new Set up this server group; it was External setup, under Monitoring & troubleshooting Read-only —
Set up this server → Setting profiles (new in 6.3.4) The four setting profiles — office, gateway, relay, sender — each with its difference from this server, applied with a box to tick where an entry closes a listener or weakens a check, put back, or the shipped settings kept COM Settings.GetProfile, ApplyProfile, RevertProfile As each setting’s own mark says
Set up this server → DNS records The SPF, DKIM, DMARC, MTA-STS and TLS-RPT records each domain should publish, with a live check Read-only —
Accounts & domains → Domains (on screen “Domains & accounts”) Domains, accounts, aliases, distribution lists, domain aliases, DKIM. Since 6.3.4 the domain dialog has fourteen tabs and the account dialog fifteen COM Domains At once
Accounts & domains → Groups Account groups, used for public-folder permissions; Members… edits who is in each COM Settings.Groups At once
Accounts & domains → Public folders Shared IMAP folders and their permissions COM Settings.PublicFolders At once
Accounts & domains → Administrator roles (new in 6.3.4) Who may administer what: the four built-in roles (server administrator, domain administrator, help desk, auditor), custom roles, and the accounts that hold each in which domains The database, through COM At once
Mail flow & delivery → Delivery of e-mail Retry schedule and delivery limits (Delivery tab), the smart-host relayer and its OAuth2 (Relayer tab), the rule loop limit (Rules tab), PIPELINING, CHUNKING and hard-linked local copies; since 6.3.4 the Bounces and the suppression list card. The host name is on Protocols (SMTP tab) and the text of bounces on Server messages COM Settings and [Settings] keys At once
Mail flow & delivery → Routes Per-domain delivery overrides COM Settings.Routes At once
Mail flow & delivery → Remote domains (new in 6.3.4) Per remote domain: the TLS this server demands in each direction, the size and connections it will attempt, recipient verification for a domain it backs up, and gateway encryption to a partner’s key COM Settings.RemoteDomainPolicies At once
Mail flow & delivery → Incoming relays Upstream gateways (spam filters, load balancers) whose IP addresses should not count as the connecting client in anti-spam host checks COM Settings.IncomingRelays At once
Mail flow & delivery → Rules (on screen “Global rules”) Server-wide mail rules, evaluated top to bottom COM Application.Rules At once
Mail flow & delivery → Server messages The text of bounces and system messages: a fixed set, whose text you may edit but not the list COM Settings.ServerMessages At once
Mail flow & delivery → Server sendout Send a message to every account (maintenance notices) Action only At once
Connections & protocols → TCP/IP ports Listening ports, their SSL bindings, from 6.3.6 each SMTP listener’s SMTP authentication (On, Off or Honeypot: Off and Honeypot answer 502 and ban the address at the first AUTH of a session; a new installation’s port 25 is a Honeypot, and an upgraded server’s ports are all On), and a live “Listening” badge (the badge on the local server only) COM Settings.TCPIPPorts Stored at once; a listener added or changed takes effect at the next reinitialise or service restart
Connections & protocols → Protocols SMTP, IMAP and POP3 on or off and their behaviour, the server’s host name (SMTP tab), and every idle timeout; tabs Services, SMTP, IMAP, POP3 and Timeouts COM Settings and [Settings] keys At once
Connections & protocols → Web services & autoconfiguration (on screen “Web services & client autoconfiguration”) The HTTP/HTTPS listener for Thunderbird and Outlook autoconfiguration and MTA-STS policy hosting, and the CalDAV and CardDAV redirects [Settings] keys At once, except the listener’s ports, bind address and certificate files, which wait for a reinitialise (Apply now)
Connections & protocols → Webmail (new in 6.3.5) How people sign in to the webmail, the self-service portal and the Control Deck, how long a session lasts and what a person may do, on the tabs Sign-in (passkeys, OpenID Connect, SAML, account recovery), Browser sessions, What people may do, Encryption, Notifications and Import Mostly the REST API, which is off as shipped (RestApiPort 0); while it is off, each tab that needs it says so As the line under each field says
Connections & protocols → DNS resolver Name servers, the Windows DNS cache, when DNSBL checks run [Settings] keys At once; an answer already cached stays until its TTL runs out
Certificates & encryption → SSL certificates Certificates available to TLS ports, with parse, expiry and key-match state COM Settings.SSLCertificates At once
Certificates & encryption → Certificates (ACME) (on screen “Automatic certificates (ACME)”) Automatic Let’s Encrypt issuance and renewal [Settings] keys At once, except the switch (AcmeEnabled) and AcmeCertificateDirectory, which wait for a reinitialise (Apply now)
Certificates & encryption → Transport encryption overview Read-only judgement of TLS versions, ciphers, ports and certificates together Read-only —
Certificates & encryption → SSL/TLS TLS versions, ciphers, key-exchange groups, session resumption COM Settings and [Settings] keys The TLS 1.3 cipher suites, the key-exchange groups and the session-resumption settings after a reinitialise (Apply now); the listeners build their TLS context when the servers start
Certificates & encryption → Transport security MTA-STS, DANE, DNSSEC, ARC, TLS-RPT, DMARC rua, SRS and BATV [Settings] keys At once
Spam & virus filtering → Spam filtering overview Read-only judgement of the whole spam pipeline, each row linking to its page Read-only —
Spam & virus filtering → Quarantine Messages held as suspected spam: release, delete, expire. It holds nothing until QuarantineEnabled is on (Anti-spam settings) Actions only At once
Spam & virus filtering → Anti-spam settings (on screen “Anti-spam”) Thresholds, quarantine, SPF, DKIM, DMARC and ARC, host checks, greylisting, SpamAssassin, the external filter engine, the recipient tarpit COM Settings.AntiSpam and [Settings] keys At once
Spam & virus filtering → SURBL servers URL blocklists checked against message bodies COM collection At once
Spam & virus filtering → DNS blacklists IP blocklists checked against connecting servers COM collection At once
Spam & virus filtering → White list (on screen “Anti-spam white list”) Senders exempt from spam checking COM collection At once
Spam & virus filtering → Blocked senders Claimed sender addresses and domains refused with a score (100 by default, above the delete threshold) COM collection At once
Spam & virus filtering → Allow and block list (new in 6.3.4) Addresses and domains every account wants or does not: the server’s list, below each account’s own list and its domain’s; an entry may be a URL COM AntiSpam.SenderList At once
Spam & virus filtering → Greylisting white list Senders exempt from greylisting only COM collection At once
Spam & virus filtering → Greylisting bypass list (new in 6.3.4) Domains and HELO suffixes let past greylisting only when the message passes SPF, DKIM or DMARC as the entry requires COM AntiSpam.GreyListingBypassEntries At once
Spam & virus filtering → Domain white and black list (new in 6.3.4) Sending domains this server has decided about, by envelope domain or HELO suffix: white skips greylisting and the score’s refusals, black is refused at MAIL FROM COM AntiSpam.DomainPolicyList At once
Spam & virus filtering → Virus scanning overview Read-only judgement of whether the configured scanners can actually run Read-only —
Spam & virus filtering → Anti-virus settings (on screen “Anti-virus”) ClamAV, ClamWin or a custom scanner; action on detection; what to do when a scanner cannot run; the switch for attachment blocking COM Settings.AntiVirus and [Settings] keys At once
Spam & virus filtering → Blocked attachments Attachment-name wildcards (*.exe) whose attachments are stripped — only while attachment blocking is on in Anti-virus settings COM collection At once
Sign-in & access → Authentication OAuth2/XOAUTH2 token authentication, password hashing and pepper, ports without AUTH [Settings] keys, except the password pepper, which is kept in hMailServer.INI only; under NT SERVICE\hMailServer the page cannot write it, and it is set in the file with an elevated editor At once, except a pepper set in the file, which applies when the service is restarted
Sign-in & access → Administrative access Administrator password, mailbox password policy, two-factor authentication (tabs Password and Two-factor) COM and [Settings] keys; the administrator password and the server-enforced second factor in hMailServer.INI [Security], the Control-Panel-only second factor under HKLM\SOFTWARE\hMailServer. Under NT SERVICE\hMailServer that file is read-only to the service, so the password card and enrolling or removing the server-enforced second factor are refused: hMailServer.exe --set-admin-password, from an elevated prompt, sets the password instead; enrolling needs the service moved to LocalSystem for the enrolment and back, and the factor is removed by deleting its line with an elevated editor and restarting the service At once, except a password set with --set-admin-password, which the running service takes only when it is restarted or reinitialised
Sign-in & access → IP ranges Who may connect, authenticate and relay, and whether a password needs TLS COM Settings.SecurityRanges At once
Sign-in & access → Auto-ban Failed-logon banning, per-name lockout, logon tarpit COM Settings and [Settings] keys At once
Sign-in & access → Directory authentication (LDAP) Authenticate mailbox passwords against a directory hMailServer.INI [LDAP], on the server itself Within two seconds — no restart
Sign-in & access → Directory synchronisation Provision mailboxes from the directory, preview then apply hMailServer.INI [LDAP], and COM actions Preview and Apply run inside the server when asked
Monitoring & troubleshooting → Server status Version, state (Pause and Resume), uptime, database, session and message counters, configuration warnings, and the update check with its Download and Install buttons. A server from 6.3.4 on installs a later release by its live update; on Windows a service running as NT SERVICE\hMailServer leaves the install to an administrator (§18.2) Actions only At once
Monitoring & troubleshooting → Delivery queue Outbound mail waiting; view the source, retry or delete individual messages Actions only At once
Monitoring & troubleshooting → Live logs Real-time colour-coded log tail — local server only — —
Monitoring & troubleshooting → Logging Log level, format, retention, JSON output, message trace; since 6.3.4 a syslog (RFC 5424) card COM Logging.* and [Settings] keys At once, except the syslog card, whose settings are taken when the syslog sink starts: at the next reinitialise or service start
Monitoring & troubleshooting → Diagnosing stalled mail Which half stalled (accepting or delivering), debug logging on and off, and the settings that bound a session, each with a button to its page COM Logging.* At once
Monitoring & troubleshooting → Message trace What happened to one message, by sender or recipient. It records nothing until MessageTraceEnabled is on (the Logging page) Actions only At once
Monitoring & troubleshooting → Diagnostics Built-in self-test, plus the last message-store consistency scan — —
Monitoring & troubleshooting → MX query Look up any domain’s MX records using the server’s own resolver — —
Monitoring & troubleshooting → Suppression list (new in 6.3.4) Addresses a domain no longer sends to because mail to them bounced, visible and removable by hand. Bounces are read only once bounce processing, off by default, is on (Delivery of e-mail, card Bounces and the suppression list) The database, through COM At once
Monitoring & troubleshooting → Audit trail (new in 6.3.4) Who changed what, when, over which interface and from where — every administrative change one row of a hash chain — and Put back, which returns a setting to the value it had (server administrator only) Read-only, plus Put back At once
Monitoring & troubleshooting → Journaling and dual delivery (new in 6.3.4) A copy of a domain’s mail, or of one account’s, sent to an address, as an envelope report with the message attached or as the message itself (server administrator only) The database (hm_journalrules), through COM At once
Monitoring & troubleshooting → Discovery (new in 6.3.4) The eDiscovery cases, read-only: each case’s searches, holds and exports, and its lines of the audit trail. Discovery has no COM interface, so the page reads the REST listener: it needs RestApiPort set, a certificate it can check for an address beyond loopback, and an administrator without a second factor; a case is opened, searched, held and exported on the Control Deck Nothing: it reads over REST —
Monitoring & troubleshooting → API & monitoring The REST API and the browser Control Deck, Prometheus and OpenTelemetry monitoring and metric history, the Windows Event Log, ManageSieve, the shutdown drain, the live-update settings and the HttpProxy forward proxy. Since 6.3.4 the update feed field is labelled Update feed URL, and the default feed is https://updates.progressiverobot.com [Settings] keys At once, except the ports, bind addresses and certificate files of the REST, metrics and ManageSieve listeners and the metrics credentials, which wait for a reinitialise (Apply now)
Monitoring & troubleshooting → REST API keys Scoped, expiring credentials for the REST API — local server only hMailServerApiKeys.ini At once: the server re-reads the file on every request
Maintenance → Backup & restore Backup contents, destination, schedule, verification and restore COM Settings.Backup and [Settings] keys At once
Maintenance → Performance Caching, connection and thread tuning, message indexing, database connections; since 6.3.4 a Message store tab — storage compression, the smallest attachment held once, and what the store saves COM, [Settings] keys and hMailServer.INI [Database] At once, except the external-fetch thread count and DatabaseStatementTimeout (after a reinitialise) and the Database tab’s [Database] rows, edited on the server itself (after a service restart); the Message store switch applies to the next message stored
Maintenance → Advanced Default domain, IPv6 preference, mirroring and archiving, disk-space limits, and the scripting engine (Scripting tab): on or off, and the language, since 6.3.4 VBScript, JScript or JavaScript (the Linux builds’ engine, which a Windows server refuses at load) COM and [Settings] keys At once; the script engine reloads on Save
Maintenance → Event scripts The VBScript/JScript event handlers themselves — local server only The script file on disk “Save & reload” reloads the engine
Maintenance → Server limits & expert settings DPAPI secrets, fsync, consistency checking, proxies, sending limits, ceilings; since 6.3.4 the Windows service account card — what the service runs as (a new 6.3.4 installation runs as NT SERVICE\hMailServer, an upgraded one keeps its account), and Grant access, Move to NT SERVICE\hMailServer and Move to LocalSystem, each after the elevation prompt [Settings] keys; [SendingLimits] and [SendingLimitsOverrides] in hMailServer.INI At once; the sending limits are re-read within seconds; a changed service account takes effect when the service is restarted
About Version, licence, build information; since 6.3.4 also in the command bar’s administrator menu — —

Pages not pictured above

The remaining pages are shown here for completeness. The screenshots were captured from a 6.2.10 build, before the redesign 6.3.4 carries: the sidebar they show is the older tree, and the page pictured as “Advanced and scripting” is now titled “Advanced”.

Connections & protocols → Protocols — five tabs (Services, SMTP, IMAP, POP3, Timeouts) enabling or disabling the three protocol servers and tuning their behaviour:

Protocols
Protocols

Maintenance → Performance — caching, connection and thread tuning on the Threads, Cache, Indexing and Database tabs, and since 6.3.4 a fifth, Message store:

Performance
Performance

Maintenance → Advanced — default domain, mirroring and archiving, disk-space limits, the scripting engine:

Advanced and scripting
Advanced, titled “Advanced and scripting” in the 6.2.10 build pictured

Mail flow & delivery → Server messages — the text of bounces and system messages:

Server messages
Server messages

Accounts & domains → Groups — account groups, used for public-folder permissions; today each group also has a Members… button, which the picture, taken before the member editor existed, does not show:

Groups
Groups

Mail flow & delivery → Server sendout — send a message to every account, for maintenance notices:

Server sendout
Server sendout

About — version, licence and build information:

About
About

21. Settings reference (hMailServer.INI)

Nearly every [Settings] key here is also editable in the Control Panel. Since 6.3.4 each of their fields there is marked Applies on save, After reinitialize or After a restart, and a bar at the foot of the window says how many saved settings still wait, with Apply now, which is a reinitialise rather than a service restart. Over REST, PUT /api/v1/settings/ini/{name} writes any of them but the five of the live update’s trust, which from 6.3.6 are read from the file alone (Self-update, below), with the administrator password only (an API key is refused); of the [Settings] keys the Control Deck edits only the four its Settings view carries, SmtpTarpitCount, SmtpTarpitDelaySeconds, UseLanguage and RewriteEnvelopeFromWhenForwarding (chapters 24 and 36). Unless a section says otherwise, the keys below live in [Settings], and the values shown are the defaults.

The file is Bin\hMailServer.INI on Windows and /etc/hmailserver/hMailServer.ini on Linux. The server looks for the file beside its own executable first and falls back to the packaged path; --config <file> overrides both.

Since 6.2.19 these values are also stored in the database, so they can be administered remotely through the Control Panel. Since 6.3.4 the database is the settings store, and [Settings] in the file is its copy. At the first start after the upgrade the section’s keys are taken into the database, with nothing to run by hand; a line edited after the old server last started, including one edited to prepare the upgrade, is taken for an edit and not applied (and put back where the service may write the file), so make such a change after upgrading. From then on a line edited in the file is ignored in favour of the stored value, named in the error log as HM5804 and, where the service may write the file, put back, and deleting a line no longer returns a setting to its default: DELETE /api/v1/settings/ini/{name}, hmctl ini delete <name> or COM Settings.DeleteIniSetting(name) does. The Control Panel cannot: clearing a box there stores an empty value, which a number reads as 0 rather than as its default. Change a setting in the Control Panel, over COM (Settings.SetIniSetting), over REST (PUT /api/v1/settings/ini/{name}) or, on Linux, with sudo hmailserver --set-setting NAME=VALUE, which reaches a running service at its next save, reinitialise (sudo systemctl reload hmailserver) or restart. Under a service account that cannot write the file — NT SERVICE\hMailServer, a new Windows installation’s, and the Linux package’s hmailserver user, whose file is 0640 root:hmailserver — the copy is not refreshed and the database’s values are used. A [SettingsOverride] section is the deliberate way round a database that cannot be read, and is named as HM5807 at every start. No new key is added to [Settings] from 6.3.4 on: a new setting is a row in the database. Every key now says when a save applies it. Of the keys below, the REST, metrics, web-services and ManageSieve listeners’ ports, bind addresses, credentials and certificates, the six TLS context keys from TlsKeyExchangeGroups to TlsTicketKeyRotationSeconds, AcmeEnabled and DatabaseStatementTimeout wait for a reinitialise, and ServiceAccountName and ServiceAccountPassword for a restart; every other [Settings] key below applies when it is saved. The [Database] and [Security] keys, read from the file, wait for a restart.

More: Settings: what is not stock, when a save takes effect, and the three levels on the forum.

Transport security and authentication

MtaStsEnabled=1               ; honour recipient MTA-STS policies when sending
DaneEnforcementEnabled=1      ; honour recipient DANE/TLSA records when sending
DnssecValidationEnabled=1     ; validate DNSSEC for DANE and SPF/DKIM/DMARC lookups
DnssecTrustAnchors=           ; override root trust anchors ("tag alg digesttype hex;...")
ArcSealingEnabled=0           ; add ARC seals when forwarding (uses the domain's DKIM key)
TlsRptFromAddress=            ; sender for daily TLS-RPT reports (empty = disabled)
TlsRptOrganizationName=hMailServer
TlsKeyExchangeGroups=X25519MLKEM768:SecP256r1MLKEM768:X25519:secp384r1:secp256r1
TlsCipherSuites13=            ; empty = OpenSSL's own defaults
TlsSessionTicketsEnabled=1
TlsSessionCacheSize=0         ; 0 = OpenSSL's default, negative = off
TlsSessionTimeoutSeconds=0
TlsTicketKeyRotationSeconds=0 ; 0 = OpenSSL's single, never-rotated key

The default key-exchange list puts post-quantum hybrids first. An OpenSSL older than those hybrids — the one Debian and Ubuntu ship, for instance — rejects the list and the fallback handles it. Since 6.3.0 that is reported once rather than once per listener and once per outbound delivery, which is the difference between a note and hundreds of medium errors an hour.

Automatic certificates (Let’s Encrypt)

AcmeEnabled=0                 ; issue and renew certificates automatically
AcmeContactEmail=             ; expiry notices from the CA
AcmeDomains=                  ; comma-separated host names for the certificate
AcmeDirectoryUrl=https://acme-v02.api.letsencrypt.org/directory
AcmeHttpPort=80               ; port for http-01 challenges
AcmeReuseKey=1                ; keep the same key across renewals (keeps TLSA records valid)

Web services (MTA-STS hosting, client autoconfiguration)

WebServicesHttpPort=0         ; 80 to enable
WebServicesHttpsPort=0        ; 443 to enable (uses the ACME certificate if none is set)
WebServicesBindAddress=0.0.0.0
MtaStsHostingEnabled=1        ; serve https://mta-sts.<domain>/.well-known/mta-sts.txt
MtaStsPolicyMode=enforce      ; enforce, testing or none
MtaStsPolicyMaxAge=604800
MtaStsPolicyMx=               ; override mx patterns (default: the domain's live MX records)
AutoconfigEnabled=1           ; Thunderbird autoconfig + Outlook autodiscover
AutoconfigClientHost=         ; host name clients connect to (default: the server's host name)

Per domain, point mta-sts.<domain>, autoconfig.<domain> and autodiscover.<domain> at this server, and include them in AcmeDomains for HTTPS.

Administration and monitoring

RestApiPort=0                 ; REST API + the Control Deck at / and the portal at /portal
RestApiBindAddress=127.0.0.1  ; TLS is required unless bound to 127.0.0.1, localhost or ::1
RestApiCertificateFile=       ; PEM; falls back to the ACME certificate
RestApiPrivateKeyFile=
MetricsServerPort=0           ; Prometheus metrics endpoint (/metrics) + health probes
MetricsServerBindAddress=127.0.0.1
MetricsServerAuthToken=       ; bearer token for /metrics (with no credential set, /metrics answers
                              ; 503 on any bind but loopback; the probes still answer)
MetricsServerAuthUsername=    ; or HTTP Basic
MetricsServerAuthPassword=
MetricsServerCertificateFile= ; TLS on the metrics listener
MetricsServerPrivateKeyFile=
MetricsHistoryDays=7          ; per-minute metric samples kept in hm_metricsamples
MetricsPerDomainEnabled=0
OtelMetricsEndpoint=          ; OTLP metrics (separate from OtelEndpoint)
OtelLogsEndpoint=             ; OTLP logs
OtelMetricsInterval=60
HttpProxy=                    ; host:port for the server's web client: the update feed and downloads, JWKS,
                              ; introspection and the like; ACME, the outbound XOAUTH2 token and inbound
                              ; webhooks go direct
CalDavRedirectUrl=            ; /.well-known/caldav redirects here, to another server's CalDAV;
                              ; empty = this server's own CalDAV at /dav/ (6.3.4), on the web-services listener
CardDavRedirectUrl=           ; the same for /.well-known/carddav (this server's own CardDAV since 6.3.3)
IMAPCompressionEnabled=1      ; advertise IMAP COMPRESS=DEFLATE (RFC 4978)
WindowsEventLogEnabled=1      ; critical/high errors to the Windows Event Log — syslog on Linux
WindowsEventLogLevel=2        ; 1=Critical, 2=+High, 3=+Medium, 4=everything
DatabaseStatementTimeout=30   ; seconds a statement may run (0 = none), set at connect on PostgreSQL and
                              ; MySQL; MS SQL and SQL CE use ADO's own 30 unless an upgrade or maintenance
                              ; script ran on the connection
SlowQueryLogMilliseconds=0    ; log queries slower than this (0 = off)
LogDeleteDays=0               ; prune date-stamped logs older than N days (0 = keep all)
ShutdownDrainSeconds=0        ; on stop, wait up to N seconds for sessions to finish
MessageStoreFsync=0           ; force each message to physical disk before acknowledging
MessageStoreConsistencyCheck=0; periodically cross-check message rows against files
ManageSieveServerPort=0       ; ManageSieve (RFC 5804); standard port 4190
ManageSieveServerBindAddress=127.0.0.1
JsonLogging=0                 ; write logs as JSON lines

Bounded waits and timeouts

New in 6.2.17–6.2.18. Every wait that can hold a thread from a shared pool now has a ceiling, and the log names the one that was hit. All values are in seconds.

FinalizationTimeout=240       ; ceiling on accepting one message after end-of-data; past it
                              ; the sender gets 451 and retries (0 = off)
SAMaxTimeout=90               ; SpamAssassin ceiling; past it the message is accepted unscanned
DBConnectionAcquireTimeout=60 ; maximum wait for a pooled database connection
DNSQueryTimeout=10            ; per DNS query
ScriptTimeout=60              ; per event-script invocation
ExternalProcessTimeout=300    ; external helper processes (command-line scanners and the like)
ClientSessionCeiling=1800     ; absolute lifetime of an outbound delivery session — distinct
                              ; from the idle timeout, which re-arms on every byte received

Secret protection and least privilege

ProtectStoredSecretsWithDPAPI=1   ; machine-scoped DPAPI for reversible stored secrets
ServiceAccountName=               ; retired in 6.3.4: the installer, or /Register --service-account, chooses
ServiceAccountPassword=           ; retired in 6.3.4

On Linux the same key means a key file, not DPAPI. 1 selects <DataFolder>/.hmailserver-secret-key — 32 random bytes at mode 0600, written once and never rewritten, each secret AES-256-GCM under it and stored as LINUX1:<base64>. Back it up with the database (chapter 15). ServiceAccountName and ServiceAccountPassword do nothing there: the systemd unit runs the server as its own hmailserver user.

Deliverability and SMTP standards

SRSEnabled=0                         ; Sender Rewriting Scheme for forwarded mail
SRSSecret=                           ; HMAC secret for SRS (required when SRSEnabled=1)
MaxSubmissionsPerIPPerMinute=0       ; cap MAIL FROM per source IP per minute (0 = off)
MaxOutboundPerDestinationPerMinute=0 ; cap outbound per destination domain (0 = off)
OutboundPipelining=1                 ; pipeline the envelope where the remote advertises PIPELINING
OutboundChunking=1                   ; send the message as one BDAT chunk where it advertises CHUNKING
SmtpAuthenticatedSenderCheck=0       ; 1 = a signed-in account may send only as itself, an alias of it, an
                                     ; address it verified or a mailbox that granted it the post right
DkimOversignHeaders=                 ; header fields to over-sign
DkimAcceptSha1=0                     ; restore rsa-sha1 signing and verification (RFC 8301 forbids it)
DmarcTreeWalkEnabled=1
DmarcRptFromAddress=                 ; DMARC aggregate reports (empty = disabled)
DmarcRptOrganizationName=hMailServer
DmarcRptSchemaVersion=1
SpfVoidLookupLimit=2
SMTPProxyProtocolEnabled=0           ; accept PROXY protocol from a trusted reverse proxy
SMTPProxyProtocolTrustedIPs=
SMTPXClientEnabled=0                 ; accept XCLIENT from a trusted front end
SMTPXClientTrustedIPs=
MinimumFreeDiskSpaceMB=100           ; below this, mail is refused with a temporary error
DiskSpaceWarningThresholdMB=1024

MinimumFreeDiskSpaceMB changes behaviour on upgrade without a switch. It is new since 6.2.21 and defaults to 100: below that floor the server refuses mail with a temporary error rather than filling the disk. Set it to 0 to restore the older behaviour.

PIPELINING, SMTPUTF8/EAI (RFC 6531/6532), ENHANCEDSTATUSCODES (RFC 2034), DSN (RFC 3461), CHUNKING/BDAT and BINARYMIME (RFC 3030) and 8BITMIME are advertised automatically and need no configuration. Legacy HELO sessions keep the classic non-enhanced replies.

Anti-spam, anti-virus and filtering

FilterHookUrl=                ; external HTTP filter (rspamd and the like); empty = off
FilterHookTimeoutSeconds=10
FilterHookRejectScore=100
FilterHookMaxMessageSizeKB=10240
FilterHookFailClosed=0
SpamAssassinUser=             ; whose spamd preferences to apply
SpamAssassinUserFromRecipient=0
SpamAssassinLearnOnMove=0     ; moving to Junk teaches spam, out of it teaches ham
QuarantineEnabled=0
QuarantineRetentionDays=30
AVFailAction=0                ; what to do when the scanner cannot be reached
AVFailRetryMinutes=15
AVFailMaxHolds=16

Passwords, lockout and tarpits

PasswordHashIterations=0      ; 0 = the built-in work factor; re-derived upward only
PasswordHashMemoryKB=0
PasswordHashTimeCost=0
PasswordPolicyMinimumLength=0
PasswordPolicyRequireMixedCase=0
PasswordPolicyRequireDigit=0
PasswordPolicyRequireNonAlphanumeric=0
PasswordPolicyRejectCommon=0
PasswordPolicyHistoryCount=0
PasswordPolicyMaximumAgeDays=0
AccountLockoutThreshold=0
AccountLockoutWindowMinutes=30
AccountLockoutMinutes=30
LogonTarpitSeconds=0
SmtpTarpitCount=0
SmtpTarpitDelaySeconds=0
Pop3LoginDelaySeconds=0
ScriptAllowedObjects=*        ; which COM classes an event script may create (Windows only)

PreferredHashAlgorithm takes 3 (SHA256), 4 (PBKDF2, the default), 5 (Argon2id) or 7 (scrypt, new in 6.2.25). Anything else is refused at read time and reported as HM5528; see §17.2.

PasswordPepper, a server-wide secret mixed into Argon2id and scrypt hashes (empty, and off, by default), is since 6.3.4 the one [Settings] key kept in the file only: never stored in the database and never in a backup archive. It is changed in the file, or over COM or REST, which write only the file; where the service cannot write the file, as under NT SERVICE\hMailServer and in the Linux package, an administrator sets it with an elevated editor and restarts the service. Set it before accounts exist: changing it later invalidates every Argon2id and scrypt password. A server restored onto new hardware needs the same value copied into its file by hand (chapter 15), or no account whose password is an Argon2id or scrypt hash can sign in.

OAuth2

OAuth2JwksUrl=                ; the provider's JWK Set as the signing-key source
OAuth2JwksCacheSeconds=3600
OAuth2IntrospectionUrl=       ; token introspection, so revocation takes effect
OAuth2IntrospectionClientId=
OAuth2IntrospectionClientSecret=
OAuth2IntrospectionCacheSeconds=300
OAuth2IntrospectionFailOpen=0
OutboundOAuth2TokenUrl=       ; XOAUTH2 for outbound relay (Microsoft 365 and the like)
OutboundOAuth2ClientId=
OutboundOAuth2ClientSecret=
OutboundOAuth2Scope=https://outlook.office365.com/.default
OutboundOAuth2Hosts=smtp.office365.com
OutboundOAuth2FixedToken=
FetchOAuth2Hosts=outlook.office365.com

Storage, retention and archive

ArchiveDomains=               ; scope the archive to named domains (empty = all)
ArchiveRetentionDays=0
ArchiveHardLinks=0
DeliveryHardLinks=0           ; one file on disk, a name in each recipient's folder
BackupVerifyRestore=1         ; verify the archive before calling a backup complete
ScheduledBackupTime=          ; HH:MM local for a daily backup; wins over the interval
ScheduledBackupIntervalMinutes=0
ScheduledBackupKeepCount=0
ScheduledBackupMaxAgeDays=0
IMAPExpungeRetentionRecords=5000
IndexerFullText=0             ; full-text index (schema 6023); off
IndexerFullTextBatchSize=250
IndexerFullTextMinTokenLength=3
IndexerFullTextMaxTokensPerMessage=2048
MessageTraceEnabled=0
MessageTraceRetentionDays=30
QuotaWarningPercent=90        ; since 6.3.4 a domain or an account may hold its own value
RejectFullMailboxAtRcpt=1

Self-update

New in 6.2.28. Every one of these is inert until UpdateCheckEnabled=1: the default makes no request, sends no identifier and counts nothing (§18.7).

UpdateCheckEnabled=0          ; nothing happens at all until this is 1
UpdateCheckHours=24
UpdateChannel=stable
UpdateFeedUrl=                ; empty = this project's own release feed
UpdateSourceRepository=       ; the repository a signature must be bound to
UpdateSigningIdentity=
UpdateSigningIssuer=
UpdateTrustRootsFile=         ; point these five at a private Sigstore instance
UpdateLogPublicKeyFile=
UpdateRequireAuthenticode=0   ; 1 also requires Authenticode (Windows only)
UpdateAutoDownload=0
UpdateWindow=                 ; the window an unattended apply may run in
UpdateBackupBeforeApply=1
UpdateServiceWaitSeconds=180  ; wait this long for the service, then roll back

UpdateRequireAuthenticode only became worth setting in 6.3.1. It has existed since 6.2.28, and until then 1 refused every release of this project, because none carried an Authenticode signature; the Windows installer has carried one since 6.3.1 (§4.1). The check is WinVerifyTrust on the downloaded installer and it is Windows-only — on Linux there is nothing to verify, so the server says so rather than reporting a pass. Up to 6.3.3 updating a Linux server is the package manager’s job; since 6.3.4 the live update applies there through a root helper, though 6.3.4 itself is installed by hand, from its .deb or .rpm file (§35.13).

UpdateFeedUrl and 6.3.4. The release feed moved off GitHub: a server from 6.3.4 on reads https://updates.progressiverobot.com, which GitLab Pages serves from the project’s releases, without being told. A server from 6.2.28 to 6.3.3 still reads the former GitHub repository’s Releases API, which has answered 404 since 18 September 2026; set UpdateFeedUrl=https://updates.progressiverobot.com/releases.json there and restart the service, and its check works again. Such a server’s live update trusts only GitHub’s signer, so it refuses every release signed on GitLab, from 6.3.5 on; installing the current release by hand, once, avoids that, and until then keep UpdateAutoDownload off there, or it downloads a GitLab-signed installer at every check and refuses it. 6.3.4 itself carries no Sigstore bundle and is installed by hand (§18.7).

From 6.3.6 the live update’s trust is read from the file alone. UpdateSourceRepository, UpdateSigningIdentity, UpdateSigningIssuer, UpdateTrustRootsFile and UpdateLogPublicKeyFile are read from [Settings] in hMailServer.ini on every platform, never from the database, so that the process an update is checked for does not choose what it is checked against. At the first start of 6.3.6 a value 6.3.4 or 6.3.5 stored in the database is taken out, and where it said something the file does not, the error log names it once as HM5809, with both values; a line for one of the five in [SettingsOverride] is ignored and named at every start. COM and the REST API (409) refuse a change to them, and so the Control Panel and the Control Deck. Most servers set none of the five. To set one, edit the file – on Windows with an editor run as an administrator, on Linux with sudo hmailserver --set-setting NAME=VALUE – and restart the service; where several servers share a database, put it in every server’s file before the first is upgraded, since the first 6.3.6 to start takes the rows out for all of them.

Under NT SERVICE\hMailServer, the account a new 6.3.4 installation runs as, the server still checks for releases, but an apply is refused before anything happens, because that account cannot run an installer: an administrator downloads the release from its release page, checks its Authenticode signature and runs it (§17.3). An UpdateWindow there applies nothing and takes no backup, and says why once in the error log (HM7463).

The [Database] and [Security] sections

These do not live in [Settings], because they say how the database is reached, and they stay in the file after 6.3.4 moved the rest into the database:

[Database]
TlsMode=                      ; new in 6.3.4: empty = prefer; require, verify-full or disable
TlsCaFile=                    ; new in 6.3.4: the CA certificates verify-full checks against
UpgradeOnStart=0              ; new in 6.3.4: 1 applies the schema steps as the service starts
PostgreSQLSslMode=            ; require, verify-full, ... an unknown mode refuses the connection
PostgreSQLSslRootCert=
ConnectionStringOptions=      ; appended to the OLE DB connection string (MS SQL)
AllowUnencryptedConnection=0  ; still read; since 6.3.4 known to change nothing

Since 6.3.4 the connection is encrypted whenever the database server offers TLS. MySQL and MariaDB through MariaDB Connector/C — the client bundled on Windows and the one Ubuntu packages — had never used TLS, whatever the documents said; with TlsMode empty they now use it whenever it is offered, without checking the certificate, and connect without it when it is not. SQL Server through MSOLEDBSQL 18 and SQL Native Client now encrypts the whole session; through SQLOLEDB, which the server uses where MSOLEDBSQL 18 or later is not installed and no failover partner is set, only the login is encrypted unless TlsMode is require or verify-full. PostgreSQL is unchanged when nothing is set, and its two keys above, which came first, win over TlsMode and TlsCaFile. The application log says at every start what the connection got, and --check-database makes one connection and says the same.

And [Security] AdministratorTotpSecret holds the administrator’s TOTP secret. It is written by enrolment, not by hand; clearing the line is how a locked-out administrator credential is recovered (§17.4). Under NT SERVICE\hMailServer the service cannot write the file, so enrolment is refused until the service is moved to LocalSystem for it and back, and the line is cleared with an elevated editor and a restart. [Security] AdministratorPassword holds the administrator password as a PBKDF2 hash; since 6.3.4 an older MD5 or SHA-256 value is written again as PBKDF2 at the next administrator sign-in, where the service may write the file. Where it cannot, as under that account and in the Linux package, set the password with hMailServer.exe --set-admin-password from an elevated prompt, or sudo hmailserver --set-admin-password, then restart the service: a running one keeps the old password until it is restarted or reinitialised. Neither takes the password as an argument. On Windows the verb reads the HM_ADMIN_PASSWORD environment variable or, when that is not set, one line of piped input (from Windows PowerShell 5.1, a password in plain ASCII only); on Linux it asks for it.

New in 6.3.4: [Security] StoredSecretsKey. It holds the installation’s stored-secrets key, under this machine’s DPAPI (under the key file above on Linux). Each account’s TOTP second factor and the [Settings] secrets (SRSSecret, BATVSecret, OAuth2HmacSecret, OAuth2IntrospectionClientSecret, OutboundOAuth2ClientSecret, OutboundOAuth2FixedToken, MetricsServerAuthToken, MetricsServerAuthPassword, ServiceAccountPassword and GssapiServicePassword) are sealed under it, so that every server sharing the database can open them. The Windows installer makes the key on a new installation. An upgrade gets none, and nothing is sealed until an administrator runs hMailServer.exe --create-stored-secrets-key from an elevated prompt, or sudo hmailserver --create-stored-secrets-key, which is also how a packaged Linux server gets one. The key is never in the database or in a backup archive: export it at once with --export-stored-secrets-key <file>, keep that file with the database backups but stored apart from them, and import it with --import-stored-secrets-key on every other server that shares the database, then restart each. A database dump restored without the key brings back every second factor and [Settings] secret unopenable. ProtectStoredSecretsWithDPAPI=0 keeps them as written.


22. Ports reference

Port Protocol Encryption Who uses it Open to the internet?
25 SMTP STARTTLS Other mail servers delivering to you Yes — required
587 SMTP submission STARTTLS Your users sending mail Yes
465 SMTPS Implicit TLS Your users sending mail (legacy-modern) Optional
143 IMAP STARTTLS Your users reading mail Optional — prefer 993
993 IMAPS Implicit TLS Your users reading mail Yes
110 POP3 STARTTLS Your users reading mail Optional — prefer 995
995 POP3S Implicit TLS Your users reading mail Optional
80 HTTP None ACME challenges, autoconfig Only if using ACME/web services
443 HTTPS TLS MTA-STS policy, autoconfig and autodiscover, the Apple profile, CardDAV and (since 6.3.4) CalDAV for phones and desktop clients, and the OpenPGP Web Key Directory (6.3.4) Only if using MTA-STS hosting, autoconfiguration (autoconfig, autodiscover or the Apple profile), CardDAV or CalDAV, or the Web Key Directory
4190 ManageSieve STARTTLS when an IMAP, POP3 or SMTP port has a certificate Users managing Sieve scripts No — off by default (ManageSieveServerPort=0), localhost only when on
(your choice) REST API, Control Deck, portal TLS Administration scripts, the Control Deck at /, and mailbox users through the portal at /portal Only with TLS. Off by default (RestApiPort=0) and bound to loopback when on; a portal your users reach from outside needs a certificate and a bound address, or a TLS-terminating proxy in front
(your choice) Metrics None, or TLS Prometheus, health probes No — off by default (MetricsServerPort=0) and bound to 127.0.0.1 by default; the bind address must be an IP address, not localhost. On a non-loopback bind, /metrics answers 503 until you set a credential (a token, or a user name and password), and the health probes answer without one. TLS is optional: a certificate and key make the whole port HTTPS, probes included; without them it is plain HTTP
161 UDP SNMP (new in 6.3.4) SNMPv3 at authPriv only (HMAC-SHA-256, AES-128); SNMPv2c optional and off, its community in the clear A monitoring system that speaks SNMP. Set over REST, in the Control Deck or with hmctl; there is no Control Panel page No — off by default (SnmpEnabled), and it answers nobody until SnmpAllowedAddresses names a manager; bind it to a management address
783 spamd None hMailServer talking to SpamAssassin (chapter 10) No — outbound, to your own spamd
3310 clamd None hMailServer talking to ClamAV (chapter 11) No — outbound, to your own clamd

A new database is created with four listeners, all without encryption: SMTP on 25 and 587, POP3 on 110 and IMAP on 143. Ports 465, 993 and 995 are not created for you; add them on the TCP/IP ports page (on Linux, the Control Deck’s Ports view or hmctl port create) once you have a certificate. A port added or changed is listened on from the next reinitialise (POST /api/v1/server/reinitialize, or systemctl reload on Linux) or service start, not from the save. Since 6.3.4 a new installation (the Windows installer after its database step, or on Linux the service’s first start after --create-database; never an upgraded or attached database) gives those four a self-signed certificate for the machine and sets them to STARTTLS optional, and makes the Internet IP range require TLS for authentication, so that a password from another machine never crosses the network in the clear. Replace the certificate with a real one: ACME gives its certificate to every listener that holds the self-signed one (chapter 9).

The mail ports are configured under Connections & protocols → TCP/IP ports on Windows, the web services ports (80 and 443) under Connections & protocols → Web services & autoconfiguration, and the REST, metrics and ManageSieve listeners under Monitoring & troubleshooting → API & monitoring. On Linux there is no Control Panel: the mail listeners are configured through the REST API (or hmctl, since 6.3.4) or the Control Deck’s Ports view, and the web services, REST, metrics and ManageSieve listeners, whose ports are [Settings] keys the Control Deck has no editor for, over the REST API (PUT /api/v1/settings/ini/{name}, or hmctl ini set) or with sudo hmailserver --set-setting (chapters 24, 35 and 36). The REST API, hmctl and the Control Deck all need the REST listener, and since 6.3.4 you turn it on with sudo hmailserver --set-setting RestApiPort=8045 and sudo hmailserver --set-setting RestApiBindAddress=127.0.0.1, then sudo systemctl reload hmailserver, a reinitialise, because the listener binds only when the services start. Editing those keys in hMailServer.INI after the first start does not work: the database holds the [Settings] keys, and a line edited in the file is reported as HM5804 and the stored value is used.

The REST listener carries all of this on one port — the JSON API under /api/v1/, the Control Deck at /, the webmail portal at /portal (with its script at /portal.js), and POST /api/v1/session, which exchanges a password for an HttpOnly, SameSite=Strict session cookie. Since 6.3.4 it also serves SCIM 2.0 under /scim/v2/, the list manager’s public pages under /lists/, and the single sign-on pages under /portal/oidc/ and /portal/saml/. There is no second port to open. The portal comes out of the binary; the Control Deck is served from the file WebAdmin/index.html that the Windows installer and the Linux packages install with the server (since 6.3.4 with its language catalogues in WebAdmin/languages), and a server without that file answers / with a short page that says "Web administration page not installed". The listener does not start until the administrator password is set, and TLS is required unless the listener is bound to 127.0.0.1, localhost or ::1.

Port 80 alone is not quite enough for Apple devices. Since 6.2.25 the .mobileconfig profile is served over HTTPS only: a plain-HTTP request is answered with a 301 to the same URL on the WebServicesHttpsPort listener where one is configured, and a 403 that says why where none is. A TLS-terminating proxy signals with X-Forwarded-Proto: https, which the listener believes only from loopback or from a proxy listed in WebServicesTrustedProxies (new in 6.3.3); from any other peer the header is ignored. Thunderbird autoconfig and Outlook autodiscover are unchanged.

TCP/IP ports
TCP/IP ports

23. The COM API and scripting

hMailServer exposes a full COM API. Anything the Control Panel can do, a script can do — it is the same interface, apart from a few pages that do not go through COM: Discovery (6.3.4) reads eDiscovery cases over the REST listener, because discovery has no COM interface, and REST API keys writes hMailServerApiKeys.ini on the local server. This chapter is the introduction; chapters 29–31 are the complete object reference, and chapter 28 documents every event script in depth.

COM is Windows. It is not part of the Linux build, and neither are VBScript and JScript event scripts — their engine is Windows Active Scripting; since 6.3.4 a Linux server runs event scripts in JavaScript instead (§28.2). On Linux every automation example in this chapter and in chapters 29–31 is replaced by the REST API (chapter 24), which since 6.3.0 writes most of what COM writes, or by hmctl, the command-line client over it that the Linux packages have installed since 6.3.4. Chapter 35 covers what that does and does not reach.

From PowerShell

$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
   }
}

Call Save(). Add() returns a new, unsaved object, and a script that forgets Save() looks as though it worked and changes nothing. Since 6.3.4 the same holds for a property set on an existing route, incoming relay, DNS blacklist, SURBL server, remote domain policy, server message, blocked attachment, SSL certificate or group: it is written on the script’s own copy and takes effect at Save(), where up to 6.3.3 it changed the running server at once, saved or not. A script that relied on that must now call Save().

Authorization note. In 6.2.10 a real defect was fixed here: fifteen COM methods returned S_OK when they had actually refused the call, because they returned false from a function whose return type is HRESULT — and false is 0, which is S_OK. Five of them returned before writing their out-parameter, so an unauthorized caller got “success” plus uninitialized memory. If you script against the COM API, upgrade to 6.2.10 or later and make sure your scripts check return values. Since 6.3.4 a refusal also says why: up to 6.3.3, 72 COM classes answered one with a bare HRESULT (0x800403E9), and each now carries the server’s own sentence. Authenticate no longer accepts an account’s app password as a logon (up to 6.3.3 it did, past any second factor, which let a domain administrator who issued an app password on the server administrator’s mailbox sign in as the server administrator); an app password still opens the mailbox over IMAP, POP3 and SMTP.

Event scripts

Event scripts are VBScript or JScript on Windows and, since 6.3.4, JavaScript on Linux. Scripting is off as shipped. In the Control Panel, switch it on and choose the language on the Scripting tab of Maintenance → Advanced, and edit the script itself on Maintenance → Event scripts (local server only). The server runs the script at fifteen defined points in the mail flow; the main ones are below, and chapter 28 has them all:

Event Fires when
OnClientConnect A client connects
OnAcceptMessage A message has been received and is about to be queued (the usual place for a script to refuse it)
OnDeliverMessage A message is about to be delivered
OnDeliveryFailed / OnDeliveryStart A delivery attempt starts, before virus scanning (OnDeliveryStart); delivery to a recipient has failed, once for each such recipient (OnDeliveryFailed)
OnError An error is logged
OnExternalAccountDownload Mail is fetched from an external account
Event scripts
Event scripts
Sub OnAcceptMessage(oClient, oMessage)
   If InStr(LCase(oMessage.Subject), "urgent invoice") > 0 Then
      oMessage.HeaderValue("X-Suspicious") = "possible-invoice-fraud"
      oMessage.Save
   End If
End Sub

Event scripts run inside the mail flow. A slow script slows every message behind it, and one that hangs holds them until ScriptTimeout (60 seconds by default) interrupts it; on Windows the interrupt cannot release a handler blocked inside a COM call, which holds them for as long as that call takes. A script that raises an error is reported, and the event keeps whatever Result it had, which for most events means carrying on. Keep them short, and test on a non-production server.

Upgrade a server that runs event scripts to 6.3.4 or later. In every release from 6.0.0 to 6.3.3, on a Windows server scripting in JScript with an OnClientValidatePassword handler, a password could run code inside the server before the client had signed in; OnExternalAccountDownload and OnDeliveryFailed were open the same way, and so was a rule’s Run function action in either language (CVE-2026-100741, §28.2). Until you can upgrade, switch event scripting off. Since 6.3.4 a Run function action must name a plain function (ASCII letters, digits, _ and $, not starting with a digit); any other name runs nothing, and on Windows is reported as HM5018 each time the rule matches.


24. The REST API

The REST API is an HTTP interface to the running server. It was a small read-mostly surface until 6.3.0. It is now the interface the server is administered through wherever there is no Control Panel and no COM — which is to say, on Linux — and it is the engine underneath the two pages the server itself serves, the Control Deck at / and the webmail portal at /portal.

The scale, before the detail. At 6.3.4 build/check-deck-parity.py counts 586 method-and-path operations, 258 of them writes (hmailserver/docs/DeckParity.md). The tables below were written for a smaller API and do not list every one of them; the server describes the /api/v1/ routes itself at GET /api/v1/openapi.json, which is the list to trust.

What the API cannot write matters as much as what it can. Up to 6.3.2 no route wrote a domain’s DKIM signing configuration, and on Linux, which has no Control Panel and no COM, nothing did. Since 6.3.3 PUT /api/v1/domains/{domain} writes it, with the rest of the domain, and at 6.3.4 the project’s census finds a route for every property the Control Panel writes but an account’s eight directory fields, which PUT /api/v1/accounts/{address}/directory writes under names the census does not match. Section 24.15 has the detail. COM remains the richer interface on Windows; the REST API is narrower, but it is reachable from any machine and any language with an HTTP client, and it is the only one of the two that exists on Linux. Chapter 35 covers the Linux build, chapter 36 the Deck and the portal.

24.1 Turning the API on

The API is off in a default installation on both platforms. Four keys in the [Settings] section of hMailServer.ini control it, and only the first has to be set. Since 6.3.4 that section is the database’s copy (chapter 21): once the server has started, set them in the Control Panel (Monitoring & troubleshooting → API & monitoring), over COM with Settings.SetIniSetting or, on Linux, with sudo hmailserver --set-setting RestApiPort=8443 and a reload, because a line edited in the file afterwards is ignored in favour of the stored value, named in the error log as HM5804 and, where the service may write the file, put back. A [SettingsOverride] section of the same file, applied over the store at every start and reported as HM5807 each time, is the other way through. The two examples below are the file as edited before the server’s first start.

INI key Default Effect
RestApiPort 0 0 is off; any positive port starts the listener. The only switch there is — a server upgraded without touching its settings behaves exactly as 6.2.28 did.
RestApiBindAddress 127.0.0.1 The address the listener binds.
RestApiCertificateFile (empty) PEM certificate, for HTTPS.
RestApiPrivateKeyFile (empty) The matching private key.

Loopback only, which needs no certificate; and reachable from elsewhere, which must have one:

[Settings]
RestApiPort=8443
RestApiBindAddress=127.0.0.1
[Settings]
RestApiPort=8443
RestApiBindAddress=0.0.0.0
RestApiCertificateFile=/etc/hmailserver/certs/fullchain.pem
RestApiPrivateKeyFile=/etc/hmailserver/certs/privkey.pem

What the server refuses at start

Four messages begin “RestApi: Refusing to start”, in three situations. Each writes one line to the application log and leaves the rest of the server running.

  1. An empty administrator password disables the API entirely — “RestApi: Refusing to start – the administrator password is not set.” On Linux, set it with sudo hmailserver --set-admin-password first. On Windows, set it on the Control Panel’s Sign-in & access → Administrative access page where the service may write hMailServer.ini; a new 6.3.4 installation runs the service as NT SERVICE\hMailServer, which may not, and there the page refuses the change: run hMailServer.exe --set-admin-password from an elevated prompt instead, the password in HM_ADMIN_PASSWORD or on standard input and never as an argument, then restart the service.
  2. TLS is mandatory off loopback. TLS counts as configured only when both the certificate and the key are set. If it is not, and the bind address is not exactly 127.0.0.1, localhost or ::1, the listener refuses to start: “RestApi: Refusing to start – TLS certificate is required unless bound to 127.0.0.1 or ::1. Set RestApiCertificateFile and RestApiPrivateKeyFile.” Those three literals are matched exactly: 127.0.0.2, an IPv4-mapped form and a 127/8 range are none of them exempt. It is a security gate, and widening it is a separate decision.
  3. A certificate the shared TLS configuration will not take. Two of the four messages: one when the shared configuration could not be applied to the configured certificate, one when building the context threw. The specific cause — an unreadable file, a key that does not match — is reported as HM5113.

If RestApiCertificateFile is empty the server looks for fullchain.pem and privkey.pem in the ACME certificate directory and uses them when both exist, so a server already renewing through ACME gets an HTTPS API without a second copy of the paths.

The listener now goes through the same TLS initialisation as the mail protocols, so the cipher list, the protocol toggles, the DH parameters and the TlsKeyExchangeGroups post-quantum hybrids all apply to it. Before 6.3.0 it built its own context and took OpenSSL’s defaults, so an administrator who configured post-quantum groups got classical-only key exchange with nothing saying so. One thing is deliberately not shared: a TLS 1.2 floor, applied afterwards so it can only tighten, and not configurable. Chapter 17 covers the rest.

Apply the change, then ask the server how it is. Since 6.3.4 all four keys wait for a reinitialise rather than a restart: the Control Panel’s Apply now on Windows, sudo systemctl reload hmailserver on Linux, or Application.Reinitialize over COM; a service restart does it too. The first configuration above has no certificate, so that listener speaks plain HTTP and says so at startup — “RestApi: Listening on 127.0.0.1:8443 (http, loopback only).”:

curl -s -u Administrator:your-admin-password http://127.0.0.1:8443/api/v1/status
{"version":"6.3.8","state":3,"processedMessages":18422,"spamMessages":903,
 "virusesRemoved":4,"sessions":{"smtp":2,"imap":11,"pop3":0},
 "build":{"script_engine":true,"script_languages":["VBScript","JScript"],...}}

state is a number, not a word: 0 unknown, 1 stopped, 2 starting, 3 running, 4 stopping. Once the listener has a certificate the scheme becomes https, and the rest of this chapter uses it. Reach for curl -k only while the certificate is self-signed, and drop it the moment there is a real one: a client that ignores certificates on loopback and then keeps the habit over a network has thrown the gate away.

On Linux this listener is the administration interface — chapter 35. The Deck is a file the package installs at /usr/share/hmailserver/WebAdmin/index.html and the server serves at GET /; if it is missing the server serves a built-in stub saying so, and hmailserver --check-config will not warn you, because that page is not one of the paths it prints. The portal, by contrast, is compiled into the binary. §36.2 is this same switch seen from the two pages’ side, and says what turning it on exposes.

24.2 Authentication

Four credentials exist. Which one a request carries is decided once, before routing.

Credential Presented as Reaches
Administrator password Authorization: Basic, user Administrator; plus X-hMailServer-OTP once a second factor is enrolled Everything except /api/v1/me and, since 6.3.4, the routes only an API key reaches: /api/v1/send and SCIM at /scim/v2. Nothing narrows it.
API key Authorization: Bearer <token> What its Scope, since 6.3.4 its role, and its optional Domains list allow; tried first when present.
Account credential Authorization: Basic, the mailbox address as user name /api/v1/me/* and the two /api/v1/session routes. Nothing else, unless (since 6.3.4) the account holds an administrator role, by its admin level or an assignment, and the password is its own rather than an app password.
Session cookie Cookie: hmailsession=<token> Whatever the credential that minted it reaches.

A Basic user name of administrator, compared without regard to case, is tried against the administrator password and nothing else; any other user name is treated as a mailbox address and is never tried against it.

Since 6.3.4 the credentials overlap in places. An account that holds an administrator role (serveradmin, domainadmin, helpdesk, auditor or a custom one, by assignment or, for the first two, through its admin level Server or Domain) reaches, with its own address and password or its session cookie, the administrative routes its role’s permissions allow, in the domains the role reaches; an app password or a bearer session never carries the role. An API key holds a role too; a key without one is what it always was, Scope=full acting as serveradmin and Scope=readonly as auditor, still confined by its Domains list. A key is the only credential POST /api/v1/send takes (a key that names a sender and whose role holds send_through_api) and the only one /scim/v2 takes (a key whose role holds provision). A one-time recovery code travels in X-hMailServer-Recovery-Code wherever the authenticator’s code is asked for. A session can also be minted from a passkey or a one-use OpenID Connect or SAML ticket, and POST /api/v1/session?bearer=1 answers an account’s session as a token in the body, which a page holding a second account sends as Authorization: Bearer; that is never minted for the administrator. hmailserver/docs/Roles.md has the permissions.

Sessions

New in 6.2.28 for an account’s own sign-in (the portal) and in 6.3.0 for the administrator (the Control Deck), so that a browser page need not hold a password. POST /api/v1/session takes HTTP Basic once — an account’s address and password, or Administrator and the administrator password, with X-hMailServer-OTP when a second factor is enrolled — and answers 201 with:

Set-Cookie: hmailsession=<64 hex chars>; Path=/; HttpOnly; SameSite=Strict; Max-Age=43200

; Secure is appended whenever the browser came over HTTPS: when the listener speaks TLS, which is everywhere but loopback, and, since 6.3.4, when a reverse proxy on this machine in front of the loopback listener says X-Forwarded-Proto: https. A proxy that terminates HTTPS should set that header; one that does not leaves the cookie without Secure, and a browser would then send it over plain HTTP too. From 6.3.6 the plain loopback port answers only a request whose Host is localhost, an IP address, the server’s host name or the host of an address configured as the one the webmail is reached at from outside, and refuses any other with 421 before authentication, so that a web page that points a name of its own at this machine cannot reach the console through a browser on it: a proxy that forwards the browser’s own Host needs that name set first, as the webmail’s outside address or as the server’s host name, and must pass the Control Deck’s page through unchanged, since its script policy names the SHA-256 of the page’s one inline script. A wrong administrator password now costs a growing delay, up to ten seconds, from every source including the loopback address, so behind a proxy on the same machine one client’s wrong guesses keep the password sign-in waiting for everyone while they last; API keys, passkeys and single sign-on are not touched by it.

  • The token is 32 random bytes in lower-case hex, and only its SHA-256 is kept in memory, so reading the session table gives an attacker nothing usable.
  • Two lifetimes, both enforced on the server: by default 30 minutes idle, 12 hours absolute. Since 6.3.4 they are session policies, set server-wide or per domain (session_idle_seconds and session_lifetime_seconds, at most thirty days each; 0 means not set, and the defaults apply), beside two switches: one session per device, and a session bound to the address it began on. The policy is re-read at every request, and an account’s session is listed among its devices, where the account or the administrator can revoke it. Max-Age carries only the absolute ceiling.
  • At most 1,000 live sessions; expired ones are swept first, then the least recently used is evicted.
  • Sessions live in process memory only, so a restart invalidates every one — including a restart through POST /api/v1/server/reinitialize.
  • The session ends when the password behind it changes. Every request on an administrator session re-derives a stamp from the administrator credential and compares it in constant time with the stamp taken at sign-in; a different one, or an empty password, revokes every administrator session at once. Per account the same: a password change ends the account’s other sessions while keeping the one that made it, and an account deactivated or deleted mid-session is refused from its next request; since 6.3.4 so is a session whose domain has been switched off.
  • An API key cannot mint a session (403), and a session cannot mint another. DELETE /api/v1/session ends the session it came with; a request that carried a password gets 400, there being nothing to end.

Every write made with a session cookie must carry X-Requested-With: hMailServer, compared case-sensitively. The check runs before authentication is even tested; without it the answer is 403, “a request that changes something must carry X-Requested-With: hMailServer when it is authenticated by a session cookie”. That closes cross-site request forgery twice over: SameSite=Strict stops the cookie travelling with another site’s request at all, and no other origin could add that header without a preflight this server never grants. A client using Basic, or an API key as Bearer, needs no such header — there is no cookie to abuse; a session in the bearer form (since 6.3.4, POST /api/v1/session?bearer=1) must carry it to change anything, as the cookie must. §36.3 shows the same exchange as the Control Deck and the portal perform it, including what a second factor adds.

What each credential can and cannot do

Authorisation is decided in exactly one place, on the route’s identity rather than on a path string, before any handler runs — so an endpoint that forgets to check something cannot exist, because no endpoint does the checking. In order:

  1. The session routes: account or administrator only; an API key gets 403.
  2. The self-service routes, everything under /api/v1/me: an account credential only. The administrator password and API keys are refused 403 — “this endpoint answers to an account’s own credentials, not to the administrator password or an api key” — there being no mailbox behind either.
  3. An account credential anywhere else: 403, “an account’s credentials reach only the account’s own endpoints under /api/v1/me” — unless, since 6.3.4, the account holds an administrator role, by its admin level or an assignment, and the request came with its own password or its session cookie rather than an app password or a bearer session. Then the role’s permissions decide, in the domains it reaches, and a route it may not call answers 403 naming the role and the permission it lacks: “the role ‘helpdesk’ does not hold the permission ‘manage_settings'”.
  4. The administrator credential: allowed, always, except on the routes only an API key reaches since 6.3.4 (/api/v1/send and /scim/v2). Including through a cookie, which is why an administrator session reaches /api/v1/apikeys.
  5. An API key under /api/v1/apikeys, or on another route no API key may call: 401, not 403 — the API answering misleadingly on purpose, so that a key probing these routes learns neither “valid but not permitted” from “not a key at all” nor whether a verb exists there. The other routes are /api/v1/settings/ini (since 6.3.3) and, since 6.3.4, the audit trail, the alerts, the 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, whatever the key’s scope or role. The key store, the ini routes, /api/v1/settings/changed and /api/v1/server/settings/pending answer to the administrator password alone; the audit trail, the alerts, the webhooks and the review also answer to an account whose role holds the permission.
  6. A read-only key on a route that changes something: 403, “this api key is read-only”. Mutating is a property of the route, not the HTTP method, so a route that changed state under a GET could not slip past.
  7. A key with a Domains list: see 24.3.

Scope and Domains fail closed: an absent or unrecognised scope leaves the key read-only, and a Domains entry that is not a real domain matches nothing rather than everything.

At the front door there is a rate limit of 200 requests per 10 seconds per credential — per credential, not per source address, so one leaked key cannot spend the listener’s capacity by rotating addresses — answered 429 with Retry-After. Failed credentials feed the auto-ban exactly as the SMTP, IMAP and POP3 front ends do. 401 responses conceal everything: no credential, a wrong password, an unknown key, an expired key and a key refused by source address answer identically, the sole exception being X-hMailServer-OTP: required when the password was right and the one-time code was missing or wrong. And WWW-Authenticate: Basic is suppressed for a page’s own fetch, so the Deck and the portal show their own message instead of the browser’s credential box.

Since 6.3.4 a one-time code is taken once, over REST and COM alike: the same code, or one from an earlier step, is refused with that 401 and not counted as a wrong one. A script that sent the administrator password and a code with every request should sign in once with POST /api/v1/session and use the cookie from there, or use an API key; otherwise only its first request in each half minute is accepted. After five wrong codes in fifteen minutes every code for that account, or for the administrator, is refused with 429 for fifteen minutes, even behind a reverse proxy on this machine, which the auto-ban does not reach; on the administrator’s Basic sign-in the answer carries code second_factor_locked and Retry-After.

24.3 Conventions

  • The path is versioned. Everything is under /api/v1/ but the pages and what they load (the three page endpoints, the webmail’s language catalogues and scripts and, since 6.3.4, the Deck’s catalogues, the sign-in, reset and reading pages and the list manager’s pages) and, since 6.3.4, SCIM at /scim/v2, a provisioning API of its own.
  • JSON in, JSON out. Every response is Content-Type: application/json with Cache-Control: no-store — except the attachment download and the three pages, which set their own media type and are no-store as well, and the other pages and scripts served beside them and the downloads that are not JSON, such as a message’s raw source, a folder’s export, an account’s export as a zip and a report asked for with format=csv.
  • Status codes are conventional: 200 read or applied change, 201 created, 202 accepted but not yet done — server/reinitialize and POST /api/v1/backup and, since 6.3.4, routes that queue their work, among them POST /api/v1/password-reset, an eDiscovery search or export and POST /api/v1/store/measure — 400 a body the server will not accept, 401 a credential it will not accept, 403 a credential it accepts but will not permit here, 404 an unmatched path or unknown id, 409 a conflict with what already exists or with the server’s state, 413 over the size limit, 429 over the rate limit.
  • The error shape is one field, {"error":"..."}, to which a few 6.3.4 answers add a code (second_factor_locked, support_session, mailbox_key), and which SCIM at /scim/v2 replaces with SCIM’s own; an unmatched method-and-path pair answers 404 {"error":"not found"}. No stack trace, no SQL, no file path, and 403 responses say why in fixed sentences that never name a file, a query, a row or another domain.
  • Read-only keys are refused by route, not by verb. POST /api/v1/update/check is a write because it changes the recorded verdict and makes the server call out, so a read-only key cannot make it.
  • Writes are all or nothing. Most visible on the settings groups, where a request naming twenty keys applies twenty or none, but it is the general habit.
  • Request size. POST /api/v1/me/messages and POST /api/v1/me/drafts are allowed a body of up to sixteen megabytes. Since 6.3.4 PUT /api/v1/me/settings is a large request too, like sending a message, so that a formatted signature with its images is refused by the route’s own limit of 64,000 bytes rather than by the listener’s cap. Every other request is held to 64 KB unless its route is granted more: since 6.3.4 the SAML sign-in’s POST /portal/saml/acs takes up to 256 KB.
  • One matching quirk. Prefix and suffix comparisons are case-insensitive in this codebase while exact ones are not, and the router preserves that deliberately. Rely on neither; send paths as written here.

Domain scoping

A key may carry a Domains list. What that does depends on the route, in three cases.

  1. Refused outright. The delivery queue and the quarantine are refused entirely rather than filtered, each in its own sentence — “the delivery queue is server-wide” and “the quarantine is server-wide” — and, since 6.3.4, so are the volume and storage reports, “that report is server-wide”. One queued message names recipients in any number of domains, and the listing shows the sender and every recipient of all of them, so there is no honest way to narrow it.
  2. Refused as server-wide. Domain create and delete, every IP-range route, certificate list, create and delete, every port route, every SMTP-route route, every global rule route (since 6.3.4 an account’s and a domain’s own rules are checked against the list instead), the log list and tail, backup start and status, every settings-group route, server/reinitialize, all four update routes and, since 6.3.4, the account-group (/api/v1/groups) and remote-domain routes: 403, “that resource is server-wide”. The other server-wide routes 6.3.4 adds, the warm-up schedules, the message store and its measurement, maintenance mode and the diagnostics among them, are refused to such a key too.
  3. Checked against the list. Account list and create, domain update, alias list and create, distribution-list list and create and the DKIM read take the domain from the path. Account delete, account update, alias delete and list delete take it from the address — which is the whole reason the mechanism exists, since otherwise a key for one domain could delete an account in another by editing one path segment.

Everything else falls through to allowed and is filtered inside the handler: the domain listing and /api/v1/srv drop domains the key is not entitled to, the archive routes refuse an unnamed or wrong domain with their own 403, and, since 6.3.4, a report asked for without a domain is confined to the key’s own domains. Every route table below uses one vocabulary; a cell lists every credential the route accepts, and anything not listed is refused. Since 6.3.4 an account that holds an administrator role, and a key’s role, reach a route when the role holds its permission; the tables do not show that.

Token Meaning
admin The administrator password over Basic, or the cookie minted from it
key-ro Any API key, read-only ones included
key-full An API key only when Scope=full; a read-only key gets 403
key-alldom A key with no Domains list; a domain-restricted key gets 403
key-dom A domain-restricted key is accepted, confined to its own domains
account The mailbox’s own address and password, or the cookie minted from it
key:401 Any API key refused with 401 rather than 403, deliberately
key:403 Any API key refused with 403
none No credential required

24.4 Server and status

Method Path Auth What it does
GET / none The Control Deck page, also served at /index.html; read from WebAdmin/index.html, with a stub when absent
GET /portal none The webmail markup, compiled into the binary; no inline script
GET /portal.js none The portal’s script, separate so the page’s CSP can forbid inline script entirely
GET /api/v1/status admin, key-ro, key-dom version, state (0 unknown, 1 stopped, 2 starting, 3 running, 4 stopping), the processed, spam and virus counts, live session counts per protocol and, since 6.3.3, build: what this build can do, with script_languages since 6.3.4 (VBScript and JScript on Windows, JavaScript on Linux)
POST /api/v1/server/reinitialize admin, key-full, key-alldom Stop every service, reload the configuration, start again in the same process
GET /api/v1/openapi.json admin, key-ro, key-dom The OpenAPI 3.0.3 description of every route (24.17)
GET /api/v1/tlsa admin, key-ro, key-dom Recommended DANE TLSA records (3 1 1) for the configured certificates
GET /api/v1/srv admin, key-ro, key-dom Recommended client-discovery SRV records (RFC 6186 and 8314, plus Outlook _autodiscover) built from the ports actually enabled

Reinitialize answers before it acts — 202 {"reinitializing":true}, then the work on a detached thread half a second later, because the listener carrying the answer is one of the things that stops. Poll GET /api/v1/status to find out when it is back; it holds its own lock, so two callers restart in turn rather than at once. This route is what makes the write surface usable without stopping the process: a new listener, a certificate binding, a settings-group key marked restart (24.5) or a [Settings] key that waits for a reinitialise becomes live once it has run. Since 6.3.4 most saved [Settings] keys apply when they are saved and need no reinitialise. The 28 that do (the REST, metrics, web-services and ManageSieve listeners, their certificates and the mail listeners’ TLS 1.3 ciphers, key-exchange groups and session options among them) are listed, once saved with a value the server is not running on, by GET /api/v1/server/settings/pending until it has run; only the two [Settings] keys marked restart and the bootstrap keys ([Database], [Directories], [Security] and [GUILanguages]) wait for a service restart. A reinitialise also ends maintenance mode: the answer is then {"reinitializing":true,"drained":true}, and "drained":false otherwise.

/tlsa and /srv generate records and publish nothing. /srv emits nothing for a disabled service, a loopback-bound port or port 25 presented as submission, and skips inactive domains, so what it gives back matches the server as it actually runs. Chapter 22 covers the ports.

24.5 Settings

Six routes and three groups — the largest single piece of the write surface 6.3.0 added, and the counts in this section are 6.3.0’s. The groups have grown since, to nine at 6.3.3 and thirteen at 6.3.4, each with a GET and all but the directories group with a PUT; the OpenAPI document (24.17) lists them. Everything known about a setting is one row of one table: its name, its type, whether it can be read or written, when it takes effect, the words an enumerated value may take, its description, and the getter, setter and pre-apply check that reach the same code COM reaches. The GET walks that table, the PUT walks it and the OpenAPI description is generated from it, so the three cannot disagree about a key.

Method Path Auth What it does
GET /api/v1/settings admin, key-ro, key-alldom The 62 readable server-wide settings as one flat object — 63 rows less the write-only relayer password
PUT /api/v1/settings admin, key-full, key-alldom Change any subset of the 63 writable server-wide settings. All or nothing
GET /api/v1/settings/antispam admin, key-ro, key-alldom The 34 anti-spam settings as one flat object
PUT /api/v1/settings/antispam admin, key-full, key-alldom Change any subset of the 34 anti-spam settings. All or nothing
GET /api/v1/settings/logging admin, key-ro, key-alldom The 16 logging keys: 11 settings plus 5 read-only facts about where the log is written
PUT /api/v1/settings/logging admin, key-full, key-alldom Change any subset of the 11 writable logging settings; naming a read-only key is a 400
Group Keys Writable Readable
server 63 63 62
anti-spam 34 34 34
logging 16 11 16
total 113 108 112

113 keys described, 108 written, 112 readable. The gap on each side has exactly one cause. The five read-only logging keys are directory, current_default_log, current_error_log, current_event_log and current_awstats_log — facts about where the log is being written rather than choices anyone makes; a PUT naming one is a 400, “<key> is read-only”. The one write-only key is smtp_relayer_password in the server group: accepted by PUT, emitted by no route at all. Nothing you can call will read a relayer password out of this server. Since 6.3.4 the SNMP agent’s snmp_v3_auth_password, snmp_v3_priv_password and snmp_v2c_community, in the same group, are write-only too.

A write applies only when every key is accepted. A PUT takes any subset of its group’s writable keys and is all or nothing: every member of the body is resolved to a row and a typed value, every row’s own check is run, and only then is anything applied. An unknown key, a wrong type, a read-only key, or a value the setting refuses is a 400 naming the key — or carrying the setting’s own refusal sentence, the one the Control Panel shows — with nothing changed. The three settings whose setters can themselves fail — the IMAP hierarchy delimiter, which is refused while a folder or a rule action still contains the new character, and the two tarpit values, which are [Settings] keys, written to the settings store (since 6.3.4 in the database, with hMailServer.ini as its copy; before it, to the file) — are applied in a first pass, so a refusal there also leaves nothing else applied.

The setters are the ones COM calls. A property write reaches the settings store at once and raises the configuration-change notification, which refreshes the logger, the work queues and the delivery manager: a change made over REST is seen by the running server exactly as one made in the Control Panel is. Each key declares when it takes effect — now, restart (the network thread pool, which services start, the TLS contexts), or stored, meaning written and read by nothing, kept because the Control Panel writes it. Since 6.3.4 the rows kept in the [Settings] store, the two tarpit values and the forwarding rewrite, also carry that store’s own mark, now, reinitialize or restart, as x-applies in the OpenAPI document; that restart waits for a service restart, while a group’s own restart is taken at a service start or a reinitialise (24.4). The PUT also logs the keys and never the values, since a value may be the relayer password, and enumerated values travel as words rather than numbers: connection security is none, starttls_optional, starttls_required or tls, the log device unknown, sql or file, the log format default or csa.

The server group covers identity and defaults, capacity and threading, which services run and what they say, the server-wide relayer including its write-only password, delivery and retry policy, protocol behaviour, nine IMAP keys, four auto-ban keys, eight TLS keys and three conversation-logging toggles. The anti-spam group covers the two score thresholds and the marking options, the SPF, HELO, MX and PTR checks each with its score, DKIM verification, DMARC and ARC, five SpamAssassin keys, tarpitting and six greylisting keys. The logging group is enabled, seven log toggles, keep_files_open, device and log_format, plus the five read-only keys above. Since 6.3.4 the server group also carries, among others, the nine audit-trail and alert settings, which take effect at once, the four session policies of 24.2 (session_idle_seconds and the rest) and twenty-seven more keys (message_store_*, mailbox_key_idle_minutes, pst_import_directory and the SNMP agent’s snmp_*, which are marked restart and read when the agent starts, at a service start or a reinitialise), and the logging group the twelve syslog keys, marked restart in the same sense. The OpenAPI document is the definitive list.

24.6 Domains, accounts and aliases

Method Path Auth What it does
GET /api/v1/domains admin, key-ro, key-dom List domains; a domain-restricted key sees only its own
POST /api/v1/domains admin, key-full, key-alldom Create a domain from name, active and postmaster; every other setting takes the new-domain default
PUT /api/v1/domains/{domain} admin, key-full, key-dom Change a domain: active (required) and any of its other properties — the postmaster, the name (which renames the domain and every address in it), its size and count limits, plus addressing, its signature, its DKIM signing and key rotation, its relay host, its out-of-office and quota texts and the rest the Control Panel’s domain pages hold. The whole domain since 6.3.3 (the key rotation and the quota texts since 6.3.4); before it, only the active flag and the postmaster
DELETE /api/v1/domains/{domain} admin, key-full, key-alldom Delete a domain with its accounts and messages, aliases, lists, domain aliases and directories; since 6.3.4 refused while the domain or any account in it is under an eDiscovery legal hold
GET /api/v1/domains/{domain}/dkim admin, key-ro, key-dom The DKIM signing configuration: enabled, selector, sign_aliases, private_key_file. Read only: a PUT here is a 404. DKIM is written with the rest of the domain, through PUT /api/v1/domains/{domain}
GET /api/v1/domains/{domain}/accounts admin, key-ro, key-dom List the accounts in a domain
POST /api/v1/domains/{domain}/accounts admin, key-full, key-dom Create an account: address and password required, optional active, first_name, last_name, max_size_mb
PUT /api/v1/accounts/{address} admin, key-full, key-dom Update an account: any subset of active, password, max_size_mb, the two name fields, the forwarding fields, the three signature fields and admin_level, and the rest of what the Control Panel’s account dialog writes: the automatic reply, the spam thresholds, message retention, the Sieve script and restricted delivery among them. Since 6.3.4 also kind, which makes the account a room or a piece of equipment. The eight directory fields go to PUT /api/v1/accounts/{address}/directory (24.15)
DELETE /api/v1/accounts/{address} admin, key-full, key-dom Delete an account and its messages; the domain is taken from the address. Since 6.3.4 refused with 409 while an eDiscovery legal hold covers the account
GET /api/v1/domains/{domain}/aliases admin, key-ro, key-dom List the aliases in a domain
POST /api/v1/domains/{domain}/aliases admin, key-full, key-dom Create an alias from name, value and active; in effect for the next message
DELETE /api/v1/aliases/{address} admin, key-full, key-dom Delete an alias, scoped to the address’s domain

Up to 6.3.2 the only domain properties the API wrote were the name at creation, the active flag and the postmaster, so a domain that had to sign its outbound mail with DKIM could not be configured on Linux. Since 6.3.3 PUT /api/v1/domains/{domain} takes every other property a domain has — its DKIM signing configuration, its size limits, its signature and its relay host among them — and the Control Deck edits them, on both platforms (24.15). Since 6.3.4 a name the server keeps its own files under, such as protected, quarantine, files, lists, temp or database, is refused for a new domain and for a rename, over REST as over COM.

Accounts. Create is under the domain, update and delete by address, and the address must belong to the named domain on create; unknown fields on the update are refused by name. admin_level is governed as COM governs it: the administrator password, or a key issued for every domain, may set all three levels and may update an account that is itself a server administrator, while a key restricted to named domains may set user or domain only and may not touch a server administrator at all. Since 6.3.4 one rule covers every route that changes an account named by its address (update, delete, move, a password reset, a role, and what is in the mailbox): a credential may not change an account whose authority it does not take in, so a key or role confined to a domain is refused, with 403, the server administrator’s own mailbox in that domain, “that account holds authority this credential does not”.

Aliases. Judged as the Control Panel judges one — a name an account or a distribution list already holds, or a domain already at its alias limit, is refused in the same sentence. There is no PUT: an alias is changed by deleting it and creating it again.

24.7 Distribution lists

Method Path Auth What it does
GET /api/v1/domains/{domain}/lists admin, key-ro, key-dom List the distribution lists in a domain, with their members
POST /api/v1/domains/{domain}/lists admin, key-full, key-dom Create a list: address and members, whether it is active, its mode (public, membership, announcement or domain_members), require_auth, the sender it requires, the moderator and the bounce address; since 6.3.4 also its membership query and the list manager’s fields
PUT /api/v1/lists/{address} admin, key-full, key-dom Change any of those fields but the members, scoped to the address’s domain. Since 6.3.3
DELETE /api/v1/lists/{address} admin, key-full, key-dom Delete a distribution list, scoped to the address’s domain
GET /api/v1/lists/{address}/members admin, key-ro, key-dom Who the list delivers to now, its membership query resolved, each address marked row or query; 503 when the accounts cannot be read. Since 6.3.4

Every write is domain-scoped. Since 6.3.3 a list is changed in place with PUT /api/v1/lists/{address}, so a list whose mode has to change no longer needs COM or deleting and recreating it; aliases still have no PUT. Since 6.3.4 a list may also carry a membership query (membership: domain, domain:, pattern: or everyone, answered against the server’s active accounts when a message is sent), which a key confined to named domains may set only within them, and the list manager’s fields (managed, subscribe_policy, archive, digest and the rest), with routes under /api/v1/lists/{address}/members to approve a request to join and clear a member’s bounces, and a managed list’s archive at GET /api/v1/lists/{address}/archive. Chapter 13 covers lists themselves.

24.8 Rules and routes

Method Path Auth What it does
GET /api/v1/rules admin, key-ro, key-alldom List the global rules with their criteria and actions
POST /api/v1/rules admin, key-full, key-alldom Create a global rule: name, active, all_criteria, criteria[] and actions[], in the order they run
PUT /api/v1/rules/{id} admin, key-full, key-alldom Replace a rule whole, keeping its place in the order
DELETE /api/v1/rules/{id} admin, key-full, key-alldom Delete a global rule with its criteria and actions
POST /api/v1/rules/match admin, key-ro, key-alldom Try one criterion (match_value, match_type, test_value) and answer what delivery would decide; nothing is read or written. Since 6.3.4
GET /api/v1/routes admin, key-ro, key-alldom List the SMTP routes the server delivers by; the relay password is never returned
POST /api/v1/routes admin, key-full, key-alldom Create an SMTP route: domain_name and target_smtp_host required, plus port, retry policy, relayer authentication, the local-domain flags, all_addresses, addresses[] and connection_security
PUT /api/v1/routes/{id} admin, key-full, key-alldom Replace a route whole; the address list is replaced by addresses
DELETE /api/v1/routes/{id} admin, key-full, key-alldom Delete an SMTP route and its addresses

Rules. Criteria fields are from, to, cc, subject, body, message_size, recipient_list, delivery_attempts and header — the header’s name goes in the header field — and, since 6.3.4, attachment_name, sensitive_content, attachment_type, external_recipient and content. Matches are equals, not_equals, contains, not_contains, less_than, greater_than, regex and wildcard. Action types are delete, forward, reply, move_to_folder, script_function, stop, set_header, send_using_route, copy and bind_to_address, and, since 6.3.4, add_disclaimer (its text in body), reject and warn_sender (their text in value), require_tls and quarantine. A PUT replaces a rule whole — old criteria and actions deleted, new ones created — which is what saving an edited rule in the Control Panel comes to. These are the server’s rules only; an account’s rule is 404 here. Since 6.3.4 a domain’s own rules, which run after the server’s, are at /api/v1/domains/{domain}/rules and an account’s at /api/v1/accounts/{address}/rules, each with /{id} for one rule, in the same documents as the server’s, and a domain-restricted key reaches those of its own domains.

SMTP routes. treat_security_as_local_domain is the COM name for treat_recipient_as_local_domain; both spellings are accepted and both emitted. relayer_auth_password is write-only, and it is the one field a PUT keeps when the body omits it — every other omitted field takes its default. So the safe way to edit a route is to read it, change the one field, and send the whole object back. Since 6.3.4 a policy for one remote domain — TLS required on mail to it or from it, size and rate limits, replies and forwards, the recipient callout — is a remote domain policy rather than a route, at /api/v1/remote-domains (chapter 14).

24.9 Certificates and listeners

Method Path Auth What it does
GET /api/v1/certificates admin, key-ro, key-alldom List the SSL certificates: names and file paths, never a private-key password
POST /api/v1/certificates admin, key-full, key-alldom Add a certificate by name and the PEM certificate and key paths, with an optional write-only private_key_password; both files must exist or the request is refused naming the missing one
DELETE /api/v1/certificates/{id} admin, key-full, key-alldom Delete a certificate; refused while a port binds it, and the refusal names the port
GET /api/v1/ports admin, key-ro, key-alldom List every listener: id, protocol, address, port, connection_security, certificate_id, client_certificate_policy, CA file
POST /api/v1/ports admin, key-full, key-alldom Add a TCP/IP port; tls and both STARTTLS values need a certificate_id. Takes effect at the next reinitialise or service restart
PUT /api/v1/ports/{id} admin, key-full, key-alldom Replace a listener whole — an omitted field takes its default, so send back what GET returned with the change made
DELETE /api/v1/ports/{id} admin, key-full, key-alldom Delete a listener; the row goes at once, the listener stays up until the next reinitialise or service restart

Certificate paths are checked for existence at create time and refused with the file named, rather than saved for a listener to fail on at the next start. That matters more than it sounds on Linux, and since 6.3.4 on a new Windows installation, whose service runs as NT SERVICE\hMailServer: the certificate is read by the service’s own account, so a path readable to the administrator’s shell may not be readable to the server. On Windows, move a file outside the folders that account is granted into Data\Certificates, or approve its folder for Read (hMailServer.exe --approve-read "<folder>" /GrantServiceAccess, run elevated). A port written here takes effect when the server restarts — or at once after POST /api/v1/server/reinitialize, the pairing that makes this write surface usable without stopping the process. Chapter 22 covers what the ports are for, chapter 17 which should be exposed.

24.10 IP ranges

Method Path Auth What it does
GET /api/v1/ipranges admin, key-ro, key-alldom List the IP ranges
POST /api/v1/ipranges admin, key-full, key-alldom Create a range: name, lower, upper, priority, the three protocol flags, the four relay flags, the four SMTP-auth requirements, require_tls_for_auth, spam_protection, virus_protection, expires and expires_time
PUT /api/v1/ipranges/{id} admin, key-full, key-alldom Change a range in place: any subset of the create’s fields. Since 6.3.3
DELETE /api/v1/ipranges/{id} admin, key-full, key-alldom Delete an IP range

Server-wide. Since 6.3.3 a range is changed in place with PUT /api/v1/ipranges/{id}, which takes any subset of the create’s fields, so there is no need to delete a range and create it again to change it. If you do replace one, create the new range before deleting the old: deleting the range that permits your own relaying and then failing to create its replacement is how a working server stops accepting mail from its own network, and no route restores the defaults — there is no equivalent of the Control Panel’s reset. Chapter 17 covers what the ranges should say.

24.11 Queue, quarantine and archive

Method Path Auth What it does
GET /api/v1/queue admin, key-ro, key-alldom List the delivery queue: id, created, from, recipients, next_try, locked, tries
POST /api/v1/queue/{id}/retry admin, key-full, key-alldom Retry a queued message now; an unknown id is a 404, not a reported success
DELETE /api/v1/queue/{id} admin, key-full, key-alldom Remove a message from the delivery queue
GET /api/v1/quarantine admin, key-ro, key-alldom List quarantined messages, bounded to the newest 1,000
POST /api/v1/quarantine/{id}/release admin, key-full, key-alldom Release a quarantined message to its original recipients
DELETE /api/v1/quarantine/{id} admin, key-full, key-alldom Delete a quarantined message
GET /api/v1/archive admin, key-ro, key-dom Search the archive index by domain, mailbox, sender, recipient, subject, since, until, hold and limit
GET /api/v1/archive/{id} admin, key-ro, key-dom One archive entry
POST /api/v1/archive/{id}/hold admin, key-full, key-dom Put an archived copy on legal hold: never removed by the retention sweep or an address erasure
DELETE /api/v1/archive/{id}/hold admin, key-full, key-dom Lift the legal hold on an archived copy

The queue and the quarantine are refused outright to a domain-restricted key — refused, not narrowed — for the reason given in 24.3. A quarantine release delivers directly rather than back through the filters, because a release is an administrator overruling them, and sending the message back through them would simply quarantine it again. The account’s own quarantine is a separate surface at /api/v1/me/quarantine, showing only the entries that one address is a recipient of and not the other recipients.

The archive routes enforce their domain restriction in the handler rather than at the authorisation choke point, as the 6.3.4 report routes also do, because there the domain is a query parameter or a property of the row. A domain-restricted key must name one of its own domains in domain= on the search, and is refused an entry whose domain is not one of its own.

24.12 Logs, metrics, backup and updates

Method Path Auth What it does
GET /api/v1/logs admin, key-ro, key-alldom List the log files with size and creation time
GET /api/v1/logs/{name} admin, key-ro, key-alldom The last lines of one log file; lines= defaults to 200, at most 2,000
GET /api/v1/metrics/history admin, key-ro, key-dom The recorded history of one metric; metric and range (24h, 7d, 30d). Empty when MetricsHistoryDays is 0
GET /api/v1/backup admin, key-ro, key-alldom The backup manager’s status text and the last lines of the backup log
POST /api/v1/backup admin, key-full, key-alldom Start a backup with the configured settings; it runs on the maintenance queue, so poll the GET
GET /api/v1/update admin, key-ro, key-alldom The update check’s recorded verdict (state 0-5, availableVersion, releaseName, publishedAt, releaseUrl, installer). Fetches nothing itself
POST /api/v1/update/check admin, key-full, key-alldom Read the release feed now and return the verdict. Since 6.3.4 the default feed is https://updates.progressiverobot.com; a server up to 6.3.3 reads the former GitHub repository’s, which has answered 404 since 18 September 2026, so its check fails until UpdateFeedUrl names https://updates.progressiverobot.com/releases.json and the service has been restarted
POST /api/v1/update/download admin, key-full, key-alldom Download the newer installer and its Sigstore bundle and verify it. Nothing is run
POST /api/v1/update/install admin, key-full, key-alldom Re-verify the installer, fetch the running version as a rollback image, hand both to the update helper. The service stops and starts. Since 6.3.4 it enters maintenance mode first (listeners closed, delivery paused), refuses with “already draining” while another drain is in progress, and on Linux hands the package to a root helper

Log names are strictly checked. A name is admitted only if it ends .log, is at most 128 characters, contains no .., has no leading dot, and is drawn from letters, digits, underscore, hyphen and dot — no path separators at all. The list route is the only place a client should get a name from.

Metrics. metric is a name from the Prometheus exporter without its hmailserver_ prefix, and range sets the bucket as well as the window: 24h averaged per minute, 7d per ten minutes, 30d per hour. Counters are totals; a rate is the difference between two samples.

Backups, since 6.3.4. GET /api/v1/backup/archives lists the archives in the backup destination and GET /api/v1/backup/{archive}/mailboxes/{address} one account’s folders and messages as an archive holds them; a POST to its /restore puts back one mailbox or one message, merged or replacing, followed at GET /api/v1/backup/restore-mailbox/{job}, and POST /api/v1/backup/restore restores a whole archive. All of them are server-wide.

All three update POSTs are classed as writes, so a read-only key is refused all three: a check changes the recorded verdict and makes the server call out, a download writes a file the next step will run, an install runs it. The download verifies the installer against its Sigstore bundle — the digest, the certificate chain as of the transparency-log entry, the release workflow’s identity, the signature and the log’s own signature — and a file that fails any of those is deleted rather than kept. The digest it checks is the one the release feed states for the asset it fetched, which is worth knowing from 6.3.1 onwards: the installer published on a release is now the signed file rather than the one the build produced, so it has a different SHA-256 from the build’s. The feed and the download agree; a hash copied out of a build log will not.

6.3.4 carries no Sigstore bundle. It was built and Authenticode-signed by hand, after GitHub suspended the project’s account and before the GitLab pipeline that signs releases existed, so a server of any version that is offered it says it cannot be verified and downloads nothing; it is installed by hand (§18.2). The bundles of the releases up to 6.3.3 name GitHub’s release workflow; from 6.3.5 they name this project’s GitLab CI pipeline: 6.3.5’s at the project’s former path, https://gitlab.com/Progressiverobot/hmailserver//.gitlab-ci.yml@refs/tags/v6.3.5, and from 6.3.6 at the group path the project moved to on 29 September 2026, https://gitlab.com/hmailserver/hmailserver//.gitlab-ci.yml@refs/tags/v<version>. A server from 6.3.4 on trusts all three, the group path only when the certificate carries the project’s id, 86730559. A 6.3.4 server installs a later release without a rollback image: the rollback is the running version’s own installer, fetched and verified before the install, and 6.3.4 has no bundle to verify it by. A server from 6.3.4 on also holds a file to its release: a certificate made for another tag is refused, a GitHub-signed file installs only as a release up to 6.3.3, and on Windows the installer’s version resource must name the release it is offered as.

UpdateRequireAuthenticode, and what 6.3.1 changed about it. The key is a [Settings] key, has existed since 6.2.28 and defaults to 0. Since 6.3.4 it is stored in the database: set it in the Control Panel, over REST (/api/v1/settings/ini/UpdateRequireAuthenticode) or over COM, not in hMailServer.ini, where an edited line is ignored in favour of the stored value and reported as HM5804. Set to 1 it adds a WinVerifyTrust check on the downloaded installer, run after the Sigstore verification and before the file is moved into place; a file that fails is deleted and the reason is reported in the verdict, so POST /api/v1/update/download answers state 5 rather than state 3. Until 6.3.1 the setting was unusable: no release of this project carried an Authenticode signature, so turning it on refused every one of them. The Windows installer has been Authenticode-signed since 6.3.1 — signed with Azure Artifact Signing against a certificate profile issued to Progressive Robot Ltd after Microsoft’s identity validation, with an RFC 3161 countersignature — so the check now has something to find. Three things to know before turning it on. Releases before 6.3.1 are unsigned and always will be, a published release being immutable, so the check refuses every one of them — and the rollback image an install fetches goes through the same download, so on a server still running a pre-6.3.1 version the install proceeds without one and says so in the log: if the new version does not start, that machine needs a reinstall by hand. The check is Windows-only: on Linux there is no Authenticode to verify, and the server says so rather than reporting a pass — “Authenticode signatures can only be checked on Windows” — which is the honest answer and, up to 6.3.3, also the reason updating a Linux installation was the package manager’s job rather than this API’s; since 6.3.4 this route applies a signed package there through a root helper (§35.13). And the signature changes nothing about SmartScreen, which still flags a new installer as unrecognised; it is the machine-checkable signature this setting wants, not a reputation.

A defect that affected this route before 6.3.2. The installer it hands over moves the database schema, and the 6029-to-6030 upgrade step has an ordering fault in the sweep that clears orphaned rows before it adds seventeen foreign keys: children are cleaned before the parents whose deletion re-orphans them, so the constraint that follows is refused. It fires only on an upgrade from a schema below 6030 — a database not yet taken through 6.2.25 — and only where orphaned rows are already present; a database without them upgrades correctly, and a fresh installation is never on that path. When it does fire it fails loudly with the database engine’s own words and rolls back rather than doing anything quietly, it affects all four database backends, and no installation has reported hitting it. The fix shipped in 6.3.2. A self-update is on exactly the same path as a hand-run installer, so read chapter 18 before calling this route on an old database.

24.13 API keys

Method Path Auth What it does
GET /api/v1/apikeys admin, key:401 List the API keys. Administrator credential only — a key that could read the store is a step towards minting one
POST /api/v1/apikeys admin, key:401 Create a key from label, expires, allowed_from, scope and domains, and since 6.3.4 role, send_from, send_per_minute and send_per_day. The clear-text token exists once, in this 201
DELETE /api/v1/apikeys/{id} admin, key:401 Revoke a key. Administrator credential only, so a key cannot lock the administrator out

Three routes and a deliberate catch-all: any other verb under /api/v1/apikeys is treated as part of the same administrator-only set, so a key probing the prefix cannot learn whether a verb exists there. The store is hMailServerApiKeys.ini, beside hMailServer.ini, re-read on every authentication attempt, so a key added or revoked takes effect with no restart. Only the SHA-256 of a token is ever stored — losing the 201 means minting a new key. A record carries a label, an expiry date (90 days after creation when the request names none; there is no “never expires”), allowed_from (an address, a range or a CIDR block; empty means any), scope (readonly by default) and domains (empty means every domain). Both defaults fail safe: a create request naming no scope produces a read-only key, and one naming no expiry a key that stops working after 90 days. A key that does less than you expected is the failure mode; one that does more is not. Since 6.3.4 a key can hold an administrator role (role; a key without one is what it was, full acting as serveradmin and readonly as auditor) and can name the one sender it may send as through POST /api/v1/send (send_from, with send_per_minute and send_per_day ceilings; its role must hold send_through_api). The key store itself still answers to the administrator password alone: not to a key of any role, and not to an account holding serveradmin, which is refused 403.

24.14 The mailbox API

The table is the mailbox’s core: twenty-four operations across nineteen paths under /api/v1/me/, plus the two session operations. This is the largest single group and it is what the webmail at /portal is built on, and it has grown well past the table: 6.3.3 added the webmail’s own routes (preferences, identities, scheduled sending, app passwords, S/MIME and more) and 6.3.4 calendars and tasks, shared address books, the organisation’s directory, other accounts, recovery, devices, OpenPGP and more, all under /api/v1/me/ and all in the OpenAPI document (24.17). The portal also reads routes that need no credential, such as /api/v1/signin-methods and /api/v1/help. Every route under /api/v1/me/ answers to the account’s own credentials and nothing else: the administrator password and API keys are both refused 403, because neither is an account and there is no mailbox behind either whose quota or vacation message could be meant.

Method Path Auth What it does
POST /api/v1/session admin, account, key:403 Exchange a password for the hmailsession cookie; since 6.3.4 also a passkey assertion or a one-use OpenID Connect or SAML ticket, and ?bearer=1 answers an account’s session as a token in the body instead
DELETE /api/v1/session admin, account, key:403 End the session the request came with and clear the cookie
GET /api/v1/me account The account’s own state: address, domain, active, quota, vacation, password_changed, second_factor, directory_linked
POST /api/v1/me/password account Change the account’s own password (current, new); the policy and reuse history apply as when an administrator sets one
PUT /api/v1/me/vacation account Set the automatic reply whole: enabled (required), subject, message, expires, expires_date
GET /api/v1/me/settings account The account’s own name, forwarding and signature settings
PUT /api/v1/me/settings account Change name, forwarding or signature; each one the body names is applied whole, one it does not name is left alone
GET /api/v1/me/filters account The account’s active Sieve script
PUT /api/v1/me/filters account Set the active Sieve script, checked as ManageSieve’s PUTSCRIPT checks it (max 256 KB); an empty script removes the filter
GET /api/v1/me/quarantine account The messages held as suspected spam for this address only, without the other recipients
POST /api/v1/me/quarantine/{id}/release account Deliver a held message to this address only; the entry stays for its other recipients
DELETE /api/v1/me/quarantine/{id} account Give up this address’s copy of a held message; nothing is delivered
GET /api/v1/me/folders account The folder tree as IMAP LIST gives it, plus a shared section for the public namespace and delegated folders; since 6.3.4 each folder also carries uidnext beside uidvalidity
POST /api/v1/me/folders account Create a folder, judged as IMAP CREATE judges it; a name carrying the delimiter creates every missing level
PUT /api/v1/me/folders/{id} account Rename or move a folder, judged as IMAP RENAME judges it; subfolders follow by parent id
DELETE /api/v1/me/folders/{id} account Delete a folder with its subtree; the inbox and any special-use folder are always refused
GET /api/v1/me/folders/{id}/messages account One folder’s messages newest first; limit 1-200, before_uid to page back, q to filter; at most 2,000 message files opened, or about two and a half seconds of reading, per request
GET /api/v1/me/changes account An opaque token hashing, per folder, the message and unseen counts, the folder’s current UID and its name; hand the previous one back as since= and the answer carries changed
GET /api/v1/me/search account Search every readable folder, newest first by message id; q required, limit 1-200, at most 2,000 message files opened or about two and a half seconds of reading per request; each hit names its folder. Since 6.3.4 an answer that is not the end says complete: false with next_before_id, which before_id continues from
POST /api/v1/me/messages account Send as the signed-in account through the same pipeline as SMTP submission; a copy is filed in \Sent. Since 6.3.4 a rule’s reject action refuses it first, 403 with the code policy_rejected, and nothing is queued
GET /api/v1/me/messages/{id} account One message with to, cc, text, html and attachment entries; over one megabyte it is described with truncated true and no body
DELETE /api/v1/me/messages/{id} account Delete one message — to \Trash where the account has one, final otherwise or with ?permanent=1; since 6.3.4, when recover_deleted_days is set (it is off by default), a final delete is kept for that many days and the account can put it back through /api/v1/me/recoverable
PUT /api/v1/me/messages/{id}/flags account Change any of seen, flagged, answered, draft, deleted; only the flags named change, and every IMAP session on the folder is told
POST /api/v1/me/messages/{id}/move account Move a message to another folder of the account, as IMAP MOVE does
GET /api/v1/me/messages/{id}/attachments/{index} account One attachment decoded, under the type the listing reported — except HTML, SVG, XML and script, served as application/octet-stream
POST /api/v1/me/drafts account Save a draft in Drafts, created when the account has none, optionally superseding replace_id; flagged \Draft and \Seen

Folder writes speak IMAP’s own sentences. Create, rename and delete judge a name exactly as CREATE, RENAME and DELETE judge it and refuse in those commands’ own words, so a page and a mail client are never told different things about the same mailbox. Delete is stricter than IMAP on purpose: the inbox is refused however it is reached, and so is any folder the server has designated for a special use, because one mis-click on \Sent in a page with no undo takes every sent message with it. Such a folder is not made undeletable — it stays deletable over IMAP — and a page can tell in advance, because a folder whose special_use is anything but the empty string will be refused.

The change probe is designed not to lose a message. A since that was never this mailbox’s token answers changed: true — “something differs” is the answer that cannot lose anything. The portal polls it every six seconds and stops dead while the tab is hidden. Reading is wider than writing: the public namespace and the folders a delegating owner has shared are readable through the message routes under the rights the owner granted, but they are not this account’s to create, rename or delete.

Inline images, and why the mechanism looks odd. Attachment entries carry content_type and content_id, but the portal cannot point an <img> at the attachment route: the body is rendered in a frame sandboxed without allow-same-origin, so its document has an opaque origin, so the browser counts its subresource requests as cross-site, so the SameSite=Strict cookie never travels, so the route answers 401. The only thing that would fix it is the attribute the frame exists to withhold. So the page fetches the attachment itself, with its own credentials, and hands the frame the bytes as a data: URL. What decides whether something is inlined is the server’s own Content-Type on the download, not the message’s claim — which is why the types the download route deliberately neuters are never inlined.

Sending has its own limits: at most 20 attachments and twelve megabytes of files together, in a request of up to sixteen megabytes, because base64 adds a third. The portal’s page and its script are served under one set of headers, and it is a strict set:

Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'unsafe-inline';
  img-src data:; connect-src 'self'; frame-src 'self'; form-action 'none';
  frame-ancestors 'none'; base-uri 'none'
X-Content-Type-Options: nosniff
Referrer-Policy: no-referrer
Cache-Control: no-store

That set is older than 6.3.4, and its img-src has widened since: the organisation directory’s photos are loaded by URL from the server itself, which a policy of data: and blob: alone would forbid, so CI holds the directive to what the script actually loads. Read the headers off a live response rather than copying these. The page still allows no image from another host, fetches no font and no stylesheet from anywhere, and comes out of the binary rather than out of files beside it. Its script is executed in CI against a small DOM and a stubbed API. Chapter 36 covers the portal as a thing to use; §36.10 and §36.11 go through the inline-image path and this header set in full.

24.15 What the API cannot write yet

This is the counterpart to the rest of the chapter, and the section to read before deciding where to host a domain. What it says has changed: up to 6.3.2 the only domain properties the API wrote were the name at creation, the active flag and the postmaster, and the table below was the list of what that left out. Since 6.3.3 every row of it is written by PUT /api/v1/domains/{domain}, and the Control Deck edits it, so a domain that must sign its outbound mail with DKIM can be configured on Linux, where there is no Control Panel and no COM. What is left is measured rather than asserted: build/check-deck-parity.py counts every property the desktop Control Panel writes against the REST API and the Deck, and writes hmailserver/docs/DeckParity.md. At 6.3.4 it counts 466, finds 458 of them writable over REST and every one of those reached from a Deck view, and lists eight as missing: an account’s directory fields — title, department, phone, mobile, office, note, photo and the opt-out. Those are written too, by PUT /api/v1/accounts/{address}/directory and the Deck’s account editor, under names the census does not match.

Not writable over REST up to 6.3.2 Since 6.3.3
Per-domain DKIM signing — enabled, selector, private key file, whether aliases are signed, the signing algorithm, both canonicalisation methods, the secondary selector and key The dkim_* keys of PUT /api/v1/domains/{domain}, and since 6.3.4 the key rotation’s too. GET /api/v1/domains/{domain}/dkim is still a read only, and a PUT there answers 404.
Per-domain size limits — maximum domain, message and account size, and the account, alias and list count limits The max_* keys and their switches; a credential confined to a domain cannot lift its own domain’s limits.
The domain signature — enabled, method, plain text, HTML, whether it is added to local mail and to replies The signature_* keys.
A per-domain relay host — host, port, user name, password, whether authentication is required, connection security The relay_* keys; the password is taken and never given back.
Other domain properties — the plus-addressing character and whether it is used, message retention days, the Active Directory domain name, the domain vacation fields, the domain anti-spam options and greylisting flag The keys of the same route, and the greylisting flag as use_greylisting.

The rest of what this section once listed as absent — among them the IP ranges, incoming relays, groups, the administrator’s second factor and the [Settings] keys (kept in the database since 6.3.4, with hMailServer.ini as their copy) — has routes of its own at 6.3.4 (/api/v1/ipranges/{id}, /api/v1/incoming-relays, /api/v1/groups, /api/v1/administrator/second-factor, /api/v1/settings/ini); the OpenAPI document (24.17) is the list the server itself answers from. The route tables earlier in this chapter do not list every route the server has at 6.3.4.

How the gap was measured at 6.3.0: the Linux regression run skipped 649 of 1,405 tests — 756 passed, 0 failed — because a fixture that reached something no route could answer was skipped with a reason naming what was missing, rather than weakened until it passed.

24.16 Worked examples

All of these assume a loopback listener on port 8443 with a self-signed certificate, hence -k. Over a real certificate, drop it. Where a second factor is enrolled on the administrator credential, each call made with the administrator password also carries the code in X-hMailServer-OTP, and since 6.3.4 a code is taken once: a script that makes more than one call signs in once for a session (the browser-style example below) or uses an API key, or only its first call in each half minute is accepted.

Create a domain and an account

curl -sk -u Administrator:secret -X POST \
  -H 'Content-Type: application/json' \
  -d '{"name":"example.com","active":true,"postmaster":"[email protected]"}' \
  https://127.0.0.1:8443/api/v1/domains

curl -sk -u Administrator:secret -X POST \
  -H 'Content-Type: application/json' \
  -d '{"address":"[email protected]","password":"a-long-passphrase","max_size_mb":2048}' \
  https://127.0.0.1:8443/api/v1/domains/example.com/accounts

The domain now exists and accepts mail. It does not sign its outbound mail until DKIM is set on it: dkim_enabled, dkim_selector and dkim_private_key_file on PUT /api/v1/domains/{domain} (24.15). Since 6.3.4 the server can make the key too: with dkim_rotation_enabled on, a domain with no key at all is given its first one, and GET /api/v1/domains/{domain}/dkim/rotation shows the record to publish; signing still waits for dkim_enabled.

Change settings, safely

Read first, change only the keys you mean, and send only those back. The write is all or nothing, so a typo costs the whole request and changes nothing.

curl -sk -u Administrator:secret https://127.0.0.1:8443/api/v1/settings

curl -sk -u Administrator:secret -X PUT \
  -H 'Content-Type: application/json' \
  -d '{"max_message_size_kb":51200,"smtp_no_of_tries":5,"auto_ban_on_logon_failure":true}' \
  https://127.0.0.1:8443/api/v1/settings

That one applies, and the answer is the whole server group as it now stands. Name a key the group does not have, or one it will not let you write, and nothing at all is applied:

curl -sk -u Administrator:secret -X PUT \
  -H 'Content-Type: application/json' \
  -d '{"enabled":true,"current_error_log":"D:\\logs\\error.log"}' \
  https://127.0.0.1:8443/api/v1/settings/logging
{"error":"current_error_log is read-only"}

The enabled in that body was perfectly valid and was not applied either.

Add a listener and make it live

{
  "protocol": "imap",
  "address": "0.0.0.0",
  "port": 993,
  "connection_security": "tls",
  "certificate_id": 3
}
curl -sk -u Administrator:secret -X POST \
  -H 'Content-Type: application/json' -d @port.json \
  https://127.0.0.1:8443/api/v1/ports

curl -sk -u Administrator:secret -X POST \
  https://127.0.0.1:8443/api/v1/server/reinitialize

The second call answers 202 {"reinitializing":true,"drained":false} (since 6.3.4 drained is true when the reinitialise ended maintenance mode). Poll GET /api/v1/status until it answers again, and note that it drops every session cookie, so a browser signed in to the Deck will have to sign in afresh.

Mint a scoped API key

curl -sk -u Administrator:secret -X POST \
  -H 'Content-Type: application/json' \
  -d '{"label":"provisioning","scope":"full","domains":"example.com","allowed_from":"10.0.0.0/24"}' \
  https://127.0.0.1:8443/api/v1/apikeys

curl -sk -H 'Authorization: Bearer hmapi_4f3c...' \
  https://127.0.0.1:8443/api/v1/domains/example.com/accounts

curl -sk -H 'Authorization: Bearer hmapi_4f3c...' https://127.0.0.1:8443/api/v1/queue

The first response carries the clear-text token once — it begins hmapi_, and the whole string including that prefix is what goes in the header. Store it before you close the terminal. The second call works. The third does not:

{"error":"this api key is restricted to named domains, and the delivery queue is server-wide"}

A browser-style session

What the Control Deck does: exchange the password once, then use the cookie — and remember the header on every write.

curl -sk -c jar.txt -u Administrator:secret -X POST \
  https://127.0.0.1:8443/api/v1/session

curl -sk -b jar.txt https://127.0.0.1:8443/api/v1/domains

curl -sk -b jar.txt -X PUT \
  -H 'X-Requested-With: hMailServer' \
  -H 'Content-Type: application/json' \
  -d '{"active":false}' \
  https://127.0.0.1:8443/api/v1/domains/example.com

curl -sk -b jar.txt -X DELETE -H 'X-Requested-With: hMailServer' \
  https://127.0.0.1:8443/api/v1/session

Leave the header off that PUT and the answer is 403 — the single most common surprise for anyone writing a client that keeps cookies by default.

A mailbox reading its own mail

curl -sk -u [email protected]:a-long-passphrase \
  https://127.0.0.1:8443/api/v1/me/folders

curl -sk -u [email protected]:a-long-passphrase \
  'https://127.0.0.1:8443/api/v1/me/search?q=invoice&limit=25'

curl -sk -u [email protected]:a-long-passphrase -X POST \
  -H 'Content-Type: application/json' \
  -d '{"to":"[email protected]","subject":"Re: invoice","text":"Attached."}' \
  https://127.0.0.1:8443/api/v1/me/messages

The search answers at most limit hits, newest first across every folder. Since 6.3.4 an answer that is not the end says complete: false with next_before_id, and before_id continues from it; up to 6.3.3 a search with more hits than limit answered complete: true, more: true and the rest could not be reached.

Try the same three with the administrator password and every one answers 403: there is no mailbox behind the administrator credential.

24.17 The OpenAPI document

curl -sk -u Administrator:secret \
  https://127.0.0.1:8443/api/v1/openapi.json > hmailserver-openapi.json

GET /api/v1/openapi.json returns an OpenAPI 3.0.3 document describing every route under /api/v1/ and, at 6.3.4, the /scim/v2 routes, the sign-in, reset and reading pages, the list manager’s pages and the static catalogues and scripts beside them: 441 paths and 621 path-and-method operations, 413 and 585 of them under /api/. The three page endpoints of 24.3 are not in it, and the webmail page at /portal itself is mentioned only in prose. It is valid JSON as of 6.3.0, and it was not before — anything that tried to generate from it in an earlier release, a client, a form generator or a schema validator, would have failed to parse it. If you have a tool that gave up on this document in the past, try it again.

  • It lives beside the router it describes, so a route change and its documentation change land in the same commit. It is assembled from a static head, a contribution from each unit of the router — at 6.3.0 the settings, rules, certificates, routes and mailbox units; at 6.3.4 every RestApi*.cpp file, with RestApiSettings.cpp writing the settings groups’ paths from its tables — and a static tail, each contributor returning either nothing or a run of entries that each begin with a comma. That is what keeps the document well-formed however many units contribute.
  • The settings sections are generated from the same row tables the GET and the PUT walk, so every setting appears with its type, its permitted words, whether it is read-only and when it takes effect: since 6.3.4 a row kept in the settings store carries x-applies — now, reinitialize or restart — which the Deck shows as a badge beside the field.
  • The Control Deck generates its settings forms from this document. That is why the Deck gained editors for the entire settings surface at once rather than field by field: nobody hand-wrote the form controls of the thirteen settings groups 6.3.4 has, and nobody has to hand-write the next one. Chapter 36.
  • Several regression fixtures hold the document to its own contract: each carries the paths it owns and fails the moment the document stops naming one of them. Those lists are the tests’ copy of the contract rather than a complete inventory of the router, so extending the API means extending the document and the list together — which is what the failure message says.
  • components.securitySchemes declares basic and bearer; the session cookie is described in prose under /api/v1/session rather than as a scheme, so a generated client will hold the password rather than the cookie unless you write that part yourself.

One thing the document does not tell you is the subject of 24.15. It describes every route that exists and says nothing about a property no route writes. Up to 6.3.2 that left a generated client looking complete with no way to turn on DKIM signing for a domain; since 6.3.3 that is the dkim_* keys of PUT /api/v1/domains/{domain}, and at 6.3.4 the census of 24.15 lists only an account’s directory fields, which PUT /api/v1/accounts/{address}/directory writes under other names. Not every description in it is current at 6.3.4 either: its text for /api/v1/settings/ini still calls hMailServer.ini the store and says a domain-restricted key is refused, where the value goes to the database first and every API key is answered 401.

More: The REST API: turning it on, credentials, roles, one-time codes, OpenAPI and examples on the forum.


25. Version history

6.3.8 — security fixes for every earlier version, and the Helm chart starts a server

  • Read this first: upgrade every server running 6.3.7 or earlier. Released 2 November 2026, 6.3.8 is a security release: it fixes vulnerabilities the project found in its own code, which between them reach back to 6.0.0’s first beta, each a work item, public from the day of the release, with a CVE request through GitLab’s CNA. They are in the database layer’s test for the marker that lets a statement’s error be ignored (CVE requested, work item #100); in the auto-ban’s naming of the IP range it adds (CVE requested, work item #102); in the match of a sign-in’s name to its account (CVE requested, work item #104); in the IP ranges at the web services listener (CVE requested, work item #105), and in the address a failed sign-in at that listener is charged to (CVE requested, work item #106); in the address a failed sign-in at the REST listener is charged to (CVE requested, work item #107); in the cost of the REST API’s reading of a request’s JSON body (CVE requested, work item #110; CVE requested, work item #111); in the database client’s check of the database server’s certificate (CVE requested, work item #112), and in its check of a database server on this machine (CVE requested, work item #113); and on Windows in the database clients’ reading of the root certificate file named (CVE requested, work item #114), in the server’s reading of the files it hands to OpenSSL by name (CVE requested, work item #115) and in the folder the bundled database client loads its authentication plugins from (CVE requested, work item #116). The four narrower cases of work item #78 that 6.3.7’s notes listed as still open are fixed too: 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 (four CVE requests, work item #78). The Security section of 6.3.8’s CHANGELOG has every one, with what changed and how it is tested. The advisory for a flaw in the auto-ban on MySQL and MariaDB that 6.3.4 fixed is published with 6.3.8 (CVE requested, work item #103).
  • Read this too: no schema step. 6.3.8 needs 6108, as 6.3.6 and 6.3.7 do, so an upgrade from either changes no table, and the live update’s rollback, should 6.3.8 not start, meets no newer schema. From 6.3.5 or older the upgrade runs 6.3.6’s three one-way steps first, and the backup is the only way back (§18.2).
  • Read this too: why there is a 6.3.8, and what else it carries. 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. 6.3.8’s chart starts a server: it 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. The container image reaches a MariaDB database. On Windows the installer refuses an installation of the original hMailServer’s 5.7 builds of August and September 2026 before it changes anything, where the installers of 6.3.7 and earlier replaced its programs. A certificate that does not name every host in AcmeDomains is due at once, a save of the list in the running server starts the check, and a check can be asked for (POST /api/v1/acme/run, GET /api/v1/acme, COM Utilities.RunAcmeCheck and GetAcmeStatus, the Control Deck’s Certificates page; work item #98). Rspamd learns from the Junk folder, beside SpamAssassin (work item #108), and scripts have an event after an IMAP move, OnIMAPMessageMoved (work item #109), each off until it is switched on.
  • A Helm release of the 6.3.6 or 6.3.7 chart is moved with helm upgrade --reset-then-reuse-values (Helm 3.14 or later) and then kubectl delete pod, since the StatefulSet replaces no pod while one is not Running and Ready; --reuse-values keeps the old chart’s fsGroup 1000 and database.port 5432. In a cluster whose policy refuses every sysctl, set unprivilegedPortsSysctl: false.
  • The live update. Every file is signed as the release’s pipeline at the project’s path in the group hmailserver, https://gitlab.com/hmailserver/hmailserver//.gitlab-ci.yml@refs/tags/v6.3.8, with the project’s id in the certificate. A 6.3.7 or 6.3.6 server installs it with its own file as its rollback image: on Windows a service running as NT SERVICE\hMailServer leaves the install to an administrator, whose Install update on the Server status page installs it with one Windows prompt; on Linux the installed version’s update helper applies it to the generic .deb and .rpm and to both AppImages, and a server installed from a per-release package is upgraded with its package manager and its release’s own package of 6.3.8. 6.3.4 and 6.3.5 are as in 6.3.7’s entry below; a server from 6.2.28 to 6.3.3 is upgraded by hand, once (§18.2). A server whose UpdateSigningIdentity, UpdateSigningIssuer and UpdateSourceRepository still name the project’s former path refuses 6.3.8 until the three are cleared.
  • What changes on the upgrade without a switch (§18.5), twenty-nine things, is in 6.3.8’s section of the CHANGELOG, and its Before you upgrade names what most likely needs something done first. On MySQL and MariaDB through MariaDB Connector/C, TlsMode=verify-full without TlsCaFile is refused with HM7535 on a connection to a database server on this machine: 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. On Windows a TlsCaFile (with TlsMode=verify-full) or a PostgreSQLSslRootCert (HM7534), or a TLS certificate or key file or another file the server hands to OpenSSL by name (HM7536), whose path holds characters outside ASCII may be refused: give each such file, and the folders above it, an ASCII name before upgrading. On Linux file names outside ASCII are UTF-8 whatever LANG says: on a server that ran under a LANG that is not UTF-8, rename each such file and folder to its UTF-8 name before upgrading. On Windows setup refuses, with exit code 7, 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, naming the icacls that deals with it, and asks for a restart of Windows before it runs again. In 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, and with it the Control Deck, the webmail and hmctl, does not start. A host in AcmeDomains that the CA cannot validate fails the whole order and holds the renewal of the other hosts with it: take such a host off the list before upgrading. 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, may no longer sign in; a REST request body that is not one JSON object, or passes the bounds, is answered 400; and an administrator’s IP range that allows an address none of SMTP, POP3 and IMAP closes the web services listener to it as well.
  • Platforms. The release carries 161 files in all, the Sigstore bundles among them: the files 6.3.7 carried, at 6.3.8, and hmailserver-6.3.8.intoto.jsonl, the provenance as one line of JSON. The container image, registry.gitlab.com/hmailserver/hmailserver:6.3.8, also tagged 6.3 and latest, for linux/amd64 and linux/arm64, is pushed from the release’s own pipeline by a job started by hand once the release is published; the package channels take 6.3.8 by jobs started by hand after that (§35.2).
  • Verification. As for 6.3.7, with --certificate-identity 'https://gitlab.com/hmailserver/hmailserver//.gitlab-ci.yml@refs/tags/v6.3.8'; both installers are Authenticode-signed by Progressive Robot Ltd (§4.1). More: the release’s page, with its notes and every file.

6.3.7 — security fixes for every earlier version, and the first security release made while releases pause

  • Read this first: upgrade every server running 6.3.6 or earlier. Released 17 October 2026, 6.3.7 fixes vulnerabilities in every earlier version, each found by the project’s own review unless the release notes say otherwise. 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, in every release from 6.0.0 to 6.3.6; the default folder under Program Files was never affected, and hMailServer.exe /ProtectFolders, which setup runs at every installation and upgrade, now hardens the program folder, Bin and ControlPanel (CVE-2026-107571, work item #78). Mail from outside whose From named a domain hosted here was signed with that domain’s DKIM key (CVE-2026-107567, work item #84); an account’s Sieve notify was sent from whatever its :from said (CVE-2026-107568, work item #82), and its editheader could rewrite who a message was from ahead of a redirect (CVE-2026-107566, work item #83); and any sender could lift the rule loop limit off a message (CVE-2026-107569, work item #81). Others, whose CVE identifiers GitLab assigned after the release (CVE-2026-114470 to CVE-2026-114479, CVE-2026-114489 and CVE-2026-114490), concern what a credential confined to named domains may change or read over the REST and COM APIs, a state-changing request that a browser holding a REST credential could be made to send from another site, the ACME challenge responder, the envelope sender of forwards and copies of mail from outside, the anti-virus action that removes attachments, mail to an SRS address and the relay rules, and credentials in the SMTP log. The Windows installers carry the .NET Desktop Runtime 10.0.13, where those of 6.3.4 to 6.3.6 carried 10.0.12, and the x64 installer, as the MySQL and MariaDB client, MariaDB Connector/C 3.4.11, where those of 6.2.3 to 6.3.6 carried 3.4.9; the ARM64 one carries no MySQL or MariaDB client. Setup installs the runtime only where no .NET 10 Desktop Runtime is installed, so an upgraded server keeps the one it had: bring it up to date through Microsoft Update. The Security section of 6.3.7’s CHANGELOG has every one, and What 6.3.7 does not fix, above that section, the four narrower cases of work item #78 that this release does not fix.
  • Read this too: no schema step. 6.3.7 needs 6108, as 6.3.6 does, so an upgrade from 6.3.6 changes no table, and 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 first, and the entries below apply (§18.2).
  • Read this too: a security release, cut from master. 6.3.6 was the last release planned by date; the releases since are security releases, each cut from master with whatever finished work master holds then, and 6.3.7 is the first. So it also carries the work that was under way on 13 October 2026: the Control Panel’s corrections to its pages, texts, translations and keyboard and screen-reader behaviour, and its regrouped sidebar; IMAP and POP3 taking the client’s address from a PROXY header; a link’s host that is not ASCII read by UTS 46; and the first units of the active-active work, which is still not built. It carries the first step of the Control Deck’s and the webmail’s rebuild planned for 6.4 as well, with no product code.
  • The live update. Every file is signed as the release’s pipeline at the project’s path in the group hmailserver, https://gitlab.com/hmailserver/hmailserver//.gitlab-ci.yml@refs/tags/v6.3.7, with the project’s id in the certificate, as 6.3.6 was. A 6.3.6 server installs it with 6.3.6’s own file as its rollback image: on Windows a service running as NT SERVICE\hMailServer leaves the install to an administrator, whose Install update on the Control Panel’s Server status page, on the server, installs it with one Windows prompt; on Linux 6.3.6’s own update helper applies it to the generic .deb and .rpm and to both AppImages, and a server installed from one of 6.3.6’s per-release packages is upgraded with its package manager and its release’s own package of 6.3.7. A 6.3.5 Windows server running as LocalSystem installs it with 6.3.5’s file as its rollback image, a 6.3.4 one without; that image is no way back once the upgrade’s schema steps have run, since 6.3.5 refuses a database they have moved to 6108, so from 6.3.5, as from 6.3.4, the backup taken before the upgrade is the way back (§18.6). Under NT SERVICE\hMailServer, and on Linux, 6.3.4 and 6.3.5 are upgraded by hand, once (§18.2). A server whose UpdateSigningIdentity, UpdateSigningIssuer and UpdateSourceRepository still name the project’s former path refuses 6.3.7 until the three are cleared. A server from 6.2.28 to 6.3.3, or one from 6.3.3’s AArch64 packages or AppImages, is upgraded by hand, once (§18.2).
  • What changes on the upgrade without a switch (§18.5) is in 6.3.7’s section of the CHANGELOG. An account that sends over authenticated SMTP as an address of another domain hosted here, with no alias or grant that lets it, has that mail sent unsigned as that domain. A domain’s or an account’s rule and a Sieve script may no longer rewrite who a message is from, and one stored before the upgrade runs without that action or edit, which the application log names. RuleLoopLimit 0 stops out-of-office replies too. An account given the serveradmin role with a list of domains is confined to those domains over COM, as over REST. A database account that signs in with MySQL Enterprise’s Windows authentication can no longer sign in. SMTP, POP3 and ManageSieve refuse a SASL PLAIN sign-in that asks to act as another account.
  • Platforms. The release carries 160 files in all, the Sigstore bundles among them: the x64 installer and, for Windows on ARM, hMailServer-6.3.7-arm64.exe, a preview that has never run on ARM64 hardware; the generic .deb and .rpm for x86-64 and AArch64 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, the AArch64 ones cross-compiled and tested under emulation; both AppImages, with the source of the libraries linked into their runtime and of those they carry; the source tarball, with nothing non-free in it; the Helm chart; the PowerShell module; an SBOM for each compiled file and the tarball; the sums of every Linux file; and the release’s SLSA build provenance. The container image, registry.gitlab.com/hmailserver/hmailserver:6.3.7, also tagged 6.3 and latest, for linux/amd64 and linux/arm64, is pushed from the release’s own pipeline by a job started by hand once the release is published. On its release day no channel published it; on 21 October 2026 the Ubuntu PPA, Fedora Copr, the openSUSE Build Service, Docker Hub, PyPI and the PowerShell Gallery took it, Chocolatey listed it on 23 October, once its moderators had approved it, and the Snap Store took no snap of it.
  • Verification. Every file but the provenance has <file>.cosign.bundle beside it, and every one of those but a compiled file’s or the tarball’s SBOM (<file>.cdx.json) the same bundle again as <file>.sigstore.json: cosign verify-blob with --certificate-identity 'https://gitlab.com/hmailserver/hmailserver//.gitlab-ci.yml@refs/tags/v6.3.7' and --certificate-oidc-issuer https://gitlab.com, or on Linux hmailserver --verify-update <file>; hmailserver-6.3.7.intoto.sigstore.json is the release’s SLSA build provenance, checked with cosign verify-blob-attestation; both installers are Authenticode-signed by Progressive Robot Ltd (§4.1). Known issue, as in 6.3.6: 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 (glibc-gconv-modules-extra on openSUSE) by hand (§35.2). More: the release’s page, with its notes and every file.

6.3.6 — security fixes for every earlier version, every platform again, and the last release planned by date

  • Read this first: upgrade every server running 6.3.5 or earlier. 6.3.6 fixes the security flaws the project’s own reviews of the code found. The code of the legacy Blowfish scheme of stored secrets that the project inherited from the original hMailServer had two heap buffer overflows: the one in its encryption is in every version up to 6.3.5, and 6.3.6 fixes it; the one in its decryption is in every version up to 6.3.3, and was fixed in 6.3.4, whose notes did not say so. They are CVE-2026-103011 (the encryption) and CVE-2026-103010 (the decryption), assigned through GitLab (work item #49); 6.3.6 replaces that code with the project’s own, which writes and reads every value as before. The same release fixes the rest, each with a CVE identifier from GitLab, the CVE Numbering Authority for projects on GitLab.com (CVE-2026-103647, CVE-2026-103649, CVE-2026-104658 to CVE-2026-104660, CVE-2026-104704 and CVE-2026-107572 to CVE-2026-107587), and its work item public from the day of the release: on Windows, COM objects that carry no credential were not refused what only a signed-in caller may do, and the installer left the data, log, temp, database and event folders and hMailServer.INI readable by every account on the computer; on Linux, the live update’s apply step, which runs as root, trusted what the service account wrote, and some network waits had no timeout; the REST listener’s loopback port answered a request whatever its Host, and a wrong administrator password from the loopback address was not slowed down; the webmail handed the browser a decrypted attachment under the type the message named, and kept a stranger’s S/MIME certificate for encrypting replies; outbound DANE used DANE-EE records alone, and read a lookup that did not complete as “unsigned”; the REST listener’s tables of browser sessions and pending sign-ins could be emptied by anybody who could sign in, or start a sign-in; and a message’s HTML or entities, a header field, an IMAP FETCH, a returned report, a MIME parameter, a DKIM or ARC signature, an SPF macro, a TLS report’s JSON or a Sieve script could hold a thread for minutes, or until the service was restarted. The Windows installers carry OpenSSL 4.0.3, where 6.3.5’s carried 4.0.2: CVE-2026-84783 (Moderate) applies to the Windows server on a port that asks for client certificates, and every Windows installer from 6.0.0-B1 to 6.3.5 carried a 4.0.x it affects. The Security section of 6.3.6’s CHANGELOG has every one.
  • Read this too: the schema moves from 6105 to 6108 in three steps, one way, so back up first: 6106 gives external accounts’ days to keep messages one meaning for POP3 and IMAP, 6107 finishes the greylisting key, and 6108 adds the never-ban list by reverse name and each SMTP listener’s choice of what it does with AUTH (§18.2). From 6.3.4 the upgrade runs 6.3.5’s step first, and from 6.3.3 or older 6.3.4’s sixty-four, and the entries below apply.
  • Read this too: releases pause after 6.3.6. 6.3.6 was the last release planned by date: no release is planned by date until the maintainers say otherwise in the security policy. A release is still made when a confirmed vulnerability needs one, 6.3.7, 6.3.8 and so on, and the releases since are security releases, each cut from master with whatever finished work master holds then; the 6.3 line receives security fixes until 30 September 2027.
  • The live update. Every file is signed as the release’s pipeline at the project’s path in the group hmailserver, https://gitlab.com/hmailserver/hmailserver//.gitlab-ci.yml@refs/tags/v6.3.6, with the project’s id in the certificate, which every server from 6.3.4 on trusts: a 6.3.5 server installs it with 6.3.5’s own file as its rollback image, a 6.3.4 server without one. On Windows a service running as NT SERVICE\hMailServer still leaves the install of 6.3.6 to an administrator, who runs hMailServer-6.3.6-x64.exe by hand; 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 the package manager: the update to 6.3.6 is applied by the apply step of the version it replaces, the one fixed here. A server whose UpdateSigningIdentity, UpdateSigningIssuer and UpdateSourceRepository still name the project’s former path refuses 6.3.6 until the three are cleared. A server from 6.2.28 to 6.3.3, or one from 6.3.3’s AArch64 packages or AppImages, is upgraded by hand, once (§18.2).
  • What changes on the upgrade without a switch (§18.5), forty-six things. Mail can stop flowing if the resolver strips DNSSEC: an MX or TLSA lookup that cannot tell is no longer read as “unsigned”, and the delivery is deferred. External accounts’ days to keep messages means one thing for POP3 and IMAP. The eight helpers of COM Utilities that do work, and a Message or FetchAccount a program creates itself, answer only a program that has signed in. Setup makes the data, log, temp, database and event folders and hMailServer.INI private to SYSTEM, the Administrators and the service. The administrator’s commands write nothing to the error log, the store commands refuse a link the administrators do not alone control, and the installer exits 10 or 11 when a step after its files did not complete. The live update’s five trust settings are read from hMailServer.ini alone. The REST loopback port answers only a Host it expects, and a wrong administrator password costs a growing delay. Port 25 still takes AUTH until you change it. The URI block lists are asked about every domain a message links to, and a list’s error answer is never a listing. ManageSieve is held to deadlines. An IP address in text is read the same way on Windows as on Linux. The webmail keeps a correspondent’s S/MIME certificate only when its chain is trusted. The MX-record and SURBL tests cut a host name by the Public Suffix List; backups leave out the live update’s folder, and a destination inside the data directory works; tlds.txt is removed; a calendar message delivered by mail changes a stored event only from a verified sender; the Control Panel’s text can be selected and copied; on Linux the backup and event logs are UTF-8; scripting off runs no script engine.
  • New. A never-ban list by reverse name, forward-confirmed (work item 56); each SMTP listener’s AUTH On, Off or Honeypot, a new installation’s port 25 a Honeypot (work item 54); an auto-ban widened to its reverse-name neighbours in the same IPv4 /24 (AutoBanWidenByReverseName, off by default; work item 55); a HELO name or a sender’s domain that is not a name refused (RefuseHeloNotFqdn and RefuseMailFromDomainNotFqdn, 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; a new Windows installation set up against SQL Server, PostgreSQL or MySQL with no window (/dbtype= and the other switches); and the last inherited code whose licence a distribution would have had to settle — the legacy Blowfish scheme, the backup index’s XML and the date class — is the project’s own.
  • Platforms. The release carries every file its pipeline makes, 62 files with a Sigstore bundle beside each: the x64 installer and, for Windows on ARM, hMailServer-6.3.6-arm64.exe, a preview that has never run on ARM64 hardware; the generic .deb and .rpm for x86-64 and AArch64 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, the AArch64 ones cross-compiled and tested under emulation; both AppImages, with the source of the libraries linked into their runtime and of those they carry; the source tarball, with nothing non-free in it; the Helm chart; the PowerShell module, Authenticode-signed; an SBOM for each compiled file and the tarball; the sums of every Linux file; and the release’s SLSA build provenance. The container image, registry.gitlab.com/hmailserver/hmailserver:6.3.6, for linux/amd64 and linux/arm64, is pushed from the release’s own pipeline by a job started by hand once the release is published; until then the registry holds no image. A Nix flake and an Alpine APKBUILD are in the tree; no apt or dnf repository or other channel is live yet.
  • Verification. Every file has <file>.cosign.bundle beside it, and every file but a compiled file’s SBOM the same bundle again as <file>.sigstore.json: cosign verify-blob with --certificate-identity 'https://gitlab.com/hmailserver/hmailserver//.gitlab-ci.yml@refs/tags/v6.3.6' and --certificate-oidc-issuer https://gitlab.com, or on Linux hmailserver --verify-update <file>; hmailserver-6.3.6.intoto.sigstore.json is the release’s SLSA build provenance, checked with cosign verify-blob-attestation; both installers are Authenticode-signed by Progressive Robot Ltd (§4.1). Known issue: an AppImage on Fedora, Enterprise Linux or openSUSE runs on the host’s glibc and cannot require its extra character-set converters, glibc-gconv-extra (glibc-gconv-modules-extra on openSUSE), without which text in BIG5, ISO-2022-JP, Windows-1251 or ISO-8859-2 is not converted: install the package by hand. The .rpms require it where dnf is installed on Fedora and Enterprise Linux and recommend it elsewhere, and a server without it whose dnf has no repository enabled has the live update refused until it is installed; the server says at its start what it cannot convert (§35.2). More: the release’s page, with its notes and every file.

6.3.5 — the first release its GitLab pipeline signed, and the quarantine grouped by domain and HELO

  • Read this first: a 6.3.4 server can install 6.3.5 by its live update; an older one is upgraded by hand. 6.3.5 is the first release built and signed by the project’s GitLab CI pipeline for its tag: every file has a Sigstore bundle naming https://gitlab.com/Progressiverobot/hmailserver//.gitlab-ci.yml@refs/tags/v6.3.5 and the issuer https://gitlab.com, which a 6.3.4 server trusts, and the Windows installer is Authenticode-signed as before. On 29 September 2026 a 6.3.4 server whose service runs as LocalSystem installed it by its own live update, with about 24 seconds without mail; one whose service runs as NT SERVICE\hMailServer, as a new 6.3.4 installation does, downloads and verifies it and leaves the install to an administrator (HM7463). A 6.3.4 server installs it without a rollback image, because 6.3.4 carries no Sigstore bundle. A server from 6.2.28 to 6.3.3 trusts only the GitHub signer: install 6.3.4 or 6.3.5 by hand, once, and keep UpdateAutoDownload off until then (§18.7).
  • Read this too: the schema moves from 6104 to 6105 in one step, one way, adding a column to the quarantine’s table, so back up first. From 6.3.3 or older the upgrade runs 6.3.4’s sixty-four steps first, and the 6.3.4 entry below applies.
  • What changes on the upgrade without a switch. The Control Panel’s sidebar is in a new order, with a new group, Set up this server, holding the Setup checklist (which was External setup), Setting profiles and DNS records, and two groups renamed: TLS & certificates is Certificates & encryption, Access & abuse protection is Sign-in & access (§5.3); the quarantine shows the HELO name and groups by the sender’s domain; the webmail’s message list is one Tab stop, and Enter, Escape and the formatted signature’s keys do what they say; an invitation sent the way Outlook sends one is shown as one; and GET /api/v1/quarantine gives three more fields, with Quarantine.Bulk and five new QuarantinedMessage members over COM.
  • Fixed. A rule with a Readable text or Sensitive content found condition read a message’s HTML part in time that could rise with the square of its size, so that 400,000 &amp; held a delivery for about a minute; and an invitation whose calendar part carries parameters in its Content-Type and no .ics name was never marked as one. 6.3.4 has both.
  • The quarantine reviewed: 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 a message, a group or the checked ones, with a dry run first; a held message’s headers and a plain-text preview without releasing it; the Control Deck level with the Control Panel.
  • Administration. 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 a new Webmail page, 103 of them saved over the REST API, which is off as shipped. The Control Panel and the webmail were audited in both themes, by keyboard and, for the webmail, with NVDA in English and German. The Deck reaches 498 of the 508 properties the Control Panel writes.
  • Platforms. As 6.3.4: the Windows x64 installer, and on Linux an x86-64 .deb for Ubuntu 26.04 and 24.04 and an x86-64 .rpm for Fedora 43 and 44 and Enterprise Linux 10, with 6.3.4’s dependencies. The pipeline builds the packages for Debian, Ubuntu, Fedora, RHEL and openSUSE on x86-64 and AArch64, the AppImages and the ARM64 installer as CI job artifacts only, not signed and not in the release, so 6.3.3’s AArch64 packages and AppImages are still the latest; the ARM64 installer’s job stops on a toolchain check that looks in the wrong place. No container image, no apt or dnf repository and no SLSA provenance.
  • Verification. Every file has <file>.cosign.bundle beside it: cosign verify-blob with --certificate-identity 'https://gitlab.com/Progressiverobot/hmailserver//.gitlab-ci.yml@refs/tags/v6.3.5' and --certificate-oidc-issuer https://gitlab.com, or on Linux hmailserver --verify-update <file>; hmailserver-linux-6.3.5-SHA256SUMS.txt gives the two packages’ SHA-256; the installer, the .deb and the .rpm each have an SBOM of their own; and Get-AuthenticodeSignature on the installer should answer Valid, signed by Progressive Robot Ltd (§4.1). linux-repro rebuilt the .deb and the .rpm byte for byte before the release. The project moved to gitlab.com/hmailserver/hmailserver on 29 September 2026, after this release; the old addresses redirect, and 6.3.5’s bundles keep the former path. More: hMailServer 6.3.5 on the forum.

6.3.4 — the settings in the database, calendars, roles and eDiscovery, installed by hand

  • Read this first: install 6.3.4 by hand, on every server. It is the first release built after GitHub suspended the project’s account on 18 September 2026. Its Windows installer is Authenticode-signed by Progressive Robot Ltd and it carries no Sigstore bundle, so the live update of any version that is offered it says it cannot be verified and downloads nothing. Run hMailServer-6.3.4-x64.exe; on Linux, install the .deb with apt on Ubuntu 26.04 or 24.04, or the .rpm with dnf on Fedora 43 or 44 or Enterprise Linux 10. A server from 6.2.28 to 6.3.3 still looks for releases at the former GitHub repository, which answers 404: UpdateFeedUrl=https://updates.progressiverobot.com/releases.json under [Settings], and a restart, make its check work again. 6.3.4 reads that feed by itself and trusts both the GitHub signer of the releases up to 6.3.3 and this project’s GitLab pipeline, which signs the releases from 6.3.5 on (§18.7).
  • Read this too: the schema moves from 6040 to 6104 in sixty-four steps, one way, so back up first. On MySQL and MariaDB step 6102 rebuilds every table as utf8mb4, which takes as long as copying the database and needs MySQL 5.7.9 or MariaDB 10.2.2 or later and the ALTER privilege on the database (on an older database server the upgrade stops at 6041 with error 1071 and changes nothing), and step 6101 adds two columns to hm_messages; §18.2 has both. A silent upgrade of a server with an administrator password takes it with /adminpassword= or, new in 6.3.4, in HM_ADMIN_PASSWORD (§4.2).
  • What changes on the upgrade without a switch. The database is the settings store and [Settings] in hMailServer.ini its copy (chapter 21); most saved settings take effect at once; SPF is RFC 7208; the audit trail and alerts are on; compromised-account detection watches; a destination that asks the server to slow down is paused; a backup carries an account’s address book, calendars, preferences and keys beside its mail; IMAP answers as the RFCs say where imaptest found it did not; MySQL and MariaDB tables become utf8mb4; the database connection is encrypted when the database server offers TLS; the administrator password is hashed again at sign-in; a COM change to a shared configuration object takes effect at Save(); ISO-8859-1 is decoded as windows-1252; a restore refuses an unsafe archive whole; a password hash made before 6.3.4 of a password with a character above U+00FF is refused until the password is set again; a one-time code is taken once; the outbound relay sends its OAuth2 token over TLS only; and a domain may not be named like one of the server’s own directories. §18.5 says what to do where anything needs doing.
  • Security: a password could run code inside the server. On a Windows server scripting in JScript with an OnClientValidatePassword handler, the server escaped the apostrophe and not the backslash in the values it built each call from, so any client that could reach a sign-in and name an existing, active account could have JScript of its own run inside hMailServer.exe before it had authenticated — with ScriptAllowedObjects at its shipped *, any command the service’s account may run. The same shape was open through OnExternalAccountDownload and OnDeliveryFailed, and a rule’s Run function action wrote its name into the call unescaped in either language. 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), so treat any server older than 6.3.4 with event scripting on and JScript as its language as affected. VBScript servers were not open through a password, and the Linux builds had no script engine. Every JScript value is now escaped in one place, and a Run function name must be a plain identifier. The flaw is CVE-2026-100741; the CVE record was not yet published on 27 September 2026. Until you can upgrade, switch event scripting off. A server that cannot do without its scripts should take the OnClientValidatePassword, OnExternalAccountDownload and OnDeliveryFailed handlers out of a JScript EventHandlers.js and reload the scripts, and, in either language, let nobody but the server administrator make or edit a rule with a Run function action. More: hMailServer 6.3.4: a password could run code on Windows servers with JScript scripting on the forum.
  • Fixed before release. 6.3.4 was audited area by area before it was cut, and its code scanned by GitLab’s SAST, and the defects they found are fixed in it; several were in 6.3.3 as well. A password of Cyrillic, Greek or CJK letters could be opened by any other password of its length, because every hash was made over a conversion that lost those characters; hashes are now made over UTF-8. SCRAM-SHA-256 took an account’s own password past its second factor and its maximum age; one-time codes could be used again within their window and, at several places, guessed without limit; a single sign-on ticket could be redeemed in another person’s browser; a domain administrator could become the server administrator through an app password on the server administrator’s own mailbox where it is in that domain; an app password or a support session could read the account’s wrapped keys, and replace some of them; fetch accounts could reach inside the network or collect a Microsoft 365 mailbox with the server’s own token; and two IMAP sessions meeting on one message’s keywords could abort the server. Seven of the SAST analyzer’s High findings were real and are fixed — the crash-dump helper’s DLL search, the Control Panel’s elevated restart, the administrator password on DBUpdater.exe’s command line (two), the backup index parser (two) and a MIME parser test that compared pointers into two arrays — and the security exceptions file accepted 455 findings at the release as not exploitable, each with its reason. Found beside them and fixed too: the MySQL client’s DLL search, the password on DBSetupQuick.exe’s command line, and a custom virus scanner handed a path cut at its first space, which scanned no file and passed the message as clean. The installer’s 7-Zip is 26.03.
  • A new installation is secure before anyone configures it — a self-signed certificate, STARTTLS on 25, 587, 110 and 143, TLS required for authentication from the Internet — and on Windows it runs as NT SERVICE\hMailServer rather than LocalSystem (§17.3). Administrator roles over 27 permissions, passkeys and OpenID Connect and SAML sign-in, SCIM provisioning, account recovery, sessions and devices, and a check of new passwords against published breaches.
  • Mail. Remote-domain policies, throttling per destination and per tenant, IP warm-up, a send API for applications, DKIM key rotation, a domain’s external-sender tag and disclaimer, restricted delivery, message recall, journaling, quota and out-of-office policies, and mail in to a webhook. Against spam: greylisting hardened, SMTP policy a score cannot express, allow and block lists with quarantine digests, mail flow rules and an outbound content policy, impersonation protection, attachments opened in a sandbox, spam traps and country blocking.
  • The webmail. Calendars over CalDAV beside CardDAV, with shares, invitations by mail, free-busy, and rooms and equipment that answer their own invitations; contact groups, a directory and shared address books; recover deleted items, messages that expire, offline mail and Web Push; a second account in the same page; masked addresses and sending as another address; OpenPGP in the browser; and Arabic, Hebrew and Persian with the layout mirrored.
  • Administration and compliance. An audit trail that can be verified as a hash chain, alerts on eleven conditions, one value set for the server, a domain or an account, profiles, hmctl and a PowerShell module, a setup checklist the server judges, the Control Panel on one design system with 65 pages, the Control Deck in twenty languages besides English, eDiscovery with legal holds, and a person’s data exported and erased.
  • Storage and encryption. Messages on an S3-compatible store or Azure Blob Storage; storage compression and attachment de-duplication; mailbox encryption at rest, zero-access mailboxes and gateway encryption to partner domains; an organisation CA for S/MIME; and the TOTP secrets and the [Settings] secrets sealed under a stored-secrets key.
  • Migration and operations. Mailboxes migrated from another IMAP server, Google Workspace or Microsoft 365 as jobs that resume, and PST import with a reader of the project’s own. Event scripts on Linux, in JavaScript (§28.2); the live update applies on Linux through a root helper (§35.13); a read-only SNMPv3 agent, syslog, outbound webhooks and per-domain reports; upgrades that apply the schema at start ([Database] UpgradeOnStart=1); and encrypted backups.
  • Platforms. The Windows x64 installer, and on Linux an x86-64 .deb for Ubuntu 26.04 and 24.04 and an x86-64 .rpm for Fedora 43 and 44 and Enterprise Linux 10; both need glibc 2.38 or later, so neither installs on Debian 12 or Enterprise Linux 9. There are no AArch64 packages and no AppImage in 6.3.4 — the project has no ARM machine and the tool the AppImage is made with is published only on GitHub — so 6.3.3’s are the latest of those. Windows on ARM compiles but has never run, and 6.3.4 has no ARM64 installer. No container image of 6.3.4 is published, and there is no apt or dnf repository yet. The update feed is https://updates.progressiverobot.com, served by GitLab Pages, and its /download/ links lead to the newest stable release’s files.
  • Verification. hmailserver-6.3.4-SHA256SUMS.txt gives the SHA-256 of every file of the release but itself and its signature, and is signed with the release tags’ SSH key (hmailserver-6.3.4-SHA256SUMS.txt.sig, checked with ssh-keygen -Y verify); the installer, the .deb and the .rpm each have an SBOM of their own (<file>.cdx.json); and Get-AuthenticodeSignature on the installer should answer Valid, signed by Progressive Robot Ltd. There is no Sigstore bundle to verify (§4.1).

6.3.3 — large messages over BDAT, a webmail rebuilt, CardDAV

  • Read this first if you are upgrading from anything older than 6.3.1 (GitHub issue 263). 6.3.2 could not upgrade a database whose schema was older than its own: the guard added in 6.3.2, which makes a database the server has refused answer COM with the refusal instead of crashing, covered the whole Settings object, and DBUpdater reads the script directory from it before it runs the first script — so every upgrade from 5.x or 6.2.x stopped with The server has not loaded its configuration and left the database untouched. 6.3.3 upgrades such a database in one go; nothing in the database was changed by the failed attempt. A regression test now walks DBUpdater’s path over COM against a database the server has refused.
  • Read this too if you run 6.3.1 or 6.3.2 and use the Control Panel’s live update. The first real update, 6.3.1 to 6.3.2, failed with the installer’s exit code 5 because the Control Panel that started it kept its own files open. That is fixed here, but the helper that runs a live update is the one already installed, so an installation on 6.3.1 or 6.3.2 has to be updated by hand once: close the Control Panel, run this installer. Live updates worked from then on until GitHub suspended the project’s account on 18 September 2026; every server installs 6.3.4 by hand (above).
  • Delivery: an outbound BDAT chunk larger than one send buffer no longer stalls (GitHub issue 261). Every message over 60,000 bytes to a server advertising CHUNKING — Gmail, iCloud, Outlook.com, Postfix — had stalled after its first buffer and timed out at the remote, ever since outbound BDAT arrived in 6.2.28; a message that fit one buffer went, which is why short ones did. The operation queue now runs once more after a read has started, so a write behind it goes at once. Two regression tests relay a 222 KB message over BDAT, pipelined and not. Until you upgrade, OutboundChunking=0 in hMailServer.ini is the workaround. The full-text indexer also no longer reports an error for a message deleted before its terms were saved.
  • The webmail, rebuilt to the shape of Gmail and Outlook.com. The page is real files now (Portal.html, Portal.js), embedded into the binary at build time: a top bar with one search box, a navigation column, a list with star, avatar, snippet and hover actions, a reading pane on the right, below or off, a toast with Undo after every move, the keys a mail client has, light and dark. Compose docks, minimises or pops out; replies are written under the message they answer; an attachment reminder asks once. The list gains drag-to-folder, a right-click menu, pin ($Pinned), block sender, sweep, shift-click and ctrl-click selection. Mute ($Muted) and an inbox in tabs — Primary, Social, Promotions, Updates and Forums, or Focused and Other — sorted by the server from the headers alone. Search offers the last ten searches, completes a contact’s name to from:, and understands fourteen more operators (cc:, bcc:, filename:, larger:, smaller:, older_than:, newer_than:, is:muted, is:pinned, category:, has:link, in_reply_to:, -word, OR). Follow-up dates ($FollowUp, $Due-YYYY-MM-DD), nudges, quick steps and clean-up conversation round it off, in twenty languages, with 224 CI checks on the page’s script.
  • CardDAV (RFC 6352): the account’s address book on phones and desktop clients. On the web services listener /.well-known/carddav redirects to /dav/; the principal answers the address-book home, which holds one book, Contacts — the one the webmail keeps. The card a client sends is the card it reads back (stored beside the contact, schema 6040, vCard 3.0 or 4.0). HTTP Basic as the account or an application password, over HTTPS only, with the IMAP logon’s lockout and auto-ban; addressbook-query, addressbook-multiget and sync-collection reports; ETags with If-Match and If-None-Match. iOS, macOS Contacts, DAVx5 and Thunderbird speak it. The web services HTTPS listener has to be on (WebServicesHttpsPort).
  • The Linux Control Deck, measured and moved. build/check-deck-parity.py counts every field the desktop Control Panel writes against the REST API and the Deck; hmailserver/docs/DeckParity.md is its report. Of 330 properties, 240 were writable over REST and 153 reachable from a Deck view at the start of 14 September; 328 and 322 by its end, the two not writable being groups and their members. New write routes for the anti-virus group, sixteen more account and nine more domain fields, distribution-list settings, DNS blacklists, SURBL servers, white-list addresses, blocked senders, incoming relays, blocked attachments, the greylisting white list, the cache group’s ceilings, an IP range’s expiry, and — under an account, for the administrator — its application passwords, folder permissions and messages. The Deck gained its own harness (292 checks, in CI), full domain editing, an IP-ranges view, fetch-account and backup views, the account editor in full, distribution lists and aliases, and the scripting, cache and indexing groups.
  • The Linux regression suite’s route backlog is largely lifted. A REST route for every fixture family the Linux run had skipped, the fixtures rewritten so that one skips only against an older server whose OpenAPI lacks the route. The hosted Linux run on the release tree: 1,520 tests, 1,289 passed, 0 failed, 231 skipped. Three server defects found on the way (a vacation save PostgreSQL refused, ncsa refused as a log format, a negative thread id on Linux) — and one on every Windows installation: a domain’s relay password could not be saved, because the column was 255 characters and a DPAPI envelope is 314. Schema 6039 widens it.
  • Security, from the release’s own review. The [Settings] section of hMailServer.ini over REST answered to any API key — a read-only key could read the OAuth2 HMAC secret, the password pepper and the service account’s password, and a write key could set AutoBanCommand. The four ini routes answer to the administrator password only now. A key restricted to a domain could lift that domain’s limits through PUT /api/v1/domains/{domain}; it is refused with 403. In CardDAV, a REPORT body of a million ampersands cost a worker thread tens of seconds (the entity scan is bounded), a multiget answered the same href as often as asked (a repeated href is answered once; a multistatus over 64 MiB is refused), and on MySQL the card column could not hold the megabyte the collection advertises (it can). Two tests hold the two key-scope findings.
  • Database. On PostgreSQL a backslash is stored as one backslash: the escaper doubled it unconditionally, which is right only while standard_conforming_strings is off, and it has been on by default since PostgreSQL 9.1. Values already stored doubled stay as they are — edit and save them once. Schema 6039 (relay-password column) and 6040 (the CardDAV tables) both upgrade in place.
  • Build and supply chain. One licence header on every source file, checked by build/add-license-headers.py; the container image job of a tag run asks Docker for the image by a lower-case name (at 6.3.2 the image was pushed but its smoke test was refused the capital letter); the Linux suite’s shims learned the anti-virus group and the nine domain properties.
  • Verification. The full regression suite ran on the stamped binary: 2,302 tests, 2,294 passed, 0 failed, 8 skipped; the assertion build ran the same suite first with nothing asserted; three clean Release builds produced the same hMailServer.exe 6.3.3.42, SHA-256 2c57f78050ecc43ee6d69ac3a2dc42b1dd802cc35ec369c09457d8119d53510a. The installer has two hashes, deliberately: as built, 79,926,537 bytes, 45a9d7f1a78bb31031dde27849fa21828a60aff178f32cbc2b0d9cd93f6b33f2; as published, after Authenticode signing, 3188be2fa16a4c43d3b43ddbd7274e4cfe878c082343ef25ffbd00489610a057. The published value is the one to check a download against.

6.3.2 — a webmail people would choose, Kerberos single sign-on, auto-ban at the firewall

  • The webmail became a mail client. The self-service portal at /portal shipped in 6.3.0 with a mailbox page; fifteen waves later it reads conversations grouped by thread, archives, junks and deletes with one key, searches the way a reader writes a search (from:, to:, subject:, has:attachment, before:, after:, is:unread, label: and the rest), and keeps labels as IMAP keywords (schema 6036). Writing has a formatting bar and an editor whose HTML is rebuilt from an allowed subset, a From picker offering exactly the addresses the server would accept from the account, a signature and templates. S/MIME in the browser: the page signs, verifies, encrypts and decrypts with the Web Crypto API; the private key is wrapped under a key derived from the account password and stored on the server so wrapped (schema 6037) — the server cannot open it. It installs as an app, keeps the inbox listing and the last thirty opened messages for reading offline, imports .eml files, exports a folder as mbox, takes a name, logo and announcement from an administrator (PUT /api/v1/portal/branding), speaks the Control Panel’s seventeen languages plus Greek, Korean and European Portuguese, and works with a screen reader. Everything is over /api/v1/me, described in the OpenAPI document.
  • Kerberos single sign-on — SASL GSSAPI (RFC 4752) on SMTP, IMAP and POP3, on Windows. A client presents a ticket for this server’s service principal, the server proves itself back, and the client’s principal names the account. Off unless GssapiEnabled=1 in [Settings]; the credentials are the process’s own on a domain-joined host, or a service account named by GssapiServiceAccount and GssapiServicePassword. Proven end to end against a Windows Server domain controller.
  • An auto-ban reaches the operating system’s firewall. With AutoBanFirewall=1, Windows gets an inbound block rule per banned address in Windows Defender Firewall, in a rule group of its own; Linux gets a packaged hook that keeps an nftables set, and a fail2ban filter and jail. AutoBanCommand runs anything else, and AutoBanNeverBan lists the addresses that are never banned. All three are off as shipped. Making an app password now requires the account’s own password. A SURBL server’s answers are judged the way a DNSBL’s are, against an expected result (schema 6038) — until now a server resolving through a public resolver tagged every message carrying a link as spam.
  • The Linux packages install on the next distribution release. The 6.3.1 .deb depended on the builder’s exact Boost sonames and would not install on Ubuntu 26.04; Boost is linked statically now, and the package-install job installs the .deb on Ubuntu 26.04 in a container on every run. A container image is built from every tag, run as its own user, smoke-tested beside a PostgreSQL over TLS. The Linux build is green on every push.
  • Database. The 6029 → 6030 upgrade step sweeps parents before children — the defect 6.3.1 named as known and unfixed is fixed in all four backends, and a gate now executes a schema upgrade on a seeded database on every push, winding a created database back to 6029 and forward again. Schema 6032 to 6037: contacts, account preferences, scheduled sends and snoozes, files sent as links, message keywords, S/MIME keys; 6038: SURBL expected results. The fresh-schema create scripts drop dependent tables before hm_accounts, and every statement in a MySQL script is followed by a blank line, with a check that keeps it so.
  • Build and supply chain. Thirty-seven of the forty committed binaries left git; a manifest (hmailserver/docs/third-party-binaries.json) says how each is obtained and build/get-installer-binaries.ps1 fetches them from the build-inputs-1 release against the manifest’s SHA-256. Beside every cosign bundle a .sigstore.json twin, and SLSA build provenance for the assets as attached, verified with slsa-verifier after upload. Every Linux job runs behind Harden-Runner with its service images pinned by digest.
  • Quality and tests. Code Quality is at zero findings and stays there; native coverage can now be measured; the regression suite can drive a console server; the pre-flight refuses to pass while WSL is running. A server holding a database it refused answers COM with the refusal, not an access violation (HM5011) — a guard that reached too far: it covered the whole Settings object, so 6.3.2 could not upgrade a database whose schema was older than its own. Upgrade straight to 6.3.3 or later, which puts it right. The Control Panel’s first live update, 6.3.1 to 6.3.2, also failed with the installer’s exit code 5; 6.3.3 fixes that too.
  • Documentation and governance. SECURITY.md states the vulnerability management policy the automation enforces and an OpenVEX statement accompanies it; GOVERNANCE.md names two maintainers; the release checklist says that releases are immutable once published.
  • Verification. The full regression suite ran on the stamped binary: 2,219 tests, 2,211 passed, 0 failed, 8 skipped; the assertion build ran the same suite first with nothing asserted; three clean Release builds produced the same hMailServer.exe 6.3.2.41, SHA-256 4c7d5a2c7fa6537e460eadd6dcc85d5aedb3559fe349e2dec9547fa99086d3e8. The installer as built, 79,683,220 bytes: 746f17303d219930aaed03d836f3c2c55e367bcd59620ba60e16eae7efb5f589; as published, after Authenticode signing: d10eb643c7d4717655da19b8facbd4916d393024ba7d18ac5ccde492f2140de9. The three MIME fuzzing harnesses ran thirty minutes each on this release’s source: no crash, no hang, no violated assertion.

6.3.1 — a signed installer, an unchanged server

  • The Windows installer carries an Authenticode signature — the first release of this project that does. It is signed with Azure Artifact Signing against a certificate profile issued to Progressive Robot Ltd after Microsoft’s identity validation, and it is countersigned (RFC 3161). On the downloaded file Get-AuthenticodeSignature reads Valid, signer CN=Progressive Robot Ltd, O=Progressive Robot Ltd, L=Chester, S=Cheshire West and Chester, C=GB, issued by Microsoft ID Verified CS EOC CA 03 and countersigned by Microsoft Public RSA Time Stamping Authority — which is what keeps the signature valid after a signing certificate that lives about seventy-two hours has expired. What it changes for a reader: the elevation prompt names the publisher instead of reading Unknown publisher, and an enterprise policy that refuses unsigned binaries outright no longer refuses this one.
  • SmartScreen still warns. Said plainly, because the signature is easy to overread: Microsoft’s own comparison puts a signed installer in the same row as an unsigned one, flagged unrecognised until reputation accumulates, and reputation attaches to a file that does not change — which a new 80 MB installer every few weeks never is. An EV certificate would not help; Microsoft removed EV’s SmartScreen bypass in 2024. Expect the same Windows protected your PC screen, now carrying the real publisher name.
  • Only the Windows installer is Authenticode-signed, and Sigstore is unchanged. There is no Authenticode for a .deb, an .rpm or an AppImage, so Sigstore remains the check that means something for every asset on the release: each carries a .cosign.bundle beside it, and cosign verify-blob against that bundle is the verification to run. Releases before 6.3.1 are unsigned and will stay unsigned — a published release here is immutable, so 6.3.0 could not be signed retroactively and never will be.
  • UpdateRequireAuthenticode becomes usable. The setting has existed since 6.2.28 and defaults to 0; until this release, setting it to 1 made the server’s own update path refuse every release of this project, because none carried a signature — the check is WinVerifyTrust on the downloaded installer, and an unsigned file is refused with the file carries no Authenticode signature. From 6.3.1 the installer it downloads carries one. The check is Windows-only: on Linux there is no Authenticode to verify, the server says so rather than reporting a pass, and updating is the package manager’s job.
  • Nothing in the server changed. The compiled server differs from 6.3.0 by its version stamp and one comment line. Everything else in the release is the path a release travels — the workflows that sign and verify it, found wanting while preparing to use the certificate for the first time — plus three documentation corrections: the README offers the Linux packages on its download line rather than 140 lines further on; two files that named a version 6.2.29, which does not exist and never will — the [Directories] header comment in the server source and the relocating guide’s note on when a relative [Directories] path began to resolve against the program folder (a 6.3.0 change, described below) — now say 6.3.0, where both shipped; and the release checklist gives ARTIFACT_SIGNING_ENDPOINT as a full URI rather than a bare host, which is the form the signing action documents. Comment and prose only. Among the release-path repairs, each of which could have put out a permanently wrong release: the gate that guards signing checked the wrong four of the six settings it protects and let five half-configured states through; the release is now verified as it stands on the release page rather than as it left the signing job, in both directions, an asset with no bundle and a bundle whose asset has gone; both SBOMs are a hard failure before a code-signing certificate is spent on the release; the countersignature is proved before the release asset is replaced; a verification step that matched nothing can no longer pass having checked nothing; and runs are serialised per tag rather than interleaved. So do not read the version bump as new server behaviour — there is none.
  • Verification, and a download that now has two hashes. The suite ran on the stamped binary: 2,175 tests, 2,166 passed, 0 failed, 9 skipped (the seven explicit IMAP stress tests and two ignored), in 40 minutes, with no ERROR log written. The assertion build ran the same suite first with nothing violated, and two clean builds produced a byte-identical hMailServer.exe 6.3.1.40, SHA-256 8b0ab1a2d1f3645f1e12c3bca1315023474bb3a63abf0efb44f60ba0b2d83235. No parser changed, so 6.3.0’s fuzzing run stands, which the checklist allows for a patch release. The schema is still 6031 and nothing new is on by default. The installer, though, now hashes two ways, deliberately: as built it is 79,408,192 bytes, SHA-256 3986fbcccda9e46fee29c1e538a79f2423ebc4d5e3f6e59306ffba4cc7bd8cba; as published it is 79,423,896 bytes, SHA-256 68c72a60e7a8530bdd32d28b60eaadab5b9c90505e174c03196e5dfa58ac9b70. The signing job replaces the installer asset with the signed one before cosign runs, so the bytes on the release are not the bytes the build produced — this is the first release for which that is true, and the published hash is the one to check a download against.
  • Known and not fixed here: the orphan sweep in the 6029-to-6030 upgrade step runs children before parents (chapter 18). Before that step adds its seventeen foreign keys it deletes rows whose parent is gone; three of the parent tables are themselves pruned by those deletes, and each one’s children are cleaned earlier, against a parent that has not yet lost its rows — so pruning an orphaned account, fetch account or distribution list re-orphans rows nothing revisits, and the constraint that follows is refused. The scope, stated completely: it fires only on an upgrade from a schema below 6030 on a database that already holds orphaned rows; a database without them upgrades correctly and a fresh installation is never on this path; it is in all four database backends; when it does fire it fails loudly, in the database engine’s own words, and rolls back rather than doing anything quietly; and no installation has reported hitting it. The fix is a reordering of seventeen delete statements and shipped in 6.3.2, with the gate that executes a schema upgrade on a seeded database on every push — the gate that was missing when the defect got in. It matters here because an installation still on schema 6011, which is where 6.2.19 to 6.2.21 sit, crosses this step on the way up: take the database and data-directory backup chapter 18 asks for.
  • The report that led to that review — a SQL Server Compact upgrade from schema 6011 refused at the same step — turned out to be GitHub issue 114, already fixed in 6.2.26: the upgrade had succeeded, the verification probe crashed the database provider afterwards, and the tool then printed a cause it had no way to know. That installation upgrades on 6.3.0 and on 6.3.1.

6.3.0 — Linux, a REST write surface, the Control Deck, webmail

  • hMailServer runs on Linux (chapter 35). A server that had been Windows-only since 2002 now builds, installs, runs and delivers mail on x86-64 and AArch64 Linux from the same source tree — which is the whole reason this is 6.3 and not 6.2.29. Every core translation unit is counted as it compiles, one file at a time, under clang on x86-64 and on a native AArch64 runner rather than a cross-build, and the job fails when a single file stops compiling; a third job builds and links the whole core with GCC on x86-64, which is what stops the port quietly becoming clang-only. The Windows build is the same MSVC project it was — nothing in this release changes what it compiles or how it behaves.
  • Installed the way a Linux service is installed. A .deb and an .rpm for both architectures, a PKGBUILD and an AppImage, carrying the binary, the schema scripts, a systemd unit that runs the server as its own user, a logrotate rule, the configuration under /etc/hmailserver, and the Control Deck. hmailserver --create-database, --upgrade-database, --set-admin-password and --check-config do the setup. Proven rather than assumed: against PostgreSQL 18 and MariaDB 11.8 the schema is created, a domain and account made over the API, a message submitted over SMTP, delivered and read back over IMAP; directory authentication runs against a real slapd over StartTLS and LDAPS. Stored secrets are AES-256-GCM under a key file the installation owns, which is what DPAPI does on Windows. The AArch64 packages are built and installed by CI on an ARM runner; nobody has installed one on ARM hardware by hand yet.
  • The REST API writes what only COM could write (chapter 24). PUT /api/v1/settings and its anti-spam and logging groups write 108 settings, each through the same setter the Control Panel calls and applied only when every key in the request is accepted — the three groups describe 113 keys in all, of which five are read-only facts about where the log is written rather than settings, and the relayer password is written and never read back. Also global rules with their criteria and actions, SMTP routes with their address lists, aliases, accounts including the password and administrator level, certificates and the listeners that bind them, and POST /api/v1/server/reinitialize — what the Control Panel’s Reinitialize does, so a new listener takes effect without stopping the process. Every route is refused to a read-only key, refused to a key issued for other domains, and scoped to its own domain where a domain owns it. The OpenAPI document is now valid JSON; it never was before this release. What it still does not write is said as plainly: a domain’s own properties beyond its active flag and its postmaster, and above all DKIM signing — a read route and no write route — and with it the per-domain size limits, the domain signature and a per-domain relay host.
  • The Control Deck writes, and signs in properly (chapter 36). The page the REST listener serves at / gained editors for most of that: rules, routes and listeners are created, edited and deleted, a certificate is added and deleted, an account is created and deleted under a domain, and a button restarts the services in place. (The release notes say certificates are edited too; there is no route that edits one, on the page or off it — replace a certificate by adding the new one, repointing the listeners and deleting the old.) Its settings forms are generated from the server’s own OpenAPI document, so every setting the API accepts appears with its type, its permitted words, whether it is read-only and whether it takes effect on restart. It no longer holds the administrator password in the browser: POST /api/v1/session exchanges it once for an HttpOnly, SameSite=Strict cookie that ends when the administrator password changes, a write must carry X-Requested-With, and an API key cannot mint a session. Four write surfaces the API has are not on the page yet — domains, aliases, distribution lists, and changing an account that already exists. (The release notes count three, omitting distribution lists; the page carries no control for those either. Chapter 36 lists all four with the routes that do the work.)
  • The /portal page gains what a webmail needs beyond reading and sending (chapter 36). It has addresses — #/f/<id>, #/m/<id>, #/search, #/compose — so a folder or a message can be linked to, Back and Forward work, and a half-written reply survives a reload. Folders are created, renamed and deleted from the page itself (POST /api/v1/me/folders, PUT and DELETE /api/v1/me/folders/{id}), a name judged exactly as IMAP’s CREATE, RENAME and DELETE judge it and refused in IMAP’s own sentences; the page offers no delete for the inbox or for a folder the server has designated for a special use, because a page has no undo. GET /api/v1/me/changes hashes the per-folder message and unseen counts into an opaque token and answers changed against the one handed back, which the page polls every six seconds and stops polling while the tab is hidden. Inline images work by a route that was measured rather than assumed: the body is rendered in a frame sandboxed without allow-same-origin, so its subresource requests are cross-site, so the SameSite=Strict session cookie never travels and the attachment route answers 401 — the page therefore fetches the attachment with its own credentials and hands the frame the bytes as a data: URL. What is inlined is decided by the server’s own Content-Type on the download rather than by the message’s claim, and remote images stay off. The markup and the script are two responses, /portal and /portal.js, so the page’s Content-Security-Policy can allow no inline script at all; both still come out of the binary rather than out of files beside it, and the page fetches no font, no image and no stylesheet from anywhere. build/check-portal-script.py now lifts the two out of the C++ literals and runs the script against a small DOM and a stubbed API — 88 checks, where what stood there before was a test that asserted certain substrings appeared.
  • Three Linux-only defects fixed before anyone could meet them: a non-ASCII folder name was mangled in both directions by the modified UTF-7 conversion, a command line with a byte above 127 was cut short (which truncated an IMAP SEARCH CHARSET UTF-8 and a MAIL FROM under SMTPUTF8), and a TLS key-exchange group list OpenSSL rejects was logged once per listener and per outbound delivery — hundreds of medium errors an hour on the OpenSSL Debian and Ubuntu ship, for a condition the fallback already handled. On Windows, the administrator’s user name is no longer translated in the Control Panel’s sign-in box, which had made a fresh Chinese, German or Swedish installation refuse the credential it had just asked for (GitHub issues 156 and 177), and a diagnostic lists every directory the server was configured with and whether it exists (GitHub issue 158).
  • A relative path in [Directories] now resolves against the program folder. The five directories other than ProgramFolder may be written relative from 6.3.0, so a new installation can be made movable without seven absolute strings that have to agree. An existing installation is untouched — its absolute values are left exactly as they are, because rewriting a configured server’s paths during an upgrade is not something an upgrade may do. (Two files described this as arriving in “6.2.29”, a version that does not exist; 6.3.1 corrected them. It shipped in 6.3.0.)
  • No schema change — 6031, as 6.2.28 — and nothing new is on by default. The REST API still needs RestApiPort, which is what the Control Deck and the portal are served on, so a Windows server upgraded without touching its settings behaves exactly as 6.2.28 did. The suite reports 2,175 tests, 2,166 passed, 0 failed, 9 skipped, run first on an assertion-enabled build with nothing violated; it also ran against the Linux server on PostgreSQL, 260 of its own files compiled in place under a REST-backed fixture layer, for 756 passed, 0 failed, 649 skipped of 1,405 — and those skips are the API’s gaps, not the server’s. Three limitations the release states plainly: per-domain DKIM cannot be configured on Linux (a read route, no write route, a PUT answers 404), so a domain that must sign its outbound mail is not one to run there today; moving a Windows installation to Linux is not a supported migration — same schema, same on-disk message format, no tested path; and its installer carried no Authenticode signature, leaving the Sigstore bundle — the check that meant something on every asset of every release from 6.2.19 to 6.3.3 — to do that work alone. Two of those have since changed. 6.3.1’s installer is signed, though a published release is immutable, so 6.3.0’s own installer stays unsigned; and since 6.3.3 a domain’s DKIM signing is written with PUT /api/v1/domains/{domain} or on the Control Deck (§36.5). 6.3.4 carries no Sigstore bundle at all; the 6.3.4 entry above says how it is verified.

6.2.28 — self-update, webmail, and seventeen languages

  • The server can update itself, and every step of it is opt-in. UpdateCheckEnabled=0 is the default and nothing happens at all until it is set — no request, no identifier, no counts. Turned on, a scheduled task reads the project’s release feed every UpdateCheckHours (24) and the Server status page reports what it found; fetching an installer and applying it are two further opt-in steps. What is applied is what the project signed: the installer is verified against its Sigstore bundle — chain to Fulcio, identity and issuer the release workflow, entry in the public transparency log — before it runs. The apply survives its own failure, reinstalling the previous version if the service does not come back within UpdateServiceWaitSeconds (180), and the database upgrade authenticates with a single-use token revoked at every service start rather than the administrator password. The upgrade to 6.2.28 is manual for everybody.
  • An account can use its mailbox in a browser — /portal on the REST listener, with /api/v1/me behind it. It answers to an account’s own credentials and nothing else, no administrator password and no API key, and signing in exchanges the password for a session cookie so the password is sent once. Folders with counts, threads, flags, move, delete, search over the account’s own mail, attachments served under a type a browser will not execute, and shared and public folders alongside its own; it sends text or HTML with attachments, every address through the checks a submitted message meets. It also carries the account’s quarantine, forwarding, signature, vacation reply, active Sieve script and a password change — which is why the password-expiry text now says an expired password is renewed by an administrator or by the person themselves, if they hold an app password.
  • A real HTTP server underneath. The REST API and the web services (autoconfiguration, autodiscover, the mobileconfig profile, the CalDAV and CardDAV redirects) had been running on a single-threaded HTTP/1.0 loop. They now run on HTTP/1.1 over Boost.Asio with its own io_context and four workers, keep-alive, chunked bodies, header and body limits, separate header and body deadlines, and Cache-Control: no-store on every API answer. That is what made the other two possible.
  • The Control Deck arrives as the page the REST listener serves at /, reading the server’s settings, the log files, the certificates, and the global rules in evaluation order. At this release it reads only — writing is the Control Panel’s and COM’s, and because a browser session for the administrator was still owed it held the administrator password in sessionStorage while open. Both are fixed in 6.3.0. Also IMAP COMPRESS=DEFLATE (RFC 4978), with STARTTLS refused once a session is compressed, and the Control Panel in seventeen languages beside English — every page, dialog, message, hint and verdict sentence, 3,376 texts and not a subset, with CI checkers that fail the build on an untranslated new caption.
  • A masked password was typed backwards from the second character (GitHub issue 156): 12345678 became 18765432 with the password hidden, while revealed text was fine — the box restored the caret to the index it had read before the keystroke. Saving the SSL/TLS page dropped the post-quantum key-exchange groups and saving the Scripting page took every COM object from every script, both because a settings editor shipped without the server’s absent-key default and saving a page writes every field. Five defects in the live update and ten in the portal were found by reading them back against the tree before either had shipped. Schema 6031, unchanged; nothing new is on by default, and a server upgraded without touching its settings behaves exactly as 6.2.27 did.

6.2.27 — DNSSEC proof of absence, IMAP folder mirroring

  • A missing DS record is now proved missing, not assumed (RFC 4035 §5.2, RFC 5155 §8). The validating resolver that guards DANE and the SPF, DKIM and DMARC lookups treated a DS query answered with nothing as an unsigned delegation — which is exactly what an attacker stripping the DS from the answer in transit presents, so a signed zone quietly became an unsigned one and took DANE and validated TXT with it. The resolver now keeps the authority section of a negative answer and requires the parent’s proof there: an NSEC at the delegation name with NS set and DS clear, an NSEC3 whose hashed owner matches with the same bits, or an Opt-Out NSEC3 covering the hash, each signed by the parent’s key. A proof that fails to verify, has expired, claims a DS exists or belongs to another name is no proof, and a delegation without one under a signed parent is Bogus rather than Insecure. An unsigned parent still makes everything below it Insecure, and an unreachable resolver still degrades to Insecure rather than lying. Twelve tests run against a chain the suite signs itself.
  • An external IMAP account can mirror every folder (FetchAccount.MirrorFolders, schema 6031; in the Control Panel, Mirror every folder). 6.2.25 collected the remote INBOX and nothing else. With mirroring on, the server lists the remote mailboxes and collects each into the local folder of the same name: every message byte for byte, its \Seen \Flagged \Answered \Draft \Deleted flags, its internal date, the remote hierarchy delimiter mapped to the local one. Nothing is delivered — no header, rule, anti-spam or anti-virus touches a copy, because it is mail the other server already accepted. Each folder keeps its own collection record, so a second poll takes only what is new. This is the imapsync shape, and the migration route the project never had.
  • The Import Tool reads a Maildir — the directory with cur, new and tmp — importing the INBOX and every Maildir++ folder beside it, with the flags the file names carry and line endings made CRLF. hmailserver/docs/Migration.md documents every route: from another IMAP server, from mbox, from a Maildir, from Outlook through IMAP, from the upstream hMailServer in place, and accounts in bulk. PST is deliberately not parsed, and the page says why.
  • A message imported into a folder a client has open now appears at once; the import told nobody the folder had changed, so a session with the list cached saw the copy only after a restart. Schema 6030 → 6031 is one column on hm_fetchaccounts, added on every backend.

6.2.26 — the Compact upgrade that reported its own success as failure

  • Every SQL Server Compact upgrade through schema 6030 was reported as failed after it had in fact succeeded, and the false verdict took the service down (GitHub issue 114). The updater proves each step by running a probe statement; the four probes for schema 6030 — the foreign keys — were written with case when exists (subquery) in the SET expression, which is valid on SQL Server, MySQL and PostgreSQL and an access violation inside the OLE DB provider on SQL Server Compact, on a correct database with every constraint present. The server reported it as HM10045 Unknown error, the updater declared that Upgrade6029to6030MSSQLCE.sql had not created fk_hm_accounts_domain, the installer said the database could not be upgraded, and two seconds later the crash oracle — which treats a memory-safety fault as fatal — ended the service. The probes are now written so that a present constraint matches no row and an absent one fails by division by zero, on every backend, leaving hm_dbversion untouched.
  • If the 6.2.25 installer failed on your database it is at schema 6030 with its foreign keys in place, and this installer finds nothing left to upgrade; if you restored a backup from before, the whole chain runs and verification passes.
  • The verification itself was tightened as part of the fix: the create-script check now runs every probe against the database it builds, through the provider the server uses, with a negative control that must fail, and a regression fixture runs the probes through the same COM path the updater takes. Both fail against the 6.2.25 statement. Nothing else in the server changes, and the schema stays 6030 — the release changes the verification probe, not the schema.

6.2.25 — the ACME crash, IMAP fetching, schema 6030

  • ACME issuance and renewal ended the process (GitHub issue 93). Two calls in the ACME client handed the OpenSSL DLL a FILE* opened by the server’s own C runtime — the DANE TLSA computation logged straight after issuance, and the private-key re-read at the start of every renewal, which is the default. OpenSSL routes stdio back into the executable through an OPENSSL_Applink export this executable has never provided, and its answer to the missing export is not an error return: it logs “OPENSSL_Uplink(…): no OPENSSL_Applink” and calls TerminateProcess. The whole symptom set was an OpenSSL Application-log event whose message looks blank, a 7031 from the service control manager in the same second, no crash dump, and no “ACME (automatic)” certificate record. Both calls now use OpenSSL’s own file I/O, and deployment runs before the TLSA line. If 6.2.24 issued a certificate before dying, the files under Data\ACME are valid and 6.2.25 deploys them at its first ACME check after start-up. Two simultaneous restarts — one over COM, one from an ACME deployment or a backup restore — could also end in an access violation; restarts now run one after the other.
  • Mail flow. External accounts can be collected over IMAP by UID, with LOGIN or XOAUTH2 over STARTTLS or TLS — the INBOX only at this release; folders are not mirrored until 6.2.27. Message retention by stored creation time and never by the sender’s Date; an archive index with one row per copy, searchable over COM and GET /api/v1/archive, with a legal hold that survives retention and address erasure. Outbound pipelining and chunking are on by default where the remote advertises PIPELINING and CHUNKING. SEARCH BODY and TEXT now read text-bearing attachments, index on or off.
  • Authentication. SASL EXTERNAL on SMTP, IMAP and POP3 — and with it the AUTH line limit raised from 500/510 octets to 12288, RFC 5034’s figure, so a real RS256 bearer token fits. OAuth2 JWKS and token introspection, both off unless configured; scrypt as a password hash, with schemes compared by strength rather than by number; tarpitting on the connection’s own timer, off by default; and the administrator’s second factor is now the server’s — COM needs AuthenticateWithCode, REST needs an X-hMailServer-OTP header. SmtpAuthenticatedSenderCheck confines an authenticated session to addresses its account owns, and ScriptAllowedObjects bounds what an event script may CreateObject.
  • Schema 6025 → 6030 in five steps, one way: a settings row, two retention columns, hm_metricsamples, hm_archiveindex, and then seventeen FOREIGN KEYs with ON DELETE CASCADE, which removes the orphan rows they would refuse and reads every child table once on a large database. The REST API reaches what was COM-only for reading — IP ranges, distribution lists, certificates (never the private-key password), DKIM, global rules, logs, backup, a settings snapshot — while writing settings, rules and certificates deliberately stays COM-only until 6.3.0, and writing DKIM until 6.3.3.
  • OpenSSL 4.0.2 (a security patch release closing eleven CVEs, the most severe Moderate) and Boost 1.92.0; every TLS context built with SSLv2, SSLv3, TLS 1.0 and TLS 1.1 off; all fourteen open CodeQL C++ findings dealt with and the weekly C++ analysis running again on a hosted runner. Eight behaviours change on upgrade without a switch, of which two are worth knowing: the Apple .mobileconfig profile is served over HTTPS only, and the four HTTPS clients — ACME, the OAuth2 token client, the MTA-STS policy fetch and the introspection/JWKS client — require TLS 1.2 or later whatever the mail protocol toggles allow.

6.2.24 — the two pre-release lines land, schema 6011 to 6025

  • The first stable release since 6.2.21, and the vehicle for everything the two pre-release lines carried. No stable release ever shipped as 6.2.22 or 6.2.23: the work was done in 6.2.22 pre1, pre2, pre3 and pre6 and in 6.2.23 Alpha 1 and Alpha 2, and 6.2.24 is the code of Alpha 2, cut the same day, plus NuGet lock files beside all nine .NET tool projects with every CI restore held to them. Nothing compiled into the server, the tools or the installer changed otherwise. (pre4 was withdrawn — its installer could not create its own database — and pre5 was republished as pre6 so the SBOMs and Sigstore bundles could be attached.)
  • Mail that was silently lost is not any more. A delegated APPEND, COPY or MOVE filed bytes under the wrong account, and a MOVE destroyed the only readable copy. Local delivery could lose a message outright: the recipient row and the queued message were deleted after a failed account-level copy, with the sender already told 250. Every Thunderbird Sent copy was refused once UTF8=ACCEPT was enabled (GitHub issue 53), a kept-open log was never flushed for up to forty minutes (GitHub issue 33), and the built-in ACME client had never once succeeded against real Let’s Encrypt (GitHub issue 34) — Boulder pretty-prints its JSON, and the challenge locator searched for the compact spelling.
  • Three anti-spam bypasses closed. A sender could un-mark their own mail by forging a header into a per-account spam override; a sender chose which address the DNSBL, SPF and HELO tests ran against, because the Received-header parser took the first bracketed value after from — the client’s own HELO literal — rather than the address the receiving server observed; and RSET before EHLO, or STARTTLS without a fresh EHLO, opened a transaction with an empty HELO host, skipping the HELO-host test and the OnHELO/OnEHLO script events.
  • Features, all of them inherited from the pre-release lines. Shared and delegated IMAP mailboxes, a full-text index, RFC 3464 machine-readable bounces, BINARYMIME, per-account spam thresholds, a sender blacklist, domain-wide out-of-office, distribution-list moderation, Windows Event Log integration, OTLP metrics and logs, a PROXY-protocol front for SMTP, an external HTTP filter hook, eleven IMAP extensions (APPENDLIMIT, LITERAL-, LIST-STATUS, UNAUTHENTICATE, QUOTA, PREVIEW, MULTIAPPEND, REPLACE, SAVEDATE, METADATA, BINARY), Microsoft 365 XOAUTH2 in both directions, app passwords, per-account TOTP enforced by the server, an admin-reviewable quarantine, password expiry and history, a queryable message trace, per-domain outbound relay, DMARC aggregate reporting, the DMARCbis DNS tree walk, an ACME renewal window sized as two thirds of the certificate’s lifetime plus ARI, and a disk-space floor. More than fifty new INI settings, 23 of them from 6.2.23 Alpha 1; most shipped off or inert, but not all. Among those that act as shipped, MinimumFreeDiskSpaceMB refuses mail below 100 MB free (the last item below), WindowsEventLogEnabled at 1 sends serious errors to the Windows Event Log as well, DatabaseStatementTimeout sets a 30-second statement timeout on PostgreSQL and MySQL, and IMAPExpungeRetentionRecords prunes QRESYNC tombstones to 5,000 per mailbox.
  • The Control Panel and the database tools have run on .NET 10 since 6.2.19, which was worth restating here because this was the release most upgrades landed on. The installer bundles the .NET 10 Desktop Runtime and installs it when it is missing. The server itself is native code with no .NET dependency, but DBSetup, DBSetupQuick and DBUpdater are .NET applications, this release must move the schema, and the server refuses to start against an old schema — so coming from 6.2.18 or earlier the runtime is load-bearing on upgrade: a failed runtime install leaves no running mail server, not a working one without its administration tool. The repair by hand is the Desktop Runtime (x64) from dotnet.microsoft.com/download/dotnet/10.0, then DBUpdater.exe from the installation’s Bin folder, then start the service (chapter 18).
  • The schema moves from 6011 to 6025 in fourteen steps, one way, and two are worth a maintenance window: 6024 → 6025 rewrites hm_messages.messageflags from tinyint to smallint on MS SQL, SQL Server Compact and MySQL/MariaDB — a table rewrite on the largest table you have, holding locks (PostgreSQL is unaffected, its column was already smallint) — and 6022 → 6023 creates the full-text index tables, empty and inert. An older server refuses a newer database rather than misreading it, and there is no downgrade, so take a database and data-directory backup first. Seven behaviours change without a switch, the two most likely to be noticed being 503 Bad sequence of commands for a client that sends MAIL FROM or AUTH straight after STARTTLS without a second EHLO, and a temporary refusal of mail below 100 MB free (MinimumFreeDiskSpaceMB, set 0 to restore the old behaviour). Control Flow Guard on the server, a reproducible build, signed release tags, SBOMs and Sigstore bundles on every asset, and the COM vtable repaired to be binary compatible with 6.2.21.

6.2.21 — the Postfix hang, dot transparency, Sieve body

  • The Postfix relay hang is fixed — found by running a real Postfix. When Postfix has nothing further for a connection it sends the end of the body, the terminating dot and QUIT in one TCP segment — and every end-of-data check this server ever shipped examined only the end of the received buffer, so a terminator with QUIT behind it was never seen and the session waited forever. The receive path now finds <CRLF>.<CRLF> anywhere in what has arrived and hands the rest back to the command parser. Verified against Postfix 3.10, now part of the repository’s test rig. The bare-LF spellings are still recognised only at the end of the buffer — that is the CVE-2023-51764 SMTP-smuggling rule, pinned by its own test.
  • SMTP dot transparency across buffer boundaries, in both directions. A line-leading dot falling just inside a chunk could be sent unprotected — so .X silently became X, and a lone-dot body line truncated the message at that point.
  • Sieve scripts now follow renames and deletes. Renaming a domain or account left every filter behind (mail was delivered to INBOX with no error); deleting one left the script on disk, where recreating the address silently reactivated the previous holder’s filter — redirects included.
  • The Sieve body test (RFC 5173, §12.2) with all three transforms; anything unimplemented is refused at upload with an error naming the construct.
  • An empty-criteria SORT no longer spins a connection thread forever at 100% of a core; the folder message-list cache can now actually evict (it never could, and its accounting only counted upwards); a DNS answer Windows rejects as malformed is retried once over TCP; the MX-query tool asks the server’s own resolver (Utilities.ResolveMXRecords, chapter 31); and seven things the retired Administrator could do are restored in the Control Panel, including domain rename and pause/resume.

6.2.19 – 6.2.20 — directory provisioning, schema 6011, IMAP THREAD

  • Active Directory as an account source (§6.4): preview-then-apply directory sync with a Control Panel page and an optional unattended schedule. Opt-in per domain; nothing is ever deleted; verified against a live Windows Server 2025 domain controller.
  • The database schema moves from 6005 to 6011 — the first schema change since 6.2.1 (6004 to 6005). INI [Settings] values are also stored in the database for remote administration; the file won where both carried a value until 6.3.4, which made the database the settings store and [Settings] in hMailServer.ini its copy (chapter 21).
  • The custom-DNS-server regression in 6.2.16–6.2.18 is fixed. With one configured, every lookup through it failed — MX, DNSBL, SPF, SURBL — because the rewritten resolver set a destination port the Windows DNS client requires to be zero. Pinned by a negative-control regression test.
  • IMAP THREAD (RFC 5256, §34.3), both algorithms, with a pre-ship adversarial review that caught a stack overflow reachable through a deep reply chain. Sieve imap4flags actions now reach the stored message instead of being discarded at delivery.
  • 6.2.19 fresh installations failed — multi-statement batches in the create scripts broke SQL CE and MySQL, and a silent install hung on a hidden dialog. 6.2.20 fixes both (upgrades were never affected), makes all 28 installer dialogs take their default under /SUPPRESSMSGBOXES, and installs every release on a clean machine before promotion. Release assets are signed.

6.2.17 – 6.2.18 — bounded waits, and the relay stall bounded

  • The relay-stall mechanism, reproduced and bounded. For a trusted incoming relay the whole spam battery runs after end-of-data, on a bounded pool of 15 threads — and SpamAssassin’s wait had no ceiling, so a stalled scanner under concurrent load meant total silence after 354. SpamAssassin now has a hard ceiling, a new FinalizationTimeout (240 s) answers 451 4.3.1 so the sender retries instead of timing out, and acceptance is timed per stage with slow stages named in the log.
  • A database that did not answer no longer bounces valid mail. A recipient lookup that failed was indistinguishable from one that found nothing, so a database briefly locked by a backup made the server tell senders a valid mailbox did not exist — mail loss with a delivery receipt. A failed lookup now answers 451, which is what made it safe for DBConnectionAcquireTimeout to default to 60 s.
  • Every remaining shared-pool wait is bounded (chapter 21): an absolute outbound session ceiling defeats one-byte-at-a-time peers, and ClamAV, DNS queries, event scripts and external processes all have deadlines. A saturated work queue reports which task holds each thread, with session and peer IP.
  • Pre-authentication IMAP command buffering is capped; backup restore validates the archive before deleting what it is restoring over; a first /analyze static-analysis pass fixed a buffer overrun on long paths and a log-rotation defect.
  • Roadmap.md rewritten — what the fork is ahead on, the gaps that cost users, and reasoned refusals — including the correction that ARC sealing did not cover relayed third-party mail, because a seal was added only to mail already DKIM-signed for a domain hosted here. That has since been fixed: sealing no longer depends on author-domain signing, and it stays off until ArcSealingEnabled is set (chapter 26).

6.2.15 – 6.2.16 — IMAP sequence sets, files with no owner

  • IMAP sequence sets now behave as RFC 3501 defines. * was parsed as the number zero anywhere except the end of a range, and descending ranges (3:1) matched nothing — so FETCH * returned nothing, while UID STORE *:* +FLAGS (\Deleted) flagged every message in the mailbox and UID EXPUNGE * expunged them all. All four sequence-set parsers fixed; twelve regression tests pin the behaviour.
  • A failed restore could destroy the data directory. Restoring messages deleted the live data directory first and only then looked for the backup’s message store. It now confirms the replacement exists before deleting anything, and if a later step fails the extracted copy is kept and its location logged.
  • Files with no owner, deliveries that could never succeed. Failed header rewrites, rejected external-account fetches and unsendable bounces each stranded orphan files; an unreadable message file hung its outbound connection for up to ten minutes; and a truncated BDAT chunk could be NUL-padded and delivered. All fixed — a message merely locked by a scanner stays queued rather than bouncing.
  • ManageSieve now limits authentication attempts — three failures, then disconnect and auto-ban registration, like every other protocol.
  • Control Panel pages for settings that previously existed only in the INI — authentication, administrative access (with two-factor reachable after logon), DNS resolver, web services, archiving, and the consistency scan’s findings. 6.2.16 fixed the settings palette raising an error dialog on close. For the recurring “what did this fork change?” question: of 980 server source files shared with official master, 936 are byte-identical; 44 differ and 30 are new.

6.2.14 — 21 defect fixes, and settings you can find

  • An adversarial audit found 21 defects, several of which could lose or corrupt mail: a failed message-file copy crashed delivery and wedged the message in the queue; IMAP APPEND answered OK for messages never written (Sent Items lost while the client showed them saved); every string SQL parameter on MS SQL and SQL CE was bound from freed memory; a failed transaction start leaked its pooled connection until the pool starved.
  • Security. DKIM test mode (t=y) no longer turns a failed signature into a pass that satisfies DMARC; IMAP SASL credentials are masked in the protocol log; every key record at a DKIM selector is tried, so a sender rotating keys no longer loses half its mail; MTA-STS enforcement and MX failover no longer degrade for large recipient sets.
  • Settings you can find. The Ctrl+K palette searches all 227 settings by label and INI key (chapter 5); settings moved to the page where they are used; “Advanced hardening” became “Advanced INI settings” under Maintenance.
  • Multiple smart hosts documented at last (§14.1). Community fixes: backup start was blocked by a call to a method that does not exist (GitHub issue 19), and log retention existed but was unfindable (GitHub issue 16).

6.2.13 — community bug fixes and protocol hardening

  • The relayed-mail DATA stall, first pass: the reverse-DNS lookup for the Received header moved off the network I/O thread, where an internal relay with no reverse zone could stall the whole session.
  • SMTP robustness: data pipelined with DATA is consumed correctly; a rejected BDAT drains its in-flight payload instead of desyncing the session (Exchange uses BDAT whenever CHUNKING is advertised); a parse exception can no longer wedge a connection permanently.
  • IMAP: a malformed partial-fetch range (BODY[]<0.-1>) could crash the server or send heap memory from before the buffer to an authenticated client — clamped and normalised. OnClientLogon now fires from every IMAP AUTHENTICATE mechanism.
  • SpamAssassin: a malformed spamd response could spin a core, write the raw response header into the message, or replace it with a zero-byte file. The client now parses defensively and the original message is preserved on any failure.
  • DBUpdater labels schema versions 6002–6005; a failed database create/upgrade propagates a real exit code instead of reporting success against a broken schema; a customised EventHandlers.vbs survives uninstall/reinstall.

6.2.12 — .NET 8 tools, Import Tool, installer hardening

  • Every C# component now targets .NET 8. DBSetup, DBSetupQuick, DBUpdater and the Data Directory Synchronizer move off .NET Framework 4.8.1; the silent command-line behaviour the installer drives at post-install is unchanged. No server-core changes; no database change (the schema stays 6005).
  • The VB6 migration wizards are replaced by the Import Tool (§33.8): accounts from a comma-separated text file, messages from mbox files — with every defect documented in the old tools fixed.
  • Installer hardening. The .NET 8 Desktop Runtime is now a prerequisite of the server component — installed, with its exit code checked, before the database tools run — not just of the Control Panel. The obsolete .NET Framework 4.5 check is gone, and the supported-OS floor is Windows 10 1607, the runtime’s own minimum.
  • CI now builds every C# project with warnings as errors, and the test suite runs on NUnit 4.

6.2.11 — Control Panel accessibility

  • Two Control Panel lists announced their class name to screen readers — every row of the alias list on the Domains page, and every line of Live logs. A ListViewItem‘s accessible name falls back to ToString() on the bound object; both now expose the text a sighted user sees.
  • The regression suite now runs in full. 27 of its 1026 tests had always reported inconclusive because SpamAssassin and ClamAV were not installed on the release machine. With both installed, two tests failed — defects in the tests, not the server — and all 1026 now execute and pass on every release.

6.2.10 — security and housekeeping

  • Fifteen COM methods reported success on calls they had refused. They returned false from HRESULT functions, and false is S_OK. Five InterfaceCache getters returned before writing *pVal, handing an unauthorized caller success plus uninitialized memory; SetAdministratorPassword and five siblings reported a password change that never happened. Found by running CodeQL’s C++ suite by hand — the workflow had only ever analysed C#, leaving the entire protocol surface unscanned.
  • The PHP WebAdmin is removed. It stored the administrator password in plaintext in a PHP session and required DCOM to be opened up for the web server account.
  • The retired Administrator is removed from the repository. Interop.hMailServer.dll now comes from Shared, so the packaged output is unchanged.
  • Code-quality findings on shipped, hand-written code taken to zero.

6.2.9

  • LiveCharts upgraded; dashboard charts no longer render as a white block on the dark theme.
  • First automated tests for the Control Panel.

6.2.8

  • Fixed list editors rendering every row blank.
  • Fixed the Control Panel becoming unusable after an hMailServer service restart.

6.2.7

  • Control Panel usability: DKIM key generation with the DNS record shown and copyable, password reveal toggles and a strong-password generator, date pickers, Enter/Esc in dialogs, window state remembered.

6.2.6

  • IMAP4rev2 (RFC 9051) as an opt-in session mode.
  • Control Panel visual redesign; complete settings coverage; real QR code for 2FA.

6.2.5

  • Fixed two defects that broke default fresh installs: DPAPI database-password truncation, and the create-table scripts stamping the wrong schema version.

6.2.0 – 6.2.4

  • The Control Panel replaces the classic Administrator.
  • SCRAM-SHA-256 and SCRAM-SHA-256-PLUS, Argon2id, OAuth2 bearer authentication.
  • Sieve (RFC 5228) and ManageSieve (RFC 5804).
  • SMTPUTF8/EAI, PIPELINING, ENHANCEDSTATUSCODES, DSN, SRS, CHUNKING/BDAT, BATV.
  • Health probes (/livez, /readyz, /healthz) and more Prometheus metrics; slow-query log.
  • Active Directory account pickers.

6.1.0

  • The Web Control Deck, served by the REST API listener (RestApiPort): a live dashboard, domain and account management, queue retry and delete, and the DANE TLSA records to publish.
  • The classic Administrator gains light and dark themes, a Ctrl+K command palette and a modernised dashboard.
  • The installer’s wizard is rebranded and ships the Web Control Deck’s page (the WebAdmin folder).
  • The server core is 6.0.0 build 4’s, apart from the endpoint that serves the page; schema 6001.

6.0.0

  • Visual Studio 2026 toolchain, OpenSSL 4.0, Boost 1.91, PostgreSQL 18, 64-bit only.
  • PBKDF2-HMAC-SHA256 password hashing; TLS 1.2/1.3 defaults.
  • MTA-STS, DANE with in-process DNSSEC validation, TLS-RPT.
  • DMARC evaluation, ARC sealing, Ed25519 DKIM.
  • ACME v2 (Let’s Encrypt) built in; REST API; Prometheus metrics; JSON logging; web services server.
  • IMAP MOVE, ID and SPECIAL-USE; SMTP 8BITMIME.

26. How mail flows through the server

When a message goes missing, scores strangely, or turns up somewhere unexpected, the explanation is almost always obvious once you know the order in which hMailServer does things. This chapter follows a message through the server — from the first TCP packet to the mailbox file or the outbound queue — showing where each decision is made, which setting drives it, and where you can intervene.

26.1 The journey at a glance

 Sending server                     hMailServer
 ──────────────                     ───────────
       │  connect                        │
       │───────────────────────────────▶ │  IP ranges, auto-ban, connection limits
       │  EHLO, MAIL FROM, RCPT TO       │
       │───────────────────────────────▶ │  recipient resolution            (§26.4)
       │                                 │  envelope spam tests, greylisting (§26.5)
       │  DATA  (message content)        │
       │───────────────────────────────▶ │  content spam tests              (§26.5)
       │  250 accepted                   │
       │ ◀───────────────────────────────│  ◀── the message is now hMailServer's problem
       │                                 │
       │                     anti-virus (chapter 11),
       │                     rules and Sieve (chapter 12)
       │                                 │
       │                  ┌──────────────┴───────────────┐
       │                  ▼                              ▼
       │          local recipient               remote recipient
       │          written from the queue into   delivery queue ─▶ retries (§26.7)
       │          the account's mailbox                        ─▶ bounce  (§26.8)

The most important line in that diagram is the 250 after DATA. Before it, hMailServer can simply refuse — the sending server still holds the message and the duty of telling its user. After it, the message is hMailServer’s responsibility, and anything that goes wrong from then on must be handled by retrying or reported with a bounce. hMailServer therefore rejects as early in the conversation as it can: it is cheaper, and it never generates backscatter.

26.2 Two senders, two recipient lists

Every message carries two independent sets of addressing, and confusing them is behind a surprising number of “impossible” support cases.

Row The envelope (SMTP) The headers (MIME)
Sender MAIL FROM — where bounces go From: — what the reader sees
Recipients RCPT TO — where the message is actually delivered To: / Cc: — what the reader sees

The two do not have to agree, and legitimately often do not:

  • A BCC recipient appears in the envelope but nowhere in the headers — the server knows where to deliver, the other readers never see it.
  • A forwarded message keeps its original To: header while the envelope recipients change — the content is unchanged, the destination is not.
  • A forged From: header costs a spammer nothing. Seeing your own address in From: proves only that the sender typed it there.
  • Spam apparently “addressed to someone else” was delivered to you because you were the envelope recipient; the headers were dressing.

hMailServer delivers on the envelope, always. Rules, Sieve scripts and event scripts that test To: or From: are testing the headers — reliable for filing legitimate mail, worthless as proof of origin or destination. Keep this distinction in mind throughout the rest of this chapter: “recipient” below means the envelope recipient.

26.3 The SMTP conversation

An inbound delivery is a fixed sequence of commands, and hMailServer attaches specific work to each one:

Stage What hMailServer does
Connection Looks the client IP up in IP ranges and the auto-ban list; enforces the connection-count limit; sends the welcome banner
EHLO / HELO Records the client’s stated host name (checked later by the HELO spam test); on EHLO, advertises extensions — PIPELINING, SMTPUTF8, ENHANCEDSTATUSCODES, DSN, CHUNKING, 8BITMIME, STARTTLS. New in 6.x — every extension in that list except STARTTLS is a fork addition
STARTTLS Upgrades the connection to TLS — 1.2/1.3 by default in 6.x. Since 6.3.4 a new installation (not an upgrade) offers it, optional, on ports 25 and 587 from the start, with a self-signed certificate made for the machine: the Windows installer and the Linux packages’ first start do this, and anywhere else (a server started by hand rather than by the packages’ unit) you run --apply-new-install-defaults by hand, with the service stopped
AUTH Authenticates a submitting user; failures count towards auto-ban. On a new 6.3.4 installation the Internet IP range requires TLS for authentication, so a client on another machine is not offered AUTH before STARTTLS and is refused with 530 if it sends it; the My computer range, and mail from other servers on port 25, are unaffected. An upgraded server keeps its ranges as they were
MAIL FROM Accepts the envelope sender. An empty sender (<>) is allowed by default — see §26.8 for why it must be. The submission rate limit (§17.5; new in 6.x), if set, refuses excess senders here with a 421. As shipped, the envelope spam tests (§26.5) also run here, before any recipient has been named (DNSBLChecksAfterMailFrom is 1; set to 0, it moves them to the first RCPT TO)
RCPT TO The busiest moment of the session: the recipient is resolved (§26.4), the relay decision is made, and greylisting runs (§26.5) — preceded by the envelope spam tests only if they have been moved here from MAIL FROM. Each recipient is accepted or refused individually
DATA Receives the content; enforces the maximum message size (default 20 MB, which a domain and, since 6.3.4, an account may lower but not raise; a size the client declares with SIZE is already checked at MAIL FROM) and, optionally, strict line endings; then runs the content spam tests before the final reply. Anti-virus runs after acceptance (§26.6)
QUIT Ends the session

Since 6.3.4 the session has more places to say no, each off until an administrator sets it: a country policy when the connection is accepted (with a country database you supply), the domain white and black list at MAIL FROM, restricted delivery and a null sender naming a second recipient at RCPT TO, and a mail flow rule whose action is to reject, at the end of DATA.

Two protocol-hygiene settings are worth knowing about. hMailServer can reject messages with malformed line endings (the standard requires CRLF; some spamware cannot manage it, but neither can some buggy legitimate software), and it can disconnect a client after too many invalid commands — the classic signature of a spammer probing your server for deliverable addresses. Both live with the other SMTP behaviour on the SMTP tab of Connections & protocols → Protocols.

How commands are processed internally

hMailServer does not dedicate a thread to each connection. Instead, the service starts a fixed pool of worker threads, and each command that arrives — on any session — is picked up by whichever worker happens to be free, handled, and released. A mail client spends almost all of its time idle, so one worker can serve a great many sessions, and thousands of simultaneous connections do not translate into thousands of threads — or the context-switching cost that would come with them.

The trade-off is that no more commands can run at once than there are workers; anything beyond that waits briefly for a worker to come free. In normal operation the wait is invisible, because individual commands finish in milliseconds. The pool size is tuned on the Threads tab of Maintenance → Performance.

This is why a slow event script (chapter 23) is so damaging: it occupies a worker for its whole running time, and the pool is small. One sluggish script slows every session on the server.

26.4 Resolving recipients

When RCPT TO arrives, hMailServer must answer one question: is this address mine, and if so, whose mailbox is it? Since 6.3.4 one question comes first: an address, or a whole domain, routed to a webhook is accepted at RCPT TO whatever follows, because the route decides that the address exists (nothing is routed until an administrator makes a route). Otherwise the resolution runs as follows.

1. Domain-alias rewrite. If the recipient’s domain matches a domain alias of a local domain (chapter 13), the address is rewritten to the primary domain name first: with example.net set up as an alias of example.com, mail for [email protected] becomes mail for [email protected] before anything else happens.

2. The domain is local. If a domain matching the recipient exists on the server:

  • If the domain is marked inactive, the recipient is refused.
  • An account matching the full address wins: the message will be delivered to that mailbox. An inactive account is refused.
  • Failing that, an alias matching the address is looked up. If one is found, the whole resolution restarts from step 1 with the alias’s target address — which may be another local account, another alias, or an address on a completely different server. An inactive alias is refused.
  • Failing that, a distribution list matching the address is looked up. If one is found (and the sender is entitled to post to it — chapter 13), the list’s members are added as recipients, and each member address is resolved through this same process. Members may themselves be aliases, other lists, or remote addresses.
  • Since 6.3.4, failing that, a masked address (chapter 13) matching the address is looked up. One that is switched on delivers to the account that made it; one switched off, or removed, is refused as an unknown address is, and never handed to the catch-all.
  • If nothing matched and the domain has a catch-all address, the message goes there.
  • If nothing matched and there is no catch-all, the recipient is refused as unknown — after the OnRecipientUnknown event script (chapter 23), if you have one, has been told (it is a notification only: nothing it does changes the answer).

3. The domain is not local. If no local domain matches, hMailServer looks for a route (chapter 14) matching the recipient’s domain and, failing that, falls back to ordinary DNS-MX delivery. Route lookup uses the address as the client gave it — the domain-alias rewrite in step 1 only applies to local domains. Either way, accepting a non-local recipient means agreeing to relay, so the session must be entitled to it: an authenticated user, or an IP range that explicitly permits external-to-external delivery. Everything else gets the “relay denied” refusal — which is exactly the behaviour that keeps you off the blacklists (chapter 17).

Worked example

[email protected] arrives; example.net is a domain alias of example.com; [email protected] is an alias pointing at [email protected], which is a real account:

[email protected]
   └─ domain alias      ─▶ [email protected]
        └─ alias        ─▶ [email protected]
             └─ account ─▶ delivered to bob's mailbox

Three lookups, one 250, one copy of the message.

Catch-all

The catch-all address receives everything sent to non-existent addresses in the domain — a safety net for misspelt local parts. It is the domain’s Postmaster address (General tab of the domain dialog) and can be any address at all, including one on another server; leave it empty and unknown recipients are refused instead.

A catch-all means your domain accepts mail for every possible local part. Spammers notice, and dictionary-attack traffic that would have been refused at RCPT TO now has to be received, scanned and stored. Use one deliberately or not at all.

Plus addressing

With plus addressing enabled on a domain (Accounts & domains → Domains → Properties → Limits, alongside a choice of separator character, + by default), the tag between the separator and the @ is ignored during resolution: [email protected] lands in [email protected]‘s mailbox. Users can hand out a distinct tagged address to every service they sign up to, then file — or identify leaks — with a rule or Sieve script that tests the recipient address (chapter 12).

26.5 The anti-spam pipeline

Chapter 10 covers configuring the spam layers; this section covers when each one runs. The principle is the one from §26.1: judge the message as early as the available evidence allows, so bad mail can be refused before it costs bandwidth — and so the refusal reaches a legitimate sender honestly, via their own server, instead of as backscatter.

The first checkpoint is before the body — at MAIL FROM as shipped, or at the first RCPT TO if DNSBLChecksAfterMailFrom is set to 0, which leaks less to a client probing for addresses but accepts more of the conversation first — and the second is after DATA (or the last BDAT); mail handed over by a configured incoming relay has both phases run after DATA. For every other session the checks fall as follows:

Check Runs Judges
Blocked senders before the body The envelope sender, against the blocked-senders list; runs first, and never matches a bounce’s empty sender
DNS blacklists before the body The connecting IP address
SPF before the body Envelope sender’s domain vs the connecting IP. Only a hard fail (-all) scores, unless SoftFail, TempError and PermError are given scores of their own (since 6.3.4, off by default). Since 6.3.4 the check follows RFC 7208, and a client on 127.0.0.0/8 or ::1 no longer passes before any policy is read
HELO host check before the body Whether the HELO/EHLO host name resolves to the connecting IP
Sender MX check before the body Whether the envelope sender’s domain has MX records at all
Reverse-DNS (PTR) check before the body Whether the connecting IP has a PTR (reverse-DNS) record whose forward lookup comes back to it
Greylisting at RCPT TO Whether this (IP, sender, recipient) triplet has been seen before
SURBL after DATA The links inside the message body
DKIM verification after DATA Whether a DKIM-Signature header, if present, matches the content
ARC after DATA Whether a chain sealed by a sealer you trust recorded an original pass; it can only cancel a DMARC failure score, and does nothing until trusted sealers are named (off by default)
DMARC after DATA New in 6.x. The From: domain’s published policy, using the SPF and DKIM results
SpamAssassin after DATA The whole message, against hundreds of content rules
Filter hook after DATA The whole message, sent over HTTP to an external engine such as rspamd; runs last, and is off until FilterHookUrl is set

Mail fetched from external accounts by POP3 or IMAP (§6.2) goes through the same tests before the message is saved — with one exception: a fetch account set to mirror every folder copies messages in verbatim and delivers nothing, so no header, rule, anti-spam or anti-virus pass touches a mirrored copy.

How the score accumulates

Every test that fires adds its configured score to a running total for the message, and the total is compared against the two thresholds from §10.1. What “delete” means in practice depends on when the threshold is crossed:

  • Delete threshold reached during the envelope stage — the command is refused immediately with 550 5.7.1 and the first failing test’s text (at MAIL FROM as shipped, before any recipient is named), and DATA never happens.
  • Delete threshold reached only after the content tests — the message is refused in the final reply to DATA, with 554 5.7.1 and the first failing test’s text. Either way the sending server is told plainly, during the session, that the message was not accepted — nothing is stored, and there is nothing to bounce. The exception is the quarantine (QuarantineEnabled, off by default): with it on, a message condemned after DATA is answered 250 and held on Spam & virus filtering → Quarantine for review, so a false positive can be released without any bounce; a verdict reached before the body has no message to hold and stays a refusal.
  • Mark threshold reached (but not delete) — the message is delivered tagged, exactly as §10.1 describes: X-hMailServer-Spam: YES and the X-hMailServer-Reason-* headers are added, and the subject is prefixed with [SPAM] if you have switched that on (it is off by default), so rules and Sieve can file it.

For externally fetched mail there is no session and nobody to refuse, so a message over the delete threshold is simply deleted.

A message is only scored at all if at least one spam test is enabled, the session is not authenticated (your own users’ mail is never spam-tested), the sender or connecting IP is not on the white list, and the connecting IP falls in an IP range with anti-spam enabled. The content tests are also skipped for a message larger than the maximum message size to scan (1024 KB by default) — spam is overwhelmingly small, and content-scanning very large messages buys little for its CPU cost.

Greylisting

Greylisting is the odd one out: it does not score, it stalls. The first time a given (IP, sender, recipient) triplet appears, hMailServer records it and answers with a temporary error: 451 at RCPT TO or, when the envelope sender is empty, 450 at the end of DATA, because an empty sender at RCPT TO is often another server checking that the address exists. A real mail server retries and is accepted once the initial delay has passed (30 minutes by default); most spamware never comes back. Because the rejection is temporary and happens before acceptance, greylisted messages are never scored, delivered or counted — they simply have not arrived yet.

You control how long the deferral lasts, how long unused triplets are kept, and how long a successful triplet remains valid (so established correspondents are never delayed again). Two bypasses take most of the sting out of the delay: skip greylisting when SPF passes (on by default, but it acts only when the SPF test itself is switched on, which it is not as shipped; the big providers all publish SPF, so with the test on their mail flows straight through), and skip it when the connecting IP appears in the A or MX records of the sender’s domain (off by default; it costs an A and an MX lookup of the sender’s domain per message). The greylisting white list (§10.4) handles the genuinely incompatible senders — services that retry from a different IP each time and so never complete a triplet. Since 6.3.4 there is also a greylisting bypass list (Spam & virus filtering → Greylisting bypass list), which lets a named domain or HELO suffix past only when every result the entry requires (SPF, DKIM or DMARC, at least one) passes for that name (an entry that requires DKIM or DMARC is judged after DATA, once the body has arrived), and two switches for senders that retry too soon, a sliding wait and an abuse lock, both off by default.

Greylisting must be switched on in two places: globally on the Greylisting tab of Spam & virus filtering → Anti-spam settings, and per domain with Enable greylisting for this domain on Accounts & domains → Domains → Properties → Limits — which is what lets you greylist a back-office domain while sparing a customer-facing one.

26.6 Between acceptance and the mailbox

Once the final 250 is sent, the accepted message passes through the remaining machinery on its way to a mailbox or the outbound queue:

  • Anti-virus (chapter 11) — an infected message is deleted or has its attachments stripped; before the scan, while attachment blocking is switched on (off as shipped), an attachment whose name matches the blocked attachments list is renamed with .txt and its content replaced by a notice (§11.2), so the message still arrives. Since 6.3.4 a message from outside whose attachment is of a risky type (an executable, a script, an archive, an office document, a PDF and the like) can also be held until a sandbox service you name has answered (off by default).
  • Server-wide rules, then (since 6.3.4) the rules of a domain the message is for or from, then the recipient account’s own rules and Sieve script (chapter 12; Sieve support is new in 6.x) — filing, forwarding, replying, deleting, or handing the message to a script.
  • Account features — auto-reply, forwarding, and quota enforcement. Since 6.3.4 the recipient’s allow and block lists are read here too, the account’s own before its domain’s and the server’s: an allowed sender’s mail is delivered unmarked, and a blocked sender’s is held in the quarantine (marked and delivered when the quarantine is off).

Two guards operate here. The rule loop count (RuleLoopLimit, 5 by default, on the Rules tab of Mail flow & delivery → Delivery of e-mail) caps how long a chain of messages made by rules can grow: a forward, a reply or a copy carries X-hMailServer-LoopCount one higher than the message that caused it, and at the limit nothing more is generated, so a pair of rules forwarding to each other cannot ping-pong forever. And when forwarding sends a message back out to the world, SRS (§17.6; new in 6.x), if enabled, rewrites the envelope sender so the forwarded copy still passes SPF at its destination. ARC sealing (new in 6.x; ArcSealingEnabled, off by default, on Certificates & encryption → Transport security) seals mail this server relays onward, a forwarded copy included, as well as mail from a hosted domain. The seal is made with a hosted domain’s DKIM key, so on a server with no domain DKIM key the switch reads as enabled and seals nothing; a message that cannot be sealed is still delivered.

One quirk to remember when writing rules: the Delivered-To header, if you have enabled it, is added after all rule and script processing — a rule can never match on it.

This is also where the event scripts of chapter 23 fire: OnAcceptMessage just before the final reply to DATA (its Result can still refuse the message), then OnDeliveryStart as delivery processing begins — before anti-virus and the server-wide rules — and OnDeliverMessage after them, just before the message is handed to local and remote delivery. OnDeliveryFailed reports each failed remote delivery attempt described next.

26.7 The delivery queue and the retry schedule

What happens next depends on where each recipient lives:

  • Local recipients get the message written into their mailbox folder in the data directory by the delivery thread that picks it up. Every accepted message is written to the queue before the 250 is sent and taken from there moments later, so a message for local recipients only leaves the queue again almost at once — this is why the test in §6.3 leaves the queue empty.
  • Remote recipients keep the message in the delivery queue, visible at Monitoring & troubleshooting → Delivery queue, where you can retry or delete individual messages by hand (and do the same over the REST API — chapter 24; the API itself is new in 6.x).

For each remote domain, hMailServer picks the destination in a fixed order: a rule’s Send using route action, then a matching route, wins; otherwise a relay host set on the sending domain (the Relay tab of its domain dialog); otherwise the smart host, if one is configured, carries the rest; otherwise a DNS-MX lookup names the destination, and the MX hosts are tried in priority order — up to a configurable maximum number of hosts, 15 by default. Large recipient lists are split into batches (the maximum recipients per delivery is configurable), because many receivers treat a single message with hundreds of recipients as spam in itself.

Outbound connections use STARTTLS whenever the remote server offers it. New in 6.x. With MTA-STS and DANE enforcement on (their defaults — chapter 9), a domain that has published a policy must present verified TLS or the mail is held back rather than sent unprotected. Since 6.3.4 a remote-domain policy (Mail flow & delivery → Remote domains) can also demand encrypted, verified or DANE-verified TLS of one destination or pattern; a delivery that cannot meet it is deferred, never sent in the clear. The outbound rate limit (§17.5) also acts here, deferring — not bouncing — anything over the per-destination cap.

Since 6.3.4, and on after an upgrade too, a destination that asks this server to slow down — a 421, or a 4xx with a provider’s rate-limit code or wording — is given a back-off: nothing more is sent to it for OutboundBackoffMinutes (2 by default), doubling on each repeat up to OutboundBackoffMaxMinutes (60); OutboundBackoffMinutes set to 0 turns it off. A wait this server chose — the back-off, and the connection ceilings per destination and per tenant or an IP warm-up schedule where you set them — does not spend a message’s retries until the message has been queued for longer than its retry schedule spans, so a destination that never stops asking still ends in a bounce.

The reply to each attempt decides what happens next:

Outcome Meaning hMailServer does
2xx Accepted Done. Counted in hmailserver_messages_delivered_total (§16.3; the Prometheus metrics are new in 6.x)
4xx, or no connection Temporary failure Back in the queue for the next retry
5xx Permanent refusal No retry — a bounce is generated now

The retry schedule is set on the Delivery tab of Mail flow & delivery → Delivery of e-mail: the number of retries, and the minutes between them (a route may override both for its own domains). The defaults are 4 retries at 60-minute intervals — five attempts in total, spread across roughly four hours — after which the message is abandoned and bounced. A rebooting server, a full mailbox that gets cleared, a DNS blip: all of these heal within that window, which is why a growing queue is a warning sign (§16.1) but a briefly non-empty one is routine.

26.8 Bounces

A bounce — formally a delivery status notification — is generated when delivery fails for good: a remote server refused the message permanently, the retry schedule ran out, or a local delivery could not be completed (a message that would take a mailbox past its quota — a mailbox already at or over its quota is, by default, refused at RCPT TO with a temporary 452 instead — or a Sieve reject). It is sent to the envelope sender, and for a remote failure it quotes the remote server’s last response, which is usually all the troubleshooting you need. The wording of the notification is yours to edit under Mail flow & delivery → Server messages.

Bounces are sent with an empty envelope sender (MAIL FROM:<>), as the standard requires — a bounce that could itself bounce would loop forever. This is also why the allow empty sender address option (on the SMTP tab of Connections & protocols → Protocols) should stay on: disable it and you refuse every delivery report the internet tries to send you. To refuse only the null senders that cannot be delivery reports, 6.3.4 adds RejectMultiRcptNullSender (off by default): a MAIL FROM:<> that names a second recipient is refused, because a real report has one.

Bounce as little as possible. Because spam and viruses forge their senders, any bounce you generate after accepting such a message lands on an innocent third party — backscatter, and a fast route onto blacklists. This is the deeper reason the whole pipeline rejects during the SMTP session wherever it can: an in-session refusal makes the sending server responsible for informing its user, which for forged mail means the bounce goes nowhere. BATV (§17.6; new in 6.x) closes the loop from the other side, tagging your outgoing envelope senders so that forged bounces arriving at your server can be recognised and dropped.

26.9 What hMailServer caches

Almost every step above needs objects from the database — the domain at the alias rewrite, the account at resolution, the list at expansion. Reading them fresh for every recipient of every message would make the database the bottleneck, so hMailServer keeps an in-memory object cache for four object types: domains, accounts, aliases and distribution lists. Only the object itself is cached — caching a domain does not cache its accounts.

Each cached object carries a time-to-live (default 60 seconds). A lookup checks the cache first; an entry younger than its TTL is used as-is, an older one is discarded and re-read from the database. The hit rate per object type tells you how well this is working: 90% means nine lookups in ten never touched the database. Raising a TTL raises the hit rate.

Invalidation is the part that matters operationally:

  • A change made through the Control Panel, the COM API or the REST API updates the cache immediately. Administer the server only through these, and the cache is always coherent — you can set the TTLs as high as an hour and collect the benefit for free. (Older documentation lists the classic hMailServer Administrator and the PHP WebAdmin here too; both have been removed from the fork — the Control Panel replaces them.)
  • A change made directly in the database is invisible until the TTL expires. A password changed by a third-party tool, or an account deactivated by hand-written SQL, simply does not exist as far as the running server is concerned — for up to a full TTL.

If a change you made “didn’t take”, ask yourself whether you made it behind the server’s back. Direct database edits and a long TTL are a reliable recipe for ghost-chasing.

The cache is configured on the Cache tab of Maintenance → Performance — enabled or disabled as a whole, with a TTL per object type — and is scriptable through the COM Cache object:

$app = New-Object -ComObject 'hMailServer.Application'
$app.Authenticate('Administrator', 'your-admin-password') | Out-Null

$cache = $app.Settings.Cache
'Domains:            {0}% hit rate' -f $cache.DomainHitRate
'Accounts:           {0}% hit rate' -f $cache.AccountHitRate
'Aliases:            {0}% hit rate' -f $cache.AliasHitRate
'Distribution lists: {0}% hit rate' -f $cache.DistributionListHitRate

# The safe pattern for direct database work:
$cache.Enabled = $false      # ...make the database changes...
$cache.Enabled = $true       # coherent again, nothing stale survives

$cache.Clear()               # or: flush everything and reset the statistics

Run 6.2.10 or later for this kind of scripting: earlier builds had COM methods that reported success on calls they had actually refused, and Cache getters that could return uninitialized values. On current releases, still check return values.

If some external tool must write to the database routinely, either shorten the TTLs to match how quickly those changes need to be seen, disable the cache around each batch of changes as above, or — bluntest of all — stop the service while the surgery happens. For everyone else, the right answer is the default one: leave the cache on, administer through the proper interfaces, and let it quietly absorb the database load of every message that flows through the server.


27. Rules in practice: a cookbook

Chapter 12 introduced the rules engine in a paragraph. This chapter is the working manual: how matching actually behaves, every criterion and action, the order things run in, eleven recipes you can copy, and the reasons rules mysteriously fail to fire.

Everything here concerns the built-in rules engine — the Rules page and each account’s Rules tab. Sieve — the per-user standard filtering language, new in 6.2 — is a separate system covered in chapter 12.

27.1 Where rules live, and when they run

Rules exist at three levels; the domain level is new in 6.3.4:

Level Where Applies to
Global Mail flow & delivery → Rules (on screen, Global rules) Every message the server delivers, to any recipient
Domain (since 6.3.4) The same page, with the domain chosen at the top Messages with a recipient in the domain or a sender it hosts, after the server’s rules. A recipient domain’s rules act on the whole message only when every recipient is its own; otherwise only on its own recipients
Account Accounts & domains → Domains → your domain → Accounts → the account → Rules tab Messages delivered to that one account

Rules run during delivery — after a message has been accepted, as it is being placed into mailboxes or sent onward. Since 6.3.4 there is one exception: when a server rule, or a rule of a domain whose rules act on the whole message, has a Reject with a text action, those rules are also asked at the end of DATA, before the message is accepted (§27.3). Three consequences follow, and they explain most rule surprises:

  • A rule acts on the recipient’s copy. If a global rule moves messages into a Spam folder and User 1 mails User 2, the message lands in User 2’s Spam folder. Nothing happens in User 1’s mailbox — the message was never being delivered to User 1.
  • Global rules run first. If a global rule deletes a message, the recipient’s own account rules never see it.
  • Account rules get the last word on anything both levels touch. A global rule that files a message into Filed, followed by an account rule that files it into Clients, leaves the message in Clients.

Within each level, rules run top to bottom in the order listed, and each rule’s actions run in their listed order. Reorder with Move up and Move down. Order is not cosmetic — it is half of rule debugging (§27.5).

AND and OR

A rule combines its criteria in exactly one of two ways, chosen in the box beside the word IF:

  • Match ALL criteria (AND) — every criterion must match.
  • Match ANY criterion (OR) — any single criterion is enough.

There is no mixing within one rule. If you need “(A and B) or C”, write two rules.

27.2 Criteria

A criterion is a field, a search type and a value.

Field What is examined
From The From header
To The To header — not who the message is actually delivered to
CC The CC header
Recipient list The actual recipients, taken from the SMTP envelope
Subject The Subject header
Body The message body — both the plain-text and the HTML part
Message size The size of the message (numeric — see the unit note below)
Delivery attempts 1 on the first delivery attempt, 2 on the second, and so on
Custom header Any header you name — List-Id, Precedence, X-Mailer, anything
Attachment name (since 6.3.4) Every attachment’s file name, with ; between them, so Contains .exe or Wildcard *.zip reads “any attachment is”
Sensitive content (since 6.3.4) For the outbound content policy: what its detectors found in the readable text — a card number by its Luhn check digit, an IBAN, or an account number by a pattern of your own
Attachment type (since 6.3.4) Every attachment’s type, read from the file’s first bytes rather than its name, so a renamed executable is still an executable
External recipients (since 6.3.4) Every recipient this server does not host
Readable text (since 6.3.4) The plain and HTML parts and every attachment whose bytes are text, for a pattern of the rule’s own. A message sealed end to end has no readable text, so this and Sensitive content do not match it

Headers are not the envelope. Every message carries two recipient lists: the To/CC headers that a mail client displays, and the SMTP envelope — the addresses the message is actually delivered to. They routinely differ. A BCC address exists only in the envelope and appears in no header at all. Filter on To and BCC’d mail will never match; filter on Recipient list and it will.

Recipient list only works in global rules. When a message is placed into an account, the envelope recipient list is cleared. A Recipient list criterion in an account rule therefore matches nothing, ever. Use it in global rules only.

Missing equals empty. An absent header and an empty header are treated identically. A criterion of “Subject Equals (empty value)” matches messages with a blank subject and messages that have no Subject header at all — which may be more than you intended.

Message size units. The upstream project’s documentation was inconsistent about the unit for this criterion — bytes in one example, megabytes in another. The source settles it: the criterion compares the raw message size in bytes (the size of the message file). Do not confuse it with the COM API’s Message.Size property, which reports kilobytes. If in doubt, send yourself a message of known size before you attach a destructive action to a size criterion.

Search types

Search type Matches when
Equals The field matches the value exactly
Not equals The field does not match the value exactly
Contains The value appears anywhere in the field
Not contains The value appears nowhere in the field
Less than / Greater than Numeric comparison — for Message size and Delivery attempts
Wildcard A pattern using * (any sequence of characters) and ? (any single character) matches the field
Regular expression The expression matches the field — see below

Every search type except Regular expression compares case-insensitively — Equals, Contains and Wildcard all ignore case. Regular expressions are case-sensitive unless you say otherwise (below).

Regular expressions

Regular expressions are evaluated by Boost.Regex using Perl syntax. One fact matters more than all the others:

The expression must match the entire field value. There is no partial matching. invoice will not match the subject “Your invoice is attached” — write .*invoice.*. Forgetting the leading and trailing .* is the single most common regex-rule mistake.

Prefix (?i) for a case-insensitive match. For syntax details, the Boost.Regex documentation is the authority.

Test before you save

The retired classic Administrator’s criteria editor had a built-in Test field; the Control Panel’s criteria dialog does not. The same check is still one call away through the COM API (chapter 23): Utilities.CriteriaMatch(pattern, matchType, sampleValue) returns whether the sample would match — pass match type 5 for a regular expression, 8 for a wildcard. Since 6.3.4 the same check is also a REST call (chapter 24): POST /api/v1/rules/match with match_value, match_type (regex, wildcard or another of the words the rule listing uses) and test_value; nothing is read or written. Use it every time you write a wildcard or a regular expression — it is far faster than sending yourself test messages, and it catches the entire-value regex mistake on the spot.

27.3 Actions

Action What it does
Delete The message is not delivered. Deletion takes effect once rule processing has finished
Forward Send a copy to another address, local or external. The original is still delivered normally
Reply Send an automatic reply, with a from-address, subject and body you set, to the sender
Move to IMAP folder File the message into the named folder. Local deliveries only; the folder is created if it does not exist
Set header value Add a header to the message, or overwrite one that already exists
Run function (on screen, Run script function) Call a named function in your event scripts, passing the message object. Since 6.3.4 the name must be a plain identifier (below)
Send using route Deliver via a named route instead of the normal MX lookup (chapter 14). Server and domain rules only
Create copy Put an independent copy of the message into the delivery queue, addressed to the original recipients
Bind to address Deliver the message outbound from a specific local IP address. Server and domain rules only
Stop rule processing Skip every remaining rule in this rule’s set. The rest of this rule’s own actions still run, since a matching rule always runs all of its actions, so make Stop the last action
Add a disclaimer Since 6.3.4. Append a text to the plain and HTML parts, with an X-hMailServer-Disclaimer header naming the rule. The domain’s own disclaimer is separate and stays unconditional
Reject with a text Since 6.3.4. Refuse the message with 550 5.7.1 and your text at the end of DATA, before it is accepted, so the sender is told. A message the rule meets only at delivery is dropped as a delete is, and a sender this server hosts is told it was not sent
Require TLS Since 6.3.4. Every external delivery of the message needs STARTTLS; a destination that offers none is deferred with a reason naming the rule, never sent in the clear
Quarantine Since 6.3.4. Hold the message in the quarantine under Rule: <name>, for an administrator to release or delete
Warn the sender Since 6.3.4. The message goes on as the other actions decide, and a sender this server hosts is mailed a notice naming the rule; an outside sender is told nothing

Details worth knowing:

  • Move to IMAP folder understands subfolders: Archive.2026.Invoices files three levels deep, using . as the separator (if you have changed the IMAP hierarchy delimiter from the default dot, use your delimiter instead). The folder name may also contain the macros %YEAR%, %MONTH% and %DAY%, expanded at delivery time.
  • Forward updates the envelope, not the headers. The person receiving the forwarded copy sees the original To and CC in their client. A forwarded message is a new message — and new messages are run through the rules themselves, which is how forwarding loops start (§27.5). (Through the COM API, a Forward action also exposes an AbortSpamFlagged property — set it and messages already flagged as spam are not forwarded.)
  • Send using route and Bind to address are absent from the account rule dialog — both steer outbound delivery, which account-level rules never handle. Server rules have them, and since 6.3.4 so do a domain’s rules.
  • Create copy stamps the copy with an X-CopyRule header naming the rule that made it — which is also a ready-made loop guard: a Custom header criterion on X-CopyRule keeps a copy rule from processing its own copies.
  • Run function calls a function that takes the message as its one parameter — Sub MyFunction(oMessage) in VBScript — in the event-script file on Maintenance → Event scripts. Scripting must be switched on (Maintenance → Advanced, Scripting tab), and the syntax check is worth a click before you save. Since 6.3.4 the action names the function alone, as a plain identifier: ASCII letters, digits, _ and $, not starting with a digit, so MyFunction, not MyFunction(x). On Windows any other name is refused as HM5018 each time the rule matches, and nothing is run. Every release from 6.0.0 to 6.3.3 wrote the name into the script’s call unescaped, in VBScript and JScript alike (CVE-2026-100741), so whoever could edit a rule could run code in the service: on an older server, let nobody but the server administrator make or edit a rule with this action. On Linux, where event scripts are JavaScript and this action works since 6.3.4, the function is function MyFunction(oMessage) { … } in EventHandlers.js (chapter 28). As with all event scripts, this runs inside the mail flow: keep it short.
  • Reply deserves caution: never let it answer bulk mail or bounces. Recipe 6 shows the guards.

More: Mail flow rules, the outbound content policy and what a domain does to a message on the forum.

27.4 The cookbook

Eleven recipes, each stating its level. Addresses, folder names and thresholds are examples — substitute your own.

Recipe 1: file tagged spam into Junk

Chapter 10’s spam-mark threshold tags suspect mail (an X-hMailServer-Spam: YES header and, if you switch it on, a [SPAM] subject prefix) but still delivers it. This global rule files the tagged mail out of the inbox:

Part Field Search type Value
Criterion Subject Contains [SPAM]
Action Move to IMAP folder Junk

Match whatever prefix your anti-spam settings actually add. The prefix is off unless you, or a setting profile, switched it on (Spam & virus filtering → Anti-spam settings, General tab); while it is off this criterion matches nothing. If you prefer to leave subjects out of it, match the tagging header instead: a Custom header criterion on X-hMailServer-Spam, Equals YES, which the server writes while AddHeaderSpam is on, as it is by default.

Recipe 2: trusted senders skip the filters

A white-list rule, placed above the rules it bypasses. Global:

Part Field Search type Value
Criterion From Contains @yourbiggestclient.com
Action Stop rule processing

Everything below this rule is skipped for matching mail. If it sits below your filing and deletion rules instead of above them, it protects nothing — order is the whole mechanism here. Note this is a different job from the anti-spam white list (§10.4): that exempts senders from spam scoring; this rule exempts them from the rest of your rules.

Recipe 3: forward-and-keep an off-site copy

Copy everything addressed to sales@ to an external archive address, while delivering the original as normal. Global rule, Match ALL criteria (AND):

Part Field Search type Value
Criterion Recipient list Contains [email protected]
Criterion Recipient list Not contains [email protected]
Action Forward [email protected]

The second criterion is the loop guard. The forwarded copy is a new message and is run through the rules itself. With these exact criteria the copy escapes on its own — its envelope recipient is the archive address, not sales@ — but the moment the rule is re-edited to match on To or Subject, both of which the forwarded copy carries unchanged, it starts forwarding its own forwards. The guard costs nothing and keeps the rule safe to edit. The server’s rule loop limit will stop a runaway eventually, but the archive still receives duplicates in the meantime.

Recipient list criteria only work in global rules (§27.2). For a simple unconditional forward of one mailbox, the account’s Forwarding tab (§6.2) is the easier tool — rules earn their keep when conditions are involved.

Recipe 4: delete oversized messages

Part Field Search type Value
Criterion Message size Greater than your limit, in bytes (§27.2)
Action Delete

Global. Note that a rule delete is silent — the sender learns nothing. A protocol-level maximum message size (on the SMTP tab of Connections & protocols → Protocols) rejects the message at the door with an error the sender sees, which is usually kinder. Use the rule when you want size handling that depends on other conditions — over a limit and from outside the organisation, say. Since 6.3.4 such a rule can tell the sender too: with Reject with a text in place of Delete, the global rule refuses the message with 550 5.7.1 and your text before it is accepted.

Recipe 5: delete a problem sender, server-wide

Part Field Search type Value
Criterion From Contains [email protected]
Action Delete

Global. Two cautions. First, if the sender is simply a spammer, chapter 10’s machinery (DNSBLs, scoring) is the right tool — a rule is for the awkward case the filters legitimately pass. Second, resist the tempting variant “delete anything with an empty subject”: an empty value also matches messages with no Subject header (§27.2), and some legitimate automated mail qualifies.

Recipe 6: an out-of-office reply that behaves

The account’s Auto-reply tab (§6.2) covers the ordinary out-of-office. Use a rule when you need to be choosy about what earns a reply. Account rule, Match ALL criteria (AND):

Part Field Search type Value
Criterion To Contains [email protected]
Criterion Custom header Precedence Not contains bulk
Criterion From Not contains mailer-daemon
Action Reply Subject Out of office, body as you like

Why each guard is there:

  • To Contains means only mail addressed to Alice directly gets a reply. Mailing-list traffic carries the list’s address in To, not Alice’s, so it never matches — exactly what you want. (This is a case where the header-not-envelope behaviour of To works for you.)
  • The Precedence guard skips well-behaved bulk senders that mark their mail.
  • The mailer-daemon guard stops the rule answering bounce messages — replying to a bounce generates another bounce, and so on.

If two correspondents both auto-reply, the server’s rule loop limit caps the ping-pong; with these guards in place it rarely begins.

Recipe 7: date-stamped archive folders

On a dedicated archive account — the one fed by Recipe 3, say — file everything into a folder per month:

Part Field Search type Value
Criterion Message size Greater than 0
Action Move to IMAP folder Archive.%YEAR%.%MONTH%

“Message size Greater than 0” is the traditional match-everything criterion. The macros expand at delivery, so August 2026 mail lands in Archive/2026/08, and each new month’s folder is created automatically on first use.

Recipe 8: flag mail from outside the organisation

A useful phishing defence: mark every external message so clients can display it differently. Global:

Part Field Search type Value
Criterion From Not contains @yourcompany.com
Action Set header value X-External = yes

Client-side rules — or a Sieve script (chapter 12) — can then colour, flag or file on the X-External header. Set header value overwrites an existing header of the same name, so an outsider cannot smuggle in a pre-set X-External: no.

Since 6.3.4 a domain can mark outside mail without a rule: the external-sender tag on the Tagging and disclaimer tab of the domain’s properties (Accounts & domains → Domains) stamps X-hMailServer-External: YES, or puts [EXTERNAL] at the front of the subject, on each recipient’s copy. It judges “outside” by more than the From header — an authenticated session, a trusted incoming relay, and whether both the envelope sender and the From address are at domains this server hosts — and it is off on every domain until you switch it on.

Recipe 9: tag large messages in the subject

A rule can tag the subject without a script: Set header value on Subject with the value [LARGE] %MACRO_ORIGINAL_HEADER%, where the macro stands for the header’s current value. This recipe shows the general tool instead, for changes no built-in action can make: Run function hands the message to a script. Global or account:

Part Field Search type Value
Criterion Message size Greater than your threshold
Action Run function OnTagLarge

Then on Maintenance → Event scripts, add the function and run the syntax check, and make sure scripting is switched on (Maintenance → Advanced, Scripting tab). In VBScript, on Windows:

Sub OnTagLarge(oMessage)
   oMessage.Subject = "[LARGE] " & oMessage.Subject
   oMessage.Save
End Sub

On Linux, where event scripts are JavaScript since 6.3.4, the same function goes in EventHandlers.js, with the scripting language set to JavaScript (chapter 28): function OnTagLarge(oMessage) { oMessage.Subject = '[LARGE] ' + oMessage.Subject; oMessage.Save(); }

The same pattern — criterion, Run function, small script — handles any per-message transformation the built-in actions cannot express. It runs inside the mail flow, so keep the script short and test it on a non-production server first.

Recipe 10: route one sender’s mail through a different server

Suppose [email protected] sends bulk mail and you want it delivered through a separate route — a different smart host with its own IP reputation — so a blacklisting incident never touches your ordinary mail. Define the route under Mail flow & delivery → Routes (chapter 14), then, globally:

Part Field Search type Value
Criterion From Contains [email protected]
Action Send using route the bulk-mail route

Normally a route is chosen by recipient domain; Send using route overrides that per rule. The same shape works keyed on a Custom header — have your mailing software add X-Campaign: yes and match on that instead of the sender address.

Recipe 11: file machine-generated invoices by pattern

Account rule, using a regular expression:

Part Field Search type Value
Criterion Subject Regular expression (?i).*invoice\s+#?\d{4,}.*
Action Move to IMAP folder Accounts.Invoices

Reading the expression: (?i) makes it case-insensitive; .* at each end satisfies the entire-value rule (§27.2); invoice\s+#?\d{4,} requires the word followed by an optional # and at least four digits. “Your Invoice #20260807” matches; a casual message that merely mentions an invoice does not. Check it against sample subjects with Utilities.CriteriaMatch (§27.2) before saving.

27.5 When a rule does not fire

Rules fail quietly — there is no “rule declined to match” log line. Work from symptom to cause:

Symptom Likely cause
Never matches, though the mail looks right Filtering the wrong list: To/CC are headers, but delivery follows the envelope. BCC’d and list mail carry the recipient only in the envelope
A Recipient list criterion in an account rule never matches It cannot — the envelope list is cleared before account rules run (§27.2). Move it to a global rule
Regex matches in your head but not on the server It must match the entire value. Add .* at both ends; confirm with Utilities.CriteriaMatch (§27.2)
A regular-expression rule never fires, or fires for a while and then stops The pattern does not compile (HM6042 in the error log), or one match took 250 ms or more and the pattern is suspended for 300 seconds (HM6043). Fix or simplify it; a corrected pattern is used from the next message
The rule matches but the action seems undone A later rule overrode it — the last move wins, and account rules run after global ones. The account’s Sieve script runs after both, and its fileinto outranks a rule’s folder (chapter 12)
The rule never even runs An earlier rule executed Stop rule processing, or a global rule deleted the message before your account rule’s turn. Or the rule has no criteria: a rule without one runs no actions in either match mode, so a catch-all needs one that is always true, such as Message size Greater than 0 (Recipe 7)
Forwarded or auto-replied mail arrives late Up to 6.3.3 a message a rule created waited for the delivery manager’s next look at the queue, up to a minute (below). Since 6.3.4 it leaves the queue at once, so a delay now is in its delivery: look for it on Monitoring & troubleshooting → Delivery queue
The archive receives duplicates Forwarding loop: the forwarded copy re-enters rule processing. Add a Not contains loop guard (Recipe 3). Or retries: server and domain rules run again on every delivery attempt, so a message deferred for some recipient can forward, copy or reply again on each attempt. Add a Delivery attempts Equals 1 criterion
Matches most mail but not certain messages Encoded or malformed headers — see below

Encoded and malformed headers

Header values arrive MIME-encoded more often than you might think, particularly subjects in non-Latin scripts. hMailServer decodes headers before matching where it can, but content in some encodings is effectively unsearchable — a Contains criterion on a Japanese subject may never match even though the client displays it perfectly.

Malformed messages are the sneakier case. A correctly formed message ends every line with carriage return plus line feed; some sending software emits bare line feeds. When that happens in the header block, hMailServer cannot reliably read individual header values — and criteria against them fail. The server is behaving correctly: a line that is not properly ended is not a line.

What makes this confusing is that mail clients paper over the damage and render the message perfectly. To check, open the raw message file from the data directory in Notepad, which shows the file exactly as it is — word processors quietly repair the very line endings you are trying to observe. Several headers jammed onto one visual line means a malformed message. You can refuse such messages outright with the SMTP RFC-compliance options on the SMTP tab of Connections & protocols → Protocols — disable the allowance for incorrectly formatted line endings (AllowIncorrectLineEndings).

A rule’s forward no longer waits a minute

Up to 6.3.3 a message created by a rule — a forward, a copy, a reply — and an account’s own forwarding waited in the queue until the delivery manager next read it, up to a minute later: each is queued from inside the delivery that caused it, and nothing told the manager it was there. Since 6.3.4 the manager is told, and each leaves the queue at once. What stops a badly-written rule pair from bouncing messages between two accounts is the Rule loop limit setting (under Mail flow & delivery → Delivery of e-mail, Rules tab, 5 by default; RuleLoopLimit in the COM API), which caps how long a chain of messages made by rules can grow: a forward, a reply or a copy carries X-hMailServer-LoopCount one higher than the message that caused it, and at the limit nothing more is generated (HM5064, HM5065 or HM5201 in the error log).

Watch it happen

The fastest diagnosis is empirical: open Monitoring & troubleshooting → Live logs, send yourself a message crafted to match, and watch the delivery. Combine that with a Utilities.CriteriaMatch check of your patterns (§27.2) and there is very little a misbehaving rule can hide.

Scripting rules? Rules can also be created and managed through the COM API (chapter 23), and over the REST API (chapter 24): /api/v1/rules for the server’s, and since 6.3.4 /api/v1/domains/{name}/rules for a domain’s and /api/v1/accounts/{address}/rules for an account’s. If you do this, run 6.2.10 or later and check return values — earlier builds contained methods that reported success on calls they had actually refused.


28. Event scripts: the complete reference

Rules and Sieve (chapter 12) act on messages. Event scripts act on the server. At fifteen fixed points in hMailServer’s processing — a connection opening, a message arriving, a delivery failing, a backup finishing — the server hands control to a script you have written, and in several of those events your script decides what happens next: accept, reject, tag, redirect or drop.

Chapter 23 introduced event scripts in a paragraph and a short table. This chapter is the full reference: every event, its exact signature, which objects are live inside it, what you may change, and what the Result object means in each one.

The scripting model is carried over from classic hMailServer — the same file, the same Result semantics — and 6.x extends it. The classic ten events are all still here under their classic names, including OnError (the site documentation’s chapter 23 previously listed it as “OnErrorLog”; that was an error — the event is and always was OnError), so the large body of published 5.x-era scripts still applies unchanged. Five new events join them: OnHELO, OnClientValidatePassword, OnClientLogon, OnRecipientUnknown and OnTooManyInvalidCommands. What has moved is the switch: the classic Administrator — retired in 6.2 — kept it at Settings → Advanced → Scripts; it is now on the Scripting tab of Maintenance → Advanced in the Control Panel, and the script itself is edited on Maintenance → Event scripts.

28.1 Getting a script running

Four pieces, all of them small:

Piece Where
The script file C:\Program Files\hMailServer\Events\EventHandlers.vbs; on Linux, /var/lib/hmailserver/events/EventHandlers.js
The on/off switch Control Panel → Maintenance → Advanced, Scripting tab; off as shipped
The language VBScript (the default) or JScript, chosen in the Language (VBScript, JScript or JavaScript) field on the same tab. JavaScript on Linux since 6.3.4, where there is no Control Panel: the language and the switch are set with hmctl or the REST API, and the script is reloaded over REST (§28.2)
The reload Save & reload on Maintenance → Event scripts, the page that edits the file; saving the Advanced page reloads the script as well

Every handler lives in that one file. Define only the events you need — an event with no matching handler (Sub in VBScript, function in JScript) is simply skipped, at no cost. If you choose JScript, the file is EventHandlers.js in the same folder.

To enable scripting on Windows (a Linux server has no Control Panel; its steps are in §28.2):

  1. Open the Control Panel and go to Maintenance → Advanced, Scripting tab.
  2. Switch scripting on, pick the language, and click Save.
  3. Go to Maintenance → Event scripts, which edits the script file in the Events folder (EventHandlers.vbs, or EventHandlers.js for JScript), and add your handlers. The page reads and writes the file on the machine the Control Panel runs on, so use it on the server itself.
  4. Click Save & reload. It writes a .bak copy first, then reloads the script and checks its syntax.

Edits do nothing until you reload. hMailServer reads the file when it loads the script — at start and at every reload — and keeps that text in memory; each event then runs it in a fresh engine, the whole file from the top and then the handler (so keep code outside a handler to declarations, §28.8). An edit on disk is not seen until the next load. After every edit, click Save & reload (or restart the service); a file that does not compile installs nothing, and the script loaded before stays in force (§28.6). Forgetting this is the single most common “my script doesn’t work” report.

The script runs as the service account — by default NT SERVICE\hMailServer on an installation made with 6.3.4 or later, LocalSystem on one upgraded from an earlier version unless it was moved (§17.3), and the hmailserver user on Linux. Anyone who can write to the Events folder can execute code as that account. On Windows, restrict NTFS write access to administrators, and treat the file like the configuration it is.

28.2 VBScript, JScript or JavaScript

One language per server, chosen on the Scripting tab of Maintenance → Advanced. The events, objects and Result semantics are identical in VBScript and JScript; only the syntax differs. JavaScript, the Linux language, is below.

Sub OnAcceptMessage(oClient, oMessage)
   EventLog.Write("Accepted a message from " & oMessage.FromAddress)
End Sub
function OnAcceptMessage(oClient, oMessage)
{
   EventLog.Write("Accepted a message from " + oMessage.FromAddress);
}

In practice, on Windows, choose VBScript unless you have a strong reason not to: nearly every published hMailServer example, forum answer and snippet you will ever find is VBScript, and translating on the fly is a needless source of bugs. The rest of this chapter uses VBScript throughout.

On Linux, since 6.3.4, the language is JavaScript — the QuickJS engine, embedded in the server, for the same events under the same function names with the same objects, in /var/lib/hmailserver/events/EventHandlers.js. The scripting language setting takes it as a third word; Windows keeps VBScript and JScript and refuses JavaScript at load, and Linux refuses the other two. The setting is seeded VBScript on every platform, so a Linux server sets it to JavaScript before switching scripting on: hmctl settings set scripting --set-string language=JavaScript, then hmctl settings set scripting --set enabled=true, then a reload with POST /api/v1/settings/scripting/reload. A script there has no file, process or network access, and ScriptTimeout and a 64 MB ceiling bound it. Not every COM member is there: Message.Attachments and Message.Headers are not (a header is written with SetHeaderValue(name, value)), names are case-sensitive, CreateObject throws a TypeError, and null stands where Windows passes Nothing. A JScript file carried from Windows is where a port starts, not a file that runs unchanged; EventScripts.md has the member-by-member contract and a ported handler.

Scripting in JScript? Upgrade to 6.3.4 or later. In every release from 6.0.0 to 6.3.3, a JScript server with an OnClientValidatePassword handler could be made to run code inside hMailServer.exe by any client that named an existing, active account at a sign-in, because the server escaped the apostrophe but not the backslash in the values it passed; the same was open through OnExternalAccountDownload and OnDeliveryFailed, and through a rule’s Run function action in either language. It is CVE-2026-100741; chapter 25 has the detail. Until you can upgrade, switch event scripting off. A server that cannot do without its scripts should take the OnClientValidatePassword, OnExternalAccountDownload and OnDeliveryFailed handlers out of a JScript EventHandlers.js and reload the scripts, and, in either language, let nobody but the server administrator make or edit a rule with a Run function action. Since 6.3.4 a handler receives every value exactly as it was, backslashes included, so a handler that undid the old escaping by hand must stop; and a rule’s Run function action must name a plain identifier (ASCII letters, digits, _ and $, not starting with a digit), or nothing is run: on Windows the name is refused as HM5018 each time the rule matches, and on Linux it finds no function.

28.3 Where the events sit in the mail flow

Seven of the fifteen events fire in a fixed order as a message travels through the server, interleaved with the built-in checks. Four more police the session — the two login events, unknown recipients and hostile clients. Knowing the order tells you what has already happened — and therefore what you can rely on — inside each handler:

TCP connection opened
│
├── OnClientConnect            (oClient)
│
├── HELO / EHLO
├── OnHELO                     (oClient)
│
├── AUTH, if the client logs in
│   ├── OnClientValidatePassword   (oAccount, sPassword)   not for SCRAM
│   └── OnClientLogon              (oClient)
│
├── MAIL FROM / RCPT TO
│   ├── connection-stage checks    DNS blacklists, HELO host, MX, SPF, greylisting
│   └── OnRecipientUnknown         (oClient, oMessage)   on an unknown local address
│
├── DATA command issued
├── OnSMTPData                 (oClient, oMessage)
├── message content received
│
├── content-stage checks       SURBL, DKIM, SpamAssassin
│
├── OnAcceptMessage            (oClient, oMessage)      ← last chance to reject
│
│   message accepted, saved, queued
│
├── OnDeliveryStart            (oMessage)
├── anti-virus, global rules
├── OnDeliverMessage           (oMessage)
├── delivery to mailboxes / onward relay, then account-level rules
│
└── OnDeliveryFailed           (oMessage, sRecipient, sErrorMessage)

The two login events fire on POP3 and IMAP sessions too, not only SMTP, and OnTooManyInvalidCommands can fire at any point in an SMTP session — the moment the server decides the client is hostile and disconnects it. The remaining four fire outside the mail flow: OnExternalAccountDownload when mail is fetched from an external POP3 or IMAP account, and OnBackupCompleted, OnBackupFailed and OnError when the thing their name describes happens.

Event Fires when
OnClientConnect A client opens a TCP connection to the server
OnHELO An SMTP client greets the server with HELO or EHLO — new in 6.x
OnClientValidatePassword An account password is about to be validated — new in 6.x
OnClientLogon An SMTP, POP3 or IMAP authentication attempt completes — new in 6.x
OnSMTPData An SMTP client issues the DATA command
OnAcceptMessage A message is about to be accepted for delivery
OnRecipientUnknown A RCPT TO names an unknown local recipient — new in 6.x
OnTooManyInvalidCommands The server disconnects a session for too many invalid commands — new in 6.x
OnDeliveryStart Delivery of an accepted message begins, before any rules
OnDeliverMessage During delivery — after global rules, before account rules
OnDeliveryFailed Delivery of an accepted message to a recipient has failed
OnExternalAccountDownload A message has been fetched from an external POP3 or IMAP account
OnBackupCompleted A backup has finished successfully
OnBackupFailed A backup has failed
OnError The server has logged an error

28.4 The Result object

Result is a temporary object that the server creates for the events that consult it — eight of the fifteen. Your script writes to it; hMailServer reads it when the handler returns and acts accordingly. It has three properties:

Property Type Meaning
Result.Value number Your decision. Pre-set to 0 — “carry on as normal” — in every event except OnClientValidatePassword, where the server pre-sets 2 (“built-in validation decides”)
Result.Message string The error text used when an event supports a script-defined rejection
Result.Parameter number An extra numeric argument; used only by OnExternalAccountDownload

What each value means depends on the event:

Event 0 1 2 3
OnClientConnect accept the connection close the connection — —
OnHELO accept the greeting reject with 554 5.7.1 Rejected reject with 554 5.7.1 and Result.Message temporary failure: 453 4.7.0 and Result.Message
OnClientValidatePassword password is correct — log the user in password is wrong — refuse the login built-in validation decides (the pre-set default) —
OnSMTPData accept reject with 554 5.7.1 Rejected reject with 554 5.7.1 and Result.Message temporary failure: 453 4.7.0 and Result.Message
OnAcceptMessage accept reject with 554 5.7.1 Rejected reject with 554 5.7.1 and Result.Message temporary failure: 453 4.7.0 and Result.Message
OnDeliveryStart deliver do not deliver — —
OnDeliverMessage deliver do not deliver — —
OnExternalAccountDownload account’s own setting applies delete from the remote server now delete after Result.Parameter days never delete from the remote server

The other seven events — OnClientLogon, OnRecipientUnknown, OnTooManyInvalidCommands, OnDeliveryFailed, OnBackupCompleted, OnBackupFailed and OnError — are notifications only: setting Result there does nothing. In OnClientLogon, OnRecipientUnknown, OnTooManyInvalidCommands, OnDeliveryFailed and OnError the server does not create the Result object at all, so referencing it there is a runtime error, not a no-op.

Older hMailServer documentation quoted the fixed rejection reply as 542 Rejected — a code no SMTP standard defines. What the server actually sends is 554 5.7.1 Rejected. The temporary-failure value 3 (a 453 4.7.0 reply) asks a well-behaved sender to retry later — useful for “not right now” policies where a permanent 554 would be a lie.

“Do not deliver” is silent. A Result.Value of 1 in OnDeliveryStart or OnDeliverMessage drops the message with no bounce and no trace beyond the logs. The sender believes it arrived. Use it deliberately, never as a lazy alternative to rejecting at SMTP time, where the sender is told.

28.5 The event reference

Every event below is available in 6.2 and later, and on Linux since 6.3.4. Alongside the named parameters, EventLog (§28.6) exists in every handler, and Result (§28.4) in the eight events that honour it. On Windows, scripts may also create COM objects with CreateObject (§28.7): any class while ScriptAllowedObjects is at its shipped *, and only the listed ones once it is narrowed. On Linux CreateObject exists only to throw a TypeError.

New in 6.x. The hMailServer.Client object handed to the connection and message events has grown: alongside the classic IPAddress, Port, Username and HELO it now reports SessionID (matches the session ID in the logs), Authenticated, EncryptedConnection, and — on TLS sessions — the negotiated CipherVersion, CipherName and CipherBits. All are read-only.

OnClientConnect

Fires the moment a client opens a TCP connection to any of the server’s protocol ports — SMTP, IMAP and POP3 alike. Nothing has been said on the wire yet: no banner, no login. oClient.Username is therefore always an empty string here.

Sub OnClientConnect(oClient)
Parameter Type Populated
oClient hMailServer.Client IPAddress, Port and SessionID only

No message object exists at this point. Check oClient.Port when your policy applies to one protocol only.

Result.Value Effect
0 Accept the connection
1 Close the connection immediately

This event fires for every connection the server receives. It is the hottest path in the whole scripting surface — a handler that takes 100 ms puts 100 ms in front of every SMTP, IMAP and POP3 session. Keep it to a few comparisons. For static allow and deny policy, Sign-in & access → IP ranges is faster and clearer; script only what ranges cannot express.

OnHELO

New in 6.x. Fires when an SMTP client greets the server — once per HELO or EHLO command. A client that greets again fires it again, and every STARTTLS client does exactly that after the handshake, so expect more than one firing per session.

Sub OnHELO(oClient)
Parameter Type Populated
oClient hMailServer.Client IPAddress, Port, SessionID, HELO, and the encryption properties; Username is still empty

oClient.HELO holds the name the client just presented — the natural place to refuse greetings that claim to be your own server or bare IP-address literals your policy forbids.

Result.Value Effect
0 Accept the greeting
1 Reject with 554 5.7.1 Rejected
2 Reject with 554 5.7.1 and the text in Result.Message
3 Reject temporarily with 453 4.7.0 and the text in Result.Message

A rejection refuses the greeting itself, so the session goes no further than trying again or leaving. The built-in HELO host check (§26.5) already covers the common cases; script the exotic ones.

OnClientValidatePassword

New in 6.x. Fires whenever hMailServer is about to check a password a client sent — SMTP AUTH, POP3 and IMAP logins with a password. It lets a script take over that check entirely: an external directory, a per-application password scheme, an extra policy gate. It is not consulted for SCRAM-SHA-256 (or -PLUS), which proves the password without handing it to the server and is offered wherever AUTH is: a client that signs in with SCRAM never reaches the handler, so a gate built on it does not stop that client. The server pre-sets Result.Value to 2, so a handler that does nothing changes nothing.

Sub OnClientValidatePassword(oAccount, sPassword)
Parameter Type Meaning
oAccount hMailServer.Account The account logging in — loaded fresh from the database, not from the cache
sPassword string The password the client supplied, in clear text
Result.Value Effect
0 The password is correct — the login succeeds, and built-in validation is skipped
1 The password is wrong — the login fails
2 (the pre-set default) hMailServer’s own validation decides

This handler sees every user’s password in clear text, on every password sign-in. Never write sPassword to a log, and remember this is a hot path — it runs for every password check on every protocol. Note too that Result.Value = 0 bypasses all built-in checks, including the refusal of empty passwords — if you accept, be sure you have actually checked something.

OnClientLogon

New in 6.x. Fires when an authentication attempt completes on SMTP, POP3 or IMAP — successful or not. oClient.Authenticated tells you which, and oClient.Username holds the name that was attempted.

Sub OnClientLogon(oClient)
Parameter Type Populated
oClient hMailServer.Client fully — Username, Authenticated, the connection and encryption properties (HELO on SMTP sessions only)

Notification only — no Result object. This is the place for login auditing and custom alerting on suspicious patterns; the brute-force case itself is already handled by auto-ban (chapter 17), which needs no script.

OnSMTPData

Fires once per message, when the SMTP client issues the DATA command — the message content has not yet been transferred. The connection-stage anti-spam tests (DNS blacklists, HELO host, MX, SPF) and greylisting, which decides at RCPT TO, have run; the content transfer, the content-stage tests (SURBL, DKIM, SpamAssassin) and OnAcceptMessage all lie ahead. Greylisting waits until after DATA for an empty envelope sender, or for a sender whose greylisting bypass entry needs DKIM or DMARC; and on a connection from a listed incoming relay the connection-stage tests wait for the body too. Rejecting here spares the server the transfer and all of that later work.

Sub OnSMTPData(oClient, oMessage)
Parameter Type Populated
oClient hMailServer.Client fully
oMessage hMailServer.Message partially — see below

At this early point — no content has arrived, so there is no subject, body or header to read — only four things on oMessage are reliable:

Property What it holds
oMessage.Charset The message character set — utf-8, say
oMessage.Filename The message’s .eml file under the Data directory
oMessage.FromAddress The envelope sender
oMessage.Recipients Each recipient’s Address
Result.Value Effect
0 Accept the message
1 Reject with the fixed SMTP reply 554 5.7.1 Rejected
2 Reject with 554 5.7.1 and the text you put in Result.Message
3 Reject temporarily with 453 4.7.0 and the text in Result.Message

Never call oMessage.Save inside OnSMTPData. The message is still being processed; saving it here corrupts it. Inspect and decide — nothing more. If you want to modify the message, wait for OnAcceptMessage.

The fixed 554 5.7.1 Rejected tells the sender nothing. Prefer Result.Value = 2 with a plain, truthful Result.Message — or 3 when “try again later” is the honest answer.

OnAcceptMessage

The workhorse. Fires when SMTP delivery of the message to the server is complete and every built-in anti-spam check has run — the last moment at which the sender can still be told “no”. The message has not yet been written to the database, so it has no ID yet; everything else about it is available.

Sub OnAcceptMessage(oClient, oMessage)
Parameter Type Populated
oClient hMailServer.Client fully
oMessage hMailServer.Message fully, except ID

You may change anything on the message — headers via oMessage.HeaderValue, Subject, Body, the Recipients and Attachments collections — and the changes stick once you call oMessage.Save. Save once, at the end, not after every change.

Result.Value Effect
0 Accept the message
1 Reject with the fixed SMTP reply 554 5.7.1 Rejected
2 Reject with 554 5.7.1 and the text you put in Result.Message
3 Reject temporarily with 453 4.7.0 and the text in Result.Message

OnRecipientUnknown

New in 6.x. Fires when an SMTP client’s RCPT TO names an address in a local domain that does not exist — just before the server answers 550 Unknown user. The rejection has already been decided; the event is your window onto it.

Sub OnRecipientUnknown(oClient, oMessage)
Parameter Type Populated
oClient hMailServer.Client fully
oMessage hMailServer.Message the envelope so far — FromAddress and any previously accepted Recipients

Notification only — no Result object; you cannot un-reject the recipient. Use it to spot directory-harvest probes (many unknown recipients from one IP in quick succession) and to catch persistently mistyped addresses that deserve an alias (chapter 13).

OnTooManyInvalidCommands

New in 6.x. Fires when the server gives up on an SMTP session that has exceeded the invalid-command limit and disconnects it — just after it sends 421 4.7.0 Too many invalid commands. Bye!.

Sub OnTooManyInvalidCommands(oClient, oMessage)
Parameter Type Populated
oClient hMailServer.Client fully
oMessage hMailServer.Message the message transaction in progress, if any — guard before touching it, since a session can be dropped before any MAIL FROM

Notification only — the disconnect has already been decided. Log the client details; if the same address keeps appearing, an IP range or firewall rule finishes the job. The event fires only while Disconnect clients sending too many invalid commands is on (Connections & protocols → Protocols, SMTP tab), and it is off by default.

OnDeliveryStart

Fires when delivery of an accepted message begins — before the anti-virus check and before any global rules run. The client is long gone; only the message is available.

Sub OnDeliveryStart(oMessage)
Parameter Type Populated
oMessage hMailServer.Message fully
Result.Value Effect
0 Deliver the message
1 Do not deliver — the message is dropped silently, with no bounce

OnDeliverMessage

Fires during delivery, after global rules have run but before account-level rules. This is the difference from OnDeliveryStart, which fires before both. Use OnDeliverMessage when you want to see the message as the global rules have left it.

Sub OnDeliverMessage(oMessage)
Parameter Type Populated
oMessage hMailServer.Message fully
Result.Value Effect
0 Deliver the message
1 Do not deliver — dropped silently, no bounce

OnDeliveryFailed

Fires when delivery of an accepted message to a recipient fails. A message the server rejected — at SMTP time, by anti-spam, or by your own script — never reaches this event, because it was never accepted in the first place. For a message with several failed recipients the event fires once per recipient. A temporary deferral that is still being retried is not yet a failure; the event accompanies the failure that is final.

Sub OnDeliveryFailed(oMessage, sRecipient, sErrorMessage)
Parameter Type Meaning
oMessage hMailServer.Message The message that could not be delivered
sRecipient string The recipient it could not be delivered to
sErrorMessage string The error that caused the failure, typically the remote server’s reply

Notification only — no Result object.

OnExternalAccountDownload

Fires when a message has been downloaded from an external account — the per-account POP3 or IMAP fetching configured on the account dialog’s External tab (chapter 6).

Sub OnExternalAccountDownload(oFetchAccount, oMessage, sRemoteUID)
Parameter Type Meaning
oFetchAccount hMailServer.FetchAccount The external account the message was fetched for
oMessage hMailServer.Message The downloaded message — Nothing (null in JavaScript) if it had been downloaded before, or if the download failed
sRemoteUID string The message’s UID on the remote server

Here the Result object controls one thing only: what happens to the copy on the remote server. The downloaded message is delivered locally whatever you set.

Result.Value Effect on the remote copy
unset (0) The external account’s own deletion setting applies
1 Delete it immediately
2 Delete it after the number of days in Result.Parameter
3 Never delete it

Always test If oMessage Is Nothing before touching the message — a previously seen message arrives with no message object at all, and an unguarded property access is a runtime error.

OnBackupCompleted

Fires when a backup (chapter 15) has completed successfully. No parameters, no result — use it on Windows to notify someone, or to copy the finished backup off the machine. On Linux a handler has no file, network or process access and creates no objects, so there it can only write to EventLog.

Sub OnBackupCompleted()

OnBackupFailed

Fires when a backup has failed, and tells you why. A backup that fails silently is worse than no backup at all — at minimum, log it somewhere a human will look. Since 6.3.4 the server also raises its own backup.failed alert, on as shipped, which is mailed once AlertRecipient and AlertSenderAddress are set. A handler that logs the reason:

Sub OnBackupFailed(sReason)
   EventLog.Write("BACKUP FAILED — " & sReason)
End Sub
Parameter Type Meaning
sReason string Why the backup failed — "The specified backup directory is not accessible: …", "Could not backup data directory." and the like

Notification only — setting Result here does nothing.

OnError

Fires when the server logs an error. Notification only — no Result object.

Sub OnError(iSeverity, iCode, sSource, sDescription)
Parameter Type Meaning
iSeverity number 1 Critical, 2 High, 3 Medium, 4 Low
iCode number The hMailServer error code
sSource string The component that reported the error
sDescription string A description of the error

This event has the same name it had in classic hMailServer — OnError — so 5.x-era handlers carry over unchanged. The site documentation’s chapter 23 previously listed it as “OnErrorLog”; that was an error in the documentation, not a rename in the server.

Keep OnError handlers minimal, and never do anything in one that can itself fail and be logged as an error — that way lies a loop. Filter on iSeverity and pass the details somewhere cheap. For serious alerting, the Prometheus metrics endpoint (chapter 16) is the sturdier tool.

28.6 Logging and debugging

The EventLog object is available in every handler:

EventLog.Write("OnAcceptMessage: from=" & oMessage.FromAddress)

It appends to the events log, hmailserver_events.log, in the Logs directory (/var/log/hmailserver on Linux; chapter 16). Liberal EventLog.Write calls while developing, stripped back once the script works, is the debugging method that works.

When a script goes wrong:

  • A compile error loads nothing. If the file will not compile, or its code outside a function throws, none of it is installed, and the error log records the error with its line and column. At a service start that means no events fire at all (HM5016) and mail flows as if scripting were switched off; at a reload the previous script stays in force, whole, and goes on running (HM5710), so the old handlers still run and your edit does not. A language the build does not run (VBScript or JScript on Linux, JavaScript on Windows) is refused the same way, with a line naming the language to use. A Linux server still on the seeded VBScript runs no script until the language is set to JavaScript. Check the error log first whenever a script appears to be ignored, and use Check syntax on Maintenance → Event scripts (or POST /api/v1/settings/scripting/check) to see the compile error and its line.
  • A runtime error aborts the handler at the point it strikes and is written to the error log. Whatever Result.Value held at that moment is what the server acts on — for most events the pre-set 0, so the message carries on (chapter 23), but a value the handler set before the error still stands. Do not paper over this with a blanket On Error Resume Next; if you must use it, check Err afterwards and log what happened.
  • Watch it live. Keep Monitoring & troubleshooting → Live logs open and send yourself a test message, exactly as in chapter 6:
Send-MailMessage -SmtpServer localhost -Port 25 `
   -From "[email protected]" -To "[email protected]" `
   -Subject "script test" -Body "Trigger the events."

And develop on a test server, not the production one. Every mistake in EventHandlers.vbs is a mistake in the path of live mail.

28.7 Reaching the rest of the COM API

On Windows, event scripts are not limited to the objects handed to them. The whole COM API of chapter 23 is available — create the Application object, authenticate, and read or change anything the Control Panel can:

Sub OnBackupFailed(sReason)
   Dim oApp
   Set oApp = CreateObject("hMailServer.Application")

   If oApp.Authenticate("Administrator", "your-admin-password") Is Nothing Then
      EventLog.Write("OnBackupFailed: COM authentication failed.")
      Exit Sub
   End If

   ' oApp.Settings, oApp.Domains and the rest are now available.
End Sub

Check the return values. On current builds a refused COM call genuinely fails — Authenticate returns Nothing on a bad password — so a script that tests its results finds out immediately. The background is the 6.2.10 authorization fix, described in chapter 29. Since 6.3.4 a refusal from any COM class also arrives with the server’s own sentence, where 72 classes gave only the bare error 0x800403E9, so log the error’s description. And once the administrator credential has a second factor (§17.4), Authenticate refuses Administrator outright: AuthenticateWithCode is the only way past it, and a script has no code to give.

Putting the administrator password in EventHandlers.vbs hands it to anyone who can read the file, so restrict reading the Events folder as well as writing to it (§28.1): administrators, and the service account, which needs only Read there. Fetch only what you need, and do not cache authenticated objects across events — each handler invocation stands alone. Since 6.3.4 a property set over COM on a route, an incoming relay, a DNS blacklist, a SURBL server, a remote-domain policy, a server message, a blocked attachment, an SSL certificate or a group is held on that object’s own copy until Save(), so a script that changes one of these must save it or nothing changes.

None of this section applies on Linux, where event scripts are JavaScript since 6.3.4 and there is no COM: CreateObject and new ActiveXObject exist there only to throw a TypeError, and a handler reaches nothing beyond the objects it is handed.

28.8 Performance and safety

Event scripts run synchronously, inside the mail flow. The rules that follow are all consequences of that one fact.

  • Budget in milliseconds. Every line of OnClientConnect runs for every connection; every line of OnAcceptMessage runs for every inbound message — and every line of OnClientValidatePassword runs for every password sign-in on every protocol (not SCRAM-SHA-256, which has no password to hand it).
  • Keep the top level to declarations. Every event gets a fresh script engine that runs the whole file before it calls the handler, so code outside a function runs for every connection, every message and every error, and nothing a script keeps in a global survives to the next event. A top level that throws, or runs past the timeout, fails the load.
  • The timeout is a backstop, not a budget. A handler still running after ScriptTimeout seconds (60 by default) is interrupted — HM5019 on Windows, HM7341 on Linux — and everything behind it has waited that long; on Windows an interrupt cannot release a handler blocked inside a COM call. The key is set on Maintenance → Server limits & expert settings (card Timeouts and queue bounds), over REST or COM, or with hmailserver --set-setting on Linux.
  • No network calls. An HTTP request, DNS lookup or database query to another machine inside an event puts that machine’s latency — and its outages — in front of your mail. If an external check matters, note the message and let something outside the mail flow do the checking.
  • Handlers run concurrently. The server processes many sessions at once, so the same event can be executing several times simultaneously. Keep handlers re-entrant: no shared state, and treat any shared file as contended.
  • Never anything interactive. The script runs inside the server’s service, with no desktop. A MsgBox has nowhere to appear and will hang the handler; on Linux a JavaScript handler has no print or console either, so write to EventLog.
  • Save once, and never in OnSMTPData. Make all your changes, then one oMessage.Save at the end of the handler.
  • Prefer the built-in tool when one exists. IP ranges, rules, Sieve, auto-ban and the anti-spam stack — since 6.3.4 also the domain white and black list, and country blocking given a country database you keep up to date — are all faster than script and need no script maintenance. Script is for the gaps between them.

28.9 Worked examples

Four complete, deployable handlers. Adjust the addresses and lists, paste into EventHandlers.vbs, reload, and test. They are VBScript, for Windows. On Linux, where the language is JavaScript since 6.3.4, the first three can be rewritten in JavaScript in EventHandlers.js; beyond the syntax, write message.SetHeaderValue(name, value) where VBScript assigns HeaderValue, and message.Save() with its brackets; the fourth has no Linux form, because a JavaScript handler cannot create objects.

Tag suspicious mail instead of rejecting it

Subject-pattern matching produces false positives, so mark the message and let it through — a tagged message is recoverable, a rejected one is not. Rules or Sieve (chapter 12) can then file anything carrying the header.

Sub OnAcceptMessage(oClient, oMessage)
   Dim sSubject
   sSubject = LCase(oMessage.Subject)

   If InStr(sSubject, "urgent invoice") > 0 Or _
      InStr(sSubject, "payment overdue") > 0 Then

      oMessage.HeaderValue("X-PR-Suspect") = "subject-pattern"
      oMessage.Subject = "[SUSPECT] " & oMessage.Subject
      oMessage.Save
   End If
End Sub

Refuse unwanted networks at connect

Prefix matching against a deny list, applied to port 25 only so your own users on the submission port are never touched. For ranges you can write down statically, prefer Sign-in & access → IP ranges. For whole countries, prefer the country blocking built in since 6.3.4 (chapter 10): with a MaxMind country database named in GeoIpDatabaseFile, a CountryPolicySMTP of block:CN,RU closes those countries’ connections before the greeting, with no script. That policy covers every SMTP listener, submission included, so a prefix list loaded into this array remains the way to police port 25 alone.

Sub OnClientConnect(oClient)
   ' Police port 25 only — our users authenticate on 587.
   If oClient.Port <> 25 Then Exit Sub

   Dim aBlocked, sPrefix
   aBlocked = Array("203.0.113.", "198.51.100.")

   For Each sPrefix In aBlocked
      If Left(oClient.IPAddress, Len(sPrefix)) = sPrefix Then
         Result.Value = 1          ' close the connection
         Exit Sub
      End If
   Next
End Sub

Block a known spammer at SMTP time

Rejecting in OnSMTPData tells the sender honestly and spares the server the content transfer and the content-stage anti-spam work. Only FromAddress and the recipient addresses are reliable here — and no oMessage.Save. For a whole sending domain, 6.3.4’s Spam & virus filtering → Domain white and black list needs no script: a black entry refuses it at MAIL FROM, before anything else runs, for an unauthenticated session from an IP range that runs the spam tests.

Sub OnSMTPData(oClient, oMessage)
   Dim sFrom
   sFrom = LCase(oMessage.FromAddress)

   If sFrom = "[email protected]" Or _
      InStr(sFrom, "@spam-domain.example") > 0 Then
      Result.Message = "Mail from this sender is not accepted here."
      Result.Value = 2
   End If
End Sub

Tell the postmaster when delivery fails

Creating a new hMailServer.Message and saving it queues it for delivery like any other mail. The guards at the top matter: skip bounces (their envelope sender is empty) and skip mail from the notification address itself, or one stuck recipient becomes an endless loop of notifications about notifications.

Sub OnDeliveryFailed(oMessage, sRecipient, sErrorMessage)
   If oMessage.FromAddress = "" Then Exit Sub
   If LCase(oMessage.FromAddress) = "[email protected]" Then Exit Sub

   Dim oNotice
   Set oNotice = CreateObject("hMailServer.Message")
   oNotice.From        = "[email protected]"
   oNotice.FromAddress = "[email protected]"
   oNotice.AddRecipient "", "[email protected]"
   oNotice.Subject = "Delivery failure: " & sRecipient
   oNotice.Body = "A message from " & oMessage.FromAddress & _
                  " to " & sRecipient & " could not be delivered." & vbCrLf & vbCrLf & _
                  "Error: " & sErrorMessage & vbCrLf & _
                  "Subject: " & oMessage.Subject
   oNotice.Save              ' saving a new message queues it for delivery
End Sub

More: Event scripts: every event and its arguments, the limits, and the 6.3.4 security fix on the forum.



29. COM API reference I: server, domains and accounts

This is the first of three reference chapters covering every object in the COM API. Chapter 23 introduces the API and event scripts; this chapter documents the root Application object, the server-wide Settings object, and everything that makes up a domain — accounts, aliases, distribution lists, groups and external fetch accounts — plus the API’s constants and its changelog. The remaining objects (messages, folders, rules, anti-spam and the other Settings sub-objects) follow in the next two reference chapters.

Nearly everything the Control Panel can do, the API can do — the Control Panel is a COM API client, as were the two administration tools this fork retired, the classic hMailServer Administrator and the PHP WebAdmin (chapter 25). Some features have no COM surface at all and are REST only (chapter 24), among them SCIM provisioning, SAML and OpenID Connect sign-in, passkeys, the send API, migrations and eDiscovery, whose read-only Control Panel page reads over REST. PowerShell is the preferred scripting language today and is used for the examples; VBScript still works, both standalone under cscript.exe and in event scripts. The whole API is also available to .NET code through C:\Program Files\hMailServer\Bin\Interop.hMailServer.dll. All of this is Windows: the Linux build has no COM, is administered over the REST API (chapter 24) and its command line, and since 6.3.4 runs its event scripts in JavaScript rather than VBScript or JScript (chapter 28).

Connecting and authenticating

Every script starts the same way: create an hMailServer.Application object and call Authenticate. Nothing else in the API is reachable until you have — every other object is navigated to through the Application object, and that is deliberate: it is how the API enforces authentication. The one exception is a server with no administrator password: while [Security] AdministratorPassword is empty and no second factor is enrolled on the administrator credential, every COM session starts already authenticated as a server administrator, with no credential at all (the REST API refuses to start in that state, but COM does not). Set the password (chapter 17).

$app = New-Object -ComObject 'hMailServer.Application'
$session = $app.Authenticate('Administrator', 'your-admin-password')
if ($null -eq $session) { throw 'Authentication failed' }

"Connected to hMailServer $($app.Version)"

Authenticate returns an object representing the authenticated identity on success, and nothing at all on failure — test the return value, do not assume.

Two kinds of credentials are accepted:

User name Password Rights
Administrator The main administration password Full server administration
An account address ([email protected]) That account’s password (not one of its app passwords, which Authenticate answers as a wrong password) Whatever the account’s AdminLevel grants — its own settings, its domain, or the whole server. Since 6.3.4, an account given an administrator role (Accounts & domains → Administrator roles) reaches what that role’s permissions reach, in the domains its assignment names (none named: every domain for serveradmin and auditor, the account’s own domain for the other roles); Application.CurrentRole and CurrentPermissions say what the session holds

Accessing something your credentials do not cover throws an exception; it does not fail silently. Since 6.3.4 the exception carries the server’s own sentence (You do not have access to this property / method…), where up to 6.3.3 seventy-two classes answered with a bare Exception from HRESULT: 0x800403E9.

A second factor changes the call. Once a second factor is enrolled on the administrator credential (chapter 17), Authenticate refuses the administrator whatever the password. Read $app.AdministratorTOTPEnabled, which answers before you authenticate, and call $app.AuthenticateWithCode('Administrator', $password, $code) instead. Since 6.3.4 a code is accepted once only: a second sign-in within the same half minute waits for the authenticator’s next code.

VBScript is unchanged:

Dim app: Set app = CreateObject("hMailServer.Application")
If app.Authenticate("Administrator", "your-admin-password") Is Nothing Then
   WScript.Echo "Authentication failed"
   WScript.Quit 1
End If

COM calls run against the local installation. The Control Panel administers remote servers over DCOM, and a script can do the same — the Remote administration support component in the installer (chapter 4) registers the COM type library for exactly that, and DCOM must be permitted between the two machines. For remote automation, though — and from anything that is not Windows — the REST API (chapter 24) is the better tool.

Because this fork is 64-bit only, the old 5.x gotcha of needing a 32-bit script host to create the COM object is gone. Any 64-bit PowerShell or cscript.exe works.

The 6.2.10 return-value fix. Before 6.2.10, fifteen COM methods reported success (S_OK) on calls they had actually refused — among them SetAdministratorPassword, which could claim a password change that never happened, and five getters that handed an unauthorised caller “success” plus uninitialised memory. 6.2.10 makes refused calls return errors, as they always should have. If you script against the COM API, run 6.2.10 or later, and check every return value — a script that appeared to work on an earlier version may have been silently doing nothing.

How the API fits together

The object model is a tree of its own; the Control Panel’s eight task-based groups do not follow it. Every object is reached from Application; you cannot create a Domains or Settings object directly.

hMailServer.Application
├─ Settings                          ← server-wide configuration (this chapter)
│    ├─ AntiSpam, AntiVirus, Logging, MessageIndexing, Routes, SecurityRanges, TCPIPPorts, …
│    │                               ← later reference chapters
│    └─ Groups ── Group ── Members ── GroupMember
├─ Domains ── Domain
│    ├─ Accounts ── Account
│    │    ├─ IMAPFolders ── IMAPFolder ── Messages ── Message
│    │    ├─ FetchAccounts ── FetchAccount
│    │    └─ Rules ── Rule
│    ├─ Aliases ── Alias
│    ├─ DistributionLists ── DistributionList ── Recipients ── DistributionListRecipient
│    ├─ DomainAliases ── DomainAlias
│    └─ Rules ── Rule                ← the domain's own rules (6.3.4)
├─ GlobalObjects ── DeliveryQueue, Languages, MessageTrace
├─ Links                             ← shortcut lookups by database ID
├─ Rules                             ← global rules
└─ Status, Utilities, Database, BackupManager, Diagnostics

Collections follow one pattern. A plural object (Domains, Accounts, Aliases…) holds singular items. To create an item, call Add() on the collection — the new object exists only in memory until you call Save() on it, which writes it to the database and adds it to the collection. Collections expose Count, an indexed Item() (zero-based), lookups by database ID (ItemByDBID) and usually by name or address, a matching set of Delete methods, and Refresh() to re-read from the database. PowerShell can enumerate any collection directly with foreach.

Since 6.3.4 a change to an item the running server shares waits for Save(). For a route or one of its addresses, an incoming relay, a DNS blacklist, a SURBL server, a remote domain policy, a server message, a blocked attachment, an SSL certificate or a group, setting a property writes your COM object’s own copy; Save() writes it to the database and the server reads the list again. A property set and never saved changes nothing (up to 6.3.3 it changed the running server until the next restart), and another object for the same item, read before the save, keeps what it read: read the item again from its collection after saving.

The API throws exceptions — surfaced in PowerShell as a terminating COMException, so wrap risky calls in try/catch:

It throws when… Example
You create a duplicate Two accounts with the same address
A collection lookup finds nothing ItemByName('no-such-domain.com')
Your credentials are insufficient A user-level session touching Settings
The database is unreachable Connection to the database server lost

More: The COM API on Windows: object model, Save() since 6.3.4, roles and the Import Tool on the forum.

Application

The root of the API. Its properties are the doorways to everything else; its methods control the server process itself.

Property Type Access Meaning
AdministratorTOTPEnabled bool RO Whether a second factor is enrolled on the administrator credential; readable before authenticating (6.2.25)
BackupManager BackupManager RO Runs backups and restores
Database Database RO The database interface — connection state, transactions, setup
Diagnostics Diagnostics RO New in 6.x. Connectivity self-tests — PerformTests() returns a DiagnosticResults collection (documented in a later reference chapter)
Domains Domains RO Every domain on the server
GlobalObjects GlobalObjects RO Delivery queue, message trace and language catalogue
InitializationFile string RO Path of the hMailServer.INI in use
Links Links RO Shortcut lookups by database ID
Rules Rules RO The global (server-wide) rules
ServerState eServerState RO Current state of the server engine
Settings Settings RO All server-wide configuration
Status Status RO Live counters and status text
Utilities Utilities RO Helper functions (hashing, DNS checks, message import)
Version string RO The hMailServer version
VersionArchitecture string RO The build architecture — x64 in the x64 installer’s build; the ARM64 installer’s build, which 6.3.6 is the first release to carry, answers ARM64
Method Does
Authenticate(username, password) Authenticates the session; returns the authenticated identity, or nothing on failure
AuthenticateWithCode(username, password, code) Authenticates with a one-time code as well — the only call that satisfies a second factor, the administrator credential’s included
Connect() Opens the database connection without authenticating — used by setup tooling
Reinitialize() Reinitialises the server in place: every service stops and starts again, with its settings read afresh from the database. It is what the Control Panel’s Apply now does; since 6.3.4 it applies the saved [Settings] keys that wait for a reinitialise, and it ends maintenance mode (a drain)
Start() Starts the server engine
Stop() Stops the server engine
SubmitEMail() Kicks off delivery of pending messages — useful after inserting a message directly into the database

New in 6.3.4: administrator roles. Application gains Roles and RoleAssignments, which list the roles and who holds them; AssignRole(address, role, domains), which gives an account a role (an empty role name takes it away); CurrentRole and CurrentPermissions, which say what this session holds; and ResetPassword and UnlockAccount, which a help desk may call for the accounts it reaches. An account’s own role reads back as Account.RoleName, RoleDomains and RolePermissions. COM’s settings objects answer only a server administrator, so an auditor reads the configuration through the Control Deck or REST.

Settings

Server-wide configuration — everything that is not tied to one domain. This is the largest object in the API, so its scalar properties are grouped below by what they control. The read-only properties that return whole sub-objects (AntiSpam, Logging, …) are listed last. The tables cover the long-standing core: 6.3.4 appends many more members, among them the breach check (BreachCheck…), bounce and complaint processing (Bounce…, Complaint…), webhooks, country blocking, session policy, outbound shaping, spam traps, the attachment sandbox (Sandbox…), message-store compression and setting profiles (GetProfiles, ApplyProfile, RevertProfile). Every one is appended, so an early-bound client built against 6.3.3 keeps working.

Settings has no Save(). Each property write is persisted immediately, and most take effect at once, as when the same field is saved in the Control Panel (chapter 20). The thread counts (TCPIPThreads, MaxDeliveryThreads, MaxAsynchronousThreads) size pools the server builds when its services start, so a new value waits for a reinitialise (Application.Reinitialize) or a restart. Most keys that were the [Settings] section of hMailServer.INI have no property of their own and are reached through GetIniSetting and SetIniSetting (below); a few do, such as RewriteEnvelopeFromWhenForwarding under Performance and everything else. Since 6.3.4 the database holds them, and a save applies 211 of them at once, 28 at a reinitialise and 2 at a service restart. Since 6.3.4, too, a setting the server’s environment manages (HM_PROPERTY_<name>, HM_SETTING_<key>) refuses a change until a start without that variable.

Protocols and sessions

Property Type Access Meaning
ServiceSMTP bool RW Enable the SMTP service
ServiceIMAP bool RW Enable the IMAP service
ServicePOP3 bool RW Enable the POP3 service
HostName string RW The host name the server identifies itself with
DefaultDomain string RW Domain assumed when a user logs on without @domain
WelcomeSMTP string RW SMTP greeting banner (empty = default)
WelcomeIMAP string RW IMAP greeting banner
WelcomePOP3 string RW POP3 greeting banner
MaxSMTPConnections long RW Simultaneous SMTP connections (0 = unlimited)
MaxIMAPConnections long RW Simultaneous IMAP connections
MaxPOP3Connections long RW Simultaneous POP3 connections
MaxNumberOfInvalidCommands long RW Invalid client commands tolerated before disconnect
DisconnectInvalidClients bool RW Drop clients that do not follow the RFCs
AllowIncorrectLineEndings bool RW Accept mail with a bare CR or LF
AllowSMTPAuthPlain bool RW Permit plain-text SMTP authentication mechanisms
AddDeliveredToHeader bool RW Add a Delivered-To header to incoming messages
DenyMailFromNull bool RW Reject mail with an empty envelope sender

Leave DenyMailFromNull off. The null sender (MAIL FROM:<>) is how bounces and delivery reports arrive; rejecting it breaks the standard and loses you legitimate non-delivery notices.

Delivery and relaying

Property Type Access Meaning
SMTPNoOfTries long RW Retries of a temporarily failed delivery before the message is bounced (0 = one attempt, then a bounce)
SMTPMinutesBetweenTry long RW Minutes between delivery attempts
SMTPConnectionSecurity eConnectionSecurity RW Encryption used when delivering after an MX lookup
SMTPDeliveryBindToIP string RW Local IP address to bind for outbound delivery
MaxDeliveryThreads long RW Simultaneous SMTP delivery threads
MaxSMTPRecipientsInBatch long RW Recipients per outbound SMTP transaction
MaxNumberOfMXHosts long RW How many of a domain’s MX hosts to try per delivery (0 = all)
MaxMessageSize long RW Largest message accepted for sending, in KB (0 = no limit)
MirrorEMailAddress string RW An address that receives a copy of all mail (empty = off)
SMTPRelayer string RW Smart host to relay all outbound mail through (empty = deliver directly); several hosts separated by | are tried in turn
SMTPRelayerPort long RW Smart host port
SMTPRelayerConnectionSecurity eConnectionSecurity RW Encryption for the smart host connection
SMTPRelayerRequiresAuthentication bool RW Smart host requires a login
SMTPRelayerUsername string RW Smart host user name
SMTPRelayerUseSSL bool RW Legacy toggle — superseded by SMTPRelayerConnectionSecurity

A smart host (chapter 14) configured entirely by script:

$s = $app.Settings
$s.SMTPRelayer                        = 'smtp.example.net'
$s.SMTPRelayerPort                    = 587
$s.SMTPRelayerConnectionSecurity      = 3          # eCSSTARTTLSRequired
$s.SMTPRelayerRequiresAuthentication  = $true
$s.SMTPRelayerUsername                = 'apikey'
$s.SetSMTPRelayerPassword('the-relay-password')

Auto-ban

Property Type Access Meaning
AutoBanOnLogonFailure bool RW Ban an IP after repeated failed logins
AutoBanMinutes long RW How long a ban lasts
MaxInvalidLogonAttempts long RW Failures before a ban
MaxInvalidLogonAttemptsWithin long RW Minutes before a failure record expires

IMAP and public folders

Property Type Access Meaning
IMAPACLEnabled bool RW Enable the IMAP ACL extension
IMAPIdleEnabled bool RW Enable the IMAP IDLE extension
IMAPQuotaEnabled bool RW Enable the IMAP QUOTA extension
IMAPSortEnabled bool RW Enable the IMAP SORT extension
IMAPHierarchyDelimiter string RW The folder-hierarchy delimiter used in the IMAP protocol
IMAPMasterUser string RW The IMAP master-user name (empty = disabled)
IMAPSASLPlainEnabled bool RW Offer the SASL PLAIN authentication mechanism to IMAP clients
IMAPSASLInitialResponseEnabled bool RW Accept a SASL initial response (SASL-IR) from IMAP clients
IMAPPublicFolderName string RW The public-folder root name shown to IMAP clients
PublicFolderDiskName string RO The on-disk folder name public folders are stored under
PublicFolders IMAPFolders RO The public folder tree

TLS

These are the same settings as Certificates & encryption → SSL/TLS in the Control Panel; the certificates are on Certificates & encryption → SSL certificates and Certificates & encryption → Certificates (ACME).

Property Type Access Meaning
SslCipherList string RW Permitted ciphers for TLS 1.2 and below, in OpenSSL format; it does not touch TLS 1.3, whose suites are the [Settings] key TlsCipherSuites13 (SetIniSetting)
VerifyRemoteSslCertificate bool RW Verify the remote certificate when delivering outbound
SSLCertificates SSLCertificates RO Certificates configured on the server
TlsVersion10Enabled bool RW Enable TLS 1.0 — off by default, see below
TlsVersion11Enabled bool RW Enable TLS 1.1 — off by default, see below
TlsVersion12Enabled bool RW Enable TLS 1.2 — on by default
TlsVersion13Enabled bool RW New in 6.x. Enable TLS 1.3 — on by default
TlsOptionPreferServerCiphersEnabled bool RW Prefer the server’s cipher order over the client’s
TlsOptionPrioritizeChaChaEnabled bool RW Prefer ChaCha20-Poly1305 when the client puts it first; it takes effect only with TlsOptionPreferServerCiphersEnabled on

The defaults — TLS 1.2 and 1.3 only — are correct, for the reasons chapter 9 gives. The TLS 1.0 and 1.1 toggles still function, for the rare ancient client that cannot do better; if a script must turn one on, plan to replace that client rather than live with it. The 5.x SslVersion30Enabled property is gone from the API entirely — OpenSSL 4.0, which this fork builds against, does not implement SSL 3.0 at all, and a script that still touches that property now fails instead of silently doing nothing.

Performance and everything else

Property Type Access Meaning
TCPIPThreads long RW Threads handling TCP/IP communication
WorkerThreadPriority long RW Meant as the OS priority for worker threads; stored but never read by the server, so setting it changes nothing (the Control Panel shows it read-only)
MaxAsynchronousThreads long RW Cap on asynchronous task threads
RuleLoopLimit long RW The rule loop guard: how long a chain of messages made by rules can grow (a forward, a reply or a copy carries X-hMailServer-LoopCount one higher than the message that caused it; at the limit nothing more is generated)
IPv6PreferredEnabled bool RW Prefer IPv6 over IPv4 when both are available
RewriteEnvelopeFromWhenForwarding bool RW Rewrite the envelope sender to the forwarding account’s address when forwarding
UserInterfaceLanguage string RW Language for administrative text; only third-party COM tools read it (the Control Panel has its own language choice)
CrashSimulationMode long RW Deliberately faults the server — used by the test suite; leave at 0

The 5.x SendStatistics telemetry switch has been removed from the API, along with the phone-home statistics feature behind it. Old scripts that set it will fail on this fork; delete the line.

Sub-object gateways

Each of these read-only properties returns a whole configuration object, documented in the following reference chapters.

Property Returns
AntiSpam Spam thresholds, SPF, greylisting, SpamAssassin, DNSBL and SURBL collections
AntiVirus Virus scanning and attachment blocking
Backup Backup configuration (a BackupSettings object)
Cache Object-cache tuning and hit rates
Directories Program, data, log and temp directory paths
Groups Server-wide account groups (documented in this chapter)
IncomingRelays Upstream gateways (spam filters, load balancers) whose IP addresses do not count as the connecting client in anti-spam host checks
Logging Log switches, paths and retention; since 6.3.4 also syslog forwarding (Syslog…)
MessageIndexing New in 6.x. The message-indexing engine — enable it, read the total/indexed message counts, Clear() and rebuild with Index() (documented in a later reference chapter)
RemoteDomainPolicies New in 6.3.4. Per remote domain: the TLS this server demands in each direction, the size and connections it will attempt, recipient verification and gateway encryption (Mail flow & delivery → Remote domains)
Routes Per-domain delivery overrides
Scripting Event-script configuration
SecurityRanges IP ranges — who may connect, authenticate and relay
ServerMessages The text of bounces and system messages
TCPIPPorts Listening ports and their TLS bindings

Settings methods

Method Does
SetAdministratorPassword(newVal) Sets the main administration password (see below for a new 6.3.4 installation)
SetSMTPRelayerPassword(newVal) Sets the smart host password
ClearLogonFailureList() Clears the failed-login records behind auto-ban, for every address; server administrator only. It does not lift a ban already made: delete that ban’s range on Sign-in & access → IP ranges
GetIniSetting(name), SetIniSetting(name, value), DeleteIniSetting(name) Read, write or remove one key of the settings store that was the [Settings] section of hMailServer.INI (chapter 21); since 6.3.4 the database holds it, and a removed key returns to its default. IniSettingNames lists every key the server knows
IniSettingApplies(name) New in 6.3.4. When a save of that key applies: now, reinitialize or restart. PendingIniSettings lists the saved keys the server does not run on yet, one per line
EnrolAdministratorTOTP(), DisableAdministratorTOTP() Enrol a second factor on the administrator credential (returns the otpauth:// URI) or remove it (chapter 17)

SetAdministratorPassword was among the methods fixed in 6.2.10 — on earlier versions it could report success for a refused call, leaving the old password silently in place. Check the call succeeds, and verify with a fresh Authenticate.

The password is kept in [Security] AdministratorPassword in hMailServer.INI, and a new 6.3.4 installation runs the service as NT SERVICE\hMailServer, which may not write that file. There the call is refused with a sentence, as are EnrolAdministratorTOTP and DisableAdministratorTOTP. Set the password from an elevated prompt with hMailServer.exe --set-admin-password, giving it in HM_ADMIN_PASSWORD or on standard input and never as an argument, then restart the service; on Linux the same is sudo hmailserver --set-admin-password.

Domains and Domain

The Domains collection

All domains on the server, reached as Application.Domains.

Property Type Access Meaning
Count long RO Number of domains
Item(index) Domain RO Domain by position in the collection
ItemByDBID(dbid) Domain RO Domain by database ID
ItemByName(name) Domain RO Domain by name — throws if not found
Names string RO A flat listing of every domain’s database ID and name — a cheap way to fill a picker
Method Does
Add() Returns a new, unsaved Domain
DeleteByDBID(dbid) Deletes the domain with that database ID. Since 6.3.4 it is refused, with an error, while the domain or any account in it is under an eDiscovery legal hold
Refresh() Re-reads the collection from the database

The Domain object

One hosted domain, with its collections of accounts, aliases, lists and domain aliases. 6.3.4 appends further members, among them Rules (the domain’s own rules), the quota-warning policy (QuotaWarning…), DKIM key rotation (DKIMRotation…), what the domain does to a message (ExternalTag…, Disclaimer…), impersonation protection, spam traps and setting levels (SettingOverride, EffectiveSetting).

Property Type Access Meaning
Accounts Accounts RO The domain’s accounts
Active bool RW Whether the domain is enabled
Aliases Aliases RO The domain’s aliases
ADDomainName string RW Active Directory domain to synchronise with
AddSignaturesToLocalMail bool RW Apply signatures to domain-internal mail
AddSignaturesToReplies bool RW Apply signatures to replies
AllocatedSize long RO Space already allocated to the domain’s accounts, in MB
AntiSpamEnableGreylisting bool RW Greylisting on or off for this domain
DKIMSignEnabled bool RW Sign outgoing mail from this domain with DKIM
DKIMSelector string RW The DKIM selector — names the DNS record holding the public key
DKIMPrivateKeyFile string RW Path to the private key used for signing. Since 6.3.4, while the domain’s DKIM key rotation is on or has a key in flight, a Save() that changes the selector or the key file is refused: use the rotation’s own members (DKIMRotation…)
DKIMSignAliasesEnabled bool RW Also sign mail sent from the domain’s aliases
DKIMSigningAlgorithm eDKIMAlgorithm RW Signature hash — SHA-1 (1) or SHA-256 (2); use SHA-256
DKIMHeaderCanonicalizationMethod eDKIMCanonicalizationMethod RW Header canonicalization — simple (1) or relaxed (2)
DKIMBodyCanonicalizationMethod eDKIMCanonicalizationMethod RW Body canonicalization — simple (1) or relaxed (2)
DistributionLists DistributionLists RO The domain’s distribution lists
DomainAliases DomainAliases RO Alternative names for the domain
ID long RO Database ID
MaxAccountSize long RW Largest quota that may be set on an account in this domain, in MB
MaxMessageSize long RW Largest message sent from this domain, in KB
MaxNumberOfAccounts long RW Account limit for the domain
MaxNumberOfAccountsEnabled bool RW Enforce the account limit
MaxNumberOfAliases long RW Alias limit for the domain
MaxNumberOfAliasesEnabled bool RW Enforce the alias limit
MaxNumberOfDistributionLists long RW Distribution-list limit for the domain
MaxNumberOfDistributionListsEnabled bool RW Enforce the list limit
MaxSize long RW Size cap for the whole domain, in MB (0 = unlimited)
Name string RW The domain name. Since 6.3.4 a single-label name that is one of the directories the server keeps its own files in beside the domains’ mail (quarantine, files, lists, blobs and the like) is refused for a new domain or a rename
PlusAddressingCharacter string RW The separator for plus addressing (usually +)
PlusAddressingEnabled bool RW alice+tag@ delivers to alice@
Postmaster string RW Catch-all — mail to a nonexistent address in the domain is redirected here
SignatureEnabled bool RW Domain signature on or off
SignatureHTML string RW HTML version of the signature
SignatureMethod eDomainSignatureMethod RW How the domain signature combines with account signatures
SignaturePlainText string RW Plain-text version of the signature
Size long RO Current size of the domain, in MB
Method Does
Save() Writes the domain to the database
Delete() Deletes the domain — and everything in it. Since 6.3.4 it is refused, with an error, while the domain or any account in it is under an eDiscovery legal hold
SynchronizeDirectory() Synchronises the domain against Active Directory

The domain’s limits — the MaxNumberOf… counts, MaxAccountSize and MaxSize — are enforced by the server itself when an account, alias or distribution list is created or edited: a save that would break one is refused, with the limit’s own message. They are not checked during mail flow. Each count applies only while its …Enabled flag is on; clearing the flag keeps the number but stops enforcing it.

Creating a domain with an alternative name:

$domain = $app.Domains.Add()
$domain.Name       = 'example.com'
$domain.Active     = $true
$domain.Postmaster = '[email protected]'
$domain.Save()

$da = $domain.DomainAliases.Add()
$da.AliasName = 'example.net'
$da.Save()

Accounts and Account

The Accounts collection

All accounts in one domain, reached as Domain.Accounts.

Property Type Access Meaning
Count long RO Number of accounts
Item(index) Account RO Account by position
ItemByAddress(address) Account RO Account by email address
ItemByDBID(dbid) Account RO Account by database ID
Method Does
Add() Returns a new, unsaved Account
Delete(index) Deletes the account at that position
DeleteByDBID(dbid) Deletes the account with that database ID
Refresh() Re-reads the collection from the database

The Account object

One mailbox: its address, password, quota, forwarding, auto-reply, signature and Active Directory link, plus gateways to its folders, messages, rules and fetch accounts.

Property Type Access Meaning
Active bool RW Whether the account may log on and receive mail
Address string RW The email address
AdminLevel eAdminLevel RW Administration rights — see Constants below
ADDomain string RW Active Directory domain name
ADUsername string RW Active Directory user name
IsAD bool RW The account authenticates against Active Directory
DomainID long RW Database ID of the owning domain
FetchAccounts FetchAccounts RO External POP3 and IMAP accounts downloaded into this mailbox
ForwardEnabled bool RW Forwarding on or off
ForwardAddress string RW Where to forward
ForwardKeepOriginal bool RW Keep a copy after forwarding
ForwardAbortSpamFlagged bool RW Do not forward messages flagged as spam
ID long RO Database ID
IMAPFolders IMAPFolders RO The account’s folder tree
LastLogonTime date RO When the account was last used
MaxSize long RW Mailbox quota in MB (0 = unlimited)
Messages Messages RO The messages in the mailbox
Password string RW Set to change the password — the server stores a hash, so the plaintext cannot be read back. A new password must pass the password policy and, since 6.3.4 and only when it is switched on (it is off by default), the breach check, which refuses one with a COM error carrying the reason
PersonFirstName string RW First name of the holder
PersonLastName string RW Last name of the holder
QuotaUsed long RO Percentage of quota used, 0–100
Rules Rules RO The account’s own rules
SieveScript string RW New in 6.x. The account’s active Sieve (RFC 5228) filter script
SignatureEnabled bool RW Signature on or off
SignatureHTML string RW HTML signature
SignaturePlainText string RW Plain-text signature
Size float RO Current mailbox size, in MB
VacationMessageIsOn bool RW Auto-reply on or off
VacationSubject string RW Auto-reply subject
VacationMessage string RW Auto-reply body
VacationMessageAbortSpamFlagged bool RW Do not auto-reply to messages flagged as spam
VacationMessageExpires bool RW Auto-reply switches itself off on a date
VacationMessageExpiresDate string RW That date
Method Does
Save() Writes the account to the database
Delete() Deletes the account and its mail
DeleteMessages() Deletes every message in the mailbox
UnlockMailbox() Releases a stuck POP3 mailbox lock
ValidatePassword(password) Returns true if the password is correct — the supported way to check a credential

Delete() and DeleteMessages() are immediate and unrecoverable short of a backup restore. There is no confirmation dialog in a script. Since 6.3.4 both are refused, with an error that says why, while an eDiscovery legal hold covers the account or its domain.

Members the tables leave out. AppPasswords (the account’s app passwords), TOTPEnabled, EnrolTOTP and DisableTOTP (its second factor), SpamMarkThreshold and SpamDeleteThreshold (its own spam thresholds), AntiSpamEnabled, MessageRetentionDays (0 = the domain’s policy, -1 = keep forever), VacationMessageBeginDate and ExportMessages. 6.3.4 appends more, after every earlier member, so an early-bound client built against 6.3.3 keeps working (recompile it to reach them): mailbox move (MoveTo, InUseReason, LastMove); the account’s administrator role (RoleName, RoleDomains, RolePermissions); recovery (RecoveryAddress, which answers the address masked, RecoveryAddressVerified, RecoveryCodesUnused, ClearRecoveryAddress, ClearRecoveryCodes); the out-of-office policy (VacationMessageDays, VacationInternalSubject, VacationInternalMessage, VacationMessageInternalOnly); devices (GetDevices, RevokeDevice); Web Push (PushSubscriptionCount, RemovePushSubscriptions); the directory entry (DirectoryTitle, DirectoryDepartment, DirectoryPhone, DirectoryMobile, DirectoryOffice, DirectoryOptOut, DirectoryNote, DirectoryPhoto, DirectoryPhotoType); OpenPGPPublish and OpenPGPKeys; ExportArchive; ZeroAccess; RestrictedDelivery; setting levels (SettingOverride, EffectiveSetting, EffectiveSettingSource); ExternalId, the identity provider’s id for an account SCIM made; and Kind — 0 a person (the default), 1 a meeting room, 2 a piece of equipment, saved by Save().

Creating a mailbox with a quota and an auto-reply:

$account = $app.Domains.ItemByName('example.com').Accounts.Add()
$account.Address           = '[email protected]'
$account.Password          = 'a-strong-password'
$account.Active            = $true
$account.MaxSize           = 2000                    # MB
$account.VacationSubject   = 'Out of office'
$account.VacationMessage   = 'Back on Monday.'
$account.VacationMessageIsOn = $true
$account.Save()

A housekeeping sweep — disable anything unused for a year:

$cutoff = (Get-Date).AddYears(-1)
foreach ($d in $app.Domains) {
   foreach ($a in $d.Accounts) {
      if ($a.LastLogonTime -and [datetime]$a.LastLogonTime -lt $cutoff) {
         $a.Active = $false
         $a.Save()
         "{0} disabled (last logon {1})" -f $a.Address, $a.LastLogonTime
      }
   }
}

Aliases and Alias

An alias (chapter 13) is a second address delivering to an existing one. The Aliases collection hangs off a domain; each Alias maps a Name (the alias address) to a Value (where it delivers).

The Aliases collection

Property Type Access Meaning
Count long RO Number of aliases
Item(index) Alias RO Alias by position
ItemByDBID(dbid) Alias RO Alias by database ID
ItemByName(name) Alias RO Alias by its address
Method Does
Add() Returns a new, unsaved Alias
Delete(index) Deletes the alias at that position
DeleteByDBID(dbid) Deletes by database ID
Refresh() Re-reads from the database

The Alias object

Property Type Access Meaning
Active bool RW Whether the alias resolves
DomainID long RW Database ID of the owning domain
ID long RO Database ID
Name string RW The alias address, e.g. [email protected]
Value string RW Where it delivers, e.g. [email protected]
Method Does
Save() Writes the alias to the database
Delete() Deletes the alias
$alias = $app.Domains.ItemByName('example.com').Aliases.Add()
$alias.Name   = '[email protected]'
$alias.Value  = '[email protected]'
$alias.Active = $true
$alias.Save()

DomainAliases and DomainAlias

A domain alias is an alternative name for a whole domain — mail to [email protected] delivered to the accounts of example.com. Do not confuse it with an ordinary alias, which maps a single address.

The DomainAliases collection

Property Type Access Meaning
Count long RO Number of domain aliases
Item(index) DomainAlias RO Domain alias by position
ItemByDBID(dbid) DomainAlias RO Domain alias by database ID
Method Does
Add() Returns a new, unsaved DomainAlias
Delete(index) Deletes by position
DeleteByDBID(dbid) Deletes by database ID
Refresh() Re-reads from the database

The DomainAlias object

Property Type Access Meaning
AliasName string RW The alternative domain name
DomainID long RW Database ID of the real domain
ID long RO Database ID
Method Does
Save() Writes to the database
Delete() Deletes the domain alias

Distribution lists

A distribution list (chapter 13) delivers one message to many recipients. Three objects are involved: the domain’s DistributionLists collection, each DistributionList, and its Recipients collection of DistributionListRecipient objects.

The DistributionLists collection

Property Type Access Meaning
Count long RO Number of lists
Item(index) DistributionList RO List by position
ItemByAddress(address) DistributionList RO List by its address
ItemByDBID(dbid) DistributionList RO List by database ID
Method Does
Add() Returns a new, unsaved DistributionList
DeleteByDBID(dbid) Deletes by database ID
Refresh() Re-reads from the database

The DistributionList object

Property Type Access Meaning
Active bool RW Whether the list accepts and delivers mail
Address string RW The list address, e.g. [email protected]
BounceAddress string RW The envelope sender of every copy the list sends, so bounces reach it and not the poster; empty = the poster (6.2.24). A list with Managed on uses the list manager’s own bounce handling instead
ID long RO Database ID
Mode eDistributionListMode RW Who may post: 0 anyone, 1 list members only, 2 the one announcement address, 3 anyone in the list’s domain (a claimed address, so require SMTP authentication with it); 4 is refused
ModeratorAddress string RW Where a posting the mode refuses is sent instead; the moderator approves it by sending it again from an authenticated session. Empty = no moderation (6.2.24)
Recipients DistributionListRecipients RO The membership
RequireSenderAddress string RW The one address allowed to post, in announcement mode
RequireSMTPAuth bool RW Posting requires an authenticated SMTP session
Method Does
Save() Writes the list to the database
Delete() Deletes the list and its membership

6.3.4 appends Membership, a membership query answered against the server’s accounts when a message is sent — empty for a list of its recipients alone, or domain, domain:name, pattern:wildcard or everyone, always in union with the recipients and judged when the list is saved — and the list manager’s Managed, SubscribePolicy, Archive, ArchiveDays, Digest, DigestHour, ReplyToList, SubjectPrefix, Footer, Verp and Owner, with the method SendDigest; the list manager’s fields do nothing until Managed is on.

Set the posting mode deliberately. A list anyone may post to is a spam relay in waiting — the same warning as in chapter 13 applies doubly to lists created by script, where no dialog prompts you to think about it.

DistributionListRecipients and DistributionListRecipient

The membership of one list.

Property Type Access Meaning
Count long RO Number of recipients
Item(index) DistributionListRecipient RO Recipient by position
ItemByDBID(dbid) DistributionListRecipient RO Recipient by database ID
Method Does
Add() Returns a new, unsaved recipient
DeleteByDBID(dbid) Removes a recipient by database ID

Each recipient is small, and its long-standing members are below. 6.3.4 adds the list manager’s state for a member — Delivery (every message, the daily digest, or nothing), Confirmed, HardBounces and SoftBounces — and the method ResetBounces.

Property Type Access Meaning
ID long RO Database ID
RecipientAddress string RW The member’s email address — local or external
Method Does
Save() Writes the recipient to the database
Delete() Removes the recipient from the list
$list = $app.Domains.ItemByName('example.com').DistributionLists.Add()
$list.Address         = '[email protected]'
$list.Active          = $true
$list.RequireSMTPAuth = $true
$list.Save()

foreach ($member in '[email protected]', '[email protected]') {
   $r = $list.Recipients.Add()
   $r.RecipientAddress = $member
   $r.Save()
}

Groups and group members

Groups collect accounts so that public-folder permissions (chapter 13) can be granted to many people at once. They are server-wide, not per-domain — the collection hangs off Settings.Groups, and appears in the Control Panel under Accounts & domains → Groups. Groups are one of the collections the running server reads while it works, so since 6.3.4 a property set on a Group is held on your object’s own copy until Save(): one set and never saved changes nothing, and an object read before the save keeps what it read, so read the group again from Settings.Groups to see the saved values (chapter 23).

The Groups collection

Property Type Access Meaning
Count long RO Number of groups
Item(index) Group RO Group by position
ItemByDBID(dbid) Group RO Group by database ID
ItemByName(name) Group RO Group by name
Method Does
Add() Returns a new, unsaved Group
DeleteByDBID(dbid) Deletes by database ID
Refresh() Re-reads from the database

The Group object

Property Type Access Meaning
ID long RO Database ID
Members GroupMembers RO The group’s membership
Name string RW The group name
Method Does
Save() Writes the group to the database
Delete() Deletes the group

GroupMembers and GroupMember

The membership collection carries the standard members — Count, Item(index), ItemByDBID(dbid), Add(), DeleteByDBID(dbid), Refresh(). Each member links a group to an account by database ID:

Property Type Access Meaning
Account Account RO The member account itself
AccountID long RW Database ID of the member account
GroupID long RW Database ID of the group
ID long RO Database ID of the membership row
Method Does
Save() Writes the membership to the database
Delete() Removes the member from the group
$group = $app.Settings.Groups.Add()
$group.Name = 'Support'
$group.Save()

$alice = $app.Domains.ItemByName('example.com').Accounts.ItemByAddress('[email protected]')

$member = $group.Members.Add()
$member.AccountID = $alice.ID
$member.Save()

Fetch accounts

A fetch account downloads mail from an external POP3 or IMAP mailbox into a local account — the External tab of the Control Panel’s account dialog. The FetchAccounts collection hangs off an Account.

ServerType 0 is POP3 and 1 is IMAP (new in 6.2.25): the remote INBOX is collected once by UID, over LOGIN or XOAUTH2, with STARTTLS or implicit TLS. In that mode it is the INBOX only — other folders are not mirrored, and a message that leaves the remote INBOX by some other route is forgotten rather than deleted locally.

MirrorFolders (new in 6.2.27, schema 6031 — Mirror every folder in the Control Panel) changes that. Every remote folder is collected into the local folder of the same name: the message bytes as they are, the \Seen \Flagged \Answered \Draft \Deleted flags, the internal date, the remote hierarchy delimiter mapped to the local one, and a per-folder record so a second poll takes only what is new. Nothing is delivered — no header, rule, anti-spam or anti-virus pass touches a mirrored copy — and setting days to keep messages to -1 turns the mirror into a move (to 0 up to 6.3.5, when the IMAP fetcher read the two the other way round; see DaysToKeepMessages below).

Since 6.3.4, FetchAccount.Save refuses a user name or password that contains a CR, LF or NUL, which the fetchers would otherwise have sent to the external server as a command of its own. Only a server administrator may add or change a fetch account for a host on FetchOAuth2Hosts, whose accounts can log in with the server’s own OAuth2 token, and a bearer token is sent over TLS only. And Save, Delete, DownloadNow and AccountID are refused to a credential that does not reach the local account and take in its authority, so a domain administrator can neither point a fetch account at another domain’s account nor add one to the server administrator’s own mailbox.

The FetchAccounts collection

Standard members: Count, Item(index), ItemByDBID(dbid), Add(), Delete(index), DeleteByDBID(dbid), Refresh().

The FetchAccount object

Property Type Access Meaning
AccountID long RW The local account the mail is delivered into
ConnectionSecurity eConnectionSecurity RW Encryption for the connection to the external server
DaysToKeepMessages long RW Retention on the external server — see below
EnableRouteRecipients bool RW Also deliver to recipients matched by routes
Enabled bool RW Whether the fetch runs
ID long RO Database ID
IsLocked bool RO Whether a fetch is currently running against this account
MIMERecipientHeaders string RW The MIME headers recipients are read from, when ProcessMIMERecipients is on
MinutesBetweenFetch long RW Polling interval
MirrorFolders bool RW IMAP only: collect every remote folder into the local folder of the same name, filed and not delivered (6.2.27, described above)
Name string RW A display name for the fetch account
NextDownloadTime string RO When the next attempt is due
Password string RW Password for the external server
Port long RW External server port: 110 for POP3, or 995 with implicit TLS; 143 for IMAP, or 993 with implicit TLS
ProcessMIMEDate bool RW Take the receive date from the Received header
ProcessMIMERecipients bool RW Resolve recipients from the To/CC headers
ServerAddress string RW The external POP3 or IMAP server
ServerType long RW 0 POP3, 1 IMAP (6.2.25)
UseAntiSpam bool RW Run downloaded mail through anti-spam
UseAntiVirus bool RW Run downloaded mail through anti-virus
Username string RW User name for the external server
UseSSL bool RW Legacy toggle — superseded by ConnectionSecurity

DaysToKeepMessages says what happens on the external server to a message once it has been collected:

Value Behaviour on the external server
-1 Delete it from the external server as soon as it has been collected here
0 Never delete: every message is left on the external server — the default for a new external account over COM, in the Control Panel and over the REST API
n > 0 Keep messages n days, then delete

That is what the setting has always meant to the POP3 fetcher, to COM and to the OnExternalAccountDownload event. From 6.2.25 to 6.3.5 the IMAP fetcher read 0 as “delete” and -1 as “keep”, and the Control Panel, the Deck, the webmail and the REST API described that reversed meaning for both kinds of account; from 6.3.6 both fetchers read the table above, and schema step 6106 translates what is stored so that every account goes on doing what it did or stops deleting, and none starts (§18.5). A program or script that wrote the value to the IMAP fetcher’s earlier meaning is changed before it next writes it.

Method Does
Save() Writes the fetch account to the database
Delete() Deletes the fetch account
DownloadNow() Runs a download immediately instead of waiting for the interval
$fetch = $account.FetchAccounts.Add()
$fetch.Name               = 'Old provider'
$fetch.ServerAddress      = 'pop.oldprovider.example'
$fetch.Port               = 995
$fetch.ConnectionSecurity = 1                        # eCSTLS — implicit TLS
$fetch.Username           = '[email protected]'
$fetch.Password           = 'the-old-password'
$fetch.MinutesBetweenFetch = 15
$fetch.DaysToKeepMessages = -1                       # delete there once collected here
$fetch.Enabled            = $true
$fetch.Save()
$fetch.DownloadNow()

GlobalObjects

A small container, reached as Application.GlobalObjects, for three server-wide objects that fit nowhere else. It has no methods.

Property Type Access Meaning
DeliveryQueue DeliveryQueue RO The outbound queue — inspect, retry and remove waiting messages (documented in a later reference chapter)
Languages Languages RO The catalogue of administrative-interface languages
MessageTrace MessageTrace RO The message trace: what happened to a message, searched by sender, recipient or queue id. It records only while MessageTraceEnabled is on, which it is not by default

Application.Links is a shortcut: when you already hold a database ID — stored from an earlier run, or read straight from the database — it fetches the object directly, without walking the domain and account collections.

Property Type Access Meaning
Account(dbid) Account RO Account by database ID
Alias(dbid) Alias RO Alias by database ID
DistributionList(dbid) DistributionList RO Distribution list by database ID
Domain(dbid) Domain RO Domain by database ID
$account = $app.Links.Account(4711)

An unknown ID throws, like any missing collection item.

Constants

The API’s enumerated types are plain numbers on the wire. PowerShell and VBScript do not load them automatically — use the literal values below, or declare Const lines in VBScript, or read them from the interop assembly (shown after the tables).

eAdminLevel — the rights carried by Account.AdminLevel, and by extension what an account’s credentials may do when used with Authenticate. Three things changed in 6.3.4. An account given an administrator role — serveradmin, domainadmin, helpdesk, auditor or a custom role, in named domains, with Application.AssignRole — reaches what the role’s permissions reach, while an account with no role holds exactly what its level gives; Account.RoleName, RoleDomains and RolePermissions read the assignment back. An app password is no longer a COM logon: Authenticate answers one as a wrong password. And a write to an account or to what is in it (Account.Save and Delete, Accounts.Delete and DeleteByDBID, FetchAccount.Save among them) is refused to a credential that does not reach that account and take in its authority, so a domain administrator is refused them on the server administrator’s own mailbox in its domain. The levels:

Constant Value Grants
hAdminLevelNormal 0 The account’s own settings only
hAdminLevelDomainAdmin 1 Administration of the account’s domain
hAdminLevelServerAdmin 2 Full server administration

eConnectionSecurity — used by Settings.SMTPConnectionSecurity, Settings.SMTPRelayerConnectionSecurity and elsewhere:

Constant Value Meaning
eCSNone 0 No encryption
eCSTLS 1 Implicit TLS from the first byte
eCSSTARTTLSOptional 2 STARTTLS when the remote side offers it
eCSSTARTTLSRequired 3 STARTTLS, or the connection fails

The values are not in order of strength — implicit TLS is 1, between “none” and the STARTTLS modes. Scripts that compare these numerically get it wrong. One naming note: the 5.x documentation and old scripts call constant 1 eCSSSL; this fork’s type library names it eCSTLS. The value is the same either way.

eACLPermission — public-folder rights. These are bit flags: add (or OR) them together to grant several at once.

Constant Value Grants
ePermissionLookup 1 See that the folder exists
ePermissionRead 2 Read messages
ePermissionWriteSeen 4 Change the seen flag
ePermissionWriteOthers 8 Change other flags
ePermissionInsert 16 Copy messages in
ePermissionPost 32 Post to the folder
ePermissionCreate 64 Create sub-folders
ePermissionDeleteMailbox 128 Delete the folder
ePermissionWriteDeleted 256 Set the deleted flag
ePermissionExpunge 512 Expunge deleted messages
ePermissionAdminister 1024 Manage the folder’s permissions

eACLPermissionType — who a public-folder permission applies to:

Constant Value Meaning
ePermissionTypeUser 0 One account
ePermissionTypeGroup 1 A group
ePermissionTypeAnyone 2 Everyone

Further enumerations — eServerState, eDistributionListMode, eDomainSignatureMethod, eSessionType, eMessageFlag, eRuleActionType, eDKIMAlgorithm, eDKIMCanonicalizationMethod and others — are used by individual properties and events. 6.3.4 adds hStateDraining (5) to eServerState, new rule criteria and actions (chapter 30), and two enumerations for remote domain policies: the outbound TLS level (eRTNone 0, eRTEncrypted 1, eRTVerified 2, eRTDane 3) and gateway encryption (eREEncryptNone 0, eREEncryptOpenPgp 1, eREEncryptSmime 2). All of them, names and values, can be read straight out of the interop assembly:

Add-Type -Path 'C:\Program Files\hMailServer\Bin\Interop.hMailServer.dll'
[Enum]::GetValues([hMailServer.eConnectionSecurity]) |
   ForEach-Object { '{0,-24} {1}' -f $_, [int]$_ }

In VBScript, declare what you use:

Const hAdminLevelServerAdmin = 2
Const eCSSTARTTLSRequired = 3

COM API changelog highlights

The API has been stable for a long time, which is why twenty-year-old scripts still run. The changes that matter when reading old scripts and forum posts:

Version Compatibility What changed
4.3 Breaking Authenticate() became mandatory before any other call (only the Message object is exempt). New objects for anti-spam, blocked attachments, SURBL, server messages and TCP/IP ports; new enumerations including eSessionType and eDistributionListMode.
4.4 Additive White-list objects; Account.LastLogonTime; auto-reply expiry; per-domain account/alias/list limits; FetchAccount.DownloadNow(); rule reordering with MoveUp()/MoveDown().
5.0 Breaking IPHome/IPHomes replaced by the TCPIPPort objects. Port numbers, tarpitting, SPF, MX-check and DNSBL settings moved off Settings — to TCPIPPorts and the new AntiSpam object. Domains.ItemByName changed from a method to a property. Delete() added across most objects; Database gained transactions and setup methods.
5.x → 6.2 (this fork) Mostly additive Scripts written against 5.x run unchanged unless they touch what was removed. Added: the Diagnostics/DiagnosticResults and MessageIndexing objects, Account.SieveScript, Settings.TlsVersion13Enabled, and Sieve syntax-check/evaluate helpers on Utilities. Removed: Settings.SslVersion30Enabled and Settings.SendStatistics. Renamed: the constant eCSSSL is now eCSTLS (same value, 1). Most other fork functionality was configured through hMailServer.INI (chapter 21) and the REST API (chapter 24) rather than new COM objects; since 6.3.4 its [Settings] keys are held in the database and a script changes one with Settings.SetIniSetting, not by editing the file (an edited line is reported as HM5804 and, where the service may write the file, put back).
6.2.10 Behavioural fix Fifteen methods that reported S_OK on refused calls now return errors, and five getters no longer hand back uninitialised memory to unauthorised callers. No signatures changed — but scripts that never checked return values may now discover calls that were failing silently all along.
6.3.4 Additive, with behavioural changes Six new interfaces (100 coclasses, up from 94) and new members on Application, Settings, Domain, Account, Utilities and others, all appended, so an early-bound client built against 6.3.3 keeps working; administrator roles arrive as Application.Roles, AssignRole, CurrentRole and CurrentPermissions. What an older script may notice: a property set on a route or a route’s address, an incoming relay, a DNS black list, a SURBL server, a remote domain policy, a server message, a blocked attachment, an SSL certificate or a group takes effect only at that object’s Save() — one set and never saved changes nothing, and an object read before the save keeps the values it read; Authenticate and AuthenticateWithCode no longer accept an app password; a one-time code is accepted once, and wrong codes count against a budget of five in fifteen minutes; 72 classes that answered a refusal with a bare HRESULT now say why; Utilities.MD5 and SHA256 hash text outside ASCII as its UTF-8; and Settings.SetIniSetting writes to the database, with IniSettingApplies and PendingIniSettings saying when a saved key takes effect.

The full details of the 6.2.10 fix are in the version history (chapter 25), which also has an entry for 6.3.4; §18.5 lists what the upgrade to 6.3.4 changes without a switch, the Save() rule for shared configuration objects among them.


30. COM API reference II: messages, folders and rules

Chapter 23 introduced the COM API and the pattern every script follows: create hMailServer.Application, call Authenticate, then walk down to the object you want. The companion reference chapter covers the configuration side — Application, domains and accounts. This chapter covers the other half: the mail itself. Messages and their headers, attachments and recipients; the IMAP folder tree and its permissions; and rules, the criteria-plus-actions system from chapter 12, driven entirely from code.

Two conventions hold throughout:

  • Collections all look the same. Every collection has a read-only Count and a zero-based Item(index). Most add ItemByDBID(id), many add ItemByName(name), and writable ones add Add() and DeleteByDBID(id). Once you have iterated one collection, you have iterated them all.
  • Objects are database rows. Anything with an ID property lives in the database. Save() writes your changes; Delete() removes the row. Until you call Save(), you are editing a copy in memory.

Check your return values. The 6.2.10 authorization fix (chapter 29) applies to every object in this chapter: a refused call fails properly instead of pretending to succeed. Authenticate first, and test what comes back. Since 6.3.4 a refusal also says why, in the server’s own sentence rather than a bare 0x800403E9. And a write through a Rule (with its actions and criteria), an IMAPFolder, an IMAPFolderPermission or a Message asks whether your credential reaches the account it belongs to (as the server administrator, an administrator of that account’s domain, or the account itself) and takes in that account’s authority, and is refused otherwise: a domain administrator can no longer change what is in the server administrator’s mailbox, or save a rule or an action under an account it does not reach. The objects an event script is handed are not affected.

None of these objects is created directly with New-Object — you reach them from an authenticated Application object. The single exception is hMailServer.Message, which can be created standalone to send mail (see the worked examples at the end). From 6.3.6, with an administrator password set, such a message carries the credential of an Application the same program holds and has signed in on, and Message.Save and Copy refuse one that carries none: sign in first and keep the Application until the message is saved (in .NET, GC.KeepAlive), or take the message from the new Application.CreateMessage(), which a .NET program reaches once rebuilt against 6.3.6’s type library, or late-bound. The same holds for a hMailServer.FetchAccount created on its own. Mail that a program signed in as an account queues must be from an address that account may send as; to send as anybody, sign in as the server administrator. The server’s event scripts, which create these objects without signing in, work as before, and a server with no administrator password and no second factor answers every caller as it did.

30.1 The object map

Where everything hangs:

Application
├─ Domains ──▶ Domain
│               ├─ Rules (the domain's own, 6.3.4) ──▶ Rule …
│               └─ Accounts ──▶ Account
│                                ├─ IMAPFolders ──▶ IMAPFolder
│                                │                   ├─ SubFolders ──▶ IMAPFolder …
│                                │                   ├─ Messages ──▶ Message
│                                │                   │                ├─ Headers ─────▶ MessageHeader
│                                │                   │                ├─ Attachments ─▶ Attachment
│                                │                   │                └─ Recipients ──▶ Recipient
│                                │                   └─ Permissions ──▶ IMAPFolderPermission
│                                └─ Rules ──▶ Rule
│                                              ├─ Criterias ──▶ RuleCriteria
│                                              └─ Actions ────▶ RuleAction
├─ Rules (server-wide) ──▶ Rule …
└─ Settings.PublicFolders ──▶ IMAPFolder …

Account rules live on Account.Rules; the server-wide rules from the Control Panel’s Mail flow & delivery → Rules page (on screen, Global rules) live on Application.Rules; and since 6.3.4 a domain’s own rules, edited on the same page with the domain chosen at the top, live on Domain.Rules. The server’s rules run first, then the domain’s, then, at local delivery, the account’s. The public folder tree from chapter 13 is an ordinary IMAPFolders collection reached through Settings.PublicFolders — everything below applies to it equally.

30.2 Messages and their parts

Message

One email message. You meet it in three places: in a folder’s Messages collection, as the oMessage parameter of an event script (chapter 23), and freshly created via New-Object -ComObject 'hMailServer.Message' when sending mail from a script. In a JavaScript event script on Linux (since 6.3.4) the object has no Attachments or Headers, and its two parameterised properties are written with methods, SetHeaderValue(name, value) and SetFlag(flag, on); reading them is unchanged.

Property Type Access What it holds
Attachments Attachments read-only The message’s attachments
Body string read/write The plain-text body
CC string read-only The CC header
Charset string read/write The message’s main character set
Date string read/write The Date header
DeliveryAttempt long read-only Which delivery attempt is in progress
EncodeFields bool read/write Whether header text you set is MIME-encoded (matters for non-ASCII)
Filename string read-only Path of the file holding the message in the data directory. Since 6.3.4 that file is not always the message as it arrived: with MessageStoreCompression on it is the store’s compressed container, in a mailbox encrypted at rest it is encrypted, and with an object store there is no file at that path once the message is committed. Read the message through the API instead
Flag(type) bool read/write One IMAP-style flag — pass a value from the flag table below
From string read/write The From header, display name included
FromAddress string read/write The envelope sender — the SMTP MAIL FROM, written as the first Return-Path
Headers MessageHeaders read-only Every MIME header, as a collection
HeaderValue(name) string read/write The first header with that name; the name is case-insensitive
HTMLBody string read/write The HTML body
ID hyper (64-bit) read-only Database identifier
InternalDate variant (date) read-only The message’s internal (IMAP) date
Recipients Recipients read-only The envelope recipients
Size long read-only Message size, in kilobytes
State long read-only Internal delivery state
Subject string read/write The Subject header
To string read-only The To header
UID long read-only The message’s IMAP UID — unique within its mailbox
Method What it does
AddRecipient(name, address) Adds one recipient, given a display name and an address
ClearRecipients() Removes every recipient, MIME headers included
Copy(folderID) Copies the message into the IMAP folder with that database ID
HasBodyType(type) Whether the message contains a body part of the given content type
RefreshContent() Re-reads the message content
Save() Writes changes — and hands a new message to the delivery queue

Save() on a new message sends it. There is no separate “send” call and no draft state — the moment a newly created message is saved, it is queued for delivery. Set every property first, save once.

Flag() takes one of these values. The flags are stored together as a bit-mask (a message that is Recent and virus-scanned stores 32 + 64 = 96), but through the API you read and set them one at a time as booleans:

Value Flag
1 Seen (read)
2 Deleted
4 Flagged
8 Answered
16 Draft
32 Recent
64 Virus-scanned
128 Spam

Messages

The messages inside one IMAP folder, reached through IMAPFolder.Messages.

Property Type Access What it holds
Count long read-only Number of messages in the folder
Item(index) Message read-only The message at that position (zero-based)
ItemByDBID(id) Message read-only The message with that database ID
Method What it does
Add() Creates a new message in the collection
Clear() Removes every message in the folder
DeleteByDBID(id) Deletes one message by database ID

Deleting shifts the indexes of everything after it. If you delete while iterating with Item(i), walk the collection backwards — from Count - 1 down to 0 — or you will skip every second message. And Clear() empties a mailbox folder with no undo; be certain of which folder object you are holding. Recover deleted items (6.3.4) keeps what a person deletes and what a retention policy removes, not what a script deletes, unless the account or its domain is on an eDiscovery legal hold: a hold keeps a copy of an administrator’s delete among the account’s deleted items.

MessageHeader

A single MIME header — one Name: Value line.

Property Type Access What it holds
Name string read/write The header’s name
Value string read/write The header’s value
Method What it does
Delete() Removes this header from the message

MessageHeaders

Every MIME header of one message, via Message.Headers. It has no methods of its own.

Property Type Access What it holds
Count long read-only Number of headers
Item(index) MessageHeader read-only The header at that position
ItemByName(name) MessageHeader read-only The first header with that name

For a quick read or write of a single header, Message.HeaderValue('X-Whatever') is the shorter path. Use the collection when a header occurs more than once (Received being the classic case) or when you need to delete one. Either way, header changes take effect when you save the owning message.

Attachment

One attachment of one message.

Property Type Access What it holds
Filename string read-only The attachment’s file name
Size long read-only The attachment’s size
Method What it does
Delete() Strips this attachment out of the message
SaveAs(path) Writes the attachment to disk at the given path

Attachments

All attachments of one message, via Message.Attachments.

Property Type Access What it holds
Count long read-only Number of attachments
Item(index) Attachment read-only The attachment at that position
Method What it does
Add(path) Attaches the file at the given path to the message
Clear() Removes every attachment

Give SaveAs and Add full paths, and save the message after stripping or adding attachments — the change is not on disk until you do. From 6.3.6 both answer the server administrator alone, since one reads a file from the server’s disk and the other writes one: a program signed in as a domain administrator or as an account is refused them.

Recipient

One recipient, as recorded in the SMTP envelope — not the To header. Envelope recipients include people the headers never mention, BCC recipients above all.

Property Type Access What it holds
Address string read-only The recipient’s address
IsLocalUser bool read-only Whether the address belongs to an account on this server
OriginalAddress string read-only The address as first given, before any rewriting (alias resolution, for instance)

Recipients

The envelope recipient list of one message, via Message.Recipients. It is read-only and has no methods — to change the recipient list, use Message.AddRecipient and Message.ClearRecipients.

Property Type Access What it holds
Count long read-only Number of recipients
Item(index) Recipient read-only The recipient at that position

30.3 IMAP folders and permissions

IMAPFolder

One IMAP folder — a user’s Inbox, a sub-folder three levels deep, or a public folder.

Property Type Access What it holds
CreationTime string read-only When the folder was created
CurrentUID long read-only The latest message UID issued in this folder
ID long read-only Database identifier — what Message.Copy() wants
Messages Messages read-only The messages in this folder
Name string read/write The folder’s name (one level, not a path)
ParentID long read-only Database ID of the parent folder
Permissions IMAPFolderPermissions read-only The folder’s ACL: on a public folder, and on an account’s own folder that its owner shares with other accounts (IMAP SETACL)
SubFolders IMAPFolders read-only The folders inside this one
Subscribed bool read/write The folder’s IMAP subscription state
Method What it does
Delete() Deletes the folder from the database
Save() Writes changes to the database

Folder names are single components. To reach Projects/2026/Invoices, chain through SubFolders:

$folder = $account.IMAPFolders.ItemByName('Projects').SubFolders.ItemByName('2026').SubFolders.ItemByName('Invoices')

IMAPFolders

The root-level folders of an account (Account.IMAPFolders) or the public folder tree (Settings.PublicFolders). Sub-folder collections returned by IMAPFolder.SubFolders behave identically.

Property Type Access What it holds
Count long read-only Number of folders at this level
Item(index) IMAPFolder read-only The folder at that position
ItemByDBID(id) IMAPFolder read-only The folder with that database ID
ItemByName(name) IMAPFolder read-only The folder with that name
Method What it does
Add(name) Creates a folder with that name — saved immediately
DeleteByDBID(id) Deletes the folder with that database ID

Add(name) is the odd one out: most Add() calls give you an unsaved object that needs Save(), but a folder added here is written to the database at once.

IMAPFolderPermission

One entry in a folder’s ACL: this account (or group, or anyone) may do these things in this folder. Chapter 13 covers what public folders are; this object is how you script who sees them. An account’s own folders carry entries too when their owner shares them, which an IMAP client does with SETACL: that is how a shared or delegated mailbox is made, and this object reads and writes those entries as well.

Property Type Access What it holds
Account Account read-only The account this entry refers to
Group Group read-only The group this entry refers to
ID long read-only Database identifier
Permission(right) bool read/write One right — pass a value from the table below
PermissionAccountID long read/write ID of the account being granted rights
PermissionGroupID long read/write ID of the group being granted rights
PermissionType eACLPermissionType read/write What kind of grantee the entry targets
ShareFolderID long read-only ID of the folder being shared
Value long read/write The whole set of rights as one bit-mask
Method What it does
Delete() Removes the entry from the database
Save() Writes changes to the database

The rights, and their bit values:

Value Right Allows
1 Lookup Seeing that the folder exists
2 Read Opening the folder and reading its messages
4 Keep seen flag Persisting read/unread state
8 Keep other flags Persisting flagged/answered/draft state
16 Insert Adding messages to the folder
32 Post Sending mail to the folder’s submission address (not enforced by IMAP itself). On another account’s INBOX it also lets the grantee send as that account’s address: in the webmail, and over SMTP while SmtpAuthenticatedSenderCheck is on
64 Create mailbox Creating sub-folders
128 Delete mailbox Deleting the folder
256 Keep deleted flag Marking messages as deleted
512 Expunge Permanently removing deleted messages
1024 Administer Changing the folder’s permissions

Set Value to the sum of the rights you want: insert plus the ability to mark deletions is 16 + 256 = 272; everything except deleting the folder itself is 1 + 2 + 4 + 8 + 16 + 32 + 64 + 256 + 512 + 1024 = 1919. Permission(right) reads or flips a single bit instead.

# Let alice see, read and add messages to a public folder
$folder = $app.Settings.PublicFolders.ItemByName('Noticeboard')
$perm   = $folder.Permissions.Add()
$perm.PermissionAccountID = $app.Domains.ItemByName('example.com').Accounts.ItemByAddress('[email protected]').ID
$perm.Value = 1 + 2 + 16          # lookup + read + insert
$perm.Save()

Entries are inherited, and never merged. A folder with no entries of its own takes them from the nearest folder above it that has any, and a folder with entries is decided by those alone. So the first entry you add to a sub-folder replaces the whole inherited set for it, and everyone the parent’s entries let in loses the sub-folder until they have entries there too. Grant Lookup (1) with anything else: without it the folder does not exist for that grantee.

IMAPFolderPermissions

The full ACL of one folder, public or shared, via IMAPFolder.Permissions.

Property Type Access What it holds
Count long read-only Number of entries
Item(index) IMAPFolderPermission read-only The entry at that position
ItemByDBID(id) IMAPFolderPermission read-only The entry with that database ID
ItemByName(name) IMAPFolderPermission read-only The entry with that name
Method What it does
Add() Adds a new, unsaved entry
Delete(index) Deletes the entry at that position
DeleteByDBID(id) Deletes the entry with that database ID
Refresh() Reloads the collection from the database

30.4 Rules

A rule, as chapter 12 puts it, is criteria plus actions. Through the API that is three objects: the Rule itself, its RuleCriterias, and its RuleActions.

Enum constants are just numbers over COM. Samples in the original hMailServer documentation used names like eRAForwardEmail, which work in VBA because VBA imports the type library. PowerShell and VBScript do not — use the numeric values from the tables below, and comment the name next to the number so the script stays readable.

Rule

One rule — server-wide (from Application.Rules), belonging to one domain (from Domain.Rules, since 6.3.4) or belonging to one account (from Account.Rules). Rules run in priority order, top first; MoveUp and MoveDown reorder them. A matching rule does not stop the set: only a Stop rule processing action (type 6) does.

Property Type Access What it holds
AccountID long read/write The owning account, for account-level rules
Actions RuleActions read-only What the rule does when it matches
Active bool read/write Whether the rule runs
Criterias RuleCriterias read-only What the rule tests
ID long read-only Database identifier
Name string read/write The rule’s name
UseAND bool read/write true: every criterion must match; false: any one is enough
Method What it does
Delete() Deletes the rule from the database
MoveDown() Moves the rule down the priority order
MoveUp() Moves the rule up the priority order
Save() Writes changes to the database

Rules

A rule collection — Application.Rules for the server-wide set, Domain.Rules for one domain’s set (since 6.3.4), Account.Rules for one mailbox’s set.

Property Type Access What it holds
Count long read-only Number of rules
Item(index) Rule read-only The rule at that position (priority order)
ItemByDBID(id) Rule read-only The rule with that database ID
Method What it does
Add() Adds a new, unsaved rule
DeleteByDBID(id) Deletes the rule with that database ID
Refresh() Reloads the collection from the database

RuleAction

One thing a rule does when it matches. Type picks the action; one or more of the other properties carry its payload.

Property Type Access What it holds
AbortSpamFlagged bool read/write Skip this action when the message is spam-flagged — stops replies and forwards going out for spam
Body string read/write Reply text, for reply actions; the disclaimer’s text, for disclaimer actions (6.3.4)
Filename string read/write Unused
FromAddress string read/write Reply sender address, for reply actions
FromName string read/write Reply sender name, for reply actions
HeaderName string read/write Header to set, for set-header actions
ID long read-only Database identifier
IMAPFolder string read/write Target folder name, for move actions
RouteID long read/write Route to deliver through, for route actions
RuleID long read-only The rule this action belongs to
ScriptFunction string read/write Function name to run, for script actions
Subject string read/write Reply subject, for reply actions
To string read/write Forwarding address, for forward actions
Type eRuleActionType read/write Which action — see the table below
Value string read/write Payload for the remaining action types
Method What it does
Delete() Deletes the action from the database
MoveDown() Moves the action down the execution order
MoveUp() Moves the action up the execution order
Save() Writes changes to the database

Action types:

Type Constant Does Payload
1 eRADeleteEmail Deletes the message —
2 eRAForwardEmail Forwards the message To
3 eRAReply Sends a reply FromName, FromAddress, Subject, Body
4 eRAMoveToImapFolder Moves the message into a folder IMAPFolder
5 eRARunScriptFunction Runs a function from your event scripts, passing the message object ScriptFunction
6 eRAStopRuleProcessing Stops any further rules from running —
7 eRASetHeaderValue Sets a header, replacing any existing one of the same name HeaderName, Value
8 eRASendUsingRoute Delivers through a specific route (chapter 14) RouteID
9 eRACreateCopy Creates an exact copy of the message, recipients included —
10 eRABindToAddress Binds outgoing SMTP for the message to a local IP address Value
11 eRAAddDisclaimer Appends the text to the plain and HTML parts, with X-hMailServer-Disclaimer naming the rule (6.3.4) Body
12 eRAReject Refuses the message with 550 5.7.1 and the text while the sender is still connected, before it is accepted; a message the rule meets only after acceptance is dropped, as a delete is. With no text, the answer names the rule (6.3.4) Value
13 eRARequireTLS Every external delivery of the message needs STARTTLS; a destination that offers none is deferred, and the message is not sent in the clear (6.3.4) —
14 eRAQuarantine Holds the message in the quarantine under Rule: <name>, for an administrator to release or delete (6.3.4) —
15 eRAWarnSender Lets the message go on and mails a sender this server hosts a notice naming the rule; an outside sender gets nothing (6.3.4) Value
0 eRAUnknown Nothing defined — never set this —

A Run function action names a plain function. Type 5 calls the named function of the event script with the message, and needs scripting switched on. Since 6.3.4 ScriptFunction must be ASCII letters, digits, _ and $, not starting with a digit. Any other name runs nothing: on Windows each match is reported as HM5018, and on Linux, where event scripts are JavaScript, the name finds no function. Up to 6.3.3 the name was written into the script’s call as it was, in VBScript and JScript alike, so whoever could edit a rule, a domain’s own administrator included, could run code in the server (CVE-2026-100741). On a server not yet on 6.3.4, let nobody but the server administrator make or edit a rule with this action.

More: Mail flow rules, the outbound content policy and what a domain does to a message on the forum.

RuleActions

All actions of one rule, via Rule.Actions. Actions run in order; MoveUp and MoveDown on the individual action reorder them.

Property Type Access What it holds
Count long read-only Number of actions
Item(index) RuleAction read-only The action at that position
ItemByDBID(id) RuleAction read-only The action with that database ID
Method What it does
Add() Adds a new, unsaved action
Delete(index) Deletes the action at that position
DeleteByDBID(id) Deletes the action with that database ID
Refresh() Reloads the collection from the database

One trap: in the type library, the parameter of Delete here (and on RuleCriterias) is named DBID — but the implementation treats it as a position. Pass an index to Delete; when what you have is a database ID, use DeleteByDBID.

RuleCriteria

One test in a rule. Either it tests a predefined field (UsePredefined = true, field chosen by PredefinedField) or a named header (UsePredefined = false, header named in HeaderField). MatchType and MatchValue say what counts as a match.

Property Type Access What it holds
HeaderField string read/write Header to test, when not using a predefined field
ID long read-only Database identifier
MatchType eRuleMatchType read/write How to compare — see below
MatchValue string read/write What to compare against (a regular expression for type 5)
PredefinedField eRulePredefinedField read/write Which built-in field to test — see below
RuleID long read-only The rule this criterion belongs to
UsePredefined bool read/write true: test PredefinedField; false: test HeaderField
Method What it does
Delete() Deletes the criterion from the database
Save() Writes changes to the database

Match types:

MatchType Constant Matches when
1 eMTEquals The whole field equals the value, ignoring case
2 eMTContains The field contains the value
3 eMTLessThan The field is less than the value, both read as decimal numbers (a comma as a decimal point); a field that is not a number reads as 0
4 eMTGreaterThan The field is greater than the value, both read the same way
5 eMTRegExMatch The whole field matches the regular expression (Perl syntax, case-sensitive). It is not a search: write .*invoice.* to find a word anywhere. A pattern that takes 250 ms or more is suspended for 300 seconds and matches nothing meanwhile
6 eMTNotContains The field does not contain the value; also true when the field is empty, as for a header the message does not have
7 eMTNotEquals The field differs from the value; also true when the field is empty
8 eMTWildcard The whole field matches the wildcard expression (* any run of characters, ? exactly one), ignoring case: write *invoice* to find a word anywhere. A broken pattern fails silently
0 eMTUnknown Never set this

Predefined fields:

PredefinedField Constant Tests
1 eFTFrom The From header
2 eFTTo The To header
3 eFTCC The CC header
4 eFTSubject The Subject header
5 eFTBody The plain-text body and the HTML body, joined into one string; not the attachments. A message encrypted end to end (S/MIME or PGP/MIME), judged from its body and not its label, has no body the server can read, and the condition does not match it whichever way it is phrased, does not contain included; a signed message, whose text is in the clear, is read as before (6.3.4)
6 eFTMessageSize The message size, in bytes, not the kilobytes of Message.Size: 25 MB is 26214400
7 eFTRecipientList Every envelope recipient — BCC recipients included
8 eFTDeliveryAttempts The number of delivery attempts, the current one included
9 eFTAttachmentName Every attachment’s file name, with ; between them (6.3.4)
10 eFTSensitiveContent The detectors that found something in the readable text: card_number, iban, account_number (6.3.4)
11 eFTAttachmentType Every attachment’s type, read from its first bytes rather than its name (6.3.4)
12 eFTExternalRecipient Every envelope recipient this server does not host; empty when every recipient is local (6.3.4)
13 eFTContent The readable text: the plain and HTML parts and every attachment whose bytes are text (6.3.4)
0 eFTUnknown Never set this

RuleCriterias

All criteria of one rule, via Rule.Criterias. Rule.UseAND decides whether they combine with AND or OR.

Property Type Access What it holds
Count long read-only Number of criteria
Item(index) RuleCriteria read-only The criterion at that position
ItemByDBID(id) RuleCriteria read-only The criterion with that database ID
Method What it does
Add() Adds a new, unsaved criterion
Delete(index) Deletes the criterion at that position
DeleteByDBID(id) Deletes the criterion with that database ID
Refresh() Reloads the collection from the database

30.5 Worked examples

All of the following are PowerShell, run on the Windows server itself — or on another machine where the installer’s Remote administration support component has registered the COM API (chapter 4), with DCOM permitted between the two machines. The preamble’s New-Object line is the local form; from another machine, replace it with one that names the server (here mail01):

$app = [Activator]::CreateInstance([Type]::GetTypeFromProgID('hMailServer.Application', 'mail01'))

The Control Panel administers remote servers over this same interface. hMailServer has been 64-bit only since 6.0.0, so use the ordinary 64-bit PowerShell; the old 5.x-era advice about running the 32-bit SysWOW64 PowerShell no longer applies. A Linux server has no COM: there the same jobs go through the REST API (chapter 24), the Control Deck and hmctl.

Every example except Sending a message starts from this preamble, shown once:

$app = New-Object -ComObject 'hMailServer.Application'
if (-not $app.Authenticate('Administrator', 'your-admin-password')) {
    throw 'Authentication failed'
}

These scripts change a live server the moment they run — there is no dry-run mode in the COM API. Read a script until you can say what it does, then run it against a test installation before production.

Creating an account

$domain  = $app.Domains.ItemByName('example.com')
$account = $domain.Accounts.Add()
$account.Address  = '[email protected]'
$account.Password = 'a-strong-password'
$account.Active   = $true
$account.MaxSize  = 1000            # MB, 0 = unlimited
$account.Save()

Changing a password

$account = $app.Domains.ItemByName('example.com').Accounts.ItemByAddress('[email protected]')
$account.Password = 'the-new-password'
$account.Save()

The server hashes the password on save (chapter 17 covers the algorithms), so Password is effectively write-only — do not expect to read a password back. Since 6.3.4 a password set this way can also be checked against published data breaches, when that check is turned on (it is off by default): a breached password is refused with a COM error carrying the reason.

Sending a message

The one object you create directly. No Authenticate is needed: the message is injected straight into the local server’s delivery queue.

$msg = New-Object -ComObject 'hMailServer.Message'
$msg.From        = 'Alice Example <[email protected]>'
$msg.FromAddress = '[email protected]'
$msg.AddRecipient('Bob', '[email protected]')
$msg.Subject     = 'Scripted message'
$msg.Body        = 'Sent through the COM API.'
$msg.Save()                          # queued for delivery immediately

Set both From (the header, with display name) and FromAddress (the envelope sender) — mail with an empty envelope sender is treated as a bounce by receiving servers.

Copying messages into an IMAP folder

Copies everything in Alice’s Inbox into a Backup folder, creating it if needed. Copy() takes the destination folder’s database ID, not its name.

$account = $app.Domains.ItemByName('example.com').Accounts.ItemByAddress('[email protected]')
$folders = $account.IMAPFolders

$inbox = $folders.ItemByName('Inbox')
try   { $backup = $folders.ItemByName('Backup') }
catch { $backup = $folders.Add('Backup') }       # Add() saves immediately

$messages = $inbox.Messages
for ($i = 0; $i -lt $messages.Count; $i++) {
    $messages.Item($i).Copy($backup.ID) | Out-Null
}

Listing a user’s folders

Folder trees are recursive, so listing one is too:

function Show-FolderTree($folders, $indent = 0) {
    for ($i = 0; $i -lt $folders.Count; $i++) {
        $folder = $folders.Item($i)
        (' ' * $indent) + $folder.Name
        Show-FolderTree $folder.SubFolders ($indent + 3)
    }
}

$account = $app.Domains.ItemByName('example.com').Accounts.ItemByAddress('[email protected]')
Show-FolderTree $account.IMAPFolders

Enabling a domain

$domain = $app.Domains.ItemByName('example.com')
$domain.Active = $true
$domain.Save()

Setting $false and saving disables it again — the same switch as the box on the General tab of the domain’s Properties, under Accounts & domains → Domains in the Control Panel.

Deleting an account

Deletion goes through the collection and needs the account’s database ID, so fetch the account first:

$accounts = $app.Domains.ItemByName('example.com').Accounts
$account  = $accounts.ItemByAddress('[email protected]')
$accounts.DeleteByDBID($account.ID)

Deleting an account deletes its mailbox contents with it, and Recover deleted items, new in 6.3.4, keeps nothing of an account being deleted. If there is any chance the mail matters, take a backup first (chapter 15) or copy the folders elsewhere using the example above. Since 6.3.4 an account under an eDiscovery legal hold, or in a domain under one, cannot be deleted: the call is refused with a sentence saying why.

Building a rule from code

Everything from §30.4 in one piece: an account rule that forwards every message — any message larger than 0 bytes — to an archive address.

$account = $app.Domains.ItemByName('example.com').Accounts.ItemByAddress('[email protected]')

$rule = $account.Rules.Add()
$rule.Name   = 'Archive everything'
$rule.Active = $true
$rule.UseAND = $true
$rule.Save()                         # first: criteria and actions need its ID

$crit = $rule.Criterias.Add()
$crit.UsePredefined   = $true
$crit.PredefinedField = 6            # eFTMessageSize
$crit.MatchType       = 4            # eMTGreaterThan
$crit.MatchValue      = '0'
$crit.Save()

$action = $rule.Actions.Add()
$action.Type = 2                     # eRAForwardEmail
$action.To   = '[email protected]'
$action.Save()

Swap $account.Rules for $app.Rules and the same code creates a server-wide rule instead — the objects are identical either way. Since 6.3.4 a domain has rules of its own as well: swap in $app.Domains.ItemByName('example.com').Rules and the rule runs, after the server’s rules, on that domain’s mail: on the whole message when the domain sent it (its own account submitted it, or DMARC passed for it) or every recipient is its own, and otherwise on its own recipients only (chapter 12).

More: The COM API on Windows: object model, Save() since 6.3.4, roles and the Import Tool on the forum.


31. COM API reference III: security, filtering and operations

This chapter covers the objects you script when you operate the server rather than administer its mailboxes: anti-spam and anti-virus, IP ranges and ports, certificates, relays and routes, backup, the delivery queue, logging, status counters and the utility functions. Between them they bring most of what chapters 9–11 and 14–17 cover within reach of automation.

Every example assumes the connection boilerplate from chapter 23:

$app = New-Object -ComObject 'hMailServer.Application'
$app.Authenticate('Administrator', 'your-admin-password') | Out-Null

Check your return values. Several of the methods covered by the 6.2.10 authorization fix (chapter 29) live on the objects in this chapter — the Cache getters among them. A refused call fails properly: in PowerShell it throws; in VBScript it raises an error you can inspect with Err. Wrap operational scripts in try/catch (or On Error handling) rather than ignoring failures. Since 6.3.4 a refusal also says why, in the server’s own sentence, where up to 6.3.3 seventy-two classes answered one with a bare HRESULT (chapter 23).

Where each object lives

Object Reached from
AntiSpam Application.Settings.AntiSpam
DNSBlackLists Application.Settings.AntiSpam.DNSBlackLists
SURBLServers Application.Settings.AntiSpam.SURBLServers
WhiteListAddresses Application.Settings.AntiSpam.WhiteListAddresses
GreyListingWhiteAddresses Application.Settings.AntiSpam.GreyListingWhiteAddresses
GreyListingBypassEntries Application.Settings.AntiSpam.GreyListingBypassEntries (new in 6.3.4)
SenderListEntries Application.Settings.AntiSpam.SenderList; a domain’s own is Domain.SenderList (new in 6.3.4)
BlockedSenders Application.Settings.AntiSpam.BlockedSenders
Quarantine Application.Settings.AntiSpam.Quarantine
AntiVirus Application.Settings.AntiVirus
BlockedAttachments Application.Settings.AntiVirus.BlockedAttachments
SecurityRanges Application.Settings.SecurityRanges
TCPIPPorts Application.Settings.TCPIPPorts
SSLCertificates Application.Settings.SSLCertificates
IncomingRelays Application.Settings.IncomingRelays
Routes Application.Settings.Routes
RouteAddresses Route.Addresses
RemoteDomainPolicies Application.Settings.RemoteDomainPolicies (new in 6.3.4; chapter 14)
BackupSettings Application.Settings.Backup
BackupManager Application.BackupManager
Backup Returned by BackupManager.LoadBackup()
Database Application.Database
DeliveryQueue Application.GlobalObjects.DeliveryQueue
Directories Application.Settings.Directories
EventLog Global object inside event scripts
Logging Application.Settings.Logging
Status Application.Status
Utilities Application.Utilities
Diagnostics Application.Diagnostics (new in 6.x)
Scripting Application.Settings.Scripting
ServerMessages Application.Settings.ServerMessages
Client Passed as a parameter to event handlers
Cache Application.Settings.Cache
MessageIndexing Application.Settings.MessageIndexing (new in 6.x)
Languages Application.GlobalObjects.Languages

Two kinds of object

The objects here fall into two families, and knowing which is which saves confusion:

  • Settings objects — AntiSpam, AntiVirus, Logging, Cache, BackupSettings, Directories, Scripting. Assigning a property writes it immediately; there is no Save() to call.
  • Row objects — anything with an ID property: a DNSBlackList, a SecurityRange, a Route, and so on. Each represents one database row. Changes take effect only when you call Save(), and Delete() removes the row. Since 6.3.4 that holds for the items the running server reads as well (routes and their addresses, incoming relays, DNS blacklists, SURBL servers, remote domain policies, server messages, blocked attachments, SSL certificates and groups): up to 6.3.3 a property set on one of them changed the running server at once, saved or not. An object read before a Save() keeps what it read, so read the item again from its collection to see the saved values (chapter 23).

Row objects come in collections, and the collections share a standard shape:

Member Kind Does
Count property (read-only) Number of members
Item(index) property (read-only) The member at a zero-based position
ItemByDBID(dbid) property (read-only) The member with a given database identifier
Add() method Creates a new, unsaved member — set its properties, then call its Save()
DeleteByDBID(dbid) method Deletes the member with a given database identifier
Refresh() method Re-reads the collection from the database

The tables below list every member per object; where a collection matches the standard shape exactly, that is stated rather than repeated.

Types are those of the COM type library: long, bool, string, Date/Time, and a handful of enumerations whose numeric values are given where they matter.


AntiSpam

The server-wide anti-spam configuration — the object behind Spam & virus filtering → Anti-spam settings (chapter 10). Every enabled test adds to a message’s score; the two thresholds decide whether the total gets the message tagged or refused.

Methods

Method Does
ClearGreyListingTriplets() Empties the greylisting triplet table, so every sender is treated as new again
DKIMVerify(file) Verifies the DKIM signature of a message file on disk and returns the result as an eDKIMResult
TestSpamAssassinConnection(host, port) Tests the connection to a SpamAssassin spamd, returning success plus a result text — what the Control Panel’s test button calls

Properties

Property Type Description
AddHeaderReason bool Add an X-hMailServer-Reason-1, -2 and so on header for each failing test, and X-hMailServer-Reason-Score with their sum (on as shipped; the per-account spam overrides act only while it is on)
AddHeaderSpam bool Add X-hMailServer-Spam: YES to messages at or above the mark threshold (on as shipped)
BypassGreylistingOnMailFromMX bool Skip greylisting when the sending IP is one of the sender domain’s MX hosts
BypassGreylistingOnSPFSuccess bool Skip greylisting when the sending IP passes the sender domain’s SPF record (on as shipped; it needs UseSPF on, and it trusts every SPF pass, where GreyListingBypassEntries, below, names partners)
CheckHostInHelo bool Test whether the HELO/EHLO host name matches the connecting host
CheckHostInHeloScore long Score added when the HELO check fails
CheckPTR bool Test that the connecting IP has a reverse-DNS (PTR) record whose forward lookup comes back to that IP
CheckPTRScore long Score added when the PTR check fails
DKIMVerificationEnabled bool Verify DKIM signatures on incoming messages
DKIMVerificationFailureScore long Score added when DKIM verification fails
DMARCEnabled bool New in 6.x. Evaluate the sender domain’s DMARC policy (chapter 10)
DMARCFailureScore long New in 6.x. Score added when DMARC evaluation fails
DNSBlackLists DNSBlackLists The DNSBL collection (read-only)
GreyListingEnabled bool Turn greylisting on
GreyListingInitialDelay long Minutes a new sender triplet is temporarily rejected
GreyListingInitialDelete long Hours after which a triplet that never retried is dropped (24 as shipped; the Control Panel shows this value in days)
GreyListingFinalDelete long Hours after which an established triplet is dropped, counted from its last accepted message (864, that is 36 days, as shipped; the Control Panel shows it in days)
GreyListingWhiteAddresses GreyListingWhiteAddresses Addresses exempt from greylisting (read-only)
MaximumMessageSize long Largest message the tests made after the body (SURBL, DKIM, ARC, DMARC, SpamAssassin and the filter hook) will scan, in KB (1024 as shipped); a larger message skips them, and the tests made before the body still run
PrependSubject bool Prefix the subject of tagged messages
PrependSubjectText string The prefix to use, e.g. [SPAM]
SpamAssassinEnabled bool Hand messages to a SpamAssassin spamd for content analysis
SpamAssassinHost string Host spamd listens on
SpamAssassinPort long Port spamd listens on (conventionally 783)
SpamAssassinMergeScore bool Use SpamAssassin’s own score instead of the fixed one
SpamAssassinScore long Fixed score added when SpamAssassin flags a message
SpamDeleteThreshold long Total score at or above which the message is refused during the SMTP conversation, or held in the quarantine when quarantine is on and the verdict came after the body (20 as shipped; 0 never refuses)
SpamMarkThreshold long Total score at or above which the message is tagged
SURBLServers SURBLServers The SURBL collection (read-only)
TarpitCount long The recipient tarpit (since 6.2.25): how many recipients an unauthenticated SMTP session may name before each further RCPT TO waits; 0, as shipped, is off. It is the [Settings] key SmtpTarpitCount, and a save applies it
TarpitDelay long Seconds each RCPT TO past TarpitCount waits for its reply, at most 30; 0, as shipped, is off (SmtpTarpitDelaySeconds). Authenticated sessions and IP ranges with spam protection off are exempt
UseMXChecks bool Test that the sender’s domain can actually receive mail
UseMXChecksScore long Score added when the MX check fails
UseSPF bool Evaluate the sender’s SPF record (to RFC 7208 since 6.3.4)
UseSPFScore long Score added on an SPF hard Fail only; since 6.3.4 SoftFail, TempError and PermError can carry scores of their own (below)
WhiteListAddresses WhiteListAddresses Senders exempt from spam checking entirely (read-only)

Also on AntiSpam, and not in the table. BlockedSenders, the Blocked senders list (an address or domain, a score, 100 on a new entry, and a description); Quarantine, the messages held on Spam & virus filtering → Quarantine (Refresh(), Count, ReleaseByDBID, DeleteByDBID, DeleteExpired()); and ArcFilteringEnabled with ArcTrustedSealers, inbound ARC, off as shipped and doing nothing while the sealer list is empty. Added in 6.3.4, with every switch among them off as shipped: SenderList, the server’s allow and block list, with SendQuarantineDigests() and SendQuarantineDigestsAtHour(hour); for greylisting, GreyListingBypassEntries (below), GreyListingSlideOnEarlyRetry, GreyListingAbuseThreshold and GreyListingAbuseLockMinutes; and the SMTP policy a score cannot express: UseSPFSoftFailScore, UseSPFTempErrorScore, UseSPFPermErrorScore, SPFTempErrorDefer, RejectMultiRcptNullSender, BanMultiRcptNullSender and BanMultiRcptNullSenderHours, with the domain white and black list as DomainPolicyList (a JSON document), AddDomainPolicyListEntry(pattern, matchOn, action, note) and RemoveDomainPolicyListEntry(id). Chapter 10 says what each does.

The tag-first advice from chapter 10 applies to scripts too: before raising scores or lowering thresholds programmatically, run with deletion disabled (SpamDeleteThreshold of 0) and read what gets tagged.

DNSBlackList and DNSBlackLists

One DNSBlackList is one IP blocklist queried for every connecting server — the rows on Spam & virus filtering → DNS blacklists. DNSBlackLists is their collection.

DNSBlackList properties (plus Save() and Delete())

Property Type Description
Active bool The list is queried
DNSHost string The DNSBL zone to query, e.g. zen.spamhaus.org
ExpectedResult string The answer(s) that mean “listed”: | separates alternatives, a range on the last octet such as 127.0.0.2-8 covers .2 to .8, and * is a wildcard
ID long Database identifier (read-only)
RejectMessage string SMTP rejection text shown to a listed sender
Score long Score added when the connecting IP is listed

DNSBlackLists follows the standard collection shape, plus:

Member Does
ItemByDNSHost(host) Returns the list with a given DNS zone (read-only)

A new installation already has an inactive zen.spamhaus.org row (expected result 127.0.0.2-8|127.0.0.10-11, score 3). To use it, tick Active on it, or from a script fetch it with ItemByDNSHost('zen.spamhaus.org'), set Active and call Save(), rather than adding a second row. On a server that has no such row, add the workhorse list from chapter 10:

$dnsbl = $app.Settings.AntiSpam.DNSBlackLists.Add()
$dnsbl.Active         = $true
$dnsbl.DNSHost        = 'zen.spamhaus.org'
$dnsbl.ExpectedResult = '127.0.0.2|127.0.0.3|127.0.0.4|127.0.0.9|127.0.0.10|127.0.0.11'
$dnsbl.Score          = 5
$dnsbl.RejectMessage  = 'Rejected: sending IP is listed by Spamhaus ZEN.'
$dnsbl.Save()

SURBLServer and SURBLServers

A SURBLServer is a URL blocklist checked against the links inside message bodies rather than the connecting IP (Spam & virus filtering → SURBL servers). The members mirror DNSBlackList minus the expected-result field.

SURBLServer properties (plus Save() and Delete())

Property Type Description
Active bool The list is queried
DNSHost string The SURBL zone to query, e.g. multi.surbl.org
ID long Database identifier (read-only)
RejectMessage string SMTP rejection text used when the message is refused
Score long Score added when a linked domain is listed

SURBLServers follows the standard collection shape, plus ItemByDNSHost(host) (read-only).

WhiteListAddress and WhiteListAddresses

The global spam white list (Spam & virus filtering → White list). A message that matches an entry skips spam checking altogether. An entry matches on two axes at once: the sender address must match EmailAddress, and the sending IP must fall inside the LowerIPAddress–UpperIPAddress span. To whitelist a sender regardless of where they send from, use the span 0.0.0.0 to 255.255.255.255.

WhiteListAddress properties (plus Save() and Delete())

Property Type Description
Description string Free-text note about the entry
EmailAddress string The sender address to exempt; it may be a wildcard, and empty or * means any sender in the IP span
ID long Database identifier (read-only)
LowerIPAddress string Bottom of the sending-IP span
UpperIPAddress string Top of the sending-IP span

WhiteListAddresses follows the standard collection shape, plus:

Member Does
Clear() Deletes every entry in the white list

GreyListingWhiteAddress and GreyListingWhiteAddresses

The narrower of the two white lists (§10.4): an IP address here is exempt from greylisting only and still runs the full battery of spam tests. Use it for senders whose first message must not be delayed.

GreyListingWhiteAddress properties (plus Save() and Delete())

Property Type Description
Description string Free-text note about the entry
ID long Database identifier (read-only)
IPAddress string The sending IP to exempt from greylisting

GreyListingWhiteAddresses follows the standard collection shape, plus ItemByName(ipAddress) (read-only), which looks an entry up by its IP address.

New in 6.3.4: GreyListingBypassEntries. Application.Settings.AntiSpam.GreyListingBypassEntries is the list on Spam & virus filtering → Greylisting bypass list: a domain, or a suffix with a leading dot, let past greylisting only when the message passes the SPF, DKIM or DMARC check the entry requires, for that name. A GreyListingBypassEntry has ID, Pattern, MatchOn (1 to 3, which says whether the pattern is compared with the envelope sender’s domain, the HELO host or either), RequireSPF, RequireDKIM and RequireDMARC (at least one must be set), Note, Save() and Delete(). The collection follows the standard shape, plus ItemByName. Like the greylisting white list it skips greylisting only; every spam test still runs.


AntiVirus

The virus-scanning configuration behind Spam & virus filtering → Anti-virus settings (chapter 11). Three scanner hookups are available, and all three can be on at once; ClamAV over TCP is the one to use where you can — ClamWin and the custom scanner start a process for every file scanned (the whole message, then each attachment), which is slow. The custom scanner is also how a command-line engine such as Microsoft Defender is used, and the Control Panel has presets for several.

Methods

Method Does
TestClamAVScanner(host, port) Tests the connection to a clamd daemon, returning success plus a result text
TestClamWinScanner(executable, dbFolder) Tests the ClamWin hookup the same way
TestCustomerScanner(executable, virusReturnCode) Tests the custom-scanner hookup (the misspelt name is the type library’s, kept as-is)

Properties

Property Type Description
Action eAntivirusAction What to do with an infected message: 0, as shipped, deletes it for every recipient; 1 (Delete infected attachments only) rebuilds the message from its first text part, so an HTML version, inline images and every clean attachment are discarded along with the infected one
BlockedAttachments BlockedAttachments The blocked-extension collection (read-only)
ClamAVEnabled bool Scan with a ClamAV clamd daemon over TCP
ClamAVHost string Host name or IP where clamd listens
ClamAVPort long clamd port (conventionally 3310)
ClamWinEnabled bool Scan by running the ClamWin scanner executable per message
ClamWinExecutable string Path to the ClamWin scanner
ClamWinDBFolder string Path to the ClamWin signature database
CustomScannerEnabled bool Scan with an arbitrary command-line scanner
CustomScannerExecutable string The command line to run against each file; %FILE% marks where the file’s path goes (without it, the path is appended). Since 6.3.4 a bare %FILE% is quoted for you: up to 6.3.3 a path with a space in it, as under C:\Program Files, was cut at the space, nothing was scanned and the message was delivered as clean
CustomScannerReturnValue long Exit code the custom scanner uses to mean “infected”
EnableAttachmentBlocking bool Turn extension blocking on
MaximumMessageSize long Largest message passed to the scanners, in KB; 0, as shipped, is no limit. A larger message is delivered unscanned with no log line, so a generous-looking limit is a hole
NotifyReceiver bool Tell the intended recipient that a message was removed
NotifySender bool Tell the apparent sender that a message was removed

Leave NotifySender off. Virus senders are forged, so the notification lands on an innocent third party — that is backscatter, and chapter 11 explains why it gets you blacklisted.

BlockedAttachment and BlockedAttachments

The attachment block list (Spam & virus filtering → Blocked attachments). Each entry is one file-name wildcard, matched without regard to case against an attachment’s declared name, independent of virus scanning. The list acts only while AntiVirus.EnableAttachmentBlocking is on, and it is off as shipped. A matching attachment is not refused: .txt is appended to its name and its content is replaced by the ATTACHMENT_REMOVED server message, so the message is still delivered.

BlockedAttachment properties (plus Save() and Delete())

Property Type Description
Description string Free-text note, e.g. “Executable”
ID long Database identifier (read-only)
Wildcard string The pattern to block, e.g. *.exe

BlockedAttachments follows the standard collection shape exactly.


SecurityRange and SecurityRanges

A SecurityRange is one IP range — the rows on Sign-in & access → IP ranges, and the single most security-sensitive collection in the API (§17.1). When a client connects, the server finds every range containing the client’s IP and applies the one with the highest Priority. The range then answers three questions: which protocols may this client use, which mail flows will we accept from it, and which of those flows demand authentication.

The option flags. The delivery flags form a grid. “Local” and “remote” refer to the sender and recipient addresses, not to IP addresses: an address is local when its domain is hosted on this server (or belongs to a route marked as local — see TreatSenderAsLocalDomain and TreatRecipientAsLocalDomain under Route), and remote otherwise. A colleague on the office LAN sending to Gmail is a local-to-remote delivery; a stranger on the internet sending to one of your mailboxes is remote-to-local.

Flow Accept it? Demand authentication? On an internet-facing range
Local → local AllowDeliveryFromLocalToLocal RequireSMTPAuthLocalToLocal Allow, require auth — otherwise anyone can forge internal mail
Local → remote AllowDeliveryFromLocalToRemote RequireSMTPAuthLocalToExternal Allow, require auth — this is your users relaying outbound
Remote → local AllowDeliveryFromRemoteToLocal RequireSMTPAuthExternalToLocal Allow, no auth — this is inbound mail from the world
Remote → remote AllowDeliveryFromRemoteToRemote RequireSMTPAuthExternalToExternal Deny — allowing it unauthenticated is an open relay

(The two halves of the grid use different vocabulary — Remote in the allow flags, External in the auth flags. They mean the same thing.)

Methods: Save() and Delete().

Properties

Property Type Description
AllowDeliveryFromLocalToLocal bool Accept mail from a local sender to a local recipient
AllowDeliveryFromLocalToRemote bool Accept mail from a local sender to a remote recipient
AllowDeliveryFromRemoteToLocal bool Accept mail from a remote sender to a local recipient
AllowDeliveryFromRemoteToRemote bool Accept mail from a remote sender to a remote recipient
AllowSMTPConnections bool Clients in this range may connect to SMTP
AllowIMAPConnections bool Clients in this range may connect to IMAP
AllowPOP3Connections bool Clients in this range may connect to POP3
EnableAntiVirus bool Run virus scanning on mail from this range
EnableSpamProtection bool Run spam tests on mail from this range
Expires bool The range is temporary and will be removed — how auto-ban entries work
ExpiresTime Date/Time When a temporary range lapses
ID long Database identifier (read-only)
IsForwardingRelay bool Obsolete — use the IncomingRelays collection instead
LowerIP string Bottom of the range
Name string Display name, e.g. “Internet” or “Office LAN”
Priority long Higher wins when ranges overlap
RequireAuthForDeliveryToLocal bool Obsolete — use the RequireSMTPAuth* flags instead
RequireAuthForDeliveryToRemote bool Obsolete — use the RequireSMTPAuth* flags instead
RequireSMTPAuthLocalToLocal bool Demand authentication for local-to-local deliveries
RequireSMTPAuthLocalToExternal bool Demand authentication for local-to-remote deliveries
RequireSMTPAuthExternalToLocal bool Demand authentication for remote-to-local deliveries
RequireSMTPAuthExternalToExternal bool Demand authentication for remote-to-remote deliveries
RequireSSLTLSForAuth bool Refuse authentication on unencrypted connections from this range: AUTH is not even offered before TLS, on SMTP, POP3, IMAP and ManageSieve. A new 6.3.4 installation sets it on the Internet range; an upgraded server keeps what it had
UpperIP string Top of the range

Trusting an upstream forwarder — so spam tests judge the originating IP from the Received headers rather than your own front-end filter — is not an IP-range option. That is what Mail flow & delivery → Incoming relays and the IncomingRelays collection are for (§14.3, below).

SecurityRanges follows the standard collection shape, plus:

Member Does
Delete(index) Deletes the range at a zero-based position
ItemByName(name) Returns the range with a given name (read-only)
SetDefault() Discards the current ranges and writes the two classic ones, Internet and My computer, as the create scripts make them. Neither requires TLS for authentication, so on a new 6.3.4 installation, whose Internet range does, set RequireSSLTLSForAuth on it again afterwards

Adding a range for the office network:

$range = $app.Settings.SecurityRanges.Add()
$range.Name     = 'Office LAN'
$range.LowerIP  = '192.168.1.0'
$range.UpperIP  = '192.168.1.255'
$range.Priority = 25
$range.AllowSMTPConnections = $true
$range.AllowIMAPConnections = $true
$range.AllowPOP3Connections = $true
$range.AllowDeliveryFromLocalToLocal   = $true
$range.AllowDeliveryFromLocalToRemote  = $true
$range.AllowDeliveryFromRemoteToLocal  = $true
$range.AllowDeliveryFromRemoteToRemote = $false
$range.RequireSMTPAuthLocalToLocal     = $true
$range.RequireSMTPAuthLocalToExternal  = $true
$range.RequireSSLTLSForAuth            = $true
$range.Save()

Auto-ban (§17.1) blocks an abusive IP by inserting a range with Expires set, at priority 100 and named Auto-ban: <user>. Since 6.3.4 a spam-trap hit and a banned multi-recipient null sender are recorded the same way, so the loop below lifts those too; with AutoBanFirewall on, the firewall is brought into line when a range is deleted. To lift every current ban:

$ranges = $app.Settings.SecurityRanges
for ($i = $ranges.Count - 1; $i -ge 0; $i--) {
   $r = $ranges.Item($i)
   if ($r.Expires) { $ranges.DeleteByDBID($r.ID) }
}

A careless range is how servers become open relays. After any scripted change, confirm that no internet-facing range allows remote-to-remote delivery without authentication.

TCPIPPort and TCPIPPorts

A TCPIPPort is one listener — an address, a port, a protocol and its TLS behaviour (Connections & protocols → TCP/IP ports, chapter 22). Listeners are bound when the servers start, so a change takes effect at the next reinitialise (Application.Reinitialize, or POST /api/v1/server/reinitialize) or service restart.

TCPIPPort properties (plus Save() and Delete())

Property Type Description
Address string The local address to bind — 0.0.0.0 for all interfaces
ClientCertificateCAFile string The CA bundle (a file of the certificate authorities) that a client certificate on this port must chain to — the only trust anchor for them, not the Windows certificate store
ClientCertificatePolicy long Mutual TLS on this port: 0 off (the default), 1 request (verified if offered, never fails the handshake), 2 require (the handshake fails without a certificate that chains to ClientCertificateCAFile, and with no loadable bundle the listener does not start). Refused on a port with no TLS
ConnectionSecurity eConnectionSecurity How TLS is applied on this port (see below)
ID long Database identifier (read-only)
PortNumber long The TCP port
Protocol eSessionType Which protocol answers on this port (see below)
SSLCertificateID long ID of the certificate in SSLCertificates used on this port
UseSSL bool Older on/off TLS switch, superseded by ConnectionSecurity; kept for script compatibility

eConnectionSecurity

Value Meaning
0 None — plaintext
1 SSL/TLS — encrypted from the first byte (implicit TLS, as on 993/995/465)
2 STARTTLS optional — starts plaintext, upgrade offered
3 STARTTLS required — commands refused until the session upgrades

eSessionType

Value Protocol
1 SMTP
3 POP3
5 IMAP

The gaps are the server’s own client sessions, 2 for its SMTP client and 4 for its POP3 client, which no listener takes; the same values are used by Status.SessionCount.

The “SSL” in these names is historical. Whatever a property is called, the versions actually negotiated are those configured on the Protocol versions tab of Certificates & encryption → SSL/TLS — TLS 1.2/1.3 by default, and chapter 9’s advice stands: leave the older protocols off.

TCPIPPorts follows the standard collection shape, plus:

Member Does
SetDefault() Replaces the collection with the classic default listeners — SMTP on 25 and 587, POP3 on 110, IMAP on 143 — with no TLS and no certificate (the Restore defaults button of Connections & protocols → TCP/IP ports does the same). It does not recreate what a new 6.3.4 installation is given, STARTTLS optional with the self-signed certificate, and where the Internet IP range requires TLS for authentication nobody outside this computer can then sign in, so set each port’s connection security and certificate again afterwards

SSLCertificate and SSLCertificates

The certificate store (Certificates & encryption → SSL certificates, §9.2): each entry names a PEM certificate file and its private key, ready to be assigned to ports via TCPIPPort.SSLCertificateID. The built-in ACME client (§9.1) needs no entry from you: it creates and updates its own entry here and assigns it to the TLS ports that have no certificate and, since 6.3.4, to those still holding the self-signed certificate a new installation made (listed as Self-signed (made at installation)).

SSLCertificate properties (plus Save() and Delete())

Property Type Description
CertificateFile string Path to the PEM certificate, which must hold the leaf certificate followed by its issuer chain
ID long Database identifier (read-only)
Name string Display name
PrivateKeyFile string Path to the PEM private key
PrivateKeyPassword string The passphrase of an encrypted private key, per certificate — write-only; it cannot be read back, and without it the TLS ports using an encrypted key do not start

SSLCertificates follows the standard collection shape, plus:

Member Does
Clear() Deletes every certificate entry — TLS ports referencing them stop working, so treat with care

IncomingRelay and IncomingRelays

The trusted-forwarder list (Mail flow & delivery → Incoming relays, §14.3). When mail arrives from an IP inside one of these ranges — a front-end spam filter, say — hMailServer takes the originating IP from the Received headers instead, so spam tests judge the real sender rather than your own infrastructure.

IncomingRelay properties (plus Save() and Delete())

Property Type Description
ID long Database identifier (read-only)
LowerIP string Bottom of the trusted range
Name string Display name
UpperIP string Top of the trusted range

IncomingRelays follows the standard collection shape, plus:

Member Does
Delete(index) Deletes the relay at a zero-based position
ItemByName(name) Returns the relay with a given name (read-only)

Route and Routes

A Route overrides delivery for one domain (Mail flow & delivery → Routes, §14.2): mail for DomainName goes to TargetSMTPHost instead of wherever DNS points. The route can carry its own credentials, its own retry schedule, and its own idea of which recipient addresses it covers.

Methods

Method Does
Delete() Deletes the route
Save() Saves changes
SetRelayerAuthPassword(password) Sets the password used to authenticate to the target server — write-only; it cannot be read back

Properties

Property Type Description
Addresses RouteAddresses The recipient addresses this route covers, when AllAddresses is off (read-only)
AllAddresses bool Route every address in the domain, rather than only the listed ones
ConnectionSecurity eConnectionSecurity How TLS is applied on the connection to the target server — same values as under TCPIPPort
Description string Free-text note
DomainName string The domain whose mail is rerouted; wildcards such as *.partner.example are matched, and the first matching route in name order wins
ID long Database identifier (read-only)
MinutesBetweenTry long Retry interval for this route
NumberOfTries long Delivery attempts before giving up
RelayerAuthUsername string User name for authenticating to the target server
RelayerRequiresAuth bool The target server demands authentication
TargetSMTPHost string The server to hand the mail to
TargetSMTPPort long The port on the target server
TreatRecipientAsLocalDomain bool Count recipient addresses in this domain as local when IP-range rules are evaluated (see SecurityRange)
TreatSecurityAsLocalDomain bool Obsolete alias of TreatRecipientAsLocalDomain; kept for script compatibility
TreatSenderAsLocalDomain bool Count sender addresses in this domain as local when IP-range rules are evaluated
UseSSL bool Older on/off TLS switch, superseded by ConnectionSecurity; kept for script compatibility

Routes follows the standard collection shape, plus ItemByName(domainName) (read-only), which looks a route up by the domain it serves. The policies of Mail flow & delivery → Remote domains (one per remote domain or pattern), new in 6.3.4, are a separate collection, Settings.RemoteDomainPolicies.

Changed in 6.3.4. The running server reads routes, route addresses, incoming relays and SSL certificates while it works, so a property set on one of them is now written on your object’s own copy and is in effect only after Save(); up to 6.3.3 it changed the running server at once, and an unsaved change lasted until the next restart. An object read before a save keeps the values it read: read the route again (ItemByName, ItemByDBID) to see the saved ones, with its Addresses. Addresses added or deleted through Route.Addresses are in effect at once, as before, and a save whose re-read fails is reported as HM7390 and is in effect at the next reinitialise, restart or save of that collection.

RouteAddress and RouteAddresses

When a route does not cover the whole domain, RouteAddresses lists exactly which recipients it applies to. Each RouteAddress is one recipient address.

RouteAddress properties (plus Save() and Delete())

Property Type Description
Address string The recipient address
ID long Database identifier (read-only)
RouteID long The route the address belongs to

RouteAddresses members

Member Kind Does
Count property (read-only) Number of addresses
Item(index) property (read-only) The address at a zero-based position
ItemByDBID(dbid) property (read-only) The address with a given database identifier
Add() method Creates a new, unsaved address
DeleteByAddress(address) method Deletes an entry by its address text
DeleteByDBID(dbid) method Deletes an entry by database identifier

BackupSettings

What the built-in backup includes and where it goes — the object behind Maintenance → Backup & restore (chapter 15). Reached as Application.Settings.Backup; being a settings object, assignments persist immediately.

Properties

Property Type Description
BackupDomains bool Include domains, accounts, aliases and lists
BackupMessages bool Include the message files themselves
BackupSettings bool Include server-wide settings
CompressDestinationFiles bool Put the message files into each run’s archive. Off, they are copied to one DataBackup folder beside the archives that every run overwrites, so there is only ever one generation of mail; since 6.3.4 an encrypted backup of messages is refused without it
Destination string Directory the backup is written to. Since 6.3.4, while the service runs as NT SERVICE\hMailServer (the account a new installation is given), a folder outside the program folder’s own Data, Logs, Temp and Database must first be approved for the service, as chapter 15 says, or the backup cannot be written there
EncryptionPassphrase string New in 6.3.4. Write-only. Once set, every archive is encrypted as it is written (7-Zip AES-256, headers included); 12 to 256 printable ASCII characters, and an empty value goes back to unencrypted archives. It is never written into an archive or read back, and an archive whose passphrase is lost cannot be restored by anybody
EncryptionPassphraseSet bool New in 6.3.4. Whether a passphrase is set (read-only)
LogFile string Path of the backup log (read-only)

BackupMessages is the flag people forget. A backup without it contains your configuration but not your mail — the exact trap chapter 15 warns about. It also needs BackupDomains: messages without domains stage nothing and the run fails.

BackupManager

The object that runs backups and restores. StartBackup() takes what BackupSettings describes; LoadBackup() opens an existing backup file and hands you a Backup object to restore from.

Methods

Method Does
DescribeMailbox(file, address, passphrase) New in 6.3.4. One account’s folders and messages as an archive in the backup destination holds them
LoadBackup(file) Opens a backup archive (HMBackup date time.7z) and returns it as a Backup object. An archive that is truncated, not a backup or written by a newer version raises an error instead
LoadEncryptedBackup(file, passphrase) New in 6.3.4. The same, for an archive written with an encryption passphrase; a missing or wrong passphrase is refused before anything on the server is touched
MailboxRestoreStatus(job) New in 6.3.4. How a mailbox or message restore went: folders created, messages added, skipped and failed
RestoreMailbox(file, address, mode, passphrase) New in 6.3.4. Restores one account’s mailbox, and nothing else, from an archive in the backup destination taken with domains and messages: merge adds what the live mailbox lacks, replace empties its folders first, so mail that arrived since the backup is gone (kept only for RecoverDeletedDays, which is off by default)
RestoreMessage(file, address, id, folder, passphrase) New in 6.3.4. Restores one message of that mailbox, into the folder it was in or the one named
StartBackup() Starts a backup using the current BackupSettings

StartBackup() returns once the backup is under way; the outcome is written to the log file named by BackupSettings.LogFile. Since 6.2.25 Maintenance → Backup & restore can also run backups on a schedule of its own, with retention; a nightly scheduled task of your own reduces to:

$app = New-Object -ComObject 'hMailServer.Application'
$app.Authenticate('Administrator', 'your-admin-password') | Out-Null

$cfg = $app.Settings.Backup
$cfg.Destination    = 'D:\Backups\hMailServer'
$cfg.BackupDomains  = $true
$cfg.BackupMessages = $true
$cfg.BackupSettings = $true
$cfg.CompressDestinationFiles = $true   # off, every run overwrites the one copy of the mail
$app.BackupManager.StartBackup()

Backup

One loaded backup file. The Contains* flags report what is in the file; the Restore* flags choose what to bring back; StartRestore() does it.

Methods

Method Does
StartRestore() Starts restoring the selected parts of this backup

Properties

Property Type Description
ContainsDomains bool The file includes domain data (read-only)
ContainsMessages bool The file includes messages (read-only)
ContainsSettings bool The file includes server settings (read-only)
RestoreDomains bool Restore domain data
RestoreMessages bool Restore messages
RestoreSettings bool Restore server settings
$latest = Get-ChildItem 'D:\Backups\hMailServer' -Filter 'HMBackup *.7z' |
   Sort-Object LastWriteTime | Select-Object -Last 1

$backup = $app.BackupManager.LoadBackup($latest.FullName)
$backup.RestoreDomains  = $true
$backup.RestoreMessages = $true
$backup.RestoreSettings = $true
$backup.StartRestore()

A whole restore deletes every domain, account and public folder, and the whole message store, before it puts the archive’s back, with no transaction, and nothing can undo it, so rehearse it on a scratch server first. Chapter 15’s DPAPI caveat is about copying the database, not this restore: route, fetch-account and certificate-key passwords, and since 6.3.4 the second factors and the [Settings] secrets, travel in the archive in a portable form and are protected again on the new machine. The per-domain relay settings (host, port, user name, password and whether authentication is required) are in no archive and must be re-entered on the domain’s dialog, and no archive written by 6.3.4 or later carries PasswordPepper: if one was set, copy it into the new machine’s hMailServer.INI first, or no account whose password is an Argon2id or scrypt hash can sign in.

More: Backup and restore: what is in the archive, what is not, and restoring one mailbox on the forum.


Database

Information about the configured database, and a small toolkit for running SQL against it. It is also the object the installer and DBUpdater drive when creating and upgrading databases (chapter 18). Since 6.3.4, while the service runs as NT SERVICE\hMailServer (the account a new installation is given), CreateInternalDatabase, CreateExternalDatabase and SetDefaultDatabase are refused before anything is made, because they write [Database] into hMailServer.INI, which that account may not write. The installer does its database step while the service is still LocalSystem; to change the database later, move the service to LocalSystem for the change and back, or edit [Database] and restart the service.

Methods

Method Does
BeginTransaction() Opens a transaction — affects ExecuteSQL() calls only
CommitTransaction() Commits it
RollbackTransaction() Rolls it back
CreateExternalDatabase(type, server, port, name, user, password) Creates a new external database of the given type
CreateInternalDatabase() Creates the built-in database
EnsurePrerequisites(dbVersion) Ensures the prerequisites for a given schema version are fulfilled — a DBUpdater helper
ExecuteSQL(statement) Runs one SQL statement
ExecuteSQLScript(filename) Runs a file of SQL statements
ExecuteSQLWithReturn(statement) Runs one SQL statement and returns the unique ID it generated
SetDefaultDatabase(type, server, port, name, user, password) Points the server at a database
UtilGetFileNameByMessageID(messageID) Returns the on-disk path of a message’s .eml file, given its database ID. Since 6.3.4, with MessageStoreCompression on, that file can be a compressed container rather than the message as it arrived: read the message through the API instead

Properties (all read-only)

Property Type Description
CurrentVersion long Schema version of the connected database
DatabaseExists bool Whether the configured database exists
DatabaseName string Its name
DatabaseType eDBtype Which engine — the built-in database, MySQL/MariaDB, MS SQL or PostgreSQL
IsConnected bool Whether the server currently has a working connection
RequiredVersion long Schema version this build expects (6108 in 6.3.6 to 6.3.8; 6105 in 6.3.5, 6104 in 6.3.4, 6040 in 6.3.3, 6038 in 6.3.2, 6031 in 6.2.27–6.3.1, 6030 in 6.2.25–6.2.26, 6025 in 6.2.24, 6011 in 6.2.19–6.2.21, 6005 before that — chapter 18)
RequiresUpgrade bool The schema is older than this build expects
ServerName string The database server’s host name

ExecuteSQL bypasses everything. Writing to the database directly skips validation, business logic and the object cache — the server can serve stale data until the cache expires. If you must write raw SQL, call $app.Settings.Cache.Clear() afterwards, and prefer the proper COM objects whenever one exists.

DeliveryQueue

The outbound queue you see on Monitoring & troubleshooting → Delivery queue (§16.1). The message identifiers its methods take come from Status.UndeliveredMessages.

Methods

Method Does
Clear() Empties the queue — every queued message is discarded, not delivered
Remove(messageID) Removes one message from the queue
ResetDeliveryTime(messageID) Makes one message due for delivery immediately
StartDelivery() Wakes the deliverer to look for due messages now

The classic operation — “resend everything that is stuck, now”:

$queue = $app.GlobalObjects.DeliveryQueue

foreach ($line in $app.Status.UndeliveredMessages -split "`n") {
   if ($line.Trim()) {
      $fields = $line -split "`t"
      $queue.ResetDeliveryTime([long]$fields[0])
   }
}
$queue.StartDelivery()

Clear() deletes queued mail outright. If the queue is full of legitimate messages stuck behind a DNS or smart-host problem, fix the cause and resend — do not clear.

Directories

The directories the server is using, as configured in hMailServer.INI. The defaults are listed in §19.7. A changed directory takes effect when the service next starts. Since 6.3.4, while the service runs as NT SERVICE\hMailServer (the account a new installation is given), setting one over COM is refused with a sentence, because that account may not write hMailServer.INI: an administrator changes the file and approves the new folder for the service (hMailServer.exe --confirm /GrantServiceAccess from an elevated prompt, or Grant access on the Windows service account card of Maintenance → Server limits & expert settings).

Properties

Property Type Description
DatabaseDirectory string Where the built-in database lives
DataDirectory string Where messages are stored
DBScriptDirectory string Where the schema SQL scripts live (read-only)
EventDirectory string Where the event-script file lives
LogDirectory string Where logs are written
ProgramDirectory string The installation directory
TempDirectory string Scratch space for messages in transit

EventLog

A single-purpose object available to event scripts (chapter 23) as the global EventLog: it writes a line to the server’s event log, which lands in the file named by Logging.CurrentEventLog and in the live log view. Since 6.3.4 the JavaScript engine of the Linux builds has it too, written in full as EventLog.Write(text).

Methods

Method Does
Write(message) Appends a message to the event log
Sub OnClientConnect(oClient)
   EventLog.Write("Connection from " & oClient.IPAddress & " on port " & oClient.Port)
End Sub

Logging

The logging configuration behind Monitoring & troubleshooting → Logging (§16.2), plus the live log the Control Panel’s Monitoring & troubleshooting → Live logs page drains.

Methods

Method Does
EnableLiveLogging(enabled) Starts or stops capturing the live log buffer

Properties

Property Type Description
AWStatsEnabled bool Also write an AWStats-compatible log
CurrentAwstatsLog string Path of the current AWStats log (read-only)
CurrentDefaultLog string Path of the current main log (read-only)
CurrentErrorLog string Path of the current error log (read-only)
CurrentEventLog string Path of the current event log (read-only)
Device eLogDevice Where log output is sent
Directory string The log directory (read-only)
Enabled bool Master switch for logging
KeepFilesOpen bool Hold log files open between writes rather than reopening each time
LiveLog string Drains and returns the captured live-log output (read-only)
LiveLoggingEnabled bool Whether the live-log buffer is currently capturing (read-only)
LogApplication bool Log server events
LogDebug bool Log debug detail
LogFormat eLogOutputFormat On-disk log format
LogSMTP bool Log SMTP conversations
LogIMAP bool Log IMAP conversations
LogPOP3 bool Log POP3 conversations
LogTCPIP bool Log connection-level activity
MaskPasswordsInLog bool Obsolete — passwords are now always masked in the logs; kept for script compatibility
SyslogEnabled, SyslogHost, SyslogPort, SyslogTransport, SyslogFacility, SyslogMinimumSeverity, SyslogLogSMTP, SyslogLogPOP3, SyslogLogIMAP, SyslogLogApplication, SyslogLogTCPIP, SyslogLogDebug various New in 6.3.4. A copy of the log entries, as RFC 5424, to a syslog collector: off by default, port 514, transport 0 UDP (the default), 1 TCP or 2 TCP with TLS, facility 2 (mail), minimum severity 6 (informational; less severe entries are not sent), and one switch per category. Errors are always sent. An empty SyslogHost is not off: on Linux under systemd it selects the journal, and on Windows nothing is sent. Saved at once; the running sink takes them at the next reinitialise or service start

The chapter 16 warning applies from scripts as much as from the GUI: LogDebug is for diagnosis, not for leaving on. JSON log output is the [Settings] key JsonLogging (chapter 21), which since 6.3.4 is kept in the database and applies when saved: set it with $app.Settings.SetIniSetting('JsonLogging', '1'), not by editing hMailServer.INI, where a changed line is reported as HM5804 and, where the service may write the file, put back.

Status

Uptime and counters — the numbers behind Monitoring & troubleshooting → Server status and the Dashboard (chapter 16). The counters accumulate from service start and reset when it restarts; for long-term monitoring, scrape the Prometheus endpoint instead (§16.3).

Properties (all read-only)

Property Type Description
ProcessedMessages long Messages delivered since the service started
RemovedSpamMessages long Messages the spam tests deleted
RemovedViruses long Infected messages or attachments removed
SessionCount(sessionType) long Current sessions for a protocol — eSessionType: 1 SMTP, 3 POP3, 5 IMAP
StartTime string When the service started
ThreadID long The current thread number — of diagnostic interest only
UndeliveredMessages string The delivery queue as text — one message per line, tab-separated fields, the first field being the identifier DeliveryQueue methods take
$status = $app.Status
'Started:       {0}' -f $status.StartTime
'Processed:     {0}' -f $status.ProcessedMessages
'Spam removed:  {0}' -f $status.RemovedSpamMessages
'Viruses:       {0}' -f $status.RemovedViruses
'SMTP sessions: {0}' -f $status.SessionCount(1)
'IMAP sessions: {0}' -f $status.SessionCount(5)

Utilities

A grab-bag of helper functions. Several are what Control Panel pages call under the bonnet: ResolveMXRecords powers Monitoring & troubleshooting → MX query — since 6.2.21 it asks the server’s own resolver, honouring a configured custom DNS server, rather than the operating system’s — and EmailAllAccounts powers Mail flow & delivery → Server sendout.

Methods

Method Does
BlowfishEncrypt(input) / BlowfishDecrypt(input) The legacy reversible encoding for stored secrets — kept for script compatibility
CheckSieveSyntax(script) New in 6.x. Checks a Sieve (RFC 5228) script — returns an empty string when valid, otherwise the error
CriteriaMatch(matchValue, matchType, testValue) Tests whether a rule criteria matches a value, using the same matching as message rules
EmailAllAccounts(recipientWildcard, fromAddress, fromName, subject, body) Sends a message to every account matching the wildcard
EvaluateSieveScript(script, rawMessage) New in 6.x. Runs a Sieve script against a raw message and returns the resulting action summary — for testing scripts before deploying them
GenerateGUID() Returns a new GUID
GetMailServer(emailAddress) Resolves which server receives mail for an address (an MX lookup)
ImportMessageFromFile(filename, accountID) Imports a raw .eml file — with an account ID it lands in that account’s inbox; with 0 it is delivered to the message’s recipients. The service reads the file, so since 6.3.4, while it runs as NT SERVICE\hMailServer, the file must be somewhere that account may read
ImportMessageFromFileToIMAPFolder(filename, accountID, imapFolder) As above, but into a named IMAP folder of the account
IsLocalHost(hostname) Whether a host name points at this machine
IsStrongPassword(username, password) Whether a password passes the server’s strength rules
IsValidDomainName(domainName) Whether a string is a well-formed domain name
IsValidEmailAddress(emailAddress) Whether a string is a well-formed address
IsValidIPAddress(ipAddress) Whether a string parses as an IP address
MakeDependent(otherService) Adds a Windows service dependency, so hMailServer starts only after the named service — useful when your database engine runs on the same machine. Since 6.3.4 it is refused, with a sentence saying why, while the service runs as NT SERVICE\hMailServer, the account a new installation is given; an administrator sets the dependency instead, from an elevated prompt: sc.exe config hMailServer depend= RPCSS/service
MD5(input) / SHA256(input) Hex digest of a string. Since 6.3.4 it is the digest of the string’s UTF-8, so for text outside ASCII it can differ from what 6.3.3 answered
PerformMaintenance(operation) Runs a database maintenance operation — eMaintenanceOperation: 1 updates IMAP folder UIDs to their current values
RetrieveMessageID(filename) Returns the database ID of a message file — the inverse of Database.UtilGetFileNameByMessageID
ResolveMXRecords(domainName) New in 6.2.21. Resolves a domain’s MX records through the server’s own resolver — one hostname<TAB>ip line per delivery target, in the order delivery would try them. Empty when the domain has no mail servers; an error when the lookup itself failed — the distinction that matters when your own DNS server is unreachable
RunTestSuite(testPassword) Runs the internal test suite — destructive, see below

Never call RunTestSuite on a production server. It deletes every object on the server as part of its run. It exists for development against a scratch installation.

New in 6.3.4. Utilities gains the scripted side of that release’s features, among them the audit trail (GetAuditTrail, VerifyAuditTrail, RevertAuditedChange), the suppression list and bounces (GetSuppressionList, AddSuppression, RemoveSuppression, GetBounceCounts, ClassifyBounce), journal rules, spam-trap hits, the reputation check, DKIM rotation, webhook subscriptions, OpenPGP keys, the setup checklist (GetSetupChecklist), the sample domain, mailbox moves (MoveMailbox, GetMailboxMoves) and BackupArchiveIsEncrypted. Every one is appended, so a script or an early-bound client written for 6.3.3 keeps working.

From 6.3.6 the helpers that do work answer a caller that has signed in. BlowfishEncrypt, BlowfishDecrypt, ResolveMXRecords, GetMailServer, IsLocalHost, CheckSieveSyntax, EvaluateSieveScript and CriteriaMatch refuse a COM client that has not called Authenticate, with You do not have access to this property / method, whether it reached them through Application.Utilities or created hMailServer.Utilities itself. They answer an account of any level or the administrator that has signed in, and an event script running in the service, which needs no change. MD5, SHA256, GenerateGUID, IsStrongPassword, IsValidEmailAddress, IsValidIPAddress and IsValidDomainName stay open to any caller, and a server with no administrator password and no second factor answers every caller as it did. A program or script outside the service creates hMailServer.Application, calls Authenticate and uses that object’s Utilities: a Utilities created on its own has no way to sign in.

The MD5 and Blowfish helpers are historical. No new account password is stored with them — it is hashed with PBKDF2-HMAC-SHA256 by default, or with SHA-256, Argon2id or scrypt as PreferredHashAlgorithm chooses (§17.2), and an older, weaker hash is upgraded at the account’s next successful logon — but old scripts that call these helpers keep working, signed in from 6.3.6 (above).

ImportMessageFromFile is the workhorse for migrations: exported .eml files from another server can be dropped straight into mailboxes, or handed to the deliverer. For mbox exports, and since 6.2.27 a Maildir, there is no need to script it yourself — the Import Tool (§33.8, new in 6.2.12) wraps ImportMessageFromFileToIMAPFolder in a supported workflow.

Diagnostics, DiagnosticResults and DiagnosticResult

New in 6.x. The scripted face of the server’s self-tests: point Diagnostics at a domain you host and a remote domain to test against, call PerformTests(), and read the outcome per test from the returned collection. Reached as Application.Diagnostics. Since 6.3.4 the same tests also run over REST, as POST /api/v1/diagnostics/run with the two domain names, which answers each result’s name, description, success and details; on Linux, which has no COM, that is the way to script them.

Diagnostics members

Member Kind Does
LocalDomainName property (string) A domain hosted on this server to run the tests on
TestDomainName property (string) A remote domain to test outbound connectivity against
PerformTests() method Runs the diagnostics and returns a DiagnosticResults collection
DnssecChainStatus(name, type) method The server’s DNSSEC verdict for one name: 0 secure, 1 insecure, 2 bogus (since 6.2.27)

DiagnosticResult properties (all read-only)

Property Type Description
Description string What the test checks
ExecutionDetails string The test’s output — what it did and what it saw
Name string The test’s name
Result bool Whether the test passed

DiagnosticResults is read-only: Count and Item(index) only.

$diag = $app.Diagnostics
$diag.LocalDomainName = 'example.com'
$diag.TestDomainName  = 'gmail.com'

$results = $diag.PerformTests()
for ($i = 0; $i -lt $results.Count; $i++) {
   $r = $results.Item($i)
   '{0}: {1}' -f $r.Name, $(if ($r.Result) { 'OK' } else { 'FAILED' })
}

Scripting

The event-script configuration behind two Control Panel pages (chapter 23): the Scripting tab of Maintenance → Advanced, which holds the switch and the language, and Maintenance → Event scripts, which edits the file. The script file itself lives in Directories.EventDirectory; if you edit it on disk rather than through the Control Panel, call Reload() or the running server will keep the old version.

Methods

Method Does
CheckSyntax() Compiles the current script and returns the result — an empty result means it compiled cleanly
Reload() Re-reads the script file from the event directory

Properties

Property Type Description
CurrentScriptFile string Path of the script file in use (read-only)
Directory string The event-script directory (read-only)
Enabled bool Event scripts run
Language string VBScript or JScript, the two a Windows server runs. Since 6.3.4 it also takes JavaScript, the language of the Linux builds, and accepts all three on either platform (any other word is refused and nothing is stored); a language the build does not run is refused when the script is loaded, as HM5016 at a start or HM5710 at a reload, which keeps the earlier script. Storing the language loads nothing: the script in force keeps its language until the next load, so call Reload() after changing it (on Linux, which has no COM, POST /api/v1/settings/scripting/reload)

ServerMessage and ServerMessages

The customisable texts the server sends — bounce messages and other system mail — as edited on Mail flow & delivery → Server messages. The set of messages is fixed: you edit their text, but you cannot add or remove entries, and the collection’s shape reflects that. Since 6.3.4 a text set over COM is written on the object’s own copy and is in effect only at Save(): one never saved changes nothing, where up to 6.3.3 it changed the running server at once. An object read from the collection before the save keeps what it read, so read the message again (ItemByName, ItemByDBID) to see the saved text.

ServerMessage properties (plus Save())

Property Type Description
ID long Database identifier (read-only)
Name string Which message this is
Text string The text sent

ServerMessages members

Member Kind Does
Count property (read-only) Number of messages
Item(index) property (read-only) The message at a zero-based position
ItemByDBID(dbid) property (read-only) The message with a given database identifier
ItemByName(name) property (read-only) The message with a given name
Refresh() method Re-reads the collection

Client

A temporary object handed to event handlers (chapter 23) describing the connected client. It only exists inside an event; there is no way to reach it from an external script. Since 6.3.4 a JavaScript handler on Linux is handed the same object, with the same ten properties, all read-only.

Properties (all read-only)

Property Type Description
Authenticated bool Whether the client has logged on
CipherBits long Key bits of the negotiated TLS cipher
CipherName string Name of the negotiated TLS cipher
CipherVersion string TLS version of the connection
EncryptedConnection bool Whether the connection is TLS-encrypted
HELO string The host name the client gave in HELO/EHLO
IPAddress string The connecting client’s IP address
Port long The local port the client connected to
SessionID long The identifier assigned to this TCP/IP session — the session number you see in the logs
Username string The user name the client logged on, or tried to log on, with. In OnClientLogon, which fires after every logon attempt, successful or not, it names the account that was tried; Authenticated says whether the logon succeeded

Cache

hMailServer caches domains, accounts, aliases and distribution lists in memory to spare the database. This object — behind the Cache tab of Maintenance → Performance — tunes the time-to-live and size cap per object type and reports how often the cache is hit. A size cap set here applies at once but is not kept: every restart puts the four caps back to 10240 KB.

Methods

Method Does
Clear() Empties the cache and resets the hit-rate statistics — call it after writing to the database directly. Since 6.3.4 it also drops the cached setting levels of domains and accounts, so a level row written straight into the database is in effect at once rather than within sixty seconds

Properties

Property Type Description
AccountCacheMaxSizeKb long Cap on the account cache, in KB
AccountCacheSizeKb long Current size of the account cache, in KB (read-only)
AccountCacheTTL long Seconds an account stays cached
AccountHitRate long Account cache hit rate (read-only)
AliasCacheMaxSizeKb long Cap on the alias cache, in KB
AliasCacheSizeKb long Current size of the alias cache, in KB (read-only)
AliasCacheTTL long Seconds an alias stays cached
AliasHitRate long Alias cache hit rate (read-only)
DistributionListCacheMaxSizeKb long Cap on the distribution-list cache, in KB
DistributionListCacheSizeKb long Current size of the distribution-list cache, in KB (read-only)
DistributionListCacheTTL long Seconds a distribution list stays cached
DistributionListHitRate long Distribution list cache hit rate (read-only)
DomainCacheMaxSizeKb long Cap on the domain cache, in KB
DomainCacheSizeKb long Current size of the domain cache, in KB (read-only)
DomainCacheTTL long Seconds a domain stays cached
DomainHitRate long Domain cache hit rate (read-only)
Enabled bool Caching is on

The Cache getters are among the methods hardened in 6.2.10: called without authentication they now fail instead of returning success and uninitialized memory.

MessageIndexing

New in 6.x. The message index: when enabled, the server keeps an index of the messages’ headers in the database (hm_message_metadata), built in passes. On its own it does not index message bodies: the full-text term index that narrows which messages IMAP SEARCH BODY and TEXT have to read is IndexerFullText, on the Indexing tab of Maintenance → Performance, off by default and indexing nothing unless this switch is on as well. Reached as Application.Settings.MessageIndexing; a settings object, so assignments persist immediately.

Methods

Method Does
Clear() Clears the message-indexing cache — the index is rebuilt on the next run
Index() Triggers an indexing run now, if one is not already in progress

Properties

Property Type Description
Enabled bool Message indexing is on
TotalIndexedCount long Messages indexed so far, across the installation (read-only)
TotalMessageCount long Total messages in the installation (read-only)

Comparing the two counters tells you how far along a rebuild is.

Language and Languages

The interface translations shipped with the server. Languages is a read-only collection; each Language can translate an English source string into that language. Their main consumer was the classic hMailServer Administrator, which the fork retired in 6.2.0 (source removed in 6.2.10) in favour of the Control Panel; the objects remain in the COM API for script compatibility.

Language members

Member Kind Does
IsDownloaded property (read-only) Whether the language has been downloaded
Name property (read-only) The language’s name
String(englishString) property (read-only) The translation of an English source string
Download() method Not implemented — it fetched translations from a download service on hmailserver.com, which this fork does not use

Languages members (all read-only)

Member Does
Count Number of languages
Item(index) The language at a zero-based position
ItemByName(name) The language with a given name

32. Error messages explained

This chapter is a lookup reference. When a log line, a bounce message or a mail client hands you an exact error text, find it here and you will get the cause and the fix. Chapter 19 is the first-line checklist — “mail doesn’t arrive, where do I start?”. Come here once you have a specific message in front of you.

Errors surface in three places:

Where What you see
Monitoring & troubleshooting → Live logs (in a Control Panel running on the server itself) and the Logs directory (/var/log/hmailserver on Linux) Everything, as it happens. The error log carries the detail
Monitoring & troubleshooting → Delivery queue The last error a queued message received from the remote server
Bounce messages What the sender is told when delivery fails permanently

All console paths in this chapter refer to the hMailServer Control Panel, which is a Windows application. On Linux, the Control Deck, the browser administration page the REST listener serves, has queue and logs views that show the same. The classic hMailServer Administrator was retired in 6.2.0 and the PHP WebAdmin was removed in 6.2.10 — neither is an error surface in 6.3.8.

The texts hMailServer issues can be customised under Mail flow & delivery → Server messages, so an edited installation may word them slightly differently. The reply codes do not change.

32.1 Reading an SMTP reply

Every SMTP reply starts with a three-digit code, and the first digit tells you almost everything:

First digit Meaning What happens next
2 Success Nothing — carry on
3 Intermediate — send more Normal mid-transaction state
4 Temporary failure The sending server keeps the message and retries later. No bounce
5 Permanent failure The sending server gives up and bounces the message to the sender

The distinction matters when you diagnose: a 4xx in your delivery queue means wait, or fix and wait — the message is not lost. A 5xx means the message has already bounced and resending requires the sender to act.

New in 6.2. On modern (EHLO) sessions hMailServer also appends an enhanced status code — a dotted triple such as 5.7.1 — after the three-digit code, because it advertises ENHANCEDSTATUSCODES (chapter 21). Legacy HELO sessions get the classic replies. For everything in this chapter, the three-digit code and the text are what you match on.

32.2 Database errors

hMailServer keeps accounts, settings and message metadata in its database (§3.5). When the database misbehaves, the symptoms range from a harmless log line to refused mail. Database errors always land in the error log; the entry immediately before a refusal usually names the real cause.

Connection failures

Message Cause Fix
MySQL server has gone away The connection to the database server dropped mid-query — a network blip, or the database server shutting down (typically just before a reboot of its host) Usually none needed. hMailServer reconnects and re-runs the query automatically; this line is only logged when the retry also fails. Investigate the network path or the database host if it recurs
Cannot connect to the database at startup (HM4354 in the error log: hMailServer failed to connect to the database server) The database service is not running, the connection details in the [Database] section of hMailServer.INI are wrong, or a firewall sits between hMailServer and a remote database host. Since 6.3.4 it can also be [Database] TlsMode: a value other than prefer, require, verify-full or disable is refused (HM7530), verify-full naming a TlsCaFile that is not there is refused (HM7531), and require or verify-full will not connect to a database server that offers no TLS Start the database service; verify host, port and credentials; check the firewall; correct TlsMode and restart. Since 6.3.4, hMailServer.exe --check-database (sudo -u hmailserver hmailserver --check-database on Linux) makes one connection with the file’s settings, says whether it is encrypted or why it failed, and changes nothing, so it can run beside the service
Connection lost during operation, repeatedly An unreliable network path to a remote database server Move the database onto the same machine, or fix the link. A mail server that cannot reach its database cannot accept mail

Running the database on a separate machine is fine, but the link between the two is now part of your mail server. Treat it with the same seriousness as port 25.

Schema mismatches

The database schema is versioned — 6108 in 6.3.6 to 6.3.8, 6105 in 6.3.5, 6104 in 6.3.4, 6040 in 6.3.3, 6038 in 6.3.2, 6031 from 6.2.27 to 6.3.1, 6030 in 6.2.25 and 6.2.26, 6025 in 6.2.24, 6011 in 6.2.19 to 6.2.21 and 6005 for the 6.2 releases before that — and the installer runs DBUpdater to walk older schemas forward automatically (chapter 18). On Linux the same work is hmailserver --upgrade-database, which the package’s post-install step runs (chapter 35).

If a SQL Server Compact upgrade to schema 6030 reported failure on 6.2.25, it had in fact succeeded: a memory-safety fault inside the OLE DB provider made the verification step give a false verdict and took the service down with it, logged as HM10045 Unknown error; two seconds later the server’s crash oracle, which treats a memory-safety fault as fatal, ended the service, and service recovery restarted it. Fixed in 6.2.26. Your database is at 6030 with its foreign keys in place, and the 6.2.26 or later installer finds nothing left to upgrade.

Symptom Cause Fix
The database is too old for this version of hMailServer. Please run hMailServer Database updater (DBUpdater.exe) to upgrade it. at startup The program files were updated but the schema upgrade did not run or did not finish Run the installer again and let it upgrade the database, or run DBUpdater.exe directly, or, since 6.3.4, hMailServer.exe --upgrade-database from Bin with the service stopped, which needs neither .NET nor the administrator password; on Linux, sudo -u hmailserver hmailserver --upgrade-database. Since 6.3.4 a service whose binaries were replaced by something other than the installer can do it itself as it starts, with UpgradeOnStart=1 in the [Database] section of hMailServer.INI: off by default, it takes no backup first, and it belongs on one node at a time. On a fresh install this error was a real defect through 6.2.4 — the install scripts stamped schema 6004 — fixed in 6.2.5; install the current release (§19.6)
Errors after a rollback to an older version: The database is too new for this version of hMailServer. Please upgrade hMailServer. An older server refuses a newer schema — the schema only moves forward Restore the database from the backup you took before upgrading, then reinstall the older version (§18.6). There is no downgrade path without one. If the version you are leaving had sealed its stored secrets (from schema 6103, in 6.3.4 and later), restore the pre-upgrade hMailServer.INI with the database: an older server does not open the sealed values that version wrote into the file
The upgrade stops at the step from schema 6040 to 6041 with MySQL error 1071, and the database stays at 6040 New in 6.3.4, MySQL and MariaDB only. Steps 6041, 6101 and 6102 need index keys that only MySQL 5.7.9 or MariaDB 10.2.2 and later can hold. Each step opens with a probe, so nothing has been changed Upgrade the database server, then run the schema upgrade again
Schema step 6102 stops with MySQL error 1062, naming an index and a value New in 6.3.4, MySQL and MariaDB only. Step 6102 converts every table to utf8mb4, and two rows the old collation kept apart (straße and strase on MariaDB 11.5 and later, for example) are one value under the new one Rename or remove one of the two rows and run the upgrade again: every statement of the step can run twice. The step also needs the ALTER privilege on the database itself, not only on its tables, and it rebuilds every table, so on a large database it takes as long as copying it

More: Upgrading from 5.x, 6.2 or 6.3: the 24 changes without a switch, and rolling back on the forum.

Timeouts and save failures

Message Cause Fix
451 Your message was received but it could not be saved. Please retry later. (sent to the SMTP client) The message arrived over the wire but the database insert failed Read the error log entries immediately before this line — they contain the underlying database error. The sending server will retry, so nothing is lost yet
Queries timing out; the server feels sluggish The database host is overloaded, or the built-in database is being pushed past its comfortable size Check CPU, memory and disk on the database host. For a large installation on the built-in database, migrate to MySQL/MariaDB or PostgreSQL (§3.5)
451 Unable to verify the recipient at the moment. Please retry later. (sent to the SMTP client at RCPT TO) The database did not answer the recipient lookup. hMailServer tells a database that cannot answer apart from an address that does not exist, so the mail is deferred rather than refused as 550 Unknown user Check the database, and /readyz (§16.3). The sending server retries, so nothing is lost yet

32.3 DNS errors

On Windows, hMailServer resolves names through the Windows resolver (DnsQuery), using the DNS servers configured in the machine’s network settings, unless Connections & protocols → DNS resolver names a server of its own (DNSServer: one IPv4 address, no port; every lookup then bypasses the Windows DNS cache, so leave it empty unless you need a specific resolver). Since 6.3.4 SPF lookups follow the same setting. When a lookup fails, the Windows error code is written to the error log.

On Linux it uses its own DNS client, which reads the nameserver list out of /etc/resolv.conf and falls back to TCP on truncation by itself. It reports the same verdicts under the same numbers — 9003 for NXDOMAIN, 9501 for no records of that type — so the table below reads correctly on either platform. One difference worth knowing: it passes over IPv6 nameservers in /etc/resolv.conf while the file lists an IPv4 one, and up to 6.3.5 always, so a resolver reachable only over IPv6 was not used; from 6.3.6 the first two IPv6 nameservers are asked when no IPv4 one is listed.

Code Windows name Meaning Effect on delivery
1460 ERROR_TIMEOUT The query timed out — the DNS server is rebooting, overloaded or unreachable Temporary. The message stays in the queue and is retried on the schedule on the Delivery tab of Mail flow & delivery → Delivery of e-mail
9002 DNS_ERROR_RCODE_SERVER_FAILURE The DNS server answered SERVFAIL — it failed internally, or (on a validating resolver) the domain’s DNSSEC signatures are broken Temporary. Retried as above. If one specific domain always fails, suspect its DNSSEC
9003 DNS_ERROR_RCODE_NAME_ERROR NXDOMAIN — the domain does not exist Not delivered, but hMailServer answers an MX lookup failure with a class 4 code, so the message is retried and bounces when the retries run out; check the recipient address for typos
9501 DNS_INFO_NO_RECORDS The name exists but has no records of the requested type — for an MX query, the domain publishes no MX records If nothing else resolves for the domain, as for 9003: retried, then bounced when the retries run out

When the MX lookup for a recipient domain fails, the message is retried, and the bounce the sender receives once the retries run out says which failure it was: No mail servers appear to exists for the recipient’s address. when DNS answered that the domain publishes nowhere to deliver mail, and Unable to find the recipient’s email server. The DNS query has failed. when the lookup itself failed. Two very different situations produce them:

  1. The recipient domain really is broken or gone. Verify from a machine outside your network, or with Monitoring & troubleshooting → MX query.
  2. Your own resolver is broken. The give-away is every outbound message waiting in the queue with the same DNS failure. If Connections & protocols → DNS resolver names a server, it is that server, not the machine’s, that must answer, and the commands below test only the machine’s own resolvers. Windows keeps separate DNS settings per adapter, so a working browser does not prove the service’s lookups work. Test what the machine actually resolves:
Resolve-DnsName -Type MX gmail.com

On Linux, against the nameservers the server itself will read:

cat /etc/resolv.conf
dig +short MX gmail.com

If that does not return Google’s mail servers, fix the machine’s DNS configuration before touching anything in hMailServer.

New in 6.0 — DNSSEC changes the failure modes. With DnssecValidationEnabled=1 (the default), SPF, DKIM, DMARC and DANE lookups against a domain with a bogus DNSSEC chain are treated as failed — deliberately. And with DANE enforcement on, a recipient domain whose TLSA records cannot be validated will have your mail deferred rather than delivered unencrypted (chapter 9). If one domain’s mail suddenly sits in the queue, check whether their DNSSEC broke. From 6.3.6 an MX or TLSA lookup that cannot tell — no answer, SERVFAIL, a signed zone’s answer without its signatures or its proof that no records exist — is indeterminate rather than “unsigned”: the host is not used, and a delivery with no host left is deferred with 4.7.0 and a reason naming the lookup. So if every domain’s mail sits in the queue after the upgrade, check your own resolver: one that strips DNSSEC signatures, or a firewall that drops DNS answers longer than 512 octets, leaves the server unable to prove most domains unsigned, and the application log says so once in ten minutes (“DNSSEC: the root zone’s keys did not validate”). Make the resolver answer DNSSEC queries, or set DaneEnforcementEnabled to 0 (§18.5).

32.4 SMTP errors hMailServer issues

Everything in this section is sent by your server to a connecting client or server. Grouped by what kind of problem each code family signals.

Temporary refusals (4xx)

Reply When it is sent What to do
421 Connection timeout. The session sat idle past the SMTP server’s idle timeout (the Timeouts tab of Connections & protocols → Protocols), so hMailServer closed it Nothing — a slow or stalled client. Harmless in isolation
421 Excessive amounts of data sent to server. A client sent a very large run of data with no line terminator A broken or malicious client. Investigate the source IP if it repeats
421 Too many messages from your IP address. Please slow down and try again later. New in 6.2. MaxSubmissionsPerIPPerMinute is set and the source IP exceeded it (§17.5) Working as intended — this is what contains a compromised account. Find out which account is submitting
450 Please try again later. Greylisting, for a message with an empty sender. The refusal is deliberately delayed until after DATA so that another server’s recipient-verification callback (which probes RCPT TO with an empty sender) still gets a straight answer Nothing. The sender retries and passes
451 Please try again later. Greylisting: the sender/recipient/IP triplet has not been seen before Nothing — a real server retries, is let through once the initial delay (30 minutes by default) has passed, and is remembered. Exempt time-critical senders via Spam & virus filtering → Greylisting white list; since 6.3.4, Spam & virus filtering → Greylisting bypass list can instead let a named domain through when its SPF, DKIM or DMARC passes
451 (enhanced code 4.4.3) New in 6.3.4, and off until SPFTempErrorDefer is on: the sender’s SPF check could not finish (DNS timed out or answered SERVFAIL, or the lookup limit was reached), so the transaction is deferred rather than scored Nothing, usually: the sender retries once DNS answers. If it recurs for one domain, check that domain’s SPF record and your own resolver

Protocol errors — the client is misbehaving

These indicate a defective client (or a probe), not a server problem:

Reply When it is sent
500 Line too long. A command exceeded the permitted length — a client defect or a hacking attempt
501 EHLO Invalid domain address. / 501 HELO Invalid domain address. The host name given in EHLO/HELO is syntactically invalid
502 TURN disallowed. / 502 VRFY disallowed. TURN and VRFY are not supported — both are historic verbs with abuse potential
503 Bad sequence of commands The client issued commands before identifying itself with EHLO/HELO, or used a verb hMailServer does not implement or that is invalid in the current session state
503 Must have sender first. The client sent RCPT TO before MAIL FROM
503 Must have sender and recipient first. The client sent DATA (or BDAT) before completing the envelope
503 Issue a reset if you want to start over The client started a new message mid-transaction without RSET
504 Authentication type not specified. / 504 Authentication mechanism not supported. The client attempted AUTH with no mechanism, or one hMailServer does not offer
421 Too many invalid commands. Bye! Sent just before hMailServer disconnects a client that has had too many commands refused — a probe, or a badly broken client. Only when Disconnect clients sending too many invalid commands is on (off by default; the SMTP tab of Connections & protocols → Protocols)

Authentication and relay refusals

530 SMTP authentication is required. — the IP range the client connects from requires authentication, and the client is not authenticating. The right fix is almost always on the client: configure it to authenticate and to submit on port 587, not port 25. The defaults are deliberate: connections from localhost are exempt, except when they relay from an external sender to an external recipient; external servers delivering to local accounts need not authenticate (otherwise nobody could send you mail); external clients sending to external recipients always do. From the Internet range, a message whose envelope sender is at one of this server’s active domains (or a domain a route treats as local) needs authentication too, whatever the recipient, so an outside server or service that delivers to a local mailbox in the name of one of your own domains gets this 530 and must authenticate. Weakening the IP-range requirement instead of fixing the client invites spam through your server.

530 Must issue STARTTLS first. — the port’s connection security is STARTTLS Required and the client tried to carry on in plain text. Fix the client: enable STARTTLS for that port, or point it at the right port.

530 A SSL/TLS-connection is required for authentication. — the IP range the client connects from requires TLS for authentication (the range’s Require TLS for auth option, on Sign-in & access → IP ranges), so AUTH is not offered before STARTTLS and is refused with this reply if it is sent. IMAP answers BAD A SSL/TLS-connection is required for authentication. and POP3 -ERR. New in 6.3.4, a new installation (not an upgrade) starts with the option on for the Internet range, so a client on another machine that does not start TLS meets this reply; an upgraded server has it only where someone set it, for example by accepting the setup checklist’s Passwords only over TLS fix. Clients on the server itself, and other servers delivering on port 25, are not affected by the new installation’s default. Fix the client: STARTTLS on 587, 143 or 110. Do not clear the range’s option to make an old client work.

Some routers and firewalls inspect and rewrite SMTP traffic (“SMTP fixup” on older Cisco equipment, ALG features elsewhere). They can mangle the authentication exchange before it reaches hMailServer. If a correctly configured client still cannot authenticate, disable SMTP inspection on the network path.

535 Authentication failed. Restarting authentication process. — wrong username or password, or the account is disabled. The username is the full address ([email protected]). Since 6.3.4 a password hash stored before the upgrade, for a password with a character above U+00FF (Cyrillic, Greek or CJK letters, for example), is refused even to the right password, and the application log names the account: set that password again. ASCII and Latin-1 passwords sign in as before.

Repeated 535s from one IP will trip auto-ban (§17.1): the reply becomes 535 Authentication failed. Too many invalid logon attempts. and the IP is then refused at connection. If a user reports being locked out after mistyping a password, that is auto-ban doing its job — the ban expires (by default, three failures ban the address for 60 minutes), or you can lift it early by deleting the expiring range listed under Sign-in & access → IP ranges.

550 Login credentials no longer valid. Please re-authenticate. — one session can carry many messages, and hMailServer checks the credentials afresh for each of them, so disabling an account or changing its password bites immediately rather than when the session eventually closes. The client should reconnect and authenticate again. Before 6.3.4 a session that signed in without a password (SCRAM-SHA-256, XOAUTH2, OAUTHBEARER, EXTERNAL or GSSAPI) got this reply at its second message whatever the account’s state; since 6.3.4 its account is checked again instead: still active, in an active domain and not held for review, among other checks.

550 Delivery is not allowed to this address. — the IP-range rules refused the recipient: the range the client connects from does not allow this combination of sender and recipient (each local or external) at all. The two ranges hMailServer ships with allow all four combinations and require authentication for external-to-external relaying, so an unauthenticated relay attempt against them gets 530 SMTP authentication is required. (above), not this reply: a 550 comes from a range that has been changed or added. A sender counts as local only when its domain is an active domain hosted here, or a route says so. Check Sign-in & access → IP ranges before changing anything, and re-read §17.1 before you loosen it.

Recipient and sender rejections

Reply When it is sent What to do
550 Unknown user The recipient domain is hosted here, but no matching account exists and the domain has no catch-all Expected for mistyped addresses. If the account should exist, check the spelling in Domains. Since 6.3.4 an address an administrator has declared a spam trap answers with the same words, and the connecting address can then be auto-banned (SpamTrapBanMinutes, 1440 by default; the ban is listed on Sign-in & access → IP ranges)
550 Account is not active. / 550 Alias is not active. / 550 Distribution list is not active. The recipient object exists but is disabled Re-enable it, or accept that mail to it is refused
550 Domain has been disabled. The whole recipient domain is inactive Accounts & domains → Domains → the domain’s Properties → General tab: make the domain active again
550 Not authorized sender. / 550 Not authorized owner. / 550 Not authorized domain. The sender is not permitted to post to this distribution list — the text names which permission mode refused Adjust the list’s sender permissions if they should be
550 Recipient not in route list. The recipient domain matches a route configured to accept only listed addresses, and this address is not listed Add the address to the route (chapter 14), or loosen the route’s address checking
550 <address> is on the suppression list of <domain>: … New in 6.3.4, and only while bounce or complaint processing is on (both off by default): mail from that sending domain to the address bounced permanently (or drew a complaint, or an administrator listed it), so with the default action, refuse, a submission to it is refused at RCPT TO; a message already queued is returned in a non-delivery report with the same sentence Check the address with the sender. If it works again, remove the entry on Monitoring & troubleshooting → Suppression list
550 A valid address is required. / 550 The address is not valid. The recipient or sender address is syntactically invalid A client defect — the address cannot be parsed
550 Invalid syntax. Syntax should be MAIL FROM:<mailbox@domain>[crlf] Malformed MAIL FROM command (a matching reply exists for RCPT TO:<mailbox@domain>[crlf]) A client defect
550 Sender address must be specified. The server is configured to refuse an empty sender address, and the client used one Empty senders carry bounces and delivery reports — refusing them breaks standards and breaks other servers’ sender verification. Reconsider the setting
550 Sender domain does not have any MX records. The MX check is enabled and the claimed sender domain publishes no MX records Legitimate senders occasionally fail this. The remote administrator must fix their DNS — or you can disable the check
550 Blocked by SPF. The message was rejected on the sender domain’s SPF policy; if the record supplies an explanation, it is appended in parentheses The sending server is not authorised by the sender’s SPF record. Usually spam; occasionally a misconfigured forwarder — SRS at the forwarding end fixes that (§17.6). Since 6.3.4 SPF follows RFC 7208, and a client on 127.0.0.0/8 or ::1 no longer passes before the policy is read: a content filter that hands mail back to the server on the same machine should authenticate, or connect from an IP range with anti-spam off
550 with other text (554 when the refusal comes after the body) The spam score reached the delete threshold, and the reply carries the first failing test’s text. DNS blacklist and SURBL entries carry their own, in the Reject message column of Spam & virus filtering → DNS blacklists and Spam & virus filtering → SURBL servers The text is the first failing test’s. For a DNS blacklist or SURBL entry it names the list, and the sending IP or a URL in the body is listed there
550 Mail from <name> is refused by this server's policy. New in 6.3.4. The envelope sender’s domain or the HELO name is black on Spam & virus filtering → Domain white and black list, checked at MAIL FROM Working as configured. Remove or narrow the entry if the sender is wanted
550 <address>: this mailbox accepts mail from named domains only. / 550 <address>: this account may send to named domains only. New in 6.3.4. Restricted delivery: the mailbox, or its whole domain, may exchange mail only with named domains Working as configured. The named domains are on the domain’s Restricted delivery tab
550 to the second recipient of an empty sender, and to its DATA New in 6.3.4, off until RejectMultiRcptNullSender is on: a delivery report has exactly one recipient, so a null sender naming a second is refused Working as configured. With BanMultiRcptNullSender on, the address is also auto-banned (24 hours by default)
550 Too many delivery attempts before the greylisting delay had passed. … New in 6.3.4, off until GreyListingAbuseThreshold is set: one sender, recipient and connecting address were tried that many times before their greylisting window opened, and are locked out for a while A server that retries from its queue at ordinary intervals rarely meets it; one that delivers many queued messages to one mailbox down one connection can, since every RCPT TO before the window opens counts. The 550 is permanent to the sender, so a wanted sender caught by it is stranded until the lock lifts, after GreyListingAbuseLockMinutes (60 by default); Clear greylisting triplets (the Greylisting tab of Spam & virus filtering → Anti-spam settings) lifts every lock at once, at the cost of greylisting every sender once more. For a bulk sender that delivers many messages to one mailbox at once, raise or clear the threshold
552 with the reason, to an authenticated sender at MAIL FROM New in 6.3.4. The account has reached its quota’s send-stop share, set for the server or the domain (off by default) Make room in the mailbox, or raise its size limit; mail to the account is still received

550 The host name specified in HELO does not match IP address. — the optional HELO host check in the anti-spam settings verified that the name the sending server announced resolves to the IP it is connecting from, and it did not. Legitimate servers fail this surprisingly often, which is why the check is off by default. If a wanted correspondent is refused: tell their administrator their HELO name is wrong, or disable the check, or configure spam handling to tag rather than reject so the mail still arrives marked as suspect (chapter 10).

550 Mail server configuration error. Too many recursive forwards. — hMailServer follows aliases and forwards to find the end recipient, and it went round in circles or ran out of road. Two configurations cause nearly all of these:

  1. An alias loop — [email protected] points at [email protected], which points back at [email protected]. There is no end recipient.
  2. A catch-all pointing at nothing — the domain’s catch-all names an address that is not an existing account.

Walk the chain in Domains — every alias, forward and catch-all must terminate at a real, active account.

Message-content and storage rejections

Reply When it is sent What to do
552 Message size exceeds fixed maximum message size. The message exceeds the maximum size set in the SMTP settings or on the recipient domain Raise the limit, or the sender shrinks the message. The reply states both sizes
554 5.7.1 Rejected An OnSMTPData or OnAcceptMessage event script rejected the message without giving a reason Your script’s logic decided this — check the script
554 5.7.1 <text> An OnSMTPData or OnAcceptMessage script rejected the message with a reason — the reply is the script’s text verbatim after the code As above; the text is whatever the script supplied
453 4.7.0 <text> An OnSMTPData or OnAcceptMessage script refused the message for now, with its own text: a temporary refusal, so the sender retries As above
554 Tagged as Spam by SpamAssassin SpamAssassin’s verdict pushed the message over the rejection line Working as configured. Tune thresholds per §10.1 if legitimate mail is caught
451 Rejected - No data saved. The message file could not be written to disk — the data directory is not writable by the service account. A 4xx, so the sender retries Fix permissions on the Data directory for the account the service runs as. Since 6.3.4 a new installation runs the service as NT SERVICE\hMailServer, which reaches only the folders it is granted: a data directory outside the program folder must be approved and granted by hMailServer.exe --confirm /GrantServiceAccess, run elevated from Bin (--report-only lists what would be granted and what the service cannot reach, and changes nothing; §17.3)
451 Your message was received but it could not be saved. Please retry later. The database insert failed after the message was received. A 4xx, so the sender retries See §32.2 — the error log immediately before holds the cause
550 with a rule’s text, or naming the rule New in 6.3.4. A mail flow rule whose action is to reject with a text, or an outbound content policy (a card number, an IBAN, an attachment type) that blocks, refused the message at the end of DATA. A reject with no text of its own names the rule Working as configured. Check the rule that fired, among the server’s rules (Mail flow & delivery → Rules) or the domain’s own

554 Rejected - Message containing bare LF's. — the SMTP standard requires every line to end with CRLF (carriage return + line feed). This message contained lines ending in a bare LF, and the option to reject such messages is enabled. Spam tools and buggy software are the usual senders — but so are hand-rolled scripts.

If your own application triggers this: terminate every line of the message, headers and body alike, with \r\n (VBScript: vbCrLf). A gotcha for Unix-hosted senders: Linux’s native line ending is LF alone, and some Unix mail systems convert LF to CRLF automatically — so a message already using CRLF can come out as CR CR LF. Generate CRLF exactly once.

32.5 SMTP errors from remote servers

When hMailServer delivers mail, the remote server’s replies decide the message’s fate:

  • 4xx — the message stays in Monitoring & troubleshooting → Delivery queue and is retried on the schedule on the Delivery tab of Mail flow & delivery → Delivery of e-mail. The queue shows the last reply received, which is your primary diagnostic.
  • 5xx — hMailServer gives up and bounces the message to the sender, quoting the remote reply.

Common remote replies and what they mean for you:

Remote reply (typical text) Meaning What to do
550 Mailbox unavailable / 550 User unknown The recipient does not exist on that server. (hMailServer’s own wording for the same situation is 550 Unknown user — if you see “Mailbox unavailable”, it came from the far end) Check the address. If it is definitely right, the recipient’s server or DNS is misconfigured — their problem to fix
450/451 Try again later (greylisting) The remote server is greylisting you Nothing. hMailServer retries automatically and the retry is accepted
421 Too many connections/messages You hit the remote server’s rate limits Wait — retries handle it. Since 6.3.4 hMailServer also backs off by itself: a 421, or a 4xx with one of the providers’ rate-limit codes or phrases, holds deliveries to that destination alone for OutboundBackoffMinutes (2 by default, doubling to OutboundBackoffMaxMinutes, 60; 0 turns it off). The queue then shows a 4.4.5 deferral whose reason names the destination and the reply it gave, the application log says Delivery deferred by this server's own shaping, not by a remote server., and such a wait does not spend the message’s retries until it has been queued longer than its retry schedule spans. Through a smart host the relay is one destination, so one such reply holds everything sent through it. To stay under large providers’ limits permanently, set MaxOutboundPerDestinationPerMinute (§17.5, new in 6.2), which defers rather than bounces
550/554 naming a blacklist (Spamhaus etc.) Your IP is on a DNS blacklist Check the IP at mxtoolbox, fix the cause (open relay? compromised account? missing PTR?), then use the list’s delisting form
5.7.x policy rejections from Google, Microsoft and other large providers Your sending reputation or DNS is inadequate — missing PTR, SPF, DKIM or DMARC Work through §19.4. PTR first; it is the biggest single factor

550 Sender verification failed — the receiving server tried to verify that your sender address really exists before accepting the mail. It looked up your domain’s MX records, connected to the host they name — which should be your server — and probed RCPT TO: your sender’s address with an empty envelope sender. Three things break it:

  1. Your MX records are wrong, so the verifier connected to the wrong machine.
  2. The sender account does not exist on your server.
  3. You refuse empty sender addresses, so the verifier’s probe was rejected at MAIL FROM — see 550 Sender address must be specified. in §32.4. This is the self-inflicted version; allow empty senders.

Bounce texts hMailServer generates about its own delivery attempts

“Could not connect to <IP> on port 25 since this would mean connecting to myself.” — loop protection. Before connecting out, hMailServer checks that the destination IP and port are not ones it is itself listening on; delivering to yourself for an address you do not host would loop forever, so the attempt fails with this text instead. It appears in the delivery queue and the TCP/IP log, and in the eventual bounce once the retries are exhausted. Two configurations cause it:

  1. The smart host points at this machine. Check the smart host on the Relayer tab of Mail flow & delivery → Delivery of e-mail — if it says localhost, 127.0.0.1, the server’s own name or your own domain, that is the fault. The smart host is someone else’s server you relay through (§14.1), never your own.
  2. An MX record points at this server, but the domain is not added here. Either add the domain under Domains, or fix the MX record.

A bounce saying No mail servers appear to exists for the recipient's address. (the wording, typo included, is hMailServer’s) — DNS answered, and the recipient’s domain publishes nowhere to deliver mail (enhanced code 4.1.2). Its companion, Unable to find the recipient's email server. The DNS query has failed. (4.4.4), means the lookup itself failed: check your resolver and Connections & protocols → DNS resolver. Both are class 4, so the message is retried before it bounces, and the error log carries the DNS error code (§32.3).

A delivery through a relay or route that signs in with OAuth2 waits in the queue with a 450, and the error log has HM5912 — new in 6.3.4. The host is on OutboundOAuth2Hosts and its connection security is none, or STARTTLS (optional) against a server that does not offer it, and hMailServer no longer sends its token in the clear. Set the route’s or the relay’s connection security to SSL/TLS or STARTTLS (required); Exchange Online offers STARTTLS on 587.

32.6 Deeper troubleshooting

Chapter 19 is the checklist; run it first. These techniques are for when the checklist passes and the problem persists.

Speak SMTP by hand

A manual session separates network problems from policy problems in under a minute. From any machine with the telnet client (on Windows it is an optional feature) — or openssl s_client, which also handles the encrypted ports:

# Plain session to port 25
telnet mail.yourcompany.com 25

# STARTTLS session — also proves the certificate and TLS configuration
openssl s_client -starttls smtp -connect mail.yourcompany.com:25 -brief

Then type a session and watch the replies:

220 mail.yourcompany.com ESMTP
EHLO test.example.net
250-mail.yourcompany.com
250 ...capabilities...
MAIL FROM:<[email protected]>
250 OK
RCPT TO:<[email protected]>
250 OK
QUIT

If the connection opens but a command is refused, the refusal text plus §32.4 tells you exactly which rule fired. If the connection does not open at all, it is network — work through §19.2.

Prove outbound port 25 works

Mail sitting in the queue with connection errors usually means your ISP blocks outbound port 25 — very common on consumer lines. Test against a real, known-good MX host:

Resolve-DnsName -Type MX gmail.com          # pick any MX host from the answer
Test-NetConnection gmail-smtp-in.l.google.com -Port 25

TcpTestSucceeded : False against several different MX hosts means the port is blocked. Your options: ask the ISP to open it, or relay outbound mail through a smart host (§14.1).

Follow one message end-to-end in the logs

Enable debug logging under Monitoring & troubleshooting → Logging, reproduce the problem once, then turn it off — it is verbose (§16.2). Read the whole session in order: connection, HELO, envelope, the anti-spam verdicts, and the final reply. The last line before a refusal names the check that refused. With JsonLogging=1 (new in 6.0), each line is a JSON object, which makes filtering one session out of a busy log trivial.

For persistent watching, the OnError event (chapter 23) fires whenever an error is logged — the script receives the severity, error code, source and description, and a few lines there can raise an alert instead of waiting for a user to complain. Since 6.3.4 the server also raises alerts of its own, a failed backup and an expiring certificate among them. They are on by default, but nothing is mailed until AlertRecipient and AlertSenderAddress are set: in the Control Deck’s Settings view or with PUT /api/v1/settings, where they are alert_recipient and alert_sender_address, or over COM. If you run Prometheus (metrics are new in 6.0), trend hmailserver_messages_deferred_total and _bounced_total (§16.3): a deferral spike points at a network, DNS or remote-policy problem; a bounce spike points at bad addresses or a reputation problem.

“Someone is sending spam through my server”

The symptom: Monitoring & troubleshooting → Delivery queue fills with messages you do not recognise, to recipients you have never heard of. Two causes, in order of likelihood:

  1. A compromised account. The spammer is authenticating with stolen credentials, so the relay rules are satisfied. The SMTP log shows which account — change its password immediately. MaxSubmissionsPerIPPerMinute (§17.5) caps the damage next time. Since 6.3.4, compromised-account detection judges every authenticated submission against the account’s own history and raises the account.compromised alert when one has the shape of a takeover (mailed once AlertRecipient and AlertSenderAddress are set). It starts in watch mode and holds nothing: set compromise_detection_mode to enforce in the Control Deck’s Settings view (it has no Control Panel page) to have it hold the messages and lock the account, and review what it holds in the Deck’s Held for review view.
  2. An open relay. An IP range permits external-to-external delivery without authentication. Check Sign-in & access → IP ranges, then run the test below.

Either way, delete the junk from the queue before it drains — every delivered spam message damages your IP’s reputation.

32.7 Testing your own server for open relay

An open relay accepts mail from anyone, to anyone — and once found, it becomes a spam cannon within hours, not days. hMailServer’s defaults are safe (chapter 17), but you should verify after installing and after any change to IP ranges. Only ever test a server you operate.

The manual test

From a connection outside your network — a phone hotspot will do — open a session to your port 25, do not authenticate, and try to relay:

220 mail.yourcompany.com ESMTP
EHLO relaytest.example.net
250 ...
MAIL FROM:<[email protected]>
250 OK
RCPT TO:<[email protected]>
530 SMTP authentication is required.

That 530 is the correct answer on the IP ranges hMailServer ships with, which allow external-to-external delivery only after authentication: an unauthenticated stranger may not send through you. A range that does not allow that delivery at all refuses it with 550 Delivery is not allowed to this address. instead, which is just as correct. If you instead get 250 OK on the RCPT TO, you are an open relay — stop and fix it now.

Test from outside. A session opened on the server to 127.0.0.1 matches the My computer range, not the Internet range a stranger meets, and any internal ranges you have added may allow more than the Internet range does — so a test run on the server, or from inside your network, proves nothing. The test must arrive the way a spammer would.

Online testers

Web-based relay testers probe a series of relay tricks (unqualified recipients, %-hack addresses, mismatched envelope forms) in one pass — MxToolbox’s SMTP diagnostics at https://mxtoolbox.com/diagnostic.aspx include an open-relay probe. They use throwaway addresses and are safe to run against your own server. Bear in mind that if your server is open, a thorough test successfully relays a real probe message — one more reason to fix a failure immediately rather than re-running the test.

False positives

A tester may warn about relay when nothing is wrong. The classic combination:

  • A default domain is configured, so a recipient given without a domain (RCPT TO:<webmaster>) has the default domain appended — [email protected].
  • That domain has a catch-all address, so every address in it is accepted.

Put together, your server accepts RCPT TO:<any-random-string> — and the tester flags it. But the mail is delivered to your own catch-all mailbox, never relayed onward, so it is not an open relay. It is still worth asking whether you want both settings: a catch-all plus a default domain accepts a great deal of junk addressed to nobody.

If the test genuinely fails

  1. Sign-in & access → IP ranges — find the range that permits unauthenticated external-to-external delivery and remove that permission, or require SMTP authentication for it as the shipped ranges do. This is the fault.
  2. Empty the delivery queue of spam (Monitoring & troubleshooting → Delivery queue).
  3. Check your IP against the major blacklists at mxtoolbox and use the delisting forms — after the fix, not before, or you will be relisted immediately.

32.8 Codes in the error log

The error log prefixes its own numbers with HM — HM6364, HM10045. Most are internal, but these are the ones worth recognising on sight.

Code What it means What to do
HM6364 Assertion failed: <expression> (<file>, line <n>). This binary keeps its assertions; the server carried on past this one. Severity Critical You are running a diagnostic build. Only the assertion-enabled build can report this; the binary that ships never does. Report it with the expression, file and line, and move back to a release build for production
HM5528 PreferredHashAlgorithm is set to a value that is not a password-hashing scheme this server will store a new secret under PBKDF2 (4) is used instead. Set it deliberately to 3, 4, 5 or 7 (§17.2)
HM5607 PreferredHashAlgorithm is 5 (Argon2id) but this build’s OpenSSL has no Argon2id KDF scrypt (7) is used instead. OpenSSL 3.2 or later has Argon2id
HM5562 SpamAssassinUser contains a control character and has been ignored No User: header is sent to spamd. Fix the value on Spam & virus filtering → Anti-spam settings, SpamAssassin tab (§10.3), or over REST or COM, and it applies when saved; on Linux hmailserver --set-setting stores it too, and a running service takes it at its next save, reinitialise or restart. Since 6.3.4 the database holds it, and a line edited in hMailServer.ini is reported as HM5804 and not applied
HM6410 — HM6413 Linux only. The stored-secret key file could not be created (6410), its mode lets other accounts read it (6411), it could not be opened or is not exactly 32 bytes (6412), or a secret could not be protected (6413) The message names the chmod that fixes 6411. After a restore, the does not exist form of 6412 means the key file was not restored with the data directory — without it every stored password has to be re-entered (chapter 15)
HM6414 Linux only. A stored secret is a Windows DPAPI blob, which nothing on Linux can open This is what an INI carried across from a Windows installation produces. Re-enter the password in plain text
HM6416 Linux only. A secret could not be protected at all It is not stored. On Linux this build never falls back to the old fixed-key scheme on its own — fix the store rather than working around it. (On Windows a DPAPI failure falls back to that scheme instead, so that no secret is lost.)
HM5804 A [Settings] line in hMailServer.ini was changed away from the value the server last wrote there. Since 6.3.4 the database holds these settings and the file is its copy The stored value is used, the key is named and, where the service may write the file, the line is put back. Make the change in the Control Panel, over REST or COM, or on Linux with hmailserver --set-setting (chapter 21). On a warm standby, or after rolling back a database migration, it is the file’s copy catching up, not a fault
HM5807 hMailServer.ini has a [SettingsOverride] section. Named at every start and reinitialise for as long as the section is there Its values are used over whatever is stored. It is the deliberate way round a database that cannot be read; take it out once it is no longer needed, because a value saved for a key it forces is stored but not in effect until the override is removed (HM5808)
HM7495 At a start (new in 6.3.4): the database records a stored-secrets key, or holds values sealed under one, and this server has no key, a key line that does not open here, or a key that is not the database’s. Said at each start until it is mended. Beside it the error log names what will not open: HM7492 once for each [Settings] secret, and HM7493 once for the second factors, naming the first account met. /readyz still answers 200 on such a server (§33.7) Mail is still served, but every second factor sealed under that key refuses every code, and the sealed [Settings] secrets read as not set, so SRS, BATV and HS256 tokens fail, and a /metrics protected by a token or password answers 503, or, on a loopback address, answers without a credential. Stop the service, import the key exported where it was made (hMailServer.exe --import-stored-secrets-key FILE from an elevated prompt, sudo hmailserver --import-stored-secrets-key FILE on Linux) and start it again. A server that made a key of its own needs its [Security] StoredSecretsKey line removed first; never copy that line from another machine, where it is sealed under that machine’s own DPAPI or key file
HM5018 On Windows, a rule’s Run function action names a function that is not a plain identifier (ASCII letters, digits, _ and $, not starting with a digit): refused since 6.3.4, each time the rule matches. Reported once per load instead, it means an event reached a script loaded in a language this build does not run, or a call built for VBScript met JScript after a reload, or the reverse The function is not run. Give it a plain name, in the event script and in the rule (§27.3). On Linux such a name writes a Script Error line to the error log instead
HM5912 A route or the SMTP relay whose host is on OutboundOAuth2Hosts would have sent its OAuth2 token without TLS: its connection security is none, or STARTTLS optional against a server that does not offer it (new in 6.3.4) Nothing is sent: the delivery fails with a temporary 450 4.7.0 and the message waits for the retries the route allows. Set the connection security to SSL/TLS or to STARTTLS required (chapter 14)
HM10045 Unknown error during a SQL Server Compact schema upgrade on 6.2.25 See §32.2 — the upgrade succeeded; the verification was the defect. Fixed in 6.2.26

33. Operations cookbook

Installing a mail server is a day’s work. Running one is a decade’s. This chapter is a set of recipes for the jobs that surface only after months of uptime: knowing what is actually on the disk, moving to new hardware, outgrowing the built-in database, keeping the database healthy, automating the routine work, running on a connection that was never meant to host a mail server — and, at the other end of the scale, running a standby machine so that losing one is survivable. Each recipe stands alone — read the one you need.

33.1 The installation on disk

Everything hMailServer owns lives under the installation directory — C:\Program Files\hMailServer by default. Knowing what each part is tells you what to back up, what to prune, and what you can always get back from the installer.

Path What it is Back up?
Bin\ The programs: the server itself (hMailServer.exe) and the database tools (DBSetup, DBSetupQuick, DBUpdater), plus the bundled database client libraries — MariaDB Connector/C (as libmysql.dll), PostgreSQL’s libpq and OpenSSL No — reinstallable
Bin\hMailServer.INI The configuration kept out of the database (chapter 21): the database connection, the directories, the administrator password, the [LDAP], [SendingLimits] and [SendingLimitsOverrides] sections, and PasswordPepper. Since 6.3.4 every other [Settings] key is stored in the database and the file’s [Settings] section is the server’s copy of them. Once the installation has one, the file also holds the stored-secrets key, sealed under this machine’s DPAPI on Windows, so a copy of the file is no copy of the key (§33.4) Yes
Bin\hMailServerApiKeys.ini The REST API’s key store (chapter 24): a SHA-256 digest of each key, written by the service when a key is made or revoked Yes, if you use API keys
ControlPanel\ New in 6.2. The .NET 10 Control Panel (hMailCP.exe), which replaced the retired Administrator No — reinstallable
Addons\ The Data Directory Synchronizer, a wizard that reconciles database rows with the files under Data\, and — since 6.2.12 — the Import Tool (§33.8) No — reinstallable
Data\ The mail itself. Files in the root of Data are messages not yet delivered — the queue. Subfolders hold delivered mail, organised by domain and account, beside stores of the server’s own such as Blobs, Discovery, Files, Lists and Recoverable; since 6.3.4 a new domain may not be named after one of them Yes — irreplaceable
Data\ACME\ The ACME account key and automatically issued certificates Yes — it travels with Data\
Database\ hMailServer.sdf, the built-in database Yes, if you use the built-in engine
DBScripts\ The SQL scripts that create and upgrade the schema. Upgrade scripts are named UpgradeXXXXtoYYYYZZZZ.sql — old schema, new schema, database engine No — ships with the installer
Events\ Your event scripts — the code behind Maintenance → Event scripts Yes — it is your code
Languages\ Translations for administration text, loaded by the server at startup and served to admin clients over the COM API No — reinstallable
Logs\ Log files, controlled by Monitoring & troubleshooting → Logging Optional — useful for audit, regenerable otherwise
Temp\ Scratch space — for example, attachments extracted as .tmp files for virus scanning No — safe to empty with the service stopped, but leave Temp\IMAPAppend: since 6.3.4 its markers let the next start remove the file of an IMAP APPEND a crash or a power cut interrupted
WebAdmin\ New in 6.x. The Control Deck: index.html, which the REST API listener (chapter 24) serves at its root, and, since 6.3.4, the Deck’s language catalogues in languages\. No relation to the removed PHP WebAdmin No — reinstallable

Message files carry the .eml extension; an installation with a long history may also hold .hma files written by much older versions. Both are ordinary mail messages and both are still served. Since 6.3.4 that holds while MessageStoreCompression is off, as shipped. With it on, a message stored in a mailbox is a deflated container rather than plain RFC 822, each attachment of at least MessageStoreDedupMinKB (256 KB) is kept once under Data\Blobs, and every client still sees the message as it arrived; the queue is never compressed. A script that opens the path Message.Filename gives over COM, or anything else outside the server that reads the files, reads the container: read the message through the API instead, or turn compression off, stop the service and run hMailServer.exe --compress-store --decompress to put the files back as plain RFC 822.

Two things live outside this tree: an external database, if you use one (§33.3), and your DKIM key files, which sit wherever you put them when you generated them — chapter 15 reminds you to back both up. Since 6.3.4 a domain whose DKIM keys rotate keeps them under Data\DKIM instead. One more file belongs outside it: the one you export the stored-secrets key to (§33.4), kept with the database backups but stored apart from them.

Never delete message files by hand. Every file under Data has a matching row in the database. Delete the file and the row points at nothing; delete the row and the file is orphaned on disk. Remove mail through a client, a rule or the API, and let the server keep the two sides in step. MessageStoreConsistencyCheck set to 1 (Maintenance → Server limits & expert settings, card Message store durability) makes the server cross-check rows against files periodically and report drift.

Leftovers from 5.x. An installation upgraded from the original hMailServer may still carry pieces this fork no longer uses — a PHPWebAdmin folder (the PHP web interface, removed for good reasons in chapter 25) and the retired Administrator’s hMailAdmin.exe in Bin. They are inert. Once you have a verified backup, they can go. Do not confuse them with two folders that are current: Languages is still shipped and loaded, and the fork’s own WebAdmin (table above) shares nothing but a name with the old PHP interface.

33.2 Moving hMailServer to a new machine

Hardware ages, Windows versions expire, and sooner or later the server must move. The move is a restore onto fresh metal — which means chapter 15 is a prerequisite, and the first time you do this it should be a rehearsal onto a test machine, not the real thing under pressure.

The plan

  1. Check the new machine against the chapter 3 checklist — 64-bit Windows, always on, and a disk sized for the mail store you actually have now, not the one you had at installation.
  2. Install the same version of hMailServer, in the same path. If the old server runs 6.3.4 in C:\Program Files\hMailServer, install exactly that on the new one. A different version or a different directory is where migrations go wrong. Since 5.4 a message row holds a bare file name and the path is rebuilt from the data directory; a database from before 5.4 can still hold full paths. A new 6.3.4 installation runs the service as NT SERVICE\hMailServer, which reaches only the folders it is granted, where the old server may still run as LocalSystem. After the restore, run Grant access (Maintenance → Server limits & expert settings, card Windows service account): it names every certificate or DKIM key file outside those folders, which the service cannot read. Move such files into Data\Certificates or approve their folder for Read. The archive you restore from is read the same way, so keep it in a folder the service may read. LocalSystem, which the installer’s service-account page also offers, reads them wherever they are, but the setup checklist reports it as action needed.
  3. Back up the old server. Use Maintenance → Backup & restore with everything selected — remember the built-in backup does not include messages unless you tick the option (chapter 15). Let the delivery queue empty first: mail still in it is not carried by the archive. For a large store, the external-tools approach in §33.5 is faster: stop the service, copy Data\, dump the database natively, and take hMailServer.INI, Events\, your DKIM keys and any certificate and key files kept outside Data\ — and, since 6.3.4, the stored-secrets key, exported with hMailServer.exe --export-stored-secrets-key FILE from an elevated prompt (see the caveat below).
  4. Stop the old server for good. Once the backup is taken, stop the hMailServer service on the old machine and set it to Disabled. Anything it accepts after this moment is not in your backup and will be stranded.
  5. Restore on the new machine — data directory and database, in the order chapter 15 gives. Do not copy the old hMailServer.INI over the new one: its database password and, since 6.3.4, its stored-secrets key line are sealed under the old machine’s DPAPI and open nowhere else. Keep the new file’s own [Database] and [Directories], and carry across what only the file holds: PasswordPepper, if one was set, and the [LDAP], [SendingLimits] and [SendingLimitsOverrides] sections if you use them. Since 6.3.4 the [Settings] keys arrive with the database.
  6. Re-enter protected secrets. See the caveat below.
  7. Fix up the network. See the checklist below.
  8. Confirm, then decommission. Send a message in from an outside address and out to one; log in over IMAP; watch Monitoring & troubleshooting → Server status and Monitoring & troubleshooting → Live logs. Keep the old machine powered off but intact for a week or two — it is your rollback.

The DPAPI caveat, again. As chapter 15 explains, with ProtectStoredSecretsWithDPAPI=1 (the default) stored passwords are encrypted with machine-scoped Windows DPAPI and cannot be decrypted on the new machine. Which of them come across depends on how the data moved. The database connection password in a copied hMailServer.INI never does: keep the new file’s own [Database] section, as step 5 says, or re-enter the password — for an external database, run Bin\DBSetup.exe to do it (§33.3 says what that needs under NT SERVICE\hMailServer). Route, smart-host relayer and external-account passwords and certificate key passphrases come across through the built-in backup and restore, which carries them in a portable form and protects them again on the new machine; after a database copied with its own tools, re-enter them in the Control Panel (a certificate’s passphrase on Certificates & encryption → SSL certificates). Each domain’s own relay settings (host, port, user name, password and whether it requires authentication) are in no archive, so re-enter them after a restore, on the Relay tab of the domain’s dialog; a copied database brings them but not the password, so re-enter that. The same goes, since 6.3.4, for the backup passphrase, which is in no archive and does not open on another machine: set it again on the new machine (Maintenance → Backup & restore, Encryption), or after a move through an archive its backups are written unencrypted, and after a copied database they fail. The SNMP agent’s passwords, the object store’s secret key and the sandbox’s bearer token are in no archive either; enter them again after a restore, and place any eDiscovery legal holds again, since a server rebuilt from an archive has no case and no hold. If you would rather have portable database copies, chapter 15 describes the trade-off of turning the setting off. Budget for this step; a migration that forgets it produces a server that cannot reach its own database.

Since 6.3.4, the stored-secrets key. Every account’s second factor and the [Settings] secrets (SRSSecret, BATVSecret, OAuth2HmacSecret and the rest) are sealed in the database under the installation’s stored-secrets key, once it has one, and the key is in no database and no backup. A move through the built-in backup does not need it: the archive carries those secrets opened, and the restore seals them under the new machine’s own key. A database moved with its own tools does. Before the new machine’s service first starts on it, remove the [Security] StoredSecretsKey line the new machine’s installer made, then import the file exported in step 3 (or the one exported when the key was made) with hMailServer.exe --import-stored-secrets-key FILE from an elevated prompt. Never copy the line from the old machine. Without the key the error log says HM7495 at every start, every second factor refuses every code, and those settings read as not set.

A Linux-to-Linux move is easier here and not elsewhere: the secrets are encrypted under <DataFolder>/.hmailserver-secret-key, so copying the data directory and the database together carries the passwords with them. Copy that file with everything else, preserving its 0600 mode and its ownership. Since 6.3.4 the stored-secrets key line in /etc/hmailserver/hMailServer.ini is sealed under that file too, so bring the configuration across with it, or export and import the key with sudo hmailserver --export-stored-secrets-key FILE and sudo hmailserver --import-stored-secrets-key FILE.

Network and DNS follow-ups

Check Why
Keep the host name if you can If mail.example.com simply resolves to a new IP, your MX, certificate and client configurations all survive unchanged
Update the A record Point the host name at the new IP; lower its TTL a day before the move so the change propagates quickly
PTR record The new IP needs reverse DNS — ask the new machine’s provider (§3.4). Without it, deliverability drops immediately
Windows Firewall The installer does not open ports; re-run the rules from §4.4
Router / cloud port forwards Point them at the new machine
SpamAssassin and ClamAV If they run on other hosts, confirm the new server can reach them
Data\ACME If it came across with Data\, your certificates and keys are intact. If not, ACME re-issues — but a new key invalidates any published TLSA records (chapter 9)
Monitoring & troubleshooting → Diagnostics Run the self-test last, with the local-domain and test-domain boxes filled in: left empty, the MX and local-connect tests are skipped silently, and the outbound test too when no smart host is set. It checks the installation paths, the outbound connection (port 25, or the smart host), the backup destination, your MX records and a connection in to them, the message file locations, whether localhost has been auto-banned, and any error log; the PTR record and the scanners you check yourself

Finally, tell your users — only if something they see has changed. If the host name survived the move, nobody needs to know it happened.

More: Moving to new hardware, and why Windows to Linux is a rebuild on the forum.

33.3 Switching database engines

The built-in database (SQL Server Compact) is the installer’s default: it runs inside the hMailServer process and needs no external software. Microsoft stopped maintaining it long ago, and it has hard limits that one day you may hit.

When to switch

  • The 4 GB ceiling. Database\hMailServer.sdf cannot grow past 4 GB. That is room for references to millions of messages, but a busy server gets there.
  • Recoverability. If an .sdf file corrupts — power loss, disk failure — there is very little tooling to repair it. The server engines have mature backup and repair tools.
  • Scale. Hundreds of thousands of messages, or many accounts, run faster on a real server engine.
  • Integration. If you report on or integrate with the mail database, you want an engine with proper client tooling.

The choice of target follows §3.5: MySQL/MariaDB for most production use (the client library is bundled), Microsoft SQL Server — including Express — if you already run it, PostgreSQL for larger installations, or if you already know and like it.

The procedure

The built-in backup is the vehicle: it captures domains, accounts, settings and messages in an engine-neutral form, and restores them into whatever database the server is then connected to.

  1. Prepare the target engine. Install it, create an empty database, and create a user with full rights on that database only. Since 6.3.4 (schema 6102) every MySQL and MariaDB table is utf8mb4, which needs MySQL 5.7.9 or MariaDB 10.2.2 or later; make the database utf8mb4 too (CREATE DATABASE hmailserver CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;). For SQL Server with Windows authentication on the same machine, a service that runs as NT SERVICE\hMailServer needs a login of that name (CREATE LOGIN [NT SERVICE\hMailServer] FROM WINDOWS) and a user in the database with db_owner.
  2. Back up everything — Maintenance → Backup & restore, messages included (chapter 15). Take this backup at a quiet moment, immediately before the switch — nothing that arrives between backup and switch comes across. Let the delivery queue (Monitoring & troubleshooting → Delivery queue) empty first: mail still in it is not carried by the archive, and nothing queues it again after the restore. It is both the migration vehicle and the safety net.
  3. Run Bin\DBSetup.exe. Since 6.3.4 a service that runs as NT SERVICE\hMailServer, a new installation’s account, may not write hMailServer.INI, and DBSetup’s change to [Database] is refused before anything is made, so first move such a service to LocalSystem (Maintenance → Server limits & expert settings, card Windows service account, Move to LocalSystem, then restart the service, which keeps its old account until it restarts). Point DBSetup at the new engine; it creates the hMailServer schema there and updates the connection settings in hMailServer.INI. The schema is created at the current version (6108 at 6.3.8 — chapter 18), so DBUpdater is not involved. Restart the service when it finishes, so the server comes up connected to the new database, and afterwards move it back with Move to NT SERVICE\hMailServer and another restart.
  4. Restore the backup through Maintenance → Backup & restore (an encrypted archive asks for its passphrase). Then enter again what no archive carries: each domain’s own relay settings (host, port, user name, password and whether it requires authentication), on the Relay tab of the domain’s dialog in Accounts & domains → Domains, and, since 6.3.4, the backup passphrase (Encryption, on the same backup page), the SNMP agent’s passwords and community, the object store’s secret key or SAS, and the sandbox’s bearer token. The new database holds none of them, and until the passphrase is set again every backup is written unencrypted, with the server’s stored secrets and every account’s second factor in the clear. Place any eDiscovery legal holds again as well: the new database has no case and no hold.
  5. Verify. Run Monitoring & troubleshooting → Diagnostics, check Monitoring & troubleshooting → Server status, send a message in and out, and spot-check a mailbox over IMAP.

Three things worth knowing:

  • Your messages do not move. The Data\ directory stays exactly where it is; only the rows referencing it change homes.
  • Same hMailServer version throughout. An engine switch is not the moment to upgrade as well. One change at a time.
  • The new database password is a stored secret, protected by DPAPI like the rest (chapter 15).

Keep the old Database\hMailServer.sdf untouched until you are confident, then archive it with your backups.

33.4 Database maintenance

The database needs less attention than people expect — hMailServer creates and upgrades its own schema, and never needs manual table surgery. What it does need is engine-native care, an eye on growth, and one obscure ceiling checked once a year.

Engine-native care

Engine Backup Integrity
Built-in (SQL CE) The built-in backup (chapter 15) — there is no separate tooling Few options if the file corrupts; this is a reason to switch (§33.3)
MySQL / MariaDB mysqldump on a schedule CHECK TABLE; OPTIMIZE TABLE occasionally after large deletions
SQL Server Native BACKUP DATABASE or a maintenance plan DBCC CHECKDB; index and statistics maintenance
PostgreSQL pg_dump Autovacuum handles routine upkeep; leave it on

Since 6.3.4, keep the stored-secrets key with these backups. Every account’s second factor and the [Settings] secrets are sealed in the database under the installation’s stored-secrets key, once it has one: the Windows installer makes it on a new installation, while an upgraded installation, and a Linux server installed from the packages, has none until an administrator runs --create-stored-secrets-key (sudo hmailserver --create-stored-secrets-key on Linux). The key is never in the database or in any backup, so a dump, a SQL Server backup or a copy of hMailServer.sdf restored without it brings back every second factor and [Settings] secret unopenable. Export it once, from an elevated prompt beside hMailServer.exe, with hMailServer.exe --export-stored-secrets-key FILE (sudo hmailserver --export-stored-secrets-key FILE on Linux), and keep the file with the database backups, stored apart from them. The built-in backup does not need it: it carries those secrets opened.

Between the database and the message store, MessageStoreConsistencyCheck set to 1 (Maintenance → Server limits & expert settings, card Message store durability) has the server periodically confirm that message rows and message files still agree — cheap insurance after any crash or restore.

What is safe to prune

Safe to remove Never remove by hand
Old logs — better, let LogDeleteDays do it (chapter 16) Files under Data\ — every one is referenced by a database row (§33.1)
The contents of Temp\, with the service stopped, except Temp\IMAPAppend (§33.1) Rows in the hm_ tables — the server owns its schema
Backup archives you have superseded (keep several generations) Lines in hMailServer.INI you do not recognise
5.x leftover folders, after a verified backup (§33.1) The delivery queue — the files in the root of Data\ are undelivered mail

The honest answer on database size is that it tracks message count. The levers are account quotas (MaxSize), public-folder discipline, and users who never empty Trash — not database tricks.

The folder-UID ceiling

Every message added to an IMAP folder takes the next value from that folder’s UID counter — the foldercurrentuid column in hm_imapfolders — and copies count, so a message delivered to an inbox and later copied to another IMAP folder has consumed a UID in each folder it touched. IMAP requires these identifiers to fit in 32 bits — below roughly four billion — and the server holds them as exactly that. The ceiling is per folder, not per server: a folder through which a couple of billion messages have ever passed is past the halfway mark, however small the store looks today, and IMAP-heavy use gets there faster.

Check where you stand by running this against the hMailServer database:

SELECT MAX(foldercurrentuid) FROM hm_imapfolders;

Almost every installation will find the answer laughably far from four billion — this is a large-hosting concern, not a small-office one. If yours is genuinely approaching it, note that a rebuild does not reset the counters: the built-in backup carries each folder’s UIDNEXT and each message’s UID, and a restore into a freshly created database puts them back (chapter 15), so a rebuild is no remedy. Plan any change to such a folder as a maintenance window, not an emergency.

33.5 Automating maintenance

Before scripting anything, note what the server already automates: LogDeleteDays prunes logs, ACME renews certificates, auto-ban handles password guessers, and the backup has a scheduler of its own — ScheduledBackupTime for a daily run or ScheduledBackupIntervalMinutes for an interval, with ScheduledBackupKeepCount and ScheduledBackupMaxAgeDays for retention (§15.2). Since 6.3.4 the server also raises alerts: they are on, with seven of their eleven conditions, backup.failed and certificate.expiring among them, but nothing is mailed until AlertRecipient and AlertSenderAddress are set. What scripts are still for is the things the server has no opinion about: copying the archive off the machine, reports, and the odd repair.

Anything the Control Panel can do, a script can do, over the same COM API (chapter 23). On Windows the REST API (chapter 24) is an equally capable alternative; on Linux, where there is no COM and no Control Panel, it and the Control Deck are the only routes there are — so write portable automation against the API rather than against COM. Since 6.3.4 two command-line clients speak it on either platform: hmctl, which the Linux packages install, and the HmailServer PowerShell module; hmctl backup start runs the server’s own backup. For anything unattended an API key is a better credential than the administrator password: it can be read-only, restricted to one domain and revoked on its own.

A scheduled backup script

The BackupManager object starts the backup you have configured in Maintenance → Backup & restore. A task that starts it runs the backup on a schedule of your own instead of the built-in one, and can then copy the archive off the machine.

# hms-backup.ps1 — start the configured hMailServer backup
try {
    $app = New-Object -ComObject 'hMailServer.Application'
    $app.Authenticate('Administrator', 'your-admin-password') | Out-Null
    $app.BackupManager.StartBackup()
}
catch {
    Add-Content 'C:\Scripts\hms-backup-failures.log' "$(Get-Date -Format s)  $_"
    exit 1
}

Register it with Task Scheduler from an elevated PowerShell:

$action  = New-ScheduledTaskAction -Execute 'powershell.exe' `
           -Argument '-NoProfile -ExecutionPolicy Bypass -File C:\Scripts\hms-backup.ps1'
$trigger = New-ScheduledTaskTrigger -Daily -At 02:30
Register-ScheduledTask -TaskName 'hMailServer backup' -Action $action `
           -Trigger $trigger -User 'SYSTEM' -RunLevel Highest

The script contains the administration password. Keep it in a directory only Administrators and SYSTEM can read, and never in a location that syncs or gets committed anywhere.

Check for failure, every run. Since 6.2.10 a refused API call fails properly instead of reporting success (chapter 23 has the history), so a wrong password or insufficient rights now raises an error your catch block will see. An unattended script without error handling is a backup that silently stopped months ago.

For large mail stores, wrap the built-in backup with an external copy — or replace it, backing up the data directory separately as chapter 15 suggests: a nightly robocopy mirror of Data\ while the server runs, then a brief service stop and a second robocopy pass that copies only the delta. That keeps the consistent-copy window to seconds. Read robocopy’s switches carefully first; its mirror mode deletes as well as copies.

Repair recipe: updating folder UID counters

One specific symptom — new messages not appearing in IMAP folders until the service is restarted — has one specific fix: bringing each folder’s UID counter back up to the highest message UID it actually contains.

' Update IMAP folder UID counters.
' Run ONLY for the symptom described above, and back up the database first --
' this touches the counter of every folder. The script stops and restarts the server.
Dim app
Set app = CreateObject("hMailServer.Application")
Call app.Authenticate("Administrator", "your-admin-password")
Call app.Stop()
Call app.Utilities.PerformMaintenance(1)   ' 1 = update IMAP folder UIDs
Call app.Start()
MsgBox "Finished"

Save it as a .vbs file, fill in the password, and run it by double-clicking. It is a repair tool, not routine maintenance — do not schedule it.

33.6 Running on a dynamic IP or home connection

Chapter 3 told you to get a static IP, and chapter 3 was right. But a home lab, a hobbyist domain or a test bed can run on a dynamic address if you go in with accurate expectations.

You want to On a dynamic IP
Receive mail directly Works, if your ISP leaves inbound port 25 open — with brief gaps when the address changes
Send mail directly Barely. Residential ranges are pre-listed on blacklists, and the PTR record is your ISP’s generic name, which you cannot change (§19.4)
Send through a smart host Works well. This is the configuration to run

Dynamic DNS

Your MX record must point at a host name, and that host name must always resolve to your current address. A dynamic-DNS provider gives you both: a host name, and an update client — run on the server or, better, on the router — that rewrites the A record whenever your ISP moves you.

  1. Register a host name with a dynamic-DNS provider and get its updater running.
  2. Point your domain’s MX record at that host name (chapter 8 covers the mechanics).
  3. Forward port 25 — and the client ports you use — from the router to the server, and open Windows Firewall (§4.4).
  4. Set the host name on the SMTP tab of Connections & protocols → Protocols so the server introduces itself consistently.

When the address changes, there is a window where the record is stale. Sending servers retry failed deliveries for days, so a prompt updater turns an IP change into a delay of minutes, not lost mail. A broken updater, on the other hand, turns it into a silent outage — monitor it.

Sending: use a smart host

Do not fight the reputation system; route around it. Configure a smart host in Mail flow & delivery → Delivery of e-mail (Relayer tab) exactly as chapter 14 describes — your ISP’s SMTP server, or a relay service such as SendGrid, Mailgun or Amazon SES — and let its static, well-kept IP do the talking.

Two DNS consequences:

  • SPF must authorise the relay, not your home IP — use the include: the relay service documents.
  • DKIM stays yours. Sign your own mail (chapter 8); the signature survives the relay and proves the mail is really from your domain regardless of whose IP delivered it.

Certificates

ACME’s http-01 challenge needs inbound port 80 forwarded to the server, and the dynamic-DNS host name listed in AcmeDomains (chapter 9). That done, certificates renew themselves like on any other installation — have your clients connect using that host name so the certificate always matches.

Know when to stop. A dynamic-IP installation is a fine lab and an acceptable personal server. The moment lost or junked mail costs you money, the arithmetic changes: a small VPS with a static IP, open port 25 and a proper PTR record costs a few pounds a month and removes every limitation in this section. The chapter 3 checklist was written for that server.

33.7 A warm standby: active/passive high availability

New in 6.x. hMailServer contains no clustering code, and that is a decision, not a gap: a single active node cannot split-brain. What the fork supports — and documents in the repository’s HighAvailabilityRunbook.md — is an active/passive pair: two identically configured machines, exactly one of which runs the hMailServer service at any moment, with failover performed by the infrastructure around the server rather than by the server itself.

Four pieces are shared:

Piece Requirement
Database An external engine (§33.3) — the built-in database is local-only and disqualifies itself. Both nodes carry identical [Database] settings in hMailServer.INI
Message store Both nodes point at the same Data directory on shared storage — SAN, NAS or a cloud file share — with identical access for both service accounts. Keep it on the same failover boundary as the database; the rows and the files must move together
Virtual IP Clients connect to a floating VIP or load balancer, never to a node’s own address
Health check The VIP routes on the /readyz probe — 200 only when the server is running and the database is connected

Enable the probes on both nodes. Since 6.3.4 these are stored settings, kept in the shared database, not lines to edit in hMailServer.INI: set them in the Control Panel, over REST (PUT /api/v1/settings/ini/{name}), with hmctl, or on Linux with hmailserver --set-setting NAME=VALUE, then reinitialise (Apply now) or restart the service, because the listener binds its port and address when it starts. They are shown here in the [Settings] form they have always been written in. On a non-loopback bind /metrics also wants MetricsServerAuthToken, though the probes answer without it:

[Settings]
MetricsServerPort=8080
MetricsServerBindAddress=0.0.0.0
MetricsServerAuthToken=<32+ random characters>
ShutdownDrainSeconds=30

The passive node’s service stays stopped (set it to Manual), so its /readyz refuses connections and it is never routed to. On a graceful stop, the active node flips /readyz to 503 first, stops accepting connections and pauses delivery, then waits up to ShutdownDrainSeconds for the sessions and deliveries in flight before its listeners come down. Since 6.3.4 a planned failover can drain before anything stops: POST /api/v1/server/drain on A closes its SMTP, IMAP, POP3 and ManageSieve listeners, pauses its delivery queue and answers /readyz with 503 while what is in flight finishes, and GET /api/v1/server/drain says drained is true when a stop would be instant. Then stop A, watch its /readyz go dark, move the VIP, start B, wait for its /readyz to return 200, check B’s error log for HM7495, HM7492 and HM7493 (a node without the stored-secrets key still answers 200), and send a test message through. Reverse the steps to fail back.

Fence before you fail over. In an unplanned failover, power off or isolate the failed node from the storage and database network before starting the standby. Two nodes writing the same message store is the split-brain this design exists to prevent, and it corrupts mailbox state. One more habit: configuration made in the Control Panel lives in the shared database, so the standby inherits it, and since 6.3.4 that includes every [Settings] key but PasswordPepper. The [Settings] section of B’s own hMailServer.INI is only a copy: B’s start names each setting changed since as HM5804, which is the copy catching up, not a fault, so do not edit it to match. What is per node is yours to keep in step: [Directories], [Database] and [Security]; PasswordPepper, which must be the same on both or no account whose password is an Argon2id or scrypt hash signs in on B; and, since 6.3.4, the stored-secrets key, carried with hMailServer.exe --export-stored-secrets-key FILE on A and --import-stored-secrets-key FILE on B (if B’s installer made a key of its own, remove B’s [Security] StoredSecretsKey line first, or the import is refused), never by copying its line. Do not copy A’s INI over B’s wholesale: its database password and that key line are sealed under A’s own DPAPI. And the passwords the server has to use rather than check — the smart host, routes, external accounts, the per-domain relay, and certificate key passphrases — are DPAPI-protected in the shared database, so re-enter them on B after a failover, and on A after failing back. The backup passphrase, the Web Push key, the import and single sign-on client secrets and the sandbox token are kept the same way; while a backup passphrase is set, B’s backups fail until it is entered again there.

More: Warm standby and high availability: what a second machine can and cannot do on the forum.

33.8 Importing accounts and mail: the Import Tool

New in 6.2.12. Addons\ImportTool replaces the old VB6 migration wizards with one supported tool for the two imports that still matter when mail moves into hMailServer from somewhere else:

  • Accounts from a text file. One account per line, comma-separated — the same format the old wizard used. Each line is validated individually, and the run ends with a created/updated/failed summary instead of stopping at the first malformed line.
  • Messages from mbox files. Each mbox file becomes one IMAP folder in the account you choose, streamed through the COM API (Utilities.ImportMessageFromFileToIMAPFolder, chapter 31) — so files of any size work, and the imported messages are ordinary mail the server indexes like any other. Unix (LF) and Windows (CRLF) mailboxes are both handled, as are mboxrd >From quoting and dot-stuffed copies saved from SMTP sessions — all documented defects of the VB6 tool, all fixed.

Maildir, since 6.2.27. Point the tool at the directory holding cur, new and tmp and it imports the INBOX and every Maildir++ folder beside it, taking the flags the file names carry — ;2, and !2, are accepted as well as :2,, since a colon cannot appear in a Windows file name — and normalising line endings to CRLF. An import into a folder a client already has open now appears at once: the folder is refreshed, as it is after a delivery.

mbox is the lingua franca of mail exports: Thunderbird stores folders as mbox natively, Google Takeout delivers Gmail as one, and every Unix mail system can produce one. For .eml exports — one file per message — script ImportMessageFromFile directly; chapter 31 shows how. Under NT SERVICE\hMailServer, a new 6.3.4 installation’s service account, the server reads such a file only where that account may, so put it in a folder the service has been granted (§17.3).

For the wider job — moving from another IMAP server, from mbox, from a Maildir, from Outlook, from the upstream hMailServer in place, or accounts in bulk — the repository’s Migration.md is the reference. Mirroring folders over IMAP (§29 FetchAccount.MirrorFolders) is usually the least painful route into hMailServer from a live server. Since 6.3.4 two more routes are built in. A migration copies many mailboxes from another IMAP server, Google Workspace or Microsoft 365 as jobs, one per mailbox, throttled and resumed without copying anything twice, with contacts and calendars beside the mail. And the server reads Outlook data files (PST) itself, as a migration of kind pst: copy the file into <PstImportDirectory>\<domain>\ (the setting is empty as shipped, so no PST job runs until an administrator names a directory) and start the job over REST (POST /api/v1/migrations), with hmctl or from the Control Deck. The Import Tool does not read PST files, and Outlook over IMAP remains the route for a file the reader refuses.

The tool runs on the server itself — it authenticates to the COM API as an administrator — and needs the .NET 10 Desktop Runtime the installer already provides. Re-running an account import is safe: existing accounts are reported as updated, not duplicated.

More: Bringing mailboxes in: IMAP mirror, Google, Microsoft 365, mbox, Maildir and PST on the forum.


34. Standards, protocols and interoperability

Email works because everyone implements the same RFCs. This chapter is the inventory: which standards hMailServer implements, how to see for yourself what your server advertises, the IMAP behaviours worth knowing before your users ask, where IPv6 support stands, and answers to the questions the rest of this documentation does not cover.

Configuration lives elsewhere — chapters 8, 9, 10 and 17 tell you how to turn things on. This chapter is the map of what is there.

34.1 Supported RFCs

Everything below is built in. The SMTP and IMAP extensions are advertised automatically the moment a client says EHLO or asks for CAPABILITY — for most of them there is nothing to enable. (The IMAP IDLE, SORT, QUOTA and ACL toggles on the IMAP tab of Connections & protocols → Protocols exist, but all four are on by default. Off as shipped, among others: ManageSieve, noted below; XCLIENT; and, on IMAP, the SASL mechanisms (AUTH=PLAIN, AUTH=SCRAM-SHA-256 and its -PLUS form) and SASL-IR, which Allow SASL PLAIN authentication and Allow SASL initial client response on the same tab turn on. Until then an IMAP client signs in with LOGIN.)

Core protocols

RFC Standard Notes
5321 SMTP Server-to-server delivery, port 25
6409 Message submission Authenticated sending by your users, port 587
5322 Internet Message Format The syntax of the messages themselves
3501 IMAP4rev1 The default IMAP dialect
9051 IMAP4rev2 New in 6.x. Always advertised; a client opts in per session with ENABLE IMAP4rev2
1939 POP3 Download-and-delete mail access

Calendars and contacts. Beside the mail protocols, the web services listener, off until it is given a port, serves each account’s address books over CardDAV (RFC 6352, since 6.3.3) and, new in 6.3.4, its calendars and tasks over CalDAV (RFC 4791), on an iCalendar (RFC 5545) reader and writer of the project’s own. Clients find both through /.well-known/carddav and /.well-known/caldav, and both are served over HTTPS only. Invitations travel by mail as iMIP (RFC 6047).

SMTP extensions

RFC Extension What it adds
3207 STARTTLS Upgrades a plaintext session to TLS
4954 AUTH SASL authentication for submission
2920 PIPELINING New in 6.x. Batches commands into fewer round trips
6152 8BITMIME New in 6.x. 8-bit message content without re-encoding
3030 CHUNKING / BDAT New in 6.x. Transfers message data in binary-safe chunks
3461 DSN New in 6.x. Senders can request delivery status notifications
2034 ENHANCEDSTATUSCODES New in 6.x. Three-part status codes (RFC 3463) on every ESMTP reply
6531 / 6532 SMTPUTF8 / EAI New in 6.x. Unicode email addresses and headers
3030 BINARYMIME New since 6.2.21. Advertised alongside CHUNKING; a binary message is relayed as it is to a remote that accepts BINARYMIME
3464 DSN report format New in 6.2.24. Bounces carry the machine-readable delivery-status part, not just prose
5034 POP3 AUTH / SASL line limits Changed in 6.2.25. The AUTH line and the SASL responses after it accept up to 12288 octets on SMTP and POP3 (they were 510 and 500) — which is what lets a real RS256 bearer token fit
— XCLIENT New since 6.2.21. Postfix’s de-facto extension, not an RFC. Advertised only when SMTPXClientEnabled=1 and the peer is in SMTPXClientTrustedIPs

A client that opens with the legacy HELO instead of EHLO gets classic replies — extensions and enhanced status codes are only offered to ESMTP sessions, as the standards require.

Changed in 6.2.24. A client that sends MAIL FROM or AUTH straight after the STARTTLS handshake without a second EHLO now gets 503 Bad sequence of commands. RFC 3207 has always required that second EHLO; the server used to tolerate its absence.

IMAP extensions

RFC Extension What it adds
2177 IDLE Push notification of new and changed messages
5256 SORT and THREAD Server-side sorting of large mailboxes; THREAD (both algorithms) since 6.2.20
4314 ACL Per-folder permissions, used by public folders
9208 QUOTA / QUOTA=RES-STORAGE Clients can display mailbox quota usage. RFC 9208 obsoletes RFC 2087; RES-STORAGE is the only resource advertised, because storage is the only quota the server enforces
6851 MOVE New in 6.x. Atomic move of messages between folders
2971 ID New in 6.x. Client and server identify their implementations
6154 SPECIAL-USE New in 6.x. Standard folder roles — \Sent, \Trash, \Junk, \Drafts, \Archive. CREATE-SPECIAL-USE (RFC 6154 §6) is advertised too — it is the atom a client checks before sending CREATE ... USE (\Sent)
4978 COMPRESS=DEFLATE New in 6.2.28 (IMAPCompressionEnabled, on by default). Compresses both directions of a session. Changed in 6.3.4: advertised and taken only once the client has signed in (before that COMPRESS is answered NO), and withdrawn once compression is on; STARTTLS is refused once a session is compressed
7889 APPENDLIMIT The APPEND size limit is discoverable — the bare atom before authentication, the exact value for this account afterwards
7888 LITERAL- Non-synchronising literals up to 4096 bytes. LITERAL- rather than LITERAL+ deliberately, so an oversized APPEND can still be refused with TOOBIG before the data is sent
5819 LIST-STATUS LIST RETURN (STATUS ...) answers each mailbox’s STATUS inline — one round trip at client start-up instead of one per mailbox
8970 PREVIEW A server-generated body snippet, so a client renders its message list without fetching bodies
3502 MULTIAPPEND Several messages in one APPEND, atomically
8508 REPLACE Replace a message in one command instead of APPEND + STORE \Deleted + EXPUNGE
8514 SAVEDATE The SAVEDATE FETCH item and the SAVEDBEFORE/SAVEDON/SAVEDSINCE search keys, backed by the column schema 6013 added
5464 METADATA Annotations on mailboxes and on the server, over the table schema 6014 added
8474 OBJECTID Stable object ids — EMAILID survives COPY, MAILBOXID survives RENAME. THREADID is answered NIL, which the RFC provides for
3516 BINARY FETCH BINARY[] and BINARY.SIZE[] decode a part’s transfer encoding server-side; APPEND accepts the literal8 form
8437 UNAUTHENTICATE An authenticated session returns to the not-authenticated state, for connection reuse. Advertised only once authenticated
5032 WITHIN SEARCH OLDER and YOUNGER

New in 6.x. The capability line also carries a set of smaller extensions added across the 6.x releases: UIDPLUS (RFC 4315), UNSELECT (RFC 3691), ENABLE (RFC 5161), ESEARCH (RFC 4731), CONDSTORE and QRESYNC (RFC 7162), LIST-EXTENDED (RFC 5258), SEARCHRES (RFC 5182), STATUS=SIZE (RFC 8438) and UTF8=ACCEPT (RFC 6855). The classic NAMESPACE (RFC 2342), CHILDREN (RFC 3348) and SASL-IR (RFC 4959) carry over from the original.

Section 34.3 covers the ones with practical consequences.

Mail filtering

RFC Standard Notes
5228 Sieve New in 6.x. The standard mail filtering language — see chapter 12 for the supported command set
5804 ManageSieve New in 6.x. Lets mail clients upload and manage Sieve scripts — off by default, see chapter 12
5173 Sieve body extension New in 6.2.21. Filters can test the message body — decoded text, named MIME types, or raw — see chapter 12
5490 / 5463 / 5183 / 5260 / 5235 / 7352 / 5293 / 5229 / 5429 / 6609 / 5435 Sieve extensions New since 6.2.21. mailbox, ihave, environment, date/index, spamtest/spamtestplus, duplicate, editheader, variables, reject/ereject, include and enotify — plus regex, which never became an RFC. §12.2 has the whole table
5804 ManageSieve capability response Corrected since 6.2.21. A SASL mechanism that would be refused on a cleartext connection is no longer advertised, and STARTTLS is offered wherever a certificate is available (§12.2)

Authentication

RFC Standard Notes
7677 SCRAM-SHA-256 and SCRAM-SHA-256-PLUS New in 6.x. The password never crosses the wire; -PLUS adds TLS channel binding
7628 OAUTHBEARER New in 6.x. OAuth2 bearer tokens; the de facto XOAUTH2 variant is also accepted
6238 TOTP New in 6.x. Six-digit two-factor codes for administrator logon — enforced by the server itself since 6.2.25 (§17.4) — and for accounts. Since 6.3.4 a code the server has accepted is refused if it comes again, as the RFC requires
4422 App. A SASL EXTERNAL New in 6.2.25. A client certificate verified against the port’s CA logs on as the mailbox its addresses name — on SMTP, IMAP and POP3
4752 GSSAPI (Kerberos) New in 6.3.2. Kerberos single sign-on on SMTP, IMAP and POP3, on Windows only; off until GssapiEnabled is set to 1

PLAIN and LOGIN remain available for ordinary password authentication, PLAIN on SMTP and IMAP only once it is allowed on the SMTP and IMAP tabs of Connections & protocols → Protocols, where it is off as shipped (§34.2). Chapter 17 explains when to prefer SCRAM.

Sender authentication and transport security

RFC Standard Notes
7208 SPF Checked on inbound mail as part of anti-spam scoring. Changed in 6.3.4: an implementation of RFC 7208 replaced the RFC 4408 library the server had carried. Loopback addresses no longer pass unconditionally before the policy is read, so a content filter that re-injects mail should authenticate or connect from an IP range with anti-spam off; lookups follow the server’s own resolver settings on Windows, and the check works on Linux, where every result had been TempError. Only a fail scores, as before
6376 DKIM RSA signing and verification
8463 Ed25519 DKIM New in 6.x. Modern short-key signatures, alongside RSA
7489 DMARC New in 6.x. Policy evaluation on inbound mail. Since 6.3.4 the aggregate reports other servers send about your own domains are also read and charted, once a domain names a report mailbox (§8.6)
8617 ARC New in 6.x. Sealing of mail this server relays onward, a forwarded copy included, as well as mail from a hosted domain, with a hosted domain’s DKIM key; off until ArcSealingEnabled is set on Certificates & encryption → Transport security (chapter 26)
8461 MTA-STS New in 6.x. Honoured outbound; hMailServer can also host your own policy
8460 TLS-RPT New in 6.x. Daily reports to recipient domains about TLS failures encountered delivering to them, sent once a sender address is set (TlsRptFromAddress, §9.4); until then the statistics are collected and nothing is sent. Since 6.3.4 the reports other servers send about your own domains are read and charted too, once a domain names a report mailbox (§8.6)
6698 / 7672 DANE / TLSA for SMTP New in 6.x. Certificate pinning in DNS, enforced outbound
4033–4035 / 5155 DNSSEC New in 6.x. In-process validation of the server’s own lookups: the MX and TLSA records DANE rests on, DKIM keys and DMARC policies and, since 6.3.4, SPF records, which until then did not go through the server’s resolver. Corrected in 6.2.27: a DS query answered with nothing used to be treated as an unsigned delegation — which is exactly what a stripping attacker shows a resolver. The validator now requires the parent’s proof of absence in the authority section (RFC 4035 §5.2, RFC 5155 §8), and a delegation without one under a signed parent is Bogus, not Insecure. Diagnostics.DnssecChainStatus reports what it concluded
8555 ACME New in 6.x. Automatic Let’s Encrypt certificates
5246 / 8446 TLS 1.2 / TLS 1.3 The only protocol versions enabled by default — TLS 1.3 support and the 1.2/1.3-only default are new in 6.x
8314 Implicit TLS Ports 465, 993 and 995

New in 6.x. SRS and BATV are also implemented (chapter 17), but neither ever became an RFC — both follow the widely deployed draft specifications, as does everything that interoperates with them. In practice the missing RFC number changes nothing.

Key exchange. The default group list is X25519MLKEM768:SecP256r1MLKEM768:X25519:secp384r1:secp256r1 — post-quantum hybrids first, classical curves behind them (TlsKeyExchangeGroups, §21). An OpenSSL older than those hybrids rejects the list and the fallback handles it; since 6.3.0 that condition is reported once rather than once per listener and once per outbound delivery.

Message encoding

Message bodies and headers are handled per MIME (RFC 2045–2047): encoded body parts, encoded words for non-ASCII header text, and RFC 2231 parameter encoding for things like attachment file names in other character sets. With SMTPUTF8 (above), headers can also simply be UTF-8.

34.2 Seeing it for yourself

Do not take a table’s word for it — ask the server. Any machine with OpenSSL will do; it does not have to be the server itself.

For SMTP, connect and read the EHLO response:

openssl s_client -connect mail.yourcompany.com:25 -starttls smtp -quiet

Type EHLO test.example.org and the server lists its capabilities — something like:

250-mail.yourcompany.com
250-SIZE 20971520
250-8BITMIME
250-PIPELINING
250-CHUNKING
250-BINARYMIME
250-SMTPUTF8
250-ENHANCEDSTATUSCODES
250-DSN
250-AUTH LOGIN PLAIN SCRAM-SHA-256 SCRAM-SHA-256-PLUS
250 HELP

For IMAP:

openssl s_client -connect mail.yourcompany.com:993 -quiet

A new installation has no port 993 until you add one; its IMAP listener is 143, with STARTTLS, which openssl s_client -connect mail.yourcompany.com:143 -starttls imap -quiet reaches.

Type a CAPABILITY and you will see something like (one line on the wire, wrapped here):

* CAPABILITY IMAP4 IMAP4rev1 IMAP4rev2 CHILDREN IDLE QUOTA QUOTA=RES-STORAGE SORT ACL
  AUTH=PLAIN AUTH=SCRAM-SHA-256 AUTH=SCRAM-SHA-256-PLUS SASL-IR NAMESPACE
  RIGHTS=texk MOVE ID SPECIAL-USE CREATE-SPECIAL-USE UNSELECT UIDPLUS ENABLE STATUS=SIZE
  ESEARCH SEARCHRES CONDSTORE QRESYNC LIST-EXTENDED LIST-STATUS UTF8=ACCEPT WITHIN
  THREAD=ORDEREDSUBJECT THREAD=REFERENCES APPENDLIMIT LITERAL- PREVIEW MULTIAPPEND
  REPLACE SAVEDATE METADATA OBJECTID BINARY
a OK CAPABILITY completed

The exact lists vary with your settings and the port you connect to. SMTP’s AUTH PLAIN appears only once plain-text authentication is allowed on the SMTP tab of Connections & protocols → Protocols, and IMAP’s AUTH= mechanisms and SASL-IR only once they are switched on, on its IMAP tab; each is off as shipped. AUTH=EXTERNAL appears only when the client presents a certificate that the port’s CA verifies and that names an address. Since 6.3.4 COMPRESS=DEFLATE is offered only after sign-in, beside UNAUTHENTICATE and the account’s exact APPENDLIMIT= value, so ask for CAPABILITY again after LOGIN to see them. If something you expect is missing, check those tabs and the port’s configuration under Connections & protocols → TCP/IP ports before assuming a fault.

More: IMAP and POP3 on the server: capabilities, switches, shared mailboxes, search limits on the forum.

34.3 IMAP details worth knowing

Server-side sorting (SORT)

Without SORT, a client that wants the ten newest messages in a 10,000-message folder must download headers for all 10,000 and sort them itself. With SORT, it asks the server for the ten newest and gets exactly that. On large mailboxes the difference is dramatic, which is why every serious IMAP client uses it when offered.

The THREAD half of RFC 5256 — server-side conversation threading — is implemented as of 6.2.20, with both algorithms: ORDEREDSUBJECT and REFERENCES (real References-chain threading). Clients that ask for conversation trees get them from the server; clients that thread locally carry on as before. Changed in 6.3.4: SORT uses every criterion a client gives it, REVERSE reverses only the criterion it precedes, and ties go to sequence order; both THREAD algorithms build the trees the RFC describes. A client can therefore list the same folder in a different order than under 6.3.3.

Push mail (IDLE)

A client that issues IDLE on a folder is told the moment a message arrives or changes, instead of polling. This is how phones and desktop clients get near-instant new-mail notification from hMailServer with no extra configuration.

One protocol limitation to know: IDLE reports changes within the selected folder. There is no standard mechanism for telling a connected client that another client has created a new folder — the second client sees it the next time it refreshes its folder list. This is IMAP, not a fault.

Who names the folders (SPECIAL-USE)

hMailServer does not decide that your sent mail lives in a folder called “Sent items” — the client creates that folder and files messages into it. Historically this meant two clients on one account could each create their own sent folder with different names.

The SPECIAL-USE extension fixes the ambiguity: folders carry role attributes (\Sent, \Trash, \Junk, \Drafts, \Archive), so any client that supports the extension agrees on which folder does what, whatever it is displayed as. If a user wants a folder renamed, they rename it in their mail client — not on the server.

Atomic moves (MOVE)

Before MOVE, “move to folder” was three operations — copy, flag as deleted, expunge — and a dropped connection halfway could leave the message in both places or flagged in neither. MOVE does it as one atomic operation. Clients use it automatically when advertised.

Identification (ID)

ID lets client and server exchange implementation names and versions. Its practical value is diagnostic: when one particular client misbehaves, the ID exchange in the IMAP log tells you exactly which client and version you are dealing with.

Quota and ACL

The QUOTA extension exposes the account’s Maximum size setting to the client, so users can see how full their mailbox is without asking you. ACL is what public folders (chapter 13) use for per-account and per-group rights.

The folder separator is a dot

By default, hMailServer uses . as the IMAP folder hierarchy separator: Projects.2026 is a folder named 2026 inside Projects.

Consequence: with the default separator, a folder name cannot itself contain a dot. A user trying to create a folder called example.com will get example containing com. The separator is configurable — Settings.IMAPHierarchyDelimiter in the COM API (chapter 23) — and hMailServer refuses to change it while any existing folder name contains the new character. Change it on a new server or not at all; connected clients cache folder lists built with the old separator.

34.4 IPv6

hMailServer is dual-stack: IPv4 and IPv6 both work, for both delivery and client access. There is no IPv6 on/off switch in hMailServer itself — an operating system with IPv6 enabled is all it needs, on either platform. The one wrinkle: the service takes stock of the operating system’s capabilities when it starts, so if IPv6 is switched on after installation, restart the service before expecting it to be used.

One Linux-specific caveat: the server’s own DNS client passes over IPv6 nameservers in /etc/resolv.conf while the file lists an IPv4 one, and up to 6.3.5 always, so a resolver reachable only over IPv6 was not used for lookups; from 6.3.6 the first two IPv6 nameservers are asked when no IPv4 one is listed (§32.3). That is about resolving names, not about delivering over IPv6, which works.

Everywhere an address is accepted — TCP/IP ports, IP ranges, incoming relays, the SMTP relayer, routes — both IPv4 and IPv6 forms are valid, and the database stores either. The one exception is a name server of your own (DNSServer, on Connections & protocols → DNS resolver), which is a single IPv4 address: an IPv6 one is reported as HM4401 and the system’s resolver is used instead.

Outbound needs no configuration: with IPv6 available on the machine, hMailServer can deliver to IPv6 destinations as it is. By default it resolves IPv4 addresses first; IPv6 preferred (on the General tab of Maintenance → Advanced; Settings.IPv6PreferredEnabled in the COM API) makes delivery look up AAAA records before A records. Changed in 6.3.4: an outbound bind address (Bind outbound connections to IP, on the Delivery tab of Mail flow & delivery → Delivery of e-mail) may name one address of each family, separated by a comma, and the one of the destination’s family is used; an address of one family no longer stops delivery to the other.

Listening on IPv6 is not enabled by default. Two steps:

  1. Add ports. Under Connections & protocols → TCP/IP ports, add a port per protocol bound to the IPv6 address you want to listen on. The IPv6 equivalent of 0.0.0.0 (all addresses) is ::.
  2. Add IP ranges. Under Sign-in & access → IP ranges, add an IPv6 internet range from :: to ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff with the same settings as your existing internet range, and a loopback range ::1 to ::1 matching “My computer”.

The confusing failure mode: with a port but no matching IP range, hMailServer accepts the IPv6 connection and then immediately drops it, because no range grants the client anything. If IPv6 clients connect and instantly disconnect, this is why.

Two words of caution. Most mail on the internet still moves over IPv4, so IPv6 is an addition, never a replacement — your IPv4 setup must stay correct. And the large providers police IPv6 senders more strictly than IPv4: expect a missing PTR record or unaligned SPF/DKIM to be refused over IPv6 where IPv4 might have been tolerated.

34.5 Frequently asked questions

Does hMailServer run on anything other than Windows?

Yes, since 6.3.0 — Linux on x86-64 and AArch64, built from the same source tree as the Windows build, which is unchanged. It is packaged as a .deb, an .rpm and an AppImage, with a PKGBUILD for Arch in the source tree — 6.3.8 for both architectures, with a .deb or an .rpm of each supported Debian, Ubuntu, Fedora, Enterprise Linux and openSUSE release’s own, the AArch64 ones cross-compiled and tested under emulation, and a container image for linux/amd64 and linux/arm64; a Nix flake and an Alpine APKBUILD are in the source tree. The packages install a systemd unit that runs the server as its own user, and configuration under /etc/hmailserver. 64-bit only either way; 32-bit builds were dropped in 6.0. Chapter 35 is the walk-through.

What differs, stated plainly:

  • No Control Panel and no COM. A Linux server is administered through the hmailserver command line, the REST API (chapter 24) — with hmctl, a command-line client over it, since 6.3.4 — and the Control Deck in a browser (chapter 36). Its INI file keeps [Database], [Directories], [LDAP] and the other sections outside [Settings]. Since 6.3.4 the database holds the [Settings] keys (every one but PasswordPepper), changed over the REST API (PUT /api/v1/settings/ini/{name}, or hmctl ini set) or with hmailserver --set-setting NAME=VALUE; the Control Deck edits only the four of them its Settings view carries (chapter 21), and a value edited into the file is reported as HM5804 and not applied. Event scripts run there in JavaScript since 6.3.4, in an engine built into the server; VBScript and JScript stay on Windows, whose scripting host runs them (§28.2).
  • PostgreSQL or MySQL/MariaDB only. Microsoft SQL Server and the built-in SQL Server Compact database are refused by name (§3.5).
  • Per-domain DKIM signing is set over the REST API or on the Control Deck. Up to 6.3.2 there was no route that wrote it, so a domain that had to DKIM-sign its outbound mail was not one to run on Linux; since 6.3.3 PUT /api/v1/domains/{domain} takes it with the rest of the domain (§24.15).
  • Moving a Windows installation to Linux is not a supported migration. The schema and the on-disk message format are the same on both, so the pieces exist — but there is no tested path, and this manual will not pretend otherwise (§18.2).

Do I need an internet connection to run it?

No. hMailServer runs happily on a closed network — as an internal mail system, or as a real SMTP/IMAP endpoint for testing software — with the obvious limitation that it cannot exchange mail with the outside world.

How many domains and accounts can one server handle?

hMailServer imposes no limit of its own. Capacity is set by hardware and usage patterns: installations with tens of thousands of accounts on a single server exist, and a hundred active IMAP users cost more than a thousand occasional POP3 users. For anything beyond a small installation, use a server database rather than the built-in one (§3.5).

Do I need separate pop., smtp. and imap. host names?

No. Technically, one name — mail.yourcompany.com — serves every protocol; all that matters is that whatever name clients use resolves to your server and appears on your TLS certificate (chapter 9). If you want users to type nothing at all, enable automatic client configuration (§7.2) instead of multiplying host names.

If my server is offline for a few hours, is mail lost?

No. SMTP is store-and-forward: a sending server that cannot reach you queues the message and retries, typically for days, before giving up and bouncing it. Downtime delays inbound mail; it does not lose it. Only an outage longer than the sender’s retry window turns delay into a bounce.

Why are messages stored as files on disk rather than in the database?

Deliberately. Files stream to clients with flat memory usage, where database storage would mean reading whole messages into memory; virus scanners want files to scan; and an administrator can open any message in a text editor or delete it directly, no SQL required. The database stores the index — accounts, folders, message metadata — and the .eml files under Data are the mail. Since 6.3.4 three options, each off as shipped, change what is there: storage compression keeps a message as a compressed container, mailbox encryption at rest encrypts one account’s messages to a key of its own, and object storage keeps the files in an S3-compatible store or Azure Blob Storage, the database still the index. Clients see no difference; a text editor, or a script that opens Message.Filename, does.

Why doesn’t hMailServer store attachments separately?

By default it does not, because a message is not “a body plus attachments” — it is one continuous MIME byte stream, delivered to clients exactly as received. Splitting attachments out means decoding messages on arrival and rebuilding them on every fetch: more CPU and more memory. Extracting attachments is the client’s job. Since 6.3.4 the server can, off as shipped: with MessageStoreCompression on, a stored message is compressed and each attachment of at least MessageStoreDedupMinKB (256 KB once decoded, unless changed) is kept once for every message on the server that carries the same bytes. Clients still receive the original message byte for byte. The switch is on the Message store tab of Maintenance → Performance, and applies to the next message stored.

More: Message storage: compression, de-duplication, object storage and encryption at rest on the forum.

Why are there empty two-character directories in the data directory?

Message files are split across sub-directories named 00–FF under each account, because Windows handles enormous directories poorly and busy mailboxes would otherwise put tens of thousands of files in one folder. The sub-directories are created on demand and only removed when the account is deleted, so empty ones are normal and harmless. There are at most 256 per account.

Can my users have non-ASCII email addresses?

Yes — SMTPUTF8 (RFC 6531/6532, new in 6.x) is advertised automatically, so addresses like bjö[email protected] work. The caveat is the rest of the world: every server a message crosses must also support SMTPUTF8, and one that does not cannot accept mail involving a Unicode address at all. Give internationalised users an ASCII address as well until the domains they write to have caught up.

Can printers, scanners and applications send mail through hMailServer?

Yes. Create a dedicated account for the device, and have it submit on port 587 with authentication — with TLS, which a new 6.3.4 installation requires before a password from any address but this computer’s (the Internet IP range’s Require SSL/TLS when authenticating). A device that cannot start TLS is refused at sign-in until an IP range covering its own address alone leaves that option off, and its password then crosses your network in the clear. Do not solve this by letting the device’s IP range relay without authentication; that is one careless subnet away from an open relay (chapter 17). An application can instead use the send API that 6.3.4 added: an API key made a sender, with an address and ceilings of its own, posts to /api/v1/send.

What happened to PHPWebAdmin and the classic Administrator?

Both are gone. The classic Administrator was retired in 6.2.0 in favour of the Control Panel, which does everything it did and more; PHPWebAdmin was removed outright in 6.2.10 because it stored the administrator password in plaintext and required DCOM opened up for the web server. Chapter 18 covers what replaced them and how the transition works.

Is the fork still compatible with software written for the original hMailServer?

Yes, by design. Everything the original exposed through the COM API is still there — the fork adds to it (the Diagnostics and MessageIndexing objects, Account.SieveScript, and more; chapter 23) but removes nothing — mail clients see the same protocols plus new extensions, and almost everything the fork adds is additive and off by default. The one behavioural change worth knowing before 6.3.4: since 6.2.10, COM methods correctly report failure on refused calls where they previously claimed success — scripts that check return values (as they always should have) now find out when a call was denied (chapter 23). 6.3.4 adds more a program can notice, among them: a COM change to a route, an incoming relay or another shared configuration object takes effect at Save(); IMAP answers some commands as the RFCs say where it did not; a one-time code is taken once, so a script that sent the administrator’s password and a code with every REST request must now sign in once (POST /api/v1/session) or use an API key, or only its first request in each half minute is accepted; and a [Settings] line edited in hMailServer.ini is ignored in favour of the value the database holds, reported as HM5804 and, where the service may write the file, put back (§18.5).


35. Running hMailServer on Linux

hMailServer has been a Windows program since 2002. Since 6.3.0 it is not only that: the same source tree builds, installs, runs and delivers mail on x86-64 and AArch64 Linux, as a systemd service, from a .deb, an .rpm or an Arch PKGBUILD. That is why the release is numbered 6.3 and not 6.2.29. The Windows build is unchanged in what it compiles and how it behaves.

What crossed is the server — the SMTP, POP3 and IMAP engines, delivery, routing, rules, anti-spam, anti-virus, ACME, the backup manager, the REST API, the Control Deck and the self-service portal. What did not cross is everything built on Windows-only interfaces: the Control Panel, the COM API, the VBScript and JScript event engine and the SQL Server backends (6.3.4 brought event scripts across in JavaScript instead). This chapter says where that line falls, then takes you from a bare machine to a running mail server.

Read §35.1 before you install anything. There are capabilities this platform does not have today, and finding that out after the mailboxes are populated is the wrong order.

Everything in chapters 1–34 that is not about the Control Panel still applies: the protocols, the delivery path, DNS, DKIM, SPF, DMARC, rules, routes and the settings in chapter 21 are the same code with the same behaviour. Where a chapter tells you to click something in the Control Panel, the equivalent here is the REST API (chapter 24), the Control Deck (chapter 36) or, since 6.3.4, hmctl, the command-line client over that API that the packages install as /usr/bin/hmctl. Where it names a path under C:\Program Files\hMailServer, §35.6 gives the Linux one.

35.1 What runs on Linux, and what does not

What is not there

Missing What it means for you
VBScript and JScript event scripts. Up to 6.3.3 there were no event scripts here at all: the script engine is the Windows Active Scripting host, any non-empty script file was reported as uncompilable, and no handler flag was ever believed to be registered. Since 6.3.4 a Linux server runs event scripts in JavaScript, in the QuickJS engine built into the server, for the same events under the same function names with the same objects, less the members only COM can offer (a message’s Attachments and Headers among them; §28.2); VBScript and JScript are refused at load. The language setting is seeded VBScript on every platform, so set it to JavaScript before switching scripting on.
The COM API (chapter 23). The ATL administration API is not compiled. Every third-party COM script and every tool built on one stops at the Windows boundary.
The Control Panel and the Windows tools. Every .NET project under source/Tools — ten of them — targets net10.0-windows. No Control Panel, no DBSetup, no DBSetupQuick, no DBUpdater, no Import Tool, no Data Directory Synchronizer. The database tools are replaced by --create-database and --upgrade-database, and the Control Panel by the Control Deck and the REST API (§35.10). Since 6.3.4 accounts can be imported from a spreadsheet over REST and with hmctl; the Import Tool’s mbox and Maildir imports and the Data Directory Synchronizer have no replacement.
SQL Server and SQL Server Compact. Refused by name, in two places, and neither refusal is a connection error. PostgreSQL and MySQL/MariaDB are the backends here (§35.4).
Message catalogues (Languages). Not shipped; their only reader is the COM interface. The installation-paths diagnostic prints the line and does not count its absence as a failure.
Self-update, up to 6.3.3. The update checker works and knows which package your machine would install, but a server from 6.3.0 to 6.3.3 reads the former GitHub repository’s feed, which has answered 404 since 18 September 2026, until UpdateFeedUrl=https://updates.progressiverobot.com/releases.json points it at the project’s own; 6.3.4 reads that one without being told. Up to 6.3.3 the apply step refuses with a Medium 6401: the helper it runs is a Windows program. Since 6.3.4 the apply runs through a root helper outside the server’s unit (§35.13) — but 6.3.4 itself carries no Sigstore bundle, so it is installed with apt, dnf or pacman like the releases before it.
Negotiate binds for LDAP. [LDAP] BindMethod=1 is Windows SSPI. It reports HM6420 once and refuses the logon as a directory failure rather than a wrong password. Use BindMethod=0 with Security=2 (LDAPS) or 1 (StartTLS). A GSSAPI bind through Cyrus SASL is not written, the directory’s certificate must chain to the system trust store, and there is no private-CA setting.
A re-keying command. The stored-secret key file is made once and never rotated. Replacing it by hand means re-entering every stored password; nothing re-encrypts them. Since 6.3.4 the stored-secrets key’s line in hMailServer.ini is sealed under it too, so a replaced key file also means importing that key again from its export (sudo hmailserver --import-stored-secrets-key FILE, then sudo systemctl restart hmailserver).
A tested path from Windows. Moving a Windows installation to Linux is not a supported migration (§35.14).
AArch64 on real hardware. Up to 6.3.3 the ARM packages were built and installed by CI on GitHub’s ARM runners on every push. GitHub suspended the project’s account on 18 September 2026 and the project has no ARM machine, so 6.3.4 and 6.3.5 have no AArch64 package. From 6.3.6 GitLab CI cross-compiles the AArch64 packages and AppImage and installs and runs them under emulation (qemu-user), and the release carries them. Nobody has installed an ARM package on ARM hardware yet.

What is the same as Windows

  • The protocol engines and delivery: SMTP, POP3, IMAP, submission, the queue, routing, rules, greylisting, SPF, DKIM verification, DMARC, ARC, MTA-STS, TLS-RPT, DANE, the anti-virus integrations and the archive.
  • The database schema — 6108 at 6.3.8, the same number the Windows build needs.
  • The on-disk message format: one file per message, one directory per domain and per mailbox.
  • The REST API (chapter 24), the Control Deck and the portal (chapter 36). The Deck is the same index.html the Windows installer ships — one file in the repository — and CI compares the page the Linux server serves against it byte for byte.
  • ACME, the backup manager, the diagnostics, the crash record and the log format.
  • Directory authentication as a simple bind over LDAPS or StartTLS. The OpenLDAP arm records the same result codes, so the wrong-password versus directory-down classification is the same decision on both platforms.

Stored secrets work differently, and better. Windows protects route, fetch-account and relay passwords, private-key passphrases and the administrator’s TOTP secret with machine-scoped DPAPI, which does not travel. Here they are AES-256-GCM under a key file the installation owns, so the data directory and the database move together and every stored password opens on the new machine. The price is that the key file is part of the backup (§35.12). Since 6.3.4 what the database holds and every node must open — each account’s TOTP second factor and the [Settings] secrets — is sealed under the installation’s stored-secrets key instead (HMK1:), once one is made (§35.3, step 5). Its line in hMailServer.ini is sealed under the key file, so that key travels in its export, or in the configuration backed up together with the key file.

35.2 Supported platforms and packages

x86-64 and AArch64. Up to 6.3.3 both were built natively in CI on GitHub’s runners, ubuntu-latest and ubuntu-24.04-arm, and both were packaged. 6.3.4 was built on the project’s own x86-64 machine and 6.3.5 by the project’s GitLab CI, and both were published for x86-64 alone. From 6.3.6 the release carries both again: GitLab CI builds each package in its distribution release’s own container, cross-compiles the AArch64 ones, and installs and starts every one in a clean container of its release, the AArch64 ones under emulation (qemu-user), since the project has no ARM machine. No other architecture is built.

Distribution Package Notes
Debian 12 and 13, Ubuntu 24.04 and 26.04 (from 6.3.6: a .deb of Debian 12’s, Debian 13’s and Ubuntu 24.04’s own, and the generic one, built on Ubuntu 26.04, for each architecture). Up to 6.3.5 only the generic one, which needs glibc 2.38 or later, so not Debian 12. .deb Library dependencies resolved by dpkg-shlibdeps, so libpq comes with it. Maintainer scripts create the user, the directories and the systemd enablement.
Fedora, RHEL and derivatives (from 6.3.6: an .rpm of Fedora 44’s and 43’s and Enterprise Linux 10’s and 9’s own, for each architecture, and the generic one. Up to 6.3.5 only the generic x86-64 one, which Enterprise Linux 9 refuses, its glibc and libstdc++ being older than the package asks for) .rpm rpm’s own auto-requires; scriptlets do the same work in the shape rpm expects.
openSUSE and SUSE .rpm From 6.3.6: an .rpm of Leap 16.0’s and Tumbleweed’s own, for each architecture. Up to 6.3.5 the generic RPM, not tried there: zypper is not dnf.
Arch PKGBUILD makepkg -si, from the release’s source tarball since 6.3.6 (from the git tag before), built without the live update. No maintainer scripts: a sysusers.d file declares the user and a tmpfiles.d file the directories, and pacman’s own hooks run them. Arch packages do not enable services, so there the unit is neither enabled nor started.
Alpine Linux APKBUILD From 6.3.6 the server builds and runs on musl. platform/packaging/alpine/APKBUILD builds the server, an OpenRC service and hmctl from the release’s source tarball with abuild, without the live update, naming apk upgrade hmailserver. Not in Alpine’s archive yet.
NixOS and Nix flake From 6.3.6 a flake of the project’s own at the repository’s root, with a NixOS module, services.hmailserver: nix run gitlab:hmailserver/hmailserver/v6.3.8. Built without the live update, naming nix flake update hmailserver. Not in nixpkgs.

The generic x86-64 .rpm is built on Ubuntu, so its automatic requires name Ubuntu sonames; CI installs it in a Fedora container with --nodeps for that reason. The release’s own package of each Fedora, Enterprise Linux and openSUSE release is built in that release’s container, and is the one to install. On Fedora 43 or 44 or Enterprise Linux 10, dnf install ./<file>.rpm resolves the real dependencies from your own repositories. Enterprise Linux 9 refuses the generic package, because its glibc and libstdc++ are older than the package asks for; from 6.3.6 it has its own. From 6.3.6 every .rpm asks for glibc’s extra character-set converters, glibc-gconv-extra on Fedora and Enterprise Linux and glibc-gconv-modules-extra on openSUSE, which glibc only recommends and the distributions’ container images lack: required where dnf is installed on Fedora or Enterprise Linux, so dnf installs them with the package, and recommended elsewhere, since on openSUSE the live update installs with rpm -Uvh, which installs nothing beside the package (zypper installs the recommendation unless solver.onlyRequires is on). Without them the server converts no text in a character set but Unicode’s, US-ASCII, UTF-7, ISO-8859-1, ISO-8859-15 and Windows-1252, which every .rpm up to 6.3.5 did silently; from 6.3.6 the server says so at its start and in hmailserver --check-config. On Fedora or Enterprise Linux a server without them whose dnf has no repository enabled (an unregistered RHEL, say) has the live update to 6.3.6 or later refused until glibc-gconv-extra is installed. An AppImage runs on the host’s glibc and cannot require the package: install it by hand.

Up to 6.3.1 the packages installed only on the distribution release they were built on. 6.3.0 and 6.3.1 linked Boost dynamically, and a Boost library’s soname carries its version (libboost_thread.so.1.83.0), so the .deb — built on Ubuntu 24.04 — was refused by Ubuntu 26.04 on 11 September 2026 for five Boost sonames and nothing else. Up to 6.3.1, match the distribution release, or build from source. Linking Boost statically, which removes the dependency altogether, is in the tree and is not in 6.3.1. From 6.3.2 Boost is linked statically and the package installs on the next distribution release as well; CI installs the .deb on Ubuntu 26.04 on every run. 6.3.5 is built on Ubuntu 26.04, in the CI image hmailserver-ci/linux:26.04-dad0ab749505, as 6.3.4 was, and its packages ask for what 6.3.4’s did. Its .deb installs on Ubuntu 26.04 and 24.04 and asks for glibc 2.38 or later, so it does not install on Debian 12. Its .rpm installs on Fedora 43 and 44 and Enterprise Linux 10 and is refused by Enterprise Linux 9, whose glibc and libstdc++ are older than it asks for. On anything older, build from source.

The release assets

One release carries every platform at one version. The current release is 6.3.8; its page, which updates.progressiverobot.com/download/ leads to, lists every file with its SHA-256, and every file has a Sigstore bundle beside it, <file>.cosign.bundle. /download/hmailserver_amd64.deb and /download/hmailserver.x86_64.rpm on the same site lead to the newest stable release’s packages, and /download/hmailserver_arm64.deb, /download/hmailserver.aarch64.rpm, /download/hMailServer-x86_64.AppImage and /download/hMailServer-aarch64.AppImage to the others. The set is:

Asset For
hmailserver_6.3.8_amd64.deb, hmailserver_6.3.8_arm64.deb The generic .deb, Ubuntu 26.04’s build — x86-64 and AArch64; the names a server’s update check asks for
hmailserver_6.3.8-1~<release>_<arch>.deb Debian 12 (debian12), Debian 13 (debian13) and Ubuntu 24.04 (ubuntu24.04), each release’s own — amd64 and arm64
hmailserver-6.3.8-1.x86_64.rpm, hmailserver-6.3.8-1.aarch64.rpm The generic .rpm: the x86-64 one built on Ubuntu, the AArch64 one Fedora 44’s build; the names a server’s update check asks for
hmailserver-6.3.8-1.<release>.<arch>.rpm Fedora 44 and 43 (fc44, fc43), Enterprise Linux 10 and 9 (el10, el9), openSUSE Leap 16.0 (suse.lp160) and Tumbleweed (suse.tw), each release’s own — x86_64 and aarch64
hMailServer-6.3.8-x86_64.AppImage, -aarch64.AppImage Try it out only — see below
hmailserver-6.3.8-appimage-runtime-sources.tar.gz, hmailserver-6.3.8-appimage-bundled-sources.tar.gz The source of the libraries linked into the AppImages’ runtime and of those they carry in usr/lib, which their licences ask for
hmailserver-6.3.8.tar.gz The source tarball, reproducible, with nothing non-free in it
hmailserver-chart-6.3.8.tgz The Helm chart, naming this release’s container image
hmailserver-linux-6.3.8-SHA256SUMS.txt Checksums of every Linux file, the two sources files and the chart (6.3.5’s covered its two packages; 6.3.4’s was hmailserver-6.3.4-SHA256SUMS.txt, for every file, with an SSH signature)
hMailServer-6.3.8-x64.exe, hMailServer-6.3.8-arm64.exe The Windows installers, x64 and ARM64 (a preview), on the same release
hmailserver.spdx.json, hmailserver.cyclonedx.json Software bills of material of the source tree
<file>.cdx.json From 6.3.4, the software bill of materials of the installer, the .deb and the .rpm, one each, beside it; from 6.3.6, of every compiled file and the tarball
<file>.cosign.bundle From 6.3.5, each file’s Sigstore bundle, beside it; from 6.3.6 also as <file>.sigstore.json, byte for byte, beside each file but a compiled file’s or the tarball’s SBOM, and neither beside the provenance, itself a bundle
hmailserver-6.3.8.intoto.sigstore.json From 6.3.6, the release’s SLSA v1 build provenance, naming every file above by its SHA-256
hmailserver-6.3.8.intoto.jsonl From 6.3.8, the provenance’s DSSE envelope as one line of JSON, the form OpenSSF Scorecard counts as a release’s provenance

The names are load-bearing. A Linux server’s update checker builds the name its package manager and architecture would install — it probes for /usr/bin/dpkg, then /usr/bin/rpm — and matches an asset on exact equality, so CPack’s names are uploaded as they are and never tidied. Note the RPM’s -1 release field: the asset is hmailserver-6.3.8-1.x86_64.rpm. The 6.3.0 release notes’ upgrading section wrote it without the -1; the release page is right.

Signatures, and which one applies here. Since 6.3.1 the Windows installer on the same release carries an Authenticode signature, issued to Progressive Robot Ltd and countersigned. None of the Linux assets does, and none ever will: there is no Authenticode for a .deb, an .rpm or an AppImage. Up to 6.3.3 what covers every asset on both platforms is Sigstore, keylessly — each file has a .cosign.bundle beside it, and verifying against that bundle is the check that means something here. 6.3.4 has no bundle: it was built and signed by hand after GitHub suspended the project’s account, so on Linux its check is hmailserver-6.3.4-SHA256SUMS.txt and the SSH signature beside it, made with the key that signs the release tags. ssh-keygen -Y verify (OpenSSH 8.1 or later) checks the signature against .gitlab/allowed_signers in a clone of the repository, and must say Good "file" signature for [email protected]. The Windows installers on releases before 6.3.1 carry no signature and never will, because a published release is immutable. And the signature buys less on Windows than it sounds like it should: the elevation prompt names the publisher instead of reading Unknown publisher, but SmartScreen still warns, because reputation attaches to a file that does not change and a new installer every few weeks never is one.

ssh-keygen -Y verify -f .gitlab/allowed_signers -I [email protected] -n file -s hmailserver-6.3.4-SHA256SUMS.txt.sig < hmailserver-6.3.4-SHA256SUMS.txt
sha256sum -c --ignore-missing hmailserver-6.3.4-SHA256SUMS.txt

From 6.3.5 every file has a bundle again, made by the project’s GitLab CI pipeline for the release’s tag and checked with the server’s own verifier before the release was published. For 6.3.8’s packages, signed at the project’s path in the group hmailserver:

sha256sum -c --ignore-missing hmailserver-linux-6.3.8-SHA256SUMS.txt

cosign verify-blob hmailserver_6.3.8_amd64.deb \
  --bundle hmailserver_6.3.8_amd64.deb.cosign.bundle \
  --certificate-identity 'https://gitlab.com/hmailserver/hmailserver//.gitlab-ci.yml@refs/tags/v6.3.8' \
  --certificate-oidc-issuer https://gitlab.com

cosign verify-blob-attestation --new-bundle-format \
  --bundle hmailserver-6.3.8.intoto.sigstore.json --type slsaprovenance1 \
  --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_amd64.deb

The second checks the file against the release’s build provenance, and says Verified OK only when the file’s SHA-256 is one it names. Any other file of the release is checked the same way. On a server from 6.3.4 on, hmailserver --verify-update <file>, with the bundle beside the file, makes the check its live update makes and prints the identity, which must end in @refs/tags/v6.3.8. 6.3.5 was signed before the project moved, so its identity names the project’s former path:

sha256sum -c --ignore-missing hmailserver-linux-6.3.5-SHA256SUMS.txt

cosign verify-blob hmailserver_6.3.5_amd64.deb \
  --bundle hmailserver_6.3.5_amd64.deb.cosign.bundle \
  --certificate-identity 'https://gitlab.com/Progressiverobot/hmailserver//.gitlab-ci.yml@refs/tags/v6.3.5' \
  --certificate-oidc-issuer https://gitlab.com

cosign verify-blob hmailserver-6.3.5-1.x86_64.rpm \
  --bundle hmailserver-6.3.5-1.x86_64.rpm.cosign.bundle \
  --certificate-identity 'https://gitlab.com/Progressiverobot/hmailserver//.gitlab-ci.yml@refs/tags/v6.3.5' \
  --certificate-oidc-issuer https://gitlab.com

For 6.3.3 and the releases before it, the bundle as well:

sha256sum -c --ignore-missing hmailserver-linux-6.3.3-SHA256SUMS.txt

cosign verify-blob hmailserver_6.3.3_amd64.deb \
  --bundle hmailserver_6.3.3_amd64.deb.cosign.bundle \
  --certificate-identity-regexp '^https://github\.com/Progressiverobot/hmailserver/' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

The PKGBUILD, and what the AppImage is for

The Arch PKGBUILD lives in the tree at hmailserver/source/Server/platform/packaging/PKGBUILD. It fetches the tag from the public repository rather than using whatever tree it was copied out of, which is what makes the result reproducible by somebody who does not have your working copy. From 6.3.6 what it fetches is the release’s source tarball, from the project’s package registry, and it builds the server as a distribution does, without the live update, naming makepkg -si as the way to update. It is not a release file.

The AppImage is not for running mail. It runs as whoever started it — no hmailserver user, no unit, none of the hardening in §35.8. Its store, logs and configuration go under ${HMAILSERVER_HOME:-${XDG_DATA_HOME:-$HOME/.local/share}/hmailserver}, which is exactly where a mail store should not be. It cannot bind port 25 — an ordinary user has no CAP_NET_BIND_SERVICE, and the listener ports are database rows rather than configuration keys — and it still needs a database, because there is no such thing as a self-contained hMailServer. Use it to see the thing work on a laptop. Install the .deb or the .rpm to run mail.

Building from source

# Debian, Ubuntu
sudo apt install cmake ninja-build clang \
     libssl-dev zlib1g-dev libpq-dev libldap-dev \
     libboost-thread-dev libboost-chrono-dev libboost-filesystem-dev libboost-regex-dev \
     rpm file

# Fedora, RHEL
sudo dnf install cmake ninja-build clang \
     openssl-devel zlib-devel libpq-devel openldap-devel boost-devel rpm-build dpkg

export CC=clang CXX=clang++
cmake -S hmailserver/source/Server -B build/linux -G Ninja \
      -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr
cmake --build build/linux
cmake --build build/linux --target package   # produces the .deb and the .rpm

The dependencies are OpenSSL 3.0 or newer, zlib, libpq, OpenLDAP (both libldap and liblber, named explicitly so the link survives --as-needed), Boost 1.83 or newer and iconv, which on glibc is part of the C library. clang is a preference, not a requirement: GCC 13 and later builds the same tree with export CC=gcc CXX=g++; CI built with both until GitHub became unavailable on 18 September 2026, and GitLab CI’s linux-build job builds with clang. The Arch PKGBUILD still exports clang and still carries a comment saying GCC cannot compile one of the headers; that comment is out of date — it was believed rather than measured.

-DCMAKE_INSTALL_PREFIX=/usr matters more than it looks: only with that prefix does GNUInstallDirs answer /etc and /var. With any other prefix the configuration installs to <prefix>/etc/hmailserver, where nothing reads it.

35.3 Installing

The whole sequence, in order. The package installs the server enabled and stopped, on purpose: it has no database yet, and nothing in a package can know which backend you have or its credentials. A server started here would fail to reach a database it has never been told about and would say so in the log once a second until somebody noticed.

1. Install the package

sudo apt install ./hmailserver_6.3.8-1~debian13_amd64.deb   # your release's own .deb (§35.2); on Ubuntu 26.04: hmailserver_6.3.8_amd64.deb
sudo dnf install ./hmailserver-6.3.8-1.fc44.x86_64.rpm      # your release's own .rpm: fc43, el10, el9; on openSUSE, zypper install ./hmailserver-6.3.8-1.suse.tw.x86_64.rpm

If your backend is MySQL or MariaDB, install its client library now — the server opens it at run time and no package manager will pull it in (§35.4). What the maintainer script then does, and nothing more:

Step What happens
Create the identity The hmailserver system group and user: no password, no login shell, home /var/lib/hmailserver.
Create the directories /var/lib/hmailserver with its temp, events and database subdirectories, and /var/log/hmailserver, all 0750 hmailserver:hmailserver; /etc/hmailserver at 0750 root:hmailserver and the configuration at 0640. The store underneath is deliberately not walked and not chowned — on a large installation that is a multi-hour pass over millions of files, and anywhere it would quietly undo a deliberate ownership choice.
Register the unit daemon-reload, then enable hmailserver.service. Enabled, not started. Since 6.3.4 the .deb and the .rpm also enable and start hmailserver-update.path, the root path unit a live update is applied through (§35.13); nothing happens through it until the update check is on and an install is asked for.
Say what to do next Printed on a first install only.

2. Create the database user

sudo -u postgres createuser --pwprompt --createdb hmailserver

3. Edit the configuration

sudoedit /etc/hmailserver/hMailServer.ini
[Database]
Type=PostgreSQL
Server=localhost
Database=hmailserver
Username=hmailserver
Password=the-password-you-just-set
Passwordencryption=0
Port=5432
Internal=0

Leave [Security] AdministratorPassword alone. The next step writes it.

4. Set the administrator password

sudo hmailserver --set-admin-password

It reads the password from standard input with the terminal’s echo off, hashes it with PBKDF2 — exactly as the Control Panel hashes it on Windows, by the same code — writes it into the file preserving owner, group and mode, and zeroes its own copy. The password is never an argument, so it is never in the process list or the shell history. This runs as root, because the file is 0640 root:hmailserver. A pipe works for an unattended install: printf '%s\n' "$PW" | sudo hmailserver --set-admin-password.

5. Create the schema

sudo -u hmailserver hmailserver --create-database

Since 6.3.4 a database this command creates is marked as a new installation’s, and it leaves a one-line file, .new-installation, in the DataFolder the configuration names; a database it finds already holding a schema gets neither. The first start then secures the new one (step 7). A DataFolder outside the unit’s ReadWritePaths (/var/lib/hmailserver and /var/log/hmailserver) is read-only to the service, so add it with a drop-in (sudo systemctl edit hmailserver, ReadWritePaths= under [Service]) before that first start.

The stored-secrets key (since 6.3.4). Each account’s TOTP second factor and the [Settings] secrets are sealed in the database under a key kept in hMailServer.ini, never in the database or a backup archive. Run as the service user, the command above cannot write the root-owned file, so it makes none and says so. Make it, and export it at once, to keep with the database backups but apart from them:

sudo hmailserver --create-stored-secrets-key
sudo hmailserver --export-stored-secrets-key /root/hmailserver-stored-secrets.key

Until a key is made those secrets are kept as written, and the setup checklist says so. A database dump restored without the key brings every second factor and [Settings] secret back unopenable.

6. Check it before starting anything

sudo -u hmailserver hmailserver --check-config
Configuration file: /etc/hmailserver/hMailServer.ini
Program directory:  /usr/share/hmailserver/
Data directory:     /var/lib/hmailserver
Log directory:      /var/log/hmailserver
Database type:      3

Database type: 0 means the Type key did not take. Run it as the service user and without --config, so the packaged server has to find /etc/hmailserver/hMailServer.ini on its own — which is exactly what CI does, for the same reason. Since 6.3.4, sudo -u hmailserver hmailserver --check-database goes one step further: it makes one connection with the [Database] settings, says whether it is encrypted and whether the certificate was checked, and exits 0 connected, 1 not, 2 not configured.

7. Start it

sudo systemctl start hmailserver
sudo systemctl status hmailserver
journalctl -u hmailserver -f

A freshly created database holds four listeners: SMTP on 25 and 587, POP3 on 110 and IMAP on 143, bound to every address. Up to 6.3.3 all four started with connection security set to none. Since 6.3.4 the first start secures a new installation: before every start the unit runs hmailserver --apply-new-install-defaults --if-marked, which does nothing unless step 5 left its .new-installation file. At the first start after step 5, if it comes within a day of it by the machine’s clock, that step gives the server a self-signed certificate (RSA 2048, 825 days, under /var/lib/hmailserver/Certificates), STARTTLS, offered and optional, on all four listeners, and TLS required for authentication on the Internet IP range, so that a password from another machine never crosses the network in the clear; the My computer range is left as it was. A first start more than a day after step 5 leaves the database alone; then run sudo -u hmailserver hmailserver --apply-new-install-defaults by hand, with the server stopped. Mail clients warn about the self-signed certificate: replace it with ACME or your own (§35.9). The listeners are database rows, not configuration keys, so §35.9 and chapter 22 are where you turn them into something you would expose to the internet.

8. Turn on the administration interface

[Settings]
RestApiPort=8045
RestApiBindAddress=127.0.0.1

Since 6.3.4 the server keeps those in the database. Lines that were in the file at step 3, before the first start, were taken in at that start; a line edited into the file after that start is reported in the error log as HM5804 and not applied, so the packaged RestApiPort=0 stands and a reload brings up no listener (chapter 21). Store them with the command line instead, as root:

sudo hmailserver --set-setting RestApiPort=8045
sudo hmailserver --set-setting RestApiBindAddress=127.0.0.1
sudo systemctl reload hmailserver
curl -u Administrator http://127.0.0.1:8045/api/v1/status

Then open http://127.0.0.1:8045/ for the Control Deck. §35.10 is the rest.

35.4 Creating the database

PostgreSQL

libpq is a link-time dependency. The packages declare it automatically and your package manager installs it alongside; the Arch package names postgresql-libs. Nothing else is needed.

# Either: let the service role create the database itself
sudo -u postgres createuser --pwprompt --createdb hmailserver

# Or, the tidier policy: no CREATEDB, and make the empty database yourself
sudo -u postgres createuser --pwprompt hmailserver
sudo -u postgres createdb --owner hmailserver hmailserver

For a database on another host, encrypt the connection. Both keys are commented out in the packaged file:

PostgreSQLSslMode=verify-full
PostgreSQLSslRootCert=/etc/ssl/certs/ca-certificates.crt

libpq’s own default is prefer, which encrypts when the server offers it and verifies nothing. Set require at least, and verify-full with the CA when the database is elsewhere. A mode the server does not recognise refuses the connection and says so, rather than falling back to the default. Since 6.3.4 the [Database] keys TlsMode and TlsCaFile say the same for every backend; for PostgreSQL they become sslmode and sslrootcert when the two keys above are not set, and those two win when they are.

MySQL and MariaDB

The client is loaded with dlopen at run time rather than linked, so nothing pulls it in for you.

Distribution Client package
Debian, Ubuntu libmariadb3
Fedora, RHEL mariadb-connector-c
Arch mariadb-libs

The server asks the dynamic linker for four sonames, in this order:

libmariadb.so.3
libmysqlclient.so.21
libmysqlclient.so
libmariadb.so

MariaDB Connector/C is first because it is the client this program bundles on Windows and the one most distributions install. There is no “the library beside the executable” here: a shared object is found along the linker’s own search path — ld.so.conf, LD_LIBRARY_PATH, the program’s RPATH — so what is named is a soname, not a path. If none of the four loads, the server reports a Critical 5094 listing what it tried and the linker’s own last error.

[Database]
Type=MySQL
Server=localhost
Database=hmailserver
Username=hmailserver
Password=...
Port=3306

Write the port out. The value goes into the connection string as it stands and there is no useful default. PostgreSQL wants 5432, MySQL and MariaDB want 3306. MySQL alone falls back to 3306 when Port=0; PostgreSQL would try to connect to port 0.

Since 6.3.4 the connection is encrypted when the database server offers TLS. Up to 6.3.3 the server never asked MariaDB Connector/C for TLS, so the connection was plaintext even to a server that offered it. Now, with nothing set, it uses TLS whenever the server offers it, without checking the certificate, and connects without it when the server offers none. Under [Database], TlsMode=require refuses a connection without TLS (through MariaDB Connector/C, only after the login exchange) and TlsMode=verify-full also checks the certificate against TlsCaFile, a PEM file, or the system’s store when that is empty, but from 6.3.8, through MariaDB Connector/C, refuses a connection to a database server on this machine without TlsCaFile, with HM7535 (name in TlsCaFile the CA that issued the database server’s certificate, or set TlsMode=require); --check-database (§35.3, step 6) says what the connection got. The database server must be MySQL 5.7.9 or MariaDB 10.2.2 or later, since every table is utf8mb4 from schema 6102. Oracle’s libmysqlclient.so.24, MySQL 8.4’s client, is not among the four names above.

The Type key, and the two that are refused

Type= Reported as On Linux
PostgreSQL Database type: 3 Works, through libpq
MySQL Database type: 1 Works, through the dlopened client
MSSQL Database type: 2 Refused by name. SQL Server is reached through ADO, which is Windows-only
MSSQLCE Database type: 4 Refused by name. SQL Server Compact is Windows-only
anything else, or empty Database type: 0 The server refuses to start rather than half-starting

Spellings are compared case-insensitively. The refusal happens twice over and neither is a connection error. --create-database and --upgrade-database refuse before touching the network, on standard error, exit 2 —

[Database] Type is SQL Server, which this build does not have: ADO and SQL Server Compact are
Windows. PostgreSQL and MySQL are the backends here.

— and at run time the connection factory reports a Critical 6390 rather than quietly returning an empty connection the caller would dereference. The consequence is worth stating plainly: an INI carried across from a Windows installation is repointed here or it does not start. Internal=0 is likewise the only value that works; Internal=1 marks the Windows installer’s built-in database, which is SQL Server Compact (and, in installations from long ago, the bundled MySQL server), and there is none here.

What --create-database does

It is what DBSetupQuick does on Windows, without COM. It reads [Database], refuses the two Windows backends by name, and then:

  1. Tries the named database first. If it opens and hm_dbversion has a row, the create script is not run: it must never run over an existing schema. Since 6.3.4 a database that already holds this build’s schema, or an older one, is left as it is with exit status 0 and nothing marked (an older one is moved on by --upgrade-database), and a newer one exits 1. Up to 6.3.3 it refused any existing schema, naming that version and --upgrade-database.
  2. If it opens and holds no hMailServer schema — the usual shape when you ran createdb yourself — it fills the empty database.
  3. Otherwise it connects to the server with no database named, runs CREATE DATABASE in the backend’s dialect, reconnects, and runs CreateTablesPGSQL.sql or CreateTablesMySQL.sql from <ProgramFolder>/DBScripts.

The create script produces the current schema outright, so a fresh database needs no upgrade afterwards.

Database hmailserver created on PostgreSQL at schema version 6105, from
/usr/share/hmailserver/DBScripts/CreateTablesPGSQL.sql.

Since 6.3.4 it goes on to say whether the first start will secure the new database (§35.3, step 7) and, run as the service user, that it made no stored-secrets key and what to run instead (step 5).

Proven, not assumed. The full sequence — create the schema, start the server, create an account over REST, submit a message over authenticated SMTP, deliver it, read it back over IMAP with its subject intact — was run against PostgreSQL 18 on 8 September 2026 and against MariaDB 11.8 on 9 September 2026.

35.5 The configuration file

One file, hMailServer.ini, with the sections and keys chapter 21 documents. What differs here is where it is found and who may write it.

Where it is looked for, and in what order

  1. --config <file>, if given. Applied before anything else reads the configuration, so it wins outright.
  2. hMailServer.ini beside the running executable — the directory /proc/self/exe resolves to. Tried first so a build tree or an unpacked tarball needs no argument.
  3. /etc/hmailserver/hMailServer.ini — used when there is no file beside the executable and either that file exists or the directory /etc/hmailserver exists.

The second half of that last condition is deliberate. After an apt-get remove, dpkg’s conffile record keeps the file from coming back on a reinstall; without the directory test, --set-admin-password would create /usr/bin/hMailServer.ini and write the hash there — a shadowing configuration nobody knows they have. hmailserver --help states the same order.

The comment and encoding rules

The file is a from-scratch reimplementation of the Win32 profile API, so it keeps Win32’s rules rather than inventing POSIX ones:

  • Only ; starts a comment, and only at the start of a line.
  • Text after a value on the same line is part of the value. Type=PostgreSQL ; the backend sets the type to the string PostgreSQL ; the backend, which matches no backend name, so the server reports Database type: 0. This is why every note in the packaged file is on a line of its own.
  • A line beginning # is a key whose name begins with #, as Win32 makes of it — not a comment.
  • UTF-8 without a byte order mark. “Narrow” means UTF-8 here, not a Windows ANSI code page; a file that is not valid UTF-8 is round-tripped as Latin-1 rather than turned into question marks.
  • Comments, blank lines and key order all survive a write by the server, so an edited file stays an edited file.
  • Section and key names match case-insensitively; whitespace around a key or value is separator, not content; a matching pair of quotation marks around a value is stripped.
  • A missing file, section or key gives the caller’s default, silently. A key that is present but empty does not — it reads as an empty string, and as 0 where an integer is wanted. Since 6.3.4 neither applies to a [Settings] key the database holds: its stored value is used, so deleting the line does not return it to its default, and emptying it is reported as HM5804 and not applied (chapter 21).

Who may write it

Only root. The COM administration API, which rewrites it on Windows, is not in this build, so the file is 0640 root:hmailserver — the service reads it through the group and cannot write it — the unit mounts /etc read-only for the service, and Passwordencryption=0, plain text, is this file’s normal state here. The command-line verbs that write it run as root for that reason: --set-admin-password and, since 6.3.4, --set-setting, --create-stored-secrets-key, --import-stored-secrets-key and --forget-sealed-secrets (§35.7). Since 6.3.4 [Settings] is the database’s copy (chapter 21), and the server writes each stored value back into it. Here it cannot, so the copy is not refreshed and the database’s values are used: a value stored over the REST API is in effect, but it is reported as HM5806 when it is stored and as HM5803 at every start until the key’s line in the file holds the same value. --set-setting writes the line as well as the database, which ends that report.

--check-config is a report, not a validator. It returns 0 unconditionally, so it cannot be a pass/fail gate in a script — read its output. It prints only three of the six directories. The diagnostic that lists all six and says whether each exists is Installation paths, reached over the API or the Control Deck rather than from the command line.

The keys the packaged file sets

Every key below is a [Settings] key, and since 6.3.4 the database holds them: the server’s first start takes in the values the file sets, and after it a line edited in the file for a key the database holds is reported as HM5804 and not applied. A line for a key the database has never held, such as the two commented out below, is taken in at the next start or reload. Change a key with sudo hmailserver --set-setting NAME=VALUE, over the REST API (PUT /api/v1/settings/ini/{name}) or with hmctl ini set (chapter 21).

Key Packaged Meaning on Linux
ProtectStoredSecretsWithDPAPI 1 Named for the Windows store and kept for compatibility; here 1 means a key file (§35.12). 0 deliberately selects the old fixed-key Blowfish scheme — “portable and protected from nobody who has the binary”. This build never falls back to it on its own. Since 6.3.4 0 also keeps an account’s second factor and the [Settings] secrets as written, rather than sealed under the stored-secrets key.
RestApiPort 0 (off) The only administration surface here. It will not start until AdministratorPassword is set.
RestApiBindAddress 127.0.0.1 TLS is required unless the address is exactly 127.0.0.1, localhost or ::1.
RestApiCertificateFile, RestApiPrivateKeyFile commented out Needed to bind anything but loopback.
WindowsEventLogEnabled 1 syslog here, identity hmailserver, facility LOG_MAIL — so operational events reach the journal.
WindowsEventLogLevel 2 1 = Critical, 2 = + High, 3 = + Medium, 4 = everything. Protocol chatter never goes to syslog whatever this says.
LogDeleteDays 0 Leave at 0 while the logrotate rule is in place (§35.11).
ShutdownDrainSeconds 0 How long a stop waits for sessions in flight. 0 drops a delivery mid-DATA. Raise TimeoutStopSec with it.
SepSvcLogs 0 One combined log, which makes the logrotate glob simplest. 1 splits SMTP, POP3 and IMAP.
MinimumFreeDiskSpaceMB / DiskSpaceWarningThresholdMB 100 / 1024 Below the floor, mail is refused with a temporary error rather than accepted and possibly lost. The threshold is where you are told, well before that.

35.6 The filesystem layout

Path Owner, mode What lives there Back up?
/usr/bin/hmailserver root The server. One file. No
/usr/bin/hmctl root Since 6.3.4: the command-line client over the REST API (§35.10), including hmctl ini set for the [Settings] keys. No
/usr/bin/tlds.txt root Up to 6.3.5 only: SURBL’s two-level domains of 2006, read from the executable’s own directory, and 4335 when it was missing. 6.3.6 removed it: the Public Suffix List compiled into the server decides, 4335 is retired, and the upgrade removes the file. No
/usr/bin/dh2048.pem root The finite-field Diffie-Hellman group, also read from the executable’s own directory. Without it every TLS context reports a critical 5603 and comes up without it: the server keeps running, and keeps accepting TLS over ECDHE, while writing a critical error for every listener and every outbound delivery. No
/usr/share/hmailserver/DBScripts/ root Schema creation and upgrade scripts. Read from ProgramFolder. No
/usr/share/hmailserver/WebAdmin/index.html root The Control Deck, served at GET /. Read from ProgramFolder. No
/usr/lib/systemd/system/hmailserver.service root The unit. Replaced on upgrade — change it with a drop-in (§35.8). No
/usr/lib/hmailserver/hmailserver-update, and the units hmailserver-update.path and hmailserver-update.service root Since 6.3.4: the live update’s apply step, a shell script run as root outside the server’s unit. When a request appears in /run/hmailserver, the path unit, which the .deb and .rpm enable (on Arch, run sudo systemctl enable --now hmailserver-update.path), starts the service unit, which runs the script (§35.13). No
/etc/hmailserver/ root:hmailserver 0750 The configuration directory, and a good place for certificate and key files. Yes
/etc/hmailserver/hMailServer.ini root:hmailserver 0640 The configuration. Marked so an upgrade never overwrites it: a conffile in the .deb, %config(noreplace) in the .rpm, backup= in the PKGBUILD. Yes
/etc/logrotate.d/hmailserver root The rotation rule. Also a conffile. If edited
/var/lib/hmailserver/ hmailserver 0750 The mail store. One directory per domain, one per mailbox, one file per message. The path that grows, and the one to put on the volume with the space. Since 6.3.4 a domain cannot be named after a directory the server keeps here — temp, events and database among them — and a domain that already has such a name is reported as HM7597 at every start: rename it. Yes — irreplaceable
/var/lib/hmailserver/.hmailserver-secret-key hmailserver 0600 32 random bytes, made when first needed — by the server the first time a secret is stored or, since 6.3.4, by a root-run verb such as --create-stored-secrets-key, for the service’s user — and never rewritten. Every stored password is encrypted under it. Since 6.3.4 it also seals the stored-secrets key line in hMailServer.ini. That key, which opens the second factors and [Settings] secrets in the database, is in no backup archive and no database dump: export it with sudo hmailserver --export-stored-secrets-key FILE and keep the file with the database backups, stored apart from them (§35.7). Yes — with the database
/var/lib/hmailserver/ACME/ hmailserver The ACME account key and the issued fullchain.pem and privkey.pem, unless AcmeCertificateDirectory points elsewhere. Yes
/var/lib/hmailserver/Certificates/ hmailserver Since 6.3.4, on a new installation: the self-signed certificate and its key (mode 0600) that the first start makes for the listeners (§35.9). Yes
/var/lib/hmailserver/temp/ hmailserver 0750 Scratch space, on the same volume as the store on purpose: a virus scanner writes a message here before handing the path to an external scanner, and a backup stages and verifies its archive here. PrivateTmp=yes does not affect it — the server does not use /tmp. No
/var/lib/hmailserver/events/ hmailserver 0750 Where event scripts live: EventHandlers.js, in JavaScript, since 6.3.4 (§28.2). Up to 6.3.3 it was created and stayed empty, because there was no script engine. No
/var/lib/hmailserver/database/ hmailserver 0750 Only the two Windows-only backends put anything here. This build has neither, so it exists and stays empty. No
/var/log/hmailserver/ hmailserver 0750 The logs (§35.11). Optional

Two different lookup mechanisms, and it matters which. dh2048.pem (and, up to 6.3.5, tlds.txt) is read from the running executable’s own directory; DBScripts and WebAdmin are read from ProgramFolder in the configuration. Move either pair and its partner has to move with it.

The six [Directories] keys point at exactly this layout: ProgramFolder=/usr/share/hmailserver, DataFolder=/var/lib/hmailserver, LogFolder=/var/log/hmailserver, TempFolder=/var/lib/hmailserver/temp, EventFolder=/var/lib/hmailserver/events, DatabaseFolder=/var/lib/hmailserver/database. ProgramFolder points at the package’s data directory rather than at /usr/bin because the executable is one file and everything it reads from ProgramFolder is under /usr/share.

Removal and purge

  • Upgrade is not removal. The .deb‘s prerm deliberately does nothing on an upgrade, so the new binary is unpacked under the running process and the service restarted afterwards rather than stopped twice.
  • Remove stops the service and undoes the systemd enablement.
  • Purge (dpkg only; rpm has none) also removes the configuration file, and removes /etc/hmailserver only if it is empty — a certificate you put beside the configuration keeps the directory standing. Since 6.3.4, once the key has been made (sudo hmailserver --create-stored-secrets-key), that file also holds the stored-secrets key ([Security] StoredSecretsKey), which no backup archive carries and without which the second factors and [Settings] secrets sealed in the database do not open: export it first (sudo hmailserver --export-stored-secrets-key FILE) if the database is to be used again.
  • /var/lib/hmailserver and /var/log/hmailserver are always kept, and the script says so on its way out. A package manager is the wrong thing to be deciding that a mailbox is finished with. The database is untouched for the same reason.
  • The hmailserver user and group are kept, so what is left keeps an owner with a name. A numeric owner with no name is how a store becomes unreadable to the reinstall that was meant to pick it up.
# When you are certain, and only then
sudo rm -rf /etc/hmailserver /var/lib/hmailserver /var/log/hmailserver
sudo deluser --system hmailserver     # userdel hmailserver on RPM distributions

35.7 Command-line reference

Up to 6.3.3 the Linux binary had exactly eight options, the ones in the first table below; 6.3.4 adds the verbs in the second. Any other argument prints Unknown option: <argument> and Try --help. to standard error and exits 2.

Flag What it does Exit
--help, -h The usage block, beginning hMailServer 6.3.8. Parsed immediately — nothing after it is read. 0
--version Prints hMailServer 6.3.8 build 47. 0
--config <file> Sets the configuration file, overriding the search order in §35.5. A missing argument prints --config needs a file name. 2 on a missing argument
--foreground Accepted and ignored. The foreground is already the only behaviour — the process never daemonises and never writes a pid file. The option exists so a unit file can say so. —
--check-config Reads the configuration, prints five lines, exits without opening a listener or the database. 0 always
--set-admin-password Reads a password from stdin, PBKDF2-hashes it, writes it to [Security] AdministratorPassword. 0; 2 when empty (No password was given; nothing was changed.)
--create-database Creates the database and runs the create script (§35.4). Since 6.3.4 it marks a database it creates or fills as a new installation’s and leaves .new-installation in the DataFolder for the unit’s first start (§35.8), and it leaves a database that already holds this build’s schema, or an older one, as it is. Run as the service user it cannot write the configuration, so it makes no stored-secrets key and says what to run instead. 0, also for a database it leaves as it is (up to 6.3.3 that was 1); 1 on any connection, CREATE or script failure, or a database newer than the build; 2 when [Database] names no Type or Database, or a Windows-only backend
--upgrade-database Walks the upgrade-script chain to the schema this build needs (§35.13). 0 on success or “nothing to do”; 1 for no hm_dbversion row, a database newer than the build, a broken chain, a failed script, or a script that runs without advancing the version; 2 as above
(no flag) Runs the server: install the crash oracle, initialise, open the listeners, wait for a signal. 0 on a clean stop; 1 if initialisation fails (hMailServer could not start: ...) or the listeners cannot be opened

Among these eight, when several are given the first of these wins: --set-admin-password, then --create-database, then --upgrade-database, then --check-config, then run the server. --config and --foreground are modifiers and apply wherever they appear.

Since 6.3.4 the binary also has these verbs:

Flag What it does Run as
--check-database Makes one connection to the database with its TlsMode and TlsCaFile and says whether it is encrypted and whether the certificate was checked, or why there is no connection. Changes nothing, so it runs beside a running service. Exit 0 connected, 1 not, 2 not configured. The service user
--set-setting NAME=VALUE, --set-setting NAME Stores one [Settings] value in the database and writes the file’s copy: the way in for the settings needed before the REST API can be reached, RestApiPort above all. With the name alone the value is read from standard input, and a secret’s value is refused on the command line. Refuses a name the server does not read, a port outside 0 to 65535 and a bind address that is not an IP address or localhost. A running server takes the value at its next save, reinitialise or restart. root
--apply-environment Stores every setting the environment names (HM_SETTING_<key>, HM_PROPERTY_<name>, their _FILE forms and the container’s HM_REST_*), each checked first: if one is refused nothing is stored and it exits 1. An administrator’s change to a setting it names is then refused until a run without it. It writes the file’s copy too, so the file must be writable: on a package installation, whose file is root’s, it refuses before storing anything. The container’s entrypoint runs it, as the user that owns the image’s configuration. The service user
--apply-profile NAME [--confirm] [--once] [--if-new], --revert-profile Applies one of the four setting profiles, or puts back what the last one changed. --confirm is needed where an entry closes a listener, trusts an address range, weakens a check or shortens a window; --if-new does nothing on a server with ten accounts or more. Exit 0 done or nothing to do, 1 refused or failed, 2 no such profile or no database, 3 an entry needs --confirm, 5 --if-new on a server in service. The service user, with the server stopped
--apply-new-install-defaults [--if-marked [<file>]] A new installation’s secure defaults (§35.9), only on a database --create-database marked and still as the create script left it: otherwise exit 5 and nothing changed, and exit 2, the mark kept, when the database cannot be read well enough to tell. The unit runs it with --if-marked before every start (§35.8), where a mark more than a day from the machine’s clock also gives exit 5, the mark kept for a run by hand. The service user, with the server stopped. Refuses root
--sample-domain create, --sample-domain remove Makes, or removes again, the throwaway example.test: three mailboxes, a list, a rule and eight messages. The mailboxes’ one password is minted when the sample is made and shown once. Remove it before the server takes real mail. Exit 0 done, 2 refused, 1 could not. The service user
--measure-store, --compress-store [--decompress], --recount-store What compressing the message store and holding its large parts once would save, the rewrite either way, and a recount of the shared parts. The rewrite and the recount are refused while the server runs (exit 4). The service user
--encrypt-store, --decrypt-store Encrypt what a mailbox encrypted at rest stored before encryption was switched on, or, for one account or domain, decrypt every file encrypted to its key with the domain’s recovery passphrase, read from HMAILSERVER_RECOVERY_PASSPHRASE and never from the command line. After decrypting, switch the mailbox’s encryption off before the server starts again. Refused while the server runs (exit 4). The service user, with the server stopped
--copy-store-to-object-storage, --copy-store-from-object-storage Copy the message store into the object store the settings name, or back, each file verified and a stopped run resumed. Exit 0 nothing failed, 1 a name not copied or verified, 2 no store configured. The service user
--verify-update <file> Checks a downloaded package or AppImage against the Sigstore bundle beside it, as the server does before applying one; exit 0 when a signer the server trusts made it. The live update’s root helper runs it again before installing. The live update’s root helper, as root
--create-stored-secrets-key, --export-stored-secrets-key <file>, --import-stored-secrets-key <file>, --forget-sealed-secrets <key id> Make the key the second factors and [Settings] secrets are sealed under in the database, export it to keep with the database backups but apart from them, import it on another node that shares the database, or, with the key lost for good, clear what was sealed under it. Restart the service after an import. --forget-sealed-secrets is refused while the server runs (exit 4). root

The updater’s flags do not exist here. Options such as --service, --installer, --rollback and --rollback-version belong to the Windows live-update helper, which is excluded from the Linux build. Passing any of them gives Unknown option: and exit 2. Since 6.3.4 a Linux server’s live update is applied by a root helper outside the unit, /usr/lib/hmailserver/hmailserver-update (§35.13), which checks the package with --verify-update.

Signal Effect
SIGTERM, SIGINT Stop. The wait loop ends, the listeners stop accepting, and the application drains for ShutdownDrainSeconds before exiting.
SIGHUP Re-read the configuration, the way the Control Panel’s Apply now does. Logged as SIGHUP: re-reading the configuration. This stops and restarts every listener inside the running process — sessions in flight are dropped.
SIGPIPE Ignored. A peer closing a socket mid-write must not kill the process.

The handlers are installed without SA_RESTART, on purpose: a blocking read in a worker should come back with EINTR during a stop rather than hold the shutdown open.

35.8 The systemd service

On Windows the server registers itself with the Service Control Manager from inside its own process. Here the supervisor is declared rather than programmed, and /usr/lib/systemd/system/hmailserver.service is the whole of that declaration.

[Unit]
Description=hMailServer, a mail server for SMTP, IMAP and POP3
Wants=network-online.target
After=network-online.target
After=postgresql.service mariadb.service mysql.service mysqld.service

[Service]
Type=simple
ExecStartPre=-/usr/bin/hmailserver --apply-new-install-defaults --if-marked
ExecStart=/usr/bin/hmailserver --foreground
User=hmailserver
Group=hmailserver
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
TimeoutStartSec=300
TimeoutStopSec=300
WorkingDirectory=/var/lib/hmailserver
RuntimeDirectory=hmailserver
ReadWritePaths=/var/lib/hmailserver /var/log/hmailserver

[Install]
WantedBy=multi-user.target

Three lines are new in 6.3.4. ExecStartPre runs before every start and exits at once, opening nothing, unless --create-database left .new-installation in the DataFolder; at the first start after it, it applies a new installation’s secure defaults (§35.9) to a database marked within a day of the machine’s clock, and removes the file either way; a database it cannot reach at that one try leaves the file, so the next start asks again. The - forgives a failure and not a timeout, so TimeoutStartSec=300 gives its one database attempt room. A DataFolder moved outside ReadWritePaths needs a drop-in adding it before that first start (below). RuntimeDirectory=hmailserver is /run/hmailserver, where the server leaves a live update’s request for the root helper (§35.13).

network-online rather than network, because a listener binds an explicit address whenever a bind address is not 0.0.0.0, and a bind to an address the machine does not have yet fails outright rather than waiting.

The four database units are After= only, and deliberately not Requires= or Wants=: the backend is as often on another host, and a hard requirement on a unit that is not installed would refuse to start the server at all. Naming an absent unit in After= is a no-op, so all four can be listed. The server survives a database that is not up yet in any case — it retries ConnectionAttempts times, ConnectionAttemptsDelay seconds apart.

It runs as hmailserver:hmailserver. Never root. Run everything that touches the store or the database as the same user; only --set-admin-password and, since 6.3.4, --set-setting and the four stored-secrets key verbs are root, because the configuration file is root-owned and they write it or read the key from it. --apply-new-install-defaults refuses root, since the certificate key it writes must be the service’s.

sudo systemctl start hmailserver
sudo systemctl stop hmailserver
sudo systemctl reload hmailserver        # SIGHUP: re-read the configuration
sudo systemctl status hmailserver
journalctl -u hmailserver -f
journalctl -u hmailserver --since "1 hour ago" -p warning

sudo -u hmailserver hmailserver --check-config
sudo -u hmailserver hmailserver --upgrade-database

reload is not free. It sends SIGHUP, which is Reinitialize: the listeners stop, the configuration is re-read, and they start again inside the running process. Connections in progress are dropped. Do it when nothing is mid-delivery. It is offered as reload rather than done automatically for that reason, and log rotation deliberately does not use it.

TimeoutStopSec and ShutdownDrainSeconds are a pair. The server’s drain setting decides how long it waits for active sessions; the unit’s timeout is the ceiling systemd puts on that, after which it kills the process whatever the server was waiting for. The unit’s value must be comfortably larger. Raise both together, never just one.

The hardening

Directive Value Why
AmbientCapabilities, CapabilityBoundingSet CAP_NET_BIND_SERVICE Ports 25, 110, 143, 465, 587, 993 and 995 are below 1024, as are ACME’s port 80 and, since 6.3.4, the SNMP agent’s UDP 161, and the process never runs as root. The bounding set is emptied of everything else.
NoNewPrivileges yes Ambient capabilities survive it, which is why both can be set.
ProtectSystem strict /usr, /boot and /etc read-only, everything under /var read-only except the two ReadWritePaths. /etc/hmailserver is deliberately on the read-only side: the configuration is root’s, and the verbs that write it run as root (§35.5). Since 6.3.4 the server’s own write of a stored setting back into the file’s copy fails here by design: the stored value is used, and the key is reported as HM5806 when it is stored and HM5803 at each start until the line holds the stored value, which --set-setting writes.
ProtectHome, PrivateDevices yes
PrivateTmp yes The server’s temporary files go to TempFolder, not /tmp, so this costs nothing. The exception is a Custom or ClamWin scanner, which hands a file path to an external program: a helper started outside this unit cannot see into a private /tmp. clamd is unaffected — it is reached over TCP with INSTREAM.
ProtectKernelTunables, ProtectKernelModules, ProtectKernelLogs, ProtectControlGroups, ProtectClock, ProtectHostname yes
ProtectProc / ProcSubset invisible / pid /proc/self/exe is the only thing under /proc this program reads — it is how the installation directory is found, and therefore how the configuration is found at all. Both settings leave a process’s own entries alone.
RestrictNamespaces, RestrictRealtime, RestrictSUIDSGID, LockPersonality, RemoveIPC yes
RestrictAddressFamilies AF_INET AF_INET6 AF_UNIX AF_NETLINK AF_UNIX is syslog and the name service switch. AF_NETLINK is what glibc’s getaddrinfo uses to enumerate local addresses, and without it name resolution degrades in ways that are hard to attribute afterwards.
SystemCallArchitectures / SystemCallFilter / SystemCallErrorNumber native / @system-service / EPERM
UMask 0027 A message file is readable by the group and by nobody else. The store holds other people’s mail and the logs hold their addresses.
LimitNOFILE 65535 One connection is one descriptor, and so is every open message file and database connection. The usual 1024 is reached by a few hundred concurrent IMAP sessions, and what happens then is accept() failing on a healthy server.

What is deliberately not enabled

The unit lists six options it does not set, each with its reason, so nobody adds them from a checklist:

  • DynamicUser=yes — the mail store outlives every version of the program; a uid allocated afresh on each start would leave it owned by a user that no longer exists.
  • PrivateUsers=yes — it puts the service in a user namespace, where CAP_NET_BIND_SERVICE applies to that namespace and not to the host’s ports. The server could not bind 25.
  • MemoryDenyWriteExecute=yes — not verified against this program. The MySQL/MariaDB client is loaded with dlopen, and an unverified W^X policy that breaks the database client breaks the server. Very likely safe, which is not the same as known to be.
  • StateDirectory= and LogsDirectory= — they would create and own the two directories. The package creates them instead, so that removing the package, and nothing else, decides what happens to a mail store.
  • IPAddressDeny=any with an allow list — an MTA talks to every mail server on the internet. There is no list to write.
  • PrivateNetwork=yes — named only because it appears on every hardening list: a mail server with no network is not a mail server.

LimitCORE is commented out rather than set: a core of a mail server holds message bodies, passwords and TLS keys, so the limit is left at the distribution’s default and turned on only for the time it takes to catch one.

Changing the unit

Always with a drop-in, because an upgrade replaces the unit file and a drop-in survives it:

sudo systemctl edit hmailserver
[Service]
# A backup destination or an ACME directory outside the store must be named
# here, or the write fails: everything else under /var is read-only.
ReadWritePaths=/srv/backup/hmailserver
TimeoutStopSec=600

ReadWritePaths is the only writable ground the service has for data (since 6.3.4 the unit’s RuntimeDirectory, /run/hmailserver, is writable too, for a live update’s request). An archive directory, an ACME certificate directory and a backup destination are all administrator-chosen and may sit elsewhere; a path outside the list is read-only to the service and the write fails. This is the single most common cause of a Linux installation that runs perfectly and cannot write a backup.

35.9 Certificates on Linux

Certificates work as chapter 9 describes. Two things are specific to this platform: how you configure them without a Control Panel, and where the files may live.

Since 6.3.4 a new installation starts with one. When the unit’s first start comes within a day of --create-database, it makes a self-signed certificate for the machine (RSA 2048, 825 days, under /var/lib/hmailserver/Certificates, recorded as Self-signed (made at installation)), gives it to the four listeners with STARTTLS optional, and requires TLS for authentication on the Internet IP range. It acts only on a database still as the create script left it, and never on an upgraded or attached one. A first start more than a day later changes nothing; with the service stopped, sudo -u hmailserver hmailserver --apply-new-install-defaults applies the defaults then. Mail clients warn about a self-signed certificate, so replace it, with ACME or your own.

How a certificate is configured

A certificate here is a database row with a name and two file paths; a listener is another row naming a certificate by id. Neither is an INI key, so neither is edited in hMailServer.ini. On Linux you create both over the REST API, or on the Control Deck’s Certificates and Ports views.

API=http://127.0.0.1:8045/api/v1
AUTH='-u Administrator:your-admin-password'

# Both files must exist or the request is refused, naming the one that does not.
curl $AUTH -H 'Content-Type: application/json' -X POST "$API/certificates" \
  -d '{"name":"mail.example.com",
       "certificate_file":"/etc/hmailserver/fullchain.pem",
       "private_key_file":"/etc/hmailserver/privkey.pem"}'

# A new listener, here SMTP with implicit TLS on 465. The create script's 25, 587, 110 and 143
# are already there, and a POST for an address and port already listened on is refused (409):
# give one of those the certificate with a PUT of its whole record to "$API/ports/<id>",
# the id from GET "$API/ports".
curl $AUTH -H 'Content-Type: application/json' -X POST "$API/ports" \
  -d '{"protocol":"smtp","address":"0.0.0.0","port":465,
       "connection_security":"tls","certificate_id":3}'

# A listener takes effect on restart - or now, without stopping the process
curl $AUTH -X POST "$API/server/reinitialize"

Paths are checked when the certificate is created, so a typo is refused there rather than saved for the listener to fail on at the next start. A certificate cannot be deleted while a port binds it, and the refusal names the port.

The service must be able to read the key. It runs as hmailserver, not as root. A private key at 0640 root:hmailserver in /etc/hmailserver is readable by the service and by nobody else. A key under /etc/letsencrypt/live is typically not readable by a non-root service without a deploy hook that copies it or a group that grants it.

ACME

AcmeEnabled=1
[email protected]
AcmeDomains=mail.example.com
AcmeDirectoryUrl=https://acme-v02.api.letsencrypt.org/directory
AcmeHttpPort=80
AcmeReuseKey=1
;AcmeCertificateDirectory=/var/lib/hmailserver/ACME

Since 6.3.4 these are stored settings (chapter 21). A line for a key the database has never held is taken in at the next start or reinitialise; once the database holds a key, a line edited in the file is reported as HM5804 and not applied. On a running server set them over the REST API, with hmctl ini set or with sudo hmailserver --set-setting NAME=VALUE. AcmeEnabled and AcmeCertificateDirectory take effect at a reinitialise (sudo systemctl reload hmailserver), the others when saved over the API; a value stored with --set-setting reaches a running server at its next save, reinitialise or restart.

  • Port 80 works without root. The HTTP-01 challenge needs a listener on AcmeHttpPort, and the unit’s CAP_NET_BIND_SERVICE is what lets a non-root process bind it. Nothing else on the machine may already hold that port.
  • The default certificate directory is inside the writable ground. Unset, it is <DataFolder>/ACME, which is inside ReadWritePaths. Point AcmeCertificateDirectory elsewhere and you must add that path to ReadWritePaths with a drop-in, or issuance fails on a read-only filesystem.
  • Deployment is automatic and visible. After a successful issuance the server creates or updates a certificate record named ACME (automatic) and assigns it to every TLS-enabled listener that has none of its own and, since 6.3.4, to every listener that holds the self-signed certificate a new installation made, logging each assignment; a listener holding a certificate somebody chose is left alone. A listener the assignment could not be saved for is reported as a High 6100 rather than logged as a success, because a port left without a certificate accepts no TLS at all.

To use an external client instead — certbot, acme.sh, your own CA — leave AcmeEnabled=0, put the PEM files where the service can read them, and create the certificate record over the API as above. A renewal that replaces the files in place needs a POST /api/v1/server/reinitialize or a restart to be picked up.

TLS for the administration listener

Its rule is a security gate, not a convenience. TLS is required unless the bind address is exactly 127.0.0.1, localhost or ::1 — those three literals and nothing else; 127.0.0.2 and a mapped form are not exempt. Bound elsewhere without a certificate it refuses to start:

RestApi: Refusing to start - TLS certificate is required unless bound to 127.0.0.1 or ::1.
Set RestApiCertificateFile and RestApiPrivateKeyFile.
RestApiPort=8443
RestApiBindAddress=0.0.0.0
RestApiCertificateFile=/etc/hmailserver/rest.pem
RestApiPrivateKeyFile=/etc/hmailserver/rest.key

Since 6.3.4 those four are stored settings: once the database holds one — RestApiPort and RestApiBindAddress from the first start, which took them from the packaged file — a line edited in the file is not applied (§35.5). So store all four with the command line, since the listener may not be reachable yet, and reload: all four apply at a reinitialise.

sudo hmailserver --set-setting RestApiPort=8443
sudo hmailserver --set-setting RestApiBindAddress=0.0.0.0
sudo hmailserver --set-setting RestApiCertificateFile=/etc/hmailserver/rest.pem
sudo hmailserver --set-setting RestApiPrivateKeyFile=/etc/hmailserver/rest.key
sudo systemctl reload hmailserver

If RestApiCertificateFile is empty, the server uses the ACME directory’s fullchain.pem and privkey.pem when both are there, and logs that it has. The simplest safe arrangement on a server with no public administration surface is to leave it on loopback and tunnel:

ssh -N -L 8045:127.0.0.1:8045 [email protected]

dh2048.pem belongs beside the binary. It is read from /usr/bin/, beside the executable rather than from ProgramFolder. Missing, every TLS context reports a critical 5603 and is built without the finite-field Diffie-Hellman group. The server goes on running and goes on accepting TLS through ECDHE, so the symptom is an error log filling with criticals rather than a listener that stops working. If that is what you are looking at after moving or repackaging the binary, check this file first.

35.10 Administering a Linux server

There is no Control Panel here and no COM. The Windows administration program is a .NET 10 desktop application that drives the server over COM, and neither is in this build. Three surfaces replace them: the configuration file (§35.5), which since 6.3.4 keeps [Database], [Directories], [Security], [LDAP] and a few other keys while the database holds every other [Settings] key; the command line (§35.7) for the administrator password, the schema and, since 6.3.4, a [Settings] value that has to be set before the API can be reached (--set-setting); and the REST API (chapter 24) with the Control Deck (chapter 36) and, since 6.3.4, hmctl, a command-line client over the API, for everything else.

The Control Deck

Turn the API on (§35.3 step 8) and open http://127.0.0.1:<RestApiPort>/. The page is a file the package installs at /usr/share/hmailserver/WebAdmin/index.html, found through ProgramFolder. Sign in as Administrator, with the one-time code when a second factor is enrolled; the password is exchanged once for an HttpOnly, SameSite=Strict session cookie, which ends after thirty idle minutes or twelve hours, and is not kept by the page. Since 6.3.4 an account that holds an administrator role signs in too and sees only what its role allows; the administrator can also sign in with a passkey enrolled on the Deck’s Passkeys view, and an address on the server’s list of Deck administrators through the organisation’s OpenID Connect or SAML identity provider. No API key signs in to it. It has forty views in 6.3.4 (fifteen in 6.3.3), among them Dashboard, Checklist, Domains, Lists, Delivery queue, Settings, Rules, Routes, Remote domains, Certificates, Ports, Roles, Logs, Audit trail, Update and API keys. Its settings forms are generated from the server’s own OpenAPI document, so every setting of its settings groups appears with its type, its permitted words and a mark when it is read-only or write-only. Since 6.3.4 each field kept in the settings store carries a badge saying when a save applies (on save, after a reinitialise or after a restart), the saved settings still waiting are listed above the groups with Apply now, and a setting the environment manages is marked Set by the environment. A [Settings] key no group carries is set over the API (PUT /api/v1/settings/ini/{name}) or with hmctl ini set.

If the page says “Web administration page not installed”, that file is missing and the server has fallen back to a built-in stub. --check-config will not tell you — the page is not one of the paths it prints. The Installation paths diagnostic does report it, and so does looking at the file.

What the Deck does not do, and the same writes over the API

In 6.3.4 every property the desktop Control Panel writes that the API carries is reached from a view (458 of them, by the census in DeckParity.md), the domain, the alias, the change to an existing account and the distribution list below included. What the Deck does not do is edit the event script, which is a file on the server’s disk (on Linux, in /var/lib/hmailserver/events), or the alert rules, which are REST and hmctl api only. For a script, or a session with no browser, the calls below are the same writes: use curl or any other HTTP client. Since 6.3.4 a one-time code is accepted once, so a script whose administrator password has a second factor signs in once (POST /api/v1/session) or uses an API key.

API=http://127.0.0.1:8045/api/v1
AUTH='-u Administrator:your-admin-password'
JSON='-H Content-Type:application/json'

# 1. A domain. 201 on success, 409 if it exists, 400 with the reason if the name is not one.
curl $AUTH $JSON -X POST "$API/domains" \
  -d '{"name":"example.com","active":true,"postmaster":"[email protected]"}'

# 2. An alias
curl $AUTH $JSON -X POST "$API/domains/example.com/aliases" \
  -d '{"name":"[email protected]","value":"[email protected]","active":true}'

# 3. Changing an account that already exists
curl $AUTH $JSON -X PUT "$API/accounts/[email protected]" -d '{"active":false}'

# 4. A distribution list
curl $AUTH $JSON -X POST "$API/domains/example.com/lists" \
  -d '{"address":"[email protected]",
       "members":["[email protected]","[email protected]"],"require_auth":false}'

# 5. An account, a POST under the domain
curl $AUTH $JSON -X POST "$API/domains/example.com/accounts" \
  -d '{"address":"[email protected]","password":"...","active":true,"max_size_mb":2048}'

The daily jobs

Job How
Is it up, and is it busy? systemctl status hmailserver, then GET /api/v1/status — version, state, processed, spam and virus counts, and live SMTP, IMAP and POP3 session counts. The Deck’s dashboard re-reads it every three seconds.
Is mail leaving? GET /api/v1/queue, or the Delivery queue view. A growing queue is the earliest warning you get. POST /api/v1/queue/{id}/retry retries one now; DELETE removes it.
What went wrong? journalctl -u hmailserver for operational events; GET /api/v1/logs and GET /api/v1/logs/{name}?lines=N, or the Logs view, for the server’s own logs (§35.11).
Add a domain or a mailbox The Deck’s Domains view creates the domain and then its accounts; over the API, the POSTs above.
Change a setting The Settings view, or PUT /api/v1/settings and its named groups (/api/v1/settings/antispam, /logging, /backup and the others the OpenAPI document lists), each key applied through the same setter the Control Panel calls, and applied only when every key in the request is accepted. Since 6.3.4 the [Settings] keys are stored in the database: set one with PUT /api/v1/settings/ini/{name}, which takes the administrator password and refuses every API key, or hmctl ini set, never by editing the file, whose edited line is reported as HM5804 and not applied. 211 keys apply when saved, 28 at a reinitialise (the Deck’s Apply now, POST /api/v1/server/reinitialize or systemctl reload) and 2 at a restart. sudo hmailserver --set-setting is for when the API cannot be reached; what it stores reaches a running server at its next save, reinitialise or restart. The file’s own sections, [Database], [Directories] and the rest, are still edited in the file and apply at a restart. Up to 6.3.3 every INI key was the file: edit it and systemctl reload.
Add a listener or certificate The Ports and Certificates views, or §35.9. Then POST /api/v1/server/reinitialize.
Restart the services in place The Ports view has a button. It confirms first, because connections in progress are dropped and the browser session ends with them. The route answers 202 before it happens — poll GET /api/v1/status until it answers again.
Run a backup POST /api/v1/backup starts one with the configured settings; GET /api/v1/backup is the status text and the tail of the backup log. §35.12 for what it does and does not cover.
Check for a new version POST /api/v1/update/check. It names the package your machine would install; up to 6.3.3 nothing can install it, and since 6.3.4 POST /api/v1/update/install applies a signed one (§35.13).
Give an integration a credential POST /api/v1/apikeys, or the Deck’s API keys view — scoped read-only or full, since 6.3.4 with an administrator role, optionally restricted to named domains and source addresses, and expiring after 90 days unless another date is given. The clear-text token exists once, in that response. A key never reaches /api/v1/settings/ini, the audit trail or the alerts, and never signs in to the Deck.

Mailbox users have the self-service portal at /portal on the same listener (chapter 36). It is compiled into the binary rather than read from a file, so it is there on a machine that has nothing else.

35.11 Logging and log rotation

File in /var/log/hmailserver What it is
hmailserver_<date>.log The main log. A new one each day.
ERROR_hmailserver_<date>.log The error log. A new one each day. Read this one first.
hmailserver_awstats.log, _backup.log, _events.log Grow without bound. These are the real reason the rotation rule exists.
hmailserver_SMTP_<date>.log, _POP3_, _IMAP_ Only when SepSvcLogs=1.
crash-oracle.log The crash record — see below.
sudo tail -f /var/log/hmailserver/ERROR_hmailserver_$(date +%F).log

syslog and the journal. WindowsEventLogEnabled=1 is a Windows inheritance in name only: here it sends operational events — the database going away, a listener that could not bind, a failed backup, the disk floor — to syslog under the identity hmailserver with facility LOG_MAIL, and so to the journal. Same list of events and same throttle as the Windows event log. A healthy server writes none of them. WindowsEventLogLevel=2 means Critical and High; protocol chatter never goes to syslog whatever it is set to. The journal also carries the startup line and anything on standard error, because the unit is Type=simple with --foreground:

hMailServer 6.3.8 is running. Send SIGTERM to stop it.

The whole log to a collector, since 6.3.4. A separate sink, off as shipped, copies every entry of the server’s own log as RFC 5424 to a syslog collector over UDP, TCP or TLS, or, with no collector host set, to the journal. It is a copy: the files above are written as before. It is set with the syslog_* keys of /api/v1/settings/logging (the Deck’s Settings view; hmctl settings get logging prints them) and takes effect at the next start or reinitialise. Errors are always sent, whatever categories are chosen.

The crash record. Handlers for SIGSEGV, SIGBUS, SIGILL, SIGFPE and SIGABRT write a one-line record to crash-oracle.log before the process dies, then let it die by the signal it caught, so the kernel’s own core policy applies. During a start-up fault, before the log directory has been resolved, the record goes to /tmp/hmailserver-crash-oracle.log instead — worse than the log directory and better than nowhere. Same file name and record shape as on Windows.

Log rotation

/var/log/hmailserver/*.log {
    su hmailserver hmailserver
    daily
    rotate 14
    compress
    delaycompress
    missingok
    notifempty
    copytruncate
}

copytruncate, and not a signal. By default the server does not hold a log file open at all — it opens in append mode, writes the line and closes. It keeps a handle only for the protocol logs and only when a particular log-mask bit is set, and the only thing that makes it reopen is the file being gone. logrotate’s create defeats exactly that check: a fresh file under the old name means the server sees a file that exists, keeps its old handle, and goes on writing into the rotated inode. There is no signal to send either — the one signal this process has is SIGHUP, which restarts every listener, and rotating a log file is not worth dropping the sessions in flight. copytruncate is safe in both modes: the file is never replaced, so a held handle stays valid, and it is an O_APPEND handle, so the next write after the truncation lands at offset zero. The narrow cost is the lines written between the copy and the truncate. create is deliberately absent — logrotate ignores it under copytruncate anyway, and writing it would be misleading.

su hmailserver hmailserver is required, not decorative. The log directory is owned by the service user and is not world-writable, and logrotate refuses to rotate inside a directory it does not own unless told whose it is.

Use one retention mechanism, never both. LogDeleteDays prunes the same date-stamped files from inside the server. Leave it at 0 while the logrotate rule is in place — two things deleting the same files by two different rules is how a retention policy stops meaning anything. If you would rather the server owned it, set LogDeleteDays and remove the logrotate rule.

35.12 Backups on Linux

Chapter 15 is the strategy. This is what it means here — and there is one item on the list that does not exist on Windows.

What Where Why
Messages /var/lib/hmailserver The mail itself. Irreplaceable.
The database Your PostgreSQL or MariaDB server Accounts, domains, settings, rules, routes, listeners, certificates.
The key file /var/lib/hmailserver/.hmailserver-secret-key Every stored password is encrypted under it, and since 6.3.4 so is the stored-secrets key’s line in the configuration. A database restored without it has lost every one of them, and the error log names the file when that happens.
The configuration /etc/hmailserver/hMailServer.ini Directories, database credentials, the administrator hash, PasswordPepper (kept nowhere else: without it no Argon2id or scrypt password verifies on a machine restored from these backups) and, since 6.3.4, the stored-secrets key. Since 6.3.4 the REST settings and the rest of [Settings] are held by the database, and the section in the file is its copy.
The exported stored-secrets key (since 6.3.4) The file sudo hmailserver --export-stored-secrets-key FILE wrote Each account’s second factor and the [Settings] secrets are sealed under it in the database. It is never in the database or the built-in backup archive, so a database dump restored without the key (this export, or the configuration and the key file above, which carry it too on Linux) brings them back unopenable. Keep it with the database backups, stored apart from them.
Certificates and DKIM keys Wherever you put them; /var/lib/hmailserver/ACME for ACME Recreating these is painful and, for DKIM, means a DNS change.

The key file. Where Windows uses machine-scoped DPAPI, this platform uses a file: 32 random bytes, mode 0600, made the first time a secret is stored and never rewritten. Each secret is AES-256-GCM under that key with a 12-byte nonce and the full 16-byte tag, stored as LINUX1:<base64> so the value says which store wrote it. It is created through a temporary file and a hard link, which fails when the name is taken, so two processes protecting their first secret at the same moment end with one key file between them. It is refused, with the chmod that fixes it, if any account but its owner can read it (HM6411), and the mode is checked on every read. Two consequences, cutting in opposite directions: back it up with the database, because they are useless apart; and it travels — copy the data directory and the database together and every stored password opens on the new machine, which Windows DPAPI cannot offer. Since 6.3.4 each account’s second factor and the [Settings] secrets are sealed under the installation’s stored-secrets key (HMK1:), once there is one. Run as the service user (§35.3 step 5), --create-database cannot write the configuration and makes none: make it with sudo hmailserver --create-stored-secrets-key, and export it at once with sudo hmailserver --export-stored-secrets-key FILE. Its line in hMailServer.ini is sealed under this key file, so the configuration and this file travel together as well.

The built-in backup manager (POST /api/v1/backup) runs 7-Zip, which the .deb recommends: installed without its recommends it has none, and a backup fails with “Could not add the backup settings to the archive”. Since 6.3.4 an archive carries each account’s second factor opened, beside its password hash, so set a backup passphrase, which encrypts it with AES-256 (encryption_passphrase in PUT /api/v1/settings/backup, the Deck’s Backup view, or hmctl backup passphrase set). An encrypted run needs 7-Zip (7zz) or p7zip-full: 7zr, the reduced p7zip build, cannot encrypt and fails the run. The manager stages into TempFolder and writes its archive to the destination in the backup settings. If that destination is not inside ReadWritePaths the write fails. Add it with a drop-in (§35.8), or take the backup from outside the service, which is usually simpler:

#!/bin/bash
set -euo pipefail
DEST=/srv/backup/hmailserver/$(date +%F)
mkdir -p "$DEST"

# 1. The database, consistent in itself
sudo -u postgres pg_dump -Fc hmailserver > "$DEST/hmailserver.dump"
#   MariaDB: mysqldump --single-transaction --routines hmailserver | gzip > "$DEST/db.sql.gz"

# 2. The store, the key file and the configuration
sudo tar -C /var/lib -cf - hmailserver | zstd -T0 -o "$DEST/store.tar.zst"
sudo cp -a /etc/hmailserver/hMailServer.ini "$DEST/"
chmod -R go-rwx "$DEST"

# 3. Prove the dotfile and the ACME directory were caught
zstd -dc "$DEST/store.tar.zst" | tar -tf - \
  | grep -E 'hmailserver-secret-key|hmailserver/ACME/'

Message files and database rows are two halves of one thing. Every file under the data directory has a matching row. A store from one night and a database from another will not agree. Take both at the same point, and take a filesystem snapshot first if the volume supports one.

Restoring: install the same version and let the package create the user and directories; restore /var/lib/hmailserver in full, including the key file, then chown -R hmailserver:hmailserver it and chmod 0600 the key; restore the database and then the configuration, keeping it 0640 root:hmailserver; if the restored configuration does not hold the stored-secrets key, import the key’s exported file with sudo hmailserver --import-stored-secrets-key FILE before the service starts; run --check-config; start the service and read the error log and GET /api/v1/status.

35.13 Upgrading and rollback

Chapter 18 is the general advice. Here is what a Linux upgrade actually does.

sudo apt install ./hmailserver_6.3.8-1~debian13_amd64.deb   # the package you installed from, of the new version, or
sudo dnf install ./hmailserver-6.3.8-1.fc44.x86_64.rpm

On an upgrade, and only on an upgrade, the maintainer script:

  1. Leaves the running service alone while the new binary is unpacked under it. An upgrade is not a removal, and stopping first would make every upgrade an outage twice as long as it needs to be.
  2. Runs --upgrade-database as the service user, but only if a database is configured — it greps the INI for a Type= line. A failure here is reported, not fatal to the package: the binary is already in place, and the server’s own log is a better place to read why than a half-configured dpkg. Read what it printed.
  3. Runs systemctl try-restart, which restarts the service if it was running and does nothing at all if it was not. An installation that was never configured stays stopped; one that was configured does not silently keep running the version just replaced.

Your configuration is never overwritten. A new default lands beside it as .dpkg-dist, .rpmnew or .pacnew; diff those after a release that adds keys. Since 6.3.4 no key is added to [Settings]: a new setting is a row in the database.

Upgrading to 6.3.4 moves the settings into the database. The new server’s first start, which is the maintainer script’s try-restart if the service was running, makes the database the settings store: keys the 6.3.3 server already mirrored keep their stored values, and any [Settings] key of the file it never held is taken in. A [Settings] line edited after the old server last started, one edited to prepare the upgrade included, is taken for an edit at that first start, reported as HM5804 and not applied: restart the old server once after the last change, or make the change after upgrading, over the API, with hmctl ini set or with sudo hmailserver --set-setting. An upgrade makes no stored-secrets key (§35.12) and never applies the secure defaults of a new installation; the setup checklist, the Deck’s Checklist view, says what is left. Running the schema upgrade by hand is safe at any time:

sudo -u hmailserver hmailserver --upgrade-database
6029 -> 6030: /usr/share/hmailserver/DBScripts/Upgrade6029to6030PGSQL.sql
6030 -> 6031: /usr/share/hmailserver/DBScripts/Upgrade6030to6031PGSQL.sql
Upgraded in 2 steps; the database is at schema version 6031.

That output is from a build at schema 6031 (6.3.0 or 6.3.1) upgrading a database at 6029; run again once the upgrade is done, it reports nothing to do. It scans the scripts directory for the next step rather than guessing the next number, because the chain is not consecutive. It refuses a database from a newer build, and stops rather than looping if a script runs and the version row does not move. 6.3.8 needs schema 6108, as 6.3.6 and 6.3.7 do, and adds no step, so an upgrade from either changes no table. 6.3.6 needs schema 6108, three one-way steps from 6.3.5’s 6105 (6.3.4 needed 6104; 6.3.3 needed 6040; 6.3.0 and 6.3.1 needed 6031, as 6.2.28 did; 6.3.2 moved it to 6038), so an upgrade from 6.3.5 is three steps, one from 6.3.4 four, one from 6.3.3 walks 6.3.4’s sixty-four steps, 6.3.5’s one and 6.3.6’s three in place, and one from anything older the whole chain. On MySQL and MariaDB step 6102 rebuilds every table as utf8mb4, which takes as long as copying the database and needs MySQL 5.7.9 or MariaDB 10.2.2 or later and the ALTER privilege on the database; on an older server the upgrade stops at 6041 with error 1071 and changes nothing (§18.2).

One step in that chain had a known defect until 6.3.2 fixed it. The 6029-to-6030 step deletes orphaned rows before adding seventeen foreign keys, and those deletes run in an order that can re-orphan rows nothing revisits, so the constraint that follows is refused. It fires only on an upgrade from a schema below 6030 against a database that already holds orphaned rows — a database without them upgrades correctly, and --create-database is never on this path. It affects all four backends. When it fires it fails loudly with the engine’s own words and rolls back rather than doing anything quietly, and no installation has reported hitting it. On Linux you can only reach that step with a database carried across from a Windows installation (§35.14), since this platform is itself new in 6.3.0. The fix shipped in 6.3.2, with a gate that upgrades a seeded database: on every push on GitHub Actions up to 6.3.3, and since 6.3.4 as the linux-db-upgrade-pgsql and linux-db-upgrade-mysql jobs of GitLab CI, for a push to master, a batch* branch or a release tag; their first run, on 26 September 2026, failed on a fault in the gate itself that 6.3.4 fixes, and both passed in 6.3.5’s tag pipeline on 29 September 2026. Take the database backup first, which §35.12 asks of you in any case.

The update checker. POST /api/v1/update/check reads the release feed and reports the verdict. A server from 6.3.4 on reads https://updates.progressiverobot.com, which GitLab Pages serves from the project’s releases; one from 6.3.0 to 6.3.3 reads the former GitHub repository’s feed, which has answered 404 since 18 September 2026, until UpdateFeedUrl=https://updates.progressiverobot.com/releases.json is added under [Settings] and the service restarted. On Linux the check builds the asset name your machine would install, probing for /usr/bin/dpkg first and /usr/bin/rpm second. On Arch it names nothing; up to 6.3.3 neither did the AppImage, and since 6.3.4 an AppImage looks for its own. POST /api/v1/update/download fetches the package and verifies it against its Sigstore bundle. Up to 6.3.3 nothing runs it: POST /api/v1/update/install hands the file to a helper that is a Windows program, so on those versions it refuses with a Medium 6401 saying exactly that. Since 6.3.4 it is applied: the server writes a one-file request to /run/hmailserver, and hmailserver-update.path, a root path unit the package enables, starts hmailserver-update.service, which checks every line of the request against root’s own configuration, stops the unit, verifies the package and the rollback image against their Sigstore bundles again as root, refuses anything that is not a genuine upgrade, installs with apt-get or dnf, starts the unit, and puts the previous version back when it does not stay up. sudo /usr/lib/hmailserver/hmailserver-update check says whether the machine can do it; Arch enables no unit from a package, so there run sudo systemctl enable --now hmailserver-update.path yourself. 6.3.4 itself carries no bundle, so it is installed by hand. From 6.3.5 every file carries one, so a server from 6.3.4 on applies a later release by its live update, a 6.3.4 server without a rollback image (§18.6). On 29 September 2026 a 6.3.4 server from the .deb on Ubuntu 26.04 installed 6.3.5 so, unattended, and one from the .rpm on Fedora 44 by hand over the REST API. Up to 6.3.5 a backup destination inside /var/lib/hmailserver fails the backup taken before an unattended apply (HM5014), and the apply is refused until the destination is moved; 6.3.6 fixes that, but an update from 6.3.4 or 6.3.5 is applied by the version already installed, whose apply step is also among what 6.3.6’s security fixes replace (chapter 25): there, install 6.3.8 with apt, dnf or zypper, or turn the apply off until then with systemctl disable --now hmailserver-update.path, which leaves the check and the download on. From 6.3.6, its own update helper applies 6.3.8 to the generic .deb and .rpm of either architecture and to both AppImages. From 6.3.6 the release carries the AArch64 packages and the AppImages again; a server installed from 6.3.3’s trusts only GitHub’s signer and is upgraded by hand, once. A server installed from one of 6.3.6’s or 6.3.7’s per-release packages is offered the next release’s generic package, 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. UpdateRequireAuthenticode=1 is no use here either. From 6.3.1 the Windows installer does carry an Authenticode signature, but the check behind that key is WinVerifyTrust and exists only on Windows, so rather than report a pass it cannot make, this build refuses the download with Authenticode signatures can only be checked on Windows. Up to 6.3.3, updating a Linux installation is the package manager’s job.

Rollback

There is no --rollback here — that belongs to the Windows updater. A rollback is a package downgrade, and its one real constraint is the schema.

sudo systemctl stop hmailserver
# Across a schema change (6.3.4 back to 6.3.3 is one): restore the pre-upgrade database,
# and /etc/hmailserver/hMailServer.ini with it, here - before the older package
sudo apt install --allow-downgrades ./hmailserver_6.3.3_amd64.deb    # or
sudo dnf downgrade ./hmailserver-6.3.3-1.x86_64.rpm
sudo systemctl start hmailserver
  • Within a schema version this is straightforward. Keep the previous package file and its .cosign.bundle somewhere reachable without the network; 6.3.4’s packages have no bundle, so for them keep hmailserver-6.3.4-SHA256SUMS.txt and its .sig.
  • Across a schema change it is not. The server refuses to start against a database newer than the build, with a critical 5011: “The database is too new for this version of hMailServer.” The upgrade scripts run one way. Rolling back across a schema change means restoring the database from the backup you took before the upgrade — which is the reason to take one — and then installing the older package. Restore the pre-upgrade hMailServer.ini with it: a 6.3.4 server that sealed its stored secrets writes the [Settings] secrets into the file as HMK1: envelopes, which an older server reads as they are. Mail that arrived after the backup stays on disk with no row in the restored database, and is invisible.

More: Upgrading from 5.x, 6.2 or 6.3: the 24 changes without a switch, and rolling back on the forum.

35.14 Moving from Windows

This is not a supported migration, and the project does not claim it is. The release notes put it plainly: the database schema is the same and the message store’s on-disk format is the same on both platforms, so the pieces are there; what is missing is a tested path.

That is the whole of the honest position. Nobody has run a Windows installation onto Linux and verified the result, so there is no procedure in this manual to follow and no claim about what would survive. What follows is what is known about the pieces, so you can judge the risk rather than discover it.

What is genuinely shared. The schema — both platforms of 6.3.8 need 6108, so a PostgreSQL or MySQL/MariaDB database created by a Windows installation is one this build understands. The message store — one directory per domain and per mailbox, one file per message named <guid>.eml, the same layout and the same contents. And the files the server marks with a byte order mark — Sieve scripts and the backup index — are UTF-16LE on both platforms, so a file written on one reads on the other; the backup and event logs are too on Windows, and from 6.3.6 UTF-8 on Linux, as its other logs are.

What is known to break:

  1. The configuration, if the backend is SQL Server. An INI carried across with Type=MSSQL or Type=MSSQLCE does not start; it is refused by name (§35.4). Those installations have to migrate the database to PostgreSQL or MySQL first, on Windows, which is a separate exercise.
  2. Every DPAPI-protected secret. Windows encrypts stored passwords with machine-scoped DPAPI and nothing on Linux can open one. Each is recognised by its version word and provider GUID, reported once per process as HM6414, and answers empty. That takes in route passwords, fetch-account passwords, per-domain relay passwords, private-key passphrases and the administrator’s TOTP secret — every one has to be re-entered. A [Database] Passwordencryption=6 carried across is the same problem: rewrite it as 0 with the password in plain text, and rely on the file being 0640 root:hmailserver. The smart host’s password, the backup passphrase and the other secrets kept in hm_settings are machine-bound the same way. Since 6.3.4 an account’s second factor and the [Settings] secrets are not machine-bound: once the installation has a stored-secrets key they are sealed under it (HMK1:), and that key moves only as the file --export-stored-secrets-key writes, imported with sudo hmailserver --import-stored-secrets-key FILE; never copy the key’s line from the Windows file, which is sealed under that machine’s DPAPI. Where the Windows server has a PasswordPepper, it is in no database and no backup either and is kept as written: copy its value from the Windows hMailServer.ini into the Linux one before the first start, or no account whose password is an Argon2id or scrypt hash can sign in.
  3. Every event script. VBScript does not run here, and a JScript file is where a JavaScript port starts, not a file that runs unchanged (§28.2).
  4. Every COM integration. Provisioning, monitoring, billing hooks. Some of what they did has a REST equivalent (chapter 24); some does not yet.
  5. DKIM key paths. The per-domain key-file setting holds a Windows path such as C:\keys\mail.pem, which is nothing on Linux. Copy the keys to a path the service can read and point every signing domain at it, with PUT /api/v1/domains/{domain} or on the Control Deck (§24.15).
  6. File ownership and case. NTFS is case-insensitive and ext4 is not, and this build settles that by lower-casing every directory name it derives from an address, where the Windows build leaves the component as it found it. A store carried across with a Test.com directory in it is a store this server looks for under test.com. Everything under it also has to end up owned by hmailserver.

If you attempt it anyway, treat it as a move onto untested ground: do it on a copy with the Windows server still running, and verify that accounts authenticate, that mail delivers, that IMAP folder names with non-ASCII characters are intact, and that every stored password has been re-entered — before you move any DNS. Keep the Windows installation until you are certain.

More: Moving to new hardware, and why Windows to Linux is a rebuild on the forum.

35.15 How the port is verified

Up to 6.3.3 the jobs below ran in GitHub Actions. Since GitHub suspended the project’s account, GitLab CI builds the Linux server with clang on the project’s own runner and compiles every core file for AArch64 there; its first pipeline ran on 26 September 2026. Since 6.3.6 linux-build-gcc also builds the server and its tester programs with GCC 15 on master and batch* branches; the GCC census is run by hand (bash build/linux-tu-census.sh --cc g++). Nothing is built or run on AArch64 hardware: the project has no ARM machine, so from 6.3.6 the AArch64 packages and AppImage are cross-compiled and tested under emulation, and neither 6.3.4 nor 6.3.5 carries an AArch64 package. 6.3.4’s and 6.3.5’s packages were built in the CI image hmailserver-ci/linux:26.04-dad0ab749505 with SOURCE_DATE_EPOCH set to the tag commit’s time.

A claim that a twenty-four-year-old Windows codebase now runs on Linux is worth little on its own. This is what stands behind it.

Every translation unit compiles. build/linux-tu-census.sh compiles every core source on its own — one compiler invocation per file, syntax only, no linking — and prints how many succeeded, how many failed, and the first error of each failure grouped by shape. It is deliberately not the CMake build: a census must not stop at the first failure and must not care about linking. The script always exits 0, because its output is the measurement; the gate is in the workflow, which reads the failure count and fails the job when it is not zero. Up to 6.3.3 it ran on both native runners and, in a separate job, cross-targeted at aarch64-linux-gnu; since 6.3.4 GitLab CI runs only the cross half, as the linux-cross job on the project’s own runner.

The GCC job. Up to 6.3.3 a third matrix entry built and linked the whole core with GCC on x86-64, which is what stopped the port quietly becoming clang-only. GitLab CI did not port that job: since 6.3.4 no pipeline builds with GCC on x86-64, and the GCC census (bash build/linux-tu-census.sh --cc g++) is run by hand.

Reproducibility. CMakeLists.txt passes -ffile-prefix-map, so no absolute path reaches the binary, and the build job (up to 6.3.3 the Linux workflow on GitHub; since 6.3.4 GitLab CI’s linux-build) exports SOURCE_DATE_EPOCH from the tagged commit’s own date, so that is what __DATE__, the archiver, dpkg-deb and rpmbuild stamp instead of the clock. Measured on 8 September 2026: one commit built twice on the hosted runners — two attempts of the same run, forty-four minutes apart, on two separate runner allocations — and every job’s binary hashed the same both times, for x86-64 clang, x86-64 GCC and AArch64 clang.

The caveat, stated rather than buried. What is not pinned is what the image holds. On GitHub up to 6.3.3 ubuntu-latest moved; GitLab CI’s hmailserver-ci/linux image is Ubuntu 26.04 at a pinned digest, but its packages are the archive’s of the day it was built. A compiler or a Boost that moved between two builds changes the binary legitimately. The job log records the versions it built with, and the Linux claim is therefore “reproducible on the runner image of the day” — weaker than the Windows claim on the same release, which is two clean builds of the stamped source producing a byte-identical hMailServer.exe. The package containers are not compared either; the binaries inside them are what the hashes cover.

The version stamp. build/check-linux-version-stamp.sh, which needs no toolchain (up to 6.3.3 the first step of the Linux workflow; since 6.3.4 run by GitLab CI’s checkers job and again by linux-build before it packages, so a drifted stamp makes no package), checks that Version.h agrees with itself, that CMakeLists.txt derives the version from it rather than carrying a literal, that the Arch pkgver matches, and that nothing else under the packaging directory carries a hard-coded version at all.

The Linux regression run

hmailserver/test/LinuxRegressionTests is a .NET 10 project that compiles 260 of the regression suite’s own files in place — not copies of them — under a REST-backed fixture layer standing where COM stands on Windows. A fixture edited for the Windows gate is the same text both gates run. The shim layer presents the COM object model the fixtures already speak — Domain, Account, Alias, Settings, Rules, Utilities — and turns each property set and method call into a request on /api/v1/.... A call no route can answer becomes a skip that names its reason, never a weakened assertion.

At 6.3.0 it put 1,405 of the suite’s 2,175 tests against a Linux server. Measured on 10 September 2026 against PostgreSQL: 756 passed, 0 failed, 649 skipped, in about 40 minutes. For 6.3.4 it was run by hand on the tag’s packaged binary, on the project’s Linux machine: 2,013 tests, 1,562 passed, 0 failed, 451 skipped (the Windows suite has 3,811). For 6.3.5 it ran in the tag’s own pipeline, on the project’s Linux runner: 2,213 tests, 1,892 passed, 0 failed, 321 skipped (the Windows suite has 3,828). Up to 18 September 2026 CI ran it on every push on GitHub Actions, against the binary the same workflow had just built, on a postgres:16 service container, with no continue-on-error. Since 6.3.4 it is the linux-suite job of the project’s GitLab CI, run whole on the project’s own runner against PostgreSQL 16 for master, batch* branches and release tags, where one failing test fails the job; its first run, on 26 September 2026, failed on faults of the tree that 6.3.4 fixes, and 6.3.5’s tag pipeline ran it whole, with the result above.

Why the skips are mostly the API’s gaps, not the server’s. A skipped test is one whose fixture reaches something the REST API cannot express. At the first measurement that was 299 call sites across 83 reasons, the heaviest being the message object, distribution lists, fetch accounts and folder ACLs, and each of those was “and no REST route carries it” rather than anything COM can do that HTTP cannot; fetch accounts, folder permissions and the Sieve evaluation have had routes since 6.3.3. What skips in 6.3.4 is what cannot be asked over HTTP or has no route yet: the public folders, because the /api/v1/me routes reach the signed-in account’s own mailbox by design and a public folder is nobody’s; a stored message’s MIME as a COM object; the COM logon; and the suite’s fake DNS zone, which is served to the server’s own resolver from the test host. Every skip names its reason.

The project does not call this finished, and neither should you. In its own words: the tick waits until the skip list is things that genuinely cannot be asked over HTTP rather than a backlog of routes nobody has written yet. At 6.3.0 it was 649 skips against 756 passes, mostly the latter; 6.3.4’s run skipped 451 of 2,013, and the roadmap still lists the suite as done only in part. A green run that skips more than a fifth of what it was handed is worth having, and is not the same thing as a suite that tests the Linux server.

What the Linux suite found, which is the argument for building it: a narrow-to-wide conversion that stopped at the first byte above 127, so IMAP SEARCH CHARSET UTF-8 and MAIL FROM under SMTPUTF8 arrived cut short; modified UTF-7 folder names handed to the client as stored rather than decoded; a folder created over REST stored in a form no IMAP client could afterwards name; an IMAP literal terminated with the platform’s newline, which is CRLF on the Windows bench and a bare LF anywhere else, so a counted STATUS hung for two minutes; and a family of fixtures naming ports 25, 110 and 143 literally — which on a machine where WSL mirrors the network meant a fixture inside Linux quietly connecting to the Windows service and testing a server that had never heard of the account it had just made.

What CI proves about the packages. Up to 6.3.3 the package-install job on GitHub Actions installed the .deb on a clean machine, edited the packaged INI in place rather than replacing it, ran --check-config, --create-database and --set-admin-password as documented, asserted the INI was still root:hmailserver 0640 afterwards, and started the server under the packaged unit with all the hardening in force and the listeners left on 25, 587, 110 and 143 — so a server that binds them there is a server whose one capability actually works, which is the part of the unit that would otherwise be silently wrong. It then asked whether the REST API answered and whether GET / returned the real Control Deck rather than the stub, compared byte for byte against both the installed file and the file in the repository. The .rpm was installed in a Fedora container and checked for its files, its identity, its directories, its %config(noreplace) marking and a clean removal. Since 6.3.4 these are GitLab CI’s linux-package-deb and linux-package-rpm. The first installs the .deb in a clean Ubuntu 26.04 container, configures the packaged INI against a PostgreSQL service, runs the server’s own verbs as the service user, starts the server as its unit would (as hmailserver, with CAP_NET_BIND_SERVICE alone and no new privileges) and asks whether the REST API answers and whether GET / is the Control Deck, byte for byte. A container has no systemd, so the start under the unit’s sandbox is no longer shown. The second installs the .rpm in Fedora 44 with --nodeps.

35.16 Troubleshooting a Linux installation

Chapter 19 covers the mail problems, which are the same on both platforms. These are the ones specific to this one. Start every investigation with the same three commands:

systemctl status hmailserver
journalctl -u hmailserver -n 100 --no-pager
sudo tail -n 100 /var/log/hmailserver/ERROR_hmailserver_$(date +%F).log
Symptom Cause and fix
The service will not start; the journal says hMailServer could not start: Initialisation failed before any listener opened — almost always the database or the directories. Run --check-config and, since 6.3.4, --check-database, which makes one connection to the database and says what it got, and read the error log. Standard error carries the reason as well as the log, because at that point the log directory may be exactly what is wrong.
Database type: 0 The Type key did not take. Check the spelling, check it is under [Database], and check for a trailing comment on the same line — text after a value is the value (§35.5).
HM7530 or HM7531, and the database is never reached Since 6.3.4: [Database] TlsMode is not one of prefer, require, verify-full or disable (HM7530), or verify-full names a TlsCaFile that is not there (HM7531). Neither falls back to less. Correct the key and restart; sudo -u hmailserver hmailserver --check-database says what one connection gets.
Critical 6390 Type names SQL Server or SQL Server Compact. Neither exists in this build. Repoint at PostgreSQL or MySQL (§35.4).
Critical 5094, “The MySQL client library could not be loaded” The MariaDB or MySQL client package is not installed, or the linker cannot find it. Install libmariadb3, mariadb-connector-c or mariadb-libs. The error lists the four sonames it tried and the linker’s own last message.
Critical 5011, “run DBUpdater.exe” The message text is a Windows inheritance. Here the command is sudo -u hmailserver hmailserver --upgrade-database. “Too new” instead means you have downgraded across a schema change (§35.13). After an upgrade to 6.3.4 on MySQL or MariaDB, a database server older than MySQL 5.7.9 or MariaDB 10.2.2 stops the schema upgrade at 6041 with error 1071, having changed nothing: upgrade the database server, then the schema.
Critical 5603 for every TLS listener and every outbound delivery /usr/bin/dh2048.pem is missing, so no TLS context gets the finite-field Diffie-Hellman group. It is read from the executable’s own directory, not from ProgramFolder. Reinstall the package.
4335 (up to 6.3.5) /usr/bin/tlds.txt is missing — same directory, same fix. From 6.3.6 the file is gone and 4335 retired: the Public Suffix List is compiled in.
The REST listener does not come up Three possibilities and the log says which: RestApiPort=0; AdministratorPassword empty, which disables the API entirely (“Refusing to start – the administrator password is not set.”; sudo hmailserver --set-admin-password sets it); or a bind address that is not one of the three loopback literals with no certificate configured (§35.9). Since 6.3.4 the port is the value stored in the database, which on a new installation is whatever the file held at the first start, the packaged RestApiPort=0 unless the line had been changed before then: a port edited into hMailServer.ini after that is reported as HM5804 and not used, and the reload brings up no listener. Store it with sudo hmailserver --set-setting RestApiPort=8045 (and RestApiBindAddress the same way), then sudo systemctl reload hmailserver (§35.3).
GET / says “Web administration page not installed” /usr/share/hmailserver/WebAdmin/index.html is missing, or ProgramFolder does not point at the directory holding it. --check-config will not tell you.
HM5804; or HM5806 when a setting is saved, then HM5803 at every start Since 6.3.4 the database holds the [Settings] keys and the file is its copy (chapter 21). HM5804: a line edited in the file after the first start; the stored value is used, so set the key over the API (PUT /api/v1/settings/ini/{name}), with hmctl ini set, or with sudo hmailserver --set-setting, which reaches a running server at the next reload. HM5806 then HM5803: the value is stored and in effect, but the service cannot write its copy into the file, which is 0640 root:hmailserver under ProtectSystem=strict. The report stops once the key’s line in the file is changed by hand to the same value, or the value is stored with sudo hmailserver --set-setting, which writes the file as root.
A listener below port 1024 will not bind Under the packaged unit this works — CAP_NET_BIND_SERVICE is ambient. It will not work from an AppImage, from a hand-started binary, or under a drop-in setting PrivateUsers=yes, which moves the capability into a namespace where the host’s ports are out of reach.
A backup, archive or ACME issuance fails with a write error ProtectSystem=strict: the destination is outside ReadWritePaths and read-only to the service. Add it with systemctl edit hmailserver (§35.8).
An external virus scanner never sees the file it was handed PrivateTmp=yes (§35.8). Prefer clamd, which is unaffected; otherwise drop PrivateTmp with a drop-in and understand what you have given up.
Name resolution misbehaves under the unit but not from a shell Check any drop-in that narrowed RestrictAddressFamilies: AF_NETLINK is not optional (§35.8).
HM6411 — the secret key file is refused Its mode lets another account read it. The error names the chmod that fixes it; the mode is re-checked on every read.
HM6412 — a secret cannot be opened The key file is missing, and the error log names it. This is what a database restored without it looks like (§35.12). Since 6.3.4, once a stored-secrets key has been made, its line in hMailServer.ini is sealed under the same file, so the server also reports HM7495 at every start and every second factor sealed under that key refuses its codes: put the file back. If it is gone, import the exported key with sudo hmailserver --import-stored-secrets-key FILE and restart, which mends the second factors and the [Settings] secrets, and re-enter the other stored passwords, which nothing can open without the old file (§35.14).
HM6414 — a DPAPI blob was found A secret written by a Windows installation. Nothing here can open it; the value answers empty and the condition is reported once per process. Re-enter the password (§35.14).
HM6420 — the logon was refused as a directory failure [LDAP] BindMethod=1 (Negotiate) is Windows SSPI. Set BindMethod=0 with Security=2 or Security=1.
HM6421 A directory-linked account exists but [LDAP] Enabled is off. The error names the settings to set. A certificate error on the bind instead means the directory’s certificate does not chain to the system trust store — TLS_CACERT in /etc/ldap/ldap.conf. There is no private-CA setting in hMailServer for it.
An event script does nothing Up to 6.3.3 it never runs: there is no script engine here. Since 6.3.4 a Linux server runs event scripts in JavaScript only (§35.1). Scripting is off as shipped and the language is seeded VBScript on every platform, so set JavaScript before switching it on (hmctl settings set scripting --set-string language=JavaScript). A VBScript or JScript language, or a script that does not compile, is refused when the script is loaded, with HM5016 (HM5710 at a reload) naming the language or the error. The file is /var/lib/hmailserver/events/EventHandlers.js; load it with POST /api/v1/settings/scripting/reload, and POST /api/v1/settings/scripting/check gives the error and its line (§28.2).
Logs stop appearing after a rotation Check that nothing added create to /etc/logrotate.d/hmailserver and that the su hmailserver hmailserver line is still there. Both matter (§35.11). Test with sudo logrotate -d /etc/logrotate.d/hmailserver.
A stop takes five minutes and the process is then killed ShutdownDrainSeconds is larger than TimeoutStopSec, or a session is genuinely stuck. Raise both together (§35.8).
crash-oracle.log has a new line A memory-safety fault. The record is written before the process dies, and Restart=on-failure brings it back after five seconds. During a start-up fault, look in /tmp/hmailserver-crash-oracle.log instead. Report it with the surrounding error log.
Something is wrong with the paths and you cannot tell which Run the Installation paths diagnostic, one of the tests POST /api/v1/diagnostics/run and the Control Deck’s Diagnostics view run (both since 6.3.4): it lists all six configured directories and whether each exists, where --check-config prints three. The Languages line is expected to be absent here and is deliberately not counted as a failure.

When you report a problem, say which package and architecture (hmailserver --version, and dpkg -l hmailserver or rpm -q hmailserver), which backend and version, the output of --check-config, and the last hundred lines of the error log and of journalctl -u hmailserver. On a platform this new, the packaging details are often the answer.

More: The hmailserver command line on Linux: every verb, who runs it, the files, the unit and Databases on Linux: PostgreSQL or MariaDB, the client, TLS and the upgrade floor on the forum.


36. The Control Deck and the webmail portal

hMailServer has two browser pages: the Control Deck at /, which an administrator uses to run the server, and the portal at /portal, which a mailbox owner uses to read and send mail. Both existed before 6.3.0 and both did far less — the Deck read and did not write, and the portal read mail rather than sending it — and 6.3.0 is what made them what this chapter describes. Both are served by the REST listener of chapter 24, on the same port, under the same authentication. Much of this chapter describes the Control Deck and the portal as they were at 6.3.1, which changed nothing in either page, and a sentence that gives a later change names the release. 6.3.3 rebuilt the webmail to the shape of Gmail and Outlook.com (tabs, mute, pop-out windows, search history, fourteen more search operators, follow-up dates, quick steps, CardDAV for the address book) and moved the Deck most of the way to the desktop Control Panel. 6.3.4 added calendars and tasks over CalDAV, OpenPGP, offline mail and a second account to the webmail, and brought the Deck to 458 of the 466 properties the Control Panel writes by the project’s census, whose eight missing, an account’s directory fields, the Deck edits on a route of their own; 6.3.4 also took the Deck from the fifteen views of 6.3.3 to forty. 6.3.5 brought it to 498 of the 508, the ten missing an account’s directory fields and its external ID, and the server’s country policy. The 6.3.3 and 6.3.4 entries in chapter 25 and the release notes on the downloads page describe the rest until this chapter is rewritten. At 6.3.1 the compiled server differed from 6.3.0’s by a version stamp and one comment line, and everything else in it is the release path.

They matter most because a Linux server has no Control Panel and no COM. On Windows they do not replace it — chapters 5 and 20 still describe the program that does everything. At 6.3.5 the Deck reaches 498 of the 508 properties that program writes, but a few things are still the Control Panel’s alone, editing the event scripts and the Active Directory pickers among them, and some views are the Deck’s alone: the migrations, the reports, the webhooks and the inbound webhooks. On Linux (chapter 35) the Deck is the only graphical administration front end there is; since 6.3.4 the packages also install hmctl, a command-line client over the same REST API.

36.1 Two pages, one listener

One listener answers three things and, since 6.3.4, the sign-in, reset, reading and list pages named below. GET / and GET /index.html return the Deck; GET /portal and GET /portal.js return the portal; everything under /api/v1/ is the API, and since 6.3.4 so is SCIM provisioning under /scim/v2/, which answers only to an API key holding provision. There is no second port and no second process.

All four page routes — /, /index.html, /portal and /portal.js — are unauthenticated to fetch, deliberately: what comes back is a static sign-in card with no data in it, and everything its script then calls is authenticated normally. Serving the shell without a credential is what stops the browser raising its own native password box over the page. What the pages load beside them is served without a credential too: the webmail’s language catalogues at /portal-lang/<code>.json and its S/MIME module at /portal-smime.js and, since 6.3.4, its OpenPGP module at /portal-openpgp.js and its Markdown renderer at /portal-markdown.js, the Deck’s catalogues at /deck-lang/<code>.json and the help documents at /api/v1/help. So, since 6.3.4, are the single sign-on pages (/portal/oidc/, /portal/saml/, /saml/metadata), the password-reset pages (/portal/reset, /recovery/verify and /api/v1/password-reset), the reading pages of a message that expires or is protected by a password (/confidential/, /portal/protected/ and /api/v1/protected/), the list manager’s pages under /lists/, and the two routes the sign-in card asks before anyone has signed in, /api/v1/signin-methods and /api/v1/passkeys/challenge.

Aspect Control Deck Portal / webmail
Credential The administrator password; since 6.3.4 also an account that holds an administrator role, which sees only what the role allows, a passkey the administrator enrolled, or the organisation’s OpenID Connect or SAML sign-in for an address on the server’s list of Deck administrators A mailbox address and its password, or an app password once the account has a second factor; since 6.3.4 also a passkey, single sign-on, or a recovery code beside the password, once
Where the bytes live A file on disk: <ProgramFolder>/WebAdmin/index.html, 66,732 bytes at 6.3.1; since 6.3.4 its language catalogues are files beside it, in WebAdmin/languages Compiled into the binary: 21,636 bytes of markup and 61,411 of script at 6.3.1, and since then its catalogues and its S/MIME and OpenPGP modules too
If it is missing A built-in stub page says so Cannot be missing
Inline script Yes — script-src 'unsafe-inline' No — script-src 'self'
Addresses, Back and Forward No Yes
Its script executed in CI No at 6.3.1; at 6.3.4 yes, by build/check-deck-script.py in the harnesses job Yes — 88 checks on every push at 6.3.1; at 6.3.4 by build/check-portal-script.py in the same job

The Deck is read off the disk as raw bytes, so the characters in its own markup survive any code page. The Linux packages install it at /usr/share/hmailserver/WebAdmin/index.html and the Windows installer at {app}\WebAdmin; it is found through ProgramFolder in the configuration, not through the binary’s directory. If it is not there the server answers a stub reading “Web administration page not installed. The REST API is available under /api/v1/.” and nothing else breaks.

The portal is the opposite, and the source says why: a portal is exactly the page that has to work on a machine with nothing else on it, and a page that depends on a file beside the binary is a page that is missing where somebody most needs it. At 6.3.1 it was three C++ string literals; its language catalogues and its S/MIME and OpenPGP modules have since been compiled in the same way. Nothing is read from disk and nothing is fetched from anywhere — no font, no image, no stylesheet, no script from a CDN.

36.2 Turning them on

There is one switch for all three surfaces. RestApiPort turns on the API, the Deck and the portal together. There is no separate setting for either page; those routes are unconditional on the listener the API opens. If you want one you have all three, and section 36.13 is about what to do with that. Since 6.3.4 these keys are kept in the database (chapter 21): set them in the Control Panel on Windows (Monitoring & troubleshooting → API & monitoring), or with sudo hmailserver --set-setting on Linux, since a line edited in hMailServer.ini after the server’s first start is ignored in favour of the stored value, named in the error log as HM5804 and, where the service may write the file, put back. Written into the file, the lines below are taken into the database at a new installation’s first start. On a server being upgraded, a line edited after the old server last started, including one edited to prepare the upgrade, is taken for an edit at the first 6.3.4 start (HM5804) and not applied, so set these keys after upgrading. All four wait for a reinitialise rather than a restart.

[Settings]
RestApiPort=8080              ; 0 leaves everything off; this is the default
RestApiBindAddress=127.0.0.1
;RestApiCertificateFile=/etc/hmailserver/rest.pem
;RestApiPrivateKeyFile=/etc/hmailserver/rest.key

The listener refuses to start rather than come up without a credential or without TLS, and logs which. Four messages begin RestApi: Refusing to start; these are the two you are likely to meet:

RestApi: Refusing to start - the administrator password is not set.
RestApi: Refusing to start - TLS certificate is required unless bound to 127.0.0.1 or ::1.
Set RestApiCertificateFile and RestApiPrivateKeyFile.

A TLS context that cannot be prepared accounts for the other two, logged in the same form: one when the shared TLS configuration could not be applied to the configured certificate — an unreadable certificate file, a private key that does not match, each of which SslContextInitializer has already reported in detail as HM5113 — and one when constructing the context threw. An empty administrator password disables the whole thing. The TLS exemption applies only to a bind address that is exactly 127.0.0.1, localhost or ::1 — exact literals, not 127/8 and not mapped forms, because this is a security gate and widening it would be a separate decision. With no certificate configured the listener falls back to the ACME certificate the server already renews, when both fullchain.pem and privkey.pem exist, and logs that it has. When TLS is used the context is built through the same initialiser the mail protocols use, so the configured cipher list, option mask and TlsKeyExchangeGroups — including the hybrid post-quantum key exchanges — apply to it. That matters because the obvious alternative, a listener that builds its own context, sets a floor and then takes OpenSSL’s defaults for everything else: configured post-quantum groups would silently not reach it. The one thing deliberately not taken from the shared configuration is the protocol floor: TLS 1.2 stays the minimum here, applied after the shared settings so that it can only tighten them, because an administrator who opened the mail protocols up to TLS 1.0 for an ancient client was not deciding anything about an HTTP API. §24.1 is the same switch seen from the API’s side, with the four keys in a table. See chapter 9 for certificates and chapter 22 for where this port sits.

sudo apt install ./hmailserver_6.3.8_amd64.deb     # Ubuntu 26.04; any other release: its own .deb, or with dnf its own .rpm (§35.2)

# set [Database] in /etc/hmailserver/hMailServer.ini first - chapter 35 has the whole sequence
sudo hmailserver --set-admin-password              # as root: the file is root-owned, and this writes it
sudo -u hmailserver hmailserver --create-database
sudo systemctl enable --now hmailserver

sudo hmailserver --set-setting RestApiPort=8080     # since 6.3.4 the database holds it, not the file
sudo systemctl reload hmailserver
# open http://127.0.0.1:8080/ and sign in as Administrator

Reloading stops and restarts the listeners inside the running process, so do it when nothing is mid-delivery. On Windows, run the installer over the existing installation: nothing 6.3.0 added is on by default, and without RestApiPort a server upgraded to 6.3.0 behaves exactly as 6.2.28 did; 6.3.4 changes some things on the upgrade without a switch (§18.5). Two things about that installer are worth knowing before you run it. From 6.3.1 it is Authenticode-signed — the elevation prompt names Progressive Robot Ltd rather than reading Unknown publisher — but SmartScreen still warns, because a signature is not reputation, and every release before 6.3.1 is unsigned and will stay that way, a published release being immutable. Only the Windows installer carries an Authenticode signature; there is none for a .deb, an .rpm or an AppImage, and up to 6.3.3 the check that covers every asset on either platform is the Sigstore .cosign.bundle beside it, verified with cosign verify-blob; 6.3.4 carries none (§4.1). A 6.3.4 download is checked instead against the release’s hmailserver-6.3.4-SHA256SUMS.txt, whose SSH signature, hmailserver-6.3.4-SHA256SUMS.txt.sig, is made with the key that signs the release tags: ssh-keygen -Y verify on the sums file, then sha256sum -c on the downloads (SECURITY.md, Verifying a release). And if the installation you are upgrading is older than schema 6030, read chapter 18 first: the 6029-to-6030 upgrade step adds seventeen foreign keys and reads every child table once, so plan it like an index build. The defect 6.3.1 named in that step, on a database that already held orphaned rows, was fixed in 6.3.2, so an upgrade to 6.3.4 does not meet it.

36.3 Signing in to the Control Deck

Before 6.3.0 the Deck kept the base64 of user:password in the browser’s session storage and sent Authorization: Basic on every request. That is gone. The password is now presented once, exchanged for a cookie, and forgotten. §24.2 is the mechanism as the API defines it; this section is what the two pages do with it.

POST /api/v1/session HTTP/1.1
Authorization: Basic QWRtaW5pc3RyYXRvcjpzZWNyZXQ=
X-Requested-With: hMailServer

HTTP/1.1 201 Created
Set-Cookie: hmailsession=<64 lowercase hex>; Path=/; HttpOnly; SameSite=Strict; Max-Age=43200; Secure

{"administrator":true,"idle_seconds":1800,"lifetime_seconds":43200}

The portal’s sign-in is the same exchange with the mailbox address in place of Administrator. The token is 32 bytes from the CSPRNG, hex-encoded; only its SHA-256 digest is kept server-side and the comparison is constant-time, so the session table holds nothing that logs anybody in. HttpOnly means the page’s own script never reads it; SameSite=Strict means a request originating on another site does not carry it; Secure is set whenever the browser came over HTTPS: when the listener speaks TLS, which is everywhere but loopback, and, since 6.3.4, when a reverse proxy on this machine in front of the loopback listener says X-Forwarded-Proto: https. A proxy that terminates HTTPS should send that header; one that does not leaves the cookie without Secure. There are two ceilings — by default 30 minutes idle and 12 hours absolute, whatever the user is doing, so a captured cookie stays useful for a bounded time rather than for as long as the victim keeps clicking — and at most 1,000 live sessions, the least recently used dropped if the table is full. Since 6.3.4 the two ceilings are session policies an administrator sets server-wide or per domain (session_idle_seconds and session_lifetime_seconds, at most thirty days each; 0 means not set, and the defaults apply), beside two switches: one session per device, and a session bound to the address it began on. They are re-read at every request, and the four server-wide values are in the Deck’s Settings view, in the server group.

A write must announce itself. Any cookie-authenticated request whose method is not GET or HEAD must carry X-Requested-With: hMailServer, enforced centrally before routing. The refusal is a 403:

{"error":"a request that changes something must carry X-Requested-With: hMailServer when it is authenticated by a session cookie"}

Cross-site request forgery is closed twice over: SameSite=Strict means another site’s request does not carry the cookie at all, and the header is one a browser never adds on its own and another origin cannot add without a preflight this server never grants. It applies to cookie authentication only — a script using HTTP Basic or a bearer key is unaffected.

What ends a session. An administrator session row holds a credential stamp: the SHA-256 of the stored administrator password hash as it stood at sign-in — a hash of a hash, so the table still holds nothing usable. It is re-checked on every request, and if it is now empty (the API switched off) or different, every administrator session is dropped. For accounts, POST /api/v1/me/password signs out every other browser on that account, and deactivating or deleting an account drops its sessions on their next request. Since 6.3.4 an account’s browser session is also an entry of its device list, and revoking it, on the webmail’s Security page or the Deck’s Devices view, ends it at its next request; a password reset through a recovery address ends every session of the account. And the first administrator sign-in after the upgrade to 6.3.4 hashes an administrator password stored the old way (the Windows installer’s unsalted MD5 among them) again as PBKDF2, which changes the stamp, so a Deck session made under the old hash ends once. Sessions live in a process-local table, so a restart of the services holds none of them: there is no persistence and no “remember me”.

An API key cannot mint a session. Both session routes are allowed only to an account password or the administrator password and, since 6.3.4, to a passkey’s assertion and to the one-use ticket a finished OpenID Connect or SAML sign-in carries, each presented to the same POST /api/v1/session; a bearer key is refused with “a browser session is started with an account’s password or with the administrator password, not with an api key”. A key is already the credential a script keeps, and a cookie minted from one would be a second credential with none of the key’s restrictions written on it. A session cookie cannot mint another session either.

When a second factor is enrolled on the administrator credential, the first POST /api/v1/session answers 401 with X-hMailServer-OTP: required and "second_factor":"required" in the body; the Deck then reveals a code field and re-sends with X-hMailServer-OTP. Since 6.3.4 a code is taken once: somebody asked for one again within the same half minute (to register a passkey straight after signing in, say) waits for the authenticator’s next code. After five wrong codes in fifteen minutes every code is refused for fifteen minutes, with 429. The portal’s sign-in has no field for the authenticator’s code — an account with a second factor enrolled cannot sign in with its own password at all, and uses an app password; since 6.3.4 a passkey or single sign-on works too, and a recovery code beside the password opens the webmail once. The authenticator’s code is asked for where the account’s own password is, the password change among them (section 36.6).

Two details for debugging by hand. A 401 omits WWW-Authenticate: Basic for a request a page’s script made, detected by Sec-Fetch-Mode or by X-Requested-With being present; curl and a browser that navigated straight to a route still get the challenge, and without this an expired session would put the browser’s native password box over a working page. And the page keeps no secret: after sign-in it clears the password field and writes one non-secret mark, sessionStorage["hmsSession"] = "1", so a reload asks the server whether the session is live instead of drawing the sign-in card over a working one.

More: Signing in: passwords, second factor, passkeys, single sign-on, recovery, devices on the forum.

36.4 What the Control Deck can do

At 6.3.1, ten views: Dashboard, Domains, Delivery queue, DANE / TLSA, Settings, Rules, Routes, Certificates, Ports, Logs. 6.3.3 had fifteen and 6.3.4 has forty; the twenty-five 6.3.4 added are Checklist, API keys, Roles, Quarantine, Held for review, Archive, Discovery, Journaling, Audit trail, Changed from stock, Metric history, Reports, Reputation, Spam traps, Webhooks, Inbound webhooks, Devices, Remote domains, Groups, Folders, Migrations, Update, DNS records, Diagnostics and Passkeys. The dashboard re-reads GET /api/v1/status every three seconds while it is the open view.

Settings forms generated from the server’s own document

This is the part that keeps working after this manual is out of date. The Deck fetches GET /api/v1/openapi.json once per session and draws each settings form from the schema of that group’s PUT. Nothing in the page lists a settings key by name. Its own note states the contract: “Every field below, its type and its words are read from the server’s own OpenAPI document, so a setting added to the API appears here on its own.”

A boolean becomes a checkbox, an integer a number box, an array a one-per-line textarea, an enum a select of exactly the permitted words, anything else a text box. readOnly gets a read-only badge and static text rather than a control; writeOnly, or a key ending in password, gets a write-only badge and an empty password box placeholdered “left unchanged unless filled”, sent only if filled in; a key in the schema’s required list gets a required badge. At 6.3.1 a key whose description matched “takes effect when the server restarts” got a restart badge — not from a hand-written list, but because the server appended that sentence to a key’s description when its effect was marked as such. Since 6.3.4 a field kept in the settings store carries one of the three marks the Control Panel shows, Applies on save, After reinitialize or After a restart, read from the document’s x-applies, so the page is still the reader and a key whose effect changes re-badges itself. A bar above the groups lists the saved settings that still wait, with their stored and running values and Apply now, and a setting the server’s environment manages is shown as Set by the environment, with no control. The table gives 6.3.1’s three groups.

Group Path Keys Read-only Write-only
Server /api/v1/settings 63 0 1
Anti-spam /api/v1/settings/antispam 34 0 0
Logging /api/v1/settings/logging 16 5 0
Total 113 5 1

At 6.3.1, 113 keys described, five of them read-only facts about where the log is written rather than settings, which leaves 108 writable. The one write-only key is the SMTP relayer password: written and never read back, and omitted from its group’s GET by design. Chapter 21 remains the reference for what each key means. Saving sends only the keys whose control differs from the value the form was drawn with, then re-reads the group; a save with no differences says “Nothing changed” and sends nothing. A filter box narrows the fields by key, caption or description. The API’s groups have grown since, to nine at 6.3.3 and thirteen at 6.3.4, anti-virus, backup, bounces, cache and scripting among them, each with a GET and all but the read-only directories group with a PUT, so at 6.3.4 the Deck draws twelve forms; the OpenAPI document (chapter 24) lists them.

Rules, routes, certificates, listeners, accounts

Object Create Edit Delete
Global rules — /api/v1/rules yes yes yes
SMTP routes — /api/v1/routes yes yes yes
Listeners — /api/v1/ports yes yes yes
Certificates — /api/v1/certificates yes no yes
Accounts — POST /api/v1/domains/<domain>/accounts, PUT and DELETE /api/v1/accounts/<address> yes yes since 6.3.3 (no at 6.3.1) yes
Domains and distribution lists — since 6.3.3 (section 36.5) yes yes yes
Aliases — since 6.3.3; the API has no route to change one, so replace it by deleting and adding yes no yes
Settings, the groups (three at 6.3.1, twelve at 6.3.4; the API’s thirteenth, the read-only directories group, has no form) — yes —
Delivery queue — retry or remove — — yes

At 6.3.1 the read-only views were GET on /status, /domains, /domains/<d>/accounts, /queue, /tlsa, /logs and /logs/<name>?lines=N.

A certificate cannot be edited, because the API has no route to edit one. Replace one by adding the new certificate, pointing the listeners at it and deleting the old. The release notes say “rules, routes, certificates and listeners are created, edited and deleted”; the accurate sentence is that rules, routes and listeners are created, edited and deleted, while a certificate is added and deleted.

The same machinery draws the rule, route, certificate and listener forms from the document, and prints the document’s own description prose above each editor. One honest exception: the rule vocabulary — which words a criterion’s field and match take, and which parameters each action type carries — is read out of the prose of the POST /api/v1/rules description rather than from an enumeration, with a hard-coded fallback behind it. The document does now carry proper enum arrays for a criterion’s field, its match and an action’s type — though not for the parameters each action type carries, which are only schema properties — but the shipped page still parses the sentence, and its own comment says that if the fallback is ever used the document and the page have drifted apart. Do not read the rule editor as schema-generated the way the settings forms are.

The in-place restart

The Ports view carries a Restart the services now button. It confirms first — “Restart the server’s services now? Connections in progress are dropped, and this browser session ends with them.” — and then posts:

curl -u Administrator:your-admin-password -X POST \
     https://mail.example.com:8080/api/v1/server/reinitialize

It answers 202, and it answers before it happens, because the REST listener restarts with everything else. This is exactly what the Control Panel’s Apply now does (chapter 20): every service stopped, the configuration reloaded, the services started again in the same process, so that a new port, a certificate binding or a setting marked After reinitialize takes effect without stopping the process; a setting marked After a restart still waits for the service itself to be restarted, and since 6.3.4 most settings apply when saved and need neither; the Settings view’s Apply now is the same reinitialise. A reinitialise also ends maintenance mode: on a server that is draining, the listeners open again and delivery resumes, and the answer then carries "drained": true. The page then polls GET / once a second, up to sixty times, without credentials, until it answers 200, then shows the sign-in card saying “The services restarted, which ended this session. Sign in again.” The credential-free poll is deliberate: a request the server answers 401 is a request that puts the browser’s own password box on the screen. From a script, poll GET /api/v1/status instead.

Every refused request answers {"error":"<a sentence>"} and the Deck shows that sentence beside the control that caused it, unreworded. A 403 says why; a 401 deliberately says nothing beyond that authentication failed. Every server value rendered into markup is HTML-escaped first, and buttons carry their arguments as data- attributes rather than inline JavaScript, so no server value is ever placed where an HTML entity in an account address would be decoded before the handler is parsed.

36.5 What the Control Deck cannot do yet

At 6.3.4 this section is history. Since 6.3.3 the Deck edits domains, distribution lists and existing accounts, and adds and deletes aliases. The project’s parity census (hmailserver/docs/DeckParity.md) at 6.3.4 counts 466 properties the desktop Control Panel writes, 458 of them writable over REST and every one of those reachable from a Deck view; the eight it lists as missing, an account’s directory fields, are written by PUT /api/v1/accounts/{address}/directory and the directory card of the Deck’s account editor. What follows describes 6.3.1.

Said plainly, because the gap is real. The API writes all four of these; the Deck has no control anywhere for any of them.

Not on the page The routes that do it
Creating, changing or deleting a domain POST /api/v1/domains, PUT and DELETE /api/v1/domains/<domain>
Aliases POST /api/v1/domains/<domain>/aliases, DELETE /api/v1/aliases/<address>
Distribution lists /api/v1/domains/<domain>/lists, /api/v1/lists/<address>
Changing an account that already exists — password, active flag, size limit, administrator level PUT /api/v1/accounts/<address>

The Domains view is read-only: it lists domains and, under each, that domain’s accounts, and it can create and delete an account. It cannot change one. Until the Deck grows those views, use curl or any other HTTP client:

curl -u Administrator:your-admin-password -H "Content-Type: application/json" \
     -d '{"name":"example.com","active":true,"postmaster":"[email protected]"}' \
     http://127.0.0.1:8080/api/v1/domains

One more thing belongs here rather than being discovered later. A domain’s DKIM signing is written with the rest of the domain — its size limits, its signature and its relay host — at PUT /api/v1/domains/{domain} or on the Control Deck, since 6.3.3; /api/v1/domains/{domain}/dkim is a read, and a PUT there answers 404. Up to 6.3.2 none of these could be written over the API, so a domain that had to sign its outbound mail with DKIM was not one to run on Linux (§24.15). Since 6.3.4 a DKIM key rotation’s schedule is saved with the domain on the same route, and the rotation’s own actions are under /api/v1/domains/{domain}/dkim/rotation, with a DKIM key rotation card in the Deck’s domain editor.

36.6 The portal: what a user gets

/portal was a page that read mail. It is now a webmail client. A mailbox owner signs in with their own address and password, and everything they do goes through the /api/v1/me/ routes, which answer to an account’s own credentials and to nothing else — the administrator password and API keys are refused on every one of them, because neither is an account and there is no mailbox behind them whose quota or vacation message could be meant. The one way an administrator reaches them is a support session (6.3.3, POST /api/v1/accounts/<address>/support-session), which acts as the person in the webmail and leaves the webmail’s note that an administrator opened the mailbox; since 6.3.4 it is refused every change to the account’s S/MIME and OpenPGP keys and to zero access, and cannot remove or rewrite that note.

Capability Route under /api/v1/me Notes
Account and quota GET /api/v1/me, exactly The credential is checked exactly as an IMAP logon is, per-name lockout and auto-ban included.
Folder tree GET /folders Also shared: public folders and other users’ folders, flattened into the same sidebar under the rights their owner granted.
Read a folder GET /folders/<id>/messages limit 1-200, before_uid to page back, q to search inside. Fields decoded as FETCH ENVELOPE would.
Open a message GET /messages/<id> Over 1 MB it is truncated: the attachment list is still there, the text and HTML are not. Over 32 MB nothing is parsed, so the attachment list is empty too and the attachment download route answers 413. Another account’s message is a 404.
Search GET /search?q=... Subject, From, To, Cc, text or HTML, case-insensitively, over every folder the account may read, newest first. At most 2,000 messages scanned, and scanned, complete and more say when a search was cut short. Since 6.3.4 a request also stops after about two and a half seconds of reading, the hits are newest first across every folder by message id rather than folder by folder, and an answer that is not the end of the search carries complete: false with next_before_id, which before_id continues from. With the full-text index on, a message the index rules out is passed without its file being opened.
Flags PUT /messages/<id>/flags Only the flags named change; the rights STORE asks for. Every IMAP session on the folder is told.
Move POST /messages/<id>/move As IMAP MOVE: a copy with a new UID, then the original expunged.
Delete DELETE /messages/<id> To the folder designated \Trash when there is one and it is not already there; final otherwise, or with ?permanent=1. Since 6.3.4 a message the person deletes is kept aside for recover_deleted_days (0, off, by default) and can be put back from Recover deleted items.
Send POST /messages Every address goes through the checks RCPT TO makes for an authenticated sender, and a refused one is named. Queued through the same pipeline as SMTP submission. A copy marked read is kept in \Sent when there is one and the quota allows. At most 20 attachments, 12 MB together.
Save a draft POST /drafts replace_id supersedes the previous draft, which is expunged — new content is a new message with a new UID, as IMAP requires. Sending from a draft deletes it permanently.
Change password POST /password current must be the account password, not an app password; policy and reuse history apply as when an administrator sets one. This is where the account second factor applies: without X-hMailServer-OTP it answers 401 and X-hMailServer-OTP: required. Every other browser on the account is signed out. Hidden entirely for a directory-linked account. Since 6.3.4 a recovery code in X-hMailServer-Recovery-Code stands in for the authenticator’s, and five wrong codes in fifteen minutes refuse every code for fifteen minutes, with 429.
Vacation reply PUT /vacation The whole state at once, with expires_date as YYYY-MM-DD when it expires.
Sieve filter GET / PUT /filters Checked as ManageSieve’s PUTSCRIPT checks it, with the same wording in error (chapter 12). Empty removes it; over 256 KB is refused.
Name, forwarding, signature GET / PUT /settings Each object the body names is applied whole; one it does not name is left alone. Enabled forwarding needs an address, and not the account’s own.
Held mail /quarantine and its release / delete routes Only entries this address is a recipient of, and without the other recipients. A release delivers to this address only. A server that does not hold suspected spam for review says so, rather than showing an empty list.

At 6.3.1 bulk actions called the flag, move and delete routes one message at a time, in order, then reloaded once. Since 6.3.4 an action on a whole folder’s selection, Mark as read, Mark as unread, Archive, Junk, Delete or Move, is sent to the server through POST /api/v1/me/folders/{id}/bulk, which works through it a bounded step at a time: at most 1,000 messages a call for a flag and 500 for a move or a delete, and no new message taken on after about two and a half seconds of work. Each answer says how far it got and the next call goes on from there, up to the newest message the page had shown, while the bar counts Done so far; with SpamAssassin on, Junk over a whole folder queues one lesson for each message moved. A keyboard cursor moves with j/k or the arrows, Enter opens, x ticks; shortcuts are suppressed inside a text field and when Ctrl, Meta or Alt is held. Signing out clears the cookie, stops the new-mail probe, and empties the message pane, the list, the folder tree, the attachment list, the message frame and the draft.

At 6.3.1 the only thing the portal stored in the browser was the light/dark theme. The webmail of 6.3.4 keeps more there, among it the inbox listing and the last thirty-one opened messages in localStorage, an outbox of sends that failed for want of a connection, the last ten searches and, only when the reader chooses it in Settings, the mail of the last days, attachments included, in the browser’s IndexedDB for reading offline, up to reader_offline_days_max days (30 by default; 0 offers none). Anyone who can open that browser can read the offline mail, and signing out removes it. And one point that surprised people at 6.3.1: an HTML message was not displayed automatically. The reader saw the text part, and a Show as sent button revealed the frame of section 36.10. Since 6.3.3 the reading pane shows a message’s HTML as the server’s own document (GET /api/v1/me/messages/{id}/html) under a policy where nothing runs and nothing is fetched until the reader allows it.

More: The webmail in depth: search, offline mail, expiring messages, send-as and recovery on the forum.

36.7 Addresses and navigation

The portal has an address space, and it is in the fragment rather than the path. The reasoning generalises: the server answers GET /portal and no path below it for a folder or a message, so a path route would be a 404 on the first reload. (Since 6.3.4 some separate pages do live under /portal/, among them the password reset, the single sign-on steps and a password-protected message’s reading page.) The fragment needs no server route at all, is never sent to the server — so no folder name and no search term reaches a log — and gives the history API everything it needs.

Address Shows
#/f/<id> A folder. ?q=text searches inside it; &before=<uid> pages back.
#/search?q=text A search across every folder
#/m/<id> One message
#/compose A new message. ?reply=, ?replyall=, ?forward= and ?draft= are each an address of their own.
#/held, #/folders Quarantine; managing folders
#/settings, #/filters, #/password The three self-service panels

Because these are real history entries, Back and Forward work: Back out of a reply returns to the message, Back again returns to the folder listing, which is shown again with the cursor on the row it was on. And a reload lands where the reader was — on load the script stashes the incoming fragment before anything else and replays it once GET /api/v1/me has succeeded, so a bookmark at #/settings with no session shows the sign-in card, and signing in lands on Settings rather than the inbox. Sign-out deliberately clears the address, replacing it with #/ through history.replaceState — no history entry, no navigation event — because otherwise a stale #/m/<id> would repaint the previous account’s message into the next account’s session without a request.

The half-written reply. The release notes say one survives a reload; be precise, because the source is narrower and the source is right. Back out of the compose form and in again within a session and the words are still there, because the form is re-primed only when the compose address changes. Across a reload the reply is rebuilt from the original — the recipient, the Re: subject, the quoted original, the Cc list for a reply-all, and for Forward and Edit draft the original’s attachments — but the characters typed into the body are not restored; nothing persists them. A draft saved with the Save draft button survives everything, because it is then a real message in the Drafts folder reachable at #/compose?draft=<id>. So: a reload rebuilds the reply rather than losing it. Save the draft if you want the words.

36.8 Noticing new mail

A webmail page has to answer “is there anything new” without becoming a load problem. The portal does it with one small GET.

GET /api/v1/me/changes?since=9f2c8a...
HTTP/1.1 200 OK

{"token":"1b77e4...","folders":[{"id":1,"count":214,"unseen":3},
                                {"id":4,"count":11,"unseen":0}],"changed":true}

The server walks the account’s own folder tree breadth-first, skipping any folder an ACL keeps from the account — the same two rights the folder listing asks for. A ceiling of 10,000 folders per answer is what stops a parent-id cycle in a hand-edited or half-restored database turning a poll into an endless walk. The counts come from the cached per-folder message collection that IMAP, POP3 and delivery all share, so nothing opens a message file or walks the store; that is what makes it cheap enough to poll. The list is sorted by folder id before anything is hashed, so the token stands for the state and not for how the tree happened to load. Then this is built, SHA-256’d unsalted, and the hex digest is the token:

hmailserver/me/changes/2
account=<account id>
<folder id>:<message count>:<unseen>:<current UID>:<folder name>
... one line per folder, sorted by id

Note what is in there. The answer reports each folder’s message and unseen counts; the token additionally covers the folder’s current UID and its name. Both extras are needed: counts alone cannot see a message replaced — a poll spanning a delete and a delivery finds the same two numbers — and cannot see a rename at all. A UID is never reissued, so no addition leaves it alone, and the name is what a rename changes and nothing else does. The account id prefix is there so that two accounts whose mailboxes happen to have the same shape do not share a token.

Because it is a hash, the token carries no folder id, no count and no address a holder could read back; it is a value to hand in again and nothing else. And ?since= is compared, never parsed: equal means "changed":false, anything else — including a token this mailbox never had — means "changed":true, because “something differs” is the answer that cannot lose a message. Without since there is no changed member at all. It is a /me route, so it is refused to the administrator password and to API keys.

On the page side:

  • Every six seconds. The first probe carries no token; every one after it carries the token the last answer gave. When something changed — or when the answer names a folder the page has never seen — the folder tree and the open listing are both re-read.
  • The cursor is restored by message id, not by row. The page records the id under the cursor, the ticked boxes and the scroll offset, re-fetches, then finds that id among the new rows. The case that matters is a message arriving above the cursor: the cursor stays on the message it was on, which has moved down. Ticked boxes for messages no longer in the listing are dropped.
  • It stops dead while the tab is hidden, saying “Paused – this tab is in the background”, and probes immediately when the tab comes back. A portal left open all day in a background tab costs nothing.
  • A server that does not answer the route is asked once and then never again. A 404 (it does not know the path) or a 403 (it does not know this is one of the account’s own) turns the probe off for the session, saying “This server does not report changes”. Asking an old server every six seconds for the rest of the day would be a defect of the page.
  • A transient failure backs off to thirty seconds. A request that never reached the server is reported internally as status 0, specifically so the probe’s own continuation still runs and the next tick is still scheduled — otherwise a dropped network would stop the probe for good while the page went on claiming to watch. Polling stops on sign-out.

36.9 Managing folders

POST, PUT and DELETE on /api/v1/me/folders create, rename and delete a folder. All three are /me routes, so a browser session must also carry X-Requested-With: hMailServer.

The point of the implementation is that the judgements are IMAP’s, in IMAP’s own sentences. Create runs IMAP CREATE’s checks in IMAP’s order: a name at all, then the folder already existing, then the name being valid. A name carrying the hierarchy delimiter creates the whole path exactly as CREATE "a.b.c" does, and a first element beginning with # — the public namespace — is invalid here exactly as it is for a non-public CREATE. A folder is created unsubscribed, as CREATE creates it. Rename is IMAP RENAME’s classic non-public flow: the new name is a whole mailbox name, so renaming Projects to Work.Old moves it under Work, creating Work if it is missing; subfolders are not touched and do not need to be, because they carry their parent’s id. Depth is checked against the same maximum IMAP checks, and the right checked is the delete-half right, per RFC 4314.

The refusals are literally the strings IMAP answers with, typos and all. These are the ones you are likely to meet; the document lists a few more, and every 500 means the row could not be written:

Refusal HTTP From
Folder name not specified. 400 The route’s own
Folder already exists. 409 CREATE
CREATE The folder name is invalid. 400 CREATE
Folder could not be found. 404 DELETE
Cannot rename INBOX. 403 RENAME
Target folder already exist. 409 RENAME
To many sub-folders in structure. 400 RENAME
A folder cannot be moved into one of its subfolders. 400 RENAME
ACL DeleteMailbox permission denied (required for RENAME). 403 RENAME
ACL: DeleteMailbox permission denied (required for DELETE). 403 DELETE
You cannot delete the inbox. 403 DELETE
You cannot delete a folder the server has designated for a special use. 403 New here — not an IMAP sentence

The two ACL sentences differ by a colon. That is IMAP’s own inconsistency, carried over verbatim rather than tidied, so that a refusal from the API and a refusal from IMAP are the same string. A rename the database refuses answers 500 The folder could not be renamed. and reports HM6520 in the error log; the in-memory move is deliberately not unwound, because reversing a move between containers half-correctly leaves a tree no reload would produce.

The one rule stricter than IMAP. DELETE refuses a folder the server has designated for a special use — the effective designation the listing reports, whether set by CREATE ... USE or inferred from the name. IMAP itself allows it. The route’s reasoning: a portal is a different proposition. One mis-click on the folder designated \Sent takes every sent message with it, the page has no undo, and the designation is the one piece of evidence that says the folder is not an ordinary one. The whole subtree is checked, not just the folder named — deleting an ordinary Archive that happens to hold Archive.Sent is refused, because the delete would take the subfolders and every message in all of them. And such a folder is not undeletable: it stays deletable over IMAP, where the client that designated it can undesignate it first, and where the person deleting it asked for that mailbox by name.

The Manage folders view draws one row per folder of the account’s own tree. Public folders and folders shared from elsewhere are excluded — by comparing the owning account rather than testing it for truth, after a bug where a public folder’s account id of 0 read as falsy and drew controls the routes always refuse.

Folder Rename Delete
INBOX no no
One with a special use, badged kept for \Sent and so on yes no
One the listing reports as not writable, badged read-only no no
Anything else yes yes

The page’s own comment: the server refuses to delete the inbox and a designated folder, and it is right to, because there is no undo here — and not offering the control is better than a refusal the reader did not ask for. Three further behaviours, each pinned by CI. Create posts the whole path in the server’s own delimiter — choosing INBOX as the parent and typing Bills posts {"name":"INBOX.Bills"}, the delimiter taken from the folder listing rather than hard-coded. Rename shows the whole path — INBOX.Work, not Work — because a rename may move the folder, and showing only the leaf would make a move look like a rename and a rename to the top level look like nothing. Delete takes two presses: the first turns the button into Really delete?, and nothing goes to the server until the second. When the server does refuse, the reader is shown its sentence verbatim.

36.10 How inline images work, and why

A sender embeds a logo in an HTML message. The HTML part points at it as <img src="cid:[email protected]">, and the image itself is a MIME part in the same message carrying that Content-ID. Displaying it is the most interesting problem on either page, because the obvious design cannot work, and the reason it cannot work is worth understanding.

1. What the message tells you. GET /api/v1/me/messages/<id> returns an attachments array in which every part — inline or not — is an entry:

{"attachments":[
  {"index":0,"name":"invoice.pdf","size":184213,"content_type":"application/pdf","content_id":""},
  {"index":1,"name":"logo.png","size":8422,"content_type":"image/png","content_id":"[email protected]"},
  {"index":2,"name":"chart.svg","size":2190,"content_type":"image/svg+xml","content_id":"[email protected]"}
]}

content_type is the media type the part declares, lower-cased and without its parameters, and empty when the part declares none. content_id is the part’s Content-ID with the angle brackets stripped — the form a cid: URL uses.

2. The body is rendered in a frame with an origin of its own. The sender’s markup is never merged into the portal’s document. It goes into an iframe:

<iframe id="message-html"
        sandbox="allow-popups allow-popups-to-escape-sandbox"
        referrerpolicy="no-referrer"
        title="The message as it was sent" hidden></iframe>

The two sandbox tokens present exist only so that a link the reader clicks opens. The two that matter are the ones absent: allow-scripts, so nothing in the message runs, and allow-same-origin, so the framed document does not get the portal’s origin.

3. And therefore the download route answers 401. Each link in the chain is forced by the one before it:

  • The frame is sandboxed without allow-same-origin, so its document has an opaque origin — not the portal’s origin, and not any origin.
  • A browser therefore counts every subresource request that document makes as cross-site.
  • The session cookie is SameSite=Strict, so it is not attached to a cross-site request.
  • So an <img src="/api/v1/me/messages/42/attachments/1"> inside the frame — the obvious design — arrives with no credential, and the route answers 401.

This was measured, not assumed. The source records the experiment: in Chrome on 9 September 2026, through a logging proxy, the same frame without the sandbox attribute sent the cookie and got 200 image/png; with the sandbox attribute it sent no cookie and got 401. And the only thing that would fix it is the very attribute the frame exists to withhold — weakening the sandbox to make the URL work would hand the sender’s markup this account’s own origin, which is the whole thing the frame is there to prevent. There is no configuration that makes the obvious design safe.

4. So the page fetches the bytes itself. The portal’s own document is on the portal’s origin, so a request it makes is same-site and does carry the cookie. It fetches the attachment with its own credentials, reads the response as a blob, converts it to a data: URL, and rewrites the cid: reference in the HTML to that data URL before handing the markup to the frame. The frame never makes a request at all; it is given the bytes. The budgets are on the page side, because an unbounded version of this turns one message into a hundred megabytes of base64: at most 4 MiB per image, 12 MiB across all of them, and at most 12 images per message. That was how 6.3.1 did it. By 6.3.3 the server had taken this step over: the frame shows the server’s own rendering of the message, GET /api/v1/me/messages/<id>/html, which puts each cid: image in place as a data: URL, at 6.3.4 up to 64 images, 4 MB each and 12 MB together, and the page frames the HTML only of a message of up to 1 MB, showing a larger one as its text.

5. The frame’s own policy names no URL at all. The document handed to the frame begins with its own policy and a base target:

<meta http-equiv="Content-Security-Policy"
      content="default-src 'none'; style-src 'unsafe-inline'; img-src data:">
<base target="_blank" rel="noopener noreferrer">

img-src data: and nothing else. Not 'self', not the server’s own host, not any host. So a remote image — the classic tracking pixel at https://tracker.example.org/pixel.gif — is left in the markup exactly as the sender wrote it and is blocked by the policy. Opening a message still tells its sender nothing. At 6.3.1 the outer page said the same at the outer level: its own policy was also img-src data: (section 36.11 has what changed there). By 6.3.3 the bar over a message that carries remote images offered to show them, for that message once or always for that sender, so nothing is fetched until the reader asks for it.

What decides whether something is inlined

This is how the page decided at 6.3.1, when it did the inlining itself (step 4). Not the message’s claim. The server’s Content-Type on the download. Two gates, in this order:

  1. A pre-filter on the message’s claim, which only decides what is worth fetching at all. The part must have a content_id, must be within the budgets, and its declared content_type must be one of image/png, image/jpeg, image/gif, image/webp, image/bmp, image/x-icon, image/vnd.microsoft.icon — or absent, since a part declaring nothing is left to the second gate like everything else.
  2. The decisive gate, on the bytes that come back. The fetched blob’s type must start with image/, and the resulting data URL must start with data:image/. Anything else is discarded and the cid: reference is left exactly as it was — so it renders as nothing, and the part is still in the attachment list below, named, where the reader can download it deliberately.

The download route is what makes that second gate meaningful. It serves an attachment under the very media type the listing reported — which is what would let an <img> pointed at it render, with nosniff set — except for the types a browser would run or render as a page, which all become application/octet-stream:

text/html            application/xhtml+xml      image/svg+xml
text/xml             application/xml            anything ending "+xml"
text/javascript      application/javascript     a part declaring no usable type at all

The reason is stated in the source: with the session cookie attached, an attachment that rendered as a page on this origin could act as the page. Every download also carries Content-Disposition: attachment (the filename in both the plain and the RFC 8187 form), X-Content-Type-Options: nosniff, Content-Security-Policy: sandbox and Cache-Control: no-store.

So an SVG is never inlined, even though SVG is an image format and the message says image/svg+xml — because SVG can carry script, so the download route serves it as application/octet-stream, so the blob’s type does not start with image/, so the second gate discards it. The cid:[email protected] reference survives untouched in the frame’s document, renders as nothing, and chart.svg stays in the attachment list. The same applies to HTML, XML and script parts. The mechanism needed no special case for any of them: one rule about what the server is willing to serve as an image produces the right answer for all of them.

36.11 Content-Security-Policy and why the script is a separate response

GET /portal returns text/html and GET /portal.js returns text/javascript. Both carry identical headers:

Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'unsafe-inline';
    img-src data:; connect-src 'self'; frame-src 'self'; form-action 'none';
    frame-ancestors 'none'; base-uri 'none'
X-Content-Type-Options: nosniff
Referrer-Policy: no-referrer
Cache-Control: no-store

The page’s only script element is <script src="/portal.js"></script>, the last line before </body>. That split is the whole point. Because the script is a separate response, the policy can be script-src 'self' with no 'unsafe-inline' — which means an inline <script> block would not execute and an inline onclick= handler would not fire. Had the script stayed inside the page, the policy would have had to permit inline script, and then it would have permitted any inline script, including one that got there by accident.

The consequence for the page’s own code is strict: every value from the server is written as text, never as markup. The script builds DOM nodes and sets their text content; there is no assignment of server data to innerHTML anywhere. The single exception is the message body, which is not written into the document at all but handed to the sandboxed frame of section 36.10.

Read the rest of the policy as a list of things the page has decided it will never need. default-src 'none' — nothing, from anywhere, unless named below. img-src data: — no image from any host, including this one. That was 6.3.1’s policy; by 6.3.4 the page loads the directory’s photos by URL from this server, so its policy allows images from this server too, and still nothing from anywhere else. connect-src 'self' — it talks to the server that served it and to nothing else. form-action 'none' — no form posts anywhere; every write is a scripted request. frame-ancestors 'none' — nobody may frame the portal, which is clickjacking closed. base-uri 'none' — no injected <base> can redirect the page’s relative calls somewhere else.

The Control Deck does not have this property, and the difference is honest rather than accidental. The Deck is one self-contained file with its script inline, so its policy permits inline script: default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'. It compensates in code rather than in policy, escaping every server value and dispatching through data- attributes. That is a weaker guarantee, and it is why the portal — the page a much larger number of much less trusted people sign in to — is the one built the harder way.

36.12 What CI guarantees

The portal’s script is not merely linted; it is executed on every push. build/check-portal-script.py does three things.

It recovers the literals. It parses RestApiPortal.cpp for definitions of exactly the shape const char *Name = followed by one string literal per line until the one ending in a semicolon, unescapes them back into bytes, and requires all three of PortalHtml, PortalScript and PortalHeaders to be there. A C++ raw string literal is deliberately not understood: the concatenated form is what keeps the page readable in a diff.

# extract the three literals, check them, run the script
python3 build/check-portal-script.py

# extract only, and leave portal.html, portal.js and portal.headers in DIR
python3 build/check-portal-script.py --extract-only --keep DIR
portal script: 88 checks passed

It asserts cheap structural truths before executing anything. These are the only substring assertions in the harness, and each one is a property of the page rather than a name in it. The build fails if the page loads any script that is not /portal.js; if it carries inline script; if it carries an inline event handler; if any src or href names an http:, https: or protocol-relative URL; if the headers do not name script-src 'self' or frame-ancestors 'none'; or if any -src directive names a source starting http or //, or is *.

Then it runs the script. build/portal-script-test.js builds a small DOM from the extracted markup, stubs the fetch API with a recorded server, and stubs local storage, the HTML parser, the file reader, the timers, the location and the history — defining them over node’s own globals so the script sees only this world — executes the portal’s script in it, and asserts 88 behaviours. Why it exists is stated in the harness itself: until it did, the only thing standing behind these two literals was a regression test asserting that certain substrings were present in what the server served, which proves that a name is spelled somewhere in a file and nothing whatever about what the page does. A script can carry a syntax error, a handler wired to an element that does not exist, or a sign-in that keeps the password, and pass every one of those assertions.

In order, the 88 cover: the sign-in gate; the password sent exactly once and only to POST /api/v1/session, the field emptied, nothing secret stored, every write carrying X-Requested-With; a reload landing where the reader was; the sidebar and its unseen counts; a folder being an address and its listing rendering; the keyboard cursor, and that a keystroke in a text box is not a shortcut; opening a message and its being marked read; inline images and the frame — all of section 36.10, including that the page and not the frame fetched the bytes, that the frame is pointed at no URL on this server at all, that a remote image is left alone and blocked, and that an SVG the download route will not serve as an image is neither inlined nor smuggled into the page; Reply as an address, and Back and Forward returning to the message, the folder and the row the cursor was on; the change probe, its token, and the cursor restored by id after a message arrived above it; nothing polled while hidden; a server with no change route asked once and then not again; folder management, including the whole path in the server’s own delimiter, the two-press delete and the server’s own refusal shown verbatim; search as an address; the theme remembered; and sign-out, with the message pane, list, folder tree, attachment list, frame, draft and address bar all emptied.

At 6.3.1 nothing equivalent existed for the Control Deck: its markup was checked into the repository and CI verified that the Linux packages and the AppImage carried it byte for byte, but its script was not executed. That is no longer so. The Deck has a harness of its own, build/check-deck-script.py and build/deck-script-test.js, which runs the page against a recorded server, and in 6.3.4 the harnesses job of the project’s GitLab pipeline runs the Deck’s and the webmail’s harnesses, with the S/MIME and OpenPGP checks, in every pipeline but a scheduled one. The job failed in the first two pipelines to run the project’s whole GitLab CI file, the first of them on 26 September 2026; 6.3.4 fixes the cause (the portal harness depended on the runner’s time zone), and when 6.3.4 was released the job had not yet run on the fixed tree; it passed in 6.3.5’s tag pipeline. The count of 88 is 6.3.1’s too: by 6.3.4 the webmail is kept as Portal.html and Portal.js, which the server serves from PortalPageData.cpp, and its harness has grown with each feature (the importance marker alone has forty-three checks).

36.13 Deploying them safely

Start from the two facts that drive every other decision. There is one switch and one listener, so exposing the portal exposes the Deck. And the listener will not start without TLS unless it is bound to loopback, so there is no accidental plaintext deployment to make.

Posture one: administration only. The simplest, and the one the shipped configuration documents. Bind to loopback, leave RestApiCertificateFile and RestApiPrivateKeyFile empty, and reach it through an SSH tunnel. Nothing is reachable from the network, and mailbox users do not get the portal — which may be exactly what you want. Since 6.3.4 both keys are kept in the database, so a line commented out in hMailServer.ini does not clear a value the server has stored. And where an ACME certificate has been issued, the listener takes it even on loopback and answers HTTPS, with a certificate that does not name 127.0.0.1; open https://127.0.0.1:8080/ in that case.

ssh -N -L 8080:127.0.0.1:8080 [email protected]
# then open http://127.0.0.1:8080/ in a browser on your own machine

Posture two: the portal for users, the Deck for you. If mailbox owners are to reach /portal from the internet, the administration page at / is on the same port by definition. This is a deployment decision the server does not make for you. Put a reverse proxy in front and split the surfaces there:

Path Where it belongs
/portal, /portal.js, /api/v1/me and every path under it Public
Every other path that begins /portal: the webmail’s own files (its language catalogues, the installed app’s manifest and service worker, and its script modules: /portal-lang/, /portal.webmanifest, /portal-sw.js, /portal-smime.js and, since 6.3.4, /portal-openpgp.js, /portal-markdown.js and /portal-protected.js) and, since 6.3.4, the pages for a password reset, single sign-on, a password-protected message and the return from another provider’s sign-in for Other accounts (/portal/reset, /portal/oidc/, /portal/saml/, /portal/protected/, /portal/oauth/callback) Public — the webmail loads them, or a person opens them from a link
/api/v1/session Public — both pages sign in through it
/api/v1/portal/branding, GET only Public — after sign-in the webmail reads its domain’s own name, logo and announcement here
Since 6.3.4: /api/v1/signin-methods, /api/v1/passkeys and the paths under it, /api/v1/password-reset and /api/v1/password-reset/complete, /recovery/verify, /api/v1/help and the paths under it, and /saml/metadata Public — sign-in by passkey or single sign-on, a forgotten password, a recovery address confirmed, the help button, and the metadata a SAML identity provider imports
/files/*; since 6.3.4 also /confidential/*, /api/v1/protected/*, /lists/* and /held/* Public — opened from a link by people who may have no account here: a file sent as a link, a message that expires, a password-protected message, a list’s subscribe, unsubscribe and archive pages, and the quarantine digest’s pages that deliver held mail or stop the digest
/, /index.html; since 6.3.4 also /deck-lang/* Internal network or VPN — this is the Control Deck
Everything else under /api/v1/ Internal network or VPN — this is the administration surface
Since 6.3.4, /scim/v2 and the paths under it Wherever the organisation’s identity provider calls from: public for a cloud one such as Microsoft Entra ID or Okta, internal otherwise. It takes nothing but an API key whose role holds provision

/api/v1/session has to be public for the portal to sign anybody in, and it is the same route the Deck signs in through. That is safe in itself — the session it mints for a mailbox address is an account session, which reaches only the account’s own routes unless, since 6.3.4, the account holds an administrator role, and then only the administration routes its role allows, which the proxy keeps internal — but it does mean an attacker can reach the administrator sign-in, which is the argument for enrolling a second factor on it. A proxy on the same machine that terminates HTTPS in front of the loopback listener should send X-Forwarded-Proto: https. Since 6.3.4 that header is what marks the session cookie and the sign-in cookies Secure, and without it they go without. Nothing in the server reads a forwarded-for header, so behind such a proxy every request comes from loopback, which the auto-ban leaves alone; an account’s per-name lockout, where AccountLockoutThreshold sets one (it is 0, off, by default), and the limit on wrong one-time codes (five in fifteen minutes for each owner) still apply. If you do not run a proxy, terminate TLS on the listener itself, or let it fall back to the ACME certificate. Either way the port belongs in your firewall policy and in chapter 22’s picture of what this server listens on.

  1. Leave it off unless you want it. RestApiPort=0 is the default and turns off the API, the Deck and the portal together.
  2. Set a second factor on the administrator credential. Both pages already handle the 401 that asks for the code. The secret is kept in hMailServer.ini, so where the service may not write that file — a new 6.3.4 Windows installation, which runs as NT SERVICE\hMailServer, and the Linux package, whose file is root’s — enrolling is refused (409 over REST); on Windows, move the service to LocalSystem for the enrolment and back.
  3. Use scoped API keys for scripts, never the administrator password. A key is read-only unless you say otherwise, always carries an expiry — a bounded default when the request names none, and a date in the past is refused rather than stored — may be confined to named domains and a source address, and is stored only as a digest. No key can mint or revoke keys, or mint a browser session.
  4. Expect the restart button to sign everyone out, and remember that sessions are in memory: a restart, a crash or the 1,000-session cap ends them.
  5. On Linux, back up the secret key file with the database. A database restored without /var/lib/hmailserver/.hmailserver-secret-key has lost every stored password (chapters 15 and 35). Since 6.3.4 that file also opens the stored-secrets key line in hMailServer.ini, once there is one (sudo hmailserver --create-stored-secrets-key makes it), and that key seals every account’s second factor and the [Settings] secrets: keep the key file and the configuration together, or export the key with sudo hmailserver --export-stored-secrets-key FILE and keep the file with the database backups, stored apart from them as the password pepper is kept.

What the server already does, so you need not configure it: the session cookie described in section 36.3; X-Requested-With on every cookie-authenticated write; a rate limit of 200 requests per 10 seconds per credential, budgeted against the credential rather than the source address, with one log line per credential per window; 64 connections, and a 64 KB / 30-second cap on a request, except the routes granted more: the two attachment-carrying routes at 16 MB / 300 seconds and, since 6.3.4, PUT /api/v1/me/settings (a large request like sending a message, so that a formatted signature is held by the route’s own limit of 64,000 bytes rather than by the listener’s cap) and the SAML sign-in’s POST /portal/saml/acs at 256 KB within the ordinary 30 seconds; and nosniff, Referrer-Policy: no-referrer, Cache-Control: no-store, frame-ancestors 'none' and base-uri 'none' on both pages. Chapter 17 is the wider hardening picture.

36.14 Troubleshooting

Symptom Cause and fix
Nothing answers on the port Look in the application log for RestApi: Refusing to start. Either the administrator password is not set, or the listener is bound to a non-loopback address with no certificate and key. Where the service may write hMailServer.ini, set the password on the Control Panel’s Sign-in & access → Administrative access page. A new 6.3.4 installation runs the service as NT SERVICE\hMailServer, which may not write it, and there the page’s change is refused with a sentence: from an elevated prompt beside hMailServer.exe, put the password in HM_ADMIN_PASSWORD and run hMailServer.exe --set-admin-password (it never takes the password as an argument), then restart the service. On Linux run sudo hmailserver --set-admin-password. Since 6.3.4 RestApiPort, RestApiBindAddress and the certificate keys are kept in the database: set them on the Control Panel’s Monitoring & troubleshooting → API & monitoring page, or with sudo hmailserver --set-setting on Linux, because a line edited into hMailServer.ini is reported as HM5804 and the stored value is used. The listener takes either change only at a reinitialise (for those keys, Apply now on the Control Panel’s pending bar; on Linux sudo systemctl reload hmailserver) or a restart of the service.
“Web administration page not installed” <ProgramFolder>/WebAdmin/index.html is missing and the server has fallen back to its stub — on Linux, /usr/share/hmailserver/WebAdmin/index.html. hmailserver --check-config will not tell you, because the page is not one of the paths it prints. Check the file itself.
The portal is missing but the Deck works Not possible from an absent file: the portal is compiled into the binary. Check you are asking for /portal exactly: the page keeps its own addresses after the #, and the only paths below /portal/ the server answers are the sign-in, password-reset and protected-message pages 6.3.4 added, and /portal/oauth/callback, where another provider’s sign-in for Other accounts returns. Check too that a proxy is not rewriting it or holding back the files the page loads beside it, whose paths begin /portal (/portal.js, /portal-lang/ and the others).
Every write answers 403 about X-Requested-With You are authenticated by a session cookie and did not send the header. Either send it, or authenticate with HTTP Basic or an API key sent as a bearer token, which are exempt. Since 6.3.4 the header is also required on a write made with a webmail second account’s session token, and on POST /api/v1/session when the credential is in the body (a passkey assertion or a single sign-on ticket).
A bearer key gets 403 from POST /api/v1/session Working as intended. A browser session is started with an account’s password or the administrator password (since 6.3.4 also with a passkey or a single sign-on ticket), never with an API key.
A correct one-time code is refused Since 6.3.4 a code the server has accepted is refused if it comes again, for the administrator and for an account alike. Asked for a code twice within half a minute (to sign in to the Deck and then to enrol a passkey, say), wait for the authenticator’s next one. A script that sends the administrator password and a code with every request has only its first request in each half minute accepted; sign it in once with POST /api/v1/session, or give it an API key. Since 6.3.4 too, five wrong codes in fifteen minutes lock every code for that owner, the right one too, for the next fifteen minutes; the REST API answers 429 with second_factor_locked and Retry-After. Wait it out; a restart or a reinitialise of the services also clears it.
The browser raises its own password box over the page Something reached a route as a navigation rather than as a scripted request — usually a link or bookmark pointing straight at /api/v1/....
Everybody signed out at once The administrator password changed, or the REST API was switched off and on; either drops every administrator session on its next request. A restart or a reinitialise of the services does the same, and since 6.3.4 that includes Apply now in the Control Panel or the Deck. After an upgrade to 6.3.4 the first administrator sign-in can also end the administrator’s browser sessions once, when it writes an older stored password hash again as PBKDF2. One user signed out everywhere means they changed their own password or, since 6.3.4, reset it through a recovery address, or the account was deactivated or deleted. Since 6.3.4 switching a domain off also ends its people’s sessions at their next request.
A session ends sooner than expected, or 429 under normal use By default a session ends after 30 minutes idle or 12 hours in all. Since 6.3.4 these are session policies an administrator can change: server-wide as session_idle_seconds and session_lifetime_seconds (the server group of the Deck’s Settings view, as §36.3 names them; SessionIdleSeconds and SessionLifetimeSeconds over COM), and per domain in the domain’s dialog, and per account, which may shorten the values the account would otherwise have and not lengthen them; the administrator’s own session follows the server-wide values. Two more policies end a session early when switched on: one session per device, and a session bound to the address it began on, which ends when a phone moves from wi-fi to a mobile network. Revoking a client on the Devices view ends its session too. The rate limit is 200 requests per 10 seconds per credential. If a script shares the administrator password with a browser session they share the budget — give the script its own scoped key.
“This server does not report changes” The server answered 404 or 403 to GET /api/v1/me/changes — usually a newer page talking to an older server, or a proxy not passing that path. The probe is off for the rest of the session; reload to try again. “Not watching (0)” means the request never reached the server, and it backs off to 30 seconds.
An inline image shows as a broken or empty box The image was not inlined. In 6.3.4 the frame shows the server’s own rendering of the HTML (GET /api/v1/me/messages/<id>/html), which puts each cid: image in place as a data: URL, up to 64 images, 4 MB each and 12 MB together; an image past those limits is not inlined. The download route serves SVG, HTML, XML and script parts as application/octet-stream on purpose. At 6.3.1, when the page inlined images itself from that route, that kept them out of the frame (section 36.10). The part is still in the attachment list, named, and can be downloaded.
An HTML message shows as plain text, or a remote image does not load A message over 1 MB is shown as its text: the page frames a message’s HTML only up to 1 MB. Remote images are blocked by the frame’s policy until the reader allows them: the bar over the message offers to show them for this message, or always for this sender. There is no server setting for either. In a second account’s messages (6.3.4) remote images are not loaded at all, and the bar says so.
Sending a large attachment fails At most 20 attachments, 12 MB together, inside a 16 MB request with a 300-second deadline. Ordinary requests are capped at 64 KB and 30 seconds. Since 6.3.2 a larger file can be sent as a link that expires instead.
A folder cannot be created, renamed or deleted The sentence shown is the server’s own, and it is the same sentence IMAP answers with; section 36.9 lists the common ones. The only refusal that is not IMAP’s is the one about a folder designated for a special use — delete it over IMAP if you really mean it. A 500 means the database refused the write; HM6520 is in the error log.
A setting saved in the Deck has not taken effect Look for the mark beside the field. Since 6.3.4 a field kept in the settings store says when a save applies it — Applies on save, After reinitialize or After a restart — and most apply on save. A saved value still waiting is listed above the Settings view’s groups with its stored and running values; Apply now there reinitialises the services, which applies every key marked After reinitialize and ends this browser session. A key marked After a restart waits for the service itself to be restarted. A listener changed on the Ports view takes effect at a reinitialise too. It is not a [Settings] key, so it is not listed as waiting; the Ports view has the button that restarts the services in place. A value set with hmailserver --set-setting while the service runs, or by another server on the same database, reaches this one at its next save, reinitialise or restart.
The Update view will not install a release A release that carries no Sigstore bundle the server can verify is not installed: the view says it cannot be verified and downloads nothing. 6.3.4 itself carries none, so no live update installs it and it is installed by hand. On Windows a server running as NT SERVICE\hMailServer, the account a new 6.3.4 installation runs as, cannot run an installer, so the apply is refused and the view says why. Download the installer from the release page, check that its Authenticode signature is valid, and run it as an administrator; chapter 18 describes the upgrade.
No control for a domain, an alias, a list, or changing an account Since 6.3.3 there is: the Deck edits domains, distribution lists and existing accounts and adds and deletes aliases; section 36.5 records what 6.3.1 lacked. In 6.3.5 the project’s census counts 498 of the 508 properties the desktop Control Panel writes as reachable from a Deck view. Signed in with an administrator role rather than the administrator password, you see only the views the role allows, and a role confined to named domains is refused the server-wide ones.

More: The Control Deck: browser administration at / and The webmail portal at /portal on the forum.


37. Glossary

Term Meaning
ACME The protocol Let’s Encrypt uses to issue certificates automatically
ARC Authenticated Received Chain — preserves authentication results across forwarding
BATV Bounce Address Tag Validation — signs your envelope sender to detect backscatter
Backscatter Bounces sent to a forged sender address; you are the innocent third party
DANE Publishing your TLS certificate in DNS (TLSA records), secured by DNSSEC
DKIM A cryptographic signature proving a message came from your domain unaltered
DMARC A policy saying what to do when SPF and DKIM fail, and where to send reports
DNSBL DNS blacklist — a list of IPs known to send spam
DNSSEC Cryptographic signing of DNS, so answers cannot be forged
Greylisting Temporarily rejecting unknown senders; real servers retry, most spambots do not
IMAP Protocol for reading mail that stays on the server
MTA Mail Transfer Agent — a mail server
MTA-STS A policy telling other servers that mail to you must be encrypted
MX record The DNS record saying which server receives a domain’s mail
Open relay A server that forwards mail for anyone. Never be one
POP3 Protocol for downloading mail to one device
PTR record Reverse DNS — maps an IP back to a host name
Reinitialise Stopping and starting the server’s services in place, so they reload their configuration: Apply now in the Control Panel or the Control Deck, or POST /api/v1/server/reinitialize. Since 6.3.4, 28 settings wait for one; most apply when saved
SASL The framework mail protocols use to authenticate
SCRAM A SASL mechanism where the password never crosses the wire
Settings store Since 6.3.4, the database, which holds the [Settings] keys; [Settings] in hMailServer.ini is its copy, and a line edited there is ignored in favour of the stored value, reported as HM5804 and, where the service may write the file, put back (chapter 21)
Sieve The standard language for filtering mail during delivery
Smart host Another server you relay your outbound mail through
SMTP The protocol for transferring mail between servers
SPF A DNS record listing which servers may send mail for your domain
SRS Sender Rewriting Scheme — rewrites the envelope sender when you forward, so SPF still passes
STARTTLS Upgrading a plaintext connection to an encrypted one
SURBL A blocklist of domains appearing in spam message bodies
TLS-RPT Reports other servers send you when TLS to your server fails, asked for by your TLS-RPT DNS record; hMailServer sends the same reports about other domains once TlsRptFromAddress is set

hMailServer 6.3.8 — maintained by Christopher Holloway / Progressive Robot Ltd. Licensed under AGPL-3.0-or-later. Source: https://gitlab.com/hmailserver/hmailserver, mirrored on GitHub. Bug reports: GitLab issues, or contact-project+hmailserver-hmailserver-86730559-issue-@incoming.gitlab.com without a GitLab account; security reports only by the routes in SECURITY.md (§19.8).