自托管应用如何发送邮件:配置SMTP中继
无需运行邮件服务器,让自托管应用统一发送邮件:在主机配置一次SMTP中继,设置SPF、DKIM和DMARC,并用587或465端口解决VPS出站25端口被阻止导致的超时问题。
自托管应用需要发送邮件的内容
要让自托管应用发送邮件,您不需要邮件服务器。您需要的是一个中继:在主机上配置一次经过身份验证的 SMTP 账户,由这台主机上的所有应用将外发邮件交给它。运行邮箱才是难点,而且这是另一个问题。
接收邮件意味着在 port 25 上接受来自整个互联网的连接、过滤垃圾邮件、存储并备份邮箱,以及在服务器运行期间持续维护 IP 信誉。这项工作确实越来越难。发送邮件则包括密码重置、注册确认、“备份失败”警报和论坛回复通知。这些邮件内容简短、数量少,并且一次只发送一封。中继可以处理这些邮件,完成配置通常只需一个下午。
先确定您实际要解决的是哪一个问题。是否仍然值得运行自己的邮箱是一个确实需要回答的问题,而对大多数人来说,答案是否定的。如果您的答案是肯定的,在 VPS 上运行完整的 Mailcow 邮件服务器才是正确方案。另一部分是发送邮件,这是几乎所有人都需要、却几乎没人提前规划的功能。
先了解两个术语。SMTP(简单邮件传输协议)是这一流程各部分使用的协议。中继也称为 smarthost,是一个接受经过身份验证的邮件,并使用自身地址和信誉将邮件继续投递的服务器。
为什么您的 VPS 无法通过 25 端口发送邮件
几乎所有 VPS 提供商默认都会阻止出站 TCP 25 端口。邮件服务器使用 25 端口相互连接,因此,如果被入侵的 VPS 可以通过出站 25 端口发送流量,就能直接向所有接收邮件服务器发送垃圾邮件。提供商会丢弃这些数据包,而不是拒绝连接。因此,典型现象是连接长时间无响应,最后超时,而不是立即返回错误。
在服务器上执行测试:
sudo apt update && sudo apt install -y netcat-openbsd
nc -vz -w 5 gmail-smtp-in.l.google.com 25
nc -vz -w 5 smtp.relay.example 587如果第一个命令完整等待五秒,而第二个命令立即返回结果,则可以确认端口被阻止。有些提供商会在审核账户后解除限制。大多数提供商不会解除。
端口被阻止并不是使用中继的主要原因。即使 25 端口开放,直接从新 VPS 地址发送的邮件也可能进入垃圾邮件文件夹,或直接被拒收。原因是该地址没有发送历史,并且属于接收方视为托管空间的地址段。Google 的发件方指南要求发送 IP 配置有效的正向 DNS 和反向 DNS,而许多 VPS 地址使用您无法更改的通用 PTR(指针)记录。中继可以提供已有发送历史的地址。
提交端口可以解决这个问题。587 端口承载 STARTTLS,会先以明文建立会话,再升级为加密连接。465 端口承载隐式 TLS(传输层安全),会从第一个字节开始加密会话。两个端口都用于经过身份验证的客户端,VPS 网络通常都允许访问,并且您的中继至少支持其中一个。
选择中继服务和发信子域名
事务性邮件服务商有很多,它们执行的任务基本相同。请从以下4个方面评估:
- 提交端口为587或465,并支持 SMTP AUTH
- 使用您自己的域名和 selector 进行 DKIM 签名,而不只是使用服务商的域名和 selector
- 可以通过控制台或 webhook 查看退信和投诉数据
- 提供适合您发送量的套餐。截至2026年8月,仍有一些服务商每月免费提供几千封邮件;这些条款经常变化,因此应查看当前定价页面,不要依赖任何博客文章
使用子域名发送应用邮件。例如使用 notify.example.com,而不是 example.com。收件方会按域名评估发信信誉,因此应用发送异常不会直接影响您发送账单和团队邮件所使用的域名。请注意,这种隔离并不绝对:一些收件方会将子域名的信号汇总到组织域名,因此子域名只能减少影响,不能完全隔离影响。
为每个自托管应用配置一次中继
一种看似简单的做法是打开每个应用的设置页面,将 SMTP 主机、用户名和密码分别填入其中。Nextcloud、论坛、Grafana、Vaultwarden 和运行时间监控器都有这样的表单。这样做会让凭据以 6 种格式存放在 6 个位置,其中一些还存储在数据库中,而您备份数据库时通常将其作为数据而不是配置处理。轮换密码时,您需要更新其中 5 个位置。第 6 个位置会停止发送,而且通常不会明显提示,因为大多数应用只在服务器端记录 SMTP 错误,仍然向用户显示成功页面。
改为在主机上配置一次,然后让应用通过本地提交邮件。两种工具都适合这样做,选择哪一种取决于您是否需要队列。
msmtp 是一个兼容 sendmail 的客户端,不运行 daemon。它连接服务器、发送邮件,然后退出。它不提供队列,因此中继不可达时邮件会丢失,调用它的应用会收到非零退出状态。
配置为卫星模式的 Postfix 是完整的邮件传输代理,提供真正的队列。它会立即接受邮件,失败时重试数天,并将中继凭据保存在仅 root 可读的文件中。如果中继中断时不能丢失告警,或者多个应用以不同系统用户运行,请使用它。
msmtp:小型方案
sudo apt update
sudo apt install -y msmtp msmtp-mta ca-certificatesmsmtp-mta 会安装 /usr/sbin/sendmail 符号链接,因此任何调用 sendmail 的程序都会到达 msmtp,而无需知道它的存在。
写入 /etc/msmtprc:
defaults
auth on
tls on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
syslog on
account relay
host smtp.relay.example
port 587
from apps@notify.example.com
set_from_header on
user <relay username>
password <relay password>
account default : relayset_from_header on 始终设置 From 头并覆盖已有的 From 头,因此会用 from 中的地址替换应用生成的地址。未设置它时,cron 作业会以 root@your-hostname 作为发件人发送邮件,而中继会拒绝该地址,因为它不是您验证过的地址。syslog on 会通过 syslog 发送日志,因此您可以使用 journalctl -t msmtp 读取日志。也可以使用共享的 logfile 路径,但它需要为所有发送邮件的用户授予写权限,这在多用户主机上容易造成问题。
自行设置权限。对于每用户配置文件(~/.msmtprc),msmtp 会检查权限;如果文件权限为 contains secrets and therefore must have no more than user read/write permissions,它会拒绝运行。对于 /etc/msmtprc,它不执行权限检查,因为只要该文件可读,它就会直接加载。
sudo chown root:root /etc/msmtprc
sudo chmod 600 /etc/msmtprc
printf 'Subject: relay test\n\nsent from the host\n' | sudo sendmail -v you@example.com-v 会输出完整的 SMTP 会话,因此您可以看到中继返回的每条响应。发送成功时,最后会出现 250 响应,表示中继已接受邮件。出现 authentication failed 行表示用户名或密码错误,或者中继要求使用 API 密钥代替账户密码。
问题在于,正是这一点让许多人最终改用 Postfix。将文件权限设为 600 且所有者设为 root 后,只有 root 能发送邮件。以 www-data 运行的应用无法读取该文件,msmtp 会跳过它,应用则会因找不到默认账户而失败。解决方法是使用用户组:
sudo chgrp mail /etc/msmtprc
sudo chmod 640 /etc/msmtprc
sudo usermod -aG mail www-data需要明确这一设置的含义:mail 组中的每个成员都能读取中继密码,并从该主机以您的域名发送邮件。在由您独自管理的 VPS 上,这通常可以接受。如果多个由他人编写的应用以不同用户运行,则不应这样做。此时 Postfix 更合适,因为这些应用永远不会接触凭据。
Postfix 作为卫星
sudo debconf-set-selections <<'EOF'
postfix postfix/main_mailer_type select Satellite system
postfix postfix/mailname string notify.example.com
postfix postfix/relayhost string [smtp.relay.example]:587
EOF
sudo DEBIAN_FRONTEND=noninteractive apt install -y postfix libsasl2-moduleslibsasl2-modules 不是可选项。缺少它时,Postfix 会记录 warning: SASL authentication failure: No worthy mechs found,因为 PLAIN 和 LOGIN 机制库未安装在 /usr/lib/sasl2 下。
使用 postconf -e 设置其余配置。该命令会直接修改 /etc/postfix/main.cf:
sudo postconf -e 'relayhost = [smtp.relay.example]:587'
sudo postconf -e 'smtp_sasl_auth_enable = yes'
sudo postconf -e 'smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd'
sudo postconf -e 'smtp_sasl_security_options = noanonymous'
sudo postconf -e 'smtp_tls_security_level = encrypt'
sudo postconf -e 'smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt'
sudo postconf -e 'inet_interfaces = loopback-only'中继主机名两侧的方括号会阻止 Postfix 为该名称查询 MX 记录,并使其直接连接到该名称。有些中继主机会发布指向其他位置的 MX 记录。没有方括号时,邮件可能会沿这些记录发送到错误的服务器。
smtp_tls_security_level = encrypt 会强制使用 TLS,因此邮件不会以明文发送。但它不会验证证书。Postfix 文档对此有明确说明:在该级别,即使服务器证书不受信任或名称不匹配,仍会继续投递。如果需要检查证书,请使用 verify 或 secure,并保持 smtp_tls_CAfile 启用。
将凭据写入一个仅 root 可读的文件:
echo '[smtp.relay.example]:587 relay-username:relay-password' | sudo tee /etc/postfix/sasl_passwd
sudo chmod 600 /etc/postfix/sasl_passwd
sudo postmap hash:/etc/postfix/sasl_passwd
sudo systemctl restart postfixpostmap 会生成 Postfix 实际读取的索引副本。之后如果编辑文本文件却忘记运行 postmap,Postfix 会继续使用旧数据库,日志中也不会提示这一点。在 Postfix 3.9 及更高版本中,默认映射类型是 lmdb。如果您希望使用该类型,请在参数和 postmap 参数中都写入 lmdb:。在两行中同时指定类型,才能确保二者保持一致。
应用仍然将邮件发往 root@hostname。重写发件人:
echo '/.+/ apps@notify.example.com' | sudo tee /etc/postfix/sender_canonical
sudo postconf -e 'sender_canonical_classes = envelope_sender, header_sender'
sudo postconf -e 'sender_canonical_maps = regexp:/etc/postfix/sender_canonical'
sudo systemctl reload postfixregexp: 表会被直接读取,因此不需要 postmap。现在每封邮件都会使用相同的信封发件人和 From 头发出,这正是中继所要求的。代价是所有回复都会到达同一个位置。因此,如果回复应送达某位人员,请在每个应用中设置 Reply-To 头。
发送测试邮件并读取日志:
printf 'Subject: postfix relay test\n\nsent through the queue\n' | sendmail you@example.com
sudo tail -n 20 /var/log/mail.log成功投递的邮件会记录 status=sent,后面括号中是中继返回的响应。其他内容会说明具体原因。status=deferred 和 Connection timed out 一起出现时,表示仍有某个配置指向端口 25。Host or domain name not found. Name service error for name=smtp.relay.example type=A 表示中继主机名错误,或该主机上的 DNS 工作异常。mailq 会列出当前卡住的邮件,sudo postqueue -f 会立即重试这些邮件。
从 Docker 容器访问主机中继
容器无法调用主机上的 sendmail,因为该二进制文件不在镜像中,队列也未共享。应为容器提供网络目标。Postfix 可以监听 Docker 网桥地址。
ip -4 addr show docker0
sudo postconf -e 'inet_interfaces = 127.0.0.1, 172.17.0.1'
sudo postconf -e 'mynetworks = 127.0.0.0/8 172.17.0.0/16'
sudo systemctl restart postfix请从第一条命令的输出中读取实际网桥地址,不要直接复制此处的地址,因为 Compose 项目会在其他子网中创建自己的网络,而 docker network inspect <name> 会显示该地址。此处应使用 restart,不要使用 reload:Postfix 文档说明,修改 inet_interfaces 后必须停止并重新启动服务,重新加载不会应用此更改。随后,每个应用将 SMTP 主机设置为 172.17.0.1,端口设置为 25,不启用身份验证和 TLS,因为这些流量不会离开主机。如果服务位于 Compose 网络中,请参阅在 VPS 上运行 Docker Compose,了解该子网的来源。
这一步可能造成严重问题。监听公网地址且 mynetworks 范围过宽的 Postfix 会成为开放中继:陌生人会通过你的中继账户发送邮件,服务提供商会暂停该账户,你的域名信誉也会在数月内受损。每次修改后都要检查两端。
ss -tlnp | grep ':25'输出中只能显示环回地址和网桥地址。从另一台计算机执行 nc -vz your.server.ip 25 时,必须失败。
发送域的 SPF、DKIM 和 DMARC
在首次正式发送邮件前发布全部 3 条记录。它们免费,属于 DNS 记录,也是收件方首先检查的内容。
SPF(发件人策略框架)列出哪些服务器可以将您的域名放入信封发件人地址。请在发送子域上发布它:
notify.example.com. IN TXT "v=spf1 include:_spf.relay.example -all"从中继服务自己的设置页面复制 include: 的值,因为无法解析的 include 会导致永久性错误,而不是通过。SPF 评估最多处理 10 个会触发 DNS 查询的机制,超过后返回 permerror。收件方会将其视为失败,因此应尽量减少 include。每个名称只能准确发布 1 条 v=spf1 记录:发布 2 条同类记录同样会导致 permerror。
DKIM(域密钥识别邮件)使用中继服务持有的私钥为每封邮件签名,收件方则从 DNS 获取匹配的公钥。您的中继服务会提供一个选择器,以及一条 TXT 记录或一条 CNAME 记录供您发布:
sel1._domainkey.notify.example.com. IN CNAME sel1.dkim.relay.example.DKIM 比 SPF 更重要,因为 DKIM 可以在转发后继续有效。当邮件列表或 .forward 规则转发您的邮件时,收件方看到的来源 IP 地址会变成转发方的 IP 地址,因此 SPF 会失败,而签名仍可验证。
DMARC(基于域的邮件身份验证、报告和一致性)规定当两项检查都不匹配时收件方应如何处理,并要求收件方返回报告。请在组织域上发布它:
_dmarc.example.com. IN TXT "v=DMARC1; p=none; rua=mailto:dmarc@example.com; adkim=r; aspf=r"从 p=none 开始,并查看 2 周的报告。p=none 不会改变邮件投递行为,只会启用报告功能。您可以借此发现遗忘的、仍在使用您的域名发送邮件的系统。然后改为 p=quarantine,最后改为 p=reject。在第 1 天就发布 p=reject,可能会让您通过一位未收到发票的客户发现:您的开票系统一直在使用该域名发送邮件。
请检查公网实际看到的内容,而不是 DNS 控制面板显示的内容:
dig +short TXT notify.example.com
dig +short TXT sel1._domainkey.notify.example.com
dig +short TXT _dmarc.example.com输出为空表示记录尚未传播,或名称不正确。您在 5 分钟前修复的记录,可能会因缓存而继续保持错误状态,直到其原有 TTL(生存时间)到期。因此,请先检查 TTL,再下结论。
保持 From 与 Return-Path 对齐
每封邮件都包含两个发件人地址,检查方式也不同。信封发件人通过 SMTP MAIL FROM 命令提供,并在投递后的邮件中显示为 Return-Path。邮件头 From 是收件人看到的地址。
SPF 会将信封发件人所在域与连接 IP 地址进行校验。DKIM 会在 d= 中报告签名所用的域。只有当这两个域中至少有一个与邮件头 From 中的域对齐时,DMARC 才会通过。在宽松对齐模式(adkim=r、aspf=r,默认模式)下,子域也算作对齐,因此 notify.example.com 中的信封发件人域与 example.com 中的邮件头 From 域对齐。在严格对齐模式下则不对齐。
实际规则很简单:让邮件头 From 和信封发件人使用同一个域,这个问题就不会出现。这正是 msmtp 中的 set_from_header on 和 Postfix 中的 sender_canonical_maps 所执行的操作。
请查看已投递邮件中的验证结果。在 Gmail 中,“显示原始邮件”会显示收件方写入的邮件头:
Authentication-Results: mx.google.com;
dkim=pass header.i=@notify.example.com;
spf=pass (google.com: domain of apps@notify.example.com designates 198.51.100.25 as permitted sender) smtp.mailfrom=apps@notify.example.com;
dmarc=pass (p=NONE sp=NONE dis=NONE) header.from=example.com这里的 3 项检查全部通过。如果结果不同,通常会指出失败的检查及其原因。这是调试此类问题时最快获得的信息。
邮件量增加前:退信与投诉
退信是收件方拒绝您的邮件。硬退信是永久性失败,Gmail 将其表述为 550 5.1.1 The email account that you tried to reach does not exist。软退信是临时性失败,例如邮箱已满或启用灰度发布时返回的 4xx 状态码;中继服务器会自动重试。
中继服务器会统计您的硬退信率。如果您持续向不存在的地址发送邮件,服务商可能会暂停您的账户,因为这种模式看起来像是在使用购买的邮件列表。投诉更重要。投诉是收件人点击垃圾邮件按钮。Google 的发件人指南(截至 August 2026 检查)要求发件人在 Postmaster Tools 中报告的垃圾邮件率低于 0.30%,并建议保持在 0.10% 以下。
在邮件量增加前,应准备好以下 4 项:
- webhook,或每周查看一次中继服务器的抑制列表,以便发现退信
- 一个真实且有人查看的 From 地址,并将
Reply-To设置为回复地址 - 在将任何地址添加到任何列表前先完成确认,确保不会向地址所有者未提交的地址发送邮件
- 为任何会触发发信的表单设置速率限制
后两项最容易导致自托管应用出问题。未受保护的注册表单允许任何人输入他人的地址,您的服务器随后发送确认邮件,而该收件人可能会将其标记为垃圾邮件。阻止注册表单遭受订阅轰炸既是投递问题,也是滥用防护问题。
不要通过这条路径发送批量邮件。新闻稿需要邮件列表管理和退订标头,而事务性邮件通常不需要这些功能。因此,应在独立子域上运行自托管的 Listmonk 实例,并为其建立独立的发件人信誉。来自自托管论坛的通知邮件介于两者之间:格式上属于事务性邮件,数量上却接近批量邮件。它通常是最先暴露配置是否可靠的邮件类型。
作为参考,Gmail 的批量发件人规则适用于每天向 Gmail 地址发送超过 5,000 封邮件的发件人,并要求营销邮件配置 SPF、DKIM、DMARC 和一键退订。大多数自托管应用都达不到这一数量。无论是否达到该数量,所有发件人现在都应配置这些身份验证机制。
在信任它之前先进行测试
swaks 可用于执行此测试。它使用 SMTP,并输出完整的交互过程,因此您可以看到具体在哪一步失败。
sudo apt install -y swaks
swaks --to you@example.com --from apps@notify.example.com --server smtp.relay.example --port 587 --tls --auth-user '<relay username>' --auth-password '<relay password>'此命令会直接向中继服务器验证凭据。要测试应用实际使用的路径,请将目标改为主机上的中继:
swaks --to you@example.com --from apps@notify.example.com --server 127.0.0.1然后使用真实邮件从服务器端进行端到端检查。配置文件无法证明这些配置确实有效,因此请亲自执行测试:
- 发送邮件到 mail-tester.com 等评分服务。该服务会检查您的 SPF、DKIM、DMARC 和邮件内容,并说明评分原因
- 向用户实际使用的两个邮件服务商各发送一封邮件,然后在原始邮件中读取
Authentication-Results - 当对齐结果不明确时,将一封邮件提交到 learndmarc.com 进行分析
- 直接从应用触发发送,而不只是从命令行发送,因为应用负责设置 From 标头
最后说明一个需要注意的事实。即使新域名的三条记录都正确,邮件有时仍会进入垃圾邮件文件夹,因为该域名没有历史记录,接收方会谨慎对待上周才出现的域名。请先少量发送用户期待收到的邮件。域名信誉会逐步建立,任何配置都无法跳过这一过程。
FAQ
为什么我的 VPS 出站 TCP 端口 25 被阻止?
几乎所有服务商默认都会阻止出站 TCP 端口 25,因为服务器一旦被入侵且该端口开放,就可能直接向接收方邮件服务器发送垃圾邮件。数据包会被丢弃,而不是被拒绝,因此表现为连接一直等待后超时,而不是显示错误消息。可以在 nc -vz -w 5 smtp.relay.example 587 旁边运行 nc -vz -w 5 gmail-smtp-in.l.google.com 25 进行确认:前者会一直等待,后者会立即响应。解决方法不是请求解除阻止,而是通过 587 或 465 提交端口发送。这些端口通常保持开放,并且用于经过身份验证的客户端。
只发送少量应用通知,也需要 SPF、DKIM 和 DMARC 吗?
需要,发送量不会改变这一点。接收方对一封密码重置邮件和一次包含五万封邮件的活动执行相同的检查。如果没有 SPF 和 DKIM,邮件就未经过身份验证;Google 当前的发件人指南要求每个发件人至少配置其中一项。如果没有 DMARC,您不会收到报告,因此发现问题的第一个迹象可能是用户反馈没有收到重置链接。这三项都是 DNS 记录,不产生费用,发布它们大约需要十分钟。
应该使用 msmtp 还是 Postfix 作为中继客户端?
如果只有一个人管理服务器,并且中继故障时丢失邮件可以接受,请使用 msmtp。它只有一个配置文件,不运行守护进程;由于它不会排队,中继不可访问时邮件就会丢失。如果您需要持续数天重试的队列,或者有多个应用以不同的系统用户运行,请将 Postfix 用作卫星节点。Postfix 将中继密码保存在仅 root 可读的文件中,应用无法读取;而 msmtp 要求每个发送邮件的用户都能读取其配置文件。
为什么我的应用邮件因发件人是 root 而被拒收?
Cron 任务和许多应用会根据本地用户和主机名生成发件人地址,结果类似 root@srv1.localdomain。这不是您在中继服务器上验证过的地址,因此中继服务器会返回 553 或 554 响应,并指出该发件人地址,然后拒绝邮件。应在主机级别修复,而不是在每个应用中分别配置:在 /etc/msmtprc 中将 from 地址与 set_from_header on 配合使用,或在 Postfix 中使用 sender_canonical_maps 和 sender_canonical_classes = envelope_sender, header_sender。如果回复需要送达指定人员,请在每个应用中设置 Reply-To。
为应用邮件使用单独的子域,真的能保护主域吗?
部分可以,但仍然值得这样做。接收方会按域名跟踪信誉,因此针对 notify.example.com 的投诉大多会停留在 notify.example.com,主域仍可继续投递邮件。限制也很明确:部分接收方会将子域信号汇总到组织域名;如果您没有单独设置 sp=,发布在组织域名上的 DMARC 策略也会应用于子域。应将子域视为降低影响的措施,而不是绝对保证。