mirror of
https://github.com/mjl-/mox.git
synced 2024-12-26 16:33:47 +03:00
1e049a087d
for a uid set, the syntax <num>:* must be interpreted as <num>:<maxuid>. a wrong check turned the uid set into <maxuid>:<maxuid>. that check was meant for the case where <num> is higher than <maxuid>, in which case num must be replaced with maxuid. this affected "uid expunge" with a uid set, possibly causing messages marked for deletion not to be actually removed, and this affected "search" with the uid parameter, possibly not returning all messages that were searched for. found while writing tests for upcoming condstore/qresync extensions.
186 lines
4 KiB
Go
186 lines
4 KiB
Go
package imapserver
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/mjl-/mox/store"
|
|
)
|
|
|
|
type numSet struct {
|
|
searchResult bool // "$"
|
|
ranges []numRange
|
|
}
|
|
|
|
// 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 = 1
|
|
}
|
|
last := first
|
|
if r.last != nil {
|
|
last = r.last.number
|
|
if r.last.star {
|
|
last = uint32(len(uids))
|
|
}
|
|
}
|
|
if last > uint32(len(uids)) {
|
|
last = uint32(len(uids))
|
|
}
|
|
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[0]
|
|
}
|
|
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]
|
|
if first > last {
|
|
first = last
|
|
}
|
|
} else if r.first.star && last < first {
|
|
last = first
|
|
}
|
|
}
|
|
if uid < first || uid > last {
|
|
continue
|
|
}
|
|
if uidSearch(uids, uid) > 0 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (ss numSet) String() string {
|
|
if ss.searchResult {
|
|
return "$"
|
|
}
|
|
s := ""
|
|
for _, r := range ss.ranges {
|
|
if s != "" {
|
|
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")
|
|
}
|
|
continue
|
|
}
|
|
s += ":"
|
|
if r.last.star {
|
|
s += "*"
|
|
} else {
|
|
s += fmt.Sprintf("%d", r.last.number)
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
|
|
type setNumber struct {
|
|
number uint32
|
|
star bool
|
|
}
|
|
|
|
type numRange struct {
|
|
first setNumber
|
|
last *setNumber // if nil, this numRange is just a setNumber in "first" and first.star will be false
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|