MyTalentという採用MAサービスの開発を担当している、バックエンドエンジニアの樋口です。
MyTalentではSendGridを利用して候補者と人事がメールのやり取りを行う機能があります。その中で、システム経由の返信がGmail上でスレッド表示されないという問題が発生しました。
この記事では、その問題を解決するために行ったメールヘッダーの設定方法を、Go言語の実装例を交えて紹介します。
Gmailがメールをスレッドとしてまとめる基準
そもそも、Gmailはどのような基準でメールを一つのスレッドとしてまとめているのでしょうか?
Gmailのスレッド化基準は「Gmail のスレッド表示におけるスレッド化に関する変更」にて触れられていますが、実際に試したところ下記2つの条件を満たすことでスレッド表示になることが確認できました。
- 同じ件名 (Subject)であること
Re:やFwd:などの接頭辞は無視されます。
ReferencesとIn-Reply-Toヘッダーに、元のメールのMessage-IDが含まれていること
今回はSendGridのInbound Parse Webhookで受け取ったメール情報からMessage-ID, Referencesを取得する方法と、送信時にReferencesとIn-Reply-Toヘッダーを設定する方法を紹介します。
なお、Message-ID, In-Reply-To, Referencesの仕様はRFC 5322にて定められています。
SendGridでの実装方法 (Golang)
それでは、実際にGo言語で実装する方法を見ていきましょう。 Inbound Parse Webhookで必要な情報を取得し、それを返信メールのヘッダーに設定して送信する流れになります。
1. Inbound Parse Webhookで必要な情報を取得する
SendGridからのWebhookリクエストは multipart/form-data 形式でPOSTされます。リクエストから、スレッド化に必要なヘッダー情報(Message-IDとReferences)を取り出します。
Message-IDとReferencesは、POSTされるheadersというフィールドに含まれています。
package main import ( "fmt" "log" "net/http" "os" "strings" "github.com/labstack/echo/v4" ) type postParam struct { Headers string `form:"headers"` } func main() { e := echo.New() e.POST("/inbound", inboundHandler) port := os.Getenv("PORT") if port == "" { port = "8080" } log.Printf("listening on :%s", port) e.Logger.Fatal(e.Start(":" + port)) } func inboundHandler(c echo.Context) error { var p postParam if err := c.Bind(&p); err != nil { return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("bind error: %w", err)) } var headers []string scanner := bufio.NewScanner(strings.NewReader(param.Headers)) for scanner.Scan() { headers = append(headers, scanner.Text()) } var references, messageID string for i := range headers { headerParam := strings.Split(headers[i], ":") switch headerParam[0] { case "References": references = strings.TrimSpace(strings.Join(headerParam[1:], ":")) case "Message-ID": messageID = strings.TrimSpace(strings.Join(headerParam[1:], ":")) } } log.Printf("message-id=%q references=%q", messageID, references) return c.NoContent(http.StatusOK) }
2. GmailにてスレッドになるようにSendGridでメールを送信する
元のメールと同じ件名を設定Re:やFwd:などの接頭辞は無視されます。
In-Reply-To: 元のメールのMessage-IDを設定References:- 元のメールに
Referencesヘッダーが存在する場合、その末尾にスペース区切りで元のメールのMessage-IDを追加 - 元のメールに
Referencesヘッダーが存在しない場合、元のメールのMessage-IDをそのまま設定
- 元のメールに
package main import ( "fmt" "log" "github.com/sendgrid/sendgrid-go" "github.com/sendgrid/sendgrid-go/helpers/mail" ) func main() { message := mail.NewV3Mail() message.SetFrom(&mail.Email{ Name: <送信元名>, Address: <送信元メールアドレス>, }) message.Subject = <元のメールと同じ件名> message.AddContent(mail.NewContent("text/plain", <メール本文>)) message.AddContent(mail.NewContent("text/html", <メール本文>)) p := mail.NewPersonalization() p.AddTos(&mail.Email{ Name: <送信先名>, Address: <送信先メールアドレス>, }) p.SetHeader("In-Reply-To", <元のメールのMessageID>) references = func() string { refs := <元のメールのReferences> if refs == "" { return <元のメールのMessageID> } return fmt.Sprintf("%s %s", refs, <元のメールのMessageID>) }() p.SetHeader("References",references) message.AddPersonalizations(p) client := sendgrid.NewSendClient(<SendGridのAPIキー>) _, err := client.Send(message) if err != nil { log.Print(err) return } return }
まとめ
SendGridのInbound Parse Webhookを利用したシステムからの返信がGmailでスレッド化されない問題は、In-Reply-To と References メールヘッダーを正しく設定することで解決でき、コミュニケーションをよりスムーズにすることができます。
TalentXでは一緒に働く仲間を募集しており、カジュアル面談も行っていますので気になる方はぜひご応募いただければ幸いです!