背景
在一台 Arch KDE 桌面主机上,存在一个很实际的需求:当脚本、备份任务、系统维护任务或定时任务开始执行时,发送一封通知邮件,提示“任务已经开始,请不要关机”;当任务完成或失败时,再发送一封结果邮件。
目标不是在桌面主机上搭建完整邮件服务器,也不是让它接收外部邮件,而是让它具备一个稳定、统一、简单的“系统级发信出口”。
最终希望脚本中可以直接写:
echo "任务已经开始,请不要关机。" | mailx -s "[desktop][START] task name" user@example.com
任务完成时:
echo "任务已经完成。" | mailx -s "[desktop][OK] task name" user@example.com
任务失败时:
echo "任务失败,请检查日志。" | mailx -s "[desktop][FAILED] task name" user@example.com
无论脚本由普通用户执行,还是由 root 执行,都应能用同样方式发信。
已有经验:PVE/PBS 的系统级邮件出口
在已有服务器环境中,PVE 和 PBS 已经配置过邮件通知。它们通过本机 Postfix 将系统通知中继到外部 SMTP 服务,并使用专用账号作为发件人。备份任务、系统任务完成后都能正常发送通知邮件。
这类方案的特点是:
系统任务 / root 任务 / 备份任务
↓
本机 Postfix
↓
外部 SMTP 服务
↓
主邮箱收到通知
这种结构非常适合 PVE/PBS 这类服务器系统,因为系统任务天然由 root、systemd 或 cron 执行。如果桌面主机也希望 root 任务和普通用户任务都能统一发信,那么系统级 Postfix relay/null-client 方案比用户级 SMTP 客户端更合适。
为什么没有采用用户级 msmtp 方案
最初也可以考虑 msmtp + mailx 方案。它很轻量,适合普通用户脚本发信:
普通用户脚本
↓
msmtp
↓
外部 SMTP
但当需求扩展为“root 任务也要发信”时,用户级方案会迅速变复杂:
普通用户任务 → 使用普通用户 msmtp 配置
root 任务 → 需要 root 自己配置 msmtp,或通过 sudo -u 调用普通用户 mailx
这样会带来几个问题:
需要维护普通用户和 root 的执行边界
可能要复制 SMTP 密码到 /root
可能要写 root wrapper 和 user wrapper
脚本目录、配置目录、日志目录变多
长期维护复杂度上升
而实际需求只是希望以后脚本里能简单调用 mailx。因此,继续扩展用户级方案并不合适。
最终选择:系统级 Postfix relay/null-client
最终方案是将 Arch KDE 主机配置为系统级 Postfix relay/null-client。
结构如下:
Arch KDE 桌面主机
↓
system-level Postfix
↓
外部 SMTPS 服务
↓
专用通知邮箱
↓
主邮箱收到通知
这个方案的核心目标是:
整台机器统一发信
root 可以发信
普通用户可以发信
脚本只需要调用 mailx
不在本机接收外部邮件
不对局域网或公网开放 SMTP 服务
设计原则
本次配置遵循以下原则:
不安装 Dovecot
不把桌面主机配置成完整邮件服务器
不修改系统 hostname
不修改已有 PVE/PBS 配置
不创建用户级 ~/.config/msmtp
不创建用户级 run-and-mail 包装脚本
不维护 root/user 两套 wrapper
SMTP 密码只写入 root 拥有的 Postfix 凭据文件
普通用户和 root 都通过系统级 Postfix 发信
Postfix 只作为本机 relay/null-client 使用,不负责收取外部邮件。
包管理处理
Arch KDE 上如果已经安装了 msmtp-mta,它可能已经提供了 /usr/bin/sendmail 兼容入口。改用系统级 Postfix 后,应让 Postfix 接管系统级 MTA/sendmail 出口。
示例处理方式:
sudo pacman -Rns msmtp-mta
sudo pacman -S --needed postfix ca-certificates s-nail
其中:
postfix 系统级邮件转发器
s-nail 提供 mailx 命令
ca-certificates 提供 CA 证书
msmtp 本体可以保留,但不再作为主方案;msmtp-mta 不应继续作为系统级 sendmail 入口。
配置 Postfix
建议使用 postconf -e 写入配置,而不是手动向 main.cf 追加大段内容。这样可以减少重复参数和旧配置残留。
先备份:
sudo cp -a /etc/postfix/main.cf /etc/postfix/main.cf.bak.$(date +%Y%m%d-%H%M%S)
然后写入 relay/null-client 相关配置:
sudo postconf -e 'inet_interfaces = loopback-only'
sudo postconf -e 'inet_protocols = all'
sudo postconf -e 'mynetworks = 127.0.0.0/8 [::1]/128'
sudo postconf -e 'relayhost = [smtp.example.com]:SMTPS_PORT'
sudo postconf -e 'smtp_tls_security_level = encrypt'
sudo postconf -e 'smtp_tls_wrappermode = yes'
sudo postconf -e 'smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt'
sudo postconf -e 'smtp_sasl_auth_enable = yes'
sudo postconf -e 'smtp_sasl_password_maps = lmdb:/etc/postfix/sasl_passwd'
sudo postconf -e 'smtp_sasl_security_options = noanonymous'
sudo postconf -e 'smtp_sasl_tls_security_options = noanonymous'
sudo postconf -e 'smtp_generic_maps = lmdb:/etc/postfix/generic'
sudo postconf -e 'recipient_delimiter = +'
sudo postconf -e 'append_dot_mydomain = no'
这里使用的是 SMTPS/SSL wrapper 模式:
smtp_tls_wrappermode = yes
smtp_tls_security_level = encrypt
这对应“直接 SSL 连接”的 SMTP 服务,而不是 587 STARTTLS。
保存 SMTP 认证信息
SMTP 密码不应出现在命令历史、博客、截图、日志或聊天记录中。建议交互式输入:
sudo install -m 600 -o root -g root /dev/null /etc/postfix/sasl_passwd
read -rsp 'SMTP password for notify@example.com: ' smtp_pass
printf '\n'
printf '[smtp.example.com]:SMTPS_PORT notify@example.com:%s\n' "$smtp_pass" | sudo tee /etc/postfix/sasl_passwd >/dev/null
unset smtp_pass
sudo chmod 600 /etc/postfix/sasl_passwd
sudo postmap lmdb:/etc/postfix/sasl_passwd
sudo chmod 600 /etc/postfix/sasl_passwd.lmdb
注意不要执行:
cat /etc/postfix/sasl_passwd
该文件包含明文 SMTP 凭据,只应由 root 读取。
配置发件人映射
不同用户执行 mailx 时,本地原始发件人可能是:
root
normal-user
root@desktop-host
normal-user@desktop-host
为了让外部 SMTP 接受发信,也为了让邮件来源清晰,需要通过 generic 把这些本地发件人统一改写成合法通知邮箱。
示例:
sudo tee /etc/postfix/generic >/dev/null <<'EOF'
root notify@example.com
normal-user notify@example.com
root@desktop-host notify@example.com
normal-user@desktop-host notify@example.com
@desktop-host notify@example.com
@localhost notify@example.com
@localhost.localdomain notify@example.com
EOF
sudo chmod 644 /etc/postfix/generic
sudo postmap lmdb:/etc/postfix/generic
sudo chmod 644 /etc/postfix/generic.lmdb
这样 root 和普通用户发出的邮件,最终外部看到的发件地址都会统一为:
notify@example.com
Arch 上的一个关键差异:Postfix map 类型可能是 lmdb
在 Debian/Proxmox 环境中,Postfix 教程常见写法是:
hash:/etc/postfix/sasl_passwd
hash:/etc/postfix/generic
并生成:
sasl_passwd.db
generic.db
但在 Arch 环境中,Postfix 的默认数据库类型可能是 lmdb。如果继续使用 hash:,可能出现类似错误:
Berkeley DB support for 'hash:/etc/postfix/sasl_passwd' is not available
此时应检查:
postconf -h default_database_type
postconf -m | grep -E '^(hash|lmdb|btree|cdb)$'
如果默认类型是 lmdb,则配置应使用:
lmdb:/etc/postfix/sasl_passwd
lmdb:/etc/postfix/generic
生成的文件也不是 .db,而是:
sasl_passwd.lmdb
generic.lmdb
这是本次配置中最重要的排障点之一。
启动与检查 Postfix
配置完成后执行:
sudo postfix check
sudo systemctl enable --now postfix.service
sudo systemctl restart postfix.service
systemctl status postfix.service --no-pager
查看有效配置:
postconf -n
检查监听:
ss -ltnp | grep -E ':(25|465|587)\b' || true
目标是 Postfix 只监听本机 loopback,不对局域网或公网开放 SMTP 服务。
查看日志:
journalctl -u postfix.service -n 100 --no-pager
查看队列:
mailq
成功时日志通常可以看到类似:
relay=smtp.example.com[...]:SMTPS_PORT
status=sent
测试普通用户发信
普通用户执行:
echo 'Test mail from normal user on desktop host via Postfix relay.' \
| mailx -s '[desktop][TEST] user postfix test' admin@example.com
如果收到邮件,并且发件地址显示为 notify@example.com,说明普通用户发信成功,generic 映射生效。
测试 root 发信
整条命令由 root 执行:
sudo sh -c "echo 'Test mail from root on desktop host via Postfix relay.' \
| mailx -s '[desktop][TEST] root postfix test' 'admin@example.com'"
如果 root 发信也成功,并且发件地址仍然显示为 notify@example.com,说明系统级邮件出口已经完成基本验收。
脚本中的使用方式
配置完成后,脚本不需要关心 SMTP 服务器、端口、TLS、密码或发件人映射,只需要调用 mailx。
示例:
#!/usr/bin/env bash
set -euo pipefail
TO="admin@example.com"
TASK="example maintenance task"
echo "任务已经开始,请不要关机。
Task: $TASK
Host: $(hostname)
Time: $(date --iso-8601=seconds)
" | mailx -s "[desktop][START] $TASK" "$TO"
if /path/to/real-task.sh; then
echo "任务已经完成。
Task: $TASK
Host: $(hostname)
Time: $(date --iso-8601=seconds)
Status: OK
" | mailx -s "[desktop][OK] $TASK" "$TO"
else
rc=$?
echo "任务失败,请检查日志。
Task: $TASK
Host: $(hostname)
Time: $(date --iso-8601=seconds)
Status: FAILED
Exit code: $rc
" | mailx -s "[desktop][FAILED:$rc] $TASK" "$TO"
exit "$rc"
fi
该脚本由普通用户执行时,会通过系统级 Postfix 发信;由 root 执行时,也会通过同一个 Postfix 出口发信。
可选项:root 本地邮件转发
generic 负责改写发件人,不负责处理“发给 root 的本地邮件”。如果希望 cron 或 systemd 默认发给 root 的本地邮件也转发到主邮箱,可以另外配置 /etc/aliases:
root: admin@example.com
然后执行:
sudo newaliases
第一阶段可以先不做,只测试显式收件人的 mailx 发信。
回滚方式
停用 Postfix:
sudo systemctl disable --now postfix.service
恢复主配置:
sudo cp -a /etc/postfix/main.cf.bak.TIMESTAMP /etc/postfix/main.cf
sudo postfix check
sudo systemctl restart postfix.service
删除认证与映射文件:
sudo rm -f /etc/postfix/sasl_passwd /etc/postfix/sasl_passwd.lmdb
sudo rm -f /etc/postfix/generic /etc/postfix/generic.lmdb
如果永久废弃该配置,还应在邮件服务器系统中重置或禁用对应通知账号的 SMTP 密码。仅删除本机文件不能让服务器端旧密码失效。
如果需要回到 msmtp-mta 作为 sendmail 入口,可以在确认不再使用 Postfix 后再切换:
sudo pacman -Rns postfix
sudo pacman -S --needed msmtp-mta
这属于方案切换,不应和 Postfix 排障混在一起做。
最终验收标准
完成后应满足:
postfix.service 正常运行
s-nail/mailx 可用
Postfix 接管系统级 sendmail/MTA 出口
不安装 Dovecot
不修改系统 hostname
Postfix 只监听 loopback
relayhost 指向外部 SMTPS 服务
smtp_tls_wrappermode = yes
smtp_tls_security_level = encrypt
smtp_sasl_auth_enable = yes
sasl_passwd 权限受限
generic 能统一改写 root 与普通用户发件人
普通用户发信成功
root 发信成功
journal 中可看到投递成功记录
mailq 无异常积压
小结
如果只是普通用户脚本偶尔发信,用户级 msmtp 足够轻量。但如果目标是让整台 Arch KDE 主机都具备统一邮件通知能力,尤其是 root 任务和普通用户任务都要简单发信,那么系统级 Postfix relay/null-client 更合适。
最终效果是:脚本只需要在开始、完成、失败时调用 mailx,邮件服务器细节全部交给 Postfix 统一处理。这种结构更接近服务器上的 PVE/PBS 通知方案,也更适合长期维护。