ちょっと話題の記事

メール受信もAPI Gateway+Lambdaで処理してみる

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

ウィスキー、シガー、パイプをこよなく愛する大栗です。 先日JAWS-UGアーキテクチャ専門支部で、API GatewayとLambdaでメール受信を行うという内容で発表してきたので、紹介します。サンプル実装もしたので解説します。

どうやってメールを受信するのか?

API Gatewayはhttpsのみ体操しているためsmtpは受けられません。AWSにsmtpを受信する機能が無いので外部サービスを活用しています。今回はSendGridを使用しています。

SendGrid Inbound Parse Webhook

SendGridは世界最大規模のメール送信プラットフォームですが、Inbound Parse Webhookというメール受信を行う機能があります。これは、SendGridがメールを受信して、メールの内容をWebhookとしてhttpリクエストしてくれます。

全体構成

SendGridで受信したメールをWebhookでAPI Gatewayへ送り、Lambdaで処理をします。 構成としては、下図の様になります。今回は赤い枠線の中について試してみました。 なお、各サービスのリージョンは全てVirginiaにしています。

sendgrid+api-gateway+lambda

設定してみる

Lambdaの設定

まず、Lambdaは以下の内容でFunctionを作成します。 Lambdaを実行するRoleでDynamoDBを操作する権限を付与することを忘れないで下さい。

PostMail

console.log('Loading function');

var AWS, dynamodb, Region, tableName;

tableName = 'mails';
Region = 'us-east-1';

AWS = require('aws-sdk');

AWS.config.update({
  region: Region
});

dynamodb = new AWS.DynamoDB();

exports.handler = function(event, context) {
  console.log('Received event:', JSON.stringify(event, null, 2));
  var decode = new Buffer(event.body, 'base64').toString();
  console.log('Decode Data:', decode);
  var params;
  params = {
    TableName: tableName,
    Item: {
      'request-key' : {"S": event.resourceId},
      'mail-key' : {"S": event.mailKey},
      'mail-secret' : {"S": event.mailSecret},
      'body': {"S": decode }
  }};
  console.log('Put Data:', JSON.stringify(params, null, 2));
  dynamodb.putItem(params, function(err, data) {
    if (err) {
        console.log('Error:', JSON.stringify(err, null, 2));
        context.done(null, err);
        return;
      }
    return context.done(null, 'Post succeeded.');
  });
};

これは、入力データのbodyをbase64デコードして、DynamoDBへ保存しています。

DynamoDB

DynamoDBはPrimary KeyをHashにしてrequest-keyという名称にしてテーブル[mails]を作成します。

API Gatewayの設定

今回は/mailsというリソースを作成して、POSTメソッドを作成します。

Method Request

Method RequestではQuery StringとしてMailKeyMailSecretを追加します。(ただし、MailKeyMailSecret確認するロジックを実装していないため、実際の使用ではロジックの追加が必要です) 以下の理由があるため、API GatewayのAPI Keyを使用していません。

  1. SendGridのWebhookでは独自のヘッダが使用できないため
  2. API GatewayのAPI Keyは認可で利用するものでないため

API_Gateway

Integration Request

Integration Requestで、作成したLambdaへの接続とデータの整形を行います。POSTされるデータはJSONである必要が有るため、Integration RequestでJSONへ整形します。 基本項目は以下のように設定します。

項目 内容
Integration type Lambda Function
Lambda Region us-east-1
Lambda Function PostMail

API_Gateway

Mapping TemplatesではContent-Typeにapplication/jsonmultipart/form-dataを設定します。 WebhookのContent-Typeはmultipart/form-dataなのですが、Test実行時のContent-Typeはapplication/jsonであるため、双方設定します。 テンプレートの内容は以下になります。POSTされる内容をBASE64エンコードしてJSONの値としています。

application/json, multipart/form-data

#set($inputParams = $util.base64Encode($input.path('$')))
#set($inputMailKey = $input.params('MailKey'))
#set($inputMailSecret = $input.params('MailSecret'))
{
  "stage" : "$context.stage",
  "requestId" : "$context.requestId",
  "apiId" : "$context.apiId",
  "resourcePath" : "$context.resourcePath",
  "resourceId" : "$context.resourceId",
  "httpMethod" : "$context.httpMethod",
  "sourceIp" : "$context.identity.sourceIp",
  "userAgent" : "$context.identity.userAgent",
  "accountId" : "$context.identity.accountId",
  "apiKey" : "$context.identity.apiKey",
  "caller" : "$context.identity.caller",
  "user" : "$context.identity.user",
  "userArn" : "$context.identity.userArn",
  "mailKey": "$inputMailKey",
  "mailSecret": "$inputMailSecret",
  "body": "$inputParams"
}

Method RequestとIntegration Requestを設定したらDeploy APIします。Stageには任意の名称を設定してください。

Stageの設定

Stageの設定では、CloudWatch Settingsの各項目を有効にしてLog LevelをINFOに設定しました。

API_Gateway

Route 53の設定

SendGridへメールを送信するために、メール受信用野ドメインのMXレコードをmx.sendgrid.netへ向ける必要があります。

Route_53_Management_Console

SendGridの設定

SendGridのダッシュボードから[SETTINGS]-[Inbound Parse]を選択します。

SendGrid

Add Host & URLをクリックして、HOSTURLを設定します。

  • HOST:受信するメールアドレスのドメイン
  • URL:API Gatewayで設定したMethodのURL+Query String 今回はhttps://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/prd/mails?MailKey=abc&MailSecret=1234のように設定します。

メール送信してみる

設定が環境したら、実際にメールを送信してみます。 以下の様にメールを作成して送信します。

send-mail

1分程度待つと、DynamoDBへメールの内容が保存されます。

DynamoDB_AWS_Console

メール本文はSendGridでPOSTされた内容のまま、以下のように保存されます。

--xYzZY
Content-Disposition: form-data; name="headers"

Received: by mx0034p1XXXX.sendgrid.net with SMTP id wNCQCLXXXX Wed, 25 Sep 2013 09:17:53 +0000 (UTC)
Received: from ss06.example.com (ss06.example.com [192.0.2.1]) by mx0034p1xxxx.sendgrid.net (Postfix) with SMTP id 169E2A8XXXX for <testmail@sendgrid10.example.com>; Wed, 25 Sep 2013 09:17:52 +0000 (UTC)
Received: from ([192.0.2.1]) (envelope sender: <local@example.com>) by ss06.example.com with Active!gate esmtp server; Wed, 25 Sep 2013 18:17:16 +0900
Received: from ([192.0.2.1]) (envelope sender: <local@example.com>) by mail.example.com with TransWARE esmtp server; Wed, 25 Sep 2013 18:17:16 +0900
Received: by lahh2 with SMTP id h2so9031974XXXX.0 ; Wed, 25 Sep 2013 02:17:13 -0700 (PDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=google; h=mime-version:from:date:message-id:subject:to:content-type; bh=774H8YhD73M6Iv12382PjN/VL65678/kQI3o/Dctpus=; b=DStS12345owhGmCs6K5F56oytabcde+vaXrVT/0EUz1dBiMDEf8kHGc5GZo6E98Cpr LxLrstT/5EvWAS12345mIXqYabcdeXnH0bWcs=
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20130820; h=x-gm-message-state:mime-version:from:date:message-id:subject:to :content-type; bh=774H8Y123456Iv+uQE2PjN123451/a/kQI3o/Dctpus=; b=PIvHArVabcdewumhfGvhWlnmN8PQy8y+KDW5wwWaVDjDK9YebcdeUCqLC5ay8UB/z SbEZOnGo5xR3OnFoxij4a/fnApMfW3Uv6Msv1abcdekIaKnOgOg3evwoDhfLsO+U6UD3 IK8OZiYAUJRwegsCF oYNdhgf/L94Oj0G+ Ghqw==
X-Gm-Message-State: ALoCoHkY0DHEG1GGsM6OkKiQSPLYKJkw4abcdermWSwAKTNA4P/x14Pq3koYrh6MrIqdtVIgLDLlrIWA+Ytq6L1TbM2jDoeN/Tbmw0Rq0+IPMQW3elGp6tnC5qECQ8yCY/12345N4AfPBf1TOAalMKRL74rW4PbZn3z2jqMdrmsoSG2gdpRIFwg=
X-Received: by 192.0.2.1 with SMTP id s2mr278326lbr.29.1443172633158; Wed, 25 Sep 2013 02:17:13 -0700 (PDT)
X-Received: by 192.0.2.1 with SMTP id s2mr278312lbr.29.1443172632593; Wed, 25 Sep 2013 02:17:12 -0700 (PDT)
MIME-Version: 1.0
Received: by 192.0.2.1 with HTTP; Wed, 25 Sep 2013 02:16:53 -0700 (PDT)
From: =?UTF-8?B?5aSn5qCX5a6X?= <local@example.com>
Date: Wed, 25 Sep 2013 18:16:53 +0900
Message-ID: <CAOgvZW5XuQfYQmYUDNndpCMWoxqY2aUpAKtSJnyzHvq5mLOyJw@mail.example.com>
Subject: =?UTF-8?B?44K/44Kk44OI44Or44Gn44GZ?=
To: testmail@sendgrid10.example.com
Content-Type: multipart/alternative; boundary=582amvu10gk31db1lfy3208ow0df

--xYzZY
Content-Disposition: form-data; name="dkim"

{@example.com : pass}
--xYzZY
Content-Disposition: form-data; name="to"

testmail@sendgrid10.example.com
--xYzZY
Content-Disposition: form-data; name="html"

<div dir="ltr"><div>本文です。</div><div>あいうえお</div><div>かきくけこ</div>
</div>

--xYzZY
Content-Disposition: form-data; name="from"

大栗宗 <local@example.com>
--xYzZY
Content-Disposition: form-data; name="text"

本文です。
あいうえお
かきくけこ

--xYzZY
Content-Disposition: form-data; name="sender_ip"

223.27.116.84
--xYzZY
Content-Disposition: form-data; name="envelope"

{"to":["testmail@sendgrid10.example.com"],"from":"local@example.com"}
--xYzZY
Content-Disposition: form-data; name="attachments"

0
--xYzZY
Content-Disposition: form-data; name="subject"

タイトルです
--xYzZY
Content-Disposition: form-data; name="charsets"

{"to":"UTF-8","html":"UTF-8","subject":"UTF-8","from":"UTF-8","text":"UTF-8"}
--xYzZY
Content-Disposition: form-data; name="SPF"

pass
--xYzZY--

最後に

外部サービスを利用することで、Non EC2アーキテクチャでメールの受信処理が出来る様になりました。今回の構成では障害時のリトライ処理やメール受信の担保を省いているので、実運用に活用する場合はクリティカルな場面を避けたり、リトライや冗長化を図る必要がありますので注意してください。