최근 업그레이드 후 ARN 구성이 누락되었다는 프롬프트가 표시되어 AWS SES에서 바운스 알림을 설정하려고 했습니다. 토픽을 구성하고 ARN을 추가했지만, 테스트용 바운스 이메일이 표시되지 않았습니다. AWS에서 해당 이메일이 토픽에 도달한 것은 확인했습니다. 아래는 Codex가 분석하고 제안한 패치입니다(미테스트).
요약
SNS를 통해 전달되는 Amazon SES 바운스 알림은 /webhooks/aws에서 수신되지만, SMTP를 통해 SES를 사용하는 경우 현재 Discourse는 해당 EmailLog를 바운스로 표시하지 않습니다.
Jobs::ProcessSnsNotification은 mail.messageId를 사용하여 로그를 조회합니다. AWS 문서에 따르면 이는 SES가 할당하는 식별자이며, Email::Sender는 EmailLog.message_id에 Discourse의 원래 RFC Message-ID를 저장합니다. 두 식별자는 서로 다릅니다.
이는 2026년 6월의 보안 강화 커밋 61f12e13aa1b760f81d5ff60f12e3a7e77434b94로 인해 발생한 회귀(regression)로 보입니다. 토픽 허용 목록, 서명 검증, 수신자 바인딩, 중복 방지 기능은 그대로 유지해야 하며, 조회에 사용되는 식별자만 변경되어야 합니다.
환경
- Discourse 커밋:
2239124ce41df4ea23a21686a78342adb5f6b3b5 - Amazon SES SMTP 전송
- SES 바운스 알림 토픽이 SNS를 통해
/webhooks/aws로 전달 aws_sns_topic_arn_allowlist에 해당 SNS 토픽 ARN이 정확히 포함됨- SNS 구독 확인됨
재현 단계
- Discourse를 Amazon SES SMTP 엔드포인트를 통해 전송하도록 구성합니다.
- 원래 헤더가 포함된 SES 바운스 알림 토픽을 구성합니다.
https://<discourse-host>/webhooks/aws를 해당 토픽에 구독합니다.- 해당 토픽 ARN을
aws_sns_topic_arn_allowlist에 추가합니다. bounce@simulator.amazonses.com으로 Discourse 이메일을 전송합니다.- SES가 바운스를 게시하고 SNS가 HTTPS 전달 성공을 보고하는지 확인합니다.
/admin/email-logs/bounced를 엽니다.
실제 결과
웹훅은 성공을 반환하고 SNS는 실패한 전달이 없음을 보고하지만, 이메일은 바운스된 이메일 로그에 나타나지 않으며 해당 Discourse 바운스 상태도 업데이트되지 않습니다.
기대 결과
Discourse는 원래 RFC Message-ID와 바운스된 수신자를 사용하여 SES 바운스를 전송된 EmailLog와 일치시켜야 하며, 이후 바운스 상태와 점수를 업데이트해야 합니다.
원인
Discourse는 전달 전 메시지 ID를 저장합니다:
email_log.message_id = @message.message_id
SNS 작업은 현재 SES가 할당된 ID를 사용합니다:
message_id = message.dig("mail", "messageId")
AWS는 이 필드들을 다음과 같이 구분합니다:
mail.messageId는 SES가 할당합니다.- 원래 이메일의
Message-ID는 원래 헤더가 활성화된 경우mail.headers와mail.commonHeaders에서 사용할 수 있습니다.
현재 요청 스펙은 mail.messageId, mail.headers의 Message-ID, mail.commonHeaders.messageId, 그리고 EmailLog.message_id에 동일한 픽스처 값을 부여하므로, 실제 SES 동작을 재현하지 못합니다.
이 불일치에 대한 직접적인 프로젝트 역사가 있습니다: PR #7284는 2019년에 SNS ID가 EmailLog의 ID와 같지 않았기 때문에 엄격한 ID 매칭을 제거했습니다. 2026년 6월의 보안 수정은 엄격한 매칭을 복원했지만 SES가 할당된 ID를 사용했습니다.
제안된 수정 사항
mail.commonHeaders.messageId를 우선 사용하고, Email::MessageIdService.message_id_clean으로 정규화한 뒤, 원래 헤더가 없는 경우 호환성을 위해 mail.messageId로 폴백합니다. 기존 (message_id, to_address) 조회, TopicArn 허용 목록, SNS 서명 검증, 중복 처리는 유지합니다.
첨부된 패치는 또한 SES가 할당된 ID가 원래 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)
참고 자료
- 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