Lambda と Amazon SES で学ぶメール送信の基本と仕組み
はじめに
本記事は、これまでメールサーバの構築経験がなかった私が、AWS Lambda と Amazon SES を使って Gmail アドレス宛にメールを送信する必要が生じたことをきっかけに、メール送信の仕組みについて調べた記録です。
調べた当時の私のレベルに併せて、かなり初学者の方むけの内容になっています。
以下の方にとっては読みやすいかもしれません。
- SMTPは聞いたことがあるレベル。仕組みはほとんど理解していない。
- LambdaとSESはなんとなく触ったことがある。
- SPF、DKIM、DMARCという言葉は聞いたことがあるが、仕組みの説明はできない。
Lambda から SES を使って Gmail へメールを送る流れ
今回やりたいことはざっと以下のイメージです。
0.メール送信の基本(SMTPって何?)
SMTPの基本については、他の方も多く説明されているので、ここでは言及しません。
下記の記事が大変参考になりました。
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