package imapclient import ( "fmt" "io" "strconv" "strings" ) func (c *Conn) recorded() string { s := string(c.recordBuf) c.recordBuf = nil c.record = false return s } func (c *Conn) recordAdd(buf []byte) { if c.record { c.recordBuf = append(c.recordBuf, buf...) } } func (c *Conn) xtake(s string) { buf := make([]byte, len(s)) _, err := io.ReadFull(c.r, buf) c.xcheckf(err, "taking %q", s) if !strings.EqualFold(string(buf), s) { c.xerrorf("got %q, expected %q", buf, s) } c.recordAdd(buf) } func (c *Conn) readbyte() (byte, error) { b, err := c.r.ReadByte() if err == nil { c.recordAdd([]byte{b}) } return b, err } func (c *Conn) unreadbyte() { if c.record { c.recordBuf = c.recordBuf[:len(c.recordBuf)-1] } err := c.r.UnreadByte() c.xcheckf(err, "unread byte") } func (c *Conn) readrune() (rune, error) { x, _, err := c.r.ReadRune() if err == nil { c.recordAdd([]byte(string(x))) } return x, err } func (c *Conn) space() bool { return c.take(' ') } func (c *Conn) xspace() { c.xtake(" ") } func (c *Conn) xcrlf() { c.xtake("\r\n") } func (c *Conn) peek(exp byte) bool { b, err := c.readbyte() if err == nil { c.unreadbyte() } return err == nil && strings.EqualFold(string(rune(b)), string(rune(exp))) } func (c *Conn) peekstring() bool { return c.peek('"') || c.peek('{') } func (c *Conn) take(exp byte) bool { if c.peek(exp) { _, _ = c.readbyte() return true } return false } func (c *Conn) xstatus() Status { w := c.xword() W := strings.ToUpper(w) switch W { case "OK": return OK case "NO": return NO case "BAD": return BAD } c.xerrorf("expected status, got %q", w) panic("not reached") } // Already consumed: tag SP status SP func (c *Conn) xresult(status Status) Result { respText := c.xrespText() return Result{status, respText} } func (c *Conn) xrespText() RespText { var code string var codeArg CodeArg if c.take('[') { code, codeArg = c.xrespCode() c.xtake("]") c.xspace() } more := "" for !c.peek('\r') { more += string(rune(c.xbyte())) } return RespText{code, codeArg, more} } var knownCodes = stringMap( // Without parameters. "ALERT", "PARSE", "READ-ONLY", "READ-WRITE", "TRYCREATE", "UIDNOTSTICKY", "UNAVAILABLE", "AUTHENTICATIONFAILED", "AUTHORIZATIONFAILED", "EXPIRED", "PRIVACYREQUIRED", "CONTACTADMIN", "NOPERM", "INUSE", "EXPUNGEISSUED", "CORRUPTION", "SERVERBUG", "CLIENTBUG", "CANNOT", "LIMIT", "OVERQUOTA", "ALREADYEXISTS", "NONEXISTENT", "NOTSAVED", "HASCHILDREN", "CLOSED", "UNKNOWN-CTE", "OVERQUOTA", // ../rfc/9208:472 // With parameters. "BADCHARSET", "CAPABILITY", "PERMANENTFLAGS", "UIDNEXT", "UIDVALIDITY", "UNSEEN", "APPENDUID", "COPYUID", "HIGHESTMODSEQ", "MODIFIED", ) func stringMap(l ...string) map[string]struct{} { r := map[string]struct{}{} for _, s := range l { r[s] = struct{}{} } return r } // ../rfc/9051:6895 func (c *Conn) xrespCode() (string, CodeArg) { w := "" for !c.peek(' ') && !c.peek(']') { w += string(rune(c.xbyte())) } W := strings.ToUpper(w) if _, ok := knownCodes[W]; !ok { var args []string for c.space() { arg := "" for !c.peek(' ') && !c.peek(']') { arg += string(rune(c.xbyte())) } args = append(args, arg) } return W, CodeOther{W, args} } var codeArg CodeArg switch W { case "BADCHARSET": var l []string // Must be nil initially. if c.space() { c.xtake("(") l = []string{c.xcharset()} for c.space() { l = append(l, c.xcharset()) } c.xtake(")") } codeArg = CodeList{W, l} case "CAPABILITY": c.xtake(" ") caps := []string{c.xatom()} for c.space() { caps = append(caps, c.xatom()) } c.CapAvailable = map[Capability]struct{}{} for _, cap := range caps { c.CapAvailable[Capability(cap)] = struct{}{} } codeArg = CodeWords{W, caps} case "PERMANENTFLAGS": l := []string{} // Must be non-nil. if c.space() { c.xtake("(") l = []string{c.xflagPerm()} for c.space() { l = append(l, c.xflagPerm()) } c.xtake(")") } codeArg = CodeList{W, l} case "UIDNEXT", "UIDVALIDITY", "UNSEEN": c.xspace() codeArg = CodeUint{W, c.xnzuint32()} case "APPENDUID": c.xspace() destUIDValidity := c.xnzuint32() c.xspace() uid := c.xnzuint32() codeArg = CodeAppendUID{destUIDValidity, uid} case "COPYUID": c.xspace() destUIDValidity := c.xnzuint32() c.xspace() from := c.xuidset() c.xspace() to := c.xuidset() codeArg = CodeCopyUID{destUIDValidity, from, to} case "HIGHESTMODSEQ": c.xspace() codeArg = CodeHighestModSeq(c.xint64()) case "MODIFIED": c.xspace() modified := c.xuidset() codeArg = CodeModified(NumSet{Ranges: modified}) } return W, codeArg } func (c *Conn) xbyte() byte { b, err := c.readbyte() c.xcheckf(err, "read byte") return b } // take until b is seen. don't take b itself. func (c *Conn) xtakeuntil(b byte) string { var s string for { x, err := c.readbyte() c.xcheckf(err, "read byte") if x == b { c.unreadbyte() return s } s += string(rune(x)) } } func (c *Conn) xdigits() string { var s string for { b, err := c.readbyte() if err == nil && (b >= '0' && b <= '9') { s += string(rune(b)) continue } c.unreadbyte() return s } } func (c *Conn) peekdigit() bool { if b, err := c.readbyte(); err == nil { c.unreadbyte() return b >= '0' && b <= '9' } return false } func (c *Conn) xint32() int32 { s := c.xdigits() num, err := strconv.ParseInt(s, 10, 32) c.xcheckf(err, "parsing int32") return int32(num) } func (c *Conn) xint64() int64 { s := c.xdigits() num, err := strconv.ParseInt(s, 10, 63) c.xcheckf(err, "parsing int64") return num } func (c *Conn) xuint32() uint32 { s := c.xdigits() num, err := strconv.ParseUint(s, 10, 32) c.xcheckf(err, "parsing uint32") return uint32(num) } func (c *Conn) xnzuint32() uint32 { v := c.xuint32() if v == 0 { c.xerrorf("got 0, expected nonzero uint") } return v } // todo: replace with proper parsing. func (c *Conn) xnonspace() string { var s string for !c.peek(' ') && !c.peek('\r') && !c.peek('\n') { s += string(rune(c.xbyte())) } if s == "" { c.xerrorf("expected non-space") } return s } // todo: replace with proper parsing func (c *Conn) xword() string { return c.xatom() } // "*" SP is already consumed // ../rfc/9051:6868 func (c *Conn) xuntagged() Untagged { w := c.xnonspace() W := strings.ToUpper(w) switch W { case "PREAUTH": c.xspace() r := UntaggedPreauth(c.xrespText()) c.xcrlf() return r case "BYE": c.xspace() r := UntaggedBye(c.xrespText()) c.xcrlf() return r case "OK", "NO", "BAD": c.xspace() r := UntaggedResult(c.xresult(Status(W))) c.xcrlf() return r case "CAPABILITY": // ../rfc/9051:6427 var caps []string for c.space() { caps = append(caps, c.xnonspace()) } c.CapAvailable = map[Capability]struct{}{} for _, cap := range caps { c.CapAvailable[Capability(cap)] = struct{}{} } r := UntaggedCapability(caps) c.xcrlf() return r case "ENABLED": // ../rfc/9051:6520 var caps []string for c.space() { caps = append(caps, c.xnonspace()) } for _, cap := range caps { c.CapEnabled[Capability(cap)] = struct{}{} } r := UntaggedEnabled(caps) c.xcrlf() return r case "FLAGS": c.xspace() r := UntaggedFlags(c.xflagList()) c.xcrlf() return r case "LIST": c.xspace() r := c.xmailboxList() c.xcrlf() return r case "STATUS": // ../rfc/9051:6681 c.xspace() mailbox := c.xastring() c.xspace() c.xtake("(") attrs := map[StatusAttr]int64{} for !c.take(')') { if len(attrs) > 0 { c.xspace() } s := c.xatom() c.xspace() S := StatusAttr(strings.ToUpper(s)) var num int64 // ../rfc/9051:7059 switch S { case "MESSAGES": num = int64(c.xuint32()) case "UIDNEXT": num = int64(c.xnzuint32()) case "UIDVALIDITY": num = int64(c.xnzuint32()) case "UNSEEN": num = int64(c.xuint32()) case "DELETED": num = int64(c.xuint32()) case "SIZE": num = c.xint64() case "RECENT": c.xneedDisabled("RECENT status flag", CapIMAP4rev2) num = int64(c.xuint32()) case "APPENDLIMIT": if c.peek('n') || c.peek('N') { c.xtake("nil") } else { num = c.xint64() } case "HIGHESTMODSEQ": num = c.xint64() case "DELETED-STORAGE": num = c.xint64() default: c.xerrorf("status: unknown attribute %q", s) } if _, ok := attrs[S]; ok { c.xerrorf("status: duplicate attribute %q", s) } attrs[S] = num } r := UntaggedStatus{mailbox, attrs} c.xcrlf() return r case "NAMESPACE": // ../rfc/9051:6778 c.xspace() personal := c.xnamespace() c.xspace() other := c.xnamespace() c.xspace() shared := c.xnamespace() r := UntaggedNamespace{personal, other, shared} c.xcrlf() return r case "SEARCH": // ../rfc/9051:6809 c.xneedDisabled("untagged SEARCH response", CapIMAP4rev2) var nums []uint32 for c.space() { // ../rfc/7162:2557 if c.take('(') { c.xtake("MODSEQ") c.xspace() modseq := c.xint64() c.xtake(")") c.xcrlf() return UntaggedSearchModSeq{nums, modseq} } nums = append(nums, c.xnzuint32()) } r := UntaggedSearch(nums) c.xcrlf() return r case "ESEARCH": r := c.xesearchResponse() c.xcrlf() return r case "LSUB": c.xneedDisabled("untagged LSUB response", CapIMAP4rev2) r := c.xlsub() c.xcrlf() return r case "ID": // ../rfc/2971:243 c.xspace() var params map[string]string if c.take('(') { params = map[string]string{} for !c.take(')') { if len(params) > 0 { c.xspace() } k := c.xstring() c.xspace() v := c.xnilString() if _, ok := params[k]; ok { c.xerrorf("duplicate key %q", k) } params[k] = v } } else { c.xtake("nil") } c.xcrlf() return UntaggedID(params) // ../rfc/7162:2623 case "VANISHED": c.xspace() var earlier bool if c.take('(') { c.xtake("EARLIER") c.xtake(")") c.xspace() earlier = true } uids := c.xuidset() c.xcrlf() return UntaggedVanished{earlier, NumSet{Ranges: uids}} // ../rfc/9208:668 ../2087:242 case "QUOTAROOT": c.xspace() c.xastring() var roots []string for c.space() { root := c.xastring() roots = append(roots, root) } c.xcrlf() return UntaggedQuotaroot(roots) // ../rfc/9208:666 ../rfc/2087:239 case "QUOTA": c.xspace() root := c.xastring() c.xspace() c.xtake("(") xresource := func() QuotaResource { name := c.xatom() c.xspace() usage := c.xint64() c.xspace() limit := c.xint64() return QuotaResource{QuotaResourceName(strings.ToUpper(name)), usage, limit} } seen := map[QuotaResourceName]bool{} l := []QuotaResource{xresource()} seen[l[0].Name] = true for c.space() { res := xresource() if seen[res.Name] { c.xerrorf("duplicate resource name %q", res.Name) } seen[res.Name] = true l = append(l, res) } c.xtake(")") c.xcrlf() return UntaggedQuota{root, l} default: v, err := strconv.ParseUint(w, 10, 32) if err == nil { num := uint32(v) c.xspace() w = c.xword() W = strings.ToUpper(w) switch W { case "FETCH": if num == 0 { c.xerrorf("invalid zero number for untagged fetch response") } c.xspace() r := c.xfetch(num) c.xcrlf() return r case "EXPUNGE": if num == 0 { c.xerrorf("invalid zero number for untagged expunge response") } c.xcrlf() return UntaggedExpunge(num) case "EXISTS": c.xcrlf() return UntaggedExists(num) case "RECENT": c.xneedDisabled("should not send RECENT in IMAP4rev2", CapIMAP4rev2) c.xcrlf() return UntaggedRecent(num) default: c.xerrorf("unknown untagged numbered response %q", w) panic("not reached") } } c.xerrorf("unknown untagged response %q", w) } panic("not reached") } // ../rfc/3501:4864 ../rfc/9051:6742 // Already parsed: "*" SP nznumber SP "FETCH" SP func (c *Conn) xfetch(num uint32) UntaggedFetch { c.xtake("(") attrs := []FetchAttr{c.xmsgatt1()} for c.space() { attrs = append(attrs, c.xmsgatt1()) } c.xtake(")") return UntaggedFetch{num, attrs} } // ../rfc/9051:6746 func (c *Conn) xmsgatt1() FetchAttr { f := "" for { b := c.xbyte() if b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b >= '0' && b <= '9' || b == '.' { f += string(rune(b)) continue } c.unreadbyte() break } F := strings.ToUpper(f) switch F { case "FLAGS": c.xspace() c.xtake("(") var flags []string if !c.take(')') { flags = []string{c.xflag()} for c.space() { flags = append(flags, c.xflag()) } c.xtake(")") } return FetchFlags(flags) case "ENVELOPE": c.xspace() return FetchEnvelope(c.xenvelope()) case "INTERNALDATE": c.xspace() return FetchInternalDate(c.xquoted()) // todo: parsed time case "RFC822.SIZE": c.xspace() return FetchRFC822Size(c.xint64()) case "RFC822": c.xspace() s := c.xnilString() return FetchRFC822(s) case "RFC822.HEADER": c.xspace() s := c.xnilString() return FetchRFC822Header(s) case "RFC822.TEXT": c.xspace() s := c.xnilString() return FetchRFC822Text(s) case "BODY": if c.space() { return FetchBodystructure{F, c.xbodystructure(false)} } c.record = true section := c.xsection() var offset int32 if c.take('<') { offset = c.xint32() c.xtake(">") } F += c.recorded() c.xspace() body := c.xnilString() return FetchBody{F, section, offset, body} case "BODYSTRUCTURE": c.xspace() return FetchBodystructure{F, c.xbodystructure(true)} case "BINARY": c.record = true nums := c.xsectionBinary() F += c.recorded() c.xspace() buf := c.xnilStringLiteral8() return FetchBinary{F, nums, string(buf)} case "BINARY.SIZE": c.record = true nums := c.xsectionBinary() F += c.recorded() c.xspace() size := c.xint64() return FetchBinarySize{F, nums, size} case "UID": c.xspace() return FetchUID(c.xuint32()) case "MODSEQ": // ../rfc/7162:2488 c.xspace() c.xtake("(") modseq := c.xint64() c.xtake(")") return FetchModSeq(modseq) } c.xerrorf("unknown fetch attribute %q", f) panic("not reached") } func (c *Conn) xnilString() string { if c.peek('"') { return c.xquoted() } else if c.peek('{') { return string(c.xliteral()) } else { c.xtake("nil") return "" } } func (c *Conn) xstring() string { if c.peek('"') { return c.xquoted() } return string(c.xliteral()) } func (c *Conn) xastring() string { if c.peek('"') { return c.xquoted() } else if c.peek('{') { return string(c.xliteral()) } return c.xatom() } func (c *Conn) xatom() string { var s string for { b, err := c.readbyte() c.xcheckf(err, "read byte for atom") if b <= ' ' || strings.IndexByte("(){%*\"\\]", b) >= 0 { c.r.UnreadByte() if s == "" { c.xerrorf("expected atom") } return s } s += string(rune(b)) } } // ../rfc/9051:6856 ../rfc/6855:153 func (c *Conn) xquoted() string { c.xtake(`"`) s := "" for !c.take('"') { r, err := c.readrune() c.xcheckf(err, "reading rune in quoted string") if r == '\\' { r, err = c.readrune() c.xcheckf(err, "reading escaped char in quoted string") if r != '\\' && r != '"' { c.xerrorf("quoted char not backslash or dquote: %c", r) } } // todo: probably refuse some more chars. like \0 and all ctl and backspace. s += string(r) } return s } func (c *Conn) xliteral() []byte { c.xtake("{") size := c.xint64() sync := c.take('+') c.xtake("}") c.xcrlf() if size > 1<<20 { c.xerrorf("refusing to read more than 1MB: %d", size) } if sync { _, err := fmt.Fprintf(c.conn, "+ ok\r\n") c.xcheckf(err, "write continuation") } buf := make([]byte, int(size)) _, err := io.ReadFull(c.r, buf) c.xcheckf(err, "reading data for literal") return buf } // ../rfc/9051:6565 // todo: stricter func (c *Conn) xflag0(allowPerm bool) string { s := "" if c.take('\\') { s = `\` if allowPerm && c.take('*') { return `\*` } } else if c.take('$') { s = "$" } s += c.xatom() return s } func (c *Conn) xflag() string { return c.xflag0(false) } func (c *Conn) xflagPerm() string { return c.xflag0(true) } func (c *Conn) xsection() string { c.xtake("[") s := c.xtakeuntil(']') c.xtake("]") return s } func (c *Conn) xsectionBinary() []uint32 { c.xtake("[") var nums []uint32 for !c.take(']') { if len(nums) > 0 { c.xtake(".") } nums = append(nums, c.xnzuint32()) } return nums } func (c *Conn) xnilStringLiteral8() []byte { // todo: should make difference for literal8 and literal from string, which bytes are allowed if c.take('~') || c.peek('{') { return c.xliteral() } return []byte(c.xnilString()) } // ../rfc/9051:6355 func (c *Conn) xbodystructure(extensibleForm bool) any { c.xtake("(") if c.peek('(') { // ../rfc/9051:6411 parts := []any{c.xbodystructure(extensibleForm)} for c.peek('(') { parts = append(parts, c.xbodystructure(extensibleForm)) } c.xspace() mediaSubtype := c.xstring() var ext *BodyExtensionMpart if extensibleForm && c.space() { ext = c.xbodyExtMpart() } c.xtake(")") return BodyTypeMpart{parts, mediaSubtype, ext} } // todo: verify the media(sub)type is valid for returned data. var ext *BodyExtension1Part mediaType := c.xstring() c.xspace() mediaSubtype := c.xstring() c.xspace() bodyFields := c.xbodyFields() if !c.space() { // Basic type without extension. c.xtake(")") return BodyTypeBasic{mediaType, mediaSubtype, bodyFields, nil} } if c.peek('(') { // ../rfc/9051:6415 envelope := c.xenvelope() c.xspace() bodyStructure := c.xbodystructure(extensibleForm) c.xspace() lines := c.xint64() if extensibleForm && c.space() { ext = c.xbodyExt1Part() } c.xtake(")") return BodyTypeMsg{mediaType, mediaSubtype, bodyFields, envelope, bodyStructure, lines, ext} } if !strings.EqualFold(mediaType, "text") { if !extensibleForm { c.xerrorf("body result, basic type, with disallowed extensible form") } ext = c.xbodyExt1Part() // ../rfc/9051:6407 c.xtake(")") return BodyTypeBasic{mediaType, mediaSubtype, bodyFields, ext} } // ../rfc/9051:6418 lines := c.xint64() if extensibleForm && c.space() { ext = c.xbodyExt1Part() } c.xtake(")") return BodyTypeText{mediaType, mediaSubtype, bodyFields, lines, ext} } // ../rfc/9051:6376 ../rfc/3501:4604 func (c *Conn) xbodyFields() BodyFields { params := c.xbodyFldParam() c.xspace() contentID := c.xnilString() c.xspace() contentDescr := c.xnilString() c.xspace() cte := c.xnilString() c.xspace() octets := c.xint32() return BodyFields{params, contentID, contentDescr, cte, octets} } // ../rfc/9051:6371 ../rfc/3501:4599 func (c *Conn) xbodyExtMpart() (ext *BodyExtensionMpart) { ext = &BodyExtensionMpart{} ext.Params = c.xbodyFldParam() if !c.space() { return } ext.Disposition, ext.DispositionParams = c.xbodyFldDsp() if !c.space() { return } ext.Language = c.xbodyFldLang() if !c.space() { return } ext.Location = c.xbodyFldLoc() for c.space() { ext.More = append(ext.More, c.xbodyExtension()) } return } // ../rfc/9051:6366 ../rfc/3501:4584 func (c *Conn) xbodyExt1Part() (ext *BodyExtension1Part) { ext = &BodyExtension1Part{} ext.MD5 = c.xnilString() if !c.space() { return } ext.Disposition, ext.DispositionParams = c.xbodyFldDsp() if !c.space() { return } ext.Language = c.xbodyFldLang() if !c.space() { return } ext.Location = c.xbodyFldLoc() for c.space() { ext.More = append(ext.More, c.xbodyExtension()) } return } // ../rfc/9051:6401 ../rfc/3501:4626 func (c *Conn) xbodyFldParam() [][2]string { if c.take('(') { k := c.xstring() c.xspace() v := c.xstring() l := [][2]string{{k, v}} for c.space() { k = c.xstring() c.xspace() v = c.xstring() l = append(l, [2]string{k, v}) } c.xtake(")") return l } c.xtake("nil") return nil } // ../rfc/9051:6381 ../rfc/3501:4609 func (c *Conn) xbodyFldDsp() (string, [][2]string) { if !c.take('(') { c.xtake("nil") return "", nil } disposition := c.xstring() c.xspace() param := c.xbodyFldParam() c.xtake(")") return disposition, param } // ../rfc/9051:6391 ../rfc/3501:4616 func (c *Conn) xbodyFldLang() (lang []string) { if c.take('(') { lang = []string{c.xstring()} for c.space() { lang = append(lang, c.xstring()) } c.xtake(")") return lang } if c.peekstring() { return []string{c.xstring()} } c.xtake("nil") return nil } // ../rfc/9051:6393 ../rfc/3501:4618 func (c *Conn) xbodyFldLoc() string { return c.xnilString() } // ../rfc/9051:6357 ../rfc/3501:4575 func (c *Conn) xbodyExtension() (ext BodyExtension) { if c.take('(') { for { ext.More = append(ext.More, c.xbodyExtension()) if !c.space() { break } } c.xtake(")") } else if c.peekdigit() { num := c.xint64() ext.Number = &num } else if c.peekstring() { str := c.xstring() ext.String = &str } else { c.xtake("nil") } return ext } // ../rfc/9051:6522 func (c *Conn) xenvelope() Envelope { c.xtake("(") date := c.xnilString() c.xspace() subject := c.xnilString() c.xspace() from := c.xaddresses() c.xspace() sender := c.xaddresses() c.xspace() replyTo := c.xaddresses() c.xspace() to := c.xaddresses() c.xspace() cc := c.xaddresses() c.xspace() bcc := c.xaddresses() c.xspace() inReplyTo := c.xnilString() c.xspace() messageID := c.xnilString() c.xtake(")") return Envelope{date, subject, from, sender, replyTo, to, cc, bcc, inReplyTo, messageID} } // ../rfc/9051:6526 func (c *Conn) xaddresses() []Address { if !c.take('(') { c.xtake("nil") return nil } l := []Address{c.xaddress()} for !c.take(')') { l = append(l, c.xaddress()) } return l } // ../rfc/9051:6303 func (c *Conn) xaddress() Address { c.xtake("(") name := c.xnilString() c.xspace() adl := c.xnilString() c.xspace() mailbox := c.xnilString() c.xspace() host := c.xnilString() c.xtake(")") return Address{name, adl, mailbox, host} } // ../rfc/9051:6584 func (c *Conn) xflagList() []string { c.xtake("(") var l []string if !c.take(')') { l = []string{c.xflag()} for c.space() { l = append(l, c.xflag()) } c.xtake(")") } return l } // ../rfc/9051:6690 func (c *Conn) xmailboxList() UntaggedList { c.xtake("(") var flags []string if !c.peek(')') { flags = append(flags, c.xflag()) for c.space() { flags = append(flags, c.xflag()) } } c.xtake(")") c.xspace() var quoted string var b byte if c.peek('"') { quoted = c.xquoted() if len(quoted) != 1 { c.xerrorf("mailbox-list has multichar quoted part: %q", quoted) } b = byte(quoted[0]) } else if !c.peek(' ') { c.xtake("nil") } c.xspace() mailbox := c.xastring() ul := UntaggedList{flags, b, mailbox, nil, ""} if c.space() { c.xtake("(") if !c.peek(')') { c.xmboxListExtendedItem(&ul) for c.space() { c.xmboxListExtendedItem(&ul) } } c.xtake(")") } return ul } // ../rfc/9051:6699 func (c *Conn) xmboxListExtendedItem(ul *UntaggedList) { tag := c.xastring() c.xspace() if strings.ToUpper(tag) == "OLDNAME" { // ../rfc/9051:6811 c.xtake("(") name := c.xastring() c.xtake(")") ul.OldName = name return } val := c.xtaggedExtVal() ul.Extended = append(ul.Extended, MboxListExtendedItem{tag, val}) } // ../rfc/9051:7111 func (c *Conn) xtaggedExtVal() TaggedExtVal { if c.take('(') { var r TaggedExtVal if !c.take(')') { comp := c.xtaggedExtComp() r.Comp = &comp c.xtake(")") } return r } // We cannot just parse sequence-set, because we also have to accept number/number64. So first look for a number. If it is not, we continue parsing the rest of the sequence set. b, err := c.readbyte() c.xcheckf(err, "read byte for tagged-ext-val") if b < '0' || b > '9' { c.unreadbyte() ss := c.xsequenceSet() return TaggedExtVal{SeqSet: &ss} } s := c.xdigits() num, err := strconv.ParseInt(s, 10, 63) c.xcheckf(err, "parsing int") if !c.peek(':') && !c.peek(',') { // not a larger sequence-set return TaggedExtVal{Number: &num} } var sr NumRange sr.First = uint32(num) if c.take(':') { var num uint32 if !c.take('*') { num = c.xnzuint32() } sr.Last = &num } ss := c.xsequenceSet() ss.Ranges = append([]NumRange{sr}, ss.Ranges...) return TaggedExtVal{SeqSet: &ss} } // ../rfc/9051:7034 func (c *Conn) xsequenceSet() NumSet { if c.take('$') { return NumSet{SearchResult: true} } var ss NumSet for { var sr NumRange if !c.take('*') { sr.First = c.xnzuint32() } if c.take(':') { var num uint32 if !c.take('*') { num = c.xnzuint32() } sr.Last = &num } ss.Ranges = append(ss.Ranges, sr) if !c.take(',') { break } } return ss } // ../rfc/9051:7097 func (c *Conn) xtaggedExtComp() TaggedExtComp { if c.take('(') { r := c.xtaggedExtComp() c.xtake(")") return TaggedExtComp{Comps: []TaggedExtComp{r}} } s := c.xastring() if !c.peek(' ') { return TaggedExtComp{String: s} } l := []TaggedExtComp{{String: s}} for c.space() { l = append(l, c.xtaggedExtComp()) } return TaggedExtComp{Comps: l} } // ../rfc/9051:6765 func (c *Conn) xnamespace() []NamespaceDescr { if !c.take('(') { c.xtake("nil") return nil } l := []NamespaceDescr{c.xnamespaceDescr()} for !c.take(')') { l = append(l, c.xnamespaceDescr()) } return l } // ../rfc/9051:6769 func (c *Conn) xnamespaceDescr() NamespaceDescr { c.xtake("(") prefix := c.xstring() c.xspace() var b byte if c.peek('"') { s := c.xquoted() if len(s) != 1 { c.xerrorf("namespace-descr: expected single char, got %q", s) } b = byte(s[0]) } else { c.xtake("nil") } var exts []NamespaceExtension for !c.take(')') { c.xspace() key := c.xstring() c.xspace() c.xtake("(") values := []string{c.xstring()} for c.space() { values = append(values, c.xstring()) } c.xtake(")") exts = append(exts, NamespaceExtension{key, values}) } return NamespaceDescr{prefix, b, exts} } // require all of caps to be disabled. func (c *Conn) xneedDisabled(msg string, caps ...Capability) { for _, cap := range caps { if _, ok := c.CapEnabled[cap]; ok { c.xerrorf("%s: invalid because of enabled capability %q", msg, cap) } } } // ../rfc/9051:6546 // Already consumed: "ESEARCH" func (c *Conn) xesearchResponse() (r UntaggedEsearch) { if !c.space() { return } if c.take('(') { // ../rfc/9051:6921 c.xtake("TAG") c.xspace() r.Correlator = c.xastring() c.xtake(")") } if !c.space() { return } w := c.xnonspace() W := strings.ToUpper(w) if W == "UID" { r.UID = true if !c.space() { return } w = c.xnonspace() W = strings.ToUpper(w) } for { // ../rfc/9051:6957 switch W { case "MIN": if r.Min != 0 { c.xerrorf("duplicate MIN in ESEARCH") } c.xspace() num := c.xnzuint32() r.Min = num case "MAX": if r.Max != 0 { c.xerrorf("duplicate MAX in ESEARCH") } c.xspace() num := c.xnzuint32() r.Max = num case "ALL": if !r.All.IsZero() { c.xerrorf("duplicate ALL in ESEARCH") } c.xspace() ss := c.xsequenceSet() if ss.SearchResult { c.xerrorf("$ for last not valid in ESEARCH") } r.All = ss case "COUNT": if r.Count != nil { c.xerrorf("duplicate COUNT in ESEARCH") } c.xspace() num := c.xuint32() r.Count = &num // ../rfc/7162:1211 ../rfc/4731:273 case "MODSEQ": c.xspace() r.ModSeq = c.xint64() default: // Validate ../rfc/9051:7090 for i, b := range []byte(w) { if !(b >= 'A' && b <= 'Z' || strings.IndexByte("-_.", b) >= 0 || i > 0 && strings.IndexByte("0123456789:", b) >= 0) { c.xerrorf("invalid tag %q", w) } } c.xspace() ext := EsearchDataExt{w, c.xtaggedExtVal()} r.Exts = append(r.Exts, ext) } if !c.space() { break } w = c.xnonspace() // todo: this is too loose W = strings.ToUpper(w) } return } // ../rfc/9051:6441 func (c *Conn) xcharset() string { if c.peek('"') { return c.xquoted() } return c.xatom() } // ../rfc/9051:7133 func (c *Conn) xuidset() []NumRange { ranges := []NumRange{c.xuidrange()} for c.take(',') { ranges = append(ranges, c.xuidrange()) } return ranges } func (c *Conn) xuidrange() NumRange { uid := c.xnzuint32() var end *uint32 if c.take(':') { x := c.xnzuint32() end = &x } return NumRange{uid, end} } // ../rfc/3501:4833 func (c *Conn) xlsub() UntaggedLsub { c.xspace() c.xtake("(") r := UntaggedLsub{} for !c.take(')') { if len(r.Flags) > 0 { c.xspace() } r.Flags = append(r.Flags, c.xflag()) } c.xspace() if c.peek('"') { s := c.xquoted() if !c.peek(' ') { r.Mailbox = s return r } if len(s) != 1 { // todo: check valid char c.xerrorf("invalid separator %q", s) } r.Separator = byte(s[0]) } c.xspace() r.Mailbox = c.xastring() return r }