背景

在一台 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 通知方案,也更适合长期维护。

Leave a Reply

Your email address will not be published. Required fields are marked *