كنت أحاول إعداد إشعارات الارتداد (bounce) من AWS SES بعد ظهور تنبيه حول إعداد ARN المفقود لها بعد آخر ترقية. قمت بتكوين الموضوع (topic) وإضافة ARN، لكن بريد الاختبار الخاص بالارتداد لم يظهر أبداً. ومع ذلك، تأكدت من وصوله إلى الموضوع (topic) على AWS. فيما يلي تحليل وصلح مقترح من Codex (لم يتم اختباره).
ملخص
تُقبل إشعارات الارتداد من Amazon SES المُرسلة عبر SNS بواسطة /webhooks/aws، لكن Discourse الحالي لا يحدد سجل EmailLog المقابل كمرتد عند استخدام SES عبر SMTP.
يبحث Jobs::ProcessSnsNotification عن السجل باستخدام mail.messageId. توثّق AWS أن هذا معرّف مُسنَد من SES، بينما يخزّن Email::Sender معرّف Message-ID الأصلي من RFC الخاص بـ Discourse في EmailLog.message_id. يختلف هذان المعرّفان.
يبدو أن هذا هو تراجع (regression) سبّبه تعزيز الأمان في يونيو 2026 في التأكيد 61f12e13aa1b760f81d5ff60f12e3a7e77434b94. يجب أن تبقى قائمة السماح بالموضوعات، والتحقق من التوقيع، وربط المستلم، والحماية من التكرار سليمة؛ فقط المعرّف المستخدم للبحث يحتاج إلى التغيير.
البيئة
- تأكيد Discourse:
2239124ce41df4ea23a21686a78342adb5f6b3b5 - الإرسال عبر SMTP من Amazon SES
- موضوع إشعار الارتداد من SES يُسلَّم عبر SNS إلى
/webhooks/aws - يحتوي
aws_sns_topic_arn_allowlistعلى ARN الموضوع (SNS topic) الدقيق - تم تأكيد اشتراك SNS
خطوات إعادة الإنتاج
- اضبط Discourse للإرسال عبر نقطة نهاية SMTP الخاصة بـ Amazon SES.
- اضبط موضوع إشعار ارتداد SES، مع تضمين الترويسات الأصلية.
- اشترك
https://<discourse-host>/webhooks/awsفي الموضوع. - أضف ARN ذلك الموضوع إلى
aws_sns_topic_arn_allowlist. - أرسل بريد Discourse إلى
bounce@simulator.amazonses.com. - تأكد من أن SES ينشر الارتداد وأن SNS يبلغ عن تسليم HTTPS ناجح.
- افتح
/admin/email-logs/bounced.
النتيجة الفعلية
يعيد webhook النجاح ولا يبلغ SNS عن فشل في التسليم، لكن البريد غير موجود في سجل بريد الارتداد ولم يتم تحديث حالة الارتداد الخاصة بـ Discourse.
النتيجة المتوقعة
يجب على Discourse مطابقة ارتداد SES مع EmailLog المُرسَل باستخدام معرّف Message-ID الأصلي من RFC والمستلم المرتد، ثم تحديث حالة الارتداد والنقاط (score).
السبب
يخزّن Discourse معرّف الرسالة قبل التسليم:
email_log.message_id = @message.message_id
يستخدم مهمة SNS حالياً المعرّف المُسنَد من SES:
message_id = message.dig("mail", "messageId")
تميّز AWS بين هذه الحقول:
mail.messageIdيُسنَد من قبل SES.- معرّف
Message-IDللبريد الأصلي متاح فيmail.headersوmail.commonHeadersعند تمكين الترويسات الأصلية.
يمنح مواصفة الطلب الحالية قيم fixture متطابقة لـ mail.messageId، و Message-ID في mail.headers، و mail.commonHeaders.messageId، و EmailLog.message_id، لذا لا تعيد إنتاج سلوك SES الفعلي.
يوجد تاريخ مباشر للمشروع لهذا عدم التطابق: أزال PR #7284 المطابقة الصارمة للمعرّف في عام 2019 لأن معرّف SNS لم يكن يساوي المعرّف في EmailLog. أعادت إصلاحات الأمان في يونيو 2026 المطابقة الصارمة لكنها استخدمت المعرّف المُسنَد من SES.
الإصلاح المقترح
يفضّل mail.commonHeaders.messageId، ويُطبعه باستخدام Email::MessageIdService.message_id_clean، ويلجأ إلى mail.messageId للتوافق عند عدم توفر الترويسات الأصلية. احتفظ بالبحث الموجود (message_id, to_address)، وقائمة السماح لـ TopicArn، والتحقق من توقيع SNS، ومعالجة التكرار.
يغيّر الصلح المرفق أيضاً fixture الطلب بحيث يختلف المعرّف المُسنَد من SES عن معرّف Message-ID الأصلي من RFC.
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