仓库: GitHub - overgrow/discourse-bounce-guard · GitHub
许可证: MIT
&tldr; 你将获得什么: 更干净的错误日志(针对无效地址的每小时重试将停止)。Discourse 将停止发送无法送达的邮件。用户满意度提升:因为无效地址会被及时替换为有效地址,用户能够重新登录其账户。
如果你的出站邮件通过你自己的中继服务器(Postfix、Exim 或大多数自托管设置)发送,你可能会看到 /logs 中充满了类似这样的成对错误:
SMTP Error Net::SMTPServerBusy with message: 450 4.1.2 <someone@gone-domain.com>: Recipient address rejected: Domain not found
Job exception: Net::SMTPServerBusy
该域名已消失且不会恢复。但中继服务器返回的是临时错误代码(450),因为理论上 DNS 查找可能会暂时失败。Discourse 将任何临时错误解释为“一小时后重试”,而 Sidekiq 会持续重试数周。核心系统的退信检测机制从未触发。它仅在收到退信消息时才会做出反应,无论是作为电子邮件(VERP)还是来自邮件提供商的 webhook。而在这里,邮件在发送现场就被拒绝了,因此根本不存在退信消息。退信分数保持为零,重试继续,用户保留着一个无法接收任何内容(包括密码重置邮件)的地址。
这个问题在这里出现过几次,但没有内置的解决方案,例如 处理发往不存在域名的邮件、停用硬退信用户 以及 如何停用未收到邮件的用户的账户。我们的论坛也遇到了同样的问题,因此我们开发了一个插件。
它的作用
Bounce Guard 挂钩了发送路径本身,并对每个 SMTP 拒绝进行分类:
- 带有无效收件人增强状态(
5.1.1、5.1.2、5.2.1等)的 5xx 响应被视为硬失败。 - 任何匹配可配置短语列表(“域名未找到”、“用户未知”等)的响应,无论代码如何,都视为硬失败。这可以捕获那些用 450 软失败永久性条件的中继服务器。
- 灰名单、满邮箱和速率限制保持不变。核心系统的重试行为未受干扰。
记录的硬失败随后会经历一个阶梯式流程:
- 每一次失败都会作为硬退信计入核心系统的退信分数(默认开启)。两次之后,核心系统自身的阈值会被触发,Discourse 将停止向该用户发送邮件。日志噪音在此结束,即使你从未启用其他功能。
- 在可配置的数量(默认 3 次)分布在可配置的时间跨度(默认 48 小时,因此短暂的故障不会导致任何人被停用)内达到后,插件会采取行动。默认操作是
log_only:记录一条工作人员操作日志,指出该用户本应被停用。当你信任该功能时,可以切换到deactivate。 - 停用后,用户在下一次登录时将通过标准的激活流程,其中内置了更改地址的功能。他们验证一个有效的电子邮件地址,并继续保留其完整的账户。关键在于可恢复性:一个活跃账户,如果其唯一的恢复渠道是一个死信箱,那就是一场等待发生的锁定。
如果你的站点确实会收到退信(VERP 或 webhook),你也可以让插件在核心系统的退信分数超过你选择的级别时采取行动,该级别应高于核心系统停止发送的分数。各处都适用一些安全规则。工作人员和机器人永远不会被触碰。由于冷却窗口的存在,每小时重试一次的卡住邮件只计为一次失败。只有当服务器拒绝的地址是该用户当前使用的地址时,失败才计入该用户。并且所有记录的失败都会保留 90 天,以便你可以检查发生了什么(README 中有 Data Explorer 查询)。
安装
标准的 插件安装:
hooks:
after_code:
- exec:
cd: $home/plugins
cmd:
- git clone https://github.com/discourse/docker_manager.git
- git clone https://github.com/overgrow/discourse-bounce-guard.git
启用 bounce_guard_enabled,将 bounce_guard_action 保持在 log_only 状态一两周,审查它标记的内容,然后再决定是否停用。设置参考请参阅 README。
仅服务器端,无主题或 JS 组件。针对当前核心版本(2026.8)构建并测试,包含 31 个规格,CI 运行标准的 discourse-plugin 工作流。欢迎反馈,特别是来自其他中继服务器的拒绝短语,这些短语是默认短语列表所遗漏的。