Skip to content

APIs Reference

chrisholloway5 edited this page Sep 5, 2026 · 33 revisions

APIs Reference

The REST API (listener, credentials, every route), the WebAdmin page, the web services listener (autoconfig, autodiscover, MTA-STS hosting, security.txt, ACME), the metrics and health listener, and the COM object model with verified script examples. 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: hMailServer 6.2.24 fork as checked out on 2026-09-04 (branch server-fixes-wave, HEAD 4d5da3f). Paths below are relative to hmailserver/source/ unless stated. RestApiServer.cpp = Server/Common/Util/RestApiServer.cpp, WebServicesServer.cpp = Server/Common/Util/WebServicesServer.cpp, MetricsServer.cpp = Server/Common/Util/MetricsServer.cpp, IniFileSettings.cpp = Server/Common/Application/IniFileSettings.cpp, Application.cpp = Server/Common/Application/Application.cpp, hMailServer.idl = Server/hMailServer/hMailServer.idl. Legacy doc = hmailserver documentation/hmailserver-documentation.md.

Every fact is one line with a source. Anything I could not confirm is under "Unconfirmed" at the end.


1. Four HTTP listeners, all off by default

  • There are FOUR separate HTTP(S) listeners, each with its own bind address and port, each disabled until its port is set to non-zero in [Settings] of hMailServer.ini (source: Application.cpp:495-548).
  • REST administration API: RestApiPort (default 0 = off), RestApiBindAddress (default 127.0.0.1), RestApiCertificateFile (default empty), RestApiPrivateKeyFile (default empty) (source: IniFileSettings.cpp:591-594).
  • Prometheus metrics + health probes: MetricsServerPort (default 0), MetricsServerBindAddress (default 127.0.0.1), MetricsServerAuthToken (empty), MetricsServerAuthUsername (empty), MetricsServerAuthPassword (empty), MetricsServerCertificateFile (empty), MetricsServerPrivateKeyFile (empty), MetricsPerDomainEnabled (default 0) (source: IniFileSettings.cpp:426-437, 656).
  • Public web services (MTA-STS hosting, autoconfig/autodiscover, security.txt, ACME challenges, CalDAV/CardDAV redirects): WebServicesHttpPort (default 0), WebServicesHttpsPort (default 0), WebServicesBindAddress (default 0.0.0.0), WebServicesCertificateFile (empty), WebServicesPrivateKeyFile (empty) (source: IniFileSettings.cpp:602-606).
  • ManageSieve (not HTTP, listed for completeness of "extra listeners"): ManageSieveServerPort (default 0), ManageSieveServerBindAddress (default 127.0.0.1) (source: IniFileSettings.cpp:453-454; started at Application.cpp:539-545).
  • Startup order after the mail servers are running: metrics listener, then OTel exporters, then REST, then web services, then ManageSieve (source: Application.cpp:495-548).
  • All four HTTP listeners are raw-socket, single-worker-thread servers speaking HTTP/1.0 with Connection: close on every response (source: RestApiServer.cpp:1931-1958; WebServicesServer.cpp:936-961; MetricsServer.cpp:2277).
  • A bind address of :: is made dual-stack (IPV6_V6ONLY cleared) on the REST, web-services and metrics listeners; a specific IPv6 literal is IPv6-only; scoped link-local literals (fe80::1%3) are rejected as invalid bind addresses (source: RestApiServer.cpp:562-596; WebServicesServer.cpp:300-320; MetricsServer.h:207-213).
  • Control Panel: the REST, metrics, OTel, ManageSieve and Windows-event-log settings live on the page Settings > Network > API & monitoring (page id api); API keys have their own page REST API keys (id apikeys); the web-services settings live on Settings > Network > Web services & autoconfiguration (id webservices); ACME on Settings > Security > Certificates (ACME) (id acme) (source: Tools/ControlPanel/Services/NavigationMap.cs:261-267, 417, 426, 584, 603; Tools/ControlPanel/Services/SettingsSearchIndex.g.cs:107-128, 351-359).

2. REST API - listener and TLS rules

  • The REST listener refuses to start when the administrator password ([Security] AdministratorPassword) is empty; log line "RestApi: Refusing to start - the administrator password is not set." (source: RestApiServer.cpp:685-689; password read at IniFileSettings.cpp:97).
  • TLS is mandatory unless RestApiBindAddress is exactly 127.0.0.1, localhost or ::1; otherwise the listener refuses to start with "TLS certificate is required unless bound to 127.0.0.1 or ::1. Set RestApiCertificateFile and RestApiPrivateKeyFile." (source: RestApiServer.cpp:699-706).
  • TLS is used when BOTH RestApiCertificateFile and RestApiPrivateKeyFile are non-empty; when the certificate setting is empty, Application.cpp substitutes the ACME certificate (<AcmeCertificateDirectory>\fullchain.pem + privkey.pem) if both files exist, logging "RestApi: Using the ACME certificate for HTTPS." (source: Application.cpp:510-533; RestApiServer.cpp:691).
  • The TLS context comes from the shared SslContextInitializer::InitServer (same cipher list, protocol toggles, DH params and TlsKeyExchangeGroups as the mail protocols), then a TLS 1.2 floor is applied on top (source: RestApiServer.cpp:709-770, floor at 762).
  • localhost is translated to 127.0.0.1 before binding (source: RestApiServer.cpp:774).
  • Startup log line: "RestApi: Listening on : (https)" or "(http, loopback only)" (source: RestApiServer.cpp:808-810).
  • Request limits: whole request (headers + body) capped at 64 KB -> 413 {"error":"request too large"}; 10 s socket timeout per read; 30 s total deadline per request -> 400 {"error":"malformed request"}; any NUL byte in the request -> 400 (source: RestApiServer.cpp:64-68, 460-534, 920-931).
  • Per-credential rate limit: 200 requests per fixed 10-second window per credential identity (administrator or key:<id>); over budget -> 429 {"error":"too many requests"} with Retry-After: 10; refused requests count towards the window; the table holds at most 64 credentials (source: RestApiServer.cpp:158-171, 1163-1226, 2005-2013).
  • Failed authentication feeds the same per-IP auto-ban accounting as SMTP/IMAP/POP3 (AccountLogon::RegisterFailedLogin with label "REST API"); loopback peers (127.0.0.1, ::1) are excluded; when the auto-ban trips, the address is additionally refused at accept() (before TLS, before any DB access) for 5 minutes, in an in-process set bounded at 256 addresses (source: RestApiServer.cpp:230-232, 1238-1340, 1344-1407).
  • Every response body is JSON (Content-Type: application/json); status codes known to BuildResponse_ are 200, 201, 400, 403, 404, 409, 413, 429; anything else is rewritten to 500 (source: RestApiServer.cpp:1931-1958).
  • 401 responses carry WWW-Authenticate: Basic realm="hMailServer" and the body {"error":"authentication failed"}; the same 401 is returned for no credential, wrong password, unknown key, expired key and out-of-network key (deliberately indistinguishable) (source: RestApiServer.cpp:1961-1982).
  • Every request is wrapped in an OpenTelemetry server span (continuing an inbound traceparent) that is a no-op unless OtelEndpoint is set (source: RestApiServer.cpp:1428-1441).
  • The JSON request parser is a minimal string-value extractor: only string values are read, keys are looked up independently, \", \\ and \/ escapes are honoured (source: RestApiServer.cpp:3449-3494).

3. REST API - credentials

3a. HTTP Basic with the administrator password

  • Authorization: Basic base64("Administrator:<password>"); the user name must be administrator (case-insensitive); the password is validated against [Security] AdministratorPassword via Crypt::Validate so it works whether the ini holds a hash or plain text (source: RestApiServer.cpp:1066-1093).
  • The administrator credential is full authority: not read-only, not domain-restricted, and it is the ONLY credential that can list/create/revoke API keys (source: RestApiServer.cpp:1049-1057, 1843-1859).
  • A rejected Basic credential logs "REST API: administrator authentication failed." and registers an auth failure; presenting no credential at all is not logged (source: RestApiServer.cpp:1059-1063).
  • Verified example (tests use this exact form): curl -u Administrator:<password> http://127.0.0.1:9104/api/v1/status - regression fixture RestApiApiKeys runs the listener on loopback port 9104 with admin password testar over plain HTTP (source: hmailserver/test/RegressionTests/API/RestApiApiKeys.cs:58-64).

3b. Bearer API keys

  • Authorization: Bearer hmapi_<64 lower-case hex chars>; a token is hmapi_ + 32 random bytes (RAND_bytes) as hex; the key id is 8 random bytes as 16 hex chars (source: RestApiServer.cpp:93-103, 2474-2478).
  • Bearer is tried first when present; a token with the wrong length/prefix/alphabet is rejected before the store is read (source: RestApiServer.cpp:1023-1041, 1095-1109).
  • Only the SHA-256 (unsalted, lower-case hex) of a token is stored; comparison is constant-time over every stored record (source: RestApiServer.cpp:2058-2085, 1114-1125).
  • Key store file: hMailServerApiKeys.ini in the same directory as hMailServer.ini; one [Key.<id>] section per key with Label, Hash, Expires (YYYY-MM-DD HH:MM:SS local), AllowedFrom, Scope, Domains (source: RestApiServer.cpp:2017-2055).
  • The store is re-read on EVERY authentication attempt (parsed from bytes, not the cached profile API), so creating, revoking, hand-editing or deleting the file takes effect on the next request with no restart (source: RestApiServer.cpp:2075-2085, 2098-2105).
  • Fail-closed rules: a record whose Hash is not 64 lower-case hex is ignored (logged as "RestApi: Ignoring a record ... with no usable Hash value"); a missing/unparseable Expires counts as expired; Scope is read-only unless it is the literal full (case-insensitive); an AllowedFrom that cannot be parsed refuses every request; Domains entries are trimmed/lower-cased, empty list = every domain (source: RestApiServer.cpp:2116-2135, 2189-2232, 2238-2254, 2257-2283).
  • AllowedFrom forms: single address, lower-upper range, or CIDR (IPv4 prefix <= 32, IPv6 <= 128); families must match the peer (source: RestApiServer.cpp:317-431, 2257-2283).
  • Expired or out-of-network keys are logged by label ("was presented after it expired" / "outside its allowed source") and answered 401 (source: RestApiServer.cpp:1128-1150).
  • Default lifetime when a create request names no expires: 90 days; there is no "never expires" option (source: RestApiServer.cpp:105-108, 2431-2440).
  • Default scope when a create request names no scope: readonly (source: RestApiServer.cpp:2358-2370).
  • Scope enforcement: read-only keys are refused (403 {"error":"this api key is read-only"}) on the mutating route kinds: create/revoke key, create/delete account, queue retry/delete, quarantine release/delete (source: RestApiServer.cpp:1797-1820, 1861-1865).
  • Domain enforcement: a key with a Domains list may only list/create accounts and list aliases in those domains, may only delete an account whose address's domain is in the list (403 "this api key is not permitted for that domain"), sees only its domains in GET /api/v1/domains and GET /api/v1/srv, and is refused the queue routes ("...the delivery queue is server-wide") and the quarantine routes ("...the quarantine is server-wide") with 403 (source: RestApiServer.cpp:1867-1928, 2664-2700, 3384-3395).
  • Any API key (any scope) presented to /api/v1/apikeys* (including unsupported verbs under that prefix) gets 401, not 403, so a key cannot learn anything about key management (source: RestApiServer.cpp:1779-1795, 1851-1859).

3c. Managing keys from the Control Panel

  • The Control Panel page "REST API keys" reads, creates and revokes keys by editing hMailServerApiKeys.ini directly (no server round trip), so it works while the REST listener is off; the store path is derived from hMailServer.INI's location and is null (page explains keys are server-local) when the panel is not on the server (source: Tools/ControlPanel/Services/ApiKeyStore.cs:74-96, 128-146; Views/ApiKeysView.cs:17-38, 357-372).
  • The panel mirrors the server constants: prefix hmapi_, 32 secret bytes, 8 id bytes, 90-day default, 64-char label cap, SHA-256 over ASCII, Hash written last (source: ApiKeyStore.cs:99-125, 341-352, 407-411).
  • The create dialog defaults to read-only; the expiry date picker defaults to today + 90 days, must be at least tomorrow, and the stored expiry is the END of the chosen day (23:59:59) (source: Views/ApiKeysView.cs:614-676, 693, 725).
  • The new token is shown once in a card with "Copy to clipboard" and is cleared when the page is left (source: Views/ApiKeysView.cs:99-103, 174-232).
  • The panel's preamble for a new store differs in wording from the server's (mentions the Control Panel page as a revoke route) but the field semantics are identical (source: ApiKeyStore.cs:587-618 vs RestApiServer.cpp:2493-2521).

4. REST API - complete route table

Routing is exact-match on method and path after stripping any query string; StartsWith/EndsWith comparisons are case-insensitive, == is case-sensitive (source: RestApiServer.cpp:1418-1425, 1580-1588). "Admin" = administrator password only. "RO ok" = a read-only key may call it. "Dom" = how a domain-restricted key is treated.

Method Path Auth RO ok Dom Returns / notes Source (RestApiServer.cpp)
GET / or /index.html none - - WebAdmin page (text/html); see section 5 1446-1447, 2615-2640
GET /api/v1/status Basic or Bearer yes allowed {"version","state","processedMessages","spamMessages","virusesRemoved","sessions":{"smtp","imap","pop3"}}; state 0 Unknown, 1 Stopped, 2 Starting, 3 Running, 4 Stopping 1607-1611, 2642-2662; states Server/Common/Util/ServerStatus.h:18-22
GET /api/v1/domains Basic or Bearer yes filtered JSON array [{"name","active"}] 1613-1617, 2664-2707
GET /api/v1/domains/<name>/accounts Basic or Bearer yes must match array [{"address","active"}]; 404 domain not found 1620-1637, 2709-2749
POST /api/v1/domains/<name>/accounts Basic or full key no must match body {"address","password"} (only these two fields are read); 201 {"address","created":true}; 400 if fields missing, address not in domain, or PreSaveLimitationsCheck fails (message passed through); 404 unknown domain; 409 account already exists; creates the INBOX; password hashed with PreferredHashAlgorithm 1638-1643, 2751-2869
DELETE /api/v1/accounts/<address> Basic or full key no address's domain must match 200 {"deleted":true}; 404 account not found 1646-1658, 2871-2885
GET /api/v1/queue Basic or Bearer yes REFUSED 403 {"count","messages":[{"id","created","from","recipients","next_try","locked","tries"}]} (same query as COM Status.UndeliveredMessages) 1660-1664, 2887-2930
POST /api/v1/queue/<id>/retry Basic or full key no REFUSED 403 200 {"retried":true} after DeliveryQueue::ResetDeliveryTime + StartDelivery; 404 unless the id is a Delivering (type 1) or ETRN-held (type 3) message 1667-1687, 2932-2970
DELETE /api/v1/queue/<id> Basic or full key no REFUSED 403 200 {"deleted":true}; 404 as above 1689-1707, 2972-2983
GET /api/v1/tlsa Basic or Bearer yes allowed {"host","count","records":[{"certificate","spki_sha256","record":"_25._tcp.<host>. IN TLSA 3 1 1 <hex>"}]} for every configured SSL certificate file, falling back to the ACME fullchain.pem; host placeholder <your-mx-hostname> when no host name is configured 1710-1714, 3141-3195
GET /api/v1/srv Basic or Bearer yes filtered {"target","count","services":[{"service","priority","weight","port"}],"records":[{"domain","service","record"}]}; services _imaps/_imap/_pop3s/_pop3/_submissions/_submission._tcp derived from enabled, non-loopback ports (port 25 never advertised), plus _autodiscover._tcp when the web-services HTTPS listener is RUNNING and AutoconfigEnabled=1; one record set per active domain; target = AutoconfigClientHost else host name else <your-mail-hostname> 1716-1720, 3197-3437
GET /api/v1/quarantine Basic or Bearer yes REFUSED 403 array [{"id","sender","recipients","subject","reason","score","size","created"}], newest 1000 1722-1727, 2985-3018
POST /api/v1/quarantine/<id>/release Basic or full key no REFUSED 403 200 {"released":true}; 404 quarantined message not found; 500 with the store's error text 1729-1745, 3020-3039
DELETE /api/v1/quarantine/<id> Basic or full key no REFUSED 403 200 {"deleted":true}; 404 1747-1757, 3041-3052
GET /api/v1/domains/<name>/aliases Basic or Bearer yes must match array [{"name","value","active"}]; 404 unknown domain 1760-1772, 3054-3095
GET /api/v1/openapi.json Basic or Bearer yes allowed a static OpenAPI 3.0.3 document (see contradictions - it is not fully accurate) 1775-1776, 3097-3139
GET /api/v1/apikeys Admin - - {"count","keys":[{"id","label","scope","domains","expires","allowed_from","expired"}]}; hashes never returned 1590-1605, 2285-2324
POST /api/v1/apikeys Admin - - body {"label"(required, <=64 chars, no control chars),"scope"("readonly" or "full", default readonly),"domains"(comma list, validated with IsValidDomainName),"expires"(YYYY-MM-DD HH:MM:SS, default +90 days, must be future),"allowed_from"}; 201 {"id","label","scope","domains","expires","allowed_from","key"} - key is the clear-text token, shown once 2326-2569
DELETE /api/v1/apikeys/<id> Admin - - 200 {"revoked":true}; 404 api key not found (id must be 16 lower-case hex) 2571-2613
any anything else under /api/v1/apikeys/ Admin - - 404 for the administrator, 401 for a key 1590-1605, 1851-1859, 1559-1560
any anything else authenticated - - 404 {"error":"not found"}; unhandled exception -> 500 {"error":"internal error"} 1555-1566
  • Ids in queue/quarantine paths must be 1-18 decimal digits and > 0, otherwise the route is unknown (404) (source: RestApiServer.cpp:435-448).
  • Domain/account path segments may not contain /; query strings are ignored everywhere (source: RestApiServer.cpp:1418-1425, 1627, 1651).

5. The WebAdmin page (/)

  • GET / and GET /index.html are the ONLY unauthenticated routes; the server serves <ProgramDirectory>\WebAdmin\index.html as text/html; charset=utf-8, or, when that file is absent, a one-paragraph placeholder "Web administration page not installed. The REST API is available under /api/v1/." (source: RestApiServer.cpp:1442-1447, 2615-2640).
  • The installer ships WebAdmin\index.html into {app}\WebAdmin with the server component (source: hmailserver/installation/section_files_64.iss:12; file at hmailserver/installation/WebAdmin/index.html, 19,471 bytes).
  • The page is a single static HTML/JS file with no external resources; it authenticates with HTTP Basic only (btoa(user + ":" + pass)), keeps the credential in sessionStorage["hmsAuth"], validates it by calling GET /api/v1/status, and logs out by clearing sessionStorage (source: WebAdmin/index.html:178-211).
  • Views: Dashboard (status KPIs + SMTP/IMAP/POP3 session counts, polled every 3 s), Domains (with per-domain account list, create account = POST accounts, delete account = DELETE), Delivery queue (retry / delete), DANE / TLSA (copy-all of /api/v1/tlsa records); theme toggle stored in localStorage["hmsTheme"] (default dark) (source: WebAdmin/index.html:193-198, 213-370).
  • The page does NOT use API keys, /api/v1/srv, /api/v1/quarantine, /api/v1/aliases or /api/v1/apikeys (source: grep of index.html - only status/domains/accounts/queue/tlsa paths appear).
  • A 401 from any call clears the stored credential and reloads to the login gate (source: WebAdmin/index.html:184, 237).

6. Web services listener (WebServicesServer)

6a. Listener behaviour

  • Started only when WebServicesHttpPort > 0 or WebServicesHttpsPort > 0; both default to 0 (source: Application.cpp:535-547; WebServicesServer.cpp:573-592).
  • HTTPS: uses WebServicesCertificateFile/WebServicesPrivateKeyFile, falling back to the ACME fullchain.pem/privkey.pem when either is empty; with no certificate at all the HTTPS listener is silently kept DOWN and the HTTP one still runs ("No TLS certificate available yet. The HTTPS listener is disabled until a certificate exists (enable ACME or set WebServicesCertificateFile).") (source: WebServicesServer.cpp:594-679).
  • TLS context is the shared SslContextInitializer one with a TLS 1.2 floor (source: WebServicesServer.cpp:617-660).
  • At every service start (even with both ports 0) the server logs which enabled features are unreachable: "WebServices: these features are enabled but unreachable, because no web services listener is configured: ... Nothing answers those URLs until WebServicesHttpPort and/or WebServicesHttpsPort is set..." plus a note that MTA-STS needs the HTTPS port specifically; a plain-HTTP-only listener with MTA-STS hosting on logs a second warning (source: Application.cpp:516-533; WebServicesServer.cpp:373-531).
  • Requests are unauthenticated; 64 KB request cap; 10 s socket timeout; every request wrapped in an OTel span (source: WebServicesServer.cpp:41-42, 236-280, 862-873).
  • Response status codes known: 200, 301, 400, 404; anything else becomes 500 (source: WebServicesServer.cpp:936-961).

6b. Route table (all unauthenticated)

Method Path Gate Behaviour Source (WebServicesServer.cpp)
GET /.well-known/acme-challenge/<token> always returns the key authorisation from AcmeChallengeStore (text/plain) or 404; token must have no / and be <= 256 chars 880-884, 1010-1023
GET /.well-known/mta-sts.txt MtaStsHostingEnabled=1 (default 1) Host header must be mta-sts.<domain>; domain must be a local ACTIVE domain; body version: STSv1, mode: <MtaStsPolicyMode>, one mx: per host, max_age: <MtaStsPolicyMaxAge>; else 404 886-889, 1105-1152
GET /.well-known/security.txt and /security.txt always (no setting) served when the Host (or its parent after dropping one label) is an active local domain whose Postmaster is a real address; body Contact: mailto:<postmaster>, Expires: now+90 days (RFC 3339 UTC), Policy: https://github.com/Progressiverobot/hmailserver/blob/master/.github/SECURITY.md, Preferred-Languages: en; else 404 891-895, 1154-1243; constants 50-54
ANY /.well-known/caldav, /.well-known/carddav CalDavRedirectUrl / CardDavRedirectUrl set 301 to the configured absolute URL; 404 when unset; an unusable value (not http(s)://, >512 chars, non-printable) is reported once per start as error 5780 and answers 404; hMailServer implements no CalDAV/CardDAV itself 897-903, 1245-1347
GET /mail/config-v1.1.xml, /.well-known/autoconfig/mail/config-v1.1.xml AutoconfigEnabled=1 (default 1) Thunderbird clientConfig XML; domain from autoconfig.<domain> Host, else emailaddress= query, else client host; authentication is password-cleartext, username %EMAILADDRESS% 905-911, 1473-1543
GET /email.mobileconfig, /mail/config.mobileconfig AutoconfigEnabled=1 Apple com.apple.mail.managed profile (application/x-apple-aspen-config), IMAP preferred over POP3, needs both an incoming and an SMTP port else 404; stable name-based UUIDs; address pre-filled only from a plausible emailaddress= query 913-918, 1658-1802
POST or GET /autodiscover/autodiscover.xml (case-insensitive) AutoconfigEnabled=1 Outlook POX Autodiscover response with IMAP/POP3/SMTP <Protocol> blocks; LoginName from the POX <EMailAddress> 920-925, 1545-1622
any other - 404 not found (text/plain) 927

6c. Settings that shape the answers

  • CalDavRedirectUrl and CardDavRedirectUrl are read straight from [Settings] with GetPrivateProfileString (no IniFileSettings accessor), cached 60 s, so they can be changed without a restart (source: WebServicesServer.cpp:61, 105-125, 1245-1273).
  • MtaStsPolicyMode (default enforce; accepted enforce|testing|none, anything else -> enforce), MtaStsPolicyMaxAge (default 604800; clamped to 86400..31557600), MtaStsPolicyMx (comma list overriding the mx lines; default empty = the domain's live MX records, cached 1 h, falling back to the server host name) (source: IniFileSettings.cpp:608-610; WebServicesServer.cpp:44, 1025-1103, 1128-1137).
  • MtaStsEnabled (default 1) is the OUTBOUND "honour recipient policies" switch and is unrelated to hosting; hosting is MtaStsHostingEnabled (source: IniFileSettings.cpp:402, 607).
  • AutoconfigClientHost (default empty = Settings.HostName) is the host name written into autoconfig/autodiscover/mobileconfig and used as the SRV target; ports per protocol are chosen from the configured TCP/IP ports, ranked implicit TLS (100) > STARTTLS required (90 on 587, else 80) > STARTTLS optional (70/60) > plain (10) (source: IniFileSettings.cpp:612; WebServicesServer.cpp:1349-1442; RestApiServer.cpp:3218-3225).
  • Autoconfig reads the configured port table, not the running listeners, so a port added since the last restart is advertised early (source: RestApiServer.cpp:3210-3217 comment; WebServicesServer.cpp:1394).
  • DNS the README requires per domain: mta-sts.<domain>, autoconfig.<domain>, autodiscover.<domain> pointed at the server and included in AcmeDomains (source: README.md:358).

6d. ACME

  • ACME settings: AcmeEnabled (default 0), AcmeDirectoryUrl (default https://acme-v02.api.letsencrypt.org/directory), AcmeContactEmail (empty), AcmeDomains (empty), AcmeCertificateDirectory (empty = <DataDirectory>\ACME), AcmeHttpPort (default 80), AcmeReuseKey (default 1) (source: IniFileSettings.cpp:595-601; Server/Common/Util/AcmeClient.cpp:248-256).
  • Certificates are written as fullchain.pem / privkey.pem (plus account.key) in that directory (source: AcmeClient.h:4-8, 79-80).
  • http-01 challenges: if the web-services HTTP listener owns AcmeHttpPort, it serves them via /.well-known/acme-challenge/; otherwise AcmeClient starts a transient AcmeChallengeServer on AcmeHttpPort for the duration of the issuance (logged at startup when AcmeEnabled=1 and the ports differ) (source: AcmeClient.cpp:1066-1079; WebServicesServer.cpp:361-364, 435-441).
  • The REST listener and the web-services HTTPS listener both fall back to the ACME certificate when no certificate of their own is configured (source: Application.cpp:513-527; WebServicesServer.cpp:599-610).

7. Metrics and health listener (MetricsServer)

  • Enabled by MetricsServerPort > 0; bind MetricsServerBindAddress (default 127.0.0.1) (source: Application.cpp:495-500; IniFileSettings.cpp:426-427).
  • Only GET is recognised; paths are matched whole and case-insensitively; query strings stripped (source: MetricsServer.cpp:1224-1264, 913-928).
Path Auth Returns Source (MetricsServer.cpp)
/livez never 200 alive - no dependency checks 930-936
/readyz never 200 ready when ServerStatus is Running AND the DB pool is populated AND the last DB probe succeeded AND it succeeded within 20 s; else 503 not ready: <reason> 937-944, 2304-2364; constants 99, 108
/healthz never JSON with status, server state (running/starting/stopping/stopped/unknown), database and uptime seconds; 200 when running and DB answering, else 503; session counts deliberately NOT included 945-950, 2367-2440
/metrics see rules Prometheus text exposition text/plain; version=0.0.4 951-976
other - 404, empty body 978-981
  • The DB probe is a real select * from hm_dbversion every 5 s on a refresher thread; nothing on the request path touches the database or the file system (source: MetricsServer.cpp:99, 1824-1986; MetricsServer.h rules 5-6).
  • /metrics access rules: on a loopback bind (all of 127.0.0.0/8 and ::1) it is open unless a credential is configured; on a non-loopback bind with NO credential it answers 503 with a body naming MetricsServerAuthToken / MetricsServerAuthUsername+MetricsServerAuthPassword (the listener still starts and the probes still answer) (source: MetricsServer.cpp:301-470, 632-675, 1415-1460).
  • Credentials: Authorization: Bearer <MetricsServerAuthToken> and/or Authorization: Basic base64(<MetricsServerAuthUsername>:<MetricsServerAuthPassword>); when both are configured either satisfies; token and user name are trimmed, the password is not; setting only one half of the Basic pair disables Basic and logs a warning (source: MetricsServer.cpp:344-365, 1311-1392).
  • A wrong credential is answered 401 and counted in hmailserver_metrics_unauthorized_requests_total; it does NOT feed auto-ban (source: MetricsServer.cpp:962-972, 1394-1413; MetricsServer.h "What is deliberately NOT here").
  • TLS on this port is opt-in: both MetricsServerCertificateFile and MetricsServerPrivateKeyFile must be set; then the WHOLE port is HTTPS, probes included; if TLS is configured but cannot be prepared, the probes are served over plain HTTP and /metrics answers 503 (source: MetricsServer.cpp:367-390, 462; MetricsServer.h rule 3).
  • Non-loopback bind with a credential but no TLS logs a clear-text warning but still starts (source: MetricsServer.cpp:404-413).
  • Startup log line states the access shape, e.g. "MetricsServer: Listening on 127.0.0.1:8080 over plain HTTP. /metrics is open, loopback bind only. /livez, /readyz and /healthz are always served without authentication." (source: MetricsServer.cpp:510-530).
  • Metric families served (names as in code): hmailserver_auth_failures_total, hmailserver_auth_success_total, hmailserver_build_info, hmailserver_command_processing_seconds (histogram), hmailserver_database_connected, hmailserver_database_probe_age_seconds, hmailserver_database_probe_success_timestamp_seconds, hmailserver_database_up, 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_domain_messages_received_total and hmailserver_domain_messages_sent_total (only with MetricsPerDomainEnabled=1), 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{state=...}, 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 (source: grep of "hmailserver_ in MetricsServer.cpp; per-domain gate at 1598).
  • Queue depth/age and certificate expiry are read from caches refreshed on demand by the refresher thread, so an unscraped listener costs nothing (source: MetricsServer.cpp:1988-2216).
  • Related OTel settings (same counters pushed over OTLP/HTTP): OtelEndpoint (traces, empty = off), OtelMetricsEndpoint, OtelLogsEndpoint, OtelServiceName (default hmailserver), OtelMetricsInterval (default 60 s) (source: IniFileSettings.cpp:448-452; README.md:87).
  • A Grafana dashboard for these metrics ships at hmailserver/docs/grafana-dashboard.json (source: ls hmailserver/docs).

8. COM API - object model overview

  • Type library hMailServer (uuid DB241B59-A1B1-4C59-98FC-8D101A2995F2, version 1.0, helpstring "hMailServer Type Library"); 94 coclasses; the root coclass is Application (CLSID D6567EF8-0A6C-48E7-9288-A2463123C2F3, default interface IInterfaceApplication) (source: hMailServer.idl:3218-3223, 3446-3453; grep -c coclass = 94).
  • ProgID hMailServer.Application is what scripts and the Control Panel instantiate (CreateObject("hMailServer.Application") in VBScript, New-Object -ComObject 'hMailServer.Application' in PowerShell, Type.GetTypeFromProgID("hMailServer.Application"[, host]) in the Control Panel) (source: Addons/Utilities/StartBackup.vbs:15; Tools/ControlPanel/Services/ServerSession.cs:431-432; legacy doc ch. 23).
  • All interfaces are dual IDispatch interfaces, so late binding (VBScript, PowerShell, dynamic) works without registering the type library; early-bound clients depend on vtable order, which is why new members are always appended (source: hMailServer.idl:1678-1686 comment; README.md:146).
  • The installer component admintools registers the type library "so scripts can administer a remote instance"; the Control Panel does not need it (binds late) (source: README.md:146).
  • Every session starts with Application.Authenticate(Username, Password) returning an Account (null on failure); the administrator logs in as Administrator with [Security] AdministratorPassword; AuthenticateWithCode(Username, Password, Code) is the TOTP variant (source: hMailServer.idl:1672, 1687; ServerSession.cs:441).
  • IInterfaceApplication members: Start, Stop, Settings, Domains, ServerState (eServerState: 0 Unknown, 1 Stopped, 2 Starting, 3 Running, 4 Stopping), Database, Utilities, SubmitEMail, Status, Version, Connect, InitializationFile, Reinitialize, Rules (global), BackupManager, GlobalObjects, Authenticate, Links, Diagnostics, VersionArchitecture, AuthenticateWithCode (source: hMailServer.idl:1655-1688; enum at 153-165).
  • Object tree (collections -> items):
    • Application.Domains -> Domain (Name, Active, Postmaster, MaxSize, MaxMessageSize, plus-addressing, greylisting, Save, Delete) -> Accounts, Aliases, DistributionLists, DomainAliases (source: hMailServer.idl:800-919, 1698-1717).
    • Domains and Accounts collections offer Item, Count, Add, Refresh, ItemByDBID, ItemByName/ItemByAddress, DeleteByDBID (source: hMailServer.idl:919-938, 1698-1717).
    • Account members include Address, Password, Active, MaxSize, Size, QuotaUsed, AdminLevel, Messages, IMAPFolders, Rules, FetchAccounts, vacation/forwarding/signature properties, SieveScript, AppPasswords, TOTPEnabled, EnrolTOTP, DisableTOTP, SpamMarkThreshold, SpamDeleteThreshold, ExportMessages, ValidatePassword, UnlockMailbox, Save, Delete (source: hMailServer.idl:939-1044; 1010-1014).
    • Application.Settings is the root for server-wide settings and sub-objects: AntiSpam (which owns Quarantine), AntiVirus, Backup, Cache, Directories, Groups, IncomingRelays, Logging, MessageIndexing, PublicFolders, Routes, Scripting, SecurityRanges, ServerMessages, SSLCertificates, TCPIPPorts, plus ~150 scalar properties (HostName, MaxMessageSize, SMTPRelayer*, TlsVersion1xEnabled, IMAP*, etc.) and SetAdministratorPassword, GetIniSetting/SetIniSetting/DeleteIniSetting/IniSettingNames (source: hMailServer.idl:550-800; Quarantine at 2595).
    • Application.GlobalObjects -> DeliveryQueue (Clear, ResetDeliveryTime, StartDelivery, Remove), Languages, MessageTrace (source: hMailServer.idl:2351-2386).
    • Application.Status -> UndeliveredMessages (tab-separated text), StartTime, ProcessedMessages, RemovedViruses, RemovedSpamMessages, SessionCount(eSessionType), ThreadID; eSessionType: 1 SMTP, 2 SMTPClient, 3 POP3, 4 POP3Client, 5 IMAP (source: hMailServer.idl:532-540, 362-376).
    • Application.Utilities -> DNS/validation helpers (GetMailServer, IsValidEmailAddress, IsValidDomainName, IsValidIPAddress, ResolveMXRecords), hashing (MD5, SHA256, Blowfish), MakeDependent, ImportMessageFromFile[ToIMAPFolder], EmailAllAccounts, PerformMaintenance, CheckSieveSyntax, EvaluateSieveScript, SendTlsRptReports, SendDmarcReports, EraseAddressTraces (source: hMailServer.idl:1168-1211).
    • Application.Database -> ExecuteSQL, ExecuteSQLWithReturn, transactions, RequiresUpgrade, CreateInternalDatabase/CreateExternalDatabase, EnsurePrerequisites, IsConnected, DatabaseType (eDBtype: 1 MySQL, 2 MSSQL, 3 PostgreSQL, 4 MSSQLCE) (source: hMailServer.idl:1045-1078, 107-126).
    • Application.BackupManager -> StartBackup, LoadBackup (source: hMailServer.idl:2289-2302).
    • Application.Links -> Domain(DBID), Account(DBID), Alias(DBID), DistributionList(DBID) (source: hMailServer.idl:3100-3106).
    • Application.Diagnostics -> DiagnosticResults -> DiagnosticResult (source: hMailServer.idl:3178-3220).
  • COM rights: methods that require server-administrator rights return an error (since 6.2.10 they no longer return S_OK on refusal) (source: legacy doc ch. 23 "Authorization note"; unverified against code by this reader - see Unconfirmed).

9. COM API - verified script examples

  • VBScript backup (ships in the repo; replace the password): Set oApp = CreateObject("hMailServer.Application") / Call oApp.Authenticate("Administrator", sAdminPassword) / Call oApp.BackupManager.StartBackup() (source: Addons/Utilities/StartBackup.vbs:12-20; StartBackup at hMailServer.idl:2289-2302).
  • VBScript service dependency: oApp.Utilities.MakeDependent(sName) after Authenticate (source: Addons/Utilities/MakeServiceDependent.vbs:4-19; MakeDependent at hMailServer.idl:1175).
  • Other shipped utilities: DecryptBlowfish.vbs, EncryptAllPasswords.vbs, LoadAllMessages.vbs, RemoveUnusedAccounts.vbs, ChangeMySQLCollation.sql (source: ls Addons/Utilities).
  • The PowerShell example in legacy doc ch. 23 is consistent with the IDL: Domains.Add(), $domain.Name/.Active/.Save(), Accounts.Add(), $account.Address/.Password/.Active/.MaxSize/.Save(), $a.Size all exist (source: hMailServer.idl:800-830, 919-1000, 1698-1717). Note Account.Size helpstring is "The size of the account" and the doc divides it by 1MB - unit not verified here.
  • Event-script example in legacy doc ch. 23 uses oMessage.HeaderValue("X-...") = value and oMessage.Save - both exist on IInterfaceMessage (propput HeaderValue, Save) (source: hMailServer.idl:1079-1130).

10. Event scripts (server-side scripting)

  • Enabled via Settings.Scripting.Enabled, language Settings.Scripting.Language (VBScript/JScript), script file <events dir>\EventHandlers.<vbs|js>, with Reload, CheckSyntax, Directory, CurrentScriptFile on IInterfaceScripting (source: hMailServer.idl:1992-2004; Server/Common/Scripting/ScriptServer.cpp:170, 299).
  • Hook functions the server looks for, by name: OnClientConnect, OnAcceptMessage, OnDeliverMessage, OnBackupCompleted, OnBackupFailed, OnDeliveryStart, OnError, OnDeliveryFailed, OnExternalAccountDownload, OnSMTPData, OnHELO, OnClientLogon, OnClientValidatePassword, OnRecipientUnknown, OnTooManyInvalidCommands (source: ScriptServer.cpp:235-249).
  • OnAcceptMessage is invoked as OnAcceptMessage(HMAILSERVER_CLIENT, HMAILSERVER_MESSAGE) (source: Server/SMTP/SMTPConnection.cpp:2234).

11. Contradictions found (documents vs code)

  1. OpenAPI document path for API keys is wrong. GET /api/v1/openapi.json documents /api/v1/api-keys and /api/v1/api-keys/{id}, but the router only recognises /api/v1/apikeys and /api/v1/apikeys/<id> (source: RestApiServer.cpp:3125-3128 vs 1590-1605). A client generated from the OpenAPI document gets 404.
  2. OpenAPI create-account schema overclaims. It lists active and maxSizeMB properties; HandleCreateAccount_ reads only address and password and always sets Active=true (source: RestApiServer.cpp:3110 vs 2753-2784).
  3. OpenAPI /api/v1/status says "Status, state and uptime"; the handler returns no uptime field (source: RestApiServer.cpp:3105 vs 2642-2662).
  4. README and legacy doc describe the REST API as "HTTP Basic auth, administrator password" only (README.md:363; legacy doc ch. 24 and line 1448) - Bearer API keys with scope/domain/source/expiry restrictions have existed since Aug 2026 (RestApiServer.h header; RestApiServer.cpp:1023-1041).
  5. README REST endpoint list (README.md:472) and legacy doc ch. 24 table omit /api/v1/srv, /api/v1/quarantine[/<id>[/release]], /api/v1/domains/<name>/aliases, /api/v1/openapi.json; the legacy doc also omits /api/v1/apikeys entirely.
  6. Legacy doc ch. 24 says "TLS is required unless bound to 127.0.0.1"; the code also exempts localhost and ::1 (RestApiServer.cpp:699-701). README.md:364 has the same narrower wording.
  7. Legacy doc ch. 23 event table lists OnErrorLog; the hook the server looks for is OnError (ScriptServer.cpp:241). The doc's table also omits OnBackupCompleted, OnBackupFailed, OnSMTPData, OnHELO, OnClientLogon, OnClientValidatePassword, OnRecipientUnknown, OnTooManyInvalidCommands.
  8. Legacy doc 7.2 says autoconfig is enabled under "Settings > Network > API & monitoring"; in the current Control Panel the web-services settings are on "Settings > Network > Web services & autoconfiguration" (NavigationMap.cs:266, 584; SettingsSearchIndex.g.cs:351-359). Legacy doc ch. 20 (line 1353) likewise lists "web services" under API & monitoring.
  9. Legacy doc 16.x says /readyz is "200 when running with a connected database"; the current check is a real round-trip probe that must have succeeded within the last 20 s (MetricsServer.cpp:108, 2304-2353). README.md:470 is correct on this.
  10. README.md:120 "Thunderbird autoconfig and Outlook autodiscover are served for every local domain" is true only once WebServicesHttpPort/WebServicesHttpsPort is non-zero; both default to 0, so the feature is inert by default (Application.cpp:516-533 comment; WebServicesServer.cpp:454-491). Same for MTA-STS hosting (README.md:43), which additionally needs the HTTPS port.
  11. The WebAdmin page's renderDomains tolerates either an array or {domains:[...]}; the server returns a bare array (WebAdmin/index.html:249 vs RestApiServer.cpp:2664-2707) - harmless, but the page should not be cited as evidence of the response shape.

12. Gaps (things a page writer will want that no document currently states)

  • No document explains the API key store file format, the fail-closed rules, or that keys are manageable from the Control Panel; the only prose is in code comments and the store's own preamble (RestApiServer.cpp:2017-2055, 2493-2521; ApiKeyStore.cs:74-96).
  • No document lists the REST rate limit (200/10 s per credential, 429 + Retry-After) or the 5-minute refused-address window after auto-ban (RestApiServer.cpp:158-171, 230-232).
  • No document describes /api/v1/srv (ready-to-publish RFC 6186/8314 SRV records) or /api/v1/openapi.json.
  • No document describes the security.txt endpoint, the CalDAV/CardDAV redirect settings (CalDavRedirectUrl, CardDavRedirectUrl), or the Apple .mobileconfig endpoint - all three are code-only (WebServicesServer.cpp:891-918, 1245-1347, 1658-1802).
  • No document states that the metrics port's health probes are never authenticated while /metrics answers 503 on a non-loopback bind without a credential, except README.md:367-373 (correct) - the legacy doc predates it.
  • The legacy doc has no COM object-model reference beyond one PowerShell sample; the IDL is the only complete reference (hMailServer.idl, 3996 lines, 94 coclasses).

13. Unconfirmed (not verified by this reader; do not publish as fact)

  • Whether Account.Size is in bytes or MB (legacy doc divides by 1MB; IDL helpstring only says "The size of the account").

  • The legacy doc's claim that fifteen COM methods returned S_OK on refusal before 6.2.10 and now return errors - plausible from the doc, not checked against Server/COM/*.cpp here.

  • Exact JSON field names of /healthz (the handler was read only as far as its comments; fields "status, state, database, uptime" are from MetricsServer.h:279-283 and README.md:470, not from the emitted string).

  • Whether hmailserver/test/WebAdmin test cases/Web form tests.txt reflects the current WebAdmin page (not read).

  • Behaviour of the REST listener when RestApiBindAddress is 0.0.0.0 with only the ACME certificate present but not yet issued at startup: from code it refuses to start (no cert), and nothing re-tries later - inferred, not tested.

  • Whether hmailserver/docs/HighAvailabilityRunbook.md section 3's metrics example (MetricsServerPort=8080, bind 0.0.0.0, no credential) has been updated for the 503-on-non-loopback rule (referenced in MetricsServer.h comments; the runbook itself was not read).

  • REST (5 Sep 2026): GET /api/v1/metrics/history?metric=<name>&range=24h|7d|30d returns {metric, known, enabled, retention_days, minutes_back, bucket_minutes, samples:[{time, value}]} - the samples of one metric averaged per minute / ten minutes / hour; 400 with the list of metric names for an unknown metric, 400 for an unknown range; in the OpenAPI document. Metric names: sessions_smtp, sessions_imap, sessions_pop3, processed_messages_total, messages_delivered_total, messages_deferred_total, messages_bounced_total, spam_messages_total, viruses_removed_total, auth_success_total, auth_failures_total, tls_handshakes_total, tls_handshake_failures_total, messagestore_missing_files.

  • COM (5 Sep 2026): Utilities.SampleMetricsNow() (long: rows written, -1 when MetricsHistoryDays is 0; also prunes) and Utilities.GetMetricHistory(Metric, MinutesBack, BucketMinutes) (BSTR JSON as above; BucketMinutes 0 = every sample). Server administrators only.

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