68
Help us understand the problem. What are the problem?

More than 5 years have passed since last update.

posted at

updated at

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

目的

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

構成

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

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

image

S3バケットの作成

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

S3_Management_Console.png

IAMの設定

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

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

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

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>は適宜変更してください。

{
    "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に残しておこうと思って名前変更を行いました。

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 メール受信のセットアップ
Amazon SES による E メール受信のためのドメインの検証
Amazon SES による E メール受信のための MX レコードの公開

受信ルールの設定

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

SES_Management_Console.png

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

SES_Management_Console.png

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

SES_Management_Console.png

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

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

SES_Management_Console.png

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

※SESのSand Box環境では登録外のメールアドレスに送信することができないので、登録外のメールアドレスにSESから送信したい場合は、制限解除申請が必要です。
Amazon SESの送信制限を解除する(SandBoxの外へ移動する)
送信先が登録されたメールアドレスであれば必要ありません。

試してみる

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

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

山中のてすと_—_Google__すべてのメール_.png

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

山中のてすと_—_Inbox.png

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

S3_Management_Console.png

以上

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

Node.jsで文字コードの自動判別と自動変換

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

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

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

Amazon SESでメールを受信してみる その3

AWS Lambda内で文字コードを変換する方法

追記

メールサーバーを捨てて、問い合わせ受付をサーバーレスで実現する その2(HTMLメールと添付ファイルに対応)でHTMLメールと添付メールに対応しました!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
68
Help us understand the problem. What are the problem?