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",
122 "BADCHARSET", "CAPABILITY", "PERMANENTFLAGS", "UIDNEXT", "UIDVALIDITY", "UNSEEN", "APPENDUID", "COPYUID",
123 "HIGHESTMODSEQ", "MODIFIED",
126func stringMap(l ...string) map[string]struct{} {
127 r := map[string]struct{}{}
128 for _, s := range l {
135func (c *Conn) xrespCode() (string, CodeArg) {
137 for !c.peek(' ') && !c.peek(']') {
138 w += string(rune(c.xbyte()))
140 W := strings.ToUpper(w)
142 if _, ok := knownCodes[W]; !ok {
146 for !c.peek(' ') && !c.peek(']') {
147 arg += string(rune(c.xbyte()))
149 args = append(args, arg)
151 return W, CodeOther{W, args}
157 var l []string // Must be nil initially.
160 l = []string{c.xcharset()}
162 l = append(l, c.xcharset())
166 codeArg = CodeList{W, l}
169 caps := []string{c.xatom()}
171 caps = append(caps, c.xatom())
173 c.CapAvailable = map[Capability]struct{}{}
174 for _, cap := range caps {
175 c.CapAvailable[Capability(cap)] = struct{}{}
177 codeArg = CodeWords{W, caps}
179 case "PERMANENTFLAGS":
180 l := []string{} // Must be non-nil.
183 l = []string{c.xflagPerm()}
185 l = append(l, c.xflagPerm())
189 codeArg = CodeList{W, l}
190 case "UIDNEXT", "UIDVALIDITY", "UNSEEN":
192 codeArg = CodeUint{W, c.xnzuint32()}
195 destUIDValidity := c.xnzuint32()
198 codeArg = CodeAppendUID{destUIDValidity, uid}
201 destUIDValidity := c.xnzuint32()
206 codeArg = CodeCopyUID{destUIDValidity, from, to}
207 case "HIGHESTMODSEQ":
209 codeArg = CodeHighestModSeq(c.xint64())
212 modified := c.xuidset()
213 codeArg = CodeModified(NumSet{Ranges: modified})
218func (c *Conn) xbyte() byte {
219 b, err := c.readbyte()
220 c.xcheckf(err, "read byte")
224// take until b is seen. don't take b itself.
225func (c *Conn) xtakeuntil(b byte) string {
228 x, err := c.readbyte()
229 c.xcheckf(err, "read byte")
238func (c *Conn) xdigits() string {
241 b, err := c.readbyte()
242 if err == nil && (b >= '0' && b <= '9') {
251func (c *Conn) xint32() int32 {
253 num, err := strconv.ParseInt(s, 10, 32)
254 c.xcheckf(err, "parsing int32")
258func (c *Conn) xint64() int64 {
260 num, err := strconv.ParseInt(s, 10, 63)
261 c.xcheckf(err, "parsing int64")
265func (c *Conn) xuint32() uint32 {
267 num, err := strconv.ParseUint(s, 10, 32)
268 c.xcheckf(err, "parsing uint32")
272func (c *Conn) xnzuint32() uint32 {
275 c.xerrorf("got 0, expected nonzero uint")
280// todo: replace with proper parsing.
281func (c *Conn) xnonspace() string {
283 for !c.peek(' ') && !c.peek('\r') && !c.peek('\n') {
284 s += string(rune(c.xbyte()))
287 c.xerrorf("expected non-space")
292// todo: replace with proper parsing
293func (c *Conn) xword() string {
297// "*" SP is already consumed
299func (c *Conn) xuntagged() Untagged {
301 W := strings.ToUpper(w)
305 r := UntaggedPreauth(c.xrespText())
311 r := UntaggedBye(c.xrespText())
315 case "OK", "NO", "BAD":
317 r := UntaggedResult(c.xresult(Status(W)))
325 caps = append(caps, c.xnonspace())
327 c.CapAvailable = map[Capability]struct{}{}
328 for _, cap := range caps {
329 c.CapAvailable[Capability(cap)] = struct{}{}
331 r := UntaggedCapability(caps)
339 caps = append(caps, c.xnonspace())
341 for _, cap := range caps {
342 c.CapEnabled[Capability(cap)] = struct{}{}
344 r := UntaggedEnabled(caps)
350 r := UntaggedFlags(c.xflagList())
356 r := c.xmailboxList()
363 mailbox := c.xastring()
366 attrs := map[StatusAttr]int64{}
373 S := StatusAttr(strings.ToUpper(s))
378 num = int64(c.xuint32())
380 num = int64(c.xnzuint32())
382 num = int64(c.xnzuint32())
384 num = int64(c.xuint32())
386 num = int64(c.xuint32())
390 c.xneedDisabled("RECENT status flag", CapIMAP4rev2)
391 num = int64(c.xuint32())
393 if c.peek('n') || c.peek('N') {
398 case "HIGHESTMODSEQ":
400 case "DELETED-STORAGE":
403 c.xerrorf("status: unknown attribute %q", s)
405 if _, ok := attrs[S]; ok {
406 c.xerrorf("status: duplicate attribute %q", s)
410 r := UntaggedStatus{mailbox, attrs}
417 personal := c.xnamespace()
419 other := c.xnamespace()
421 shared := c.xnamespace()
422 r := UntaggedNamespace{personal, other, shared}
428 c.xneedDisabled("untagged SEARCH response", CapIMAP4rev2)
438 return UntaggedSearchModSeq{nums, modseq}
440 nums = append(nums, c.xnzuint32())
442 r := UntaggedSearch(nums)
447 r := c.xesearchResponse()
452 c.xneedDisabled("untagged LSUB response", CapIMAP4rev2)
460 var params map[string]string
462 params = map[string]string{}
470 if _, ok := params[k]; ok {
471 c.xerrorf("duplicate key %q", k)
479 return UntaggedID(params)
493 return UntaggedVanished{earlier, NumSet{Ranges: uids}}
502 roots = append(roots, root)
505 return UntaggedQuotaroot(roots)
514 xresource := func() QuotaResource {
520 return QuotaResource{QuotaResourceName(strings.ToUpper(name)), usage, limit}
523 seen := map[QuotaResourceName]bool{}
524 l := []QuotaResource{xresource()}
525 seen[l[0].Name] = true
529 c.xerrorf("duplicate resource name %q", res.Name)
531 seen[res.Name] = true
536 return UntaggedQuota{root, l}
539 v, err := strconv.ParseUint(w, 10, 32)
544 W = strings.ToUpper(w)
548 c.xerrorf("invalid zero number for untagged fetch response")
557 c.xerrorf("invalid zero number for untagged expunge response")
560 return UntaggedExpunge(num)
564 return UntaggedExists(num)
567 c.xneedDisabled("should not send RECENT in IMAP4rev2", CapIMAP4rev2)
569 return UntaggedRecent(num)
572 c.xerrorf("unknown untagged numbered response %q", w)
576 c.xerrorf("unknown untagged response %q", w)
582// Already parsed: "*" SP nznumber SP "FETCH" SP
583func (c *Conn) xfetch(num uint32) UntaggedFetch {
585 attrs := []FetchAttr{c.xmsgatt1()}
587 attrs = append(attrs, c.xmsgatt1())
590 return UntaggedFetch{num, attrs}
594func (c *Conn) xmsgatt1() FetchAttr {
598 if b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b >= '0' && b <= '9' || b == '.' {
606 F := strings.ToUpper(f)
613 flags = []string{c.xflag()}
615 flags = append(flags, c.xflag())
619 return FetchFlags(flags)
623 return FetchEnvelope(c.xenvelope())
627 return FetchInternalDate(c.xquoted()) // todo: parsed time
631 return FetchRFC822Size(c.xint64())
636 return FetchRFC822(s)
638 case "RFC822.HEADER":
641 return FetchRFC822Header(s)
646 return FetchRFC822Text(s)
650 return FetchBodystructure{F, c.xbodystructure()}
653 section := c.xsection()
661 body := c.xnilString()
662 return FetchBody{F, section, offset, body}
664 case "BODYSTRUCTURE":
666 return FetchBodystructure{F, c.xbodystructure()}
670 nums := c.xsectionBinary()
673 buf := c.xnilStringLiteral8()
674 return FetchBinary{F, nums, string(buf)}
678 nums := c.xsectionBinary()
682 return FetchBinarySize{F, nums, size}
686 return FetchUID(c.xuint32())
694 return FetchModSeq(modseq)
696 c.xerrorf("unknown fetch attribute %q", f)
700func (c *Conn) xnilString() string {
703 } else if c.peek('{') {
704 return string(c.xliteral())
711func (c *Conn) xstring() string {
715 return string(c.xliteral())
718func (c *Conn) xastring() string {
721 } else if c.peek('{') {
722 return string(c.xliteral())
727func (c *Conn) xatom() string {
730 b, err := c.readbyte()
731 c.xcheckf(err, "read byte for atom")
732 if b <= ' ' || strings.IndexByte("(){%*\"\\]", b) >= 0 {
735 c.xerrorf("expected atom")
744func (c *Conn) xquoted() string {
748 r, err := c.readrune()
749 c.xcheckf(err, "reading rune in quoted string")
751 r, err = c.readrune()
752 c.xcheckf(err, "reading escaped char in quoted string")
753 if r != '\\' && r != '"' {
754 c.xerrorf("quoted char not backslash or dquote: %c", r)
757 // todo: probably refuse some more chars. like \0 and all ctl and backspace.
763func (c *Conn) xliteral() []byte {
770 c.xerrorf("refusing to read more than 1MB: %d", size)
773 _, err := fmt.Fprintf(c.conn, "+ ok\r\n")
774 c.xcheckf(err, "write continuation")
776 buf := make([]byte, int(size))
777 _, err := io.ReadFull(c.r, buf)
778 c.xcheckf(err, "reading data for literal")
784func (c *Conn) xflag0(allowPerm bool) string {
788 if allowPerm && c.take('*') {
791 } else if c.take('$') {
798func (c *Conn) xflag() string {
799 return c.xflag0(false)
802func (c *Conn) xflagPerm() string {
803 return c.xflag0(true)
806func (c *Conn) xsection() string {
808 s := c.xtakeuntil(']')
813func (c *Conn) xsectionBinary() []uint32 {
820 nums = append(nums, c.xnzuint32())
825func (c *Conn) xnilStringLiteral8() []byte {
826 // todo: should make difference for literal8 and literal from string, which bytes are allowed
827 if c.take('~') || c.peek('{') {
830 return []byte(c.xnilString())
834func (c *Conn) xbodystructure() any {
838 parts := []any{c.xbodystructure()}
840 parts = append(parts, c.xbodystructure())
843 mediaSubtype := c.xstring()
844 // todo: parse optional body-ext-mpart
846 return BodyTypeMpart{parts, mediaSubtype}
849 mediaType := c.xstring()
851 mediaSubtype := c.xstring()
853 bodyFields := c.xbodyFields()
857 envelope := c.xenvelope()
859 bodyStructure := c.xbodystructure()
863 return BodyTypeMsg{mediaType, mediaSubtype, bodyFields, envelope, bodyStructure, lines}
868 return BodyTypeText{mediaType, mediaSubtype, bodyFields, lines}
872 return BodyTypeBasic{mediaType, mediaSubtype, bodyFields}
874 // todo: verify the media(sub)type is valid for returned data.
878func (c *Conn) xbodyFields() BodyFields {
879 params := c.xbodyFldParam()
881 contentID := c.xnilString()
883 contentDescr := c.xnilString()
885 cte := c.xnilString()
888 return BodyFields{params, contentID, contentDescr, cte, octets}
892func (c *Conn) xbodyFldParam() [][2]string {
897 l := [][2]string{{k, v}}
902 l = append(l, [2]string{k, v})
912func (c *Conn) xenvelope() Envelope {
914 date := c.xnilString()
916 subject := c.xnilString()
918 from := c.xaddresses()
920 sender := c.xaddresses()
922 replyTo := c.xaddresses()
928 bcc := c.xaddresses()
930 inReplyTo := c.xnilString()
932 messageID := c.xnilString()
934 return Envelope{date, subject, from, sender, replyTo, to, cc, bcc, inReplyTo, messageID}
938func (c *Conn) xaddresses() []Address {
943 l := []Address{c.xaddress()}
945 l = append(l, c.xaddress())
951func (c *Conn) xaddress() Address {
953 name := c.xnilString()
955 adl := c.xnilString()
957 mailbox := c.xnilString()
959 host := c.xnilString()
961 return Address{name, adl, mailbox, host}
965func (c *Conn) xflagList() []string {
969 l = []string{c.xflag()}
971 l = append(l, c.xflag())
979func (c *Conn) xmailboxList() UntaggedList {
983 flags = append(flags, c.xflag())
985 flags = append(flags, c.xflag())
994 if len(quoted) != 1 {
995 c.xerrorf("mailbox-list has multichar quoted part: %q", quoted)
998 } else if !c.peek(' ') {
1002 mailbox := c.xastring()
1003 ul := UntaggedList{flags, b, mailbox, nil, ""}
1007 c.xmboxListExtendedItem(&ul)
1009 c.xmboxListExtendedItem(&ul)
1018func (c *Conn) xmboxListExtendedItem(ul *UntaggedList) {
1021 if strings.ToUpper(tag) == "OLDNAME" {
1024 name := c.xastring()
1029 val := c.xtaggedExtVal()
1030 ul.Extended = append(ul.Extended, MboxListExtendedItem{tag, val})
1034func (c *Conn) xtaggedExtVal() TaggedExtVal {
1038 comp := c.xtaggedExtComp()
1044 // 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.
1045 b, err := c.readbyte()
1046 c.xcheckf(err, "read byte for tagged-ext-val")
1047 if b < '0' || b > '9' {
1049 ss := c.xsequenceSet()
1050 return TaggedExtVal{SeqSet: &ss}
1053 num, err := strconv.ParseInt(s, 10, 63)
1054 c.xcheckf(err, "parsing int")
1055 if !c.peek(':') && !c.peek(',') {
1056 // not a larger sequence-set
1057 return TaggedExtVal{Number: &num}
1060 sr.First = uint32(num)
1068 ss := c.xsequenceSet()
1069 ss.Ranges = append([]NumRange{sr}, ss.Ranges...)
1070 return TaggedExtVal{SeqSet: &ss}
1074func (c *Conn) xsequenceSet() NumSet {
1076 return NumSet{SearchResult: true}
1082 sr.First = c.xnzuint32()
1091 ss.Ranges = append(ss.Ranges, sr)
1100func (c *Conn) xtaggedExtComp() TaggedExtComp {
1102 r := c.xtaggedExtComp()
1104 return TaggedExtComp{Comps: []TaggedExtComp{r}}
1108 return TaggedExtComp{String: s}
1110 l := []TaggedExtComp{{String: s}}
1112 l = append(l, c.xtaggedExtComp())
1114 return TaggedExtComp{Comps: l}
1118func (c *Conn) xnamespace() []NamespaceDescr {
1124 l := []NamespaceDescr{c.xnamespaceDescr()}
1126 l = append(l, c.xnamespaceDescr())
1132func (c *Conn) xnamespaceDescr() NamespaceDescr {
1134 prefix := c.xstring()
1140 c.xerrorf("namespace-descr: expected single char, got %q", s)
1146 var exts []NamespaceExtension
1152 values := []string{c.xstring()}
1154 values = append(values, c.xstring())
1157 exts = append(exts, NamespaceExtension{key, values})
1159 return NamespaceDescr{prefix, b, exts}
1162// require all of caps to be disabled.
1163func (c *Conn) xneedDisabled(msg string, caps ...Capability) {
1164 for _, cap := range caps {
1165 if _, ok := c.CapEnabled[cap]; ok {
1166 c.xerrorf("%s: invalid because of enabled capability %q", msg, cap)
1172// Already consumed: "ESEARCH"
1173func (c *Conn) xesearchResponse() (r UntaggedEsearch) {
1182 r.Correlator = c.xastring()
1189 W := strings.ToUpper(w)
1196 W = strings.ToUpper(w)
1203 c.xerrorf("duplicate MIN in ESEARCH")
1206 num := c.xnzuint32()
1211 c.xerrorf("duplicate MAX in ESEARCH")
1214 num := c.xnzuint32()
1218 if !r.All.IsZero() {
1219 c.xerrorf("duplicate ALL in ESEARCH")
1222 ss := c.xsequenceSet()
1223 if ss.SearchResult {
1224 c.xerrorf("$ for last not valid in ESEARCH")
1230 c.xerrorf("duplicate COUNT in ESEARCH")
1239 r.ModSeq = c.xint64()
1243 for i, b := range []byte(w) {
1244 if !(b >= 'A' && b <= 'Z' || strings.IndexByte("-_.", b) >= 0 || i > 0 && strings.IndexByte("0123456789:", b) >= 0) {
1245 c.xerrorf("invalid tag %q", w)
1249 ext := EsearchDataExt{w, c.xtaggedExtVal()}
1250 r.Exts = append(r.Exts, ext)
1256 w = c.xnonspace() // todo: this is too loose
1257 W = strings.ToUpper(w)
1263func (c *Conn) xcharset() string {
1271func (c *Conn) xuidset() []NumRange {
1272 ranges := []NumRange{c.xuidrange()}
1274 ranges = append(ranges, c.xuidrange())
1279func (c *Conn) xuidrange() NumRange {
1280 uid := c.xnzuint32()
1286 return NumRange{uid, end}
1290func (c *Conn) xlsub() UntaggedLsub {
1295 if len(r.Flags) > 0 {
1298 r.Flags = append(r.Flags, c.xflag())
1308 // todo: check valid char
1309 c.xerrorf("invalid separator %q", s)
1311 r.Separator = byte(s[0])
1314 r.Mailbox = c.xastring()