mirror of
https://github.com/mjl-/mox.git
synced 2024-12-27 08:53:48 +03:00
14aa85482e
in some cases, they were interpreted as meaning "the first sequence/uid", but it should always be "the last sequence/uid", just like patterns of the form "123:*". this wrong interpretation was used in the "fetch" command when combined with "changedsince", and in the search command for some parameters, and during expunge with an explicit uid range. the form "*" and "*:123" aren't very common.
342 lines
7.7 KiB
Go
342 lines
7.7 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
|
|
}
|
|
for _, r := range ss.ranges {
|
|
first := r.first.number
|
|
if r.first.star || first > uint32(len(uids)) {
|
|
first = uint32(len(uids))
|
|
}
|
|
|
|
last := first
|
|
if r.last != nil {
|
|
last = r.last.number
|
|
if r.last.star || last > uint32(len(uids)) {
|
|
last = uint32(len(uids))
|
|
}
|
|
}
|
|
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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
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
|
|
}
|