Skip to content
  • Categories
  • Recent
  • Popular
  • Tags
  • Users
  • Groups
  • Documentation
  • Downloads
  • Report a bug
  • GitHub mirror
  • Paid support
Skins
  • Light
  • Brite
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dark
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

  • Default (No Skin)
  • No Skin
Collapse
hMailServer Community Forum
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

hMailServer Team

Private

Progressive Robot engineers who maintain hMailServer. Posts from this group appear in the Dev Tracker.

Posts


  • Documentation index: every guide on this forum, by subject
    ProgressiverobotP Progressiverobot
    Guides & tutorials administration installation api

    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.


  • Alerts, the audit trail, syslog and the journal, and per-domain reports
    ProgressiverobotP Progressiverobot
    Monitoring, metrics & logs logs administration security rest-api systemd

    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

  • Monitoring and health: Prometheus metrics, health probes, Grafana, OTLP and SNMPv3
    ProgressiverobotP Progressiverobot
    Monitoring, metrics & logs diagnostics administration rest-api linux windows

    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

  • Journaling and dual delivery: a compliance copy, and what a journal rule does not see
    ProgressiverobotP Progressiverobot
    Rules, Sieve & routing smtp security administration rest-api limitations

    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:

    1. The SMTP session. The outbound half judges an authenticated sender; this is what a desktop or phone client uses.
    2. 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.
    3. 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:

    1. 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.
    2. Create one rule with scope domain, target the domain name, direction both, mode report and the journal_address.
    3. 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.
    4. 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

  • Aliases, distribution lists, membership queries, mailing lists and public folders
    ProgressiverobotP Progressiverobot
    Rules, Sieve & routing smtp imap administration dkim dmarc

    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:

    1. Domain aliases rewrite the domain.
    2. Plus addressing trims the local part.
    3. Account? If found, deliver to the mailbox.
    4. Alias? If found, follow it and start over, up to 25 hops.
    5. Distribution list? If found, expand to the members, or to the moderator.
    6. Route? If found, send to the route's target server.
    7. 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.

    1. 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.
    2. 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.
    3. 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

  • Routes, relays and smart hosts, OAuth2 relaying, restricted delivery and recall
    ProgressiverobotP Progressiverobot
    Rules, Sieve & routing smtp tls security administration api

    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:

    1. 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.
    2. Is there a route whose domain matches the recipient domain? Wildcards are allowed. The route's host, port, connection security and credentials are used.
    3. 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.
    4. Is a server-wide SMTP relayer configured? The relayer's host, port, security and credentials. Port 0 means 25.
    5. 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:

    1. Open Mail flow & delivery → Delivery of e-mail → Relayer.
    2. SMTP relayer: smtp.isp.example. Port: 587.
    3. Connection security: STARTTLS (required) - required, not optional, so a downgrade cannot send your credentials in clear.
    4. Tick Relayer requires authentication and fill in the user name and password.
    5. 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:

    1. 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.
    2. 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.
    3. 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

  • Rules and Sieve: what runs when, vacation replies and the Run function action
    ProgressiverobotP Progressiverobot
    Rules, Sieve & routing sieve smtp administration security linux

    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:

    1. 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.
    2. 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).
    3. The message is split into local and external recipients. External recipients go to relays, routes and MX delivery.
    4. 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

  • SCIM provisioning from Entra ID and Okta, and single sign-on for the organisation
    ProgressiverobotP Progressiverobot
    REST API & integrations rest-api security administration portal control-deck

    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

    1. Issue the key as above and note the token.
    2. 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).
    3. 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.
    4. 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.
    5. 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.
    6. Assign the users and groups, and switch Provisioning Status on.

    Setting up Okta for SCIM

    1. Issue the key as above.
    2. Applications > Create App Integration > SAML 2.0 or SWA, then on General enable SCIM provisioning.
    3. 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.
    4. Provisioning > To App: enable Create Users, Update User Attributes and Deactivate Users.
    5. 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.

  • Integrating applications: the send API, mail in to a webhook, and outbound webhooks
    ProgressiverobotP Progressiverobot
    REST API & integrations rest-api api smtp security

    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) refuses a state-changing REST request that carries a credential when the browser making it reports it as cross-site (Sec-Fetch-Site, or an Origin that is not this server's), so a page on another site cannot post to these routes with a credential, whether a key or the administrator password the browser holds; a program or script that sends no such header, as the curl examples here do not, is unaffected. See the 6.3.7 announcement. hMailServer 6.3.8 (2 November 2026) answers 400, saying why, before a route reads a member, to a body that is not one JSON object, names a member twice, holds an escaped U+0000 or passes 100,000 values or 32 levels, so correct a program that sends such a body to these routes; and it raises account.locked only for a locked name that is an account's own. See the 6.3.8 announcement.

    Since 6.3.4 hMailServer has three ways for another program to work with the server's mail: a send API, through which an application sends as one sender and asks afterwards what became of each recipient; mail in to a webhook, where the mail for an address or a whole domain is posted to a URL; and outbound webhooks, which tell a program what the server's mail did. Nothing is on until a key, a route or a subscription is made. The one thing to know first: each of the three takes a different credential, and the difference is deliberate.

    Feature Routes Who may use it
    The send API /api/v1/send An API key that names a sender and whose role holds send_through_api. Not the administrator password, not an account.
    Mail in to a webhook /api/v1/inbound-webhooks view_configuration to read, manage_inbound_webhooks to change. Meant for an API key's role, confined to the application's domain.
    Outbound webhooks /api/v1/webhooks The administrator password, or an account whose role holds the permission. No API key reaches these routes; the refusal is 401.

    All three are made and managed over the REST listener, so RestApiPort has to be on; outbound webhook subscriptions can also be managed over COM, on Windows.

    The send API: the key

    POST /api/v1/me/messages is the webmail's send and stays the webmail's. The send API is for programs.

    Route What it does
    POST /api/v1/send Sends a message as the key's sender. 202 {queue_id, message_id, recipients, recorded}
    GET /api/v1/send The key's own sends, newest first, paged
    GET /api/v1/send/{queue_id} One of them, with the state of each address its body named

    A key sends when two things are true of it: its role holds send_through_api, and it names a sender. Either alone sends nothing. Three fields are set when the key is made, on POST /api/v1/apikeys with the administrator password or on the Control Deck's API keys page.

    Field What it is
    send_from The one sender the key may use: an address of a domain this server hosts, or *@domain for any address of that domain. Empty, the default, means the key cannot send.
    send_per_minute The key's own ceiling on messages a minute. 0, the default, sets none.
    send_per_day The key's own ceiling on messages in any 24 hours. 0 sets none, which only a key whose sender is an account's address may have.

    The built-in serveradmin role holds the permission, so a key made with "scope": "full" holds it. No other built-in role does. A custom role is how a key that sends and does nothing else is made:

    POST /api/v1/roles
    {"name": "mailer", "description": "sends mail", "permissions": "send_through_api"}
    
    POST /api/v1/apikeys
    {"label": "shop confirmations", "role": "mailer", "send_from": "[email protected]",
     "send_per_day": 5000}
    

    The route refuses with 400, among other things, a send_from at a domain this server does not host (a domain alias does not count), at a domain that is switched off, on a read-only key, or outside the domains a key is confined to. A send_from that is *@domain, or an address that is no account's, needs a send_per_day above 0, because such a sender has no account's sending quota to hold it. A key's sender cannot be changed afterwards: a new sender is a new key.

    What *@domain grants. To the server, such a key is every mailbox of that domain sending as itself. It can post to a members-only list as any member and send as the domain administrator's mailbox. The display name and reply_to are the application's own and are not checked against anything; what the key vouches for is the address.

    On Windows, the desktop Control Panel's key page does not show these three fields and cannot set them, so a key made there cannot send. The Control Deck and the route do.

    The send API: sending and asking afterwards

    curl https://mail.example.com:8443/api/v1/send \
      -H "Authorization: Bearer hmapi_0123…" \
      -H "Content-Type: application/json" \
      -d '{"to":["[email protected]"],"subject":"Your order","text":"Thank you."}'
    
    Member Rule
    from Optional when the key's sender is an address, required when it is *@domain. "address" or "Name <address>".
    to, cc, bcc Arrays of mailboxes; at least one recipient between them. Bcc goes to the envelope and nowhere else.
    reply_to Optional.
    subject Required; it may be empty.
    text, html At least one. Both makes a multipart/alternative.
    headers Optional: X- fields other than the server's own X-hMailServer- fields, List-Unsubscribe, List-Unsubscribe-Post, In-Reply-To and References. Any other field is 400.
    attachments Optional: [{filename, content_type, data}], data in base64, at most 20 and twelve megabytes together.

    A line break is refused with 400 in every value that becomes a header, and a member the route does not take (body for text, for example) is 400 and named, not dropped.

    What the server does to the message. It is written and checked by the same code as the webmail's send. Every recipient goes through what RCPT TO asks of an authenticated sender, with the server's recipient ceiling (MaxSMTPRecipientsInBatch) and the suppression list. The domain's signature and disclaimer are added, the rules that block are asked (403, policy_rejected), and the message is held to the maximum message size (413). It is DKIM-signed at delivery by the domain's key. The envelope sender is the From address, so a bounce comes back to it. A Received field names the server, the send API and the key's label, never the key's id. No Sent copy is kept anywhere. When the sender is an account's address, that account's sending quota is consumed as an SMTP submission would consume it.

    Ceiling Answer
    send_per_minute 429, Retry-After: 60
    send_per_day 429, Retry-After: 3600
    The sender account's sending quota 429, Retry-After one twenty-fourth of the account's period
    The REST request budget, 200 requests in 10 seconds 429, Retry-After: 10
    The recipient ceiling, the size 400, 413

    A message is counted only once every other check has passed, and when a later ceiling refuses, every earlier one is given back. A refused message is never a counted one.

    The answer to a message that is taken is 202:

    {"queue_id": 4711, "message_id": "<…@mail.example.com>", "recipients": 2, "recorded": true}
    

    queue_id is the one to ask about afterwards with GET /api/v1/send/4711. The answer names only the addresses the body named, each with a state.

    State Meaning
    queued In the queue and not yet tried
    deferred An attempt failed and will be retried
    delivered The trace's delivered event
    bounced Refused for good, or the retries ran out
    refused The suppression list stopped it at delivery
    unknown Nothing names the address; the answer says why

    It needs the message trace, and the trace is off by default (MessageTraceEnabled), because it records who corresponds with whom. With it off, a send reads queued or deferred while it is in the queue and unknown once the queue has let it go; trace in the answer is false and a trace_note says so. Turn the trace on, and choose its retention, before relying on the answer. The record of a send is kept for MessageTraceRetentionDays, thirty by default. Only a send this key made is answered; any other id is the same 404. The backup does not carry the records.

    Mail in to a webhook: a route

    SendGrid and Mailgun call this inbound parse. A route is made over REST or on the Control Deck's Inbound webhooks view. There is no COM member and no Control Panel page, on either platform.

    POST /api/v1/inbound-webhooks
    {
      "address": "[email protected]",
      "url": "https://helpdesk.example/hooks/mail",
      "secret": "a-long-random-string"
    }
    
    Field What it is Default
    address [email protected], or example.com for every address of the domain. The domain must be one this server hosts. required
    url https:// to the endpoint; plain http:// only to this machine. Printable ASCII only. required
    secret Write-only. Every request is signed under it and it is never returned. required
    deliver false: the route alone receives the mail. true: what the address reaches without the route receives it as well. false
    on_failure keep or bounce keep
    format form (multipart/form-data in SendGrid's field names) or json form
    attachments include, names or none include
    include_raw Also send the message as it arrived false
    max_size_kb A larger message is not posted. At most 81920. 10240
    enabled A route switched off is as though it were not there for new mail true

    A route decides that the address exists. RCPT TO is accepted for a routed address whether or not an account, an alias or a list has it, and a route is asked before any of them. With deliver off, the route takes the mail instead of whatever has the address, so a list whose address a route takes never sees the message. Route a list's domain with care. An address route wins over its domain's, and a recipient with a plus tag ([email protected]) is found by the address as written, then by the address before the tag, then by the domain, with the tag kept in what is posted.

    What arrives is a signed POST:

    POST /hooks/mail HTTP/1.0
    Content-Type: multipart/form-data; boundary=hMailServer-4193-...
    X-hMailServer-Timestamp: 1789000042
    X-hMailServer-Signature: sha256=<hex>
    X-hMailServer-Event: message.inbound
    X-hMailServer-Delivery: 4193
    X-hMailServer-Attempt: 1
    

    X-hMailServer-Delivery is the same on every attempt. The form format carries SendGrid's fields (headers, from, to, cc, subject, text, html, envelope, sender_ip, SPF, dkim, charsets, attachments, attachment-info, attachment1 onward) with a few of this server's own beside them (spam, dmarc, route, queue_id, attachments_left_out). Mailgun's field names are not sent. The json format is this server's own shape. The SPF, dkim and dmarc results are the ones this server wrote into its own Authentication-Results field, and only while AuthenticationResultsEnabled is on.

    Retries and what is kept. The message is taken after the global rules, the virus scan and the sandbox have had it, and nothing is posted from the delivery thread. Any 2xx delivers. Anything else, a redirect included, is a failure. The retries are the outbound webhooks': WebhookMaxAttempts attempts (5 by default), with waits of 0, 1, 5, 15 and then 60 minutes. When they run out:

    • keep: the message is kept and never swept. It can be read with GET /api/v1/inbound-webhooks/deliveries/{id}/message, tried again with POST /api/v1/inbound-webhooks/deliveries/{id}/retry or removed with DELETE /api/v1/inbound-webhooks/deliveries/{id}. The error log says so once, as HM7401.
    • bounce: the message is returned to its sender with a delivery-status report (4.4.7), without repeating what the endpoint answered. HM7402 records the return. Where the sender cannot be told, the message is kept instead.

    A message larger than the route's ceiling is not posted at all: it is returned at once (5.3.4) or kept at once (HM7403). Delivered and returned rows are removed after WebhookRetentionDays (7 by default), and the rows survive a restart.

    Where an endpoint may be depends on who made the route. The server's administrator may name an address only this server can reach, but never a link-local or cloud metadata address (169.254.0.0/16, fe80::/10 and the clouds' own metadata addresses). Every other credential, an API key of any scope or role included, may name a public address only. Both are judged when the route is saved and again at every connection. No redirect is followed and HttpProxy is never used.

    Who may. Only serveradmin holds manage_inbound_webhooks as shipped, since routing a domain's mail to a URL is as much as reading all of it. A reader without it is answered a route's URL as its scheme and host alone, so put a token in the secret, not in the URL. Reading a kept message needs view_mailbox as well. POST /api/v1/inbound-webhooks/run makes one pass now and needs a credential that reaches every domain.

    Outbound webhooks

    An outbound webhook does not carry the message. No body, no attachments, no header block. It is a notification. The one piece of content it can carry is the subject, and only where the domain's webhook_subjects and the subscription's include_subject are both on; both are off by default, and even then a subject travels on message.received and message.quarantined only.

    Event Raised when
    message.received The message has been saved and the server has taken responsibility for it, once per local recipient
    message.delivered A recipient, local or remote, has been delivered to
    message.bounced A non-delivery report has been written and queued for the sender, once per failed recipient
    message.quarantined The held copy and its row exist
    account.locked Repeated authentication failures have locked a name

    A delivery attempt that failed and will be retried is not a bounce, and no event says deferred.

    POST /api/v1/webhooks
    {
      "name": "ticketing",
      "url": "https://tickets.example/hooks/mail",
      "secret": "a-long-random-string",
      "events": "message.bounced,account.locked",
      "domains": "example.com",
      "max_per_minute": 120
    }
    

    An empty events, domains or addresses list means every one. A subscription confined to a send API key's sender ("addresses": "[email protected]") hears message.delivered and message.bounced for the recipients outside this server, with the message trace on or off. Plain http off this machine is refused when the subscription is made. The secret is write-only, protected at rest, and an empty secret in a change keeps the one that is set.

    Permissions. Reading needs view_configuration; deleting needs manage_settings; creating, changing, testing and running a pass need manage_server, because whoever can do those can make the server connect to a host of their choosing. A role confined to named domains is refused all eight routes.

    The queue. The delivery pass runs once a minute, attempts at most 200 rows and runs for at most 60 seconds. A delivery succeeds on any 2xx; a request that has taken more than ten seconds is a failure. After WebhookMaxAttempts the row is dead-lettered and the error log says so once as HM6971. Past a subscription's ceiling per minute, events are dropped and counted as dropped, not queued.

    Setting REST key Default
    WebhooksEnabled webhooks_enabled on
    WebhookMaxAttempts webhook_max_attempts 5
    WebhookMaxPerMinute webhook_max_per_minute 60
    WebhookRetentionDays webhook_retention_days 7

    WebhooksEnabled silences the outbound webhooks and not the inbound routes. From a shell:

    hmctl webhook create --set name=ticketing --set url=https://tickets.example/hook \
                         --set-string secret="$SECRET" --set events=message.bounced --set domains=example.com
    hmctl webhooks test --event message.bounced --domain example.com
    hmctl webhooks run
    hmctl webhooks deliveries --state dead-lettered
    

    webhooks test raises a real event through the path a real one takes and says how many subscriptions took it, so 0 is an answer. In PowerShell the functions are Get-HmWebhook, New-HmWebhook, Set-HmWebhook, Remove-HmWebhook, Get-HmWebhookDelivery, Invoke-HmWebhookDelivery and Test-HmWebhook. Over COM, on Windows, Utilities.GetWebhookSubscriptions(name) and Utilities.SaveWebhookSubscription(json) take and answer the same JSON documents.

    Verifying a signature

    One recipe signs all of them: the outbound webhooks, the inbound routes and the webhook of an alert rule. The signature is HMAC-SHA256 over the timestamp, a full stop, and the raw request body, under the shared secret, as lower-case hex, in X-hMailServer-Signature. Verify it against the bytes received, before parsing them.

    import hmac, hashlib, time
    
    def verify(secret, headers, raw_body):
        timestamp = headers["X-hMailServer-Timestamp"]
        signature = headers["X-hMailServer-Signature"]
    
        # Refuse anything older than five minutes, or the signature can be replayed.
        if abs(time.time() - int(timestamp)) > 300:
            return False
    
        expected = hmac.new(secret.encode(), (timestamp + ".").encode() + raw_body,
                            hashlib.sha256).hexdigest()
    
        return hmac.compare_digest("sha256=" + expected, signature)
    
    1. Compare in constant time.
    2. Check the timestamp: a signature with no freshness check can be replayed.
    3. De-duplicate on X-hMailServer-Delivery together with X-hMailServer-Event. A retry is the same body with a fresh timestamp and so a fresh signature, and an inbound and an outbound delivery can carry the same number.
    4. To rotate a secret, PUT the new one and accept either signature at the receiver while the change is made. There is no overlap window in the server.

    The documents also give the recipe in JavaScript. An alert's webhook carries three of the headers: X-hMailServer-Timestamp, X-hMailServer-Signature and X-hMailServer-Event.

    What they do not do

    • The send API has no templates, no substitutions, no scheduled send and no per-recipient ceiling of the key's own, and gives no state without the trace once a message has left the queue.
    • An inbound route makes one post per route per message, not one per recipient. It has no setting of its own and no per-route retry ladder.
    • An outbound webhook does not carry the message and has no per-subscription retry policy.
    • Neither kind of webhook is in the configuration backup. Export the routes with GET /api/v1/inbound-webhooks and the subscriptions with hmctl webhook list; the secrets are in no read.
    • One pass at a time is per process, not per database, for both kinds of webhook.

    Where to read more

    • The operator documents: SendApi.md, InboundWebhooks.md, Webhooks.md and, for the alerts' webhook, AlertsAndAuditTrail.md.
    • The wiki: Mail In to a Webhook and The REST API.
    • The public documentation: chapter 24, The REST API.
    • On this forum: The Control Deck, the 6.3.4 announcement, the REST API guide and hmctl and the PowerShell module; turning the message trace on is in Troubleshooting by symptom, and the alert rules whose webhook is signed the same way are in Alerts, the audit trail, syslog and the journal.

  • hmctl and the PowerShell module: shell administration and accounts from a CSV file
    ProgressiverobotP Progressiverobot
    REST API & integrations rest-api administration linux windows api

    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.

Member List

ProgressiverobotP Progressiverobot Group Owner
109 Posts 0 Reputation
  • Login

  • Don't have an account? Register

  • Login or register to search.
  • First post
    Last post
0
  • Categories
  • Recent
  • Popular
  • Tags
  • Users
  • Groups
  • Documentation
  • Downloads
  • Report a bug
  • GitHub mirror
  • Paid support