-
Notifications
You must be signed in to change notification settings - Fork 3
Upgrading Guide
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.
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 upstream project. This fork did not branch the schema; it extended it. The current version is a superset, reached by the same mechanism upstream used.
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.
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 for most DDL; the server also rolls back on its own for some errors | Normally as it was |
| MySQL / MariaDB | Commits DDL as it executes | The steps that ran, applied. Re-running resumes from whatever hm_dbversion now says |
| SQL Server Compact | Commits DDL as it executes | The same |
This is why DBUpdater's messages say "rolled back where the backend allows it"
rather than promising a rollback it cannot deliver. On the two that 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.
The installer checks both that the database tool launched and its exit code, and a non-zero exit code now fails the installation: the specific dialog naming the exit code is shown, the service start is still attempted, and then setup raises and finishes with a non-zero exit code of its own. A scripted deployment can therefore tell, which it could not in earlier releases - the dialog used to be the only signal and the wizard still reported success.
Two other things the installer refuses to do, both 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.
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.
Moving to a different database backend is not an upgrade and the chain does not do it. There is no supported in-place conversion between backends. The route is a backup, a repoint and a restore, which is written up in Migrating the Database Backend.
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.
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, or by writing the plain value and letting the server re-protect it - 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> and passes it through, and under /silent the shared
authenticator fails with an exit code instead of prompting. So:
- administrator password empty -> a silent upgrade works;
- administrator password set -> pass
/adminpassword=<password>; without it the database upgrade fails and the installer reports the non-zero exit code rather than waiting.
(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.)
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.
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 issue #114.
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 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.
hMailServer 6.3.2 · AGPL-3.0-or-later · Repository · Report a documentation error
Hmail Server — full index
Start here
1. Install and run
- Before You Install
- Installing hMailServer
- Installing on Linux
- Running in a Container
- The Control Panel
- Your First Domain and Mailbox
- Connecting a Mail Client
- DNS for Your Domain
2. Secure it
3. Operate it
- Monitoring and Health
- Backup and Restore
- Troubleshooting
- Diagnosing Stalled Mail
- Relocating an Installation
- Upgrading hMailServer
- Upgrading Guide
- Migrating the Database Backend
- High Availability Runbook
- Warm Standby
- Runbooks Digest
4. Extend it
- Rules and Sieve
- Aliases Lists and Public Folders
- Routes and Relays
- The COM API and Scripting
- The REST API
- 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