🕊️

AWSでのメール配信入門:Amazon SESの使い方と認証設定のポイント

2024/11/07に公開

Amazon SES でメールを操作する

近年、メールの送信は以前と比べて難易度が上がっています。
神奈川県公立高校ネット出願システムでGmailが使えないというトラブルGmailガイドラインの適用開始は記憶に新しいかと思います。

本記事ではAmazon SES(Amazon Simple Email Service)でのメールの送受信の方法について簡単に説明します。

メール送信者の一般的ガイドライン

スパムメールやフィッシング詐欺の増加やセキュリティ強化の必要性から、最近ではメール送信に厳格なガイドラインが設けられています。

例えばGoogleは以下のようなメール送信者のガイドラインを発表しています。
https://support.google.com/a/answer/81126?hl=ja

このガイドラインでは「すべての送信者」と「1 日あたり 5,000 件以上のメールを送信する場合」に分けてガイドラインが定義されています。

この「1 日あたり 5,000 件以上のメールを送信する場合」という定義は次のようになっています。
https://support.google.com/a/answer/14229414

  • 上限である 5,000 件の計算には、同じプライマリ ドメインから送信されたすべてのメールがカウントされる
    • solarmora.com から 2,500 件、promotions.solarmora.com から 2,500 件のメールを送信した場合、5000件となる
  • 一括送信者として分類されたメール送信者は、恒常的に一括送信者として分類されます

メール認証についてのガイドラインは以下の通りとなります。

  • すべての送信者
    • SPFまたはDKIM
  • 一括送信者
    • SPF、DKIM、DMARCをすべて満たす

これらの認証結果については、メールの詳細やヘッダ情報を表示することで確認できます。受信ヘッダのAuthentication-Resultsを確認し、dkim、spf、dmarcがpassかfailになっているかを確認できます。

Authentication-Results: mx.google.com; dkim=pass header.i=@youtube.com
 header.s=20230601 header.b=JAX4sYsO; spf=pass (google.com: domain of
 3-c8vzxeladazadqbxk-bgdotmeqekagfgnq.oay@scoutcamp.bounces.google.com
 designates 209.85.220.69 as permitted sender)
 smtp.mailfrom=3-c8VZxELADAZadQbXk-bgdOTMeQekagfgNQ.OaY@scoutcamp.bounces.google.com;
 dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=youtube.com; dara=pass
 header.i=@gmail.com

SPF(Sender Policy Framework)

送信者のメールアドレスが正規のメールサーバーを経由して送信されているかを確認するための仕組みです。ここで確認されるメールアドレスは「エンベロープFrom」であり、これはReturn-Pathに表示されるものと同一です。

ドメイン所有者は自分のドメインのDNS設定にSPFレコードを追加し、メール送信を許可するサーバーの情報を記載します。
メールが届くと、受信サーバーは送信者のドメインのSPFレコードを確認し、実際の送信元IPと一致するかをチェックします。

受信の流れは以下の通りです。まず、受信したメールのReturn-Pathに表示されるメールアドレスのドメインについてDNSサーバのSPFレコードを確認します。SPFレコード内の許可されたIPやドメインに送信元が含まれていれば、認証は合格となります。

例として、以下のメールヘッダを見てみましょう:

例:

Return-Path: <3JYYXZxELAM478By95I-9EBw1uCyCI8EDEvy.w86@scoutcamp.bounces.google.com>
Received: from mail-sor-f69.google.com (mail-sor-f69.google.com. [209.85.220.69])
        by mx.google.com with SMTPS id ca18e2360f4ac-83ad1c94e17sor259720139f.2.2024.10.22.04.01.57
        for <hogehoge@gmail.com>
        (Google Transport Security);
        Tue, 22 Oct 2024 04:01:57 -0700 (PDT)

nslookupコマンドでドメインのSPFレコードを確認し、許可されている送信元IPアドレスやドメインを調べることができます。

% nslookup -type=TXT scoutcamp.bounces.google.com

Server:		240f:e0:274f:1:569b:49ff:fe60:75bc
Address:	240f:e0:274f:1:569b:49ff:fe60:75bc#53

Non-authoritative answer:
scoutcamp.bounces.google.com	text = "v=spf1 redirect=_spf.google.com"

Authoritative answers can be found from:

出力に"v=spf1 redirect=_spf.google.com"とありますので、_spf.google.comの内容を確認します。

% nslookup -type=TXT _spf.google.com

Server:		240f:e0:274f:1:569b:49ff:fe60:75bc
Address:	240f:e0:274f:1:569b:49ff:fe60:75bc#53

Non-authoritative answer:
_spf.google.com	text = "v=spf1 include:_netblocks.google.com include:_netblocks2.google.com include:_netblocks3.google.com ~all"

_netblocks.google.comと_netblocks2.google.comと_netblocks3.google.comのDNSレコードを確認します。

% nslookup -type=TXT _netblocks.google.com

Server:		240f:e0:274f:1:569b:49ff:fe60:75bc
Address:	240f:e0:274f:1:569b:49ff:fe60:75bc#53

Non-authoritative answer:
_netblocks.google.com	text = "v=spf1 ip4:35.190.247.0/24 ip4:64.233.160.0/19 ip4:66.102.0.0/20 ip4:66.249.80.0/20 ip4:72.14.192.0/18 ip4:74.125.0.0/16 ip4:108.177.8.0/21 ip4:173.194.0.0/16 ip4:209.85.128.0/17 ip4:216.58.192.0/19 ip4:216.239.32.0/19 ~all"

Authoritative answers can be found from:

ip4:209.85.128.0/17は209.85.128.0 ~ 209.85.255.255となるため、209.85.220.69が含まれており、SPF認証は合格します。
なお、DNSのSPFレコードの確認はオンラインツールなどを使用することもできます。
https://dmarcian.com/spf-survey/

DKIM(DomainKeys Identified Mail)

メールの内容が改ざんされずに送信者から受信者に届いたことを確認するための仕組みです。

alt text

送信元のドメインのDNSにDKIM公開鍵を記録しておき、送信サーバーはその対となる秘密鍵を使って、メールの内容(ヘッダと本文の一部)から署名を作成してメールに付与します。受信サーバーはDNSからDKIM公開鍵を取得し、メールの署名を検証して、内容が改ざんされていないことを確認します。

受信したメールには以下のようなDKIM-Signatureというヘッダが存在しています。

DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
        d=youtube.com; s=20230601; t=1729594917; x=1730199717; dara=google.com;
        h=to:from:subject:message-id:feedback-id:reply-to:date:mime-version
         :from:to:cc:subject:date:message-id:reply-to;
        bh=n7LvQ1fzosAN/nyTHtSXkdrlBsFr/9rdKC7q55nZZtw=;
        b=kzc7320PcS6RUM/1wU335hfthab+tau7EbsadaTEpOIaXYfhhmhnApFdzj7xGgcK5J
         AUKjB9FXnTNvVGHjg0Sv58YViaUMR5IFCXERvwz21A1+k1/2M0RBwlZJSQ5p++bwOzsp
         IIjzgv9GAp+o1dmdKnVhYYqdUbsTTUBeJzJ8Kls9ZQKa0JJ6o/vu4daCgjJx54ljOKUC
         v40+RMvkg6khWhsOutnUZ9enEDwF2pkoZwIOW09816kZgm9fXH9Dg16ebUgBKyjeVE6R
         baxnpXwiC2lQbnOwemUXdtEMiuDQ4BI1u/kmiQnMgNNsMLuXrpBcoQORevUqPYvdpAAA
         ni3g==
  • dは署名を付与したドメイン
  • sはDKIMの選択子 dとsの値を使用して公開鍵を取得する
  • aは署名のアルゴリズム
  • cはCanonicalization(正規化)方法。メールの内容を一貫した形に整え、改行や空白の差異があっても同一視できるようにする
  • bhは本文のハッシュ値。受信サーバーで本文をハッシュ化し、この値と一致すれば、本文が改ざんされていないことを確認できる
  • bは署名本体。この署名は送信サーバーで生成され、受信サーバーで検証される

このヘッダの情報を元に本文ハッシュ値(bh)と署名本体(b)を検証します。
ヘッダのdkim-signatureを確認することで、送信者のDKIM署名が適切に検証されているかを確認できます。

ハッシュ値(bh)の検証方法:

まず c= で指定された方法で本文を正規化します。ここでは c=relaxed/relaxed とあるので、ヘッダと本文共にrelaxed正規化が適用され、余分な空白や改行の一部が無視されます。次に正規化した内容を、a= で指定されたアルゴリズム(例:SHA-256)でハッシュ化します。この結果が bh= の値と一致するかを確認します。

署名本体(b)の検証方法:

受信サーバーでは DKIM-Signature の d=(署名をしたドメイン)と s=(DKIMの選択子)を使用して公開鍵を取得します。上記の例では、d=youtube.com と s=20230601 となり、以下のようなコマンドで公開鍵が得られます。

nslookup -type=TXT 20230601._domainkey.youtube.com
20230601._domainkey.youtube.com	text = "v=DKIM1; k=rsa; 

公開鍵を用いて署名本体(b)の検証を行い、その結果が受信サーバーで計算したヘッダと本文のハッシュ値と一致すれば、署名が正当であると判断されます。

DMARC(Domain-based Message Authentication, Reporting & Conformance)

DMARCはSPFとDKIMの認証結果に基づき、送信元が正当なサーバーであるかを確認する仕組みです。DMARC認証には、SPFまたはDKIMのいずれかが合格し、さらに「アライメント(alignment)」と呼ばれるドメイン一致チェックに合格することが必要です。

アライメントでは、メールの送信元アドレス(Fromヘッダ)のドメインが、SPFまたはDKIMで認証されたドメインと一致していることが求められます。

送信メールのドメイン所有者は自分のドメインのDNSにDMARCポリシーを設定し、これにより認証失敗時に受信サーバーが取るべきアクションを指定できます。

主なポリシー設定:

  • p=none:特にアクションを指定しない。失敗時のレポートが送信されるのみ。
  • p=quarantine:認証に失敗したメールを隔離(スパムフォルダに入れるなど)する。
  • p=reject:認証に失敗したメールを拒否する。

例えば、YouTubeのDMARCポリシーが設定されている場合、以下のようにnslookupを使用して確認できます。

 % nslookup -type=TXT _dmarc.youtube.com
Non-authoritative answer:
_dmarc.youtube.com	text = "v=DMARC1; p=reject; rua=mailto:mailauth-reports@google.com"

この例では「p=reject」なので認証に失敗した場合はメールが拒否されます。

AWSでのメールの送信

IDの作成

SESでメールを送信する場合、最初にIDを作成する必要があります。
SESではEメールアドレスIDとドメインIDの二種類が存在します。

https://docs.aws.amazon.com/ja_jp/ses/latest/dg/creating-identities.html

EメールアドレスID

AWS SESでメールを送信する最も簡単な方法は、EメールアドレスIDを利用することです。
ID作成時に指定したメールアドレスにAWSからメールが届くことで、EメールアドレスIDの使用が可能になります。

alt text

この場合、Return-PathにAWS SESのドメインが設定されるため、Fromヘッダのドメインと一致せず、DMARC認証には失敗します。

ドメインID

ドメインIDを使用すると、そのドメインのアドレスでメールを送信できるようになります。

alt text

DMARC認証を合格させたい場合は「カスタム MAIL FROMドメインの使用」を選択してください。

ドメインIDでは送信ドメインのDNSレコードを追加・修正する必要があります。
Route53でドメインを作成している場合は自動でレコードの更新が可能ですが、別の方法でドメインを管理している場合、自分でDNSのレコードを修正する必要があります。

注意点として、カスタム MAIL FROMを使用すると、同じメールアドレスでAWS SESを使用した受信ができなくなることに気をつけてください。(後述のAWS SESでのメール受信を参照)

boto3によるメール送信例

単純な送信例

単純にメールを送るだけならsend_raw_emailを使用することでHTML,プレーンテキスト両方とも送信可能です。

import boto3
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

# SESクライアントの作成
ses_client = boto3.client('ses', region_name='ap-northeast-1')  # リージョンは適宜変更してください

# 送信するメールの詳細
sender = "SESで検証済みのIDであること"
recipient = "送信先のメールアドレス"
subject = "Test Email with Custom Header"
body_text = "This is a test email sent through Amazon SES using the AWS SDK for Python (Boto3)."
body_html = """<html>
<head></head>
<body>
  <h1>Test Email</h1>
  <p>This is a test email sent through <b>Amazon SES</b> using the <b>AWS SDK for Python (Boto3)</b>.</p>
</body>
</html>
"""
custom_header = "X-Custom-Header"

# メールの作成
msg = MIMEMultipart('mixed')
msg['Subject'] = subject
msg['From'] = sender
msg['To'] = recipient
msg[custom_header] = "CustomHeaderValue"

# メールの本文(テキスト部分とHTML部分)
text_part = MIMEText(body_text, 'plain')
html_part = MIMEText(body_html, 'html')

# 本文をメールに添付
msg.attach(text_part)
msg.attach(html_part)

# メールの生データを生成
raw_message = {
    'Data': msg.as_string()
}

# メールの送信
try:
    response = ses_client.send_raw_email(
        Source=sender,
        Destinations=[
            recipient,
        ],
        RawMessage=raw_message
    )
    print("Email sent! Message ID:"),
    print(response['MessageId'])
except Exception as e:
    print("Error sending email: ", e)

テンプレートを使用した送信例

テンプレートを使用することで、一括でメールを送信することが可能になります。*1

*1 一括で送れる送信先は最大50となります。

https://docs.aws.amazon.com/ja_jp/ses/latest/dg/send-personalized-email-api.html

まずテンプレートの登録を行います。

import boto3
from botocore.exceptions import ClientError

# SESクライアントの作成
ses_client = boto3.client('ses', region_name='ap-northeast-1')

template_name = 'MyTemplate20240712'

# テンプレートの削除
try:
    response = ses_client.delete_template(TemplateName=template_name)
    print("Template deleted successfully!")
except ClientError as e:
    print(f"Error deleting template: {e.response['Error']['Message']}")


# テンプレートの定義
template = {
    'TemplateName': template_name,
    'SubjectPart': 'Hello {{name}}',
    'TextPart': 'Dear {{name}},\r\nYour favorite animal is {{favoriteanimal}}.',
    'HtmlPart': '<h1>Hello {{name}}</h1><p>Your favorite animal is {{favoriteanimal}}.'
}

# テンプレートの作成
try:
    response = ses_client.create_template(Template=template)
    print("Template created successfully!")
except ClientError as e:
    print(f"Error creating template: {e.response['Error']['Message']}")

次にテンプレートを使用してメールを送信します。
以下の例では送信先Aと送信者Bに一部内容が動的に変わったメールが送信されています。

import boto3

# # SESv2クライアントの作成(SESv2 APIの使用)
sesv2_client = boto3.client('sesv2', region_name='ap-northeast-1')  # リージョンは適宜変更してください

# 送信するメールの詳細
sender = "SESで検証済みのIDであること"
template_name = "MyTemplate20240712"

# 宛先リストと個別のテンプレートデータ
destinations = [
    {
        'Destination': {
            'ToAddresses': ['送信先A'],
        },
        'ReplacementEmailContent': {
            'ReplacementTemplate': {
                'ReplacementTemplateData': '{ "name":"Recipient1", "favoriteanimal":"blue" }'
            }
        }, 
        'ReplacementHeaders': [
            {
                'Name': 'X-CustomHeader',  'Value': 'xxxxxxxxxxx'
            }
        ]
    },
    {
        'Destination': {
            'ToAddresses': ['送信先B'],
        },
        'ReplacementEmailContent': {
            'ReplacementTemplate': {
                'ReplacementTemplateData': '{ "name":"Recipient2", "favoriteanimal":"green" }'
            }
        }
    }
]

# デフォルトのテンプレートデータ(すべての宛先に共通)
default_template_data = '{ "name":"DefaultName", "favoriteanimal":"red" }'

# メールの送信
try:
    # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/sesv2/client/send_bulk_email.html
    response = sesv2_client.send_bulk_email(
        DefaultContent={
            'Template': {
                'TemplateName': template_name,
                'TemplateData': default_template_data,
                'Headers': [
                    {'Name': 'X-CustomHeader', 'Value':'value....'}
                ]
            }
        },
        BulkEmailEntries=destinations,
        FromEmailAddress=sender
    )
    print("Bulk email sent! Response:"),
    print(response)
except Exception as e:
    print("Error sending bulk email: ", e)

Virtual Deliverability Manager

Virtual Deliverability Manager を有効化することでバウンスメールの発生率や、どのアドレスにメールが届かなかったかを確認することができます。

https://docs.aws.amazon.com/ja_jp/ses/latest/dg/vdm.html

ダッシュボード
ダッシュボードを使用することで、送信数、バウンスメールの発生率、苦情率、開封率などを確認できます。

また、過去30日間に送信したメッセージの配信状況を個別に確認できます。

メールの未達が発生した場合、『一時的なバウンス』、『永続的なバウンス』と表示されます。*2

*2 一時的なバウンスや永続的なバウンスと表示されなくても、実際にはメールが到達していない場合があります。これはウィルス対策ソフトやプロキシサーバーの設定によっては、ここでは届いたように見えても実際に未到達の場合があります

Virtual Deliverability Manager アドバイザー:
Virtual Deliverability Manager アドバイザーでは配信の問題などについてアドバイスが表示されます。

https://docs.aws.amazon.com/ja_jp/ses/latest/dg/vdm-advisor.html

コスト:

Virtual Deliverability Manager は、メール送信料などの他の SES 料金とは別に、メール送信 1,000 通ごとに 0.07 USD かかります。AWS コンソール、CLI、または API を使用して Virtual Deliverability Manager の情報にアクセスすると、1,000 クエリごとに0.0005 USD かかります。毎月最初の 5,000 クエリは無料です。

https://aws.amazon.com/jp/ses/pricing/

AWS SESでのメール受信

AWS SESでメールの受信も可能です。
受信したメールをS3に保存し、必要に応じてLambda関数を呼び出すこともできます。

https://docs.aws.amazon.com/ja_jp/ses/latest/dg/receiving-email.html

受信を行うには受信したいメールアドレスのドメインのMXレコードに受信のエンドポイントを付与する必要があります。

例:

10 inbound-smtp.ap-northeast-1.amazonaws.com

https://docs.aws.amazon.com/ja_jp/ses/latest/dg/receiving-email-mx-record.html

ここで指定するエンドポイントは以下から選択します。

https://docs.aws.amazon.com/general/latest/gr/ses.html#ses_inbound_endpoints

ここの設定は「カスタム MAIL FROM ドメインの使用」を設定する場合に、SPF用のレコードと競合を引き起こす可能性があります。具体的には、カスタム MAIL FROM ドメインに設定したサブドメインと、メールの受信に使用するサブドメインが同じ場合、MXレコードを受信用に設定できなくなります。そのため、カスタム MAIL FROM ドメインと、メールの受信に使用するドメインには、異なるサブドメインを指定する必要があります。

https://docs.aws.amazon.com/ja_jp/ses/latest/dg/mail-from.html#mail-from-requirements

受信の手順

  1. 受信するメールアドレスのDNSドメインのMXレコードに受信のエンドポイントを付与する
  2. Amazon SES > 設定: E メール受信 でルールセットを作成する
  3. 作成したルールセットでルールを作成する
  4. 受信者の条件に検証済みドメイン IDに属するメールを指定する
  5. アクションを設定する

アクションの例

受信したメールについて、どのようなアクションを行うかを以下の中から複数選択できます。

以下の例ではS3バケットに保存したのち、Lambda関数を呼び出しています。

S3バケットへの配信

メールを保存したい S3バケット名を指定します。

メールを受信すると以下のようにS3バケットにファイルが作成されます。

Lambda関数の呼び出し

呼び出すLambda関数名を選択することで任意の関数を呼び出せます。

Lambdaの実装例:

import json
import boto3
import email
from email import policy
from email.parser import BytesParser
s3_bucket = "your-s3-bucket-name"


def lambda_handler(event, context):
    # SESからの通知イベントを取得
    ses_notification = event['Records'][0]['ses']
    
    # メールヘッダー情報の取得
    mail = ses_notification['mail']
    common_headers = mail.get('commonHeaders', {})
    
    from_address = common_headers.get('from', [''])[0]
    to_address = common_headers.get('to', [''])[0]
    subject = common_headers.get('subject', '')
    message_id = event['Records'][0]['ses']['mail']['messageId']
    # ログ出力
    print(f"From: {from_address}")
    print(f"To: {to_address}")
    print(f"Subject: {subject}")
    print(f"MessageId: {message_id}")

    # SES APIを使用して実際のメッセージ内容を取得する
    s3_client = boto3.client('s3')
    response = s3_client.get_object(
        Bucket = s3_bucket,
        Key    = message_id
    )
    print(response)
    # Emlデータ取得
    raw_message = response['Body'].read()
    print(raw_message)
    # メッセージの内容を処理して本文を抽出する
    parsed_message = BytesParser(policy=policy.default).parsebytes(raw_message)
    body = ''
    # メッセージがマルチパートかどうかをチェックする
    if parsed_message.is_multipart():
        for part in parsed_message.iter_parts():
            # 'text/plain'部分を見つけて、本文を取得する
            if part.get_content_type() == 'text/plain':
                body = part.get_payload(decode=True).decode(part.get_content_charset())
    else:
        # メッセージが単一部分の場合
        body = parsed_message.get_payload(decode=True).decode(parsed_message.get_content_charset())

    print(body)
    text = f"""from: {from_address}
to: {to_address}
subject: {subject}
message_id: {message_id}
body: {body}
"""
    # TODO textをSlackに送ったり任意の作業
    return {
        'statusCode': 200,
        'body': json.dumps(text)
    }

まとめ

AWS SESを使用することで、メールの送受信が容易に行えます。
また、Virtual Deliverability Managerを有効にすることで、メール送信結果の監視が容易になります。
なお、神奈川県公立高校ネット出願システムの障害調査報告書には、「Amazon SESの仕様でエンベロープFromとヘッダーFromをそろえるのは不可能」と記載されていたようですが、これはカスタム MAIL FROMドメインの使用で回避可能です。

CareNet Engineers

Discussion