AI事業本部のDynalystでエンジニアをしている黒崎 ( @kuro_m88 ) です。
AWSにはAmazon SES(Simple Email Service)というメールを送受信できるマネージドサービスがあるのはご存知でしょうか。

Amazon SESでメール送信する例は多いですが、Amazon SESでメールを受信→AWS Lambdaでメールを開く→メールの内容に従って作業を自動化という例はあまりなさそうだったので紹介します。

今回作ったもの

今回作ったのはSlack botです。
社内のスケジュール管理システムのスケジュール通知を受け取ったら、内容をパースしてSlackに通知します。

公開予定はSlackの自分のtimesに通知するようにしてみました。

timesに通知する

非公開予定は予定がある事だけを通知するようにしてみました。

非公開予定の場合

一方で、自分が把握できないのは不便なので、privateチャネルに自分宛てに通知するようにしてみました。

非公開予定はprivateチャネルに通知

概要

今回作ったものをもう少し細かくフローに落とし込むと

  • スケジュール通知メールが送信される
  • Amazon SESでスケジュール通知メールを受信し、メールの内容をS3に保存する
  • スケジュール通知メールをフックにLambda Functionをキックする
  • スケジュール通知メールの内容をパースする
  • スケジュール通知をSlackに流す

という感じです。

アプリケーション

コードはGitHubに公開しました。

https://github.com/kurochan/schedule-slack-notify-bot

エントリーポイント

https://github.com/kurochan/schedule-slack-notify-bot/blob/master/lambda_function.rb#L16-L31

エントリーポイントはAWS Lambdaから最初に呼び出される関数で、lambda_handlerという関数名にしています。Lambda Function実行時に呼び出す関数名はAWS Lambdaの設定で変更できます。

Lambdaに渡されるメールの情報について

変数eventにはメールに関する情報が格納されます。
データ構造のサンプルは長いのでgistに添付しました。気になる方はご覧ください。

https://gist.github.com/kurochan/dfb83b4ebf3a5d690b56b96816873480

Lambdaに渡されるイベントデータにはメールのメタデータ(ヘッダ等)のみが格納されていて、メールの本文は含まれていません。
メールヘッダと本文を含むメールの全体はS3に保存されているので、イベントデータからmessage idを取り出し、message idをキーにS3からメール全体をダウンロードする必要があります。
S3に保存されているメールは特に加工されておらず、受信したものがそのまま保存されています。
メールはInternet Message Formatというフォーマットで記述されています。具体的にはRFC 5322( https://www.ietf.org/rfc/rfc5322.txt )等で 規定されています。
もちろんMIME(Multipurpose Internet Mail Extensions)にも対応しているため、プレーンテキスト以外にもHTML形式や、添付ファイルにも対応しています。

プレーンテキスト形式以外は自分でメールをパースするのは大変だと思うので、メールをパースするライブラリを使うのが良いと思います。よく使われている言語であればメール系のライブラリはすぐ見つかると思います。標準ライブラリにメール関連の操作APIが含まれていることもあります。

メールをパースする

https://github.com/kurochan/schedule-slack-notify-bot/blob/master/app/mail_handler.rb#L26-L41

メールのmessage idをイベントデータから取り出し、それをキーにS3に保存されたメッセージを保存しています。
メールのパースはrubyのmailというgemを使っています。
メールがマルチパートの場合とプレーンテキストの場合で扱いが違うので処理を分岐しています。

message_id = context.lambda_event['mail']['messageId']
s3_bucket = context.config.mail.s3.bucket
s3_key = "#{context.config.mail.s3.path}/#{message_id}"
context.logger.debug("message_object: s3://#{s3_bucket}/#{s3_key}")

message = context.s3.get_object({bucket: s3_bucket, key: s3_key}).body.string
mail = Mail.read_from_string(message)

mail_text = ""
if mail.multipart?
  charset = mail.text_part.content_type_parameters[:charset]
  mail_text = mail.text_part.body.to_s.force_encoding(charset).encode("UTF-8").gsub("\r", "")
else
  charset = mail.content_type_parameters[:charset]
  mail_text = mail.body.decoded.force_encoding(charset).encode("UTF-8").gsub("\r", "")
end

これ以降はメールに含まれるメッセージ本文テキストをパースしている部分は社内のスケジュール管理システムのメール本文のフォーマットに合わせて経験則でハードコードしているので特に参考にはならないかと思います。
かなり雑なパースをしているので、メール本文に特定の文字列が含まれているとパースに失敗しますが、実務上問題は起きなさそうなのでそのままにしています。

Slackに通知する

summary = "#{schedule.title_text}|#{schedule.place_text}|#{schedule.schedule_text}"
blocks = generate_block_public(user_id, schedule, true)
context.client.chat_postMessage(channel: context.config.slack.channel_id_private, text: summary, blocks: blocks, as_user: true) if context.config.slack.post_to_private
blocks = generate_block_public(user_id, schedule, false)
context.client.chat_postMessage(channel: context.config.slack.channel_id_public, text: summary, blocks: blocks, as_user: true) if context.config.slack.post_to_public

メッセージのフォーマットはBlock Kit Builderで作りました。
JSONをそのまま書くのは大変ですが、Block Kit Builderでテンプレートが簡単につくれるので簡単に見栄えを調整できます。

https://github.com/kurochan/schedule-slack-notify-bot/blob/master/app/mail_handler.rb#L96-L122

Amazon SESの設定

Amazon SESは利用可能なリージョンは少ないです。東京リージョン(ap-northeast-1)もAmazon SESは使えないので、us-east-1を使っています。
Amazon SES以外にもS3とAWS Lambdaもus-east-1で統一します。

メール受信のルールセット

何かしらのメールを受信したときにどのような処理をするのかという処理を書きます。
ここでは、特定のドメインのメールアドレス宛に来たメールは指定したS3のバケットに保存し、その後指定したLambda Functionをキックするようにしました。

メール受信のルールセット

ドメインのバリデーション

メールの送信先ドメインをAmazon SESに向け、メールを受信できるようにします。
それに加え、ドメインの所有確認をします。

ドメインのバリデーション

ここまでひととおり設定すると、メールを受信するとメール本体がS3に保存され、Lambda Functionがキックされるようになります。

課題

公開しているコードでは特に対策をしていませんが、通知メールが本物かどうかのバリデーションが必要です。
実際に自分で使っているものでは、送信先アドレスとメールの受信ドメインに設定したサブドメインと送信先メールアドレスをそれぞれランダムに生成した文字列を設定し、それらが一致しているかどうかでバリデーションを掛けました。

まとめ

AWSでメールを受信したらLambda Functionをキックし、作業を自動化する仕組みの例についてご紹介しました。