【バウンス対策】Amazon SESでバウンスが発生したメールアドレスをDynamoDBに保存する処理作ってみた

記事タイトルとURLをコピーする

概要

今回はAmazon SES(以下、SES)でバウンスが発生した時に、バウンス情報をAmazon SNS(以下、SNS)とAWS Lambda(以下、Lambda)を使って、Amazon DynamoDB(以下、DynamoDB)に保存する処理について解説します。

バウンス対策として様々な方法がありますが、今回は「バウンスが発生した宛先・差出メールアドレスをDynamoDBに保存し、メール送信時に、都度そのデータを確認しにいき、過去バウンスした宛先は除外する」といったような処理を行う想定で、その中の「バウンスが発生した宛先・差出メールアドレスをDynamoDBに保存する処理」についてお話しようと思います。

処理全体の流れ

バウンス処理

  1. SESからEメールが送信されたときに、バウンスが発生

  2. SESからSNSにバウンスしたメールのデータを渡す

  3. SNSからLambdaにバウンスのデータを含む通知のデータを渡す

  4. Lambdaで必要なデータを抽出し、boto3を使用し、DynamoDBに登録する。

手順

リソースの詳細な構築手順は今回の趣旨とはずれるため、簡単に紹介します。

AWS Lambda

基本的にデフォルトの設定で作成します。 付与するIAMロールにDynamoDBにアクセスできるポリシーのアタッチだけお忘れなきように。

Amazon SNS

トピックの作成

基本的にデフォルトの設定で作成します。 タイプのみ「スタンダード」で作成してください。

※「FIFO」で作成すると、サブスクリプションに直接Lambdaを設定できません。

サブスクリプションの設定

先ほど作ったLambdaを登録します。

Amazon SES

※検証済みのIDが存在する前提で進めます。

左ペインの[検証済みID]から今回使用するIDを選択し、[通知]タブを選択する。

[フィードバック通知]の編集から、バウンスの[バウンスフィードバック]に先ほど作成した SNSトピック を選択する。

[元のEメールヘッダーを含める]項目にもチェックを入れる。(今回は、上記に添付したJSONに合わせるためにオンにする。特にヘッダーから情報を取得する必要がない場合はオフでよい)

これで、バウンスが発生したときにSNSに連携されるようになりました。

Amazon DynamoDB

基本的にデフォルトの設定で作成します。

パーティションキー:from_mailaddress

ソートキー:to_mailaddress

でテーブルを作成しました。

バウンスしたメールアドレスを登録する処理

さて、ここから本題です。

まず、Lambdaに渡されたデータの中から、必要なデータを抽出するわけですが、SES-> SNS -> Lambdaと渡されてきたデータはどのような構造になっているのかというと、、

参考

SES->SNS(https://docs.aws.amazon.com/ja_jp/ses/latest/dg/notification-examples.html

SNS->Lambda(https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/with-sns.html

最終的に以下のようなデータがLambdaに渡されています。

SES->SNSに渡されたデータ、つまりバウンスについてのデータはというと、["Records"][0]["Sns"]["Message"]内に格納されています。

{
  "Records": [
    {
      "EventVersion": "1.0",
      "EventSubscriptionArn": "arn:aws:sns:us-east-1:123456789012:sns-lambda:21be56ed-a058-49f5-8c98-aedd2564c486",
      "EventSource": "aws:sns",
      "Sns": {
        "SignatureVersion": "1",
        "Timestamp": "2019-01-02T12:45:07.000Z",
        "Signature": "tcc6faL2yUC6dgZdmrwh1Y4cGa/ebXEkAi6RibDsvpi+tE/1+82j...65r==",
        "SigningCertUrl": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-ac565b8b1a6c5d002d285f9598aa1d9b.pem",
        "MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e",
        "Message": {
          "notificationType":"Bounce",
          "bounce":{
            "bounceType":"Permanent",
            "reportingMTA":"dns; email.example.com",
            "bouncedRecipients":[
                {
                  "emailAddress":"jane@example.com",
                  "status":"5.1.1",
                  "action":"failed",
                  "diagnosticCode":"smtp; 550 5.1.1 <jane@example.com>... User"
                }
            ],
            "bounceSubType":"General",
            "timestamp":"2016-01-27T14:59:38.237Z",
            "feedbackId":"00000138111222aa-33322211-cccc-cccc-cccc-ddddaaaa068a-000000",
            "remoteMtaIp":"127.0.2.0"
          },
          "mail":{
            "timestamp":"2016-01-27T14:59:38.237Z",
            "source":"john@example.com",
            "sourceArn": "arn:aws:ses:us-east-1:888888888888:identity/example.com",
            "sourceIp": "127.0.3.0",
            "sendingAccountId":"123456789012",
            "callerIdentity": "IAM_user_or_role_name",
            "messageId":"00000138111222aa-33322211-cccc-cccc-cccc-ddddaaaa0680-000000",
            "destination":[
              "jane@example.com",
              "mary@example.com",
              "richard@example.com"],
            "headersTruncated":false,
            "headers":[ 
              { 
                "name":"From",
                "value":"\"John Doe\" <john@example.com>"
              },
              { 
                "name":"To",
                "value":"\"Jane Doe\" <jane@example.com>, \"Mary Doe\" <mary@example.com>, \"Richard Doe\" <richard@example.com>"
              },
              { 
                "name":"Message-ID",
                "value":"custom-message-ID"
              },
              { 
                "name":"Subject",
                "value":"Hello"
              },
              { 
                "name":"Content-Type",
                "value":"text/plain; charset=\"UTF-8\""
              },
              { 
                "name":"Content-Transfer-Encoding",
                "value":"base64"
              },
              { 
                "name":"Date",
                "value":"Wed, 27 Jan 2016 14:05:45 +0000"
              }
            ],
            "commonHeaders":{ 
              "from":[ 
                "John Doe <john@example.com>"
              ],
              "date":"Wed, 27 Jan 2016 14:05:45 +0000",
              "to":[ 
                "Jane Doe <jane@example.com>, Mary Doe <mary@example.com>, Richard Doe <richard@example.com>"
              ],
              "messageId":"custom-message-ID",
              "subject":"Hello"
            }
          }
        },
        "MessageAttributes": {
          "Test": {
            "Type": "String",
            "Value": "TestString"
          },
          "TestBinary": {
            "Type": "Binary",
            "Value": "TestBinary"
          }
        },
        "Type": "Notification",
        "UnsubscribeUrl": "https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&amp;SubscriptionArn=arn:aws:sns:us-east-1:123456789012:test-lambda:21be56ed-a058-49f5-8c98-aedd2564c486",
        "TopicArn":"arn:aws:sns:us-east-1:123456789012:sns-lambda",
        "Subject": "TestInvoke"
      }
    }
  ]
}

次は、この中から必要なデータを抽出する作業です。

今回必要となるデータ(バウンスした宛先、差出メールアドレス)が格納されている場所は以下の通りです。

宛先メールアドレス

["Records"][0]["Sns"]["Message"]["bounce"]内の

"bouncedRecipients": [
    {
        "emailAddress":"jane@example.com",
        "status":"5.1.1",
        "action":"failed",
        "diagnosticCode":"smtp; 550 5.1.1 <jane@example.com>... User"
    }
],

この場合、jane@example.comがバウンスした宛先メールアドレスになります。

差出メールアドレス

["Records"][0]["Sns"]["Message"]["mail"]内の

"source":"john@example.com",

この場合、john@example.comがバウンスした差出メールアドレスになります。

実際のコード

これらの必要なデータを取得して、boto3を使ってDynamoDBに重複がないように登録する処理を書いてみました。

import json
import os
import boto3
from boto3.dynamodb.conditions import Key
 
BOUNCELIST_TABLE_NAME = os.environ["BOUNCELIST_TABLE_NAME"]
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(BOUNCELIST_TABLE_NAME)
 
 
def lambda_handler(event, context):
    # SESからSNSに渡されたデータを引数から取得
    ses_message = json.loads(event["Records"][0]["Sns"]["Message"])
    # バウンスした宛先メールアドレスをリストに格納する
    to_address_list = get_to_mailaddress(ses_message)
    # 送信元のメールアドレスを取得する
    from_address = ses_message["mail"]["source"]
    # 送信元のメールアドレスで絞り込んで、DynamoDBからバウンスリスト(過去バウンスしたアドレス)取得
    # to_address_list(今回バウンスしたアドレス)と照合し、重複する(=すでにバウンスリストに登録されている)場合はto_address_listから削除
    to_address_list = get_target_bounce_list(from_address, to_address_list)
    # バウンスリストに追加するデータが存在する場合、テーブルに追加する
    if to_address_list:
        add_bounce_list(to_address_list, from_address)
    return
 
 
def get_to_mailaddress(ses_message: dict) -> list:
    bounce_list = ses_message["bounce"]["bouncedRecipients"]
    address_list = []
    for bounce in bounce_list:
        mailaddress = bounce["emailAddress"]
        address_list.append(mailaddress)
    return address_list
 
 
def get_target_bounce_list(from_address: str, to_address_list: list) -> list:
    bounce_list = get_bounce_list(from_address)
    for bounced in bounce_list:
        if bounced["to_mailaddress"] in to_address_list:
            to_address_list.remove(bounced["to_mailaddress"])
    return to_address_list
 
 
def get_bounce_list(from_address: str) -> list:
    response = table.query(
        KeyConditionExpression=Key("from_mailaddress").eq(from_address)
    )
    return response["Items"]
 
 
def add_bounce_list(to_address_list: list, from_address: str) -> None:
    for address in to_address_list:
        table.put_item(
            Item={
                "from_mailaddress": from_address,
                "to_mailaddress": address
            }
        )

テスト

では、実際にバウンスが発生したときの挙動をSESのテスト機能を使って確認してみます。

SESの画面左ペインの[検証済みID]から今回使用するIDを選択すると、右上に[テストEメールの送信]と出てくるので、押下します。

次の画面で、テストするメッセージの詳細を決めます。

条件に合わせて、メールの形式や、送り主のメールアドレス等カスタマイズできるようになっていますが、今回はバウンスのテストをするので、[シナリオ]でバウンスを選択ます。

選択するとなにやらメールアドレスが表示されますが、「bounce@simulator.amazonses.comに対してメール送信時にバウンスが発生した」というシナリオでテストされるということです。

入力を終えたら、テストEメールの送信をします。

特に問題なくいけば、「正常に送信されました」表示がされるかと思います。

実際にDynamoDBを見にいくと、以下のようにデータが登録されており、無事にLamdaで処理が行われていることが確認できます。

もし、データが登録できていないなどあれば、CloudWatchLogsでLambdaのログを確認するなどして対応してみてください。

ちなみに、もう一度全く同じテストを行った場合、重複するデータは登録されない処理をLambdaに書いているので、さらにDynamoDBにデータが登録されることはありません。

最後に

Amazon SESでは、有効な対策を講じずに、バウンス率が高い状態を保ったままになっていると、メールが一時的に配信停止となる場合もあります。(一応事前に警告は来ます。) ということなので、しっかりバウンス対策しましょう。

最後までご覧いただきありがとうございました。少しでも参考になっていれば幸いです。

平松 暢顕 (記事一覧)

22卒。日々勉強中。

少しでもお役に立てる情報を発信できるよう頑張ります