SendGrid Webhookを用いて送信失敗メールをキャッチする

## はじめに
SendGrid APIを用いて送信したメールのうち、送信失敗したメールをキャッチしたかったので、方法を模索し実装しました。
実用性のあるメソッドだと思うので、記事として残そうと思います。

## SendGridとは
サーバーを構築せずにAPIベースでメール送信をできるプラットフォームです。
導入はとても簡単です。詳しくは下記ページを参照。

https://sendgrid.kke.co.jp/



## Webhookとは
外部アプリケーション連携手段の一つです。
APIと異なり、こちらから通信を行うのではなく、外部アプリケーション側から情報を送ってもらいます。
API通信の場合はこちらから都度通信を行わないと情報を取得できません。したがって、外部アプリケーション側でトリガーを持っており、リアルタイムで情報を取得したいときにWebhookが有用です。
下記記事の説明がわかりやすかったです。

https://qiita.com/soarflat/items/ed970f6dc59b2ab76169


## SendGridでのメール送信
SendGridでメール送信APIを叩いて、メール送信機能を実行しています。
```ts
data = JSON.stringify({
    "personalizations": [
        {
            "to": [{"email": address}],
            "subject" : emailTitle
        }
    ],
    "from": {
        "email": fromAddress,
        "name": fromAddressName
    },
    "content": [
        {
            "type": "text/html",
            "value": value
        }
    ]
});
    
    
const config = {
    method: 'post',
    url: 'https://api.sendgrid.com/v3/mail/send',
    headers: { 
      'Authorization': `Bearer ${process.env.SENDGRID_AUTH_KEY}`, 
      'Content-Type': 'application/json'
    },
    data : data
};

await axios(config);
```

メールを送信したことはAPI response`status:202`で判別できます。しかし、そのメールがきちんと届いたかどうかはAPI responseではキャッチできません。

メール送信状況はSendGridの[Activityページ](https://app.sendgrid.com/email_activity)で確認できますが、一定期間しか記録が残らず、また通知を受け取ることはできません。
そこで、SendGrid Webhookを構築してみました。


## 実装内容

SendGrid webhookを利用して、送信したメールのその後の状況をキャッチしました。
下記ページの通りに実行します。

https://sendgrid.kke.co.jp/docs/Tutorials/C_Manage_Events/using_event_webhook.html

SendGridのSettingsページ→Mail Settings/Webhook Settingsより新しいEventhookを作成します。
POST先URLとPOST対象イベントを選択します。
URLはCloud Functionsで新しいAPI関数のURLを作成することとしました。
今回は送信失敗メールをキャッチしたいため、対象イベントは**Dropped**と**Bouced**を選択します。

作成すると、対象イベントが発生した場合に設定したエンドポイントURLにリクエストが飛んできます。
```
method: POST
headers: { host, user-agent, content-length, content-type, x-twilio-email-event-webhook-signature, x-twilio-email-event-webhook-timestamp, x-cloud-trace-context, traceparent, x-forwarded-for, x-forwarded-proto, forwarded, accept-encoding }
body: {[{email, event, reason, sg_event_id, sg_message_id, smtp-id, timestamp}]}
```

今回は飛んできたリクエストを元に、Slack APIを用いてチャンネルに投稿するシステムを実装してみました。
```ts
const data = JSON.stringify({
    "channel": channelName,
    "text": text
});

const config = {
method: 'post',
url: 'https://slack.com/api/chat.postMessage',
headers: { 
    'Authorization': `Bearer ${process.env.SLACK_BOT_OAUTH_TOKEN}`, 
    'Content-Type': 'application/json'
},
data : data
};

await axios(config);
```



## デジタル署名の実装

SendGrid WebhookではWebhook用のURLに飛んできたAPIの送信元がSendGridであることを証明できるように、デジタル署名を利用できます。
デジタル署名とは公開鍵と秘密鍵を用いた暗号化技術を応用し、送信元が本人であることを証明する技術です。
デジタル署名についての説明とイメージについては、下記ページがわかりやすいです。

https://zerokara.dds.co.jp/signature/digitalsignature/

今回の場合は

:::note info
**秘密鍵**：SendGridが保有し、通信内容をハッシュ化→暗号化し`x-twilio-email-event-webhook-signature`値として送ります。
**公開鍵**：Webhookの受け手側が保有。`x-twilio-email-event-webhook-signature`値を復号し、通信内容のハッシュ化値と一致していれば送り主がSendGridであることを証明できます。
:::


暗号化のアルゴリズムは楕円曲線DSAというものを利用しています。

実装自体は簡単です。（[参考ページ](https://stackoverflow.com/questions/76215848/sendgrid-webhook-signature-verification-failures-in-node-js)）

```ts
import { EventWebhook } from "@sendgrid/eventwebhook";

const signature = req.headers["x-twilio-email-event-webhook-signature"] as string;
const timestamp = req.headers["x-twilio-email-event-webhook-timestamp"] as string;
const publicKey = process.env.SENDGRID_WEBHOOK_SIGNATURE; //　事前にSendGrid上で入手
const payload = req.rawBody;

const verify = new EventWebhook();
const ecdsaPublicKey = verify.convertPublicKeyToECDSA(publicKey);

const isVerified: boolean = verify.verifySignature(
  ecdsaPublicKey,
  payload,
  signature,
  timestamp
);

// => isVerified値のboolで判断する
```

また、他にもSendGridではOAuthを用いたVerificationも可能です！


## まとめ
SendGrid Webhookを利用することでSendGridでのメール送信失敗をキャッチしてSlack通知を送ることができました！