mox/imapserver/protocol.go
Mechiel Lukkien aa631c604c
imapserver: implement PREVIEW extension (RFC 8970), and store previews in message database
We were already generating previews of plain text parts for the webmail
interface, but we didn't store them, so were generating the previews each time
messages were listed.

Now we store previews in the database for faster handling. And we also generate
previews for html parts if needed. We use the first part that has textual
content.

For IMAP, the previews can be requested by an IMAP client. When we get the
"LAZY" variant, which doesn't require us to generate a preview, we generate it
anyway, because it should be fast enough. So don't make clients first ask for
"PREVIEW (LAZY)" and then again a request for "PREVIEW".

We now also generate a preview when a message is added to the account. Except
for imports. It would slow us down, the previews aren't urgent, and they will
be generated on-demand at first-request.
2025-03-28 17:10:17 +01:00

344 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
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
}