Repository navigation
Settings Reference
Every key of hMailServer.INI as the server reads it today: section, default, what it does, which code consumes it and whether it is read at startup or live. Generated from IniFileSettings.cpp and the other readers of the file, with the source line for each fact. File and line references point into the repository at the commit this page was written from (6.2.24, master of 4 September 2026). Sections headed Unconfirmed or Contradictions record what could not be verified or where documents disagreed, and are left in on purpose. 6.3.7, the latest release (17 October 2026, schema 6108), is not yet reflected here: the keys were last re-checked at v6.3.6 (below).
Every fact below was checked against the code on disk today. Paths are relative to
hmailserver/source/Server/ unless stated. IFS.cpp = Common/Application/IniFileSettings.cpp,
IFS.h = Common/Application/IniFileSettings.h, ISS.cpp = Common/Application/IniSettingStore.cpp.
Line numbers are from the working tree at commit 4d5da3f; passages corrected on 2026-09-08 cite the
tree at 40ae9491d, from which 6.2.28 was cut later the same day. Anything marked "new in 6.2.28"
ships for the first time in that release. HttpProxy (section 9.16) landed after 40ae9491d and is
cited from the release commit f088336c2, where it is IFS.cpp:492 and pushes every later [Settings]
read in that file down one line.
settings.md at the repo root (dated 2026-06-15, server 6.2.5) was used only as a hint; where it
disagrees with the code the code is recorded and the disagreement is listed under "Contradictions".
Re-checked on 15 September 2026 at commit 3233b0cc4, after 6.3.3: the set of
[Settings] keys IniFileSettings.cpp reads is the same as in v6.3.3. No [Settings] key has
been added since 6.3.3 - and, by a rule adopted on the same day, none will be. The project's
CONTRIBUTING.md
says that a new setting goes in the database, in hm_settings through the COM Settings
object, with a Control Panel editor, and that no new key is added to the [Settings] section of
hMailServer.ini; build/check-ini-coverage.py fails on one (in GitHub's CI until 18 September 2026, by hand since, and now in the checkers job of the project's .gitlab-ci.yml). In 6.3.3
every key in section 9 is read from the file, with the database mirror of section 4. From 6.3.4 - and in 6.3.7, the current
release - schema 6042 makes the database the store for those keys: they are
read from and written to hm_inisettings, the [Settings] section of the file is a copy the server
rewrites where it may write the file, and an edit to a key the database holds is not used at the next
start and is reported as HM5804, its line put back where the service may write the file - not under
NT SERVICE\hMailServer, to which the file is read-only (section 4a; The Service Account). The key names, defaults, validation and consumers below are the same either way; what
changes is where a value is changed. PasswordPepper stays in the file only, and [Database], [Directories],
[Security], [GUILanguages], [LDAP], [SendingLimits] and [SendingLimitsOverrides] stay in the
file. See Settings Overview for the two stores side by side.
Re-checked on 24 September 2026 at master 540347500: the
set of [Settings] keys is the same again - 241 read by the loader, and UseLanguage read per call.
What was added around them since the store is in two new sections: 4b, when a save applies each
key, the deepest level it may be set at and its name in the environment; and 17, the rows of
hm_settings added since 6.3.3, which is where every new setting now goes. One key was
added to the file, and it is a bootstrap key: [Database] UpgradeOnStart (section 5).
Re-checked on 26 September 2026 at 92b8f9f63 (schema 6103, Constants.h:1205), and at the tag
v6.3.4 (51e2dbda0), at schema 6104, and on 5 October 2026 at v6.3.6 (e3c448aa6, 6 October 2026), at schema 6108: the [Settings] keys are the same 241 again, with UseLanguage
read per call, and IniSettingsBaseline.h still lists 241 - 211 applied at once, 28 at a
reinitialise, 2 at a restart. Three keys were added to the file outside [Settings], each read before
the database can be: [Database] TlsMode and TlsCaFile (section 5, database TLS) and [Security] StoredSecretsKey (section 8, secrets at rest). Twenty-seven rows were added to hm_settings
(section 17, 191 in all at 92b8f9f63): storage compression, object storage, mailbox encryption at rest, PST
import and the SNMP agent; the tag adds two more, PasskeyAllowedOrigins and
WebPushLoopbackHttpForTests (193 in all). ServiceAccountName and ServiceAccountPassword are still read, and
no longer used by /Register (section 9.1).
Timing legend used in the tables:
-
S — latched into a member by
IniFileSettings::LoadSettings(); the consumer calls the getter at the moment of use, so the value changes whenever LoadSettings re-runs (see "When the file is read"). -
S! — consumed only when a listener/task is constructed at server (re)start, so a LoadSettings
re-run on its own does not apply it; a service restart or
Application.Reinitializedoes. - L — read live from the file by its consumer, not through LoadSettings (own cache rules noted).
The legend describes 6.3.3, where a change reaches the running server only when LoadSettings runs again.
Since 6.3.4 a save publishes, and each key's applies mark - now, reinitialize or
restart, section 4b - says when a saved value is in effect; the S and S! marks below still say how the
code reads each key, and section 4b names every key that does not apply on save.
The legend drawn, so that "S", "S!" and "L" have a picture behind them:
flowchart LR
subgraph SOURCES["Sources of truth"]
FILE["hMailServer.ini in the Bin directory"]
MIRROR[("hm_inisettings, mirroring the Settings section only")]
APIKEYS["hMailServerApiKeys.ini"]
LIMITS["SendingLimits and SendingLimitsOverrides sections"]
LDAPS["LDAP section"]
end
FILE --> LOAD["IniFileSettings::LoadSettings"]
MIRROR --> LOAD
LOAD --> MEMBERS["About 200 typed members on the IniFileSettings singleton"]
MEMBERS --> S["S - the consumer calls the getter at the moment of use, so a later LoadSettings changes the answer"]
MEMBERS --> SB["S! - consumed once, when a listener or task is constructed at server start"]
APIKEYS --> L1["L - RestApiServer::LoadKeys_ reads the file bytes on every request"]
LIMITS --> L2["L - RateLimiter re-reads on a file-timestamp change, checked at most every 2 s"]
LDAPS --> L3["L - LdapSettings does the same"]
FILE --> L4["L - UseLanguage on every get, LogFolder once then cached"]
-
[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). That is 6.3.3; since 6.3.4 every write rebuilds the settings from the store and publishes them (section 4b).
- 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 in 6.3.3: persist now, apply on the next start (source: IFS.h:59-65). Nothing in
SetIniSettingcalls LoadSettings. Since 6.3.4 the contract is the applies mark instead: a write stores the value and publishes a new block of values, and a key markednowis in effect when the write returns (section 4b). - Keys that are NOT latched but read live:
UseLanguage(every call, IFS.cpp:1110-1117),LogFolder(lazily once, then cached 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()).
Everything in this section is how 6.3.3 and earlier releases behave (schema 6011 to 6040), with line references from the 6.2.28 tree. 6.3.4 replaces the merge with the store of section 4a.
-
LoadDatabaseSettings()reconciles[Settings]with 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). REQUIRED_DB_VERSION was 6031 when this bullet was written (Common/Application/Constants.h:173); it is 6040 in 6.3.3 and 6104 in 6.3.4 (Constants.h:1224at the tag, 6103 at92b8f9f63), and Changes-Since-6210 section 2 has the steps between. 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). In 6.3.3 it also writes the file directly when it is on the same machine, and the merge picks the change up at the next start. Since 6.3.4 it reads over COM and falls back to the file, and writes over COM only; without a session it refuses with "Settings are stored in the server's database, so changing one needs a connection to the server" (IniFeatureStore.cs).
Table shape (schema 6011, DBScripts/Upgrade6010to6011*.sql):
hm_inisettings
├─ inisettingid int identity(1,1), primary key
├─ inisettingname nvarchar(100), UNIQUE ← a name over 100 chars is skipped with HM5800
├─ inisettingvalue ntext ← the value in force; over 4000 chars, HM5801
└─ inisettingfilevalue ntext ← what the file said the last time the two agreed
inisettingfilevalue is the whole trick: it is what lets the reconciliation tell "the file
changed" from "the row changed", and "the operator deleted a key" from "a restore added one".
flowchart TD
START["Synchronize, at every LoadSettings that has a database"] --> READ{"Could hm_inisettings be read?"}
READ -- no --> E802["HM5802 Medium. Run on the file alone. Settings stop being included in backups."]
READ -- yes --> P1["Pass 1: every key present in the file"]
P1 --> Q1{"Is there a row for this key?"}
Q1 -- no --> INS["Insert a row with value = filevalue = the file's value"]
Q1 -- yes --> Q2{"file value differs from inisettingfilevalue?"}
Q2 -- yes --> Q3{"inisettingvalue also differs from inisettingfilevalue?"}
Q3 -- yes --> E804["HM5804 conflict. The file wins. Row updated to the file's value."]
Q3 -- no --> W1["The file wins. Row updated."]
Q2 -- no --> Q4{"inisettingvalue differs from inisettingfilevalue?"}
Q4 -- yes --> W2{"Write that value into the file"}
W2 -- succeeded --> OKR["Row now agrees. The database-side change is applied."]
W2 -- failed --> E803["HM5803 Medium. The change is NOT applied. The file value is used for this run."]
Q4 -- no --> SAME["They agree. Use the stored value."]
P2["Pass 2: every row whose key is gone from the file"] --> Q5{"Does inisettingvalue still equal inisettingfilevalue?"}
Q5 -- yes --> DEL["The deletion is the newer fact. Delete the row."]
Q5 -- no --> PUSH["A database-side edit or a restore. Write it back into the file."]
- 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).
Nothing in this section is in 6.3.3. Sources are the tree at 3233b0cc4, and for what a file the service may not write changes, which the tag v6.3.4 already has, the tree at 5e49abf06: IniSettingStore.cpp/.h
(ISS), IniFileSettings.cpp (IFS) LoadDatabaseSettings, WriteSettingsValue and
RemoveSettingsValue, and Application.cpp InitInstance/Reinitialize.
-
The step.
Upgrade6041to6042*.sqlisupdate hm_dbversion set value = 6042and nothing else, in all four dialects; the table has had the right columns since 6011.Application::OnDatabaseConnectedrefuses a version mismatch in both directions, so a 6.3.3 server (6040) is refused against a 6042+ database with HM5011, and the file-wins merge of section 4 can never run against a database that is now the store. -
When it runs. Unchanged: the first
LoadSettingsis file-only, thenLoadDatabaseSettings()runsIniSettingStore::Synchronizeonce the schema is accepted, thenLoadSettingsagain with the resolved values as the overlay (Application.cpp,InitInstance).ReinitializecallsForgetDatabaseSettings()and runsInitInstanceagain, so a reinitialise - COMApplication.Reinitialize, a restore,SIGHUPon Linux - reconciles exactly as a start does. The--create-databaseand--upgrade-databaseoptions on Linux, and DBUpdater on Windows, do not. -
The rules, per
[Settings]key in the file (ISS.cppSynchronize, pass 1):- no row: adopted - inserted with value = filevalue = the file's value. This is the migration, done on the first start at 6042 for every key the section holds; there is no flag and nothing to run by hand.
- row exists, and the file's value differs from both
inisettingfilevalueand the stored value: an edit. The stored value is used, the key is named in HM5804, and the line is put back to the stored value where the service may write the file; where it may not -NT SERVICE\hMailServer, to which the file is read-only, or the Linux packages'0640 root:hmailserverfile - the line is left as edited andinisettingfilevaluetakes what it holds, so that HM5804 names it once. - row exists, and the file already holds the stored value: used; only the bookkeeping is brought up to date.
- row exists, the file still holds the value last written but the stored value differs (a restore, a change from another node): the stored value is written into the file where the service may write it, and otherwise the line is left as it is and counted - a reinitialise names it in the application log, where application logging is on, among the lines older than the stored value.
- Rows whose key is missing from the file (pass 2): the line is written back where the service may write the file, and the row kept either way. Deleting a line no longer returns a setting to its default. The rule of section 4, pass 2 - "the deletion is the newer fact" - is exactly what was inverted.
-
[SettingsOverride](IFS.cppLoadDatabaseSettings): every key in that section of the file is applied over the resolved values, read byReadOverridesfrom the file with no database call, so it applies even when the table cannot be read. Named at every start as HM5807. Not written by the server, not offered by any editor, not carried by a backup. An override is not copied into[Settings], so the stored value stays visible beside it. -
Writes (
WriteSetting, reached from COMSettings.SetIniSetting,PUT /api/v1/settings/ini/{name},hmctl ini set, the PowerShell module'sSet-HmIniSettingand the Control Panel): the value is trimmed, the row first, then the line, where the service may write the file; where it may not, the row alone,inisettingfilevaluetaking what the line holds, and the line is left as it is. A failed row write fails the call and changes nothing ("The setting could not be stored. Settings are held in the database, so this means the server could not write to it."). A failed file write does not fail it: the value is stored and used, HM5806 is reported, andinisettingfilevalueis put back to what the file really holds so the next start rewrites the line quietly instead of reporting an edit. A key shadowed by[SettingsOverride]is stored anyway and reported as HM5808. -
Removal (
RemoveSetting, reached from COMSettings.DeleteIniSetting,DELETE /api/v1/settings/ini/{name}andhmctl ini delete; the Control Panel never calls it, and the server's own log line that says "delete the setting in the Control Panel" overstates it): the line first (removed, not left asKey=), then the row, where the service may write the file; where it may not and the file has the line, the line is left and the row kept holding the key's default, which the server treats the same, because a dropped row would be put back from the line at the next start, and that is said in the application log, while the removal ofPasswordPepperand of a key this build does not read is refused, HM5806 (ISS.cppRemoveSetting; The Service Account). A leftover row is repaired by the next start; a leftover line would be adopted as a new setting, which is why the order is the opposite of a write. -
PasswordPepperis file-only (IsFileOnlyName): never adopted, never served from the store, written only to the file when set over COM or REST - refused with a sentence where the service may not write the file,NT SERVICE\hMailServer's case, where an administrator sets it with an elevated editor and restarts the service - and left out ofXMLStore, so no backup carries it. A row an earlier start adopted is deleted once the file holds the value the server has been running on. Restoring an archive that carries a pepper - one taken by 6.3.3 or earlier - writes it into the file only when the file has none, and where the service may not write the file says so as HM5805, for an administrator to put it there with an elevated editor (ISS.cppXMLLoad). -
The whole
[Settings]section stays mirrored into the file where the service may write it, becausehMailServer.exe /RegisterreadsServiceAccountNameandServiceAccountPasswordwith no database open (section 3, item 5), and because a server whose table cannot be read runs on that copy (HM5802) rather than not starting. UnderNT SERVICE\hMailServer, to which the file is read-only, the copy is not refreshed, and is stale for whatever reads the file itself (The Service Account). -
[SettingsStore]. At every start the server writes two keys into a section of that name when they differ and it may write the file:Store=database, andReadMe=a sentence saying the[Settings]section is a cache, that an edit is undone and reported, where settings are changed, and what[SettingsOverride]is for (ISS.cppWriteFileNotice_). Keys rather than comment lines, because the profile API - the only file writer the POSIX build has - writes keys. -
Backups.
XMLStorewrites every row exceptPasswordPepper;XMLLoadwrites each archived setting into the row and the file, recording them as agreed only when the file write succeeded (HM5805 otherwise). -
CI.
build/check-ini-coverage.pycompares theReadIniSetting*_calls inIniFileSettings.cppwithbuild/ini-settings-baseline.txt- 263 keys in a 289-line file: 238[Settings], 17[Database], 5[Directories], 2[Security], 1[GUILanguages]- and fails a new[Settings]key with the rule quoted; a new key in one of the four bootstrap sections passes once it is added to the baseline in the same commit. Like the check of section 13, it matched literal calls, so the threePasswordHash*keys andUseLanguagewere not among its 238. At540347500the baseline holds 267 keys: 241[Settings]- the 238 and the three work factors, which it now sees - 18[Database](UpgradeOnStartthe one added), 5[Directories], 2[Security]and 1[GUILanguages], each line carrying the key's kind, its shipped default, its applies mark and its levels mark (section 4b). The checkers ran in the project's GitHub workflows until GitHub suspended the project's account on 18 September 2026; since 26 September 2026 GitLab CI'scheckersjob runs everybuild/check-*.py, this one among them (Contributing, The checkers). -
Coverage.
RegressionTests/Infrastructure/IniSettingsMirror.cs, seven tests: a stored value survives a restart; an edit to the file loses, is named once and put back; an override wins and is named at every start; a value changed behind the server is written into the file; a file-only value is adopted; the pepper stays out of the database; a value with surrounding spaces is stored as the file reads it. The full Windows suite on3233b0cc4: 2,410 tests, 2,402 passed, none failed, 8 skipped (7 explicit stress tests and 1 ignored); Linux on00bec4412, the same server: 1,606 tests, 1,326 passed, 0 failed, 280 skipped. The fixture has run in every gate since; the latest, batch 33's of 24 September 2026, ran 3,390 tests in two halves with none failing, and a fixture of its own,SettingsApplyOnSave, proves the applies marks of section 4b.
Changing a [Settings] key since 6.3.4, by surface:
| Surface | Set | Return to default | Notes |
|---|---|---|---|
| Control Panel (Windows) | the key's editor | no - clearing a box stores an empty value, which an integer reads as 0 | Over COM even on the server itself; refuses without a session |
| REST |
PUT /api/v1/settings/ini/{name} with {"value":"..."}
|
DELETE /api/v1/settings/ini/{name} |
Administrator password only; an API key of any scope is refused |
hmctl |
hmctl ini set <name> <value> |
hmctl ini delete <name> |
Over REST, so the listener must already be on (Command Line) |
| PowerShell module | Set-HmIniSetting |
- | Over REST, the same vocabulary as hmctl
|
| COM | Settings.SetIniSetting(name, value) |
Settings.DeleteIniSetting(name) |
Windows only |
| Control Deck | only SmtpTarpitCount, SmtpTarpitDelaySeconds, UseLanguage and RewriteEnvelopeFromWhenForwarding, through its Settings view's groups |
- | The Deck does not call /api/v1/settings/ini
|
hMailServer.ini [Settings]
|
no for a key the database holds - not used at the next start, HM5804, its line put back where the service may write the file; a key it has never held is adopted once | no - the stored value is used, and the line written back where the service may write the file | Except PasswordPepper, which lives in the file |
hMailServer.ini [SettingsOverride]
|
yes, over everything, HM5807 at every start | remove the line | For a database that is unreachable or holds a value the server will not start on |
At 3233b0cc4 every one of these applied at the next start or reinitialise, as in section 3. From 23 September 2026 (batch 26) each of them publishes the value, and when it is in effect is the key's applies mark (section 4b). Where the server's environment names a key (HM_SETTING_<key>), every one of these refuses to change it - REST with 409 - until a start without the variable.
Sources: build/ini-settings-baseline.txt at 540347500, whose six fields are section, key, kind, default, applies and levels; hmailserver/docs/SettingsApply.md, SettingLevels.md and EnvironmentConfiguration.md.
When a save applies it. The [Settings] values are one immutable block, IniFileSettings::Values, published with one atomic store; a write stores the row, rebuilds the block from the rows alone (no reconcile, no adoption, no report) with every now key taking its stored value and every other key keeping the one it runs on, publishes it, and then calls the subscribers of the keys whose value moved. Every String getter returns a copy, because a reference into a block a save may free would be a use-after-free.
| Applies | Count | Keys |
|---|---|---|
now |
211 | Every [Settings] key not named below. The greylist cleaner's period (GreylistingRecordExpirationInterval), the backup schedule (ScheduledBackupTime, ScheduledBackupIntervalMinutes), the five Otel* keys, CalDavRedirectUrl and CardDavRedirectUrl, SimulateDatabaseFailureFor, MaxSubmissionsPerIPPerMinute and MaxOutboundPerDestinationPerMinute are re-latched by a subscriber; the rest are read at the moment of use |
reinitialize |
28 |
AcmeCertificateDirectory, AcmeEnabled, DatabaseStatementTimeout, ManageSieveServerBindAddress, ManageSieveServerPort, MaxNumberOfExternalFetchThreads, MetricsServerAuthPassword, MetricsServerAuthToken, MetricsServerAuthUsername, MetricsServerBindAddress, MetricsServerCertificateFile, MetricsServerPort, MetricsServerPrivateKeyFile, RestApiBindAddress, RestApiCertificateFile, RestApiPort, RestApiPrivateKeyFile, TlsCipherSuites13, TlsKeyExchangeGroups, TlsSessionCacheSize, TlsSessionTicketsEnabled, TlsSessionTimeoutSeconds, TlsTicketKeyRotationSeconds, WebServicesBindAddress, WebServicesCertificateFile, WebServicesHttpPort, WebServicesHttpsPort, WebServicesPrivateKeyFile
|
restart |
2 |
ServiceAccountName, ServiceAccountPassword - and every one of the 29 bootstrap keys, which are never in the store and so never pending |
A reinitialize or restart key whose stored value is not the one the server runs on is pending: GET /api/v1/server/settings/pending (the administrator password only), COM Settings.PendingIniSettings, pending on every /api/v1/settings/ini answer, the Control Panel's bar at the foot of the window and the Deck's note above its Settings view. Settings.IniSettingApplies(name) and Settings.IniSettingAppliesAll answer the marks over COM, and applies answers them over REST; a name the loader does not read - UseLanguage among them - has no mark and is refused by IniSettingApplies. Apply now in the Control Panel and the Deck, POST /api/v1/server/reinitialize and Application.Reinitialize take every reinitialize key. A value stored by another process - another node, --set-setting, --apply-environment while the service runs - reaches this server at its next save, reinitialise or restart. [SettingsOverride] is re-read by a reinitialise or a start, not by a save.
Its level. The sixth field says the deepest level a key may be set at. Every [Settings] key is server except QuotaWarningPercent, which is account: a domain (hm_domains.domainquotawarningpercent, -1 taking the server's) and an account (a row of hm_settingoverrides, schema 6088) may hold their own value, and Application/EffectiveSetting resolves the account's own, else its domain's, else this key at every read. The server's REST answers carry <key>_levels beside such a key. It is one of twenty-seven settings with levels; the other twenty-six are hm_settings properties, fields of the domain or the account, or [SendingLimits] keys, and build/settings-levels.txt marks the hm_settings side as this file marks the [Settings] side (Settings Overview lists them all).
Its name in the environment. Every key in section 9 is also HM_SETTING_<key>, matched without case - HM_SETTING_MaxSubmissionsPerIPPerMinute=30 - and HM_SETTING_<key>_FILE reads the value from a file of at most 64 KB. hmailserver --apply-environment (and hMailServer.exe --apply-environment) checks every variable before it stores any and refuses a name that is no key with the nearest one; the container's entrypoint runs it at every start. A key the environment manages is recorded in the hm_settings row EnvironmentManagedSettings, and every surface refuses an administrator's change to it until a start without the variable. The four HM_REST_* variables the image took before are aliases of the HM_SETTING_ names they always stored. The two [SendingLimits] keys and the [LDAP] section have no environment name, and the _FILE form of QuotaWarningPercent is refused, because a setting with levels is shown to a domain's administrators.
| Key | Type | Default | What it does | Consumer | Timing |
|---|---|---|---|---|---|
Type |
string | "" | One of MSSQL, MYSQL, PostgreSQL, MSSQLCE (case-insensitive); anything else = TypeUnknown and InitInstance returns false ("database settings do not exist") (IFS.cpp:107-121, 681-688; Application.cpp:134-137) |
DatabaseConnectionManager, SQLStatement, SqlLogDevice, PersistentAccount/Domain/FetchAccount, SMTPDeliveryManager | S! (connect) |
Server |
string | "" | DB host. Required for every type except MSSQLCE (CheckSettings, IFS.cpp:908-928, HM5005 at Application.cpp:197-203) |
DatabaseConnectionManager.cpp:96 | S! |
Database |
string | "" | DB name / SQL CE file name. Required for MSSQLCE (IFS.cpp:912-916) | DatabaseConnectionManager.cpp:99 | S! |
Username |
string | "" | DB user (IFS.cpp:101) | DatabaseConnectionManager.cpp:97; MySQLConnection.cpp:366-369 (internal-DB heuristic) | S! |
Password |
string | "" | DB password, decrypted with Passwordencryption scheme at load (IFS.cpp:102, 109-112) |
DatabaseConnectionManager.cpp:98 | S! |
Passwordencryption |
int | 0 |
Crypt::EncryptionType for Password: 0 none, 1 Blowfish, 6 DPAPI (machine-scoped). Crypt::DeCrypt handles only 0/1/6; any other value hits assert(0)/returns "" (Common/Util/Crypt.cpp:153-185; enum Common/Util/Crypt.h:18-27). Written by the server as PasswordEncryption (different case; harmless) = 6 when ProtectStoredSecretsWithDPAPI=1 and DPAPI succeeds, else 1 (IFS.cpp:1093-1118) |
IFS.cpp:109-112 | S! |
Port |
int | 0 | DB TCP port (IFS.cpp:123); 3307 + user root/hmailserver + Internal=1 marks the legacy internal MySQL (MySQLConnection.cpp:364-373) | DatabaseConnectionManager.cpp:102; MySQLConnection | S! |
Internal |
int (0/1) | 0 | Marks the legacy bundled MySQL; drives the post-install Internal MySQL\HMS4.3-MySQL4.1.18.sql script (IFS.cpp:103; MySQLConnection.cpp:364-380) |
MySQLConnection | S! |
ServerFailoverPartner |
string | "" | ADO "Failover Partner" for MSSQL mirroring; when set and Provider empty the provider becomes SQLNCLI (IFS.cpp:104; Common/SQL/ADOConnection.cpp:86-110) |
ADOConnection | S! |
Provider |
string | "" | OLE DB provider name for MSSQL. Empty → MSOLEDBSQL if v18+ installed, else sqloledb; SQLNCLI if a failover partner is set (ADOConnection.cpp:90-110) |
ADOConnection | S! |
NumberOfConnections |
int | 5 | Pool size; forced to 1 for MSSQLCE (IFS.cpp:144, 148-154) | DatabaseConnectionManager.cpp:91 | S! |
ConnectionAttempts |
int | 6 | Connect retries at startup (IFS.cpp:145) | DatabaseConnectionManager.cpp:54-59 | S! |
ConnectionAttemptsDelay |
int seconds | 5 | Delay between attempts (multiplied by 1000 ms) (IFS.cpp:146; DatabaseConnectionManager.cpp:55) | DatabaseConnectionManager | S! |
AllowUnencryptedConnection |
int (0/1) | 0 | MySQL/MariaDB only. Up to 6.3.3 described as making the bundled MariaDB Connector/C prefer TLS instead of requiring it; the test lab measured on 25 September 2026 that the client never asked for TLS at all, so every connection through it was in plaintext whatever this said. Since 6.3.4 it is still read and changes nothing: what it described - TLS preferred, plaintext allowed - is the default, and TlsMode below chooses otherwise (hmailserver/docs/DatabaseTls.md). Added 5 September 2026 |
MySQLConnection | S! |
PostgreSQLSslMode |
string | "" | PostgreSQL only. libpq's sslmode, put into the conninfo: disable, allow, prefer, require, verify-ca or verify-full. Empty leaves libpq's default (prefer: encrypted when the server offers it, verified never). Any other value refuses the connection with HM5563 rather than falling back (Common/SQL/PGConnection.cpp). Added 5 September 2026 |
PGConnection::Connect | S! |
PostgreSQLSslRootCert |
string | "" | PostgreSQL only. libpq's sslrootcert: the CA file verify-ca/verify-full check the server certificate against. A Windows path; the backslashes are escaped for conninfo by the server. Added 5 September 2026 |
PGConnection::Connect | S! |
ConnectionStringOptions |
string | "" | MS SQL Server only. Appended verbatim to the OLE DB connection string (a ; is added if missing) - the provider's own keywords, e.g. Encrypt=yes;TrustServerCertificate=no for MSOLEDBSQL. Added 5 September 2026 |
ADOConnection::Connect | S! |
TlsMode |
string | "" |
6.3.4. TLS between this server and its database, one setting for every backend that crosses a network: empty or prefer - TLS whenever the server offers it, the connection without it when it offers none, the certificate not checked; disable; require - TLS or no connection, the certificate not checked; verify-full - TLS or no connection, the chain checked against TlsCaFile (or the system's store) and the name or address against Server=. Anything else, verify-ca among them, is refused without an attempt (HM7530). With nothing set, MySQL and MariaDB now use TLS when offered (they never did through MariaDB Connector/C), SQL Server through MSOLEDBSQL 18 and SQL Native Client encrypts the whole session, and PostgreSQL is unchanged (PostgreSQLSslMode and PostgreSQLSslRootCert win when set). The application log says at every start what the connection got, and --check-database makes one connection and says the same. A file key, since it says how the store is reached (IniFileSettings.cpp ReadIniSettingString_("Database", "TlsMode", ""); hmailserver/docs/DatabaseTls.md) |
MySQLConnection, ADOConnection, PGConnection | S! |
TlsCaFile |
path | "" |
6.3.4. A PEM file of CA certificates, read by verify-full and by nothing else; a TlsCaFile that verify-full would read and that is not there refuses the connection (HM7531) rather than falling back to the system's store. Keep the path ASCII. SQL Server does not read it and checks against the Windows store (HM7532 says so once) |
MySQLConnection, PGConnection | S! |
UpgradeOnStart |
int (0/1) | 0 |
6.3.4 (batch 25, 23 September 2026). 1 makes Application::OnDatabaseConnected bring a database older than this build requires forward through the upgrade scripts before it decides whether it can run, and then continue the start; a step that fails leaves HM5011 naming the step and the version the database is at, and the service not started. 0 refuses an older database as before. A file key and not a setting of the store, because it is read before the database is at a version this build can open. The walk is Common/Persistence/SchemaUpgrader, which hmailserver --upgrade-database on Linux also uses; applies at a restart |
Application.cpp OnDatabaseConnected; SchemaUpgrader |
S! |
Setters that write this section: SetDatabaseServer/Name/Username/Password/Type/Port/IsInternalDatabase (IFS.cpp:1060-1173), reached from COM Database.* (COM/InterfaceDatabase.cpp).
| Key | Type | Default | Normalisation | Used for | Consumer(s) | Timing |
|---|---|---|---|---|---|---|
ProgramFolder |
path | "" | trailing \ appended (IFS.cpp:125-127) |
Bin dir (ProgramFolder\Bin, IFS.cpp:1187-1191), ProgramFolder\DBScripts (IFS.cpp:139-142), ProgramFolder\Languages (IFS.cpp:893-897), ProgramFolder\WebAdmin\index.html (RestApiServer.cpp:2611-2614) |
Compression.cpp, DatabaseSettings.cpp, MySQLConnection.cpp, Language(s).cpp, RestApiServer.cpp | S |
DataFolder |
path | "" | trailing \ stripped (IFS.cpp:129-131) |
Message store root; ACME dir default DataFolder\ACME (AcmeClient.cpp:250-255); sending-limits state file DataFolder\hmailserver_sendinglimits.dat (RateLimiter.cpp:34,268-270); quarantine, sieve storage, backups, disk-space checks; live-update download directory DataFolder\Updates (UpdateDownloader.cpp:39-46; new in 6.2.28; left out of backups since 6.3.6) |
16 files (PersistentMessage, PersistentAccount/Domain, QuarantineStore, SieveStorage, BackupExecuter, BackupScheduleTask, DiskSpace, RateLimiter, ServerStatus, MailImporter, AcmeClient, NameChanger, Message, TestDataDirectory, VirusScannerTester), UpdateDownloader, UpdateCheckTask | S |
TempFolder |
path | "" | trailing \ stripped (IFS.cpp:133-135) |
Temp files for scanners/compression/process launcher | ClamWinVirusScanner, VirusScanner, Compression, FileUtilities, ProcessLauncher, Utilities | S |
EventFolder |
path | "" | none (IFS.cpp:137) |
EventFolder\EventHandlers.<vbs|js> script (ScriptServer.cpp:166-170, 291-295) |
ScriptServer.cpp; COM InterfaceScripting | S |
DatabaseFolder |
path | "" | trailing \ stripped (IFS.cpp:164-166) |
Location of the internal SQL CE database (InterfaceDatabase.cpp:488-495) | DatabaseConnectionManager, InterfaceDatabase | S |
LogFolder |
path | "" | none | Log file directory; read lazily on first GetLogDirectory() with a 255-char buffer and cached (IFS.cpp:880-891) |
Logger.cpp:36, LogRetentionTask, MessageStoreConsistencyTask, CrashOracle, ExceptionLogger, Mime.cpp, MessageData.cpp, TestErrorLogs; COM InterfaceLogging/BackupSettings | L (once) |
Installer defaults: ProgramFolder={app}, DatabaseFolder={app}\Database, DataFolder={app}\Data, LogFolder={app}\Logs, TempFolder={app}\Temp, EventFolder={app}\Events (source: installation/section_ini.iss:1-8). Setters: IFS.cpp:995-1058 (COM Settings.Directories).
| 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 |
StoredSecretsKey |
protected string | "" |
6.3.4 (secrets at rest, schema 6103). The installation's stored-secrets key - 32 random bytes every server sharing the database holds - kept here under this machine's DPAPI (the Linux key file <data directory>/.hmailserver-secret-key on Linux), never in the database and never in a backup; the database records only its id (hm_settings StoredSecretsKeyId). Under it an account's TOTP secret and the [Settings] secrets (SRSSecret, BATVSecret, OAuth2HmacSecret, OAuth2IntrospectionClientSecret, OutboundOAuth2ClientSecret, OutboundOAuth2FixedToken, MetricsServerAuthToken, MetricsServerAuthPassword, ServiceAccountPassword, GssapiServicePassword) are sealed as HMK1:<key id>:<base64> (AES-256-GCM). Written only by an elevated step, never by the service: the Windows installer on a new installation, the container's --create-database, or --create-stored-secrets-key; moved with --export-stored-secrets-key and --import-stored-secrets-key; a lost key's seals cleared with --forget-sealed-secrets <key id>. Empty after an upgrade, and then nothing is sealed, as before 6.3.4. A key line that does not open here, or is not the recorded one, seals nothing and is reported at each start (HM7495). ProtectStoredSecretsWithDPAPI=0 keeps everything as written (hmailserver/docs/SecretsAtRest.md) |
StoredSecrets.cpp, IniFileSettings.cpp | S |
AdministratorPassword |
hash string | "" | COM/REST "Administrator" credential. Empty = no authentication required for COM (COM/COMAuthentication.cpp) and the REST API refuses to start (RestApiServer.cpp:559-561, confirmed). Hash type detected by prefix/length: PBKDF2 prefix, Argon2id prefix, scrypt $s2$ prefix, 32 chars = MD5, 70 chars = SHA256 (Common/Util/Crypt.cpp:171-186 GetHashType). Written by the server as PBKDF2 (IFS.cpp:1165); up to 6.3.3 written by the installer as an unsalted MD5 digest when a password was supplied (section_ini.iss:17; hMailServerInnoExtension.iss), which nothing re-hashed. Since 6.3.4 the installer writes PBKDF2 itself, and the first successful administrator sign-in (Control Panel, COM, REST, hmctl or the Deck) writes an MD5 or salted SHA-256 value, or a PBKDF2 hash with fewer iterations than PasswordHashIterations, again as PBKDF2; a file the service cannot write keeps the old value and says so once (HM7494) (hmailserver/docs/SecretsAtRest.md) |
COMAuthentication.cpp, RestApiServer.cpp, ScramSha256.cpp | S |
At master 540347500 (24 September 2026) the loader reads the same 241, and UseLanguage per call - 242 again; build/ini-settings-baseline.txt lists the 241. The baseline is the same at the tag v6.3.6: 6.3.6 adds no [Settings] key and removes none, but reads the five update-trust keys from the file alone (the note under the live-update keys, before section 9.16). In 6.2.28, 237 keys were read in LoadSettings (IFS.cpp:175-796; 234 through ReadIniSettingString_/ReadIniSettingInteger_ and three — PasswordHashIterations, PasswordHashMemoryKB, PasswordHashTimeCost — through ReadPasswordHashWorkFactor_), plus UseLanguage read per call (IFS.cpp:1116) = 238 [Settings] keys in total. Counted again on 15 September 2026 in v6.3.3 and at 3233b0cc4, which read the same set: 238 through ReadIniSettingString_/ReadIniSettingInteger_, the three work factors and UseLanguage = 242 [Settings] keys. The four added since 6.2.28 are AutoBanFirewall, AutoBanCommand and AutoBanNeverBan (6.3.2, section 9.1) and WebServicesTrustedProxies (6.3.3, section 9.5). "238" elsewhere - in the commit history, build/ini-settings-baseline.txt and some pages - is the count of the literal calls alone. All are owned by IniFileSettings. Since the HTTP foundation (new in 6.2.28) CalDavRedirectUrl/CardDavRedirectUrl are latched by LoadSettings like every other key (IFS.cpp:493-494); nothing in [Settings] is read outside IniFileSettings any more. Booleans are 1/anything-else unless stated. "Consumer" is the file that calls the getter.
Bin\
├─ hMailServer.ini ← everything in sections 5 to 11 below
│ ├─ [Database] file only, never mirrored
│ ├─ [Directories] file only, never mirrored
│ ├─ [GUILanguages] file only, never mirrored
│ ├─ [Security] file only, never mirrored
│ ├─ [Settings] 242 keys, mirrored into hm_inisettings (6.3.3)
│ ├─ [SendingLimits] read live by RateLimiter, not mirrored
│ ├─ [SendingLimitsOverrides] one line per account, same reader
│ └─ [LDAP] read live by LdapSettings, not mirrored
└─ hMailServerApiKeys.ini ← section 12; one [Key.<id>] section per REST API key
Since 6.3.4 the picture changes in one line and gains two
sections: [Settings] is a copy of hm_inisettings, which is the store, except for
PasswordPepper, which lives only here; [SettingsStore] holds the server's two-key note
saying so; and [SettingsOverride], when an administrator writes one, forces its keys over the
store (section 4a).
Five keys turn a listener on, and every one of them is 0 by default, so a stock server
listens on nothing but the mail protocols it was configured with. A sixth, AcmeHttpPort,
defaults to 80 but only opens a socket of its own while an issuance is running:
| Key | Default | Listener | Extra condition before it starts | Message when it refuses |
|---|---|---|---|---|
MetricsServerPort |
0 | Prometheus + /livez /readyz /healthz
|
the bind address must be an IP literal (localhost is not accepted here) |
MetricsServer: Invalid bind address: ... |
RestApiPort |
0 | REST API, WebAdmin page, self-service portal |
[Security] AdministratorPassword must be set; TLS unless bound to 127.0.0.1, localhost or ::1
|
RestApi: Refusing to start - the administrator password is not set. / ... TLS certificate is required unless bound to 127.0.0.1 or ::1.
|
WebServicesHttpPort |
0 | MTA-STS policy, autoconfig, ACME http-01, DAV redirects | — | — |
WebServicesHttpsPort |
0 | the same over HTTPS | a certificate of its own, or an ACME one on disk | No TLS certificate available yet. The HTTPS listener is disabled until a certificate exists (enable ACME or set WebServicesCertificateFile). |
ManageSieveServerPort |
0 | ManageSieve, RFC 5804 | — | — |
AcmeHttpPort |
80 | (not a listener of its own unless issuance is running) |
AcmeEnabled=1 and a non-empty AcmeDomains
|
ACME: No domains configured |
And these are the settings whose stated value suggests a working feature while nothing is actually happening — either because they are on by default with no listener behind them, or because a companion value is missing. This shape has produced more "it is enabled but it does not work" reports than any other:
flowchart TD
A["A feature is enabled but inert"] --> B{"Which one?"}
B --> C["MtaStsHostingEnabled=1"]
C --> C1["Needs WebServicesHttpsPort. Reported at every start by ReportUnreachableFeatures."]
B --> D["AutoconfigEnabled=1"]
D --> D1["Needs WebServicesHttpPort or WebServicesHttpsPort. Same report."]
B --> E["IndexerFullText=1"]
E --> E1["Also needs COM Settings.MessageIndexing to be on."]
B --> F["WindowsEventLogEnabled=1"]
F --> F1["Writes nothing on a healthy server at the default level 2."]
B --> G["OutboundOAuth2Hosts and FetchOAuth2Hosts have non-empty defaults"]
G --> G1["Need OutboundOAuth2TokenUrl or OutboundOAuth2FixedToken. With both empty, delivery quietly falls back to LOGIN and logs nothing."]
B --> H["SRSEnabled=1"]
H --> H1["Needs SRSSecret. SRS::Forward returns an empty address for an empty secret and the forwarder then leaves the envelope alone, silently."]
B --> I["TlsRptFromAddress empty"]
I --> I1["Statistics are collected and discarded. Nothing is sent and nothing is logged."]
B --> J["The SendingLimits and LDAP sections"]
J --> J1["Entirely inert until Enabled, or a non-zero limit, is set."]
- 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 and scrypt hashes (Crypt::ApplyPepper_ is called for those two schemes and no other; PBKDF2 stays unpeppered for SCRAM); empty = off (IFS.cpp:200-203). Since 6.3.4, the one [Settings] key kept in the file only: never stored in hm_inisettings, never in a backup (section 4a) |
Crypt.cpp | S |
PasswordHashIterations |
int | 0 | 0, or 10000-10000000, else HM5561 and read as 0 | PBKDF2 iterations for NEW password hashes; 0 = the built-in 210,000. A stored hash carries its own count, so raising this never breaks a logon: a hash cheaper than the configured value is re-derived on the next successful logon (PasswordValidator::UpgradeStoredPasswordHash_ via HashCreator::NeedsRehash), a costlier one is left alone - lowering it affects new hashes only (IFS.cpp ReadPasswordHashWorkFactor_). Added 5 September 2026 |
HashCreator, PasswordValidator | S |
PasswordHashMemoryKB |
int | 0 | 0, or 4096-1048576, else HM5561 and read as 0 | Argon2id memory for new hashes, in KiB; 0 = the built-in 19,456. Same re-derive rule as PasswordHashIterations. Every logon verification allocates this much on a connection thread. Added 5 September 2026 |
HashCreator, PasswordValidator | S |
PasswordHashTimeCost |
int | 0 | 0, or 1-20, else HM5561 and read as 0 | Argon2id passes for new hashes; 0 = the built-in 2. Same re-derive rule as PasswordHashIterations. Added 5 September 2026 |
HashCreator, PasswordValidator | S |
ProtectStoredSecretsWithDPAPI |
bool | 1 | Store the ini DB password and DB-held route/fetch/relayer secrets as machine-bound DPAPI (DPAPI: prefix) instead of Blowfish; legacy values stay readable (IFS.cpp:230-236; Crypt.cpp:188-220). Since 6.3.4, 0 also keeps an account's second factor and the [Settings] secrets as written rather than sealing them under the stored-secrets key (section 8) |
Crypt.cpp, IFS.cpp:1105-1117, StoredSecrets.cpp | S | |
ServiceAccountName |
string | "" | Up to 6.3.3, the Windows account the service logs on as; empty = LocalSystem. Applied only by hMailServer.exe /Register (CreateService/ChangeServiceConfig) (IFS.cpp:238-247; Common/Util/ServiceManager.cpp:57-62,123-127; hMailServer.cpp:216-249). Since 6.3.4 still read, and no longer used by /Register, which takes the account from its command line alone (--service-account virtual|localsystem|keep|DOMAIN\name$): the value is a row of the settings store that administrators who need not be Windows administrators can set, so a registration that read it could be made to give the service LocalSystem back. A new installation runs as NT SERVICE\hMailServer (hmailserver/docs/ServiceAccount.md) |
IniFileSettings.cpp (read) | - | |
ServiceAccountPassword |
string | "" | Password for the above; empty for virtual/managed accounts (IFS.cpp:247). Since 6.3.4 not used by /Register either, and one of the [Settings] secrets sealed under the stored-secrets key (section 8) |
IniFileSettings.cpp (read) | - | |
PasswordPolicyMinimumLength |
int | 0 | Enforced where a password is CHOSEN (COM Account.Password); 0 = off (IFS.cpp:666; IFS.h:414-418) | PasswordPolicy.cpp | S | |
PasswordPolicyRequireMixedCase |
bool | 0 | as above (IFS.cpp:667) | PasswordPolicy.cpp | S | |
PasswordPolicyRequireDigit |
bool | 0 | (IFS.cpp:668) | PasswordPolicy.cpp | S | |
PasswordPolicyRequireNonAlphanumeric |
bool | 0 | (IFS.cpp:669) | PasswordPolicy.cpp | S | |
PasswordPolicyRejectCommon |
bool | 0 | Rejects passwords in a compiled-in list (PasswordPolicy.cpp:29,153-159) (IFS.cpp:670) | PasswordPolicy.cpp | S | |
PasswordPolicyHistoryCount |
int | 0 | Refuse reuse of the last N passwords; 0 = off (IFS.cpp:671; IFS.h:425-432) | PasswordHistory.cpp | S | |
PasswordPolicyMaximumAgeDays |
int | 0 | Password expiry in days; 0 = off. IMAP, POP3 and SMTP cannot change a password and the self-service portal (new in 6.2.28) refuses an expired one at sign-in, so an expired password is renewed by an administrator — or by the person themselves only if they hold an app password, which still signs them in to /portal, where POST /api/v1/me/password takes the expired password as the current one. Active Directory accounts are exempt (IFS.cpp:794; IFS.h:556) |
PasswordHistory.cpp | S | |
AccountLockoutThreshold |
int | 0 | Failed logons per canonical username before lockout; 0 = mechanism off (IFS.cpp:558; Common/Util/AccountLockout.cpp:136,175) |
AccountLockout.cpp | S | |
AccountLockoutWindowMinutes |
int | 30 | <1 → 30 | Window in which failures are counted (IFS.cpp:559-562) | AccountLockout.cpp | S |
AccountLockoutMinutes |
int | 30 | <1 → 30 | Lock duration (IFS.cpp:560-564) | AccountLockout.cpp | S |
LogonTarpitSeconds |
int | 0 | clamped 0–30 | Login tarpit (added 5 Sep 2026): each failed logon's refusal waits this × the failures so far on that connection, capped 30s, on SMTP/POP3/IMAP; 0 = off. A pause on the connection's own timer (TCPConnection::EnqueueDelay, BCTDelay), not a thread asleep. A correct password never waits. |
AccountLogon::TarpitDelaySeconds; SMTP/POP3/IMAPConnection | S |
SmtpTarpitCount |
int | 0 | clamped ≥0 | Recipient tarpit (added 5 Sep 2026): recipients an unauthenticated SMTP session may name before the delay applies; 0 = off. Also settable via COM AntiSpam.TarpitCount. |
SMTPConnection::TarpitRecipient_ | S |
SmtpTarpitDelaySeconds |
int | 0 | clamped 0–30 | Recipient tarpit: seconds each RCPT TO past SmtpTarpitCount waits for its reply; 0 = off. Authenticated sessions and ranges with spam protection off are exempt. Also settable via COM AntiSpam.TarpitDelay. |
SMTPConnection::TarpitRecipient_ | S |
AutoBanFirewall |
bool | 0 | Added in 6.3.2. Pushes every auto-ban below the server. Windows: an inbound TCP block rule per banned address in Windows Defender Firewall, scoped to the ports this server listens on and named hMailServer auto-ban <address>. Linux: the packaged hook /usr/lib/hmailserver/autoban-hook (an nftables set with a timeout, iptables where there is no nft), which needs CAP_NET_ADMIN the systemd unit does not grant by default. A reconciliation rather than an event stream: AutoBanFirewall::Synchronise makes the firewall match the auto-ban ranges at start-up, on every ban, on every range deletion and once a minute (IFS.cpp:540 at 3233b0cc4; Common/Util/AutoBanFirewall.h) |
AutoBanFirewall.cpp | S | |
AutoBanCommand |
string | "" | Added in 6.3.2. A program of the administrator's, run as <command> ban <address> <minutes> <ports> and <command> unban <address> on both platforms, instead of or as well as AutoBanFirewall (IFS.cpp:541). Since 6.3.6 the command's account is kept in autoban-command.ledger in the temp folder, under the SHA-256 of the command line and never its text; at a start the command is told unban for each address it was left holding that is not banned now (the ledger begins at the first start of 6.3.6). A command replaced or cleared is never run again: the addresses it was told to ban and not to un-ban are named in the error log (HM6523), and the new command is told every ban in force. A command of your own is still best made to let a block lapse after the <minutes> it is handed |
AutoBanFirewall.cpp | S | |
AutoBanNeverBan |
list | "" | an entry that is not an address or CIDR block is ignored, reported once per value as HM6524 | Added in 6.3.2. Addresses and CIDR blocks (1.2.3.4, 1.2.3.0/24, 2001:db8::/32), separated by commas, semicolons or whitespace, whose failed logons are never counted, so they are never auto-banned; the per-connection disconnect still applies (IFS.cpp:542; AutoBanFirewall.cpp IsNeverBanned; AccountLogon.cpp RegisterFailedLogin). Since 6.3.6 an entry written in a form Windows used to take (192.168.1/24, which protected 192.168.0.0/24 without a word) is reported as HM6524 and protects nothing, on Windows as on Linux; the never-ban list by reverse name (section 17) spares an address by its reverse name |
AccountLogon.cpp, AutoBanFirewall.cpp | S |
The JWK Set and introspection fetches are the server acting as a web client, so HttpProxy (section 9.16) carries them when one is set. Since 6.3.6 the outbound/fetch XOAUTH2 token request goes through HttpsClient too, and so through HttpProxy when one is set; a port in OutboundOAuth2TokenUrl is used, and the request is given up after 20 seconds whole. Up to 6.3.5 OutboundOAuth2TokenClient.cpp opened its own connection and never saw that key.
| Key | Type | Default | What it does | Consumer | Timing |
|---|---|---|---|---|---|
OAuth2Enabled |
bool | 0 | Accept SASL XOAUTH2/OAUTHBEARER JWTs verified locally (IFS.cpp:205-208) | OAuth2TokenValidator.cpp | S |
OAuth2RequireTLS |
bool | 1 | Mechanism only offered on TLS (IFS.cpp:209) | OAuth2TokenValidator.cpp | S |
OAuth2AllowedAlgorithms |
comma list | RS256 |
Accepted JWT alg: HS256, RS256, ES256 recognised; none never honoured (IFS.cpp:218-220; OAuth2TokenValidator.cpp:216-220,390) |
OAuth2TokenValidator.cpp | S |
OAuth2HmacSecret |
string | "" | HS256 key (IFS.cpp:222) | OAuth2TokenValidator.cpp | S |
OAuth2PublicKeyFile |
path | "" | PEM RSA/EC public key for RS256/ES256 (IFS.cpp:224; getter is named GetOAuth2RsaPublicKeyFile) |
OAuth2TokenValidator.cpp | S |
OAuth2Issuer |
string | "" | Expected iss; empty disables the check (IFS.cpp:226) |
OAuth2TokenValidator.cpp | S |
OAuth2Audience |
string | "" | Expected aud; empty disables (IFS.cpp:227) |
OAuth2TokenValidator.cpp | S |
OAuth2UsernameClaim |
string | email |
Claim carrying the login name (IFS.cpp:229) | OAuth2TokenValidator.cpp | S |
OAuth2JwksUrl |
string | "" | RFC 7517 JWK Set URL: the provider's published signing keys, selected by the token's kid for RS256/ES256, with OAuth2PublicKeyFile as the fallback. https, or plain http to a loopback address only (Common/Util/JwksKeySet.cpp, HttpsClient.cpp). Added 5 September 2026 |
OAuth2TokenValidator | S |
OAuth2JwksCacheSeconds |
int seconds | 3600 | How long the JWK Set is kept before re-fetching; a token naming an unknown kid re-fetches at once, at most once a minute. Minimum 10. Added 5 September 2026 |
JwksKeySet | S |
OAuth2IntrospectionUrl |
string | "" | RFC 7662 introspection endpoint, asked after a token verifies locally whether it is still active; empty = no revocation check (Common/Util/TokenIntrospection.cpp). Added 5 September 2026 |
OAuth2TokenValidator | S |
OAuth2IntrospectionClientId / OAuth2IntrospectionClientSecret
|
string | "" | The client this server introspects as (HTTP Basic, RFC 6749 2.3.1). Added 5 September 2026 | TokenIntrospection | S |
OAuth2IntrospectionCacheSeconds |
int seconds | 300 | How long a verdict (active or not) is reused, never past the token's exp. Added 5 September 2026 |
TokenIntrospection | S |
OAuth2IntrospectionFailOpen |
int (0/1) | 0 | 1 accepts a token when the endpoint cannot answer (RFC 7662 says treat it as inactive, which 0 does); every such acceptance is written to the application log. Added 5 September 2026 | TokenIntrospection | S |
OutboundOAuth2TokenUrl |
URL | "" | client_credentials token endpoint for outbound relay XOAUTH2 (IFS.cpp:210; IFS.h:161-166) | OutboundOAuth2TokenClient.cpp, ExternalDelivery.cpp | S |
OutboundOAuth2ClientId |
string | "" | (IFS.cpp:211) | OutboundOAuth2TokenClient.cpp | S |
OutboundOAuth2ClientSecret |
string | "" | (IFS.cpp:212) | OutboundOAuth2TokenClient.cpp | S |
OutboundOAuth2Scope |
string | https://outlook.office365.com/.default |
(IFS.cpp:213-214) | OutboundOAuth2TokenClient.cpp | S |
OutboundOAuth2Hosts |
comma list | smtp.office365.com |
Relay hosts the token is presented to (IFS.cpp:215) | ExternalDelivery.cpp | S |
OutboundOAuth2FixedToken |
string | "" | Used verbatim instead of fetching (tests / externally obtained tokens) (IFS.cpp:216) | ExternalDelivery.cpp, POP3ClientConnection.cpp | S |
FetchOAuth2Hosts |
comma list | outlook.office365.com |
External (POP3 fetch) servers that authenticate with XOAUTH2 using the outbound credentials (IFS.cpp:217; IFS.h:174-177) | POP3ClientConnection.cpp | S |
| 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! |
WebServicesTrustedProxies |
comma list of IP/CIDR | "" | Added in 6.3.3. The peers, besides loopback, whose X-Forwarded-Proto: https the plain HTTP web-services listener believes - which decides whether the Apple .mobileconfig profile is served and whether CalDAV and CardDAV treat the request as HTTPS. From any other peer the header is ignored. An entry that is not an address or CIDR range matches nothing and is reported once per service start as HM6281 (IFS.cpp:780 at 3233b0cc4; WebServicesServer.cpp RequestArrivedOverHttps_; ProxyProtocol.cpp TrustedProxyList) |
WebServicesServer.cpp | S |
MtaStsHostingEnabled |
bool | 1 | Serve /.well-known/mta-sts.txt — inert unless a WebServices port is set; ReportUnreachableFeatures logs this (IFS.cpp:607; Application.cpp:542-557) |
WebServicesServer.cpp | S! |
MtaStsPolicyMode |
string | enforce |
Must be enforce, testing or none; anything else → enforce (IFS.cpp:608; WebServicesServer.cpp:1128-1131) |
WebServicesServer.cpp | S |
MtaStsPolicyMaxAge |
int seconds | 604800 | (IFS.cpp:609) | WebServicesServer.cpp | S |
MtaStsPolicyMx |
comma list | "" | MX hosts in the policy; empty → derived from MX lookup (mx_cache) (IFS.cpp:610; WebServicesServer.cpp:1030-1040, 95-97) | WebServicesServer.cpp | S |
AutoconfigEnabled |
bool | 1 | Thunderbird/Outlook autoconfig endpoints — also inert without a WebServices port (IFS.cpp:611) | WebServicesServer.cpp, RestApiServer.cpp | S |
AutoconfigClientHost |
host | "" | Host advertised to clients; empty → COM Settings.HostName (IFS.cpp:612; WebServicesServer.cpp:1352-1358) |
WebServicesServer.cpp, RestApiServer.cpp | S |
CalDavRedirectUrl |
http(s) URL, absolute, no spaces/control chars | "" | 3xx redirect target for /.well-known/caldav (RFC 6764), naming another server that serves CalDAV. In 6.3.3, which has no CalDAV of its own, empty = 404 silently. Since 6.3.4, whose /dav/ serves CalDAV beside CardDAV, empty redirects to this server's own /dav/. Either way a non-empty unusable value = 404 and HM5780 once per service start, never a fall-back to this server (IFS.cpp:493; WebServicesServer.cpp:1062-1140 in 6.2.28, HandleWellKnownDavRedirect_ in 6.3.4). Latched by LoadSettings since the HTTP foundation (new in 6.2.28) and read through the getter with a 60 s cache in WebServicesServer (WebServicesServer.cpp:67) |
WebServicesServer.cpp | S (60 s cache) |
CardDavRedirectUrl |
same | "" |
/.well-known/carddav (IFS.cpp:493). This server has served CardDAV since 6.3.3, so empty redirects to its own /dav/; a value names another server, and an unusable one is 404 and HM5780 as above |
WebServicesServer.cpp | S (60 s cache) |
AcmeEnabled |
bool | 0 | Let's Encrypt issuance/renewal (IFS.cpp:595; Application.cpp:791) | Application.cpp, AcmeClient.cpp, WebServicesServer.cpp | S! |
AcmeDirectoryUrl |
URL | https://acme-v02.api.letsencrypt.org/directory |
(IFS.cpp:596) | AcmeClient.cpp | S |
AcmeContactEmail |
string | "" | (IFS.cpp:597) | AcmeClient.cpp | S |
AcmeDomains |
comma list | "" | (IFS.cpp:598) | AcmeClient.cpp | S |
AcmeCertificateDirectory |
path | "" | Empty → DataFolder\ACME (IFS.cpp:599; AcmeClient.cpp:250-255) |
AcmeClient.cpp, Application.cpp | S |
AcmeHttpPort |
int | 80 | Port expected for HTTP-01 (IFS.cpp:600) | AcmeClient.cpp, WebServicesServer.cpp | S |
AcmeReuseKey |
bool | 1 | Keep the private key across renewals (IFS.cpp:601) | AcmeClient.cpp | S |
| 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, four decimal parts, no port syntax, read the same way by every lookup since 6.3.6 (the Windows resolver read it with inet_addr, which took the older forms, while the DNSSEC resolver and the TLS policy's lookups used inet_pton: DNSServer=8.8.8 went to 8.8.0.8 for one and the system's servers for the other); a value that is not one, an older form such as 8.8.8 included, is reported invalid (HM4401) and the system's servers are asked; port field deliberately 0 (IFS.cpp:401; DNSResolverWinApi.cpp:252-291) |
DNSResolverWinApi.cpp, DnssecResolver.cpp, TlsPolicy.cpp | S |
DNSQueryTimeout |
int seconds | 10 | Bound on a resolver call; 0 = INFINITE (IFS.cpp:293; DNSResolverWinApi.cpp:120-124) | DNSResolverWinApi.cpp | S |
DnssecValidationEnabled |
bool | 1 | Built-in DNSSEC validation (IFS.cpp:404). The validating resolver asks DNSServer, or else the system's first two IPv4 DNS servers, and since 6.3.6, when the system lists no IPv4 DNS server, its first two IPv6 ones |
DNSResolver.cpp, DnssecResolver.cpp, TlsPolicy.cpp | S |
DnssecTrustAnchors |
;-separated DS records keytag alg digesttype hexdigest
|
"" | Overrides the built-in IANA root KSKs (20326 …) when non-empty (IFS.cpp:405; DnssecResolver.cpp:1003-1040) | DnssecResolver.cpp | S |
DaneEnforcementEnabled |
bool | 1 | DANE/TLSA enforcement on outbound delivery. Getter is GetDaneEnabled; the key is NOT DaneEnabled (IFS.cpp:403; ExternalDelivery.cpp). Since 6.3.6 needs a resolver that answers DNSSEC queries: behind one that strips signatures, mail to every domain the server cannot prove unsigned is deferred (Encryption and Certificates, What DANE actually does); 0 leaves DANE off |
ExternalDelivery.cpp | S |
MtaStsEnabled |
bool | 1 | Outbound MTA-STS policy fetching/enforcement (IFS.cpp:402) | ExternalDelivery.cpp | S |
SpfVoidLookupLimit |
int | 2 | RFC 7208 4.6.4 void-lookup limit; 0 disables (IFS.cpp:640; IFS.h:317-321) | SPF/SPF.cpp | S |
DmarcTreeWalkEnabled |
bool | 1 | RFC 9989 tree walk for organisational domain; 0 = compiled-in PSL (IFS.cpp:639; IFS.h:310-315) | DMARC/DMARC.cpp | S |
| 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). Since 6.3.4 the set stores the row and then writes the line where the service may write the file, like every other key, and the get still reads the file |
IFS.cpp:1115 | L |
Every key here is new in 6.2.28. The scheduled task UpdateCheckTask (Common/Application/UpdateCheckTask.cpp) is created at start regardless of these keys (runs once at start, then every 15 minutes: Application.cpp:709-716) and calls the getters in DoWork, so every key is S. UpdateChecker.cpp, UpdateDownloader.cpp, UpdateInstaller.cpp, UpdateWindow.cpp and SigstoreVerifier.cpp are under Common/Util/.
| Key | Type | Default | What it does | Consumer | Timing |
|---|---|---|---|---|---|
UpdateCheckEnabled |
bool | 0 | Turns on the scheduled release check: when 1, UpdateCheckTask reads the release feed every UpdateCheckHours and logs/records when a newer release exists (UpdateCheckTask.cpp:139-151; IFS.cpp:489). An on-demand check over COM or REST calls UpdateChecker::CheckNow directly and ignores this key (InterfaceStatus.cpp:275; RestApiServer.cpp:8232). A helper-applied update is reported on the next start whether or not this is on (UpdateCheckTask.cpp:134) |
UpdateCheckTask.cpp, UpdateChecker.cpp | S |
UpdateChannel |
string | stable |
prerelease (or pre-release, case-insensitive) makes pre-releases eligible and switches the default feed; anything else is the stable channel (UpdateChecker.cpp:95-99, 488-492; IFS.cpp:490) |
UpdateChecker.cpp | S |
UpdateFeedUrl |
URL | "" | Release feed in GitHub's releases JSON shape (one release or a list). Empty = https://updates.progressiverobot.com/releases/latest on the stable channel or https://updates.progressiverobot.com/releases.json on the pre-release channel since 6.3.4, which GitLab Pages serves from the project's releases (UpdateChecker.cpp:177-178, 259 at e3df8b5e6); up to 6.3.3 it was https://api.github.com/repos/Progressiverobot/hmailserver/releases/latest or .../releases?per_page=10, which has answered HTTP 404 since GitHub suspended the project's account on 18 September 2026, so such a server's check fails with The update feed answered HTTP 404. until this names https://updates.progressiverobot.com/releases.json. Set it for a mirror - which must be https unless it is on this machine, since HttpsClient refuses plain http to anything but a loopback address (HttpsClient.cpp:315-319). The per-tag lookup keeps everything up to /releases of the configured URL (UpdateChecker.cpp:274-283; IFS.cpp:491) |
UpdateChecker.cpp | S |
UpdateCheckHours |
int hours | 24 | Interval between feed reads once the check is on; <1 → 1 (IFS.cpp:495-497; UpdateChecker.cpp:326-333). No ceiling is applied by the server although the Control Panel's label says "1 to 168" (FeatureSettingsView.xaml.cs:1933) | UpdateChecker.cpp | S |
UpdateAutoDownload |
bool | 0 | When a check finds a newer release, download its installer and Sigstore bundle at once into DataFolder\Updates and verify them (UpdateCheckTask.cpp:157-162; UpdateDownloader.cpp:39-46; IFS.cpp:509). With 0 the download is a person's click |
UpdateCheckTask.cpp, UpdateChecker.cpp | S |
UpdateWindow |
string | "" | When a verified installer may be applied unattended. 03:00 = every day from 03:00 for an hour; Sun 03:00 = Sundays; Sat,Sun 02:00-05:00 = those days between the two times. Days are English three-letter names in any case/order, times are local 24-hour; a range crossing midnight belongs to the day it starts on (UpdateWindow.h:11-22). Empty = never apply on its own. An unparsable value is logged once per process and nothing is applied (UpdateCheckTask.cpp:171-183; IFS.cpp:508) |
UpdateCheckTask.cpp, UpdateChecker.cpp, UpdateWindow.cpp | S |
UpdateBackupBeforeApply |
bool | 1 | Before an unattended apply, take a backup to the configured backup destination; no destination or a failed backup cancels that apply (UpdateCheckTask.cpp:82-118, 186; IFS.cpp:510). Applies to the unattended path only | UpdateCheckTask.cpp | S |
UpdateRequireAuthenticode |
bool | 0 | After the Sigstore bundle verifies, also require a trusted Authenticode signature on the downloaded installer; otherwise the download is refused (UpdateDownloader.cpp:143-146; IFS.cpp:504) | UpdateDownloader.cpp | S |
UpdateServiceWaitSeconds |
int seconds | 180 | Passed as --wait to hMailServer.Updater.exe: how long the helper gives the service to come back after the installer ran before it reinstalls the previous version; <5 → 5 (IFS.cpp:505-507; UpdateInstaller.cpp:26, 234) |
UpdateInstaller.cpp | S |
UpdateTrustRootsFile |
path | "" | PEM file whose CA certificate(s) replace the public Sigstore trust roots the bundle's certificate chain is checked against. Must exist and contain a PEM certificate, else verification fails with that reason (SigstoreVerifier.cpp:415-434; IFS.cpp:499) | SigstoreVerifier.cpp | S |
UpdateLogPublicKeyFile |
path | "" | PEM public key of the transparency log whose inclusion proof the bundle carries; replaces the public instance's key. Must exist and parse as a PEM public key (SigstoreVerifier.cpp:436-451; IFS.cpp:500) | SigstoreVerifier.cpp | S |
UpdateSigningIdentity |
string | "" | Prefix the signing certificate's identity must carry; empty = each compiled-in signer's own: up to 6.3.3 https://github.com/Progressiverobot/hmailserver/.github/workflows/sign-release.yml@ alone, and since 6.3.4 that one, https://gitlab.com/Progressiverobot/hmailserver//.gitlab-ci.yml@refs/tags/v and https://gitlab.com/hmailserver/hmailserver//.gitlab-ci.yml@refs/tags/v (SigstoreVerifier.cpp:103, 131, 174, 611-615 at 396c01d4d; IFS.cpp:501) |
SigstoreVerifier.cpp | S |
UpdateSigningIssuer |
string | "" | OIDC issuer the certificate must name; empty = each compiled-in signer's own: https://token.actions.githubusercontent.com for the GitHub workflow, and since 6.3.4 https://gitlab.com for the two GitLab entries (SigstoreVerifier.cpp:104, 132 at 396c01d4d; IFS.cpp:502) |
SigstoreVerifier.cpp | S |
UpdateSourceRepository |
string | "" | Repository the certificate must name; empty = each compiled-in signer's own: https://github.com/Progressiverobot/hmailserver, and since 6.3.4 https://gitlab.com/Progressiverobot/hmailserver and https://gitlab.com/hmailserver/hmailserver, the group the project moved to on 29 September 2026, where the certificate must also carry the project's id, 86730559 (since 6.3.6 a certificate for https://gitlab.com/Progressiverobot/hmailserver needs the id too, as the group's path always did); the single character - switches the repository check off, except that since 6.3.4 a certificate that names the group's path or its repository is refused without the project's id whatever this key and the other two say, - included (SigstoreVerifier.cpp:105, 133, 175, 719, 1233-1245 at 396c01d4d; IFS.cpp:503). Up to 6.3.3 the defaults of this key, UpdateSigningIdentity and UpdateSigningIssuer name GitHub's release workflow alone, which signed the releases from 6.2.19 to 6.3.3 and can sign no later one, so such a server refuses a release signed anywhere else unless the three are set to its identity. Since 6.3.4 the defaults trust the GitHub workflow for the releases up to 6.3.3 and the project's GitLab CI pipeline for a v* tag at either path; the three set to the values of the path the project began at keep a server to that path alone, so it refuses what is signed after the move, every release from 6.3.6. Never set the three to the group's path: a server from 6.2.28 to 6.3.3 checks no project id, so it would install whatever anyone who holds that path signs for a v* tag, and since the project moved such a server is upgraded by hand (UpdateFeed.md) |
SigstoreVerifier.cpp | S |
Since 6.3.6 the five trust keys - UpdateTrustRootsFile, UpdateLogPublicKeyFile, UpdateSigningIdentity, UpdateSigningIssuer and UpdateSourceRepository - are read from hMailServer.ini's [Settings] section alone, on every platform, never from the database (item 2 of 6.3.6's "What changes on upgrade"). At the first start a value 6.3.4 or 6.3.5 stored in the database is taken out (HM5809 names it once when it differed from the file's); a line for one of the five in [SettingsOverride] is ignored and named as HM5809 at every start. COM and the REST API refuse a change to them (409), and so the Control Panel, the Deck, a settings profile and the audit trail's put-back; hmctl config export, Export-HmConfiguration and a backup leave them out. Set them in the file - on Linux sudo hmailserver --set-setting NAME=VALUE - and restart (The Update Feed, Where the trust is read from).
One key, new in 6.2.28. It is read by Common/Util/HttpsClient.cpp, which is the web client behind four fetches: the release feed and the installer/bundle downloads (UpdateChecker.cpp, UpdateDownloader.cpp, section 9.15) and the OAuth2 JWK Set and token introspection (JwksKeySet.cpp, TokenIntrospection.cpp, section 9.2). AcmeClient.cpp and OutboundOAuth2TokenClient.cpp build their own TLS connections and do not honour it, so ACME issuance and the outbound-relay token request still go direct.
| Key | Type | Default | What it does | Consumer | Timing |
|---|---|---|---|---|---|
HttpProxy |
host:port or [ipv6]:port
|
"" | Empty = connect direct. Set, every request through HttpsClient goes to the proxy. For an https target the proxy is asked to CONNECT host:port and the TLS handshake then runs inside the tunnel, with verify_peer, the same CertificateVerifier and the same SNI name as a direct connection, so the proxy sees the host name and nothing after it (HttpsClient.cpp:138-187, 370-378). For plain http the request line carries the absolute URL instead of the path (HttpsClient.cpp:191-198) - although HttpsClient refuses plain http to anything but a loopback address whether or not a proxy is set, so the absolute-URL form only ever carries a feed hosted on this machine (HttpsClient.cpp:315-319, 517-521). A proxy answering CONNECT with anything outside 2xx fails the request with The proxy <host:port> refused CONNECT to <host:port>: <its status line> (HttpsClient.cpp:179-183). A value with no port, or a bracketless IPv6 literal, fails it with HttpProxy must be host:port (or [ipv6]:port). — a mis-set proxy is never quietly a direct connection; surrounding whitespace is trimmed (HttpsClient.cpp:89-131; IFS.cpp:492). No proxy credentials are sent. Editor: beside the feed URL on the Control Panel's Updates card (FeatureSettingsView.xaml.cs:1936) |
HttpsClient.cpp, for UpdateChecker, UpdateDownloader, JwksKeySet, TokenIntrospection, and since 6.3.6 AcmeClient, OutboundOAuth2TokenClient, the MTA-STS policy fetch, brand logos, WKD, Web Push, unsubscribe and inbound webhook routes held to public addresses. A request held to public addresses (the MTA-STS fetch and those after it) asks the proxy for a tunnel to the address the server checked, never the name, so a proxy that allows CONNECT only to names refuses it; the refusal is reported by the proxy's status code alone ("HttpProxy refused CONNECT to X with status N."). A request to an endpoint the administrator chose still quotes the proxy's status line | S |
- 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).
-
6.3.4, a key's section also carries
Role(the administrator role it holds, batch 10) and three fields of the send API (batch 31):SendFrom(an address of a hosted domain, or*@domain; empty - every key made before - sends nothing),SendPerMinuteandSendPerDay, written beforeHashas the other restrictions are and read back failing closed. The key store is not visible over COM, so these are set withPOST /api/v1/apikeys(source: RestApiServer.cppHandleCreateApiKey_,LoadKeys_; hmailserver/docs/SendApi.md).
- 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 in 6.3.3; since 6.3.4, COM for every write and COM first for reads, section 4),Views/FeatureSettingsView.xaml.cs(exposes the keys listed there),Views/LdapSettingsView.cs([LDAP]),Services/ApiKeyStore.cs,Services/WindowsServiceInfo.cs,Views/BackupView.xaml.cs,Views/DirectorySyncView.cs,Views/QuarantineView.cs,Views/MessageTraceView.cs,Views/ApiKeysView.cs. - Installer:
installation/section_ini.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): fails - in CI until 18 September 2026, by hand since - when a[Settings]key read by IniFileSettings.cpp is not named anywhere in the Control Panel source (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 reported 234 keys where LoadSettings read 237; in 6.3.3 it sees 238 of 241. In 6.3.4 it also holds the rule that a setting goes in the database, againstbuild/ini-settings-baseline.txt(section 4a).
Every one of these is written to the application log, and to the Windows event log when
WindowsEventLogEnabled=1 and WindowsEventLogLevel is at or above the severity. Only
HM5005 stops the server; the rest change what a setting means and let it run.
| Code | Severity | Raised when | What the server does then | Source |
|---|---|---|---|---|
| HM5005 | Medium |
[Database] Server or Database is missing for the configured Type
|
Application::OpenDatabase returns false, so the database is never opened and the server does not start |
Application.cpp:197-203, OpenDatabase
|
| HM5528 | Medium |
PreferredHashAlgorithm is not 3, 4, 5 or 7 |
Read as 4 (PBKDF2). New passwords are hashed with PBKDF2, not with whatever was meant | IFS.cpp:190 |
| HM5561 | Medium |
PasswordHashIterations, PasswordHashMemoryKB or PasswordHashTimeCost is outside its allowed range |
Read as 0, which means the built-in work factor. Existing hashes are unaffected | IFS.cpp ReadPasswordHashWorkFactor_
|
| HM5562 | Medium |
SpamAssassinUser contains a control character |
Read as empty. No User: header is sent to spamd, so its global configuration applies |
IFS.cpp:347 |
| HM5780 | Medium |
CalDavRedirectUrl or CardDavRedirectUrl is not a usable absolute URL |
/.well-known/caldav and /.well-known/carddav answer 404. Reported once per service start |
WebServicesServer.cpp:1126-1136 |
| HM6119 | High |
SimulateDatabaseFailureFor is set |
Every SQL statement containing that substring fails, on purpose. Reported on every start and reinitialise, because a server running with it is deliberately broken | Application.cpp:221, OpenDatabase
|
| HM6210 | Medium |
DmarcRptSchemaVersion is not 1 or 2 |
Read as 1 (RFC 7489 Appendix C). Reports go out in the older schema | IFS.cpp:522 |
| HM4401 | Low |
DNSServer is not a parseable IPv4 literal |
The custom resolver is not used: every lookup goes to the system DNS instead. A host:port value or an IPv6 literal lands here, and since 6.3.6 an older form such as 8.8.8
|
DNSResolverWinApi.cpp:261 |
| HM5800 | Medium | A [Settings] key name is longer than 100 characters |
That key is skipped for the mirror. It still works from the file | ISS.cpp ReadIniSection_
|
| HM5801 | Medium | A [Settings] value is longer than 4000 characters |
Same: skipped for the mirror, works from the file | ISS.cpp ReadIniSection_
|
| HM5802 | Medium |
hm_inisettings cannot be read at all |
The server runs on the file alone, and a backup that includes settings fails - "The server settings could not be written to the backup... No archive has been created" - until it is fixed | ISS.cpp Synchronize, before pass 1; BackupExecuter, Configuration::XMLStore
|
| HM5803 | Medium | (6.3.3) A database-side change could not be written back into the file | The change is not applied. The file's value is used for this run. The service account needs write access to hMailServer.ini | ISS.cpp Synchronize, pass 1 |
| HM5804 | Medium | (6.3.3) The file and the row both changed since they last agreed | The file wins and the row is overwritten. The database-side change is lost | ISS.cpp Synchronize, pass 1 |
| HM5805 | Medium | A restore could not write a setting into the file | The row is restored; the file is not | ISS.cpp XMLLoad, the restore path |
| HM5806 | Medium | (6.3.3) SetIniSetting wrote the file but could not read the mirror afterwards |
The file holds the new value; the row may not | ISS.cpp WriteSetting
|
Since 6.3.4 (section 4a), HM5800, HM5801, HM5802 and HM5805 mean what the table says, and three codes change meaning while two are new:
| Code | Severity | Raised when | What the server does then | Source |
|---|---|---|---|---|
| HM5803 | Medium | A stored value could not be written into a file the service may write - to put back an edited line, or to bring a stale one up to date | The stored value is used. hMailServer.exe /Register and anything else reading the file directly see the old line until a start can write it, and the report repeats at every start until then. A file the service may not write, NT SERVICE\hMailServer's, is not written at all, and that is said in the application log, not as an error (The Service Account) |
ISS.cpp Synchronize
|
| HM5804 | Medium | A [Settings] value in the file was changed away from the value the server last wrote there |
The stored value is used, the key is named, and the line is put back where the service may write the file; said once, because the two then agree again - or, where the file is read-only to the service (NT SERVICE\hMailServer), because the row then records the edited line, which stays in the file |
ISS.cpp Synchronize
|
| HM5806 | Medium | A write stored the row but could not write the file; or a removal took out the line but could not delete the row; or, where the service may not write the file, a change that only the file can hold was refused - PasswordPepper, the live update's trust set or removed without an administrator's session, or the removal of a key this build does not read while the file has its line |
Write: the value is stored and used, and the next start rewrites the line. Removal: the call fails, the setting is not back at its default, and the next start writes the line back. Refusal: nothing is changed, and the sentence names the file and the account the server runs as and says what an administrator does instead (IniFileSettings::InitializationFileRefusal) |
ISS.cpp WriteSetting, RemoveSetting
|
| HM5807 | Medium |
[SettingsOverride] holds one or more keys |
Those values are used over whatever is stored. Named at every start and reinitialise, for as long as the section is there | IFS.cpp LoadDatabaseSettings
|
| HM5808 | Medium | A value was stored for a key that [SettingsOverride] forces |
Stored, but not in effect until the override is removed | IFS.cpp WriteSettingsValue
|
- HM5113, although not from the ini layer, is the one an administrator meets next: it names an unreadable certificate file or a private key that does not match, and it is what precedes "RestApi: Refusing to start - the shared TLS configuration could not be applied" and the equivalent metrics-listener line (source:
SslContextInitializer::InitServer). - Nothing in this table is raised by a value that is merely unused. A misspelled key name produces no error at all — it is simply absent, and the default applies. That is the first thing to suspect when a setting appears to do nothing.
-
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". In 6.2.28 IniFileSettings read 238[Settings]keys (237 in LoadSettings + UseLanguage); in 6.3.3, and unchanged in 6.3.4, it reads 242 (section 9), and with the 4[SendingLimits]keys (section 10) and the 21[LDAP]keys (section 11), both read outside IniFileSettings, there are 267 keys outside [Database]/[Directories]/[Security]/[GUILanguages], plus the one-line-per-address[SendingLimitsOverrides].build/ini-settings-baseline.txtholds none of those 25, because it lists only what IniFileSettings reads. 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 - four decimal parts since 6.3.6, an older form such as8.8.8being reported asHM4401; 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).
From schema 6042 a new setting is a row of hm_settings, never a [Settings] key, so everything 6.3.4 made configurable is here rather than in section 9. 193 rows - the names in Common/Application/SettingsDefaults.h at the tag v6.3.4 (313 entries; 311 at 92b8f9f63, 284 at master 540347500) that neither 6.3.3's create script nor its code knew: 164 counted at 540347500, the 27 of storage compression, object storage, mailbox encryption at rest, PST import and the SNMP agent, the last five rows of the table, and the two the audit before the release added, PasskeyAllowedOrigins and WebPushLoopbackHttpForTests, in the passkeys and Web Push rows. Each is read and written over COM through Settings (Property, PropertySet, or a named member), over REST through a settings group row, and from the environment as HM_PROPERTY_<name>; each has its shipped default in SettingsDefaults.h, which is what the changed-from-stock list compares against; and each applies when it is saved, the reader asking for the value when it uses it. A default here is what a new installation's create script writes; an upgrade inserts a row only where one is missing, and in one case, noted, with a different value.
| Feature (schema step) | Rows, with their defaults | Document |
|---|---|---|
| Audit trail and alerts (6043) |
AuditTrailEnabled 1, AuditRetentionDays 0; AlertsEnabled 1, AlertRecipient '', AlertSenderAddress '', AlertDigestEnabled 1, AlertDigestHour 7, AlertMaxPerHour 20, AlertWebhookMaxAttempts 5 |
AlertsAndAuditTrail.md |
| Syslog (no step) |
SyslogEnabled 0, SyslogHost '', SyslogPort 514, SyslogTransport 0, SyslogFacility 2, SyslogMinimumSeverity 6, SyslogLogTypes 86 |
Syslog.md |
| Quarantine digests (6046) |
QuarantineDigestBaseUrl '', QuarantineDigestLinkHours 72 |
SenderListsAndDigests.md |
| Bounces and the suppression list (6049) |
BounceProcessingEnabled 0, BounceReadDeliveryReports 1, BounceHardBounceThreshold 1, BounceMailboxFullIsHard 0, BounceReportWindowDays 7, BounceCountRetentionDays 90, BounceSuppressionAction 0, BounceSuppressionExpiryDays 0 |
BounceSuppression.md |
| The password breach check (no step) |
BreachCheckEnabled 0, BreachCheckUrl https://api.pwnedpasswords.com/range/, BreachCheckMinimumCount 1, BreachCheckUnreachableAction 0 |
PasswordBreachCheck.md |
| The OpenPGP key store and Web Key Directory (6050) |
OpenPGPWebKeyDirectoryEnabled 0, OpenPGPKeyMaximumBytes 65536, OpenPGPKeysPerAccount 8, OpenPGPRequireAddressInUserId 1 |
OpenPGP.md |
| Webhooks (6051) |
WebhooksEnabled 1, WebhookMaxAttempts 5, WebhookMaxPerMinute 60, WebhookRetentionDays 7 |
Webhooks.md |
| Country blocking (6052) |
GeoIpDatabaseFile '', GeoIpDatabaseMaxAgeDays 45, CountryBlockingExemptRanges (loopback, the private and link-local ranges and 100.64.0.0/10), CountryPolicySMTP, CountryPolicyIMAP, CountryPolicyPOP3, CountryPolicyHTTP '' |
CountryBlocking.md |
| Sessions and devices (6053) |
SessionIdleSeconds 0, SessionLifetimeSeconds 0, SessionOnePerDevice 0, SessionBindToAddress 0 (0 is the built-in policy; all four have levels) |
DevicesAndSessions.md |
| Web Push (6054) |
WebPushEnabled 0, WebPushSubject '', WebPushTimeToLiveSeconds 86400, WebPushMaxPerAccount 10, WebPushMaxAttempts 5, WebPushVapidPublicKey '', WebPushVapidPrivateKey '' (write-only; the public key is derived from it), WebPushLoopbackHttpForTests '' (loopback_http_for_tests under /api/v1/settings/push: a switch for the regression suite, off, that lets a push go over plain http to this machine's loopback, from a request made on the machine itself) |
none of its own; Roadmap2.md
|
| Complaints (6055) |
ComplaintProcessingEnabled 0, ComplaintMailbox '', ComplaintRetentionDays 180, ComplaintSuppressionThreshold 0 |
Complaints.md |
| The reputation watch (6057) |
ReputationWatchEnabled 1 in a new installation and 0 after an upgrade, ReputationWatchLists (Spamhaus ZEN, SpamCop, PSBL, Mailspike), ReputationWatchAddresses '' |
ReputationWatch.md |
| Spam traps (6058) |
SpamTrapBanMinutes 1440, SpamTrapBlockDays 30, SpamTrapHitRetentionDays 90, SpamTrapTeachSpamAssassin 0 |
SpamTraps.md |
| The encrypted backup (6059) |
backupencryptionpassphrase (write-only; the number says whether one is set) |
Backup.md |
| Quota policy (6061) |
QuotaGracePercent 0, QuotaSendStopPercent 0 (both with levels) |
QuotaPolicy.md |
| Other accounts and the import sign-in (6065) |
ReaderFetchAccountsMax 3, ReaderFetchPrivateHosts 0; ImportOAuthRedirectBaseUrl '', ImportOAuthGoogleClientId '', ImportOAuthGoogleClientSecret '', ImportOAuthGoogleAuthorizeUrl and ImportOAuthGoogleTokenUrl (Google's public endpoints), ImportOAuthMsClientId '', ImportOAuthMsClientSecret '', ImportOAuthMsAuthorizeUrl and ImportOAuthMsTokenUrl (Microsoft's common endpoints) |
Migration.md |
| Send as, masked addresses (6066, 6067) |
ReaderSenderAddressesMax 5, ReaderMaskedAddressesMax 20 (both with levels) |
SendAs.md, MaskedAddresses.md |
| Brand logos (no step) |
BrandLogosEnabled 1 |
BrandLogos.md |
| Recover deleted items (6068) |
RecoverDeletedDays 0, off (with levels) |
RecoverDeleted.md |
| Messages that expire (6069) |
ConfidentialBaseUrl '' |
ConfidentialMessages.md |
| Offline mail (no step) |
ReaderOfflineDaysMax 30 (with levels) |
OfflineMail.md |
| Outbound content policy (no step) |
DlpAccountNumberPattern '' |
OutboundContentPolicy.md |
| The reports received (6070) |
DomainReportRetentionDays 365 |
DmarcReports.md |
| Compromised-account detection (6071) |
CompromiseDetectionMode 1 (watch; 0 off, 2 enforce), CompromiseBaselineDays 30, CompromiseMinimumHistory 20, CompromiseVolumeFloor 30, CompromiseVolumeFactor 4, CompromiseRecipientFloor 20, CompromiseNewRecipientShare 80, CompromiseScoreToLock 3, CompromiseHeldDays 7 |
CompromisedAccounts.md |
| Greylisting, hardened (6072) |
GreyListingSlideOnEarlyRetry 0, GreyListingAbuseThreshold 0, GreyListingAbuseLockMinutes 60 |
Greylisting.md |
| SMTP policy (6073) |
UseSPFSoftFailScore 0, UseSPFPermErrorScore 0, UseSPFTempErrorScore 0, SPFTempErrorDefer 0, RejectMultiRcptNullSender 0, BanMultiRcptNullSender 0, BanMultiRcptNullSenderHours 24 |
SmtpPolicy.md |
| The sample domain (no step) |
SampleDomainId 0 - the server's own record of the sample it made, set by nobody |
SampleDomain.md |
| Account recovery (6076) |
AccountRecoveryEnabled 1, RecoveryResetLinkMinutes 30, RecoveryVerifyLinkHours 24 |
AccountRecovery.md |
| Passkeys and OpenID Connect (6077) |
PasskeysEnabled 1, PasskeyRelyingParty '', PasskeyAllowedOrigins '' (passkey_allowed_origins: the origins this server's pages are served at, comma separated; required once a relying party is set, and while a relying party is set without it no passkey is registered or taken); OidcIssuer '', OidcClientId '', OidcClientSecret '' (write-only), OidcScopes openid email, OidcRedirectBaseUrl '', OidcAutoProvision 0, OidcAdministrators '' |
PasskeysAndSingleSignOn.md |
| The directory (6078) |
DirectoryEnabled 1, DirectoryUserEditable 1 (with levels) |
Directory.md |
| OpenPGP in the page (6080) |
OpenPGPPortalEnabled 1 (with levels), OpenPGPLookupWkd 1, OpenPGPLookupKeyserver 0, OpenPGPKeyserverUrl https://keys.openpgp.org, OpenPGPLookupTimeout 10 |
OpenPGP.md |
| Password-protected messages (6082) |
ProtectedMessageDays 30 |
OpenPGP.md |
| Zero-access mailboxes (6083) |
ZeroAccessAllowed 0 (with levels) |
ZeroAccess.md |
| Message recall (6085) |
RecallDays 14 (with levels) |
MessageRecall.md |
| Help (no step) |
HelpWikiBaseUrl https://gitlab.com/hmailserver/hmailserver/-/wikis/ - the group's path for a database set up on 6.3.6; a database set up before keeps the project's first path, which GitLab redirects (Help.md, The wiki link) |
Help.md |
| Throttling per destination and per tenant (no step) |
MaxOutboundConnsPerDestination 0 and MaxOutboundConnsPerTenant 0 (Settings.MaxOutboundConnectionsPerDestination and ...PerTenant over COM; a row name is at most thirty characters), OutboundBackoffMinutes 2, OutboundBackoffMaxMinutes 60 |
OutboundShaping.md |
| Migrations (6090) |
MigrationConnectionsPerHost 4 |
Migration.md |
| The attachment sandbox (6091) |
SandboxEnabled 0, SandboxSubmitUrl '', SandboxVerdictUrl '', SandboxToken '' (write-only), SandboxTypes (exe, dll, scr, the script types, msi, jar, lnk, iso, img, the Office types with and without macros, rtf, pdf, zip, 7z, rar), SandboxMaxSizeKB 20480, SandboxMaxFiles 10, SandboxPollSeconds 30, SandboxTimeoutMinutes 15, SandboxTimeoutAction 0, SandboxScoreThreshold 70, SandboxCacheHours 24, SandboxInternalMail 0 |
Sandbox.md |
| The list manager (6092) |
ListBaseUrl '', ListHardBouncesToUnsubscribe 1, ListSoftBouncesToUnsubscribe 5, ListBounceWindowDays 30 |
MailingLists.md |
| SAML sign-in (no step) |
SamlIdpEntityId '', SamlIdpSsoUrl '', SamlIdpCertificate '', SamlSpEntityId '', SamlEmailAttribute (the WS-Federation emailaddress claim), SamlRequireSignedAssertion 1, SamlAutoProvision 0, SamlAdministrators '', SamlButtonLabel Sign in with SAML
|
PasskeysAndSingleSignOn.md |
| The S/MIME authority (6095) |
SmimeAuthorityBaseUrl '' |
SmimeAuthority.md |
| Storage compression and attachment de-duplication (6098) |
MessageStoreCompression 0 (off), MessageStoreDedupMinKB 256 (0 to 65536; 0 keeps nothing once) - message_store_compression, message_store_dedup_min_kb over REST, Settings.MessageStoreCompression and MessageStoreDedupMinKB over COM; apply at once |
MessageStore.md |
| Object storage for the message store (no step) |
MessageStoreBackend file (or object), MessageStoreObjectService '' (none; s3 or azure), MessageStoreObjectEndpoint '', MessageStoreObjectRegion us-east-1, MessageStoreObjectBucket '', MessageStoreObjectAddressing auto (path, virtual), MessageStoreObjectPrefix '', MessageStoreObjectAccessKey '', MessageStoreObjectSecret '' and MessageStoreObjectSasToken '' (both write-only, stored protected), MessageStoreObjectCaFile '', MessageStoreObjectCacheMB 32 - the message_store_* keys of /api/v1/settings, no COM property; taken within five seconds. Off until an administrator names a store: a server with file and no service reads and writes as before |
ObjectStorage.md |
| Mailbox encryption at rest (6099) |
MailboxKeyIdleMinutes 15 (1 to 1440) - mailbox_key_idle_minutes over REST; encryption itself is switched on per account, and is off on every account |
EncryptionAtRest.md |
| PST import (no step) |
PstImportDirectory '' - pst_import_directory over REST. Empty as shipped, which takes no PST job; an absolute local path under which a job names <directory>/<domain>/<file>.pst
|
Migration.md |
| The SNMP agent (no step) |
SnmpEnabled 0, SnmpPort 161, SnmpBindAddress '', SnmpAllowedAddresses '' (answers nobody), SnmpV3User '', SnmpV3AuthPassword and SnmpV3PrivPassword '' (write-only), SnmpV2cEnabled 0, SnmpV2cCommunity '' (write-only), SnmpContact '', SnmpLocation '' - the snmp_* keys of /api/v1/settings, applied at a restart or a reinitialise, no COM property, and since 6.3.6 the Control Panel's SNMP tab under Monitoring & troubleshooting > Logging, which reads and writes them over the REST API (Ports Reference 4.6) |
Snmp.md |
| 6.3.6: the auto-ban widened by reverse name (no step) |
AutoBanWidenByReverseName 0 (server level) - widen an auto-ban, in the background, to the contiguous run of addresses around it inside its IPv4 /24 whose reverse names share its registrable domain, forward-confirmed, at most 32 lookups each way; never across a never-ban entry or an administrator's IP range, never IPv6 or a private network. COM Settings.AutoBanWidenByReverseName, REST auto_ban_widen_by_reverse_name on /api/v1/settings, Control Panel Settings > Auto-ban (work item #55) |
AutoBan.md |
| 6.3.6: the never-ban list by reverse name (6108) | the table hm_neverbannames (id, pattern, note, created), empty as shipped: patterns in the domain list's form - mail.example.net that name, .example.net it and every name beneath - a public suffix, an address, a network or * refused; an address is spared from all three auto-bans when one of its reverse names is covered and resolves back to it. COM Settings.AutoBanNeverBanNames, AddAutoBanNeverBanName, RemoveAutoBanNeverBanName, CheckAutoBanNeverBanName; REST /api/v1/never-ban-names (APIs Reference); Control Panel Settings > Auto-ban (work item #56) |
AutoBan.md |
| 6.3.6: host names that are not fully qualified (no step) |
RefuseHeloNotFqdn 0 - 550 5.7.1 at MAIL FROM to a session whose HELO/EHLO name is not a fully qualified domain name (an address, one label, a public suffix); never for an authenticated session, an IP range without spam tests, a white-listed sender or an incoming relay (work item #51). RefuseMailFromDomainNotFqdn 0 - the same for the envelope sender's domain; MAIL FROM:<> is not judged. Both server level; COM AntiSpam.RefuseHeloNotFqdn and RefuseMailFromDomainNotFqdn, REST refuse_helo_not_fqdn and refuse_mail_from_domain_not_fqdn on /api/v1/settings/antispam, Control Panel Anti-spam > Host checks
|
SmtpPolicy.md |
| 6.3.6: the URI lists' domain bound (no step) |
SurblTooManyDomainsScore 5 (server level) - scored once, however many URI lists are active, by a message whose links name more than 200 registrable domains (only 200 of them are asked about); 5, the default, is the stock mark threshold; 0 scores nothing, and nothing is scored while no URI list is active. COM AntiSpam.SurblTooManyDomainsScore, REST surbl_too_many_domains_score on /api/v1/settings/antispam, the Control Panel's Anti-spam page (General), the Deck. A database gets its row at the first start (item 30 of 6.3.6's "What changes on upgrade") |
Stopping Spam §10.6 |
| 6.3.6: each SMTP listener's AUTH (6108) |
hm_tcpipports.portsmtpauth: 0 On, 1 Off, 2 Honeypot, as COM TCPIPPort.SMTPAuthentication and REST smtp_authentication on /api/v1/ports; only On on a POP3 or IMAP port (Ports Reference) |
SmtpAuthOnDeliveryPorts.md |
The documents are in hmailserver/docs. Seven more rows are the server's own records, written by it and set by nobody, and are not in SettingsDefaults.h: EnvironmentManagedSettings and EnvironmentSecretSettings (what the environment manages, and which of those a file supplied), ProfileSnapshot and ProfileApplied (a profile's way back, and the last choice), StoredSecretsKeyId and StoredSecretsKeyExported (the stored-secrets key's id, never the key, and whether it has been exported - SecretsAtRest.md) and NewInstallDefaultsPending (the mark a database the Linux create step made carries until the unit's first start gives it the secure defaults - SecureDefaults.md). The send API's per-key sender and ceilings are not settings: they are fields of the key in hMailServerApiKeys.ini (section 12).
hMailServer 6.3.7, the latest release (17 October 2026, schema 6108) · AGPL-3.0-or-later · Repository · Mirror on GitHub · Report a documentation error (a GitLab issue naming the page)
Home — full index
Start here
1. Install and run
- Before You Install
- Installing hMailServer
- Installing on Linux
- Running in a Container
- Profiles and Environment
- The Control Panel
- The Control Deck
- Your First Domain and Mailbox
- Connecting a Mail Client
- The Webmail
- DNS for Your Domain
- Migration
2. Secure it
- Encryption and Certificates
- Database TLS
- Encryption for People
- Stopping Spam
- Stopping Viruses
- Security Hardening
- Secure Defaults
- The Service Account
- Secrets at Rest
- Sign-in and Identity
- Security Policy
3. Operate it
- Monitoring and Health
- The SNMP Agent
- Backup and Restore
- Message Storage
- Compliance and eDiscovery
- Troubleshooting
- Diagnosing Stalled Mail
- Relocating an Installation
- Upgrading hMailServer
- Upgrading Guide
- The Update Feed
- Migrating the Database Backend
- High Availability Runbook
- Warm Standby
- Runbooks Digest
4. Extend it
- Rules and Sieve
- Aliases Lists and Public Folders
- Rooms and Equipment
- Mailing Lists
- Routes and Relays
- Mail In to a Webhook
- Outbound Shaping
- The COM API and Scripting
- Event Scripts
- The REST API
- Command Line
- APIs Reference
5. Contribute to it
- Project Handbook
- Architecture
- Contributing
- Release Process
- Governance
- Assurance Case
- Regression Test Environment
- Fuzzing
- Regulatory Scope
- Third-Party Binaries
Look it up — from any journey
- Settings Reference
- Settings Overview
- Capabilities and Configuration
- Control Panel Reference
- Control Panel Pages
- Ports Reference
- Changes-Since-6210
- Version-History-to-6210
- Glossary
About this wiki
- hMailServer 6.3.7 is the latest release (17 October 2026, schema 6108)
- Pages say since 6.3.4 where a feature is new in it
- AGPL-3.0-or-later
- Repository, the one place the project is worked on
- Its mirror on GitHub, since 18 October 2026, with no Issues
- Questions: a GitLab issue with the Question template, GitHub Discussions Q&A, or the forum
- Report a documentation error: a GitLab issue naming the page