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