Skip to content

Capabilities and Configuration

Christopher Holloway edited this page Oct 26, 2026 · 31 revisions

Capabilities and Configuration

The capability inventory as the server advertises it today (every SMTP, IMAP, POP3 and ManageSieve keyword), transport security, sender authentication, operations, and the configuration reference checked key by key against the code. File and line references point into the repository at the commit this page was written from (6.2.24, master of 4 September 2026); the passages marked 6.2.25, 6.2.27 or "new in 6.2.28" were re-checked against master 40ae9491d on 8 September 2026 and their line numbers are from that tree; the only change after it is the HttpProxy line at IniFileSettings.cpp:492 in the tree 6.2.28 was cut from, which moves what follows it down by one. Sections headed Unconfirmed or Contradictions record what could not be verified or where documents disagreed, and are left in on purpose. The figures and the passages on what came after 6.3.3 were read again against master 540347500 on 24 September 2026 (schema 6096), and against 92b8f9f63 (schema 6103) on 26 September 2026; the release is the tag v6.3.4 (51e2dbda0, schema 6104), which adds a last batch to that tree, and the schema, the IMAP capabilities and the 6.3.4 figures below that it changes are read from the tag. What this page marks 6.3.4 is in that release, and 6.3.3 did not have it. Brought up on 5 October 2026 to the tag v6.3.6 (e3c448aa6; 6 October 2026, schema 6108), for what 6.3.6 changes on this page: the version and schema stamps, the libraries, QuickJS, the live update's trust and states, HttpProxy's callers and the auto-ban's never-ban names. 6.3.7, the latest release (17 October 2026, schema 6108), is not yet reflected here, but for the version stamp, the published releases and the schema each release requires (section 1), and the releases the release jobs have made (the quality gates).

Scope: README.md (581 lines) at repo root, checked line by line against the server source under hmailserver/source/Server, the installer under hmailserver/installation, the DB scripts, the Tools solutions and .github/workflows. All paths below are relative to c:/Users/chris/Documents/projects/hmailserver/; Server/ means hmailserver/source/Server/. "README:N" is a README line number.

Method note for page writers: [Settings] keys are read through IniFileSettings::ReadIniSettingString_/Integer_, which first consults a database-reconciled overlay and only then hMailServer.INI; every OTHER section ([Database], [Directories], [Security], [LDAP], [GUILanguages]) is read from the file only (source: Server/Common/Application/IniFileSettings.cpp:803-833). The [LDAP] section has its own reader (Server/Common/LDAP/LdapSettings.cpp:17,357,368). Since 6.3.4 (schema 6042), that overlay is the store: the [Settings] keys (241 in the baseline) are read from and written to hm_inisettings, the section in the file is a copy the server keeps current where it may write the file, a key with no stored row is adopted from the file, an edit to a stored key is reported as HM5804 and the stored value used, the line put back where the service may write the file (under NT SERVICE\hMailServer, to which the file is read-only, the edit stays in the file and the copy is not refreshed - The Service Account), a [SettingsOverride] section is applied over the store at every start and reported as HM5807, PasswordPepper stays in the file only and out of the backup, and values are stored trimmed (source: Server/Common/Application/IniSettingStore.h, IniSettingStore.cpp Synchronize, IniFileSettings.cpp:942,1015). build/check-ini-coverage.py refuses a new key under [Settings] against build/ini-settings-baseline.txt (263 keys across five sections when the rule was made, 238 of them in [Settings]; 270 in 6.3.4, 241 in [Settings], all with an editor, and 29 in the bootstrap sections, among them [Database] TlsMode, TlsCaFile and [Security] StoredSecretsKey) - a new setting belongs in hm_settings.

Reading this page: what "GAP", "CONFIRMED" and "UNCONFIRMED" mean

The annotations throughout are not editorial noise; they are the finding of a line-by-line comparison between the README's claims and the code, and each one tells you which document to trust.

Marker Meaning What to do with it
CONFIRMED The README says it and the code does it Nothing. Cite either
GAP The code does it and the README does not say so, or a setting exists with no README entry Trust the code and the Settings Reference. This is the largest category, and most of it is settings added faster than the README was updated
UNCONFIRMED A claim that cannot be checked from the source tree — usually a statement about testing against external infrastructure Treat as a claim, not a fact
Contradiction Two documents disagree, or a document disagrees with the code The code wins. Section 13 lists them

The reason this page exists in this shape is a specific failure recorded in the project's own history: the August 2026 audit found fifteen overclaims, two of which had already shipped in release notes. The failure shape was always the same — a capability that exists in the code but is inert by default. So this page records what is advertised and enabled, not what is compiled in.

1. Identity, version, schema

  • Product: hMailServer, SMTP/IMAP/POP3 server for Windows, maintained fork by Christopher Holloway / Progressive Robot Ltd (source: README:7-9)
  • Version stamp is 6.3.7, build 46 (source at the tag v6.3.7, 31d656c0c: Server/Common/Application/Version.h:6-8 HMAILSERVER_VERSION "6.3.7", HMAILSERVER_VERSION_NUMERIC 6,3,7,46, HMAILSERVER_BUILD 46); 6.3.6 was build 45 (read at the tag v6.3.6, e3c448aa6), 6.3.5 build 44 (read at v6.3.5), 6.3.4 build 43 (HMAILSERVER_VERSION_NUMERIC 6,3,4,43 at 92b8f9f63; hmailserver/installation/section_setup_64.iss:2,6,10 OutputBaseFilename=hMailServer-6.3.4-x64, AppVersion=6.3.4, VersionInfoVersion=6.3.4.0) and 6.3.3 build 42. Published releases since this page was first written: v6.2.25 (6 Sep 2026), v6.2.26 (6 Sep 2026), v6.2.27 (7 Sep 2026), v6.2.28 (8 Sep 2026), v6.3.0 (10 Sep 2026, the first with Linux packages), v6.3.1 (11 Sep 2026), v6.3.2 (13 Sep 2026), v6.3.3 (15 Sep 2026), v6.3.4 (27 Sep 2026), v6.3.5 (29 Sep 2026), v6.3.6 (6 Oct 2026), v6.3.7 (17 Oct 2026, latest). 6.3.4 is Authenticode-signed and carries no Sigstore bundle, so it is installed by hand; its Linux packages are x86-64 only (hmailserver_6.3.4_amd64.deb, hmailserver-6.3.4-1.x86_64.rpm), with no AArch64 package and no AppImage. Work that reached a published release for the first time in v6.2.28 is marked "new in 6.2.28" where it appears below, and work first in 6.3.4 is marked 6.3.4.
  • Required database schema version was 6031 when this page was first written, at 6.2.27; 6.3.3 requires 6040, 6.3.4 6104, 6.3.5 6105, and 6.3.6 and 6.3.7 6108 (next bullet but one) (source at 6.2.27: Server/Common/Application/Constants.h:173 #define REQUIRED_DB_VERSION 6031; DBScripts/CreateTablesMSSQL.sql:1213 insert into hm_dbversion values (6031); last DBUpdater step new UpgradeScript(6030, 6031), Tools/DBUpdater/formMain.cs:242). README:110 says "Database version 6027" — STALE (6.2.25 took the schema to 6030, 6.2.27 to 6031).
  • Schema history 6006-6010 documented in the same header: 6006 folderspecialuse; 6007 secondary DKIM selector/key; 6008 per-port client-certificate policy; 6009 encrypted private-key passphrase column; 6010 seeds ASArcFilteringEnabled (0) and ASArcTrustedSealers ('') (source: Constants.h:157-175). Later bumps (6013 SAVEDATE column, 6014 hm_imap_metadata) are referenced from IMAPCommandCapability.cpp:142-149.
  • 6.3.6 requires schema 6108 (source at the tag v6.3.6, e3c448aa6: Server/Common/Application/Constants.h:1276 #define REQUIRED_DB_VERSION 6108; the three steps after 6.3.5's 6105 are Upgrade6105to6106*, Upgrade6106to6107* and Upgrade6107to6108* - the external accounts' days to keep translated, the greylisting key finished, hm_neverbannames and hm_tcpipports.portsmtpauth); 6.3.5 required 6105 (hm_quarantine.quarantinehelo) and 6.3.4 6104 (source at the tag v6.3.4, 51e2dbda0: Constants.h:1224 #define REQUIRED_DB_VERSION 6104; the three CreateTables*.sql scripts insert 6104; DBUpdater's step new UpgradeScript(6103, 6104), Tools/DBUpdater/formMain.cs:314). 6.3.3 is at 6040, and sixty-four steps follow it to 6104; Changes-Since-6210 section 2 has each. The last eight, after master 540347500's 6096: 6097 the four eDiscovery tables, 6098 hm_storeblobs and the two compression settings, 6099 hm_accountkeys and hm_domainrecovery, 6100 hm_inboundhooks and hm_inboundhookqueue, 6101 hm_accounts.accountkind, the four booking tables and two columns on hm_messages, 6102 every MySQL and MariaDB table converted to utf8mb4 (the other backends move only the version), 6103 accounttotpsecret widened to 1024, 6104 hm_fetchaccounts.faorigin - who set a fetch account's server and user name, so that one a person made is fetched only from a public address (unless ReaderFetchPrivateHosts is on) and one made without the server's authority is never given the server's own OAuth2 token. The schema was 6045 at 3233b0cc4, and the first five steps after 6.3.3 were: 6041 hm_calendars and hm_calendarobjects for CalDAV; 6042 no DDL - the version itself is the fence that keeps an older server off a database whose settings store is the database; 6043 hm_audit, hm_alertrules and hm_alertevents with the six shipped rules, and nine hm_settings rows each inserted only when no row of that name exists; 6044 seven hm_domains columns for the external-sender tag, the first-contact note and the disclaimer, and hm_knownsenders; 6045 hm_remotedomainpolicies (source: DBScripts/Upgrade6040to6041MSSQL.sql through Upgrade6044to6045MSSQL.sql, with MSSQLCE, MySQL and PGSQL variants). build/check-upgrade-inserts.py (run by style.yml) refuses a setting row inserted with plain VALUES in any upgrade step from 6030 on.
  • Later bumps still: 6026 seeds hm_settings row CreateDefaultSpecialUseFolders (0); 6027 adds hm_domains.domainmessageretentiondays and hm_accounts.accountmessageretentiondays (0 = no policy); 6028 creates hm_metricsamples; 6029 creates hm_archiveindex; 6030 adds seventeen FOREIGN KEY ... ON DELETE CASCADE constraints after deleting the orphan rows they would refuse (all five shipped in 6.2.25); 6031 adds hm_fetchaccounts.famirrorfolders (6.2.27) (source: DBScripts/Upgrade6025to6026MSSQL.sql through Upgrade6030to6031MSSQL.sql, with MSSQLCE, MySQL and PGSQL variants).
  • Installer is a single x64 Inno Setup exe; ArchitecturesAllowed=x64, ArchitecturesInstallIn64BitMode=x64 (source: section_setup_64.iss:4-5)
  • License AGPL-3.0-or-later (source: SPDX header on every source file, e.g. Server/IMAP/IMAPCommandCapability.cpp:4; LICENSE exists at repo root)

2. Mail protocols — what is actually advertised

In 6.3.4 the SMTP and POP3 sets below are 6.3.3's, unchanged, and IMAP's names the same capabilities with two conditions added (checked at master 540347500, at 92b8f9f63 and at the tag v6.3.4): SendEHLOKeywords_ emits the same keywords and POP3's ProtocolCAPA_ answers the same list; IMAPCommandCapability.cpp now offers STARTTLS only before TLS is active and before the client has signed in, and COMPRESS=DEFLATE only once it has signed in, since those are when the commands are taken (the audit before the release; CHANGELOG 6.3.4). What changed is how IMAP answers, where Dovecot's imaptest found it did not follow the RFCs: a command pipelined behind an APPEND with a non-synchronising literal is answered (mbsync copied one message and stopped); APPEND stores the literal byte for byte, so RFC822.SIZE of a newly appended message is two octets smaller than before; a mailbox that does not exist, or a right the account lacks, is NO rather than BAD, and a failed SELECT or EXAMINE leaves no mailbox selected; an unknown LIST selection or return option is BAD; every BODY[<section>] without .PEEK sets \Seen; SORT uses every criterion and THREAD builds the RFC's trees; and the first session told of new messages counts them \Recent (commit 90df29ac5; Changes-Since-6210 section 5). A mailbox encrypted at rest is signed in by IMAP and POP3 only with its own password, and a message only an unreachable object store holds is answered IMAP NO [UNAVAILABLE] and POP3 -ERR [SYS/TEMP]. What 6.3.4 added to the SMTP conversation is refusals rather than capabilities - the SMTP policy's null-sender and SPF TempError answers, the domain black list at MAIL FROM, greylisting's abuse lock, restricted delivery, a mail flow rule's reject at the end of DATA, a spam trap's unknown-user answer - and they are in Stopping Spam and Changes-Since-6210 section 4e.

SMTP EHLO keywords (source: Server/SMTP/SMTPConnection.cpp:2445-2557 SendEHLOKeywords_)

Unconditional, in this order:

  • SIZE <n> (n = MaxMessageSize KB x 1024; bare SIZE when 0) (2453-2461)
  • 8BITMIME (2464)
  • PIPELINING (2469)
  • CHUNKING — BDAT, RFC 3030 (2473)
  • BINARYMIME — RFC 3030; local delivery only: a recipient needing onward relay is refused at RCPT TO with 554 5.6.3 (2474-2481, 1322); DATA is refused with BODY=BINARYMIME (3123). README:34 does NOT list BINARYMIME — GAP.
  • SMTPUTF8 (2484)
  • ENHANCEDSTATUSCODES (2487)
  • DSN — RET/ENVID on MAIL, NOTIFY/ORCPT on RCPT, NOTIFY=NEVER honoured (2489-2491)
  • HELP as the terminating line (2555)

Conditional:

  • XCLIENT ADDR NAME PORT PROTO HELO LOGIN only when XClientPermitted_() (real TCP peer is in SMTPXClientTrustedIPs); untrusted peers never see it and get 550 if they try (2493-2499)
  • STARTTLS on a non-TLS connection when the port is STARTTLS-optional or -required (2501-2507)
  • AUTH LOGIN [PLAIN] SCRAM-SHA-256 [SCRAM-SHA-256-PLUS] [XOAUTH2 OAUTHBEARER] only when AUTH is enabled and (TLS, or cleartext auth is not refused). PLAIN requires the SMTP "allow plain text" setting; SCRAM-SHA-256 is always offered when AUTH is; -PLUS only on TLS; XOAUTH2/OAUTHBEARER only when OAuth2Enabled=1 and (OAuth2RequireTLS=0 or TLS) (2525-2553); EXTERNAL only when the connection's client certificate verified against the port's CA and names an address (shipped in 6.2.25)
  • AUTH is withheld entirely on cleartext when the port is STARTTLS-required OR the IP range has RequireTLSForAuth (2525-2530)
  • AUTH and the SASL continuation lines that follow it accept up to 12288 octets (RFC 5034); every other command line keeps 510 (source: SMTPConnection.cpp:476-483 maxLength = saslLine ? 12288 : 510; POP3Connection.cpp:295-299 for POP3, 500 otherwise) — 6.2.25
  • MaxSubmissionsPerIPPerMinute exceeded -> 421 Too many messages from your IP address... (source: SMTPConnection.cpp:736)
  • Acceptance deadline exceeded after end-of-data -> temporary 451 and the message is not accepted (source: SMTPConnection.cpp:2020); FinalizationTimeout default 240 s is the deadline (IniFileSettings.cpp:281)
  • Legacy HELO sessions get plain non-enhanced replies (README:495; SMTPConnection.cpp:1227 comment) — consistent
  • HAProxy PROXY protocol v1/v2 accepted only when SMTPProxyProtocolEnabled=1 and the peer is in SMTPProxyProtocolTrustedIPs (README:395-404; IniFileSettings.cpp:620-621). PROXY parsing code itself not read for this fact base — the setting names and defaults are confirmed.

IMAP CAPABILITY (source: Server/IMAP/IMAPCommandCapability.cpp:24-183)

Always: IMAP4 IMAP4rev1 IMAP4rev2 CHILDREN (24), THREAD=ORDEREDSUBJECT THREAD=REFERENCES (42), NAMESPACE RIGHTS=texk MOVE ID SPECIAL-USE CREATE-SPECIAL-USE UNSELECT UIDPLUS ENABLE STATUS=SIZE ESEARCH CONDSTORE QRESYNC LIST-EXTENDED SEARCHRES WITHIN UTF8=ACCEPT (114), LIST-STATUS (118), PREVIEW (122), MULTIAPPEND (126), REPLACE (130), SAVEDATE (135), METADATA (139), OBJECTID (144), BINARY (148), LITERAL- (163), APPENDLIMIT (bare before auth, APPENDLIMIT=<bytes> after) (170-181)

Gated by database settings (hm_settings rows, all seeded 1 = on): IDLE (enableimapidle), QUOTA QUOTA=RES-STORAGE (enableimapquota), SORT (enableimapsort), ACL (enableimapacl) (source: IMAPCommandCapability.cpp:28-46; hmailserver/source/DBScripts/CreateTablesMSSQL.sql:910-918)

Connection-dependent: STARTTLS (49-51); LOGINDISABLED when auth is refused on cleartext (76-77); AUTH=PLAIN, AUTH=SCRAM-SHA-256 when EnableImapSASLPlain is on and auth is available (79-91); AUTH=SCRAM-SHA-256-PLUS only on TLS (96-97); AUTH=XOAUTH2 AUTH=OAUTHBEARER when OAuth2 enabled (101-103); AUTH=EXTERNAL when auth is available and the client certificate verified against the port's CA and names an address (shipped in 6.2.25); SASL-IR when EnableImapSASLInitialResponse (105-106); UNAUTHENTICATE only once authenticated (155-156); COMPRESS=DEFLATE (RFC 4978) when IMAPCompressionEnabled=1 (the default) and the session is not yet compressed, withdrawn once COMPRESS is active — both directions of the session compressed on request (source: IMAPCommandCapability.cpp:61-62; IniFileSettings.cpp:497; Server/IMAP/IMAPCommandCompress.cpp; zlib 1.3.1 vendored under Server/zlib). New in 6.2.28.

  • EnableImapSASLPlain and EnableImapSASLInitialResponse are hm_settings rows seeded 0 (source: CreateTablesMSSQL.sql:1037-1039). This is the "one setting whose shipped default is off" in README:66 — CONFIRMED. Note it gates SCRAM on IMAP too, not only PLAIN (IMAPCommandCapability.cpp:79-97).
  • ENABLE IMAP4rev2 switches the session to rev2 semantics and implies UTF8=ACCEPT; ENABLE UTF8=ACCEPT alone is also honoured (source: Server/IMAP/StaticIMAPCommandHandlers.cpp:203-218)
  • rev2 deltas implemented: RECENT/\Recent removed, [UNSEEN] removed on SELECT/EXAMINE, ESEARCH-by-default SEARCH, STATUS RECENT removed (source: IMAPCommandSelect.cpp:134,148; IMAPCommandExamine.cpp:121,135; IMAPCommandSearch.cpp:344; IMAPCommandStatus.cpp:105)
  • README:35 now lists LIST-STATUS, non-synchronising literals and BINARY as implemented, matching the code (LIST-STATUS:141, BINARY:171, LITERAL-:187 in the current tree). LITERAL- rather than LITERAL+ is deliberate (4096-byte non-sync cap) (181-187). The earlier contradiction is resolved.
  • README:35 also omits the following advertised capabilities: CHILDREN, RIGHTS=texk, UNSELECT, ENABLE, STATUS=SIZE, LIST-EXTENDED, WITHIN, UTF8=ACCEPT, PREVIEW, MULTIAPPEND, REPLACE, SAVEDATE, METADATA, OBJECTID, UNAUTHENTICATE, APPENDLIMIT, QUOTA=RES-STORAGE, LOGINDISABLED, SASL-IR — GAP.
  • IMAP SEARCH bounded by IMAPSearchTimeout=60 s and IMAPSearchMaxMegabytes=2048 (source: IniFileSettings.cpp:347-349)
  • Expunge history for QRESYNC/CONDSTORE pruned to IMAPExpungeRetentionRecords=5000, task runs every 12 x 60 minutes (source: IniFileSettings.cpp:646; Server/Common/Application/Application.cpp:679 SetMinutesBetweenRun(12 * 60))

POP3 CAPA (source: Server/POP3/POP3Connection.cpp:438-536 ProtocolCAPA_)

  • Always: UIDL, TOP (440), RESP-CODES (485), AUTH-RESP-CODE (493), IMPLEMENTATION hMailServer (499), PIPELINING (508), UTF8 (511), EXPIRE NEVER (520)
  • Conditional: SASL PLAIN SCRAM-SHA-256 SCRAM-SHA-256-PLUS on TLS, SASL PLAIN SCRAM-SHA-256 on cleartext when permitted (461-463), plus EXTERNAL when the client certificate verified against the port's CA and names an address (shipped in 6.2.25); STLS when applicable (480); LOGIN-DELAY <n> when Pop3LoginDelaySeconds > 0 (532)
  • Pop3LoginDelaySeconds=0 default (source: IniFileSettings.cpp:619) — not in README (GAP)
  • External account fetching exists over POP3 and, since 6.2.25, IMAP: FetchAccount.ServerType 0 = POP3, 1 = IMAP (the INBOX collected once by UID, LOGIN or XOAUTH2, STARTTLS or TLS; left on the server unless DaysToKeepMessages says otherwise); FetchAccount.MirrorFolders (6.2.27, schema 6031) makes an IMAP account collect every remote folder into a local folder of the same name, byte for byte with flags and internal date, filed rather than delivered — the imapsync-shaped migration route. MaxNumberOfExternalFetchThreads=15 (source: hMailServer.idl:1854,1889; Server/Common/BO/FetchAccount.h:21-24; Server/ExternalFetcher/IMAPClientConnection.h:32, ExternalFetchClientBase.h:27; DBScripts/Upgrade6030to6031MSSQL.sql; IniFileSettings.cpp:175) — README:36 now says the same, CONFIRMED.

ManageSieve (source: Server/Common/Sieve/ManageSieveServer.cpp)

  • Commands handled: LOGOUT, CAPABILITY, NOOP, STARTTLS, AUTHENTICATE, UNAUTHENTICATE, LISTSCRIPTS, PUTSCRIPT, GETSCRIPT, SETACTIVE, DELETESCRIPT, RENAMESCRIPT, CHECKSCRIPT, HAVESPACE (1164-1489)
  • Capability response: "IMPLEMENTATION" "hMailServer ManageSieve", "SIEVE" "<extensions>", "STARTTLS" only pre-handshake with a certificate, "SASL" "PLAIN" (or "SASL" "" when cleartext auth is refused), "UNAUTHENTICATE", "VERSION" "1.0" (909-946)
  • Only SASL mechanism on ManageSieve is PLAIN (934) — no SCRAM there.
  • README:468 lists the commands but omits RENAMESCRIPT, UNAUTHENTICATE, NOOP, LOGOUT — GAP.
  • Listener off by default: ManageSieveServerPort=0, ManageSieveServerBindAddress=127.0.0.1 (source: IniFileSettings.cpp:453-454); standard port 4190 is README's statement (README:461), not enforced by code.

What decides each conditional EHLO keyword

The unconditional keywords are the same on every connection. The conditional ones are the interesting part, because a keyword that is absent is the visible symptom of a configuration decision made somewhere else entirely — and "my client will not authenticate" is nearly always this diagram rather than a bug.

flowchart TD
    CONN["A connection reaches EHLO"] --> UNCOND["Always advertised:<br/>SIZE, 8BITMIME, PIPELINING, CHUNKING,<br/>BINARYMIME, SMTPUTF8, ENHANCEDSTATUSCODES, DSN<br/>- and HELP as the terminating line"]
    UNCOND --> X{"Is the real TCP peer in<br/>SMTPXClientTrustedIPs?"}
    X -- yes --> XADV["XCLIENT ADDR NAME PORT PROTO HELO LOGIN"]
    X -- no --> XNO["No XCLIENT line at all.<br/>An untrusted peer that tries it gets 550"]
    XADV --> T
    XNO --> T{"Already on TLS?"}
    T -- yes --> TNO["No STARTTLS - it would be meaningless"]
    T -- no --> T2{"Is the port STARTTLS-optional<br/>or STARTTLS-required?"}
    T2 -- yes --> TADV["STARTTLS"]
    T2 -- "no, plain port" --> TNO
    TADV --> A
    TNO --> A{"Is SMTP AUTH enabled?"}
    A -- no --> ANONE["No AUTH line"]
    A -- yes --> A2{"On cleartext, is the port<br/>STARTTLS-required, or does the<br/>IP range set RequireTLSForAuth?"}
    A2 -- yes --> ANONE2["AUTH withheld ENTIRELY on cleartext.<br/>This is the one that looks like a bug<br/>and is not"]
    A2 -- no --> LIST["AUTH LOGIN<br/>+ PLAIN if the SMTP 'allow plain text'<br/>setting is on<br/>+ SCRAM-SHA-256, always when AUTH is<br/>+ SCRAM-SHA-256-PLUS only on TLS<br/>+ XOAUTH2 OAUTHBEARER when OAuth2Enabled=1<br/>and (OAuth2RequireTLS=0 or TLS)<br/>+ EXTERNAL when the client certificate verified<br/>against the port's CA and names an address"]
Loading

Two line-length facts that belong beside that diagram, because they are the cause of a failure that looks like a truncation bug: AUTH and the SASL continuation lines that follow it accept up to 12,288 octets (RFC 5034); every other SMTP command line keeps 510, and POP3 keeps 500. A long base64 token is therefore fine on the AUTH line and refused anywhere else.

The same question for the other three protocols

Listener Advertised unconditionally The conditional part, and what gates it
IMAP IMAP4 IMAP4rev1 IMAP4rev2 CHILDREN, both THREAD= variants, NAMESPACE RIGHTS=texk MOVE ID SPECIAL-USE CREATE-SPECIAL-USE UNSELECT UIDPLUS ENABLE STATUS=SIZE ESEARCH CONDSTORE QRESYNC LIST-EXTENDED SEARCHRES WITHIN UTF8=ACCEPT LIST-STATUS PREVIEW MULTIAPPEND REPLACE SAVEDATE METADATA OBJECTID BINARY LITERAL- APPENDLIMIT IDLE, QUOTA, SORT and ACL are database settings (enableimapidle, enableimapquota, enableimapsort, enableimapacl), all seeded on. AUTH=PLAIN and AUTH=SCRAM-SHA-256 need EnableImapSASLPlain, which is seeded off — and note it gates SCRAM too, not only PLAIN. SASL-IR needs EnableImapSASLInitialResponse, also seeded off. COMPRESS=DEFLATE appears while IMAPCompressionEnabled=1 and the session is not yet compressed, and is withdrawn once it is
POP3 UIDL TOP RESP-CODES AUTH-RESP-CODE IMPLEMENTATION PIPELINING UTF8, EXPIRE NEVER SASL mechanisms depend on TLS; STLS on the port's security; LOGIN-DELAY <n> only when Pop3LoginDelaySeconds > 0
ManageSieve IMPLEMENTATION, SIEVE, VERSION "1.0", UNAUTHENTICATE STARTTLS only pre-handshake and only with a certificate; SASL "PLAIN" — PLAIN is the only mechanism here, there is no SCRAM on ManageSieve. The whole listener is off until ManageSieveServerPort is non-zero

EnableImapSASLPlain seeded 0 is the one shipped default that is off where a reader might expect on. It is deliberate, and it is why an IMAP client configured for "encrypted password" can fail against a stock install until you turn it on.

3. Transport security

  • TLS versions are a bitmask SslVersions in hm_settings: TLS1.0=2, 1.1=4, 1.2=8, 1.3=16; seeded value 24 = TLS 1.2 + 1.3 (source: Server/Common/TCPIP/SocketConstants.h:69-75; CreateTablesMSSQL.sql:1033) — README:42 "TLS 1.2 and 1.3 by default" CONFIRMED.
  • TlsKeyExchangeGroups default X25519MLKEM768:SecP256r1MLKEM768:X25519:secp384r1:secp256r1 (source: IniFileSettings.cpp:572) — hybrid PQ first, CONFIRMED.
  • TlsCipherSuites13 default empty = OpenSSL defaults; TLS<=1.2 list is the DB setting SslCipherList (source: IniFileSettings.cpp:578; Constants.h:139; CreateTablesMSSQL.sql:1031 seeds the legacy long list)
  • AEAD-ONLY preset: setting SslCipherList to exactly that name (case-insensitive) expands to 9 ECDHE/DHE GCM and CHACHA20 suites; misspellings are reported as error 5511, not ignored (source: Server/Common/TCPIP/SslContextInitializer.cpp:649-690)
  • Session tickets/cache/timeout/rotation: TlsSessionTicketsEnabled=1, TlsSessionCacheSize=0, TlsSessionTimeoutSeconds=0, TlsTicketKeyRotationSeconds=0 (source: IniFileSettings.cpp:584-588)
  • MtaStsEnabled=1, DaneEnforcementEnabled=1, DnssecValidationEnabled=1, DnssecTrustAnchors="" (source: IniFileSettings.cpp:402-405)
  • Remote domain policies (6.3.4; schema 6045): per remote domain or wildcard pattern, the most specific active record governs. Outbound TLS none/encrypted/verified/dane, combined with MTA-STS and DANE as the strongest of the three and applied to routed deliveries too; a delivery it cannot satisfy is deferred with 4.7.0 and a sentence naming the policy, never sent in the clear. Inbound: 530 5.7.0 at MAIL FROM on an unencrypted, unauthenticated session from the domain, incoming relays exempt. Limits: a message over the size bounced at once with 5.3.4; connections and messages a minute deferred with 4.4.5. Switches for automatic replies and for forwarding (Sieve redirect included). Backup-MX recipient callout at RCPT TO: null-sender probe, only a 5xx refuses (550 5.1.1), cached 60 min (1 min for no verdict), 10 s timeout (at most 60), 10 per minute per remote, at most two hosts, never for a hosted domain. Edited in the Control Panel (Mail flow & delivery > Remote domains), the Deck, /api/v1/remote-domains and COM Settings.RemoteDomainPolicies; carried by the backup with the settings (source: Server/Common/BO/RemoteDomainPolicy.h/.cpp; Server/SMTP/ExternalDelivery.cpp; SMTPConnection.cpp; RecipientCallout.cpp; RemoteDomainThrottle.cpp; Common/Util/RestApiRemoteDomains.cpp; docs/RemoteDomains.md). See Routes and Relays §14.2a.
  • The connection to the database uses TLS where it is offered (6.3.4): MySQL and MariaDB through MariaDB Connector/C had never used TLS, whatever the documents said, and now do whenever the database server offers it; SQL Server through MSOLEDBSQL 18 and SQL Native Client encrypts the whole session; PostgreSQL is unchanged. [Database] TlsMode (empty for prefer, disable, require, verify-full) and TlsCaFile choose otherwise, anything else refused (HM7530); the application log says at every start what the connection got, and --check-database says the same (source: IniFileSettings.cpp ReadIniSettingString_("Database", "TlsMode", ""); hmailserver/docs/DatabaseTls.md).
  • A certificate for an IP address is accepted on Linux (6.3.4): an HTTPS endpoint named by its address - an object store, a webhook, an identity provider, the update feed - was refused when its certificate named the address in an iPAddress entry, where RFC 5280 puts it; and no server name is sent for an address (hmailserver/docs/DatabaseTls.md).
  • A new installation is secure before anyone configures it (6.3.4): a database the installation created - never an upgraded or attached one - gets a self-signed certificate (RSA 2048, 825 days), STARTTLS optional on 25, 587, 110 and 143, and TLS required for authentication on the Internet range, through --apply-new-install-defaults, run once by the Windows installer and by the Linux unit's first start, and from the release after 6.3.7 by the Compose file's and the Helm chart's first start (the charts of 6.3.6 and 6.3.7 do not run it); the AppImage and any other container run it by hand. The setup checklist's Passwords only over TLS item judges every server (hmailserver/docs/SecureDefaults.md; Ports Reference 3.1).
  • DANE validates the MX RRset under DNSSEC before applying TLSA to a host; an MX chain that fails validation skips that host (RFC 7672 s2.2) (source: Server/SMTP/ExternalDelivery.cpp:251-320; resolver in Server/Common/TCPIP/DnssecResolver.cpp) — README:44 CONFIRMED.
  • DNSSEC denial of existence (6.2.27): a DS query answered with nothing is no longer treated as an unsigned delegation; the resolver keeps the authority section and requires the parent's NSEC (NS set, DS clear), matching NSEC3, or Opt-Out NSEC3 proof, signed by the parent's key; a delegation under a signed parent without such a proof is Bogus, an unsigned parent still makes everything below Insecure. Diagnostics.DnssecChainStatus exposes the verdict (0 secure, 1 insecure, 2 bogus) (source: Server/Common/TCPIP/DnssecResolver.cpp, nine NSEC3 sites; hMailServer.idl:3223).
  • TLS-RPT: TlsRptFromAddress="" (off), TlsRptOrganizationName=hMailServer; with the address empty the task logs that statistics are collected but never sent (source: IniFileSettings.cpp:483-484; Server/SMTP/TlsRptReporterTask.cpp:52) — README:49 CONFIRMED.
  • ACME: AcmeEnabled=0, AcmeDirectoryUrl=https://acme-v02.api.letsencrypt.org/directory, AcmeContactEmail="", AcmeDomains="", AcmeHttpPort=80, AcmeReuseKey=1, plus AcmeCertificateDirectory="" (not in README) which defaults to <DataFolder>\ACME (source: IniFileSettings.cpp:595-601; Server/Common/Util/AcmeClient.cpp:248-256)
  • ACME applies the certificate as an SSL certificate record named "ACME (automatic)" (fullchain.pem/privkey.pem), assigns it to TLS-enabled ports with no certificate, then restarts the TCP servers to load it — the Windows service itself is not restarted (source: AcmeClient.cpp:1524-1592) — README:50,342 CONFIRMED.
  • ACME (6.2.25, GitHub issue 93): the TLSA-line and key-reuse paths read files through OpenSSL's own BIOs rather than a CRT FILE* — 6.2.24 terminated the process via the missing OPENSSL_Applink at every issuance and renewal; deployment now runs before the TLSA line, and a certificate pair under Data\ACME that no "ACME (automatic)" record names is deployed at the first check after start ("issued but never deployed" in the log) (source: AcmeClient.cpp:85,316,768 BIO_new_file; :1581).
  • Encrypted private-key passphrase column hm_sslcertificates.sslprivatekeypassword (schema 6009) (source: Constants.h:167-169) — README:45 CONFIRMED by schema comment only.
  • Per-port inbound client-certificate policy exists (schema 6008; InterfaceTCPIPPort ClientCertificatePolicy) (source: Constants.h:163-166; Server/COM/InterfaceTCPIPPort.cpp) — NOT in README (GAP).
  • SNI: symbol hits in Server/Common/TCPIP/TCPConnection.cpp and Server/SMTP/SMTPClientConnection.h only; not read in depth — treat README:42 "with SNI" as plausible, unconfirmed here.

4. Sender authentication and anti-abuse

  • DKIM a= tag: ed25519-sha256, rsa-sha256, or rsa-sha1 (source: Server/Common/AntiSpam/DKIM/DKIM.cpp:372); DkimAcceptSha1=0 default refuses SHA-1 on verification (IniFileSettings.cpp:642)
  • DKIM t= is always emitted and x= when DKIMSignatureValiditySeconds > 0 (source: DKIM.cpp:1284 " t=%I64d; x=%I64d;"); DKIMSignatureValiditySeconds=0, DKIMEnforceSignatureExpiry=1, DKIMExpiryClockSkewSeconds=300, DkimOversignHeaders="" (IniFileSettings.cpp:464-474) — README:56 CONFIRMED.
  • ARC sealing ArcSealingEnabled=0 (IniFileSettings.cpp:455); ARC seal algorithms rsa-sha256 / ed25519-sha256 (Server/Common/AntiSpam/DKIM/Arc.cpp:129)
  • Inbound ARC filtering is NOT an INI key: ASArcFilteringEnabled and ASArcTrustedSealers are hm_settings rows seeded 0 / '' and exposed as COM AntiSpam.ArcTrustedSealers (source: Constants.h:118-119; CreateTablesMSSQL.sql:995-997; Server/COM/InterfaceAntiSpam.cpp:1237-1261; Server/Common/AntiSpam/DKIM/SpamTestArc.cpp:275). README:57 names the setting without saying where it lives.
  • DMARC organisational domain from a compiled-in Public Suffix List (source: Server/Common/AntiSpam/DMARC/PublicSuffixList.cpp, PublicSuffixListData.h) — README:55 CONFIRMED. DmarcTreeWalkEnabled=1 (IniFileSettings.cpp:639) — not in README.
  • DMARC aggregate reports: DmarcRptFromAddress="" (off), DmarcRptOrganizationName=hMailServer, DmarcRptSchemaVersion=1 (2 = RFC 9990; other values -> error 6210 and 1) (source: IniFileSettings.cpp:490-527). README config lists only DmarcRptSchemaVersion — the sender address key is not in README (GAP).
  • SpfVoidLookupLimit=2 (IniFileSettings.cpp:820 at master 540347500) — not in README.
  • SPF, since 6.3.4: RMSPF (RFC 4408, its own DnsQuery_A lookups, a refusal shim on Linux that made every check TempError, an unconditional pass for 127.0.0.0/8 and ::1) is replaced by an RFC 7208 implementation ported from upstream (Server/SMTP/SPF/SPFEvaluator.cpp, SPFRecord.cpp, SPFMacroExpander.cpp, SPFSyntax.cpp, SPFAddress.cpp), which asks DNS through the server's own resolver (SPFDnsResolver over DNSResolver::GetRecordsOfType, with the DNSSEC gate TXT lookups already had) and so works on Linux and honours DNSServer. Loopback clients get no pass. All seven results are written to Authentication-Results under their own RFC 8601 keywords; scoring is unchanged (a fail scores, a pass is recorded). The openspf.org RFC 7208 suite, 203 cases, runs in SPFTester from ClassTester::DoTests and on Linux as the spf-tests binary (source: SPF.cpp SPFTester::Test; SPF/Conformance/README.md; CMakeLists.txt add_executable(spf-tests).
  • Domain transforms (6.3.4; schema 6044), per domain and off by default: external_tag_subject (the tag text, [EXTERNAL] when empty, at the front of the Subject - breaks a sender's DKIM signature over it), external_tag_header (X-hMailServer-External: YES), first_contact_tip (X-hMailServer-First-Contact: YES, answered from hm_knownsenders), applied at local delivery to the recipient's copy; and disclaimer_enabled with plain and HTML texts, appended to the text parts of outgoing mail on the SMTP accept path only - not to mail sent from the webmail or by send-later, nor to a signed or encrypted message, an automatic reply, a bounce or a list posting. Outside means neither authenticated (read from this server's own topmost Received), nor handed over by an incoming relay, nor with both envelope sender and From at hosted domains (source: Common/Util/MessageOrigin.cpp, ExternalSenderTagger.cpp, DisclaimerAdder.cpp; SMTP/LocalDelivery.cpp:430; SMTP/SMTPConnection.cpp:2316; docs/DomainTransforms.md). See Stopping Spam §10.7a.
  • SRS: SRSEnabled=0, SRSSecret=""; addresses valid for kMaxAgeDays = 21 (source: IniFileSettings.cpp:614-615; Server/Common/Util/SRS.cpp:24) — README:497 CONFIRMED. Legacy RewriteEnvelopeFromWhenForwarding=0 still exists (IniFileSettings.cpp:613).
  • BATV: BATVEnabled=0, BATVSecret=""; prvs= implementation in Server/Common/Util/BATV.cpp (source: IniFileSettings.cpp:616-617). README:58 mentions BATV but the config section omits both keys (GAP).
  • SpamAssassin, DNSBL, SURBL, greylisting, HELO host check, PTR check, MX check all present (source: Server/COM/InterfaceAntiSpam.cpp; Server/COM/InterfaceDNSBlackLists.cpp; Server/Common/AntiSpam/SpamTestHeloHost.cpp) — README:59 CONFIRMED in outline.
  • SpamAssassin per-user preferences and learning (6.2.25): SpamAssassinUser="", SpamAssassinUserFromRecipient=0 tell spamd whose preferences to apply; SpamAssassinLearnOnMove=0 makes an IMAP move into the Junk folder teach spam and a move out teach ham through spamc's TELL, off the IMAP thread. All off by default (source: IniFileSettings.cpp:341-354; Server/Common/AntiSpam/SpamAssassin/SpamAssassinClient.cpp:69-73 TELL SPAMC/1.4).
  • Anti-virus: ClamAV (ClamAVEnabled/Host/Port DB settings), clamscan and custom scanner via InterfaceAntiVirus (source: Constants.h:141-143; Server/COM/InterfaceAntiVirus.cpp); scanner-failure policy AVFailAction=0 (deliver), AVFailRetryMinutes=15, AVFailMaxHolds=16 (IniFileSettings.cpp:537-551) — the AVFail keys are NOT in README (GAP).
  • The ClamAV connection test asks PING and VERSION first and streams its EICAR sample from memory, so Defender cannot remove the sample file before clamd reads it (source: Server/Common/AntiVirus/ClamAVVirusScanner.cpp:136-157) — 6.2.25.
  • Auto-ban reaches the operating system's firewall (in the tree since 11 September 2026; unreleased until 6.3.2). An auto-ban is an expiring IP range the listeners consult at accept time, so the operating system still accepted the address and the server dropped it. Since 6.3.6 a never-ban list by reverse name (hm_neverbannames, schema 6108; Settings.AutoBanNeverBanNames, /api/v1/never-ban-names, the Auto-ban page, the Deck's Lists view) spares an address from all three auto-bans when one of its reverse names is covered by a pattern (mail.example.net, or .example.net for it and every name beneath) and resolves back to it, and AutoBanWidenByReverseName (off) widens a ban to the contiguous run of addresses around it inside its IPv4 /24 whose reverse names share its registrable domain, forward-confirmed, never across a never-ban entry or an administrator's IP range (AutoBan.md; work items #55 and #56). Three [Settings] keys, off as shipped, with editors on the Control Panel's Auto-ban page: AutoBanFirewall=1 - on Windows an inbound block rule per banned address in Windows Defender Firewall (TCP, the server's listening ports only, rule group hMailServer auto-ban), on Linux the packaged /usr/lib/hmailserver/autoban-hook keeping an nftables set with a timeout; AutoBanCommand - any program, run as <command> ban <address> <minutes> <ports> and <command> unban <address>; AutoBanNeverBan - addresses and CIDR blocks that are never counted, which matters once a ban can lock an administrator out of every mail port. The firewall is reconciled against the ranges at start-up, on ban, on range delete and every minute, so a rule never outlives its ban. Two fixed-shape application-log lines (Auto-ban: logon failure N of M from <ip> for <user>, Auto-ban: <ip> is banned for N minutes ...) are what the fail2ban filter the Linux packages install matches - see Installing on Linux. Errors 6521-6525.
  • Attachment blocking (BlockedAttachment), auto-ban (AutoBanOnLogonFailureEnabled seeded 1) (source: Server/COM/InterfaceAntiVirus.cpp; CreateTablesMSSQL.sql:1001)
  • Login tarpit LogonTarpitSeconds (SMTP/POP3/IMAP) and recipient tarpit SmtpTarpitCount/SmtpTarpitDelaySeconds (COM AntiSpam.TarpitCount/TarpitDelay), both off by default, shipped in 6.2.25 (source: IniFileSettings.cpp:675-679); a pause on the connection's own timer (TCPConnection::EnqueueDelay, BCTDelay), never a worker thread (source: AccountLogon::TarpitDelaySeconds; SMTPConnection::TarpitRecipient_)
  • Rate limits: MaxSubmissionsPerIPPerMinute=0, MaxOutboundPerDestinationPerMinute=0 (source: IniFileSettings.cpp:618,673)
  • Trace headers off by default: AuthenticationResultsEnabled=0, ReceivedSpfHeaderEnabled=0, AuthenticationResultsIdentity="" (IniFileSettings.cpp:480-482) — not in README (GAP).
  • External filter hook: FilterHookUrl="", FilterHookTimeoutSeconds=10, FilterHookFailClosed=0, FilterHookRejectScore=100, FilterHookMaxMessageSizeKB=10240 (IniFileSettings.cpp:658-662) — not in README (GAP).
  • Quarantine: QuarantineEnabled=0, QuarantineRetentionDays=30 (IniFileSettings.cpp:624-625) — not in README (GAP).
  • 6.3.4, every one off until it is switched on unless it says otherwise, and every setting a row of hm_settings (Settings Reference section 17): sender allow and block lists with URL entries and quarantine digests (6046); DKIM key rotation (6048); bounces and a suppression list (6049); country blocking from a MaxMind database (6052); complaints from the feedback loops (6055); the reputation watch (6057; on in a new installation); spam traps (6058); mail flow rules for a domain, with reject, require-TLS, quarantine and disclaimer actions (6063) and an outbound content policy of card numbers, IBANs, account numbers and attachment types (no step); compromised-account detection, in watch by default (6071); greylisting keyed by a digest, with a sliding wait, an abuse lock and a bypass list that needs SPF, DKIM or DMARC (6072); SPF SoftFail, PermError and TempError scores, a TempError deferral, a multi-recipient null sender refused or banned, and a domain white and black list by envelope domain or HELO (6073); restricted delivery (6084); impersonation protection (6087); throttling per destination and per tenant, with a back-off that is on at 2 minutes (no step); IP warm-up (6093); and a sandbox for attachments (6091). Changes-Since-6210 section 4e has each.

5. Account security and authentication

  • Password hash scheme ids: 0 None, 1 Blowfish, 2 MD5, 3 SHA256, 4 PBKDF2, 5 Argon2id, 6 DPAPI, 7 scrypt (source: Server/Common/Util/Crypt.h:18-30; scrypt added in 6.2.25, numbered 7 because DPAPI took 6). Schemes are compared by strength, not by number: Argon2id and scrypt are peers.
  • PreferredHashAlgorithm=4 (PBKDF2) is the shipped default; a value that is not 3, 4, 5 or 7 is reported as error 5528 and reset to 4 (source: IniFileSettings.cpp:190-209).
  • Work factor is a setting since 6.2.25: PasswordHashIterations, PasswordHashMemoryKB, PasswordHashTimeCost (read with a floor and a ceiling by ReadPasswordHashWorkFactor_, IniFileSettings.cpp:233-235); a stored hash derived under a cheaper factor is re-derived at the next logon, upward only.
  • README:70 lists "Argon2id and PBKDF2-HMAC-SHA256" without stating the default; a header comment claiming Argon2id is the default (Server/Common/Util/Pop3LoginDelay.h:9) is inconsistent with the INI default.
  • MinimumAcceptedHashAlgorithm=0 (policy off), PasswordPepper="" (only affects Argon2id) (source: IniFileSettings.cpp:198-203)
  • Password policy keys (all off/0): PasswordPolicyMinimumLength, PasswordPolicyRequireMixedCase, PasswordPolicyRequireDigit, PasswordPolicyRequireNonAlphanumeric, PasswordPolicyRejectCommon, PasswordPolicyHistoryCount, PasswordPolicyMaximumAgeDays (IniFileSettings.cpp:666-672) — not in README (GAP).
  • Account lockout: AccountLockoutThreshold=0 (off), AccountLockoutWindowMinutes=30, AccountLockoutMinutes=30 (IniFileSettings.cpp:558-564) — not in README (GAP).
  • SCRAM-SHA-256 with deterministic anti-enumeration salts (source: Server/Common/Util/Hashing/ScramSha256.cpp, symbol hit "deterministic"); SASLprep in Server/Common/Util/Parsing/StringParser.cpp — README:66,71 CONFIRMED in outline.
  • OAuth2 bearer: OAuth2Enabled=0, OAuth2RequireTLS=1, OAuth2AllowedAlgorithms=RS256, OAuth2HmacSecret="", OAuth2PublicKeyFile="", OAuth2Issuer="", OAuth2Audience="", OAuth2UsernameClaim=email (source: IniFileSettings.cpp:208-229) — README:67 names the feature; none of the keys appear in the README config section (GAP).
  • OAuth2 key sources (6.2.25): OAuth2JwksUrl="" fetches the provider's JWK Set as the signing-key source (rotation handled, cached OAuth2JwksCacheSeconds=3600); OAuth2IntrospectionUrl="" with OAuth2IntrospectionClientId/OAuth2IntrospectionClientSecret="", OAuth2IntrospectionCacheSeconds=300, OAuth2IntrospectionFailOpen=0 adds token introspection for revocation. Both off until configured (source: IniFileSettings.cpp:265-275); both fetches share HttpsClient with the update check, so from 6.2.28 they go through [Settings] HttpProxy when one is set; since 6.3.6 the outbound/fetch XOAUTH2 token request does too, a port in OutboundOAuth2TokenUrl is used, and the request is given up after 20 seconds whole.
  • SmtpAuthenticatedSenderCheck=0 (6.2.25): when 1, an authenticated session may only send as an address its account owns or has been granted; the post right on a mailbox's INBOX is the Send-As grant (source: IniFileSettings.cpp:734).
  • Outbound XOAUTH2 to smart hosts: OutboundOAuth2TokenUrl, OutboundOAuth2ClientId, OutboundOAuth2ClientSecret, OutboundOAuth2Scope=https://outlook.office365.com/.default, OutboundOAuth2Hosts=smtp.office365.com, OutboundOAuth2FixedToken, FetchOAuth2Hosts=outlook.office365.com (IniFileSettings.cpp:210-217) — not in README at all (GAP).
  • DisableAUTHList="" (IniFileSettings.cpp:674) — not in README.
  • TOTP for administrative logon: Server/COM/COMAuthentication.cpp; Control Panel Tools/ControlPanel/Views/ConnectView (source: grep hits) — README:72,115 CONFIRMED in outline.
  • SERVER-enforced administrator second factor (shipped in 6.2.25): [Security] AdministratorTotpSecret (DPAPI-protected). COMAuthentication refuses password-only once enrolled, AuthenticateWithCode is the way in; REST API needs X-hMailServer-OTP header and 401s with X-hMailServer-OTP: required. Enrol/remove via Settings.EnrolAdministratorTOTP/DisableAdministratorTOTP; state via Application.AdministratorTOTPEnabled. CP connect flow prompts and retries; AdministratorTwoFactorDialog enrols. Distinct from and stronger than the older HKLM Control-Panel-only TOTP (source: COMAuthentication.cpp; RestApiServer.cpp AuthenticateBasic_/BuildUnauthorizedResponse_; InterfaceSettings.cpp; ServerSession.cs).
  • Control Panel AD pickers: Tools/ControlPanel/Services/ActiveDirectoryService.cs (System.DirectoryServices) — README:114 CONFIRMED in outline.
  • 6.3.4: administrator roles - serveradmin, domainadmin, helpdesk, auditor and custom ones over 27 permissions (24 before eDiscovery's two and the inbound webhooks' one), decided in one place for COM and REST (6047); the password breach check by k-anonymity (off); sessions and devices with revocation, and session policies per server, domain and account (6053); a recovery address, ten one-time recovery codes and a self-service password reset (6076); passkeys (WebAuthn, 6077) and single sign-on into the webmail and the Deck by OpenID Connect or SAML 2.0 - not into IMAP, POP3 or SMTP, which still take the password; and SCIM 2.0 provisioning at /scim/v2 for an identity provider (6096). hmailserver/docs/Roles.md, PasskeysAndSingleSignOn.md, AccountRecovery.md, Scim.md.
  • 6.3.4, secrets at rest (6103): an account's TOTP secret and ten [Settings] secrets (SRSSecret, BATVSecret, OAuth2HmacSecret, OAuth2IntrospectionClientSecret, OutboundOAuth2ClientSecret, OutboundOAuth2FixedToken, MetricsServerAuthToken, MetricsServerAuthPassword, ServiceAccountPassword, GssapiServicePassword) are sealed with AES-256-GCM (HMK1:) under the installation's stored-secrets key, kept in each server's [Security] StoredSecretsKey under DPAPI or the Linux key file and never in the database or a backup; the database records its id (StoredSecretsKeyId). Off after an upgrade until --create-stored-secrets-key; a new Windows installation's installer makes it. The administrator password is PBKDF2 from the installer, and an MD5 or SHA-256 value is hashed again at the next administrator sign-in (HM7494 when the file cannot be written) (hmailserver/docs/SecretsAtRest.md).
  • 6.3.4, mailbox encryption at rest (6099): per account, off on every account, and only after the domain's recovery key has been made: every message stored from then on is encrypted to an X25519 key of the account's own (AES-256-GCM), which the account's password opens at sign-in and which closes MailboxKeyIdleMinutes (15) after its last use; app passwords are refused for such a mailbox, and it cannot be combined with zero access or a TOTP second factor. REST and the Control Deck only (hmailserver/docs/EncryptionAtRest.md).
  • 6.3.4, a low-privilege service account on Windows: a new installation runs the service as NT SERVICE\hMailServer, an upgrade keeps its account; hMailServer.exe /GrantServiceAccess grants the program folder's Data, Logs, Temp and Database folders and the folders an administrator approved; hMailServer.INI is read-only to the service; the live update's apply, the auto-ban's firewall rules and Utilities.MakeDependent are refused under it with a sentence; /Register takes the account from --service-account alone (hmailserver/docs/ServiceAccount.md).

Password hashing: what happens at each moment

Three settings interact, and they do different jobs at different times. Getting them confused is the usual cause of "I set Argon2id and the hashes are still PBKDF2".

flowchart TD
    subgraph write["When a password is SET"]
        W1["PreferredHashAlgorithm<br/>default 4 = PBKDF2"] --> W2{"Is it 3, 4, 5 or 7?<br/>SHA256, PBKDF2, Argon2id, scrypt"}
        W2 -- no --> W3["Error 5528 and reset to 4.<br/>None, Blowfish and MD5 are<br/>never used for a NEW secret"]
        W2 -- yes --> W4["Derive with the work factor:<br/>PasswordHashIterations,<br/>PasswordHashMemoryKB,<br/>PasswordHashTimeCost"]
        W4 --> W5["PasswordPepper is mixed in -<br/>Argon2id only"]
    end
    subgraph read["When a password is CHECKED at logon"]
        R0["Compare against whatever scheme<br/>the stored hash uses"] --> R1{"Comparison<br/>succeeded?"}
        R1 -- no --> RFAIL["Refused"]
        R1 -- yes --> R2{"MinimumAcceptedHashAlgorithm > 0<br/>and the stored scheme ranks<br/>weaker than it?"}
        R2 -- yes --> R3["Refused, AFTER a correct comparison -<br/>deliberately, so a policy refusal cannot<br/>be reached by a wrong password.<br/>Application log: 'the password must be reset'"]
        R2 -- no --> R4{"Was the row still clear text,<br/>pending an upgrade?"}
        R4 -- yes --> RUP["Re-derive under the preferred scheme"]
        R4 -- no --> R5{"Is the preferred scheme STRONGER<br/>by StrengthRank than the stored one?"}
        R5 -- yes --> RUP
        R5 -- no --> R6{"Same scheme, but derived under<br/>a CHEAPER work factor than<br/>the ini asks for now?"}
        R6 -- yes --> RUP
        R6 -- no --> R7["Left exactly as it is"]
        RUP --> R8{"Could the new hash<br/>be persisted?"}
        R8 -- no --> R9["The login still succeeds on the old hash.<br/>Logged to the APPLICATION log, not ErrorManager -<br/>an unwritable database would otherwise report<br/>this on every login by every affected account"]
        R8 -- yes --> R10["Stored"]
    end
Loading

Read the right-hand column as one rule: upgrades happen at a successful logon, and only upward. Raising PreferredHashAlgorithm migrates each account the next time its owner signs in; lowering it, or lowering a work factor, leaves stored hashes alone and applies only to new ones — so an administrator who backs out of an expensive setting is not rewarded with a rewrite of every password to something weaker.

Two consequences worth planning for:

  • MinimumAcceptedHashAlgorithm is a lockout with extra steps if you set it too high. The refusal happens after a correct comparison, and the upgrade that would satisfy the policy can only happen on a successful login — so an account whose stored hash is below the minimum can never climb out on its own. Its password has to be reset.
  • The scheme ids are 0 None, 1 Blowfish, 2 MD5, 3 SHA256, 4 PBKDF2, 5 Argon2id, 6 DPAPI, 7 scrypt, and they are compared by StrengthRank, not by number — which is why Argon2id (5) and scrypt (7) are peers rather than one outranking the other, and why scrypt could be numbered 7 after DPAPI took 6 without changing any comparison.

[LDAP] section (source: Server/Common/LDAP/LdapSettings.cpp)

  • Section absent by default; Enabled=0 (229)
  • Server="" (230, required when enabled: 119), Port=0 -> 636 for LDAPS else 389 (231, 68-71)
  • Security=2 — 0 plain, 1 StartTLS, 2 LDAPS (235; LdapSettings.h:20-22)
  • VerifyCertificate=1 (252), AllowUnprotectedPassword=0 (253)
  • BindMethod=0 — 0 Simple, 1 Negotiate (SSPI Kerberos/NTLM; Windows only - on Linux 1 is refused with HM6420) (255; LdapSettings.h:40-41)
  • SearchBase="" (258), UserSearchFilter=(&(objectCategory=person)(objectClass=user)(sAMAccountName=%u)) (259, 18-19), UserDnTemplate="" (260)
  • ServiceUsername, ServiceDomain, ServicePassword all "" (261-263)
  • TimeoutSeconds=10, clamped 1-90 (270, 25-27)
  • FallbackToWindowsLogon=0 (276) — Windows only; on Linux the fallback refuses (HM6404)
  • Provisioning: SyncFilter=(&(objectClass=user)(objectCategory=person)(mail=*)(!(userAccountControl:1.2.840.113556.1.4.803:=2))) (278, 40-42), SyncMailAttribute=mail, SyncUsernameAttribute=sAMAccountName, SyncDisplayNameAttribute=displayName (279-281, 44-46), SyncMaxUsers=5000 max 100000 (299, 48-54)
  • SyncScheduleMinutes=0 (off), clamped 15-10080 when set; the task is registered only when set (322-334, 62-63; Server/Common/LDAP/DirectorySyncTask.h:33) — README:69 CONFIRMED.
  • README:68-69 claim "verified against a live Windows Server 2025 DC" is not verifiable from code — UNCONFIRMED.

6. Mail filtering and routing

Sieve (source: Server/Common/Sieve/SieveParser.cpp:138-213, SieveEvaluator.cpp)

  • Actions: keep, fileinto, discard, redirect, stop, setflag/addflag/removeflag, vacation, reject/ereject, include/return/global, addheader/deleteheader (editheader), notify (SieveEvaluator.cpp:329-553; SieveParser.cpp:536-542)
  • Tests: header, address, envelope, exists, size, allof/anyof/not/true, hasflag, body, mailboxexists, ihave, environment, date, currentdate, string, duplicate, spamtest (SieveParser.cpp:561-562)
  • Supported extensions (complete): fileinto, envelope, imap4flags, body, mailbox, vacation, vacation-seconds, copy, relational, subaddress, regex, ihave, environment, date, index, spamtest, spamtestplus, duplicate, editheader, variables, reject, ereject, include, enotify, comparator-i;ascii-casemap, comparator-i;octet, comparator-i;ascii-numeric (SieveParser.cpp:144-213)
  • ManageSieve advertises: fileinto copy relational subaddress vacation vacation-seconds imap4flags body mailbox regex ihave environment date index spamtest spamtestplus duplicate editheader variables reject ereject include enotify envelope (ManageSieveServer.cpp:331; envelope added to the line in 6.2.25)
  • README:77,468 lists only copy, relational, subaddress, imap4flags, vacation (+ :seconds) — the other 19 extensions are UNDERCLAIMED (GAP).
  • virustest deliberately absent (SieveParser.cpp:176-181)
  • Script storage: <DataFolder>\Sieve\<account>\active.sieve; global include scripts in <DataFolder>\Sieve\_global\<name>.sieve (source: Server/Common/Sieve/SieveStorage.cpp:58-112) — README:468 "stored as files under the data directory" CONFIRMED.
  • Only the five system flags can be stored (README:468) — not separately verified here.

Rules, scripts, routing

  • Event scripts implemented: OnAcceptMessage, OnBackupCompleted, OnBackupFailed, OnClientConnect, OnClientLogon, OnClientValidatePassword, OnDeliverMessage, OnDeliveryFailed, OnDeliveryStart, OnError, OnExternalAccountDownload, OnHELO, OnRecipientUnknown, OnSMTPData, OnTooManyInvalidCommands (source: Server/Common/Scripting/*.cpp string literals). README:79 lists only five categories — GAP.
  • Script engine timeout ScriptTimeout=60 s; ExternalProcessTimeout=300 s (IniFileSettings.cpp:307-308)
  • Event scripts on Linux (6.3.4): JavaScript, in the QuickJS engine (release 2025-04-26, vendored as source under Server/quickjs), for the same fifteen events under the same function names with the same objects; ScriptLanguage takes a third word, JavaScript. Windows keeps VBScript and JScript and refuses JavaScript at load; Linux refuses the other two. A JavaScript handler has no file, process or network access, ScriptTimeout and a 64 MB ceiling bound it, and GET /api/v1/status answers build.script_languages (hmailserver/docs/EventScripts.md; The COM API and Scripting 23.6).
  • The JScript escaping fix (6.3.4, commit 5a3de9001; CVE-2026-100741, issue #43): up to 6.3.3 a JScript OnClientValidatePassword handler could be made to run code in the server by a password ending its string with a backslash, before authentication; the same shape was open through OnExternalAccountDownload's UID and OnDeliveryFailed's error text and recipient. Every JScript value is now escaped in one helper, and a rule's Run script function name must be a plain identifier (HM5018 otherwise). VBScript servers were not open through those values.
  • Smart-host failover: a relayer host string containing | is split and each host tried in order (source: Server/SMTP/ExternalDelivery.cpp:444-449) — README:81 CONFIRMED.
  • Plus-addressing (InterfaceDomain), catch-all, distribution lists, routes, aliases: present in COM layer (source: Server/COM/InterfaceDomain.cpp, InterfaceCache.cpp) — README:80 CONFIRMED in outline.
  • RejectFullMailboxAtRcpt=1, QuotaWarningPercent=90 (IniFileSettings.cpp:641,643) — not in README.
  • Outbound delivery pipelines the envelope and sends the message as one BDAT chunk when the remote advertises PIPELINING and CHUNKING: OutboundPipelining=1, OutboundChunking=1 (each used only when the remote advertises the extension); a BINARYMIME message is relayed as-is to a remote that accepts BINARYMIME (source: IniFileSettings.cpp:732-733) — 6.2.25.
  • DeliveryHardLinks=0: when 1, a message to several local recipients is one file with a name in each folder; every rewrite of a message file is a temporary file renamed into place (source: IniFileSettings.cpp:312) — 6.2.25.
  • Event scripts can create only the COM classes ScriptAllowedObjects names; default * = anything, as before; a list makes CreateObject of anything else fail with error 429 (source: IniFileSettings.cpp:389; Server/Common/Scripting/ScriptObjectPolicy.cpp) — 6.2.25.
  • A Message-ID is added only to submissions (authenticated, or from a local domain on a range that does not require authentication); relayed mail keeps its headers (source: Server/SMTP/SMTPMessageHeaderCreator.cpp:83-100 is_message_submission_) — 6.2.25.
  • Message retention (schema 6027): Domain.MessageRetentionDays and Account.MessageRetentionDays (0 = no policy; on an account -1 = keep forever) remove delivered mail older than the policy in every folder, by stored creation time; MailboxRetentionTask runs at start and every 6 h, Utilities.RunMessageRetention runs it now (source: hMailServer.idl:854,988,1239; DBScripts/Upgrade6026to6027MSSQL.sql; Application.cpp:673-680) — 6.2.25.
  • Default special-use folders at account creation: hm_settings CreateDefaultSpecialUseFolders seeded 0 (COM Settings.CreateDefaultSpecialUseFoldersEnabled); off, a new account gets an inbox only (source: DBScripts/Upgrade6025to6026MSSQL.sql; Constants.h:153 PROPERTY_CREATE_DEFAULT_SPECIAL_USE_FOLDERS; hMailServer.idl:808) — 6.2.25, schema 6026.

7. Operations and observability

Metrics listener (source: Server/Common/Util/MetricsServer.cpp)

  • Paths: /livez, /readyz, /healthz, /metrics (930-951); readiness staleness ceiling DatabaseProbeStalenessMilliseconds = 20000 (108) — README:470 "within the last 20 seconds" CONFIRMED.
  • /metrics answers 503 on a non-loopback bind with no credential, and 503 when TLS was requested but could not be prepared; probes never authenticate (391-397, 1417-1458) — README:369-375 CONFIRMED.
  • Metric names exported: hmailserver_auth_failures_total, hmailserver_auth_success_total, hmailserver_build_info, hmailserver_command_processing_seconds, hmailserver_database_connected, hmailserver_database_probe_age_seconds, hmailserver_database_probe_success_timestamp_seconds, hmailserver_database_up, hmailserver_db_connections, hmailserver_db_query_seconds, hmailserver_db_slow_queries_total, hmailserver_delivery_queue_messages, hmailserver_delivery_queue_oldest_message_age_seconds, hmailserver_domain_messages_received_total, hmailserver_domain_messages_sent_total, hmailserver_messages_bounced_total, hmailserver_messages_deferred_total, hmailserver_messages_delivered_total, hmailserver_messagestore_missing_files, hmailserver_metrics_unauthorized_requests_total, hmailserver_processed_messages_total, hmailserver_sessions, hmailserver_spam_messages_total, hmailserver_start_time_seconds, hmailserver_state, hmailserver_store_bytes and hmailserver_store_messages (6.3.4 only; recomputed at most hourly by Reports::CachedStoreTotals), hmailserver_tls_certificate_expiry_seconds, hmailserver_tls_handshake_failures_total, hmailserver_tls_handshakes_total, hmailserver_viruses_removed_total, hmailserver_workqueue_blocking_tasks_waiting, hmailserver_workqueue_depth (grep of MetricsServer.cpp)
  • Per-domain counters need MetricsPerDomainEnabled=1 (default 0) (IniFileSettings.cpp:656) — not in README (GAP).
  • Settings: MetricsServerPort=0, MetricsServerBindAddress=127.0.0.1, MetricsServerAuthToken/Username/Password="", MetricsServerCertificateFile/PrivateKeyFile="" (IniFileSettings.cpp:426-437)
  • Metric history (6.2.25, schema 6028): one row per metric per minute in hm_metricsamples, kept MetricsHistoryDays=7; MetricsHistoryTask samples at start and every minute; read back over COM Utilities.GetMetricHistory/SampleMetricsNow and GET /api/v1/metrics/history; shown on the Control Panel dashboard for a day, a week or a month (source: IniFileSettings.cpp:735; Application.cpp:685-692; RestApiServer.cpp:1847; hMailServer.idl:1240-1241; DBScripts/Upgrade6027to6028MSSQL.sql).

OpenTelemetry (source: Server/Common/Util/Otel*.cpp)

  • OtelEndpoint="" traces /v1/traces (OtelTracer.cpp:72); OtelMetricsEndpoint="" /v1/metrics (OtelMetricsExporter.cpp:64); OtelLogsEndpoint="" /v1/logs (OtelLogExporter.cpp:51); default port 4318 (OtelExportChannel.cpp:21,60,67); OtelServiceName=hmailserver; OtelMetricsInterval=60 clamped 5-3600 (IniFileSettings.cpp:448-452; OtelMetricsExporter.cpp:30-31,79-80) — README:376-386 CONFIRMED.
  • traceparent read from message headers on delivery and emitted onward (source: Server/Common/Util/OtelTraceContext.cpp:188,210,266; Server/SMTP/SMTPDeliverer.cpp; Server/SMTP/SMTPMessageHeaderCreator.cpp; also HTTP in RestApiServer.cpp, WebServicesServer.cpp, HTTPClient.cpp) — README:87 CONFIRMED.

Windows Event Log, logs, disk, backup

  • WindowsEventLogEnabled=1, WindowsEventLogLevel=2 clamped 1-4 (IniFileSettings.cpp:628-636); throttle 5 events per id per 10-minute window, hard-coded (Server/Common/Application/WindowsEventLog.cpp:95-96) — README:387-394 CONFIRMED.

  • JsonLogging=0, LogDeleteDays=0, SepSvcLogs=0 (per-service log files), LogLevel=9, MaxLogLineLen=500, SlowQueryLogMilliseconds=0 (IniFileSettings.cpp:406-409,251-253). Slow-query log and SepSvcLogs keys are not in README (GAP; README:88 describes the features).

  • MessageStoreFsync=0; MessageStoreConsistencyCheck=0 writes <LogFolder>\hMailServer_messagestore_consistency.report (IniFileSettings.cpp:410,425; Server/Common/Application/MessageStoreConsistencyTask.cpp:74) — CONFIRMED.

  • MinimumFreeDiskSpaceMB=100, DiskSpaceWarningThresholdMB=1024, negatives clamped to 0 (IniFileSettings.cpp:411-420)

  • ShutdownDrainSeconds=0 (408); ScheduledBackupTime="", ScheduledBackupIntervalMinutes=0, ScheduledBackupKeepCount=0, ScheduledBackupMaxAgeDays=0 (444-447)

  • DatabaseStatementTimeout=30, negative -> 0; script runner asks for 1800 s (IniFileSettings.cpp:645-650; Server/Common/SQL/PGConnection.cpp:304; MySQLConnection.cpp:657) — README:429-433 CONFIRMED.

  • Bounded waits: DNSQueryTimeout=10, ClientSessionCeiling=1800, DBConnectionAcquireTimeout=60, ScriptTimeout=60, ExternalProcessTimeout=300, AsyncQueueStallThreshold=120, AsyncQueueReservedThreads=2, FinalizationTimeout=240 (IniFileSettings.cpp:281-315) — README:89 describes these ceilings but names none of the keys (GAP).

  • Message trace: MessageTraceEnabled=0, MessageTraceRetentionDays=30 (626-627); archive: ArchiveDir, ArchiveHardLinks=0, ArchiveRetentionDays=0, retention task every 12 h (IniFileSettings.cpp:262-265,644; Application.cpp:661) — not in README (GAP).

  • Full-text index: IndexerFullText=0, IndexerFullTextBatchSize=250 (1-100000), IndexerFullTextMinTokenLength=3 (3-64), IndexerFullTextMaxTokensPerMessage=2048 (64-1000000); also requires DB MessageIndexing (seeded 0) (IniFileSettings.cpp:365-392; CreateTablesMSSQL.sql:1013) — README:444-456 CONFIRMED.

  • Archive index (6.2.25, schema 6029): every archive copy is a row in hm_archiveindex; searched over COM Utilities.SearchArchive and GET /api/v1/archive; POST/DELETE /api/v1/archive/{id}/hold and Utilities.SetArchiveHold place a legal hold that survives the retention sweep and EraseAddressTraces; ArchiveDomains="" (comma list) scopes the archive to named domains (source: IniFileSettings.cpp:317; RestApiServer.cpp:2031; hMailServer.idl:1242-1243; DBScripts/Upgrade6028to6029MSSQL.sql).

  • BackupVerifyRestore=1 (6.2.25): every backup extracts its message store to a scratch directory through the restore code before it is called complete (source: IniFileSettings.cpp:480).

  • Backup completeness (6.3.4): the archive carries every per-account store 6.3.3 and earlier left out and a restore therefore deleted - hm_contacts, hm_accountprefs, hm_scheduled (by folder path and UID, not ids), hm_files, hm_smimekeys, hm_passwordhistory, hm_calendars with hm_calendarobjects, and hm_knownsenders - plus each message's keywords; the verified restore counts those rows back per table and discards an archive that does not hold them. build/check-account-stores.py fails when a table that reaches hm_accounts is neither carried nor declared an exception with its reason; in 6.3.4 the declaration names 62 per-account tables, 24 carried and 38 declared exceptions (9, 16 and 23 at 3233b0cc4, where the check was made and passed 84 checks, in GitHub's style.yml) (source: Common/Application/AccountStores.cpp; BackupExecuter.cpp VerifyRestore_; Common/BO/Message.cpp Keywords; test/RegressionTests/Infrastructure/BackupAccountStores.cs, 10 tests). The audit trail and the alert rules and events are not in the archive. See Backup and Restore §15.1a.

  • Syslog export (6.3.4): a copy of every log entry as RFC 5424 over UDP (1,024-byte limit), TCP or TLS (octet-counted, certificate verified), or on Linux under systemd with no host to the journal with HMAILSERVER_* fields; seven hm_settings rows (SyslogEnabled, SyslogHost, SyslogPort 514, SyslogTransport 0, SyslogFacility 2, SyslogMinimumSeverity 6, SyslogLogTypes 86), twelve COM properties on Settings.Logging and twelve keys of /api/v1/settings/logging; read at start; errors always sent; a 4,096-entry queue that drops the oldest (source: Common/Util/SyslogFormatter.cpp, SyslogSink.cpp; Configuration.cpp EnsureSettingDefaults_; docs/Syslog.md). See Monitoring and Health §16.2a.

  • Alerts (6.3.4; schema 6043): six conditions as rows of hm_alertrules at 3233b0cc4 - backup.failed and certificate.expiring on, disk.low, queue.stalled, autoban.storm and minidump.written off - and eleven at master 540347500, the later steps adding dkim.rotation.stalled, complaints.rate.high, reputation.listed, account.compromised and account.password-reset, each seeded on, so seven are on; sent once a minute by AlertTask by mail (nothing without AlertSenderAddress and a recipient) and/or an HMAC-SHA256-signed webhook retried and then dead-lettered (HM6543); a daily digest at AlertDigestHour UTC; REST only for the rules and events, administrator password only (source: Common/Util/AlertManager.cpp; RestApiAudit.cpp). See Monitoring and Health §16.7a.

  • Audit trail (6.3.4; schema 6043): hm_audit, one hash-chained row per administrative change taken at DatabaseConnectionManager::Execute and PropertySet::Set* with a thread-local actor from RestApiServer::ProcessRequest_ and COMAuthentication; GET /api/v1/audit and /verify, administrator password only; the Deck's Audit trail view. Incomplete at 3233b0cc4: five watched table names do not match the real tables (domain aliases, incoming relays, white list, greylisting white addresses, blocked senders), hm_acl, hm_remotedomainpolicies and hm_inisettings are not watched, the twelve syslog COM setters install no actor, and build/check-audit-choke-point.py, which reports 13 COM gaps, is run by no workflow (source: Common/Util/AuditTrail.cpp AuditedTables; DBScripts/CreateTablesMySQL.sql). See Monitoring and Health §16.7b.

  • Reports (6.3.4): GET /api/v1/reports and /api/v1/reports/{traffic|failures|senders|recipients|mailboxes|volume|storage|summary} with from, to (at most 366 days), domain, top (1-200) and format=csv; the first four need the message trace (MessageTraceEnabled=0 by default); volume and storage are server-wide and refused to a domain-restricted key; spam, virus and storage growth per domain are stated as unanswerable; the metric history gains store_bytes and store_messages; a Deck Reports view and hmctl report (source: Common/Util/RestApiReports.cpp, Reports.cpp; MetricsHistoryTask.cpp). See Monitoring and Health §16.6a.

  • Storage compression and attachment de-duplication (6.3.4, schema 6098): with MessageStoreCompression on (off as shipped), a message delivered or appended is stored deflated and each MIME part of at least MessageStoreDedupMinKB (256) is kept once for every message carrying the same bytes; every reader sees the message as it arrived, and a stored file is checked by its SHA-256 before it is served; the queue is never compressed. --measure-store, --compress-store and --compress-store --decompress; GET /api/v1/store; COM Settings.MessageStoreStatus (hmailserver/docs/MessageStore.md).

  • Object storage for the message store (6.3.4, no schema step): messages on an S3-compatible store (AWS S3, MinIO, Wasabi, Backblaze B2, Ceph RGW) or Azure Blob Storage with the database as the index, written to the local disk first and moved by a background committer; TLS to the store always verified. Off until an administrator names a store: MessageStoreBackend is file and MessageStoreObjectService empty as shipped. --copy-store-to-object-storage, --copy-store-from-object-storage (hmailserver/docs/ObjectStorage.md).

  • SNMP (6.3.4, no schema step): a read-only SNMPv3 agent (authPriv only: HMAC-SHA-256 and AES-128; SNMPv2c optional and off) answering the mail MIB (RFC 2789) from the counters /metrics serves. Off, and answering nobody until SnmpAllowedAddresses names a manager; UDP 161 by default; its settings on REST and applied at a restart, with no COM property and no Control Panel page; its enterprise arc is RFC 5612's documentation number (hmailserver/docs/Snmp.md; Ports Reference 4.6).

  • eDiscovery (6.3.4, schema 6097): cases, searches across every mailbox, the deleted items and the archive, legal holds that outrank deletion, retention, a purge, a move to another domain and a full restore, and exports as EML and mbox with a SHA-256 manifest; permissions view_discovery (the auditor role) and manage_discovery; every search, hold, export and download on the audit trail; routes under /api/v1/discovery/cases, a Deck view and a read-only Control Panel page (hmailserver/docs/Discovery.md).

  • PST import (6.3.4, no schema step): a migration of kind pst, one job per Outlook data file into one account, read by a reader of this project's own in a process of its own (hMailServer.exe --pst-read, hmailserver --pst-read), killed on a stop, a pause or a cancel, so a file that crashes it costs one message, not the service; ANSI files and an OST that is a PST in all but its name are read, the 4K-page layout, Windows Information Protection and an OST caching an Exchange mailbox refused. No PST job runs until PstImportDirectory names a directory, empty as shipped (hmailserver/docs/Migration.md).

  • Mail in, to a webhook (6.3.4, schema 6100): an address or a whole hosted domain routed to a URL, the mail posted there parsed, in SendGrid's inbound-parse field names or as one JSON document, signed with the outbound webhooks' HMAC-SHA256 recipe and retried on their schedule; routes over REST and the Deck under manage_inbound_webhooks, which only serveradmin holds; no COM member and no Control Panel page; nothing on until a route is made (hmailserver/docs/InboundWebhooks.md).

Live update (new in 6.2.28)

  • The feed and the signers, as of 6.3.4. Up to 6.3.3 the default feed is the Releases API of the suspended account's repository on GitHub, which has answered 404 since 18 September 2026, and the one signer compiled in is this repository's GitHub workflow sign-release.yml; such a server reads the new feed only with UpdateFeedUrl=https://updates.progressiverobot.com/releases.json. 6.3.4 reads https://updates.progressiverobot.com by default, which GitLab Pages serves from the project's releases (UpdateChecker.cpp STABLE_FEED, PRERELEASE_FEED), and trusts two signers: the GitHub workflow for a certificate issued before the suspension and a release no later than 6.3.3, and the project's GitLab CI pipeline for a v* tag, which is to sign the releases after 6.3.4 and has signed none yet, at the project's present path and at the path of the group hmailserver it is to move to, where the certificate must also carry the project's id, 86730559 (SigstoreVerifier.cpp DefaultTrust). A file signed for one tag is refused as any other release, and on Windows the installer's version resource must name the release it is installed as. 6.3.4 itself carries no Sigstore bundle, so every server installs it by hand: a live update offered it answers Release 6.3.4 carries no Sigstore bundle for its installer, so it cannot be verified. and downloads nothing (UpdateDownloader.cpp; hmailserver/docs/UpdateFeed.md). On Windows the Updates folder admits changes from SYSTEM, the Administrators and the service alone, and under the virtual service account the apply is refused: an administrator downloads the installer from the release page, checks its Authenticode signature and runs it.
  • The server reads the project's release feed once a day when UpdateCheckEnabled=1 (default 0) and on demand always (Status.CheckForUpdate, POST /api/v1/update/check); the UpdateCheckTask runs at start and every 15 minutes so an UpdateWindow an hour long is not missed; the feed is read every UpdateCheckHours=24 (source: IniFileSettings.cpp:489-494; Application.cpp:709-715).
  • Verify: the newer release's installer and its Sigstore bundle are downloaded to <DataFolder>\Updates and the installer kept only when the bundle proves it is the one the release workflow signed (digest, Fulcio chain at log time, identity, signature, log signature) — no cosign, no network beyond the download (Status.DownloadUpdate; Common/Util/SigstoreVerifier.cpp, UpdateDownloader.cpp). Trust roots can be overridden: UpdateTrustRootsFile, UpdateLogPublicKeyFile, UpdateSigningIdentity, UpdateSigningIssuer, UpdateSourceRepository, UpdateRequireAuthenticode=0 (IniFileSettings.cpp:498-503) - in hMailServer.ini only, since 6.3.6: the five trust keys are read from the file's [Settings] section alone, and a change to one over COM or REST is refused (409).
  • Apply: Status.InstallUpdate / POST /api/v1/update/install hands the verified installer to hMailServer.Updater.exe, which runs it, waits UpdateServiceWaitSeconds=180 for the service to come back, reinstalls the previous version if it does not, and reports the outcome at the next start (Status.UpdateApplyOutcome); the database upgrade authenticates with a single-use token instead of the administrator password (Common/Util/UpdateApplyToken); UpdateBackupBeforeApply=1, UpdateAutoDownload=0, UpdateWindow="", UpdateChannel=stable, UpdateFeedUrl="" (source: IniFileSettings.cpp:489-509; hMailServer.idl:540-551; installation/section_files_64.iss:6; Server/hMailServer.Updater/).
  • Surfaces: COM Status.UpdateState (0 unchecked, 1 latest, 2 newer available, 3 downloaded and verified, 4 installing, 5 failed; since 6.3.6 also 6 while the service prepares an administrator's install and 7 when it is ready), AvailableVersion, AvailableVersionPublished, AvailableVersionUrl, UpdateLastChecked, UpdateLastError, UpdateInstallerPath, UpdateSignerIdentity; REST GET /api/v1/update; the Control Panel's Updates card (FeatureSettingsView) and the three status-page buttons (source: hMailServer.idl:540-551; RestApiServer.cpp:1853-1871; Tools/ControlPanel/Views/StatusView.xaml.cs:42-44; FeatureSettingsView.xaml.cs:1932-1934).
  • Through a proxy: HttpProxy="" ([Settings]) is a forward proxy for the web requests the server makes as a client through HttpsClient - the feed, the installer and its bundle - as host:port or [ipv6]:port; empty is a direct connection, a value without a port is an error rather than a silent direct connection, and there are no proxy credentials. An https target is reached with CONNECT and the TLS handshake runs inside the tunnel under the same certificate verification as a direct connection; plain http gets the absolute URL in the request line; a proxy that says no is reported as The proxy <proxy> refused CONNECT to <host:port>: <its status line>. The setting belongs to HttpsClient, so the OAuth2 JWKS fetch and token introspection take the same route; up to 6.3.5 those were all of it - ACME, the outbound XOAUTH2 token request and the MTA-STS policy fetch built their own connections and went direct whatever this was set to - and since 6.3.6 the ACME client, the outbound OAuth2 token request, the MTA-STS policy fetch, brand logos, Web Key Directory lookups, Web Push, one-click unsubscribe and inbound webhook routes held to public addresses go through it too, a request held to public addresses asking the proxy for a tunnel to the address the server checked, never the name. The editor is on the Control Panel's Updates card beside the feed URL (source: Common/Util/HttpsClient.cpp:86-198; IniFileSettings.cpp:492; FeatureSettingsView.xaml.cs:1936).

8. Technology

  • C++ server, <PlatformToolset>v145 (source: Server/hMailServer/hMailServer.vcxproj) — README:97 CONFIRMED.
  • OpenSSL 4.0.3, Boost 1.92.0, PostgreSQL 18.6 libpq referenced from the project/build files at the tag v6.3.6 (source: build/Get-LibraryVersions.ps1:23-25 openssl=4.0.3, boost=1.92.0, postgresql=18.6; hMailServer.vcxproj names openssl-4.0.3 and postgresql-18.6); 6.3.4 and 6.3.5 carried OpenSSL 4.0.2 and PostgreSQL 18.3, both bumped in 6.2.25, and 6.3.6 takes 4.0.3 (CVE-2026-84783 applies to the Windows server on a port that asks for client certificates) and libpq 18.6, built against it, into the x64 and ARM64 installers; built by libraries/build-openssl.ps1, build-boost.ps1, build-pgsql.ps1. README:98-100 CONFIRMED (README says "4.0.x").
  • Database types accepted in [Database] Type: MSSQL, MYSQL, PostgreSQL, MSSQLCE (source: IniFileSettings.cpp:114-120); SQL CE forced to one connection (147-153)
  • libmysql.dll shipped from Extras\ into Bin as the MySQL/MariaDB client, loaded at runtime (source: hmailserver/installation/section_files_common.iss:31-33; Server/Common/SQL/MySQLInterface.cpp:75). That it is MariaDB Connector/C is stated by the .iss comment and hmailserver/docs/ThirdPartyBinaries.md; the shipped auth plugin DLLs were NOT verified here — UNCONFIRMED.
  • Tools and Control Panel target net10.0-windows (9 csproj files) (source: grep of hmailserver/source/Tools/**/*.csproj) — README:102 CONFIRMED.
  • Quality gates: workflows present — ci.yml, codeql.yml (languages csharp AND c-cpp, weekly cron), dco.yml, dependency-review.yml, installer-smoke.yml, sbom.yml (Syft), scorecard.yml, server-build.yml, sign-release.yml, style.yml, upstream-watch.yml (monthly cron 0 6 1 * *), verify-binary-provenance.yml; .github/dependabot.yml exists (source: .github/workflows/ listing; codeql.yml:88,229; upstream-watch.yml:17) — README:106 CONFIRMED. They are GitHub Actions workflows: none has run for the project since GitHub suspended its account Progressiverobot on 18 September 2026, but sbom.yml and sign-release.yml, which, in the trees of the old tags, ran once on 18 October 2026 on the copies of the releases up to 6.2.22-pre6 in the project's repository on GitHub, GitLab's mirror since that day, and have been disabled there since (Assurance Case); and since 26 September 2026 none of them is in the tree - .github/ went from it that day, its allowed_signers, hmailserver.openvex.json and upstream-sync are in .gitlab/, and the workflows are in the git history at v6.3.3. From batch49 .github/ holds files GitHub reads for its copy, the pull-request template and a new workflows/scorecard.yml, and from batch51 CODEOWNERS, which names the code owner a pull request there asks for review, and a new workflows/codeql.yml, CodeQL over that copy in the place of GitHub's default setup: the two workflows of the project's that GitHub Actions runs, neither of them the workflow of that name listed above (Contributing). In 6.3.4 the project's own CI is defined in .gitlab-ci.yml: the platform-neutral checkers and the Deck's and the webmail's script harnesses on GitLab's shared runners, and, for a push to master, a batch* branch or a v* tag only, the Linux build, the Linux suite, the packages, the container image and the upgrade gates on the project's Linux runner and the Windows build and suite on its Windows runner; the release jobs for 6.3.5 onwards (release, release-authenticode, release-sign, release-verify, release-publish), written and not yet run when 6.3.4 was made, which have made every release since - 6.3.5 on 29 September 2026, 6.3.6 on 6 October 2026 and 6.3.7 on 17 October 2026; security scanning (cppcheck, clang-tidy, Semgrep, Trivy, OSV) not yet blocking; Renovate inert until its token and schedule exist. When 6.3.4 was made none of the jobs on the project's own runners had yet run, and the batches' Linux suites were run by hand on that runner's machine (hmailserver/docs/ContinuousIntegration.md).
  • Vendored and bundled in 6.3.4: QuickJS 2025-04-26 (source, Linux event scripts; 2026-06-04 since 6.3.6, which fixes CVE-2025-62494, -62493 and -62492, reachable by a script, and adds iterator helpers, the Set methods, RegExp v, Float16Array, Promise.try and Error.isError - ES2025), zlib 1.3.2, and on Windows 7-Zip 26.03 from 7-zip.org in place of 19.00; the .NET Desktop Runtime the installer bundles is pinned to 10.0.12 by version and SHA-512, and the SBOM names seventeen components, checked by build/check-sbom-inputs.py. Windows on ARM is written and not built: a Release|ARM64 configuration and a second installer script, hMailServerARM64.iss, exist, nothing of it has been compiled (the build machine lacks ATL for ARM64), and 6.3.4 has no ARM64 installer (hmailserver/docs/WindowsOnArm.md).

9. Administration surfaces

  • Control Panel hMailCP is WPF, talks over COM; Ctrl+K search button exists ("Search pages, settings and tasks (Ctrl+K)") (source: hmailserver/source/Tools/ControlPanel/MainWindow.xaml:80) — README:111-113 CONFIRMED in outline.
  • Control Panel localisation (new in 6.2.28): resx catalogues with the English text as the key and complete translations in Czech, Danish, German, Spanish, Finnish, French, Italian, Japanese, Norwegian bokmål, Dutch, Polish, Brazilian Portuguese, Russian, Swedish, Turkish, Ukrainian and Simplified Chinese; a language picker; checkers that fail on an unmarked or untranslated caption (in CI on GitHub until 18 September 2026, by hand since); 4,142 texts in Strings.resx at master 540347500, with the seventeen catalogues held complete by check-localisation.py (source: hmailserver/source/Tools/ControlPanel/Resources/Strings.*.resx, 17 catalogues beside Strings.resx; Services/LanguageChoice.cs; .github/workflows/style.yml:72-87 check-mnemonics.py, check-localisation.py, check-catalogues.py).
  • Every [Settings] key the server reads has a Control Panel editor, checked by build/check-ini-coverage.py, in style.yml on GitHub until 18 September 2026, by hand after, and in the checkers job of the project's .gitlab-ci.yml in 6.3.4 (source: .github/workflows/style.yml:95; .gitlab-ci.yml checkers) — new in 6.2.28. Since 6.3.4 the same check also refuses any new [Settings] key against build/ini-settings-baseline.txt (241 keys at master 540347500, the three work factors now counted; 238 when the rule was made), and the Control Panel writes those settings over COM only, into the database store, even when it runs on the server itself (source: Tools/ControlPanel/Services/IniFeatureStore.cs).
  • REST surface, counted from the OpenAPI path literals in RestApi*.cpp: 137 paths and 202 path-and-method operations in 6.3.3; 158 paths and 229 operations at 3233b0cc4, 15 September 2026 (the additions since 6.3.3: /api/v1/groups with its members, message copy, /api/v1/rules/match, /api/v1/calendar/expand, the remote domain policies, the reports, the audit trail, the alerts, and /deck-lang/{code}.json). The Control Deck had nineteen views there (fifteen in 6.3.3), and hmailserver/docs/DeckParity.md counts 365 properties the desktop program writes, all 365 writable over REST, 359 reachable from a view. At master 540347500: 404 paths and 563 path-and-method operations from the literals (376 and 527 under /api/, the rest SCIM, the sign-in and reading pages and the list manager's pages). The literals do not show the settings groups, whose paths RestApiSettings.cpp writes from its group tables - nine paths and 17 operations in 6.3.3, thirteen and 25 at 540347500 - so the document itself names 146 paths and 219 operations in 6.3.3 and 417 and 588 at 540347500; 38 Deck views, and a census of 464 properties the desktop program writes, 456 writable over REST and 456 reachable from a view; the Control Panel had 64 pages to 6.3.3's 55, and its Ctrl+K index 447 entries to 6.3.3's 393. In 6.3.4 (92b8f9f63): 428 paths and 596 operations from the literals (400 and 560 under /api/), the same thirteen settings groups, and so 441 paths and 621 operations in the document (413 and 585 under /api/) - the twenty-four new paths are eDiscovery's nine, the inbound webhooks' seven, mailbox encryption at rest's four, a room's booking policy, the store's two and the webmail's bulk action; 40 Deck views (Discovery and Inbound webhooks added); a census of 466 properties the desktop program writes, 458 writable over REST and 458 reachable from a view (DeckParity.md); the Control Panel has 65 pages in eight groups (Discovery added, read-only), and its Ctrl+K index 448 entries (SettingsSearchIndex.g.cs). See APIs Reference §4-5.
  • Configuration as a document (6.3.4): hmctl config export|diff|apply and the module's Export-/Compare-/Import-HmConfiguration over REST - deterministic, a plan unless --force, deletions only with --allow-delete. At 3233b0cc4 its ini section was empty against a real server (the route listed names only); since the third batch GET /api/v1/settings/ini answers values with secrets masked, so the document is whole. An API key still stops the export with a 401 (source: Tools/Cli/hmctl read_configuration; RestApiSettings.cpp IniList). See Command Line.
  • Tutorial tours (6.3.4): one never-modal stepper in the Control Panel (seven stops, progress under HKCU\Software\hMailServer\ControlPanel value TourProgress), the Deck (the same walk, localStorage hmsTours) and the webmail (four stops, kept in the account's preferences) (source: Tools/ControlPanel/Services/Tour.cs, TourProgress.cs, MainWindow.Tour.cs; installation/WebAdmin/index.html TOURS; Common/Util/Portal.js TOURS). See The Control Panel §5.2a.
  • REST API settings: RestApiPort=0, RestApiBindAddress=127.0.0.1, RestApiCertificateFile="", RestApiPrivateKeyFile=""; falls back to the ACME certificate (IniFileSettings.cpp:591-594; Application.cpp:516-526)
  • REST routes actually served (source: Server/Common/Util/RestApiServer.cpp:1281-2045): /api/v1/apikeys (GET, POST), /api/v1/apikeys/<id> (DELETE), /api/v1/status, /api/v1/domains, /api/v1/domains/<name>/accounts, /api/v1/domains/<name>/aliases, /api/v1/accounts/<address>, /api/v1/queue, /api/v1/queue/<id> and /retry, /api/v1/tlsa, /api/v1/srv, /api/v1/quarantine, /api/v1/quarantine/<id> and /release, /api/v1/openapi.json; since 6.2.25: /api/v1/ipranges (GET, POST), /api/v1/ipranges/<id> (DELETE), /api/v1/domains/<name>/lists (GET, POST), /api/v1/lists/<id> (DELETE), /api/v1/domains/<name>/dkim (GET), /api/v1/certificates (GET, never the private-key password), /api/v1/rules (GET), /api/v1/logs and /api/v1/logs/<name> (GET), /api/v1/backup (POST, GET), /api/v1/settings (GET snapshot), /api/v1/archive (GET) and /api/v1/archive/<id>/hold (POST, DELETE), /api/v1/metrics/history (GET) (:1847, 1930-2045). Writing settings, rules, certificates and DKIM stays COM-only on purpose. New in 6.2.28: /api/v1/update (GET) and /api/v1/update/check, /download, /install (POST) (:1853-1871); the self-service surface /api/v1/session (POST, DELETE), /api/v1/me and /api/v1/me/password, /vacation, /quarantine, /folders, /folders/<id>/messages, /messages, /messages/<id> (flags, move, delete, attachments), /drafts, /settings, /filters, /search (:1581-1738), and the pages /portal, /portal.js (:1281-1284).
  • README:472 omits /srv, /quarantine*, /domains/<name>/aliases, /openapi.json — GAP.
  • The earlier internal inconsistency (router /api/v1/apikeys, OpenAPI document /api/v1/api-keys) is resolved: the served document now spells /api/v1/apikeys (grep -c api-keys RestApiServer.cpp = 0; router at :1554).
  • REST auth: Bearer (scoped API key, SHA-256 hex digest stored, 32-byte secret, 8-byte id, hmapi_ prefix, stored in hMailServerApiKeys.ini beside the INI) preferred, Basic still accepted; key scope default "readonly"; 200 requests per 10 s per credential (source: RestApiServer.cpp:158,223,2416). Request limits: 64 KB per request, 16 MB and 300 s for the two message-submission routes only, 30 s per request, 300 s per connection, 64 concurrent connections, 4 worker threads — the listener is the shared Boost.Asio HttpServer with its own io_context and absolute deadlines, and adds a TLS 1.2 floor of its own after SslContextInitializer::InitServer (source: RestApiServer.cpp:128-136, 618-636; Common/Util/HttpServer.h:79-101). The HttpServer hosting is new in 6.2.28; the earlier "socket timeout 10 s" has no counterpart in the current code. README:118 CONFIRMED; README:363 comment "HTTP Basic auth, administrator password" understates (Bearer keys also work).
  • Web services: WebServicesHttpPort=0, WebServicesHttpsPort=0, WebServicesBindAddress=0.0.0.0, plus WebServicesCertificateFile/PrivateKeyFile="" (not in README) (IniFileSettings.cpp:602-606); paths served: /.well-known/mta-sts.txt, /.well-known/security.txt (hosted domains with a postmaster address; /security.txt too), /mail/config-v1.1.xml, /.well-known/autoconfig/mail/config-v1.1.xml, /autodiscover/autodiscover.xml, and /email.mobileconfig (alias /mail/config.mobileconfig; Apple profile — not in README). Since 6.2.25 the profile is HTTPS-only: over plain HTTP it answers 301 to the WebServicesHttpsPort listener when one is configured, 403 when none is, unless a TLS-terminating proxy sends X-Forwarded-Proto: https (source: Server/Common/Util/WebServicesServer.cpp:337-340, 630, 653-659, 758, 807). The listener is hosted on the shared HttpServer as of 6.2.28.
  • CardDAV (6.3.3) and CalDAV (6.3.4) on the same listener, under /dav/: /dav/addressbooks/<address>/contacts/ is the account's address book (RFC 6352) and /dav/calendars/<address>/calendar/ its calendar (RFC 4791, VEVENT and VTODO in one collection), each authenticated as the account with its password or an application password, each made the first time it is asked for. At master 540347500 an account has several calendars with colours and several address books, and the ones shared with it - calendars by an account or a group, address books by an account or a whole domain - appear in its home, beside the organisation's directory as a read-only collection (schema 6064, 6078, 6079, 6086). Discovery is /.well-known/carddav and /.well-known/caldav, which redirect to this server's own /dav/ unless CardDavRedirectUrl / CalDavRedirectUrl names another - and a configured value that cannot be used is refused with 404 rather than quietly falling back here (HM5780 names it). Both are served over HTTPS only: HTTP Basic would put an account's password on the wire in clear, so a client reaching /dav/ on the plain-HTTP listener is refused with that reason unless a TLS-terminating proxy sets X-Forwarded-Proto: https; with WebServicesHttpsPort=0 the start-up report says so by name. The calendar side is Common/Util/CalDavServer.cpp over Common/Util/CalendarStore.cpp (schema 6041) and Common/Util/ICalendar.cpp, the project's own RFC 5545 reader, writer and recurrence expander; at 3233b0cc4 it did not do iMIP, free-busy, a per-calendar time zone, partial calendar-data, or a time-range filter on an alarm; the tree since has iMIP (invitations sent, answered and filed on delivery; batch 12) and free-busy (GET /api/v1/me/freebusy and the free-busy-query report; batch 20), and the other three are still not done (source: Server/Common/Util/WebServicesServer.cpp:359-366, 386-390, 655-694, 1220-1240; CalDavServer.cpp:62; CardDavServer.cpp).
  • MtaStsHostingEnabled=1, MtaStsPolicyMode=enforce, MtaStsPolicyMaxAge=604800, MtaStsPolicyMx="", AutoconfigEnabled=1, AutoconfigClientHost="" (IniFileSettings.cpp:607-612) — README:350-355 CONFIRMED. Note these are inert until a WebServices port is non-zero.
  • Self-service portal (new in 6.2.28; a webmail since 6.3.0 — routed on the fragment so a folder, a message, a search and a draft each have an address, new mail noticed by polling GET /api/v1/me/changes every six seconds and not at all while the tab is hidden, folders created, renamed and deleted through POST/PUT/DELETE /api/v1/me/folders under IMAP's own rules, and inline images fetched by the page and handed to its sandboxed frame as data: URLs): GET /portal on the REST listener serves a sign-in page for an account's own credentials, backed by the /api/v1/me routes — state, password change, automatic reply, own quarantine (release to self / give up own copy), folders and messages (read, flags, move, delete, attachments as downloads, search, drafts, send through the SMTP-submission pipeline with a Sent copy), settings (name, forwarding, signature) and the active Sieve script checked as ManageSieve checks it. Browser sessions: POST /api/v1/session turns the password into a cookie the server knows only by its hash, bounded in idleness, life and number, ended by DELETE /api/v1/session or a password change; writes need a header a browser never adds on its own. An account that has enrolled a TOTP secret signs in with an app password, because its own password stops being a mailbox credential the moment the secret is set (PasswordValidator.cpp:189-191); the code itself is asked for only by POST /api/v1/me/password, in X-hMailServer-OTP (RestApiServer.cpp:4479-4485). Inert while RestApiPort=0 (source: RestApiServer.cpp:1281-1284, 1581-1738, 7660-7677; IniFileSettings.cpp:706 RestApiPort).
  • Secrets: ProtectStoredSecretsWithDPAPI=1; Blowfish values always still readable; on Windows a DPAPI failure falls back to Blowfish "so a secret is never lost" (IniFileSettings.cpp:236; Server/Common/Util/Crypt.cpp:198-206) — README:482 CONFIRMED. On Linux the store is a key file (<DataFolder>/.hmailserver-secret-key, AES-256-GCM, envelopes prefixed LINUX1:) and a store failure refuses rather than falling back (HM6416).
  • ServiceAccountName="", ServiceAccountPassword=""; NT SERVICE\hMailServer is the recommended value per code comment (IniFileSettings.cpp:241-247) — README:484 CONFIRMED. Since 6.3.4 /Register no longer reads either: it takes the account from --service-account virtual|localsystem|keep|DOMAIN\name$ alone, and a new installation runs as NT SERVICE\hMailServer (hmailserver/docs/ServiceAccount.md).
  • Other INI sections read by the server: [Security] AdministratorPassword; [Database] Server, Database, Username, Password, Internal, ServerFailoverPartner, Provider, Type, Passwordencryption, Port, NumberOfConnections=5, ConnectionAttempts=6, ConnectionAttemptsDelay=5; [Directories] ProgramFolder, DataFolder, TempFolder, EventFolder, DatabaseFolder, LogFolder; [GUILanguages] ValidLanguages; [Settings] UseLanguage=English (IniFileSettings.cpp:97-168,886,939)
  • [Database] also, since 6.2.25: AllowUnencryptedConnection=0 (described then as MySQL's fall-back from a required TLS; the test lab found on 25 September 2026 that MariaDB Connector/C never asked for TLS, and since 6.3.4 the key is read and changes nothing, TlsMode and TlsCaFile taking its place), PostgreSQLSslMode="" and PostgreSQLSslRootCert="" (libpq sslmode/sslrootcert; an unknown mode refuses the connection with the reason), ConnectionStringOptions="" (appended verbatim to the MS SQL OLE DB connection string, e.g. Encrypt=yes;TrustServerCertificate=no) (source: IniFileSettings.cpp:162-165) — verified by reading only, per the 6.2.25 release notes' known limitation 1.
  • [Security] AdministratorTotpSecret (DPAPI-protected; written by Settings.EnrolAdministratorTOTP, cleared to recover) (source: IniFileSettings.cpp:112,1171-1190) — 6.2.25.
  • [Security] StoredSecretsKey (6.3.4): the installation's stored-secrets key under this machine's DPAPI or the Linux key file, written only by an elevated step (the installer, --create-stored-secrets-key, --import-stored-secrets-key), never by the service; empty after an upgrade (source: IniFileSettings.cpp, StoredSecrets.cpp; hmailserver/docs/SecretsAtRest.md).
  • [Database] TlsMode and TlsCaFile (6.3.4): see section 3 (source: IniFileSettings.cpp ReadIniSettingString_("Database", ...)).

10. Installing

  • MinVersion=10.0.14393 (Windows 10 1607 / Server 2016), error text confirms 64-bit (source: section_setup.iss:24-28; section_messages.iss:3) — README:130 CONFIRMED.
  • Components: server, admintools ("registers the COM API for scripts"), controlpanel ("requires .NET 10 Desktop Runtime") (source: section_components.iss) — README:146 CONFIRMED.
  • Custom switch /useinternaldbms read as {param:useinternaldbms|true} (source: hMailServerInnoExtension.iss:616) — README:147 CONFIRMED.
  • .NET Desktop Runtime bundled as windowsdesktop-runtime-10.0-win-x64.exe, run /install /quiet /norestart, failure surfaced by exit code (hMailServer64.iss:17-18; hMailServerInnoExtension.iss:432-446) — README:116,125 CONFIRMED.
  • Registers COM via hMailServer.exe /RegisterTypeLib and the service via /Register, checking exit codes; sets service recovery via sc.exe; runs DBSetupQuick.exe and checks ResultCode (hMailServerInnoExtension.iss:735-805, 959-961) — README:161 CONFIRMED in outline.
  • Reads 32-bit InstallLocation under hMailServer_is1 (hMailServerInnoExtension.iss:242,605) — README:163 CONFIRMED.
  • README:158-159 (admin password not settable on the command line; silent upgrade needs it) — not verified against the Pascal; UNCONFIRMED.

11. Building (as README states; file existence only)

  • All referenced files exist: hMailServer.sln, hMailServer Tools.sln, ControlPanel.sln, hMailServer Tests.sln, build/build.ps1, post-build.ps1, build-tests.ps1, run-tests.ps1, build-tools.ps1, hMailServer64.iss (checked with [ -e ]).
  • Build steps, OpenSSL/Boost/libpq recipes (README:207-295) are procedural and were not executed — treat as README's claims.
  • AddXOriginalRcptTo=0 default; README:547 says set to 1 for the X-Original-Rcpt-To tests (IniFileSettings.cpp:399) — CONFIRMED.

12. Configuration reference — README keys vs code (all [Settings] unless noted)

Every key README names was found; defaults match README in every case. Line numbers are IniFileSettings.cpp.

Key Code default Line
MtaStsEnabled 1 402
DaneEnforcementEnabled 1 403
DnssecValidationEnabled 1 404
DnssecTrustAnchors "" 405
TlsKeyExchangeGroups X25519MLKEM768:SecP256r1MLKEM768:X25519:secp384r1:secp256r1 572
TlsCipherSuites13 "" 578
TlsSessionTicketsEnabled 1 584
TlsSessionCacheSize 0 585
TlsSessionTimeoutSeconds 0 586
TlsTicketKeyRotationSeconds 0 588
DkimOversignHeaders "" 474
DKIMSignatureValiditySeconds 0 (neg->0) 464
DKIMEnforceSignatureExpiry 1 468
DKIMExpiryClockSkewSeconds 300 (neg->0) 470
ArcSealingEnabled 0 455
TlsRptFromAddress "" 483
TlsRptOrganizationName hMailServer 484
AcmeEnabled 0 595
AcmeContactEmail "" 597
AcmeDomains "" 598
AcmeDirectoryUrl https://acme-v02.api.letsencrypt.org/directory 596
AcmeHttpPort 80 600
AcmeReuseKey 1 601
WebServicesHttpPort 0 602
WebServicesHttpsPort 0 603
WebServicesBindAddress 0.0.0.0 604
MtaStsHostingEnabled 1 607
MtaStsPolicyMode enforce 608
MtaStsPolicyMaxAge 604800 609
MtaStsPolicyMx "" 610
AutoconfigEnabled 1 611
AutoconfigClientHost "" 612
RestApiPort 0 591
RestApiBindAddress 127.0.0.1 592
RestApiCertificateFile "" 593
RestApiPrivateKeyFile "" 594
MetricsServerPort 0 426
MetricsServerBindAddress 127.0.0.1 427
MetricsServerAuthToken "" 433
MetricsServerAuthUsername "" 434
MetricsServerAuthPassword "" 435
MetricsServerCertificateFile "" 436
MetricsServerPrivateKeyFile "" 437
OtelEndpoint "" 448
OtelServiceName hmailserver 449
OtelMetricsEndpoint "" 450
OtelLogsEndpoint "" 451
OtelMetricsInterval 60 (clamp 5-3600) 452
WindowsEventLogEnabled 1 628
WindowsEventLogLevel 2 (clamp 1-4) 629
SMTPProxyProtocolEnabled 0 620
SMTPProxyProtocolTrustedIPs "" 621
SMTPXClientEnabled 0 622
SMTPXClientTrustedIPs "" 623
ScheduledBackupTime "" 444
ScheduledBackupIntervalMinutes 0 445
ScheduledBackupKeepCount 0 446
ScheduledBackupMaxAgeDays 0 447
LogDeleteDays 0 407
ShutdownDrainSeconds 0 408
MessageStoreFsync 0 410
MessageStoreConsistencyCheck 0 425
MinimumFreeDiskSpaceMB 100 (neg->0) 417
DiskSpaceWarningThresholdMB 1024 (neg->0) 419
DatabaseStatementTimeout 30 (neg->0) 645
IMAPExpungeRetentionRecords 5000 (neg->0) 646
DmarcRptSchemaVersion 1 (else->1, error 6210) 506
IndexerFullText 0 365
IndexerFullTextBatchSize 250 (1-100000) 372
IndexerFullTextMinTokenLength 3 (3-64) 382
IndexerFullTextMaxTokensPerMessage 2048 (64-1000000) 390
IMAPSearchTimeout 60 347
IMAPSearchMaxMegabytes 2048 349
ManageSieveServerPort 0 453
ManageSieveServerBindAddress 127.0.0.1 454
JsonLogging 0 406
ProtectStoredSecretsWithDPAPI 1 236
ServiceAccountName "" 246
ServiceAccountPassword "" 247
SRSEnabled 0 614
SRSSecret "" 615
MaxSubmissionsPerIPPerMinute 0 618
MaxOutboundPerDestinationPerMinute 0 673
AddXOriginalRcptTo 0 399
[LDAP] SyncScheduleMinutes 0 (15-10080 when set) LdapSettings.cpp:322

README names these, which are NOT INI keys: ASArcFilteringEnabled, ASArcTrustedSealers (hm_settings rows; Constants.h:118-119), SslCipherList (hm_settings; Constants.h:139), "Enable message indexing" = MessageIndexing (hm_settings; Constants.h:126), the IMAP SASL gate = EnableImapSASLPlain (hm_settings; Constants.h:146).

INI keys present in code but absent from README's configuration reference

Since 6.2.25 README:379-384 declares its [Settings] block the operator's shortlist ("about ninety further keys ... are documented one row each ... on the wiki's Settings Reference"), so absence from README is no longer a gap in the README; the list is kept for completeness. Keys not in the README block: AcmeCertificateDirectory (599), WebServicesCertificateFile/WebServicesPrivateKeyFile (605-606), BATVEnabled/BATVSecret (616-617), RewriteEnvelopeFromWhenForwarding (613), Pop3LoginDelaySeconds (619), QuarantineEnabled/QuarantineRetentionDays (624-625), MessageTraceEnabled/MessageTraceRetentionDays (626-627), DmarcTreeWalkEnabled (639), SpfVoidLookupLimit (640), RejectFullMailboxAtRcpt (641), DkimAcceptSha1 (642), QuotaWarningPercent (643), ArchiveRetentionDays (644), MetricsPerDomainEnabled (656), FilterHook* x5 (658-662), PasswordPolicy* x7 (666-672), DisableAUTHList (674), AuthenticationResultsEnabled/ReceivedSpfHeaderEnabled/AuthenticationResultsIdentity (480-482), DmarcRptFromAddress/DmarcRptOrganizationName (490-491), AVFailAction/AVFailRetryMinutes/AVFailMaxHolds (537-551), AccountLockout* x3 (558-560), SlowQueryLogMilliseconds (409), PreferredHashAlgorithm/MinimumAcceptedHashAlgorithm/PasswordPepper (171-203), OAuth2* x8 (208-229), OutboundOAuth2* x6 and FetchOAuth2Hosts (210-217), the timeout family (DNSQueryTimeout, ClientSessionCeiling, DBConnectionAcquireTimeout, ScriptTimeout, ExternalProcessTimeout, AsyncQueueStallThreshold, AsyncQueueReservedThreads, FinalizationTimeout, POP3/SMTP/SA/Clam min/max timeouts, 266-315), and the classic keys (MaxNumberOfExternalFetchThreads, AddXAuthUserHeader, AddXAuthUserIP, DaemonAddressDomain, Greylisting*, DNSBLChecksAfterMailFrom, SepSvcLogs, LogLevel, MaxLogLineLen, QuickRetries*, QueueRandomnessMinutes, MXTriesFactor, ArchiveDir, ArchiveHardLinks, SAMoveVsCopy, AuthUserReplacementIP, IndexerFullMinutes, IndexerFullLimit, IndexerQuickLimit, LoadHeaderReadSize, LoadBodyReadSize, BlockedIPHoldSeconds, SMTPDMaxSizeDrop, BackupMessagesDBOnly, UseDNSCache, DNSServer, UseLanguage). Test hooks SimulateSpoolWriteFailure, SimulateDatabaseFailureFor (422-423) should not be documented for operators.

Added since 6.2.25 (line numbers in the current IniFileSettings.cpp): OutboundPipelining/OutboundChunking (732-733), DeliveryHardLinks (312), ArchiveDomains (317), SpamAssassinUser/SpamAssassinUserFromRecipient/SpamAssassinLearnOnMove (341-354), ScriptAllowedObjects (389), BackupVerifyRestore (480), LogonTarpitSeconds/SmtpTarpitCount/SmtpTarpitDelaySeconds (675-679), SmtpAuthenticatedSenderCheck (734), MetricsHistoryDays (735), PasswordHashIterations/PasswordHashMemoryKB/PasswordHashTimeCost (233-235), OAuth2JwksUrl/OAuth2JwksCacheSeconds (265-266), OAuth2Introspection* x5 (271-275), [Database] AllowUnencryptedConnection/PostgreSQLSslMode/PostgreSQLSslRootCert/ConnectionStringOptions (162-165), [Security] AdministratorTotpSecret (112); and, new in 6.2.28: IMAPCompressionEnabled (497), HttpProxy (492 in the release tree) and the fourteen Update* keys (489-509).

13. Contradictions between README and code

  1. Schema version: README:110 "Database version 6027"; code REQUIRED_DB_VERSION 6031 (Constants.h:173). (README:35's "still missing" claim about LIST-STATUS, non-synchronising literals and BINARY, listed here before, has been corrected in the README.)
  2. REST OpenAPI document spelled /api/v1/api-keys while the router served /api/v1/apikeys — resolved: the served document now says /api/v1/apikeys (grep -c api-keys RestApiServer.cpp = 0 at 40ae9491d; router at :1554) — README:472 matches the router.
  3. README:363 comment says the REST API uses "HTTP Basic auth, administrator password"; code prefers Bearer API keys and still accepts Basic.
  4. Password hashing default is PBKDF2 (PreferredHashAlgorithm=4), while Pop3LoginDelay.h:9 says Argon2id is the default — code-internal; README:70 is silent on the default.

14. Underclaims / gaps (feature exists, README does not say so)

(README's [Settings] block is a declared shortlist since 6.2.25, so the configuration-key items below are gaps in feature prose, not in the reference.) SMTP BINARYMIME; 19 additional Sieve extensions; ManageSieve RENAMESCRIPT/UNAUTHENTICATE; 19 additional IMAP capabilities; REST /srv, /quarantine, /aliases, /openapi.json; /email.mobileconfig; per-port client-certificate policy; quarantine; message trace; external filter hook; password policy; account lockout; POP3 login delay; per-domain metrics; outbound XOAUTH2; AV-failure policy; Authentication-Results/Received-SPF headers; DMARC aggregate reporting sender; 15 named event scripts (README lists five categories); the bounded-wait timeout keys.

15. Unconfirmed (not verifiable from code, or not read in depth here)

  • MariaDB Connector/C auth plugin DLLs shipped alongside libmysql.dll (README:101).
  • SNI on inbound/outbound (README:42) — symbol hits only.
  • DNSSEC applied to SPF/DKIM/DMARC lookups (README:48) — only the DANE/MX path was read.
  • Argon2id "transparent upgrade on login" (README:70).
  • "Only the five system flags can be stored" via Sieve imap4flags (README:468).
  • Installer: admin password not settable from the command line; silent upgrade needs it (README:158-159).
  • LDAP "verified end to end against Windows Server 2025 DC" (README:69).
  • ".NET 10 still supports Server 2012" and the 12 January 2027 floor plan (README:130-132) — external/policy statements.
  • The IMAP4rev2 per-command deltas beyond RECENT/UNSEEN/ESEARCH were not enumerated.

Home — full index

Start here

1. Install and run

2. Secure it

3. Operate it

4. Extend it

5. Contribute to it

Look it up — from any journey


About this wiki

Clone this wiki locally