1package imapserver
2
3import (
4 "errors"
5 "fmt"
6 "net/textproto"
7 "strconv"
8 "strings"
9 "time"
10
11 "github.com/mjl-/mox/mlog"
12)
13
14var (
15 listWildcards = "%*"
16 char = charRange('\x01', '\x7f')
17 ctl = charRange('\x01', '\x19')
18 quotedSpecials = `"\`
19 respSpecials = "]"
20 atomChar = charRemove(char, "(){ "+ctl+listWildcards+quotedSpecials+respSpecials)
21 astringChar = atomChar + respSpecials
22)
23
24func charRange(first, last rune) string {
25 r := ""
26 c := first
27 r += string(c)
28 for c < last {
29 c++
30 r += string(c)
31 }
32 return r
33}
34
35func charRemove(s, remove string) string {
36 r := ""
37next:
38 for _, c := range s {
39 for _, x := range remove {
40 if c == x {
41 continue next
42 }
43 }
44 r += string(c)
45 }
46 return r
47}
48
49type parser struct {
50 // Orig is the line in original casing, and upper in upper casing. We often match
51 // against upper for easy case insensitive handling as IMAP requires, but sometimes
52 // return from orig to keep the original case.
53 orig string
54 upper string
55 o int // Current offset in parsing.
56 contexts []string // What we're parsing, for error messages.
57 conn *conn
58}
59
60// toUpper upper cases bytes that are a-z. strings.ToUpper does too much. and
61// would replace invalid bytes with unicode replacement characters, which would
62// break our requirement that offsets into the original and upper case strings
63// point to the same character.
64func toUpper(s string) string {
65 r := []byte(s)
66 for i, c := range r {
67 if c >= 'a' && c <= 'z' {
68 r[i] = c - 0x20
69 }
70 }
71 return string(r)
72}
73
74func newParser(s string, conn *conn) *parser {
75 return &parser{s, toUpper(s), 0, nil, conn}
76}
77
78func (p *parser) xerrorf(format string, args ...any) {
79 var err error
80 errmsg := fmt.Sprintf(format, args...)
81 remaining := fmt.Sprintf("remaining %q", p.orig[p.o:])
82 if len(p.contexts) > 0 {
83 remaining += ", context " + strings.Join(p.contexts, ",")
84 }
85 remaining = " (" + remaining + ")"
86 if p.conn.account != nil {
87 errmsg += remaining
88 err = errors.New(errmsg)
89 } else {
90 err = errors.New(errmsg + remaining)
91 }
92 panic(syntaxError{"", "", errmsg, err})
93}
94
95func (p *parser) context(s string) func() {
96 p.contexts = append(p.contexts, s)
97 return func() {
98 p.contexts = p.contexts[:len(p.contexts)-1]
99 }
100}
101
102func (p *parser) empty() bool {
103 return p.o == len(p.upper)
104}
105
106func (p *parser) xempty() {
107 if !p.empty() {
108 p.xerrorf("leftover data")
109 }
110}
111
112func (p *parser) hasPrefix(s string) bool {
113 return strings.HasPrefix(p.upper[p.o:], s)
114}
115
116func (p *parser) take(s string) bool {
117 if !p.hasPrefix(s) {
118 return false
119 }
120 p.o += len(s)
121 return true
122}
123
124func (p *parser) xtake(s string) {
125 if !p.take(s) {
126 p.xerrorf("expected %s", s)
127 }
128}
129
130func (p *parser) xnonempty() {
131 if p.empty() {
132 p.xerrorf("unexpected end")
133 }
134}
135
136func (p *parser) xtakeall() string {
137 r := p.orig[p.o:]
138 p.o = len(p.orig)
139 return r
140}
141
142func (p *parser) xtake1n(n int, what string) string {
143 if n == 0 {
144 p.xerrorf("expected chars from %s", what)
145 }
146 return p.xtaken(n)
147}
148
149func (p *parser) xtakechars(s string, what string) string {
150 p.xnonempty()
151 for i, c := range p.orig[p.o:] {
152 if !contains(s, c) {
153 return p.xtake1n(i, what)
154 }
155 }
156 return p.xtakeall()
157}
158
159func (p *parser) xtaken(n int) string {
160 if p.o+n > len(p.orig) {
161 p.xerrorf("not enough data")
162 }
163 r := p.orig[p.o : p.o+n]
164 p.o += n
165 return r
166}
167
168func (p *parser) space() bool {
169 return p.take(" ")
170}
171
172func (p *parser) xspace() {
173 if !p.space() {
174 p.xerrorf("expected space")
175 }
176}
177
178func (p *parser) digits() string {
179 var n int
180 for _, c := range p.upper[p.o:] {
181 if c < '0' || c > '9' {
182 break
183 }
184 n++
185 }
186 if n == 0 {
187 return ""
188 }
189 s := p.upper[p.o : p.o+n]
190 p.o += n
191 return s
192}
193
194func (p *parser) nznumber() (uint32, bool) {
195 o := p.o
196 for o < len(p.upper) && p.upper[o] >= '0' && p.upper[o] <= '9' {
197 o++
198 }
199 if o == p.o {
200 return 0, false
201 }
202 if n, err := strconv.ParseUint(p.upper[p.o:o], 10, 32); err != nil {
203 return 0, false
204 } else if n == 0 {
205 return 0, false
206 } else {
207 p.o = o
208 return uint32(n), true
209 }
210}
211
212func (p *parser) xnznumber() uint32 {
213 n, ok := p.nznumber()
214 if !ok {
215 p.xerrorf("expected non-zero number")
216 }
217 return n
218}
219
220func (p *parser) number() (uint32, bool) {
221 o := p.o
222 for o < len(p.upper) && p.upper[o] >= '0' && p.upper[o] <= '9' {
223 o++
224 }
225 if o == p.o {
226 return 0, false
227 }
228 n, err := strconv.ParseUint(p.upper[p.o:o], 10, 32)
229 if err != nil {
230 return 0, false
231 }
232 p.o = o
233 return uint32(n), true
234}
235
236func (p *parser) xnumber() uint32 {
237 n, ok := p.number()
238 if !ok {
239 p.xerrorf("expected number")
240 }
241 return n
242}
243
244func (p *parser) xnumber64() int64 {
245 s := p.digits()
246 if s == "" {
247 p.xerrorf("expected number64")
248 }
249 v, err := strconv.ParseInt(s, 10, 63) // ../rfc/9051:6794 ../rfc/7162:297
250 if err != nil {
251 p.xerrorf("parsing number64 %q: %v", s, err)
252 }
253 return v
254}
255
256func (p *parser) xnznumber64() int64 {
257 v := p.xnumber64()
258 if v == 0 {
259 p.xerrorf("expected non-zero number64")
260 }
261 return v
262}
263
264// l should be a list of uppercase words, the first match is returned
265func (p *parser) takelist(l ...string) (string, bool) {
266 for _, w := range l {
267 if p.take(w) {
268 return w, true
269 }
270 }
271 return "", false
272}
273
274func (p *parser) xtakelist(l ...string) string {
275 w, ok := p.takelist(l...)
276 if !ok {
277 p.xerrorf("expected one of %s", strings.Join(l, ","))
278 }
279 return w
280}
281
282func (p *parser) xstring() (r string) {
283 if p.take(`"`) {
284 esc := false
285 r := ""
286 for i, c := range p.orig[p.o:] {
287 if c == '\\' {
288 esc = true
289 } else if c == '\x00' || c == '\r' || c == '\n' {
290 p.xerrorf("invalid nul, cr or lf in string")
291 } else if esc {
292 if c == '\\' || c == '"' {
293 r += string(c)
294 esc = false
295 } else {
296 p.xerrorf("invalid escape char %c", c)
297 }
298 } else if c == '"' {
299 p.o += i + 1
300 return r
301 } else {
302 r += string(c)
303 }
304 }
305 p.xerrorf("missing closing dquote in string")
306 }
307 size, sync := p.xliteralSize(100*1024, false)
308 s := p.conn.xreadliteral(size, sync)
309 line := p.conn.readline(false)
310 p.orig, p.upper, p.o = line, toUpper(line), 0
311 return s
312}
313
314func (p *parser) xnil() {
315 p.xtake("NIL")
316}
317
318// Returns NIL as empty string.
319func (p *parser) xnilString() string {
320 if p.take("NIL") {
321 return ""
322 }
323 return p.xstring()
324}
325
326func (p *parser) xastring() string {
327 if p.hasPrefix(`"`) || p.hasPrefix("{") || p.hasPrefix("~{") {
328 return p.xstring()
329 }
330 return p.xtakechars(astringChar, "astring")
331}
332
333func contains(s string, c rune) bool {
334 for _, x := range s {
335 if x == c {
336 return true
337 }
338 }
339 return false
340}
341
342func (p *parser) xtag() string {
343 p.xnonempty()
344 for i, c := range p.orig[p.o:] {
345 if c == '+' || !contains(astringChar, c) {
346 return p.xtake1n(i, "tag")
347 }
348 }
349 return p.xtakeall()
350}
351
352func (p *parser) xcommand() string {
353 for i, c := range p.upper[p.o:] {
354 if !(c >= 'A' && c <= 'Z' || c == ' ' && p.upper[p.o:p.o+i] == "UID") {
355 return p.xtake1n(i, "command")
356 }
357 }
358 return p.xtakeall()
359}
360
361func (p *parser) remainder() string {
362 return p.orig[p.o:]
363}
364
365// ../rfc/9051:6565
366func (p *parser) xflag() string {
367 w, _ := p.takelist(`\`, "$")
368 s := w + p.xatom()
369 if s[0] == '\\' {
370 switch strings.ToLower(s) {
371 case `\answered`, `\flagged`, `\deleted`, `\seen`, `\draft`:
372 default:
373 p.xerrorf("unknown system flag %s", s)
374 }
375 }
376 return s
377}
378
379func (p *parser) xflagList() (l []string) {
380 p.xtake("(")
381 if !p.hasPrefix(")") {
382 l = append(l, p.xflag())
383 }
384 for !p.take(")") {
385 p.xspace()
386 l = append(l, p.xflag())
387 }
388 return
389}
390
391func (p *parser) xatom() string {
392 return p.xtakechars(atomChar, "atom")
393}
394
395func (p *parser) xmailbox() string {
396 s := p.xastring()
397 // UTF-7 is deprecated in IMAP4rev2. IMAP4rev1 does not fully forbid
398 // UTF-8 returned in mailbox names. We'll do our best by attempting to
399 // decode utf-7. But if that doesn't work, we'll just use the original
400 // string.
401 // ../rfc/3501:964
402 if !p.conn.enabled[capIMAP4rev2] {
403 ns, err := utf7decode(s)
404 if err != nil {
405 p.conn.log.Infox("decoding utf7 or mailbox name", err, mlog.Field("name", s))
406 } else {
407 s = ns
408 }
409 }
410 return s
411}
412
413// ../rfc/9051:6605
414func (p *parser) xlistMailbox() string {
415 if p.hasPrefix(`"`) || p.hasPrefix("{") {
416 return p.xstring()
417 }
418 return p.xtakechars(atomChar+listWildcards+respSpecials, "list-char")
419}
420
421// ../rfc/9051:6707 ../rfc/9051:6848 ../rfc/5258:1095 ../rfc/5258:1169 ../rfc/5258:1196
422func (p *parser) xmboxOrPat() ([]string, bool) {
423 if !p.take("(") {
424 return []string{p.xlistMailbox()}, false
425 }
426 l := []string{p.xlistMailbox()}
427 for !p.take(")") {
428 p.xspace()
429 l = append(l, p.xlistMailbox())
430 }
431 return l, true
432}
433
434// ../rfc/9051:7056, RECENT ../rfc/3501:5047, APPENDLIMIT ../rfc/7889:252, HIGHESTMODSEQ ../rfc/7162:2452
435func (p *parser) xstatusAtt() string {
436 w := p.xtakelist("MESSAGES", "UIDNEXT", "UIDVALIDITY", "UNSEEN", "DELETED", "SIZE", "RECENT", "APPENDLIMIT", "HIGHESTMODSEQ")
437 if w == "HIGHESTMODSEQ" {
438 // HIGHESTMODSEQ is a CONDSTORE-enabling parameter. ../rfc/7162:375
439 p.conn.enabled[capCondstore] = true
440 }
441 return w
442}
443
444// ../rfc/9051:7133 ../rfc/9051:7034
445func (p *parser) xnumSet0(allowStar, allowSearch bool) (r numSet) {
446 defer p.context("numSet")()
447 if allowSearch && p.take("$") {
448 return numSet{searchResult: true}
449 }
450 r.ranges = append(r.ranges, p.xnumRange0(allowStar))
451 for p.take(",") {
452 r.ranges = append(r.ranges, p.xnumRange0(allowStar))
453 }
454 return r
455}
456
457func (p *parser) xnumSet() (r numSet) {
458 return p.xnumSet0(true, true)
459}
460
461// parse numRange, which can be just a setNumber.
462func (p *parser) xnumRange0(allowStar bool) (r numRange) {
463 if allowStar && p.take("*") {
464 r.first.star = true
465 } else {
466 r.first.number = p.xnznumber()
467 }
468 if p.take(":") {
469 r.last = &setNumber{}
470 if allowStar && p.take("*") {
471 r.last.star = true
472 } else {
473 r.last.number = p.xnznumber()
474 }
475 }
476 return
477}
478
479// ../rfc/9051:6989 ../rfc/3501:4977
480func (p *parser) xsectionMsgtext() (r *sectionMsgtext) {
481 defer p.context("sectionMsgtext")()
482 msgtextWords := []string{"HEADER.FIELDS.NOT", "HEADER.FIELDS", "HEADER", "TEXT"}
483 w := p.xtakelist(msgtextWords...)
484 r = &sectionMsgtext{s: w}
485 if strings.HasPrefix(w, "HEADER.FIELDS") {
486 p.xspace()
487 p.xtake("(")
488 r.headers = append(r.headers, textproto.CanonicalMIMEHeaderKey(p.xastring()))
489 for {
490 if p.take(")") {
491 break
492 }
493 p.xspace()
494 r.headers = append(r.headers, textproto.CanonicalMIMEHeaderKey(p.xastring()))
495 }
496 }
497 return
498}
499
500// ../rfc/9051:6999 ../rfc/3501:4991
501func (p *parser) xsectionSpec() (r *sectionSpec) {
502 defer p.context("parseSectionSpec")()
503
504 n, ok := p.nznumber()
505 if !ok {
506 return &sectionSpec{msgtext: p.xsectionMsgtext()}
507 }
508 defer p.context("part...")()
509 pt := &sectionPart{}
510 pt.part = append(pt.part, n)
511 for {
512 if !p.take(".") {
513 break
514 }
515 if n, ok := p.nznumber(); ok {
516 pt.part = append(pt.part, n)
517 continue
518 }
519 if p.take("MIME") {
520 pt.text = &sectionText{mime: true}
521 break
522 }
523 pt.text = &sectionText{msgtext: p.xsectionMsgtext()}
524 break
525 }
526 return &sectionSpec{part: pt}
527}
528
529// ../rfc/9051:6985 ../rfc/3501:4975
530func (p *parser) xsection() *sectionSpec {
531 defer p.context("parseSection")()
532 p.xtake("[")
533 if p.take("]") {
534 return &sectionSpec{}
535 }
536 r := p.xsectionSpec()
537 p.xtake("]")
538 return r
539}
540
541// ../rfc/9051:6841
542func (p *parser) xpartial() *partial {
543 p.xtake("<")
544 offset := p.xnumber()
545 p.xtake(".")
546 count := p.xnznumber()
547 p.xtake(">")
548 return &partial{offset, count}
549}
550
551// ../rfc/9051:6987
552func (p *parser) xsectionBinary() (r []uint32) {
553 p.xtake("[")
554 if p.take("]") {
555 return nil
556 }
557 r = append(r, p.xnznumber())
558 for {
559 if !p.take(".") {
560 break
561 }
562 r = append(r, p.xnznumber())
563 }
564 p.xtake("]")
565 return r
566}
567
568var fetchAttWords = []string{
569 "ENVELOPE", "FLAGS", "INTERNALDATE", "RFC822.SIZE", "BODYSTRUCTURE", "UID", "BODY.PEEK", "BODY", "BINARY.PEEK", "BINARY.SIZE", "BINARY",
570 "RFC822.HEADER", "RFC822.TEXT", "RFC822", // older IMAP
571 "MODSEQ", // CONDSTORE extension.
572}
573
574// ../rfc/9051:6557 ../rfc/3501:4751 ../rfc/7162:2483
575func (p *parser) xfetchAtt(isUID bool) (r fetchAtt) {
576 defer p.context("fetchAtt")()
577 f := p.xtakelist(fetchAttWords...)
578 r.peek = strings.HasSuffix(f, ".PEEK")
579 r.field = strings.TrimSuffix(f, ".PEEK")
580
581 switch r.field {
582 case "BODY":
583 if p.hasPrefix("[") {
584 r.section = p.xsection()
585 if p.hasPrefix("<") {
586 r.partial = p.xpartial()
587 }
588 }
589 case "BINARY":
590 r.sectionBinary = p.xsectionBinary()
591 if p.hasPrefix("<") {
592 r.partial = p.xpartial()
593 }
594 case "BINARY.SIZE":
595 r.sectionBinary = p.xsectionBinary()
596 case "MODSEQ":
597 // The RFC text mentions MODSEQ is only for FETCH, not UID FETCH, but the ABNF adds
598 // the attribute to the shared syntax, so UID FETCH also implements it.
599 // ../rfc/7162:905
600 // The wording about when to respond with a MODSEQ attribute could be more clear. ../rfc/7162:923 ../rfc/7162:388
601 // MODSEQ attribute is a CONDSTORE-enabling parameter. ../rfc/7162:377
602 p.conn.xensureCondstore(nil)
603 }
604 return
605}
606
607// ../rfc/9051:6553 ../rfc/3501:4748
608func (p *parser) xfetchAtts(isUID bool) []fetchAtt {
609 defer p.context("fetchAtts")()
610
611 fields := func(l ...string) []fetchAtt {
612 r := make([]fetchAtt, len(l))
613 for i, s := range l {
614 r[i] = fetchAtt{field: s}
615 }
616 return r
617 }
618
619 if w, ok := p.takelist("ALL", "FAST", "FULL"); ok {
620 switch w {
621 case "ALL":
622 return fields("FLAGS", "INTERNALDATE", "RFC822.SIZE", "ENVELOPE")
623 case "FAST":
624 return fields("FLAGS", "INTERNALDATE", "RFC822.SIZE")
625 case "FULL":
626 return fields("FLAGS", "INTERNALDATE", "RFC822.SIZE", "ENVELOPE", "BODY")
627 }
628 panic("missing case")
629 }
630
631 if !p.hasPrefix("(") {
632 return []fetchAtt{p.xfetchAtt(isUID)}
633 }
634
635 l := []fetchAtt{}
636 p.xtake("(")
637 for {
638 l = append(l, p.xfetchAtt(isUID))
639 if !p.take(" ") {
640 break
641 }
642 }
643 p.xtake(")")
644 return l
645}
646
647func xint(p *parser, s string) int {
648 v, err := strconv.ParseInt(s, 10, 32)
649 if err != nil {
650 p.xerrorf("bad int %q: %v", s, err)
651 }
652 return int(v)
653}
654
655func (p *parser) digit() (string, bool) {
656 if p.empty() {
657 return "", false
658 }
659 c := p.orig[p.o]
660 if c < '0' || c > '9' {
661 return "", false
662 }
663 s := p.orig[p.o : p.o+1]
664 p.o++
665 return s, true
666}
667
668func (p *parser) xdigit() string {
669 s, ok := p.digit()
670 if !ok {
671 p.xerrorf("expected digit")
672 }
673 return s
674}
675
676// ../rfc/9051:6492 ../rfc/3501:4695
677func (p *parser) xdateDayFixed() int {
678 if p.take(" ") {
679 return xint(p, p.xdigit())
680 }
681 return xint(p, p.xdigit()+p.xdigit())
682}
683
684var months = []string{"jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"}
685
686// ../rfc/9051:6495 ../rfc/3501:4698
687func (p *parser) xdateMonth() time.Month {
688 s := strings.ToLower(p.xtaken(3))
689 for i, m := range months {
690 if m == s {
691 return time.Month(1 + i)
692 }
693 }
694 p.xerrorf("unknown month %q", s)
695 return 0
696}
697
698// ../rfc/9051:7120 ../rfc/3501:5067
699func (p *parser) xtime() (int, int, int) {
700 h := xint(p, p.xtaken(2))
701 p.xtake(":")
702 m := xint(p, p.xtaken(2))
703 p.xtake(":")
704 s := xint(p, p.xtaken(2))
705 return h, m, s
706}
707
708// ../rfc/9051:7159 ../rfc/3501:5083
709func (p *parser) xzone() (string, int) {
710 sign := p.xtakelist("+", "-")
711 s := p.xtaken(4)
712 v := xint(p, s)
713 seconds := (v/100)*3600 + (v%100)*60
714 if sign[0] == '-' {
715 seconds = -seconds
716 }
717 return sign + s, seconds
718}
719
720// ../rfc/9051:6502 ../rfc/3501:4713
721func (p *parser) xdateTime() time.Time {
722 // DQUOTE date-day-fixed "-" date-month "-" date-year SP time SP zone DQUOTE
723 p.xtake(`"`)
724 day := p.xdateDayFixed()
725 p.xtake("-")
726 month := p.xdateMonth()
727 p.xtake("-")
728 year := xint(p, p.xtaken(4))
729 p.xspace()
730 hours, minutes, seconds := p.xtime()
731 p.xspace()
732 name, zoneSeconds := p.xzone()
733 p.xtake(`"`)
734 loc := time.FixedZone(name, zoneSeconds)
735 return time.Date(year, month, day, hours, minutes, seconds, 0, loc)
736}
737
738// ../rfc/9051:6655 ../rfc/7888:330 ../rfc/3501:4801
739func (p *parser) xliteralSize(maxSize int64, lit8 bool) (size int64, sync bool) {
740 // todo: enforce that we get non-binary when ~ isn't present?
741 if lit8 {
742 p.take("~")
743 }
744 p.xtake("{")
745 size = p.xnumber64()
746 if maxSize > 0 && size > maxSize {
747 // ../rfc/7888:249
748 line := fmt.Sprintf("* BYE [ALERT] Max literal size %d is larger than allowed %d in this context", size, maxSize)
749 err := errors.New("literal too big")
750 panic(syntaxError{line, "TOOBIG", err.Error(), err})
751 }
752
753 sync = !p.take("+")
754 p.xtake("}")
755 p.xempty()
756 return size, sync
757}
758
759var searchKeyWords = []string{
760 "ALL", "ANSWERED", "BCC",
761 "BEFORE", "BODY",
762 "CC", "DELETED", "FLAGGED",
763 "FROM", "KEYWORD",
764 "NEW", "OLD", "ON", "RECENT", "SEEN",
765 "SINCE", "SUBJECT",
766 "TEXT", "TO",
767 "UNANSWERED", "UNDELETED", "UNFLAGGED",
768 "UNKEYWORD", "UNSEEN",
769 "DRAFT", "HEADER",
770 "LARGER", "NOT",
771 "OR",
772 "SENTBEFORE", "SENTON",
773 "SENTSINCE", "SMALLER",
774 "UID", "UNDRAFT",
775 "MODSEQ", // CONDSTORE extension.
776}
777
778// ../rfc/9051:6923 ../rfc/3501:4957, MODSEQ ../rfc/7162:2492
779// differences: rfc 9051 removes NEW, OLD, RECENT and makes SMALLER and LARGER number64 instead of number.
780func (p *parser) xsearchKey() *searchKey {
781 if p.take("(") {
782 sk := p.xsearchKey()
783 l := []searchKey{*sk}
784 for !p.take(")") {
785 p.xspace()
786 l = append(l, *p.xsearchKey())
787 }
788 return &searchKey{searchKeys: l}
789 }
790
791 w, ok := p.takelist(searchKeyWords...)
792 if !ok {
793 seqs := p.xnumSet()
794 return &searchKey{seqSet: &seqs}
795 }
796
797 sk := &searchKey{op: w}
798 switch sk.op {
799 case "ALL":
800 case "ANSWERED":
801 case "BCC":
802 p.xspace()
803 sk.astring = p.xastring()
804 case "BEFORE":
805 p.xspace()
806 sk.date = p.xdate()
807 case "BODY":
808 p.xspace()
809 sk.astring = p.xastring()
810 case "CC":
811 p.xspace()
812 sk.astring = p.xastring()
813 case "DELETED":
814 case "FLAGGED":
815 case "FROM":
816 p.xspace()
817 sk.astring = p.xastring()
818 case "KEYWORD":
819 p.xspace()
820 sk.atom = p.xatom()
821 case "NEW":
822 case "OLD":
823 case "ON":
824 p.xspace()
825 sk.date = p.xdate()
826 case "RECENT":
827 case "SEEN":
828 case "SINCE":
829 p.xspace()
830 sk.date = p.xdate()
831 case "SUBJECT":
832 p.xspace()
833 sk.astring = p.xastring()
834 case "TEXT":
835 p.xspace()
836 sk.astring = p.xastring()
837 case "TO":
838 p.xspace()
839 sk.astring = p.xastring()
840 case "UNANSWERED":
841 case "UNDELETED":
842 case "UNFLAGGED":
843 case "UNKEYWORD":
844 p.xspace()
845 sk.atom = p.xatom()
846 case "UNSEEN":
847 case "DRAFT":
848 case "HEADER":
849 p.xspace()
850 sk.headerField = p.xastring()
851 p.xspace()
852 sk.astring = p.xastring()
853 case "LARGER":
854 p.xspace()
855 sk.number = p.xnumber64()
856 case "NOT":
857 p.xspace()
858 sk.searchKey = p.xsearchKey()
859 case "OR":
860 p.xspace()
861 sk.searchKey = p.xsearchKey()
862 p.xspace()
863 sk.searchKey2 = p.xsearchKey()
864 case "SENTBEFORE":
865 p.xspace()
866 sk.date = p.xdate()
867 case "SENTON":
868 p.xspace()
869 sk.date = p.xdate()
870 case "SENTSINCE":
871 p.xspace()
872 sk.date = p.xdate()
873 case "SMALLER":
874 p.xspace()
875 sk.number = p.xnumber64()
876 case "UID":
877 p.xspace()
878 sk.uidSet = p.xnumSet()
879 case "UNDRAFT":
880 case "MODSEQ":
881 // ../rfc/7162:1045 ../rfc/7162:2499
882 p.xspace()
883 if p.take(`"`) {
884 // We don't do anything with this field, so parse and ignore.
885 p.xtake(`/FLAGS/`)
886 if p.take(`\`) {
887 p.xtake(`\`) // ../rfc/7162:1072
888 }
889 p.xatom()
890 p.xtake(`"`)
891 p.xspace()
892 p.xtakelist("PRIV", "SHARED", "ALL")
893 p.xspace()
894 }
895 v := p.xnumber64()
896 sk.clientModseq = &v
897 // MODSEQ is a CONDSTORE-enabling parameter. ../rfc/7162:377
898 p.conn.enabled[capCondstore] = true
899 default:
900 p.xerrorf("missing case for op %q", sk.op)
901 }
902 return sk
903}
904
905// hasModseq returns whether there is a modseq filter anywhere in the searchkey.
906func (sk searchKey) hasModseq() bool {
907 if sk.clientModseq != nil {
908 return true
909 }
910 for _, e := range sk.searchKeys {
911 if e.hasModseq() {
912 return true
913 }
914 }
915 if sk.searchKey != nil && sk.searchKey.hasModseq() {
916 return true
917 }
918 if sk.searchKey2 != nil && sk.searchKey2.hasModseq() {
919 return true
920 }
921 return false
922}
923
924// ../rfc/9051:6489 ../rfc/3501:4692
925func (p *parser) xdateDay() int {
926 d := p.xdigit()
927 if s, ok := p.digit(); ok {
928 d += s
929 }
930 return xint(p, d)
931}
932
933// ../rfc/9051:6487 ../rfc/3501:4690
934func (p *parser) xdate() time.Time {
935 dquote := p.take(`"`)
936 day := p.xdateDay()
937 p.xtake("-")
938 mon := p.xdateMonth()
939 p.xtake("-")
940 year := xint(p, p.xtaken(4))
941 if dquote {
942 p.take(`"`)
943 }
944 return time.Date(year, mon, day, 0, 0, 0, 0, time.UTC)
945}
946