NorthRelayNorthRelay
Operators

Stalwart Mail Server Setup

Configure Stalwart Mail Server for NorthRelay's split-role relay and mailbox architecture

Stalwart Mail Server Setup

Configure Stalwart's split-role architecture for outbound relay and inbound mailbox.


Current hosted mailbox deployment

The hosted mailbox service runs Stalwart Community 0.16.23 on the former shared relay host, with native JMAP administration. Client access is documented in Hosted email access. The deployment source is the platform repository's infra/mailbox-next directory.

The generic split-role examples below also describe the separately versioned application sending instance. Do not apply an old SQL-directory configuration or /api/principal management example to the 0.16 production mailbox. Mailbox administration uses native x:Account/*, x:Domain/* and x:AppPassword/* methods.

What is Stalwart

Stalwart Mail Server is a modern, Rust-based mail server distributed as a single binary. It supports SMTP, IMAP, JMAP, and includes built-in spam filtering, DKIM, and a management API.

NorthRelay uses Stalwart in a split-role architecture with two separate instances:

InstanceRoleLocationPurpose
Outbound RelayQueue + routingSanctuary VPSAccepts SMTP submissions, routes to the sending relay, manages queue
Inbound MailboxIMAP + JMAPMailbox VPSReceives inbound mail, user mailboxes, dashboard inbox

These are independent Stalwart instances with separate configurations, separate admin tokens, and separate Management APIs.


Split-Role Architecture

Outbound Relay (Sanctuary)

The relay instance handles all outbound email:

User App → SMTP submission → Stalwart Relay → [queue.route.relay] → sovereign.north-pipe.ca → Internet
                                                  (WireGuard, all tiers)

Key characteristics:

  • Runs on the Sanctuary VPS alongside the dashboard
  • Uses host networking in Docker for SMTP port binding
  • Routes every email, on every tier, to the single sending relay (sovereign.north-pipe.ca)
  • DKIM signing is disabled (handled by OpenDKIM on relay VPSes)
  • Management API on port 8080 (internal only)

Inbound Mailbox

The mailbox instance handles all inbound email and user inbox access:

Internet → MX record → Stalwart Mailbox → User Mailbox
                                              ↓
                              Dashboard ← JMAP API → User reads email

Key characteristics:

  • Runs on a dedicated Mailbox VPS
  • MX records point here (mx.northrelay.ca)
  • Provides IMAP and JMAP access for dashboard inbox
  • Uses SQL directory queries against PostgreSQL for authentication
  • Each user's inbox email = their subdomain slot email

TOML Configuration Basics

Stalwart's static configuration uses TOML format. Key sections:

Server Configuration

[server]
hostname = "relay.northrelay.ca"
max-connections = 1024

[server.listener.smtp]
bind = ["0.0.0.0:25"]
protocol = "smtp"

[server.listener.submission]
bind = ["0.0.0.0:587"]
protocol = "smtp"
tls.implicit = true

[server.listener.management]
bind = ["127.0.0.1:8080"]
protocol = "http"

Authentication

[authentication]
fallback-admin.user = "admin"
fallback-admin.secret = "YOUR_HEX_TOKEN"

The fallback-admin.secret is the token used for Management API and CLI authentication.

Queue and Routing

[queue.outbound]
# DKIM signing disabled - handled by OpenDKIM on relay VPSes
sign.enable = false

[queue.route.relay]
type = "relay"
address = "sovereign.north-pipe.ca"   # DNS hostname — never a raw IP (Stalwart v0.15 bug)
port = 25

[queue.strategy]
route = "'relay'"

See Outbound routing below for the details.


Management API

The Management API runs on port 8080 and provides HTTP endpoints for administration.

Authentication

All API requests require authentication:

# Using basic auth
curl -u admin:TOKEN http://localhost:8080/api/...

# Using bearer token
curl -H "Authorization: Bearer TOKEN" http://localhost:8080/api/...

Common API Operations

# List all principals (user accounts)
curl -u admin:TOKEN http://localhost:8080/api/principal

# Get a specific principal
curl -u admin:TOKEN http://localhost:8080/api/principal/user@domain.com

# Create a principal
curl -u admin:TOKEN -X POST http://localhost:8080/api/principal \
  -H "Content-Type: application/json" \
  -d '{
    "type": "individual",
    "name": "user@domain.com",
    "emails": ["user@domain.com"],
    "secrets": ["$2b$10$..."]
  }'

# View mail queue
curl -u admin:TOKEN http://localhost:8080/api/queue/messages

# Delete a queued message
curl -u admin:TOKEN -X DELETE http://localhost:8080/api/queue/messages/MESSAGE_ID

CLI Tool

The Stalwart CLI (stalwart-cli) manages runtime configuration stored in RocksDB.

Basic Usage

# General syntax (inside Docker)
docker exec CONTAINER /usr/local/bin/stalwart-cli \
  -u http://localhost:8080 -c admin:TOKEN \
  COMMAND

Configuration Commands

# List all config keys matching a pattern
docker exec stalwart stalwart-cli \
  -u http://localhost:8080 -c admin:TOKEN \
  server list-config 'server.blocked-ip'

# Add a configuration key
docker exec stalwart stalwart-cli \
  -u http://localhost:8080 -c admin:TOKEN \
  server add-config 'key.name' "value"

# Delete a configuration key
docker exec stalwart stalwart-cli \
  -u http://localhost:8080 -c admin:TOKEN \
  server delete-config 'key.name'

# Reload TOML configuration
docker exec stalwart stalwart-cli \
  -u http://localhost:8080 -c admin:TOKEN \
  server reload-config

IP Blocking Prevention

This is the most common operational issue with Stalwart.

Stalwart has an auto-ban feature that blocks IP addresses after failed authentication attempts. Blocked IPs are stored in RocksDB (not the TOML config), so they persist across restarts.

Symptoms

  • SMTP connection closes immediately without a banner
  • Telnet to port 25 connects then disconnects instantly
  • No error in application logs - the connection is dropped at the Stalwart level

Diagnosis

# Check if your IP is blocked
docker exec stalwart stalwart-cli \
  -u http://localhost:8080 -c admin:TOKEN \
  server list-config 'server.blocked-ip' | grep YOUR_IP

Fix: Unblock an IP

# Remove the blocked IP
docker exec stalwart stalwart-cli \
  -u http://localhost:8080 -c admin:TOKEN \
  server delete-config 'server.blocked-ip.YOUR_IP'

Prevention

Add trusted IPs to the allowlist in your TOML config:

[server.security]
# IPs that should never be auto-banned
allowed-ips = ["10.0.0.0/8", "172.16.0.0/12", "YOUR_OFFICE_IP"]

Or disable auto-ban entirely for development:

[server.security.auto-ban]
enable = false

SQL Directory Queries

The mailbox Stalwart instance uses SQL queries against PostgreSQL for user authentication and mail routing. These queries are stored in RocksDB and queried at runtime.

Query Types

QueryPurposeSupports Catch-All
recipientsDetermine if an address should be acceptedYes
nameResolve a user account from an addressYes
emailsGet all allowed sender addresses for a userYes
secretsGet password hash for authenticationNo
membersGroup membership lookupNo

Managing SQL Queries

# List all SQL queries
docker exec mailbox_stalwart stalwart-cli \
  -u http://localhost:8080 -c admin:TOKEN \
  server list-config 'store.postgresql.query'

# Update a query (delete then add)
docker exec mailbox_stalwart stalwart-cli \
  -u http://localhost:8080 -c admin:TOKEN \
  server delete-config 'store.postgresql.query.recipients'

docker exec mailbox_stalwart stalwart-cli \
  -u http://localhost:8080 -c admin:TOKEN \
  server add-config 'store.postgresql.query.recipients' \
  "SELECT email FROM users WHERE email = \$1 UNION SELECT alias FROM mailbox_aliases WHERE alias = \$1 AND active = true"

# Always reload after query changes
docker exec mailbox_stalwart stalwart-cli \
  -u http://localhost:8080 -c admin:TOKEN \
  server reload-config

Catch-All Support

The recipients and name queries include catch-all logic: if no exact match is found, they check whether the domain has is_inbox_hosted = true in the domains table. If so, all mail to that domain routes to the domain owner.


Principal Management

Stalwart principals represent user accounts. For the mailbox instance, principals are provisioned automatically during user registration.

Creating a Principal

curl -u admin:TOKEN -X POST http://localhost:8080/api/principal \
  -H "Content-Type: application/json" \
  -d '{
    "type": "individual",
    "name": "user@domain.com",
    "emails": ["user@domain.com", "alias@domain.com"],
    "secrets": ["$2b$10$BCRYPT_HASH"],
    "quota": 0
  }'

Principal Fields

FieldDescription
typeindividual for user accounts
namePrimary identifier (usually email)
emailsArray of all email addresses this principal can send/receive as
secretsArray containing bcrypt password hash
quotaMailbox size limit in bytes (0 = unlimited)

Outbound routing

The outbound relay sends every message, on every tier, through one route to the sending relay:

RouteAddressRelay VPS
relaysovereign.north-pipe.ca:25 (over WireGuard, 10.0.2.2)76.13.27.101
[queue.route.relay]
type = "relay"
address = "sovereign.north-pipe.ca"
port = 25

[queue.strategy]
route = "'relay'"

Rules that matter in Stalwart v0.15:

  • The route address must be a DNS hostname, never a raw IP — a raw IP triggers a "record not found for MX" error. On Sanctuary, /etc/hosts maps sovereign.north-pipe.ca to the WireGuard address 10.0.2.2 so the route stays inside the tunnel.
  • queue.outbound.next-hop is ignored; only queue.strategy.route selects the route.
  • RocksDB keys override the TOML file. After changing the route, delete any stale queue.strategy.route / queue.route.* keys and run server reload-config.
  • Never set queue.outbound.is_local on the relay instance — it has no mailboxes.

Tier does not affect routing. See Sending Infrastructure for what protects the relay's reputation.


Next Steps

On this page