Stalwart programmatic management (agent account, API key, CLI/JMAP)
Last updated 05/11/2026 · mail, handtrixxx.com, api
Current state (verified 2026-10-27 after the v1 cutover, see **stalwart-1-0-upgrade**). Stalwart v1 running (image stalwartlabs/stalwart:v1; the 0.16.27 notes below that mention the old version are historical), config in /data/docker/stalwart.handtrixxx.com/compose.yml, container stalwart (ports 25/465/587/143/993/4190, HTTP through Traefik).
- Upstream releases (checked 2026-10-26): v1.0.0 published 2026-10-26 (major), 0.16.27 is the last 0.16.x patch. 0.16.27 is the required source version for the 1.0 migration (see memory
stalwart-1-0-upgrade); the exact pin protects that baseline. - Gotcha:
docker compose restartreuses the old image — after changing the pin you mustdocker compose up -d(recreates the container). Verify withdocker exec stalwart stalwart --version. Admin UIhttps://stalwart.handtrixxx.com/admin/, user portal/account/, JMAP at/jmap/, healthGET /healthz/ready. Domainhandtrixxx.comis account idd, subAddressing enabled, no catch-all.
Accounts
agent@handtrixxx.com(idv) - agent identity, System Administrator role. Password:AGENT_MAILin/data/ai-persistence/.env.arcane@handtrixxx.com(idu, “Docker Managment”) - Arcane’s SMTP sender. Password:ARCANE_MAILin/data/ai-persistence/.env.mail@niklas-stephan.de(idg) - Niklas’s inbox, notification recipient.docker@handtrixxx.com(was idt) retired 2026-10-26: account deleted,dockerregistered as an alias ofarcane@(x:Account/setpatchaliases/0), verified live (mail todocker@lands inarcane@’s INBOX).- Stalwart v0.16 has no dedicated admin account type: management rights come from
sys*permissions granted to ordinary accounts viaRoleobjects.
API key
- Key id
c“Zed agent management key (handtrixxx.com)”,expiresAt 2027-10-26,allowedIps127.0.0.1,148.251.15.253,100.64.0.0/10. Secret:STALWART_API_KEYin/data/ai-persistence/.env. - Permission mode
Disableremoves: impersonate, sysApiKey*, sysAppPassword*, sysOAuthClient*, sysDnsServer*, sysNetworkListener*, store/telemetry updates, sysEnterpriseUpdate. Effective 613 of 635 permissions. Verified withGET /api/account. - A key can only narrow an account’s permissions, never extend them; JMAP calls on accounts it cannot access return
forbidden.
CLI and helper scripts (not installed on the host)
stalwart-cliv1.0.14 lives in/data/maintenance/stalwart-cli/stalwart-cli(decided 2026-10-26: keep out of/usr/local/bin). Flags:--url,--api-keyor--user/--password. Syntax:query <object>,get <object> <id>,create,update <object> <id> --field k=v,delete <object> --ids x,y.- Helpers in the same directory:
jmap_check.py(JMAP Account/Email queries),imap_check.py(IMAP inbox list,--deleteexpunges),send_test.py(SMTPS probe). All read secrets from/data/ai-persistence/.env.
Gotchas
- Management objects are JMAP-only:
POST /api/accountsor/api/accountreturns 404/401. Body:{"using":["urn:ietf:params:jmap:core","urn:stalwart:jmap"],"methodCalls":[["x:Account/set",{"accountId":"<id>","update":{"<id>":{"aliases/0":{"name":"docker","domainId":"d","enabled":true}}}},"a"]]} aliasesis an object keyed"0","1", not a list;aliases/0patch syntax works. A duplicatename@domainfails withprimaryKeyViolation- delete the conflicting account first.stalwart-cli update account <id> --field 'aliases=[...]'returnsinvalidPatch; use the JMAP route.set<...>fields (allowedIps) must be JSON objects ({"127.0.0.1":true}), not arrays.Email/querysupports neithersort(unsupportedSort) nor areceivedAtfilter;filter: {"text":"..."}(full text) works. With the management keyEmail/querycan return an emptyidseven when IMAP shows messages - IMAP (**imap_check.py**, port 993) is the dependable read path.GET /api/schemaredirects to/api/schema/<hash>; follow with-L.- SMTP: use 465 implicit TLS. 587 STARTTLS from this host fails;
curl --mail-todoes not exist. Wrong credentials give535 Login denied, good ones250. - Stalwart file logs stopped at
.../data/logs/stalwart.log.2026-05-06;docker logs stalwartis empty - check theLogobject rather than files.
Source: Wiki.js