Skip to content

Settings Reference

chrisholloway5 edited this page Sep 9, 2026 · 11 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.

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

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

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: persist now, apply on the next start (source: IFS.h:59-65). Nothing in SetIniSetting calls LoadSettings.
  • 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

  • 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). Current REQUIRED_DB_VERSION is 6031 (Common/Application/Constants.h:173). 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).

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

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. The bundled MariaDB Connector/C requires TLS from the server and refuses one that has none ("SSL is required, but the server does not support it"); 1 sets MYSQL_OPT_SSL_ENFORCE off so it prefers TLS and falls back to plaintext. 0 keeps TLS required, and the connect error names this key. Certificate verification is the client's default either way (IFS.cpp:162; MySQLConnection.cpp Connect). 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!

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) 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
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); written by the installer as MD5 only when a password was supplied (section_ini.iss:17; hMailServerInnoExtension.iss) COMAuthentication.cpp, RestApiServer.cpp, ScramSha256.cpp S

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

237 keys are 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, all 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]               238 keys, mirrored into hm_inisettings
│   ├─ [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

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 hashes only; empty = off (IFS.cpp:200-203) 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) Crypt.cpp, IFS.cpp:1105-1117 S
ServiceAccountName string "" 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) ServiceManager.cpp S (applied at /Register)
ServiceAccountPassword string "" Password for the above; empty for virtual/managed accounts (IFS.cpp:247) ServiceManager.cpp S (at /Register)
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

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. The outbound/fetch XOAUTH2 token request does not: OutboundOAuth2TokenClient.cpp opens its own connection and never sees 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!
MtaStsHostingEnabled bool 1 Serve /.well-known/mta-sts.txtinert 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); this server implements neither CalDAV nor CardDAV, the setting names the server that does. Empty = 404 silently; a non-empty unusable value = 404 and HM5780 once per service start (IFS.cpp:493; WebServicesServer.cpp:1062-1140). 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) 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 only, no port syntax (inet_addr); invalid → HM4401 and system DNS; 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) 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) 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) 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://api.github.com/repos/Progressiverobot/hmailserver/releases/latest on the stable channel or .../releases?per_page=10 on the pre-release channel (UpdateChecker.cpp:26-27, 85-92); 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 = https://github.com/Progressiverobot/hmailserver/.github/workflows/sign-release.yml@ (SigstoreVerifier.cpp:76, 453-455; IFS.cpp:501) SigstoreVerifier.cpp S
UpdateSigningIssuer string "" OIDC issuer the certificate must name; empty = https://token.actions.githubusercontent.com (SigstoreVerifier.cpp:77, 457-459; IFS.cpp:502) SigstoreVerifier.cpp S
UpdateSourceRepository string "" Repository the certificate must name; empty = https://github.com/Progressiverobot/hmailserver; the single character - switches the repository check off (SigstoreVerifier.cpp:78, 461-467; IFS.cpp:503) SigstoreVerifier.cpp S

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

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), 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): CI fails 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 reports 234 keys where LoadSettings reads 237.

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 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 [Settings] stops being included in backups until it is fixed ISS.cpp Synchronize, before pass 1
HM5803 Medium 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 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 SetIniSetting wrote the file but could not read the mirror afterwards The file holds the new value; the row may not ISS.cpp WriteSetting
  • 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". Today IniFileSettings reads 238 [Settings] keys (237 in LoadSettings + UseLanguage); with the [SendingLimits]/[LDAP] sections there are 238 + 4 + 20 keys outside [Database]/[Directories]/[Security]/[GUILanguages]. 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; 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).

Clone this wiki locally