OpenSMTPD 常用 CLI 命令详解

OpenSMTPD 7.9.0p0 的命令行管理工具详解:smtpctl 15 个子命令与 show 10 个子命令按业务域逐条实测(队列查看/暂停恢复/重排删除/日志与运行时诊断/SPF 诊断/队列恢复),附 smtpd(8) 守护进程参数、sendmail 兼容层、离线队列闭环与排障实录

OpenSMTPD邮件服务器OpenSMTPD2026/10/21

OpenSMTPD 常用 CLI 命令详解

目标:把 OpenSMTPD 7.9.0p0 的命令行管理工具讲透——smtpctl 的全部子命令按「状态与队列查看、队列管理、日志与运行时诊断、安全与队列恢复」四个业务域逐条给出实测语法与真实回显,smtpd(8) 守护进程侧参数与 sendmail 兼容层一并覆盖,并沉淀使用过程中的排障实录(smtpctl stop 已移除、pause 状态不持久化、monitor 管道缓冲无输出等)。本文所有命令与回显均在 Ubuntu 22.04(OpenSMTPD 7.9.0p0 源码编译版)实测通过,实测日期 2026-10-21。

一、环境信息

本文所有命令与回显均在以下环境实测通过:

项目值
操作系统Ubuntu 22.04.5 LTS(x86_64)
主机名mail.example.com
软件版本OpenSMTPD 7.9.0p0(上游最新稳定版,2026-09-13 发布)
安装方式官方源码编译(安装前缀 /usr/local,安装过程见安装指南篇)
smtpctl 路径/usr/local/sbin/smtpctl(setgid _smtpq)
smtpd 路径/usr/local/sbin/smtpd
配置文件/usr/local/etc/smtpd.conf
队列目录/var/spool/smtpd
systemd 服务opensmtpd79.service
特权分离用户_smtpd / _smtpq
演示账号root / 普通用户 admin(密码 q1w2e3r4)
测试工具swaks 20201014.0(制造队列流量)

二、工具概览

OpenSMTPD 的命令行工具体系由「一个守护进程本体 + 一个控制命令 + 一组兼容层符号链接」构成。与多数 MTA 不同,OpenSMTPD 的常驻进程 smtpd 没有交互式管理子命令——运行时的全部控制都经 smtpctl 下发,改配置才需要重启 smtpd(配置语句见 《OpenSMTPD 7.9 配置参数详解》,站内链接见文末参考)。

2.1 子命令全景

不带参数运行 smtpctl,会打印全部顶级子命令(这是 7.9.0p0 内建的用法提示,也是最快的速查入口):

smtpctl

实测回显(退出码 255):

possibilities are:
discover
pause
resume
show
log
monitor
profile
remove
schedule
trace
unprofile
untrace
update
encrypt
spf

15 个顶级子命令中,show 自带第二级子命令。同样地,不带参数运行 smtpctl show:

smtpctl show

实测回显(退出码 255):

possibilities are:
show mta
show envelope
show hoststats
show message
show queue
show hosts
show relays
show routes
show stats
show status

按业务域整理后的完整命令语法清单如下(依 7.9.0p0 命令注册口径与内建用法提示整理,<msgid> 为 8 位十六进制消息 ID、<evpid> 为 16 位十六进制信封 ID):

域命令语法职责
查看show statusSMTP/MTA/MDA 三职能运行状态
查看show queue [msgid]队列信封列表(管道分隔格式)
查看show envelope <evpid>单个信封全部字段
查看show message <msgid|evpid>消息正文(含头)
查看show stats内部计数器(key=value)
查看show hosts / show relays / show routesMTA 出站主机/中继/路由表
查看show hoststats按域名统计的中继结果
查看show mta pausedMTA 域级阻断列表
队列pause mda|mta|smtp / resume mda|mta|smtp职能级暂停/恢复
队列pause|resume envelope <msgid|evpid|all>信封级暂停/恢复
队列pause|resume mta from <addr> for <domain>MTA 域级阻断/解除
队列schedule <msgid|evpid|all>立即重排投递
队列remove <msgid|evpid|all>删除队列信封
队列update table <name>热加载 file 表
队列discover <msgid|evpid>从盘上重扫描信封注入调度器
诊断log verbose|brief日志详细/简洁模式
诊断monitor每秒采样的实时监控
诊断trace <子系统> / untrace <子系统>分级调试跟踪开关
诊断profile <imsg|queue> / unprofile <…>内部剖析计数开关
安全encrypt [明文]生成 crypt 口令散列
安全spf walk从 stdin 逐行做 SPF 记录展开

💡 说明:命令分「特权」与「非特权」两档。特权命令(上表绝大多数)要求以 root 身份执行;非特权命令只有 encrypt 与 spf walk。这是由控制通道的权限模型决定的,2.3 节有实测。

2.2 权限模型与调用前提

smtpctl 通过 /var/run/smtpd.sock 控制套接字与 smtpd 主进程通信。非 root 用户执行特权命令时直接拒绝,实测(以 admin 用户执行):

su - admin -c '/usr/local/sbin/smtpctl show queue'

实测回显(退出码 1):

smtpctl: need root privileges

而非特权命令 encrypt 以普通用户可正常执行:

su - admin -c 'echo q1w2e3r4 | /usr/local/sbin/smtpctl encrypt'

实测回显(退出码 0):

$y$jCT$wNtkeQQL5PvTqy797iYqW0$hy8w05Pgtt2xrlPbNwFP22FeSzbUujm6Wm2sX33LDu1

另外 smtpctl 二进制必须保持 setgid _smtpq 位(源码版若手工补过权限,用 ls -l 确认 -r-xr-sr-x 1 root _smtpq),否则报 smtpctl: this program must be setgid _smtpq——这是源码安装篇踩过并修复过的坑。

2.3 输出约定

掌握三条输出约定,所有子命令的回显就都能读懂:

  1. 成功应答:多数控制类命令成功时输出固定一行 command succeeded(如 pause/resume/log/trace/update table);
  2. 计数应答:作用于信封集合的命令输出动作计数,如 1 envelope scheduled、2 envelopes removed、0 envelope paused——0 开头不代表失败,只是命中的信封数为 0(比如队列已空或信封处于不可操作状态);
  3. 用法提示:子命令拼写或参数形态不对时,输出 possibilities are: + 合法形态列表,退出码 255——这份提示本身就是权威的参数清单(2.1 节即由此取证)。

三、前置条件

复现本文操作需要:

  1. 一台已安装 OpenSMTPD 7.9.0p0 并正在运行的 Ubuntu 22.04 服务器(安装流程见 《Ubuntu 22.04 安装 OpenSMTPD 指南》),运行状态验证:
systemctl is-active opensmtpd79
/usr/local/sbin/smtpctl show status

实测回显:

active
MDA running
MTA running
SMTP running

MDA(本地投递)、MTA(出站中继)、SMTP(收信会话)三个职能全部 running 即服务健康。

  1. 演示用户与别名就绪(OpenSMTPD 默认拒绝向 root 直接投递,需在 /etc/aliases 中把 root 别名到普通用户):
id admin
grep '^root:' /etc/aliases

实测回显:

uid=1000(admin) gid=1000(admin) groups=1000(admin)
root: admin
  1. 测试工具 swaks(用于向队列注入真实邮件,观察队列类命令的实际效果):
swaks --version 2>&1 | head -1

实测回显:

swaks version 20201014.0

四、状态与队列查看类子命令详解

查看类命令是日常巡检与排障的第一入口。本章逐条给出 show 各子命令的语法、回显与字段解析。

4.1 show status:三职能运行状态

show status 回答「服务现在能收、能发、能投吗」。三个职能分别对应配置章节的 listen(SMTP)、relay 动作(MTA)、maildir/mbox/mda 动作(MDA):

smtpctl show status

实测回显(正常态):

MDA running
MTA running
SMTP running

职能被 pause 后状态变为 paused(见第五章),例如暂停 MDA 后:

smtpctl pause mda
smtpctl show status

实测回显:

command succeeded
MDA paused
MTA running
SMTP running

排障时若某个职能长期 paused 而无人承认操作过,优先怀疑上一次 restart 前的 pause 被忽略(pause 不持久化,见 9.2),或配置语法变更后动作未挂载。

4.2 show queue:队列列表与管道格式逐列解析

show queue 列出当前队列中的全部信封(envelope)。OpenSMTPD 的队列模型是「消息(message)—信封(envelope)」两级:一封邮件是一个消息,其每个收件人对应一个信封,独立投递、独立重试。先向队列注入两封测试邮件(暂停 MDA 使其滞留):

smtpctl pause mda
swaks --to root@mail.example.com --from admin@mail.example.com --server 127.0.0.1:25 --h-Subject 'sched-test-1' 2>&1 | grep '250 2.0.0'
swaks --to admin@mail.example.com --from admin@mail.example.com --server 127.0.0.1:25 --h-Subject 'sched-test-2' 2>&1 | grep '250 2.0.0'
smtpctl show queue

实测回显(swaks 只保留接受行,队列两条完整列出):

command succeeded
<-  250 2.0.0 cffeb9ea Message accepted for delivery
<-  250 2.0.0 04b0c493 Message accepted for delivery
04b0c49383d17823|inet4|mda||admin@mail.example.com|admin@mail.example.com|admin@mail.example.com|1792578756|1792578756|0|0|pending|0|
cffeb9ea644cdae7|inet4|mda||admin@mail.example.com|root@mail.example.com|root@mail.example.com|1792578756|1792578756|0|0|pending|0|

每行是一个信封,竖线 | 分隔共 13 列,逐列含义如下(依 smtpctl(8) 手册口径与 7.9.0p0 实测回显对照整理):

列本例取值含义
104b0c49383d17823信封 ID(evpid),16 位十六进制,前 8 位即消息 ID(msgid)
2inet4发件来源套接字族:inet4 / inet6 / local(本机 socket 注入)
3mda投递代理类型:mda / mta / bounce
4(空)信封标志组合(逗号连接):bounce、auth、internal、suspend、hold;空即无标志
5admin@mail.example.com发件人
6admin@mail.example.com收件人(RCPT TO 原值)
7admin@mail.example.com实际投递目标(经别名/虚拟域展开后的最终信箱)
81792578756信封创建时间(epoch 秒)
91792578756过期时间(创建时间 + ttl;ttl 为 0 时不过期,与创建时间相同)
100上次投递尝试时间(0 = 从未尝试)
110已重试次数
12pending|0运行状态|秒数,三种形态见下方说明
13(空)最近一次错误行(如远端 SMTP 应答),多数情况为空

第 12 列的运行状态是排障时最值得关注的一列,三种实测形态:

  • pending|N——等待投递,N 为距下次尝试的秒数;负值表示已过期待试(如 pending|-5 即 5 秒前就该重试)、pending|0 表示即刻尝试;
  • inflight|N——投递进行中,N 为距上次尝试的秒数;
  • offline|——smtpd 停止时经 show queue 离线读盘看到的信封(7.4 节实测)。

4.3 show queue <msgid>:按消息过滤

show queue 支持追加一个 msgid 参数,只列出该消息的信封。用 4.2 中第二封邮件的 msgid cffeb9ea:

smtpctl show queue cffeb9ea

实测回显:

cffeb9ea644cdae7|inet4|mda||admin@mail.example.com|root@mail.example.com|root@mail.example.com|1792578756|1792578756|0|0|pending|0|

一封邮件有多个收件人时,该命令会列出同 msgid 的多个信封行——这是定位「某封信的某个收件人为什么没收到」的标准入口。

4.4 show envelope <evpid>:单个信封全字段

show envelope 展开单个信封的全部内部字段,比队列行的 13 列更细(多出 dispatcher、helo、sockaddr、ttl、mda-user 等)。注意它只接受完整的 16 位 evpid,传 8 位 msgid 会报用法提示(实测排障,见 9.1):

smtpctl show envelope c8a469afecb34628

实测回显:

version: 3
dispatcher: local
type: mda
smtpname: mail.example.com
helo: mail.example.com
hostname: localhost
sockaddr: 127.0.0.1
sender: admin@mail.example.com
rcpt: admin@mail.example.com
dest: admin@mail.example.com
ctime: 1792578599
last-try: 0
last-bounce: 0
ttl: 0
retry: 0
dsn-notify: 0
mda-user: admin

逐项解读:version 为信封结构版本号;dispatcher/type 标识信封归属的投递路径(local 派发器 + mda 类型);smtpname 是收下该信封的监听口名;helo/hostname/sockaddr 记录 SMTP 会话来源;sender/rcpt/dest 与队列行第 5–7 列一致;ctime/last-try/last-bounce/ttl/retry 是调度计数字段;dsn-notify 标记 DSN 请求位;mda-user 是本地投递将使用的系统用户(本例经别名展开为 admin)——排查「信投给了谁、以谁的身份投」就看这一行。

4.5 show message <msgid|evpid>:查看消息正文

show message 输出消息的完整内容(头部 + 正文),既接受 msgid 也接受 evpid,是确认「队列里这封信到底是什么内容」的直读手段:

smtpctl show message c8a469af

实测回显:

Received: from mail.example.com (localhost [127.0.0.1])
        by mail.example.com (OpenSMTPD) with ESMTP id c8a469af
        for <admin@mail.example.com>;
        Wed, 21 Oct 2026 10:29:59 +0000 (UTC)
Date: Wed, 21 Oct 2026 10:29:59 +0000
To: admin@mail.example.com
From: admin@mail.example.com
Subject: cli-d1-test-2
Message-Id: <20261021102959.020138@mail.example.com>
X-Mailer: swaks v20201014.0 jetmore.org/john/code/swaks/

This is a test mailing

💡 技术拓展:MTA 的可观测手段不止 show 家族,三层手段各司其职,横向对比——

手段粒度时效典型用途
show status / show queue队列与状态快照当下巡检、定位滞留信封
monitor每秒采样的计数增量实时流观察流量趋势、确认注信是否进队
trace <子系统>会话级调试行开关期间深挖 SMTP 会话/规则命中细节
show stats进程启动以来的累计计数累计值容量与健康度复盘

本文实操顺序通常是:show queue 定位 → monitor 看增量 → 必要时 trace 深挖。

4.6 show stats:内部计数器

show stats 输出全部内部计数器(key=value 形式),覆盖控制通道、队列缓存、调度器、MDA/MTA 各子系统。输出较长,以下为实测节选(首部):

smtpctl show stats | head -24

实测回显:

control.session=1
mda.envelope=0
mda.pending=0
mda.running=0
mda.user=0
mta.connector=0
mta.domain=0
mta.envelope=0
mta.host=0
mta.relay=0
mta.route=0
mta.session=0
mta.source=0
mta.task=0
mta.task.running=0
queue.evpcache.load.hit=34
queue.evpcache.size=0
scheduler.delivery.ok=11
scheduler.envelope=0
scheduler.envelope.incoming=0
scheduler.envelope.inflight=0
scheduler.envelope.removed=1
scheduler.ramqueue.envelope=0
scheduler.ramqueue.message=0

关键指标解读:scheduler.delivery.ok 是投递成功累计次数(本例 11 次,与此前投递闭环次数一致);scheduler.envelope.inflight/incoming 是当前在投/入队中信封数,稳定为 0 说明队列排空;queue.evpcache.load.hit 是信封缓存命中数,异常暴涨通常意味着反复重读盘上信封(调度异常征兆)。mta.* 各项在无出站流量时为 0,出站活跃时用于观察连接与任务堆积。

4.7 show hosts / show relays / show routes / show hoststats:出站中继四件套

这四个命令展示 MTA 出站侧的实时拓扑。为了让它们有内容可看,先触发一笔真实的中继事务(本环境配置了 action "smart" relay host smtp://127.0.0.1:2525,将 relaytest.example 域的信转发到本机 2525 口的接收端):

swaks --to someone@relaytest.example --from admin@mail.example.com --server 127.0.0.1:25 --h-Subject 'mta-show-test' 2>&1 | grep '250 2.0.0'
smtpctl show hosts
smtpctl show relays
smtpctl show routes
smtpctl show hoststats

实测回显:

<-  250 2.0.0 cc605ffc Message accepted for delivery
127.0.0.1 localhost refcount=2 nconn=1 lastconn=0s
[relay:127.0.0.1,port=2525,smtp,mx] refcount=2 ntask=0 nconn=1 lastconn=0s timeout=1s wait=connector
  connector [] refcount=0 nconn=1 lastconn=0s timeout=- flags=
1. [] <-> 127.0.0.1 (localhost) ---- nconn=1 nerror=0 penalty=0 timeout=-
relaytest.example|1792578908|250 OK

逐条解读:

  • show hosts——MTA 已感知的对端主机表:127.0.0.1 localhost refcount=2 nconn=1 lastconn=0s,nconn 为活跃连接数,lastconn 为距上次连接的秒数;
  • show relays——中继端点(relay URI)与连接器状态:[relay:127.0.0.1,port=2525,smtp,mx] 是中继标识,wait=connector 表示正处在连接阶段,任务/连接计数随事务推进变化;
  • show routes——源地址到对端的「路由」表,nerror=0 penalty=0 表示无失败记录、无惩罚降权(MTA 对连续失败的主机会累计 penalty 并拉长重试间隔,这里是健康态);
  • show hoststats——按收件域汇总最近一次中继结果:relaytest.example|1792578908|250 OK 即该域最后一次成功中继的时间与远端应答。

排障口诀:出站卡住时按 show hoststats(哪个域最近一次结果是什么)→ show routes(penalty 是否暴涨)→ show relays(卡在 connector 还是 mx 等待)→ show queue(信封 runstate)的顺序走。

4.8 show mta paused:域级阻断列表

show mta paused 列出被 pause mta from <addr> for <domain> 阻断的「来源地址 → 目标域」对(操作见 5.3),无阻断时输出为空:

smtpctl show mta paused

实测回显(无阻断,空输出):

五、队列管理类子命令详解

队列管理类命令解决四类日常诉求:临时停掉某个方向的投递(pause/resume)、把滞留信封提前重排(schedule)、删除问题信封(remove)、热加载数据表(update table)。每条命令都在真实队列上闭环验证。

5.1 pause/resume mda|mta|smtp:职能级暂停与恢复

pause 冻结一个投递职能,resume 恢复。三个对象:mda(本地投递)、mta(出站中继)、smtp(入站会话)。最常用的场景是暂停 MDA 制造受控滞留——本章多数实验的前置步骤:

smtpctl pause mda
smtpctl show status

实测回显:

command succeeded
MDA paused
MTA running
SMTP running

暂停期间新信照常入队但不投递。恢复后滞留信封立即投出,日志中留下两条证据(info: mda resumed 与投递行,投递行的 delay 字段即滞留时长):

smtpctl resume mda
journalctl -t smtpd --no-pager -n 3 | grep -E 'resumed|mda delivery' | tail -2

实测回显:

command succeeded
Oct 21 10:33:01 mail.example.com smtpd[20218]: info: mda resumed
Oct 21 10:33:01 mail.example.com smtpd[20220]: 3f89b096506423fd mda delivery evpid=cffeb9ea644cdae7 from=<admin@mail.example.com> to=<root@mail.example.com> rcpt=<root@mail.example.com> user=admin delay=25s result=Ok stat=Delivered

delay=25s 正是该信封从入队到 resume 的滞留时长,pause/resume 闭环成立。

⚠️ 排障实录:pause 状态不持久化。实测把 MDA pause 后执行 systemctl restart opensmtpd79,重启完成即回到 running,滞留信封随即全部投出——MDA running / MTA running / SMTP running。若你的运维流程依赖「重启期间冻结投递」,restart 后要重新 pause,不能假设暂停状态跨重启保留。

5.2 pause/resume envelope:信封级暂停与恢复

比职能级更细一格,可把单个消息/信封钉在队列里不动(其余信封照常投递)。传 msgid 作用于该消息全部信封,传 evpid 作用于单个信封,all 作用于全队列。实测先 pause mda 制造 pending 信封,再按 msgid 暂停:

smtpctl pause mda
swaks --to root@mail.example.com --from admin@mail.example.com --server 127.0.0.1:25 --h-Subject 'envhold-test' 2>&1 | grep '250 2.0.0'
smtpctl pause envelope bd1f6679
smtpctl show queue

实测回显:

command succeeded
<-  250 2.0.0 bd1f6679 Message accepted for delivery
1 envelope paused
bd1f6679407685fe|inet4|mda|suspend|admin@mail.example.com|root@mail.example.com|root@mail.example.com|1792579007|1792579007|0|0|pending|-10|

对比 4.2 的队列格式:第 4 列标志位从空变为 suspend,即信封级暂停的落点。恢复并放开职能级暂停后信封正常投出:

smtpctl resume envelope bd1f6679
smtpctl resume mda
smtpctl show queue

实测回显:

1 envelope resumed
command succeeded

(队列已空,无输出。)

两个实测边界:

  1. 对不在 pending 态的信封操作会得到 0 计数。实测队列信封已进入 inflight(投递进行中)时执行 pause envelope all:
smtpctl pause envelope all

实测回显:

0 envelope paused
  1. resume envelope 同理按 msgid/evpid/all 三种粒度计数(1 envelope resumed)。

5.3 pause/resume mta from <addr> for <domain>:出站域级阻断

这是 MTA 方向的「熔断开关」:指定来源地址对指定目标域的中继全部冻结,常用于「上游域名故障/被投诉,先止损再排查」。实测阻断 127.0.0.1 → example.com:

smtpctl pause mta from 127.0.0.1 for example.com
smtpctl show mta paused
smtpctl resume mta from 127.0.0.1 for example.com
smtpctl show mta paused

实测回显:

command succeeded
127.0.0.1 -> example.com
command succeeded

(解除后 show mta paused 空输出。)阻断列表是运行时状态,重启后清空(同 5.1 的不持久化口径)。

5.4 schedule:立即重排投递

schedule 把信封的下次尝试时间改成立即,用于「配置已修复/上游已恢复,不等重试间隔了」。接受 msgid、evpid、all 三种粒度。实测(队列中留有一封滞留信):

smtpctl schedule cffeb9ea

实测回显:

1 envelope scheduled

按 evpid 亦可:

smtpctl schedule cffeb9ea644cdae7

实测回显:

1 envelope scheduled

队列空时 schedule all 返回 0 计数(成功但无事可做):

smtpctl schedule all

实测回显:

0 envelope scheduled

注意:若信封被 5.2 的 envelope 级 suspend 钉住,schedule 不会解除 suspend,先 resume envelope 再 schedule。

5.5 remove:删除队列信封

remove 从队列删除信封(不产生退信),是处理死信、误发件、垃圾洪水的主要手段。按 evpid 删除单个信封:

smtpctl remove f8b952bc7b3a7ef6

实测回显:

1 envelope removed

按 msgid 删除整封消息的全部信封;remove all 清空队列。实测注入两封滞留信后一键清队:

smtpctl remove all
smtpctl show queue

实测回显:

2 envelopes removed

(队列空,无输出。)被删除的信封直接消失,不生成退信——若需要让发件人知道邮件失败,应让 ttl 过期走 bounce 流程而不是手工 remove。

5.6 update table:热加载文件表

update table 通知守护进程重新加载指定 file 表(别名、凭据、黑白名单等),无需重启。实测(修改 /etc/aliases 后):

smtpctl update table aliases

实测回显:

command succeeded

配合 newaliases 使用(重建别名库 + 通知热加载),以及凭据表热加载后的认证验证闭环,见 《OpenSMTPD 7.9 配置参数详解》5.2 节的实测。

六、日志与运行时诊断类子命令详解

诊断类命令回答三个问题:日志现在记多细(log)、流量此刻长什么样(monitor)、某个子系统的内部过程能否展开看(trace/profile)。

6.1 log verbose / log brief:日志详细度开关

log verbose 把守护进程日志切到详细模式,log brief 切回。两命令本身只回一句成功,效果体现在 journal 的日志密度上:

smtpctl log verbose
smtpctl log brief

实测回显:

command succeeded
command succeeded

详细模式主要多出进程间消息与调度细节,用于「日志里找不到线索」的场合;排查完记得切回 brief,避免长期刷盘。

6.2 monitor:每秒采样实时监控

monitor 每秒输出一行计数增量,表头分四段:client(连接)、envelope(队列进出)、relay/delivery(投递结果)、misc(过期/删除/退信):

timeout 6 smtpctl monitor

实测回显(6 秒采样窗口,保留前几行):

--- client ---  -- envelope --   ---- relay/delivery --- ------- misc -------
curr conn disc  curr  enq  deq   ok tmpfail prmfail loop expire remove bounce
   0    8    8     0   11   11   10       0       0    0      0      1      0
   0    0    0     0    0    0    0       0       0    0      0      0      0
   0    0    0     0    0    0    0       0       0    0      0      0      0
   0    0    0     0    0    0    0       0       0    0      0      0      0
   0    0    0     0    0    0    0       0       0    0      0      0      0
   0    0    0     0    0    0    0       0       0    0      0      0      0

列序解读:curr conn disc 为当前连接数/连接累计/断开累计;curr enq deq 为队列中信封数/入队/出队;ok tmpfail prmfail loop 为投递成功/临时失败/永久失败/环路;expire remove bounce 为过期/删除/退信。首行是进程启动以来的累计值(对零做差),后续每行是上一秒增量——首行 conn=8 enq=11 ok=10 bounce=1 即为该实例启动至今的真实吞吐。

⚠️ 排障实录:把 monitor 的输出重定向到文件或管道时,timeout N smtpctl monitor > 文件 会得到空文件——printf 在非终端上是全缓冲,timeout 的 SIGTERM 直接把未刷新的缓冲区一起带走。实测两种可用姿势:① 直接在真实服务器上交互运行(本就是 monitor 的正常用法);② 采样期间另开一个会话注信,回看交互输出。真实服务器上 monitor 就是设计为交互使用的,上述坑只影响「采集到文件」的自动化场景——此时建议改用 show stats 做两次快照取差值。

6.3 trace / untrace:子系统级调试开关

trace 打开指定子系统的调试日志(输出进守护进程日志,journalctl -t smtpd 可见),untrace 关闭。合法关键字共 13 个(7.9.0p0 实测口径):

关键字跟踪对象
imsg进程间消息
io底层 IO 事件
smtpSMTP 会话(收发两侧协议行)
filters过滤器评估
mta出站中继
bounce退信
scheduler调度决策
lookup表查询
stat计数器变化
rulesmatch 规则命中
mproc进程间管道
expand别名/虚拟域展开
all以上全部(不含 debug 级)

最有实用价值的是 trace smtp——把 SMTP 会话的每一步协议交互展开成日志。实测开 trace 后注入一封信,抓取日志中的会话展开:

smtpctl trace smtp
swaks --to admin@mail.example.com --from admin@mail.example.com --server 127.0.0.1:25 --h-Subject 'trace-cap2' >/dev/null 2>&1
journalctl -t smtpd --no-pager -n 25 | tail -14
smtpctl untrace smtp

实测回显(journal 节选,trace 期间):

Oct 21 10:40:28 mail.example.com smtpd[20220]: <<< [MSG]
Oct 21 10:40:28 mail.example.com smtpd[20220]: <<< [MSG]
Oct 21 10:40:28 mail.example.com smtpd[20220]: <<< [MSG] .
Oct 21 10:40:28 mail.example.com smtpd[20220]: <<< [EOM]
Oct 21 10:40:28 mail.example.com smtpd[20220]: smtp: 0x5f355474e3a0: >>> 250 2.0.0 4c53f259 Message accepted for delivery
Oct 21 10:40:28 mail.example.com smtpd[20220]: 3f89b0b424cb05ad smtp message msgid=4c53f259 size=614 nrcpt=1 proto=ESMTP
Oct 21 10:40:28 mail.example.com smtpd[20220]: 3f89b0b424cb05ad smtp envelope evpid=4c53f2596be2c1d8 from=<admin@mail.example.com> to=<admin@mail.example.com>
Oct 21 10:40:28 mail.example.com smtpd[20220]: smtp: 0x5f355474e3a0: STATE_BODY -> STATE_HELO
Oct 21 10:40:28 mail.example.com smtpd[20222]: debug: scheduler: evp:4c53f2596be2c1d8 scheduled (mda)
Oct 21 10:40:28 mail.example.com smtpd[20220]: smtp: 0x5f355474e3a0: <<< QUIT
Oct 21 10:40:28 mail.example.com smtpd[20220]: smtp: 0x5f355474e3a0: >>> 221 2.0.0 Bye
Oct 21 10:40:28 mail.example.com smtpd[20220]: smtp: 0x5f355474e3a0: STATE_HELO -> STATE_QUIT
Oct 21 10:40:28 mail.example.com smtpd[20220]: 3f89b0b424cb05ad smtp disconnected reason=quit

<<< 是对端发来的协议行、>>> 是本端应答,msgid/evpid/会话状态机(STATE_*)一目了然。传非法关键字会被直接点名(实测 smtpctl trace bogus):

smtpctl: invalid trace keyword: bogus

trace rules 排查 match 规则命中顺序的实测(rule #1 matched 日志形态)见 《OpenSMTPD 7.9 配置参数详解》4.3 节。

6.4 profile / unprofile:内部剖析计数

profile 打开两个剖析维度之一:imsg(进程间消息量)或 queue(队列操作量),打开后 show stats 中会出现对应剖析计数;unprofile 关闭。实测:

smtpctl profile queue
smtpctl unprofile queue

实测回显:

command succeeded
command succeeded

传其他值报错(实测 smtpctl profile bogus,退出码 1):

smtpctl: invalid profile keyword: bogus

profile 的定位是性能排查辅助(哪个内部通道消息量异常),日常巡检用不到,保持关闭。

七、安全与队列恢复类子命令详解

本章覆盖三个「低频但关键」的命令:encrypt(凭据散列)、spf walk(SPF 记录诊断)、discover(队列信封重扫描),以及与 discover 配套的离线队列机制。

7.1 encrypt:生成凭据散列

encrypt 把明文口令转成 crypt 散列,用于生成 auth 凭据表内容。既可作位置参数也可从 stdin 读入(盐随机,每次输出不同):

smtpctl encrypt q1w2e3r4
echo q1w2e3r4 | smtpctl encrypt

实测回显(两次运行,散列值不同属正常):

$y$jCT$rYz5EvgWB5A.4zQ2cKYvZ0$1lwVmj0mJ5opDiQ86SsdFjEMr9SBJPpXGSWT7OjZyj7
$y$jCT$wNtkeQQL5PvTqy797iYqW0$hy8w05Pgtt2xrlPbNwFP22FeSzbUujm6Wm2sX33LDu1

散列写入凭据表文件(用户名 散列 空格分隔)并 smtpctl update table 热加载的完整闭环见 《OpenSMTPD 7.9 配置参数详解》5.2 节。

7.2 spf walk:SPF 记录展开诊断

spf walk 从 stdin 逐行读域名,解析其 SPF(Sender Policy Framework)TXT 记录,输出该域授权发信的 IP 网段(含 include 展开与重定向),可选 -4/-6 只看 IPv4/IPv6 段。用 gmail.com 实测:

echo gmail.com | smtpctl spf walk

实测回显:

74.125.0.0/16
209.85.128.0/17
2001:4860:4864::/56
2404:6800:4864::/56
2607:f8b0:4864::/56
2800:3f0:4864::/56
2a00:1450:4864::/56
2c0f:fb50:4864::/56

⚠️ 实测说明:SPF 展开结果随对端记录随时变化(上方为 2026-10-21 实测值,不同时间、不同 include 展开深度下输出会有出入),命令用法与输出形态以本文为准,具体网段以当场执行结果为准。

💡 技术拓展:发信域身份校验不止 SPF 一项,主流三件套的分工——

机制校验什么失败表现
SPF信封发件域授权哪些 IP 发信域名 TXT 记录比对不通过
DKIM邮件头/正文的密码学签名签名验证失败或头被篡改
DMARC前两者结果与 From 头域的对齐策略按 p= 策略拒收/隔离

OpenSMTPD 7.9 内建 filter DSL 可做 SPF 阶段校验,DKIM/DMARC 需外挂(rspamd 等);spf walk 是运维侧的记录诊断工具,用于「这个域到底授权了哪些网段」的核对。

7.3 discover:队列信封重扫描

discover 让队列子系统从盘上重读指定消息的信封文件并重新注入调度器,接受 msgid 或 evpid,输出发现的信封数。注意:它不是「域名/MX 发现」工具(旧资料有此误传,实测确认其操作对象是队列信封)。

对在队消息实测(返回其信封数,重读幂等):

smtpctl pause mda
swaks --to root@mail.example.com --from admin@mail.example.com --server 127.0.0.1:25 --h-Subject 'discover-live' >/dev/null 2>&1
smtpctl show queue
smtpctl discover 6d74e7db

实测回显:

6d74e7db53969bb6|inet4|mda||admin@mail.example.com|root@mail.example.com|root@mail.example.com|1792579534|1792579534|0|0|pending|0|
1 envelope discovered

对已投递/不存在的消息返回 0(实测 smtpctl discover deadbeef):

0 envelopes discovered

典型用途:从备份恢复过 /var/spool/smtpd/queue 子树、或怀疑调度器内存队列与盘上文件脱节时,用 discover 把盘上信封重新拉回调度视野。清理本次实验信封:

smtpctl remove all
smtpctl resume mda

实测回显:

1 envelope removed
command succeeded

7.4 离线队列与停机期间的队列查看

OpenSMTPD 的 sendmail 兼容层在 smtpd 停止时不会报错拒信,而是把信件落成离线文件,待服务恢复后自动接管投递。完整闭环实测:

第一步,停服务后本机提交一封信:

systemctl stop opensmtpd79
echo 'offline-queue-d1-test' | sendmail root
ls -l /var/spool/smtpd/offline/

实测回显:

total 4
-rw------- 1 root _smtpq 27 Oct 21 10:40 1792579257.XXXXTYLviV

第二步,启动服务,离线信自动投递:

systemctl start opensmtpd79
journalctl -t smtpd --no-pager -n 6 | grep 'mda delivery' | tail -1

实测回显:

Oct 21 10:40:59 mail.example.com smtpd[20578]: c96d92aa87bf6f63 mda delivery evpid=75c912050fa732e0 from=<root@mail.example.com> to=<root@mail.example.com> rcpt=<root@mail.example.com> user=admin delay=0s result=Ok stat=Delivered

第三步(灾检场景):若队列中已有滞留信封时服务停了,show queue 依然可用——此时 smtpctl 连不上控制套接字,会直接读盘上 /queue 目录列出持久化信封,runstate 显示为 offline|:

smtpctl pause mda
swaks --to root@mail.example.com --from admin@mail.example.com --server 127.0.0.1:25 --h-Subject 'offline-read-test' >/dev/null 2>&1
systemctl stop opensmtpd79
smtpctl show queue

实测回显:

66be77696e0c13d4|inet4|mda||admin@mail.example.com|root@mail.example.com|root@mail.example.com|1792579335|1792579335|0|0|offline||

恢复服务并放开投递,信封照常投出:

systemctl start opensmtpd79
smtpctl resume mda
journalctl -t smtpd --no-pager -n 8 | grep 'mda delivery' | tail -1

实测回显:

Oct 21 10:42:16 mail.example.com smtpd[20679]: 01b391065d95b6bd mda delivery evpid=66be77696e0c13d4 from=<admin@mail.example.com> to=<root@mail.example.com> rcpt=<root@mail.example.com> user=admin delay=1s result=Ok stat=Delivered

这套行为意味着:停机不影响队列数据安全,盘上队列 + 离线提交文件 + show queue 离线读盘三层兜底,排障时即使服务起不来也能看清队列积压内容。

八、smtpd(8) 守护进程与 sendmail 兼容层命令

管理 OpenSMTPD 除 smtpctl 外还有两个入口:smtpd 本体的启动参数,以及 sendmail 兼容层。

8.1 smtpd 启动参数

smtpd -h 给出版本与参数全景:

smtpd -h

实测回显:

version: OpenSMTPD 7.9.0-portable
usage: smtpd [-dFhnv] [-D macro=value] [-f file] [-P system] [-T trace]

常用参数逐项说明(均实测或源码语义核实):

参数作用实测要点
-f file指定配置文件路径源码版默认 /usr/local/etc/smtpd.conf,包版 /etc/smtpd.conf
-n仅做配置语法检查后退出输出 configuration OK,改配置的第一道安全网
-d前台运行不脱离终端配 -v 加详细度,排障利器
-v详细日志常与 -d 组合为 smtpd -dv
-D macro=value命令行覆盖配置宏测试不同宏取值时免改文件
-P system启动时即暂停指定投递代理取值 smtp/mta/mda,如 -P mda 启动即冻结本地投递
-T trace启动时即打开某 trace等价启动后 smtpctl trace
-F前台运行(不 fork)配 systemd 简单托管时可考虑

smtpd -n 实测:

smtpd -n

实测回显:

configuration OK

⚠️ 注意:7.9.0p0 的 smtpd 不支持 SIGHUP 热重载——实测对主进程发 HUP 会导致进程退出(smtpd: unexpected signal)。改配置的标准闭环是 smtpd -n 语法检查 → systemctl restart → show status 确认,完整排障实录见 《OpenSMTPD 7.9 配置参数详解》3.2 节。

8.2 sendmail 兼容层:sendmail / newaliases / mailq

安装后系统里存在指向 smtpctl 的兼容符号链接,本机程序(cron、PHP mail() 等)无需改造即可投信。实测本环境(源码版 + 曾装过 apt 包版的服务器):

ls -l /usr/local/sbin/sendmail /usr/sbin/sendmail /usr/sbin/newaliases
ls -l /usr/sbin/smtpctl

实测回显:

lrwxrwxrwx 1 root root 23 Oct 21 00:50 /usr/local/sbin/sendmail -> /usr/local/sbin/smtpctl
lrwxrwxrwx 1 root root  7 Nov 29  2021 /usr/sbin/sendmail -> smtpctl
lrwxrwxrwx 1 root root  7 Nov 29  2021 /usr/sbin/newaliases -> smtpctl
-rwxr-sr-x 1 root opensmtpq 174904 Nov 29  2021 /usr/sbin/smtpctl

要点解读:

  • smtpctl 按程序名分支行为(argv[0] 机制):名为 sendmail 时走提交逻辑(含停机时的离线落盘,见 7.4),名为 newaliases 时重建别名库,名为 mailq 时等价 smtpctl show queue(本环境源码版未建 mailq 链接,包版提供);
  • 曾装过发行版包的服务器上并存两套 smtpctl:/usr/sbin/smtpctl(包版)与 /usr/local/sbin/smtpctl(源码版)。PATH 决定裸 smtpctl 命中谁,实测:
which smtpctl

实测回显:

/usr/local/sbin/smtpctl

⚠️ 注意:两套版本的 smtpctl 操作的是同一个 /var/spool/smtpd 队列,但二进制逻辑有版本差。运维脚本中一律写绝对路径指向你实际运行的那一套(本文统一 /usr/local/sbin/smtpctl),避免 PATH 差异导致「命令跑了、对端不是这版」的隐形错位。

8.3 smtpctl stop 已移除(排障实录)

旧版资料(6.x 时代)常用 smtpctl stop 停服务。7.9.0p0 中该子命令已不存在,实测执行只会得到子命令清单,服务不受任何影响:

smtpctl stop
systemctl is-active opensmtpd79

实测回显:

possibilities are:
discover
pause
resume
show
log
monitor
profile
remove
schedule
trace
unprofile
untrace
update
encrypt
spf
active

正确停止方式是 systemd:systemctl stop opensmtpd79(对应服务名按实际单元)。这同时说明 2.1 的 possibilities are: 清单是完整闭集——清单里没有的子命令就是不存在,不必再翻旧文档。

九、注意事项

汇总使用中的高频坑位,每条都有实测依据:

  1. pause/resume 不持久化:职能级暂停、域级阻断在 restart 后全部回到 running/清空。依赖「重启期间冻结」的流程要在重启后重新执行 pause。
  2. monitor 只适合交互使用:管道/文件重定向场景因 stdio 全缓冲 + timeout 截断会拿到空输出(6.2 排障实录)。采集类需求用 show stats 前后快照取差值。
  3. show envelope 只认完整 evpid:传 8 位 msgid 会得到 possibilities are: show envelope <evpid> 用法提示(9.1);而 show message 两.ID 都收。记法:envelope 看信封(必须指到具体信封),message 看内容(消息级即可)。
  4. 两套 smtpctl 并存的服务器写绝对路径:见 8.2,PATH 命中错版本是隐形事故源。
  5. schedule 不解除 suspend:被 pause envelope 钉住的信封 schedule 无效,先 resume envelope。
  6. remove 不产生退信:需要让发件人收到失败通知时,走 ttl 过期的 bounce 流程(队列行第 9 列过期时间),不要用 remove 兜底。
  7. trace/profile 用完即关:trace 打开后调试行持续写日志(smtpctl untrace <子系统> 关闭),trace all 的量级足以刷爆日志盘。
  8. smtpctl 需 setgid _smtpq:手工补权限时用 chgrp _smtpq smtpctl && chmod g+s smtpctl,丢了该位任何队列类命令都跑不起来。
  9. 停机排障有兜底:服务起不来时 show queue 仍能离线读盘列队列(7.4),结合 discover 可把盘上信封重新纳入调度。

十、常见问题(FAQ)

Q1:smtpctl show envelope c8a469af 为什么报 possibilities are: show envelope <evpid>? show envelope 只接受 16 位完整 evpid。8 位 msgid 先用 smtpctl show queue(或日志里的 evpid 字段)换算:msgid 的 16 位形态就是队列行第 1 列。本文 4.4 有正确用法与完整回显。

Q2:timeout 5 smtpctl monitor > /tmp/out 采集到的文件是空的? stdio 全缓冲导致样本滞留在缓冲区,timeout 杀进程时未刷新。monitor 设计为交互使用;自动化采集改用两次 show stats 快照做差(6.2)。

Q3:smtpd 停止时 smtpctl show queue 会报错吗? 不会。实测退出码 0:队列空时无输出;盘上 /queue 有信封时以 offline| 状态列出(7.4 实测)。只有 smtpctl 本身缺 setgid 位或队列目录损坏才会报错。

Q4:smtpctl pause mda 之后重启服务,怎么又投递了? pause 不持久化,restart 后三职能回到 running(5.1 排障实录)。重启后需重新 pause。

Q5:smtpctl profile bogus / smtpctl trace bogus 报错是什么口径? 两者都是 smtpctl: invalid <profile|trace> keyword: bogus、退出码 1。合法关键字:profile 仅 imsg/queue;trace 共 13 个(6.3 表)。

Q6:普通用户执行 smtpctl show queue 报 need root privileges? 特权命令模型(2.2)。用 root 或 sudo 执行;encrypt、spf walk 两个非特权命令除外。

Q7:老资料里的 smtpctl stop 怎么不可用? 7.9.0p0 已移除该子命令(8.3 实测,服务不受影响)。停止/重启一律走 systemd 单元。

Q8:想确认某封信卡在队列的原因,按什么顺序查? show queue 看 runstate(pending 负值=欠调度,inflight=在投,suspend=被钉住)→ 第 13 列错误行 → show envelope <evpid> 看 mda-user/dest 是否解析对了 → 必要时 trace mda/trace mta 展开投递过程 → 修复后 schedule 提前重试或 remove 清理。

十一、参考链接

  1. smtpctl(8) 官方手册:https://man.openbsd.org/smtpctl
  2. smtpd(8) 官方手册:https://man.openbsd.org/smtpd
  3. OpenSMTPD 官方手册索引:https://www.opensmtpd.org/manual.html
  4. portable 版源码仓库(本文命令注册表与字段语义的取证来源):https://github.com/OpenSMTPD/OpenSMTPD
  5. 7.9.0p0 发布说明:https://github.com/OpenSMTPD/OpenSMTPD/releases/tag/7.9.0p0
  6. 站内参考:《OpenSMTPD 7.9.0p0 介绍与选型指南》、《Ubuntu 22.04 安装 OpenSMTPD 指南》、《OpenSMTPD 7.9 配置参数详解》、《Mox v0.0.17 介绍与选型指南》(同赛道邮件服务器)。