14 char = charRange('\x01', '\x7f')
15 ctl = charRange('\x01', '\x19')
18 atomChar = charRemove(char, "(){ "+ctl+listWildcards+quotedSpecials+respSpecials)
19 astringChar = atomChar + respSpecials
22func charRange(first, last rune) string {
33func charRemove(s, remove string) string {
37 for _, x := range remove {
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.
53 o int // Current offset in parsing.
54 contexts []string // What we're parsing, for error messages.
55 literals int // Literals in command, for limit.
56 literalSize int64 // Total size of literals in command, for limit.
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 {
67 if c >= 'a' && c <= 'z' {
74func newParser(s string, conn *conn) *parser {
75 return &parser{s, toUpper(s), 0, nil, 0, 0, conn}
78func (p *parser) xerrorf(format string, args ...any) {
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, ",")
85 remaining = " (" + remaining + ")"
86 if p.conn.account != nil {
88 err = errors.New(errmsg)
90 err = errors.New(errmsg + remaining)
92 panic(syntaxError{"", "", errmsg, err})
95func (p *parser) context(s string) func() {
96 p.contexts = append(p.contexts, s)
98 p.contexts = p.contexts[:len(p.contexts)-1]
102func (p *parser) empty() bool {
103 return p.o == len(p.upper)
106func (p *parser) xempty() {
108 p.xerrorf("leftover data")
112func (p *parser) hasPrefix(s string) bool {
113 return strings.HasPrefix(p.upper[p.o:], s)
116func (p *parser) take(s string) bool {
124func (p *parser) xtake(s string) {
126 p.xerrorf("expected %s", s)
130func (p *parser) xnonempty() {
132 p.xerrorf("unexpected end")
136func (p *parser) xtakeall() string {
142func (p *parser) xtake1n(n int, what string) string {
144 p.xerrorf("expected chars from %s", what)
149func (p *parser) xtakechars(s string, what string) string {
151 for i, c := range p.orig[p.o:] {
153 return p.xtake1n(i, what)
159func (p *parser) xtaken(n int) string {
160 if p.o+n > len(p.orig) {
161 p.xerrorf("not enough data")
163 r := p.orig[p.o : p.o+n]
168func (p *parser) space() bool {
172func (p *parser) xspace() {
174 p.xerrorf("expected space")
178func (p *parser) digits() string {
180 for _, c := range p.upper[p.o:] {
181 if c < '0' || c > '9' {
189 s := p.upper[p.o : p.o+n]
194func (p *parser) nznumber() (uint32, bool) {
196 for o < len(p.upper) && p.upper[o] >= '0' && p.upper[o] <= '9' {
202 if n, err := strconv.ParseUint(p.upper[p.o:o], 10, 32); err != nil {
208 return uint32(n), true
212func (p *parser) xnznumber() uint32 {
213 n, ok := p.nznumber()
215 p.xerrorf("expected non-zero number")
220func (p *parser) number() (uint32, bool) {
222 for o < len(p.upper) && p.upper[o] >= '0' && p.upper[o] <= '9' {
228 n, err := strconv.ParseUint(p.upper[p.o:o], 10, 32)
233 return uint32(n), true
236func (p *parser) xnumber() uint32 {
239 p.xerrorf("expected number")
244func (p *parser) xnumber64() int64 {
247 p.xerrorf("expected number64")
251 p.xerrorf("parsing number64 %q: %v", s, err)
256func (p *parser) xnznumber64() int64 {
259 p.xerrorf("expected non-zero number64")
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 {
274func (p *parser) xtakelist(l ...string) string {
275 w, ok := p.takelist(l...)
277 p.xerrorf("expected one of %s", strings.Join(l, ","))
282func (p *parser) xstring() (r string) {
286 for i, c := range p.orig[p.o:] {
289 } else if c == '\x00' || c == '\r' || c == '\n' {
290 p.xerrorf("invalid nul, cr or lf in string")
292 if c == '\\' || c == '"' {
296 p.xerrorf("invalid escape char %c", c)
305 p.xerrorf("missing closing dquote in string")
307 size, sync := p.xliteralSize(false, true)
308 s := p.conn.xreadliteral(size, sync)
309 line := p.conn.readline(false)
310 p.orig, p.upper, p.o = line, toUpper(line), 0
314func (p *parser) xnil() {
318// Returns NIL as empty string.
319func (p *parser) xnilString() string {
326func (p *parser) xastring() string {
327 if p.hasPrefix(`"`) || p.hasPrefix("{") || p.hasPrefix("~{") {
330 return p.xtakechars(astringChar, "astring")
333func contains(s string, c rune) bool {
334 for _, x := range s {
342func (p *parser) xtag() string {
344 for i, c := range p.orig[p.o:] {
345 if c == '+' || !contains(astringChar, c) {
346 return p.xtake1n(i, "tag")
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")
361func (p *parser) remainder() string {
366func (p *parser) xflag() string {
367 w, _ := p.takelist(`\`, "$")
370 switch strings.ToLower(s) {
371 case `\answered`, `\flagged`, `\deleted`, `\seen`, `\draft`:
373 p.xerrorf("unknown system flag %s", s)
379func (p *parser) xflagList() (l []string) {
381 if !p.hasPrefix(")") {
382 l = append(l, p.xflag())
386 l = append(l, p.xflag())
391func (p *parser) xatom() string {
392 return p.xtakechars(atomChar, "atom")
395func (p *parser) xdecodeMailbox(s string) string {
396 // UTF-7 is deprecated for IMAP4rev2-only clients, and not used with UTF8=ACCEPT.
397 // The future should be without UTF-7, we don't encode/decode it with modern
398 // clients. Most clients are IMAP4rev1, we need to handle UTF-7.
401 if p.conn.utf8strings() {
404 ns, err := utf7decode(s)
406 p.xerrorf("decoding utf7 mailbox name: %v", err)
411func (p *parser) xmailbox() string {
413 return p.xdecodeMailbox(s)
417func (p *parser) xlistMailbox() string {
419 if p.hasPrefix(`"`) || p.hasPrefix("{") {
422 s = p.xtakechars(atomChar+listWildcards+respSpecials, "list-char")
424 // Presumably UTF-7 encoding applies to mailbox patterns too.
425 return p.xdecodeMailbox(s)
429func (p *parser) xmboxOrPat() ([]string, bool) {
431 return []string{p.xlistMailbox()}, false
433 l := []string{p.xlistMailbox()}
436 l = append(l, p.xlistMailbox())
442func (p *parser) xstatusAtt() string {
443 w := p.xtakelist("MESSAGES", "UIDNEXT", "UIDVALIDITY", "UNSEEN", "DELETED-STORAGE", "DELETED", "SIZE", "RECENT", "APPENDLIMIT", "HIGHESTMODSEQ")
444 if w == "HIGHESTMODSEQ" {
446 p.conn.enabled[capCondstore] = true
452func (p *parser) xnumSet0(allowStar, allowSearch bool) (r numSet) {
453 defer p.context("numSet")()
454 if allowSearch && p.take("$") {
455 return numSet{searchResult: true}
457 r.ranges = append(r.ranges, p.xnumRange0(allowStar))
459 r.ranges = append(r.ranges, p.xnumRange0(allowStar))
464func (p *parser) xnumSet() (r numSet) {
465 return p.xnumSet0(true, true)
468// parse numRange, which can be just a setNumber.
469func (p *parser) xnumRange0(allowStar bool) (r numRange) {
470 if allowStar && p.take("*") {
473 r.first.number = p.xnznumber()
476 r.last = &setNumber{}
477 if allowStar && p.take("*") {
480 r.last.number = p.xnznumber()
487func (p *parser) xsectionMsgtext() (r *sectionMsgtext) {
488 defer p.context("sectionMsgtext")()
489 msgtextWords := []string{"HEADER.FIELDS.NOT", "HEADER.FIELDS", "HEADER", "TEXT"}
490 w := p.xtakelist(msgtextWords...)
491 r = §ionMsgtext{s: w}
492 if strings.HasPrefix(w, "HEADER.FIELDS") {
495 r.headers = append(r.headers, textproto.CanonicalMIMEHeaderKey(p.xastring()))
501 r.headers = append(r.headers, textproto.CanonicalMIMEHeaderKey(p.xastring()))
508func (p *parser) xsectionSpec() (r *sectionSpec) {
509 defer p.context("parseSectionSpec")()
511 n, ok := p.nznumber()
513 return §ionSpec{msgtext: p.xsectionMsgtext()}
515 defer p.context("part...")()
517 pt.part = append(pt.part, n)
522 if n, ok := p.nznumber(); ok {
523 pt.part = append(pt.part, n)
527 pt.text = §ionText{mime: true}
530 pt.text = §ionText{msgtext: p.xsectionMsgtext()}
533 return §ionSpec{part: pt}
537func (p *parser) xsection() *sectionSpec {
538 defer p.context("parseSection")()
541 return §ionSpec{}
543 r := p.xsectionSpec()
549func (p *parser) xpartial() *partial {
551 offset := p.xnumber()
553 count := p.xnznumber()
555 return &partial{offset, count}
559func (p *parser) xsectionBinary() (r []uint32) {
564 r = append(r, p.xnznumber())
569 r = append(r, p.xnznumber())
575var fetchAttWords = []string{
576 "ENVELOPE", "FLAGS", "INTERNALDATE", "RFC822.SIZE", "BODYSTRUCTURE", "UID", "BODY.PEEK", "BODY", "BINARY.PEEK", "BINARY.SIZE", "BINARY",
577 "RFC822.HEADER", "RFC822.TEXT", "RFC822", // older IMAP
578 "MODSEQ", // CONDSTORE extension.
582func (p *parser) xfetchAtt(isUID bool) (r fetchAtt) {
583 defer p.context("fetchAtt")()
584 f := p.xtakelist(fetchAttWords...)
585 r.peek = strings.HasSuffix(f, ".PEEK")
586 r.field = strings.TrimSuffix(f, ".PEEK")
590 if p.hasPrefix("[") {
591 r.section = p.xsection()
592 if p.hasPrefix("<") {
593 r.partial = p.xpartial()
597 r.sectionBinary = p.xsectionBinary()
598 if p.hasPrefix("<") {
599 r.partial = p.xpartial()
602 r.sectionBinary = p.xsectionBinary()
604 // The RFC text mentions MODSEQ is only for FETCH, not UID FETCH, but the ABNF adds
605 // the attribute to the shared syntax, so UID FETCH also implements it.
609 p.conn.xensureCondstore(nil)
615func (p *parser) xfetchAtts(isUID bool) []fetchAtt {
616 defer p.context("fetchAtts")()
618 fields := func(l ...string) []fetchAtt {
619 r := make([]fetchAtt, len(l))
620 for i, s := range l {
621 r[i] = fetchAtt{field: s}
626 if w, ok := p.takelist("ALL", "FAST", "FULL"); ok {
629 return fields("FLAGS", "INTERNALDATE", "RFC822.SIZE", "ENVELOPE")
631 return fields("FLAGS", "INTERNALDATE", "RFC822.SIZE")
633 return fields("FLAGS", "INTERNALDATE", "RFC822.SIZE", "ENVELOPE", "BODY")
635 panic("missing case")
638 if !p.hasPrefix("(") {
639 return []fetchAtt{p.xfetchAtt(isUID)}
645 l = append(l, p.xfetchAtt(isUID))
654func xint(p *parser, s string) int {
655 v, err := strconv.ParseInt(s, 10, 32)
657 p.xerrorf("bad int %q: %v", s, err)
662func (p *parser) digit() (string, bool) {
667 if c < '0' || c > '9' {
670 s := p.orig[p.o : p.o+1]
675func (p *parser) xdigit() string {
678 p.xerrorf("expected digit")
684func (p *parser) xdateDayFixed() int {
686 return xint(p, p.xdigit())
688 return xint(p, p.xdigit()+p.xdigit())
691var months = []string{"jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"}
694func (p *parser) xdateMonth() time.Month {
695 s := strings.ToLower(p.xtaken(3))
696 for i, m := range months {
698 return time.Month(1 + i)
701 p.xerrorf("unknown month %q", s)
706func (p *parser) xtime() (int, int, int) {
707 h := xint(p, p.xtaken(2))
709 m := xint(p, p.xtaken(2))
711 s := xint(p, p.xtaken(2))
716func (p *parser) xzone() (string, int) {
717 sign := p.xtakelist("+", "-")
720 seconds := (v/100)*3600 + (v%100)*60
724 return sign + s, seconds
728func (p *parser) xdateTime() time.Time {
729 // DQUOTE date-day-fixed "-" date-month "-" date-year SP time SP zone DQUOTE
731 day := p.xdateDayFixed()
733 month := p.xdateMonth()
735 year := xint(p, p.xtaken(4))
737 hours, minutes, seconds := p.xtime()
739 name, zoneSeconds := p.xzone()
741 loc := time.FixedZone(name, zoneSeconds)
742 return time.Date(year, month, day, hours, minutes, seconds, 0, loc)
746func (p *parser) xliteralSize(lit8 bool, checkSize bool) (size int64, sync bool) {
747 // todo: enforce that we get non-binary when ~ isn't present?
762 litSizeMax = 100 * 1024
763 totalLitSizeMax = 10 * litSizeMax
766 p.literalSize += size
768 if size > litSizeMax {
769 errmsg = fmt.Sprintf("max literal size %d is larger than allowed %d", size, litSizeMax)
770 } else if p.literalSize > totalLitSizeMax {
771 errmsg = fmt.Sprintf("max total literal size for command %d is larger than allowed %d", p.literalSize, totalLitSizeMax)
772 } else if p.literals > litMax {
773 errmsg = fmt.Sprintf("max literals for command %d is larger than allowed %d", p.literals, litMax)
777 err := errors.New("literal too big: " + errmsg)
781 errmsg = "* BYE [ALERT] " + errmsg
783 panic(syntaxError{errmsg, "TOOBIG", err.Error(), err})
790var searchKeyWords = []string{
791 "ALL", "ANSWERED", "BCC",
793 "CC", "DELETED", "FLAGGED",
795 "NEW", "OLD", "ON", "RECENT", "SEEN",
798 "UNANSWERED", "UNDELETED", "UNFLAGGED",
799 "UNKEYWORD", "UNSEEN",
803 "SENTBEFORE", "SENTON",
804 "SENTSINCE", "SMALLER",
806 "MODSEQ", // CONDSTORE extension.
810// differences: rfc 9051 removes NEW, OLD, RECENT and makes SMALLER and LARGER number64 instead of number.
811func (p *parser) xsearchKey() *searchKey {
814 l := []searchKey{*sk}
817 l = append(l, *p.xsearchKey())
819 return &searchKey{searchKeys: l}
822 w, ok := p.takelist(searchKeyWords...)
825 return &searchKey{seqSet: &seqs}
828 sk := &searchKey{op: w}
834 sk.astring = p.xastring()
840 sk.astring = p.xastring()
843 sk.astring = p.xastring()
848 sk.astring = p.xastring()
864 sk.astring = p.xastring()
867 sk.astring = p.xastring()
870 sk.astring = p.xastring()
881 sk.headerField = p.xastring()
883 sk.astring = p.xastring()
886 sk.number = p.xnumber64()
889 sk.searchKey = p.xsearchKey()
892 sk.searchKey = p.xsearchKey()
894 sk.searchKey2 = p.xsearchKey()
906 sk.number = p.xnumber64()
909 sk.uidSet = p.xnumSet()
915 // We don't do anything with this field, so parse and ignore.
923 p.xtakelist("PRIV", "SHARED", "ALL")
929 p.conn.enabled[capCondstore] = true
931 p.xerrorf("missing case for op %q", sk.op)
936// hasModseq returns whether there is a modseq filter anywhere in the searchkey.
937func (sk searchKey) hasModseq() bool {
938 if sk.clientModseq != nil {
941 for _, e := range sk.searchKeys {
946 if sk.searchKey != nil && sk.searchKey.hasModseq() {
949 if sk.searchKey2 != nil && sk.searchKey2.hasModseq() {
956func (p *parser) xdateDay() int {
958 if s, ok := p.digit(); ok {
965func (p *parser) xdate() time.Time {
966 dquote := p.take(`"`)
969 mon := p.xdateMonth()
971 year := xint(p, p.xtaken(4))
975 return time.Date(year, mon, day, 0, 0, 0, 0, time.UTC)