Skip to content

Settings Reference

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

Settings Reference

Every key of hMailServer.INI as the server reads it today: section, default, what it does, which code consumes it and whether it is read at startup or live. Generated from IniFileSettings.cpp and the other readers of the file, with the source line for each fact. File and line references point into the repository at the commit this page was written from (6.2.24, master of 4 September 2026). Sections headed Unconfirmed or Contradictions record what could not be verified or where documents disagreed, and are left in on purpose. 6.3.7, the latest release (17 October 2026, schema 6108), is not yet reflected here: the keys were last re-checked at v6.3.6 (below).

Every fact below was checked against the code on disk today. Paths are relative to hmailserver/source/Server/ unless stated. IFS.cpp = Common/Application/IniFileSettings.cpp, IFS.h = Common/Application/IniFileSettings.h, ISS.cpp = Common/Application/IniSettingStore.cpp. Line numbers are from the working tree at commit 4d5da3f; passages corrected on 2026-09-08 cite the tree at 40ae9491d, from which 6.2.28 was cut later the same day. Anything marked "new in 6.2.28" ships for the first time in that release. HttpProxy (section 9.16) landed after 40ae9491d and is cited from the release commit f088336c2, where it is IFS.cpp:492 and pushes every later [Settings] read in that file down one line.

settings.md at the repo root (dated 2026-06-15, server 6.2.5) was used only as a hint; where it disagrees with the code the code is recorded and the disagreement is listed under "Contradictions".

Re-checked on 15 September 2026 at commit 3233b0cc4, after 6.3.3: the set of [Settings] keys IniFileSettings.cpp reads is the same as in v6.3.3. No [Settings] key has been added since 6.3.3 - and, by a rule adopted on the same day, none will be. The project's CONTRIBUTING.md says that a new setting goes in the database, in hm_settings through the COM Settings object, with a Control Panel editor, and that no new key is added to the [Settings] section of hMailServer.ini; build/check-ini-coverage.py fails on one (in GitHub's CI until 18 September 2026, by hand since, and now in the checkers job of the project's .gitlab-ci.yml). In 6.3.3 every key in section 9 is read from the file, with the database mirror of section 4. From 6.3.4 - and in 6.3.7, the current release - schema 6042 makes the database the store for those keys: they are read from and written to hm_inisettings, the [Settings] section of the file is a copy the server rewrites where it may write the file, and an edit to a key the database holds is not used at the next start and is reported as HM5804, its line put back where the service may write the file - not under NT SERVICE\hMailServer, to which the file is read-only (section 4a; The Service Account). The key names, defaults, validation and consumers below are the same either way; what changes is where a value is changed. PasswordPepper stays in the file only, and [Database], [Directories], [Security], [GUILanguages], [LDAP], [SendingLimits] and [SendingLimitsOverrides] stay in the file. See Settings Overview for the two stores side by side.

Re-checked on 24 September 2026 at master 540347500: the set of [Settings] keys is the same again - 241 read by the loader, and UseLanguage read per call. What was added around them since the store is in two new sections: 4b, when a save applies each key, the deepest level it may be set at and its name in the environment; and 17, the rows of hm_settings added since 6.3.3, which is where every new setting now goes. One key was added to the file, and it is a bootstrap key: [Database] UpgradeOnStart (section 5).

Re-checked on 26 September 2026 at 92b8f9f63 (schema 6103, Constants.h:1205), and at the tag v6.3.4 (51e2dbda0), at schema 6104, and on 5 October 2026 at v6.3.6 (e3c448aa6, 6 October 2026), at schema 6108: the [Settings] keys are the same 241 again, with UseLanguage read per call, and IniSettingsBaseline.h still lists 241 - 211 applied at once, 28 at a reinitialise, 2 at a restart. Three keys were added to the file outside [Settings], each read before the database can be: [Database] TlsMode and TlsCaFile (section 5, database TLS) and [Security] StoredSecretsKey (section 8, secrets at rest). Twenty-seven rows were added to hm_settings (section 17, 191 in all at 92b8f9f63): storage compression, object storage, mailbox encryption at rest, PST import and the SNMP agent; the tag adds two more, PasskeyAllowedOrigins and WebPushLoopbackHttpForTests (193 in all). ServiceAccountName and ServiceAccountPassword are still read, and no longer used by /Register (section 9.1).

Timing legend used in the tables:

  • S — latched into a member by IniFileSettings::LoadSettings(); the consumer calls the getter at the moment of use, so the value changes whenever LoadSettings re-runs (see "When the file is read").
  • S! — consumed only when a listener/task is constructed at server (re)start, so a LoadSettings re-run on its own does not apply it; a service restart or Application.Reinitialize does.
  • L — read live from the file by its consumer, not through LoadSettings (own cache rules noted).

The legend describes 6.3.3, where a change reaches the running server only when LoadSettings runs again. Since 6.3.4 a save publishes, and each key's applies mark - now, reinitialize or restart, section 4b - says when a saved value is in effect; the S and S! marks below still say how the code reads each key, and section 4b names every key that does not apply on save.

The legend drawn, so that "S", "S!" and "L" have a picture behind them:

flowchart LR
    subgraph SOURCES["Sources of truth"]
        FILE["hMailServer.ini in the Bin directory"]
        MIRROR[("hm_inisettings, mirroring the Settings section only")]
        APIKEYS["hMailServerApiKeys.ini"]
        LIMITS["SendingLimits and SendingLimitsOverrides sections"]
        LDAPS["LDAP section"]
    end

    FILE --> LOAD["IniFileSettings::LoadSettings"]
    MIRROR --> LOAD
    LOAD --> MEMBERS["About 200 typed members on the IniFileSettings singleton"]

    MEMBERS --> S["S - the consumer calls the getter at the moment of use, so a later LoadSettings changes the answer"]
    MEMBERS --> SB["S! - consumed once, when a listener or task is constructed at server start"]

    APIKEYS --> L1["L - RestApiServer::LoadKeys_ reads the file bytes on every request"]
    LIMITS --> L2["L - RateLimiter re-reads on a file-timestamp change, checked at most every 2 s"]
    LDAPS --> L3["L - LdapSettings does the same"]
    FILE --> L4["L - UseLanguage on every get, LogFolder once then cached"]
Loading
  • [Database], [Directories], [Security] and [GUILanguages] are read from the file only; the mirror overlays [Settings] and nothing else (source: IFS.cpp:803-820, 836-856; IFS.h:760-766).
  • Nothing in SetIniSetting re-runs LoadSettings, which is the whole of the "persist now, apply on the next start" contract (source: IFS.h:59-65). That is 6.3.3; since 6.3.4 every write rebuilds the settings from the store and publishes them (section 4b).

1. Where the file is and how it is found

  • File name is literally hMailServer.ini, in the server's Bin directory (source: IFS.cpp:863-878 GetInitializationFile).
  • The Bin directory is HKLM\SOFTWARE\hMailServer\InstallLocation + \Bin if that registry value exists; otherwise the directory of the running hMailServer.exe (source: Common/Util/Utilities.cpp:103-122 GetBinDirectory).
  • The path is computed once and cached in a static (source: IFS.cpp:865-877).
  • The installer writes it to {app}\Bin\hMailServer.INI (source: hmailserver/installation/section_ini.iss:1-8); on upgrade it moves a pre-existing {win}\hMailServer.ini into {app}\Bin (source: installation/hMailServerInnoExtension.iss:1043-1055).
  • COM Application.InitializationFile exposes the path: hMailServer/hMailServer.idl:1702; InterfaceApplication.cpp:455 returns GetInitializationFile().
  • A second, separate ini file lives beside it: hMailServerApiKeys.ini (REST API keys; section 12).
  • Test harness candidate locations: hmailserver/test/RegressionTests/Shared/IniFileSetting.cs:34 CandidateDirectories (not read in detail).

There is no search path and no fallback file. The name is fixed, the directory is decided once, and everything else in this reference is read from whatever that resolves to:

flowchart TD
    NEED["Anything that reads or writes a key:<br/>LoadSettings, UseLanguage, LogFolder,<br/>a Control Panel save"] --> CACHE{"Path already<br/>worked out this process?"}
    CACHE -- yes --> USE["that directory + hMailServer.ini"]
    CACHE -- no --> REG{"HKLM\SOFTWARE\hMailServer,<br/>value InstallLocation -<br/>does it exist?"}
    REG -- yes --> BIN["InstallLocation + \Bin"]
    REG -- no --> EXE["the directory the running<br/>hMailServer.exe sits in"]
    BIN --> USE
    EXE --> USE
    USE --> ONCE["Kept for the life of the process.<br/>Moving the file, or changing the registry value,<br/>does nothing until the service restarts"]
Loading

Two consequences worth knowing. The installer writes InstallLocation, so on an installed machine the registry branch always wins and running a copy of hMailServer.exe from somewhere else still reads the installed file - which is why the upgrade path moves a pre-existing {win}\hMailServer.ini into {app}\Bin rather than leaving two files that look equally plausible. And because the path is resolved once, a service that was started before the registry value changed keeps reading and writing the old file for as long as it runs.

2. Read mechanics (apply to every key)

  • Values are read with GetPrivateProfileString / GetPrivateProfileInt (source: IFS.cpp:831, 858). Consequences that the code itself documents: an absent key yields the caller's default, but Key= (present, empty) reads as 0 through GetPrivateProfileInt (source: IFS.h:81-85; ISS.cpp:463-467); a value such as 5x reads as 5 and x as 0 (source: IFS.cpp:847-853).
  • Windows treats section and key names case-insensitively for the file API; the hm_inisettings mirror matches them case-sensitively (source: ISS.cpp:206-222).
  • String buffer is 4096 chars in ReadIniSettingString_ (source: IFS.cpp:829-831) — chosen because DPAPI-protected secrets are long base64 envelopes. Exception: [Directories] LogFolder is read with a 255-char buffer (IFS.cpp:885-886) and [Settings] UseLanguage with 255 (IFS.cpp:937-939).
  • Booleans are ReadIniSettingInteger_(...) == 1, so only the literal 1 is true; 2, yes, true are false (source: every == 1 in IFS.cpp:103-674).
  • Writes go through WritePrivateProfileString (source: IFS.cpp:692-702) and, for the [Settings] section via the store, are flushed with the null-null-null call (source: ISS.cpp:178-184).

3. When the file is read (startup vs. live)

  • LoadSettings() reads every key in sections 5-9 below into typed members in one pass (source: IFS.cpp:90-679). It runs:
    1. First thing in Application::InitInstance, file-only (source: Common/Application/Application.cpp:132).
    2. Again in InitInstance after the database is open and its schema accepted, this time with the hm_inisettings overlay (source: Application.cpp:177-178, comment 163-176).
    3. On Application::Reinitialize (COM Application.Reinitialize, InterfaceApplication.cpp:93-100), which calls ForgetDatabaseSettings() then InitInstance again (source: Application.cpp:924-942).
    4. On every COM Settings.Directories get (source: COM/InterfaceSettings.cpp:1129) and on Database.CreateInternalDatabase (source: COM/InterfaceDatabase.cpp:488) — these re-latch all ~200 members from the file (+overlay) on a COM thread.
    5. On hMailServer.exe /Register (no database) so ServiceAccountName/Password reach the SCM (source: hMailServer/hMailServer.cpp:216-249).
  • The house contract for [Settings] changes made through COM/Control Panel in 6.3.3: persist now, apply on the next start (source: IFS.h:59-65). Nothing in SetIniSetting calls LoadSettings. Since 6.3.4 the contract is the applies mark instead: a write stores the value and publishes a new block of values, and a key marked now is in effect when the write returns (section 4b).
  • Keys that are NOT latched but read live: UseLanguage (every call, IFS.cpp:1110-1117), LogFolder (lazily once, then cached until SetLogDirectory, IFS.cpp:1056-1066), the whole [SendingLimits]/[SendingLimitsOverrides] and [LDAP] sections (file-timestamp check at most every 2 s; section 10, 11), and hMailServerApiKeys.ini (every request; section 12). CalDavRedirectUrl/CardDavRedirectUrl are latched like every other key since the HTTP foundation (new in 6.2.28); WebServicesServer keeps a 60-second cache of the latched value (Common/Util/WebServicesServer.cpp:67, 1062-1090).
  • LoadSettings ends by calling DiskSpace::InvalidateCache() because DataFolder may have changed (source: IFS.cpp:676-678).
  • Constructor defaults differ from LoadSettings defaults for a few members and only matter for an error reported before LoadSettings has run: preferred_hash_algorithm_ 3 vs 4, log_level_ 0 vs 9, no_of_dbconnections_ 0 vs 5, dnsbl_checks_after_mail_from_ false vs 1, quick_retries_Minutes 0 vs 6, add_xauth_user_ip_ false vs 1 (source: IFS.cpp:23-82 vs 144-398). WindowsEventLogEnabled/Level are deliberately identical in both (IFS.cpp:74-78).

The five triggers, in the order a running server meets them:

sequenceDiagram
    autonumber
    participant SCM as Windows service control
    participant APP as Application::InitInstance
    participant IFS as IniFileSettings
    participant DB as Database
    participant ISS as IniSettingStore
    participant COM as COM clients

    SCM->>APP: start
    APP->>IFS: LoadSettings - file only
    Note over IFS: the database settings are in the file, so this pass is what makes opening it possible
    APP->>DB: connect, check the schema version
    DB-->>APP: open
    APP->>IFS: LoadSettings again, now with the mirror
    IFS->>ISS: Synchronize the Settings section with hm_inisettings
    ISS-->>IFS: the resolved values
    APP->>APP: StartServers - listeners and tasks read the S! values here
    COM->>IFS: Settings.Directories get
    IFS->>IFS: LoadSettings - all members re-latched on a COM thread
    COM->>IFS: Database.CreateInternalDatabase
    IFS->>IFS: LoadSettings
    COM->>APP: Application.Reinitialize
    APP->>IFS: ForgetDatabaseSettings, then InitInstance again
    Note over SCM,COM: hMailServer.exe /Register also calls LoadSettings, with no database, so ServiceAccountName and ServiceAccountPassword reach the SCM
Loading
  • The two-pass start is not an accident: the [Database] keys live in the file, so the first pass has to happen before the database can be opened, and the mirror can only be applied after (source: Application.cpp:163-178 comment).
  • Application.Reinitialize is therefore the supported way to apply a [Settings] change without stopping the service, and it is what the regression suite uses after writing a key (source: hmailserver/test/RegressionTests/API/RestApiSelfService.cs _application.Reinitialize()).

4. The hm_inisettings mirror (schema 6011+) and the COM surface

Everything in this section is how 6.3.3 and earlier releases behave (schema 6011 to 6040), with line references from the 6.2.28 tree. 6.3.4 replaces the merge with the store of section 4a.

  • LoadDatabaseSettings() reconciles [Settings] with table hm_inisettings (columns inisettingname ≤100 chars, inisettingvalue ≤4000, inisettingfilevalue) via IniSettingStore::Synchronize (source: IFS.cpp:704-724; ISS.cpp:113-136, 195-350). Table added in schema 6011 (hmailserver/source/DBScripts/Upgrade6010to6011*.sql; Tools/DBUpdater/SchemaVerification.cs:138-142). REQUIRED_DB_VERSION was 6031 when this bullet was written (Common/Application/Constants.h:173); it is 6040 in 6.3.3 and 6104 in 6.3.4 (Constants.h:1224 at the tag, 6103 at 92b8f9f63), and Changes-Since-6210 section 2 has the steps between. Schema history since 6025: 6026 seeds the CreateDefaultSpecialUseFolders setting; 6027 adds hm_domains.domainmessageretentiondays and hm_accounts.accountmessageretentiondays; 6028 creates hm_metricsamples; 6029 creates hm_archiveindex; 6030 adds foreign keys with ON DELETE CASCADE between accounts/aliases and their domains; 6031 adds hm_fetchaccounts.famirrorfolders (hmailserver/source/DBScripts/Upgrade6025to6026*.sql ... Upgrade6030to6031*.sql).
  • Only the [Settings] section is overlaid; [Database], [Directories], [Security], [GUILanguages] always come from the file (source: IFS.cpp:803-820, 836-856; IFS.h:760-766).
  • Merge rules (source: ISS.cpp:227-316): file changed → file wins and row updated (both changed → HM5804 conflict report); row changed but file unchanged → value written INTO the file (HM5803 if the write fails); key deleted from file with row unchanged → row deleted; row changed and key absent from file → written back to file. If the table cannot be read the server runs on the file alone and reports HM5802.
  • Keys >100 chars or values >4000 chars are skipped with HM5800/HM5801 (source: ISS.cpp:86-103). Comment lines (;/#) are ignored (ISS.cpp:64-68).
  • Backups include <IniSettings><Setting Name= Value=/> from the table (ISS.cpp:481-505); restore writes both row and file and is authoritative (ISS.cpp:509-580, HM5805 on file-write failure).
  • COM: Settings.GetIniSetting(Name), Settings.SetIniSetting(Name, Value), Settings.DeleteIniSetting(Name), Settings.IniSettingNames (CRLF-joined) (source: hMailServer/hMailServer.idl:735-741; COM/InterfaceSettings.cpp:2660-2760). Name must pass IsStorableName (non-empty, ≤100, no = [ ] or line break, no surrounding whitespace); value must pass IsStorableValue (≤4000, no line break) (ISS.cpp:392-426). SetIniSetting writes the FILE first and fails without changing anything if that fails (ISS.cpp:428-460, HM5806 if the mirror cannot be read afterwards). DeleteIniSetting removes the line (not Key=) and the row (ISS.cpp:456-479).
  • The Control Panel reads the file directly when it is on the same machine and falls back to GetIniSetting over COM otherwise (source: Tools/ControlPanel/Services/IniFeatureStore.cs:120-140; Services/ServerSession.cs:88-101). In 6.3.3 it also writes the file directly when it is on the same machine, and the merge picks the change up at the next start. Since 6.3.4 it reads over COM and falls back to the file, and writes over COM only; without a session it refuses with "Settings are stored in the server's database, so changing one needs a connection to the server" (IniFeatureStore.cs).

Table shape (schema 6011, DBScripts/Upgrade6010to6011*.sql):

hm_inisettings
├─ inisettingid         int identity(1,1), primary key
├─ inisettingname       nvarchar(100), UNIQUE  ← a name over 100 chars is skipped with HM5800
├─ inisettingvalue      ntext                  ← the value in force; over 4000 chars, HM5801
└─ inisettingfilevalue  ntext                  ← what the file said the last time the two agreed

inisettingfilevalue is the whole trick: it is what lets the reconciliation tell "the file changed" from "the row changed", and "the operator deleted a key" from "a restore added one".

flowchart TD
    START["Synchronize, at every LoadSettings that has a database"] --> READ{"Could hm_inisettings be read?"}
    READ -- no --> E802["HM5802 Medium. Run on the file alone. Settings stop being included in backups."]
    READ -- yes --> P1["Pass 1: every key present in the file"]
    P1 --> Q1{"Is there a row for this key?"}
    Q1 -- no --> INS["Insert a row with value = filevalue = the file's value"]
    Q1 -- yes --> Q2{"file value differs from inisettingfilevalue?"}
    Q2 -- yes --> Q3{"inisettingvalue also differs from inisettingfilevalue?"}
    Q3 -- yes --> E804["HM5804 conflict. The file wins. Row updated to the file's value."]
    Q3 -- no --> W1["The file wins. Row updated."]
    Q2 -- no --> Q4{"inisettingvalue differs from inisettingfilevalue?"}
    Q4 -- yes --> W2{"Write that value into the file"}
    W2 -- succeeded --> OKR["Row now agrees. The database-side change is applied."]
    W2 -- failed --> E803["HM5803 Medium. The change is NOT applied. The file value is used for this run."]
    Q4 -- no --> SAME["They agree. Use the stored value."]

    P2["Pass 2: every row whose key is gone from the file"] --> Q5{"Does inisettingvalue still equal inisettingfilevalue?"}
    Q5 -- yes --> DEL["The deletion is the newer fact. Delete the row."]
    Q5 -- no --> PUSH["A database-side edit or a restore. Write it back into the file."]
Loading
  • Pass 2 used to resurrect every missing row unconditionally, and the regression suite proved that wrong in the loudest available way: a test that switched ACME on, restarted, then cleaned up by deleting its keys from the file had them written straight back, so AcmeEnabled=1 survived into every following test and 394 of them failed. Removing a key is a normal way to return a setting to its default (source: ISS.cpp comment above pass 2).
  • The name comparison in pass 1 is case-sensitive while the Windows ini API is case-insensitive. The one thing that makes visible is renaming a key's case — AcmeEnabled to acmeenabled — which looks like a new key in pass 1 and a removed one in pass 2; on a case-insensitive collation the insert collides with the not-yet-deleted row, is logged, and the next start inserts cleanly. It is left alone deliberately: matching case-insensitively would mean UPDATE/DELETE clauses that behave differently on PostgreSQL from the other three backends (source: ISS.cpp comment in pass 1).

4a. Since 6.3.4: hm_inisettings is the store (schema 6042)

Nothing in this section is in 6.3.3. Sources are the tree at 3233b0cc4, and for what a file the service may not write changes, which the tag v6.3.4 already has, the tree at 5e49abf06: IniSettingStore.cpp/.h (ISS), IniFileSettings.cpp (IFS) LoadDatabaseSettings, WriteSettingsValue and RemoveSettingsValue, and Application.cpp InitInstance/Reinitialize.

  • The step. Upgrade6041to6042*.sql is update hm_dbversion set value = 6042 and nothing else, in all four dialects; the table has had the right columns since 6011. Application::OnDatabaseConnected refuses a version mismatch in both directions, so a 6.3.3 server (6040) is refused against a 6042+ database with HM5011, and the file-wins merge of section 4 can never run against a database that is now the store.
  • When it runs. Unchanged: the first LoadSettings is file-only, then LoadDatabaseSettings() runs IniSettingStore::Synchronize once the schema is accepted, then LoadSettings again with the resolved values as the overlay (Application.cpp, InitInstance). Reinitialize calls ForgetDatabaseSettings() and runs InitInstance again, so a reinitialise - COM Application.Reinitialize, a restore, SIGHUP on Linux - reconciles exactly as a start does. The --create-database and --upgrade-database options on Linux, and DBUpdater on Windows, do not.
  • The rules, per [Settings] key in the file (ISS.cpp Synchronize, pass 1):
    • no row: adopted - inserted with value = filevalue = the file's value. This is the migration, done on the first start at 6042 for every key the section holds; there is no flag and nothing to run by hand.
    • row exists, and the file's value differs from both inisettingfilevalue and the stored value: an edit. The stored value is used, the key is named in HM5804, and the line is put back to the stored value where the service may write the file; where it may not - NT SERVICE\hMailServer, to which the file is read-only, or the Linux packages' 0640 root:hmailserver file - the line is left as edited and inisettingfilevalue takes what it holds, so that HM5804 names it once.
    • row exists, and the file already holds the stored value: used; only the bookkeeping is brought up to date.
    • row exists, the file still holds the value last written but the stored value differs (a restore, a change from another node): the stored value is written into the file where the service may write it, and otherwise the line is left as it is and counted - a reinitialise names it in the application log, where application logging is on, among the lines older than the stored value.
  • Rows whose key is missing from the file (pass 2): the line is written back where the service may write the file, and the row kept either way. Deleting a line no longer returns a setting to its default. The rule of section 4, pass 2 - "the deletion is the newer fact" - is exactly what was inverted.
  • [SettingsOverride] (IFS.cpp LoadDatabaseSettings): every key in that section of the file is applied over the resolved values, read by ReadOverrides from the file with no database call, so it applies even when the table cannot be read. Named at every start as HM5807. Not written by the server, not offered by any editor, not carried by a backup. An override is not copied into [Settings], so the stored value stays visible beside it.
  • Writes (WriteSetting, reached from COM Settings.SetIniSetting, PUT /api/v1/settings/ini/{name}, hmctl ini set, the PowerShell module's Set-HmIniSetting and the Control Panel): the value is trimmed, the row first, then the line, where the service may write the file; where it may not, the row alone, inisettingfilevalue taking what the line holds, and the line is left as it is. A failed row write fails the call and changes nothing ("The setting could not be stored. Settings are held in the database, so this means the server could not write to it."). A failed file write does not fail it: the value is stored and used, HM5806 is reported, and inisettingfilevalue is put back to what the file really holds so the next start rewrites the line quietly instead of reporting an edit. A key shadowed by [SettingsOverride] is stored anyway and reported as HM5808.
  • Removal (RemoveSetting, reached from COM Settings.DeleteIniSetting, DELETE /api/v1/settings/ini/{name} and hmctl ini delete; the Control Panel never calls it, and the server's own log line that says "delete the setting in the Control Panel" overstates it): the line first (removed, not left as Key=), then the row, where the service may write the file; where it may not and the file has the line, the line is left and the row kept holding the key's default, which the server treats the same, because a dropped row would be put back from the line at the next start, and that is said in the application log, while the removal of PasswordPepper and of a key this build does not read is refused, HM5806 (ISS.cpp RemoveSetting; The Service Account). A leftover row is repaired by the next start; a leftover line would be adopted as a new setting, which is why the order is the opposite of a write.
  • PasswordPepper is file-only (IsFileOnlyName): never adopted, never served from the store, written only to the file when set over COM or REST - refused with a sentence where the service may not write the file, NT SERVICE\hMailServer's case, where an administrator sets it with an elevated editor and restarts the service - and left out of XMLStore, so no backup carries it. A row an earlier start adopted is deleted once the file holds the value the server has been running on. Restoring an archive that carries a pepper - one taken by 6.3.3 or earlier - writes it into the file only when the file has none, and where the service may not write the file says so as HM5805, for an administrator to put it there with an elevated editor (ISS.cpp XMLLoad).
  • The whole [Settings] section stays mirrored into the file where the service may write it, because hMailServer.exe /Register reads ServiceAccountName and ServiceAccountPassword with no database open (section 3, item 5), and because a server whose table cannot be read runs on that copy (HM5802) rather than not starting. Under NT SERVICE\hMailServer, to which the file is read-only, the copy is not refreshed, and is stale for whatever reads the file itself (The Service Account).
  • [SettingsStore]. At every start the server writes two keys into a section of that name when they differ and it may write the file: Store=database, and ReadMe= a sentence saying the [Settings] section is a cache, that an edit is undone and reported, where settings are changed, and what [SettingsOverride] is for (ISS.cpp WriteFileNotice_). Keys rather than comment lines, because the profile API - the only file writer the POSIX build has - writes keys.
  • Backups. XMLStore writes every row except PasswordPepper; XMLLoad writes each archived setting into the row and the file, recording them as agreed only when the file write succeeded (HM5805 otherwise).
  • CI. build/check-ini-coverage.py compares the ReadIniSetting*_ calls in IniFileSettings.cpp with build/ini-settings-baseline.txt - 263 keys in a 289-line file: 238 [Settings], 17 [Database], 5 [Directories], 2 [Security], 1 [GUILanguages] - and fails a new [Settings] key with the rule quoted; a new key in one of the four bootstrap sections passes once it is added to the baseline in the same commit. Like the check of section 13, it matched literal calls, so the three PasswordHash* keys and UseLanguage were not among its 238. At 540347500 the baseline holds 267 keys: 241 [Settings] - the 238 and the three work factors, which it now sees - 18 [Database] (UpgradeOnStart the one added), 5 [Directories], 2 [Security] and 1 [GUILanguages], each line carrying the key's kind, its shipped default, its applies mark and its levels mark (section 4b). The checkers ran in the project's GitHub workflows until GitHub suspended the project's account on 18 September 2026; since 26 September 2026 GitLab CI's checkers job runs every build/check-*.py, this one among them (Contributing, The checkers).
  • Coverage. RegressionTests/Infrastructure/IniSettingsMirror.cs, seven tests: a stored value survives a restart; an edit to the file loses, is named once and put back; an override wins and is named at every start; a value changed behind the server is written into the file; a file-only value is adopted; the pepper stays out of the database; a value with surrounding spaces is stored as the file reads it. The full Windows suite on 3233b0cc4: 2,410 tests, 2,402 passed, none failed, 8 skipped (7 explicit stress tests and 1 ignored); Linux on 00bec4412, the same server: 1,606 tests, 1,326 passed, 0 failed, 280 skipped. The fixture has run in every gate since; the latest, batch 33's of 24 September 2026, ran 3,390 tests in two halves with none failing, and a fixture of its own, SettingsApplyOnSave, proves the applies marks of section 4b.

Changing a [Settings] key since 6.3.4, by surface:

Surface Set Return to default Notes
Control Panel (Windows) the key's editor no - clearing a box stores an empty value, which an integer reads as 0 Over COM even on the server itself; refuses without a session
REST PUT /api/v1/settings/ini/{name} with {"value":"..."} DELETE /api/v1/settings/ini/{name} Administrator password only; an API key of any scope is refused
hmctl hmctl ini set <name> <value> hmctl ini delete <name> Over REST, so the listener must already be on (Command Line)
PowerShell module Set-HmIniSetting - Over REST, the same vocabulary as hmctl
COM Settings.SetIniSetting(name, value) Settings.DeleteIniSetting(name) Windows only
Control Deck only SmtpTarpitCount, SmtpTarpitDelaySeconds, UseLanguage and RewriteEnvelopeFromWhenForwarding, through its Settings view's groups - The Deck does not call /api/v1/settings/ini
hMailServer.ini [Settings] no for a key the database holds - not used at the next start, HM5804, its line put back where the service may write the file; a key it has never held is adopted once no - the stored value is used, and the line written back where the service may write the file Except PasswordPepper, which lives in the file
hMailServer.ini [SettingsOverride] yes, over everything, HM5807 at every start remove the line For a database that is unreachable or holds a value the server will not start on

At 3233b0cc4 every one of these applied at the next start or reinitialise, as in section 3. From 23 September 2026 (batch 26) each of them publishes the value, and when it is in effect is the key's applies mark (section 4b). Where the server's environment names a key (HM_SETTING_<key>), every one of these refuses to change it - REST with 409 - until a start without the variable.

4b. Since 6.3.4: when a save applies each key, its level, and its name in the environment

Sources: build/ini-settings-baseline.txt at 540347500, whose six fields are section, key, kind, default, applies and levels; hmailserver/docs/SettingsApply.md, SettingLevels.md and EnvironmentConfiguration.md.

When a save applies it. The [Settings] values are one immutable block, IniFileSettings::Values, published with one atomic store; a write stores the row, rebuilds the block from the rows alone (no reconcile, no adoption, no report) with every now key taking its stored value and every other key keeping the one it runs on, publishes it, and then calls the subscribers of the keys whose value moved. Every String getter returns a copy, because a reference into a block a save may free would be a use-after-free.

Applies Count Keys
now 211 Every [Settings] key not named below. The greylist cleaner's period (GreylistingRecordExpirationInterval), the backup schedule (ScheduledBackupTime, ScheduledBackupIntervalMinutes), the five Otel* keys, CalDavRedirectUrl and CardDavRedirectUrl, SimulateDatabaseFailureFor, MaxSubmissionsPerIPPerMinute and MaxOutboundPerDestinationPerMinute are re-latched by a subscriber; the rest are read at the moment of use
reinitialize 28 AcmeCertificateDirectory, AcmeEnabled, DatabaseStatementTimeout, ManageSieveServerBindAddress, ManageSieveServerPort, MaxNumberOfExternalFetchThreads, MetricsServerAuthPassword, MetricsServerAuthToken, MetricsServerAuthUsername, MetricsServerBindAddress, MetricsServerCertificateFile, MetricsServerPort, MetricsServerPrivateKeyFile, RestApiBindAddress, RestApiCertificateFile, RestApiPort, RestApiPrivateKeyFile, TlsCipherSuites13, TlsKeyExchangeGroups, TlsSessionCacheSize, TlsSessionTicketsEnabled, TlsSessionTimeoutSeconds, TlsTicketKeyRotationSeconds, WebServicesBindAddress, WebServicesCertificateFile, WebServicesHttpPort, WebServicesHttpsPort, WebServicesPrivateKeyFile
restart 2 ServiceAccountName, ServiceAccountPassword - and every one of the 29 bootstrap keys, which are never in the store and so never pending

A reinitialize or restart key whose stored value is not the one the server runs on is pending: GET /api/v1/server/settings/pending (the administrator password only), COM Settings.PendingIniSettings, pending on every /api/v1/settings/ini answer, the Control Panel's bar at the foot of the window and the Deck's note above its Settings view. Settings.IniSettingApplies(name) and Settings.IniSettingAppliesAll answer the marks over COM, and applies answers them over REST; a name the loader does not read - UseLanguage among them - has no mark and is refused by IniSettingApplies. Apply now in the Control Panel and the Deck, POST /api/v1/server/reinitialize and Application.Reinitialize take every reinitialize key. A value stored by another process - another node, --set-setting, --apply-environment while the service runs - reaches this server at its next save, reinitialise or restart. [SettingsOverride] is re-read by a reinitialise or a start, not by a save.

Its level. The sixth field says the deepest level a key may be set at. Every [Settings] key is server except QuotaWarningPercent, which is account: a domain (hm_domains.domainquotawarningpercent, -1 taking the server's) and an account (a row of hm_settingoverrides, schema 6088) may hold their own value, and Application/EffectiveSetting resolves the account's own, else its domain's, else this key at every read. The server's REST answers carry <key>_levels beside such a key. It is one of twenty-seven settings with levels; the other twenty-six are hm_settings properties, fields of the domain or the account, or [SendingLimits] keys, and build/settings-levels.txt marks the hm_settings side as this file marks the [Settings] side (Settings Overview lists them all).

Its name in the environment. Every key in section 9 is also HM_SETTING_<key>, matched without case - HM_SETTING_MaxSubmissionsPerIPPerMinute=30 - and HM_SETTING_<key>_FILE reads the value from a file of at most 64 KB. hmailserver --apply-environment (and hMailServer.exe --apply-environment) checks every variable before it stores any and refuses a name that is no key with the nearest one; the container's entrypoint runs it at every start. A key the environment manages is recorded in the hm_settings row EnvironmentManagedSettings, and every surface refuses an administrator's change to it until a start without the variable. The four HM_REST_* variables the image took before are aliases of the HM_SETTING_ names they always stored. The two [SendingLimits] keys and the [LDAP] section have no environment name, and the _FILE form of QuotaWarningPercent is refused, because a setting with levels is shown to a domain's administrators.

5. [Database] section

Key Type Default What it does Consumer Timing
Type string "" One of MSSQL, MYSQL, PostgreSQL, MSSQLCE (case-insensitive); anything else = TypeUnknown and InitInstance returns false ("database settings do not exist") (IFS.cpp:107-121, 681-688; Application.cpp:134-137) DatabaseConnectionManager, SQLStatement, SqlLogDevice, PersistentAccount/Domain/FetchAccount, SMTPDeliveryManager S! (connect)
Server string "" DB host. Required for every type except MSSQLCE (CheckSettings, IFS.cpp:908-928, HM5005 at Application.cpp:197-203) DatabaseConnectionManager.cpp:96 S!
Database string "" DB name / SQL CE file name. Required for MSSQLCE (IFS.cpp:912-916) DatabaseConnectionManager.cpp:99 S!
Username string "" DB user (IFS.cpp:101) DatabaseConnectionManager.cpp:97; MySQLConnection.cpp:366-369 (internal-DB heuristic) S!
Password string "" DB password, decrypted with Passwordencryption scheme at load (IFS.cpp:102, 109-112) DatabaseConnectionManager.cpp:98 S!
Passwordencryption int 0 Crypt::EncryptionType for Password: 0 none, 1 Blowfish, 6 DPAPI (machine-scoped). Crypt::DeCrypt handles only 0/1/6; any other value hits assert(0)/returns "" (Common/Util/Crypt.cpp:153-185; enum Common/Util/Crypt.h:18-27). Written by the server as PasswordEncryption (different case; harmless) = 6 when ProtectStoredSecretsWithDPAPI=1 and DPAPI succeeds, else 1 (IFS.cpp:1093-1118) IFS.cpp:109-112 S!
Port int 0 DB TCP port (IFS.cpp:123); 3307 + user root/hmailserver + Internal=1 marks the legacy internal MySQL (MySQLConnection.cpp:364-373) DatabaseConnectionManager.cpp:102; MySQLConnection S!
Internal int (0/1) 0 Marks the legacy bundled MySQL; drives the post-install Internal MySQL\HMS4.3-MySQL4.1.18.sql script (IFS.cpp:103; MySQLConnection.cpp:364-380) MySQLConnection S!
ServerFailoverPartner string "" ADO "Failover Partner" for MSSQL mirroring; when set and Provider empty the provider becomes SQLNCLI (IFS.cpp:104; Common/SQL/ADOConnection.cpp:86-110) ADOConnection S!
Provider string "" OLE DB provider name for MSSQL. Empty → MSOLEDBSQL if v18+ installed, else sqloledb; SQLNCLI if a failover partner is set (ADOConnection.cpp:90-110) ADOConnection S!
NumberOfConnections int 5 Pool size; forced to 1 for MSSQLCE (IFS.cpp:144, 148-154) DatabaseConnectionManager.cpp:91 S!
ConnectionAttempts int 6 Connect retries at startup (IFS.cpp:145) DatabaseConnectionManager.cpp:54-59 S!
ConnectionAttemptsDelay int seconds 5 Delay between attempts (multiplied by 1000 ms) (IFS.cpp:146; DatabaseConnectionManager.cpp:55) DatabaseConnectionManager S!
AllowUnencryptedConnection int (0/1) 0 MySQL/MariaDB only. Up to 6.3.3 described as making the bundled MariaDB Connector/C prefer TLS instead of requiring it; the test lab measured on 25 September 2026 that the client never asked for TLS at all, so every connection through it was in plaintext whatever this said. Since 6.3.4 it is still read and changes nothing: what it described - TLS preferred, plaintext allowed - is the default, and TlsMode below chooses otherwise (hmailserver/docs/DatabaseTls.md). Added 5 September 2026 MySQLConnection S!
PostgreSQLSslMode string "" PostgreSQL only. libpq's sslmode, put into the conninfo: disable, allow, prefer, require, verify-ca or verify-full. Empty leaves libpq's default (prefer: encrypted when the server offers it, verified never). Any other value refuses the connection with HM5563 rather than falling back (Common/SQL/PGConnection.cpp). Added 5 September 2026 PGConnection::Connect S!
PostgreSQLSslRootCert string "" PostgreSQL only. libpq's sslrootcert: the CA file verify-ca/verify-full check the server certificate against. A Windows path; the backslashes are escaped for conninfo by the server. Added 5 September 2026 PGConnection::Connect S!
ConnectionStringOptions string "" MS SQL Server only. Appended verbatim to the OLE DB connection string (a ; is added if missing) - the provider's own keywords, e.g. Encrypt=yes;TrustServerCertificate=no for MSOLEDBSQL. Added 5 September 2026 ADOConnection::Connect S!
TlsMode string "" 6.3.4. TLS between this server and its database, one setting for every backend that crosses a network: empty or prefer - TLS whenever the server offers it, the connection without it when it offers none, the certificate not checked; disable; require - TLS or no connection, the certificate not checked; verify-full - TLS or no connection, the chain checked against TlsCaFile (or the system's store) and the name or address against Server=. Anything else, verify-ca among them, is refused without an attempt (HM7530). With nothing set, MySQL and MariaDB now use TLS when offered (they never did through MariaDB Connector/C), SQL Server through MSOLEDBSQL 18 and SQL Native Client encrypts the whole session, and PostgreSQL is unchanged (PostgreSQLSslMode and PostgreSQLSslRootCert win when set). The application log says at every start what the connection got, and --check-database makes one connection and says the same. A file key, since it says how the store is reached (IniFileSettings.cpp ReadIniSettingString_("Database", "TlsMode", ""); hmailserver/docs/DatabaseTls.md) MySQLConnection, ADOConnection, PGConnection S!
TlsCaFile path "" 6.3.4. A PEM file of CA certificates, read by verify-full and by nothing else; a TlsCaFile that verify-full would read and that is not there refuses the connection (HM7531) rather than falling back to the system's store. Keep the path ASCII. SQL Server does not read it and checks against the Windows store (HM7532 says so once) MySQLConnection, PGConnection S!
UpgradeOnStart int (0/1) 0 6.3.4 (batch 25, 23 September 2026). 1 makes Application::OnDatabaseConnected bring a database older than this build requires forward through the upgrade scripts before it decides whether it can run, and then continue the start; a step that fails leaves HM5011 naming the step and the version the database is at, and the service not started. 0 refuses an older database as before. A file key and not a setting of the store, because it is read before the database is at a version this build can open. The walk is Common/Persistence/SchemaUpgrader, which hmailserver --upgrade-database on Linux also uses; applies at a restart Application.cpp OnDatabaseConnected; SchemaUpgrader S!

Setters that write this section: SetDatabaseServer/Name/Username/Password/Type/Port/IsInternalDatabase (IFS.cpp:1060-1173), reached from COM Database.* (COM/InterfaceDatabase.cpp).

6. [Directories] section

Key Type Default Normalisation Used for Consumer(s) Timing
ProgramFolder path "" trailing \ appended (IFS.cpp:125-127) Bin dir (ProgramFolder\Bin, IFS.cpp:1187-1191), ProgramFolder\DBScripts (IFS.cpp:139-142), ProgramFolder\Languages (IFS.cpp:893-897), ProgramFolder\WebAdmin\index.html (RestApiServer.cpp:2611-2614) Compression.cpp, DatabaseSettings.cpp, MySQLConnection.cpp, Language(s).cpp, RestApiServer.cpp S
DataFolder path "" trailing \ stripped (IFS.cpp:129-131) Message store root; ACME dir default DataFolder\ACME (AcmeClient.cpp:250-255); sending-limits state file DataFolder\hmailserver_sendinglimits.dat (RateLimiter.cpp:34,268-270); quarantine, sieve storage, backups, disk-space checks; live-update download directory DataFolder\Updates (UpdateDownloader.cpp:39-46; new in 6.2.28; left out of backups since 6.3.6) 16 files (PersistentMessage, PersistentAccount/Domain, QuarantineStore, SieveStorage, BackupExecuter, BackupScheduleTask, DiskSpace, RateLimiter, ServerStatus, MailImporter, AcmeClient, NameChanger, Message, TestDataDirectory, VirusScannerTester), UpdateDownloader, UpdateCheckTask S
TempFolder path "" trailing \ stripped (IFS.cpp:133-135) Temp files for scanners/compression/process launcher ClamWinVirusScanner, VirusScanner, Compression, FileUtilities, ProcessLauncher, Utilities S
EventFolder path "" none (IFS.cpp:137) EventFolder\EventHandlers.<vbs|js> script (ScriptServer.cpp:166-170, 291-295) ScriptServer.cpp; COM InterfaceScripting S
DatabaseFolder path "" trailing \ stripped (IFS.cpp:164-166) Location of the internal SQL CE database (InterfaceDatabase.cpp:488-495) DatabaseConnectionManager, InterfaceDatabase S
LogFolder path "" none Log file directory; read lazily on first GetLogDirectory() with a 255-char buffer and cached (IFS.cpp:880-891) Logger.cpp:36, LogRetentionTask, MessageStoreConsistencyTask, CrashOracle, ExceptionLogger, Mime.cpp, MessageData.cpp, TestErrorLogs; COM InterfaceLogging/BackupSettings L (once)

Installer defaults: ProgramFolder={app}, DatabaseFolder={app}\Database, DataFolder={app}\Data, LogFolder={app}\Logs, TempFolder={app}\Temp, EventFolder={app}\Events (source: installation/section_ini.iss:1-8). Setters: IFS.cpp:995-1058 (COM Settings.Directories).

7. [GUILanguages] section

Key Type Default What it does Consumer Timing
ValidLanguages comma-separated list "" Names of language files allowed to load; case-insensitive match; files are ProgramFolder\Languages\<name>.ini with english.ini as base (IFS.cpp:168-169; Common/Util/Languages.cpp:61-77; Common/Util/Language.cpp:59,84) Languages.cpp S (Languages loaded at InitInstance, Application.cpp:144)

Installer writes ValidLanguages=english,swedish (section_ini.iss:11).

8. [Security] section

Key Type Default What it does Consumer Timing
AdministratorTotpSecret protected string "" Second factor on the administrator credential (added 5 Sep 2026). Stored as a machine-bound DPAPI:-prefixed envelope of a base32 TOTP secret (Crypt::ProtectSecret), unprotected per logon. While set: COMAuthentication refuses Authenticate("administrator") and only AuthenticateWithCode passes; the REST API needs the code in an X-hMailServer-OTP header and 401s with X-hMailServer-OTP: required when it is absent. Enrol/remove via COM Settings.EnrolAdministratorTOTP / DisableAdministratorTOTP; state readable unauthenticated via Application.AdministratorTOTPEnabled. Recovery for a lost authenticator: clear this key. COMAuthentication.cpp, RestApiServer.cpp, InterfaceSettings.cpp S
StoredSecretsKey protected string "" 6.3.4 (secrets at rest, schema 6103). The installation's stored-secrets key - 32 random bytes every server sharing the database holds - kept here under this machine's DPAPI (the Linux key file <data directory>/.hmailserver-secret-key on Linux), never in the database and never in a backup; the database records only its id (hm_settings StoredSecretsKeyId). Under it an account's TOTP secret and the [Settings] secrets (SRSSecret, BATVSecret, OAuth2HmacSecret, OAuth2IntrospectionClientSecret, OutboundOAuth2ClientSecret, OutboundOAuth2FixedToken, MetricsServerAuthToken, MetricsServerAuthPassword, ServiceAccountPassword, GssapiServicePassword) are sealed as HMK1:<key id>:<base64> (AES-256-GCM). Written only by an elevated step, never by the service: the Windows installer on a new installation, the container's --create-database, or --create-stored-secrets-key; moved with --export-stored-secrets-key and --import-stored-secrets-key; a lost key's seals cleared with --forget-sealed-secrets <key id>. Empty after an upgrade, and then nothing is sealed, as before 6.3.4. A key line that does not open here, or is not the recorded one, seals nothing and is reported at each start (HM7495). ProtectStoredSecretsWithDPAPI=0 keeps everything as written (hmailserver/docs/SecretsAtRest.md) StoredSecrets.cpp, IniFileSettings.cpp S
AdministratorPassword hash string "" COM/REST "Administrator" credential. Empty = no authentication required for COM (COM/COMAuthentication.cpp) and the REST API refuses to start (RestApiServer.cpp:559-561, confirmed). Hash type detected by prefix/length: PBKDF2 prefix, Argon2id prefix, scrypt $s2$ prefix, 32 chars = MD5, 70 chars = SHA256 (Common/Util/Crypt.cpp:171-186 GetHashType). Written by the server as PBKDF2 (IFS.cpp:1165); up to 6.3.3 written by the installer as an unsalted MD5 digest when a password was supplied (section_ini.iss:17; hMailServerInnoExtension.iss), which nothing re-hashed. Since 6.3.4 the installer writes PBKDF2 itself, and the first successful administrator sign-in (Control Panel, COM, REST, hmctl or the Deck) writes an MD5 or salted SHA-256 value, or a PBKDF2 hash with fewer iterations than PasswordHashIterations, again as PBKDF2; a file the service cannot write keeps the old value and says so once (HM7494) (hmailserver/docs/SecretsAtRest.md) COMAuthentication.cpp, RestApiServer.cpp, ScramSha256.cpp S

9. [Settings] section — every key read by IniFileSettings::LoadSettings

At master 540347500 (24 September 2026) the loader reads the same 241, and UseLanguage per call - 242 again; build/ini-settings-baseline.txt lists the 241. The baseline is the same at the tag v6.3.6: 6.3.6 adds no [Settings] key and removes none, but reads the five update-trust keys from the file alone (the note under the live-update keys, before section 9.16). In 6.2.28, 237 keys were read in LoadSettings (IFS.cpp:175-796; 234 through ReadIniSettingString_/ReadIniSettingInteger_ and three — PasswordHashIterations, PasswordHashMemoryKB, PasswordHashTimeCost — through ReadPasswordHashWorkFactor_), plus UseLanguage read per call (IFS.cpp:1116) = 238 [Settings] keys in total. Counted again on 15 September 2026 in v6.3.3 and at 3233b0cc4, which read the same set: 238 through ReadIniSettingString_/ReadIniSettingInteger_, the three work factors and UseLanguage = 242 [Settings] keys. The four added since 6.2.28 are AutoBanFirewall, AutoBanCommand and AutoBanNeverBan (6.3.2, section 9.1) and WebServicesTrustedProxies (6.3.3, section 9.5). "238" elsewhere - in the commit history, build/ini-settings-baseline.txt and some pages - is the count of the literal calls alone. All are owned by IniFileSettings. Since the HTTP foundation (new in 6.2.28) CalDavRedirectUrl/CardDavRedirectUrl are latched by LoadSettings like every other key (IFS.cpp:493-494); nothing in [Settings] is read outside IniFileSettings any more. Booleans are 1/anything-else unless stated. "Consumer" is the file that calls the getter.

9.0 The file at a glance, and the keys that create a listener

Bin\
├─ hMailServer.ini              ← everything in sections 5 to 11 below
│   ├─ [Database]               file only, never mirrored
│   ├─ [Directories]            file only, never mirrored
│   ├─ [GUILanguages]           file only, never mirrored
│   ├─ [Security]               file only, never mirrored
│   ├─ [Settings]               242 keys, mirrored into hm_inisettings (6.3.3)
│   ├─ [SendingLimits]          read live by RateLimiter, not mirrored
│   ├─ [SendingLimitsOverrides] one line per account, same reader
│   └─ [LDAP]                   read live by LdapSettings, not mirrored
└─ hMailServerApiKeys.ini       ← section 12; one [Key.<id>] section per REST API key

Since 6.3.4 the picture changes in one line and gains two sections: [Settings] is a copy of hm_inisettings, which is the store, except for PasswordPepper, which lives only here; [SettingsStore] holds the server's two-key note saying so; and [SettingsOverride], when an administrator writes one, forces its keys over the store (section 4a).

Five keys turn a listener on, and every one of them is 0 by default, so a stock server listens on nothing but the mail protocols it was configured with. A sixth, AcmeHttpPort, defaults to 80 but only opens a socket of its own while an issuance is running:

Key Default Listener Extra condition before it starts Message when it refuses
MetricsServerPort 0 Prometheus + /livez /readyz /healthz the bind address must be an IP literal (localhost is not accepted here) MetricsServer: Invalid bind address: ...
RestApiPort 0 REST API, WebAdmin page, self-service portal [Security] AdministratorPassword must be set; TLS unless bound to 127.0.0.1, localhost or ::1 RestApi: Refusing to start - the administrator password is not set. / ... TLS certificate is required unless bound to 127.0.0.1 or ::1.
WebServicesHttpPort 0 MTA-STS policy, autoconfig, ACME http-01, DAV redirects — —
WebServicesHttpsPort 0 the same over HTTPS a certificate of its own, or an ACME one on disk No TLS certificate available yet. The HTTPS listener is disabled until a certificate exists (enable ACME or set WebServicesCertificateFile).
ManageSieveServerPort 0 ManageSieve, RFC 5804 — —
AcmeHttpPort 80 (not a listener of its own unless issuance is running) AcmeEnabled=1 and a non-empty AcmeDomains ACME: No domains configured

And these are the settings whose stated value suggests a working feature while nothing is actually happening — either because they are on by default with no listener behind them, or because a companion value is missing. This shape has produced more "it is enabled but it does not work" reports than any other:

flowchart TD
    A["A feature is enabled but inert"] --> B{"Which one?"}
    B --> C["MtaStsHostingEnabled=1"]
    C --> C1["Needs WebServicesHttpsPort. Reported at every start by ReportUnreachableFeatures."]
    B --> D["AutoconfigEnabled=1"]
    D --> D1["Needs WebServicesHttpPort or WebServicesHttpsPort. Same report."]
    B --> E["IndexerFullText=1"]
    E --> E1["Also needs COM Settings.MessageIndexing to be on."]
    B --> F["WindowsEventLogEnabled=1"]
    F --> F1["Writes nothing on a healthy server at the default level 2."]
    B --> G["OutboundOAuth2Hosts and FetchOAuth2Hosts have non-empty defaults"]
    G --> G1["Need OutboundOAuth2TokenUrl or OutboundOAuth2FixedToken. With both empty, delivery quietly falls back to LOGIN and logs nothing."]
    B --> H["SRSEnabled=1"]
    H --> H1["Needs SRSSecret. SRS::Forward returns an empty address for an empty secret and the forwarder then leaves the envelope alone, silently."]
    B --> I["TlsRptFromAddress empty"]
    I --> I1["Statistics are collected and discarded. Nothing is sent and nothing is logged."]
    B --> J["The SendingLimits and LDAP sections"]
    J --> J1["Entirely inert until Enabled, or a non-zero limit, is set."]
Loading
  • Sources for the rows above, in order: Application.cpp:542-557 and WebServicesServer.cpp:454-491; IFS.cpp:358-365 with MessageIndexer.cpp:87,109,169; IFS.cpp:628-638; ExternalDelivery.cpp:681-700; Common/Util/SRS.cpp Forward returns "" when the secret is empty and SMTP/SMTPForwarding.cpp only assigns a non-empty result; IFS.cpp:483-489; RateLimiter.h:15 and LdapSettings.cpp:229.

9.1 Passwords, hashing, secrets, service account

Key Type Default Validation What it does Consumer Timing
PreferredHashAlgorithm int 4 (PBKDF2) must be 3, 4, 5 or 7 else HM5528 and reset to 4 (IFS.cpp:171-193) Scheme for storing NEW account/app passwords: 3 SHA256, 4 PBKDF2, 5 Argon2id, 7 scrypt (RFC 7914, N=2^17 r=8 p=1, $s2$ hashes; added 5 September 2026; 6 is DPAPI and refused here) InterfaceAccount, AppPassword, PersistentAccount, PasswordValidator, RestApiServer S
MinimumAcceptedHashAlgorithm int 0 none Accounts whose stored hash is weaker than this scheme are refused; 0 disables (IFS.cpp:195-198). Compared by strength (Crypt::StrengthRank, since 5 September 2026), not by number: SHA256 (3) < PBKDF2 (4) < Argon2id (5) = scrypt (7), so a minimum of 5 or of 7 accepts both memory-hard schemes; 6 (DPAPI) is not a password scheme and refuses nothing. The same order decides upgrade-on-logon, so a preference for Argon2id leaves scrypt accounts alone and the other way round AppPassword, PasswordValidator, IMAPCommandAuthenticate, POP3Connection, SMTPConnection S
PasswordPepper string "" none Server-wide secret HMAC-mixed into Argon2id and scrypt hashes (Crypt::ApplyPepper_ is called for those two schemes and no other; PBKDF2 stays unpeppered for SCRAM); empty = off (IFS.cpp:200-203). Since 6.3.4, the one [Settings] key kept in the file only: never stored in hm_inisettings, never in a backup (section 4a) Crypt.cpp S
PasswordHashIterations int 0 0, or 10000-10000000, else HM5561 and read as 0 PBKDF2 iterations for NEW password hashes; 0 = the built-in 210,000. A stored hash carries its own count, so raising this never breaks a logon: a hash cheaper than the configured value is re-derived on the next successful logon (PasswordValidator::UpgradeStoredPasswordHash_ via HashCreator::NeedsRehash), a costlier one is left alone - lowering it affects new hashes only (IFS.cpp ReadPasswordHashWorkFactor_). Added 5 September 2026 HashCreator, PasswordValidator S
PasswordHashMemoryKB int 0 0, or 4096-1048576, else HM5561 and read as 0 Argon2id memory for new hashes, in KiB; 0 = the built-in 19,456. Same re-derive rule as PasswordHashIterations. Every logon verification allocates this much on a connection thread. Added 5 September 2026 HashCreator, PasswordValidator S
PasswordHashTimeCost int 0 0, or 1-20, else HM5561 and read as 0 Argon2id passes for new hashes; 0 = the built-in 2. Same re-derive rule as PasswordHashIterations. Added 5 September 2026 HashCreator, PasswordValidator S
ProtectStoredSecretsWithDPAPI bool 1 Store the ini DB password and DB-held route/fetch/relayer secrets as machine-bound DPAPI (DPAPI: prefix) instead of Blowfish; legacy values stay readable (IFS.cpp:230-236; Crypt.cpp:188-220). Since 6.3.4, 0 also keeps an account's second factor and the [Settings] secrets as written rather than sealing them under the stored-secrets key (section 8) Crypt.cpp, IFS.cpp:1105-1117, StoredSecrets.cpp S
ServiceAccountName string "" Up to 6.3.3, the Windows account the service logs on as; empty = LocalSystem. Applied only by hMailServer.exe /Register (CreateService/ChangeServiceConfig) (IFS.cpp:238-247; Common/Util/ServiceManager.cpp:57-62,123-127; hMailServer.cpp:216-249). Since 6.3.4 still read, and no longer used by /Register, which takes the account from its command line alone (--service-account virtual|localsystem|keep|DOMAIN\name$): the value is a row of the settings store that administrators who need not be Windows administrators can set, so a registration that read it could be made to give the service LocalSystem back. A new installation runs as NT SERVICE\hMailServer (hmailserver/docs/ServiceAccount.md) IniFileSettings.cpp (read) -
ServiceAccountPassword string "" Password for the above; empty for virtual/managed accounts (IFS.cpp:247). Since 6.3.4 not used by /Register either, and one of the [Settings] secrets sealed under the stored-secrets key (section 8) IniFileSettings.cpp (read) -
PasswordPolicyMinimumLength int 0 Enforced where a password is CHOSEN (COM Account.Password); 0 = off (IFS.cpp:666; IFS.h:414-418) PasswordPolicy.cpp S
PasswordPolicyRequireMixedCase bool 0 as above (IFS.cpp:667) PasswordPolicy.cpp S
PasswordPolicyRequireDigit bool 0 (IFS.cpp:668) PasswordPolicy.cpp S
PasswordPolicyRequireNonAlphanumeric bool 0 (IFS.cpp:669) PasswordPolicy.cpp S
PasswordPolicyRejectCommon bool 0 Rejects passwords in a compiled-in list (PasswordPolicy.cpp:29,153-159) (IFS.cpp:670) PasswordPolicy.cpp S
PasswordPolicyHistoryCount int 0 Refuse reuse of the last N passwords; 0 = off (IFS.cpp:671; IFS.h:425-432) PasswordHistory.cpp S
PasswordPolicyMaximumAgeDays int 0 Password expiry in days; 0 = off. IMAP, POP3 and SMTP cannot change a password and the self-service portal (new in 6.2.28) refuses an expired one at sign-in, so an expired password is renewed by an administrator — or by the person themselves only if they hold an app password, which still signs them in to /portal, where POST /api/v1/me/password takes the expired password as the current one. Active Directory accounts are exempt (IFS.cpp:794; IFS.h:556) PasswordHistory.cpp S
AccountLockoutThreshold int 0 Failed logons per canonical username before lockout; 0 = mechanism off (IFS.cpp:558; Common/Util/AccountLockout.cpp:136,175) AccountLockout.cpp S
AccountLockoutWindowMinutes int 30 <1 → 30 Window in which failures are counted (IFS.cpp:559-562) AccountLockout.cpp S
AccountLockoutMinutes int 30 <1 → 30 Lock duration (IFS.cpp:560-564) AccountLockout.cpp S
LogonTarpitSeconds int 0 clamped 0–30 Login tarpit (added 5 Sep 2026): each failed logon's refusal waits this × the failures so far on that connection, capped 30s, on SMTP/POP3/IMAP; 0 = off. A pause on the connection's own timer (TCPConnection::EnqueueDelay, BCTDelay), not a thread asleep. A correct password never waits. AccountLogon::TarpitDelaySeconds; SMTP/POP3/IMAPConnection S
SmtpTarpitCount int 0 clamped ≥0 Recipient tarpit (added 5 Sep 2026): recipients an unauthenticated SMTP session may name before the delay applies; 0 = off. Also settable via COM AntiSpam.TarpitCount. SMTPConnection::TarpitRecipient_ S
SmtpTarpitDelaySeconds int 0 clamped 0–30 Recipient tarpit: seconds each RCPT TO past SmtpTarpitCount waits for its reply; 0 = off. Authenticated sessions and ranges with spam protection off are exempt. Also settable via COM AntiSpam.TarpitDelay. SMTPConnection::TarpitRecipient_ S
AutoBanFirewall bool 0 Added in 6.3.2. Pushes every auto-ban below the server. Windows: an inbound TCP block rule per banned address in Windows Defender Firewall, scoped to the ports this server listens on and named hMailServer auto-ban <address>. Linux: the packaged hook /usr/lib/hmailserver/autoban-hook (an nftables set with a timeout, iptables where there is no nft), which needs CAP_NET_ADMIN the systemd unit does not grant by default. A reconciliation rather than an event stream: AutoBanFirewall::Synchronise makes the firewall match the auto-ban ranges at start-up, on every ban, on every range deletion and once a minute (IFS.cpp:540 at 3233b0cc4; Common/Util/AutoBanFirewall.h) AutoBanFirewall.cpp S
AutoBanCommand string "" Added in 6.3.2. A program of the administrator's, run as <command> ban <address> <minutes> <ports> and <command> unban <address> on both platforms, instead of or as well as AutoBanFirewall (IFS.cpp:541). Since 6.3.6 the command's account is kept in autoban-command.ledger in the temp folder, under the SHA-256 of the command line and never its text; at a start the command is told unban for each address it was left holding that is not banned now (the ledger begins at the first start of 6.3.6). A command replaced or cleared is never run again: the addresses it was told to ban and not to un-ban are named in the error log (HM6523), and the new command is told every ban in force. A command of your own is still best made to let a block lapse after the <minutes> it is handed AutoBanFirewall.cpp S
AutoBanNeverBan list "" an entry that is not an address or CIDR block is ignored, reported once per value as HM6524 Added in 6.3.2. Addresses and CIDR blocks (1.2.3.4, 1.2.3.0/24, 2001:db8::/32), separated by commas, semicolons or whitespace, whose failed logons are never counted, so they are never auto-banned; the per-connection disconnect still applies (IFS.cpp:542; AutoBanFirewall.cpp IsNeverBanned; AccountLogon.cpp RegisterFailedLogin). Since 6.3.6 an entry written in a form Windows used to take (192.168.1/24, which protected 192.168.0.0/24 without a word) is reported as HM6524 and protects nothing, on Windows as on Linux; the never-ban list by reverse name (section 17) spares an address by its reverse name AccountLogon.cpp, AutoBanFirewall.cpp S

9.2 OAuth2 inbound (bearer tokens) and outbound/fetch XOAUTH2

The JWK Set and introspection fetches are the server acting as a web client, so HttpProxy (section 9.16) carries them when one is set. Since 6.3.6 the outbound/fetch XOAUTH2 token request goes through HttpsClient too, and so through HttpProxy when one is set; a port in OutboundOAuth2TokenUrl is used, and the request is given up after 20 seconds whole. Up to 6.3.5 OutboundOAuth2TokenClient.cpp opened its own connection and never saw that key.

Key Type Default What it does Consumer Timing
OAuth2Enabled bool 0 Accept SASL XOAUTH2/OAUTHBEARER JWTs verified locally (IFS.cpp:205-208) OAuth2TokenValidator.cpp S
OAuth2RequireTLS bool 1 Mechanism only offered on TLS (IFS.cpp:209) OAuth2TokenValidator.cpp S
OAuth2AllowedAlgorithms comma list RS256 Accepted JWT alg: HS256, RS256, ES256 recognised; none never honoured (IFS.cpp:218-220; OAuth2TokenValidator.cpp:216-220,390) OAuth2TokenValidator.cpp S
OAuth2HmacSecret string "" HS256 key (IFS.cpp:222) OAuth2TokenValidator.cpp S
OAuth2PublicKeyFile path "" PEM RSA/EC public key for RS256/ES256 (IFS.cpp:224; getter is named GetOAuth2RsaPublicKeyFile) OAuth2TokenValidator.cpp S
OAuth2Issuer string "" Expected iss; empty disables the check (IFS.cpp:226) OAuth2TokenValidator.cpp S
OAuth2Audience string "" Expected aud; empty disables (IFS.cpp:227) OAuth2TokenValidator.cpp S
OAuth2UsernameClaim string email Claim carrying the login name (IFS.cpp:229) OAuth2TokenValidator.cpp S
OAuth2JwksUrl string "" RFC 7517 JWK Set URL: the provider's published signing keys, selected by the token's kid for RS256/ES256, with OAuth2PublicKeyFile as the fallback. https, or plain http to a loopback address only (Common/Util/JwksKeySet.cpp, HttpsClient.cpp). Added 5 September 2026 OAuth2TokenValidator S
OAuth2JwksCacheSeconds int seconds 3600 How long the JWK Set is kept before re-fetching; a token naming an unknown kid re-fetches at once, at most once a minute. Minimum 10. Added 5 September 2026 JwksKeySet S
OAuth2IntrospectionUrl string "" RFC 7662 introspection endpoint, asked after a token verifies locally whether it is still active; empty = no revocation check (Common/Util/TokenIntrospection.cpp). Added 5 September 2026 OAuth2TokenValidator S
OAuth2IntrospectionClientId / OAuth2IntrospectionClientSecret string "" The client this server introspects as (HTTP Basic, RFC 6749 2.3.1). Added 5 September 2026 TokenIntrospection S
OAuth2IntrospectionCacheSeconds int seconds 300 How long a verdict (active or not) is reused, never past the token's exp. Added 5 September 2026 TokenIntrospection S
OAuth2IntrospectionFailOpen int (0/1) 0 1 accepts a token when the endpoint cannot answer (RFC 7662 says treat it as inactive, which 0 does); every such acceptance is written to the application log. Added 5 September 2026 TokenIntrospection S
OutboundOAuth2TokenUrl URL "" client_credentials token endpoint for outbound relay XOAUTH2 (IFS.cpp:210; IFS.h:161-166) OutboundOAuth2TokenClient.cpp, ExternalDelivery.cpp S
OutboundOAuth2ClientId string "" (IFS.cpp:211) OutboundOAuth2TokenClient.cpp S
OutboundOAuth2ClientSecret string "" (IFS.cpp:212) OutboundOAuth2TokenClient.cpp S
OutboundOAuth2Scope string https://outlook.office365.com/.default (IFS.cpp:213-214) OutboundOAuth2TokenClient.cpp S
OutboundOAuth2Hosts comma list smtp.office365.com Relay hosts the token is presented to (IFS.cpp:215) ExternalDelivery.cpp S
OutboundOAuth2FixedToken string "" Used verbatim instead of fetching (tests / externally obtained tokens) (IFS.cpp:216) ExternalDelivery.cpp, POP3ClientConnection.cpp S
FetchOAuth2Hosts comma list outlook.office365.com External (POP3 fetch) servers that authenticate with XOAUTH2 using the outbound credentials (IFS.cpp:217; IFS.h:174-177) POP3ClientConnection.cpp S

9.3 Logging and diagnostics

Key Type Default Validation What it does Consumer Timing
SepSvcLogs bool 0 Separate hmailserver_SMTP_<date>.log / _POP3_ / _IMAP_ files instead of one hmailserver_<date>.log (IFS.cpp:251; Logger.cpp:489-505, re-read at 522) Logger.cpp S
LogLevel int 9 Effects found: line truncation only when LogLevel <= 2 and debug off (Logger.cpp:676-697); IMAP logs SENT: FETCH/STATUS lines only when > 2 or debug (IMAPConnection.cpp:673-677) (IFS.cpp:252) Logger.cpp, IMAPConnection.cpp S
MaxLogLineLen int 500 <100 → 100; truncation also requires ≥80 Long log lines are cut to head+" ... "+tail (never AWStats) (IFS.cpp:253-254; Logger.cpp:664-699) Logger.cpp S
JsonLogging bool 0 JSON-formatted log lines (IFS.cpp:406) Logger.cpp S
LogDeleteDays int days 0 Delete date-stamped log files (and SQL log rows) older than N days; 0 = keep forever (IFS.cpp:407; LogRetentionTask.h:8-11) LogRetentionTask.cpp, SqlLogDevice.cpp S
SlowQueryLogMilliseconds int ms 0 Statements at/over this are counted slow and logged redacted; 0 = off, latency metrics still run (IFS.cpp:409; DatabaseConnectionManager.cpp:290-293) DatabaseConnectionManager.cpp S
WindowsEventLogEnabled bool 1 Windows Application event log sink (IFS.cpp:628; IFS.h:298-308) WindowsEventLog.cpp S
WindowsEventLogLevel int 2 clamped 1..4 Severity written: 1 Critical, 2 +High, 3 +Medium, 4 +Low (IFS.cpp:629-638) WindowsEventLog.cpp S
OtelEndpoint URL "" OTLP/HTTP traces endpoint (e.g. http://127.0.0.1:4318/v1/traces); empty = tracing off (IFS.cpp:448; IFS.h:569-571) OtelTracer.cpp S! (Start)
OtelServiceName string hmailserver service.name resource (IFS.cpp:449) OtelTracer/OtelMetricsExporter/OtelLogExporter S!
OtelMetricsEndpoint URL "" OTLP metrics push endpoint; empty = off (IFS.cpp:450) OtelMetricsExporter.cpp S!
OtelLogsEndpoint URL "" OTLP logs endpoint; empty = off (IFS.cpp:451) OtelLogExporter.cpp S!
OtelMetricsInterval int seconds 60 clamped 5..3600 by exporter Push interval (IFS.cpp:452; OtelMetricsExporter.cpp:27-31,78-82) OtelMetricsExporter.cpp S!
MessageStoreConsistencyCheck bool 0 Scheduled read-only walk comparing DB to disk; publishes hmailserver_messagestore_missing_files (IFS.cpp:425; MessageStoreConsistencyTask.h:8-15) MessageStoreConsistencyTask.cpp S
MetricsPerDomainEnabled bool 0 Per-domain counters on /metrics (cardinality decision) (IFS.cpp:656; IFS.h:383-389) MetricsServer.cpp, SMTPConnection.cpp S

9.4 Metrics / health listener

Key Type Default What it does Consumer Timing
MetricsServerPort int 0 Prometheus /metrics + /livez /readyz /healthz listener; 0 = not started (IFS.cpp:426; Application.cpp:496-501) Application.cpp S!
MetricsServerBindAddress IP 127.0.0.1 Bind address (IFS.cpp:427). Non-loopback bind with no credential → /metrics answers 503, probes still served (MetricsServer.cpp:375-400) Application.cpp, MetricsServer.cpp S!
MetricsServerAuthToken string "" Bearer token for /metrics (IFS.cpp:433) MetricsServer.cpp S!
MetricsServerAuthUsername string "" Basic auth user (both user+password needed) (IFS.cpp:434) MetricsServer.cpp S!
MetricsServerAuthPassword string "" (IFS.cpp:435) MetricsServer.cpp S!
MetricsServerCertificateFile path "" Both cert+key needed for HTTPS on the metrics port; never inferred (IFS.cpp:436; IFS.h:551-556) MetricsServer.cpp S!
MetricsServerPrivateKeyFile path "" (IFS.cpp:437) MetricsServer.cpp S!

9.5 REST API, web services, ACME, autoconfig, MTA-STS hosting

Key Type Default What it does Consumer Timing
RestApiPort int 0 REST admin API + Web Admin page listener, and (new in 6.2.28) the self-service portal: /portal, /api/v1/me... and /api/v1/session answer to an account's own credentials, with browser sessions bounded at 30 minutes idle / 12 hours (RestApiServer.cpp:327-331, 1281, 1581-1640, 7593). Hosted on HttpServer (Boost.Asio; 64 KB request cap, 16 MB on the two attachment routes, 64 connections, 4 workers — constants in HttpServer.h:72-101 and RestApiServer.cpp:128-137, no ini keys). 0 = not started; also refuses to start when [Security] AdministratorPassword is empty (RestApiServer.cpp:559-561) (IFS.cpp:706; Application.cpp:515-536) Application.cpp, RestApiServer.cpp S!
RestApiBindAddress IP 127.0.0.1 Bind address. Without a certificate the listener starts only on 127.0.0.1, localhost or ::1; any other address refuses to start until RestApiCertificateFile/RestApiPrivateKeyFile (or the ACME fallback) provide TLS (IFS.cpp:707; RestApiServer.cpp:569-578) Application.cpp, RestApiServer.cpp S!
RestApiCertificateFile path "" HTTPS cert; if empty and ACME fullchain.pem+privkey.pem exist in the ACME dir they are used (IFS.cpp:593; Application.cpp:515-528) Application.cpp S!
RestApiPrivateKeyFile path "" (IFS.cpp:594) Application.cpp S!
WebServicesHttpPort int 0 Public web services (MTA-STS policy, autoconfig, ACME HTTP-01, DAV redirects) listener; started only if this or the HTTPS port > 0 (IFS.cpp:602; Application.cpp:537-563) Application.cpp, WebServicesServer.cpp S!
WebServicesHttpsPort int 0 (IFS.cpp:603) Application.cpp S!
WebServicesBindAddress IP 0.0.0.0 (IFS.cpp:604) Application.cpp, RestApiServer.cpp S!
WebServicesCertificateFile path "" (IFS.cpp:605) Application.cpp S!
WebServicesPrivateKeyFile path "" (IFS.cpp:606) Application.cpp S!
WebServicesTrustedProxies comma list of IP/CIDR "" Added in 6.3.3. The peers, besides loopback, whose X-Forwarded-Proto: https the plain HTTP web-services listener believes - which decides whether the Apple .mobileconfig profile is served and whether CalDAV and CardDAV treat the request as HTTPS. From any other peer the header is ignored. An entry that is not an address or CIDR range matches nothing and is reported once per service start as HM6281 (IFS.cpp:780 at 3233b0cc4; WebServicesServer.cpp RequestArrivedOverHttps_; ProxyProtocol.cpp TrustedProxyList) WebServicesServer.cpp S
MtaStsHostingEnabled bool 1 Serve /.well-known/mta-sts.txt — inert unless a WebServices port is set; ReportUnreachableFeatures logs this (IFS.cpp:607; Application.cpp:542-557) WebServicesServer.cpp S!
MtaStsPolicyMode string enforce Must be enforce, testing or none; anything else → enforce (IFS.cpp:608; WebServicesServer.cpp:1128-1131) WebServicesServer.cpp S
MtaStsPolicyMaxAge int seconds 604800 (IFS.cpp:609) WebServicesServer.cpp S
MtaStsPolicyMx comma list "" MX hosts in the policy; empty → derived from MX lookup (mx_cache) (IFS.cpp:610; WebServicesServer.cpp:1030-1040, 95-97) WebServicesServer.cpp S
AutoconfigEnabled bool 1 Thunderbird/Outlook autoconfig endpoints — also inert without a WebServices port (IFS.cpp:611) WebServicesServer.cpp, RestApiServer.cpp S
AutoconfigClientHost host "" Host advertised to clients; empty → COM Settings.HostName (IFS.cpp:612; WebServicesServer.cpp:1352-1358) WebServicesServer.cpp, RestApiServer.cpp S
CalDavRedirectUrl http(s) URL, absolute, no spaces/control chars "" 3xx redirect target for /.well-known/caldav (RFC 6764), naming another server that serves CalDAV. In 6.3.3, which has no CalDAV of its own, empty = 404 silently. Since 6.3.4, whose /dav/ serves CalDAV beside CardDAV, empty redirects to this server's own /dav/. Either way a non-empty unusable value = 404 and HM5780 once per service start, never a fall-back to this server (IFS.cpp:493; WebServicesServer.cpp:1062-1140 in 6.2.28, HandleWellKnownDavRedirect_ in 6.3.4). Latched by LoadSettings since the HTTP foundation (new in 6.2.28) and read through the getter with a 60 s cache in WebServicesServer (WebServicesServer.cpp:67) WebServicesServer.cpp S (60 s cache)
CardDavRedirectUrl same "" /.well-known/carddav (IFS.cpp:493). This server has served CardDAV since 6.3.3, so empty redirects to its own /dav/; a value names another server, and an unusable one is 404 and HM5780 as above WebServicesServer.cpp S (60 s cache)
AcmeEnabled bool 0 Let's Encrypt issuance/renewal (IFS.cpp:595; Application.cpp:791) Application.cpp, AcmeClient.cpp, WebServicesServer.cpp S!
AcmeDirectoryUrl URL https://acme-v02.api.letsencrypt.org/directory (IFS.cpp:596) AcmeClient.cpp S
AcmeContactEmail string "" (IFS.cpp:597) AcmeClient.cpp S
AcmeDomains comma list "" (IFS.cpp:598) AcmeClient.cpp S
AcmeCertificateDirectory path "" Empty → DataFolder\ACME (IFS.cpp:599; AcmeClient.cpp:250-255) AcmeClient.cpp, Application.cpp S
AcmeHttpPort int 80 Port expected for HTTP-01 (IFS.cpp:600) AcmeClient.cpp, WebServicesServer.cpp S
AcmeReuseKey bool 1 Keep the private key across renewals (IFS.cpp:601) AcmeClient.cpp S

9.6 ManageSieve, TLS

Key Type Default Validation What it does Consumer Timing
ManageSieveServerPort int 0 RFC 5804 listener; 0 = not started (IFS.cpp:453; Application.cpp:566-571) Application.cpp S!
ManageSieveServerBindAddress IP 127.0.0.1 (IFS.cpp:454) Application.cpp S!
TlsKeyExchangeGroups OpenSSL group list X25519MLKEM768:SecP256r1MLKEM768:X25519:secp384r1:secp256r1 empty/rejected → classical fallback Key-exchange preference for SMTP/POP3/IMAP/ManageSieve listeners, outbound SMTP, and every HTTPS listener (REST, Web Services, metrics), all of which build their context through SslContextInitializer::InitServer (SslContextInitializer.cpp:292-296; RestApiServer.cpp:618, WebServicesServer.cpp:472, MetricsServer.cpp:730); the outbound client context is built once at start (IOService.cpp:65-68) (IFS.cpp:566-572; IFS.h:668-681) SslContextInitializer.cpp S! (context build)
TlsCipherSuites13 colon list "" TLS 1.3 suites (RFC 8446 names); empty = OpenSSL default; same scope as above, including the HTTPS listeners; distinct from COM SslCipherList which governs TLS ≤1.2 only (IFS.cpp:574-578; IFS.h:682-702; SslContextInitializer.cpp:715) SslContextInitializer.cpp S!
TlsSessionTicketsEnabled bool 1 0 sets SSL_OP_NO_TICKET and num_tickets 0; this and the three session rows below share the listener scope of TlsKeyExchangeGroups (IFS.cpp:584; IFS.h:704-712; SslContextInitializer.cpp:589) SslContextInitializer.cpp S!
TlsSessionCacheSize int 0 0 = OpenSSL default; negative = server-side cache off (IFS.cpp:585; IFS.h:714-717) SslContextInitializer.cpp:571 S!
TlsSessionTimeoutSeconds int 0 0 = OpenSSL default (IFS.cpp:586) SslContextInitializer.cpp:584 S!
TlsTicketKeyRotationSeconds int 0 <0 → 0 0 = no ticket-key callback; >0 rotates keeping previous key (IFS.cpp:588-590; IFS.h:723-728) SslContextInitializer.cpp:613 S!

9.7 DNS, DNSSEC, DANE, MTA-STS (outbound)

Key Type Default What it does Consumer Timing
UseDNSCache bool 1 0 adds DNS_QUERY_BYPASS_CACHE (IFS.cpp:400; DNSResolverWinApi.cpp:184-187) DNSResolverWinApi.cpp S
DNSServer IPv4 address "" Custom resolver, IPv4, four decimal parts, no port syntax, read the same way by every lookup since 6.3.6 (the Windows resolver read it with inet_addr, which took the older forms, while the DNSSEC resolver and the TLS policy's lookups used inet_pton: DNSServer=8.8.8 went to 8.8.0.8 for one and the system's servers for the other); a value that is not one, an older form such as 8.8.8 included, is reported invalid (HM4401) and the system's servers are asked; port field deliberately 0 (IFS.cpp:401; DNSResolverWinApi.cpp:252-291) DNSResolverWinApi.cpp, DnssecResolver.cpp, TlsPolicy.cpp S
DNSQueryTimeout int seconds 10 Bound on a resolver call; 0 = INFINITE (IFS.cpp:293; DNSResolverWinApi.cpp:120-124) DNSResolverWinApi.cpp S
DnssecValidationEnabled bool 1 Built-in DNSSEC validation (IFS.cpp:404). The validating resolver asks DNSServer, or else the system's first two IPv4 DNS servers, and since 6.3.6, when the system lists no IPv4 DNS server, its first two IPv6 ones DNSResolver.cpp, DnssecResolver.cpp, TlsPolicy.cpp S
DnssecTrustAnchors ;-separated DS records keytag alg digesttype hexdigest "" Overrides the built-in IANA root KSKs (20326 …) when non-empty (IFS.cpp:405; DnssecResolver.cpp:1003-1040) DnssecResolver.cpp S
DaneEnforcementEnabled bool 1 DANE/TLSA enforcement on outbound delivery. Getter is GetDaneEnabled; the key is NOT DaneEnabled (IFS.cpp:403; ExternalDelivery.cpp). Since 6.3.6 needs a resolver that answers DNSSEC queries: behind one that strips signatures, mail to every domain the server cannot prove unsigned is deferred (Encryption and Certificates, What DANE actually does); 0 leaves DANE off ExternalDelivery.cpp S
MtaStsEnabled bool 1 Outbound MTA-STS policy fetching/enforcement (IFS.cpp:402) ExternalDelivery.cpp S
SpfVoidLookupLimit int 2 RFC 7208 4.6.4 void-lookup limit; 0 disables (IFS.cpp:640; IFS.h:317-321) SPF/SPF.cpp S
DmarcTreeWalkEnabled bool 1 RFC 9989 tree walk for organisational domain; 0 = compiled-in PSL (IFS.cpp:639; IFS.h:310-315) DMARC/DMARC.cpp S

9.8 DKIM, ARC, Authentication-Results, TLS-RPT/DMARC reporting

Key Type Default Validation What it does Consumer Timing
ArcSealingEnabled bool 0 ARC seal on outbound (IFS.cpp:455) DKIMSigner.cpp S
DKIMSignatureValiditySeconds int 0 <0 → 0 Emit x= on our signatures; 0 = none (IFS.cpp:464-466; IFS.h:587-595) DKIM.cpp S
DKIMEnforceSignatureExpiry bool 1 Refuse verified signatures whose x= has passed (IFS.cpp:468) DKIM.cpp S
DKIMExpiryClockSkewSeconds int 300 <0 → 0 Skew allowance for x= (IFS.cpp:470-472) DKIM.cpp S
DkimOversignHeaders :/,/;-separated names "" Header fields listed once more than present in h= (IFS.cpp:474; IFS.h:606-613) DKIM.cpp S
DkimAcceptSha1 bool 0 Restore rsa-sha1 signing and verification (RFC 8301 forbids) (IFS.cpp:642; IFS.h:330-335) DKIM.cpp, DKIMSigner.cpp S
AuthenticationResultsEnabled bool 0 Add RFC 8601 Authentication-Results to inbound mail (IFS.cpp:480) AuthenticationResultsWriter.cpp, SMTPConnection.cpp S
ReceivedSpfHeaderEnabled bool 0 Add RFC 7208 9.1 Received-SPF (IFS.cpp:481) AuthenticationResultsWriter.cpp, SMTPConnection.cpp S
AuthenticationResultsIdentity string "" authserv-id; empty = computer name (IFS.cpp:482; IFS.h:623-627) AuthenticationResults.cpp S
TlsRptFromAddress e-mail "" Sender for TLS-RPT reports; empty = statistics collected and discarded, nothing sent (IFS.cpp:483, 486-489) TlsRptReporterTask.cpp, InterfaceUtilities.cpp S
TlsRptOrganizationName string hMailServer (IFS.cpp:484) TlsRptReporterTask.cpp S
DmarcRptFromAddress e-mail "" DMARC aggregate report sender; empty = never sent (IFS.cpp:490) DmarcRptReporterTask.cpp, InterfaceUtilities.cpp S
DmarcRptOrganizationName string hMailServer (IFS.cpp:491) DmarcRptReporterTask.cpp S
DmarcRptSchemaVersion int 1 must be 1 or 2 else HM6210 and reset to 1 1 = RFC 7489 App. C, 2 = RFC 9990/DMARCbis (IFS.cpp:493-525) DmarcRptReporterTask.cpp S

9.9 SMTP inbound behaviour, headers, rate shaping, proxy/XCLIENT

Key Type Default What it does Consumer Timing
DNSBLChecksAfterMailFrom bool 1 1 = pre-transmission spam checks (DNSBL etc.) run at MAIL FROM; 0 = at RCPT TO (IFS.cpp:249; SMTPConnection.cpp:839-848, 1326-1335) SMTPConnection.cpp S
AddXAuthUserHeader bool 0 Add X-AuthUser: for authenticated senders if absent (IFS.cpp:157; SMTPMessageHeaderCreator.cpp:91-95) SMTPMessageHeaderCreator.cpp S
AddXAuthUserIP bool 1 Include the authenticated sender's IP in the Received header handling (IFS.cpp:398; SMTPMessageHeaderCreator.cpp:61-69) SMTPMessageHeaderCreator.cpp S
AuthUserReplacementIP IP string "" If set, authenticated senders' real IP in Received is replaced by this value (IFS.cpp:353; SMTPMessageHeaderCreator.cpp:60-69) SMTPMessageHeaderCreator.cpp S
AddXOriginalRcptTo bool 0 Add X-Original-Rcpt-To (getter GetAddXOriginalRcptToHeader) (IFS.cpp:399; SMTPMessageHeaderCreator.cpp:97-110) SMTPMessageHeaderCreator.cpp S
DaemonAddressDomain domain "" Domain used for the mailer-daemon address (IFS.cpp:159) MailerDaemonAddressDeterminer.cpp S
DisableAUTHList comma list of ports "" SMTP AUTH not offered/accepted on these local ports (IFS.cpp:674, 1193-1212; SMTPConnection.cpp:4791-4796) SMTPConnection.cpp S
SMTPDMaxSizeDrop int KB 0 Hard drop: 552 5.3.4 when the received size ≥ this; 0 = off (IFS.cpp:396; SMTPConnection.cpp:1618-1628, 3400-3412) SMTPConnection.cpp S
FinalizationTimeout int seconds 240 Ceiling on post-DATA accept/save work; past it a temporary 451 is sent; 0 = off (IFS.cpp:276-281; SMTPConnection.cpp:2010-2018) SMTPConnection.cpp S
MaxSubmissionsPerIPPerMinute int 0 421 "Too many messages from your IP" when exceeded; 0 = unlimited (IFS.cpp:618; SMTPConnection.cpp:728-738) SMTPConnection.cpp S
MaxOutboundPerDestinationPerMinute int 0 Defers delivery to a destination domain beyond N/min; 0 = unlimited (IFS.cpp:673; ExternalDelivery.cpp:180-192) ExternalDelivery.cpp S
RejectFullMailboxAtRcpt bool 1 Refuse at RCPT TO when mailbox at/over quota instead of accept-then-bounce (IFS.cpp:641; IFS.h:323-328) RecipientParser.cpp S
QuotaWarningPercent int 90 Warn the account holder when a delivery crosses this % of quota; 0 = off (IFS.cpp:643; IFS.h:337-342) QuotaWarner.cpp S
SMTPProxyProtocolEnabled bool 0 HAProxy PROXY v1/v2 on the SMTP listener, before TLS/banner (IFS.cpp:620; IFS.h:270-283) TCPServer.cpp, SMTPConnection.cpp S
SMTPProxyProtocolTrustedIPs comma list of IP/CIDR "" Real TCP peers allowed to send PROXY; empty = nobody (IFS.cpp:621) TCPServer.cpp S
SMTPXClientEnabled bool 0 Postfix XCLIENT verb (IFS.cpp:622) SMTPConnection.cpp S
SMTPXClientTrustedIPs comma list of IP/CIDR "" (IFS.cpp:623) SMTPConnection.cpp S
BlockedIPHoldSeconds int seconds 0 Hold a connection from a blocked IP before closing (no longer a Sleep on the I/O thread) (IFS.cpp:395; TCPServer.cpp:388-400) TCPServer.cpp S
RewriteEnvelopeFromWhenForwarding bool 0 Rewrite envelope sender on forwards; also settable via COM Settings.RewriteEnvelopeFromWhenForwarding which writes the file AND mirror (IFS.cpp:613, 1175-1185) RuleApplier.cpp, SMTPForwarding.cpp S
SRSEnabled bool 0 Sender Rewriting Scheme on forwards (IFS.cpp:614) RecipientParser.cpp, SMTPForwarding.cpp S
SRSSecret string "" (IFS.cpp:615) RecipientParser.cpp, SMTPForwarding.cpp S
BATVEnabled bool 0 prvs bounce-address tagging (IFS.cpp:616) RecipientParser.cpp, SMTPClientConnection.cpp S
BATVSecret string "" (IFS.cpp:617) RecipientParser.cpp, SMTPClientConnection.cpp S
Pop3LoginDelaySeconds int 0 RFC 2449 LOGIN-DELAY advertised in CAPA and enforced; 0 = neither (IFS.cpp:619; POP3Connection.cpp:527-534) Pop3LoginDelay.cpp, POP3Connection.cpp S

9.10 Delivery queue, retries, external fetch

Key Type Default Validation What it does Consumer Timing
QuickRetries int 0 First N failed deliveries are rescheduled after QuickRetriesMinutes instead of the normal interval (greylisting aid) (IFS.cpp:255; ExternalDelivery.cpp:868-918) ExternalDelivery.cpp S
QuickRetriesMinutes int minutes 6 (IFS.cpp:256) ExternalDelivery.cpp S
QueueRandomnessMinutes int minutes 0 ≤0 → 0 Random 1..N minutes added to every retry delay (IFS.cpp:257-259; ExternalDelivery.cpp:876-889) ExternalDelivery.cpp S
MXTriesFactor int 0 ≤0 → 0 Limits MX hosts tried per attempt to (retries+1) * factor; 0 = all (IFS.cpp:260-261; ExternalDelivery.cpp:397-403) ExternalDelivery.cpp S
ClientSessionCeiling int seconds 1800 Absolute ceiling on an outbound SMTP client session (IFS.cpp:295-299) SMTPClientConnection.cpp S
OutboundPipelining 0/1 1 RFC 2920 on the delivery client (added 5 Sep 2026): when the remote advertises PIPELINING, MAIL FROM, every RCPT TO and the data command go in one flight and the replies are counted back in order (SMTPClientConnection::SendPipelinedEnvelope_ / ProtocolEnvelopePipelined_); 0 = one command per reply SMTPClientConnection.cpp S
OutboundChunking 0/1 1 RFC 3030 on the delivery client (added 5 Sep 2026): when the remote advertises CHUNKING the message goes as one BDAT <size> LAST chunk streamed raw (StartBdat_), no 354 and no dot-stuffing; also what lets a BINARYMIME message be relayed (BODY=BINARYMIME needs BINARYMIME + CHUNKING advertised); 0 = DATA SMTPClientConnection.cpp S
SmtpAuthenticatedSenderCheck 0/1 0 Added 5 Sep 2026: with 1, an authenticated SMTP session's MAIL FROM must be the account's own address, an alias resolving to it, or another account's address whose owner granted the account the post (p) right on their INBOX (Send-As); otherwise 550 5.7.1 at MAIL FROM (SMTPConnection::AuthenticatedSenderPermitted_). Null sender always allowed; unauthenticated sessions unaffected SMTPConnection.cpp S
MetricsHistoryDays int days 7 negative -> 0 Added 5 Sep 2026: MetricsHistoryTask writes one row per metric per minute to hm_metricsamples (schema 6028) and prunes rows older than this hourly; 0 turns the sampler off. Read back by Utilities.GetMetricHistory, GET /api/v1/metrics/history and the dashboard's range selector MetricsHistoryTask.cpp, RestApiServer.cpp, InterfaceUtilities.cpp S
MaxNumberOfExternalFetchThreads int 15 POP3 external-account fetch concurrency (IFS.cpp:156) ExternalFetchManager.cpp S
GreylistingEnabledDuringRecordExpiration bool 1 0 temporarily disables greylisting while expired records are purged (IFS.cpp:161; GreyListCleanerTask.cpp:47-58) GreyListCleanerTask.cpp S
GreylistingRecordExpirationInterval int minutes 240 Minutes between cleaner runs (getter GetGreylistingExpirationInterval) (IFS.cpp:162; Application.cpp:613) Application.cpp S!

9.11 Protocol timeouts, external processes, work queue

Key Type Default What it does Consumer Timing
SMTPDMinTimeout / SMTPDMaxTimeout int seconds 10 / 1800 SMTP server idle timeout bounds (IFS.cpp:270-271) SMTPConnection.cpp S
SMTPCMinTimeout / SMTPCMaxTimeout int seconds 30 / 600 SMTP client (outbound) idle timeout bounds (IFS.cpp:272-273) SMTPClientConnection.cpp S
POP3DMinTimeout / POP3DMaxTimeout int seconds 10 / 600 POP3 server (IFS.cpp:266-267) POP3Connection.cpp S
POP3CMinTimeout / POP3CMaxTimeout int seconds 30 / 900 POP3 fetch client (IFS.cpp:268-269) POP3ClientConnection.cpp S
SAMinTimeout / SAMaxTimeout int seconds 30 / 90 SpamAssassin client (IFS.cpp:274-275) SpamAssassinClient.cpp S
ClamMinTimeout / ClamMaxTimeout int seconds 15 / 90 ClamAV (clamd) client (IFS.cpp:282-283) ClamAVVirusScanner.cpp S
SAMoveVsCopy bool 0 Move rather than copy the SpamAssassin result file (IFS.cpp:352) SpamAssassinClient.cpp S
SpamAssassinUser string empty Sent to spamd as the User: header of every PROCESS request, so spamd applies that user's preferences (user_prefs or its SQL preference store) instead of its global configuration. Empty sends no header. Control characters are refused and read as empty (added 5 September 2026) SpamTestSpamAssassin.cpp, SpamAssassinClient.cpp S
SpamAssassinUserFromRecipient bool 0 When 1 and the message has exactly one recipient, that recipient's address is sent as the User: header instead of SpamAssassinUser. A scan runs once per message, so a message to several recipients falls back to SpamAssassinUser (or to no header). Off by default so no existing spamd sees a header it was never sent (added 5 September 2026) SpamTestSpamAssassin.cpp S
SpamAssassinLearnOnMove bool 0 A message the user moves (or copies) into their \Junk folder is told to spamd as spam and one moved out of it as ham - spamc's TELL with Set: local, into the Bayes store the verdicts come from, named for the mailbox owner when SpamAssassinUserFromRecipient is on. Off by default: spamd refuses TELL unless started with --allow-tell (getter GetSpamAssassinLearnOnMove; IFS.cpp:354). Added 6 September 2026 SpamAssassinLearner.cpp (task on the asynchronous work queue), SpamAssassinClient.cpp (TELL mode), IMAPMove.cpp, IMAPCopy.cpp S
ScriptTimeout int seconds 60 Bound on an event script run; 0 = none (IFS.cpp:285-307) ScriptServer.cpp S
ScriptAllowedObjects string * 5 Sep 2026: the COM classes an event script may create (CreateObject / new ActiveXObject), by ProgID or {CLSID}, comma-separated; * = any (absent means *), empty = none. Enforced through IInternetHostSecurityManager on the script site; a denied class fails in the script with 429 and is logged ScriptObjectPolicy.cpp, ScriptSite.h S
ExternalProcessTimeout int seconds 300 Bound on external virus scanner / process; 0 = none (IFS.cpp:308) ProcessLauncher.cpp S
DBConnectionAcquireTimeout int seconds 60 Bound on waiting for a pooled DB connection; 0 = unbounded (IFS.cpp:300-306; DatabaseConnectionManager.cpp:436-440) DatabaseConnectionManager.cpp S
DatabaseStatementTimeout int seconds 30 <0 → 0; per-statement timeout on all four backends; 0 = none (IFS.cpp:645-651; IFS.h:350-363) DALConnection.cpp S
AsyncQueueStallThreshold int seconds 120 How long all async workers may be busy before the server reports what holds them (IFS.cpp:310-314) WorkQueue.cpp S
AsyncQueueReservedThreads int 2 Threads kept free of scanning/scripting for short work (IFS.cpp:315) WorkQueue.cpp S
ShutdownDrainSeconds int seconds 0 Wait up to N s for sessions to finish on stop; ≤0 = no wait (IFS.cpp:408; Application.cpp:822-850) Application.cpp S! (stop)

9.12 IMAP search, indexing, expunge tombstones, message loading

Key Type Default Validation What it does Consumer Timing
IMAPSearchTimeout int seconds 60 <0 → 0 Ceiling on one SEARCH/SORT; 0 = off (IFS.cpp:317-348) IMAPCommandSearch.cpp S
IMAPSearchMaxMegabytes int MB 2048 <0 → 0 Ceiling on message content examined by one search; 0 = off (IFS.cpp:349-350) IMAPCommandSearch.cpp S
IndexerFullMinutes int minutes 720 Full metadata-index pass cadence (IFS.cpp:354) MessageIndexer.cpp S
IndexerFullLimit int 25000 Max messages per full pass (IFS.cpp:355) PersistentMessageMetaData.cpp S
IndexerQuickLimit int 1000 Max messages per quick pass (IFS.cpp:356) PersistentMessageMetaData.cpp S
IndexerFullText bool 0 Full-text term index (hm_messageindexterms) for SEARCH BODY/TEXT; also requires COM Settings.MessageIndexing (getter GetIndexerFullTextEnabled) (IFS.cpp:358-365; IFS.h:228-236; MessageIndexer.cpp:87,109,169) MessageIndexer.cpp, IMAPCommandSearch.cpp S
IndexerFullTextBatchSize int 250 1..100000 Backfill id-range width per pass (IFS.cpp:367-374) MessageIndexer.cpp S
IndexerFullTextMinTokenLength int 3 3..64 Shortest run the index answers for (IFS.cpp:376-384) IMAPCommandSearch.cpp S
IndexerFullTextMaxTokensPerMessage int 2048 64..1000000 Terms per message before it is marked always-scanned (IFS.cpp:386-392) MessageIndexer.cpp S
IMAPExpungeRetentionRecords int 5000 <0 → 0 QRESYNC tombstones kept per mailbox; 0 = keep all (IFS.cpp:646-654; IFS.h:365-381) IMAPExpungeRetentionTask.cpp S
LoadHeaderReadSize int bytes 4000 Initial read size when loading a message header (IFS.cpp:393) PersistentMessage.cpp S
LoadBodyReadSize int bytes 4000 (IFS.cpp:394) PersistentMessage.cpp S
IMAPCompressionEnabled bool 1 RFC 4978 COMPRESS=DEFLATE: advertised in CAPABILITY until the session is compressed, and honoured by COMPRESS DEFLATE; 0 = not advertised and the command answers BAD like an unknown command (IFS.cpp:498; IMAPCommandCapability.cpp:60; IMAPCommandCompress.cpp:30). Getter is GetImapCompressionEnabled (note the case). New in 6.2.28 IMAPCommandCapability.cpp, IMAPCommandCompress.cpp S

9.13 Storage, archive, backup, quarantine, trace, disk space, filter hook, AV policy, fault injection

Key Type Default Validation What it does Consumer Timing
ArchiveDir path "" trailing \ stripped Copy of every message received by SMTP into ArchiveDir\<domain>\<user>\...; empty = off (IFS.cpp:262-264; SMTPConnection.cpp:1756-1775) SMTPConnection.cpp, ArchiveRetentionTask.cpp, AddressTraceEraser.cpp S
ArchiveHardLinks bool 0 Hard-link instead of copy (getter GetArchiveHardlinks) (IFS.cpp:265) SMTPConnection.cpp S
ArchiveRetentionDays int days 0 Delete archived messages older than N; 0 = keep (IFS.cpp:644; IFS.h:344-348) ArchiveRetentionTask.cpp S
ArchiveDomains comma-separated domains "" trimmed, lower-cased Scope of the archive (added 5 Sep 2026): empty = every message; otherwise a message is archived only when its local sender or a recipient belongs to a listed domain, and only the copies for listed domains are made (IsArchiveDomain, SMTPConnection::ArchiveScopeIncludesMessage_) SMTPConnection.cpp S
DeliveryHardLinks bool 0 A message to several local recipients is one file with a name in each recipient's folder instead of a copy per recipient; safe because every rewrite of a message file is a temporary file renamed into place, so a change to one copy replaces that name alone. Needs AddDeliveredToHeader off (that header names each recipient). Off by default, like ArchiveHardLinks: a link is a promise that nothing outside the server edits a message file in place (getter GetDeliveryHardLinks) 5 Sep 2026. PersistentMessage.cpp; the atomic write is FileUtilities::WriteToFileAtomically, used by MimeBody::SaveAllToFile for every message rewrite S
(no new key) - - - 5 Sep 2026: with ArchiveDir set, every copy is also recorded in hm_archiveindex (schema 6029); see the archive index entry in apis.md. A held row keeps its file through ArchiveRetentionDays and address erasure PersistentArchiveIndex.cpp S
BackupMessagesDBOnly bool 0 Backup/restore skips message files but keeps DB message rows (IFS.cpp:397; BackupExecuter.cpp:361-362, 736) BackupExecuter.cpp, BackupRestorer.cpp S
ScheduledBackupTime HH:MM local "" strict 24-h parse; unparsable → logged, no daily backup Daily backup time; wins over interval (IFS.cpp:444; IFS.h:558-564; BackupScheduleTask.cpp:68-87,146-149) BackupScheduleTask.cpp S!
ScheduledBackupIntervalMinutes int 0 Interval schedule; 0 = none (IFS.cpp:445) BackupScheduleTask.cpp S!
ScheduledBackupKeepCount int 0 Archives to keep; 0 = never delete (IFS.cpp:446) BackupExecuter.cpp, BackupScheduleTask.cpp S
BackupVerifyRestore 0/1 1 Verified restore (added 5 Sep 2026): after every backup that includes messages, BackupExecuter::VerifyRestore_ extracts the message store to the temp directory through BackupRestorer::Prepare, holds it to exactly the files staged (count and bytes; a mismatch discards the archive), and names every message row without a file in the backup log (never fails the backup). Skipped with a log line when the temp volume cannot hold the store; 0 skips it altogether BackupExecuter.cpp S
ScheduledBackupMaxAgeDays int 0 0 = never delete (IFS.cpp:447) BackupExecuter.cpp, BackupScheduleTask.cpp S
QuarantineEnabled bool 0 Store spam that would have been REFUSED instead (IFS.cpp:624; IFS.h:285-290) QuarantineStore.cpp S
QuarantineRetentionDays int days 30 0 = never sweep (IFS.cpp:625) QuarantineStore.cpp S
MessageTraceEnabled bool 0 Queryable who-corresponded-with-whom table (privacy default) (IFS.cpp:626; IFS.h:292-296) MessageTrace.cpp S
MessageTraceRetentionDays int days 30 (IFS.cpp:627) MessageTrace.cpp S
MessageStoreFsync bool 0 Flush spool file to disk before 250; failure refuses the message (IFS.cpp:410; TransparentTransmissionBuffer.cpp:311,433) TransparentTransmissionBuffer.cpp S
MinimumFreeDiskSpaceMB int MB 100 <0 → 0 Below this new mail gets SMTP 452 4.3.1 / IMAP APPEND NO [UNAVAILABLE]; 0 = off (IFS.cpp:412-418; IFS.h:449-470) DiskSpace.cpp S
DiskSpaceWarningThresholdMB int MB 1024 <0 → 0 Application-log warning threshold (IFS.cpp:419-420) DiskSpace.cpp S
FilterHookUrl URL "" External filter engine (rspamd-style) for every accepted message; empty = off (IFS.cpp:658; IFS.h:391-394) SpamTestFilterHook.cpp S
FilterHookTimeoutSeconds int 10 <1 → 1 (IFS.cpp:659, 664-665) SpamTestFilterHook.cpp S
FilterHookFailClosed bool 0 1 = refuse mail when the engine does not answer (IFS.cpp:660) SpamTestFilterHook.cpp S
FilterHookRejectScore int 100 Spam score assigned to the engine's "reject" (IFS.cpp:661) SpamTestFilterHook.cpp S
FilterHookMaxMessageSizeKB int KB 10240 Larger messages bypass the hook; 0 = no ceiling (IFS.cpp:662) SpamTestFilterHook.cpp S
AVFailAction int 0 0..1 else 0 0 deliver unscanned; 1 hold+retry then bounce (IFS.cpp:527-539; IFS.h:637-641) SMTPDeliverer.cpp S
AVFailRetryMinutes int 15 <1 → 15 Hold retry interval (IFS.cpp:544-546) SMTPDeliverer.cpp S
AVFailMaxHolds int 16 <0 → 16 Hold attempts before bounce; 0 = bounce immediately (IFS.cpp:551-553) SMTPDeliverer.cpp S
SimulateSpoolWriteFailure int 0 Test-only: 1 every spool write fails, 2 first succeeds then fail (IFS.cpp:422; IFS.h:482-504; TransparentTransmissionBuffer.cpp:492-495) TransparentTransmissionBuffer.cpp S
SimulateDatabaseFailureFor string "" Test-only: fail every SQL statement containing this substring; reported as HM6119 (High) on every start/reinitialise while set (IFS.cpp:423-424; Application.cpp:205-224) DatabaseConnectionManager.cpp, Application.cpp S

9.14 [Settings] keys read outside LoadSettings

UseLanguage is the only one; CalDavRedirectUrl/CardDavRedirectUrl are latched by LoadSettings since the HTTP foundation (new in 6.2.28) and are in section 9.5.

Key Type Default What it does Where read Timing
UseLanguage string English COM Settings.UserInterfaceLanguage; file read on every get; set writes the file and the mirror (IFS.cpp:1110-1131; InterfaceSettings.cpp). Since 6.3.4 the set stores the row and then writes the line where the service may write the file, like every other key, and the get still reads the file IFS.cpp:1115 L

9.15 Live update (check, verify, apply)

Every key here is new in 6.2.28. The scheduled task UpdateCheckTask (Common/Application/UpdateCheckTask.cpp) is created at start regardless of these keys (runs once at start, then every 15 minutes: Application.cpp:709-716) and calls the getters in DoWork, so every key is S. UpdateChecker.cpp, UpdateDownloader.cpp, UpdateInstaller.cpp, UpdateWindow.cpp and SigstoreVerifier.cpp are under Common/Util/.

Key Type Default What it does Consumer Timing
UpdateCheckEnabled bool 0 Turns on the scheduled release check: when 1, UpdateCheckTask reads the release feed every UpdateCheckHours and logs/records when a newer release exists (UpdateCheckTask.cpp:139-151; IFS.cpp:489). An on-demand check over COM or REST calls UpdateChecker::CheckNow directly and ignores this key (InterfaceStatus.cpp:275; RestApiServer.cpp:8232). A helper-applied update is reported on the next start whether or not this is on (UpdateCheckTask.cpp:134) UpdateCheckTask.cpp, UpdateChecker.cpp S
UpdateChannel string stable prerelease (or pre-release, case-insensitive) makes pre-releases eligible and switches the default feed; anything else is the stable channel (UpdateChecker.cpp:95-99, 488-492; IFS.cpp:490) UpdateChecker.cpp S
UpdateFeedUrl URL "" Release feed in GitHub's releases JSON shape (one release or a list). Empty = https://updates.progressiverobot.com/releases/latest on the stable channel or https://updates.progressiverobot.com/releases.json on the pre-release channel since 6.3.4, which GitLab Pages serves from the project's releases (UpdateChecker.cpp:177-178, 259 at e3df8b5e6); up to 6.3.3 it was https://api.github.com/repos/Progressiverobot/hmailserver/releases/latest or .../releases?per_page=10, which has answered HTTP 404 since GitHub suspended the project's account on 18 September 2026, so such a server's check fails with The update feed answered HTTP 404. until this names https://updates.progressiverobot.com/releases.json. Set it for a mirror - which must be https unless it is on this machine, since HttpsClient refuses plain http to anything but a loopback address (HttpsClient.cpp:315-319). The per-tag lookup keeps everything up to /releases of the configured URL (UpdateChecker.cpp:274-283; IFS.cpp:491) UpdateChecker.cpp S
UpdateCheckHours int hours 24 Interval between feed reads once the check is on; <1 → 1 (IFS.cpp:495-497; UpdateChecker.cpp:326-333). No ceiling is applied by the server although the Control Panel's label says "1 to 168" (FeatureSettingsView.xaml.cs:1933) UpdateChecker.cpp S
UpdateAutoDownload bool 0 When a check finds a newer release, download its installer and Sigstore bundle at once into DataFolder\Updates and verify them (UpdateCheckTask.cpp:157-162; UpdateDownloader.cpp:39-46; IFS.cpp:509). With 0 the download is a person's click UpdateCheckTask.cpp, UpdateChecker.cpp S
UpdateWindow string "" When a verified installer may be applied unattended. 03:00 = every day from 03:00 for an hour; Sun 03:00 = Sundays; Sat,Sun 02:00-05:00 = those days between the two times. Days are English three-letter names in any case/order, times are local 24-hour; a range crossing midnight belongs to the day it starts on (UpdateWindow.h:11-22). Empty = never apply on its own. An unparsable value is logged once per process and nothing is applied (UpdateCheckTask.cpp:171-183; IFS.cpp:508) UpdateCheckTask.cpp, UpdateChecker.cpp, UpdateWindow.cpp S
UpdateBackupBeforeApply bool 1 Before an unattended apply, take a backup to the configured backup destination; no destination or a failed backup cancels that apply (UpdateCheckTask.cpp:82-118, 186; IFS.cpp:510). Applies to the unattended path only UpdateCheckTask.cpp S
UpdateRequireAuthenticode bool 0 After the Sigstore bundle verifies, also require a trusted Authenticode signature on the downloaded installer; otherwise the download is refused (UpdateDownloader.cpp:143-146; IFS.cpp:504) UpdateDownloader.cpp S
UpdateServiceWaitSeconds int seconds 180 Passed as --wait to hMailServer.Updater.exe: how long the helper gives the service to come back after the installer ran before it reinstalls the previous version; <5 → 5 (IFS.cpp:505-507; UpdateInstaller.cpp:26, 234) UpdateInstaller.cpp S
UpdateTrustRootsFile path "" PEM file whose CA certificate(s) replace the public Sigstore trust roots the bundle's certificate chain is checked against. Must exist and contain a PEM certificate, else verification fails with that reason (SigstoreVerifier.cpp:415-434; IFS.cpp:499) SigstoreVerifier.cpp S
UpdateLogPublicKeyFile path "" PEM public key of the transparency log whose inclusion proof the bundle carries; replaces the public instance's key. Must exist and parse as a PEM public key (SigstoreVerifier.cpp:436-451; IFS.cpp:500) SigstoreVerifier.cpp S
UpdateSigningIdentity string "" Prefix the signing certificate's identity must carry; empty = each compiled-in signer's own: up to 6.3.3 https://github.com/Progressiverobot/hmailserver/.github/workflows/sign-release.yml@ alone, and since 6.3.4 that one, https://gitlab.com/Progressiverobot/hmailserver//.gitlab-ci.yml@refs/tags/v and https://gitlab.com/hmailserver/hmailserver//.gitlab-ci.yml@refs/tags/v (SigstoreVerifier.cpp:103, 131, 174, 611-615 at 396c01d4d; IFS.cpp:501) SigstoreVerifier.cpp S
UpdateSigningIssuer string "" OIDC issuer the certificate must name; empty = each compiled-in signer's own: https://token.actions.githubusercontent.com for the GitHub workflow, and since 6.3.4 https://gitlab.com for the two GitLab entries (SigstoreVerifier.cpp:104, 132 at 396c01d4d; IFS.cpp:502) SigstoreVerifier.cpp S
UpdateSourceRepository string "" Repository the certificate must name; empty = each compiled-in signer's own: https://github.com/Progressiverobot/hmailserver, and since 6.3.4 https://gitlab.com/Progressiverobot/hmailserver and https://gitlab.com/hmailserver/hmailserver, the group the project moved to on 29 September 2026, where the certificate must also carry the project's id, 86730559 (since 6.3.6 a certificate for https://gitlab.com/Progressiverobot/hmailserver needs the id too, as the group's path always did); the single character - switches the repository check off, except that since 6.3.4 a certificate that names the group's path or its repository is refused without the project's id whatever this key and the other two say, - included (SigstoreVerifier.cpp:105, 133, 175, 719, 1233-1245 at 396c01d4d; IFS.cpp:503). Up to 6.3.3 the defaults of this key, UpdateSigningIdentity and UpdateSigningIssuer name GitHub's release workflow alone, which signed the releases from 6.2.19 to 6.3.3 and can sign no later one, so such a server refuses a release signed anywhere else unless the three are set to its identity. Since 6.3.4 the defaults trust the GitHub workflow for the releases up to 6.3.3 and the project's GitLab CI pipeline for a v* tag at either path; the three set to the values of the path the project began at keep a server to that path alone, so it refuses what is signed after the move, every release from 6.3.6. Never set the three to the group's path: a server from 6.2.28 to 6.3.3 checks no project id, so it would install whatever anyone who holds that path signs for a v* tag, and since the project moved such a server is upgraded by hand (UpdateFeed.md) SigstoreVerifier.cpp S

Since 6.3.6 the five trust keys - UpdateTrustRootsFile, UpdateLogPublicKeyFile, UpdateSigningIdentity, UpdateSigningIssuer and UpdateSourceRepository - are read from hMailServer.ini's [Settings] section alone, on every platform, never from the database (item 2 of 6.3.6's "What changes on upgrade"). At the first start a value 6.3.4 or 6.3.5 stored in the database is taken out (HM5809 names it once when it differed from the file's); a line for one of the five in [SettingsOverride] is ignored and named as HM5809 at every start. COM and the REST API refuse a change to them (409), and so the Control Panel, the Deck, a settings profile and the audit trail's put-back; hmctl config export, Export-HmConfiguration and a backup leave them out. Set them in the file - on Linux sudo hmailserver --set-setting NAME=VALUE - and restart (The Update Feed, Where the trust is read from).

9.16 Forward proxy for the requests the server itself makes

One key, new in 6.2.28. It is read by Common/Util/HttpsClient.cpp, which is the web client behind four fetches: the release feed and the installer/bundle downloads (UpdateChecker.cpp, UpdateDownloader.cpp, section 9.15) and the OAuth2 JWK Set and token introspection (JwksKeySet.cpp, TokenIntrospection.cpp, section 9.2). AcmeClient.cpp and OutboundOAuth2TokenClient.cpp build their own TLS connections and do not honour it, so ACME issuance and the outbound-relay token request still go direct.

Key Type Default What it does Consumer Timing
HttpProxy host:port or [ipv6]:port "" Empty = connect direct. Set, every request through HttpsClient goes to the proxy. For an https target the proxy is asked to CONNECT host:port and the TLS handshake then runs inside the tunnel, with verify_peer, the same CertificateVerifier and the same SNI name as a direct connection, so the proxy sees the host name and nothing after it (HttpsClient.cpp:138-187, 370-378). For plain http the request line carries the absolute URL instead of the path (HttpsClient.cpp:191-198) - although HttpsClient refuses plain http to anything but a loopback address whether or not a proxy is set, so the absolute-URL form only ever carries a feed hosted on this machine (HttpsClient.cpp:315-319, 517-521). A proxy answering CONNECT with anything outside 2xx fails the request with The proxy <host:port> refused CONNECT to <host:port>: <its status line> (HttpsClient.cpp:179-183). A value with no port, or a bracketless IPv6 literal, fails it with HttpProxy must be host:port (or [ipv6]:port). — a mis-set proxy is never quietly a direct connection; surrounding whitespace is trimmed (HttpsClient.cpp:89-131; IFS.cpp:492). No proxy credentials are sent. Editor: beside the feed URL on the Control Panel's Updates card (FeatureSettingsView.xaml.cs:1936) HttpsClient.cpp, for UpdateChecker, UpdateDownloader, JwksKeySet, TokenIntrospection, and since 6.3.6 AcmeClient, OutboundOAuth2TokenClient, the MTA-STS policy fetch, brand logos, WKD, Web Push, unsubscribe and inbound webhook routes held to public addresses. A request held to public addresses (the MTA-STS fetch and those after it) asks the proxy for a tunnel to the address the server checked, never the name, so a proxy that allows CONNECT only to names refuses it; the refusal is reported by the proxy's status code alone ("HttpProxy refused CONNECT to X with status N."). A request to an endpoint the administrator chose still quotes the proxy's status line S

10. [SendingLimits] and [SendingLimitsOverrides] sections (per-account sending quotas)

  • Read by RateLimiter::LoadSettings_ directly with GetPrivateProfileString/Section, NOT through IniFileSettings and NOT mirrored to hm_inisettings (source: Common/Util/RateLimiter.cpp:30-31, 1025-1160).
  • Re-read live when the file's last-write time changes, checked at most every 2 s (kSettingsRecheckSeconds, RateLimiter.cpp:61, 1166-1181). Whole mechanism inert until configured (RateLimiter.h:15).
Section Key Type Default Clamp Meaning (source RateLimiter.cpp)
[SendingLimits] MaxMessagesPerAccountPerPeriod int 0 <0 → 0 Messages an authenticated account may submit per period; 0 = unlimited (1050-1051, 1062)
[SendingLimits] MaxRecipientsPerAccountPerPeriod int 0 <0 → 0 Recipients per period; 0 = unlimited (1053-1054, 1063)
[SendingLimits] PeriodHours int 24 1..168 Rolling period (53-54, 1056-1057, 1064-1069)
[SendingLimits] StateSaveIntervalSeconds int 10 <1 → 10; >3600 → 3600 Persist counters to DataFolder\hmailserver_sendinglimits.dat (34, 64-66, 1059-1074, 268-270)
[SendingLimitsOverrides] <address>=<messages>:<recipients>[:<hours>] per-line — hours 1..168 Per-account override; address lower-cased/trimmed; malformed lines counted and logged (982-1020, 1093-1146)
  • Enforced in SMTPConnection.cpp:999-1065 (TryConsumeAccountMessage/Recipients).

One authenticated submission, drawn, because the shape of the refusal is the part an administrator gets asked about:

flowchart TD
    MF["MAIL FROM"] --> AUTH{"Is the session<br/>authenticated?"}
    AUTH -- no --> PASS["Not subject to this ceiling. It is a control on<br/>what OUR accounts send, not on what we accept"]
    AUTH -- yes --> KEY["Key on the authenticated user name, with the<br/>default domain applied - never on MAIL FROM,<br/>which a compromised account can set to anything"]
    KEY --> OV{"An entry for that address in<br/>[SendingLimitsOverrides]?"}
    OV -- yes --> LIM["that line's messages:recipients[:hours]"]
    OV -- no --> GLOB["[SendingLimits], the global pair"]
    LIM --> ON{"Either limit non-zero?"}
    GLOB --> ON
    ON -- no --> PASS
    ON -- yes --> PRUNE["Sum the account's buckets inside the rolling<br/>period, discarding the ones that have aged out.<br/>It is a rolling window, not a clock that resets"]
    PRUNE --> MSG{"Would this message<br/>exceed the message limit?"}
    MSG -- yes --> R470["452 4.7.0 Sending limit for this account has<br/>been reached. Please try again later."]
    MSG -- no --> COUNT["One message counted"]
    COUNT --> RCPT["Each RCPT TO, using the limits<br/>resolved at MAIL FROM"]
    RCPT --> RQ{"Would this recipient<br/>exceed the recipient limit?"}
    RQ -- yes --> R453["452 4.5.3 Recipient sending limit for this<br/>account has been reached. Please try again later."]
    RQ -- no --> OK["Counted, and the transaction continues"]
    R470 --> LOG["Application log, NOT the ERROR log:<br/>SendingLimit refused submission, with the account,<br/>the IP, the reason, usage this period and the limits.<br/>The refusal consumes no budget and 452<br/>leaves the session open"]
    R453 --> LOG
Loading

A limit that cannot be worked out - an unreadable ini file, an exception anywhere in the limiter, or the internal table of tracked accounts being full (reported once as error 5813) - allows the message. Failing open is deliberate: refusing mail because a counter is unavailable would be a worse fault than not counting one message. Counters survive a restart through DataFolder\hmailserver_sendinglimits.dat, written at most every StateSaveIntervalSeconds (source: Common/Util/RateLimiter.cpp GetAccountLimits, CheckAndConsume_; Server/SMTP/SMTPConnection.cpp CheckAccountSendingQuotaAtMailFrom_, RefuseForAccountSendingQuota_).

11. [LDAP] section (directory authentication and directory sync)

  • Read by LdapSettings::Load_ directly (Common/LDAP/LdapSettings.cpp:206-345), re-read when the file timestamp changes, checked at most every 2 s (LdapSettings.cpp:20-24, 159-204). Not mirrored to hm_inisettings. Strings are trimmed; 4096-char buffer (350-362).
Key Type Default Clamp/validation Meaning (source LdapSettings.cpp / .h)
Enabled int (≠0 = on) 0 Master switch (229)
Server host "" required Directory host (230; IsComplete 125-145)
Port int 0 1..65535 else derived 0 → 636 for LDAPS, 389 otherwise (231; .h EffectivePort 84-90)
Security int 2 0 plain, 1 StartTLS, anything else → 2 LDAPS (233-247; .h:18-23)
VerifyCertificate int (≠0) 1 (251)
AllowUnprotectedPassword int (≠0) 0 Permit simple bind over plain transport (252)
BindMethod int 0 1 = Negotiate (SSPI Kerberos/NTLM), else Simple. Windows only: on Linux 1 is refused (HM6420) (254-255; .h:38-42)
SearchBase DN "" required unless UserDnTemplate set or Negotiate (257; 125-145)
UserSearchFilter LDAP filter (&(objectCategory=person)(objectClass=user)(sAMAccountName=%u)) empty → default (30-32, 258, 265-267)
UserDnTemplate DN template "" Bind directly without searching (259)
ServiceUsername / ServiceDomain / ServicePassword string "" Search-bind credentials (260-262)
TimeoutSeconds int 10 1..90 else 10 (38-42, 269-273)
FallbackToWindowsLogon int (≠0) 0 Fall back to LogonUser (275). Windows only; on Linux the fallback refuses (HM6404)
SyncFilter LDAP filter (&(objectClass=user)(objectCategory=person)(mail=*)(!(userAccountControl:1.2.840.113556.1.4.803:=2))) empty → default Directory-sync selection (56-60, 277, 285-286)
SyncMailAttribute attr mail empty → default (62, 278)
SyncUsernameAttribute attr sAMAccountName empty → default (63, 279)
SyncDisplayNameAttribute attr displayName empty → default (64, 280)
SyncMaxUsers int 5000 ≤0 → 5000; >100000 → 100000 (66-72, 297-309)
SyncScheduleMinutes unsigned int 0 0 = off; 1..14 → 15; >10080 → 10080 Read unsigned to avoid INT wrap (76-82, 311-338)
  • Consumers: Common/LDAP/LdapDirectoryAuthenticator.cpp, LdapClient.cpp, DirectorySync.cpp; Control Panel Views/LdapSettingsView.cs:61,715,735 reads/writes [LDAP] directly with a 4096 buffer.

12. Companion file: hMailServerApiKeys.ini (REST API keys)

  • Path: same directory as hMailServer.ini, name hMailServerApiKeys.ini (RestApiServer.cpp:2017-2055; Control Panel Services/ApiKeyStore.cs:81,137).
  • One section per key, [Key.<16 lower-hex id>] with Label, Hash (SHA-256 hex of the token, unsalted, by design), Expires (YYYY-MM-DD HH:MM:SS local, required; missing/unreadable = expired), AllowedFrom (IP, lower-upper, or CIDR; empty = any), Scope (full or readonly; anything else/missing = readonly), Domains (comma list; empty = all) (RestApiServer.cpp:88-111, 2033-2050, 2484-2508).
  • Parsed from the file bytes on every request (no profile cache) so revocation by editor takes effect immediately; written with WritePrivateProfileString, hash last (RestApiServer.cpp:2081-2090, 2512-2531). Default key lifetime 90 days (2417-2424, const at 105). No file = no keys, not an error (2094-2097).
  • No key of any scope can create/revoke keys; that needs the administrator password (preamble 2497-2505).
  • 6.3.4, a key's section also carries Role (the administrator role it holds, batch 10) and three fields of the send API (batch 31): SendFrom (an address of a hosted domain, or *@domain; empty - every key made before - sends nothing), SendPerMinute and SendPerDay, written before Hash as the other restrictions are and read back failing closed. The key store is not visible over COM, so these are set with POST /api/v1/apikeys (source: RestApiServer.cpp HandleCreateApiKey_, LoadKeys_; hmailserver/docs/SendApi.md).

13. Other readers/writers of hMailServer.ini (for the "who touches this file" page)

  • Server COM layer: InterfaceSettings, InterfaceDatabase, InterfaceDirectories, InterfaceLogging, InterfaceScripting, InterfaceBackupSettings via IniFileSettings (files under COM/).
  • Control Panel hMailCP: Services/IniFeatureStore.cs ([Settings], 2048-char read buffer, direct file when local else COM in 6.3.3; since 6.3.4, COM for every write and COM first for reads, section 4), Views/FeatureSettingsView.xaml.cs (exposes the keys listed there), Views/LdapSettingsView.cs ([LDAP]), Services/ApiKeyStore.cs, Services/WindowsServiceInfo.cs, Views/BackupView.xaml.cs, Views/DirectorySyncView.cs, Views/QuarantineView.cs, Views/MessageTraceView.cs, Views/ApiKeysView.cs.
  • Installer: installation/section_ini.iss writes [Directories], [GUILanguages], [Security]; hMailServerInnoExtension.iss relocates the file on upgrade.
  • build/hmconfig.ps1 (header says it configures COM objects "and hMailServer.ini"; key-level behaviour not verified here).
  • hmailserver/tools/PasswordDisplayer/Form1.cs:39-42 reads [Database] password.
  • Regression tests: test/RegressionTests/Shared/IniFileSetting.cs, ServerIniFile.cs; 50 distinct keys are written by tests and all 50 exist in the code (checked by diff).
  • Windows service registration (/Register) — section 3 item 5.
  • build/check-ini-coverage.py (new in 6.2.28): fails - in CI until 18 September 2026, by hand since - when a [Settings] key read by IniFileSettings.cpp is not named anywhere in the Control Panel source (only SimulateDatabaseFailureFor and SimulateSpoolWriteFailure are exempt) or when a new ini section is read outside IniFileSettings (only [LDAP], [SendingLimits], [SendingLimitsOverrides] are allowed). It matches literal ReadIniSetting..._("Settings", "Key" calls, so it does not see the three PasswordHash* keys read through ReadPasswordHashWorkFactor_ and reported 234 keys where LoadSettings read 237; in 6.3.3 it sees 238 of 241. In 6.3.4 it also holds the rule that a setting goes in the database, against build/ini-settings-baseline.txt (section 4a).

14. Error / log codes emitted by the ini layer

Every one of these is written to the application log, and to the Windows event log when WindowsEventLogEnabled=1 and WindowsEventLogLevel is at or above the severity. Only HM5005 stops the server; the rest change what a setting means and let it run.

Code Severity Raised when What the server does then Source
HM5005 Medium [Database] Server or Database is missing for the configured Type Application::OpenDatabase returns false, so the database is never opened and the server does not start Application.cpp:197-203, OpenDatabase
HM5528 Medium PreferredHashAlgorithm is not 3, 4, 5 or 7 Read as 4 (PBKDF2). New passwords are hashed with PBKDF2, not with whatever was meant IFS.cpp:190
HM5561 Medium PasswordHashIterations, PasswordHashMemoryKB or PasswordHashTimeCost is outside its allowed range Read as 0, which means the built-in work factor. Existing hashes are unaffected IFS.cpp ReadPasswordHashWorkFactor_
HM5562 Medium SpamAssassinUser contains a control character Read as empty. No User: header is sent to spamd, so its global configuration applies IFS.cpp:347
HM5780 Medium CalDavRedirectUrl or CardDavRedirectUrl is not a usable absolute URL /.well-known/caldav and /.well-known/carddav answer 404. Reported once per service start WebServicesServer.cpp:1126-1136
HM6119 High SimulateDatabaseFailureFor is set Every SQL statement containing that substring fails, on purpose. Reported on every start and reinitialise, because a server running with it is deliberately broken Application.cpp:221, OpenDatabase
HM6210 Medium DmarcRptSchemaVersion is not 1 or 2 Read as 1 (RFC 7489 Appendix C). Reports go out in the older schema IFS.cpp:522
HM4401 Low DNSServer is not a parseable IPv4 literal The custom resolver is not used: every lookup goes to the system DNS instead. A host:port value or an IPv6 literal lands here, and since 6.3.6 an older form such as 8.8.8 DNSResolverWinApi.cpp:261
HM5800 Medium A [Settings] key name is longer than 100 characters That key is skipped for the mirror. It still works from the file ISS.cpp ReadIniSection_
HM5801 Medium A [Settings] value is longer than 4000 characters Same: skipped for the mirror, works from the file ISS.cpp ReadIniSection_
HM5802 Medium hm_inisettings cannot be read at all The server runs on the file alone, and a backup that includes settings fails - "The server settings could not be written to the backup... No archive has been created" - until it is fixed ISS.cpp Synchronize, before pass 1; BackupExecuter, Configuration::XMLStore
HM5803 Medium (6.3.3) A database-side change could not be written back into the file The change is not applied. The file's value is used for this run. The service account needs write access to hMailServer.ini ISS.cpp Synchronize, pass 1
HM5804 Medium (6.3.3) The file and the row both changed since they last agreed The file wins and the row is overwritten. The database-side change is lost ISS.cpp Synchronize, pass 1
HM5805 Medium A restore could not write a setting into the file The row is restored; the file is not ISS.cpp XMLLoad, the restore path
HM5806 Medium (6.3.3) SetIniSetting wrote the file but could not read the mirror afterwards The file holds the new value; the row may not ISS.cpp WriteSetting

Since 6.3.4 (section 4a), HM5800, HM5801, HM5802 and HM5805 mean what the table says, and three codes change meaning while two are new:

Code Severity Raised when What the server does then Source
HM5803 Medium A stored value could not be written into a file the service may write - to put back an edited line, or to bring a stale one up to date The stored value is used. hMailServer.exe /Register and anything else reading the file directly see the old line until a start can write it, and the report repeats at every start until then. A file the service may not write, NT SERVICE\hMailServer's, is not written at all, and that is said in the application log, not as an error (The Service Account) ISS.cpp Synchronize
HM5804 Medium A [Settings] value in the file was changed away from the value the server last wrote there The stored value is used, the key is named, and the line is put back where the service may write the file; said once, because the two then agree again - or, where the file is read-only to the service (NT SERVICE\hMailServer), because the row then records the edited line, which stays in the file ISS.cpp Synchronize
HM5806 Medium A write stored the row but could not write the file; or a removal took out the line but could not delete the row; or, where the service may not write the file, a change that only the file can hold was refused - PasswordPepper, the live update's trust set or removed without an administrator's session, or the removal of a key this build does not read while the file has its line Write: the value is stored and used, and the next start rewrites the line. Removal: the call fails, the setting is not back at its default, and the next start writes the line back. Refusal: nothing is changed, and the sentence names the file and the account the server runs as and says what an administrator does instead (IniFileSettings::InitializationFileRefusal) ISS.cpp WriteSetting, RemoveSetting
HM5807 Medium [SettingsOverride] holds one or more keys Those values are used over whatever is stored. Named at every start and reinitialise, for as long as the section is there IFS.cpp LoadDatabaseSettings
HM5808 Medium A value was stored for a key that [SettingsOverride] forces Stored, but not in effect until the override is removed IFS.cpp WriteSettingsValue
  • HM5113, although not from the ini layer, is the one an administrator meets next: it names an unreadable certificate file or a private key that does not match, and it is what precedes "RestApi: Refusing to start - the shared TLS configuration could not be applied" and the equivalent metrics-listener line (source: SslContextInitializer::InitServer).
  • Nothing in this table is raised by a value that is merely unused. A misspelled key name produces no error at all — it is simply absent, and the default applies. That is the first thing to suspect when a setting appears to do nothing.

15. Unconfirmed (not verified in this pass — record as such, do not publish as fact)

  • Exact COM property name that returns the ini path Confirmed: the property is Application.InitializationFile (hMailServer/hMailServer.idl:1702; COM/InterfaceApplication.cpp:455).
  • Whether the REST API refuses to start when [Security] AdministratorPassword is empty Confirmed: RestApiServer::Start returns without listening and logs "RestApi: Refusing to start - the administrator password is not set." (RestApiServer.cpp:559-561). It likewise refuses a non-loopback bind without a certificate (RestApiServer.cpp:569-578).
  • Whether outbound XOAUTH2 is attempted when OutboundOAuth2Hosts matches but OutboundOAuth2TokenUrl and FixedToken are both empty Confirmed: with both empty no bearer is set and the delivery proceeds with LOGIN/password authentication and no log line; only the token-URL path reports a failure (HM5904) (ExternalDelivery.cpp:681-700).
  • Whether SslContextInitializer::InitClient (outbound) rebuilds its context per connection or once Confirmed: the outbound SMTP/POP3 client context is built once per IOService at start (IOService.cpp:65-68), so TlsKeyExchangeGroups/TlsCipherSuites13 are S! for outbound mail as well; AcmeClient and HttpsClient build a fresh context per use (AcmeClient.cpp:552; HttpsClient.cpp:248, 445), where the keys are effectively S.
  • build/hmconfig.ps1 per-key ini behaviour.
  • AcmeDomains/AcmeContactEmail parsing and requiredness inside AcmeClient.cpp Confirmed: AcmeDomains is comma-split, trimmed, lower-cased, empties dropped; an empty list logs "ACME: No domains configured" and issuance does not run (AcmeClient.cpp:1364-1380). AcmeContactEmail is optional — omitted from the ACME account when empty (AcmeClient.cpp:956-961).
  • The exact archive sub-path layout under ArchiveDir Confirmed: local sender → ArchiveDir\<senderdomain>\<sendername>\Sent-<file>; non-local sender → ArchiveDir\Inbound\<file>; then a copy or hard link per local recipient at ArchiveDir\<recipientdomain>\<recipientname>\<file>, made from the first copy when there is one; names lower-cased (SMTPConnection.cpp:1876-1940). ArchiveDomains can suppress the Sent copy for an unlisted local sender (1880-1886).

16. Contradictions and gaps found (docs vs. code)

  1. settings.md §B says "All 104 INI keys read by IniFileSettings.cpp". In 6.2.28 IniFileSettings read 238 [Settings] keys (237 in LoadSettings + UseLanguage); in 6.3.3, and unchanged in 6.3.4, it reads 242 (section 9), and with the 4 [SendingLimits] keys (section 10) and the 21 [LDAP] keys (section 11), both read outside IniFileSettings, there are 267 keys outside [Database]/[Directories]/[Security]/[GUILanguages], plus the one-line-per-address [SendingLimitsOverrides]. build/ini-settings-baseline.txt holds none of those 25, because it lists only what IniFileSettings reads. settings.md is a 6.2.5 snapshot.
  2. settings.md lists Settings.ServiceAccountName/Password under "Intentionally excluded" from the GUI; FeatureSettingsView.xaml.cs now contains ServiceAccountName, ServiceAccountPassword, ServiceAccountGrants, ServiceAccountStatus. The exclusion note is stale.
  3. settings.md names OpenTelemetry as OtelEndpoint, OtelServiceName only; code also has OtelMetricsEndpoint, OtelLogsEndpoint, OtelMetricsInterval (and the GUI exposes them).
  4. Name traps (key ≠ getter/member name — a doc writer copying from headers gets the wrong key): DaneEnforcementEnabled (getter GetDaneEnabled), IndexerFullText (GetIndexerFullTextEnabled), OAuth2PublicKeyFile (GetOAuth2RsaPublicKeyFile), ArchiveHardLinks (GetArchiveHardlinks), AddXOriginalRcptTo (GetAddXOriginalRcptToHeader), GreylistingRecordExpirationInterval (GetGreylistingExpirationInterval), Passwordencryption read vs PasswordEncryption written, Pop3LoginDelaySeconds (lower-case p3 unlike POP3DMinTimeout).
  5. "Inert by default" shapes to flag on feature pages: MtaStsHostingEnabled=1 and AutoconfigEnabled=1 do nothing until WebServicesHttpPort/HttpsPort > 0 (Application.cpp:542-557 logs this); IndexerFullText=1 does nothing unless COM Settings.MessageIndexing is on; WindowsEventLogEnabled=1 writes nothing on a healthy server at level 2; OutboundOAuth2Hosts/FetchOAuth2Hosts have non-empty defaults but need the token settings; [SendingLimits] and [LDAP] are entirely inert until Enabled/limits are set; MetricsServerPort, RestApiPort, ManageSieveServerPort, WebServices*Port all default to 0 = listener not created.
  6. Buffer mismatch: Control Panel reads [Settings] values with a 2048-char buffer (IniFeatureStore.cs:133; ProfileApi.cs:44-48 does not grow it) while the server uses 4096 (IFS.cpp:829) and [LDAP] in the CP uses 4096 — a value of 2049-4096 chars (a long DPAPI secret) would be silently truncated on the CP side.
  7. LogFolder and UseLanguage use 255-char buffers (IFS.cpp:885, 937) while everything else uses 4096.
  8. The hm_inisettings reconciliation compares key names case-sensitively while the Windows ini API is case-insensitive; a case-only rename of a key causes a one-start insert collision that self-heals (ISS.cpp:206-222) — documented in code, not in settings.md.
  9. DNSServer accepts a single IPv4 address only - four decimal parts since 6.3.6, an older form such as 8.8.8 being reported as HM4401; docs must not suggest host:port or IPv6 (DNSResolverWinApi.cpp:255-291 and memory note "DNS custom-server regression").
  10. No ini key named in hmailserver/docs/*.md or the READMEs is absent from the code (token diff against the 243-key list found only WebServicesServer, a class name).

17. The rows of hm_settings added in 6.3.4, and the four 6.3.6 adds

From schema 6042 a new setting is a row of hm_settings, never a [Settings] key, so everything 6.3.4 made configurable is here rather than in section 9. 193 rows - the names in Common/Application/SettingsDefaults.h at the tag v6.3.4 (313 entries; 311 at 92b8f9f63, 284 at master 540347500) that neither 6.3.3's create script nor its code knew: 164 counted at 540347500, the 27 of storage compression, object storage, mailbox encryption at rest, PST import and the SNMP agent, the last five rows of the table, and the two the audit before the release added, PasskeyAllowedOrigins and WebPushLoopbackHttpForTests, in the passkeys and Web Push rows. Each is read and written over COM through Settings (Property, PropertySet, or a named member), over REST through a settings group row, and from the environment as HM_PROPERTY_<name>; each has its shipped default in SettingsDefaults.h, which is what the changed-from-stock list compares against; and each applies when it is saved, the reader asking for the value when it uses it. A default here is what a new installation's create script writes; an upgrade inserts a row only where one is missing, and in one case, noted, with a different value.

Feature (schema step) Rows, with their defaults Document
Audit trail and alerts (6043) AuditTrailEnabled 1, AuditRetentionDays 0; AlertsEnabled 1, AlertRecipient '', AlertSenderAddress '', AlertDigestEnabled 1, AlertDigestHour 7, AlertMaxPerHour 20, AlertWebhookMaxAttempts 5 AlertsAndAuditTrail.md
Syslog (no step) SyslogEnabled 0, SyslogHost '', SyslogPort 514, SyslogTransport 0, SyslogFacility 2, SyslogMinimumSeverity 6, SyslogLogTypes 86 Syslog.md
Quarantine digests (6046) QuarantineDigestBaseUrl '', QuarantineDigestLinkHours 72 SenderListsAndDigests.md
Bounces and the suppression list (6049) BounceProcessingEnabled 0, BounceReadDeliveryReports 1, BounceHardBounceThreshold 1, BounceMailboxFullIsHard 0, BounceReportWindowDays 7, BounceCountRetentionDays 90, BounceSuppressionAction 0, BounceSuppressionExpiryDays 0 BounceSuppression.md
The password breach check (no step) BreachCheckEnabled 0, BreachCheckUrl https://api.pwnedpasswords.com/range/, BreachCheckMinimumCount 1, BreachCheckUnreachableAction 0 PasswordBreachCheck.md
The OpenPGP key store and Web Key Directory (6050) OpenPGPWebKeyDirectoryEnabled 0, OpenPGPKeyMaximumBytes 65536, OpenPGPKeysPerAccount 8, OpenPGPRequireAddressInUserId 1 OpenPGP.md
Webhooks (6051) WebhooksEnabled 1, WebhookMaxAttempts 5, WebhookMaxPerMinute 60, WebhookRetentionDays 7 Webhooks.md
Country blocking (6052) GeoIpDatabaseFile '', GeoIpDatabaseMaxAgeDays 45, CountryBlockingExemptRanges (loopback, the private and link-local ranges and 100.64.0.0/10), CountryPolicySMTP, CountryPolicyIMAP, CountryPolicyPOP3, CountryPolicyHTTP '' CountryBlocking.md
Sessions and devices (6053) SessionIdleSeconds 0, SessionLifetimeSeconds 0, SessionOnePerDevice 0, SessionBindToAddress 0 (0 is the built-in policy; all four have levels) DevicesAndSessions.md
Web Push (6054) WebPushEnabled 0, WebPushSubject '', WebPushTimeToLiveSeconds 86400, WebPushMaxPerAccount 10, WebPushMaxAttempts 5, WebPushVapidPublicKey '', WebPushVapidPrivateKey '' (write-only; the public key is derived from it), WebPushLoopbackHttpForTests '' (loopback_http_for_tests under /api/v1/settings/push: a switch for the regression suite, off, that lets a push go over plain http to this machine's loopback, from a request made on the machine itself) none of its own; Roadmap2.md
Complaints (6055) ComplaintProcessingEnabled 0, ComplaintMailbox '', ComplaintRetentionDays 180, ComplaintSuppressionThreshold 0 Complaints.md
The reputation watch (6057) ReputationWatchEnabled 1 in a new installation and 0 after an upgrade, ReputationWatchLists (Spamhaus ZEN, SpamCop, PSBL, Mailspike), ReputationWatchAddresses '' ReputationWatch.md
Spam traps (6058) SpamTrapBanMinutes 1440, SpamTrapBlockDays 30, SpamTrapHitRetentionDays 90, SpamTrapTeachSpamAssassin 0 SpamTraps.md
The encrypted backup (6059) backupencryptionpassphrase (write-only; the number says whether one is set) Backup.md
Quota policy (6061) QuotaGracePercent 0, QuotaSendStopPercent 0 (both with levels) QuotaPolicy.md
Other accounts and the import sign-in (6065) ReaderFetchAccountsMax 3, ReaderFetchPrivateHosts 0; ImportOAuthRedirectBaseUrl '', ImportOAuthGoogleClientId '', ImportOAuthGoogleClientSecret '', ImportOAuthGoogleAuthorizeUrl and ImportOAuthGoogleTokenUrl (Google's public endpoints), ImportOAuthMsClientId '', ImportOAuthMsClientSecret '', ImportOAuthMsAuthorizeUrl and ImportOAuthMsTokenUrl (Microsoft's common endpoints) Migration.md
Send as, masked addresses (6066, 6067) ReaderSenderAddressesMax 5, ReaderMaskedAddressesMax 20 (both with levels) SendAs.md, MaskedAddresses.md
Brand logos (no step) BrandLogosEnabled 1 BrandLogos.md
Recover deleted items (6068) RecoverDeletedDays 0, off (with levels) RecoverDeleted.md
Messages that expire (6069) ConfidentialBaseUrl '' ConfidentialMessages.md
Offline mail (no step) ReaderOfflineDaysMax 30 (with levels) OfflineMail.md
Outbound content policy (no step) DlpAccountNumberPattern '' OutboundContentPolicy.md
The reports received (6070) DomainReportRetentionDays 365 DmarcReports.md
Compromised-account detection (6071) CompromiseDetectionMode 1 (watch; 0 off, 2 enforce), CompromiseBaselineDays 30, CompromiseMinimumHistory 20, CompromiseVolumeFloor 30, CompromiseVolumeFactor 4, CompromiseRecipientFloor 20, CompromiseNewRecipientShare 80, CompromiseScoreToLock 3, CompromiseHeldDays 7 CompromisedAccounts.md
Greylisting, hardened (6072) GreyListingSlideOnEarlyRetry 0, GreyListingAbuseThreshold 0, GreyListingAbuseLockMinutes 60 Greylisting.md
SMTP policy (6073) UseSPFSoftFailScore 0, UseSPFPermErrorScore 0, UseSPFTempErrorScore 0, SPFTempErrorDefer 0, RejectMultiRcptNullSender 0, BanMultiRcptNullSender 0, BanMultiRcptNullSenderHours 24 SmtpPolicy.md
The sample domain (no step) SampleDomainId 0 - the server's own record of the sample it made, set by nobody SampleDomain.md
Account recovery (6076) AccountRecoveryEnabled 1, RecoveryResetLinkMinutes 30, RecoveryVerifyLinkHours 24 AccountRecovery.md
Passkeys and OpenID Connect (6077) PasskeysEnabled 1, PasskeyRelyingParty '', PasskeyAllowedOrigins '' (passkey_allowed_origins: the origins this server's pages are served at, comma separated; required once a relying party is set, and while a relying party is set without it no passkey is registered or taken); OidcIssuer '', OidcClientId '', OidcClientSecret '' (write-only), OidcScopes openid email, OidcRedirectBaseUrl '', OidcAutoProvision 0, OidcAdministrators '' PasskeysAndSingleSignOn.md
The directory (6078) DirectoryEnabled 1, DirectoryUserEditable 1 (with levels) Directory.md
OpenPGP in the page (6080) OpenPGPPortalEnabled 1 (with levels), OpenPGPLookupWkd 1, OpenPGPLookupKeyserver 0, OpenPGPKeyserverUrl https://keys.openpgp.org, OpenPGPLookupTimeout 10 OpenPGP.md
Password-protected messages (6082) ProtectedMessageDays 30 OpenPGP.md
Zero-access mailboxes (6083) ZeroAccessAllowed 0 (with levels) ZeroAccess.md
Message recall (6085) RecallDays 14 (with levels) MessageRecall.md
Help (no step) HelpWikiBaseUrl https://gitlab.com/hmailserver/hmailserver/-/wikis/ - the group's path for a database set up on 6.3.6; a database set up before keeps the project's first path, which GitLab redirects (Help.md, The wiki link) Help.md
Throttling per destination and per tenant (no step) MaxOutboundConnsPerDestination 0 and MaxOutboundConnsPerTenant 0 (Settings.MaxOutboundConnectionsPerDestination and ...PerTenant over COM; a row name is at most thirty characters), OutboundBackoffMinutes 2, OutboundBackoffMaxMinutes 60 OutboundShaping.md
Migrations (6090) MigrationConnectionsPerHost 4 Migration.md
The attachment sandbox (6091) SandboxEnabled 0, SandboxSubmitUrl '', SandboxVerdictUrl '', SandboxToken '' (write-only), SandboxTypes (exe, dll, scr, the script types, msi, jar, lnk, iso, img, the Office types with and without macros, rtf, pdf, zip, 7z, rar), SandboxMaxSizeKB 20480, SandboxMaxFiles 10, SandboxPollSeconds 30, SandboxTimeoutMinutes 15, SandboxTimeoutAction 0, SandboxScoreThreshold 70, SandboxCacheHours 24, SandboxInternalMail 0 Sandbox.md
The list manager (6092) ListBaseUrl '', ListHardBouncesToUnsubscribe 1, ListSoftBouncesToUnsubscribe 5, ListBounceWindowDays 30 MailingLists.md
SAML sign-in (no step) SamlIdpEntityId '', SamlIdpSsoUrl '', SamlIdpCertificate '', SamlSpEntityId '', SamlEmailAttribute (the WS-Federation emailaddress claim), SamlRequireSignedAssertion 1, SamlAutoProvision 0, SamlAdministrators '', SamlButtonLabel Sign in with SAML PasskeysAndSingleSignOn.md
The S/MIME authority (6095) SmimeAuthorityBaseUrl '' SmimeAuthority.md
Storage compression and attachment de-duplication (6098) MessageStoreCompression 0 (off), MessageStoreDedupMinKB 256 (0 to 65536; 0 keeps nothing once) - message_store_compression, message_store_dedup_min_kb over REST, Settings.MessageStoreCompression and MessageStoreDedupMinKB over COM; apply at once MessageStore.md
Object storage for the message store (no step) MessageStoreBackend file (or object), MessageStoreObjectService '' (none; s3 or azure), MessageStoreObjectEndpoint '', MessageStoreObjectRegion us-east-1, MessageStoreObjectBucket '', MessageStoreObjectAddressing auto (path, virtual), MessageStoreObjectPrefix '', MessageStoreObjectAccessKey '', MessageStoreObjectSecret '' and MessageStoreObjectSasToken '' (both write-only, stored protected), MessageStoreObjectCaFile '', MessageStoreObjectCacheMB 32 - the message_store_* keys of /api/v1/settings, no COM property; taken within five seconds. Off until an administrator names a store: a server with file and no service reads and writes as before ObjectStorage.md
Mailbox encryption at rest (6099) MailboxKeyIdleMinutes 15 (1 to 1440) - mailbox_key_idle_minutes over REST; encryption itself is switched on per account, and is off on every account EncryptionAtRest.md
PST import (no step) PstImportDirectory '' - pst_import_directory over REST. Empty as shipped, which takes no PST job; an absolute local path under which a job names <directory>/<domain>/<file>.pst Migration.md
The SNMP agent (no step) SnmpEnabled 0, SnmpPort 161, SnmpBindAddress '', SnmpAllowedAddresses '' (answers nobody), SnmpV3User '', SnmpV3AuthPassword and SnmpV3PrivPassword '' (write-only), SnmpV2cEnabled 0, SnmpV2cCommunity '' (write-only), SnmpContact '', SnmpLocation '' - the snmp_* keys of /api/v1/settings, applied at a restart or a reinitialise, no COM property, and since 6.3.6 the Control Panel's SNMP tab under Monitoring & troubleshooting > Logging, which reads and writes them over the REST API (Ports Reference 4.6) Snmp.md
6.3.6: the auto-ban widened by reverse name (no step) AutoBanWidenByReverseName 0 (server level) - widen an auto-ban, in the background, to the contiguous run of addresses around it inside its IPv4 /24 whose reverse names share its registrable domain, forward-confirmed, at most 32 lookups each way; never across a never-ban entry or an administrator's IP range, never IPv6 or a private network. COM Settings.AutoBanWidenByReverseName, REST auto_ban_widen_by_reverse_name on /api/v1/settings, Control Panel Settings > Auto-ban (work item #55) AutoBan.md
6.3.6: the never-ban list by reverse name (6108) the table hm_neverbannames (id, pattern, note, created), empty as shipped: patterns in the domain list's form - mail.example.net that name, .example.net it and every name beneath - a public suffix, an address, a network or * refused; an address is spared from all three auto-bans when one of its reverse names is covered and resolves back to it. COM Settings.AutoBanNeverBanNames, AddAutoBanNeverBanName, RemoveAutoBanNeverBanName, CheckAutoBanNeverBanName; REST /api/v1/never-ban-names (APIs Reference); Control Panel Settings > Auto-ban (work item #56) AutoBan.md
6.3.6: host names that are not fully qualified (no step) RefuseHeloNotFqdn 0 - 550 5.7.1 at MAIL FROM to a session whose HELO/EHLO name is not a fully qualified domain name (an address, one label, a public suffix); never for an authenticated session, an IP range without spam tests, a white-listed sender or an incoming relay (work item #51). RefuseMailFromDomainNotFqdn 0 - the same for the envelope sender's domain; MAIL FROM:<> is not judged. Both server level; COM AntiSpam.RefuseHeloNotFqdn and RefuseMailFromDomainNotFqdn, REST refuse_helo_not_fqdn and refuse_mail_from_domain_not_fqdn on /api/v1/settings/antispam, Control Panel Anti-spam > Host checks SmtpPolicy.md
6.3.6: the URI lists' domain bound (no step) SurblTooManyDomainsScore 5 (server level) - scored once, however many URI lists are active, by a message whose links name more than 200 registrable domains (only 200 of them are asked about); 5, the default, is the stock mark threshold; 0 scores nothing, and nothing is scored while no URI list is active. COM AntiSpam.SurblTooManyDomainsScore, REST surbl_too_many_domains_score on /api/v1/settings/antispam, the Control Panel's Anti-spam page (General), the Deck. A database gets its row at the first start (item 30 of 6.3.6's "What changes on upgrade") Stopping Spam §10.6
6.3.6: each SMTP listener's AUTH (6108) hm_tcpipports.portsmtpauth: 0 On, 1 Off, 2 Honeypot, as COM TCPIPPort.SMTPAuthentication and REST smtp_authentication on /api/v1/ports; only On on a POP3 or IMAP port (Ports Reference) SmtpAuthOnDeliveryPorts.md

The documents are in hmailserver/docs. Seven more rows are the server's own records, written by it and set by nobody, and are not in SettingsDefaults.h: EnvironmentManagedSettings and EnvironmentSecretSettings (what the environment manages, and which of those a file supplied), ProfileSnapshot and ProfileApplied (a profile's way back, and the last choice), StoredSecretsKeyId and StoredSecretsKeyExported (the stored-secrets key's id, never the key, and whether it has been exported - SecretsAtRest.md) and NewInstallDefaultsPending (the mark a database the Linux create step made carries until the unit's first start gives it the secure defaults - SecureDefaults.md). The send API's per-key sender and ceilings are not settings: they are fields of the key in hMailServerApiKeys.ini (section 12).

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