Overview
stalwart-cli is the command-line tool for administering a Stalwart server. It covers the same ground as the WebUI, but from a terminal, which makes it the natural choice for scripts, for automation and for declarative deployments, in which the desired configuration is kept in files and applied to the server on every change.
Everything the CLI does goes through the server’s management API. Stalwart describes its configuration as objects, such as domains, accounts and listeners, and exposes them over JMAP, the HTTP and JSON protocol that it also offers to mail clients. The configuration objects belong to the urn:stalwart:jmap capability, and their JMAP type names carry the x: prefix. The CLI reads and changes these objects through the same API that the WebUI uses, so a change made with one tool is visible in the other.
The tool is schema-driven. On first use, it downloads from the server a description of every object, its fields and its forms, and caches it locally. The CLI derives its validation and its output from this schema. The same binary therefore works with any Stalwart server version without being rebuilt.
Capabilities at a glance
Section titled “Capabilities at a glance”Each task has its own subcommand, and each subcommand has its own page:
| Task | Command | Title |
|---|---|---|
| Inspect what objects and enums the server exposes | describe | Exploring the schema |
| Read a single object by id (or a singleton) | get | Fetching a single object |
| List or filter objects of a given type | query | Searching and listing |
| Create a single object | create | Creating objects |
| Modify an existing object (or singleton) | update | Updating objects |
| Destroy one or more objects | delete | Removing objects |
| Apply a plan of operations from an NDJSON file | apply | Declarative bulk operations, Plan file format, Idempotent plans |
| Export the server’s configuration as a plan | snapshot | Exporting server state |
The first six commands work on one object, or one list of objects, at a time. apply and snapshot work on whole plans, which are files that describe many changes at once. Infrastructure as code shows how to drive them from Ansible, Terraform, NixOS, Pulumi and CI pipelines.
Installation
Section titled “Installation”Binaries are published for macOS (Intel and Apple Silicon), Linux (x86_64, aarch64, armv7 and arm, glibc and musl) and Windows (x86_64). A multi-architecture Docker image is published alongside them.
The installers below cover the common platforms. On macOS and Linux, the shell installer detects the host and downloads the matching binary:
macOS / Linux (shell installer)
Section titled “macOS / Linux (shell installer)”curl --proto '=https' --tlsv1.2 -LsSf \ https://github.com/stalwartlabs/cli/releases/latest/download/stalwart-cli-installer.sh | shWindows (PowerShell installer)
Section titled “Windows (PowerShell installer)”powershell -ExecutionPolicy Bypass -c "irm https://github.com/stalwartlabs/cli/releases/latest/download/stalwart-cli-installer.ps1 | iex"Homebrew (macOS / Linux)
Section titled “Homebrew (macOS / Linux)”brew install stalwartlabs/tap/stalwart-clinpm (Node.js, all platforms)
Section titled “npm (Node.js, all platforms)”npm install -g stalwart-cliDocker (all platforms)
Section titled “Docker (all platforms)”Every release publishes a multi-architecture image, for linux/amd64 and linux/arm64, to both GitHub Container Registry and Docker Hub:
docker run --rm ghcr.io/stalwartlabs/cli --versiondocker run --rm stalwartlabs/cli --versionThe image tags follow the releases. A full version such as :1.2.3 pins one release, the minor series such as :1.2 follows its patch releases, and :latest follows the newest release. The images are signed with cosign and carry build-provenance attestations, so their origin can be verified before they are run.
Inside a container, the connection settings come from the environment. Commands that read or write files, such as apply --file and snapshot --output, also need a working directory bind-mounted into the container, because the container cannot otherwise see the host’s files:
docker run --rm \ -e STALWART_URL=https://mail.example.com \ -e STALWART_TOKEN \ ghcr.io/stalwartlabs/cli query Account --fields id,name
docker run --rm \ -v "$PWD:/work" -w /work \ -e STALWART_URL -e STALWART_TOKEN \ ghcr.io/stalwartlabs/cli apply --file plan.ndjsonThe container runs as a non-root user whose home is /home/nonroot. Each new container starts with an empty home, so it would download the schema again on every run. Mounting a volume at /home/nonroot/.cache keeps the schema cache across runs and avoids that download:
docker run --rm \ -v stalwart-cli-cache:/home/nonroot/.cache \ -e STALWART_URL -e STALWART_TOKEN \ ghcr.io/stalwartlabs/cli describeWindows MSI
Section titled “Windows MSI”Each release also publishes a signed .msi installer at https://github.com/stalwartlabs/cli/releases/latest. Running it performs a standard Windows install.
Building from source
Section titled “Building from source”Building from source requires a Rust toolchain recent enough to support edition 2024:
git clone https://github.com/stalwartlabs/cli.gitcd clicargo install --path .Whatever the installation method, the following command confirms that the binary runs:
stalwart-cli --versionConnecting to a server
Section titled “Connecting to a server”Every invocation needs two things: the URL of the server and a credential to authenticate with. Both are given through global options, which may appear before or after the subcommand, or through the matching environment variables:
| Flag | Environment variable | Purpose |
|---|---|---|
--url <URL> | STALWART_URL | The Stalwart server endpoint (required). |
--user <USER> | STALWART_USER | Basic-auth username. |
--password <PASS> | STALWART_PASSWORD | Basic-auth password. Prompted interactively if absent and stdin is a TTY. |
--api-key <TOKEN> | STALWART_TOKEN | Bearer token: an API key or an OAuth access token. Mutually exclusive with --user. |
--insecure / -k | (none) | Skip TLS certificate verification (matches curl -k). |
--no-color | (none) | Disable ANSI colour output. The NO_COLOR environment variable also disables colours. |
--debug | STALWART_DEBUG | Print diagnostics of the HTTP requests and responses to standard error. |
The choice of credential depends on who runs the CLI. A user name and password suit an administrator at a terminal, while unattended jobs are better served by a bearer token. Against a local development server, the options are usually passed on the command line:
stalwart-cli --url http://localhost:8080 --user admin --password admin describeScripts and CI pipelines are better served by the environment variables, because they keep the credentials out of the command line:
export STALWART_URL=https://mail.example.comexport STALWART_USER=adminexport STALWART_PASSWORD='changeme'stalwart-cli describeFor unattended use, an API key avoids storing an account password at all:
export STALWART_URL=https://mail.example.comexport STALWART_TOKEN='API_<key>'stalwart-cli describeThe credential also determines what the CLI may do, because the server checks every call against the permissions of the account behind it. A key used with the CLI needs the authenticate permission and the sys* permissions of the objects it reads and changes. Editing domains, for example, takes sysDomainGet and sysDomainUpdate. Reading the schema requires no further permission.
The CLI talks to two endpoints. It sends its management calls to the JMAP endpoint named in the server’s session, and it fetches the schema from /api/schema.
Object naming
Section titled “Object naming”Object names are flexible on the command line. The CLI accepts them with or without the x: prefix, and it ignores case, so the following commands are all equivalent:
stalwart-cli get Domain abc123stalwart-cli get domain abc123stalwart-cli get x:Domain abc123stalwart-cli get X:DOMAIN abc123Before it sends a request, the CLI converts the name to its canonical form. The human-readable output leaves out the x: prefix, so x:Domain is shown as Domain.
Singletons
Section titled “Singletons”Most object types can have any number of instances, but some configuration objects are singletons: they have exactly one instance per server, such as the server-wide settings or the JMAP limits. A singleton has no id of its own. It is addressed by the literal id singleton, and the CLI uses that id when none is given, so the JMAP limits singleton can be fetched with either of these commands:
stalwart-cli get jmapstalwart-cli get jmap singletonBecause there is always exactly one instance, a singleton cannot be created or destroyed, only updated. Updating objects and Fetching a single object cover the details.
Multi-variant objects
Section titled “Multi-variant objects”Some objects come in several variants, which share a type name but have different fields. Account is one: an account is either a User or a Group. An @type field records the variant of each object. Creating such an object therefore needs the variant, which can be given in either of two equivalent forms:
- The
Object/Variantshorthand, as instalwart-cli create Account/User ..., which sets@type. The variant name is matched without regard to case. - An explicit
@type, passed with--fieldor as part of a--jsonpayload.
The shorthand is accepted only by create. The other commands, get, query, update and delete, take only the bare object name and reject the slash form.
Output formats
Section titled “Output formats”Every read command can produce output for people or for programs. By default, the CLI renders a human-readable view, laid out according to the form definitions in the schema. With --json, it writes compact, machine-readable JSON instead.
The shape of the JSON depends on the command. get --json writes a single line, because it returns one object. query --json, apply --json and snapshot write NDJSON (newline-delimited JSON), in which every line is a complete JSON record and there is no enclosing array. A program can therefore process the output record by record, and it pipes directly into jq and similar tools. When a tool needs a single JSON array, jq -s '.' turns an NDJSON stream into one.
The human-readable view uses ANSI colour when stdout is a terminal. Colour is turned off in any of the following cases:
- Standard output is not a terminal, for example when the output is piped to
head,lessor a file. - The
NO_COLORenvironment variable is set. --no-coloris passed.
Schema cache
Section titled “Schema cache”So that it does not download the schema on every command, the CLI fetches it from /api/schema and caches it under the standard XDG cache directory:
- Linux:
~/.cache/stalwart-cli/<server-hash>/ - macOS:
~/Library/Caches/stalwart-cli/<server-hash>/ - Windows:
%LOCALAPPDATA%\stalwart-cli\<server-hash>\
The <server-hash> is derived from the server URL, which keeps the schemas of several servers apart. Before each command, the CLI checks whether the server’s schema has changed, and downloads it again only if it has. When the server cannot be reached but a cached copy exists, the CLI prints a warning and carries on with the cached schema.
Errors
Section titled “Errors”The CLI reports every error in the same way. It writes the error to stderr in the form error: <message> and exits with status 1, so a script can detect a failure from the exit status alone.
The message carries as much detail as the server provides. A JMAP-level error is reported with its type and description. A per-object set error, such as a validation failure, a primary key violation or a refusal to delete an object that others depend on, also includes the affected properties and any linked objects that the server returns.
When the output is piped to a program that exits early, such as head, the closed pipe ends the output without an error.
Getting help
Section titled “Getting help”Every subcommand has standard help:
stalwart-cli --helpstalwart-cli get --helpstalwart-cli apply --helpThis help describes the options of the CLI. Help on the schema itself, meaning the descriptions of objects, fields, enumerations, filters and sort options, comes from the describe command.
Common tasks
Section titled “Common tasks”Create the first administrator
Section titled “Create the first administrator”A new server has no administrator account yet. Its only administrative credential is the recovery administrator, set with the STALWART_RECOVERY_ADMIN environment variable. Sign in with those credentials and create a permanent administrator account. In the following command, b is the id of the domain:
export STALWART_URL=https://mail.example.comstalwart-cli --user admin --password '<recovery password>' create Account/User \ --field name=admin \ --field domainId=b \ --field 'roles={"@type":"Admin"}' \ --field 'credentials={"0":{"@type":"Password","secret":"<new password>"}}'Then sign in as the new account and remove STALWART_RECOVERY_ADMIN. Managing accounts covers the other account operations.
Copy configuration between servers
Section titled “Copy configuration between servers”Copying configuration takes two commands. snapshot exports configuration objects of one server as a plan, and apply recreates them on another. A dry run before the real apply checks the plan without changing the target:
stalwart-cli --url https://old.example.com snapshot Tenant Domain \ --allow-unresolved Role,Directory,AcmeProvider,Certificate,DkimSignature,DnsServer \ --output plan.ndjsonstalwart-cli --url https://new.example.com apply --file plan.ndjson --dry-runstalwart-cli --url https://new.example.com apply --file plan.ndjsonThe plan does not carry secrets unless the snapshot is taken with --include-secrets from a server that returns them (Secrets). Idempotent plans explains how to make such a plan safe to run repeatedly.
Command failures
Section titled “Command failures”A failed command writes error: <message> to standard error and exits with status 1. The message usually names the cause, and these are the ones that come up most often:
authentication failed (HTTP 401): the credential is wrong, expired, or lacksauthenticate. An account with two-factor authentication refuses a password alone, so use an API key for it.permission denied (HTTP 403), or aforbiddenerror on an object: the credential lacks thesys*permission of the object or, when changing an account, a permission that the account holds (Granting permissions to other accounts).not found (HTTP 404):--urlpoints to the wrong address, for example a WebUI path such ashttps://mail.example.com/admin. It must name the server, such ashttps://mail.example.com.URL '<url>' is missing a scheme; prefix it with https:// or http://: the URL needshttps://orhttp://.- An unknown object, field or variant after a server upgrade: the cached schema is out of date. The CLI downloads the new schema when the server reports a change, and deleting the schema cache directory forces a download.
When the summary is not enough, the global --debug option prints diagnostics of the HTTP requests and responses, which show the server’s own error text.