Repository navigation
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); passages corrected on 2026-09-08 cite the tree at 40ae9491d (after v6.2.27). Anything marked "after 6.2.27" is in the tree but not yet in a published release.
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.
- 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(default127.0.0.1),RestApiCertificateFile(default empty),RestApiPrivateKeyFile(default empty) (source: IniFileSettings.cpp:591-594). - Prometheus metrics + health probes:
MetricsServerPort(default 0),MetricsServerBindAddress(default127.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(default0.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(default127.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).
- The REST API and the web services listeners run on
Server/Common/Util/HttpServer.cpp, an HTTP/1.1 server on Boost.Asio with its own io_context, a bounded pool of worker threads (4 for both), a connection cap enforced at accept before the TLS handshake (64 for REST, 128 for web services), keep-alive (the HTTP/1.1 default; an HTTP/1.0 client gets it only when it asks), at most 1000 requests per connection, and absolute deadlines rather than idle timeouts (source: HttpServer.cpp:4-40, 516-527, 759, 872, 1006-1010; RestApiServer.cpp:128-137, 647-656; WebServicesServer.cpp:45-49, 501-508). In the tree after 6.2.27, not yet in a published release. The metrics listener is still a raw-socket, single-worker-thread server speaking HTTP/1.0 withConnection: closeon every response (source: MetricsServer.cpp:509, 761, 2295). - A bind address of
::is made dual-stack (IPV6_V6ONLY cleared) on all three listeners; a specific IPv6 literal is IPv6-only. The REST and web-services listeners bind throughHttpServer::Listen, which translateslocalhostto127.0.0.1and parses the address withboost::asio::ip::make_address, logging "Invalid bind address" when that fails (source: HttpServer.cpp:683-733; MetricsServer.h:206-213). - Control Panel: the REST, metrics, OTel, ManageSieve and Windows-event-log settings live on the page
Monitoring & troubleshooting > API & monitoring(page idapi); API keys have their own pageMonitoring & troubleshooting > REST API keys(idapikeys); the web-services settings live onConnections & protocols > Web services & autoconfiguration(idwebservices); ACME onTLS & certificates > Certificates (ACME)(idacme). The older pathsSettings > Network > API & monitoring,Settings > Network > Web services & autoconfigurationandSettings > Security > Certificates (ACME)are accepted as search aliases (source: Tools/ControlPanel/Services/NavigationMap.cs:262-268 aliases; groups and pages at 358, 427, 436, 580-594, 605-613).
- 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
RestApiBindAddressis exactly127.0.0.1,localhostor::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
RestApiCertificateFileandRestApiPrivateKeyFileare non-empty; when the certificate setting is empty,Application.cppsubstitutes 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 andTlsKeyExchangeGroupsas the mail protocols), then a TLS 1.2 floor is applied on top (source: RestApiServer.cpp:709-770, floor at 762). -
localhostis translated to127.0.0.1before binding (source: RestApiServer.cpp:774). - Startup log line: "RestApi: Listening on : (https)" or "(http, loopback only)" (source: RestApiServer.cpp:808-810).
- Request limits (HttpServer, in the tree after 6.2.27): head + body capped at 64 KB -> 413
{"error":"request too large"}, refused from the declared Content-Length before the body is read;POST /api/v1/me/messagesandPOST /api/v1/me/draftsalone get a second cap of 16 MB and a 300 s request deadline; every other request has an absolute 30 s deadline from the moment the server starts waiting for it (the TLS handshake counts) to the last byte of the response, and a connection lives at most 300 s whatever it is doing - when either deadline passes the connection is closed without a response; any NUL byte in the head or body -> 400{"error":"malformed request"}; a chunked request body -> 411{"error":"length required"};Expect: 100-continueis answered once the declared length has passed the cap (source: RestApiServer.cpp:128-137, 647-656, 673-685, 6845-6856; HttpServer.h:64-101; HttpServer.cpp:221-242, 337-352, 362-380, 436-441, 499-513). - Per-credential rate limit: 200 requests per fixed 10-second window per credential identity (
administratororkey:<id>); over budget -> 429{"error":"too many requests"}withRetry-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::RegisterFailedLoginwith 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 API response body is JSON (
Content-Type: application/json) and carriesCache-Control: no-store; the exceptions are the static pages/,/index.html,/portaland/portal.jsand the attachment downloadGET /api/v1/me/messages/<id>/attachments/<index>, which serve their own media types (all alsono-store). Status codes the HTTP layer knows: 200, 201, 202, 204, 301, 302, 304, 400, 401, 403, 404, 405, 409, 411, 413, 415, 429, 500, 501, 503 (and 100 forExpect: 100-continue); anything else is rewritten to 500 (source: RestApiServer.cpp:2297-2315, 2998, 6295-6305, 7549-7576; HttpServer.cpp:968-995). - 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). One exception: when the administrator password is right but a second factor is enrolled ([Security] AdministratorTotpSecret) and theX-hMailServer-OTPheader is missing or wrong, the 401 addsX-hMailServer-OTP: requiredand the body{"error":"authentication failed","second_factor":"required"}; a missing code is not counted towards the auto-ban, a wrong one is (source: RestApiServer.cpp:846-869, 877-917, 2318-2344; since 6.2.27). - Every request is wrapped in an OpenTelemetry server span (continuing an inbound
traceparent) that is a no-op unlessOtelEndpointis set (source: RestApiServer.cpp:1428-1441). - The JSON request parser is a minimal key-by-key extractor (keys are looked up independently, not as a document): string values with every RFC 8259 escape honoured (
\",\\,\/,\n,\r,\t,\b,\f,\uXXXXwith surrogate pairs, decoded to UTF-8), boolean values (GetJsonBoolValue_) and arrays of strings (GetJsonStringArray_) (source: RestApiServer.cpp:4190-4259, 8069-8200).
-
Authorization: Basic base64("Administrator:<password>"): the user nameadministrator(case-insensitive) is validated against[Security] AdministratorPasswordviaCrypt::Validate, so it works whether the ini holds a hash or plain text. Any other Basic user name is treated as an account's own credential (address and password, or an app password) and authenticated throughAccountLogon::Logon- the same path, lockout and auto-ban as an IMAP logon; an inactive account is refused; the two are told apart before either password is tried, so an account is never tested against the administrator password nor the reverse (source: RestApiServer.cpp:813-846, 877-917, 4323-4351). The account path is in the tree after 6.2.27, not yet in a published release; see 3d. - 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 fixtureRestApiApiKeysruns the listener on loopback port 9104 with admin passwordtestarover plain HTTP (source: hmailserver/test/RegressionTests/API/RestApiApiKeys.cs:58-64).
-
Authorization: Bearer hmapi_<64 lower-case hex chars>; a token ishmapi_+ 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.iniin the same directory as hMailServer.ini; one[Key.<id>]section per key withLabel,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
Hashis not 64 lower-case hex is ignored (logged as "RestApi: Ignoring a record ... with no usable Hash value"); a missing/unparseableExpirescounts as expired;Scopeis read-only unless it is the literalfull(case-insensitive); anAllowedFromthat cannot be parsed refuses every request;Domainsentries are trimmed/lower-cased, empty list = every domain (source: RestApiServer.cpp:2116-2135, 2189-2232, 2238-2254, 2257-2283). -
AllowedFromforms: single address,lower-upperrange, 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, IP range create/delete, distribution list create/delete, backup start, archive hold/release, and (after 6.2.27) update check/download/install (source: RestApiServer.cpp:2085-2135, 2198-2202). Decided by route kind, not by HTTP method. - Domain enforcement: a key with a
Domainslist may only list/create accounts, list aliases, list/create distribution lists and read DKIM in those domains, may only delete an account or a list whose address's domain is in the list (403 "this api key is not permitted for that domain"), sees only its domains inGET /api/v1/domainsandGET /api/v1/srv, must name one of its domains onGET /api/v1/archive(checked in the handler), and is refused with 403 the queue routes ("...the delivery queue is server-wide"), the quarantine routes ("...the quarantine is server-wide") and the IP range, certificate, rules, logs, backup, settings and (after 6.2.27) update routes ("...that resource is server-wide") (source: RestApiServer.cpp:2157-2295, 4261-4266). - 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).
- The Control Panel page "REST API keys" reads, creates and revokes keys by editing
hMailServerApiKeys.inidirectly (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).
- In the tree after 6.2.27, not yet in a published release. An account presents
Authorization: Basic base64("<address>:<password>")(an app password works too); it is authenticated throughAccountLogon::Logon- the IMAP logon path, with the per-name lockout and the auto-ban - and reaches only the routes under/api/v1/meand/api/v1/session; the administrator password and API keys are refused there with 403, and an account is refused everywhere else with 403 (source: RestApiServer.cpp:813-829, 2157-2184, 4323-4351). -
POST /api/v1/sessionwith the account's password answers 201 andSet-Cookie: hmailsession=<64 hex>; Path=/; HttpOnly; SameSite=Strict; Max-Age=43200(plus; Securewhen the listener speaks TLS). The value is 32 random bytes; only its SHA-256 is kept, in memory, compared constant-time. A session ends after 30 minutes idle or 12 hours after it began, whichever comes first; at most 1000 sessions are held (expired ones go first, then the least recently used); a session cannot be started with another session (403);DELETE /api/v1/sessionends it and clears the cookie; a password change ends the account's other sessions; the account is re-read on every request and a deactivated or deleted account's sessions are dropped; stopping the listener clears every session (source: RestApiServer.cpp:327-331, 4584-4785, 723). - CSRF: a request authenticated by the cookie that is not GET or HEAD must carry
X-Requested-With: hMailServer, otherwise 403{"error":"a request that changes something must carry X-Requested-With: hMailServer when it is authenticated by a session cookie"}(source: RestApiServer.cpp:1298-1304). - The rate limit of section 2 applies to accounts as identity
account:<address>(source: RestApiServer.cpp:4350).
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. "Account" = an account's own credential (Basic <address>:<password> or the hmailsession cookie; the administrator password or an API key gets 403) - those routes, the update routes and the portal are in the tree after 6.2.27, not yet in a published release; the ipranges, lists, dkim, certificates, rules, logs, backup, settings, archive and metrics/history routes are in 6.2.27.
| 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 |
| GET | `/api/v1/metrics/history?metric=&range=24h | 7d | 30d` | Basic or Bearer | yes | allowed |
| GET | /api/v1/ipranges |
Basic or Bearer | yes | REFUSED 403 | array [{"id","name","lower","upper","priority", ...the permission booleans}]
|
1930-1937, 3465-3517 |
| POST | /api/v1/ipranges |
Basic or full key | no | REFUSED 403 | body {"name","lower","upper","priority", permission booleans}; 201 {"id"}; 400 name, lower and upper are required / lower and upper must be IP addresses / the save error |
1930-1937, 3520-3596 |
| DELETE | /api/v1/ipranges/<id> |
Basic or full key | no | REFUSED 403 | 200 {"deleted":true}; 404 ip range not found
|
1939-1948, 3599-3613 |
| GET | /api/v1/domains/<name>/lists |
Basic or Bearer | yes | must match | array [{"address","active","require_auth","members":[...]}]; 404 domain not found
|
1950-1968, 3616-3667 |
| POST | /api/v1/domains/<name>/lists |
Basic or full key | no | must match | body {"address","members":[...],"require_auth"}; 201 {"address","members":<count saved>}; 400 address is required / address does not belong to the domain / a member's validation error; 404 domain not found; 409 a list with that address exists
|
1950-1968, 3670-3724 |
| DELETE | /api/v1/lists/<address> |
Basic or full key | no | address's domain must match | 200 {"deleted":true}; 404 list not found
|
1970-1979, 3727-3747 |
| GET | /api/v1/domains/<name>/dkim |
Basic or Bearer | yes | must match |
{"domain","enabled","selector","sign_aliases","private_key_file"}; 404 domain not found
|
1981-1990, 3782-3798 |
| GET | /api/v1/certificates |
Basic or Bearer | yes | REFUSED 403 | array [{"id","name","certificate_file","private_key_file"}] - never the private key password; [] when none |
1992-1996, 3750-3779 |
| GET | /api/v1/rules |
Basic or Bearer | yes | REFUSED 403 | array [{"id","name","active","all_criteria","criteria":[{"field","header","match","value"}],"actions":[{"type","value"}]}] - the global rules from the delivery cache |
1997-2001, 3854-3943 |
| GET | /api/v1/logs |
Basic or Bearer | yes | REFUSED 403 | array [{"name","size","created"}]
|
2002-2006, 3969-3996 |
| GET | /api/v1/logs/<name>?lines=N |
Basic or Bearer | yes | REFUSED 403 |
{"name","lines":[...]} - the tail, newest last, N default 200 and at most 2000; 400 not a log file name (must be a bare *.log name); 404 no such log file
|
2007-2016, 3946-3967, 3999-4095 |
| GET | /api/v1/backup |
Basic or Bearer | yes | REFUSED 403 |
{"status","log":[...]} - the manager's status text and the backup log's last lines; 503 the backup manager is not running
|
2018-2024, 4117-4157 |
| POST | /api/v1/backup |
Basic or full key | no | REFUSED 403 | 202 {"started":true} (runs on the maintenance queue; poll GET); 409 {"error":"the backup did not start","status":"..."} when one is running or nothing is configured; 503 the backup manager is not running
|
2018-2024, 4098-4114 |
| GET | /api/v1/settings |
Basic or Bearer | yes | REFUSED 403 |
{"host_name","default_domain","max_message_size_kb","max_smtp_connections","max_imap_connections","max_pop3_connections","smtp_relayer","smtp_relayer_port","log_smtp_conversations","log_imap_conversations"} - no secrets |
2026-2030, 4160-4187 |
| GET | /api/v1/archive?domain=&mailbox=&sender=&recipient=&subject=&since=&until=&hold=1&limit= |
Basic or Bearer | yes | must name one of its domains (checked in the handler, 403) | array of index rows {"id","time","domain","mailbox","direction","sender","recipients","subject","message_id","path","size","hold"}, newest first, limit 1-1000 default 200; 500 the archive index could not be read
|
2031-2036, 4261-4294 |
| GET | /api/v1/archive/<id> |
Basic or Bearer | yes | handler-checked | the index row; 404 archive entry not found
|
2051-2060, 4297-4305 |
| POST | /api/v1/archive/<id>/hold |
Basic or full key | no | handler-checked | 200 {"hold":true}; 404 archive entry not found; 500 the hold could not be changed
|
2040-2049, 4308-4320 |
| DELETE | /api/v1/archive/<id>/hold |
Basic or full key | no | handler-checked | 200 {"hold":false}; 404 archive entry not found
|
2040-2049, 4308-4320 |
| GET | /api/v1/update |
Basic or Bearer | yes | REFUSED 403 | the update check's verdict: {"state","stateName","runningVersion","channel","checkEnabled","availableVersion","releaseName","publishedAt","releaseUrl","installer":{"name","url","size","digest","bundleUrl"},"downloaded":{"path","signer","logTime","verifiedAt"},"apply":{"status","version","detail"},...}; state 0 not checked, 1 up to date, 2 available, 3 downloaded and verified, 4 installing, 5 the last check failed (lastError); fetches nothing itself. After 6.2.27 |
1853-1857, 8203-8207; UpdateChecker.cpp:539-585 |
| POST | /api/v1/update/check |
Basic or full key | no | REFUSED 403 | reads the release feed now, whether or not UpdateCheckEnabled is on; 200 with the verdict as GET /api/v1/update (state 5 + lastError when the feed could not be read). After 6.2.27 |
1859-1863, 8226-8235 |
| POST | /api/v1/update/download |
Basic or full key | no | REFUSED 403 | fetches the installer the last check found and its Sigstore bundle into <DataDirectory>\Updates and verifies it; a file that fails is deleted; nothing is run; 200 with the verdict (state 3 when in place, 5 + lastError when not). After 6.2.27 |
1865-1869, 8218-8224 |
| POST | /api/v1/update/install |
Basic or full key | no | REFUSED 403 | verifies the downloaded installer again and hands it to hMailServer.Updater.exe, which runs it, waits for the service, and rolls back if it does not return; the service stops and starts during the update; 200 with the verdict (state 4 when the helper was started, 5 + lastError when not). After 6.2.27 |
1871-1876, 8209-8216; UpdateInstaller.cpp:26 |
| GET |
/portal, /portal.js
|
none | - | - | the self-service sign-in page (text/html) and its script (text/javascript), static and compiled into the server, with a Content-Security-Policy allowing no inline script (script-src 'self', connect-src 'self', frame-ancestors 'none'), X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer, Cache-Control: no-store. After 6.2.27 |
1281-1285, 6860-7576 |
| GET | /api/v1/me |
Account | - | - | {"address","domain","active","quota":{"limit_mb","used_bytes"},"vacation":{"enabled","active","subject","message","expires","expires_date"},"password_changed","second_factor","directory_linked"} |
1581-1585, 4391-4425 |
| POST | /api/v1/me/password |
Account | - | - | body {"current","new"} (current must be the account password itself); an account with a second factor sends the code in X-hMailServer-OTP, else 401 with X-hMailServer-OTP: required; 200 {"changed":true}; 400 fields missing or the password policy refused (reason in error); 403 the current password is not correct; 409 directory-linked account or a recently used password; ends the account's other sessions |
1720-1724, 4428-4513 |
| PUT | /api/v1/me/vacation |
Account | - | - | body {"enabled"(required),"subject"(<=200),"message"(<=20000),"expires","expires_date"(YYYY-MM-DD, required when expires)} - the whole state at once; 200 {"enabled","subject","message","expires","expires_date"} as saved; 400 |
1726-1730, 4516-4581 |
| POST | /api/v1/session |
Account (Basic password only; a session cookie -> 403) | - | - | 201 {"address","idle_seconds":1800,"lifetime_seconds":43200} with Set-Cookie: hmailsession=<64 hex>; Path=/; HttpOnly; SameSite=Strict; Max-Age=43200[; Secure]; see section 3d |
1732-1736, 4667-4739 |
| DELETE | /api/v1/session |
Account (session cookie) | - | - | 200 {"ended":true} and a clearing Set-Cookie (Max-Age=0); 400 when the request carried a password rather than a session |
1738-1742, 4742-4764 |
| GET | /api/v1/me/quarantine |
Account | - | - |
{"enabled","messages":[{"id","sender","subject","reason","score","size","created"}]} - only the entries this address is a recipient of, without the other recipients |
1587-1593, 4789-4830 |
| POST | /api/v1/me/quarantine/<id>/release |
Account | - | - | delivers the held message to this address only (the entry stays for its other recipients); 200 {"released":true}; 404 quarantined message not found when not held for this account; 500 with the store's error text |
1595-1613, 4833-4855 |
| DELETE | /api/v1/me/quarantine/<id> |
Account | - | - | this address gives up its copy; the entry and file go when no recipient is left; 200 {"deleted":true}; 404 |
1595-1613, 4858-4872 |
| GET | /api/v1/me/folders |
Account | - | - |
{"delimiter","folders":[...],"shared":[{"owner","account_id","folders":[...]}]} - every folder the account may read as IMAP LIST gives it (id, name, path, parent_id, special_use, subscribed, writable, messages, unseen, uidvalidity, subfolders); shared lists the public folders and the folders other owners shared |
1615-1619, 5143-5291 |
| GET | /api/v1/me/folders/<id>/messages?limit=&before_uid=&q= |
Account | - | - |
{"folder_id","total","query","messages":[{"id","uid","size","received","subject","from","date","flags":{"seen","flagged","answered","draft","deleted"},"message_id","in_reply_to","references"}],"scanned","complete","next_before_uid"} newest first; limit 1-200 (default 200), before_uid pages back, q matches Subject/From/To/Cc/text/HTML case-insensitively over at most 2000 messages per request; 404 folder not found for another account's or an ACL-hidden folder |
1621-1635, 5294-5410 |
| GET | /api/v1/me/search?q=&limit= |
Account | - | - |
{"query","scanned","complete","more","messages":[{"folder_id","folder","id","uid","size","received","subject","from","date","flags",...thread headers}]} - the same match as q over every readable folder, newest first, limit 1-200 default 50, at most 2000 looked at; 400 q is required
|
1661-1665, 5441-5555 |
| GET | /api/v1/me/messages/<id> |
Account | - | - |
{"id","uid","folder_id","size","received","flags","subject","from","date","message_id","in_reply_to","references","truncated","to","cc","text","html","attachments":[{"index","name","size"}]}; a message over 1 MB is described with truncated: true and empty bodies; 404 message not found for another account's or an ACL-hidden message; 404 the message file is missing
|
1712-1716, 5558-5646 |
| PUT | /api/v1/me/messages/<id>/flags |
Account | - | - | body any of seen, flagged, answered, draft, deleted as booleans (only those named change); 200 {"id","folder_id","flags"}; 400 no flag named: ...; 403 the folder does not allow it (the rights STORE asks for); 404; every IMAP session on the folder is told |
1679-1685, 5760-5827 |
| POST | /api/v1/me/messages/<id>/move |
Account | - | - | body {"folder_id"}; as IMAP MOVE: a copy with a new UID then the original expunged; 200 {"id"(the new one),"folder_id"}; 400 folder_id is required / same folder / a folder of another mailbox; 403 the folder does not allow adding or removing; 404 |
1704-1710, 5830-5879 |
| DELETE | /api/v1/me/messages/<id>[?permanent=1] |
Account | - | - | moved to the folder designated \Trash when the account has one and the message is not in it already: 200 {"deleted":false,"moved_to":<folder id>,"id":<new id>}; otherwise, or with permanent=1, expunged: 200 {"deleted":true}; 403 the folder does not allow it; 404 |
1714-1716, 5882-5929 |
| GET | /api/v1/me/messages/<id>/attachments/<index> |
Account | - | - | the attachment decoded, under its own media type except types a browser would run or render (HTML, SVG, XML, script -> application/octet-stream), with Content-Disposition: attachment; filename="..."; filename*=UTF-8''..., X-Content-Type-Options: nosniff, a sandbox CSP and Cache-Control: no-store; index is the position in the message's attachments list; 404 message not found / attachment not found; 413 the message is too large to read here (over 32 MB) |
1687-1702, 6256-6305 |
| POST | /api/v1/me/messages |
Account | - | - | body {"to","cc","bcc"(address lists, comma or semicolon separated),"subject","text","in_reply_to","references","answered_id","attachments":[{"name","type","data"(base64)}]} (at most 20 attachments, 12 MB together; this route takes a request of up to 16 MB with a 300 s deadline); every address goes through the checks RCPT TO makes for an authenticated sender; queued through the SMTP submission pipeline; a read copy kept in the \Sent folder when there is one; 201 {"queued":true,"recipients","sent_id"(0 when no copy was kept)}; 400 no recipient / <address>: not an e-mail address / the refusal reason / too many recipients; 413 the message is larger than the server allows
|
1667-1671, 5995-6181, 6781-6856 |
| POST | /api/v1/me/drafts |
Account | - | - | body {"to","cc","bcc","subject","text","attachments","replace_id"} - kept in the Drafts folder (created as Drafts when the account has none) with \Draft \Seen; replace_id names the draft this one supersedes, expunged once the new one is saved (a new message with a new UID); 201 {"id","folder_id"}; 403 the Drafts folder does not allow it; 413 the mailbox is full; 16 MB / 300 s request cap |
1637-1641, 6580-6780, 6845-6856 |
| GET | /api/v1/me/settings |
Account | - | - | {"name":{"first","last"},"forwarding":{"enabled","address","keep_original"},"signature":{"enabled","text","html"}} |
1643-1650, 6368-6393 |
| PUT | /api/v1/me/settings |
Account | - | - | body any of name, forwarding, signature (each applied whole; one not named is left alone); 200 the settings as saved; 400 nothing to change / a name over 100 characters / forwarding needs an e-mail address / forwarding to the account itself / a signature over 20000 characters |
1643-1650, 6396-6470 |
| GET | /api/v1/me/filters |
Account | - | - | {"active"(the active Sieve script, empty when none),"name"(its ManageSieve name)} |
1652-1659, 6473-6484 |
| PUT | /api/v1/me/filters |
Account | - | - | body {"script"} checked as ManageSieve PUTSCRIPT checks it; an empty script removes the filter; 200 {"active"}; 400 script is required... / a script is at most 256 KB / the parse error |
1652-1659, 6487-6519 |
| 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, IP-range, archive and
/api/v1/me/...paths must be 1-18 decimal digits and > 0, otherwise the route is unknown (404); an attachment index is 1-5 decimal digits and may be 0 (source: RestApiServer.cpp:435-448, 1603-1612, 1631, 1683-1717, 1943, 2044-2056). - Domain/account path segments may not contain
/; query strings are ignored everywhere (source: RestApiServer.cpp:1418-1425, 1627, 1651).
-
GET /,GET /index.htmland, after 6.2.27,GET /portalandGET /portal.jsare the only unauthenticated routes; the server serves<ProgramDirectory>\WebAdmin\index.htmlastext/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/."; the portal page and script are compiled into the server (source: RestApiServer.cpp:1276-1285, 2977-3001, 6860-7576). - The installer ships
WebAdmin\index.htmlinto{app}\WebAdminwith the server component (source: hmailserver/installation/section_files_64.iss:13; file at hmailserver/installation/WebAdmin/index.html, 20,746 bytes in git, 21,145 in a CRLF checkout). - 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 insessionStorage["hmsAuth"], validates it by callingGET /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/tlsarecords); theme toggle stored inlocalStorage["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/aliasesor/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).
- Started only when
WebServicesHttpPort > 0orWebServicesHttpsPort > 0; both default to 0 (source: Application.cpp:535-547; WebServicesServer.cpp:573-592). - HTTPS: uses
WebServicesCertificateFile/WebServicesPrivateKeyFile, falling back to the ACMEfullchain.pem/privkey.pemwhen 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
SslContextInitializerone 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; the listener runs on
HttpServer(after 6.2.27): 64 KB cap on head + body (413), an absolute 15 s deadline per request and 120 s per connection (the connection is closed when either passes), 128 concurrent connections, 4 worker threads, keep-alive; every request wrapped in an OTel span (source: WebServicesServer.cpp:45-49, 501-523, 604-606; HttpServer.cpp:221-242, 337-352, 872). - Every response is
Cache-Control: no-storeunless the handler says otherwise:security.txtand the Thunderbird autoconfig XML arepublic, max-age=3600, the CalDAV/CardDAV 301spublic, max-age=86400; the MTA-STS policy, ACME challenges, the Apple profile, autodiscover and every refusal areno-store(source: WebServicesServer.cpp:695-722, 1058, 1362; after 6.2.27). - Status codes the HTTP layer knows: 200, 201, 202, 204, 301, 302, 304, 400, 401, 403, 404, 405, 409, 411, 413, 415, 429, 500, 501, 503; anything else becomes 500 (source: HttpServer.cpp:968-995).
| 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; served over HTTPS only since 6 Sep 2026 - X-Forwarded-Proto: https from a TLS-terminating proxy counts, otherwise 301 to the WebServicesHttpsPort listener when configured and 403 with the reason when not |
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 |
-
CalDavRedirectUrlandCardDavRedirectUrlare read throughIniFileSettings(accessors added with the HTTP foundation, after 6.2.27; IniFileSettings.cpp:492-493) and cached for 60 s by the listener (source: WebServicesServer.cpp:67, 1062-1090, 1119-1140). -
MtaStsPolicyMode(defaultenforce; acceptedenforce|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 isMtaStsHostingEnabled(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 inAcmeDomains(source: README.md:358).
- ACME settings:
AcmeEnabled(default 0),AcmeDirectoryUrl(defaulthttps://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(plusaccount.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/; otherwiseAcmeClientstarts a transientAcmeChallengeServeronAcmeHttpPortfor the duration of the issuance (logged at startup whenAcmeEnabled=1and 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).
- After an issuance or renewal writes the files,
AcmeClientlogs "ACME: Restarting servers to load the new certificate." and reinitialises the application (StopServers/InitInstance/StartServers), which restarts the mail listeners and the metrics, REST and web-services listeners; the REST and web-services HTTPS listeners pick up the newfullchain.pem/privkey.pemat that restart, so a REST listener that refused to start withRestApiBindAddressoff loopback and no certificate yet is started once ACME has issued one (source: AcmeClient.cpp:1655-1657; Reinitializator.cpp:63; Application.cpp:501-575, 913-935, 981-1004).
- Enabled by
MetricsServerPort > 0; bindMetricsServerBindAddress(default127.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 {"status","state"(running/starting/stopping/stopped/unknown),"database","uptime_seconds"}; 200 when running and DB answering, else 503; session counts deliberately NOT included |
945-950, 2367-2440 (document at 2408-2413) |
/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_dbversionevery 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). -
/metricsaccess 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 namingMetricsServerAuthToken/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/orAuthorization: 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
MetricsServerCertificateFileandMetricsServerPrivateKeyFilemust 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/metricsanswers 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_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_totalandhmailserver_domain_messages_sent_total(only withMetricsPerDomainEnabled=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 ofline.Format("hmailserver_in MetricsServer.cpp; histograms at 1769, 1774; per-domain gate at 1598). There is nohmailserver_database_upfamily - the name appears only in a comment and as a local variable in the/healthzhandler. - 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(defaulthmailserver),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).
- Type library
hMailServer(uuid DB241B59-A1B1-4C59-98FC-8D101A2995F2, version 1.0, helpstring "hMailServer Type Library"); 94 coclasses; the root coclass isApplication(CLSID D6567EF8-0A6C-48E7-9288-A2463123C2F3, default interfaceIInterfaceApplication) (source: hMailServer.idl:3218-3223, 3446-3453;grep -c coclass= 94). - ProgID
hMailServer.Applicationis 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
dualIDispatch 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
admintoolsregisters 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 anAccount(null on failure); the administrator logs in asAdministratorwith[Security] AdministratorPassword;AuthenticateWithCode(Username, Password, Code)is the TOTP variant (source: hMailServer.idl:1672, 1687; ServerSession.cs:441). -
IInterfaceApplicationmembers: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,AdministratorTOTPEnabled(true while the administrator credential has a second factor enrolled; readable before authenticating; 6.2.27) (source: hMailServer.idl:1688-1721; enum at 153-168). - 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). -
DomainsandAccountscollections offerItem,Count,Add,Refresh,ItemByDBID,ItemByName/ItemByAddress,DeleteByDBID(source: hMailServer.idl:919-938, 1698-1717). -
Accountmembers includeAddress,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.Settingsis the root for server-wide settings and sub-objects:AntiSpam(which ownsQuarantine),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.) andSetAdministratorPassword,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, and - in the tree after 6.2.27, not yet in a published release - the live update:UpdateState(0 no check since the service started, 1 latest release, 2 a newer release is available, 3 its installer is downloaded and verified, 4 installing, 5 the last check failed),AvailableVersion,AvailableVersionPublished(ISO 8601 UTC),AvailableVersionUrl,UpdateLastChecked,UpdateLastError,CheckForUpdate()(reads the feed now, no opt-in needed),DownloadUpdate()(fetches the installer and its Sigstore bundle into<DataDirectory>\Updatesand verifies it; nothing is run),UpdateInstallerPath,UpdateSignerIdentity,InstallUpdate()(hands the verified installer tohMailServer.Updater.exe, which runs it, waits for the service and rolls back if it does not return),UpdateApplyOutcome(" : ", status ok/failed/rolled-back/rollback-failed/no-rollback) (source: hMailServer.idl:532-551; eSessionType: 1 SMTP, 2 SMTPClient, 3 POP3, 4 POP3Client, 5 IMAP at 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).
- Members added in 6.2.25-6.2.27 (all appended, vtable order kept):
Settings.CreateDefaultSpecialUseFoldersEnabled(create Drafts/Sent/Trash/Junk designated for RFC 6154 at account creation; off by default),Settings.EnrolAdministratorTOTP()(returns theotpauth://URI; enforced at once on COM and REST) andSettings.DisableAdministratorTOTP();Domain.MessageRetentionDays(0 = no policy) andAccount.MessageRetentionDays(0 = the domain's policy, -1 = keep forever);Utilities.RunMessageRetention()(runs the sweep now, returns the count removed);FetchAccount.ServerType1 = IMAP andFetchAccount.MirrorFolders(IMAP only: every remote folder into a local folder of the same name, a migration);AntiSpam.TarpitDelay/TarpitCountnow carry the recipient tarpit (SmtpTarpitDelaySeconds/SmtpTarpitCountin hMailServer.ini) instead of being obsolete;Diagnostics.AssertionsEnabled,Diagnostics.TriggerAssertion(),Diagnostics.DnssecChainStatus(Name, Type)(0 secure, 1 insecure, 2 bogus). After 6.2.27, not yet in a published release:Diagnostics.AcmeRenewalTime(notBefore, notAfter)(source: hMailServer.idl:808-811, 854-855, 988-989, 1239, 1854-1855, 1889-1890, 2576-2579, 3221-3224). - Live update (in the tree after 6.2.27, not yet in a published release):
UpdateCheckEnabled(default 0),UpdateChannel(defaultstable;prereleasereads the last ten releases),UpdateFeedUrl(empty =https://api.github.com/repos/Progressiverobot/hmailserver/releases/latest),UpdateCheckHours(default 24, floor 1),UpdateWindow(empty = no unattended apply),UpdateAutoDownload(default 0),UpdateBackupBeforeApply(default 1; no backup destination = no unattended apply),UpdateTrustRootsFile,UpdateLogPublicKeyFile,UpdateSigningIdentity,UpdateSigningIssuer,UpdateSourceRepository,UpdateRequireAuthenticode(default 0),UpdateServiceWaitSeconds(default 180, floor 5) (source: IniFileSettings.cpp:489-509). A scheduled task runs once at start and every 15 minutes, does nothing untilUpdateCheckEnabled=1, reads the feed everyUpdateCheckHours, and applies insideUpdateWindowafter a backup when asked to (source: Application.cpp:695-717; Common/Application/UpdateCheckTask.cpp:139-186; Common/Util/UpdateChecker.cpp:26-27, 331). Downloads land in<DataDirectory>\Updates(rollback image inUpdates\rollback); the apply is run byhMailServer.Updater.exe; the database upgrade authenticates with a single-use token revoked at every service start (source: UpdateInstaller.cpp:26, 52, 61; Application.cpp:702-707). Surfaces:GET /api/v1/update,POST /api/v1/update/check|download|install(section 4) andStatus.Update*(above). Every key is in the Settings Reference section 9.15.
- 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;StartBackupat hMailServer.idl:2289-2302). - VBScript service dependency:
oApp.Utilities.MakeDependent(sName)afterAuthenticate(source: Addons/Utilities/MakeServiceDependent.vbs:4-19;MakeDependentat 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 uses members that exist (
Domains.Add(),$domain.Name/.Active/.Save(),Accounts.Add(),$account.Address/.Password/.Active/.MaxSize/.Save(),$a.Size) (source: hMailServer.idl:824-870, 965-1030, 1732-1741), but its report line is wrong:Account.Sizeis already in megabytes (bytes / 1,048,576, rounded to three decimals; source: COM/InterfaceAccount.cpp:580-592), so$a.Size / 1MBprints ~0 for every mailbox. Use'{0,-40} {1,8} MB used' -f $a.Address, [math]::Round($a.Size, 1). - Event-script example in legacy doc ch. 23 uses
oMessage.HeaderValue("X-...") = valueandoMessage.Save- both exist onIInterfaceMessage(propputHeaderValue,Save) (source: hMailServer.idl:1079-1130).
- Enabled via
Settings.Scripting.Enabled, languageSettings.Scripting.Language(VBScript/JScript), script file<events dir>\EventHandlers.<vbs|js>, withReload,CheckSyntax,Directory,CurrentScriptFileonIInterfaceScripting(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). -
OnAcceptMessageis invoked asOnAcceptMessage(HMAILSERVER_CLIENT, HMAILSERVER_MESSAGE)(source: Server/SMTP/SMTPConnection.cpp:2234).
-
OpenAPI document path for API keys is wrong.Resolved:GET /api/v1/openapi.jsonnow documents/api/v1/apikeysand/api/v1/apikeys/{id}, matching the router (RestApiServer.cpp:7627-7630 vs 1554-1556). Items 2 and 3 still stand. -
OpenAPI create-account schema overclaims. It lists
activeandmaxSizeMBproperties;HandleCreateAccount_reads onlyaddressandpasswordand always sets Active=true (source: RestApiServer.cpp:3110 vs 2753-2784). -
OpenAPI
/api/v1/statussays "Status, state and uptime"; the handler returns no uptime field (source: RestApiServer.cpp:3105 vs 2642-2662). - Legacy doc ch. 24 (and line 1448) describe the REST API as "HTTP Basic auth, administrator password" only - Bearer API keys with scope/domain/source/expiry restrictions have existed since Aug 2026 (RestApiServer.h header; RestApiServer.cpp:783-806). The README is now correct (README.md:127, 447).
-
Legacy doc ch. 24 table omits
/api/v1/srv,/api/v1/quarantine[/<id>[/release]],/api/v1/domains/<name>/aliases,/api/v1/openapi.jsonand/api/v1/apikeys; the README's list (README.md:644) now carries all of them,/api/v1/openapi.jsonand the update routes included. -
Legacy doc ch. 24 says "TLS is required unless bound to 127.0.0.1"; the code also exempts
localhostand::1(RestApiServer.cpp:699-701). README.md:364 has the same narrower wording. -
Legacy doc ch. 23 event table lists
OnErrorLog; the hook the server looks for isOnError(ScriptServer.cpp:241). The doc's table also omitsOnBackupCompleted,OnBackupFailed,OnSMTPData,OnHELO,OnClientLogon,OnClientValidatePassword,OnRecipientUnknown,OnTooManyInvalidCommands. - 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.
-
Legacy doc 16.x says
/readyzis "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. -
README.md:120 "Thunderbird autoconfig and Outlook autodiscover are served for every local domain" is true only once
WebServicesHttpPort/WebServicesHttpsPortis 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. -
The WebAdmin page's
renderDomainstolerates 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.
- 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.mobileconfigendpoint - 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
/metricsanswers 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).
-
WhetherConfirmed: megabytes, rounded to three decimals (COM/InterfaceAccount.cpp:580-592); see section 9.Account.Sizeis in bytes or MB -
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/*.cpphere. -
Exact JSON field names ofConfirmed:/healthz{"status","state","database","uptime_seconds"}(MetricsServer.cpp:2408-2413). -
Whether
hmailserver/test/WebAdmin test cases/Web form tests.txtreflects the current WebAdmin page (not read). -
Behaviour of the REST listener whenConfirmed from code: it refuses to start at service start, and is started by the reinitialisationRestApiBindAddressis0.0.0.0with only the ACME certificate present but not yet issued at startupAcmeClienttriggers after issuance, which rerunsStartServersand its ACME-certificate substitution (AcmeClient.cpp:1655-1657; Application.cpp:521-538, 981-1004); see 6d. -
WhetherConfirmed: it bindshmailserver/docs/HighAvailabilityRunbook.mdsection 3's metrics example has been updated for the 503-on-non-loopback rule0.0.0.0withMetricsServerAuthTokenset and explains the 503 rule (HighAvailabilityRunbook.md:89-107). -
REST (5 Sep 2026):
GET /api/v1/metrics/history?metric=<name>&range=24h|7d|30dreturns{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) andUtilities.GetMetricHistory(Metric, MinutesBack, BucketMinutes)(BSTR JSON as above; BucketMinutes 0 = every sample). Server administrators only. -
REST (5 Sep 2026, wave 88): the surfaces that were COM-only.
GET|POST /api/v1/ipranges,DELETE /api/v1/ipranges/{id}(name, lower, upper, priority and the permission booleans as JSON);GET|POST /api/v1/domains/{domain}/lists(address, members[], require_auth) andDELETE /api/v1/lists/{address};GET /api/v1/certificates(id, name, files - never the private key password);GET /api/v1/domains/{domain}/dkim(enabled, selector, sign_aliases, private_key_file);GET /api/v1/rules(the global rules from the delivery cache, criteria as field/header/match/value, actions as type/value);GET /api/v1/logs(name, size, created) andGET /api/v1/logs/{name}?lines=N(the tail, N up to 2000, names without any path only);GET /api/v1/backup(the manager's status text) andPOST /api/v1/backup(202, or 409 with the status when one is running or nothing is configured);GET /api/v1/settings(host name, default domain, size and connection limits, relay host and port, the conversation-logging switches). Authorisation: ranges, certificates, rules, logs, backup and settings are server-wide and refused for domain-restricted keys; lists and DKIM are scoped to the key's domains; the creating and deleting routes are refused for read-only keys. All in the OpenAPI document;API/RestApiCoverage.cs(10 tests) proves each against COM and the disk (source: RestApiServer.cpp ParseRoute_/Authorize_/IsMutatingRoute_). -
Archive index (5 Sep 2026, schema 6029,
hm_archiveindex): every archived copy gets a row (time, domain, mailbox, direction sent/received/inbound, sender, recipients, subject, message-id, path, size, hold). COM:Utilities.SearchArchive(Domain, Mailbox, Sender, Recipient, Subject, Since, Until, HoldOnly, MaxRows)returns a JSON array;Utilities.SetArchiveHold(ArchiveID, Hold)returns False for an unknown row; both server administrators only. REST:GET /api/v1/archive?domain=&mailbox=&sender=&recipient=&subject=&since=&until=&hold=1&limit=(newest first; a domain-restricted key must name one of its domains),GET /api/v1/archive/{id},POST|DELETE /api/v1/archive/{id}/hold(mutating; refused for read-only keys; an Inbound copy belongs to no domain). The retention sweep asks the index before deleting (a held copy stays) and removes the row of a file it deletes; the address eraser removes the rows naming the address except held ones (source: PersistentArchiveIndex.cpp, ArchiveRetentionTask.cpp, AddressTraceEraser.cpp, RestApiServer.cpp; tests Infrastructure/ArchiveIndexTests.cs, API/RestApiArchive.cs). -
External accounts over IMAP (6 Sep 2026):
FetchAccount.ServerType1 = IMAP (0 = POP3).IMAPClientConnection(onExternalFetchClientBase, the message handling shared with POP3) logs in with LOGIN or AUTHENTICATE XOAUTH2 (theFetchOAuth2Hostsrule), STARTTLS per ConnectionSecurity, SELECT INBOX, UID SEARCH ALL, UID FETCH BODY.PEEK[] per message not yet recorded (UIDs kept as ":" in hm_fetchaccounts_uids), DaysToKeepMessages 0 = STORE \Deleted + EXPUNGE after collection, N = after N days, records forgotten when the message leaves the server. INBOX only; other folders are not mirrored. Control Panel: the external-account editor's Server type field.
hMailServer 6.3.8, the latest release (2 November 2026, schema 6108) · AGPL-3.0-or-later · Repository · Mirror on GitHub · Report a documentation error (a GitLab issue naming the page)
Home — full index
Start here
1. Install and run
- Before You Install
- Installing hMailServer
- Installing on Linux
- Running in a Container
- Profiles and Environment
- The Control Panel
- The Control Deck
- Your First Domain and Mailbox
- Connecting a Mail Client
- The Webmail
- DNS for Your Domain
- Migration
2. Secure it
- Encryption and Certificates
- Database TLS
- Encryption for People
- Stopping Spam
- Stopping Viruses
- Security Hardening
- Secure Defaults
- The Service Account
- Secrets at Rest
- Sign-in and Identity
- Security Policy
3. Operate it
- Monitoring and Health
- The SNMP Agent
- Backup and Restore
- Message Storage
- Compliance and eDiscovery
- Troubleshooting
- Diagnosing Stalled Mail
- Relocating an Installation
- Upgrading hMailServer
- Upgrading Guide
- The Update Feed
- Migrating the Database Backend
- High Availability Runbook
- Warm Standby
- Runbooks Digest
4. Extend it
- Rules and Sieve
- Aliases Lists and Public Folders
- Rooms and Equipment
- Mailing Lists
- Routes and Relays
- Mail In to a Webhook
- Outbound Shaping
- The COM API and Scripting
- Event Scripts
- The REST API
- Command Line
- APIs Reference
5. Contribute to it
- Project Handbook
- Architecture
- Contributing
- Release Process
- Governance
- Assurance Case
- Regression Test Environment
- Fuzzing
- Regulatory Scope
- Third-Party Binaries
Look it up — from any journey
- Settings Reference
- Settings Overview
- Capabilities and Configuration
- Control Panel Reference
- Control Panel Pages
- Ports Reference
- Changes-Since-6210
- Version-History-to-6210
- Glossary
About this wiki
- hMailServer 6.3.8 is the latest release (2 November 2026, schema 6108)
- Pages say since 6.3.4 where a feature is new in it
- AGPL-3.0-or-later
- Repository, the one place the project is worked on
- Its mirror on GitHub, since 18 October 2026, with no Issues
- Questions: a GitLab issue with the Question template, GitHub Discussions Q&A, or the forum
- Report a documentation error: a GitLab issue naming the page