仓库: 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 停止向该用户发送邮件。即使你不启用任何其他功能,日志噪音也会在此处结束。
- 在可配置的失败次数和可配置的时间跨度内(默认:至少相隔 48 小时的 2 次失败,因此短暂的故障不会导致任何人被停用)之后,插件会采取行动。默认操作是
log_only:记录一条工作人员操作日志,记录该用户本应被停用。当你信任它时,可以切换到deactivate。 - 停用操作会在用户下次登录时将其引导至标准的激活流程,其中内置了更改地址的功能。他们验证一个有效的电子邮件并继续保留完整的账户。关键在于可恢复性:一个唯一的恢复渠道是无效邮箱的活跃账户,是一个即将发生的锁定风险。
如果你的站点确实会收到退信(VERP 或 Webhooks),你还可以让插件在核心代码的退信分数超过你选择的级别时采取行动,该级别应高于核心代码停止发送的分数。所有地方都适用一些安全规则。工作人员和机器人永远不会被触碰。由于冷却窗口的存在,每小时重试一次的卡住邮件只计为一次失败。只有当服务器拒绝的地址是该用户当前的地址时,失败才会计入该用户。并且每个记录的失败都会保留 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 工作流。欢迎反馈,特别是来自其他中继服务器的拒绝短语,这些是默认短语列表可能遗漏的。