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


この記事は[今年もやるよ！AWS Lambda縛り Advent Calendar 2015](http://qiita.com/advent-calendar/2015/lambda)の12/12日分の記事です。

[Amazon SES のメール受信機能からの Lambda 連携](http://aws.typepad.com/aws_japan/2015/09/ses-invoke.html)をやったことがなかったので、ちょっとした処理をしてみました。

## 処理の内容
特定の人からメールを受信した時にツイッター等で通知したいことがありますよね？(ないですか、そうですか)
今までは単純に 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 でメール受信できたなーと思って使ってみる事にしました。

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

![構成図](https://qiita-image-store.s3.amazonaws.com/0/30051/260bc76c-de20-450c-24f3-ce7124e25c74.png "CloudCraft.png")

※流行りの[CLOUDCRAFT](http://cloudcraft.co)で描いてみました(余談ですけど、このサイト 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 の設定画面から、テスト実行を行う事が出来ます。

![ifttt_16.png](https://qiita-image-store.s3.amazonaws.com/0/30051/4e6ed5b2-4abf-6d64-50df-3b5b42c29ca2.png "ifttt_16.png")

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

![ifttt_17.png](https://qiita-image-store.s3.amazonaws.com/0/30051/4633d5c9-38fd-7422-fd19-e0b51e5a7ddc.png "ifttt_17.png")

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

```javascript
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 をクリック。

![ses_01.png](https://qiita-image-store.s3.amazonaws.com/0/30051/04b433b0-29da-e20d-1a3b-7a141682fd1e.png "ses_01.png")

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

![ses_02.png](https://qiita-image-store.s3.amazonaws.com/0/30051/51a6024e-a02c-35a6-2628-4f09eaacda4d.png "ses_02.png")

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

![ses_03.png](https://qiita-image-store.s3.amazonaws.com/0/30051/651061a9-3062-f32d-031f-198d29b7df80.png "ses_03.png")

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

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

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

![gmail_01.png](https://qiita-image-store.s3.amazonaws.com/0/30051/a2a98963-c707-4ac4-a202-bf93a1d6be4d.png "gmail_01.png")

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

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

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

![gmail_02.png](https://qiita-image-store.s3.amazonaws.com/0/30051/d1dd212d-3e82-ba7e-039b-e6b10061737c.png "gmail_02.png")


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

![gmail_03.png](https://qiita-image-store.s3.amazonaws.com/0/30051/fb7f51cd-096b-d13a-28bc-6498defc881a.png "gmail_03.png")

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

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

```bash
$ 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 を作成しておく必要があります。下記のようなアイテムを作成しておきました。

```json
{
  "count": 2,
  "email": "moto@example.com",
  "name": "moto",
  "unixtime": 1449829080
}
```

### Lambda ファンクションを再作成
Python で Function を同じ名前で再作成します。

処理の内容は、

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

のような流れです。

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

```python
# -*- 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
```
## デモ

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

<blockquote class="twitter-tweet" lang="ja"><p lang="ja" dir="ltr">moto からメール着信、0 日ぶり、3 回目</p>&mdash; moto (@j3tm0t0) <a href="https://twitter.com/j3tm0t0/status/675405577510649856">2015, 12月 11</a></blockquote> <script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

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

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