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.