AWS SES の SendBulkEmail API を使用してEメールを一括送信してみる

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

こんにちは😸
カスタマーサクセス部の山本です。

AWS SES の SendBulkEmail API

AWS SES の SendBulkEmail API を使用すると、メールを一括送信することができます。
この API では、一度に最大 50 件の宛先に向けてメールを送信することができます。
SES にはメールの送信テンプレートを作成する機能があり、宛先ごとに本文に挿入する内容を変更することもできます。

山本哲也さん宛には「こんにちは!山本 哲也さん」
吉川優子さん宛には「こんにちは!吉川 優子さん」
と書いたメールを、最大 50 人までを宛先にして一括送信できるということです。

SendBulkEmail API を使用する際のリクエストには以下のようなパラメータを使用します。

{
    "FromEmailAddress": "Asuka Tanaka <asuka.tanaka@example.com>",
    "DefaultContent": {
        "Template": {
            "TemplateContent": {
                "Subject": "Greetings, {{name}}!",
                "Text": "Dear {{name}},\r\nYour favorite animal is {{favoriteanimal}}.",
                "Html": "<h1>Hello {{name}},</h1><p>Your favorite animal is {{favoriteanimal}}.</p>"
            },
            "TemplateData": "{ \"name\":\"friend\", \"favoriteanimal\":\"unknown\" }"
        }
    },
    "BulkEmailEntries": [
        {
            "Destination": {
                "ToAddresses": [
                    "tetsuya.yamamoto@example.com"
                ]
            },
            "ReplacementEmailContent": {
                "ReplacementTemplate": {
                    "ReplacementTemplateData": "{ \"name\":\"Tetsuya\", \"favoriteanimal\":\"angelfish\" }"
                }
            }
        },
        {
            "Destination": {
                "ToAddresses": [
                    "yuko.yoshikawa@example.com"
                ]
            },
            "ReplacementEmailContent": {
                "ReplacementTemplate": {
                    "ReplacementTemplateData": "{ \"name\":\"Yuuko\", \"favoriteanimal\":\"dog\" }"
                }
            }
        }
    ],
    "ConfigurationSetName": "ConfigSet"
}

1. FromEmailAddress : 送信者情報

  • 送信元メールアドレス: Asuka Tanaka (asuka.tanaka@example.com)

2. DefaultContent.Template : デフォルトのメールテンプレート

  • テンプレートの内容
    • 件名
    • テキスト本文(HTML 本文を表示できないメールクライアントなどで使用される)
    • HTML 本文
  • テンプレートに入れる値
    • テンプレートの中で変更可能な値とした {{name}}{{favoriteanimal}} に入るデフォルト値を定義
      • name: "friend"
      • favoriteanimal: "unknown"

3. BulkEmailEntries : 送信先リスト

1通目

  • 宛先: tetsuya.yamamoto@example.com
  • 差し替えデータ:
    • name: "Tetsuya"
    • favoriteanimal: "angelfish"

2通目

  • 宛先: yuko.yoshikawa@example.com
  • 差し替えデータ:
    • name: "Yuuko"
    • favoriteanimal: "dog"

受信するメールのイメージ:

一括送信スクリプト(Boto3)

AWS SDK for Python (Boto3) を使用して、スクリプトを作成してみました。
同じフォルダにある宛先一覧 recipients.json を読み込んで動作します。
宛先 50 件を 1 通にまとめて送信し、1 秒待って、次の宛先 50 件を 1 通にまとめて送信し、宛先一覧の最後まで送信します。
私の検証環境はサンドボックス環境であるため、1秒間のEメール送信件数が 1 件に制限されています。
そのため、1回の送信ごとに 1 秒待つ処理を入れています。
参考:Amazon SES の Service Quotas - Amazon Simple Email Service
詳細は割愛しますが、本番利用申請を行い承認されたあとに、この制限は緩和申請することも可能です。

スクリプト内の主に、送信元メールアドレス(SESで検証済みのアドレス) 部分を環境に応じて変える必要があるかと思います。
あくまで私の検証用に書いたものなので、ご利用等は自己責任でお願いします。

  • bulk_email_sender.py
import boto3
import json
import time
import logging
from typing import List, Dict
from botocore.exceptions import ClientError

class BulkEmailSender:
    def __init__(self, source_email: str, sender_name: str):
        self.ses = boto3.client('sesv2')
        self.source_email = source_email
        self.sender_name = sender_name
        self.BATCH_SIZE = 50
        self.setup_logging()

    def setup_logging(self):
        logging.basicConfig(
            level=logging.INFO,
            format='%(asctime)s - %(levelname)s - %(message)s',
            handlers=[
                logging.FileHandler('email_sender.log'),
                logging.StreamHandler()
            ]
        )
        self.logger = logging.getLogger(__name__)

    def send_bulk_emails(self, recipients: List[Dict]) -> None:
        """メールを一括送信(バルクメール)"""
        total_sent = 0
        total_failed = 0
        
        # 50件ずつのバッチに分割
        for i in range(0, len(recipients), 50):
            batch = recipients[i:i+50]
            
            # バルクメールエントリーの準備
            bulk_email_entries = [
                {
                    'Destination': {
                        'ToAddresses': [recipient['email']]
                    },
                    'ReplacementEmailContent': {
                        'ReplacementTemplate': {
                            'ReplacementTemplateData': json.dumps(recipient['variables'])
                        }
                    }
                }
                for recipient in batch
            ]
            
            # デフォルトコンテンツテンプレート
            default_content = {
                'Template': {
                    'TemplateContent': {
                        'Subject': "Greetings, {{name}}!",
                        'Text': "Dear {{name}},\n\nYour favorite animal is {{favoriteanimal}}.",
                        'Html': "<h1>Hello {{name}},</h1><p>Your favorite animal is {{favoriteanimal}}.</p>"
                    },
                    'TemplateData': json.dumps({"name": "friend", "favoriteanimal": "unknown"})
                }
            }
            
            try:
                response = self.ses.send_bulk_email(
                    FromEmailAddress=self.source_email,
                    DefaultContent=default_content,
                    BulkEmailEntries=bulk_email_entries,
                    ConfigurationSetName="test"
                )
                
                # レスポンス処理の修正
                batch_sent = 0
                batch_failed = 0
                
                # BulkEmailEntryResultsから結果を取得
                for result in response.get('BulkEmailEntryResults', []):
                    if 'Error' in result:
                        batch_failed += 1
                        self.logger.error(f"Error in bulk email: {result.get('Error', 'Unknown error')}")
                    else:
                        batch_sent += 1
                        
                total_sent += batch_sent
                total_failed += batch_failed
                
                self.logger.info(f"Batch {i//50 + 1} complete: {batch_sent} sent, {batch_failed} failed")
                
            except ClientError as e:
                error_code = e.response.get('Error', {}).get('Code', 'Unknown')
                error_message = e.response.get('Error', {}).get('Message', 'Unknown error')
                self.logger.error(f"ClientError in batch {i//50 + 1}: {error_code} - {error_message}")
                total_failed += len(batch)
            
            time.sleep(1)  # バッチ間の遅延
        
        self.logger.info(f"全体の送信完了: {total_sent} 送信, {total_failed} 失敗")

    def _process_response(self, response: Dict) -> Dict:
        """レスポンスを処理"""
        successful = []
        failed = []
        
        for result in response.get('BulkEmailEntryResults', []):
            if 'Error' not in result:
                successful.append(result.get('MessageId'))
            else:
                failed.append({
                    'message_id': result.get('MessageId'),
                    'error': result.get('Error')
                })
        
        return {
            'successful': successful,
            'failed': failed
        }

def main():
    # JSONファイルから受信者データを読み込む
    with open('recipients.json', 'r') as f:
        data = json.load(f)
        recipients = data['recipients']

    # 送信元メールアドレス(SESで検証済みのアドレス)
    source_email = "yamamoto@karukozaka46.click"
    sender_name = "Tetsuya Yamamoto"

    # 送信処理
    sender = BulkEmailSender(source_email, sender_name)
    
    # メール送信
    sender.send_bulk_emails(recipients)

if __name__ == "__main__":
    main()
  • recipients.json
{
  "recipients": [
    {
      "email": "yamamoto@example.com",
      "variables": {
        "name": "Tetsuya",
        "favoriteanimal": "Dog"
      }
    },
    {
      "email": "yamamoto@example.com",
      "variables": {
        "name": "Tetsuya",
        "favoriteanimal": "Cat"
      }
    },
    {
      "email": "yamamoto@example.com",
      "variables": {
        "name": "Tetsuya",
        "favoriteanimal": "Bird"
      }
    }
  ]
}

実行ログの例:

受け取ったメール:

スクリプトを利用し、 宛先1 万件にメールを送信してみる

先のスクリプトを利用し、実際に宛先 1 万件に向けてEメールを送信してみました。
送信クォータ(Amazon SES の Service Quotas - Amazon Simple Email Service)にもあるように、サンドボックス環境での 1 日あたりの送信上限は 200 通になっています。
ただし、「クォータは、メッセージ数ではなく、受信者の数に基づきます。」と書かれているため、宛先 1 万件をすべて私の 1 つのメールアドレスにすることで、検証できそうです。
もちろん、メール受信サーバが一度に 1 万件のメールを受け取ることになるため、安易に検証しないことをお勧めします。

recipients.json の宛先に 1 万件を書き込みました。

  • recipients.json
{
  "recipients": [
    {
      "email": "yamamoto@example.com",
      "variables": {
        "name": "Tetsuya",
        "favoriteanimal": "animal_1"
      }
    },
    {
      "email": "yamamoto@example.com",
      "variables": {
        "name": "Tetsuya",
        "favoriteanimal": "animal_2"
      }
    },
・・・・・略・・・・・
    {
      "email": "yamamoto@example.com",
      "variables": {
        "name": "Tetsuya",
        "favoriteanimal": "animal_10000"
      }
    }
  ]
}

送信にかかる時間

日本時間の13:02:56 くらいに送信し、5 分程度で送信できました。
秒間 43 件送っている計算となり、クォータ値の秒間 50 件に近い値です。
スクリプトの中で 1 秒待っているので、ほかの処理をする時間も考えると妥当な気がします。
合計 10,000 件すべて送れています。

  • 13:02-13:03 の間
    • 164 件
  • 13:03-13:04 の間
    • 2,586 件
  • 13:04-13:05 の間
    • 2,581 件
  • 13:05-13:06 の間
    • 2,575 件
  • 13:05-13:06 の間
    • 2,094 件

Send は送信リクエストの成功数です。

参考:Amazon SES 送信アクティビティのモニタリング - Amazon Simple Email Service

送信数— 送信リクエストが成功すると、Amazon SES はそのメッセージを受信者のメールサーバーに配信しようと試行します。(アカウントレベルまたはグローバル抑制が使用されている場合でも、SES により送信済みとしてカウントされますが、配信は抑制されます)。

受信したメール数と到着までにかかった時間

受信したメールは 9262 通でした。
スクリプトでは直列処理をしているものの、受信は順番通りではないようでした。

残りの 738 通は、ハードバウンス Bounce になったようです。
送信開始から 4~5 分後の 13:06 ~ 13:07 (日本時間) 頃に、ハードバウンスが発生していました。

到着までにかかった時間

  • 最初の 100 件
    • 送信時刻 13:02 から1分程度で受信ボックスに到着
  • 1,000 件目
    • 送信時刻 13:02 から1分程度で受信ボックスに到着
  • 1,020 件目
    • 13:04 くらいから10 分程度メールが全く受信ボックスに入らなくなり、その後また継続的にメールが入るようになるという事象が発生。
    • ハードバウンスが発生した時間であるため、受信サーバがそれらのメールの処理をしていたのかもしれません。
    • 13:15 頃に到着
  • 5,000 件目
    • 引き続き受信の遅延が発生している状況
    • 14:40 頃に到着
  • 最後の 9,262 件目
    • 引き続き受信の遅延が発生している状況
    • 18:18 に到着

CloudWatch に DeliveryDelay のメトリクスは出ていないため、受信サーバー側の一時的な障害ではなく、受信ボックスがいっぱいになっているという原因でもなさそうです。
受信サーバーから私のメールボックスへの配信が遅くなっているのだと思います。

配信の遅延 – 一時的な問題が発生したため、メールを受信者のメールサーバーに配信できませんでした。配信の遅延は、受信者の受信トレイがいっぱいになった場合や、受信側の電子メールサーバーで一時的な問題が発生した場合などに発生します。

参考:Amazon SES の Amazon SNS 通知コンテンツ - Amazon Simple Email Service

バウンス

インターネット経由の E メールに関する最大の問題の 1 つは、未承諾一括 E メール (スパム) です。E メールプロバイダーは、顧客にスパムが送信されないように広範な防止策を実行しています。

参考:Amazon SES における E メール配信可能性の概要 - Amazon Simple Email Service

今回のように、一人の受信者に一度に多くのメールを送ると、受信者側のメールサーバーからハードバウンスを受け取るといったことがあります。
バウンスには一般的に以下2種類があります。

  • ソフトバウンス:本来は送信できるのはずのメールが一時的な障害で送れない。受信側の問題。
    • 受信側の受信メールボックスがいっぱい
    • 受信側のメールサーバー障害
    • メールサイズ大きい
  • ハードバウンス:本来的に送信できない。送信側の問題。
    • 送信先メールアドレスが無効
    • 受信側が受信拒否設定をしている
    • 受信側で迷惑メールと判定された

AWS SES の場合は、以下の CloudWatch メトリクスが対応します。
参考:Amazon SES 送信アクティビティのモニタリング - Amazon Simple Email Service

  • ソフトバウンス
    • DeliveryDelay
      • 公式ドキュメントの記載:「配信の遅延 – 一時的な問題が発生したため、メールを受信者のメールサーバーに配信できませんでした。配信の遅延は、受信者の受信トレイがいっぱいになった場合や、受信側の電子メールサーバーで一時的な問題が発生した場合などに発生します。」
  • ハードバウンス
    • Bounce
      • 例外として、Amazon SES から複数回メールを送信をしても配信できない場合のリトライ失敗のソフトバウンスも含みます。
      • 公式ドキュメントの記載:「バウンス – ハードバウンスにより、受信者のメールサーバーが E メールを完全に拒否しました。(ソフトバウンスは、SES が E メールの配信を再試行しない場合にのみ含まれます。通常、これらのソフトバウンスは配信障害を示しますが、メールが受信者の受信トレイに正常に届いた場合でも、場合によってはソフトバウンスが返されることがあります。これは通常、受信者が不在の自動返信を送信した場合に発生します。ソフトバウンスの詳細については、この AWS re:Post 記事を参照してください)。」

宛先となっているメールアドレスが存在しない場合に Baunce が発生するフロー図です。
Complaint は「苦情」です。AWS SES を含むメール配信サービスではバウンス率と苦情率を減らす努力が必要です。後述します。
以下公式ドキュメントより抜粋
Amazon SES における E メール送信の仕組み - Amazon Simple Email Service

  • 苦情
    • Complaint
      • 公式ドキュメントの記載:「苦情— Eメールは受信者のメールサーバーに正常に配信されましたが、受信者はスパムとしてマークしました。」

ハードバウンスや苦情が多いとメールを送信できなくなる

送信したメールについてハードバウンスや苦情になる割合が多いと、スパムや迷惑メールの配信者と推定され、メールの配信ができなくなります。
AWS の場合は以下のように規定されているようです。

  • バウンス率に関する記載

バウンス率は、Amazon SES アカウントによって生成されたハードバウンスの数に基づいています。

最良の結果を得るには、バウンス率を 5% 未満に保ちます。これより高いバウンス率は、E メールの配信に影響する可能性があります。バウンス率が 5% 以上になると、アカウントは自動的にレビュー対象になります。バウンス率が 10% を超える場合は、高いバウンス率の原因となった問題が解決するまで、お客様のアカウントの以後の E メール送信を一時停止することがあります。

  • 苦情率に関する記載

最良の結果を得るには、苦情率を 0.1% 未満に維持してください。これより高い苦情率は、E メールの配信に影響する可能性があります。苦情率が 0.1% 以上になると、アカウントは自動的にレビュー対象になります。苦情率が 0.5% を超える場合は、高い苦情率の原因となった問題が解決するまで、お客様のアカウントの以後の E メール送信を一時停止することがあります。

参考:評価メトリクスのメッセージ - Amazon Simple Email Service

アカウントの停止状態や、バウンス率、苦情率を SES のサービス画面で確認する

SES のサービス画面には「アカウントダッシュボード」があり、状態を確認できます。
私の検証用アカウントは「正常」でした。

  • [正常] - 現在、アカウントに影響する問題はありません。
  • [Under review] - お客様のアカウントはレビュー対象です。アカウントのレビュー対象となった原因の問題が、レビュー期間の終了までに修正されなかった場合、アカウントの E メール送信が一時停止される可能性があります。
  • [Pending end of review decision] - お客様のアカウントはレビュー対象です。お客様のアカウントをレビュー対象とした問題の内容により、何らかの処置を行う前に、手動でレビューを実行する必要があります。
  • [Sending paused (送信一時停止)] - アカウントの E メール送信機能を一時停止しました。アカウントが一時停止されている間は、Amazon SES を使用して E メールを送信することはできません。この決定の見直しをリクエストできます。レビューをリクエストする方法の詳細については、「Amazon SES 送信レビュープロセスに関するよくある質問」を参照してください。
  • [Pending sending pause (送信一時停止保留中)] - お客様のアカウントはレビュー対象です。アカウントをレビュー対象とした問題は解決されていません。このような状況では、通常アカウントの E メール送信機能を一時停止します。ただし、アカウントの性質により、何らかの処置を行う前に、アカウントの確認が行われます。

参考:評価メトリクスを使用して返送率と苦情率を追跡する - Amazon Simple Email Service

アカウントダッシュボードでは送信メール数や、バウンス率、苦情率も確認できます。

バウンスや苦情に関する個別の通知を受け取る

SES の「設定セット」にある通知機能を使用して、Amazon SNS 経由でバウンスや苦情に関する個別の通知を受け取ることもできます。

以下の公式ドキュメントより抜粋します。
Amazon SES が Amazon SNS に発行するイベントデータの例 - Amazon Simple Email Service

以下の例では、ハードバウンスになったメールアドレスや送信内容が分かります。
この通知を活用して、ハードバウンスを起こさないように対策することができます。

{
  "eventType":"Bounce",
  "bounce":{
    "bounceType":"Permanent",
    "bounceSubType":"General",
    "bouncedRecipients":[
      {
        "emailAddress":"recipient@example.com",
        "action":"failed",
        "status":"5.1.1",
        "diagnosticCode":"smtp; 550 5.1.1 user unknown"
      }
    ],
    "timestamp":"2017-08-05T00:41:02.669Z",
    "feedbackId":"01000157c44f053b-61b59c11-9236-11e6-8f96-7be8aexample-000000",
    "reportingMTA":"dsn; mta.example.com"
  },
  "mail":{
    "timestamp":"2017-08-05T00:40:02.012Z",
    "source":"Sender Name <sender@example.com>",
    "sourceArn":"arn:aws:ses:us-east-1:123456789012:identity/sender@example.com",
    "sendingAccountId":"123456789012",
    "messageId":"EXAMPLE7c191be45-e9aedb9a-02f9-4d12-a87d-dd0099a07f8a-000000",
    "destination":[
      "recipient@example.com"
    ],
    "headersTruncated":false,
    "headers":[
      {
        "name":"From",
        "value":"Sender Name <sender@example.com>"
      },
      {
        "name":"To",
        "value":"recipient@example.com"
      },
      {
        "name":"Subject",
        "value":"Message sent from Amazon SES"
      },
      {
        "name":"MIME-Version",
        "value":"1.0"
      },
      {
        "name":"Content-Type",
        "value":"multipart/alternative; boundary=\"----=_Part_7307378_1629847660.1516840721503\""
      }
    ],
    "commonHeaders":{
      "from":[
        "Sender Name <sender@example.com>"
      ],
      "to":[
        "recipient@example.com"
      ],
      "messageId":"EXAMPLE7c191be45-e9aedb9a-02f9-4d12-a87d-dd0099a07f8a-000000",
      "subject":"Message sent from Amazon SES"
    },
    "tags":{
      "ses:configuration-set":[
        "ConfigSet"
      ],
      "ses:source-ip":[
        "192.0.2.0"
      ],
      "ses:from-domain":[
        "example.com"
      ],
      "ses:caller-identity":[
        "ses_user"
      ]
    }
  }
}

例えば、無効なメールアドレスは「サプレッションリスト」に入れることができます。
サプレッションリストに登録したメールアドレス宛にメールを送信しても、実際には送信しないということができ、再発を防ぐことができます。

サプレッションリストへの登録については、またブログ記事を書こうと思います。

まとめ

SendBulkEmail API により、個別にカスタマイズしたメールを一括で送信でき、マーケティングなどで活用できると思います。
受信者側に負担をかけないよう、適切な数のメールを送ることや、ハードバウンス Bounce や、苦情 Complaint には注意が必要です。
「アカウントダッシュボード」には現在のハードバウンス・苦情率を表示していますので、状況をすぐに確認できます。
ハードバウンス Bounce や、苦情 Complaint といった CloudWatch メトリクスにはアラームを設定して、通知することも可能ですので、活用してみてもいいかもしれません。 苦情やハードバウンスの割合を表す Reputation.ComplaintRateReputation.BounceRate もあります。 苦情やハードバウンスを繰り返さないように、サプレッションリストを有効活用することも大切です。

余談

富士山の雪がなくなり、今年も開山したようです。
また登りに行きたいです。

山本 哲也 (記事一覧)

カスタマーサクセス部のインフラエンジニア。

山を走るのが趣味です。