hmctl and the PowerShell module: shell administration and accounts from a CSV file
-
Written for 6.3.4. hMailServer 6.3.6 (6 October 2026) carries the PowerShell module,
HmailServer.psd1andHmailServer.psm1, Authenticode-signed, as files of the release, and Linux packages for x86-64 and AArch64;hmctlas 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, whichhmctland 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 2026hmctlis on PyPI ashmailserver-cliand the module on the PowerShell Gallery asHmailServer, 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 tohmctl apior--from-jsonmust be one JSON object that names each member once, and 6.3.8'shmctlrefuses a body holdingNaNorInfinitybefore sending it. See the 6.3.8 announcement.hMailServer 6.3.4 has two command-line clients:
hmctlfor any shell, and theHmailServermodule 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/hmctlOne Python 3 file with no dependencies beyond the standard library. hmailserver/source/Tools/Cli/HmailServer.psm1The PowerShell module: Get-HmDomain,New-HmAccount,Set-HmSettingand the rest, answering objects rather than text.build/check-cli.pyRuns both against a recorded server in CI and asserts what each one sent, so the two cannot drift apart. On Linux, the packages install
hmctlas/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 directoryhmailserver/source/Tools/Cli/. Both clients are in the repository since 6.3.4.build/hmconfig.ps1is 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 ofhMailServer.inidoes not turn the listener on once the server has stored the key.sudo hmailserver --set-setting RestApiPort=8080 sudo systemctl restart hmailserverOn 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-settingmakes 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 listConnect-HmServer -Url https://mail.example.com:8045 -ApiKey (Get-Content .\api.key) Get-HmDomainhmctlreadsHMAIL_URL,HMAIL_API_KEY,HMAIL_PASSWORDandHMAIL_PASSWORD_FILE, and takes--url,--api-key,--passwordand--password-filebefore 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.--insecureis refused for anything but the loopback. A self-signed certificate onlocalhostis 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 webhookandhmctl 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,mailboxAn account is listed and created under --domain, and read and written at its address, which is how the API is shaped.mailboxrenames or moves one.Server-wide objects rule,route,port,iprange,certificate,journalrulelist, create, update, delete Mail in motion queue,quarantinequeuehas list, retry, delete;quarantinehas list, release, deleteSettings settings,ini,configA settings group whole or one setting; the keys that were the [Settings]section; the configuration as one documentDeliverability dkim,dmarc,suppression,bounce,bounces,complaint,complaints,spamtrap,traphits,reputation,openpgpreputationexits 1 when an address is listed and when a list could not be askedIntegrations apikey,webhook,webhooksAll three with the administrator password only, never an API key Operations log,backup,report,sample-domain,matchmatchsays what a rule criterion would decide, without a ruleBulk accountsimport and export, a CSV file, with a dry run Anything else apiA 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/reinitializeHow values are read.
--set name=valuereadstrue,false,null, numbers and anything starting with{or[as JSON, and everything else as a string.--set-stringkeeps a value that looks like one of those a string.--from-json FILEtakes 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, defaultEvery function answers objects, so
Where-Object,Sort-ObjectandExport-Csvwork on them, and nothing in the module formats anything. Everything that changes the server supports-WhatIfand-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 onceConnect-HmServerhas 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.csvImport-HmAccount -Path .\new-people.csv -WhatIf Import-HmAccount -Path .\new-people.csv Export-HmAccount -Domain example.com -Path .\accounts.csvThe 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 addressyes Created under the address's own domain. passwordyes 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_nameAt most 60 characters, counted as UTF-16 code units. last_nameAs first_name.activetrue,yes,onor1;false,no,offor0. Default true.max_size_mbA whole number of megabytes; 0is no limit, and is the default.admin_leveluser,domainorserver. Defaultuser.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,,domainThe 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, anamecolumn included. The clients also refuse, in the words the server would use:- a row with no password;
- an
activethat is not one of the eight words; - a
max_size_mbthat is not a whole number; - an
admin_levelthat 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;
hmctladds 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-runmakes 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_levelis 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 forserveris refused with a403.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, appliedThe document holds the settings groups, the
hMailServer.inikeys 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.applyis a plan unless--forceis added, and a deletion needs--allow-deleteon 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 passwordfield 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 journalruleedits 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 configdoes not read.What this server has changed from stock.
hmctl config changed, orGet-HmChangedSettingin 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, unlikeconfig 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 ---csvfetches the server's own CSV withformat=csvinstead of building one from the JSON. Per-domain reports has the sections, their sources and parameters, and who may read them.Run
hmctl report sectionsfirst. 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,IESet-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_statusis 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 restoreandhmctl backup passphraseask the server; the archive is written, read and restored by the server. - Neither sets the administrator password.
hmailserver --set-admin-passwordon the server does that, on WindowshMailServer.exe --set-admin-password, with the password inHM_ADMIN_PASSWORDor on standard input and never as an argument. On Windows the Control Panel does it too, but only where the service may writehMailServer.ini, as under LocalSystem: underNT 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.
hmctlhas no pending verb. SettingsApply.md notes thatGET /api/v1/server/settings/pendingis one request;hmctl apitakes 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 webhookmanages, the send API and webhooks.
Hello! It looks like you're interested in this conversation, but you don't have an account yet.
Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.
With your input, this post could be even better 💗
Register Login