Forked the emersion/go-imap v1 project.

This commit is contained in:
2025-05-01 11:58:18 +03:00
commit bcc3f95e8e
107 changed files with 16268 additions and 0 deletions

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

View 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, &not) {
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)))
}

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

View 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)
}

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