SES で受信したメールを Lambda で処理してみた


この記事は今年もやるよ!AWS Lambda縛り Advent Calendar 2015の12/12日分の記事です。

Amazon SES のメール受信機能からの Lambda 連携をやったことがなかったので、ちょっとした処理をしてみました。

処理の内容

特定の人からメールを受信した時にツイッター等で通知したいことがありますよね?(ないですか、そうですか)
今までは単純に IFTTT で Gmail と Twitter をつなげてやっていましたが、何回目とか何日ぶりかを含めたツイートにしたかったので、Lambda で作ることにしました。

構成

当初 Gmail -> IFTTT -> Maker.ch -(HTTP POST)-> API Gateway -> Lambda -(HTTP POST)-> Maker.ch -> IFTTT -> Twitter と考えていましたが、なぜか Maker channel の REST request が API Gateway を呼び出せず(憶測ですが、SNI非対応?)、ふと SES でメール受信できたなーと思って使ってみる事にしました。

最終的な構成は以下のような感じです。

※流行りのCLOUDCRAFTで描いてみました(余談ですけど、このサイト AWS じゃなくて Digital Oceanで動いてるんですね)。

セットアップ

大まかな流れは、

  1. IFTTT に Recipe を作成
  2. Lambda ファンクションを仮作成(受信したメールが確認出来る様に)
  3. SES の Rule 作成
  4. (上記の流れで) Rouet53 に SES認証用のレコードと MX レコードを設定
  5. Gmail から メール転送を設定
  6. DynamoDB Table を作成
  7. Lambda ファンクションを本番コードで更新

といった順番になります。

IFTTT に Recipe を作成

  • レシピ作成で、this に Maker 、That を Twitter で作成します
  • Maker の Event Name には、"twitter" と指定しました
  • Twitter の Action には、"Post a tweet" を選択し、Tweet text には、
{{Value1}}からメール着信、{{Value2}}日ぶり {{Value3}}回目

と入れました

作成が終わったら、Maker Channel の設定画面から、テスト実行を行う事が出来ます。

実際に Twitter に下記のようにツイートされました。

Lambda ファンクション仮作成

後ほど設定する Gmail のメール転送設定のために、先に Lambda の Function を作ってしまいます。
SESのリージョンと合わせる必要があるので、us-east-1 に作成しました。名前は ses としました。

exports.handler = function(event, context) {
    console.log(JSON.stringify(event));
    context.succeed(event);
};

入ってきた event object をそのままダンプするだけのスクリプトですので、特に解説はいらないと思います。

SESのルール作成 & Route53設定

SES のコンソールの左下から、Email Receiving > Rules Sets をクリックし、Create a Receipt Rule をクリック。

次にメールを受信する対象のドメインまたはアドレスを指定します。今回は jaws.ninja ドメイン宛のメール全てを受け付ける設定とします。
ドメインの認証を行う必要がある場合には、下記のように SES から Route53 の Hosted Zone に必要なレコードを追加してくれます。便利ですねー。

次に、実行する Action として、先ほど作成した Lambda Function を指定します。

あとはデフォルトのまま進めて、作成完了です。

Gmailの転送設定

転送先アドレスの追加

まず転送が出来るように、転送先アドレスを追加します。

  • Gmail の設定の「メール転送とPOP/IMAP」タブから、「転送先アドレスを追加」を押します。
  • アドレスは適当に、[email protected] などとします
  • 転送確認のためのメールが、SES を通じて lambda に渡されるので、CloudWatch logs から受け取った内容を確認します

subject のところに、確認用コードが書いてあるので、Gmail の方に入力します。
これで転送先アドレスの追加は完了です。

フィルタの設定

特定のアドレスから来たメールが、先ほど追加したアドレスに転送されるように設定します。

まずは抽出条件を指定し、

次に処理の内容(先ほど追加したアドレスへの転送)を指定し、フィルタを作成します。

以上で Gmail 側の設定は完了です。

DynamoDB の設定

今回は単純に、メールアドレス を Partition Key とするテーブルを作成します。
てっとり早くCLIで作成しました。

$ aws dynamodb create-table --table-name email-count ¥
 --attribute-definitions '[{"AttributeName":"email", "AttributeType":"S"}]' ¥
 --key-schema '[{"AttributeName":"email","KeyType":"HASH"}]' ¥
 --provisioned-throughput '{"ReadCapacityUnits":1, "WriteCapacityUnits":1}'
{
    "TableDescription": {
        "TableArn": "arn:aws:dynamodb:us-east-1:000000000000:table/email-count",
        "AttributeDefinitions": [
            {
                "AttributeName": "email",
                "AttributeType": "S"
            }
        ],
        "ProvisionedThroughput": {
            "NumberOfDecreasesToday": 0,
            "WriteCapacityUnits": 1,
            "ReadCapacityUnits": 1
        },
        "TableSizeBytes": 0,
        "TableName": "email-count",
        "TableStatus": "CREATING",
        "KeySchema": [
            {
                "KeyType": "HASH",
                "AttributeName": "email"
            }
        ],
        "ItemCount": 0,
        "CreationDateTime": 1449823894.715
    }
}

また、受信をカウントしたいメールアドレスの Item を作成しておく必要があります。下記のようなアイテムを作成しておきました。

{
  "count": 2,
  "email": "[email protected]",
  "name": "moto",
  "unixtime": 1449829080
}

Lambda ファンクションを再作成

Python で Function を同じ名前で再作成します。

処理の内容は、

  • 受け取ったメールの from アドレスをキーとして DynamoDB の Item を Update します
  • その際、カウントは ADD Action でアトミックに追加します
  • 変更前の unixtime を取得し、現在時刻との差分から、何日ぶりかを計算します
  • IFTTT の Maker channel の API エンドポイントに JSON 形式で POST をします

のような流れです。

当然ですが DynamoDB のテーブルへの UpdateItem 権限が必要ですので、適切に Role を設定してください。

# -*- coding: utf-8 -*-
import boto3
import json
import re
import urllib2
import time

# 使い回すオブジェクトはハンドラ外で定義しましょう
dynamodb = boto3.resource('dynamodb', region_name='us-east-1')
table = dynamodb.Table('email-count')
url = 'https://maker.ifttt.com/trigger/twitter/with/key/KEY'

def lambda_handler(event, context):
    # メアドを event オブジェクトから取得
    email = event['Records'][0]['ses']['mail']['source']
    unixtime = int(time.time())

    # update_item で、count をインクリメント、変更前のオブジェクトを取得
    response = table.update_item(
        Key={'email': email},
        AttributeUpdates={'count': {'Value': 1,'Action': 'ADD'}, 'unixtime':{'Value': unixtime} },
        Expected={'email':{'ComparisonOperator':'NOT_NULL'}},
        ReturnValues='ALL_OLD'
    )
    print response

    # 得られたデータから、IFTTT に渡すパラメータを決定
    name = response['Attributes']['name']
    count = int(response['Attributes']['count'])+1
    days = int( (unixtime - response['Attributes']['unixtime']) / (60*60*24))
    payload = json.dumps({'value1':name, 'value2':str(days), 'value3':str(count)})
    print payload

    # HTTPS POSTでリクエストを送る
    req = urllib2.Request(url, payload, {'Content-Type': 'application/json'})
    f = urllib2.urlopen(req)
    response = f.read()
    f.close()
    return response

デモ

実際にメールを送ってみると...

自動でツイートされました!

まとめ

メールベースで何か処理をしたいというケースは結構あると思いますので、これは新しいクラウドデザインパターンになりえるのではないかなーと思います。