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--