我在尝试设置来自 AWS SES 的退信通知。在最新升级后,系统提示缺少相关的 ARN 配置。我配置了主题(Topic)并添加了 ARN,但测试退信邮件始终未出现。不过,我已确认该消息确实到达了 AWS 上的主题。以下是 Codex 提供的分析和补丁建议(尚未经过测试)。
摘要
通过 SNS 传递的 Amazon SES 退信通知被 /webhooks/aws 接受,但当前的 Discourse 在使用 SES 通过 SMTP 发送时,不会将对应的 EmailLog 标记为退信状态。
Jobs::ProcessSnsNotification 使用 mail.messageId 查找日志。AWS 文档指出,这是 SES 分配的标识符,而 Email::Sender 在 EmailLog.message_id 中存储的是 Discourse 原始的 RFC Message-ID。这两个标识符是不同的。
这似乎是由 2026 年 6 月的安全加固提交 61f12e13aa1b760f81d5ff60f12e3a7e77434b94 引入的回归问题。主题允许列表、签名验证、收件人绑定和重复保护应保持完整;只需更改用于查找的标识符即可。
环境
- Discourse 提交:
2239124ce41df4ea23a21686a78342adb5f6b3b5 - 使用 Amazon SES SMTP 发送
- SES 退信通知主题通过 SNS 传递到
/webhooks/aws aws_sns_topic_arn_allowlist包含确切的 SNS 主题 ARN- 已确认 SNS 订阅
复现步骤
- 配置 Discourse 通过 Amazon SES SMTP 端点发送邮件。
- 配置 SES 退信通知主题,并包含原始标头。
- 将
https://<discourse-host>/webhooks/aws订阅到该主题。 - 将该主题 ARN 添加到
aws_sns_topic_arn_allowlist。 - 向
bounce@simulator.amazonses.com发送一封 Discourse 邮件。 - 确认 SES 发布了退信,并且 SNS 报告 HTTPS 传递成功。
- 打开
/admin/email-logs/bounced。
实际结果
Webhook 返回成功,SNS 报告没有传递失败,但该邮件未出现在退信日志中,且其 Discourse 退信状态未更新。
预期结果
Discourse 应使用原始 RFC Message-ID 和退信收件人将 SES 退信与已发送的 EmailLog 匹配,然后更新退信状态和分数。
原因
Discourse 存储的是投递前的消息 ID:
email_log.message_id = @message.message_id
SNS 任务当前使用的是 SES 分配的 ID:
message_id = message.dig("mail", "messageId")
AWS 区分这些字段:
mail.messageId是由 SES 分配的。- 当启用原始标头时,原始邮件的
Message-ID可以在mail.headers和mail.commonHeaders中找到。
当前的请求规范(request spec)将 mail.messageId、mail.headers 中的 Message-ID、mail.commonHeaders.messageId 和 EmailLog.message_id 设置为相同的测试数据值,因此无法重现实际的 SES 行为。
项目历史中直接存在这种不匹配的记录:PR #7284 在 2019 年移除了严格的 ID 匹配,因为 SNS ID 不等于 EmailLog 中的 ID。2026 年 6 月的安全修复恢复了严格匹配,但使用了 SES 分配的 ID。
建议的修复方案
优先使用 mail.commonHeaders.messageId,使用 Email::MessageIdService.message_id_clean 对其进行规范化,并在原始标头不可用时回退到 mail.messageId 以兼容旧行为。保留现有的 (message_id, to_address) 查找、TopicArn 允许列表、SNS 签名验证和重复处理。
附带的补丁还修改了请求测试数据,使 SES 分配的 ID 与原始 RFC Message-ID 不同。
diff --git a/app/jobs/regular/process_sns_notification.rb b/app/jobs/regular/process_sns_notification.rb
index 2785887d..21ebb1a2 100644
--- a/app/jobs/regular/process_sns_notification.rb
+++ b/app/jobs/regular/process_sns_notification.rb
@@ -17,7 +17,13 @@ module Jobs
end
return unless message && message["notificationType"] == "Bounce"
- return unless message_id = message.dig("mail", "messageId").presence
+ message_id =
+ message.dig("mail", "commonHeaders", "messageId").presence ||
+ message.dig("mail", "messageId").presence
+ return unless message_id
+
+ message_id = Email::MessageIdService.message_id_clean(message_id.strip)
+
return unless bounce_type = message.dig("bounce", "bounceType").presence
return if !Email::Sns.allowed_topic_arn?(json["TopicArn"])
diff --git a/spec/requests/webhooks_controller_spec.rb b/spec/requests/webhooks_controller_spec.rb
index eb7f5b44..8c77b337 100644
--- a/spec/requests/webhooks_controller_spec.rb
+++ b/spec/requests/webhooks_controller_spec.rb
@@ -794,6 +794,7 @@ RSpec.describe WebhooksController do
let(:topic_arn) { "arn:aws:sns:us-east-1:123456789012:discourse-bounces" }
let(:other_topic_arn) { "arn:aws:sns:us-east-1:999999999999:attacker-topic" }
let(:bounce_status) { "5.1.1" }
+ let(:ses_message_id) { "000001378603177f-7a5433e7-8edb-42ae-af10-f0181f34d6ee-000000" }
let(:payload) do
{
"Type" => "Notification",
@@ -823,7 +824,7 @@ RSpec.describe WebhooksController do
"sourceIp" => "127.0.3.0",
"sendingAccountId" => "123456789012",
"callerIdentity" => "IAM_user_or_role_name",
- "messageId" => message_id,
+ "messageId" => ses_message_id,
"destination" => [email, "jane@example.com", "mary@example.com", "richard@example.com"],
"headersTruncated" => false,
"headers" => [
@@ -833,7 +834,7 @@ RSpec.describe WebhooksController do
"value" =>
"\"Test\" <#{email}>, \"Jane Doe\" <jane@example.com>, \"Mary Doe\" <mary@example.com>, \"Richard Doe\" <richard@example.com>",
},
- { "name" => "Message-ID", "value" => message_id },
+ { "name" => "Message-ID", "value" => "<#{message_id}>" },
{ "name" => "Subject", "value" => "Hello" },
{ "name" => "Content-Type", "value" => "text/plain; charset=\"UTF-8\"" },
{ "name" => "Content-Transfer-Encoding", "value" => "base64" },
@@ -845,7 +846,7 @@ RSpec.describe WebhooksController do
"to" => [
"\"Test\" <#{email}>, Jane Doe <jane@example.com>, Mary Doe <mary@example.com>, Richard Doe <richard@example.com>",
],
- "messageId" => message_id,
+ "messageId" => "<#{message_id}>",
"subject" => "Hello",
},
},
@@ -870,7 +871,7 @@ RSpec.describe WebhooksController do
SiteSetting.aws_sns_topic_arn_allowlist = topic_arn
end
- it "hard bounces" do
+ it "hard bounces using the original message ID" do
user = Fabricate(:user, email: email)
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
@@ -883,7 +884,7 @@ RSpec.describe WebhooksController do
expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.hard_bounce_score)
end
- it "does not bounce an email log with a different SES message id" do
+ it "does not bounce an email log with a different original message ID" do
user = Fabricate(:user, email: email)
email_log =
Fabricate(:email_log, user: user, message_id: "other-message-id", to_address: email)
参考
- AWS 通知字段定义:Amazon SNS notification contents for Amazon SES - Amazon Simple Email Service
- 原始 Discourse 修复,PR #7284:FIX: Detect SNS notifications for SES correctly - Pull Request #7284 - discourse/discourse - GitHub
- 安全加固提交:SECURITY: Prevent any signed AWS SNS TopicARN from being accepted via… · discourse/discourse@61f12e1 · GitHub
- 现有的 Meta 报告:Bounced e-mails from Amazon SES/SNS not working