SendGridのWebhookをGoogle Cloud Functionsで受けてCloud Datastoreに貯める

やりたいこと

現在開発中のサービスではメール送信にSendGridを利用している 。
ただ、webAPIにメール送信リクエストを送って終わっているので、送信時のエラーはわかるが宛先間違いや受信メールサーバ側でのブロックまで検知できていない。
まずはSendGridのWebhookをサーバレスで貯めるところまで進める。
Node.js8とCloud Datastoreを使った例がなかったので残す。

途中までは以下と同じ
SendGrid のチュートリアル

Cloud Functionsの設定

関数作成

名前
(例)sendgrid-web-hook
※エンドポイントのURLになるのでわかりやすく
メモリ
とりあえず128MB
トリガー
HTTP
ソースコード
インラインエディタ
ブラウザで完結するのでお手軽
ランタイム
Node.js 8
※async/awaitがデフォルトで使えるようになっている
リージョン
asia-northeast1
タイムアウト
60秒
環境変数
Basic認証とCloudstore操作用に以下の3つを設定
  • BASIC_USERNAME
  • BASIC_PASSWORD
  • PROJECT_ID

SendGridの設定

ログインしてダッシュボードへ
「Settings」>「Mail Settings」>「Event Notification」>「HTTP POST URL」
ここにCloud Functionsで作成した関数のエンドポイントを指定する。
以下の記法でBasic認証も使える。
http(s)://username:password@endpoint

ソース

index.js

const Buffer = require('safe-buffer').Buffer;
const Datastore = require('@google-cloud/datastore');

const BASIC_USERNAME = process.env.BASIC_USERNAME;
const BASIC_PASSWORD = process.env.BASIC_PASSWORD;
const PROJECT_ID = process.env.PROJECT_ID;
const datastore = Datastore({projectId: PROJECT_ID});

/**
 * Verify that the webhook request came from sendgrid.
 *
 * @param {string} authorization The authorization header of the request, e.g. "Basic ZmdvOhJhcg=="
 */
function verifyWebhook (authorization) {
  const basicAuth = Buffer.from(authorization.replace('Basic ', ''), 'base64').toString();
  const parts = basicAuth.split(':');
  if (parts[0] !== BASIC_USERNAME || parts[1] !== BASIC_PASSWORD) {
    const error = new Error('Invalid credentials');
    error.code = 401;
    throw error;
  }
}

/**
 * Receive a webhook from SendGrid.
 *
 * See https://sendgrid.com/docs/API_Reference/Webhooks/event.html
 *
 * @param {object} req Cloud Function request context.
 * @param {object} res Cloud Function response context.
 */
exports.sendgridWebhook = async (req, res) => {
  try {
    if (req.method !== 'POST') {
      const error = new Error('Only POST requests are accepted');
      error.code = 405;
      throw error;
    }
    await verifyWebhook(req.get('authorization') || '');
    const events = req.body || [];
    for (const event of events) {
      event['sg_message_id_prefix'] = event['sg_message_id'].match(/^(.*?)\.filter.*/)[1];
      const key = datastore.key([
        'sg_event',
        event['sg_event_id']
      ]);
      const entity = {
        key: key,
        data: event
        
      };
      await datastore.insert(entity).catch((err) => console.error(err));
    }
    res.status(200).end()
  } catch (err) {
    console.error(err);
    res.status(err.code || 500).send(err);
  };
};

package.json

{
  "name": "sendgrid-webhook",
  "version": "0.0.1",
  "main": "index.js",
  "dependencies": {
    "safe-buffer": "^5.1.2",
    "@google-cloud/datastore": "1.3.4"
  }
}

Datastore内データイメージ

f:id:takuya_minami373:20180903171338p:plain SendGridで定義される変数+『sg_message_id_prefix』を保存する。
『sg_message_id』は【[共通部分].filter[メールアドレスごとに個別部分]】の形になっていて、
サービス側でメール送信APIを叩いたときに取得できるのは共通部分なので、検索しやすいようにカラムをわけておく。
※前方一致で無理矢理とってくることも可能だけどきれいな方法ではなかった
続きは別記事で。 tminami.hatenablog.com