Forked the emersion/go-imap v1 project.
This commit is contained in:
486
imapserver/imapmemserver/mailbox.go
Normal file
486
imapserver/imapmemserver/mailbox.go
Normal file
@@ -0,0 +1,486 @@
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
data := imap.ListData{
|
||||
Mailbox: mbox.name,
|
||||
Delim: mailboxDelim,
|
||||
}
|
||||
if mbox.subscribed {
|
||||
data.Attrs = append(data.Attrs, imap.MailboxAttrSubscribed)
|
||||
}
|
||||
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
|
||||
}
|
||||
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)
|
||||
|
||||
return &imap.SelectData{
|
||||
Flags: flags,
|
||||
PermanentFlags: permanentFlags,
|
||||
NumMessages: uint32(len(mbox.l)),
|
||||
UIDNext: mbox.uidNext,
|
||||
UIDValidity: mbox.uidValidity,
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
data := imap.SearchData{UID: numKind == imapserver.NumKindUID}
|
||||
|
||||
var (
|
||||
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
|
||||
}
|
||||
}
|
||||
273
imapserver/imapmemserver/message.go
Normal file
273
imapserver/imapmemserver/message.go
Normal file
@@ -0,0 +1,273 @@
|
||||
package imapmemserver
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/imapserver"
|
||||
gomessage "github.com/emersion/go-message"
|
||||
"github.com/emersion/go-message/mail"
|
||||
"github.com/emersion/go-message/textproto"
|
||||
)
|
||||
|
||||
type message struct {
|
||||
// immutable
|
||||
uid imap.UID
|
||||
buf []byte
|
||||
t time.Time
|
||||
|
||||
// mutable, protected by Mailbox.mutex
|
||||
flags map[imap.Flag]struct{}
|
||||
}
|
||||
|
||||
func (msg *message) fetch(w *imapserver.FetchResponseWriter, options *imap.FetchOptions) error {
|
||||
w.WriteUID(msg.uid)
|
||||
|
||||
if options.Flags {
|
||||
w.WriteFlags(msg.flagList())
|
||||
}
|
||||
if options.InternalDate {
|
||||
w.WriteInternalDate(msg.t)
|
||||
}
|
||||
if options.RFC822Size {
|
||||
w.WriteRFC822Size(int64(len(msg.buf)))
|
||||
}
|
||||
if options.Envelope {
|
||||
w.WriteEnvelope(msg.envelope())
|
||||
}
|
||||
if options.BodyStructure != nil {
|
||||
w.WriteBodyStructure(imapserver.ExtractBodyStructure(bytes.NewReader(msg.buf)))
|
||||
}
|
||||
|
||||
for _, bs := range options.BodySection {
|
||||
buf := imapserver.ExtractBodySection(bytes.NewReader(msg.buf), bs)
|
||||
wc := w.WriteBodySection(bs, int64(len(buf)))
|
||||
_, writeErr := wc.Write(buf)
|
||||
closeErr := wc.Close()
|
||||
if writeErr != nil {
|
||||
return writeErr
|
||||
}
|
||||
if closeErr != nil {
|
||||
return closeErr
|
||||
}
|
||||
}
|
||||
|
||||
for _, bs := range options.BinarySection {
|
||||
buf := imapserver.ExtractBinarySection(bytes.NewReader(msg.buf), bs)
|
||||
wc := w.WriteBinarySection(bs, int64(len(buf)))
|
||||
_, writeErr := wc.Write(buf)
|
||||
closeErr := wc.Close()
|
||||
if writeErr != nil {
|
||||
return writeErr
|
||||
}
|
||||
if closeErr != nil {
|
||||
return closeErr
|
||||
}
|
||||
}
|
||||
|
||||
for _, bss := range options.BinarySectionSize {
|
||||
n := imapserver.ExtractBinarySectionSize(bytes.NewReader(msg.buf), bss)
|
||||
w.WriteBinarySectionSize(bss, n)
|
||||
}
|
||||
|
||||
return w.Close()
|
||||
}
|
||||
|
||||
func (msg *message) envelope() *imap.Envelope {
|
||||
br := bufio.NewReader(bytes.NewReader(msg.buf))
|
||||
header, err := textproto.ReadHeader(br)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return imapserver.ExtractEnvelope(header)
|
||||
}
|
||||
|
||||
func (msg *message) flagList() []imap.Flag {
|
||||
var flags []imap.Flag
|
||||
for flag := range msg.flags {
|
||||
flags = append(flags, flag)
|
||||
}
|
||||
return flags
|
||||
}
|
||||
|
||||
func (msg *message) store(store *imap.StoreFlags) {
|
||||
switch store.Op {
|
||||
case imap.StoreFlagsSet:
|
||||
msg.flags = make(map[imap.Flag]struct{})
|
||||
fallthrough
|
||||
case imap.StoreFlagsAdd:
|
||||
for _, flag := range store.Flags {
|
||||
msg.flags[canonicalFlag(flag)] = struct{}{}
|
||||
}
|
||||
case imap.StoreFlagsDel:
|
||||
for _, flag := range store.Flags {
|
||||
delete(msg.flags, canonicalFlag(flag))
|
||||
}
|
||||
default:
|
||||
panic(fmt.Errorf("unknown STORE flag operation: %v", store.Op))
|
||||
}
|
||||
}
|
||||
|
||||
func (msg *message) reader() *gomessage.Entity {
|
||||
r, _ := gomessage.Read(bytes.NewReader(msg.buf))
|
||||
if r == nil {
|
||||
r, _ = gomessage.New(gomessage.Header{}, bytes.NewReader(nil))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (msg *message) search(seqNum uint32, criteria *imap.SearchCriteria) bool {
|
||||
for _, seqSet := range criteria.SeqNum {
|
||||
if seqNum == 0 || !seqSet.Contains(seqNum) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, uidSet := range criteria.UID {
|
||||
if !uidSet.Contains(msg.uid) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if !matchDate(msg.t, criteria.Since, criteria.Before) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, flag := range criteria.Flag {
|
||||
if _, ok := msg.flags[canonicalFlag(flag)]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, flag := range criteria.NotFlag {
|
||||
if _, ok := msg.flags[canonicalFlag(flag)]; ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if criteria.Larger != 0 && int64(len(msg.buf)) <= criteria.Larger {
|
||||
return false
|
||||
}
|
||||
if criteria.Smaller != 0 && int64(len(msg.buf)) >= criteria.Smaller {
|
||||
return false
|
||||
}
|
||||
|
||||
header := mail.Header{msg.reader().Header}
|
||||
|
||||
for _, fieldCriteria := range criteria.Header {
|
||||
if !matchHeaderFields(header.FieldsByKey(fieldCriteria.Key), fieldCriteria.Value) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if !criteria.SentSince.IsZero() || !criteria.SentBefore.IsZero() {
|
||||
t, err := header.Date()
|
||||
if err != nil {
|
||||
return false
|
||||
} else if !matchDate(t, criteria.SentSince, criteria.SentBefore) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for _, text := range criteria.Text {
|
||||
if !matchEntity(msg.reader(), text, true) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, body := range criteria.Body {
|
||||
if !matchEntity(msg.reader(), body, false) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for _, not := range criteria.Not {
|
||||
if msg.search(seqNum, ¬) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, or := range criteria.Or {
|
||||
if !msg.search(seqNum, &or[0]) && !msg.search(seqNum, &or[1]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func matchDate(t, since, before time.Time) bool {
|
||||
// We discard time zone information by setting it to UTC.
|
||||
// RFC 3501 explicitly requires zone unaware date comparison.
|
||||
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.UTC)
|
||||
|
||||
if !since.IsZero() && t.Before(since) {
|
||||
return false
|
||||
}
|
||||
if !before.IsZero() && !t.Before(before) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func matchHeaderFields(fields gomessage.HeaderFields, pattern string) bool {
|
||||
if pattern == "" {
|
||||
return fields.Len() > 0
|
||||
}
|
||||
|
||||
pattern = strings.ToLower(pattern)
|
||||
for fields.Next() {
|
||||
v, _ := fields.Text()
|
||||
if strings.Contains(strings.ToLower(v), pattern) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func matchEntity(e *gomessage.Entity, pattern string, includeHeader bool) bool {
|
||||
if pattern == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
if includeHeader && matchHeaderFields(e.Header.Fields(), pattern) {
|
||||
return true
|
||||
}
|
||||
|
||||
if mr := e.MultipartReader(); mr != nil {
|
||||
for {
|
||||
part, err := mr.NextPart()
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if matchEntity(part, pattern, includeHeader) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
} else {
|
||||
t, _, err := e.Header.ContentType()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(t, "text/") && !strings.HasPrefix(t, "message/") {
|
||||
return false
|
||||
}
|
||||
|
||||
buf, err := io.ReadAll(e.Body)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return bytes.Contains(bytes.ToLower(buf), bytes.ToLower([]byte(pattern)))
|
||||
}
|
||||
}
|
||||
|
||||
func canonicalFlag(flag imap.Flag) imap.Flag {
|
||||
return imap.Flag(strings.ToLower(string(flag)))
|
||||
}
|
||||
61
imapserver/imapmemserver/server.go
Normal file
61
imapserver/imapmemserver/server.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// Package imapmemserver implements an in-memory IMAP server.
|
||||
package imapmemserver
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/emersion/go-imap/v2/imapserver"
|
||||
)
|
||||
|
||||
// Server is a server instance.
|
||||
//
|
||||
// A server contains a list of users.
|
||||
type Server struct {
|
||||
mutex sync.Mutex
|
||||
users map[string]*User
|
||||
}
|
||||
|
||||
// New creates a new server.
|
||||
func New() *Server {
|
||||
return &Server{
|
||||
users: make(map[string]*User),
|
||||
}
|
||||
}
|
||||
|
||||
// NewSession creates a new IMAP session.
|
||||
func (s *Server) NewSession() imapserver.Session {
|
||||
return &serverSession{server: s}
|
||||
}
|
||||
|
||||
func (s *Server) user(username string) *User {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
return s.users[username]
|
||||
}
|
||||
|
||||
// AddUser adds a user to the server.
|
||||
func (s *Server) AddUser(user *User) {
|
||||
s.mutex.Lock()
|
||||
s.users[user.username] = user
|
||||
s.mutex.Unlock()
|
||||
}
|
||||
|
||||
type serverSession struct {
|
||||
*UserSession // may be nil
|
||||
|
||||
server *Server // immutable
|
||||
}
|
||||
|
||||
var _ imapserver.Session = (*serverSession)(nil)
|
||||
|
||||
func (sess *serverSession) Login(username, password string) error {
|
||||
u := sess.server.user(username)
|
||||
if u == nil {
|
||||
return imapserver.ErrAuthFailed
|
||||
}
|
||||
if err := u.Login(username, password); err != nil {
|
||||
return err
|
||||
}
|
||||
sess.UserSession = NewUserSession(u)
|
||||
return nil
|
||||
}
|
||||
140
imapserver/imapmemserver/session.go
Normal file
140
imapserver/imapmemserver/session.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package imapmemserver
|
||||
|
||||
import (
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/imapserver"
|
||||
)
|
||||
|
||||
type (
|
||||
user = User
|
||||
mailbox = MailboxView
|
||||
)
|
||||
|
||||
// UserSession represents a session tied to a specific user.
|
||||
//
|
||||
// UserSession implements imapserver.Session. Typically, a UserSession pointer
|
||||
// is embedded into a larger struct which overrides Login.
|
||||
type UserSession struct {
|
||||
*user // immutable
|
||||
*mailbox // may be nil
|
||||
}
|
||||
|
||||
var _ imapserver.SessionIMAP4rev2 = (*UserSession)(nil)
|
||||
|
||||
// NewUserSession creates a new user session.
|
||||
func NewUserSession(user *User) *UserSession {
|
||||
return &UserSession{user: user}
|
||||
}
|
||||
|
||||
func (sess *UserSession) Close() error {
|
||||
if sess != nil && sess.mailbox != nil {
|
||||
sess.mailbox.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sess *UserSession) Select(name string, options *imap.SelectOptions) (*imap.SelectData, error) {
|
||||
mbox, err := sess.user.mailbox(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mbox.mutex.Lock()
|
||||
defer mbox.mutex.Unlock()
|
||||
sess.mailbox = mbox.NewView()
|
||||
return mbox.selectDataLocked(), nil
|
||||
}
|
||||
|
||||
func (sess *UserSession) Unselect() error {
|
||||
sess.mailbox.Close()
|
||||
sess.mailbox = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sess *UserSession) Copy(numSet imap.NumSet, destName string) (*imap.CopyData, error) {
|
||||
dest, err := sess.user.mailbox(destName)
|
||||
if err != nil {
|
||||
return nil, &imap.Error{
|
||||
Type: imap.StatusResponseTypeNo,
|
||||
Code: imap.ResponseCodeTryCreate,
|
||||
Text: "No such mailbox",
|
||||
}
|
||||
} else if sess.mailbox != nil && dest == sess.mailbox.Mailbox {
|
||||
return nil, &imap.Error{
|
||||
Type: imap.StatusResponseTypeNo,
|
||||
Text: "Source and destination mailboxes are identical",
|
||||
}
|
||||
}
|
||||
|
||||
var sourceUIDs, destUIDs imap.UIDSet
|
||||
sess.mailbox.forEach(numSet, func(seqNum uint32, msg *message) {
|
||||
appendData := dest.copyMsg(msg)
|
||||
sourceUIDs.AddNum(msg.uid)
|
||||
destUIDs.AddNum(appendData.UID)
|
||||
})
|
||||
|
||||
return &imap.CopyData{
|
||||
UIDValidity: dest.uidValidity,
|
||||
SourceUIDs: sourceUIDs,
|
||||
DestUIDs: destUIDs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (sess *UserSession) Move(w *imapserver.MoveWriter, numSet imap.NumSet, destName string) error {
|
||||
dest, err := sess.user.mailbox(destName)
|
||||
if err != nil {
|
||||
return &imap.Error{
|
||||
Type: imap.StatusResponseTypeNo,
|
||||
Code: imap.ResponseCodeTryCreate,
|
||||
Text: "No such mailbox",
|
||||
}
|
||||
} else if sess.mailbox != nil && dest == sess.mailbox.Mailbox {
|
||||
return &imap.Error{
|
||||
Type: imap.StatusResponseTypeNo,
|
||||
Text: "Source and destination mailboxes are identical",
|
||||
}
|
||||
}
|
||||
|
||||
sess.mailbox.mutex.Lock()
|
||||
defer sess.mailbox.mutex.Unlock()
|
||||
|
||||
var sourceUIDs, destUIDs imap.UIDSet
|
||||
expunged := make(map[*message]struct{})
|
||||
sess.mailbox.forEachLocked(numSet, func(seqNum uint32, msg *message) {
|
||||
appendData := dest.copyMsg(msg)
|
||||
sourceUIDs.AddNum(msg.uid)
|
||||
destUIDs.AddNum(appendData.UID)
|
||||
expunged[msg] = struct{}{}
|
||||
})
|
||||
seqNums := sess.mailbox.expungeLocked(expunged)
|
||||
|
||||
err = w.WriteCopyData(&imap.CopyData{
|
||||
UIDValidity: dest.uidValidity,
|
||||
SourceUIDs: sourceUIDs,
|
||||
DestUIDs: destUIDs,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, seqNum := range seqNums {
|
||||
if err := w.WriteExpunge(sess.mailbox.tracker.EncodeSeqNum(seqNum)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sess *UserSession) Poll(w *imapserver.UpdateWriter, allowExpunge bool) error {
|
||||
if sess.mailbox == nil {
|
||||
return nil
|
||||
}
|
||||
return sess.mailbox.Poll(w, allowExpunge)
|
||||
}
|
||||
|
||||
func (sess *UserSession) Idle(w *imapserver.UpdateWriter, stop <-chan struct{}) error {
|
||||
if sess.mailbox == nil {
|
||||
return nil // TODO
|
||||
}
|
||||
return sess.mailbox.Idle(w, stop)
|
||||
}
|
||||
204
imapserver/imapmemserver/user.go
Normal file
204
imapserver/imapmemserver/user.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package imapmemserver
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/imapserver"
|
||||
)
|
||||
|
||||
const mailboxDelim rune = '/'
|
||||
|
||||
type User struct {
|
||||
username, password string
|
||||
|
||||
mutex sync.Mutex
|
||||
mailboxes map[string]*Mailbox
|
||||
prevUidValidity uint32
|
||||
}
|
||||
|
||||
func NewUser(username, password string) *User {
|
||||
return &User{
|
||||
username: username,
|
||||
password: password,
|
||||
mailboxes: make(map[string]*Mailbox),
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) Login(username, password string) error {
|
||||
if username != u.username {
|
||||
return imapserver.ErrAuthFailed
|
||||
}
|
||||
if subtle.ConstantTimeCompare([]byte(password), []byte(u.password)) != 1 {
|
||||
return imapserver.ErrAuthFailed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) mailboxLocked(name string) (*Mailbox, error) {
|
||||
mbox := u.mailboxes[name]
|
||||
if mbox == nil {
|
||||
return nil, &imap.Error{
|
||||
Type: imap.StatusResponseTypeNo,
|
||||
Code: imap.ResponseCodeNonExistent,
|
||||
Text: "No such mailbox",
|
||||
}
|
||||
}
|
||||
return mbox, nil
|
||||
}
|
||||
|
||||
func (u *User) mailbox(name string) (*Mailbox, error) {
|
||||
u.mutex.Lock()
|
||||
defer u.mutex.Unlock()
|
||||
return u.mailboxLocked(name)
|
||||
}
|
||||
|
||||
func (u *User) Status(name string, options *imap.StatusOptions) (*imap.StatusData, error) {
|
||||
mbox, err := u.mailbox(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mbox.StatusData(options), nil
|
||||
}
|
||||
|
||||
func (u *User) List(w *imapserver.ListWriter, ref string, patterns []string, options *imap.ListOptions) error {
|
||||
u.mutex.Lock()
|
||||
defer u.mutex.Unlock()
|
||||
|
||||
// TODO: fail if ref doesn't exist
|
||||
|
||||
if len(patterns) == 0 {
|
||||
return w.WriteList(&imap.ListData{
|
||||
Attrs: []imap.MailboxAttr{imap.MailboxAttrNoSelect},
|
||||
Delim: mailboxDelim,
|
||||
})
|
||||
}
|
||||
|
||||
var l []imap.ListData
|
||||
for name, mbox := range u.mailboxes {
|
||||
match := false
|
||||
for _, pattern := range patterns {
|
||||
match = imapserver.MatchList(name, mailboxDelim, ref, pattern)
|
||||
if match {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
continue
|
||||
}
|
||||
|
||||
data := mbox.list(options)
|
||||
if data != nil {
|
||||
l = append(l, *data)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(l, func(i, j int) bool {
|
||||
return l[i].Mailbox < l[j].Mailbox
|
||||
})
|
||||
|
||||
for _, data := range l {
|
||||
if err := w.WriteList(&data); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) Append(mailbox string, r imap.LiteralReader, options *imap.AppendOptions) (*imap.AppendData, error) {
|
||||
mbox, err := u.mailbox(mailbox)
|
||||
if err != nil {
|
||||
return nil, &imap.Error{
|
||||
Type: imap.StatusResponseTypeNo,
|
||||
Code: imap.ResponseCodeTryCreate,
|
||||
Text: "No such mailbox",
|
||||
}
|
||||
}
|
||||
return mbox.appendLiteral(r, options)
|
||||
}
|
||||
|
||||
func (u *User) Create(name string, options *imap.CreateOptions) error {
|
||||
u.mutex.Lock()
|
||||
defer u.mutex.Unlock()
|
||||
|
||||
name = strings.TrimRight(name, string(mailboxDelim))
|
||||
|
||||
if u.mailboxes[name] != nil {
|
||||
return &imap.Error{
|
||||
Type: imap.StatusResponseTypeNo,
|
||||
Code: imap.ResponseCodeAlreadyExists,
|
||||
Text: "Mailbox already exists",
|
||||
}
|
||||
}
|
||||
|
||||
// UIDVALIDITY must change if a mailbox is deleted and re-created with the
|
||||
// same name.
|
||||
u.prevUidValidity++
|
||||
u.mailboxes[name] = NewMailbox(name, u.prevUidValidity)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) Delete(name string) error {
|
||||
u.mutex.Lock()
|
||||
defer u.mutex.Unlock()
|
||||
|
||||
if _, err := u.mailboxLocked(name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
delete(u.mailboxes, name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) Rename(oldName, newName string) error {
|
||||
u.mutex.Lock()
|
||||
defer u.mutex.Unlock()
|
||||
|
||||
newName = strings.TrimRight(newName, string(mailboxDelim))
|
||||
|
||||
mbox, err := u.mailboxLocked(oldName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if u.mailboxes[newName] != nil {
|
||||
return &imap.Error{
|
||||
Type: imap.StatusResponseTypeNo,
|
||||
Code: imap.ResponseCodeAlreadyExists,
|
||||
Text: "Mailbox already exists",
|
||||
}
|
||||
}
|
||||
|
||||
mbox.rename(newName)
|
||||
u.mailboxes[newName] = mbox
|
||||
delete(u.mailboxes, oldName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) Subscribe(name string) error {
|
||||
mbox, err := u.mailbox(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mbox.SetSubscribed(true)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) Unsubscribe(name string) error {
|
||||
mbox, err := u.mailbox(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mbox.SetSubscribed(false)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) Namespace() (*imap.NamespaceData, error) {
|
||||
return &imap.NamespaceData{
|
||||
Personal: []imap.NamespaceDescriptor{{Delim: mailboxDelim}},
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user