前置き

SESで受信する特定のアドレス宛のメールを外部の別アドレスへ転送したい。SESの設定でこのへんをポチポチっとやれば…と思ったが、できない。できそうでできない。
そう、SESの標準機能ではメールの転送はできない。

対応方針

思い当たる案は2つ。
1. 受信メッセージをS3へ出力し、それをLambdaで拾って別アドレスへ送信する案
2. Amazon WorkMailを使う案
Lambdaを使う案が主流かもしれないが、自分の場合は品質とスピードを重視して結果的には案2を採用した。

案1 S3への出力とLambdaを使う

SESの受信ルールで、受信メッセージをS3へ出力し、それをLambdaでどうにかして別アドレスへメール送信するのが定石のようだ。

こういった情報を参考にプログラムを作成することになるのだが、文字コードや添付ファイルの有無、メッセージを編集してから転送したい、などいろいろ考慮していくとMIMEの深遠なる世界に迷い込んでなかなか大変なことになる。

加えて、SESのS3アクションで「Encrypt Message」オプションを有効化するとS3に格納するメッセージデータが暗号化されるが、それをLambda(Python)側で複合する方法がわからずハマった。というか今も解決できていない。(KMSも絡んで話が長くなるのでこれについてはあらためて別の記事にしたい。)
公式ドキュメント

スクリプト

lambda_function.py
# -*- coding: utf-8 -*-
import boto3
import json
import re
import os

#転送先アドレス(カンマ区切りで複数指定)
FORWARD_TO = os.environ['forward_to'].split(",")

#メール保存先バケット名
S3_BUCKET = "s3-bucket-name"

#転送メールの送信元ドメイン名
DOMAIN_NAME = "hogehoge.jp"

s3  = boto3.client('s3')
ses = boto3.client('ses', region_name="us-east-1")

def lambda_handler(event, context):
    #本来の送信者
    MAIL_SOURCE = event['Records'][0]['ses']['mail']['source']

    #転送メールの送信者
    MAIL_FROM = MAIL_SOURCE.replace('@','=') + "@" + DOMAIN_NAME

    #メール保存先のフォルダ名
    S3_OBJECT_PREFIX = event['Records'][0]['ses']['receipt']['recipients'][0].split("@")[0] + "/"

    #S3上のメールファイル
    s3_key = S3_OBJECT_PREFIX + event['Records'][0]['ses']['mail']['messageId']

    #メールファイル取得
    try:
        response = s3.get_object(
            Bucket = S3_BUCKET,
            Key    = s3_key
        )
    except Exception as e:
        raise e

    #メールヘッダの書き換え
    try:
        replaced_message = response['Body'].read().decode('utf-8')
        replaced_message = re.sub("\nTo: .+?\n", "\nTo: %s\n" % ", ".join(FORWARD_TO), replaced_message,1)
        replaced_message = re.sub("\nFrom: .+?\n", "\nFrom: %s\n" % MAIL_FROM, replaced_message,1)
        replaced_message = re.sub("^Return-Path: .+?\n", "Return-Path: %s\n" % MAIL_FROM, replaced_message,1)
    except Exception as e:
        raise e

    #メール送信
    try:
        response = ses.send_raw_email(
            Source = MAIL_FROM,
            Destinations= FORWARD_TO ,
            RawMessage={
                'Data': replaced_message
            }
        )
    except Exception as e:
        raise e

send_raw_emailを使い、受信したメールのBODYをそのまま送信するのであれば比較的シンプルに実装できそうだ。ただし、一般的なメール転送でよくあるような、メール本文の冒頭に元メールのヘッダ情報を追記するようなことは行っていないため、元メールの送信者がわからないという問題がある。メール本文を編集しようとするとsend_emailを使えば良さそうだが、文字コードやHTMLメール、添付ファイルの考慮など、前述のとおりディープな世界へ足を踏み入れることになり、大変。なのでここでは諦めてシンプルに。

FORWARD_TO = os.environ['forward_to'].split(",")
転送先のメールアドレスはLambdaの環境変数を使って設定する。カンマ区切りの文字列で複数を指定することも可能にしている。

MAIL_FROM = MAIL_SOURCE.replace('@','=') + "@" + domain_name
ここでひと工夫。元メールの差出人アドレスを転送メールの差出人アドレス内に埋め込む。

S3_OBJECT_PREFIX = event['Records'][0]['ses']['receipt']['recipients'][0].split("@")[0] + "/"
SESがS3へ出力する際のPrefixに合わせていれば何でも良いが、ここでは受信アカウントをPrefixとしている。

案2 Amazon WorkMailを使う

月額4USDのコストが許容できるのであればSESの受け口としてWorkMailが使える。WorkMailには転送機能がある。

WorkMailのセットアップ方法は以下がわかりやすい。
AWS WorkMailを使ってみたら想像以上に便利だった

WorkMailでの転送設定は以下(英語の公式ドキュメント)を参考に。
How do I set up an email forwarding rule in Amazon WorkMail?

Lambdaで実現できるはずの機能を月額4USDで逃げるのはエンジニアとして負けた気がしてしまうが・・・スピードと確実性を優先するならアリかと。