Skip to content

Installing on Linux

Christopher Holloway edited this page Oct 26, 2026 · 34 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.7, the latest release (17 October 2026, schema 6108), carries, as 6.3.6 did, the generic .deb and .rpm for x86-64 and AArch64, a .deb for Debian 12 and 13 and Ubuntu 24.04 and an .rpm for Fedora 44 and 43, EL 10 and 9, openSUSE Leap 16.0 and Tumbleweed, each for both architectures, and both AppImages, built by the tag's GitLab pipeline; the AArch64 files are cross-compiled and tested under emulation. 6.3.4 and 6.3.5 carried the x86-64 .deb and .rpm only - 6.3.4 built by hand after GitHub's suspension, with no ARM machine to build on, and 6.3.5 by its tag's pipeline, which kept its AArch64 packages and AppImages as CI artifacts, out of the release - so an AArch64 or AppImage installation's last release before 6.3.6 is 6.3.3, and it upgrades to the newest release by hand, once. Brought up on 5 October 2026 to 6.3.6 (6 October 2026, schema 6108), for what 6.3.6 changes on this page, and on 18 October 2026 to 6.3.7, the latest release (17 October 2026, schema 6108), for the files it carries, its schema and how a server reaches it.

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, and 8adce850a, the commit the tag v6.3.4 was first made on, for the schema and the checks of a download, which are the same at the tag, 51e2dbda0) 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, 6.3.4 6104, 6.3.5 6105, and 6.3.6 and 6.3.7 - the latest release, which adds no step - 6108 (Changes Since 6.2.10 section 2), so a database created by 6.3.6 or 6.3.7 is stamped 6108.

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. It passed in master's first full pipeline, 2886389460 on 27 September 2026, in 9 min 19 s (ContinuousIntegration.md). The generic AArch64 .deb and .rpm are built and installed under emulation by the package jobs of .gitlab/ci/, and a release carries them again from 6.3.6.

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 from 6.3.6, and only the two x86-64 packages for 6.3.4 and 6.3.5:

Artefact For Name In 6.3.7 (as in 6.3.6)
Debian package Debian, Ubuntu and derivatives, x86-64 hmailserver_X.Y.Z_amd64.deb Yes: hmailserver_6.3.7_amd64.deb (and in 6.3.4 and 6.3.5)
Debian package the same, AArch64 hmailserver_X.Y.Z_arm64.deb Yes: hmailserver_6.3.7_arm64.deb (6.3.4 and 6.3.5 had none; 6.3.3's was the last before 6.3.6)
RPM Fedora, RHEL, openSUSE and derivatives, x86-64 hmailserver-X.Y.Z-1.x86_64.rpm Yes: hmailserver-6.3.7-1.x86_64.rpm (and in 6.3.4 and 6.3.5)
RPM the same, AArch64 hmailserver-X.Y.Z-1.aarch64.rpm Yes: hmailserver-6.3.7-1.aarch64.rpm (6.3.4 and 6.3.5 had none)
Distribution packages (6.3.6) a .deb for Debian 12 and 13 and Ubuntu 24.04, an .rpm for Fedora 44 and 43, EL 10 and 9, openSUSE Leap 16.0 and Tumbleweed, each for x86-64 and AArch64 the release page names each Yes: eighteen, new in 6.3.6
AppImage any x86-64 or AArch64 distribution hMailServer-X.Y.Z-x86_64.AppImage, hMailServer-X.Y.Z-aarch64.AppImage Yes: hMailServer-6.3.7-x86_64.AppImage and hMailServer-6.3.7-aarch64.AppImage (6.3.4 and 6.3.5 had none; 6.3.3's were the last before 6.3.6)

On GitLab the two x86-64 packages also have a version-less address that always fetches the latest release's file, https://gitlab.com/hmailserver/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. The site has served HTTPS since 26 September 2026 (issue #30), and on 27 September 2026 both addresses led to 6.3.4's packages. Since 6.3.6 the AArch64 packages and the AppImages have the same: https://gitlab.com/hmailserver/hmailserver/-/releases/permalink/latest/downloads/ followed by hmailserver_arm64.deb, hmailserver.aarch64.rpm, hMailServer-x86_64.AppImage or hMailServer-aarch64.AppImage (the link paths carry no version), or the update site's /download/ names; while 6.3.4 and 6.3.5 were the latest release, which lacked them, the links in the table were 6.3.3's own. 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, so no server's live update installs them: install the package by hand, as below. Their SHA-256 are in the release's hmailserver-6.3.4-SHA256SUMS.txt, whose signature, hmailserver-6.3.4-SHA256SUMS.txt.sig, is made with the key that signs the release tags; 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 christopher.j.holloway@outlook.com -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

The first must say Good "file" signature for christopher.j.holloway@outlook.com, the second OK for each file you downloaded. Each package also has an SBOM of its own beside it, <package>.cdx.json (Security Policy, Software bills of materials). 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.

From a build service. Three carry 6.3.7, each built from the release's source tarball and the Debian and RPM packaging inside it, as Channels.md gives them, read on 21 October 2026: OBS's home project home:progressiverobot publishes it for openSUSE Leap 16.0 (x86_64 and aarch64) and Tumbleweed (x86_64), in repositories OBS signs (sudo zypper addrepo https://download.opensuse.org/repositories/home:/progressiverobot/16.0/home:progressiverobot.repo, openSUSE_Tumbleweed in place of 16.0 for Tumbleweed; then fetch the repository's key, repodata/repomd.xml.key under the same address, and import it with sudo rpm --import only if gpg --show-keys shows the fingerprint BA0868309CFBD36498CE41D22F0B9419E67EE770, OBS's key for the project, so that zypper trusts the repository without asking; then sudo zypper install hmailserver); Copr's progressiverobot/hmailserver built it for Fedora 44 and 43 and EPEL 10, each on x86_64 and aarch64 (sudo dnf copr enable progressiverobot/hmailserver && sudo dnf install hmailserver); and the PPA ppa:hmailserver/hmailserver publishes it for Ubuntu 24.04 and 26.04, amd64 and arm64 (sudo add-apt-repository ppa:hmailserver/hmailserver && sudo apt install hmailserver). On 23 October 2026 6.3.7 was installed from each of the three as a user installs it - on Ubuntu 24.04 and 26.04, Fedora 44 and AlmaLinux 10, amd64 and arm64 each, and on Leap 16.0, both, and Tumbleweed, x86_64 - and the server it installed started and answered SMTP and the REST API over TLS (Channels.md, What was tried; PackageManagers.md). The AUR and the Snap Store have nothing yet.

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/hmailserver/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. Since 6.3.6 the AppImage makes its data, logs, temp, events and database folders under $HMAILSERVER_HOME (by default $XDG_DATA_HOME/hmailserver, or ~/.local/share/hmailserver) under umask 077 and sets them 0700 on every run, so that a tree an earlier AppImage made under the user's umask - 0755 under the usual 022, in a home other users may enter on many distributions - is closed too; its configuration is 0600 (the packages' folders were 0750 and their configuration 0640 already, below).

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 container image installs libmariadb3 itself from the release after 6.3.7 (Running in a Container).

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
Require (6.3.6) ca-certificates, which apt and dnf install with the package. Every .rpm the project makes also requires ((glibc-gconv-extra(<arch>) or glibc-gconv-modules-extra(<arch>)) if (dnf and system-release)) and recommends the same pair: required where dnf is installed on Fedora or EL, whose release package provides system-release - there dnf installs them with the package, the live update's dnf install included - and only recommended elsewhere, because on openSUSE the live update installs with rpm -Uvh; zypper installs the recommendation unless solver.onlyRequires is on. On Fedora and EL a server without them whose dnf has no repository enabled (an unregistered RHEL, say) has the update refused, and stays on the earlier version, until glibc-gconv-extra is installed. The RPM spec, whose packages have no live update, requires its distribution's one by name; an AppImage runs on its host's glibc and cannot require a package - on Fedora, EL or openSUSE install it by hand. Without the converters the server reads and writes no text in a character set but Unicode's, US-ASCII, UTF-7, ISO-8859-1, ISO-8859-15 and Windows-1252 (BIG5, ISO-2022-JP, Windows-1251 and ISO-8859-2 came out empty), and from 6.3.6 it says so at its start and in --check-config (PackagesRpm.md, The character sets)
Install (6.3.6) /usr/share/hmailserver/licenses/AGPL-3.0.txt and a copyright file - /usr/share/doc/hmailserver/copyright from the .deb, /usr/share/licenses/hmailserver/copyright from the .rpm. Alpine Linux (musl; an APKBUILD and an OpenRC service) and Nix (a flake with a NixOS module) are supported from the tree (Alpine.md, Nix.md); the Debian source package and the RPM spec a distribution's maintainer starts from are SourcePackages.md

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 DBSetup does over COM on Windows (CreateExternalDatabase; DBSetupQuick creates only the built-in database). 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 differs on 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 does not write its copy of a stored value back into the file. A value stored over the API, such as these two, is in effect, and the line left as it was is not an error: the application log says, where application logging is on, that the file is read-only to the account the server runs as and that its [Settings] copy is not refreshed, and the key's line keeps its old value until it is changed by hand to the same value (The Service Account).

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. Since 6.3.6 it ends with a line Character sets: that says every character set the server converts text in - the code page table's 24 and the PST import's - converts here, or names those that do not, with the package that has them on Fedora, EL and openSUSE; a start with any missing writes the same to standard error and to the application log. A warning, not a refusal: it still exits 0
--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). Since 6.3.6, run as root, a store command, an object-store copy, --sample-domain, a profile or --apply-environment becomes the service's account - the data directory's owner, with its groups - before it opens anything, and refuses (exit 2) a data directory that cannot be examined, or whose way is not root's and its owner's alone. Over a data directory that belongs to root - a service run as root, as a source build or a container without User= has it - it runs on as root, and says so on standard error, where the data directory and every folder under it, the log, temporary, event and database folders and every folder under them, every folder a link in them leads to, the configuration that names them, and the ways to all of them are root's alone (each folder root's and written by no other account, no default access control list giving another account a write, each link root's); it then makes what it makes with every other account's write taken away, whatever the umask. Otherwise it refuses (exit 2), naming the first folder, file or link another account owns or may write - a log directory below Ubuntu's /var/log, which the syslog group may write, among them - and what to do: give it to root and take the other accounts' write from it (chown root:root, chmod go-w, setfacl -bk), name folders that are root's alone in the configuration, or give the data directory to the service's account, as the package does. Every other command line run as root (--set-setting, the stored-secrets verbs, --create-database, --upgrade-database and the rest) writes nothing to the log directory; what it did, and why it failed, is on its own output, and a report that would have gone to the error log is not written, but for the stored-secrets code's HM7496 and HM7497, which go to standard error (The Service Account, On Linux, a command run as root). The package's layout - data and logs the hmailserver account's, below folders only root writes - is the first case and is unchanged
--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/dh2048.pem                           read from beside the binary; required by
                                              every TLS context (tlds.txt is gone since
                                              6.3.6: the Public Suffix List is compiled
                                              in, and apt and dnf remove the old file)
/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). Sieve scripts and the backup index, which the server marks with a byte-order mark, are UTF-16LE on both platforms, so a file written on one reads on the other. The backup and event logs are UTF-8 on Linux since 6.3.6, as the application and error logs are - the server converts hmailserver_backup.log and hmailserver_events.log in place the first time it opens each, and logrotate's earlier copies stay UTF-16LE - and UTF-16LE on Windows. 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 6104 - on MySQL and MariaDB the 6102 step rebuilds every table as utf8mb4, which takes time on a large database and needs the ALTER privilege, and a MySQL older than 5.7.9 or a MariaDB older than 10.2.2 stops the upgrade at 6041 with error 1071, having changed nothing (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 the suspended account's Releases API on GitHub, which has answered 404 since 18 September 2026 and still does, 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.

Since 6.3.6 the unit the helper stops and, for an AppImage, the image that verifies the new one come from its own command line (--unit, --image), never from the request; after the stop it checks that the unit is down and that nothing of the service account still runs, and it verifies, reads and installs only copies of the package and the rollback image that it made after the stop under /var/tmp. Before installing it reads the database's schema with hmailserver --schema-version - where systemd runs the machine, in the unit's own sandbox, as the unit's User=; where systemd will not, the version is not read, never outside the sandbox - and puts the earlier version back only onto the database it left (Upgrading Guide, On Linux the live update holds to the same rule). A server under a unit of another name needs --unit in a drop-in for hmailserver-update.service; an AppImage under the path unit needs --image there and 6.3.6's script copied to /usr/lib/hmailserver/. apt-get runs quietly. The helper opens the Updates directory, and makes and opens its log and its outcome, while the server is confirmed stopped, gives them to the service account through the open files before it starts the server, writes them through those files alone, and renames each into place through the open directory, which replaces a symbolic link and never follows it; 6.3.4 and 6.3.5 wrote, chowned and renamed them by name after the restart. An update is applied by the helper of the version it replaces, so 6.3.6's own helper applies 6.3.7, for the generic .deb and .rpm of either architecture and both AppImages, each finding its own file in the feed by name. On 6.3.4 or 6.3.5 install 6.3.7 with the package manager rather than by the live update, whose apply step there is the one 6.3.6 fixed, or systemctl disable --now hmailserver-update.path until then (item 24 of 6.3.6's "What changes on upgrade"). A server installed from one of 6.3.6's per-distribution packages is offered the generic package, which apt refuses on Debian 12 and dnf on Enterprise Linux 9: upgrade it with its package manager and its own release's package of 6.3.7 (Upgrading hMailServer 18.4f).

A request the helper refuses before it stops anything - an AppImage under the path unit with no --image, a line that fails a check - no longer leaves the server refusing connections for ten minutes: since 6.3.6 the server asks systemd about the helper's unit every five seconds and leaves the drain once it has ended with no outcome written (HM7151, naming the unit and its exit status).

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 suspended the project's account on 18 September 2026. The Compose file and the chart now name registry.gitlab.com/hmailserver/hmailserver, the project's GitLab container registry, where each release's pipeline pushes and signs its image since 6.3.6 - 6.3.7's is registry.gitlab.com/hmailserver/hmailserver:6.3.7, also tagged 6.3 and latest (Running in a Container). On 18 October 2026 the image was copied to ghcr.io/rainbowwizard-coder/hmailserver, for the project's repository on GitHub, GitLab's mirror, which the owner made public on 20 October 2026, and on 21 October 2026 to Docker Hub, as docker.io/progressiverobot/hmailserver, by its digest; each pulls without signing in, and cosign verify of it passes with the identity and issuer ContainerImage.md gives. Nothing here pulls from either copy.

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.

Since 6.3.6 what the auto-ban put below the server is lifted after a stop too. At every start and reinitialise, before the first listener opens, the packaged hook is told reset and then every ban in force - wherever it is installed, whether AutoBanFirewall is on or off - and again when AutoBanFirewall is switched off; its iptables calls wait for the xtables lock. AutoBanCommand's account is kept in autoban-command.ledger in the temp folder, under the SHA-256 of the command line and never its text, and at a start the command is told unban for each address it was left holding that is not banned now; a command replaced or cleared is never run again, and the addresses it was told to ban and not to un-ban are named in the error log (HM6523). A command that starts the server short of its listeners (--sample-domain, --apply-environment, --apply-profile) does none of this.

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,811 tests in 6.3.4 (2,302 in 6.3.3)
AArch64 on hardware CI built and installed the packages on an ARM runner until GitHub became unavailable on 18 September 2026; 6.3.4 and 6.3.5 had no AArch64 package or AppImage at all; 6.3.6 and 6.3.7 carry them again, cross-compiled and tested under emulation on the project's own runners, and nobody has installed one on ARM hardware by hand yet
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; it passed in master's first full pipeline, 2886389460, on 27 September 2026.

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