Skip to content

Installing on Linux

Christopher Holloway edited this page Sep 26, 2026 · 35 revisions

Installing on Linux

Released in 6.3.0, on 10 September 2026 - the first release in this server's history to carry Linux packages, and the reason that number is 6.3 and not 6.2.29. From 6.3.0 to 6.3.3 every release carried a .deb, an .rpm and an AppImage for x86-64 and AArch64, built by the tag's own GitHub workflow. 6.3.4, the latest release (26 September 2026, schema 6103), carries the x86-64 .deb and .rpm only: it was built by hand after GitHub's suspension, with no ARM machine to build on, and the tool the AppImage is made with is published only on GitHub. For AArch64, or for an AppImage, take 6.3.3's, or build from source as below.

hMailServer runs on Linux, on x86-64 and AArch64, since the work of 8 September 2026. This page says exactly how far that goes: what is packaged, what is proven, what is not there yet, and how a Linux installation is administered without a Control Panel. Every claim on it was checked against the tree that day, and brought up to 6.3.4 (master 92b8f9f63) on 26 September 2026; section 5 of Roadmap2, The Linux port beyond compiling, is the running record of the rows still open, and the packaging README is the file this page is drawn from.

Read this first. The Linux build is the server: the same SMTP, POP3, IMAP, delivery, anti-spam and REST code as the Windows service, compiled with clang or GCC from one CMakeLists.txt. It is not the Control Panel, which is a Windows application, and it has no COM, which is the surface every Windows setup tool uses. Administration on Linux is the configuration file, the server's own command-line options, and the REST API - with the Control Deck, the browser administration page the same listener serves - which creates and changes domains, accounts, rules, routes, certificates, listeners and settings. Since 6.3.4 there is also hmctl, a command-line client over that API (Command Line), and the database, not the file, holds the [Settings] keys (Administering it). If you need the Control Panel, you need Windows, or a Windows machine that reaches the Linux server's REST listener.

flowchart LR
    subgraph win["Windows"]
        CP["Control Panel"] -- COM --> WS["hMailServer.exe<br/>(service)"]
    end
    subgraph lin["Linux"]
        INI["/etc/hmailserver/hMailServer.ini"] --> LS["/usr/bin/hmailserver<br/>(systemd unit)"]
        CLI["hmailserver --set-admin-password<br/>--create-database / --upgrade-database"] --> LS
        REST["REST API :8045<br/>(and the Control Deck)"] --> LS
        LS --> DB[("PostgreSQL or MySQL/MariaDB")]
        LS --> STORE["/var/lib/hmailserver"]
    end
Loading

Contents

  1. What is proven
  2. What you get
  3. Installing the package
  4. First start, step by step
  5. The server's command line
  6. Where things are
  7. Administering it
  8. Logs and the journal
  9. Upgrading
  10. What is not there yet
  11. Building it yourself

What is proven

On 8 September 2026, against a live PostgreSQL 18:

Step Result
hmailserver --create-database Created the database and built the schema (version 6031) from the create script
hmailserver --upgrade-database Reported nothing to do
systemctl start (the unit runs --foreground) The server started and opened its SMTP, POP3, IMAP and REST listeners
POST /api/v1/domains/{domain}/accounts Created an account
SMTP submission with AUTH Accepted, delivered to the mailbox on disk
IMAP SELECT INBOX / FETCH The same message, subject intact

The schema version in the first row is the one the binary of 8 September carried: 6.3.0 and 6.3.1 shipped 6031, 6.3.2 6038, 6.3.3 6040 and 6.3.4 - the latest release - 6103 (Changes Since 6.2.10 section 2), so a database created by 6.3.4 is stamped 6103.

Both architectures compile every one of the 496 core translation units (x86-64 natively, AArch64 cross-compiled); the .deb was installed on a machine, created the service user and directories with the stated modes, and its binary read the packaged configuration. The AArch64 packages, up to 6.3.3's, were built by CI on an ARM runner until GitHub became unavailable on 18 September 2026, and have not yet been installed on ARM hardware by hand; the roadmap says so. 6.3.4 has none. Since 25 September 2026 GitLab CI has a linux-cross job, written to compile every core translation unit for AArch64 on the project's own runner and to build no package; no GitLab job had run yet when ContinuousIntegration.md was written, on 25 September 2026 (ContinuousIntegration.md).

What you get

For a tag vX.Y.Z the release page holds, beside the Windows installer, the artefacts below - every one of them for 6.3.0 to 6.3.3, and only the two x86-64 packages for 6.3.4:

Artefact For Name In 6.3.4
Debian package Debian, Ubuntu and derivatives, x86-64 hmailserver_X.Y.Z_amd64.deb Yes: hmailserver_6.3.4_amd64.deb
Debian package the same, AArch64 hmailserver_X.Y.Z_arm64.deb No - 6.3.3's
RPM Fedora, RHEL, openSUSE and derivatives, x86-64 hmailserver-X.Y.Z-1.x86_64.rpm Yes: hmailserver-6.3.4-1.x86_64.rpm
RPM the same, AArch64 hmailserver-X.Y.Z-1.aarch64.rpm No - 6.3.3's
AppImage any x86-64 or AArch64 distribution hMailServer-X.Y.Z-x86_64.AppImage, hMailServer-X.Y.Z-aarch64.AppImage No - 6.3.3's, x86-64 and AArch64

On GitLab the two x86-64 packages also have a version-less address that always fetches the latest release's file, https://gitlab.com/Progressiverobot/hmailserver/-/releases/permalink/latest/downloads/ followed by hmailserver_amd64.deb or hmailserver.x86_64.rpm. GitLab picks that release by date and has no pre-release flag, so the project's own download links, https://updates.progressiverobot.com/download/hmailserver_amd64.deb and /download/hmailserver.x86_64.rpm, always lead to the newest stable release's file instead, never a pre-release's; they work once that site serves HTTPS (issue #30), and until then the addresses above do. The AArch64 packages and the AppImages have none while the latest release lacks them: the links in the table are 6.3.3's own, https://gitlab.com/Progressiverobot/hmailserver/-/releases/v6.3.3/downloads/ followed by hmailserver_arm64.deb, hmailserver.aarch64.rpm, hMailServer-x86_64.AppImage or hMailServer-aarch64.AppImage. The server's own update checker still looks for the versioned names above.

Up to 6.3.3 each has a Sigstore .cosign.bundle beside it, signed by the same workflow that signs the installer; verify one the same way (Installing hMailServer has the cosign verify-blob line). 6.3.4's packages carry no bundle and no signature, so no server's live update installs them: install the package by hand, as below. The names are not cosmetic: a Linux server's update checker builds the name its own package manager and architecture would install and looks for exactly that on the release page.

One more thing exists and is not a release asset: a PKGBUILD for Arch in the tree (hmailserver/source/Server/platform/packaging/PKGBUILD), which builds from the tag on your own machine, which is what Arch asks for. Its source line clones the tag from the project's repository on GitLab (its url, https://gitlab.com/Progressiverobot/hmailserver). The AppImage (build-appimage.sh makes one from a built tree, with linuxdeploy and its AppImage plugin put in build/.appimage-tools by hand, since the script no longer downloads them from GitHub) is a try-it-out artefact for a daemon that is not meant to run out of a home directory, and is labelled as such on its way out.

Installing the package

sudo apt install ./hmailserver_6.3.4_amd64.deb         # Debian, Ubuntu
sudo dnf install ./hmailserver-6.3.4-1.x86_64.rpm      # Fedora, RHEL

The packages declare their library dependencies automatically (dpkg-shlibdeps for the .deb, rpm's own auto-requires), so libpq is pulled in with them. If you are going to use MySQL or MariaDB, install its client library yourself: the server opens libmariadb.so.3 at run time rather than linking it, so no package manager will do it for you - libmariadb3 on Debian and Ubuntu, mariadb-connector-c on Fedora and RHEL, mariadb-libs on Arch.

The package installs the server enabled and stopped, on purpose. It has no database yet, and a mail server that cannot reach one writes an error to the log every second until somebody notices. The post-install message says what to do next; the next section is the long form of it.

What the maintainer scripts do, so you know what to undo:

Create the hmailserver system user and group
Create /var/lib/hmailserver and its temp, events and database subdirectories, and /var/log/hmailserver, all 0750 hmailserver:hmailserver
Install /etc/hmailserver/hMailServer.ini at 0640 root:hmailserver - readable by the service, writable only by root
Enable hmailserver.service, without starting it
On upgrade run hmailserver --upgrade-database as the service user (only if a database is configured), then try-restart the service if it was running
On remove stop and disable; leave the mail store and the logs alone, and say so
On purge (.deb) remove /etc/hmailserver as well; still leave the store

First start, step by step

1. Create the database and its user. PostgreSQL:

sudo -u postgres createuser --pwprompt --createdb hmailserver

With --createdb the server creates the database itself in step 4. If you would rather the service role could not create databases - which is the tidier policy - leave the flag off and create it yourself:

sudo -u postgres createuser --pwprompt hmailserver
sudo -u postgres createdb --owner hmailserver hmailserver

Step 4 notices an existing, empty database and builds the schema inside it. Since 6.3.4 it leaves a database that already holds this build's schema, or an older one, as it is with exit status 0, and exits 1 for a newer one; on 6.3.3 it refused any database that already held a schema and named --upgrade-database.

2. Tell the server where it is.

sudoedit /etc/hmailserver/hMailServer.ini

Two things to change, and the file has a paragraph on each:

  • [Database] Type - PostgreSQL or MySQL.
  • [Database] Server, Database, Username, Password and Port. Write the port out (5432, 3306); it goes into the connection string as it stands and there is no useful default.
  • Since 6.3.4, optionally [Database] TlsMode (prefer, the default, require, verify-full or disable) and TlsCaFile. With nothing set, MySQL and MariaDB are reached over TLS whenever the database server offers it, the certificate unchecked; before 6.3.4 the connection through MariaDB Connector/C was never encrypted. sudo -u hmailserver hmailserver --check-database makes one connection and says what it got (DatabaseTls.md). A MySQL or MariaDB server must be MySQL 5.7.9 or MariaDB 10.2.2 or later, since every table is utf8mb4 from schema 6102.

Leave AdministratorPassword alone; the next step writes it.

Since 6.3.4, this is also the moment to set the REST listener of step 7 - [Settings] RestApiPort and RestApiBindAddress, and the certificate and key if you bind anything but loopback - because every [Settings] key the file holds at the server's first start is taken into the database then, the packaged RestApiPort=0 among them, and a later edit to a key the database holds is not applied (step 7 has the command for that case).

3. Set the administrator password. It is read from standard input with echo off, hashed with PBKDF2 exactly as the Control Panel hashes it on Windows, and written into the file. The file is root-owned, so this runs as root:

sudo hmailserver --set-admin-password

4. Create the schema.

sudo -u hmailserver hmailserver --create-database

If the database is not there, this connects to the server the [Database] section names with no database selected, creates it in the backend's own dialect and reconnects; if it is there and empty, it uses it. Either way it then runs the create script for the backend's type - what DBSetupQuick does over COM on Windows. It refuses the two Windows-only backends (SQL Server and SQL Server Compact) by name.

A new installation is secured at its first start (since 6.3.4). A database --create-database makes is marked as a new installation's, and it leaves a file, .new-installation, in the configured DataFolder. Before every start the unit runs hmailserver --apply-new-install-defaults --if-marked, which does nothing unless that file is there; at the first start after step 4 it gives the server a self-signed certificate (RSA 2048, 825 days, under /var/lib/hmailserver/Certificates), STARTTLS on 25, 587, 110 and 143, and TLS required for authentication on the Internet IP range, so that a password from another machine never crosses the network in the clear; this machine's own range is left as it was. It acts only on a database marked within a day of its clock and still as the create script left it, and never on an upgraded or attached one. A DataFolder outside the unit's ReadWritePaths needs a drop-in adding it before that first start. Mail clients warn about the self-signed certificate: replace it with ACME or your own (Encryption and Certificates, SecureDefaults.md).

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 that lives in hMailServer.ini, never in the database or a backup. Run as the service user, step 4 cannot write the root-owned file and makes none; make it, and export it at once to keep with the database backups, 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 (SecretsAtRest.md).

5. Check what it read, before opening anything.

sudo -u hmailserver hmailserver --check-config

It prints the configuration path, the directories and the database type it made of what you wrote, and exits without opening a listener or the database. Database type: 0 means the Type key did not take.

6. Start it.

sudo systemctl start hmailserver
sudo systemctl status hmailserver
journalctl -u hmailserver -f

7. Turn on the REST API, which is how you will administer it. In the configuration, [Settings] RestApiPort=8045 and RestApiBindAddress=127.0.0.1 (loopback, unless you also set RestApiCertificateFile and RestApiPrivateKeyFile), then:

sudo systemctl reload hmailserver        # SIGHUP: re-reads the configuration
curl -u Administrator http://127.0.0.1:8045/api/v1/status

A reload stops and restarts the listeners inside the running process, so do it when nothing is mid-delivery. The user name is Administrator, as on Windows.

That is 6.3.3, where the file is what the server reads. Since 6.3.4 it works only if those lines were in the file before step 6. From schema 6042 the database is the settings store: the first start took the packaged RestApiPort=0 into it, and a value edited into the file afterwards is reported in the error log as HM5804 and the stored value is used, so the reload brings up no listener. With the listener off, nothing that speaks the API can change it either. The server's own command is the door:

sudo hmailserver --set-setting RestApiPort=8045
sudo hmailserver --set-setting RestApiBindAddress=127.0.0.1
sudo systemctl restart hmailserver

--set-setting stores the value the way the REST API stores one, writes the server's copy into the file (it runs as root because the file is root's, and keeps its owner and mode), and prints what it stored. It refuses a name the server does not read, a port that is not 0 to 65535 and a bind address that is not an IP address or localhost; a secret's value - SRSSecret, PasswordPepper and the like - is refused on the command line, and is typed at a prompt instead with the name alone (sudo hmailserver --set-setting SRSSecret). The file's [SettingsOverride] section, applied over the store, is the other way through:

[SettingsOverride]
RestApiPort=8045
RestApiBindAddress=127.0.0.1
sudo systemctl reload hmailserver        # the listener starts; HM5807 names the override
export HMAIL_URL=http://127.0.0.1:8045
hmctl ini set RestApiPort 8045            # stored, reported as HM5808 while the override stands
hmctl ini set RestApiBindAddress 127.0.0.1
sudoedit /etc/hmailserver/hMailServer.ini   # remove the [SettingsOverride] section
sudo systemctl reload hmailserver

hmctl asks for the administrator password (or reads HMAIL_PASSWORD); curl -X PUT with {"value":"8045"} on /api/v1/settings/ini/RestApiPort does the same. One more thing will show in the error log of a package installation: the packaged file is 0640 root:hmailserver and the unit's ProtectSystem=strict leaves /etc read-only to the service, so the server cannot write its copy of a stored value back into the file. A value stored over the 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 is changed by hand to the same value.

8. Your first domain. Open the Control Deck - http://<host>:8045/ in a browser, in any of its twenty languages - sign in as Administrator with the password from step 3, and add the domain on its Domains view: the name, the postmaster, and everything else as the Control Panel would set it. The same over the API, for a script or a session with no browser:

curl -u Administrator -H "Content-Type: application/json" \
     -d '{"name":"example.com","active":true,"postmaster":"postmaster@example.com"}' \
     http://127.0.0.1:8045/api/v1/domains

The name is checked exactly as the Control Panel checks it (400 with its sentence if it is not a valid domain name, 409 if it already exists), and every other setting takes the default a new domain gets there. PUT /api/v1/domains/example.com with {"active":false} switches it off; DELETE removes it with its accounts, aliases, lists and directories. Creating and deleting a domain are server-wide: an API key restricted to named domains gets 403.

Then create the account over REST and send yourself a message - the table at the top of this page is the sequence that was run.

The server's command line

hmailserver --help, verbatim in substance:

Option What it does
--config <file> The configuration file to read. Default: hMailServer.ini beside the executable, then /etc/hmailserver/hMailServer.ini
--foreground Run in the foreground and log to standard error as well as to the log directory. This is the default; the option exists so the unit file can say so
--check-config Read the configuration, report what it says, exit without opening a listener or the database
--set-admin-password Read a new administrator password from standard input (echo off), hash it with PBKDF2, write it to the configuration, exit
--create-database Create the database [Database] names on the server it names, and run the create script for its type
--upgrade-database Run every upgrade script from the database's version to this build's, in order. Refuses a database from a newer build; stops rather than loops if a script runs and the version does not move
--check-database Since 6.3.4. Connect once to the database, with its TlsMode and TlsCaFile, say whether the connection is encrypted and whether the certificate was checked, and exit: 0 connected, 1 not, 2 not configured
--set-setting NAME=VALUE, --set-setting NAME Since 6.3.4. Store one [Settings] value in the database and write the server's copy into the file - for the settings the server needs before its REST API can be reached, RestApiPort above all (step 7). With the name alone the value is read from standard input, and a secret's value is refused on the command line. Run as root
--apply-environment Since 6.3.4. Store every setting the environment names - HM_SETTING_<key>, HM_PROPERTY_<name>, their _FILE forms and the container's HM_REST_* - each checked first, nothing stored if one is refused; an administrator's change to a setting it names is then refused until a run without it (Running in a Container, Profiles and Environment). Run as the service user
--apply-profile NAME [--confirm] [--once] [--if-new], --revert-profile Since 6.3.4. Apply one of the four setting profiles, or put back what the last one changed (Profiles and Environment). --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. Run with the server stopped, as the service user
--apply-new-install-defaults [--if-marked [<file>]] Since 6.3.4. The secure defaults of a new installation, above. Only to a database --create-database marked, still as the create script left it; exit 5 and nothing changed otherwise. Run as the service user, with the server stopped
--sample-domain create, --sample-domain remove Since 6.3.4. Make, or take away again, the throwaway example.test with three mailboxes, a list, a rule and eight delivered messages (Your First Domain and Mailbox). Remove it before the server takes real mail: its password is in the documentation
--measure-store, --compress-store [--decompress], --recount-store Since 6.3.4. What compressing the message store and holding its large parts once would save, and the rewrite either way, with the server stopped (MessageStore.md)
--encrypt-store, --decrypt-store Since 6.3.4. Encrypt, or decrypt with the domain's recovery passphrase, what a mailbox encrypted at rest stored before it was switched on (Encryption and Certificates)
--copy-store-to-object-storage, --copy-store-from-object-storage Since 6.3.4. Copy the message store into the object store the settings name, or back, each file verified and a stopped run resumed (ObjectStorage.md)
--verify-update <file> Since 6.3.4. Check a downloaded package or AppImage against the Sigstore bundle beside it, the way the server does before applying one; exit 0 when a signer the server trusts made it
--create-stored-secrets-key, --export-stored-secrets-key <file>, --import-stored-secrets-key <file>, --forget-sealed-secrets <key id> Since 6.3.4. Make, export, import on another node, or - lost for good - clear what was sealed under the stored-secrets key. Run as root
--version, --help

Signals: SIGTERM and SIGINT stop the server; SIGHUP re-reads the configuration (what the Control Panel's Reinitialize does); SIGPIPE is ignored, so a peer that hangs up cannot kill the process.

Where things are

From the package's install rules:

/usr/bin/hmailserver                          the server; one file
/usr/bin/tlds.txt, /usr/bin/dh2048.pem        read from beside the binary; the second is
                                              required by every TLS context
/usr/share/hmailserver/DBScripts/*.sql        schema creation and upgrade scripts
/usr/lib/systemd/system/hmailserver.service   the unit
/etc/hmailserver/hMailServer.ini              the configuration (a conffile: an upgrade
                                              never overwrites it). Since 6.3.4 its
                                              [Settings] section is a copy of the
                                              database, which holds those keys
/usr/bin/hmctl                                since 6.3.4: the command-line client over
                                              the REST API
/etc/logrotate.d/hmailserver                  the log rotation rule

Created by the maintainer scripts, and therefore not removed with the package:

/var/lib/hmailserver              the mail store: one directory per domain, one per mailbox
/var/lib/hmailserver/temp         scratch space for message assembly and backups
/var/lib/hmailserver/events       event scripts
/var/log/hmailserver              the logs

The six [Directories] keys in the packaged configuration point at exactly this layout, and since 6.3.0 a relative value in any of the five non-program directories is resolved against ProgramFolder, so a moved installation can be written as one absolute path rather than six (Relocating an Installation).

Administering it

There is no Control Panel and no COM here. These surfaces:

  • The configuration file. On 6.3.3, everything in the Settings Reference that is an hMailServer.ini key applies unchanged; the packaged file has a paragraph on every key it sets. Since 6.3.4, the file keeps [Directories], [Database], [Security], [GUILanguages], [LDAP], [SendingLimits], [SendingLimitsOverrides] and PasswordPepper, and every other [Settings] key is stored in the database from schema 6042: set before the first start, a value is taken in from the file; after it, a value is changed over the REST API (PUT /api/v1/settings/ini/{name}) or with hmctl ini set, and an edit to the file for a key the database holds is reported as HM5804 and not applied (Settings Overview). On 6.3.3 a change applies at the next start or systemctl reload; since 6.3.4 a value stored over the API applies when it is saved for 211 of the 241 keys, and the rest wait for a reinitialise (28, a listener's port or TLS among them) or a restart (2). Every setting can also come from the environment (hmailserver --apply-environment), and a setting profile sets the thirty or so that make a server the kind it is (Profiles and Environment). WindowsEventLogEnabled=1 is a Windows inheritance in name only - on Linux it means syslog, and the operational events (database down, a listener that could not bind, a failed backup, the disk floor) reach the journal through it.
  • Directory authentication is LDAP, and only LDAP: [LDAP] Enabled=1 with Server, Security (2 = LDAPS, 1 = StartTLS) and a simple bind, proven against OpenLDAP. There is no LogonUser here, so a directory-linked account with LDAP switched off is refused with HM6421 naming the settings to set.
  • Stored secrets - the database password in the configuration, route and fetch-account passwords, the TOTP secret - are protected under a key the server makes on first use, <DataFolder>/.hmailserver-secret-key (32 bytes, mode 0600), with AES-256-GCM; the stored form starts LINUX1:. The key file travels with the data directory: back it up with the database, and a secret moved to a machine without it cannot be opened (HM6414). A Windows DPAPI: secret is likewise unreadable on Linux and is reported once. 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 there is one (step 4 above).
  • Event scripts (since 6.3.4): JavaScript, in the QuickJS engine the Linux build embeds, for the same events and function names as on Windows, with ScriptLanguage=JavaScript; a script has no file, process or network access, and ScriptTimeout and a 64 MB ceiling bound it. Before 6.3.4 a Linux server refused any script (EventScripts.md).
  • SNMP (since 6.3.4): a read-only SNMPv3 agent, off, and answering nobody until a manager's address is in SnmpAllowedAddresses; binding UDP 161 needs the unit's CAP_NET_BIND_SERVICE, which the packaged unit has (Monitoring and Health).
  • The REST API (The REST API). Accounts, aliases, settings, the queue, the message store, the self-service portal. Authenticate as Administrator with the password from step 3. /api/v1/settings/ini - every [Settings] key - takes the administrator password only; an API key of any scope is refused.
  • The Control Deck, served by the same listener at / (Control Panel Pages), reads and writes. hmailserver/docs/DeckParity.md counts it from the sources: in 6.3.3, 15 views reach 322 of the 330 COM properties the desktop Control Panel writes - domains and accounts, lists, IP ranges, external accounts, rules, routes, certificates, listeners, server messages, the settings groups, the backup, the queue, the logs and DANE records; the gaps are account groups, folder permissions and application passwords. In 6.3.4 it has 40 views (DeckParity.md's count) - adding, among others, the audit trail, groups, roles, remote-domain policies, reports, devices, webhooks, spam traps, the reputation watch, what changed from stock, the first-hour checklist with the setting profiles, the API keys, the quarantine, the archive and its holds, the update, the DNS records to publish, folder permissions, migrations and the DMARC reports received - and the census counts 466 properties the desktop program writes, 458 of them writable over REST and all 458 reached from a view. The eight left are an account's directory fields (title, department, phones, photo), which PUT /api/v1/accounts/{address}/directory carries under names the census does not match, so read DeckParity's What the heuristic gets wrong before calling them a gap. On 6.3.3 its Settings view carries only four [Settings] keys (SmtpTarpitCount, SmtpTarpitDelaySeconds, UseLanguage, RewriteEnvelopeFromWhenForwarding), so a listener, TLS, OAuth 2.0 or logging key is the file there. Since 6.3.4 the settings groups the view draws carry more rows kept as [Settings] keys, each with a badge saying when a save applies; a key no group carries is the API or hmctl. The Control Deck is the walk through it.
  • hmctl (since 6.3.4), installed by the packages as /usr/bin/hmctl: the same API from a shell, with hmctl ini set, get, list and delete for the [Settings] keys, hmctl config export, diff and apply for the configuration as one document, hmctl config changed for what differs from stock, hmctl settings profiles and apply-profile for the setting profiles and hmctl sample-domain for the throwaway domain (Command Line).

Logs and the journal

The server's own logs are in /var/log/hmailserver, in the same format as on Windows (Troubleshooting reads them), and the files the server marks with a byte-order mark - the backup and event logs, Sieve scripts, the backup index - are UTF-16LE on both platforms, so a file written on one reads on the other. A memory-safety fault is recorded in /var/log/hmailserver/crash-oracle.log (one line: signal, address, thread, event number), after which the process dies by the signal it caught and the kernel's core-dump policy applies; the unit file says how to allow a core, and why it is off by default. The journal carries the startup line, anything written to standard error, and the operational events. The logrotate rule uses copytruncate because the server does not reopen its log on a signal; the rule says so at length.

journalctl -u hmailserver -f
tail -f /var/log/hmailserver/hmailserver_$(date +%F).log

Upgrading

Install the new package. On an upgrade the maintainer scripts run hmailserver --upgrade-database as the service user (only when a database is configured) and then try-restart the service if it was running. A failure of the schema upgrade is reported and does not fail the package - the binary is already in place, and the server's own log is the better place to read why - so read what the upgrade printed:

sudo -u hmailserver hmailserver --upgrade-database    # safe to run by hand; "nothing to do" when done

6.3.4 is installed by hand: its packages carry no Sigstore bundle, so no server's live update installs them. The configuration is a conffile and is never overwritten. Upgrading to 6.3.4: --upgrade-database moves the schema to 6103 - on MySQL and MariaDB the 6102 step rebuilds every table as utf8mb4, which takes time on a large database and needs the ALTER privilege (Upgrading hMailServer) - and the server's next start - the maintainer script's try-restart, if it was running - makes the database the settings store: keys the 6.3.3 mirror already held keep their stored values, and any [Settings] key of /etc/hmailserver/hMailServer.ini it never held is taken in from the file. A [Settings] line edited after the old server last started is treated as an edit at that first start (HM5804) and not applied; restart the old server once after the last change before upgrading. An upgrade makes no stored-secrets key and never applies the secure defaults of a new installation; the setup checklist says what is left. Before it stops, a 6.3.4 server drains - the SMTP, IMAP, POP3 and ManageSieve listeners close at the socket, so senders retry, and the queue pauses - and ShutdownDrainSeconds (30 is a reasonable figure) lets what is in flight finish; POST /api/v1/server/drain does the same on its own for maintenance. The package's post-install step still runs the schema upgrade, so the new bootstrap key [Database] UpgradeOnStart is not needed here (Upgrading hMailServer). The server's update checker, when enabled, looks for the package name its own platform would install and tells you. A 6.3.4 server reads https://updates.progressiverobot.com, which GitLab Pages serves from the project's releases; a server from 6.3.0 to 6.3.3 reads GitHub's feed, which answers 404 since 18 September 2026, until UpdateFeedUrl=https://updates.progressiverobot.com/releases.json points it there (UpdateFeed.md).

Since 6.3.4, it can also apply it - a release signed by a signer it trusts, which 6.3.4's own packages are not. The server runs unprivileged and cannot install a package, so it writes a one-file request into /run/hmailserver and a root path unit the package enables, hmailserver-update.path, takes it and runs /usr/lib/hmailserver/hmailserver-update. That program checks every line of the request against root's own configuration rather than the request's word for it, stops the unit, verifies the package again as root against its Sigstore bundle - the same check the server made, made by a process the server could not have influenced - and refuses anything that is not a genuine upgrade: the version is read from the package itself and compared with what dpkg or rpm says is installed, so a signed but superseded release cannot be put back. Then it installs with apt-get/dpkg or dnf/rpm (an AppImage is swapped in place, the old one kept as .previous), starts the unit, waits for the server to stay up - and for /readyz when the metrics listener is on - reinstalls the previous version if it does not, and writes one line to <DataFolder>/Updates/last-apply.txt that the server reports at its next check and the Control Deck shows. A refused request is explained in journalctl -u hmailserver-update and nowhere else, on purpose: root writes nothing into the service user's directory while a process of theirs may still be running.

Check that the machine can do it at all with

sudo /usr/lib/hmailserver/hmailserver-update check

which names the installer it would use, whether the path unit is active and whether the running unit has the runtime directory the request needs. The Debian and RPM packages enable and start the path unit for you; Arch does not enable units from a package, so there sudo systemctl enable --now hmailserver-update.path is yours to run. Nothing happens unless you turn the update check on and ask for the install, from the Control Deck or over the API.

In a container

In the tree since 11 September 2026; the release workflow on GitHub pushed the image to ghcr.io/progressiverobot/hmailserver from 6.3.2, which has answered 404 since GitHub became unavailable on 18 September 2026. The Compose file and the chart now name registry.gitlab.com/progressiverobot/hmailserver, the project's GitLab container registry, which is switched on and holds no image yet, so for now the image is built from the release's .deb (Running in a Container).

The same package as a container image, with the database, the REST listener and the administrator password written from the environment on start - and, since 6.3.4, every other setting too: Running in a Container. Everything on this page about what the server is, how it is administered and where things are applies to the container too; that page says what differs, which is mostly how the API is reached.

The firewall and fail2ban

Shipped in 6.3.2. Since 6.3.4, AutoBanFirewall, AutoBanCommand and AutoBanNeverBan are stored settings like the other [Settings] keys: set them over the API or with hmctl ini set rather than in the file.

An auto-ban (repeated failed logons from one address) is enforced by the server's own listeners: the kernel accepts the connection, the server refuses it. Two ways to push the ban below the server, and one is enough:

fail2ban, which needs no capability at all because it runs as root and reads the application log. The package installs /etc/fail2ban/filter.d/hmailserver.conf and a jail at /etc/fail2ban/jail.d/hmailserver.conf, disabled as Debian asks of a shipped jail. The filter matches the two fixed-shape lines the server writes for every counted failure and every ban, whether or not protocol logging is on (AutoBanLogonEnabled, on as shipped, is what writes them). Turn it on:

printf '[hmailserver]\nenabled = true\n' > /etc/fail2ban/jail.d/hmailserver.local
systemctl reload fail2ban
fail2ban-client status hmailserver

The server itself, with AutoBanFirewall=1 under [Settings] (on 6.3.3): on every ban and every expiry it runs /usr/lib/hmailserver/autoban-hook, which keeps an nftables set with a timeout (iptables where there is no nft) and drops TCP from the banned address to the ports the server listens on. Changing the firewall needs CAP_NET_ADMIN, which the unit deliberately does not grant a mail server by default. Grant it once, with a drop-in, and the hook's check verb says whether it can act:

systemctl edit hmailserver
   [Service]
   AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_NET_ADMIN
   CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_NET_ADMIN
systemctl restart hmailserver
sudo -u hmailserver /usr/lib/hmailserver/autoban-hook check

AutoBanCommand runs any program of yours instead of, or as well as, the hook - <command> ban <address> <minutes> <ports> and <command> unban <address> - for a cloud firewall or a router. And before turning either on, list the addresses you administer from in AutoBanNeverBan: a ban that reaches the firewall locks that address out of every mail port on the machine, not just out of one connection.

What is not there yet

Stated here rather than discovered later. Each is a row in the roadmap's Linux section, which is where the current state lives.

Gap What it means for you
The regression suite, in part hmailserver/test/LinuxRegressionTests compiles the suite's files in place - not copies of them, the same text the Windows gate runs - and puts a subset of its tests against a Linux server. Last measured on the tree of 17 September 2026 (7ef5fba2e): 1,725 results, 1,439 passed, 286 skipped, 0 failed. From 17 September it ran in CI as two halves on every push, with no continue-on-error, on GitHub Actions until GitHub became unavailable to the project on 18 September 2026; since 25 September 2026 .gitlab-ci.yml gives it a job, linux-suite, written to run it whole on the project's own runner for master, batch* branches and release tags, which had not yet run when ContinuousIntegration.md was written. What skips is what cannot be asked over HTTP or has no route yet - the public folders, a stored message's MIME as a COM object, the COM logon - and every skip names its reason. The Windows suite, which gates every change, has 3,698 tests in 6.3.4 (2,302 in 6.3.3)
AArch64 on hardware, and in 6.3.4 CI built and installed the packages on an ARM runner until GitHub became unavailable on 18 September 2026; nobody has installed one on ARM hardware by hand yet, and 6.3.4 has no AArch64 package or AppImage at all
Directory authentication is a simple bind over LDAPS or StartTLS, and only that [LDAP] BindMethod=1 (Negotiate) is Windows SSPI; on Linux it is refused (HM6420) as a directory failure, not a wrong password. The directory's certificate must chain to the system trust store, and Server must match the name in that certificate
No re-keying command The stored-secret key file is made once and never rotated; replacing it means re-entering every stored password

Building it yourself

The packaging README has the full text; the short form:

sudo apt install cmake ninja-build clang libssl-dev zlib1g-dev libpq-dev \
     libboost-thread-dev libboost-chrono-dev libboost-filesystem-dev libboost-regex-dev rpm file

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     # the .deb and the .rpm

clang is a preference, not a requirement: GCC 13 and later builds the same tree (export CC=gcc CXX=g++), and CI built with both until 18 September 2026; GitLab CI's linux-build job is written to build with clang.

build/linux-tu-census.sh compiles every core source on its own and prints how many a POSIX compiler accepts; --target aarch64-linux-gnu is the cross measurement. Until GitHub became unavailable on 18 September 2026, CI ran both on every push and failed when a single translation unit stopped compiling; since 25 September 2026 GitLab CI has a linux-cross job that does the AArch64 half on the project's own runner, which had not yet run when ContinuousIntegration.md was written.

Home — full index

Start here

1. Install and run

2. Secure it

3. Operate it

4. Extend it

5. Contribute to it

Look it up — from any journey


About this wiki

Clone this wiki locally