1package imapclient
2
3import (
4 "fmt"
5 "io"
6 "strconv"
7 "strings"
8)
9
10func (c *Conn) recorded() string {
11 s := string(c.recordBuf)
12 c.recordBuf = nil
13 c.record = false
14 return s
15}
16
17func (c *Conn) recordAdd(buf []byte) {
18 if c.record {
19 c.recordBuf = append(c.recordBuf, buf...)
20 }
21}
22
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)
29 }
30 c.recordAdd(buf)
31}
32
33func (c *Conn) readbyte() (byte, error) {
34 b, err := c.r.ReadByte()
35 if err == nil {
36 c.recordAdd([]byte{b})
37 }
38 return b, err
39}
40
41func (c *Conn) unreadbyte() {
42 if c.record {
43 c.recordBuf = c.recordBuf[:len(c.recordBuf)-1]
44 }
45 err := c.r.UnreadByte()
46 c.xcheckf(err, "unread byte")
47}
48
49func (c *Conn) readrune() (rune, error) {
50 x, _, err := c.r.ReadRune()
51 if err == nil {
52 c.recordAdd([]byte(string(x)))
53 }
54 return x, err
55}
56
57func (c *Conn) xspace() {
58 c.xtake(" ")
59}
60
61func (c *Conn) xcrlf() {
62 c.xtake("\r\n")
63}
64
65func (c *Conn) peek(exp byte) bool {
66 b, err := c.readbyte()
67 if err == nil {
68 c.unreadbyte()
69 }
70 return err == nil && strings.EqualFold(string(rune(b)), string(rune(exp)))
71}
72
73func (c *Conn) take(exp byte) bool {
74 if c.peek(exp) {
75 _, _ = c.readbyte()
76 return true
77 }
78 return false
79}
80
81func (c *Conn) xstatus() Status {
82 w := c.xword()
83 W := strings.ToUpper(w)
84 switch W {
85 case "OK":
86 return OK
87 case "NO":
88 return NO
89 case "BAD":
90 return BAD
91 }
92 c.xerrorf("expected status, got %q", w)
93 panic("not reached")
94}
95
96// Already consumed: tag SP status SP
97func (c *Conn) xresult(status Status) Result {
98 respText := c.xrespText()
99 return Result{status, respText}
100}
101
102func (c *Conn) xrespText() RespText {
103 var code string
104 var codeArg CodeArg
105 if c.take('[') {
106 code, codeArg = c.xrespCode()
107 c.xtake("]")
108 c.xspace()
109 }
110 more := ""
111 for !c.peek('\r') {
112 more += string(rune(c.xbyte()))
113 }
114 return RespText{code, codeArg, more}
115}
116
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",
120 "OVERQUOTA", // ../rfc/9208:472
121 // With parameters.
122 "BADCHARSET", "CAPABILITY", "PERMANENTFLAGS", "UIDNEXT", "UIDVALIDITY", "UNSEEN", "APPENDUID", "COPYUID",
123 "HIGHESTMODSEQ", "MODIFIED",
124)
125
126func stringMap(l ...string) map[string]struct{} {
127 r := map[string]struct{}{}
128 for _, s := range l {
129 r[s] = struct{}{}
130 }
131 return r
132}
133
134// ../rfc/9051:6895
135func (c *Conn) xrespCode() (string, CodeArg) {
136 w := ""
137 for !c.peek(' ') && !c.peek(']') {
138 w += string(rune(c.xbyte()))
139 }
140 W := strings.ToUpper(w)
141
142 if _, ok := knownCodes[W]; !ok {
143 var args []string
144 for c.take(' ') {
145 arg := ""
146 for !c.peek(' ') && !c.peek(']') {
147 arg += string(rune(c.xbyte()))
148 }
149 args = append(args, arg)
150 }
151 return W, CodeOther{W, args}
152 }
153
154 var codeArg CodeArg
155 switch W {
156 case "BADCHARSET":
157 var l []string // Must be nil initially.
158 if c.take(' ') {
159 c.xtake("(")
160 l = []string{c.xcharset()}
161 for c.take(' ') {
162 l = append(l, c.xcharset())
163 }
164 c.xtake(")")
165 }
166 codeArg = CodeList{W, l}
167 case "CAPABILITY":
168 c.xtake(" ")
169 caps := []string{c.xatom()}
170 for c.take(' ') {
171 caps = append(caps, c.xatom())
172 }
173 c.CapAvailable = map[Capability]struct{}{}
174 for _, cap := range caps {
175 c.CapAvailable[Capability(cap)] = struct{}{}
176 }
177 codeArg = CodeWords{W, caps}
178
179 case "PERMANENTFLAGS":
180 l := []string{} // Must be non-nil.
181 if c.take(' ') {
182 c.xtake("(")
183 l = []string{c.xflagPerm()}
184 for c.take(' ') {
185 l = append(l, c.xflagPerm())
186 }
187 c.xtake(")")
188 }
189 codeArg = CodeList{W, l}
190 case "UIDNEXT", "UIDVALIDITY", "UNSEEN":
191 c.xspace()
192 codeArg = CodeUint{W, c.xnzuint32()}
193 case "APPENDUID":
194 c.xspace()
195 destUIDValidity := c.xnzuint32()
196 c.xspace()
197 uid := c.xnzuint32()
198 codeArg = CodeAppendUID{destUIDValidity, uid}
199 case "COPYUID":
200 c.xspace()
201 destUIDValidity := c.xnzuint32()
202 c.xspace()
203 from := c.xuidset()
204 c.xspace()
205 to := c.xuidset()
206 codeArg = CodeCopyUID{destUIDValidity, from, to}
207 case "HIGHESTMODSEQ":
208 c.xspace()
209 codeArg = CodeHighestModSeq(c.xint64())
210 case "MODIFIED":
211 c.xspace()
212 modified := c.xuidset()
213 codeArg = CodeModified(NumSet{Ranges: modified})
214 }
215 return W, codeArg
216}
217
218func (c *Conn) xbyte() byte {
219 b, err := c.readbyte()
220 c.xcheckf(err, "read byte")
221 return b
222}
223
224// take until b is seen. don't take b itself.
225func (c *Conn) xtakeuntil(b byte) string {
226 var s string
227 for {
228 x, err := c.readbyte()
229 c.xcheckf(err, "read byte")
230 if x == b {
231 c.unreadbyte()
232 return s
233 }
234 s += string(rune(x))
235 }
236}
237
238func (c *Conn) xdigits() string {
239 var s string
240 for {
241 b, err := c.readbyte()
242 if err == nil && (b >= '0' && b <= '9') {
243 s += string(rune(b))
244 continue
245 }
246 c.unreadbyte()
247 return s
248 }
249}
250
251func (c *Conn) xint32() int32 {
252 s := c.xdigits()
253 num, err := strconv.ParseInt(s, 10, 32)
254 c.xcheckf(err, "parsing int32")
255 return int32(num)
256}
257
258func (c *Conn) xint64() int64 {
259 s := c.xdigits()
260 num, err := strconv.ParseInt(s, 10, 63)
261 c.xcheckf(err, "parsing int64")
262 return num
263}
264
265func (c *Conn) xuint32() uint32 {
266 s := c.xdigits()
267 num, err := strconv.ParseUint(s, 10, 32)
268 c.xcheckf(err, "parsing uint32")
269 return uint32(num)
270}
271
272func (c *Conn) xnzuint32() uint32 {
273 v := c.xuint32()
274 if v == 0 {
275 c.xerrorf("got 0, expected nonzero uint")
276 }
277 return v
278}
279
280// todo: replace with proper parsing.
281func (c *Conn) xnonspace() string {
282 var s string
283 for !c.peek(' ') && !c.peek('\r') && !c.peek('\n') {
284 s += string(rune(c.xbyte()))
285 }
286 if s == "" {
287 c.xerrorf("expected non-space")
288 }
289 return s
290}
291
292// todo: replace with proper parsing
293func (c *Conn) xword() string {
294 return c.xatom()
295}
296
297// "*" SP is already consumed
298// ../rfc/9051:6868
299func (c *Conn) xuntagged() Untagged {
300 w := c.xnonspace()
301 W := strings.ToUpper(w)
302 switch W {
303 case "PREAUTH":
304 c.xspace()
305 r := UntaggedPreauth(c.xrespText())
306 c.xcrlf()
307 return r
308
309 case "BYE":
310 c.xspace()
311 r := UntaggedBye(c.xrespText())
312 c.xcrlf()
313 return r
314
315 case "OK", "NO", "BAD":
316 c.xspace()
317 r := UntaggedResult(c.xresult(Status(W)))
318 c.xcrlf()
319 return r
320
321 case "CAPABILITY":
322 // ../rfc/9051:6427
323 var caps []string
324 for c.take(' ') {
325 caps = append(caps, c.xnonspace())
326 }
327 c.CapAvailable = map[Capability]struct{}{}
328 for _, cap := range caps {
329 c.CapAvailable[Capability(cap)] = struct{}{}
330 }
331 r := UntaggedCapability(caps)
332 c.xcrlf()
333 return r
334
335 case "ENABLED":
336 // ../rfc/9051:6520
337 var caps []string
338 for c.take(' ') {
339 caps = append(caps, c.xnonspace())
340 }
341 for _, cap := range caps {
342 c.CapEnabled[Capability(cap)] = struct{}{}
343 }
344 r := UntaggedEnabled(caps)
345 c.xcrlf()
346 return r
347
348 case "FLAGS":
349 c.xspace()
350 r := UntaggedFlags(c.xflagList())
351 c.xcrlf()
352 return r
353
354 case "LIST":
355 c.xspace()
356 r := c.xmailboxList()
357 c.xcrlf()
358 return r
359
360 case "STATUS":
361 // ../rfc/9051:6681
362 c.xspace()
363 mailbox := c.xastring()
364 c.xspace()
365 c.xtake("(")
366 attrs := map[StatusAttr]int64{}
367 for !c.take(')') {
368 if len(attrs) > 0 {
369 c.xspace()
370 }
371 s := c.xatom()
372 c.xspace()
373 S := StatusAttr(strings.ToUpper(s))
374 var num int64
375 // ../rfc/9051:7059
376 switch S {
377 case "MESSAGES":
378 num = int64(c.xuint32())
379 case "UIDNEXT":
380 num = int64(c.xnzuint32())
381 case "UIDVALIDITY":
382 num = int64(c.xnzuint32())
383 case "UNSEEN":
384 num = int64(c.xuint32())
385 case "DELETED":
386 num = int64(c.xuint32())
387 case "SIZE":
388 num = c.xint64()
389 case "RECENT":
390 c.xneedDisabled("RECENT status flag", CapIMAP4rev2)
391 num = int64(c.xuint32())
392 case "APPENDLIMIT":
393 if c.peek('n') || c.peek('N') {
394 c.xtake("nil")
395 } else {
396 num = c.xint64()
397 }
398 case "HIGHESTMODSEQ":
399 num = c.xint64()
400 case "DELETED-STORAGE":
401 num = c.xint64()
402 default:
403 c.xerrorf("status: unknown attribute %q", s)
404 }
405 if _, ok := attrs[S]; ok {
406 c.xerrorf("status: duplicate attribute %q", s)
407 }
408 attrs[S] = num
409 }
410 r := UntaggedStatus{mailbox, attrs}
411 c.xcrlf()
412 return r
413
414 case "NAMESPACE":
415 // ../rfc/9051:6778
416 c.xspace()
417 personal := c.xnamespace()
418 c.xspace()
419 other := c.xnamespace()
420 c.xspace()
421 shared := c.xnamespace()
422 r := UntaggedNamespace{personal, other, shared}
423 c.xcrlf()
424 return r
425
426 case "SEARCH":
427 // ../rfc/9051:6809
428 c.xneedDisabled("untagged SEARCH response", CapIMAP4rev2)
429 var nums []uint32
430 for c.take(' ') {
431 // ../rfc/7162:2557
432 if c.take('(') {
433 c.xtake("MODSEQ")
434 c.xspace()
435 modseq := c.xint64()
436 c.xtake(")")
437 c.xcrlf()
438 return UntaggedSearchModSeq{nums, modseq}
439 }
440 nums = append(nums, c.xnzuint32())
441 }
442 r := UntaggedSearch(nums)
443 c.xcrlf()
444 return r
445
446 case "ESEARCH":
447 r := c.xesearchResponse()
448 c.xcrlf()
449 return r
450
451 case "LSUB":
452 c.xneedDisabled("untagged LSUB response", CapIMAP4rev2)
453 r := c.xlsub()
454 c.xcrlf()
455 return r
456
457 case "ID":
458 // ../rfc/2971:243
459 c.xspace()
460 var params map[string]string
461 if c.take('(') {
462 params = map[string]string{}
463 for !c.take(')') {
464 if len(params) > 0 {
465 c.xspace()
466 }
467 k := c.xstring()
468 c.xspace()
469 v := c.xnilString()
470 if _, ok := params[k]; ok {
471 c.xerrorf("duplicate key %q", k)
472 }
473 params[k] = v
474 }
475 } else {
476 c.xtake("NIL")
477 }
478 c.xcrlf()
479 return UntaggedID(params)
480
481 // ../rfc/7162:2623
482 case "VANISHED":
483 c.xspace()
484 var earlier bool
485 if c.take('(') {
486 c.xtake("EARLIER")
487 c.xtake(")")
488 c.xspace()
489 earlier = true
490 }
491 uids := c.xuidset()
492 c.xcrlf()
493 return UntaggedVanished{earlier, NumSet{Ranges: uids}}
494
495 // ../rfc/9208:668 ../2087:242
496 case "QUOTAROOT":
497 c.xspace()
498 c.xastring()
499 var roots []string
500 for c.take(' ') {
501 root := c.xastring()
502 roots = append(roots, root)
503 }
504 c.xcrlf()
505 return UntaggedQuotaroot(roots)
506
507 // ../rfc/9208:666 ../rfc/2087:239
508 case "QUOTA":
509 c.xspace()
510 root := c.xastring()
511 c.xspace()
512 c.xtake("(")
513
514 xresource := func() QuotaResource {
515 name := c.xatom()
516 c.xspace()
517 usage := c.xint64()
518 c.xspace()
519 limit := c.xint64()
520 return QuotaResource{QuotaResourceName(strings.ToUpper(name)), usage, limit}
521 }
522
523 seen := map[QuotaResourceName]bool{}
524 l := []QuotaResource{xresource()}
525 seen[l[0].Name] = true
526 for c.take(' ') {
527 res := xresource()
528 if seen[res.Name] {
529 c.xerrorf("duplicate resource name %q", res.Name)
530 }
531 seen[res.Name] = true
532 l = append(l, res)
533 }
534 c.xtake(")")
535 c.xcrlf()
536 return UntaggedQuota{root, l}
537
538 default:
539 v, err := strconv.ParseUint(w, 10, 32)
540 if err == nil {
541 num := uint32(v)
542 c.xspace()
543 w = c.xword()
544 W = strings.ToUpper(w)
545 switch W {
546 case "FETCH":
547 if num == 0 {
548 c.xerrorf("invalid zero number for untagged fetch response")
549 }
550 c.xspace()
551 r := c.xfetch(num)
552 c.xcrlf()
553 return r
554
555 case "EXPUNGE":
556 if num == 0 {
557 c.xerrorf("invalid zero number for untagged expunge response")
558 }
559 c.xcrlf()
560 return UntaggedExpunge(num)
561
562 case "EXISTS":
563 c.xcrlf()
564 return UntaggedExists(num)
565
566 case "RECENT":
567 c.xneedDisabled("should not send RECENT in IMAP4rev2", CapIMAP4rev2)
568 c.xcrlf()
569 return UntaggedRecent(num)
570
571 default:
572 c.xerrorf("unknown untagged numbered response %q", w)
573 panic("not reached")
574 }
575 }
576 c.xerrorf("unknown untagged response %q", w)
577 }
578 panic("not reached")
579}
580
581// ../rfc/3501:4864 ../rfc/9051:6742
582// Already parsed: "*" SP nznumber SP "FETCH" SP
583func (c *Conn) xfetch(num uint32) UntaggedFetch {
584 c.xtake("(")
585 attrs := []FetchAttr{c.xmsgatt1()}
586 for c.take(' ') {
587 attrs = append(attrs, c.xmsgatt1())
588 }
589 c.xtake(")")
590 return UntaggedFetch{num, attrs}
591}
592
593// ../rfc/9051:6746
594func (c *Conn) xmsgatt1() FetchAttr {
595 f := ""
596 for {
597 b := c.xbyte()
598 if b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b >= '0' && b <= '9' || b == '.' {
599 f += string(rune(b))
600 continue
601 }
602 c.unreadbyte()
603 break
604 }
605
606 F := strings.ToUpper(f)
607 switch F {
608 case "FLAGS":
609 c.xspace()
610 c.xtake("(")
611 var flags []string
612 if !c.take(')') {
613 flags = []string{c.xflag()}
614 for c.take(' ') {
615 flags = append(flags, c.xflag())
616 }
617 c.xtake(")")
618 }
619 return FetchFlags(flags)
620
621 case "ENVELOPE":
622 c.xspace()
623 return FetchEnvelope(c.xenvelope())
624
625 case "INTERNALDATE":
626 c.xspace()
627 return FetchInternalDate(c.xquoted()) // todo: parsed time
628
629 case "RFC822.SIZE":
630 c.xspace()
631 return FetchRFC822Size(c.xint64())
632
633 case "RFC822":
634 c.xspace()
635 s := c.xnilString()
636 return FetchRFC822(s)
637
638 case "RFC822.HEADER":
639 c.xspace()
640 s := c.xnilString()
641 return FetchRFC822Header(s)
642
643 case "RFC822.TEXT":
644 c.xspace()
645 s := c.xnilString()
646 return FetchRFC822Text(s)
647
648 case "BODY":
649 if c.take(' ') {
650 return FetchBodystructure{F, c.xbodystructure()}
651 }
652 c.record = true
653 section := c.xsection()
654 var offset int32
655 if c.take('<') {
656 offset = c.xint32()
657 c.xtake(">")
658 }
659 F += c.recorded()
660 c.xspace()
661 body := c.xnilString()
662 return FetchBody{F, section, offset, body}
663
664 case "BODYSTRUCTURE":
665 c.xspace()
666 return FetchBodystructure{F, c.xbodystructure()}
667
668 case "BINARY":
669 c.record = true
670 nums := c.xsectionBinary()
671 F += c.recorded()
672 c.xspace()
673 buf := c.xnilStringLiteral8()
674 return FetchBinary{F, nums, string(buf)}
675
676 case "BINARY.SIZE":
677 c.record = true
678 nums := c.xsectionBinary()
679 F += c.recorded()
680 c.xspace()
681 size := c.xint64()
682 return FetchBinarySize{F, nums, size}
683
684 case "UID":
685 c.xspace()
686 return FetchUID(c.xuint32())
687
688 case "MODSEQ":
689 // ../rfc/7162:2488
690 c.xspace()
691 c.xtake("(")
692 modseq := c.xint64()
693 c.xtake(")")
694 return FetchModSeq(modseq)
695 }
696 c.xerrorf("unknown fetch attribute %q", f)
697 panic("not reached")
698}
699
700func (c *Conn) xnilString() string {
701 if c.peek('"') {
702 return c.xquoted()
703 } else if c.peek('{') {
704 return string(c.xliteral())
705 } else {
706 c.xtake("NIL")
707 return ""
708 }
709}
710
711func (c *Conn) xstring() string {
712 if c.peek('"') {
713 return c.xquoted()
714 }
715 return string(c.xliteral())
716}
717
718func (c *Conn) xastring() string {
719 if c.peek('"') {
720 return c.xquoted()
721 } else if c.peek('{') {
722 return string(c.xliteral())
723 }
724 return c.xatom()
725}
726
727func (c *Conn) xatom() string {
728 var s string
729 for {
730 b, err := c.readbyte()
731 c.xcheckf(err, "read byte for atom")
732 if b <= ' ' || strings.IndexByte("(){%*\"\\]", b) >= 0 {
733 c.r.UnreadByte()
734 if s == "" {
735 c.xerrorf("expected atom")
736 }
737 return s
738 }
739 s += string(rune(b))
740 }
741}
742
743// ../rfc/9051:6856 ../rfc/6855:153
744func (c *Conn) xquoted() string {
745 c.xtake(`"`)
746 s := ""
747 for !c.take('"') {
748 r, err := c.readrune()
749 c.xcheckf(err, "reading rune in quoted string")
750 if r == '\\' {
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)
755 }
756 }
757 // todo: probably refuse some more chars. like \0 and all ctl and backspace.
758 s += string(r)
759 }
760 return s
761}
762
763func (c *Conn) xliteral() []byte {
764 c.xtake("{")
765 size := c.xint64()
766 sync := c.take('+')
767 c.xtake("}")
768 c.xcrlf()
769 if size > 1<<20 {
770 c.xerrorf("refusing to read more than 1MB: %d", size)
771 }
772 if sync {
773 _, err := fmt.Fprintf(c.conn, "+ ok\r\n")
774 c.xcheckf(err, "write continuation")
775 }
776 buf := make([]byte, int(size))
777 _, err := io.ReadFull(c.r, buf)
778 c.xcheckf(err, "reading data for literal")
779 return buf
780}
781
782// ../rfc/9051:6565
783// todo: stricter
784func (c *Conn) xflag0(allowPerm bool) string {
785 s := ""
786 if c.take('\\') {
787 s = `\`
788 if allowPerm && c.take('*') {
789 return `\*`
790 }
791 } else if c.take('$') {
792 s = "$"
793 }
794 s += c.xatom()
795 return s
796}
797
798func (c *Conn) xflag() string {
799 return c.xflag0(false)
800}
801
802func (c *Conn) xflagPerm() string {
803 return c.xflag0(true)
804}
805
806func (c *Conn) xsection() string {
807 c.xtake("[")
808 s := c.xtakeuntil(']')
809 c.xtake("]")
810 return s
811}
812
813func (c *Conn) xsectionBinary() []uint32 {
814 c.xtake("[")
815 var nums []uint32
816 for !c.take(']') {
817 if len(nums) > 0 {
818 c.xtake(".")
819 }
820 nums = append(nums, c.xnzuint32())
821 }
822 return nums
823}
824
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('{') {
828 return c.xliteral()
829 }
830 return []byte(c.xnilString())
831}
832
833// ../rfc/9051:6355
834func (c *Conn) xbodystructure() any {
835 c.xtake("(")
836 if c.peek('(') {
837 // ../rfc/9051:6411
838 parts := []any{c.xbodystructure()}
839 for c.peek('(') {
840 parts = append(parts, c.xbodystructure())
841 }
842 c.xspace()
843 mediaSubtype := c.xstring()
844 // todo: parse optional body-ext-mpart
845 c.xtake(")")
846 return BodyTypeMpart{parts, mediaSubtype}
847 }
848
849 mediaType := c.xstring()
850 c.xspace()
851 mediaSubtype := c.xstring()
852 c.xspace()
853 bodyFields := c.xbodyFields()
854 if c.take(' ') {
855 if c.peek('(') {
856 // ../rfc/9051:6415
857 envelope := c.xenvelope()
858 c.xspace()
859 bodyStructure := c.xbodystructure()
860 c.xspace()
861 lines := c.xint64()
862 c.xtake(")")
863 return BodyTypeMsg{mediaType, mediaSubtype, bodyFields, envelope, bodyStructure, lines}
864 }
865 // ../rfc/9051:6418
866 lines := c.xint64()
867 c.xtake(")")
868 return BodyTypeText{mediaType, mediaSubtype, bodyFields, lines}
869 }
870 // ../rfc/9051:6407
871 c.xtake(")")
872 return BodyTypeBasic{mediaType, mediaSubtype, bodyFields}
873
874 // todo: verify the media(sub)type is valid for returned data.
875}
876
877// ../rfc/9051:6376
878func (c *Conn) xbodyFields() BodyFields {
879 params := c.xbodyFldParam()
880 c.xspace()
881 contentID := c.xnilString()
882 c.xspace()
883 contentDescr := c.xnilString()
884 c.xspace()
885 cte := c.xnilString()
886 c.xspace()
887 octets := c.xint32()
888 return BodyFields{params, contentID, contentDescr, cte, octets}
889}
890
891// ../rfc/9051:6401
892func (c *Conn) xbodyFldParam() [][2]string {
893 if c.take('(') {
894 k := c.xstring()
895 c.xspace()
896 v := c.xstring()
897 l := [][2]string{{k, v}}
898 for c.take(' ') {
899 k = c.xstring()
900 c.xspace()
901 v = c.xstring()
902 l = append(l, [2]string{k, v})
903 }
904 c.xtake(")")
905 return l
906 }
907 c.xtake("NIL")
908 return nil
909}
910
911// ../rfc/9051:6522
912func (c *Conn) xenvelope() Envelope {
913 c.xtake("(")
914 date := c.xnilString()
915 c.xspace()
916 subject := c.xnilString()
917 c.xspace()
918 from := c.xaddresses()
919 c.xspace()
920 sender := c.xaddresses()
921 c.xspace()
922 replyTo := c.xaddresses()
923 c.xspace()
924 to := c.xaddresses()
925 c.xspace()
926 cc := c.xaddresses()
927 c.xspace()
928 bcc := c.xaddresses()
929 c.xspace()
930 inReplyTo := c.xnilString()
931 c.xspace()
932 messageID := c.xnilString()
933 c.xtake(")")
934 return Envelope{date, subject, from, sender, replyTo, to, cc, bcc, inReplyTo, messageID}
935}
936
937// ../rfc/9051:6526
938func (c *Conn) xaddresses() []Address {
939 if !c.take('(') {
940 c.xtake("NIL")
941 return nil
942 }
943 l := []Address{c.xaddress()}
944 for !c.take(')') {
945 l = append(l, c.xaddress())
946 }
947 return l
948}
949
950// ../rfc/9051:6303
951func (c *Conn) xaddress() Address {
952 c.xtake("(")
953 name := c.xnilString()
954 c.xspace()
955 adl := c.xnilString()
956 c.xspace()
957 mailbox := c.xnilString()
958 c.xspace()
959 host := c.xnilString()
960 c.xtake(")")
961 return Address{name, adl, mailbox, host}
962}
963
964// ../rfc/9051:6584
965func (c *Conn) xflagList() []string {
966 c.xtake("(")
967 var l []string
968 if !c.take(')') {
969 l = []string{c.xflag()}
970 for c.take(' ') {
971 l = append(l, c.xflag())
972 }
973 c.xtake(")")
974 }
975 return l
976}
977
978// ../rfc/9051:6690
979func (c *Conn) xmailboxList() UntaggedList {
980 c.xtake("(")
981 var flags []string
982 if !c.peek(')') {
983 flags = append(flags, c.xflag())
984 for c.take(' ') {
985 flags = append(flags, c.xflag())
986 }
987 }
988 c.xtake(")")
989 c.xspace()
990 var quoted string
991 var b byte
992 if c.peek('"') {
993 quoted = c.xquoted()
994 if len(quoted) != 1 {
995 c.xerrorf("mailbox-list has multichar quoted part: %q", quoted)
996 }
997 b = byte(quoted[0])
998 } else if !c.peek(' ') {
999 c.xtake("NIL")
1000 }
1001 c.xspace()
1002 mailbox := c.xastring()
1003 ul := UntaggedList{flags, b, mailbox, nil, ""}
1004 if c.take(' ') {
1005 c.xtake("(")
1006 if !c.peek(')') {
1007 c.xmboxListExtendedItem(&ul)
1008 for c.take(' ') {
1009 c.xmboxListExtendedItem(&ul)
1010 }
1011 }
1012 c.xtake(")")
1013 }
1014 return ul
1015}
1016
1017// ../rfc/9051:6699
1018func (c *Conn) xmboxListExtendedItem(ul *UntaggedList) {
1019 tag := c.xastring()
1020 c.xspace()
1021 if strings.ToUpper(tag) == "OLDNAME" {
1022 // ../rfc/9051:6811
1023 c.xtake("(")
1024 name := c.xastring()
1025 c.xtake(")")
1026 ul.OldName = name
1027 return
1028 }
1029 val := c.xtaggedExtVal()
1030 ul.Extended = append(ul.Extended, MboxListExtendedItem{tag, val})
1031}
1032
1033// ../rfc/9051:7111
1034func (c *Conn) xtaggedExtVal() TaggedExtVal {
1035 if c.take('(') {
1036 var r TaggedExtVal
1037 if !c.take(')') {
1038 comp := c.xtaggedExtComp()
1039 r.Comp = &comp
1040 c.xtake(")")
1041 }
1042 return r
1043 }
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' {
1048 c.unreadbyte()
1049 ss := c.xsequenceSet()
1050 return TaggedExtVal{SeqSet: &ss}
1051 }
1052 s := c.xdigits()
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}
1058 }
1059 var sr NumRange
1060 sr.First = uint32(num)
1061 if c.take(':') {
1062 var num uint32
1063 if !c.take('*') {
1064 num = c.xnzuint32()
1065 }
1066 sr.Last = &num
1067 }
1068 ss := c.xsequenceSet()
1069 ss.Ranges = append([]NumRange{sr}, ss.Ranges...)
1070 return TaggedExtVal{SeqSet: &ss}
1071}
1072
1073// ../rfc/9051:7034
1074func (c *Conn) xsequenceSet() NumSet {
1075 if c.take('$') {
1076 return NumSet{SearchResult: true}
1077 }
1078 var ss NumSet
1079 for {
1080 var sr NumRange
1081 if !c.take('*') {
1082 sr.First = c.xnzuint32()
1083 }
1084 if c.take(':') {
1085 var num uint32
1086 if !c.take('*') {
1087 num = c.xnzuint32()
1088 }
1089 sr.Last = &num
1090 }
1091 ss.Ranges = append(ss.Ranges, sr)
1092 if !c.take(',') {
1093 break
1094 }
1095 }
1096 return ss
1097}
1098
1099// ../rfc/9051:7097
1100func (c *Conn) xtaggedExtComp() TaggedExtComp {
1101 if c.take('(') {
1102 r := c.xtaggedExtComp()
1103 c.xtake(")")
1104 return TaggedExtComp{Comps: []TaggedExtComp{r}}
1105 }
1106 s := c.xastring()
1107 if !c.peek(' ') {
1108 return TaggedExtComp{String: s}
1109 }
1110 l := []TaggedExtComp{{String: s}}
1111 for c.take(' ') {
1112 l = append(l, c.xtaggedExtComp())
1113 }
1114 return TaggedExtComp{Comps: l}
1115}
1116
1117// ../rfc/9051:6765
1118func (c *Conn) xnamespace() []NamespaceDescr {
1119 if !c.take('(') {
1120 c.xtake("NIL")
1121 return nil
1122 }
1123
1124 l := []NamespaceDescr{c.xnamespaceDescr()}
1125 for !c.take(')') {
1126 l = append(l, c.xnamespaceDescr())
1127 }
1128 return l
1129}
1130
1131// ../rfc/9051:6769
1132func (c *Conn) xnamespaceDescr() NamespaceDescr {
1133 c.xtake("(")
1134 prefix := c.xstring()
1135 c.xspace()
1136 var b byte
1137 if c.peek('"') {
1138 s := c.xquoted()
1139 if len(s) != 1 {
1140 c.xerrorf("namespace-descr: expected single char, got %q", s)
1141 }
1142 b = byte(s[0])
1143 } else {
1144 c.xtake("NIL")
1145 }
1146 var exts []NamespaceExtension
1147 for !c.take(')') {
1148 c.xspace()
1149 key := c.xstring()
1150 c.xspace()
1151 c.xtake("(")
1152 values := []string{c.xstring()}
1153 for c.take(' ') {
1154 values = append(values, c.xstring())
1155 }
1156 c.xtake(")")
1157 exts = append(exts, NamespaceExtension{key, values})
1158 }
1159 return NamespaceDescr{prefix, b, exts}
1160}
1161
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)
1167 }
1168 }
1169}
1170
1171// ../rfc/9051:6546
1172// Already consumed: "ESEARCH"
1173func (c *Conn) xesearchResponse() (r UntaggedEsearch) {
1174
1175 if !c.take(' ') {
1176 return
1177 }
1178 if c.take('(') {
1179 // ../rfc/9051:6921
1180 c.xtake("TAG")
1181 c.xspace()
1182 r.Correlator = c.xastring()
1183 c.xtake(")")
1184 }
1185 if !c.take(' ') {
1186 return
1187 }
1188 w := c.xnonspace()
1189 W := strings.ToUpper(w)
1190 if W == "UID" {
1191 r.UID = true
1192 if !c.take(' ') {
1193 return
1194 }
1195 w = c.xnonspace()
1196 W = strings.ToUpper(w)
1197 }
1198 for {
1199 // ../rfc/9051:6957
1200 switch W {
1201 case "MIN":
1202 if r.Min != 0 {
1203 c.xerrorf("duplicate MIN in ESEARCH")
1204 }
1205 c.xspace()
1206 num := c.xnzuint32()
1207 r.Min = num
1208
1209 case "MAX":
1210 if r.Max != 0 {
1211 c.xerrorf("duplicate MAX in ESEARCH")
1212 }
1213 c.xspace()
1214 num := c.xnzuint32()
1215 r.Max = num
1216
1217 case "ALL":
1218 if !r.All.IsZero() {
1219 c.xerrorf("duplicate ALL in ESEARCH")
1220 }
1221 c.xspace()
1222 ss := c.xsequenceSet()
1223 if ss.SearchResult {
1224 c.xerrorf("$ for last not valid in ESEARCH")
1225 }
1226 r.All = ss
1227
1228 case "COUNT":
1229 if r.Count != nil {
1230 c.xerrorf("duplicate COUNT in ESEARCH")
1231 }
1232 c.xspace()
1233 num := c.xuint32()
1234 r.Count = &num
1235
1236 // ../rfc/7162:1211 ../rfc/4731:273
1237 case "MODSEQ":
1238 c.xspace()
1239 r.ModSeq = c.xint64()
1240
1241 default:
1242 // Validate ../rfc/9051:7090
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)
1246 }
1247 }
1248 c.xspace()
1249 ext := EsearchDataExt{w, c.xtaggedExtVal()}
1250 r.Exts = append(r.Exts, ext)
1251 }
1252
1253 if !c.take(' ') {
1254 break
1255 }
1256 w = c.xnonspace() // todo: this is too loose
1257 W = strings.ToUpper(w)
1258 }
1259 return
1260}
1261
1262// ../rfc/9051:6441
1263func (c *Conn) xcharset() string {
1264 if c.peek('"') {
1265 return c.xquoted()
1266 }
1267 return c.xatom()
1268}
1269
1270// ../rfc/9051:7133
1271func (c *Conn) xuidset() []NumRange {
1272 ranges := []NumRange{c.xuidrange()}
1273 for c.take(',') {
1274 ranges = append(ranges, c.xuidrange())
1275 }
1276 return ranges
1277}
1278
1279func (c *Conn) xuidrange() NumRange {
1280 uid := c.xnzuint32()
1281 var end *uint32
1282 if c.take(':') {
1283 x := c.xnzuint32()
1284 end = &x
1285 }
1286 return NumRange{uid, end}
1287}
1288
1289// ../rfc/3501:4833
1290func (c *Conn) xlsub() UntaggedLsub {
1291 c.xspace()
1292 c.xtake("(")
1293 r := UntaggedLsub{}
1294 for !c.take(')') {
1295 if len(r.Flags) > 0 {
1296 c.xspace()
1297 }
1298 r.Flags = append(r.Flags, c.xflag())
1299 }
1300 c.xspace()
1301 if c.peek('"') {
1302 s := c.xquoted()
1303 if !c.peek(' ') {
1304 r.Mailbox = s
1305 return r
1306 }
1307 if len(s) != 1 {
1308 // todo: check valid char
1309 c.xerrorf("invalid separator %q", s)
1310 }
1311 r.Separator = byte(s[0])
1312 }
1313 c.xspace()
1314 r.Mailbox = c.xastring()
1315 return r
1316}
1317