Ubuntu 22.04 安装 Stalwart v0.16.26 指南
Ubuntu 22.04 从零部署 Stalwart v0.16.26 全流程实测:官方安装脚本与 GitHub 二进制手动安装双方案、systemd 托管、初始化向导、587/143 监听器补建与 SMTP/IMAP/JMAP 全链路验证
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 为内嵌引擎(无需额外服务) |
path | RocksDB 数据目录(含邮件 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 记录影响域名信誉。
七、参考链接
- Stalwart 官方安装文档(install.sh 说明):https://docs.stalw.art/get-started/install
- GitHub Releases v0.16.26(二进制与校验文件下载):https://github.com/stalwartlabs/stalwart/releases/tag/v0.16.26
- 官方 systemd 配置参考:https://docs.stalw.art/get-started/configure
- stalwart-cli 发布页:https://github.com/stalwartlabs/cli/releases
- swaks 项目主页(SMTP 测试工具):http://www.jetmore.org/john/code/swaks/
- curl IMAP 用法参考:https://everything.curl.dev/protocols/imap
- JMAP 协议规范 RFC 8620(核心):https://datatracker.ietf.org/doc/html/rfc8620