diff --git a/pkg/message/parser.go b/pkg/message/parser.go index ecb85679..491d3323 100644 --- a/pkg/message/parser.go +++ b/pkg/message/parser.go @@ -119,7 +119,7 @@ func parse(p *parser.Parser, allowInvalidAddressLists bool) (Message, error) { } if err := patchInlineImages(p); err != nil { - return Message{}, err + return Message{}, errors.Wrap(err, "patching inline images failed") } m, err := parseMessageHeader(p.Root().Header, allowInvalidAddressLists) @@ -671,11 +671,10 @@ func patchInlineImages(p *parser.Parser) error { if rfc822.MIMEType(contentType) == rfc822.TextPlain { result[i] = &inlinePatchBodyOnly{part: curPart, contentTypeMap: contentTypeMap} } else if strings.HasPrefix(contentType, "image/") { - disposition, _, err := curPart.ContentDisposition() + disposition, err := getImageContentDisposition(curPart) if err != nil { - return fmt.Errorf("failted to get content disposition for child %v:%w", i, err) + return fmt.Errorf("failed to get content disposition for child %v:%w", i, err) } - if disposition == "inline" && !curPart.HasContentID() { if rfc822.MIMEType(prevContentType) == rfc822.TextPlain { result[i-1] = &inlinePatchBodyWithInlineImage{ @@ -707,6 +706,23 @@ func patchInlineImages(p *parser.Parser) error { return nil } +func getImageContentDisposition(curPart *parser.Part) (string, error) { + disposition, _, err := curPart.ContentDisposition() + if err == nil { + return disposition, nil + } + + if curPart.Header.Get("Content-Disposition") != "" { + return "", err + } + + if curPart.HasContentID() { + return "inline", nil + } + + return "attachment", nil +} + type inlinePatchJob interface { Patch() } diff --git a/pkg/message/parser_test.go b/pkg/message/parser_test.go index ca9fbdc8..6108270e 100644 --- a/pkg/message/parser_test.go +++ b/pkg/message/parser_test.go @@ -495,6 +495,7 @@ func TestParseTextHTMLWithImageInline(t *testing.T) { assert.Equal(t, `"Receiver" `, m.ToList[0].String()) require.Len(t, m.Attachments, 1) + require.Equal(t, m.Attachments[0].Disposition, proton.InlineDisposition) assert.Equal(t, fmt.Sprintf(`This is body of HTML mail with attachment`, m.Attachments[0].ContentID), string(m.RichBody)) assert.Equal(t, "This is body of *HTML mail* with attachment", string(m.PlainBody)) @@ -506,6 +507,27 @@ func TestParseTextHTMLWithImageInline(t *testing.T) { assert.Equal(t, 8, img.Height) } +func TestParseTextHTMLWithImageInlineNoDisposition(t *testing.T) { + f := getFileReader("text_html_image_inline_no_disposition.eml") + + m, err := Parse(f) + require.NoError(t, err) + + assert.Equal(t, `"Sender" `, m.Sender.String()) + assert.Equal(t, `"Receiver" `, m.ToList[0].String()) + + require.Len(t, m.Attachments, 1) + + assert.Equal(t, `This is body of HTML mail with attachment`, string(m.RichBody)) + assert.Equal(t, "This is body of *HTML mail* with attachment", string(m.PlainBody)) + + // The inline image is an 8x8 mic-dropping gopher. + img, err := png.DecodeConfig(bytes.NewReader(m.Attachments[0].Data)) + require.NoError(t, err) + assert.Equal(t, 8, img.Width) + assert.Equal(t, 8, img.Height) +} + func TestParseWithAttachedPublicKey(t *testing.T) { f := getFileReader("text_plain.eml") diff --git a/pkg/message/testdata/text_html_image_inline_no_disposition.eml b/pkg/message/testdata/text_html_image_inline_no_disposition.eml new file mode 100644 index 00000000..d0e7c4ac --- /dev/null +++ b/pkg/message/testdata/text_html_image_inline_no_disposition.eml @@ -0,0 +1,35 @@ +From: Sender +To: Receiver +Content-Type: multipart/mixed; boundary=longrandomstring + +--longrandomstring +Content-Type: text/html + +This is body of HTML mail with attachment +--longrandomstring +Content-Type: image/png +Content-ID: +Content-Transfer-Encoding: base64 + +iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAABGdBTUEAALGPC/xhBQAAACBjSFJ +NAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFAR +IAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAA +ABaAAAAAAAAASwAAAABAAABLAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAACKADAAQAAAAB +AAAACAAAAAAAXWZ6AAAACXBIWXMAAC4jAAAuIwF4pT92AAACZmlUWHRYTUw6Y29tLmFkb2JlLnh +tcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIE +NvcmUgNS40LjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5O +TkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91 +dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4 +wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC +8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgI +CAgICA8dGlmZjpSZXNvbHV0aW9uVW5pdD4yPC90aWZmOlJlc29sdXRpb25Vbml0PgogICAgICAg +ICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl +4ZWxYRGltZW5zaW9uPjE2PC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6UG +l4ZWxZRGltZW5zaW9uPjE2PC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgPC9yZGY6RGVzY +3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CgZBD4sAAAEISURBVBgZY2CAAO5F +x07Zz96xZ0Pn4lXqIKGGhgYmsFTHvAWdW6/dvnb89Yf/B5+9/r/y9IXzbVPahCH6/jMysfAJygo +JC2r++/T619Mb139J8HIb8Gs5hYMUzJ+/gJ1Jmo9H6c+L5wz3bt5iEeLmYOHn42fQ4vyacqGNQS +0xMfEHc7Cvl6CYho4rh5jUPyYefqafLKyMbH9+/d28/dFfdWtfDaZvTy7Zvv72nYGZkeEvw98/f +5j//2P4yCvxq/nU7zVs//8yM2gzMMitOnnu5cUff/8ff/v5/5Xf///vuHBhJcSRDAws9aEMr38c +W7XjNgvzexZ2rn9vbjx/IXl/M9iLM2fOZAUAKCZv7dU+UgAAAAAASUVORK5CYII= +--longrandomstring--