Stalwart 单机部署随记

2025-12-01

Stalwart Mail & Collaboration Server. Secure, scalable and fluent in every protocol (IMAP, JMAP, SMTP, CalDAV, CardDAV, WebDAV).

最近给邮件服务从 Zoho 改为 Self-hosting,搜索过程中,Stalwart 这个使用 Rust 的软件越看越符合需求。R门!

主要看重两点,资源占用以及支持 JMAP,在本人的邮件服务器上,内存占用只有不到 200MB,并且支持 JMAP 意味着出站只需开放 25/80/443,传统的 465/587/993 在支持 JMAP 的客户端上都可以不需要。

DNS解析

提前解析部分域名,方便初次配置

首先是 Mail eXchanger,我这里以 mx.domain.org A 记录指向 1.2.4.8 ,且 1.2.4.8 的 rDNS 同样指向 mx.domain.org 为例 其次是 MX 解析,@domain.org MX 记录指向 mx.domain.org,优先级 10 最后是 Web Admin,这里使用 mail.domain.org A 记录指向 1.2.4.8,使用 CDN

这里我给出 Zonefile 作为该步参考(Stalwart也会有该文件)

;; A Records
mail.domain.org.	1	IN	A	1.2.4.8 ; cf_tags=cf-proxied:true
mx.domain.org.	1	IN	A	1.2.4.8 ; cf_tags=cf-proxied:false

;; CNAME Records
autoconfig.domain.org.	1	IN	CNAME	mail.domain.org. ; cf_tags=cf-proxied:true
autodiscover.domain.org.	1	IN	CNAME	mail.domain.org. ; cf_tags=cf-proxied:true
mta-sts.domain.org.	1	IN	CNAME	mail.domain.org. ; cf_tags=cf-proxied:true

;; MX Records
domain.org.	1	IN	MX	10 mx.domain.org.

;; SRV Records
_caldavs._tcp.domain.org.	1	IN	SRV	0 1 443 mail.domain.org.
_carddavs._tcp.domain.org.	1	IN	SRV	0 1 443 mail.domain.org.
_imaps._tcp.domain.org.	1	IN	SRV	0 1 993 mx.domain.org.
_submissions._tcp.domain.org.	1	IN	SRV	0 1 465 mx.domain.org.
_submission._tcp.domain.org.	1	IN	SRV	0 1 587 mx.domain.org.

安装 Stalwart

为了方便起见我选择直接使用 Docker 来进行安装,并使用 Caddy 来代理 webadmin, 使用 acme.sh 来统一管理证书

Stalwart:

 1services:
 2  stalwart:
 3    image: stalwartlabs/stalwart:latest-alpine
 4    container_name: stalwart
 5    restart: unless-stopped
 6    ports:
 7      - "25:25" # SMTP
 8      - "465:465" # SMTPS
 9      - "993:993" # IMAPS
10    volumes:
11      - /data/stalwart/data:/opt/stalwart
12      - /data/acme.sh/data:/certs:ro
13
14    networks:
15      - mailserver
16
17networks:
18  mailserver:
19    external: true

注意,我这里只对外映射了 25/465/993 三个端口,对于常规使用来说是完全足够的

Caddy:

 1services:
 2  caddy:
 3    image: caddy:2-alpine
 4    container_name: caddy
 5    restart: unless-stopped
 6
 7    ports:
 8      - "80:80"
 9      - "443:443"
10
11    volumes:
12      - /data/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
13      - /data/caddy/data:/data
14      - /data/caddy/config:/config
15      - /data/acme.sh/data:/certs:ro
16
17    networks:
18      - mailserver
19
20networks:
21  mailserver:
22    external: true

注意,Caddy 与 Stalwart 需要在同一个网络中

Caddyfile

mail.domain.org {
    reverse_proxy stalwart:8080 {
        header_up X-Forwarded-Proto {scheme}
        header_up X-Forwarded-For {remote}
        header_up X-Forwarded-Host {host}
        header_up Host {host}
    }

    tls /certs/domain.org_ecc/fullchain.cer /certs/domain.org_ecc/domain.org.key

    log {
        output file /data/log/mail.access.log
    }
}

mta-sts.domain.org, autoconfig.domain.org, autodiscover.domain.org {
    handle /.well-known/* {
        reverse_proxy stalwart:8080 {
            header_up X-Forwarded-Proto {scheme}
            header_up X-Forwarded-For {remote}
            header_up X-Forwarded-Host {host}
            header_up Host {host}
        }
    }

    handle {
        respond "Forbidden" 403
    }

    tls /certs/domain.org_ecc/fullchain.cer /certs/domain.org_ecc/domain.org.key

    log {
        output file /data/log/mail.access.log
    }
}

与官方示例不同,这里我只代理 http 流量,其余流量直接映射到 Stalwart 容器内

Stalwart 配置

准备完上述文件后就可以启动 Stalwart 和 Caddy 了

使用: docker logs stalwart 来获取管理员账户及密码

访问: https://mail.domain.org 来进行配置

基本信息

Setting->Server->
  Network->Hostname:
    mx.domain.org
  TLS->Certificates:
    Create certificate:
      Certificate Id: domain.org
      Certificate : %{file:/certs/domain.org/fullchain.cer}%
      Private Key: %{file:/certs/domain.org/domain.org.key}%
      启用 Default
Setting->HTTP->Setting:
  Base URL: 'https://mail.domain.org'
  启用 Obtain remote IP from Forwarded header
Setting->SMTP->
  Inbound->MTA-STS->MX Patterns:
    mx.domain.org
  Reports->Outbound->Default Domain:
    domain.org
Setting->Security-Allowed IPs:
  Create address:
    你的内网地址块

点击 Reload configuration 或重启容器加载配置

域名配置

Management->Directory->
  Domains:
    Create domain:
      Domian name: domain.org

现在就可以去添加对应的TXT记录:

;; TXT Records
dkime._domainkey.domain.org.	1	IN	TXT	"v=DKIM1; k=ed25519; h=sha256; p=xxx"
dkimr._domainkey.domain.org.	1	IN	TXT	"v=DKIM1; k=rsa; h=sha256; p=xxx"
_dmarc.domain.org.	1	IN	TXT	"v=DMARC1; p=reject; rua=mailto:[email protected]; ruf=mailto:[email protected]"
_mta-sts.domain.org.	1	IN	TXT	"v=STSv1; id=16426630999589944260"
mx.domain.org.	1	IN	TXT	"v=spf1 a ra=postmaster -all"
_smtp._tls.domain.org.	1	IN	TXT	"v=TLSRPTv1; rua=mailto:[email protected]"
domain.org.	1	IN	TXT	"v=spf1 mx ra=postmaster -all"

至于 TLSA 看个人需要添加

账户管理

转到 Management->Directory->Accounts

点击 Create Account 创建账户,需要注意的是,Stalwart 的 Login name 并不强制要求 @domain 结尾,但是如果你需要客户端自动发现功能,还是添加 @domain 为上。

对于 Catch-all ,Stalwart 的默认策略是发给 Aliases 中为 Domain-part 的用户(在本例中为:@domain.org),可自行前往 Setting->SMTP->Inbound->RCPT stage->Catch-all 修改策略。

账户基本信息设定完成之后,点击 Authentication 设置主密码、应用密码。

点击 Permissions 可以为用户添加不同的细分权限或角色,可以添加一个 [email protected] 用户并给予 admin 角色来代替默认的 admin 账户。

到此,便可以正常的使用支持 IMAP/SMTP 的客户端来收发邮件了,但是,在开头我就说过,使用 Stalwart 的一个原因是支持 JMAP,下面便是针对此方面的配置

JMAP 配置

Stalwart 的 JMAP 支持基本是开箱即用的,只是要做一些细节调整

OIDC

Setting->Authentication->OpenID Connect

默认的 Signature algorithm 为 HS256,改为 ES256或 RS256 皆可,这里使用 ES256

Signature Key 我们使用 openssl 来生成 prime256v1 ecdsa key:

1openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:prime256v1 -out ec_private_key_pkcs8.pem

OAuth

Setting->Authentication->OAuth

在 Dynamic Client Registration 中启用 Require client registration 选项

.well-known

虽然在上面部分中,已经使用 CNAME/SRV 配置了部分服务的自动发现服务,但是实际情况中还是一定程度依赖 .well-known

根据 RFC 8615 中的定义,domain.org/.well-known/* 下存放着特定用途的配置文件用于自动发现服务

而我们需要用到 openid-configuration/webfinger/jmap

如果你的主域名能直接 A 解析到 MX 主机上那是最好不过的,但是实际情况中,主域名基本都有别的业务

所以就需要根据实际情况来配置主域的.well-known了

我这里的选择是将所有对 domain.org/.well-known/* 的请求转发到 mail.domain.org/.well-known/*

当然,其实不做这一步也可以,手动填写服务 URL 也行

客户端

截至本文完结当天,也没有一个功能完备的 GUI 客户端可供选择

Webmail这里推荐:https://github.com/thunderbird/stormbox

Android: https://github.com/linagora/tmail-flutter https://codeberg.org/iNPUTmice/lttrs-android

结语

又是折腾的一天,笑

参考