mox/imapclient/parse.go
Mechiel Lukkien 507ca73b96
imapserver: implement UIDONLY extension, RFC 9586
Once clients enable this extension, commands can no longer refer to "message
sequence numbers" (MSNs), but can only refer to messages with UIDs. This means
both sides no longer have to carefully keep their sequence numbers in sync
(error-prone), and don't have to keep track of a mapping of sequence numbers to
UIDs (saves resources).

With UIDONLY enabled, all FETCH responses are replaced with UIDFETCH response.
2025-04-11 11:45:49 +02:00

1622 lines
32 KiB
Go

package imapclient
import (
"fmt"
"io"
"strconv"
"strings"
"time"
"github.com/mjl-/mox/mlog"
)
func (c *Conn) recorded() string {
s := string(c.recordBuf)
c.recordBuf = nil
c.record = false
return s
}
func (c *Conn) recordAdd(buf []byte) {
if c.record {
c.recordBuf = append(c.recordBuf, buf...)
}
}
func (c *Conn) xtake(s string) {
buf := make([]byte, len(s))
_, err := io.ReadFull(c.br, buf)
c.xcheckf(err, "taking %q", s)
if !strings.EqualFold(string(buf), s) {
c.xerrorf("got %q, expected %q", buf, s)
}
c.recordAdd(buf)
}
func (c *Conn) readbyte() (byte, error) {
b, err := c.br.ReadByte()
if err == nil {
c.recordAdd([]byte{b})
}
return b, err
}
func (c *Conn) xunreadbyte() {
if c.record {
c.recordBuf = c.recordBuf[:len(c.recordBuf)-1]
}
err := c.br.UnreadByte()
c.xcheckf(err, "unread byte")
}
func (c *Conn) readrune() (rune, error) {
x, _, err := c.br.ReadRune()
if err == nil {
c.recordAdd([]byte(string(x)))
}
return x, err
}
func (c *Conn) space() bool {
return c.take(' ')
}
func (c *Conn) xspace() {
c.xtake(" ")
}
func (c *Conn) xcrlf() {
c.xtake("\r\n")
}
func (c *Conn) peek(exp byte) bool {
b, err := c.readbyte()
if err == nil {
c.xunreadbyte()
}
return err == nil && strings.EqualFold(string(rune(b)), string(rune(exp)))
}
func (c *Conn) peekstring() bool {
return c.peek('"') || c.peek('{')
}
func (c *Conn) take(exp byte) bool {
if c.peek(exp) {
_, _ = c.readbyte()
return true
}
return false
}
func (c *Conn) xstatus() Status {
w := c.xword()
W := strings.ToUpper(w)
switch W {
case "OK":
return OK
case "NO":
return NO
case "BAD":
return BAD
}
c.xerrorf("expected status, got %q", w)
panic("not reached")
}
// Already consumed: tag SP status SP
func (c *Conn) xresult(status Status) Result {
respText := c.xrespText()
return Result{status, respText}
}
func (c *Conn) xrespText() RespText {
var code string
var codeArg CodeArg
if c.take('[') {
code, codeArg = c.xrespCode()
c.xtake("]")
c.xspace()
}
more := ""
for !c.peek('\r') {
more += string(rune(c.xbyte()))
}
return RespText{code, codeArg, more}
}
var knownCodes = stringMap(
// Without parameters.
"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",
"OVERQUOTA", // ../rfc/9208:472
"COMPRESSIONACTIVE", // ../rfc/4978:143
// With parameters.
"BADCHARSET", "CAPABILITY", "PERMANENTFLAGS", "UIDNEXT", "UIDVALIDITY", "UNSEEN", "APPENDUID", "COPYUID",
"HIGHESTMODSEQ", "MODIFIED",
"INPROGRESS", // ../rfc/9585:104
"BADEVENT", "NOTIFICATIONOVERFLOW", // ../rfc/5465:1023
"SERVERBUG",
"UIDREQUIRED", // ../rfc/9586:136
)
func stringMap(l ...string) map[string]struct{} {
r := map[string]struct{}{}
for _, s := range l {
r[s] = struct{}{}
}
return r
}
// ../rfc/9051:6895
func (c *Conn) xrespCode() (string, CodeArg) {
w := ""
for !c.peek(' ') && !c.peek(']') {
w += string(rune(c.xbyte()))
}
W := strings.ToUpper(w)
if _, ok := knownCodes[W]; !ok {
var args []string
for c.space() {
arg := ""
for !c.peek(' ') && !c.peek(']') {
arg += string(rune(c.xbyte()))
}
args = append(args, arg)
}
return W, CodeOther{W, args}
}
var codeArg CodeArg
switch W {
case "BADCHARSET":
var l []string // Must be nil initially.
if c.space() {
c.xtake("(")
l = []string{c.xcharset()}
for c.space() {
l = append(l, c.xcharset())
}
c.xtake(")")
}
codeArg = CodeList{W, l}
case "CAPABILITY":
c.xtake(" ")
caps := []string{c.xatom()}
for c.space() {
caps = append(caps, c.xatom())
}
c.CapAvailable = map[Capability]struct{}{}
for _, cap := range caps {
cap = strings.ToUpper(cap)
c.CapAvailable[Capability(cap)] = struct{}{}
}
codeArg = CodeWords{W, caps}
case "PERMANENTFLAGS":
l := []string{} // Must be non-nil.
if c.space() {
c.xtake("(")
l = []string{c.xflagPerm()}
for c.space() {
l = append(l, c.xflagPerm())
}
c.xtake(")")
}
codeArg = CodeList{W, l}
case "UIDNEXT", "UIDVALIDITY", "UNSEEN":
c.xspace()
codeArg = CodeUint{W, c.xnzuint32()}
case "APPENDUID":
c.xspace()
destUIDValidity := c.xnzuint32()
c.xspace()
uids := c.xuidrange()
codeArg = CodeAppendUID{destUIDValidity, uids}
case "COPYUID":
c.xspace()
destUIDValidity := c.xnzuint32()
c.xspace()
from := c.xuidset()
c.xspace()
to := c.xuidset()
codeArg = CodeCopyUID{destUIDValidity, from, to}
case "HIGHESTMODSEQ":
c.xspace()
codeArg = CodeHighestModSeq(c.xint64())
case "MODIFIED":
c.xspace()
modified := c.xuidset()
codeArg = CodeModified(NumSet{Ranges: modified})
case "INPROGRESS":
// ../rfc/9585:238
var tag string
var current, goal *uint32
if c.space() {
c.xtake("(")
tag = c.xquoted()
c.xspace()
if c.peek('n') || c.peek('N') {
c.xtake("nil")
} else {
v := c.xuint32()
current = &v
}
c.xspace()
if c.peek('n') || c.peek('N') {
c.xtake("nil")
} else {
v := c.xnzuint32()
goal = &v
}
c.xtake(")")
}
codeArg = CodeInProgress{tag, current, goal}
case "BADEVENT":
// ../rfc/5465:1033
c.xspace()
c.xtake("(")
var l []string
for {
s := c.xatom()
l = append(l, s)
if !c.space() {
break
}
}
c.xtake(")")
codeArg = CodeBadEvent(l)
}
return W, codeArg
}
func (c *Conn) xbyte() byte {
b, err := c.readbyte()
c.xcheckf(err, "read byte")
return b
}
// take until b is seen. don't take b itself.
func (c *Conn) xtakeuntil(b byte) string {
var s string
for {
x, err := c.readbyte()
c.xcheckf(err, "read byte")
if x == b {
c.xunreadbyte()
return s
}
s += string(rune(x))
}
}
func (c *Conn) xdigits() string {
var s string
for {
b, err := c.readbyte()
if err == nil && (b >= '0' && b <= '9') {
s += string(rune(b))
continue
}
c.xunreadbyte()
return s
}
}
func (c *Conn) peekdigit() bool {
if b, err := c.readbyte(); err == nil {
c.xunreadbyte()
return b >= '0' && b <= '9'
}
return false
}
func (c *Conn) xint32() int32 {
s := c.xdigits()
num, err := strconv.ParseInt(s, 10, 32)
c.xcheckf(err, "parsing int32")
return int32(num)
}
func (c *Conn) xint64() int64 {
s := c.xdigits()
num, err := strconv.ParseInt(s, 10, 63)
c.xcheckf(err, "parsing int64")
return num
}
func (c *Conn) xuint32() uint32 {
s := c.xdigits()
num, err := strconv.ParseUint(s, 10, 32)
c.xcheckf(err, "parsing uint32")
return uint32(num)
}
func (c *Conn) xnzuint32() uint32 {
v := c.xuint32()
if v == 0 {
c.xerrorf("got 0, expected nonzero uint")
}
return v
}
// todo: replace with proper parsing.
func (c *Conn) xnonspace() string {
var s string
for !c.peek(' ') && !c.peek('\r') && !c.peek('\n') {
s += string(rune(c.xbyte()))
}
if s == "" {
c.xerrorf("expected non-space")
}
return s
}
// todo: replace with proper parsing
func (c *Conn) xword() string {
return c.xatom()
}
// "*" SP is already consumed
// ../rfc/9051:6868
func (c *Conn) xuntagged() Untagged {
w := c.xnonspace()
W := strings.ToUpper(w)
switch W {
case "PREAUTH":
c.xspace()
r := UntaggedPreauth(c.xrespText())
c.xcrlf()
return r
case "BYE":
c.xspace()
r := UntaggedBye(c.xrespText())
c.xcrlf()
return r
case "OK", "NO", "BAD":
c.xspace()
r := UntaggedResult(c.xresult(Status(W)))
c.xcrlf()
return r
case "CAPABILITY":
// ../rfc/9051:6427
var caps []string
for c.space() {
caps = append(caps, c.xnonspace())
}
c.CapAvailable = map[Capability]struct{}{}
for _, cap := range caps {
cap = strings.ToUpper(cap)
c.CapAvailable[Capability(cap)] = struct{}{}
}
r := UntaggedCapability(caps)
c.xcrlf()
return r
case "ENABLED":
// ../rfc/9051:6520
var caps []string
for c.space() {
caps = append(caps, c.xnonspace())
}
for _, cap := range caps {
cap = strings.ToUpper(cap)
c.CapEnabled[Capability(cap)] = struct{}{}
}
r := UntaggedEnabled(caps)
c.xcrlf()
return r
case "FLAGS":
c.xspace()
r := UntaggedFlags(c.xflagList())
c.xcrlf()
return r
case "LIST":
c.xspace()
r := c.xmailboxList()
c.xcrlf()
return r
case "STATUS":
// ../rfc/9051:6681
c.xspace()
mailbox := c.xastring()
c.xspace()
c.xtake("(")
attrs := map[StatusAttr]int64{}
for !c.take(')') {
if len(attrs) > 0 {
c.xspace()
}
s := c.xatom()
c.xspace()
S := StatusAttr(strings.ToUpper(s))
var num int64
// ../rfc/9051:7059
switch S {
case "MESSAGES":
num = int64(c.xuint32())
case "UIDNEXT":
num = int64(c.xnzuint32())
case "UIDVALIDITY":
num = int64(c.xnzuint32())
case "UNSEEN":
num = int64(c.xuint32())
case "DELETED":
num = int64(c.xuint32())
case "SIZE":
num = c.xint64()
case "RECENT":
c.xneedDisabled("RECENT status flag", CapIMAP4rev2)
num = int64(c.xuint32())
case "APPENDLIMIT":
if c.peek('n') || c.peek('N') {
c.xtake("nil")
} else {
num = c.xint64()
}
case "HIGHESTMODSEQ":
num = c.xint64()
case "DELETED-STORAGE":
num = c.xint64()
default:
c.xerrorf("status: unknown attribute %q", s)
}
if _, ok := attrs[S]; ok {
c.xerrorf("status: duplicate attribute %q", s)
}
attrs[S] = num
}
r := UntaggedStatus{mailbox, attrs}
c.xcrlf()
return r
case "METADATA":
// ../rfc/5464:807
c.xspace()
mailbox := c.xastring()
c.xspace()
if !c.take('(') {
// Unsolicited form, with only annotation keys, not values.
var keys []string
for {
key := c.xastring()
keys = append(keys, key)
if !c.space() {
break
}
}
c.xcrlf()
return UntaggedMetadataKeys{mailbox, keys}
}
// Form with values, in response to GETMETADATA command.
r := UntaggedMetadataAnnotations{Mailbox: mailbox}
for {
key := c.xastring()
c.xspace()
var value []byte
var isString bool
if c.take('~') {
value = c.xliteral()
} else if c.peek('"') {
value = []byte(c.xstring())
isString = true
// note: the abnf also allows nstring, but that only makes sense when the
// production rule is used in the setmetadata command. ../rfc/5464:831
} else {
// For response to extended list.
c.xtake("nil")
}
r.Annotations = append(r.Annotations, Annotation{key, isString, value})
if c.take(')') {
break
}
c.xspace()
}
c.xcrlf()
return r
case "NAMESPACE":
// ../rfc/9051:6778
c.xspace()
personal := c.xnamespace()
c.xspace()
other := c.xnamespace()
c.xspace()
shared := c.xnamespace()
r := UntaggedNamespace{personal, other, shared}
c.xcrlf()
return r
case "SEARCH":
// ../rfc/9051:6809
c.xneedDisabled("untagged SEARCH response", CapIMAP4rev2)
var nums []uint32
for c.space() {
// ../rfc/7162:2557
if c.take('(') {
c.xtake("MODSEQ")
c.xspace()
modseq := c.xint64()
c.xtake(")")
c.xcrlf()
return UntaggedSearchModSeq{nums, modseq}
}
nums = append(nums, c.xnzuint32())
}
r := UntaggedSearch(nums)
c.xcrlf()
return r
case "ESEARCH":
r := c.xesearchResponse()
c.xcrlf()
return r
case "LSUB":
c.xneedDisabled("untagged LSUB response", CapIMAP4rev2)
r := c.xlsub()
c.xcrlf()
return r
case "ID":
// ../rfc/2971:243
c.xspace()
var params map[string]string
if c.take('(') {
params = map[string]string{}
for !c.take(')') {
if len(params) > 0 {
c.xspace()
}
k := c.xstring()
c.xspace()
v := c.xnilString()
if _, ok := params[k]; ok {
c.xerrorf("duplicate key %q", k)
}
params[k] = v
}
} else {
c.xtake("nil")
}
c.xcrlf()
return UntaggedID(params)
// ../rfc/7162:2623
case "VANISHED":
c.xspace()
var earlier bool
if c.take('(') {
c.xtake("EARLIER")
c.xtake(")")
c.xspace()
earlier = true
}
uids := c.xuidset()
c.xcrlf()
return UntaggedVanished{earlier, NumSet{Ranges: uids}}
// ../rfc/9208:668 ../2087:242
case "QUOTAROOT":
c.xspace()
c.xastring()
var roots []string
for c.space() {
root := c.xastring()
roots = append(roots, root)
}
c.xcrlf()
return UntaggedQuotaroot(roots)
// ../rfc/9208:666 ../rfc/2087:239
case "QUOTA":
c.xspace()
root := c.xastring()
c.xspace()
c.xtake("(")
xresource := func() QuotaResource {
name := c.xatom()
c.xspace()
usage := c.xint64()
c.xspace()
limit := c.xint64()
return QuotaResource{QuotaResourceName(strings.ToUpper(name)), usage, limit}
}
seen := map[QuotaResourceName]bool{}
l := []QuotaResource{xresource()}
seen[l[0].Name] = true
for c.space() {
res := xresource()
if seen[res.Name] {
c.xerrorf("duplicate resource name %q", res.Name)
}
seen[res.Name] = true
l = append(l, res)
}
c.xtake(")")
c.xcrlf()
return UntaggedQuota{root, l}
default:
v, err := strconv.ParseUint(w, 10, 32)
if err == nil {
num := uint32(v)
c.xspace()
w = c.xword()
W = strings.ToUpper(w)
switch W {
case "FETCH", "UIDFETCH":
if num == 0 {
c.xerrorf("invalid zero number for untagged fetch response")
}
c.xspace()
attrs := c.xfetch()
c.xcrlf()
if W == "UIDFETCH" {
return UntaggedUIDFetch{num, attrs}
}
return UntaggedFetch{num, attrs}
case "EXPUNGE":
if num == 0 {
c.xerrorf("invalid zero number for untagged expunge response")
}
c.xcrlf()
return UntaggedExpunge(num)
case "EXISTS":
c.xcrlf()
return UntaggedExists(num)
case "RECENT":
c.xneedDisabled("should not send RECENT in IMAP4rev2", CapIMAP4rev2)
c.xcrlf()
return UntaggedRecent(num)
default:
c.xerrorf("unknown untagged numbered response %q", w)
panic("not reached")
}
}
c.xerrorf("unknown untagged response %q", w)
}
panic("not reached")
}
// ../rfc/3501:4864 ../rfc/9051:6742
// Already parsed: "*" SP nznumber SP "FETCH" SP
func (c *Conn) xfetch() []FetchAttr {
c.xtake("(")
attrs := []FetchAttr{c.xmsgatt1()}
for c.space() {
attrs = append(attrs, c.xmsgatt1())
}
c.xtake(")")
return attrs
}
// ../rfc/9051:6746
func (c *Conn) xmsgatt1() FetchAttr {
f := ""
for {
b := c.xbyte()
if b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b >= '0' && b <= '9' || b == '.' {
f += string(rune(b))
continue
}
c.xunreadbyte()
break
}
F := strings.ToUpper(f)
switch F {
case "FLAGS":
c.xspace()
c.xtake("(")
var flags []string
if !c.take(')') {
flags = []string{c.xflag()}
for c.space() {
flags = append(flags, c.xflag())
}
c.xtake(")")
}
return FetchFlags(flags)
case "ENVELOPE":
c.xspace()
return FetchEnvelope(c.xenvelope())
case "INTERNALDATE":
c.xspace()
s := c.xquoted()
v, err := time.Parse("_2-Jan-2006 15:04:05 -0700", s)
c.xcheckf(err, "parsing internaldate")
return FetchInternalDate{v}
case "SAVEDATE":
c.xspace()
var t *time.Time
if c.peek('"') {
s := c.xquoted()
v, err := time.Parse("_2-Jan-2006 15:04:05 -0700", s)
c.xcheckf(err, "parsing savedate")
t = &v
} else {
c.xtake("nil")
}
return FetchSaveDate{t}
case "RFC822.SIZE":
c.xspace()
return FetchRFC822Size(c.xint64())
case "RFC822":
c.xspace()
s := c.xnilString()
return FetchRFC822(s)
case "RFC822.HEADER":
c.xspace()
s := c.xnilString()
return FetchRFC822Header(s)
case "RFC822.TEXT":
c.xspace()
s := c.xnilString()
return FetchRFC822Text(s)
case "BODY":
if c.space() {
return FetchBodystructure{F, c.xbodystructure(false)}
}
c.record = true
section := c.xsection()
var offset int32
if c.take('<') {
offset = c.xint32()
c.xtake(">")
}
F += c.recorded()
c.xspace()
body := c.xnilString()
return FetchBody{F, section, offset, body}
case "BODYSTRUCTURE":
c.xspace()
return FetchBodystructure{F, c.xbodystructure(true)}
case "BINARY":
c.record = true
nums := c.xsectionBinary()
F += c.recorded()
c.xspace()
buf := c.xnilStringLiteral8()
return FetchBinary{F, nums, string(buf)}
case "BINARY.SIZE":
c.record = true
nums := c.xsectionBinary()
F += c.recorded()
c.xspace()
size := c.xint64()
return FetchBinarySize{F, nums, size}
case "UID":
c.xspace()
return FetchUID(c.xuint32())
case "MODSEQ":
// ../rfc/7162:2488
c.xspace()
c.xtake("(")
modseq := c.xint64()
c.xtake(")")
return FetchModSeq(modseq)
case "PREVIEW":
// ../rfc/8970:348
c.xspace()
var preview *string
if c.peek('n') || c.peek('N') {
c.xtake("nil")
} else {
s := c.xstring()
preview = &s
}
return FetchPreview{preview}
}
c.xerrorf("unknown fetch attribute %q", f)
panic("not reached")
}
func (c *Conn) xnilString() string {
if c.peek('"') {
return c.xquoted()
} else if c.peek('{') {
return string(c.xliteral())
} else {
c.xtake("nil")
return ""
}
}
func (c *Conn) xstring() string {
if c.peek('"') {
return c.xquoted()
}
return string(c.xliteral())
}
func (c *Conn) xastring() string {
if c.peek('"') {
return c.xquoted()
} else if c.peek('{') {
return string(c.xliteral())
}
return c.xatom()
}
func (c *Conn) xatom() string {
var s string
for {
b, err := c.readbyte()
c.xcheckf(err, "read byte for atom")
if b <= ' ' || strings.IndexByte("(){%*\"\\]", b) >= 0 {
c.xunreadbyte()
if s == "" {
c.xerrorf("expected atom")
}
return s
}
s += string(rune(b))
}
}
// ../rfc/9051:6856 ../rfc/6855:153
func (c *Conn) xquoted() string {
c.xtake(`"`)
s := ""
for !c.take('"') {
r, err := c.readrune()
c.xcheckf(err, "reading rune in quoted string")
if r == '\\' {
r, err = c.readrune()
c.xcheckf(err, "reading escaped char in quoted string")
if r != '\\' && r != '"' {
c.xerrorf("quoted char not backslash or dquote: %c", r)
}
}
// todo: probably refuse some more chars. like \0 and all ctl and backspace.
s += string(r)
}
return s
}
func (c *Conn) xliteral() []byte {
c.xtake("{")
size := c.xint64()
sync := c.take('+')
c.xtake("}")
c.xcrlf()
// todo: for some literals, read as tracedata
if size > 1<<20 {
c.xerrorf("refusing to read more than 1MB: %d", size)
}
if sync {
fmt.Fprintf(c.xbw, "+ ok\r\n")
c.xflush()
}
buf := make([]byte, int(size))
defer c.xtraceread(mlog.LevelTracedata)()
_, err := io.ReadFull(c.br, buf)
c.xcheckf(err, "reading data for literal")
c.xtraceread(mlog.LevelTrace)
return buf
}
// ../rfc/9051:6565
// todo: stricter
func (c *Conn) xflag0(allowPerm bool) string {
s := ""
if c.take('\\') {
s = `\`
if allowPerm && c.take('*') {
return `\*`
}
} else if c.take('$') {
s = "$"
}
s += c.xatom()
return s
}
func (c *Conn) xflag() string {
return c.xflag0(false)
}
func (c *Conn) xflagPerm() string {
return c.xflag0(true)
}
func (c *Conn) xsection() string {
c.xtake("[")
s := c.xtakeuntil(']')
c.xtake("]")
return s
}
func (c *Conn) xsectionBinary() []uint32 {
c.xtake("[")
var nums []uint32
for !c.take(']') {
if len(nums) > 0 {
c.xtake(".")
}
nums = append(nums, c.xnzuint32())
}
return nums
}
func (c *Conn) xnilStringLiteral8() []byte {
// todo: should make difference for literal8 and literal from string, which bytes are allowed
if c.take('~') || c.peek('{') {
return c.xliteral()
}
return []byte(c.xnilString())
}
// ../rfc/9051:6355
func (c *Conn) xbodystructure(extensibleForm bool) any {
c.xtake("(")
if c.peek('(') {
// ../rfc/9051:6411
parts := []any{c.xbodystructure(extensibleForm)}
for c.peek('(') {
parts = append(parts, c.xbodystructure(extensibleForm))
}
c.xspace()
mediaSubtype := c.xstring()
var ext *BodyExtensionMpart
if extensibleForm && c.space() {
ext = c.xbodyExtMpart()
}
c.xtake(")")
return BodyTypeMpart{parts, mediaSubtype, ext}
}
// todo: verify the media(sub)type is valid for returned data.
var ext *BodyExtension1Part
mediaType := c.xstring()
c.xspace()
mediaSubtype := c.xstring()
c.xspace()
bodyFields := c.xbodyFields()
if !c.space() {
// Basic type without extension.
c.xtake(")")
return BodyTypeBasic{mediaType, mediaSubtype, bodyFields, nil}
}
if c.peek('(') {
// ../rfc/9051:6415
envelope := c.xenvelope()
c.xspace()
bodyStructure := c.xbodystructure(extensibleForm)
c.xspace()
lines := c.xint64()
if extensibleForm && c.space() {
ext = c.xbodyExt1Part()
}
c.xtake(")")
return BodyTypeMsg{mediaType, mediaSubtype, bodyFields, envelope, bodyStructure, lines, ext}
}
if !strings.EqualFold(mediaType, "text") {
if !extensibleForm {
c.xerrorf("body result, basic type, with disallowed extensible form")
}
ext = c.xbodyExt1Part()
// ../rfc/9051:6407
c.xtake(")")
return BodyTypeBasic{mediaType, mediaSubtype, bodyFields, ext}
}
// ../rfc/9051:6418
lines := c.xint64()
if extensibleForm && c.space() {
ext = c.xbodyExt1Part()
}
c.xtake(")")
return BodyTypeText{mediaType, mediaSubtype, bodyFields, lines, ext}
}
// ../rfc/9051:6376 ../rfc/3501:4604
func (c *Conn) xbodyFields() BodyFields {
params := c.xbodyFldParam()
c.xspace()
contentID := c.xnilString()
c.xspace()
contentDescr := c.xnilString()
c.xspace()
cte := c.xnilString()
c.xspace()
octets := c.xint32()
return BodyFields{params, contentID, contentDescr, cte, octets}
}
// ../rfc/9051:6371 ../rfc/3501:4599
func (c *Conn) xbodyExtMpart() (ext *BodyExtensionMpart) {
ext = &BodyExtensionMpart{}
ext.Params = c.xbodyFldParam()
if !c.space() {
return
}
ext.Disposition, ext.DispositionParams = c.xbodyFldDsp()
if !c.space() {
return
}
ext.Language = c.xbodyFldLang()
if !c.space() {
return
}
ext.Location = c.xbodyFldLoc()
for c.space() {
ext.More = append(ext.More, c.xbodyExtension())
}
return
}
// ../rfc/9051:6366 ../rfc/3501:4584
func (c *Conn) xbodyExt1Part() (ext *BodyExtension1Part) {
ext = &BodyExtension1Part{}
ext.MD5 = c.xnilString()
if !c.space() {
return
}
ext.Disposition, ext.DispositionParams = c.xbodyFldDsp()
if !c.space() {
return
}
ext.Language = c.xbodyFldLang()
if !c.space() {
return
}
ext.Location = c.xbodyFldLoc()
for c.space() {
ext.More = append(ext.More, c.xbodyExtension())
}
return
}
// ../rfc/9051:6401 ../rfc/3501:4626
func (c *Conn) xbodyFldParam() [][2]string {
if c.take('(') {
k := c.xstring()
c.xspace()
v := c.xstring()
l := [][2]string{{k, v}}
for c.space() {
k = c.xstring()
c.xspace()
v = c.xstring()
l = append(l, [2]string{k, v})
}
c.xtake(")")
return l
}
c.xtake("nil")
return nil
}
// ../rfc/9051:6381 ../rfc/3501:4609
func (c *Conn) xbodyFldDsp() (string, [][2]string) {
if !c.take('(') {
c.xtake("nil")
return "", nil
}
disposition := c.xstring()
c.xspace()
param := c.xbodyFldParam()
c.xtake(")")
return disposition, param
}
// ../rfc/9051:6391 ../rfc/3501:4616
func (c *Conn) xbodyFldLang() (lang []string) {
if c.take('(') {
lang = []string{c.xstring()}
for c.space() {
lang = append(lang, c.xstring())
}
c.xtake(")")
return lang
}
if c.peekstring() {
return []string{c.xstring()}
}
c.xtake("nil")
return nil
}
// ../rfc/9051:6393 ../rfc/3501:4618
func (c *Conn) xbodyFldLoc() string {
return c.xnilString()
}
// ../rfc/9051:6357 ../rfc/3501:4575
func (c *Conn) xbodyExtension() (ext BodyExtension) {
if c.take('(') {
for {
ext.More = append(ext.More, c.xbodyExtension())
if !c.space() {
break
}
}
c.xtake(")")
} else if c.peekdigit() {
num := c.xint64()
ext.Number = &num
} else if c.peekstring() {
str := c.xstring()
ext.String = &str
} else {
c.xtake("nil")
}
return ext
}
// ../rfc/9051:6522
func (c *Conn) xenvelope() Envelope {
c.xtake("(")
date := c.xnilString()
c.xspace()
subject := c.xnilString()
c.xspace()
from := c.xaddresses()
c.xspace()
sender := c.xaddresses()
c.xspace()
replyTo := c.xaddresses()
c.xspace()
to := c.xaddresses()
c.xspace()
cc := c.xaddresses()
c.xspace()
bcc := c.xaddresses()
c.xspace()
inReplyTo := c.xnilString()
c.xspace()
messageID := c.xnilString()
c.xtake(")")
return Envelope{date, subject, from, sender, replyTo, to, cc, bcc, inReplyTo, messageID}
}
// ../rfc/9051:6526
func (c *Conn) xaddresses() []Address {
if !c.take('(') {
c.xtake("nil")
return nil
}
l := []Address{c.xaddress()}
for !c.take(')') {
l = append(l, c.xaddress())
}
return l
}
// ../rfc/9051:6303
func (c *Conn) xaddress() Address {
c.xtake("(")
name := c.xnilString()
c.xspace()
adl := c.xnilString()
c.xspace()
mailbox := c.xnilString()
c.xspace()
host := c.xnilString()
c.xtake(")")
return Address{name, adl, mailbox, host}
}
// ../rfc/9051:6584
func (c *Conn) xflagList() []string {
c.xtake("(")
var l []string
if !c.take(')') {
l = []string{c.xflag()}
for c.space() {
l = append(l, c.xflag())
}
c.xtake(")")
}
return l
}
// ../rfc/9051:6690
func (c *Conn) xmailboxList() UntaggedList {
c.xtake("(")
var flags []string
if !c.peek(')') {
flags = append(flags, c.xflag())
for c.space() {
flags = append(flags, c.xflag())
}
}
c.xtake(")")
c.xspace()
var quoted string
var b byte
if c.peek('"') {
quoted = c.xquoted()
if len(quoted) != 1 {
c.xerrorf("mailbox-list has multichar quoted part: %q", quoted)
}
b = byte(quoted[0])
} else if !c.peek(' ') {
c.xtake("nil")
}
c.xspace()
mailbox := c.xastring()
ul := UntaggedList{flags, b, mailbox, nil, ""}
if c.space() {
c.xtake("(")
if !c.peek(')') {
c.xmboxListExtendedItem(&ul)
for c.space() {
c.xmboxListExtendedItem(&ul)
}
}
c.xtake(")")
}
return ul
}
// ../rfc/9051:6699
func (c *Conn) xmboxListExtendedItem(ul *UntaggedList) {
tag := c.xastring()
c.xspace()
if strings.ToUpper(tag) == "OLDNAME" {
// ../rfc/9051:6811
c.xtake("(")
name := c.xastring()
c.xtake(")")
ul.OldName = name
return
}
val := c.xtaggedExtVal()
ul.Extended = append(ul.Extended, MboxListExtendedItem{tag, val})
}
// ../rfc/9051:7111
func (c *Conn) xtaggedExtVal() TaggedExtVal {
if c.take('(') {
var r TaggedExtVal
if !c.take(')') {
comp := c.xtaggedExtComp()
r.Comp = &comp
c.xtake(")")
}
return r
}
// 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.
b, err := c.readbyte()
c.xcheckf(err, "read byte for tagged-ext-val")
if b < '0' || b > '9' {
c.xunreadbyte()
ss := c.xsequenceSet()
return TaggedExtVal{SeqSet: &ss}
}
s := c.xdigits()
num, err := strconv.ParseInt(s, 10, 63)
c.xcheckf(err, "parsing int")
if !c.peek(':') && !c.peek(',') {
// not a larger sequence-set
return TaggedExtVal{Number: &num}
}
var sr NumRange
sr.First = uint32(num)
if c.take(':') {
var num uint32
if !c.take('*') {
num = c.xnzuint32()
}
sr.Last = &num
}
ss := c.xsequenceSet()
ss.Ranges = append([]NumRange{sr}, ss.Ranges...)
return TaggedExtVal{SeqSet: &ss}
}
// ../rfc/9051:7034
func (c *Conn) xsequenceSet() NumSet {
if c.take('$') {
return NumSet{SearchResult: true}
}
var ss NumSet
for {
var sr NumRange
if !c.take('*') {
sr.First = c.xnzuint32()
}
if c.take(':') {
var num uint32
if !c.take('*') {
num = c.xnzuint32()
}
sr.Last = &num
}
ss.Ranges = append(ss.Ranges, sr)
if !c.take(',') {
break
}
}
return ss
}
// ../rfc/9051:7097
func (c *Conn) xtaggedExtComp() TaggedExtComp {
if c.take('(') {
r := c.xtaggedExtComp()
c.xtake(")")
return TaggedExtComp{Comps: []TaggedExtComp{r}}
}
s := c.xastring()
if !c.peek(' ') {
return TaggedExtComp{String: s}
}
l := []TaggedExtComp{{String: s}}
for c.space() {
l = append(l, c.xtaggedExtComp())
}
return TaggedExtComp{Comps: l}
}
// ../rfc/9051:6765
func (c *Conn) xnamespace() []NamespaceDescr {
if !c.take('(') {
c.xtake("nil")
return nil
}
l := []NamespaceDescr{c.xnamespaceDescr()}
for !c.take(')') {
l = append(l, c.xnamespaceDescr())
}
return l
}
// ../rfc/9051:6769
func (c *Conn) xnamespaceDescr() NamespaceDescr {
c.xtake("(")
prefix := c.xstring()
c.xspace()
var b byte
if c.peek('"') {
s := c.xquoted()
if len(s) != 1 {
c.xerrorf("namespace-descr: expected single char, got %q", s)
}
b = byte(s[0])
} else {
c.xtake("nil")
}
var exts []NamespaceExtension
for !c.take(')') {
c.xspace()
key := c.xstring()
c.xspace()
c.xtake("(")
values := []string{c.xstring()}
for c.space() {
values = append(values, c.xstring())
}
c.xtake(")")
exts = append(exts, NamespaceExtension{key, values})
}
return NamespaceDescr{prefix, b, exts}
}
// require all of caps to be disabled.
func (c *Conn) xneedDisabled(msg string, caps ...Capability) {
for _, cap := range caps {
if _, ok := c.CapEnabled[cap]; ok {
c.xerrorf("%s: invalid because of enabled capability %q", msg, cap)
}
}
}
// ../rfc/9051:6546
// Already consumed: "ESEARCH"
func (c *Conn) xesearchResponse() (r UntaggedEsearch) {
if !c.space() {
return
}
if c.take('(') {
// ../rfc/9051:6921 ../rfc/7377:465
seen := map[string]bool{}
for {
var kind string
if c.peek('t') || c.peek('T') {
kind = "TAG"
c.xtake(kind)
c.xspace()
r.Tag = c.xastring()
} else if c.peek('m') || c.peek('M') {
kind = "MAILBOX"
c.xtake(kind)
c.xspace()
r.Mailbox = c.xastring()
if r.Mailbox == "" {
c.xerrorf("invalid empty mailbox in search correlator")
}
} else if c.peek('u') || c.peek('U') {
kind = "UIDVALIDITY"
c.xtake(kind)
c.xspace()
r.UIDValidity = c.xnzuint32()
} else {
c.xerrorf("expected tag/correlator, mailbox or uidvalidity")
}
if seen[kind] {
c.xerrorf("duplicate search correlator %q", kind)
}
seen[kind] = true
if !c.take(' ') {
break
}
}
if r.Tag == "" {
c.xerrorf("missing tag search correlator")
}
if (r.Mailbox != "") != (r.UIDValidity != 0) {
c.xerrorf("mailbox and uidvalidity correlators must both be absent or both be present")
}
c.xtake(")")
}
if !c.space() {
return
}
w := c.xnonspace()
W := strings.ToUpper(w)
if W == "UID" {
r.UID = true
if !c.space() {
return
}
w = c.xnonspace()
W = strings.ToUpper(w)
}
for {
// ../rfc/9051:6957
switch W {
case "MIN":
if r.Min != 0 {
c.xerrorf("duplicate MIN in ESEARCH")
}
c.xspace()
num := c.xnzuint32()
r.Min = num
case "MAX":
if r.Max != 0 {
c.xerrorf("duplicate MAX in ESEARCH")
}
c.xspace()
num := c.xnzuint32()
r.Max = num
case "ALL":
if !r.All.IsZero() {
c.xerrorf("duplicate ALL in ESEARCH")
}
c.xspace()
ss := c.xsequenceSet()
if ss.SearchResult {
c.xerrorf("$ for last not valid in ESEARCH")
}
r.All = ss
case "COUNT":
if r.Count != nil {
c.xerrorf("duplicate COUNT in ESEARCH")
}
c.xspace()
num := c.xuint32()
r.Count = &num
// ../rfc/7162:1211 ../rfc/4731:273
case "MODSEQ":
c.xspace()
r.ModSeq = c.xint64()
default:
// Validate ../rfc/9051:7090
for i, b := range []byte(w) {
if !(b >= 'A' && b <= 'Z' || strings.IndexByte("-_.", b) >= 0 || i > 0 && strings.IndexByte("0123456789:", b) >= 0) {
c.xerrorf("invalid tag %q", w)
}
}
c.xspace()
ext := EsearchDataExt{w, c.xtaggedExtVal()}
r.Exts = append(r.Exts, ext)
}
if !c.space() {
break
}
w = c.xnonspace() // todo: this is too loose
W = strings.ToUpper(w)
}
return
}
// ../rfc/9051:6441
func (c *Conn) xcharset() string {
if c.peek('"') {
return c.xquoted()
}
return c.xatom()
}
// ../rfc/9051:7133
func (c *Conn) xuidset() []NumRange {
ranges := []NumRange{c.xuidrange()}
for c.take(',') {
ranges = append(ranges, c.xuidrange())
}
return ranges
}
func (c *Conn) xuidrange() NumRange {
uid := c.xnzuint32()
var end *uint32
if c.take(':') {
x := c.xnzuint32()
end = &x
}
return NumRange{uid, end}
}
// ../rfc/3501:4833
func (c *Conn) xlsub() UntaggedLsub {
c.xspace()
c.xtake("(")
r := UntaggedLsub{}
for !c.take(')') {
if len(r.Flags) > 0 {
c.xspace()
}
r.Flags = append(r.Flags, c.xflag())
}
c.xspace()
if c.peek('"') {
s := c.xquoted()
if !c.peek(' ') {
r.Mailbox = s
return r
}
if len(s) != 1 {
// todo: check valid char
c.xerrorf("invalid separator %q", s)
}
r.Separator = byte(s[0])
}
c.xspace()
r.Mailbox = c.xastring()
return r
}