10func (c *Conn) recorded() string {
11 s := string(c.recordBuf)
17func (c *Conn) recordAdd(buf []byte) {
19 c.recordBuf = append(c.recordBuf, buf...)
23func (c *Conn) xtake(s string) {
24 buf := make([]byte, len(s))
25 _, err := io.ReadFull(c.r, buf)
26 c.xcheckf(err, "taking %q", s)
27 if !strings.EqualFold(string(buf), s) {
28 c.xerrorf("got %q, expected %q", buf, s)
33func (c *Conn) readbyte() (byte, error) {
34 b, err := c.r.ReadByte()
36 c.recordAdd([]byte{b})
41func (c *Conn) unreadbyte() {
43 c.recordBuf = c.recordBuf[:len(c.recordBuf)-1]
45 err := c.r.UnreadByte()
46 c.xcheckf(err, "unread byte")
49func (c *Conn) readrune() (rune, error) {
50 x, _, err := c.r.ReadRune()
52 c.recordAdd([]byte(string(x)))
57func (c *Conn) xspace() {
61func (c *Conn) xcrlf() {
65func (c *Conn) peek(exp byte) bool {
66 b, err := c.readbyte()
70 return err == nil && strings.EqualFold(string(rune(b)), string(rune(exp)))
73func (c *Conn) take(exp byte) bool {
81func (c *Conn) xstatus() Status {
83 W := strings.ToUpper(w)
92 c.xerrorf("expected status, got %q", w)
96// Already consumed: tag SP status SP
97func (c *Conn) xresult(status Status) Result {
98 respText := c.xrespText()
99 return Result{status, respText}
102func (c *Conn) xrespText() RespText {
106 code, codeArg = c.xrespCode()
112 more += string(rune(c.xbyte()))
114 return RespText{code, codeArg, more}
117var knownCodes = stringMap(
118 // Without parameters.
119 "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",
121 "BADCHARSET", "CAPABILITY", "PERMANENTFLAGS", "UIDNEXT", "UIDVALIDITY", "UNSEEN", "APPENDUID", "COPYUID",
122 "HIGHESTMODSEQ", "MODIFIED",
125func stringMap(l ...string) map[string]struct{} {
126 r := map[string]struct{}{}
127 for _, s := range l {
134func (c *Conn) xrespCode() (string, CodeArg) {
136 for !c.peek(' ') && !c.peek(']') {
137 w += string(rune(c.xbyte()))
139 W := strings.ToUpper(w)
141 if _, ok := knownCodes[W]; !ok {
145 for !c.peek(' ') && !c.peek(']') {
146 arg += string(rune(c.xbyte()))
148 args = append(args, arg)
150 return W, CodeOther{W, args}
156 var l []string // Must be nil initially.
159 l = []string{c.xcharset()}
161 l = append(l, c.xcharset())
165 codeArg = CodeList{W, l}
168 caps := []string{c.xatom()}
170 caps = append(caps, c.xatom())
172 c.CapAvailable = map[Capability]struct{}{}
173 for _, cap := range caps {
174 c.CapAvailable[Capability(cap)] = struct{}{}
176 codeArg = CodeWords{W, caps}
178 case "PERMANENTFLAGS":
179 l := []string{} // Must be non-nil.
182 l = []string{c.xflagPerm()}
184 l = append(l, c.xflagPerm())
188 codeArg = CodeList{W, l}
189 case "UIDNEXT", "UIDVALIDITY", "UNSEEN":
191 codeArg = CodeUint{W, c.xnzuint32()}
194 destUIDValidity := c.xnzuint32()
197 codeArg = CodeAppendUID{destUIDValidity, uid}
200 destUIDValidity := c.xnzuint32()
205 codeArg = CodeCopyUID{destUIDValidity, from, to}
206 case "HIGHESTMODSEQ":
208 codeArg = CodeHighestModSeq(c.xint64())
211 modified := c.xuidset()
212 codeArg = CodeModified(NumSet{Ranges: modified})
217func (c *Conn) xbyte() byte {
218 b, err := c.readbyte()
219 c.xcheckf(err, "read byte")
223// take until b is seen. don't take b itself.
224func (c *Conn) xtakeuntil(b byte) string {
227 x, err := c.readbyte()
228 c.xcheckf(err, "read byte")
237func (c *Conn) xdigits() string {
240 b, err := c.readbyte()
241 if err == nil && (b >= '0' && b <= '9') {
250func (c *Conn) xint32() int32 {
252 num, err := strconv.ParseInt(s, 10, 32)
253 c.xcheckf(err, "parsing int32")
257func (c *Conn) xint64() int64 {
259 num, err := strconv.ParseInt(s, 10, 63)
260 c.xcheckf(err, "parsing int64")
264func (c *Conn) xuint32() uint32 {
266 num, err := strconv.ParseUint(s, 10, 32)
267 c.xcheckf(err, "parsing uint32")
271func (c *Conn) xnzuint32() uint32 {
274 c.xerrorf("got 0, expected nonzero uint")
279// todo: replace with proper parsing.
280func (c *Conn) xnonspace() string {
282 for !c.peek(' ') && !c.peek('\r') && !c.peek('\n') {
283 s += string(rune(c.xbyte()))
286 c.xerrorf("expected non-space")
291// todo: replace with proper parsing
292func (c *Conn) xword() string {
296// "*" SP is already consumed
298func (c *Conn) xuntagged() Untagged {
300 W := strings.ToUpper(w)
304 r := UntaggedPreauth(c.xrespText())
310 r := UntaggedBye(c.xrespText())
314 case "OK", "NO", "BAD":
316 r := UntaggedResult(c.xresult(Status(W)))
324 caps = append(caps, c.xnonspace())
326 c.CapAvailable = map[Capability]struct{}{}
327 for _, cap := range caps {
328 c.CapAvailable[Capability(cap)] = struct{}{}
330 r := UntaggedCapability(caps)
338 caps = append(caps, c.xnonspace())
340 for _, cap := range caps {
341 c.CapEnabled[Capability(cap)] = struct{}{}
343 r := UntaggedEnabled(caps)
349 r := UntaggedFlags(c.xflagList())
355 r := c.xmailboxList()
362 mailbox := c.xastring()
365 attrs := map[string]int64{}
372 S := strings.ToUpper(s)
377 num = int64(c.xuint32())
379 num = int64(c.xnzuint32())
381 num = int64(c.xnzuint32())
383 num = int64(c.xuint32())
385 num = int64(c.xuint32())
389 c.xneedDisabled("RECENT status flag", CapIMAP4rev2)
390 num = int64(c.xuint32())
392 if c.peek('n') || c.peek('N') {
397 case "HIGHESTMODSEQ":
400 c.xerrorf("status: unknown attribute %q", s)
402 if _, ok := attrs[S]; ok {
403 c.xerrorf("status: duplicate attribute %q", s)
407 r := UntaggedStatus{mailbox, attrs}
414 personal := c.xnamespace()
416 other := c.xnamespace()
418 shared := c.xnamespace()
419 r := UntaggedNamespace{personal, other, shared}
425 c.xneedDisabled("untagged SEARCH response", CapIMAP4rev2)
435 return UntaggedSearchModSeq{nums, modseq}
437 nums = append(nums, c.xnzuint32())
439 r := UntaggedSearch(nums)
444 r := c.xesearchResponse()
449 c.xneedDisabled("untagged LSUB response", CapIMAP4rev2)
457 var params map[string]string
459 params = map[string]string{}
467 if _, ok := params[k]; ok {
468 c.xerrorf("duplicate key %q", k)
476 return UntaggedID(params)
490 return UntaggedVanished{earlier, NumSet{Ranges: uids}}
493 v, err := strconv.ParseUint(w, 10, 32)
498 W = strings.ToUpper(w)
502 c.xerrorf("invalid zero number for untagged fetch response")
511 c.xerrorf("invalid zero number for untagged expunge response")
514 return UntaggedExpunge(num)
518 return UntaggedExists(num)
521 c.xneedDisabled("should not send RECENT in IMAP4rev2", CapIMAP4rev2)
523 return UntaggedRecent(num)
526 c.xerrorf("unknown untagged numbered response %q", w)
530 c.xerrorf("unknown untagged response %q", w)
536// Already parsed: "*" SP nznumber SP "FETCH" SP
537func (c *Conn) xfetch(num uint32) UntaggedFetch {
539 attrs := []FetchAttr{c.xmsgatt1()}
541 attrs = append(attrs, c.xmsgatt1())
544 return UntaggedFetch{num, attrs}
548func (c *Conn) xmsgatt1() FetchAttr {
552 if b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b >= '0' && b <= '9' || b == '.' {
560 F := strings.ToUpper(f)
567 flags = []string{c.xflag()}
569 flags = append(flags, c.xflag())
573 return FetchFlags(flags)
577 return FetchEnvelope(c.xenvelope())
581 return FetchInternalDate(c.xquoted()) // todo: parsed time
585 return FetchRFC822Size(c.xint64())
590 return FetchRFC822(s)
592 case "RFC822.HEADER":
595 return FetchRFC822Header(s)
600 return FetchRFC822Text(s)
604 return FetchBodystructure{F, c.xbodystructure()}
607 section := c.xsection()
615 body := c.xnilString()
616 return FetchBody{F, section, offset, body}
618 case "BODYSTRUCTURE":
620 return FetchBodystructure{F, c.xbodystructure()}
624 nums := c.xsectionBinary()
627 buf := c.xnilStringLiteral8()
628 return FetchBinary{F, nums, string(buf)}
632 nums := c.xsectionBinary()
636 return FetchBinarySize{F, nums, size}
640 return FetchUID(c.xuint32())
648 return FetchModSeq(modseq)
650 c.xerrorf("unknown fetch attribute %q", f)
654func (c *Conn) xnilString() string {
657 } else if c.peek('{') {
658 return string(c.xliteral())
665func (c *Conn) xstring() string {
669 return string(c.xliteral())
672func (c *Conn) xastring() string {
675 } else if c.peek('{') {
676 return string(c.xliteral())
681func (c *Conn) xatom() string {
684 b, err := c.readbyte()
685 c.xcheckf(err, "read byte for flag")
686 if b <= ' ' || strings.IndexByte("(){%*\"\\]", b) >= 0 {
689 c.xerrorf("expected atom")
698func (c *Conn) xquoted() string {
702 r, err := c.readrune()
703 c.xcheckf(err, "reading rune in quoted string")
705 r, err = c.readrune()
706 c.xcheckf(err, "reading escaped char in quoted string")
707 if r != '\\' && r != '"' {
708 c.xerrorf("quoted char not backslash or dquote: %c", r)
711 // todo: probably refuse some more chars. like \0 and all ctl and backspace.
717func (c *Conn) xliteral() []byte {
724 c.xerrorf("refusing to read more than 1MB: %d", size)
727 _, err := fmt.Fprintf(c.conn, "+ ok\r\n")
728 c.xcheckf(err, "write continuation")
730 buf := make([]byte, int(size))
731 _, err := io.ReadFull(c.r, buf)
732 c.xcheckf(err, "reading data for literal")
738func (c *Conn) xflag0(allowPerm bool) string {
742 if allowPerm && c.take('*') {
745 } else if c.take('$') {
752func (c *Conn) xflag() string {
753 return c.xflag0(false)
756func (c *Conn) xflagPerm() string {
757 return c.xflag0(true)
760func (c *Conn) xsection() string {
762 s := c.xtakeuntil(']')
767func (c *Conn) xsectionBinary() []uint32 {
774 nums = append(nums, c.xnzuint32())
779func (c *Conn) xnilStringLiteral8() []byte {
780 // todo: should make difference for literal8 and literal from string, which bytes are allowed
781 if c.take('~') || c.peek('{') {
784 return []byte(c.xnilString())
788func (c *Conn) xbodystructure() any {
792 parts := []any{c.xbodystructure()}
794 parts = append(parts, c.xbodystructure())
797 mediaSubtype := c.xstring()
798 // todo: parse optional body-ext-mpart
800 return BodyTypeMpart{parts, mediaSubtype}
803 mediaType := c.xstring()
805 mediaSubtype := c.xstring()
807 bodyFields := c.xbodyFields()
811 envelope := c.xenvelope()
813 bodyStructure := c.xbodystructure()
817 return BodyTypeMsg{mediaType, mediaSubtype, bodyFields, envelope, bodyStructure, lines}
822 return BodyTypeText{mediaType, mediaSubtype, bodyFields, lines}
826 return BodyTypeBasic{mediaType, mediaSubtype, bodyFields}
828 // todo: verify the media(sub)type is valid for returned data.
832func (c *Conn) xbodyFields() BodyFields {
833 params := c.xbodyFldParam()
835 contentID := c.xnilString()
837 contentDescr := c.xnilString()
839 cte := c.xnilString()
842 return BodyFields{params, contentID, contentDescr, cte, octets}
846func (c *Conn) xbodyFldParam() [][2]string {
851 l := [][2]string{{k, v}}
856 l = append(l, [2]string{k, v})
866func (c *Conn) xenvelope() Envelope {
868 date := c.xnilString()
870 subject := c.xnilString()
872 from := c.xaddresses()
874 sender := c.xaddresses()
876 replyTo := c.xaddresses()
882 bcc := c.xaddresses()
884 inReplyTo := c.xnilString()
886 messageID := c.xnilString()
888 return Envelope{date, subject, from, sender, replyTo, to, cc, bcc, inReplyTo, messageID}
892func (c *Conn) xaddresses() []Address {
897 l := []Address{c.xaddress()}
899 l = append(l, c.xaddress())
905func (c *Conn) xaddress() Address {
907 name := c.xnilString()
909 adl := c.xnilString()
911 mailbox := c.xnilString()
913 host := c.xnilString()
915 return Address{name, adl, mailbox, host}
919func (c *Conn) xflagList() []string {
923 l = []string{c.xflag()}
925 l = append(l, c.xflag())
933func (c *Conn) xmailboxList() UntaggedList {
937 flags = append(flags, c.xflag())
939 flags = append(flags, c.xflag())
948 if len(quoted) != 1 {
949 c.xerrorf("mailbox-list has multichar quoted part: %q", quoted)
952 } else if !c.peek(' ') {
956 mailbox := c.xastring()
957 ul := UntaggedList{flags, b, mailbox, nil, ""}
961 c.xmboxListExtendedItem(&ul)
963 c.xmboxListExtendedItem(&ul)
972func (c *Conn) xmboxListExtendedItem(ul *UntaggedList) {
975 if strings.ToUpper(tag) == "OLDNAME" {
983 val := c.xtaggedExtVal()
984 ul.Extended = append(ul.Extended, MboxListExtendedItem{tag, val})
988func (c *Conn) xtaggedExtVal() TaggedExtVal {
992 comp := c.xtaggedExtComp()
998 // 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.
999 b, err := c.readbyte()
1000 c.xcheckf(err, "read byte for tagged-ext-val")
1001 if b < '0' || b > '9' {
1003 ss := c.xsequenceSet()
1004 return TaggedExtVal{SeqSet: &ss}
1007 num, err := strconv.ParseInt(s, 10, 63)
1008 c.xcheckf(err, "parsing int")
1009 if !c.peek(':') && !c.peek(',') {
1010 // not a larger sequence-set
1011 return TaggedExtVal{Number: &num}
1014 sr.First = uint32(num)
1022 ss := c.xsequenceSet()
1023 ss.Ranges = append([]NumRange{sr}, ss.Ranges...)
1024 return TaggedExtVal{SeqSet: &ss}
1028func (c *Conn) xsequenceSet() NumSet {
1030 return NumSet{SearchResult: true}
1036 sr.First = c.xnzuint32()
1045 ss.Ranges = append(ss.Ranges, sr)
1054func (c *Conn) xtaggedExtComp() TaggedExtComp {
1056 r := c.xtaggedExtComp()
1058 return TaggedExtComp{Comps: []TaggedExtComp{r}}
1062 return TaggedExtComp{String: s}
1064 l := []TaggedExtComp{{String: s}}
1066 l = append(l, c.xtaggedExtComp())
1068 return TaggedExtComp{Comps: l}
1072func (c *Conn) xnamespace() []NamespaceDescr {
1078 l := []NamespaceDescr{c.xnamespaceDescr()}
1080 l = append(l, c.xnamespaceDescr())
1086func (c *Conn) xnamespaceDescr() NamespaceDescr {
1088 prefix := c.xstring()
1094 c.xerrorf("namespace-descr: expected single char, got %q", s)
1100 var exts []NamespaceExtension
1106 values := []string{c.xstring()}
1108 values = append(values, c.xstring())
1111 exts = append(exts, NamespaceExtension{key, values})
1113 return NamespaceDescr{prefix, b, exts}
1116// require all of caps to be disabled.
1117func (c *Conn) xneedDisabled(msg string, caps ...Capability) {
1118 for _, cap := range caps {
1119 if _, ok := c.CapEnabled[cap]; ok {
1120 c.xerrorf("%s: invalid because of enabled capability %q", msg, cap)
1126// Already consumed: "ESEARCH"
1127func (c *Conn) xesearchResponse() (r UntaggedEsearch) {
1136 r.Correlator = c.xastring()
1143 W := strings.ToUpper(w)
1150 W = strings.ToUpper(w)
1157 c.xerrorf("duplicate MIN in ESEARCH")
1160 num := c.xnzuint32()
1165 c.xerrorf("duplicate MAX in ESEARCH")
1168 num := c.xnzuint32()
1172 if !r.All.IsZero() {
1173 c.xerrorf("duplicate ALL in ESEARCH")
1176 ss := c.xsequenceSet()
1177 if ss.SearchResult {
1178 c.xerrorf("$ for last not valid in ESEARCH")
1184 c.xerrorf("duplicate COUNT in ESEARCH")
1193 r.ModSeq = c.xint64()
1197 for i, b := range []byte(w) {
1198 if !(b >= 'A' && b <= 'Z' || strings.IndexByte("-_.", b) >= 0 || i > 0 && strings.IndexByte("0123456789:", b) >= 0) {
1199 c.xerrorf("invalid tag %q", w)
1203 ext := EsearchDataExt{w, c.xtaggedExtVal()}
1204 r.Exts = append(r.Exts, ext)
1210 w = c.xnonspace() // todo: this is too loose
1211 W = strings.ToUpper(w)
1217func (c *Conn) xcharset() string {
1225func (c *Conn) xuidset() []NumRange {
1226 ranges := []NumRange{c.xuidrange()}
1228 ranges = append(ranges, c.xuidrange())
1233func (c *Conn) xuidrange() NumRange {
1234 uid := c.xnzuint32()
1240 return NumRange{uid, end}
1244func (c *Conn) xlsub() UntaggedLsub {
1249 if len(r.Flags) > 0 {
1252 r.Flags = append(r.Flags, c.xflag())
1262 // todo: check valid char
1263 c.xerrorf("invalid separator %q", s)
1265 r.Separator = byte(s[0])
1268 r.Mailbox = c.xastring()