如果你在一台老旧的Linux服务器上执行mail -s "hello" someone@example.com,然后看着邮件被投递出去,这背后的功臣很可能就是sendmail。作为Unix世界最古老的邮件传输代理(MTA)之一,sendmail已经默默跑了四十多年,很多企业内部的日志告警、系统备份通知、定时任务结果,都是靠它转发的。
我最早接触sendmail是在维护一台跑着CentOS 5的老机器时,当时什么都不懂,只知道邮件发不出去就去改/etc/mail/sendmail.mc,改完跑一遍m4,再重启服务。后来踩的坑多了,才慢慢搞明白它背后的队列机制、中继控制和日志体系。这篇文章就打算把我这些年在sendmail上积攒的经验完整盘一盘,从它到底是什么、怎么安装、核心配置怎么写,到实际运维中你会遇到的队列积压、退信、反解不匹配等问题,都给你讲透。
内容会比较长,但我保证每一段都是能直接落地的东西。适合几类人看:正在维护存量sendmail服务器的运维、想在低配VPS或老机器上自己搭一套邮件服务的爱好者,以及被Postfix文档逼疯想回头看看原始MTA的探索者。
1. 先弄清楚:sendmail到底是个什么东西
1.1 sendmail的基本身世
sendmail诞生于上世纪70年代末,作者是Eric Allman,最初是在delivermail基础上改进而来的。1983年它随BSD 4.1c一起发布,从此成为Unix世界的事实标准邮件传输代理。在很长一段时间里,几乎每台Unix服务器都能找到一个叫/usr/sbin/sendmail的二进制文件。
它负责的工作很简单:接收邮件、解析收件人、投递邮件。你可以把它理解成小区物业的收发室——住户把信投进去,收发室看一眼地址,如果是本小区的就送到对应的信箱,如果是外小区的就联系对方物业,联系不上的就暂存在柜子里过会儿再试,实在送不出去就退给寄件人。
这个比喻基本能概括sendmail的全部核心逻辑:接收、路由、队列、投递、退信。它不关心你用Outlook还是Foxmail,不关心邮件正文是HTML还是纯文本,更不关心是否带附件,它只做传输。
现在很多发行版已经把默认MTA换成了Postfix,但sendmail依然存在于大量生产环境里。尤其是嵌入式设备、老版本操作系统、内部报表系统、打印机和扫描仪的邮件推送功能,很多还在用sendmail或者兼容sendmail接口的程序。理解sendmail,与其说是学一个老古董,不如说是掌握一套邮件系统的基础框架。
1.2 一次邮件投递背后的完整链路
在配置之前,先得把sendmail的工作流程搞明白。完整链路大概是这样的:
- 用户在用MUA(邮件客户端)里点发送,MUA通过SMTP协议连接到服务器的25端口。
- sendmail接收邮件,检查发件人是否被允许发送,收件人是否本地用户,或者是不是要转发到其他域名。
- 邮件内容被写入队列目录,生成一对文件:qf开头的信封文件(记录发件人、收件人、时间戳、错误重试次数)和df开头的正文文件。
- 队列运行器(queue runner)周期性地把队列里的邮件捞出来处理。
- sendmail查询收件人域名的MX记录,找到目标邮件服务器。
- 建立TCP 25连接,开始SMTP会话,把邮件投递过去。
- 对方返回250表示收下,sendmail把队列里的邮件删除;对方返回临时错误则保留队列等待下次重试;永久错误则给发件人退回一封退信邮件。
这里有个关键点很多人容易忽略:sendmail并不是“收到邮件立刻发送”的,而是先落地到队列再异步处理。这个设计好处很大——即使目标服务器宕机、网络断开、DNS解析失败,邮件也不会丢,只是滞留在队列里,等恢复后自动补投。
1.3 和Postfix、Exim比,到底该选谁
不少新手会问:既然Postfix更现代,为什么还要折腾sendmail?我直接给一个对比表,你就能看明白。
| 对比维度 | sendmail | Postfix | Exim |
|---|---|---|---|
| 配置方式 | m4宏生成sendmail.cf | 多文件文本配置 | 单一文本配置 |
| 学习曲线 | 较陡 | 平缓 | 中等 |
| 性能 | 一般,单进程模型有瓶颈 | 多进程模块化,性能好 | 灵活,中小规模够用 |
| 安全性历史 | 早期漏洞较多,现在稳定 | 设计安全,漏洞少 | 出现过严重漏洞 |
| 适用场景 | 老系统、嵌入式、兼容需求 | 大多数新建邮件服务器 | Debian系默认、策略型部署 |
我的建议很明确:如果你是从零搭建一套全新邮件系统,Postfix是更省心的选择;但如果你要维护的是已有sendmail服务器,或者被要求兼容某些老系统,那本文这套知识就是刚需。而且不管你最后用哪个MTA,理解“队列机制、日志分析、中继控制、反转解析校验”这些底层逻辑都是通用的,学sendmail等于把基础打牢了。
2. 环境准备与安装
2.1 安装前的网络规划(hostname、DNS、反解)
很多人装完sendmail发现投递失败,第一反应是配置写错了,其实很多时候问题出在环境没准备好。邮件系统最敏感的三样东西:主机名、MX记录、PTR反解。
主机名必须是完整的FQDN。比如你的服务器叫mail.example.com,那就得把hostname设成mail.example.com,不能只叫mail或者localhost。sendmail启动时会检查主机名,如果无法解析成完整域名,它会拒绝启动或者以未修饰的主机名运行,导致HELO问候语不合法,被对方服务器拒收。
MX记录要提前规划好。比如你的域名是example.com,你要让发往user@example.com的邮件到达这台服务器,就得在DNS里添加一条MX记录指向mail.example.com,同时添加mail.example.com的A记录指向服务器IP。可以用dig +short MX example.com验证。
PTR反解是反垃圾邮件的重要检查项。很多大型邮箱服务商在收到邮件时会反查IP地址,如果发件服务器IP没有PTR记录,或者PTR记录指向的主机名和发件域名不匹配,邮件会被直接拒收或者丢进垃圾箱。这个在安装前就应该确认好,尤其是云服务器和IDC机房,一般是在管理面板里提交工单或自助配置。
检查命令很简单:
hostname -f dig +short MX example.com dig -x 203.0.113.10如果PTR结果解析出来的域名和你邮件服务器主机名一致,这一步就算合格了。
2.2 安装sendmail和相关组件
装sendmail不能只装一个sendmail二进制,还需要sendmail-cf(或者叫sendmail-devel)和m4。sendmail-cf里存放着生成配置文件所需的大量宏定义和feature模板,m4则是用来展开这些宏的工具。很多人在这一步只装了sendmail,结果改完sendmail.mc跑m4时报“找不到宏文件”,白白折腾半天。
Debian/Ubuntu系的安装命令:
apt-get update apt-get install -y sendmail sendmail-cf m4RHEL/CentOS系的安装命令:
yum install -y sendmail sendmail-cf m4装完以后有个容易踩的坑:某些发行版把sendmail这个命令做成了Postfix的兼容链接,也就是说你运行sendmail实际调用的是Postfix。检查方法很简单:
ls -l /usr/sbin/sendmail如果输出显示/usr/sbin/sendmail -> exim4或者/usr/sbin/sendmail -> postfix,说明这个系统默认用的是别的MTA,你需要先卸载或者停用默认MTA,再安装真正的sendmail。很多时候服务器上“邮件发不出去”就是这个原因——你以为在操作sendmail,实际是另一个程序在响应。
2.3 启动守护进程并验证
安装完成后,先把服务启动起来:
systemctl start sendmail systemctl enable sendmail然后确认端口监听是否正常:
ss -lntp | grep :25正常情况下能看到 sendmail 监听在25端口。如果没看到,基本是配置有问题,直接查日志:
tail -100 /var/log/maillog常见启动失败原因:主机名解析失败、25端口被占用、队列目录权限不对。队列目录默认是/var/spool/mqueue,权限必须是root所有且mode为700,这个经常被忽略。
3. sendmail的核心配置
3.1 配置哲学:m4宏 vs 直接改sendmail.cf
我第一次打开/etc/mail/sendmail.cf的时候整个人是懵的,几百行以R和S开头的火星文,比正则表达式还劝退。后来才明白,正常人不该直接改sendmail.cf,而是通过/etc/mail/sendmail.mc这个宏文件来配置。
m4宏系统是sendmail最独特的设计。简单说,sendmail.mc 是给人看的配置描述,一眼能看懂它要干什么;sendmail.cf 是给sendmail主程序解析的规则集,它包含了一整套复杂的邮件路由重写规则,这些规则对大多数场景完全相同,只是细节参数不同。m4的作用就是把我们的配置意图和系统默认规则模板合并,展开生成完整的sendmail.cf。
这样做的好处有两个。一是可读性:改一行宏定义,比手工在一堆规则里改参数要安全得多。二是可维护性:系统升级时sendmail的规则集可能变化,但你写的宏定义可以保持不变,重新生成一次cf文件就行。
生成配置的标准流程是:
cd /etc/mail cp sendmail.mc sendmail.mc.bak # 编辑 sendmail.mc m4 sendmail.mc > sendmail.cf这里必须提醒一句:在生成前一定备份原来的sendmail.cf,因为m4生成是全量覆盖。万一新配置有问题,至少能快速恢复。
还有一个更省事的办法:如果在Red Hat系发行版上,/etc/mail目录下自带Makefile,你改完sendmail.mc后直接执行make -C /etc/mail,它会自动重新生成sendmail.cf并重建aliases、access等数据库文件,比手动一步一敲命令省心得多。
3.2 sendmail.mc中必须理解的关键参数
一个最简可用的sendmail.mc大概长这样:
dnl 基础配置 define(`MYHOSTNAME', `mail.example.com') define(`SMART_HOST', `smtp.isp.com') dnl 监听设置:只监听内网地址 DAEMON_OPTIONS(`Family=inet, Name=MTA-v4, Port=smtp, Addr=127.0.0.1') dnl 启用访问控制数据库 FEATURE(`access_db') FEATURE(`mailertable') FEATURE(`virtusertable') dnl 定义本地投递和SMTP投递 MAILER(`local') MAILER(`smtp') MAILER(`procmail')逐个说参数的作用:
MYHOSTNAME指定这台邮件服务器的主机名。如果环境里没有显式定义,sendmail会通过系统hostname命令获取。显式写出来能避免DNS解析顺序带来的不一致。
DAEMON_OPTIONS控制监听地址和端口。如果不加限制,sendmail会监听所有网卡的25端口。对于只做内网邮件转发的服务器,强烈建议把监听地址限制在127.0.0.1或内网IP上,减少暴露面。修改后记得重新生成配置,否则邮件服务还是会接受外网连接。
SMART_HOST这个参数很实用,后面讲中继时会重点说,简单理解就是设置一个“把所有外发邮件统一转交给它”的智能主机。非常适合没有公网IP、运营商封了25端口的场景。
FEATURE是sendmail的插件系统开关。access_db启用访问控制数据库,virtusertable启用虚拟用户表。每开一个feature,m4就会把对应的一组规则模板织入生成的cf文件中。
有个细节经常坑新手:sendmail.mc 里的dnl是“删除到行尾”的意思,本质是注释。但dnl必须写在行首,而且有的版本要求后面跟一个空格,写成dnl。如果你把dnl写错位置,m4展开时会原样输出一行垃圾文本到sendmail.cf,导致服务启动失败。
3.3 aliases别名和virtusertable虚拟用户表
/etc/aliases是sendmail本地别名机制,作用是把发给某个系统用户的邮件转给另一个用户或者邮箱。系统里有个经典规则:
root: admin@example.com postmaster: root这样发给root的邮件会转到管理员真实邮箱。修改完aliases后必须运行newaliases生成数据库文件,否则不生效。这个步骤很多人忘,而且不是重启sendmail能解决的——重启服务不会自动重建别名库。
虚拟用户表virtusertable解决的是另一个问题:一台服务器上托管多个域名,每个域名想有自己独立的邮箱映射。例如:
webmaster@example.com localuser info@example.com admin @example.net catchall最后一行@example.net表示这个域名下所有找不到匹配用户的邮件统一收进 catchall 账户。虚拟用户表的配置同样不是直接读文本,需要生成哈希数据库:
makemap hash /etc/mail/virtusertable < /etc/mail/virtusertable这里有个经验之谈:生成完哈希文件后,检查一下文件权限,确保sendmail进程(通常以root身份运行)可读。很多诡异问题就是权限不对导致的,尤其是你从别的机器复制配置过来时。
4. 实操:从发信到日志排查
4.1 手工SMTP会话测试
配置好服务后,第一步不是急着用mail命令测试,而是用工具“裸聊”一次SMTP协议,这样可以清晰看到每一句交互和返回码。我用nc(netcat)演示一遍:
nc localhost 25连接成功后,进行手工会话:
220 mail.example.com ESMTP Sendmail EHLO localhost 250-mail.example.com Hello localhost [127.0.0.1] 250-AUTH LOGIN PLAIN 250-STARTTLS 250 HELP MAIL FROM:<test@example.com> 250 2.1.0 OK RCPT TO:<user@example.com> 250 2.1.5 OK DATA 354 Go ahead Subject: test message This is a test. . 250 2.0.0 OK QUIT 221 2.0.0 bye每一步的返回码都有含义:250表示命令接受,354表示可以输入邮件正文,221表示服务关闭连接。如果RCPT TO返回550,说明收件人被拒,可能是收件人不存在或者中继策略禁止。如果MAIL FROM返回550,多半是发件人被限制,要检查access_db配置。
很多人忽略了一个细节:握手时EHLO后面跟的主机名会被记录在日志里,而且部分反垃圾系统会校验这个名称是否和IP的PTR一致。测试时随便写个EHLO test,可能导致对方服务器拒收,但你自己还排查不出来。
4.2 观察日志判断投递结果
sendmail的所有动作都会写在日志里。Red Hat系是/var/log/maillog,Debian系是/var/log/mail.log。一条典型的外发日志长这样:
Jan 01 10:00:01 mail sendmail[12345]: 1ABC123DEF: from=<user@example.com>, size=1024, class=0, nrcpts=1, msgid=<202301011000.1ABC123DEF@mail.example.com>, proto=ESMTP Jan 01 10:00:02 mail sendmail[12346]: 1ABC123DEF: to=<target@gmail.com>, delay=00:01:00, xdelay=00:01:00, mailer=esmtp, relay=gmail-smtp-in.l.google.com. [142.250.1.27], dsn=2.0.0, stat=Sent看懂几个关键字段就行:
from=发件人to=收件人relay=实际连接的对方MX服务器地址dsn=传递状态,2.0.0表示成功stat=最终状态,Sent表示成功投递
如果stat=Deferred,说明邮件暂时没投出去,后面通常会跟着具体原因,比如Name server timeout(DNS超时)或者421 too many connections(对方限流)。看到stat=Bounce说明投递永久失败,已经生成了退信。
我排查问题的固定动作是先grep出一段时间内所有非Sent状态的行:
grep "sendmail.*stat=" /var/log/maillog | grep -v "stat=Sent"这样能快速把问题邮件集中捞出来,再逐条看失败原因。
4.3 队列管理与邮件滞留处理
sendmail的队列目录默认是/var/spool/mqueue。在Debian系,sendmail 8.12以后引入了clientmqueue机制,普通用户用命令行提交的邮件先进/var/spool/clientmqueue,再由专门的队列运行器转交主队列。用mailq看主队列,mailq -Ac看clientmqueue。
查看当前积压情况:
mailq输出里每一行代表一个队列条目,包含队列ID、邮件大小、发件人、收件人、停留时间和重试次数。如果积压多,先看停留时间——都超过几小时了,说明是持续投递失败,需要查原因,而不是继续等。
手动触发队列处理用:
sendmail -q这个命令会立即把所有队列里的邮件拿出来尝试投递一次。但我强烈不建议在对方服务器正在限流时反复手动flush,因为每一次尝试都会增加投递频率,很可能被对方反垃圾系统盯上,反而拖得更久。正确做法是让队列按周期自然运行,同时把日志里stat=Deferred的原因找出来解决掉,邮件自然会慢慢出去。
队列运行间隔可以在sendmail.mc里调整:
define(`confQUEUE_LA', `4') define(`QUEUEINTERVAL', `30m')QUEUEINTERVAL 30m表示每30分钟跑一轮队列处理。系统负载高于confQUEUE_LA设定值时sendmail会跳过这轮队列处理,以免影响正常业务。
5. 进阶:TLS加密与认证
5.1 为什么收件方要求加密
现在主流邮箱服务商已经在逐渐收紧明文邮件投递。Gmail、Outlook这些大服务在你连上他们的MX服务器时,一般会要求先用STARTTLS握手协商加密,然后再传输邮件内容。如果你的sendmail没配TLS,对于某些服务商,你的邮件会掉进“低信誉”档位,直接影响送达率。
要理解这个问题,可以想象你拿纸质信件到邮局,邮局工作人员要求你用信封密封再投递,而不是把内容都摊在桌面上。STARTTLS就是这个“密封”过程。不做这个动作,邮件正文在网络上就是明文传输的,这是一个非常严重的安全风险。
sendmail对TLS的支持已经相当成熟,只是默认配置没有启用。所以你必须显式把证书路径告诉它。
5.2 配置StartTLS的步骤
第一步,准备证书。如果有公网域名,直接用Let's Encrypt签免费证书最省事:
apt-get install -y certbot certbot certonly --standalone -d mail.example.com证书会生成在/etc/letsencrypt/live/mail.example.com/下。我们先建一个目录存放证书副本:
mkdir -p /etc/mail/certs cp /etc/letsencrypt/live/mail.example.com/fullchain.pem /etc/mail/certs/server.pem cp /etc/letsencrypt/live/mail.example.com/privkey.pem /etc/mail/certs/server_key.pem chmod 600 /etc/mail/certs/server_key.pem然后在sendmail.mc里加这些定义:
define(`confCACERT_PATH', `/etc/ssl/certs') define(`confCACERT', `/etc/ssl/certs/ca-certificates.crt') define(`confSERVER_CERT', `/etc/mail/certs/server.pem') define(`confSERVER_KEY', `/etc/mail/certs/server_key.pem')重新生成配置并重启服务后,用openssl s_client验证:
openssl s_client -starttls smtp -connect localhost:25如果输出最后能看到Verify return code: 0 (ok)或者握手成功打印会话证书,就说明TLS生效了。
证书续期是个大坑。Let's Encrypt证书有效期90天,到期的当天sendmail还能用旧证书,但之后对方服务器会发现证书过期直接拒绝。我的做法是写一个定时任务,每月跑一次certbot renew --force-renewal,然后自动把新证书复制到/etc/mail/certs并重启sendmail。
5.3 中继和Smart Host配置
中继(Relay)是sendmail里最容易出安全问题的地方。所谓开放中继,就是这台服务器无条件代发任意来源的邮件。垃圾邮件发送者最喜欢这种服务器,一旦被利用,你的IP会立刻进各种黑名单,整个企业邮件都会跟着遭殃。
sendmail默认只信任本机和中继配置表里允许的域名。看/etc/mail/access这个控制文件:
Connect:localhost.localdomain RELAY Connect:localhost RELAY Connect:127.0.0.1 RELAY Connect:192.168.10.0/24 RELAY To:spam@example.com REJECT这个文件的作用是定义谁能通过这台服务器发邮件,以及哪些收件地址要被拦截。修改后同样要执行makemap hash /etc/mail/access < /etc/mail/access生成哈希库。
在云服务器上,25端口出站经常被运营商封锁,但你仍然可以依赖Smart Host把邮件转发出去。比如通过企业的邮件网关或者第三方SES服务,在sendmail.mc里设置:
define(`SMART_HOST', `smtp.qq.com')这告诉sendmail:所有外发邮件不要直接去找收件人的MX,而是统一交给你指定的智能主机处理。这种方式也是规避封端口的最常用方案,而且对日志收口、统一审计也有帮助。
6. 常见问题与故障排查实录
6.1 拒收、退信、反解不匹配
最典型的故障就是:能从本机发给本机,但发到外部邮箱就退信或者被拒收。我遇到过太多次,原因几乎都是同一类——反解没有配置。
例子:你的服务器IP是203.0.113.10,主机名是mail.example.com,但IP的PTR记录指向了203-0-113-10.example-idc.com,或者干脆解析不出来。大型邮箱服务商收到邮件会尝试反查PTR,发现不匹配,就给一个类似451 4.7.1 Please try again later的临时拒绝。
排查方法是:
dig -x 203.0.113.10如果结果和你主机名不一致,去找云服务商或机房提工单配置PTR记录。注意:反向解析一定是IP所有方才能配置,你自己改DNS服务商是没用的。
还有一个容易被忽略的点:发送域名和服务器主机名不一致。很多人用阿里云、腾讯云服务器发邮件,但DNS的MX记录和PTR记录互相都对不上,导致各种诡异的发送失败。解决思路很简单——发件域名、服务器主机名、IP反解三者保持一致,问题基本消失。
6.2 投递延迟、队列不退
邮件一直在队列里,日志显示stat=Deferred,但看不出什么明显错误,这种情况多半是对方反垃圾策略在搞你。对方不会直接拒绝,而是给你一个临时失败,让你过段时间再试。
我的处理原则:先观察,不冲动。查看队列里滞留邮件的错误次数和重试间隔,如果错误是421 too many connections,说明对方在限流,越频繁flush越糟糕。等的时间够了,sendmail自己会按指数退避策略重试。
如果积压过多,可以考虑在sendmail.mc里调低队列运行间隔和最大队列大小:
define(`confMAX_QUEUE_RUN_SIZE', `1000') define(`confMIN_QUEUE_AGE', `30m')confMAX_QUEUE_RUN_SIZE限制单轮处理的消息数,防止一次处理太多导致机器负载飙升。confMIN_QUEUE_AGE表示邮件在队列中至少待30分钟才允许被处理,避免过早重试。
6.3 常见错误速查表
把我在处理sendmail问题时最常见的错误整理成了一张速查表,基本囊括了80%的日常问题。
| 日志或错误信息 | 可能原因 | 处理建议 |
|---|---|---|
| stat=Deferred: Name server timeout | DNS解析失败 | 检查/etc/resolv.conf,确认MX记录是否存在 |
| stat=Deferred: 421 4.7.1 Please try again later | 对方反垃圾临时拒绝 | 等队列重试,不要手动flush |
| NOQUEUE: reject: RCPT from ... | open relay或access规则拦截 | 检查/etc/mail/access,调整中继权限 |
| stat=Sent (250 2.0.0 OK) | 投递成功 | 无需处理 |
| mailq看到大量滞留且error count大 | 目标服务器长期不可达 | 检查目标域名MX和A记录是否仍有效 |
| Clientmqueue不断膨胀 | 普通用户命令行发信积压 | 用mailq -Ac查看,重启sm-msp队列服务 |
| 证书过期后对方拒收 | TLS证书过期 | 续期证书并重启sendmail |
sendmail -d0.1 -bv xxx@example.com没输出 | sendmail链接被替换 | 检查二进制实际指向 |
需要说明的是,sendmail -d0.1 -bv 收件人是一个很好用的诊断命令,它会模拟一次投递并打印出解析过程中使用的各项配置,比猜日志高效得多。
最后按我的习惯提醒一句:如果你要维护一台长时间运行的sendmail服务器,建议把日志轮转配置好,同时每天看一眼队列数量。队列一旦长期有积压,八成是DNS或者对方服务在出问题。另外,别忘了定期给系统打补丁,sendmail历史上出过不少影响很大的远程漏洞,即使它现在看起来不温不火,依然是公网暴露面的一部分。