Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 18420] ファイルの概要

このコミットは、Go言語の net/mail パッケージにおいて、メールアドレスのフォーマット時に名前に含まれる空白文字(ホワイトスペース)が正しく処理されないバグを修正するものです。具体的には、RFC 5322で許可されている quoted-string 内の空白文字が、不適切にエンコードされたり、エラーとして扱われたりする問題を解決します。

コミット

  • コミットハッシュ: d3b9567a15cd0f20a927c87b8172902717020304
  • Author: Jakub Ryszard Czarnowicz j.czarnowicz@gmail.com
  • Date: Fri Feb 7 10:49:10 2014 +1100

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/d3b9567a15cd0f20a927c87b8172902717020304

元コミット内容

net/mail: correctly handle whitespaces when formatting an email address

Whitespace characters are allowed in quoted-string according to RFC 5322 without
being "Q"-encoding. Address.String() already always formats the name portion in
quoted string, so whitespace characters should be allowed in there.

Fixes #6641.

LGTM=dave, dsymonds
R=golang-codereviews, gobot, dsymonds, dave
CC=golang-codereviews
https://golang.org/cl/55770043

変更の背景

この変更は、Go言語の net/mail パッケージがメールアドレスの表示名(Address.Name)をフォーマットする際に、RFC 5322で定義されている quoted-string の仕様を完全に遵守していなかったことに起因します。

RFC 5322(Internet Message Format)では、メールアドレスの表示名に特殊文字や空白文字が含まれる場合、その表示名を二重引用符で囲んだ quoted-string 形式で表現することが許可されています。この quoted-string の内部では、特定の文字(バックスラッシュ \ や二重引用符 ")を除いて、空白文字(スペースやタブ)はエスケープなしでそのまま使用できると規定されています。

しかし、net/mail パッケージの Address.String() メソッドの実装では、quoted-string 内の空白文字を Q-エンコーディング(MIME "Q" Quoted-Printable Encoding)の対象として誤って扱ったり、あるいは isVchar(可視文字)や isQtext(quoted-stringテキスト文字)のチェックで弾いてしまったりする問題がありました。これにより、例えば Name: "Bob Jane" のような表示名を持つメールアドレスが、"Bob=20Jane" のように不適切にエンコードされたり、あるいはフォーマット時に予期せぬ挙動を引き起こしたりしていました。

この問題は、GoのIssue #6641として報告されており、このコミットはその問題を解決するために導入されました。既存の Address.String() が常に名前部分を quoted-string としてフォーマットしているため、その内部での空白文字の扱いをRFC 5322に準拠させる必要がありました。

前提知識の解説

RFC 5322 (Internet Message Format)

RFC 5322は、インターネットメッセージ(主に電子メール)の標準フォーマットを定義する仕様です。このRFCは、メッセージヘッダーの構文、日付と時刻のフォーマット、アドレスの構文など、電子メールの構造に関する詳細を規定しています。

アドレスの構文と quoted-string

RFC 5322では、メールアドレスは通常 display-name <local-part@domain> の形式で表現されます。ここで display-name はオプションの表示名です。

display-name には、空白文字や一部の特殊文字が含まれる場合、二重引用符で囲まれた quoted-string 形式を使用することができます。

quoted-string の定義は以下のようになります(簡略化): quoted-string = DQUOTE *([FWS] qcontent) [FWS] DQUOTE qcontent = qtext / quoted-pair qtext = %d33 / %d35-91 / %d93-126 (printable US-ASCII characters except DQUOTE and backslash) quoted-pair = "\" VCHAR / WSP

重要な点は、qtext には二重引用符(")とバックスラッシュ(\)以外の可視ASCII文字が含まれ、これらの文字は quoted-string 内でそのまま使用できるということです。また、quoted-pair の定義により、バックスラッシュでエスケープされた可視文字(VCHAR)や空白文字(WSP)も quoted-string 内で許可されます。

特に、WSP (Whitespace) はスペース( )と水平タブ(\t)を指し、これらは quoted-string 内でエスケープなしで直接使用できる文字です(ただし、quoted-pair の文脈ではエスケープされることもあります)。このコミットの焦点は、この「エスケープなしで空白文字が許可される」という点にあります。

Go言語の net/mail パッケージ

net/mail パッケージは、Go言語の標準ライブラリの一部であり、RFC 5322に準拠した電子メールメッセージの解析とフォーマットを提供します。このパッケージは、メールヘッダーの解析、アドレスの抽出、メッセージのエンコード/デコードなどの機能を提供します。

  • mail.Address 構造体: メールアドレスを表す構造体で、Name (表示名) と Address (メールアドレス本体) のフィールドを持ちます。
  • Address.String() メソッド: mail.Address 構造体をRFC 5322に準拠した文字列形式にフォーマットするメソッドです。このメソッドが、表示名に空白文字が含まれる場合の quoted-string の生成を担当します。

isVcharisQtext

コミットの変更箇所に登場する isVcharisQtext は、文字の種類を判定するためのヘルパー関数です。

  • isVchar(c byte) bool: VCHAR (Visible Character) を判定します。RFC 5234 (ABNF) で定義されており、印刷可能なUS-ASCII文字(! から ~ まで)を指します。
  • isQtext(c byte) bool: qtext (quoted-string text) を判定します。RFC 5322で定義されており、quoted-string 内でエスケープなしで直接使用できる文字のうち、二重引用符(")とバックスラッシュ(\)を除く可視文字を指します。

技術的詳細

このバグは、net/mail パッケージの Address.String() メソッドが、表示名(a.Name)を quoted-string としてフォーマットする際に、空白文字の扱いを誤っていたことにあります。

元のコードでは、a.Name の各文字が allPrintable かどうかをチェックするループ内で、isVchar(a.Name[i]) のみを確認していました。isVchar は可視文字のみを真とするため、空白文字(スペースやタブ)は allPrintablefalse にしてしまい、結果として quoted-string の内部で空白文字が適切に扱われない可能性がありました。

また、quoted-string の内容を構築するループ内では、!isQtext(a.Name[i]) の条件でバックスラッシュ(\)を挿入していました。isQtext も空白文字を真としないため、空白文字が isQtext の条件に合致せず、不必要にバックスラッシュでエスケープされてしまう可能性がありました。RFC 5322では、quoted-string 内の空白文字は通常エスケープ不要です。

このコミットでは、以下の2つの主要な変更によってこの問題を解決しています。

  1. allPrintable の判定ロジックの修正: allPrintable フラグを判定する際に、isVchar(a.Name[i]) に加えて isWSP(a.Name[i])(空白文字であるか)もチェックするように変更されました。これにより、表示名に空白文字が含まれていても allPrintabletrue になる可能性が広がり、quoted-string としての適切な処理パスに進むようになります。 コメント // isWSP here should actually be isFWS, // but we don't support folding yet. は、RFC 5322の FWS (Folding White Space) の概念に触れていますが、現在の実装では単純な WSP のチェックで十分であり、将来的な FWS のサポートを示唆しています。

  2. quoted-string 内のエスケープロジックの修正: quoted-string の内容を構築する際に、!isQtext(a.Name[i]) の条件に加えて !isWSP(a.Name[i]) もチェックするように変更されました。これにより、文字が qtext でない、かつ WSP でもない場合にのみバックスラッシュでエスケープされるようになります。つまり、空白文字は isQtext には該当しないものの、isWSP に該当するため、エスケープされずにそのまま quoted-string に挿入されるようになります。これはRFC 5322の quoted-string の仕様に完全に準拠した挙動です。

  3. isWSP ヘルパー関数の追加: スペース( )と水平タブ(\t)が空白文字(WSP)であるかを判定する新しいヘルパー関数 isWSP が追加されました。これにより、コードの可読性が向上し、空白文字の判定ロジックが一箇所に集約されます。

これらの変更により、"Bob Jane" のような表示名が net/mail パッケージによって正しく "Bob Jane" <bob@example.com> の形式でフォーマットされるようになり、不適切なエンコーディングやエスケープが回避されます。

コアとなるコードの変更箇所

src/pkg/net/mail/message.go

--- a/src/pkg/net/mail/message.go
+++ b/src/pkg/net/mail/message.go
@@ -159,7 +159,9 @@ func (a *Address) String() string {
 	// If every character is printable ASCII, quoting is simple.
 	allPrintable := true
 	for i := 0; i < len(a.Name); i++ {
-		if !isVchar(a.Name[i]) {
+		// isWSP here should actually be isFWS,
+		// but we don't support folding yet.
+		if !isVchar(a.Name[i]) && !isWSP(a.Name[i]) {
 			allPrintable = false
 			break
 		}
@@ -167,7 +169,7 @@ func (a *Address) String() string {
 	if allPrintable {
 		b := bytes.NewBufferString(`"`)
 		for i := 0; i < len(a.Name); i++ {
-			if !isQtext(a.Name[i]) {
+			if !isQtext(a.Name[i]) && !isWSP(a.Name[i]) {
 				b.WriteByte('\\')
 			}
 			b.WriteByte(a.Name[i])
@@ -535,3 +537,9 @@ func isVchar(c byte) bool {
 	// Visible (printing) characters.
 	return '!' <= c && c <= '~'
 }
+
+// isWSP returns true if c is a WSP (white space).
+// WSP is a space or horizontal tab (RFC5234 Appendix B).
+func isWSP(c byte) bool {
+	return c == ' ' || c == '\t'
+}

src/pkg/net/mail/message_test.go

--- a/src/pkg/net/mail/message_test.go
+++ b/src/pkg/net/mail/message_test.go
@@ -277,6 +277,14 @@ func TestAddressFormatting(t *testing.T) {
 			&Address{Name: "Böb", Address: "bob@example.com"},
 			`=?utf-8?q?B=C3=B6b?= <bob@example.com>`,
 		},
+		{
+			&Address{Name: "Bob Jane", Address: "bob@example.com"},
+			`"Bob Jane" <bob@example.com>`,
+		},
+		{
+			&Address{Name: "Böb Jacöb", Address: "bob@example.com"},
+			`=?utf-8?q?B=C3=B6b_Jac=C3=B6b?= <bob@example.com>`,
+		},
 	}
 	for _, test := range tests {
 		s := test.addr.String()

コアとなるコードの解説

src/pkg/net/mail/message.go の変更

  1. Address.String() メソッド内の allPrintable 判定ロジックの修正: 変更前: if !isVchar(a.Name[i]) { 変更後: if !isVchar(a.Name[i]) && !isWSP(a.Name[i]) { この変更により、表示名 a.Name の文字が可視文字(isVchar)でない場合でも、それが空白文字(isWSP)であれば allPrintable フラグが false になるのを防ぎます。つまり、空白文字は quoted-string 内で「印刷可能」な文字として扱われるべきであるというRFC 5322の意図を反映しています。これにより、空白文字を含む表示名が正しく quoted-string として処理されるパスに進むようになります。

  2. Address.String() メソッド内の quoted-string エスケープロジックの修正: 変更前: if !isQtext(a.Name[i]) { 変更後: if !isQtext(a.Name[i]) && !isWSP(a.Name[i]) { quoted-string の内容を構築する際、文字が qtext でない場合にバックスラッシュでエスケープするロジックがありました。しかし、空白文字は isQtext では false を返すため、このままでは空白文字が不必要にエスケープされてしまいます。この変更により、「文字が qtext でない、かつ 空白文字でもない」場合にのみエスケープが行われるようになります。これにより、空白文字はエスケープされずにそのまま quoted-string に挿入され、RFC 5322の仕様に準拠します。

  3. isWSP 関数の追加:

    // isWSP returns true if c is a WSP (white space).
    // WSP is a space or horizontal tab (RFC5234 Appendix B).
    func isWSP(c byte) bool {
    	return c == ' ' || c == '\t'
    }
    

    この新しいヘルパー関数は、与えられたバイトがスペース( )または水平タブ(\t)であるかを判定します。これはRFC 5234で定義されている WSP (Whitespace) に対応します。この関数を導入することで、空白文字の判定ロジックが明確になり、コードの可読性と保守性が向上しています。

src/pkg/net/mail/message_test.go の変更

新しいテストケースが TestAddressFormatting 関数に追加されました。

  • {"Bob Jane", "Bob Jane" bob@example.com}: このテストケースは、表示名にスペースが含まれる場合に、それが正しく二重引用符で囲まれ、スペースがそのまま保持されることを確認します。これは、このコミットが修正しようとしている主要なシナリオです。

  • {"Böb Jacöb", =?utf-8?q?B=C3=B6b_Jac=C3=B6b?= bob@example.com}: このテストケースは、表示名に非ASCII文字とスペースが混在する場合の挙動を確認します。この場合、RFC 2047の"Q"エンコーディングが適用され、スペースはアンダースコア(_)に変換されることが期待されます。これは、このコミットの変更が既存のエンコーディングロジックに悪影響を与えないことを保証するためのものです。

これらのテストケースの追加により、修正が正しく機能していること、および他の関連するフォーマットロジックに回帰がないことが検証されます。

関連リンク

参考にした情報源リンク