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) space() bool {
61func (c *Conn) xspace() {
65func (c *Conn) xcrlf() {
69func (c *Conn) peek(exp byte) bool {
70 b, err := c.readbyte()
74 return err == nil && strings.EqualFold(string(rune(b)), string(rune(exp)))
77func (c *Conn) peekstring() bool {
78 return c.peek('"') || c.peek('{')
81func (c *Conn) take(exp byte) bool {
89func (c *Conn) xstatus() Status {
91 W := strings.ToUpper(w)
100 c.xerrorf("expected status, got %q", w)
104// Already consumed: tag SP status SP
105func (c *Conn) xresult(status Status) Result {
106 respText := c.xrespText()
107 return Result{status, respText}
110func (c *Conn) xrespText() RespText {
114 code, codeArg = c.xrespCode()
120 more += string(rune(c.xbyte()))
122 return RespText{code, codeArg, more}
125var knownCodes = stringMap(
126 // Without parameters.
127 "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",
130 "BADCHARSET", "CAPABILITY", "PERMANENTFLAGS", "UIDNEXT", "UIDVALIDITY", "UNSEEN", "APPENDUID", "COPYUID",
131 "HIGHESTMODSEQ", "MODIFIED",
134func stringMap(l ...string) map[string]struct{} {
135 r := map[string]struct{}{}
136 for _, s := range l {
143func (c *Conn) xrespCode() (string, CodeArg) {
145 for !c.peek(' ') && !c.peek(']') {
146 w += string(rune(c.xbyte()))
148 W := strings.ToUpper(w)
150 if _, ok := knownCodes[W]; !ok {
154 for !c.peek(' ') && !c.peek(']') {
155 arg += string(rune(c.xbyte()))
157 args = append(args, arg)
159 return W, CodeOther{W, args}
165 var l []string // Must be nil initially.
168 l = []string{c.xcharset()}
170 l = append(l, c.xcharset())
174 codeArg = CodeList{W, l}
177 caps := []string{c.xatom()}
179 caps = append(caps, c.xatom())
181 c.CapAvailable = map[Capability]struct{}{}
182 for _, cap := range caps {
183 c.CapAvailable[Capability(cap)] = struct{}{}
185 codeArg = CodeWords{W, caps}
187 case "PERMANENTFLAGS":
188 l := []string{} // Must be non-nil.
191 l = []string{c.xflagPerm()}
193 l = append(l, c.xflagPerm())
197 codeArg = CodeList{W, l}
198 case "UIDNEXT", "UIDVALIDITY", "UNSEEN":
200 codeArg = CodeUint{W, c.xnzuint32()}
203 destUIDValidity := c.xnzuint32()
206 codeArg = CodeAppendUID{destUIDValidity, uid}
209 destUIDValidity := c.xnzuint32()
214 codeArg = CodeCopyUID{destUIDValidity, from, to}
215 case "HIGHESTMODSEQ":
217 codeArg = CodeHighestModSeq(c.xint64())
220 modified := c.xuidset()
221 codeArg = CodeModified(NumSet{Ranges: modified})
226func (c *Conn) xbyte() byte {
227 b, err := c.readbyte()
228 c.xcheckf(err, "read byte")
232// take until b is seen. don't take b itself.
233func (c *Conn) xtakeuntil(b byte) string {
236 x, err := c.readbyte()
237 c.xcheckf(err, "read byte")
246func (c *Conn) xdigits() string {
249 b, err := c.readbyte()
250 if err == nil && (b >= '0' && b <= '9') {
259func (c *Conn) peekdigit() bool {
260 if b, err := c.readbyte(); err == nil {
262 return b >= '0' && b <= '9'
267func (c *Conn) xint32() int32 {
269 num, err := strconv.ParseInt(s, 10, 32)
270 c.xcheckf(err, "parsing int32")
274func (c *Conn) xint64() int64 {
276 num, err := strconv.ParseInt(s, 10, 63)
277 c.xcheckf(err, "parsing int64")
281func (c *Conn) xuint32() uint32 {
283 num, err := strconv.ParseUint(s, 10, 32)
284 c.xcheckf(err, "parsing uint32")
288func (c *Conn) xnzuint32() uint32 {
291 c.xerrorf("got 0, expected nonzero uint")
296// todo: replace with proper parsing.
297func (c *Conn) xnonspace() string {
299 for !c.peek(' ') && !c.peek('\r') && !c.peek('\n') {
300 s += string(rune(c.xbyte()))
303 c.xerrorf("expected non-space")
308// todo: replace with proper parsing
309func (c *Conn) xword() string {
313// "*" SP is already consumed
315func (c *Conn) xuntagged() Untagged {
317 W := strings.ToUpper(w)
321 r := UntaggedPreauth(c.xrespText())
327 r := UntaggedBye(c.xrespText())
331 case "OK", "NO", "BAD":
333 r := UntaggedResult(c.xresult(Status(W)))
341 caps = append(caps, c.xnonspace())
343 c.CapAvailable = map[Capability]struct{}{}
344 for _, cap := range caps {
345 c.CapAvailable[Capability(cap)] = struct{}{}
347 r := UntaggedCapability(caps)
355 caps = append(caps, c.xnonspace())
357 for _, cap := range caps {
358 c.CapEnabled[Capability(cap)] = struct{}{}
360 r := UntaggedEnabled(caps)
366 r := UntaggedFlags(c.xflagList())
372 r := c.xmailboxList()
379 mailbox := c.xastring()
382 attrs := map[StatusAttr]int64{}
389 S := StatusAttr(strings.ToUpper(s))
394 num = int64(c.xuint32())
396 num = int64(c.xnzuint32())
398 num = int64(c.xnzuint32())
400 num = int64(c.xuint32())
402 num = int64(c.xuint32())
406 c.xneedDisabled("RECENT status flag", CapIMAP4rev2)
407 num = int64(c.xuint32())
409 if c.peek('n') || c.peek('N') {
414 case "HIGHESTMODSEQ":
416 case "DELETED-STORAGE":
419 c.xerrorf("status: unknown attribute %q", s)
421 if _, ok := attrs[S]; ok {
422 c.xerrorf("status: duplicate attribute %q", s)
426 r := UntaggedStatus{mailbox, attrs}
433 personal := c.xnamespace()
435 other := c.xnamespace()
437 shared := c.xnamespace()
438 r := UntaggedNamespace{personal, other, shared}
444 c.xneedDisabled("untagged SEARCH response", CapIMAP4rev2)
454 return UntaggedSearchModSeq{nums, modseq}
456 nums = append(nums, c.xnzuint32())
458 r := UntaggedSearch(nums)
463 r := c.xesearchResponse()
468 c.xneedDisabled("untagged LSUB response", CapIMAP4rev2)
476 var params map[string]string
478 params = map[string]string{}
486 if _, ok := params[k]; ok {
487 c.xerrorf("duplicate key %q", k)
495 return UntaggedID(params)
509 return UntaggedVanished{earlier, NumSet{Ranges: uids}}
518 roots = append(roots, root)
521 return UntaggedQuotaroot(roots)
530 xresource := func() QuotaResource {
536 return QuotaResource{QuotaResourceName(strings.ToUpper(name)), usage, limit}
539 seen := map[QuotaResourceName]bool{}
540 l := []QuotaResource{xresource()}
541 seen[l[0].Name] = true
545 c.xerrorf("duplicate resource name %q", res.Name)
547 seen[res.Name] = true
552 return UntaggedQuota{root, l}
555 v, err := strconv.ParseUint(w, 10, 32)
560 W = strings.ToUpper(w)
564 c.xerrorf("invalid zero number for untagged fetch response")
573 c.xerrorf("invalid zero number for untagged expunge response")
576 return UntaggedExpunge(num)
580 return UntaggedExists(num)
583 c.xneedDisabled("should not send RECENT in IMAP4rev2", CapIMAP4rev2)
585 return UntaggedRecent(num)
588 c.xerrorf("unknown untagged numbered response %q", w)
592 c.xerrorf("unknown untagged response %q", w)
598// Already parsed: "*" SP nznumber SP "FETCH" SP
599func (c *Conn) xfetch(num uint32) UntaggedFetch {
601 attrs := []FetchAttr{c.xmsgatt1()}
603 attrs = append(attrs, c.xmsgatt1())
606 return UntaggedFetch{num, attrs}
610func (c *Conn) xmsgatt1() FetchAttr {
614 if b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b >= '0' && b <= '9' || b == '.' {
622 F := strings.ToUpper(f)
629 flags = []string{c.xflag()}
631 flags = append(flags, c.xflag())
635 return FetchFlags(flags)
639 return FetchEnvelope(c.xenvelope())
643 return FetchInternalDate(c.xquoted()) // todo: parsed time
647 return FetchRFC822Size(c.xint64())
652 return FetchRFC822(s)
654 case "RFC822.HEADER":
657 return FetchRFC822Header(s)
662 return FetchRFC822Text(s)
666 return FetchBodystructure{F, c.xbodystructure(false)}
669 section := c.xsection()
677 body := c.xnilString()
678 return FetchBody{F, section, offset, body}
680 case "BODYSTRUCTURE":
682 return FetchBodystructure{F, c.xbodystructure(true)}
686 nums := c.xsectionBinary()
689 buf := c.xnilStringLiteral8()
690 return FetchBinary{F, nums, string(buf)}
694 nums := c.xsectionBinary()
698 return FetchBinarySize{F, nums, size}
702 return FetchUID(c.xuint32())
710 return FetchModSeq(modseq)
712 c.xerrorf("unknown fetch attribute %q", f)
716func (c *Conn) xnilString() string {
719 } else if c.peek('{') {
720 return string(c.xliteral())
727func (c *Conn) xstring() string {
731 return string(c.xliteral())
734func (c *Conn) xastring() string {
737 } else if c.peek('{') {
738 return string(c.xliteral())
743func (c *Conn) xatom() string {
746 b, err := c.readbyte()
747 c.xcheckf(err, "read byte for atom")
748 if b <= ' ' || strings.IndexByte("(){%*\"\\]", b) >= 0 {
751 c.xerrorf("expected atom")
760func (c *Conn) xquoted() string {
764 r, err := c.readrune()
765 c.xcheckf(err, "reading rune in quoted string")
767 r, err = c.readrune()
768 c.xcheckf(err, "reading escaped char in quoted string")
769 if r != '\\' && r != '"' {
770 c.xerrorf("quoted char not backslash or dquote: %c", r)
773 // todo: probably refuse some more chars. like \0 and all ctl and backspace.
779func (c *Conn) xliteral() []byte {
786 c.xerrorf("refusing to read more than 1MB: %d", size)
789 _, err := fmt.Fprintf(c.conn, "+ ok\r\n")
790 c.xcheckf(err, "write continuation")
792 buf := make([]byte, int(size))
793 _, err := io.ReadFull(c.r, buf)
794 c.xcheckf(err, "reading data for literal")
800func (c *Conn) xflag0(allowPerm bool) string {
804 if allowPerm && c.take('*') {
807 } else if c.take('$') {
814func (c *Conn) xflag() string {
815 return c.xflag0(false)
818func (c *Conn) xflagPerm() string {
819 return c.xflag0(true)
822func (c *Conn) xsection() string {
824 s := c.xtakeuntil(']')
829func (c *Conn) xsectionBinary() []uint32 {
836 nums = append(nums, c.xnzuint32())
841func (c *Conn) xnilStringLiteral8() []byte {
842 // todo: should make difference for literal8 and literal from string, which bytes are allowed
843 if c.take('~') || c.peek('{') {
846 return []byte(c.xnilString())
850func (c *Conn) xbodystructure(extensibleForm bool) any {
854 parts := []any{c.xbodystructure(extensibleForm)}
856 parts = append(parts, c.xbodystructure(extensibleForm))
859 mediaSubtype := c.xstring()
860 var ext *BodyExtensionMpart
861 if extensibleForm && c.space() {
862 ext = c.xbodyExtMpart()
865 return BodyTypeMpart{parts, mediaSubtype, ext}
868 // todo: verify the media(sub)type is valid for returned data.
870 var ext *BodyExtension1Part
871 mediaType := c.xstring()
873 mediaSubtype := c.xstring()
875 bodyFields := c.xbodyFields()
877 // Basic type without extension.
879 return BodyTypeBasic{mediaType, mediaSubtype, bodyFields, nil}
883 envelope := c.xenvelope()
885 bodyStructure := c.xbodystructure(extensibleForm)
888 if extensibleForm && c.space() {
889 ext = c.xbodyExt1Part()
892 return BodyTypeMsg{mediaType, mediaSubtype, bodyFields, envelope, bodyStructure, lines, ext}
894 if !strings.EqualFold(mediaType, "text") {
896 c.xerrorf("body result, basic type, with disallowed extensible form")
898 ext = c.xbodyExt1Part()
901 return BodyTypeBasic{mediaType, mediaSubtype, bodyFields, ext}
905 if extensibleForm && c.space() {
906 ext = c.xbodyExt1Part()
909 return BodyTypeText{mediaType, mediaSubtype, bodyFields, lines, ext}
913func (c *Conn) xbodyFields() BodyFields {
914 params := c.xbodyFldParam()
916 contentID := c.xnilString()
918 contentDescr := c.xnilString()
920 cte := c.xnilString()
923 return BodyFields{params, contentID, contentDescr, cte, octets}
927func (c *Conn) xbodyExtMpart() (ext *BodyExtensionMpart) {
928 ext = &BodyExtensionMpart{}
929 ext.Params = c.xbodyFldParam()
933 ext.Disposition, ext.DispositionParams = c.xbodyFldDsp()
937 ext.Language = c.xbodyFldLang()
941 ext.Location = c.xbodyFldLoc()
943 ext.More = append(ext.More, c.xbodyExtension())
949func (c *Conn) xbodyExt1Part() (ext *BodyExtension1Part) {
950 ext = &BodyExtension1Part{}
951 ext.MD5 = c.xnilString()
955 ext.Disposition, ext.DispositionParams = c.xbodyFldDsp()
959 ext.Language = c.xbodyFldLang()
963 ext.Location = c.xbodyFldLoc()
965 ext.More = append(ext.More, c.xbodyExtension())
971func (c *Conn) xbodyFldParam() [][2]string {
976 l := [][2]string{{k, v}}
981 l = append(l, [2]string{k, v})
991func (c *Conn) xbodyFldDsp() (string, [][2]string) {
996 disposition := c.xstring()
998 param := c.xbodyFldParam()
1000 return disposition, param
1004func (c *Conn) xbodyFldLang() (lang []string) {
1006 lang = []string{c.xstring()}
1008 lang = append(lang, c.xstring())
1014 return []string{c.xstring()}
1021func (c *Conn) xbodyFldLoc() string {
1022 return c.xnilString()
1026func (c *Conn) xbodyExtension() (ext BodyExtension) {
1029 ext.More = append(ext.More, c.xbodyExtension())
1035 } else if c.peekdigit() {
1038 } else if c.peekstring() {
1048func (c *Conn) xenvelope() Envelope {
1050 date := c.xnilString()
1052 subject := c.xnilString()
1054 from := c.xaddresses()
1056 sender := c.xaddresses()
1058 replyTo := c.xaddresses()
1060 to := c.xaddresses()
1062 cc := c.xaddresses()
1064 bcc := c.xaddresses()
1066 inReplyTo := c.xnilString()
1068 messageID := c.xnilString()
1070 return Envelope{date, subject, from, sender, replyTo, to, cc, bcc, inReplyTo, messageID}
1074func (c *Conn) xaddresses() []Address {
1079 l := []Address{c.xaddress()}
1081 l = append(l, c.xaddress())
1087func (c *Conn) xaddress() Address {
1089 name := c.xnilString()
1091 adl := c.xnilString()
1093 mailbox := c.xnilString()
1095 host := c.xnilString()
1097 return Address{name, adl, mailbox, host}
1101func (c *Conn) xflagList() []string {
1105 l = []string{c.xflag()}
1107 l = append(l, c.xflag())
1115func (c *Conn) xmailboxList() UntaggedList {
1119 flags = append(flags, c.xflag())
1121 flags = append(flags, c.xflag())
1129 quoted = c.xquoted()
1130 if len(quoted) != 1 {
1131 c.xerrorf("mailbox-list has multichar quoted part: %q", quoted)
1134 } else if !c.peek(' ') {
1138 mailbox := c.xastring()
1139 ul := UntaggedList{flags, b, mailbox, nil, ""}
1143 c.xmboxListExtendedItem(&ul)
1145 c.xmboxListExtendedItem(&ul)
1154func (c *Conn) xmboxListExtendedItem(ul *UntaggedList) {
1157 if strings.ToUpper(tag) == "OLDNAME" {
1160 name := c.xastring()
1165 val := c.xtaggedExtVal()
1166 ul.Extended = append(ul.Extended, MboxListExtendedItem{tag, val})
1170func (c *Conn) xtaggedExtVal() TaggedExtVal {
1174 comp := c.xtaggedExtComp()
1180 // 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.
1181 b, err := c.readbyte()
1182 c.xcheckf(err, "read byte for tagged-ext-val")
1183 if b < '0' || b > '9' {
1185 ss := c.xsequenceSet()
1186 return TaggedExtVal{SeqSet: &ss}
1189 num, err := strconv.ParseInt(s, 10, 63)
1190 c.xcheckf(err, "parsing int")
1191 if !c.peek(':') && !c.peek(',') {
1192 // not a larger sequence-set
1193 return TaggedExtVal{Number: &num}
1196 sr.First = uint32(num)
1204 ss := c.xsequenceSet()
1205 ss.Ranges = append([]NumRange{sr}, ss.Ranges...)
1206 return TaggedExtVal{SeqSet: &ss}
1210func (c *Conn) xsequenceSet() NumSet {
1212 return NumSet{SearchResult: true}
1218 sr.First = c.xnzuint32()
1227 ss.Ranges = append(ss.Ranges, sr)
1236func (c *Conn) xtaggedExtComp() TaggedExtComp {
1238 r := c.xtaggedExtComp()
1240 return TaggedExtComp{Comps: []TaggedExtComp{r}}
1244 return TaggedExtComp{String: s}
1246 l := []TaggedExtComp{{String: s}}
1248 l = append(l, c.xtaggedExtComp())
1250 return TaggedExtComp{Comps: l}
1254func (c *Conn) xnamespace() []NamespaceDescr {
1260 l := []NamespaceDescr{c.xnamespaceDescr()}
1262 l = append(l, c.xnamespaceDescr())
1268func (c *Conn) xnamespaceDescr() NamespaceDescr {
1270 prefix := c.xstring()
1276 c.xerrorf("namespace-descr: expected single char, got %q", s)
1282 var exts []NamespaceExtension
1288 values := []string{c.xstring()}
1290 values = append(values, c.xstring())
1293 exts = append(exts, NamespaceExtension{key, values})
1295 return NamespaceDescr{prefix, b, exts}
1298// require all of caps to be disabled.
1299func (c *Conn) xneedDisabled(msg string, caps ...Capability) {
1300 for _, cap := range caps {
1301 if _, ok := c.CapEnabled[cap]; ok {
1302 c.xerrorf("%s: invalid because of enabled capability %q", msg, cap)
1308// Already consumed: "ESEARCH"
1309func (c *Conn) xesearchResponse() (r UntaggedEsearch) {
1318 r.Correlator = c.xastring()
1325 W := strings.ToUpper(w)
1332 W = strings.ToUpper(w)
1339 c.xerrorf("duplicate MIN in ESEARCH")
1342 num := c.xnzuint32()
1347 c.xerrorf("duplicate MAX in ESEARCH")
1350 num := c.xnzuint32()
1354 if !r.All.IsZero() {
1355 c.xerrorf("duplicate ALL in ESEARCH")
1358 ss := c.xsequenceSet()
1359 if ss.SearchResult {
1360 c.xerrorf("$ for last not valid in ESEARCH")
1366 c.xerrorf("duplicate COUNT in ESEARCH")
1375 r.ModSeq = c.xint64()
1379 for i, b := range []byte(w) {
1380 if !(b >= 'A' && b <= 'Z' || strings.IndexByte("-_.", b) >= 0 || i > 0 && strings.IndexByte("0123456789:", b) >= 0) {
1381 c.xerrorf("invalid tag %q", w)
1385 ext := EsearchDataExt{w, c.xtaggedExtVal()}
1386 r.Exts = append(r.Exts, ext)
1392 w = c.xnonspace() // todo: this is too loose
1393 W = strings.ToUpper(w)
1399func (c *Conn) xcharset() string {
1407func (c *Conn) xuidset() []NumRange {
1408 ranges := []NumRange{c.xuidrange()}
1410 ranges = append(ranges, c.xuidrange())
1415func (c *Conn) xuidrange() NumRange {
1416 uid := c.xnzuint32()
1422 return NumRange{uid, end}
1426func (c *Conn) xlsub() UntaggedLsub {
1431 if len(r.Flags) > 0 {
1434 r.Flags = append(r.Flags, c.xflag())
1444 // todo: check valid char
1445 c.xerrorf("invalid separator %q", s)
1447 r.Separator = byte(s[0])
1450 r.Mailbox = c.xastring()