summaryrefslogtreecommitdiff
path: root/src/pkg/mime/multipart
diff options
context:
space:
mode:
authorMichael Stapelberg <stapelberg@debian.org>2013-03-04 21:27:36 +0100
committerMichael Stapelberg <michael@stapelberg.de>2013-03-04 21:27:36 +0100
commit04b08da9af0c450d645ab7389d1467308cfc2db8 (patch)
treedb247935fa4f2f94408edc3acd5d0d4f997aa0d8 /src/pkg/mime/multipart
parent917c5fb8ec48e22459d77e3849e6d388f93d3260 (diff)
downloadgolang-upstream/1.1_hg20130304.tar.gz
Imported Upstream version 1.1~hg20130304upstream/1.1_hg20130304
Diffstat (limited to 'src/pkg/mime/multipart')
-rw-r--r--src/pkg/mime/multipart/multipart.go24
-rw-r--r--src/pkg/mime/multipart/multipart_test.go28
-rw-r--r--src/pkg/mime/multipart/quotedprintable.go130
-rw-r--r--src/pkg/mime/multipart/quotedprintable_test.go199
-rw-r--r--[-rwxr-xr-x]src/pkg/mime/multipart/testdata/nested-mime0
-rw-r--r--src/pkg/mime/multipart/writer.go29
-rw-r--r--src/pkg/mime/multipart/writer_test.go35
7 files changed, 441 insertions, 4 deletions
diff --git a/src/pkg/mime/multipart/multipart.go b/src/pkg/mime/multipart/multipart.go
index e9e337b92..77e969b41 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
@@ -71,7 +76,7 @@ func (p *Part) parseContentDisposition() {
}
}
-// NewReader creates a new multipart Reader reading from r using the
+// NewReader creates a new multipart Reader reading from reader using the
// given MIME boundary.
func NewReader(reader io.Reader, boundary string) *Reader {
b := []byte("\r\n--" + boundary + "--")
@@ -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 cd65e177e..d662e8340 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 000000000..9e18e1ea1
--- /dev/null
+++ b/src/pkg/mime/multipart/quotedprintable.go
@@ -0,0 +1,130 @@
+// 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.
+// Deviations:
+// 1. in addition to "=\r\n", "=\n" is also treated as soft line break.
+// 2. it will pass through a '\r' or '\n' not preceded by '=', consistent
+// with other broken QP encoders & decoders.
+
+package multipart
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "io"
+)
+
+type qpReader struct {
+ br *bufio.Reader
+ skipWhite bool
+ 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),
+ skipWhite: true,
+ }
+}
+
+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 isQPSkipWhiteByte(b byte) bool {
+ return b == ' ' || b == '\t'
+}
+
+func isQPDiscardWhitespace(r rune) bool {
+ switch r {
+ case '\n', '\r', ' ', '\t':
+ return true
+ }
+ return false
+}
+
+var (
+ crlf = []byte("\r\n")
+ lf = []byte("\n")
+ softSuffix = []byte("=")
+)
+
+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.skipWhite = true
+ q.line, q.rerr = q.br.ReadSlice('\n')
+
+ // Does the line end in CRLF instead of just LF?
+ hasLF := bytes.HasSuffix(q.line, lf)
+ hasCR := bytes.HasSuffix(q.line, crlf)
+ wholeLine := q.line
+ q.line = bytes.TrimRightFunc(wholeLine, isQPDiscardWhitespace)
+ if bytes.HasSuffix(q.line, softSuffix) {
+ rightStripped := wholeLine[len(q.line):]
+ q.line = q.line[:len(q.line)-1]
+ if !bytes.HasPrefix(rightStripped, lf) && !bytes.HasPrefix(rightStripped, crlf) {
+ q.rerr = fmt.Errorf("multipart: invalid bytes after =: %q", rightStripped)
+ }
+ } else if hasLF {
+ if hasCR {
+ q.line = append(q.line, '\r', '\n')
+ } else {
+ q.line = append(q.line, '\n')
+ }
+ }
+ continue
+ }
+ b := q.line[0]
+ if q.skipWhite && isQPSkipWhiteByte(b) {
+ q.line = q.line[1:]
+ continue
+ }
+ q.skipWhite = false
+
+ 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 == '\r' || b == '\n':
+ break
+ case 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 000000000..7bcf5767b
--- /dev/null
+++ b/src/pkg/mime/multipart/quotedprintable_test.go
@@ -0,0 +1,199 @@
+// 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 (
+ "bufio"
+ "bytes"
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "os/exec"
+ "regexp"
+ "sort"
+ "strings"
+ "testing"
+ "time"
+)
+
+func TestQuotedPrintable(t *testing.T) {
+ tests := []struct {
+ in, want string
+ err interface{}
+ }{
+ {in: "", want: ""},
+ {in: "foo bar", want: "foo bar"},
+ {in: "foo bar=3D", want: "foo bar="},
+ {in: "foo bar=\n", want: "foo bar"},
+ {in: "foo bar\n", want: "foo bar\n"}, // somewhat lax.
+ {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: " A B =\r\n C ", want: "A B C"},
+ {in: " A B =\n C ", want: "A B C"}, // lax. treating LF as CRLF
+ {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"},
+
+ // Equal sign.
+ {in: "=3D30\n", want: "=30\n"},
+ {in: "=00=FF0=\n", want: "\x00\xff0"},
+
+ // Trailing whitespace
+ {in: "foo \n", want: "foo\n"},
+ {in: "foo \n\nfoo =\n\nfoo=20\n\n", want: "foo\n\nfoo \nfoo \n\n"},
+
+ // Tests that we allow bare \n and \r through, despite it being strictly
+ // not permitted per RFC 2045, Section 6.7 Page 22 bullet (4).
+ {in: "foo\nbar", want: "foo\nbar"},
+ {in: "foo\rbar", want: "foo\rbar"},
+ {in: "foo\r\nbar", want: "foo\r\nbar"},
+
+ // Different types of soft line-breaks.
+ {in: "foo=\r\nbar", want: "foobar"},
+ {in: "foo=\nbar", want: "foobar"},
+ {in: "foo=\rbar", want: "foo", err: "multipart: invalid quoted-printable hex byte 0x0d"},
+ {in: "foo=\r\r\r \nbar", want: "foo", err: `multipart: invalid bytes after =: "\r\r\r \n"`},
+ }
+ 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)
+ }
+ }
+ }
+
+}
+
+func everySequence(base, alpha string, length int, fn func(string)) {
+ if len(base) == length {
+ fn(base)
+ return
+ }
+ for i := 0; i < len(alpha); i++ {
+ everySequence(base+alpha[i:i+1], alpha, length, fn)
+ }
+}
+
+var useQprint = flag.Bool("qprint", false, "Compare against the 'qprint' program.")
+
+var badSoftRx = regexp.MustCompile(`=([^\r\n]+?\n)|([^\r\n]+$)|(\r$)|(\r[^\n]+\n)|( \r\n)`)
+
+func TestQPExhaustive(t *testing.T) {
+ if *useQprint {
+ _, err := exec.LookPath("qprint")
+ if err != nil {
+ t.Fatalf("Error looking for qprint: %v", err)
+ }
+ }
+
+ var buf bytes.Buffer
+ res := make(map[string]int)
+ everySequence("", "0A \r\n=", 6, func(s string) {
+ if strings.HasSuffix(s, "=") || strings.Contains(s, "==") {
+ return
+ }
+ buf.Reset()
+ _, err := io.Copy(&buf, newQuotedPrintableReader(strings.NewReader(s)))
+ if err != nil {
+ errStr := err.Error()
+ if strings.Contains(errStr, "invalid bytes after =:") {
+ errStr = "invalid bytes after ="
+ }
+ res[errStr]++
+ if strings.Contains(errStr, "invalid quoted-printable hex byte ") {
+ if strings.HasSuffix(errStr, "0x20") && (strings.Contains(s, "=0 ") || strings.Contains(s, "=A ") || strings.Contains(s, "= ")) {
+ return
+ }
+ if strings.HasSuffix(errStr, "0x3d") && (strings.Contains(s, "=0=") || strings.Contains(s, "=A=")) {
+ return
+ }
+ if strings.HasSuffix(errStr, "0x0a") || strings.HasSuffix(errStr, "0x0d") {
+ // bunch of cases; since whitespace at the end of of a line before \n is removed.
+ return
+ }
+ }
+ if strings.Contains(errStr, "unexpected EOF") {
+ return
+ }
+ if errStr == "invalid bytes after =" && badSoftRx.MatchString(s) {
+ return
+ }
+ t.Errorf("decode(%q) = %v", s, err)
+ return
+ }
+ if *useQprint {
+ cmd := exec.Command("qprint", "-d")
+ cmd.Stdin = strings.NewReader(s)
+ stderr, err := cmd.StderrPipe()
+ if err != nil {
+ panic(err)
+ }
+ qpres := make(chan interface{}, 2)
+ go func() {
+ br := bufio.NewReader(stderr)
+ s, _ := br.ReadString('\n')
+ if s != "" {
+ qpres <- errors.New(s)
+ if cmd.Process != nil {
+ // It can get stuck on invalid input, like:
+ // echo -n "0000= " | qprint -d
+ cmd.Process.Kill()
+ }
+ }
+ }()
+ go func() {
+ want, err := cmd.Output()
+ if err == nil {
+ qpres <- want
+ }
+ }()
+ select {
+ case got := <-qpres:
+ if want, ok := got.([]byte); ok {
+ if string(want) != buf.String() {
+ t.Errorf("go decode(%q) = %q; qprint = %q", s, want, buf.String())
+ }
+ } else {
+ t.Logf("qprint -d(%q) = %v", s, got)
+ }
+ case <-time.After(5 * time.Second):
+ t.Logf("qprint timeout on %q", s)
+ }
+ }
+ res["OK"]++
+ })
+ var outcomes []string
+ for k, v := range res {
+ outcomes = append(outcomes, fmt.Sprintf("%v: %d", k, v))
+ }
+ sort.Strings(outcomes)
+ got := strings.Join(outcomes, "\n")
+ want := `OK: 21576
+invalid bytes after =: 3397
+multipart: invalid quoted-printable hex byte 0x0a: 1400
+multipart: invalid quoted-printable hex byte 0x0d: 2700
+multipart: invalid quoted-printable hex byte 0x20: 2490
+multipart: invalid quoted-printable hex byte 0x3d: 440
+unexpected EOF: 3122`
+ if got != want {
+ t.Errorf("Got:\n%s\nWant:\n%s", got, want)
+ }
+}
diff --git a/src/pkg/mime/multipart/testdata/nested-mime b/src/pkg/mime/multipart/testdata/nested-mime
index 71c238e38..71c238e38 100755..100644
--- a/src/pkg/mime/multipart/testdata/nested-mime
+++ b/src/pkg/mime/multipart/testdata/nested-mime
diff --git a/src/pkg/mime/multipart/writer.go b/src/pkg/mime/multipart/writer.go
index ec70be492..e13a956af 100644
--- a/src/pkg/mime/multipart/writer.go
+++ b/src/pkg/mime/multipart/writer.go
@@ -30,11 +30,38 @@ func NewWriter(w io.Writer) *Writer {
}
}
-// Boundary returns the Writer's randomly selected boundary string.
+// Boundary returns the Writer's boundary.
func (w *Writer) Boundary() string {
return w.boundary
}
+// SetBoundary overrides the Writer's default randomly-generated
+// boundary separator with an explicit value.
+//
+// SetBoundary must be called before any parts are created, may only
+// contain certain ASCII characters, and must be 1-69 bytes long.
+func (w *Writer) SetBoundary(boundary string) error {
+ if w.lastpart != nil {
+ return errors.New("mime: SetBoundary called after write")
+ }
+ // rfc2046#section-5.1.1
+ if len(boundary) < 1 || len(boundary) > 69 {
+ return errors.New("mime: invalid boundary length")
+ }
+ for _, b := range boundary {
+ if 'A' <= b && b <= 'Z' || 'a' <= b && b <= 'z' || '0' <= b && b <= '9' {
+ continue
+ }
+ switch b {
+ case '\'', '(', ')', '+', '_', ',', '-', '.', '/', ':', '=', '?':
+ continue
+ }
+ return errors.New("mime: invalid boundary character")
+ }
+ w.boundary = boundary
+ return nil
+}
+
// FormDataContentType returns the Content-Type for an HTTP
// multipart/form-data with this Writer's Boundary.
func (w *Writer) FormDataContentType() string {
diff --git a/src/pkg/mime/multipart/writer_test.go b/src/pkg/mime/multipart/writer_test.go
index 494e936c4..52d68bcb6 100644
--- a/src/pkg/mime/multipart/writer_test.go
+++ b/src/pkg/mime/multipart/writer_test.go
@@ -7,6 +7,7 @@ package multipart
import (
"bytes"
"io/ioutil"
+ "strings"
"testing"
)
@@ -76,3 +77,37 @@ func TestWriter(t *testing.T) {
t.Fatalf("expected end of parts; got %v, %v", part, err)
}
}
+
+func TestWriterSetBoundary(t *testing.T) {
+ var b bytes.Buffer
+ w := NewWriter(&b)
+ tests := []struct {
+ b string
+ ok bool
+ }{
+ {"abc", true},
+ {"", false},
+ {"ungültig", false},
+ {"!", false},
+ {strings.Repeat("x", 69), true},
+ {strings.Repeat("x", 70), false},
+ {"bad!ascii!", false},
+ {"my-separator", true},
+ }
+ for i, tt := range tests {
+ err := w.SetBoundary(tt.b)
+ got := err == nil
+ if got != tt.ok {
+ t.Errorf("%d. boundary %q = %v (%v); want %v", i, tt.b, got, err, tt.ok)
+ } else if tt.ok {
+ got := w.Boundary()
+ if got != tt.b {
+ t.Errorf("boundary = %q; want %q", got, tt.b)
+ }
+ }
+ }
+ w.Close()
+ if got := b.String(); !strings.Contains(got, "\r\n--my-separator--\r\n") {
+ t.Errorf("expected my-separator in output. got: %q", got)
+ }
+}