SESに返信されてきたメールをメーラーで受信してみた

あえてWorkMailを使わない方法を模索してみました
2024.01.19

こんにちは、なおにしです。
ふとしたことからWorkMailを使わずにSESでメール受信まで出来ないかなと思いました。

はじめに

Amazon SESはユーザー自身のEメールアドレスとドメインを使用してEメールを送受信するためのマネージドサービスです。
ですが、以下に記載のとおりメールの受信については制限があります。例えばMicrosoft Outlookのようなメーラーを用いてメールを受信することは、SES単体の機能では実現することができません。

受信メールの受信に Microsoft Outlook を使用することはできますか?

Amazon SESには、Eメールを受信するための POPサーバーやIMAPサーバーは含まれていません。つまり、Microsoft Outlook などのEメールクライアントを使用してEメールを受信することはできません。Eメールクライアントを使用して送信と受信の両方が可能なソリューションが必要な場合は、Amazon WorkMailの使用を検討してください。

とはいえ

以下のような観点から、ドキュメントに記載のあるWorkMailを使わなくてもメール受信ができたらいいなと思うことがあるかもしれません。私はありました。

  • とりあえずSESで送信されたメールに対する返信をメーラーで確認できればいい
  • 送信したメールに対して想定される返信メールの数はごくわずか
  • SESのメール受信機能でAmazon SNSを選択して転送した場合、受信サイズ等に制限があったり、そのままだとヘッダー情報まで送信されて読みづらかったり、デコードされていなかったりするため読めない(読みづらい)ことがある
  • WorkMailには1ユーザあたり4ドル/月のコストがかかる
  • WorkMailが東京・大阪リージョンでは提供されていない(2024年1月時点)

どうすれば良いか

WorkMailを使わないということは、メール受信のためにはドキュメントに記載のとおりPOPサーバやIMAPサーバが必要になります。ですが例えばEC2でそれらのサーバを構築するとなると、WorkMailを使う以上に費用も労力もかかります。
一方、SESが提供する受信機能ではメール受信をトリガーとしてその内容をS3に保存したりLambdaを実行したりできます。

そこで、本機能を用いてLambdaからSESを呼び出すことで、受信したメールをフリーメールサービスに転送してみます。 以下のようにSESで受信したメールをMicrosoft OutlookやGmailのようなフリーメールサービス(今回はMS Outlookを使用)に転送する方法を検討しました。

しかし、ここで気をつけないといけないのはLambdaからSESを呼び出してメールを送信する時に設定する送信元アドレスです。SESには以下の制限があります。
Amazon SES サンドボックス外への移動

アカウントがサンドボックスの外にあるときは、受信者のアドレスまたはドメインが検証されたかどうかにかかわらず、任意の受信者にEメールを送信できます。​ただし、「From」、「Source」、「Sender」または「Return-Path」アドレスとして使用するすべてのIDは引き続き検証する必要があります。

SESでメールアドレスを検証するためには、対象のメールアドレス宛に送信された確認URLをクリックする必要があります。また、ドメインを検証するためには対象のDNSサーバでレコードの更新を行う必要があります。
つまり、任意の受信者のメールアドレスやドメインを全て管理できているのであればSESに検証済みIDとして登録可能ですが、管理されていない不特定多数がメール配信先になるケースでは登録することができません。
したがって、このような配信先からの返信メールをSESで受信しても、その送信元アドレスを「Lambda+SESで転送する時の送信元アドレス」として名乗ることができません(考えてみれば当たり前でした。できてしまうとなりすましし放題になってしまいます)。

このため不特定多数から送られたメールをSESで受信してLambda+SESで転送するには、妥協案になってしまいましたが送信元として自身が管理するメールアドレスを設定して転送することで目的は達成できました。

やってみた

SESで受信する際の送信元アドレスおよびLambda+SESによる転送先アドレスの両方がSESに検証済みIDとして登録済みのパターンについては、先人が以下のとおり既に実施していましたので、参考にさせていただきました。

このため環境の準備については上記の記事をご参照ください。
以下は送信元として自身が管理するメールアドレスを設定して転送するように変更したLambda関数となります。

import os
import boto3
import logging
import re
import email
from email.header import Header
from email.header import decode_header

s3_client = boto3.client('s3')
ses_client = boto3.client('ses')
logger = logging.getLogger()
logger.setLevel(logging.INFO)
s3_bucket = os.environ['S3_BUCKET']
forward_to = os.environ['FORWARD_TO']
msg_from = os.environ['MSG_FROM']

def get_header(name):
    header = ''
    for tup in decode_header(name):
        if type(tup[0]) is bytes:
            charset = tup[1]
            if charset:
                header += tup[0].decode(tup[1])
            else:
                header += tup[0].decode()
        elif type(tup[0]) is str:
            header += tup[0]
    return header

def send_mail(message):
    ses_client.send_raw_email(
        Source = forward_to,
        Destinations=[
            forward_to
        ],
        RawMessage={
            'Data': message
        }
    )

def lambda_handler(event, context):
    logger.info(event)
    #メッセージID取得
    message_id=event['Records'][0]['ses']['mail']['messageId']
    #メッセージIDをキーにS3オブジェクト(メール)取得
    response = s3_client.get_object(
        Bucket = s3_bucket,
        Key    = message_id
    )
    # Emlデータから送信元の情報を取得
    raw_message = response['Body'].read()
    msg = email.message_from_bytes(raw_message)
    ReturnPath_org = msg['Return-Path']
    from_org = msg['From']
    from_name = get_header(re.compile('^(.*) <(.*)>$').search(from_org).group(1))
    from_address = re.compile('^(.*) <(.*)>$').search(from_org).group(2)

    # 送信元情報を修正したEmlデータを作成
    from_modified = f'送信元:【{from_name} <{from_address}>】からのメール'
    del msg['Return-Path']
    msg['Return-Path'] = '<%s>'%(msg_from)
    del msg['From']
    msg['From'] = '%s <%s>'%(Header(from_modified.encode('iso-2022-jp'),'iso-2022-jp').encode(), msg_from)  
    # 転送元メールヘッダーのDKIM-Signatureを削除(DKIM-Signatureの重複によるエラー防止)
    del msg['DKIM-Signature']

    # メール送信
    send_mail(msg.as_string())

送信元を自身のメールアドレスに設定する都合上、そのままでは本来の送信元の名前・メールアドレスが分からなくなってしまうので、今回は表示名で判別できるようにしました。実際にWeb版のMicrosoft Outlookで受信すると以下のような感じになります。

なお、Microsoft Outlookであれば自身の所有であると確認できたメールアドレスについては「outlook.jp」や「outlook.jp」ドメインではなくても自身のメールアドレスとして利用することができます。

このため、アプリ版のクライアントソフトで受信サーバとしてoutlook.comのIMAPサーバ、送信サーバとしてSES(SMTPサーバ)を指定することでメーラーとして送受信できる状態にもできます。ただし、送信元は自身のメールアドレスになっているため、「返信」の操作をすると宛先は自身のメールアドレスになりますのでご注意ください。

ハマったところ

結果として元のメール内容をコピーして送信元を変更するだけで良かったので、当初は以下のとおりdel文無しで上書きしていたのですが、まるで意図どおりに動きませんでした。

    # 送信元情報を修正したEmlデータを作成
    from_modified = f'送信元:【{from_name} <{from_address}>】からのメール'
    msg['Return-Path'] = '<%s>'%(msg_from)
    msg['From'] = '%s <%s>'%(Header(from_modified.encode('iso-2022-jp'),'iso-2022-jp').encode(), msg_from)

最終的にメール送信のライブラリ仕様を確認したところ、きちんと明記されていました。。

注意: このメソッドでは、すでに同一の名前で存在するフィールドは上書きされません。もしメッセージが名前 name をもつフィールドをひとつしか持たないようにしたければ、最初にそれを除去してください。たとえば:
del msg['subject']
msg['subject'] = 'Python roolz!'

おわりに

月4ドルをケチらずに大人しくWorkMailを使えば良かったと思いました。
POP/IMAPサーバの構築やWorkMailの利用以外の選択肢の一つとして、
この記事がどなたかのお役に立てれば幸いです。