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",
137func stringMap(l ...string) map[string]struct{} {
138 r := map[string]struct{}{}
139 for _, s := range l {
146func (c *Conn) xrespCode() (string, CodeArg) {
148 for !c.peek(' ') && !c.peek(']') {
149 w += string(rune(c.xbyte()))
151 W := strings.ToUpper(w)
153 if _, ok := knownCodes[W]; !ok {
157 for !c.peek(' ') && !c.peek(']') {
158 arg += string(rune(c.xbyte()))
160 args = append(args, arg)
162 return W, CodeOther{W, args}
168 var l []string // Must be nil initially.
171 l = []string{c.xcharset()}
173 l = append(l, c.xcharset())
177 codeArg = CodeList{W, l}
180 caps := []string{c.xatom()}
182 caps = append(caps, c.xatom())
184 c.CapAvailable = map[Capability]struct{}{}
185 for _, cap := range caps {
186 cap = strings.ToUpper(cap)
187 c.CapAvailable[Capability(cap)] = struct{}{}
189 codeArg = CodeWords{W, caps}
191 case "PERMANENTFLAGS":
192 l := []string{} // Must be non-nil.
195 l = []string{c.xflagPerm()}
197 l = append(l, c.xflagPerm())
201 codeArg = CodeList{W, l}
202 case "UIDNEXT", "UIDVALIDITY", "UNSEEN":
204 codeArg = CodeUint{W, c.xnzuint32()}
207 destUIDValidity := c.xnzuint32()
209 uids := c.xuidrange()
210 codeArg = CodeAppendUID{destUIDValidity, uids}
213 destUIDValidity := c.xnzuint32()
218 codeArg = CodeCopyUID{destUIDValidity, from, to}
219 case "HIGHESTMODSEQ":
221 codeArg = CodeHighestModSeq(c.xint64())
224 modified := c.xuidset()
225 codeArg = CodeModified(NumSet{Ranges: modified})
229 var current, goal *uint32
234 if c.peek('n') || c.peek('N') {
241 if c.peek('n') || c.peek('N') {
249 codeArg = CodeInProgress{tag, current, goal}
254func (c *Conn) xbyte() byte {
255 b, err := c.readbyte()
256 c.xcheckf(err, "read byte")
260// take until b is seen. don't take b itself.
261func (c *Conn) xtakeuntil(b byte) string {
264 x, err := c.readbyte()
265 c.xcheckf(err, "read byte")
274func (c *Conn) xdigits() string {
277 b, err := c.readbyte()
278 if err == nil && (b >= '0' && b <= '9') {
287func (c *Conn) peekdigit() bool {
288 if b, err := c.readbyte(); err == nil {
290 return b >= '0' && b <= '9'
295func (c *Conn) xint32() int32 {
297 num, err := strconv.ParseInt(s, 10, 32)
298 c.xcheckf(err, "parsing int32")
302func (c *Conn) xint64() int64 {
304 num, err := strconv.ParseInt(s, 10, 63)
305 c.xcheckf(err, "parsing int64")
309func (c *Conn) xuint32() uint32 {
311 num, err := strconv.ParseUint(s, 10, 32)
312 c.xcheckf(err, "parsing uint32")
316func (c *Conn) xnzuint32() uint32 {
319 c.xerrorf("got 0, expected nonzero uint")
324// todo: replace with proper parsing.
325func (c *Conn) xnonspace() string {
327 for !c.peek(' ') && !c.peek('\r') && !c.peek('\n') {
328 s += string(rune(c.xbyte()))
331 c.xerrorf("expected non-space")
336// todo: replace with proper parsing
337func (c *Conn) xword() string {
341// "*" SP is already consumed
343func (c *Conn) xuntagged() Untagged {
345 W := strings.ToUpper(w)
349 r := UntaggedPreauth(c.xrespText())
355 r := UntaggedBye(c.xrespText())
359 case "OK", "NO", "BAD":
361 r := UntaggedResult(c.xresult(Status(W)))
369 caps = append(caps, c.xnonspace())
371 c.CapAvailable = map[Capability]struct{}{}
372 for _, cap := range caps {
373 cap = strings.ToUpper(cap)
374 c.CapAvailable[Capability(cap)] = struct{}{}
376 r := UntaggedCapability(caps)
384 caps = append(caps, c.xnonspace())
386 for _, cap := range caps {
387 cap = strings.ToUpper(cap)
388 c.CapEnabled[Capability(cap)] = struct{}{}
390 r := UntaggedEnabled(caps)
396 r := UntaggedFlags(c.xflagList())
402 r := c.xmailboxList()
409 mailbox := c.xastring()
412 attrs := map[StatusAttr]int64{}
419 S := StatusAttr(strings.ToUpper(s))
424 num = int64(c.xuint32())
426 num = int64(c.xnzuint32())
428 num = int64(c.xnzuint32())
430 num = int64(c.xuint32())
432 num = int64(c.xuint32())
436 c.xneedDisabled("RECENT status flag", CapIMAP4rev2)
437 num = int64(c.xuint32())
439 if c.peek('n') || c.peek('N') {
444 case "HIGHESTMODSEQ":
446 case "DELETED-STORAGE":
449 c.xerrorf("status: unknown attribute %q", s)
451 if _, ok := attrs[S]; ok {
452 c.xerrorf("status: duplicate attribute %q", s)
456 r := UntaggedStatus{mailbox, attrs}
463 mailbox := c.xastring()
466 // Unsolicited form, with only annotation keys, not values.
470 keys = append(keys, key)
476 return UntaggedMetadataKeys{mailbox, keys}
479 // Form with values, in response to GETMETADATA command.
480 r := UntaggedMetadataAnnotations{Mailbox: mailbox}
488 } else if c.peek('"') {
489 value = []byte(c.xstring())
491 // note: the abnf also allows nstring, but that only makes sense when the
494 // For response to extended list.
497 r.Annotations = append(r.Annotations, Annotation{key, isString, value})
510 personal := c.xnamespace()
512 other := c.xnamespace()
514 shared := c.xnamespace()
515 r := UntaggedNamespace{personal, other, shared}
521 c.xneedDisabled("untagged SEARCH response", CapIMAP4rev2)
531 return UntaggedSearchModSeq{nums, modseq}
533 nums = append(nums, c.xnzuint32())
535 r := UntaggedSearch(nums)
540 r := c.xesearchResponse()
545 c.xneedDisabled("untagged LSUB response", CapIMAP4rev2)
553 var params map[string]string
555 params = map[string]string{}
563 if _, ok := params[k]; ok {
564 c.xerrorf("duplicate key %q", k)
572 return UntaggedID(params)
586 return UntaggedVanished{earlier, NumSet{Ranges: uids}}
595 roots = append(roots, root)
598 return UntaggedQuotaroot(roots)
607 xresource := func() QuotaResource {
613 return QuotaResource{QuotaResourceName(strings.ToUpper(name)), usage, limit}
616 seen := map[QuotaResourceName]bool{}
617 l := []QuotaResource{xresource()}
618 seen[l[0].Name] = true
622 c.xerrorf("duplicate resource name %q", res.Name)
624 seen[res.Name] = true
629 return UntaggedQuota{root, l}
632 v, err := strconv.ParseUint(w, 10, 32)
637 W = strings.ToUpper(w)
641 c.xerrorf("invalid zero number for untagged fetch response")
650 c.xerrorf("invalid zero number for untagged expunge response")
653 return UntaggedExpunge(num)
657 return UntaggedExists(num)
660 c.xneedDisabled("should not send RECENT in IMAP4rev2", CapIMAP4rev2)
662 return UntaggedRecent(num)
665 c.xerrorf("unknown untagged numbered response %q", w)
669 c.xerrorf("unknown untagged response %q", w)
675// Already parsed: "*" SP nznumber SP "FETCH" SP
676func (c *Conn) xfetch(num uint32) UntaggedFetch {
678 attrs := []FetchAttr{c.xmsgatt1()}
680 attrs = append(attrs, c.xmsgatt1())
683 return UntaggedFetch{num, attrs}
687func (c *Conn) xmsgatt1() FetchAttr {
691 if b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b >= '0' && b <= '9' || b == '.' {
699 F := strings.ToUpper(f)
706 flags = []string{c.xflag()}
708 flags = append(flags, c.xflag())
712 return FetchFlags(flags)
716 return FetchEnvelope(c.xenvelope())
721 v, err := time.Parse("_2-Jan-2006 15:04:05 -0700", s)
722 c.xcheckf(err, "parsing internaldate")
723 return FetchInternalDate{v}
730 v, err := time.Parse("_2-Jan-2006 15:04:05 -0700", s)
731 c.xcheckf(err, "parsing savedate")
736 return FetchSaveDate{t}
740 return FetchRFC822Size(c.xint64())
745 return FetchRFC822(s)
747 case "RFC822.HEADER":
750 return FetchRFC822Header(s)
755 return FetchRFC822Text(s)
759 return FetchBodystructure{F, c.xbodystructure(false)}
762 section := c.xsection()
770 body := c.xnilString()
771 return FetchBody{F, section, offset, body}
773 case "BODYSTRUCTURE":
775 return FetchBodystructure{F, c.xbodystructure(true)}
779 nums := c.xsectionBinary()
782 buf := c.xnilStringLiteral8()
783 return FetchBinary{F, nums, string(buf)}
787 nums := c.xsectionBinary()
791 return FetchBinarySize{F, nums, size}
795 return FetchUID(c.xuint32())
803 return FetchModSeq(modseq)
809 if c.peek('n') || c.peek('N') {
815 return FetchPreview{preview}
817 c.xerrorf("unknown fetch attribute %q", f)
821func (c *Conn) xnilString() string {
824 } else if c.peek('{') {
825 return string(c.xliteral())
832func (c *Conn) xstring() string {
836 return string(c.xliteral())
839func (c *Conn) xastring() string {
842 } else if c.peek('{') {
843 return string(c.xliteral())
848func (c *Conn) xatom() string {
851 b, err := c.readbyte()
852 c.xcheckf(err, "read byte for atom")
853 if b <= ' ' || strings.IndexByte("(){%*\"\\]", b) >= 0 {
854 err := c.br.UnreadByte()
855 c.xcheckf(err, "unreadbyte")
857 c.xerrorf("expected atom")
866func (c *Conn) xquoted() string {
870 r, err := c.readrune()
871 c.xcheckf(err, "reading rune in quoted string")
873 r, err = c.readrune()
874 c.xcheckf(err, "reading escaped char in quoted string")
875 if r != '\\' && r != '"' {
876 c.xerrorf("quoted char not backslash or dquote: %c", r)
879 // todo: probably refuse some more chars. like \0 and all ctl and backspace.
885func (c *Conn) xliteral() []byte {
891 // todo: for some literals, read as tracedata
893 c.xerrorf("refusing to read more than 1MB: %d", size)
896 fmt.Fprintf(c.xbw, "+ ok\r\n")
899 buf := make([]byte, int(size))
900 _, err := io.ReadFull(c.br, buf)
901 c.xcheckf(err, "reading data for literal")
907func (c *Conn) xflag0(allowPerm bool) string {
911 if allowPerm && c.take('*') {
914 } else if c.take('$') {
921func (c *Conn) xflag() string {
922 return c.xflag0(false)
925func (c *Conn) xflagPerm() string {
926 return c.xflag0(true)
929func (c *Conn) xsection() string {
931 s := c.xtakeuntil(']')
936func (c *Conn) xsectionBinary() []uint32 {
943 nums = append(nums, c.xnzuint32())
948func (c *Conn) xnilStringLiteral8() []byte {
949 // todo: should make difference for literal8 and literal from string, which bytes are allowed
950 if c.take('~') || c.peek('{') {
953 return []byte(c.xnilString())
957func (c *Conn) xbodystructure(extensibleForm bool) any {
961 parts := []any{c.xbodystructure(extensibleForm)}
963 parts = append(parts, c.xbodystructure(extensibleForm))
966 mediaSubtype := c.xstring()
967 var ext *BodyExtensionMpart
968 if extensibleForm && c.space() {
969 ext = c.xbodyExtMpart()
972 return BodyTypeMpart{parts, mediaSubtype, ext}
975 // todo: verify the media(sub)type is valid for returned data.
977 var ext *BodyExtension1Part
978 mediaType := c.xstring()
980 mediaSubtype := c.xstring()
982 bodyFields := c.xbodyFields()
984 // Basic type without extension.
986 return BodyTypeBasic{mediaType, mediaSubtype, bodyFields, nil}
990 envelope := c.xenvelope()
992 bodyStructure := c.xbodystructure(extensibleForm)
995 if extensibleForm && c.space() {
996 ext = c.xbodyExt1Part()
999 return BodyTypeMsg{mediaType, mediaSubtype, bodyFields, envelope, bodyStructure, lines, ext}
1001 if !strings.EqualFold(mediaType, "text") {
1002 if !extensibleForm {
1003 c.xerrorf("body result, basic type, with disallowed extensible form")
1005 ext = c.xbodyExt1Part()
1008 return BodyTypeBasic{mediaType, mediaSubtype, bodyFields, ext}
1012 if extensibleForm && c.space() {
1013 ext = c.xbodyExt1Part()
1016 return BodyTypeText{mediaType, mediaSubtype, bodyFields, lines, ext}
1020func (c *Conn) xbodyFields() BodyFields {
1021 params := c.xbodyFldParam()
1023 contentID := c.xnilString()
1025 contentDescr := c.xnilString()
1027 cte := c.xnilString()
1029 octets := c.xint32()
1030 return BodyFields{params, contentID, contentDescr, cte, octets}
1034func (c *Conn) xbodyExtMpart() (ext *BodyExtensionMpart) {
1035 ext = &BodyExtensionMpart{}
1036 ext.Params = c.xbodyFldParam()
1040 ext.Disposition, ext.DispositionParams = c.xbodyFldDsp()
1044 ext.Language = c.xbodyFldLang()
1048 ext.Location = c.xbodyFldLoc()
1050 ext.More = append(ext.More, c.xbodyExtension())
1056func (c *Conn) xbodyExt1Part() (ext *BodyExtension1Part) {
1057 ext = &BodyExtension1Part{}
1058 ext.MD5 = c.xnilString()
1062 ext.Disposition, ext.DispositionParams = c.xbodyFldDsp()
1066 ext.Language = c.xbodyFldLang()
1070 ext.Location = c.xbodyFldLoc()
1072 ext.More = append(ext.More, c.xbodyExtension())
1078func (c *Conn) xbodyFldParam() [][2]string {
1083 l := [][2]string{{k, v}}
1088 l = append(l, [2]string{k, v})
1098func (c *Conn) xbodyFldDsp() (string, [][2]string) {
1103 disposition := c.xstring()
1105 param := c.xbodyFldParam()
1107 return disposition, param
1111func (c *Conn) xbodyFldLang() (lang []string) {
1113 lang = []string{c.xstring()}
1115 lang = append(lang, c.xstring())
1121 return []string{c.xstring()}
1128func (c *Conn) xbodyFldLoc() string {
1129 return c.xnilString()
1133func (c *Conn) xbodyExtension() (ext BodyExtension) {
1136 ext.More = append(ext.More, c.xbodyExtension())
1142 } else if c.peekdigit() {
1145 } else if c.peekstring() {
1155func (c *Conn) xenvelope() Envelope {
1157 date := c.xnilString()
1159 subject := c.xnilString()
1161 from := c.xaddresses()
1163 sender := c.xaddresses()
1165 replyTo := c.xaddresses()
1167 to := c.xaddresses()
1169 cc := c.xaddresses()
1171 bcc := c.xaddresses()
1173 inReplyTo := c.xnilString()
1175 messageID := c.xnilString()
1177 return Envelope{date, subject, from, sender, replyTo, to, cc, bcc, inReplyTo, messageID}
1181func (c *Conn) xaddresses() []Address {
1186 l := []Address{c.xaddress()}
1188 l = append(l, c.xaddress())
1194func (c *Conn) xaddress() Address {
1196 name := c.xnilString()
1198 adl := c.xnilString()
1200 mailbox := c.xnilString()
1202 host := c.xnilString()
1204 return Address{name, adl, mailbox, host}
1208func (c *Conn) xflagList() []string {
1212 l = []string{c.xflag()}
1214 l = append(l, c.xflag())
1222func (c *Conn) xmailboxList() UntaggedList {
1226 flags = append(flags, c.xflag())
1228 flags = append(flags, c.xflag())
1236 quoted = c.xquoted()
1237 if len(quoted) != 1 {
1238 c.xerrorf("mailbox-list has multichar quoted part: %q", quoted)
1241 } else if !c.peek(' ') {
1245 mailbox := c.xastring()
1246 ul := UntaggedList{flags, b, mailbox, nil, ""}
1250 c.xmboxListExtendedItem(&ul)
1252 c.xmboxListExtendedItem(&ul)
1261func (c *Conn) xmboxListExtendedItem(ul *UntaggedList) {
1264 if strings.ToUpper(tag) == "OLDNAME" {
1267 name := c.xastring()
1272 val := c.xtaggedExtVal()
1273 ul.Extended = append(ul.Extended, MboxListExtendedItem{tag, val})
1277func (c *Conn) xtaggedExtVal() TaggedExtVal {
1281 comp := c.xtaggedExtComp()
1287 // 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.
1288 b, err := c.readbyte()
1289 c.xcheckf(err, "read byte for tagged-ext-val")
1290 if b < '0' || b > '9' {
1292 ss := c.xsequenceSet()
1293 return TaggedExtVal{SeqSet: &ss}
1296 num, err := strconv.ParseInt(s, 10, 63)
1297 c.xcheckf(err, "parsing int")
1298 if !c.peek(':') && !c.peek(',') {
1299 // not a larger sequence-set
1300 return TaggedExtVal{Number: &num}
1303 sr.First = uint32(num)
1311 ss := c.xsequenceSet()
1312 ss.Ranges = append([]NumRange{sr}, ss.Ranges...)
1313 return TaggedExtVal{SeqSet: &ss}
1317func (c *Conn) xsequenceSet() NumSet {
1319 return NumSet{SearchResult: true}
1325 sr.First = c.xnzuint32()
1334 ss.Ranges = append(ss.Ranges, sr)
1343func (c *Conn) xtaggedExtComp() TaggedExtComp {
1345 r := c.xtaggedExtComp()
1347 return TaggedExtComp{Comps: []TaggedExtComp{r}}
1351 return TaggedExtComp{String: s}
1353 l := []TaggedExtComp{{String: s}}
1355 l = append(l, c.xtaggedExtComp())
1357 return TaggedExtComp{Comps: l}
1361func (c *Conn) xnamespace() []NamespaceDescr {
1367 l := []NamespaceDescr{c.xnamespaceDescr()}
1369 l = append(l, c.xnamespaceDescr())
1375func (c *Conn) xnamespaceDescr() NamespaceDescr {
1377 prefix := c.xstring()
1383 c.xerrorf("namespace-descr: expected single char, got %q", s)
1389 var exts []NamespaceExtension
1395 values := []string{c.xstring()}
1397 values = append(values, c.xstring())
1400 exts = append(exts, NamespaceExtension{key, values})
1402 return NamespaceDescr{prefix, b, exts}
1405// require all of caps to be disabled.
1406func (c *Conn) xneedDisabled(msg string, caps ...Capability) {
1407 for _, cap := range caps {
1408 if _, ok := c.CapEnabled[cap]; ok {
1409 c.xerrorf("%s: invalid because of enabled capability %q", msg, cap)
1415// Already consumed: "ESEARCH"
1416func (c *Conn) xesearchResponse() (r UntaggedEsearch) {
1423 seen := map[string]bool{}
1426 if c.peek('t') || c.peek('T') {
1430 r.Tag = c.xastring()
1431 } else if c.peek('m') || c.peek('M') {
1435 r.Mailbox = c.xastring()
1436 if r.Mailbox == "" {
1437 c.xerrorf("invalid empty mailbox in search correlator")
1439 } else if c.peek('u') || c.peek('U') {
1440 kind = "UIDVALIDITY"
1443 r.UIDValidity = c.xnzuint32()
1445 c.xerrorf("expected tag/correlator, mailbox or uidvalidity")
1449 c.xerrorf("duplicate search correlator %q", kind)
1459 c.xerrorf("missing tag search correlator")
1461 if (r.Mailbox != "") != (r.UIDValidity != 0) {
1462 c.xerrorf("mailbox and uidvalidity correlators must both be absent or both be present")
1471 W := strings.ToUpper(w)
1478 W = strings.ToUpper(w)
1485 c.xerrorf("duplicate MIN in ESEARCH")
1488 num := c.xnzuint32()
1493 c.xerrorf("duplicate MAX in ESEARCH")
1496 num := c.xnzuint32()
1500 if !r.All.IsZero() {
1501 c.xerrorf("duplicate ALL in ESEARCH")
1504 ss := c.xsequenceSet()
1505 if ss.SearchResult {
1506 c.xerrorf("$ for last not valid in ESEARCH")
1512 c.xerrorf("duplicate COUNT in ESEARCH")
1521 r.ModSeq = c.xint64()
1525 for i, b := range []byte(w) {
1526 if !(b >= 'A' && b <= 'Z' || strings.IndexByte("-_.", b) >= 0 || i > 0 && strings.IndexByte("0123456789:", b) >= 0) {
1527 c.xerrorf("invalid tag %q", w)
1531 ext := EsearchDataExt{w, c.xtaggedExtVal()}
1532 r.Exts = append(r.Exts, ext)
1538 w = c.xnonspace() // todo: this is too loose
1539 W = strings.ToUpper(w)
1545func (c *Conn) xcharset() string {
1553func (c *Conn) xuidset() []NumRange {
1554 ranges := []NumRange{c.xuidrange()}
1556 ranges = append(ranges, c.xuidrange())
1561func (c *Conn) xuidrange() NumRange {
1562 uid := c.xnzuint32()
1568 return NumRange{uid, end}
1572func (c *Conn) xlsub() UntaggedLsub {
1577 if len(r.Flags) > 0 {
1580 r.Flags = append(r.Flags, c.xflag())
1590 // todo: check valid char
1591 c.xerrorf("invalid separator %q", s)
1593 r.Separator = byte(s[0])
1596 r.Mailbox = c.xastring()