Postfix + Dovecot 自建发信服务实战:SPF/DKIM/DMARC 配置、587 提交认证与进垃圾箱排查全流程

为什么要自己鼓捣发信服务

站点跑起来之后,总有一些邮件是非发不可的:Typecho 的评论回复提醒、新用户注册验证、忘记密码的重置链接、备份任务失败时的告警、给读者发的订阅推送。这些邮件量都不大,一天可能只有几十封,但一旦发不出去,用户体验就直接塌方。以前我图省事,注册了某个免费邮箱的 SMTP,配置完当天很爽,过了一个月开始出现「邮件发出去了但对方没收到」,再过两个月连发信配额都被扣光。

后来算了一笔账:一天几十封邮件,用商业 ESP 的免费额度其实够,但发送域名不是自己的,邮件里带着别人家的品牌名,还受制于对方的发送策略和审核。而自建 SMTP 的成本是「一台已有 VPS + 一个已经解析在自己手上的域名 + 一个自动证书」,边际成本几乎是零。所以我最后选了 Postfix 加 Dovecot 的经典组合:Postfix 负责 SMTP 投递,Dovecot 负责 IMAP 收信和 SASL 认证,两者配合可以给站点的 PHP 程序提供本地认证的提交端口,也让站长自己能用邮件客户端登录查看退信。

这篇文章不讲「一条命令装完收工」那种流程,因为真正的坑全部在装完之后:DNS 记录对不对、反向解析有没有、25 端口通不通、大厂为什么不收你的信、发出去进了垃圾箱该怎么查。下面按我实际踩过的顺序来写。

第一步:先把 DNS 的五条记录摆平

绝大多数「自建邮局发信进垃圾箱」的根因不在 Postfix 配置,而在 DNS。发信域名必须同时具备下面五类记录,缺一不可,而且互相之间要一致。

MX 记录指向收信主机,这一步决定别人往你的域名发信时投到哪。如果你的服务器只用来发信不收信,理论上可以不要 MX,但一般还是配上,方便自己接退信。

A 记录与 PTR 记录是最容易被忽略的一对。收信方(尤其是 Gmail、Outlook)会拿你连接的源 IP 做反向解析,得到一个主机名,再对这个主机名做正向解析,看能不能回到同一个 IP。这个叫「正反解析一致」。云服务商默认给你的 IP 反向解析往往是 xxx.static.cloud.example.net 这种名字,与你的域名毫无关系,Gmail 会直接判定为可疑来源。绝大多数云厂商在控制台里允许你提交 PTR 申请,填写 mail.你的域名,通常几分钟到几小时生效。

SPF 记录是一条 TXT 记录,声明「哪些发信主机可以使用我这个域名发信」。最简写法是 v=spf1 mx ~all,意思是只允许 MX 指向的主机,其余软拒绝。注意两个坑:一是整个域名只能有一条 SPF 记录,如果之前用过别家服务已经有一条,必须把 include 合并进去而不是再加一条,否则两条并存会被判为 SPF permerror,等于没有;二是 SPF 有十次 DNS 查询上限,include 嵌套太多会超限,报 SPF Permanent Error: too many DNS lookups。

DKIM 记录用私钥对每封邮件的头和正文做签名,收信方用 DNS 里的公钥验签。这一步是提升送达率最有效的单项措施。Postfix 配 DKIM 通常用 OpenDKIM 或者 Rspamd,我选 OpenDKIM,配置简单、日志清楚。生成密钥的流程是:

mkdir -p /etc/opendkim/keys/example.com
cd /etc/opendkim/keys/example.com
opendkim-genkey -b 2048 -d example.com -s mail -v
chown opendkim:opendkim mail.private
cat mail.txt

生成的 mail.txt 里就是需要发布的公钥,把它作为 mail._domainkey.example.com 的 TXT 记录发出去。要注意 DNS 面板通常要求你把那串 p= 的内容拆成多段引号,很多面板在粘贴带换行的长字符串时会截断,这是 DKIM 验证失败的头号原因。发布后用 dig 校验:

dig +short TXT mail._domainkey.example.com
dig +short TXT _dmarc.example.com

DMARC 记录告诉收信方「当 SPF 和 DKIM 都失败时你该怎么办」,并且要求对方把结果回报给你。起步阶段用 v=DMARC1; p=none; rua=mailto:dmarc@example.com,先收集两周报告,确认自己所有发信来源都被 SPF/DKIM 覆盖了,再逐步升级到 p=quarantine、p=reject。直接从 reject 开始会把还没配置好的业务邮件全部打掉。

第二步:Postfix 的最小可用配置

Debian 上用 apt install postfix 时会弹出一个交互式对话框,选「Internet Site」即可,系统主机名填 mail.example.com。装完之后不要直接去改 main.cf 的每一行,先把这几个核心参数确认清楚:

# /etc/postfix/main.cf 关键片段
myhostname = mail.example.com
mydomain = example.com
myorigin = $mydomain
inet_interfaces = all
mydestination = $myhostname, localhost.$mydomain, localhost
relayhost =
smtpd_tls_cert_file = /etc/letsencrypt/live/mail.example.com/fullchain.pem
smtpd_tls_key_file = /etc/letsencrypt/live/mail.example.com/privkey.pem
smtpd_tls_security_level = may
smtp_tls_security_level = may

这里有个非常关键的细节:mydestination 里绝对不能包含你的裸域名 example.com,除非你真的打算给每个用户名都开一个本地邮箱。一旦把裸域名放进去,Postfix 会认为「发往 @example.com 的邮件我自己就是终点」,于是本该投给正经邮箱服务商的邮件被本地退回,日志里出现 User unknown in local recipient table,而你会以为是对方的服务器拒收。

另一个坑是 myorigin。它决定了本地程序(比如 cron、PHP)发出邮件的默认发件域。如果没设,它默认取 myhostname,很多情况下会变成 mail 或者干脆是 VPS 的内部主机名,收信方看到 From: root@mail 这种不合法的发件域,直接 550 拒掉。所以一定要设成你的真实域名。

接下来配置提交通道。25 端口是服务器之间互相投递用的,不应该开放给认证用户;587 端口是提交端口,要求 TLS 加认证。在 /etc/postfix/master.cf 里把这两段前面的注释去掉:

submission inet n       -       y       -       -       smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_client_restrictions=permit_sasl_authenticated,reject

认证的后端交给 Dovecot,因为 Dovecot 已经管着系统用户或者虚拟用户的密码库,Postfix 只要通过 socket 问它就行:

# main.cf
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes
smtpd_recipient_restrictions = permit_sasl_authenticated, reject_unauth_destination

Dovecot 那边对应的配置在 /etc/dovecot/conf.d/10-master.conf:

service auth {
  unix_listener /var/spool/postfix/private/auth {
    mode = 0660
    user = postfix
    group = postfix
  }
}

这里路径必须是默认 chroot 之后的 /var/spool/postfix/private/auth,写成 /var/run/dovecot/auth 会连不上,而且 Postfix 的报错只是含糊的 SASL authentication failed,不告诉你 socket 找不到。排查时先 ls -l /var/spool/postfix/private/auth 看文件在不在、属主对不对,基本一眼就能看出问题。

第三步:让 PHP 程序用上这个提交通道

Typecho、WordPress 这些程序发信一般走 PHP 的 mail() 函数,也就是调用本机的 sendmail 兼容接口。最省事的做法是装 msmtp-mta,把 /usr/sbin/sendmail 接管过来,转发到本机 587 端口:

# /etc/msmtprc
defaults
auth           on
tls            on
tls_starttls   on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
logfile        /var/log/msmtp.log

account        local
host           127.0.0.1
port           587
from           noreply@example.com
user           noreply@example.com
password       ******

account default : local

然后把文件权限设成 chmod 600 /etc/msmtprc; chown www-data /etc/msmtprc。注意这里的用户必须和 PHP-FPM 运行用户一致,否则 PHP 读不到配置,邮件静默失败。测试可以用一条命令:

echo -e "Subject: test from server\nFrom: noreply@example.com\n\nhello" | sendmail -v you@gmail.com

-v 会打印整个 SMTP 对话过程,包括服务器的每一条响应码。这是排查发信问题最有效的工具,比翻任何日志都直接。看到 250 2.0.0 Ok: queued 说明本机这一侧没问题,问题在对面;看到 5xx 就要读具体原因。

第四步:为什么还是进了垃圾箱

DNS 全配好、认证也过了,最可能的结果依然是「发出去了,但进了对方的推广或垃圾邮件目录」。这时候要按下面几个方向挨个排除。

新 IP 没有信誉积累。一个刚开通的 VPS IP,在各大邮件服务商眼里就是「白纸」,甚至因为同网段邻居发过垃圾邮件而带着负分。这个没有捷径,只能靠时间和小量发送慢慢养。刚上线的头两周,每天把量控制在几十封以内,全部发给真实会打开邮件的人(比如自己的几个邮箱、真实订阅用户),不要一上来就群发。

发信 IP 落在公共黑名单里。先查一下:

# 把 1.2.3.4 换成你的发信 IP(倒序写进查询域名)
dig +short 4.3.2.1.zen.spamhaus.org
dig +short 4.3.2.1.bl.spamcop.net

有返回结果就说明被列了。Spamhaus 的 PBL(Policy Block List)最常命中云服务器,因为云厂商把整段 IP 标成「不应直接发信」。这种情况可以在 Spamhaus 官网提交移除申请,但同时要确认 PTR 已经改成自己的域名,否则移除后很快又被列回去。

内容本身触发了规则。短链接、大量图片、只有一个链接的正文、全大写的主题、Click here 这类措辞、Base64 编码的怪字符,都会被扣分。站点的通知邮件最稳妥的形态是纯文本加少量 HTML、一到两个指向自己域名的链接、正文里带上收件人能看到的具体内容(比如评论原文),而不是「您有一条新消息,点击查看」。

投诉率。这是最致命的指标。收信方会统计「用户把你的邮件标记为垃圾」的比例,超过千分之一就可能被降权。所以订阅邮件必须有一键退订,而且退订链接要真实可用。我给站点加的是一个带签名的退订 URL,点开直接生效,不需要登录,这比「回复 TD 退订」靠谱得多。

第五步:日志与排错的固定动作

Postfix 的日志在 /var/log/mail.log(Debian)或者 journald 里。排查时不要一行一行翻,直接用队列 ID 串起来看:

# 看某个收件人的完整投递轨迹
grep "to=<user@gmail.com>" /var/log/mail.log | tail -20

# 看队列里还积压了什么
postqueue -p

# 强制重投某一封
postqueue -f

# 删除某封卡住的任务
postsuper -d QUEUEID

常见响应码的含义值得背下来:421 是对方暂时不可用,稍后重试;450 是临时性灰名单,Postfix 会自动退避重试;550 5.7.1 是被策略拒收,通常和 IP 信誉或者内容有关;554 5.7.1 加 Message rejected 基本就是 Spamhaus 类的拦截。看懂这几个码,比看十篇「如何提高送达率」的文章有用。

另外强烈建议给队列长度加一个监控。Postfix 本身不会因为队列堆积而报警,一旦上游出了大问题(比如大规模退信),队列会在几小时内涨到几万封,最后把 /var/spool/postfix 所在的分区写满,连带着整台服务器的其他服务一起挂掉。一个 cron 一分钟跑一次的小脚本就够:

#!/bin/sh
n=$(postqueue -p | tail -1 | awk '{print $5}')
[ -z "$n" ] && n=0
if [ "$n" -gt 200 ]; then
  echo "postfix queue is $n" | mail -s "mail queue alert" admin@example.com
fi

收尾:这套方案的边界

自建 Postfix 在「一天几十到几百封、收件人主要是真实用户」这个量级上完全够用,成本只有 VPS 的固定支出。但它有明确的边界:发信量上到几千封之后,一个 IP 的信誉曲线会变得很难维持;需要精细的退信处理、A/B 测试、打开率统计时,自己写脚本维护的成本会超过商业服务。我的做法是通知类邮件全部自建,营销类邮件(如果有)交给专业 ESP,两套体系共用同一个发信域,用 SPF 的 include 把两边的发信源都授权进去,这样既保住了品牌一致性,也不用为了一个低频需求去对抗大厂的整套风控体系。

Last modification:October 18th, 2026 at 09:23 pm

Leave a Comment