Skip to content

Commit 93036b3

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

File tree

3 files changed

+224
-1
lines changed

3 files changed

+224
-1
lines changed

imapclient/fetch.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -990,7 +990,11 @@ func readBodyType1part(dec *imapwire.Decoder, typ string, options *Options) (*im
990990
return &bs, nil
991991
}
992992

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

9961000
msg.Envelope, err = readEnvelope(dec, options)

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

internal/imapwire/decoder.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,14 @@ func (dec *Decoder) acceptByte(want byte) bool {
122122
return true
123123
}
124124

125+
func (dec *Decoder) NextByteIs(want byte) bool {
126+
if dec.acceptByte(want) {
127+
dec.mustUnreadByte()
128+
return true
129+
}
130+
return false
131+
}
132+
125133
// EOF returns true if end-of-file is reached.
126134
func (dec *Decoder) EOF() bool {
127135
_, err := dec.r.ReadByte()

0 commit comments

Comments
 (0)