Estava tentando configurar notificações de rejeição (bounce) do AWS SES após receber um aviso sobre a falta de configuração de ARN para isso, depois da atualização mais recente. Configurei o tópico e adicionei o ARN, mas um e-mail de teste de rejeição nunca apareceu. Confirmei, no entanto, que a mensagem chegou ao tópico no AWS. Abaixo está uma análise e um patch proposto pelo Codex (não testado).
Resumo
As notificações de rejeição do Amazon SES entregues por meio do SNS são aceitas por /webhooks/aws, mas o Discourse atual não marca o EmailLog correspondente como rejeitado quando o SES é usado via SMTP.
Jobs::ProcessSnsNotification consulta o log usando mail.messageId. A documentação da AWS indica que este é um identificador atribuído pelo SES, enquanto Email::Sender armazena o Message-ID original da RFC do Discourse em EmailLog.message_id. Os dois identificadores são diferentes.
Isso parece ser uma regressão introduzida pelo endurecimento de segurança de junho de 2026 no commit 61f12e13aa1b760f81d5ff60f12e3a7e77434b94. A lista de permissão de tópicos, a verificação de assinatura, o vínculo do destinatário e a proteção contra duplicatas devem permanecer intactos; apenas o identificador usado para a consulta precisa ser alterado.
Ambiente
- Commit do Discourse:
2239124ce41df4ea23a21686a78342adb5f6b3b5 - Envio via SMTP do Amazon SES
- Tópico de notificação de rejeição do SES entregue por SNS para
/webhooks/aws aws_sns_topic_arn_allowlistcontém o ARN exato do tópico SNS- Assinatura do SNS confirmada
Passos para reproduzir
- Configure o Discourse para enviar por meio do endpoint SMTP do Amazon SES.
- Configure um tópico de notificação de rejeição do SES, incluindo os cabeçalhos originais.
- Assine
https://<discourse-host>/webhooks/awsao tópico. - Adicione o ARN desse tópico a
aws_sns_topic_arn_allowlist. - Envie um e-mail do Discourse para
bounce@simulator.amazonses.com. - Confirme que o SES publica a rejeição e que o SNS relata uma entrega HTTPS bem-sucedida.
- Abra
/admin/email-logs/bounced.
Resultado atual
O webhook retorna sucesso e o SNS não relata falha de entrega, mas o e-mail está ausente do log de e-mails rejeitados e seu estado de rejeição no Discourse não é atualizado.
Resultado esperado
O Discourse deve corresponder a rejeição do SES ao EmailLog enviado usando o Message-ID original da RFC e o destinatário rejeitado, e então atualizar o estado de rejeição e a pontuação.
Causa
O Discourse armazena o ID da mensagem antes da entrega:
email_log.message_id = @message.message_id
O trabalho do SNS atualmente usa o ID atribuído pelo SES:
message_id = message.dig("mail", "messageId")
A AWS distingue esses campos:
mail.messageIdé atribuído pelo SES.- O
Message-IDdo e-mail original está disponível emmail.headersemail.commonHeadersquando os cabeçalhos originais estão habilitados.
A especificação de solicitação atual atribui a mail.messageId, o Message-ID em mail.headers, o mail.commonHeaders.messageId e o EmailLog.message_id o mesmo valor de fixture, portanto, não reproduz o comportamento real do SES.
Há histórico direto do projeto para essa incompatibilidade: a PR #7284 removeu a correspondência estrita de IDs em 2019 porque o ID do SNS não era igual ao ID em EmailLog. A correção de segurança de junho de 2026 restaurou a correspondência estrita, mas usou o ID atribuído pelo SES.
Correção proposta
Dar preferência a mail.commonHeaders.messageId, normalizá-lo com Email::MessageIdService.message_id_clean e usar mail.messageId como alternativa para compatibilidade quando os cabeçalhos originais não estiverem disponíveis. Manter a consulta existente (message_id, to_address), a lista de permissão de TopicArn, a verificação de assinatura do SNS e o tratamento de duplicatas.
O patch anexado também altera a fixture de solicitação para que o ID atribuído pelo SES seja diferente do Message-ID original da 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)
Referências
- Definições dos campos de notificação da AWS: Amazon SNS notification contents for Amazon SES - Amazon Simple Email Service
- Correção original do Discourse, PR #7284: FIX: Detect SNS notifications for SES correctly - Pull Request #7284 - discourse/discourse - GitHub
- Commit de endurecimento de segurança: SECURITY: Prevent any signed AWS SNS TopicARN from being accepted via… · discourse/discourse@61f12e1 · GitHub
- Relatório existente no Meta: Bounced e-mails from Amazon SES/SNS not working