-
Notifications
You must be signed in to change notification settings - Fork 3
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.Reinitializedoes. - 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"]
-
[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
SetIniSettingre-runsLoadSettings, which is the whole of the "persist now, apply on the next start" contract (source: IFS.h:59-65).
- File name is literally
hMailServer.ini, in the server's Bin directory (source: IFS.cpp:863-878GetInitializationFile). - The Bin directory is
HKLM\SOFTWARE\hMailServer\InstallLocation+\Binif 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.iniinto{app}\Bin(source:installation/hMailServerInnoExtension.iss:1043-1055). - COM
Application.InitializationFileexposes the path:hMailServer/hMailServer.idl:1702;InterfaceApplication.cpp:455returnsGetInitializationFile(). - 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"]
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.
- 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, butKey=(present, empty) reads as 0 throughGetPrivateProfileInt(source: IFS.h:81-85; ISS.cpp:463-467); a value such as5xreads as 5 andxas 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] LogFolderis read with a 255-char buffer (IFS.cpp:885-886) and[Settings] UseLanguagewith 255 (IFS.cpp:937-939). - Booleans are
ReadIniSettingInteger_(...) == 1, so only the literal1is true;2,yes,trueare false (source: every== 1in 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).
-
LoadSettings()reads every key in sections 5-9 below into typed members in one pass (source: IFS.cpp:90-679). It runs:- First thing in
Application::InitInstance, file-only (source:Common/Application/Application.cpp:132). - 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).
- On
Application::Reinitialize(COMApplication.Reinitialize,InterfaceApplication.cpp:93-100), which callsForgetDatabaseSettings()then InitInstance again (source: Application.cpp:924-942). - On every COM
Settings.Directoriesget (source:COM/InterfaceSettings.cpp:1129) and onDatabase.CreateInternalDatabase(source:COM/InterfaceDatabase.cpp:488) — these re-latch all ~200 members from the file (+overlay) on a COM thread. - On
hMailServer.exe /Register(no database) soServiceAccountName/Passwordreach the SCM (source:hMailServer/hMailServer.cpp:216-249).
- First thing in
- 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
SetIniSettingcalls LoadSettings. - Keys that are NOT latched but read live:
UseLanguage(every call, IFS.cpp:1110-1117),LogFolder(lazily once, then cached untilSetLogDirectory, IFS.cpp:1056-1066), the whole[SendingLimits]/[SendingLimitsOverrides]and[LDAP]sections (file-timestamp check at most every 2 s; section 10, 11), andhMailServerApiKeys.ini(every request; section 12).CalDavRedirectUrl/CardDavRedirectUrlare 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). -
LoadSettingsends by callingDiskSpace::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_Minutes0 vs 6,add_xauth_user_ip_false vs 1 (source: IFS.cpp:23-82 vs 144-398).WindowsEventLogEnabled/Levelare 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
- 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.Reinitializeis 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()).
-
LoadDatabaseSettings()reconciles[Settings]with tablehm_inisettings(columnsinisettingname≤100 chars,inisettingvalue≤4000,inisettingfilevalue) viaIniSettingStore::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 theCreateDefaultSpecialUseFolderssetting; 6027 addshm_domains.domainmessageretentiondaysandhm_accounts.accountmessageretentiondays; 6028 createshm_metricsamples; 6029 createshm_archiveindex; 6030 adds foreign keys withON DELETE CASCADEbetween accounts/aliases and their domains; 6031 addshm_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 passIsStorableName(non-empty, ≤100, no= [ ]or line break, no surrounding whitespace); value must passIsStorableValue(≤4000, no line break) (ISS.cpp:392-426).SetIniSettingwrites the FILE first and fails without changing anything if that fails (ISS.cpp:428-460, HM5806 if the mirror cannot be read afterwards).DeleteIniSettingremoves the line (notKey=) 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
GetIniSettingover 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."]
- 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=1survived 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 —
AcmeEnabledtoacmeenabled— 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 meanUPDATE/DELETEclauses that behave differently on PostgreSQL from the other three backends (source: ISS.cpp comment in pass 1).
| 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).
| 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).
| 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).
| 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 |
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.
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."]
- 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.cppForwardreturns "" when the secret is empty andSMTP/SMTPForwarding.cpponly assigns a non-empty result; IFS.cpp:483-489; RateLimiter.h:15 and LdapSettings.cpp:229.
| 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 |
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 |
| 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 |
| 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! |
| 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.txt — inert unless a WebServices port is set; ReportUnreachableFeatures logs this (IFS.cpp:607; Application.cpp:542-557) |
WebServicesServer.cpp | S! |
MtaStsPolicyMode |
string | enforce |
Must be enforce, testing or none; anything else → enforce (IFS.cpp:608; WebServicesServer.cpp:1128-1131) |
WebServicesServer.cpp | S |
MtaStsPolicyMaxAge |
int seconds | 604800 | (IFS.cpp:609) | WebServicesServer.cpp | S |
MtaStsPolicyMx |
comma list | "" | MX hosts in the policy; empty → derived from MX lookup (mx_cache) (IFS.cpp:610; WebServicesServer.cpp:1030-1040, 95-97) | WebServicesServer.cpp | S |
AutoconfigEnabled |
bool | 1 | Thunderbird/Outlook autoconfig endpoints — also inert without a WebServices port (IFS.cpp:611) | WebServicesServer.cpp, RestApiServer.cpp | S |
AutoconfigClientHost |
host | "" | Host advertised to clients; empty → COM Settings.HostName (IFS.cpp:612; WebServicesServer.cpp:1352-1358) |
WebServicesServer.cpp, RestApiServer.cpp | S |
CalDavRedirectUrl |
http(s) URL, absolute, no spaces/control chars | "" | 3xx redirect target for /.well-known/caldav (RFC 6764); 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 |
| 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! |
| 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 |
| 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 |
"" | 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 |
"" | 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 |
| 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 |
| 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! |
| 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) |
| 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 |
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 |
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 |
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 |
- Read by
RateLimiter::LoadSettings_directly withGetPrivateProfileString/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
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_).
- 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 PanelViews/LdapSettingsView.cs:61,715,735reads/writes[LDAP]directly with a 4096 buffer.
- Path: same directory as hMailServer.ini, name
hMailServerApiKeys.ini(RestApiServer.cpp:2017-2055; Control PanelServices/ApiKeyStore.cs:81,137). - One section per key,
[Key.<16 lower-hex id>]withLabel,Hash(SHA-256 hex of the token, unsalted, by design),Expires(YYYY-MM-DD HH:MM:SSlocal, required; missing/unreadable = expired),AllowedFrom(IP,lower-upper, or CIDR; empty = any),Scope(fullorreadonly; 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).
- Server COM layer:
InterfaceSettings,InterfaceDatabase,InterfaceDirectories,InterfaceLogging,InterfaceScripting,InterfaceBackupSettingsviaIniFileSettings(files underCOM/). - 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.isswrites [Directories], [GUILanguages], [Security];hMailServerInnoExtension.issrelocates 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-42reads[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 (onlySimulateDatabaseFailureForandSimulateSpoolWriteFailureare exempt) or when a new ini section is read outside IniFileSettings (only[LDAP],[SendingLimits],[SendingLimitsOverrides]are allowed). It matches literalReadIniSetting..._("Settings", "Key"calls, so it does not see the threePasswordHash*keys read throughReadPasswordHashWorkFactor_and reports 234 keys where LoadSettings reads 237.
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.
-
Exact COM property name that returns the ini pathConfirmed: the property isApplication.InitializationFile(hMailServer/hMailServer.idl:1702;COM/InterfaceApplication.cpp:455). -
Whether the REST API refuses to start whenConfirmed:[Security] AdministratorPasswordis emptyRestApiServer::Startreturns 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 whenConfirmed: 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).OutboundOAuth2Hostsmatches butOutboundOAuth2TokenUrlandFixedTokenare both empty -
WhetherConfirmed: the outbound SMTP/POP3 client context is built once per IOService at start (IOService.cpp:65-68), soSslContextInitializer::InitClient(outbound) rebuilds its context per connection or onceTlsKeyExchangeGroups/TlsCipherSuites13are 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.ps1per-key ini behaviour. -
Confirmed:AcmeDomains/AcmeContactEmailparsing and requiredness inside AcmeClient.cppAcmeDomainsis comma-split, trimmed, lower-cased, empties dropped; an empty list logs "ACME: No domains configured" and issuance does not run (AcmeClient.cpp:1364-1380).AcmeContactEmailis optional — omitted from the ACME account when empty (AcmeClient.cpp:956-961). -
The exact archive sub-path layout underConfirmed: local sender →ArchiveDirArchiveDir\<senderdomain>\<sendername>\Sent-<file>; non-local sender →ArchiveDir\Inbound\<file>; then a copy or hard link per local recipient atArchiveDir\<recipientdomain>\<recipientname>\<file>, made from the first copy when there is one; names lower-cased (SMTPConnection.cpp:1876-1940).ArchiveDomainscan suppress the Sent copy for an unlisted local sender (1880-1886).
-
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. -
settings.mdlistsSettings.ServiceAccountName/Passwordunder "Intentionally excluded" from the GUI;FeatureSettingsView.xaml.csnow containsServiceAccountName,ServiceAccountPassword,ServiceAccountGrants,ServiceAccountStatus. The exclusion note is stale. -
settings.mdnames OpenTelemetry asOtelEndpoint,OtelServiceNameonly; code also hasOtelMetricsEndpoint,OtelLogsEndpoint,OtelMetricsInterval(and the GUI exposes them). - Name traps (key ≠ getter/member name — a doc writer copying from headers gets the wrong key):
DaneEnforcementEnabled(getterGetDaneEnabled),IndexerFullText(GetIndexerFullTextEnabled),OAuth2PublicKeyFile(GetOAuth2RsaPublicKeyFile),ArchiveHardLinks(GetArchiveHardlinks),AddXOriginalRcptTo(GetAddXOriginalRcptToHeader),GreylistingRecordExpirationInterval(GetGreylistingExpirationInterval),Passwordencryptionread vsPasswordEncryptionwritten,Pop3LoginDelaySeconds(lower-case p3 unlikePOP3DMinTimeout). - "Inert by default" shapes to flag on feature pages:
MtaStsHostingEnabled=1andAutoconfigEnabled=1do nothing untilWebServicesHttpPort/HttpsPort> 0 (Application.cpp:542-557 logs this);IndexerFullText=1does nothing unless COMSettings.MessageIndexingis on;WindowsEventLogEnabled=1writes nothing on a healthy server at level 2;OutboundOAuth2Hosts/FetchOAuth2Hostshave non-empty defaults but need the token settings;[SendingLimits]and[LDAP]are entirely inert untilEnabled/limits are set;MetricsServerPort,RestApiPort,ManageSieveServerPort,WebServices*Portall default to 0 = listener not created. - 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. -
LogFolderandUseLanguageuse 255-char buffers (IFS.cpp:885, 937) while everything else uses 4096. - 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.
-
DNSServeraccepts a single IPv4 address only; docs must not suggesthost:portor IPv6 (DNSResolverWinApi.cpp:255-291 and memory note "DNS custom-server regression"). - No ini key named in
hmailserver/docs/*.mdor the READMEs is absent from the code (token diff against the 243-key list found onlyWebServicesServer, a class name).
hMailServer 6.3.2 · AGPL-3.0-or-later · Repository · Report a documentation error
Hmail Server — full index
Start here
1. Install and run
- Before You Install
- Installing hMailServer
- Installing on Linux
- Running in a Container
- The Control Panel
- Your First Domain and Mailbox
- Connecting a Mail Client
- DNS for Your Domain
2. Secure it
3. Operate it
- Monitoring and Health
- Backup and Restore
- Troubleshooting
- Diagnosing Stalled Mail
- Relocating an Installation
- Upgrading hMailServer
- Upgrading Guide
- Migrating the Database Backend
- High Availability Runbook
- Warm Standby
- Runbooks Digest
4. Extend it
- Rules and Sieve
- Aliases Lists and Public Folders
- Routes and Relays
- The COM API and Scripting
- The REST API
- APIs Reference
5. Contribute to it
- Project Handbook
- Architecture
- Contributing
- Release Process
- Governance
- Assurance Case
- Regression Test Environment
- Fuzzing
- Regulatory Scope
- Third-Party Binaries
Look it up — from any journey