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
    181 Views
    ProgressiverobotP
    Every documentation topic on this forum, by the category it sits in. Each one is written from the project's wiki and the operator documents at the release tag, for hMailServer 6.3.4, and links the pages it was written from. The long tail is in the wiki and the operator documents; the public documentation is the same material as one long page. A question that none of these answers belongs in the category it is about, as a new topic; Read this before you post says what a support request needs. Running hMailServer General support Read this before you post: what a support request needs Where to post what: a map of the forum categories Diagnosing stalled mail: a sender that times out, and a message that never leaves Reading the logs: levels, files, the Logs page, following a connection, and fail2ban Backup and restore: what is in the archive, what is not, and restoring one mailbox Settings: what is not stock, when a save takes effect, and the three levels The Control Panel on Windows: every page, what it writes, and what applies when IMAP and POP3 on the server: capabilities, switches, shared mailboxes, search limits Troubleshooting by symptom: no mail in, no mail out, a service that will not start Every port hMailServer listens on or connects to, and the firewall rules to match Installation, migration & upgrades Upgrading to 6.3.4: any 5.3 or later goes straight there Moving to new hardware, and why Windows to Linux is a rebuild Installing on Windows: wizard pages, service account, profile and the first hour Choosing a database, creating it, encrypting the link and leaving the built-in one Your first domain and mailbox, and the DNS records your domain needs Bringing mailboxes in: IMAP mirror, Google, Microsoft 365, mbox, Maildir and PST Unattended installs, winget and Chocolatey, and configuration from the environment Warm standby and high availability: what a second machine can and cannot do Message storage: compression, de-duplication, object storage and encryption at rest The update check and the live update: feed, verification, window and rollback Upgrading from 5.x, 6.2 or 6.3: the 24 changes without a switch, and rolling back Linux hMailServer on Linux: what you get and what you do not First install on Linux: the order of operations, Ubuntu to Enterprise Linux The hmailserver command line on Linux: every verb, who runs it, the files, the unit In a container or on Kubernetes: the image, Compose, Helm and the environment Databases on Linux: PostgreSQL or MariaDB, the client, TLS and the upgrade floor Webmail, portal & Control Deck The webmail portal at /portal The Control Deck: browser administration at / Calendars, contacts, the directory and shared address books over CalDAV and CardDAV Signing in: passwords, second factor, passkeys, single sign-on, recovery, devices The webmail in depth: search, offline mail, expiring messages, send-as and recovery Connecting a mail client: IMAP, POP3, SMTP submission, app passwords and the ports TLS, certificates & deliverability Let's Encrypt certificates are automatic: the built-in ACME v2 client Being trusted by the servers you send to TLS on the listeners: your own certificate, versions, ciphers and client certificates Requiring TLS: MTA-STS and TLS-RPT, DANE with ACME, and remote domain policies DKIM rotation, DMARC and TLS reports, complaints, bounces and the reputation watch Outbound shaping and IP warm-up: one slow destination must not stall the queue End-to-end encryption: S/MIME authority, OpenPGP and WKD, zero access, sealed mail Anti-spam & message filtering The anti-spam stack end to end: score, greylisting, SPF, DKIM, DMARC, DNSBL, SURBL Allow and block lists, digests, spam traps, country blocking and impersonation Viruses and dangerous attachments: ClamAV, custom scanners, the sandbox, sealed mail Mail flow rules, the outbound content policy and what a domain does to a message Guides & tutorials Quotas, retention and the archive: how much mail the server keeps, and for how long Administrator roles end to end: the 27 permissions, giving a role, where each works How the service is built and what runs out first: pools, connections, large folders IP ranges, auto-ban, lockout, tarpits, sending limits, send-as and password storage Behind a gateway, load balancer or reverse proxy: relays, PROXY, XCLIENT and HTTPS Hardening a public mail server: secure defaults, checklist, sealed secrets and roles Compliance: eDiscovery, legal holds, personal data, erasure and the audit trail How email works, and what hMailServer is and is not Relocating an installation to another drive, and moving or renaming a mailbox Automation & integration Scripting & the COM API Event scripts: every event and its arguments, the limits, and the 6.3.4 security fix The COM API on Windows: object model, Save() since 6.3.4, roles and the Import Tool REST API & integrations The REST API: turning it on, credentials, roles, one-time codes, OpenAPI and examples Active Directory and LDAP sign-in, directory synchronisation and OAuth2 bearer tokens hmctl and the PowerShell module: shell administration and accounts from a CSV file Integrating applications: the send API, mail in to a webhook, and outbound webhooks SCIM provisioning from Entra ID and Okta, and single sign-on for the organisation Rules, Sieve & routing Rules and Sieve: what runs when, vacation replies and the Run function action Routes, relays and smart hosts, OAuth2 relaying, restricted delivery and recall Aliases, distribution lists, membership queries, mailing lists and public folders Journaling and dual delivery: a compliance copy, and what a journal rule does not see Monitoring, metrics & logs Monitoring and health: Prometheus metrics, health probes, Grafana, OTLP and SNMPv3 Alerts, the audit trail, syslog and the journal, and per-domain reports Releases The current release, its announcement and the release history are in Announcements & releases.
  • 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
    132 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) signs an authenticated submission as another hosted domain only when the 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; otherwise the mail is sent unsigned as that domain, so give such an account the alias or the grant before upgrading. Mail from outside whose From names a hosted domain is no longer signed as it, nor is an alias's or a distribution list's copy of it to an outside address; where such a message did not pass DMARC for the organisation its From names when it arrived, a copy whose envelope sender is at this server's host name, or at a hosted domain or a subdomain of one, in that organisation is not sent where an alias or a distribution list expands to an outside address, a managed list does not distribute a posting whose From names the list domain's organisation, and a distribution list's bounce address is not given to it; a managed list's copies of a posting from outside are not signed as the list's domain when an address of the list domain's organisation is in its From fields. Creating a domain alias is now the server administrator's: a credential confined to named domains is refused it, over REST and over COM. See the 6.3.7 announcement. There are three ways of making one address reach somewhere other than a mailbox of the same name - an alias, a distribution list and a route - and one way of giving several people the same folder. This topic covers all of them as they are in 6.3.4 (27 September 2026, schema 6104): aliases, domain aliases and plus addressing; distribution lists, who may post to them and how moderation works; the membership query, which lets a list define its own members; the list manager, which turns a distribution list into a mailing list people join and leave themselves; and public folders with their permissions. The membership query and the list manager are new in 6.3.4; the distribution list itself is in every release. The one thing to know first: the address is resolved in one function (RecipientParser::CheckDeliveryPossibility), in a fixed order, and an account always wins. The order an address is resolved in At RCPT TO the server works through this, and stops at the first answer: Domain aliases rewrite the domain. Plus addressing trims the local part. Account? If found, deliver to the mailbox. Alias? If found, follow it and start over, up to 25 hops. Distribution list? If found, expand to the members, or to the moderator. Route? If found, send to the route's target server. The domain's postmaster address, the catch-all: when nothing above matched, the resolution starts again with that address, which is why a mistyped address is delivered there rather than refused. If it is empty, 550 Unknown user. Since 6.3.4 one question is asked before all of these: is the address, or its domain, routed to a webhook? A webhook route that only posts takes the address's mail instead of its mailbox, alias or list - a list whose address is routed never sees the message, nor applies its posting rules to it. Route the addresses you mean rather than a list's whole domain; mail in to a webhook has the routes themselves. Aliases An alias is a second address that delivers to an existing target. In the Control Panel: Accounts & domains → Domains, select the domain, then the Aliases panel. The Control Panel and COM are Windows-only. On Linux, where there is neither, the Control Deck is the administration people click through, and it is the front end of the REST API, which both platforms have: What REST route A domain's aliases GET and POST /api/v1/domains/{domain}/aliases; DELETE /api/v1/aliases/{address} A domain's other names GET and POST /api/v1/domains/{domain}/domain-aliases; DELETE on .../{name} beneath it A domain's lists GET and POST /api/v1/domains/{domain}/lists; PUT and DELETE /api/v1/lists/{address} A domain's plus addressing PUT /api/v1/domains/{domain} (plus_addressing_enabled, plus_addressing_character) The Control Panel paths and COM properties below are for Windows. Field COM property Meaning Alias address Alias.Name The address people send to. Must be in this domain Target Alias.Value May be anything: another account here, another alias, a distribution list, an address covered by a route, or an address at a different server Active Alias.Active Unticked: RCPT TO is answered 550 Alias is not active. - not passed on to the catch-all The target replaces the address and resolution starts again, so an alias may point at an alias, which may point at a list. Beyond 25 hops the answer is 550 Mail server configuration error. Too many recursive forwards. An alias is only consulted when no account has that address. Creating sales@ as an account after sales@ exists as an alias silently changes where the mail goes. An alias to an external address is treated as inbound mail, not as relaying, so [email protected] → [email protected] works for external senders without opening a relay. Since 6.3.4 three more kinds of second address exist, and they are the person's own rather than the administrator's: the alias a mailbox move leaves (renaming or moving a mailbox keeps the old address as an alias of the new one by default); masked addresses (schema 6067), which the person makes in the webmail for one shop or sign-up and which, switched off, are refused as an address that never existed; and verified send-as (schema 6066), which receives nothing here - it is a From address, not an alias. Domain aliases. Accounts & domains → Domains → Properties → Names. The rewrite is total: [email protected] becomes [email protected] before anything else is looked at. You do not create per-address aliases for an alias domain; the sender address is rewritten too, which is what lets a list in membership mode recognise a member who posts from the alias domain; and the alias domain needs its own MX record. Since 6.3.7 adding one is the server administrator's: a credential confined to named domains - such as a domain administrator, or an API key issued for its domains - is refused, over REST and over COM, because a domain alias decides what names the server accepts mail for. Plus addressing is off by default and enabled per domain on Accounts & domains → Domains → Properties → Limits (default character +). With it on, [email protected] resolves to [email protected], and the full original address is still in the To: header. It applies to the recipient address only, after domain aliases and before the account, alias and list lookups. When off, alice+invoices@ is an unknown address. Distribution lists A distribution list delivers one message to many recipients. In the Control Panel: the Distribution lists panel of the domain, then Properties and Members. Mode Stored value Who may post Refusal Public - anyone can send 0 Anyone at all - Membership - only list members can send 1 Any address on the member list, after domain aliases are applied to both sides 550 Not authorized sender. Announcements only 2 Only the address in Require sender address 550 Not authorized owner. Anyone in the domain can send 3 Any sender whose domain is the list's own 550 Not authorized domain. (on any mode) Require SMTP authentication flag An unauthenticated session is refused first 550 SMTP authentication required. "Anyone in the domain" is a claim, not a proof. MAIL FROM is free text, so an outsider can assert a local address. Tick Require SMTP authentication with that mode or the list is open to the Internet. A fifth mode, 4, "Anyone with a server account can send", existed only in the type library and behaved as Public; put_Mode now refuses the value and the option is gone from the dialog. One more check runs for every mode, Public included: the poster must be allowed to reach every recipient of the list, so a list containing a members-only list the poster is not in is refused as a whole. Moderation. Set a Moderator address and a posting the rules above would refuse is accepted with 250 and forwarded to the moderator instead, stamped X-hMailServer-Moderation. The moderator approves it by resending it to the list from an authenticated session; that authentication is the security boundary and is unconditional. Three failures are numbered in the log: 6360 (the moderator was removed between the check and the expansion), 6361 (the moderator address does not resolve; the recipient is refused with 550) and 6363 (the list is its own moderator). Bounce address. Empty means bounces go to the poster. Set, it becomes the envelope sender of every copy, so a dead subscriber's bounce reaches the list owner, and an external member's server evaluates SPF against the list's own domain. A bounce address pointing at the list itself is ignored, with error 6362. Headers. An unmanaged list adds List-Id, List-Post (the literal NO for an announcements-only list), List-Help, List-Owner and a mailto:-only List-Unsubscribe, before DKIM signing. Nothing is added when the message already carries a List-Id, or when the list or owner address is not US-ASCII. A list that defines itself: the membership query, since 6.3.4 Schema 6074. A list can carry a membership query: one short text that says who its members are, answered against the server's accounts at the moment a message is sent to it. [email protected] with the query domain reaches every active account of the domain on the day, including the person who joined this morning and not the one switched off last night. Query Members (empty) A static list: the addresses under Recipients and nothing else domain Every active account of the list's own domain domain:other.example Every active account of that domain, which this server has to host pattern:sales-*@example.com Every active account whose address matches the wildcard; * is any run, ? exactly one, case ignored everyone Every active account of every active domain on the server It is a union with the list's own recipients, never a replacement. An address that is both is delivered to once. It answers accounts only - not aliases, other lists or outside addresses - and never a disabled account or an account of a disabled domain. It is resolved once per SMTP transaction, not once per RCPT TO, and nothing is stored. Who may set which form. A domain administrator, or an API key with a domain scope, may set domain or a pattern: in a domain it holds. everyone, domain: naming another tenant's domain, and a pattern whose domain part is itself a wildcard are the server administrator's. The same rule governs reading the answer: GET /api/v1/lists/{address}/members answers 403 to a confined credential when the query reaches past its domains. A query is judged when it is saved. domain: naming a domain this server does not host, a pattern: with no @, or any text that is none of the five forms is refused, by the same sentence over COM, over REST and in the Control Panel. "Answers nobody" and "could not be answered" are different. A query whose named domain has been deleted is not an error at delivery: the message goes to the list's rows and code 6980 is logged. But when the accounts cannot be read - the database locked by a backup - the recipient is refused 451 4.3.2 and the sender retries, with code 6982, and the members route answers 503. It is set as Membership query in the Control Panel's list dialog and the Control Deck, membership over REST, DistributionList.Membership over COM, and hmctl list update [email protected] --set membership=domain. The Deck's list editor shows Delivers to now, each address marked Kept by hand or From the query. The list manager: a mailing list, since 6.3.4 Schema 6092. A distribution list with the list manager switched on is one people join and leave themselves. Nothing changes for a list until it is switched on: every list an upgrade finds is unmanaged. On the list: Managed in the Control Panel's list dialog, managed in the Deck's list editor and on PUT /api/v1/lists/{address}, or DistributionList.Managed over COM. The links. The confirmation page, each member's page, the owner's approval page, the subscribe form and the archive are served by the REST listener, which is off until RestApiPort is set (turning the REST API on). Make it reachable over HTTPS, then set list_base_url (ListBaseUrl), such as https://lists.example.com. Empty, the default, offers no link at all. Only an https:// base gives RFC 8058's one-click. DKIM on the list's domain. The copies are signed as that domain after the list's fields are in place, so the signature covers List-Unsubscribe and List-Unsubscribe-Post. With DKIM off for the domain the copies go unsigned and the providers do not offer the one-click. Address What it does [email protected] Post to the list [email protected] Ask to join, and confirm by replying [email protected] Ask to leave: the member is sent a link and a token, and nothing more happens until they follow it [email protected] The help text [email protected] The person who runs the list [email protected] Delivery reports; with VERP, [email protected] None of them is an account; an account, an alias or a list of exactly one of those names is the administrator's and wins over the manager. Joining. subscribe_policy is 0 closed (the default: the administrator adds members), 1 confirm (the address asked for is sent a confirmation with a token) or 2 moderated (after the address confirms, the owner approves on a page, signed in to the webmail as the owner's own account, or on the Deck; a reply by mail approves nothing). The list answers at most twenty requests an hour from one requester and two hundred an hour in all. Delivery. Each member chooses each message, one digest a day, or none. With digest on, one digest goes at digest_hour (7 by default, the server's local time), at most 100 messages and 10 MB. With archive at 1 (members) or 2 (public), every distributed message is kept and shown by month, as text only, never as HTML. The rows of hm_listarchive are not in the backup. What a member's copy carries, over an https:// base with the archive on: List-Id: <team.example.com> List-Post: <mailto:[email protected]> (NO for an announcement list) List-Help: <mailto:[email protected]?subject=help> List-Subscribe: <mailto:[email protected]?subject=subscribe>, <https://lists.example.com/lists/[email protected]/subscribe> List-Unsubscribe: <mailto:[email protected]?subject=unsubscribe>, <https://lists.example.com/lists/unsubscribe?t=...> List-Unsubscribe-Post: List-Unsubscribe=One-Click List-Owner: <mailto:[email protected]> List-Archive: <https://lists.example.com/lists/[email protected]/archive> Precedence: list X-Loop: [email protected] X-hMailServer-List-Member: <a tag, see Bounces> Bounces. A 5.x.x is a hard bounce, a 4.x.x a soft one; a delayed warning counts for nothing. Anybody can write to the bounce address, so a report is believed only when it carries the member's tag, X-hMailServer-List-Member. Setting REST key Default ListHardBouncesToUnsubscribe list_hard_bounces_to_unsubscribe 1 ListSoftBouncesToUnsubscribe list_soft_bounces_to_unsubscribe 5 ListBounceWindowDays list_bounce_window_days 30 A member who reaches either count is set to none - paused, never unsubscribed - and the owner is told once. Public folders Accounts & domains → Public folders, in the Control Panel on Windows, creates shared IMAP folders; public folders have no REST route yet, so Linux has no administrator's page for them. On either platform a client holding the a right manages a folder's permissions over IMAP. Public folders live in their own namespace, #Public, and sub-folders are made by typing the whole path with the hierarchy delimiter, . by default: #Public.Team.Archive. #[email protected] is a different namespace: one account's own folders, shared. Each permission entry grants RFC 4314 rights to an account, a group or Anyone (every authenticated account). Right Letter What it allows Lookup l The folder appears in LIST/LSUB. Without it the folder does not exist as far as that user is concerned Read messages r SELECT and STATUS Keep seen/unseen state s Set or clear \Seen Set flags w Flags other than \Seen and \Deleted Insert i APPEND and COPY into the folder Post p Send mail to the folder's submission address Create sub-folders k CREATE beneath it Delete folder x DELETE Delete messages t Set or clear \Deleted Expunge e EXPUNGE Administer a SETACL, DELETEACL, GETACL, LISTRIGHTS An owner has every right implicitly. For anyone else the server walks up from the folder to the nearest ancestor that has any ACL rows, then uses the row for the account, else for a group the account is in, else for Anyone. The nearest ancestor with any rows wins, and rows are not merged. Adding one permission to a sub-folder removes everyone else's access to it. \Seen is shared in a shared folder, not per user. Grant lrs and delegates share one read/unread state; grant lr and they leave no trace. Turning ACL enforcement off opens public folders only. It does not extend to #Users, which stops existing entirely. What it does not do The list manager does not rewrite From for DMARC. A posting from a domain that publishes p=reject reaches outside members with a From that fails DMARC when the list changed the message, and Gmail and Yahoo may refuse it. No posting from the web, no list password, no approval by reply, no NNTP, no IMAP access to an archive, no weekly digest, and one owner address per list. A membership query has no directory-attribute form and no group: form, and no query can name another list's query. The list manager's fixture is not linked into the Linux test suite: MailingLists.md says its thirty-four tests are of the Windows suite. Where to read more Wiki: Aliases, Lists and Public Folders, Mailing Lists, Mail In to a Webhook Documents at the 6.3.4 tag: DistributionLists.md, MailingLists.md The public documentation: chapter 13 On this forum: Being trusted by the servers you send to for DKIM and DMARC, the 6.3.4 release announcement, the webmail in depth for masked addresses and send-as, and IMAP on the server for shared mailboxes
  • 0 Votes
    1 Posts
    131 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) holds a credential confined to named domains - such as a domain administrator, or an API key issued for its domains - to a public address when it changes a domain's relay host, so it cannot point the server's delivery at the server's own network, and makes a domain's or an account's rule's Send using route and Bind to address actions the server administrator's to set; an account given the serveradmin role with a list of domains is, over COM as over REST, confined to those domains, so a script's account that edits the routes or the relayer needs the role with no list. See the 6.3.7 announcement. There are four ways this server can decide where an outbound message goes, and they are tried in a fixed order. Almost every "it went to the wrong server" question is answered by knowing that order, so this topic starts with it. It then covers the smart host and its failover, per-domain relays, routes and what they do to an unknown recipient, sending an OAuth2 token to a relay and what 6.3.4 changed about it, the COM change that is in effect only after Save(), and three things new in 6.3.4 (27 September 2026, schema 6104) that are not about where mail goes: remote domain policies (one paragraph here; the fuller treatment is Requiring TLS: MTA-STS, DANE and remote domain policies) and restricted delivery, which decide whether mail may go, and message recall, which withdraws unread copies already delivered on this server. The one thing to know first: a route is a statement about the destination and beats everything; a per-domain relay is a statement about the sender; the server-wide relayer is the fallback for mail nothing else has an opinion about. The order of decision When a message is ready to leave, its recipients are grouped by domain and the server asks: Did a global rule apply the Send using route action? Then every recipient goes to that route's target host and nothing else is consulted. Is there a route whose domain matches the recipient domain? Wildcards are allowed. The route's host, port, connection security and credentials are used. Does the sender's local domain have a relay host of its own? That domain's relay host, port, security and credentials. Port 0 means 25. Is a server-wide SMTP relayer configured? The relayer's host, port, security and credentials. Port 0 means 25. Otherwise, an ordinary MX lookup for the recipient domain, then A or AAAA for each MX host. Targets that turn out to be identical are merged: a message for four domains that all relay through one smart host opens one connection, not four. A bounce never picks up a per-domain relay, because a bounce has no envelope sender, so there is no sending domain to consult. Since 6.3.4 one question comes before all of these for inbound mail: an address or a hosted domain routed to a webhook has each message posted to an application, instead of being delivered (deliver off) or as well (deliver on). That is a different thing from the routes below, which send a domain's mail to another mail server; mail in to a webhook has the webhook routes. Sending through a smart host On Windows, configure it in the Control Panel at Mail flow & delivery → Delivery of e-mail → Relayer. The Control Panel and the COM API are Windows-only; over REST, which Linux has too, the relayer's settings are keys of PUT /api/v1/settings (smtp_relayer_password is write-only). Setting COM name Default When it is wrong SMTP relayer SMTPRelayer empty: MX lookup A host that does not resolve gives the bounce text "The host specified as SMTP relay server could not be found" with 4.3.5, a temporary failure, so the message is retried Port SMTPRelayerPort 25 Looks like a connection timeout in the SMTP log Connection security SMTPRelayerConnectionSecurity None Required against a host without STARTTLS defers every message Relayer requires authentication SMTPRelayerRequiresAuthentication off Off against a host that requires it gives 530/535 and the mail stays queued Number of delivery retries SMTPNoOfTries 4 0 means one attempt and then a bounce Minutes between retries SMTPMinutesBetweenTry 60 Outbound delivery security SMTPConnectionSecurity STARTTLS (optional) Applies to MX-resolved delivery, not to the relayer Several smart hosts. Separate the names with a vertical bar: smtp1.example.net|smtp2.example.net Each name is resolved in turn and every address it resolves to is appended, giving one ordered candidate list tried top to bottom. A single name with several A records already gives that failover. All hosts in the list share the port, security and credentials. A permanent 5xx rejection is a real delivery failure and the next host is not tried: trying it would deliver the message twice if the first server had in fact accepted it. Relaying everything through an ISP: Open Mail flow & delivery → Delivery of e-mail → Relayer. SMTP relayer: smtp.isp.example. Port: 587. Connection security: STARTTLS (required) - required, not optional, so a downgrade cannot send your credentials in clear. Tick Relayer requires authentication and fill in the user name and password. Save changes, send one message, and read Monitoring & troubleshooting → Live logs. The line that proves the relayer was used is: "SMTPDeliverer - Message 41: Relaying to host smtp.isp.example." If the message went to an MX host instead, the relayer field is empty, or a route or a per-domain relay took precedence. Per-domain relay. Accounts & domains → Domains → Properties → Relay sets where mail from that domain leaves through. It is for a server hosting several independent domains where each has bought its own delivery provider. An empty host means the domain has no opinion. Since 6.3.7 a credential confined to named domains - such as a domain administrator, or an API key issued for its domains - may set the host to a public address only, over REST and over COM; a host on the server's own network is the server administrator's to set, and one the server administrator set stays the domain administrator's to save, since only a change of the host is judged. Routes A route overrides delivery for a specific recipient domain. Mail flow & delivery → Routes: type the domain and target host, press Add, then Properties for everything else. Over REST the routes are /api/v1/routes. Field Default Note Domain name - Wildcards are matched, so *.partner.example works. The first matching route in name order wins; a wildcard broader than you meant silently captures other domains Target SMTP host, port -, 25 A name or a literal IP address; the host1\|host2 failover applies here too Number of delivery retries 3 Replaces SMTPNoOfTries. The delivery path applies it as tries − 1, so a route set to 3 schedules 2 further attempts Minutes between retries 10 Where one message has recipients on several routes, the largest count and the largest interval win Deliver to all addresses on Off: only the addresses listed are routed. Off with an incomplete list is the single most common route mistake Connection security None None, SSL/TLS, STARTTLS optional, STARTTLS required Treat sender domain as local off Turn it on only for a domain you actually control Treat recipient domain as local off On, unauthenticated senders may send to it from the Internet range What a route does to an unknown recipient: Route setting Domain has a catch-all? Address in the route's list? Result Deliver to all addresses - - Routed Specific addresses only no no 550 Recipient not in route list. Specific addresses only yes no Delivered to the catch-all, not routed Specific addresses only yes yes Routed; the catch-all does not intercept it Routes also make ETRN work: when a remote site issues ETRN <domain> and a route matches, the messages held for it are released for immediate delivery. OAuth2 to a relay A route or the SMTP relay whose host is on OutboundOAuth2Hosts (default smtp.office365.com) is sent AUTH XOAUTH2 with the server's own token: a client-credentials token fetched from OutboundOAuth2TokenUrl with OutboundOAuth2ClientId, OutboundOAuth2ClientSecret and OutboundOAuth2Scope (default https://outlook.office365.com/.default), or OutboundOAuth2FixedToken used verbatim instead of fetching. The default host list is inert on its own: with OutboundOAuth2TokenUrl and OutboundOAuth2FixedToken both empty, no bearer is set and the delivery proceeds with LOGIN and the password, with no log line. Since 6.3.4 the token is sent over TLS only. A route or the SMTP relay whose host is on OutboundOAuth2Hosts and whose connection security is none - or STARTTLS optional against a server that does not offer it - used to send AUTH XOAUTH2 with the token in the clear. Such a delivery now sends nothing in its place, fails with a temporary 450 4.7.0, and the error log says to set the security to TLS or STARTTLS required (HM5912); the message waits for the retries the route allows. Exchange Online offers STARTTLS on 587, so a route set up as Microsoft documents it is not affected. Also since 6.3.4, a relay that refuses the login no longer gets the token into the bounce, which now reads hMailServer sent: AUTH XOAUTH2 <Token removed>; the logs write AUTH XOAUTH2 ***. These are [Settings] keys, and since 6.3.4 the database is the settings store: a line edited in the file changes nothing, the stored value is used and named as HM5804, and the line is put back where the service can write the file (when a saved setting takes effect). Change them in the Control Panel, with PUT /api/v1/settings/ini/{name} or with hmctl ini set. A COM change is in effect only after Save() Since 6.3.4 a COM change to a route or an incoming relay is in effect only after the object's Save(), and a property set and never saved changes nothing; the list of objects this applies to does not name the server-wide SMTP relayer's settings. COM is Windows-only, so this concerns scripts on Windows: the COM API topic has the whole list, what an object read before the save shows, and error 7390. Remote domain policies, in one paragraph Since 6.3.4 (schema 6045). A route says where mail for a domain goes; a remote domain policy says what this server will and will not do when it talks to one domain it does not host, however that domain's server was found, and it applies to a routed delivery exactly as to an MX one. One record per name or pattern (bank.example, *.example, *), the most specific active one governing, holds the outbound TLS the domain demands (none, encrypted, verified or dane; a delivery that cannot meet it is deferred with 4.7.0, never sent in the clear), TLS required of its senders (530 5.7.0), a largest message (5.3.4), connection and rate ceilings (deferred with 4.4.5), whether automatic replies and forwards may go there, a recipient callout for a domain this server is a backup MX for, and gateway encryption to the partner's own key. It is server-administrator only, and GET /api/v1/remote-domains/effective?domain=mail.bank.example answers which record a delivery will use. Restricted delivery, since 6.3.4 Schema 6084. Accounts, or a whole domain, that may only exchange mail with named domains: a school whose pupils write to the school and to nobody else. It is not a route: it decides whether mail may go, not where. The list is the domain's: Domain.RestrictedDomains over COM, restricted_domains on GET/PUT /api/v1/domains/{domain}, the Restricted delivery tab of the Control Panel's domain dialog, the same group in the Deck's domain editor. At most 2,000 characters, names separated by commas or spaces, ASCII only, an internationalised name in its xn--... form. Written as Covers partner.example that domain, and nothing beneath it .example example and everything beneath it, anchored at a label boundary An account follows its domain unless it says otherwise (Account.RestrictedDelivery, restricted_delivery on the account routes): Value Word Meaning 0 domain Follows the domain. The default 1 restricted Restricted even when the domain names nothing; the account is then confined to its own domain 2 exempt May exchange mail with anyone An empty list, the default, means nothing runs, so an upgraded server delivers exactly as it did. Always allowed: the account's own domain under its own name or any alias domain, the domain's postmaster, the null sender MAIL FROM:<>, and mail this server writes itself. Other domains this server hosts are not allowed unless named. It is enforced in three places: Inbound, at RCPT TO. A restricted recipient whose envelope sender is not allowed is answered 550 5.7.1 <address>: this mailbox accepts mail from named domains only., per recipient, so one refused RCPT does not end the transaction. Every session is judged, authenticated or not. Outbound, at RCPT TO. A restricted submitting account is answered 550 5.7.1 <address>: this account may send to named domains only. for a recipient outside. The webmail's POST /api/v1/me/messages answers 400 with the sentence and queues nothing. At local delivery, for a recipient reached by expansion: a distribution list member, an alias target, a catch-all. The restricted recipient is dropped, and a non-delivery report goes to the sender only when the sender is local. A restricted account's forward - the account forward, an account rule's forward, a Sieve redirect - to a domain not allowed is not made, and the message is still delivered to the mailbox. Its out-of-office reply is not sent to a sender who is not allowed. Every refusal is a DEBUG log line; nothing goes to the error log, since a refusal is the feature working. Message recall within the server, since 6.3.4 Schema 6085. An unread message sent to accounts on this server can be withdrawn by its sender. Nothing does this across the Internet: a message that has left for another server is that server's. No recall notice reaches the recipients; the Sent copy's own mark is the record. What is recorded. A message accepted from one of the server's own accounts - an authenticated SMTP submission, credited to the account that signed in, or the webmail's send - gets one row in hm_recall per recipient that resolved to a local account. Nothing is recorded for: mail from outside, and mail this server writes itself; an unauthenticated submission from a local IP range in a local account's name, because a recall keyed on a claim would let anybody delete mail from other people's mailboxes; a message with a Message-ID longer than 255 characters, and a message that expires; the 201st and later recipient rows of one sender in a rolling day. The recall. In the webmail, Recall in the menu of a message in the Sent folder. For a program, POST /api/v1/me/recall with {"message_id": "<the Message-ID>", "sent_id": n}; it is refused with 403 on an app password. Each recipient is judged on their own: Outcome Meaning read Some live copy of theirs carries \Seen. Nothing was done: whoever has read it keeps it recalled No copy was read; every unread copy has been deleted the way the webmail deletes a message gone No copy of theirs is there any more: deleted by the reader, never delivered, or still in the queue failed The database did not answer; a later call decides them again The answer also counts elsewhere, the To and Cc addresses whose domain this server does not host, and says in sent_marked whether the Sent copy carries the keyword $Recalled. Read is \Seen and nothing more: a POP3 client, or an IMAP client that peeks, has the message and the recall cannot know. RecallDays (recall_days on PUT /api/v1/settings) is 14 by default, 1 to 365; a domain or an account may hold a shorter window, never a longer one. There is no COM property and no Control Panel page. What it does not do A route does not decide whether mail may go, and restricted delivery and a remote domain policy do not decide where. A per-policy SMTP port or credential is a route's job. Restricted delivery does not check the From header, does not filter what is already in the mailbox, and does not gate a journal rule's copy or an alias or list in the domain that reaches outside. Recall does not reach another server, does not touch the queue, and does not recall a message submitted without authentication. A 5xx from one smart host is not retried on the next. Where to read more Wiki: Routes and Relays, Settings Reference for the OutboundOAuth2* keys, APIs Reference, Diagnosing Stalled Mail Documents at the 6.3.4 tag: RemoteDomains.md, RestrictedDelivery.md, MessageRecall.md, Upgrading.md The public documentation: chapter 14, Routes and relays On this forum: Being trusted by the servers you send to, the 6.3.4 release announcement, requiring TLS and remote domain policies, and diagnosing stalled mail for a message that will not leave the queue
  • 0 Votes
    1 Posts
    137 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) holds a rule and a Sieve script to their owner: a domain's or an account's rule may no longer set From, Sender, Resent-From, Resent-Sender, DKIM-Signature or the loop count, and its Reply goes only from an address its owner may send as; a Sieve script may no longer add or delete those fields, Auto-Submitted or the loop count, and its notify goes from the account's own address unless its :from reaches the account. One stored before the upgrade still runs without that action or edit, and the application log names it. A loop count that is not digits alone, or is above the limit, reads as the limit, and RuleLoopLimit 0 now stops out-of-office replies too. See the 6.3.7 announcement. hMailServer filters mail with two systems that both run during delivery and do not know about each other. Rules are the server's own engine: criteria and actions, edited in the Control Panel, and the only one of the two that can run server-wide. Sieve (RFC 5228) is the standard mail filtering language, owned by the mailbox user and edited from a mail client over ManageSieve or on the Control Panel. This topic covers the order they run in, rules and the Run function action, Sieve and vacation, the out-of-office policy, and what both see on sealed mail. It describes 6.3.4 (27 September 2026, schema 6104). The one thing to know first: they run at different points, they see the message at different stages, and when they disagree about where a message goes, Sieve wins. The order they run in The filtering pipeline for one inbound message: During the SMTP conversation, while the sender is still connected. The pre-transmission spam tests (SPF, DNSBL, HELO, greylisting) run at MAIL FROM and RCPT TO; the script event OnSMTPData runs at the DATA command, before any of the body is sent; once the body is in come the post-transmission tests (SURBL, DKIM, ARC, SpamAssassin). A message over the delete threshold is refused, with 550 when the verdict came before the body and with 554 after it; with quarantine on, one judged after the body is held in quarantine instead and the sender answered 250. Otherwise the script event OnAcceptMessage runs and the message is queued. On the delivery thread, once per message and on every delivery attempt: OnDeliveryStart, blocked attachments and the virus scan, the global rules, OnDeliverMessage, then DKIM signing and mirroring (first attempt only). The message is split into local and external recipients. External recipients go to relays, routes and MX delivery. For each local recipient, on that recipient's own copy: per-account spam overrides, the account auto-reply, the account rules, account forwarding, trace headers, the Sieve script, the domain-wide auto-reply (only if the account said nothing), Sieve reject then discard, Sieve flags, the folder decision, and the save to the mailbox. Because It follows that Spam scoring happens during the SMTP conversation A rule cannot make a message spam; it can only read the flag, or a header the filter wrote Virus scanning happens before global rules A rule never sees an infected message Global rules run on the shared message, before the split Delete e-mail in a global rule destroys the message for every recipient; in an account rule, only that recipient's copy Global rules re-run on every delivery attempt Forward, Create copy and Reply can fire more than once for a deferred message. Write Delivery attempts equals 1 to prevent it Sieve runs after account rules A Sieve fileinto overrides the folder a rule chose. A header a rule set, or a copy it queued, has already happened and Sieve cannot undo it What a rule is Rules in the Control Panel sidebar is the server-wide set ("Global rules"); each account has its own on the Rules tab of its account dialog. A rule is a name, an enabled flag, a match mode - Match ALL criteria (AND) or Match ANY criterion (OR) - an ordered list of criteria and an ordered list of actions. Criterion What is compared Note From, To, CC The From:, To: and Cc: headers Not the envelope. Mail that reached you by Bcc, an alias or a list has no To: naming you Body The plain-text body concatenated with the HTML body A phrase inside an attachment is not matched Message size The file's size in bytes 25 MB is 26214400 Recipient list Every SMTP envelope recipient, joined with ; The field that sees a Bcc, an alias expansion or a list member Delivery attempts The retry count plus one The first attempt is 1 Custom header The named header's value Name it without the colon Match types. equals, does not equal, contains and does not contain are case-insensitive. is less than and is greater than parse both sides as numbers, and a non-numeric string parses as 0. matches wildcard and matches regex require the whole value to match; a regex is also case-sensitive, so to find a word anywhere write .*invoice.*, and (?i).*invoice.* to ignore case. does not equal and does not contain are true when the field is empty: Subject does not contain [ok] -> Delete e-mail deletes every message that has no Subject at all. Add a positive criterion under match mode ALL when the action is Delete. Action What it does Delete e-mail Global: the message is dropped and Message will be deleted. Action triggered by a global rule (<name>) is logged. Account: only this recipient's copy Forward e-mail Queues a second message to the named address, loop-count guarded Reply Queues a new message to the envelope sender, stamped Auto-Submitted; never answers auto-submitted mail Move to IMAP folder Happens at the end of local delivery. INBOX.Archive; #Public.Name for a public folder, subject to the insert permission; %YEAR%, %MONTH% and %DAY% come from the server's local date. A missing folder is created Run script function Calls a function in the event script - next section Stop rule processing Stops the rule set after this rule's remaining actions have run Set header value Rewrites the message file; %MACRO_ORIGINAL_HEADER% is the header's current value Create copy Queues a duplicate carrying X-CopyRule: <rule name> Send using route, Bind to address Force a named route, or the local address the connection leaves from. Absent from an account's rules The Run function action, since 6.3.4 The action the Control Panel calls Run script function calls the named function in the event script as <function>(HMAILSERVER_MESSAGE), then reloads the message from disk, so a script that edits and saves is seen by the rest of the rule set. It needs scripting enabled. Since 6.3.4 the name must be a plain identifier: ASCII letters, digits, _ and $, not starting with a digit. On Windows, in either script language (VBScript or JScript), any other name is refused as HM5018 each time the rule fires. Before 6.3.4 the name was written into the call unescaped, in either language, so whoever may edit a rule, a domain's own administrator included, could run code in the server's process: that is CVE-2026-100741, in every release from 6.0.0 to 6.3.3, and event scripts and the 6.3.4 security fix has the flaw and what to do until a server is upgraded. On Linux the action works since 6.3.4, where event scripts are JavaScript. The Linux engine looks a handler up by name instead of building a call, so such a name is never evaluated and finds no function: nothing is run, and the ERROR log has a Script Error line, not HM5018. Since 6.3.7 such a name is refused when the rule is saved, over COM and REST, for a rule of every owner; and the action on a domain's or an account's rule is the server administrator's to set, since it reaches a function of the server's event script: a credential confined to named domains - such as a domain administrator, or an API key issued for its domains - is refused it. Evaluation: a matching rule does not stop the set For each active rule, in order, the criteria are evaluated under the match mode and, if they matched, every action runs. Only a Stop rule processing action ends the set, so make that the last action of every rule that should be terminal. A rule with no criteria runs no actions, in either match mode. A catch-all rule needs a criterion that is always true. If the message file cannot be loaded, the whole set is skipped and the message is delivered unfiltered, with HM6040 in the log, rather than run against an empty message. When several choose a folder, a Sieve fileinto beats an account rule's folder, which beats a global rule's. The regular-expression breaker. A regex runs against a value the sender wrote, so it has a budget of 250 ms per evaluation. A pattern that will not compile is reported as HM6042; one that takes 250 ms or more as HM6043; either is suspended for 300 seconds, keyed on the pattern text, so a corrected pattern takes effect on the next message. A suspended pattern does not match. The same breaker covers Sieve's :regex, and suspension is shared. Loop protection. Every generated message carries X-hMailServer-LoopCount. Forward, Reply and Create copy check it against RuleLoopLimit (COM Settings.RuleLoopLimit; Mail flow & delivery → Delivery of e-mail → Rules, since 6.3.7 a card on the Rules page; default 5); at or above it nothing is generated and HM5064, HM5065, HM5201 or HM4404 is logged. An Auto-Submitted header stops Reply and bounce reports only; Forward deliberately ignores it. What else 6.3.4 changed for rules Three collections instead of two. A domain's rules (Domain.Rules, /api/v1/domains/{name}/rules) run after the server's. Account rules are on /api/v1/accounts/{address}/rules. Five more actions: Add a disclaimer, Reject with a text, Require TLS, Quarantine and Warn the sender. A legal hold keeps, among the account's deleted items, what an account rule's Delete or Reject, or a Sieve discard, reject or redirect that keeps no local copy, dropped. It does not reach the server's or a domain's rules: a Delete there drops the message for everybody, held accounts included. A domain's rules do not always act on the whole message. Mail flow rules and what a domain does to a message says when they act on all of it and when on the domain's own recipients only, what each new action does, and two 6.3.4 fixes to what a rule queues. Sieve: where a script lives and how it becomes active Each account has one active script, plus any number of named scripts. They are files, not database rows, under [Directories] DataFolder: <DataFolder>\ Sieve\ _global\ administrator-placed scripts, for include :global example.com\ alice\ active.sieve THE ACTIVE SCRIPT - this is what delivery runs vacation.responses who has already had a vacation reply, and until when duplicate.seen identifiers the duplicate test has seen scripts\ holiday.sieve named scripts, from ManageSieve PUTSCRIPT Four routes write that same active.sieve, and all four check the syntax first: Route Where Control Panel (Windows) Account dialog, Sieve tab COM (Windows) Account.SieveScript; no Save() is needed REST GET and PUT /api/v1/me/filters, as the account itself; 256 KB cap; an empty script removes the filter ManageSieve PUTSCRIPT then SETACTIVE Deactivating a script also clears that account's vacation-response records. ManageSieve is off by default: ManageSieveServerPort=0 means the listener is never created. Set the port (the wiki's example is 4190) and ManageSieveServerBindAddress on the ManageSieve (RFC 5804) card of the Control Panel's API & monitoring page on Windows (since 6.3.7 a ManageSieve tab of the Protocols page; Ctrl+K takes the old title to it), or on either platform with PUT /api/v1/settings/ini/{name} or hmctl ini set. Since 6.3.4 both wait for a reinitialise, not a restart: Apply now in the Control Panel or the Deck, or POST /api/v1/server/reinitialize. A script is at most 1 MB. With no certificate, STARTTLS is not advertised at all; with no certificate and no range policy, keep the listener on 127.0.0.1. What this server's Sieve accepts and refuses require accepts fileinto, envelope, copy, relational, subaddress, vacation, vacation-seconds, imap4flags, body, mailbox, regex, ihave, environment, date, index, spamtest, spamtestplus, duplicate, editheader, variables, reject, ereject, include and enotify, and the comparators comparator-i;ascii-casemap, comparator-i;octet and comparator-i;ascii-numeric. What matters in practice: fileinto :copy is refused at upload. This server stores exactly one local copy. Use :copy on redirect. redirect without :copy cancels the implicit keep, and the message leaves the mailbox entirely. imap4flags stores only the five system flags, \Seen \Answered \Flagged \Deleted \Draft. A keyword is accepted, logged, and stored nowhere. spamtest reads the server's internal flag, never a header a sender could forge: "0" when the message was not classified, "10" when it was. Any threshold from 1 to 10 behaves identically. duplicate keys on Message-ID over 7 days by default, and its store fails open, because a wrong "duplicate" destroys mail. reject and ereject send a non-delivery report with status 5.7.1 to whoever the envelope names. Against forged senders use discard. editheader refuses Received and Return-Path, and since 6.3.7 From, Sender, Resent-From, Resent-Sender, DKIM-Signature, Auto-Submitted and X-hMailServer-LoopCount, at upload and at delivery, where a script stored before the upgrade still runs without the edit; enotify accepts mailto: only, and since 6.3.7 a notify goes from the account's own address unless its :from is the account's own address or an active alias pointing at it; include nests at most three levels. The everyday script: require ["fileinto", "mailbox", "spamtest", "relational", "comparator-i;ascii-numeric"]; if spamtest :value "ge" :comparator "i;ascii-numeric" "5" { fileinto :create "INBOX.Junk"; stop; } if header :contains "List-Id" "announce.example.com" { fileinto :create "INBOX.Lists.announce"; stop; } Vacation, and the out-of-office policy require ["vacation"]; vacation :days 7 :subject "Out of the office until 22 September" :addresses ["[email protected]", "[email protected]"] "I am away until 22 September. For anything urgent please write to [email protected]."; :addresses is not decoration: a reply is sent only when one of the user's own addresses appears in the envelope or the headers, and that list is what "the user's own addresses" means. :days defaults to 7; vacation-seconds gives :seconds, and :seconds 0 means "reply every time". The window is per sender, in vacation.responses, and that store fails closed: if it cannot be read, no reply is sent. The account's Auto-reply tab and the domain's Out of office tab are the server's own reply, separate from Sieve. At most one reply answers any message. The account's own, when it is on and inside its dates, comes first; a domain's reply answers only for accounts that have none. If both an account-level auto-reply and a Sieve vacation are configured, the account-level one wins. Account Domain General text VacationSubject, VacationMessage VacationSubject, VacationMessage Text for colleagues VacationInternalSubject, VacationInternalMessage the same two Colleagues only VacationMessageInternalOnly leave the general text empty Strangers get the domain's text - VacationExternalOverride A sender is internal when the envelope sender resolves to an account hosted here or to an active alias. That is whatever the submitting client said, so an internal text must never carry anything that cannot be shown to a stranger. Each sender is answered once per absence; a window - VacationMessageDays on the account, VacationDays on the domain, 0 to 3650 - answers the same sender again after that many days. The memory is the table hm_autoreplies (schema 6060), so since 6.3.4 a restart no longer repeats every reply. Never answered: a null return path; Auto-Submitted other than no; Precedence: bulk, list or junk; anything with a List-* header; a sender that set X-Auto-Response-Suppress; postmaster and mailer-daemon; ourselves; and marked spam when VacationMessageAbortSpamFlagged says so; and since 6.3.7 a message whose X-hMailServer-LoopCount has reached RuleLoopLimit, so RuleLoopLimit 0 stops out-of-office replies as it stops every other generated message. Every reply goes out with a null return path and Auto-Submitted: auto-replied. What rules and Sieve see on sealed mail A message encrypted end to end (S/MIME or PGP/MIME) is opaque: the server holds no key to it. How the server decides that, and what the rest of the server does with such a message, is in end-to-end encryption and sealed mail. For rules and Sieve: A rule's body criterion on an opaque message does not match, whichever way it is phrased, and one line per rule per message in the log says so. Under AND, a rule with a body criterion does not fire; under OR, the other criteria are still evaluated. Before 6.3.4, does not contain matched every encrypted message. A multipart/signed message is read as it always was. To catch encrypted mail, test the Content-Type header for multipart/encrypted or application/pkcs7-mime with a custom-header criterion. Sieve's evaluation is unchanged; a log line is new. An encrypted message has no text part, so body :contains "x" is false and not body :contains "x" is true, and the delivery writes a log line saying so, once per message and recipient. body :raw is the undecoded body exactly as it arrived, so if body :raw :contains "BEGIN PGP MESSAGE" is the honest way a script can recognise a sealed message. What it does not do Sieve cannot run server-wide, force a route or bind an outbound address. A rule's Delete e-mail is silent. The sender was already answered 250. To tell the sender, use Sieve reject, or since 6.3.4 the Reject with a text action. A journal rule does not see a rule's forward, copy or reply, or a Sieve redirect: the server queues those itself. Where to read more Wiki: Rules and Sieve, The COM API and Scripting Documents at the 6.3.4 tag: OutOfOffice.md, SealedMail.md, MailFlowRules.md, EventScripts.md The public documentation: chapter 12 and the rules cookbook On this forum: the 6.3.4 release announcement, the Linux guide, mail flow rules and domain rules, event scripts, and when a saved setting takes effect for the ManageSieve keys
  • 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
    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
    124 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) changes three things this guide relies on. A Control Panel signed in to a server on another computer no longer reads or writes its own computer's hMailServer.ini for the [LDAP] values, which it cannot read from the server, where it showed that file or the defaults as the server's; the sidebar is regrouped, and Ctrl+K takes every old page title to its page. With SMTP logging on, 6.3.6 and every earlier version that offered them wrote the bearer token of AUTH XOAUTH2 and AUTH OAUTHBEARER and the lines of SCRAM-SHA-256, OAUTHBEARER and GSSAPI into the SMTP log in the clear; 6.3.7 hides every line of a SASL exchange but the user name of AUTH LOGIN and the authentication identity of AUTH PLAIN. An account given the serveradmin role with a list of domains is, over COM as over REST, confined to those domains and no longer the server's administrator, so a script that runs the directory synchronisation's preview and apply as such an account needs the role given with no list. 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, for the name an OAuth2 bearer token carries as for a password (work item 104), so a name that differs from the stored address in more than the case of the letters A to Z, or has white space at either end, may no longer sign in. On Windows an OAuth2PublicKeyFile whose path holds characters outside ASCII may be refused with HM7536 (one outside the code page, which worked up to 6.3.7, is refused), and then the key verifies no token: give the file, and the folders above it, an ASCII name before upgrading. See the 6.3.8 announcement. This topic covers three ways an organisation's directory or identity provider vouches for somebody on hMailServer 6.3.4's mail protocols: checking a password against Active Directory or another LDAP directory, creating mailboxes from that directory, and accepting OAuth2 bearer tokens. Know this first: 6.3.4 keeps settings in the database, but the [LDAP] section stays in hMailServer.ini on each server, with no REST route and no environment name; the server re-reads it within two seconds of a change, with no restart. The OAuth2 keys are ordinary [Settings] keys, stored like any other. Three mechanisms, three questions Mechanism The question it answers Control Panel page (Windows) LDAP directory authentication Is this the password of the directory user the account is linked to? Access & abuse protection → Directory authentication (LDAP) Directory synchronisation Which mailboxes should exist, according to the directory? Access & abuse protection → Directory synchronisation OAuth2 bearer tokens Did the identity provider sign this token, and which mailbox does it name? Access & abuse protection → Authentication All three are off by default, and an upgrade switches none of them on: the [LDAP] section is absent until you add it, and OAuth2Enabled defaults to 0. Where the [LDAP] section lives Windows. The Control Panel's page writes the hMailServer.INI of its own machine. Connected to another machine, its Save is disabled, although "the connection test still works with the values typed here". Work on the server, over Remote Desktop. Linux. No Control Panel and no COM. The file is /etc/hmailserver/hMailServer.ini, 0640 root:hmailserver: sudoedit /etc/hmailserver/hMailServer.ini. Not REST or hmctl. The REST ini routes and hmctl ini set handle [Settings] keys only. Per server. On a warm standby each node has its own [LDAP]; keep the two alike by hand. ServicePassword is a line of that file. It is not among the [Settings] secrets 6.3.4 seals under the stored-secrets key; the Control Panel's field for it is write-only. The sign-in keys Key Default What it does Enabled 0 The master switch Server empty The directory host; required Port 0 0 means 636 for LDAPS, 389 otherwise Security 2 2 LDAPS, 1 StartTLS, 0 plain; anything else is LDAPS VerifyCertificate 1 Check the directory's certificate AllowUnprotectedPassword 0 Permit a simple bind over a plain connection BindMethod 0 0 simple; 1 Negotiate (Kerberos or NTLM through Windows SSPI) SearchBase empty Required unless UserDnTemplate is set or the bind is Negotiate UserSearchFilter (&(objectCategory=person)(objectClass=user)(sAMAccountName=%u)) How the user's entry is found UserDnTemplate empty Bind directly, without searching ServiceUsername, ServiceDomain, ServicePassword empty What the search binds with TimeoutSeconds 10 Bounds every directory operation; 1 to 90 FallbackToWindowsLogon 0 Also try Windows LogonUser; Windows only LDAPS with VerifyCertificate=1 is the default and the recommended configuration, proven end to end against a Windows Server 2025 domain controller. Server must be the name in the directory's certificate: the same setup pointed at the controller's IP address, with the right password, is refused, and HM5920 reports a failure "unrelated to the credentials supplied". On Windows, put the issuing CA in LocalMachine\Root; when this was proven the service ran as LocalSystem, and a CurrentUser\Root import served it nothing. The bind needs no extra privilege under NT SERVICE\hMailServer, which a new 6.3.4 installation runs as. On Linux the certificate must chain to the system trust store. StartTLS (Security=1) connects on 389 and upgrades before anything is sent. Plain LDAP is labelled Unprotected LDAP - cleartext (lab networks only): there a simple bind is refused (HM5921) rather than sent, unless AllowUnprotectedPassword is 1. Simple or Negotiate. A simple bind sends the password to the directory, so it belongs on LDAPS or StartTLS. Negotiate, Windows only, never sends it; it worked on 389 against a controller that had no LDAPS certificate yet, and suits a mail server not joined to the domain. Search or template. A search binds with the service credentials, finds the user under SearchBase with UserSearchFilter, then binds as the user. It asks for two entries to see ambiguity: more than one match is a configuration fault and authenticates nobody. UserDnTemplate skips the search, so no service credential need be stored at all; with a simple bind, that is the configuration the feature was proven with. Linking an account to the directory The directory is asked only about linked accounts; others keep their own password. The webmail's sign-in shares the IMAP and POP3 logon, so it takes the directory password too. Control Panel: the account dialog's Windows directory tab, with the domain (ADDomain) and user name (ADUsername); Browse Active Directory searches the forest to fill them. Since 6.3.4 the tab has that name; it was called Directory, and the new Directory entry tab is the organisation's address book, unrelated to Active Directory. REST: PUT /api/v1/accounts/{address} with ad_enabled, ad_domain and ad_username. COM (Windows): IsAD, ADDomain and ADUsername. What a link changes. The account's stored password is no fallback: in the proof run, a linked account's local password was refused. Linked accounts bypass local hashing, so they are exempt from the minimum-hash policy and from password expiry, and cannot sign in with SCRAM-SHA-256; their clients use PLAIN or LOGIN over TLS. App passwords work for them: the directory holds the password, but an app password is local, which is what makes a second factor possible for a directory account at all. The person cannot change the password in the webmail (POST /api/v1/me/password answers 409), and Forgotten your password? cannot reset it, because this server does not hold it. Without LDAP, on Windows, a linked account is checked by LogonUser, which needs the mail server's host joined to the domain. On a host that is not joined, Windows answers every domain name, real or misspelt, as a wrong password, so the account can never sign in; HM5911 says so, naming the domain and the computer. An empty domain, . or the computer's own name means a local Windows account. Never leave the domain empty by accident: with [LDAP] Enabled off, an account linked with an empty domain and the user name administrator would accept the server's local Administrator password. FallbackToWindowsLogon never applies to a wrong password: a directory's rejection deliberately does not fall through to LogonUser, because that would double every failed attempt and trip a lockout policy on a single wrong password. Leave it off on a host that is not domain-joined. Setting it up on Windows For LDAPS or StartTLS, put the directory's issuing CA in LocalMachine\Root on the mail server. On the mail server, open Directory authentication (LDAP). Switch directory authentication on (Enabled); until then a linked account is checked by Windows LogonUser, not with these settings. Fill in the server, the security, the bind, and either the search or UserDnTemplate; the first card asks Is this configuration complete? Test the connection with a directory user name, its AD domain and its password. It tries the values typed on the page. Save. The server re-reads [LDAP] within two seconds. Link one account and sign in with the right password and a wrong one. Over POP3 the proof run got +OK Mailbox locked and ready and -ERR, and the log showed the bind and the rejection separately. Link the others by hand, or with directory synchronisation. Setting it up on Linux The only directory path is an LDAP simple bind over LDAPS or StartTLS, proven against OpenLDAP's slapd. Make sure the directory's certificate chains to the system trust store, and use the name it carries. Add the section with sudoedit /etc/hmailserver/hMailServer.ini, filling in the angle brackets. Without a UserSearchFilter line the search uses Active Directory's filter, (&(objectCategory=person)(objectClass=user)(sAMAccountName=%u)), which finds nobody in another directory such as OpenLDAP; there, write a filter that matches its user entries, with %u for the name: [LDAP] Enabled=1 Server=<the host name the directory's certificate carries> Security=2 VerifyCertificate=1 BindMethod=0 SearchBase=<the DN the users are under> UserSearchFilter=<that filter, unless the directory is Active Directory> ServiceUsername=<the search account> ServicePassword=<its password> Two seconds later the server has it. Link an account over REST (ad_enabled, ad_domain, ad_username) and sign in with the right password and a wrong one. Linux has no Negotiate (BindMethod=1 is refused as a directory failure, not a wrong password: HM6420, reported once), no LogonUser (a linked account while [LDAP] Enabled is off is refused with HM6421, which names the settings to set; FallbackToWindowsLogon refuses with HM6404), and no GSSAPI bind. When the directory cannot answer So that an outage does not read as users mistyping, infrastructure failures are reported apart from credential rejections, throttled per error and endpoint, and TimeoutSeconds bounds the wait. Code Meaning HM5920 The directory could not be used (a certificate not matching Server, say). Reported at High: unrelated to the credentials, and every account on that directory is affected HM5921 A simple bind would have sent the password unprotected, so the logon was refused first HM5911 Windows LogonUser: the host is not joined to the domain; at most once a minute per problem Directory synchronisation: preview, then apply Synchronisation reads the directory, works out which mailboxes should exist, and only when asked creates and updates them; it was verified end to end against a Windows Server 2025 domain controller. The planner writes nothing and the applier decides nothing, so a preview cannot promise what the apply would not do. Per-domain opt-in. A domain takes part only if its Active Directory domain name is set (ADDomainName in the domain dialog, ad_domain_name over REST), so a search base one level too high cannot provision into unrelated hosted domains. Nothing is ever deleted, account or message. The most it does is clear Active, only when asked, and never on a truncated or empty enumeration or for a domain in which nothing was seen. No password. A made account is marked IsAD and signs in through the directory from the start. No existing account claimed. An account that is not linked, or is linked to another AD domain, is reported, not changed. Paged. Active Directory answers a search beyond MaxPageSize (1000) with the first page and a success status, so the enumeration pages; SyncMaxUsers is the ceiling. [LDAP] key Default SyncFilter (&(objectClass=user)(objectCategory=person)(mail=*)(!(userAccountControl:1.2.840.113556.1.4.803:=2))) SyncMailAttribute mail; proxyAddresses works, preferring the capitalised SMTP: primary SyncUsernameAttribute, SyncDisplayNameAttribute sAMAccountName, displayName SyncMaxUsers 5000; at most 100000 SyncScheduleMinutes 0, off; 15 at least, 10080 (seven days) at most On Windows: Get authentication working first. Synchronisation binds as the service account, honouring BindMethod, so Negotiate works where there is no LDAPS certificate. Set the Active Directory domain name on each mail domain that takes part. Open Directory synchronisation, check the selection keys, choose the domain and whether absent accounts are marked inactive, and run Preview. It changes nothing. Read the report, including the existing accounts it reports and leaves alone. Apply. The button stays disabled until a preview of the same options has run. Behind the page are Settings.PreviewDirectorySync(DomainName, DisableMissing) and Settings.ApplyDirectorySync, for a server administrator and callable from a script. From a Control Panel on another machine the six selection boxes are read-only and, up to 6.3.6, show their defaults; since 6.3.7 the Control Panel no longer reads its own computer's file for the [LDAP] values, which it cannot read from the server, so it no longer shows them as the server's. Preview and Apply still run inside the server. The unattended schedule (SyncScheduleMinutes) runs at start and then every so many minutes. It creates and updates but never disables: unattended is where nobody reads the report that would show a mailbox about to be taken away. The schedule is one of the tasks the server registers as it starts, and only if SyncScheduleMinutes is set then, so restart the service once after first setting it. On Linux, with no COM, there is no preview or apply, and whether the schedule runs there is not documented: treat it as untested. OAuth2 bearer tokens on IMAP, POP3 and SMTP With OAuth2Enabled=1, SMTP, IMAP and POP3 offer XOAUTH2 and OAUTHBEARER (RFC 7628), by default only over TLS (OAuth2RequireTLS=1). The server verifies the token's signature itself, takes the login name from the claim OAuth2UsernameClaim names (email), and signs in to that mailbox; a user= the client asserts must match. The mailbox must already exist: a token never makes one. Key Default What it does OAuth2JwksUrl empty The provider's JWK Set; the token's kid picks the key. https, or plain http to loopback only OAuth2JwksCacheSeconds 3600 How long the set is kept OAuth2PublicKeyFile empty A PEM RSA or EC public key; the fallback OAuth2HmacSecret empty The HS256 key OAuth2AllowedAlgorithms RS256 HS256, RS256 and ES256 recognised; none never OAuth2Issuer, OAuth2Audience empty Expected iss and aud; empty turns the check off OAuth2IntrospectionUrl empty RFC 7662 endpoint asked whether a verified token is still active OAuth2IntrospectionClientId, OAuth2IntrospectionClientSecret empty Who this server introspects as, over HTTP Basic OAuth2IntrospectionCacheSeconds 300 Verdict reuse, never past the token's exp OAuth2IntrospectionFailOpen 0 1 accepts a token the endpoint cannot judge, logging each An unknown kid, which is what a rotation looks like, fetches the set again at once, at most once a minute; a failed fetch keeps the keys held. HttpProxy, when set, carries these fetches. Where to set them. Each applies when saved: on the Control Panel's Authentication page, with PUT /api/v1/settings/ini/{name} and the administrator password, with hmctl ini set, or with COM Settings.SetIniSetting. Not in hMailServer.ini: since 6.3.4 the database holds them, and a line edited in the file is not used; Settings: when a save takes effect explains the store. Choose the key source: OAuth2JwksUrl, so a rotation needs no copied file; or OAuth2PublicKeyFile; or OAuth2HmacSecret with HS256 allowed. Set OAuth2Issuer and OAuth2Audience to what the provider writes into its tokens. Left empty, neither is checked: any token the provider's keys signed is accepted, whatever client it was issued to. Optionally set introspection, so a revoked token stops working before it expires. Only then set OAuth2Enabled to 1. On Windows, read the OAuth2 / XOAUTH2 item of the Control Panel's External setup list. Why that order. Switched on without usable key material, the mechanisms are still advertised, every token fails, and failed bearer sign-ins feed the auto-ban, so a correct client can have its address banned. On Linux before the REST listener is up, hmailserver --set-setting stores a key; a secret is given by name alone and typed at a prompt. A value stored this way reaches the running server at its next save, reinitialise or restart. Set the issuer and audience too, as step 2 says, putting your provider's values in place of these examples: sudo hmailserver --set-setting OAuth2JwksUrl=https://idp.example.com/keys sudo hmailserver --set-setting OAuth2Issuer=https://idp.example.com sudo hmailserver --set-setting OAuth2Audience=hmailserver sudo hmailserver --set-setting OAuth2Enabled=1 sudo systemctl restart hmailserver Since 6.3.4: a bearer token meets the domain's and the account's country policy, as a password does (a stolen token used to sign in from a refused country); an SMTP session signed in with a token can send more than one message (the second used to be refused with 550 "Login credentials no longer valid"); and OAuth2HmacSecret and OAuth2IntrospectionClientSecret are sealed at rest once the installation has a stored-secrets key, which an upgrade does not make until an administrator runs --create-stored-secrets-key. Tested how. The regression fixtures play the identity provider on the loopback, to make it misbehave: RS256 and ES256, the JWK Set and its rotation, introspection and fail-open. A real provider's tokens are not yet part of them. This is inbound only. The OutboundOAuth2 keys and FetchOAuth2Hosts are the server's own token for relaying through and collecting from Microsoft 365; Routes, relays and smart hosts covers relaying with it. Where these end: SCIM, single sign-on and Kerberos The LDAP backend authenticates; SCIM provisions. Synchronisation reads the directory; SCIM is the other direction, Entra ID or Okta calling /scim/v2 under an API key whose role holds provision, and it removes accounts, which synchronisation never does. Single sign-on, OpenID Connect or SAML 2.0, opens the webmail and the Control Deck only, ending in POST /api/v1/session; the protocols' bearer tokens are a separate, older mechanism with the OAuth2 settings above, and single sign-on does not reach mail clients. On Windows, SASL GSSAPI (GssapiEnabled) lets a mail client sign in with a Kerberos ticket, mapped to the account linked to that directory user. SCIM provisioning and single sign-on sets those up; Signing in and Connecting a mail client show the person's side. What it does not do No Negotiate, Windows logon or GSSAPI bind on Linux. hMailServer on Linux lists the rest. No LDAP server here, and the webmail's organisation directory is typed in, not imported from Active Directory. Where to read more Wiki: Settings Reference sections 9.2 and 11, Capabilities and Configuration, Control Panel Reference, Installing on Linux, Security Hardening, Sign-in and Identity, Architecture Documents: SettingsApply.md, ServiceAccount.md, Scim.md, PasskeysAndSingleSignOn.md, TestLab.md The Roadmap rows on LDAP, which record how each part was proven On this forum: the stored-secrets key, auto-ban and the warm standby The public documentation's Creating accounts from Active Directory and Modern authentication Report a security flaw in any of this by SECURITY.md's routes: a confidential GitLab issue, or email to contact-project+hmailserver-hmailserver-86730559-issue-@incoming.gitlab.com, which GitLab files as one.
  • 0 Votes
    1 Posts
    143 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) confines an account given the serveradmin role with a list of domains to those domains over COM as over REST, so it is no longer the server's administrator: give the role with no list to an account that is to administer the whole server, a script's or a Control Panel's account among them. Over COM, one given domainadmin with a list that leaves out its own domain no longer administers that domain's ordinary accounts; 6.3.4 to 6.3.6 allowed both where REST refused them. 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, and a credential confined to named domains is refused over COM, as over REST, a domain's rename, a domain alias, a relay host changed to an address that is not public and an action on a domain's or an account's rule that reaches one of the server's own addresses, a configured route or a function of the event script. Rule.Save and RuleAction.Save refuse, on a domain's or an account's rule, a Set header value of From, Sender, Resent-From, Resent-Sender or DKIM-Signature and a Reply from an address the owner may not send as, and on a rule of every owner a Set header value of the loop count; one stored before the upgrade is taken without that action, and the application log names it. For a rule of every owner they refuse a Run function that is not a plain function, which one stored before still meets when the rule fires, as since 6.3.4. Utilities.CheckSieveSyntax refuses a Sieve script that adds or deletes From, Sender, Resent-From, Resent-Sender, DKIM-Signature, Auto-Submitted or the loop count. Once the database has ended a Database object's transaction, every later statement is refused and Database.CommitTransaction fails: call Database.RollbackTransaction and run the whole transaction again, or stop. See the 6.3.7 announcement. hMailServer 6.3.8 (2 November 2026) signs a name in over COM, as at every other sign-in, only when it is the account's stored address, compared in lower case after a domain alias and the default domain: a mailbox address spelt otherwise, in more than the case of the letters A to Z, or with white space at either end, may no longer sign in; it fails as a name that does not exist fails, and is counted as a failure. See the 6.3.8 announcement. This topic covers the COM API of hMailServer 6.3.4: what a script connects to and how it authenticates, the object model and the shape every collection shares, working PowerShell examples, the change 6.3.4 makes to when a configuration change takes effect, what 6.3.4 adds to the object model, administrator roles over COM and why API keys are not there, the changed-from-stock document, the COM surfaces of distribution lists and the directory, and the Import Tool. The one thing to know first is that the COM API is Windows-only, and that an object you add to a collection, or an item of one you change, is not stored until you call its Save(); a few writes need none, among them an account's SieveScript and Settings.SetIniSetting, and an address saved or deleted through Route.Addresses needs no Save() of the route. Since 6.3.4 the running server's routes, relays and other shared configuration objects also wait for that Save(); up to 6.3.3 they changed the moment a property was set. What you are connecting to hMailServer exposes a full COM API. Almost anything the Control Panel can do, a script can do, because the Control Panel does its work through COM. The exceptions are the pages that edit a file on the server itself: REST API keys (described under roles below), the two directory pages' [LDAP] section, the sending limits on Server limits & expert settings, the [Database] rows of the Performance page, and the event script; and Discovery, which reads the REST listener because eDiscovery has no COM interface. The COM API and the Control Panel are both Windows-only: a Linux server is administered over REST, hmctl and the Control Deck (hMailServer on Linux: what you get and what you do not). One type library, hMailServer, with 94 coclasses in 6.3.3 and 100 in 6.3.4. The root is Application, ProgID hMailServer.Application. Every interface is a dual IDispatch interface, so late binding works without registering the type library. CreateObject, New-Object -ComObject and C# dynamic all work on a machine where only the server is installed. Early binding depends on vtable order, so new members are always appended, never inserted. An interop assembly built against 6.3.3 keeps working; recompile it to reach the new members, and on a hand-copied installation register the server again, elevated, so that the new interfaces are known to COM. Late-bound scripts need nothing. The installer's admintools component exists only to register the type library, so that scripts on another machine can administer this one. # Local $app = New-Object -ComObject 'hMailServer.Application' # Remote, over DCOM, against a machine where admintools is registered $app = [Activator]::CreateInstance([Type]::GetTypeFromProgID('hMailServer.Application', 'mail01')) Authenticating $app = New-Object -ComObject 'hMailServer.Application' $account = $app.Authenticate('Administrator', 'your-admin-password') if (-not $account) { throw 'Authentication failed.' } Authenticate returns an Account object, or $null on failure. Sign in as Gets Administrator with [Security] AdministratorPassword from hMailServer.INI server administrator A mailbox address whose AdminLevel is 2 server administrator A mailbox address whose AdminLevel is 1 domain administrator: their own domain only A mailbox address that holds a role (since 6.3.4) what the role's permissions reach, in the domains it reaches An empty administrator password is an open door. If the administrator password is empty, and no administrator TOTP secret is enrolled, every COM session starts already authenticated as a server administrator. This is the historical behaviour. A second factor changes the call you must make. With Settings.EnrolAdministratorTOTP() used, Authenticate refuses the administrator outright and AuthenticateWithCode is the only way in: if ($app.AdministratorTOTPEnabled) { # readable BEFORE authenticating $app.AuthenticateWithCode('Administrator', $password, $code) | Out-Null } else { $app.Authenticate('Administrator', $password) | Out-Null } Three things about that sign-in change in 6.3.4: Change, since 6.3.4 What a script sees A one-time code is taken once a TOTP code the server has accepted is refused if it comes again, for the administrator and for an account alike; the refusal is not counted as a wrong code A wrong code is counted AuthenticateWithCode checks the password first, and a wrong code after a right password counts against a budget of five in fifteen minutes, then no code is taken for fifteen An app password is not a COM logon Authenticate and AuthenticateWithCode answer an app password as they answer a wrong password; it still opens the mailbox over IMAP, POP3 and SMTP How a refusal reaches you. If you are not entitled to an object, every member returns an error HRESULT: a terminating exception in PowerShell, a trappable error in VBScript, a COMException in C#. Up to 6.3.3, seventy-two COM classes answered a refusal as a bare Exception from HRESULT: 0x800403E9. Since 6.3.4 a refusal arrives with the server's own sentence. The object model From Application What is there Domains Domain, and under each Accounts, Aliases, DistributionLists, DomainAliases and, since 6.3.4, Rules Account (under Accounts) Address, Password, Active, MaxSize, Size, AdminLevel; Messages, IMAPFolders, Rules, FetchAccounts, AppPasswords; SieveScript, TOTPEnabled Settings the server-wide properties and the sub-objects AntiSpam (which owns Quarantine), AntiVirus, Backup, Cache, Directories, Groups, IncomingRelays, Logging, MessageIndexing, PublicFolders, Routes, Scripting, SecurityRanges, ServerMessages, SSLCertificates, TCPIPPorts; since 6.3.4, RemoteDomainPolicies Rules the global rules Status UndeliveredMessages, StartTime, ProcessedMessages, RemovedViruses, RemovedSpamMessages, SessionCount Utilities helpers such as CheckSieveSyntax, CriteriaMatch, MakeDependent, PerformMaintenance, ImportMessageFromFileToIMAPFolder Database ExecuteSQL, ExecuteSQLWithReturn, RequiresUpgrade, IsConnected, DatabaseType BackupManager StartBackup, LoadBackup GlobalObjects DeliveryQueue, MessageTrace, Languages Links Domain(DBID), Account(DBID), Alias(DBID), DistributionList(DBID) Diagnostics DiagnosticResults, each a DiagnosticResult Every collection offers the same shape: Count, Item(index), ItemByDBID(id), Add(), Refresh(), Delete(index) and DeleteByDBID(id), with ItemByName or ItemByAddress where a name exists. Items have Save() and Delete(). Add() does not persist. It returns a new, unsaved object. Set the properties, then call Save(). A script that forgets Save() looks as though it worked and changes nothing. In PowerShell, keep the object in a variable rather than chaining. Working examples $app = New-Object -ComObject 'hMailServer.Application' $app.Authenticate('Administrator', 'your-admin-password') | Out-Null # Add a domain $domain = $app.Domains.Add() $domain.Name = 'example.com' $domain.Active = $true $domain.Save() # Add an account $account = $domain.Accounts.Add() $account.Address = '[email protected]' $account.Password = 'a-strong-password' $account.Active = $true $account.MaxSize = 1000 # MB, 0 = unlimited $account.Save() # Report on every mailbox foreach ($d in $app.Domains) { foreach ($a in $d.Accounts) { '{0,-40} {1,8} MB used' -f $a.Address, [math]::Round($a.Size, 1) # Size is already in MB } } Three units, three objects. Account.Size is megabytes. Dividing it by 1MB again, which older examples do, prints 0 for every mailbox. Message.Size is kilobytes, and the Message size rule criterion is bytes. Find one account without walking every domain: $domain = $app.Domains.ItemByName('example.com') $account = $domain.Accounts.ItemByAddress('[email protected]') Send a message from a script. Message is a creatable coclass in its own right, and Save() on a new one queues it for delivery: $m = New-Object -ComObject 'hMailServer.Message' $m.FromAddress = '[email protected]' $m.From = 'Postmaster <[email protected]>' $m.AddRecipient('Alice', '[email protected]') $m.Subject = 'Scheduled maintenance tonight' $m.Body = 'The server will restart at 22:00.' $m.Save() The queue, and a backup: $app.Status.UndeliveredMessages # tab-separated text, one row per message $app.GlobalObjects.DeliveryQueue.ResetDeliveryTime() $app.GlobalObjects.DeliveryQueue.StartDelivery() $b = $app.Settings.Backup $b.BackupDomains = $true; $b.BackupSettings = $true; $b.BackupMessages = $true $b.Destination = 'D:\backups' $app.BackupManager.StartBackup() The [Settings] keys are reachable generically: $app.Settings.IniSettingNames # every key the server knows $app.Settings.GetIniSetting('ManageSieveServerPort') $app.Settings.SetIniSetting('ManageSieveServerPort', '4190') Up to 6.3.3 a change this way usually needed a restart. Since 6.3.4 the keys are kept in the database, and Settings.IniSettingApplies(name) says whether a save applies a key at once, at a reinitialise or at a restart; Settings.PendingIniSettings lists the saved ones the server does not run on yet. Settings: when a save takes effect, and the three levels has the counts and what becomes of the file's copy. What changed in 6.3.4: a shared object takes effect at Save() Some collections are not loaded for your COM session alone: the running server reads the very same items while it works. They are the routes and their addresses, the incoming relays, the DNS black lists, the SURBL servers, the remote domain policies, the server messages, the blocked attachments, the SSL certificates and the groups. Up to 6.3.3, setting a property on one of those items wrote the live object every SMTP, IMAP and delivery thread was reading, which let a session read a string while it was being freed. Since 6.3.4: The first property you set makes a copy, held by your COM object, and every later set writes the copy. Reading a property back from the same object reads what you set. Nothing is in effect until Save(). Save() writes the copy to the database and the server reads the collection again. A property set and never saved changes nothing at all. Up to 6.3.3 it changed the running server until the next restart while the database kept the old value. A script that relied on that must now call Save(). An object read before the save keeps what it read. After a Save(), read the item again from its collection (ItemByDBID, ItemByName) to see the saved values. A route's addresses. A change to an existing address waits for that address's Save(), like every item here. An address saved or deleted through Route.Addresses reaches the running route at once, without the route's own Save(), as it always did. A RouteAddresses list read before the route's save is the old route's list, though an address saved or deleted through it still reaches the running route. A save the database refuses leaves the running item as it was, and your object keeps the refused values, to be corrected and saved again. A remote domain policy is the exception: it is read back as the database holds it. A save whose re-read fails still answers success, because the save succeeded, and is reported as error HM7390: the change is in the database and not yet in effect, until a reinitialise, a restart, or another save. The Control Panel reads each item fresh, sets, saves and reads again, so it is not affected. The other collections were never shared this way and behave as before: TCP/IP ports, security ranges, the white list, the greylisting lists, blocked senders, sender list entries, and everything loaded for the COM object itself (domains, accounts, aliases, lists, rules, fetch accounts, app passwords, folder permissions). What 6.3.4 adds to the object model Six new interfaces make 100 coclasses, and the IDL has 1,884 HRESULT declarations against 6.3.3's 1,375. The wiki page lists every member; this is the shape of it. Interface Added in 6.3.4 Application Roles, CurrentRole, CurrentPermissions, RoleAssignments, AssignRole, ResetPassword, UnlockAccount Settings RemoteDomainPolicies; GetChangedFromDefault, GetSettingDefault; IniSettingApplies, IniSettingAppliesAll, PendingIniSettings; the profiles (GetProfiles, ApplyProfile, RevertProfile and the rest) Domain Rules; the quota policy; the domain transforms and disclaimer; SenderList; the DKIM rotation members; SpamTraps Account MoveTo; GetDevices, RevokeDevice; RoleName, RoleDomains, RolePermissions; the nine Directory properties; ExportArchive; Kind (0 a person, 1 a meeting room, 2 a piece of equipment) Utilities among others GetAuditTrail, VerifyAuditTrail, RevertAuditedChange, GetSetupChecklist, MoveMailbox, RunDkimRotation DistributionList Membership, and the list manager's members BackupManager, BackupSettings LoadEncryptedBackup, RestoreMailbox, RestoreMessage; EncryptionPassphrase (write-only), EncryptionPassphraseSet New RemoteDomainPolicies and RemoteDomainPolicy; SenderListEntries and SenderListEntry; GreyListingBypassEntries and GreyListingBypassEntry eServerState gains hStateDraining (5). The rule criteria gain eFTAttachmentName (9), eFTSensitiveContent (10), eFTAttachmentType (11), eFTExternalRecipient (12) and eFTContent (13), and the rule actions eRAAddDisclaimer (11), eRAReject (12), eRARequireTLS (13), eRAQuarantine (14) and eRAWarnSender (15). Roles over COM, and why API keys are not Since 6.3.4, authority is a role: serveradmin, domainadmin, helpdesk, auditor or a custom one, given to an account for named domains. Nothing changes on the day you upgrade: an account with no assignment holds exactly what its AdminLevel has always given it. An account with a role signs in with its own address and password, and COM asks the same question REST asks. Application.CurrentRole and CurrentPermissions say what the session holds. Administrator roles end to end has the permissions, the domains a role reaches and the Control Panel pages each role sees. Giving a role over COM is Application.AssignRole("[email protected]", "helpdesk", "example.test"), with an empty role name to take it away. Account.RoleName, RoleDomains and RolePermissions read it back. Custom roles are made over REST (POST /api/v1/roles), and COM has no way to delete a role at all. A help desk and an auditor over COM. A help desk's script has Application.ResetPassword, UnlockAccount and GlobalObjects.MessageTrace; COM's settings objects answer only the server administrator, so an auditor reads the configuration through the Deck or REST. Refusals that do not give anything away. Application.ResetPassword and UnlockAccount answer No such account. alike for an account that does not exist and for one the role does not reach. Settings.ClearLogonFailureList answers the server administrator alone. The question 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) each 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. API keys are not a COM object. The API key store has never been COM-visible. The key store (/api/v1/apikeys) answers to the administrator password and to nothing else, and a key is given a role with POST /api/v1/apikeys and "role":"helpdesk". The Control Panel's REST API keys page does not go through COM either: it edits hMailServerApiKeys.ini directly. What this server has changed from stock Since 6.3.4, Settings.GetChangedFromDefault answers every setting whose value is not the one this version ships with, with the shipped value beside it: the same document, byte for byte, as GET /api/v1/settings/changed. Only the administrator password may read it; an API key of any scope and an account holding an administrative role are both refused. The settings topic says what each entry carries. Distribution lists and the directory A list that defines itself. Since 6.3.4, DistributionList.Membership is a string property, read and written like the moderator and bounce addresses, holding a membership query whose accounts are added to the list's own recipients. A domain administrator over COM may set domain, a pattern: in a domain it holds, or nothing; the server administrator may set any form, and a query the server cannot answer is refused when it is saved. Aliases, distribution lists and membership queries has the five forms and how a query is answered. The directory. Since 6.3.4 an account has Account.DirectoryTitle, .DirectoryDepartment, .DirectoryPhone, .DirectoryMobile, .DirectoryOffice, .DirectoryOptOut, .DirectoryNote, .DirectoryPhoto (base64) and .DirectoryPhotoType (read-only). The row is read at the first get and written by Account.Save() with the rest. A change of the opt-out through Account.Save() is recorded on the audit trail as directory-opt-out-changed. The Import Tool The Import Tool is a Windows program (Start menu > hMailServer > Import Tool) that works through the COM API: it brings mbox files into an account, handing each message to the server through Utilities.ImportMessageFromFileToIMAPFolder, reads a Maildir with its flags, and creates accounts in bulk from a text file. Bringing mailboxes in covers each of its jobs. The breach check applies to a COM script. It is off by default. Where it has been switched on, it runs whenever a password is set through COM Account.Password, the Import Tool's included, and a bulk operation makes one request per account: run a large import against a mirror, or with the check off. What it does not do The COM API and the Control Panel do not exist on Linux, and there is no Import Tool on Linux. The Import Tool does not read PST files. The file goes into the server's import directory, and REST, hmctl or the Control Deck start the job. COM has no API keys and cannot delete a role. These features have no COM surface at all and are REST only: SCIM provisioning, SAML and OpenID Connect sign-in, passkeys, the S/MIME authority, the send API, migrations, IP warm-up, eDiscovery, mail in to a webhook and mailbox encryption at rest, among others the wiki lists. Where to read more The COM API and Scripting, sections 23.1 to 23.5, and APIs Reference, sections 8 and 9. Roles.md, ChangedFromStock.md, DistributionLists.md and Directory.md. Migration.md for the Import Tool, and Upgrading.md for what an upgrader's scripts now see. The public documentation's COM API chapter and its Import Tool section. On this forum: Upgrading to 6.3.4, the 6.3.4 announcement, event scripts, and the REST API for what COM does not reach.
  • 0 Votes
    1 Posts
    135 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) refuses a rule's Run function action when the rule is saved unless it names a plain function, for a rule of every owner, over COM and REST, and setting one on a domain's or an account's rule is held to the server administrator alone, where up to 6.3.6 a credential confined to named domains, a domain administrator among them, could set it. An event script that goes on in a transaction after Database.ExecuteSQL has failed is refused every statement after it once the database has ended the transaction, and Database.CommitTransaction then fails: call Database.RollbackTransaction and run the whole transaction again, or stop. A ScriptTimeout above 4,294,967 seconds is taken as that, where it ended every Windows event script at once, and the VBScript calls of OnClientValidatePassword, OnExternalAccountDownload and OnDeliveryFailed are quoted as every other call is. See the 6.3.7 announcement. hMailServer 6.3.8 (2 November 2026) adds a sixteenth event, OnIMAPMessageMoved, called once for each message an IMAP MOVE or UID MOVE moved between two folders of the account signed in, after the command and off the IMAP thread, with the account and thirteen values, in VBScript and JScript on Windows and JavaScript on Linux; it is off as shipped, and IMAPMessageMovedEvents (COM Settings.Scripting.IMAPMessageMovedEvents, imap_message_moved_events in PUT /api/v1/settings/scripting) switches it on (EventScripts.md). Work that outlasts a stop of the servers now writes its interruption to the debug log, not the ERROR log, so OnError is not called for it, and an event script's call that meets a stop fails with "The server is stopping. The request was not completed.", its script error going to the debug log. See the 6.3.8 announcement. This topic covers event scripts in hMailServer 6.3.4: the languages, switching scripting on, the fifteen events, the objects, the limits and errors, and the 6.3.4 security fix. If you run a Windows server older than 6.3.4 with event scripting on, that fix is the one thing to know first: a value written into a JScript handler's call, a password among them, could run code inside the server, and so could the name in a rule's Run function action, in either language. The section on CVE-2026-100741 below says what to do until the server is upgraded. What an event script is An event script is one file, EventHandlers.vbs or EventHandlers.js, in the server's Events directory, holding functions named after the events they handle. When scripting is on, the server calls the function for each event as it happens, hands it the objects the event is about (the client, the message, the account) and reads back what the function decided through the Result object. A rule's Run function action calls a function of the same file by name, and it too needs scripting on. Scripting is off as shipped, and with it off no handler is called. The file is loaded all the same: at start, at every reload, at Reload() and at POST /api/v1/settings/scripting/reload. Loading a file runs its code outside functions, once to compile it and once more for each of the fifteen handlers looked for, so keep the top level to declarations. A top level that throws, or runs past the timeout, is reported as a load that failed (HM5016, HM5710 or HM5021), whether scripting is on or not. A handler runs inside the server, on the thread that is doing the work, as the service's account: LocalSystem on a Windows installation that began before 6.3.4, NT SERVICE\hMailServer on a new one from 6.3.4, the hmailserver user on Linux. Everything behind a slow script waits, so try a script first on a server not carrying your mail. Which language runs where Language Windows Linux Engine File VBScript runs refused at load Windows Active Scripting (vbscript.dll) EventHandlers.vbs JScript runs refused at load Windows Active Scripting (jscript.dll), ECMAScript 3 EventHandlers.js JavaScript refused at load runs QuickJS 2025-04-26, built into the server, ES2023 EventHandlers.js Up to 6.3.3 a Linux server had no script engine and refused any script; since 6.3.4 it runs JavaScript in QuickJS, with the same fifteen events, function names and objects. Windows keeps VBScript and JScript unchanged. The language is one setting: Settings.Scripting.Language over COM (Windows only), "language" in PUT /api/v1/settings/scripting over REST, and the Language (VBScript, JScript or JavaScript) field on the Control Panel's Advanced page. Since 6.3.4 it accepts all three words on both platforms, in any case, and refuses any other word without storing anything. It is seeded VBScript on every platform, so a Linux server switching scripting on sets JavaScript first. Storing the language loads nothing. The script in force goes on running, in the language it was loaded in, until the next load. A language the build does not run is refused when the script is loaded: one line in the ERROR log, HM5016 at a start or HM5710 at a reload (the earlier script then stays in force), naming the language to use, once per load. While scripting is switched off this refusal is not reported, so switch scripting on and reload before reading the log for it. GET /api/v1/status says which languages a build runs, under build.script_languages: ["VBScript","JScript"] on Windows, ["JavaScript"] on Linux. Switching it on On Windows there are two Control Panel pages under Maintenance: Advanced, whose Scripting tab turns the engine on and chooses the language, and Event scripts, which edits the file. Save & reload writes a .bak first, then reloads and checks the syntax. Over COM the switch is Settings.Scripting.Enabled; Settings.Scripting.Reload() re-reads the file and CheckSyntax() returns the error text, where empty means clean. There is one file and no include mechanism. On Linux, from the command line: Set the language: hmctl settings set scripting --set-string language=JavaScript Write the file as the service's user: sudo -u hmailserver tee /var/lib/hmailserver/events/EventHandlers.js <<'EOF' function OnAcceptMessage(client, message) { if (message.Subject.includes('[URGENT]') && !client.Authenticated) { Result.Value = 2; Result.Message = 'Urgent mail comes from people who have logged on.'; } } EOF Switch scripting on: hmctl settings set scripting --set enabled=true Confirm which file the server reads: hmctl settings get scripting current_script_file Load it with POST /api/v1/settings/scripting/reload. POST /api/v1/settings/scripting/check answers {"result":""} when the file compiles and the error, with its line, when it does not. The fifteen events Every handler is optional. When the script is loaded the server checks which of these names the file declares and fires only those. Names are matched exactly: OnErrorLog is not a handler, the name is OnError. Handler Fires when Called with What Result.Value does OnClientConnect(client) any TCP connection is accepted, before a greeting the client 1 closes the connection at once OnHELO(client) the SMTP client sent HELO or EHLO the client 1 answers 554 5.7.1 Rejected; 2 answers 554 5.7.1 with Result.Message; 3 answers 453 4.7.0 with Result.Message OnClientLogon(client) an SMTP, POP3 or IMAP client finished a logon attempt, successful or not the client; client.Authenticated says how the logon went nothing: the logon has been decided OnClientValidatePassword(account, password) a password is being checked, before the built-in check the account and the password typed 0 lets the client in; 1 turns it away; 2 (as given) leaves it to the stored password OnSMTPData(client, message) the DATA command arrived, before the body the client and the message as OnHELO OnAcceptMessage(client, message) the body has been received and is about to be queued the client and the message as OnHELO OnRecipientUnknown(client, message) a RCPT TO named an address this server does not host the client and the message nothing OnTooManyInvalidCommands(client, message) an SMTP client sent too many invalid commands the client and the message nothing OnDeliveryStart(message) the first step of a delivery attempt, before virus scanning the message 1 deletes the message OnDeliverMessage(message) after virus scanning and global rules, before the recipient split the message 1 deletes the message OnDeliveryFailed(message, recipient, error) once per failed recipient; may fire several times per message the message, the recipient, the error text nothing OnExternalAccountDownload(fetchAccount, message, uid) a message was fetched from an external POP3 or IMAP account the fetch account, the message or null, its UID on the remote server 1 deletes it on the remote server now; 2 after Result.Parameter days; 3 never OnBackupCompleted() a backup finished nothing nothing OnBackupFailed(reason) a backup failed the reason nothing OnError(severity, code, source, description) anything is written to the ERROR log the error, as the ERROR log has it nothing OnError's severity is 1 Critical, 2 High, 3 Medium, 4 Low, and its code is the HM<nnnn> number without the prefix. OnClientValidatePassword setting 0 is a full authentication bypass for that account, on every protocol. A handler that bridges to another directory should fail closed: set 1 when the lookup errors, never 0. The objects a handler is given The objects an event has are also globals of the same names, HMAILSERVER_CLIENT, HMAILSERVER_MESSAGE, HMAILSERVER_ACCOUNT, HMAILSERVER_FETCHACCOUNT, Result and EventLog, and the parameter and the global are the same object. An object an event does not have is null as a parameter; as a global it may not be defined at all (the message of an external download that failed), and reading an undefined global throws a ReferenceError, so a handler reads its parameter. Do not use Result in the seven events that are notifications. EventScripts.md says it is not defined at all in OnClientLogon, OnRecipientUnknown, OnTooManyInvalidCommands, OnDeliveryFailed and OnError; the public documentation adds that the server makes no Result for OnBackupCompleted and OnBackupFailed. Object Members On JavaScript (Linux) Client Port, IPAddress, Username, HELO, Authenticated, EncryptedConnection, CipherVersion, CipherName, CipherBits, SessionID the same, all read-only Message ID, Filename, Subject, From, FromAddress, To, CC, Date, Body, HTMLBody, Charset, Recipients, AddRecipient(name, address), ClearRecipients(), HeaderValue(name), HasBodyType(type), Flag(flag), Size (KB), State, DeliveryAttempt, UID, InternalDate, Copy(folderId), RefreshContent(), Save(), Attachments, Headers Attachments and Headers are not there; SetHeaderValue(name, value) and SetFlag(flag, on) write; InternalDate is text Result Value, Message, Parameter Value and Parameter take a whole Number only EventLog Write(text) the same FetchAccount the COM FetchAccount readable, not assignable; Password, Save(), DownloadNow() and Delete() are not there Account the COM Account, freshly loaded the properties that describe the account are readable, not assignable; Password, the collections and every method are not there Save() is what writes a change to disk. Changes to the subject, the body and the headers are written to the message file by Save(), on both platforms; a handler that does not call it changes nothing on disk. Do not call it in OnSMTPData: the public documentation says the message is still being processed there and saving corrupts it, and that only the character set, the file name, the envelope sender and the recipients are reliable at that point. Change a message in OnAcceptMessage. Result is read after the handler returns. On JavaScript a string ('1' too), a boolean, null, undefined, NaN, a fraction or an object assigned to Value or Parameter throws a TypeError, and the event goes on with the Result it had. So in OnClientValidatePassword, Result.Value = decisions[user] for a user the table does not have leaves 2, the stored password. On Windows, COM turns a boolean, undefined, a fraction and a string of digits into numbers, and undefined into 0: the same line in JScript lets that client in. EventLog.Write is the debugger. It appends a line to hmailserver_events.log in the log directory, and there is no breakpoint. A rule's Run function A rule's Run script function action calls the named function as <function>(HMAILSERVER_MESSAGE), then reloads the message from disk, so anything the function saves is seen by the rest of the rule set. Since 6.3.4 it works on Linux, where the function is a JavaScript one in EventHandlers.js. Since 6.3.4 the action names the function alone, as a plain identifier: ASCII letters, digits, _ and $, not starting with a digit. StampReceivedDate qualifies; StampReceivedDate(x) does not. On Windows, in VBScript or JScript, any other name is refused as HM5018 each time the rule matches and the function is not run. On Linux such a name is never evaluated and finds no function: nothing is run, and the ERROR log has a Script Error line, not HM5018. A ported handler JScript and JavaScript share the file name, so a JScript file carried from Windows to Linux is the file the JavaScript engine is given. EventScripts.md's example starts from VBScript on Windows: Sub OnAcceptMessage(oClient, oMessage) If InStr(oMessage.HeaderValue("X-Spam-Flag"), "YES") > 0 And Not oClient.Authenticated Then oMessage.HeaderValue("X-Quarantine") = "yes" oMessage.Subject = "[SPAM] " & oMessage.Subject oMessage.Save EventLog.Write("Tagged spam from " & oClient.IPAddress) End If End Sub The same handler for a Linux server, in EventHandlers.js: function OnAcceptMessage(client, message) { if (message.HeaderValue('X-Spam-Flag').includes('YES') && !client.Authenticated) { message.SetHeaderValue('X-Quarantine', 'yes'); message.Subject = `[SPAM] ${message.Subject}`; message.Save(); EventLog.Write(`Tagged spam from ${client.IPAddress}`); } } What changes in a port: Parameterised properties have no assignment. message.HeaderValue('X') = 'v' becomes message.SetHeaderValue('X', 'v'), and message.Flag(n) = true becomes message.SetFlag(n, true). The reading forms are unchanged. Names are case-sensitive and spelled as the IDL spells them. JScript found message.FileName; JavaScript needs message.Filename. No CreateObject, no new ActiveXObject. Both exist on JavaScript only to throw a TypeError that says so. null, not Nothing, for an object an event does not have. The limits A fresh engine for every event, on Windows and Linux alike: the whole file is parsed and run, the handler is called, and the engine is freed before the event goes on, so nothing a script makes outlives the event. No file system, no network, no processes on JavaScript. QuickJS keeps these in quickjs-libc.c, and that file is not in the tree. No module loader is installed, so import() is refused and an import statement is a syntax error; there is no print and no console. What a handler can change is what its objects change: a message's file through Save(), a copy into a folder through Copy(), the event log through EventLog.Write. On Windows, ScriptAllowedObjects decides what a script may create with CreateObject or new ActiveXObject. Its shipped value is *, every class. Empty means no class beyond the server's own objects, and a comma-separated list of ProgIDs means only those. Up to 6.3.3 it applied at a service restart; since 6.3.4 a save applies it. It has nothing to apply to on Linux. ScriptTimeout is in seconds, with 0 for no limit. It is kept in the settings store: set it with PUT /api/v1/settings/ini/ScriptTimeout, COM Settings.SetIniSetting or the Control Panel, not by editing hMailServer.ini. Limit Platform Value When a handler reaches it ScriptTimeout Windows 60 seconds by default HM5019; the interrupt cannot release a handler blocked inside a COM call ScriptTimeout Linux 60 seconds by default HM7341; the event goes on with whatever Result the handler had set Memory Linux 64 MB for each run, a constant HM7342; the event goes on Stack Linux 512 KB of the calling thread's stack a runaway recursion throws an InternalError The Linux timeout does not stop one call into a built-in that loops in the engine's C code without polling, such as includes or indexOf on an object whose length is huge: do not call an array method on an object whose length the handler has not checked. Errors, and where they are written What Where A script that does not compile, or whose top level throws HM5016 at start (nothing loaded), HM5710 at a reload (the earlier script stays in force) A top level that runs past the timeout HM5021, and the load refused An exception a handler does not catch one Script Error line in the ERROR log; the event goes on The load is all or nothing on every engine, so a typo in a reload never switches a working OnClientValidatePassword off. The 6.3.4 security fix: CVE-2026-100741 Up to 6.3.3 a password could run code inside the server. On Windows, with event scripting on, the language JScript and an OnClientValidatePassword handler in EventHandlers.js, any client that could reach a sign-in and name an existing, active account could have JScript of its own run inside hMailServer.exe, with the rights of the service's account, before it had authenticated. The server built each call as source text and escaped the apostrophe but not the backslash before it. With ScriptAllowedObjects at its shipped *, that script could create any COM object, WScript.Shell among them. Path Who could use it Open in The password given to OnClientValidatePassword any client that can reach a sign-in JScript The UID given to OnExternalAccountDownload a remote POP3 server JScript The error text and failed recipient given to OnDeliveryFailed a remote mail server JScript The name in a rule's Run function action whoever may edit a rule, a domain's own administrator included JScript and VBScript Which servers are affected. The flaw is CVE-2026-100741; the record was not yet public when this was written. The flaw is in every release from 6.0.0 to 6.3.3: the escaping was checked at every one of those tags (GitLab issue 27). Treat any server older than 6.3.4 with event scripting on and JScript as its language as affected. A rule's Run function action wrote its function name into the call unescaped, in either language, so VBScript servers too. What 6.3.4 changes. Every JScript value is escaped in one helper, ScriptServer::EscapeForJScriptLiteral: every backslash first, then both quotes, CR, LF, U+2028, U+2029 and NUL; OnBackupFailed's reason, which neither language escaped and the table above does not list, is escaped in both. A rule's Run function name must be a plain identifier. On Linux the handler is called with values as values, so nothing is quoted into source text. A JScript handler now receives every value exactly as it was, backslashes included; one that undid the old half-escaping by hand must stop doing so. Until you can upgrade, switch event scripting off. That is the only measure that closes every path, in either language. A server that cannot do without its scripts can narrow what is open: Take the OnClientValidatePassword, OnExternalAccountDownload and OnDeliveryFailed handlers out of a JScript EventHandlers.js and reload the scripts. Check the reload: one that fails (HM5710 in the ERROR log) keeps the earlier script in force, those handlers included. In either language, let nobody but the server administrator make or edit a rule with a Run function action. Roles.md lets an account's rules be written over COM by the server administrator, an administrator of its domain, or the account itself, so "whoever may edit a rule" is wider than administrators. Narrow ScriptAllowedObjects from *, which limits what an injected script can create. The key exists from 6.2.25, and before 6.3.4 it applies only after a service restart. Moving a handler to VBScript closes the password, UID and error-text paths and leaves the rule path open. Before you upgrade, check your rules' Run function actions and put the function's bare name in any that does not qualify. After it, an HM5018 in the ERROR log (on Linux, a Script Error line naming the function) means one was missed. Upgrading to 6.3.4 covers the upgrade itself. Reporting a vulnerability goes through a confidential GitLab issue or the Service Desk address contact-project+hmailserver-hmailserver-86730559-issue-@incoming.gitlab.com, and not through a public issue or this forum. What it does not do OnClientValidatePassword is not consulted for SCRAM, which has no password to give it. Where to read more EventScripts.md: the member-by-member contract of every object, and the engine. The COM API and Scripting, sections 23.6 to 23.10. Rules and Sieve for the Run script function action, and Security Hardening, section 17.11. The public documentation's event scripts chapter. On this forum: hMailServer on Linux: what you get and what you do not, the 6.3.4 security announcement, the COM API on Windows and rules and the Run function action.
  • 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
    132 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) signs an authenticated SMTP submission as another domain hosted here only when its account may send as the address its From names - the list the From picker below draws on: 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; otherwise the mail is still accepted and sent, unsigned as that domain. The webmail's own send already holds From to the account, so mail sent from the page is signed as it was. 6.3.7 also carries the first step of the Control Deck's and the webmail's rebuild planned for 6.4 - the framework decision, the threat model of the web surface and the authorisation matrix with its check - with no product code. See the 6.3.7 announcement. This topic goes deeper into the webmail that hMailServer serves at /portal on the REST listener; what the page is and how it is turned on is in The webmail portal at /portal. The one thing to know first: most of what follows is the reader's own choice, bounded by a setting the administrator holds. Search with operators, the keyboard map, the rich-text editor, the From picker over aliases and granted mailboxes, plain signatures and the offline inbox listing were in 6.3.3; the three stars, the importance marker and its keys, and the rest of what follows are since 6.3.4. What the administrator bounds Each is a setting on PUT /api/v1/settings and the Control Deck's Settings view; since 6.3.4 many can also be set per domain or per account. Setting What it bounds Default recover_deleted_days How long deleted mail can be put back 0, off; at most 365 reader_offline_days_max The most days a reader may keep offline 30; at most 365; 0 offers none reader_masked_addresses_max Masked addresses per person 20; at most 200; 0 takes the page away reader_sender_addresses_max Send-as addresses per person, proved or not 5; at most 20; 0 takes the section away brand_logos_enabled Brand logos at all On; needs AuthenticationResultsEnabled and DMARC confidential_base_url The address the webmail is reached at from outside, which links are written against; falls back to quarantine_digest_base_url, then import_oauth_redirect_base_url Empty Search operators and keyboard shortcuts The search box searches the folder shown; the options panel beside it builds the same syntax from fields and can search every folder the account may read. Operator What it matches word, "quoted phrase" The word in the subject or the sender, failing those anywhere in the message; the words together, in that order -term, a OR b What follows the minus must not match; one of the two must match. OR in capitals - or is a word from:, to:, cc:, bcc:, subject: The sender's address or name; a recipient's, in To or Cc; an address or name in Cc, or in Bcc (only in the sender's own copy); the subject has:attachment, has:link, filename: An attachment; an http or https link; an attachment whose file name contains the text larger:, smaller: By size, as in larger:10M before:YYYY-MM-DD, after:YYYY-MM-DD By date older_than:, newer_than: By age: 7d, 2w, 3m, 1y in:folder Messages in that folder; a whole path may be given is:unread, is:read, is:flagged, is:unflagged, is:answered, is:muted, is:pinned By the flag or keyword label:name An IMAP keyword; label:$Important is the sidebar's Important view category: The inbox tab: primary, social, promotions, updates or forums in_reply_to: Messages whose In-Reply-To or References names the message-id Since 6.3.4 a search request opens at most 2,000 message files and stops after about two and a half seconds, saying where to go on from; Search older messages carries on. The keyboard map (? lists it) acts on the message list and an open message, and nothing is captured while the cursor is in a text field. j and k move, Enter or o opens, u or Esc goes back, x ticks a row, e archives, ! files as junk (or, in Junk, as not junk), # deletes to Trash, l labels, m mutes, c writes, r a f reply, reply to all and forward, 1 to 9 run a quick step, / searches and Ctrl+K opens the palette. s gives the next of three stars; the second and third are the $Star2 and $Star3 keywords on top of the IMAP flag, so another client sees a flagged message. Offline mail The webmail has always kept the inbox listing and the last opened messages - thirty-one of them according to OfflineMail.md and EvidencePack.md, thirty according to README.md and the wiki - and an outbox of sends that failed for want of a connection. Since 6.3.4 it can also keep the text, formatting and attachments of the last days in the browser's own storage, and nothing is kept until the reader chooses. Settings, Offline in this browser, offers Nothing beyond the inbox listing and then the last 7, 14, 30, 60, 90, 180 or 365 days, up to the server's ceiling. The choice is the account's offline_days preference, so it follows the person between browsers; what is kept does not. How it fills. The store is IndexedDB, database hmPortalOffline. A message goes in when it is opened and the window covers it, and in the background, newest first. An attachment above 8 MB is not kept, and a message's parts stop at 32 MB together. When the window is bigger than the browser allows the site, the card says so and the newest are kept. Without a connection a message opens from the store, a folder lists what the store holds, and a search runs over the kept words with the operators the store can answer. has:link and in_reply_to: let a message through rather than hide it, OR is ignored, and a date operator is measured on the browser's clock. What it is not. It is not a backup and it is not synchronised back: a flag set offline is not sent when the connection returns. It is not encrypted beyond what the browser does with its own storage, so anyone who can open this browser profile can read what is kept. It does not survive sign-out: the database is deleted. Clear what is kept deletes everything now and sets the choice to nothing. Important, and sections of the inbox Gmail's importance marker, decided in the browser: no model is trained anywhere, and nothing about whom this person reads or writes to is sent to the server to decide it. The page counts in localStorage and forgets the whole of it at sign-out. What it decides is one IMAP keyword: $Important on a message judged important, $NotImportant on one the reader says is not, so Thunderbird, a phone and hmctl all see them. Each inbox message is judged once, the first time this session lists it; two points make it important. Reason Points You marked mail from this sender important, or not important +4, or -4 You write to this sender +3 You answered this sender +2 You open mail from this sender (two or more opened here) +1 You starred mail from this sender +1 It was sent to you by name (a To of five or fewer) +1 It is in one of the other tabs: Social, Promotions, Updates or Forums -2 A message that already carries either keyword is never judged again. The marker is changed from the arrow on the row, with an Undo, or with the keys + or = and -. Sent and Drafts have no marker. In Settings, Importance marker offers the page deciding (the default), marking by hand only, or no marker; Inbox sections can draw Important, Starred, Everything else (off by default). It will be wrong, which is why every mark can be undone in one press. It does not sort, hide or delay anything, and what it has learned stays in this browser; the keywords on the mail travel. A message that expires Gmail's confidential mode: the message is kept on this server, and what crosses the internet is a short notice with a link. It is not end-to-end encryption. A message this server can delete on a timer is a message this server can read, and anyone who administers the server, or a backup of it, can read a kept message while it exists. Nor does it stop a recipient photographing the screen. Sending. On the compose form, Expires (never, in an hour, a day, a week, a month) and an optional Passcode. Over the API, expires_in (seconds, 1 to 7,776,000 - ninety days) and passcode (4 to 64 characters) on POST /api/v1/me/messages. The send is refused when the server has no public address to write the link against (403, confidential_unavailable), and for a message the page signed or encrypted. The kept copy is a file under <data directory>/Confidential/<account id>/ with a row in hm_confidential (schema 6069); it counts against the mailbox's quota (413, quota_exceeded), and an account has at most 1,000 outstanding at once (400, confidential_ceiling). Each recipient is sent the notice; the sender's Sent folder gets the full message. Reading. The page at /confidential/<token> takes no credentials. The reader proves they are a recipient in one of three ways: signed in to this server's webmail as a recipient address; a six-digit code mailed to a recipient address, good for ten minutes and five tries; or the passcode, where ten wrong answers pause it for a quarter of an hour. After thirty wrong answers in all, codes stop opening the message. The page answers the same way whether or not the address typed is a recipient's. It shows the text, names other attachments without offering them, and runs no script. After the expiry or a withdrawal it answers 410. The sender's side. Messages that expire, on the Sent folder's menu, lists what was sent and whether and by whom it was opened, with a Withdraw button. The kept file is deleted when the expiry passes. The copies are not in the backup, but an archive taken by a 6.3.4 build made before the release holds them, in the clear: treat it as the mail itself, or delete it. Send-as and masked addresses The webmail's From picker offers four kinds of address: Kind Given by The account's own address Always An alias that points at it The administrator Another mailbox on this server That mailbox's owner, with the post (p) right on their INBOX An address at another provider The person, proved with a code mailed to it With SmtpAuthenticatedSenderCheck on, an authenticated SMTP session's MAIL FROM is held to the same list: see who may send as whom. Since 6.3.7 the DKIM signature follows the list too: an authenticated submission whose From names another domain hosted here is signed as that domain only when the account may send as that address, and is otherwise accepted and sent unsigned as that domain; the webmail's own send already holds From to the account. Proving an address elsewhere (since 6.3.4). Under Settings, Other accounts, Send mail as the person types the address and presses Send the code. The server mails a six-digit code to it from the postmaster of the account's domain, and the person types it back. The code lives in the server's memory for an hour and is never stored; five wrong tries end it. An address of a domain on this server is refused. What this cannot change: mail this server sends as [email protected] is not signed by Gmail's DKIM key and not sent from an address Gmail's SPF record names, so a receiver that honours the domain's DMARC policy will quarantine or refuse it when that policy says so. The page says so beside an address whose domain publishes quarantine or reject. For such an address, send from its own provider. Masked addresses. Under Settings, Masked addresses the person says what the address is for and presses Make an address; the server makes one in the account's own domain, such as [email protected]. While it is on, mail to it is delivered to the account, it is in the From picker (kind: masked), and a reply to mail that came through it goes out from it. Switched off, mail to it is refused at RCPT exactly as mail to an address that never existed is - not handed to the domain's catch-all. An account, alias or list made later with the same address takes it over. Both kinds of address are carried by the backup with the account. Recovering deleted mail It is off by default: recover_deleted_days (0 to 365) turns it on for the whole server, and a domain or an account may keep its copies for fewer days than the level above, never for more. What is kept is a message a person deletes: IMAP EXPUNGE, UID EXPUNGE and CLOSE on a folder with messages marked \Deleted; POP3 DELE; the webmail's delete and its emptying of a folder; and the retention the person chose, or the administrator's retention policy. What is removed for any other reason is not kept: an IMAP MOVE, a scheduled draft that has been sent, an account being deleted. Putting it back. Recover deleted items, on the Trash folder's menu and in the command palette, lists what was deleted within the window. Put back returns a message to its folder with its flags - or to a folder of the same path, or otherwise the INBOX - as a new message with a new UID. Delete for good removes it now. The copies are files under <data directory>/Recoverable/<account id>/; they count against no quota and are not in the backup. The retention task, six-hourly, ends the copies older than the window. Signatures and formatted messages A logo in the signature. Add image in the signature's toolbar (Settings > Name and signature) takes a PNG, JPEG, GIF or WebP file, judged by its first bytes. The whole formatted signature is at most 64 KB - 64,000 bytes, its images included, which leaves room for a logo of up to about 45 KB. A file that does not fit is drawn smaller, starting at 600 by 200 pixels. The limit is the account's signature through the webmail: the Control Panel, COM and the administrator's REST route do not check it. What the recipient receives. Gmail and Outlook do not display a data: image, so the server moves each one into a part of its own with a Content-ID and points the HTML at it with a cid: URL - multipart/related inside the alternative. At most 64 different images in one message are moved. Not every mail client displays WebP; a logo that has to show everywhere is safer as a PNG. The formatted editor rebuilds what is written from an allowed subset of HTML and drops everything else while keeping the words: a style, a colspan, a <script>, a remote <img src="https://...">. Since 6.3.4 it takes three more things: Emoji: a picker of about a thousand characters in eight groups, kept in the page itself. The names are English; there are no skin tones and no flags. Pictures, pasted, dropped or chosen: PNG, JPEG, GIF and WebP by their first bytes, with SVG refused; drawn down to 1,200 pixels on the longer side; at most 64 in a message. Tables, up to 50 by 20, written with border="1" cellpadding="6" cellspacing="0" as attributes, because Outlook renders mail with Word's engine and ignores most of a stylesheet. Tab moves to the next cell and, in the last cell, adds a row. There are no merged cells, no column widths or colours of the reader's choosing, and no resizing a picture once it is in. A second account in the same page Two mailboxes side by side: the account the person signed in as, and one more, added under Settings, Other accounts with its address and password. One press switches the whole page to the other. Nothing is collected or copied; this is not the Other accounts feature beside it, which collects mail from another mailbox into this one. The second account's session lives in the page, not in the browser. A cookie is one per origin, so its session is a token the page holds in memory (POST /api/v1/session?bearer=1), written nowhere - so a reload forgets it, and the person adds the account again. Everything the browser keeps between visits stays the first account's: the offline store, the outbox, push, the recent searches and the Important tally. What is thinner. One second account, not several. Remote images are not loaded in its messages, its S/MIME keys are unlocked by hand, and the one-session-per-device policy counts it. The form is never minted for the administrator (403), and a bearer session is the mailbox and nothing more: the server drops any administrative role from every request made on one. Brand logos The list shows a sender's BIMI logo as its avatar only when the claim is checked. Three things have to be on: AuthenticationResultsEnabled=1, the DMARC check, and BrandLogosEnabled (on by default). GET /api/v1/me/brand-logo?domain= answers the SVG only when all of these hold: default._bimi.<domain>, or the same name at its organizational domain, is a v=BIMI1 record with an l= location. The domain's DMARC policy is quarantine or reject with pct=100. A domain at p=none gets no logo, even for mail that passed. The location is https, reached only at a public address, within ten seconds and 32 KB, with no redirect followed. The file is SVG Tiny Portable/Secure. Every answer is kept a day per domain. A refusal is 404 with an outcome - no_record, declined, weak_policy, not_https, fetch_failed, not_tiny_ps or switched_off - and the webmail shows the initials. A Verified Mark Certificate (a=) is not validated. What it does not do No ActiveSync and no Exchange Web Services: a phone uses IMAP and SMTP, and CalDAV and CardDAV. Recall reaches this server's own mailboxes only, and only unread copies. A folder of a hundred thousand messages is paged, searched and selected whole without the page copying it: built, and not yet measured. Where to read more Wiki: The Webmail. Documents: WebmailShortcuts.md, OfflineMail.md, ImportantMail.md, ConfidentialMessages.md, SendAs.md, MaskedAddresses.md, RecoverDeleted.md, Signatures.md, FormattedMessages.md, SecondAccount.md, BrandLogos.md. The public documentation: 36.6 The portal: what a user gets. On this forum: The webmail portal at /portal, The Control Deck, Signing in for passkeys and account recovery, and the 6.3.4 announcement.
  • 0 Votes
    1 Posts
    132 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.8 (2 November 2026) has the web services listener, which serves CalDAV and CardDAV, refuse an address an auto-ban or an IP range bans: it closes the connection with no answer, before the TLS handshake, when the IP range that decides for the address allows none of SMTP, POP3 and IMAP, where up to 6.3.7 a banned address could go on guessing passwords there. So an IP range of your own that allows some addresses nothing now closes CalDAV and CardDAV to them as well: check that these services' clients do not come from one. Loopback and the addresses in WebServicesTrustedProxies are never refused by a range, and a failed sign-in from one of them is counted against the name alone (AccountLockoutThreshold, off by default), never towards a ban of that address. A 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. See the 6.3.8 announcement. This topic covers what hMailServer 6.3.4 serves to a phone or a desktop client over CalDAV and CardDAV, and what the webmail's Calendar, Tasks, Contacts and Directory pages do with the same data. The one thing to know first: CalDAV and CardDAV are served on the web-services listener, they take HTTP Basic as the account - its password or an app password - over HTTPS only, and a passkey or single sign-on does not reach them. CardDAV has been there since 6.3.3 with one address book; calendars, several and shared address books, the directory and rooms are all since 6.3.4. Setting up a phone or a desktop client A client given the mail address finds the server by discovery: /.well-known/carddav and /.well-known/caldav redirect to /dav/, a PROPFIND there answers the principal, and the principal answers the address-book home and the calendar home. iOS, macOS Contacts and Calendar, DAVx5 on Android and Thunderbird all sync what is there. The IMAP logon's lockout and auto-ban apply to the sign-in. Which password. An account with a TOTP second factor enrolled uses an app password, because once a secret exists the account password stops being a mailbox credential. Minting an app password on the webmail's Security page asks for the account's own password again, so an account that single sign-on or SCIM made, whose random password nobody was told, needs a password set by an administrator before its person can make an app password for a phone. The listener. The web services are off as shipped: WebServicesHttpPort and WebServicesHttpsPort default to 0, and the HTTPS port takes WebServicesCertificateFile and WebServicesPrivateKeyFile. Since 6.3.4 these keys are kept in the database: change them in the Control Panel (Windows), over REST, or on Linux with hmailserver --set-setting NAME=VALUE, not by editing hMailServer.ini. A saved port or certificate waits for a reinitialise or a restart, since the listener binds when it starts. On the Devices list a DAV client appears with the protocol dav and its user agent. It has no connection to drop - each request authenticates afresh - so revoking it means revoking the app password behind it. Collection What it is /dav/calendars/<address>/calendar/ The default calendar, Calendar, made the first time it is asked for /dav/calendars/<address>/<name>/ Another calendar of the account's own, made in the webmail or with MKCALENDAR /dav/calendars/<address>/shared-<id>/ A calendar another account, or a group, shares with this one /dav/addressbooks/<address>/contacts/ The default address book, where a client that has synced since 6.3.3 still finds it /dav/addressbooks/<address>/book-<id>/ Another book of the account's own (schema 6079) /dav/addressbooks/<address>/shared-<id>/ A book shared with this account, or with the whole domain /dav/addressbooks/<address>/directory/ The organisation's directory, read-only (schema 6078) Each calendar collection advertises VEVENT and VTODO, so a phone's calendar and its reminders list are the same collection. Calendars, events and tasks An account has one calendar until it makes more. The Calendars box beside the grid adds one with a name and a colour (POST /api/v1/me/calendars), renames or recolours it and deletes it with its events; the default cannot be deleted. Over CalDAV the colour travels as Apple's calendar-color, so a phone shows the same one, and PROPPATCH sets the name and colour from the phone's side. Events. An event takes a title, start and end as wall-clock times in tz (an IANA zone the server knows), all_day, a location and notes, rrule (daily, weekly on chosen days, monthly, yearly, with an interval and an end by count or by date), alarm_minutes and guests. The server writes DTSTART;TZID=<zone> so a weekly 09:00 stays 09:00 across a clock change, and a VALARM for the reminder, which is what CalDAV clients ring for. A repeating event is expanded across daylight saving in any of the 592 zone names the server knows; calendar-query with a time range, calendar-multiget and sync-collection are all answered, with ETags and If-Match. Changing one occurrence alone writes an override; deleting one writes an EXDATE on the master. Tasks. The Tasks page lists every VTODO of every calendar the account can see, soonest due first. Ticking one off writes STATUS:COMPLETED, COMPLETED and PERCENT-COMPLETE:100 as a client does. A second calendar is a task list of its own. Sharing a calendar, and delegates The owner names another account of this server and whether it may write (PUT /api/v1/me/calendars/{id}/shares/{address} with write). The other account sees the calendar in its own Calendars box and in its CalDAV home as shared-{id}/, with owner pointing at the sharer's principal and a current-user-privilege-set of read, or read and write. A read-only share takes no PUT, no event from the page and no PROPPATCH. The sharee leaves with a CalDAV DELETE of shared-{id}/ or from the page. Shared with a group. Since schema 6086 the name may be one of the server's groups (hm_groups) instead of an address; a name that is both is the account. The membership is read at the moment a member asks, so somebody added later sees the calendar and somebody removed no longer does. A member cannot leave a group share (403): they leave the group, or the owner takes the share back by the group's name. A delegate. A share may be a delegation (delegate: true, which implies write the sharee may answer the owner's invitations on the owner's behalf. The answer is POST /api/v1/me/calendar/events/{id}/reply with partstat ACCEPTED, TENTATIVE or DECLINED, allowed to the owner and to a delegate and refused (403) to a plain writer or a reader. The organiser receives a METHOD:REPLY whose ATTENDEE carries SENT-BY="mailto:<delegate>", From: the owner with Sender: the delegate. Free-busy An account of the domain can see when a colleague is busy, and nothing else about them. GET /api/v1/me/freebusy?address=<account>&from=<when>&to=<when> answers the busy periods of that account's own calendars - not the ones shared with it - over a window of at most 62 days (400 beyond). An event that is TRANSP:TRANSPARENT or STATUS:CANCELLED, or that the account declined, is left out; a period is tentative when the account's PARTSTAT is TENTATIVE or NEEDS-ACTION, else busy. No title, location or description is answered. Another hosted domain's account, an address nobody has, an inactive account, and an account whose freebusy preference is nobody are one and the same 404, so the route enumerates nothing. The preference is the person's own, in Settings: "Who may see when you are busy". Over CalDAV the same computation answers the free-busy-query REPORT on a calendar the caller may read, as one bare VFREEBUSY, over a time-range that needs both bounds and spans at most 62 days. The webmail's event editor draws an availability strip for the guests of a meeting from the same computation. Invitations by mail (iMIP) An event with guests, saved with "Send invitations by e-mail", mails each guest the event as a text/calendar part with method=REQUEST (RFC 6047) - what Outlook, Gmail and Apple Mail show as an invitation card. Saving a change mails an update with the sequence stepped; deleting the event mails METHOD:CANCEL. A message that carries a text/calendar part or an .ics attachment shows a card with Accept, Maybe, Decline and "Propose a new time" (METHOD:COUNTER). Filed on delivery. A METHOD:REQUEST delivered to an account is filed into its default calendar as it arrived (NEEDS-ACTION as a rule) when, and only when, it names the account as an ATTENDEE and not as the organiser. A message flagged as spam files nothing; a REQUEST naming somebody else is not kept; and an account that has never opened a calendar gets nothing filed. A stored copy under the same UID is replaced only by the same or a later SEQUENCE, and never when its ORGANIZER is not the invitation's. A REPLY delivered to the organiser sets that attendee's PARTSTAT in the stored event, so every phone syncing the calendar shows who accepted without anyone opening the mail; a CANCEL delivered to a guest marks the stored copy cancelled. Rooms and equipment (since 6.3.4) A room or a projector is an account whose kind says so: person (the default), room or equipment, a column of hm_accounts (schema 6101). It is invited like any guest and answers by itself, from its own address, by its booking policy; what it does not decide it leaves to its delegates. Where What The Control Deck The account editor's General group has a select of the three kinds; a room or equipment gets a Booking policy card REST kind on GET and PUT /api/v1/accounts/{address}; the policy at GET and PUT /api/v1/accounts/{address}/booking-policy COM (Windows) Account.Kind (0 person, 1 room, 2 equipment), saved by Save(); the booking policy is not on COM The Windows Control Panel Does not show it: a room is an ordinary account to it From the moment its kind is saved every sign-in as the room is refused, whatever the credential, with the ordinary wrong-password answer. A room with no policy row answers by the defaults: Field Default What it decides auto_accept true Answer invitations by this policy; false leaves every one for the delegates window_days 180 How many days ahead a booking may start, 1 to 1080 max_duration_minutes 1440 The longest booking; 0 is no limit working_hours_only false Only inside the working hours, on the working days work_days, work_start, work_end Monday to Friday, 08:00 to 17:00 The working days and hours time_zone UTC The zone the working hours are in, such as Europe/London allow_conflicts false Accept a booking over another one allow_recurring true Accept a recurring meeting bookers empty Empty is everyone in this server's own domains; a list is an address, @domain, a distribution list's address or a group's name. An organiser from outside must be named here capacity 0 How many people the room seats, shown in the directory; it decides nothing delegates none Addresses and group names, each given a delegate share of the room's calendar Who is asking. The room first decides whether the invitation's ORGANIZER is who sent it, from what the server recorded when the message arrived and never from a From: header or a MAIL FROM. An organiser at this server's own domains is believed only when the message was submitted by a signed-in session as that account, or as one allowed to send as it; a message sent through an API key is not believed. An organiser from outside is believed only when bookers names the address or its @domain and the message passed DMARC for that domain. SPF on its own is not enough, and with DMARC off no outside organiser is ever believed. Anything else is neither filed nor answered, and the delegates are told, within limits. The answer is decided just after delivery, on a work queue of the rooms' own; no delivery and no SMTP answer waits for it. In order: who is asking; may they book; sent automatically (an empty reverse path or Auto-Submitted, never answered); auto_accept; at most 20 answers an hour to one organiser; recurrence; the window; the longest booking; working hours; conflicts, counting every booking the room holds that is not cancelled as busy. Accepted, the booking is filed and a METHOD:REPLY goes from the room's address, marked Auto-Submitted: auto-replied. Declined, the reply carries the reason alone; a conflict says when, never whose. A request left for the delegates is never put into the room's calendar on the sender's word. A room with no delegates keeps what it leaves and tells nobody: name at least one. Two servers sharing one database are not serialised for rooms: run rooms on one node. Finding a room. The directory lists a room with its kind and capacity, and its CardDAV card says KIND:location (KIND:device for equipment). Every CalDAV principal says its calendar-user-type - INDIVIDUAL, ROOM or RESOURCE - and the principal-property-search REPORT on /dav/principals/, which is what a client's room finder sends, answers the domain's rooms and equipment. Apple Calendar searches for locations this way; Thunderbird has no room finder. Every request stays in the room's mailbox, and a full mailbox answers nothing more, so give a room a quota and a MessageRetentionDays that suit it. Contacts, groups and several address books Kept, and other contacts. A contact has a source: manual for what the person added, imported or kept, and collected for every address the person has written to - from the webmail's send and, since 6.3.4, from a mail client over an authenticated SMTP submission. A collected address is offered under Other contacts and by the To, Cc and Bcc completion, and nowhere else: CardDAV serves only what the person kept, so a phone's address book is not filled with everyone ever written to. Keep makes it a contact. What a card carries. A card a CardDAV client sends is kept as sent and served back byte for byte. The page reads birthday (BDAY), note, phones (every TEL) and the embedded PHOTO; a photo that is only a URL is not the server's to fetch. Groups. A group is a card of kind group - KIND:group in vCard 4.0, X-ADDRESSBOOKSERVER-KIND:group in 3.0 - so a group made on the phone appears in the webmail and the reverse. A group's name in To becomes that group's recipients, so the reader sees the people and not the group. The ceilings hold on both surfaces: 500 members in one group (a card past it is refused 403 CARDDAV:valid-address-data) and 200 groups in one address book (400 over REST, 507 Insufficient Storage over CardDAV). A group is not a distribution list: nothing about it reaches SMTP. Several address books, and shared ones. Since schema 6079 the account starts with one book, Contacts, which cannot be deleted, and may have at most 50. The owner shares one from the Contacts page with an address - one account of this server - or with its own domain's name; Can edit makes the share writable, and a book takes at most 200 shares. A domain-wide share aimed at another hosted domain is 403, and a person cannot leave a domain share: only the owner or an administrator removes it. One account holds one address once across all its books, so a contact is moved between books, never copied. Over CardDAV a read-only collection grants read and read-current-user-privilege-set and nothing else, a PUT into it is 403, and DELETE on the collection gives the share back and deletes nobody's contacts. MKCOL is refused: a book is made from the Contacts page or /api/v1/me/address-books. No group is shared, in either direction. The backup carries the books and their contacts; hm_addressbookshares is not carried, so the sharing is set again. The Control Panel and the Control Deck have no page for this. An administrator has GET /api/v1/accounts/{address}/address-books, which shows the shape of the sharing and never the contacts, and a route that removes one share, recorded as address-book-share-removed. Import and export. The Contacts page imports a vCard file or a CSV, including Google's and Outlook's exports. Export as vCard downloads contacts.vcf; Export as CSV downloads contacts.csv and leaves groups out. The organisation's directory It is one domain at a time, and always the reader's own: every active account of the domain whose owner has not opted out, and every enabled distribution list, with what is set on top in hm_directory (schema 6078) - job title, department, work telephone, mobile, office, a photo (JPEG, PNG or GIF, at most 100 KB decoded, judged by its bytes), and a note that is the administrator's alone. Two switches, both rows of hm_settings and both on by default. DirectoryEnabled is whether the directory is answered at all; turn it off on a server whose one domain holds people who are not one organisation. With it off the webmail hides its Directory page and the CardDAV collection stays, empty, so a subscribed phone syncs to nothing rather than failing every poll. DirectoryUserEditable is whether a person may set their own fields. The opt-out is never what a switch takes away, and it is not invisibility: mail still arrives and the address still works. An administrator changing somebody's opt-out is on the audit trail as directory-opt-out-changed. The surfaces. The webmail's Directory page (#/directory); the To completion, which offers the reader's own contacts first and the directory hits after; GET /api/v1/me/directory?q=&limit= for the person and GET and PUT /api/v1/accounts/{address}/directory for the administrator; a Directory entry tab in the Windows Control Panel's account dialog, and the same card in the Control Deck; and the read-only CardDAV collection, where PUT, DELETE and PROPPATCH are refused 403. Nothing is learned from anywhere else: there is no import from Active Directory or LDAP, even on a server that authenticates against one, and no LDAP server for a mail client to search. What it does not do No RFC 6638 scheduling (no schedule-inbox or schedule-outbox), no free-busy of a calendar on another server, no per-calendar time zone (calendar-timezone is not kept), no partial calendar-data, and no time-range filter on a VALARM. No ActiveSync and no Exchange Web Services: a phone uses IMAP and SMTP for mail, and CalDAV and CardDAV for the rest. Calendar shares and address-book shares are not in the backup, so set them again after a restore; the booking policy, the books, their contacts and the directory rows are. Where to read more Wiki: Connecting a Mail Client, Rooms and Equipment, The Webmail, Ports Reference. Documents: Calendar.md, Contacts.md, SharedAddressBooks.md, Directory.md, RoomsAndEquipment.md. On this forum: The webmail portal at /portal, The Control Deck, Signing in, Connecting a mail client, and the 6.3.4 announcement.
  • 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
    120 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) calls the anti-virus action that strips attachments Remove all attachments and deliver the text (Delete infected attachments only in 6.3.6's Control Panel), and under it a message whose one part is an attachment, and an S/MIME message signed as one part, now arrive as the action's note alone, where every version to 6.3.6 delivered them as they arrived with what the scanner had found in them; a message the server cannot read is deleted and each of its recipients sent the "message deleted" notice whatever the two notify settings say, while an encrypted message, and one that is one text part, are delivered as they are. In the Control Panel the sidebar has a Virus scanning group, and Ctrl+K takes every old title to its page. See the 6.3.7 announcement. hMailServer 6.3.8 (2 November 2026) lets the Linux server reach clamd over its Unix socket: ClamAVHost takes the absolute path of clamd's LocalSocket, such as /var/run/clamav/clamd.ctl for Debian's and Ubuntu's clamav-daemon, which listens on nothing else by default, and ClamAVPort is then not used; no connection test is offered for it on Linux, so a test message and the log show the scan (ClamAV.md). See the 6.3.8 announcement. hMailServer does not scan for viruses itself. It hands each message to an engine you install (ClamAV, ClamWin or any command-line scanner) and acts on the answer, and nothing described here is enabled on a fresh install. This topic covers the three scanners, what happens when one finds something or cannot run, the blocked-attachment list, and what 6.3.4 added: the sandbox, and the handling of mail sealed end to end. The one thing to know first is the failure mode: a configuration that looks enabled and is inert. A scanner switched on with a wrong host or a missing executable does not refuse mail and does not disable itself; when every scanner has errored the verdict is NoVirusFound, which from the message's point of view is identical to having been examined and found clean. 6.3.4 fixed one such case in the custom scanner, described below. What runs, and when Scanning happens on the delivery side, not during the SMTP conversation: the message has already been accepted and spooled by the time any scanner sees it. Which messages are scanned. For SMTP, a message is flagged when the connecting IP's security range has the virus-protection option; both default ranges ("Internet" and "My computer") have it. For mail this server fetches over POP3 or IMAP, the flag is the external account's own "use anti-virus" setting. A flagged message larger than AntiVirus.MaximumMessageSize is not scanned at all, silently; the default is 0, no limit. Two passes. The whole spool file is scanned first; then every MIME attachment is written to a temporary file and scanned separately, which is what matters for a scanner that does not decode MIME itself. If the message cannot be parsed for the second pass, error HM6001 is reported and only the whole file was scanned; if one attachment cannot be written to the temp directory, HM6002 is reported and that attachment was not examined. Concurrency. At most 10 scans run at once (MaxRunningScanners). A thread that cannot get a slot waits up to 60 seconds and then scans anyway, writing Scanning message without an available virus scanner slot. to the debug log. The read-only overview. Spam & virus filtering → Virus scanning overview in the Control Panel (Windows only) answers the question the settings page cannot: will an infected message be caught here. The three scanners All three can be enabled at once. They run in the order ClamWin, custom, ClamAV, stopping at the first that reports a virus. A scanner that errors does not stop the others. ClamAV (clamd) ClamWin (clamscan.exe) Custom scanner Switch AntiVirus.ClamAVEnabled AntiVirus.ClamWinEnabled AntiVirus.CustomScannerEnabled Default off off off Reached by TCP socket, INSTREAM one process per file one process per file Settings AntiVirus.ClamAVHost (localhost), AntiVirus.ClamAVPort (3310) AntiVirus.ClamWinExecutable, AntiVirus.ClamWinDBFolder AntiVirus.CustomScannerExecutable "Infected" is a reply matching stream…: <name> FOUND exit code 1 exit code equal to AntiVirus.CustomScannerReturnValue Timeout ClamMinTimeout 15 s to ClamMaxTimeout 90 s ExternalProcessTimeout, 300 s ExternalProcessTimeout, 300 s Virus name reported the name clamd returned always Unknown always Unknown ClamAV. Install ClamAV, run freshclam to download signatures, run clamd listening on TCP 3310, set the host and port, and press Test ClamAV connection. The button does three things in order: It sends nPING and requires PONG, so "nothing is listening" is told apart from "something else is listening". It sends nVERSION and reports what answered. It streams a harmless sample and then the EICAR test string, both from memory, and requires the first to come back clean and the second FOUND. A success looks like ClamAV 1.4.2/27700/Mon Sep 8 09:12:11 2026. Result: stream: Eicar-Signature FOUND; a failure names the step, for example Unable to connect to ClamAV server at localhost:3310. The custom scanner. Any program that exits with a distinguishable code will do. Put %FILE% where the file to scan belongs; without it, the file name is appended as the single quoted argument. The Control Panel ships presets, all of them Windows programs: Preset Command line "Infected" exit code Microsoft Defender "C:\Program Files\Windows Defender\MpCmdRun.exe" -Scan -ScanType 3 -File "%FILE%" -DisableRemediation 2 Sophos "C:\Program Files\Sophos\Sophos Anti-Virus\savscan.exe" -ss -archive "%FILE%" 3 ESET "C:\Program Files\ESET\ESET Security\ecls.exe" "%FILE%" 50 Bitdefender "C:\Program Files\Bitdefender\Endpoint Security\bdscan.exe" "%FILE%" 1 Kaspersky "C:\Program Files (x86)\Kaspersky Lab\Kaspersky Endpoint Security\avp.com" SCAN "%FILE%" 2 The 6.3.4 fix: a bare %FILE%. CHANGELOG records that a custom scanner whose command line holds a bare, unquoted %FILE% was handed the message file's path cut at its first space, and the default data directory, under C:\Program Files, has one. The scanner therefore scanned no file, answered with an exit code other than the virus one, and the message was delivered as clean. Since 6.3.4 a %FILE% outside double quotes is replaced by the path in quotes. One already inside them, "%FILE%" as the documented examples above write it, was not affected and is replaced as before. If you run an earlier release with a command line of your own, check it for an unquoted %FILE%. The same fix removes a trailing backslash from ClamWin's database and temporary folders, which escaped the closing quote of --database="..." and took the file to scan with it. The service account must be able to run the scanner. A new 6.3.4 installation on Windows runs the service as NT SERVICE\hMailServer by default; an upgrade keeps the account it had, LocalSystem unless the installer's page is answered otherwise (docs/ServiceAccount.md; the wiki's "LocalSystem by default" describes earlier releases and upgraded servers). Under the virtual account a custom scanner or ClamWin runs as that account: it reads the message in the temp folder, which is granted, and its own program and signatures, which the Users group can normally read; a scanner that needs more has to be granted it (Installing on Windows covers the account and its grants). Whichever account runs yours, the executable, its database directory and the temp directory must all be reachable by it. The symptom of getting it wrong is Unable to launch the virus scanner… reported as HM5406, once per message. For an engine with no command-line interface, the wiki points to an OnAcceptMessage event script or the HTTP filter hook. If you edit hm_settings by hand, note that the custom scanner's return value is stored under the misspelled name customviursscannerreturnvalue. Testing it end to end The test buttons check the scanner. To check the pipeline, send yourself a message with the EICAR test file attached and watch the application log: Virus found in message from [email protected]. Taking actions SMTPDeliverer - Message 4711: Message will be deleted (contained virus Eicar-Signature). Two things spoil that test on Windows. The host antivirus eats the EICAR file before hMailServer sees it, so exclude the mail store and the temp directory from real-time scanning. And your own outbound mail is not scanned by this server if it never comes back in through SMTP from an unauthenticated range, so send the test from an external address. What happens when a virus is found AntiVirus.Action What happens Delete entire e-mail (0, the default) The message is delivered to nobody. Logged as Message will be deleted (contained virus X). Delete infected attachments only (1; Remove all attachments and deliver the text in 6.3.7's Control Panel) The message is rebuilt, not edited: the first text part is kept, everything else is discarded, the VIRUS_ATTACHMENT_REMOVED server message is prepended to the body and the subject is prefixed with the VIRUS_FOUND message. Logged as Message attachments stripped (contained virus X). Since 6.3.7 a message whose one part is an attachment arrives as the note alone, where every version to 6.3.6 delivered it as it arrived, and one the server cannot read is deleted, each recipient sent the "message deleted" notice whatever the two notify settings say Understand the rebuild before you choose it: an HTML alternative, inline images and every clean attachment go with the infected one. AntiVirus.NotifySender and AntiVirus.NotifyReceiver are both off by default, and the wiki's advice is to leave them off: the sender address on a virus is virtually always forged, so a notification is backscatter. When a scanner cannot run Setting Default Meaning AVFailAction 0 0 delivers the message anyway. 1 never delivers unscanned mail AVFailRetryMinutes 15 Minutes between scan attempts while a message is held AVFailMaxHolds 16 Holds before the message is returned to its sender. 0 tells the sender immediately They are on Anti-virus settings → General, card When a scanner cannot run. The wiki describes them as stored in hMailServer.INI; since 6.3.4 the database is the settings store and a [Settings] line edited in the file is put back, so change them in the Control Panel, over REST (PUT /api/v1/settings/ini/{name}) or, on Linux, with hmailserver --set-setting NAME=VALUE. At the defaults, AVFailAction 1 gives a message about four hours to be scanned before its sender is told. The hold counter is the policy's own, not the message's try counter, because incrementing that would make the pass that finally scans the message skip DKIM signing. It lives in memory, so a restart gives every held message a fresh budget, and it is bounded at 100,000 tracked messages; beyond that messages are still held, never delivered unscanned, and HM6195 is reported. Every scanner error is reported as HM5406 with the failing scanner's own text. Blocked attachments Spam & virus filtering → Blocked attachments refuses files by name pattern. The master switch is AntiVirus.EnableAttachmentBlocking, default off. The wiki's starting list: *.exe *.scr *.pif *.bat *.cmd *.com *.vbs *.js *.jse *.wsf *.wsh *.hta *.jar *.msi *.msp *.cpl *.lnk *.reg *.ps1 *.iso *.img The attachment is not deleted. Its file name has .txt appended and its content is replaced by the ATTACHMENT_REMOVED server message, which names the original file through the %MACRO_FILE% macro, so the recipient receives invoice.exe.txt containing an explanation. Matching is a case-insensitive wildcard over the declared file name. It does not look inside archives and cannot see the real type of a file whose extension lies: it is a policy control, not a scanner. Since 6.3.4 the rewrite is logged. A rewritten message produces VirusScanner - Message 1234 from [email protected]: the attachment(s) invoice.exe matched the blocked-attachment list. Their content has been replaced with a notice and the message rewritten. Before this the rewrite left no record anywhere but inside the message itself. Sealed mail: what the list and the scanner do with it Since 6.3.4. A message encrypted end to end, S/MIME or PGP/MIME, is opaque to the server, which decides that from the body, never from the Content-Type label, so a message only labelled encrypted is blocked, stripped and scanned like any other. What opaque means exactly, and what the rest of the server does with such a message, is in the topic on end-to-end encryption and sealed mail. For the list and the scanner, none of it is a setting: The blocked-attachment list never rewrites an opaque message. In 6.3.3 and earlier blocking *.asc, *.p7m, *.p7s or *.* replaced the content of every sealed message with the notice, with no log line, and the plaintext existed nowhere else. It is now delivered as it arrived, and the application log says it was not checked against the list. A signed message is checked like any other, with its detached signature left alone. The scanner still scans it, and what it examines is ciphertext, so a clean result is not a verdict on the message; the log says its content COULD NOT BE EXAMINED for viruses. It is deliberately not treated as a scanner failure, so a server with AVFailAction 1 does not hold every encrypted message it receives; a policy that unscannable mail must not be delivered belongs in a rule or a Sieve script, not the scanner. The "delete attachments" action never strips an encrypted message. When the scanner objects to one, the message is left as it arrived and the log ends Set the anti-virus action to delete the message if such a message must not be delivered. That is not a quiet pass: the message still carries what the scanner objected to. To 6.3.6 this held for every opaque message; since 6.3.7 an S/MIME message signed as one part (application/pkcs7-mime holding signed-data, which is opaque but not encrypted) is replaced by the action's note, as any other single-part attachment is. The sandbox Since 6.3.4 (schema 6091). ClamAV says whether a file is known to be bad. What an unknown file does when it is opened takes a sandbox, a service outside this server which you run or subscribe to. It does nothing until you name one: SandboxEnabled is off, and cannot be saved on while SandboxSubmitUrl or SandboxVerdictUrl is empty. Both addresses must be https, or plain http to this machine. After the anti-virus scan and before the rules, a message that no signed-in user of this server submitted, with an attachment whose extension or media type is in SandboxTypes, is held: in no mailbox, not in the quarantine, and in nobody's digest. The default types are: exe,dll,scr,js,jse,vbs,wsf,hta,msi,bat,cmd,ps1,jar,lnk,iso,img,docm,xlsm,pptm, doc,xls,ppt,rtf,pdf,zip,7z,rar The sandbox says What happens every file clean delivered, with an X-hMailServer-Sandbox header reading clean (<n> file(s), <seconds>s) any file malicious, or suspicious at or above SandboxScoreThreshold (70) quarantined with the verdict as the reason; returned to its sender with 5.7.1 while QuarantineEnabled is off an error, once every file has answered, or no answer within SandboxTimeoutMinutes (15) SandboxTimeoutAction: deliver (0, the default) delivers with a header saying so; bounce (1) returns it to its sender The contract. One POST per file to SandboxSubmitUrl as multipart/form-data with four fields (file, filename, sha256, message_id), then a GET to SandboxVerdictUrl every SandboxPollSeconds (30), answered with JSON such as {"state":"done","verdict":"clean","score":3,"detail":"opened, nothing happened"}. With SandboxToken set, every request carries Authorization: Bearer <token>; the token is not in the backup archive. No vendor adapter is shipped. The document notes that a service which shares what it is sent, as VirusTotal shares uploads with its subscribers, is a place the attachments of the mail you receive go. What is exempt. What your own signed-in users submit (authenticated SMTP, the webmail, the send API) is not uploaded unless SandboxInternalMail is on. The document states the threat model of that default: a stolen password used to send attachments to colleagues is not examined by the sandbox, though ClamAV, the blocked-attachment list and compromised-account detection still apply. Limits. An attachment larger than SandboxMaxSizeKB (20480) is not submitted. A message with more files to submit than SandboxMaxFiles (10) is not held, and SandboxTimeoutAction decides it at once. Archives are submitted whole; links are not opened anywhere. A clean or malicious verdict is remembered by the file's SHA-256 for SandboxCacheHours (24), server-wide. What the administrator sees. GET /api/v1/sandbox/held lists what is held; POST /api/v1/sandbox/held/{queue_id}/release delivers without the verdict and DELETE /api/v1/sandbox/held/{queue_id} discards, both on the audit trail. The Control Deck's queue page has a Held for a sandbox box, and hmctl queue list has a held column. The settings are on the Control Panel's Anti-virus page, Sandbox tab (Windows only), and on PUT /api/v1/settings. Rules, and the message store Since 6.3.4 the mail flow rules can match the attachment's name and, in the outbound content policy, its type read from its bytes, so a rule can hold or refuse what the blocked-attachment list would only rename; that is in the topic on mail flow rules. The message store options added in 6.3.4 change nothing for a scanner: a message is scanned as plain RFC 822 in the queue and only placed in the mailbox in its stored form afterwards. A scanner pointed at the data directory from outside the server reads a compressed or encrypted container, not a message. Error codes Code Meaning HM5406 An enabled scanner errored; the detail names the scanner and the failure HM6001 The message could not be parsed, so only the whole file was scanned HM6002 An attachment could not be written to the temp directory for scanning HM6195 100,000 messages are already held for want of a scan; further holds are not counted HM7272 The sandbox called a message malicious and the quarantine could not store it; it is held again, never delivered Where to read more Wiki: https://gitlab.com/hmailserver/hmailserver/-/wikis/Stopping-Viruses https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/Sandbox.md https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/SealedMail.md Public documentation: https://www.progressiverobot.com/hmailserver-documentation/#11-stopping-viruses, https://www.progressiverobot.com/hmailserver-documentation/#111-clamav and https://www.progressiverobot.com/hmailserver-documentation/#112-blocked-attachments Upgrading to 6.3.4 and the 6.3.4 security announcement
  • 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
    145 Views
    ProgressiverobotP
    Written at 6.3.4, when no image was published. hMailServer 6.3.6 (6 October 2026) publishes the first: registry.gitlab.com/hmailserver/hmailserver:6.3.6, also tagged 6.3 and latest, one index over linux/amd64 and linux/arm64, signed with cosign as the release's pipeline; the Helm chart is a file of the release, and the packaging Compose file now runs PostgreSQL 18. See the 6.3.6 announcement and ContainerImage.md at the v6.3.6 tag. hMailServer 6.3.7 (17 October 2026), a security release, is registry.gitlab.com/hmailserver/hmailserver:6.3.7, for linux/amd64 and linux/arm64; in the container the update check names the image to pull and installs nothing, and the schema stays at 6108, so a container recreated on it from 6.3.6 changes no table. Its IMAP and POP3 listeners 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. See the 6.3.7 announcement. hMailServer 6.3.8 (2 November 2026), a security release, is registry.gitlab.com/hmailserver/hmailserver:6.3.8, and 6.3 and latest now name it; the schema stays at 6108. Its image carries MariaDB Connector/C, so HM_DB_TYPE=MySQL reaches a MariaDB server, where the images of 6.3.7 and earlier stopped at the first start, and its entrypoint secures a new installation at its first start by itself, whatever the container's command, so the run by hand below is left for a database its --create-database made more than a day before. The Helm charts of 6.3.6 and 6.3.7 never started a pod; 6.3.8's runs it as 999:999, fills new volumes in an init container and leaves database.port to the type's own port. Move a release of the older chart with helm repo update, helm upgrade <release> hmailserver/hmailserver --reset-then-reuse-values (Helm 3.14 or later) and then kubectl delete pod <release>-hmailserver-0, since the StatefulSet never replaces the stuck pod by itself (--reuse-values would keep the old chart's fsGroup 1000 and database.port 5432); where a cluster's policy refuses every sysctl, set unprivilegedPortsSysctl: false. For a Compose deployment made from an earlier Compose file, keep its HM_REST_CERTIFICATE_FILE, HM_REST_PRIVATE_KEY_FILE and ./certs mount in the new file, the key readable by 999, or its REST API does not start. See the 6.3.8 announcement. The Linux server is also a container image: the x86-64 .deb installed on Ubuntu 24.04 and run as the hmailserver user, with the database connection, the REST listener, the administrator password and, since 6.3.4, every other setting taken from the environment at start. This topic describes the image, the Compose file and the Helm chart as they are in the tree at the v6.3.4 tag, and the HM_* variables the entrypoint reads. The one thing to know first, from the 6.3.4 changelog: no container image of 6.3.4 is published. The images published on ghcr.io/progressiverobot/hmailserver before GitHub's suspension answer 404, and registry.gitlab.com/progressiverobot/hmailserver, which the Compose file and the chart now name, holds nothing. Meanwhile, build the image from the release's .deb and use your own tag wherever the files say registry.gitlab.com/.... What a Linux server is and is not is in hMailServer on Linux. Building the image yourself Use the docker directory of the same tag as the package: 6.3.4's entrypoint runs verbs a 6.3.3 binary does not have. git clone https://gitlab.com/hmailserver/hmailserver.git cd hmailserver git checkout v6.3.4 cd hmailserver/source/Server/platform/packaging/docker mkdir packages curl -L -o packages/hmailserver_6.3.4_amd64.deb \ https://gitlab.com/hmailserver/hmailserver/-/releases/v6.3.4/downloads/hmailserver_amd64.deb docker build -t hmailserver:6.3.4 . The Dockerfile copies packages/hmailserver_*_amd64.deb (the HM_DEB build argument names another path) and pins its Ubuntu base by digest. 6.3.4's package carries no Sigstore bundle to check it against, as 6.3.3's did. The image is x86-64 only: the Dockerfile takes the amd64 package. What is in the image, and what is not In: /usr/bin/hmailserver, the packaged hMailServer.ini at /etc/hmailserver/, the database scripts, the Control Deck, the fail2ban filter and jail the package carries, ca-certificates for the TLS the server does as a client, and an entrypoint. About 40 MB. The process runs as hmailserver; ports below 1024 are bound through cap_net_bind_service on the binary, not through root. Since 6.3.4 the Dockerfile installs the package's two Recommends, 7zip and python3: without them a backup failed with "Could not add the backup settings to the archive", and hmctl had no Python 3. Not in: the database, and it never will be. Point the image at a PostgreSQL or MariaDB server, in another container or anywhere else; on first start the entrypoint creates the schema there, and on every later start upgrades it if it is behind. Also not in: curl, an editor, or a shell of any consequence beyond bash. There is no Control Panel and no COM; the REST API is the administration. The variables the entrypoint reads itself Sixteen of these are older than 6.3.4 and keep their names and meanings. Variable Writes Notes HM_DB_TYPE [Database] Type PostgreSQL or MySQL (MariaDB is MySQL). Setting this is what makes the section be written at all HM_DB_HOST, HM_DB_PORT Server, Port Defaults db, and the type's port (5432 or 3306) HM_DB_NAME, HM_DB_USER, HM_DB_PASSWORD Database, Username, Password Defaults hmailserver, hmailserver, empty. The user needs the right to create the database on first start HM_DB_TLS_MODE, HM_DB_TLS_CA TlsMode, TlsCaFile Since 6.3.4. disable, prefer (empty, the default), require or verify-full, and the path of a PEM file of CA certificates mounted into the container HM_ADMIN_PASSWORD [Security] AdministratorPassword Through the server's own --set-admin-password: the file holds a PBKDF2 hash, never the password. Without one the REST API refuses to start HM_REST_PORT RestApiPort The packaged file has 0, which is off; 8045 is the conventional port HM_REST_BIND_ADDRESS RestApiBindAddress The packaged file has 127.0.0.1, which inside a container is the container's own loopback HM_REST_CERTIFICATE_FILE, HM_REST_PRIVATE_KEY_FILE RestApiCertificateFile, RestApiPrivateKeyFile Paths inside the container, readable by the hmailserver user. Paths the server reads, not _FILE forms HM_DB_PASSWORD_FILE, HM_ADMIN_PASSWORD_FILE the same two secrets Read from a file: a Docker or Compose secret. Setting both a variable and its _FILE is refused HM_CREATE_DATABASE, HM_UPGRADE_DATABASE nothing 0 skips --create-database or --upgrade-database at start. Both are no-ops when there is nothing to do HM_INI which file is written Default /etc/hmailserver/hMailServer.ini HM_PROFILE, HM_PROFILE_CONFIRM nothing Since 6.3.4: the setting profile applied once, and its confirmation On 6.3.3 and earlier the entrypoint writes these into hMailServer.ini on every start, and every other setting is the packaged file in the /etc/hmailserver volume. Since 6.3.4 the database is the settings store, so the four HM_REST_* variables are aliases of HM_SETTING_RestApiPort and its three neighbours, stored like any other setting; [Database] is still written into the file at every start, because it says where the database is. The image's /etc/hmailserver is owned by the hmailserver user, so there, unlike on a package installation, the server keeps its copy of each stored value in the file, and a --create-database that creates the schema also makes the stored-secrets key. Every setting from the environment (since 6.3.4) After --create-database and --upgrade-database, and before the server, the entrypoint runs the server's own verb, hmailserver --apply-environment, whenever there is anything for it to do. HM_SETTING_<key> sets a [Settings] key and HM_PROPERTY_<name> a database setting (HM_SETTING_MaxSubmissionsPerIPPerMinute=30, HM_PROPERTY_maxmessagesize=20480), and either with _FILE reads the value from a file, a Compose secret or a Kubernetes Secret mounted as one. The environment wins, at every start. An administrator cannot change a setting the environment names until the variable is unset and the container restarted, and a ConfigMap change, too, is applied by a restart and not live. Everything is checked before anything is stored. When anything is refused, nothing is stored and the container does not start; docker compose logs or kubectl logs says which variable and why. The naming rule, every refusal, what the REST API and the Control Deck show for a setting the environment manages, and how a value from a file stays secret are in configuration from the environment. HM_PROFILE names one of the four setting profiles, office, gateway, relay or sender, which the entrypoint applies once, with --apply-profile NAME --once, after the environment's settings and before the server starts. HM_PROFILE=none records that none is wanted. A profile that closes a listener, trusts an address range, turns a filter or a check off or weakens it, or shortens a window of days needs HM_PROFILE_CONFIRM=yes, and without it the container does not start. A profile skips a setting the environment manages, naming its variable. Not yet run in a container. The verb has been run as hMailServer.exe --apply-environment on Windows, and its POSIX half compiles; the entrypoint as a whole, the POSIX verb against a database, the chart and the Compose file have been read and not executed. HM_PROFILE is in the same state. Try it on a scratch database first. Reaching the API The server requires TLS on a REST listener that is not bound to loopback. A listener left at the packaged bind address answers only processes inside the container, which is none, since the image carries no client. Two ways out; either will do: Bind it and give it a certificate. HM_REST_BIND_ADDRESS=0.0.0.0, with HM_REST_CERTIFICATE_FILE and HM_REST_PRIVATE_KEY_FILE pointing at a certificate and key mounted into the container, and the port published. Publish it on the host's loopback (127.0.0.1:8045:8045) unless the certificate is one clients will trust. Host networking (--network host). The container's 127.0.0.1 is the host's, the packaged bind address is right as it is, no certificate is needed, and the API is at http://127.0.0.1:8045 from the host and through an SSH tunnel from elsewhere. The mail ports are then the host's ports directly. First start, with Compose The file to use is in the repository, at hmailserver/source/Server/platform/packaging/docker/docker-compose.yml. It reads the two passwords as Compose secrets from files, mounts the REST certificate and key in, and carries the HM_SETTING_ and HM_PROPERTY_ examples commented out. A workflow resolved it with docker compose config on every change until 18 September 2026, so it is known to parse. Below is the mail service of the wiki's shorter example, which puts it under services: beside a postgres:16 service named db and declares the named volumes. It names the image built above, not the empty registry: mail: image: hmailserver:6.3.4 depends_on: - db environment: HM_DB_TYPE: PostgreSQL HM_DB_HOST: db HM_DB_NAME: hmailserver HM_DB_USER: hmailserver HM_DB_PASSWORD: change-me HM_ADMIN_PASSWORD: change-me-too HM_REST_PORT: 8045 HM_REST_BIND_ADDRESS: 0.0.0.0 HM_REST_CERTIFICATE_FILE: /certs/rest.pem HM_REST_PRIVATE_KEY_FILE: /certs/rest.key volumes: - ./certs:/certs:ro - hm-data:/var/lib/hmailserver - hm-logs:/var/log/hmailserver - hm-etc:/etc/hmailserver ports: - "25:25" - "587:587" - "993:993" - "127.0.0.1:8045:8045" restart: unless-stopped restart: unless-stopped matters on the first start: depends_on starts the database container first but does not wait for it to accept connections, so the entrypoint's --create-database fails and the container exits if the database is not there yet; the restart tries again a moment later. The first start logs the [Database] section written, the administrator password hashed and the database created at schema 6104, and also what --create-database did about the stored-secrets key and one line per HM_SETTING_ or HM_PROPERTY_ variable stored. The first domain is then one request away (First install on Linux). Passwords in a Compose file are passwords in a file: for anything beyond a first look, use HM_DB_PASSWORD_FILE and HM_ADMIN_PASSWORD_FILE with Compose secrets. The secure defaults of a new installation are not applied in a container by themselves. On a package installation the systemd unit's first start gives a database --create-database made a self-signed certificate, STARTTLS and TLS required for authentication on the Internet IP range. The image has no unit, so that step is run by hand, as the service's user, with the server stopped. SecureDefaults.md gives the verb and no container command for it, and lists its refusal of a running service (exit 4) for Windows only: hmailserver --apply-new-install-defaults A run by hand has no one-day deadline, so an image first booted days after its --create-database is secured this way. What it changes, its exit codes and the checklist item that says what is left are in Hardening a public mail server. Kubernetes, with the chart Since 6.3.4 the chart is at hmailserver/source/Server/platform/packaging/helm/hmailserver. It is not published to a chart repository; install it from a checkout. It renders a StatefulSet of one, because a message store is not something two pods may share: the three volumes the image declares become claim templates, and the database is pointed at, never bundled. The chart refuses to render without database.host. Its image.repository names the empty registry, so set it to the image you built, pushed to a registry the cluster's nodes can pull from: helm install mail ./hmailserver \ --set image.repository=<your registry>/hmailserver \ --set database.host=postgres.databases.svc.cluster.local \ --set database.password=... \ --set administrator.password=... \ --set rest.tlsSecretName=mail-tls Value What it is image.repository registry.gitlab.com/progressiverobot/hmailserver as shipped, which holds no image: set it to where you pushed the image you built database.host, .type, .name, .user The PostgreSQL or MariaDB the server uses; required. database.create and database.upgrade make the schema on first start and move it forward on an upgrade database.password, administrator.password Go into a Secret the chart makes. Point existingSecret at a Secret you made and the chart writes none; its two keys are named by existingSecretKeys rest.enabled, .port, .tlsSecretName The one listener that serves the REST API, the Control Deck and the webmail. Off loopback the server requires TLS, so without a kubernetes.io/tls Secret named here the listener will not start; the chart's notes say so on install settings, properties, settingsFrom Since 6.3.4: any [Settings] key (rendered as HM_SETTING_<key>), any database setting (HM_PROPERTY_<name>), and a key of a Secret you made, projected into /run/secrets/hmailserver-settings/<n> and named through _FILE, so a secret value is in no environment variable, rendered manifest or log line service.type, .ports, .externalTrafficPolicy A LoadBalancer with the mail ports, and the REST port when the listener is on. Local keeps the client's address, which auto-ban, greylisting and the Received header all need persistence.* The message store, the logs and the configuration, one claim each Probes Port 25, as the image's own health check is, or the metrics listener's /livez and /readyz when that listener is on settings, properties and settingsFrom look like this in a values file; a settingsFrom entry says setting: in place of property: for a [Settings] key: settings: MaxSubmissionsPerIPPerMinute: "30" properties: maxmessagesize: "20480" settingsFrom: - secretName: mail-relay key: password property: smtprelayerpassword A switch is 1 or 0 for a [Settings] key; true, false, yes, no, on and off are taken as well for a setting whose REST settings row is a switch, and only there: maxmessagesize=off is refused rather than stored as 0, which is "no limit". Volumes, ports and the health check Path What lives there /var/lib/hmailserver The mail store, and .hmailserver-secret-key, the key under which this machine's stored secrets are protected. The key travels with this directory: a database restored beside a fresh data volume cannot open the secrets it holds /var/log/hmailserver The logs. Bind-mount it to a host path if fail2ban on the host is to read it /etc/hmailserver hMailServer.ini; since 6.3.4 the only home of PasswordPepper and [Security] StoredSecretsKey, which no database and no backup holds The image exposes 25, 110, 143, 465, 587, 993, 995, 4190 (ManageSieve) and 8045; publish the ones you serve. The health check opens a connection to the container's own port 25 every 30 seconds after a 60-second start period, and three failures in a row mark the container unhealthy. Upgrades, backups and auto-ban Upgrading is building the image of the new tag, as no 6.3.4 image is published to pull, and recreating the container: the entrypoint runs --upgrade-database on every start, so a schema that is behind is brought forward before the server opens a port. On MySQL and MariaDB the 6102 step rebuilds every table as utf8mb4, which takes time on a large database and needs the database server version that Databases on Linux gives. An upgraded database gets no stored-secrets key until one is made with hmailserver --create-stored-secrets-key, and never the new installation's defaults. Take the backup first (Upgrading to 6.3.4). Backups are three things together: the database, the /var/lib/hmailserver volume, and /etc/hmailserver. Two things are only in /etc/hmailserver/hMailServer.ini: a PasswordPepper, if one is set, without which a restored database refuses the password of every account hashed with Argon2id or scrypt, and the stored-secrets key, without which a restored database opens no account's second factor and no [Settings] secret. Export the key once, and keep the file with the database backups, apart from them: docker exec <container> hmailserver --export-stored-secrets-key /var/lib/hmailserver/stored-secrets.key docker cp <container>:/var/lib/hmailserver/stored-secrets.key ./stored-secrets.key A replacement container takes it back with --import-stored-secrets-key FILE. Auto-ban works as everywhere: an address that fails to log on repeatedly is refused by the listeners. The hook that adds the address to the host's firewall cannot run from inside a container, so run fail2ban on the host instead: bind-mount /var/log/hmailserver, copy the filter and jail out of the image, point the jail's logpath at the mounted directory, and enable it. What was proven, and what has not been Until 18 September 2026 the image job of the GitHub workflow built the image on every release tag, pushed it, and smoke-tested the pushed image as the example above runs it: the container healthy, the SMTP banner and the API answering, and the administrator password in no file in clear. Nothing has run it since. GitLab CI's linux-image job is written to build the image from the .deb with kaniko and inspect it; it neither pushes nor runs the image, so the smoke test has no replacement yet. The chart-and-compose job is written to lint and render the chart and resolve the Compose file. Neither had run when ContinuousIntegration.md was written, on 25 September 2026. The changelog's 6.3.4 section records the first full pipeline the day after: it finished failed, the image job among those that passed, and no pipeline has run on the fixed tree since. 6.3.4 was made by hand. Where to read more Running in a Container and Profiles and Environment on the wiki. EnvironmentConfiguration.md for the naming rule and every refusal, Profiles.md for every entry of the four profiles, SecureDefaults.md for the verb run by hand, and DatabaseTls.md for HM_DB_TLS_MODE. The 6.3.4 section of CHANGELOG.md for the state of the image. On this forum: the 6.3.4 announcement, hMailServer on Linux, First install on Linux and Upgrading to 6.3.4; configuration from the environment for HM_SETTING_ and HM_PROPERTY_ in full, and the hmailserver command line on Linux for the verbs the entrypoint runs.