Skip to content

Commit 30508aa

Browse files
committed
imapclient: handle message/global BODYSTRUCTURE
The BODYSTRUCTURE response for a message/global attachment may or may not contain the data that would be included for message/rfc822. In my experience with Dovecot, it does not. Fixes #678
1 parent 1905c46 commit 30508aa

File tree

2 files changed

+226
-14
lines changed

2 files changed

+226
-14
lines changed

imapclient/fetch.go

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -994,25 +994,26 @@ func readBodyType1part(dec *imapwire.Decoder, typ string, options *Options) (*im
994994
var msg imap.BodyStructureMessageRFC822
995995

996996
msg.Envelope, err = readEnvelope(dec, options)
997-
if err != nil {
998-
return nil, err
999-
}
997+
if err == nil {
998+
// If err is not nil, it likely means that the server omitted the MessageRFC822 data,
999+
// which is permitted in some circumstances for message/global.
1000+
if !dec.ExpectSP() {
1001+
return nil, dec.Err()
1002+
}
10001003

1001-
if !dec.ExpectSP() {
1002-
return nil, dec.Err()
1003-
}
1004+
msg.BodyStructure, err = readBody(dec, options)
1005+
if err != nil {
1006+
return nil, err
1007+
}
10041008

1005-
msg.BodyStructure, err = readBody(dec, options)
1006-
if err != nil {
1007-
return nil, err
1008-
}
1009+
if !dec.ExpectSP() || !dec.ExpectNumber64(&msg.NumLines) {
1010+
return nil, dec.Err()
1011+
}
10091012

1010-
if !dec.ExpectSP() || !dec.ExpectNumber64(&msg.NumLines) {
1011-
return nil, dec.Err()
1013+
bs.MessageRFC822 = &msg
1014+
hasSP = false
10121015
}
10131016

1014-
bs.MessageRFC822 = &msg
1015-
hasSP = false
10161017
} else if strings.EqualFold(bs.Type, "text") {
10171018
var text imap.BodyStructureText
10181019

imapclient/parse_test.go

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
package imapclient
2+
3+
import (
4+
"bufio"
5+
"encoding/json"
6+
"reflect"
7+
"strings"
8+
"testing"
9+
10+
"github.com/emersion/go-imap/v2"
11+
"github.com/emersion/go-imap/v2/internal/imapwire"
12+
)
13+
14+
var bodyStructureCases = []struct {
15+
description string
16+
data string
17+
parsed imap.BodyStructure
18+
}{
19+
{
20+
description: "example from RFC 3501",
21+
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") `,
22+
parsed: &imap.BodyStructureMultiPart{
23+
Children: []imap.BodyStructure{
24+
&imap.BodyStructureSinglePart{
25+
Type: "TEXT",
26+
Subtype: "PLAIN",
27+
Params: map[string]string{"charset": "US-ASCII"},
28+
ID: "",
29+
Description: "",
30+
Encoding: "7BIT",
31+
Size: 1152,
32+
Text: &imap.BodyStructureText{
33+
NumLines: 23,
34+
},
35+
},
36+
&imap.BodyStructureSinglePart{
37+
Type: "TEXT",
38+
Subtype: "PLAIN",
39+
Params: map[string]string{
40+
"charset": "US-ASCII",
41+
"name": "cc.diff",
42+
},
43+
44+
Description: "Compiler diff",
45+
Encoding: "BASE64",
46+
Size: 4554,
47+
Text: &imap.BodyStructureText{
48+
NumLines: 73,
49+
},
50+
},
51+
},
52+
Subtype: "MIXED",
53+
},
54+
},
55+
{
56+
description: "issue #678",
57+
data: `("text" "html" ("charset" "utf-8") NIL NIL "quoted-printable" 5606 133 NIL NIL NIL NIL)`,
58+
parsed: &imap.BodyStructureSinglePart{
59+
Type: "text",
60+
Subtype: "html",
61+
Params: map[string]string{"charset": "utf-8"},
62+
ID: "",
63+
Description: "",
64+
Encoding: "quoted-printable",
65+
Size: 5606,
66+
Text: &imap.BodyStructureText{
67+
NumLines: 133,
68+
},
69+
Extended: &imap.BodyStructureSinglePartExt{},
70+
},
71+
},
72+
{
73+
description: "message/global attachment in Dovecot",
74+
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)`,
75+
parsed: &imap.BodyStructureMultiPart{
76+
Children: []imap.BodyStructure{
77+
&imap.BodyStructureMultiPart{
78+
Children: []imap.BodyStructure{
79+
&imap.BodyStructureMultiPart{
80+
Children: []imap.BodyStructure{
81+
&imap.BodyStructureSinglePart{
82+
Type: "text",
83+
Subtype: "plain",
84+
Params: map[string]string{
85+
"charset": "UTF-8",
86+
},
87+
ID: "",
88+
Description: "",
89+
Encoding: "quoted-printable",
90+
Size: 576,
91+
Text: &imap.BodyStructureText{
92+
NumLines: 31,
93+
},
94+
Extended: &imap.BodyStructureSinglePartExt{},
95+
},
96+
&imap.BodyStructureSinglePart{
97+
Type: "text",
98+
Subtype: "html",
99+
Params: map[string]string{
100+
"charset": "UTF-8",
101+
},
102+
ID: "",
103+
Description: "",
104+
Encoding: "quoted-printable",
105+
Size: 4691,
106+
Text: &imap.BodyStructureText{
107+
NumLines: 112,
108+
},
109+
Extended: &imap.BodyStructureSinglePartExt{},
110+
},
111+
},
112+
Subtype: "alternative",
113+
Extended: &imap.BodyStructureMultiPartExt{
114+
Params: map[string]string{
115+
"boundary": "----=_NextPart_002_0063_01D85E63.43189F50",
116+
},
117+
},
118+
},
119+
&imap.BodyStructureSinglePart{
120+
Type: "image",
121+
Subtype: "png",
122+
Params: map[string]string{
123+
"name": "image001.png",
124+
},
125+
126+
Description: "",
127+
Encoding: "base64",
128+
Size: 3832,
129+
Extended: &imap.BodyStructureSinglePartExt{},
130+
},
131+
},
132+
Subtype: "related",
133+
Extended: &imap.BodyStructureMultiPartExt{
134+
Params: map[string]string{
135+
"boundary": "----=_NextPart_001_0062_01D85E63.43189F50",
136+
},
137+
},
138+
},
139+
&imap.BodyStructureSinglePart{
140+
Type: "message",
141+
Subtype: "delivery-status",
142+
Params: map[string]string{
143+
"name": "details.txt",
144+
},
145+
ID: "",
146+
Description: "",
147+
Encoding: "7bit",
148+
Size: 594,
149+
Extended: &imap.BodyStructureSinglePartExt{
150+
Disposition: &imap.BodyStructureDisposition{
151+
Value: "attachment",
152+
Params: map[string]string{
153+
"filename": "details.txt",
154+
},
155+
},
156+
},
157+
},
158+
&imap.BodyStructureSinglePart{
159+
Type: "message",
160+
Subtype: "global",
161+
Params: map[string]string{
162+
"name": "Untitled attachment 00019.dat",
163+
},
164+
ID: "",
165+
Description: "",
166+
Encoding: "7bit",
167+
Size: 6726,
168+
MessageRFC822: nil,
169+
Extended: &imap.BodyStructureSinglePartExt{
170+
Disposition: &imap.BodyStructureDisposition{
171+
Value: "attachment",
172+
Params: map[string]string{
173+
"filename": "Untitled attachment 00019.dat",
174+
},
175+
},
176+
},
177+
},
178+
},
179+
Subtype: "mixed",
180+
Extended: &imap.BodyStructureMultiPartExt{
181+
Params: map[string]string{
182+
"boundary": "----=_NextPart_000_0061_01D85E63.43189F50",
183+
},
184+
Language: []string{
185+
"en-us",
186+
},
187+
},
188+
},
189+
},
190+
}
191+
192+
func TestParseBodyStructure(t *testing.T) {
193+
for _, c := range bodyStructureCases {
194+
dec := imapwire.NewDecoder(bufio.NewReader(strings.NewReader(c.data)), imapwire.ConnSideClient)
195+
s, err := readBody(dec, &Options{})
196+
if err != nil {
197+
t.Fatalf("%s: error parsing body structure: %v", c.description, err)
198+
}
199+
if !reflect.DeepEqual(s, c.parsed) {
200+
t.Fatalf("%s: parsed structure doesn't match: want %s, got %s", c.description, toJSON(c.parsed), toJSON(s))
201+
}
202+
}
203+
}
204+
205+
func toJSON(v any) []byte {
206+
data, err := json.Marshal(v)
207+
if err != nil {
208+
panic(err)
209+
}
210+
return data
211+
}

0 commit comments

Comments
 (0)