Commit d32d1e09 authored by Brad Fitzpatrick's avatar Brad Fitzpatrick

mime/multipart: transparently decode quoted-printable transfer encoding

Fixes #4411

R=dsymonds
CC=gobot, golang-dev
https://golang.org/cl/6854067
parent aeca7a7c
......@@ -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
}()
......
......@@ -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
......
// 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
}
// 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)
}
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment