niklas‑stephan.de

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 restart reuses the old image — after changing the pin you must docker compose up -d (recreates the container). Verify with docker exec stalwart stalwart --version. Admin UI https://stalwart.handtrixxx.com/admin/, user portal /account/, JMAP at /jmap/, health GET /healthz/ready. Domain handtrixxx.com is account id d, subAddressing enabled, no catch-all.

Accounts

  • agent@handtrixxx.com (id v) - agent identity, System Administrator role. Password: AGENT_MAIL in /data/ai-persistence/.env.
  • arcane@handtrixxx.com (id u, “Docker Managment”) - Arcane’s SMTP sender. Password: ARCANE_MAIL in /data/ai-persistence/.env.
  • mail@niklas-stephan.de (id g) - Niklas’s inbox, notification recipient.
  • docker@handtrixxx.com (was id t) retired 2026-10-26: account deleted, docker registered as an alias of arcane@ (x:Account/set patch aliases/0), verified live (mail to docker@ lands in arcane@’s INBOX).
  • Stalwart v0.16 has no dedicated admin account type: management rights come from sys* permissions granted to ordinary accounts via Role objects.

API key

  • Key id c “Zed agent management key (handtrixxx.com)”, expiresAt 2027-10-26, allowedIps 127.0.0.1, 148.251.15.253, 100.64.0.0/10. Secret: STALWART_API_KEY in /data/ai-persistence/.env.
  • Permission mode Disable removes: impersonate, sysApiKey*, sysAppPassword*, sysOAuthClient*, sysDnsServer*, sysNetworkListener*, store/telemetry updates, sysEnterpriseUpdate. Effective 613 of 635 permissions. Verified with GET /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-cli v1.0.14 lives in /data/maintenance/stalwart-cli/stalwart-cli (decided 2026-10-26: keep out of /usr/local/bin). Flags: --url, --api-key or --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, --delete expunges), send_test.py (SMTPS probe). All read secrets from /data/ai-persistence/.env.

Gotchas

  • Management objects are JMAP-only: POST /api/accounts or /api/account returns 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"]]}
  • aliases is an object keyed "0", "1", not a list; aliases/0 patch syntax works. A duplicate name@domain fails with primaryKeyViolation - delete the conflicting account first.
  • stalwart-cli update account <id> --field 'aliases=[...]' returns invalidPatch; use the JMAP route.
  • set<...> fields (allowedIps) must be JSON objects ({"127.0.0.1":true}), not arrays.
  • Email/query supports neither sort (unsupportedSort) nor a receivedAt filter; filter: {"text":"..."} (full text) works. With the management key Email/query can return an empty ids even when IMAP shows messages - IMAP (**imap_check.py**, port 993) is the dependable read path.
  • GET /api/schema redirects to /api/schema/<hash>; follow with -L.
  • SMTP: use 465 implicit TLS. 587 STARTTLS from this host fails; curl --mail-to does not exist. Wrong credentials give 535 Login denied, good ones 250.
  • Stalwart file logs stopped at .../data/logs/stalwart.log.2026-05-06; docker logs stalwart is empty - check the Log object rather than files.

Source: Wiki.js