TalentX Tech Blog

Tech Blog

Sendgridを利用した返信をGmailでスレッド化する方法

MyTalentという採用MAサービスの開発を担当している、バックエンドエンジニアの樋口です。

MyTalentではSendGridを利用して候補者と人事がメールのやり取りを行う機能があります。その中で、システム経由の返信がGmail上でスレッド表示されないという問題が発生しました。

この記事では、その問題を解決するために行ったメールヘッダーの設定方法を、Go言語の実装例を交えて紹介します。


Gmailがメールをスレッドとしてまとめる基準

そもそも、Gmailはどのような基準でメールを一つのスレッドとしてまとめているのでしょうか?

Gmailのスレッド化基準は「Gmail のスレッド表示におけるスレッド化に関する変更」にて触れられていますが、実際に試したところ下記2つの条件を満たすことでスレッド表示になることが確認できました。

  1. 同じ件名 (Subject)であること
    • Re:Fwd: などの接頭辞は無視されます。
  2. ReferencesIn-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-IDReferences)を取り出します。

Message-IDReferencesは、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でメールを送信する

  1. 元のメールと同じ件名を設定
    • Re:Fwd: などの接頭辞は無視されます。
  2. In-Reply-To: 元のメールの Message-ID を設定
  3. 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-ToReferences メールヘッダーを正しく設定することで解決でき、コミュニケーションをよりスムーズにすることができます。

TalentXでは一緒に働く仲間を募集しており、カジュアル面談も行っていますので気になる方はぜひご応募いただければ幸いです!

i-myrefer.jp