10 "github.com/mjl-/mox/mlog"
13// todo: stricter parsing. xnonspace() and xword() should be replaced by proper parsers
15// Keep the parsing method names and the types similar to the ABNF names in the RFCs.
17func (p *Proto) recorded() string {
18 s := string(p.recordBuf)
24func (p *Proto) recordAdd(buf []byte) {
26 p.recordBuf = append(p.recordBuf, buf...)
30func (p *Proto) xtake(s string) {
31 buf := make([]byte, len(s))
32 _, err := io.ReadFull(p.br, buf)
33 p.xcheckf(err, "taking %q", s)
34 if !strings.EqualFold(string(buf), s) {
35 p.xerrorf("got %q, expected %q", buf, s)
40func (p *Proto) readbyte() (byte, error) {
41 b, err := p.br.ReadByte()
43 p.recordAdd([]byte{b})
48func (p *Proto) xunreadbyte() {
50 p.recordBuf = p.recordBuf[:len(p.recordBuf)-1]
52 err := p.br.UnreadByte()
53 p.xcheckf(err, "unread byte")
56func (p *Proto) readrune() (rune, error) {
57 x, _, err := p.br.ReadRune()
59 p.recordAdd([]byte(string(x)))
64func (p *Proto) space() bool {
68func (p *Proto) xspace() {
72func (p *Proto) xcrlf() {
76func (p *Proto) peek(exp byte) bool {
77 b, err := p.readbyte()
81 return err == nil && strings.EqualFold(string(rune(b)), string(rune(exp)))
84func (p *Proto) peekstring() bool {
85 return p.peek('"') || p.peek('{')
88func (p *Proto) take(exp byte) bool {
96func (p *Proto) xstatus() Status {
98 W := strings.ToUpper(w)
107 p.xerrorf("expected status, got %q", w)
111// Already consumed: tag SP status SP
112func (p *Proto) xresult(status Status) Result {
113 code, text := p.xrespText()
114 return Result{status, code, text}
117func (p *Proto) xrespText() (code Code, text string) {
124 text += string(rune(p.xbyte()))
130func (p *Proto) xrespCode() Code {
132 for !p.peek(' ') && !p.peek(']') {
133 w += string(rune(p.xbyte()))
135 W := strings.ToUpper(w)
139 var l []string // Must be nil initially.
142 l = []string{p.xcharset()}
144 l = append(l, p.xcharset())
148 return CodeBadCharset(l)
151 caps := []Capability{}
154 s = strings.ToUpper(s)
155 caps = append(caps, Capability(s))
160 return CodeCapability(caps)
161 case "PERMANENTFLAGS":
162 l := []string{} // Must be non-nil.
165 l = []string{p.xflagPerm()}
167 l = append(l, p.xflagPerm())
171 return CodePermanentFlags(l)
174 return CodeUIDNext(p.xnzuint32())
177 return CodeUIDValidity(p.xnzuint32())
180 return CodeUnseen(p.xnzuint32())
183 destUIDValidity := p.xnzuint32()
185 uids := p.xuidrange()
186 return CodeAppendUID{destUIDValidity, uids}
189 destUIDValidity := p.xnzuint32()
194 return CodeCopyUID{destUIDValidity, from, to}
195 case "HIGHESTMODSEQ":
197 return CodeHighestModSeq(p.xint64())
200 modified := p.xuidset()
201 return CodeModified(NumSet{Ranges: modified})
205 var current, goal *uint32
210 if p.peek('n') || p.peek('N') {
217 if p.peek('n') || p.peek('N') {
225 return CodeInProgress{tag, current, goal}
239 return CodeBadEvent(l)
244 p.xtake("LONGENTRIES")
247 return CodeMetadataLongEntries(num)
249 w := strings.ToUpper(p.xatom())
255 return CodeMetadataMaxSize(num)
258 return CodeMetadataTooMany{}
261 return CodeMetadataNoPrivate{}
263 p.xerrorf("parsing METADATA response code, got %q, expected one of MAXSIZE, TOOMANY, NOPRIVATE", w)
266 // Known codes without parameters.
274 "AUTHENTICATIONFAILED",
275 "AUTHORIZATIONFAILED",
303 for !p.peek(' ') && !p.peek(']') {
304 arg += string(rune(p.xbyte()))
306 args = append(args, arg)
311 return CodeParams{W, args}
315func (p *Proto) xbyte() byte {
316 b, err := p.readbyte()
317 p.xcheckf(err, "read byte")
321// take until b is seen. don't take b itself.
322func (p *Proto) xtakeuntil(b byte) string {
325 x, err := p.readbyte()
326 p.xcheckf(err, "read byte")
335func (p *Proto) xdigits() string {
338 b, err := p.readbyte()
339 if err == nil && (b >= '0' && b <= '9') {
348func (p *Proto) peekdigit() bool {
349 if b, err := p.readbyte(); err == nil {
351 return b >= '0' && b <= '9'
356func (p *Proto) xint32() int32 {
358 num, err := strconv.ParseInt(s, 10, 32)
359 p.xcheckf(err, "parsing int32")
363func (p *Proto) xint64() int64 {
365 num, err := strconv.ParseInt(s, 10, 63)
366 p.xcheckf(err, "parsing int64")
370func (p *Proto) xuint32() uint32 {
372 num, err := strconv.ParseUint(s, 10, 32)
373 p.xcheckf(err, "parsing uint32")
377func (p *Proto) xnzuint32() uint32 {
380 p.xerrorf("got 0, expected nonzero uint")
385// todo: replace with proper parsing.
386func (p *Proto) xnonspace() string {
388 for !p.peek(' ') && !p.peek('\r') && !p.peek('\n') {
389 s += string(rune(p.xbyte()))
392 p.xerrorf("expected non-space")
397// todo: replace with proper parsing
398func (p *Proto) xword() string {
402// "*" SP is already consumed
404func (p *Proto) xuntagged() Untagged {
406 W := strings.ToUpper(w)
410 code, text := p.xrespText()
411 r := UntaggedPreauth{code, text}
417 code, text := p.xrespText()
418 r := UntaggedBye{code, text}
422 case "OK", "NO", "BAD":
424 r := UntaggedResult(p.xresult(Status(W)))
430 var caps []Capability
433 s = strings.ToUpper(s)
435 caps = append(caps, cc)
438 return UntaggedCapability(caps)
442 var caps []Capability
445 s = strings.ToUpper(s)
447 caps = append(caps, cc)
450 return UntaggedEnabled(caps)
454 r := UntaggedFlags(p.xflagList())
460 r := p.xmailboxList()
467 mailbox := p.xastring()
470 attrs := map[StatusAttr]int64{}
477 S := StatusAttr(strings.ToUpper(s))
482 num = int64(p.xuint32())
484 num = int64(p.xnzuint32())
486 num = int64(p.xnzuint32())
488 num = int64(p.xuint32())
490 num = int64(p.xuint32())
494 num = int64(p.xuint32())
496 if p.peek('n') || p.peek('N') {
501 case "HIGHESTMODSEQ":
503 case "DELETED-STORAGE":
506 p.xerrorf("status: unknown attribute %q", s)
508 if _, ok := attrs[S]; ok {
509 p.xerrorf("status: duplicate attribute %q", s)
513 r := UntaggedStatus{mailbox, attrs}
520 mailbox := p.xastring()
523 // Unsolicited form, with only annotation keys, not values.
527 keys = append(keys, key)
533 return UntaggedMetadataKeys{mailbox, keys}
536 // Form with values, in response to GETMETADATA command.
537 r := UntaggedMetadataAnnotations{Mailbox: mailbox}
545 } else if p.peek('"') {
546 value = []byte(p.xstring())
548 // note: the abnf also allows nstring, but that only makes sense when the
551 // For response to extended list.
554 r.Annotations = append(r.Annotations, Annotation{key, isString, value})
567 personal := p.xnamespace()
569 other := p.xnamespace()
571 shared := p.xnamespace()
572 r := UntaggedNamespace{personal, other, shared}
587 return UntaggedSearchModSeq{nums, modseq}
589 nums = append(nums, p.xnzuint32())
591 r := UntaggedSearch(nums)
596 r := p.xesearchResponse()
608 var params map[string]string
610 params = map[string]string{}
618 if _, ok := params[k]; ok {
619 p.xerrorf("duplicate key %q", k)
627 return UntaggedID(params)
641 return UntaggedVanished{earlier, NumSet{Ranges: uids}}
650 roots = append(roots, root)
653 return UntaggedQuotaroot(roots)
662 xresource := func() QuotaResource {
668 return QuotaResource{QuotaResourceName(strings.ToUpper(name)), usage, limit}
671 seen := map[QuotaResourceName]bool{}
672 l := []QuotaResource{xresource()}
673 seen[l[0].Name] = true
677 p.xerrorf("duplicate resource name %q", res.Name)
679 seen[res.Name] = true
684 return UntaggedQuota{root, l}
687 v, err := strconv.ParseUint(w, 10, 32)
692 W = strings.ToUpper(w)
694 case "FETCH", "UIDFETCH":
696 p.xerrorf("invalid zero number for untagged fetch response")
702 return UntaggedUIDFetch{num, attrs}
704 return UntaggedFetch{num, attrs}
708 p.xerrorf("invalid zero number for untagged expunge response")
711 return UntaggedExpunge(num)
715 return UntaggedExists(num)
719 return UntaggedRecent(num)
722 p.xerrorf("unknown untagged numbered response %q", w)
726 p.xerrorf("unknown untagged response %q", w)
732// Already parsed: "*" SP nznumber SP "FETCH" SP
733func (p *Proto) xfetch() []FetchAttr {
735 attrs := []FetchAttr{p.xmsgatt1()}
737 attrs = append(attrs, p.xmsgatt1())
744func (p *Proto) xmsgatt1() FetchAttr {
748 if b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b >= '0' && b <= '9' || b == '.' {
756 F := strings.ToUpper(f)
763 flags = []string{p.xflag()}
765 flags = append(flags, p.xflag())
769 return FetchFlags(flags)
773 return FetchEnvelope(p.xenvelope())
778 v, err := time.Parse("_2-Jan-2006 15:04:05 -0700", s)
779 p.xcheckf(err, "parsing internaldate")
780 return FetchInternalDate{v}
787 v, err := time.Parse("_2-Jan-2006 15:04:05 -0700", s)
788 p.xcheckf(err, "parsing savedate")
793 return FetchSaveDate{t}
797 return FetchRFC822Size(p.xint64())
802 return FetchRFC822(s)
804 case "RFC822.HEADER":
807 return FetchRFC822Header(s)
812 return FetchRFC822Text(s)
816 return FetchBodystructure{F, p.xbodystructure(false)}
819 section := p.xsection()
827 body := p.xnilString()
828 return FetchBody{F, section, offset, body}
830 case "BODYSTRUCTURE":
832 return FetchBodystructure{F, p.xbodystructure(true)}
836 nums := p.xsectionBinary()
839 buf := p.xnilStringLiteral8()
840 return FetchBinary{F, nums, string(buf)}
844 nums := p.xsectionBinary()
848 return FetchBinarySize{F, nums, size}
852 return FetchUID(p.xuint32())
860 return FetchModSeq(modseq)
866 if p.peek('n') || p.peek('N') {
872 return FetchPreview{preview}
874 p.xerrorf("unknown fetch attribute %q", f)
878func (p *Proto) xnilString() string {
881 } else if p.peek('{') {
882 return string(p.xliteral())
889func ptr[T any](v T) *T {
893func (p *Proto) xnilptrString() *string {
895 return ptr(p.xquoted())
896 } else if p.peek('{') {
897 return ptr(string(p.xliteral()))
904func (p *Proto) xstring() string {
908 return string(p.xliteral())
911func (p *Proto) xastring() string {
914 } else if p.peek('{') {
915 return string(p.xliteral())
920func (p *Proto) xatom() string {
923 b, err := p.readbyte()
924 p.xcheckf(err, "read byte for atom")
925 if b <= ' ' || strings.IndexByte("(){%*\"\\]", b) >= 0 {
928 p.xerrorf("expected atom")
937func (p *Proto) xquoted() string {
941 r, err := p.readrune()
942 p.xcheckf(err, "reading rune in quoted string")
944 r, err = p.readrune()
945 p.xcheckf(err, "reading escaped char in quoted string")
946 if r != '\\' && r != '"' {
947 p.xerrorf("quoted char not backslash or dquote: %c", r)
950 // todo: probably refuse some more chars. like \0 and all ctl and backspace.
956func (p *Proto) xliteral() []byte {
962 // todo: for some literals, read as tracedata
964 p.xerrorf("refusing to read more than 1MB: %d", size)
968 p.xerrorf("cannot parse literals without connection")
970 fmt.Fprintf(p.xbw, "+ ok\r\n")
973 buf := make([]byte, int(size))
974 defer p.xtraceread(mlog.LevelTracedata)()
975 _, err := io.ReadFull(p.br, buf)
976 p.xcheckf(err, "reading data for literal")
977 p.xtraceread(mlog.LevelTrace)
983func (p *Proto) xflag0(allowPerm bool) string {
987 if allowPerm && p.take('*') {
990 } else if p.take('$') {
997func (p *Proto) xflag() string {
998 return p.xflag0(false)
1001func (p *Proto) xflagPerm() string {
1002 return p.xflag0(true)
1005func (p *Proto) xsection() string {
1007 s := p.xtakeuntil(']')
1012func (p *Proto) xsectionBinary() []uint32 {
1019 nums = append(nums, p.xnzuint32())
1024func (p *Proto) xnilStringLiteral8() []byte {
1025 // todo: should make difference for literal8 and literal from string, which bytes are allowed
1026 if p.take('~') || p.peek('{') {
1029 return []byte(p.xnilString())
1033func (p *Proto) xbodystructure(extensibleForm bool) any {
1037 parts := []any{p.xbodystructure(extensibleForm)}
1039 parts = append(parts, p.xbodystructure(extensibleForm))
1042 mediaSubtype := p.xstring()
1043 var ext *BodyExtensionMpart
1044 if extensibleForm && p.space() {
1045 ext = p.xbodyExtMpart()
1048 return BodyTypeMpart{parts, mediaSubtype, ext}
1051 // todo: verify the media(sub)type is valid for returned data.
1053 var ext *BodyExtension1Part
1054 mediaType := p.xstring()
1056 mediaSubtype := p.xstring()
1058 bodyFields := p.xbodyFields()
1060 // Basic type without extension.
1062 return BodyTypeBasic{mediaType, mediaSubtype, bodyFields, nil}
1066 envelope := p.xenvelope()
1068 bodyStructure := p.xbodystructure(extensibleForm)
1071 if extensibleForm && p.space() {
1072 ext = p.xbodyExt1Part()
1075 return BodyTypeMsg{mediaType, mediaSubtype, bodyFields, envelope, bodyStructure, lines, ext}
1077 if !strings.EqualFold(mediaType, "text") {
1078 if !extensibleForm {
1079 p.xerrorf("body result, basic type, with disallowed extensible form")
1081 ext = p.xbodyExt1Part()
1084 return BodyTypeBasic{mediaType, mediaSubtype, bodyFields, ext}
1088 if extensibleForm && p.space() {
1089 ext = p.xbodyExt1Part()
1092 return BodyTypeText{mediaType, mediaSubtype, bodyFields, lines, ext}
1096func (p *Proto) xbodyFields() BodyFields {
1097 params := p.xbodyFldParam()
1099 contentID := p.xnilString()
1101 contentDescr := p.xnilString()
1103 cte := p.xnilString()
1105 octets := p.xint32()
1106 return BodyFields{params, contentID, contentDescr, cte, octets}
1110func (p *Proto) xbodyExtMpart() (ext *BodyExtensionMpart) {
1111 ext = &BodyExtensionMpart{}
1112 ext.Params = p.xbodyFldParam()
1116 disp, dispParams := p.xbodyFldDsp()
1117 ext.Disposition, ext.DispositionParams = &disp, &dispParams
1121 ext.Language = ptr(p.xbodyFldLang())
1125 ext.Location = ptr(p.xbodyFldLoc())
1127 ext.More = append(ext.More, p.xbodyExtension())
1133func (p *Proto) xbodyExt1Part() (ext *BodyExtension1Part) {
1134 ext = &BodyExtension1Part{}
1135 ext.MD5 = p.xnilptrString()
1139 disp, dispParams := p.xbodyFldDsp()
1140 ext.Disposition, ext.DispositionParams = &disp, &dispParams
1144 ext.Language = ptr(p.xbodyFldLang())
1148 ext.Location = ptr(p.xbodyFldLoc())
1150 ext.More = append(ext.More, p.xbodyExtension())
1156func (p *Proto) xbodyFldParam() [][2]string {
1161 l := [][2]string{{k, v}}
1166 l = append(l, [2]string{k, v})
1176func (p *Proto) xbodyFldDsp() (*string, [][2]string) {
1181 disposition := p.xstring()
1183 param := p.xbodyFldParam()
1185 return ptr(disposition), param
1189func (p *Proto) xbodyFldLang() (lang []string) {
1191 lang = []string{p.xstring()}
1193 lang = append(lang, p.xstring())
1199 return []string{p.xstring()}
1206func (p *Proto) xbodyFldLoc() *string {
1207 return p.xnilptrString()
1211func (p *Proto) xbodyExtension() (ext BodyExtension) {
1214 ext.More = append(ext.More, p.xbodyExtension())
1220 } else if p.peekdigit() {
1223 } else if p.peekstring() {
1233func (p *Proto) xenvelope() Envelope {
1235 date := p.xnilString()
1237 subject := p.xnilString()
1239 from := p.xaddresses()
1241 sender := p.xaddresses()
1243 replyTo := p.xaddresses()
1245 to := p.xaddresses()
1247 cc := p.xaddresses()
1249 bcc := p.xaddresses()
1251 inReplyTo := p.xnilString()
1253 messageID := p.xnilString()
1255 return Envelope{date, subject, from, sender, replyTo, to, cc, bcc, inReplyTo, messageID}
1259func (p *Proto) xaddresses() []Address {
1264 l := []Address{p.xaddress()}
1266 l = append(l, p.xaddress())
1272func (p *Proto) xaddress() Address {
1274 name := p.xnilString()
1276 adl := p.xnilString()
1278 mailbox := p.xnilString()
1280 host := p.xnilString()
1282 return Address{name, adl, mailbox, host}
1286func (p *Proto) xflagList() []string {
1290 l = []string{p.xflag()}
1292 l = append(l, p.xflag())
1300func (p *Proto) xmailboxList() UntaggedList {
1304 flags = append(flags, p.xflag())
1306 flags = append(flags, p.xflag())
1314 quoted = p.xquoted()
1315 if len(quoted) != 1 {
1316 p.xerrorf("mailbox-list has multichar quoted part: %q", quoted)
1319 } else if !p.peek(' ') {
1323 mailbox := p.xastring()
1324 ul := UntaggedList{flags, b, mailbox, nil, ""}
1328 p.xmboxListExtendedItem(&ul)
1330 p.xmboxListExtendedItem(&ul)
1339func (p *Proto) xmboxListExtendedItem(ul *UntaggedList) {
1342 if strings.ToUpper(tag) == "OLDNAME" {
1345 name := p.xastring()
1350 val := p.xtaggedExtVal()
1351 ul.Extended = append(ul.Extended, MboxListExtendedItem{tag, val})
1355func (p *Proto) xtaggedExtVal() TaggedExtVal {
1359 comp := p.xtaggedExtComp()
1365 // 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.
1366 b, err := p.readbyte()
1367 p.xcheckf(err, "read byte for tagged-ext-val")
1368 if b < '0' || b > '9' {
1370 ss := p.xsequenceSet()
1371 return TaggedExtVal{SeqSet: &ss}
1374 num, err := strconv.ParseInt(s, 10, 63)
1375 p.xcheckf(err, "parsing int")
1376 if !p.peek(':') && !p.peek(',') {
1377 // not a larger sequence-set
1378 return TaggedExtVal{Number: &num}
1381 sr.First = uint32(num)
1389 ss := p.xsequenceSet()
1390 ss.Ranges = append([]NumRange{sr}, ss.Ranges...)
1391 return TaggedExtVal{SeqSet: &ss}
1395func (p *Proto) xsequenceSet() NumSet {
1397 return NumSet{SearchResult: true}
1403 sr.First = p.xnzuint32()
1412 ss.Ranges = append(ss.Ranges, sr)
1421func (p *Proto) xtaggedExtComp() TaggedExtComp {
1423 r := p.xtaggedExtComp()
1425 return TaggedExtComp{Comps: []TaggedExtComp{r}}
1429 return TaggedExtComp{String: s}
1431 l := []TaggedExtComp{{String: s}}
1433 l = append(l, p.xtaggedExtComp())
1435 return TaggedExtComp{Comps: l}
1439func (p *Proto) xnamespace() []NamespaceDescr {
1445 l := []NamespaceDescr{p.xnamespaceDescr()}
1447 l = append(l, p.xnamespaceDescr())
1453func (p *Proto) xnamespaceDescr() NamespaceDescr {
1455 prefix := p.xstring()
1461 p.xerrorf("namespace-descr: expected single char, got %q", s)
1467 var exts []NamespaceExtension
1473 values := []string{p.xstring()}
1475 values = append(values, p.xstring())
1478 exts = append(exts, NamespaceExtension{key, values})
1480 return NamespaceDescr{prefix, b, exts}
1484// Already consumed: "ESEARCH"
1485func (p *Proto) xesearchResponse() (r UntaggedEsearch) {
1492 seen := map[string]bool{}
1495 if p.peek('t') || p.peek('T') {
1499 r.Tag = p.xastring()
1500 } else if p.peek('m') || p.peek('M') {
1504 r.Mailbox = p.xastring()
1505 if r.Mailbox == "" {
1506 p.xerrorf("invalid empty mailbox in search correlator")
1508 } else if p.peek('u') || p.peek('U') {
1509 kind = "UIDVALIDITY"
1512 r.UIDValidity = p.xnzuint32()
1514 p.xerrorf("expected tag/correlator, mailbox or uidvalidity")
1518 p.xerrorf("duplicate search correlator %q", kind)
1528 p.xerrorf("missing tag search correlator")
1530 if (r.Mailbox != "") != (r.UIDValidity != 0) {
1531 p.xerrorf("mailbox and uidvalidity correlators must both be absent or both be present")
1540 W := strings.ToUpper(w)
1547 W = strings.ToUpper(w)
1554 p.xerrorf("duplicate MIN in ESEARCH")
1557 num := p.xnzuint32()
1562 p.xerrorf("duplicate MAX in ESEARCH")
1565 num := p.xnzuint32()
1569 if !r.All.IsZero() {
1570 p.xerrorf("duplicate ALL in ESEARCH")
1573 ss := p.xsequenceSet()
1574 if ss.SearchResult {
1575 p.xerrorf("$ for last not valid in ESEARCH")
1581 p.xerrorf("duplicate COUNT in ESEARCH")
1590 r.ModSeq = p.xint64()
1594 for i, b := range []byte(w) {
1595 if !(b >= 'A' && b <= 'Z' || strings.IndexByte("-_.", b) >= 0 || i > 0 && strings.IndexByte("0123456789:", b) >= 0) {
1596 p.xerrorf("invalid tag %q", w)
1600 ext := EsearchDataExt{w, p.xtaggedExtVal()}
1601 r.Exts = append(r.Exts, ext)
1607 w = p.xnonspace() // todo: this is too loose
1608 W = strings.ToUpper(w)
1614func (p *Proto) xcharset() string {
1622func (p *Proto) xuidset() []NumRange {
1623 ranges := []NumRange{p.xuidrange()}
1625 ranges = append(ranges, p.xuidrange())
1630func (p *Proto) xuidrange() NumRange {
1631 uid := p.xnzuint32()
1637 return NumRange{uid, end}
1641func (p *Proto) xlsub() UntaggedLsub {
1646 if len(r.Flags) > 0 {
1649 r.Flags = append(r.Flags, p.xflag())
1659 // todo: check valid char
1660 p.xerrorf("invalid separator %q", s)
1662 r.Separator = byte(s[0])
1665 r.Mailbox = p.xastring()