1package imapserver
2
3import (
4 "fmt"
5 "strconv"
6 "strings"
7 "testing"
8 "time"
9
10 "github.com/mjl-/mox/imapclient"
11)
12
13var searchMsg = strings.ReplaceAll(`Date: Mon, 1 Jan 2022 10:00:00 +0100 (CEST)
14From: mjl <mjl@mox.example>
15Subject: mox
16To: mox <mox@mox.example>
17Cc: <xcc@mox.example>
18Bcc: <bcc@mox.example>
19Reply-To: <noreply@mox.example>
20Message-Id: <123@mox.example>
21MIME-Version: 1.0
22Content-Type: multipart/alternative; boundary=x
23
24--x
25Content-Type: text/plain; charset=utf-8
26
27this is plain text.
28
29--x
30Content-Type: text/html; charset=utf-8
31
32this is html.
33
34--x--
35`, "\n", "\r\n")
36
37func uint32ptr(v uint32) *uint32 {
38 return &v
39}
40
41func (tc *testconn) xsearch(nums ...uint32) {
42 tc.t.Helper()
43
44 tc.xuntagged(imapclient.UntaggedSearch(nums))
45}
46
47func (tc *testconn) xsearchmodseq(modseq int64, nums ...uint32) {
48 tc.t.Helper()
49
50 if len(nums) == 0 {
51 tc.xnountagged()
52 return
53 }
54 tc.xuntagged(imapclient.UntaggedSearchModSeq{Nums: nums, ModSeq: modseq})
55}
56
57func (tc *testconn) xesearch(exp imapclient.UntaggedEsearch) {
58 tc.t.Helper()
59
60 exp.Tag = tc.client.LastTag
61 tc.xuntagged(exp)
62}
63
64func TestSearch(t *testing.T) {
65 tc := start(t)
66 defer tc.close()
67 tc.client.Login("mjl@mox.example", password0)
68 tc.client.Select("inbox")
69
70 // Add 5 and delete first 4 messages. So UIDs start at 5.
71 received := time.Date(2020, time.January, 1, 10, 0, 0, 0, time.UTC)
72 saveDate := time.Now()
73 for range 5 {
74 tc.client.Append("inbox", makeAppendTime(exampleMsg, received))
75 }
76 tc.client.StoreFlagsSet("1:4", true, `\Deleted`)
77 tc.client.Expunge()
78
79 received = time.Date(2022, time.January, 1, 9, 0, 0, 0, time.UTC)
80 tc.client.Append("inbox", makeAppendTime(searchMsg, received))
81
82 received = time.Date(2022, time.January, 1, 9, 0, 0, 0, time.UTC)
83 mostFlags := []string{
84 `\Deleted`,
85 `\Seen`,
86 `\Answered`,
87 `\Flagged`,
88 `\Draft`,
89 `$Forwarded`,
90 `$Junk`,
91 `$Notjunk`,
92 `$Phishing`,
93 `$MDNSent`,
94 `custom1`,
95 `Custom2`,
96 }
97 tc.client.Append("inbox", imapclient.Append{Flags: mostFlags, Received: &received, Size: int64(len(searchMsg)), Data: strings.NewReader(searchMsg)})
98
99 // We now have sequence numbers 1,2,3 and UIDs 5,6,7.
100
101 // We need to be selected. Not the case for ESEARCH command.
102 tc.client.Unselect()
103 tc.transactf("no", "search all")
104 tc.client.Select("inbox")
105
106 tc.transactf("ok", "search all")
107 tc.xsearch(1, 2, 3)
108
109 tc.transactf("ok", "uid search all")
110 tc.xsearch(5, 6, 7)
111
112 tc.transactf("ok", "search answered")
113 tc.xsearch(3)
114
115 tc.transactf("ok", `search bcc "bcc@mox.example"`)
116 tc.xsearch(2, 3)
117
118 tc.transactf("ok", "search before 1-Jan-2038")
119 tc.xsearch(1, 2, 3)
120 tc.transactf("ok", "search before 1-Jan-2020")
121 tc.xsearch() // Before is about received, not date header of message.
122
123 // WITHIN extension with OLDER & YOUNGER.
124 tc.transactf("ok", "search older 60")
125 tc.xsearch(1, 2, 3)
126 tc.transactf("ok", "search younger 60")
127 tc.xsearch()
128
129 // SAVEDATE extension.
130 tc.transactf("ok", "search savedbefore %s", saveDate.Add(24*time.Hour).Format("2-Jan-2006"))
131 tc.xsearch(1, 2, 3)
132 tc.transactf("ok", "search savedbefore %s", saveDate.Add(-24*time.Hour).Format("2-Jan-2006"))
133 tc.xsearch()
134 tc.transactf("ok", "search savedon %s", saveDate.Format("2-Jan-2006"))
135 tc.xsearch(1, 2, 3)
136 tc.transactf("ok", "search savedon %s", saveDate.Add(-24*time.Hour).Format("2-Jan-2006"))
137 tc.xsearch()
138 tc.transactf("ok", "search savedsince %s", saveDate.Add(-24*time.Hour).Format("2-Jan-2006"))
139 tc.xsearch(1, 2, 3)
140 tc.transactf("ok", "search savedsince %s", saveDate.Add(24*time.Hour).Format("2-Jan-2006"))
141 tc.xsearch()
142
143 tc.transactf("ok", `search body "Joe"`)
144 tc.xsearch(1)
145 tc.transactf("ok", `search body "Joe" body "bogus"`)
146 tc.xsearch()
147 tc.transactf("ok", `search body "Joe" text "Blurdybloop"`)
148 tc.xsearch(1)
149 tc.transactf("ok", `search body "Joe" not text "mox"`)
150 tc.xsearch(1)
151 tc.transactf("ok", `search body "Joe" not not body "Joe"`)
152 tc.xsearch(1)
153 tc.transactf("ok", `search body "this is plain text"`)
154 tc.xsearch(2, 3)
155 tc.transactf("ok", `search body "this is html"`)
156 tc.xsearch(2, 3)
157
158 tc.transactf("ok", `search cc "xcc@mox.example"`)
159 tc.xsearch(2, 3)
160
161 tc.transactf("ok", `search deleted`)
162 tc.xsearch(3)
163
164 tc.transactf("ok", `search flagged`)
165 tc.xsearch(3)
166
167 tc.transactf("ok", `search from "foobar@Blurdybloop.example"`)
168 tc.xsearch(1)
169
170 tc.transactf("ok", `search keyword $Forwarded`)
171 tc.xsearch(3)
172
173 tc.transactf("ok", `search keyword Custom1`)
174 tc.xsearch(3)
175
176 tc.transactf("ok", `search keyword custom2`)
177 tc.xsearch(3)
178
179 tc.transactf("ok", `search new`)
180 tc.xsearch() // New requires a message to be recent. We pretend all messages are not recent.
181
182 tc.transactf("ok", `search old`)
183 tc.xsearch(1, 2, 3)
184
185 tc.transactf("ok", `search on 1-Jan-2022`)
186 tc.xsearch(2, 3)
187
188 tc.transactf("ok", `search recent`)
189 tc.xsearch()
190
191 tc.transactf("ok", `search seen`)
192 tc.xsearch(3)
193
194 tc.transactf("ok", `search since 1-Jan-2020`)
195 tc.xsearch(1, 2, 3)
196
197 tc.transactf("ok", `search subject "afternoon"`)
198 tc.xsearch(1)
199
200 tc.transactf("ok", `search text "Joe"`)
201 tc.xsearch(1)
202
203 tc.transactf("ok", `search to "mooch@owatagu.siam.edu.example"`)
204 tc.xsearch(1)
205
206 tc.transactf("ok", `search unanswered`)
207 tc.xsearch(1, 2)
208
209 tc.transactf("ok", `search undeleted`)
210 tc.xsearch(1, 2)
211
212 tc.transactf("ok", `search unflagged`)
213 tc.xsearch(1, 2)
214
215 tc.transactf("ok", `search unkeyword $Junk`)
216 tc.xsearch(1, 2)
217
218 tc.transactf("ok", `search unkeyword custom1`)
219 tc.xsearch(1, 2)
220
221 tc.transactf("ok", `search unseen`)
222 tc.xsearch(1, 2)
223
224 tc.transactf("ok", `search draft`)
225 tc.xsearch(3)
226
227 tc.transactf("ok", `search header "subject" "afternoon"`)
228 tc.xsearch(1)
229
230 tc.transactf("ok", `search larger 1`)
231 tc.xsearch(1, 2, 3)
232
233 tc.transactf("ok", `search not text "mox"`)
234 tc.xsearch(1)
235
236 tc.transactf("ok", `search or seen unseen`)
237 tc.xsearch(1, 2, 3)
238
239 tc.transactf("ok", `search or unseen seen`)
240 tc.xsearch(1, 2, 3)
241
242 tc.transactf("ok", `search sentbefore 8-Feb-1994`)
243 tc.xsearch(1)
244
245 tc.transactf("ok", `search senton 7-Feb-1994`)
246 tc.xsearch(1)
247
248 tc.transactf("ok", `search sentsince 6-Feb-1994`)
249 tc.xsearch(1, 2, 3)
250
251 tc.transactf("ok", `search smaller 9999999`)
252 tc.xsearch(1, 2, 3)
253
254 tc.transactf("ok", `search uid 1`)
255 tc.xsearch()
256
257 tc.transactf("ok", `search uid 5`)
258 tc.xsearch(1)
259
260 tc.transactf("ok", `search or larger 1000000 smaller 1`)
261 tc.xsearch()
262
263 tc.transactf("ok", `search undraft`)
264 tc.xsearch(1, 2)
265
266 tc.transactf("no", `search charset unknown text "mox"`)
267 tc.transactf("ok", `search charset us-ascii text "mox"`)
268 tc.xsearch(2, 3)
269 tc.transactf("ok", `search charset utf-8 text "mox"`)
270 tc.xsearch(2, 3)
271
272 // Check for properly formed INPROGRESS response code.
273 orig := inProgressPeriod
274 inProgressPeriod = 0
275 tc.cmdf("tag1", "search undraft")
276 tc.response("ok")
277
278 inprogress := func(cur, goal uint32) imapclient.UntaggedResult {
279 return imapclient.UntaggedResult{
280 Status: "OK",
281 RespText: imapclient.RespText{
282 Code: "INPROGRESS",
283 CodeArg: imapclient.CodeInProgress{Tag: "tag1", Current: &cur, Goal: &goal},
284 More: "still searching",
285 },
286 }
287 }
288 tc.xuntagged(
289 imapclient.UntaggedSearch([]uint32{1, 2}),
290 // Due to inProgressPeriod 0, we get an inprogress response for each message in the mailbox.
291 inprogress(0, 3),
292 inprogress(1, 3),
293 inprogress(2, 3),
294 )
295 inProgressPeriod = orig
296
297 esearchall := func(ss string) imapclient.UntaggedEsearch {
298 return imapclient.UntaggedEsearch{All: esearchall0(ss)}
299 }
300
301 // Do new-style ESEARCH requests with RETURN. We should get an ESEARCH response.
302 tc.transactf("ok", "search return () all")
303 tc.xesearch(esearchall("1:3")) // Without any options, "ALL" is implicit.
304
305 tc.transactf("ok", "search return (min max count all) all")
306 tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 3, Count: uint32ptr(3), All: esearchall0("1:3")})
307
308 tc.transactf("ok", "UID search return (min max count all) all")
309 tc.xesearch(imapclient.UntaggedEsearch{UID: true, Min: 5, Max: 7, Count: uint32ptr(3), All: esearchall0("5:7")})
310
311 tc.transactf("ok", "search return (min) all")
312 tc.xesearch(imapclient.UntaggedEsearch{Min: 1})
313
314 tc.transactf("ok", "search return (min) 3")
315 tc.xesearch(imapclient.UntaggedEsearch{Min: 3})
316
317 tc.transactf("ok", "search return (min) NOT all")
318 tc.xesearch(imapclient.UntaggedEsearch{}) // Min not present if no match.
319
320 tc.transactf("ok", "search return (max) all")
321 tc.xesearch(imapclient.UntaggedEsearch{Max: 3})
322
323 tc.transactf("ok", "search return (max) 1")
324 tc.xesearch(imapclient.UntaggedEsearch{Max: 1})
325
326 tc.transactf("ok", "search return (max) not all")
327 tc.xesearch(imapclient.UntaggedEsearch{}) // Max not present if no match.
328
329 tc.transactf("ok", "search return (min max) all")
330 tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 3})
331
332 tc.transactf("ok", "search return (min max) 1")
333 tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 1})
334
335 tc.transactf("ok", "search return (min max) not all")
336 tc.xesearch(imapclient.UntaggedEsearch{})
337
338 tc.transactf("ok", "search return (all) not all")
339 tc.xesearch(imapclient.UntaggedEsearch{}) // All not present if no match.
340
341 tc.transactf("ok", "search return (min max all) not all")
342 tc.xesearch(imapclient.UntaggedEsearch{})
343
344 tc.transactf("ok", "search return (min max all count) not all")
345 tc.xesearch(imapclient.UntaggedEsearch{Count: uint32ptr(0)})
346
347 tc.transactf("ok", "search return (min max count all) 1,3")
348 tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 3, Count: uint32ptr(2), All: esearchall0("1,3")})
349
350 tc.transactf("ok", "search return (min max count all) UID 5,7")
351 tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 3, Count: uint32ptr(2), All: esearchall0("1,3")})
352
353 tc.transactf("ok", "uid search return (min max count all) 1,3")
354 tc.xesearch(imapclient.UntaggedEsearch{UID: true, Min: 5, Max: 7, Count: uint32ptr(2), All: esearchall0("5,7")})
355
356 tc.transactf("ok", "uid search return (min max count all) UID 5,7")
357 tc.xesearch(imapclient.UntaggedEsearch{UID: true, Min: 5, Max: 7, Count: uint32ptr(2), All: esearchall0("5,7")})
358
359 tc.transactf("no", `search return () charset unknown text "mox"`)
360 tc.transactf("ok", `search return () charset us-ascii text "mox"`)
361 tc.xesearch(esearchall("2:3"))
362 tc.transactf("ok", `search return () charset utf-8 text "mox"`)
363 tc.xesearch(esearchall("2:3"))
364
365 tc.transactf("bad", `search return (unknown) all`)
366
367 tc.transactf("ok", "search return (save) 2")
368 tc.xnountagged() // ../rfc/9051:3800
369 tc.transactf("ok", "fetch $ (uid)")
370 tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6)}})
371
372 tc.transactf("ok", "search return (all) $")
373 tc.xesearch(esearchall("2"))
374
375 tc.transactf("ok", "search return (save) $")
376 tc.xnountagged()
377
378 tc.transactf("ok", "search return (save all) all")
379 tc.xesearch(esearchall("1:3"))
380
381 tc.transactf("ok", "search return (all save) all")
382 tc.xesearch(esearchall("1:3"))
383
384 tc.transactf("ok", "search return (min save) all")
385 tc.xesearch(imapclient.UntaggedEsearch{Min: 1})
386 tc.transactf("ok", "fetch $ (uid)")
387 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5)}})
388
389 // Do a seemingly old-style search command with IMAP4rev2 enabled. We'll still get ESEARCH responses.
390 tc.client.Enable("IMAP4rev2")
391 tc.transactf("ok", `search undraft`)
392 tc.xesearch(esearchall("1:2"))
393
394 // Long commands should be rejected, not allocating too much memory.
395 lit := make([]byte, 100*1024+1)
396 for i := range lit {
397 lit[i] = 'x'
398 }
399 writeTextLit := func(n int, expok bool) {
400 _, err := fmt.Fprintf(tc.client, " TEXT ")
401 tcheck(t, err, "write text")
402
403 _, err = fmt.Fprintf(tc.client, "{%d}\r\n", n)
404 tcheck(t, err, "write literal size")
405 line, err := tc.client.Readline()
406 tcheck(t, err, "read line")
407 if expok && !strings.HasPrefix(line, "+") {
408 tcheck(t, fmt.Errorf("no continuation after writing size: %s", line), "sending literal")
409 } else if !expok && !strings.HasPrefix(line, "x0 BAD [TOOBIG]") {
410 tcheck(t, fmt.Errorf("got line %s", line), "expected TOOBIG error")
411 }
412 if !expok {
413 return
414 }
415 _, err = tc.client.Write(lit[:n])
416 tcheck(t, err, "write literal data")
417 }
418
419 // More than 100k for a literal.
420 _, err := fmt.Fprintf(tc.client, "x0 uid search")
421 tcheck(t, err, "write start of uit search")
422 writeTextLit(100*1024+1, false)
423
424 // More than 1mb total for literals.
425 _, err = fmt.Fprintf(tc.client, "x0 uid search")
426 tcheck(t, err, "write start of uit search")
427 for range 10 {
428 writeTextLit(100*1024, true)
429 }
430 writeTextLit(1, false)
431
432 // More than 1000 literals.
433 _, err = fmt.Fprintf(tc.client, "x0 uid search")
434 tcheck(t, err, "write start of uit search")
435 for range 1000 {
436 writeTextLit(1, true)
437 }
438 writeTextLit(1, false)
439}
440
441// esearchall makes an UntaggedEsearch response with All set, for comparisons.
442func esearchall0(ss string) imapclient.NumSet {
443 seqset := imapclient.NumSet{}
444 for _, rs := range strings.Split(ss, ",") {
445 t := strings.Split(rs, ":")
446 if len(t) > 2 {
447 panic("bad seqset")
448 }
449 var first uint32
450 var last *uint32
451 if t[0] != "*" {
452 v, err := strconv.ParseUint(t[0], 10, 32)
453 if err != nil {
454 panic("parse first")
455 }
456 first = uint32(v)
457 }
458 if len(t) == 2 {
459 if t[1] != "*" {
460 v, err := strconv.ParseUint(t[1], 10, 32)
461 if err != nil {
462 panic("parse last")
463 }
464 u := uint32(v)
465 last = &u
466 }
467 }
468 seqset.Ranges = append(seqset.Ranges, imapclient.NumRange{First: first, Last: last})
469 }
470 return seqset
471}
472
473// Test the MULTISEARCH extension. Where we don't need to have a mailbox selected,
474// operating without messag sequence numbers, and return untagged esearch responses
475// that include the mailbox and uidvalidity.
476func TestSearchMulti(t *testing.T) {
477 testSearchMulti(t, false)
478 testSearchMulti(t, true)
479}
480
481// Run multisearch tests with or without a mailbox selected.
482func testSearchMulti(t *testing.T, selected bool) {
483 defer mockUIDValidity()()
484
485 tc := start(t)
486 defer tc.close()
487 tc.client.Login("mjl@mox.example", password0)
488 tc.client.Select("inbox")
489
490 // Add 5 messages to Inbox and delete first 4 messages. So UIDs start at 5.
491 received := time.Date(2020, time.January, 1, 10, 0, 0, 0, time.UTC)
492 for range 6 {
493 tc.client.Append("inbox", makeAppendTime(exampleMsg, received))
494 }
495 tc.client.StoreFlagsSet("1:4", true, `\Deleted`)
496 tc.client.Expunge()
497
498 // Unselecting mailbox, esearch works in authenticated state.
499 if !selected {
500 tc.client.Unselect()
501 }
502
503 received = time.Date(2022, time.January, 1, 9, 0, 0, 0, time.UTC)
504 tc.client.Append("inbox", makeAppendTime(searchMsg, received))
505
506 received = time.Date(2022, time.January, 1, 9, 0, 0, 0, time.UTC)
507 mostFlags := []string{
508 `\Deleted`,
509 `\Seen`,
510 `\Answered`,
511 `\Flagged`,
512 `\Draft`,
513 `$Forwarded`,
514 `$Junk`,
515 `$Notjunk`,
516 `$Phishing`,
517 `$MDNSent`,
518 `custom1`,
519 `Custom2`,
520 }
521 tc.client.Append("Archive", imapclient.Append{Flags: mostFlags, Received: &received, Size: int64(len(searchMsg)), Data: strings.NewReader(searchMsg)})
522
523 // We now have sequence numbers 1,2,3 and UIDs 5,6,7 in Inbox, and UID 1 in Archive.
524
525 // Basic esearch with mailboxes.
526 tc.cmdf("Tag1", `Esearch In (Personal) Return () All`)
527 tc.response("ok")
528 tc.xuntagged(
529 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, All: esearchall0("5:7")},
530 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Archive", UIDValidity: 1, UID: true, All: esearchall0("1")},
531 )
532
533 // Again, but with progress information.
534 orig := inProgressPeriod
535 inProgressPeriod = 0
536 inprogress := func(cur, goal uint32) imapclient.UntaggedResult {
537 return imapclient.UntaggedResult{
538 Status: "OK",
539 RespText: imapclient.RespText{
540 Code: "INPROGRESS",
541 CodeArg: imapclient.CodeInProgress{Tag: "Tag1", Current: &cur, Goal: &goal},
542 More: "still searching",
543 },
544 }
545 }
546 tc.cmdf("Tag1", `Esearch In (Personal) Return () All`)
547 tc.response("ok")
548 tc.xuntagged(
549 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, All: esearchall0("5:7")},
550 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Archive", UIDValidity: 1, UID: true, All: esearchall0("1")},
551 inprogress(0, 4),
552 inprogress(1, 4),
553 inprogress(2, 4),
554 inprogress(3, 4),
555 )
556 inProgressPeriod = orig
557
558 // Explicit mailboxes listed, including non-existent one that is ignored,
559 // duplicates are ignored as well.
560 tc.cmdf("Tag1", `Esearch In (Mailboxes (INBOX Archive Archive)) Return (Min Max Count All) All`)
561 tc.response("ok")
562 tc.xuntagged(
563 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Min: 5, Max: 7, Count: uint32ptr(3), All: esearchall0("5:7")},
564 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Archive", UIDValidity: 1, UID: true, Min: 1, Max: 1, Count: uint32ptr(1), All: esearchall0("1")},
565 )
566
567 // No response if none of the mailboxes exist.
568 tc.cmdf("Tag1", `Esearch In (Mailboxes bogus Mailboxes (nonexistent)) Return (Min Max Count All) All`)
569 tc.response("ok")
570 tc.xuntagged()
571
572 // Inboxes evaluates to just inbox on new account. We'll add more mailboxes
573 // matching "inboxes" later on.
574 tc.cmdf("Tag1", `Esearch In (Inboxes) Return () All`)
575 tc.response("ok")
576 tc.xuntagged(
577 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, All: esearchall0("5:7")},
578 )
579
580 // Subscribed is set for created mailboxes by default.
581 tc.cmdf("Tag1", `Esearch In (Subscribed) Return (Max) All`)
582 tc.response("ok")
583 tc.xuntagged(
584 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Max: 7},
585 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Archive", UIDValidity: 1, UID: true, Max: 1},
586 )
587
588 // Asking for max does a reverse search.
589 tc.cmdf("Tag1", `Esearch In (Personal) Return (Max) All`)
590 tc.response("ok")
591 tc.xuntagged(
592 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Max: 7},
593 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Archive", UIDValidity: 1, UID: true, Max: 1},
594 )
595
596 // Min stops early.
597 tc.cmdf("Tag1", `Esearch In (Personal) Return (Min) All`)
598 tc.response("ok")
599 tc.xuntagged(
600 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Min: 5},
601 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Archive", UIDValidity: 1, UID: true, Min: 1},
602 )
603
604 // Min and max do forward and reverse search, stopping early.
605 tc.cmdf("Tag1", `Esearch In (Personal) Return (Min Max) All`)
606 tc.response("ok")
607 tc.xuntagged(
608 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Min: 5, Max: 7},
609 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Archive", UIDValidity: 1, UID: true, Min: 1, Max: 1},
610 )
611
612 if selected {
613 // With only 1 inbox, we can use SAVE with Inboxes. Can't anymore when we have multiple.
614 tc.transactf("ok", `Esearch In (Inboxes) Return (Save) All`)
615 tc.xuntagged()
616
617 // Using search result ($) works with selected mailbox.
618 tc.cmdf("Tag1", `Esearch In (Selected) Return () $`)
619 tc.response("ok")
620 tc.xuntagged(
621 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, All: esearchall0("5:7")},
622 )
623 } else {
624 // Cannot use "selected" if we are not in selected state.
625 tc.transactf("bad", `Esearch In (Selected) Return () All`)
626 }
627
628 // Add more "inboxes", and other mailboxes for testing "subtree" and "subtree-one".
629 more := []string{
630 "Inbox/Sub1",
631 "Inbox/Sub2",
632 "Inbox/Sub2/SubA",
633 "Inbox/Sub2/SubB",
634 "Other",
635 "Other/Sub1", // sub1@mox.example in config.
636 "Other/Sub2",
637 "Other/Sub2/SubA", // ruleset for sub2@mox.example in config.
638 "Other/Sub2/SubB",
639 "List", // ruleset for a mailing list
640 }
641 for _, name := range more {
642 tc.client.Create(name, nil)
643 tc.client.Append(name, makeAppendTime(exampleMsg, received))
644 }
645
646 // Cannot use SAVE with multiple mailboxes that match.
647 tc.transactf("bad", `Esearch In (Inboxes) Return (Save) All`)
648
649 // "inboxes" includes everything below Inbox, and also anything that we might
650 // deliver to based on account addresses and rulesets, but not mailing lists.
651 tc.cmdf("Tag1", `Esearch In (Inboxes) Return () All`)
652 tc.response("ok")
653 tc.xuntagged(
654 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, All: esearchall0("5:7")},
655 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox/Sub1", UIDValidity: 3, UID: true, All: esearchall0("1")},
656 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox/Sub2", UIDValidity: 4, UID: true, All: esearchall0("1")},
657 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox/Sub2/SubA", UIDValidity: 5, UID: true, All: esearchall0("1")},
658 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox/Sub2/SubB", UIDValidity: 6, UID: true, All: esearchall0("1")},
659 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Other/Sub1", UIDValidity: 8, UID: true, All: esearchall0("1")},
660 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Other/Sub2/SubA", UIDValidity: 10, UID: true, All: esearchall0("1")},
661 )
662
663 // subtree
664 tc.cmdf("Tag1", `Esearch In (Subtree Other) Return () All`)
665 tc.response("ok")
666 tc.xuntagged(
667 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Other", UIDValidity: 7, UID: true, All: esearchall0("1")},
668 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Other/Sub1", UIDValidity: 8, UID: true, All: esearchall0("1")},
669 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Other/Sub2", UIDValidity: 9, UID: true, All: esearchall0("1")},
670 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Other/Sub2/SubA", UIDValidity: 10, UID: true, All: esearchall0("1")},
671 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Other/Sub2/SubB", UIDValidity: 11, UID: true, All: esearchall0("1")},
672 )
673
674 // subtree-one
675 tc.cmdf("Tag1", `Esearch In (Subtree-One Other) Return () All`)
676 tc.response("ok")
677 tc.xuntagged(
678 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Other", UIDValidity: 7, UID: true, All: esearchall0("1")},
679 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Other/Sub1", UIDValidity: 8, UID: true, All: esearchall0("1")},
680 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Other/Sub2", UIDValidity: 9, UID: true, All: esearchall0("1")},
681 )
682
683 // Search with sequence set also for non-selected mailboxes(!). The min/max would
684 // get the first and last message, but the message sequence set forces a scan.
685 tc.cmdf("Tag1", `Esearch In (Mailboxes Inbox) Return (Min Max) 1:*`)
686 tc.response("ok")
687 tc.xuntagged(
688 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Min: 5, Max: 7},
689 )
690
691 // Search with uid set with "$highnum:*" forces getting highest uid.
692 tc.cmdf("Tag1", `Esearch In (Mailboxes Inbox) Return (Min Max) Uid *:100`)
693 tc.response("ok")
694 tc.xuntagged(
695 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Min: 7, Max: 7},
696 )
697 tc.cmdf("Tag1", `Esearch In (Mailboxes Inbox) Return (Min Max) Uid 100:*`)
698 tc.response("ok")
699 tc.xuntagged(
700 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Min: 7, Max: 7},
701 )
702 tc.cmdf("Tag1", `Esearch In (Mailboxes Inbox) Return (Min Max) Uid 1:*`)
703 tc.response("ok")
704 tc.xuntagged(
705 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Min: 5, Max: 7},
706 )
707
708 // We use another session to add a new message to Inbox and to Archive. Searching
709 // with Inbox selected will not return the new message since it isn't available in
710 // the session yet. The message in Archive is returned, since there is no session
711 // limitation.
712 tc2 := startNoSwitchboard(t)
713 defer tc2.closeNoWait()
714 tc2.client.Login("mjl@mox.example", password0)
715 tc2.client.Append("inbox", makeAppendTime(searchMsg, received))
716 tc2.client.Append("Archive", makeAppendTime(searchMsg, received))
717
718 tc.cmdf("Tag1", `Esearch In (Mailboxes (Inbox Archive)) Return (Count) All`)
719 tc.response("ok")
720 if selected {
721 tc.xuntagged(
722 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Count: uint32ptr(3)},
723 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Archive", UIDValidity: 1, UID: true, Count: uint32ptr(2)},
724 imapclient.UntaggedExists(4),
725 imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(8), imapclient.FetchFlags(nil)}},
726 )
727 } else {
728 tc.xuntagged(
729 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Count: uint32ptr(4)},
730 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Archive", UIDValidity: 1, UID: true, Count: uint32ptr(2)},
731 )
732 }
733
734 if selected {
735 // Saving a search result, and then using it with another mailbox results in error.
736 tc.transactf("ok", `Esearch In (Mailboxes Inbox) Return (Save) All`)
737 tc.transactf("no", `Esearch In (Mailboxes Archive) Return () $`)
738 } else {
739 tc.transactf("bad", `Esearch In (Inboxes) Return (Save) All`) // Need a selected mailbox with SAVE.
740 tc.transactf("no", `Esearch In (Inboxes) Return () $`) // Cannot use saved result with non-selected mailbox.
741 }
742
743 tc.transactf("bad", `Esearch In () Return () All`) // Missing values for "IN"-list.
744 tc.transactf("bad", `Esearch In (Bogus) Return () All`) // Bogus word for "IN".
745 tc.transactf("bad", `Esearch In ("Selected") Return () All`) // IN-words can't be quoted.
746 tc.transactf("bad", `Esearch In (Selected-Delayed) Return () All`) // From NOTIFY, not in ESEARCH.
747 tc.transactf("bad", `Esearch In (Subtree-One) Return () All`) // After subtree-one we need a list.
748 tc.transactf("bad", `Esearch In (Subtree-One ) Return () All`) // After subtree-one we need a list.
749 tc.transactf("bad", `Esearch In (Subtree-One (Test) ) Return () All`) // Bogus space.
750
751 if !selected {
752 return
753 }
754 // From now on, we are in selected state.
755
756 tc.cmdf("Tag1", `Esearch In (Selected) Return () All`)
757 tc.response("ok")
758 tc.xuntagged(
759 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, All: esearchall0("5:8")},
760 )
761
762 // Testing combinations of SAVE with MIN/MAX/others ../rfc/9051:4100
763 tc.transactf("ok", `Esearch In (Selected) Return (Save) All`)
764 tc.xuntagged()
765
766 tc.cmdf("Tag1", `Esearch In (Selected) Return () $`)
767 tc.response("ok")
768 tc.xuntagged(
769 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, All: esearchall0("5:8")},
770 )
771
772 // Inbox happens to be the selected mailbox, so OK.
773 tc.cmdf("Tag1", `Esearch In (Mailboxes Inbox) Return () $`)
774 tc.response("ok")
775 tc.xuntagged(
776 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, All: esearchall0("5:8")},
777 )
778
779 // Non-selected mailboxes aren't allowed to use the saved result.
780 tc.transactf("no", `Esearch In (Mailboxes Archive) Return () $`)
781 tc.transactf("no", `Esearch In (Mailboxes Archive) Return () uid $`)
782
783 tc.cmdf("Tag1", `Esearch In (Selected) Return (Save Min Max) All`)
784 tc.response("ok")
785 tc.xuntagged(
786 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Min: 5, Max: 8},
787 )
788 tc.cmdf("Tag1", `Esearch In (Selected) Return () $`)
789 tc.response("ok")
790 tc.xuntagged(
791 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, All: esearchall0("5,8")},
792 )
793
794 tc.cmdf("Tag1", `Esearch In (Selected) Return (Save Min) All`)
795 tc.response("ok")
796 tc.xuntagged(
797 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Min: 5},
798 )
799
800 tc.cmdf("Tag1", `Esearch In (Selected) Return () $`)
801 tc.response("ok")
802 tc.xuntagged(
803 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, All: esearchall0("5")},
804 )
805
806 tc.cmdf("Tag1", `Esearch In (Selected) Return (Save Max) All`)
807 tc.response("ok")
808 tc.xuntagged(
809 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Max: 8},
810 )
811
812 tc.cmdf("Tag1", `Esearch In (Selected) Return () $`)
813 tc.response("ok")
814 tc.xuntagged(
815 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, All: esearchall0("8")},
816 )
817
818 tc.cmdf("Tag1", `Esearch In (Selected) Return (Save Min Max Count) All`)
819 tc.response("ok")
820 tc.xuntagged(
821 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Min: 5, Max: 8, Count: uint32ptr(4)},
822 )
823
824 tc.cmdf("Tag1", `Esearch In (Selected) Return () $`)
825 tc.response("ok")
826 tc.xuntagged(
827 imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, All: esearchall0("5:8")},
828 )
829}
830