Skip to content

Runbooks Digest

chrisholloway5 edited this page Sep 8, 2026 · 4 revisions

Runbooks Digest

What each runbook in the repository covers, its procedures and prerequisites, and where its claims were checked 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). Sections headed Unconfirmed or Contradictions record what could not be verified or where documents disagreed, and are left in on purpose.

Scope: every file in hmailserver/docs/. Checked against the working tree at branch server-fixes-wave (HEAD 4d5da3f) on 2026-09-04; sections 1, 2, 6, 6a, 7, 8, 10, 12a and 13 re-checked against master 40ae9491d (the tree 6.2.28 was cut from later that day) on 2026-09-08. Paths below are relative to the repository root c:/Users/chris/Documents/projects/hmailserver/. Where a doc claim disagrees with the code, the code wins and the disagreement is listed under "Stale or wrong in the doc" for that document and again in the summary at the end.


0. Which runbook do I need?

This page is the digest — what each runbook covers and where its claims were checked. If you have a job to do rather than an audit to perform, start here.

flowchart TD
    Q["What are you doing?"]
    Q --> A["Moving to a newer release"]
    Q --> B["Mail is stuck"]
    Q --> C["Planning for the machine dying"]
    Q --> D["Changing database engine"]
    Q --> E["Bringing mailboxes in from<br/>another mail system"]
    Q --> F["Setting up a machine to run<br/>the regression suite"]
    Q --> G["Hunting parser bugs"]
    Q --> H["Answering a compliance or<br/>supply-chain question"]
    Q --> I["Building a monitoring dashboard"]

    A --> A1["docs/Upgrading.md<br/>wiki: Upgrading Guide"]
    B --> B1["docs/DiagnosingStalledMail.md<br/>wiki: Diagnosing Stalled Mail"]
    C --> C1{"How much downtime<br/>can you accept?"}
    C1 -- "minutes, and you have<br/>a second machine" --> C2["docs/HighAvailabilityRunbook.md<br/>wiki: High Availability Runbook"]
    C1 -- "hours, and you want<br/>something simpler" --> C3["docs/WarmStandby.md<br/>wiki: Warm Standby"]
    D --> D1["docs/MigratingDatabaseBackend.md<br/>wiki: Migrating the Database Backend"]
    E --> E1["docs/Migration.md - new in 6.2.27.<br/>IMAP mirror, mbox, Maildir,<br/>Outlook, upstream hMailServer"]
    F --> F1["docs/RegressionEnvironment.md<br/>wiki: Regression Test Environment"]
    G --> G1["docs/Fuzzing.md<br/>wiki: Fuzzing"]
    H --> H1["docs/RegulatoryScope.md and<br/>docs/ThirdPartyBinaries.md<br/>wiki: Regulatory Scope,<br/>Third-Party Binaries"]
    I --> I1["docs/grafana-dashboard.json<br/>wiki: Monitoring and Health"]
Loading

The docs directory, and where each one is mirrored here

Thirteen files plus a Licenses/ folder. Every runbook has a wiki page except Migration.md, which is the newest and has none yet.

hmailserver/docs/ Covers Wiki page "Verified against the code" stamp in the doc
README.md The index for the other twelve undated
Upgrading.md Release-to-release moves, what the schema upgrade touches, backup, failure, rollback, silent upgrade Upgrading Guide 21 Aug 2026
DiagnosingStalledMail.md Which half stalled — accepting or delivering — and the settings that bound a session Diagnosing Stalled Mail 13 Aug 2026
HighAvailabilityRunbook.md Two machines, shared storage, failover High Availability Runbook 13 Aug 2026
WarmStandby.md One spare machine, restored periodically Warm Standby 21 Aug 2026
MigratingDatabaseBackend.md Moving between MySQL, MS SQL, PostgreSQL and SQL CE Migrating the Database Backend undated
Migration.md (6.2.27) Bringing existing mailboxes in: IMAP mirror, mbox, Maildir, Outlook, upstream hMailServer none yet newest of the set
RegressionEnvironment.md Building the bench the suite needs Regression Test Environment undated
Fuzzing.md The MIME harnesses, the two oracles, what to do with a finding Fuzzing 13 Aug 2026
RegulatoryScope.md CRA and PLD determination, dated and reviewable Regulatory Scope determination 12 Aug, list 13 Aug 2026
ThirdPartyBinaries.md The 40 committed binaries, one by one Third-Party Binaries undated
grafana-dashboard.json 19 panel entries over the Prometheus metrics Monitoring and Health n/a
third-party-binaries.json The machine-readable manifest CI verifies n/a

The five migration routes, and what each keeps

From Migration.md, which is the newest runbook and the one with no wiki page of its own. What a route does not keep is the part worth reading before starting.

Route Mechanism Keeps Does not keep
From another IMAP server An external account with Mirror every folder on — FetchAccount.ServerType 1 plus FetchAccount.MirrorFolders, schema 6031 Folders and hierarchy (delimiter mapped), every message byte for byte, \Seen \Flagged \Answered \Draft \Deleted, the internal date Subscriptions, custom keywords, ACLs
From mbox files The Import Tool, one IMAP folder per file, via Utilities.ImportMessageFromFileToIMAPFolder The messages Flags
From a Maildir The Import Tool's Select Maildir button — reads cur/new/tmp and every Maildir++ folder beside it Flags from the :2, / ;2, / !2, file-name suffixes; line endings made CRLF
From Outlook Through IMAP: add this server as an IMAP account and drag folders across Whatever the client copies PST is deliberately not parsed
From upstream hMailServer In place, per Upgrading.md; accounts in bulk through the Import Tool's comma-separated account import Everything, including the data directory

Two things about the mirror route that surprise people, both deliberate: nothing is delivered, so no rule, anti-spam or anti-virus check touches a copy and OnExternalAccountDownload does not fire; and setting days to keep messages to 0 turns the mirror into a move. Use a new external account for a mirror rather than converting an existing fetch account.


1. The docs directory itself

  • hmailserver/docs/ holds 13 files + a Licenses/ folder: README.md, Upgrading.md, DiagnosingStalledMail.md, HighAvailabilityRunbook.md, WarmStandby.md, MigratingDatabaseBackend.md, Migration.md (6.2.27), RegressionEnvironment.md, Fuzzing.md, RegulatoryScope.md, ThirdPartyBinaries.md, grafana-dashboard.json, third-party-binaries.json (source: ls hmailserver/docs/)
  • README.md is the index; it says everything is "operator-facing" and points code readers to .github/CONTRIBUTING.md and RELEASE.md (source: docs/README.md:4-7)
  • README.md's table lists Migration.md (:14) and RegressionEnvironment.md (:16) but still NOT WarmStandby.md or Fuzzing.md (source: grep -c "WarmStandby\|Fuzzing" hmailserver/docs/README.md = 0) — GAP
  • README.md's closing paragraph still says "the two documents above both exist because a specific problem took far longer to diagnose" — written when there were two docs; there are now eleven (source: docs/README.md:36-39)
  • README "Elsewhere" table links: README.md, build/hmconfig.ps1, ARCHITECTURE.md, Roadmap.md, .github/SUPPORT.md, .github/SECURITY.md, RELEASE.md — all exist (source: ls of each)
  • build/hmconfig.ps1 apply is a dry run unless -Force; deletions additionally need -AllowDelete (source: build/hmconfig.ps1:42-57,85-87)
  • Licenses/ contains 10 RTF licence texts (7Zip, Boost, Fat Cow Icons, MIME Message encoder analyzer, Microsoft SQL Server Compact Edition, OpenSSL, PostgreSQL interface, RMSPF, String library, ZLib) plus License list.ods; there is NO LGPL text for MariaDB Connector/C (source: ls hmailserver/docs/Licenses/)
  • "Verified against the code" dates stamped in the docs: Upgrading 21 Aug 2026; DiagnosingStalledMail 13 Aug; HighAvailabilityRunbook 13 Aug; WarmStandby 21 Aug; RegulatoryScope determination 12 Aug / list 13 Aug; Fuzzing 13 Aug; ThirdPartyBinaries undated. Code has moved since on several of them (see below)

2. Upgrading.md

What it covers

  • Moving an installation to this fork or between releases; what the schema upgrade touches; backup; failure; rollback; silent upgrade; .NET 10 (source: docs/Upgrading.md:1-7)

Procedure ("Doing it")

  1. Back up both the database (native dump: mysqldump, pg_dump, SQL Server backup) and the data directory (source: Upgrading.md:81-86)
  2. Note current versions: server version in Control Panel; schema version via select value from hm_dbversion (single-column single-row table, not a settings row) (source: Upgrading.md:87-92)
  3. Run the new installer over the existing install: stops service, installs, upgrades schema, restarts (source: Upgrading.md:93-94)
  4. Check the service is listening and the ERROR log for the upgrade window is empty (source: Upgrading.md:95-97)
  5. Send a message to yourself end-to-end and read it back over IMAP/POP3 (source: Upgrading.md:98-99)

Facts verified against code

  • REQUIRED_DB_VERSION is 6031 (source: hmailserver/source/Server/Common/Application/Constants.h:173)
  • Upgrade chain: scripts Upgrade0to1100… through Upgrade6030to6031… exist (source: ls hmailserver/source/DBScripts/); 6025→6030 shipped in 6.2.25, 6030→6031 in 6.2.27
  • Per-dialect coverage: MSSQL and MySQL scripts exist from Upgrade0to1100; MSSQLCE and PGSQL scripts start at Upgrade5001to5002 (source: ls hmailserver/source/DBScripts/ — first CE/PGSQL files are Upgrade5001to5002MSSQLCE.sql, Upgrade5001to5002PGSQL.sql)
  • DBUpdater registers steps in formMain.LoadScripts(); last registered step is new UpgradeScript(6030, 6031) (source: hmailserver/source/Tools/DBUpdater/formMain.cs:159,242). After each step it runs a probe statement; since 6.2.26 the four foreign-key probes for 6030 read update hm_dbversion set value = value / (value - value) where not exists (select 1 from information_schema.table_constraints ...) because the earlier case when exists form crashed the SQL Server Compact provider and the crash oracle then stopped the service (#114); build/check-db-scripts.ps1 now executes every probe against a freshly created database through the provider the server uses, with a negative control that must fail (source: v6.2.26 body; build/check-db-scripts.ps1:126-156)
  • build/check-schema-versions.ps1 exists and reads REQUIRED_DB_VERSION from Constants.h and the step list (source: build/check-schema-versions.ps1:12,82-90)
  • The two refusal messages are verbatim in code: "The database is too new for this version of hMailServer. Please upgrade hMailServer." and "The database is too old for this version of hMailServer. Please run hMailServer Database updater (DBUpdater.exe) to upgrade it." — error 5011; refusal is in BOTH directions (< or > required version); a version that cannot be detected is error 5010 (source: hmailserver/source/Server/Common/Application/Application.cpp:251-275)
  • The server reads the version with select * from hm_dbversion (doc gives select value from hm_dbversion for the operator; column is value) (source: hmailserver/source/Server/Common/SQL/DatabaseConnectionManager.cpp:416)
  • Control Panel shows "Database schema version" on the Status > Server status page (source: hmailserver/source/Tools/ControlPanel/Views/StatusView.xaml.cs:165; NavigationMap.cs:207)
  • DBSetupQuick decides create vs upgrade by _application.Database.DatabaseExists and launches DBUpdater.exe from its own directory (AppContext.BaseDirectory) with /SilentIfOk, plus /silent if it was given /silent; returns DBUpdater's exit code (source: hmailserver/source/Tools/DBSetupQuick/Program.cs:29-62)
  • Installer runs {app}\Bin\DBSetupQuick.exe when a database type is configured or the internal engine is chosen, else {app}\Bin\DBSetup.exe; checks both launch success AND exit code (source: hmailserver/installation/hMailServerInnoExtension.iss:955-985)
  • .NET Desktop Runtime major is 10; bundled file name is windowsdesktop-runtime-<channel>-win-x64.exe; installed with /install /quiet /norestart only when missing (source: hmailserver/installation/hMailServer64.iss:16,18; hMailServerInnoExtension.iss:432)
  • DBSetup, DBSetupQuick and DBUpdater are .NET apps needing the same runtime (source: hmailserver/installation/section_files_64.iss:17)
  • hm_adsynchronization is dropped in step 5004→5005 on MSSQL, MySQL, MSSQLCE and PGSQL (select hm_drop_table('hm_adsynchronization')), and no CreateTables script creates it (source: hmailserver/source/DBScripts/Upgrade5004to5005PGSQL.sql:35; Upgrade5004to5005MSSQL.sql:101; Upgrade5004to5005MySQL.sql:35; Upgrade5004to5005MSSQLCE.sql:42; grep -il adsynchronization CreateTables*.sql = none)
  • Unattended install flags documented in root README: /SILENT, /VERYSILENT, example uses /VERYSILENT /SUPPRESSMSGBOXES /LOG= (source: README.md:134-152)

Stale or wrong in the doc

  • "carries on": doc says a non-zero DBSetupQuick exit shows a dialog and the installer "goes on to start the service and the wizard finishes" (Upgrading.md:104-113). Code now raises: if (RunPostInstallTasks() = false) then RaiseException('The hMailServer database could not be created or upgraded…') so the install FAILS with a non-zero exit code; the comment says the result "used to be discarded" (source: hmailserver/installation/hMailServerInnoExtension.iss:1211-1218)
  • Silent-upgrade hang: doc says DBSetupQuick.exe does not forward password: on the upgrade path and DBUpdater opens a modal dialog regardless of /silent (Upgrading.md:169-188). Code now forwards password:<value> on the upgrade path (source: Tools/DBSetupQuick/Program.cs:49-55), the installer passes password: + admin password to DBSetupQuick (source: hMailServerInnoExtension.iss:936-944, comment: "until 6.2.23 DBSetupQuick forwarded only /silent"), and Authenticator.AuthenticateUser tries empty → the password argument → each raw argument → then under /silent returns false instead of prompting (source: hmailserver/source/Tools/Shared/Miscellaneous/Authenticator.cs:24-49). The paragraph is stale as of 6.2.23.
  • PostgreSQL keeps hm_adsynchronization: doc says step 5004→5005 drops it "but not on PostgreSQL" (Upgrading.md:69-76). The PGSQL script drops it too (source: Upgrade5004to5005PGSQL.sql:35).
  • Doc's "Verified against the code" section (Upgrading.md:200-216) names the right functions but was written before the three changes above.

3. DiagnosingStalledMail.md

What it covers

  • Mail not moving while the server looks healthy; split into "Accepting" (sender times out after DATA) vs "Delivering" (accepted, never leaves) (source: DiagnosingStalledMail.md:14-28)
  • Origin: discussion #18, three releases to track down (source: DiagnosingStalledMail.md:7-9)

Procedure

  1. Decide which half: Postfix timed out while sending end of data = accepting; message visible in queue and never leaves = delivering (source: :20-26)
  2. Control Panel → Logging → tick "Debug messages"; reproduce once; turn it off afterwards (source: :33-38). Control Panel label confirmed: "Debug messages" bound to Logging.LogDebug (source: hmailserver/source/Tools/ControlPanel/Views/ServerSettingsView.xaml.cs:2045)
  3. Read the accept-pipeline stage lines in sequence; the stuck stage is the one after the last line written (source: :60-69)
  4. If every message stalls, look for the work-queue saturation lines (source: :111-131)
  5. For delivery: check scanner, slow remote, external tool, database; check delivery queue in Control Panel and the SMTP log (source: :133-156)
  6. Report: log from 354 onward with stage lines, ERROR_hmailserver_<date>.log, scanners/scripts enabled, whether the message arrives (source: :190-201)

Log lines (all verified verbatim)

  • SMTPConnection - accept: start spam-protection. (source: hmailserver/source/Server/SMTP/SMTPConnection.cpp:1732)
  • SMTPConnection - accept: done <stage> in <n> ms (session <id>). for stages spam-protection, message-modifications, script/save (source: SMTPConnection.cpp:1746,1750,1962,1993)
  • SMTPConnection - accept: start script/save. (source: SMTPConnection.cpp:1882)
  • There is NO start message-modifications line — the middle stage only has a done (source: SMTPConnection.cpp:1746-1750)
  • A stage ≥ 10 000 ms is logged at application level, otherwise debug (source: SMTPConnection.cpp:1999-2007)
  • Spam test: <name>, Score: <n>, Time: <n> ms; ≥ 10 000 ms escalates to application level (source: hmailserver/source/Server/Common/AntiSpam/SpamTestRunner.cpp:125-126)
  • Task {0} waited {1} seconds for a thread in work queue {2}. {3} task(s) are still queued. (source: hmailserver/source/Server/Common/Threading/WorkQueue.cpp:317)
  • All {0} threads in work queue {1} have been busy for at least {2} seconds, so no further task on this queue can start. {3} task(s) are queued. Running: {4} — error 5526, High (source: WorkQueue.cpp:566-569)
  • The queue is named "Asynchronous task queue" (source: hmailserver/source/Server/Common/Application/Application.cpp:87)
  • Finalization deadline: error 5525 (Medium) then 451 4.3.1 Server temporarily overloaded while accepting the message; please retry.; FinalizationTimeout <= 0 disables the deadline (source: SMTPConnection.cpp:2010-2024)
  • Database-unavailable recipient check answers 451 4.3.2 Unable to verify the recipient at the moment. Please retry later. instead of 550, in two DatabaseUnavailableMarker::Scope blocks (source: SMTPConnection.cpp:1218-1228,1380-1386)
  • Error log file name is ERROR_hmailserver_<date>.log (source: hmailserver/source/Server/Common/Application/Logger.cpp:477)

Settings table — every default verified in IniFileSettings::LoadSettings

  • [Settings] FinalizationTimeout default 240 (source: hmailserver/source/Server/Common/Application/IniFileSettings.cpp:281); consumed at SMTPConnection.cpp:2012
  • SAMaxTimeout default 90; SAMinTimeout default 30 (source: IniFileSettings.cpp:274-275)
  • ClamMaxTimeout default 90; ClamMinTimeout default 15 (source: IniFileSettings.cpp:282-283)
  • DNSQueryTimeout default 10 (source: IniFileSettings.cpp:293); consumed at Common/TCPIP/DNSResolverWinApi.cpp:120
  • ScriptTimeout default 60 (source: IniFileSettings.cpp:307); consumed in Common/Scripting/ScriptServer.cpp
  • ExternalProcessTimeout default 300 (source: IniFileSettings.cpp:308); consumed in Common/Util/ProcessLauncher.cpp
  • ClientSessionCeiling default 1800 (source: IniFileSettings.cpp:299); consumed at SMTP/SMTPClientConnection.cpp:65 via SetSessionCeiling
  • AsyncQueueStallThreshold default 120 (source: IniFileSettings.cpp:314); consumed at WorkQueue.cpp:451
  • DBConnectionAcquireTimeout default 60 (source: IniFileSettings.cpp:306); consumed at Common/SQL/DatabaseConnectionManager.cpp:436
  • DNSServer default "" (source: IniFileSettings.cpp:401)
  • SpamAssassin: idle timeout = TimeoutCalculator::Calculate(SAMinTimeout, SAMaxTimeout); session ceiling = SAMaxTimeout + 30 (source: hmailserver/source/Server/Common/AntiSpam/SpamAssassin/SpamAssassinClient.cpp:32,39)
  • ClamAV: per-operation timeout = Calculate(ClamMinTimeout, ClamMaxTimeout) (source: hmailserver/source/Server/Common/AntiVirus/ClamAVVirusScanner.cpp:62)
  • TimeoutCalculator::Calculate(min,max) starts at max, scales down with connection count, and clamps UP to min if below it — so max=0 yields min, never "unbounded" (source: hmailserver/source/Server/Common/Application/TimeoutCalculator.cpp:27-55; note the file lives in Common/Application/, not Common/Util/)

DNSServer warning (verified)

  • DNSResolverWinApi.cpp sets serverAddress.sin_port = 0 with a comment "Do not 'fix' this line"; port 53 returns status 87 and no records (source: hmailserver/source/Server/Common/TCPIP/DNSResolverWinApi.cpp:273-292)
  • Custom server list is applied together with DNS_QUERY_BYPASS_CACHE (source: DNSResolverWinApi.cpp:306)

Stale or wrong in the doc

  • Nothing contradicted. Minor: Logger.cpp is under Common/Application/, TimeoutCalculator under Common/Application/ (neither is in Common/Util/).

4. HighAvailabilityRunbook.md

What it covers

  • Active/passive topology with shared database + shared message store + VIP + health probe; no clustering code; active/active out of scope (source: HighAvailabilityRunbook.md:3-11)

Prerequisites

  • External database (MSSQL/MySQL/PostgreSQL), not SQL CE (source: :50-51)
  • Both nodes: identical [Database] INI settings and the same [Directories] DataFolder on shared storage; there is no setting called DataDirectory (source: :52,59-64). Confirmed: key is ("Directories","DataFolder") and the accessor is data_directory_ (source: IniFileSettings.cpp:129)
  • Other [Directories] keys that exist: ProgramFolder, TempFolder, EventFolder, DatabaseFolder, LogFolder (source: IniFileSettings.cpp:125-164,886)

Settings named (all in [Settings], defaults verified)

  • MetricsServerPort default 0 (off) (source: IniFileSettings.cpp:426)
  • MetricsServerBindAddress default 127.0.0.1 (source: IniFileSettings.cpp:427)
  • MetricsServerAuthToken, MetricsServerAuthUsername, MetricsServerAuthPassword, MetricsServerCertificateFile, MetricsServerPrivateKeyFile all default "" (source: IniFileSettings.cpp:433-437)
  • ShutdownDrainSeconds default 0 = no drain (source: IniFileSettings.cpp:408; Application.cpp:826-828)
  • Doc's example block: MetricsServerPort=8080, MetricsServerBindAddress=0.0.0.0, MetricsServerAuthToken=<32+ chars>, ShutdownDrainSeconds=30 (source: HighAvailabilityRunbook.md:87-93)

Probes (verified)

  • /livez → 200 alive whenever the listener is up; /readyz → 200 ready or 503 not ready: <reason>; /healthz → JSON, 200/503; all three are answered BEFORE any refusal branch (source: hmailserver/source/Server/Common/Util/MetricsServer.cpp:925-950)
  • /readyz is 200 only when running AND the database answered a probe within DatabaseProbeStalenessMilliseconds = 20000 (source: MetricsServer.cpp:108,2345)
  • /metrics answers 503 (not 401) on a non-loopback bind with no credential; the log line names MetricsServerAuthToken, the username/password pair, "or bind to 127.0.0.1 or ::1" (source: MetricsServer.cpp:375-397,415-419)
  • Bearer scheme: WWW-Authenticate: Bearer realm="hMailServer metrics" (source: MetricsServer.cpp:1404)
  • Loopback test accepts the whole of 127.0.0.0/8 and exactly ::1 (source: MetricsServer.cpp:632-672)
  • Request-read deadline 5000 ms, response-write deadline 15 000 ms, socket timeout 5000 ms (source: MetricsServer.cpp:59,65,76)
  • Invalid bind address logs MetricsServer: Invalid bind address: <addr> (source: MetricsServer.cpp:319)
  • Drain order: StopServers sets StateStopping first, then waits up to ShutdownDrainSeconds, then tears down listeners; metrics listener kept up through the drain (source: Application.cpp:808-851)
  • Regression coverage: hmailserver/test/RegressionTests/Infrastructure/HealthProbes.cs, MetricsSecurity.cs (source: ls)

Procedures

  • Planned failover: (1) stop service on A — /readyz 503 then drain; (2) verify /readyz on A refused; (3) move VIP; (4) start B, wait for /readyz 200; (5) send test message, check /healthz shows database: up, state: running (source: HighAvailabilityRunbook.md:155-166)
  • Unplanned: LB drops A; fence A; move VIP; start B; bring A back as passive (source: :172-185)
  • Validation checklist of 7 items (source: :191-203)

Stale or wrong in the doc

  • IPv4-only bind address: doc says MetricsServerBindAddress "takes an IPv4 literal and nothing else" and IPv6 is rejected (HighAvailabilityRunbook.md:112-117). Code: ParseBindAddress accepts IPv4 or IPv6 literals (family chosen by presence of a colon; scoped fe80::1%3 rejected) and IsLoopbackAddress_ accepts ::1 (source: MetricsServer.cpp:195-231,632-660). Host names are still rejected.
  • /healthz body: doc says it includes sessions per protocol (HighAvailabilityRunbook.md:130). Code: body is exactly {"status","state","database","uptime_seconds"}; session counts were deliberately removed from /healthz because it is unauthenticated (source: MetricsServer.cpp:2395-2412)
  • Doc's verification section (:220-235) predates both changes.

5. WarmStandby.md

What it covers

  • A second machine that takes over on failure; the four clauses: share the database, replicate the data directory, keep the standby's service stopped, DPAPI-protected passwords do not travel (source: WarmStandby.md:1-11)

Constraints (verified)

  • PersistentMessage::UnlockAll() runs update hm_messages set messagelocked = 0 where messagetype = 1 and messagelocked = 1 and is called from SMTPDeliveryManager::Start() (source: hmailserver/source/Server/Common/Persistence/PersistentMessage.cpp:226-228; SMTP/SMTPDeliveryManager.cpp:80)
  • Queue selection: select * from hm_messages where messagetype = 1 and messagelocked = 0 and messagenexttrytime <= <now> order by messagesize, messagecurnooftries, messageid asc in LoadPendingMessageList_ (source: SMTPDeliveryManager.cpp:115-127)
  • PersistentFetchAccount::UnlockAll() called from ExternalFetchManager at start (source: Persistence/PersistentFetchAccount.cpp:50; ExternalFetcher/ExternalFetchManager.cpp:71)
  • DPAPI: CryptProtectData(..., L"hMailServer secret", ..., CRYPTPROTECT_LOCAL_MACHINE, ...) (source: hmailserver/source/Server/Common/Util/DataProtector.cpp:38); Crypt::ProtectSecret (source: Common/Util/Crypt.cpp:191)
  • Protected columns: routeauthenticationpassword (PersistentRoute.cpp:74,145), fapassword (PersistentFetchAccount.cpp:102,167), sslprivatekeypassword (PersistentSSLCertificate.cpp:53,100), domainrelaypassword (PersistentDomain.cpp:484) — matches the doc's "route, fetch-account, per-domain-relay and SSL-key passwords"
  • [Settings] mirror: IniSettingStore mirrors the [Settings] section into hm_inisettings with a three-way merge; on both-changed conflict "the file wins" (source: hmailserver/source/Server/Common/Application/IniSettingStore.h:18,36,46; DBScripts/CreateTablesMSSQL.sql:337)
  • Certificate/DKIM key columns hold PATHS: hm_sslcertificates.sslcertificatefile, sslprivatekeyfile (CreateTablesMSSQL.sql:696-697); hm_domains.domaindkimprivatekeyfile (CreateTablesMSSQL.sql:251)
  • DatabaseStatementTimeout default 30 (source: IniFileSettings.cpp:645)
  • /readyz 200 = running and database answered within 20 s (source: MetricsServer.cpp:108,2345)
  • Schema pin refuses mismatch in both directions (source: Application.cpp:257-265)

Procedures

  • Building the standby: (1) same build, same drive letter/layout; (2) point [Database] at the same server, verify with Control Panel, then stop and set to Manual (not Disabled); (3) one-way replication primary→standby of message store + certs + DKIM keys (robocopy /MIR every few minutes is the baseline; DFS-R/storage replication OK; never SMB or CSV as the live store); (4) record the DPAPI credential list; (5) rehearse (source: WarmStandby.md:183-196)
  • Failover: (1) confirm primary is stopped/powered off/isolated; (2) stop replication; (3) start standby service; (4) move DNS/VIP/port-forward (keep MX TTL 300–3600 s); (5) re-enter route/fetch/relay passwords on the standby; (6) verify (source: :198-219)
  • Failback: resync data directory backwards first, stop standby, final delta, start primary, move endpoint, re-enter DPAPI credentials on primary, re-establish replication (source: :221-229)
  • Verify: GET /readyz 200; inbound + IMAP/POP3 read-back; one message through each credentialed route; check application log for ERROR since failover; confirm TLS certificate on 25/143/110/587/993/995 (source: :231-243)

Warnings

  • Two servers on one database double-deliver (startup unlock + uncoordinated selection); a POP3 fetch account fetched twice with delete-after-download splits mail (source: :16-34)
  • Never copy the primary's INI over the standby's — its [Database] Password is DPAPI-per-machine (source: :109-113). Confirmed: INI password written via protectedValue (source: IniFileSettings.cpp:1110)
  • SQL Server Compact is excluded from everything (source: :171-174)

Stale or wrong in the doc

  • None found. The SMB/CSV section is declared "operational judgement" not code fact (source: :263-267).

6. MigratingDatabaseBackend.md

What it covers

  • Moving from one of the four backends to another (in practice off SQL CE, which the installer still defaults to) via backup → repoint → restore; no migration tool exists or is needed (source: MigratingDatabaseBackend.md:1-13)

Why it works (verified)

  • Neither BackupExecuter nor BackupRestorer branches on database type (source: grep DatabaseType|MSSQL|MySQL|PostgreSQL over both files = no hits)
  • Account::XMLStore writes no id; PersistentAccount treats GetID() == 0 as new (source: hmailserver/source/Server/Common/BO/Account.cpp:296; Persistence/PersistentAccount.cpp:478-494)
  • On-disk message path: <DataFolder>\<domain>\<local-part>\<2 chars of guid>\<guid>.eml; public folder is <DataFolder>\#Public\<xx>\<guid>.eml; queue is <DataFolder>\<partial> (source: Persistence/PersistentMessage.cpp GetFileName(...FileLocation); IMAP/IMAPConfiguration.cpp:207-210 returns "#Public")
  • Backup refuses with All messages are not located in the data folder. if any row points outside DataFolder (source: Common/Application/BackupExecuter.cpp:369)
  • Archive name HMBackup <time>.7z; index hMailServerBackup.xml (source: BackupExecuter.cpp:432,443; BackupRestorer.cpp:24; BackupRetention.cpp:23 documents HMBackup YYYY-MM-DD HHMMSS.7z)
  • Restore refuses an archive lacking domains / settings / messages when that option is selected, each with a "Restore refused: …Nothing has been changed on this server." message (source: BackupRestorer.cpp:332,338,344,452)
  • Restore ends with Reinitializator::Instance()->ReInitialize() ("Reinitializing server (async)...") — the server reinitialises, it does not restart the Windows service (source: BackupExecuter.cpp:844-851)

Settings and commands (verified)

  • [Database] Type accepts MSSQL, MYSQL, PostgreSQL, MSSQLCE case-insensitively; Internal (0/1), Server, Database, Username, Password, Port (0 = default) (source: IniFileSettings.cpp:99-123)
  • [Settings] BackupMessagesDBOnly default 0; read by both BackupExecuter (362,736) and BackupRestorer (361) so it must match on both sides (source: IniFileSettings.cpp:397; BackupExecuter.cpp:362,736; BackupRestorer.cpp:361)
  • MSSQL session isolation: SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED (source: Common/SQL/ADOConnection.cpp:213)
  • MySQL: transactions only if every table reports InnoDB at connect (source: Common/SQL/MySQLConnection.cpp:494; DatabaseConnectionManager.cpp:566-578)
  • COM API names used in the doc's PowerShell all exist: Settings.Backup (IDL:605), IInterfaceBackupSettings.Destination/BackupSettings/BackupDomains/BackupMessages (IDL:2304-2312), BackupManager (IDL:1672), StartBackup(), LoadBackup(file) (IDL:2291-2292), IInterfaceBackup.RestoreDomains/RestoreSettings (COM/InterfaceBackup.cpp:85-133), StartRestore() (InterfaceBackup.cpp:19) (source: hmailserver/source/Server/hMailServer/hMailServer.idl; COM/InterfaceBackup.cpp)
  • DBSetupQuick.exe creates the schema when Database.DatabaseExists is false and returns a non-zero exit code on failure (source: Tools/DBSetupQuick/Program.cs:29)
  • DBSetup.exe (interactive wizard) exists (source: ls hmailserver/source/Tools/DBSetup)
  • Stop-Service hMailServer / Start-Service hMailServer — service name hMailServer (source: build/preflight-tests.ps1 uses hMailServer; hMailServerInnoExtension.iss:709 IsServiceRunning('hMailServer'))
  • SQL log device writes to table hm_log, created at runtime by SqlLogDevice (not by CreateTables scripts) — so it is indeed outside the backup (source: Common/Application/SqlLogDevice.cpp:296,731,750; grep hm_log CreateTablesMSSQL.sql = none)
  • [Database] also accepts (6.2.25): AllowUnencryptedConnection (0; MySQL fallback from TLS), PostgreSQLSslMode and PostgreSQLSslRootCert (""; libpq sslmode/sslrootcert — an unknown mode refuses the connection with the reason rather than falling back to prefer), ConnectionStringOptions (""; appended verbatim to the MS SQL OLE DB connection string) — the doc's backend table now shows the recommended values (ConnectionStringOptions=Encrypt=yes;TrustServerCertificate=no, PostgreSQLSslMode=verify-full + PostgreSQLSslRootCert, AllowUnencryptedConnection=1 for a MySQL with no TLS) (source: IniFileSettings.cpp:162-165; MigratingDatabaseBackend.md:61-63). Verified by reading only; the bench runs SQL Server Compact (v6.2.25 known limitation 1).

Procedure (doc's steps)

  1. Stop-Service hMailServer; restart for the backup itself; hold inbound traffic (source: :104-117)
  2. Backup with domains + settings + messages, via Control Panel or the COM script (source: :119-143); optionally BackupMessagesDBOnly=1 before both backup and restore, remove afterwards (source: :145-159)
  3. Create an empty target DB, edit [Database], run & "C:\Program Files\hMailServer\Bin\DBSetupQuick.exe" and check $LASTEXITCODE (source: :161-197)
  4. Start-Service hMailServer, LoadBackup(...), set RestoreDomains/RestoreMessages/RestoreSettings, StartRestore() (source: :199-215)
  5. Verify counts, IMAP visibility, a full message body, settings, send/receive, empty error log (source: :217-236)
  • Rollback: put the old [Database] section back; old DB untouched (source: :238-252)

Stale or wrong in the doc

  • Public folder directory name: doc says <DataFolder>\.Public_Folder\<xx>\<guid>.eml (:47). Code returns "#Public" (source: IMAPConfiguration.cpp:209).
  • Error log file name: doc says hmailserver_ERROR_<date>.log (:234). Code writes ERROR_hmailserver_<date>.log (source: Logger.cpp:477). DiagnosingStalledMail.md has it right.
  • Control Panel navigation: doc says "Settings → Advanced → Backup" (:121). hMailCP's map is Utilities > Backup / Utilities > Backup & restore (source: Tools/ControlPanel/Services/NavigationMap.cs:279-280). "Settings > Advanced" exists but holds Routes/IP ranges/SSL certificates/TCP-IP ports (NavigationMap.cs:227-231).
  • SQLLogging: doc says "If you use SQLLogging" (:267-268). No setting by that name exists; the SQL log device is selected by the database-held Logging.LogDevice property (PROPERTY_LOGDEVICE, COM hLogDeviceSQL) (source: Common/Application/Configuration.cpp:103-111; COM/InterfaceLogging.cpp:342-380).
  • "The server restarts itself" (:215): it reinitialises in-process via Reinitializator; the Windows service is not restarted (source: BackupExecuter.cpp:844-851).

6a. Migration.md (6.2.27)

What it covers

  • Bringing existing mailboxes into this server: from another IMAP server, from mbox files, from a Maildir, from Outlook, and from the upstream hMailServer in place; what each route keeps and what it does not; why PST is not parsed (source: Migration.md:1-8, 129). Points to Upgrading.md for release-to-release moves and MigratingDatabaseBackend.md for a change of database (:9-12)

Routes (verified)

  • Another IMAP server: an external account with Mirror every folder on — FetchAccount.ServerType 1 (IMAP) plus FetchAccount.MirrorFolders (schema 6031, hm_fetchaccounts.famirrorfolders); LIST "" "*", then per selectable mailbox SELECT, UID SEARCH ALL, UID FETCH (FLAGS INTERNALDATE BODY.PEEK[]) for every UID not on record; keeps folders and hierarchy (delimiter mapped), every message byte for byte, \Seen \Flagged \Answered \Draft \Deleted, the internal date; does not keep subscriptions, custom keywords, ACLs; nothing is delivered, so no rule, anti-spam or anti-virus touches a copy and OnExternalAccountDownload does not fire; days to keep messages 0 makes it a move; use a new external account for the mirror (source: Migration.md:26-87; hMailServer.idl:1854,1889; DBScripts/Upgrade6030to6031MSSQL.sql; v6.2.27 body)
  • mbox: the Import Tool, one IMAP folder per file, via Utilities.ImportMessageFromFileToIMAPFolder (IDL id 13); no flags (source: Migration.md:18-20, 88-100; hMailServer.idl:1209)
  • Maildir: the Import Tool's Select Maildir button reads cur/new/tmp and every Maildir++ folder beside it, flags from :2, / ;2, / !2, file names, line endings made CRLF (source: Migration.md:101-127; Tools/ImportTool/formChooser.Designer.cs:65; v6.2.27 body)
  • Outlook: through IMAP (add this server as an IMAP account, drag folders) — PST is deliberately not parsed (source: Migration.md:129-139)
  • Upstream hMailServer: in place, per Upgrading.md; accounts in bulk: the Import Tool's account import from a comma-separated text file (address, password, optional quota) (source: Migration.md:141-157)

Stale or wrong in the doc

  • None found; the doc is newer than every other runbook.

7. RegressionEnvironment.md

What it covers

  • Building a machine on which build/run-tests.ps1 passes; build/preflight-tests.ps1 is the machine-readable twin (source: RegressionEnvironment.md:1-18)
  • Warning: never run against production — authenticates as Administrator, binds live ports, wipes data (source: :17-18)

Pieces and steps (verified)

  • Suite size: 2127 [Test] attributes today ("roughly 2,000" in the doc since 6.2.28; the 6.2.28 release run reported 2,127 tests, 2,119 passed, 8 skipped) (source: grep -rn "^\s*\[Test\]" hmailserver/test/RegressionTests --include=*.cs | wc -l at f088336c2; v6.2.28 body)
  • Step 1: build/build.ps1 -Configuration Release, then build/post-build.ps1 -Configuration Release (self-elevates via Start-Process powershell -Verb runAs); service must point at hmailserver\source\Server\hMailServer\x64\Release\hMailServer.exe (source: build/post-build.ps1:8-9,17; preflight-tests.ps1:36-44)
  • Preflight check 1 reads the service PathName; recovery text: "post-build.ps1, re-apply the sc sdset AU grant, then delete HKLM\SOFTWARE\hMailServer in BOTH /reg:32 and /reg:64" (source: preflight-tests.ps1:42-44)
  • Preflight check 2: reg query "HKLM\SOFTWARE\hMailServer" /v InstallLocation in both views must FAIL (source: preflight-tests.ps1:54-59)
  • Provisioning list is in IMPLEMENTATION-NOTES.md under "Test-environment recipe (dev tree, no installer)"; sc sdset grant at line 334 (source: IMPLEMENTATION-NOTES.md:288,334)
  • build/make-hmailserver-writable.ps1 exists (source: ls build/)
  • Step 2: SQL CE 4.0 x64 package in hmailserver/installation/SQLCE/SSCERuntime_x64-ENU.msi (source: third-party-binaries.json path list); COM Database.CreateInternalDatabase(), DatabaseExists, Application.Reinitialize() exist (source: hMailServer.idl:1053,1055,1670)
  • Preflight check 4 compares DB version to REQUIRED_DB_VERSION ("Database schema at the required version") (source: preflight-tests.ps1:96); build/upgrade-test-database.ps1 and build/check-db-scripts.ps1 exist; RELEASE.md step 6 is build\check-db-scripts.ps1 (source: RELEASE.md:32)
  • Step 3: data folder C:\HMTest\Data is hard-coded in the preflight (source: preflight-tests.ps1:261)
  • Step 4: preflight expects Domains.Count -eq 1 and Settings.TCPIPPorts.Count -eq 4; listening check is on 25, 110, 143 only (source: preflight-tests.ps1:72-78,136-139)
  • Admin password testar: application.Authenticate("Administrator", "testar") (source: hmailserver/test/RegressionTests/Shared/TestSetup.cs:34; preflight-tests.ps1:69)
  • Step 5: ClamAV — preflight checks a listener on 3310; CustomAsserts starts C:\clamav\clamd.exe if no clamd process (source: preflight-tests.ps1:128-129; test/RegressionTests/Infrastructure/CustomAsserts.cs:179-184 — note the file is under Infrastructure/, not Shared/)
  • Step 6: service name SpamAssassinJAM checked by preflight; used by AntiSpam/SpamAssassin.cs and Infrastructure/CustomAsserts.cs (source: preflight-tests.ps1:133-134; grep)
  • Step 8: build/build-tests.ps1 restores via MSBuild /t:Restore /p:RestorePackagesConfig=true into hmailserver\test\packages and builds /p:Platform=x64 (source: build/build-tests.ps1:33-47)
  • Step 9: runner is hmailserver\test\packages\NUnit.ConsoleRunner.3.22.0\tools\nunit3-console.exe; assembly hmailserver\test\RegressionTests\bin\x64\Debug\RegressionTests.dll; -Where maps to --where; -StopOnError maps to /stoponerror (source: build/run-tests.ps1:6,16-17,25,28)
  • "Never abort a run" and "the run is void if anything changes" are RELEASE.md steps (source: RELEASE.md:103-105, step 9 — they moved down when steps 8b and 8c were added in 6.2.25)
  • preflight-tests.ps1 -Clean removes a stale ERROR log, leftover test-only INI keys, and empty orphan directories; the INI it inspects is hmailserver\source\Server\hMailServer\x64\Release\hMailServer.ini (source: preflight-tests.ps1:14-16,113,190-242,307)
  • Leftover keys the preflight scans for (exact list): DNSServer, Pop3LoginDelaySeconds, PasswordPolicyMinimumLength, PasswordPolicyRequireMixedCase, PasswordPolicyRequireDigit, PasswordPolicyRequireNonAlphanumeric, PasswordPolicyRejectCommon, QuarantineEnabled, PasswordPolicyHistoryCount, PasswordPolicyMaximumAgeDays, DmarcTreeWalkEnabled, AuthenticationResultsEnabled, DNSQueryTimeout, SpfVoidLookupLimit, RejectFullMailboxAtRcpt, DkimAcceptSha1, QuotaWarningPercent, ArchiveDir, ArchiveRetentionDays, MetricsPerDomainEnabled, PreferredHashAlgorithm, FilterHookUrl, FilterHookFailClosed, FilterHookTimeoutSeconds, FilterHookRejectScore, SMTPProxyProtocolEnabled, SMTPProxyProtocolTrustedIPs, SMTPXClientEnabled, SMTPXClientTrustedIPs, OtelEndpoint, OtelMetricsEndpoint, OtelLogsEndpoint; a key with an EMPTY value is not a leftover (source: preflight-tests.ps1:206-236)
  • Success line: Pre-flight passed - safe to run the suite. (source: preflight-tests.ps1:323)

Traps listed (doc claims; environmental, not code-checkable)

  • ESET + just send it; ProtonVPN/WireGuard loopback; CRLF checkout (.gitattributes); Mark-of-the-Web; "passes alone, fails in a group"; build number naming a dead tree (source: RegressionEnvironment.md:202-229)

Stale or wrong in the doc

  • Doc says "the suite's CustomAsserts hard-codes C:\clamav\clamd.exe" — true, but the file is test/RegressionTests/Infrastructure/CustomAsserts.cs (no Shared/CustomAsserts.cs).
  • Doc says the runner assembly is built "for x64" — run-tests.ps1 runs the Debug x64 assembly (bin\x64\Debug), not Release (source: build/run-tests.ps1:27-28). The doc does not say which configuration; a page writer should say Debug.
  • "hMailServer fails open when the scanner gives no verdict" (:123) — UNCONFIRMED; no explicit fail-open branch was found by grep in Common/AntiVirus/ (VirusScanner.cpp returns NoVirusFound at :412 when nothing found; error paths not traced).

8. Fuzzing.md

What it covers

  • libFuzzer + ASan harness for the MIME parser; HTML, iCalendar, vCard, Sieve/ManageSieve and the SMTP command parser are NOT covered (source: Fuzzing.md:10-13,367-383)
  • The harness lives in fuzz\ (source: :15). Directory contains Find-ClangCl.ps1, README.md, artifacts/, bin/, build/, build-fuzz.ps1, corpus/, dict/, findings/, harness/, make-corpus.ps1, regression/, run-fuzz.ps1 (source: ls fuzz/)

Why it waited (verified)

  • Server compiled /EHa: <ExceptionHandling>Async</ExceptionHandling> in both configurations (source: hmailserver/source/Server/hMailServer/hMailServer.vcxproj:79,150)
  • Common/Util/CrashOracle.{h,cpp} exist (source: ls)
  • Fuzz build uses /EHsc (source: fuzz/build-fuzz.ps1:299)

Build (verified)

  • Prerequisite: clang-cl with libFuzzer + ASan runtimes; portable LLVM to %LOCALAPPDATA%\Programs\LLVM-22.1.8 (doc example, external) or VS component Microsoft.VisualStudio.Component.VC.Llvm.Clang (source: Fuzzing.md:88-121)
  • .\fuzz\build-fuzz.ps1 [-Target <name>] [-BoostInclude <dir>] [-RuntimeLibrary MT|MD, default MT] (source: fuzz/build-fuzz.ps1:43-78)
  • Defines /D_DISABLE_STRING_ANNOTATION=1 /D_DISABLE_VECTOR_ANNOTATION=1; library objects -fsanitize=fuzzer-no-link,address (source: build-fuzz.ps1:284,304)
  • Boost from %hMailServerLibs%\boost_1_92_0, else $env:BOOST_INCLUDE_PATH, else -BoostInclude (source: build-fuzz.ps1:193-199; 1.92.0 since 6.2.25)
  • The server's HM_ASSERT (the lowercase assert sites were renamed to it on 5 September 2026) maps onto ASSERT in the shim, so -Asserts governs both; the suite counterpart is build\build.ps1 -Configuration Release -Asserts, where a violated assertion is HM6364 (source: Fuzzing.md:312,358-361, updated 5 Sep 2026)
  • A timed fuzz run is a release step: RELEASE.md step 8c runs each harness for 30 minutes on the release source and records the execution counts in the notes; step 8b runs the whole suite on the assertion build first (source: RELEASE.md:84-101; v6.2.25 verification line: 265,770 / 274,986 / 4,793,507 executions, no finding)
  • Seed corpus regenerated by make-corpus.ps1 (source: build-fuzz.ps1:456)
  • Three targets: mime_message_fuzzer, mime_header_fuzzer, mime_decode_fuzzer (source: build-fuzz.ps1:125-127; fuzz/harness/)
  • Shim header fuzz/harness/shim/stdafx.h (source: ls)

Run (verified)

  • .\fuzz\run-fuzz.ps1 -Target <t> [-Minutes N (default 5)] [-Jobs N] [-Replay] [-Reproduce <path>] [-MaxLen N (default 16384)]; passes -error_exitcode=77; exit 77 = finding (source: fuzz/run-fuzz.ps1:22,38-56,153,191)
  • Doc examples: -Minutes 5 smoke; -Replay; -Minutes 480 -Jobs 4 overnight; -merge=1 corpus minimisation; -minimize_crash=1 -runs=100000 -exact_artifact_path=... (source: Fuzzing.md:201-244,320-323)
  • Artifacts to fuzz\artifacts\<target>\crash-<sha1> (timeout-, oom-, leak-) (source: Fuzzing.md:264-266)
  • kMaxTraversalDepth = 8 in the harness (source: fuzz/harness/fuzz_mime_common.h:151)
  • mime_header_fuzzer reproduces Utilities::GetMimeHeader's call as shape 3 (source: fuzz/harness/mime_header_fuzzer.cpp:38,57)
  • BEncode/QEncode guard if (nMaxBlockSize < 3) present (source: hmailserver/source/Server/Common/Mime/MimeCode.cpp:681)
  • Committed reproducers: fuzz/regression/mime_decode_fuzzer/{heap-buffer-overflow-mimeencodedword-decode, out-of-memory-mimeunicodeencoder-encodevalue}, fuzz/regression/mime_message_fuzzer/{heap-buffer-overflow-findstring-boundary-scan, new-delete-type-mismatch-mimecodebase-nonvirtual-dtor}; no mime_header_fuzzer reproducers yet (source: ls fuzz/regression/*)
  • fuzz/.gitattributes: regression/** -text -diff, corpus/** -text -diff, artifacts/** -text -diff, regression/README.md text eol=crlf diff, dict/*.dict text eol=crlf (source: fuzz/.gitattributes)

Warnings

  • Exclude fuzz\ from AV real-time scanning; do not run on the mail server; a stack-overflow in hm_fuzz::ExerciseBody is a harness limit, not a finding (source: Fuzzing.md:249-259,290-295)

Stale or wrong in the doc

  • Doc's suite count was corrected in 6.2.28 and is no longer stale: "over 2,000 tests as of September 2026", against 2,127 run in the 6.2.28 gate (2127 [Test] attributes in the tree) — and the doc itself says treat it as a floor (source: Fuzzing.md:57-58; v6.2.28 body).
  • Doc does not mention fuzz/README.md, fuzz/dict/, fuzz/findings/ or make-corpus.ps1 by name (only "regenerates the seed corpus").

9. RegulatoryScope.md

What it covers

  • Dated self-assessment (12 Aug 2026): out of scope as a manufacturer under the EU Cyber Resilience Act (Reg. 2024/2847) and the revised Product Liability Directive (2024/2853); ineligible as an OSS steward (sole individual); triggers for re-doing it (source: RegulatoryScope.md:4-124)
  • Key dates quoted: CRA in force 10 Dec 2024; Art. 14 reporting from 11 Sep 2026; remaining obligations from 11 Dec 2027; PLD transposition 9 Dec 2026 (source: :32-35,74-75) — legal claims, not code-checkable
  • Distribution facts it depends on: AGPLv3, no paid edition/support/hosting, no telemetry, one individual maintainer, no donations (source: :19-27)
  • Not legal advice (source: :13-14)

"What we do anyway" — each item checked

  • SBOM workflow .github/workflows/sbom.yml exists, runs on release and manual release_tag dispatch; build/merge-native-dependencies-into-sbom.ps1 exists (source: ls; sbom.yml:27-33)
  • Binary inventory: 40 entries in third-party-binaries.json; verify-binary-provenance.yml runs on push and pull_request, warns (not fails) on remove-* dispositions (source: json artifact count; verify-binary-provenance.yml:35-38,89-90,152-153)
  • Signing: sign-release.yml uses sigstore/cosign-installer (pinned to v3.10.1 SHA), sign-blob/verify-blob bundles (source: sign-release.yml:32-33,113-114)
  • .github/SECURITY.md exists; security.txt served at /.well-known/security.txt by WebServicesServer — "for hosted domains with a postmaster address", no enable setting of its own (source: hmailserver/source/Server/Common/Util/WebServicesServer.cpp:476-479,891)
  • CodeQL now analyses BOTH csharp and c-cpp (source: .github/workflows/codeql.yml:88,229); scorecard.yml exists
  • Authenticode signing of the installer is still outstanding (doc claim; Roadmap-tracked) — not verified here

Stale or wrong in the doc

  • None found in the checkable half.

10. ThirdPartyBinaries.md and third-party-binaries.json

What they cover

  • Inventory and policy for the 40 committed binaries; JSON is the only place hashes live; CI (Binary provenance workflow) fails on hash change, missing file, or unlisted binary (source: ThirdPartyBinaries.md:4-19,152-171)

Inventory facts (verified against the JSON and the tree)

  • 40 artifacts; dispositions: retain 13, retain-review 13, remove-duplicate 10, remove-unused 3, retain-generated 1 (source: python count over third-party-binaries.json)
  • Authenticode: 12 Valid (10 signed CN=Microsoft Windows Software Compatibility Publisher = MSVC v145 CRT 14.51.36231.0; 2 CN=Microsoft Corporation, OU=MOPR = the SQL CE MSIs), 28 NotSigned (source: json authenticode/authenticode_signer counts)
  • ALL 40 files listed still exist in the tree, including the 3 remove-unused (installation/System Files/dnsapi.dll, installation/isxdl.dll, installation/SQLCE/SSCERuntime_x86-ENU.msi) and the 10 remove-duplicate (installation/Extras/libmysql.dll + Extras/plugin/*.dll) — nothing has been deleted since the doc was written (source: existence check per path)
  • JSON schema id hmailserver-third-party-binaries/1; policy field points at hmailserver/docs/ThirdPartyBinaries.md (source: third-party-binaries.json:2-3)
  • Per-entry fields: path, sha256, size, component, version, publisher, license, upstream, authenticode, authenticode_signer, disposition, used_by, why (source: third-party-binaries.json:6-20)
  • 7za.exe is 7-Zip 19.00; used via Compression::GetExecutableFullPath_() (source: json entry; hmailserver/source/Server/Common/Util/Compression.cpp:80)
  • MariaDB Connector/C 3.4.9: MySQLInterface.cpp builds <exe dir>\libmysql.dll and loads plugins via MYSQL_PLUGIN_DIR (source: Common/SQL/MySQLInterface.cpp:75,113)
  • ADO type libraries #imported at hmailserver/source/Server/hMailServer/stdafx.h:48 (x64) and :52 (x32) (doc says 47/51)
  • installation/section_files_common.iss:11-12 says atl70.dll "looks vestigial on a v145 build" and installs it to {sys}; :33,37 ship Extras\libmysql.dll and Extras\plugin\*.dll (source: section_files_common.iss)
  • hMailServerInnoExtension.iss: CheckPorts external from ISC.DLL at :107-108, called at :709; the eight isxdl_* externals at :111-133; x86 branch comment at :852 (doc says 94/695, 97–119, 839 — line-number drift only)
  • RMSPF.cpp calls LoadLibrary(_T("DNSAPI.DLL")) at hmailserver/source/Server/SMTP/SPF/RMSPF.cpp:421 (doc says 415; and it sits under #ifdef DNSAPI_SUPP, which is commented out at :51)
  • libraries/build-openssl.ps1, build-boost.ps1 and build-pgsql.ps1 (with build-common.ps1) replaced build-dependencies.ps1 in 6.2.25; each downloads one source archive from the project's own release server, verifies it against a SHA-256 pinned in the script and builds outside the tree; nothing fetched is committed and no script fetches any of the 40 listed binaries (source: ls libraries/; ThirdPartyBinaries.md:49-52, updated 5 Sep 2026)
  • The one manifest change since 6.2.24: hmailserver/source/Tools/Interop/Interop.hMailServer.dll regenerated by the 6.2.25 release build (sha256 f74c3e95…, 195,072 bytes) (source: third-party-binaries.json:412-414; commit f7418b81d)
  • hmailserver/source/Server/hMailServer/post-build.bat and hmailserver/source/Tools/Interop/README.md ("Regenerating" section, tlbimp) exist (source: ls; Interop/README.md:3-11)
  • No scorecard.yml / .github/scorecard.yml annotation file exists — consistent with the doc's "deliberately not been added yet" (source: ls)
  • Still no LGPL-2.1 text for Connector/C in docs/Licenses/ — consistent with "Not yet done" (source: ls hmailserver/docs/Licenses/)

Procedure: updating the manifest

  1. Change the file; 2. update sha256, size, version, upstream, authenticode — hash via (Get-FileHash <path> -Algorithm SHA256).Hash.ToLower(); 3. update the doc row; 4. push (source: ThirdPartyBinaries.md:176-184)
  • Warning: never paste the hash CI printed without knowing why it changed (source: :186-188)

Stale or wrong in the doc

  • Only line-number drift (listed above). Substantive claims hold.

11. grafana-dashboard.json

Structure (verified)

  • Title hMailServer, uid hmailserver-overview, schemaVersion 39, version 2, refresh 1m, default range now-24h; templating: datasource (prometheus) and instance from label_values(hmailserver_build_info, instance) (source: grafana-dashboard.json:3-31)
  • 19 entries in panels: 4 row headers (Health; Mail flow; Authentication and TLS; Latency and database), 5 stat panels, 10 timeseries panels (source: python count). docs/README.md's "19 panels" counts the row headers.
  • Stat panels: Server state, Database, Uptime, Missing message files, TLS certificate expiry (amber < 21 d = 1 814 400 s, red < 7 d = 604 800 s) (source: :40-132)
  • Timeseries: Delivery outcomes (stacked), Delivery queue depth + oldest age (right axis), Processed/spam/viruses, Active sessions, Authentication outcomes, TLS handshakes, Command processing latency p50/p95/p99, Database query latency p50/p95/p99, Database connection pool, Slow queries (source: :139-331)

Every metric the dashboard references exists in MetricsServer.cpp

  • hmailserver_auth_failures_total, hmailserver_auth_success_total, hmailserver_build_info{version,architecture,database_schema_version}, hmailserver_command_processing_seconds (histogram → _bucket{le}), hmailserver_database_connected, hmailserver_db_connections{state="busy"|"available"}, hmailserver_db_query_seconds (histogram), hmailserver_db_slow_queries_total, hmailserver_delivery_queue_messages, hmailserver_delivery_queue_oldest_message_age_seconds, hmailserver_messages_{bounced,deferred,delivered}_total, hmailserver_messagestore_missing_files, hmailserver_processed_messages_total, hmailserver_sessions{protocol="smtp"|"imap"|"pop3"}, hmailserver_spam_messages_total, hmailserver_start_time_seconds, hmailserver_state{state=...} (StateSet), hmailserver_tls_certificate_expiry_seconds{certificate=...}, hmailserver_tls_handshake_failures_total, hmailserver_tls_handshakes_total, hmailserver_viruses_removed_total (source: grep -o "hmailserver_[a-z_0-9]*" hmailserver/source/Server/Common/Util/MetricsServer.cpp | sort -u; labels at MetricsServer.cpp:1539-1543,1640,1661,1718-1720,1803-1811,2206)
  • hmailserver_database_up is NOT emitted (only mentioned in a comment at :1667) — the Database panel description's "_connected rather than _up" remains accurate

Metrics emitted but NOT on the dashboard (gaps a page writer may want to list)

  • hmailserver_database_probe_age_seconds, hmailserver_database_probe_success_timestamp_seconds (source: MetricsServer.cpp:1708-1710)
  • hmailserver_workqueue_depth{queue}, hmailserver_workqueue_blocking_tasks_waiting{queue} (source: MetricsServer.cpp:1566-1587)
  • hmailserver_metrics_unauthorized_requests_total (source: MetricsServer.cpp:1517-1519)
  • hmailserver_domain_messages_received_total, hmailserver_domain_messages_sent_total — only when [Settings] MetricsPerDomainEnabled=1 (default 0) (source: MetricsServer.cpp:1594-1601; IniFileSettings.cpp:656)

12. Cross-document contradictions and doc-vs-code drift (summary)

  • Upgrading.md vs installer: installer now FAILS (RaiseException, non-zero exit) on a failed DBSetupQuick; doc says it "carries on" (hMailServerInnoExtension.iss:1211-1218)
  • Upgrading.md vs DBSetupQuick/Authenticator: silent upgrade with an admin password no longer hangs — password: is forwarded and /silent fails instead of prompting (Program.cs:49-55; Authenticator.cs:31-49; iss:936-944 "until 6.2.23")
  • Upgrading.md vs DBScripts: PGSQL 5004→5005 DOES drop hm_adsynchronization (Upgrade5004to5005PGSQL.sql:35)
  • HighAvailabilityRunbook.md vs MetricsServer: bind address accepts IPv6 literals and ::1 loopback (MetricsServer.cpp:195-231,632-660); /healthz has no sessions field (MetricsServer.cpp:2405-2412)
  • MigratingDatabaseBackend.md vs code: public folder dir is #Public not .Public_Folder (IMAPConfiguration.cpp:209); error log is ERROR_hmailserver_<date>.log not hmailserver_ERROR_<date>.log (Logger.cpp:477); Backup is under Utilities not Settings > Advanced in hMailCP (NavigationMap.cs:279); no setting named SQLLogging — it is Logging.LogDevice = SQL (Configuration.cpp:103-111); restore reinitialises in-process rather than restarting the service (BackupExecuter.cpp:844-851)
  • MigratingDatabaseBackend.md vs DiagnosingStalledMail.md: the two docs spell the error-log filename differently; DiagnosingStalledMail is correct
  • docs/README.md omits WarmStandby.md and Fuzzing.md from its index; its closing "two documents above" is stale
  • Line-number citations in ThirdPartyBinaries.md have drifted (stdafx.h 47/51→48/52; Compression.cpp 81→80; RMSPF.cpp 415→421; iss 94/97–119/695/839→107/111–133/709/852)
  • Upgrading.md's DBUpdater.exe "from the installation's Bin folder": consistent — DBSetupQuick resolves DBUpdater beside itself and the installer runs {app}\Bin\DBSetupQuick.exe (Program.cs:39; iss:959)

12a. Since 6.2.24 (shipped in 6.2.25; all confirmed in the tree)

  • build/analyze.ps1 (5 Sep 2026): the static-analysis build - the server Release x64 rebuilt with MSVC /analyze (/p:EnablePREfast=true /p:RunCodeAnalysis=true), full log to logs/analyze-Release.log, one line per distinct finding to logs/analyze-warnings.txt, the project's own defect-class findings (C6xxx, C28182, C33xxx) printed apart from those under libraries/, Boost, SQL CE and the Windows SDK. It replaces the Release binaries with the analysis build's output, so build/build.ps1 -Configuration Release is run again before a regression gate. First full triage 5 Sep 2026: 632 distinct findings, 28 defect-class in own code, 20 fixed in 7 files, 8 left with the reason in Roadmap.md (Static-analysis backlog) (source: ls build/; v6.2.25 body).
  • Schema 6030 (5 Sep 2026): seventeen FOREIGN KEY ... ON DELETE CASCADE constraints (accounts/aliases/domain aliases/lists -> domain, list recipients -> list, route addresses -> route, fetch accounts/app passwords/password history/index state -> account, fetch UIDs -> fetch account, rule criteria/actions -> rule, group members -> group, message recipients/metadata/index terms -> message, expunged marks -> folder). The upgrade deletes the orphans each constraint would reject first (children before parents); MySQL gets ENGINE=InnoDB on every table involved. Columns where 0 is a value and two type-mismatched columns are deliberately without a key (roadmap row 'Referential integrity in the schema'). Infrastructure/ReferentialIntegrity proves refusal and cascade against the SQL CE file (source: DBScripts/Upgrade6029to6030MSSQL.sql, 17 FOREIGN KEY lines; v6.2.25 body — an earlier draft of this page said nineteen).
  • Authorisation choke point (5 Sep 2026): every folder-access decision is ACLManager::CheckPermission (enforcement off = everything allowed, the historical meaning) or ACLManager::CheckDelegatedRight (a right one account grants another - l under #Users, p for Send-As; enforcement off = nothing granted). GetPermissionForFolder only reports rights (MYRIGHTS, SELECT). build/check-authz-choke-point.py in style.yml fails any other caller. Behaviour change: with IMAP ACL off, rules and deliveries may file into public folders, as IMAP always allowed (source: Server/Common/Application/ACLManager.h:49,57; .github/workflows/style.yml:100).
  • Control Panel High Contrast (shipped in 6.2.25): MainWindow.ApplySavedTheme applies ApplicationTheme.HighContrast (Wpf.Ui's SystemColors dictionary) whenever SystemParameters.HighContrast is set, follows the switch at runtime (SystemParameters.StaticPropertyChanged) and ignores the light/dark toggle while it is on; ThemeTokens.IsHighContrast is the one answer and honours HMAILCP_HIGH_CONTRAST=1 so build/capture-cp.ps1 -Launch -HighContrast can capture the result on an ordinary desktop (source: Tools/ControlPanel/MainWindow.xaml.cs:60,873; build/capture-cp.ps1:11-13).

13. Unconfirmed (not found in code by grep; record as claims, not facts)

  • "hMailServer fails open when the scanner gives no verdict" (RegressionEnvironment.md:123) — no explicit branch located
  • "Authenticode signing of the installer is still outstanding" (RegulatoryScope.md:160) — Roadmap not checked for this row
  • The legal determinations in RegulatoryScope.md (CRA/PLD dates, Commission guidance C(2026) 5252) — not code-checkable
  • Grafana colour-blind validation claims in the dashboard __comment — not checkable here
  • The doc-level statement that the ADO .tlb files are "PE images despite the .tlb extension" — not re-checked

Clone this wiki locally