📑

Lambda と Amazon SES で学ぶメール送信の基本と仕組み

に公開

はじめに

本記事は、これまでメールサーバの構築経験がなかった私が、AWS Lambda と Amazon SES を使って Gmail アドレス宛にメールを送信する必要が生じたことをきっかけに、メール送信の仕組みについて調べた記録です。

調べた当時の私のレベルに併せて、かなり初学者の方むけの内容になっています。
以下の方にとっては読みやすいかもしれません。

  • SMTPは聞いたことがあるレベル。仕組みはほとんど理解していない。
  • LambdaとSESはなんとなく触ったことがある。
  • SPF、DKIM、DMARCという言葉は聞いたことがあるが、仕組みの説明はできない。

Lambda から SES を使って Gmail へメールを送る流れ

今回やりたいことはざっと以下のイメージです。

0.メール送信の基本(SMTPって何?)

SMTPの基本については、他の方も多く説明されているので、ここでは言及しません。
下記の記事が大変参考になりました。
https://qiita.com/utsunomiya_ff/items/d95f32375de9edfa207f#メール送信の流れ

SMTP通信には、EnvelopeとHeaderそれぞれにFrom/Toの概念があることを知りませんでした。。

1. LambdaからAmazonSESへの通信(Lambda作成編)

Amazon SESへメールを送るためのLambda関数を作成する必要があります。
ses:SendEmail または ses:SendRawEmail を実行してSESを呼び出します。

  • EnvelopeのFromアドレスはReturnPathで指定します。Return-Path を独自ドメインにしたい場合は Custom MAIL FROM を有効化し、そのサブドメインを SPF と MX で検証する必要があります。設定しない場合は amazonses.com が自動で使用されます。
  • Lambda 実行ロールに ses:SendEmail または ses:SendRawEmail の権限が必要です。
  • AWS SDK が自動で認証を行うため、SMTP クレデンシャルは不要です。
  • サンプルコードでは記載していませんが、設定セットを指定してメールを送信することもできます。
    設定セットを利用することで、SESの実行ログをkinesis経由でS3に保存することも可能です。
import boto3
from botocore.exceptions import ClientError

ses = boto3.client('ses', region_name='ap-northeast-1')

try:
    response = ses.send_email(
        Source='sender@example.com',  # **Header From**
        Destination={
            'ToAddresses': ['test@gmail.com'],  **# Header To / Envelope RCPT TO**
        },
        Message={
            'Subject': {'Data': 'こんにちは', 'Charset': 'UTF-8'},
            'Body': {
                'Text': {'Data': '本文テキスト', 'Charset': 'UTF-8'},
                # 'Html': {'Data': '<h1>HTML本文</h1>', 'Charset': 'UTF-8'},
            }
        },**
        # ReturnPath='bounces@example.com',  # カスタム Return-Path(Envelope MAIL **FROM)
    )
    message_id = response['MessageId']
    print(f"Email sent! Message ID: {message_id}")
except ClientError as e:
    print(f"Error: {e.response['Error']['Message']}")

2. AmazonSESの受信処理(Lambda⇒SES)

  • AmazonSES は Lambdaから呼び出されると、Header Fromで指定された値がAmazon SESの検証済みIDに含まれることを確認します。
  • DKIMを有効にしている場合は、SESはメールにDKIM-Signatureヘッダーを付与します。
  • DKIMは大きく3つのパターンで設定が可能です。
    (DKIMの詳細な仕組みについては、本記事の終盤の参考情報に記載しています。)
    • Easy DKIM
    • BYO DKIM
    • 手動署名

3. AmazonSESによるDNS問い合わせ(SES⇒Gmail MTA)

  • 宛先メールアドレスのドメイン(例:gmail.com)に対し、まずMX レコードを問い合わせます。Googleが公開しているMTA のホスト名(ASPMX.L.GOOGLE.COM.、ALT1.ASPMX.L.GOOGLE.COM.など)を取得します。
  • 得られたMTAのホスト名に対しAまたはAAAAレコードを問い合わせ、SMTP接続先(メール送信先)のIPを取得します。

4. AmazonSES⇒Gmail MTA への配送

  • SES は前項で取得した接続先のIPアドレスと許可されたSMTPポート(25)を使ってSMTPセッションを確立します。
  • SMTPセッション内で以下を順次送信します。
  1. EHLO/HELO
  2. MAIL FROM:<Return-Path>(Envelope)
  3. RCPT TO:test@gmail.com(Envelope)
  4. DATA(ヘッダー+本文+DKIM-Signature)
  5. QUIT

5. Gmail 側の配信処理(Gamil MTA⇒受信トレイ)

  • Gmail MTAがメールをMDAに渡し、受信トレイ(Mailbox)に格納します。
  • Gmail MTAはDKIM-Signatureを公開鍵で検証し、改ざんの有無を確認します。

まとめ

ざっくり上記の流れで、LambdaからAmazon SESでメールを送信することができます。
かなり説明を省略した部分もありましたが、ご覧いただきありがとうございました。


以下は参考情報

SPFの仕組みを今回の事例で考えてみる

受信側の MTA(今回の場合はGmailのMTA)は、SMTP セッション中の
MAIL FROM:<Return-Path> から Envelope MAIL FROMを取り出します。

  • MTA は DNS リゾルバへ「Envelope MAIL FROMの TXTレコードを教えて」と問い合わせます。
  • 返ってくる TXTレコードには、以下のようにSPFポリシーが書かれています
v=spf1 include:amazonses.com -all

上記は「amazonses.com ドメインに定義された IP 範囲を許可する」という意味合いです。

次にMTAは “amazonses.com” の TXT レコードを問い合わせます。
その結果、以下のようなAmazon SESが利用するIP範囲を示すリストを取得することが可能です。

v=spf1 ip4:54.240.0.0/18 ip4:34.192.0.0/12 … -all

これが “amazonses.com ドメインに定義された許可 IP の一覧になります。

SMTP セッションはTCP/IP で確立されるため、MTA はコネクション元の IP(例:54.240.12.34)を把握しています。
その IPアドレスが上記で取得した ip4:…/… のいずれかのネットワーク範囲に含まれていれば
「PASS(一致)」と判断。
含まれていなければ「FAIL」となり、最終的に -all(REJECT)に従って処理されます。

レコードの値 説明 ハードフェイル/ソフトフェイル
v=spf1 include:amazonses.com -all 許可外はすべて「拒否」 ハードフェイル(受信は許可しない)
v=spf1 include:amazonses.com ~all 許可外はすべて「警告」 ソフトフェイル(受信は許可する)

カスタムMAIL FORMドメインを使用する場合には事前にTXT レコードおよびMXレコードを登録する必要がありますが、デフォルトのドメインを利用する場合は不要です。
それぞれの観点で以下に記載します。

デフォルトの MAIL FROM ドメイン(amazonses.com のサブドメイン)

  • AWS が管理するサブドメインを Envelope MAIL FROM として自動設定する。
  • AWS管理のため、あらかじめ SPF 用 TXT レコードを公開済みと考えられる。
  • ユーザー側でTXT レコードを登録する必要はなく、SPF 認証は暗黙的に「自動有効」になる。

カスタム MAIL FROM ドメイン

  • Envelope MAIL FROM ドメインを自社サブドメイン(例:mail.example.com)に切り替えると、
    そのドメインのSPF TXT レコードにAWSが管理するSESの送信IP範囲が含まれていないため、
    受信MTAがTXT を問い合わせても「許可リストにこのIPがない」と判断してSPFチェックがFAILになる

そのため、事前にTXTレコードの登録が必要になります。
また、下記のようなMXレコードの追加も必要です。

feedback-smtp.<region>.amazonses.com(バウンス受信用MXレコード)

SESはバウンス発生時に配送不能メールを受け取る準備が必要です。
カスタム MAIL FROM ドメインでMX レコードを登録することで、
どのサーバー(SES の受信用エンドポイント)にこれらの通知を送ればいいかをDNS上で指定できます。

DKIMの仕組みを今回の事例で考えてみる

  • SES コンソールで Easy DKIM を有効化すると、検証するドメインに対して「3つの CNAME レコード」を発行するよう案内されます。
  • この CNAME を辿ると、最終的に amazonses.com 側にある TXT レコード(例: k=rsa; p=<Base64の公開鍵>)に到達します。CNAMEをたどった結果、「実際の公開鍵レコードは AWS 側で管理してますよ」と指し示していることがわかります。
  • メール送信前にSES が自動的に【DKIM‑Signature】ヘッダーを生成・付与** します。
    – Easy DKIM を使う場合は、SESが秘密鍵で署名し、あらかじめ DNS に登録した 3つのCNAMEレコードで公開している公開鍵を使って受信サーバが検証します。

受信サーバがどのように検証するのか

  • メールヘッダーに付与された DKIM-Signature ヘッダーの例
DKIM-Signature: v=1; a=rsa-sha256; d=yourdomain.com; s=selector1; … b=<署名データ>
- d=yourdomain.com が「署名ドメイン(Signing Domain Identifier, SDID)」
- s=selector1 が「セレクタ(Selector)」

受信サーバーはこれらを組み合わせて、DNS の問い合わせ先を次のように決定します

**selector1._domainkey.yourdomain.com**

TXT レコードを参照すると以下のようなCNAME レコードが返ってきます。

selector1._domainkey.yourdomain.com CNAME selector1._domainkey.yourdomain.com.dkim.amazonses.com.

ここで返ってくるのは、DKIMの設定時に追加した3つのCNAMEレコードのうちの1つです。

DNSの再帰的解決機能により、クエリは自動的にselector1._domainkey.yourdomain.com.dkim.amazonses.com へフォールバックされます。

selector1._domainkey.yourdomain.com.dkim.amazonses.com のゾーン(amazonses.com 側)には、TXT レコードとして "p=<Base64公開鍵>" が登録されており、
これがクライアントに返却されます。
その中に書かれた 公開鍵 を使ってヘッダーの b= 部分(署名)を検証します

CNAMEが3つ必要な理由

Easy DKIM では、1つのドメインごとにCNAMEレコードを3つ発行します。

それぞれの CNAME は異なる「セレクタ名」(selector1, selector2, selector3)を指しており、SES が内部で管理する 3 つの公開鍵それぞれへフォールバックします。

定期的(約 3 ヶ月ごと)に DKIM キーをローテートする際、すべてのレコードを一度に書き換えると、伝播遅延で一時的に検証失敗が起こるリスクがあります。
そこで、3 つのキーを同時に公開しておくことで、SES 側は新旧どちらの鍵でも署名・検証ができ、ローテーションをスムーズに行えます。

予備も含めて3つの公開鍵を事前に作成し、そのうちのどれか1つの秘密鍵で署名をします。
どの秘密鍵で署名をしたかをDKIM-Signatureヘッダーにセレクタとして情報を与えています。

Discussion