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

# はじめに

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

![スクリーンショット 2022-05-25 0.18.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/0e072ab6-64c3-ede6-2d3d-290a4aa9707e.png)



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


# SESの受信メール設定
受信メールで使用するドメインは、`mail.hoge.com`です。

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

https://docs.aws.amazon.com/ja_jp/general/latest/gr/ses.html

下記のように、バージニア州のSESに遷移し、`Verified identities`から、サブドメインを`mail.hoge.com`とし、作成します。
![スクリーンショット 2022-05-23 21.14.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/44bd26ae-d391-9d88-45db-c12d86dfbdcb.png)

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

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

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

https://qiita.com/holdout0521/items/43a4b60810c094b42344#ses%E5%88%B6%E9%99%90%E8%A7%A3%E9%99%A4


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

![スクリーンショット 2022-05-23 21.31.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/a8116596-8597-4747-2efd-f0c74d837692.png)
![スクリーンショット 2022-05-23 21.34.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/528dfd44-6f53-175a-0075-e8bc350dd404.png)

`Create rule`をクリックします。
![スクリーンショット 2022-05-23 21.39.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/628d8956-1125-ac1c-7dc1-c8281e129484.png)

名前を記載し、他はデフォルトにします。
![スクリーンショット 2022-05-23 21.40.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/3952da72-7de4-87d1-c1f5-4a6e5b3f3792.png)

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

![スクリーンショット 2022-05-23 22.00.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/3a283ad9-830f-df10-1b38-acf58dbe9ce7.png)


SNSに送るため、SNSを作成します。
`create SNS topic`をクリックしましょう。
`Encoding`は、`UTF-8`にします。`Base64`すると、Lambdaでエンコードする必要でます。
また、SNSもバージニアリージョンでの作成が必須になります。
![スクリーンショット 2022-05-23 21.46.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/c6ace628-eb83-c9d8-dbe6-66db90f3051f.png)

![スクリーンショット 2022-05-23 21.50.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/02b029c8-af25-8e6a-3c0d-e6c4555c5560.png)

## 受信ルールを有効化
受信ルールのStatusが`inactive`になっています。
`Set as active`をクリックして、有効化しましょう。
![スクリーンショット 2022-05-23 21.52.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/bdcf8425-848a-3d76-514e-0a2783e78e2c.png)
有効になりましたね
![スクリーンショット 2022-05-23 21.52.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/620c54f5-2f16-1d7d-d6c3-9f999450585c.png)


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

https://docs.aws.amazon.com/ja_jp/ses/latest/dg/receiving-email-notifications-contents.html

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

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

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

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




# Route53の設定
受信メールで使用するドメインは、`mail.hoge.com`にします。

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

https://docs.aws.amazon.com/ja_jp/ses/latest/dg/receiving-email-mx-record.html

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

![スクリーンショット 2022-05-23 22.12.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/90ee9688-6395-0cf8-62ce-1c066c8ff0c6.png)


# PyhonでLambdaを作成



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

:::note warn
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/38ad2088-aefe-1160-a23c-6fd3fb060b03.png)

```py: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は、ヘッダーとボディの２つから構成されています。
宛先や送信元はヘッダーに、本文や添付ファイルはボディに入っています。
ボディはさらにMIMEという形式でエンコードされているのが一般的です。

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

https://ponsuke-tarou.hatenablog.com/entry/2018/02/24/163729

## メールソフトによっては、エラーになります
今回は、Gmailからの受信に限定しましたが、メールソフトによって、以下の理由で、今回のLambdaがエラーになる場合があります。
- IMF形式やMIMEエンコードを正しく実装出来ていない
- 文字コードの指定と実際のエンコーディングがソフトによって異なることがある
- 文字コードの名称が標準と異なることがある

実際に、別のメールソフトでメールを受信した場合、うまくいかないことがありました。
具体的には`get_email_body`メソッドでエラーになりました。
そのため、以下のように修正するとうまくいきました。逆にGmailではエラーになりましたが。
```py:メールソフトによっては、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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/a3a4b12b-54bc-c933-e59d-95bc8c7d53e5.png)


LambdaのARNを貼り付け、`サブスクリプションの作成`をクリックしましょう。
![スクリーンショット 2022-05-25 0.08.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/703b627a-4f73-1b76-3ad1-b5d7ef302bbb.png)



これで、Gmailで以下の通りにメールを送信すると、SESに受信し、SNS経由でLambdaに適切なメール情報が取得されます。
- 宛先：`<お好きな文字>@mail.hoge.com`
- 本文：`ほんぶんです。`
- 件名：`けんめいです。`

Lambdaのlogにも出力されることを確認しましょう。
```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？が入っていますね。。

```log: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で開発することをおすすめします。

https://qiita.com/kenshiroh/items/2f52484fbf7919478559

https://nodemailer.com/extras/mailparser/

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

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

$ 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.zip`LambdaLayerにアップします。

![スクリーンショット 2022-05-25 22.37.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/56936af6-0bf2-8795-8705-9ef8cd0b1ead.png)

![スクリーンショット 2022-05-25 22.34.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/a0ad1ec2-eaf2-ca5c-4eb7-02d21794c99b.png)

## Node.jsのLambda作成
Node.jsでLambdaを作成し、LambdaLayerで先程のレイヤーを追加します。![スクリーンショット 2022-05-25 22.40.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/2c2bdbae-bf0a-2660-967b-be4948073d91.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の連携を行い、受信テストをしましょう。](https://qiita.com/holdout0521/items/458b4b886edb58735836#sns)

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





# 参考

Python

https://memo.silverpigeon.jp/try-to-parse-the-e-mail-with-python3-but-its-not-so-canning-well/

https://qiita.com/jsaito/items/a058611cf9386addbc12

https://qiita.com/stkdev/items/a44976fb81ae90a66381

https://docs.python.org/ja/3/library/email.html

Node.js

https://qiita.com/Kakimoty_Field/items/4485a4e76d677d85ade0#aws-lambda