Skip to content

Commit

Permalink
mime/multipart: add Part.NextRawPart to avoid QP decoding
Browse files Browse the repository at this point in the history
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 <[email protected]>
TryBot-Result: Gobot Gobot <[email protected]>
Reviewed-by: Ian Lance Taylor <[email protected]>
  • Loading branch information
buchanae authored and ianlancetaylor committed Oct 11, 2019
1 parent a08cb9f commit df38069
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 11 deletions.
38 changes: 27 additions & 11 deletions src/mime/multipart/multipart.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,6 @@ type Part struct {
// 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
Expand Down Expand Up @@ -126,7 +121,7 @@ func (r *stickyErrorReader) Read(p []byte) (n int, _ error) {
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,
Expand All @@ -135,10 +130,14 @@ func newPart(mr *Reader) (*Part, error) {
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
}
Expand Down Expand Up @@ -300,7 +299,24 @@ type Reader struct {

// 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()
}
Expand All @@ -325,7 +341,7 @@ func (r *Reader) NextPart() (*Part, error) {

if r.isBoundaryDelimiterLine(line) {
r.partsRead++
bp, err := newPart(r)
bp, err := newPart(r, rawPart)
if err != nil {
return nil, err
}
Expand Down
60 changes: 60 additions & 0 deletions src/mime/multipart/multipart_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,66 @@ func testQuotedPrintableEncoding(t *testing.T, cte string) {
}
}

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
Expand Down

0 comments on commit df38069

Please sign in to comment.