コピペコードで快適生活

明日使えるソースを自分のために

Rails×SES バウンスメール(不達メール)対策

「SESで不達メールが多いから、対策してくれなかったらSES止めるよ」って過去にAWSから言われたことがあって、そのときの対応メモを書きだしてみた。

対応の基本的な流れ
SESは不達メールがあった場合に特定のURLに対してリクエストをjson付きで投げてくれるので、それを特定のURLでうけてJSONパースして、そのパースしたJSONの中にメールアドレスがあるので、それをブラックリストテーブルか何か作ってINSERTして、メール送信の時はそのブラックリストテーブルを見て、送っていいメールアドレスいいか判定するって感じ。

AWSの設定はここを参考に。
Amazon SESのBounce SNS通知をRailsで処理する|WEBデザイン Tips

Railsのソースはこんな感じ。

app/controllers/api/aws_controller.rb

#
# AWSから飛んできたリクエストを受け取るAPI
#
require 'json'
require 'open-uri'
class Api::AwsController < ActionController::Base
  skip_before_filter :verify_authenticity_token

  #
  # SESからリクエストされる
  # 不達メールアドレスリストをブラックリストに登録する
  #
  def receive_bounce_notice
    # data = JSON.parse(dummy_raw_post) # debug用
    data = JSON.parse(request.raw_post)

    # Subscription認証用URLを開いて認証を完了する
    if data['SubscribeURL'].present?
      open(data['SubscribeURL'])    

    # ブラックリスト登録する
    else
      data2 = JSON.load(data['Message'])
      type = data2['notificationType']

      if type=='Bounce'
        bounce = data2['bounce']
        bouncerecps = bounce['bouncedRecipients']
        bouncerecps.each do |recp|
          email = recp['emailAddress']
          if BouncedEmailAddress.where(email: email).blank?
            BouncedEmailAddress.create(email: email)
          end
        end
      end
    end

    render text: ""
  end

  private
    def dummy_raw_post
str =<<EOS
{
  "Type" : "Notification",
  "MessageId" : "463fbb0b-63ea-4a6d-90c5-33c8686e3bd1",
  "TopicArn" : "arn:aws:sns:us-east-1:811118151095:suz-lab-ses",
  "Message" : {
      "notificationType":"Bounce",
      "bounce":{
         "bounceType":"Permanent",
         "bounceSubType": "General",
         "bouncedRecipients":[
            {
               "emailAddress":"recipient1@example.com"
            },
            {
               "emailAddress":"recipient2@example.com"
            }
         ],
         "timestamp":"2012-05-25T14:59:38.237-07:00",
         "feedbackId":"00000137860315fd-869464a4-8680-4114-98d3-716fe35851f9-000000"
      },
      "mail":{
         "timestamp":"2012-05-25T14:59:38.237-07:00",
         "messageId":"00000137860315fd-34208509-5b74-41f3-95c5-22c1edc3c924-000000",
         "source":"email_1337983178237@amazon.com",
         "destination":[
            "recipient1@example.com",
            "recipient2@example.com",
            "recipient3@example.com",
            "recipient4@example.com"
         ]
      }
  },
  "Timestamp" : "2012-10-08T13:00:40.691Z",
  "SignatureVersion" : "1", 
  "Signature" : "FDNBWbFhc5MXs+2tjw327zXhiKca3GLHbbVEN8vUmLAmnj60...",
  "SigningCertURL" : "https://sns.us-east-1.amazonaws.com/SimpleN...",
  "UnsubscribeURL" : "https://sns.us-east-1.amazonaws.com/?Action..."
}
EOS
      return str
    end
end


lib/mail/email_interceptor.rb

#
# email_interceptor は、action mailer の
# before action 的な動きをしてくれるヤツ(らしい。。)
#
class EmailInterceptor
  #
  # ブラックリスト宛には送らない
  #
  def self.delivering_email(message)
    _to = message.to
    message.to = probe_email(message.to)   if message.to.present?
    message.cc = probe_email(message.cc)   if message.cc.present?
    message.bcc = probe_email(message.bcc) if message.bcc.present?

    message = set_to(message, _to)
  # rescue => e
  #   binding.pry
  #   Rails.logger.error(e.to_s << "\n" << e.backtrace.join("\n"))
  end

  #
  # ブラックリストに載っていないアドレスだけにする
  #
  def self.probe_email(emails)
    bounced_email_addresses = BouncedEmailAddress.pluck(:email)

    list = []
    emails = [emails] if emails.instance_of?(String)
    emails.each do |email|
      list << email if !(bounced_email_addresses.include?(email))
    end
    return list
  end

  #
  # toがすべて空だった場合
  # 送信処理するとエラーになるため、
  # とりあえず仮で送信先を入れる
  #
  def self.set_to(message, _to)
    return message if message.to.present?

    # ブラックリストに送ろうとしたメールはここに送信する
    message.to = ["sample@gmail.com"]
    # message.subject = "Can not sent to bouced email address"
    return message
  end
end

email_interceptor.rb は、lib/mail の配下とかにおいて、
config/application.rb で読みこむようにすればOK。

  class Application < Rails::Application
  # 略
    config.autoload_paths += %W(#{config.root}/lib/mail)
 # 略


※BouncedEmailAddressというモデルはブラックリスト用テーブル。
 メールアドレスを格納しているだけなのでソースは割愛。