これは意外な落とし穴!? Webサービスに必須のメール送信機能の実装ナレッジ・運用ノウハウ・失敗談などあなたのTipsを投稿してください! by blastengineのカレンダー | Advent Calendar 2023 - Qiita7日目の記事です(大変遅くなりました…)。
DevRelというのは、Developer Relationsの略です。自社や自社サービスと外部の開発者との間に、良好な関係性を築くためのマーケティング手法になります。情報発信だけでなく、傾聴(フィードバックなど)を重視している点が特徴です。最近、ちらほらとDevRelをはじめましたといったブログ記事を見かけることが増えてきています。
そんなDevRelについて、グローバルに行われているカンファレンスがDevRelConです。2015年にロンドンではじまり、現在はアメリカや中国、日本で開催されています。日本(東京や横浜)は、私がハンドリングして開催しています。
2023年は3月にDevRelCon Yokohama 2023と、DevRel/Japan CONFERENCE 2023という2つのカンファレンスを同日開催したのですが、このWebサイトにおけるメール配信・受信周りの実装について紹介します。
アーキテクチャ
基本的なアーキテクチャは以下の通りです。Webサイトは静的サイトとしていて、システム不具合につながるポイントをなくしています。スピーカーやセッション情報など、適宜更新される情報はGoogleスプレッドシートで管理しており、シートをJSONで取得することでデータファイルとして利用しています。
データ取得部分についてはGoogleスプレッドシートへ簡単にREST APIを追加するSheetToRESTの紹介 #GoogleAppsScript - Qiitaを参照してください。Googleスプレッドシートとの連携が便利にできます。
Webサイトが静的なので、動的な部分(お問い合わせやスピーカーへの連絡など)は別な実装が必要です。そこで使っていたのがGoogleスプレッドシートのGAS(Google Apps Script)です。そしてメール部分はSendGridを使っていました。
お問い合わせ
お問い合わせは主に2つの経路から来ます。1つはメール、もう一つはお問い合わせフォームです。
メールでの問い合わせ
メールの問い合わせはSendGridのInboud Parseを使って処理しています。受け取ったメールは、WebhookでGASのURLを呼び出しています。SendGridからはPOSTで呼ばれますが、GASは doPost
関数で受け取ります。また、メールの各情報は以下のコードで分割できます。
function doPost(e) {
const { to, from, subject, text, html, headers } = e.parameter;
// 省略
}
注意点としては、Inbound Parseのスパムフィルターを有効にしても、相当数のスパムが送られてくることです。対策としては、Googleスプレッドシートのメールシート(実際に届いたメールが入るシート)にスパムフラグを立てて、送信元ベースでのスパム判定を行っていました。
お問い合わせフォーム
先ほどのアーキテクチャにはなかったのですが、お問い合わせフォームはニフクラ mobile backendのスクリプト機能を呼び出していました。これは単にFaaSであれば何でも良かったので、Cloud Functions for Firebaseでも問題ありません。
そのFaaSでお問い合わせ内容をメールすれば、メールでの問い合わせと同じフローで処理できます。
メール送信
スピーカーや参加者へメール送信する場面は幾つかあります。
- プロポーザルを受け取った際の確認メール
- プロポーザルが通ったときの連絡メール
- プロポーザルが通ったときの連絡メール(再送)
- プロポーザルが通らなかったときのお祈りメール
- スピーカーへの連絡
- 参加者への連絡
こうしたメールはSendGridを使って送信していました。GASからGmailを呼び出せますが、大量のメール送信はエラーになる(1日当たりの送信数制限に引っかかる)のと、送信元を変更したいためです。
SendGridでのメール送信は、以下のようなコードになります。残念ながらSDKがないので、 UrlFetchApp
を使っています。蛇足ですが、筆者がエバンジェリストをしているblastengineにはGAS SDKがあります。
function sendEmail(to, subject, body_text, body_html = null, from = 'no-reply@devrel.dev', from_name = 'DevRel/Japan CONFERENCE事務局') {
const SEND_GRID_ENDPOINT = 'https://api.sendgrid.com/v3/mail/send';
const property = PropertiesService.getScriptProperties();
const body = {
"personalizations": [
{
"to": [
{
"email": to
}
],
"subject": subject
}
],
"from": {
"email": from,
"name" : from_name
},
"content": [
{
"type": "text/plain",
"value": body_text
},
]
};
if (body_html) {
body.personalizations[0].content.push({
"type": "text/html",
"value": body_html
});
}
const payload = JSON.stringify(body);
const res = UrlFetchApp.fetch(SEND_GRID_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${property.getProperty('SENDGRID_API_KEY')}`},
payload: payload
});
return res.getResponseCode() === 202;
}
APIキーはスクリプトプロパティに保存しておくことで、安全に利用できます。
懸念点
グローバルなカンファレンスの場合、実はFirebaseはお勧めしません。おそらく、Firebaseは中国からアクセスできないからです。Cloudflare Pagesは使えると思うので、次回からはそちらに移行しようかなと思っています。Cloudflare Pagesを使うなら、ついでにEmail Workersを試すのも面白そうです。
Inbound Parseは早いレスポンスが求められるため、GASだとSendGridから3回くらい連続でアクセスが来ます。それをすべて記録していると、メール1通に対して3行書き込まれてしまいます。対策として、メール情報からメールIDを取得し、それがすでにメールシートにあるかどうかで重複処理を防いでいます。
const id = headers
.split("\n")
.find(header => header.toUpperCase().match(/^MESSAGE\-ID:/))
.split(/\s+/)[1]
.replace("<", "")
.replace(">", "");
実際には、上記処理だけではうまく取れない場合もあるので、その場合はメールデータ全体を id
としています。
まとめ
メールの送受信は、24時間365日運用できていて当たり前だと考えられており、不着は大きな問題です。そのため、あまり予算のないカンファレンスサイトでは外部サービスを利用するのが一番楽な方法になります。今回はSendGrid + Firebaseの形で運用しています。
WordPressなどを立てて運用すると、サーバー運用費用などもかかってくきます(カンファレンス後も立ち上げ続ける必要があったり…)。静的サイトであれば、ほぼコストゼロで運用できるのでお勧めです。