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 (tc *testconn) xsearch(nums ...uint32) {
38 tc.t.Helper()
39
40 tc.xuntagged(imapclient.UntaggedSearch(nums))
41}
42
43func (tc *testconn) xsearchmodseq(modseq int64, nums ...uint32) {
44 tc.t.Helper()
45
46 if len(nums) == 0 {
47 tc.xnountagged()
48 return
49 }
50 tc.xuntagged(imapclient.UntaggedSearchModSeq{Nums: nums, ModSeq: modseq})
51}
52
53func (tc *testconn) xesearch(exp imapclient.UntaggedEsearch) {
54 tc.t.Helper()
55
56 exp.Correlator = tc.client.LastTag
57 tc.xuntagged(exp)
58}
59
60func TestSearch(t *testing.T) {
61 tc := start(t)
62 defer tc.close()
63 tc.client.Login("mjl@mox.example", password0)
64 tc.client.Select("inbox")
65
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))
71 }
72 tc.client.StoreFlagsSet("1:4", true, `\Deleted`)
73 tc.client.Expunge()
74
75 received = time.Date(2022, time.January, 1, 9, 0, 0, 0, time.UTC)
76 tc.client.Append("inbox", nil, &received, []byte(searchMsg))
77
78 received = time.Date(2022, time.January, 1, 9, 0, 0, 0, time.UTC)
79 mostFlags := []string{
80 `\Deleted`,
81 `\Seen`,
82 `\Answered`,
83 `\Flagged`,
84 `\Draft`,
85 `$Forwarded`,
86 `$Junk`,
87 `$Notjunk`,
88 `$Phishing`,
89 `$MDNSent`,
90 `custom1`,
91 `Custom2`,
92 }
93 tc.client.Append("inbox", mostFlags, &received, []byte(searchMsg))
94
95 // We now have sequence numbers 1,2,3 and UIDs 5,6,7.
96
97 tc.transactf("ok", "search all")
98 tc.xsearch(1, 2, 3)
99
100 tc.transactf("ok", "uid search all")
101 tc.xsearch(5, 6, 7)
102
103 tc.transactf("ok", "search answered")
104 tc.xsearch(3)
105
106 tc.transactf("ok", `search bcc "bcc@mox.example"`)
107 tc.xsearch(2, 3)
108
109 tc.transactf("ok", "search before 1-Jan-2038")
110 tc.xsearch(1, 2, 3)
111 tc.transactf("ok", "search before 1-Jan-2020")
112 tc.xsearch() // Before is about received, not date header of message.
113
114 // WITHIN extension with OLDER & YOUNGER.
115 tc.transactf("ok", "search older 60")
116 tc.xsearch(1, 2, 3)
117 tc.transactf("ok", "search younger 60")
118 tc.xsearch()
119
120 // SAVEDATE extension.
121 tc.transactf("ok", "search savedbefore %s", saveDate.Add(24*time.Hour).Format("2-Jan-2006"))
122 tc.xsearch(1, 2, 3)
123 tc.transactf("ok", "search savedbefore %s", saveDate.Add(-24*time.Hour).Format("2-Jan-2006"))
124 tc.xsearch()
125 tc.transactf("ok", "search savedon %s", saveDate.Format("2-Jan-2006"))
126 tc.xsearch(1, 2, 3)
127 tc.transactf("ok", "search savedon %s", saveDate.Add(-24*time.Hour).Format("2-Jan-2006"))
128 tc.xsearch()
129 tc.transactf("ok", "search savedsince %s", saveDate.Add(-24*time.Hour).Format("2-Jan-2006"))
130 tc.xsearch(1, 2, 3)
131 tc.transactf("ok", "search savedsince %s", saveDate.Add(24*time.Hour).Format("2-Jan-2006"))
132 tc.xsearch()
133
134 tc.transactf("ok", `search body "Joe"`)
135 tc.xsearch(1)
136 tc.transactf("ok", `search body "Joe" body "bogus"`)
137 tc.xsearch()
138 tc.transactf("ok", `search body "Joe" text "Blurdybloop"`)
139 tc.xsearch(1)
140 tc.transactf("ok", `search body "Joe" not text "mox"`)
141 tc.xsearch(1)
142 tc.transactf("ok", `search body "Joe" not not body "Joe"`)
143 tc.xsearch(1)
144 tc.transactf("ok", `search body "this is plain text"`)
145 tc.xsearch(2, 3)
146 tc.transactf("ok", `search body "this is html"`)
147 tc.xsearch(2, 3)
148
149 tc.transactf("ok", `search cc "xcc@mox.example"`)
150 tc.xsearch(2, 3)
151
152 tc.transactf("ok", `search deleted`)
153 tc.xsearch(3)
154
155 tc.transactf("ok", `search flagged`)
156 tc.xsearch(3)
157
158 tc.transactf("ok", `search from "foobar@Blurdybloop.example"`)
159 tc.xsearch(1)
160
161 tc.transactf("ok", `search keyword $Forwarded`)
162 tc.xsearch(3)
163
164 tc.transactf("ok", `search keyword Custom1`)
165 tc.xsearch(3)
166
167 tc.transactf("ok", `search keyword custom2`)
168 tc.xsearch(3)
169
170 tc.transactf("ok", `search new`)
171 tc.xsearch() // New requires a message to be recent. We pretend all messages are not recent.
172
173 tc.transactf("ok", `search old`)
174 tc.xsearch(1, 2, 3)
175
176 tc.transactf("ok", `search on 1-Jan-2022`)
177 tc.xsearch(2, 3)
178
179 tc.transactf("ok", `search recent`)
180 tc.xsearch()
181
182 tc.transactf("ok", `search seen`)
183 tc.xsearch(3)
184
185 tc.transactf("ok", `search since 1-Jan-2020`)
186 tc.xsearch(1, 2, 3)
187
188 tc.transactf("ok", `search subject "afternoon"`)
189 tc.xsearch(1)
190
191 tc.transactf("ok", `search text "Joe"`)
192 tc.xsearch(1)
193
194 tc.transactf("ok", `search to "mooch@owatagu.siam.edu.example"`)
195 tc.xsearch(1)
196
197 tc.transactf("ok", `search unanswered`)
198 tc.xsearch(1, 2)
199
200 tc.transactf("ok", `search undeleted`)
201 tc.xsearch(1, 2)
202
203 tc.transactf("ok", `search unflagged`)
204 tc.xsearch(1, 2)
205
206 tc.transactf("ok", `search unkeyword $Junk`)
207 tc.xsearch(1, 2)
208
209 tc.transactf("ok", `search unkeyword custom1`)
210 tc.xsearch(1, 2)
211
212 tc.transactf("ok", `search unseen`)
213 tc.xsearch(1, 2)
214
215 tc.transactf("ok", `search draft`)
216 tc.xsearch(3)
217
218 tc.transactf("ok", `search header "subject" "afternoon"`)
219 tc.xsearch(1)
220
221 tc.transactf("ok", `search larger 1`)
222 tc.xsearch(1, 2, 3)
223
224 tc.transactf("ok", `search not text "mox"`)
225 tc.xsearch(1)
226
227 tc.transactf("ok", `search or seen unseen`)
228 tc.xsearch(1, 2, 3)
229
230 tc.transactf("ok", `search or unseen seen`)
231 tc.xsearch(1, 2, 3)
232
233 tc.transactf("ok", `search sentbefore 8-Feb-1994`)
234 tc.xsearch(1)
235
236 tc.transactf("ok", `search senton 7-Feb-1994`)
237 tc.xsearch(1)
238
239 tc.transactf("ok", `search sentsince 6-Feb-1994`)
240 tc.xsearch(1, 2, 3)
241
242 tc.transactf("ok", `search smaller 9999999`)
243 tc.xsearch(1, 2, 3)
244
245 tc.transactf("ok", `search uid 1`)
246 tc.xsearch()
247
248 tc.transactf("ok", `search uid 5`)
249 tc.xsearch(1)
250
251 tc.transactf("ok", `search or larger 1000000 smaller 1`)
252 tc.xsearch()
253
254 tc.transactf("ok", `search undraft`)
255 tc.xsearch(1, 2)
256
257 tc.transactf("no", `search charset unknown text "mox"`)
258 tc.transactf("ok", `search charset us-ascii text "mox"`)
259 tc.xsearch(2, 3)
260 tc.transactf("ok", `search charset utf-8 text "mox"`)
261 tc.xsearch(2, 3)
262
263 esearchall := func(ss string) imapclient.UntaggedEsearch {
264 return imapclient.UntaggedEsearch{All: esearchall0(ss)}
265 }
266
267 uint32ptr := func(v uint32) *uint32 {
268 return &v
269 }
270
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.
274
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")})
277
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")})
280
281 tc.transactf("ok", "search return (min) all")
282 tc.xesearch(imapclient.UntaggedEsearch{Min: 1})
283
284 tc.transactf("ok", "search return (min) 3")
285 tc.xesearch(imapclient.UntaggedEsearch{Min: 3})
286
287 tc.transactf("ok", "search return (min) NOT all")
288 tc.xesearch(imapclient.UntaggedEsearch{}) // Min not present if no match.
289
290 tc.transactf("ok", "search return (max) all")
291 tc.xesearch(imapclient.UntaggedEsearch{Max: 3})
292
293 tc.transactf("ok", "search return (max) 1")
294 tc.xesearch(imapclient.UntaggedEsearch{Max: 1})
295
296 tc.transactf("ok", "search return (max) not all")
297 tc.xesearch(imapclient.UntaggedEsearch{}) // Max not present if no match.
298
299 tc.transactf("ok", "search return (min max) all")
300 tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 3})
301
302 tc.transactf("ok", "search return (min max) 1")
303 tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 1})
304
305 tc.transactf("ok", "search return (min max) not all")
306 tc.xesearch(imapclient.UntaggedEsearch{})
307
308 tc.transactf("ok", "search return (all) not all")
309 tc.xesearch(imapclient.UntaggedEsearch{}) // All not present if no match.
310
311 tc.transactf("ok", "search return (min max all) not all")
312 tc.xesearch(imapclient.UntaggedEsearch{})
313
314 tc.transactf("ok", "search return (min max all count) not all")
315 tc.xesearch(imapclient.UntaggedEsearch{Count: uint32ptr(0)})
316
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")})
319
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")})
322
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")})
325
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")})
328
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"))
334
335 tc.transactf("bad", `search return (unknown) all`)
336
337 tc.transactf("ok", "search return (save) 2")
338 tc.xnountagged() // ../rfc/9051:3800
339 tc.transactf("ok", "fetch $ (uid)")
340 tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6)}})
341
342 tc.transactf("ok", "search return (all) $")
343 tc.xesearch(esearchall("2"))
344
345 tc.transactf("ok", "search return (save) $")
346 tc.xnountagged()
347
348 tc.transactf("ok", "search return (save all) all")
349 tc.xesearch(esearchall("1:3"))
350
351 tc.transactf("ok", "search return (all save) all")
352 tc.xesearch(esearchall("1:3"))
353
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)}})
358
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"))
363
364 // Long commands should be rejected, not allocating too much memory.
365 lit := make([]byte, 100*1024+1)
366 for i := range lit {
367 lit[i] = 'x'
368 }
369 writeTextLit := func(n int, expok bool) {
370 _, err := fmt.Fprintf(tc.client, " TEXT ")
371 tcheck(t, err, "write text")
372
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")
381 }
382 if !expok {
383 return
384 }
385 _, err = tc.client.Write(lit[:n])
386 tcheck(t, err, "write literal data")
387 }
388
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)
393
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)
399 }
400 writeTextLit(1, false)
401
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)
407 }
408 writeTextLit(1, false)
409
410}
411
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, ":")
417 if len(t) > 2 {
418 panic("bad seqset")
419 }
420 var first uint32
421 var last *uint32
422 if t[0] != "*" {
423 v, err := strconv.ParseUint(t[0], 10, 32)
424 if err != nil {
425 panic("parse first")
426 }
427 first = uint32(v)
428 }
429 if len(t) == 2 {
430 if t[1] != "*" {
431 v, err := strconv.ParseUint(t[1], 10, 32)
432 if err != nil {
433 panic("parse last")
434 }
435 u := uint32(v)
436 last = &u
437 }
438 }
439 seqset.Ranges = append(seqset.Ranges, imapclient.NumRange{First: first, Last: last})
440 }
441 return seqset
442}
443