こんにちは、技術本部 Strategic Products Engineering Unit Order One Devグループの中塚です。
Order Oneの新機能としてメール連携機能をリリースしました。
受注専用アドレスがOrder Oneユーザに対して発行され、そのアドレスに対して注文メールを送信することで、Order One上で直接メール経由の注文書を受信できるようになりました。 また、注文に関するメールでのやり取りもOrder One上でできるようになっています。
この記事では、メール連携機能でどのようにメールの送受信を実現したかを紹介します。
どうやって実現しているか
Order Oneではリリース当初からシステムメールなどのメール配信機能があり、配信基盤としてSendGridを利用しています。
SendGridにはメール配信だけでなく、Webhook経由でメールの受信を行える機能もあり、それを活用して送受信全て実現しています。
今回は、受信と送信、そして送信結果の永続化 、これらの3つの実現方法を簡単に紹介します。
受信
メールの受信にはInbound Parse Webhookを使っています。
処理の流れは次のようにしています。
- Inbound Parse Webhookから受信メールの内容をCloud Functionsで受け取る
- Cloud Functionsで受信メールの内容をパースする
- パースした内容をリクエストとし、Cloud Tasksに受信メール生成のタスクを積み、非同期処理でOrder Oneに登録する
それぞれ簡単に解説します。
1. Inbound Parse Webhookから受信メールの内容をCloud Functionsで受け取る
Inbound Parse Webhookはメールを受信すると、あらかじめ設定しておいたURLにメールの内容をポストしてくれます。
注意点として指定するURLはPublicなURLにする必要があります。 Order OneではInbound Parse Webhookからリクエストを受け取る機能しか持っていないCloud Functionsを用意し、そのURLを指定しています。
2. Cloud Functionsで受信メールの内容をパースする
受信メールの内容をパースする処理はSendGridのライブラリを使うことで簡単にパースできます。
実装のサンプルです。
package main import ( "fmt" "log" "net/http" "github.com/sendgrid/sendgrid-go/helpers/inbound" ) func main() { http.HandleFunc("/inbound", inboundHandler) if err := http.ListenAndServe(":8000", nil); err != nil { log.Fatal(err) } } func inboundHandler(response http.ResponseWriter, request *http.Request) { // メールの内容をパース parsedEmail, err := inbound.ParseWithAttachments(request) if err != nil { log.Fatal(err) } // パースしたメールの内容をアプリケーションの要件に合わせて処理していく fmt.Print(parsedEmail.Envelope.From) fmt.Print(parsedEmail.Envelope.To) fmt.Print(parsedEmail.TextBody) response.WriteHeader(http.StatusOK) }
3. パースした内容をリクエストとし、Cloud Tasksに受信メール生成のタスクを積み、非同期処理でOrder Oneに登録する
受信メールをOrder Oneに登録するため、パースした内容をリクエストとするCloud Tasksのタスクを積み、後続の処理へとつないでいきます。
送信
送信に関してSendGridのドキュメント以上に特筆すべき点はないため詳しい説明は省きます。 選択肢としてはSMTPでメールを送信する方法とSendGridのWeb APIを利用する2つがあります。
Order Oneではベンダーロックインを避けるため、SMTPでメールを送信する選択をしています。
SMTPで送信する場合の注意点としては、Content-Typeをアプリケーションで適切に設定する必要があることです。 特にファイル添付をする場合などは複雑になるため、わかりやすく説明されている次の記事などを参考にすると良いと思います。
送信結果の永続化
SengGridで送信したメールの送信結果はSendGridのWeb上などで確認できますが、直近の結果しか残らないため、プロダクト上で表示したい場合や調査等で必要な場合は永続化しておく必要があります。
そのため、Order OneではEvent Webhookを使い、送信結果をWebhook経由で取得し、永続化しています。
処理の流れは次のようになっています。
- Event Webhookから送信結果の内容をCloud Functionsで受け取る
- Cloud Functionsで認証の確認をする
- Cloud Tasksに送信結果生成のタスクを積み、非同期処理でOrder Oneに登録する
それぞれ簡単に解説します。
1. Event Webhookから送信結果の内容をCloud Functionsで受け取る
Event WebhookからのリクエストもInbound Parse Webhookと同様にリクエストを受け取る専用のCloud Functionsを準備しています。
2. Cloud Functionsで認証の確認をする
Inbound Parse Webhookには認証機能がありませんが、Event Webhookは公開鍵暗号やOAuthを使った認証を利用できるため、それらを利用しています。
認証の確認はSendGridのライブラリを使うことで簡単に行うことができます。
実装のサンプルです。
package main import ( "log" "net/http" "github.com/sendgrid/sendgrid-go/helpers/eventwebhook" ) func main() { http.HandleFunc("/inbound", inboundHandler) if err := http.ListenAndServe(":8000", nil); err != nil { log.Fatal(err) } } func inboundHandler(response http.ResponseWriter, request *http.Request) { requestBody, err := eventwebhook.GetRequestBody(eventwebhook.NewSettings()) if err != nil { log.Fatal(err) } // 実際には環境変数等からbase64PublicKeyを取得して利用する pk, err := eventwebhook.ConvertPublicKeyBase64ToECDSA("base64PublicKey") if err != nil { log.Fatal(err) } signature := request.Header.Get("X-Twilio-Email-Event-Webhook-Signature") timestamp := request.Header.Get("X-Twilio-Email-Event-Webhook-Timestamp") authResult, err := eventwebhook.VerifySignature(pk, requestBody, signature, timestamp) if err != nil { log.Fatal(err) } if !authResult { log.Fatal(err) } // 認証成功後の処理を以降に記述していく response.WriteHeader(http.StatusOK) }
3. Cloud Tasksに送信結果生成のタスクを積み、非同期処理でOrder Oneに登録する
パースした内容で送信結果をOrder Oneに登録するため、Cloud Tasksのタスクを積み、後続の処理へとつないでいきます。
まとめ
メールの配信だけでなく、Inbound Parse Webhook、Event Webhookを活用することでSendGridだけでメールの送受信を実現できます。
今回はインフラ寄りの話でしたが、プロダクトでメールの送受信機能を実現するには、メールの仕様についても深く理解し、アプリケーションロジックに落とし込んでいく必要があります。
- メールにはどんな項目があり、それぞれどんな意味があるのか?
- 必須項目は何か?どんな値が許容されるのか?
- メールの繋がりはどうやって表現されているのか?
などなど。
実際に運用していくと、アプリケーションロジックでの困りどころやハマりどころにも多く遭遇したので、そういった知見もまた別の記事で紹介したいと思います。