Skip to content
hMailServer 6.3.8 is the current release, signed 2 November 2026. Windows and Linux, and an in-place upgrade from 5.3 or later, but for the original hMailServer's 5.7.Download 6.3.8 Documentation FAQ
  • 0 Votes
    1 Posts
    246 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) moves the Control Panel's Audit trail page, with journaling and eDiscovery, into a new Compliance group, and the alerts' settings, the outbound webhooks and the SNMP agent onto its Monitoring & alerts page (API & monitoring until 6.3.6; 6.3.5 and 6.3.6 had them on Logging); Ctrl+K takes an old title or route to its page. It also fixes the SMTP log, whose lines the syslog sink copies, which wrote some credentials in the clear with SMTP logging on, in every version from 6.0.0 to 6.3.6 (work item #95). See the 6.3.7 announcement. This topic covers four things 6.3.4 adds for being told what the server is doing and who changed it: the alerts, the audit trail, the syslog sink (or the systemd journal on Linux) and the per-domain reports, with a paragraph on following one connection live. The one thing to know first: after the upgrade to 6.3.4 the audit trail and the alerts are on, with seven of the eleven conditions enabled, but nothing is mailed until AlertRecipient and AlertSenderAddress are set. What 6.3.4 switches on without a switch From the CHANGELOG's list of what changes on upgrade: the audit trail is on (schema 6043), and keeps every row until a retention is set. Compromised-account detection starts in watch mode (schema 6071): it raises account.compromised but holds nothing until compromise_detection_mode is set to enforce. The reputation watch, which raises reputation.listed, is on in a new installation and off after an upgrade. Alerts A condition is a row in hm_alertrules (on or off, threshold, mail or webhook or both, cool-down, at once or in the digest); an event is a row in hm_alertevents. Delivery is AlertTask, once a minute, and nothing else sends, so an alert about the delivery queue is never itself submitted into the stuck queue. Condition On as shipped Raised by Threshold backup.failed yes A backup that failed, and a scheduled backup that could not run. Sent at once — certificate.expiring yes The notAfter of every configured certificate, read once a minute 7 days disk.low no The free-space warning band, and again below the floor; cleared on recovery — queue.stalled no A work queue whose every thread has been in the same task past AsyncQueueStallThreshold — autoban.storm no Auto-bans in the last hour 25 minidump.written no A crash dump newer than the last one recorded, in the log directory — complaints.rate.high yes A hosted domain's complaint rate over the feedback loops' seven-day window; inert while complaint processing is off, and a domain that sent fewer than 100 messages is not judged 1,000 per million dkim.rotation.stalled yes A rotation whose next key's record has not been seen for the threshold, or a running rotation whose last move failed 7 days account.compromised yes Compromised-account detection at submission, one event per account per hour; in watch mode the detail says what enforcing would have done — account.password-reset yes A password reset completed through an account's confirmed recovery address; no cool-down and no digest — reputation.listed yes The reputation watch's stored rows; sent at once, and a check that was refused or timed out does not clear it — The four that are off are noisy on a healthy server. The settings. All nine are rows of hm_settings, none an hMailServer.ini key. They are in the server group of GET/PUT /api/v1/settings, on the Deck's Settings page, and on Windows over COM through Settings. The CHANGELOG and AlertsAndAuditTrail.md spell some in their hm_settings form (AlertRecipient, AlertSenderAddress and so on); the REST keys are these. REST key Default Meaning alerts_enabled on The master switch alert_recipient empty Where alerts and the digest go; a rule can name an address of its own instead alert_sender_address empty What they are sent from; a rule with no sender sends nothing alert_digest_enabled on Whether rules marked for the digest wait for it alert_digest_hour 7 The hour, UTC, the digest goes out; a digest with nothing in it is not sent alert_max_per_hour 20 Past this many single alerts in an hour, everything goes into the digest alert_webhook_max_attempts 5 Webhook attempts before an event is dead-lettered (1 to 20) audit_trail_enabled on Whether administrative changes are recorded audit_retention_days 0 Days of trail to keep; 0 keeps everything Nothing may flap. A state condition already raised is not raised again; an occurrence inside the rule's cool-down is recorded but held for the digest; and the digest is one message listing what fired. Webhooks. A rule whose actions is 2 (webhook) or 3 (mail and webhook) POSTs one JSON object per event (condition, state, severity, time, time_utc, summary, detail, server) with three headers: X-hMailServer-Timestamp, X-hMailServer-Signature: sha256=<hex> and X-hMailServer-Event. It is signed under the rule's shared secret, as every webhook the server sends is; the topic on integrating applications shows how to verify it. A rule that calls a webhook is refused without an address and without a secret; webhook_secret is write-only and the listing says only webhook_secret_set. Only https, or plain http to a loopback address, is used. An attempt that does not get a 2xx is retried on the next pass, then after 1, 5 and 15 minutes, which is the default five attempts, and 60 minutes apart after that if the maximum is raised. After the last attempt the event is dead-lettered: kept, no longer retried, and reported once in the error log as HM6543. Route Does Permission GET /api/v1/alerts/rules Every condition and its rule view_configuration PUT /api/v1/alerts/rules/{condition} Change one, or create one for a condition nothing ships; a field left out keeps its value manage_settings GET /api/v1/alerts/events What has fired, newest first, with the webhook's state view_configuration POST /api/v1/alerts/test Raise a condition through the path a real one takes: {"condition":"disk.low","summary":"...","occurrence":false} manage_settings POST /api/v1/alerts/run Run one pass now manage_server All five are refused to every API key. The administrator password reaches them, and so does an account whose role holds the permission and reaches every domain. Neither the Control Panel nor the Deck has a page for the rules, and hmctl and the PowerShell module have no alerts or audit verbs; hmctl api reaches the routes. The audit trail One row per administrative change made over COM, REST or the Deck, in hm_audit: when (audittime, UTC); who (auditactor: Administrator over COM, administrator or key:<id> over REST, and an account signed in with its own password is its address over COM and account:<address> over REST and the Deck); the interface and, except over COM, the peer address; the object's type and name; the action (created, updated or deleted, also unlocked, cleared, moved and move failed, the last for a mailbox move that failed and could not be put back); the detail (the columns the change carried, or for a setting Name: old -> new, ending [role: <name>] under a role); and the two hashes of the chain. Both kinds of setting are recorded: the hm_settings values and the [Settings] keys (in hm_inisettings since schema 6042), with the value they had. A key set for the first time reads SmtpTarpitCount: (default) -> 5; returned to its default it reads SmtpTarpitCount: 5 -> (default), with the action deleted. A write of the value a setting already has records nothing. What is never recorded as a value. A password, key, token, secret, hash, salt or passphrase is recorded as (changed), decided by the name of the column or setting, matched as a substring, so MetricsServerAuthToken and PasswordPepper are among them. Fifteen keys whose names only look like secrets, the seven PasswordPolicy* keys among them, are recorded with their values. The server's own writes (delivery, retention, greylisting, the index) are never recorded. Verifying the chain. Each row carries the SHA-256 of the row before it and of itself. GET /api/v1/audit/verify answers {"intact","rows_checked","first_broken_id","reason"}, and the reason tells a row that was edited from one that was removed. This makes tampering detectable, not impossible: whoever can write the table can recompute the whole chain. Rows hashed the older way, which only a build from before the 6.3.4 release can have written, did not cover a character above U+00FF; they still verify, with that weakness. Reading it. GET /api/v1/audit?actor=&object_type=&action=&since=&until=&limit=&offset= answers newest first; actor is a substring, so key: finds every change made with an API key, and limit is 1 to 1,000, default 100. It needs the administrator password, or an account whose role holds view_audit (the auditor role) and is not confined to named domains. Every API key is refused, with 401 rather than 403, so that it learns nothing from asking. Putting a change back. POST /api/v1/audit/{id}/revert writes a setting's old value back through the same setter, so the put-back is itself a row in the trail. It needs manage_settings and is refused with 409 for a row that is not a setting's change, a secret's row, a value the trail cut at 120 characters, and a setting this version no longer has. Where to see it. The Deck's Audit trail view has the filters, paging, Verify the chain and Put back. On Windows the Control Panel has the same page, over COM, at Monitoring & troubleshooting → Audit trail (Compliance → Audit trail since 6.3.7; Ctrl+K takes the old route to it). Switching recording off is itself recorded. The trail, the rules and the events are not in the built-in backup. What it holds for compliance. A download of a discovery export is recorded on the trail alone, so with the trail off it is recorded nowhere. An erasure is recorded with the address it erased wherever it deletes from recorded tables (aliases, distribution-list recipients and, with the archive included, the archive index), and audit_retention_days is how those rows age out. The compliance topic covers eDiscovery and the erasure. What it does not record. Reads; what a restore replaces (the request is one row, restore started); creating or revoking a REST API key; releasing or deleting a quarantined message; creating, renaming or deleting a public folder; the [Security], [Directories] and [Database] sections of hMailServer.ini; and a change nobody made through an interface. A change to the twelve syslog settings over COM, from the Control Panel's Logging page for one, is recorded in 6.3.4: the settings property setters carry an audit scope of their own. The wiki's syslog section, 16.2a, still describes the earlier state. Syslog and the journal (since 6.3.4) Every entry the server logs can also be sent as RFC 5424 to a syslog collector over UDP, TCP or TLS. It is a copy: the files under LogFolder are written as before. On Windows the settings are the syslog (RFC 5424) card on the Control Panel's Logging page; on Linux they are keys of PUT /api/v1/settings/logging and the Control Deck, and hmctl settings get logging prints them. They take effect when the sink starts, at the next service start or Reinitialize, not when they are saved. hm_settings name REST key Default Meaning SyslogEnabled syslog_enabled off The whole feature SyslogHost syslog_host empty Empty is not "off": on Linux under systemd it selects the journal; on Windows nothing is sent, which the server says once in the application log SyslogPort syslog_port 514 514 for UDP and plain TCP; 6514 is the registered port for TLS SyslogTransport syslog_transport 0 (UDP) 0 UDP, 1 TCP, 2 TCP over TLS; the REST key takes udp, tcp or tls SyslogFacility syslog_facility 2 (mail) 0 to 23; 16 to 23 are local0 to local7 SyslogMinimumSeverity syslog_minimum_severity 6 (informational) Entries less severe than this are not sent SyslogLogTypes syslog_log_smtp, syslog_log_pop3, syslog_log_imap, syslog_log_application, syslog_log_tcpip, syslog_log_debug 86 One bit mask: SMTP, POP3, application and IMAP on; TCP/IP and debug off Errors are sent whatever the mask says. A message looks like this: <22>1 2026-09-15T14:21:09.284+01:00 mail.example.com hMailServer 4812 SMTPD [hmailserver@32473 thread="7412" session="41" client="203.0.113.9"] <BOM>RECEIVED: MAIL FROM:<[email protected]> The message id is the log category (SMTPD and so on) or, on an error line carrying an hMailServer error number, that number, so msgid == HM5165 is a usable filter at the collector. The structured data under hmailserver@32473 carries thread, and session and client on a conversation line. UDP truncates each message to 1024 bytes, cutting the text and never the header; TCP and TLS frame each message with its length and truncate nothing below 65000 bytes. TLS verifies the collector's certificate against the machine's trust store and the configured host name, so set the host to the name in the certificate, not an IP address; no client certificate is offered. A collector that is down costs log lines, not mail. Entries go to a background thread over a queue of 4096 that drops the oldest when full; a stream that cannot connect backs off from one second to a minute; and the application log gets one line when an outage starts and one when it ends, with the number dropped. Nothing is retried from disk. Each step of a stream transport (the connection, the TLS handshake, one entry written) is given ten seconds, so a collector that accepts the connection and then goes quiet is a failure like a refused one, and a reinitialise or a service stop waits for that step at most. For rsyslog, in /etc/rsyslog.d/30-hmailserver.conf (UDP for the default transport, TCP for SyslogTransport = 1; the stop keeps the lines out of /var/log/mail.log too, so drop it if you want both): module(load="imudp") input(type="imudp" port="514") module(load="imtcp") input(type="imtcp" port="514") if ($app-name == "hMailServer") then { action(type="omfile" file="/var/log/hmailserver.log" template="RSYSLOG_SyslogProtocol23Format") stop } Without RSYSLOG_SyslogProtocol23Format rsyslog reformats to the older RFC 3164 shape and the structured data is lost. For TLS (SyslogTransport = 2 module(load="imtcp" StreamDriver.Name="gtls" StreamDriver.Mode="1" StreamDriver.AuthMode="anon") input(type="imtcp" port="6514") global(DefaultNetstreamDriverCAFile="/etc/ssl/certs/ca.pem" DefaultNetstreamDriverCertFile="/etc/rsyslog.d/collector-cert.pem" DefaultNetstreamDriverKeyFile="/etc/rsyslog.d/collector-key.pem") A self-signed collector certificate has to be installed as trusted on the hMailServer machine, or the handshake is refused and nothing is sent. The journal, on Linux only, needs no collector. Where the server runs as the packaged hmailserver.service, switch the sink on and leave the collector host empty: $ hmctl settings set logging --set syslog_enabled=true $ sudo systemctl restart hmailserver $ journalctl -u hmailserver -f Beside the standard fields the journal gets HMAILSERVER_CATEGORY, HMAILSERVER_MSGID, HMAILSERVER_THREAD, HMAILSERVER_SESSION and HMAILSERVER_CLIENT: $ journalctl -u hmailserver HMAILSERVER_SESSION=41 $ journalctl -u hmailserver HMAILSERVER_MSGID=HM5165 If a collector host is configured the journal is not used. The start of the sink is itself logged, as Syslog: sending RFC 5424 log entries to <host>:<port> over UDP, facility 2. or Syslog: writing log entries to the systemd journal.; a server that logs neither has the feature off. Not sent: the AWStats log, the backup log and the event-script log. Per-domain reports (since 6.3.4) GET /api/v1/reports is an index of the sections, the state of their three sources and what this server cannot answer. A report drawn while its source was off is empty, which is not the same statement as "nothing happened"; each section's note says which it is. GET /api/v1/reports/{section} answers one section, the Deck's Reports view draws the summary, and hmctl report prints a section or saves it as CSV. There is no Control Panel page. Section Counted from Says traffic the message trace Per day and per local domain: incoming, outgoing, failed_incoming, failed_outgoing failures the message trace Per local domain and SMTP status code senders, recipients the message trace The local addresses that sent or received the most, with their failures mailboxes hm_messages The largest mailboxes as they are now: a size, not a history volume the metric history, and the daily per-domain sample Processed, delivered, deferred, bounced, spam and viruses per day, with by_domain rows of spam and viruses storage the same The store's size per day and growth_bytes, with by_domain rows of each domain's size summary all three sources Every section the credential may see, with an omitted list and the reason quota the store Every mailbox with a limit and at least one message, nearest its limit first, as a share of the limit: a share now, not a history countries the metric history and the country counters What a country policy refused, per day; the split by country is since the server last started, not per day The last two are described in QuotaPolicy.md and CountryBlocking.md, not in Reports.md. Parameters. from and to as YYYY-MM-DD, both inclusive, defaulting to the last 30 days and never more than 366; domain, one hosted domain (a domain not hosted here is a 404); top, 1 to 200, default 10; format=csv, refused for summary. In CSV a cell starting with =, +, - or @ is prefixed with an apostrophe. Three things decide whether a report means anything. The message trace is off by default (MessageTraceEnabled=0), and while it is off the four sections counted from it are empty. Processed, delivered, deferred and bounced cannot be given per domain, because those counters carry no domain; spam, viruses and storage can, since schema 6062 added the daily sample in hm_domainsamples, which starts the day it was first taken. And only hosted domains are counted. Authorisation. The administrator password reads everything; every other credential needs both view_configuration and view_message_trace. domainadmin and auditor hold both; helpdesk is refused. A key restricted to named domains sees only those and is refused 403 for another. On volume and storage Reports.md contradicts itself: its Authorisation section refuses them to such a key outright, while its section What cannot be answered, and what it would take (and, for volume, its table of sections under The route) gives a credential restricted to domains the per-domain rows, as the wiki's section 16.6a does. Test both with your own restricted key before a script relies on either. The cost. One report makes at most three grouped queries; the trace read is capped at 100 000 grouped rows and the answer says truncated when it reaches the cap. hmctl report sections hmctl report traffic --from 2026-09-01 --to 2026-09-15 --domain example.com Following one connection live Since 6.3.4 the Deck's Logs page lists the sessions open now and those recently ended, and Follow shows one session's lines as they are written, from an in-memory ring of the last 4,096 log entries that no restart keeps; it needs view_logs. Reading the logs covers following a connection in full, and the log files themselves. Where to read more Documents at v6.3.4: AlertsAndAuditTrail.md, Syslog.md, Reports.md and FollowConnection.md Wiki: Monitoring and Health, sections 16.2a, 16.6a, 16.7a and 16.7b The public documentation: 16.2 Logs on disk On this forum: Monitoring and health for the metrics, the probes, the dashboard and the SNMP agent; Administrator roles for the permissions named here. See also The Control Deck and hMailServer on Linux: what you get and what you do not
  • 0 Votes
    1 Posts
    163 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.6 (6 October 2026) publishes the first container image, registry.gitlab.com/hmailserver/hmailserver:6.3.6, and a monitor that matches "The response carried no HTTP headers." needs the new sentence, which says who ended the connection, after how many bytes and seconds. See the 6.3.6 announcement. hMailServer 6.3.7 (17 October 2026) publishes registry.gitlab.com/hmailserver/hmailserver:6.3.7, tagged 6.3 and latest until 6.3.8; renames the Control Panel page that holds the metrics listener's settings from API & monitoring to Monitoring & alerts, with the alerts' settings, the outbound webhooks and the SNMP agent on it (Ctrl+K takes the old title to it); and refuses GET /api/v1/metrics/history, and the same history over COM, to a credential confined to named domains, such as a domain administrator, the auditor role given a list of domains or an API key issued for named domains. With IMAP and POP3 now taking the client's address from a PROXY protocol header as SMTP does, the High Availability Runbook says that a balancer's /readyz check must not carry that header: the metrics listener reads none and answers a request behind one 404. See the 6.3.7 announcement. hMailServer 6.3.8 (2 November 2026) publishes registry.gitlab.com/hmailserver/hmailserver:6.3.8, also tagged 6.3 and latest. See the 6.3.8 announcement. This topic covers what hMailServer 6.3.4 offers a monitoring system: the metrics listener's Prometheus exposition and health probes, the repository's Grafana dashboard, the OpenTelemetry push, the server's own metric history, the new SNMPv3 agent and, briefly, the daily block-list check of the server's own addresses. The one thing to know first: the metrics listener is off until you give it a port, and once it is on, the health check to configure is /readyz, because it is proved by a real database round trip rather than by a flag. The metrics listener One small HTTP listener serves both the exposition and the probes. Its seven settings are [Settings] keys, set on the Control Panel's Monitoring & troubleshooting → API & monitoring page (Monitoring & alerts since 6.3.7; Windows only), with PUT /api/v1/settings/ini/{name}, with hmctl ini set, or, on a Linux server whose REST listener is not yet on, with hmailserver --set-setting NAME=VALUE; the topic on settings explains where they are stored and when a save applies. All seven are taken when the listener starts, the three credential settings included, so a saved change waits for a reinitialise (POST /api/v1/server/reinitialize, or Apply now in the Control Panel or the Deck) or a service restart. A token that has been saved and not applied leaves /metrics answering as it did before. Setting Default Effect When it is wrong MetricsServerPort 0 0 means the listener is never created No port, no probes, no metrics, and no complaint anywhere MetricsServerBindAddress 127.0.0.1 An IP literal only, parsed with inet_pton: 0.0.0.0, 127.0.0.1, :: or a specific IPv6 address A host name or localhost is rejected: the log says MetricsServer: Invalid bind address, the listener does not start and the probes go with it MetricsServerAuthToken empty Sent by the scraper as Authorization: Bearer <token> Absent on a non-loopback bind: /metrics answers 503, not 401, and the body names the settings that would open it. The probes keep answering MetricsServerAuthUsername and MetricsServerAuthPassword empty HTTP Basic; with both schemes configured, either satisfies the check As above MetricsServerCertificateFile and MetricsServerPrivateKeyFile empty Both set makes the whole port HTTPS, probes included Set but unusable: the listener still serves the probes in the clear and /metrics answers 503 :: serves both address families; a specific IPv6 literal serves IPv6 only. "Loopback" means the whole of 127.0.0.0/8 and ::1. The probes are answered before any branch that can refuse. A Kubernetes httpGet probe, an ELB or ALB health check and a Docker HEALTHCHECK have nowhere to keep a bearer token. Refused credentials on /metrics are counted in hmailserver_metrics_unauthorized_requests_total rather than logged. Nothing on the request path touches the database or the disk. A refresher thread probes the database every 5 s (every 30 s after a failure), refreshes the delivery-queue aggregates at most every 10 s and only when a scrape has asked for them, and re-reads the certificate files every 5 minutes. One exception since 6.3.4: the two message-store gauges are cached for an hour and recomputed by whichever caller finds them stale, so about once an hour a scrape can run select sum(messagesize), count(*) from hm_messages on the listener thread itself: on a large store, a slow answer to that scrape and to any probe queued behind it. It is a single accept loop serving one connection at a time. A request's header read is bounded at 5 s and a response write at 15 s. For a load balancer's check the High Availability Runbook gives an interval of 5 s and a timeout of 2 s; never point a sub-second health check at it. The three health probes No credential in any configuration; HTTP/1.0 responses that close the connection. Path 200 when Body Use it for /livez Always, if the listener answered; never 503 alive Liveness restarts. It checks nothing on purpose: a database outage must not get a healthy process killed /readyz State is Running and the database has proved within the last 20 s that it can answer; 503 otherwise ready, or not ready: <reason> Load-balancer and VIP routing /healthz The same conditions {"status":"ok\|unavailable","state":"...","database":"up\|down","uptime_seconds":N} Dashboards and human debugging Readiness is four conditions in order, each with its own reason: not ready: server not in running state; not ready: database not connected; not ready: database did not answer the last readiness probe; and not ready: database readiness probe has not completed recently. The last exists because a probe against a database that has stopped answering can hang for as long as DBConnectionAcquireTimeout allows, 60 s by default. Since 6.3.4 a server in maintenance mode answers 503 not ready: draining: POST /api/v1/server/drain enters it, GET reports {"draining","since","sessions","deliveries","drained"}, and DELETE leaves it. Start-up is a refused connection, not a 503: the listener is brought up only after the state has gone to Running. Shutdown is a 503 first, then a refusal: the state goes to Stopping before anything is torn down, so /readyz reports 503 while ShutdownDrainSeconds runs. The High Availability Runbook builds its active/passive design on this: both nodes have the listener on, with MetricsServerBindAddress=0.0.0.0 and a MetricsServerAuthToken, and the VIP is gated strictly on /readyz. The passive node's service is stopped, so its /readyz is refused and it is never routed to. Since the probes need no credential, a missing token shows as missing dashboards, not as a failed failover. Every metric family Thirty-eight metric families in 6.3.4 (thirty in 6.3.3). Counters are monotonic since server start; a restart resets them, which is what hmailserver_start_time_seconds is for. Mail flow Metric Type Meaning, and the trap in it hmailserver_processed_messages_total counter One per delivery pass started, not per message received: a message deferred three times and then delivered counts four hmailserver_messages_delivered_total counter Delivery passes that completed with no failed recipient hmailserver_messages_deferred_total counter Passes rescheduled: a temporary failure, greylisting, or a message the virus scanner could not examine (AVFailAction=1) hmailserver_messages_bounced_total counter Delivery-failure notifications queued to the sender; one can cover several recipients hmailserver_delivery_queue_messages gauge Rows in hm_messages in the delivering state hmailserver_delivery_queue_oldest_message_age_seconds gauge Age of the oldest queued message; 0 when the queue is empty. Alert on this, not on depth hmailserver_spam_messages_total, hmailserver_viruses_removed_total counter Messages classified as spam; viruses removed during delivery preprocessing hmailserver_store_bytes, hmailserver_store_messages gauge Since 6.3.4. The size and the number of the messages in hm_messages, recomputed at most once an hour hmailserver_domain_messages_received_total{domain}, hmailserver_domain_messages_sent_total{domain} counter Only present when MetricsPerDomainEnabled=1; only hosted domains are ever labelled Added in 6.3.4. hmailserver_country_blocked_total and hmailserver_country_blocked_by_country_total{country} count connections and sign-ins refused by a country policy, and hmailserver_spam_trap_hits_total counts hits on a declared spam trap. Three gauges cover the feedback loops' seven-day window: hmailserver_domain_complaints{domain}, hmailserver_domain_complaint_window_messages{domain} and hmailserver_domain_complaint_rate{domain}. They are present only when MetricsPerDomainEnabled is set and complaint processing is on. The rate gauge is a ratio, not parts per million: Complaints.md alerts on hmailserver_domain_complaint_rate > 0.001. Parts per million is the unit of the complaints.rate.high alert rule's threshold, so an expression written in parts per million against the gauge would never fire. Sessions, authentication and TLS Metric Type Meaning hmailserver_sessions{protocol} gauge Concurrent sessions; protocol is smtp, imap or pop3 hmailserver_auth_success_total, hmailserver_auth_failures_total counter A rising failure rate against a flat success rate is a password-guessing run hmailserver_tls_handshakes_total, hmailserver_tls_handshake_failures_total counter The mail protocols only; a failed handshake on the metrics port is never counted here hmailserver_tls_certificate_expiry_seconds{certificate} gauge notAfter as a Unix timestamp per configured certificate, re-read every 5 minutes; at most 128 series. Absent entirely when no certificate is configured. With no certificate row, the ACME certificate is labelled ACME (automatic) hmailserver_metrics_unauthorized_requests_total counter Always 0 when no credential is configured The server and the database Metric Type Meaning hmailserver_start_time_seconds gauge Uptime is time() - hmailserver_start_time_seconds hmailserver_build_info{version,architecture,database_schema_version} gauge Always 1; the information is in the labels hmailserver_state{state} gauge One series per state (unknown, stopped, starting, running, stopping and, since 6.3.4, draining), exactly one of which is 1 hmailserver_workqueue_depth{queue}, hmailserver_workqueue_blocking_tasks_waiting{queue} gauge Seven fixed queue values: Server queue, Maintenance queue, Asynchronous task queue, Name lookup queue, IOCPQueue, SMTP delivery queue, External fetch queue. Asynchronous task queue sends the final 250, so depth there is mail waiting to be acknowledged hmailserver_messagestore_missing_files gauge Always 0 unless MessageStoreConsistencyCheck=1 hmailserver_command_processing_seconds, hmailserver_db_query_seconds histogram Bucket bounds from 0.0001 s to 1.0 s, and from 0.001 s to 10.0 s; both emit the full _bucket, _sum and _count set hmailserver_database_connected gauge 1 when the database answered a real round trip recently hmailserver_database_probe_success_timestamp_seconds, hmailserver_database_probe_age_seconds gauge When it last answered, and how long ago on the server's own clock; /readyz reports 503 once the age passes 20 s hmailserver_db_connections{state} gauge busy and available; available pinned at zero is saturation hmailserver_db_slow_queries_total counter Statements slower than SlowQueryLogMilliseconds; 0 while that setting is 0 Scraping it scrape_configs: - job_name: hmailserver scrape_interval: 30s scrape_timeout: 10s metrics_path: /metrics authorization: credentials: "<the value of MetricsServerAuthToken>" static_configs: - targets: ["mail.example.com:9100"] Use scheme: https when the certificate pair is set, and keep scrape_timeout well under scrape_interval. The wiki gives nine alerting rules against these metrics, among them hmailserver_database_connected == 0 for 2m, hmailserver_delivery_queue_oldest_message_age_seconds > 3600 for 15m and hmailserver_tls_certificate_expiry_seconds - time() < 14 * 86400. The Grafana dashboard The repository ships hmailserver/docs/grafana-dashboard.json. Import it as it is and pick your Prometheus data source; the instance variable, from label_values(hmailserver_build_info, instance), selects the server. Its uid is hmailserver-overview; it refreshes every 1m and opens on now-24h. Its panels array has 19 entries: four row headers, five stat panels and ten time series. Every rate is over [5m] and every query is filtered by instance=~"$instance". Health: server state (hmailserver_state == 1), database, uptime, missing message files, and TLS certificate expiry, amber below 21 days and red below 7. Mail flow: delivery outcomes (delivered, deferred and bounced, stacked), queue depth with the oldest message's age on a right-hand axis, messages processed with spam and viruses removed, and active sessions by protocol. Authentication and TLS: authentication outcomes and TLS handshakes, as rates. Latency and database: command processing and database query latency (p50, p95 and p99 by histogram_quantile over the two histograms' _bucket series), the connection pool by state, and slow queries. What the dashboard does not draw: the two database probe gauges, the two work-queue gauges, hmailserver_metrics_unauthorized_requests_total, the per-domain counters, the two store gauges and the six families added in 6.3.4. hmailserver_build_info is read by the instance variable and by no panel. The metric history inside the server The server keeps a short history of its own, so the Control Panel's dashboard can show 24 hours, 7 days and 30 days without Prometheus. MetricsHistoryDays (default 7; 0 switches it off) is the days kept in hm_metricsamples: one row per metric per minute, plus a sample at service start. Fourteen metrics are stored in 6.3.3, named as the exposition's without the hmailserver_ prefix and with the label folded in, such as sessions_smtp and processed_messages_total. 6.3.4 adds store_bytes and store_messages, whose history begins the day the server first samples them, and country_blocked_total and spam_trap_hits_total. Read it with GET /api/v1/metrics/history?metric=…&range=24h|7d|30d, or on Windows over COM with Utilities.GetMetricHistory. It has no labels and no histograms and is not a replacement for Prometheus, but it survives a restart. Pushing to an OpenTelemetry collector Three OTLP/HTTP signals, each a no-op until its own endpoint is set. The pushed metric names are the Prometheus names verbatim. Setting Default Effect OtelMetricsEndpoint empty OTLP metrics; the default path is /v1/metrics OtelMetricsInterval 60 Seconds between exports, clamped to 5–3600 OtelEndpoint empty OTLP traces only: one span per delivery attempt, joined to the trace named by the message's own traceparent; default path /v1/traces OtelLogsEndpoint empty OTLP logs: the same lines, categories and mask the log files get; default path /v1/logs What is pushed: the twelve counters that predate 6.3.4 and, since 6.3.4, hmailserver_spam_trap_hits_total, sessions as one gauge with a protocol attribute, hmailserver_messagestore_missing_files, and the per-domain counters when they are enabled. The histograms, the database and queue gauges and certificate expiry are scrape-only. On Linux, in every release from 6.3.0 to 6.3.3, a collector that accepted the connection and never answered held the exporter, and the stop that waits for it, for good; 6.3.4 fixes that. The SNMPv3 agent (since 6.3.4) For a monitoring system that speaks SNMP and not Prometheus or OpenTelemetry, 6.3.4 adds a read-only agent answering the mail MIB (RFC 2789, MTA-MIB) and the standard SNMP groups from the same counters /metrics serves; an object /metrics has no counter for is answered noSuchObject, not made up. It speaks SNMPv3 with the User-based Security Model for one user at authPriv only (HMAC-SHA-256 and AES-128), and SNMPv2c as an option that stays off even when the agent is on. SNMPv1 is not answered, a Set is answered noAccess, and nothing is sent unasked: no traps, no informs. It is off as shipped, and an empty allow list answers nobody. Setting (hm_settings) REST key Default Meaning SnmpEnabled snmp_enabled off The whole agent; off, no socket is opened SnmpPort snmp_port 161 The UDP port SnmpBindAddress snmp_bind_address empty, every address 127.0.0.1 for a collector on the same machine; otherwise an address on a management network SnmpAllowedAddresses snmp_allowed_addresses empty Addresses and CIDR ranges, at most 64. Name the managers one by one: UDP does not prove a source address SnmpV3User snmp_v3_user empty The one SNMPv3 user SnmpV3AuthPassword, SnmpV3PrivPassword snmp_v3_auth_password, snmp_v3_priv_password (write-only) empty 8 to 64 ASCII characters each SnmpV2cEnabled, SnmpV2cCommunity snmp_v2c_enabled, snmp_v2c_community (write-only) off, empty The community crosses the network in the clear; an answer is never more than eight times its request SnmpContact, SnmpLocation snmp_contact, snmp_location empty sysContact and sysLocation They are on /api/v1/settings (the server group), on the Control Deck's Settings page under Server and in hmctl settings. There is no COM property for any of them, and in 6.3.4 no Control Panel page, so Windows and Linux are configured the same way; since 6.3.7 they are the SNMP tab of the Control Panel's Monitoring & alerts page on Windows, which 6.3.5 and 6.3.6 had on Logging. They can also be given as HM_PROPERTY_SnmpEnabled and so on in the environment, which reaches the database only when hmailserver --apply-environment is run (hMailServer.exe --apply-environment on Windows): the service does not read the environment itself. Each setting is read when the agent starts, at the next service start or Reinitialize. The two passwords and the community are stored protected, never read back, and not carried by a backup: enter them again after a restore. Snmp.md's example keeps every password off the command line. On the server, for a manager at 192.0.2.10: $ umask 077 $ cat > snmp.json {"snmp_allowed_addresses": "192.0.2.10", "snmp_v3_user": "monitor", "snmp_v3_auth_password": "...", "snmp_v3_priv_password": "...", "snmp_contact": "[email protected]", "snmp_location": "Rack 4, London", "snmp_enabled": true} ^D $ hmctl settings set --from-json snmp.json $ rm snmp.json $ hmctl api POST /api/v1/server/reinitialize On the manager, with Net-SNMP 5.9 or later, defVersion 3, the user (defSecurityName), both passphrases, defSecurityLevel authPriv, defAuthType SHA-256 and defPrivType AES go in ~/.snmp/snmp.conf, readable by you alone (chmod 600); snmpwalk mail.example.com 1.3.6.1.2.1.28 then walks the mail MIB. What it serves. In the MTA-MIB mtaGroupTable has one row per protocol the server runs: 1 SMTP, 2 POP3, 3 IMAP. What has a counter is answered: mtaStoredMessages is the delivery queue's depth, mtaTransmittedMessages is hmailserver_messages_delivered_total, mtaGroupOldestMessageStored is the oldest queued message's age, and mtaGroupInboundAssociations is hmailserver_sessions for the protocol. mtaReceivedMessages is noSuchObject, because no counter of messages received exists, and so are the volume, recipient and conversion columns. Sixteen scalars at 1.3.6.1.4.1.32473.1.2.<n>.0 carry /metrics series directly, among them hmailserver_state as a number at 14 (3 is running, 5 draining) and the two delivery-queue gauges at 15 and 16. The enterprise number is a placeholder. 1.3.6.1.4.1.32473 is the number RFC 5612 reserves for documentation and examples; it is not registered to this project with IANA. When a number is registered, sysObjectID, the scalars and the engine ID's prefix move to it in one release, and a manager configured for these OIDs will need them changed. There is no MIB module file yet, for the same reason. When it does not start. Error 7420: the port could not be bound, because another program holds it (the operating system's own SNMP service, on Windows or Linux, listens on 161 when it is installed) or, on Linux outside the packaged unit, the process lacks CAP_NET_BIND_SERVICE; the server runs on without the agent. 7421: the engine's seed or boot count in hm_settings could not be read or written. 7422: an exception left the agent's thread; a Reinitialize starts it again. On Linux the packaged hmailserver.service has CAP_NET_BIND_SERVICE for port 25, which is what lets it bind 161. No container image of 6.3.4 is published; a container built from the repository's Dockerfile publishes the port as UDP (-p 161:161/udp) and needs a host name of its own, since two started with one fixed hostname are one SNMP engine. The daily block-list check, briefly Since 6.3.4 the server looks the addresses it sends from up on DNS block lists once a day and raises the reputation.listed alert when one is listed. The watch is on in a new installation and off after an upgrade, and is read at GET /api/v1/reputation; the topic on DKIM rotation, reports and the reputation watch has the lists, the addresses checked and what a failed lookup means. Where to read more Wiki: Monitoring and Health and High Availability Runbook Documents at v6.3.4: Snmp.md, HighAvailabilityRunbook.md, ReputationWatch.md, Complaints.md and the dashboard itself, grafana-dashboard.json The public documentation: 16.3 Prometheus metrics and health probes On this forum: Alerts, the audit trail, syslog and the journal, and per-domain reports, Reading the logs, and Warm standby and high availability for the pair the probes gate. See also The Control Deck and hMailServer on Linux: what you get and what you do not
  • 0 Votes
    1 Posts
    128 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) regroups the Control Panel's sidebar: the Journaling and dual delivery page is Journaling & dual delivery, in the new Compliance group rather than under Monitoring & troubleshooting as the table below has it, and Ctrl+K takes the old title to it. See the 6.3.7 announcement. A journal rule sends a copy of a domain's mail, or of one account's, to an address. Exchange calls it journaling and sends an envelope report with the message attached; Google calls it dual delivery and sends the message itself. In hMailServer they are one mechanism with two shapes of copy, new in 6.3.4 (27 September 2026, schema 6104; the rules themselves arrived with schema 6075). This topic covers what a rule says, the two shapes, when a rule is evaluated, how it avoids looping, where it is edited, and what it deliberately does not do. The one thing to know first: a journal rule covers mail that comes in through the SMTP session, a fetch account or the send API, and nothing this server queues itself. A compliance archive built on a journal rule therefore has known gaps, and they are listed exactly below. Nothing is journalled until a rule exists. With an empty table the whole of this costs one comparison per accepted message. What a rule says Field Meaning scope domain or account: whose mail the rule is about target The domain name, or the account address. It must be a domain this server hosts, or an account in one direction inbound (a message with a local recipient the rule covers), outbound (a message from an authenticated sender it covers), or both journal_address Where the copy goes. One address, local or anywhere else, checked as an address before the row is written. It may be a mailbox in the very domain the rule journals; it may not be the account an account rule names mode report or copy enabled A rule that is off copies nothing and keeps everything it said note Why the rule exists. At most 255 characters The rules are stored in hm_journalrules. The row holds the domain id and, for an account rule, the account id, so nothing breaks when a domain or an account is renamed under it. A domain's rules go with the domain when it is deleted, and a rule naming an account goes with the account. A rule has no conditions. It covers all of a domain's or an account's mail in the direction it names. There is no "only messages over 10 MB" and no "only from these senders"; a mail flow rule is the place for that. The two shapes report is the Exchange journal shape: a multipart/mixed message whose first part is the envelope report and whose second part is the original message attached as message/rfc822, written through byte for byte. The report is four fixed fields, one per line, in one order: Sender: [email protected] Subject: Quarterly figures Message-Id: <[email protected]> Recipient: [email protected] Recipient: [email protected] The envelope is the point. A message's To and Cc headers are what the sender typed; the Recipient: lines are where the message actually went, blind copies and expanded lists included. A message with no envelope sender - a bounce - writes Sender: <>. copy is the message itself, as it was accepted, with one header line (X-hMailServer-Journal) in front of it and nothing else changed: not the body, not its transfer encoding, not its signature. The file is written byte for byte rather than through the MIME writer. What a migration or a second mail system wants is the mail, not a report about it; that is what copy is for. Both shapes are queued through the ordinary delivery queue, so a journal address somewhere else is delivered, retried and bounced as any other mail is, and a local one lands in a mailbox. The envelope sender of a journal message is the journalled domain's postmaster, not the person who wrote the message. A journal address that stops answering is the administrator's problem, and bouncing it to the sender would tell somebody outside this server that their mail is being copied. When a rule is evaluated Once per accepted message, in the SMTP session, after the row is saved and the message file is settled - the same place, and the same recipient list, the on-disk archive decision is made from. Not at delivery: the deliverer runs again for every retry and for every route a message splits into, and a compliance archive that gained a second copy because a remote host was busy would be worse than useless. One report or copy per matching rule per message, not per recipient. A domain rule that matches a message addressed to three accounts of the domain sends one report, with all three on it. There are three points of entry where rules are evaluated: The SMTP session. The outbound half judges an authenticated sender; this is what a desktop or phone client uses. A fetch account. Mail a mailbox collects from another provider by POP3 or IMAP is journalled once per message, at the same point in that path. A download has no authenticated sender, so it is the inbound half of a rule that speaks for it, and it has no SMTP session, so its lines go to the application log. The send API, POST /api/v1/send, which is new in 6.3.4 and is journalled in it (CHANGELOG lists the fix in its 6.3.4 section). The key's sender counts as the sender an outbound rule judges when that sender is an account here. What a journal rule does not see A message the server queues itself is queued past all three points and is not journalled in either direction, even when it is addressed to a mailbox a rule covers: Not journalled Note A message the webmail queues directly POST /api/v1/me/messages A send-later draft A message written by an event script A mail flow rule's forward, copy or reply An automatic reply A Sieve redirect An account's forwarding A delivery report Two consequences follow, and both matter to anyone relying on this for compliance: The outbound half covers SMTP submission and the send API, and not the browser. Mail a person sends from the webmail is not journalled by an outbound rule. The inbound half covers mail from outside this server, and not what the server sends on to a mailbox of its own. Two further limits of the same kind: A message that was refused is not journalled. A message rejected at RCPT TO or by a rule never existed as far as this is concerned. A message held for review is journalled, because it was accepted. Not looping There is one guard, and it takes nobody's word for anything. Every journal message this server writes carries X-hMailServer-Journal, and its value is the rule's id, a full stop, and a mark: 128 random bits the server process chose when it first needed one. The evaluation reads that header before it does anything else and stops only when the mark is this process's own. A journal message that comes back into this server - relayed by the far end, forwarded by the journal mailbox's owner - is never journalled again, and the guard stops the copy, not the delivery: the mailbox still gets its mail. A message carrying the header with any other value is journalled exactly as it would have been without it, and the attempt is noted in the log. The header's name is in the published document, so a guard that honoured the header as written would let anybody under an outbound compliance rule keep a message out of the archive by typing one line into their mail client. An archive that the people it is about can opt out of is not an archive. Three properties of the mark to understand: It is per process, and nothing is stored for it. A restart forgets the marks it wrote, so one of this server's own reports that returns after a restart is journalled once more, and the report that makes carries a mark the running process does know, so it stops there. A loop cannot run away; the price of keeping no secret on disk is one duplicate. It is not a signature over the message. It travels in the messages it marks, so whoever can read this server's journal mail - the journal address itself, and anything relaying to it - could quote a mark back in a message of their own and keep that one message out of the archive. If that relay is a worry, journal to a local mailbox, or over an authenticated route. There is no second guard, and no rule is skipped for where its journal address points. Journalling a domain into a mailbox of that domain works. example.com to [email protected] sends one report per message into that mailbox and nothing cascades, because a journal message is written straight into the delivery queue and never passes an evaluation point. It used to be refused at run time, silently: the rule matched, the copy was dropped with a line in the log, and every surface went on showing the rule as enabled. That guard went. The one arrangement refused is an account journalled into itself, and it is refused when the rule is written rather than found out later: it would put every message in the mailbox twice and archive nothing anywhere. Where it is edited Journal rules are the server administrator's. A rule says that somebody's mail is copied to an address outside their sight, so a credential confined to named domains is refused even though a rule names one. Surface How Platform REST GET/POST /api/v1/journal-rules, PUT/DELETE /api/v1/journal-rules/{id} Windows and Linux The Control Deck The Journaling view: every rule, a switch on each row, and a form to add one Windows and Linux The Control Panel The Journaling and dual delivery page, under Monitoring & troubleshooting (since 6.3.7 Journaling & dual delivery, in the Compliance group; Ctrl+K takes the old title to it), over three Utilities methods (GetJournalRules, SaveJournalRule, DeleteJournalRule) Windows only hmctl hmctl journalrule list\|create\|update\|delete A client over the REST API PowerShell module Get-HmJournalRule, New-HmJournalRule, Set-HmJournalRule, Remove-HmJournalRule Over the REST API Over REST, reading needs view_configuration and writing needs manage_server, so the auditor role reads the rules and writes none. Every surface goes through one function on the server, Journaling::SaveRule, so the three small vocabularies (domain/account, inbound/outbound/both, report/copy) mean the same thing whichever you use, and a refusal reads the same sentence. Writing, changing or removing a rule is on the audit trail. Setting up a compliance copy for one domain: Decide the journal address. A mailbox in the same domain, such as [email protected], is allowed; an address elsewhere is delivered through the ordinary queue. Create one rule with scope domain, target the domain name, direction both, mode report and the journal_address. Over SMTP - from a mail client signed in to an account, or from outside, and not from the webmail - send one message to two recipients of the domain, one of them as a blind copy, and open what arrives at the journal address: one report for the message, with a Recipient: line for each envelope recipient. Then send one message from the webmail and confirm that it is not journalled, so that whoever owns the archive knows the gap is there. A rule is refused before it is written for an unhosted target, a word outside the three vocabularies, no journal address, one that is not an address, one carrying a line break, and an account journalled into itself. Journaling beside three other features Sealed mail. The journal keeps the bytes verbatim, so a message that was encrypted end to end still decrypts from the copy - but nobody reviewing the journal can read its content. Restricted delivery. A journal rule's copy of a restricted account's mail goes where the rule says, because the copy is the administrator's and not the account's forward. Rules and Sieve. A rule's forward, copy or reply and a Sieve redirect are among the messages the server queues itself, so they are not journalled. What it does not do It is not the archive. ArchiveDir and ArchiveDomains keep files on this server; a journal rule sends mail elsewhere. Neither replaces the other, and turning one on does not turn the other off. It is not eDiscovery. A case searches the mailboxes, their deleted items and the archive on this server, and a journal copy has left it. A journal address that is a mailbox here is searched like any other custodian. It is not an account's forwarding. A forward is the account holder's, and can deliver instead of the mailbox. A journal copy is the administrator's, and never changes what the mailbox gets. It is not a mail flow rule's forward or copy action. Those have conditions, run per delivery attempt, and can be edited by the account. It is not retention. Once a journal message leaves the queue it belongs to whatever receives it. This server keeps no second copy, expires nothing and withdraws nothing; deleting a rule stops the next copy and does not reach the ones already sent. It is not encryption or redaction. A journal address outside your control sees everything the recipient sees. Where to read more The document at the 6.3.4 tag: Journaling.md, with Discovery.md, SendApi.md, SealedMail.md and RestrictedDelivery.md Wiki: Routes and Relays (section 14.2d) and Compliance and eDiscovery On this forum: the 6.3.4 release announcement; Rules and Sieve for the forwards and redirects a journal rule does not see; restricted delivery; the send API; compliance, eDiscovery and legal holds; and the archive
  • 0 Votes
    1 Posts
    134 Views
    ProgressiverobotP
    An organisation that keeps its people in an identity provider wants two things of its mail server: mailboxes that follow the directory, and a sign-in that goes through it. Since 6.3.4 hMailServer has both. SCIM 2.0 at /scim/v2 lets Microsoft Entra ID or Okta make, rename, suspend and remove accounts, with their aliases and distribution lists. Single sign-on, by OpenID Connect or SAML 2.0, opens the webmail at /portal and the Control Deck at /. The one thing to know first: they answer different questions and take different credentials. SCIM answers to an API key whose role holds provision and to nothing else. Single sign-on opens the two pages only: IMAP, POP3 and SMTP still take the account's password and its application passwords. Passkeys and account recovery are covered in the topic on signing in. How the pieces fit Piece Direction What it does LDAP directory authentication This server asks the directory Checks a password when somebody signs in SCIM provisioning The identity provider calls this server Says what changed, as it changes Single sign-on The browser goes to the identity provider and back Signs a person in to the webmail or the Control Deck oidc_auto_provision, saml_auto_provision In passing, at sign-in Makes a mailbox for whoever arrives with a verified address, and never touches it again SCIM is the deliberate version of auto-provisioning. The two work together: SCIM keeps the mailboxes, and single sign-on is how the people it made sign in to them. Where the settings are. The single sign-on settings are the REST API's and the Control Deck's. There is no COM property and no Control Panel page for them, on Windows or on Linux. SCIM: the key and the permission A SCIM request is a request of the REST listener. RestApiPort has to be on and the listener reachable from the identity provider's service over TLS, with a certificate it trusts (RestApiCertificateFile, or the ACME certificate). A body is at most 64 KB, and the key's requests count against the listener's budget of 200 in ten seconds per credential, beyond which the answer is 429 with Retry-After. The key's role must hold provision. The built-in serveradmin role holds it, so every key issued with "scope":"full" provisions; no other built-in role does, so a read-only key is answered 403. The way to issue a key that provisions and does nothing else is a custom role: curl -u Administrator:your-admin-password -H "Content-Type: application/json" \ -d '{"name":"entra-provisioning","description":"Microsoft Entra ID","permissions":"provision"}' \ https://mail.example.com:8443/api/v1/roles curl -u Administrator:your-admin-password -H "Content-Type: application/json" \ -d '{"label":"Entra provisioning","role":"entra-provisioning"}' \ https://mail.example.com:8443/api/v1/apikeys The second request, POST /api/v1/apikeys, answers the hmapi_... token once, and that token is the identity provider's secret. provision is needed for SCIM's reads as well as its writes. Both requests present the administrator password, which works while no second factor is enrolled on it. With one enrolled, sign in once at POST /api/v1/session with the code in X-hMailServer-OTP, and send both requests with the hmailsession cookie and X-Requested-With: hMailServer: since 6.3.4 a one-time code is taken once, so a second request carrying the same code within the same half minute is refused with 401. Every other credential is refused. The administrator password, an account's password and a browser session are answered 401 on /scim/, in SCIM's shape, with WWW-Authenticate: Bearer. A key confined to named domains ("domains":"example.com") provisions those domains and learns nothing about the rest: listings are filtered, an account elsewhere is the same 404 as one that is not there, and an address elsewhere is refused 400 invalidValue. A key may not change or remove an account whose authority it does not take in, and is answered 403. A key whose role holds provision alone takes in no administrator at all: not a domain administrator, not a help desk, not an auditor. A full key confined to named domains takes in an administrator of those domains and nobody else. A full key issued for every domain takes in every account. SCIM: the routes and the mapping Route What it does GET /scim/v2/ServiceProviderConfig What this server supports: patch, filter (200 at most), changePassword and etag; not bulk and not sort GET /scim/v2/Schemas, GET /scim/v2/ResourceTypes The User and Group schemas, listing only what this server keeps; the two resource types GET, POST /scim/v2/Users The accounts: listed and filtered, or one made GET, PUT, PATCH, DELETE /scim/v2/Users/{id} One account: read, replaced, changed, removed GET, POST /scim/v2/Groups The distribution lists: listed and filtered, or one made GET, PUT, PATCH, DELETE /scim/v2/Groups/{id} One list: read, replaced, changed, removed Every answer under /scim/ is application/scim+json, the refusals included. A User is an account. SCIM This server id, userName The account's address. The id is the address, so a rename changes the id. name.givenName, name.familyName The first and last names, sixty characters each displayName The two names composed. Written alone to an account that already has a name, it is accepted and not kept. active The account's switch. false suspends it and deletes nothing. emails The address, type work and primary, and every alias whose value is the address, type other password Write-only. Judged by the password policy, the reuse history and, when it is on, the breach check. externalId The identity provider's own id for the person, at most 255 characters (schema 6096) meta.created and meta.lastModified are not answered, because the tables keep no such times. A Group is a distribution list: id and displayName are the list's address, and members are its members. The sharing groups at /api/v1/groups are folder-permission principals and are not SCIM groups. Attributes the schemas have and this server does not keep, such as title, phoneNumbers and the enterprise extension's department and manager, are accepted and not kept. A misspelt attribute or path is refused by name. SCIM: what each operation does Making a user. POST /scim/v2/Users needs a userName at a domain this server hosts and the key reaches. A body with no password, which is what Entra sends, makes the account with thirty-two random bytes for one, which nobody is ever told. An address something already has is 409 uniqueness. Filters. These and no others: userName eq, userName co, userName sw, externalId eq, emails.value eq and active eq. Any other filter is 400 invalidFilter. count is at most 200, and sortBy and sortOrder are ignored. Groups filter on displayName eq only. Changing. PUT takes the body a POST takes, and an attribute it leaves out is left as it is. PATCH takes a PatchOp, with op in any case, as Entra capitalises it. If-Match is honoured on PUT, PATCH and DELETE; a version other than the current one is 412. A change is judged whole before anything is written. Suspending. active false refuses every sign-in from the next attempt, refuses mail to the account, ends its browser sessions and drops its live IMAP, POP3 and SMTP connections. The mailbox, the aliases and the lists are untouched. Renaming. A new userName renames the account through the mailbox move. It is refused 409 while the mailbox is in use. The id changes, the old id is 404 from then on, and the old address is kept as an alias until a later request that sends emails leaves it out. Every alias of the account is part of emails, including one an administrator made. An identity provider that replaces emails removes the aliases it does not list. Where aliases are managed on this server and not in the directory, do not map emails beyond the work address. Removing. DELETE /scim/v2/Users/{id} removes the account and its mailbox, every alias emails answered for it, and its row in every distribution list. It answers 204. Nothing of it can be put back but from a backup. Entra sends active false straight away and the DELETE thirty days after a user is soft-deleted, so the suspension is the safe first step. Five diagnostics in the error log, HM7220 to HM7224, name what a SCIM write left behind when it failed part way. Every SCIM write is on the audit trail, each row naming key:<id>. Setting up Microsoft Entra ID for SCIM Issue the key as above and note the token. In the Entra admin centre, Enterprise applications > New application > Create your own application > Integrate any other application you don't find in the gallery (Non-gallery). Provisioning > Automatic. Tenant URL: https://<host>:<port>/scim/v2, where the port is RestApiPort. Secret token: the hmapi_... token. Test connection asks for a user that does not exist and expects an empty list, which is what this server answers. Mappings > Provision Microsoft Entra ID Users: map userName from userPrincipalName where that is the mailbox address, or from mail where it is not. emails[type eq "work"].value must be the same address as userName. Keep active, name.givenName, name.familyName, displayName and externalId, and delete the mappings this server does not keep. Mappings > Provision Microsoft Entra ID Groups, for mail-enabled groups that should be distribution lists: map displayName from mail, keep members, and delete the mapping of externalId. Assign the users and groups, and switch Provisioning Status on. Setting up Okta for SCIM Issue the key as above. Applications > Create App Integration > SAML 2.0 or SWA, then on General enable SCIM provisioning. Provisioning > Integration: SCIM connector base URL https://<host>:<port>/scim/v2; Unique identifier field for users userName; Supported provisioning actions: Import New Users and Profile Updates, Push New Users, Push Profile Updates, Push Groups; Authentication Mode HTTP Header, and the token in Authorization. Provisioning > To App: enable Create Users, Update User Attributes and Deactivate Users. Push Groups sends a group under its Okta name as displayName, and here that is the list's address, so a push of Engineering is 400 invalidValue. Name each pushed group with the list's address, or leave Push Groups off. Okta sends emails with the primary address alone, so an alias made on this server is removed by Okta's next update. Scim.md gives no steps for Google Workspace: at the time it was written, a custom application there could not be pointed at a SCIM endpoint. Single sign-on with OpenID Connect Register this server at the issuer as an authorization-code client with PKCE, with the redirect URI <oidc_redirect_base_url>/portal/oidc/callback. Setting What it is oidc_issuer For example https://login.example.com. The discovery document is read from <issuer>/.well-known/openid-configuration and cached for an hour. Empty offers no single sign-on at all. oidc_client_id The client id this server is registered under oidc_client_secret Its secret, stored protected and never emitted. Empty for a public client. oidc_scopes openid email by default oidc_redirect_base_url The address the webmail is reached at from outside. Empty falls back to import_oauth_redirect_base_url. oidc_auto_provision Whether a verified address with no mailbox here gets one. Off. oidc_administrators The addresses that open the Control Deck as the administrator. Empty means single sign-on reaches the webmail only. The flow is GET /portal/oidc/start[?deck=1] to the issuer and GET /portal/oidc/callback back. The callback verifies the id_token: its signature against the issuer's published keys (RS256 and ES256 only), iss, aud, exp, iat and nbf with a minute of skew, and the nonce. It then takes email, and only when email_verified says the issuer verified it. With oidc_issuer set, both sign-in pages draw a Sign in with button naming the issuer's host. What they draw is decided by GET /api/v1/signin-methods, which needs no credential and says nothing about who exists here. With SAML set up it answers saml: true and saml_label, and both pages draw a second button. The callback does not make the session. It mints a one-use ticket, good for two minutes, and the page hands it to POST /api/v1/session, which is where every browser session is made. The ticket is bound to the browser by an hmsso cookie, and POST /api/v1/session refuses a ticket that arrives without it. Mapping. The verified address is looked up as a mailbox. With auto-provisioning off, the default, a sign-in with no mailbox is refused. With it on, a mailbox is made, but only at a domain this server already has, and with a random password nobody is told. For the Control Deck, the address must be in oidc_administrators. An issuer that says somebody is an administrator is not obeyed. Single sign-on with SAML 2.0 For identity providers that speak only SAML 2.0: ADFS above all, and an Entra ID or Okta application set up the SAML way. It ends in the same ticket, the same POST /api/v1/session and the same mapping rules. Setting What it is saml_idp_entity_id The identity provider's entity ID. Empty, the default, offers no SAML sign-in at all. saml_idp_sso_url Its sign-in URL for the HTTP-Redirect binding saml_idp_certificate Its signing certificate in PEM, or two while it rolls its key over. Write-only. saml_sp_entity_id This server's entity ID. Empty means <base URL>/saml. saml_email_attribute The attribute the address is read from when the NameID is not an email address saml_require_signed_assertion Whether the Assertion must carry its own signature. On by default. saml_auto_provision, saml_administrators As the OpenID Connect settings of the same names saml_button_label The words on the button: Sign in with SAML by default The base URL is OpenID Connect's: oidc_redirect_base_url, falling back to import_oauth_redirect_base_url. From it follow the entity ID <base URL>/saml, the ACS URL <base URL>/portal/saml/acs and the metadata at <base URL>/saml/metadata. Identity provider What to enter there What to copy back Microsoft Entra ID Identifier (Entity ID) <base URL>/saml; Reply URL <base URL>/portal/saml/acs; token encryption off Login URL to saml_idp_sso_url; Microsoft Entra Identifier to saml_idp_entity_id; Certificate (Base64) to saml_idp_certificate Okta Single sign-on URL <base URL>/portal/saml/acs; Audience URI (SP Entity ID) <base URL>/saml; Name ID format EmailAddress; Assertion Encryption Unencrypted Identity Provider Single Sign-On URL to saml_idp_sso_url; Identity Provider Issuer to saml_idp_entity_id; X.509 Certificate to saml_idp_certificate ADFS A claims-aware relying party trust, imported from <base URL>/saml/metadata or entered by hand; E-Mail-Addresses sent as E-Mail Address https://<adfs host>/adfs/ls/ as saml_idp_sso_url; the Federation Service Identifier as saml_idp_entity_id; the Token-signing certificate as saml_idp_certificate The flow. GET /portal/saml/start[?deck=1] to the identity provider, with an AuthnRequest, and the hmsaml cookie POST /portal/saml/acs back from it, with the Response GET /portal/saml/finish on from the ACS, where the cookie is asked for GET /saml/metadata this service provider, for the identity provider The ACS takes a form of at most 256 KB; a larger body is answered 413. The signature is verified against the certificates an administrator stored and never a key the message carries. The signature method must be rsa-sha256 or ecdsa-sha256, and SHA-1 is refused. A Response is taken only when it answers a request this server issued, not yet answered, with the same RelayState. The checks refuse signature wrapping, a replayed Response or assertion, a Response nobody asked for and an encrypted assertion; comment injection in the NameID gains nothing, because the address read is the whole of what was signed. A saml_idp_certificate that cannot be read stops every SAML sign-in. The settings refuse one, so it comes only from a database edited by hand; HM7320 in the error log then names the setting to paste again. A SAML identity provider says nothing like email_verified. The attribute the address is read from must be one the directory controls, such as mail or the user principal name, and never one a person can set themselves. This server cannot check that. What it does not do SCIM: no bulk, no sorting and no POST .search (/Bulk, /Me and POST .search answer 501); no enterprise user extension; no groups but distribution lists; not a SCIM client. externalId is shown by nothing but SCIM and COM's Account.ExternalId, which is Windows-only. Single sign-on, both kinds: no logout in either direction, so an account disabled at the identity provider keeps a session it already started until that session's own ceiling ends it. One OpenID Connect issuer and one SAML identity provider. Nothing is enforced at IMAP, POP3 and SMTP. OpenID Connect: no claims mapping. The email claim and nothing else. SAML: no encrypted assertions, no signed requests, no metadata fetch, no artifact binding and no sign-in started by the identity provider. Where to read more The operator documents: Scim.md, PasskeysAndSingleSignOn.md and Roles.md. The wiki: The REST API and APIs Reference. The public documentation: chapter 24, The REST API and chapter 36, The Control Deck and the webmail portal. On this forum: passkeys, recovery and the rest of sign-in are in Signing in: passwords, second factor, passkeys, single sign-on; LDAP sign-in and directory synchronisation are in Active Directory and LDAP sign-in; The Control Deck; the 6.3.4 announcement; and the REST API guide.
  • 0 Votes
    1 Posts
    126 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) refuses a state-changing REST request that carries a credential when the browser making it reports it as cross-site (Sec-Fetch-Site, or an Origin that is not this server's), so a page on another site cannot post to these routes with a credential, whether a key or the administrator password the browser holds; a program or script that sends no such header, as the curl examples here do not, is unaffected. See the 6.3.7 announcement. hMailServer 6.3.8 (2 November 2026) answers 400, saying why, before a route reads a member, to a body that is not one JSON object, names a member twice, holds an escaped U+0000 or passes 100,000 values or 32 levels, so correct a program that sends such a body to these routes; and it raises account.locked only for a locked name that is an account's own. See the 6.3.8 announcement. Since 6.3.4 hMailServer has three ways for another program to work with the server's mail: a send API, through which an application sends as one sender and asks afterwards what became of each recipient; mail in to a webhook, where the mail for an address or a whole domain is posted to a URL; and outbound webhooks, which tell a program what the server's mail did. Nothing is on until a key, a route or a subscription is made. The one thing to know first: each of the three takes a different credential, and the difference is deliberate. Feature Routes Who may use it The send API /api/v1/send An API key that names a sender and whose role holds send_through_api. Not the administrator password, not an account. Mail in to a webhook /api/v1/inbound-webhooks view_configuration to read, manage_inbound_webhooks to change. Meant for an API key's role, confined to the application's domain. Outbound webhooks /api/v1/webhooks The administrator password, or an account whose role holds the permission. No API key reaches these routes; the refusal is 401. All three are made and managed over the REST listener, so RestApiPort has to be on; outbound webhook subscriptions can also be managed over COM, on Windows. The send API: the key POST /api/v1/me/messages is the webmail's send and stays the webmail's. The send API is for programs. Route What it does POST /api/v1/send Sends a message as the key's sender. 202 {queue_id, message_id, recipients, recorded} GET /api/v1/send The key's own sends, newest first, paged GET /api/v1/send/{queue_id} One of them, with the state of each address its body named A key sends when two things are true of it: its role holds send_through_api, and it names a sender. Either alone sends nothing. Three fields are set when the key is made, on POST /api/v1/apikeys with the administrator password or on the Control Deck's API keys page. Field What it is send_from The one sender the key may use: an address of a domain this server hosts, or *@domain for any address of that domain. Empty, the default, means the key cannot send. send_per_minute The key's own ceiling on messages a minute. 0, the default, sets none. send_per_day The key's own ceiling on messages in any 24 hours. 0 sets none, which only a key whose sender is an account's address may have. The built-in serveradmin role holds the permission, so a key made with "scope": "full" holds it. No other built-in role does. A custom role is how a key that sends and does nothing else is made: POST /api/v1/roles {"name": "mailer", "description": "sends mail", "permissions": "send_through_api"} POST /api/v1/apikeys {"label": "shop confirmations", "role": "mailer", "send_from": "[email protected]", "send_per_day": 5000} The route refuses with 400, among other things, a send_from at a domain this server does not host (a domain alias does not count), at a domain that is switched off, on a read-only key, or outside the domains a key is confined to. A send_from that is *@domain, or an address that is no account's, needs a send_per_day above 0, because such a sender has no account's sending quota to hold it. A key's sender cannot be changed afterwards: a new sender is a new key. What *@domain grants. To the server, such a key is every mailbox of that domain sending as itself. It can post to a members-only list as any member and send as the domain administrator's mailbox. The display name and reply_to are the application's own and are not checked against anything; what the key vouches for is the address. On Windows, the desktop Control Panel's key page does not show these three fields and cannot set them, so a key made there cannot send. The Control Deck and the route do. The send API: sending and asking afterwards curl https://mail.example.com:8443/api/v1/send \ -H "Authorization: Bearer hmapi_0123…" \ -H "Content-Type: application/json" \ -d '{"to":["[email protected]"],"subject":"Your order","text":"Thank you."}' Member Rule from Optional when the key's sender is an address, required when it is *@domain. "address" or "Name <address>". to, cc, bcc Arrays of mailboxes; at least one recipient between them. Bcc goes to the envelope and nowhere else. reply_to Optional. subject Required; it may be empty. text, html At least one. Both makes a multipart/alternative. headers Optional: X- fields other than the server's own X-hMailServer- fields, List-Unsubscribe, List-Unsubscribe-Post, In-Reply-To and References. Any other field is 400. attachments Optional: [{filename, content_type, data}], data in base64, at most 20 and twelve megabytes together. A line break is refused with 400 in every value that becomes a header, and a member the route does not take (body for text, for example) is 400 and named, not dropped. What the server does to the message. It is written and checked by the same code as the webmail's send. Every recipient goes through what RCPT TO asks of an authenticated sender, with the server's recipient ceiling (MaxSMTPRecipientsInBatch) and the suppression list. The domain's signature and disclaimer are added, the rules that block are asked (403, policy_rejected), and the message is held to the maximum message size (413). It is DKIM-signed at delivery by the domain's key. The envelope sender is the From address, so a bounce comes back to it. A Received field names the server, the send API and the key's label, never the key's id. No Sent copy is kept anywhere. When the sender is an account's address, that account's sending quota is consumed as an SMTP submission would consume it. Ceiling Answer send_per_minute 429, Retry-After: 60 send_per_day 429, Retry-After: 3600 The sender account's sending quota 429, Retry-After one twenty-fourth of the account's period The REST request budget, 200 requests in 10 seconds 429, Retry-After: 10 The recipient ceiling, the size 400, 413 A message is counted only once every other check has passed, and when a later ceiling refuses, every earlier one is given back. A refused message is never a counted one. The answer to a message that is taken is 202: {"queue_id": 4711, "message_id": "<…@mail.example.com>", "recipients": 2, "recorded": true} queue_id is the one to ask about afterwards with GET /api/v1/send/4711. The answer names only the addresses the body named, each with a state. State Meaning queued In the queue and not yet tried deferred An attempt failed and will be retried delivered The trace's delivered event bounced Refused for good, or the retries ran out refused The suppression list stopped it at delivery unknown Nothing names the address; the answer says why It needs the message trace, and the trace is off by default (MessageTraceEnabled), because it records who corresponds with whom. With it off, a send reads queued or deferred while it is in the queue and unknown once the queue has let it go; trace in the answer is false and a trace_note says so. Turn the trace on, and choose its retention, before relying on the answer. The record of a send is kept for MessageTraceRetentionDays, thirty by default. Only a send this key made is answered; any other id is the same 404. The backup does not carry the records. Mail in to a webhook: a route SendGrid and Mailgun call this inbound parse. A route is made over REST or on the Control Deck's Inbound webhooks view. There is no COM member and no Control Panel page, on either platform. POST /api/v1/inbound-webhooks { "address": "[email protected]", "url": "https://helpdesk.example/hooks/mail", "secret": "a-long-random-string" } Field What it is Default address [email protected], or example.com for every address of the domain. The domain must be one this server hosts. required url https:// to the endpoint; plain http:// only to this machine. Printable ASCII only. required secret Write-only. Every request is signed under it and it is never returned. required deliver false: the route alone receives the mail. true: what the address reaches without the route receives it as well. false on_failure keep or bounce keep format form (multipart/form-data in SendGrid's field names) or json form attachments include, names or none include include_raw Also send the message as it arrived false max_size_kb A larger message is not posted. At most 81920. 10240 enabled A route switched off is as though it were not there for new mail true A route decides that the address exists. RCPT TO is accepted for a routed address whether or not an account, an alias or a list has it, and a route is asked before any of them. With deliver off, the route takes the mail instead of whatever has the address, so a list whose address a route takes never sees the message. Route a list's domain with care. An address route wins over its domain's, and a recipient with a plus tag ([email protected]) is found by the address as written, then by the address before the tag, then by the domain, with the tag kept in what is posted. What arrives is a signed POST: POST /hooks/mail HTTP/1.0 Content-Type: multipart/form-data; boundary=hMailServer-4193-... X-hMailServer-Timestamp: 1789000042 X-hMailServer-Signature: sha256=<hex> X-hMailServer-Event: message.inbound X-hMailServer-Delivery: 4193 X-hMailServer-Attempt: 1 X-hMailServer-Delivery is the same on every attempt. The form format carries SendGrid's fields (headers, from, to, cc, subject, text, html, envelope, sender_ip, SPF, dkim, charsets, attachments, attachment-info, attachment1 onward) with a few of this server's own beside them (spam, dmarc, route, queue_id, attachments_left_out). Mailgun's field names are not sent. The json format is this server's own shape. The SPF, dkim and dmarc results are the ones this server wrote into its own Authentication-Results field, and only while AuthenticationResultsEnabled is on. Retries and what is kept. The message is taken after the global rules, the virus scan and the sandbox have had it, and nothing is posted from the delivery thread. Any 2xx delivers. Anything else, a redirect included, is a failure. The retries are the outbound webhooks': WebhookMaxAttempts attempts (5 by default), with waits of 0, 1, 5, 15 and then 60 minutes. When they run out: keep: the message is kept and never swept. It can be read with GET /api/v1/inbound-webhooks/deliveries/{id}/message, tried again with POST /api/v1/inbound-webhooks/deliveries/{id}/retry or removed with DELETE /api/v1/inbound-webhooks/deliveries/{id}. The error log says so once, as HM7401. bounce: the message is returned to its sender with a delivery-status report (4.4.7), without repeating what the endpoint answered. HM7402 records the return. Where the sender cannot be told, the message is kept instead. A message larger than the route's ceiling is not posted at all: it is returned at once (5.3.4) or kept at once (HM7403). Delivered and returned rows are removed after WebhookRetentionDays (7 by default), and the rows survive a restart. Where an endpoint may be depends on who made the route. The server's administrator may name an address only this server can reach, but never a link-local or cloud metadata address (169.254.0.0/16, fe80::/10 and the clouds' own metadata addresses). Every other credential, an API key of any scope or role included, may name a public address only. Both are judged when the route is saved and again at every connection. No redirect is followed and HttpProxy is never used. Who may. Only serveradmin holds manage_inbound_webhooks as shipped, since routing a domain's mail to a URL is as much as reading all of it. A reader without it is answered a route's URL as its scheme and host alone, so put a token in the secret, not in the URL. Reading a kept message needs view_mailbox as well. POST /api/v1/inbound-webhooks/run makes one pass now and needs a credential that reaches every domain. Outbound webhooks An outbound webhook does not carry the message. No body, no attachments, no header block. It is a notification. The one piece of content it can carry is the subject, and only where the domain's webhook_subjects and the subscription's include_subject are both on; both are off by default, and even then a subject travels on message.received and message.quarantined only. Event Raised when message.received The message has been saved and the server has taken responsibility for it, once per local recipient message.delivered A recipient, local or remote, has been delivered to message.bounced A non-delivery report has been written and queued for the sender, once per failed recipient message.quarantined The held copy and its row exist account.locked Repeated authentication failures have locked a name A delivery attempt that failed and will be retried is not a bounce, and no event says deferred. POST /api/v1/webhooks { "name": "ticketing", "url": "https://tickets.example/hooks/mail", "secret": "a-long-random-string", "events": "message.bounced,account.locked", "domains": "example.com", "max_per_minute": 120 } An empty events, domains or addresses list means every one. A subscription confined to a send API key's sender ("addresses": "[email protected]") hears message.delivered and message.bounced for the recipients outside this server, with the message trace on or off. Plain http off this machine is refused when the subscription is made. The secret is write-only, protected at rest, and an empty secret in a change keeps the one that is set. Permissions. Reading needs view_configuration; deleting needs manage_settings; creating, changing, testing and running a pass need manage_server, because whoever can do those can make the server connect to a host of their choosing. A role confined to named domains is refused all eight routes. The queue. The delivery pass runs once a minute, attempts at most 200 rows and runs for at most 60 seconds. A delivery succeeds on any 2xx; a request that has taken more than ten seconds is a failure. After WebhookMaxAttempts the row is dead-lettered and the error log says so once as HM6971. Past a subscription's ceiling per minute, events are dropped and counted as dropped, not queued. Setting REST key Default WebhooksEnabled webhooks_enabled on WebhookMaxAttempts webhook_max_attempts 5 WebhookMaxPerMinute webhook_max_per_minute 60 WebhookRetentionDays webhook_retention_days 7 WebhooksEnabled silences the outbound webhooks and not the inbound routes. From a shell: hmctl webhook create --set name=ticketing --set url=https://tickets.example/hook \ --set-string secret="$SECRET" --set events=message.bounced --set domains=example.com hmctl webhooks test --event message.bounced --domain example.com hmctl webhooks run hmctl webhooks deliveries --state dead-lettered webhooks test raises a real event through the path a real one takes and says how many subscriptions took it, so 0 is an answer. In PowerShell the functions are Get-HmWebhook, New-HmWebhook, Set-HmWebhook, Remove-HmWebhook, Get-HmWebhookDelivery, Invoke-HmWebhookDelivery and Test-HmWebhook. Over COM, on Windows, Utilities.GetWebhookSubscriptions(name) and Utilities.SaveWebhookSubscription(json) take and answer the same JSON documents. Verifying a signature One recipe signs all of them: the outbound webhooks, the inbound routes and the webhook of an alert rule. The signature is HMAC-SHA256 over the timestamp, a full stop, and the raw request body, under the shared secret, as lower-case hex, in X-hMailServer-Signature. Verify it against the bytes received, before parsing them. import hmac, hashlib, time def verify(secret, headers, raw_body): timestamp = headers["X-hMailServer-Timestamp"] signature = headers["X-hMailServer-Signature"] # Refuse anything older than five minutes, or the signature can be replayed. if abs(time.time() - int(timestamp)) > 300: return False expected = hmac.new(secret.encode(), (timestamp + ".").encode() + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest("sha256=" + expected, signature) Compare in constant time. Check the timestamp: a signature with no freshness check can be replayed. De-duplicate on X-hMailServer-Delivery together with X-hMailServer-Event. A retry is the same body with a fresh timestamp and so a fresh signature, and an inbound and an outbound delivery can carry the same number. To rotate a secret, PUT the new one and accept either signature at the receiver while the change is made. There is no overlap window in the server. The documents also give the recipe in JavaScript. An alert's webhook carries three of the headers: X-hMailServer-Timestamp, X-hMailServer-Signature and X-hMailServer-Event. What they do not do The send API has no templates, no substitutions, no scheduled send and no per-recipient ceiling of the key's own, and gives no state without the trace once a message has left the queue. An inbound route makes one post per route per message, not one per recipient. It has no setting of its own and no per-route retry ladder. An outbound webhook does not carry the message and has no per-subscription retry policy. Neither kind of webhook is in the configuration backup. Export the routes with GET /api/v1/inbound-webhooks and the subscriptions with hmctl webhook list; the secrets are in no read. One pass at a time is per process, not per database, for both kinds of webhook. Where to read more The operator documents: SendApi.md, InboundWebhooks.md, Webhooks.md and, for the alerts' webhook, AlertsAndAuditTrail.md. The wiki: Mail In to a Webhook and The REST API. The public documentation: chapter 24, The REST API. On this forum: The Control Deck, the 6.3.4 announcement, the REST API guide and hmctl and the PowerShell module; turning the message trace on is in Troubleshooting by symptom, and the alert rules whose webhook is signed the same way are in Alerts, the audit trail, syslog and the journal.
  • 0 Votes
    1 Posts
    115 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.6 (6 October 2026) carries the PowerShell module, HmailServer.psd1 and HmailServer.psm1, Authenticode-signed, as files of the release, and Linux packages for x86-64 and AArch64; hmctl as a PyPI package and the module on the PowerShell Gallery are built but not yet published. See the 6.3.6 announcement. hMailServer 6.3.7 (17 October 2026) carries the module the same way, and since 18 October 2026 the repository and its releases are also mirrored on GitHub, issues and merge requests staying on GitLab. 6.3.7's REST API refuses a state-changing request that carries a credential when a browser reports it as cross-site, which hmctl and the module, sending no such header, never meet, and it refuses a key restricted to named domains a domain's rename, a domain alias and a custom role's creation, change or delete. See the 6.3.7 announcement. Since 21 October 2026 hmctl is on PyPI as hmailserver-cli and the module on the PowerShell Gallery as HmailServer, each at 6.3.7. hMailServer 6.3.8 (2 November 2026) carries the module as files of the release, as before, and both are sent 6.3.8 by a job started by hand once the release is published. 6.3.8's server answers 400 to a body that is not one JSON object or names a member twice, so a body given to hmctl api or --from-json must be one JSON object that names each member once, and 6.3.8's hmctl refuses a body holding NaN or Infinity before sending it. See the 6.3.8 announcement. hMailServer 6.3.4 has two command-line clients: hmctl for any shell, and the HmailServer module for PowerShell. They share one vocabulary and one API, so both administer a Windows server, a Linux server or a server on another machine, and neither needs COM or the Control Panel. This guide covers where each comes from, the credentials they take, the verbs, the import of accounts from a CSV file with its dry run, the configuration as one document, and what neither client does. The one thing to know first: both speak the REST API, so the REST listener has to be on before either can do anything, and neither client can turn it on. Neither is a replacement for the administration programs. The Windows Control Panel is the place to configure a server on Windows, and the browser Control Deck is that place on Linux. A command line is for what a command line is better at: making fifty accounts from a spreadsheet, reading one setting in a script, watching the delivery queue, or doing the same thing to twenty servers. Where the clients come from File What it is hmailserver/source/Tools/Cli/hmctl One Python 3 file with no dependencies beyond the standard library. hmailserver/source/Tools/Cli/HmailServer.psm1 The PowerShell module: Get-HmDomain, New-HmAccount, Set-HmSetting and the rest, answering objects rather than text. build/check-cli.py Runs both against a recorded server in CI and asserts what each one sent, so the two cannot drift apart. On Linux, the packages install hmctl as /usr/bin/hmctl. The 6.3.4 packages are x86-64 only, and the changelog records Python 3 as one of the .deb's two Recommends, the other being 7-Zip. On Windows, the wiki says the installer carries neither client: take them from the repository directory hmailserver/source/Tools/Cli/. Both clients are in the repository since 6.3.4. build/hmconfig.ps1 is a different tool and stays. It exports the whole configuration as one JSON document and applies it to another machine over COM, on Windows. The two clients here are per-object verbs over HTTP. Before anything else: the listener On Linux, store the port on the server itself and restart the service. Since 6.3.4 the database is the settings store, so editing the [Settings] section of hMailServer.ini does not turn the listener on once the server has stored the key. sudo hmailserver --set-setting RestApiPort=8080 sudo systemctl restart hmailserver On Windows, the Control Panel does this over COM. On either platform, TLS is required unless the listener is bound to the loopback, so give it a certificate the client trusts if it is to be reached from another machine. The REST API guide has the listener's settings, the reinitialise they wait for and the three ways it refuses to start, and the hmailserver command line on Linux has the checks --set-setting makes and how it takes a secret. Credentials Either the administrator password, sent as HTTP Basic with the user name Administrator, or an API key, sent as a bearer token. An API key is the better answer for anything unattended: it can be read-only, it can be restricted to one domain, and it can be revoked without changing the administrator password. export HMAIL_URL=https://mail.example.com:8045 export HMAIL_API_KEY="$(cat /etc/hmailserver/api.key)" hmctl domain list Connect-HmServer -Url https://mail.example.com:8045 -ApiKey (Get-Content .\api.key) Get-HmDomain hmctl reads HMAIL_URL, HMAIL_API_KEY, HMAIL_PASSWORD and HMAIL_PASSWORD_FILE, and takes --url, --api-key, --password and --password-file before the verb or after it. With no credential at all it asks for one, but only when there is a console to answer. In a scheduled task or a pipeline it says what is missing and stops. --insecure is refused for anything but the loopback. A self-signed certificate on localhost is the ordinary case and nothing is on the wire. An administrator password sent to a remote host over a connection nobody verified is what the refusal prevents, and the remedy is a certificate the client trusts. Some commands take the administrator password and never an API key, because no key reaches the routes behind them: hmctl apikey, hmctl ini, hmctl config changed, hmctl webhook and hmctl webhooks. The command synopsis names no option for a one-time code. Since 6.3.4 a one-time code is taken once, and the changelog's advice to a script that sent the administrator's password and a code with every request is a session or an API key; the REST API guide has the detail. hmctl hmctl [--url URL] [--api-key KEY | --password PASSWORD | --password-file FILE] [--insecure] [--json] <resource> <verb> [arguments] The resources, grouped. CommandLine.md has a row for each. Area Resources Notes Domains and what is in them domain, account, alias, list, group, fetchaccount, app-password, device, mailbox An account is listed and created under --domain, and read and written at its address, which is how the API is shaped. mailbox renames or moves one. Server-wide objects rule, route, port, iprange, certificate, journalrule list, create, update, delete Mail in motion queue, quarantine queue has list, retry, delete; quarantine has list, release, delete Settings settings, ini, config A settings group whole or one setting; the keys that were the [Settings] section; the configuration as one document Deliverability dkim, dmarc, suppression, bounce, bounces, complaint, complaints, spamtrap, traphits, reputation, openpgp reputation exits 1 when an address is listed and when a list could not be asked Integrations apikey, webhook, webhooks All three with the administrator password only, never an API key Operations log, backup, report, sample-domain, match match says what a rule criterion would decide, without a rule Bulk accounts import and export, a CSV file, with a dry run Anything else api A method and a path: the escape hatch, for a route newer than the client A few whole commands: hmctl domain create --set name=example.com --set active=true hmctl account create --domain example.com --set [email protected] --set password=... hmctl account list --domain example.com hmctl settings get antispam use_spf hmctl settings set antispam --set use_spf=true --set spam_mark_threshold=5 hmctl device [email protected] list hmctl queue list --json | jq '.[] | select(.retry_count > 3)' hmctl match contains viagra "cheap viagra here" hmctl report traffic --from 2026-09-01 --to 2026-09-15 --domain example.com hmctl api POST /api/v1/server/reinitialize How values are read. --set name=value reads true, false, null, numbers and anything starting with { or [ as JSON, and everything else as a string. --set-string keeps a value that looks like one of those a string. --from-json FILE takes a whole object, and - reads it from standard input. Output and exit codes. Output is a table by default and the server's own JSON with --json. Exit code Meaning 0 Success 1 An error 2 A usage mistake 3 The server refused the credential 4 What was named does not exist A script can therefore tell "no such account" from "wrong password" without reading the text. The PowerShell module Import-Module .\hmailserver\source\Tools\Cli\HmailServer.psm1 Connect-HmServer -Url https://localhost:8045 -Insecure Get-HmDomain | Where-Object active -eq $false Get-HmAccount -Domain example.com | Sort-Object max_size_mb -Descending | Select-Object -First 10 New-HmAccount -Domain example.com -Address [email protected] -Password (Read-Host -AsSecureString) Set-HmAccount -Address [email protected] -Property @{ max_size_mb = 500 } Set-HmSetting -Group antispam -Property @{ use_spf = $true } Get-HmQueue | Where-Object retry_count -gt 3 | Start-HmQueueDelivery Get-HmChangedSetting | Format-Table store, name, value, default Every function answers objects, so Where-Object, Sort-Object and Export-Csv work on them, and nothing in the module formats anything. Everything that changes the server supports -WhatIf and -Confirm, and deleting a domain or an account asks before it acts unless told not to. A domain name or an address completes from the server once Connect-HmServer has run. Invoke-HmApi -Method GET -Path /api/v1/... is the escape hatch, and is what every other function is built on. Accounts from a CSV file hmctl accounts import new-people.csv --dry-run hmctl accounts import new-people.csv hmctl accounts export example.com accounts.csv Import-HmAccount -Path .\new-people.csv -WhatIf Import-HmAccount -Path .\new-people.csv Export-HmAccount -Domain example.com -Path .\accounts.csv The file is UTF-8 (in Excel, CSV UTF-8) and its header row names the columns. These are the columns, and there are no others. Column Required Notes address yes Created under the address's own domain. password yes Sent as text, so digits alone are still a password. Never trimmed: white space at either end is refused. Judged by the server's password policy and, when it is switched on, the breach check. first_name At most 60 characters, counted as UTF-16 code units. last_name As first_name. active true, yes, on or 1; false, no, off or 0. Default true. max_size_mb A whole number of megabytes; 0 is no limit, and is the default. admin_level user, domain or server. Default user. address,password,first_name,last_name,active,max_size_mb,admin_level [email protected],S0me-Long-Passphrase,Ann,Adams,true,500,user [email protected],An0ther-Long-Passphrase,Bob,"Brown, Jr",true,,domain The columns may come in any order and their names in any case. Every cell but the password is trimmed, and an empty cell is not sent, so the server's default applies. A value with a comma in it goes in quotes. A row with more cells than the header has columns is refused rather than shifted along. What is refused before anything is sent. Each row becomes one POST /api/v1/domains/<domain>/accounts, and that route refuses a field it does not know by name. So a column that is not in the table is refused before anything is sent, a name column included. The clients also refuse, in the words the server would use: a row with no password; an active that is not one of the eight words; a max_size_mb that is not a whole number; an admin_level that is not one of the three; a name past its 60 characters; a password with a space or a tab at its start or end, which is the one row the clients refuse and the server would take. What a dry run cannot foresee. The password policy, the domain's account limit and an address the server does not accept are judged by the server alone. They are refused per row when the import runs, and listed with the address and the server's reason; hmctl adds the line number. An address that already exists is left alone and counted, never overwritten. An import that reset a password without saying so would lock people out with one command. --dry-run makes no request that changes anything and prints what would happen, line by line. The exit code is 1 when any row was refused. Who may make an administrator. admin_level is decided by who is asking. The administrator password, or a key issued for every domain, may make a server administrator. A key restricted to named domains may make users and domain administrators, and a row asking it for server is refused with a 403. The export writes the same columns less password, because the server does not have the passwords to give. Add a password column and change the addresses, and the exported file imports into another domain or another server as the same accounts: names, sizes, levels and whether each is active. The configuration as one document hmctl config changed # every setting that is not the shipped default hmctl config export server.json # the whole configuration, as a document hmctl config diff server.json # what would change; exit 1 if anything would hmctl config apply server.json # the plan, and nothing else hmctl config apply server.json --force # the plan, applied The document holds the settings groups, the hMailServer.ini keys the server exposes, the domains with their accounts, aliases and distribution lists, the groups, the rules, the routes, the listeners and the IP ranges. It is sorted throughout and carries nothing the server allocated, so two exports of an unchanged server are the same bytes. apply is a plan unless --force is added, and a deletion needs --allow-delete on top of that. Without it, an entry the server has and the document does not is left alone and counted. Not carried Why Passwords The server does not give them out. An account in the document that does not exist on the server is refused rather than created with a password nobody chose; add a password field for the accounts to be created. The secrets among the ini keys Listed as (set) or empty, and left out of the document. hmctl ini get <name> reads one by name. Certificates and the directories They are files and paths on one machine's disk. The journaling rules An address chosen for one server is almost never the right one for another. hmctl journalrule edits them. What lives under an account Application passwords, fetch accounts and folder permissions are the account holder's. The selector and key files of a domain whose DKIM key rotation is on The rotation moves them on its own schedule. Settings the environment manages In a container the value is the deployment's. The server refuses a change to such a setting with 409. Remote domain policies and alert rules The wiki's own addition: they have routes of their own that config does not read. What this server has changed from stock. hmctl config changed, or Get-HmChangedSetting in PowerShell, lists every setting whose value is not the one this version ships with, with the shipped value beside it. Settings: what is not stock has the route behind it, the other places to read the same list, what "default" means and who may see it. $ hmctl config changed STORE NAME VALUE DEFAULT ini LogLevel 5 9 ini SMTPDMaxTimeout 600 1800 ini OAuth2HmacSecret (set) ini DnsServer 10.0.0.1 (forced from [SettingsOverride]) database smtpnoofretries 6 4 5 of 391 settings are not what this version ships with. A secret is named and never shown. A value that the file's [SettingsOverride] section is forcing says (forced from [SettingsOverride]), a value the environment sets says (set by the environment), and a name the server holds a value for and ships no default for says (none: this server does not read it). A difference is not an exit code here, unlike config diff, because every real server has changed something. Reports and country blocking from a shell hmctl report sections hmctl report summary --from 2026-09-01 --to 2026-09-15 hmctl report senders --domain example.com --top 25 hmctl report mailboxes --csv mailboxes.csv hmctl report traffic --csv - --csv fetches the server's own CSV with format=csv instead of building one from the JSON. Per-domain reports has the sections, their sources and parameters, and who may read them. Run hmctl report sections first. It says which sources are recording and what this server cannot answer. The four sections counted from the message trace are empty when the trace is switched off, which is the default, and that is not the same statement as "nothing happened". Without --csv, the section's own note is printed above the table. CountryBlocking.md shows the country policy, which the topic on allow and block lists and country blocking explains, set from both clients: hmctl settings get settings geoip_database_status hmctl settings set --set geoip_database_file=/var/lib/GeoIP/GeoLite2-Country.mmdb hmctl settings set --set country_policy_smtp=block:CN,RU hmctl domain update example.com --set active=true --set country_policy=allow:GB,IE Set-HmSetting -Property @{ geoip_database_file = '/var/lib/GeoIP/GeoLite2-Country.mmdb' country_policy_smtp = 'block:CN,RU' } (Get-HmSetting).geoip_database_status Set-HmDomain -Name example.com -Property @{ active = $true; country_policy = 'allow:GB,IE' } geoip_database_status is read-only. A settings change is all or nothing, so a policy the grammar refuses leaves every other key in the same request unapplied. What neither client does Neither reads or writes the message store. Mail is IMAP's business. Neither is a backup tool of its own. hmctl backup start, hmctl backup restore and hmctl backup passphrase ask the server; the archive is written, read and restored by the server. Neither sets the administrator password. hmailserver --set-admin-password on the server does that, on Windows hMailServer.exe --set-admin-password, with the password in HM_ADMIN_PASSWORD or on standard input and never as an argument. On Windows the Control Panel does it too, but only where the service may write hMailServer.ini, as under LocalSystem: under NT SERVICE\hMailServer, a new 6.3.4 installation's account, its Administrator password card is refused with a sentence. Neither makes or carries the stored-secrets key. That is done on the server itself, never over the API. Neither can turn the REST listener on, since both need it. Neither sets what the environment sets in a container. No container image of 6.3.4 is published. Neither invents a field. A field that was not given is one the server decides. hmctl has no pending verb. SettingsApply.md notes that GET /api/v1/server/settings/pending is one request; hmctl api takes a method and a path. Where to read more The operator document: CommandLine.md, with a row for every resource and sections on DKIM rotation, bounces, complaints, DMARC reports, OpenPGP keys, the mailbox move and setting profiles. The wiki page: Command Line. For the examples above: ChangedFromStock.md, Reports.md and CountryBlocking.md. The public documentation: chapter 24, The REST API, which both clients speak, and 35.10, Administering a Linux server. On this forum: the Linux guide, The Control Deck, the 6.3.4 announcement, the REST API guide and, for what hmctl webhook manages, the send API and webhooks.
  • 0 Votes
    1 Posts
    139 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) changes how the listener judges some requests. A state-changing request that carries a credential, HTTP Basic included, is refused when the browser reports it as cross-site (Sec-Fetch-Site, or an Origin that is not this server's); scripts (the curl examples below among them), hmctl and the PowerShell module send no such header and are unaffected. A credential confined to named domains (a domain administrator, a key issued for named domains, or serveradmin given with a list of domains) is refused a domain's rename, a domain alias, a custom role's creation, change or delete, the bind_to_address, send_using_route and script_function actions on a domain's or an account's rule, a relay host changed to an address that is not public, and GET /api/v1/metrics/history; and a role is given or taken away only by a credential that holds manage_roles in the account's domain and takes in what the account holds now and afterwards, so such a credential cannot take a role away from an account whose admin level is the server administrator's. The OpenAPI document describes each path once, with every method it answers. In the Control Panel, API & monitoring is Monitoring & alerts and REST API keys is under Sign-in & access; Ctrl+K takes every old title to its page. See the 6.3.7 announcement. hMailServer 6.3.8 (2 November 2026) reads a JSON body in time and memory that follow its bytes (work item 110), and answers 400, saying why, before a route reads a member, to a body that is not one JSON object, names a member twice, holds an escaped U+0000 or passes 100,000 values or 32 levels, and to a member whose absence is the wider setting written as another type than its route reads, such as domains, allowed_from, role or expires on POST /api/v1/apikeys as anything but a string or null: correct a client of your own that sends such a body. An account's failed sign-in from loopback, as through a reverse proxy on the same machine, is counted against the name alone, AccountLockoutThreshold (off by default) being the limit, and never towards a ban of the address (work item 107); the listener's own accounting spares all of 127.0.0.0/8. See the 6.3.8 announcement. The REST API is the alternative to COM for automating hMailServer, and the only administration interface that works from a machine that is not running Windows. This guide covers how to turn the listener on, the three credentials it takes and the roles that narrow them, how one-time codes behave since 6.3.4, the OpenAPI document the server emits, and a set of curl examples that use only routes the project's own documents name. The one thing to know first: the listener is off as shipped (RestApiPort=0), and since 6.3.4 RestApiPort is a setting stored in the database. Editing the [Settings] section of hMailServer.ini does not turn the API on once the server has stored the key. Turning the listener on Four settings shape the listener. Setting Shipped value What it is RestApiPort 0 The port. 0 means there is no listener. RestApiBindAddress 127.0.0.1 The address it binds. TLS is required unless this is 127.0.0.1, localhost or ::1. RestApiCertificateFile empty The certificate, in PEM. Left empty, the listener falls back to the ACME certificate. RestApiPrivateKeyFile empty Its private key, in PEM. Since 6.3.4 the database is the settings store (schema 6042) and the [Settings] section of hMailServer.ini is its copy. A key the server has never stored is taken in from the file at the next start, so on a fresh installation an edit made before the first start still works; once the key is stored, the database's value is the one used. Settings: what is not stock, when a save takes effect covers the store itself, why an edited line is put back (HM5804) and [SettingsOverride]. Where the stored value is changed depends on the platform. Platform How RestApiPort is set Windows The Control Panel page Monitoring & troubleshooting → API & monitoring, or Settings.SetIniSetting over COM. The Control Panel and the COM API are Windows-only. Linux hmailserver --set-setting on the server itself, then a restart of the service. Either, once the API answers PUT /api/v1/settings/ini/{name} with the administrator password. sudo hmailserver --set-setting RestApiPort=8443 sudo systemctl restart hmailserver --set-setting checks what it is given: a port must be a number from 0 to 65535 and RestApiBindAddress an IP address or localhost. On Linux this matters most, because there the REST API is the administration interface. The packaged /etc/hmailserver/hMailServer.ini ships RestApiPort=0 and RestApiBindAddress=127.0.0.1 under [Settings], so the first start stores both, and changing those lines afterwards turns nothing on. The one part of the file that still decides a stored key is a [SettingsOverride] section, applied over the store at every start and reported as HM5807 each time; the wiki gives it as the route where neither the Control Panel nor COM is available. The new value waits for a reinitialise. Since 6.3.4 most settings apply when they are saved, but RestApiPort, RestApiBindAddress, RestApiCertificateFile and RestApiPrivateKeyFile are among the 28 keys marked reinitialize, because the listener binds and its TLS context is built when the servers start. They take effect at Apply now in the Control Panel (Windows) or the Control Deck, at Application.Reinitialize over COM (Windows), at POST /api/v1/server/reinitialize, or at a restart of the service. The listener can refuse to start, and each refusal is written once to the application log. What is wrong Log line Fix The administrator password is empty RestApi: Refusing to start - the administrator password is not set. Set an administrator password The bind address is not loopback and there is no certificate RestApi: Refusing to start - TLS certificate is required unless bound to 127.0.0.1 or ::1. Set RestApiCertificateFile and RestApiPrivateKeyFile. Set both files, enable ACME, or bind loopback The certificate cannot be loaded, or its key does not match RestApi: Refusing to start - the shared TLS configuration could not be applied to the configured certificate., preceded by HM5113 naming the certificate or key file Correct the PEM paths and permissions Nothing RestApi: Listening on 127.0.0.1:8443 (http, loopback only). or ... (https). None needed The Listening on line is the only proof that the listener is up. The loopback exemption is a string comparison against 127.0.0.1, localhost and ::1; 127.0.0.2 and ::ffff:127.0.0.1 do not qualify. The TLS configuration is the one the mail protocols use (cipher list, protocol switches, TlsKeyExchangeGroups), with a TLS 1.2 minimum applied on top, so opening an older protocol for a mail client does not open it for the management API. The three credentials Credential Header Reaches Administrator password Authorization: Basic with the user name Administrator (case-insensitive) Every administrative route. It is never read-only and never confined to domains. API key Authorization: Bearer hmapi_<64 hex> What its scope, role and domains allow, less the routes listed below. An account's own Authorization: Basic with the address and the mailbox password or an app password, or the hmailsession cookie /api/v1/me and /api/v1/session; since 6.3.4 also the administrative routes a role it holds allows. The two Basic forms are told apart before either password is tried: any user name but administrator is treated as a mailbox address and goes down the path an IMAP logon takes, with the same lockout and auto-ban. An account is never tested against the administrator password. API keys are the credential for scripts. A key is read-only unless it is made with "scope":"full", expires after 90 days unless the request names an expires (there is no "never expires"), and may be narrowed to named domains and to a source address, range or CIDR. Only the SHA-256 of a key is stored, in hMailServerApiKeys.ini beside hMailServer.ini, and that file is read again at every authentication attempt, so removing a key's section revokes it on the next request. # Administrator password only: a key can never make or revoke keys curl -u Administrator:your-admin-password \ -H "Content-Type: application/json" \ -d '{"label":"backup monitor","scope":"readonly","expires":"2027-01-01 00:00:00"}' \ http://127.0.0.1:8443/api/v1/apikeys {"id":"3f9a1c4b5d6e7f80","label":"backup monitor","scope":"readonly","domains":"", "expires":"2027-01-01 00:00:00","allowed_from":"","key":"hmapi_7c1f...c92a"} key is the clear text. It is returned once and cannot be recovered, so store it then. GET /api/v1/apikeys lists the keys with an expired flag and never a hash; DELETE /api/v1/apikeys/<id> revokes one. The create request takes label (required, at most 64 characters), scope, domains, expires and allowed_from, and since 6.3.4 role and the send API's send_from, send_per_minute and send_per_day. What no API key reaches. Whatever its scope or role, a key is answered 401 (not 403, so that it learns nothing) on the key store /api/v1/apikeys and on /api/v1/settings/ini, and since 6.3.4 also on /api/v1/audit, /api/v1/alerts, /api/v1/webhooks, the compromised-account review, /api/v1/settings/changed, /api/v1/server/settings/pending, an account's export, the address erasure and the second factor's enrolment. In the other direction, since 6.3.4 a key is the only credential POST /api/v1/send takes (a key naming a sender) and the only one /scim/v2 takes (a key whose role holds provision). Roles, since 6.3.4 A role is a named set of permissions, and an assignment gives an account a role in named domains (schema 6047). Nothing changes on upgrade: an account with no assignment holds what its admin level has always given it, and a key without a role is what it always was: Scope=full is serveradmin and Scope=readonly is auditor. A key is given a role with role on POST /api/v1/apikeys; an account is given one at /api/v1/role-assignments by a credential holding manage_roles. What a refusal says. A route a role may not call answers 403 naming the role and the permission, for example the role 'helpdesk' does not hold the permission 'manage_settings'. An address in a domain the role does not reach answers the role 'helpdesk' does not reach that domain whether or not the account exists. A request authenticated with an app password never carries the account's role. Administrator roles end to end has the built-in roles and their permissions, giving a role on each surface, custom roles, and the two stores, /api/v1/apikeys and /api/v1/settings/ini, that answer to the administrator password alone, not even to an account holding serveradmin. One-time codes When a second factor is enrolled on the administrator credential, the code travels in an extra header: curl -u Administrator:your-admin-password \ -H "X-hMailServer-OTP: 123456" \ http://127.0.0.1:8443/api/v1/status Without it the answer is the one 401 on this listener that says what is missing: HTTP/1.1 401 Unauthorized WWW-Authenticate: Basic realm="hMailServer" X-hMailServer-OTP: required Cache-Control: no-store {"error":"authentication failed","second_factor":"required"} A missing code is not counted towards the auto-ban; a wrong code is. Since 6.3.4 a one-time code is taken once, over COM and over REST, for the administrator and for an account alike. A code the server has accepted is refused if it comes again. The consequence for automation is direct: a script that sent the administrator's password and a code with every request now has only its first request in each half minute accepted. The others are refused with 401 and are not counted as wrong codes, so such a script does not lock the administrator out. The changelog names the two remedies: sign in once with POST /api/v1/session and use the hmailsession cookie from there, or use an API key. # Sign in once; the password and the code travel on this request alone curl -c cookies.txt -u Administrator:your-admin-password \ -H "X-hMailServer-OTP: 123456" \ -X POST http://127.0.0.1:8443/api/v1/session # Reads carry the cookie curl -b cookies.txt http://127.0.0.1:8443/api/v1/status # Anything that changes something also carries the CSRF header curl -b cookies.txt -X POST -H "X-Requested-With: hMailServer" \ http://127.0.0.1:8443/api/v1/queue/81234/retry # Sign out curl -b cookies.txt -X DELETE -H "X-Requested-With: hMailServer" \ http://127.0.0.1:8443/api/v1/session The session rules are enforced by the server: 30 minutes idle and 12 hours from sign-in until a session policy sets others (since 6.3.4, session_idle_seconds and session_lifetime_seconds), at most 1000 sessions, and every session dropped when the listener stops. A cookie-authenticated request that is not GET or HEAD without X-Requested-With: hMailServer is refused with 403. Somebody who is asked for a code twice within half a minute, to sign in to the Control Deck and then to register a passkey for example, waits for the authenticator's next code. Two further changes in 6.3.4 belong here. A wrong code is counted against a budget of five in fifteen minutes, after which no code is taken for fifteen minutes; on the administrator's Basic sign-in the answer is then 429 with code second_factor_locked and Retry-After. And for an account, one of its one-time recovery codes, in X-hMailServer-Recovery-Code, is taken wherever the authenticator's code is asked for. The administrator signs in at POST /api/v1/session with HTTP Basic, and with the code in X-hMailServer-OTP when a second factor is enrolled: AccountRecovery.md names the header for this route, the 6.3.4 changelog gives the session as the remedy above, and the Control Deck signs in that way. The wiki's credentials table, which lists /api/v1/session among the routes the administrator password does not reach, is out of date on this. How a request is judged The server tries Bearer, then Basic, then the session cookie. It then applies the CSRF check for cookie-authenticated writes (and, since 6.3.7, refuses any state-changing request that carries a credential when the browser reports it as cross-site, by Sec-Fetch-Site or an Origin that is not this server's) and the rate budget of 200 requests in a fixed 10-second window per credential, turns the method and path into a route kind, and decides on that kind in one place before any handler runs. Read-only is decided by route kind, not by HTTP method. A read-only key on a mutating route is answered 403 with this api key is read-only. A key with a domain list is refused server-wide resources outright: the queue, the quarantine, IP ranges, certificates, rules, logs, the backup and the settings among them. On a route that names a domain or an address outside its list the answer is 403 with this api key is not permitted for that domain. Routing is exact. Method and path must match after the query string is removed, a trailing slash is a different path, and an id in a queue, quarantine, IP-range, archive or /api/v1/me/... path must be 1 to 18 decimal digits and greater than zero, or the answer is 404. The OpenAPI document GET /api/v1/openapi.json answers the API's description to the administrator password or to any key, a read-only one included. The wiki's advice is that this document is the count to trust: in 6.3.4 it names 441 paths and 621 path-and-method operations, 413 and 585 of them under /api/, against 146 and 219 in 6.3.3. The rest are /scim/v2, the sign-in, reset and reading pages, the list manager's pages and the static catalogues and scripts. The Control Deck draws its settings groups from this document, and since 6.3.4 the document gives the ini-backed rows of the settings groups an x-applies mark saying when a save takes effect. The APIs Reference describes it as a static OpenAPI 3.0.3 document and records where its text is not accurate: the summary of /api/v1/status mentions uptime, and the handler returns no uptime field; the text for /api/v1/settings/ini still describes the file as the store, where since 6.3.4 the value goes to the database first. Settings saved and not yet running Since 6.3.4 a key marked reinitialize or restart whose stored value is not the value the server is running on is pending. The list is recomputed at every request. Route Answer GET /api/v1/server/settings/pending [{name, stored, running, applies, secret}]; a secret's two values are (set) or empty GET /api/v1/settings/ini The keys as {name, value, secret}, an applies map and a pending array of names GET, PUT, DELETE /api/v1/settings/ini/{name} One key, with applies and pending POST /api/v1/server/reinitialize 202 before it happens; {"reinitializing": true, "drained": true} when it ended a drain All but the last answer to the administrator password alone. A DELETE is now the only way to return a stored key to its default, because deleting a line from the file no longer does. A reinitialise restarts the REST listener with the other services, so it ends a Control Deck session; poll /api/v1/status until it answers again. Worked examples These use the loopback, plain-HTTP shape that RestApiPort=8443 and RestApiBindAddress=127.0.0.1 give; put the port that was stored in place of 8443. With a certificate configured, change http:// to https://. They present the administrator password on every request, which works while no second factor is enrolled; with one enrolled, use the session above or an API key. # Status: state is 0 Unknown, 1 Stopped, 2 Starting, 3 Running, 4 Stopping curl -u Administrator:pw http://127.0.0.1:8443/api/v1/status # Domains and accounts curl -u Administrator:pw http://127.0.0.1:8443/api/v1/domains curl -u Administrator:pw http://127.0.0.1:8443/api/v1/domains/example.com/accounts curl -u Administrator:pw -H "Content-Type: application/json" \ -d '{"address":"[email protected]","password":"S0me-Long-Passphrase"}' \ http://127.0.0.1:8443/api/v1/domains/example.com/accounts curl -u Administrator:pw -X DELETE \ http://127.0.0.1:8443/api/v1/accounts/[email protected] # The delivery queue curl -u Administrator:pw http://127.0.0.1:8443/api/v1/queue curl -u Administrator:pw -X POST http://127.0.0.1:8443/api/v1/queue/81234/retry curl -u Administrator:pw -X DELETE http://127.0.0.1:8443/api/v1/queue/81234 # Logs and the backup curl -u Administrator:pw "http://127.0.0.1:8443/api/v1/logs/hmailserver_2026-09-08.log?lines=50" curl -u Administrator:pw -X POST http://127.0.0.1:8443/api/v1/backup curl -u Administrator:pw http://127.0.0.1:8443/api/v1/backup # With a key curl -H "Authorization: Bearer hmapi_7c1f...c92a" http://127.0.0.1:8443/api/v1/status # Store another port: it waits for a reinitialise, and the pending list shows both values curl -u Administrator:pw -X PUT -H "Content-Type: application/json" \ -d '{"value":"8444"}' http://127.0.0.1:8443/api/v1/settings/ini/RestApiPort curl -u Administrator:pw http://127.0.0.1:8443/api/v1/server/settings/pending # Put the running port back; a value put back before anything applied it is no longer pending curl -u Administrator:pw -X PUT -H "Content-Type: application/json" \ -d '{"value":"8443"}' http://127.0.0.1:8443/api/v1/settings/ini/RestApiPort Creating an account answers {"address":"[email protected]","created":true}, or 400 for a missing field, 404 {"error":"domain not found"} or 409 {"error":"account already exists"}. In 6.3.4 the create route takes address and password and the other columns of the CSV import as well (first_name, last_name, active, max_size_mb and admin_level), and refuses a field it does not know by name (CommandLine.md). The older statement in the wiki and the APIs Reference, that only address and password are read, no longer holds. For a log, lines defaults to 200 and is capped at 2000. Limits and status codes Limit Value What the client sees Request size 64 KB, head and body 413, connection closed Large requests 16 MB and 300 s on POST /api/v1/me/messages and POST /api/v1/me/drafts; a few other routes are granted more than 64 KB as well, among them PUT /api/v1/me/settings and the SAML ACS (256 KB) 413 above that Request deadline 30 s, the TLS handshake included Connection closed with no response Connection lifetime 300 s, and 1000 requests Connection closed Concurrent connections 64 Connection closed Rate 200 requests per 10 s per credential 429 with Retry-After: 10 Auto-ban Shared with SMTP, IMAP and POP3; loopback exempt The address is refused at accept for 5 minutes A 401 is deliberately the same for no credential, a wrong password and an unknown, expired or out-of-network key; the application log says which. A request with Transfer-Encoding is answered 411, because the server does not de-chunk. An API response is application/json with Cache-Control: no-store, with exceptions: the static pages and the attachment download serve their own media types, a report asked for with format=csv is text/csv, and every answer under /scim/ is application/scim+json. Where to read more The wiki: The REST API and, for a row per route, APIs Reference section 4. The operator documents: Roles.md and SettingsApply.md. The public documentation: chapter 24, The REST API, with API keys and the OpenAPI document. On this forum: The Control Deck, which is the browser front end for this API; the Linux guide; the 6.3.4 announcement; hmctl and the PowerShell module; the send API and webhooks; and SCIM provisioning and single sign-on.
  • 0 Votes
    1 Posts
    140 Views
    ProgressiverobotP
    Since 6.3.4 the delivery queue shapes what it sends. It pauses a destination that asked it to slow down, caps how many sessions it holds open to one destination and how many deliveries one tenant has in flight, and holds a new outbound address to a daily ceiling at each large provider. Everything in this topic is new in 6.3.4 except MaxOutboundPerDestinationPerMinute, a fixed per-destination rate that 6.3.3 already has. What to know first: of what this topic covers, two things change behaviour on upgrade without a switch. The back-off is on by default, and a deferral this server chose does not spend a message's retries until the message has been queued for longer than its retry schedule spans; only a failure at a remote does. The second covers every hold in this topic, and so changes an upgraded server that already sets MaxOutboundPerDestinationPerMinute. The connection ceilings, the tenant share and the warm-up do nothing until you set them. The order of the checks For each batch of recipients on one target, the server judges in this order, before any connection is opened: The remote domain policy's largest message: 5.3.4, permanent, measured on the queued message. The destination's back-off window. Held until the window ends; then one delivery goes through as the probe, and any other arriving meanwhile waits a minute. The policy's messages a minute (max_messages_per_minute). The connection ceiling: the policy's max_connections where the policy sets one, otherwise the server-wide MaxOutboundConnectionsPerDestination. The tenant share, MaxOutboundConnectionsPerTenant. The server-wide MaxOutboundPerDestinationPerMinute. DNS: the MX lookup (or the route's host) and MTA-STS; then the IP warm-up's daily ceiling; then, where DANE applies, whether the MX set is DNSSEC-validated. Gateway encryption, for a domain whose policy encrypts. The copy is made here, so a batch that is only going to be deferred is never encrypted. The connection attempts. Every one of 2 to 6 is a deferral with 4.4.5 ("mail system congestion") and a sentence naming the rule. The target each check is keyed by is what the batch is delivered to: the recipient domain of an MX delivery, or the host of a route or a relay, or its address when the route names an IP literal, lower-cased. The back-off: a destination said slow down When a destination answers a delivery with a request to slow down, it gets a back-off window: nothing is sent to it until the window ends, one delivery then probes it, and the same answer doubles the window. Deliveries to every other destination are untouched. What is read as a signal. Reply Read as Code 421, unless an exclusion below applies A signal, whatever the text, at the banner or anywhere after it A 4xx whose enhanced status is 4.7.0, 4.7.28, 4.7.500, 4.7.650, 4.7.652 or 4.3.2 A signal A 4xx whose text, lower-cased, contains one of the phrases below A signal The phrases: rate limit, rate-limit, ratelimit, too many messages, too many connections, too much, unusual rate, unexpected volume, temporarily deferred, temporarily rate, throttl, try again later, server busy, back off, slow down. They are English, and a remote that says the same in another language is read by its codes alone. The table is fixed and cannot be edited. What is deliberately not a signal, each of which wins over the text and over the 421 rule: any 5xx: a refusal is not a request to wait; 4.7.1, which greylisting and policy refusals answer, and any reply whose text says greylist or graylist; 4.2.x: one mailbox, never the destination; 4.5.3: too many recipients for one message; 4.4.x: 421 4.4.2 timeout exceeded is the remote's idle timeout on a slow DATA; a connection that was refused or timed out, a DNS failure, and this server's own deferrals. The window and the probe. The first slow-down reply opens a window of OutboundBackoffMinutes. While it is open a batch for that destination is held with 4.4.5, and the application log says: Delivery to <target> deferred: the destination asked this server to slow down at <time> (<reply>); next attempt after <time>. When the window has passed, the first delivery goes through as the probe. If it is delivered, or fails with anything that is not a slow-down reply, the window is cleared. If it is answered with a slow-down reply, the window doubles, up to OutboundBackoffMaxMinutes. A probe nobody ended - a delivery thread that died with it - goes stale after fifteen minutes and the next delivery takes it over. Setting Default Range Meaning OutboundBackoffMinutes 2 0 to 10080 The first window. 0 turns the back-off off OutboundBackoffMaxMinutes 60 1 to 10080 The ceiling a doubling window stops at The two refuse to cross each other on every surface: to move both, move the ceiling first when raising and the base first when lowering. A relay is one destination. With SMTPRelayer set, or a route through a smart host, one slow-down reply from it holds every message that goes through it for the window. One miss, stated in the document. The delivery client keeps a recipient's first non-fatal reply, so a genuine 421 from a second MX host after a plain 4xx from the first is not seen for that recipient. Connection ceilings and the tenant share Both use the same counting semaphore, and it never blocks: a delivery thread that waited for a slot would be a thread not delivering anybody else's mail. Over the ceiling the batch is deferred one to five minutes. Setting Default Range What it bounds MaxOutboundConnectionsPerDestination 0, off 0 to 1000 Sessions open at once to one target that no remote domain policy sets max_connections for MaxOutboundConnectionsPerTenant 0, off 0 to 1000 Deliveries one tenant may have in flight at once The per-domain ceilings are on the remote domain policy, whose TLS half is in Requiring TLS: Policy field 0 means Beyond it Largest message (KB) no limit of ours Not attempted: no connection is opened, and the message is bounced at once with 5.3.4, naming the policy and both sizes Simultaneous connections unlimited Deferred with 4.4.5 for one to five minutes. The count covers every domain the policy's pattern matches Messages a minute unlimited Deferred with 4.4.5 for one to five minutes. Independent of MaxOutboundPerDestinationPerMinute; both apply A policy's connection ceiling wins over the server-wide one wherever the policy sets it. Who a tenant is. The hosted domain of the envelope sender, with an alias domain resolved to its domain. A sender this server does not host - mail relayed for an IP range or a route, forwarded external mail, the null sender - is the one tenant (external), so relayed mail cannot starve the hosted domains either. The tenant share is not a volume control. It is a share of connections, bounded by MaxDeliveryThreads: a share larger than the thread count is no share at all. The per-account sending limits are the volume control. This server's own delivery reports have the null sender, so they share (external) with relayed mail. Where the settings are. The four new settings are on the Control Panel's Delivery of e-mail page beside MaxOutboundPerDestinationPerMinute (Windows), on the Deck's Settings view, over COM as Settings.OutboundBackoffMinutes, OutboundBackoffMaxMinutes, MaxOutboundConnectionsPerDestination and MaxOutboundConnectionsPerTenant (Windows), and over REST on /api/v1/settings as outbound_backoff_minutes, outbound_backoff_max_minutes, max_outbound_connections_per_destination and max_outbound_connections_per_tenant. In hm_settings the two connection rows are named MaxOutboundConnsPerDestination and MaxOutboundConnsPerTenant, because the column holds thirty characters. A deferral this server chose is not counted When every non-fatal failure of a delivery pass was one of this server's own deferrals, the message's next try is set to the longest deferral of the pass with the tries counter untouched, and the log says so: Delivery deferred by this server's own shaping, not by a remote server. Scheduling it for later delivery in N minute(s); the attempt is not counted against the retry limit. When any recipient of the pass failed at a remote, the counted path runs as it always has. Before this, with a small retry count, a message could be bounced having never been offered to anybody. The hold is bounded. A message queued for longer than the span its counted schedule would have allowed - (retries + 1) times the minutes between tries, from the time it was accepted - takes the counted path from then on, so that a destination that says "slow down" for ever still ends in a bounce: Deferred by this server's own shaping, but it has been queued for N minute(s), longer than the M minute(s) its retry schedule spans. This deferral is counted, so that the message is not held for ever. A held pass is a first pass only once. DKIM and ARC signing, the mirror copy and a rule's policy notice happen once, however many passes a message is held through. Nor does a held pass forward, copy, reply or call a rule's script function again: those four actions run once per counted attempt. IP warm-up Schema 6093. A schedule holds one outbound address to a daily ceiling of recipients per provider, rising day by day along a fixed plan; the queue sends up to the ceiling and holds the rest, usually until the next UTC day. Nothing is held until an administrator makes a schedule. A schedule is about one address, and the address has to be bound. It is the local address a delivery leaves from: a global rule's bind to address action when one applies, otherwise SMTPDeliveryBindToIP (Bind outbound connections to IP, smtp_delivery_bind_to_ip). A delivery that binds no address leaves from whatever the operating system chooses, and no schedule is consulted for it. So a schedule does nothing, even enabled, until the address is configured on the host and one of those binds it. The server can check the setting and nothing else: the listing says in_bind_setting, and the Deck warns when it is false. The provider is decided from the mail host, never from the recipient's domain. A company on Google Workspace is reached through Google's MX hosts, and it is Google that judges the address. The first host the batch will try decides. Provider Key Mail host names Google google google.com, googlemail.com, aspmx.l.google.com Microsoft microsoft outlook.com, hotmail.com, protection.outlook.com, olc.protection.outlook.com Yahoo yahoo yahoodns.net, yahoo.com, aol.com Apple apple icloud.com, apple.com Proton proton protonmail.ch, proton.me Every other provider other everything else, counted together at five times the named providers' ceiling Mail relayed through a smart host that is not one of those names is other, whoever the smart host delivers it to. The plans, in recipients per named provider per day. After a plan's last day there is no ceiling, and the schedule holds and counts nothing. Day of standard Ceiling other 1 50 250 2 100 500 3 200 1,000 4 500 2,500 5 1,000 5,000 6 2,000 10,000 7 5,000 25,000 8 to 10 10,000 50,000 11 to 14 20,000 100,000 15 to 21 50,000 250,000 22 to 30 100,000 500,000 The plans side by side: Plan Length Day 1 Last day Rule standard 30 days 50 100,000 the table above gentle 60 days 25 50,000 half of each ceiling, twice the days fast 15 days 100 200,000 twice each ceiling, half the days Days are UTC days, whatever the server's time zone, and a start later than today is refused. Recipients, not messages. A message to forty people at Gmail is forty to Google. Before a delivery connects, its recipients reserve room out of the day's allowance; when the attempts are over, every recipient that was delivered is added to sent and the reservation is let go. A recipient that was not delivered is not counted. So a provider that refuses the warming address is offered as many recipients a day as the queue has; its slow-down replies are answered by the back-off. The counts are written to hm_warmupcounts at every change, so a restart resumes the day. Local delivery is not counted. The hold. Recipients that do not fit - the last of the batch, not chosen by any priority - are marked 4.4.5. When the day's ceiling was reached, the next try is one to five minutes after the next midnight UTC: SMTPDeliverer - Message 1234: 1 recipient(s) held by the IP warm-up of 192.0.2.10: day 1 of its standard schedule allows 50 recipient(s) a day at Google, and today's have been sent. Next attempt after midnight UTC, in 723 minute(s); the attempt is not counted against the retry limit. How long a backlog is carried. With the default schedule - four retries, sixty minutes apart, a five-hour span - a recipient queued in the morning comes back on each of the next five UTC days and is returned if the fifth still has no room. Send no more from a warming address than the sum of those days' ceilings, or raise the retry count for the duration of the warm-up. Managing schedules. On the Control Deck's Delivery queue page, in the Warm-up box, or over REST. The desktop Control Panel has nothing for it, and there is no COM interface. Route What it does GET /api/v1/warmup Today's UTC date and the schedules, each with its day, in_bind_setting, and per provider sent, deferred and ceiling POST /api/v1/warmup Adds one: address (required), plan (standard by default), started (YYYY-MM-DD, today by default), enabled PUT /api/v1/warmup/{id} Changes plan, started or enabled. The address cannot be changed DELETE /api/v1/warmup/{id} Removes a schedule and its counts GET /api/v1/warmup/{id}/history The counts, newest first, with the day and ceiling in force then Reading takes view_queue and changing takes manage_queue; a credential confined to named domains is refused. A second schedule for one address is 409. Back up settings carries the schedules but not the counts. When the database does not answer, the queue does not stop: schedules that cannot be read (7210) and a day's count that cannot be read or started (7211, 7212) leave the delivery going as though the address had no schedule. A count that cannot be saved (7213) is kept in memory; if the service restarts first, the day can go past its ceiling by that much. Where to see it GET /api/v1/queue/shaping (view_queue, server-wide): the open back-off windows - destination, since, until, the window's minutes, the reply and the host that gave it, how many deliveries it has held, whether a probe is out - and the connections open under each ceiling and each tenant. DELETE /api/v1/queue/shaping/{destination} (manage_queue) clears one window once you have fixed the cause: 200 with {"cleared":true}, 404 when no window is open for that name, and an event on the audit trail. The Deck's Delivery queue shows the same as Slowed destinations, with a Resume button, and the connection counts as they were when the page was drawn. The application log says, for each batch held, which rule held it and until when. What is per process. The windows and the connection counts live in memory. A restart forgets every window, which costs at most one more slow-down reply per destination. Nothing is shared between the nodes of a warm-standby pair. What it does not do The signal phrases, the providers and the plans are fixed. A sixth provider, or a plan of your own, is not possible. No per-tenant volume quota beyond the per-account one: this shares connections, not messages a day. No pool of addresses. A schedule is about one address, and mail that did not fit waits. No reputation feedback. The ceilings rise by the calendar, not by how the providers are answering. No delayed delivery report. A sender learns of a long hold only when it ends in a bounce. A deferral by a rate or a ceiling costs one delivery pass per message every one to five minutes for as long as the backlog lasts. The warm-up counts are not exported as metrics. Not proved by any test, according to IpWarmup.md: room held by deliveries in progress; a later host of the other address family skipped for being reached from another warming address; what a database failure does (7210 to 7213); the audit trail's entry; the configuration backup's round trip of the schedules; and an MX lookup's own host names reaching the classification, since every delivery in the fixture goes through a route. Where to read more Operator documents at the v6.3.4 tag: OutboundShaping.md, IpWarmup.md and RemoteDomains.md. Wiki: Outbound Shaping and Routes and Relays. Public documentation: 26.7 The delivery queue and the retry schedule and 14.1 Sending through a smart host. On this forum: The Control Deck, Upgrading to 6.3.4, Requiring TLS for the remote domain policy's TLS half, and DKIM rotation, DMARC and TLS reports, complaints, bounces and the reputation watch for bounces, complaints and block lists.
  • 0 Votes
    1 Posts
    124 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) holds a domain's or an account's rule to its owner: it may no longer set From, Sender, Resent-From, Resent-Sender, DKIM-Signature or the loop count, and its Reply goes only from an address the owner may send as; such a rule is refused when it is saved, one stored before the upgrade runs without that action, and the application log names it. Its Warn sender notice, and the notice of a Reject met after acceptance, come from the postmaster of the rule owner's domain. A credential confined to named domains may no longer give a domain's or an account's rule a route, a bound address or a script function, and a Run function that is not a plain function name is refused when the rule is saved. A rule's forward of mail from outside whose From names a hosted domain's organisation is given an envelope sender in that organisation only when the message passed DMARC for it on arrival; otherwise it keeps the envelope sender it came with or, where that is itself at this server's host name or at a hosted domain in the organisation its From names, is not sent to an outside address at all, and the application log names each recipient. A message over 80 MB goes through the rules by its header alone: the conditions that need the body do not match, a script function is not run, and the disclaimer and the signature are not appended. See the 6.3.7 announcement. This topic covers the rules the server runs on every message, the conditions and actions 6.3.4 added to them to make an outbound content policy, and the three things a domain can be told to do to the mail passing through it: mark what comes from outside, say when a sender has never written before, and put a legal footer on what leaves. The one thing to know first is that these run at three different moments, and the moment decides what each can see. The server's and the domains' rules run in the deliverer on every delivery attempt, with reject also asked at the end of DATA, and an account's own rules once for their recipient; the external tag and the first-contact note are applied to the recipient's own copy at local delivery; and the disclaimer goes on at submission, before this server's DKIM signature is made. Three collections of rules Collection Where it is edited Runs for The server's Application.Rules, /api/v1/rules, the Control Panel's Rules page, the Deck's every message, first A domain's (since 6.3.4, schema 6063) Domain.Rules, /api/v1/domains/{name}/rules, the same pages with the domain chosen at the top every message with a recipient in the domain or a sender it hosts, after the server's An account's Account.Rules, /api/v1/accounts/{address}/rules that account's mail, at local delivery The server's and the domains' rules run in SMTPDeliverer on every delivery attempt, after virus scanning and before DKIM signing and the split between local and external delivery. An account's own rules run after the split, at local delivery, on that recipient's own copy: once for their recipient, when its copy is stored. All three have the same conditions and actions, except that the wiki's Rules and Sieve page lists two actions, send using a route and bind to an address, as absent from an account's rules. Since 6.3.7 a domain's or an account's rule may not set From, Sender, Resent-From, Resent-Sender or DKIM-Signature, which are for the server's rules alone, nor the loop count, which no rule sets, and its Reply goes only from an address the owner may send as: one the account may send as for an account's rule, an address of the domain or of one of its domain aliases for a domain's; a credential confined to named domains may not give such a rule a route, a bound address or a script function. A rule stored before the upgrade runs without the refused action, and the application log names it; to set From on a domain's mail, make the rule a server rule with a condition on the domain. A domain's rules are how a domain administrator has rules of their own: a domain-restricted key reaches its domain's rules and no other's. The Control Panel and the COM objects are Windows-only; the Deck and REST serve both platforms. A repeated pass does not repeat the side effects. A pass that repeats an attempt because this server's own outbound shaping held the message, without counting a try, runs the rules again but does not forward, copy, reply or call a script function again. CHANGELOG records the fix: before it, a held message sent one copy per pass. What a rule queues goes at once. Since 6.3.4 a rule's forward, copy, reply or notice to the sender, and an account's own forwarding, no longer wait up to a minute in the queue: each is queued inside the delivery that caused it, and the delivery manager is now told of it, where before it sat until something else woke the manager. A domain's rules act only on what is that domain's. Domain rules are new in 6.3.4 and shipped with this limit. The sending domain's rules act on the whole message. The sending domain is that of the account that submitted the message (SMTP AUTH, the webmail, a scheduled send), and the envelope sender's domain when that is vouched for: its account submitted the message, DMARC passed for the domain on mail from outside, or the message reaches no other domain's mailboxes. An envelope sender nobody vouched for does not choose a domain's rules for mail to another domain's mailboxes. A recipient domain's rules act on the whole message only when every recipient is its own. Otherwise they act on the domain's own recipients: a delete or a reject takes those recipients off the message, a quarantine keeps a copy for them under the rule's name, and a copy goes to them alone. Such a reject is met at delivery, not in the SMTP session, whose one answer is for every recipient; a sender this server hosts is mailed a notice naming the recipients refused. A header, a disclaimer, a script function, a route, a bound address and required TLS are not applied, since they would change the message for everybody. As domain rules were first written in the source tree, without this limit, one tenant's delete, reject, header or route reached another tenant's recipients; no release had that. Conditions and actions The long-standing conditions (From, To, CC, Subject, body, size, the recipient list, delivery attempts, any named header), the match types, the long-standing actions and how a rule set is evaluated are the rule engine's, and are set out in Rules and Sieve: what runs when. Since 6.3.4 there is one more condition, the attachment name (attachment_name every attachment's file name, with ; between them, so contains .exe or a wildcard *.zip reads as "any attachment is". Run a script function takes a plain function name since 6.3.4, without a switch: ASCII letters, digits, _ and $, not starting with a digit. On Windows any other name is refused as HM5018 each time the rule fires; on Linux it finds no function, nothing is run, and the error log has a Script Error line. Since 6.3.7 such a rule is also refused when it is saved, over COM and REST, for a rule of every owner. Give every such rule a plain function name before you upgrade, and look for HM5018 after; why it changed is in event scripts and the 6.3.4 security fix and the 6.3.4 security announcement. Since 6.3.4 there are five more actions: Action REST word What it does Add a disclaimer add_disclaimer Appends the text to the message's plain and HTML parts when the rule matches, with X-hMailServer-Disclaimer naming the rule. The domain disclaimer stays the unconditional footer it was Reject with a text reject The SMTP session answers 550 5.7.1 with the text before the message is accepted. A reject with no text of its own names the rule Require TLS require_tls Every external delivery of the message needs STARTTLS. A destination that offers none is deferred with a reason naming the rule; the message is not sent in the clear Quarantine quarantine Held in the quarantine under Rule: <name> for an administrator to release or delete. A quarantine that could not store it delivers the message rather than lose it, and says so in the error log Warn the sender warn_sender The message goes where the rule's other actions send it, and a sender this server hosts is mailed a notice Where reject is asked. The server's rules, and those of every domain whose rules act on the whole message, are asked at the end of DATA, and the message is parsed for the match only when one of them has a reject action at all. The webmail's send route asks the same rules before it queues, and answers 403, code policy_rejected, with the text. A message that reached delivery another way is dropped, as a delete is, with the rule named in the log. The outbound content policy Since 6.3.4. What Exchange calls data loss prevention is here four more conditions and one more action in the same rules, with no second engine. Each condition answers as a list with ; between the entries, so contains x reads as "any of them is x" and equals "" as "none". Condition REST word Answers Sensitive content sensitive_content The detectors that found something in the readable text: card_number, iban, account_number Attachment type attachment_type Every attachment's type from its first bytes, not its name: executable, elf, pdf, zip, office_xml, jar, office, rar, 7z, gzip, png, jpeg, gif, rtf, script, text, empty, binary External recipients external_recipient Every envelope recipient this server does not host; empty when every recipient is local Readable text content The text itself, for a pattern of the rule's own The readable text is the plain part, the HTML part with its tags removed, and every attachment whose bytes are text, a .csv sent as application/octet-stream included. It is bounded at a megabyte. The detectors. card_number: a run of 13 to 19 digits, with at most one space or hyphen between any two, that passes the Luhn check and is not one digit repeated. A run longer than 19 digits is not searched for a card inside it. iban: two letters of a country in the IBAN registry and two digits, then exactly that country's number of characters, passing the ISO 13616 check. account_number: the regular expression in the dlp_account_number_pattern setting, such as \b\d{2}-\d{2}-\d{2} \d{8}\b for a sort code and account. Empty, the default, is a detector that never fires. A pattern that does not compile is reported as HM6821, and a search given up as too complex, or one that takes over the 250 ms budget, as HM6822; either way the pattern is rested for five minutes, finding nothing meanwhile. That is fail-open, on purpose and said out loud. Marked internal needs no new condition: header Sensitivity contains Company-Confidential, combined with external_recipient contains @. A sealed message, encrypted end to end and judged from its body rather than its label, has no text the server can read, so sensitive_content and content do not match it, whichever way they are phrased, and the application log says why once per rule. attachment_type and external_recipient still answer. A worked policy. From the document: POST /api/v1/rules { "name": "No card numbers leave", "all_criteria": true, "criteria": [ { "field": "external_recipient", "match": "contains", "value": "@" }, { "field": "sensitive_content", "match": "contains", "value": "card_number" } ], "actions": [ { "type": "reject", "value": "Card numbers may not be sent by mail. Use the payment portal." } ] } A rule with external_recipient contains @ fires only on a message with a recipient this server does not host; a rule without that condition applies to inbound mail too. A policy is a rule of the server or of the sender's domain. It cannot be an account's rule, because those are the recipient's own filters and never see the account's own outgoing mail. The notice warn_sender sends comes from postmaster@<their domain> (to 6.3.6; since 6.3.7 from the postmaster of the rule owner's domain, and for a server rule of the default domain when one is set and hosted here, else of the hosted domain the message was addressed to, else of the sender's own, signed as that domain) with the subject Policy notice: <rule name>, the headers X-hMailServer-Policy: <rule name> and X-hMailServer-Policy-Outcome: delivered, and Auto-Submitted: auto-generated. It is sent only when this server hosts the sender's domain, because a notice to an outside sender is backscatter, and it goes once per message, on the first delivery attempt. A rule with only that action delivers the message. A released message is not held again by the rule that quarantined it; the rule's other actions still run. What the policy does not do. It is pattern matching, and patterns have false positives: the document says one 16-digit reference number in ten passes Luhn, and recommends warn_sender or quarantine as the first choice for a new pattern rather than reject. It reads text, not documents: a card number inside a PDF, a Word file or a spreadsheet is not found, an image of a card is an image, and an encrypted zip is zip. There are no fingerprints, classifiers or dictionaries, no per-user exceptions, and no policy tip in the sender's mail client before they press Send. What a domain does to a message Since 6.3.4 (schema 6044). Three per-domain features, off on every existing and every new domain, costing nothing while off. They are set in the Control Panel's domain dialog, tab Tagging and disclaimer (Windows only); in the Control Deck's domain editor, What this domain does to a message; or with PUT /api/v1/domains/{name} and the keys external_tag_subject, external_tag_header, external_tag_text, first_contact_tip, disclaimer_enabled, disclaimer_plain_text and disclaimer_html. Who counts as outside is one rule, MessageOrigin::IsExternalSender. A sender is outside when none of these is true: The session authenticated. This is read from the topmost Received: header this server wrote; X-AuthUser is deliberately not consulted. A trusted incoming relay handed the message over. Every address the message names, the envelope sender and the From: address, is at a domain this server hosts. Either being outside is enough. The sharp edge: an inbound gateway. If you run a filtering gateway in front of this server and list it as an incoming relay, nothing arriving through it is ever tagged. The choice is between the tag and the relay declaration; the document says there is no third answer today. It is not anti-spoofing. A message that forges a hosted address in both its envelope and its From is judged inside and is not tagged. The control for that is DMARC with a policy on your own domain. Switch What the reader gets Cost external_tag_subject The tag text at the front of the Subject: external_tag_text, at most 100 characters, [EXTERNAL] when empty. A subject that already begins with it gets no second copy Breaks any DKIM signature the sender made over the Subject for anyone who verifies downstream, as the spam subject prefix does. The verdict this server reached is unaffected external_tag_header X-hMailServer-External: YES, which the webmail shows as This message came from outside your organisation. None to the signature. Prefer this wherever the reader's client can show it first_contact_tip X-hMailServer-First-Contact: YES when an outside sender writes to an account that has had mail from other people but never from them. The webmail shows You do not usually get mail from this sender. One indexed SELECT per delivered copy, then an UPDATE, or a further SELECT and an INSERT the first time The tag is applied to the recipient account's own copy, above the account's rules and its Sieve script, so both can match on the header, and below the per-account spam settings. A domain with tagging on does not share one delivered file between several recipients (DeliveryHardLinks). Where a domain has turned the feature on, a forged copy of the header on a message judged inside is removed. The first-contact memory is the table hm_knownsenders, one row per account and sender. When an account that signed in sends a message, each recipient is recorded as somebody that account knows, so their reply is not announced; an unauthenticated session records nothing. The wiki adds that mail sent from the webmail or by send-later does not record its recipients, so a reply to a message first sent that way can be announced once. Two limits are by design. After a cold start, a new installation or the day the switch is turned on, most senders look new; the document says to expect a week. After a restore, the memory is as of the backup. The domain disclaimer disclaimer_enabled appends the domain's legal footer to mail leaving the organisation. Filling in only disclaimer_plain_text uses it for both forms. It runs where the domain signature runs, before DKIMSigner::Sign, so this server's own signature covers the footer. The footer is appended to the text/plain part and the text/html part in place; a message with neither is left alone. It is never added to: a signed or encrypted message (multipart/signed, multipart/encrypted, application/pkcs7-mime, application/x-pkcs7-mime, application/pgp-encrypted or application/pgp-signature), which is sent exactly as written, with a line in the application log; an automatic reply, a vacation message or a delivery report; a bounce; a list posting (List-Id, List-Unsubscribe, or a Precedence: of bulk, list or junk); a message with no recipient outside this server; a message this server already footed, which carries X-hMailServer-Disclaimer; a message relayed in from outside that merely claims a hosted sender domain. A reply quoting a message that already carried the footer does not collect a second copy. A footer shorter than eight characters is never treated as already present. Webmail and send-later get the footers too, since 6.3.4. The domain's signature and disclaimer go on mail sent from the webmail (POST /api/v1/me/messages, which is also its reply, forward and phishing report) and from its send-later, through the same function as SMTP (docs/DomainTransforms.md; CHANGELOG). In 6.3.3 and earlier only the SMTP path had either footer, which is what the wiki's Stopping Spam §10.7a still describes. Two sharp edges on the webmail paths. Send-later sends with the account as the envelope sender, so a draft scheduled with a chosen From at another hosted domain gets the account's domain's footers. And with the domain's signature method set to overwrite the account's, the webmail has already written the account's signature into the body, which the server does not take out. Signed and encrypted messages get no signature either, since 6.3.4. Before, a signature was appended inside a multipart/signed, breaking the S/MIME signature. This is a change on the SMTP path too: an installation that relied on a signature inside signed mail will now see none there. Where to read more Sieve, the account's own filters and the rule engine itself are in Rules and Sieve; the blocked-attachment list and the sandbox are in the topic on viruses and dangerous attachments. The two browser surfaces have topics of their own: The Control Deck and The webmail portal at /portal. https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/MailFlowRules.md https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/OutboundContentPolicy.md https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/DomainTransforms.md Wiki: https://gitlab.com/hmailserver/hmailserver/-/wikis/Rules-and-Sieve and, for the tag and the note, https://gitlab.com/hmailserver/hmailserver/-/wikis/Stopping-Spam Public documentation: https://www.progressiverobot.com/hmailserver-documentation/#121-hmailserver-rules and https://www.progressiverobot.com/hmailserver-documentation/#27-rules-in-practice-a-cookbook
  • 0 Votes
    1 Posts
    136 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) reads a link's host that is not ASCII by UTS 46, and an HTML part's character references as a browser does, for the URL entries of the allow and block lists, so a message whose links escaped a list that way can now be tagged or held; a URL entry written in Unicode is stored in its xn-- form and covers a link written either way, and one with a label UTS 46 refuses is refused when it is saved. In the Control Panel the Spam filtering group's six allow and block lists are under Lists, and Ctrl+K takes every old title to its page. See the 6.3.7 announcement. This topic covers five layers that sit beside the score-based spam tests rather than inside them: the allow and block lists at three levels, the quarantine digest, spam traps, country blocking and impersonation protection. All five are new since 6.3.4, and the one thing to know first is that every one of them is off, or inert, until an administrator sets it: a server upgraded to 6.3.4 and left alone has no list entries, sends no digest, has no traps, judges nothing by country and checks no display name. The allow and block list: one table, three levels Since 6.3.4 (schema 6046). The older lists (the anti-spam white list, Blocked senders, the greylisting white list) act during the SMTP conversation, for every recipient at once. The allow and block list is per recipient. It is one table, hm_senderlist, with a scope on each row: each account's own safe and blocked senders, a domain's list, and the server's. Entry Covers [email protected] That one address, in any case. No wildcards example.com example.com and every subdomain; not notexample.com, not example.com.attacker.net @example.com The same as example.com, and stored as example.com https://bad.example/anything A URL: stored as bad.example/, covering a link in the message to that host or a subdomain, whoever sent it An entry covers a message when it matches either the envelope sender or the address in the From header, because mail sent through a mailing service carries the service's address in the envelope and the brand's in the header. A message with the null sender is never decided by any list. URL entries are in 6.3.4. CHANGELOG's 6.3.4 section lists them, and they are consulted only when neither address decided anything. The closing section of docs/SenderListsAndDigests.md, which says "URLs are not list entries", is older text left in the file. What allow and block do. The lists are read at local delivery, once per recipient, after the spam tests have run. The message was classified as spam It was not Allowed Delivered unmarked for this recipient: the spam flag, the X-hMailServer-Spam and reason headers and the subject tag are removed from this copy Delivered as it is Blocked Held in the quarantine for this recipient when the quarantine is on; marked as spam and delivered when it is not The same A block never deletes, and no score is recorded on a blocked message, because nothing measured it. A message released from the quarantine is never held again by a list. The other recipients of the same message are not affected. Precedence is nearest first: the account, then its domain, then the server. The first of the three that has anything to say about the sender is the only one consulted, so an account's allow beats a domain's block. That is the reverse of Microsoft 365's order, and deliberate: the person the message is addressed to can tell whether they wanted it. An administrator who needs a sender stopped for everyone still has Blocked senders, which nobody's own list overrules. Inside one list the more specific entry wins (an address beats a domain, a longer domain beats a shorter one), and on an exact tie the block wins. Neither address is proof. Both are whatever the sender wrote, so an allow entry is as forgeable as the address it names. The document's advice: keep allow entries to correspondents whose domains publish DMARC, and prefer a single address to a whole domain. List Webmail Control Panel (Windows) Control Deck REST The account's own Settings → Safe senders none none /api/v1/me/sender-list A domain's none Domain → Allow and block list tab Domain editor → Allow and block list /api/v1/domains/{domain}/sender-list The server's none Spam & virus filtering → Allow and block list Lists → Allow and block list /api/v1/sender-list Over COM (Windows only) the domain's list is Domain.SenderList and the server's Settings.AntiSpam.SenderList. There is no administrator path to an account's own list. Block this sender and Report phishing in the webmail now add the sender to the account's blocked senders; they used to write a rule into the Sieve script, and rules written that way are left where they are. A list holds at most a thousand entries; adding a value already there rewrites the entry (200 rather than 201). Expiry can be set over REST, in the Deck and in the Control Panel's two list editors (a switch and a date); the webmail does not offer it. docs/SenderListsAndDigests.md still says the Control Panel has no expiry; that passage predates the change, which the repository's roadmap records as made after 6.3.3. The quarantine digest Once a day, one plain-text message goes to each account of a domain that has something newly held for it, listing each message's subject, sender, when it was held and why, with a link that delivers each entry, a link that delivers everything listed, and a footer link that stops the digest (also the List-Unsubscribe header). An empty digest is never sent, and an account that has had one is not sent another for twenty hours. The message comes from the domain's postmaster with the null envelope sender and Auto-Submitted: auto-generated, and never asks for a password. Setting Default QuarantineDigestBaseUrl, the address the links point at, such as https://mail.example.com empty: no digest is sent QuarantineDigestLinkHours, 1 to 720 72 The domain's switch and hour (quarantine_digest, quarantine_digest_hour; 0 to 23, local time), which an account may override off; 7 All three are in the database, none in hMailServer.ini. The quarantine itself (QuarantineEnabled) has to be on, and the address must be an absolute http:// or https:// URL of the listener the webmail is served from. When it is sent. The task wakes every five minutes and, within the digest hour, sends only its share, so a domain of a thousand accounts is spread across the hour. At most five hundred digests go in one run. Over COM (Windows only), AntiSpam.SendQuarantineDigests() sends now, ignoring the hour and the twenty-hour rule and nothing else. The links. Each is 32 random bytes recorded against one account and one entry, usable once, and no longer valid after QuarantineDigestLinkHours. Following a link does not act on its own: it opens a page with one button, because mail security products follow every link in a message, and a link that released mail on its own would have one of them releasing every held message. Stopping the digest does not stop mail being held. Spam traps Since 6.3.4 (schema 6058). A trap is an address that was never real and never given to anybody, so a message to one comes from a harvested, bought or guessed list. At RCPT TO a trap is refused exactly as any unknown address is, 550 Unknown user, made by the same code, so the sender cannot tell. The check runs before the delivery lookups, so a catch-all, greylisting or a backup-MX callout cannot answer first. What happens Setting The connecting address Banned through the server's own auto-ban, as an expiring range named Auto-ban: spam trap <address> SpamTrapBanMinutes, 1440. 0 bans nobody The envelope sender Added to the server's block list with an expiry, only when its domain's SPF passes for the connecting address SpamTrapBlockDays, 30. 0 blocks nobody The hit Recorded in hm_spamtraphits and counted in hmailserver_spam_trap_hits_total SpamTrapHitRetentionDays, 90 The application log gets one line per hit and one per ban: "Auto-ban: 203.0.113.9 is banned for 1440 minutes after addressing the spam trap [email protected]" The packaged fail2ban filter matches that line, so a Linux installation that runs fail2ban with the packaged jail turned on bans the address at its own firewall as well. The jail ships disabled; the wiki's Installing on Linux page turns it on with: printf '[hmailserver]\nenabled = true\n' > /etc/fail2ban/jail.d/hmailserver.local systemctl reload fail2ban The SPF condition. As first written, in the source tree, a trap put the envelope sender of every message sent to it on the block list, whatever that sender was; spam traps are new in 6.3.4, which carries the fix, so no release did. CHANGELOG records why that changed: the envelope is whatever the client types, so one connection naming a bank's address could have had the bank's real mail held or marked for every mailbox. Now only an SPF pass blocks; the address is banned either way. The wiki's summary in Stopping Spam §10.15 does not mention the condition; docs/SpamTraps.md and CHANGELOG do. Who is never banned: an authenticated session, an address whose IP range does not run the spam tests, an address or sender the anti-spam white list covers, an incoming relay, and an address on AutoBanNeverBan. The sender is not blocked for the null sender, for a sender claiming a domain this server hosts, or where the server's allow list already names it. Training. A trap hit trains nothing by itself, because no content arrives. With SpamTrapTeachSpamAssassin on (off by default; it needs SpamAssassin enabled and a spamd started with --allow-tell), a message sent to a real mailbox in the same transaction is taught to spamd as spam. Declaring a trap. It must be an address in the domain it is declared on, and one nobody uses: an account, alias or list with that address is refused, and none can be given a trap's address while the trap exists. Where How Control Deck Domain editor → Spam traps; the Spam traps view for the record across every domain REST GET/POST /api/v1/domains/{domain}/spam-traps, DELETE /api/v1/spam-traps/{address}, GET/DELETE /api/v1/spam-traps/hits Command line hmctl spamtrap create --domain example.com --set [email protected], hmctl traphits list PowerShell Add-HmSpamTrap, Get-HmSpamTrapHit COM (Windows only) Domain.AddSpamTrap, Utilities.GetSpamTrapHits A good trap is an address never published anywhere, or one published once, long ago, and unused for years (give it a year of refusals first). A bad trap is a typo of a real address such as saels@, or any address in a domain with a catch-all: there every other unknown address is accepted and the trap is not, which is the difference a sender can measure. Clearing the record of hits undoes nothing; the bans and blocks stay until they expire or are removed. The traps are in the backup with their domain; the record of hits is not. Country blocking Since 6.3.4 (schema 6052). Two kinds of policy, in one grammar. A policy per listener family (SMTP, IMAP, POP3, and the HTTP listeners) is applied when a connection is accepted: before the greeting, before the TLS handshake and before anything touches the database. A policy per domain is applied when somebody signs in to an account of that domain, on any protocol, and covers OAuth2 bearer tokens, client certificates, Kerberos tickets, passkeys and single sign-on as well as passwords. Value What it means (empty) Nothing is judged by country. The default everywhere allow:GB,IE,FR Only these countries may connect, or sign in block:CN,RU These countries may not connect, or sign in The database is yours to supply. Point GeoIpDatabaseFile at a MaxMind .mmdb file, such as /var/lib/GeoIP/GeoLite2-Country.mmdb or C:\GeoIP\GeoLite2-Country.mmdb. The server ships none and never fetches one. Replace the file in place and the server reloads it within five minutes. GeoIpDatabaseMaxAgeDays (45) is how old the file's build date may be before the log says so; a stale database is still used. What is exempt. CountryBlockingExemptRanges defaults to the loopback addresses and every private network: 127.0.0.0/8,::1/128,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,100.64.0.0/10,fe80::/10,fc00::/7 There is no second list compiled into the server. If you empty it, do so with country_policy_http off, or the Control Deck is the next thing you lose. An open session is never disturbed; a policy takes effect on the next connection. Which way it fails. With no database at all (none configured, or the file missing or unreadable) everything is allowed, whatever the policies say, and the reason is logged once. With a database loaded that has no country for an address, an allow list refuses it and a block list allows it. What a refused client is told: nothing. A refused connection is closed with no banner. A refused sign-in gets the ordinary invalid-credentials reply, byte-identical to a wrong password, and is counted against neither the account's lockout nor the address's auto-ban, since the person refused is very often the account's real owner, travelling. Your copy is in the application log: Country blocking: refused SMTP from 203.0.113.9 (CN) for connection. Country blocking: refused sign-in from 203.0.113.9 (CN) for example.com. The counters are hmailserver_country_blocked_total and hmailserver_country_blocked_by_country_total{country="CN"}, and the report is GET /api/v1/reports/countries. Setting it up. In the Control Deck, Settings → Server holds the server's settings and Domains → (a domain) → Country blocking the domain's policy. From a shell: hmctl settings set --set geoip_database_file=/var/lib/GeoIP/GeoLite2-Country.mmdb hmctl settings set --set country_policy_smtp=block:CN,RU hmctl domain update example.com --set active=true --set country_policy=allow:GB,IE The REST keys are country_policy_smtp, country_policy_imap, country_policy_pop3, country_policy_http and, on a domain, country_policy. Over COM (Windows only) it is Settings.CountryPolicy("smtp") and Domain.CountryPolicy. What it does not do. It is not a per-message control. It says nothing about a proxy: behind HAProxy with the PROXY protocol, the address judged is the proxy's, so put the policy on the proxy. And it is only as true as the database; it is not an authentication control. Impersonation protection Since 6.3.4 (schema 6087). Per domain, off on every existing and new domain. It runs at local delivery on the recipient's own copy, only on mail from outside, and makes two checks. A borrowed name. The From display name, folded, equals the name of an active account of the recipient's domain or of a protected person, while the address is not theirs. A lookalike domain. The From domain, and the envelope sender's when it differs, is equal to a hosted domain, an alias domain or a protected partner after the confusable folding while not equal before it (examp1e.test, exarnple.test), or is one Damerau-Levenshtein edit away inside one label (examplee.test, exampel.test). Distance 1 is the rule and 2 is not. A DMARC pass changes nothing here: the attacker publishes the lookalike's records exactly as a legitimate domain would. X-hMailServer-Impersonation: name "Ada L0velace" matches Ada Lovelace ([email protected]); from [email protected] X-hMailServer-Impersonation: domain examp1e.test looks like example.test; from [email protected] Windows and Linux differ in one step of the folding. Unicode NFKC normalisation, which turns a full-width or ligature spelling into its letters, runs on Windows. It does not run on Linux, which has no Unicode tables in this build; there the step is skipped and the confusable table does the rest. The other steps run on both. What slips past. Word order and decorations are not folded: Holloway, Chris and Chris Holloway (CEO) are not Chris Holloway. A name carried only in the local part of the address is not compared. And a domain with an account named Support or Info will have every vendor's "Support" flagged. impersonation_action What happens off (0) Nothing is read flag (1) The copy gets the X-hMailServer-Impersonation header. The subject is left alone and the sender's DKIM signature is untouched. The webmail shows a warning bar and a Suspicious sender mark quarantine (2) The copy goes to the quarantine with the reason and score 0. A release does not re-check. With QuarantineEnabled off, the domain gets flag instead What is exempt: a sender the recipient's own safe-sender list allows (or the domain's or server's allow list, where nothing nearer decides), and a From domain that is itself hosted here or a protected partner. Nothing else: a person's own name from their personal address at another provider is flagged until that address is on the safe-sender list. The setting is on the Impersonation protection tab of the Control Panel's domain dialog (Windows only), in the Deck's domain editor, and on PUT /api/v1/domains/{domain} as impersonation_action, protected_persons and protected_partners. Each list holds at most 2,000 characters. Where to read more The score-based pipeline these layers sit beside is in the anti-spam stack end to end, the outside-sender tag in the topic on mail flow rules, and the auto-ban a trap uses in IP ranges, auto-ban, lockout and tarpits. For the Deck itself, see The Control Deck. Wiki, §10.10a and §10.15: https://gitlab.com/hmailserver/hmailserver/-/wikis/Stopping-Spam https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/SenderListsAndDigests.md https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/SpamTraps.md https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/CountryBlocking.md https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/ImpersonationProtection.md Public documentation: https://www.progressiverobot.com/hmailserver-documentation/#104-white-lists
  • 0 Votes
    1 Posts
    130 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) regroups the Control Panel's sidebar: the audit trail, journaling and eDiscovery are a Compliance group of their own rather than the last rows of Monitoring & troubleshooting, as the table below has it, and Ctrl+K takes every old title and route to its page. See the 6.3.7 announcement. This guide covers what an operator is asked for when a lawyer, a regulator or a person with a data request arrives: eDiscovery (cases, searches, legal holds and exports), a person's own data (the export and the erasure), and the audit trail that records who did what. eDiscovery, the person's data as one archive, the erasure of log lines, the erasure's reach into the stores beside the mailbox, and the audit trail are new since 6.3.4. The one thing to know first: cases, their files and their holds are not in the backup. A server rebuilt from a backup has no cases, no exports and no holds, so place the holds again before its mail can be deleted. Where it is Where What The Control Deck, Discovery view The whole of it: cases, the search form, holds with Lift the hold, exports with Download, Open and Close REST, /api/v1/discovery/cases The same ten routes the Deck calls, in the OpenAPI document The Control Panel (Windows only), Monitoring & troubleshooting → Discovery (eDiscovery, in the Compliance group, since 6.3.7) Read-only: the cases and their lines of the audit trail, read over the REST listener, which must be on (RestApiPort). It changes nothing COM, hmctl Neither On Linux, then, discovery is worked from the Deck or over REST. Discovery and the erasure do nothing until somebody opens a case or asks for an erasure; the audit trail is on from the first start. Two neighbouring tools are not this one. The archive has had a legal hold of its own since before 6.3.4 (/api/v1/archive/{id}/hold), which keeps one archived copy from the archive's retention sweep (Quotas, retention and the archive). Journaling, a copy of a domain's or an account's mail sent elsewhere as it passes, has its own topic: Journaling and dual delivery. Cases A case has a name (up to 255 characters), a description (up to 1,000), who opened it and when, and a state: open or closed. Its searches, holds and exports belong to it. Closing a case releases every hold of it that is still live, each release on the audit trail; a closed case runs no search, places no hold and makes no export, and what it made stays readable and downloadable. A case is never deleted, and an address's erasure does not reach it. The files a case makes are under the data directory, in Discovery\case-<id>\: search-<id>.json for each search and export-<id>.zip for each export. Nothing removes an export; delete its zip by hand when it is no longer needed. Searching POST /api/v1/discovery/cases/{id}/searches takes: Part What it means custodians (required, at most 100) An address, a domain, or * for every account on the server and the whole archive from, to (optional, YYYY-MM-DD) The day a message is dated by its Date field, read as UTC; else the day the server stored it words (optional, at most 50) Every word must be found, a quoted phrase as one: a case-insensitive substring of what IMAP SEARCH TEXT reads attachment (optional) name, type and min_kb; {} is any attachment at all scope mailboxes, archive or both, the default There is no sender or recipient criterion: a word is looked for in the whole decoded header, and a person is named as a custodian. A query the server cannot take is refused with 400 and the reason; 202 answers with the search's id, and the search is queued. Where it looks. For each custodian account: its folders, its deleted items (what a hold kept, and what RecoverDeletedDays keeps), and the archive. A search copies nothing; it lists each hit, and lists separately what it could not read, with the reason. What it cannot see. 100,000 hits. A search stops there and says truncated. Narrow it and run it again. Text inside a PDF, a Word document or a spreadsheet. Only text/* attachments are read, up to 1 MB of characters a message. An attachment criterion still finds the PDF by name, type and size. More than 80 MB a message. It is listed as not searched when the query asks about its body, and never dropped as a non-match. Zero-access mailboxes and messages encrypted end to end are matched on their headers alone and marked headers_only. The stores beside the mailbox, <data directory>\Confidential and \Protected, and archived copies with no index row (a bounce, a delivery report). The full-text index is a filter, never the answer. It is off by default (IndexerFullText). When it is on, a message the index proves cannot contain a word is ruled out; every other message is read. One at a time. Searches and exports are queued and run by one runner, the server's own. Only one hMailServer service may run against a database at a time (Warm standby and high availability), so a standby started after a failover takes over the queue rather than sharing it. Holds POST /api/v1/discovery/cases/{id}/holds takes one of: Body Holds {"scope":"account","target":"[email protected]"} Every message of the account, and its archived copies {"scope":"domain","target":"example.com"} Every message of every account in the domain, and their archived copies {"scope":"archive","search":12} The archived copies search 12 of this case matched, at most 10,000 at once A hold is live until it is released or its case is closed, and what it keeps is kept by any live hold of any case. Path Under a hold The person's delete (IMAP EXPUNGE, POP3 DELE, the webmail's delete, a folder deleted) and an administrator's delete of a message The message leaves the folder, and a copy is kept among the account's deleted items whatever RecoverDeletedDays says (0 by default). If the copy cannot be made, the delete is refused (HM7333) An IMAP MOVE out of the account The original is kept among the held account's deleted items Every message of the account deleted at once Refused, 409 over REST The account's own filters at delivery: a rule's Delete or Reject, a Sieve discard, reject or redirect, a forward that keeps no copy The copy is kept among the deleted items instead of dropped; if it cannot be kept, the message is delivered Retention, the recover-deleted sweep, deleting a deleted item for good Nothing of the held account is deleted A mailbox restore with mode replace, a full restore with the domains option Refused (the full restore as HM5014) Moving the account to another domain, removing its mailbox key, deleting the account or its domain Refused, over REST, SCIM and COM alike The archive's retention sweep and an address's erasure A held copy keeps its file and its index row What a hold does not reach: the server's and a domain's rules, which decide before a message is split between its recipients; spam and viruses refused or deleted before a message is the account's, and what the quarantine discards; the delivery queue; the confidential and password-protected stores; archived copies with no index row. If ArchiveDir was put inside the data directory, a full restore with the messages option replaces held archived copies too, so keep the archive outside it. The hold is not hidden from the custodian. The account's own GET /api/v1/me/recoverable answers held: true and lists what the hold kept, and the webmail's Recover deleted items page offers no delete for good. It fails closed. When the holds cannot be read, every question is answered held, and the error log says so once a minute (HM7332). A hold is a row in the database. The server reads the holds again as soon as one is placed or released, and no more than five seconds after a change made elsewhere. Exports POST .../searches/{search}/export queues an export of a finished search's hits. The zip holds: Each hit as EML, the message's own bytes as the server stores them, under <custodian>/<folder>/<n>-<message id>.eml, and an mbox per custodian folder beside them, in parts of at most 256 MB. SHA256SUMS, so that sha256sum -c SHA256SUMS checks the unpacked export. manifest.json: the case, the search as it was run, who ran it, who asked for the export, every item with its path, SHA-256 and size, and every item withheld, with the reason. The zip's own SHA-256 is kept with the export, and the case answers it (exports[].sha256). Withheld: a zero-access mailbox's messages, and anything deleted, moved or swept since the search ran. Search again to find where a message is now. Limits. The zip written is the 32-bit format, so no export passes 4 GB: narrow the search and export each part. A download past 500 MB answers 413 with where the file is on the server's disk and its SHA-256. An export that would leave the data directory's volume with less than a gigabyte free fails. A discovery export is never a PST; an examiner who needs one converts the EML files with a third-party converter. Who may Permission Allows Held by view_discovery Reading the cases, their searches and hits, holds and exports The server administrator and the auditor role manage_discovery Opening and closing a case, running a search, placing and releasing a hold, making an export The server administrator; a custom role may be given it Downloading an export needs view_mailbox as well, since it hands over the mail itself, the custodians' deleted items among it. A search that reads the archive needs view_archive as well. A credential confined to named domains names no custodian outside them, never *, and is answered 404 for a case that reaches outside them. A person's data: the export One .zip of everything the server holds for an account. The person downloads it from the webmail's Your data card (GET /api/v1/me/export.zip), with no administrator involved; that download is refused (403) to a request authenticated with an app password. The administrator has the Deck's "Everything (.zip)" on the account row, GET /api/v1/accounts/{address}/export.zip (the administrator password's alone), POST /api/v1/accounts/{address}/export with archive, hmctl privacy export <address> --archive <file> and, on Windows, COM Account.ExportArchive(file). Entry What it is manifest.json The address, the moment, and the counts of what follows mail/<folder path>.mbox Every folder with mail, one mbox each account.xml What the backup carries for the account, less the credentials contacts.vcf The address book as vCard 3.0 calendars/<calendar>/<object> Every calendar object as the client sent it preferences.json, filters.sieve The webmail's preferences, and the active Sieve script logs.txt The lines of the server's own log files that name the address, up to 50 MB A download past 500 MB is refused with 413, and the operator writes the archive to disk instead; the disk form has no ceiling but the format's 4 GB. A person's data: the erasure POST /api/v1/addresses/erase, hmctl privacy erase, the Deck's "Erase traces" and, on Windows, the Control Panel's "Erase traces..." remove what deleting the account deliberately leaves: quarantine entries, message-trace rows, greylisting triplets, distribution-list memberships, aliases on either side, the bounce and complaint records, and the rows naming the address in other people's address books and first-contact memory. With the archive included, the person's archive tree is swept and every index row naming the address goes. The log lines are asked for by name (include_logs on the REST body, --logs every occurrence becomes [erased], and a log past 256 MB is reported instead of rewritten. Since 6.3.4 it also reaches the stores beside the mailbox. A message held for review that the address sent or would receive is discarded. The address is taken out of the recipients of a message that expires, and one it sent is ended. A password-protected message to it is ended. The recipients an account remembers writing to, the out-of-office memory, booking policies and correspondents' S/MIME certificates and OpenPGP keys lose it. Every list of addresses is matched whole. Erasing [email protected] leaves [email protected] and [email protected] as they are. A legal hold outranks it. An erasure never deletes a message from a mailbox, so a hold meets it only in the stores it deletes from, and there the hold wins. The REST answer's held counts what a hold kept; run the erasure again once the case is closed. When the holds themselves cannot be read, the erasure fails (HM6250; 500 over REST) and says nothing about a legal hold. What it leaves, by design, among others: mail in mailboxes, the person's own and everybody else's, and a mailbox's deleted items; a list's archive of its posts; other people's calendars; configuration somebody made that names the address (a forward, a rule, a list's moderator); the audit trail, and the alert events, which nothing prunes yet; the spam trap record; the delivery queue and the webhook queue; the contacts in DMARC and TLS reports; the archive's Inbound folder; and backups, which are outside the running server's reach. PersonalData.md has the full list, with each reason. The audit trail, for a compliance reader The audit trail is on by default. It keeps one row in hm_audit for each administrative change made over COM, REST or the Deck, a mailbox's move included, with a few exceptions such as an API key made or revoked or a quarantined message released or deleted; it records changes, never reads. A password, key, token or other secret is recorded as (changed), never as a value. Each row carries the SHA-256 of the row before it and its own, so GET /api/v1/audit/verify tells an edited row from a removed one: tampering is detectable, not impossible, since anyone who can write the table can recompute the whole chain. audit_retention_days is 0 by default, which keeps everything. Three things on it matter here. Every discovery search is recorded with its whole query; a download of a discovery export is recorded on the trail alone, so with the trail off a download is recorded nowhere; and an erasure is recorded with the address it erased wherever it deletes from recorded tables (aliases, list recipients, the archive index): those rows name the person until audit_retention_days ages them out. Alerts, the audit trail, syslog and the journal has the columns, reading and putting a change back, who may read it, and what it does not record. Regulatory scope, in one paragraph The project's own determination, reviewed 12 August 2026, is that the EU Cyber Resilience Act and the revised Product Liability Directive do not place a manufacturer's obligations on it, because it is supplied outside a commercial activity. The document calls itself a self-assessment and not legal advice, and lists the triggers that would re-open it. Under GDPR and UK GDPR the project is neither controller nor processor: the operator of an installation is. The export, the erasure and the log retention limits exist so that operators can meet their own obligations. What it does not do No PST export, and no text read from attachments but text/*. No silent hold: the custodian can see what a hold kept. No export past 4 GB and no download past 500 MB. No saved queries, review tags, redaction or de-duplication across custodians: a message sent to two custodians is two hits. No search of backups, and no case carried by a backup. Where to read more Wiki: Compliance and eDiscovery. Documents: Discovery.md, PersonalData.md, AlertsAndAuditTrail.md and RegulatoryScope.md. On this forum: The Control Deck, Journaling and dual delivery, and what a backup carries and what it leaves in Backup and restore.
  • 0 Votes
    1 Posts
    122 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) is a security release that bears on the send-as and sign-in sections here. An authenticated submission is now DKIM-signed as another domain hosted here only when its account may send as the address its From names (its own address, an alias pointing at it, a mailbox that granted it the post right, an address it proved, or a masked address of its own, the addresses the send-as table below lists); otherwise the mail is still accepted and sent, unsigned as that domain, so give such an account the alias or the grant before upgrading. A domain's or an account's rule replies only from an address its owner may send as. SMTP, POP3 and ManageSieve refuse a SASL PLAIN sign-in that asks to act as another account; with SRS on, mail to an SRS address this server handed out is held to the relay rules unless it is a bounce; the IMAP and POP3 listeners can take the client's address from a PROXY protocol header as the SMTP listener does, each from a list of peers of its own, both off by default; and the Control Panel's IP ranges page, which could show no list of ranges at all, is fixed. See the 6.3.7 announcement. hMailServer 6.3.8 (2 November 2026) is a security release that bears on the auto-ban, the lockout and the IP ranges here. The auto-ban saves the IP range it adds under a name of its own, the user name cut so that the name and its suffix fit in the 100 characters a range's name holds, so that it bans the address: up to 6.3.7 a second or later ban for a long user name could be refused (HM6099) and the address given a whole new set of attempts, and now an address's counted failures are cleared only once its range is saved. A sign-in's name is matched to its account as the per-name lockout counts it, so a mail client that signs in with another spelling of the account's address than the stored one, in more than the case of the letters A to Z, or with white space at either end, may no longer sign in, and each such try is counted as a failure: correct the name in the client. With the web services listener on, an address whose IP range allows none of SMTP, POP3 and IMAP, an auto-ban's or one of your own, is now refused there too, before the TLS handshake, loopback and the addresses in WebServicesTrustedProxies apart; such a range of your own closes to its addresses ACME's http-01 validation when this listener answers it, MTA-STS, CalDAV, CardDAV, Autodiscover, autoconfiguration and the Web Key Directory, so before upgrading check that the certificate authority, the sending MTAs and these services' clients do not come from one. See the 6.3.8 announcement. This topic covers who may use this server and how hard it is to abuse it. Know this first: only the IP ranges and auto-ban work out of the box; every other brake here is off until you set it. Until you do, nothing else stops a botnet spending one guess per address, or a stolen password sending thousands of messages. The secure defaults of a new installation, TLS for sign-in and compromised-account detection are in Hardening a public mail server. Where each one is set Since 6.3.4 the database is the settings store, and for a key it holds, a [Settings] line edited in hMailServer.ini changes nothing; on 6.3.3 these keys were lines of the file, applied at the next start. The store, and when a save takes effect, are in the settings topic. What Kept in Set with Takes effect IP ranges the database Control Panel Access & abuse protection → IP ranges; the Control Deck's IP ranges view; /api/v1/ipranges; hmctl iprange; COM Settings.SecurityRanges at once Auto-ban's four settings the database Control Panel Access & abuse protection → Auto-ban; the Deck's settings; PUT /api/v1/settings; COM at once Lockout, tarpits, rate limit, send-as check, hashing, policy the settings store, under their [Settings] names Control Panel; PUT /api/v1/settings/ini/{name}; hmctl ini set <name> <value>; COM Settings.SetIniSetting; on Linux hmailserver --set-setting NAME=VALUE when saved [SendingLimits], [SendingLimitsOverrides], PasswordPepper hMailServer.ini only the file, by hand or through the Control Panel (below) sending limits within seconds; the pepper after a restart The Control Panel and COM are Windows-only. Of these [Settings] keys the Deck edits only SmtpTarpitCount and SmtpTarpitDelaySeconds. A value set with hmailserver --set-setting while the service runs waits for the next save, reinitialise or restart. IP ranges: priority and the relay grid When a client connects, every range containing its address is found and the highest priority wins. The shipped ranges are Internet (priority 10, 0.0.0.0–255.255.255.255) and My computer (priority 30, 127.0.0.1); a range added on the Control Panel's page starts at 15, so a narrow range sits in front of Internet without editing it. At every RCPT TO two decisions are made, in order: may this combination be delivered at all (if not, 550 Delivery is not allowed to this address.), and must the client authenticate first (if so, 530 SMTP authentication is required.). "Local" and "remote" describe the addresses, not the client: an address is local when its domain is a hosted active domain or a route says so. Combination REST fields: allowed / auth required Internet My computer Local → local deliver_local_to_local / require_auth_local_to_local allowed, auth required allowed, no auth Local → remote deliver_local_to_remote / require_auth_local_to_remote allowed, auth required allowed, no auth Remote → local deliver_remote_to_local / require_auth_remote_to_local allowed, no auth allowed, no auth Remote → remote (relay) deliver_remote_to_remote / require_auth_remote_to_remote allowed, auth required allowed, auth required Anti-spam, anti-virus spam_protection, virus_protection on on Require TLS for auth require_tls_for_auth on in a new 6.3.4 installation; otherwise as the server had it off Remote → remote is the open-relay box. Both shipped ranges allow it with authentication required, even from 127.0.0.1, so a client that has not signed in gets 530; a range that does not allow it at all gets 550. The Control Panel's Server status page raises a Critical warning for any range that allows it without authentication. Remote → local is inbound mail and must need no authentication. Local → local needs it on Internet so that nobody outside can forge mail from one of your addresses to another. The protection switches reach further than their names. Clearing Enable anti-spam turns off every spam test, greylisting and the recipient tarpit for the range; clearing Enable anti-virus means its mail is never flagged for scanning. Since 6.3.4 SPF gives loopback no pass, so a content filter that re-injects mail should authenticate or use a range with anti-spam off. Require TLS for auth belongs to the range, not the port. With it set, AUTH is not advertised on a cleartext connection from that range, on SMTP, POP3, IMAP and ManageSieve. A device that cannot sign in gets a range of its own above Internet. For a scanner at 192.0.2.50 that sends from your domains to your own mailboxes only: hmctl iprange create --set name=scanner --set lower=192.0.2.50 --set upper=192.0.2.50 \ --set priority=20 --set allow_smtp=true --set allow_imap=false --set allow_pop3=false \ --set deliver_local_to_local=true --set require_auth_local_to_local=false \ --set deliver_local_to_remote=false --set deliver_remote_to_local=false \ --set deliver_remote_to_remote=false --set spam_protection=true --set virus_protection=true hmctl iprange update <id> and PUT /api/v1/ipranges/{id} (since 6.3.3) change any subset of the fields. The routes refuse a domain-restricted API key; a role needs manage_settings. The Control Panel's Restore defaults (SecurityRanges.SetDefault()) discards every range, your own and the auto-ban ones, and writes back the two shipped ones as the create scripts make them, without Require TLS for auth. Auto-ban, and why its ranges sit at priority 100 Auto-ban counts failed logons per connecting address; SMTP AUTH, POP3, IMAP, ManageSieve, the REST API and the web services listener's CardDAV, CalDAV and Autodiscover sign-ins feed one counter, but since 6.3.8 neither REST, for a failure from loopback (all of 127.0.0.0/8, and ::1), nor the web services listener, for one from loopback or an address in WebServicesTrustedProxies, counts it towards a ban of the address: an account's failed sign-in from there is counted against the name alone. Up to 6.3.7 an account's failed sign-in at REST from loopback was counted towards a ban of 127.0.0.1, which could then be refused at the SMTP, POP3, IMAP and ManageSieve listeners; on 6.3.2 to 6.3.7 AutoBanNeverBan with 127.0.0.1, ::1 avoids it. Behind a reverse proxy on the same machine (the Linux package binds REST to 127.0.0.1), the webmail's failed sign-ins are never auto-banned; account lockout, below, is what limits them there, and it is off until you set it. At the limit auto-ban clears that address's counted failures, creates an expiring range named Auto-ban: <user> at priority 100, and disconnects; SMTP says 535 Authentication failed. Too many invalid logon attempts. Priority 100, above both shipped ranges, is what makes a ban effective; AutoBanNeverBan, below, exempts an address. Setting (COM name) Default Trap AutoBanOnLogonFailure on — MaxInvalidLogonAttempts 3 0 disables auto-ban entirely, whatever the switch says MaxInvalidLogonAttemptsWithin 30 minutes Not a sliding window: how long a failure record survives, deleted by a pass every minute that runs only while auto-ban is on AutoBanMinutes 60 0 disconnects the client but creates no range, so nothing is banned Their REST keys include auto_ban_on_logon_failure and max_invalid_logon_attempts. The office profile (since 6.3.4) raises the limit to 5. AutoBanNeverBan (since 6.3.2): addresses and CIDR blocks whose failures are never counted; the per-connection disconnect still applies, and a bad entry is reported once as HM6524. Server status warns "Localhost is currently banned in the IP ranges." when a ban covers 127.0.0.1. Control Panel sign-in over COM is outside auto-ban and lockout, so guessing an administrator's mailbox name cannot lock them out of the tool. Spam traps and the multi-recipient null-sender check ban through the same ranges (Auto-ban: spam trap <address>, Auto-ban: multi-recipient null sender <address> see Allow and block lists and the anti-spam stack. Firewall bans (AutoBanFirewall, AutoBanCommand, fail2ban) are in Reading the logs. Releasing a banned address. Clear logon-failure list (POST /api/v1/settings/logon-failures/clear, COM Settings.ClearLogonFailureList()) clears counted failures only, for every address at once; a ban stays until its range expires. To release one now: Have the person correct the password in their client first: the counted failures were cleared when the ban was made, so a client still retrying the old one reaches the limit again as soon as the range is gone. Find the range: the IP ranges page, the Deck's IP ranges view, hmctl iprange list or GET /api/v1/ipranges. Its name begins Auto-ban: and its priority is 100. Delete it: on the page, hmctl iprange delete <id>, or DELETE /api/v1/ipranges/<id>. With AutoBanFirewall on, the firewall is reconciled on every range deletion, so its rule goes too. Five error-log codes mean the control has quietly stopped working: Code Meaning HM6114 A failed logon could not be recorded, so no ban will trigger while this persists HM6099 The auto-ban range could not be saved: the address has not been blocked HM6113 Spent failures could not be cleared, so the address may be banned again as soon as its ban expires HM6112 Expired IP ranges could not be removed, so an expired ban keeps blocking HM6115 Expired failures could not be cleared, so occasional failures may add up to a ban Account lockout, by name A distributed attack never crosses any one address's threshold. Lockout counts the name being guessed at, across SMTP AUTH, POP3, IMAP, ManageSieve, REST and the webmail's sign-in. It is off until set; the office profile sets 10. AccountLockoutThreshold=10 ; failed logons per username before a lockout. 0 = off AccountLockoutWindowMinutes=30 ; the window they must fall inside AccountLockoutMinutes=30 ; how long the lock lasts It counts names, not accounts, so a nonexistent name locks as a real one would, and reveals nothing. A locked name is refused before the password is checked, with the ordinary invalid-credentials reply; only its timing differs, deliberately. A lockout refusal is not charged to the connecting address, so an owner retrying from an office NAT does not get everyone behind it auto-banned. Passkeys and single sign-on are not counted, being no guesses. To lift one early: POST /api/v1/accounts/{address}/unlock, which a help desk scoped to the account's domain may call, COM Application.UnlockAccount, or Unlock on the Deck's Accounts row. A password reset clears it too. Tarpits and the submission rate limit All 0, off, until set; these values are a starting point. LogonTarpitSeconds=2 ; per failure on this connection, capped at 30 s. 0 = off SmtpTarpitCount=10 ; recipients an unauthenticated session may name before... SmtpTarpitDelaySeconds=5 ; ...each further RCPT TO waits this long, capped at 30 s MaxSubmissionsPerIPPerMinute=30 ; MAIL FROM commands per source IP per minute. 0 = off The logon tarpit (Auto-ban page) makes a failed logon's refusal wait LogonTarpitSeconds times the failures so far on that connection, on SMTP, POP3 and IMAP. A correct password never waits. The recipient tarpit (Anti-spam settings page; COM AntiSpam.TarpitCount and AntiSpam.TarpitDelay; REST tarpit_count and tarpit_delay on /api/v1/settings/antispam) spares authenticated sessions and ranges with anti-spam off. MaxSubmissionsPerIPPerMinute counts every MAIL FROM, authenticated or not, and refuses the excess with 421 Too many messages from your IP address, which contains a compromised account. The office profile sets 60; gateway keeps 0, since all inbound mail comes from the gateway's few addresses. Since 6.3.4 a save restarts its one-minute window. Behind a load balancer, give the SMTP listener SMTPProxyProtocolEnabled with SMTPProxyProtocolTrustedIPs (or XCLIENT), or auto-ban, the rate limit and the ranges all reason about the proxy's address: see Behind a gateway or reverse proxy. Per-account sending limits The brake on a compromised account, counted against the authenticated user name (never MAIL FROM) over a rolling period. The server level stays in the file: [SendingLimits] MaxMessagesPerAccountPerPeriod=500 MaxRecipientsPerAccountPerPeriod=2000 PeriodHours=24 ; clamped to 1-168 StateSaveIntervalSeconds=10 ; clamped to 1-3600 [SendingLimitsOverrides] [email protected]=5000:50000 [email protected]=0:0 Both limits default to 0, no limit; recipients are the stricter measure. An override replaces the global pair for that address, 0:0 exempts it, and an optional third field sets its period in hours; a malformed line is ignored with a log entry and the global limit applies. On Windows edit C:\Program Files\hMailServer\Bin\hMailServer.INI with an elevated editor, or use the Control Panel's Maintenance → Server limits & expert settings on the server itself; on Linux, sudoedit /etc/hmailserver/hMailServer.ini. They are not in the settings store, so no environment variable or setting profile writes them. What the sender sees: 452 4.7.0 Sending limit for this account has been reached. Please try again later. at MAIL FROM, or 452 4.5.3 Recipient sending limit for this account has been reached. Please try again later. at RCPT TO. The session stays open, the refusal spends no budget, and the application log, not the error log, says SendingLimit refused submission. Counters survive a restart in hmailserver_sendinglimits.dat in the data directory. It fails open: an unreadable file, a limiter exception or a full table of tracked accounts (error 5813) lets the message through. Since 6.3.4 there are three levels. A domain and an account may hold sending_max_messages and sending_max_recipients, which tighten the level above and never loosen it; an address's override line is its server level. Set them on the Levels tab of the domain and account dialogs, in the Deck's editors, with PUT /api/v1/accounts/{address}, or hmctl account set <address> sending_max_messages 100 (inherit returns to the level above); they apply at once. Also since 6.3.4 the webmail's send and the send API consume the same quota, answering 429; a scheduled draft is not counted when it goes. Who may send as whom SmtpAuthenticatedSenderCheck (Control Panel Connections & protocols → Protocols, SMTP tab) came in 6.2.25 and is off by default, since an installation may depend on what off allows: any account sending as anyone. The office and gateway profiles set it to 1. Turned on, it applies to every authenticated session, whose MAIL FROM must then be: Kind Given by The account's own address always An alias resolving to it, up to five hops the administrator A mailbox whose owner granted the post (p) right on their INBOX that owner, with any IMAP client's SETACL An address elsewhere proved by a code mailed to it (since 6.3.4) the person, in the webmail's Settings, Other accounts, Send mail as A masked address the account made, while it is on (since 6.3.4) the person, in the webmail's Settings, Masked addresses Anything else is refused 550 5.7.1 before the message is accepted. The webmail's From picker lists exactly these, so what it offers is what the server takes. The null sender is always allowed; unauthenticated sessions are unaffected. The post right counts only while IMAP ACL enforcement is on (enableimapacl, on by default). Before turning the check on, grant p to every delegate who sends as a shared mailbox. Mail sent as a proved outside address is not aligned with that address's domain, so a receiver honouring its DMARC policy may quarantine or refuse it. How passwords are stored In the Control Panel, Access & abuse protection → Authentication, card Password storage. Schemes: 0 none, 1 Blowfish, 2 MD5, 3 SHA-256, 4 PBKDF2-HMAC-SHA256 (the default), 5 Argon2id, 6 DPAPI (stored secrets only), 7 scrypt. PreferredHashAlgorithm=4 ; 3, 4, 5 or 7. Anything else is refused (HM5528) and read as 4 MinimumAcceptedHashAlgorithm=0 ; refuse a logon whose stored hash is weaker. 0 = off PasswordPepper= ; a server-wide secret mixed into Argon2id and scrypt hashes PasswordHashIterations=0 ; PBKDF2. 0 = 210,000. Range 10,000 to 10,000,000 PasswordHashMemoryKB=0 ; Argon2id. 0 = 19,456 KiB. Range 4,096 to 1,048,576 PasswordHashTimeCost=0 ; Argon2id passes. 0 = 2. Range 1 to 20 Hashes upgrade at a successful logon, only upward. Raising the scheme or a work factor re-derives each hash when its owner next signs in; lowering either affects new hashes only. MinimumAcceptedHashAlgorithm set too high is a lockout. The refusal follows a correct comparison and the upgrade needs a successful logon, so an account below the floor never climbs out: its password must be reset. Raise the preference first, let accounts sign in, then set the floor. Argon2id and scrypt are peers, so 5 accepts either. A work factor out of range is reported as HM5561 and read as 0; HM5607 means Argon2id was asked for on a build whose OpenSSL has none, and another scheme is used. Argon2id allocates PasswordHashMemoryKB on a connection thread at every logon. PasswordPepper must be set before accounts exist: changing it invalidates every Argon2id and scrypt password. PBKDF2 is not peppered, so it stays usable for SCRAM. Since 6.3.4 it lives in hMailServer.ini only and no backup carries it, so keep a copy of the value with the backups: a database or archive restored onto a machine whose file has no pepper, or a different one, refuses every Argon2id and scrypt password, and a warm standby needs the same value in its own file. Where the service cannot write the file (a new Windows installation running as NT SERVICE\hMailServer, or a Linux package installation), COM and REST refuse it: set it with an elevated editor (sudoedit on Linux) and restart the service. The password policy All off until set, applied where a password is chosen, never at logon; on Windows under Access & abuse protection → Administrative access, Password tab. PasswordPolicyMinimumLength=12 PasswordPolicyRequireMixedCase=1 PasswordPolicyRequireDigit=1 PasswordPolicyRequireNonAlphanumeric=1 PasswordPolicyRejectCommon=1 ; refuse passwords from a built-in common list PasswordPolicyHistoryCount=5 ; refuse the last N passwords PasswordPolicyMaximumAgeDays=0 ; 0 = passwords never expire The office profile sets a minimum of 10 and the common list. Expiry needs care: IMAP, POP3 and SMTP cannot change a password and /portal refuses an expired one, so an administrator renews it, or its owner through an app password, which still signs in to /portal. Active Directory accounts are exempt. The breach check is in Hardening a public mail server; the rest of sign-in is in Signing in. What it does not do None of it spam-tests your own users: an authenticated session never is. A setting profile never writes an IP range. Where to read more Wiki: Security Hardening sections 17.1, 17.2, 17.5, 17.7 and 17.9; Settings Reference sections 9.1, 9.9 and 10; Stopping Spam section 10.1; Troubleshooting sections 19.3 and 19.7. Documents: SettingsApply.md, SettingLevels.md, SendAs.md, Profiles.md, Roles.md, ServiceAccount.md. Public documentation: SecurityRange and SecurityRanges, for the COM names of the range flags. On this forum: Being trusted by the servers you send to, The Control Deck, Upgrading to 6.3.4, hMailServer on Linux.
  • 0 Votes
    1 Posts
    156 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) is a security release that closes gaps in what this guide describes. Over COM an account given serveradmin with a list of domains was the server's administrator, its list aside, and one given domainadmin with a list that left out its own domain administered that domain's ordinary accounts; both are now confined as over REST, so before upgrading give serveradmin with no list to any account that is to administer the whole server over COM, a script's or a Control Panel's account among them. A credential confined to named domains is refused a domain's rename, a domain alias, a custom role's creation, change or deletion, a rule action that reaches the server's own addresses, a route's stored credentials or the event script, a relay host that is not public, and the server-wide metric history, over REST and COM alike; and a role is given or taken away only by a credential that takes in what the account holds now and afterwards, so manage_roles in named domains can no longer take a role from an account whose admin level is the server administrator's. A state-changing REST request that a browser reports as cross-site is refused. In the Control Panel, API & monitoring is Monitoring & alerts and REST API keys is under Sign-in & access; Ctrl+K takes every old title to its page. See the 6.3.7 announcement. This guide puts administrator roles in one place. Roles are new since 6.3.4 (schema 6047). The one thing to know first: an upgrade changes nobody's authority. The upgrade writes no assignment, and an account with no assignment holds exactly what its admin level has always given it. Nothing narrows until you give somebody a role. Permissions, roles and assignments A permission is one thing a credential may do, a role is a named set of permissions, and an assignment gives one account one role in named domains. An account holds at most one role. Role For Reaches when no domain is named The same as serveradmin Everything, including the secrets no other role reads Every domain The administrator password, admin level Server, an API key with Scope=full and no role domainadmin The accounts, aliases, lists and mailboxes of a domain The account's own domain Admin level Domain helpdesk Finding an account, resetting its password, unlocking it, and reading where its mail went, and nothing else The account's own domain Nothing before 6.3.4 auditor Reading everything the administration exposes, the audit trail included, and changing nothing Every domain An API key with Scope=readonly and no role The built-in roles cannot be edited or deleted. Their permissions are in the server's code, not the hm_roles table, so a change to the table cannot hand anybody a permission. The 27 permissions and who holds them A permission marked server-wide is reached only by a credential with no list of domains. The queue, the quarantine, the archive, the logs and the settings carry every domain's mail, so a role confined to named domains is refused them whatever it holds. Permission What it reaches serveradmin domainadmin helpdesk auditor administer_server Everything, secrets such as password hashes and private keys included; COM's "is this the server administrator?" yes administer_domain COM's "is this a domain administrator?" yes yes view_configuration Status, settings, listeners, certificates, rules, routes, anti-spam lists, the roles and who holds them, alert rules and events, bounce and complaint readings; never a secret yes yes yes view_accounts Domains, accounts, aliases, lists, groups (server-wide), fetch accounts yes yes yes yes view_mailbox Another account's folders and messages yes yes yes view_queue The delivery queue (server-wide) yes yes view_quarantine The quarantine (server-wide) yes yes yes view_archive The archive (server-wide) yes yes view_logs The logs (server-wide) yes yes view_message_trace The message trace; with view_configuration, the reports and a domain's suppression list yes yes yes yes view_audit The audit trail (server-wide) yes yes manage_accounts Creating, changing and deleting accounts, aliases, lists, groups, fetch accounts and app passwords; a domain's spam traps, DKIM rotation and suppression list; moving a mailbox (needs both domains) yes yes reset_password POST /api/v1/accounts/{address}/password, Application.ResetPassword yes yes yes unlock_account POST /api/v1/accounts/{address}/unlock, Application.UnlockAccount, Account.UnlockMailbox; clearing the logon-failure list (server-wide) yes yes yes manage_mailbox Another account's folders, messages and folder rights, a support session, a PST migration job (with manage_accounts) yes yes manage_queue Retrying or deleting a queued message (server-wide) yes manage_quarantine Releasing or deleting held mail (server-wide) yes yes manage_archive Holding and releasing archived messages (server-wide) yes manage_settings Every setting, listener, certificate, route, rule and anti-spam list, the alert rules (server-wide); the webmail's branding and file-link policy, a confined credential only its own domains' yes manage_domains Which domains exist (server-wide) yes manage_server Reinitialise, back up, reload scripts, clear caches, install an update, run a pass now (server-wide) yes manage_roles Roles and who holds them (server-wide) yes send_through_api Sending through POST /api/v1/send; only an API key reaches it yes provision SCIM 2.0 at /scim/v2, reads and writes; only an API key reaches it yes view_discovery eDiscovery's cases, searches, holds and exports yes yes manage_discovery Opening and closing a case, searching, legal holds, exports yes manage_inbound_webhooks Routing an address or a domain to a URL, as much as reading every message the route takes yes Four rules sit beside the table: The auditor reads mail. It holds view_mailbox; the help desk does not. The reports need two permissions. GET /api/v1/reports needs view_configuration and view_message_trace, because it says who mails whom. domainadmin and auditor read them; helpdesk is refused, and the Deck leaves the Reports view out for any role without both. The trace follows the role's reach. A confined help desk traces one whole address in a domain it reaches and sees only events naming that address exactly: [email protected] does not also show [email protected]. Clearing the logon-failure list is server-wide. hm_logon_failures records only the address a failure came from, with no account or domain, so a confined help desk is refused POST /api/v1/settings/logon-failures/clear, and Settings.ClearLogonFailureList answers the server administrator alone. The help desk unlocks one account with POST /api/v1/accounts/{address}/unlock instead; a password reset clears the lockout too. A ban already placed is an IP range under /api/v1/ipranges, which needs manage_settings. What only serveradmin has Reading a secret. No view_ permission covers a password hash or a private key: an auditor that could read one would be an offline attack away from being every role. Handing out authority. No custom role may hold administer_server, administer_domain or manage_roles, and a credential may never give a role holding a permission, or reaching a domain, that it does not hold itself. Taking over a wider account. A credential may not reset the password of, or change, an account whose whole authority it does not take in. A help desk is refused a domain administrator's or an auditor's password, even in its own domain, because otherwise it could sign in as them; a domain administrator is refused an auditor's. Where the server administrator's own mailbox is in a domain somebody else administers, that domain's administrator is refused all of these on it, over REST and COM. Over COM this is asked where the write is made, not where the object was reached: Account.Save, Account.Delete, Account.MoveTo, Application.ResetPassword, the app passwords and every write to what is in an account (FetchAccount.Save, Rule.Save, IMAPFolder.Save, Message.Save and the rest) ask that the credential reach the account and take in its authority. An event script's objects carry no credential and are left as they were. Roles.md lists every route and COM member that asks this. Which domains a role reaches Credential No domains named * A list An assignment of serveradmin or auditor Every domain Every domain Only those An assignment of any other role, custom ones included The account's own domain Every domain Only those An API key's Domains= line, whatever its role Every domain Matches no domain Only those A custom role holding a server-wide permission needs * before an account can use it at all. The listings answer every_domain over REST, and a fourth field over COM. A credential confined to named domains sees only the assignments of accounts in those domains. Giving a role, surface by surface Control Panel (Windows only): Accounts & domains → Administrator roles lists the roles and who holds them. Type an address, choose a role, optionally name the domains, and press Give the role; select a row and press Take the role away. It applies at once. Control Deck (Windows and Linux): the Roles view does the same. REST: giving or taking away a role needs manage_roles, which only serveradmin holds and which is server-wide: the administrator password, or a serveradmin credential reaching every domain. PUT /api/v1/role-assignments/[email protected] {"role":"helpdesk","domains":"example.test,example.org"} DELETE /api/v1/role-assignments/[email protected] GET /api/v1/role-assignments GET /api/v1/roles COM (Windows only): an empty role name takes the role away, and Account.RoleName, RoleDomains and RolePermissions read it back: $app = New-Object -ComObject 'hMailServer.Application' $account = $app.Authenticate('Administrator', 'your-admin-password') if (-not $account) { throw 'Authentication failed.' } $app.AssignRole('[email protected]', 'helpdesk', 'example.test') $helen = $app.Domains.ItemByName('example.test').Accounts.ItemByAddress('[email protected]') $helen.RoleName; $helen.RoleDomains; $helen.RolePermissions With a second factor on the administrator, Authenticate refuses and the script uses AuthenticateWithCode. Application.CurrentRole and CurrentPermissions say what a session holds. An API key: POST /api/v1/apikeys with "role":"helpdesk" writes a Role= line to hMailServerApiKeys.ini. The key store answers to the administrator password alone. The Deck's API keys view, signed in with that password, also makes a key with a role and its domains, showing the token once. curl -u Administrator:your-admin-password \ -H "Content-Type: application/json" \ -d '{"label":"help desk tool","role":"helpdesk","domains":"example.test"}' \ https://<host>:8045/api/v1/apikeys Use the address and port your listener answers on; bound to loopback with no certificate, it is http://127.0.0.1:8045 (https:// once a certificate, or an ACME one, is in place). A key's role decides what it reaches. The Control Panel's REST API keys page, as its reference describes it, names no role; give a key its role over REST or in the Deck. No API key signs in to the Deck. Custom roles are made over REST from a name, a description and a comma-separated permission list; both editors list them and give them out. A role that releases held mail holds server-wide permissions, so it is given with * and reaches every domain's held mail: POST /api/v1/roles {"name":"quarantine-desk","description":"releases held mail","permissions":"view_quarantine,manage_quarantine"} PUT /api/v1/role-assignments/[email protected] {"role":"quarantine-desk","domains":"*"} A permission the server does not know is dropped, not refused, so a custom role ends with less authority than it asked for, never more. A custom role somebody holds cannot be deleted: DELETE /api/v1/roles/{name} answers 409 saying how many accounts hold it, and the Deck's Delete shows that sentence. Give each holder another role or take this one away first; assignments are never removed for you, because an account with none falls back to its admin level. A built-in role answers 400, and COM cannot delete a role. An assignment decides even when its role holds nothing: holders of a custom role whose permissions name nothing this version knows are ordinary accounts, not their admin level. Where each role works Role Control Deck REST API Control Panel (COM) serveradmin Every view Every route Every page domainadmin Dashboard, DANE / TLSA, domains, fetch accounts, lists, roles and reports for its domains Its domains' accounts, aliases, lists and mailboxes, the status, who holds a role there Domains and Administrator roles helpdesk Domains, fetch accounts and lists, with Reset password and Unlock on every account The reset, the unlock, the account listings, the trace of an address in its domains Welcome and About only; Application.ResetPassword, UnlockAccount and GlobalObjects.MessageTrace answer a script auditor Every view that reads, the audit trail and reports included Every read, the audit trail and alerts included; every write refused Welcome, About and Administrator roles The help desk and the auditor work in the Deck. COM's settings objects answer only the server administrator, so an auditor reads the configuration through the Deck or REST. A domain administrator is not shown the settings, rules, routes, remote domains, certificates, ports, IP ranges, groups, backup, server messages, queue, logs or audit trail, which are server-wide. The quarantine is server-wide as well, so a domain administrator confined to its domain is refused it. Hiding a page is a courtesy, not the boundary. The server refuses the request whatever a page draws, and a Control Panel connected to a server that predates roles shows every page. A role holder signs in with its own address and password, and its second factor where it has one. An app password never carries the role: over REST it is an ordinary account's request, and COM's Application.Authenticate and AuthenticateWithCode treat it as a wrong password. A support session into a mailbox does not carry the person's role either. A role holder's own mail and settings under /api/v1/me stay its own. Setting up a help desk Turn the REST listener on if it is off. It needs RestApiPort set, RestApiBindAddress set to an address staff can reach (the default, 127.0.0.1, serves this machine alone), a certificate unless it is bound to loopback, and a name staff can reach. On Windows set them on the Control Panel's API & monitoring page (Monitoring & alerts since 6.3.7) and press Apply now, since they wait for a reinitialise. On Linux run sudo hmailserver --set-setting RestApiPort=8045 and the same for RestApiBindAddress and, for any address but loopback, RestApiCertificateFile and RestApiPrivateKeyFile (or let an ACME certificate stand in), then sudo systemctl restart hmailserver. The Control Deck topic has the details. Give each person a mailbox of their own; the role goes to that account. Decide the domains. With none named, helpdesk reaches only the account's own domain; name several, comma separated, for a desk serving more. Give the role on any surface above and check it with GET /api/v1/role-assignments or the Roles view. The person signs in to the Deck at https://<host>:8045/ as themselves, then uses Reset password or Unlock on an account. Tell them the limits: no mail reading, no reports, no domain administrator's or auditor's password, no removing a ban, and, when confined, no clearing of the logon-failure list. Each change the help desk makes is on the audit trail as its own, over COM and REST, with actor type account and a detail ending [role: helpdesk]. What a refusal says A permission the role lacks: 403 naming both, for example the role 'helpdesk' does not hold the permission 'manage_settings'. A domain it does not reach: the role 'helpdesk' does not reach that domain, whether or not the account exists, so nobody can list another domain's mailboxes one request at a time. Over COM, Application.ResetPassword and UnlockAccount answer No such account. for both. An account wider than the credential: that account holds authority this credential does not. Over COM, since 6.3.4 a refusal carries the server's sentence rather than a bare Exception from HRESULT: 0x800403E9. An API key on the key store, the [Settings] routes, the audit trail or the alerts: 401 with {"error":"authentication failed"}, the same as "not a key". The two stores only the administrator password opens The key store (/api/v1/apikeys) and the [Settings] section of hMailServer.ini (/api/v1/settings/ini) answer to the administrator password and nothing else: not an account holding serveradmin, not a key of any role or scope. A key that could mint keys could issue itself an unrestricted replacement, and the file holds the server's secrets. An account is refused 403 with the reason, a key 401. The audit trail (/api/v1/audit) and the alerts (/api/v1/alerts/...) are refused to every API key with the same 401, so a stolen key cannot read what it did or silence the alerting. An account whose role reaches every domain reaches them through view_audit, view_configuration, manage_settings or manage_server; a confined role is refused them. Upgrades, the audit trail and backups The upgrade. Schema step 6047 adds hm_roles, seeded with the four built-in rows, and hm_roleassignments, and writes no assignment. Roles are data, not settings: nothing goes into hm_settings or hMailServer.ini. The audit trail. Every role created, changed or deleted, and every role given or taken away, is a row naming the credential that did it; an assignment's row reads like role: helpdesk -> auditor; domains: the account's own domain -> every domain. The actor type follows the role: the administrator password and holders of serveradmin or domainadmin are administrator, every other account is account. Backup and restore. An account's assignment travels with it, by role name, and the custom roles travel whenever the domains or the settings do, restored first. A role the restoring server already has is kept as it is, with any difference named in the backup log. The admin level waits until the assignment is written, so a restore that stops part-way leaves no account wider than its role. An assignment naming a role neither side has is dropped, the level stays withheld, and HM6129 names the account. No archive carries the API key store. What it does not do No mapping from an identity provider. There is no claims mapping; an identity provider saying somebody is an administrator is not obeyed. The server's roles and oidc_administrators or saml_administrators decide. No role on an app password or a support session. No Control Panel or COM on Linux: roles are given there in the Deck and over REST. Where to read more Roles.md, the authority on all of this. Sign-in and Identity §7, The Control Deck, The REST API §2 and The COM API and Scripting §23.2. AlertsAndAuditTrail.md, Backup.md and Reports.md. On this forum: The Control Deck: browser administration at /, The REST API: turning it on, credentials and roles, The COM API on Windows, Hardening a public mail server, Alerts and the audit trail and IP ranges, auto-ban and lockout.
  • 0 Votes
    1 Posts
    118 Views
    ProgressiverobotP
    This topic covers how much mail an hMailServer 6.3.4 server keeps, and for how long: each mailbox's limit and the quota policy 6.3.4 adds around it; retention, the administrator's and each person's, with the recover-deleted window; and the server archive under ArchiveDir. The one thing to know first: out of the box nothing is deleted because of its age. A new account has no limit (0), no domain has a retention policy, the archive is off, and once it is on, ArchiveRetentionDays is 0, keep for ever. The limit, and what a full mailbox does Where it is set. Account.MaxSize over COM, max_size_mb over REST, in megabytes; 0, the default, is no limit. Set it on the Control Panel's account dialog (Quota (MB), Windows only), in the Control Deck's account editor, with PUT /api/v1/accounts/{address}, in the max_size_mb column of an accounts CSV file (since 6.3.4), or with Set-HmAccount. A domain's Limits tab caps the whole domain (Maximum domain size (MB), 0 for none; once reached, delivery to its mailboxes fails) and sets the quota new accounts get. What counts is each message's original size: compression does not lower it, and a mailbox encrypted at rest keeps its sizes in the clear. The kept copy of a message the person sent to expire counts against their quota (413, quota_exceeded). Where At the limit SMTP RCPT TO, with RejectFullMailboxAtRcpt on (1, the default) A mailbox already at or over its limit, and past any grace (below), is answered 452 4.2.2 Mailbox is full., a temporary refusal: the sending server keeps the message and retries A message that arrives while there is room and would overflow it Caught at delivery, with a non-delivery report to the envelope sender RejectFullMailboxAtRcpt at 0 Accepted, then bounced to an envelope sender that spam has usually forged. Leave it on IMAP APPEND, COPY, MOVE Refused with [OVERQUOTA], so a client can tell a full mailbox from any other refusal APPEND into a folder shared from another mailbox Since 6.3.4 charged to the folder's owner, as COPY and MOVE were A migration or PST import job Pauses with the account has no room left under its quota and tries again; raise the limit or make room A full disk is a different refusal. Below MinimumFreeDiskSpaceMB (100) free on the store's volume, MAIL FROM gets 452 4.3.1 Insufficient system storage. and IMAP APPEND NO [UNAVAILABLE], for every mailbox. The log warns first, below DiskSpaceWarningThresholdMB (1024). What the person sees. The webmail's Storage page (since 6.3.2, GET /api/v1/me/storage the share used, every folder by size, the twenty largest messages, and a clean-up of Trash and Junk older than 7, 30 or 90 days, or everything. See The webmail portal at /portal. GET /api/v1/me answers quota with limit_mb and used_bytes. IMAP QUOTA: QUOTA=RES-STORAGE is advertised; GETQUOTA and GETQUOTAROOT answer one unnamed root for the whole account with STORAGE in kilobytes, and an empty list for an account with no limit. SETQUOTA is refused: the limit is the administrator's. It is on as seeded; the switch is imap_quota_enabled on PUT /api/v1/settings (IMAPQuotaEnabled over COM). The quota policy (since 6.3.4) The server-wide warning at QuotaWarningPercent (90) has been there since 6.2.22-pre6. Schema 6061 adds a domain's own warning share and words, a grace and a send-stop, each a share of the limit; schema 6088 lets an account hold its own shares too. Point Server Domain (-1 takes the server's) What it does Warning QuotaWarningPercent, 90; 0 warns nobody quota_warning_percent The delivery that takes the mailbox across this share sends its owner one notice, from the domain's postmaster Grace quota_grace_percent, 0 quota_grace_percent How far past the limit mail from other people is still accepted Send-stop quota_send_stop_percent, 0 (never) quota_send_stop_percent The share at which the account can no longer send Over COM (Windows): Settings.QuotaGracePercent, Settings.QuotaSendStopPercent, and Domain properties of the same names. The warning is sent once, by the delivery that crosses the threshold; nothing records who was warned, so a mailbox emptied and filled again is warned again. The grace is for mail other people send, the mail otherwise lost. A 1 GB mailbox with a 50% grace receives until it holds 1.5 GB; the owner's own saves and copies over IMAP and the webmail stop at the limit. The send-stop refuses the account, not the address. An authenticated MAIL FROM is refused with 552 5.2.2 and the reason, keyed on the account that signed in, even under a domain alias, not the address it named. Receiving goes on. QuotaPolicy.md describes the refusal at SMTP only; test the webmail's send and the send API before relying on it there. The words are the QUOTA_WARNING_SUBJECT and QUOTA_WARNING server messages, with %MACRO_MAILBOX%, %MACRO_QUOTA% (MB), %MACRO_USED% (MB) and %MACRO_PERCENT%. Server messages are server-wide (the Control Panel's Mail flow & delivery → Server messages, the Deck's Server messages page, PUT /api/v1/settings/messages/{name}). A domain sets its own subject and text, quota_warning_subject and quota_warning_text (Domain.QuotaWarningSubject, Domain.QuotaWarningText), so its people can be warned in their own language. They are chosen apart: a domain that sets only the text keeps the server's subject. Levels. The warning is a preference: the deepest level holding a value wins. The grace and the send-stop are limits: an account's own value may tighten its domain's, never loosen it, while a domain's may sit either side of the server's. The domain's shares are the domain administrator's, and a domain-restricted key's. <name>_effective and <name>_source on the domain and account routes say why a mailbox was not warned. An account's own quota shares are tested only through what the routes answer, and only on Windows. The mechanism is in Settings: what is not stock, when a save takes effect, and the three levels. Where it is set. Control Deck: the domain editor's Quota policy group; the server's grace and send-stop on the Settings page. Control Panel (Windows): the domain dialog's Limits tab; the server's grace and send-stop under SMTP, beside QuotaWarningPercent. REST: the five domain keys on PUT /api/v1/domains/{name}; the server's two on PUT /api/v1/settings. An account's own value: the Levels tab of the Control Panel's account dialog, the Deck account editor's levels card, hmctl account set, or PUT /api/v1/accounts/{address}. QuotaWarningPercent is a key of the settings store: set it in the Control Panel (Windows) on Connections & protocols → Protocols, SMTP tab, with PUT /api/v1/settings/ini/{name} or hmctl ini set, which answer to the administrator password alone (never an API key, nor an account holding a role), or, on the Linux server itself, hmailserver --set-setting, not by editing hMailServer.ini. Every value here applies when it is saved through the server, at every level; when a save takes effect explains why a line edited in the file does not hold and when a value stored by --set-setting arrives. A backup taken before the policy restores with the server's values. One domain warned at 85%, stopped from sending at the limit and given a 20% grace, with a German subject; then one account put back on its domain's grace: hmctl domain set example.com quota_warning_percent 85 hmctl domain set example.com quota_send_stop_percent 100 hmctl domain set example.com quota_grace_percent 20 hmctl domain update example.com --set "quota_warning_subject=Ihr Postfach ist fast voll" hmctl account set [email protected] quota_grace_percent inherit Who is nearest their limit GET /api/v1/reports/quota lists every mailbox with a limit and at least one message, nearest first: megabytes, quota_megabytes, percent, the warns_at and sending_stops_at points that apply to it, and a state of ok, warned, stopped or over. It takes domain, top (1 to 200, default 10) and format=csv, and ignores the date window. Its test has run on the Windows bench only. The Deck's Reports page shows it as Near their quota. Other than the administrator password, a credential needs view_configuration and view_message_trace. The sections hmctl report documents do not include it: curl -u Administrator:your-admin-password \ "http://127.0.0.1:8443/api/v1/reports/quota?domain=example.com&top=200&format=csv" Nothing records what a mailbox held yesterday; per-domain storage by day is the storage section, in Alerts, the audit trail, syslog and the journal, and per-domain reports. Retention: the administrator's policy MessageRetentionDays (since 6.2.25) is set per domain and per account; there is no server value. On a domain, 0 (the default) is no policy and a number deletes mail older than that many days. On an account, 0 (the default) takes the domain's, a number is its own, and -1 keeps for ever whatever the domain says. The sweep. MailboxRetentionTask runs at start-up and every six hours. It removes delivered messages whose stored creation time is older than the window, in every folder of the mailbox, through the same deletion an IMAP EXPUNGE performs, so the quota, the QRESYNC record and an open IMAP session all see it. Utilities.RunMessageRetention (COM, Windows only) runs it now and returns the count; no document names a REST route or hmctl verb for it, so on Linux wait for the next pass. Where it is set. On Windows the Control Panel's domain dialog (Limits tab) and account dialog (General tab); the Deck's domain and account editors; message_retention_days on the domain and account routes; hmctl domain set and hmctl account set. A domain's retention is the server administrator's (COM refuses a domain administrator, the domain route a restricted key); an account's own value is the domain administrator's, so an account's -1 can switch its domain's policy off for that account. Before switching a domain's policy on: Give -1 to each account that must keep everything. Decide whether what the sweep removes should be recoverable for a while (recover_deleted_days, below). Off, a removed message is gone. Set the domain's days. It applies when saved; the next pass, at most six hours away, deletes. Easy to miss. A restore of the built-in backup does not carry a domain's retention days: the wiki's Backup and Restore page lists them as silently reset to 0, so re-enter them. And a legal hold outranks retention: a held account is left alone by the policy and by the person's own choices, and the log says so. Each person's own retention (since 6.3.4) Three choices on the webmail's preferences page: Empty Trash after, Empty Junk after and Archive Inbox mail after, each never, 7, 30 or 90 days, or a year, kept as retention_trash_days, retention_junk_days and retention_archive_days. The same six-hourly sweep runs them after the administrator's policy. Trash and Junk (the folders designated \Trash and \Junk, or named as clients name them) are emptied of what is older; Inbox mail older than the choice is copied to the \Archive folder, made as a top-level Archive if there is none, then removed from the Inbox. That is a folder of the person's mailbox, not the server archive. A value that is not a whole number of days from 1 to 3650 is off. The recover-deleted window (since 6.3.4) recover_deleted_days on PUT /api/v1/settings or the Deck's Settings view (0 to 365; 0, off, by default) keeps a copy of what a person deletes, and of what either retention removes, for the person to put back from the webmail's Recover deleted items. A domain or an account may keep copies for fewer days than the level above, never more. The copies, under <data directory>/Recoverable/<account id>/, count against no quota and are not in the backup, so size the window with the disk in mind; the six-hourly task ends those past the window, and all of them at 0. The person's side is in The webmail in depth: search, offline mail, expiring messages, send-as and recovery. The server archive With ArchiveDir set, the server writes a copy of every message received by SMTP into a tree of its own, as received and in the clear: encryption at rest does not cover it, and the mailbox retention policy does not reach it. The layout, as the Settings Reference writes it (Windows separators, names lower-cased): Message Where the copy goes From a local sender ArchiveDir\<senderdomain>\<sendername>\Sent-<file> From anybody else ArchiveDir\Inbound\<file> For each local recipient ArchiveDir\<recipientdomain>\<recipientname>\<file>, made from the first copy when there is one A bounce or report with a null sender The archive's Error folder, with no index row The archive's settings: Key Default What it does ArchiveDir empty (off) The folder the copies go to ArchiveDomains empty (every message) Comma-separated: a message is archived only when its local sender or a recipient is in a listed domain, and only the listed domains' copies are made ArchiveHardLinks 0 Hard-link instead of copy: a promise that nothing outside the server edits a message file in place ArchiveRetentionDays 0 (keep for ever) Deletes archived copies older than this many days Its sweep runs at start-up and every twelve hours. It removes only .eml files, follows no symbolic link or junction, is depth-capped, and asks the index first: a held copy stays however old. The index (since 6.2.25, schema 6029): a row per copy in hm_archiveindex, with who, when, what, where and whether it is held; copies made before schema 6029 have none. Search it in the Deck's Archive view (since 6.3.4), with GET /api/v1/archive (domain, mailbox, sender, recipient, subject, since, until, hold=1, limit 1 to 1000, default 200, newest first), or with Utilities.SearchArchive over COM (Windows). A role needs view_archive; a key restricted to named domains must name one of its own, and the Inbound copies are the administrator's alone. Searching the words of archived mail is an eDiscovery case's work. A hold on one copy, POST /api/v1/archive/{id}/hold or Utilities.SetArchiveHold, keeps its file and row from the sweep and from an address's erasure; DELETE lifts it. Holds on whole accounts and domains are eDiscovery's: see Compliance: eDiscovery, legal holds, personal data, erasure and the audit trail. curl -u Administrator:your-admin-password \ "http://127.0.0.1:8443/api/v1/archive?domain=example.com&[email protected]&since=2026-09-01&limit=50" curl -u Administrator:your-admin-password -X POST http://127.0.0.1:8443/api/v1/archive/4471/hold Where it is set. On Windows, the Control Panel's Maintenance → Advanced, Copies of mail tab (ArchiveDir, ArchiveHardLinks, ArchiveDomains), and ArchiveRetentionDays on the SMTP tab of Connections & protocols → Protocols. The Deck has no page for these keys: use PUT /api/v1/settings/ini/{name} or hmctl ini set <name> <value> (the administrator password only), which take effect when saved, or hmailserver --set-setting NAME=VALUE (with sudo) on the Linux server itself, which, while the service runs, waits for the next save, a reinitialise (POST /api/v1/server/reinitialize) or a restart. Setting one up: Choose a folder outside the data directory: a full restore with the messages option replaces that directory, and an archive inside it too, held copies included. On Windows, a new installation runs the service as NT SERVICE\hMailServer, which writes only where it is granted: approve the folder by running hMailServer.exe /GrantServiceAccess with --approve-modify "<folder>" from an elevated Command Prompt in the installation's Bin folder. It must be a full path on a fixed local drive, outside the program folder, Program Files, the Windows folder and the profiles folder: never a share, a drive's root, or a path through a junction or link. On Linux the service runs as User=hmailserver, and the packaged unit lets it write only under /var/lib/hmailserver, the data directory itself, and /var/log/hmailserver. Every other folder is read-only to it (/home is not even visible), and an ArchiveDir it cannot write stops archiving. Make the folder writable by the hmailserver user, add it to the unit with a drop-in (sudo systemctl edit hmailserver, then under [Service] a ReadWritePaths= line naming the folder) and restart the service. SecureDefaults.md gives this drop-in for a data folder; no document gives it for the archive. Set ArchiveDomains first if only some domains are archived, then ArchiveDir. Send a test message and find its row in the Deck's Archive view. Before relying on it. The backup does not carry it. The backup copies the data directory, and hm_archiveindex is not carried: back up the archive folder, and the database with its own tool for the index. Moving it strands the index. hm_archiveindex.archivepath is matched as an exact string by the sweep, holds and erasure; do not rewrite it in bulk. If the folder moves, move it back or accept that older rows point at the old place. A mailbox move leaves the archive under the old address, and an address's erasure with the archive included leaves the copies in other people's trees and in Inbound until ArchiveRetentionDays ages them out. It is not journaling, which sends a copy elsewhere and keeps nothing here: Journaling and dual delivery. SMTP, as documented. No 6.3.4 document says whether mail the server queues itself, such as the webmail's send, or mail a fetch account collects, is archived. What it does not do No message-count quota: storage is the only quota enforced. No server-wide retention policy, and no documented way to run the retention sweep on demand on Linux. No write-once archive: a hold is a flag an administrator can lift. The archive only grows while ArchiveRetentionDays is 0, and the recover-deleted store is bounded by its window alone: watch the disk they are on. Where to read more Documents at v6.3.4: QuotaPolicy.md, SettingLevels.md, Reports.md, RecoverDeleted.md, Discovery.md, Backup.md, ServiceAccount.md, RelocatingAnInstallation.md, PersonalData.md, Journaling.md, SecureDefaults.md, SettingsApply.md. Wiki: Settings Reference (section 9.13), Your First Domain and Mailbox, Compliance and eDiscovery, Backup and Restore, APIs Reference. The documentation page: Storage, retention and archive, Queue, quarantine and archive. On this forum: Backup and restore, for what a restore resets; Relocating an installation, and moving or renaming a mailbox, for the archive index; the service account on Windows and the Linux unit, for the folders the service may write; The Control Deck.
  • 0 Votes
    1 Posts
    137 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.8 (2 November 2026), a security release, reads on Windows the CA file MessageStoreObjectCaFile names from the file configured, or refuses its name: a name it cannot hand to OpenSSL as it is named, as a path holding characters outside ASCII may be, is refused with HM7536 in the error log, and then no request to the object store is allowed, so give the file, and the folders above it, an ASCII name before upgrading. See the 6.3.8 announcement. This topic covers the three things 6.3.4 can do to a stored message that 6.3.3 could not: store it compressed with its large attachments held once, keep it in an S3-compatible or Azure Blob store instead of under the data directory, and store it encrypted to a key of the mailbox's own. All three are new in 6.3.4, and all three are off until an administrator turns them on: a server that turns none of them on stores every message exactly as 6.3.3 did. The one thing to know first: the delivery queue is never compressed, encrypted or put in the store. A queued message is plain RFC 822 on the local disk, so the event scripts, the virus scanners, SpamAssassin, the sandbox and the filter hook read what they always read. One door, and what a client sees Every place that reads, sizes, copies, moves or deletes a stored message now goes through one seam, MessageStore. Everything that hands a message out reads the original bytes back through it, so a client sees nothing different: IMAP FETCH BODY[], RFC822.SIZE, BODYSTRUCTURE, SEARCH, SORT and THREAD; POP3 RETR, TOP, LIST and STAT; the webmail and the REST API; the rules, Sieve, the quota (which counts the original sizes), and a DKIM signature checked against the stored copy. What changes is the file itself. Message.Filename over COM still answers the stored file's path. With compression on, a COM client or a script that opens that path reads a container, not RFC 822; with an object store, once the message is committed there is no file there. Read the message through the API instead. Compression and attachments held once Schema 6098. With MessageStoreCompression on, a message delivered to a mailbox, or put there by IMAP APPEND, is stored as a container: the message compressed with deflate, and each large MIME part, an attachment of at least MessageStoreDedupMinKB once decoded, cut out and held once as a blob that every message carrying the same bytes shares. A mail sent to forty people on this server with a 10 MB presentation keeps the presentation once. Where What Control Panel (Windows) Settings > Maintenance > Performance, the Message store tab Control Deck The two settings on the server settings page, and a Message store section on the dashboard REST message_store_compression and message_store_dedup_min_kb in /api/v1/settings; GET /api/v1/store; POST /api/v1/store/measure COM (Windows) Settings.MessageStoreCompression, Settings.MessageStoreDedupMinKB, Settings.MessageStoreStatus Both are rows of hm_settings and take effect for the next message stored. MessageStoreDedupMinKB is 256 unless changed, 0 to 65536, and 0 holds nothing once and compresses only. The switch rewrites nothing. The reader never asks the setting what a file is: it reads either form, per file, by the file's first eight bytes. Turning it off leaves every stored message readable. Only delivery and IMAP APPEND write the new form. A webmail draft, a message saved through COM, the importers and the IMAP mirror write plain. A message larger than 64 MB is stored as it came, because a container is decoded whole in memory. Every container is checked before a byte is served: its structure, each blob against its hash, and the SHA-256 of the whole. A damaged one is refused and named in the error log (HM7350), never served as something else. Two messages share a blob when the part's encoded bytes are identical. The same file attached twice by a client that wraps its base64 differently is two blobs. The command line, run as the user the server runs as (hMailServer.exe takes the same switches on Windows): hmailserver --measure-store [--account <address> | --domain <name>] hmailserver --compress-store [--account <address> | --domain <name>] [--decompress] hmailserver --recount-store Verb What it does Server --measure-store Changes nothing; prints what the store takes now, what it would take, and how much more it would save May be running --compress-store Rewrites every stored file in place, each written beside itself, read back and compared byte for byte, then renamed over its name Stopped --compress-store --decompress Puts the files back as plain RFC 822 Stopped --recount-store Sets every blob's count to what the stored files reference Stopped Exit 0 is a finished walk, 1 a file that could not be read or written, 2 no database or no such account or domain, and 4 a running server. How much it saves has not been measured on a real mailbox. MessageStore.md holds a placeholder where the figures are to go, and the wiki claims none. Run --measure-store on your own store and read the answer. The document also records that its regression fixture had not yet run against a bench when it was written. Object storage: S3 and Azure Blob The messages in the mailboxes, the blobs and the recover-deleted store's kept copies can be kept in an S3-compatible store (AWS S3, MinIO, Wasabi, Backblaze B2's S3 API, Ceph RGW), signed with AWS Signature Version 4, or in Azure Blob Storage, with Shared Key or a SAS. The database stays the index, and an object is named by the path its file had. There is no schema step. Nothing changes until a store is named. Every write is a local one. A message is placed on the local disk exactly as without a store, and a committer puts it into the store afterwards, and only then removes the local file. It runs every minute; what it has been handed is journalled in <data directory>\_ObjectStore\pending.txt and taken up again after a restart. So a delivery never waits for the store and never fails for it, and every read looks at the local disk first. Property REST key Default MessageStoreBackend message_store_backend file; the other value is object MessageStoreObjectService message_store_object_service empty; s3 or azure MessageStoreObjectEndpoint message_store_object_endpoint empty, which is the service's own MessageStoreObjectRegion message_store_object_region us-east-1 MessageStoreObjectBucket message_store_object_bucket empty MessageStoreObjectAddressing message_store_object_addressing auto; also path, virtual MessageStoreObjectPrefix message_store_object_prefix empty MessageStoreObjectAccessKey message_store_object_access_key empty MessageStoreObjectSecret message_store_object_secret empty MessageStoreObjectSasToken message_store_object_sas_token empty MessageStoreObjectCaFile message_store_object_ca_file empty MessageStoreObjectCacheMB message_store_object_cache_mb 32 These are rows of hm_settings, set over /api/v1/settings or on the Control Deck's Settings > Server form. There is no COM member and no Control Panel page for them. The secret and the SAS are protected, never emitted, and left out of a backup, so after a restore onto another server they are entered again. TLS verification is always on, and no setting turns it off. With MessageStoreObjectCaFile set, the certificate is verified against that file: beside the system's bundle on Linux, and alone on Windows. Turning it on: Make the bucket or container. This server makes none. For S3, an access key allowed s3:GetObject, s3:PutObject, s3:DeleteObject, s3:ListBucket and the multipart upload calls, and a lifecycle rule that aborts incomplete multipart uploads after a day. Name it, with message_store_backend still file. Copy the store into it: hmailserver --copy-store-to-object-storage. It runs beside the server, verifies every object, and goes on where it stopped when run again. Switch new mail to it: message_store_backend to object, taken within five seconds. Remove the local copies when you are ready: stop the server and run the copy again with --remove-source. Going back is the same the other way: message_store_backend to file, then hmailserver --copy-store-from-object-storage, then, once it says nothing failed, message_store_object_service to empty. When the store cannot be reached, mail is not lost. Door Answer for a message only the store holds A delivery Placed on the local disk and done; committed when the store answers again IMAP FETCH NO [UNAVAILABLE] POP3 RETR and TOP -ERR [SYS/TEMP] The webmail and REST 503 HM7473 says, once an hour at most, how many files wait on the local disk. HM7471 is the store refusing this server's credentials (401, 403), which is never taken for an absent message. HM7476 names a message that is neither on the disk nor in the store; no placeholder is written for it while a store is named. What is thinner than it could be: one connection per request; one committer, a pass at a time; credentials are the two the settings hold, with no instance role, no workload identity and no Azure AD token; the Import Tool and the data directory synchronizer walk the local disk only; a changed prefix is not a move. The requests were proved against MinIO and Azurite on the project's lab on 25 September 2026. The server's fixture needs that lab, is skipped on a bench without one, and has not run in the Linux suite. Mailbox encryption at rest Schema 6099. A mailbox whose messages are stored encrypted to an X25519 key of the account's own, with AES-256-GCM, as the container's last stage, after compression. The key opens when the person signs in with their password and closes MailboxKeyIdleMinutes after the owner's last use (15 by default, 1 to 1440). Mail that arrives while nobody is signed in is encrypted all the same, because encrypting needs only the key's public half. When a password is lost, the domain's recovery key opens the key again. It is off for every account until an administrator switches it on for that account. What it protects: a disk taken out of the machine, a snapshot of the volume, a backup archive, the data directory copied somewhere, the database dumped, once the account has signed in with its password after the switch. What it does not protect. It does not protect the mail from the running server, which reads it while the key is open, nor from whoever runs the server, who can capture a password at sign-in. A person who needs that wants a zero-access mailbox, and the two are never on one mailbox. Until the first sign-in the key is pending, held under this machine's own secret: the machine's DPAPI key on Windows, and the key file .hmailserver-secret-key inside the data directory on Linux. On Linux a copy of the data directory together with the database therefore opens every pending key. An account that only receives mail and never signs in with its password stays pending. Switching it on, on the Control Deck or over REST: The domain's recovery key first: the domain page's Mailbox recovery key card, or PUT /api/v1/domains/{domain}/mailbox-recovery with {"passphrase": ..., "hint": ...}. The passphrase is at least 12 characters, and this server never stores it. Keep it where this server is not. Then the account: the Encryption at rest card, or PUT /api/v1/accounts/{address}/mailbox-encryption with {"enabled": true}. The switch-on ends every session the account had. A mail client sees its connection drop; the person signs in again with the password. Then what is already there: hmailserver --encrypt-store --account <address>, with the server stopped. Refused, with a sentence: an account whose domain has no recovery key; a zero-access mailbox; an account that signs in against a Windows directory; and an account with a TOTP second factor enrolled. The last runs both ways: a second factor cannot be enrolled on an account that has a mailbox key. Sign-in. IMAP and POP3 sign a keyed mailbox in only with its own password. An app password, an OAuth2 token, a client certificate or a Kerberos ticket is answered NO [UNAVAILABLE] (POP3: -ERR [SYS/PERM]), and the IMAP master user is refused whether or not the owner's key is open. The webmail refuses a passkey, single sign-on or an app password (403, mailbox_key). SMTP, ManageSieve and DAV are not refused, since they do not read the mailbox. Passwords. The person's own change wraps the key by the new password. An administrator's reset does so while the key is open or pending, and is refused while it is closed, with a sentence naming the recovery: POST /api/v1/accounts/{address}/mailbox-encryption/recover with the domain's passphrase and the new password. A lost password and a lost recovery passphrase together are the mail lost. Nobody can open it. DELETE /api/v1/accounts/{address}/mailbox-encryption?confirm=forfeit removes the key, and with it every message still encrypted to it, for ever. It is refused while a legal hold covers the account. Switching it off stores new mail as it comes. What was stored encrypted stays encrypted until hmailserver --decrypt-store, run with the server stopped and the domain's passphrase in the environment variable HMAILSERVER_RECOVERY_PASSPHRASE, never on the command line. A keyed mailbox stays in its domain. A move to another domain is refused (409); a rename inside the domain goes ahead. What stays in the clear: the queue and the copy a delivery works on, which is overwritten with zeros once placed, though a copy-on-write filesystem, a snapshot or an SSD may not honour that; the calendar and the address book; the quarantine, the archive and journal copies, and a forward; each message's row in the database, with its size, flags, folder and dates; logs and the trace. An encrypted mailbox holds no part once. A blob shared between accounts cannot be under one account's key, so every part stays inside the message's own container. The cost is the de-duplication, for these mailboxes only. Backups A backup with messages copies the data directory as always, with the containers, the Blobs directory and encrypted files as they are. It copies file by file while the server runs, so an archive taken while mail is delivered and deleted can hold a container whose part was written after the Blobs directory was copied: take the backup when the server is quiet. With an object store it also reads every message and blob that is only in the store into the archive; a store that cannot be read fails the backup. A full restore rebuilds the blobs' counts from the stored files. hm_storeblobs is not in the archive. An encrypted mailbox's key is carried with its account, and the domain's recovery key with its domain. A pending key on Windows opens only on the machine that wrote it; elsewhere the mailbox is opened with the recovery. On Linux it opens wherever the key file goes, and the archive does not carry that file. A restore onto a server that never turned compression on reads the containers the archive brought. Known limits in 6.3.4 Limit Where it is stated Encryption at rest is on REST and the Control Deck, with no webmail card and no Control Panel page. The person's own view is GET /api/v1/me/mailbox-encryption CHANGELOG, EncryptionAtRest.md No COM for the switch, the recovery or MailboxKeyIdleMinutes EncryptionAtRest.md A search over an encrypted mailbox reads every message, and works only while its key is open. The full-text index holds none of its words EncryptionAtRest.md With compression on, Message.Filename over COM names the stored form; with an object store it names a path that holds no file once committed CHANGELOG, MessageStore.md, ObjectStorage.md An account's key is never rotated; the domain's recovery key can be replaced EncryptionAtRest.md The encryption fixture runs on Windows and is not in the Linux suite EncryptionAtRest.md No zstd: deflate is the one stage MessageStore.md Where to read more The wiki page: Message Storage The documents: MessageStore.md, ObjectStorage.md and EncryptionAtRest.md, each with its error codes and what proves each claim Zero-access mailboxes: the wiki's Encryption for People, and on this forum End-to-end encryption The documentation page: Backup and restore On this forum: The Control Deck, where the cards and the settings form are, and Backup and restore, for what the archive holds and what it leaves out
  • 0 Votes
    1 Posts
    143 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) has the migrations as a card on the Control Panel's Backup, restore & migration page, where this topic says there is no Control Panel page; and a message collected from an external account is sent to SpamAssassin with the Return-Path the other server recorded, where it was the guess from the From field, so its SPF and envelope rules may score such mail differently. See the 6.3.7 announcement. The Windows installer of hMailServer 6.3.8 (2 November 2026) refuses an installation of the original hMailServer's 5.7 (5.7.0, its pre-release builds 2788 and 2843, 5.7.1 and 5.7.2), which this fork's upgrade chain has no step from, before it changes anything, and the installers of 6.3.7 and earlier must never be run over one: such a server's mail comes in by a migration of kind imap, as Migration.md's From the original hMailServer says. See the 6.3.8 announcement. This topic covers every route 6.3.4 offers for bringing existing mail into this server: the webmail's Other accounts page for one person, an external account with the mirror on for a few mailboxes that must stay in step, migration jobs for hundreds of mailboxes from an IMAP server, Google Workspace or Microsoft 365, the Import Tool for mbox files and a Maildir, and the PST import for Outlook data files. It ends with the mailbox move, for a mailbox that is already here and needs a new address or a new domain. The one thing to know first: every route needs the local account to exist. Create the accounts with the Import Tool's text-file import, POST /api/v1/domains/{domain}/accounts, a spreadsheet over REST and hmctl, or SCIM, and only then bring the mail. Moving an installation is a different subject: Upgrading to 6.3.4 for an upgrade and Moving to new hardware for a new machine. Which route You are moving Use Keeps Does not keep One person's own mailbox, by that person The webmail's Other accounts (since 6.3.4) Folders as copies, then new mail collected Rules, signatures A few mailboxes that must stay in step until the cut-over An external account with Mirror every folder on (6.3.3 and later) Folders, every message byte for byte, the five standard flags, the internal date Subscriptions, custom keywords, ACLs Many mailboxes from an IMAP server, Google Workspace or Microsoft 365 A migration, a job per mailbox (since 6.3.4, schema 6090) The above, with keywords, special-use folders, contacts and calendars Rules, signatures, shared mailboxes, ACLs, Gmail's labels as labels mbox files The Import Tool, one folder per file (Windows only) Every message, the date from the headers Flags A Maildir The Import Tool's Select Maildir (Windows only) The Maildir++ hierarchy, the flags in the file names Custom keywords Outlook PST files PST import, a migration of kind pst (since 6.3.4) Folders, messages rebuilt as Internet mail, contacts, calendar items, tasks Attachments by reference, OLE objects, distribution lists What PST import refuses Outlook itself, over IMAP Whatever Outlook uploads Anything Outlook does not put on the wire One person, from the webmail: Other accounts Since 6.3.4. Settings, Other accounts lets a person do the IMAP route for their own mailbox without an administrator. They pick the old provider (Gmail, Outlook.com, Yahoo Mail and iCloud Mail fill in the server, port and encryption), give the user name and password, choose whether to leave the mail there, and tick Bring the folders over too. It is an external account made through /api/v1/me/fetch-accounts, with these differences from one an administrator makes: The spam and virus filters are on for new mail; folders brought over arrive as copies, without the filters. The host must resolve to public addresses only, on port 110 or 995 (POP3) or 143 or 993 (IMAP), unless the administrator turns on reader_fetch_accounts_private_addresses. Schema 6104 marks such an account reader_created, and the host is judged again at every collection: a name that answers a private address later is not collected, and the error log says why (HM5909). The interval is at least 15 minutes, and a person has at most reader_fetch_accounts_max of them: 3 by default, and 0 takes the page away. A host on FetchOAuth2Hosts (outlook.office365.com by default) is refused from the page; only a server administrator adds one. Signing in rather than giving a password. Outlook.com no longer accepts a password from another mail server, and Gmail takes only an app password. Where an administrator has registered the server with Google and Microsoft, the page offers Sign in to Gmail and Sign in to Outlook.com: the person grants IMAP access there and the account logs in with XOAUTH2. No password is stored, and the permission can be taken back at the provider. The first collection then lists the folders to tick, with Gmail's All Mail, Starred and Important unticked so that nothing arrives twice. The registration is set on PUT /api/v1/settings: import_oauth_redirect_base_url (the redirect URI to register is that plus /portal/oauth/callback), import_oauth_google_client_id and import_oauth_google_client_secret, import_oauth_microsoft_client_id and import_oauth_microsoft_client_secret. The Google client needs the Gmail API and the https://mail.google.com/ scope; the Microsoft Entra registration needs the delegated IMAP.AccessAsUser.All and offline_access permissions. Until Google verifies the app, only the test users named on its consent screen can sign in. A few mailboxes that must stay in step: the mirror With Mirror every folder on, the server's own IMAP client does what imapsync does. The switch is on the account dialog's External tab in the Control Panel (Windows) and on the Deck's fetch-accounts view; over COM (Windows) it is FetchAccount.ServerType = 1 and FetchAccount.MirrorFolders = true. Create the local account. Add an external account on it: server type IMAP, the remote host, port and security (implicit TLS on 993, STARTTLS on 143), the remote credentials, and the mirror on. Decide what happens on the far side. Days to keep messages at 0 deletes each message from the remote folder once it is filed here; any other value leaves it there, so the two servers stay in step for as long as the account polls. A migration usually leaves the messages in place until the cut-over, then one last poll, and the account is disabled. Poll. Download now starts a collection at once. A large mailbox is collected over several polls, and a message is recorded as collected only after it has been filed. A mirrored message is stored byte for byte: no X-hMailServer-ExternalAccount header, no delivery, no rules, no anti-spam or anti-virus, because it is a copy of mail the other server had already accepted. The remote flags and internal date become the local ones, and Archive/2025 becomes Archive.2025. Use a new external account for the mirror. One that has been collecting the INBOX without the mirror collects it once more when the mirror is switched on. A \Noselect mailbox is skipped with a line in the application log, OnExternalAccountDownload does not fire for a mirrored message, and subscriptions, ACLs, quotas, Sieve and vacation settings on the far side are not read. Many mailboxes: migration jobs Since 6.3.4, schema 6090. A migration is a batch the server runs itself: a job per mailbox, throttled, resumed after a failure or a restart, with a report per mailbox and the contacts and calendars beside the mail. It is administered under /api/v1/migrations and in the Control Deck's Migrations view; there is no COM interface, and in 6.3.4 no Control Panel page (in 6.3.7's Control Panel the migrations are a card on Backup, restore & migration). A migration names its source: kind imap (a password per mailbox), google or microsoft365 (each mailbox signed in to by its owner), or pst; concurrency (1 to 32, 4 by default); and messages_per_minute (0 for no limit). A Google or Microsoft 365 migration reads imap.gmail.com or outlook.office365.com on port 993 over TLS, and any other host is refused. A job is queued, running, paused, done, failed or cancelled, through the phases mail, contacts, calendars and report. POST /api/v1/migrations {"name": "Old server", "host": "imap.old.example"} POST /api/v1/migrations/{id}/jobs the CSV below, or one job as JSON POST /api/v1/migrations/{id}/start GET /api/v1/migrations/{id}/jobs/{job}/report GET /api/v1/migrations/{id}/summary.csv sourceuser,secret,target [email protected],Anna's old password,[email protected] [email protected],,[email protected] One request takes up to 5,000 lines. Every line is checked before any job is made, and a bad line refuses the whole request and names itself, so nothing is half added. A job into an account another migration is copying into now is refused (409). Google Workspace and Microsoft 365. A job added with an empty secret waits for its owner: the answer carries signin_url, a one-use link for that person to open, sign in as that mailbox and grant access. A sign-in as anybody else attaches nothing, and POST .../jobs/{job}/signin makes a new link. The scopes asked for are Google's https://mail.google.com/, https://www.googleapis.com/auth/carddav and https://www.googleapis.com/auth/calendar, and Microsoft's https://outlook.office.com/IMAP.AccessAsUser.All and offline_access with Microsoft Graph Contacts.Read and Calendars.Read. A work or school tenant may need an administrator's consent. What a job does. It maps the INBOX to the INBOX, a special-use folder (\Sent, \Drafts, \Junk, \Trash, \Archive) to the account's own folder of that use whatever it is called, and everything else to the same path. It opens each folder with EXAMINE, so nothing on the source is marked read, and stores each message byte for byte with its flags, keywords and internal date. A message too large is skipped as too_large; an account over its quota pauses the job. No rule, filter or scan runs over a copied message. Contacts and calendars follow: from an IMAP server by CardDAV and CalDAV found by RFC 6764 discovery (or dav_url), from Google by its CardDAV and CalDAV, from Microsoft 365 by Microsoft Graph. A phase that cannot run is skipped with its reason and never fails a job whose mail is copied. A job is one copy, not a synchronisation: move the MX records first and migrate after, or keep a mirror for the mailboxes that must stay in step. It survives being interrupted. After every 50 messages and at the end of each folder the job writes its resume marks, and a job that has run before reads back what it wrote, so each message arrives once. A failure time may mend is retried after 1, 5 and 15 minutes and then hourly; after 24 attempts in a row without a message copied the job is failed. Wrong credentials fail it at once, and POST .../jobs/{job}/retry with {"secret": "..."} takes a mended password. Throttling is threefold: concurrency per migration; MigrationConnectionsPerHost (4 by default, 1 to 64) across every migration from one source host, because a provider counts sessions per client address; and at most 64 jobs on the server at once. Who may. Reading needs view_accounts and writing manage_accounts in the target's domain. The server administrator creates a migration, and a domain administrator then sees and adds to it. The audit trail records migrations and jobs, never a secret's value. Jobs are not carried by a backup. HM7240 (a resume mark could not be written) and HM7241 (a message could not be filed) are the two codes that reach the error log for a job from an IMAP server, Google or Microsoft 365. A PST job says the same two as HM7370 and HM7371, and its contacts and calendars phases add HM7410 and HM7411. The report says, folder by folder, how many messages the source held, how many were copied and why any were skipped: duplicate, another_folder, too_large, refused, gone, empty. mbox files, a Maildir and accounts from a text file: the Import Tool The Import Tool is a Windows program (Start menu > hMailServer > Import Tool, or Addons\ImportTool\ImportTool.exe) that works through COM, each message going to the server through Utilities.ImportMessageFromFileToIMAPFolder; there is no Import Tool on Linux. It imports a folder of mbox files into one account, one IMAP folder per file; Inbox in any case is the INBOX, and a subfolder's files are a second run. Each message is dated from its Received or Date header. mbox carries no flags the tool could read, so every imported message is unread. Select Maildir points at the directory holding cur, new and tmp, and lists the INBOX and every Maildir++ folder beside it. The flags after :2, in a file name are set on the copy: S seen, F flagged, R answered, D draft, T deleted. A colon is not allowed in a Windows file name, so the tool also reads ;2, and the same marker written with an exclamation mark in place of the colon; a copy whose names lost the info part imports unread. Accounts from a text file. The tool's other button creates or updates the accounts of one domain from a comma-separated text file, three fields a line: the account's name without the domain, its password and its size limit in MB (0 for none). Where the breach check is on, it runs for each password the tool sets through COM's Account.Password, one request per account: run a large import against your own mirror of the endpoint, or with the check off. Outlook data files: PST import Since 6.3.4. The server reads a PST itself, with a reader of this project's own written from Microsoft's published specifications and no third-party PST code. It is a migration of kind pst: one job per file, each into one account. Off until a directory is named. A job names a file, never a path, and the file is looked for only in the subdirectory named for the target account's domain inside the directory pst_import_directory names (PstImportDirectory in hm_settings, set with PUT /api/v1/settings or on the Deck's Settings page). It is empty as shipped, and while it is empty every PST job is refused with No PST import directory is set on this server. D:\PstImport\ <- pst_import_directory D:\PstImport\example.com\archive.pst <- a job for [email protected] names archive.pst Copy the file into the domain's subdirectory, where the service's account can read it. A share on another machine is refused, and so is a name that climbs out of the domain's directory. Make the migration, POST /api/v1/migrations with {"name": "Archives", "kind": "pst"}, then jobs as CSV lines of file,password,target over REST or in the Deck, or one job as JSON with hmctl api POST /api/v1/migrations/{id}/jobs. There is no Import Tool button for it and no COM route. Start it. It needs manage_accounts and manage_mailbox in the target's domain. What comes in. The folders a person sees in Outlook, with Sent Items, Deleted Items, Drafts and Junk E-mail found by what the store says they are, never by their names. Every message is rebuilt as Internet mail, deterministically, so a resumed job knows what it filed. Read, unsent, flagged and replied state become \Seen, \Draft, \Flagged and \Answered, forwarded the keyword $Forwarded, categories keywords. Contacts go into the address book, calendar items and tasks into the calendars, notes and journal entries into mail folders as messages. ANSI files (Outlook 97 to 2002) are read. What it refuses, with a sentence saying what to do: the 4K-page layout Outlook 2013 and later write for an OST, an OST caching an Exchange mailbox, a file under Windows Information Protection, and a missing or wrong password. A file Outlook still has open pauses the job. A message over 128 MB of text and data together is left out and named. Migration.md is candid about its evidence: no password-protected PST and no OST from Outlook were at hand. The reader runs in a process of its own (hMailServer.exe --pst-read on Windows, hmailserver --pst-read on Linux, not a command for an administrator), killed on a pause, a cancel or a stop, or when it has not answered in ten minutes. A file that crashes it costs one message, not the service. This is crash containment, not a sandbox: the reader runs as the service's own account, bounded in memory (2 GB committed on Windows, 4 GB of address space on Linux). The fixture API.PstImport runs on Windows; nothing runs it on Linux yet. What is not migrated Rules and filters, signatures, out-of-office replies and forwarding: make them again here. Shared mailboxes and permissions; a Microsoft 365 shared mailbox, which nobody signs in as, cannot be a job. Subscriptions: every folder is created subscribed. Gmail's labels as labels. A google migration copies each message once, and the star arrives as \Flagged. Contact groups; from Microsoft 365, the default contacts folder and calendar only. A mailbox already here: renaming it, or moving it to another domain Do not copy it into a second account: that loses everything hanging off the account id, which is its rules, folders, application passwords, second factor, contacts, calendars and role. Move or rename (since 6.3.4) keeps the account's row, and leaves the old address as an alias unless you say otherwise. Relocating an installation, and moving or renaming a mailbox has where it is done, what moves and what is refused. Where to read more The wiki page: Migration The document, with every field, limit and refusal: Migration.md Renaming and moving a mailbox: MailboxMove.md The documentation page: Importing accounts and mail, the Import Tool On this forum: The Control Deck, where the Migrations view is, and The webmail portal at /portal, where Other accounts is.
  • 0 Votes
    1 Posts
    136 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) changes some of what a client gets. IMAP COPY and MOVE by message number read their set in the client's numbering, and a set that names a message another session has just expunged is refused NO [EXPUNGEISSUED] for the client to send again; a COPY or MOVE that fails part way leaves the destination as it was; SEARCH, SORT and THREAD ask a criterion named again beside itself once; and FETCH BODY[N.MIME], and BODY[N] of a multipart, are the stored octets. POP3 accepts the UTF8 classic Outlook for Windows (2016 to 2024) sends after the login, until the session's first STAT, LIST, UIDL, RETR, TOP, DELE or RSET, so its downloads work again. SMTP, POP3 and ManageSieve refuse a SASL PLAIN sign-in that asks to act as another account. The IMAP and POP3 listeners can take the client's address from a PROXY protocol header, as the SMTP listener does, each from the peers on a list of its own, both off by default. The send-as grant now bears on DKIM too: an authenticated submission is signed as another hosted domain only when its account may send as the address its From names - its own, an alias pointing at it, a mailbox that granted it the post right, an address it proved, or a masked address of its own - and is otherwise accepted and sent unsigned as that domain. See the 6.3.7 announcement. hMailServer 6.3.8 (2 November 2026) signs a name in to an account only when it is the account's stored address, compared in lower case after a domain alias and the default domain, on IMAP, POP3 and SMTP alike, SCRAM-SHA-256 among them: a client that signs in with another spelling of the address, in more than the case of the letters A to Z, or with white space at either end, may no longer sign in, since such a name fails as a name that does not exist fails and is counted as a failure. Correct the name in the client. See the 6.3.8 announcement. This topic is the server side of IMAP, POP3 and SMTP in hMailServer 6.3.4: what each listener advertises and which switch gates it, search limits and the full-text index, QRESYNC's expunge history, connection limits, shared mailboxes, and what 6.3.4 fixed that a client may notice. Know this first: a capability missing from an answer is nearly always a switch or the session's state, not a fault. Two IMAP switches ship off, and since 6.3.4 IMAP offers STARTTLS only before TLS and sign-in, and COMPRESS=DEFLATE only after sign-in. What a mail client needs is in Connecting a mail client; the ACL rights table is in Aliases, distribution lists, mailing lists and public folders. Seeing what the server advertises Ask the server, from any machine with OpenSSL. For SMTP type EHLO test.example.org; for IMAP, CAPABILITY: openssl s_client -connect mail.yourcompany.com:25 -starttls smtp -quiet openssl s_client -connect mail.yourcompany.com:993 -quiet On 6.3.4 ask IMAP before LOGIN and after: the answers differ by design. For POP3, connect to 995 the same way and send CAPA. Ports 993 and 995 exist only once you have created them (Connections & protocols → TCP/IP ports in the Windows Control Panel, or the Control Deck). IMAP: what is always there On every connection, as in 6.3.3: IMAP4 IMAP4rev1 IMAP4rev2 CHILDREN THREAD=ORDEREDSUBJECT THREAD=REFERENCES NAMESPACE RIGHTS=texk MOVE ID SPECIAL-USE CREATE-SPECIAL-USE UNSELECT UIDPLUS ENABLE STATUS=SIZE ESEARCH CONDSTORE QRESYNC LIST-EXTENDED SEARCHRES WITHIN UTF8=ACCEPT LIST-STATUS PREVIEW MULTIAPPEND REPLACE SAVEDATE METADATA OBJECTID BINARY LITERAL- APPENDLIMIT APPENDLIMIT is bare before authentication and APPENDLIMIT=<bytes> after. LITERAL- caps non-synchronising literals at 4096 bytes, so an oversized APPEND can still be refused with TOOBIG. A client opts in to IMAP4rev2 per session with ENABLE IMAP4rev2, which implies UTF8=ACCEPT. ENABLE recognises exactly four names: QRESYNC (implies CONDSTORE), CONDSTORE, UTF8=ACCEPT and IMAP4rev2. After ENABLE IMAP4rev2 the session drops the RECENT response and the [UNSEEN] code on SELECT and EXAMINE, drops RECENT as a STATUS item, and answers a SEARCH with no RETURN options as ESEARCH. IMAP: what is gated, and by what Capability Advertised when Shipped IDLE enableimapidle is on On QUOTA QUOTA=RES-STORAGE enableimapquota is on On SORT enableimapsort is on On ACL enableimapacl is on; it also switches the #Users namespace On AUTH=PLAIN, AUTH=SCRAM-SHA-256 EnableImapSASLPlain is on and authentication is available Off SASL-IR EnableImapSASLInitialResponse is on Off COMPRESS=DEFLATE IMAPCompressionEnabled=1, the client has signed in (since 6.3.4), and the session is not yet compressed On STARTTLS The port is STARTTLS-optional or -required, TLS is not active, and (since 6.3.4) the client has not signed in Per port LOGINDISABLED Authentication is refused on this cleartext connection (STARTTLS-required port, or RequireTLSForAuth on the IP range) Per range The rest depend on the connection: AUTH=SCRAM-SHA-256-PLUS only on TLS (and behind the same SASL switch), AUTH=XOAUTH2 AUTH=OAUTHBEARER with OAuth2Enabled and, unless OAuth2RequireTLS=0, only over TLS, AUTH=EXTERNAL on a verified client certificate that names an address, and UNAUTHENTICATE once signed in. IDLE reports changes in the selected folder (EXISTS, RECENT, EXPUNGE, FLAGS) as they happen. A folder another client has just created is seen at the next folder-list refresh, as there is no IMAP NOTIFY; that is IMAP, not a fault. QUOTA answers with one unnamed root, "", and a STORAGE resource in kilobytes from the account's maximum size; an account with no limit gets an empty list. SETQUOTA is refused, because the quota is the administrator's. Refusals of APPEND, COPY and MOVE for a full mailbox carry OVERQUOTA. SORT: with message indexing off, as it ships, SORT reads each message's header from disk. EnableImapSASLPlain gates SCRAM as well as PLAIN, despite its name. Roadmap.md says it gates the whole AUTHENTICATE command, while the Capabilities wiki gives OAuth2 and EXTERNAL conditions of their own: with it off, test XOAUTH2, OAUTHBEARER and EXTERNAL over IMAP before relying on them. COMPRESS DEFLATE (RFC 4978) compresses both directions of the rest of the session; a client that does not ask is unaffected. A second one is NO [COMPRESSIONACTIVE], STARTTLS under compression is BAD, and IMAPCompressionEnabled=0 withdraws it, the command then answering BAD like an unknown one. POP3 and SMTP: the same question POP3 CAPA always carries UIDL, TOP, RESP-CODES, AUTH-RESP-CODE, IMPLEMENTATION hMailServer, PIPELINING, UTF8 and EXPIRE NEVER. The SASL mechanisms depend on TLS, STLS on the port, and LOGIN-DELAY <n> appears only when Pop3LoginDelaySeconds is above 0 (0 ships). That key advertises and enforces a minimum interval between sign-ins, refusing an early one with -ERR [LOGIN-DELAY] …; on Windows it is the POP3 tab's card How often a client may check for mail. SMTP EHLO always carries SIZE <n> (the maximum message size in KB times 1024; bare SIZE when 0), 8BITMIME, PIPELINING, CHUNKING, BINARYMIME, SMTPUTF8, ENHANCEDSTATUSCODES, DSN and HELP. BINARYMIME is for local delivery: a recipient needing onward relay is refused at RCPT TO with 554 5.6.3. Conditionally: XCLIENT only when SMTPXClientEnabled=1 and the real peer is in SMTPXClientTrustedIPs; an untrusted peer that tries it gets 550. STARTTLS on a cleartext connection to a STARTTLS-optional or -required port. AUTH only when authentication is enabled, withheld on cleartext when the port requires STARTTLS or the range sets RequireTLSForAuth, and never on the ports in DisableAUTHList. Not a keyword, but decided the same way: the HAProxy PROXY protocol is accepted only with SMTPProxyProtocolEnabled=1 and from a peer in SMTPProxyProtocolTrustedIPs, and nothing in EHLO announces it. On SMTP and POP3 the AUTH line and its SASL responses take up to 12,288 octets; every other line keeps 510 (SMTP) or 500 (POP3). What 6.3.4 added to SMTP is refusals, not capabilities. Where the switches are set, and when a change applies What Windows Control Panel Elsewhere Applies IDLE, QUOTA, SORT, ACL, the two SASL switches, the connection limits Connections & protocols → Protocols, IMAP, SMTP and POP3 tabs COM Settings (IMAPIdleEnabled and its neighbours); REST PUT /api/v1/settings (imap_idle_enabled, imap_quota_enabled, imap_sort_enabled, imap_acl_enabled, imap_sasl_plain_enabled, imap_sasl_initial_response_enabled); the Deck's Settings view; hmctl settings set At once Message indexing Maintenance → Performance, Indexing tab COM Settings.MessageIndexing; PUT /api/v1/settings/indexing (enabled); hmctl settings, group indexing At once IMAPCompressionEnabled, IMAPSearchTimeout, IMAPSearchMaxMegabytes, IMAPExpungeRetentionRecords, Pop3LoginDelaySeconds, IndexerFullText and its tuning keys Cards on the IMAP and POP3 tabs; the Indexing tab COM Settings.SetIniSetting; PUT /api/v1/settings/ini/{name} (administrator password only); hmctl ini; on Linux hmailserver --set-setting NAME=VALUE. No Deck page When saved The Control Panel and COM are Windows-only. The last row's keys were the [Settings] section of hMailServer.ini; since 6.3.4 the database holds them, none of them waits for a reinitialise or a restart (docs/SettingsApply.md), and the server does not take a line edited in the file, so set them through an interface. Settings: when a save takes effect covers the store, HM5804, and a value stored by --set-setting while the service runs. The REST lines assume the listener on 127.0.0.1:8443, as the REST API wiki's examples do; RestApiPort ships at 0 (off) and waits for a reinitialise. # Switch IDLE off; in effect when the answer comes back curl -u Administrator:your-admin-password -X PUT -H "Content-Type: application/json" \ -d '{"imap_idle_enabled":false}' http://127.0.0.1:8443/api/v1/settings # A [Settings] key: the administrator password only, never an API key curl -u Administrator:your-admin-password -X PUT -H "Content-Type: application/json" \ -d '{"value":"120"}' http://127.0.0.1:8443/api/v1/settings/ini/IMAPSearchTimeout # On Linux, then a restart so the running server takes it sudo hmailserver --set-setting IMAPSearchTimeout=120 sudo systemctl restart hmailserver Search: two ceilings and the full-text index Without the full-text index, SEARCH BODY and TEXT read every message in the mailbox, attachments whose media type is text included (decoded, bounded at 1 MB). Two ceilings, measured from the moment a command starts and never re-armed by progress, bound SEARCH, SORT and THREAD: IMAPSearchTimeout (60 seconds) and IMAPSearchMaxMegabytes (2048 MB of message content read); 0 turns either off. They are the IMAP tab's card IMAP search limits, and README.md says to raise them for mailboxes of several hundred thousand messages. What a client gets at a ceiling is stated two ways. Roadmap.md records NO with RFC 9051 LIMIT and no untagged result, so a cut-short list cannot pass for a complete one; README.md's comment beside IMAPSearchTimeout says the search returns the matches found so far. Until that is settled, treat a search that hit a ceiling as incomplete, and let nothing act on its result in bulk. The full-text index is off, and needs two switches: IndexerFullText=1 indexes nothing unless message indexing (Settings.MessageIndexing) is on too. It costs a term table of a few kilobytes per message and a backfill pass over every message already delivered, which is why no upgrade turns it on. The index can only prove that a message cannot match; everything else is read and scanned as before, so results are identical on or off. The webmail's search and eDiscovery use the same filter. Key Default What it does IndexerFullTextBatchSize 250 (1 to 100000) Messages one backfill pass reads before the cursor is saved and the thread pauses IndexerFullTextMinTokenLength 3 (3 to 64) The shortest search string the index answers for IndexerFullTextMaxTokensPerMessage 2048 (64 to 1000000) Distinct terms before a message is marked always-scanned instead of indexed The Indexing tab's Index now, and show how far behind it is and Discard the index and rebuild it are POST /api/v1/settings/indexing/index and /clear over REST, and Index() and Clear() over COM, where TotalIndexedCount against TotalMessageCount shows a rebuild's progress. With mailboxes encrypted at rest, the indexed count stays short by their messages for good: those are never indexed. QRESYNC: how much expunge history is kept IMAPExpungeRetentionRecords keeps the newest 5000 expunge records per mailbox (0 keeps all), pruned every twelve hours and once at service start; on Windows it is the card Change history for synchronising clients. The newest is never removed. A client whose last sync is older than what is kept gets a full resync, never a wrong one: every UID in its range that the mailbox does not hold. An installation upgraded from a release older than 6.2.23-alpha1 prunes the table in one pass at its first start. README.md says 0 defers that prune, and an IMAPExpungeRetentionRecords line of 0 added to [Settings] before the upgrade is taken: 6.3.4's first start adopts from the file a key the database has never held, and before 6.2.23-alpha1 the database held this one only if the file had the line. Restart the old service once after adding or changing it (Upgrading hMailServer wiki, 18.3, step 6): CHANGELOG.md's note that a line edited to prepare an upgrade is put back concerns a key the database already holds, and that restart settles it. Set it back to 5000 afterwards through an interface; Upgrading to 6.3.4 has the rest of the order. Connection limits and what guards them MaxSMTPConnections, MaxIMAPConnections and MaxPOP3Connections are on the Protocols page (REST max_smtp_connections, max_imap_connections, max_pop3_connections). None has a limit by default; SMTP and IMAP ship at 0, no cap. The Services tab turns a protocol off altogether (ServiceSMTP, ServiceIMAP, ServicePOP3). Watch hmailserver_sessions{protocol} on /metrics (off until MetricsServerPort is set, which waits for a reinitialise) or sessions in GET /api/v1/status. Around the limits: auto-ban counts failed sign-ins per IP across SMTP, IMAP, POP3, ManageSieve and REST (on: 3 in 30 minutes, a 60-minute ban); LogonTarpitSeconds (off) delays each failed sign-in's refusal; a second POP3 session on one mailbox gets -ERR [IN-USE]; and since 6.3.4 an IMAP LIST or LSUB pattern over 8,192 characters, or more than 32 patterns in one LIST, is answered BAD. The submission rate limit and the tarpits are in IP ranges, auto-ban, lockout and tarpits; refusing a connection by country is in allow and block lists and country blocking. Opening a mailbox to a colleague Shared and delegated mailboxes are IMAP ACL (RFC 4314). The owner opens a folder to another account, and it appears to that account under the Other Users namespace as #[email protected]. No administrator step is needed: any account whose client speaks SETACL can do it, and the audit trail records no row for it. Leave ACL on. With it off, #Users stops existing (NAMESPACE advertises NIL), public folders are open to everyone, and the send-as grant is not honoured. The owner grants rights from an IMAP client with ACL support. An administrator can do it over REST: GET on /api/v1/accounts/{address}/folders gives the folder's id, and /api/v1/accounts/{address}/folders/{id}/permissions lists, adds, changes and removes entries (since 6.3.3; writes need a full key). On Windows, COM has IMAPFolderPermissions.Add. The grantee is an account, a group (/api/v1/groups, since 6.3.4), or Anyone, which is every authenticated account on the server. Grant l, or the folder does not exist for the colleague. r reads, s changes read state, i, t and e file and delete, and p on the INBOX is the send-as grant. Grant on each top-level folder needed. A right is decided at the nearest folder up the owner's tree that has any entries, and entries are not merged: a grant on INBOX reaches its sub-folders that have none of their own, not the Sent folder, and one entry on a sub-folder cuts it off from the parent's. Over SMTP the p grant counts only while SmtpAuthenticatedSenderCheck is on, and it ships off: until then nothing constrains an authenticated MAIL FROM, so any account can send as the mailbox, or as anyone else. Turned on, it applies to every authenticated session on the server; the addresses it then allows are in who may send as whom. In the colleague's client, a pane of subscribed folders shows only the ones the owner subscribed to; the webmail shows them under Shared by <owner>. What follows from sharing: \Seen is stored on the message, not per person: with s, a delegate marks a message read for everyone; with lr alone they leave no trace, and their client cannot track what they have read. Since 6.3.4 an APPEND into a #Users folder is charged to the owner's quota, as COPY and MOVE already were. A delegate reads a folder of a mailbox encrypted at rest only while the owner's key is open; otherwise a FETCH that reads the message answers NO [UNAVAILABLE], and copying out is refused. In the webmail a read-only share refuses a flag or a delete with 403, a message cannot move between one's own mailbox and a shared one, and the owner cannot grant access from the page. Since 6.3.4 a restore puts each folder's access grants back. Every LIST runs one more query while ACL is on. What 6.3.4 fixed that a client may notice Where Dovecot's imaptest found IMAP answering other than the RFCs say (\Seen without .PEEK, APPEND byte for byte, SORT and THREAD order, among others), Connecting a mail client has the list. The rest: What a client did What it gets since 6.3.4 Sent STARTTLS after LOGIN, or inside TLS BAD, and no STARTTLS in CAPABILITY Sent COMPRESS DEFLATE before signing in NO Appended over a compressed connection, data inflating past the 250,000-octet receive buffer The whole message, where it was stored cut short or never finished Read ENVELOPE with a backslash in Date, or BODYSTRUCTURE with a quote or backslash in a part's parameters A structure that parses: the date's backslashes left out; quotes and backslashes escaped, a control character sent as a space Fetched a message the server cannot read NO [UNAVAILABLE] for a closed key; a damaged one (HM7350) left out, the rest fetched and the command ending NO - not OK with an empty ENVELOPE Changed one message's flags from two sessions at once Both changes kept; STORE ... (UNCHANGEDSINCE n) tested in the same step Went on sending a MULTIAPPEND's later literals after a refusal, one over the maximum message size (and 1 MB) The connection closed with BAD [TOOBIG], where it was read whatever its size Fetched a message only an unreachable object store holds IMAP NO [UNAVAILABLE], POP3 -ERR [SYS/TEMP] (object storage is new in 6.3.4, and off) Two heap-corruption aborts are gone as well (two sessions on one message's keywords; LIST beside folder changes). Most of these were in 6.3.3 too. What it does not do No APOP or CRAM-MD5 on POP3, and THREADID is answered NIL. Delegation is not measured in Thunderbird or Outlook, and an Outlook IMAP profile never acts on the Autodiscover answer's delegated mailboxes; the #Users namespace is what works. Where to read more Wiki: Capabilities and Configuration, Settings Reference (9.12), Connecting a Mail Client, Aliases, Lists and Public Folders (13.3), Control Panel Reference (7.5), Security Hardening (17.5, 17.7), Changes Since 6.2.10 (4.3, 4.4). At the 6.3.4 tag: SettingsApply.md, SendAs.md, EncryptionAtRest.md, DeckParity.md, README.md, CHANGELOG.md (6.3.4, IMAP, POP3 and the webmail), Roadmap.md. The public documentation: 34.3 IMAP details worth knowing, 30.3 IMAP folders and permissions. On this forum: Upgrading to 6.3.4, The Control Deck, The REST API: turning it on, Message storage: object storage and encryption at rest, What runs out first: pools, connections, large folders and Monitoring and health. A client that does not get on with these answers is a bug: a GitLab issue or, without a GitLab account, an email to contact-project+hmailserver-hmailserver-86730559-issue-@incoming.gitlab.com, filed as a confidential issue (SUPPORT.md). A vulnerability goes only by SECURITY.md's routes.
  • 0 Votes
    1 Posts
    137 Views
    ProgressiverobotP
    This topic is about the settings themselves, whichever interface you use: where a setting is stored, which of yours are not what 6.3.4 ships with, when a saved value is in effect, and how the twenty-seven settings a domain or an account may hold its own value of combine with the server's. The one thing to know first: since 6.3.4 the database is the settings store. A [Settings] line edited by hand in hMailServer.ini changes nothing; at the next start the server uses the stored value, names the key in the error log as HM5804, and, where the service may write the file, puts the line back. Everything below describes 6.3.4 unless it says otherwise; a server on 6.3.3 still reads the file and applies a change at the next start. Two stores, and how a setting is changed The table hm_settings. Everything the COM Settings object has always carried, such as the anti-spam thresholds and the delivery options. Since 6.3.4 every new setting goes here, and no key is added to [Settings]. 6.3.4 added 193 rows for its features. The [Settings] keys. 241 keys read by the settings loader, plus UseLanguage, read per call. On 6.3.3 they live in the file, mirrored into hm_inisettings. Since 6.3.4 (schema 6042) hm_inisettings is the store and the file's section is a copy, which the server keeps current where the service may write the file. Where the service cannot write the file, the copy goes stale. That is a new Windows installation, whose service runs as NT SERVICE\hMailServer, and a Linux package installation, where /etc/hmailserver/hMailServer.ini is 0640 root:hmailserver. There the copy is not refreshed and an edited line is not put back; the database's values are still the ones used. The server reports this once a start in the application log, not as an error, and ServiceAccount.md says the same of the Linux packages; the wiki's Installing on Linux page names HM5806 at a save and HM5803 at every start for them instead. On such a server read a setting from the store, not from the file. The migration is the first start. A key the table has never seen is adopted with the file's value, and there is nothing to run by hand. A line edited after the old server last started, including one edited to prepare the upgrade, is taken for an edit and put back, so make such a change after upgrading. A 6.3.3 server refuses a database at schema 6042 or later with HM5011; see Upgrading to 6.3.4. What stays in the file. [Database], [Directories], [Security] and [GUILanguages] are the bootstrap, read before the database is open and never in the store. [LDAP], [SendingLimits] and [SendingLimitsOverrides] stay in the file. PasswordPepper is kept in the file only, never in the database and never in a backup. [SettingsOverride] is the one way to make the file win, for a database that is unreachable or holds a value the server will not start on: a key written there is applied over whatever is stored and named in the error log as HM5807 at every start, because no page shows it and no backup carries it. Storing a value for a key an override shadows is accepted and reported as HM5808. Surface Set a [Settings] key Return it to the default Control Panel (Windows only) the key's editor, written over COM even on the server itself No. Clearing a box stores an empty value, which a number reads as 0 REST PUT /api/v1/settings/ini/{name} with {"value": "..."} DELETE /api/v1/settings/ini/{name} hmctl, over REST hmctl ini set <name> <value> hmctl ini delete <name> PowerShell module Set-HmIniSetting none COM (Windows only) Settings.SetIniSetting(name, value) Settings.DeleteIniSetting(name) Linux command line, as root hmailserver --set-setting NAME=VALUE, with no listener running. A secret's value is refused on the command line: give the name alone and type the value at the prompt none Control Deck only SmtpTarpitCount, SmtpTarpitDelaySeconds, UseLanguage and RewriteEnvelopeFromWhenForwarding none The REST ini routes take the administrator password only; an API key of any scope is refused. Since 6.3.4 a setting the environment names, as HM_SETTING_<key> or HM_PROPERTY_<name> (configuration from the environment), refuses an administrator's change, 409 over REST, until a start without the variable. What this server has changed from stock Since 6.3.4. One list of every setting whose value is not the one this version ships with, with the shipped value beside it. The example in ChangedFromStock.md: STORE NAME VALUE DEFAULT ini LogLevel 5 9 ini MaxLogLineLen 2000 500 ini SMTPDMaxTimeout 600 1800 ini OAuth2HmacSecret (set) ini DnsServer 10.0.0.1 (forced from [SettingsOverride]) database smtpnoofretries 6 4 database maxsmtpconnections 200 0 7 of 391 settings are not what this version ships with. Where What to ask Control Deck Changed from stock, in the navigation beside Settings REST GET /api/v1/settings/changed Command line hmctl config changed PowerShell Get-HmChangedSetting COM Settings.GetChangedFromDefault The REST route and the COM call answer the same document. store is ini (hm_inisettings) or database (hm_settings). What "default" means. The value the server behaves as though the setting has when nothing has been stored for it, not what a fresh installation's files contain. hMailServer.ini ships with lines the packaging wrote, such as RestApiBindAddress=127.0.0.1, and a line holding the shipped value is not a change; one holding another value is listed. An upgraded installation lists more than a new one. database defaults are what a database created by this version holds. An installation upgraded from an old schema got its values from the upgrade scripts of the time, and where those differ (maxmessagesize, usecache, protocolimap, smtpallowincorrectlineendings and half a dozen more) the older installation is genuinely different and is listed. A row nothing reads any more (useordb, usespamhaus, tarpitcount, filters, imapport, relaymode) is not reported at all. The marks on an entry. secret: a setting whose name says its value is a secret is listed by name, with (set) or empty, and never shown. A key file, such as RestApiPrivateKeyFile, is a path and is shown. known false: the server holds a value and ships no default for it, such as a key stored for a script of your own. hmctl prints (none: this server does not read it). overridden: [SettingsOverride] is forcing the value. Such a key is listed even when neither store holds anything for it; hmctl prints (forced from [SettingsOverride]). environment is variable or file, and hmctl prints (set by the environment). profile names the setting profile whose apply wrote the value, while the setting still holds it. Which values these are. The value stored, not the value the server settled on where the two differ: a PasswordHashIterations outside the range the server accepts is reported as HM5561 and the built-in work factor used; an unusable PreferredHashAlgorithm is reported as HM5528 (or HM5607, for Argon2id on a build whose OpenSSL has none). Who may see it. The administrator password, and nothing else. An API key of any scope is refused with a 401, and an account holding an administrative role, an auditor included, with a 403, because the list includes commands the server runs: AutoBanCommand runs as the service account. The feature changes nothing. It does not compare two servers (two hmctl config changed --json answers and diff do), and it does not say when or by whom a setting was changed; the audit trail does (GET /api/v1/audit?object_type=setting). When a saved setting takes effect Since 6.3.4. Until then every [Settings] key was latched when the service started. Now a save publishes: the store takes the value, the server rebuilds its settings from the rows, and every key whose readers can take a new value is in effect when the save returns, whichever door saved it. Every key carries one of three marks: Mark Keys What it means now 211 A save applies it reinitialize 28 Bound into something the servers build when they start, so it waits for a reinitialise: Apply now in the Control Panel or the Deck, POST /api/v1/server/reinitialize, or Application.Reinitialize over COM restart 31 Read once per process: 2 [Settings] keys and the 29 bootstrap keys (20 [Database], 5 [Directories], 3 [Security], 1 [GUILanguages]) The 28 that wait for a reinitialise. What is rebuilt Keys The REST listener RestApiBindAddress, RestApiPort, RestApiCertificateFile, RestApiPrivateKeyFile The metrics listener MetricsServerBindAddress, MetricsServerPort, MetricsServerCertificateFile, MetricsServerPrivateKeyFile, MetricsServerAuthPassword, MetricsServerAuthToken, MetricsServerAuthUsername The web-services listener WebServicesBindAddress, WebServicesHttpPort, WebServicesHttpsPort, WebServicesCertificateFile, WebServicesPrivateKeyFile The ManageSieve listener ManageSieveServerBindAddress, ManageSieveServerPort The SSL contexts of the SMTP, IMAP, POP3 and ManageSieve listeners and the outbound client TlsCipherSuites13, TlsKeyExchangeGroups, TlsSessionCacheSize, TlsSessionTicketsEnabled, TlsSessionTimeoutSeconds, TlsTicketKeyRotationSeconds ACME, the database pool, the fetch queue AcmeEnabled, AcmeCertificateDirectory, DatabaseStatementTimeout, MaxNumberOfExternalFetchThreads The 2 that wait for a restart are ServiceAccountName and ServiceAccountPassword, which are retired: since 6.3.4 hMailServer.exe /Register takes the account from its command line alone, and a stored value is shown as pending until the next start. The bootstrap keys are never in the store, so none is ever pending. Some keys are now because a subscriber re-latches them. MaxSubmissionsPerIPPerMinute and MaxOutboundPerDestinationPerMinute start each limit's one-minute window again, so a saved limit counts from when it was saved. A cache of data is not a setting: a changed DNSServer is used for the next lookup, and an answer already cached stays until its TTL runs out. Pending. A reinitialize or restart key whose stored value is not the value the server is running on is pending. The list is recomputed at every request and never remembered, so a value put back before anything applied it is not pending. Where What REST GET /api/v1/server/settings/pending answers [{name, stored, running, applies, secret}], and applies and pending come on every /api/v1/settings/ini/{name} answer. The administrator password's route COM Settings.IniSettingApplies(name), Settings.IniSettingAppliesAll, Settings.PendingIniSettings Control Panel Applies on save, After reinitialize or After a restart under each field kept in the store, and a pending bar with Apply now Control Deck The same words as a badge, and Apply now above its Settings view A name the loader does not read (UseLanguage, a key a script of your own reads, a misspelling) has no mark: COM's IniSettingApplies refuses it and REST answers applies: null. Apply now is a reinitialise. Every service stops and starts again in place, which takes every reinitialize key and not a restart key. It asks first. A reinitialise ends maintenance mode, and POST /api/v1/server/reinitialize answers {"reinitializing": true, "drained": true} when it ended a drain. The Deck's session ends with the REST listener it was using; the Control Panel's COM session survives. What a save does not do. It does not re-read the bootstrap sections or [SettingsOverride]; a reinitialise or a start re-reads the override section and announces it as HM5807. It does not reconcile the file against the table; that is a start's and a reinitialise's only. And a value stored by another process (another node on the same database, or hmailserver --set-setting while the service runs) reaches this server at the next save here, a reinitialise or a restart, because nothing watches the table. hmctl has no pending verb. On Linux the regression fixture alone proves the marks, with the three COM members answered over REST; the internal tester that swaps settings under load does not run there. One value, three levels Since 6.3.4 (schema 6088). Twenty-seven settings may be set for the server, for a domain and for an account, and every place that shows one shows what is in effect and where it came from. For an account: its own value, else its domain's, else the server's. Every setting on the list is exactly one kind: Kind How many The rule Who sets a level below the server Limit 15 A level below may tighten the level above and never loosen it. A looser value is refused when it is written; a value the level above has since tightened past is capped when it is read Anybody who administers that level: a domain administrator, a key restricted to the domain Gate 4 The deepest level that holds a value wins, either way The server administrator alone; a domain-restricted key is refused with 403 Preference 8 The deepest level that holds a value wins The domain's administrators, and a domain-restricted key The last column is the rule for the levels held in hm_settingoverrides. A field the domain already had keeps the authority it had, whatever its kind: the domain's maximum message size and the domain's retention are the server administrator's. Kind Settings Limits max_message_size_kb, session_idle_seconds, session_lifetime_seconds, session_one_per_device, session_bind_to_address, quota_grace_percent, quota_send_stop_percent, recall_days, recover_deleted_days, sending_max_messages, sending_max_recipients, reader_fetch_accounts_max, reader_sender_addresses_max, reader_masked_addresses_max, reader_offline_days_max Gates plus_addressing_enabled, openpgp_portal_enabled, zero_access_allowed, directory_user_editable Preferences quarantine_digest, quarantine_digest_hour, external_tag_subject, external_tag_header, impersonation_action, quota_warning_percent, country_policy, message_retention_days A server setting not on the list is the server's alone: its name is refused on the domain and account routes (400, unknown field) and by COM's SettingOverride. Of the [Settings] keys only QuotaWarningPercent has levels. Where each level is held. A field the domain or the account already had, such as the domain's MaxMessageSize or MessageRetentionDays, stays where it was, with the meaning it always had; its 0 or -1 is the level holding nothing. The rest are rows of hm_settingoverrides, where no row means "take the level above". An owner's rows are cached for sixty seconds; every write through the interfaces drops the cache at once, and so does POST /api/v1/settings/cache/clear or COM Cache.Clear. A row put in by hand that the interfaces would refuse is treated as holding nothing and reported as HM7311. Nothing changes for a domain or an account that holds no value of its own. Where What REST GET /api/v1/settings/levels lists the twenty-seven. GET /api/v1/domains/{domain} and GET /api/v1/accounts/{address} answer <name>_effective and <name>_source, and <name> where the level is held in the overlay. For a level held in the overlay a PUT takes a value of the setting's own JSON type, and null makes it inherit; for a level held in a field of its own null leaves it unchanged, and the field's own 0 or -1 is "take the level above" COM (Windows only) Domain.SettingOverride(name) and Account.SettingOverride(name), an empty string for "take the level above", written at once and not by Save; EffectiveSetting(name) and EffectiveSettingSource(name) hmctl hmctl domain set <domain> <name> <value\|inherit> and hmctl account set <address> <name> <value\|inherit> Control Panel (Windows only) A Levels tab on the domain and account dialogs, with "In effect: ..., from the ..." beside each setting and Save levels now Control Deck The same card in its domain and account editors A saved level applies at once, at every level and from every door, because the resolver is asked when the value is used (at MAIL FROM, when a session is checked) and never latched. Deleting, backing up and moving. Deleting an account or a domain deletes its rows; a delete that cannot remove them is reported as HM7310 for a domain and HM6109 for an account. The XML backup carries the rows, and on restore a row the build refuses is left out and reported as HM7312. A mailbox moved to another domain keeps its own values. What is thinner than it sounds. Attachment blocking, web push, passkeys and account recovery have no levels. The domain's session seconds and switches and its quota grace and send-stop shares could always be set looser than the server's and still can; the ceiling binds the account below them. The maximum message size is resolved at MAIL FROM and at IMAP APPEND; the EHLO SIZE advertisement, the webmail's own send and the send API stay the server's. The session policies are the browser sessions' (the webmail, the portal, the Deck); IMAP, POP3 and SMTP sessions have none. country_policy cannot be set to empty over COM, where an empty string means "take the level above"; REST's "" does it. Twelve of the twenty-seven are proved by what the account and its domain answer rather than by driving the behaviour per account, and the levels' own regression fixture is Windows-only. When a setting appears to do nothing Is it pending? A reinitialize key waits for Apply now and a restart key for a service restart. Was it typed into hMailServer.ini? The stored value wins and HM5804 names the key. Store it instead. Is the key spelled exactly as the code reads it, and is a boolean literally 1? An unknown key is the default, and only 1 is true: 2, yes and true all read as false. Does it need a companion? SRSEnabled needs SRSSecret, ACME needs AcmeDomains, autoconfig needs WebServicesHttpPort or WebServicesHttpsPort, and MTA-STS hosting needs the HTTPS one. Does the error log show HM5807 or HM5808? A [SettingsOverride] section is forcing a value. Where to read more The tour: https://gitlab.com/hmailserver/hmailserver/-/wikis/Settings-Overview. Every key with its default, validation and mark: https://gitlab.com/hmailserver/hmailserver/-/wikis/Settings-Reference. Where the two disagree, the reference is generated from the code and is right. The three documents behind this topic: https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/ChangedFromStock.md, https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/SettingsApply.md and https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/SettingLevels.md The public documentation's settings chapter: https://www.progressiverobot.com/hmailserver-documentation/#21-settings-reference-hmailserverini On this forum: the Control Panel on Windows, hmctl and the PowerShell module, the hmailserver command line, the Control Deck, Upgrading to 6.3.4 and hMailServer on Linux.