Repository navigation
Relocating an Installation
Moving a configured installation to another directory or drive. The Installation paths diagnostic and the relative [Directories] values it describes shipped in 6.3.0. What changes when the settings store moves into the database - steps 1, 4 and 9 - and the DKIM key rotation's paths in steps 1 and 9 are new in 6.3.4. This is the wiki copy of docs/RelocatingAnInstallation.md, which is the one the line references were checked against; the two are kept the same, but for this wiki's own additions, which the end of this note names. Re-copied from master 92b8f9f63 on 26 September 2026, and the same at the tag v6.3.4 (51e2dbda0), the release; 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, as v6.3.6 left it (RelocatingAnInstallation.md at v6.3.7): from 6.3.6 UpdateTrustRootsFile and UpdateLogPublicKeyFile, with the other three settings of the live update's trust, are read from the file alone, so the two paths move in step 4. Re-copied on 20 October 2026 from the document as master has it from batch50, 0c0b6bae7 (RelocatingAnInstallation.md at 0c0b6bae7): the inventory of step 1 and step 4 name [Database] TlsCaFile beside PostgreSQLSslRootCert, and step 4 says that a path in [SettingsOverride] and the two trust files move there too; and Seeing where the paths are says that the Diagnostics page's Installation paths lists every path-shaped setting the server holds but TlsCaFile, where it said every one. Re-copied on 21 October 2026 from the document as master has it from batch51, 5e49abf06 (RelocatingAnInstallation.md at 5e49abf06), whose opening now says that steps 0, 1, 3, 4, 6, 8, 9, 11 and 12 were revised on 19 and 20 October 2026 for 6.3.7: a new paragraph after the opening sends an installation whose programs somebody who is not an administrator may have changed to Reinstalling into a new folder in The Service Account instead; step 0 converts full message paths with the Data Directory Synchronizer on the installation before anything is copied, a row that names a file outside the data directory first, in the folder it names, in five numbered points, where step 8 ran the synchronizer against the new data directory after the copy, and step 8 now checks that none is left and converts one that is the same way in the old data folder; step 3 makes the new root with the permissions setup gives a program folder before the copy, since a copy takes the permissions of the folder it lands in; step 4 says that the server reads Bin\hMailServer.ini under the folder the registry's InstallLocation names, so the edited copy only from step 5 on, where it said that the file beside the running hMailServer.exe was read first, and, with step 1, that an edit of a stored setting in the file is put back only where the service may write the file; step 6 runs hMailServer.exe /ProtectFolders after /Register, and /GrantServiceAccess for a service that runs as anything but LocalSystem; step 7 names the Diagnostics page's Installation paths, where it named Settings → Directories; step 9 changes a certificate's files with Edit… rather than deleting it and adding it again, and says that under NT SERVICE\hMailServer a setting stored there does not reach the file's copy; step 11 runs the release's setup again at the new root with /DIR, the 32-bit uninstall entry taken away first, and never the old uninstaller; and step 12 keeps the old tree private until it is deleted by hand. This wiki's passage that ends step 4 now says the same of an edit of a stored setting and of the file's copy, and its paragraph on moving to new hardware that the stored settings are written into the new machine's file where the service may write it, and that from 6.3.6 the live update's trust, which a backup does not carry, is copied to the new machine with the file. Its links to the repository's other documents point at them as 5e49abf06 has them, or at this wiki's copy where there is one. This wiki's own additions are the paragraph on moving to new hardware, before step 0; the pointers to Monitoring and Health §16.8, in step 0 and under Seeing where the paths are; what 6.3.3 and earlier do, in the note under step 1's table of the settings store, in the hm_inisettings row of its table of the database's columns and in the passage that ends step 4, which also gives two ways of moving the [Settings] paths; and the sentence in step 7 on reading the ERROR log since 6.3.4.
Moving a configured installation from one directory to another - C:\Program Files (x86)\hMailServer to C:\Program Files\hMailServer, or onto another
drive - is not one change. The installation path is recorded in the registry, in
the service, in the COM registration, in hMailServer.ini, and in a handful of
database columns that hold absolute file names an administrator typed. This is
the ordered list of every one of them, what repairs each, and the two places
where the honest answer is "there is no clean fix".
It is written for 6.3.0, and steps 1, 4 and 9 were revised on 15 September 2026
for schema 6042, which made the database the store for the settings the
[Settings] section of hMailServer.ini used to hold; steps 0, 1, 3, 4, 6, 8,
9, 11 and 12 were revised on 19 and 20 October 2026 for 6.3.7: the copy's permissions
and hMailServer.exe /ProtectFolders, a certificate's files changed with
Edit…, setup run again at the new root, full message paths converted before
the copy rather than after it, and which hMailServer.ini the server reads and
what it does with an edit of a stored setting under each account. Every claim
below was read out of the code; the file and line references are there so you
can check any of them.
Installing somewhere new is a different, solved problem. /DIR="<path>" on
the installer puts a fresh installation wherever you want. This document is
about an installation that already has mail in it.
Moving to new hardware is not this runbook either, but the settings half of it
is worth one paragraph here. On 6.3.3 and earlier a built-in backup that includes settings puts the
[Settings] values back into the new machine's hMailServer.ini as well as its
database; a database copied with the backend's own tools does not, and a new machine
whose file has no [Settings] keys makes the mirror drop those copied rows at its first
start - so bring the old file's [Settings] section across. Since 6.3.4 the
database is the store, so those settings arrive with the database (or with a
built-in backup that includes settings) and, where the service may write the file,
are written into the new machine's file at its first start -
except PasswordPepper, which is in no database and no backup
and has to be copied from the old file by hand, or every password hashed with Argon2id
or scrypt is refused. From 6.3.6 the live update's trust is read from the file alone
too, and a backup does not carry it: a site that sets it copies it to the new machine
with the file.
Copying the tree, or reinstalling into the new folder. This document copies the whole installation, its programs included. Where somebody who is not an administrator may have changed a program in the old folder - an installation at a drive's root before 6.3.7, say; The Service Account, Before upgrading an installation at a drive's root, says when - do not copy it: follow Reinstalling into a new folder on the same page instead, which brings across the mail, the built-in database, the settings file and the event script, each read first, and has the release's setup put the programs in the new folder. Its step 3 runs setup as this document's step 11 does, and its step 4 moves the database's paths by this document's step 9.
The good news, first. The mail store itself is already relocatable. Message
rows hold a bare {guid}.eml with no directory (Message::GenerateFileName,
Common/BO/Message.cpp:576), and the path is rebuilt at every read against the
data directory (PersistentMessage::GetFileName,
Common/Persistence/PersistentMessage.cpp:2039). Moving the data directory and
pointing DataFolder at its new home moves every message with it. The exception
is a database that predates hMailServer 5.4, where the column can hold a full
path, which is used as it is wherever the data directory is; step 0 tells you
whether yours does, and converts them before anything is copied.
-
Take a backup and verify it restores. Everything below is reversible only while the old tree and a good backup both exist.
-
Ask the server whether its message rows are relative. In the Control Panel, Monitoring & troubleshooting → Diagnostics → Test message file locations (Monitoring and Health §16.8) (
Common/Diagnostics/TestDataDirectory.cpp:31). If it reportsERROR: Full paths are stored in the database, the move is still possible, but those paths are converted now, on this installation, before step 2 - after the copy they no longer can be. Where it also reportsMessages exists which are located outside of the data directory, begin with the third point:-
With the service running, run the Data Directory Synchronizer (
<old root>\Addons\DataDirectorySynchronizer\DataDirectorySynchronizer.exe, or Addons → Data Directory Synchronizer in the Start menu) and sign in as the administrator. Choose Import messages which are not in the database into the database, select every domain, and include the public folders if you have any. It walks the data directoryDataFoldernames, and for each file whose full path a row holds it writes the row's partial name instead (MailImporter::Import→ReplaceMessagePath_,Common/Util/MailImporter.cpp); a file not in the layout the server builds paths from - an.emlor.hmafile directly in an account's folder or in#Public, or in a folder of theirs not named for the two characters after the{of its name - is first moved into it, under a new name. -
It imports every file there that no row names as well: such a file at the data directory's root is imported into the queue and delivered to its local recipients, and one in an account's folder becomes a message in that account's Inbox. A file it could not place is listed as skipped.
-
It converts only a row whose full path is where the file is now, below the data directory
DataFoldernames:PersistentMessage::GetMessageIDlooks the file up by its partial name and then by its full path as found. A row that names a file outside the data directory - the diagnostic's other error,Messages exists which are located outside of the data directory- names a folder the data was in before an earlier move, and is converted in that folder, as the server's own message about such a row says. Do that before the run the first point describes: the data folder you were using can hold a copy, left by the earlier move, of a file such a row names, which that run would take for one no row names and import a second time. In this order, withhMailServer.exe /ProtectFoldersrun fromBinafter each edit ofhMailServer.ini, as step 6 says:-
Let the service write that folder. A service that runs as anything
but LocalSystem (step 6) needs Modify there.
/GrantServiceAccessgives it that on the program folder's ownData, and on another folder once approved - from an elevated prompt inBin,hMailServer.exe --approve-modify "<folder the rows named>" /GrantServiceAccess(The Service Account, Approving a folder). A folder that is never granted - one in a Program Files folder, such as an earlier installation'sDatainC:\Program Files (x86)- is listedREFUSED: run the service as LocalSystem for points 3 and 4 instead, and move it back after them (ServiceAccount.md, Moving it, and going back). Without Modify the synchronizer cannot move a file outside the layout there, and lists that file as skipped, as it does each file the service's account may not read. -
Delete from the data folder you were using its copy, at the same
place, of each file outside the layout in the folder the rows named. The
run moves each of those under a new name, and the copy back in point 4
removes nothing, so the copy would stay under the old name, where no row
names it, for a later run to import as a new message - or, in
#Public, to list as skipped (step 8). Do it now, while those files still have the names their copies have. -
Hold the mail still, then convert. While
DataFoldernames the other folder, the file of each message written since the data left it is not there: a queued one is deleted when it is tried (HM5006,SMTPDeliverer::DeliverMessage), and one a client opens over IMAP or POP3 is given a placeholder there, under its name, and its row the placeholder's size (PersistentMessage::EnsureFileExistance). So untick SMTP server, IMAP server and POP3 server on Connections & protocols → Protocols, Services, save, restart the service, and wait until Monitoring & troubleshooting → Delivery queue is empty. Then putDataFolderat the folder the rows named, run/ProtectFolders, restart the service and run the synchronizer there as the first point says. As it does with any file no row names, it imports again each copy the earlier move left there of a message deleted or delivered since, a queued one to be delivered again to its local recipients. -
Bring the two folders together, as the server's message goes on to
say: stop the service, give the data folder you were using what the
folder the rows named has and it lacks
(
robocopy "<folder the rows named>" "<data folder>" /E /XC /XN /XO, which replaces nothing there: a plain/Ecopies over the file in use each file whose size or time differs, an older copy of a file changed since or a placeholder among them), putDataFolderback to the data folder you were using, run/ProtectFoldersand start the service. Then tick the three services again, save, and restart the service. - Run the diagnostic. Where it reports relative paths for all messages, the conversion is done and the run the first point describes is not needed: on the data folder you were using it would convert nothing, and would import each file there that no row names, each copy the earlier move left of a message deleted or delivered since among them. Where it still reports full paths, the rows left name a file below the data folder you were using, which that run converts, or one the run in the other folder listed as skipped or did not find, which only a run there converts, once the file is where its row names it and point 1 is done.
-
Let the service write that folder. A service that runs as anything
but LocalSystem (step 6) needs Modify there.
-
Run the diagnostic again. It should report that relative message paths are stored for all messages before you go on.
Never run it after the copy, against the new data directory: a row that holds a full path still names the file in the old tree, the copy is found under the new one, the two do not match, and in the import mode every such message is imported a second time while the first row goes on reading the old tree; in the other mode, Deletes message which are not in the database from disk, the copies are deleted.
-
-
Write down what you are about to strand. The inventory is in step 1. Doing it afterwards means finding out from a failure.
| Section | Key | What breaks if it is left pointing at the old tree |
|---|---|---|
[Directories] |
ProgramFolder |
Language files, the database scripts and 7-Zip for backups are looked for under it (IniFileSettings.cpp:1915, :268, Compression.cpp:727) |
[Directories] |
DataFolder |
Every message path is rebuilt from it. The store appears empty |
[Directories] |
DatabaseFolder |
The SQL Server Compact database file, if you use that backend |
[Directories] |
LogFolder |
Logs are written to the old tree, silently |
[Directories] |
TempFolder |
Message assembly and backup staging |
[Directories] |
EventFolder |
Event scripts are not found; events stop running |
[Database] |
PostgreSQLSslRootCert |
PostgreSQL TLS verification fails |
[Database] |
TlsCaFile |
On MySQL, MariaDB and PostgreSQL, TlsMode=verify-full reads it, and refuses to connect once it is gone (HM7531) |
Leave the [Database] connection keys alone. They name a server, not a path.
These were [Settings] keys of hMailServer.ini, and the file still holds a copy
of each. Since schema 6042 the database is where they are stored: at the next
start a value edited into the file is named in the error log as HM5804 and the
stored value is used instead - the line is put back where the service may write
the file, and left as edited where it may not, the virtual account's case
(IniSettingStore::Synchronize). They are changed in step 9, not in step 4.
| Setting | What breaks if it is left pointing at the old tree |
|---|---|
ArchiveDir |
Archiving stops; existing archived copies are not found |
AcmeCertificateDirectory |
ACME state and issued certificates |
OAuth2PublicKeyFile |
Token validation refuses |
UpdateTrustRootsFile, UpdateLogPublicKeyFile
|
Update verification refuses. From 6.3.6 these two, with the other three settings of the live update's trust, are read from the [Settings] section of the file alone and never from this store (The Update Feed, "Where the trust is read from"): change them in the file, in step 4, and restart the service |
RestApiCertificateFile / PrivateKeyFile, MetricsServerCertificateFile / PrivateKeyFile, WebServicesCertificateFile / PrivateKeyFile
|
The listener starts without TLS, or does not start |
On 6.3.3 and earlier, which come before the settings store (schema 6042, since 6.3.4), these are
[Settings] keys of hMailServer.ini like any other, and step 4 changes them there.
| Value | Where | Repaired by |
|---|---|---|
InstallLocation |
HKLM\SOFTWARE\hMailServer in the 32-bit view
|
Step 5, by hand |
Service ImagePath
|
HKLM\SYSTEM\CurrentControlSet\Services\hMailServer |
Step 6, /Register
|
LocalServer32 (79 keys) and InprocServer32 (15) |
HKCR\CLSID\... |
Step 6, /Register
|
TypeLib win64 and HELPDIR
|
HKCR\TypeLib\... |
Step 6, /Register
|
InstallLocation, UninstallString, DisplayIcon
|
HKLM\...\Uninstall\hMailServer_is1 |
Nothing. See step 11 |
The 32-bit view is not a mistake and not optional. The server reads that value
through KEY_WOW64_32KEY (Common/Util/Registry.cpp:67), and the installer
writes it there for the same reason
(hmailserver/installation/section_registry.iss). Writing the 64-bit view
changes nothing and looks like it worked.
Each of these holds a path an administrator typed. None of them is rewritten by anything, and there is no function that enumerates them.
| Table and column | What it is |
|---|---|
hm_sslcertificates.sslcertificatefile, .sslprivatekeyfile
|
Certificate and key files |
hm_domains.domaindkimprivatekeyfile, .domaindkimsecondaryprivatekeyfile
|
DKIM signing keys - by default, those the key rotation generated live under the data directory's DKIM folder |
hm_dkimrotations.dkrcurrentkeyfile, .dkrnextkeyfile, .dkrretiringkeyfile, .dkrretiredkeyfile
|
The DKIM key rotation's copy of the same paths (DkimKeyRotation.md). The rotation follows the domain's own columns at its next pass, so correct those; a retiring key's path is only used to delete it, and a stale one deletes nothing, because the file must be inside the current DKIM folder |
hm_tcpipports.portclientcertificatecafile |
The CA bundle for inbound client certificates |
hm_settings row backupdestination
|
Where backups are written |
hm_settings rows avclamwinexec, avclamwindb
|
ClamWin's scanner and database |
hm_settings row customvirusscannerexecutable
|
An external scanner command line |
hm_inisettings |
The path settings listed above, under In the settings store. On 6.3.3 it mirrors the whole [Settings] section (since schema 6011), and the file is what the server reads |
hm_archiveindex.archivepath |
One row per archived copy |
hm_quarantine.quarantinefilename |
Held messages |
hm_rule_actions.actionfilename |
A rule that runs a program or writes a file |
hm_messages.messagefilename |
Relative since 5.4; a full path an older server wrote is converted in step 0 |
Nothing in this runbook may run against a live server. Stop it, and confirm it is stopped, before anything is copied.
Copy, do not move. The old tree is the rollback.
Include Bin\hMailServerApiKeys.ini if it exists: the REST API key store lives
there (Common/Util/RestApiServer.cpp:10827), and no installer manifest mentions
it.
A copy takes the permissions of the folder it lands in, not those of the tree
it came from: robocopy without /COPY:S or /SEC, xcopy without /O, copy
and Explorer write no entries of their own on what they make. Under C:\Program Files that gives BUILTIN\Users - every account that can sign in to the computer -
Read on the new Data, Logs, Temp, Database, Events and
Bin\hMailServer.INI: every message, the logs, the built-in database and the
database's password, which setup makes private at every installation and upgrade
from 6.3.6. At a drive's root on a Windows client it gives Authenticated Users
Modify on all of it, the programs in Bin included. And the entries the old
installation's grant gave a service that runs as NT SERVICE\hMailServer are not
copied, so that service cannot write the new data, log and temp folders; a server
that cannot write its log directory starts and then says nothing. Step 6 puts both
right, from the new Bin.
So that the programs are never copied into a folder anybody else may change, make the new root first and give it what setup gives a program folder it makes - SYSTEM and the Administrators full control, the Users Read & execute, nothing inherited - before anything is copied into it. From an elevated prompt:
mkdir "<new root>"
icacls "<new root>" /inheritance:r /grant:r *S-1-5-18:(OI)(CI)F *S-1-5-32-544:(OI)(CI)F *S-1-5-32-545:(OI)(CI)RX
robocopy "<old root>" "<new root>" /E
The SIDs are SYSTEM's, the Administrators' and the Users', the same in every
language of Windows. Where no user may read the folder the new root is made in,
leave out the Users' grant at the end, as /ProtectFolders does. robocopy ends
in 1 when it has copied files, and in 8 or more when something could not be copied.
Edit <new root>\Bin\hMailServer.ini and change the keys from step 1's
hMailServer.ini table whose value was under the old root: the [Directories]
folders and [Database] PostgreSQLSslRootCert and TlsCaFile. Nothing else of
that table; a path in [SettingsOverride] and the two trust files named below move
here too.
Not the settings-store paths. Editing one of them in the [Settings] section
changes nothing: at the next start the stored value - the old path - is used and
the edit is named in the error log as HM5804. A service that may write the
file - LocalSystem, or an account given write to it - puts the line back to the
old path, and the message says so. One that may only read it -
NT SERVICE\hMailServer, to which the file is read-only
(The Service Account, hMailServer.INI is read-only to the
service) - leaves the edit in the file and says that it has. HM5804 does not
name it again; the application log's line on the read-only file, which a
reinitialise writes where application logging is on, counts it by name among the
settings whose line there is older than the stored value. The old path goes on
being used, and the file shows a path the server is not using. They move in step
9, once the moved server is running.
What is still read from this file, and only from it, is what the server needs to
reach the database or to let an administrator in without one: [Directories],
[Database] and [Security] (the administrator password and its second factor),
with the language list in [GUILanguages]. So are the sections the settings store
has not taken over - [LDAP], [SendingLimits] and [SendingLimitsOverrides],
none of which holds a path; a [SettingsOverride] section, if you have one, which
is applied over the store at every start (a path in it moves here too); and six
[Settings] keys kept out of the database on purpose: PasswordPepper, which is
not a path, and from 6.3.6 the live update's trust, of which
UpdateTrustRootsFile and UpdateLogPublicKeyFile are paths and move here too.
The file is not looked for beside the running hMailServer.exe first. It is
Bin\hMailServer.ini under the folder the registry's InstallLocation names, in
the 32-bit view (step 5), and only when that value cannot be read the one in the
folder the executable is in (Utilities::GetBinDirectory,
Common/Util/Utilities.cpp:156; IniFileSettings::GetInitializationFile). Until
step 5 has repointed that value, the server - and hMailServer.exe run from the
new Bin - reads the old tree's file, not the copy you have just edited; from
step 5 on it reads the copy.
On 6.3.3 and earlier that covers the [Settings] keys too: the file wins over the
hm_inisettings mirror when the file is what changed, so the moved server takes
the new paths. Since 6.3.4 it does not. The
database is the store for [Settings] from schema 6042, and a value edited into the
file is reported as HM5804 at the next start and the stored value used instead, the
line put back where the service may write the file - so ArchiveDir,
AcmeCertificateDirectory, the certificate and key files and the other
[Settings] rows of step 1 but the two trust files would go on pointing at the
old tree. Edit [Directories] and [Database] in the file as above, and
change the [Settings] paths in one of two ways:
-
In step 9, once the moved server is running: store the new values in the
Control Panel, with
PUT /api/v1/settings/ini/{name}, withhmctl ini setor with COMSettings.SetIniSetting. Each is applied at once, but for the listeners' certificate paths and the ACME certificate directory, which wait for a reinitialise; until then the server runs on the old paths, which still resolve because step 3 copied the tree. The store also writes the new value into the file's copy where the service may write the file, and it leaves the database holding the new paths: going back to the old tree means storing the old values again. -
Or at the first start: put the new values in a
[SettingsOverride]section of the newhMailServer.ini, which is applied over the store and named in the error log as HM5807; then, with the moved server running, store the same values as above (each is reported as HM5808, stored but shadowed, until the override goes) and remove the section before the next restart.
PasswordPepper is one exception: it lives in the file only, is not a path, and
must simply come across with the copied file. From 6.3.6 the live update's trust
is read from the file alone too, and of its keys UpdateTrustRootsFile and
UpdateLogPublicKeyFile are paths, changed there in step 4.
From an elevated prompt:
reg add "HKLM\SOFTWARE\hMailServer" /v InstallLocation /t REG_SZ /d "<new root>" /reg:32 /f
/reg:32 is the whole of the point. See step 1.
From an elevated prompt, from the new Bin:
hMailServer.exe /Register
hMailServer.exe /ProtectFolders
/Register repairs the service's ImagePath
(ServiceManager.cpp:162 → ChangeServiceConfig), all 79 LocalServer32
registrations, the 15 InprocServer32 ones, and the type library's win64 and
HELPDIR values, and keeps the account the service runs as. It must run elevated,
and it must run from the new tree - it registers the path of the executable that
runs it.
/ProtectFolders gives the new tree the permissions setup gives an installation,
which a copy by hand does not (step 3): setup runs it at every installation and
upgrade from 6.3.6, and nothing else runs it for you. It finds the folders from where the
hMailServer.exe that runs it is, never from hMailServer.ini, and:
- makes the program folder's own
Data,Logs,Temp,DatabaseandEvents, with everything in them, andBin\hMailServer.INIprivate - SYSTEM and the Administrators full control,NT SERVICE\hMailServerwhat the grant gives it and Read onhMailServer.INI, and nothing the folder above hands down, the Users' Read above all; - from 6.3.7, hardens the program folder itself,
BinandControlPanel: SYSTEM and the Administrators may change them, the Users may read and run them where every user could read them before, and nobody else may change them, whatever a drive's root hands down; a folder somebody else owns is given to the Administrators.
Each line it writes says made private, already private, hardened, already hardened or FAILED with the reason, and it ends in 0 when each was made private
or hardened, or was already; 1 when one could not be; 2 when it was not run from an
installation's Bin; 3 when it was not run as an administrator. Run it after step
4, not before: an editor that saves hMailServer.ini by writing a new file and
renaming it over the old one gives it Bin's permissions again. A folder that
[Directories] names outside the new root - a data folder kept on another drive -
is not changed, only checked: where Users, Authenticated Users, Everyone or another
group of every user may read it, a CHECK line names them, with the two icacls
that close it. hMailServer.exe --report-only /ProtectFolders says what it would change, and changes nothing. What it does, and
why, is in The Service Account, What the other users of the
computer may read.
A service that runs as anything but LocalSystem - sc qc hMailServer names
its account on the SERVICE_START_NAME line, NT SERVICE\hMailServer for the
virtual account - needs its folders granted again, as setup grants them after
/ProtectFolders:
hMailServer.exe /GrantServiceAccess
It gives the account Modify on the new Data, Logs, Temp and Database, Read
on the program's other folders and Modify on Bin\hMailServerApiKeys.ini, and
reports what the settings file and the database name that those grants do not
reach. Until step 9 has moved them, that includes every path the database still
holds into the old tree, a certificate's or a DKIM key's: it lists each, OUTSIDE,
NOT APPROVED or REFUSED, and ends in 4 or 5, its grants made all the same. Run
it again after step 9, when its report no longer names the old tree. A data
folder kept outside the new root is granted only once approved, with
--approve-modify "<folder>" before the verb (The Service Account,
Approving a folder).
Start the service and read the application log. Since 6.3.4, also
read the ERROR log: an HM5804 there names a [Settings] path that was edited in
the file and not stored, and is still pointing at the old tree.
A server whose anchors are stale reports Running and serves an empty
configuration. That failure is silent, and it is the one this whole document
exists to prevent: build/preflight-tests.ps1 carries a check for it precisely
because it has happened. Confirm over COM or in the Control Panel that the
Installation paths section of Monitoring & troubleshooting → Diagnostics
shows the new paths, that your domains are listed, and
that the log file you are reading is under the new log directory.
Run Test message file locations again, on the moved server, as soon as it has started. It should report that relative message paths are stored for all messages, as it did at the end of step 0.
ERROR: Full paths are stored in the database here means that step 0's
conversion was not made or not finished. Those rows still name files in the old
tree, and the moved server reads them there - a full path is used as it is
(PersistentMessage::GetFileName) - so the old tree must stay where it is (step
12). Do not run the Data Directory Synchronizer against the new data directory:
none of those rows matches a file there, and it would import each of those
messages again (step 0). Instead, as the server's own message about such a row
says (MessageStoreMaintenance.cpp), convert them in the old data folder the way
step 0 converts a row that names a file outside the data directory, the old data
folder being the folder the rows named and the new one the data folder you were
using. Step 0's points 2 to 4 apply as they stand; its point 1 is not needed,
since the account the service runs as wrote the old data folder before the move,
and nothing since has changed what it may do there. So the new data folder's
copy of each file the old one holds outside the layout is deleted first, the mail
is held still, DataFolder in the new hMailServer.ini is put back to the old
data folder and the synchronizer run there; then the service is stopped, the new
data folder given what the old one has and it lacks
(robocopy "<old data folder>" "<new data folder>" /E /XC /XN /XO) and
DataFolder put back to it. After each of the two edits of the file, run
hMailServer.exe /ProtectFolders from the new Bin before the service starts
on it (step 6), and at the end run the diagnostic again. The synchronizer is the
supported converter; an UPDATE hm_messages SET ... is not, because the rows are
not uniform and the file layout is derived, not stored.
Those copies go before the run because the synchronizer first moves under a new
name each file outside the layout that it converts or imports, and the copy back
removes nothing: the copy step 3 made would stay in the new data folder under the
old name, where no row names it - a partial name names a file in the layout alone
(PersistentMessage::GetFileName) - and a later run of the synchronizer's import
mode on the moved server would import each one in an account's folder as a new
message in its Inbox, as it does any file no row names, and, with the public
folders included, list each one in #Public as skipped.
Nor does this put right what the moved server has done since it started, which is why the check comes at once. The old data folder still holds its copy of each file the moved server has since deleted or moved in the new one - a message deleted, a queued one delivered - and where no row names that copy any longer the run imports it again, as step 0 says of any such file, a queued one to be delivered again to its local recipients. And where a message whose row held a full path was deleted, the file deleted was the old tree's, so the copy step 3 made stays in the new data folder, where no row names it either.
For each row from step 1's settings-store and database tables that pointed under the old root:
-
The settings-store paths. In the Control Panel under Settings, whose
search box finds each one by name; or
Settings.SetIniSettingover COM,PUT /api/v1/settings/ini/{name}, orhmctl ini set <name> <value>. Each stores the new value in the database, writes it into the file's copy as well where the service may write the file (underNT SERVICE\hMailServerthe copy is left as it is,IniSettingStore::WriteSetting), and applies it - except the listeners' certificate paths and the ACME certificate directory, which are bound when the servers start and wait for a reinitialise (Apply now, orPOST /api/v1/server/reinitialize; SettingsApply.md lists them). Until then the server runs on those old paths, which still resolve because step 3 copied the tree rather than moving it. Do not make the old tree unreachable (step 12) until that reinitialise has picked up the new paths. -
TLS certificates. Certificates & encryption → TLS certificates: select
each certificate whose files moved, Edit…, and give it the new certificate
and private key file names. It keeps its id, so every TCP/IP port that presents
it keeps it; the ports read its files again at the next reinitialise (Apply
now) or restart. Over COM, setting the certificate's
CertificateFileandPrivateKeyFileand calling itsSave()does the same; the REST API adds and deletes a certificate, and changes none. Do not delete a certificate and add it again: the one added is a new certificate, and each port that presented the deleted one is left with none - an SSL/TLS or STARTTLS port without a certificate does not start (HM5113) until another is chosen for it on Connections & protocols → TCP/IP ports. The page's question before a delete names those ports. - DKIM keys. Domain → DKIM, both the primary and, if used, the secondary. A domain whose key rotation manages its keys refuses that edit: cancel the next key, retire or wait out the old one and switch the schedule off first, or let the rotation make a new key under the new data directory.
- Client-certificate CA bundle. Connections & protocols → TCP/IP ports, per port. A port set to require a client certificate refuses every client when its bundle cannot be loaded.
- Backup destination, ClamWin, custom scanner, rule actions. Each in its own page.
-
Archive index.
hm_archiveindex.archivepathholds one row per archived copy and is matched on the exact string by retention, legal hold and the address eraser. Do not bulk-rewrite it. If the archive directory moved, move the archive back or accept that pre-move rows point at the old location; there is no supported converter for this column.
- Send and receive a message.
- Complete a TLS handshake on every port that has a certificate.
- Connect on any port whose client-certificate policy is require.
- Run a backup to completion. It refuses outright when its destination is unwritable, which is the check you want.
- Run an event script, if you use them.
- Check the log directory is being written.
-
Add/Remove Programs still points at the old
unins000.exe. Do not editunins000.dat; it is a compiled record, and a hand edit produces an uninstaller that removes the wrong tree. Never run the old uninstaller either: it stops the service and runs the old tree'shMailServer.exe /Unregister, which removes the service and the COM registration that the new tree now holds. The supported repair is to run the release's setup again at the new root, which rewrites the uninstall metadata - below. - Start-menu shortcuts hold absolute targets. Re-create them, or run setup again at the new root, which writes them for it.
- Message content. The old path appears inside messages the server itself wrote - a delivery report, for example, names the file it could not find. Those are historical records of what happened. Never rewrite message content, and be careful with any "search and replace across the installation" approach for exactly this reason.
Running setup again at the new root. Setup run over an installation shows no
folder page and installs into the folder it finds - the old one, from the uninstall
entry - so it is given the new root with /DIR. And an installation that began in
C:\Program Files (x86), as a 32-bit installation, has an uninstall entry in the
registry's 32-bit view, which every upgrade since has left there and whose folder
setup takes whatever /DIR says. Where the first line below finds that entry, take
it away with the second, then run setup, from an elevated prompt:
reg query "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\hMailServer_is1" /reg:32
reg delete "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\hMailServer_is1" /reg:32 /f
hMailServer-<version>-x64.exe /DIR="<new root>"
Setup finds the settings file in the new Bin and goes on as an upgrade of the
moved installation, the service's account kept. It writes the uninstall entry and
the shortcuts for the new root, runs /Register there - sc qc hMailServer then
names the new Bin - and runs /ProtectFolders and, for a service that runs as
anything but LocalSystem, the grant again. This is the same step as step 3 of
Reinstalling into a new folder in The Service Account.
Keep it, unreachable by the service, for at least one full backup cycle. Once it is deleted, the only rollback is the step 0 backup. Delete it by hand, never with its uninstaller (step 11).
Keep it private while it is there. Nothing runs /ProtectFolders on a folder no
installation is in, and where 6.3.5 or earlier left its Data, Logs,
Database and Bin\hMailServer.INI taking the permissions of the folder they are
in, every user may still read the mail, the logs, the built-in database and the
settings file there: make it the Administrators' and SYSTEM's alone as step 4 of
Reinstalling into a new folder in The Service Account does.
The Control Panel's Monitoring & troubleshooting → Diagnostics page (Monitoring and Health §16.8) has an Installation paths
section that lists the registry root, every [Directories] key, the derived
directories, and every path-shaped setting the running server holds but
[Database] TlsCaFile, with the ones that do not exist marked. It changes
nothing; it is there so that the inventory in step 1 can be read off a running
server rather than assembled by hand.
This came up as GitHub issue 158, on the project's former home.
The short answer is that ProgramFolder is not the root of anything: it is one
of six independent absolute strings, and the registry holds a seventh copy that
the server actually uses to find its own INI file. From 6.3.0 a relative
value in the five non-program directories is resolved against the program folder,
so a new installation can be written to be movable; an existing installation's
absolute values are left exactly as they are, because rewriting a configured
server's paths during an upgrade is not something an upgrade may do.
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