この記事は 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