公開日: 2024年10月23日(水)
こんにちは。新技術基盤開発室の若林です。
開発用に会社とは別ドメインのメールアドレスが欲しくなり、AWS SESを使用してメールを受信できる環境を作成しましたので、ご紹介します。
環境構築用のCloudFormationテンプレートもありますので、よろしければご利用ください。
構成概要
今回作成した環境は、下図のようになります。
図中のメールアドレスの役割は、下表のとおりです。new@example.com
宛てにメールが届くと、existing-user@example.net
にメールが転送されます。
メールアドレス | 役割 |
example.com | 新しく用意したメール用のドメインです。 |
new@example.com | 新規作成するメールアドレスです。 |
forward@example.com | new@example.com宛てに送信されたメールを転送する際のFROMメールアドレスです。 |
existing-user@example.net | new@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 | 生成されるリソース |
ForwardLambdaRole | Lambda用ロール |
SesRole | SES用ロール |
ForwardLambda | メール転送用Lambda関数 |
ForwardLambdaPermission | Lambda実行権限設定 |
ForwardLambdaLogGroup | Lambdaのログ |
ForwardLambdaLogGroupFilter001 | Lambdaのログのエラーフィルタ |
MailBucket | メールデータを格納するS3バケット |
AlarmForwardError | Lambdaログにエラーが書き込まれた際にエラー通知用SNSトピックに通知するCloudWatchアラーム |
FailureSnsTopic | 転送失敗などのエラー通知用のSNSトピック |
ReceiptRuleSet | 受信ルールセット |
ReceiptRule01 | S3にメールデータを保管し、メール転送用Lambda関数を起動する受信ルール |
ReceiptRule02 | S3にメールデータを保管し、エラー通知用の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
にメールが転送されるでしょうか。
以上でメールを受信できる環境ができました。