- 后端
- 网络/通信
【免费下载链接】Haraka
A fast, highly extensible, and event driven SMTP server
导读
本文围绕 Haraka(一个快速、高扩展、事件驱动的开源 SMTP 服务器)内置的status插件展开。该插件允许运维人员在**本地回环(localhost)**发起STATUS命令,实时获取出站连接池(pool)与投递队列(queue)的内部状态,甚至可以直接对队列文件执行“丢弃(DISCARD)”或“立即重投(PUSH)”操作。读完本文,你将掌握全部 5 条 STATUS 子命令的用法、返回的 JSON 字段含义、集群(cluster)模式下的合并语义,以及对应源码实现与测试用例,能够立即上手对生产环境中的 Haraka 实例做状态巡检与队列干预。
1. 插件定位:本机运维的“后门”
status插件由 plugins/status.js 实现,其设计目标非常明确:只对来自本机(localhost)的连接开放,让管理员无需外部工具即可通过标准 SMTP 会话读取 Haraka 内部运行状态。
这一点在源码中体现得淋漓尽致:
- 在
hook_capabilities钩子中,仅当connection.remote.is_local为真时,才向会话通告STATUS扩展能力(plugins/status.js); - 在
hook_unrecognized_command钩子中,任何不以STATUS开头的命令都直接放行;而远程发来的STATUS命令会被拒绝(返回DENY),拒绝理由是'STATUS not allowed remotely'(plugins/status.js)。
关于“本机”的判定:Haraka 会在连接方 IP 写入remote.ip时自动调用net_utils.is_local_ip()计算remote.is_local,127.0.0.1 / ::1 等回环地址自然命中(见 connection.js)。
因此,启用该插件前请确认两个前提:
- 该插件在 config/plugins 中未被注释(默认配置里
# status是注释状态,需手动启用); - 你的监控与运维手段均从本机发起(如本机 cron、本机脚本、
nc localhost 25等)。
对应的访问控制测试用例见 test/plugins/status.js:当remote.is_local = false时,STATUS POOL LIST必须返回DENY。
2. 通信协议:一行命令,一段 JSON
status插件复用了 SMTP 会话本身,通过未识别命令钩子(hook_unrecognized_command)拦截STATUS开头的一行文本,交互格式如下:
- 请求:
STATUS <CMD> [param1] [param2].... - 响应:
<SMTP code 211 或 500><空格><JSON 编码响应>\r\n
命令成功时服务器返回211(system status 类别的 SMTP 应答码),其后紧跟 JSON 字符串;命令无法识别时返回500。文档给出的完整会话示例:
< 220 example.com ESMTP Haraka ready > STATUS QUEUE INSPECT < 211 {"delivery_queue":[],"temp_fail_queue":[]}从源码看,响应体由connection.respond(211, result ? JSON.stringify(result) : 'null', () => next(OK))发出(plugins/status.js);当插件内部执行出错(如未知命令)时,则通过next(DENY, err.message)回以 SMTP 拒绝应答。
命令解析入口为command_action:按空格切分参数,第一段必须是POOL或QUEUE,否则回调'unknown STATUS command'(plugins/status.js)。
实战提示:由于响应是 JSON,建议用 Python、jq 或 Node 脚本解析,例如
printf 'STATUS QUEUE STATS\r\nQUIT\r\n' | nc localhost 25即可在一条命令里拿到队列统计。
3. 可用命令清单
下表汇总了status插件支持的全部子命令(官方文档原表,逐条展开):
| 命令 | 作用 | 返回示例 |
|---|---|---|
STATUS POOL LIST | 以host:port为键返回当前活跃的出站连接池映射 | {"mx.example.com:25":{"inUse":0,"size":3}} |
STATUS QUEUE STATS | 队列统计,格式为"<in_progress>/<delivery_queue 长度>/<temp_fail_queue 长度>" | "1/2/0" |
STATUS QUEUE LIST | 列出磁盘上的队列文件,附带 uuid、domain、mail_from、rcpt_to 等属性 | [{"file":"...","uuid":"...","domain":"...","from":"<...>","to":["<...>"]}] |
STATUS QUEUE INSPECT | 合并返回outbound.delivery_queue与outbound.temp_fail_queue的全部内容 | {"delivery_queue":[{"id":"..."}],"temp_fail_queue":[{"id":"...","fire_time":123}]} |
STATUS QUEUE DISCARD file | 停止投递指定的队列邮件文件 | "OK" |
STATUS QUEUE PUSH file | 立即尝试重新投递指定邮件 | "OK" |
命令解析流程(POOL/QUEUE两级 switch)与完整命令分支均可在 plugins/status.js 中核对。
4. 子命令逐条深度解析
4.1STATUS POOL LIST:连接池快照
作用:返回以host:port为键的活跃出站连接池映射。实现位于pool_list(plugins/status.js):
const result = {} if (server.notes.pool) { for (const name of Object.keys(server.notes.pool)) { const instance = server.notes.pool[name] result[name] = { inUse: instance.inUseObjectsCount(), size: instance.getPoolSize(), } } }每个池条目包含两个指标:
inUse:当前被占用的连接数(正在投递邮件、从池中借出的 socket);size:该池的总容量(池大小由 outbound 配置决定)。
这两个指标直接来自连接池对象(server.notes.pool中按host:port命名的池实例)。当 Haraka 尚未建立任何出站连接时,返回空对象{}。
使用场景:排查“某目标域投递拥塞”时,对比inUse与size,若inUse长期等于size,说明该目标的并发连接已打满,需要结合 outbound.ini 中的pool_size等参数调整。
4.2STATUS QUEUE STATS:三段式队列统计
作用:返回形如"in_progress/delivery_queue/temp_fail_queue"的字符串。实现直接调用outbound.get_stats()(plugins/status.js),其底层逻辑位于 outbound/queue.js:
let in_progress = 0 const delivery_queue = new Queue(async (hmail) => { /* 实际投递,in_progress++/-- */ }) const temp_fail_queue = new TimerQueue(1000, { logger }) exports.get_stats = () => `${in_progress}/${exports.delivery_queue.length()}/${exports.temp_fail_queue.length()}`三个数字的含义:
- in_progress:正在投递中的邮件数(进入
delivery_queue工作函数并尚未回调完成的 hmail 数); - delivery_queue 长度:等待投递(排队中 + 运行中)的邮件数,即 outbound/queue.js 中
tasks.length + running; - temp_fail_queue 长度:临时失败、等待重试的邮件数,底层是
TimerQueue(定时器队列,默认以 1 秒为刻度调度重试)。
使用场景:快速判断服务器“当前活跃投递量”与“重试积压量”。配合 outbound.ini 的concurrency_max、temp_fail_period等参数可评估是否需要扩容或调优重试节奏。
测试用例验证了返回格式必须匹配正则^\d+\/\d+\/\d+$(test/plugins/status.js)。
4.3STATUS QUEUE LIST:磁盘队列文件清单
作用:列出磁盘上(queue_dir目录)的全部队列文件及其信封元数据。实现位于queue_list(plugins/status.js),底层调用outbound.list_queue()→ outbound/queue.js 的_load_cur_queue,逐个读取队列文件头部(_list_file,见 outbound/queue.js)。每个条目包含:
| 字段 | 含义 |
|---|---|
file | 队列文件名(如1507509981169_..._haraka) |
uuid | 邮件唯一标识(来自事务 UUID,TODO 对象属性) |
queue_time | 入队时间戳(毫秒) |
domain | 目标投递域 |
from | 信封发件人mail_from.toString() |
to | 信封收件人数组(rcpt_to.map(r => r.toString())) |
关于文件名格式:队列文件遵循$arrival_$nextattempt_$attempts_$pid_$uniquetag_$counter_$host的结构,解析规则见 outbound/qfile.js,其中next_attempt(下次尝试时间)与attempts(失败次数)正是重试调度的依据。TODO 头部的字段定义见 outbound/todo.js。
使用场景:结合 test/queue 目录下的真实队列文件(如1507509981169_1507509981169_0_61403_e0Y0Ym_1_fixed)即可直观对照字段格式。注意QUEUE LIST读取的是磁盘,语义与下面的实时QUEUE INSPECT不同。
4.4STATUS QUEUE INSPECT:实时队列内容
作用:返回内存中delivery_queue与temp_fail_queue的合并内容。实现位于queue_inspect(plugins/status.js):
cb(null, { delivery_queue: delivery_queue_items.map((hmail) => ({ id: hmail.file })), temp_fail_queue: fail_queue_items.map((tqtimer) => ({ id: tqtimer.id, fire_time: tqtimer.fire_time, })), })字段含义:
delivery_queue:正在/即将投递的邮件数组,每项含id(即队列文件名);temp_fail_queue:等待重试的邮件数组,每项含id(文件名)与fire_time(计划触发投递的时间戳)。
测试用例(test/plugins/status.js)先向temp_fail_queue添加两条定时任务,再断言 INSPECT 返回delivery_queue长度为 0、temp_fail_queue长度为 2。
与QUEUE LIST的区别:INSPECT 反映的是进程内存中的实时状态,仅包含正在处理或等待重试的邮件;LIST 反映的是磁盘上的全部队列文件,如果已投递成功的文件尚未被清理,LIST 仍会将其列出。
4.5STATUS QUEUE DISCARD file:丢弃待投递邮件
作用:停止投递指定队列文件。实现位于queue_discard(plugins/status.js),执行两步操作:
- 调用
outbound.temp_fail_queue.discard(file)将该文件从重试队列中移除(未命中时静默忽略错误); fs.unlink(path.join(this.queue_dir || '', file))删除磁盘上的队列文件。
queue_dir在插件注册时取自outbound/queue模块(plugins/status.js),其解析规则为:优先config.get('queue_dir'),其次$HARAKA/queue,最后回退到test/test-queue(outbound/queue.js)。
使用场景:管理员判定某封邮件投递无意义(如地址永久失效)时,直接丢弃避免反复重试。测试用例验证 DISCARD 后 INSPECT 中的temp_fail_queue长度从 2 变为 1,且被丢弃任务的回调不再触发(test/plugins/status.js)。
4.6STATUS QUEUE PUSH file:立即重投
作用:尝试立即重新投递指定邮件。实现位于queue_push(plugins/status.js):在temp_fail_queue.queue中按id查找该文件,找到后将其从队列中摘除并立即执行其回调item.cb()(该回调会把邮件重新送入投递流程),随后响应"OK"。
使用场景:当目标域故障恢复后,管理员希望跳过剩余退避时间、立刻重试重要邮件时使用。测试用例验证:添加一个 1500ms 后触发的定时任务,立即 PUSH 后其回调马上执行(test/plugins/status.js)。
注意:PUSH 与 DISCARD 均要求参数为磁盘队列文件名(即 INSPECT / LIST 返回的
id/file)。如果文件只存在于内存队列而磁盘文件已被清理,PUSH 会找不到对应项但依然返回"OK",这一点从实现逻辑可以推断,使用时需以 INSPECT 结果交叉确认。
5. 集群模式下的聚合语义
在 cluster(多进程)模式下,各 worker 进程各自持有独立的delivery_queue、temp_fail_queue与连接池,status插件通过进程间消息(IPC)把查询路由到主进程,再由主进程广播给所有 worker 并合并结果。路由逻辑见run(plugins/status.js):
if (server.cluster && !/^QUEUE LIST/.test(cmd)) { this.call_master(cmd, cb) // 其余命令经主进程汇总 } else { this.command_action(cmd, cb) // 非集群,或 QUEUE LIST 直接本进程执行 }聚合策略由merge_worker_responses实现(plugins/status.js),与官方文档表述一一对应:
| 命令 | 集群聚合方式 |
|---|---|
POOL LIST | 所有 worker 的池映射合并进一个对象(Object.assign({}, ...results)) |
QUEUE STATS | 各 worker 的N/N/N计数器逐位求和,输出单个"N/N/N"字符串 |
QUEUE INSPECT | 各 worker 的delivery_queue与temp_fail_queue数组首尾拼接 |
QUEUE LIST | 始终在主进程执行(因它读取所有 worker 共享的磁盘队列文件) |
三条聚合规则的测试覆盖位于 test/plugins/status.js,例如['1/2/3', '0/1/0', '2/0/1']求和为'3/3/4';INSPECT 的delivery_queue/temp_fail_queue数组会跨 worker 顺序拼接。
IPC 机制方面:主进程通过hook_init_master监听status.request事件并广播(call_workers使用Promise.allSettled,超时未响应的 worker 被过滤掉);worker 通过hook_init_child处理请求并回传结果;单次 worker 请求设有1000ms 超时(call_worker中的setTimeout,见 plugins/status.js)。这些细节可从源码结构推断,对排查“集群下状态查询偶发缺失”很有价值。
非集群模式则无此开销:所有命令都在当前进程内直接执行。
6. 快速启用与使用示例
6.1 启用插件
编辑 config/plugins,取消status所在行的注释,随后重启 Haraka(或执行haraka -c /path/to/haraka/config重新加载)使配置生效。可用haraka -l查看已安装插件列表、haraka -h status查看插件文档。
6.2 交互式使用(telnet / nc)
$ nc localhost 25 220 example.com ESMTP Haraka ready STATUS QUEUE STATS 211 "1/2/0" STATUS QUEUE LIST 211 [{"file":"1507509981169_1507509981169_0_61403_e0Y0Ym_1_haraka","uuid":"...","queue_time":1507509981169,"domain":"example.org","from":"<sender@example.com>","to":["<rcpt@example.org>"]}] QUIT 221 2.0.0 Bye6.3 脚本化监控
# 队列积压监控(每分钟一次) echo -e "STATUS QUEUE STATS\r\nQUIT\r\n" | nc -w 5 localhost 25 | grep '^211' # 丢弃一封确定无意义的待投递邮件 echo -e "STATUS QUEUE DISCARD 1507509981169_1507509981169_0_61403_e0Y0Ym_1_haraka\r\nQUIT\r\n" | nc -w 5 localhost 25 # 目标域恢复后立即重投 echo -e "STATUS QUEUE PUSH 1508269674999_1508269674999_0_34002_socVUF_1_haraka\r\nQUIT\r\n" | nc -w 5 localhost 25以上队列文件名示例可对照 test/queue 下的真实文件命名验证格式。
6.4 运行测试
仓库自带完整的插件测试套件,可执行:
npm test -- test/plugins/status.js或运行 run_tests 脚本执行全部测试,验证包括访问控制、五个子命令行为、集群合并逻辑在内的所有用例(见 test/plugins/status.js)。
7. 总结与注意事项
- 安全性:
STATUS仅对本地连接开放(remote.is_local判定),远程一律DENY;如需远程运维,应先通过本机隧道或 ACL 限制访问,切勿将其直接暴露到公网。 - 数据来源差异:
POOL LIST、QUEUE STATS、QUEUE INSPECT反映内存实时状态;QUEUE LIST反映磁盘文件状态,可能包含尚未清理的已投递邮件。 - 集群语义:前三者自动聚合所有 worker 结果,
QUEUE LIST固定由主进程执行。 - 干预类命令:
DISCARD会删除磁盘队列文件且无法恢复;PUSH只对仍在temp_fail_queue中的文件生效,请先 INSPECT 确认目标id再操作。 - 响应码:成功为 211,无法识别的命令按 SMTP 错误应答返回。
相关资源
- 插件实现:plugins/status.js
- 队列与统计实现:outbound/queue.js、outbound/index.js
- 队列文件格式:outbound/qfile.js、outbound/todo.js
- 连接池:outbound/client_pool.js
- 测试用例:test/plugins/status.js
- 插件启用配置:config/plugins
- 后端
- 网络/通信
【免费下载链接】Haraka
A fast, highly extensible, and event driven SMTP server
相关推荐
Apache APISIX node-status 插件:通过内置接口实时获取 Nginx 连接状态
Apache APISIX node status 插件:通过内置接口实时获取 Nginx 连接状态 node status 是 Apache APISIX 内
API网关后端云原生微服务文档模板复制API:ONLYOFFICE Docs创建模板的终极指南
文档模板复制API:ONLYOFFICE Docs创建模板的终极指南 ONLYOFFICE Docs是一款功能强大的开源在线办公套件,提供文档、电子表格、演示文
agents24 仓库 Agent Teams 插件实战:详解 /team-status 命令的团队成员、任务状态与进度监控
agents24 仓库 Agent Teams 插件实战:详解 /team status 命令的团队成员、任务状态与进度监控 在 Agent Teams 插件
AI 插件AI 技能开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考