概要
今回はAmazon SES(以下、SES)でバウンスが発生した時に、バウンス情報をAmazon SNS(以下、SNS)とAWS Lambda(以下、Lambda)を使って、Amazon DynamoDB(以下、DynamoDB)に保存する処理について解説します。
バウンス対策として様々な方法がありますが、今回は「バウンスが発生した宛先・差出メールアドレスをDynamoDBに保存し、メール送信時に、都度そのデータを確認しにいき、過去バウンスした宛先は除外する」といったような処理を行う想定で、その中の「バウンスが発生した宛先・差出メールアドレスをDynamoDBに保存する処理」についてお話しようと思います。
処理全体の流れ
SESからEメールが送信されたときに、バウンスが発生
SESからSNSにバウンスしたメールのデータを渡す
SNSからLambdaにバウンスのデータを含む通知のデータを渡す
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&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では、有効な対策を講じずに、バウンス率が高い状態を保ったままになっていると、メールが一時的に配信停止となる場合もあります。(一応事前に警告は来ます。) ということなので、しっかりバウンス対策しましょう。
最後までご覧いただきありがとうございました。少しでも参考になっていれば幸いです。