mime/multipart: transparently decode quoted-printable transfer encoding
Fixes #4411
R=dsymonds
CC=gobot, golang-dev
https://golang.org/cl/6854067
diff --git a/src/pkg/mime/multipart/multipart.go b/src/pkg/mime/multipart/multipart.go
index fb07e1a..77e969b 100644
--- a/src/pkg/mime/multipart/multipart.go
+++ b/src/pkg/mime/multipart/multipart.go
@@ -37,6 +37,11 @@
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 @@
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 @@
// 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 cd65e17..d662e83 100644
--- a/src/pkg/mime/multipart/multipart_test.go
+++ b/src/pkg/mime/multipart/multipart_test.go
@@ -339,9 +339,10 @@
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 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 0000000..0a60a6e
--- /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 0000000..796a41f
--- /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)
+ }
+ }
+ }
+
+}