[インデックス 19288] ファイルの概要
このコミットは、Go言語の標準ライブラリ net/mail
パッケージにおけるメールアドレスのパース処理を改善するものです。具体的には、サポートされていない文字セット(charset)が指定された場合に、そのエラーが適切に呼び出し元に伝播されるように修正しています。また、関連するテストケースも追加されています。
変更されたファイルは以下の通りです。
src/pkg/net/mail/message.go
: メールメッセージのパースロジックが含まれる主要なファイル。decodeRFC2047Word
関数およびconsumePhrase
関数が修正されています。src/pkg/net/mail/message_test.go
:net/mail
パッケージのテストファイル。TestAddressParsingError
という新しいテストケースが追加されています。
コミット
commit 8bc1bfb6bfeb27b64b3654af091227c89cd29300
Author: David Crawshaw <david.crawshaw@zentus.com>
Date: Wed May 7 05:58:36 2014 -0400
net/mail: propagate unsupported charset error
Fixes #6807.
LGTM=iant
R=golang-codereviews, iant
CC=golang-codereviews
https://golang.org/cl/95060043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8bc1bfb6bfeb27b64b3654af091227c89cd29300
元コミット内容
このコミットは、net/mail
パッケージにおいて、サポートされていない文字セットが指定された場合に、そのエラーが適切に伝播されるように修正します。これにより、以前はエラーが無視されたり、不正確なパース結果が返されたりしていた問題が解決されます。
変更の背景
このコミットの背景には、net/mail
パッケージがメールヘッダのデコード処理において、サポートされていない文字セットに遭遇した場合に、そのエラーを適切に処理できていなかったという問題があります。具体的には、RFC 2047でエンコードされたワードをデコードする decodeRFC2047Word
関数において、iso-8859-1
や utf-8
以外の文字セットが指定された場合、エラーが発生してもそれが呼び出し元に伝播されず、空文字列が返されるか、あるいは不正確なパース結果が生成される可能性がありました。
このような挙動は、メールアドレスやヘッダ情報が正しく解釈されない原因となり、アプリケーションの誤動作やセキュリティ上の問題を引き起こす可能性があります。このコミットは、この問題を修正し、サポートされていない文字セットのエラーを明示的に返すことで、呼び出し元が適切なエラーハンドリングを行えるようにすることを目的としています。コミットメッセージにある "Fixes #6807" は、この問題がGoのイシュートラッカーで報告されていたことを示唆しています。
前提知識の解説
RFC 2047: MIME (Multipurpose Internet Mail Extensions) Header Extensions for Non-ASCII Text
RFC 2047は、MIME(Multipurpose Internet Mail Extensions)の一部であり、電子メールのヘッダフィールド(例: Subject, From, To)に非ASCII文字(日本語、中国語、ロシア語など)を含めるためのエンコーディング方法を定義しています。
通常のメールヘッダはASCII文字のみをサポートしているため、非ASCII文字を直接記述することはできません。RFC 2047は、これを解決するために「エンコードされたワード(Encoded-Word)」という形式を導入しました。
エンコードされたワードの一般的な形式は以下の通りです。
=?charset?encoding?encoded-text?=
charset
: 使用されている文字セット(例:iso-2022-jp
,utf-8
,iso-8859-1
)。encoding
: エンコーディング方式。Q
(Quoted-Printable): 比較的読みやすい形式で、ASCII文字以外のバイトを=XX
の形式でエンコードします。B
(Base64): バイナリデータをASCII文字列に変換するエンコーディング。
encoded-text
:encoding
で指定された方式でエンコードされた実際のテキスト。
例: =?UTF-8?B?44GZ44Gk44Go44Gh44Gf?=
(これは「こんにちは」のUTF-8 Base64エンコードです)
net/mail
パッケージの decodeRFC2047Word
関数は、このRFC 2047形式の文字列をデコードする役割を担っています。
net/mail
パッケージ
Go言語の標準ライブラリである net/mail
パッケージは、RFC 5322(インターネットメッセージフォーマット)および関連するRFC(RFC 2047など)に準拠した電子メールメッセージのパースと生成をサポートします。このパッケージは、メールヘッダの解析、アドレスの抽出、メッセージ本文の読み取りなど、メール処理に必要な基本的な機能を提供します。
エラー伝播 (Error Propagation)
プログラミングにおいて、エラー伝播とは、関数やメソッド内で発生したエラーを、その関数を呼び出した上位の関数に伝えるメカニズムを指します。適切にエラーが伝播されない場合、エラーが発生してもプログラムがそれに気づかず、予期せぬ動作をしたり、デバッグが困難になったりする可能性があります。
このコミットでは、net/mail
パッケージがサポートしていない文字セットに遭遇した場合に、以前はエラーが適切に伝播されなかった問題を修正し、明示的にエラーを返すように変更しています。これにより、呼び出し元はエラーを検知し、適切なエラーハンドリング(例: ログ出力、ユーザーへの通知、代替処理の実行)を行うことができるようになります。
技術的詳細
このコミットの主要な変更点は、src/pkg/net/mail/message.go
内の2つの関数、consumePhrase
と decodeRFC2047Word
におけるエラーハンドリングの改善です。
consumePhrase
関数の変更
consumePhrase
関数は、メールアドレスのパース中にフレーズ(例: John Doe
<john.doe@example.com
> の John Doe
部分)を消費する役割を担っています。元のコードでは、フレーズのパース中にエラーが発生し、かつワードが一つも取得できなかった場合にのみ、新しいエラー errors.New("mail: missing word in phrase")
を返していました。しかし、このエラーは元のエラー情報を失っていました。
変更後:
- return "", errors.New("mail: missing word in phrase")
+ return "", fmt.Errorf("mail: missing word in phrase: %v", err)
この変更により、errors.New
の代わりに fmt.Errorf
が使用され、元のエラー err
が新しいエラーメッセージに埋め込まれるようになりました。これにより、consumePhrase
の呼び出し元は、フレーズのパース中に発生した具体的なエラー(例: 文字セットのサポートされていないエラー)をより詳細に把握できるようになります。これは、エラーのデバッグや適切なエラーハンドリングを行う上で非常に重要です。
decodeRFC2047Word
関数の変更
decodeRFC2047Word
関数は、RFC 2047形式でエンコードされたワード(例: =?UTF-8?B?…?=
) をデコードする役割を担っています。この関数は、エンコードされたワードの形式が不正な場合や、サポートされていない文字セット、エンコーディング方式が指定された場合にエラーを返す必要があります。
元のコードでは、以下の3つのケースでエラーメッセージに "mail: "
というプレフィックスが付加されていました。
- RFC 2047形式が不正な場合:
- return "", errors.New("mail: address not RFC 2047 encoded") + return "", errors.New("address not RFC 2047 encoded")
- サポートされていない文字セットの場合(
iso-8859-1
とutf-8
以外):- return "", fmt.Errorf("mail: charset not supported: %q", charset) + return "", fmt.Errorf("charset not supported: %q", charset)
- サポートされていないエンコーディング方式の場合(
b
とq
以外):- return "", fmt.Errorf("mail: RFC 2047 encoding not supported: %q", enc) + return "", fmt.Errorf("RFC 2047 encoding not supported: %q", enc)
これらの変更は、エラーメッセージから冗長な "mail: "
プレフィックスを削除しています。これは、Goのエラーハンドリングの慣習に沿ったものです。通常、エラーメッセージは、エラーが発生したコンテキスト(この場合は net/mail
パッケージ)を暗黙的に示しているため、明示的なプレフィックスは不要とされます。これにより、エラーメッセージがより簡潔になり、プログラムによるエラーの解析が容易になります。
テストケースの追加
src/pkg/net/mail/message_test.go
に TestAddressParsingError
という新しいテストケースが追加されました。
func TestAddressParsingError(t *testing.T) {
const txt = "=?iso-8859-2?Q?Bogl=E1rka_Tak=E1cs?= <unknown@gmail.com>"
_, err := ParseAddress(txt)
if err == nil || !strings.Contains(err.Error(), "charset not supported") {
t.Errorf(`mail.ParseAddress(%q) err: %q, want ".*charset not supported.*"`, txt, err)
}
}
このテストケースは、iso-8859-2
という net/mail
パッケージがサポートしていない文字セットを含むメールアドレス文字列 txt
を ParseAddress
関数に渡しています。そして、返されたエラーが nil
でないこと、およびエラーメッセージに "charset not supported"
という文字列が含まれていることを検証しています。
このテストの追加により、サポートされていない文字セットのエラーが正しく伝播されることが保証され、将来の回帰を防ぐことができます。
コアとなるコードの変更箇所
diff --git a/src/pkg/net/mail/message.go b/src/pkg/net/mail/message.go
index 4b332c1b5b..ba0778caa7 100644
--- a/src/pkg/net/mail/message.go
+++ b/src/pkg/net/mail/message.go
@@ -363,7 +363,7 @@ func (p *addrParser) consumePhrase() (phrase string, err error) {
// Ignore any error if we got at least one word.
if err != nil && len(words) == 0 {
debug.Printf("consumePhrase: hit err: %v", err)
- return "", errors.New("mail: missing word in phrase")
+ return "", fmt.Errorf("mail: missing word in phrase: %v", err)
}
phrase = strings.Join(words, " ")
return phrase, nil
@@ -442,11 +442,11 @@ func (p *addrParser) len() int {
func decodeRFC2047Word(s string) (string, error) {\n \tfields := strings.Split(s, \"?\")\n \tif len(fields) != 5 || fields[0] != \"=\" || fields[4] != \"=\" {\n-\t\treturn \"\", errors.New(\"mail: address not RFC 2047 encoded\")\n+\t\treturn \"\", errors.New(\"address not RFC 2047 encoded\")\n \t}\n \tcharset, enc := strings.ToLower(fields[1]), strings.ToLower(fields[2])\n \tif charset != \"iso-8859-1\" && charset != \"utf-8\" {\n-\t\treturn \"\", fmt.Errorf(\"mail: charset not supported: %q\", charset)\n+\t\treturn \"\", fmt.Errorf(\"charset not supported: %q\", charset)\n \t}\n \n \tin := bytes.NewBufferString(fields[3])\n@@ -457,7 +457,7 @@ func (p *addrParser) len() int {\n \tcase \"q\":\n \t\tr = qDecoder{r: in}\n \tdefault:\n-\t\treturn \"\", fmt.Errorf(\"mail: RFC 2047 encoding not supported: %q\", enc)\n+\t\treturn \"\", fmt.Errorf(\"RFC 2047 encoding not supported: %q\", enc)\n \t}\n \n \tdec, err := ioutil.ReadAll(r)\ndiff --git a/src/pkg/net/mail/message_test.go b/src/pkg/net/mail/message_test.go
index 1bb4e8bc40..eb9c8cbdc9 100644
--- a/src/pkg/net/mail/message_test.go
+++ b/src/pkg/net/mail/message_test.go
@@ -8,6 +8,7 @@ import (\n \t\"bytes\"\n \t\"io/ioutil\"\n \t\"reflect\"\n+\t\"strings\"\n \t\"testing\"\n \t\"time\"\n )\n@@ -116,6 +117,14 @@ func TestDateParsing(t *testing.T) {\n \t}\n }\n \n+func TestAddressParsingError(t *testing.T) {\n+\tconst txt = \"=?iso-8859-2?Q?Bogl=E1rka_Tak=E1cs?= <unknown@gmail.com>\"\n+\t_, err := ParseAddress(txt)\n+\tif err == nil || !strings.Contains(err.Error(), \"charset not supported\") {\n+\t\tt.Errorf(`mail.ParseAddress(%q) err: %q, want \".*charset not supported.*\"`, txt, err)\n+\t}\n+}\n+\n func TestAddressParsing(t *testing.T) {\n \ttests := []struct {\n \t\taddrsStr string\n```
## コアとなるコードの解説
### `src/pkg/net/mail/message.go` の変更点
* **`consumePhrase` 関数:**
* `- return "", errors.New("mail: missing word in phrase")`
* `+ return "", fmt.Errorf("mail: missing word in phrase: %v", err)`
* この変更は、`consumePhrase` 関数内でエラーが発生し、かつワードが一つもパースできなかった場合に、新しいエラーを生成する際に元のエラー `err` の情報を失わないようにするためのものです。`errors.New` は固定のエラーメッセージしか返しませんが、`fmt.Errorf` を使用することで、元のエラーの詳細を新しいエラーメッセージに含めることができます。これにより、デバッグ時にエラーの原因を特定しやすくなります。
* **`decodeRFC2047Word` 関数:**
* `- return "", errors.New("mail: address not RFC 2047 encoded")`
* `+ return "", errors.New("address not RFC 2047 encoded")`
* RFC 2047形式のエンコードされたワードの形式が不正な場合に返されるエラーメッセージから、冗長な `"mail: "` プレフィックスが削除されました。
* `- return "", fmt.Errorf("mail: charset not supported: %q", charset)`
* `+ return "", fmt.Errorf("charset not supported: %q", charset)`
* サポートされていない文字セットが指定された場合に返されるエラーメッセージから、冗長な `"mail: "` プレフィックスが削除されました。
* `- return "", fmt.Errorf("mail: RFC 2047 encoding not supported: %q", enc)`
* `+ return "", fmt.Errorf("RFC 2047 encoding not supported: %q", enc)`
* サポートされていないエンコーディング方式が指定された場合に返されるエラーメッセージから、冗長な `"mail: "` プレフィックスが削除されました。
これらの変更は、Goのエラーメッセージの慣習に沿ったものであり、エラーメッセージをより簡潔にし、プログラムによるエラーの解析を容易にすることを目的としています。
### `src/pkg/net/mail/message_test.go` の変更点
* `+ "strings"`
* 新しく追加されたテストケース `TestAddressParsingError` で `strings.Contains` 関数を使用するために、`strings` パッケージがインポートされました。
* `+func TestAddressParsingError(t *testing.T) { ... }`
* この新しいテスト関数は、`net/mail` パッケージがサポートしていない文字セット(この場合は `iso-8859-2`)を含むメールアドレス文字列を `ParseAddress` 関数に渡した場合に、期待通りにエラーが返されることを検証します。
* `const txt = "=?iso-8859-2?Q?Bogl=E1rka_Tak=E1cs?= <unknown@gmail.com>"`: サポートされていない文字セット `iso-8859-2` を含むRFC 2047エンコードされたメールアドレス文字列を定義しています。
* `_, err := ParseAddress(txt)`: 定義した文字列を `ParseAddress` 関数でパースし、返されるエラーを取得します。
* `if err == nil || !strings.Contains(err.Error(), "charset not supported") { ... }`:
* `err == nil`: エラーが返されなかった場合(つまり、`nil` の場合)をチェックします。エラーが返されるべきなので、`nil` であればテストは失敗します。
* `!strings.Contains(err.Error(), "charset not supported")`: 返されたエラーメッセージに `"charset not supported"` という文字列が含まれていない場合をチェックします。この文字列が含まれていなければ、期待するエラーが返されていないため、テストは失敗します。
* `t.Errorf(...)`: 上記の条件のいずれかが真であった場合、テストは失敗し、詳細なエラーメッセージが出力されます。
このテストの追加により、`net/mail` パッケージがサポートしていない文字セットのエラーを適切に伝播するようになったことが保証されます。
## 関連リンク
* GitHubコミットページ: [https://github.com/golang/go/commit/8bc1bfb6bfeb27b64b3654af091227c89cd29300](https://github.com/golang/go/commit/8bc1bfb6bfeb27b64b3654af091227c89cd29300)
* Go Change List: [https://golang.org/cl/95060043](https://golang.org/cl/95060043)
## 参考にした情報源リンク
* RFC 2047: MIME (Multipurpose Internet Mail Extensions) Part Three: Message Header Extensions for Non-ASCII Text (https://datatracker.ietf.org/doc/html/rfc2047)
* Go言語 `net/mail` パッケージのドキュメント (https://pkg.go.dev/net/mail)
* Go言語のエラーハンドリングに関する一般的な慣習 (Goの公式ドキュメントやブログ記事など)
* Go issue #6807については、直接的な公式イシュートラッカーのリンクを見つけることができませんでした。これは、非常に古いイシューであるか、あるいは内部的なトラッキング番号である可能性があります。