mirror of
https://github.com/mjl-/mox.git
synced 2025-06-28 01:48:15 +03:00
403 lines
9.2 KiB
Go
403 lines
9.2 KiB
Go
package imapserver
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/mjl-/mox/store"
|
|
)
|
|
|
|
type numSet struct {
|
|
searchResult bool // "$"
|
|
ranges []numRange
|
|
}
|
|
|
|
type numRange struct {
|
|
first setNumber
|
|
last *setNumber // if nil, this numRange is just a setNumber in "first" and first.star will be false
|
|
}
|
|
|
|
type setNumber struct {
|
|
number uint32
|
|
star bool // References last message (max sequence number/uid). ../rfc/9051:799
|
|
}
|
|
|
|
// containsSeq returns whether seq is in the numSet, given uids and (saved) searchResult.
|
|
// uids and searchResult must be sorted. searchResult can have uids that are no longer in uids.
|
|
func (ss numSet) containsSeq(seq msgseq, uids []store.UID, searchResult []store.UID) bool {
|
|
if len(uids) == 0 {
|
|
return false
|
|
}
|
|
if ss.searchResult {
|
|
uid := uids[int(seq)-1]
|
|
return uidSearch(searchResult, uid) > 0 && uidSearch(uids, uid) > 0
|
|
}
|
|
return ss.containsSeqCount(seq, uint32(len(uids)))
|
|
}
|
|
|
|
// containsSeqCount returns whether seq is contained in ss, which must not be a searchResult, assuming the message count.
|
|
func (ss numSet) containsSeqCount(seq msgseq, msgCount uint32) bool {
|
|
if msgCount == 0 {
|
|
return false
|
|
}
|
|
for _, r := range ss.ranges {
|
|
first := r.first.number
|
|
if r.first.star || first > msgCount {
|
|
first = msgCount
|
|
}
|
|
|
|
last := first
|
|
if r.last != nil {
|
|
last = r.last.number
|
|
if r.last.star || last > msgCount {
|
|
last = msgCount
|
|
}
|
|
}
|
|
if first > last {
|
|
first, last = last, first
|
|
}
|
|
|
|
if uint32(seq) >= first && uint32(seq) <= last {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (ss numSet) containsUID(uid store.UID, uids []store.UID, searchResult []store.UID) bool {
|
|
if len(uids) == 0 {
|
|
return false
|
|
}
|
|
if ss.searchResult {
|
|
return uidSearch(searchResult, uid) > 0 && uidSearch(uids, uid) > 0
|
|
}
|
|
for _, r := range ss.ranges {
|
|
first := store.UID(r.first.number)
|
|
if r.first.star || first > uids[len(uids)-1] {
|
|
first = uids[len(uids)-1]
|
|
}
|
|
last := first
|
|
// Num in <num>:* can be larger than last, but it still matches the last...
|
|
// Similar for *:<num>. ../rfc/9051:4814
|
|
if r.last != nil {
|
|
last = store.UID(r.last.number)
|
|
if r.last.star || last > uids[len(uids)-1] {
|
|
last = uids[len(uids)-1]
|
|
}
|
|
}
|
|
if first > last {
|
|
first, last = last, first
|
|
}
|
|
if uid >= first && uid <= last && uidSearch(uids, uid) > 0 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// containsKnownUID returns whether uid, which is known to exist, matches the numSet.
|
|
// highestUID must return the highest/last UID in the mailbox, or an error. A last UID must
|
|
// exist, otherwise this method wouldn't have been called with a known uid.
|
|
// highestUID is needed for interpreting UID sets like "<num>:*" where num is
|
|
// higher than the uid to check.
|
|
func (ss numSet) containsKnownUID(uid store.UID, searchResult []store.UID, highestUID func() (store.UID, error)) (bool, error) {
|
|
if ss.searchResult {
|
|
return uidSearch(searchResult, uid) > 0, nil
|
|
}
|
|
|
|
for _, r := range ss.ranges {
|
|
a := store.UID(r.first.number)
|
|
// Num in <num>:* can be larger than last, but it still matches the last...
|
|
// Similar for *:<num>. ../rfc/9051:4814
|
|
if r.first.star {
|
|
if r.last != nil && uid >= store.UID(r.last.number) {
|
|
return true, nil
|
|
}
|
|
|
|
var err error
|
|
a, err = highestUID()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
b := a
|
|
if r.last != nil {
|
|
b = store.UID(r.last.number)
|
|
if r.last.star {
|
|
if uid >= a {
|
|
return true, nil
|
|
}
|
|
|
|
var err error
|
|
b, err = highestUID()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
}
|
|
if a > b {
|
|
a, b = b, a
|
|
}
|
|
if uid >= a && uid <= b {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
// contains returns whether the numset contains the number.
|
|
// only allowed on basic, strictly increasing numsets.
|
|
func (ss numSet) contains(v uint32) bool {
|
|
for _, r := range ss.ranges {
|
|
if r.first.number == v || r.last != nil && v > r.first.number && v <= r.last.number {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (ss numSet) empty() bool {
|
|
return !ss.searchResult && len(ss.ranges) == 0
|
|
}
|
|
|
|
// Strings returns the numset in zero or more strings of maxSize bytes. If
|
|
// maxSize is <= 0, a single string is returned.
|
|
func (ss numSet) Strings(maxSize int) []string {
|
|
if ss.searchResult {
|
|
return []string{"$"}
|
|
}
|
|
var l []string
|
|
var line string
|
|
for _, r := range ss.ranges {
|
|
s := ""
|
|
if r.first.star {
|
|
s += "*"
|
|
} else {
|
|
s += fmt.Sprintf("%d", r.first.number)
|
|
}
|
|
if r.last == nil {
|
|
if r.first.star {
|
|
panic("invalid numSet range first star without last")
|
|
}
|
|
} else {
|
|
s += ":"
|
|
if r.last.star {
|
|
s += "*"
|
|
} else {
|
|
s += fmt.Sprintf("%d", r.last.number)
|
|
}
|
|
}
|
|
|
|
nsize := len(line) + len(s)
|
|
if line != "" {
|
|
nsize++ // comma
|
|
}
|
|
if maxSize > 0 && nsize > maxSize {
|
|
l = append(l, line)
|
|
line = s
|
|
continue
|
|
}
|
|
if line != "" {
|
|
line += ","
|
|
}
|
|
line += s
|
|
}
|
|
if line != "" {
|
|
l = append(l, line)
|
|
}
|
|
return l
|
|
}
|
|
|
|
func (ss numSet) String() string {
|
|
l := ss.Strings(0)
|
|
if len(l) == 0 {
|
|
return ""
|
|
}
|
|
return l[0]
|
|
}
|
|
|
|
// interpretStar returns a numset that interprets stars in a numset, returning a new
|
|
// numset without stars with increasing first/last.
|
|
func (s numSet) interpretStar(uids []store.UID) numSet {
|
|
var ns numSet
|
|
if len(uids) == 0 {
|
|
return ns
|
|
}
|
|
|
|
for _, r := range s.ranges {
|
|
first := r.first.number
|
|
if r.first.star || first > uint32(uids[len(uids)-1]) {
|
|
first = uint32(uids[len(uids)-1])
|
|
}
|
|
last := first
|
|
if r.last != nil {
|
|
last = r.last.number
|
|
if r.last.star || last > uint32(uids[len(uids)-1]) {
|
|
last = uint32(uids[len(uids)-1])
|
|
}
|
|
}
|
|
if first > last {
|
|
first, last = last, first
|
|
}
|
|
nr := numRange{first: setNumber{number: first}}
|
|
if first != last {
|
|
nr.last = &setNumber{number: last}
|
|
}
|
|
ns.ranges = append(ns.ranges, nr)
|
|
}
|
|
return ns
|
|
}
|
|
|
|
// whether numSet only has numbers (no star/search), and is strictly increasing.
|
|
func (s *numSet) isBasicIncreasing() bool {
|
|
if s.searchResult {
|
|
return false
|
|
}
|
|
var last uint32
|
|
for _, r := range s.ranges {
|
|
if r.first.star || r.first.number <= last || r.last != nil && (r.last.star || r.last.number < r.first.number) {
|
|
return false
|
|
}
|
|
last = r.first.number
|
|
if r.last != nil {
|
|
last = r.last.number
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
type numIter struct {
|
|
s numSet
|
|
i int
|
|
r *rangeIter
|
|
}
|
|
|
|
// newIter must only be called on a numSet that is basic (no star/search) and ascending.
|
|
func (s numSet) newIter() *numIter {
|
|
return &numIter{s: s}
|
|
}
|
|
|
|
func (i *numIter) Next() (uint32, bool) {
|
|
if v, ok := i.r.Next(); ok {
|
|
return v, ok
|
|
}
|
|
if i.i >= len(i.s.ranges) {
|
|
return 0, false
|
|
}
|
|
i.r = i.s.ranges[i.i].newIter()
|
|
i.i++
|
|
return i.r.Next()
|
|
}
|
|
|
|
type rangeIter struct {
|
|
r numRange
|
|
o int
|
|
}
|
|
|
|
// newIter must only be called on a range in a numSet that is basic (no star/search) and ascending.
|
|
func (r numRange) newIter() *rangeIter {
|
|
return &rangeIter{r: r, o: 0}
|
|
}
|
|
|
|
func (r *rangeIter) Next() (uint32, bool) {
|
|
if r == nil {
|
|
return 0, false
|
|
}
|
|
if r.o == 0 {
|
|
r.o++
|
|
return r.r.first.number, true
|
|
}
|
|
if r.r.last == nil || r.r.first.number+uint32(r.o) > r.r.last.number {
|
|
return 0, false
|
|
}
|
|
v := r.r.first.number + uint32(r.o)
|
|
r.o++
|
|
return v, true
|
|
}
|
|
|
|
// append adds a new number to the set, extending a range, or starting a new one (possibly the first).
|
|
// can only be used on basic numsets, without star/searchResult.
|
|
func (s *numSet) append(v uint32) {
|
|
if len(s.ranges) == 0 {
|
|
s.ranges = []numRange{{first: setNumber{number: v}}}
|
|
return
|
|
}
|
|
ri := len(s.ranges) - 1
|
|
r := s.ranges[ri]
|
|
if v == r.first.number+1 && r.last == nil {
|
|
s.ranges[ri].last = &setNumber{number: v}
|
|
} else if r.last != nil && v == r.last.number+1 {
|
|
r.last.number++
|
|
} else {
|
|
s.ranges = append(s.ranges, numRange{first: setNumber{number: v}})
|
|
}
|
|
}
|
|
|
|
type partial struct {
|
|
offset uint32
|
|
count uint32
|
|
}
|
|
|
|
type sectionPart struct {
|
|
part []uint32
|
|
text *sectionText
|
|
}
|
|
|
|
type sectionText struct {
|
|
mime bool // if "MIME"
|
|
msgtext *sectionMsgtext
|
|
}
|
|
|
|
// a non-nil *sectionSpec with nil msgtext & nil part means there were []'s, but nothing inside. e.g. "BODY[]".
|
|
type sectionSpec struct {
|
|
msgtext *sectionMsgtext
|
|
part *sectionPart
|
|
}
|
|
|
|
type sectionMsgtext struct {
|
|
s string // "HEADER", "HEADER.FIELDS", "HEADER.FIELDS.NOT", "TEXT"
|
|
headers []string // for "HEADER.FIELDS"*
|
|
}
|
|
|
|
type fetchAtt struct {
|
|
field string // uppercase, eg "ENVELOPE", "BODY". ".PEEK" is removed.
|
|
peek bool
|
|
section *sectionSpec
|
|
sectionBinary []uint32
|
|
partial *partial
|
|
previewLazy bool // Not regular "PREVIEW", but "PREVIEW (LAZY)".
|
|
}
|
|
|
|
type searchKey struct {
|
|
// Only one of searchKeys, seqSet and op can be non-nil/non-empty.
|
|
searchKeys []searchKey // In case of nested/multiple keys. Also for the top-level command.
|
|
seqSet *numSet // In case of bare sequence set. For op UID, field uidSet contains the parameter.
|
|
op string // Determines which of the fields below are set.
|
|
|
|
headerField string
|
|
astring string
|
|
date time.Time
|
|
atom string
|
|
number int64
|
|
searchKey *searchKey
|
|
searchKey2 *searchKey
|
|
uidSet numSet
|
|
clientModseq *int64
|
|
}
|
|
|
|
func compactUIDSet(l []store.UID) (r numSet) {
|
|
for len(l) > 0 {
|
|
e := 1
|
|
for ; e < len(l) && l[e] == l[e-1]+1; e++ {
|
|
}
|
|
first := setNumber{number: uint32(l[0])}
|
|
var last *setNumber
|
|
if e > 1 {
|
|
last = &setNumber{number: uint32(l[e-1])}
|
|
}
|
|
r.ranges = append(r.ranges, numRange{first, last})
|
|
l = l[e:]
|
|
}
|
|
return
|
|
}
|