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 for i := 0; i < 5; i++ {
69 tc.client.Append("inbox", nil, &received, []byte(exampleMsg))
70 }
71 tc.client.StoreFlagsSet("1:4", true, `\Deleted`)
72 tc.client.Expunge()
73
74 received = time.Date(2022, time.January, 1, 9, 0, 0, 0, time.UTC)
75 tc.client.Append("inbox", nil, &received, []byte(searchMsg))
76
77 received = time.Date(2022, time.January, 1, 9, 0, 0, 0, time.UTC)
78 mostFlags := []string{
79 `\Deleted`,
80 `\Seen`,
81 `\Answered`,
82 `\Flagged`,
83 `\Draft`,
84 `$Forwarded`,
85 `$Junk`,
86 `$Notjunk`,
87 `$Phishing`,
88 `$MDNSent`,
89 `custom1`,
90 `Custom2`,
91 }
92 tc.client.Append("inbox", mostFlags, &received, []byte(searchMsg))
93
94 // We now have sequence numbers 1,2,3 and UIDs 5,6,7.
95
96 tc.transactf("ok", "search all")
97 tc.xsearch(1, 2, 3)
98
99 tc.transactf("ok", "uid search all")
100 tc.xsearch(5, 6, 7)
101
102 tc.transactf("ok", "search answered")
103 tc.xsearch(3)
104
105 tc.transactf("ok", `search bcc "bcc@mox.example"`)
106 tc.xsearch(2, 3)
107
108 tc.transactf("ok", "search before 1-Jan-2038")
109 tc.xsearch(1, 2, 3)
110 tc.transactf("ok", "search before 1-Jan-2020")
111 tc.xsearch() // Before is about received, not date header of message.
112
113 tc.transactf("ok", `search body "Joe"`)
114 tc.xsearch(1)
115 tc.transactf("ok", `search body "Joe" body "bogus"`)
116 tc.xsearch()
117 tc.transactf("ok", `search body "Joe" text "Blurdybloop"`)
118 tc.xsearch(1)
119 tc.transactf("ok", `search body "Joe" not text "mox"`)
120 tc.xsearch(1)
121 tc.transactf("ok", `search body "Joe" not not body "Joe"`)
122 tc.xsearch(1)
123 tc.transactf("ok", `search body "this is plain text"`)
124 tc.xsearch(2, 3)
125 tc.transactf("ok", `search body "this is html"`)
126 tc.xsearch(2, 3)
127
128 tc.transactf("ok", `search cc "xcc@mox.example"`)
129 tc.xsearch(2, 3)
130
131 tc.transactf("ok", `search deleted`)
132 tc.xsearch(3)
133
134 tc.transactf("ok", `search flagged`)
135 tc.xsearch(3)
136
137 tc.transactf("ok", `search from "foobar@Blurdybloop.example"`)
138 tc.xsearch(1)
139
140 tc.transactf("ok", `search keyword $Forwarded`)
141 tc.xsearch(3)
142
143 tc.transactf("ok", `search keyword Custom1`)
144 tc.xsearch(3)
145
146 tc.transactf("ok", `search keyword custom2`)
147 tc.xsearch(3)
148
149 tc.transactf("ok", `search new`)
150 tc.xsearch() // New requires a message to be recent. We pretend all messages are not recent.
151
152 tc.transactf("ok", `search old`)
153 tc.xsearch(1, 2, 3)
154
155 tc.transactf("ok", `search on 1-Jan-2022`)
156 tc.xsearch(2, 3)
157
158 tc.transactf("ok", `search recent`)
159 tc.xsearch()
160
161 tc.transactf("ok", `search seen`)
162 tc.xsearch(3)
163
164 tc.transactf("ok", `search since 1-Jan-2020`)
165 tc.xsearch(1, 2, 3)
166
167 tc.transactf("ok", `search subject "afternoon"`)
168 tc.xsearch(1)
169
170 tc.transactf("ok", `search text "Joe"`)
171 tc.xsearch(1)
172
173 tc.transactf("ok", `search to "mooch@owatagu.siam.edu.example"`)
174 tc.xsearch(1)
175
176 tc.transactf("ok", `search unanswered`)
177 tc.xsearch(1, 2)
178
179 tc.transactf("ok", `search undeleted`)
180 tc.xsearch(1, 2)
181
182 tc.transactf("ok", `search unflagged`)
183 tc.xsearch(1, 2)
184
185 tc.transactf("ok", `search unkeyword $Junk`)
186 tc.xsearch(1, 2)
187
188 tc.transactf("ok", `search unkeyword custom1`)
189 tc.xsearch(1, 2)
190
191 tc.transactf("ok", `search unseen`)
192 tc.xsearch(1, 2)
193
194 tc.transactf("ok", `search draft`)
195 tc.xsearch(3)
196
197 tc.transactf("ok", `search header "subject" "afternoon"`)
198 tc.xsearch(1)
199
200 tc.transactf("ok", `search larger 1`)
201 tc.xsearch(1, 2, 3)
202
203 tc.transactf("ok", `search not text "mox"`)
204 tc.xsearch(1)
205
206 tc.transactf("ok", `search or seen unseen`)
207 tc.xsearch(1, 2, 3)
208
209 tc.transactf("ok", `search or unseen seen`)
210 tc.xsearch(1, 2, 3)
211
212 tc.transactf("ok", `search sentbefore 8-Feb-1994`)
213 tc.xsearch(1)
214
215 tc.transactf("ok", `search senton 7-Feb-1994`)
216 tc.xsearch(1)
217
218 tc.transactf("ok", `search sentsince 6-Feb-1994`)
219 tc.xsearch(1, 2, 3)
220
221 tc.transactf("ok", `search smaller 9999999`)
222 tc.xsearch(1, 2, 3)
223
224 tc.transactf("ok", `search uid 1`)
225 tc.xsearch()
226
227 tc.transactf("ok", `search uid 5`)
228 tc.xsearch(1)
229
230 tc.transactf("ok", `search or larger 1000000 smaller 1`)
231 tc.xsearch()
232
233 tc.transactf("ok", `search undraft`)
234 tc.xsearch(1, 2)
235
236 tc.transactf("no", `search charset unknown text "mox"`)
237 tc.transactf("ok", `search charset us-ascii text "mox"`)
238 tc.xsearch(2, 3)
239 tc.transactf("ok", `search charset utf-8 text "mox"`)
240 tc.xsearch(2, 3)
241
242 esearchall := func(ss string) imapclient.UntaggedEsearch {
243 return imapclient.UntaggedEsearch{All: esearchall0(ss)}
244 }
245
246 uint32ptr := func(v uint32) *uint32 {
247 return &v
248 }
249
250 // Do new-style ESEARCH requests with RETURN. We should get an ESEARCH response.
251 tc.transactf("ok", "search return () all")
252 tc.xesearch(esearchall("1:3")) // Without any options, "ALL" is implicit.
253
254 tc.transactf("ok", "search return (min max count all) all")
255 tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 3, Count: uint32ptr(3), All: esearchall0("1:3")})
256
257 tc.transactf("ok", "UID search return (min max count all) all")
258 tc.xesearch(imapclient.UntaggedEsearch{UID: true, Min: 5, Max: 7, Count: uint32ptr(3), All: esearchall0("5:7")})
259
260 tc.transactf("ok", "search return (min) all")
261 tc.xesearch(imapclient.UntaggedEsearch{Min: 1})
262
263 tc.transactf("ok", "search return (min) 3")
264 tc.xesearch(imapclient.UntaggedEsearch{Min: 3})
265
266 tc.transactf("ok", "search return (min) NOT all")
267 tc.xesearch(imapclient.UntaggedEsearch{}) // Min not present if no match.
268
269 tc.transactf("ok", "search return (max) all")
270 tc.xesearch(imapclient.UntaggedEsearch{Max: 3})
271
272 tc.transactf("ok", "search return (max) 1")
273 tc.xesearch(imapclient.UntaggedEsearch{Max: 1})
274
275 tc.transactf("ok", "search return (max) not all")
276 tc.xesearch(imapclient.UntaggedEsearch{}) // Max not present if no match.
277
278 tc.transactf("ok", "search return (min max) all")
279 tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 3})
280
281 tc.transactf("ok", "search return (min max) 1")
282 tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 1})
283
284 tc.transactf("ok", "search return (min max) not all")
285 tc.xesearch(imapclient.UntaggedEsearch{})
286
287 tc.transactf("ok", "search return (all) not all")
288 tc.xesearch(imapclient.UntaggedEsearch{}) // All not present if no match.
289
290 tc.transactf("ok", "search return (min max all) not all")
291 tc.xesearch(imapclient.UntaggedEsearch{})
292
293 tc.transactf("ok", "search return (min max all count) not all")
294 tc.xesearch(imapclient.UntaggedEsearch{Count: uint32ptr(0)})
295
296 tc.transactf("ok", "search return (min max count all) 1,3")
297 tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 3, Count: uint32ptr(2), All: esearchall0("1,3")})
298
299 tc.transactf("ok", "search return (min max count all) UID 5,7")
300 tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 3, Count: uint32ptr(2), All: esearchall0("1,3")})
301
302 tc.transactf("ok", "uid search return (min max count all) 1,3")
303 tc.xesearch(imapclient.UntaggedEsearch{UID: true, Min: 5, Max: 7, Count: uint32ptr(2), All: esearchall0("5,7")})
304
305 tc.transactf("ok", "uid search return (min max count all) UID 5,7")
306 tc.xesearch(imapclient.UntaggedEsearch{UID: true, Min: 5, Max: 7, Count: uint32ptr(2), All: esearchall0("5,7")})
307
308 tc.transactf("no", `search return () charset unknown text "mox"`)
309 tc.transactf("ok", `search return () charset us-ascii text "mox"`)
310 tc.xesearch(esearchall("2:3"))
311 tc.transactf("ok", `search return () charset utf-8 text "mox"`)
312 tc.xesearch(esearchall("2:3"))
313
314 tc.transactf("bad", `search return (unknown) all`)
315
316 tc.transactf("ok", "search return (save) 2")
317 tc.xnountagged() // ../rfc/9051:3800
318 tc.transactf("ok", "fetch $ (uid)")
319 tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6)}})
320
321 tc.transactf("ok", "search return (all) $")
322 tc.xesearch(esearchall("2"))
323
324 tc.transactf("ok", "search return (save) $")
325 tc.xnountagged()
326
327 tc.transactf("ok", "search return (save all) all")
328 tc.xesearch(esearchall("1:3"))
329
330 tc.transactf("ok", "search return (all save) all")
331 tc.xesearch(esearchall("1:3"))
332
333 tc.transactf("ok", "search return (min save) all")
334 tc.xesearch(imapclient.UntaggedEsearch{Min: 1})
335 tc.transactf("ok", "fetch $ (uid)")
336 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5)}})
337
338 // Do a seemingly old-style search command with IMAP4rev2 enabled. We'll still get ESEARCH responses.
339 tc.client.Enable("IMAP4rev2")
340 tc.transactf("ok", `search undraft`)
341 tc.xesearch(esearchall("1:2"))
342
343 // Long commands should be rejected, not allocating too much memory.
344 lit := make([]byte, 100*1024+1)
345 for i := range lit {
346 lit[i] = 'x'
347 }
348 writeTextLit := func(n int, expok bool) {
349 _, err := fmt.Fprintf(tc.client, " TEXT ")
350 tcheck(t, err, "write text")
351
352 _, err = fmt.Fprintf(tc.client, "{%d}\r\n", n)
353 tcheck(t, err, "write literal size")
354 line, err := tc.client.Readline()
355 tcheck(t, err, "read line")
356 if expok && !strings.HasPrefix(line, "+") {
357 tcheck(t, fmt.Errorf("no continuation after writing size: %s", line), "sending literal")
358 } else if !expok && !strings.HasPrefix(line, "x0 BAD [TOOBIG]") {
359 tcheck(t, fmt.Errorf("got line %s", line), "expected TOOBIG error")
360 }
361 if !expok {
362 return
363 }
364 _, err = tc.client.Write(lit[:n])
365 tcheck(t, err, "write literal data")
366 }
367
368 // More than 100k for a literal.
369 _, err := fmt.Fprintf(tc.client, "x0 uid search")
370 tcheck(t, err, "write start of uit search")
371 writeTextLit(100*1024+1, false)
372
373 // More than 1mb total for literals.
374 _, err = fmt.Fprintf(tc.client, "x0 uid search")
375 tcheck(t, err, "write start of uit search")
376 for i := 0; i < 10; i++ {
377 writeTextLit(100*1024, true)
378 }
379 writeTextLit(1, false)
380
381 // More than 1000 literals.
382 _, err = fmt.Fprintf(tc.client, "x0 uid search")
383 tcheck(t, err, "write start of uit search")
384 for i := 0; i < 1000; i++ {
385 writeTextLit(1, true)
386 }
387 writeTextLit(1, false)
388
389}
390
391// esearchall makes an UntaggedEsearch response with All set, for comparisons.
392func esearchall0(ss string) imapclient.NumSet {
393 seqset := imapclient.NumSet{}
394 for _, rs := range strings.Split(ss, ",") {
395 t := strings.Split(rs, ":")
396 if len(t) > 2 {
397 panic("bad seqset")
398 }
399 var first uint32
400 var last *uint32
401 if t[0] != "*" {
402 v, err := strconv.ParseUint(t[0], 10, 32)
403 if err != nil {
404 panic("parse first")
405 }
406 first = uint32(v)
407 }
408 if len(t) == 2 {
409 if t[1] != "*" {
410 v, err := strconv.ParseUint(t[1], 10, 32)
411 if err != nil {
412 panic("parse last")
413 }
414 u := uint32(v)
415 last = &u
416 }
417 }
418 seqset.Ranges = append(seqset.Ranges, imapclient.NumRange{First: first, Last: last})
419 }
420 return seqset
421}
422