メールサーバーを捨てて、問い合わせ受付をサーバーレスで実現する

# 目的

メールサーバーを廃止して、問い合わせ受付をサーバーレスで実現したい

# 構成

検討段階では、SESから直接もしくはSNS経由でLambda呼べばいいのでは？と思っていたのですが、どちらのパターンもeventにメール本文が入っていなかったため、一旦S3に格納されたメッセージを取得することにしました。
流れは以下のとおりです。

1. ユーザーが問い合わせメールを送信
2. SESが受け取り、S3にメール格納
3. Lambdaで「S3からメッセージ取得、SESでメール送信」を行う

![image](https://qiita-image-store.s3.amazonaws.com/0/40136/cf1d5451-79ff-5a86-ae33-3918b2e340a3.png)

# S3バケットの作成

S3にメール保存用バケットを作成します。
※今回「inquiry-bucket」バケットを作成しました。

<img width="357" alt="S3_Management_Console.png" src="https://qiita-image-store.s3.amazonaws.com/0/40136/4b4846bc-fe73-92f6-9fce-6ecc7dd92c64.png">

# IAMの設定

Lambda用のIAMロールを作成します。
以下の権限を持ったロールを作成します。

+ S3からメッセージオブジェクトを取得する
+ CloudWatch Logsにログを出力する
+ SESでメールを送信する
+ S3のメッセージオブジェクトを複製する（S3のオブジェクト名変更のため）
+ S3のメッセージオブジェクトを削除する（S3のオブジェクト名変更のため）

今回は、inquiryRoleというロールを作成し、以下ポリシーを設定しました。

```json:inquiryPolicy
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmt1474188898000",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Sid": "Stmt1474188915000",
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:DeleteObject",
                "s3:ReplicateObject"
            ],
            "Resource": [
                "arn:aws:s3:::inquiry-bucket"
            ]
        },
        {
            "Sid": "Stmt1474294979000",
            "Effect": "Allow",
            "Action": [
                "ses:SendEmail"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}
```

# バケットポリシーの設定

以下を許可するために「inquiry-bucket」にバケットポリシーを設定します。

+ SESからオブジェクトをPUTする
+ inquiryRoleからinquiry-bucketへの操作を許可する

実際に設定したポリシーは以下のとおりです。
※\<AWS Account No>は適宜変更してください。

```json:
{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Sid": "AllowSESPuts-1472784238688",
			"Effect": "Allow",
			"Principal": {
				"Service": "ses.amazonaws.com"
			},
			"Action": "s3:PutObject",
			"Resource": "arn:aws:s3:::inquiry-bucket/*",
			"Condition": {
				"StringEquals": {
					"aws:Referer": "<AWS Account No>"
				}
			}
		},
		{
			"Sid": "AllowFromRole",
			"Effect": "Allow",
			"Principal": {
				"AWS": "arn:aws:iam::<AWS Account No>:role/inquiryRole"
			},
			"Action": "s3:*",
			"Resource": "arn:aws:s3:::inquiry-bucket/*"
		}
	]
}
```

# Lambdaの設定

## Lambdaファンクションの作成
各種設定は以下を選んでください。
　Runtime ： **Node.js 4.3**
　Role ： **inquiryRole**
また、今回は
　Memory ： **128MB**
　Timeout ： **10 sec**
としています。

また、リージョンは「**us-east-1**」に作成します。

## コード
コードは以下の通りです。
簡単に説明すると以下のようなことを行っています。

1. S3に格納されたメッセージを取得する
2. 文字コードを「iso-2022-jp」から「utf-8」に変換する
3. 送信する内容を整理する
4. メールを送信する
5. S3に格納されたメッセージを「日付 + 送り主のアドレス」としてコピーする（おまけ）
6. S3に格納されたオリジナルのメッセージを削除する（おまけ）

メッセージ送信後はオリジナルオブジェクトを削除しようかと思ったのですが、一応S3に残しておこうと思って名前変更を行いました。

```js:inquiryFunction
'use strict';

console.log('Loading function');
const AWS = require('aws-sdk');
const fs = require('fs');
const s3 = new AWS.S3();
const filepath = '/tmp/message';

const TOADDRESS = 'info@xxxx.co.jp';
const SOURCEADDRESS = 'no-reply@xxxx.co.jp';
const BUCKETNAME = 'inquiry-bucket';

exports.handler = function(event, context, callback) {
    
    const messageId = event.Records[0].ses.mail.messageId;
    
    const generator  = (function *() {
        
        try {
            // S3からメッセージを取得する
            const s3Object = yield getObject(messageId, generator);
            
            // 文字コード変換
            yield convertCharacter(s3Object, generator);
            
            // 送信内容定義
            const sendMailInfo = yield setSendMailInfo(generator);
            
            // メッセージ送信
            yield sendMessage(sendMailInfo, generator);
            
            // S3オブジェクトキー書き換え
            yield copyObject(messageId, sendMailInfo.from[0].address, generator);
            yield deleteObject(messageId, generator);
            
            callback(null,'succeed!');
            
        } catch (e) {
            callback(e.message);
        }
    })();
    
    /* 処理開始 */
    generator.next();
};

// S3からメッセージを取得する
function getObject(messageId, generator) {
    
    const params = {
        Bucket: BUCKETNAME,
        Key: messageId
    };
    
    console.log('Getting object from ' + BUCKETNAME);
    
    s3.getObject(params, function(err, data) {
        if(err) {
            console.log(err, err.stack);
            generator.throw(new Error('getObject Error'));
            return;
        }
        console.log('Got object');
        generator.next(data);
    });
}

// 文字コード変換
function convertCharacter(s3Object, generator) {
    
    const exec = require('child_process').exec;
    const cmd = "iconv -f iso-2022-jp -t utf-8 " + filepath;
    
    // Getしたオブジェクトを書き込み
    fs.writeFileSync(filepath, String(s3Object.Body));
    
    console.log('Executing command :' + cmd);
    
    // 文字コード変換
    const child = exec(cmd, function(err, stdout, stderr) {
        if (err) {
            console.log(err, err.stack);
            generator.throw(new Error('Conversion Error'));
            return;
        }
        console.log('Conversion successful');
        console.log(stdout);
        generator.next();
    });
}

// 送信内容定義
function setSendMailInfo(generator) {
    
    const MailParser = require("mailparser").MailParser;
    const mailparser = new MailParser();
    
    mailparser.on("end", function(mail_object){
        const from = mail_object.from;
        const subject = mail_object.subject;
        const text = mail_object.text;
        const sendMailInfo = {
            from: from,
            subject: subject,
            text: text
        };
        generator.next(sendMailInfo);
    });
    
    fs.readFile(filepath, function(err, data) {
        if (err) {
            console.log(err, err.stack);
            generator.throw(new Error('setSendMailInfo Error'));
            return;
        }
        mailparser.write(data);
        mailparser.end();
    });
}

// メッセージを送信する
function sendMessage(sendMailInfo, generator) {
    const ses = new AWS.SES({'region' : 'us-east-1'});
    
    // SES送信用パラメータ
    const params = {
        Destination: {
            ToAddresses: [
                TOADDRESS
            ]
        },
        Message: {
            Body: {
                Text: {
                    Data: sendMailInfo.text,
                    Charset: 'utf-8'
                }
            },
            Subject: {
                Data: sendMailInfo.subject,
                Charset: 'utf-8'
            }
        },
        ReplyToAddresses: [
            sendMailInfo.from[0].address
        ],
        Source: SOURCEADDRESS
    };
    
    console.log('Sending Email ..');
    ses.sendEmail(params, function(err, data) {
        if (err) {
            console.log(err, err.stack);
            generator.throw(new Error('SES Error'));
            return;
        } else {
            console.log('Send Successful');
            console.log(data);
            generator.next();
        }
    });
}

// S3オブジェクトコピー
function copyObject(key, sourceAddress, generator) {
    
    const now = new Date();
    
    const params = {
        CopySource: BUCKETNAME + '/' + key,
        Bucket: BUCKETNAME,
        Key: now + ' from ' + sourceAddress
    };
    
    console.log('Copying Object from ' + params.CopySource + ' to ' + params.Bucket + '/' + params.Key);
    
    s3.copyObject(params, function(err, data) {
        if(err) {
            console.log(err, err.stack);
            generator.throw(new Error('CopyObject Error'));
            return;
        }
        console.log('Copied Successful');
        console.log(data);
        generator.next();
    });
}

// S3オブジェクト削除
function deleteObject(key, generator) {
    
    const params = {
        Bucket: BUCKETNAME,
        Key: key
    };
    
    console.log('Deleting Object ' + params.Bucket + '/' + params.Key);
    
    s3.deleteObject(params, function(err, data) {
        if(err) {
            console.log(err, err.stack);
            generator.throw(new Error('DeleteObject Error'));
            return;
        }
        console.log('Deleted Successful');
        console.log(data);
        generator.next();
    });
}
```

# SESの設定

## ドメインの検証、MXレコードの公開
今回はバージニア北部リージョンでSESを使用します。
※いつ東京リージョンにくるんだろう…

以下手順等を参考にドメインの検証、MXレコードの公開を行ってください。
[Amazon SES による E メール受信のセットアップ](https://docs.aws.amazon.com/ja_jp/ses/latest/DeveloperGuide/receiving-email-setting-up.html)
[Amazon SES による E メール受信のためのドメインの検証](https://docs.aws.amazon.com/ja_jp/ses/latest/DeveloperGuide/receiving-email-verification.html)
[Amazon SES による E メール受信のための MX レコードの公開](https://docs.aws.amazon.com/ja_jp/ses/latest/DeveloperGuide/receiving-email-mx-record.html)

## 受信ルールの設定

SESの[Email Receiving]から[Rule Sets]を選択すると以下の画面が出るので、［Create a Receipt Rule］をクリックします。

<img width="1065" alt="SES_Management_Console.png" src="https://qiita-image-store.s3.amazonaws.com/0/40136/97889149-ee1b-b3eb-9417-914d2df988f3.png">

SESで受け付けたいメールアドレス（info@xxx.co.jpやxxx.co.jpなど）を入力し、［Add Recipient］をクリックします。

<img width="1077" alt="SES_Management_Console.png" src="https://qiita-image-store.s3.amazonaws.com/0/40136/e4c02529-0e5a-3c97-0de0-8b4fe6a1fc1b.png">

すると、追加したメールアドレスorドメインがリストに追加されるので、Verify済みでなければ[Verify domain]をクリックし認証を行ってください。

<img width="1075" alt="SES_Management_Console.png" src="https://qiita-image-store.s3.amazonaws.com/0/40136/e82f9e42-5b68-87a5-d9c4-90190b1af0c5.png">

次にメールをSESが受け付けた後のアクションを設定します。
以下のように設定します。

+ Action1 : メッセージをS3の「inquiry-bucket」に保存
+ Action2 : Lambdaファンクション「inquiryFunction」を実行

<img width="1099" alt="SES_Management_Console.png" src="https://qiita-image-store.s3.amazonaws.com/0/40136/727e3a77-b743-a6b8-0b67-925d68bdfa8d.png">

これでSESの設定は完了です。

※SESのSand Box環境では登録外のメールアドレスに送信することができないので、登録外のメールアドレスにSESから送信したい場合は、制限解除申請が必要です。
[Amazon SESの送信制限を解除する（SandBoxの外へ移動する）](https://www.infoscoop.org/blogjp/2015/07/29/amazon-ses-release-limit/)
送信先が登録されたメールアドレスであれば必要ありません。

# 試してみる

実際にメールを送ってみて、うまく動くか試してみます。

以下のようにメールを作成し送信すると、

<img width="627" alt="山中のてすと_—_Google__すべてのメール_.png" src="https://qiita-image-store.s3.amazonaws.com/0/40136/34dbae46-d559-48ae-311f-f12b870b2a94.png">

きちんと返ってきました！

<img width="447" alt="山中のてすと_—_Inbox.png" src="https://qiita-image-store.s3.amazonaws.com/0/40136/4c2c83ac-14e0-c6a7-e7e6-d80cd450783d.png">

S3のメッセージもきちんと「日付 + 送り主のメールアドレス」に変更されていました！

<img width="492" alt="S3_Management_Console.png" src="https://qiita-image-store.s3.amazonaws.com/0/40136/d88f8bb7-1c09-94c7-40ad-ee0af1025f39.png">




# 以上

今回は特に日本語の文字コード変換の部分でかなりはまりました。
また、以下のように文字コードを判別してUTF-8に変換する方法も要検討かなと考えたのですが、とりあえず日本語メール前提でいいかなと思い今回は見送りました。

[Node.jsで文字コードの自動判別と自動変換](http://qiita.com/shoma2da/items/c77ec534dd36873d73c9)

また、一度に大量のメールが来た際に対応できるかわかりませんので、コードを都度修正していこうと思っています。

# 参考にさせていただきました

以下を参考にさせていただきました！ありがとうございます！

[Amazon SESでメールを受信してみる　その３](http://akira-tt.github.io/aws/2016/05/22/amazon-ses-3.html)

[AWS Lambda内で文字コードを変換する方法](http://www.bokukoko.info/entry/2015/08/30/AWS_Lambda%E5%86%85%E3%81%A7%E6%96%87%E5%AD%97%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E5%A4%89%E6%8F%9B%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95)

# 追記

[メールサーバーを捨てて、問い合わせ受付をサーバーレスで実現する その２（HTMLメールと添付ファイルに対応）](http://qiita.com/d-yamanaka/items/f35472bbe1565501bdd8)でHTMLメールと添付メールに対応しました！