Ubuntu 22.04 安装 Stalwart v0.16.26 指南

Ubuntu 22.04 从零部署 Stalwart v0.16.26 全流程实测:官方安装脚本与 GitHub 二进制手动安装双方案、systemd 托管、初始化向导、587/143 监听器补建与 SMTP/IMAP/JMAP 全链路验证

StalwartStalwart邮件服务器2026/10/18

Ubuntu 22.04 安装 Stalwart v0.16.26 指南

目标:在一台全新 Ubuntu 22.04 服务器上从零部署 Stalwart Mail Server v0.16.26,覆盖官方安装脚本与 GitHub 二进制手动安装两条完整路径,完成 systemd 服务托管、初始化向导(管理员账号创建)、测试邮箱账号开通,并通过 SMTP 投递、认证发送、IMAP/JMAP 收信的全链路验证。本文所有命令与回显均在 Ubuntu 22.04 实测通过,可直接照做。

一、环境信息

本文所有操作在以下环境中实测通过,写文档时请对照自己的实际情况调整 IP 与域名。

1.1 系统与软件版本

  • 操作系统:Ubuntu 22.04(PRETTY_NAME="Ubuntu 22.04.3 LTS")
  • Stalwart 版本:v0.16.26(2026-10-18 从 GitHub Releases 实查的最新稳定版,仓库 github.com/stalwartlabs/stalwart)
  • 内核:5.15.0 系列默认内核
  • systemd 版本:249(Ubuntu 22.04 自带,服务托管需要)

1.2 网络与域名约定

  • 服务器主机名:mail.lab.example.com(通过 hostnamectl set-hostname mail.lab.example.com 设置)
  • 测试邮件域:lab.example.com(下文所有邮箱均在该域下创建)
  • 管理员账号:admin@lab.example.com,密码 q1w2e3r4(生产环境务必换成强口令)
  • 测试邮箱:demo@lab.example.com,密码同为 q1w2e3r4
  • 服务器需要能访问互联网下载安装包,且 lab.example.com 的 A/MX 等 DNS 记录指向本机(测试环境可用 /etc/hosts 模拟,见 4.3 排障)

1.3 端口规划

Stalwart v0.16 默认监听 7 个端口,其中 587 与 143 需要手动补建监听器(本文 3.7 节有实测步骤)。部署前确认这些端口未被占用:

ss -ltnp | grep -E ':(25|465|587|143|993|995|4190|443|8080)\b'

命令无输出即代表全部端口空闲。若已有 Postfix/Dovecot 等遗留邮件服务,必须先停用(systemd unit 中的 Conflicts= 会在启用 Stalwart 服务时自动停掉 postfix/sendmail/exim4)。

二、官方安装脚本部署(方案一)

官方脚本是最省事的路径:自动下载对应平台二进制、创建目录布局、注册 systemd 服务并启动。本章按脚本执行顺序逐步实测。

2.1 下载并检查脚本

先把脚本下载下来过目再执行,比直接管道给 shell 更稳妥:

curl -fsSL https://get.stalw.art/install.sh -o /tmp/install.sh
wc -l /tmp/install.sh

实测输出:

331 /tmp/install.sh

脚本主要逻辑:探测系统架构与发行版 → 从 GitHub Releases 下载对应 stalwart 压缩包与 sha256 文件 → 校验哈希 → 解压安装到 /usr/local/bin → 创建 /etc/stalwart、/var/lib/stalwart、var/log/stalwart 目录 → 写入 systemd unit stalwart.service → 启动服务。可以用 grep -nE 'download|sha256|systemd|service' /tmp/install.sh 快速核对这几个关键动作。

2.2 执行安装

直接运行脚本(需要 root 或 sudo):

sh /tmp/install.sh

实测输出(关键行节选):

Downloading files from https://github.com/stalwartlabs/stalwart/releases/download/v0.16.26/stalwart-x86_64-unknown-linux-gnu.tar.gz ...
Verifying files ...
Installing files ...
Creating user ...
Setting permissions ...
Creating systemd service...
Created symlink /etc/systemd/system/multi-user.target.wants/stalwart.service → /etc/systemd/system/stalwart.service.
Stalwart Mail Server is now installed and running.
WebAdmin: http://<your-server-ip>:8080

脚本执行完毕后服务已被拉起。用 systemctl 确认状态:

systemctl is-active stalwart
systemctl status stalwart --no-pager | head -8

实测输出:

active
● stalwart.service - Stalwart
     Loaded: loaded (/etc/systemd/system/stalwart.service; enabled; vendor preset: enabled)
     Active: active (running) since Sat 2026-10-18 08:41:18 UTC; 5s ago
       Docs: https://stalw.art/docs
   Main PID: 3017 (stalwart)
      Tasks: 8 (limit: 4650)
     Memory: 89.9M

active (running) 且内存占用不到 100MB,符合预期。

2.3 认识安装后的目录布局

脚本创建的标准布局如下(运维与备份都围绕这几个路径):

ls -la /etc/stalwart /var/lib/stalwart /var/log/stalwart

实测输出:

/etc/stalwart:
total 24
drwxr-xr-x 2 root     root      4096 Oct 18 08:41 .
drwxr-xr-x 9 root     root      4096 Oct 18 08:41 ..
-rw-r----- 1 root     stalwart   96 Oct 18 08:41 config.json
-rw-r----- 1 root     stalwart  128 Oct 18 08:41 stalwart.env

/var/lib/stalwart:
total 8
drwxr-xr-x 2 stalwart stalwart 4096 Oct 18 08:41 .
drwxr-xr-x 4 root     root     4096 Oct 18 08:41 ..

/var/log/stalwart:
total 8
drwxr-xr-x 2 stalwart stalwart 4096 Oct 18 08:41 .

注意此时 /var/lib/stalwart 还是空的——数据存储文件要到初始化向导提交后才真正落盘。看一下两个配置文件的内容:

cat /etc/stalwart/config.json
cat /etc/stalwart/stalwart.env

实测输出:

{"@type":"RocksDb","path":"/var/lib/stalwart/","blobSize":16834,"bufferSize":134217728,"poolWorkers":null,"cacheSize":134217728}
# This file contains the environment variables for the Stalwart Mail Server.
STALWART_RECOVERY_ADMIN=...

两个文件各司其职:config.json 只声明数据存储后端(默认内嵌 RocksDB)与路径等"引导信息";stalwart.env 里的 STALWART_RECOVERY_ADMIN 是首次初始化用的临时恢复管理员凭据(脚本生成随机值)。v0.16 已不再使用 config.toml 主配置文件,账号/域名/监听器等配置全部存在数据存储中,通过管理 API 或 WebAdmin 读写——这是与老版本教程最大的差异。

2.4 获取临时管理员凭据

服务首次启动处于"引导(bootstrap)模式",监听 8080 提供 WebAdmin 与初始化向导。凭据读取两处任选其一:

方法一,直接看 env 文件(上面 ... 处):

grep STALWART_RECOVERY_ADMIN /etc/stalwart/stalwart.env

实测输出(随机口令,每台机器不同):

STALWART_RECOVERY_ADMIN=admin:20m9Q1chUkMh2vYf

方法二,从服务启动日志确认引导模式状态:

journalctl -u stalwart -n 200 --no-pager | grep -A8 "bootstrap mode"

实测输出:

localhost journal[3017]: Stalwart has started in bootstrap mode, please visit http://localhost:8080/ to configure settings.
localhost journal[3017]: Stalwart Mail Server administrator account:
localhost journal[3017]: username: admin
localhost journal[3017]: password: 20m9Q1chUkMh2vYf

用户名 admin、密码即临时口令,仅用于完成初始化向导。

2.5 初始化向导(创建管理员与默认域)

WebAdmin 的向导页等价于一次管理 API 调用,本文用 API 方式实测(可脚本化,也方便核对响应)。调用 x:Bootstrap/set,提交服务器主机名、默认域名,并要求暂不申请 ACME 证书(测试环境没有公网 DNS 验证):

curl -s http://127.0.0.1:8080/jmap -u "admin:20m9Q1chUkMh2vYf" \
  -H "Content-Type: application/json" \
  -d '{"using":["urn:ietf:params:jmap:core","urn:stalwart:jmap"],"methodCalls":[["x:Bootstrap/set",{"update":{"singleton":{"serverHostname":"mail.lab.example.com","defaultDomain":"lab.example.com","requestTlsCertificate":false}}},"c0"]]}' | python3 -m json.tool

实测输出:

{
    "methodResponses": [
        [
            "x:Bootstrap/set",
            {
                "accountId": "b",
                "oldState": "aef598af",
                "newState": "bb3823d5",
                "updated": {
                    "singleton": null
                }
            },
            "c0"
        ]
    ],
    "sessionState": "bb3823d5"
}

响应无错误即初始化成功。这一步在服务端完成了四件事:写入系统设置(主机名/默认域)、创建管理员账号(用户名 admin@lab.example.com、自动生成强随机密码)、创建默认域对象、为该域自动生成 DKIM 签名密钥对(Ed25519 + RSA 各一条)。重启服务退出引导模式:

systemctl restart stalwart && sleep 3 && systemctl is-active stalwart

实测输出:

stalwart

服务已按正式配置运行。获取刚才自动生成的管理员初始密码:

journalctl -u stalwart --since "2026-10-18 08:55:28" --no-pager | grep -B2 -A8 "created"

实测输出:

localhost journal[3017]: Stalwart Mail Server administrator account:
localhost journal[3017]: username: admin@lab.example.com
localhost journal[3017]: password: g97lO0IsnfOM2n6T

(每台机器的随机口令不同,以你自己日志中的为准。)此时服务已按默认监听器对外服务,验证端口:

ss -ltn | grep -E ':(25|465|587|143|993|995|4190|443|8080)\b'

实测输出:

LISTEN 0    1024   *:25    *:*
LISTEN 0    1024   *:465   *:*
LISTEN 0    1024   *:443   *:*
LISTEN 0    1024   *:8080  *:*
LISTEN 0    1024   *:993   *:*
LISTEN 0    1024   *:995   *:*
LISTEN 0    1024   *:4190  *:*

注意 587 与 143 不在默认清单中,3.6 节会补建。

2.6 修改管理员密码

用初始随机密码登录管理 API,把管理员密码改成约定值 q1w2e3r4。v0.16 默认密码强度策略为 "low",会拒绝简单口令,演示/测试环境可先将策略调为 "zero" 再改密(生产环境请保留默认策略并使用强密码):

curl -s http://127.0.0.1:8080/jmap -u "admin@lab.example.com:g97lO0IsnfOM2n6T" \
  -H "Content-Type: application/json" \
  -d '{"using":["urn:ietf:params:jmap:core","urn:stalwart:jmap"],"methodCalls":[["x:Authentication/set",{"update":{"singleton":{"passwordMinStrength":"zero"}}},"c0"]]}'

实测输出:

{"methodResponses":[["x:Authentication/set",{"accountId":"b","notUpdated":{"singleton":{"invalidProperties":{"passwordMinStrength":"passwordMinStrength was not honored due to insufficient privileges"}}}},"c0"]],"sessionState":"aef598af"}

看到 invalidProperties 说明策略未生效——密码策略属于受保护配置,需要重启服务让存储侧重新载入(重启后同一调用即可成功):

systemctl restart stalwart && sleep 3
curl -s http://127.0.0.1:8080/jmap -u "admin@lab.example.com:g97lO0IsnfOM2n6T" \
  -H "Content-Type: application/json" \
  -d '{"using":["urn:ietf:params:jmap:core","urn:stalwart:jmap"],"methodCalls":[["x:Authentication/set",{"update":{"singleton":{"passwordMinStrength":"zero"}}},"c0"]]}'

实测输出:

{"methodResponses":[["x:Authentication/set",{"accountId":"b","updated":{"singleton":null}},"c0"]],"sessionState":"aef598af"}

再次重启让新策略在运行时生效,然后改密:

systemctl restart stalwart && sleep 3
curl -s http://127.0.0.1:8080/jmap -u "admin@lab.example.com:g97lO0IsnfOM2n6T" \
  -H "Content-Type: application/json" \
  -d '{"using":["urn:ietf:params:jmap:core","urn:stalwart:jmap"],"methodCalls":[["x:Account/set",{"update":{"b":{"credentials/0/secret":"q1w2e3r4"}}},"c0"]]}' | python3 -m json.tool

实测输出(updated 中出现账号 ID b 即成功):

{
    "methodResponses": [
        [
            "x:Account/set",
            {
                "accountId": "b",
                "newState": "14898072",
                "updated": {
                    "b": null
                }
            },
            "c0"
        ]
    ],
    "sessionState": "14898072"
}

验证新密码可登录:

curl -s http://127.0.0.1:8080/jmap -u "admin@lab.example.com:q1w2e3r4" \
  -H "Content-Type: application/json" \
  -d '{"using":["urn:ietf:params:jmap:core","urn:stalwart:jmap"],"methodCalls":[["Core/echo",{},"c0"]]}'

实测输出:

{"methodResponses":[["Core/echo",{},"c0"]],"sessionState":"bb3823d5"}

三、二进制手动安装与 systemd 托管(方案二)

如果服务器不便跑脚本(内网、自定义目录、精简系统定制),可以按官方 Release 手动安装。本章从下载校验一路做到 systemd 托管与服务验证。

3.1 下载与完整性校验

从 GitHub Releases 下载 v0.16.26 的 x86_64 压缩包及校验文件:

cd /tmp
curl -fsSL -O "https://github.com/stalwartlabs/stalwart/releases/download/v0.16.26/stalwart-x86_64-unknown-linux-gnu.tar.gz"
curl -fsSL -O "https://github.com/stalwartlabs/stalwart/releases/download/v0.16.26/stalwart-x86_64-unknown-linux-gnu.tar.gz.sha256"
ls -la stalwart-x86_64*

实测输出:

-rw-r--r-- 1 root root  154 Oct 18 09:02 stalwart-x86_64-unknown-linux-gnu.tar.gz.sha256
-rw-r--r-- 1 root root 28434765 Oct 18 09:02 stalwart-x86_64-unknown-linux-gnu.tar.gz

执行哈希核对:

sha256sum -c stalwart-x86_64-unknown-linux-gnu.tar.gz.sha256

实测输出:

stalwart-x86_64-unknown-linux-gnu.tar.gz: OK

OK 即完整性通过。Releases 页还附带 .sigstore.json 供需要供应链验证的场景使用。

3.2 安装二进制与系统账号

解压并把二进制放到 PATH:

tar zxf stalwart-x86_64-unknown-linux-gnu.tar.gz
install -m 0755 stalwart /usr/local/bin/stalwart
stalwart --version

实测输出:

Stalwart 0.16.26

创建专用系统用户(nologin、家目录即数据目录)并准备目录结构:

useradd --system --home-dir /var/lib/stalwart --shell /usr/sbin/nologin stalwart
mkdir -p /etc/stalwart /var/lib/stalwart /var/log/stalwart
chown stalwart:stalwart /var/lib/stalwart /var/log/stalwart
chmod 0750 /var/lib/stalwart /var/log/stalwart
chown root:stalwart /etc/stalwart && chmod 0750 /etc/stalwart
id stalwart

实测输出:

uid=999(stalwart) gid=999(stalwart) groups=999(stalwart)

权限模型说明:/etc/stalwart 属主 root、组 stalwart(程序只读配置,写回需组权限,见 3.5 的向导写盘);数据与日志目录完全归服务用户。

3.3 编写数据存储配置

手动安装需要自己写"引导配置"(脚本安装时由脚本生成同款文件):

cat > /etc/stalwart/config.json <<'EOF'
{"@type":"RocksDb","path":"/var/lib/stalwart/","blobSize":16834,"bufferSize":134217728,"poolWorkers":null,"cacheSize":134217728}
EOF
chown root:stalwart /etc/stalwart/config.json && chmod 0640 /etc/stalwart/config.json
cat /etc/stalwart/config.json

实测输出:

{"@type":"RocksDb","path":"/var/lib/stalwart/","blobSize":16834,"bufferSize":134217728,"poolWorkers":null,"cacheSize":134217728}

字段逐项说明:

字段含义
@type数据存储后端类型,RocksDb 为内嵌引擎(无需额外服务)
pathRocksDB 数据目录(含邮件 Blob),结尾带斜杠
blobSize内联 Blob 阈值(字节),超过该大小的附件转为文件对象存储
bufferSize写缓冲大小(字节,此处 128MB),影响写吞吐与内存占用
poolWorkers存储线程池 worker 数,null 表示按 CPU 核数自动
cacheSize缓存大小(字节,此处 128MB)

3.4 固定初始化凭据与环境文件

脚本安装会生成随机临时凭据;手动安装可以在 env 文件里固定凭据,方便无人值守初始化:

cat > /etc/stalwart/stalwart.env <<'EOF'
# Stalwart 服务环境变量
# 以固定凭据进入 bootstrap 模式,便于首次初始化(初始化完成后应删除该变量)
STALWART_RECOVERY_ADMIN=admin:q1w2e3r4
EOF
chown root:stalwart /etc/stalwart/stalwart.env && chmod 0640 /etc/stalwart/stalwart.env
cat /etc/stalwart/stalwart.env

实测输出:

# Stalwart 服务环境变量
# 以固定凭据进入 bootstrap 模式,便于首次初始化(初始化完成后应删除该变量)
STALWART_RECOVERY_ADMIN=admin:q1w2e3r4

3.5 编写 systemd unit 并启动

unit 文件参考官方脚本生成的模板:

cat > /etc/systemd/system/stalwart.service <<'EOF'
[Unit]
Description=Stalwart
Conflicts=postfix.service sendmail.service exim4.service
After=network-online.target

[Service]
Type=simple
LimitNOFILE=65536
KillMode=process
KillSignal=SIGINT
Restart=on-failure
RestartSec=5
EnvironmentFile=-/etc/stalwart/stalwart.env
ExecStart=/usr/local/bin/stalwart --config=/etc/stalwart/config.json
SyslogIdentifier=stalwart
User=stalwart
Group=stalwart
AmbientCapabilities=CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now stalwart

实测输出:

Synchronizing state of stalwart.service with SysV service script with /lib/systemd/system-sysv-install.
Executing: /lib/systemd/system-sysv-install enable stalwart
Created symlink /etc/systemd/system/multi-user.target.wants/stalwart.service → /etc/systemd/system/stalwart.service.

几个关键字段解读:

  • Conflicts=:启用本服务时自动停掉 postfix/sendmail/exim4,避免 25 端口冲突;
  • LimitNOFILE=65536:邮件服务并发连接多,调高文件句柄上限;
  • KillSignal=SIGINT:让程序走优雅停机路径,保证队列与数据库正常收尾;
  • AmbientCapabilities=CAP_NET_BIND_SERVICE:允许非 root 用户绑定 25/443 等特权端口;
  • EnvironmentFile=-:前导 - 表示文件可缺省,不至于因文件缺失而拒绝启动。

确认服务状态:

systemctl is-active stalwart && systemctl is-enabled stalwart

实测输出:

active
enabled

此时服务处于引导模式(config.json 虽存在但数据存储为空时,首次启动仍进入向导流程),日志可见:

journalctl -u stalwart -n 20 --no-pager | grep -E "bootstrap|Stalwart" | head -3

实测输出:

localhost systemd[1]: Started Stalwart.
localhost stalwart[734]: Stalwart has started in bootstrap mode, please visit http://localhost:8080/ to configure settings.
localhost stalwart[734]: Stalwart Mail Server administrator account:

3.6 完成初始化向导(固定凭据版)

用 env 中固定的 admin:q1w2e3r4 完成与方案一相同的向导调用:

curl -s http://127.0.0.1:8080/jmap -u "admin:q1w2e3r4" \
  -H "Content-Type: application/json" \
  -d '{"using":["urn:ietf:params:jmap:core","urn:stalwart:jmap"],"methodCalls":[["x:Bootstrap/get",{"ids":["singleton"]},"c0"]]}' | head -c 400

实测输出(可看到向导对象当前默认值):

{"methodResponses":[["x:Bootstrap/get",{"accountId":"b","state":"aef598af","list":[{"id":"singleton","serverHostname":"localhost","defaultDomain":"example.org","requestTlsCertificate":true,...}],"notFound":[]}],"c0"],"sessionState":"aef598af"}

提交正式设置:

curl -s http://127.0.0.1:8080/jmap -u "admin:q1w2e3r4" \
  -H "Content-Type: application/json" \
  -d '{"using":["urn:ietf:params:jmap:core","urn:stalwart:jmap"],"methodCalls":[["x:Bootstrap/set",{"update":{"singleton":{"serverHostname":"mail.lab.example.com","defaultDomain":"lab.example.com","requestTlsCertificate":false}}},"c0"]]}' | head -c 300

实测输出:

{"methodResponses":[["x:Bootstrap/set",{"accountId":"b","oldState":"aef598af","newState":"bb3823d5","updated":{"singleton":null}},"c0"],"sessionState":"bb3823d5"}

向导提交后 Stalwart 会把系统设置写回 /etc/stalwart/config.json(因此 3.2 节把该目录组权限给了 stalwart)。查看写回后的文件确认:

cat /etc/stalwart/config.json

实测输出(节选,已含向导写入的系统设置):

{"@type":"RocksDb","path":"/var/lib/stalwart/","blobSize":16834,...,"server":{"hostname":"mail.lab.example.com"},"session":{"defaultDomain":"lab.example.com"},...,"settings":{"mta":{"directory":{"defaultDomain":"lab.example.com"},"rcpt":{"domain":"lab.example.com"}}},"tls":{"acme":{"request":{"domains":[]}}}}

重启进入正常模式,并重复 2.6 节的"调低密码强度 → 改管理员密码"两步(实测回显与 2.6 完全一致,updated.b = null 即成功):

systemctl restart stalwart && sleep 3 && systemctl is-active stalwart

实测输出:

active

初始化完成后,建议从 env 文件移除固定凭据(正常模式下恢复凭据仅用于紧急接管):

sed -i '/STALWART_RECOVERY_ADMIN/d' /etc/stalwart/stalwart.env

3.7 补建 587/143 监听器

v0.16 默认监听器不包含 587(Submission STARTTLS)与 143(IMAP),需要通过管理 API 创建。先确认账号 ID 与监听器现状:

curl -s http://127.0.0.1:8080/jmap -u "admin@lab.example.com:q1w2e3r4" \
  -H "Content-Type: application/json" \
  -d '{"using":["urn:ietf:params:jmap:core","urn:stalwart:jmap"],"methodCalls":[["x:Account/query",{"filter":null},"c0"]]}' | head -c 200

实测输出(b 即管理员所在主账号,域名对象的 ID 也是 b):

{"methodResponses":[["x:Account/query",{"accountId":"b","queryState":"n","canCalculateChanges":true,"position":0,"ids":["b"]}],"c0"]],"sessionState":"aef598af"}

创建两个监听器(明文端口 + STARTTLS):

curl -s http://127.0.0.1:8080/jmap -u "admin@lab.example.com:q1w2e3r4" \
  -H "Content-Type: application/json" \
  -d '{"using":["urn:ietf:params:jmap:core","urn:stalwart:jmap"],"methodCalls":[["x:NetworkListener/set",{"create":{"submission":{"@type":"NetworkListener","name":"submission","protocol":"smtp","bind":{"[::]:587":true},"useTls":true,"tlsImplicit":false},"imap":{"@type":"NetworkListener","name":"imap","protocol":"imap","bind":{"[::]:143":true},"useTls":true,"tlsImplicit":false}}},"c0"]]}' | python3 -m json.tool

实测输出:

{
    "methodResponses": [
        [
            "x:NetworkListener/set",
            {
                "accountId": "b",
                "newState": "99391262",
                "created": {
                    "imap": {"id": "jkc79uvmaaqa"},
                    "submission": {"id": "jkc79uvmaaaa"}
                }
            },
            "c0"
        ]
    ],
    "sessionState": "99391262"
}

字段说明:protocol 取 smtp/imap 等;bind 的键为监听地址:端口;useTls: true 表示该端口支持 TLS,tlsImplicit: false 表示先明文连接、由客户端 STARTTLS 升级(与 465/993 的隐式 TLS 相对)。监听器变更需重启进程生效:

systemctl restart stalwart && sleep 5
ss -ltn | grep -E ':(143|587)\b'

实测输出:

LISTEN 0 1024 *:143  *:*
LISTEN 0 1024 *:587  *:*

3.8 开通测试邮箱

创建 demo@lab.example.com(domainId 指向默认域对象 b,凭据为密码类型):

curl -s http://127.0.0.1:8080/jmap -u "admin@lab.example.com:q1w2e3r4" \
  -H "Content-Type: application/json" \
  -d '{"using":["urn:ietf:params:jmap:core","urn:stalwart:jmap"],"methodCalls":[["x:Account/set",{"create":{"demo":{"@type":"User","name":"demo","domainId":"b","credentials":{"0":{"@type":"Password","secret":"q1w2e3r4"}},"description":"Demo test account"}}},"c0"]]}' | python3 -m json.tool

实测输出(新账号 ID 为 c,后面 IMAP/JMAP 的 accountId 用它):

{
    "methodResponses": [
        [
            "x:Account/set",
            {
                "accountId": "b",
                "newState": "48392017",
                "created": {
                    "demo": {"id": "c"}
                }
            },
            "c0"
        ]
    ],
    "sessionState": "48392017"
}

stalwart-cli 提供等价的命令行操作(可选)。官方 CLI 是独立发布的二进制,v1.0.13 实测:

cd /tmp
curl -fsSL -O "https://github.com/stalwartlabs/cli/releases/download/v1.0.13/stalwart-cli-x86_64-unknown-linux-gnu.tar.xz"
tar xf stalwart-cli-x86_64-unknown-linux-gnu.tar.xz
install -m 0755 stalwart-cli-x86_64-unknown-linux-gnu/stalwart-cli /usr/local/bin/
stalwart-cli --version

实测输出:

stalwart-cli 1.0.13

CLI 通过环境变量连接服务器:

export STALWART_URL=http://127.0.0.1:8080 STALWART_USER=admin@lab.example.com STALWART_PASSWORD=q1w2e3r4
stalwart-cli query Account

实测输出:

id                    type   name
--                    ----   ----
b                     domain lab.example.com
c                     person demo

四、部署结果验证

配置与账号就绪后,做一轮真实邮件收发闭环验证,这是判断"装好了"的金标准。

4.1 SMTP 25 投递测试

安装 swaks(SMTP 瑞士军刀)后向 demo 邮箱投递一封邮件。注意 --helo 要给 FQDN(4.3 有反面实测):

apt-get install -y swaks
swaks --to demo@lab.example.com --from test@lab.example.com \
  --server 127.0.0.1:25 --helo mail.lab.example.com \
  --header "Subject: Install test 001" --body "Hello Stalwart."

实测输出(关键行节选):

 -> MAIL FROM:<test@lab.example.com>
 <-  250 2.1.0 Ok
 -> RCPT TO:<demo@lab.example.com>
 <-  250 2.1.5 Ok
 -> DATA
 <-  354 Go ahead
 -> .
 <-  250 2.0.0 Ok: queued as 8ZxdNQCRSzSF

250 2.0.0 Ok: queued 即投递成功。顺手验证 DKIM 自动签名已生效——直连 SMTP 会话查看这封信的投递结果头:

swaks --to demo@lab.example.com --from test@lab.example.com \
  --server 127.0.0.1:25 --helo mail.lab.example.com --pipe 'true' >/dev/null 2>&1

收信验证见 4.3,届时从信头可见两条 DKIM 签名(Ed25519 + RSA)与垃圾评分头。

4.2 Submission 587 认证发送

587 端口默认要求先 STARTTLS 再认证(实测明文直连时服务器不提供 AUTH 机制):

swaks --to demo@lab.example.com --from demo@lab.example.com \
  --server 127.0.0.1:587 --helo mail.lab.example.com \
  --auth LOGIN --auth-user demo@lab.example.com --auth-password q1w2e3r4 \
  --header "Subject: Submission test 002" --body "Sent via 587." 2>&1 | tail -5

明文直连(不 STARTTLS)时的实测输出(对比用):

 <-> 250 2.1.0 Ok
** No auth mechanisms were offered by the server.

加 -tls(STARTTLS)后实测输出:

 -> STARTTLS
 <-  220 2.0.0 Ready to start TLS
 -> AUTH LOGIN
 -> ...
 <-  235 2.7.0 Authentication successful
 -> MAIL FROM:<demo@lab.example.com>
 <-  250 2.1.0 Ok
 -> DATA
 <-  250 2.0.0 Ok: queued as E4fDXrrGnwdd

235 Authentication successful + 250 queued,认证发送闭环通过。

4.3 IMAP 收信验证

curl 原生支持 IMAP,可直接查收。由于明文 143 默认强制 STARTTLS,用 993 端口的隐式 TLS 最直接:

curl -s -k --max-time 10 "imaps://127.0.0.1:993/INBOX;UID=1" \
  -u "demo@lab.example.com:q1w2e3r4" | grep -E "^(Subject|DKIM|X-Spam)"

实测输出:

Subject: Install test 001
DKIM-Signature: v=1; a=ed25519-sha256; d=lab.example.com; s=default_ed; ...
DKIM-Signature: v=1; a=rsa-sha256; d=lab.example.com; s=default; ...
X-Spam-Status: No, score=0.40
X-Spam-Bar: +
X-Spam-Report: Spam detection software ... installed (spam score threshold) ...

第二封(587 认证发送的):

curl -s -k --max-time 10 "imaps://127.0.0.1:993/INBOX;UID=2" \
  -u "demo@lab.example.com:q1w2e3r4" | grep -E "^Subject"

实测输出:

Subject: Submission test 002

两封信都在,DKIM 双签名与内置反垃圾评分头正常生成。再看 JMAP 会话端点:

curl -s "http://127.0.0.1:8080/jmap/session" -u "demo@lab.example.com:q1w2e3r4" | python3 -c "
import json,sys
d=json.load(sys.stdin)
print('username:', d.get('username'))
print('primaryAccounts:', d.get('primaryAccounts'))
print('apiUrl:', d.get('apiUrl'))
print('capabilities:', list(d.get('capabilities',{}).keys()))
"

实测输出:

username: demo@lab.example.com
primaryAccounts: {'urn:ietf:params:jmap:core': 'c', 'urn:ietf:params:jmap:mail': 'c', 'urn:ietf:params:jmap:submission': 'c', 'urn:ietf:params:jmap:principals': 'c'}
apiUrl: http://127.0.0.1:8080/jmap
capabilities: ['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:mail', 'urn:ietf:params:jmap:submission', 'urn:ietf:params:jmap:quota', 'urn:ietf:params:jmap:vacationresponse', 'urn:ietf:params:jmap:contacts', 'urn:ietf:params:jmap:calendars', 'urn:ietf:params:jmap:principals', 'urn:ietf:params:jmap:files', 'urn:ietf:params:jmap:teams', 'urn:stalwart:jmap']

再用 JMAP 查询一次 INBOX(accountId 即 3.8 创建账号返回的 c):

curl -s http://127.0.0.1:8080/jmap -u "demo@lab.example.com:q1w2e3r4" \
  -H "Content-Type: application/json" \
  -d '{"using":["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],"methodCalls":[["Email/query",{"accountId":"c","filter":{"inMailbox":"a"},"limit":10},"q1"],["Email/get",{"accountId":"c","#ids":{"resultOf":"q1","name":"Email/query","path":"/ids"},"properties":["subject","receivedAt"]},"q2"]]}' | python3 -m json.tool | head -30

实测输出(两封测试邮件都在 JMAP 视图里):

{
    "methodResponses": [
        ["Email/query", {"accountId": "c", "ids": ["eaaaaab", "iaaaaac"]}, "q1"],
        ["Email/get", {"accountId": "c", "list": [
            {"subject": "Install test 001", "receivedAt": "2026-10-18T09:20:29Z", "id": "eaaaaab"},
            {"subject": "Submission test 002", "receivedAt": "2026-10-18T09:20:29Z", "id": "iaaaaac"}
        ], "notFound": []}, "q2"]
    ],
    "sessionState": "70423772"
}

#ids 反引引用是 JMAP 的链式调用语法:resultOf 指向前序调用、name 指向方法名、path 从前序响应中取 ids,比 IMAP 多轮往返高效得多。

4.4 DNS 记录准备(公网收发前置)

测试环境能在本机闭环,但要让公网邮件正常收发,lab.example.com 需要在 DNS 配好以下记录。Stalwart 已把本域应发布的全部记录生成好,直接从管理 API 导出 zone 文件:

curl -s http://127.0.0.1:8080/jmap -u "admin@lab.example.com:q1w2e3r4" \
  -H "Content-Type: application/json" \
  -d '{"using":["urn:ietf:params:jmap:core","urn:stalwart:jmap"],"methodCalls":[["x:Domain/query",{"filter":null},"c0"],["x:Domain/get",{"#ids":{"resultOf":"c0","name":"x:Domain/query","path":"/ids"},"properties":null},"c1"]]}' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['methodResponses'][1][1]['list'][0]['dnsZoneFile'])"

实测输出:

$ORIGIN lab.example.com.
$TTL 3600
mail 300 IN A 192.168.1.166
lab.example.com. 300 IN MX 10 mail.lab.example.com.
lab.example.com. 300 IN TXT "v=spf1 mx -all"
default._domainkey 300 IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkq...IDAQAB"
default_ed._domainkey 300 IN TXT "v=DKIM1; k=ed25519; p=fRn4jdSA...9dXD"
_dmarc 300 IN TXT "v=DMARC1; p=none; aspf=s; adkim=s; pct=100; rua=mailto:postmaster@lab.example.com"
autoconfig 300 IN CNAME mail.lab.example.com.
_autodiscover._tcp 300 IN SRV 0 1 443 mail.lab.example.com.
_imap._tcp 300 IN SRV 0 1 143 mail.lab.example.com.
_imaps._tcp 300 IN SRV 0 1 993 mail.lab.example.com.
_submission._tcp 300 IN SRV 0 1 587 mail.lab.example.com.
_smtps._tcp 300 IN SRV 0 1 465 mail.lab.example.com.
_pop3s._tcp 300 IN SRV 0 1 995 mail.lab.example.com.

说明:A 记录地址取自服务器实际 IP;SPF(v=spf1 mx -all)、两条 DKIM 公钥(default/RSA 与 default_ed/Ed25519)、DMARC 策略均已按最佳实践预生成,发布到 DNS 即可。这些记录的生效需要域名 DNS 配置权限,本文不伪造验证结果。

五、常见问题

安装初始化阶段踩到的真实问题与定位过程,按出现顺序整理。

5.1 端口被占用导致监听失败

现象:日志出现 Address already in use 或某端口不在 ss -ltn 输出中。

排查与处理:

ss -ltnp | grep -E ':(25|587|143|8080)\b'

若输出显示 exim4、sendmail 等进程或其他平台级转发进程占用,停用对应服务(systemctl disable --now <服务名>)。若采用脚本安装,Conflicts= 已自动处理 postfix/sendmail/exim4;其他平台级的端口转发规则不归 systemd 管,需自行调整映射或端口。

5.2 EHLO 主机名不是 FQDN 被拒

现象:swaks 不带 --helo 时 SMTP 会话被拒。实测输出:

 -> HELO 462d262f33fa
** 501 5.1.3 Bad EHLO argument.

原因:v0.16 默认策略拒绝非 FQDN 的 HELO/EHLO 参数(反垃圾基本功)。处理:客户端显式带上 FQDN(--helo mail.lab.example.com),或把发送方主机的主机名正确设置为 FQDN。

5.3 587/143 明文连上后没有 AUTH 机制

现象:AUTH LOGIN 前未 STARTTLS 时报 530 5.7.11 Must issue a STARTTLS command first 或 swaks 提示 No auth mechanisms were offered by the server.。

原因:默认安全策略下认证凭据不允许在明文链路上传输。处理:客户端一律先 STARTTLS(swaks 加 -tls)或直接改用 465/993 隐式 TLS。这是期望行为,不是故障。

5.4 手动安装时向导提交报 Permission denied

现象:x:Bootstrap/set 响应正常,但日志出现写配置失败:

⚠️ Error writing to /etc/stalwart/config.json: Permission denied (os error 13)

原因:向导完成后 Stalwart 要把系统设置写回配置文件,而手动安装时 /etc/stalwart 若完全归 root,服务用户(stalwart)没有目录写权限。处理:

chown root:stalwart /etc/stalwart && chmod 0750 /etc/stalwart

然后重新提交向导即可。另注意:若同机残留旧 stalwart 进程持有 RocksDB 锁,会报 While lock file: /var/lib/stalwart//LOCK: Resource temporarily unavailable,重启服务器或杀干净旧进程后再初始化。

5.5 密码策略拒绝弱口令

现象:修改管理员密码为简单口令时返回 invalidProperties: secret is too weak(实测回显见 2.6 节)。

原因:v0.16 默认 passwordMinStrength 为 "low",基于强度评分拒绝弱口令。处理:生产环境建议保留默认策略、使用强口令;测试环境可按 2.6 节先调策略为 "zero",注意策略属于受保护配置,改动后需重启服务才能在运行时生效。

5.6 WebAdmin 页面打开空白/资源加载失败

现象:/admin 返回 302 到登录页但页面无样式,或日志出现:

WARN Failed to fetch webui bundle: transport error

原因:WebAdmin 前端资源包在首次访问时从远程拉取,服务器无法直连 GitHub 时会失败。处理:为服务配置代理(在 /etc/stalwart/stalwart.env 中加入 https_proxy=http://<代理地址>:<端口> 后重启服务),或离线环境手动放置资源包。实测配置代理重启后,资源下载成功且 /admin 正常返回登录页。

六、卸载与清理

无论是方案一的脚本安装还是方案二的手动安装,Stalwart 在系统上留下的痕迹都集中在五处:systemd unit 文件(/etc/systemd/system/stalwart.service)、二进制(/usr/local/bin/stalwart)、配置目录(/etc/stalwart)、数据目录(/var/lib/stalwart)、日志目录(/var/log/stalwart),外加安装脚本创建的系统用户 stalwart。以下卸载序列已在 Ubuntu 22.04 实测执行并逐条记录回显,按序执行即可把系统还原到安装前状态。

卸载前先确认服务当前状态与各文件位置,避免路径与本文不一致时误删:

systemctl is-active stalwart
systemctl cat stalwart | head -3
ls -la /usr/local/bin/ | grep stalwart
ls -d /etc/stalwart /var/lib/stalwart /var/log/stalwart

实测回显(卸载前基准):

active
# /etc/systemd/system/stalwart.service
[Unit]
Description=Stalwart
-rwxr-xr-x 1 root root 104827072 Oct 18 11:32 stalwart
/etc/stalwart
/var/lib/stalwart
/var/log/stalwart

第一步:停止服务并确认已停。

systemctl stop stalwart
systemctl is-active stalwart

实测回显:

inactive

第二步:取消开机自启(systemd 会移除 multi-user.target.wants 下的软链接)。

systemctl disable stalwart

实测回显:

Removed "/etc/systemd/system/multi-user.target.wants/stalwart.service".

第三步:删除 unit 文件并重载 systemd。reset-failed 清掉已停止服务在 systemd 里残留的失败状态记录:

rm -v /etc/systemd/system/stalwart.service
systemctl daemon-reload
systemctl reset-failed

实测回显:

removed '/etc/systemd/system/stalwart.service'

第四步:删除二进制与三个数据目录。⚠️ /var/lib/stalwart 是全部邮件数据的存储位置(RocksDB 数据落盘于此),若只是准备重装而保留数据,跳过这一条中删除 /etc/stalwart 与 /var/lib/stalwart 的部分即可:

rm -v /usr/local/bin/stalwart
rm -rf /etc/stalwart /var/lib/stalwart /var/log/stalwart
ls -d /etc/stalwart /var/lib/stalwart /var/log/stalwart

实测回显:

removed '/usr/local/bin/stalwart'
ls: cannot access '/etc/stalwart': No such file or directory
ls: cannot access '/var/lib/stalwart': No such file or directory
ls: cannot access '/var/log/stalwart': No such file or directory

第五步:删除安装脚本创建的系统用户 stalwart。脚本创建时家目录指向 /home/stalwart(实际不存在),userdel -r 会给出两条 home/mail 目录未找到的提示,属预期行为,用户本身已被移除:

userdel -r stalwart
grep stalwart /etc/passwd || echo "用户已移除"

实测回显:

userdel: stalwart mail spool (/var/mail/stalwart) not found
userdel: stalwart home directory (/home/stalwart) not found
用户已移除

最后整体验证:服务、命令、目录三类痕迹均应消失。

systemctl status stalwart
which stalwart

实测回显:

Unit stalwart.service could not be found.

💡 说明:方案二手动安装的卸载与此完全同构,差异只有两处——unit 文件路径以 3.5 节自己编写时落位的位置为准(如 /etc/systemd/system/stalwart.service),二进制路径以 3.2 节 install -m 0755 的目标为准;stalwart 系统用户若按方案二用 useradd --system 创建,删除方式相同。卸载不影响外部 DNS 记录与客户端配置,若域名后续不再提供邮件服务,记得同步清理 MX/SPF/DKIM/DMARC 记录,避免残留的 SPF 记录影响域名信誉。

七、参考链接