news 2026/10/6 1:56:51

Haraka STATUS 插件实战指南:用 SMTP 命令实时洞察出站队列与连接池状态

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Haraka STATUS 插件实战指南:用 SMTP 命令实时洞察出站队列与连接池状态
  • 后端
  • 网络/通信

【免费下载链接】Haraka

A fast, highly extensible, and event driven SMTP server

项目地址:https://gitcode.com/gh_mirrors/ha/Haraka
点击查看免费下载

导读

本文围绕 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)。

因此,启用该插件前请确认两个前提:

  1. 该插件在 config/plugins 中未被注释(默认配置里# status是注释状态,需手动启用);
  2. 你的监控与运维手段均从本机发起(如本机 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()}`

三个数字的含义:

  1. in_progress:正在投递中的邮件数(进入delivery_queue工作函数并尚未回调完成的 hmail 数);
  2. delivery_queue 长度:等待投递(排队中 + 运行中)的邮件数,即 outbound/queue.js 中tasks.length + running;
  3. 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),执行两步操作:

  1. 调用outbound.temp_fail_queue.discard(file)将该文件从重试队列中移除(未命中时静默忽略错误);
  2. 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 Bye

6.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

项目地址:https://gitcode.com/gh_mirrors/ha/Haraka
点击查看免费下载
上一篇:ngx-admin 菜单状态管理:NgRx 与本地存储同步
下一篇:SDRPlusPlus 接收 GSM-R 信号实战教程

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/6 1:54:34

Java基础面试:默认值、引用与boolean大小,哪些说法容易背错?

原笔记整理了语言特点、面向对象、八种基本类型、命名和 instanceof。大方向可以保留&#xff0c;但几个答案把不同层次混在一起&#xff1a;字段的默认值套到局部变量、字节码的表示套到内存大小、语言风格套到性能高低。 这次按 Java 17 规范核对&#xff0c;并用完整程序和…

作者头像 李华
网站建设 2026/10/6 1:47:58

Rust By Example 精讲:HashMap 与 HashSet 键值容器实战指南

文档教程 【免费下载链接】rust-by-example Learn Rust with examples (Live code editor included) 项目地址&#xff1a; https://gitcode.com/gh_mirrors/ru/rust-by-example 点击查看 免费下载 HashMap 是 Rust 标准库中最常用的键值存储容器&#xff0c;它以哈希表为底层…

作者头像 李华