3
Help us understand the problem. What are the problem?

posted at

updated at

SESで受信したメールを、Lambdaで適切に変換し、件名や本文を取得する

はじめに

SESでメールを受信し、Lambdaを使用して、Dynamodb等にメール情報を書き込む、という処理がありました。
SESのメール受信機能は、リージョンが限られており、また、メール情報を適切に変換し、件名や本文を取得するのに苦労したため、その点を中心に記載します。

スクリーンショット 2022-05-25 0.18.59.png

事前構築

  • Route53でドメインの設定済み。仮にドメインをhoge.comとします。
    • 受信メールで使用するドメインは、今回、サブドメインを使用する想定です。mail.hoge.com
  • 送信元は、Gmailを想定
    • メールによって、MINEタイプの書き方が異なります。

SESの受信メール設定

受信メールで使用するドメインは、mail.hoge.comです。

受信機能が使えるリージョンは、現在3つしかありません。今回は、バージニア州で設定を行います。

下記のように、バージニア州のSESに遷移し、Verified identitiesから、サブドメインをmail.hoge.comとし、作成します。
スクリーンショット 2022-05-23 21.14.32.png

sandbox制限解除

sandbox制限解除をする必要があります。

下記の記事通りにするとよいです。
送信制限緩和が必要な方は、合わせて行ってください。

すでにご使用のバージニアリージョンで設定済みの方は、パスして問題ありません。

SES受信設定

バージニアのSESのEmail receivingに遷移し、ルールを作成します。
send-to-lambdaという名前にしました。

スクリーンショット 2022-05-23 21.31.16.png
スクリーンショット 2022-05-23 21.34.29.png

Create ruleをクリックします。
スクリーンショット 2022-05-23 21.39.38.png

名前を記載し、他はデフォルトにします。
スクリーンショット 2022-05-23 21.40.37.png

受信で使用するメールアドレスを設定します。
mail.hoge.comを設定しました。
特定のサブドメイン内のすべてのアドレスとマッチされます。
例えば、gmailでtest@mail.hoge.com宛に送信すると、SESでルールに従って受信できるイメージです。

スクリーンショット 2022-05-23 22.00.58.png

SNSに送るため、SNSを作成します。
create SNS topicをクリックしましょう。
Encodingは、UTF-8にします。Base64すると、Lambdaでエンコードする必要でます。
また、SNSもバージニアリージョンでの作成が必須になります。
スクリーンショット 2022-05-23 21.46.41.png

スクリーンショット 2022-05-23 21.50.09.png

受信ルールを有効化

受信ルールのStatusがinactiveになっています。
Set as activeをクリックして、有効化しましょう。
スクリーンショット 2022-05-23 21.52.02.png
有効になりましたね
スクリーンショット 2022-05-23 21.52.26.png

SNSを経由する理由

SESから直接Lambdaに通知することもできますが、SNSを経由した場合のみ、通知には E メールの本文(content)が含まれます。
そのため、SNSを経由します。

ただし、SNSにも弱点があり、メールのサイズが150KBを超えると、通知がLambdaに行きません。
色々と注意点がありますので、ドキュメントを一読することをおすすめします。

Amazon SNS の通知を通じて E メールを受信することを選択した場合、E メールの最大サイズ (ヘッダーを含む) は 150 KB です。それよりも大きいメールはバウンスします。このサイズよりも大きい E メールが予想される場合は、代わりに Amazon S3 バケットに E メールを保存してください。

Route53の設定

受信メールで使用するドメインは、mail.hoge.comにします。

SES による E メール受信のため、 MX レコードの公開設定が必要です。

  • レコード名:mail
  • レコードタイプ:MX
  • :10 inbound-smtp.us-east-1.amazonaws.com(リージョンは、バージニア州)

スクリーンショット 2022-05-23 22.12.30.png

PyhonでLambdaを作成

東京リージョンで作成します。

Pythonではなく、Node.jsでも良い場合、Node.jsでLambdaを作成しましょう。
作成方法は、こちら↓

https://qiita.com/holdout0521/items/458b4b886edb58735836#nodejs%E3%81%AE%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%81%8C%E5%84%AA%E7%A7%80

スクリーンショット 2022-05-24 21.33.58.png

ses-receive
import json
from decimal import Decimal
import email
from email.header import decode_header

def get_email_body(email_messages):
    for email_message in email_messages.get_payload(decode=False):
        charset = email_message.get_content_charset()
        print("charset = %s" % charset)
        contentType = email_message.get_content_type()
        print("contentType = %s" % contentType)
        payload = email_message.get_payload(decode=True)
        if email_message and charset and contentType == "text/plain":
                return payload.decode(charset, errors="ignore") + "\n\n"

def get_email_header(email_message, name):
    header = ''
    if email_message[name]:
        for tup in decode_header(str(email_message[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

# json形式でログを出力するため、Decimalがある場合取り除く
def decimal_to_int(obj):
    if isinstance(obj, Decimal):
        return int(obj)

def lambda_handler(event, context):
    print("Received event:" + json.dumps(event, default=decimal_to_int, ensure_ascii=False))

    SES_message = event['Records'][0]['Sns']['Message']
    SES_message_json = json.loads(SES_message)
    SES_message_content = SES_message_json['content']

    email_message = email.message_from_string(SES_message_content)
    print("email_message start--------")
    print(email_message)
    print("email_message end--------")

    # 送信元アドレス
    email_from = get_email_header(email_message, 'From')
    print("email_from = %s" % email_from)
    # 送信日時
    email_date = get_email_header(email_message, 'Date')
    print("email_date = %s" % email_date)
    # 件名
    email_subject = get_email_header(email_message, 'Subject')
    print("email_subject = %s" % email_subject)
    # 本文
    email_body = get_email_body(email_message)
    print("email_body = %s" % email_body)

メールを受信すると、Lambdaで処理し、メール内の送信元アドレス送信日時件名本文を適切に取得することができます。

メールのフォーマット

メールは IMF (Internet Message Format) という形式でやりとりされています。
メールソフトはこのIMF形式を人間が読める形に変換して表示しています。

IMFは、ヘッダーとボディの2つから構成されています。
宛先や送信元はヘッダーに、本文や添付ファイルはボディに入っています。
ボディはさらにMIMEという形式でエンコードされているのが一般的です。

ヘッダー

ヘッダーの一つ一つのフィールド情報は、下記の記事がまとめられておりました。

メールソフトによっては、エラーになります

今回は、Gmailからの受信に限定しましたが、メールソフトによって、以下の理由で、今回のLambdaがエラーになる場合があります。

  • IMF形式やMIMEエンコードを正しく実装出来ていない
  • 文字コードの指定と実際のエンコーディングがソフトによって異なることがある
  • 文字コードの名称が標準と異なることがある

実際に、別のメールソフトでメールを受信した場合、うまくいかないことがありました。
具体的にはget_email_bodyメソッドでエラーになりました。
そのため、以下のように修正するとうまくいきました。逆にGmailではエラーになりましたが。

メールソフトによっては、get_email_bodyを修正する必要があります
def get_email_body(email_message):
    charset = email_message.get_content_charset()
    print("charset = %s" % charset)
    payload = email_message.get_payload(decode=True)
    print("payload = %s" % payload)
      if payload and charset:
        return payload.decode(charset)

受信するメールソフトが分かっている場合、そのソフトに合わせましょう。

SNS

サブスクリプションの作成をクリックし、先程作成したLambdaに配信するよう設定しましょう。

スクリーンショット 2022-05-23 22.55.17.png

LambdaのARNを貼り付け、サブスクリプションの作成をクリックしましょう。
スクリーンショット 2022-05-25 0.08.36.png

これで、Gmailで以下の通りにメールを送信すると、SESに受信し、SNS経由でLambdaに適切なメール情報が取得されます。

  • 宛先:<お好きな文字>@mail.hoge.com
  • 本文:ほんぶんです。
  • 件名:けんめいです。

Lambdaのlogにも出力されることを確認しましょう。

Lambdaのlog
email_subject = けんめいです。

payload = Content-Type: text/plain; charset="UTF-8"
Content-Transfer-Encoding: base64

charset = utf-8

contentType = text/plain

email_body = ほんぶんです。

取得した値をDynamoDB等に保存など行えますね。

yahooのメールアドレスから受信

yahooのメールアドレスから受信すると、以下のようにlogが出力されました。
本文にCSS?が入っていますね。。

Lambdaのlog
email_subject = けんめいです。

payload = Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: quoted-printable
* { font-size: 13px; font-family: 'MS P=E3=82=B4=E3=82=B7=E3=83=83=E3=82=AF=
', sans-serif;}p, ul, ol, blockquote { margin: 0;}a { color: #0064c8; text-=
decoration: none;}a:hover { color: #0057af; text-decoration: underline;}a:a=
ctive { color: #004c98;}
=E6=9C=AC=E6=96=87=E3=81=A7=E3=81=99=E3=80=81
=EF=BC=92=EF=BD=87=EF=BD=99=EF=BD=8F=EF=BD=95=EF=BD=8D=EF=BD=85

charset = utf-8

contentType = text/plain

email_body = * { font-size: 13px; font-family: 'MS Pゴシック', sans-serif;}p, ul, ol, blockquote { margin: 0;}a { color: #0064c8; text-decoration: none;}a:hover { color: #0057af; text-decoration: underline;}a:active { color: #004c98;}
ほんぶんです。

Node.jsのモジュールが優秀!

PythonではなくNode.jsの場合、Mailparserモジュールでemailのパースをします。
結論から言うと、Mailparserモジュールを使用すると、ほとんどのメールソフトに対応できており、かつコード量が少ないため、Node.jsで開発することをおすすめします。

MailparserをLambdaLayerにアップ

MailparserモジュールをローカルからLambdaにアップする必要があります。
今回は、LambdaLayerを利用します。

ローカルの適当なパスで行います。

$ mkdir nodejs
$ cd nodejs

# initで初期化処理 package name以外、デフォルトでよいので、
# エンターを押してください

$ npm init
package name: (nodejs) my-package
version: (1.0.0) 
description: 
entry point: (index.js) 
test command: 
git repository: 
keywords: 
author: 
license: (ISC) 

# パッケージインストール
$ npm install mailparser --save

#
# レイヤー用 Zip ファイル作成
$ cd ..
$ zip -r layer2.zip ./nodejs

layer2.zipLambdaLayerにアップします。

スクリーンショット 2022-05-25 22.37.46.png

スクリーンショット 2022-05-25 22.34.22.png

Node.jsのLambda作成

Node.jsでLambdaを作成し、LambdaLayerで先程のレイヤーを追加します。スクリーンショット 2022-05-25 22.40.03.png

Node.js
const simpleParser = require("mailparser").simpleParser;

exports.handler = async (event) => {
  console.log("Received event:", JSON.stringify(event, null, 2));

  const SES_message = event["Records"][0]["Sns"]["Message"];

  const SES_message_json = JSON.parse(SES_message);
  const SES_message_content = SES_message_json["content"];

  const email_message = await simpleParser(SES_message_content);
  console.log("email_message start --------");
  console.log(
    "Received email_message:",
    JSON.stringify(email_message, null, 2)
  );
  console.log("email_message end   --------");

  // 送信元アドレス
  const email_from = email_message.from.value[0]["address"];
  console.log(email_from);
  // 送信日時
  const email_date = JSON.stringify(email_message.date).replace(/"/g, "");
  console.log(email_date);
  // 件名
  const email_subject = email_message.subject;
  console.log(email_subject);
  // 本文
  const email_content = email_message.text;
  console.log(email_content);
};

SNSの連携を行い、受信テストをしましょう。

Pythonと比べると、コード量が少なく、Gmailやyahooなどのメールソフトでも問題なく対応できました。

参考

Python

Node.js

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
3
Help us understand the problem. What are the problem?