11func (c *Conn) recorded() string {
12 s := string(c.recordBuf)
18func (c *Conn) recordAdd(buf []byte) {
20 c.recordBuf = append(c.recordBuf, buf...)
24func (c *Conn) xtake(s string) {
25 buf := make([]byte, len(s))
26 _, err := io.ReadFull(c.br, buf)
27 c.xcheckf(err, "taking %q", s)
28 if !strings.EqualFold(string(buf), s) {
29 c.xerrorf("got %q, expected %q", buf, s)
34func (c *Conn) readbyte() (byte, error) {
35 b, err := c.br.ReadByte()
37 c.recordAdd([]byte{b})
42func (c *Conn) unreadbyte() {
44 c.recordBuf = c.recordBuf[:len(c.recordBuf)-1]
46 err := c.br.UnreadByte()
47 c.xcheckf(err, "unread byte")
50func (c *Conn) readrune() (rune, error) {
51 x, _, err := c.br.ReadRune()
53 c.recordAdd([]byte(string(x)))
58func (c *Conn) space() bool {
62func (c *Conn) xspace() {
66func (c *Conn) xcrlf() {
70func (c *Conn) peek(exp byte) bool {
71 b, err := c.readbyte()
75 return err == nil && strings.EqualFold(string(rune(b)), string(rune(exp)))
78func (c *Conn) peekstring() bool {
79 return c.peek('"') || c.peek('{')
82func (c *Conn) take(exp byte) bool {
90func (c *Conn) xstatus() Status {
92 W := strings.ToUpper(w)
101 c.xerrorf("expected status, got %q", w)
105// Already consumed: tag SP status SP
106func (c *Conn) xresult(status Status) Result {
107 respText := c.xrespText()
108 return Result{status, respText}
111func (c *Conn) xrespText() RespText {
115 code, codeArg = c.xrespCode()
121 more += string(rune(c.xbyte()))
123 return RespText{code, codeArg, more}
126var knownCodes = stringMap(
127 // Without parameters.
128 "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",
132 "BADCHARSET", "CAPABILITY", "PERMANENTFLAGS", "UIDNEXT", "UIDVALIDITY", "UNSEEN", "APPENDUID", "COPYUID",
133 "HIGHESTMODSEQ", "MODIFIED",
136func stringMap(l ...string) map[string]struct{} {
137 r := map[string]struct{}{}
138 for _, s := range l {
145func (c *Conn) xrespCode() (string, CodeArg) {
147 for !c.peek(' ') && !c.peek(']') {
148 w += string(rune(c.xbyte()))
150 W := strings.ToUpper(w)
152 if _, ok := knownCodes[W]; !ok {
156 for !c.peek(' ') && !c.peek(']') {
157 arg += string(rune(c.xbyte()))
159 args = append(args, arg)
161 return W, CodeOther{W, args}
167 var l []string // Must be nil initially.
170 l = []string{c.xcharset()}
172 l = append(l, c.xcharset())
176 codeArg = CodeList{W, l}
179 caps := []string{c.xatom()}
181 caps = append(caps, c.xatom())
183 c.CapAvailable = map[Capability]struct{}{}
184 for _, cap := range caps {
185 cap = strings.ToUpper(cap)
186 c.CapAvailable[Capability(cap)] = struct{}{}
188 codeArg = CodeWords{W, caps}
190 case "PERMANENTFLAGS":
191 l := []string{} // Must be non-nil.
194 l = []string{c.xflagPerm()}
196 l = append(l, c.xflagPerm())
200 codeArg = CodeList{W, l}
201 case "UIDNEXT", "UIDVALIDITY", "UNSEEN":
203 codeArg = CodeUint{W, c.xnzuint32()}
206 destUIDValidity := c.xnzuint32()
209 codeArg = CodeAppendUID{destUIDValidity, uid}
212 destUIDValidity := c.xnzuint32()
217 codeArg = CodeCopyUID{destUIDValidity, from, to}
218 case "HIGHESTMODSEQ":
220 codeArg = CodeHighestModSeq(c.xint64())
223 modified := c.xuidset()
224 codeArg = CodeModified(NumSet{Ranges: modified})
229func (c *Conn) xbyte() byte {
230 b, err := c.readbyte()
231 c.xcheckf(err, "read byte")
235// take until b is seen. don't take b itself.
236func (c *Conn) xtakeuntil(b byte) string {
239 x, err := c.readbyte()
240 c.xcheckf(err, "read byte")
249func (c *Conn) xdigits() string {
252 b, err := c.readbyte()
253 if err == nil && (b >= '0' && b <= '9') {
262func (c *Conn) peekdigit() bool {
263 if b, err := c.readbyte(); err == nil {
265 return b >= '0' && b <= '9'
270func (c *Conn) xint32() int32 {
272 num, err := strconv.ParseInt(s, 10, 32)
273 c.xcheckf(err, "parsing int32")
277func (c *Conn) xint64() int64 {
279 num, err := strconv.ParseInt(s, 10, 63)
280 c.xcheckf(err, "parsing int64")
284func (c *Conn) xuint32() uint32 {
286 num, err := strconv.ParseUint(s, 10, 32)
287 c.xcheckf(err, "parsing uint32")
291func (c *Conn) xnzuint32() uint32 {
294 c.xerrorf("got 0, expected nonzero uint")
299// todo: replace with proper parsing.
300func (c *Conn) xnonspace() string {
302 for !c.peek(' ') && !c.peek('\r') && !c.peek('\n') {
303 s += string(rune(c.xbyte()))
306 c.xerrorf("expected non-space")
311// todo: replace with proper parsing
312func (c *Conn) xword() string {
316// "*" SP is already consumed
318func (c *Conn) xuntagged() Untagged {
320 W := strings.ToUpper(w)
324 r := UntaggedPreauth(c.xrespText())
330 r := UntaggedBye(c.xrespText())
334 case "OK", "NO", "BAD":
336 r := UntaggedResult(c.xresult(Status(W)))
344 caps = append(caps, c.xnonspace())
346 c.CapAvailable = map[Capability]struct{}{}
347 for _, cap := range caps {
348 cap = strings.ToUpper(cap)
349 c.CapAvailable[Capability(cap)] = struct{}{}
351 r := UntaggedCapability(caps)
359 caps = append(caps, c.xnonspace())
361 for _, cap := range caps {
362 cap = strings.ToUpper(cap)
363 c.CapEnabled[Capability(cap)] = struct{}{}
365 r := UntaggedEnabled(caps)
371 r := UntaggedFlags(c.xflagList())
377 r := c.xmailboxList()
384 mailbox := c.xastring()
387 attrs := map[StatusAttr]int64{}
394 S := StatusAttr(strings.ToUpper(s))
399 num = int64(c.xuint32())
401 num = int64(c.xnzuint32())
403 num = int64(c.xnzuint32())
405 num = int64(c.xuint32())
407 num = int64(c.xuint32())
411 c.xneedDisabled("RECENT status flag", CapIMAP4rev2)
412 num = int64(c.xuint32())
414 if c.peek('n') || c.peek('N') {
419 case "HIGHESTMODSEQ":
421 case "DELETED-STORAGE":
424 c.xerrorf("status: unknown attribute %q", s)
426 if _, ok := attrs[S]; ok {
427 c.xerrorf("status: duplicate attribute %q", s)
431 r := UntaggedStatus{mailbox, attrs}
438 mailbox := c.xastring()
441 // Unsolicited form, with only annotation keys, not values.
445 keys = append(keys, key)
451 return UntaggedMetadataKeys{mailbox, keys}
454 // Form with values, in response to GETMETADATA command.
455 r := UntaggedMetadataAnnotations{Mailbox: mailbox}
464 value = []byte(c.xstring())
466 // note: the abnf also allows nstring, but that only makes sense when the
469 r.Annotations = append(r.Annotations, Annotation{key, isString, value})
482 personal := c.xnamespace()
484 other := c.xnamespace()
486 shared := c.xnamespace()
487 r := UntaggedNamespace{personal, other, shared}
493 c.xneedDisabled("untagged SEARCH response", CapIMAP4rev2)
503 return UntaggedSearchModSeq{nums, modseq}
505 nums = append(nums, c.xnzuint32())
507 r := UntaggedSearch(nums)
512 r := c.xesearchResponse()
517 c.xneedDisabled("untagged LSUB response", CapIMAP4rev2)
525 var params map[string]string
527 params = map[string]string{}
535 if _, ok := params[k]; ok {
536 c.xerrorf("duplicate key %q", k)
544 return UntaggedID(params)
558 return UntaggedVanished{earlier, NumSet{Ranges: uids}}
567 roots = append(roots, root)
570 return UntaggedQuotaroot(roots)
579 xresource := func() QuotaResource {
585 return QuotaResource{QuotaResourceName(strings.ToUpper(name)), usage, limit}
588 seen := map[QuotaResourceName]bool{}
589 l := []QuotaResource{xresource()}
590 seen[l[0].Name] = true
594 c.xerrorf("duplicate resource name %q", res.Name)
596 seen[res.Name] = true
601 return UntaggedQuota{root, l}
604 v, err := strconv.ParseUint(w, 10, 32)
609 W = strings.ToUpper(w)
613 c.xerrorf("invalid zero number for untagged fetch response")
622 c.xerrorf("invalid zero number for untagged expunge response")
625 return UntaggedExpunge(num)
629 return UntaggedExists(num)
632 c.xneedDisabled("should not send RECENT in IMAP4rev2", CapIMAP4rev2)
634 return UntaggedRecent(num)
637 c.xerrorf("unknown untagged numbered response %q", w)
641 c.xerrorf("unknown untagged response %q", w)
647// Already parsed: "*" SP nznumber SP "FETCH" SP
648func (c *Conn) xfetch(num uint32) UntaggedFetch {
650 attrs := []FetchAttr{c.xmsgatt1()}
652 attrs = append(attrs, c.xmsgatt1())
655 return UntaggedFetch{num, attrs}
659func (c *Conn) xmsgatt1() FetchAttr {
663 if b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b >= '0' && b <= '9' || b == '.' {
671 F := strings.ToUpper(f)
678 flags = []string{c.xflag()}
680 flags = append(flags, c.xflag())
684 return FetchFlags(flags)
688 return FetchEnvelope(c.xenvelope())
693 v, err := time.Parse("_2-Jan-2006 15:04:05 -0700", s)
694 c.xcheckf(err, "parsing internaldate")
695 return FetchInternalDate{v}
702 v, err := time.Parse("_2-Jan-2006 15:04:05 -0700", s)
703 c.xcheckf(err, "parsing savedate")
708 return FetchSaveDate{t}
712 return FetchRFC822Size(c.xint64())
717 return FetchRFC822(s)
719 case "RFC822.HEADER":
722 return FetchRFC822Header(s)
727 return FetchRFC822Text(s)
731 return FetchBodystructure{F, c.xbodystructure(false)}
734 section := c.xsection()
742 body := c.xnilString()
743 return FetchBody{F, section, offset, body}
745 case "BODYSTRUCTURE":
747 return FetchBodystructure{F, c.xbodystructure(true)}
751 nums := c.xsectionBinary()
754 buf := c.xnilStringLiteral8()
755 return FetchBinary{F, nums, string(buf)}
759 nums := c.xsectionBinary()
763 return FetchBinarySize{F, nums, size}
767 return FetchUID(c.xuint32())
775 return FetchModSeq(modseq)
777 c.xerrorf("unknown fetch attribute %q", f)
781func (c *Conn) xnilString() string {
784 } else if c.peek('{') {
785 return string(c.xliteral())
792func (c *Conn) xstring() string {
796 return string(c.xliteral())
799func (c *Conn) xastring() string {
802 } else if c.peek('{') {
803 return string(c.xliteral())
808func (c *Conn) xatom() string {
811 b, err := c.readbyte()
812 c.xcheckf(err, "read byte for atom")
813 if b <= ' ' || strings.IndexByte("(){%*\"\\]", b) >= 0 {
816 c.xerrorf("expected atom")
825func (c *Conn) xquoted() string {
829 r, err := c.readrune()
830 c.xcheckf(err, "reading rune in quoted string")
832 r, err = c.readrune()
833 c.xcheckf(err, "reading escaped char in quoted string")
834 if r != '\\' && r != '"' {
835 c.xerrorf("quoted char not backslash or dquote: %c", r)
838 // todo: probably refuse some more chars. like \0 and all ctl and backspace.
844func (c *Conn) xliteral() []byte {
851 c.xerrorf("refusing to read more than 1MB: %d", size)
854 _, err := fmt.Fprintf(c.bw, "+ ok\r\n")
855 c.xcheckf(err, "write continuation")
858 buf := make([]byte, int(size))
859 _, err := io.ReadFull(c.br, buf)
860 c.xcheckf(err, "reading data for literal")
866func (c *Conn) xflag0(allowPerm bool) string {
870 if allowPerm && c.take('*') {
873 } else if c.take('$') {
880func (c *Conn) xflag() string {
881 return c.xflag0(false)
884func (c *Conn) xflagPerm() string {
885 return c.xflag0(true)
888func (c *Conn) xsection() string {
890 s := c.xtakeuntil(']')
895func (c *Conn) xsectionBinary() []uint32 {
902 nums = append(nums, c.xnzuint32())
907func (c *Conn) xnilStringLiteral8() []byte {
908 // todo: should make difference for literal8 and literal from string, which bytes are allowed
909 if c.take('~') || c.peek('{') {
912 return []byte(c.xnilString())
916func (c *Conn) xbodystructure(extensibleForm bool) any {
920 parts := []any{c.xbodystructure(extensibleForm)}
922 parts = append(parts, c.xbodystructure(extensibleForm))
925 mediaSubtype := c.xstring()
926 var ext *BodyExtensionMpart
927 if extensibleForm && c.space() {
928 ext = c.xbodyExtMpart()
931 return BodyTypeMpart{parts, mediaSubtype, ext}
934 // todo: verify the media(sub)type is valid for returned data.
936 var ext *BodyExtension1Part
937 mediaType := c.xstring()
939 mediaSubtype := c.xstring()
941 bodyFields := c.xbodyFields()
943 // Basic type without extension.
945 return BodyTypeBasic{mediaType, mediaSubtype, bodyFields, nil}
949 envelope := c.xenvelope()
951 bodyStructure := c.xbodystructure(extensibleForm)
954 if extensibleForm && c.space() {
955 ext = c.xbodyExt1Part()
958 return BodyTypeMsg{mediaType, mediaSubtype, bodyFields, envelope, bodyStructure, lines, ext}
960 if !strings.EqualFold(mediaType, "text") {
962 c.xerrorf("body result, basic type, with disallowed extensible form")
964 ext = c.xbodyExt1Part()
967 return BodyTypeBasic{mediaType, mediaSubtype, bodyFields, ext}
971 if extensibleForm && c.space() {
972 ext = c.xbodyExt1Part()
975 return BodyTypeText{mediaType, mediaSubtype, bodyFields, lines, ext}
979func (c *Conn) xbodyFields() BodyFields {
980 params := c.xbodyFldParam()
982 contentID := c.xnilString()
984 contentDescr := c.xnilString()
986 cte := c.xnilString()
989 return BodyFields{params, contentID, contentDescr, cte, octets}
993func (c *Conn) xbodyExtMpart() (ext *BodyExtensionMpart) {
994 ext = &BodyExtensionMpart{}
995 ext.Params = c.xbodyFldParam()
999 ext.Disposition, ext.DispositionParams = c.xbodyFldDsp()
1003 ext.Language = c.xbodyFldLang()
1007 ext.Location = c.xbodyFldLoc()
1009 ext.More = append(ext.More, c.xbodyExtension())
1015func (c *Conn) xbodyExt1Part() (ext *BodyExtension1Part) {
1016 ext = &BodyExtension1Part{}
1017 ext.MD5 = c.xnilString()
1021 ext.Disposition, ext.DispositionParams = c.xbodyFldDsp()
1025 ext.Language = c.xbodyFldLang()
1029 ext.Location = c.xbodyFldLoc()
1031 ext.More = append(ext.More, c.xbodyExtension())
1037func (c *Conn) xbodyFldParam() [][2]string {
1042 l := [][2]string{{k, v}}
1047 l = append(l, [2]string{k, v})
1057func (c *Conn) xbodyFldDsp() (string, [][2]string) {
1062 disposition := c.xstring()
1064 param := c.xbodyFldParam()
1066 return disposition, param
1070func (c *Conn) xbodyFldLang() (lang []string) {
1072 lang = []string{c.xstring()}
1074 lang = append(lang, c.xstring())
1080 return []string{c.xstring()}
1087func (c *Conn) xbodyFldLoc() string {
1088 return c.xnilString()
1092func (c *Conn) xbodyExtension() (ext BodyExtension) {
1095 ext.More = append(ext.More, c.xbodyExtension())
1101 } else if c.peekdigit() {
1104 } else if c.peekstring() {
1114func (c *Conn) xenvelope() Envelope {
1116 date := c.xnilString()
1118 subject := c.xnilString()
1120 from := c.xaddresses()
1122 sender := c.xaddresses()
1124 replyTo := c.xaddresses()
1126 to := c.xaddresses()
1128 cc := c.xaddresses()
1130 bcc := c.xaddresses()
1132 inReplyTo := c.xnilString()
1134 messageID := c.xnilString()
1136 return Envelope{date, subject, from, sender, replyTo, to, cc, bcc, inReplyTo, messageID}
1140func (c *Conn) xaddresses() []Address {
1145 l := []Address{c.xaddress()}
1147 l = append(l, c.xaddress())
1153func (c *Conn) xaddress() Address {
1155 name := c.xnilString()
1157 adl := c.xnilString()
1159 mailbox := c.xnilString()
1161 host := c.xnilString()
1163 return Address{name, adl, mailbox, host}
1167func (c *Conn) xflagList() []string {
1171 l = []string{c.xflag()}
1173 l = append(l, c.xflag())
1181func (c *Conn) xmailboxList() UntaggedList {
1185 flags = append(flags, c.xflag())
1187 flags = append(flags, c.xflag())
1195 quoted = c.xquoted()
1196 if len(quoted) != 1 {
1197 c.xerrorf("mailbox-list has multichar quoted part: %q", quoted)
1200 } else if !c.peek(' ') {
1204 mailbox := c.xastring()
1205 ul := UntaggedList{flags, b, mailbox, nil, ""}
1209 c.xmboxListExtendedItem(&ul)
1211 c.xmboxListExtendedItem(&ul)
1220func (c *Conn) xmboxListExtendedItem(ul *UntaggedList) {
1223 if strings.ToUpper(tag) == "OLDNAME" {
1226 name := c.xastring()
1231 val := c.xtaggedExtVal()
1232 ul.Extended = append(ul.Extended, MboxListExtendedItem{tag, val})
1236func (c *Conn) xtaggedExtVal() TaggedExtVal {
1240 comp := c.xtaggedExtComp()
1246 // 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.
1247 b, err := c.readbyte()
1248 c.xcheckf(err, "read byte for tagged-ext-val")
1249 if b < '0' || b > '9' {
1251 ss := c.xsequenceSet()
1252 return TaggedExtVal{SeqSet: &ss}
1255 num, err := strconv.ParseInt(s, 10, 63)
1256 c.xcheckf(err, "parsing int")
1257 if !c.peek(':') && !c.peek(',') {
1258 // not a larger sequence-set
1259 return TaggedExtVal{Number: &num}
1262 sr.First = uint32(num)
1270 ss := c.xsequenceSet()
1271 ss.Ranges = append([]NumRange{sr}, ss.Ranges...)
1272 return TaggedExtVal{SeqSet: &ss}
1276func (c *Conn) xsequenceSet() NumSet {
1278 return NumSet{SearchResult: true}
1284 sr.First = c.xnzuint32()
1293 ss.Ranges = append(ss.Ranges, sr)
1302func (c *Conn) xtaggedExtComp() TaggedExtComp {
1304 r := c.xtaggedExtComp()
1306 return TaggedExtComp{Comps: []TaggedExtComp{r}}
1310 return TaggedExtComp{String: s}
1312 l := []TaggedExtComp{{String: s}}
1314 l = append(l, c.xtaggedExtComp())
1316 return TaggedExtComp{Comps: l}
1320func (c *Conn) xnamespace() []NamespaceDescr {
1326 l := []NamespaceDescr{c.xnamespaceDescr()}
1328 l = append(l, c.xnamespaceDescr())
1334func (c *Conn) xnamespaceDescr() NamespaceDescr {
1336 prefix := c.xstring()
1342 c.xerrorf("namespace-descr: expected single char, got %q", s)
1348 var exts []NamespaceExtension
1354 values := []string{c.xstring()}
1356 values = append(values, c.xstring())
1359 exts = append(exts, NamespaceExtension{key, values})
1361 return NamespaceDescr{prefix, b, exts}
1364// require all of caps to be disabled.
1365func (c *Conn) xneedDisabled(msg string, caps ...Capability) {
1366 for _, cap := range caps {
1367 if _, ok := c.CapEnabled[cap]; ok {
1368 c.xerrorf("%s: invalid because of enabled capability %q", msg, cap)
1374// Already consumed: "ESEARCH"
1375func (c *Conn) xesearchResponse() (r UntaggedEsearch) {
1384 r.Correlator = c.xastring()
1391 W := strings.ToUpper(w)
1398 W = strings.ToUpper(w)
1405 c.xerrorf("duplicate MIN in ESEARCH")
1408 num := c.xnzuint32()
1413 c.xerrorf("duplicate MAX in ESEARCH")
1416 num := c.xnzuint32()
1420 if !r.All.IsZero() {
1421 c.xerrorf("duplicate ALL in ESEARCH")
1424 ss := c.xsequenceSet()
1425 if ss.SearchResult {
1426 c.xerrorf("$ for last not valid in ESEARCH")
1432 c.xerrorf("duplicate COUNT in ESEARCH")
1441 r.ModSeq = c.xint64()
1445 for i, b := range []byte(w) {
1446 if !(b >= 'A' && b <= 'Z' || strings.IndexByte("-_.", b) >= 0 || i > 0 && strings.IndexByte("0123456789:", b) >= 0) {
1447 c.xerrorf("invalid tag %q", w)
1451 ext := EsearchDataExt{w, c.xtaggedExtVal()}
1452 r.Exts = append(r.Exts, ext)
1458 w = c.xnonspace() // todo: this is too loose
1459 W = strings.ToUpper(w)
1465func (c *Conn) xcharset() string {
1473func (c *Conn) xuidset() []NumRange {
1474 ranges := []NumRange{c.xuidrange()}
1476 ranges = append(ranges, c.xuidrange())
1481func (c *Conn) xuidrange() NumRange {
1482 uid := c.xnzuint32()
1488 return NumRange{uid, end}
1492func (c *Conn) xlsub() UntaggedLsub {
1497 if len(r.Flags) > 0 {
1500 r.Flags = append(r.Flags, c.xflag())
1510 // todo: check valid char
1511 c.xerrorf("invalid separator %q", s)
1513 r.Separator = byte(s[0])
1516 r.Mailbox = c.xastring()