$shibayu36->blog;

株式会社はてなでエンジニアをしています。プログラミングや読書のことなどについて書いています。

Amazon SESとSNSを利用してバウンスメールを自動的にハンドリングする

メールを送るアプリケーションを作成していると、使われていないメールアドレスで登録された時や、携帯のメールアドレス変更によって登録されているメールアドレスが使えない状態になって、メール送信時にバウンスメールとして返ってくることがある。この時バウンスメールとして返ってくるメールアドレスをアプリケーション側で無効にするなどしておかないと、メール送信の無駄が発生する。また、AWS SESを使っている場合、バウンス率が高くなった場合、規制されることもある( https://docs.aws.amazon.com/ja_jp/ses/latest/DeveloperGuide/e-faq-bn.html )。

今回は、AWSを利用してバウンスメールとして返ってきたメールアドレスを自動的にハンドリングするというのをやってみたので、それを書いてみる。

前提

今回はAWS SESを利用して、メールを送信しているということを前提とする。

概要

  • Amazon SESでバウンスメールが返ってきた時にAmazon SNSに通知する
  • Amazon SNSに通知が来たら、特定エンドポイントに向けてリクエストを飛ばす
  • エンドポイントでAmazon SNSからの通知を受け取り、無効なメールアドレスをハンドリングする

Amazon SESでバウンスメールが返ってきた時にAmazon SNSに通知する

まずAmazon SESでメールを送った際、バウンスメールが返ってきたらAmazon SNSに通知を送るようにする。Amazon SNSとはプッシュ通知サービス( https://aws.amazon.com/jp/sns/ )で、通常モバイル通知などを担当するが、SESのエラーハンドリングにも用いることが出来る。

これは https://docs.aws.amazon.com/ja_jp/ses/latest/DeveloperGuide/configure-sns-notifications.html あたりを見ながら設定すれば良い。

まず最初にAmazon SNS上で、バウンスメール通知を受け取るためのTopicを作成する。Amazon SNSのHomeからCreate Topicを選択し、Topic Nameなどを入力してTopicを作成する。
f:id:shiba_yu36:20150827091823p:plain:h500
f:id:shiba_yu36:20150827092013p:plain:h300

続いてAmazon SES上で、作成したSNS Topicに対して、Bounce通知が行われるように設定する。Amazon SESのコンソールにいき、Domains > (バウンス通知を設定したいドメイン)を選択し、View Detailsを押し、NotificationsのEdit Configurationから設定する。Bouncesに先ほどのTopicを選択すればOK。

f:id:shiba_yu36:20150827092327p:plain:h300
f:id:shiba_yu36:20150827092348p:plain:h300

最後にAmazon SNS上でそのTopicをSubscribeすれば通知されるようになる。ここではひとまずEmailでBounce通知を受け取れるようにする。Amazon SNS上で設定したいTopicを選択し、Subscribe to topicしてEmailを選択すると良い。
f:id:shiba_yu36:20150827093223p:plain:h300
f:id:shiba_yu36:20150827092609p:plain:h300

ここまででAmazon SESでメールを送った際に、送り先からバウンスメールが届いたら、Amazon SNSに通知を飛ばし、その内容をメールで受け取る、ということが出来た。

Amazon SNSに通知が来たら、特定エンドポイントに向けてリクエストを飛ばす

Amazon SNSでは、通知が届いた時にHTTPもしくはHTTPSのエンドポイントにリクエストを飛ばすように設定もできる。これは簡単。先ほどのように、Subscribe to Topicをして、HTTPもしくはHTTPSを選択し、URLを入れるだけで良い。

f:id:shiba_yu36:20150827093342p:plain:h300

エンドポイントでAmazon SNSからの通知を受け取り、無効なメールアドレスをハンドリングする

Subscribeを手動で承認する

以上の設定まで行っても、まだHTTPへの通知がPending状態になっている。そのエンドポイントに送られたSubscriptionConfirmationに応答しないといけないためである。

一番簡単な方法は、そこに通知されたJSONからSubscribeURLをコピーし、ブラウザでアクセスすることである。これでSubscribeは完了できる。

ただしこれだと毎回設定のたびに人力でアクセスすることになるので、ここを自動化する。

Subscribeを自動で承認する

Subscribeを自動で承認するには、

のを知る必要がある。上のエントリを読んでおくと良い。

まず別の場所からのなりすましのアクセスを防ぐために、署名の検証をする必要がある。これは公式でAWS SDKが提供されている場合はそれを利用すれば良い。今回はPerlで実装したので、CPANモジュールのAWS::SNS::Verifyというのを利用する。

また、他の人が作成した別の人のTopicからのSubscribeでなりすます、というのを防ぐために、TopicArnの検証も同時に行う必要がある。これは http://dev.classmethod.jp/cloud/aws/amazon-sms-message-verification/ を見ると分かりやすい。


これらの検証が済んだら、SubscribeURLに対してアクセスすれば良い。自動的に承認するためのコードは以下のとおりである。

# $reqがPlack::Requestとする
my $sns = AWS::SNS::Verify->new(body => $req->content);
return unless $sns->verify;

my $message = $sns->message;
return unless $message->{TopicArn} eq '...'; # 自分のTopicArn

if ($message->{Type} eq 'SubscriptionConfirmation') {
    my $furl = Furl->new(timeout => 3);
    $furl->get($sns->message->{SubscribeURL});
}

Bounce通知が届いた時に無効なメールアドレスをハンドリングする

ここまででBounce通知が届いたらエンドポイントにJSONがPOSTされるようになっているので、次は無効なメールアドレスをハンドリングする。

SESでのBounce通知の形式は https://docs.aws.amazon.com/ja_jp/ses/latest/DeveloperGuide/notification-contents.html を参照する。これによると

  • もう使えないアドレスの場合はnotificationTypeにBouncebounce->bounceTypeにPermanentが入っている
  • bounce->bouncedRecipientsに無効なメールアドレスが入っている

という事がわかる。そこで無効なメールアドレスをハンドリングするために、これらを用いる。もちろんこの時も署名の検証などは行わないといけないので注意すること。

# $reqがPlack::Requestとする
my $sns = AWS::SNS::Verify->new(body => $req->content);
return unless $sns->verify;

my $message = $sns->message;
return unless $message->{TopicArn} eq '...'; # 自分のTopicArn

if ($message->{Type} eq 'Notification') {
    my $ses_message = decode_json($message->{Message});
    return unless $ses_message->{notificationType} eq 'Bounce' && $ses_message->{bounce}->{bounceType} eq 'Permanent';

    my $unavailable_mail_addresses = [
        map { $_->{emailAddress} } @{ $ses_message->{bounce}->{bouncedRecipients} },
    ];

    # あとはこの$unavailable_mail_addressesに対して順に処理をしていく
}

これで無効なメールアドレスを処理することができる。

最終的なエンドポイントの実装

Subscribeを自動で承認するのと、無効なメールアドレスのハンドリングは同じエンドポイントで行わないといけないので、最終的なコードは以下のようになる。

# $reqがPlack::Requestとする
my $sns = AWS::SNS::Verify->new(body => $req->content);
return unless $sns->verify;

my $message = $sns->message;
return unless $message->{TopicArn} eq '...'; # 自分のTopicArn

if ($message->{Type} eq 'SubscriptionConfirmation') {
    my $furl = Furl->new(timeout => 3);
    $furl->get($sns->message->{SubscribeURL});
}
elsif ($message->{Type} eq 'Notification') {
    my $ses_message = decode_json($message->{Message});
    return unless $ses_message->{notificationType} eq 'Bounce' && $ses_message->{bounce}->{bounceType} eq 'Permanent';

    my $unavailable_mail_addresses = [
        map { $_->{emailAddress} } @{ $ses_message->{bounce}->{bouncedRecipients} },
    ];

    # あとはこの$unavailable_mail_addressesに対して順に処理をしていく
}

またバウンスメールのテストをしたい場合は、そのためのメールアドレスがあるので利用すると良い。
https://docs.aws.amazon.com/ja_jp/ses/latest/DeveloperGuide/mailbox-simulator.html

まとめ

今回はAmazon SESとAmazon SNSを利用して自動的にバウンスメールに対応する方法について書いた。AWSは便利。