大约两周前,我帮朋友排查一台Linux服务器,现象很典型:cron里跑的备份脚本明明执行了,但负责运维的同事始终没收到结果邮件。查了一圈,发现脚本里用的是mail -s,而系统上虽然装着sendmail,服务却根本没起来,邮件全部积压在队列里。这类问题在真实运维中太常见了——不是不会发邮件,而是对sendmail的定位、配置和排错路径不熟。这篇就从Linux下sendmail发送邮件这个主题出发,把从安装、配置、命令行调用到外部中继、排错排查的完整链路捋一遍,希望能帮你少踩几个坑。
1. sendmail不是给用户发信的工具,而是邮件的"中转站"
1.1 先分清MTA、MDA、MUA这三个角色
很多人第一次接触sendmail,会误以为它像Outlook或Foxmail那样是个"发邮件的客户端"。真不是。在邮件系统里,sendmail属于MTA(Mail Transfer Agent),它的核心职责是接收、路由和投递邮件。你可以把它想象成邮政系统的干线运输车:你把信交给某个邮局,邮局通过层层中转最终把信送到收件人所在城市的邮局,再由当地邮递员(MDA)送到具体地址。
而平时命令行里常用的mail、mailx、mutt这类工具,属于MUA(Mail User Agent),它们负责替你编辑信件内容、把信件递交到MTA。也就是说,你真正在终端里敲mail -s "test" user@example.com时,实际工作流是:MUA收集内容,转交给本机sendmail,sendmail再根据收件地址去查DNS的MX记录,然后把邮件投递给对方邮箱服务器。
搞清这个关系非常关键。因为后续排错时,你要先判断问题出在哪一环:是MUA没把信交到sendmail?还是sendmail投递时被对方拒绝?不同环节对应的日志位置、排查命令完全不一样。
1.2 哪些场景真的需要sendmail
现在云服务商托管邮件、企业微信、第三方报警平台一大堆,是不是就不需要自己折腾sendmail了?我的答案是:看场景。至少有四类情况你依然绕不开sendmail或同类MTA:
- 服务器上跑着cron定时任务,需要在脚本里发送状态通知、备份结果。
- 系统本身的日志告警、monit、fail2ban等工具需要发送邮件提醒。
- 内网环境中没有独立邮件服务器,需要从多台服务器向一台中央主机汇总投递邮件。
- 作为开发测试环境,需要在本机模拟完整的SMTP收发流程。
这些都是"程序发信"而不是"人发信"的典型场景。sendmail虽然配置复杂、被很多人诟病"老旧",但在各类Linux发行版中依然是默认安装率最高、行为最稳定的MTA之一。尤其当你只需要"把服务器上的邮件送出去",而不需要搭建一套收件服务时,sendmail其实很够用。
1.3 装了sendmail不等于邮件就能发出去
这里要打破一个常见误区:yum install sendmail后启动服务,并不代表你马上就能给外网发信。默认安装状态下,sendmail只监听本机回环地址的25端口,目的是防止外部网络随意使用你的服务器发垃圾邮件。同时,本机的主机名、域名解析、防火墙出网策略,都会影响一封邮件能否成功投递到目标邮箱。
我见过很多新人第一次测试,直接systemctl start sendmail,然后mail -s test xxx@qq.com,结果等了几分钟邮件没到,就去怀疑sendmail坏了。实际上邮件可能已经发出去了,只是进了对方邮箱的垃圾箱;也可能因为发件域名没有SPF记录、服务器IP没有PTR反向解析,被对方直接拒收。这些属于反垃圾信任体系的问题,跟sendmail本身的发信逻辑无关。所以在你动手配置之前,先想明白:你的服务器到底是什么角色、要往哪里发、目标对方会如何判定你的信誉。想清楚这些,后面的配置才有方向。
2. 从零装好sendmail,让它先能完成本机投递
2.1 不同发行版的安装包差异
sendmail在Red Hat、CentOS、Rocky Linux、Ubuntu、Debian等主流发行版中都有打包,但包名略有出入。我这里列一下常见发行版的安装方式,方便你直接抄。
RHEL/CentOS/Rocky/Fedora系:
yum install -y sendmail sendmail-cf sendmail-doc其中sendmail-cf包含配置宏定义文件,如果你后面要修改sendmail.mc并重新生成sendmail.cf,这个包必须有。sendmail-doc提供文档,非必需,但建议装上,遇到问题翻文档比网上搜帖子靠谱。
Debian/Ubuntu系:
apt install -y sendmail sendmail-bin sendmail-cfsendmail-bin是主程序包,sendmail-cf同样是配置生成工具。安装完成后,可以看到/etc/mail/sendmail.mc、/etc/mail/sendmail.cf、/var/spool/mqueue等目录。/var/spool/mqueue是邮件队列,所有待发送和失败的邮件都会先落在这里。
另外提醒一句:有些精简镜像或容器里,默认可能没有安装sendmail,但存在一个叫sendmail的命令,这是由postfix、exim等其它MTA提供的兼容接口。此时你用rpm -qf $(which sendmail)或dpkg -S $(which sendmail)看一下它属于哪个包,避免误判系统里装的是谁。
2.2 服务启动与开机自启,以及25端口
安装完成后,第一件事不是急着发邮件,而是启动服务并确认它真的在跑。
systemctl enable --now sendmail systemctl status sendmail如果输出显示active (running),说明服务已起来。接着检查监听端口:
ss -lntp | grep :25正常情况下你会看到sendmail只监听127.0.0.1:25和[::1]:25。这个设计是安全的:默认不允许外部主机来relay,避免成为垃圾邮件跳板。如果这里监听的是0.0.0.0,那多半是配置修改过,需要注意。
如果你的服务器出于某种原因没有启用systemd,那使用service sendmail start,并把sendmail加入rc.local或sysvinit自启。对于现代发行版,我建议尽量用systemd统一管理。
2.3 一个最小的本机投递验证
服务起来后,最快速的本机自测方法是直接给当前系统用户发一封邮件:
echo "hello sendmail" | sendmail -v root然后查看本地邮箱:
mail -H如果看到一封新邮件,说明sendmail的本地投递链路是通的。这里再解释一下:sendmail -v root等价于调用sendmail程序向本地用户root投递一封内容为"hello sendmail"的邮件。-v参数会显示投递过程中的交互细节,是排查问题的利器。
为什么要先验证本机投递?因为本地投递不涉及DNS、不涉及外网连接、不涉及对方MX服务器,成功率高,适合用来确认MTA基础功能正常。如果这步都不通过,说明sendmail安装或服务启动有问题,先去解决基础问题,再看外发。
3. 命令行发邮件的常用姿势,以及各自的坑
3.1 mail/mailx:最好记也最容易碰壁
mail命令是大多数人最熟悉的,但不同发行版默认安装的可能是mail、mailx或s-nail,三者语法略有差异。先看系统里有没有:
which mail如果没有,装一下:
yum install -y mailx # RHEL系 apt install -y s-nail # Debian系较新的发行版中mailx命令由s-nail提供基本用法:
echo "邮件正文内容" | mail -s "测试主题" user@example.com注意,管道方式输入的是邮件正文,-s指定主题。如果正文有多行,可以将其写入文件,用<重定向:
mail -s "备份结果" ops@example.com < /tmp/backup.log常见的坑有三个:
- 某些旧版mailx不支持
-s位置放在收件人前面,会报参数错误。 mail发送时会读取当前用户名作为发件人,例如root发出去就是root@hostname。如果hostname解析不到位,发件人地址可能变成root@localhost.localdomain,对方反垃圾系统看到这种域名容易直接拒绝。- 没有设置MTA时,部分发行版的mail命令会尝试使用本地socket与sendmail通信。如果sendmail服务没起来,命令会挂住等待超时。
3.2 直接喂给sendmail命令:脚本里最灵活
如果想要更多控制权,或者脚本环境比较简陋,直接使用sendmail命令反而是最稳定的方案。因为sendmail本身就是一个二进制程序,接受标准输入作为邮件内容,你只需要按照SMTP邮件格式把头部和正文准备好。
一个最简单的例子:
( echo "Subject: backup finished" echo "To: ops@example.com" echo "From: backup@myhost.example.com" echo "MIME-Version: 1.0" echo "Content-Type: text/plain; charset=utf-8" echo "" echo "备份已完成,请查看日志。" ) | sendmail -t -f backup@myhost.example.com这里-t表示从邮件头部里读取收件人(To、Cc、Bcc字段),-f指定发件人地址。手动拼装邮件头部时,标题头部和正文之间必须有一个空行,也就是echo一个空字符串的位置。如果不加空行,sendmail会把正文当成头部解析,导致邮件格式错误。
为什么说脚本里最灵活?因为你可以自由指定Header、使用变量拼装内容,甚至写入临时文件再整体重定向给sendmail。对于需要通过cron发送报表的场景非常方便。
3.3 想带附件但不想装mutt?用sendmail自己拼MIME
很多朋友一说到发附件,第一反应是装mutt。其实不装额外软件,单纯依靠sendmail也能发送带附件的邮件,只是需要自己拼MIME格式。我给一个标准模板:
( echo "From: backup@myhost.example.com" echo "To: ops@example.com" echo "Subject: 今天的数据备份" echo "MIME-Version: 1.0" echo "Content-Type: multipart/mixed; boundary=\"arkerboundary\"" echo "" echo "--arkerboundary" echo "Content-Type: text/plain; charset=utf-8" echo "" echo "请查收附件中的备份文件。" echo "" echo "--arkerboundary" echo "Content-Type: application/octet-stream; name=\"backup.tar.gz\"" echo "Content-Transfer-Encoding: base64" echo "Content-Disposition: attachment; filename=\"backup.tar.gz\"" echo "" base64 /path/to/backup.tar.gz echo "" echo "--arkerboundary--" ) | sendmail -t这个模板里,boundary是自定义的分隔字符串,--arkerboundary表示一个MIME部分的开始,--arkerboundary--表示整个邮件体的结束。附件内容用base64编码,因为SMTP协议早期只支持7位ASCII文本,遇到非文本或二进制内容必须编码传输,否则邮件会被对方服务器截断。
实际使用中,我更推荐写一个简单的shell函数封装这段拼装逻辑,而不是每次手写。比如用tar czf - /data | base64生成加密流,再塞进MIME模板,就能实现"压缩打包并作为附件发送"的完整链路。
4. 服务器直投被拒的破解思路:配置外部SMTP中继
4.1 先明白为什么不能直投:IP信誉、PTR和反垃圾
很多人在云服务器上配好sendmail后,往QQ邮箱或163邮箱发信,结果要么进垃圾箱,要么被退信。这往往不是你配置错了,而是对方邮件系统根本不相信你的服务器。
这里有几个信任维度:
- 服务器IP是否有PTR反向解析记录。家用宽带、新购云主机的IP通常没有PTR记录,对方会认为来源不可靠。
- 发件域的SPF记录是否允许当前IP代发。如果你用
abc.com作为发件域名,但SPF记录里没有列出当前服务器IP,对方会判定伪造。 - 服务器IP是否曾经被拉黑。某些IP段因为过去有人发垃圾邮件,会被公共RBL(域名黑名单)收录。
为了保证送达率,最稳妥的做法不是跟对方反垃圾规则硬碰硬,而是把sendmail配置成"通过一个信誉良好的SMTP服务器中转发送",也就是SMTP中继。比如记录在阿里云、腾讯云、企业邮箱或第三方邮件服务商给你提供的SMTP地址上。你在本机sendmail里设置好认证信息,让发往外部的邮件统一走这个中继,对方看到的是中继服务器的地址,信誉自然好得多。
4.2 sendmail走第三方邮箱的认证配置
要让sendmail通过第三方SMTP发送,需要做几件事:定义中继主机、配置认证账密、生成hash库。以常见的smtp.example.com:587为例。
首先,修改/etc/mail/sendmail.mc,在文件末尾附近加入:
define(`SMART_HOST', `smtp.example.com')dnl define(`RELAY_MAILER_ARGS', `TCP $h 587')dnl define(`ESMTP_MAILER_ARGS', `TCP $h 587')dnl FEATURE(`authinfo')dnlSMART_HOST指定所有非本地的外发邮件都交给谁;RELAY_MAILER_ARGS和ESMTP_MAILER_ARGS把端口固定到587,因为很多服务商的SMTP提交端口需要认证;authinfo功能允许我们从映射表中读取用户名和密码。
然后创建认证信息文件/etc/mail/authinfo:
AuthInfo: smtp.example.com "U:root" "I:your_username" "P:your_password" "M:PLAIN"这里U:root意思是该认证规则对所有用户生效,I是登录用户名,P是密码,M是认证方式,常见有PLAIN、LOGIN、CRAM-MD5,一般选PLAIN。
接着生成hash数据库并重新生成sendmail.cf:
makemap -r hash /etc/mail/authinfo.db < /etc/mail/authinfo m4 /etc/mail/sendmail.mc > /etc/mail/sendmail.cf最后重启sendmail:
systemctl restart sendmailmakemap命令由sendmail-cf提供,如果找不到,说明sendmail-cf没装或PATH不对。m4生成配置时,如果很久没输出,大概率是sendmail.mc里有宏定义错误,可以先m4 -E做语法检查。
4.3 配置验证与常见翻车点
改完中继配置后,可以用带-v参数的命令测试:
echo "中继测试" | sendmail -v -f from@example.com to@qq.com观察输出,如果看到220 smtp.example.com开头的响应,并且最终出现250 2.0.0字样,说明投递成功。如果看到535 authentication failed,说明用户名或密码错误;看到530 Must issue a STARTTLS command first,说明对方要求TLS,但sendmail配置文件里没有开启starttls功能。这时需要加入:
define(`CERT_DIR', `/etc/pki/tls/certs')dnl include(`/etc/mail/tls/starttls.m4')dnl然后重新生成配置。另外,很多地方要求服务器上打开587端口的出站方向,如果安全组或iptables拦了,会导致连接超时。建议先用telnet smtp.example.com 587测一下能否连上。
还有一个容易被忽略的点:/etc/mail/authinfo文件里包含明文密码,权限必须设为600或400,否则sendmail会拒绝读取并告警。用chmod 600 /etc/mail/authinfo。
5. 邮件发不出去?从日志、队列和DNS三个方向查
5.1 /var/log/maillog的关键字段解读
sendmail的日志通常记录在/var/log/maillog(RHEL系)或/var/log/mail.log(Debian系)。排查发信问题时,这个文件是第一手证据。一条典型的失败日志长这样:
May 18 10:23:45 host sendmail[12345]: 4AI1Bv000123: to=<ops@example.com>, delay=00:01:23, xdelay=00:00:30, mailer=esmtp, pri=120393, relay=mail.example.com [203.0.113.10], dsn=2.0.0, stat=Sent (ok)relay=显示实际连接的目标服务器;dsn=2.0.0表示成功,如果是dsn=5.x.x则代表永久失败,4.x.x是暂时失败。比如:
stat=User unknown:收件人不存在。stat=Service unavailable:对方拒绝或服务器暂时不可用。stat=Deferred:暂时投递失败,已放进队列等待重试。
看到日志后,先确认最后一步到底是连了谁、被谁拒了,再对症下药。不要盲目改配置。
5.2 队列积压的处理:mailq与强制重发
当目标服务器临时拒绝时,邮件会留在队列/var/spool/mqueue中。用mailq查看当前队列:
mailq输出里每一行代表一个队列条目,包含邮件ID、大小、发送时间、发件人和错误状态。如果队里积压很多,想立刻重新投递,可以使用:
sendmail -q -v-q会立即处理队列中所有邮件,-v显示详细状态。注意,如果在邮件量很大的系统上,全部刷新可能造成短暂的高负载。更轻柔的方式是sendmail -qR指定某个队列ID重试单个邮件,例如:
sendmail -qR4AI1Bv000123如果确定某封邮件根本不需要再投递,可以用sendmail -qR处理完后,再用mailq确认队列为空。对于长期刷不掉的死信,可以定期用sendmail -q30m配合crontab设置每30分钟自动刷新一次队列,防止它们一直占用磁盘。
5.3 出网封锁和DNS解析最容易假装"发信失败"
最后一类坑,也是最容易让人绕远路的:sendmail配置没问题,日志里也尝试连接了,但就是超时或退信。这种时候,八成是网络或DNS的问题。
先测出网连通性。目标邮件服务器的25端口通常走公网SMTP,如果云平台安全组或本地防火墙限制了出站25端口,sendmail会长时间卡在连接阶段。测试方法:
timeout 10 telnet smtp.qq.com 25如果连不通,再看是否被防火墙拦截。很多云服务器默认只开放80、443等常用端口,出站25往往被限制。对于这种情况,最省事的方案还是走中继,中继端口选587或465,一般云厂商默认放行。
DNS方面,sendmail投递之前要解析收件域名的MX记录。如果系统内部DNS配置错误,dig MX qq.com查不出来,sendmail就会报"无法解析邮件服务器"之类的错误。先用:
dig +short MX qq.com确认能正常返回MX记录。另外还要检查本机hostname能否反解出对应IP,也就是hostname -f输出的全限定域名是否能在/etc/hosts或DNS中找到对应条目。否则sendmail会用localhost.localdomain作为本机标识,这在发送时很容易被对方拒绝。
这一路排查下来,你会发现大多数"发不出去"其实都离sendmail本体很远。把日志、队列、网络三层拆开看,问题定位会快很多。
我个人在实际排查中,最常对别人说的一句话是:不要盯着配置改,先看日志里它到底在连谁。sendmail的日志详细程度远超你的想象,读明白一行,胜过重启十次服务。另外,如果你只是想让服务器内部脚本之间的通知互通,其实不必非要装上完整的sendmail,postfix的兼容命令sendmail也很够用,但前提是不要被"命令同名"迷惑。实际生产里,我更倾向于把sendmail配置好中继后固定下来,再配合前面提到的MIME封装技巧,就能覆盖绝大部分服务器自动发信需求。这套组合我已经稳定用了好几年,希望你也能少踩一些我踩过的坑。