AWS SESを使用したメール受信環境

公開日: 2024年10月23日(水)

こんにちは。新技術基盤開発室の若林です。
開発用に会社とは別ドメインのメールアドレスが欲しくなり、AWS SESを使用してメールを受信できる環境を作成しましたので、ご紹介します。

環境構築用のCloudFormationテンプレートもありますので、よろしければご利用ください。

構成概要

今回作成した環境は、下図のようになります。

図中のメールアドレスの役割は、下表のとおりです。new@example.com 宛てにメールが届くと、existing-user@example.net にメールが転送されます。

メールアドレス役割
example.com新しく用意したメール用のドメインです。
new@example.com新規作成するメールアドレスです。
forward@example.comnew@example.com宛てに送信されたメールを転送する際のFROMメールアドレスです。
existing-user@example.netnew@example.com宛てに送信されたメールを転送する際のTOメールアドレスです。

事前準備

環境の多くはCloudFormationで構築しますが、いくつか手動で作成する部分がありますので、以下のものを準備します。

DNSの設定

メールを受信するためには、DNSにMXレコードを登録する必要があります。

東京リージョンの場合は、MXレコードに 10 inbound-smtp.ap-northeast-1.amazonaws.com を登録します。

Route 53をご利用の場合は、下図のようになります。

SESのID設定

SESを使用してメールを転送するので、SESのIDに新規ドメインと転送先メールアドレスを登録します。

CloudFormationを使用して環境構築

CloudFormationのテンプレートを使用して下表のリソースを作成します。

論理ID生成されるリソース
ForwardLambdaRoleLambda用ロール
SesRoleSES用ロール
ForwardLambdaメール転送用Lambda関数
ForwardLambdaPermissionLambda実行権限設定
ForwardLambdaLogGroupLambdaのログ
ForwardLambdaLogGroupFilter001Lambdaのログのエラーフィルタ
MailBucketメールデータを格納するS3バケット
AlarmForwardErrorLambdaログにエラーが書き込まれた際にエラー通知用SNSトピックに通知するCloudWatchアラーム
FailureSnsTopic転送失敗などのエラー通知用のSNSトピック
ReceiptRuleSet受信ルールセット
ReceiptRule01S3にメールデータを保管し、メール転送用Lambda関数を起動する受信ルール
ReceiptRule02S3にメールデータを保管し、エラー通知用のSNSトピックに失敗を通知する受信ルール

テンプレートは、以下になります。

AWSTemplateFormatVersion: 2010-09-09

Description:
Mail environment.

Parameters:
ResourcePrefix:
Description: Each resource prefix (ex. prod-)
Type: String
MailDomainName:
Description: Mail domain name (ex. mymail.com)
Type: String
ReceiveMailAddress:
Description: Receive mail address (ex. user001@mymail.com)
Type: String
ForwardToMailAddress:
Description: Forward mail address (ex. original-mail@mycompany.com)
Type: String

Resources:
MailBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub "${ResourcePrefix}${AWS::AccountId}"

FailureSnsTopic:
Type: AWS::SNS::Topic
Properties:
TopicName: !Sub "${ResourcePrefix}failure-notification"
DisplayName: !Sub "${ReceiveMailAddress}転送エラー"

SesRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub "${ResourcePrefix}ses-role"
Description: !Sub "for ${MailDomainName} mail address."
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: ses.amazonaws.com
Action: sts:AssumeRole
Condition:
StringEquals:
"AWS:SourceAccount": !Sub "${AWS::AccountId}"
StringLike:
"AWS:SourceArn": !Sub "arn:aws:ses:${AWS::Region}:${AWS::AccountId}:receipt-rule-set/*:receipt-rule/*"
Policies:
- PolicyName: policy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: S3Access
Effect: Allow
Action: s3:PutObject
Resource: !Sub "arn:aws:s3:::${MailBucket}/*"
- Sid: SNSAccess
Effect: Allow
Action: sns:Publish
Resource: !Sub "arn:aws:sns:*:${AWS::AccountId}:*"

ForwardLambdaRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub "${ResourcePrefix}forward-lambda-role"
Description: !Sub "for ${MailDomainName} mail address."
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: policy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: SendMail
Effect: Allow
Action:
- ses:SendEmail
- ses:SendRawEmail
Resource: "*"
- Sid: S3Access
Effect: Allow
Action: s3:GetObject
Resource: !Sub "arn:aws:s3:::${MailBucket}/*"
- Sid: SNSAccess
Effect: Allow
Action: sns:Publish
Resource: !Sub "arn:aws:sns:*:${AWS::AccountId}:*"


ReceiptRuleSet:
Type: AWS::SES::ReceiptRuleSet
Properties:
RuleSetName: !Sub "${ResourcePrefix}forward-${MailDomainName}"

ReceiptRule01:
Type: AWS::SES::ReceiptRule
Properties:
RuleSetName: !Ref ReceiptRuleSet
Rule:
Name: !Sub "${ResourcePrefix}forward-${MailDomainName}"
Recipients:
- !Ref ReceiveMailAddress
Enabled: True
ScanEnabled: False
TlsPolicy: Require
Actions:
- S3Action:
BucketName: !Ref MailBucket
ObjectKeyPrefix: !Sub "${ReceiveMailAddress}/"
IamRoleArn: !GetAtt SesRole.Arn
- LambdaAction:
FunctionArn: !GetAtt ForwardLambda.Arn
InvocationType: Event

ReceiptRule02:
Type: AWS::SES::ReceiptRule
Properties:
RuleSetName: !Ref ReceiptRuleSet
Rule:
Name: !Sub "${ResourcePrefix}forward-${MailDomainName}-failed"
Recipients:
- !Sub forward@${MailDomainName}
Enabled: True
ScanEnabled: False
TlsPolicy: Require
Actions:
- S3Action:
BucketName: !Ref MailBucket
ObjectKeyPrefix: !Sub "forward@${MailDomainName}/"
IamRoleArn: !GetAtt SesRole.Arn
- SNSAction:
TopicArn: !Ref FailureSnsTopic
Encoding: Base64

ForwardLambdaLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub /aws/lambda/${ForwardLambda}
RetentionInDays: 120

ForwardLambdaLogGroupFilter001:
Type: AWS::Logs::MetricFilter
Properties:
FilterName: error
FilterPattern: "%ERROR|Error|error%"
LogGroupName: !Ref ForwardLambdaLogGroup
MetricTransformations:
- MetricValue: "1"
Unit: Count
MetricNamespace: Custom
MetricName: !Sub "${ResourcePrefix}forward-${MailDomainName}-error"

AlarmForwardError:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "${ResourcePrefix}${ReceiveMailAddress}のメール転送が失敗"
AlarmActions:
- !Ref FailureSnsTopic
TreatMissingData: notBreaching

Namespace: Custom
MetricName: !Sub "${ResourcePrefix}forward-${MailDomainName}-error"
Statistic: Sum

Period: 60
EvaluationPeriods: 1
DatapointsToAlarm: 1
Threshold: '0'
ComparisonOperator: GreaterThanThreshold

ForwardLambdaPermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !GetAtt ForwardLambda.Arn
Action: lambda:InvokeFunction
Principal: ses.amazonaws.com
SourceAccount: !Sub "${AWS::AccountId}"
SourceArn: !Sub "arn:aws:ses:${AWS::Region}:${AWS::AccountId}:receipt-rule-set/*:receipt-rule/*"

ForwardLambda:
Type: AWS::Lambda::Function
Properties:
FunctionName: !Join [ "-", !Split [ ".", !Sub "${ResourcePrefix}forward-${MailDomainName}" ] ]
Description: !Sub "${ReceiveMailAddress}宛てに届いたメールを${ForwardToMailAddress}に転送する"
Handler: index.lambda_handler
Role: !GetAtt ForwardLambdaRole.Arn
Runtime: python3.12
Timeout: 600
Environment:
Variables:
S3_BUCKET: !Ref MailBucket
MAIL_DOMAIN_NAME: !Ref MailDomainName
RECEIVE_MAIL_ADDRESS: !Ref ReceiveMailAddress
FORWARD_TO_MAIL_ADDRESS: !Ref ForwardToMailAddress
LoggingConfig:
LogFormat: JSON
ApplicationLogLevel: INFO
SystemLogLevel: INFO
Code:
ZipFile: |
import os
import boto3
import json
import textwrap
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email import encoders
import urllib.parse

S3_BUCKET = os.environ['S3_BUCKET']
MAIL_DOMAIN_NAME = os.environ['MAIL_DOMAIN_NAME']
RECEIVE_MAIL_ADDRESS = os.environ['RECEIVE_MAIL_ADDRESS']
FORWARD_TO_MAIL_ADDRESS = os.environ['FORWARD_TO_MAIL_ADDRESS']

def forward_mail(message_id, original_mail, original_subject, original_from):
msg = MIMEMultipart()
msg['Subject'] = f'【転送】{original_subject}'
msg['From'] = f'forward@{MAIL_DOMAIN_NAME}'
msg['To'] = FORWARD_TO_MAIL_ADDRESS

body_message = textwrap.dedent(f"""
※このメールは、自動送信です。
{RECEIVE_MAIL_ADDRESS} 宛てにメールが届きました。
詳細は添付ファイルをご確認ください。(~.msg を Outlook で開いてください)

・送信元: {original_from}
・件名: {original_subject}

--
AWS Lambda から送信されました。
""").strip()

print(body_message)

msg.attach(MIMEText(body_message, 'plain'))

# 添付ファイルの作成
part = MIMEBase('application', 'octet-stream')
part.set_payload(original_mail)
encoders.encode_base64(part)
safe_filename = urllib.parse.quote(f'{message_id}.eml')
part.add_header('Content-Disposition', f'attachment; filename={safe_filename}')
msg.attach(part)

ses_client = boto3.client('ses')
ses_client.send_raw_email(
Source = msg['From'],
Destinations = [msg['To']],
RawMessage = { 'Data': msg.as_string() }
)

def find_common_header(record, header_name, default_value):
if 'commonHeaders' in record['ses']['mail']:
if header_name in record['ses']['mail']['commonHeaders']:
return record['ses']['mail']['commonHeaders'][header_name]
return default_value

def process_record(record):
s3_client = boto3.client('s3')
message_id = record['ses']['mail']['messageId']
response = s3_client.get_object(
Bucket = S3_BUCKET,
Key = f'{RECEIVE_MAIL_ADDRESS}/{message_id}'
)
original_mail = response['Body'].read()
forward_mail(message_id,
original_mail,
find_common_header(record, 'subject', '(件名不明)'),
find_common_header(record, 'from', ['(送信元不明)']))

def show_config():
print(
json.dumps({
'S3_BUCKET': S3_BUCKET,
'MAIL_DOMAIN_NAME': MAIL_DOMAIN_NAME,
'RECEIVE_MAIL_ADDRESS': RECEIVE_MAIL_ADDRESS,
'FORWARD_TO_MAIL_ADDRESS': FORWARD_TO_MAIL_ADDRESS,
}, indent=2)
)

def lambda_handler(event, context):
try:
print(json.dumps(event))
for record in event['Records']:
process_record(record)
except Exception as e:
print('ERROR: 例外発生。転送に失敗。')
print(e)

パラメータは以下になります。

パラメータ名説明設定例
ResourcePrefix各リソースのプレフィックスを指定します。prefix-
MailDomainName新しいメールアドレス用のドメイン名を指定します。example.com
ReceiveMailAddress新しい受信用のメールアドレスを指定します。new@example.com
ForwardToMailAddress転送先のメールアドレスを指定します。existing-user@example.net

残設定

CloudFormationでリソースを作成した後に、以下の設定をします。

SESの受信ルールセットの有効化

SESのEメール受信ルールセットを有効にします。

ルールセットは、リージョンにつき一つしか有効にできないので、他のルールセットが有効になっている場合は、ご注意ください。

エラー通知用のSNSトピックにメールアドレスを登録

最後に、エラー発生時の通知先をSNSトピックに登録して完了です。

動作確認

new@example.com にメールを送信すると、existing-user@example.net にメールが転送されるでしょうか。

以上でメールを受信できる環境ができました。

>お役立ち資料のダウンロード

お役立ち資料のダウンロード

ブログでは紹介しきれないシステム開発や導入におけるケーススタディを資料にまとめました。お気軽にダウンロードください。

CTR IMG