Le notifiche di rimbalzo SES/SNS non corrispondono più a EmailLog dopo il rafforzamento della sicurezza di TopicArn

Stavo cercando di configurare le notifiche di rimbalzo da AWS SES dopo aver ricevuto un avviso sulla mancata configurazione dell’ARN al termine dell’ultimo aggiornamento. Ho configurato il topic e aggiunto l’ARN, ma una email di test di rimbalzo non è mai arrivata. Tuttavia, ho confermato che è stata ricevuta dal topic su AWS. Di seguito è riportata un’analisi e una patch proposta da Codex (non testata).

Riepilogo

Le notifiche di rimbalzo di Amazon SES consegnate tramite SNS vengono accettate da /webhooks/aws, ma l’attuale versione di Discourse non segna il corrispondente EmailLog come rimbalzato quando SES viene utilizzato tramite SMTP.

Jobs::ProcessSnsNotification cerca il log utilizzando mail.messageId. AWS documenta che questo è un identificatore assegnato da SES, mentre Email::Sender archivia l’originale RFC Message-ID di Discourse in EmailLog.message_id. I due identificatori sono diversi.

Questo sembra essere un regresso introdotto dal rafforzamento della sicurezza di giugno 2026 nel commit 61f12e13aa1b760f81d5ff60f12e3a7e77434b94. La whitelist dei topic, la verifica della firma, il vincolo del destinatario e la protezione dai duplicati dovrebbero rimanere intatti; è necessario modificare solo l’identificatore utilizzato per la ricerca.

Ambiente

  • Commit di Discourse: 2239124ce41df4ea23a21686a78342adb5f6b3b5
  • Invio tramite SMTP di Amazon SES
  • Topic di notifica dei rimbalzi SES consegnato tramite SNS a /webhooks/aws
  • aws_sns_topic_arn_allowlist contiene l’ARN esatto del topic SNS
  • L’abbonamento SNS è confermato

Passaggi per riprodurre

  1. Configura Discourse per inviare tramite l’endpoint SMTP di Amazon SES.
  2. Configura un topic di notifica dei rimbalzi SES, includendo gli header originali.
  3. Abbona https://<discourse-host>/webhooks/aws al topic.
  4. Aggiungi quell’ARN del topic a aws_sns_topic_arn_allowlist.
  5. Invia un’email di Discourse a bounce@simulator.amazonses.com.
  6. Conferma che SES pubblica il rimbalzo e che SNS riporta una consegna HTTPS riuscita.
  7. Apri /admin/email-logs/bounced.

Risultato effettivo

Il webhook restituisce successo e SNS non riporta alcuna consegna fallita, ma l’email è assente dal registro delle email rimbalzate e il suo stato di rimbalzo in Discourse non viene aggiornato.

Risultato atteso

Discourse dovrebbe abbinare il rimbalzo SES all’EmailLog inviato utilizzando l’originale RFC Message-ID e il destinatario rimbalzato, quindi aggiornare lo stato di rimbalzo e il punteggio.

Causa

Discourse archivia l’ID messaggio pre-consegna:

email_log.message_id = @message.message_id

Il job SNS attualmente utilizza l’ID assegnato da SES:

message_id = message.dig("mail", "messageId")

AWS distingue questi campi:

  • mail.messageId è assegnato da SES.
  • L’originale Message-ID dell’email è disponibile in mail.headers e mail.commonHeaders quando gli header originali sono abilitati.

Lo spec di richiesta attuale assegna a mail.messageId, Message-ID in mail.headers, mail.commonHeaders.messageId e EmailLog.message_id lo stesso valore di fixture, quindi non riproduce il comportamento reale di SES.

C’è una storia diretta del progetto per questa discrepanza: la PR #7284 ha rimosso l’abbinamento rigoroso degli ID nel 2019 perché l’ID SNS non era uguale all’ID in EmailLog. La modifica di sicurezza di giugno 2026 ha ripristinato l’abbinamento rigoroso ma ha utilizzato l’ID assegnato da SES.

Soluzione proposta

Preferire mail.commonHeaders.messageId, normalizzarlo con Email::MessageIdService.message_id_clean e fare affidamento su mail.messageId per compatibilità quando gli header originali non sono disponibili. Mantenere la ricerca esistente (message_id, to_address), la whitelist TopicArn, la verifica della firma SNS e la gestione dei duplicati.

La patch allegata modifica anche la fixture di richiesta in modo che l’ID assegnato da SES differisca dall’originale 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)

Riferimenti

1 Mi Piace

Riproduzione su un’installazione self-hosted che esegue la v2026.7.3 (release/2026.7), SES SMTP in us-east-1, PostgreSQL 18 incorporato.

Configurazione

  • aws_sns_topic_arn_allowlist contiene l’ARN esatto del topic (verificato nella console SNS).
  • Confermata l’iscrizione HTTPS di SNS a /webhooks/aws; la stessa iscrizione ha elaborato correttamente i bounce da marzo a giugno 2026 su release/2026.4 e 2026.5.0.

Osservazione
Due messaggi di test a bounce@simulator.amazonses.com. Ciascuno ha generato un POST SNS che ha raggiunto il container:

"POST /webhooks/aws HTTP/1.1" "Amazon Simple Notification Service Agent" 200 402

Niente in /logs, niente sotto /admin/email-logs/bounced. Dato che WebhooksController#aws restituisce 406 in caso di fallimento dell’allowlist o della firma, il 200 conferma che entrambi i controlli passano e che Jobs::ProcessSnsNotification viene messo in coda; il job termina poi con next if email_log.nil? perché mail.messageId (assegnato da SES) non corrisponde mai a EmailLog.message_id (il proprio Message-ID di Discourse, impostato in Email::Sender tramite email_log.message_id = @message.message_id).

La cronologia git conferma l’analisi sopra: #7284 (2019) ha rimosso il confronto degli ID per esattamente questo motivo, e 61f12e1 (giugno 2026) lo ha ripristinato utilizzando l’ID assegnato da SES. Su release/2026.7 il job non è stato modificato da quel commit.

Effetto netto per gli utenti self-hosted di SES sull’attuale ESR: l’elaborazione dei bounce è disabilitata silenziosamente, e la nuova notifica della dashboard orienta gli amministratori verso una configurazione che poi non ha alcun effetto. Sarebbe gradito un backport su release/2026.7 non appena verrà applicata una correzione.

2 Mi Piace