Estaba intentando configurar las notificaciones de rebote desde AWS SES después de recibir un aviso sobre una configuración de ARN faltante para ello tras la última actualización. Configuré el tema y agregué el ARN, pero un correo de prueba de rebote nunca apareció. Sin embargo, confirmé que llegó al tema en AWS. A continuación, se presenta un análisis y un parche propuesto por Codex (sin probar).
Resumen
Las notificaciones de rebote de Amazon SES entregadas a través de SNS son aceptadas por /webhooks/aws, pero la versión actual de Discourse no marca el EmailLog correspondiente como rebotado cuando SES se utiliza a través de SMTP.
Jobs::ProcessSnsNotification busca el registro utilizando mail.messageId. AWS documenta que este es un identificador asignado por SES, mientras que Email::Sender almacena el Message-ID RFC original de Discourse en EmailLog.message_id. Los dos identificadores son diferentes.
Esto parece ser una regresión introducida por el endurecimiento de seguridad de junio de 2026 en el commit 61f12e13aa1b760f81d5ff60f12e3a7e77434b94. La lista blanca de temas, la verificación de firma, la vinculación del destinatario y la protección contra duplicados deben permanecer intactos; solo el identificador utilizado para la búsqueda necesita cambiar.
Entorno
- Commit de Discourse:
2239124ce41df4ea23a21686a78342adb5f6b3b5 - Envío de Amazon SES SMTP
- Tema de notificación de rebote de SES entregado a través de SNS a
/webhooks/aws aws_sns_topic_arn_allowlistcontiene el ARN exacto del tema SNS- La suscripción a SNS está confirmada
Pasos para reproducir
- Configura Discourse para enviar a través del punto de finalización SMTP de Amazon SES.
- Configura un tema de notificación de rebote de SES, con los encabezados originales incluidos.
- Suscríbe
https://<discourse-host>/webhooks/awsal tema. - Agrega el ARN de ese tema a
aws_sns_topic_arn_allowlist. - Envía un correo de Discourse a
bounce@simulator.amazonses.com. - Confirma que SES publique el rebote y que SNS informe una entrega HTTPS exitosa.
- Abre
/admin/email-logs/bounced.
Resultado actual
El webhook devuelve éxito y SNS no informa ninguna entrega fallida, pero el correo no aparece en el registro de correos rebotados y su estado de rebote en Discourse no se actualiza.
Resultado esperado
Discourse debería coincidir el rebote de SES con el EmailLog enviado utilizando el Message-ID RFC original y el destinatario rebotado, y luego actualizar el estado de rebote y la puntuación.
Causa
Discourse almacena el ID de mensaje antes de la entrega:
email_log.message_id = @message.message_id
El trabajo de SNS actualmente utiliza el ID asignado por SES:
message_id = message.dig("mail", "messageId")
AWS distingue estos campos:
mail.messageIdes asignado por SES.- El
Message-IDdel correo original está disponible enmail.headersymail.commonHeaderscuando los encabezados originales están habilitados.
La especificación de solicitud actual da a mail.messageId, el Message-ID de mail.headers, mail.commonHeaders.messageId y EmailLog.message_id el mismo valor de fixture, por lo que no reproduce el comportamiento real de SES.
Hay un historial directo del proyecto para esta discrepancia: la PR #7284 eliminó la coincidencia estricta de ID en 2019 porque el ID de SNS no era igual al ID en EmailLog. La corrección de seguridad de junio de 2026 restauró la coincidencia estricta, pero utilizó el ID asignado por SES.
Solución propuesta
Preferir mail.commonHeaders.messageId, normalizarlo con Email::MessageIdService.message_id_clean, y usar mail.messageId como respaldo para compatibilidad cuando los encabezados originales no estén disponibles. Mantener la búsqueda existente (message_id, to_address), la lista blanca de TopicArn, la verificación de firma de SNS y el manejo de duplicados.
El parche adjunto también cambia el fixture de solicitud para que el ID asignado por SES difiera del 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)
Referencias
- Definiciones de campos de notificación de AWS: Amazon SNS notification contents for Amazon SES - Amazon Simple Email Service
- Corrección original de Discourse, PR #7284: FIX: Detect SNS notifications for SES correctly - Pull Request #7284 - discourse/discourse - GitHub
- Commit de endurecimiento de seguridad: SECURITY: Prevent any signed AWS SNS TopicARN from being accepted via… · discourse/discourse@61f12e1 · GitHub
- Informe existente en Meta: Bounced e-mails from Amazon SES/SNS not working