1package imapserver
2
3import (
4 "strconv"
5 "strings"
6 "testing"
7 "time"
8
9 "github.com/mjl-/mox/imapclient"
10)
11
12var searchMsg = strings.ReplaceAll(`Date: Mon, 1 Jan 2022 10:00:00 +0100 (CEST)
13From: mjl <mjl@mox.example>
14Subject: mox
15To: mox <mox@mox.example>
16Cc: <xcc@mox.example>
17Bcc: <bcc@mox.example>
18Reply-To: <noreply@mox.example>
19Message-Id: <123@mox.example>
20MIME-Version: 1.0
21Content-Type: multipart/alternative; boundary=x
22
23--x
24Content-Type: text/plain; charset=utf-8
25
26this is plain text.
27
28--x
29Content-Type: text/html; charset=utf-8
30
31this is html.
32
33--x--
34`, "\n", "\r\n")
35
36func (tc *testconn) xsearch(nums ...uint32) {
37 tc.t.Helper()
38
39 tc.xuntagged(imapclient.UntaggedSearch(nums))
40}
41
42func (tc *testconn) xsearchmodseq(modseq int64, nums ...uint32) {
43 tc.t.Helper()
44
45 if len(nums) == 0 {
46 tc.xnountagged()
47 return
48 }
49 tc.xuntagged(imapclient.UntaggedSearchModSeq{Nums: nums, ModSeq: modseq})
50}
51
52func (tc *testconn) xesearch(exp imapclient.UntaggedEsearch) {
53 tc.t.Helper()
54
55 exp.Correlator = tc.client.LastTag
56 tc.xuntagged(exp)
57}
58
59func TestSearch(t *testing.T) {
60 tc := start(t)
61 defer tc.close()
62 tc.client.Login("mjl@mox.example", "testtest")
63 tc.client.Select("inbox")
64
65 // Add 5 and delete first 4 messages. So UIDs start at 5.
66 received := time.Date(2020, time.January, 1, 10, 0, 0, 0, time.UTC)
67 for i := 0; i < 5; i++ {
68 tc.client.Append("inbox", nil, &received, []byte(exampleMsg))
69 }
70 tc.client.StoreFlagsSet("1:4", true, `\Deleted`)
71 tc.client.Expunge()
72
73 received = time.Date(2022, time.January, 1, 9, 0, 0, 0, time.UTC)
74 tc.client.Append("inbox", nil, &received, []byte(searchMsg))
75
76 received = time.Date(2022, time.January, 1, 9, 0, 0, 0, time.UTC)
77 mostFlags := []string{
78 `\Deleted`,
79 `\Seen`,
80 `\Answered`,
81 `\Flagged`,
82 `\Draft`,
83 `$Forwarded`,
84 `$Junk`,
85 `$Notjunk`,
86 `$Phishing`,
87 `$MDNSent`,
88 `custom1`,
89 `Custom2`,
90 }
91 tc.client.Append("inbox", mostFlags, &received, []byte(searchMsg))
92
93 // We now have sequence numbers 1,2,3 and UIDs 5,6,7.
94
95 tc.transactf("ok", "search all")
96 tc.xsearch(1, 2, 3)
97
98 tc.transactf("ok", "uid search all")
99 tc.xsearch(5, 6, 7)
100
101 tc.transactf("ok", "search answered")
102 tc.xsearch(3)
103
104 tc.transactf("ok", `search bcc "bcc@mox.example"`)
105 tc.xsearch(2, 3)
106
107 tc.transactf("ok", "search before 1-Jan-2038")
108 tc.xsearch(1, 2, 3)
109 tc.transactf("ok", "search before 1-Jan-2020")
110 tc.xsearch() // Before is about received, not date header of message.
111
112 tc.transactf("ok", `search body "Joe"`)
113 tc.xsearch(1)
114 tc.transactf("ok", `search body "Joe" body "bogus"`)
115 tc.xsearch()
116 tc.transactf("ok", `search body "Joe" text "Blurdybloop"`)
117 tc.xsearch(1)
118 tc.transactf("ok", `search body "Joe" not text "mox"`)
119 tc.xsearch(1)
120 tc.transactf("ok", `search body "Joe" not not body "Joe"`)
121 tc.xsearch(1)
122 tc.transactf("ok", `search body "this is plain text"`)
123 tc.xsearch(2, 3)
124 tc.transactf("ok", `search body "this is html"`)
125 tc.xsearch(2, 3)
126
127 tc.transactf("ok", `search cc "xcc@mox.example"`)
128 tc.xsearch(2, 3)
129
130 tc.transactf("ok", `search deleted`)
131 tc.xsearch(3)
132
133 tc.transactf("ok", `search flagged`)
134 tc.xsearch(3)
135
136 tc.transactf("ok", `search from "foobar@Blurdybloop.example"`)
137 tc.xsearch(1)
138
139 tc.transactf("ok", `search keyword $Forwarded`)
140 tc.xsearch(3)
141
142 tc.transactf("ok", `search keyword Custom1`)
143 tc.xsearch(3)
144
145 tc.transactf("ok", `search keyword custom2`)
146 tc.xsearch(3)
147
148 tc.transactf("ok", `search new`)
149 tc.xsearch() // New requires a message to be recent. We pretend all messages are not recent.
150
151 tc.transactf("ok", `search old`)
152 tc.xsearch(1, 2, 3)
153
154 tc.transactf("ok", `search on 1-Jan-2022`)
155 tc.xsearch(2, 3)
156
157 tc.transactf("ok", `search recent`)
158 tc.xsearch()
159
160 tc.transactf("ok", `search seen`)
161 tc.xsearch(3)
162
163 tc.transactf("ok", `search since 1-Jan-2020`)
164 tc.xsearch(1, 2, 3)
165
166 tc.transactf("ok", `search subject "afternoon"`)
167 tc.xsearch(1)
168
169 tc.transactf("ok", `search text "Joe"`)
170 tc.xsearch(1)
171
172 tc.transactf("ok", `search to "mooch@owatagu.siam.edu.example"`)
173 tc.xsearch(1)
174
175 tc.transactf("ok", `search unanswered`)
176 tc.xsearch(1, 2)
177
178 tc.transactf("ok", `search undeleted`)
179 tc.xsearch(1, 2)
180
181 tc.transactf("ok", `search unflagged`)
182 tc.xsearch(1, 2)
183
184 tc.transactf("ok", `search unkeyword $Junk`)
185 tc.xsearch(1, 2)
186
187 tc.transactf("ok", `search unkeyword custom1`)
188 tc.xsearch(1, 2)
189
190 tc.transactf("ok", `search unseen`)
191 tc.xsearch(1, 2)
192
193 tc.transactf("ok", `search draft`)
194 tc.xsearch(3)
195
196 tc.transactf("ok", `search header "subject" "afternoon"`)
197 tc.xsearch(1)
198
199 tc.transactf("ok", `search larger 1`)
200 tc.xsearch(1, 2, 3)
201
202 tc.transactf("ok", `search not text "mox"`)
203 tc.xsearch(1)
204
205 tc.transactf("ok", `search or seen unseen`)
206 tc.xsearch(1, 2, 3)
207
208 tc.transactf("ok", `search or unseen seen`)
209 tc.xsearch(1, 2, 3)
210
211 tc.transactf("ok", `search sentbefore 8-Feb-1994`)
212 tc.xsearch(1)
213
214 tc.transactf("ok", `search senton 7-Feb-1994`)
215 tc.xsearch(1)
216
217 tc.transactf("ok", `search sentsince 6-Feb-1994`)
218 tc.xsearch(1, 2, 3)
219
220 tc.transactf("ok", `search smaller 9999999`)
221 tc.xsearch(1, 2, 3)
222
223 tc.transactf("ok", `search uid 1`)
224 tc.xsearch()
225
226 tc.transactf("ok", `search uid 5`)
227 tc.xsearch(1)
228
229 tc.transactf("ok", `search or larger 1000000 smaller 1`)
230 tc.xsearch()
231
232 tc.transactf("ok", `search undraft`)
233 tc.xsearch(1, 2)
234
235 tc.transactf("no", `search charset unknown text "mox"`)
236 tc.transactf("ok", `search charset us-ascii text "mox"`)
237 tc.xsearch(2, 3)
238 tc.transactf("ok", `search charset utf-8 text "mox"`)
239 tc.xsearch(2, 3)
240
241 esearchall := func(ss string) imapclient.UntaggedEsearch {
242 return imapclient.UntaggedEsearch{All: esearchall0(ss)}
243 }
244
245 uint32ptr := func(v uint32) *uint32 {
246 return &v
247 }
248
249 // Do new-style ESEARCH requests with RETURN. We should get an ESEARCH response.
250 tc.transactf("ok", "search return () all")
251 tc.xesearch(esearchall("1:3")) // Without any options, "ALL" is implicit.
252
253 tc.transactf("ok", "search return (min max count all) all")
254 tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 3, Count: uint32ptr(3), All: esearchall0("1:3")})
255
256 tc.transactf("ok", "UID search return (min max count all) all")
257 tc.xesearch(imapclient.UntaggedEsearch{UID: true, Min: 5, Max: 7, Count: uint32ptr(3), All: esearchall0("5:7")})
258
259 tc.transactf("ok", "search return (min) all")
260 tc.xesearch(imapclient.UntaggedEsearch{Min: 1})
261
262 tc.transactf("ok", "search return (min) 3")
263 tc.xesearch(imapclient.UntaggedEsearch{Min: 3})
264
265 tc.transactf("ok", "search return (min) NOT all")
266 tc.xesearch(imapclient.UntaggedEsearch{}) // Min not present if no match.
267
268 tc.transactf("ok", "search return (max) all")
269 tc.xesearch(imapclient.UntaggedEsearch{Max: 3})
270
271 tc.transactf("ok", "search return (max) 1")
272 tc.xesearch(imapclient.UntaggedEsearch{Max: 1})
273
274 tc.transactf("ok", "search return (max) not all")
275 tc.xesearch(imapclient.UntaggedEsearch{}) // Max not present if no match.
276
277 tc.transactf("ok", "search return (min max) all")
278 tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 3})
279
280 tc.transactf("ok", "search return (min max) 1")
281 tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 1})
282
283 tc.transactf("ok", "search return (min max) not all")
284 tc.xesearch(imapclient.UntaggedEsearch{})
285
286 tc.transactf("ok", "search return (all) not all")
287 tc.xesearch(imapclient.UntaggedEsearch{}) // All not present if no match.
288
289 tc.transactf("ok", "search return (min max all) not all")
290 tc.xesearch(imapclient.UntaggedEsearch{})
291
292 tc.transactf("ok", "search return (min max all count) not all")
293 tc.xesearch(imapclient.UntaggedEsearch{Count: uint32ptr(0)})
294
295 tc.transactf("ok", "search return (min max count all) 1,3")
296 tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 3, Count: uint32ptr(2), All: esearchall0("1,3")})
297
298 tc.transactf("ok", "search return (min max count all) UID 5,7")
299 tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 3, Count: uint32ptr(2), All: esearchall0("1,3")})
300
301 tc.transactf("ok", "uid search return (min max count all) 1,3")
302 tc.xesearch(imapclient.UntaggedEsearch{UID: true, Min: 5, Max: 7, Count: uint32ptr(2), All: esearchall0("5,7")})
303
304 tc.transactf("ok", "uid search return (min max count all) UID 5,7")
305 tc.xesearch(imapclient.UntaggedEsearch{UID: true, Min: 5, Max: 7, Count: uint32ptr(2), All: esearchall0("5,7")})
306
307 tc.transactf("no", `search return () charset unknown text "mox"`)
308 tc.transactf("ok", `search return () charset us-ascii text "mox"`)
309 tc.xesearch(esearchall("2:3"))
310 tc.transactf("ok", `search return () charset utf-8 text "mox"`)
311 tc.xesearch(esearchall("2:3"))
312
313 tc.transactf("bad", `search return (unknown) all`)
314
315 tc.transactf("ok", "search return (save) 2")
316 tc.xnountagged() // ../rfc/9051:3800
317 tc.transactf("ok", "fetch $ (uid)")
318 tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6)}})
319
320 tc.transactf("ok", "search return (all) $")
321 tc.xesearch(esearchall("2"))
322
323 tc.transactf("ok", "search return (save) $")
324 tc.xnountagged()
325
326 tc.transactf("ok", "search return (save all) all")
327 tc.xesearch(esearchall("1:3"))
328
329 tc.transactf("ok", "search return (all save) all")
330 tc.xesearch(esearchall("1:3"))
331
332 tc.transactf("ok", "search return (min save) all")
333 tc.xesearch(imapclient.UntaggedEsearch{Min: 1})
334 tc.transactf("ok", "fetch $ (uid)")
335 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5)}})
336
337 // Do a seemingly old-style search command with IMAP4rev2 enabled. We'll still get ESEARCH responses.
338 tc.client.Enable("IMAP4rev2")
339 tc.transactf("ok", `search undraft`)
340 tc.xesearch(esearchall("1:2"))
341}
342
343// esearchall makes an UntaggedEsearch response with All set, for comparisons.
344func esearchall0(ss string) imapclient.NumSet {
345 seqset := imapclient.NumSet{}
346 for _, rs := range strings.Split(ss, ",") {
347 t := strings.Split(rs, ":")
348 if len(t) > 2 {
349 panic("bad seqset")
350 }
351 var first uint32
352 var last *uint32
353 if t[0] != "*" {
354 v, err := strconv.ParseUint(t[0], 10, 32)
355 if err != nil {
356 panic("parse first")
357 }
358 first = uint32(v)
359 }
360 if len(t) == 2 {
361 if t[1] != "*" {
362 v, err := strconv.ParseUint(t[1], 10, 32)
363 if err != nil {
364 panic("parse last")
365 }
366 u := uint32(v)
367 last = &u
368 }
369 }
370 seqset.Ranges = append(seqset.Ranges, imapclient.NumRange{First: first, Last: last})
371 }
372 return seqset
373}
374