- 后端
- 网络/通信
【免费下载链接】Haraka
A fast, highly extensible, and event driven SMTP server
导读:queue/smtp_bridge是 Haraka 邮件服务器中的一个队列插件,它的核心职责是把入站连接(inbound connection)上已经认证成功的用户名与密码原样转发给另一台 SMTP 服务器,并将整封邮件投递到该服务器,从而在不依赖配置文件存储凭据的前提下实现“认证即转发”。本文将以 docs/plugins/queue/smtp_bridge.md 为骨架,结合插件源码、配套认证插件与测试用例,完整讲解它的工作模型、配置方法、实现原理以及与smtp_proxy、smtp_forward的差异,读完后你将能够独立部署一套基于“客户端 AUTH 凭证桥接”的转发链路。
插件定位:认证转发而非固定凭据转发
根据官方文档,queue/smtp_bridge负责向另一台 SMTP 服务器投递邮件(deliver to another SMTP server),桥接来自初始连接(initial connection)的认证细节与邮件正文数据:
- 它被设计为与认证插件
auth/auth_bridge配套使用(“This plugin is meant to be used with the pluginauth/auth_bridge”),两者共享同一份配置文件config/smtp_bridge.ini; - 它与
queue/smtp_proxy、queue/smtp_forward的本质区别在于:后两者从配置文件读取auth_user/auth_pass等静态凭据来向远端服务器认证,而smtp_bridge完全不读取配置文件中的 AUTH 细节,它只是“简单地使用原始连接的 AUTH 细节(original AUTH details)把邮件正文投递到远端 SMTP 服务器”; - 也就是说,这台 Haraka 在此场景下扮演的是一个**“信任上游客户端认证结果”的中继角色**:客户端用什么账号密码在 Haraka 上完成了 AUTH,Haraka 就用同一组账号密码去远端服务器完成认证并投递。
与之配套的 auth/auth_bridge 插件则负责认证侧的工作:将客户端的用户名/密码桥接到远端 SMTP 服务器,再把远端返回的认证结果代理回给客户端。它不需要用户采用user@domain.com格式,也不检查域名是否存在于配置中(这正是它与auth/auth_proxy的差异),因此非常适合与smtp_bridge组成一条整条链路都复用同一组客户端凭据的转发通道。
配置说明:config/smtp_bridge.ini
queue/smtp_bridge的配置存放在config/smtp_bridge.ini,采用 INI 风格格式,仓库中给出的默认示例文件(config/smtp_bridge.ini)内容如下:
host=localhost #port= #auth_type= #priority=10各配置项的含义如下:
| 配置项 | 默认值 | 说明 |
|---|---|---|
host | 无(唯一必填项) | 你要进行认证并投递(authenticating and posting)的目标主机,例如smtp.host.tld、192.0.2.25:587或[2001:db8::1]:587。参考 auth/auth_bridge 中的说明,host 支持haraka-net-utils的 Endpoint 格式;IPv6 地址若想附带端口,必须加方括号,如[2001:db8::1]:587 |
port | 空,此时 Haraka 使用25 | 目标服务器端口。仅当host中未包含端口时生效;若host中已写明端口(如192.0.2.25:587),则以 host 内嵌端口为准 |
auth_type | 空,此时 Haraka 会尝试自动挑选合适的认证方法 | 指定向远端服务器发送的 SMTP AUTH 类型 |
priority | 10 | 用于hook_get_mx返回的 MX 优先级 |
注意:
host是唯一必需的设置(“This is the only setting required”),其余三项按需启用即可。配置解析由 plugins/queue/smtp_bridge.js 中的load_flat_ini完成,它通过this.config.get('smtp_bridge.ini', ...)加载并注册了重载回调,因此修改配置文件后 Haraka 会热重载(SIGHUP / 配置重载机制),无需重启进程。
工作流程:两个 Hook 完成“复制凭据 + 覆盖 MX”
queue/smtp_bridge只注册了两个 Hook,所有逻辑都非常精简(plugins/queue/smtp_bridge.js):
1.hook_data_post:把连接级凭据复制进事务
exports.hook_data_post = (next, connection) => { const txn = connection?.transaction if (!txn) return next() // Copy auth notes to transaction notes so they're available in hmail.todo.notes txn.notes.auth_user = connection.notes.auth_user txn.notes.auth_passwd = connection.notes.auth_passwd return next() }这一步发生在邮件正文接收完成之后:它把保存在连接对象(connection.notes)上的认证用户名与密码复制到事务(txn.notes)。注释中明确写明了意图:复制到 transaction notes 是为了让它们最终出现在出站邮件条目hmail.todo.notes中,供出站投递阶段使用。如果当前没有事务(如连接在 DATA 阶段前被丢弃),则直接放行。
2.hook_get_mx:覆盖 MX 解析,把投递目标指到配置的主机
exports.hook_get_mx = function (next, hmail) { let priority = 10 if (this.cfg.main.priority) { priority = this.cfg.main.priority } let authType = null if (this.cfg.main.auth_type) { authType = this.cfg.main.auth_type } let port = null if (this.cfg.main.port) { port = this.cfg.main.port } return next(OK, { priority, exchange: this.cfg.main.host, port, auth_type: authType, auth_user: hmail.todo.notes.auth_user, auth_pass: hmail.todo.notes.auth_passwd, }) }- 该方法在 Haraka 出站投递查询 MX 时被调用,并直接以
OK返回一个 MX 对象,从而完全跳过 DNS MX 查询; - 返回对象中的
exchange固定为配置文件中的host; auth_user/auth_pass取自hmail.todo.notes——这正是hook_data_post预先复制进去的那组客户端凭据,实现了“原始连接的 AUTH 细节随邮件一起走到投递阶段”;priority、port、auth_type分别对应配置项,未配置时返回10/null/null(源码中的默认值清晰可见)。
配合 outbound/hmail.js 中get_mx_respond的处理逻辑可以确认:当get_mx钩子返回OK与 MX 对象时,出站系统会直接使用该对象建立连接并尝试投递,而不再执行 DNS MX 解析。
出站认证的底层实现:从“自动选型”到“AUTH 命令”
当queue/smtp_bridge通过hook_get_mx把认证凭据带入 MX 对象后,出站投递的实际认证动作发生在 outbound/hmail.js 的auth_and_mail_phase()函数中,这里可以印证文档所述“auth_type 留空时 Haraka 会自动挑选合适的方法”:
- 只有当
mx.auth_user与mx.auth_pass都存在时才发起 AUTH(否则直接进入MAIL阶段); - 若远端服务器未在 EHLO 能力中通告
AUTH,会记录 warning 并不认证直接投递; - 若未显式配置
auth_type,Haraka 的自动选型优先级为:CRAM-MD5 > PLAIN > LOGIN(源码注释说明 CRAM-MD5 最安全优先、PLAIN 往返次数少次之、LOGIN 兜底); - 选定的类型与远端通告的能力不匹配时,同样降级为“不认证直接投递”;
- 认证成功进入
MAIL阶段;AUTH LOGIN/AUTH CRAM-MD5属于多轮交互,出站代码会基于远端返回的挑战串(VXNlcm5hbWU6即 base64 的 “Username:”、UGFzc3dvcmQ6即 “Password:”)依次回填 base64 的用户名与密码(详见process_ehlo_data与line事件处理中的case 'auth'分支)。
也就是说,即便配置中不写auth_type,只要远端支持上述任一种标准 SASL 机制,桥接认证也能自动完成;当然你也可以通过auth_type=显式锁定某一种。
认证侧配套:auth/auth_bridge如何桥接客户端 AUTH
要理解整条链路,还需要看认证侧。客户端在 Haraka 上完成 SMTP AUTH 时,认证插件会把结果写入connection.notes.auth_user与connection.notes.auth_passwd(plugins/auth/auth_base.js 中认证成功后执行connection.notes.auth_user = safe_user、connection.notes.auth_passwd = credentials[1];也可以通过plugin.blankout_password = true禁止密码写入)。queue/smtp_bridge的hook_data_post正是从这里取数。
而 plugins/auth/auth_bridge.js 实现了check_plain_passwd:
exports.check_plain_passwd = function (connection, user, passwd, cb) { const { host, port } = this.cfg.main let ep try { ep = net_utils.Endpoint.parse(host, port || 25) } catch (err) { connection.logerror(this, `invalid host: ${err.message}`) return cb(false) } this.try_auth_proxy(connection, `${ep}`, user, passwd, cb) }- 它读取的是同一份
smtp_bridge.ini(this.config.get('smtp_bridge.ini', ...)),因此host与port在认证侧与投递侧保持一致; port缺省时按 25 处理;host 非法时记录错误并拒绝认证;- 核心动作是调用继承自
auth/auth_proxy的try_auth_proxy:向目标主机发起一次 SMTP 会话,若远端通告STARTTLS则先升级 TLS(机会式加密,本地 key/cert 仅在配置了互认证时才附带),然后优先用AUTH PLAIN、其次AUTH LOGIN提交客户端传来的原始user/passwd,把远端结果作为 Haraka 端认证的最终结果返回。
由此形成完整闭环:客户端 → (AUTH 桥接到远端) → 认证通过 → 邮件正文收完后 → 凭据复制进事务 → 出站时覆盖 MX 指向同一主机 → 用同一组凭据认证并投递。
测试验证:行为已被测试用例锁定
仓库中的单元测试 test/plugins/queue/smtp_bridge.js 完整覆盖了上述行为,可作为实现事实的佐证:
hook_data_post组:无事务时直接next();有事务时把connection.notes.auth_user/auth_passwd复制到transaction.notes;连接上未设置认证值时复制undefined;hook_get_mx组:默认priority=10且exchange为配置的 host;配置priority=20时返回 20;配置auth_type: 'PLAIN'时透传;配置port: '587'时透传;auth_user/auth_pass从hmail.todo.notes透传;未配置port/auth_type时返回null。
这些断言与文档中的默认值描述(port默认空、auth_type默认空、priority默认 10)逐条对应,说明文档所述行为有测试兜底。
与queue/smtp_proxy、queue/smtp_forward的对比
文档特别强调了三者的区别。结合 docs/plugins/queue/smtp_proxy.md 与 docs/plugins/queue/smtp_forward.md 及对应源码,可归纳如下:
| 维度 | queue/smtp_bridge | queue/smtp_proxy | queue/smtp_forward |
|---|---|---|---|
| AUTH 凭据来源 | 原始客户端的 AUTH 细节(经hook_data_post复制进事务 notes) | 配置文件中的auth_user/auth_pass | 配置文件中的auth_user/auth_pass(支持按域路由) |
| 建立上游连接的时机 | 出站投递阶段(覆盖get_mx,绕过 DNS) | MAIL FROM 时即建立连接(见 smtp_proxy.js),可即时获得远端收件人过滤等 SMTP 阶段过滤能力 | 队列(queue)时才建立连接(见 smtp_forward.js),适合搭配内容过滤减少连接数,但收件人校验被推迟到队列阶段 |
| 典型用途 | 复用客户端凭证的认证转发链路 | 前向到具备成熟外发能力的邮件服务器 | 前向到具备成熟外发能力的邮件服务器 |
简单来说:smtp_proxy/smtp_forward解决的是“固定凭据/固定路由”的前向转发,而smtp_bridge解决的是“把客户端已经认证过的身份原样桥接给下一跳”,两者适用场景不同,可按需组合或互斥使用。
启用与部署步骤
在 Haraka 中启用该插件与配套认证插件的步骤如下:
- 在 config/plugins 中启用两个插件:确认包含
auth/auth_bridge与queue/smtp_bridge(可参考现有插件列表文件中的注释格式;启用队列插件的同时应禁用或协调其他queue类插件,避免多个队列插件冲突); - 编辑 config/smtp_bridge.ini:至少设置
host=指向目标 SMTP 服务器(唯一必填项),按需开启port=、auth_type=、priority=; - 配置 TLS:若远端服务器支持
STARTTLS,机会式加密会在认证代理与出站投递阶段自动启用(前提是 Haraka 的 tls.ini 已正确配置证书与密钥); - 重载配置:修改
smtp_bridge.ini后由 Haraka 的配置重载机制(SIGHUP)自动生效,两个插件共享同一份配置与重载回调; - 验证:运行测试套件(如
npm test下的 test/plugins/queue/smtp_bridge.js)可确认插件行为;实际部署时可通过观察 Haraka 日志中的 AUTH 与投递记录,确认客户端凭据被原样用于远端认证。
小结
queue/smtp_bridge是一个设计极简但语义清晰的桥接型队列插件:它不接触任何静态凭据,而是通过hook_data_post把客户端认证身份带入事务、通过hook_get_mx把投递目标固定到配置主机,与auth/auth_bridge协同完成“客户端 AUTH 凭证即转发凭据”的完整闭环。相比smtp_proxy/smtp_forward的静态凭据转发,这种模式更适合那些希望用户用同一组账号在 Haraka 与后端邮件系统之间无缝认证的场景,也是理解 Haraka 认证体系与出站投递体系如何协作的一个极佳切入点。
- 后端
- 网络/通信
【免费下载链接】Haraka
A fast, highly extensible, and event driven SMTP server
相关推荐
Haraka queue/smtp_forward 插件:SMTP 服务器间转发与多域路由实战指南
Haraka queue/smtp_forward 插件:SMTP 服务器间转发与多域路由实战指南 导读 queue/smtp_forward 是 Haraka
后端网络/通信Haraka auth/auth_proxy 插件:按域名代理 AUTH 认证到远程 SMTP 服务器的实战指南
Haraka auth/auth_proxy 插件:按域名代理 AUTH 认证到远程 SMTP 服务器的实战指南 导读 auth/auth_proxy 是 Ha
后端网络/通信Haraka prevent_credential_leaks 插件详解:拦截 SMTP 认证用户的凭据泄露
Haraka prevent_credential_leaks 插件详解:拦截 SMTP 认证用户的凭据泄露 导读 prevent_credential_lea
后端网络/通信
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考