Skip to content

Relocating an Installation

Christopher Holloway edited this page Oct 21, 2026 · 16 revisions

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.


0. Before you touch anything

  1. Take a backup and verify it restores. Everything below is reversible only while the old tree and a good backup both exist.

  2. 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 reports ERROR: 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 reports Messages 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 directory DataFolder names, 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 .eml or .hma file 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 DataFolder names: PersistentMessage::GetMessageID looks 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, with hMailServer.exe /ProtectFolders run from Bin after each edit of hMailServer.ini, as step 6 says:

      1. Let the service write that folder. A service that runs as anything but LocalSystem (step 6) needs Modify there. /GrantServiceAccess gives it that on the program folder's own Data, and on another folder once approved - from an elevated prompt in Bin, 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's Data in C:\Program Files (x86) - is listed REFUSED: 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.
      2. 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.
      3. Hold the mail still, then convert. While DataFolder names 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 put DataFolder at 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.
      4. 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 /E copies 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), put DataFolder back to the data folder you were using, run /ProtectFolders and start the service. Then tick the three services again, save, and restart the service.
      5. 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.
    • 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.

  3. Write down what you are about to strand. The inventory is in step 1. Doing it afterwards means finding out from a failure.


1. The inventory

In Bin\hMailServer.ini

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.

In the settings store (hm_inisettings)

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.

In the registry

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.

In the database

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

2. Stop the service

Nothing in this runbook may run against a live server. Stop it, and confirm it is stopped, before anything is copied.


3. Copy the tree

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.


4. Rewrite the file keys in hMailServer.ini

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}, with hmctl ini set or with COM Settings.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 new hMailServer.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.


5. Repoint the registry root

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.


6. Re-register the server, and make its folders private

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, Database and Events, with everything in them, and Bin\hMailServer.INI private - SYSTEM and the Administrators full control, NT SERVICE\hMailServer what the grant gives it and Read on hMailServer.INI, and nothing the folder above hands down, the Users' Read above all;
  • from 6.3.7, hardens the program folder itself, Bin and ControlPanel: 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).


7. Start it, and check what it read

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.


8. Check that no full message paths are left

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.


9. Fix the database-borne paths

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.SetIniSetting over COM, PUT /api/v1/settings/ini/{name}, or hmctl 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 (under NT SERVICE\hMailServer the 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, or POST /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 CertificateFile and PrivateKeyFile and calling its Save() 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.archivepath holds 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.

10. Verify before deleting anything

  • 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.

11. What stays broken, and why

  • Add/Remove Programs still points at the old unins000.exe. Do not edit unins000.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's hMailServer.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.


12. Keep the old tree

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.


Seeing where the paths are, without doing any of this

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.

Why the paths are not one setting

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.

Home — full index

Start here

1. Install and run

2. Secure it

3. Operate it

4. Extend it

5. Contribute to it

Look it up — from any journey


About this wiki

Clone this wiki locally