Repository navigation
Upgrading Guide
Source of truth: hmailserver/docs/Upgrading.md, kept in the repository and copied here. Re-copied from master 92b8f9f63 on 26 September 2026, and brought up the same day to the document as the tag v6.3.4 (51e2dbda0), the release, has it: the installer's
HM_ADMIN_PASSWORD, the named lock that serialises the schema steps on MySQL and MariaDB, and the probe that stops an upgrade from 6.3.3 at step 6041 on too old a MySQL or MariaDB, with the repair of a database an earlier build left half done. Re-copied on 5 October 2026 from the document as the tag v6.3.6 (e3c448aa6) has it (Upgrading.md at v6.3.6): the schema steps 6106, 6107 and 6108 and what an upgrade from 6.3.5 needs; the installer's exit codes 10 and 11, and the rule that the earlier version is put back only onto a database as it left it, on Windows and on Linux (hmailserver --schema-version); the token the live update hands a 6.3.6 installer in its environment; and the live update's five trust settings, read from the file alone. Re-copied on 18 October 2026 from the document as the tag v6.3.7 (31d656c0c), the release, tagged and published on 17 October 2026, has it (Upgrading.md at v6.3.7): a server on 6.3.4 or 6.3.5 underNT SERVICE\hMailServeris brought to the newest release from the release page, once, and on Windows and on Linux an update from 6.3.5 or earlier, to 6.3.6 or a later release, is made as before, the rules for an update holding for one from 6.3.6 on, as this copy said in those three places from 17 October 2026; the route from one database backend to another is a backup, a new database on the new backend and a restore into it, as Migrating the Database Backend describes, where v6.3.6's text said there was no first-class tool for it and this copy pointed there; and setup does not update a .NET 10 Desktop Runtime the machine already has, which Microsoft Update services. The pointer to Upgrading hMailServer after the opening, the section What one run looks like, with its flowchart and the exit codes, the bullet NothMailServer.iniunder What the upgrade touches, the section How a backend's own behaviour changes the failure mode and its backends table, the note that An upgrade without a window is new in 6.3.4, the installer's refusals and the notes on the two numbersHM5011carries and onHM5010under If the upgrade fails, the paragraph on rolling back across schema 6042 under Rolling back, the notes under Special cases on where the[Settings]values come from on a new machine, on the database password and on an installation from before hMailServer 5, the notes for contributors and the corrections under Verified against the code are this wiki's own additions. Since 20 October 2026 the backends table says what a failed step leaves on each backend as the document says it, asmasterhas it from batch50, 0c0b6bae7, where it had SQL Server Compact commit DDL as it executes. Since 21 October 2026 three of them say what a file the service may not write changes, as The Service Account has it, where they said it whatever the account: the bullet NothMailServer.inithat an edit to the section is not used, its line put back only where the service may write the file, where it said that the edit was undone; the paragraph on rolling back across schema 6042 that the file the newer server left holds a full copy of the stored settings only where the service could write it; and the note on a new machine that the restored values are written into the new file at the first start only where the service may write it. Re-copied on 22 October 2026 from the document asmasterhas it from batch52, 72be22b4e, onmastersince 21 October 2026 (Upgrading.md at 72be22b4e), which changes it in one place: Where you can upgrade from says that the chain includes the databases of the original hMailServer up to schema 5708 - every 5.6 release's, 5601, and those of its development builds of 5.7 - from which this fork extended the schema by its own steps, and that the original's published 5.7 builds are not on it: in August 2026 the original went on from 5708 by steps of its own, 5709 to 5711, which DBUpdater has no step from, so this fork's installer is not run over an installation of the original's 5.7, whose mail moves to this server over IMAP instead; and it points at ForkAndOriginal.md for what else differs between the two projects, where it said that this fork did not branch the schema and that its version was a superset. Re-copied on 23 October 2026 from the document asmasterhas it from batch53, 6fb8164c6, onmastersince 22 October 2026 (Upgrading.md at 6fb8164c6), which changes it in one place: Where you can upgrade from says what setup up to 6.3.7 did over an installation of the original hMailServer's 5.7 - over one whose settings are in itsBinfolder it ran DBUpdater only after it had replaced the program files, and this server then refused the database as too old (HM5011); over one that the original's 5.7.0 build 3013 or later made new, which keeps its settings and data inC:\ProgramData\hMailServer, it found no settings file, took it for a new installation and set up a new, empty database beside the original's - and that from the release after 6.3.7 the installer refuses an installation of the original's 5.7 before it changes anything, the installers of 6.3.7 and earlier not looking, so that they must not be run over one; how setup decides, from the version of the earlier installation'shMailServer.exe, the database's version as the running service answers it, the settings file the installed server reads and the original's upgrade scripts past5708in itsDBScriptsfolder; what the refusal says and where it is written; and that the decision ishmailserver/installation/OriginalHMailServer.iss, whose self-test the Windows package jobs run before they build the installer, where it said that setup ran DBUpdater after it had replaced the program files and that this fork's installer was not to be run over an installation of the original's 5.7. The list under If the upgrade fails of what else the installer refuses, this wiki's own, has a third point, that from the release after 6.3.7 it will not run over an installation of the original's 5.7, where it had two. Re-copied on 24 October 2026 from the document asmasterhas it from batch54, 9336fdd59, onmastersince 24 October 2026 (Upgrading.md at 9336fdd59), which changes it in one place: Where you can upgrade from says that the log the Chocolatey package has setup write (/LOG) and the file with the refusal's words are in Chocolatey's cache folder, which its CLI makes setup'sTEMP, where it said only that the package passes/LOGand shows that file's words when the install fails. The document's links to the repository's other documents point at them as 9336fdd59 has them, or at this wiki's copy where there is one.
How to move an existing hMailServer installation to this fork, or from one release of it to the next, and what the database upgrade actually does. Written because the upgrade is the one operation where an unpleasant surprise costs you mail rather than time.
This is the mechanism. For the step-by-step procedure, the per-step cost table and the decision tree, see Upgrading hMailServer.
Run the new installer over the existing installation. It preserves your configuration and your mail, stops the service, upgrades the database schema, and starts the service again. There is no separate migration step and no export and re-import.
Take a backup first anyway. Not because the upgrade is unreliable - the chain below has run continuously since hMailServer 4 - but because the one thing an upgrade cannot do is undo itself. See Rolling back.
Sealed secrets are off after an upgrade until you turn them on. From schema 6103
each account's second factor and the [Settings] secrets can be sealed in the
database under the installation's stored-secrets key, but an upgrade makes no key:
the server goes on storing them as written, says so in the application log at each
start, and the setup checklist offers the step. To turn sealing on, from an elevated
prompt on one server (sudo hmailserver ... on Linux):
.\hMailServer.exe --create-stored-secrets-key
.\hMailServer.exe --export-stored-secrets-key D:\Escrow\hmailserver-stored-secrets.key
Restart-Service hMailServerthen --import-stored-secrets-key the same file on every other server that shares the
database, and restart each. Keep the exported file with the database backups, stored
apart from them: from then on a database-level backup restored without it opens none
of those secrets (Secrets at Rest). A new installation's installer
makes the key itself and says to export it.
The upgrade chain is continuous, from schema version 0 through to whatever
version this build requires, applied one step at a time. A database at any
intermediate version is brought forward step by step, so there is no "you must
first upgrade to 5.x" hop to plan around.
The step count and the target version are deliberately not written here.
They have gone stale on this page four times now, and a version number is
exactly the sort of fact a reader trusts without checking. Read them from the
tree instead: REQUIRED_DB_VERSION in
hmailserver/source/Server/Common/Application/Constants.h is the version this
build demands, and build\check-schema-versions.ps1 prints both numbers and
proves the chain reaches one in exactly the other.
That includes databases created by the original hMailServer up to schema
5708: every 5.6 release's, 5601 - the version each of the original's 5.6
branches requires - and those of its development builds of 5.7 up to 5708. This
fork extended the schema from there, with the same mechanism, by its own steps
from 5708 to 6001 and on.
The original's published 5.7 builds are not on the chain. In August 2026 the
original went on from 5708 by steps of its own: 5709 (special-use folders),
5710 (the password-hashing settings) and 5711 (DMARC). Its two pre-release
builds of 5.7.0 of August 2026, 2788 and 2843, require 5709, and 5.7.0 build
3013, 5.7.1 and 5.7.2 require 5711. DBUpdater has no step from any of the
three, and stops before it changes anything: "Upgrade path not found. No upgrade
script is registered for database version 5711". Over an installation whose
settings are in its Bin folder - the builds 2788 and 2843, which have no other
layout, or a later 5.7 installed over one of them or over a release up to 5.6,
whose layout its installer keeps - setup ran it only after it had replaced the
program files, and this server then refused the database as too old (HM5011).
Over one that the original's 5.7.0 build 3013 or later made new, which keeps its
settings and data in C:\ProgramData\hMailServer - read first by the original's
server, and not at all by this fork's setup up to 6.3.7 - setup found no
settings file, took it for a new installation, and set up a new, empty database
beside the original's.
So the installer refuses an installation of the original's 5.7, before it
changes anything - from the release after 6.3.7; the installers of 6.3.7 and
earlier do not look, and must not be run over one. Setup's first step, before a
page is shown, reads the version of the earlier installation's hMailServer.exe.
Before 6 - that is, not this fork's - it asks the running hMailServer service for
the database's version: a version past 5708 and before 6001, this fork's first
after it, is refused, and 5708 or older - a release of the original up to 5.6,
or one of its 5.7 development builds - is upgraded as any other. Where the service
is not running, or does not answer, the settings file the installed server reads
is read first: for the original's 5.7.0 build 3013 and later,
C:\ProgramData\hMailServer\hMailServer.ini when it exists - they keep a new
installation's there and read it before the one in Bin, and this fork never
writes it - and otherwise, and for the original's earlier builds (2788, 2843, its
releases up to 5.6 and its 5.7 development builds), which read the one in Bin
alone, the hMailServer.ini in the installation's Bin folder. The original's
CI builds 5.7.0.2948, 2951 and 2957, of the branch that brought that layout, and
5.7.0.2958 to 3012 read the ProgramData file first as well, but setup takes them
as its earlier builds. So a file in ProgramData that the original's uninstaller
left behind does not decide for a
build that never reads it. A [Database]
PasswordEncryption of 6, the database's password kept with DPAPI, is this
fork's alone. This fork writes it, from 6.2.0 on and unless
ProtectStoredSecretsWithDPAPI is off, whenever its database setup makes a
database or connects to one, the built-in one among them; the original writes 1.
Such a database is this fork's own, at 6001 or later, as the original's 5.7
installer leaves it when it is run over an installation of this fork. The
original's service cannot decrypt that password, so it does not answer the
version (with an empty password, which this fork keeps as 6 too, it connects and
answers 6001 or later, which is not refused either), and setup does not refuse
it: it puts this fork's programs back, and this fork's server upgrades its own
database. Otherwise the original's own upgrade scripts past 5708 in the
installation's DBScripts folder (Upgrade5708to5709*.sql) decide, and the
refusal then says that an installation whose database is at 5708 or older, or is
this fork's own at 6001 or later, is upgraded once its service runs and setup is
run again. This fork's own reaches that refusal only where the settings file the
installed server reads keeps the password as the original can read it -
PasswordEncryption 1 (or 0), as this fork's own keeps it for an
installation upgraded from the original, one set up before 6.2.0, or one with
ProtectStoredSecretsWithDPAPI off - and the original's service, started, then
connects, does not start its servers on a database too new for it, and still
answers the version, which is not refused. The version is
not held to 5.7, because a build of the original's 5.7 made from its source
without its build script reports itself as 1.0.0: every 5.7 tag of the original
commits that placeholder. The refusal says what it found and that nothing has been
changed; a silent setup ends with exit code 1, its log (/LOG) has the refusal on
a line that begins "Refused:", and with or without a log setup writes it to
hMailServer-setup-refused.txt in the temporary folder (%TEMP%) of the account
it runs as, which a later setup that is not refused deletes. The Chocolatey
package passes /LOG and shows that file's words when the install fails (the
log and the file are in Chocolatey's cache folder, which its CLI makes
setup's TEMP: PackageManagers.md), and
winget writes the log where winget install --log <file> says
(PackageManagers.md). Installations of this fork, whose programs are 6 and later,
are not looked at past their hMailServer.exe's version. The decision is
hmailserver/installation/OriginalHMailServer.iss, and the Windows package jobs
run its self-test before they build the installer
(PackagesWindows.md).
A server of the original's 5.7 moves its mail to this one over IMAP instead (Migration.md, "From the original hMailServer"). ForkAndOriginal.md says what else differs between the two projects.
How far back the chain reaches depends on your backend, and this is the one qualification worth knowing before you start. The steps are registered once, for every backend, but the SQL scripts they run are per-dialect and they do not all exist:
| Backend | Steps that ship | Reaches back to |
|---|---|---|
| MySQL / MariaDB | every step | schema 0 (hMailServer 1.0) |
| Microsoft SQL Server | every step | schema 0 (hMailServer 1.0) |
| PostgreSQL | every step from 5001 onwards |
schema 5001
|
| SQL Server Compact (internal) | every step from 5001 onwards |
schema 5001
|
In practice that is not a gap, because neither PostgreSQL nor the internal SQL CE
database was a supported backend before hMailServer 5, so no database older than
5001 exists on either. But it is why the answer is "any version of your
backend" rather than "any version". If a step's script is missing, DBUpdater says
so by name and stops before touching anything - it checks that every file on the
path exists before it runs the first one.
flowchart TD
A["Read hm_dbversion"] --> B{"Newer than this build requires?"}
B -- yes --> C["Refuse by name: a database from a newer<br/>version, and downgrading is not supported"]
B -- no --> D{"Equal?"}
D -- yes --> E["Nothing to do, exit code 0"]
D -- no --> F["Walk the registered steps from the current<br/>version, bounded by the number registered,<br/>and check every script file exists"]
F -- "no step is registered for this version" --> G["Refuse by name"]
F -- "a script file is missing" --> H["Refuse, naming the file"]
F --> I["BEGIN TRANSACTION"]
I --> J["Prerequisites script<br/>MS SQL and PostgreSQL only"]
J --> K["For each step in order:<br/>EnsurePrerequisites, then that step's<br/>script for this dialect"]
K -- "a statement throws" --> L["ROLLBACK where the backend can,<br/>show the script and the error"]
K --> M["Run the schema probes on the SAME<br/>connection, so they see uncommitted DDL"]
M -- "a probe is not satisfied" --> N["ROLLBACK where the backend can,<br/>naming the object that is missing"]
M --> O["COMMIT"]
O --> P["Read hm_dbversion back"]
P -- "not the required version" --> Q["Report: the scripts ran without error,<br/>but the database is at the wrong version.<br/>Nothing can be rolled back from here"]
P --> R["Delete the stale ERROR log, exit code 0"]
Three parts of that are worth understanding rather than skimming.
The whole path is validated before the first statement runs. Building the path also proves each file exists; a missing dialect script therefore stops the upgrade while the database is still untouched, not halfway through.
The probes exist 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. The data-access layer matches that marker anywhere in the statement and
then discards every error - while 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 carries a probe that must be satisfied before the commit,
and the probes run on the transaction's own connection so they can still see
uncommitted DDL. build\check-schema-versions.ps1 is what makes sure a probe exists
to run.
The version is read back rather than assumed. hm_dbversion is written by a
statement of its own, in the same script as the schema change but independent of
whether it worked, so the last thing DBUpdater does is re-query it. Nothing can be
rolled back at that point, so a mismatch is reported and the ERROR log is
deliberately left in place rather than cleaned up.
Its exit codes, which is what a scripted install actually reads:
| Code | Meaning |
|---|---|
| 0 | Success, or the database was already at the required version |
| 1 | The upgrade failed |
| 2 | A configuration error: unknown database type, no script directory, or no registered path to the required version |
| 3 | Authentication was cancelled - or, under /silent, could not be satisfied |
- The schema only. Messages live on disk, not in the database - the database holds accounts, domains, settings and message metadata. No upgrade step rewrites message files.
- Every supported backend. Separate script sets exist for MySQL/MariaDB, MS SQL Server, PostgreSQL and the embedded SQL CE. The installer picks the set matching your configured backend.
-
Nothing is deleted that carries live data. One table has been dropped in the
chain's history and it is worth knowing about because a schema comparison will
show it:
hm_adsynchronization, created by step3402 -> 4000(which ships for MS SQL and MySQL only) and dropped by step5004 -> 5005on all four dialects. NoCreateTablesscript creates it on any backend, so a database that came through the chain and a freshly created one agree about it afterwards. If you do find a table a fresh database does not have, it came from somewhere other than this chain. -
Not
hMailServer.ini- with one change since 6.3.4. DBUpdater never reads or writes the file's settings. Since 6.3.4, schema 6042 makes the database the store for the[Settings]section, and the move itself is done by the server at its first start afterwards: every key in the section that the database has never held is taken into it, and from then on the section is a copy the server rewrites where it may write the file, and an edit to it is not used at the next start (HM5804), its line put back where the service may write the file - underNT SERVICE\hMailServer, to which the file is read-only, the edit stays in the file (The Service Account). A value typed into the file after the old server last started, and never picked up by it, is one of those unused edits - restart the old server once before upgrading. The rules are in Settings Overview.
An upgrade keeps the account the service runs as - LocalSystem, for an
installation that began before 25 September 2026 - unless the installer's
Service account page is answered otherwise; a new installation runs it as the
virtual account NT SERVICE\hMailServer. The page offers the way each side, and
/serviceaccount=virtual|localsystem|keep does the same for a silent upgrade.
A move is made after the database is upgraded, and only once the service's folders
are granted cleanly: an attended installation asks about any folder or key file
the configuration names outside the program folder, a silent one leaves the
service on LocalSystem when there is one. On SQL Server with Windows
authentication the installer first gives NT SERVICE\hMailServer a login there, a
user in the database and db_owner, as the administrator running it
(hMailServer.exe --grant-database-login); and a service it has moved is held to
opening its database as that account, and moved back to LocalSystem, with the
statements a SQL Server administrator runs and exit code 10 or 11, when it does
not. Under the virtual account the live
update is checked, downloaded and verified, and an administrator installs it with
Install update in the Control Panel on the server, which asks Windows once and
checks the installer itself, since the service cannot run an installer - from
6.3.6; a server on 6.3.4 or 6.3.5 is brought to the newest release from the release
page, once, as below - and hMailServer.INI is read-only to
the service - the administrator password is then changed with
hMailServer.exe /SetAdministratorPassword. The Service Account
has the rest.
The chain is the same everywhere; what a failed step leaves behind is not.
| Backend | DDL in a transaction | So a failed upgrade leaves |
|---|---|---|
| PostgreSQL | Transactional | The database exactly as it was. This is the best-behaved of the four |
| Microsoft SQL Server | Transactional: DBUpdater runs the whole chain in one transaction | The database as it was |
| MySQL / MariaDB | Commits each step's DDL as it goes | The steps that ran, applied, and perhaps part of the one that failed, the version not moved. Re-running resumes from whatever hm_dbversion now says, and may stop on what that step left (Repairing a database an earlier build left half done) |
| SQL Server Compact | Transactional: DBUpdater runs the whole chain in one transaction, but for a chain from a database older than 6072 that crosses 6107, whose steps before 6107 it commits | The database as it was; on that chain, a failure from 6107 on leaves it at 6106, and setup ends with 11 |
This is why DBUpdater's messages say "rolled back where the backend allows it"
rather than promising a rollback it cannot deliver. On MySQL and MariaDB, which commit
as they go, the recovery is to re-run: the chain resumes from the version the database
actually reports, and [IGNORE-ERRORS] makes a re-applied ALTER harmless.
-
Back up. Both halves, and they are separate things:
- the database - with your backend's own tools (
mysqldump,pg_dump, SQL Server backup). The built-in backup covers this too, but a native dump is what you want if you need to restore into a different server. - the data directory - the message files. This is the part that cannot be reconstructed from anything else.
- the database - with your backend's own tools (
-
Note your current versions. The server version is in the Control Panel. The
schema version is in a table of its own -
select value from hm_dbversion, a single-column, single-row table, not a row in the settings table. (The Control Panel shows it on the Server status page and DBUpdater reads it too; the SQL is here for when the server will not start and neither of them will connect.) Write both down - if you need support, they are the first two questions. - Run the new installer. It stops the service, installs, upgrades the schema, and restarts.
- Check it came up. The service should be running and listening on your configured ports. Then check the ERROR log for the window around the upgrade - an empty ERROR log is the signal you want.
- Send a message to yourself, end to end, and read it back over IMAP or POP3. A service that starts is not the same as a server that delivers.
Everything in this section is new in 6.3.4.
Since 23 September 2026 an upgrade no longer needs a booked window: the server drains before it stops, and it can bring its own schema forward when it starts. What a single node can and cannot promise is written down at the end of this section, because the difference matters when you plan.
The sequence. Whether the live update runs it (POST /api/v1/update/install,
Status.InstallUpdate, or the unattended window) or you do:
-
Drain. The SMTP, IMAP, POP3 and ManageSieve listeners are closed and the delivery queue is paused. A connection attempted from now on is refused at the socket - not accepted and then cut - which every MTA on the internet treats as "try again later" and retries on its own schedule, so no mail is lost and no sender sees a bounce. The sessions already open finish at their own pace; a delivery already talking to a remote finishes, and a queued message not yet started stays in the queue, unlocked.
/readyzanswers503 not ready: draining; the metrics listener, the REST API and the web services stay up, so the Deck, a load balancer's probe and the update helper can all still see the server. The application log saysDrain: listeners closed; N session(s) and M delivery(ies) in flight; ....What a person sees, from the code: a mail client's IMAP, POP3 and SMTP submission are refused and retried by the client, and so is a Sieve client on ManageSieve; the webmail keeps working - it reads mail over the REST listener in-process, and its send (
POST /api/v1/me/messages) writes the message into the delivery queue rather than opening an SMTP session, so a message sent from it during the drain is accepted and waits, unlocked, in the queue until the drain ends; CalDAV and CardDAV are served by the web-services listener and keep answering; the Deck and every/api/v1route keep answering. -
Stop. The service stop finds the doors already closed and waits up to
ShutdownDrainSecondsfor what is still in flight (nothing, usually, by now: a stop after a drain does not wait again for what has already drained), then tears the servers down. WithShutdownDrainSecondsat its default of0the doors still close first, but nothing is waited for - set it on a production server (30 is a reasonable figure; the HA runbook says the same), or a session mid-DATA at the moment of the stop is cut. -
Install. The binaries are replaced. The Windows installer runs DBUpdater as it always has; the
.deband.rpmrunhmailserver --upgrade-databasein their post-install step; the container's entrypoint runs it at every start. -
The schema step, in the service's own start. When the database is older than the binary and
[Database]inhMailServer.inicarriesUpgradeOnStart=1, the service applies the upgrade chain itself as it starts - the same codehmailserver --upgrade-databaseruns, one step at a time, one log line per step (Schema: 6086 -> 6087 (Upgrade6086to6087MSSQLCE.sql)), each step in a transaction where the backend has one - and then continues the start. The lines go to the application log when application logging is on, written as soon as the start has read the settings, since the logger writes nothing before. Without it, or when a step fails, the start is refused withHM5011as before, and the message names the failing step and the version the database is at; the steps before it have been applied and a second start resumes there. The key is in the file, not the settings store, by necessity: it has to be read before the database is at a version this build can open. It is off by default. Nothing is backed up first - Take a backup above is the rule, and this is the step that makes it matter. One node at a time: set it on the node you start first; a second node that starts against the same database reads the new version and does nothing. Two nodes that do start together are serialised step by step - on PostgreSQL and SQL Server by the version row, which each step locks (select ... for update,with (updlock, holdlock)) for the length of its transaction; on MySQL and MariaDB, whose first DDL statement in a step commits the transaction and frees that lock, by a named lock (GET_LOCK('hmailserver_schema_upgrade')) held until the step is over and waited on for up to four hours - so the second blocks, then finds the step done and moves on - but that is the net, not the plan. Until 26 September 2026 MySQL had only the row lock, so the second node ran the same step beside the first and failed on a table or a column already there. A server that refusesGET_LOCKaltogether - MariaDB Galera with wsrep on - still upgrades, with only the row lock, and the upgrade's output says so for each step: there, upgrade from one node at a time. -
Start.
/readyzanswers 200 again once the database probe agrees, and the load balancer routes to the node again.
What is refused, and for how long. From the drain to the moment the new version is listening: the length of the install plus the service's start, which is seconds to a couple of minutes on a single node, spent as refused connections, which senders retry. That is the honest limit of a single node. A rolling restart with no refusal at all needs a second node (section 7 of Roadmap2, the HA runbook's planned failover), and the drain is what that failover now uses.
Maintenance mode without an upgrade. The drain is a state of its own, with
three routes, all server-wide (manage_server to enter and leave, view_configuration
to read; refused to a domain-restricted or read-only key; recorded on the audit
trail as server / drain / entered and left):
POST /api/v1/server/drain enter: listeners closed, queue paused, /readyz 503 "draining"
GET /api/v1/server/drain {"draining":true,"since":"2026-09-23 10:15:02","sessions":1,"deliveries":0,"drained":false}
DELETE /api/v1/server/drain leave: listeners reopened on the same ports, queue resumed, /readyz 200
POST while draining, or when the server is not running, answers 409 with the
reason; so does DELETE when nothing is draining. drained is true once nothing
is in flight, which is when a stop would be instantaneous. If a port cannot be
reopened on DELETE - something else took it while the listeners were closed -
the answer names it in not_reopened, the error log carries HM7150, and the rest
are open; a restart tries every port again. If none could be reopened because the
io threads did not answer, the state stays draining and DELETE answers 500
with the same document and error: the listeners could not be reopened, so that
a running is never declared over closed ports; try again, or restart. The state
shows as draining in /healthz, hmailserver_state, the Deck's dashboard and
its update view (which carries Drain and Resume buttons), the Control
Panel's status page and Application.ServerState (hStateDraining, 5). A live
update refuses to apply while somebody else's drain is in progress ("already
draining"): leave it, or wait for it to end, and apply again. The update's own
drain is watched. The helper is meant to stop the service, after which the
outcome it writes is read at the next start. The helper writes it only once it
has seen the service running again, and the service says it is running before
it has loaded anything, so the first thing a start does is look: when the
helper's log in the Updates directory (apply-<version>.log, or on Linux the
apply.XXXXXX it writes until it exits) is newer than the last outcome
reported and there is no outcome yet, the start-up update task looks for it
every second, for UpdateServiceWaitSeconds plus two minutes, and reports it
as soon as it is there rather than at the quarter-hour run after. A helper that finishes without
stopping it - an installer that failed while the service stayed up, or one that
did nothing - or that never gets that far would otherwise leave the server
drained for ever. So after the helper is started the server polls for its
outcome every five seconds, for up to ten minutes (a constant,
UPDATE_APPLY_DRAIN_MINUTES in UpdateInstaller.cpp, not a setting): an outcome
written while the service is still running, or the ten minutes passing with no
stop, and the server leaves the drain itself, records left on the audit trail
as server, logs why, and reports the outcome then rather than at the next start
(HM7151 when the helper wrote nothing at all). On Linux the server also asks
systemd, at each of those polls, about the unit the helper runs as
(hmailserver-update.service, or hmailserver-update-<version>.service when
systemd-run started it): a helper that has ended without stopping the service
and wrote no outcome - it refused the request, which it reports in its journal and
nowhere else - has the drain left at that poll, with HM7151 naming the unit and
its exit status, rather than after the ten minutes (until 6.3.6 the server refused
connections for all of them). Where systemctl cannot be asked, the ten minutes
stand. A reinitialise
ends maintenance mode: SIGHUP, the Control Panel's Reinitialize, POST /api/v1/server/reinitialize, a backup restore and an ACME deployment all stop
and restart the servers in place, and the restarted servers accept again; the
server records the left event itself (actor server), so the trail pairs your
entered with what ended it. Apply now, on the Control Panel's pending bar
and the Deck's Settings view (SettingsApply.md), is a
reinitialise too: while the server is draining its question says the drain ends,
and POST /api/v1/server/reinitialize answers drained: true when it did.
On Windows, hMailServer.exe --upgrade-database now runs the same chain from
the command line, for all four backends, without DBUpdater's window or the
administrator password; it attaches to the prompt it was started from and exits
0 (done, or nothing to do), 1 (failed; the output names the step) or 2 (no
[Database] configured). Stop the service first. On Linux the option is what it
was; it now runs the backend's prerequisites script before the chain, as
DBUpdater always has.
What is thinner than a window-free upgrade. A single node refuses
connections for the length of the install; there is no rolling restart without a
second node; the in-process schema step does not run DBUpdater's after-the-fact
schema probes (SchemaVerification.cs), which the installer's path keeps; and the
drain's state is per process, so a warm-standby pair drains one node at a time.
The installer checks both that the database tool launched and its exit code, so a
failed or cancelled schema upgrade is reported instead of passing silently: on a
non-zero exit code the installer shows an error dialog naming the exit code and
telling you what to run again - DBSetupQuick.exe, for an upgrade; for a new
installation, the setup itself again, or for its external database, which DBSetup
sets up, DBSetup.exe as an administrator, either of which finishes the
installation as a new one (The Service Account, "A new
installation whose database step did not complete") - and then fails the
install. Setup ends with one of two exit codes whenever a step after the files
did not complete - the service account's folders, the administrator password, the
database, the service, or the move to NT SERVICE\hMailServer when that account
cannot open the database (the service then runs as LocalSystem, and serves mail)
- and which one says whether the database is still the earlier version's:
| Exit code | The database | What it means |
|---|---|---|
| 10 | As the earlier version left it | The step's failure changed nothing in the database: a step before the database step, or a database step that failed without moving the database's version. The earlier version can be put back on it. |
| 11 | Changed by the database step, or not shown unchanged | The database step moved the database's version - all the way, or part-way before it failed - or its version could not be read before the step, or after it on an upgrade whose new version needs a newer schema than the database had, and then a step did not complete. Do not reinstall the earlier version without restoring the database first: it may refuse a newer database, and serve no mail. |
The installer tells the two apart by the database's version, hm_dbversion, which it
reads through the hMailServer service - as DBUpdater does, with the service's own
connection - before the installation (from the running service it replaces) and again
after the database step. Where it cannot read it after the step - the new version's
service does not start, or does not answer - a database that was already at the
version the new build needs (REQUIRED_DB_VERSION, which the installer is compiled
with), or past it, is still known unchanged: nothing in the step moves such a database,
DBUpdater having no step to run on it and the service's own start none either. So an
update that changes no schema, and whose new service does not start, ends with 10 and
is put back. So does one whose database step could not run at all: DBSetupQuick.exe,
which the step starts, exits with 4 when it cannot reach the hMailServer service
through COM - the service does not start, or does not answer - having started no
DBUpdater, and says why in the installer's log ("DBSetupQuick.exe returned 4:" and its
reason, the COM error's HRESULT among it); with [Database] UpgradeOnStart off, the
service's own start ran no step either, and the database is known unchanged ("Database
step left the database unchanged ... DBSetupQuick.exe could not reach the hMailServer
service (exit code 4)"). Until 6.3.6 it died of the COM error unhandled, with exit code
-532462766, and said nothing anywhere. A database step on SQL Server Compact, SQL Server
or PostgreSQL that fails moves nothing, since DBUpdater runs the whole chain in one
transaction there, which takes the schema's changes back with the rest on all three -
except a chain from a database older than 6072 that crosses 6107 on SQL Server Compact,
where DBUpdater commits the steps before 6107 so that 6107's greylisting keying runs
outside the transaction, whose temporary file Compact limits in size (Greylisting.md,
"The triplet key"), so that a failure from 6107 on leaves the database at 6106 and setup ends
with 11; on MySQL and
MariaDB each step's DDL commits as it goes, so a chain of steps can stop part-way, and
so can the service's own start with [Database] UpgradeOnStart=1, which takes one step
at a time. On MySQL and MariaDB the step that failed may also have left part of itself
behind without moving the version - a column or a table the earlier version does not
read - which an exit code read from the version cannot see: setup ends with 10, the
earlier version runs on the database, and the next upgrade's run of that step may stop
on what it left, as Repairing a database an earlier build left half done below
describes. The installer's log says what it found ("Database step begun", "Database
step changed the database: it was at version 6105 before the step and is at version
6106", or "Database step left the database unchanged").
Before 6.3.6 setup ended with 0 there: the step raised an error that Inno Setup logs
and shows and then carries on past, so a scripted deployment could not tell it from
success, and neither could the live update's helper, which reported such an update
installed. If you script installs, treat a non-zero exit code, or a hm_dbversion
that is still on the old value afterwards, as the failure; do not treat "the
installer finished" as success; and roll back by reinstalling the earlier version on
10 only.
The live update and the administrator's one-click install run the installer
through hMailServer.Updater, and a rollback is only ever made while the database
is as the earlier version left it:
- The Updater tells the installer which version it puts back
(
HM_UPDATE_ROLLBACK_VERSION, beside the token), when it holds that version's installer to run: it opens the rollback image and checks its SHA-256 before the installer runs, and holds it so, unchanged and undeletable, until it has run or is not needed. A rollback image that is gone or changed by then is not run, and the installer is not told of it. A step before the database step that does not complete - the .NET runtime, SQL Server Compact, the service's registration, the grant of its folders, the administrator password - then stops the installation before its database step, with the service not started, and setup ends with 10: the Updater puts the earlier version back on the database it left. An installation run by hand, or by an Updater with no version to put back, carries on to the database step as it always did, since the new version is then the only one that can run on what the step leaves. - On 10, or when the service does not come back after 0, the Updater runs the rollback image, as before (The Update Feed). On 10 and 11 it does not first wait for the service to come back, since what it does next does not depend on it: an installation that stopped before its database step does not start the service, and the wait only kept the server down for its length.
- On 11 it puts nothing back. Nor does it when the installer's log says its
database step began and not that it left the database unchanged - a service that
stopped again after setup ended with 0, an installer that crashed or ran for more
than the Updater's hour. The outcome is
upgraded-incomplete: what did not complete, how the database's version moved, and what to do. Where the failed step came after the database step (the service's start, say), put it right and start the service: the database is the new version's. Where the database upgrade itself stopped part-way, runDBSetupQuick.exefrom theBinfolder to finish it once its cause is put right - DBUpdater resumes from the version the database reports - or, to go back, restore the backup the update took before it (UpdateBackupBeforeApply; the outcome names the backup destination and when the backup finished, fromUpdates\update-backup-<version>.txt, which the service writes for the Updater) and reinstall the earlier version, as Rolling back below says. The service reports the outcome at its next start as HM5481 (HM5480 is the update that did not succeed and changed nothing); the Control Panel, after an administrator's install, says it in the administrator's language;Status.UpdateApplyOutcomeandapplyinGET /api/v1/updatecarry it as they carry the others.
An Updater from 6.3.5 or earlier knows neither: it rolls back only when the service does not come back, and does not tell the installer to stop. So an update from those versions, to 6.3.6 or a later release, is made as before, and these rules hold for an update from 6.3.6 on.
On Linux the live update holds to the same rule. There the schema step is the
package's own: the .deb's postinst and the .rpm's %post run hmailserver --upgrade-database as the service user while the package is installed, and a server
with [Database] UpgradeOnStart=1 - an AppImage's too - runs the chain at its own start.
Either can move the database before the new version's server fails to come back.
hmailserver-update, the helper that installs the update as root, reads the database's
version with hmailserver --schema-version (which prints required <n>, the version
that program needs, and then database <n>, the database's hm_dbversion; exit 0, or
1 when the database's cannot be read, or 2 with no [Database]), run as the user the
unit runs the server as and in the unit's own sandbox, which systemd sets up for it as
for the server (where systemd will not, the version is not read), before it installs and
again when the new version is not back.
It reinstalls the earlier version only while that version is what it was before - or,
where it cannot be read afterwards, was already at or past what the new version needs,
which nothing in the install moves. Otherwise it puts nothing back, with a rollback
image or without one, and the outcome is upgraded-incomplete, reported at the server's
next start as HM5481: what did not come back, how the database moved ("the database
was at schema version 6105 before the install and is at 6108 now", with "short of the
6108 hMailServer 6.3.7 needs" when its upgrade stopped part-way) or that its version
could not be read, and what to do - put right what keeps the service from starting
(journalctl -u hmailserver) and start it, finish an upgrade that stopped part-way
with sudo -u hmailserver hmailserver --upgrade-database, or restore the backup the
update took (UpdateBackupBeforeApply; the outcome names where, from the request the
server writes) and reinstall the earlier version. A version that cannot be read before
the install - a database the earlier version's program cannot reach, or a program from
before 6.3.6, which has no --schema-version - leaves nothing to hold the database to
afterwards, and nothing is put back. On MySQL and MariaDB a step that failed may have
left part of itself behind without moving the version, as on Windows (above). An update
from 6.3.5, to 6.3.6 or a later release, is made by 6.3.5's helper, which puts the
earlier version back whenever the service does not come back; the rule holds for an
update from 6.3.6 on.
sudo /usr/lib/hmailserver/hmailserver-update check says whether the database's
version can be read on a machine.
Three other things the installer refuses to do, all of which produce a failed install rather than a broken one:
-
It will not proceed if the service did not stop. The wait is bounded by
ShutdownDrainSecondsplus thirty seconds, derived from your own setting because that setting deliberately holds the stop open while sessions finish. If the service is still running when the file copy starts, Inno offers Retry/Ignore, and Ignore leaves the old binary in place while the schema moves forward - after which the server refuses to start at all. Refusing to move past the Ready page is the only point at which that outcome can still be prevented. -
It will not silently skip the .NET runtime. The bundled .NET 10 Desktop
Runtime is installed
/quiet /norestartwhen it is missing, and afterwards the probe is re-run rather than the exit code being interpreted - the only question that matters is whether the runtime is present now. A failure here is reported and named, and it matters because the database tools are themselves .NET applications. -
From the release after 6.3.7, it will not run over an installation of the
original hMailServer's 5.7. It refuses one before it changes anything, with exit
code 1 and the reason in its log and in
%TEMP%\hMailServer-setup-refused.txt(Where you can upgrade from, above). The installers of 6.3.7 and earlier do not look, and must not be run over one.
If it does fail, the service may be installed but the schema only partly upgraded.
The server does not paper over that: on startup it compares the database's version
against the version it requires and refuses to run if they differ, in either
direction, reporting HM5011 with both numbers appended. The two messages are worth
knowing because they are what you will search for:
The database is too old for this version of hMailServer. Please run hMailServer Database updater (DBUpdater.exe) to upgrade it.The database is too new for this version of hMailServer. Please upgrade hMailServer.
Both are followed by a space and then
Database version: <n>, Required database version: <m>.
A third, Database version could not be detected. (HM5010), means hm_dbversion
could not be read at all - a connection or permission problem rather than a version
one.
For the first, run DBUpdater.exe from the installation's Bin folder - that is
the standalone updater, and it is what the error message names. (The installer drives
DBSetupQuick.exe for the same job during an install; either will bring the schema
forward.) It picks up from whichever 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, and that step number is the useful thing to put in
an issue.
The second message means the database has already been upgraded past this binary - you are running an older server against a newer schema. Install the matching or newer server rather than trying to downgrade the database.
There is no downgrade path, and this is the one genuine sharp edge in the process.
The schema upgrade is one-way: an older server will refuse to run against a newer
dbversion rather than misinterpret it, which is the correct behaviour but means
"just reinstall the old version" does not work on its own.
To roll back you need the pre-upgrade database backup. Restore it, then reinstall the older server. The data directory does not normally need restoring - message files are not rewritten by an upgrade - but if you restore an older database against a newer data directory, any mail that arrived after the backup will exist on disk with no metadata row, and will be invisible.
Which is the real argument for the backup: not fear of the upgrade, but the fact that rollback is only as good as the snapshot you took first.
Rolling back across schema 6042 - from 6.3.4 to 6.3.3 or earlier - keeps the
hMailServer.ini the newer server left: where the service may write the file, its
[Settings] section is a full copy of the stored settings, and the older server reads the
file as the truth, so settings changed after the upgrade survive - not under
NT SERVICE\hMailServer, nor under the Linux packages' hmailserver user, to either of
which the file is read-only, and where the copy is as the last account that could write it
left it (The Service Account). Upgrading hMailServer 18.6 has the detail.
Restore hMailServer.ini with it, when the version you roll back from sealed the
stored secrets (schema 6103 on, Secrets at Rest). Such a server
writes the [Settings] secrets into the file as HMK1: envelopes, and an older server
reads the file's lines as they are: its /Register - which the older installer runs -
hands ServiceAccountPassword's envelope to Windows as the service account's password,
and the service then cannot log on and does not start; its first moments, before the
restored database puts its rows back into the file, run SRS, BATV and OAuth2 on the
envelopes. So restore the pre-upgrade hMailServer.ini beside the database, or, where
you have none, set ServiceAccountPassword (and any other of those secrets you use)
again with the older version's Control Panel once it runs, and run hMailServer.exe /Register again.
Moving to a different database backend is not an upgrade and the chain does not do it. There is no in-place conversion between backends: the route is a backup, a new database on the new backend, and a restore into it, as Migrating the Database Backend describes step by step.
Moving to a new machine: install the same version on the new machine, restore the database and the data directory, and confirm the data-directory path in the settings matches where you actually put it. Upgrade afterwards, as a separate step, so that if something goes wrong you know which of the two changes caused it.
Two values that are not in the database go with it by hand, before the new machine's
service first starts on the restored database: PasswordPepper, whose value is copied
into the new machine's hMailServer.ini (Backup.md), and - from schema
6103, where the installation has one - the stored-secrets key every account's second
factor and the [Settings] secrets are sealed under. Export it on the old machine
(hMailServer.exe --export-stored-secrets-key FILE, elevated, or sudo hmailserver --export-stored-secrets-key FILE on Linux) - or take the file exported when the key was
made, if the old machine is gone - and import it on the new one with
--import-stored-secrets-key FILE. The new machine's installer made a key of its own for
the empty database it installed with: remove that [Security] StoredSecretsKey line from
the new machine's hMailServer.ini first, or the import is refused. Never copy the line
from the old machine, which is sealed under its own DPAPI. Without the key every second
factor refuses every code on the new machine and those settings read as not set, and the
error log says HM7495 and names this step; with the key lost for good,
--forget-sealed-secrets KEYID clears what it sealed (Secrets at Rest). A
move through a backup archive rather than the database does not need the key: the archive
carries those secrets opened, and the restore seals them under the new machine's own. It
still needs the pepper.
Where the
[Settings]values come from on the new machine. On 6.3.3 they belong tohMailServer.ini: a built-in restore that includes settings writes them into the new file as well as the database, but a database restored with its own tools does not, and a new file with no[Settings]keys makes the mirror drop the restored rows at the first start - copy the old file's[Settings]section across (never its[Database]password; see below). Since 6.3.4 the database is the store, so the values arrive with either kind of restore, and are written into the new file at the first start where the service may write it (not underNT SERVICE\hMailServer, a new installation's account, to which the file is read-only - The Service Account) - exceptPasswordPepper, which that build keeps in the file only and leaves out of every backup. Copy that one line from the old file by hand, or every account whose password is hashed with Argon2id or scrypt is refused on the new machine.
The database password does not travel with the file. With
ProtectStoredSecretsWithDPAPI=1, which is the default, the[Database] Passwordvalue inhMailServer.INIis protected with machine-scoped DPAPI (CRYPTPROTECT_LOCAL_MACHINE) andPasswordEncryptionrecords that. Copied to a different machine it cannot be decrypted, and the server will not connect. Re-enter it there - throughDBSetup.exe, which writes the section with the password protected, or by writing the plain value withPasswordEncryption=0, which the server reads as it is and does not re-protect - rather than expecting a copied ini to work. The same is true of every other secret the server stores in that envelope.
Upgrading silently — see the unattended install
notes. Until 6.2.23 a silent upgrade of an installation with an administrator password
set hung: DBSetupQuick.exe forwarded only /SilentIfOk and /silent to
DBUpdater.exe, not the password, and DBUpdater then opened a modal password dialog
nobody was there to answer. That is fixed. DBSetupQuick forwards the password on the
upgrade path as well as the create path, the installer accepts it as
/adminpassword=<password> or, from 6.3.4, in the environment variable
HM_ADMIN_PASSWORD, and under /silent the shared authenticator fails with an exit
code instead of prompting. From 6.3.4 the installer hands the password to
DBSetupQuick.exe, and DBSetupQuick to DBUpdater.exe, in HM_ADMIN_PASSWORD; until
then it went on their command lines unquoted, so a password with a space, a double
quote or backslashes before one reached them changed and the database step failed with
exit code 3. So:
- administrator password empty -> a silent upgrade works;
- administrator password set -> pass
/adminpassword=<password>or setHM_ADMIN_PASSWORD; without it the database upgrade fails and the installer reports the non-zero exit code rather than waiting. A password with a double quote goes inHM_ADMIN_PASSWORD: Inno Setup drops every double quote from its command line. Set the variable in an elevated prompt: setup started without administrator rights elevates itself through the UAC prompt, and the variable does not cross it.
(New in 6.2.28: the live update's helper
passes a single-use /upgradetoken=<hex> in place of the password, so an unattended
update never handles the administrator password at all. From 6.3.6 it gives a 6.3.6
or later installer the token in its environment, HM_UPGRADE_TOKEN, instead, so that
it is on no command line and in no log.)
Coming from before hMailServer 5, hMailServer.ini lived in the Windows
directory. The installer copies it to Bin\hMailServer.ini, leaves a .old copy
where it was and deletes the original; the server reads only the one in Bin.
The .NET 10 runtime. From the first release after 6.2.18, the Control Panel and
the setup tools require the .NET 10 Desktop Runtime, which the installer bundles and
installs when missing; 6.2.18 and earlier used .NET 8. 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. The runtime is bundled as
windowsdesktop-runtime-10.0-win-x64.exe and installed /quiet /norestart, only
when it is missing, and the setup tools (DBSetup, DBSetupQuick, DBUpdater)
need it as well as the Control Panel does.
Setup does not update a .NET 10 Desktop Runtime the machine already has, of any
10.0 release: it installs the one it carries only where none is there. The
runtime is Microsoft's product, and Microsoft Update services it - on Windows
Server through WSUS and the Microsoft Update Catalog, and through Automatic
Updates only where the server has been opted in (Microsoft's Install .NET on
Windows, "Microsoft
Update"). So keep it current there, or install Microsoft's newer release
yourself: dotnet --list-runtimes lists the releases a machine has, and the one
the installer carries is the release
third-party-binaries.json pins.
For contributors, because two of these have shipped as defects and both were invisible locally.
-
Statements are split on a blank line.
SQLScriptParserbreaks a script into commands at a blank line (;then a blank line, for PostgreSQL). Two statements written on consecutive lines are sent as one command, which SQL Server Compact rejects outright - and a create script that fails leaves a fresh install with no database, whose symptom is not an error anybody sees: the service starts and simply does not listen. Leave a blank line between statements. -
Five hand-maintained lists describe the chain and nothing but
build\check-schema-versions.ps1was checking that they agree:REQUIRED_DB_VERSIONinConstants.h; the step table inDBUpdater'sformMain.LoadScripts; the version names inGetDatabaseVersionName; the fourUpgrade<from>to<to><dialect>.sqlfiles per step; and the probes inSchemaVerification.cs. Run that script after adding a step and before stamping a release;-SelfTestchecks the checks. -
build\check-db-scripts.ps1executes them. It builds a throwaway SQL CE database from the create script and applies every upgrade script to a second one, using exactly the parser's splitting rules, and runs every probe against a freshly created database with a negative control. This is what would have caught the 6.2.22-pre4 create-script defect, and what now catches a probe a backend cannot run - the SQL CEcase when exists (...)crash behind GitHub issue 114.
MySQL and MariaDB: schema 6102 rebuilds every table. The step converts every
table to utf8mb4 under utf8mb4_general_ci, InnoDB and ROW_FORMAT=DYNAMIC, so
that a character outside the Basic Multilingual Plane - an emoji - is stored instead
of refused; Migrating the Database Backend
says why each of the three. What it means for the upgrade:
-
It takes as long as copying the database. Each table is rebuilt, one after the other, and the largest -
hm_messages,hm_message_metadata,hm_messageindexterms,hm_messagerecipients,hm_audit- take the time. With the schema empty it is one to five seconds; on a large installation plan minutes or more, and have free disk space of the order of the largest table, since InnoDB writes the new copy before it drops the old. -
It needs MySQL 5.7.9 or MariaDB 10.2.2. So do steps 6041 and 6101 before it, whose new tables have keys of the same size. Each of the three opens with a statement that needs such a key - 6041 and 6101 make and drop a table
hm_upgradeprobewith one, 6102 convertshm_accounts- so on an older server the upgrade from 6.3.3 stops at the first, 6040 to 6041, with error 1071, and nothing has been changed; upgrade the database server, then the schema. Until 26 September 2026 steps 6041 and 6101 made their first tables before the index that failed, and MySQL commits a CREATE or ALTER TABLE at once, so they stopped half done and every later run failed on a table already there. -
Repairing a database an earlier build left half done. No upgrade path repairs it -
UpgradeOnStart,hmailserver --upgrade-databaseand DBUpdater all run the same step again and stop on the same table - so it is put back by hand, once the database server can hold the keys (MySQL 5.7.9 or MariaDB 10.2.2 or later, InnoDB as the default engine,innodb_large_prefixon where the server still has it), or the probe stops the step again. Stop the hMailServer service and take a backup of the database first.At 6040, from 6.3.3 -
select value from hm_dbversionanswers6040andshow tables like 'hm_calendar%'listshm_calendars. The old step 6041 made that table and stopped at its index; nothing at schema 6040 uses it, so it is empty (select count(*) from hm_calendarsanswers 0). Drop what the step made and run the upgrade again:DROP TABLE IF EXISTS hm_calendarobjects; DROP TABLE IF EXISTS hm_calendars;
At 6100 -
select value from hm_dbversionanswers6100andshow tables like 'hm_booking%'listshm_bookingpoliciesorhm_bookinganswers. A server that took the keys of step 6041 takes these as well, so this needs a database server changed between the two steps; if it happened, the old step 6101 had added a column tohm_accounts, madehm_bookingpolicies, added two columns tohm_messagesand madehm_bookinganswers. Drop those - each column only ifshow columns from <table> like '<column>'lists it, since MySQL has noDROP COLUMN IF EXISTS- and run the upgrade again:DROP TABLE IF EXISTS hm_bookingdecisions; DROP TABLE IF EXISTS hm_bookingqueue; DROP TABLE IF EXISTS hm_bookinganswers; DROP TABLE IF EXISTS hm_bookingpolicies; ALTER TABLE hm_messages DROP COLUMN messagesubmitterid, DROP COLUMN messageauthdomains; ALTER TABLE hm_accounts DROP COLUMN accountkind;
Dropping the two
hm_messagescolumns rebuilds that table, which takes as long as copying it. Neither repair touches a row the server had before the step. -
It needs the ALTER privilege on the database, not only on its tables, because it also changes the database's default character set. An account granted
ALL PRIVILEGES ON <database>.*has it. -
It can stop at a duplicate. Most tables compared under
utf8_general_ci, of whichutf8mb4_general_ciis the twin, and a unique index over them cannot meet a duplicate it did not meet before. Some compared under a collation that tells apart a few thingsutf8mb4_general_cidoes not: everyutf8table on MariaDB 11.5 and later (utf8mb3_uca1400_ai_ci, which keepsstraßeapart fromstrase), the tables that wereutf8mb4already - the calendar tables, and the IMAP metadata table of a database upgraded through 6014 - under the server's ownutf8mb4collation (utf8mb4_0900_ai_cion MySQL 8,utf8mb4_uca1400_ai_cion MariaDB 11.4 and 11.8, both of which keep apart two names that differ only in an emoji), and, in a database an administrator made, the fifteen tables that named no character set and took the server's default (latin1_swedish_ci, for one, keepsÄapart fromA). Two such rows under one unique index stop the step with error 1062, which names the index and the value. Rename or remove one and run the upgrade again: every statement of the step can run twice, a table already converted is converted again at no harm, and the step finishes. -
Afterwards a
textcolumn may readmediumtext, and amediumtextonelongtext: MySQL widens a column it converts to four bytes a character so that it holds as many characters as it did. A fresh database keeps the declared types; the server reads both alike.
Schema 6107 keys the greylisting triplets. The step gives every greylisting triplet
written before schema 6072 its key - in the server, since no database can compute it -
deletes the ones in which an address had been cut to the old columns' 200 characters and
the second row of any triplet, and then makes the key NOT NULL and unique
(Greylisting.md says why each). A sender whose row
was deleted is greylisted once more; the output of --upgrade-database, or with
DBUpdater and UpgradeOnStart the application log when application logging is on, says how
many rows were keyed and deleted. What it means for the upgrade:
-
On a large
hm_greylisting_tripletsthe time is the keying, a few thousand rows a query and a hundred to a statement, and it is done before the step's transaction, each statement committed as it goes. An upgrade stopped part-way keeps what it keyed, the database is meanwhile a valid one at the schema before, and the next run of DBUpdater,hmailserver --upgrade-databaseorUpgradeOnStartcontinues from the first row still without a key. - From before 6072 with DBUpdater on SQL Server Compact or MySQL, where the key columns do not exist until step 6072 has run, DBUpdater commits the steps up to 6106 once their probes pass and keys the rows outside a transaction then; a failure in step 6107 leaves the database at 6106, and the next run continues there. On SQL Server and PostgreSQL the upgrade stays one transaction.
- Clear greylisting triplets (the Control Panel's Anti-spam page, Greylisting tab) before the upgrade leaves the step nothing to key, at the cost of greylisting every sender once.
Scripts that change the configuration over COM. Since 6.3.4, a
COM change to a route (and a route's addresses), an incoming relay, a DNS black
list, a SURBL server, a remote domain policy, a server message, a blocked
attachment, an SSL certificate or a group takes effect when the object's Save() is
called, and not before. Setting a property writes the object's own copy of the item;
Save() writes the copy to the database and the server reads the list again. Two
things a script written for an earlier release may have relied on no longer happen:
a property set and never saved used to change the running server until the next
restart, and now changes nothing; and another object for the same item, read before
the Save(), used to show the new value, and now keeps the value it read - read the
item again from its collection after the save. The Control Panel already reads,
changes and saves each item in turn, and is not affected. The reason is in
Architecture ("A COM change to an item of a shared
collection is written onto a copy"): the old behaviour wrote the fields of the very
object the server's sessions were reading.
Re-checked 8 September 2026 against the 6.2.28 tree, and two statements on this page were wrong and have been corrected:
- The page said a failed database upgrade showed a dialog and then let the installer
finish, so "the signal is the dialog and the log, not a failed installation".
CurStepChangednow raises on a false result fromRunPostInstallTasks, so setup finishes with a non-zero exit code. - The page said step
5004 -> 5005dropshm_adsynchronizationon MS SQL, MySQL and SQL CE "but not on PostgreSQL", and offered that as an example of an upgraded database differing from a fresh one.Upgrade5004to5005PGSQL.sqldrops it too, viahm_drop_table, so the example was not real. What is true is recorded above.
The repository text was re-copied from master 540347500 on 24 September 2026, which brought
An upgrade without a window, from master 92b8f9f63 on 26 September 2026, which brought the
service account, the stored-secrets key, the COM Save() rule and MySQL's utf8mb4 step, and
from 8adce850a the same day, the commit the tag v6.3.4 was first made on - the document is the same at the tag, 51e2dbda0 - which brought the installer's HM_ADMIN_PASSWORD,
the named lock on MySQL and MariaDB and the probe at step 6041; this page keeps its correction of the hm_adsynchronization
example, which the repository document does not yet have.
The earlier note stands and is worth keeping: the step count and target version had gone stale a fourth time - the page said 73 steps to 6022 while the tree had moved on - after a previous revision had already written down that deriving them beats transcribing them, and then transcribed them anyway. They are gone from the prose; the sources below are the answer.
Against: formMain.LoadScripts, CreateUpgradePath, DoUpgrade and
VerifyUpgradedSchema in DBUpdater (the registered steps, the path walk, the
transaction, the probes and the read-back), DBUpdater/Program.cs (the exit codes),
Constants.h's REQUIRED_DB_VERSION, hmailserver/source/DBScripts (which dialects
ship which steps, and the hm_adsynchronization history),
DatabaseConnectionManager::GetCurrentDatabaseVersion (select ... from hm_dbversion), Application::OnDatabaseConnected (the three refusal messages and
their error codes, verbatim), hMailServerInnoExtension.iss (StopHMailServerService,
InstallDotNetRuntime, MoveIni, the exit-code check and the RaiseException that
follows it), DBSetupQuick's UpgradeDatabase (which arguments are forwarded),
Authenticator.AuthenticateUser (the silent-upgrade password dialog),
IniFileSettings::SetPassword and DataProtector (the machine-scoped DPAPI
envelope), and build\check-schema-versions.ps1 and build\check-db-scripts.ps1. The notes on
hMailServer.ini marked since 6.3.4 were checked on 15 September 2026 against
commit 3233b0cc4: Upgrade6041to6042*.sql, IniSettingStore::Synchronize and XMLLoad, and
IniSettingStore::IsFileOnlyName for the pepper; the 6.3.3 behaviour on a new machine is the
second pass of Synchronize in v6.3.3.
hMailServer 6.3.7, the latest release (17 October 2026, schema 6108) · AGPL-3.0-or-later · Repository · Mirror on GitHub · Report a documentation error (a GitLab issue naming the page)
Home — full index
Start here
1. Install and run
- Before You Install
- Installing hMailServer
- Installing on Linux
- Running in a Container
- Profiles and Environment
- The Control Panel
- The Control Deck
- Your First Domain and Mailbox
- Connecting a Mail Client
- The Webmail
- DNS for Your Domain
- Migration
2. Secure it
- Encryption and Certificates
- Database TLS
- Encryption for People
- Stopping Spam
- Stopping Viruses
- Security Hardening
- Secure Defaults
- The Service Account
- Secrets at Rest
- Sign-in and Identity
- Security Policy
3. Operate it
- Monitoring and Health
- The SNMP Agent
- Backup and Restore
- Message Storage
- Compliance and eDiscovery
- Troubleshooting
- Diagnosing Stalled Mail
- Relocating an Installation
- Upgrading hMailServer
- Upgrading Guide
- The Update Feed
- Migrating the Database Backend
- High Availability Runbook
- Warm Standby
- Runbooks Digest
4. Extend it
- Rules and Sieve
- Aliases Lists and Public Folders
- Rooms and Equipment
- Mailing Lists
- Routes and Relays
- Mail In to a Webhook
- Outbound Shaping
- The COM API and Scripting
- Event Scripts
- The REST API
- Command Line
- APIs Reference
5. Contribute to it
- Project Handbook
- Architecture
- Contributing
- Release Process
- Governance
- Assurance Case
- Regression Test Environment
- Fuzzing
- Regulatory Scope
- Third-Party Binaries
Look it up — from any journey
- Settings Reference
- Settings Overview
- Capabilities and Configuration
- Control Panel Reference
- Control Panel Pages
- Ports Reference
- Changes-Since-6210
- Version-History-to-6210
- Glossary
About this wiki
- hMailServer 6.3.7 is the latest release (17 October 2026, schema 6108)
- Pages say since 6.3.4 where a feature is new in it
- AGPL-3.0-or-later
- Repository, the one place the project is worked on
- Its mirror on GitHub, since 18 October 2026, with no Issues
- Questions: a GitLab issue with the Question template, GitHub Discussions Q&A, or the forum
- Report a documentation error: a GitLab issue naming the page