512 lines
12 KiB
Go
512 lines
12 KiB
Go
package imapmemserver
|
|
|
|
import (
|
|
"bytes"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/emersion/go-imap/v2"
|
|
"github.com/emersion/go-imap/v2/imapserver"
|
|
)
|
|
|
|
// Mailbox is an in-memory mailbox.
|
|
//
|
|
// The same mailbox can be shared between multiple connections and multiple
|
|
// users.
|
|
type Mailbox struct {
|
|
tracker *imapserver.MailboxTracker
|
|
uidValidity uint32
|
|
|
|
mutex sync.Mutex
|
|
name string
|
|
subscribed bool
|
|
specialUse []imap.MailboxAttr
|
|
l []*message
|
|
uidNext imap.UID
|
|
}
|
|
|
|
// NewMailbox creates a new mailbox.
|
|
func NewMailbox(name string, uidValidity uint32) *Mailbox {
|
|
return &Mailbox{
|
|
tracker: imapserver.NewMailboxTracker(0),
|
|
uidValidity: uidValidity,
|
|
name: name,
|
|
uidNext: 1,
|
|
}
|
|
}
|
|
|
|
func (mbox *Mailbox) list(options *imap.ListOptions) *imap.ListData {
|
|
mbox.mutex.Lock()
|
|
defer mbox.mutex.Unlock()
|
|
|
|
if options.SelectSubscribed && !mbox.subscribed {
|
|
return nil
|
|
}
|
|
if options.SelectSpecialUse && len(mbox.specialUse) == 0 {
|
|
return nil
|
|
}
|
|
|
|
data := imap.ListData{
|
|
Mailbox: mbox.name,
|
|
Delim: mailboxDelim,
|
|
}
|
|
if mbox.subscribed {
|
|
data.Attrs = append(data.Attrs, imap.MailboxAttrSubscribed)
|
|
}
|
|
if (options.ReturnSpecialUse || options.SelectSpecialUse) && len(mbox.specialUse) > 0 {
|
|
data.Attrs = append(data.Attrs, mbox.specialUse...)
|
|
}
|
|
if options.ReturnStatus != nil {
|
|
data.Status = mbox.statusDataLocked(options.ReturnStatus)
|
|
}
|
|
return &data
|
|
}
|
|
|
|
// StatusData returns data for the STATUS command.
|
|
func (mbox *Mailbox) StatusData(options *imap.StatusOptions) *imap.StatusData {
|
|
mbox.mutex.Lock()
|
|
defer mbox.mutex.Unlock()
|
|
return mbox.statusDataLocked(options)
|
|
}
|
|
|
|
func (mbox *Mailbox) statusDataLocked(options *imap.StatusOptions) *imap.StatusData {
|
|
data := imap.StatusData{Mailbox: mbox.name}
|
|
if options.NumMessages {
|
|
num := uint32(len(mbox.l))
|
|
data.NumMessages = &num
|
|
}
|
|
if options.UIDNext {
|
|
data.UIDNext = mbox.uidNext
|
|
}
|
|
if options.UIDValidity {
|
|
data.UIDValidity = mbox.uidValidity
|
|
}
|
|
if options.NumUnseen {
|
|
num := uint32(len(mbox.l)) - mbox.countByFlagLocked(imap.FlagSeen)
|
|
data.NumUnseen = &num
|
|
}
|
|
if options.NumDeleted {
|
|
num := mbox.countByFlagLocked(imap.FlagDeleted)
|
|
data.NumDeleted = &num
|
|
}
|
|
if options.Size {
|
|
size := mbox.sizeLocked()
|
|
data.Size = &size
|
|
}
|
|
if options.NumRecent {
|
|
num := uint32(0)
|
|
data.NumRecent = &num
|
|
}
|
|
return &data
|
|
}
|
|
|
|
func (mbox *Mailbox) countByFlagLocked(flag imap.Flag) uint32 {
|
|
var n uint32
|
|
for _, msg := range mbox.l {
|
|
if _, ok := msg.flags[canonicalFlag(flag)]; ok {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
func (mbox *Mailbox) sizeLocked() int64 {
|
|
var size int64
|
|
for _, msg := range mbox.l {
|
|
size += int64(len(msg.buf))
|
|
}
|
|
return size
|
|
}
|
|
|
|
func (mbox *Mailbox) appendLiteral(r imap.LiteralReader, options *imap.AppendOptions) (*imap.AppendData, error) {
|
|
var buf bytes.Buffer
|
|
if _, err := buf.ReadFrom(r); err != nil {
|
|
return nil, err
|
|
}
|
|
return mbox.appendBytes(buf.Bytes(), options), nil
|
|
}
|
|
|
|
func (mbox *Mailbox) copyMsg(msg *message) *imap.AppendData {
|
|
return mbox.appendBytes(msg.buf, &imap.AppendOptions{
|
|
Time: msg.t,
|
|
Flags: msg.flagList(),
|
|
})
|
|
}
|
|
|
|
func (mbox *Mailbox) appendBytes(buf []byte, options *imap.AppendOptions) *imap.AppendData {
|
|
msg := &message{
|
|
flags: make(map[imap.Flag]struct{}),
|
|
buf: buf,
|
|
}
|
|
|
|
if options.Time.IsZero() {
|
|
msg.t = time.Now()
|
|
} else {
|
|
msg.t = options.Time
|
|
}
|
|
|
|
for _, flag := range options.Flags {
|
|
msg.flags[canonicalFlag(flag)] = struct{}{}
|
|
}
|
|
|
|
mbox.mutex.Lock()
|
|
defer mbox.mutex.Unlock()
|
|
|
|
msg.uid = mbox.uidNext
|
|
mbox.uidNext++
|
|
|
|
mbox.l = append(mbox.l, msg)
|
|
mbox.tracker.QueueNumMessages(uint32(len(mbox.l)))
|
|
|
|
return &imap.AppendData{
|
|
UIDValidity: mbox.uidValidity,
|
|
UID: msg.uid,
|
|
}
|
|
}
|
|
|
|
func (mbox *Mailbox) rename(newName string) {
|
|
mbox.mutex.Lock()
|
|
mbox.name = newName
|
|
mbox.mutex.Unlock()
|
|
}
|
|
|
|
// SetSubscribed changes the subscription state of this mailbox.
|
|
func (mbox *Mailbox) SetSubscribed(subscribed bool) {
|
|
mbox.mutex.Lock()
|
|
mbox.subscribed = subscribed
|
|
mbox.mutex.Unlock()
|
|
}
|
|
|
|
func (mbox *Mailbox) selectDataLocked() *imap.SelectData {
|
|
flags := mbox.flagsLocked()
|
|
|
|
permanentFlags := make([]imap.Flag, len(flags))
|
|
copy(permanentFlags, flags)
|
|
permanentFlags = append(permanentFlags, imap.FlagWildcard)
|
|
|
|
// TODO: skip if IMAP4rev1 is disabled by the server, or IMAP4rev2 is
|
|
// enabled by the client
|
|
firstUnseenSeqNum := mbox.firstUnseenSeqNumLocked()
|
|
|
|
return &imap.SelectData{
|
|
Flags: flags,
|
|
PermanentFlags: permanentFlags,
|
|
NumMessages: uint32(len(mbox.l)),
|
|
FirstUnseenSeqNum: firstUnseenSeqNum,
|
|
UIDNext: mbox.uidNext,
|
|
UIDValidity: mbox.uidValidity,
|
|
}
|
|
}
|
|
|
|
func (mbox *Mailbox) firstUnseenSeqNumLocked() uint32 {
|
|
for i, msg := range mbox.l {
|
|
seqNum := uint32(i) + 1
|
|
if _, ok := msg.flags[canonicalFlag(imap.FlagSeen)]; !ok {
|
|
return seqNum
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (mbox *Mailbox) flagsLocked() []imap.Flag {
|
|
m := make(map[imap.Flag]struct{})
|
|
for _, msg := range mbox.l {
|
|
for flag := range msg.flags {
|
|
m[flag] = struct{}{}
|
|
}
|
|
}
|
|
|
|
var l []imap.Flag
|
|
for flag := range m {
|
|
l = append(l, flag)
|
|
}
|
|
|
|
sort.Slice(l, func(i, j int) bool {
|
|
return l[i] < l[j]
|
|
})
|
|
|
|
return l
|
|
}
|
|
|
|
func (mbox *Mailbox) Expunge(w *imapserver.ExpungeWriter, uids *imap.UIDSet) error {
|
|
expunged := make(map[*message]struct{})
|
|
mbox.mutex.Lock()
|
|
for _, msg := range mbox.l {
|
|
if uids != nil && !uids.Contains(msg.uid) {
|
|
continue
|
|
}
|
|
if _, ok := msg.flags[canonicalFlag(imap.FlagDeleted)]; ok {
|
|
expunged[msg] = struct{}{}
|
|
}
|
|
}
|
|
mbox.mutex.Unlock()
|
|
|
|
if len(expunged) == 0 {
|
|
return nil
|
|
}
|
|
|
|
mbox.mutex.Lock()
|
|
mbox.expungeLocked(expunged)
|
|
mbox.mutex.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (mbox *Mailbox) expungeLocked(expunged map[*message]struct{}) (seqNums []uint32) {
|
|
// TODO: optimize
|
|
|
|
// Iterate in reverse order, to keep sequence numbers consistent
|
|
var filtered []*message
|
|
for i := len(mbox.l) - 1; i >= 0; i-- {
|
|
msg := mbox.l[i]
|
|
if _, ok := expunged[msg]; ok {
|
|
seqNum := uint32(i) + 1
|
|
seqNums = append(seqNums, seqNum)
|
|
mbox.tracker.QueueExpunge(seqNum)
|
|
} else {
|
|
filtered = append(filtered, msg)
|
|
}
|
|
}
|
|
|
|
// Reverse filtered
|
|
for i := 0; i < len(filtered)/2; i++ {
|
|
j := len(filtered) - i - 1
|
|
filtered[i], filtered[j] = filtered[j], filtered[i]
|
|
}
|
|
|
|
mbox.l = filtered
|
|
|
|
return seqNums
|
|
}
|
|
|
|
// NewView creates a new view into this mailbox.
|
|
//
|
|
// Callers must call MailboxView.Close once they are done with the mailbox view.
|
|
func (mbox *Mailbox) NewView() *MailboxView {
|
|
return &MailboxView{
|
|
Mailbox: mbox,
|
|
tracker: mbox.tracker.NewSession(),
|
|
}
|
|
}
|
|
|
|
// A MailboxView is a view into a mailbox.
|
|
//
|
|
// Each view has its own queue of pending unilateral updates.
|
|
//
|
|
// Once the mailbox view is no longer used, Close must be called.
|
|
//
|
|
// Typically, a new MailboxView is created for each IMAP connection in the
|
|
// selected state.
|
|
type MailboxView struct {
|
|
*Mailbox
|
|
tracker *imapserver.SessionTracker
|
|
searchRes imap.UIDSet
|
|
}
|
|
|
|
// Close releases the resources allocated for the mailbox view.
|
|
func (mbox *MailboxView) Close() {
|
|
mbox.tracker.Close()
|
|
}
|
|
|
|
func (mbox *MailboxView) Fetch(w *imapserver.FetchWriter, numSet imap.NumSet, options *imap.FetchOptions) error {
|
|
markSeen := false
|
|
for _, bs := range options.BodySection {
|
|
if !bs.Peek {
|
|
markSeen = true
|
|
break
|
|
}
|
|
}
|
|
|
|
var err error
|
|
mbox.forEach(numSet, func(seqNum uint32, msg *message) {
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if markSeen {
|
|
msg.flags[canonicalFlag(imap.FlagSeen)] = struct{}{}
|
|
mbox.Mailbox.tracker.QueueMessageFlags(seqNum, msg.uid, msg.flagList(), nil)
|
|
}
|
|
|
|
respWriter := w.CreateMessage(mbox.tracker.EncodeSeqNum(seqNum))
|
|
err = msg.fetch(respWriter, options)
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (mbox *MailboxView) Search(numKind imapserver.NumKind, criteria *imap.SearchCriteria, options *imap.SearchOptions) (*imap.SearchData, error) {
|
|
mbox.mutex.Lock()
|
|
defer mbox.mutex.Unlock()
|
|
|
|
mbox.staticSearchCriteria(criteria)
|
|
|
|
var (
|
|
data imap.SearchData
|
|
seqSet imap.SeqSet
|
|
uidSet imap.UIDSet
|
|
)
|
|
for i, msg := range mbox.l {
|
|
seqNum := mbox.tracker.EncodeSeqNum(uint32(i) + 1)
|
|
|
|
if !msg.search(seqNum, criteria) {
|
|
continue
|
|
}
|
|
|
|
// Always populate the UID set, since it may be saved later for SEARCHRES
|
|
uidSet.AddNum(msg.uid)
|
|
|
|
var num uint32
|
|
switch numKind {
|
|
case imapserver.NumKindSeq:
|
|
if seqNum == 0 {
|
|
continue
|
|
}
|
|
seqSet.AddNum(seqNum)
|
|
num = seqNum
|
|
case imapserver.NumKindUID:
|
|
num = uint32(msg.uid)
|
|
}
|
|
if data.Min == 0 || num < data.Min {
|
|
data.Min = num
|
|
}
|
|
if data.Max == 0 || num > data.Max {
|
|
data.Max = num
|
|
}
|
|
data.Count++
|
|
}
|
|
|
|
switch numKind {
|
|
case imapserver.NumKindSeq:
|
|
data.All = seqSet
|
|
case imapserver.NumKindUID:
|
|
data.All = uidSet
|
|
}
|
|
|
|
if options.ReturnSave {
|
|
mbox.searchRes = uidSet
|
|
}
|
|
|
|
return &data, nil
|
|
}
|
|
|
|
func (mbox *MailboxView) staticSearchCriteria(criteria *imap.SearchCriteria) {
|
|
seqNums := make([]imap.SeqSet, 0, len(criteria.SeqNum))
|
|
for _, seqSet := range criteria.SeqNum {
|
|
numSet := mbox.staticNumSet(seqSet)
|
|
switch numSet := numSet.(type) {
|
|
case imap.SeqSet:
|
|
seqNums = append(seqNums, numSet)
|
|
case imap.UIDSet: // can happen with SEARCHRES
|
|
criteria.UID = append(criteria.UID, numSet)
|
|
}
|
|
}
|
|
criteria.SeqNum = seqNums
|
|
|
|
for i, uidSet := range criteria.UID {
|
|
criteria.UID[i] = mbox.staticNumSet(uidSet).(imap.UIDSet)
|
|
}
|
|
|
|
for i := range criteria.Not {
|
|
mbox.staticSearchCriteria(&criteria.Not[i])
|
|
}
|
|
for i := range criteria.Or {
|
|
for j := range criteria.Or[i] {
|
|
mbox.staticSearchCriteria(&criteria.Or[i][j])
|
|
}
|
|
}
|
|
}
|
|
|
|
func (mbox *MailboxView) Store(w *imapserver.FetchWriter, numSet imap.NumSet, flags *imap.StoreFlags, options *imap.StoreOptions) error {
|
|
mbox.forEach(numSet, func(seqNum uint32, msg *message) {
|
|
msg.store(flags)
|
|
mbox.Mailbox.tracker.QueueMessageFlags(seqNum, msg.uid, msg.flagList(), mbox.tracker)
|
|
})
|
|
if !flags.Silent {
|
|
return mbox.Fetch(w, numSet, &imap.FetchOptions{Flags: true})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (mbox *MailboxView) Poll(w *imapserver.UpdateWriter, allowExpunge bool) error {
|
|
return mbox.tracker.Poll(w, allowExpunge)
|
|
}
|
|
|
|
func (mbox *MailboxView) Idle(w *imapserver.UpdateWriter, stop <-chan struct{}) error {
|
|
return mbox.tracker.Idle(w, stop)
|
|
}
|
|
|
|
func (mbox *MailboxView) forEach(numSet imap.NumSet, f func(seqNum uint32, msg *message)) {
|
|
mbox.mutex.Lock()
|
|
defer mbox.mutex.Unlock()
|
|
mbox.forEachLocked(numSet, f)
|
|
}
|
|
|
|
func (mbox *MailboxView) forEachLocked(numSet imap.NumSet, f func(seqNum uint32, msg *message)) {
|
|
// TODO: optimize
|
|
|
|
numSet = mbox.staticNumSet(numSet)
|
|
|
|
for i, msg := range mbox.l {
|
|
seqNum := uint32(i) + 1
|
|
|
|
var contains bool
|
|
switch numSet := numSet.(type) {
|
|
case imap.SeqSet:
|
|
seqNum := mbox.tracker.EncodeSeqNum(seqNum)
|
|
contains = seqNum != 0 && numSet.Contains(seqNum)
|
|
case imap.UIDSet:
|
|
contains = numSet.Contains(msg.uid)
|
|
}
|
|
if !contains {
|
|
continue
|
|
}
|
|
|
|
f(seqNum, msg)
|
|
}
|
|
}
|
|
|
|
// staticNumSet converts a dynamic sequence set into a static one.
|
|
//
|
|
// This is necessary to properly handle the special symbol "*", which
|
|
// represents the maximum sequence number or UID in the mailbox.
|
|
//
|
|
// This function also handles the special SEARCHRES marker "$".
|
|
func (mbox *MailboxView) staticNumSet(numSet imap.NumSet) imap.NumSet {
|
|
if imap.IsSearchRes(numSet) {
|
|
return mbox.searchRes
|
|
}
|
|
|
|
switch numSet := numSet.(type) {
|
|
case imap.SeqSet:
|
|
max := uint32(len(mbox.l))
|
|
for i := range numSet {
|
|
r := &numSet[i]
|
|
staticNumRange(&r.Start, &r.Stop, max)
|
|
}
|
|
case imap.UIDSet:
|
|
max := uint32(mbox.uidNext) - 1
|
|
for i := range numSet {
|
|
r := &numSet[i]
|
|
staticNumRange((*uint32)(&r.Start), (*uint32)(&r.Stop), max)
|
|
}
|
|
}
|
|
|
|
return numSet
|
|
}
|
|
|
|
func staticNumRange(start, stop *uint32, max uint32) {
|
|
dyn := false
|
|
if *start == 0 {
|
|
*start = max
|
|
dyn = true
|
|
}
|
|
if *stop == 0 {
|
|
*stop = max
|
|
dyn = true
|
|
}
|
|
if dyn && *start > *stop {
|
|
*start, *stop = *stop, *start
|
|
}
|
|
}
|