8 "github.com/mjl-/bstore"
10 "github.com/mjl-/mox/imapclient"
11 "github.com/mjl-/mox/store"
14func TestFetch(t *testing.T) {
18func TestFetchUIDOnly(t *testing.T) {
22func testFetch(t *testing.T, uidonly bool) {
23 tc := start(t, uidonly)
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")
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>",
45 noflags := imapclient.FetchFlags(nil)
46 bodystructbody1 := imapclient.BodyTypeText{
48 MediaSubtype: "PLAIN",
49 BodyFields: imapclient.BodyFields{
50 Params: [][2]string{[...]string{"CHARSET", "US-ASCII"}},
55 bodyxstructure1 := imapclient.FetchBodystructure{
57 Body: bodystructbody1,
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)),
67 bodystructbody1.Ext = &bodyext1
68 bodystructure1.Body = bodystructbody1
70 split := strings.SplitN(exampleMsg, "\r\n\r\n", 2)
71 exampleMsgHeader := split[0] + "\r\n\r\n"
72 exampleMsgBody := split[1]
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"}
97 flagsSeen := imapclient.FetchFlags{`\Seen`}
100 tc.transactf("ok", "fetch 1 all")
101 tc.xuntagged(tc.untaggedFetch(1, 1, date1, rfcsize1, env1, noflags))
103 tc.transactf("ok", "fetch 1 fast")
104 tc.xuntagged(tc.untaggedFetch(1, 1, date1, rfcsize1, noflags))
106 tc.transactf("ok", "fetch 1 full")
107 tc.xuntagged(tc.untaggedFetch(1, 1, date1, rfcsize1, env1, bodyxstructure1, noflags))
109 tc.transactf("ok", "fetch 1 flags")
110 tc.xuntagged(tc.untaggedFetch(1, 1, noflags))
112 tc.transactf("ok", "fetch 1 bodystructure")
113 tc.xuntagged(tc.untaggedFetch(1, 1, bodystructure1))
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))
121 tc.transactf("ok", "fetch 1 binary[1]")
122 tc.xuntagged(tc.untaggedFetch(1, 1, binarypart1)) // Seen flag not changed.
124 tc.client.MSNStoreFlagsClear("1", true, `\Seen`)
125 tc.transactf("ok", "uid fetch 1 binary[]<1.1>")
127 tc.untaggedFetch(1, 1, binarypartial1, noflags),
128 tc.untaggedFetch(1, 1, flagsSeen), // For UID FETCH, we get the flags during the command.
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))
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))
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))
149 tc.transactf("ok", "fetch 1 binary.size[]")
150 tc.xuntagged(tc.untaggedFetch(1, 1, binarysize1))
152 tc.transactf("ok", "fetch 1 binary.size[1]")
153 tc.xuntagged(tc.untaggedFetch(1, 1, binarysizepart1))
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.
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))
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))
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))
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))
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))
196 tc.transactf("ok", "fetch 1 rfc822.header")
197 tc.xuntagged(tc.untaggedFetch(1, 1, rfcheader1))
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))
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))
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))
218 tc.transactf("ok", "fetch 1 binary.peek[]")
219 tc.xuntagged(tc.untaggedFetch(1, 1, binary1))
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.
229 tc.transactf("no", "fetch 1 body.peek[1.header]")
232 tc.transactf("ok", "fetch 1 body.peek[1.mime]")
233 tc.xuntagged(tc.untaggedFetch(1, 1, mime1))
236 tc.transactf("bad", "fetch 2 body[]")
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))
244 tc.client.UIDStoreFlagsAdd("1", true, `\Seen`)
245 tc.transactf("ok", "noop")
249 tc.transactf("ok", "uid fetch 1 body[]")
250 tc.xuntagged(tc.untaggedFetch(1, 1, body1))
252 tc.transactf("ok", "uid fetch 2 body[]")
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")
263 t.Fatalf("missing inbox")
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")
270 saveDate = m.SaveDate.Truncate(time.Second)
273 tc.check(err, "get savedate")
274 tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchSaveDate{SaveDate: &saveDate}))
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
300 tc.transactf("no", "fetch 1 body[2]") // No such part.
303 // Add more complex message.
305 bodystructure2 := imapclient.FetchBodystructure{
306 RespAttr: "BODYSTRUCTURE",
307 Body: imapclient.BodyTypeMpart{
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{
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)),
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)),
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"}},
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},
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")),
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)),
364 tc.client.Append("inbox", makeAppendTime(nestedMessage, received))
365 tc.transactf("ok", "uid fetch 2 bodystructure")
366 tc.xuntagged(tc.untaggedFetch(2, 2, bodystructure2))
368 // Multiple responses.
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))
386 tc.transactf("ok", "uid fetch 1:* bodystructure")
387 tc.xuntagged(tc.untaggedFetch(1, 1, bodystructure1), tc.untaggedFetch(2, 2, bodystructure2))
389 tc.transactf("ok", "uid fetch 1:2 bodystructure")
390 tc.xuntagged(tc.untaggedFetch(1, 1, bodystructure1), tc.untaggedFetch(2, 2, bodystructure2))
392 tc.transactf("ok", "uid fetch 1,2 bodystructure")
393 tc.xuntagged(tc.untaggedFetch(1, 1, bodystructure1), tc.untaggedFetch(2, 2, bodystructure2))
395 tc.transactf("ok", "uid fetch 2:2 bodystructure")
396 tc.xuntagged(tc.untaggedFetch(2, 2, bodystructure2))
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}))
402 part1 := tocrlf(` ... Some text appears here ...
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
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}))
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.
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))}))
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}))
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}))
427 part3 := tocrlf(`--unique-boundary-2
428Content-Type: audio/basic
429Content-Transfer-Encoding: base64
434Content-Type: image/jpeg
435Content-Transfer-Encoding: base64
436Content-Disposition: inline; filename=image.jpg
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}))
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}))
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
455 ... Additional text in ISO-8859-1 goes here ...
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}))
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
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}))
470 part5mime := tocrlf(`Content-Type: message/rfc822
471Content-MD5: MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=
472Content-Language: en,de
473Content-Location: http://localhost
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}))
478 part5text := " ... Additional text in ISO-8859-1 goes here ...\r\n"
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}))
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}))
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]")
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`)
494 tc.client.Examine("inbox")
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}))
501 tc.transactf("ok", "uid fetch 1 preview (lazy)")
502 tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchPreview{Preview: &preview}))
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}
508 tcheck(t, err, "get message")
510 t.Fatalf("uid %d instead of 1", m.UID)
514 tcheck(t, err, "remove preview from message")
517 tcheck(t, err, "remove preview from database")
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")
530 tc.transactf("bad", "uid fetch 1 preview (bogus)")
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`)
543 tc.transactf("ok", "uid fetch 1 binary[]")
545 tc.untaggedFetch(1, 1, imapclient.FetchFlags{`\Deleted`}),
546 imapclient.UntaggedVanished{UIDs: xparseNumSet("1")},
548 // Message no longer available in session.
550 tc.transactf("ok", "fetch 1 binary[]")
551 tc.xuntagged(tc.untaggedFetch(1, 1, binary1))
553 tc.transactf("ok", "fetch 1 body[]")
554 tc.xuntagged(tc.untaggedFetch(1, 1, body1))
556 tc.transactf("ok", "fetch 1 rfc822.text")
557 tc.xuntagged(tc.untaggedFetch(1, 1, rfctext1))
559 tc.transactf("ok", "fetch 1 rfc822")
560 tc.xuntagged(tc.untaggedFetch(1, 1, rfc1))