1package imapserver
2
3import (
4 "cmp"
5 "fmt"
6 "log/slog"
7 "maps"
8 "net/textproto"
9 "slices"
10 "strings"
11 "time"
12
13 "github.com/mjl-/bstore"
14
15 "github.com/mjl-/mox/message"
16 "github.com/mjl-/mox/store"
17)
18
19// If last search output was this long ago, we write an untagged inprogress
20// response. Changed during tests. ../rfc/9585:109
21var inProgressPeriod = time.Duration(10 * time.Second)
22
23// ESEARCH allows searching multiple mailboxes, referenced through mailbox filters
24// borrowed from the NOTIFY extension. Unlike the regular extended SEARCH/UID
25// SEARCH command that always returns an ESEARCH response, the ESEARCH command only
26// returns ESEARCH responses when there were matches in a mailbox.
27//
28// ../rfc/7377:159
29func (c *conn) cmdEsearch(tag, cmd string, p *parser) {
30 c.cmdxSearch(true, true, tag, cmd, p)
31}
32
33// Search returns messages matching criteria specified in parameters.
34//
35// State: Selected for SEARCH and UID SEARCH, Authenticated or selectd for ESEARCH.
36func (c *conn) cmdxSearch(isUID, isE bool, tag, cmd string, p *parser) {
37 // Command: ../rfc/9051:3716 ../rfc/7377:159 ../rfc/6237:142 ../rfc/4731:31 ../rfc/4466:354 ../rfc/3501:2723
38 // Examples: ../rfc/9051:3986 ../rfc/7377:385 ../rfc/6237:323 ../rfc/4731:153 ../rfc/3501:2975
39 // Syntax: ../rfc/9051:6918 ../rfc/7377:462 ../rfc/6237:403 ../rfc/4466:611 ../rfc/3501:4954
40
41 // We will respond with ESEARCH instead of SEARCH if "RETURN" is present or for IMAP4rev2 or for isE (ESEARCH command).
42 var eargs map[string]bool // Options except SAVE. Nil means old-style SEARCH response.
43 var save bool // For SAVE option. Kept separately for easier handling of MIN/MAX later.
44
45 if c.enabled[capIMAP4rev2] || isE {
46 eargs = map[string]bool{}
47 }
48
49 // The ESEARCH command has various ways to specify which mailboxes are to be
50 // searched. We parse and gather the request first, and evaluate them to mailboxes
51 // after parsing, when we start and have a DB transaction.
52 var mailboxSpecs []mailboxSpecifier
53
54 // ../rfc/7377:468
55 if isE && p.take(" IN (") {
56 for {
57 ms := p.xfilterMailbox(mbspecsEsearch)
58 mailboxSpecs = append(mailboxSpecs, ms)
59
60 if !p.take(" ") {
61 break
62 }
63 }
64 p.xtake(")")
65 // We are not parsing the scope-options since there aren't any defined yet. ../rfc/7377:469
66 }
67 // ../rfc/9051:6967
68 if p.take(" RETURN (") {
69 eargs = map[string]bool{}
70
71 for !p.take(")") {
72 if len(eargs) > 0 || save {
73 p.xspace()
74 }
75 if w, ok := p.takelist("MIN", "MAX", "ALL", "COUNT", "SAVE"); ok {
76 if w == "SAVE" {
77 save = true
78 } else {
79 eargs[w] = true
80 }
81 } else {
82 // ../rfc/4466:378 ../rfc/9051:3745
83 xsyntaxErrorf("ESEARCH result option %q not supported", w)
84 }
85 }
86 }
87 // ../rfc/4731:149 ../rfc/9051:3737
88 if eargs != nil && len(eargs) == 0 && !save {
89 eargs["ALL"] = true
90 }
91
92 // If UTF8=ACCEPT is enabled, we should not accept any charset. We are a bit more
93 // relaxed (reasonable?) and still allow US-ASCII and UTF-8. ../rfc/6855:198
94 if p.take(" CHARSET ") {
95 charset := strings.ToUpper(p.xastring())
96 if charset != "US-ASCII" && charset != "UTF-8" {
97 // ../rfc/3501:2771 ../rfc/9051:3836
98 xusercodeErrorf("BADCHARSET", "only US-ASCII and UTF-8 supported")
99 }
100 }
101 p.xspace()
102 sk := &searchKey{
103 searchKeys: []searchKey{*p.xsearchKey()},
104 }
105 for !p.empty() {
106 p.xspace()
107 sk.searchKeys = append(sk.searchKeys, *p.xsearchKey())
108 }
109
110 // Sequence set search program must be rejected with UIDONLY enabled. ../rfc/9586:220
111 if c.uidonly && sk.hasSequenceNumbers() {
112 xsyntaxCodeErrorf("UIDREQUIRED", "cannot search message sequence numbers in search program with uidonly enabled")
113 }
114
115 // Even in case of error, we ensure search result is changed.
116 if save {
117 c.searchResult = []store.UID{}
118 }
119
120 // We gather word and not-word searches from the top-level, turn them
121 // into a WordSearch for a more efficient search.
122 // todo optimize: also gather them out of AND searches.
123 var textWords, textNotWords, bodyWords, bodyNotWords []string
124 n := 0
125 for _, xsk := range sk.searchKeys {
126 switch xsk.op {
127 case "BODY":
128 bodyWords = append(bodyWords, xsk.astring)
129 continue
130 case "TEXT":
131 textWords = append(textWords, xsk.astring)
132 continue
133 case "NOT":
134 switch xsk.searchKey.op {
135 case "BODY":
136 bodyNotWords = append(bodyNotWords, xsk.searchKey.astring)
137 continue
138 case "TEXT":
139 textNotWords = append(textNotWords, xsk.searchKey.astring)
140 continue
141 }
142 }
143 sk.searchKeys[n] = xsk
144 n++
145 }
146 // We may be left with an empty but non-nil sk.searchKeys, which is important for
147 // matching.
148 sk.searchKeys = sk.searchKeys[:n]
149 var bodySearch, textSearch *store.WordSearch
150 if len(bodyWords) > 0 || len(bodyNotWords) > 0 {
151 ws := store.PrepareWordSearch(bodyWords, bodyNotWords)
152 bodySearch = &ws
153 }
154 if len(textWords) > 0 || len(textNotWords) > 0 {
155 ws := store.PrepareWordSearch(textWords, textNotWords)
156 textSearch = &ws
157 }
158
159 // Note: we only hold the account rlock for verifying the mailbox at the start.
160 c.account.RLock()
161 runlock := c.account.RUnlock
162 // Note: in a defer because we replace it below.
163 defer func() {
164 runlock()
165 }()
166
167 // If we only have a MIN and/or MAX, we can stop processing as soon as we
168 // have those matches.
169 var min1, max1 int
170 if eargs["MIN"] {
171 min1 = 1
172 }
173 if eargs["MAX"] {
174 max1 = 1
175 }
176
177 // We'll have one Result per mailbox we are searching. For regular (UID) SEARCH
178 // commands, we'll have just one, for the selected mailbox.
179 type Result struct {
180 Mailbox store.Mailbox
181 MaxModSeq store.ModSeq
182 UIDs []store.UID
183 }
184 var results []Result
185
186 // We periodically send an untagged OK with INPROGRESS code while searching, to let
187 // clients doing slow searches know we're still working.
188 inProgressLast := time.Now()
189 // Only respond with tag if it can't be confused as end of response code. ../rfc/9585:122
190 inProgressTag := "nil"
191 if !strings.Contains(tag, "]") {
192 inProgressTag = dquote(tag).pack(c)
193 }
194
195 c.xdbread(func(tx *bstore.Tx) {
196 // Gather mailboxes to operate on. Usually just the selected mailbox. But with the
197 // ESEARCH command, we may be searching multiple.
198 var mailboxes []store.Mailbox
199 if len(mailboxSpecs) > 0 {
200 // While gathering, we deduplicate mailboxes. ../rfc/7377:312
201 m := map[int64]store.Mailbox{}
202 for _, ms := range mailboxSpecs {
203 switch ms.Kind {
204 case mbspecSelected:
205 // ../rfc/7377:306
206 if c.state != stateSelected {
207 xsyntaxErrorf("cannot use ESEARCH with selected when state is not selected")
208 }
209
210 mb := c.xmailboxID(tx, c.mailboxID) // Validate.
211 m[mb.ID] = mb
212
213 case mbspecInboxes:
214 // Inbox and everything below. And we look at destinations and rulesets. We all
215 // mailboxes from the destinations, and all from the rulesets except when
216 // ListAllowDomain is non-empty.
217 // ../rfc/5465:822
218 q := bstore.QueryTx[store.Mailbox](tx)
219 q.FilterEqual("Expunged", false)
220 q.FilterGreaterEqual("Name", "Inbox")
221 q.SortAsc("Name")
222 for mb, err := range q.All() {
223 xcheckf(err, "list mailboxes")
224 if mb.Name != "Inbox" && !strings.HasPrefix(mb.Name, "Inbox/") {
225 break
226 }
227 m[mb.ID] = mb
228 }
229
230 conf, _ := c.account.Conf()
231 for _, dest := range conf.Destinations {
232 if dest.Mailbox != "" && dest.Mailbox != "Inbox" {
233 mb, err := c.account.MailboxFind(tx, dest.Mailbox)
234 xcheckf(err, "find mailbox from destination")
235 if mb != nil {
236 m[mb.ID] = *mb
237 }
238 }
239
240 for _, rs := range dest.Rulesets {
241 if rs.ListAllowDomain != "" || rs.Mailbox == "" {
242 continue
243 }
244
245 mb, err := c.account.MailboxFind(tx, rs.Mailbox)
246 xcheckf(err, "find mailbox from ruleset")
247 if mb != nil {
248 m[mb.ID] = *mb
249 }
250 }
251 }
252
253 case mbspecPersonal:
254 // All mailboxes in the personal namespace. Which is all mailboxes for us.
255 // ../rfc/5465:817
256 for mb, err := range bstore.QueryTx[store.Mailbox](tx).FilterEqual("Expunged", false).All() {
257 xcheckf(err, "list mailboxes")
258 m[mb.ID] = mb
259 }
260
261 case mbspecSubscribed:
262 // Mailboxes that are subscribed. Will typically be same as personal, since we
263 // subscribe to all mailboxes. But user can manage subscriptions differently.
264 // ../rfc/5465:831
265 for mb, err := range bstore.QueryTx[store.Mailbox](tx).FilterEqual("Expunged", false).All() {
266 xcheckf(err, "list mailboxes")
267 if err := tx.Get(&store.Subscription{Name: mb.Name}); err == nil {
268 m[mb.ID] = mb
269 } else if err != bstore.ErrAbsent {
270 xcheckf(err, "lookup subscription for mailbox")
271 }
272 }
273
274 case mbspecSubtree, mbspecSubtreeOne:
275 // The mailbox name itself, and children. ../rfc/5465:847
276 // SUBTREE is arbitrarily deep, SUBTREE-ONE is one level deeper than requested
277 // mailbox. The mailbox itself is included too ../rfc/7377:274
278
279 // We don't have to worry about loops. Mailboxes are not in the file system.
280 // ../rfc/7377:291
281
282 for _, name := range ms.Mailboxes {
283 name = xcheckmailboxname(name, true)
284
285 one := ms.Kind == mbspecSubtreeOne
286 var ntoken int
287 if one {
288 ntoken = len(strings.Split(name, "/")) + 1
289 }
290
291 q := bstore.QueryTx[store.Mailbox](tx)
292 q.FilterEqual("Expunged", false)
293 q.FilterGreaterEqual("Name", name)
294 q.SortAsc("Name")
295 for mb, err := range q.All() {
296 xcheckf(err, "list mailboxes")
297 if mb.Name != name && !strings.HasPrefix(mb.Name, name+"/") {
298 break
299 }
300 if !one || mb.Name == name || len(strings.Split(mb.Name, "/")) == ntoken {
301 m[mb.ID] = mb
302 }
303 }
304 }
305
306 case mbspecMailboxes:
307 // Just the specified mailboxes. ../rfc/5465:853
308 for _, name := range ms.Mailboxes {
309 name = xcheckmailboxname(name, true)
310
311 // If a mailbox doesn't exist, we don't treat it as an error. Seems reasonable
312 // giving we are searching. Messages may not exist. And likewise for the mailbox.
313 // Just results in no hits.
314 mb, err := c.account.MailboxFind(tx, name)
315 xcheckf(err, "looking up mailbox")
316 if mb != nil {
317 m[mb.ID] = *mb
318 }
319 }
320
321 default:
322 panic("missing case")
323 }
324 }
325 mailboxes = slices.Collect(maps.Values(m))
326 slices.SortFunc(mailboxes, func(a, b store.Mailbox) int {
327 return cmp.Compare(a.Name, b.Name)
328 })
329
330 // If no source mailboxes were specified (no mailboxSpecs), the selected mailbox is
331 // used below. ../rfc/7377:298
332 } else {
333 mb := c.xmailboxID(tx, c.mailboxID) // Validate.
334 mailboxes = []store.Mailbox{mb}
335 }
336
337 if save && !(len(mailboxes) == 1 && mailboxes[0].ID == c.mailboxID) {
338 // ../rfc/7377:319
339 xsyntaxErrorf("can only use SAVE on selected mailbox")
340 }
341
342 runlock()
343 runlock = func() {}
344
345 // Determine if search has a sequence set without search results. If so, we need
346 // sequence numbers for matching, and we must always go through the messages in
347 // forward order. No reverse search for MAX only.
348 needSeq := (len(mailboxes) > 1 || len(mailboxes) == 1 && mailboxes[0].ID != c.mailboxID) && sk.hasSequenceNumbers()
349
350 forward := eargs == nil || max1 == 0 || len(eargs) != 1 || needSeq
351 reverse := max1 == 1 && (len(eargs) == 1 || min1+max1 == len(eargs)) && !needSeq
352
353 // We set a worst-case "goal" of having gone through all messages in all mailboxes.
354 // Sometimes, we can be faster, when we only do a MIN and/or MAX query and we can
355 // stop early. We'll account for that as we go. For the selected mailbox, we'll
356 // only look at those the session has already seen.
357 goal := "nil"
358 var total uint32
359 for _, mb := range mailboxes {
360 if mb.ID == c.mailboxID && !c.uidonly {
361 total += c.exists
362 } else {
363 total += uint32(mb.Total + mb.Deleted)
364 }
365 }
366 if total > 0 {
367 // Goal is always non-zero. ../rfc/9585:232
368 goal = fmt.Sprintf("%d", total)
369 }
370
371 var progress uint32
372 for _, mb := range mailboxes {
373 var lastUID store.UID
374
375 result := Result{Mailbox: mb}
376
377 msgCount := uint32(mb.MailboxCounts.Total + mb.MailboxCounts.Deleted)
378 if mb.ID == c.mailboxID && !c.uidonly {
379 msgCount = c.exists
380 }
381
382 // Used for interpreting UID sets with a star, like "1:*" and "10:*". Only called
383 // for UIDs that are higher than the number, since "10:*" evaluates to "10:5" if 5
384 // is the highest UID, and UID 5-10 would all match.
385 var cachedHighestUID store.UID
386 xhighestUID := func() store.UID {
387 if cachedHighestUID > 0 {
388 return cachedHighestUID
389 }
390
391 q := bstore.QueryTx[store.Message](tx)
392 q.FilterNonzero(store.Message{MailboxID: mb.ID})
393 q.FilterEqual("Expunged", false)
394 if mb.ID == c.mailboxID {
395 q.FilterLess("UID", c.uidnext)
396 }
397 q.SortDesc("UID")
398 q.Limit(1)
399 m, err := q.Get()
400 if err == bstore.ErrAbsent {
401 xuserErrorf("cannot use * on empty mailbox")
402 }
403 xcheckf(err, "get last uid")
404 cachedHighestUID = m.UID
405 return cachedHighestUID
406 }
407
408 progressOrig := progress
409
410 if forward {
411 // We track this for non-selected mailboxes. searchMatch will look the message
412 // sequence number for this session up if we are searching the selected mailbox.
413 var seq msgseq = 1
414
415 q := bstore.QueryTx[store.Message](tx)
416 q.FilterNonzero(store.Message{MailboxID: mb.ID})
417 q.FilterEqual("Expunged", false)
418 if mb.ID == c.mailboxID {
419 q.FilterLess("UID", c.uidnext)
420 }
421 q.SortAsc("UID")
422 for m, err := range q.All() {
423 xcheckf(err, "list messages in mailbox")
424
425 // We track this for the "reverse" case, we'll stop before seeing lastUID.
426 lastUID = m.UID
427
428 if time.Since(inProgressLast) > inProgressPeriod {
429 c.xwritelinef("* OK [INPROGRESS (%s %d %s)] still searching", inProgressTag, progress, goal)
430 inProgressLast = time.Now()
431 }
432 progress++
433
434 if c.searchMatch(tx, msgCount, seq, m, *sk, bodySearch, textSearch, xhighestUID) {
435 result.UIDs = append(result.UIDs, m.UID)
436 result.MaxModSeq = max(result.MaxModSeq, m.ModSeq)
437 if min1 == 1 && min1+max1 == len(eargs) {
438 if !needSeq {
439 break
440 }
441 // We only need a MIN and a MAX, but we also need sequence numbers so we are
442 // walking through and collecting all UIDs. Correct for that, keeping only the MIN
443 // (first)
444 // and MAX (second).
445 if len(result.UIDs) == 3 {
446 result.UIDs[1] = result.UIDs[2]
447 result.UIDs = result.UIDs[:2]
448 }
449 }
450 }
451 seq++
452 }
453 }
454 // And reverse search for MAX if we have only MAX or MAX combined with MIN, and
455 // don't need sequence numbers. We just need a single match, then we stop.
456 if reverse {
457 q := bstore.QueryTx[store.Message](tx)
458 q.FilterNonzero(store.Message{MailboxID: mb.ID})
459 q.FilterEqual("Expunged", false)
460 q.FilterGreater("UID", lastUID)
461 if mb.ID == c.mailboxID {
462 q.FilterLess("UID", c.uidnext)
463 }
464 q.SortDesc("UID")
465 for m, err := range q.All() {
466 xcheckf(err, "list messages in mailbox")
467
468 if time.Since(inProgressLast) > inProgressPeriod {
469 c.xwritelinef("* OK [INPROGRESS (%s %d %s)] still searching", inProgressTag, progress, goal)
470 inProgressLast = time.Now()
471 }
472 progress++
473
474 var seq msgseq // Filled in by searchMatch for messages in selected mailbox.
475 if c.searchMatch(tx, msgCount, seq, m, *sk, bodySearch, textSearch, xhighestUID) {
476 result.UIDs = append(result.UIDs, m.UID)
477 result.MaxModSeq = max(result.MaxModSeq, m.ModSeq)
478 break
479 }
480 }
481 }
482
483 // We could have finished searching the mailbox with fewer
484 mailboxProcessed := progress - progressOrig
485 mailboxTotal := uint32(mb.MailboxCounts.Total + mb.MailboxCounts.Deleted)
486 progress += max(0, mailboxTotal-mailboxProcessed)
487
488 results = append(results, result)
489 }
490 })
491
492 if eargs == nil {
493 // We'll only have a result for the one selected mailbox.
494 result := results[0]
495
496 // In IMAP4rev1, an untagged SEARCH response is required. ../rfc/3501:2728
497 if len(result.UIDs) == 0 {
498 c.xbwritelinef("* SEARCH")
499 }
500
501 // Old-style SEARCH response. We must spell out each number. So we may be splitting
502 // into multiple responses. ../rfc/9051:6809 ../rfc/3501:4833
503 for len(result.UIDs) > 0 {
504 n := min(100, len(result.UIDs))
505 s := ""
506 for _, v := range result.UIDs[:n] {
507 if !isUID {
508 v = store.UID(c.xsequence(v))
509 }
510 s += " " + fmt.Sprintf("%d", v)
511 }
512
513 // Since we don't have the max modseq for the possibly partial uid range we're
514 // writing here within hand reach, we conveniently interpret the ambiguous "for all
515 // messages being returned" in ../rfc/7162:1107 as meaning over all lines that we
516 // write. And that clients only commit this value after they have seen the tagged
517 // end of the command. Appears to be recommended behaviour, ../rfc/7162:2323.
518 // ../rfc/7162:1077 ../rfc/7162:1101
519 var modseq string
520 if sk.hasModseq() {
521 // ../rfc/7162:2557
522 modseq = fmt.Sprintf(" (MODSEQ %d)", result.MaxModSeq.Client())
523 }
524
525 c.xbwritelinef("* SEARCH%s%s", s, modseq)
526 result.UIDs = result.UIDs[n:]
527 }
528 } else {
529 // New-style ESEARCH response syntax: ../rfc/9051:6546 ../rfc/4466:522
530
531 if save {
532 // ../rfc/9051:3784 ../rfc/5182:13
533 c.searchResult = results[0].UIDs
534 c.checkUIDs(c.searchResult, false)
535 }
536
537 // No untagged ESEARCH response if nothing was requested. ../rfc/9051:4160
538 if len(eargs) > 0 {
539 for _, result := range results {
540 // For the ESEARCH command, we must not return a response if there were no matching
541 // messages. This is unlike the later IMAP4rev2, where an ESEARCH response must be
542 // sent if there were no matches. ../rfc/7377:243 ../rfc/9051:3775
543 if isE && len(result.UIDs) == 0 {
544 continue
545 }
546
547 // The tag was originally a string, became an astring in IMAP4rev2, better stick to
548 // string. ../rfc/4466:707 ../rfc/5259:1163 ../rfc/9051:7087
549 if isE {
550 fmt.Fprintf(c.xbw, `* ESEARCH (TAG "%s" MAILBOX %s UIDVALIDITY %d)`, tag, result.Mailbox.Name, result.Mailbox.UIDValidity)
551 } else {
552 fmt.Fprintf(c.xbw, `* ESEARCH (TAG "%s")`, tag)
553 }
554 if isUID {
555 fmt.Fprintf(c.xbw, " UID")
556 }
557
558 // NOTE: we are potentially converting UIDs to msgseq, but keep the store.UID type
559 // for convenience.
560 nums := result.UIDs
561 if !isUID {
562 // If searchResult is hanging on to the slice, we need to work on a copy.
563 if save {
564 nums = slices.Clone(nums)
565 }
566 for i, uid := range nums {
567 nums[i] = store.UID(c.xsequence(uid))
568 }
569 }
570
571 // If no matches, then no MIN/MAX response. ../rfc/4731:98 ../rfc/9051:3758
572 if eargs["MIN"] && len(nums) > 0 {
573 fmt.Fprintf(c.xbw, " MIN %d", nums[0])
574 }
575 if eargs["MAX"] && len(result.UIDs) > 0 {
576 fmt.Fprintf(c.xbw, " MAX %d", nums[len(nums)-1])
577 }
578 if eargs["COUNT"] {
579 fmt.Fprintf(c.xbw, " COUNT %d", len(nums))
580 }
581 if eargs["ALL"] && len(nums) > 0 {
582 fmt.Fprintf(c.xbw, " ALL %s", compactUIDSet(nums).String())
583 }
584
585 // Interaction between ESEARCH and CONDSTORE: ../rfc/7162:1211 ../rfc/4731:273
586 // Summary: send the highest modseq of the returned messages.
587 if sk.hasModseq() && len(nums) > 0 {
588 fmt.Fprintf(c.xbw, " MODSEQ %d", result.MaxModSeq.Client())
589 }
590
591 c.xbwritelinef("")
592 }
593 }
594 }
595
596 c.ok(tag, cmd)
597}
598
599type search struct {
600 c *conn
601 tx *bstore.Tx
602 msgCount uint32 // Number of messages in mailbox (or session when selected).
603 seq msgseq // Can be 0, for other mailboxes than selected in case of MAX.
604 m store.Message
605 mr *store.MsgReader
606 p *message.Part
607 xhighestUID func() store.UID
608}
609
610func (c *conn) searchMatch(tx *bstore.Tx, msgCount uint32, seq msgseq, m store.Message, sk searchKey, bodySearch, textSearch *store.WordSearch, xhighestUID func() store.UID) bool {
611 if m.MailboxID == c.mailboxID {
612 // If session doesn't know about the message yet, don't return it.
613 if c.uidonly {
614 if m.UID >= c.uidnext {
615 return false
616 }
617 } else {
618 // Set seq for use in evaluations.
619 seq = c.sequence(m.UID)
620 if seq == 0 {
621 return false
622 }
623 }
624 }
625
626 s := search{c: c, tx: tx, msgCount: msgCount, seq: seq, m: m, xhighestUID: xhighestUID}
627 defer func() {
628 if s.mr != nil {
629 err := s.mr.Close()
630 c.xsanity(err, "closing messagereader")
631 s.mr = nil
632 }
633 }()
634 return s.match(sk, bodySearch, textSearch)
635}
636
637func (s *search) match(sk searchKey, bodySearch, textSearch *store.WordSearch) (match bool) {
638 match = s.match0(sk)
639 if match && bodySearch != nil {
640 if !s.xensurePart() {
641 match = false
642 return
643 }
644 var err error
645 match, err = bodySearch.MatchPart(s.c.log, s.p, false)
646 xcheckf(err, "search words in bodies")
647 }
648 if match && textSearch != nil {
649 if !s.xensurePart() {
650 match = false
651 return
652 }
653 var err error
654 match, err = textSearch.MatchPart(s.c.log, s.p, true)
655 xcheckf(err, "search words in headers and bodies")
656 }
657 return
658}
659
660// ensure message, reader and part are loaded. returns whether that was
661// successful.
662func (s *search) xensurePart() bool {
663 if s.mr != nil {
664 return s.p != nil
665 }
666
667 // Closed by searchMatch after all (recursive) search.match calls are finished.
668 s.mr = s.c.account.MessageReader(s.m)
669
670 if s.m.ParsedBuf == nil {
671 s.c.log.Error("missing parsed message")
672 return false
673 }
674 p, err := s.m.LoadPart(s.mr)
675 xcheckf(err, "load parsed message")
676 s.p = &p
677 return true
678}
679
680func (s *search) match0(sk searchKey) bool {
681 c := s.c
682
683 // Difference between sk.searchKeys nil and length 0 is important. Because we take
684 // out word/notword searches, the list may be empty but non-nil.
685 if sk.searchKeys != nil {
686 for _, ssk := range sk.searchKeys {
687 if !s.match0(ssk) {
688 return false
689 }
690 }
691 return true
692 } else if sk.seqSet != nil {
693 if sk.seqSet.searchResult {
694 // Interpreting search results on a mailbox that isn't selected during multisearch
695 // is likely a mistake. No mention about it in the RFC. ../rfc/7377:257
696 if s.m.MailboxID != c.mailboxID {
697 xuserErrorf("can only use search result with the selected mailbox")
698 }
699 return uidSearch(c.searchResult, s.m.UID) > 0
700 }
701 // For multisearch, we have arranged to have a seq for non-selected mailboxes too.
702 return sk.seqSet.containsSeqCount(s.seq, s.msgCount)
703 }
704
705 filterHeader := func(field, value string) bool {
706 lower := strings.ToLower(value)
707 h, err := s.p.Header()
708 if err != nil {
709 c.log.Debugx("parsing message header", err, slog.Any("uid", s.m.UID), slog.Int64("msgid", s.m.ID))
710 return false
711 }
712 for _, v := range h.Values(field) {
713 if strings.Contains(strings.ToLower(v), lower) {
714 return true
715 }
716 }
717 return false
718 }
719
720 // We handle ops by groups that need increasing details about the message.
721
722 switch sk.op {
723 case "ALL":
724 return true
725 case "NEW":
726 // We do not implement the RECENT flag, so messages cannot be NEW.
727 return false
728 case "OLD":
729 // We treat all messages as non-recent, so this means all messages.
730 return true
731 case "RECENT":
732 // We do not implement the RECENT flag. All messages are not recent.
733 return false
734 case "NOT":
735 return !s.match0(*sk.searchKey)
736 case "OR":
737 return s.match0(*sk.searchKey) || s.match0(*sk.searchKey2)
738 case "UID":
739 if sk.uidSet.searchResult && s.m.MailboxID != c.mailboxID {
740 // Interpreting search results on a mailbox that isn't selected during multisearch
741 // is likely a mistake. No mention about it in the RFC. ../rfc/7377:257
742 xuserErrorf("cannot use search result from another mailbox")
743 }
744 return sk.uidSet.xcontainsKnownUID(s.m.UID, c.searchResult, s.xhighestUID)
745 }
746
747 // Parsed part.
748 if !s.xensurePart() {
749 return false
750 }
751
752 // Parsed message, basic info.
753 switch sk.op {
754 case "ANSWERED":
755 return s.m.Answered
756 case "DELETED":
757 return s.m.Deleted
758 case "FLAGGED":
759 return s.m.Flagged
760 case "KEYWORD":
761 kw := strings.ToLower(sk.atom)
762 switch kw {
763 case "$forwarded":
764 return s.m.Forwarded
765 case "$junk":
766 return s.m.Junk
767 case "$notjunk":
768 return s.m.Notjunk
769 case "$phishing":
770 return s.m.Phishing
771 case "$mdnsent":
772 return s.m.MDNSent
773 default:
774 return slices.Contains(s.m.Keywords, kw)
775 }
776 case "SEEN":
777 return s.m.Seen
778 case "UNANSWERED":
779 return !s.m.Answered
780 case "UNDELETED":
781 return !s.m.Deleted
782 case "UNFLAGGED":
783 return !s.m.Flagged
784 case "UNKEYWORD":
785 kw := strings.ToLower(sk.atom)
786 switch kw {
787 case "$forwarded":
788 return !s.m.Forwarded
789 case "$junk":
790 return !s.m.Junk
791 case "$notjunk":
792 return !s.m.Notjunk
793 case "$phishing":
794 return !s.m.Phishing
795 case "$mdnsent":
796 return !s.m.MDNSent
797 default:
798 return !slices.Contains(s.m.Keywords, kw)
799 }
800 case "UNSEEN":
801 return !s.m.Seen
802 case "DRAFT":
803 return s.m.Draft
804 case "UNDRAFT":
805 return !s.m.Draft
806 case "BEFORE", "ON", "SINCE":
807 skdt := sk.date.Format("2006-01-02")
808 rdt := s.m.Received.Format("2006-01-02")
809 switch sk.op {
810 case "BEFORE":
811 return rdt < skdt
812 case "ON":
813 return rdt == skdt
814 case "SINCE":
815 return rdt >= skdt
816 }
817 panic("missing case")
818 case "LARGER":
819 return s.m.Size > sk.number
820 case "SMALLER":
821 return s.m.Size < sk.number
822 case "MODSEQ":
823 // ../rfc/7162:1045
824 return s.m.ModSeq.Client() >= *sk.clientModseq
825 case "SAVEDBEFORE", "SAVEDON", "SAVEDSINCE":
826 // If we don't have a savedate for this message (for messages received before we
827 // implemented this feature), we use the "internal date" (received timestamp) of
828 // the message. ../rfc/8514:237
829 rt := s.m.Received
830 if s.m.SaveDate != nil {
831 rt = *s.m.SaveDate
832 }
833
834 skdt := sk.date.Format("2006-01-02")
835 rdt := rt.Format("2006-01-02")
836 switch sk.op {
837 case "SAVEDBEFORE":
838 return rdt < skdt
839 case "SAVEDON":
840 return rdt == skdt
841 case "SAVEDSINCE":
842 return rdt >= skdt
843 }
844 panic("missing case")
845 case "SAVEDATESUPPORTED":
846 // We return whether we have a savedate for this message. We support it on all
847 // mailboxes, but we only have this metadata from the time we implemented this
848 // feature.
849 return s.m.SaveDate != nil
850 case "OLDER":
851 // ../rfc/5032:76
852 seconds := int64(time.Since(s.m.Received) / time.Second)
853 return seconds >= sk.number
854 case "YOUNGER":
855 seconds := int64(time.Since(s.m.Received) / time.Second)
856 return seconds <= sk.number
857 }
858
859 if s.p == nil {
860 c.log.Info("missing parsed message, not matching", slog.Any("uid", s.m.UID), slog.Int64("msgid", s.m.ID))
861 return false
862 }
863
864 // Parsed message, more info.
865 switch sk.op {
866 case "BCC":
867 return filterHeader("Bcc", sk.astring)
868 case "BODY", "TEXT":
869 // We gathered word/notword searches from the top-level, but we can also get them
870 // nested.
871 // todo optimize: handle deeper nested word/not-word searches more efficiently.
872 headerToo := sk.op == "TEXT"
873 match, err := store.PrepareWordSearch([]string{sk.astring}, nil).MatchPart(s.c.log, s.p, headerToo)
874 xcheckf(err, "word search")
875 return match
876 case "CC":
877 return filterHeader("Cc", sk.astring)
878 case "FROM":
879 return filterHeader("From", sk.astring)
880 case "SUBJECT":
881 return filterHeader("Subject", sk.astring)
882 case "TO":
883 return filterHeader("To", sk.astring)
884 case "HEADER":
885 // ../rfc/9051:3895
886 lower := strings.ToLower(sk.astring)
887 h, err := s.p.Header()
888 if err != nil {
889 c.log.Errorx("parsing header for search", err, slog.Any("uid", s.m.UID), slog.Int64("msgid", s.m.ID))
890 return false
891 }
892 k := textproto.CanonicalMIMEHeaderKey(sk.headerField)
893 for _, v := range h.Values(k) {
894 if lower == "" || strings.Contains(strings.ToLower(v), lower) {
895 return true
896 }
897 }
898 return false
899 case "SENTBEFORE", "SENTON", "SENTSINCE":
900 if s.p.Envelope == nil || s.p.Envelope.Date.IsZero() {
901 return false
902 }
903 dt := s.p.Envelope.Date.Format("2006-01-02")
904 skdt := sk.date.Format("2006-01-02")
905 switch sk.op {
906 case "SENTBEFORE":
907 return dt < skdt
908 case "SENTON":
909 return dt == skdt
910 case "SENTSINCE":
911 return dt > skdt
912 }
913 panic("missing case")
914 }
915 panic(serverError{fmt.Errorf("missing case for search key op %q", sk.op)})
916}
917