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