この記事について
準備編、受信編、送信編の続き
準備編
受信編
送信編
SESでのメール受信、GmailからSESを通じてのメール送信ができるようになった。あとはSESで受信したメールをGmailに転送してやれば完成のはずだだだ。
(2022/2/13追記)
ここまで進めてきてはみたものの、SESでは転送するメールのFromについても検証済みのドメインの必要があったり(エンベロープではどうにもできなかった。調査不足の可能性はある)、転送するメールのDKIMについても対応が難しくて結局別のメール転送ソリューションに頼ることにした。
送信はGmail → Amazon SES →
受信は→ Cloudflare Email Routing → Gmail
で一旦FIX。送受信ともに、SPF/DKIMは正しく扱われる。
Lambda
SESのEmail receivingのruleからLambdaが起動できるので、こいつを使ってSES経由でGmailに転送してやればOK。
起動したLambdaに渡されるeventにはメッセージ本体(body)は渡されないので、一旦S3に格納した受信メールをmessageIdを用いて参照する必要がある。
Lambdaの作成
では作っていきましょう!
「何も考えずにコードを実行する」Lambda。最高です。(考えろ)
「関数の作成」からスタート。__SESと同じリージョンで作成する__のを忘れないように。
(今回はバージニア北部 us-east-1)
「一から作成」を選択し、適当に関数名を入力。
ランタイムは利用できるPythonの最新として「Python 3.9」を選択。
アーキテクチャはお好きなものを。特に今回の処理はアーキテクチャに依存しないのでarm64にしときました。
アクセス権限の「デフォルトの実行ロールの変更」を開いて「既存のロールを使用する」を選択。
利用するIAMロールを作りに「IAMコンソール移動します」のリンクからIAMへ。
IAMロールの作成
- 信頼されたエンティティ: 「AWSのサービス」
- ユースケース: Lambda
「許可を追加」では特に何も選ばずに「次へ」
(あとでインラインポリシーをアタッチします)
ロール名を適当につけて作成。
作成したポリシーを選択して「アクセス許可を追加」から「インラインポリシーを作成」
JSONモードでポリシーを記述。
ポリシーは以下のようにした。
<accountId><bucketName>(あと下記ではus-east-1としてあるregion)については環境に合わせて編集。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "logs:CreateLogGroup",
"Resource": "arn:aws:logs:us-east-1:<accountId>:*"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": [
"arn:aws:logs:us-east-1:<accountId>:log-group:/aws/lambda/mailForwarding:*"
]
},
{
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": "arn:aws:s3:::<bucketName>/*"
},
{
"Effect": "Allow",
"Action": [
"ses:SendEmail",
"ses:SendRawEmail"
],
"Resource": "*"
}
]
}
適当にポリシーの名前を記入して完了。
Lambdaの作成の続き
Lambda作成の画面に戻りましょう。
更新ボタンを押して先程作成したロールを選択します。
詳細設定は特に触れるところはないので、「関数の作成」へ。
メールの転送処理を書く。
やることとしては以下の3つ。
- S3からメールデータを取得する
- メールデータのヘッダを書き換える
- SESで送信する
FromやReturn-PathがSESで検証済みのドメインでないと蹴られちゃうので、その辺りを書き換えるのがメイン。あとは転送先で返信しやすいよう、元々のFromをReply-Toにセットしてる。
試行錯誤の結果、一旦こうなっているがもっと色々弄るべきところはありそう。エイリアス対応したり、存在しないメールアドレス宛をどう扱うかを考えたり。 対応してみた。
あとは転送するとDKIM通らないとかその辺りが気になるところ。転送の際にメールヘッダのDKIMに関する部分を書き換えれば良いのだろうけれど、そもそも元のメールがどうだったかが大事だもんなぁ。対応保留。
lambda_function.py
import os
import boto3
import json
import re
s3 = boto3.client('s3')
ses = boto3.client('ses')
BUCKETNAME = os.environ['BUCKETNAME']
FORWARDS = json.loads(os.environ['FORWARDS'])
DEFAULT = json.loads(os.environ['DEFAULT'])
def main(record):
# get mail data from s3 bucket
message_id = record['ses']['mail']['messageId']
mail_object = s3.get_object(Bucket=BUCKETNAME, Key=message_id)
message = mail_object['Body'].read().decode('utf-8')
# get "To:","From:" From original mail headers
destinations = record['ses']['mail']['destination']
original_from = get_mail_header(record, 'From')
# modify email headers
# set original from address to Reply-To
if get_mail_header(record, 'Reply-To') is None:
message = add_mail_header(message, 'Reply-To', original_from)
# set original from address to X-Original-Mail-From
message = add_mail_header(message, 'X-Original-Mail-From', original_from)
transfered = False
for forward in FORWARDS:
for destination in destinations:
if forward['from'] == re.sub(r'\+[^@]+', '', destination):
mail_transfer(destination, forward['to'], original_from, message)
transfered = True
# if destinations did not match FORWARDS, then send to DEFAULT
if not transfered:
mail_transfer(DEFAULT['from'], DEFAULT['to'], original_from, message)
def get_mail_header(record, name):
values = list(filter(
lambda x: x['name'] == name, record['ses']['mail']['headers']
))
return values[0]['value'] if values else None
def upd_mail_header(message, header, value):
return re.sub(
r'(^|\n)' + header + r': .*?(\n[^\s\t])',
'\\1' + header + ': ' + value + '\\2',
message, 1, flags=(re.DOTALL)
)
def add_mail_header(message, header, value):
return f'{header}: {value}\n' + message
def mail_transfer(from_addr, to_addr, original_from, message):
# message modify
message = message
message = upd_mail_header(message, 'To', to_addr)
message = upd_mail_header(message, 'From', from_addr)
message = upd_mail_header(message, 'Return-Path', from_addr)
result = send_mail(message, from_addr, to_addr)
print(f'Transfer process complete: messageId {result}')
def send_mail(message, from_addr, to_addr):
print(f'from {from_addr} to {to_addr}')
print(message)
return ses.send_raw_email(
Source=from_addr,
Destinations=[to_addr],
RawMessage={'Data': message}
)
def lambda_handler(event, context):
for record in event['Records']:
main(record)
環境変数の設定
設定 → 環境変数から
- BUCKETNAME: メールデータを格納したバケットの名前
- FORWARDS: JSONで記述したfromとtoの辞書リスト。fromに来たメールをtoに転送する。
[{"from": "foo@example.com", "to": "example@gmail.com"}]
- DEFAULT: JSONで記述したfromとtoの辞書。FORWARDSに該当しなかった場合はここに転送する。
{"from": "transfer-agent@example.com", "to": "example@gmail.com"}
アクセス権限の追加
アクセス権限を追加
- AWSのサービス
- サービス: Other
- ステートメント ID: 適当
- プリンシパル: ses.amazonaws.com
- ソース ARN: 呼び出し元となるSESルールのARN?
- アクション: lambda:InvokeFunction
ごめん、ここでソースARNが?となっているのはCLIで設定したから。
なんでかCLIだとソースARNの指定がなくとも通るんよ……。
aws lambda add-permission --function-name mailForwarding \
--action lambda:InvokeFunction \
--statement-id ses \
--principal ses.amazonaws.com
これでLambdaをDeployすればOK。
SESの設定
SESのEmail receiving → 受信編で作成したルールセット → 受信編で作成したルールを選択して「Edit」
「Add actions」までNextで進める。
前回作成した「1. Deliver to Amazon S3 bucket」の次に「Invoke AWS Lambda function」を追加する。
- Lambda function: 先ほど作成したLambda
- Invocation type: Event Invocation
- SNS topic: 指定なし
追加したらNextして「Save changes」
確認
Lambdaの環境変数のFORWARDSの"from"に設定したアドレスにメールを配信してみて、"to"に転送されれば成功! あかんときはCloudwatchLogsとにらめっこだ。