mirror of
https://github.com/mjl-/mox.git
synced 2025-06-28 07:48:13 +03:00

The imapclient needs more changes, like more strict parsing, before it can be a generally usable IMAP client, these are a few steps towards that. - Fix a bug in the imapserver METADATA responses for TOOMANY and MAXSIZE. - Split low-level IMAP protocol handling (new Proto type) from the higher-level client command handling (existing Conn type). The idea is that some simple uses of IMAP can get by with just using these commands, while more intricate uses of IMAP (like a synchronizing client that needs to talk to all kinds of servers with different behaviours and implemented extensions) can write custom commands and read untagged responses or command completion results explicitly. The lower-level method names have clearer names now, like ReadResponse instead of Response. - Merge the untagged responses and (command completion) "Result" into a new type Response. Makes function signatures simpler. And make Response implement the error interface, and change command methods to return the Response as error if the result is NO or BAD. Simplifies error handling, and still provides the option to continue after a NO or BAD. - Add UIDSearch/MSNSearch commands, with a custom "search program", so mostly to indicate these commands exist. - More complete coverage of types for response codes, for easier handling. - Automatically handle any ENABLED or CAPABILITY untagged response or response code for IMAP command methods on type Conn. - Make difference between MSN vs UID versions of FETCH/STORE/SEARCH/COPY/MOVE/REPLACE commands more clear. The original MSN commands now have MSN prefixed to their name, so they are grouped together in the documentation. - Document which capabilities are needed for a command.
1668 lines
32 KiB
Go
1668 lines
32 KiB
Go
package imapclient
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/mjl-/mox/mlog"
|
|
)
|
|
|
|
// todo: stricter parsing. xnonspace() and xword() should be replaced by proper parsers
|
|
|
|
// Keep the parsing method names and the types similar to the ABNF names in the RFCs.
|
|
|
|
func (p *Proto) recorded() string {
|
|
s := string(p.recordBuf)
|
|
p.recordBuf = nil
|
|
p.record = false
|
|
return s
|
|
}
|
|
|
|
func (p *Proto) recordAdd(buf []byte) {
|
|
if p.record {
|
|
p.recordBuf = append(p.recordBuf, buf...)
|
|
}
|
|
}
|
|
|
|
func (p *Proto) xtake(s string) {
|
|
buf := make([]byte, len(s))
|
|
_, err := io.ReadFull(p.br, buf)
|
|
p.xcheckf(err, "taking %q", s)
|
|
if !strings.EqualFold(string(buf), s) {
|
|
p.xerrorf("got %q, expected %q", buf, s)
|
|
}
|
|
p.recordAdd(buf)
|
|
}
|
|
|
|
func (p *Proto) readbyte() (byte, error) {
|
|
b, err := p.br.ReadByte()
|
|
if err == nil {
|
|
p.recordAdd([]byte{b})
|
|
}
|
|
return b, err
|
|
}
|
|
|
|
func (p *Proto) xunreadbyte() {
|
|
if p.record {
|
|
p.recordBuf = p.recordBuf[:len(p.recordBuf)-1]
|
|
}
|
|
err := p.br.UnreadByte()
|
|
p.xcheckf(err, "unread byte")
|
|
}
|
|
|
|
func (p *Proto) readrune() (rune, error) {
|
|
x, _, err := p.br.ReadRune()
|
|
if err == nil {
|
|
p.recordAdd([]byte(string(x)))
|
|
}
|
|
return x, err
|
|
}
|
|
|
|
func (p *Proto) space() bool {
|
|
return p.take(' ')
|
|
}
|
|
|
|
func (p *Proto) xspace() {
|
|
p.xtake(" ")
|
|
}
|
|
|
|
func (p *Proto) xcrlf() {
|
|
p.xtake("\r\n")
|
|
}
|
|
|
|
func (p *Proto) peek(exp byte) bool {
|
|
b, err := p.readbyte()
|
|
if err == nil {
|
|
p.xunreadbyte()
|
|
}
|
|
return err == nil && strings.EqualFold(string(rune(b)), string(rune(exp)))
|
|
}
|
|
|
|
func (p *Proto) peekstring() bool {
|
|
return p.peek('"') || p.peek('{')
|
|
}
|
|
|
|
func (p *Proto) take(exp byte) bool {
|
|
if p.peek(exp) {
|
|
_, _ = p.readbyte()
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (p *Proto) xstatus() Status {
|
|
w := p.xword()
|
|
W := strings.ToUpper(w)
|
|
switch W {
|
|
case "OK":
|
|
return OK
|
|
case "NO":
|
|
return NO
|
|
case "BAD":
|
|
return BAD
|
|
}
|
|
p.xerrorf("expected status, got %q", w)
|
|
panic("not reached")
|
|
}
|
|
|
|
// Already consumed: tag SP status SP
|
|
func (p *Proto) xresult(status Status) Result {
|
|
code, text := p.xrespText()
|
|
return Result{status, code, text}
|
|
}
|
|
|
|
func (p *Proto) xrespText() (code Code, text string) {
|
|
if p.take('[') {
|
|
code = p.xrespCode()
|
|
p.xtake("]")
|
|
p.xspace()
|
|
}
|
|
for !p.peek('\r') {
|
|
text += string(rune(p.xbyte()))
|
|
}
|
|
return
|
|
}
|
|
|
|
// ../rfc/9051:6895
|
|
func (p *Proto) xrespCode() Code {
|
|
w := ""
|
|
for !p.peek(' ') && !p.peek(']') {
|
|
w += string(rune(p.xbyte()))
|
|
}
|
|
W := strings.ToUpper(w)
|
|
|
|
switch W {
|
|
case "BADCHARSET":
|
|
var l []string // Must be nil initially.
|
|
if p.space() {
|
|
p.xtake("(")
|
|
l = []string{p.xcharset()}
|
|
for p.space() {
|
|
l = append(l, p.xcharset())
|
|
}
|
|
p.xtake(")")
|
|
}
|
|
return CodeBadCharset(l)
|
|
case "CAPABILITY":
|
|
p.xtake(" ")
|
|
caps := []Capability{}
|
|
for {
|
|
s := p.xatom()
|
|
s = strings.ToUpper(s)
|
|
caps = append(caps, Capability(s))
|
|
if !p.space() {
|
|
break
|
|
}
|
|
}
|
|
return CodeCapability(caps)
|
|
case "PERMANENTFLAGS":
|
|
l := []string{} // Must be non-nil.
|
|
if p.space() {
|
|
p.xtake("(")
|
|
l = []string{p.xflagPerm()}
|
|
for p.space() {
|
|
l = append(l, p.xflagPerm())
|
|
}
|
|
p.xtake(")")
|
|
}
|
|
return CodePermanentFlags(l)
|
|
case "UIDNEXT":
|
|
p.xspace()
|
|
return CodeUIDNext(p.xnzuint32())
|
|
case "UIDVALIDITY":
|
|
p.xspace()
|
|
return CodeUIDValidity(p.xnzuint32())
|
|
case "UNSEEN":
|
|
p.xspace()
|
|
return CodeUnseen(p.xnzuint32())
|
|
case "APPENDUID":
|
|
p.xspace()
|
|
destUIDValidity := p.xnzuint32()
|
|
p.xspace()
|
|
uids := p.xuidrange()
|
|
return CodeAppendUID{destUIDValidity, uids}
|
|
case "COPYUID":
|
|
p.xspace()
|
|
destUIDValidity := p.xnzuint32()
|
|
p.xspace()
|
|
from := p.xuidset()
|
|
p.xspace()
|
|
to := p.xuidset()
|
|
return CodeCopyUID{destUIDValidity, from, to}
|
|
case "HIGHESTMODSEQ":
|
|
p.xspace()
|
|
return CodeHighestModSeq(p.xint64())
|
|
case "MODIFIED":
|
|
p.xspace()
|
|
modified := p.xuidset()
|
|
return CodeModified(NumSet{Ranges: modified})
|
|
case "INPROGRESS":
|
|
// ../rfc/9585:238
|
|
var tag string
|
|
var current, goal *uint32
|
|
if p.space() {
|
|
p.xtake("(")
|
|
tag = p.xquoted()
|
|
p.xspace()
|
|
if p.peek('n') || p.peek('N') {
|
|
p.xtake("nil")
|
|
} else {
|
|
v := p.xuint32()
|
|
current = &v
|
|
}
|
|
p.xspace()
|
|
if p.peek('n') || p.peek('N') {
|
|
p.xtake("nil")
|
|
} else {
|
|
v := p.xnzuint32()
|
|
goal = &v
|
|
}
|
|
p.xtake(")")
|
|
}
|
|
return CodeInProgress{tag, current, goal}
|
|
case "BADEVENT":
|
|
// ../rfc/5465:1033
|
|
p.xspace()
|
|
p.xtake("(")
|
|
var l []string
|
|
for {
|
|
s := p.xatom()
|
|
l = append(l, s)
|
|
if !p.space() {
|
|
break
|
|
}
|
|
}
|
|
p.xtake(")")
|
|
return CodeBadEvent(l)
|
|
|
|
case "METADATA":
|
|
p.xspace()
|
|
if !p.take('(') {
|
|
p.xtake("LONGENTRIES")
|
|
p.xspace()
|
|
num := p.xuint32()
|
|
return CodeMetadataLongEntries(num)
|
|
}
|
|
w := strings.ToUpper(p.xatom())
|
|
switch w {
|
|
case "MAXSIZE":
|
|
p.xspace()
|
|
num := p.xuint32()
|
|
p.xtake(")")
|
|
return CodeMetadataMaxSize(num)
|
|
case "TOOMANY":
|
|
p.xtake(")")
|
|
return CodeMetadataTooMany{}
|
|
case "NOPRIVATE":
|
|
p.xtake(")")
|
|
return CodeMetadataNoPrivate{}
|
|
}
|
|
p.xerrorf("parsing METADATA response code, got %q, expected one of MAXSIZE, TOOMANY, NOPRIVATE", w)
|
|
panic("not reached")
|
|
|
|
// Known codes without parameters.
|
|
case "ALERT",
|
|
"PARSE",
|
|
"READ-ONLY",
|
|
"READ-WRITE",
|
|
"TRYCREATE",
|
|
"UIDNOTSTICKY",
|
|
"UNAVAILABLE",
|
|
"AUTHENTICATIONFAILED",
|
|
"AUTHORIZATIONFAILED",
|
|
"EXPIRED",
|
|
"PRIVACYREQUIRED",
|
|
"CONTACTADMIN",
|
|
"NOPERM",
|
|
"INUSE",
|
|
"EXPUNGEISSUED",
|
|
"CORRUPTION",
|
|
"SERVERBUG",
|
|
"CLIENTBUG",
|
|
"CANNOT",
|
|
"LIMIT",
|
|
"ALREADYEXISTS",
|
|
"NONEXISTENT",
|
|
"NOTSAVED",
|
|
"HASCHILDREN",
|
|
"CLOSED",
|
|
"UNKNOWN-CTE",
|
|
"OVERQUOTA", // ../rfc/9208:472
|
|
"COMPRESSIONACTIVE", // ../rfc/4978:143
|
|
"NOTIFICATIONOVERFLOW", // ../rfc/5465:1023
|
|
"UIDREQUIRED": // ../rfc/9586:136
|
|
return CodeWord(W)
|
|
|
|
default:
|
|
var args []string
|
|
for p.space() {
|
|
arg := ""
|
|
for !p.peek(' ') && !p.peek(']') {
|
|
arg += string(rune(p.xbyte()))
|
|
}
|
|
args = append(args, arg)
|
|
}
|
|
if len(args) == 0 {
|
|
return CodeWord(W)
|
|
}
|
|
return CodeParams{W, args}
|
|
}
|
|
}
|
|
|
|
func (p *Proto) xbyte() byte {
|
|
b, err := p.readbyte()
|
|
p.xcheckf(err, "read byte")
|
|
return b
|
|
}
|
|
|
|
// take until b is seen. don't take b itself.
|
|
func (p *Proto) xtakeuntil(b byte) string {
|
|
var s string
|
|
for {
|
|
x, err := p.readbyte()
|
|
p.xcheckf(err, "read byte")
|
|
if x == b {
|
|
p.xunreadbyte()
|
|
return s
|
|
}
|
|
s += string(rune(x))
|
|
}
|
|
}
|
|
|
|
func (p *Proto) xdigits() string {
|
|
var s string
|
|
for {
|
|
b, err := p.readbyte()
|
|
if err == nil && (b >= '0' && b <= '9') {
|
|
s += string(rune(b))
|
|
continue
|
|
}
|
|
p.xunreadbyte()
|
|
return s
|
|
}
|
|
}
|
|
|
|
func (p *Proto) peekdigit() bool {
|
|
if b, err := p.readbyte(); err == nil {
|
|
p.xunreadbyte()
|
|
return b >= '0' && b <= '9'
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (p *Proto) xint32() int32 {
|
|
s := p.xdigits()
|
|
num, err := strconv.ParseInt(s, 10, 32)
|
|
p.xcheckf(err, "parsing int32")
|
|
return int32(num)
|
|
}
|
|
|
|
func (p *Proto) xint64() int64 {
|
|
s := p.xdigits()
|
|
num, err := strconv.ParseInt(s, 10, 63)
|
|
p.xcheckf(err, "parsing int64")
|
|
return num
|
|
}
|
|
|
|
func (p *Proto) xuint32() uint32 {
|
|
s := p.xdigits()
|
|
num, err := strconv.ParseUint(s, 10, 32)
|
|
p.xcheckf(err, "parsing uint32")
|
|
return uint32(num)
|
|
}
|
|
|
|
func (p *Proto) xnzuint32() uint32 {
|
|
v := p.xuint32()
|
|
if v == 0 {
|
|
p.xerrorf("got 0, expected nonzero uint")
|
|
}
|
|
return v
|
|
}
|
|
|
|
// todo: replace with proper parsing.
|
|
func (p *Proto) xnonspace() string {
|
|
var s string
|
|
for !p.peek(' ') && !p.peek('\r') && !p.peek('\n') {
|
|
s += string(rune(p.xbyte()))
|
|
}
|
|
if s == "" {
|
|
p.xerrorf("expected non-space")
|
|
}
|
|
return s
|
|
}
|
|
|
|
// todo: replace with proper parsing
|
|
func (p *Proto) xword() string {
|
|
return p.xatom()
|
|
}
|
|
|
|
// "*" SP is already consumed
|
|
// ../rfc/9051:6868
|
|
func (p *Proto) xuntagged() Untagged {
|
|
w := p.xnonspace()
|
|
W := strings.ToUpper(w)
|
|
switch W {
|
|
case "PREAUTH":
|
|
p.xspace()
|
|
code, text := p.xrespText()
|
|
r := UntaggedPreauth{code, text}
|
|
p.xcrlf()
|
|
return r
|
|
|
|
case "BYE":
|
|
p.xspace()
|
|
code, text := p.xrespText()
|
|
r := UntaggedBye{code, text}
|
|
p.xcrlf()
|
|
return r
|
|
|
|
case "OK", "NO", "BAD":
|
|
p.xspace()
|
|
r := UntaggedResult(p.xresult(Status(W)))
|
|
p.xcrlf()
|
|
return r
|
|
|
|
case "CAPABILITY":
|
|
// ../rfc/9051:6427
|
|
var caps []Capability
|
|
for p.space() {
|
|
s := p.xnonspace()
|
|
s = strings.ToUpper(s)
|
|
cc := Capability(s)
|
|
caps = append(caps, cc)
|
|
}
|
|
p.xcrlf()
|
|
return UntaggedCapability(caps)
|
|
|
|
case "ENABLED":
|
|
// ../rfc/9051:6520
|
|
var caps []Capability
|
|
for p.space() {
|
|
s := p.xnonspace()
|
|
s = strings.ToUpper(s)
|
|
cc := Capability(s)
|
|
caps = append(caps, cc)
|
|
}
|
|
p.xcrlf()
|
|
return UntaggedEnabled(caps)
|
|
|
|
case "FLAGS":
|
|
p.xspace()
|
|
r := UntaggedFlags(p.xflagList())
|
|
p.xcrlf()
|
|
return r
|
|
|
|
case "LIST":
|
|
p.xspace()
|
|
r := p.xmailboxList()
|
|
p.xcrlf()
|
|
return r
|
|
|
|
case "STATUS":
|
|
// ../rfc/9051:6681
|
|
p.xspace()
|
|
mailbox := p.xastring()
|
|
p.xspace()
|
|
p.xtake("(")
|
|
attrs := map[StatusAttr]int64{}
|
|
for !p.take(')') {
|
|
if len(attrs) > 0 {
|
|
p.xspace()
|
|
}
|
|
s := p.xatom()
|
|
p.xspace()
|
|
S := StatusAttr(strings.ToUpper(s))
|
|
var num int64
|
|
// ../rfc/9051:7059
|
|
switch S {
|
|
case "MESSAGES":
|
|
num = int64(p.xuint32())
|
|
case "UIDNEXT":
|
|
num = int64(p.xnzuint32())
|
|
case "UIDVALIDITY":
|
|
num = int64(p.xnzuint32())
|
|
case "UNSEEN":
|
|
num = int64(p.xuint32())
|
|
case "DELETED":
|
|
num = int64(p.xuint32())
|
|
case "SIZE":
|
|
num = p.xint64()
|
|
case "RECENT":
|
|
num = int64(p.xuint32())
|
|
case "APPENDLIMIT":
|
|
if p.peek('n') || p.peek('N') {
|
|
p.xtake("nil")
|
|
} else {
|
|
num = p.xint64()
|
|
}
|
|
case "HIGHESTMODSEQ":
|
|
num = p.xint64()
|
|
case "DELETED-STORAGE":
|
|
num = p.xint64()
|
|
default:
|
|
p.xerrorf("status: unknown attribute %q", s)
|
|
}
|
|
if _, ok := attrs[S]; ok {
|
|
p.xerrorf("status: duplicate attribute %q", s)
|
|
}
|
|
attrs[S] = num
|
|
}
|
|
r := UntaggedStatus{mailbox, attrs}
|
|
p.xcrlf()
|
|
return r
|
|
|
|
case "METADATA":
|
|
// ../rfc/5464:807
|
|
p.xspace()
|
|
mailbox := p.xastring()
|
|
p.xspace()
|
|
if !p.take('(') {
|
|
// Unsolicited form, with only annotation keys, not values.
|
|
var keys []string
|
|
for {
|
|
key := p.xastring()
|
|
keys = append(keys, key)
|
|
if !p.space() {
|
|
break
|
|
}
|
|
}
|
|
p.xcrlf()
|
|
return UntaggedMetadataKeys{mailbox, keys}
|
|
}
|
|
|
|
// Form with values, in response to GETMETADATA command.
|
|
r := UntaggedMetadataAnnotations{Mailbox: mailbox}
|
|
for {
|
|
key := p.xastring()
|
|
p.xspace()
|
|
var value []byte
|
|
var isString bool
|
|
if p.take('~') {
|
|
value = p.xliteral()
|
|
} else if p.peek('"') {
|
|
value = []byte(p.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.
|
|
p.xtake("nil")
|
|
}
|
|
r.Annotations = append(r.Annotations, Annotation{key, isString, value})
|
|
|
|
if p.take(')') {
|
|
break
|
|
}
|
|
p.xspace()
|
|
}
|
|
p.xcrlf()
|
|
return r
|
|
|
|
case "NAMESPACE":
|
|
// ../rfc/9051:6778
|
|
p.xspace()
|
|
personal := p.xnamespace()
|
|
p.xspace()
|
|
other := p.xnamespace()
|
|
p.xspace()
|
|
shared := p.xnamespace()
|
|
r := UntaggedNamespace{personal, other, shared}
|
|
p.xcrlf()
|
|
return r
|
|
|
|
case "SEARCH":
|
|
// ../rfc/9051:6809
|
|
var nums []uint32
|
|
for p.space() {
|
|
// ../rfc/7162:2557
|
|
if p.take('(') {
|
|
p.xtake("MODSEQ")
|
|
p.xspace()
|
|
modseq := p.xint64()
|
|
p.xtake(")")
|
|
p.xcrlf()
|
|
return UntaggedSearchModSeq{nums, modseq}
|
|
}
|
|
nums = append(nums, p.xnzuint32())
|
|
}
|
|
r := UntaggedSearch(nums)
|
|
p.xcrlf()
|
|
return r
|
|
|
|
case "ESEARCH":
|
|
r := p.xesearchResponse()
|
|
p.xcrlf()
|
|
return r
|
|
|
|
case "LSUB":
|
|
r := p.xlsub()
|
|
p.xcrlf()
|
|
return r
|
|
|
|
case "ID":
|
|
// ../rfc/2971:243
|
|
p.xspace()
|
|
var params map[string]string
|
|
if p.take('(') {
|
|
params = map[string]string{}
|
|
for !p.take(')') {
|
|
if len(params) > 0 {
|
|
p.xspace()
|
|
}
|
|
k := p.xstring()
|
|
p.xspace()
|
|
v := p.xnilString()
|
|
if _, ok := params[k]; ok {
|
|
p.xerrorf("duplicate key %q", k)
|
|
}
|
|
params[k] = v
|
|
}
|
|
} else {
|
|
p.xtake("nil")
|
|
}
|
|
p.xcrlf()
|
|
return UntaggedID(params)
|
|
|
|
// ../rfc/7162:2623
|
|
case "VANISHED":
|
|
p.xspace()
|
|
var earlier bool
|
|
if p.take('(') {
|
|
p.xtake("EARLIER")
|
|
p.xtake(")")
|
|
p.xspace()
|
|
earlier = true
|
|
}
|
|
uids := p.xuidset()
|
|
p.xcrlf()
|
|
return UntaggedVanished{earlier, NumSet{Ranges: uids}}
|
|
|
|
// ../rfc/9208:668 ../2087:242
|
|
case "QUOTAROOT":
|
|
p.xspace()
|
|
p.xastring()
|
|
var roots []string
|
|
for p.space() {
|
|
root := p.xastring()
|
|
roots = append(roots, root)
|
|
}
|
|
p.xcrlf()
|
|
return UntaggedQuotaroot(roots)
|
|
|
|
// ../rfc/9208:666 ../rfc/2087:239
|
|
case "QUOTA":
|
|
p.xspace()
|
|
root := p.xastring()
|
|
p.xspace()
|
|
p.xtake("(")
|
|
|
|
xresource := func() QuotaResource {
|
|
name := p.xatom()
|
|
p.xspace()
|
|
usage := p.xint64()
|
|
p.xspace()
|
|
limit := p.xint64()
|
|
return QuotaResource{QuotaResourceName(strings.ToUpper(name)), usage, limit}
|
|
}
|
|
|
|
seen := map[QuotaResourceName]bool{}
|
|
l := []QuotaResource{xresource()}
|
|
seen[l[0].Name] = true
|
|
for p.space() {
|
|
res := xresource()
|
|
if seen[res.Name] {
|
|
p.xerrorf("duplicate resource name %q", res.Name)
|
|
}
|
|
seen[res.Name] = true
|
|
l = append(l, res)
|
|
}
|
|
p.xtake(")")
|
|
p.xcrlf()
|
|
return UntaggedQuota{root, l}
|
|
|
|
default:
|
|
v, err := strconv.ParseUint(w, 10, 32)
|
|
if err == nil {
|
|
num := uint32(v)
|
|
p.xspace()
|
|
w = p.xword()
|
|
W = strings.ToUpper(w)
|
|
switch W {
|
|
case "FETCH", "UIDFETCH":
|
|
if num == 0 {
|
|
p.xerrorf("invalid zero number for untagged fetch response")
|
|
}
|
|
p.xspace()
|
|
attrs := p.xfetch()
|
|
p.xcrlf()
|
|
if W == "UIDFETCH" {
|
|
return UntaggedUIDFetch{num, attrs}
|
|
}
|
|
return UntaggedFetch{num, attrs}
|
|
|
|
case "EXPUNGE":
|
|
if num == 0 {
|
|
p.xerrorf("invalid zero number for untagged expunge response")
|
|
}
|
|
p.xcrlf()
|
|
return UntaggedExpunge(num)
|
|
|
|
case "EXISTS":
|
|
p.xcrlf()
|
|
return UntaggedExists(num)
|
|
|
|
case "RECENT":
|
|
p.xcrlf()
|
|
return UntaggedRecent(num)
|
|
|
|
default:
|
|
p.xerrorf("unknown untagged numbered response %q", w)
|
|
panic("not reached")
|
|
}
|
|
}
|
|
p.xerrorf("unknown untagged response %q", w)
|
|
}
|
|
panic("not reached")
|
|
}
|
|
|
|
// ../rfc/3501:4864 ../rfc/9051:6742
|
|
// Already parsed: "*" SP nznumber SP "FETCH" SP
|
|
func (p *Proto) xfetch() []FetchAttr {
|
|
p.xtake("(")
|
|
attrs := []FetchAttr{p.xmsgatt1()}
|
|
for p.space() {
|
|
attrs = append(attrs, p.xmsgatt1())
|
|
}
|
|
p.xtake(")")
|
|
return attrs
|
|
}
|
|
|
|
// ../rfc/9051:6746
|
|
func (p *Proto) xmsgatt1() FetchAttr {
|
|
f := ""
|
|
for {
|
|
b := p.xbyte()
|
|
if b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b >= '0' && b <= '9' || b == '.' {
|
|
f += string(rune(b))
|
|
continue
|
|
}
|
|
p.xunreadbyte()
|
|
break
|
|
}
|
|
|
|
F := strings.ToUpper(f)
|
|
switch F {
|
|
case "FLAGS":
|
|
p.xspace()
|
|
p.xtake("(")
|
|
var flags []string
|
|
if !p.take(')') {
|
|
flags = []string{p.xflag()}
|
|
for p.space() {
|
|
flags = append(flags, p.xflag())
|
|
}
|
|
p.xtake(")")
|
|
}
|
|
return FetchFlags(flags)
|
|
|
|
case "ENVELOPE":
|
|
p.xspace()
|
|
return FetchEnvelope(p.xenvelope())
|
|
|
|
case "INTERNALDATE":
|
|
p.xspace()
|
|
s := p.xquoted()
|
|
v, err := time.Parse("_2-Jan-2006 15:04:05 -0700", s)
|
|
p.xcheckf(err, "parsing internaldate")
|
|
return FetchInternalDate{v}
|
|
|
|
case "SAVEDATE":
|
|
p.xspace()
|
|
var t *time.Time
|
|
if p.peek('"') {
|
|
s := p.xquoted()
|
|
v, err := time.Parse("_2-Jan-2006 15:04:05 -0700", s)
|
|
p.xcheckf(err, "parsing savedate")
|
|
t = &v
|
|
} else {
|
|
p.xtake("nil")
|
|
}
|
|
return FetchSaveDate{t}
|
|
|
|
case "RFC822.SIZE":
|
|
p.xspace()
|
|
return FetchRFC822Size(p.xint64())
|
|
|
|
case "RFC822":
|
|
p.xspace()
|
|
s := p.xnilString()
|
|
return FetchRFC822(s)
|
|
|
|
case "RFC822.HEADER":
|
|
p.xspace()
|
|
s := p.xnilString()
|
|
return FetchRFC822Header(s)
|
|
|
|
case "RFC822.TEXT":
|
|
p.xspace()
|
|
s := p.xnilString()
|
|
return FetchRFC822Text(s)
|
|
|
|
case "BODY":
|
|
if p.space() {
|
|
return FetchBodystructure{F, p.xbodystructure(false)}
|
|
}
|
|
p.record = true
|
|
section := p.xsection()
|
|
var offset int32
|
|
if p.take('<') {
|
|
offset = p.xint32()
|
|
p.xtake(">")
|
|
}
|
|
F += p.recorded()
|
|
p.xspace()
|
|
body := p.xnilString()
|
|
return FetchBody{F, section, offset, body}
|
|
|
|
case "BODYSTRUCTURE":
|
|
p.xspace()
|
|
return FetchBodystructure{F, p.xbodystructure(true)}
|
|
|
|
case "BINARY":
|
|
p.record = true
|
|
nums := p.xsectionBinary()
|
|
F += p.recorded()
|
|
p.xspace()
|
|
buf := p.xnilStringLiteral8()
|
|
return FetchBinary{F, nums, string(buf)}
|
|
|
|
case "BINARY.SIZE":
|
|
p.record = true
|
|
nums := p.xsectionBinary()
|
|
F += p.recorded()
|
|
p.xspace()
|
|
size := p.xint64()
|
|
return FetchBinarySize{F, nums, size}
|
|
|
|
case "UID":
|
|
p.xspace()
|
|
return FetchUID(p.xuint32())
|
|
|
|
case "MODSEQ":
|
|
// ../rfc/7162:2488
|
|
p.xspace()
|
|
p.xtake("(")
|
|
modseq := p.xint64()
|
|
p.xtake(")")
|
|
return FetchModSeq(modseq)
|
|
|
|
case "PREVIEW":
|
|
// ../rfc/8970:348
|
|
p.xspace()
|
|
var preview *string
|
|
if p.peek('n') || p.peek('N') {
|
|
p.xtake("nil")
|
|
} else {
|
|
s := p.xstring()
|
|
preview = &s
|
|
}
|
|
return FetchPreview{preview}
|
|
}
|
|
p.xerrorf("unknown fetch attribute %q", f)
|
|
panic("not reached")
|
|
}
|
|
|
|
func (p *Proto) xnilString() string {
|
|
if p.peek('"') {
|
|
return p.xquoted()
|
|
} else if p.peek('{') {
|
|
return string(p.xliteral())
|
|
} else {
|
|
p.xtake("nil")
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func ptr[T any](v T) *T {
|
|
return &v
|
|
}
|
|
|
|
func (p *Proto) xnilptrString() *string {
|
|
if p.peek('"') {
|
|
return ptr(p.xquoted())
|
|
} else if p.peek('{') {
|
|
return ptr(string(p.xliteral()))
|
|
} else {
|
|
p.xtake("nil")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (p *Proto) xstring() string {
|
|
if p.peek('"') {
|
|
return p.xquoted()
|
|
}
|
|
return string(p.xliteral())
|
|
}
|
|
|
|
func (p *Proto) xastring() string {
|
|
if p.peek('"') {
|
|
return p.xquoted()
|
|
} else if p.peek('{') {
|
|
return string(p.xliteral())
|
|
}
|
|
return p.xatom()
|
|
}
|
|
|
|
func (p *Proto) xatom() string {
|
|
var s string
|
|
for {
|
|
b, err := p.readbyte()
|
|
p.xcheckf(err, "read byte for atom")
|
|
if b <= ' ' || strings.IndexByte("(){%*\"\\]", b) >= 0 {
|
|
p.xunreadbyte()
|
|
if s == "" {
|
|
p.xerrorf("expected atom")
|
|
}
|
|
return s
|
|
}
|
|
s += string(rune(b))
|
|
}
|
|
}
|
|
|
|
// ../rfc/9051:6856 ../rfc/6855:153
|
|
func (p *Proto) xquoted() string {
|
|
p.xtake(`"`)
|
|
s := ""
|
|
for !p.take('"') {
|
|
r, err := p.readrune()
|
|
p.xcheckf(err, "reading rune in quoted string")
|
|
if r == '\\' {
|
|
r, err = p.readrune()
|
|
p.xcheckf(err, "reading escaped char in quoted string")
|
|
if r != '\\' && r != '"' {
|
|
p.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 (p *Proto) xliteral() []byte {
|
|
p.xtake("{")
|
|
size := p.xint64()
|
|
sync := p.take('+')
|
|
p.xtake("}")
|
|
p.xcrlf()
|
|
// todo: for some literals, read as tracedata
|
|
if size > 1<<20 {
|
|
p.xerrorf("refusing to read more than 1MB: %d", size)
|
|
}
|
|
if sync {
|
|
if p.xbw == nil {
|
|
p.xerrorf("cannot parse literals without connection")
|
|
}
|
|
fmt.Fprintf(p.xbw, "+ ok\r\n")
|
|
p.xflush()
|
|
}
|
|
buf := make([]byte, int(size))
|
|
defer p.xtraceread(mlog.LevelTracedata)()
|
|
_, err := io.ReadFull(p.br, buf)
|
|
p.xcheckf(err, "reading data for literal")
|
|
p.xtraceread(mlog.LevelTrace)
|
|
return buf
|
|
}
|
|
|
|
// ../rfc/9051:6565
|
|
// todo: stricter
|
|
func (p *Proto) xflag0(allowPerm bool) string {
|
|
s := ""
|
|
if p.take('\\') {
|
|
s = `\`
|
|
if allowPerm && p.take('*') {
|
|
return `\*`
|
|
}
|
|
} else if p.take('$') {
|
|
s = "$"
|
|
}
|
|
s += p.xatom()
|
|
return s
|
|
}
|
|
|
|
func (p *Proto) xflag() string {
|
|
return p.xflag0(false)
|
|
}
|
|
|
|
func (p *Proto) xflagPerm() string {
|
|
return p.xflag0(true)
|
|
}
|
|
|
|
func (p *Proto) xsection() string {
|
|
p.xtake("[")
|
|
s := p.xtakeuntil(']')
|
|
p.xtake("]")
|
|
return s
|
|
}
|
|
|
|
func (p *Proto) xsectionBinary() []uint32 {
|
|
p.xtake("[")
|
|
var nums []uint32
|
|
for !p.take(']') {
|
|
if len(nums) > 0 {
|
|
p.xtake(".")
|
|
}
|
|
nums = append(nums, p.xnzuint32())
|
|
}
|
|
return nums
|
|
}
|
|
|
|
func (p *Proto) xnilStringLiteral8() []byte {
|
|
// todo: should make difference for literal8 and literal from string, which bytes are allowed
|
|
if p.take('~') || p.peek('{') {
|
|
return p.xliteral()
|
|
}
|
|
return []byte(p.xnilString())
|
|
}
|
|
|
|
// ../rfc/9051:6355
|
|
func (p *Proto) xbodystructure(extensibleForm bool) any {
|
|
p.xtake("(")
|
|
if p.peek('(') {
|
|
// ../rfc/9051:6411
|
|
parts := []any{p.xbodystructure(extensibleForm)}
|
|
for p.peek('(') {
|
|
parts = append(parts, p.xbodystructure(extensibleForm))
|
|
}
|
|
p.xspace()
|
|
mediaSubtype := p.xstring()
|
|
var ext *BodyExtensionMpart
|
|
if extensibleForm && p.space() {
|
|
ext = p.xbodyExtMpart()
|
|
}
|
|
p.xtake(")")
|
|
return BodyTypeMpart{parts, mediaSubtype, ext}
|
|
}
|
|
|
|
// todo: verify the media(sub)type is valid for returned data.
|
|
|
|
var ext *BodyExtension1Part
|
|
mediaType := p.xstring()
|
|
p.xspace()
|
|
mediaSubtype := p.xstring()
|
|
p.xspace()
|
|
bodyFields := p.xbodyFields()
|
|
if !p.space() {
|
|
// Basic type without extension.
|
|
p.xtake(")")
|
|
return BodyTypeBasic{mediaType, mediaSubtype, bodyFields, nil}
|
|
}
|
|
if p.peek('(') {
|
|
// ../rfc/9051:6415
|
|
envelope := p.xenvelope()
|
|
p.xspace()
|
|
bodyStructure := p.xbodystructure(extensibleForm)
|
|
p.xspace()
|
|
lines := p.xint64()
|
|
if extensibleForm && p.space() {
|
|
ext = p.xbodyExt1Part()
|
|
}
|
|
p.xtake(")")
|
|
return BodyTypeMsg{mediaType, mediaSubtype, bodyFields, envelope, bodyStructure, lines, ext}
|
|
}
|
|
if !strings.EqualFold(mediaType, "text") {
|
|
if !extensibleForm {
|
|
p.xerrorf("body result, basic type, with disallowed extensible form")
|
|
}
|
|
ext = p.xbodyExt1Part()
|
|
// ../rfc/9051:6407
|
|
p.xtake(")")
|
|
return BodyTypeBasic{mediaType, mediaSubtype, bodyFields, ext}
|
|
}
|
|
// ../rfc/9051:6418
|
|
lines := p.xint64()
|
|
if extensibleForm && p.space() {
|
|
ext = p.xbodyExt1Part()
|
|
}
|
|
p.xtake(")")
|
|
return BodyTypeText{mediaType, mediaSubtype, bodyFields, lines, ext}
|
|
}
|
|
|
|
// ../rfc/9051:6376 ../rfc/3501:4604
|
|
func (p *Proto) xbodyFields() BodyFields {
|
|
params := p.xbodyFldParam()
|
|
p.xspace()
|
|
contentID := p.xnilString()
|
|
p.xspace()
|
|
contentDescr := p.xnilString()
|
|
p.xspace()
|
|
cte := p.xnilString()
|
|
p.xspace()
|
|
octets := p.xint32()
|
|
return BodyFields{params, contentID, contentDescr, cte, octets}
|
|
}
|
|
|
|
// ../rfc/9051:6371 ../rfc/3501:4599
|
|
func (p *Proto) xbodyExtMpart() (ext *BodyExtensionMpart) {
|
|
ext = &BodyExtensionMpart{}
|
|
ext.Params = p.xbodyFldParam()
|
|
if !p.space() {
|
|
return
|
|
}
|
|
disp, dispParams := p.xbodyFldDsp()
|
|
ext.Disposition, ext.DispositionParams = &disp, &dispParams
|
|
if !p.space() {
|
|
return
|
|
}
|
|
ext.Language = ptr(p.xbodyFldLang())
|
|
if !p.space() {
|
|
return
|
|
}
|
|
ext.Location = ptr(p.xbodyFldLoc())
|
|
for p.space() {
|
|
ext.More = append(ext.More, p.xbodyExtension())
|
|
}
|
|
return
|
|
}
|
|
|
|
// ../rfc/9051:6366 ../rfc/3501:4584
|
|
func (p *Proto) xbodyExt1Part() (ext *BodyExtension1Part) {
|
|
ext = &BodyExtension1Part{}
|
|
ext.MD5 = p.xnilptrString()
|
|
if !p.space() {
|
|
return
|
|
}
|
|
disp, dispParams := p.xbodyFldDsp()
|
|
ext.Disposition, ext.DispositionParams = &disp, &dispParams
|
|
if !p.space() {
|
|
return
|
|
}
|
|
ext.Language = ptr(p.xbodyFldLang())
|
|
if !p.space() {
|
|
return
|
|
}
|
|
ext.Location = ptr(p.xbodyFldLoc())
|
|
for p.space() {
|
|
ext.More = append(ext.More, p.xbodyExtension())
|
|
}
|
|
return
|
|
}
|
|
|
|
// ../rfc/9051:6401 ../rfc/3501:4626
|
|
func (p *Proto) xbodyFldParam() [][2]string {
|
|
if p.take('(') {
|
|
k := p.xstring()
|
|
p.xspace()
|
|
v := p.xstring()
|
|
l := [][2]string{{k, v}}
|
|
for p.space() {
|
|
k = p.xstring()
|
|
p.xspace()
|
|
v = p.xstring()
|
|
l = append(l, [2]string{k, v})
|
|
}
|
|
p.xtake(")")
|
|
return l
|
|
}
|
|
p.xtake("nil")
|
|
return nil
|
|
}
|
|
|
|
// ../rfc/9051:6381 ../rfc/3501:4609
|
|
func (p *Proto) xbodyFldDsp() (*string, [][2]string) {
|
|
if !p.take('(') {
|
|
p.xtake("nil")
|
|
return nil, nil
|
|
}
|
|
disposition := p.xstring()
|
|
p.xspace()
|
|
param := p.xbodyFldParam()
|
|
p.xtake(")")
|
|
return ptr(disposition), param
|
|
}
|
|
|
|
// ../rfc/9051:6391 ../rfc/3501:4616
|
|
func (p *Proto) xbodyFldLang() (lang []string) {
|
|
if p.take('(') {
|
|
lang = []string{p.xstring()}
|
|
for p.space() {
|
|
lang = append(lang, p.xstring())
|
|
}
|
|
p.xtake(")")
|
|
return lang
|
|
}
|
|
if p.peekstring() {
|
|
return []string{p.xstring()}
|
|
}
|
|
p.xtake("nil")
|
|
return nil
|
|
}
|
|
|
|
// ../rfc/9051:6393 ../rfc/3501:4618
|
|
func (p *Proto) xbodyFldLoc() *string {
|
|
return p.xnilptrString()
|
|
}
|
|
|
|
// ../rfc/9051:6357 ../rfc/3501:4575
|
|
func (p *Proto) xbodyExtension() (ext BodyExtension) {
|
|
if p.take('(') {
|
|
for {
|
|
ext.More = append(ext.More, p.xbodyExtension())
|
|
if !p.space() {
|
|
break
|
|
}
|
|
}
|
|
p.xtake(")")
|
|
} else if p.peekdigit() {
|
|
num := p.xint64()
|
|
ext.Number = &num
|
|
} else if p.peekstring() {
|
|
str := p.xstring()
|
|
ext.String = &str
|
|
} else {
|
|
p.xtake("nil")
|
|
}
|
|
return ext
|
|
}
|
|
|
|
// ../rfc/9051:6522
|
|
func (p *Proto) xenvelope() Envelope {
|
|
p.xtake("(")
|
|
date := p.xnilString()
|
|
p.xspace()
|
|
subject := p.xnilString()
|
|
p.xspace()
|
|
from := p.xaddresses()
|
|
p.xspace()
|
|
sender := p.xaddresses()
|
|
p.xspace()
|
|
replyTo := p.xaddresses()
|
|
p.xspace()
|
|
to := p.xaddresses()
|
|
p.xspace()
|
|
cc := p.xaddresses()
|
|
p.xspace()
|
|
bcc := p.xaddresses()
|
|
p.xspace()
|
|
inReplyTo := p.xnilString()
|
|
p.xspace()
|
|
messageID := p.xnilString()
|
|
p.xtake(")")
|
|
return Envelope{date, subject, from, sender, replyTo, to, cc, bcc, inReplyTo, messageID}
|
|
}
|
|
|
|
// ../rfc/9051:6526
|
|
func (p *Proto) xaddresses() []Address {
|
|
if !p.take('(') {
|
|
p.xtake("nil")
|
|
return nil
|
|
}
|
|
l := []Address{p.xaddress()}
|
|
for !p.take(')') {
|
|
l = append(l, p.xaddress())
|
|
}
|
|
return l
|
|
}
|
|
|
|
// ../rfc/9051:6303
|
|
func (p *Proto) xaddress() Address {
|
|
p.xtake("(")
|
|
name := p.xnilString()
|
|
p.xspace()
|
|
adl := p.xnilString()
|
|
p.xspace()
|
|
mailbox := p.xnilString()
|
|
p.xspace()
|
|
host := p.xnilString()
|
|
p.xtake(")")
|
|
return Address{name, adl, mailbox, host}
|
|
}
|
|
|
|
// ../rfc/9051:6584
|
|
func (p *Proto) xflagList() []string {
|
|
p.xtake("(")
|
|
var l []string
|
|
if !p.take(')') {
|
|
l = []string{p.xflag()}
|
|
for p.space() {
|
|
l = append(l, p.xflag())
|
|
}
|
|
p.xtake(")")
|
|
}
|
|
return l
|
|
}
|
|
|
|
// ../rfc/9051:6690
|
|
func (p *Proto) xmailboxList() UntaggedList {
|
|
p.xtake("(")
|
|
var flags []string
|
|
if !p.peek(')') {
|
|
flags = append(flags, p.xflag())
|
|
for p.space() {
|
|
flags = append(flags, p.xflag())
|
|
}
|
|
}
|
|
p.xtake(")")
|
|
p.xspace()
|
|
var quoted string
|
|
var b byte
|
|
if p.peek('"') {
|
|
quoted = p.xquoted()
|
|
if len(quoted) != 1 {
|
|
p.xerrorf("mailbox-list has multichar quoted part: %q", quoted)
|
|
}
|
|
b = byte(quoted[0])
|
|
} else if !p.peek(' ') {
|
|
p.xtake("nil")
|
|
}
|
|
p.xspace()
|
|
mailbox := p.xastring()
|
|
ul := UntaggedList{flags, b, mailbox, nil, ""}
|
|
if p.space() {
|
|
p.xtake("(")
|
|
if !p.peek(')') {
|
|
p.xmboxListExtendedItem(&ul)
|
|
for p.space() {
|
|
p.xmboxListExtendedItem(&ul)
|
|
}
|
|
}
|
|
p.xtake(")")
|
|
}
|
|
return ul
|
|
}
|
|
|
|
// ../rfc/9051:6699
|
|
func (p *Proto) xmboxListExtendedItem(ul *UntaggedList) {
|
|
tag := p.xastring()
|
|
p.xspace()
|
|
if strings.ToUpper(tag) == "OLDNAME" {
|
|
// ../rfc/9051:6811
|
|
p.xtake("(")
|
|
name := p.xastring()
|
|
p.xtake(")")
|
|
ul.OldName = name
|
|
return
|
|
}
|
|
val := p.xtaggedExtVal()
|
|
ul.Extended = append(ul.Extended, MboxListExtendedItem{tag, val})
|
|
}
|
|
|
|
// ../rfc/9051:7111
|
|
func (p *Proto) xtaggedExtVal() TaggedExtVal {
|
|
if p.take('(') {
|
|
var r TaggedExtVal
|
|
if !p.take(')') {
|
|
comp := p.xtaggedExtComp()
|
|
r.Comp = &comp
|
|
p.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 := p.readbyte()
|
|
p.xcheckf(err, "read byte for tagged-ext-val")
|
|
if b < '0' || b > '9' {
|
|
p.xunreadbyte()
|
|
ss := p.xsequenceSet()
|
|
return TaggedExtVal{SeqSet: &ss}
|
|
}
|
|
s := p.xdigits()
|
|
num, err := strconv.ParseInt(s, 10, 63)
|
|
p.xcheckf(err, "parsing int")
|
|
if !p.peek(':') && !p.peek(',') {
|
|
// not a larger sequence-set
|
|
return TaggedExtVal{Number: &num}
|
|
}
|
|
var sr NumRange
|
|
sr.First = uint32(num)
|
|
if p.take(':') {
|
|
var num uint32
|
|
if !p.take('*') {
|
|
num = p.xnzuint32()
|
|
}
|
|
sr.Last = &num
|
|
}
|
|
ss := p.xsequenceSet()
|
|
ss.Ranges = append([]NumRange{sr}, ss.Ranges...)
|
|
return TaggedExtVal{SeqSet: &ss}
|
|
}
|
|
|
|
// ../rfc/9051:7034
|
|
func (p *Proto) xsequenceSet() NumSet {
|
|
if p.take('$') {
|
|
return NumSet{SearchResult: true}
|
|
}
|
|
var ss NumSet
|
|
for {
|
|
var sr NumRange
|
|
if !p.take('*') {
|
|
sr.First = p.xnzuint32()
|
|
}
|
|
if p.take(':') {
|
|
var num uint32
|
|
if !p.take('*') {
|
|
num = p.xnzuint32()
|
|
}
|
|
sr.Last = &num
|
|
}
|
|
ss.Ranges = append(ss.Ranges, sr)
|
|
if !p.take(',') {
|
|
break
|
|
}
|
|
}
|
|
return ss
|
|
}
|
|
|
|
// ../rfc/9051:7097
|
|
func (p *Proto) xtaggedExtComp() TaggedExtComp {
|
|
if p.take('(') {
|
|
r := p.xtaggedExtComp()
|
|
p.xtake(")")
|
|
return TaggedExtComp{Comps: []TaggedExtComp{r}}
|
|
}
|
|
s := p.xastring()
|
|
if !p.peek(' ') {
|
|
return TaggedExtComp{String: s}
|
|
}
|
|
l := []TaggedExtComp{{String: s}}
|
|
for p.space() {
|
|
l = append(l, p.xtaggedExtComp())
|
|
}
|
|
return TaggedExtComp{Comps: l}
|
|
}
|
|
|
|
// ../rfc/9051:6765
|
|
func (p *Proto) xnamespace() []NamespaceDescr {
|
|
if !p.take('(') {
|
|
p.xtake("nil")
|
|
return nil
|
|
}
|
|
|
|
l := []NamespaceDescr{p.xnamespaceDescr()}
|
|
for !p.take(')') {
|
|
l = append(l, p.xnamespaceDescr())
|
|
}
|
|
return l
|
|
}
|
|
|
|
// ../rfc/9051:6769
|
|
func (p *Proto) xnamespaceDescr() NamespaceDescr {
|
|
p.xtake("(")
|
|
prefix := p.xstring()
|
|
p.xspace()
|
|
var b byte
|
|
if p.peek('"') {
|
|
s := p.xquoted()
|
|
if len(s) != 1 {
|
|
p.xerrorf("namespace-descr: expected single char, got %q", s)
|
|
}
|
|
b = byte(s[0])
|
|
} else {
|
|
p.xtake("nil")
|
|
}
|
|
var exts []NamespaceExtension
|
|
for !p.take(')') {
|
|
p.xspace()
|
|
key := p.xstring()
|
|
p.xspace()
|
|
p.xtake("(")
|
|
values := []string{p.xstring()}
|
|
for p.space() {
|
|
values = append(values, p.xstring())
|
|
}
|
|
p.xtake(")")
|
|
exts = append(exts, NamespaceExtension{key, values})
|
|
}
|
|
return NamespaceDescr{prefix, b, exts}
|
|
}
|
|
|
|
// ../rfc/9051:6546
|
|
// Already consumed: "ESEARCH"
|
|
func (p *Proto) xesearchResponse() (r UntaggedEsearch) {
|
|
if !p.space() {
|
|
return
|
|
}
|
|
|
|
if p.take('(') {
|
|
// ../rfc/9051:6921 ../rfc/7377:465
|
|
seen := map[string]bool{}
|
|
for {
|
|
var kind string
|
|
if p.peek('t') || p.peek('T') {
|
|
kind = "TAG"
|
|
p.xtake(kind)
|
|
p.xspace()
|
|
r.Tag = p.xastring()
|
|
} else if p.peek('m') || p.peek('M') {
|
|
kind = "MAILBOX"
|
|
p.xtake(kind)
|
|
p.xspace()
|
|
r.Mailbox = p.xastring()
|
|
if r.Mailbox == "" {
|
|
p.xerrorf("invalid empty mailbox in search correlator")
|
|
}
|
|
} else if p.peek('u') || p.peek('U') {
|
|
kind = "UIDVALIDITY"
|
|
p.xtake(kind)
|
|
p.xspace()
|
|
r.UIDValidity = p.xnzuint32()
|
|
} else {
|
|
p.xerrorf("expected tag/correlator, mailbox or uidvalidity")
|
|
}
|
|
|
|
if seen[kind] {
|
|
p.xerrorf("duplicate search correlator %q", kind)
|
|
}
|
|
seen[kind] = true
|
|
|
|
if !p.take(' ') {
|
|
break
|
|
}
|
|
}
|
|
|
|
if r.Tag == "" {
|
|
p.xerrorf("missing tag search correlator")
|
|
}
|
|
if (r.Mailbox != "") != (r.UIDValidity != 0) {
|
|
p.xerrorf("mailbox and uidvalidity correlators must both be absent or both be present")
|
|
}
|
|
|
|
p.xtake(")")
|
|
}
|
|
if !p.space() {
|
|
return
|
|
}
|
|
w := p.xnonspace()
|
|
W := strings.ToUpper(w)
|
|
if W == "UID" {
|
|
r.UID = true
|
|
if !p.space() {
|
|
return
|
|
}
|
|
w = p.xnonspace()
|
|
W = strings.ToUpper(w)
|
|
}
|
|
for {
|
|
// ../rfc/9051:6957
|
|
switch W {
|
|
case "MIN":
|
|
if r.Min != 0 {
|
|
p.xerrorf("duplicate MIN in ESEARCH")
|
|
}
|
|
p.xspace()
|
|
num := p.xnzuint32()
|
|
r.Min = num
|
|
|
|
case "MAX":
|
|
if r.Max != 0 {
|
|
p.xerrorf("duplicate MAX in ESEARCH")
|
|
}
|
|
p.xspace()
|
|
num := p.xnzuint32()
|
|
r.Max = num
|
|
|
|
case "ALL":
|
|
if !r.All.IsZero() {
|
|
p.xerrorf("duplicate ALL in ESEARCH")
|
|
}
|
|
p.xspace()
|
|
ss := p.xsequenceSet()
|
|
if ss.SearchResult {
|
|
p.xerrorf("$ for last not valid in ESEARCH")
|
|
}
|
|
r.All = ss
|
|
|
|
case "COUNT":
|
|
if r.Count != nil {
|
|
p.xerrorf("duplicate COUNT in ESEARCH")
|
|
}
|
|
p.xspace()
|
|
num := p.xuint32()
|
|
r.Count = &num
|
|
|
|
// ../rfc/7162:1211 ../rfc/4731:273
|
|
case "MODSEQ":
|
|
p.xspace()
|
|
r.ModSeq = p.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) {
|
|
p.xerrorf("invalid tag %q", w)
|
|
}
|
|
}
|
|
p.xspace()
|
|
ext := EsearchDataExt{w, p.xtaggedExtVal()}
|
|
r.Exts = append(r.Exts, ext)
|
|
}
|
|
|
|
if !p.space() {
|
|
break
|
|
}
|
|
w = p.xnonspace() // todo: this is too loose
|
|
W = strings.ToUpper(w)
|
|
}
|
|
return
|
|
}
|
|
|
|
// ../rfc/9051:6441
|
|
func (p *Proto) xcharset() string {
|
|
if p.peek('"') {
|
|
return p.xquoted()
|
|
}
|
|
return p.xatom()
|
|
}
|
|
|
|
// ../rfc/9051:7133
|
|
func (p *Proto) xuidset() []NumRange {
|
|
ranges := []NumRange{p.xuidrange()}
|
|
for p.take(',') {
|
|
ranges = append(ranges, p.xuidrange())
|
|
}
|
|
return ranges
|
|
}
|
|
|
|
func (p *Proto) xuidrange() NumRange {
|
|
uid := p.xnzuint32()
|
|
var end *uint32
|
|
if p.take(':') {
|
|
x := p.xnzuint32()
|
|
end = &x
|
|
}
|
|
return NumRange{uid, end}
|
|
}
|
|
|
|
// ../rfc/3501:4833
|
|
func (p *Proto) xlsub() UntaggedLsub {
|
|
p.xspace()
|
|
p.xtake("(")
|
|
r := UntaggedLsub{}
|
|
for !p.take(')') {
|
|
if len(r.Flags) > 0 {
|
|
p.xspace()
|
|
}
|
|
r.Flags = append(r.Flags, p.xflag())
|
|
}
|
|
p.xspace()
|
|
if p.peek('"') {
|
|
s := p.xquoted()
|
|
if !p.peek(' ') {
|
|
r.Mailbox = s
|
|
return r
|
|
}
|
|
if len(s) != 1 {
|
|
// todo: check valid char
|
|
p.xerrorf("invalid separator %q", s)
|
|
}
|
|
r.Separator = byte(s[0])
|
|
}
|
|
p.xspace()
|
|
r.Mailbox = p.xastring()
|
|
return r
|
|
}
|