はじめに
SESでメールを受信し、Lambdaを使用して、Dynamodb等にメール情報を書き込む、という処理がありました。
SESのメール受信機能は、リージョンが限られており、また、メール情報を適切に変換し、件名や本文を取得するのに苦労したため、その点を中心に記載します。
事前構築
- Route53でドメインの設定済み。仮にドメインを
hoge.com
とします。- 受信メールで使用するドメインは、今回、サブドメインを使用する想定です。
mail.hoge.com
- 受信メールで使用するドメインは、今回、サブドメインを使用する想定です。
- 送信元は、Gmailを想定
- メールによって、MINEタイプの書き方が異なります。
SESの受信メール設定
受信メールで使用するドメインは、mail.hoge.com
です。
受信機能が使えるリージョンは、現在3つしかありません。今回は、バージニア州で設定を行います。
下記のように、バージニア州のSESに遷移し、Verified identities
から、サブドメインをmail.hoge.com
とし、作成します。
sandbox制限解除
sandbox制限解除をする必要があります。
下記の記事通りにするとよいです。
送信制限緩和が必要な方は、合わせて行ってください。
すでにご使用のバージニアリージョンで設定済みの方は、パスして問題ありません。
SES受信設定
バージニアのSESのEmail receiving
に遷移し、ルールを作成します。
send-to-lambda
という名前にしました。
受信で使用するメールアドレスを設定します。
mail.hoge.com
を設定しました。
特定のサブドメイン内のすべてのアドレスとマッチされます。
例えば、gmailでtest@mail.hoge.com
宛に送信すると、SESでルールに従って受信できるイメージです。
SNSに送るため、SNSを作成します。
create SNS topic
をクリックしましょう。
Encoding
は、UTF-8
にします。Base64
すると、Lambdaでエンコードする必要でます。
また、SNSもバージニアリージョンでの作成が必須になります。
受信ルールを有効化
受信ルールのStatusがinactive
になっています。
Set as active
をクリックして、有効化しましょう。
有効になりましたね
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
(リージョンは、バージニア州)
PyhonでLambdaを作成
東京リージョンで作成します。
Pythonではなく、Node.jsでも良い場合、Node.jsでLambdaを作成しましょう。
作成方法は、こちら↓
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ではエラーになりましたが。
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に配信するよう設定しましょう。
LambdaのARNを貼り付け、サブスクリプションの作成
をクリックしましょう。
これで、Gmailで以下の通りにメールを送信すると、SESに受信し、SNS経由でLambdaに適切なメール情報が取得されます。
- 宛先:
<お好きな文字>@mail.hoge.com
- 本文:
ほんぶんです。
- 件名:
けんめいです。
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?が入っていますね。。
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.zip
LambdaLayerにアップします。
Node.jsのLambda作成
Node.jsでLambdaを作成し、LambdaLayerで先程のレイヤーを追加します。
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);
};
Pythonと比べると、コード量が少なく、Gmailやyahooなどのメールソフトでも問題なく対応できました。
参考
Python
Node.js