Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion imapclient/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -990,7 +990,11 @@ func readBodyType1part(dec *imapwire.Decoder, typ string, options *Options) (*im
return &bs, nil
}

if strings.EqualFold(bs.Type, "message") && (strings.EqualFold(bs.Subtype, "rfc822") || strings.EqualFold(bs.Subtype, "global")) {
// If the Content-Type is message/rfc822, the envelope, body structure, and number of
// lines must come next. If it's message/global, IMAP4rev2 servers will include them,
// but IMAP4rev1 servers may not. So in that case, we can go by whether the next byte
// is '(', since the envelope is a parenthesized list.
if strings.EqualFold(bs.Type, "message") && (strings.EqualFold(bs.Subtype, "rfc822") || strings.EqualFold(bs.Subtype, "global") && dec.NextByteIs('(')) {
var msg imap.BodyStructureMessageRFC822

msg.Envelope, err = readEnvelope(dec, options)
Expand Down
211 changes: 211 additions & 0 deletions imapclient/parse_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package imapclient

import (
"bufio"
"encoding/json"
"reflect"
"strings"
"testing"

"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/internal/imapwire"
)

var bodyStructureCases = []struct {
description string
data string
parsed imap.BodyStructure
}{
{
description: "example from RFC 3501",
data: `(("TEXT" "PLAIN" ("CHARSET" "US-ASCII") NIL NIL "7BIT" 1152 23)("TEXT" "PLAIN" ("CHARSET" "US-ASCII" "NAME" "cc.diff") "<[email protected]>" "Compiler diff" "BASE64" 4554 73) "MIXED") `,
parsed: &imap.BodyStructureMultiPart{
Children: []imap.BodyStructure{
&imap.BodyStructureSinglePart{
Type: "TEXT",
Subtype: "PLAIN",
Params: map[string]string{"charset": "US-ASCII"},
ID: "",
Description: "",
Encoding: "7BIT",
Size: 1152,
Text: &imap.BodyStructureText{
NumLines: 23,
},
},
&imap.BodyStructureSinglePart{
Type: "TEXT",
Subtype: "PLAIN",
Params: map[string]string{
"charset": "US-ASCII",
"name": "cc.diff",
},
ID: "<[email protected]>",
Description: "Compiler diff",
Encoding: "BASE64",
Size: 4554,
Text: &imap.BodyStructureText{
NumLines: 73,
},
},
},
Subtype: "MIXED",
},
},
{
description: "issue #678",
data: `("text" "html" ("charset" "utf-8") NIL NIL "quoted-printable" 5606 133 NIL NIL NIL NIL)`,
parsed: &imap.BodyStructureSinglePart{
Type: "text",
Subtype: "html",
Params: map[string]string{"charset": "utf-8"},
ID: "",
Description: "",
Encoding: "quoted-printable",
Size: 5606,
Text: &imap.BodyStructureText{
NumLines: 133,
},
Extended: &imap.BodyStructureSinglePartExt{},
},
},
{
description: "message/global attachment in Dovecot",
data: `(((("text" "plain" ("charset" "UTF-8") NIL NIL "quoted-printable" 576 31 NIL NIL NIL NIL)("text" "html" ("charset" "UTF-8") NIL NIL "quoted-printable" 4691 112 NIL NIL NIL NIL) "alternative" ("boundary" "----=_NextPart_002_0063_01D85E63.43189F50") NIL NIL NIL)("image" "png" ("name" "image001.png") "<[email protected]>" NIL "base64" 3832 NIL NIL NIL NIL) "related" ("boundary" "----=_NextPart_001_0062_01D85E63.43189F50") NIL NIL NIL)("message" "delivery-status" ("name" "details.txt") NIL NIL "7bit" 594 NIL ("attachment" ("filename" "details.txt")) NIL NIL)("message" "global" ("name" "Untitled attachment 00019.dat") NIL NIL "7bit" 6726 NIL ("attachment" ("filename" "Untitled attachment 00019.dat")) NIL NIL) "mixed" ("boundary" "----=_NextPart_000_0061_01D85E63.43189F50") NIL ("en-us") NIL)`,
parsed: &imap.BodyStructureMultiPart{
Children: []imap.BodyStructure{
&imap.BodyStructureMultiPart{
Children: []imap.BodyStructure{
&imap.BodyStructureMultiPart{
Children: []imap.BodyStructure{
&imap.BodyStructureSinglePart{
Type: "text",
Subtype: "plain",
Params: map[string]string{
"charset": "UTF-8",
},
ID: "",
Description: "",
Encoding: "quoted-printable",
Size: 576,
Text: &imap.BodyStructureText{
NumLines: 31,
},
Extended: &imap.BodyStructureSinglePartExt{},
},
&imap.BodyStructureSinglePart{
Type: "text",
Subtype: "html",
Params: map[string]string{
"charset": "UTF-8",
},
ID: "",
Description: "",
Encoding: "quoted-printable",
Size: 4691,
Text: &imap.BodyStructureText{
NumLines: 112,
},
Extended: &imap.BodyStructureSinglePartExt{},
},
},
Subtype: "alternative",
Extended: &imap.BodyStructureMultiPartExt{
Params: map[string]string{
"boundary": "----=_NextPart_002_0063_01D85E63.43189F50",
},
},
},
&imap.BodyStructureSinglePart{
Type: "image",
Subtype: "png",
Params: map[string]string{
"name": "image001.png",
},
ID: "<[email protected]>",
Description: "",
Encoding: "base64",
Size: 3832,
Extended: &imap.BodyStructureSinglePartExt{},
},
},
Subtype: "related",
Extended: &imap.BodyStructureMultiPartExt{
Params: map[string]string{
"boundary": "----=_NextPart_001_0062_01D85E63.43189F50",
},
},
},
&imap.BodyStructureSinglePart{
Type: "message",
Subtype: "delivery-status",
Params: map[string]string{
"name": "details.txt",
},
ID: "",
Description: "",
Encoding: "7bit",
Size: 594,
Extended: &imap.BodyStructureSinglePartExt{
Disposition: &imap.BodyStructureDisposition{
Value: "attachment",
Params: map[string]string{
"filename": "details.txt",
},
},
},
},
&imap.BodyStructureSinglePart{
Type: "message",
Subtype: "global",
Params: map[string]string{
"name": "Untitled attachment 00019.dat",
},
ID: "",
Description: "",
Encoding: "7bit",
Size: 6726,
MessageRFC822: nil,
Extended: &imap.BodyStructureSinglePartExt{
Disposition: &imap.BodyStructureDisposition{
Value: "attachment",
Params: map[string]string{
"filename": "Untitled attachment 00019.dat",
},
},
},
},
},
Subtype: "mixed",
Extended: &imap.BodyStructureMultiPartExt{
Params: map[string]string{
"boundary": "----=_NextPart_000_0061_01D85E63.43189F50",
},
Language: []string{
"en-us",
},
},
},
},
}

func TestParseBodyStructure(t *testing.T) {
for _, c := range bodyStructureCases {
dec := imapwire.NewDecoder(bufio.NewReader(strings.NewReader(c.data)), imapwire.ConnSideClient)
s, err := readBody(dec, &Options{})
if err != nil {
t.Fatalf("%s: error parsing body structure: %v", c.description, err)
}
if !reflect.DeepEqual(s, c.parsed) {
t.Fatalf("%s: parsed structure doesn't match: want %s, got %s", c.description, toJSON(c.parsed), toJSON(s))
}
}
}

func toJSON(v any) []byte {
data, err := json.Marshal(v)
if err != nil {
panic(err)
}
return data
}
8 changes: 8 additions & 0 deletions internal/imapwire/decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@ func (dec *Decoder) acceptByte(want byte) bool {
return true
}

func (dec *Decoder) NextByteIs(want byte) bool {
if dec.acceptByte(want) {
dec.mustUnreadByte()
return true
}
return false
}

// EOF returns true if end-of-file is reached.
func (dec *Decoder) EOF() bool {
_, err := dec.r.ReadByte()
Expand Down