Amazon SESを使うことで通常のGmailで独自ドメインを利用する -転送編-

# この記事について

準備編、受信編、送信編の続き
準備編

https://qiita.com/Sand-River/items/4c0dbb16b70e11ec594a

受信編

https://qiita.com/Sand-River/items/4b22f3d0369ab68cfb3f

送信編

https://qiita.com/Sand-River/items/c3f7f231ad501b92fcfc


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を用いて参照する必要がある。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/f48fd3a6-a81a-2caa-5d8a-718966377d88.png)


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


## Lambdaの作成

では作っていきましょう！
「何も考えずにコードを実行する」Lambda。最高です。（考えろ）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/af71c5eb-46f3-a50f-7dbc-a3403e29f720.png)

「関数の作成」からスタート。__SESと同じリージョンで作成する__のを忘れないように。
（今回はバージニア北部 us-east-1）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/d37710e9-0c3c-590a-fa5d-a8b37e3e4b45.png)


「一から作成」を選択し、適当に関数名を入力。
ランタイムは利用できるPythonの最新として「Python 3.9」を選択。
アーキテクチャはお好きなものを。特に今回の処理はアーキテクチャに依存しないのでarm64にしときました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/29890afa-9459-78d2-d05b-3eea3eeb4743.png)

アクセス権限の「デフォルトの実行ロールの変更」を開いて「既存のロールを使用する」を選択。
利用するIAMロールを作りに「IAMコンソール移動します」のリンクからIAMへ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/13331df0-9a8a-9919-6458-167f66abcafe.png)


## IAMロールの作成

- 信頼されたエンティティ: 「AWSのサービス」
- ユースケース: Lambda

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/8835cda9-19ec-4cd9-9d56-01f63d57e218.png)

「許可を追加」では特に何も選ばずに「次へ」
（あとでインラインポリシーをアタッチします）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/d57bcf95-f13f-210e-4152-f7d6ffbc6a85.png)

ロール名を適当につけて作成。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/7de1793a-1cd4-9d56-ed6e-5b3b1bd8aa97.png)

作成したポリシーを選択して「アクセス許可を追加」から「インラインポリシーを作成」

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/07177f07-86ef-cbb9-9867-f30fa991e9ae.png)

JSONモードでポリシーを記述。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/4dcd5864-8a91-1923-dd73-64ec75b3b075.png)

ポリシーは以下のようにした。
\<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": "*"
        }
    ]
}
```

適当にポリシーの名前を記入して完了。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/01c5e9a3-bf25-bb43-8baf-7b1b4b84d2a0.png)

## Lambdaの作成の続き

Lambda作成の画面に戻りましょう。
更新ボタンを押して先程作成したロールを選択します。
詳細設定は特に触れるところはないので、「関数の作成」へ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/474bcbaa-da3c-c559-5010-2f45b517008f.png)


メールの転送処理を書く。
やることとしては以下の３つ。

- 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)

```

### 環境変数の設定
設定 → 環境変数から

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/07309d28-beb3-c3b2-3691-cf7b0a50e6c5.png)

- 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"}`



### アクセス権限の追加
設定 → アクセス権限から
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/1efb3da1-50dc-c010-fd58-4df432fe5301.png)

アクセス権限を追加
- AWSのサービス
- サービス: Other
- ステートメント ID: 適当
- プリンシパル: ses.amazonaws.com
- ソース ARN: 呼び出し元となるSESルールのARN？
- アクション: lambda:InvokeFunction

ごめん、ここでソースARNが？となっているのはCLIで設定したから。
なんでかCLIだとソースARNの指定がなくとも通るんよ……。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/9b7254be-a582-dda9-6381-b8247a6ac2d9.png)


https://docs.aws.amazon.com/lambda/latest/dg/access-control-resource-based.html

```
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」

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/9ee9fb35-aa4b-7723-ddb4-f6cc89f8f83e.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/8ff52e7e-6417-34cd-b0fd-7b772ef29dab.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/9ea5fd5c-a35c-42dc-2284-24e179483dda.png)

「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」

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/15d62a4b-14a0-d706-1ef3-f90756ea7261.png)

# 確認

Lambdaの環境変数のFORWARDSの"from"に設定したアドレスにメールを配信してみて、"to"に転送されれば成功！ あかんときはCloudwatchLogsとにらめっこだ。