AWS SESを使って独自ドメインのメールアドレスの受信と転送設定を行う

はじめに

(システムのテストなどで)メールアドレスが大量に必要になった時に、AWS SESは1つの解になるかも知れません。

ドメインの取得

S3のバケットの取得

SESの設定からもバケットは作成することができますが、今回は予め作成しておきます。

  • S3のコンソールから、「バケットを作成」を押して、好きな名前をつけます。
  • SESのリージョンとS3のリージョンを一致させる必要はありません。
    • SESは、受信機能の制限によりオレゴンを選択していますが、S3は、東京リージョンを選択しました。
  • 作成したS3のバケットには、以下のバケットポリシーを設定しておきます。
    • 自分のアカウントのSESからのputObject操作を受けつけると言う意味です。
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowSESPuts-1660060428208",
            "Effect": "Allow",
            "Principal": {
                "Service": "ses.amazonaws.com"
            },
            "Action": "s3:PutObject",
            "Resource": "arn:aws:s3:::[ここにバケット名]/*",
            "Condition": {
                "StringEquals": {
                    "AWS:SourceAccount": "[ここにアカウントの番号]"
                },
                "StringLike": {
                    "AWS:SourceArn": "arn:aws:ses:*"
                }
            }
        }
    ]
}

AWS SESの設定

(注意) メールを受信できるリージョンは、限られています。残念ながら22年8月12日現在では、東京リージョンは対象外でした。
詳細は、以下の「E メールの受信」の章をご確認ください。
docs.aws.amazon.com

リージョン選択

今回は、オレゴンで設定していきます。

  • 右上のリージョン選択で、米国西部(オレゴン)を選びます。
    • 左の「Configuration」メニューに「Email receiving」のメニューがあることを確認しておきます。

ドメインの検証

次にドメインの検証をします。

  • 左の「Configuration」メニューから「Verified identities」を選択します。
  • 「create identity」ボタンを押して、「Domain」を選択します。
  • ドメイン名は、ご自身で取得してRoute53に登録してあるものを選びます。
  • 画面下の「Create Identity」ボタンを押すと、CNAMEレコードが、3行ほどRoute53の該当ドメインに追加されます。
  • Identity statusが「Verified」になっていれば、無事に成功です。(Route53でドメインを取得していると、ここまで半自動なので失敗することは少ないはずです。)

受信ルールの設定

次に、受信ルールを設定します。

  • 左の「Configuration」メニューから「Email receiving」を選択します。
    • リージョンが非対応の場合は、表示されていません。
  • 「Create rule set」ボタンを押します。
  • 「Create rule」 ボタンを押します。
  • Rule nameは、なんでも良いので、自分にとってわかりやすい名前をつけて、「Next」ボタンを押します。
  • 「Add new recipient condition」ボタンを押して、利用したいメールアドレスを入力します。
  • 次ページでは、「Add new action」から「Deliver to S3 bucket」を選択します。
  • S3 bucketに上で、バケットポリシーを設定したバケットを選択します。(バケットポリシーが間違っているとここで警告を受けます。)
    • prefixは、お好みで。
  • 次ページで、最終確認をして、画面下部の「Create rule」ボタンを押してルールを作成します。

ルールを作った後は、activateします。

  • 先ほど作成したルールを選択して、「Set as active」ボタンを押します。

MXレコードの設定

  • Route53のMXレコードに、以下のリンクで指定されているメールサーバを指定します。
    • 今回は、オレゴン(us-west-2)リージョンで設定しているので、「10 inbound-smtp.us-west-2.amazonaws.com」を設定します。

docs.aws.amazon.com

(受信の場合、CUSTOM MAIL FROMは特に設定する必要はありません。送信の場合でも、MAIL FROMが気にならないのであれば、設定する必要はありません。)

確認

  • 最後に、上で作成したメールアドレスに試験メールを送って、S3へ無事書き込まれているかを確認します。
    • 上手く設定できていれば、指定したバケット(の指定していればprefix)配下に、ファイルが作成されているはずです。
    • (注)「AMAZON_SES_SETUP_NOTIFICATION」と言うファイルは、SESでバケットを指定した時に試験的に書き込んでいるファイルなので、違います。

転送設定

毎回、直接s3のファイルを読みに行くのは面倒なので、転送設定をしておきます。

Lambda関数の作成

  • SESの受信設定を行なったのと同じリージョンで、Lambda関数を作成します。
Lambda関数の設定ロール

最低限、以下の権限を付与したロールを作成します。

  • S3のオブジェクトの読み込み (今回、SESで受信したメールをS3へ書き込むので、読み出す権限が必要になる)
  • SESを用いた送信 (メールを転送するためです)

具体的には、以下の様に設定します。
S3のポリシー

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::[バケット名]/[プレフィックス]/*"
        }
    ]
}

SESのポリシー

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "ses:SendRawEmail",
                "ses:SendEmail"
            ],
            "Resource": "arn:aws:ses:us-west-2:[アカウントID]:identity/*"
        }
    ]
}

※ses:SendRawEmailの方は、使わなければはずしてください(以下のlambda関数では使っていません。)

Lambda関数の内容

ざっくり以下の内容です。
バケット名」「プレフィックス」「転送先のメールアドレス」となっている部分は、ご自身の内容に置き換えてください。
gmailへ転送してうまくいくことは確認しました。

lambda関数にライブラリを追加しなくて済む様に、デフォルトで入っているemailライブラリを使っています。

import json
import boto3
from botocore.exceptions import ClientError

from email import policy
from email.parser import BytesParser

def parse_eml(eml_data):
    try : 
        msg = BytesParser(policy=policy.default).parsebytes(eml_data)
        
        charset = msg.get_body(preferencelist=("plain","html"))["content-type"].params["charset"]

        # parse text body
        text_content,text_charset = None,None
        text_body = msg.get_body(preferencelist=("plain"))
        if text_body is not None:
            text_content = text_body.get_content()
            text_charset = text_body["content-type"].params["charset"]

        # parse html body
        html_content,html_charset = None,None
        html_body = msg.get_body(preferencelist=("html"))
        if html_body is not None:
            html_content = html_body.get_content()            
            html_charset = html_body["content-type"].params["charset"]
        
        
        return {
            "to" : msg["to"],
            "subject" : msg["subject"],
            "charset" : charset,
            "text_body" : {"Data":text_content,"Charset":text_charset} if text_body is not None else None,
            "html_body" : {"Data":html_content,"Charset":html_charset} if html_body is not None else None
        }
    # TODO : handle error
    except Exception as e:
        return None

def build_body(parsed_data):
    body = {
        "Text" : parsed_data["text_body"],
        "Html" : parsed_data["html_body"]
    }
    
    if parsed_data["text_body"] is None:
        del body["Text"]
    if parsed_data["html_body"] is None:
        del body["Html"]
        
    return body

def lambda_handler(event, context):
    # S3 params
    BUCKET_NAME = "バケット名"
    PREFIX = "プレフィックス"

    # SES params    
    RECIPIENT = "転送先のメールアドレス"
    
    ERROR_CODE =  {
        'statusCode' : 500,
        'body' : json.dumps('Internal Server Error')
    }
    
    try : 
        s3_client = boto3.client("s3")
        ses_client = boto3.client("ses")
        
        for item in event["Records"]:
            source = item["ses"]["mail"]["source"]
            message_id = item["ses"]["mail"]["messageId"]
            
            s3_key = f"{PREFIX}/{message_id}"
            s3_response = s3_client.get_object(
                Bucket = BUCKET_NAME,
                Key = s3_key
            )
            
            eml_data = s3_response["Body"].read()
            
            parsed_data = parse_eml(eml_data)

            ses_client.send_email(
                Source = parsed_data["to"],
                Destination = {
                    "ToAddresses":[RECIPIENT]
                },
                Message={
                    'Subject': {
                        'Data': f'Fw: {parsed_data["subject"]}',
                        'Charset': parsed_data["charset"]
                    },
                    'Body': build_body(parsed_data)
                }
            )
            
            break 
            
    # TODO : Handle Error
    except KeyError:
        return ERROR_CODE
    except ClientError:
        return ERROR_CODE
    else:
        return {
            'statusCode': 200,
            'body': json.dumps('OK')
        }

SESからのLambda関数の呼び出し

  • 上の方で設定したSESのルールのアクション(現状は、S3への書き込みだけ設定されているはず)に、Lambda関数の起動を追加します。
    • SESとLambda関数のリージョンがあっていないと「Invoke AWS Lambda function」時に、Lambda関数が表示されません。

送信先のメールアドレスの認証

自分のアドレスに転送するため、サンドボックスの設定は、解除しません。
そのため、転送先のアドレスをSESから認証しておく必要があります。

  • SESの画面で、create identityから送信先のEmailアドレスを登録しておいてください。

以上です。