
この記事は every Tech Blog Advent Calendar 2024(夏) 6 日目の記事です。
目次
- はじめに
- イントロダクション
- そもそもメールヘッダーとは
- net/mail パッケージ
- net/mail パッケージのメール解析で辛いところ
- jhillyerd/enmime パッケージ
- net/mail と jhillyerd/enmime の比較
- まとめ
- 最後に
はじめに
こんにちは!最近推しの配信が多くなってきて嬉しい@きょーです!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 パッケージ
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/plainやtext/htmlの部分が組み合わされ一つのメッセージとなっています。画像や動画を送る場合はimage/png、video/mp4などのパートがメッセージに追加されます。
デコード機能が不十分
net/mailパッケージにはほぼデコードの機能がありません。(ParseAddress 関数を除く)
そのため、日本語で書かれたメールの件名や本文をエンコード方式(base64やquoted-printableなど)に合わせ適切にデコードしなければ文字化けしてしまいます。
また、net/mailパッケージではHeaderではなくBodyの中にMIMEマルチパートメッセージのエンコード方式が書かれています。そのためデコードするために形式を取得したくとも簡単には取得できない、という問題があります。
メールプロトコルに沿わせた構成にするのが大変
メールプロトコルとは
メールを送信する上で意識しなければいけないのがメールプロトコルです。メールプロトコルとは、電子メールの送受信に関する規則や手順を定めたもので、電子メール通信をする上でメールデータが正しくやり取りされるために必要です。
RFC2822でメールプロトコルが規定されています。下記に内容の一部を紹介していきます。
- ASCII コードで構成されること
- 一行は 78 文字以下が推奨
- Header フィールドは、フィールド名の後にコロン(":")、フィールド本体が続き、CRLF で終了
- Body の前は空行にする
これらの規則や手順を守らないと、メール送信できなかったり送信できても文字化けしてしまうなどの問題に繋がります。
net/mail で解析したメールを送信可能なメールにするために
net/mail パッケージでは Message 構造体の中に HeaderとBodyフィールドがあります。メール送信するためにはこれらを組み合わせ[]byte 型にしなければいけなく、具体的には以下のような処理が必要になります。
- 複数の Header のフィールド名と値をセットで取り出し、1 行に 1 セット設定する
- 一行が 78 文字以上にならないように適宜改行コードを入れる
- Header と Body を組み合わせて[]byte に変換
これを自分で対応しようとすると骨の折れる作業になります。実際に行った記事としても以下のような記事がよくまとまっています
上記の記事のコードを手元で管理したくないという思いから、MIME のエンコードやデコードを気にせず、電子メールの生成や解析をしてくれるパッケージを探し始めました。
そこで見つけたのが以下で紹介するパッケージです。
jhillyerd/enmime パッケージ
jhillyerd/enmimeパッケージは MIME エンコードおよびデコードライブラリで、MIME エンコードされた電子メールの生成と解析に重点を置いています。
net/mail パッケージでは Message 構造体のフィールドの Header と Body がそれぞれ分かれていたため、解析 → Header 修正 → MIME 対応 → Header をエンコード → Body と組み合わせる → メール送信可能な構造に修正 → メール送信といった流れでした。
jhillyerd/enmime パッケージでは Header も Body も全て一緒に MIME に対応した解析と生成をするため解析 → Header 修正 → MIME 対応したエンコード → メール送信のように処理が簡易化されます。
実際に例を見てみましょう。
メールヘッダーの設定
// 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/mailとjhillyerd/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