From d32d1e098a1022e45f4a7afd05c328b72c5df8ee Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 19 Nov 2012 19:50:19 -0800 Subject: [PATCH] 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 +++++++++++++++++++ .../mime/multipart/quotedprintable_test.go | 52 +++++++++++ 4 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 src/pkg/mime/multipart/quotedprintable.go create mode 100644 src/pkg/mime/multipart/quotedprintable_test.go 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 } + 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) + } 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. 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 }() 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--" + r := NewReader(strings.NewReader(body), "0016e68ee29c5d515f04cedf6733") + 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) + } + var buf bytes.Buffer + _, err = io.Copy(&buf, part) + if err != nil { + t.Error(err) + } + got := buf.String() + 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" + 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 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. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// The file define a quoted-printable decoder, as specified in RFC 2045. + +package multipart + +import ( + "bufio" + "bytes" + "fmt" + "io" +) + +type qpReader struct { + br *bufio.Reader + rerr error // last read error + line []byte // to be consumed before more of br +} + +func newQuotedPrintableReader(r io.Reader) io.Reader { + return &qpReader{ + br: bufio.NewReader(r), + } +} + +func fromHex(b byte) (byte, error) { + switch { + case b >= '0' && b <= '9': + return b - '0', nil + case b >= 'A' && b <= 'F': + return b - 'A' + 10, nil + } + return 0, fmt.Errorf("multipart: invalid quoted-printable hex byte 0x%02x", b) +} + +func (q *qpReader) readHexByte(v []byte) (b byte, err error) { + if len(v) < 2 { + return 0, io.ErrUnexpectedEOF + } + var hb, lb byte + if hb, err = fromHex(v[0]); err != nil { + return 0, err + } + if lb, err = fromHex(v[1]); err != nil { + return 0, err + } + return hb<<4 | lb, nil +} + +func isQPDiscardWhitespace(r rune) bool { + switch r { + case '\n', '\r', ' ', '\t': + return true + } + return false +} + +func (q *qpReader) Read(p []byte) (n int, err error) { + for len(p) > 0 { + if len(q.line) == 0 { + if q.rerr != nil { + return n, q.rerr + } + q.line, q.rerr = q.br.ReadSlice('\n') + q.line = bytes.TrimRightFunc(q.line, isQPDiscardWhitespace) + continue + } + if len(q.line) == 1 && q.line[0] == '=' { + // Soft newline; skipped. + q.line = nil + continue + } + b := q.line[0] + switch { + case b == '=': + b, err = q.readHexByte(q.line[1:]) + if err != nil { + return n, err + } + q.line = q.line[2:] // 2 of the 3; other 1 is done below + case b != '\t' && (b < ' ' || b > '~'): + return n, fmt.Errorf("multipart: invalid unescaped byte 0x%02x in quoted-printable body", b) + } + p[0] = b + p = p[1:] + q.line = q.line[1:] + n++ + } + return n, nil +} diff --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. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package multipart + +import ( + "bytes" + "fmt" + "io" + "strings" + "testing" +) + +func TestQuotedPrintable(t *testing.T) { + tests := []struct { + in, want string + err interface{} + }{ + {in: "foo bar", want: "foo bar"}, + {in: "foo bar=3D", want: "foo bar="}, + {in: "foo bar=0", want: "foo bar", err: io.ErrUnexpectedEOF}, + {in: "foo bar=ab", want: "foo bar", err: "multipart: invalid quoted-printable hex byte 0x61"}, + {in: "foo bar=0D=0A", want: "foo bar\r\n"}, + {in: "foo bar=\r\n baz", want: "foo bar baz"}, + {in: "foo=\nbar", want: "foobar"}, + {in: "foo\x00bar", want: "foo", err: "multipart: invalid unescaped byte 0x00 in quoted-printable body"}, + {in: "foo bar\xff", want: "foo bar", err: "multipart: invalid unescaped byte 0xff in quoted-printable body"}, + } + for _, tt := range tests { + var buf bytes.Buffer + _, err := io.Copy(&buf, newQuotedPrintableReader(strings.NewReader(tt.in))) + if got := buf.String(); got != tt.want { + t.Errorf("for %q, got %q; want %q", tt.in, got, tt.want) + } + switch verr := tt.err.(type) { + case nil: + if err != nil { + t.Errorf("for %q, got unexpected error: %v", tt.in, err) + } + case string: + if got := fmt.Sprint(err); got != verr { + t.Errorf("for %q, got error %q; want %q", tt.in, got, verr) + } + case error: + if err != verr { + t.Errorf("for %q, got error %q; want %q", tt.in, err, verr) + } + } + } + +} -- 2.48.1