10 "github.com/mjl-/mox/imapclient"
13var searchMsg = strings.ReplaceAll(`Date: Mon, 1 Jan 2022 10:00:00 +0100 (CEST)
14From: mjl <mjl@mox.example>
16To: mox <mox@mox.example>
19Reply-To: <noreply@mox.example>
20Message-Id: <123@mox.example>
22Content-Type: multipart/alternative; boundary=x
25Content-Type: text/plain; charset=utf-8
30Content-Type: text/html; charset=utf-8
37func (tc *testconn) xsearch(nums ...uint32) {
40 tc.xuntagged(imapclient.UntaggedSearch(nums))
43func (tc *testconn) xsearchmodseq(modseq int64, nums ...uint32) {
50 tc.xuntagged(imapclient.UntaggedSearchModSeq{Nums: nums, ModSeq: modseq})
53func (tc *testconn) xesearch(exp imapclient.UntaggedEsearch) {
56 exp.Correlator = tc.client.LastTag
60func TestSearch(t *testing.T) {
63 tc.client.Login("mjl@mox.example", password0)
64 tc.client.Select("inbox")
66 // Add 5 and delete first 4 messages. So UIDs start at 5.
67 received := time.Date(2020, time.January, 1, 10, 0, 0, 0, time.UTC)
68 saveDate := time.Now()
69 for i := 0; i < 5; i++ {
70 tc.client.Append("inbox", nil, &received, []byte(exampleMsg))
72 tc.client.StoreFlagsSet("1:4", true, `\Deleted`)
75 received = time.Date(2022, time.January, 1, 9, 0, 0, 0, time.UTC)
76 tc.client.Append("inbox", nil, &received, []byte(searchMsg))
78 received = time.Date(2022, time.January, 1, 9, 0, 0, 0, time.UTC)
79 mostFlags := []string{
93 tc.client.Append("inbox", mostFlags, &received, []byte(searchMsg))
95 // We now have sequence numbers 1,2,3 and UIDs 5,6,7.
97 tc.transactf("ok", "search all")
100 tc.transactf("ok", "uid search all")
103 tc.transactf("ok", "search answered")
106 tc.transactf("ok", `search bcc "bcc@mox.example"`)
109 tc.transactf("ok", "search before 1-Jan-2038")
111 tc.transactf("ok", "search before 1-Jan-2020")
112 tc.xsearch() // Before is about received, not date header of message.
114 // WITHIN extension with OLDER & YOUNGER.
115 tc.transactf("ok", "search older 60")
117 tc.transactf("ok", "search younger 60")
120 // SAVEDATE extension.
121 tc.transactf("ok", "search savedbefore %s", saveDate.Add(24*time.Hour).Format("2-Jan-2006"))
123 tc.transactf("ok", "search savedbefore %s", saveDate.Add(-24*time.Hour).Format("2-Jan-2006"))
125 tc.transactf("ok", "search savedon %s", saveDate.Format("2-Jan-2006"))
127 tc.transactf("ok", "search savedon %s", saveDate.Add(-24*time.Hour).Format("2-Jan-2006"))
129 tc.transactf("ok", "search savedsince %s", saveDate.Add(-24*time.Hour).Format("2-Jan-2006"))
131 tc.transactf("ok", "search savedsince %s", saveDate.Add(24*time.Hour).Format("2-Jan-2006"))
134 tc.transactf("ok", `search body "Joe"`)
136 tc.transactf("ok", `search body "Joe" body "bogus"`)
138 tc.transactf("ok", `search body "Joe" text "Blurdybloop"`)
140 tc.transactf("ok", `search body "Joe" not text "mox"`)
142 tc.transactf("ok", `search body "Joe" not not body "Joe"`)
144 tc.transactf("ok", `search body "this is plain text"`)
146 tc.transactf("ok", `search body "this is html"`)
149 tc.transactf("ok", `search cc "xcc@mox.example"`)
152 tc.transactf("ok", `search deleted`)
155 tc.transactf("ok", `search flagged`)
158 tc.transactf("ok", `search from "foobar@Blurdybloop.example"`)
161 tc.transactf("ok", `search keyword $Forwarded`)
164 tc.transactf("ok", `search keyword Custom1`)
167 tc.transactf("ok", `search keyword custom2`)
170 tc.transactf("ok", `search new`)
171 tc.xsearch() // New requires a message to be recent. We pretend all messages are not recent.
173 tc.transactf("ok", `search old`)
176 tc.transactf("ok", `search on 1-Jan-2022`)
179 tc.transactf("ok", `search recent`)
182 tc.transactf("ok", `search seen`)
185 tc.transactf("ok", `search since 1-Jan-2020`)
188 tc.transactf("ok", `search subject "afternoon"`)
191 tc.transactf("ok", `search text "Joe"`)
194 tc.transactf("ok", `search to "mooch@owatagu.siam.edu.example"`)
197 tc.transactf("ok", `search unanswered`)
200 tc.transactf("ok", `search undeleted`)
203 tc.transactf("ok", `search unflagged`)
206 tc.transactf("ok", `search unkeyword $Junk`)
209 tc.transactf("ok", `search unkeyword custom1`)
212 tc.transactf("ok", `search unseen`)
215 tc.transactf("ok", `search draft`)
218 tc.transactf("ok", `search header "subject" "afternoon"`)
221 tc.transactf("ok", `search larger 1`)
224 tc.transactf("ok", `search not text "mox"`)
227 tc.transactf("ok", `search or seen unseen`)
230 tc.transactf("ok", `search or unseen seen`)
233 tc.transactf("ok", `search sentbefore 8-Feb-1994`)
236 tc.transactf("ok", `search senton 7-Feb-1994`)
239 tc.transactf("ok", `search sentsince 6-Feb-1994`)
242 tc.transactf("ok", `search smaller 9999999`)
245 tc.transactf("ok", `search uid 1`)
248 tc.transactf("ok", `search uid 5`)
251 tc.transactf("ok", `search or larger 1000000 smaller 1`)
254 tc.transactf("ok", `search undraft`)
257 tc.transactf("no", `search charset unknown text "mox"`)
258 tc.transactf("ok", `search charset us-ascii text "mox"`)
260 tc.transactf("ok", `search charset utf-8 text "mox"`)
263 esearchall := func(ss string) imapclient.UntaggedEsearch {
264 return imapclient.UntaggedEsearch{All: esearchall0(ss)}
267 uint32ptr := func(v uint32) *uint32 {
271 // Do new-style ESEARCH requests with RETURN. We should get an ESEARCH response.
272 tc.transactf("ok", "search return () all")
273 tc.xesearch(esearchall("1:3")) // Without any options, "ALL" is implicit.
275 tc.transactf("ok", "search return (min max count all) all")
276 tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 3, Count: uint32ptr(3), All: esearchall0("1:3")})
278 tc.transactf("ok", "UID search return (min max count all) all")
279 tc.xesearch(imapclient.UntaggedEsearch{UID: true, Min: 5, Max: 7, Count: uint32ptr(3), All: esearchall0("5:7")})
281 tc.transactf("ok", "search return (min) all")
282 tc.xesearch(imapclient.UntaggedEsearch{Min: 1})
284 tc.transactf("ok", "search return (min) 3")
285 tc.xesearch(imapclient.UntaggedEsearch{Min: 3})
287 tc.transactf("ok", "search return (min) NOT all")
288 tc.xesearch(imapclient.UntaggedEsearch{}) // Min not present if no match.
290 tc.transactf("ok", "search return (max) all")
291 tc.xesearch(imapclient.UntaggedEsearch{Max: 3})
293 tc.transactf("ok", "search return (max) 1")
294 tc.xesearch(imapclient.UntaggedEsearch{Max: 1})
296 tc.transactf("ok", "search return (max) not all")
297 tc.xesearch(imapclient.UntaggedEsearch{}) // Max not present if no match.
299 tc.transactf("ok", "search return (min max) all")
300 tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 3})
302 tc.transactf("ok", "search return (min max) 1")
303 tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 1})
305 tc.transactf("ok", "search return (min max) not all")
306 tc.xesearch(imapclient.UntaggedEsearch{})
308 tc.transactf("ok", "search return (all) not all")
309 tc.xesearch(imapclient.UntaggedEsearch{}) // All not present if no match.
311 tc.transactf("ok", "search return (min max all) not all")
312 tc.xesearch(imapclient.UntaggedEsearch{})
314 tc.transactf("ok", "search return (min max all count) not all")
315 tc.xesearch(imapclient.UntaggedEsearch{Count: uint32ptr(0)})
317 tc.transactf("ok", "search return (min max count all) 1,3")
318 tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 3, Count: uint32ptr(2), All: esearchall0("1,3")})
320 tc.transactf("ok", "search return (min max count all) UID 5,7")
321 tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 3, Count: uint32ptr(2), All: esearchall0("1,3")})
323 tc.transactf("ok", "uid search return (min max count all) 1,3")
324 tc.xesearch(imapclient.UntaggedEsearch{UID: true, Min: 5, Max: 7, Count: uint32ptr(2), All: esearchall0("5,7")})
326 tc.transactf("ok", "uid search return (min max count all) UID 5,7")
327 tc.xesearch(imapclient.UntaggedEsearch{UID: true, Min: 5, Max: 7, Count: uint32ptr(2), All: esearchall0("5,7")})
329 tc.transactf("no", `search return () charset unknown text "mox"`)
330 tc.transactf("ok", `search return () charset us-ascii text "mox"`)
331 tc.xesearch(esearchall("2:3"))
332 tc.transactf("ok", `search return () charset utf-8 text "mox"`)
333 tc.xesearch(esearchall("2:3"))
335 tc.transactf("bad", `search return (unknown) all`)
337 tc.transactf("ok", "search return (save) 2")
339 tc.transactf("ok", "fetch $ (uid)")
340 tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6)}})
342 tc.transactf("ok", "search return (all) $")
343 tc.xesearch(esearchall("2"))
345 tc.transactf("ok", "search return (save) $")
348 tc.transactf("ok", "search return (save all) all")
349 tc.xesearch(esearchall("1:3"))
351 tc.transactf("ok", "search return (all save) all")
352 tc.xesearch(esearchall("1:3"))
354 tc.transactf("ok", "search return (min save) all")
355 tc.xesearch(imapclient.UntaggedEsearch{Min: 1})
356 tc.transactf("ok", "fetch $ (uid)")
357 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5)}})
359 // Do a seemingly old-style search command with IMAP4rev2 enabled. We'll still get ESEARCH responses.
360 tc.client.Enable("IMAP4rev2")
361 tc.transactf("ok", `search undraft`)
362 tc.xesearch(esearchall("1:2"))
364 // Long commands should be rejected, not allocating too much memory.
365 lit := make([]byte, 100*1024+1)
369 writeTextLit := func(n int, expok bool) {
370 _, err := fmt.Fprintf(tc.client, " TEXT ")
371 tcheck(t, err, "write text")
373 _, err = fmt.Fprintf(tc.client, "{%d}\r\n", n)
374 tcheck(t, err, "write literal size")
375 line, err := tc.client.Readline()
376 tcheck(t, err, "read line")
377 if expok && !strings.HasPrefix(line, "+") {
378 tcheck(t, fmt.Errorf("no continuation after writing size: %s", line), "sending literal")
379 } else if !expok && !strings.HasPrefix(line, "x0 BAD [TOOBIG]") {
380 tcheck(t, fmt.Errorf("got line %s", line), "expected TOOBIG error")
385 _, err = tc.client.Write(lit[:n])
386 tcheck(t, err, "write literal data")
389 // More than 100k for a literal.
390 _, err := fmt.Fprintf(tc.client, "x0 uid search")
391 tcheck(t, err, "write start of uit search")
392 writeTextLit(100*1024+1, false)
394 // More than 1mb total for literals.
395 _, err = fmt.Fprintf(tc.client, "x0 uid search")
396 tcheck(t, err, "write start of uit search")
397 for i := 0; i < 10; i++ {
398 writeTextLit(100*1024, true)
400 writeTextLit(1, false)
402 // More than 1000 literals.
403 _, err = fmt.Fprintf(tc.client, "x0 uid search")
404 tcheck(t, err, "write start of uit search")
405 for i := 0; i < 1000; i++ {
406 writeTextLit(1, true)
408 writeTextLit(1, false)
412// esearchall makes an UntaggedEsearch response with All set, for comparisons.
413func esearchall0(ss string) imapclient.NumSet {
414 seqset := imapclient.NumSet{}
415 for _, rs := range strings.Split(ss, ",") {
416 t := strings.Split(rs, ":")
423 v, err := strconv.ParseUint(t[0], 10, 32)
431 v, err := strconv.ParseUint(t[1], 10, 32)
439 seqset.Ranges = append(seqset.Ranges, imapclient.NumRange{First: first, Last: last})