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