FEATURE: AWS SNS bounce notifications webhooks

FEATURE: AWS SNS bounce notifications webhooks

diff --git a/Gemfile b/Gemfile
index 1bc558f..f413a3d 100644
--- a/Gemfile
+++ b/Gemfile
@@ -65,6 +65,7 @@ gem 'fast_xor', platform: :mri
 gem 'fastimage'
 
 gem 'aws-sdk-s3', require: false
+gem 'aws-sdk-sns', require: false
 gem 'excon', require: false
 gem 'unf', require: false
 
diff --git a/Gemfile.lock b/Gemfile.lock
index 2acdb78..7db79ab 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -57,6 +57,9 @@ GEM
       aws-sdk-core (~> 3, >= 3.26.0)
       aws-sdk-kms (~> 1)
       aws-sigv4 (~> 1.0)
+    aws-sdk-sns (1.2.0)
+      aws-sdk-core (~> 3)
+      aws-sigv4 (~> 1.0)
     aws-sigv4 (1.0.3)
     barber (0.12.0)
       ember-source (>= 1.0, < 3.1)
@@ -452,6 +455,7 @@ DEPENDENCIES
   activesupport (= 5.2.2)
   annotate
   aws-sdk-s3
+  aws-sdk-sns
   barber
   better_errors
   binding_of_caller
diff --git a/app/controllers/webhooks_controller.rb b/app/controllers/webhooks_controller.rb
index b5f3fdd..6a1bd81 100644
--- a/app/controllers/webhooks_controller.rb
+++ b/app/controllers/webhooks_controller.rb
@@ -24,7 +24,7 @@ class WebhooksController < ActionController::Base
       end
     end
 
-    render body: nil, status: 200
+    success
   end
 
   def mailjet
@@ -41,7 +41,7 @@ class WebhooksController < ActionController::Base
       end
     end
 
-    render body: nil, status: 200
+    success
   end
 
   def mandrill
@@ -58,7 +58,7 @@ class WebhooksController < ActionController::Base
       end
     end
 
-    render body: nil, status: 200
+    success
   end
 
   def sparkpost
@@ -84,7 +84,21 @@ class WebhooksController < ActionController::Base
       end
     end
 
-    render body: nil, status: 200
+    success
+  end
+
+  def aws
+    raw  = request.raw_post
+    json = JSON.parse(raw)
+
+    case json["Type"]
+    when "SubscriptionConfirmation"
+      Jobs.enqueue(:confirm_sns_subscription, raw: raw, json: json)
+    when "Notification"
+      Jobs.enqueue(:process_sns_notification, raw: raw, json: json)
+    end
+
+    success
   end
 
   private
@@ -93,7 +107,7 @@ class WebhooksController < ActionController::Base
     render body: nil, status: 406
   end
 
-  def mailgun_success
+  def success
     render body: nil, status: 200
   end
 
@@ -129,7 +143,7 @@ class WebhooksController < ActionController::Base
       process_bounce(message_id, to_address, SiteSetting.hard_bounce_score)
     end
 
-    mailgun_success
+    success
   end
 
   def handle_mailgun_new(params)
@@ -149,7 +163,7 @@ class WebhooksController < ActionController::Base
       end
     end
 
-    mailgun_success
+    success
   end
 
   def process_bounce(message_id, to_address, bounce_score)
diff --git a/app/jobs/regular/confirm_sns_subscription.rb b/app/jobs/regular/confirm_sns_subscription.rb
new file mode 100644
index 0000000..8019ea6
--- /dev/null
+++ b/app/jobs/regular/confirm_sns_subscription.rb
@@ -0,0 +1,21 @@
+require "aws-sdk-sns"
+
+module Jobs
+
+  class ConfirmSnsSubscription < Jobs::Base
+    sidekiq_options retry: false
+
+    def execute(args)
+      return unless raw  = args[:raw].presence
+      return unless json = args[:json].presence
+
+      return unless subscribe_url = json["SubscribeURL"].presence
+      return unless Aws::SNS::MessageVerifier.new.authentic?(raw)
+
+      # confirm subscription by visiting the URL
+      open(subscribe_url)
+    end
+
+  end
+
+end
diff --git a/app/jobs/regular/process_sns_notification.rb b/app/jobs/regular/process_sns_notification.rb
new file mode 100644
index 0000000..feb65b6
--- /dev/null
+++ b/app/jobs/regular/process_sns_notification.rb
@@ -0,0 +1,36 @@
+require "aws-sdk-sns"
+
+module Jobs
+
+  class ProcessSnsNotification < Jobs::Base
+    sidekiq_options retry: false
+
+    def execute(args)
+      return unless raw  = args[:raw].presence
+      return unless json = args[:json].presence
+
+      return unless message = json["Message"].presence
+      return unless message["notificationType"] == "Bounce"
+      return unless message_id = message.dig("mail", "messageId").presence
+      return unless bounce_type = message.dig("bounce", "bounceType").presence
+
+      return unless Aws::SNS::MessageVerifier.new.authentic?(raw)
+
+      message.dig("bounce", "bouncedRecipients").each do |r|
+        if email_log = EmailLog.find_by(message_id: message_id, to_address: r["emailAddress"])
+          email_log.update_columns(bounced: true)
+
+          if email_log.user&.email.present?
+            if r["status"]&.start_with?["4."] || bounce_type == "Transient"
+              Email::Receiver.update_bounce_score(email_log.user.email, SiteSetting.soft_bounce_score)
+            else
+              Email::Receiver.update_bounce_score(email_log.user.email, SiteSetting.hard_bounce_score)
+            end
+          end
+        end
+      end
+    end
+
+  end
+
+end
diff --git a/config/routes.rb b/config/routes.rb
index dca765b..eb9f3ef 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -15,10 +15,11 @@ Discourse::Application.routes.draw do
   match "/404", to: "exceptions#not_found", via: [:get, :post]
   get "/404-body" => "exceptions#not_found_body"
 
+  post "webhooks/aws" => "webhooks#aws"
   post "webhooks/mailgun"  => "webhooks#mailgun"
-  post "webhooks/sendgrid" => "webhooks#sendgrid"
   post "webhooks/mailjet"  => "webhooks#mailjet"
   post "webhooks/mandrill" => "webhooks#mandrill"
+  post "webhooks/sendgrid" => "webhooks#sendgrid"
   post "webhooks/sparkpost" => "webhooks#sparkpost"
 
   if Rails.env.development?

GitHub sha: 4d674acc

This commit has been mentioned on Discourse Meta. There might be relevant details there:

Is there any reason why this is such an old version? that gem is at 1.9.0 now.

Good point, no sure why bundle install installed that version. I’ve updated them in a3e9b809b2da0af36c2e00473d2010f66bb2a935

Marking for followup we need to only require this if it is being used, it is required a bit too early here.

Aha look like it was already followed up :smile: