Maddy 0.9.6 配置参数详解
Maddy 0.9.6 配置参数逐节实测:maddy.conf 基本变量、TLS 证书、pass_table 认证、imapsql 存储选型、msgpipeline 局部投递与远端队列、SPF/DKIM/DMARC 检查启用;本地域名+dnsmasq 模拟环境跑通收发闭环与中继握手,附全量实测回显与报错实录
Maddy 0.9.6 配置参数详解
目标:逐节实测 Maddy Mail Server 0.9.6 默认配置文件 maddy.conf 的每个配置块——基本变量、TLS、认证、存储、路由管线、安全检查、出站队列,并在本地域名 + 本地 DNS 模拟环境中跑通"外部来信投递 → 认证提交 → DKIM 签名中继握手"完整闭环。本文所有命令、配置与回显均在 Ubuntu 22.04 + Maddy 0.9.6 实测通过。
一、环境信息
- 操作系统:Ubuntu 22.04(5.15 内核,systemd 249)
- Maddy 版本:0.9.6(
maddy version→0.9.6 linux/amd64 go1.27.1),安装与 systemd 托管见《Ubuntu 22.04 安装 Maddy 0.9.6 指南》 - 配置文件:
/etc/maddy/maddy.conf(官方 tarball 默认配置,4739 字节) - 数据目录:
/var/lib/maddy(state_dir,服务以 maddy 用户运行) - 测试域名约定:服务器
mail.lab.example.org、邮件域lab.example.org;模拟外部发件域client.mailtest.internal;模拟远端 MXrelayhost.mailtest.internal(127.0.0.2:25 起 maddy@relay 第二实例) - 测试账号:
admin@lab.example.org、demo@lab.example.org、relayuser@mailtest.internal,密码均q1w2e3r4 - 本地 DNS 模拟:dnsmasq 监听 127.0.0.1:53,用
mx-host/host-record/txt-record指令模拟 MX/SPF/DMARC/DKIM 记录(细节见 4.7 节) - 端口占用复核(动工前):
ss -ltn | grep -E ':(25|465|587|143|993)\b'
实测输出(本环境 25 端口已按测试约定改绑 127.0.0.1):
LISTEN 0 4096 127.0.0.1:25 0.0.0.0:*
LISTEN 0 4096 *:587 *:*
LISTEN 0 4096 *:993 *:*
LISTEN 0 4096 *:143 *:*
LISTEN 0 4096 *:465 *:*
二、前置条件
- Maddy 0.9.6 已安装并托管为 systemd 服务(《Ubuntu 22.04 安装 Maddy 0.9.6 指南》第八章之前的内容)
- 测试邮箱账号已开通(
maddy creds create+maddy imap-acct create,见安装指南 7.3 节) - 已安装测试工具:
apt-get install -y swaks dnsutils(swaks 为 SMTP 测试瑞士军刀,dig 用于核对 DNS 记录) - 配置修改流程约定:改
/etc/maddy/maddy.conf→maddy verify-config校验 →systemctl restart maddy(监听地址/模块实例变更需重启;仅改表文件/TLS 证书可systemctl reload maddy热载)
三、参数查看与修改方式
Maddy 配置是其自定义的声明式语法(官方语法手册 https://maddy.email/reference/config-syntax/ ),掌握三条规则即可读懂全部配置:
- 指令:
指令名 参数1 参数2(如hostname mail.lab.example.org); - 变量:
$(name) = value定义,$(name)引用(如$(primary_domain) = lab.example.org); - 块:
类型 名字 { 子指令/子块 }定义模块实例,&名字引用已定义实例(如auth &local_authdb)。
校验与生效:
maddy verify-config
systemctl restart maddy
实测输出(校验通过时的完整回显;表文件缺失类告警不影响启动):
table.file: ignoring non-existent file: /etc/maddy/aliases
No errors detected
⚠️ 注意:配置语法错误时 verify-config 直接报行号,实测少一个右括号:
Error: failed to read config /etc/maddy/maddy.conf: /etc/maddy/maddy.conf:182 - Error during parsing: unexpected EOF when looking for }行号
182即出错位置,从该行向前找未闭合的块。
四、参数总览与核心块详解
默认配置按功能分为四个大区(注释行 # ---- 分隔):Base variables(基本变量)、Local storage & authentication(本地存储与认证)、SMTP endpoints + message routing(SMTP 端点与路由)、IMAP endpoints。以下逐块实测。
4.1 基本变量与 TLS(Base variables)
默认配置头部:
$(hostname) = mail.lab.example.org
$(primary_domain) = lab.example.org
$(local_domains) = $(primary_domain)
tls file /etc/maddy/certs/$(hostname)/fullchain.pem /etc/maddy/certs/$(hostname)/privkey.pem
逐项说明:
| 参数 | 默认值 | 含义 | 改动影响 |
|---|---|---|---|
$(hostname) | example.org | SMTP EHLO 标识、证书查找键、MTA-STS 身份 | 必须与 MX 记录指向的主机名一致,否则对端 SPF/反垃圾校验吃亏 |
$(primary_domain) | example.org | 主邮件域,DKIM 默认签名域、DSN 地址域 | 决定 MAILER-DAEMON@$(primary_domain) 等系统地址 |
$(local_domains) | $(primary_domain) | 本地投递域列表 | 多域追加:$(local_domains) = $(primary_domain) other.example.com |
tls file <cert> <key> | certs 目录规则 | 全局证书 | 服务启动即校验可读,maddy 用户无权限会启动失败 |
多域名配置(官方 multiple-domains 教程)只需追加 $(local_domains) 并为每个域补 DKIM 签名参数,投递与检查逻辑自动覆盖新域。
TLS 证书三种获取路径实测要点:
# 路径一:自签(测试环境,见安装指南 2.3 节)
openssl req -newkey rsa:2048 -nodes -x509 -days 365 \
-keyout /etc/maddy/certs/mail.lab.example.org/privkey.pem \
-out /etc/maddy/certs/mail.lab.example.org/fullchain.pem -subj "/CN=mail.lab.example.org"
# 路径二:certbot(生产),软链 + ACL 授权
ln -s /etc/letsencrypt/live/mail.lab.example.org /etc/maddy/certs/mail.lab.example.org
setfacl -R -m u:maddy:rX /etc/letsencrypt/live /etc/letsencrypt/archive
# 路径三:acme.sh 直接安装到 certs 目录
mkdir -p /etc/maddy/certs/mail.lab.example.org
acme.sh --install-cert -d mail.lab.example.org \
--key-file /etc/maddy/certs/mail.lab.example.org/privkey.pem \
--fullchain-file /etc/maddy/certs/mail.lab.example.org/fullchain.pem
证书变更热载:Maddy 每分钟自动重读磁盘证书(官方教程原文),也可手动 systemctl reload maddy(unit 的 ExecReload=/bin/kill -USR2 $MAINPID)。实测 TLS 握手(587 端口 STARTTLS,swaks 输出节选):
=== TLS started with cipher TLSv1.3:TLS_AES_128_GCM_SHA256:128
=== TLS peer DN="/CN=mail.lab.example.org"
4.2 认证配置(auth.pass_table + sql_table)
默认认证块:
auth.pass_table local_authdb {
table sql_table {
driver sqlite3
dsn credentials.db
table_name passwords
}
}
逐项说明:auth.pass_table 定义名为 local_authdb 的认证模块实例,从"表"里查 bcrypt 哈希校验密码;table sql_table 把表落在 SQLite 库 credentials.db(相对路径基于 state_dir /var/lib/maddy)的 passwords 表。
💡 技术拓展:Maddy 认证后端选项——
模块 凭据来源 适用 pass_table + sql_table SQLite/PG/MySQL 表 默认,配 maddy creds 管理 pass_table + file 静态文件表 少量固定账号 auth.pam / auth.shadow 系统 PAM / /etc/shadow 复用系统用户 auth.ldap LDAP 目录 企业统一身份 auth.dovecot_sasl Dovecot SASL 过渡期共存 默认采用 pass_table + sql_table,凭据与存储账户分离但用户名保持一致。
账号管理命令实测(凭据与存储账户分别创建):
maddy creds create -p q1w2e3r4 admin@lab.example.org
maddy imap-acct create admin@lab.example.org
maddy creds list
实测输出:
admin@lab.example.org
demo@lab.example.org
口令哈希算法默认 bcrypt(creds create --hash 可选 sha256/bcrypt/argon2,--bcrypt-cost 默认 10),手工生成对照:
maddy hash -p q1w2e3r4
实测输出:
bcrypt:$2a$10$clP2S9jMP3voRjxZUOyol.YQ92sKjaH4gHHrI/S0DueT37
⚠️ 注意:只建凭据不建存储账户时,来信投递被拒(实测 RCPT 阶段):
501 5.1.1 User does not exist (msg ID = f9c508aa)。两个命令都要执行,用户名用完整邮箱地址。
4.3 存储配置(storage.imapsql 与 blob)
默认存储块:
storage.imapsql local_mailboxes {
driver sqlite3
dsn imapsql.db
}
逐项说明:storage.imapsql 把 IMAP 邮箱树/UID/标记等索引与元数据写入关系数据库;driver sqlite3 为默认驱动(可换 postgres/mysql,DSN 语法随驱动变化);消息本体(blob)默认存文件系统 messages/ 目录。
💡 技术拓展:存储驱动与 blob 后端选型——
组件 选项 适用 imapsql driver sqlite3(默认) 单机小规模,备份即拷文件 postgres 多实例共享、大规模 mysql 已有 MySQL 栈 blob 后端 fs(默认) 本地磁盘 s3 消息量大、对象存储可用 本文实测采用默认组合(sqlite3 + fs);官方 README 明确 IMAP 存储为 beta,重 IMAP 生产场景评估 Dovecot 系方案(见《Maddy 0.9.6 介绍与选型指南》)。
存储管理 CLI 实测:
maddy imap-acct list
maddy imap-mboxes list admin@lab.example.org
maddy imap-msgs list admin@lab.example.org INBOX
实测输出:
admin@lab.example.org
demo@lab.example.org
INBOX [\HasNoChildren]
Sent [\Sent \HasNoChildren]
Trash [\Trash \HasNoChildren]
Junk [\Junk \HasNoChildren]
Drafts [\Drafts \HasNoChildren]
Archive [\Archive \HasNoChildren]
UID 1: <news@client.mailtest.internal> - Clean inbox delivery
[\Recent], 2026-10-19 07:39:03 +0000 UTC
imap-acct create 自动创建的 6 个文件夹全部带 special-use 标记,客户端可直接映射为收件箱/已发送/废篓/垃圾/草稿/归档。
4.4 局部投递与改写(table.chain + msgpipeline)
默认的改写链与本地路由管线:
table.chain local_rewrites {
optional_step regexp "(.+)\+(.+)@(.+)" "$1@$3"
optional_step static {
entry postmaster postmaster@$(primary_domain)
}
optional_step file /etc/maddy/aliases
}
msgpipeline local_routing {
destination postmaster $(local_domains) {
modify {
replace_rcpt &local_rewrites
}
deliver_to &local_mailboxes
}
default_destination {
reject 550 5.1.1 "User doesn't exist"
}
}
逐项说明:table.chain 串起三个 optional_step——子址重写(user+tag@ 折叠为 user@)、postmaster 角色地址、别名文件;msgpipeline 按"目的域"匹配:命中本地域 → 收件人经改写链 → 投入 local_mailboxes 存储;未命中 → 550 拒绝。别名文件 /etc/maddy/aliases 每行 别名 目标,文件缺失时仅日志 table.file: ignoring non-existent file: /etc/maddy/aliases(实测),创建后 systemctl reload maddy 生效。
4.5 SMTP 端点(25 端口 MTA/MX + 检查链)
默认 25 端口块(本文测试环境将绑定改写为 127.0.0.1,生产保持 0.0.0.0):
smtp tcp://127.0.0.1:25 {
limits {
all rate 20 1s
all concurrency 10
}
dmarc yes
check {
require_mx_record
dkim
spf
}
source $(local_domains) {
reject 501 5.1.8 "Use Submission for outgoing SMTP"
}
default_source {
destination postmaster $(local_domains) {
deliver_to &local_routing
}
default_destination {
reject 550 5.1.1 "User doesn't exist"
}
}
}
逐项说明:limits 全局限流(每秒 20 封、并发 10 连接);dmarc yes 启用 DMARC 策略执行;check 块启用三项入站检查——require_mx_record(HELO 域须有 MX/A 解析)、dkim(验证签名)、spf(发件 IP 授权校验);source $(local_domains) 阻断"本域发件人走 25 端口"(防伪造,强制走认证提交);default_source 是外部来信的正常路由。
实测一:外部来信被 DMARC 拒绝。 用真实公网有 reject 策略的域作发件人:
swaks --to admin@lab.example.org --from extuser@client.example.net --server 127.0.0.1:25 --header "Subject: t" --body "x"
实测输出(节选):
-> MAIL FROM:<extuser@client.example.net>
<- 250 2.0.0 Roger, accepting mail from <extuser@client.example.net>
-> RCPT TO:<admin@lab.example.org>
<- 250 2.0.0 I'll make sure <admin@lab.example.org> gets this
-> .
<** 550 5.7.1 DMARC check failed (msg ID = 0a627cb1)
服务端日志(journalctl -u maddy 实测原文):
smtp: DATA error {"check":"dmarc","dkim_domain":"","dkim_res":"none","msg_id":"0a627cb1","reason":"No aligned identifiers","smtp_code":550,"smtp_enchcode":"5.7.1","smtp_msg":"DMARC check failed","spf_from":"client.example.net","spf_res":"none"}
根因:_dmarc.example.net 公网记录为 "v=DMARC1;p=reject;sp=reject;adkim=s;aspf=s"(dig 实查),发件无 DKIM 且 SPF 无记录 → 无对齐标识 → 按 reject 策略拒收。这证明 dmarc/check 链真实生效。
实测二:发件域无 MX 记录 → 隔离进 Junk。 发件域 client.mailtest.internal 未配 MX 时:
quarantined {"check":"require_mx_record","msg_id":"747152a4","reason":"no such host","smtp_code":550,"smtp_enchcode":"5.7.0","smtp_msg":"DNS error during policy check"}
smtp: accepted {"msg_id":"747152a4"}
消息被接受但落入收件人的 Junk 文件夹(IMAP LIST 实测 Junk -> 1 msgs)——quarantine 动作即"投进垃圾箱"而非拒收。
实测三:本域发件人走 25 端口被拒。
swaks --to admin@lab.example.org --from demo@lab.example.org --server 127.0.0.1:25 --header "Subject: spoof" --body "x"
实测输出(节选):
-> RCPT TO:<admin@lab.example.org>
<** 501 5.1.8 Use Submission for outgoing SMTP (msg ID = a1816d2a)
实测四:检查链通过后正常入站投递。 为模拟发件域补齐 MX/SPF/DMARC 记录后(见 4.7 节 dnsmasq 配置):
swaks --to admin@lab.example.org --from news@client.mailtest.internal --server 127.0.0.1:25 --header "Subject: Clean inbox delivery" --body "clean local delivery"
实测输出(节选):
<- 250 2.0.0 Roger, accepting mail from <news@client.mailtest.internal>
-> RCPT TO:<admin@lab.example.org>
<- 250 2.0.0 I'll make sure <admin@lab.example.org> gets this
-> .
<- 250 2.0.0 OK: queued
IMAP 侧确认到达(993 端口 Python imaplib 实测):
SELECT INBOX -> OK [b'1']
1 | Subject: Clean inbox delivery / From: news@client.mailtest.internal
25 端口会话能力(swaks 握手实测原文,EHLO 响应):
<- 220 mail.lab.example.org ESMTP Service Ready
<- 250-Hello mail.lab.example.org
<- 250-PIPELINING
<- 250-8BITMIME
<- 250-ENHANCEDSTATUSCODES
<- 250-CHUNKING
<- 250-STARTTLS
<- 250-SMTPUTF8
<- 250-SIZE 33554432
<- 250 LIMITS RCPTMAX=20000
4.6 Submission 端点与出站队列(465/587 + dkim 签名 + target.remote/queue)
默认提交与出站块:
submission tls://0.0.0.0:465 tcp://0.0.0.0:587 {
limits {
all rate 50 1s
}
auth &local_authdb
source $(local_domains) {
check {
authorize_sender {
prepare_email &local_rewrites
user_to_email identity
}
}
destination postmaster $(local_domains) {
deliver_to &local_routing
}
default_destination {
modify {
dkim $(primary_domain) $(local_domains) default
}
deliver_to &remote_queue
}
}
default_source {
reject 501 5.1.8 "Non-local sender domain"
}
}
target.remote outbound_delivery {
limits {
destination rate 20 1s
destination concurrency 10
}
mx_auth {
dane
mtasts {
cache fs
fs_dir mtasts_cache/
}
local_policy {
min_tls_level encrypted
min_mx_level none
}
}
}
target.queue remote_queue {
target &outbound_delivery
autogenerated_msg_domain $(primary_domain)
bounce {
destination postmaster $(local_domains) {
deliver_to &local_routing
}
default_destination {
reject 550 5.0.0 "Refusing to send DSNs to non-local addresses"
}
}
}
逐项说明:submission 同时监听 465(隐式 TLS)与 587(STARTTLS),强制 auth &local_authdb 认证;authorize_sender 校验认证账号与 From 一致(identity 即完全一致);发往本地域的照常入 local_routing,发往外部域的先 dkim $(primary_domain) ... default 签名再进 remote_queue;target.remote 负责按 DNS MX 发现对端并执行 mx_auth 安全策略(DANE + MTA-STS + 本地 TLS 下限 encrypted);target.queue 做磁盘队列与退信(DSN bounce 回本地投递)。
实测五:587 认证提交(STARTTLS + PLAIN)。 明文阶段 587 不提供 AUTH(仅 STARTTLS),TLS 后才出现 AUTH PLAIN 与 REQUIRETLS(swaks 实测 EHLO 响应节选):
<~ 250-AUTH PLAIN
<~ 250-SMTPUTF8
<~ 250-REQUIRETLS
swaks 用 --auth LOGIN 会因服务器只广告 PLAIN 而失败(实测 *** Auth not attempted, requested type not available),改用 PLAIN:
swaks --to relayuser@mailtest.internal --from admin@lab.example.org --server 127.0.0.1:587 \
--auth PLAIN --auth-user admin@lab.example.org --auth-password q1w2e3r4 -tls \
--header "Subject: Relay handshake test" --body "submission 587 outbound relay"
实测输出(节选):
<- 235 2.7.0 Authentication succeeded
-> To: relayuser@mailtest.internal
-> .
<~ 250 2.0.0 OK: queued
服务端出站投递日志(journalctl 实测原文):
submission: accepted {"msg_id":"7df9ed6e","sender":"admin@lab.example.org","src_ip":"127.0.0.1:43200","username":"admin@lab.example.org"}
TLS verify error, trying without authentication {"domain":"mailtest.internal","msg_id":"7df9ed6e-6ad5c975","reason":"tls: failed to verify certificate: x509: certificate relies on legacy Common Name field, use SANs instead","remote_server":"relayhost.mailtest.internal."}
delivered {"attempt":1,"msg_id":"7df9ed6e","rcpt":"relayuser@mailtest.internal"}
三行日志完整呈现出站链路:入队 → 对端证书校验(自签证书只有 CN 无 SAN 时的真实告警,降级后继续)→ delivered attempt:1 中继握手成功。生产证书应含 SAN,此告警即排障线索。
实测六:MX 缺失 → 永久失败与 DSN 退信。 收件域无 MX 时:
delivery attempt failed {"msg_id":"f8ffd613","rcpt":"relayuser@mailtest.internal","reason":"dns: rcode NXDOMAIN when looking up mailtest.internal.","smtp_code":554,"smtp_enchcode":"5.4.4","smtp_msg":"MX lookup error","target":"remote"}
not delivered, permanent error {"msg_id":"f8ffd613","rcpt":"relayuser@mailtest.internal"}
generated failed DSN {"dsn_id":"56f764a2","msg_id":"f8ffd613"}
DSN 自动生成并投回发件人信箱(admin INBOX 实测出现 Subject: Undelivered Mail Returned to Sender、From: MAILER-DAEMON@lab.example.org),对应配置即 bounce 块。
实测七:IMAP 端点(993/143)。 默认块:
imap tls://0.0.0.0:993 tcp://0.0.0.0:143 {
auth &local_authdb
storage &local_mailboxes
}
143 明文连接拒绝认证(实测 Python imaplib 报错原文):
imaplib.IMAP4.error: b'Authentication disabled'
993 TLS 登录读取正常(实测节选):
SELECT INBOX -> OK [b'1']
LIST:
(\HasNoChildren) "." INBOX
(\Sent \HasNoChildren) "." "Sent"
(\Trash \HasNoChildren) "." "Trash"
(\Junk \HasNoChildren) "." "Junk"
(\Drafts \HasNoChildren) "." "Drafts"
(\Archive \HasNoChildren) "." "Archive"
4.7 本地域名模拟环境(dnsmasq + 双实例,收发闭环基础设施)
本文收发闭环不依赖公网域名,用 dnsmasq 在本机模拟全部 DNS 记录(/etc/dnsmasq.d/lab.conf 实测配置,含注释):
# 本地收发模拟域(仅测试环境使用)
domain-needed
bogus-priv
server=8.8.8.8
server=1.1.1.1
# 第一实例主机名与模拟外部客户端域
host-record=mail.lab.example.org,127.0.0.1
host-record=client.mailtest.internal,127.0.0.1
mx-host=client.mailtest.internal,mail.lab.example.org,10
txt-record=client.mailtest.internal,"v=spf1 ip4:127.0.0.1 ~all"
txt-record=_dmarc.mailtest.internal,"v=DMARC1; p=none"
# 第一实例 DKIM 公钥(内容取自 dkim_keys/lab.example.org_default.dns)
txt-record=default._domainkey.lab.example.org,"v=DKIM1; k=rsa; p=MIIBIjANBgkq..."
# 第二实例(模拟远端 MX)
host-record=relayhost.mailtest.internal,127.0.0.2
mx-host=mailtest.internal,relayhost.mailtest.internal,10
mx-host=relay.mailtest.internal,relayhost.mailtest.internal,10
记录生效核对(dig 实测):
$ dig +short MX mailtest.internal
10 relayhost.mailtest.internal.
$ dig +short TXT client.mailtest.internal
"v=spf1 ip4:127.0.0.1 ~all"
$ dig +short TXT _dmarc.mailtest.internal
"v=DMARC1; p=none"
DKIM 公钥 TXT 的取值直接来自 Maddy 生成的 dkim_keys/lab.example.org_default.dns 文件(内容以 v=DKIM1; k=rsa; p=MIIB... 开头),发布到 default._domainkey.lab.example.org 后,接收方即可验证签名——这是 DKIM 出站签名闭环的 DNS 侧前提。
第二实例(maddy@relay,模拟远端 MX)配置 /etc/maddy/relay.conf 的关键差异:$(hostname) = relayhost.mailtest.internal、SMTP 监听 tcp://127.0.0.2:25、dmarc no(简化对端检查)、存储 DSN 用绝对路径 /var/lib/maddy-relay/imapsql.db。启动与验证:
systemctl start maddy@relay
ss -ltn | grep ':25'
实测输出:
LISTEN 0 4096 127.0.0.1:25 0.0.0.0:*
LISTEN 0 4096 127.0.0.2:25 0.0.0.0:*
至此闭环三要素齐备:本地实例(收+发+签)、dnsmasq(MX/SPF/DMARC/DKIM 记录)、远端实例(收+验)。实测五的中继握手 delivered attempt:1 即跑在这套环境上。
五、生产配置模板
以下模板面向「4GB 内存、40GB SSD、个人/小团队自托管、1 个邮件域、50 邮箱以内」场景,在默认配置基础上做最小必要修改(差异处以 # ← 改 标注):
$(hostname) = mail.example.com # ← 公网 MX 主机名
$(primary_domain) = example.com # ← 邮件域
$(local_domains) = $(primary_domain)
tls file /etc/maddy/certs/$(hostname)/fullchain.pem /etc/maddy/certs/$(hostname)/privkey.pem
# 证书用 Let's Encrypt:软链 /etc/letsencrypt/live/<主机名> 并 setfacl 授权 maddy 用户
auth.pass_table local_authdb {
table sql_table {
driver sqlite3
dsn credentials.db
table_name passwords
}
}
storage.imapsql local_mailboxes {
driver sqlite3
dsn imapsql.db
}
table.chain local_rewrites {
optional_step regexp "(.+)\+(.+)@(.+)" "$1@$3"
optional_step static {
entry postmaster postmaster@$(primary_domain)
}
optional_step file /etc/maddy/aliases
}
msgpipeline local_routing {
destination postmaster $(local_domains) {
modify {
replace_rcpt &local_rewrites
}
deliver_to &local_mailboxes
}
default_destination {
reject 550 5.1.1 "User doesn't exist"
}
}
smtp tcp://0.0.0.0:25 { # 生产保持对外监听
limits {
all rate 20 1s
all concurrency 10
}
dmarc yes
check {
require_mx_record
dkim
spf
}
source $(local_domains) {
reject 501 5.1.8 "Use Submission for outgoing SMTP"
}
default_source {
destination postmaster $(local_domains) {
deliver_to &local_routing
}
default_destination {
reject 550 5.1.1 "User doesn't exist"
}
}
}
submission tls://0.0.0.0:465 tcp://0.0.0.0:587 {
limits {
all rate 50 1s
}
auth &local_authdb
source $(local_domains) {
check {
authorize_sender {
prepare_email &local_rewrites
user_to_email identity
}
}
destination postmaster $(local_domains) {
deliver_to &local_routing
}
default_destination {
modify {
dkim $(primary_domain) $(local_domains) default
}
deliver_to &remote_queue
}
}
default_source {
reject 501 5.1.8 "Non-local sender domain"
}
}
target.remote outbound_delivery {
limits {
destination rate 20 1s
destination concurrency 10
}
mx_auth {
dane
mtasts {
cache fs
fs_dir mtasts_cache/
}
local_policy {
min_tls_level encrypted
min_mx_level none
}
}
}
target.queue remote_queue {
target &outbound_delivery
autogenerated_msg_domain $(primary_domain)
bounce {
destination postmaster $(local_domains) {
deliver_to &local_routing
}
default_destination {
reject 550 5.0.0 "Refusing to send DSNs to non-local addresses"
}
}
}
imap tls://0.0.0.0:993 tcp://0.0.0.0:143 {
auth &local_authdb
storage &local_mailboxes
}
配套的 DNS 侧清单(公网权威 DNS 上必须发布):A/AAAA(mail.example.com)、MX 10、SPF v=spf1 mx ~all、DMARC _dmarc、DKIM default._domainkey(取自 dkim_keys/example.com_default.dns)、可选 MTA-STS/TLSRPT。日常巡检与备份见《Maddy 0.9.6 日常运维手册》。
六、常见问题(FAQ)
Q1:verify-config 提示 unused configuration block xxx?
定义了模块实例却没有任何 &名字 引用。实测第二实例配置里定义了 relay_authdb 但 SMTP 块没写 auth &relay_authdb:Error: unused configuration block relay_authdb (auth.pass_table)。删除未用块或补上引用。
Q2:missing required directive: hostname?
msgpipeline 等部分块依赖全局 hostname 指令(或块内显式 hostname)。实测 relay.conf 在 pipeline 前漏写 hostname $(hostname) 触发该错,补上即通过。
Q3:第二实例报 attempt to write a readonly database?
storage.imapsql 的 DSN 指向的目录不是 maddy 用户属主(实测 /var/lib/maddy-relay 由 root 创建)。修复:chown -R maddy:maddy /var/lib/maddy-relay && systemctl restart maddy@relay。
Q4:证书报 permission denied?
maddy 永不以 root 运行,证书与 key 必须对 maddy 用户可读。自签场景 chown maddy:maddy;Let's Encrypt 场景用官方推荐的 setfacl -R -m u:maddy:rX /etc/letsencrypt/{live,archive}。
Q5:为什么 587 端口明文阶段看不到 AUTH?
Maddy 的 submission 端点把认证保护在 TLS 之后(明文 EHLO 只广告 STARTTLS),TLS 建立后才广告 AUTH PLAIN。客户端务必启用 STARTTLS;465 端口则全程隐式 TLS。
Q6:来信为什么进了 Junk 而不是被拒收?
check 块里 require_mx_record/spf 等检查未通过时的默认动作是 quarantine(隔离到收件人 Junk 文件夹,日志 quarantined {"check":...}),只有 DMARC 执行到 reject 策略才当场 550 拒收。两种行为在 4.5 节实测二、实测一中均有原文回显。
Q7:出站日志出现 x509: certificate relies on legacy Common Name field?
对端(或本端自签)证书只有 CN 没有 SAN 扩展,Go TLS 校验按现代标准告警并降级。生产证书(Let's Encrypt 等)天然含 SAN;自签证书生成时加 -addext "subjectAltName=DNS:mail.example.com" 可消除。
七、参考链接
- 配置语法参考:https://maddy.email/reference/config-syntax/
- SMTP 端点参考:https://maddy.email/reference/endpoints/smtp/
- IMAP 端点参考:https://maddy.email/reference/endpoints/imap/
- target.remote 参考:https://maddy.email/reference/targets/remote/
- 存储模块参考:https://maddy.email/reference/storage/imapsql/
- 官方多域名教程:https://maddy.email/multiple-domains/
- 本系列文档:《Maddy 0.9.6 介绍与选型指南》《Ubuntu 22.04 安装 Maddy 0.9.6 指南》《Maddy 0.9.6 日常运维手册》