mime/multipart: add Part.NextRawPart to avoid QP decoding

NextPart has automatic handling of quoted-printable encoding,
which is sometimes undesirable. NextRawPart adds a method
for reading a part while bypassing such automatic handling.

Fixes #29090

Change-Id: I6a042a4077c64091efa3f5dbecce0d9a34ac7065
Reviewed-on: https://go-review.googlesource.com/c/go/+/152877
Run-TryBot: Ian Lance Taylor <iant@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
diff --git a/src/mime/multipart/multipart.go b/src/mime/multipart/multipart.go
index a222409..1750300 100644
--- a/src/mime/multipart/multipart.go
+++ b/src/mime/multipart/multipart.go
@@ -36,11 +36,6 @@
 	// The headers of the body, if any, with the keys canonicalized
 	// in the same fashion that the Go http.Request headers are.
 	// For example, "foo-bar" changes case to "Foo-Bar"
-	//
-	// As a special case, if the "Content-Transfer-Encoding" header
-	// has a value of "quoted-printable", that header is instead
-	// hidden from this map and the body is transparently decoded
-	// during Read calls.
 	Header textproto.MIMEHeader
 
 	mr *Reader
@@ -126,7 +121,7 @@
 	return n, r.err
 }
 
-func newPart(mr *Reader) (*Part, error) {
+func newPart(mr *Reader, rawPart bool) (*Part, error) {
 	bp := &Part{
 		Header: make(map[string][]string),
 		mr:     mr,
@@ -135,10 +130,14 @@
 		return nil, err
 	}
 	bp.r = partReader{bp}
-	const cte = "Content-Transfer-Encoding"
-	if strings.EqualFold(bp.Header.Get(cte), "quoted-printable") {
-		bp.Header.Del(cte)
-		bp.r = quotedprintable.NewReader(bp.r)
+
+	// rawPart is used to switch between Part.NextPart and Part.NextRawPart.
+	if !rawPart {
+		const cte = "Content-Transfer-Encoding"
+		if strings.EqualFold(bp.Header.Get(cte), "quoted-printable") {
+			bp.Header.Del(cte)
+			bp.r = quotedprintable.NewReader(bp.r)
+		}
 	}
 	return bp, nil
 }
@@ -300,7 +299,24 @@
 
 // NextPart returns the next part in the multipart or an error.
 // When there are no more parts, the error io.EOF is returned.
+//
+// As a special case, if the "Content-Transfer-Encoding" header
+// has a value of "quoted-printable", that header is instead
+// hidden and the body is transparently decoded during Read calls.
 func (r *Reader) NextPart() (*Part, error) {
+	return r.nextPart(false)
+}
+
+// NextRawPart returns the next part in the multipart or an error.
+// When there are no more parts, the error io.EOF is returned.
+//
+// Unlike NextPart, it does not have special handling for
+// "Content-Transfer-Encoding: quoted-printable".
+func (r *Reader) NextRawPart() (*Part, error) {
+	return r.nextPart(true)
+}
+
+func (r *Reader) nextPart(rawPart bool) (*Part, error) {
 	if r.currentPart != nil {
 		r.currentPart.Close()
 	}
@@ -325,7 +341,7 @@
 
 		if r.isBoundaryDelimiterLine(line) {
 			r.partsRead++
-			bp, err := newPart(r)
+			bp, err := newPart(r, rawPart)
 			if err != nil {
 				return nil, err
 			}
diff --git a/src/mime/multipart/multipart_test.go b/src/mime/multipart/multipart_test.go
index 5dc74b5..b60c54a 100644
--- a/src/mime/multipart/multipart_test.go
+++ b/src/mime/multipart/multipart_test.go
@@ -449,6 +449,66 @@
 	}
 }
 
+func TestRawPart(t *testing.T) {
+	// https://github.com/golang/go/issues/29090
+
+	body := strings.Replace(`--0016e68ee29c5d515f04cedf6733
+Content-Type: text/plain; charset="utf-8"
+Content-Transfer-Encoding: quoted-printable
+
+<div dir=3D"ltr">Hello World.</div>
+--0016e68ee29c5d515f04cedf6733
+Content-Type: text/plain; charset="utf-8"
+Content-Transfer-Encoding: quoted-printable
+
+<div dir=3D"ltr">Hello World.</div>
+--0016e68ee29c5d515f04cedf6733--`, "\n", "\r\n", -1)
+
+	r := NewReader(strings.NewReader(body), "0016e68ee29c5d515f04cedf6733")
+
+	// This part is expected to be raw, bypassing the automatic handling
+	// of quoted-printable.
+	part, err := r.NextRawPart()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if _, ok := part.Header["Content-Transfer-Encoding"]; !ok {
+		t.Errorf("missing Content-Transfer-Encoding")
+	}
+	var buf bytes.Buffer
+	_, err = io.Copy(&buf, part)
+	if err != nil {
+		t.Error(err)
+	}
+	got := buf.String()
+	// Data is still quoted-printable.
+	want := `<div dir=3D"ltr">Hello World.</div>`
+	if got != want {
+		t.Errorf("wrong part value:\n got: %q\nwant: %q", got, want)
+	}
+
+	// This part is expected to have automatic decoding of quoted-printable.
+	part, err = r.NextPart()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if te, ok := part.Header["Content-Transfer-Encoding"]; ok {
+		t.Errorf("unexpected Content-Transfer-Encoding of %q", te)
+	}
+
+	buf.Reset()
+	_, err = io.Copy(&buf, part)
+	if err != nil {
+		t.Error(err)
+	}
+	got = buf.String()
+	// QP data has been decoded.
+	want = `<div dir="ltr">Hello World.</div>`
+	if got != want {
+		t.Errorf("wrong part value:\n got: %q\nwant: %q", got, want)
+	}
+}
+
 // Test parsing an image attachment from gmail, which previously failed.
 func TestNested(t *testing.T) {
 	// nested-mime is the body part of a multipart/mixed email