Skip to content

Commit

Permalink
imapmemserver: add Namespace
Browse files Browse the repository at this point in the history
  • Loading branch information
emersion committed Jan 30, 2024
1 parent f789966 commit 11a8073
Show file tree
Hide file tree
Showing 2 changed files with 196 additions and 167 deletions.
191 changes: 191 additions & 0 deletions imapserver/imapmemserver/namespace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package imapmemserver

import (
"sort"
"strings"
"sync"

"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/imapserver"
)

type Namespace struct {
prefix string

mutex sync.Mutex
mailboxes map[string]*Mailbox
prevUidValidity uint32
}

func NewNamespace(prefix string) *Namespace {
return &Namespace{
prefix: prefix,
mailboxes: make(map[string]*Mailbox),
}
}

func (ns *Namespace) mailboxLocked(name string) (*Mailbox, error) {
mbox := ns.mailboxes[name]
if mbox == nil {
return nil, &imap.Error{
Type: imap.StatusResponseTypeNo,
Code: imap.ResponseCodeNonExistent,
Text: "No such mailbox",
}
}
return mbox, nil
}

func (ns *Namespace) mailbox(name string) (*Mailbox, error) {
ns.mutex.Lock()
defer ns.mutex.Unlock()
return ns.mailboxLocked(name)
}

func (ns *Namespace) Status(name string, options *imap.StatusOptions) (*imap.StatusData, error) {
mbox, err := ns.mailbox(name)
if err != nil {
return nil, err
}
return mbox.StatusData(options), nil
}

func (ns *Namespace) List(w *imapserver.ListWriter, ref string, patterns []string, options *imap.ListOptions) error {
ns.mutex.Lock()
defer ns.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 ns.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 (ns *Namespace) Append(mailbox string, r imap.LiteralReader, options *imap.AppendOptions) (*imap.AppendData, error) {
mbox, err := ns.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 (ns *Namespace) Create(name string, options *imap.CreateOptions) error {
ns.mutex.Lock()
defer ns.mutex.Unlock()

name = strings.TrimRight(name, string(mailboxDelim))

if ns.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.
ns.prevUidValidity++
ns.mailboxes[name] = NewMailbox(name, ns.prevUidValidity)
return nil
}

func (ns *Namespace) Delete(name string) error {
ns.mutex.Lock()
defer ns.mutex.Unlock()

if _, err := ns.mailboxLocked(name); err != nil {
return err
}

delete(ns.mailboxes, name)
return nil
}

func (ns *Namespace) Rename(oldName, newName string) error {
ns.mutex.Lock()
defer ns.mutex.Unlock()

newName = strings.TrimRight(newName, string(mailboxDelim))

mbox, err := ns.mailboxLocked(oldName)
if err != nil {
return err
}

if ns.mailboxes[newName] != nil {
return &imap.Error{
Type: imap.StatusResponseTypeNo,
Code: imap.ResponseCodeAlreadyExists,
Text: "Mailbox already exists",
}
}

mbox.rename(newName)
ns.mailboxes[newName] = mbox
delete(ns.mailboxes, oldName)
return nil
}

func (ns *Namespace) Subscribe(name string) error {
mbox, err := ns.mailbox(name)
if err != nil {
return err
}
mbox.SetSubscribed(true)
return nil
}

func (ns *Namespace) Unsubscribe(name string) error {
mbox, err := ns.mailbox(name)
if err != nil {
return err
}
mbox.SetSubscribed(false)
return nil
}

func (ns *Namespace) NamespaceDescriptor() *imap.NamespaceDescriptor {
return &imap.NamespaceDescriptor{
Prefix: ns.prefix,
Delim: mailboxDelim,
}
}
172 changes: 5 additions & 167 deletions imapserver/imapmemserver/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,26 @@ 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 namespace = Namespace

type User struct {
username, password string
*namespace // default namespace, immutable

mutex sync.Mutex
mailboxes map[string]*Mailbox
prevUidValidity uint32
username, password string
}

func NewUser(username, password string) *User {
return &User{
namespace: NewNamespace(""),
username: username,
password: password,
mailboxes: make(map[string]*Mailbox),
}
}

Expand All @@ -38,165 +35,6 @@ func (u *User) Login(username, password string) error {
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}},
Expand Down

0 comments on commit 11a8073

Please sign in to comment.