Skip to content

Upgrading hMailServer

chrisholloway5 edited this page Sep 8, 2026 · 3 revisions

Upgrading hMailServer

This page began as a chapter of the 6.2.10 manual and has been corrected for 6.2.28. The Control Panel's pages are grouped differently now, so the paths below use today's groups; the TLS ports 465, 993 and 995 exist only after you create them on the TCP/IP ports page (a fresh install seeds 25, 587, 110 and 143); and everything added since 6.2.10 is in Changes-Since-6210. Where a value here disagrees with the Settings Reference, which is generated from the code, the reference is right.

18.1 How upgrades work

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

Two things change during an upgrade:

  1. Program files in C:\Program Files\hMailServer\Bin are replaced.
  2. The database schema is upgraded by DBUpdater, which the installer runs automatically. Current schema version: 6031.

Your data directory (the messages) and hMailServer.INI are left alone.

What the installer actually does, in order

flowchart TD
    A["Setup starts"] --> B{"Is the hMailServer<br/>service running?"}
    B -- yes --> C["Stop it and wait<br/>ShutdownDrainSeconds plus 30 s"]
    B -- no --> D
    C --> E{"Reached STOPPED<br/>inside the budget?"}
    E -- no --> F["Refuse to go past the Ready page.<br/>A file copy over a running service leaves<br/>the OLD binary against the NEW schema"]
    E -- yes --> D["Move a pre-5.x hMailServer.ini<br/>out of the Windows directory into Bin"]
    D --> G["Copy the program files"]
    G --> H["Install the bundled .NET 10 Desktop Runtime,<br/>but only if it is missing"]
    H --> I["Register the service"]
    I --> J["Run DBSetupQuick, which drives DBUpdater"]
    J --> K{"Exit code 0?"}
    K -- no --> L["Dialog naming the exit code,<br/>then the install FAILS with a non-zero exit code"]
    K -- yes --> M["net start hMailServer"]
    M --> N{"Is the service running?"}
    N -- no --> O["Dialog: check hMailServer.log<br/>and the ERROR log"]
    N -- yes --> P["Completed"]
Loading

Two of those boxes are worth reading properly.

The service stop is bounded, and a failure to stop aborts the upgrade. The budget is ShutdownDrainSeconds plus 30 seconds, derived from your own setting rather than guessed, because that setting deliberately holds the stop open while sessions finish. If the service will not stop, setup refuses to go past the Ready page - because a file copy over a running service offers Retry/Ignore, and choosing Ignore leaves the old binary in place while the schema moves to the new version, after which the server refuses to start at all.

A failed database upgrade now fails the installation. The exit code of DBSetupQuick is checked, a dialog names it, the service start is still attempted, and then setup raises and finishes with a non-zero exit code. A scripted deployment can tell. (This is a change: in earlier releases the dialog was the only signal and the wizard still reported success.)

18.2 The upgrade path

The database upgrade chain is continuous from every earlier hMailServer release, on MySQL, MS SQL, PostgreSQL and SQL CE. You do not need to step through intermediate versions:

flowchart LR
    A["Schema 0<br/>hMailServer 1.0"] --> B["1100 - 3402<br/>hMailServer 1.x to 3.x"]
    B --> C["4000 - 4402<br/>hMailServer 4.x"]
    C --> D["5000 - 5708<br/>hMailServer 5.x<br/>PostgreSQL and SQL CE<br/>join at 5001"]
    D --> E["6001 - 6005<br/>hMailServer 6.0 to 6.2.18"]
    E --> F["6006 - 6011<br/>6.2.19"]
    F --> G["6012 - 6022<br/>the 6.2.22 pre-releases"]
    G --> H["6023 - 6025<br/>6.2.23-alpha1, 6.2.24"]
    H --> I["6026 - 6030<br/>6.2.25"]
    I --> J["6031<br/>6.2.27, and 6.2.28 today"]
Loading

DBUpdater walks every intermediate step for you, one at a time, in one transaction. It builds the whole path first and checks that every script file on it exists before it runs the first one, so a missing dialect script is named and refused rather than discovered halfway.

Coming from Its schema Path Notes
6.2.27 6031 Run the current installer No schema change at all
6.2.25 / 6.2.26 6030 Run the current installer One step, 6030 to 6031
6.2.24 6025 Run the current installer Six steps. One of them reads every child table - see 18.2a
6.2.19 - 6.2.21 6011 Run the current installer Twenty steps
6.2.10 - 6.2.18 6005 Run the current installer Twenty-six steps
6.0 / 6.1 read hm_dbversion Run the current installer Schema upgraded automatically
5.6 / 5.7 read hm_dbversion Run the current installer Schema upgraded automatically. Read 18.5 - the GUI has changed
5.3 - 5.5 read hm_dbversion Run the current installer As above. Very old installs: back up first and test the restore
Original hMailServer (unmaintained) any Run the current installer This fork is a drop-in successor. It did not branch the schema; it extended it

How far back the chain reaches depends on your backend. The steps are registered once for all four, but the SQL files are per-dialect and they do not all exist: MySQL and MS SQL ship every step back to schema 0; PostgreSQL and SQL Server Compact ship every step from 5001 onwards. In practice that is not a gap, because neither was a supported backend before hMailServer 5, so no older database exists on either.

18.2a The steps in this fork, and which of them cost time

Everything from 6005 onwards, with the operational note that matters. The full "what changed" table is in Changes-Since-6210; this one answers "how long will it take and what should I watch".

Step Shipped in What it does Plan for it
6005 to 6006 6.2.19 hm_imapfolders.folderspecialuse Instant
6006 to 6007 6.2.19 Two hm_domains columns for DKIM key rotation Instant
6007 to 6008 6.2.19 Two hm_tcpipports columns for client certificates Instant
6008 to 6009 6.2.19 hm_sslcertificates.sslprivatekeypassword Instant
6009 to 6010 6.2.19 Two hm_settings rows for ARC Instant
6010 to 6011 6.2.19 New table hm_inisettings Instant
6011 to 6012 6.2.22-pre1 Widens hm_rule_criterias.criteriamatchvalue to 2000; adds hm_accounts.accountvacationbegindate Fast; rule criteria are few
6012 to 6013 pre1 hm_messages.messagesavedate, then UPDATE every message row to seed it Scales with mailbox size. On a store of millions of messages this is the first slow step
6013 to 6014 pre1 New table hm_imap_metadata Instant
6014 to 6015 pre2 hm_messages.messageemailid, then UPDATE every message row Scales with mailbox size, same as 6013
6015 to 6016 pre3 New table hm_apppasswords Instant
6016 to 6017 pre3 hm_accounts.accounttotpsecret Instant
6017 to 6018 pre3 New table hm_quarantine Instant
6018 to 6019 pre3 hm_accounts.accountpasswordchanged plus an update of every account; new table hm_passwordhistory Fast; accounts are few
6019 to 6020 pre6 New table hm_messagetrace Instant
6020 to 6021 pre6 Six hm_domains.domainrelay* columns Instant
6021 to 6022 pre6 Two hm_servermessages rows for quota warnings Instant
6022 to 6023 6.2.23-alpha1 New tables hm_messageindexterms, hm_messageindexstate Instant; they stay empty until full-text indexing is switched on
6023 to 6024 alpha1 Six hm_domains.domainvacation* columns; new table hm_blocked_senders Instant
6024 to 6025 alpha1 hm_messages.messageflags tinyint to smallint, plus three hm_accounts and two hm_distributionlists columns A table rewrite of hm_messages on MS SQL, SQL CE and MySQL. PostgreSQL was already smallint. This is the single most expensive step in the chain on a large store
6025 to 6026 6.2.25 One hm_settings row Instant
6026 to 6027 6.2.25 Retention-day columns on domains and accounts Instant
6027 to 6028 6.2.25 New table hm_metricsamples Instant
6028 to 6029 6.2.25 New table hm_archiveindex Instant; only copies made from then on are indexed
6029 to 6030 6.2.25 Seventeen FOREIGN KEY ... ON DELETE CASCADE constraints, with orphan rows deleted first, children before parents; MySQL also gets ENGINE=InnoDB on every table involved Reads every child table once. Plan for it the way you would plan an index build
6030 to 6031 6.2.27 hm_fetchaccounts.famirrorfolders Instant

So: from 6.2.24 or later, the upgrade is quick whatever the size of your store. From 6.2.21 or earlier it crosses 6012, 6014 and 6024, all three of which touch every message row - which is the reason to do it in a maintenance window rather than at lunchtime.

18.2b What DBUpdater does with each step

flowchart TD
    A["Read hm_dbversion"] --> B{"Newer than<br/>this build needs?"}
    B -- yes --> C["Refuse: downgrading a database<br/>is not supported"]
    B -- no --> D{"Equal?"}
    D -- yes --> E["Nothing to do; exit code 0"]
    D -- no --> F["Build the whole path,<br/>checking every script file exists"]
    F --> G["BEGIN TRANSACTION"]
    G --> H["Run the prerequisites script<br/>MS SQL and PostgreSQL only"]
    H --> I["For each step: EnsurePrerequisites,<br/>then run Upgrade&lt;from&gt;to&lt;to&gt;&lt;dialect&gt;.sql"]
    I --> J["Run the schema PROBES<br/>on the same connection,<br/>so they see the uncommitted DDL"]
    J --> K{"Every probe satisfied?"}
    K -- no --> L["ROLLBACK where the backend can,<br/>and report which step"]
    K -- yes --> M["COMMIT"]
    M --> N["Read hm_dbversion back"]
    N --> O{"Equals the required version?"}
    O -- no --> P["Report: the scripts ran without error<br/>but the database is at the wrong version"]
    O -- yes --> Q["Success; exit code 0"]
Loading

The probe step exists because of a specific trap. Three of the four dialects mark their ALTER TABLE statements [IGNORE-ERRORS] so that re-running a step is harmless - but the update hm_dbversion statement in the same script carries no marker and always runs. A marked ALTER that failed for a real reason would therefore stamp the new version onto a database that does not have the column. Each such step now has a probe that must be satisfied before the commit, and build/check-schema-versions.ps1 proves that every step has one.

DBUpdater's exit codes, which is what a scripted install reads:

Code Meaning
0 Success, or nothing to do
1 The upgrade failed
2 A configuration error - unknown database type, missing script directory, or no path to the required version
3 Authentication was cancelled, or under /silent could not be satisfied

18.3 Before you upgrade

  1. Back up. Database, data directory and hMailServer.INI. See chapter 15. The database half is the one that makes rollback possible; the data directory is the half that cannot be reconstructed from anything else.

  2. Write down two numbers. The server version (Help → About, or the Control Panel's About page) and the schema version. The Control Panel shows the schema on Server status; when the server will not start, read it directly:

    select value from hm_dbversion;

    hm_dbversion is a single-column, single-row table of its own - not a row in the settings table. If you ever open an issue, those two numbers are the first two questions.

  3. Estimate the outage from 18.2a. From 6.2.24 or later, a minute or two. From 6.2.21 or earlier on a large store, longer, because three steps touch every message row.

  4. Pick a quiet time. Senders retry, so nothing is lost, but users will notice.

  5. Know your administrator password. A silent upgrade needs it on the command line - see 18.4a.

18.4 Doing it

  1. Download hMailServer-6.2.28-x64.exe.
  2. Run it as administrator.
  3. The installer detects the existing installation and offers to upgrade. Accept.
  4. Keep your existing database settings when asked.
  5. Let it finish, then confirm the service is running.
  6. Open the Control Panel and check About shows the version you just installed (6.2.28 today).
  7. Send a test message in and out, and read it back over IMAP or POP3. A service that starts is not the same as a server that delivers.
  8. Read the ERROR log for the window around the upgrade. An empty ERROR log is the signal you want.

18.4a Unattended

$p = Start-Process -Wait -PassThru -FilePath .\hMailServer-6.2.28-x64.exe `
     -ArgumentList '/silent', '/SUPPRESSMSGBOXES', '/adminpassword=<password>'
if ($p.ExitCode -ne 0) { throw "hMailServer upgrade failed with exit code $($p.ExitCode)" }
  • With an administrator password set, /adminpassword= is required. DBUpdater has to authenticate before it can move the schema. Without it the database upgrade fails and the installer reports the non-zero exit code rather than waiting - which is the fix for a real defect: until 6.2.23 a silent upgrade of an installation with a password set hung, on a modal password dialog nobody was there to answer.
  • With an empty administrator password, a silent upgrade works with no extra argument.
  • The password must be at least five characters, or setup refuses it up front.
  • (New in 6.2.28: the live update's helper passes a single-use /upgradetoken=<hex> in place of the password, so an unattended self-update never handles the administrator password at all.)

18.5 Things that changed, and what to do about them

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

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

  • The navigation tree deliberately mirrors the old Administrator's layout, so muscle memory mostly transfers.
  • Your TOTP secret carries over - the Control Panel uses the same one.
  • The Control Panel needs the .NET 10 Desktop Runtime, which the installer installs silently if missing.

Coming from before hMailServer 5, hMailServer.ini moves. It used to live in the Windows directory; the installer copies it to Bin\hMailServer.ini, leaves a .old copy behind and deletes the original. The server reads only the one in Bin.

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

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

The .NET runtime moved from 8 to 10 in the first release after 6.2.18. The installer bundles windowsdesktop-runtime-10.0-win-x64.exe and installs it /quiet /norestart, only when it is missing. Nothing about the server itself changed - it is native code with no .NET dependency - so an upgrade that fails to install the runtime still leaves you with a working mail server, just without the Control Panel and the setup tools.

18.6 If it goes wrong

The server does not paper over a half-finished upgrade. On startup it compares the database's version with the version it requires and refuses to run if they differ, in either direction, reporting HM5011 with both numbers appended:

Message in the ERROR log What it means What to do
The database is too old for this version of hMailServer. Please run hMailServer Database updater (DBUpdater.exe) to upgrade it. Database version: N, Required database version: M The schema did not get all the way up Run DBUpdater.exe from the installation's Bin folder. It picks up from whatever version the database actually reports, so a retry after a partial upgrade resumes rather than restarting. If it fails again, the log names the step it stopped on - that number is the useful thing to put in an issue
The database is too new for this version of hMailServer. Please upgrade hMailServer. Database version: N, Required database version: M You are running an older server against a newer schema - usually an old binary left in place, or a reinstall of a previous release Install the matching or newer server. Do not try to downgrade the database
Database version could not be detected. (HM5010) hm_dbversion could not be read at all Check the connection details in [Database] and that the login can read the table

DBSetupQuick.exe and DBUpdater.exe do the same job; the installer drives the first, and the error message names the second because that is the one you run by hand.

Rolling back

There is no downgrade path, and this is the one genuine sharp edge. The schema upgrade is one-way: an older server refuses to run against a newer dbversion rather than misinterpret it, which is correct - and means "just reinstall the old version" does not work on its own.

  1. Uninstall the version you just installed.
  2. Install your previous version.
  3. Restore the database from your backup. This is the essential step.
  4. Restore hMailServer.INI if you changed it.

The data directory does not normally need restoring - message files are not rewritten by an upgrade - but note the consequence: mail that arrived after the backup exists on disk with no metadata row, and will be invisible. That is what rollback costs, and it is why the cutover is worth doing in a quiet window.

This is why 18.3 step 1 is not optional. Without a database backup there is no rollback.

18.7 Staying up to date

  • Watch the repository on GitHub to be notified of releases.
  • Every release lists exactly what changed and why, and carries SBOMs.
  • Security fixes are called out explicitly in the release notes.
  • Let the server check for you (new in 6.2.28). With UpdateCheckEnabled=1 in hMailServer.INI the server reads the project's release feed at startup and then every UpdateCheckHours (default 24), compares the newest release on UpdateChannel (stable, the default, or prerelease) with its own version, and reports a newer one in the application log, on the Control Panel's Server status page, in Status.UpdateState / Status.AvailableVersion over COM and in GET /api/v1/update. The check is off by default; Status.CheckForUpdate and POST /api/v1/update/check run one on demand whether or not it is on. Every setting the feature reads has an editor on the Updates card of the Control Panel's API & monitoring page.
  • If this machine reaches the web through a forward proxy, set HttpProxy=<host>:<port> (or [ipv6]:<port>) in the [Settings] section of hMailServer.INI - empty, the default, means a direct connection. The update check, the installer download, and the JWKS and token-introspection fetches all go through it; an https target is reached with CONNECT and the TLS handshake runs inside the tunnel, so the certificate is verified exactly as it would be on a direct connection, and plain http is fetched by putting the absolute URL in the request line. A proxy that refuses is reported by name: The proxy <proxy> refused CONNECT to <host:port>: followed by the proxy's own status line. A value with no port is an error, and proxy credentials are not supported. The editor sits beside the feed URL on the Updates card.

For the mechanism behind all of this - what the chain does, why rollback is what it is, and how to move to a different machine or a different backend - see Upgrading Guide and Migrating the Database Backend.


Clone this wiki locally