mirror of
https://github.com/hanzoai/mail.git
synced 2026-09-23 23:53:02 +00:00
Modules declare go 1.27.1 and the build images follow. go fix applied the modernizers; omitzero is not applied, since it changes what the JSON says. Co-authored-by: Hanzo Dev <[email protected]>
1669 lines
32 KiB
Go
1669 lines
32 KiB
Go
package imapclient
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hanzoai/mail/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 {
|
|
var w strings.Builder
|
|
for !p.peek(' ') && !p.peek(']') {
|
|
w.WriteString(string(rune(p.xbyte())))
|
|
}
|
|
W := strings.ToUpper(w.String())
|
|
|
|
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() {
|
|
var arg strings.Builder
|
|
for !p.peek(' ') && !p.peek(']') {
|
|
arg.WriteString(string(rune(p.xbyte())))
|
|
}
|
|
args = append(args, arg.String())
|
|
}
|
|
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 strings.Builder
|
|
for {
|
|
b, err := p.readbyte()
|
|
if err == nil && (b >= '0' && b <= '9') {
|
|
s.WriteString(string(rune(b)))
|
|
continue
|
|
}
|
|
p.xunreadbyte()
|
|
return s.String()
|
|
}
|
|
}
|
|
|
|
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 strings.Builder
|
|
for !p.peek(' ') && !p.peek('\r') && !p.peek('\n') {
|
|
s.WriteString(string(rune(p.xbyte())))
|
|
}
|
|
if s.String() == "" {
|
|
p.xerrorf("expected non-space")
|
|
}
|
|
return s.String()
|
|
}
|
|
|
|
// 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 {
|
|
var f strings.Builder
|
|
for {
|
|
b := p.xbyte()
|
|
if b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b >= '0' && b <= '9' || b == '.' {
|
|
f.WriteString(string(rune(b)))
|
|
continue
|
|
}
|
|
p.xunreadbyte()
|
|
break
|
|
}
|
|
|
|
F := strings.ToUpper(f.String())
|
|
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.String())
|
|
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 ""
|
|
}
|
|
}
|
|
|
|
//go:fix inline
|
|
func ptr[T any](v T) *T {
|
|
return new(v)
|
|
}
|
|
|
|
func (p *Proto) xnilptrString() *string {
|
|
if p.peek('"') {
|
|
return new(p.xquoted())
|
|
} else if p.peek('{') {
|
|
return new(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(`"`)
|
|
var s strings.Builder
|
|
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.WriteString(string(r))
|
|
}
|
|
return s.String()
|
|
}
|
|
|
|
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 = new(p.xbodyFldLang())
|
|
if !p.space() {
|
|
return
|
|
}
|
|
ext.Location = new(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 = new(p.xbodyFldLang())
|
|
if !p.space() {
|
|
return
|
|
}
|
|
ext.Location = new(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 new(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
|
|
}
|