diff --git a/src/pkg/mime/multipart/multipart.go b/src/pkg/mime/multipart/multipart.go index d733130abb2..6ace4be564c 100644 --- a/src/pkg/mime/multipart/multipart.go +++ b/src/pkg/mime/multipart/multipart.go @@ -185,6 +185,14 @@ func (r *Reader) NextPart() (*Part, error) { expectNewPart := false for { line, err := r.bufReader.ReadSlice('\n') + if err == io.EOF && bytes.Equal(line, r.dashBoundaryDash) { + // If the buffer ends in "--boundary--" without the + // trailing "\r\n", ReadSlice will return an error + // (since it's missing the '\n'), but this is a valid + // multipart EOF so we need to return io.EOF instead of + // a fmt-wrapped one. + return nil, io.EOF + } if err != nil { return nil, fmt.Errorf("multipart: NextPart: %v", err) } diff --git a/src/pkg/mime/multipart/multipart_test.go b/src/pkg/mime/multipart/multipart_test.go index 89ff5e489ef..ca7108d7ad0 100644 --- a/src/pkg/mime/multipart/multipart_test.go +++ b/src/pkg/mime/multipart/multipart_test.go @@ -10,6 +10,7 @@ import ( "fmt" "io" "io/ioutil" + "os" "strings" "testing" ) @@ -377,3 +378,52 @@ func TestLineContinuation(t *testing.T) { } } } + +// 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 + // with boundary e89a8ff1c1e83553e304be640612 + f, err := os.Open("testdata/nested-mime") + if err != nil { + t.Fatal(err) + } + defer f.Close() + mr := NewReader(f, "e89a8ff1c1e83553e304be640612") + p, err := mr.NextPart() + if err != nil { + t.Fatalf("error reading first section (alternative): %v", err) + } + + // Read the inner text/plain and text/html sections of the multipart/alternative. + mr2 := NewReader(p, "e89a8ff1c1e83553e004be640610") + p, err = mr2.NextPart() + if err != nil { + t.Fatalf("reading text/plain part: %v", err) + } + if b, err := ioutil.ReadAll(p); string(b) != "*body*\r\n" || err != nil { + t.Fatalf("reading text/plain part: got %q, %v", b, err) + } + p, err = mr2.NextPart() + if err != nil { + t.Fatalf("reading text/html part: %v", err) + } + if b, err := ioutil.ReadAll(p); string(b) != "body\r\n" || err != nil { + t.Fatalf("reading text/html part: got %q, %v", b, err) + } + + p, err = mr2.NextPart() + if err != io.EOF { + t.Fatalf("final inner NextPart = %v; want io.EOF", err) + } + + // Back to the outer multipart/mixed, reading the image attachment. + _, err = mr.NextPart() + if err != nil { + t.Fatalf("error reading the image attachment at the end: %v", err) + } + + _, err = mr.NextPart() + if err != io.EOF { + t.Fatalf("final outer NextPart = %v; want io.EOF", err) + } +} diff --git a/src/pkg/mime/multipart/testdata/nested-mime b/src/pkg/mime/multipart/testdata/nested-mime new file mode 100755 index 00000000000..71c238e3890 --- /dev/null +++ b/src/pkg/mime/multipart/testdata/nested-mime @@ -0,0 +1,29 @@ +--e89a8ff1c1e83553e304be640612 +Content-Type: multipart/alternative; boundary=e89a8ff1c1e83553e004be640610 + +--e89a8ff1c1e83553e004be640610 +Content-Type: text/plain; charset=UTF-8 + +*body* + +--e89a8ff1c1e83553e004be640610 +Content-Type: text/html; charset=UTF-8 + +body + +--e89a8ff1c1e83553e004be640610-- +--e89a8ff1c1e83553e304be640612 +Content-Type: image/png; name="x.png" +Content-Disposition: attachment; + filename="x.png" +Content-Transfer-Encoding: base64 +X-Attachment-Id: f_h1edgigu0 + +iVBORw0KGgoAAAANSUhEUgAAAagAAADrCAIAAACza5XhAAAKMWlDQ1BJQ0MgUHJvZmlsZQAASImd +lndUU9kWh8+9N71QkhCKlNBraFICSA29SJEuKjEJEErAkAAiNkRUcERRkaYIMijggKNDkbEiioUB +8b2kqeGaj4aTNftesu5mob4pr07ecMywRwLBvDCJOksqlUyldAZD7g9fxIZRWWPMvXRNJROJRBIG +Y7Vx0mva1HAwYqibdKONXye3dW4iUonhWFJnqK7OaanU1gGkErFYEgaj0cg8wK+zVPh2ziwnHy07 +U8lYTNapezSzOuevRwLB7CFkqQQCwaJDiBQIBIJFhwh8AoFg0SHUqQUCASRJKkwkhMy/JfODWPEJ +BIJFhwh8AoFg0TFnQqQ55GtPFopcJsN97e1nYtNuIBYeGBgYCmYrmE3jZ05iaGAoMX0xzxkWz6Hv +yO7WvrlwzA0uLzrD+VkKqViwl9IfTBVNFMyc/x9alloiPPlqhQAAAABJRU5ErkJggg== +--e89a8ff1c1e83553e304be640612--