every Tech Blog

株式会社エブリーのTech Blogです。

Go 言語で行うメール解析

この記事は every Tech Blog Advent Calendar 2024(夏) 6 日目の記事です。

目次

はじめに

こんにちは!最近推しの配信が多くなってきて嬉しい@きょーです!DELISH KITCHEN 開発部のバックエンド中心で業務をしています。

業務でメール内容を解析、処理する機会があり、そこで経験した学びについて話していこうと思います。

イントロダクション

業務中にメールを解析メールヘッダーのカスタマイズメールの送信をするという場面に出くわしましたが、Go の標準パッケージである net/mail では解決が難しいことがわかり、苦労した経験があります。この記事では net/mail の基本的な使い方や遭遇した辛いところを紹介し、その辛さを解決してくれるパッケージ jhillyerd/enmimeについてお話しようと思います。

そもそもメールヘッダーとは

メールヘッダとは、メールの詳細情報が書かれている部分のことです。具体的には、メールが配送された経路や時間、経由したサーバーなどが記録されています。

以下は、一般的なメールヘッダーの例とその説明です。

From: 送信者のメールアドレスが記載されています。
To: 主な受信者のメールアドレスが記載されています。
Subject: メールの件名が記載されています。
Received: メールが経由したサーバーとその日時が記載されています。これはメールの配送経路を追跡するのに使われます。
Content-Type: メールの本文の形式(例:text/plain, text/html)が記載されています。
MIME-Version: メールが MIME(Multipurpose Internet Mail Extensions)規格を使用している場合、そのバージョンが記載されています。

メールヘッダーは、メールのトラブルシューティング、スパムの検出、セキュリティ分析などに使用されます。たとえば、Received ヘッダーを調べることで、メールがどのサーバーを経由してきたかを追跡し、スパムやフィッシングメールの出所を特定することができます。

net/mail パッケージ

pkg.go.dev

net/mail パッケージは、メールメッセージを解析するための機能を提供します。このパッケージを使用すると、メールのヘッダー情報やアドレスの解析、メッセージの本文の取得などが行えます。

net/mail パッケージの基本的な使用方法について紹介していきます。

メールの解析

net/mail パッケージを使用してメールを解析するには、まず mail.ReadMessage 関数を使用してメールデータを読み込みます。

   // メールのサンプルデータ
      rawEmail := `From: sender@example.com
To: recipient@example.com
Subject: This is a test email
Content-Type: text/plain; charset="utf-8"

This is the body of the email.` // ←がBody部分

    // io.Readerの作成
    reader := strings.NewReader(rawEmail)

    // ReadMessageを使用してメールを解析
    msg, _ := mail.ReadMessage(reader)

ヘッダーの取得

メールのヘッダーは Header 型で表され、これは下記のような map[string][]string の型定義です。

type Message struct {
    Header Header
    Body   io.Reader
}

type Header map[string][]string

ヘッダーの値は Header.Get(key)メソッドを使用して取得できます。このメソッドは指定されたキーに対応する最初の値を返します。

    // ヘッダーの取得
    header := msg.Header

    // Fromヘッダーの取得
    from := header.Get("From")
    fmt.Println("From:", from)  // From: sender@example.com

Body の取得

以下のようにMessage構造体の中にあるBodyからメールの本文を取得できます。

    // 本文の取得
    bytes, _ := io.ReadAll(msg.Body)
    fmt.Printf("Body: %s", string(bytes)) // Body: This is the body of the email.

net/mail パッケージのメール解析で辛いところ

net/mail パッケージは、基本的なメールメッセージの解析機能を提供しますが、いくつかの辛みポイントがあります。以下にその主な点を挙げます。

MIME マルチパートメッセージの解析が不完全

net/mail パッケージは MIME マルチパートメッセージの解析を直接サポートしていません。Message 構造体の Body フィールドには、メールの本文が含まれますが、MIME マルチパートメッセージの場合、下記のコードのような boundary 文字列(--000000000000abcdefg12345)や各パートのヘッダーなどがそのまま含まれてしまいます。これにより、メールの本文だけを簡単に取得することができない、という問題が生じます。

--000000000000abcdefg12345
Content-Type: text/plain; charset="UTF-8"
Content-Transfer-Encoding: base64

44GT44KT44Gr44Gh44Gv
--000000000000abcdefg12345
Content-Type: text/html; charset="UTF-8"
Content-Transfer-Encoding: base64

PGRpdiBkaXI9ImF1dG8iPuOBk+OCk+OBq+OBoeOBrzwvZGl2Pg==
--000000000000abcdefg12345--

MIME マルチパートメッセージとは

MIME マルチパートメッセージテキストhtml画像などそれぞれ異なるパートに分け、それらを組み合わせ構成されたものです。この仕組みは複数のファイルを電子メールに添付するときなどに使用されます。

上記のメールの本文では、text/plaintext/htmlの部分が組み合わされ一つのメッセージとなっています。画像や動画を送る場合はimage/pngvideo/mp4などのパートがメッセージに追加されます。

developer.mozilla.org

デコード機能が不十分

net/mailパッケージにはほぼデコードの機能がありません。(ParseAddress 関数を除く) そのため、日本語で書かれたメールの件名本文エンコード方式(base64quoted-printableなど)に合わせ適切にデコードしなければ文字化けしてしまいます。

また、net/mailパッケージではHeaderではなくBodyの中にMIMEマルチパートメッセージエンコード方式が書かれています。そのためデコードするために形式を取得したくとも簡単には取得できない、という問題があります。

メールプロトコルに沿わせた構成にするのが大変

メールプロトコルとは

メールを送信する上で意識しなければいけないのがメールプロトコルです。メールプロトコルとは、電子メールの送受信に関する規則や手順を定めたもので、電子メール通信をする上でメールデータが正しくやり取りされるために必要です。

RFC2822でメールプロトコルが規定されています。下記に内容の一部を紹介していきます。

  • ASCII コードで構成されること
  • 一行は 78 文字以下が推奨
  • Header フィールドは、フィールド名の後にコロン(":")、フィールド本体が続き、CRLF で終了
  • Body の前は空行にする

これらの規則や手順を守らないと、メール送信できなかったり送信できても文字化けしてしまうなどの問題に繋がります。

net/mail で解析したメールを送信可能なメールにするために

net/mail パッケージでは Message 構造体の中に HeaderBodyフィールドがあります。メール送信するためにはこれらを組み合わせ[]byte 型にしなければいけなく、具体的には以下のような処理が必要になります。

  • 複数の Header のフィールド名と値をセットで取り出し、1 行に 1 セット設定する
  • 一行が 78 文字以上にならないように適宜改行コードを入れる
  • Header と Body を組み合わせて[]byte に変換

これを自分で対応しようとすると骨の折れる作業になります。実際に行った記事としても以下のような記事がよくまとまっています

qiita.com

上記の記事のコードを手元で管理したくないという思いから、MIME のエンコードやデコードを気にせず、電子メールの生成や解析をしてくれるパッケージを探し始めました。

そこで見つけたのが以下で紹介するパッケージです。

jhillyerd/enmime パッケージ

jhillyerd/enmimeパッケージは MIME エンコードおよびデコードライブラリで、MIME エンコードされた電子メールの生成と解析に重点を置いています。

net/mail パッケージでは Message 構造体のフィールドの HeaderBody がそれぞれ分かれていたため、解析 → Header 修正 → MIME 対応 → Header をエンコード → Body と組み合わせる → メール送信可能な構造に修正 → メール送信といった流れでした。

jhillyerd/enmime パッケージでは HeaderBody も全て一緒に MIME に対応した解析と生成をするため解析 → Header 修正 → MIME 対応したエンコード → メール送信のように処理が簡易化されます。

実際に例を見てみましょう。

pkg.go.dev

メールヘッダーの設定

    // objはio.Reader型
    // メールの内容を解析
    envelope, err := enmime.ReadEnvelope(obj)

    // Fromヘッダーの上書き
    err = envelope.SetHeader("From", []string{fmt.Sprintf("%s <%s>", senderName, senderEmail)})

    // Toヘッダーの上書き
    err = envelope.SetHeader("Subject", []string{"new subject"})

    buf := &bytes.Buffer{}
    // MIMEに対応したエンコード
    err = envelope.Root.Encode(buf)

    _ := sendEmail(buf.Bytes())

以上を踏まえ、簡単にnet/mailjhillyerd/enmimeのメリット、デメリットについて以下にまとめてみました。

net/mail と jhillyerd/enmime の比較

net/mail

メリット

  • 標準パッケージのため、追加の依存関係を導入しなくて済む
  • 公式が管理しているため、安定してメンテナンスされる
  • API がシンプルで、処理が追いやすい

デメリット

  • MIME マルチパートメッセージやテキストエンコーディングの解析など、複雑なメール処理に必要な高度な機能が不足している

jhillyerd/enmime

メリット

  • MIME マルチパートメッセージの解析、添付ファイルの処理、エンコーディングの変換など、複雑なメール処理に対応している

デメリット

  • 管理しているコミュニティが小さく、メンテナンスが継続されないリスクがある

まとめ

メールヘッダーを取得・設定するだけであれば net/mail パッケージだけで十分だと思いました。MIME マルチパートメッセージの解析・エンコーディングをする必要がある場合は、複雑な処理を管理しなくて済むので jhillyerd/enmime の利用を検討してみても良いかもしれません。

最後に

Go Conference 2024 まで、あと 2 日! gocon.jp

株式会社エブリー は、Platinum Gold スポンサーとして Go Conference 2024 に参加します。 ぜひ、ブースやセッションでお会いしましょう! gocon.jp