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

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

このコミットは、Go言語の mime/multipart パッケージにおいて、quoted-printable 転送エンコーディングを透過的にデコードする機能を追加するものです。これにより、Content-Transfer-Encoding: quoted-printable ヘッダを持つMIMEパートが正しく処理され、その内容が自動的にデコードされるようになります。

コミット

  • コミットハッシュ: d32d1e098a1022e45f4a7afd05c328b72c5df8ee
  • Author: Brad Fitzpatrick bradfitz@golang.org
  • Date: Mon Nov 19 19:50:19 2012 -0800
  • コミットメッセージ: mime/multipart: transparently decode quoted-printable transfer encoding

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

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

元コミット内容

commit d32d1e098a1022e45f4a7afd05c328b72c5df8ee
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Mon Nov 19 19:50:19 2012 -0800

    mime/multipart: transparently decode quoted-printable transfer encoding
    
    Fixes #4411
    
    R=dsymonds
    CC=gobot, golang-dev
    https://golang.org/cl/6854067
---
 src/pkg/mime/multipart/multipart.go            | 22 ++++++\
 src/pkg/mime/multipart/multipart_test.go       | 28 +++++++-\
 src/pkg/mime/multipart/quotedprintable.go      | 92 ++++++++++++++++++++++++++\
 src/pkg/mime/multipart/quotedprintable_test.go | 52 +++++++++++++++
 4 files changed, 192 insertions(+), 2 deletions(-)

diff --git a/src/pkg/mime/multipart/multipart.go b/src/pkg/mime/multipart/multipart.go
index fb07e1a56d..77e969b41b 100644
--- a/src/pkg/mime/multipart/multipart.go
+++ b/src/pkg/mime/multipart/multipart.go
@@ -37,6 +37,11 @@ type Part struct {
 
 	disposition       string
 	dispositionParams map[string]string
+
+	// r is either a reader directly reading from mr, or it's a
+	// wrapper around such a reader, decoding the
+	// Content-Transfer-Encoding
+	r io.Reader
 }
 
 // FormName returns the name parameter if p has a Content-Disposition
@@ -94,6 +99,12 @@ func newPart(mr *Reader) (*Part, error) {
 	if err := bp.populateHeaders(); err != nil {
 		return nil, err
 	}\n+	bp.r = partReader{bp}\n+	const cte = "Content-Transfer-Encoding"\n+	if bp.Header.Get(cte) == "quoted-printable" {\n+		bp.Header.Del(cte)\n+		bp.r = newQuotedPrintableReader(bp.r)\n+	}\n 	return bp, nil
 }
 
 @@ -109,6 +120,17 @@ func (bp *Part) populateHeaders() error {
 // Read reads the body of a part, after its headers and before the
 // next part (if any) begins.\n func (p *Part) Read(d []byte) (n int, err error) {\n+	return p.r.Read(d)\n+}\n+\n+// partReader implements io.Reader by reading raw bytes directly from the\n+// wrapped *Part, without doing any Transfer-Encoding decoding.\n+type partReader struct {\n+\tp *Part\n+}\n+\n+func (pr partReader) Read(d []byte) (n int, err error) {\n+\tp := pr.p\n 	defer func() {\n 		p.bytesRead += n
 	}()
diff --git a/src/pkg/mime/multipart/multipart_test.go b/src/pkg/mime/multipart/multipart_test.go
index cd65e177e8..d662e83405 100644
--- a/src/pkg/mime/multipart/multipart_test.go
+++ b/src/pkg/mime/multipart/multipart_test.go
@@ -339,9 +339,10 @@ func TestLineContinuation(t *testing.T) {
 		if err != nil {
 			t.Fatalf("didn't get a part")
 		}
-		n, err := io.Copy(ioutil.Discard, part)
+		var buf bytes.Buffer
+		n, err := io.Copy(&buf, part)
 		if err != nil {
-			t.Errorf("error reading part: %v", err)
+			t.Errorf("error reading part: %v\nread so far: %q", err, buf.String())
 		}
 		if n <= 0 {
 			t.Errorf("read %d bytes; expected >0", n)
@@ -349,6 +350,29 @@ func TestLineContinuation(t *testing.T) {
 	}
 }
 
+func TestQuotedPrintableEncoding(t *testing.T) {
+	// From http://golang.org/issue/4411
+	body := "--0016e68ee29c5d515f04cedf6733\r\nContent-Type: text/plain; charset=ISO-8859-1\r\nContent-Disposition: form-data; name=text\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\nwords words words words words words words words words words words words wor=\r\nds words words words words words words words words words words words words =\r\nwords words words words words words words words words words words words wor=\r\nds words words words words words words words words words words words words =\r\nwords words words words words words words words words\r\n--0016e68ee29c5d515f04cedf6733\r\nContent-Type: text/plain; charset=ISO-8859-1\r\nContent-Disposition: form-data; name=submit\r\n\r\nSubmit\r\n--0016e68ee29c5d515f04cedf6733--"\n+	r := NewReader(strings.NewReader(body), "0016e68ee29c5d515f04cedf6733")\n+	part, err := r.NextPart()\n+	if err != nil {\n+		t.Fatal(err)\n+	}\n+	if te, ok := part.Header["Content-Transfer-Encoding"]; ok {\n+		t.Errorf("unexpected Content-Transfer-Encoding of %q", te)\n+	}\n+	var buf bytes.Buffer\n+	_, err = io.Copy(&buf, part)\n+	if err != nil {\n+		t.Error(err)\n+	}\n+	got := buf.String()\n+	want := "words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words"\n+	if got != want {\n+		t.Errorf("wrong part value:\n got: %q\nwant: %q", got, want)\n+	}\n+}\n+\n // Test parsing an image attachment from gmail, which previously failed.\n func TestNested(t *testing.T) {\n 	// nested-mime is the body part of a multipart/mixed email
diff --git a/src/pkg/mime/multipart/quotedprintable.go b/src/pkg/mime/multipart/quotedprintable.go
new file mode 100644
index 0000000000..0a60a6ed55
--- /dev/null
+++ b/src/pkg/mime/multipart/quotedprintable.go
@@ -0,0 +1,92 @@
+// Copyright 2012 The Go Authors. All rights reserved.\n+// Use of this source code is governed by a BSD-style\n+// license that can be found in the LICENSE file.\n+\n+// The file define a quoted-printable decoder, as specified in RFC 2045.\n+\n+package multipart\n+\n+import (\n+\t"bufio"\n+\t"bytes"\n+\t"fmt"\n+\t"io"\n+)\n+\n+type qpReader struct {\n+\tbr   *bufio.Reader\n+\trerr error  // last read error\n+\tline []byte // to be consumed before more of br\n+}\n+\n+func newQuotedPrintableReader(r io.Reader) io.Reader {\n+\treturn &qpReader{\n+\t\tbr: bufio.NewReader(r),\n+\t}\n+}\n+\n+func fromHex(b byte) (byte, error) {\n+\tswitch {\n+\tcase b >= '0' && b <= '9':\n+\t\treturn b - '0', nil\n+\tcase b >= 'A' && b <= 'F':\n+\t\treturn b - 'A' + 10, nil\n+\t}\n+\treturn 0, fmt.Errorf("multipart: invalid quoted-printable hex byte 0x%02x", b)\n+}\n+\n+func (q *qpReader) readHexByte(v []byte) (b byte, err error) {\n+\tif len(v) < 2 {\n+\t\treturn 0, io.ErrUnexpectedEOF\n+\t}\n+\tvar hb, lb byte\n+\tif hb, err = fromHex(v[0]); err != nil {\n+\t\treturn 0, err\n+\t}\n+\tif lb, err = fromHex(v[1]); err != nil {\n+\t\treturn 0, err\n+\t}\n+\treturn hb<<4 | lb, nil\n+}\n+\n+func isQPDiscardWhitespace(r rune) bool {\n+\tswitch r {\n+\tcase '\\n', '\\r', ' ', '\\t':\n+\t\treturn true\n+\t}\n+\treturn false\n+}\n+\n+func (q *qpReader) Read(p []byte) (n int, err error) {\n+\tfor len(p) > 0 {\n+\t\tif len(q.line) == 0 {\n+\t\t\tif q.rerr != nil {\n+\t\t\t\treturn n, q.rerr\n+\t\t\t}\n+\t\t\tq.line, q.rerr = q.br.ReadSlice('\\n')\n+\t\t\tq.line = bytes.TrimRightFunc(q.line, isQPDiscardWhitespace)\n+\t\t\tcontinue\n+\t\t}\n+\t\tif len(q.line) == 1 && q.line[0] == '=' {\n+\t\t\t// Soft newline; skipped.\n+\t\t\tq.line = nil\n+\t\t\tcontinue\n+\t\t}\n+\t\tb := q.line[0]\n+\t\tswitch {\n+\t\tcase b == '=':\n+\t\t\tb, err = q.readHexByte(q.line[1:])\n+\t\t\tif err != nil {\n+\t\t\t\treturn n, err\n+\t\t\t}\n+\t\t\tq.line = q.line[2:] // 2 of the 3; other 1 is done below\n+\t\tcase b != '\\t' && (b < ' ' || b > '~'):\n+\t\t\treturn n, fmt.Errorf("multipart: invalid unescaped byte 0x%02x in quoted-printable body", b)\n+\t\t}\n+\t\tp[0] = b\n+\t\tp = p[1:]\n+\t\tq.line = q.line[1:]\n+\t\tn++\n+\t}\n+\treturn n, nil\n+}\ndiff --git a/src/pkg/mime/multipart/quotedprintable_test.go b/src/pkg/mime/multipart/quotedprintable_test.go
new file mode 100644
index 0000000000..796a41f42d
--- /dev/null
+++ b/src/pkg/mime/multipart/quotedprintable_test.go
@@ -0,0 +1,52 @@
+// Copyright 2012 The Go Authors. All rights reserved.\n+// Use of this source code is governed by a BSD-style\n+// license that can be found in the LICENSE file.\n+\n+package multipart\n+\n+import (\n+\t"bytes"\n+\t"fmt"\n+\t"io"\n+\t"strings"\n+\t"testing"\n+)\n+\n+func TestQuotedPrintable(t *testing.T) {\n+\ttests := []struct {\n+\t\tin, want string\n+\t\terr      interface{}\n+\t}{\n+\t\t{in: "foo bar", want: "foo bar"},\n+\t\t{in: "foo bar=3D", want: "foo bar="},\n+\t\t{in: "foo bar=0", want: "foo bar", err: io.ErrUnexpectedEOF},\n+\t\t{in: "foo bar=ab", want: "foo bar", err: "multipart: invalid quoted-printable hex byte 0x61"},\n+\t\t{in: "foo bar=0D=0A", want: "foo bar\\r\\n"},\n+\t\t{in: "foo bar=\\r\\n baz", want: "foo bar baz"},\n+\t\t{in: "foo=\\nbar", want: "foobar"},\n+\t\t{in: "foo\\x00bar", want: "foo", err: "multipart: invalid unescaped byte 0x00 in quoted-printable body"},\n+\t\t{in: "foo bar\\xff", want: "foo bar", err: "multipart: invalid unescaped byte 0xff in quoted-printable body"},\n+\t}\n+\tfor _, tt := range tests {\n+\t\tvar buf bytes.Buffer\n+\t\t_, err := io.Copy(&buf, newQuotedPrintableReader(strings.NewReader(tt.in)))\n+\t\tif got := buf.String(); got != tt.want {\n+\t\t\tt.Errorf("for %q, got %q; want %q", tt.in, got, tt.want)\n+\t\t}\n+\t\tswitch verr := tt.err.(type) {\n+\t\tcase nil:\n+\t\t\tif err != nil {\n+\t\t\t\tt.Errorf("for %q, got unexpected error: %v", tt.in, err)\n+\t\t\t}\n+\t\tcase string:\n+\t\t\tif got := fmt.Sprint(err); got != verr {\n+\t\t\t\tt.Errorf("for %q, got error %q; want %q", tt.in, got, verr)\n+\t\t\t}\n+\t\tcase error:\n+\t\t\tif err != verr {\n+\t\t\t\tt.Errorf("for %q, got error %q; want %q", tt.in, err, verr)\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+}\n```

## 変更の背景

このコミットは、Go言語の `mime/multipart` パッケージが `quoted-printable` 転送エンコーディングを適切に処理できないという問題(Issue #4411)を修正するために導入されました。MIME (Multipurpose Internet Mail Extensions) は、電子メールがテキスト以外のデータ(画像、音声、動画など)や、ASCII以外の文字セットを含むテキストを送信できるようにするための標準です。MIMEメッセージは複数の「パート」に分割され、各パートは独自のヘッダとボディを持ちます。

`Content-Transfer-Encoding` ヘッダは、MIMEパートのボディがどのようにエンコードされているかを示します。`quoted-printable` は、主に7ビットASCII文字で構成されるテキストデータを、7ビット転送環境で安全に送信するために使用されるエンコーディング方式です。しかし、既存の `mime/multipart` パッケージでは、このエンコーディングが施されたパートのデコードが透過的に行われず、アプリケーション側で手動でデコードする必要がありました。これは、MIMEメッセージの処理を複雑にし、開発者の負担となっていました。

この変更の目的は、`quoted-printable` エンコードされたMIMEパートを `mime/multipart` パッケージが自動的に検出し、デコードすることで、ユーザーが `Part.Read` メソッドを呼び出した際に、既にデコードされた生データを受け取れるようにすることです。これにより、MIMEメッセージの処理がより直感的で簡単になります。

## 前提知識の解説

### MIME (Multipurpose Internet Mail Extensions)

MIMEは、電子メールシステムがASCIIテキスト以外のコンテンツ(画像、音声、動画、国際文字セットなど)を扱えるように拡張するインターネット標準です。MIMEは、メッセージの構造、コンテンツタイプ、エンコーディング方法などを定義します。

-   **MIMEタイプ (Content-Type)**: メッセージまたはパートのデータの種類を示します(例: `text/plain`, `image/jpeg`, `application/json`, `multipart/mixed`)。
-   **マルチパートメッセージ (multipart)**: 複数の異なるデータタイプを一つのメッセージに含めるためのMIMEタイプです。例えば、`multipart/mixed` は添付ファイルを含むメッセージによく使われます。各パートは境界文字列で区切られ、それぞれが独自のヘッダとボディを持ちます。
-   **Content-Transfer-Encoding**: メッセージまたはパートのボディが、転送のためにどのようにエンコードされているかを示します。これは、7ビットの電子メールシステムで8ビットデータやバイナリデータを安全に送信するために必要です。

### Quoted-Printable 転送エンコーディング (RFC 2045)

`quoted-printable` は、MIMEで定義されている `Content-Transfer-Encoding` の一つで、主に7ビットASCII文字が大部分を占めるテキストデータを、7ビット転送環境で安全に送信するために使用されます。RFC 2045で詳細が規定されています。

-   **目的**: 8ビットデータや、ASCIIの制御文字、または `=` (イコール記号) などの特殊な意味を持つ文字を、7ビット環境で安全に表現することです。
-   **エンコーディング規則**:
    *   ほとんどの印字可能なASCII文字(スペース、タブを含む)はそのまま表現されます。
    *   印字不可能なASCII文字(例: 改行コード)や、8ビット文字、または `=` 記号は、`=` の後にその文字の16進数表現(2桁)を続けて表現されます。例えば、改行コードのCRLF (`\r\n`) は `=0D=0A` となります。
    *   行の末尾にスペースやタブがある場合、それらもエンコードされます(例: `=20`)。
    *   **ソフト改行 (Soft Line Break)**: `quoted-printable` エンコードされた行は、76文字を超えてはなりません。長い行を分割するために、行の末尾に `=` を置くことで、デコード時にその改行が無視される「ソフト改行」として扱われます。これにより、元のテキストの改行位置を変えずに、エンコードされた行の長さを制限できます。
-   **可読性**: `quoted-printable` は、元のデータが主にASCIIテキストである場合、エンコード後も比較的読みやすいという特徴があります。これは、バイナリデータを主に扱う `Base64` エンコーディングとは対照的です。

## 技術的詳細

`quoted-printable` エンコーディングは、MIMEメッセージのボディを、7ビットの電子メール転送システムで安全に扱える形式に変換するためのメカニズムです。その核心は、特定の文字を「エスケープ」することにあります。

1.  **エスケープメカニズム**:
    *   **非ASCII文字と制御文字**: 8ビット文字(例: 日本語の文字)や、タブ、スペース以外のASCII制御文字(例: ヌル文字 `\x00`)は、`=` の後にその文字の16進数表現(2桁)を続けてエンコードされます。例えば、`é` (U+00E9) は `=E9` となります。
    *   **`=` (イコール) 記号**: `quoted-printable` エンコーディング自体で特殊な意味を持つ `=` 記号は、それ自身がエンコードされる必要があります。したがって、`=` は `=3D` とエンコードされます。
    *   **行末のスペースとタブ**: 行末のスペース (` `) やタブ (`\t`) は、デコード時に誤ってトリムされるのを防ぐため、それぞれ `=20` や `=09` とエンコードされるべきです。
2.  **ソフト改行**:
    *   `quoted-printable` の仕様では、エンコードされた行の長さは76文字を超えてはならないとされています。これを遵守しつつ、元のテキストの改行を維持するために「ソフト改行」が導入されています。
    *   ソフト改行は、行の末尾に `=` 記号を置くことで示されます。デコード時には、この `=` とそれに続く改行(CRLF)が無視され、次の行と連結されます。これにより、エンコードされたメッセージの行長制限を満たしながら、元のテキストの論理的な構造を保つことができます。
    *   例: `words words words words words words words words words words words words wor=\r\nds words words words words words words words words words words words words =` のような形式で、`=` の後に改行が続く場合、デコード時には `words words words words words words words words words words words words words words words words words words words words words words words words words words words` のように連結されます。
3.  **デコードプロセス**:
    *   デコーダは入力ストリームを読み込み、`=` 記号を検出すると、それに続く2文字を16進数として解釈し、対応するバイトに変換します。
    *   行末の `=` とそれに続く改行(CRLF)はソフト改行として認識され、デコードされた出力からは削除されます。
    *   それ以外の文字はそのまま出力されます。
    *   不正なエンコーディング(例: `=` の後に16進数でない文字が続く、または文字が足りない)が検出された場合、エラーを報告します。

このコミットでは、Goの `mime/multipart` パッケージが、この `quoted-printable` デコードロジックを内部に組み込むことで、ユーザーが明示的にデコード処理を記述することなく、MIMEパートのコンテンツを直接読み取れるようにしています。

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

このコミットでは、以下の4つのファイルが変更されています。

1.  `src/pkg/mime/multipart/multipart.go`:
    *   `Part` 構造体に `io.Reader` 型のフィールド `r` が追加されました。これは、パートのボディを読み取るためのリーダーであり、必要に応じて `quoted-printable` デコーダがラップされます。
    *   `newPart` 関数内で、`Content-Transfer-Encoding` ヘッダが `quoted-printable` であるかどうかがチェックされます。もしそうであれば、そのヘッダは削除され、`Part` の `r` フィールドが新しく作成される `quotedPrintableReader` でラップされます。
    *   `Part.Read` メソッドが、直接 `mr.readPart` を呼び出す代わりに、`p.r.Read` を呼び出すように変更されました。これにより、`quoted-printable` デコーダが介在できるようになります。
    *   `partReader` という新しい内部構造体が追加され、`io.Reader` インターフェースを実装しています。これは、`Part` の生のバイトを読み取るためのラッパーです。
2.  `src/pkg/mime/multipart/multipart_test.go`:
    *   `TestQuotedPrintableEncoding` という新しいテストケースが追加されました。これは、`quoted-printable` エンコードされたMIMEボディが正しくデコードされることを検証します。特に、Issue #4411で報告された長い行のソフト改行のデコードが正しく行われることを確認しています。
    *   既存の `TestLineContinuation` テストで、`io.Copy` の出力先が `ioutil.Discard` から `bytes.Buffer` に変更され、エラー発生時に読み取った内容をログに出力するように改善されました。
3.  `src/pkg/mime/multipart/quotedprintable.go`:
    *   `quoted-printable` デコーダの実装を含む新しいファイルとして追加されました。
    *   `qpReader` 構造体が定義され、`io.Reader` インターフェースを実装しています。これが `quoted-printable` デコードの主要なロジックを含みます。
    *   `newQuotedPrintableReader` 関数は、与えられた `io.Reader` をラップして `qpReader` を返します。
    *   `fromHex` 関数は、16進数文字(`0-9`, `A-F`)をバイト値に変換します。
    *   `readHexByte` 関数は、`=` の後に続く2つの16進数文字を読み取り、対応するバイトを返します。
    *   `isQPDiscardWhitespace` 関数は、`quoted-printable` デコード時に破棄されるべき空白文字(改行、スペース、タブ)を識別します。
    *   `qpReader.Read` メソッドは、`quoted-printable` エンコードされたデータを読み取り、デコードして生のバイトを返します。ソフト改行の処理、16進数エスケープのデコード、不正な文字の検出など、すべてのデコードロジックがここに実装されています。
4.  `src/pkg/mime/multipart/quotedprintable_test.go`:
    *   `quotedprintable.go` で実装された `qpReader` の単体テストを含む新しいファイルとして追加されました。
    *   `TestQuotedPrintable` 関数は、様々な `quoted-printable` エンコードされた文字列と、それらがデコードされた後の期待される結果、および発生する可能性のあるエラーを定義したテストケースのセットを実行します。これにより、`qpReader` のデコードロジックがRFC 2045の仕様に準拠していることを確認します。

## コアとなるコードの解説

このコミットの核心は、`mime/multipart` パッケージが `quoted-printable` エンコーディングを透過的に処理できるようにする点にあります。

### `src/pkg/mime/multipart/multipart.go` の変更

-   **`Part` 構造体の拡張**:
    ```go
    type Part struct {
        // ... 既存のフィールド ...
        // r is either a reader directly reading from mr, or it's a
        // wrapper around such a reader, decoding the
        // Content-Transfer-Encoding
        r io.Reader
    }
    ```
    `Part` 構造体に `r io.Reader` フィールドが追加されました。これは、MIMEパートの実際のボディデータを読み取るためのリーダーです。このリーダーは、必要に応じて `quoted-printable` デコーダでラップされます。

-   **`newPart` 関数でのデコーダの挿入**:
    ```go
    func newPart(mr *Reader) (*Part, error) {
        // ... ヘッダのパース ...
        bp.r = partReader{bp} // まずは生のリーダーを設定
        const cte = "Content-Transfer-Encoding"
        if bp.Header.Get(cte) == "quoted-printable" {
            bp.Header.Del(cte) // ヘッダを削除 (透過的な処理のため)
            bp.r = newQuotedPrintableReader(bp.r) // quoted-printable デコーダでラップ
        }
        return bp, nil
    }
    ```
    `newPart` 関数は、新しいMIMEパートが作成される際に呼び出されます。ここで、`Content-Transfer-Encoding` ヘッダが `quoted-printable` であるかどうかがチェックされます。もしそうであれば、元の `Content-Transfer-Encoding` ヘッダは削除され(これにより、ユーザーはデコードされたデータを受け取ることを期待するため、このヘッダは不要になります)、`Part` の内部リーダー `bp.r` が新しく作成された `quotedPrintableReader` でラップされます。これにより、`Part.Read` が呼び出されたときに、自動的にデコード処理が適用されるようになります。

-   **`Part.Read` メソッドの変更**:
    ```go
    func (p *Part) Read(d []byte) (n int, err error) {
        return p.r.Read(d) // 内部のリーダーから読み取る
    }

    // partReader implements io.Reader by reading raw bytes directly from the
    // wrapped *Part, without doing any Transfer-Encoding decoding.
    type partReader struct {
        p *Part
    }

    func (pr partReader) Read(d []byte) (n int, err error) {
        p := pr.p
        defer func() {
            p.bytesRead += n
        }()
        // ... 以前の生の読み取りロジック ...
    }
    ```
    `Part.Read` メソッドは、直接 `p.r.Read(d)` を呼び出すように変更されました。これにより、`newPart` で設定された `quotedPrintableReader` が介在し、ユーザーが `Part.Read` を呼び出すと、自動的にデコードされたデータが返されるようになります。`partReader` は、`quoted-printable` デコードが不要な場合に、生のバイトを読み取るためのデフォルトのリーダーとして機能します。

### `src/pkg/mime/multipart/quotedprintable.go` の新規追加

このファイルは、`quoted-printable` デコードの具体的なロジックを実装する `qpReader` 構造体と関連関数を含んでいます。

-   **`qpReader` 構造体**:
    ```go
    type qpReader struct {
        br   *bufio.Reader // underlying reader
        rerr error         // last read error
        line []byte        // to be consumed before more of br
    }
    ```
    `qpReader` は、`bufio.Reader` をラップし、デコード処理中に読み取った行を一時的に保持するための `line` バッファを持ちます。

-   **`newQuotedPrintableReader` 関数**:
    ```go
    func newQuotedPrintableReader(r io.Reader) io.Reader {
        return &qpReader{
            br: bufio.NewReader(r),
        }
    }
    ```
    この関数は、`io.Reader` を受け取り、それをラップする `qpReader` のインスタンスを返します。

-   **`qpReader.Read` メソッド**:
    このメソッドが `quoted-printable` デコードの主要なロジックを含みます。
    1.  **行の読み込み**: `q.line` バッファが空の場合、`q.br.ReadSlice('\n')` を使って基になるリーダーから1行(改行を含む)を読み込みます。
    2.  **行末の空白のトリム**: `bytes.TrimRightFunc(q.line, isQPDiscardWhitespace)` を使用して、行末のスペース、タブ、CR、LFをトリムします。これは、ソフト改行や行末の空白の処理に必要です。
    3.  **ソフト改行の処理**: `len(q.line) == 1 && q.line[0] == '='` の場合、それはソフト改行(`=` の後に改行が続く)と判断され、`q.line` を `nil` に設定してスキップします。
    4.  **エスケープシーケンスのデコード**:
        *   文字が `=` の場合、`q.readHexByte(q.line[1:])` を呼び出して、それに続く2つの16進数文字をデコードします。
        *   デコードされたバイトは出力バッファ `p` に書き込まれ、`q.line` は消費された分だけ進められます。
    5.  **不正な文字の検出**: `=` でエスケープされていない印字不可能な文字(タブを除く)や、ASCII範囲外の文字が検出された場合、エラーを返します。
    6.  **通常の文字の処理**: 上記の特殊なケースに該当しない文字は、そのまま出力バッファ `p` にコピーされます。

この `qpReader.Read` メソッドは、`io.Reader` インターフェースの要件を満たし、呼び出されるたびにデコードされたデータを少しずつ提供します。

### テストファイルの追加と変更

-   `multipart_test.go` に追加された `TestQuotedPrintableEncoding` は、実際のMIMEメッセージの例を用いて、`quoted-printable` エンコードされたパートが正しくデコードされることをエンドツーエンドで検証します。
-   `quotedprintable_test.go` は、`qpReader` の単体テストに特化しており、様々なエスケープシーケンス、ソフト改行、エラーケースを網羅的にテストすることで、デコーダのロジックの正確性を保証します。

これらの変更により、Goの `mime/multipart` パッケージは、`quoted-printable` エンコードされたMIMEパートを透過的に処理できるようになり、開発者はMIMEメッセージの複雑なエンコーディングの詳細を意識することなく、コンテンツを直接読み取れるようになりました。

## 関連リンク

-   Go CL 6854067: [https://golang.org/cl/6854067](https://golang.org/cl/6854067)
-   Go Issue 4411: [https://golang.org/issue/4411](https://golang.org/issue/4411)

## 参考にした情報源リンク

-   GitHub Commit: [https://github.com/golang/go/commit/d32d1e098a1022e45f4a7afd05c328b72c5df8ee](https://github.com/golang/go/commit/d32d1e098a1022e45f4a7afd05c328b72c5df8ee)
-   RFC 2045 - Multipurpose Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies: [https://www.rfc-editor.org/rfc/rfc2045](https://www.rfc-editor.org/rfc/rfc2045) (特に Section 6.7: Quoted-Printable Content-Transfer-Encoding)
-   Quoted-Printable - Wikipedia: [https://en.wikipedia.org/wiki/Quoted-Printable](https://en.wikipedia.org/wiki/Quoted-Printable)