1package imapserver
2
3import (
4 "strings"
5 "testing"
6 "time"
7
8 "github.com/mjl-/bstore"
9
10 "github.com/mjl-/mox/imapclient"
11 "github.com/mjl-/mox/store"
12)
13
14func TestFetch(t *testing.T) {
15 tc := start(t)
16 defer tc.close()
17
18 tc.client.Login("mjl@mox.example", password0)
19 tc.client.Enable("imap4rev2")
20 received, err := time.Parse(time.RFC3339, "2022-11-16T10:01:00+01:00")
21 tc.check(err, "parse time")
22 tc.client.Append("inbox", makeAppendTime(exampleMsg, received))
23 tc.client.Select("inbox")
24
25 uid1 := imapclient.FetchUID(1)
26 date1 := imapclient.FetchInternalDate{Date: received}
27 rfcsize1 := imapclient.FetchRFC822Size(len(exampleMsg))
28 env1 := imapclient.FetchEnvelope{
29 Date: "Mon, 7 Feb 1994 21:52:25 -0800",
30 Subject: "afternoon meeting",
31 From: []imapclient.Address{{Name: "Fred Foobar", Mailbox: "foobar", Host: "blurdybloop.example"}},
32 Sender: []imapclient.Address{{Name: "Fred Foobar", Mailbox: "foobar", Host: "blurdybloop.example"}},
33 ReplyTo: []imapclient.Address{{Name: "Fred Foobar", Mailbox: "foobar", Host: "blurdybloop.example"}},
34 To: []imapclient.Address{{Mailbox: "mooch", Host: "owatagu.siam.edu.example"}},
35 MessageID: "<B27397-0100000@Blurdybloop.example>",
36 }
37 noflags := imapclient.FetchFlags(nil)
38 bodyxstructure1 := imapclient.FetchBodystructure{
39 RespAttr: "BODY",
40 Body: imapclient.BodyTypeText{
41 MediaType: "TEXT",
42 MediaSubtype: "PLAIN",
43 BodyFields: imapclient.BodyFields{
44 Params: [][2]string{[...]string{"CHARSET", "US-ASCII"}},
45 Octets: 57,
46 },
47 Lines: 2,
48 },
49 }
50 bodystructure1 := bodyxstructure1
51 bodystructure1.RespAttr = "BODYSTRUCTURE"
52
53 split := strings.SplitN(exampleMsg, "\r\n\r\n", 2)
54 exampleMsgHeader := split[0] + "\r\n\r\n"
55 exampleMsgBody := split[1]
56
57 binary1 := imapclient.FetchBinary{RespAttr: "BINARY[]", Data: exampleMsg}
58 binarypart1 := imapclient.FetchBinary{RespAttr: "BINARY[1]", Parts: []uint32{1}, Data: exampleMsgBody}
59 binarypartial1 := imapclient.FetchBinary{RespAttr: "BINARY[]", Data: exampleMsg[1:2]}
60 binarypartpartial1 := imapclient.FetchBinary{RespAttr: "BINARY[1]", Parts: []uint32{1}, Data: exampleMsgBody[1:2]}
61 binaryend1 := imapclient.FetchBinary{RespAttr: "BINARY[]", Data: ""}
62 binarypartend1 := imapclient.FetchBinary{RespAttr: "BINARY[1]", Parts: []uint32{1}, Data: ""}
63 binarysize1 := imapclient.FetchBinarySize{RespAttr: "BINARY.SIZE[]", Size: int64(len(exampleMsg))}
64 binarysizepart1 := imapclient.FetchBinarySize{RespAttr: "BINARY.SIZE[1]", Parts: []uint32{1}, Size: int64(len(exampleMsgBody))}
65 bodyheader1 := imapclient.FetchBody{RespAttr: "BODY[HEADER]", Section: "HEADER", Body: exampleMsgHeader}
66 bodytext1 := imapclient.FetchBody{RespAttr: "BODY[TEXT]", Section: "TEXT", Body: exampleMsgBody}
67 body1 := imapclient.FetchBody{RespAttr: "BODY[]", Body: exampleMsg}
68 bodypart1 := imapclient.FetchBody{RespAttr: "BODY[1]", Section: "1", Body: exampleMsgBody}
69 bodyoff1 := imapclient.FetchBody{RespAttr: "BODY[]<1>", Section: "", Offset: 1, Body: exampleMsg[1:3]}
70 body1off1 := imapclient.FetchBody{RespAttr: "BODY[1]<1>", Section: "1", Offset: 1, Body: exampleMsgBody[1:3]}
71 bodyend1 := imapclient.FetchBody{RespAttr: "BODY[1]<100000>", Section: "1", Offset: 100000, Body: ""} // todo: should offset be what was requested, or the size of the message?
72 rfcheader1 := imapclient.FetchRFC822Header(exampleMsgHeader)
73 rfctext1 := imapclient.FetchRFC822Text(exampleMsgBody)
74 rfc1 := imapclient.FetchRFC822(exampleMsg)
75 headerSplit := strings.SplitN(exampleMsgHeader, "\r\n", 2)
76 dateheader1 := imapclient.FetchBody{RespAttr: "BODY[HEADER.FIELDS (Date)]", Section: "HEADER.FIELDS (Date)", Body: headerSplit[0] + "\r\n\r\n"}
77 nodateheader1 := imapclient.FetchBody{RespAttr: "BODY[HEADER.FIELDS.NOT (Date)]", Section: "HEADER.FIELDS.NOT (Date)", Body: headerSplit[1]}
78 date1header1 := imapclient.FetchBody{RespAttr: "BODY[1.HEADER.FIELDS (Date)]", Section: "1.HEADER.FIELDS (Date)", Body: headerSplit[0] + "\r\n\r\n"}
79 nodate1header1 := imapclient.FetchBody{RespAttr: "BODY[1.HEADER.FIELDS.NOT (Date)]", Section: "1.HEADER.FIELDS.NOT (Date)", Body: headerSplit[1]}
80 mime1 := imapclient.FetchBody{RespAttr: "BODY[1.MIME]", Section: "1.MIME", Body: "MIME-Version: 1.0\r\nContent-Type: TEXT/PLAIN; CHARSET=US-ASCII\r\n\r\n"}
81
82 flagsSeen := imapclient.FetchFlags{`\Seen`}
83
84 tc.transactf("ok", "fetch 1 all")
85 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, date1, rfcsize1, env1, noflags}})
86
87 tc.transactf("ok", "fetch 1 fast")
88 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, date1, rfcsize1, noflags}})
89
90 tc.transactf("ok", "fetch 1 full")
91 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, date1, rfcsize1, env1, bodyxstructure1, noflags}})
92
93 tc.transactf("ok", "fetch 1 flags")
94 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, noflags}})
95
96 tc.transactf("ok", "fetch 1 bodystructure")
97 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodystructure1}})
98
99 // Should be returned unmodified, because there is no content-transfer-encoding.
100 tc.transactf("ok", "fetch 1 binary[]")
101 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binary1, noflags}})
102 tc.transactf("ok", "noop")
103 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
104
105 tc.transactf("ok", "fetch 1 binary[1]")
106 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binarypart1}}) // Seen flag not changed.
107
108 tc.client.StoreFlagsClear("1", true, `\Seen`)
109 tc.transactf("ok", "uid fetch 1 binary[]<1.1>")
110 tc.xuntagged(
111 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binarypartial1, noflags}},
112 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}}, // For UID FETCH, we get the flags during the command.
113 )
114
115 tc.client.StoreFlagsClear("1", true, `\Seen`)
116 tc.transactf("ok", "fetch 1 binary[1]<1.1>")
117 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binarypartpartial1, noflags}})
118 tc.transactf("ok", "noop")
119 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
120
121 tc.client.StoreFlagsClear("1", true, `\Seen`)
122 tc.transactf("ok", "fetch 1 binary[]<10000.10001>")
123 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binaryend1, noflags}})
124 tc.transactf("ok", "noop")
125 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
126
127 tc.client.StoreFlagsClear("1", true, `\Seen`)
128 tc.transactf("ok", "fetch 1 binary[1]<10000.10001>")
129 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binarypartend1, noflags}})
130 tc.transactf("ok", "noop")
131 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
132
133 tc.client.StoreFlagsClear("1", true, `\Seen`)
134 tc.transactf("ok", "fetch 1 binary.size[]")
135 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binarysize1}})
136
137 tc.transactf("ok", "fetch 1 binary.size[1]")
138 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binarysizepart1}})
139
140 tc.client.StoreFlagsClear("1", true, `\Seen`)
141 tc.transactf("ok", "fetch 1 body[]")
142 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, body1, noflags}})
143 tc.transactf("ok", "noop")
144 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
145 tc.transactf("ok", "fetch 1 body[]<1.2>")
146 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodyoff1}}) // Already seen.
147 tc.transactf("ok", "noop")
148 tc.xuntagged() // Already seen.
149
150 tc.client.StoreFlagsClear("1", true, `\Seen`)
151 tc.transactf("ok", "fetch 1 body[1]")
152 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodypart1, noflags}})
153 tc.transactf("ok", "noop")
154 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
155
156 tc.client.StoreFlagsClear("1", true, `\Seen`)
157 tc.transactf("ok", "fetch 1 body[1]<1.2>")
158 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, body1off1, noflags}})
159 tc.transactf("ok", "noop")
160 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
161
162 tc.client.StoreFlagsClear("1", true, `\Seen`)
163 tc.transactf("ok", "fetch 1 body[1]<100000.100000>")
164 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodyend1, noflags}})
165 tc.transactf("ok", "noop")
166 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
167
168 tc.client.StoreFlagsClear("1", true, `\Seen`)
169 tc.transactf("ok", "fetch 1 body[header]")
170 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodyheader1, noflags}})
171 tc.transactf("ok", "noop")
172 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
173
174 tc.client.StoreFlagsClear("1", true, `\Seen`)
175 tc.transactf("ok", "fetch 1 body[text]")
176 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodytext1, noflags}})
177 tc.transactf("ok", "noop")
178 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
179
180 // equivalent to body.peek[header], ../rfc/3501:3183
181 tc.client.StoreFlagsClear("1", true, `\Seen`)
182 tc.transactf("ok", "fetch 1 rfc822.header")
183 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, rfcheader1}})
184
185 // equivalent to body[text], ../rfc/3501:3199
186 tc.client.StoreFlagsClear("1", true, `\Seen`)
187 tc.transactf("ok", "fetch 1 rfc822.text")
188 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, rfctext1, noflags}})
189 tc.transactf("ok", "noop")
190 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
191
192 // equivalent to body[], ../rfc/3501:3179
193 tc.client.StoreFlagsClear("1", true, `\Seen`)
194 tc.transactf("ok", "fetch 1 rfc822")
195 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, rfc1, noflags}})
196 tc.transactf("ok", "noop")
197 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
198
199 // With PEEK, we should not get the \Seen flag.
200 tc.client.StoreFlagsClear("1", true, `\Seen`)
201 tc.transactf("ok", "fetch 1 body.peek[]")
202 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, body1}})
203
204 tc.transactf("ok", "fetch 1 binary.peek[]")
205 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binary1}})
206
207 // HEADER.FIELDS and .NOT
208 tc.transactf("ok", "fetch 1 body.peek[header.fields (date)]")
209 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, dateheader1}})
210 tc.transactf("ok", "fetch 1 body.peek[header.fields.not (date)]")
211 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, nodateheader1}})
212 // For non-multipart messages, 1 means the whole message. ../rfc/9051:4481
213 tc.transactf("ok", "fetch 1 body.peek[1.header.fields (date)]")
214 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, date1header1}})
215 tc.transactf("ok", "fetch 1 body.peek[1.header.fields.not (date)]")
216 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, nodate1header1}})
217
218 // MIME, part 1 for non-multipart messages is the message itself. ../rfc/9051:4481
219 tc.transactf("ok", "fetch 1 body.peek[1.mime]")
220 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, mime1}})
221
222 // Missing sequence number. ../rfc/9051:7018
223 tc.transactf("bad", "fetch 2 body[]")
224
225 tc.transactf("ok", "fetch 1:1 body[]")
226 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, body1, noflags}})
227 tc.transactf("ok", "noop")
228 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
229
230 // UID fetch
231 tc.transactf("ok", "uid fetch 1 body[]")
232 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, body1}})
233
234 // UID fetch
235 tc.transactf("ok", "uid fetch 2 body[]")
236 tc.xuntagged()
237
238 // SAVEDATE
239 tc.transactf("ok", "uid fetch 1 savedate")
240 // Fetch exact SaveDate we'll be expecting from server.
241 var saveDate time.Time
242 err = tc.account.DB.Read(ctxbg, func(tx *bstore.Tx) error {
243 inbox, err := tc.account.MailboxFind(tx, "Inbox")
244 tc.check(err, "get inbox")
245 if inbox == nil {
246 t.Fatalf("missing inbox")
247 }
248 m, err := bstore.QueryTx[store.Message](tx).FilterNonzero(store.Message{MailboxID: inbox.ID, UID: store.UID(uid1)}).Get()
249 tc.check(err, "get message")
250 if m.SaveDate == nil {
251 t.Fatalf("zero savedate for message")
252 }
253 saveDate = m.SaveDate.Truncate(time.Second)
254 return nil
255 })
256 tc.check(err, "get savedate")
257 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, imapclient.FetchSaveDate{SaveDate: &saveDate}}})
258
259 // Test some invalid syntax.
260 tc.transactf("bad", "fetch")
261 tc.transactf("bad", "fetch ")
262 tc.transactf("bad", "fetch ")
263 tc.transactf("bad", "fetch 1") // At least one requested item required.
264 tc.transactf("bad", "fetch 1 ()") // Empty list not allowed
265 tc.transactf("bad", "fetch 1 unknown")
266 tc.transactf("bad", "fetch 1 (unknown)")
267 tc.transactf("bad", "fetch 1 (all)") // Macro's not allowed in list.
268 tc.transactf("bad", "fetch 1 binary") // [] required
269 tc.transactf("bad", "fetch 1 binary[text]") // Text/header etc only allowed for body[].
270 tc.transactf("bad", "fetch 1 binary[]<1>") // Count required.
271 tc.transactf("bad", "fetch 1 binary[]<1.0>") // Count must be > 0.
272 tc.transactf("bad", "fetch 1 binary[]<1..1>") // Single dot.
273 tc.transactf("bad", "fetch 1 body[]<1>") // Count required.
274 tc.transactf("bad", "fetch 1 body[]<1.0>") // Count must be > 0.
275 tc.transactf("bad", "fetch 1 body[]<1..1>") // Single dot.
276 tc.transactf("bad", "fetch 1 body[header.fields]") // List of headers required.
277 tc.transactf("bad", "fetch 1 body[header.fields ()]") // List must be non-empty.
278 tc.transactf("bad", "fetch 1 body[header.fields.not]") // List of headers required.
279 tc.transactf("bad", "fetch 1 body[header.fields.not ()]") // List must be non-empty.
280 tc.transactf("bad", "fetch 1 body[mime]") // MIME must be prefixed with a number. ../rfc/9051:4497
281
282 tc.transactf("no", "fetch 1 body[2]") // No such part.
283
284 // Add more complex message.
285
286 uid2 := imapclient.FetchUID(2)
287 bodystructure2 := imapclient.FetchBodystructure{
288 RespAttr: "BODYSTRUCTURE",
289 Body: imapclient.BodyTypeMpart{
290 Bodies: []any{
291 imapclient.BodyTypeBasic{BodyFields: imapclient.BodyFields{Octets: 275}},
292 imapclient.BodyTypeText{MediaType: "TEXT", MediaSubtype: "PLAIN", BodyFields: imapclient.BodyFields{Params: [][2]string{{"CHARSET", "US-ASCII"}}, Octets: 114}, Lines: 3},
293 imapclient.BodyTypeMpart{
294 Bodies: []any{
295 imapclient.BodyTypeBasic{MediaType: "AUDIO", MediaSubtype: "BASIC", BodyFields: imapclient.BodyFields{CTE: "BASE64", Octets: 22}},
296 imapclient.BodyTypeBasic{MediaType: "IMAGE", MediaSubtype: "JPEG", BodyFields: imapclient.BodyFields{CTE: "BASE64"}},
297 },
298 MediaSubtype: "PARALLEL",
299 Ext: &imapclient.BodyExtensionMpart{Params: [][2]string{{"boundary", "unique-boundary-2"}}},
300 },
301 imapclient.BodyTypeText{MediaType: "TEXT", MediaSubtype: "ENRICHED", BodyFields: imapclient.BodyFields{Octets: 145}, Lines: 5},
302 imapclient.BodyTypeMsg{
303 MediaType: "MESSAGE",
304 MediaSubtype: "RFC822",
305 BodyFields: imapclient.BodyFields{Octets: 228},
306 Envelope: imapclient.Envelope{
307 Subject: "(subject in US-ASCII)",
308 From: []imapclient.Address{{Name: "", Adl: "", Mailbox: "info", Host: "mox.example"}},
309 Sender: []imapclient.Address{{Name: "", Adl: "", Mailbox: "info", Host: "mox.example"}},
310 ReplyTo: []imapclient.Address{{Name: "", Adl: "", Mailbox: "info", Host: "mox.example"}},
311 To: []imapclient.Address{{Name: "mox", Adl: "", Mailbox: "info", Host: "mox.example"}},
312 },
313 Bodystructure: imapclient.BodyTypeText{
314 MediaType: "TEXT", MediaSubtype: "PLAIN", BodyFields: imapclient.BodyFields{Params: [][2]string{{"CHARSET", "ISO-8859-1"}}, CTE: "QUOTED-PRINTABLE", Octets: 51}, Lines: 1},
315 Lines: 7,
316 },
317 },
318 MediaSubtype: "MIXED",
319 Ext: &imapclient.BodyExtensionMpart{Params: [][2]string{{"boundary", "unique-boundary-1"}}},
320 },
321 }
322 tc.client.Append("inbox", makeAppendTime(nestedMessage, received))
323 tc.transactf("ok", "fetch 2 bodystructure")
324 tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
325
326 // Multiple responses.
327 tc.transactf("ok", "fetch 1:2 bodystructure")
328 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodystructure1}}, imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
329 tc.transactf("ok", "fetch 1,2 bodystructure")
330 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodystructure1}}, imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
331 tc.transactf("ok", "fetch 2:1 bodystructure")
332 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodystructure1}}, imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
333 tc.transactf("ok", "fetch 1:* bodystructure")
334 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodystructure1}}, imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
335 tc.transactf("ok", "fetch *:1 bodystructure")
336 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodystructure1}}, imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
337 tc.transactf("ok", "fetch *:2 bodystructure")
338 tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
339
340 tc.transactf("ok", "fetch * bodystructure") // Highest msgseq.
341 tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
342
343 tc.transactf("ok", "uid fetch 1:* bodystructure")
344 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodystructure1}}, imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
345
346 tc.transactf("ok", "uid fetch 1:2 bodystructure")
347 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodystructure1}}, imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
348
349 tc.transactf("ok", "uid fetch 1,2 bodystructure")
350 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodystructure1}}, imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
351
352 tc.transactf("ok", "uid fetch 2:2 bodystructure")
353 tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
354
355 // todo: read the bodies/headers of the parts, and of the nested message.
356 tc.transactf("ok", "fetch 2 body.peek[]")
357 tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[]", Body: nestedMessage}}})
358
359 part1 := tocrlf(` ... Some text appears here ...
360
361[Note that the blank between the boundary and the start
362 of the text in this part means no header fields were
363 given and this is text in the US-ASCII character set.
364 It could have been done with explicit typing as in the
365 next part.]
366`)
367 tc.transactf("ok", "fetch 2 body.peek[1]")
368 tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[1]", Section: "1", Body: part1}}})
369
370 tc.transactf("no", "fetch 2 binary.peek[3]") // Only allowed on leaf parts, not multiparts.
371 tc.transactf("no", "fetch 2 binary.peek[5]") // Only allowed on leaf parts, not messages.
372
373 part31 := "aGVsbG8NCndvcmxkDQo=\r\n"
374 part31dec := "hello\r\nworld\r\n"
375 tc.transactf("ok", "fetch 2 binary.size[3.1]")
376 tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBinarySize{RespAttr: "BINARY.SIZE[3.1]", Parts: []uint32{3, 1}, Size: int64(len(part31dec))}}})
377
378 tc.transactf("ok", "fetch 2 body.peek[3.1]")
379 tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[3.1]", Section: "3.1", Body: part31}}})
380
381 tc.transactf("ok", "fetch 2 binary.peek[3.1]")
382 tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBinary{RespAttr: "BINARY[3.1]", Parts: []uint32{3, 1}, Data: part31dec}}})
383
384 part3 := tocrlf(`--unique-boundary-2
385Content-Type: audio/basic
386Content-Transfer-Encoding: base64
387
388aGVsbG8NCndvcmxkDQo=
389
390--unique-boundary-2
391Content-Type: image/jpeg
392Content-Transfer-Encoding: base64
393
394
395--unique-boundary-2--
396
397`)
398 tc.transactf("ok", "fetch 2 body.peek[3]")
399 tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[3]", Section: "3", Body: part3}}})
400
401 part2mime := tocrlf(`Content-type: text/plain; charset=US-ASCII
402
403`)
404 tc.transactf("ok", "fetch 2 body.peek[2.mime]")
405 tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[2.MIME]", Section: "2.MIME", Body: part2mime}}})
406
407 part5 := tocrlf(`From: info@mox.example
408To: mox <info@mox.example>
409Subject: (subject in US-ASCII)
410Content-Type: Text/plain; charset=ISO-8859-1
411Content-Transfer-Encoding: Quoted-printable
412
413 ... Additional text in ISO-8859-1 goes here ...
414`)
415 tc.transactf("ok", "fetch 2 body.peek[5]")
416 tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[5]", Section: "5", Body: part5}}})
417
418 part5header := tocrlf(`From: info@mox.example
419To: mox <info@mox.example>
420Subject: (subject in US-ASCII)
421Content-Type: Text/plain; charset=ISO-8859-1
422Content-Transfer-Encoding: Quoted-printable
423
424`)
425 tc.transactf("ok", "fetch 2 body.peek[5.header]")
426 tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[5.HEADER]", Section: "5.HEADER", Body: part5header}}})
427
428 part5mime := tocrlf(`Content-Type: Text/plain; charset=ISO-8859-1
429Content-Transfer-Encoding: Quoted-printable
430
431`)
432 tc.transactf("ok", "fetch 2 body.peek[5.mime]")
433 tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[5.MIME]", Section: "5.MIME", Body: part5mime}}})
434
435 part5text := " ... Additional text in ISO-8859-1 goes here ...\r\n"
436 tc.transactf("ok", "fetch 2 body.peek[5.text]")
437 tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[5.TEXT]", Section: "5.TEXT", Body: part5text}}})
438
439 tc.transactf("ok", "fetch 2 body.peek[5.1]")
440 tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[5.1]", Section: "5.1", Body: part5text}}})
441
442 // In case of EXAMINE instead of SELECT, we should not be seeing any changed \Seen flags for non-peek commands.
443 tc.client.StoreFlagsClear("1", true, `\Seen`)
444 tc.client.Unselect()
445 tc.client.Examine("inbox")
446
447 // Preview
448 preview := "Hello Joe, do you think we can meet at 3:30 tomorrow?"
449 tc.transactf("ok", "fetch 1 preview")
450 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, imapclient.FetchPreview{Preview: &preview}}})
451
452 tc.transactf("ok", "fetch 1 preview (lazy)")
453 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, imapclient.FetchPreview{Preview: &preview}}})
454
455 // On-demand preview and saving on first request.
456 err = tc.account.DB.Write(ctxbg, func(tx *bstore.Tx) error {
457 m := store.Message{ID: 1}
458 err := tx.Get(&m)
459 tcheck(t, err, "get message")
460 if m.UID != 1 {
461 t.Fatalf("uid %d instead of 1", m.UID)
462 }
463 m.Preview = nil
464 err = tx.Update(&m)
465 tcheck(t, err, "remove preview from message")
466 return nil
467 })
468 tcheck(t, err, "remove preview from database")
469
470 tc.transactf("ok", "fetch 1 preview")
471 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, imapclient.FetchPreview{Preview: &preview}}})
472 m := store.Message{ID: 1}
473 err = tc.account.DB.Get(ctxbg, &m)
474 tcheck(t, err, "get message")
475 if m.Preview == nil {
476 t.Fatalf("preview missing")
477 } else if *m.Preview != preview+"\n" {
478 t.Fatalf("got preview %q, expected %q", *m.Preview, preview+"\n")
479 }
480
481 tc.transactf("bad", "fetch 1 preview (bogus)")
482
483 // Start a second session. Use it to remove the message. First session should still
484 // be able to access the messages.
485 tc2 := startNoSwitchboard(t)
486 defer tc2.closeNoWait()
487 tc2.client.Login("mjl@mox.example", password0)
488 tc2.client.Select("inbox")
489 tc2.client.StoreFlagsSet("1", true, `\Deleted`)
490 tc2.client.Expunge()
491 tc2.client.Logout()
492
493 tc.transactf("ok", "fetch 1 binary[]")
494 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binary1}})
495
496 tc.transactf("ok", "fetch 1 body[]")
497 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, body1}})
498
499 tc.transactf("ok", "fetch 1 rfc822.text")
500 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, rfctext1}})
501
502 tc.transactf("ok", "fetch 1 rfc822")
503 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, rfc1}})
504
505 tc.client.Logout()
506}
507