giftee Tech Blog

ギフティの開発を支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信しています。

Amazon SES を用いたメール送信の結果を可能な限り正確に判定する方法

eyecatch

こんにちは、ギフティでエンジニアをしている hsato です。

この記事では、Amazon SES(Simple Email Service)を使ってメール送信を行う場合、どのようにして送信結果を取得し、それらをどのように活用することで可能な限り正確に送信結果を判定することができるのかについて解説します。

はじめに

「メール送信の結果」と聞くと、まず思い浮かべるのは 成功 もしくは 失敗 といったシンプルなステータスでしょう。しかし、実際には「失敗」にもいくつかのパターンがあります。たとえば、以下のようなケースが考えられます:

  • 永久的な失敗:メールアドレスの入力ミスや存在しないアドレスへの送信など、再試行しても必ず失敗するケース。
  • 一時的な失敗:一時的なインフラの不調や受信サーバーの問題など、再試行すれば成功する可能性があるケース。

このような状況を正確に把握せず再試行を繰り返すと、不要なリソース消費を招くだけでなく、Amazon SES の利用が制限されるリスクがあります。特に、永久的な失敗に対して再試行を続けることは、運用において避けたいケースのひとつです。

そのため、送信結果を正確に判定し、それぞれのケースに応じた適切な対応を行うことが、Amazon SES の安定運用において重要な要素となります。

正確な送信結果の判定がもたらすメリット

メール送信の結果を可能な限り正確に判定することにより、以下のようなメリットを得られます:

  • 送信結果の可視化

    • メールが送信先のメールアドレスに確実に届いたかどうかを把握できるようになります。
  • 失敗原因の特定

    • メール送信が失敗した場合、その原因をある程度特定できるようになります。
    • これにより、失敗の原因に応じた適切な対応を実施できます。
    • 例:メールアドレスの入力ミスや受信サーバーの問題など。
  • 成功時のみ実行される処理の実現

    • 送信成功時にのみ特定の処理を実行するロジックを実装することが可能になります。
    • 例:重要な通知メールが確実に送信された場合のみ後続の業務処理を進める。

これらのメリットを最大限に活用するためには、送信結果を正確に把握し、適切に管理する仕組みが必要不可欠です。

メール送信に関する情報を取得する方法

Amazon SESを利用したメール送信では、Amazon SES が Amazon SNS に発行するイベントデータ(以下「SESイベントデータ」)を活用することで、以下のような送信結果に関する詳細な情報を取得できます:

  • 送信成功・失敗のステータス
  • 失敗の具体的な原因(例:ハードバウンスやソフトバウンス)
  • 送信後の受信者の操作(例:開封やリンクのクリック)

次のセクションでは、メール送信結果の判定に必要なSESイベントの種類とその詳細について解説します。

メール送信結果の判定に必要なSESイベント

メール送信結果を正確に判定するためには、以下のSESイベントを理解する必要があります。

Deliveryイベント

  • 送信に成功した際に発行されるイベントです。

Bounceイベント

  • 送信に失敗した際に発行されるイベントです。
  • このイベントのデータを見ることで、失敗の原因を特定することも可能です。

Bounceイベントの種類

Bounceイベントには大きく2種類あります。

  • ハードバウンス(Hard Bounce)

    • 概要永続的なメール送信障害で、再送信しても成功する可能性は極めて低い
    • 主な原因:メールアドレスが存在しない、ドメインが無効。
  • ソフトバウンス(Soft Bounce)

    • 概要一時的なメール送信障害で、再送信することで成功する可能性は十分にある
    • 主な原因:メールボックスがいっぱい、メッセージサイズが大きすぎる。

失敗原因が書かれているフィールド

Bounceオブジェクトに含まれる以下のフィールドの値を参照することで、失敗の原因を特定できます。

  • bounceType

    • ハードバウンスなのかソフトバウンスなのかを示すフィールドです。
    • ハードバウンスの場合はPermanent、ソフトバウンスの場合はTransientです。
  • bounceSubType

    • バウンスの具体的な理由を示します。
  • diagnosticCode

DeliveryDelayイベント

  • ソフトバウンスが発生し、送信遅延が発生した際に発行されるイベントです。
  • このイベントが発行された場合、Amazon SES側で自動的にリトライが実施されます。
  • このイベントは途中経過を把握するための補助的な情報として取得していますが、取得しなくても最終的な送信結果の判定には大きな影響はありません。

遅延原因が書かれているフィールド

DeliveryDelayオブジェクトに含まれる以下のフィールドの値を参照することで、遅延の原因を知ることができます。

  • delayType

    • 遅延の原因を示すデータ項目です。
  • diagnosticCode

補足

  • 1回の送信に対して複数回発行される場合があります。
  • DeliveryDelayイベントの発行回数は、Amazon SES側での自動リトライ回数に相当します。

Rejectイベント

  • メール内にウイルスが検知された場合に発行されるイベントです。
  • 現時点では遭遇したことはありませんが、念のため取得しておくと安心です。

SESイベントに基づくメール送信結果判定方法

ここからは、SESイベントを用いたメール送信結果判定方法を、「一般的なケース」と「特殊なケース」に分けて整理します。

一般的なケースにおける判定方法

一般的なケースでは、最後に発行されたSESイベントを基に送信結果を判定します。

DeliveryDelayイベントが発行された場合

  • このイベントが発行されたときはAmazon SES側でリトライ送信中であるため、送信結果の判定を保留します。

Deliveryイベントが発行された場合

  • 送信成功と判定します。
  • Amazon SESによるリトライ後に成功した場合は、Deliveryイベントより前にDeliveryDelayイベントが発行されます。

Bounceイベントが発行された場合

  • 送信失敗と判定します。
  • Amazon SESによるリトライ後に成功した場合は、Deliveryイベントより前にDeliveryDelayイベントが発行されます。

Rejectイベントが発行された場合

  • 送信失敗と判定します。

特殊なケースにおける判定方法

特殊なケースでは、最後に発行されたSESイベントだけでなく、直前のイベントも含めて総合的に判定する必要があります。

Deliveryイベントの後にBounceイベント(bounceType: Permanent かつ bounceSubType: General)が発行された場合

  • 送信失敗と判定します。
  • このケースに該当する場合は、受信メールクライアント側で受信拒否されている可能性が非常に高いです。

Deliveryイベントの後にBounceイベント(bounceType: Transient かつ bounceSubType: General)が発行された場合

  • 送信成功と判定します。
  • このケースに該当する場合は、受信メールクライアント側で自動応答が設定されている可能性が非常に高いです。

考慮しなければならない機能要件

以上の整理を踏まえ、SESイベントを用いて送信結果を正確に判定するためには、以下の機能要件を考慮する必要があります:

  • 最初に発行されたSESイベントが最終的な送信結果になるとは限らない

    • 最初に発行されたSESイベントだけでは、送信結果を正確に判定できないケースがあります。
    • 該当するケース
      • DeliveryDelayイベントが発行されるケース
      • Deliveryイベントの後にBounceイベントが発行されるケース
  • 最後に発行されたSESイベントが最終的な送信結果になるとは限らない

    • 最後に発行されたSESイベントだけでは、送信結果を正確に判定できないケースがあります。
    • 該当するケース
      • 自動応答設定がされており、Deliveryイベントの後にBounceイベントが発行されるケース

最終的にたどり着いたメール送信結果確定ロジック

以上を踏まえ、最終的にたどり着いたメール送信結果確定ロジックは以下の通りです。

前提条件

  • 送信リクエスト後、少なくとも1分は待つ
    • Deliveryイベントの後にBounceイベントが発行されるケースに対応するためです。
    • 待機時間中に受け取ったSESイベントを基に送信結果を判定します。
  • SESイベントを発行時刻でソートする
    • ソートされたデータに対して、以下の判定条件を適用します。

判定条件

  • 最後に発行されたイベントがDeliveryDelayの場合
    • Amazon SES側でリトライ中であるため、送信結果の確定を保留します。
  • Deliveryイベントの後にBounceイベント(bounceType: Transient かつ bounceSubType: General)が発行された場合
    • 最後に発行されたイベントに依存せず、送信結果を成功とみなします。
  • それ以外の場合
    • 最後に発行されたイベントを基準に送信結果を確定します。

補足:イベント発行時刻について

イベント発行時刻 ≠ イベントを受け取った時刻

  • イベント発行時刻とイベントを受け取った時刻は異なります
  • イベント発行時刻はSESイベントデータ内のtimestampフィールドの値を参照します。

イベント発行時刻の参照先

  • DeliveryBounceDeliveryDelayイベントの場合
    • 各イベントタイプのオブジェクト内にあるtimestampフィールドを参照します。
  • Rejectイベントの場合
    • Mailオブジェクトのtimestampフィールドを参照します。
      • Rejectオブジェクトにはtimestampフィールドがないためです。
      • 結果判定には特に影響ありません。

メール送信結果を可能な限り正確に判定するための設計と処理の流れ

メール送信から送信結果確定までは、大きく以下の4つのステップに分けることができます。

  1. Amazon SESによるメール送信

    • メール送信リクエストをAmazon SESに送ります。
  2. SESイベントデータをAmazon SNS経由でAmazon SQSキューに送信

    • Amazon SNSを利用してSESイベントデータをAmazon SQSキューに転送します。
  3. SESイベントデータの取得

    • SQSキューに溜まっているSESイベントデータを取得し、DBに保存します。
  4. 送信結果の確定

    • 上述のメール送信結果確定ロジックに基づき、送信結果を確定します。

以降では、技術スタックについて触れた後に、これらを実現するためのインフラ設計、テーブル設計を示し、最後にそれらをふまえた上での各ステップの詳細について解説します。

技術スタック

本設計・実装における技術スタックは以下の通りです。

インフラ

  • AWSを利用し、Terraformで管理しています。

バックエンド

  • Ruby on Railsで実装しています。
  • Railsアプリケーション上からAWSサービスの操作を行うために、aws-sdk-rails gemを利用しています。

インフラ設計

インフラ構成図は以下の通りです。

infra-all

各AWSサービスの役割

各AWSサービスは以下の用途で利用しています。

  • Amazon SES
    • メール送信に利用。
  • Amazon SQS
    • SESイベントデータを一時的に保持するキューとして利用。
  • Amazon SNS
    • Amazon SQSキューへのSESイベントデータ送信に利用。
  • Amazon ECS(+ AWS Fargate)
    • メール送信や結果確定等を行うためのサーバーとして利用。
  • Amazon Aurora Serverless v2
    • 送信履歴の管理等を行うためのRDBとして利用。

テーブル設計

ER図は以下の通りです。 全てのテーブルにはcreated_atupdated_atカラムが含まれていますが、簡略化のため、図中では省略しています。

erd-all

各テーブルの役割について

各テーブルの役割は以下の通りです。

メール送信周り

  • sending_requests
    • Amazon SESにメール送信リクエストを投げたこと表すテーブル。
    • メール送信に必要な情報(メールアドレス、件名、本文)を保持。
  • sending_response_ses
    • Amazon SESへのメール送信リクエストとAmazon SES側での送信リクエストの識別子(message_id)を関連付けるテーブル。
    • message_uidカラムにmessage_idを保存。

SESイベント周り

  • ses_events
    • SESイベントの共通項目を管理するテーブル。
    • timestampカラムはイベント発行時刻を保持し、判定ロジックにおける処理順序の基準となる。
    • SESイベントデータに含まれるmessage_idを基に、送信リクエストと関連付ける。
  • ses_event_*
    • SESイベントの具体的な種別を表すサブテーブル群(例:ses_event_deliveriesses_event_bouncesなど)。
    • 各イベントタイプ固有のデータを管理する。

送信結果周り

  • sending_results
    • 送信結果を抽象化するためのテーブル。
  • sending_result_successes
    • 送信成功を表すテーブル。
  • sending_result_failures
    • 送信失敗を表すテーブル。
    • messageカラムにて、SESイベントデータを基に生成したエラーメッセージを保持。
  • sending_result_ses_events
    • 「送信結果」と「その判定の根拠となったSESイベントデータ」を関連付けるため中間テーブル。

メール送信から送信結果確定までの各ステップの詳細

ここまでで示したインフラ設計とテーブル設計を踏まえた上で、各ステップで行う処理について説明します。

1. Amazon SESによるメール送信

infra-step1

まず、Amazon SESを用いてメール送信を行います。Railsアプリケーション側での処理は以下の通りです。

  1. 送信リクエストの詳細をDBに保存
    • sending_requestsレコードを作成します。
  2. メール送信リクエストの送信
  3. 送信リクエストとAmazon SES側の識別子を関連付ける
    • レスポンスに含まれるmessage_idを用いてsending_response_sesレコードを作成します。

2. SESイベントデータをAmazon SNS経由でAmazon SQSキューに送信

infra-step2

次に、SESイベントデータをSQSキューに送信します。具体的には、Amazon SES、Amazon SNS、 Amazon SQS を連携させることで、SESイベントデータをSQSキューに渡します。

詳細な実装については、本記事末尾に記載の「おまけ:Amazon SES、Amazon SNS、Amazon SQSのインフラ構築におけるTerraformのサンプルコード」をご覧ください。

この処理はAWSの仕組みによって自動で行われるため、Railsアプリケーション側での処理はありません。

3. SESイベントデータの取得

infra-step3

次に、SQSキューに蓄積されたSESイベントデータをRailsアプリケーション側で処理し、DBに保存します。

  1. SQSキュー内のメッセージ件数を確認
  2. SQSキューからSESイベントデータを取得
  3. 取得したメッセージをSESイベントデータとしてDBに保存
    • 取得したメッセージを基に、ses_eventsレコードとイベントタイプごとの関連レコードを作成します。

4. 送信結果の確定

infra-step4

最後に、送信結果を確定します。Railsアプリケーション側での処理は以下の通りです。

  1. 結果未確定の送信リクエストを抽出
    • 送信開始から1分以上経過しているが、送信結果は未確定のsending_requestsレコードを抽出します。
  2. 送信結果を確定
    • 抽出された送信リクエストに対して、上述のメール送信結果確定ロジックを適用し、送信結果を確定します。
    • 結果確定時に作成するレコードは次のとおりです:
      • 全てのケースで作成
        • sending_results
        • sending_result_ses_events
      • 成功の場合に作成
        • sending_result_successes
      • 失敗の場合に作成
        • sending_result_feailures

まとめ

以上、「Amazon SES を用いたメール送信の結果を可能な限り正確に判定する方法」について解説しました。

試行錯誤を経て辿りついた設計となっているため、少しでも参考になれば幸いです。

参考

おまけ:Amazon SES、Amazon SNS、Amazon SQSのインフラ構築におけるTerraformのサンプルコード

以下は、Amazon SES -> Amazon SNS -> Amazon SQS の連携を実現するためのTerraformのサンプルコードです。

Amazon SES -> Amazon SNSの連携

# SESの設定セットの設定
# 詳細: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sesv2_configuration_set
resource "aws_sesv2_configuration_set" "default" {
  configuration_set_name = "ses_events"

  delivery_options {
    tls_policy = "OPTIONAL"
  }

  reputation_options {
    reputation_metrics_enabled = true
  }

  sending_options {
    sending_enabled = true
  }

  suppression_options {
    suppressed_reasons = ["BOUNCE"]
  }
}

# SESの設定セットとSNSトピックの紐付け
# 詳細: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sesv2_configuration_set_event_destination
resource "aws_sesv2_configuration_set_event_destination" "ses_events" {
  event_destination_name = "ses-events"
  configuration_set_name = aws_sesv2_configuration_set.default.configuration_set_name

  event_destination {
    sns_destination {
      topic_arn = aws_sns_topic.ses_events.arn
    }

    enabled              = true
    matching_event_types = ["BOUNCE", "DELIVERY", "DELIVERY_DELAY", "REJECT"]
  }
}

# SNSトピックの作成
# 詳細: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns_topic
resource "aws_sns_topic" "ses_events" {
  name = "ses-events"
}

data "aws_iam_policy_document" "ses_events" {
  statement {
    effect = "Allow"
    principals {
      type        = "Service"
      identifiers = ["ses.amazonaws.com"]
    }

    actions = [
      "SNS:Publish",
    ]

    resources = [
      aws_sns_topic.ses_events.arn
    ]
  }
}

# SNSトピックのポリシー作成
# 詳細: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns_topic_policy
resource "aws_sns_topic_policy" "ses_events" {
  arn    = aws_sns_topic.ses_events.arn
  policy = data.aws_iam_policy_document.ses_events.json
}

Amazon SNS -> Amazon SQSの連携

# SQSキューに対するSNSトピックのサブスクリプションを作成
# 詳細: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns_topic_subscription
resource "aws_sns_topic_subscription" "ses_events" {
  topic_arn = aws_sns_topic.ses_events.arn
  protocol  = "sqs"
  endpoint  = aws_sqs_queue.ses_events.arn
}

# SQSキューを作成
# 詳細: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sqs_queue
resource "aws_sqs_queue" "ses_events_queue" {
  name                       = "ses-events-queue"
  visibility_timeout_seconds = 60
  receive_wait_time_seconds  = 0
}

data "aws_iam_policy_document" "ses_events_queue" {
  statement {
    sid    = "Allow-SNS-SendMessage"
    effect = "Allow"

    principals {
      type        = "Service"
      identifiers = ["sns.amazonaws.com"]
    }

    actions = [
      "sqs:SendMessage",
    ]

    resources = [
      aws_sqs_queue.ses_events_queue.arn,
    ]

    condition {
      test     = "ArnEquals"
      variable = "aws:SourceArn"
      values   = [aws_sns_topic.ses_events.arn]
    }
  }
}

# SQSキューのポリシーを作成
# 詳細: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sqs_queue_policy
resource "aws_sqs_queue_policy" "ses_events_queue" {
  queue_url = aws_sqs_queue.ses_events_queue.url
  policy    = data.aws_iam_policy_document.ses_events_queue.json
}