概要
今回は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では、有効な対策を講じずに、バウンス率が高い状態を保ったままになっていると、メールが一時的に配信停止となる場合もあります。(一応事前に警告は来ます。) ということなので、しっかりバウンス対策しましょう。
最後までご覧いただきありがとうございました。少しでも参考になっていれば幸いです。