J’essayais de configurer les notifications de rebond (bounce) depuis AWS SES après avoir reçu une alerte concernant une configuration ARN manquante à ce sujet suite à la dernière mise à jour. J’ai configuré le sujet et ajouté l’ARN, mais un e-mail de test de rebond n’est jamais arrivé. J’ai toutefois confirmé qu’il a bien atteint le sujet sur AWS. Voici une analyse et un correctif proposé par Codex (non testé).
Résumé
Les notifications de rebond Amazon SES livrées via SNS sont acceptées par /webhooks/aws, mais Discourse actuel ne marque pas le EmailLog correspondant comme rebondi lorsque SES est utilisé via SMTP.
Jobs::ProcessSnsNotification recherche le journal en utilisant mail.messageId. AWS documente que ceci est un identifiant attribué par SES, tandis que Email::Sender stocke l’identifiant Message-ID RFC original de Discourse dans EmailLog.message_id. Les deux identifiants diffèrent.
Cela semble être une régression introduite par le durcissement de sécurité de juin 2026 dans l’engagement 61f12e13aa1b760f81d5ff60f12e3a7e77434b94. La liste blanche des sujets, la vérification de la signature, la liaison du destinataire et la protection contre les doublons doivent rester intacts ; seul l’identifiant utilisé pour la recherche doit changer.
Environnement
- Commit Discourse :
2239124ce41df4ea23a21686a78342adb5f6b3b5 - Envoi SMTP Amazon SES
- Sujet de notification de rebond SES livré via SNS vers
/webhooks/aws aws_sns_topic_arn_allowlistcontient l’ARN exact du sujet SNS- L’abonnement SNS est confirmé
Étapes pour reproduire
- Configurer Discourse pour envoyer via le point d’accès SMTP Amazon SES.
- Configurer un sujet de notification de rebond SES, avec les en-têtes d’origine inclus.
- Abonner
https://<discourse-host>/webhooks/awsau sujet. - Ajouter cet ARN de sujet à
aws_sns_topic_arn_allowlist. - Envoyer un e-mail Discourse à
bounce@simulator.amazonses.com. - Confirmer que SES publie le rebond et que SNS signale une livraison HTTPS réussie.
- Ouvrir
/admin/email-logs/bounced.
Résultat réel
Le webhook renvoie un succès et SNS ne signale aucune livraison échouée, mais l’e-mail est absent du journal des e-mails rebondis et son état de rebond Discourse n’est pas mis à jour.
Résultat attendu
Discourse devrait associer le rebond SES au EmailLog envoyé en utilisant l’identifiant Message-ID RFC original et le destinataire rebondi, puis mettre à jour l’état et le score de rebond.
Cause
Discourse stocke l’identifiant de message avant la livraison :
email_log.message_id = @message.message_id
Le travail SNS utilise actuellement l’identifiant attribué par SES :
message_id = message.dig("mail", "messageId")
AWS distingue ces champs :
mail.messageIdest attribué par SES.- L’identifiant
Message-IDde l’e-mail original est disponible dansmail.headersetmail.commonHeaderslorsque les en-têtes d’origine sont activés.
La spécification de requête actuelle donne à mail.messageId, l’identifiant Message-ID de mail.headers, mail.commonHeaders.messageId et EmailLog.message_id la même valeur de test, de sorte qu’elle ne reproduit pas le comportement réel de SES.
Il y a un historique direct du projet pour ce décalage : la PR #7284 a supprimé l’appariement strict des identifiants en 2019 parce que l’identifiant SNS n’était pas égal à l’identifiant dans EmailLog. La correction de sécurité de juin 2026 a rétabli l’appariement strict mais a utilisé l’identifiant attribué par SES.
Correction proposée
Privilégier mail.commonHeaders.messageId, le normaliser avec Email::MessageIdService.message_id_clean, et revenir à mail.messageId pour la compatibilité lorsque les en-têtes d’origine ne sont pas disponibles. Conserver la recherche existante (message_id, to_address), la liste blanche TopicArn, la vérification de la signature SNS et la gestion des doublons.
Le correctif joint modifie également le test de requête pour que l’identifiant attribué par SES diffère de l’identifiant Message-ID RFC original.
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)
Références
- Définitions des champs de notification AWS : Amazon SNS notification contents for Amazon SES - Amazon Simple Email Service
- Correction Discourse originale, PR #7284 : FIX: Detect SNS notifications for SES correctly - Pull Request #7284 - discourse/discourse - GitHub
- Commit de durcissement de sécurité : SECURITY: Prevent any signed AWS SNS TopicARN from being accepted via… · discourse/discourse@61f12e1 · GitHub
- Rapport Meta existant : Bounced e-mails from Amazon SES/SNS not working