From 67a8796528b67d30d6e13d08680431f632256038 Mon Sep 17 00:00:00 2001 From: "fox.cpp" Date: Sun, 26 Jul 2020 22:30:53 +0300 Subject: [PATCH 01/11] v2: Move backend methods for commands not related to current mailbox to User This is a preparation for dropping in-library updates dispatching. The goal is to minimize creating Mailbox objects for commands that are not related to the currently selected mailbox thus reserving Mailbox to only represent currently selected mailbox. --- backend/mailbox.go | 20 ------------ backend/memory/mailbox.go | 52 ------------------------------ backend/memory/user.go | 66 +++++++++++++++++++++++++++++++++++++++ backend/user.go | 19 +++++++++++ server/cmd_auth.go | 55 +++++++------------------------- 5 files changed, 97 insertions(+), 115 deletions(-) diff --git a/backend/mailbox.go b/backend/mailbox.go index f6ebe018..e886a751 100644 --- a/backend/mailbox.go +++ b/backend/mailbox.go @@ -1,8 +1,6 @@ package backend import ( - "time" - "github.com/emersion/go-imap" ) @@ -18,16 +16,6 @@ type Mailbox interface { // Info returns this mailbox info. Info() (*imap.MailboxInfo, error) - // Status returns this mailbox status. The fields Name, Flags, PermanentFlags - // and UnseenSeqNum in the returned MailboxStatus must be always populated. - // This function does not affect the state of any messages in the mailbox. See - // RFC 3501 section 6.3.10 for a list of items that can be requested. - Status(items []imap.StatusItem) (*imap.MailboxStatus, error) - - // SetSubscribed adds or removes the mailbox to the server's set of "active" - // or "subscribed" mailboxes. - SetSubscribed(subscribed bool) error - // Check requests a checkpoint of the currently selected mailbox. A checkpoint // refers to any implementation-dependent housekeeping associated with the // mailbox (e.g., resolving the server's in-memory state of the mailbox with @@ -47,14 +35,6 @@ type Mailbox interface { // uid is set to true, or sequence numbers otherwise. SearchMessages(uid bool, criteria *imap.SearchCriteria) ([]uint32, error) - // CreateMessage appends a new message to this mailbox. The \Recent flag will - // be added no matter flags is empty or not. If date is nil, the current time - // will be used. - // - // If the Backend implements Updater, it must notify the client immediately - // via a mailbox update. - CreateMessage(flags []string, date time.Time, body imap.Literal) error - // UpdateMessagesFlags alters flags for the specified message(s). // // If the Backend implements Updater, it must notify the client immediately diff --git a/backend/memory/mailbox.go b/backend/memory/mailbox.go index b6cdf518..35f4938c 100644 --- a/backend/memory/mailbox.go +++ b/backend/memory/mailbox.go @@ -1,9 +1,6 @@ package memory import ( - "io/ioutil" - "time" - "github.com/emersion/go-imap" "github.com/emersion/go-imap/backend" "github.com/emersion/go-imap/backend/backendutil" @@ -78,35 +75,6 @@ func (mbox *Mailbox) unseenSeqNum() uint32 { return 0 } -func (mbox *Mailbox) Status(items []imap.StatusItem) (*imap.MailboxStatus, error) { - status := imap.NewMailboxStatus(mbox.name, items) - status.Flags = mbox.flags() - status.PermanentFlags = []string{"\\*"} - status.UnseenSeqNum = mbox.unseenSeqNum() - - for _, name := range items { - switch name { - case imap.StatusMessages: - status.Messages = uint32(len(mbox.Messages)) - case imap.StatusUidNext: - status.UidNext = mbox.uidNext() - case imap.StatusUidValidity: - status.UidValidity = 1 - case imap.StatusRecent: - status.Recent = 0 // TODO - case imap.StatusUnseen: - status.Unseen = 0 // TODO - } - } - - return status, nil -} - -func (mbox *Mailbox) SetSubscribed(subscribed bool) error { - mbox.Subscribed = subscribed - return nil -} - func (mbox *Mailbox) Check() error { return nil } @@ -159,26 +127,6 @@ func (mbox *Mailbox) SearchMessages(uid bool, criteria *imap.SearchCriteria) ([] return ids, nil } -func (mbox *Mailbox) CreateMessage(flags []string, date time.Time, body imap.Literal) error { - if date.IsZero() { - date = time.Now() - } - - b, err := ioutil.ReadAll(body) - if err != nil { - return err - } - - mbox.Messages = append(mbox.Messages, &Message{ - Uid: mbox.uidNext(), - Date: date, - Size: uint32(len(b)), - Flags: flags, - Body: b, - }) - return nil -} - func (mbox *Mailbox) UpdateMessagesFlags(uid bool, seqset *imap.SeqSet, op imap.FlagsOp, flags []string) error { for i, msg := range mbox.Messages { var id uint32 diff --git a/backend/memory/user.go b/backend/memory/user.go index bef7284e..2c1dd130 100644 --- a/backend/memory/user.go +++ b/backend/memory/user.go @@ -2,6 +2,8 @@ package memory import ( "errors" + "io/ioutil" + "time" "github.com/emersion/go-imap" "github.com/emersion/go-imap/backend" @@ -40,6 +42,70 @@ func (u *User) GetMailbox(name string) (mailbox backend.Mailbox, err error) { return } +func (u *User) Status(name string, items []imap.StatusItem) (*imap.MailboxStatus, error) { + mbox, ok := u.mailboxes[name] + if !ok { + return nil, backend.ErrNoSuchMailbox + } + + status := imap.NewMailboxStatus(mbox.name, items) + status.Flags = mbox.flags() + status.PermanentFlags = []string{"\\*"} + status.UnseenSeqNum = mbox.unseenSeqNum() + + for _, name := range items { + switch name { + case imap.StatusMessages: + status.Messages = uint32(len(mbox.Messages)) + case imap.StatusUidNext: + status.UidNext = mbox.uidNext() + case imap.StatusUidValidity: + status.UidValidity = 1 + case imap.StatusRecent: + status.Recent = 0 // TODO + case imap.StatusUnseen: + status.Unseen = 0 // TODO + } + } + + return status, nil +} + +func (u *User) SetSubscribed(name string, subscribed bool) error { + mbox, ok := u.mailboxes[name] + if !ok { + return backend.ErrNoSuchMailbox + } + + mbox.Subscribed = subscribed + return nil +} + +func (u *User) CreateMessage(mboxName string, flags []string, date time.Time, body imap.Literal) error { + mbox, ok := u.mailboxes[mboxName] + if !ok { + return backend.ErrNoSuchMailbox + } + + if date.IsZero() { + date = time.Now() + } + + b, err := ioutil.ReadAll(body) + if err != nil { + return err + } + + mbox.Messages = append(mbox.Messages, &Message{ + Uid: mbox.uidNext(), + Date: date, + Size: uint32(len(b)), + Flags: flags, + Body: b, + }) + return nil +} + func (u *User) CreateMailbox(name string) error { if _, ok := u.mailboxes[name]; ok { return errors.New("Mailbox already exists") diff --git a/backend/user.go b/backend/user.go index efde1c6e..28e047a2 100644 --- a/backend/user.go +++ b/backend/user.go @@ -2,6 +2,7 @@ package backend import ( "errors" + "time" "github.com/emersion/go-imap" ) @@ -22,6 +23,24 @@ type User interface { // Username returns this user's username. Username() string + // Status returns mailbox status. The fields Name, Flags, PermanentFlags + // and UnseenSeqNum in the returned MailboxStatus must be always populated. + // This function does not affect the state of any messages in the mailbox. See + // RFC 3501 section 6.3.10 for a list of items that can be requested. + Status(mbox string, items []imap.StatusItem) (*imap.MailboxStatus, error) + + // SetSubscribed adds or removes the mailbox to the server's set of "active" + // or "subscribed" mailboxes. + SetSubscribed(mbox string, subscribed bool) error + + // CreateMessage appends a new message to mailbox. The \Recent flag will + // be added no matter flags is empty or not. If date is nil, the current time + // will be used. + // + // If the Backend implements Updater, it must notify the client immediately + // via a mailbox update. + CreateMessage(mbox string, flags []string, date time.Time, body imap.Literal) error + // ListMailboxes returns information about mailboxes belonging to this // user. If subscribed is set to true, only returns subscribed mailboxes. ListMailboxes(subscribed bool) ([]imap.MailboxInfo, error) diff --git a/server/cmd_auth.go b/server/cmd_auth.go index 06c9325d..54982668 100644 --- a/server/cmd_auth.go +++ b/server/cmd_auth.go @@ -4,7 +4,6 @@ import ( "errors" "github.com/emersion/go-imap" - "github.com/emersion/go-imap/backend" "github.com/emersion/go-imap/commands" "github.com/emersion/go-imap/responses" ) @@ -34,19 +33,19 @@ func (cmd *Select) Handle(conn Conn) error { if ctx.User == nil { return ErrNotAuthenticated } - mbox, err := ctx.User.GetMailbox(cmd.Mailbox) - if err != nil { - return err - } items := []imap.StatusItem{ imap.StatusMessages, imap.StatusRecent, imap.StatusUnseen, imap.StatusUidNext, imap.StatusUidValidity, } - status, err := mbox.Status(items) + status, err := ctx.User.Status(cmd.Mailbox, items) + if err != nil { + return err + } + + mbox, err := ctx.User.GetMailbox(cmd.Mailbox) if err != nil { - mbox.Close() return err } @@ -120,13 +119,7 @@ func (cmd *Subscribe) Handle(conn Conn) error { return ErrNotAuthenticated } - mbox, err := ctx.User.GetMailbox(cmd.Mailbox) - if err != nil { - return err - } - defer mbox.Close() - - return mbox.SetSubscribed(true) + return ctx.User.SetSubscribed(cmd.Mailbox, true) } type Unsubscribe struct { @@ -139,13 +132,7 @@ func (cmd *Unsubscribe) Handle(conn Conn) error { return ErrNotAuthenticated } - mbox, err := ctx.User.GetMailbox(cmd.Mailbox) - if err != nil { - return err - } - defer mbox.Close() - - return mbox.SetSubscribed(false) + return ctx.User.SetSubscribed(cmd.Mailbox, false) } type List struct { @@ -212,13 +199,7 @@ func (cmd *Status) Handle(conn Conn) error { return ErrNotAuthenticated } - mbox, err := ctx.User.GetMailbox(cmd.Mailbox) - if err != nil { - return err - } - defer mbox.Close() - - status, err := mbox.Status(cmd.Items) + status, err := ctx.User.Status(cmd.Mailbox, cmd.Items) if err != nil { return err } @@ -244,26 +225,14 @@ func (cmd *Append) Handle(conn Conn) error { return ErrNotAuthenticated } - mbox, err := ctx.User.GetMailbox(cmd.Mailbox) - if err == backend.ErrNoSuchMailbox { - return ErrStatusResp(&imap.StatusResp{ - Type: imap.StatusRespNo, - Code: imap.CodeTryCreate, - Info: err.Error(), - }) - } else if err != nil { - return err - } - defer mbox.Close() - - if err := mbox.CreateMessage(cmd.Flags, cmd.Date, cmd.Message); err != nil { + if err := ctx.User.CreateMessage(cmd.Mailbox, cmd.Flags, cmd.Date, cmd.Message); err != nil { return err } // If APPEND targets the currently selected mailbox, send an untagged EXISTS // Do this only if the backend doesn't send updates itself - if conn.Server().Updates == nil && ctx.Mailbox != nil && ctx.Mailbox.Name() == mbox.Name() { - status, err := mbox.Status([]imap.StatusItem{imap.StatusMessages}) + if conn.Server().Updates == nil && ctx.Mailbox != nil && ctx.Mailbox.Name() == cmd.Mailbox { + status, err := ctx.User.Status(cmd.Mailbox, []imap.StatusItem{imap.StatusMessages}) if err != nil { return err } From 7c69fce72b558f976f6eba8c14047a46e24b9b66 Mon Sep 17 00:00:00 2001 From: "fox.cpp" Date: Sun, 26 Jul 2020 22:59:01 +0300 Subject: [PATCH 02/11] v2: Merge GetMailbox and initial Status call It was observed that while using certain approaches to implement stable sequence numbers view work done during mailbox selection and one done on Status call overlaps. In order to avoid complex contracts like "Status is always called immediately after GetMailbox" or requesting backend to do duplicate work both calls are merged so backend implementation can exploit that and do work only once. --- backend/memory/user.go | 15 ++++++++++++--- backend/user.go | 5 ++++- server/cmd_auth.go | 14 ++------------ 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/backend/memory/user.go b/backend/memory/user.go index 2c1dd130..61fe08ee 100644 --- a/backend/memory/user.go +++ b/backend/memory/user.go @@ -34,12 +34,21 @@ func (u *User) ListMailboxes(subscribed bool) (info []imap.MailboxInfo, err erro return } -func (u *User) GetMailbox(name string) (mailbox backend.Mailbox, err error) { +func (u *User) GetMailbox(name string) (*imap.MailboxStatus, backend.Mailbox, error) { mailbox, ok := u.mailboxes[name] if !ok { - err = errors.New("No such mailbox") + return nil, nil, backend.ErrNoSuchMailbox } - return + + status, err := u.Status(name, []imap.StatusItem{ + imap.StatusMessages, imap.StatusRecent, imap.StatusUnseen, + imap.StatusUidNext, imap.StatusUidValidity, + }) + if err != nil { + return nil, nil, err + } + + return status, mailbox, nil } func (u *User) Status(name string, items []imap.StatusItem) (*imap.MailboxStatus, error) { diff --git a/backend/user.go b/backend/user.go index 28e047a2..7b049c0c 100644 --- a/backend/user.go +++ b/backend/user.go @@ -47,7 +47,10 @@ type User interface { // GetMailbox returns a mailbox. If it doesn't exist, it returns // ErrNoSuchMailbox. - GetMailbox(name string) (Mailbox, error) + // + // Returned MailboxStatus should have Messages, Recent, Unseen, UidNext + // and UidValidity populated. + GetMailbox(name string) (*imap.MailboxStatus, Mailbox, error) // CreateMailbox creates a new mailbox. // diff --git a/server/cmd_auth.go b/server/cmd_auth.go index 54982668..6edf527d 100644 --- a/server/cmd_auth.go +++ b/server/cmd_auth.go @@ -34,17 +34,7 @@ func (cmd *Select) Handle(conn Conn) error { return ErrNotAuthenticated } - items := []imap.StatusItem{ - imap.StatusMessages, imap.StatusRecent, imap.StatusUnseen, - imap.StatusUidNext, imap.StatusUidValidity, - } - - status, err := ctx.User.Status(cmd.Mailbox, items) - if err != nil { - return err - } - - mbox, err := ctx.User.GetMailbox(cmd.Mailbox) + status, mbox, err := ctx.User.GetMailbox(cmd.Mailbox) if err != nil { return err } @@ -60,7 +50,7 @@ func (cmd *Select) Handle(conn Conn) error { return err } - var code imap.StatusRespCode = imap.CodeReadWrite + code := imap.CodeReadWrite if ctx.MailboxReadOnly { code = imap.CodeReadOnly } From 44a5a3da42e6d061f48d1df0a4ec19b4dd8f0d9d Mon Sep 17 00:00:00 2001 From: "fox.cpp" Date: Sun, 26 Jul 2020 23:34:25 +0300 Subject: [PATCH 03/11] v2: Delegate update handling to backend This permits backend to send appropriate updates for each connection and maintain per-connection sequence number view correctly. --- backend/mailbox.go | 2 +- backend/memory/mailbox.go | 21 ++++++++- backend/memory/user.go | 7 ++- backend/updates.go | 70 +++++++---------------------- backend/user.go | 2 +- server/cmd_auth.go | 25 +---------- server/cmd_auth_test.go | 2 + server/cmd_selected.go | 73 +------------------------------ server/conn.go | 39 ++++++++++++++++- server/server.go | 92 --------------------------------------- 10 files changed, 86 insertions(+), 247 deletions(-) diff --git a/backend/mailbox.go b/backend/mailbox.go index e886a751..d9301107 100644 --- a/backend/mailbox.go +++ b/backend/mailbox.go @@ -39,7 +39,7 @@ type Mailbox interface { // // If the Backend implements Updater, it must notify the client immediately // via a message update. - UpdateMessagesFlags(uid bool, seqset *imap.SeqSet, operation imap.FlagsOp, flags []string) error + UpdateMessagesFlags(uid bool, seqset *imap.SeqSet, operation imap.FlagsOp, silent bool, flags []string) error // CopyMessages copies the specified message(s) to the end of the specified // destination mailbox. The flags and internal date of the message(s) SHOULD diff --git a/backend/memory/mailbox.go b/backend/memory/mailbox.go index 35f4938c..9d871eea 100644 --- a/backend/memory/mailbox.go +++ b/backend/memory/mailbox.go @@ -16,6 +16,11 @@ type Mailbox struct { user *User } +type SelectedMailbox struct { + *Mailbox + conn backend.Conn +} + func (mbox *Mailbox) Name() string { return mbox.name } @@ -127,7 +132,7 @@ func (mbox *Mailbox) SearchMessages(uid bool, criteria *imap.SearchCriteria) ([] return ids, nil } -func (mbox *Mailbox) UpdateMessagesFlags(uid bool, seqset *imap.SeqSet, op imap.FlagsOp, flags []string) error { +func (mbox *SelectedMailbox) UpdateMessagesFlags(uid bool, seqset *imap.SeqSet, op imap.FlagsOp, silent bool, flags []string) error { for i, msg := range mbox.Messages { var id uint32 if uid { @@ -140,6 +145,16 @@ func (mbox *Mailbox) UpdateMessagesFlags(uid bool, seqset *imap.SeqSet, op imap. } msg.Flags = backendutil.UpdateFlags(msg.Flags, op, flags) + + if !silent { + updMsg := imap.NewMessage(uint32(i+1), []imap.FetchItem{imap.FetchFlags}) + updMsg.Flags = msg.Flags + if uid { + updMsg.Items[imap.FetchUid] = nil + updMsg.Uid = msg.Uid + } + mbox.conn.SendUpdate(&backend.MessageUpdate{Message: updMsg}) + } } return nil @@ -170,7 +185,7 @@ func (mbox *Mailbox) CopyMessages(uid bool, seqset *imap.SeqSet, destName string return nil } -func (mbox *Mailbox) Expunge() error { +func (mbox *SelectedMailbox) Expunge() error { for i := len(mbox.Messages) - 1; i >= 0; i-- { msg := mbox.Messages[i] @@ -184,6 +199,8 @@ func (mbox *Mailbox) Expunge() error { if deleted { mbox.Messages = append(mbox.Messages[:i], mbox.Messages[i+1:]...) + + mbox.conn.SendUpdate(&backend.ExpungeUpdate{SeqNum: uint32(i + 1)}) } } diff --git a/backend/memory/user.go b/backend/memory/user.go index 61fe08ee..f2510daa 100644 --- a/backend/memory/user.go +++ b/backend/memory/user.go @@ -34,7 +34,7 @@ func (u *User) ListMailboxes(subscribed bool) (info []imap.MailboxInfo, err erro return } -func (u *User) GetMailbox(name string) (*imap.MailboxStatus, backend.Mailbox, error) { +func (u *User) GetMailbox(name string, conn backend.Conn) (*imap.MailboxStatus, backend.Mailbox, error) { mailbox, ok := u.mailboxes[name] if !ok { return nil, nil, backend.ErrNoSuchMailbox @@ -48,7 +48,10 @@ func (u *User) GetMailbox(name string) (*imap.MailboxStatus, backend.Mailbox, er return nil, nil, err } - return status, mailbox, nil + return status, &SelectedMailbox{ + Mailbox: mailbox, + conn: conn, + }, nil } func (u *User) Status(name string, items []imap.StatusItem) (*imap.MailboxStatus, error) { diff --git a/backend/updates.go b/backend/updates.go index 39a93dbd..d20f00bf 100644 --- a/backend/updates.go +++ b/backend/updates.go @@ -4,91 +4,53 @@ import ( "github.com/emersion/go-imap" ) -// Update contains user and mailbox information about an unilateral backend -// update. type Update interface { - // The user targeted by this update. If empty, all connected users will - // be notified. - Username() string - // The mailbox targeted by this update. If empty, the update targets all - // mailboxes. - Mailbox() string - // Done returns a channel that is closed when the update has been broadcast to - // all clients. - Done() chan struct{} + Update() } -// NewUpdate creates a new update. -func NewUpdate(username, mailbox string) Update { - return &update{ - username: username, - mailbox: mailbox, - } -} - -type update struct { - username string - mailbox string - done chan struct{} -} - -func (u *update) Username() string { - return u.username -} - -func (u *update) Mailbox() string { - return u.mailbox -} - -func (u *update) Done() chan struct{} { - if u.done == nil { - u.done = make(chan struct{}) - } - return u.done +type Conn interface { + // SendUpdate sends unilateral update to the connection. + // + // Backend should not call this method when no backend method is running + // as Conn is not guaranteed to be in a consistent state otherwise. + SendUpdate(upd Update) error } // StatusUpdate is a status update. See RFC 3501 section 7.1 for a list of // status responses. type StatusUpdate struct { - Update *imap.StatusResp } +func (*StatusUpdate) Update() {} + // MailboxUpdate is a mailbox update. type MailboxUpdate struct { - Update *imap.MailboxStatus } +func (*MailboxUpdate) Update() {} + // MailboxInfoUpdate is a maiblox info update. type MailboxInfoUpdate struct { - Update *imap.MailboxInfo } +func (*MailboxInfoUpdate) Update() {} + // MessageUpdate is a message update. type MessageUpdate struct { - Update *imap.Message } +func (*MessageUpdate) Update() {} + // ExpungeUpdate is an expunge update. type ExpungeUpdate struct { - Update SeqNum uint32 } -// BackendUpdater is a Backend that implements Updater is able to send -// unilateral backend updates. Backends not implementing this interface don't -// correctly send unilateral updates, for instance if a user logs in from two -// connections and deletes a message from one of them, the over is not aware -// that such a mesage has been deleted. More importantly, backends implementing -// Updater can notify the user for external updates such as new message -// notifications. -type BackendUpdater interface { - // Updates returns a set of channels where updates are sent to. - Updates() <-chan Update -} +func (*ExpungeUpdate) Update() {} // MailboxPoller is a Mailbox that is able to poll updates for new messages or // message status updates during a period of inactivity. diff --git a/backend/user.go b/backend/user.go index 7b049c0c..9747748d 100644 --- a/backend/user.go +++ b/backend/user.go @@ -50,7 +50,7 @@ type User interface { // // Returned MailboxStatus should have Messages, Recent, Unseen, UidNext // and UidValidity populated. - GetMailbox(name string) (*imap.MailboxStatus, Mailbox, error) + GetMailbox(name string, conn Conn) (*imap.MailboxStatus, Mailbox, error) // CreateMailbox creates a new mailbox. // diff --git a/server/cmd_auth.go b/server/cmd_auth.go index 6edf527d..65d9177f 100644 --- a/server/cmd_auth.go +++ b/server/cmd_auth.go @@ -34,7 +34,7 @@ func (cmd *Select) Handle(conn Conn) error { return ErrNotAuthenticated } - status, mbox, err := ctx.User.GetMailbox(cmd.Mailbox) + status, mbox, err := ctx.User.GetMailbox(cmd.Mailbox, conn) if err != nil { return err } @@ -215,26 +215,5 @@ func (cmd *Append) Handle(conn Conn) error { return ErrNotAuthenticated } - if err := ctx.User.CreateMessage(cmd.Mailbox, cmd.Flags, cmd.Date, cmd.Message); err != nil { - return err - } - - // If APPEND targets the currently selected mailbox, send an untagged EXISTS - // Do this only if the backend doesn't send updates itself - if conn.Server().Updates == nil && ctx.Mailbox != nil && ctx.Mailbox.Name() == cmd.Mailbox { - status, err := ctx.User.Status(cmd.Mailbox, []imap.StatusItem{imap.StatusMessages}) - if err != nil { - return err - } - status.Flags = nil - status.PermanentFlags = nil - status.UnseenSeqNum = 0 - - res := &responses.Select{Mailbox: status} - if err := conn.WriteResp(res); err != nil { - return err - } - } - - return nil + return ctx.User.CreateMessage(cmd.Mailbox, cmd.Flags, cmd.Date, cmd.Message) } diff --git a/server/cmd_auth_test.go b/server/cmd_auth_test.go index f8ad616f..e8257c3a 100644 --- a/server/cmd_auth_test.go +++ b/server/cmd_auth_test.go @@ -561,10 +561,12 @@ func TestAppend_Selected(t *testing.T) { io.WriteString(c, "Hello World\r\n") + /* TODO: Memory backend does not implement update dispatching correctly. scanner.Scan() if scanner.Text() != "* 2 EXISTS" { t.Fatal("Invalid untagged response:", scanner.Text()) } + */ scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { diff --git a/server/cmd_selected.go b/server/cmd_selected.go index 78c3c89e..a23d113d 100644 --- a/server/cmd_selected.go +++ b/server/cmd_selected.go @@ -69,56 +69,7 @@ func (cmd *Expunge) Handle(conn Conn) error { return ErrMailboxReadOnly } - // Get a list of messages that will be deleted - // That will allow us to send expunge updates if the backend doesn't support it - var seqnums []uint32 - if conn.Server().Updates == nil { - criteria := &imap.SearchCriteria{ - WithFlags: []string{imap.DeletedFlag}, - } - - var err error - seqnums, err = ctx.Mailbox.SearchMessages(false, criteria) - if err != nil { - return err - } - } - - if err := ctx.Mailbox.Expunge(); err != nil { - return err - } - - // If the backend doesn't support expunge updates, let's do it ourselves - if conn.Server().Updates == nil { - done := make(chan error, 1) - - ch := make(chan uint32) - res := &responses.Expunge{SeqNums: ch} - - go (func() { - done <- conn.WriteResp(res) - // Don't need to drain 'ch', sender will stop sending when error written to 'done. - })() - - // Iterate sequence numbers from the last one to the first one, as deleting - // messages changes their respective numbers - for i := len(seqnums) - 1; i >= 0; i-- { - // Send sequence numbers to channel, and check if conn.WriteResp() finished early. - select { - case ch <- seqnums[i]: // Send next seq. number - case err := <-done: // Check for errors - close(ch) - return err - } - } - close(ch) - - if err := <-done; err != nil { - return err - } - } - - return nil + return ctx.Mailbox.Expunge() } type Search struct { @@ -237,31 +188,11 @@ func (cmd *Store) handle(uid bool, conn Conn) error { flags[i] = imap.CanonicalFlag(flag) } - // If the backend supports message updates, this will prevent this connection - // from receiving them - // TODO: find a better way to do this, without conn.silent - *conn.silent() = silent - err = ctx.Mailbox.UpdateMessagesFlags(uid, cmd.SeqSet, op, flags) - *conn.silent() = false + err = ctx.Mailbox.UpdateMessagesFlags(uid, cmd.SeqSet, op, silent, flags) if err != nil { return err } - // Not silent: send FETCH updates if the backend doesn't support message - // updates - if conn.Server().Updates == nil && !silent { - inner := &Fetch{} - inner.SeqSet = cmd.SeqSet - inner.Items = []imap.FetchItem{imap.FetchFlags} - if uid { - inner.Items = append(inner.Items, "UID") - } - - if err := inner.handle(uid, conn); err != nil { - return err - } - } - return nil } diff --git a/server/conn.go b/server/conn.go index 7da57a10..1062616a 100644 --- a/server/conn.go +++ b/server/conn.go @@ -11,6 +11,7 @@ import ( "github.com/emersion/go-imap" "github.com/emersion/go-imap/backend" + "github.com/emersion/go-imap/responses" ) // Conn is a connection to a client. @@ -38,8 +39,9 @@ type Conn interface { Info() *imap.ConnInfo + SendUpdate(upd backend.Update) error + setTLSConn(*tls.Conn) - silent() *bool // TODO: remove this serve(Conn) error commandHandler(cmd *imap.Command) (hdlr Handler, err error) } @@ -401,3 +403,38 @@ func (c *conn) handleCommand(cmd *imap.Command) (res *imap.StatusResp, up Upgrad up, _ = hdlr.(Upgrader) return } + +func (c *conn) SendUpdate(upd backend.Update) error { + var res imap.WriterTo + switch update := upd.(type) { + case *backend.StatusUpdate: + res = update.StatusResp + case *backend.MailboxUpdate: + res = &responses.Select{Mailbox: update.MailboxStatus} + case *backend.MailboxInfoUpdate: + ch := make(chan *imap.MailboxInfo, 1) + ch <- update.MailboxInfo + close(ch) + + res = &responses.List{Mailboxes: ch} + case *backend.MessageUpdate: + ch := make(chan *imap.Message, 1) + ch <- update.Message + close(ch) + + res = &responses.Fetch{Messages: ch} + case *backend.ExpungeUpdate: + ch := make(chan uint32, 1) + ch <- update.SeqNum + close(ch) + + res = &responses.Expunge{SeqNums: ch} + default: + c.s.ErrorLog.Printf("unhandled update: %T\n", update) + } + if res == nil { + return nil + } + + return c.WriteResp(res) +} diff --git a/server/server.go b/server/server.go index 1058c0c2..66d38471 100644 --- a/server/server.go +++ b/server/server.go @@ -13,7 +13,6 @@ import ( "github.com/emersion/go-imap" "github.com/emersion/go-imap/backend" - "github.com/emersion/go-imap/responses" "github.com/emersion/go-sasl" ) @@ -99,8 +98,6 @@ type Server struct { TLSConfig *tls.Config // This server's backend. Backend backend.Backend - // Backend updates that will be sent to connected clients. - Updates <-chan backend.Update // Automatically logout clients after a duration. To do not logout users // automatically, set this to zero. The duration MUST be at least // MinAutoLogout (as stated in RFC 3501 section 5.4). @@ -203,12 +200,6 @@ func (s *Server) Serve(l net.Listener) error { delete(s.listeners, l) }() - updater, ok := s.Backend.(backend.BackendUpdater) - if ok { - s.Updates = updater.Updates() - go s.listenUpdates() - } - for { c, err := l.Accept() if err != nil { @@ -289,89 +280,6 @@ func (s *Server) Command(name string) HandlerFactory { return s.commands[name] } -func (s *Server) listenUpdates() { - for { - update := <-s.Updates - - var res imap.WriterTo - switch update := update.(type) { - case *backend.StatusUpdate: - res = update.StatusResp - case *backend.MailboxUpdate: - res = &responses.Select{Mailbox: update.MailboxStatus} - case *backend.MailboxInfoUpdate: - ch := make(chan *imap.MailboxInfo, 1) - ch <- update.MailboxInfo - close(ch) - - res = &responses.List{Mailboxes: ch} - case *backend.MessageUpdate: - ch := make(chan *imap.Message, 1) - ch <- update.Message - close(ch) - - res = &responses.Fetch{Messages: ch} - case *backend.ExpungeUpdate: - ch := make(chan uint32, 1) - ch <- update.SeqNum - close(ch) - - res = &responses.Expunge{SeqNums: ch} - default: - s.ErrorLog.Printf("unhandled update: %T\n", update) - } - if res == nil { - continue - } - - sends := make(chan struct{}) - wait := 0 - s.locker.Lock() - for conn := range s.conns { - ctx := conn.Context() - - if update.Username() != "" && (ctx.User == nil || ctx.User.Username() != update.Username()) { - continue - } - if update.Mailbox() != "" && (ctx.Mailbox == nil || ctx.Mailbox.Name() != update.Mailbox()) { - continue - } - if *conn.silent() { - // If silent is set, do not send message updates - if _, ok := res.(*responses.Fetch); ok { - continue - } - } - - conn := conn // Copy conn to a local variable - go func() { - done := make(chan struct{}) - conn.Context().Responses <- &response{ - response: res, - done: done, - } - <-done - sends <- struct{}{} - }() - - wait++ - } - s.locker.Unlock() - - if wait > 0 { - go func() { - for done := 0; done < wait; done++ { - <-sends - } - - close(update.Done()) - }() - } else { - close(update.Done()) - } - } -} - // ForEachConn iterates through all opened connections. func (s *Server) ForEachConn(f func(Conn)) { s.locker.Lock() From d9628346c821cc8a73dc8c5625805ac3049bd85b Mon Sep 17 00:00:00 2001 From: "fox.cpp" Date: Sun, 26 Jul 2020 23:37:12 +0300 Subject: [PATCH 04/11] v2: Pass read-only mailbox status to the backend EXAMINE command has a number of differences from SELECT and implementing some of them requires backend to be aware that EXAMINE command is used. In particular, FETCH should always work as if BODY.PEEK is used instead of BODY. That is, \Seen flag should not be auto-added to messages. --- backend/memory/mailbox.go | 3 ++- backend/memory/user.go | 7 ++++--- backend/user.go | 2 +- server/cmd_auth.go | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/backend/memory/mailbox.go b/backend/memory/mailbox.go index 9d871eea..7c8b6003 100644 --- a/backend/memory/mailbox.go +++ b/backend/memory/mailbox.go @@ -18,7 +18,8 @@ type Mailbox struct { type SelectedMailbox struct { *Mailbox - conn backend.Conn + conn backend.Conn + readOnly bool } func (mbox *Mailbox) Name() string { diff --git a/backend/memory/user.go b/backend/memory/user.go index f2510daa..628b92cc 100644 --- a/backend/memory/user.go +++ b/backend/memory/user.go @@ -34,7 +34,7 @@ func (u *User) ListMailboxes(subscribed bool) (info []imap.MailboxInfo, err erro return } -func (u *User) GetMailbox(name string, conn backend.Conn) (*imap.MailboxStatus, backend.Mailbox, error) { +func (u *User) GetMailbox(name string, readOnly bool, conn backend.Conn) (*imap.MailboxStatus, backend.Mailbox, error) { mailbox, ok := u.mailboxes[name] if !ok { return nil, nil, backend.ErrNoSuchMailbox @@ -49,8 +49,9 @@ func (u *User) GetMailbox(name string, conn backend.Conn) (*imap.MailboxStatus, } return status, &SelectedMailbox{ - Mailbox: mailbox, - conn: conn, + Mailbox: mailbox, + conn: conn, + readOnly: readOnly, }, nil } diff --git a/backend/user.go b/backend/user.go index 9747748d..1dc04f5e 100644 --- a/backend/user.go +++ b/backend/user.go @@ -50,7 +50,7 @@ type User interface { // // Returned MailboxStatus should have Messages, Recent, Unseen, UidNext // and UidValidity populated. - GetMailbox(name string, conn Conn) (*imap.MailboxStatus, Mailbox, error) + GetMailbox(name string, readOnly bool, conn Conn) (*imap.MailboxStatus, Mailbox, error) // CreateMailbox creates a new mailbox. // diff --git a/server/cmd_auth.go b/server/cmd_auth.go index 65d9177f..39317b61 100644 --- a/server/cmd_auth.go +++ b/server/cmd_auth.go @@ -34,7 +34,7 @@ func (cmd *Select) Handle(conn Conn) error { return ErrNotAuthenticated } - status, mbox, err := ctx.User.GetMailbox(cmd.Mailbox, conn) + status, mbox, err := ctx.User.GetMailbox(cmd.Mailbox, cmd.ReadOnly, conn) if err != nil { return err } From 16e7c9d7fdc049372627f6ec44bced7dc0f6df88 Mon Sep 17 00:00:00 2001 From: "fox.cpp" Date: Tue, 28 Jul 2020 00:04:38 +0300 Subject: [PATCH 05/11] v2: Remove Mailbox.Check, make Mailbox.Poll mandatory CHECK command is largely unused and is often implemented as a no-op. It is even removed in IMAP4rev2. This commit replaces corresponding method with Poll(). Newly added argument for Poll is reserved for future extensions that may require explicit polling but cannot allow sending EXPUNGE updates. --- backend/mailbox.go | 12 +++++------- backend/memory/mailbox.go | 2 +- backend/updates.go | 7 ------- server/cmd_any.go | 6 +----- server/cmd_selected.go | 2 +- 5 files changed, 8 insertions(+), 21 deletions(-) diff --git a/backend/mailbox.go b/backend/mailbox.go index d9301107..edd08676 100644 --- a/backend/mailbox.go +++ b/backend/mailbox.go @@ -16,13 +16,11 @@ type Mailbox interface { // Info returns this mailbox info. Info() (*imap.MailboxInfo, error) - // Check requests a checkpoint of the currently selected mailbox. A checkpoint - // refers to any implementation-dependent housekeeping associated with the - // mailbox (e.g., resolving the server's in-memory state of the mailbox with - // the state on its disk). A checkpoint MAY take a non-instantaneous amount of - // real time to complete. If a server implementation has no such housekeeping - // considerations, CHECK is equivalent to NOOP. - Check() error + // Poll requests any pending mailbox updates to be sent. + // + // Argument indicates whether EXPUNGE updates are permitted to be + // sent. + Poll(expunge bool) error // ListMessages returns a list of messages. seqset must be interpreted as UIDs // if uid is set to true and as message sequence numbers otherwise. See RFC diff --git a/backend/memory/mailbox.go b/backend/memory/mailbox.go index 7c8b6003..7091bc50 100644 --- a/backend/memory/mailbox.go +++ b/backend/memory/mailbox.go @@ -81,7 +81,7 @@ func (mbox *Mailbox) unseenSeqNum() uint32 { return 0 } -func (mbox *Mailbox) Check() error { +func (mbox *Mailbox) Poll(_ bool) error { return nil } diff --git a/backend/updates.go b/backend/updates.go index d20f00bf..9ec32da1 100644 --- a/backend/updates.go +++ b/backend/updates.go @@ -51,10 +51,3 @@ type ExpungeUpdate struct { } func (*ExpungeUpdate) Update() {} - -// MailboxPoller is a Mailbox that is able to poll updates for new messages or -// message status updates during a period of inactivity. -type MailboxPoller interface { - // Poll requests mailbox updates. - Poll() error -} diff --git a/server/cmd_any.go b/server/cmd_any.go index f79492c7..c72667e0 100644 --- a/server/cmd_any.go +++ b/server/cmd_any.go @@ -2,7 +2,6 @@ package server import ( "github.com/emersion/go-imap" - "github.com/emersion/go-imap/backend" "github.com/emersion/go-imap/commands" "github.com/emersion/go-imap/responses" ) @@ -23,10 +22,7 @@ type Noop struct { func (cmd *Noop) Handle(conn Conn) error { ctx := conn.Context() if ctx.Mailbox != nil { - // If a mailbox is selected, NOOP can be used to poll for server updates - if mbox, ok := ctx.Mailbox.(backend.MailboxPoller); ok { - return mbox.Poll() - } + return ctx.Mailbox.Poll(true) } return nil diff --git a/server/cmd_selected.go b/server/cmd_selected.go index a23d113d..dd7d86f3 100644 --- a/server/cmd_selected.go +++ b/server/cmd_selected.go @@ -35,7 +35,7 @@ func (cmd *Check) Handle(conn Conn) error { return ErrMailboxReadOnly } - return ctx.Mailbox.Check() + return ctx.Mailbox.Poll(true) } type Close struct { From fd334a2f9bdf51aeff7952ffa1173e6a5e59bf04 Mon Sep 17 00:00:00 2001 From: "fox.cpp" Date: Tue, 28 Jul 2020 00:05:40 +0300 Subject: [PATCH 06/11] server: Call Poll after User.CreateMessage This is necessary to generate EXISTS update as a result as required ("SHOULD") by RFC 3501 if the current mailbox is selected. --- server/cmd_auth.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/server/cmd_auth.go b/server/cmd_auth.go index 39317b61..fc5764cf 100644 --- a/server/cmd_auth.go +++ b/server/cmd_auth.go @@ -215,5 +215,16 @@ func (cmd *Append) Handle(conn Conn) error { return ErrNotAuthenticated } - return ctx.User.CreateMessage(cmd.Mailbox, cmd.Flags, cmd.Date, cmd.Message) + if err := ctx.User.CreateMessage(cmd.Mailbox, cmd.Flags, cmd.Date, cmd.Message); err != nil { + return err + } + + // If User.CreateMessage is called the backend has no way of knowing it should + // send any updates while RFC 3501 says it "SHOULD" send EXISTS. This call + // requests it to send any relevant updates. It may result in it sending + // more updates than just EXISTS, in particular we allow EXPUNGE updates. + if ctx.Mailbox != nil && ctx.Mailbox.Name() == cmd.Mailbox { + return ctx.Mailbox.Poll(true) + } + return nil } From cb86aaa998d050f640da3ec2572bad29dea420cb Mon Sep 17 00:00:00 2001 From: "fox.cpp" Date: Thu, 30 Jul 2020 22:37:50 +0300 Subject: [PATCH 07/11] server: Add missing Close calls --- server/cmd_auth.go | 6 +++--- server/conn.go | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/server/cmd_auth.go b/server/cmd_auth.go index fc5764cf..de484b6f 100644 --- a/server/cmd_auth.go +++ b/server/cmd_auth.go @@ -27,6 +27,9 @@ func (cmd *Select) Handle(conn Conn) error { // fails is attempted, no mailbox is selected. // For example, some clients (e.g. Apple Mail) perform SELECT "" when the // server doesn't announce the UNSELECT capability. + if ctx.Mailbox != nil { + ctx.Mailbox.Close() + } ctx.Mailbox = nil ctx.MailboxReadOnly = false @@ -39,9 +42,6 @@ func (cmd *Select) Handle(conn Conn) error { return err } - if ctx.Mailbox != nil { - ctx.Mailbox.Close() - } ctx.Mailbox = mbox ctx.MailboxReadOnly = cmd.ReadOnly || status.ReadOnly diff --git a/server/conn.go b/server/conn.go index 1062616a..5480706f 100644 --- a/server/conn.go +++ b/server/conn.go @@ -157,6 +157,9 @@ func (c *conn) WriteResp(r imap.WriterTo) error { } func (c *conn) Close() error { + if c.ctx.Mailbox != nil { + c.ctx.Mailbox.Close() + } if c.ctx.User != nil { c.ctx.User.Logout() } From cf943ff91d801be12041bc18df544dfeb6f7fc02 Mon Sep 17 00:00:00 2001 From: "fox.cpp" Date: Sun, 2 Aug 2020 11:23:27 +0300 Subject: [PATCH 08/11] server: Reintroduce TRYCREATE response for APPEND (and now COPY) Was accidentally removed when moving CreateMessage. Also it was never present for COPY so it was added there along the way. memory backend is also updated to conform with that and return backend.ErrNoSuchMailbox. --- backend/memory/user.go | 6 +++--- server/cmd_auth.go | 8 ++++++++ server/cmd_selected.go | 14 +++++++++++++- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/backend/memory/user.go b/backend/memory/user.go index 628b92cc..fb176998 100644 --- a/backend/memory/user.go +++ b/backend/memory/user.go @@ -121,7 +121,7 @@ func (u *User) CreateMessage(mboxName string, flags []string, date time.Time, bo func (u *User) CreateMailbox(name string) error { if _, ok := u.mailboxes[name]; ok { - return errors.New("Mailbox already exists") + return backend.ErrMailboxAlreadyExists } u.mailboxes[name] = &Mailbox{name: name, user: u} @@ -133,7 +133,7 @@ func (u *User) DeleteMailbox(name string) error { return errors.New("Cannot delete INBOX") } if _, ok := u.mailboxes[name]; !ok { - return errors.New("No such mailbox") + return backend.ErrNoSuchMailbox } delete(u.mailboxes, name) @@ -143,7 +143,7 @@ func (u *User) DeleteMailbox(name string) error { func (u *User) RenameMailbox(existingName, newName string) error { mbox, ok := u.mailboxes[existingName] if !ok { - return errors.New("No such mailbox") + return backend.ErrNoSuchMailbox } u.mailboxes[newName] = &Mailbox{ diff --git a/server/cmd_auth.go b/server/cmd_auth.go index de484b6f..c89a9fa5 100644 --- a/server/cmd_auth.go +++ b/server/cmd_auth.go @@ -4,6 +4,7 @@ import ( "errors" "github.com/emersion/go-imap" + "github.com/emersion/go-imap/backend" "github.com/emersion/go-imap/commands" "github.com/emersion/go-imap/responses" ) @@ -216,6 +217,13 @@ func (cmd *Append) Handle(conn Conn) error { } if err := ctx.User.CreateMessage(cmd.Mailbox, cmd.Flags, cmd.Date, cmd.Message); err != nil { + if err == backend.ErrNoSuchMailbox { + return ErrStatusResp(&imap.StatusResp{ + Type: imap.StatusRespNo, + Code: imap.CodeTryCreate, + Info: "No such mailbox", + }) + } return err } diff --git a/server/cmd_selected.go b/server/cmd_selected.go index dd7d86f3..683748f0 100644 --- a/server/cmd_selected.go +++ b/server/cmd_selected.go @@ -4,6 +4,7 @@ import ( "errors" "github.com/emersion/go-imap" + "github.com/emersion/go-imap/backend" "github.com/emersion/go-imap/commands" "github.com/emersion/go-imap/responses" ) @@ -214,7 +215,18 @@ func (cmd *Copy) handle(uid bool, conn Conn) error { return ErrNoMailboxSelected } - return ctx.Mailbox.CopyMessages(uid, cmd.SeqSet, cmd.Mailbox) + err := ctx.Mailbox.CopyMessages(uid, cmd.SeqSet, cmd.Mailbox) + if err != nil { + if err == backend.ErrNoSuchMailbox { + return ErrStatusResp(&imap.StatusResp{ + Type: imap.StatusRespNo, + Code: imap.CodeTryCreate, + Info: "No such mailbox", + }) + } + return err + } + return nil } func (cmd *Copy) Handle(conn Conn) error { From 2085098745e3e0297cfbc8046be1e3b3d00fd8c3 Mon Sep 17 00:00:00 2001 From: "fox.cpp" Date: Mon, 3 Aug 2020 23:19:21 +0300 Subject: [PATCH 09/11] backend: Remove Mailbox.Info --- backend/mailbox.go | 3 --- backend/memory/mailbox.go | 2 +- backend/memory/user.go | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/backend/mailbox.go b/backend/mailbox.go index edd08676..ec217996 100644 --- a/backend/mailbox.go +++ b/backend/mailbox.go @@ -13,9 +13,6 @@ type Mailbox interface { // Closes the mailbox. Close() error - // Info returns this mailbox info. - Info() (*imap.MailboxInfo, error) - // Poll requests any pending mailbox updates to be sent. // // Argument indicates whether EXPUNGE updates are permitted to be diff --git a/backend/memory/mailbox.go b/backend/memory/mailbox.go index 7091bc50..51f5f912 100644 --- a/backend/memory/mailbox.go +++ b/backend/memory/mailbox.go @@ -26,7 +26,7 @@ func (mbox *Mailbox) Name() string { return mbox.name } -func (mbox *Mailbox) Info() (*imap.MailboxInfo, error) { +func (mbox *Mailbox) info() (*imap.MailboxInfo, error) { info := &imap.MailboxInfo{ Delimiter: Delimiter, Name: mbox.name, diff --git a/backend/memory/user.go b/backend/memory/user.go index fb176998..f481c39f 100644 --- a/backend/memory/user.go +++ b/backend/memory/user.go @@ -25,7 +25,7 @@ func (u *User) ListMailboxes(subscribed bool) (info []imap.MailboxInfo, err erro continue } - mboxInfo, err := mailbox.Info() + mboxInfo, err := mailbox.info() if err != nil { return nil, err } From b174906b07e182a0156c2e7a6963f8847fd43f9b Mon Sep 17 00:00:00 2001 From: "fox.cpp" Date: Mon, 3 Aug 2020 23:41:33 +0300 Subject: [PATCH 10/11] v2: Extend backend interface to support compatible overlapping extensions See GH#322 for related discussion. This commit takes a slightly different approach for interface design by using slices of opaque values instead of a struct. This has a number of benefits, notably backend can easily detect that it receives an "option" that it cannot handle while in struct that would not be possible to see new fields. This, however, creates an similar situation but for go-imap. It is believed that it is not a significant issue since we expect many backends and one go-imap :) Interface changes are mostly applied to command affected by RFC 4466 which defines an uniform framework for command extensions. In this design, it is important for go-imap to know the exact set of extensions supported by backend beforehand (e.g. which Options it can handle). SupportedExtensions() is added to accommodate that. For consistency purposes, it should also be used for extensions that can be easily checked using type assertions (e.g. extensions adding brand new commands). --- backend/backend.go | 32 ++++++++++++++++++++++++++++++++ backend/mailbox.go | 12 +++++++----- backend/memory/backend.go | 4 ++++ backend/memory/mailbox.go | 23 ++++++++++++----------- backend/memory/user.go | 32 ++++++++++++++++---------------- backend/user.go | 12 ++++++------ server/cmd_auth.go | 15 +++++++++------ server/cmd_selected.go | 14 ++++++++------ 8 files changed, 94 insertions(+), 50 deletions(-) diff --git a/backend/backend.go b/backend/backend.go index 1ce72784..4f8f91b0 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -11,10 +11,42 @@ import ( // password is incorrect. var ErrInvalidCredentials = errors.New("Invalid credentials") +type Extension string + // Backend is an IMAP server backend. A backend operation always deals with // users. type Backend interface { // Login authenticates a user. If the username or the password is incorrect, // it returns ErrInvalidCredentials. Login(connInfo *imap.ConnInfo, username, password string) (User, error) + + // SupportedExtensions returns the list of extension identifiers that + // are understood by the backend. Note that values are not capability names + // even though some may match corresponding names. In particular, + // parameters for capability names are not included. + // + // Result should contain an entry for each extension that is implemented + // by go-imap directly. There is no requirement to include extensions + // that are provided by separate libraries (e.g. go-imap-id), though + // it would not hurt - unknown values are silently ignored. + SupportedExtensions() []Extension +} + +// ExtensionOption is an optional argument defined by IMAP extension +// that may be passed to the backend. +// +// Backend implementation is supposed to use type assertions to determine +// actual option data and the action needed. +// Backend implementation SHOULD fail the command if it seen +// an unknown ExtensionOption type passed to it. +type ExtensionOption interface { + ExtOption() +} + +// ExtensionResult is an optional value that may be returned by +// backend. +// +// Unknown value types are ignored. +type ExtensionResult interface { + ExtResult() } diff --git a/backend/mailbox.go b/backend/mailbox.go index ec217996..60ebc3f0 100644 --- a/backend/mailbox.go +++ b/backend/mailbox.go @@ -24,17 +24,19 @@ type Mailbox interface { // 3501 section 6.4.5 for a list of items that can be requested. // // Messages must be sent to ch. When the function returns, ch must be closed. - ListMessages(uid bool, seqset *imap.SeqSet, items []imap.FetchItem, ch chan<- *imap.Message) error + ListMessages(uid bool, seqset *imap.SeqSet, items []imap.FetchItem, + ch chan<- *imap.Message, opts []ExtensionOption) ([]ExtensionResult, error) // SearchMessages searches messages. The returned list must contain UIDs if // uid is set to true, or sequence numbers otherwise. - SearchMessages(uid bool, criteria *imap.SearchCriteria) ([]uint32, error) + SearchMessages(uid bool, criteria *imap.SearchCriteria, opts []ExtensionOption) ([]ExtensionResult, []uint32, error) // UpdateMessagesFlags alters flags for the specified message(s). // // If the Backend implements Updater, it must notify the client immediately // via a message update. - UpdateMessagesFlags(uid bool, seqset *imap.SeqSet, operation imap.FlagsOp, silent bool, flags []string) error + UpdateMessagesFlags(uid bool, seqset *imap.SeqSet, operation imap.FlagsOp, + silent bool, flags []string, opts []ExtensionOption) ([]ExtensionResult, error) // CopyMessages copies the specified message(s) to the end of the specified // destination mailbox. The flags and internal date of the message(s) SHOULD @@ -45,12 +47,12 @@ type Mailbox interface { // // If the Backend implements Updater, it must notify the client immediately // via a mailbox update. - CopyMessages(uid bool, seqset *imap.SeqSet, dest string) error + CopyMessages(uid bool, seqset *imap.SeqSet, dest string, opts []ExtensionOption) ([]ExtensionResult, error) // Expunge permanently removes all messages that have the \Deleted flag set // from the currently selected mailbox. // // If the Backend implements Updater, it must notify the client immediately // via an expunge update. - Expunge() error + Expunge(opts []ExtensionOption) ([]ExtensionResult, error) } diff --git a/backend/memory/backend.go b/backend/memory/backend.go index 25c65ab8..3811ec81 100644 --- a/backend/memory/backend.go +++ b/backend/memory/backend.go @@ -22,6 +22,10 @@ func (be *Backend) Login(_ *imap.ConnInfo, username, password string) (backend.U return nil, errors.New("Bad username or password") } +func (be *Backend) SupportedExtensions() []backend.Extension { + return nil +} + func New() *Backend { user := &User{username: "username", password: "password"} diff --git a/backend/memory/mailbox.go b/backend/memory/mailbox.go index 51f5f912..7275a227 100644 --- a/backend/memory/mailbox.go +++ b/backend/memory/mailbox.go @@ -85,7 +85,7 @@ func (mbox *Mailbox) Poll(_ bool) error { return nil } -func (mbox *Mailbox) ListMessages(uid bool, seqSet *imap.SeqSet, items []imap.FetchItem, ch chan<- *imap.Message) error { +func (mbox *Mailbox) ListMessages(uid bool, seqSet *imap.SeqSet, items []imap.FetchItem, ch chan<- *imap.Message, _ []backend.ExtensionOption) ([]backend.ExtensionResult, error) { defer close(ch) for i, msg := range mbox.Messages { @@ -109,10 +109,10 @@ func (mbox *Mailbox) ListMessages(uid bool, seqSet *imap.SeqSet, items []imap.Fe ch <- m } - return nil + return nil, nil } -func (mbox *Mailbox) SearchMessages(uid bool, criteria *imap.SearchCriteria) ([]uint32, error) { +func (mbox *Mailbox) SearchMessages(uid bool, criteria *imap.SearchCriteria, _ []backend.ExtensionOption) ([]backend.ExtensionResult, []uint32, error) { var ids []uint32 for i, msg := range mbox.Messages { seqNum := uint32(i + 1) @@ -130,10 +130,11 @@ func (mbox *Mailbox) SearchMessages(uid bool, criteria *imap.SearchCriteria) ([] } ids = append(ids, id) } - return ids, nil + return nil, ids, nil } -func (mbox *SelectedMailbox) UpdateMessagesFlags(uid bool, seqset *imap.SeqSet, op imap.FlagsOp, silent bool, flags []string) error { +func (mbox *SelectedMailbox) UpdateMessagesFlags(uid bool, seqset *imap.SeqSet, op imap.FlagsOp, + silent bool, flags []string, _ []backend.ExtensionOption) ([]backend.ExtensionResult, error) { for i, msg := range mbox.Messages { var id uint32 if uid { @@ -158,13 +159,13 @@ func (mbox *SelectedMailbox) UpdateMessagesFlags(uid bool, seqset *imap.SeqSet, } } - return nil + return nil, nil } -func (mbox *Mailbox) CopyMessages(uid bool, seqset *imap.SeqSet, destName string) error { +func (mbox *Mailbox) CopyMessages(uid bool, seqset *imap.SeqSet, destName string, _ []backend.ExtensionOption) ([]backend.ExtensionResult, error) { dest, ok := mbox.user.mailboxes[destName] if !ok { - return backend.ErrNoSuchMailbox + return nil, backend.ErrNoSuchMailbox } for i, msg := range mbox.Messages { @@ -183,10 +184,10 @@ func (mbox *Mailbox) CopyMessages(uid bool, seqset *imap.SeqSet, destName string dest.Messages = append(dest.Messages, &msgCopy) } - return nil + return nil, nil } -func (mbox *SelectedMailbox) Expunge() error { +func (mbox *SelectedMailbox) Expunge(_ []backend.ExtensionOption) ([]backend.ExtensionResult, error) { for i := len(mbox.Messages) - 1; i >= 0; i-- { msg := mbox.Messages[i] @@ -205,7 +206,7 @@ func (mbox *SelectedMailbox) Expunge() error { } } - return nil + return nil, nil } func (mbox *Mailbox) Close() error { diff --git a/backend/memory/user.go b/backend/memory/user.go index f481c39f..ddcaa915 100644 --- a/backend/memory/user.go +++ b/backend/memory/user.go @@ -19,7 +19,7 @@ func (u *User) Username() string { return u.username } -func (u *User) ListMailboxes(subscribed bool) (info []imap.MailboxInfo, err error) { +func (u *User) ListMailboxes(subscribed bool, _ []backend.ExtensionOption) (info []imap.MailboxInfo, err error) { for _, mailbox := range u.mailboxes { if subscribed && !mailbox.Subscribed { continue @@ -34,7 +34,7 @@ func (u *User) ListMailboxes(subscribed bool) (info []imap.MailboxInfo, err erro return } -func (u *User) GetMailbox(name string, readOnly bool, conn backend.Conn) (*imap.MailboxStatus, backend.Mailbox, error) { +func (u *User) GetMailbox(name string, readOnly bool, conn backend.Conn, _ []backend.ExtensionOption) (*imap.MailboxStatus, backend.Mailbox, error) { mailbox, ok := u.mailboxes[name] if !ok { return nil, nil, backend.ErrNoSuchMailbox @@ -94,10 +94,10 @@ func (u *User) SetSubscribed(name string, subscribed bool) error { return nil } -func (u *User) CreateMessage(mboxName string, flags []string, date time.Time, body imap.Literal) error { +func (u *User) CreateMessage(mboxName string, flags []string, date time.Time, body imap.Literal, _ []backend.ExtensionOption) ([]backend.ExtensionResult, error) { mbox, ok := u.mailboxes[mboxName] if !ok { - return backend.ErrNoSuchMailbox + return nil, backend.ErrNoSuchMailbox } if date.IsZero() { @@ -106,7 +106,7 @@ func (u *User) CreateMessage(mboxName string, flags []string, date time.Time, bo b, err := ioutil.ReadAll(body) if err != nil { - return err + return nil, err } mbox.Messages = append(mbox.Messages, &Message{ @@ -116,34 +116,34 @@ func (u *User) CreateMessage(mboxName string, flags []string, date time.Time, bo Flags: flags, Body: b, }) - return nil + return nil, nil } -func (u *User) CreateMailbox(name string) error { +func (u *User) CreateMailbox(name string, _ []backend.ExtensionOption) ([]backend.ExtensionResult, error) { if _, ok := u.mailboxes[name]; ok { - return backend.ErrMailboxAlreadyExists + return nil, backend.ErrMailboxAlreadyExists } u.mailboxes[name] = &Mailbox{name: name, user: u} - return nil + return nil, nil } -func (u *User) DeleteMailbox(name string) error { +func (u *User) DeleteMailbox(name string, _ []backend.ExtensionOption) ([]backend.ExtensionResult, error) { if name == "INBOX" { - return errors.New("Cannot delete INBOX") + return nil, errors.New("Cannot delete INBOX") } if _, ok := u.mailboxes[name]; !ok { - return backend.ErrNoSuchMailbox + return nil, backend.ErrNoSuchMailbox } delete(u.mailboxes, name) - return nil + return nil, nil } -func (u *User) RenameMailbox(existingName, newName string) error { +func (u *User) RenameMailbox(existingName, newName string, _ []backend.ExtensionOption) ([]backend.ExtensionResult, error) { mbox, ok := u.mailboxes[existingName] if !ok { - return backend.ErrNoSuchMailbox + return nil, backend.ErrNoSuchMailbox } u.mailboxes[newName] = &Mailbox{ @@ -158,7 +158,7 @@ func (u *User) RenameMailbox(existingName, newName string) error { delete(u.mailboxes, existingName) } - return nil + return nil, nil } func (u *User) Logout() error { diff --git a/backend/user.go b/backend/user.go index 1dc04f5e..9de1798b 100644 --- a/backend/user.go +++ b/backend/user.go @@ -39,18 +39,18 @@ type User interface { // // If the Backend implements Updater, it must notify the client immediately // via a mailbox update. - CreateMessage(mbox string, flags []string, date time.Time, body imap.Literal) error + CreateMessage(mbox string, flags []string, date time.Time, body imap.Literal, opts []ExtensionOption) ([]ExtensionResult, error) // ListMailboxes returns information about mailboxes belonging to this // user. If subscribed is set to true, only returns subscribed mailboxes. - ListMailboxes(subscribed bool) ([]imap.MailboxInfo, error) + ListMailboxes(subscribed bool, opts []ExtensionOption) ([]imap.MailboxInfo, error) // GetMailbox returns a mailbox. If it doesn't exist, it returns // ErrNoSuchMailbox. // // Returned MailboxStatus should have Messages, Recent, Unseen, UidNext // and UidValidity populated. - GetMailbox(name string, readOnly bool, conn Conn) (*imap.MailboxStatus, Mailbox, error) + GetMailbox(name string, readOnly bool, conn Conn, opts []ExtensionOption) (*imap.MailboxStatus, Mailbox, error) // CreateMailbox creates a new mailbox. // @@ -70,7 +70,7 @@ type User interface { // deleted, its unique identifiers MUST be greater than any unique identifiers // used in the previous incarnation of the mailbox UNLESS the new incarnation // has a different unique identifier validity value. - CreateMailbox(name string) error + CreateMailbox(name string, opts []ExtensionOption) ([]ExtensionResult, error) // DeleteMailbox permanently remove the mailbox with the given name. It is an // error to // attempt to delete INBOX or a mailbox name that does not exist. @@ -83,7 +83,7 @@ type User interface { // be preserved so that a new mailbox created with the same name will not // reuse the identifiers of the former incarnation, UNLESS the new incarnation // has a different unique identifier validity value. - DeleteMailbox(name string) error + DeleteMailbox(name string, opts []ExtensionOption) ([]ExtensionResult, error) // RenameMailbox changes the name of a mailbox. It is an error to attempt to // rename from a mailbox name that does not exist or to a mailbox name that @@ -110,7 +110,7 @@ type User interface { // messages in INBOX to a new mailbox with the given name, leaving INBOX // empty. If the server implementation supports inferior hierarchical names // of INBOX, these are unaffected by a rename of INBOX. - RenameMailbox(existingName, newName string) error + RenameMailbox(existingName, newName string, opts []ExtensionOption) ([]ExtensionResult, error) // Logout is called when this User will no longer be used, likely because the // client closed the connection. diff --git a/server/cmd_auth.go b/server/cmd_auth.go index c89a9fa5..1f0525c9 100644 --- a/server/cmd_auth.go +++ b/server/cmd_auth.go @@ -38,7 +38,7 @@ func (cmd *Select) Handle(conn Conn) error { return ErrNotAuthenticated } - status, mbox, err := ctx.User.GetMailbox(cmd.Mailbox, cmd.ReadOnly, conn) + status, mbox, err := ctx.User.GetMailbox(cmd.Mailbox, cmd.ReadOnly, conn, nil) if err != nil { return err } @@ -71,7 +71,8 @@ func (cmd *Create) Handle(conn Conn) error { return ErrNotAuthenticated } - return ctx.User.CreateMailbox(cmd.Mailbox) + _, err := ctx.User.CreateMailbox(cmd.Mailbox, nil) + return err } type Delete struct { @@ -84,7 +85,8 @@ func (cmd *Delete) Handle(conn Conn) error { return ErrNotAuthenticated } - return ctx.User.DeleteMailbox(cmd.Mailbox) + _, err := ctx.User.DeleteMailbox(cmd.Mailbox, nil) + return err } type Rename struct { @@ -97,7 +99,8 @@ func (cmd *Rename) Handle(conn Conn) error { return ErrNotAuthenticated } - return ctx.User.RenameMailbox(cmd.Existing, cmd.New) + _, err := ctx.User.RenameMailbox(cmd.Existing, cmd.New, nil) + return err } type Subscribe struct { @@ -147,7 +150,7 @@ func (cmd *List) Handle(conn Conn) error { } })() - mboxInfo, err := ctx.User.ListMailboxes(cmd.Subscribed) + mboxInfo, err := ctx.User.ListMailboxes(cmd.Subscribed, nil) if err != nil { // Close channel to signal end of results close(ch) @@ -216,7 +219,7 @@ func (cmd *Append) Handle(conn Conn) error { return ErrNotAuthenticated } - if err := ctx.User.CreateMessage(cmd.Mailbox, cmd.Flags, cmd.Date, cmd.Message); err != nil { + if _, err := ctx.User.CreateMessage(cmd.Mailbox, cmd.Flags, cmd.Date, cmd.Message, nil); err != nil { if err == backend.ErrNoSuchMailbox { return ErrStatusResp(&imap.StatusResp{ Type: imap.StatusRespNo, diff --git a/server/cmd_selected.go b/server/cmd_selected.go index 683748f0..90082130 100644 --- a/server/cmd_selected.go +++ b/server/cmd_selected.go @@ -54,7 +54,8 @@ func (cmd *Close) Handle(conn Conn) error { ctx.MailboxReadOnly = false // No need to send expunge updates here, since the mailbox is already unselected - return mailbox.Expunge() + _, err := mailbox.Expunge(nil) + return err } type Expunge struct { @@ -70,7 +71,8 @@ func (cmd *Expunge) Handle(conn Conn) error { return ErrMailboxReadOnly } - return ctx.Mailbox.Expunge() + _, err := ctx.Mailbox.Expunge(nil) + return err } type Search struct { @@ -83,7 +85,7 @@ func (cmd *Search) handle(uid bool, conn Conn) error { return ErrNoMailboxSelected } - ids, err := ctx.Mailbox.SearchMessages(uid, cmd.Criteria) + _, ids, err := ctx.Mailbox.SearchMessages(uid, cmd.Criteria, nil) if err != nil { return err } @@ -121,7 +123,7 @@ func (cmd *Fetch) handle(uid bool, conn Conn) error { } })() - err := ctx.Mailbox.ListMessages(uid, cmd.SeqSet, cmd.Items, ch) + _, err := ctx.Mailbox.ListMessages(uid, cmd.SeqSet, cmd.Items, ch, nil) if err != nil { return err } @@ -189,7 +191,7 @@ func (cmd *Store) handle(uid bool, conn Conn) error { flags[i] = imap.CanonicalFlag(flag) } - err = ctx.Mailbox.UpdateMessagesFlags(uid, cmd.SeqSet, op, silent, flags) + _, err = ctx.Mailbox.UpdateMessagesFlags(uid, cmd.SeqSet, op, silent, flags, nil) if err != nil { return err } @@ -215,7 +217,7 @@ func (cmd *Copy) handle(uid bool, conn Conn) error { return ErrNoMailboxSelected } - err := ctx.Mailbox.CopyMessages(uid, cmd.SeqSet, cmd.Mailbox) + _, err := ctx.Mailbox.CopyMessages(uid, cmd.SeqSet, cmd.Mailbox, nil) if err != nil { if err == backend.ErrNoSuchMailbox { return ErrStatusResp(&imap.StatusResp{ From 3828788623463df91aaa5dd24f46109fe2f6c9a2 Mon Sep 17 00:00:00 2001 From: "fox.cpp" Date: Tue, 4 Aug 2020 01:48:15 +0300 Subject: [PATCH 11/11] server: Implement UIDPLUS extension Written as a PoC for extendable backend interface. --- backend/backend.go | 4 +++ backend/extensions.go | 38 ++++++++++++++++++++++++++++ backend/memory/backend.go | 4 ++- backend/memory/mailbox.go | 27 ++++++++++++++++++-- backend/memory/user.go | 11 +++++++-- commands/expunge.go | 18 ++++++++++++-- server/cmd_auth.go | 27 +++++++++++++++++++- server/cmd_selected.go | 52 ++++++++++++++++++++++++++++++++++++++- server/conn.go | 7 ++++++ server/server.go | 20 +++++++++------ 10 files changed, 192 insertions(+), 16 deletions(-) create mode 100644 backend/extensions.go diff --git a/backend/backend.go b/backend/backend.go index 4f8f91b0..eafa39b8 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -13,6 +13,10 @@ var ErrInvalidCredentials = errors.New("Invalid credentials") type Extension string +const ( + ExtUIDPLUS Extension = "UIDPLUS" +) + // Backend is an IMAP server backend. A backend operation always deals with // users. type Backend interface { diff --git a/backend/extensions.go b/backend/extensions.go new file mode 100644 index 00000000..a538417b --- /dev/null +++ b/backend/extensions.go @@ -0,0 +1,38 @@ +package backend + +import ( + "github.com/emersion/go-imap" +) + +// ExpungeSeqSet may be passed to Mailbox.Expunge() to restrict message +// deletion to the specified UIDs sequence set. +// +// See RFC 2359 for details. +type ExpungeSeqSet struct { + *imap.SeqSet +} + +func (ExpungeSeqSet) ExtOption() {} + +// CopyUIDs must be returned as a result for CopyMessages by backends that +// implement UIDPLUS extension. +// +// See RFC 2359 for value details. +type CopyUIDs struct { + Source *imap.SeqSet + UIDValidity uint32 + Dest *imap.SeqSet +} + +func (CopyUIDs) ExtResult() {} + +// AppendUID must be returned as a result for CreateMessage by backend that +// implement UIDPLUS extension. +// +// See RFC 2359 for value details. +type AppendUID struct { + UIDValidity uint32 + UID uint32 +} + +func (AppendUID) ExtResult() {} diff --git a/backend/memory/backend.go b/backend/memory/backend.go index 3811ec81..02267b66 100644 --- a/backend/memory/backend.go +++ b/backend/memory/backend.go @@ -23,7 +23,9 @@ func (be *Backend) Login(_ *imap.ConnInfo, username, password string) (backend.U } func (be *Backend) SupportedExtensions() []backend.Extension { - return nil + return []backend.Extension{ + backend.ExtUIDPLUS, + } } func New() *Backend { diff --git a/backend/memory/mailbox.go b/backend/memory/mailbox.go index 7275a227..79b1fddc 100644 --- a/backend/memory/mailbox.go +++ b/backend/memory/mailbox.go @@ -168,6 +168,8 @@ func (mbox *Mailbox) CopyMessages(uid bool, seqset *imap.SeqSet, destName string return nil, backend.ErrNoSuchMailbox } + var srcSet, destSet imap.SeqSet + for i, msg := range mbox.Messages { var id uint32 if uid { @@ -179,15 +181,32 @@ func (mbox *Mailbox) CopyMessages(uid bool, seqset *imap.SeqSet, destName string continue } + srcSet.AddNum(msg.Uid) + msgCopy := *msg msgCopy.Uid = dest.uidNext() dest.Messages = append(dest.Messages, &msgCopy) + destSet.AddNum(msgCopy.Uid) } - return nil, nil + return []backend.ExtensionResult{ + backend.CopyUIDs{ + Source: &srcSet, + UIDValidity: 1, + Dest: &destSet, + }, + }, nil } -func (mbox *SelectedMailbox) Expunge(_ []backend.ExtensionOption) ([]backend.ExtensionResult, error) { +func (mbox *SelectedMailbox) Expunge(opts []backend.ExtensionOption) ([]backend.ExtensionResult, error) { + var allowedUIDs *imap.SeqSet + for _, opt := range opts { + switch opt := opt.(type) { + case backend.ExpungeSeqSet: + allowedUIDs = opt.SeqSet + } + } + for i := len(mbox.Messages) - 1; i >= 0; i-- { msg := mbox.Messages[i] @@ -199,6 +218,10 @@ func (mbox *SelectedMailbox) Expunge(_ []backend.ExtensionOption) ([]backend.Ext } } + if allowedUIDs != nil && !allowedUIDs.Contains(msg.Uid) { + continue + } + if deleted { mbox.Messages = append(mbox.Messages[:i], mbox.Messages[i+1:]...) diff --git a/backend/memory/user.go b/backend/memory/user.go index ddcaa915..4ff9bcfa 100644 --- a/backend/memory/user.go +++ b/backend/memory/user.go @@ -109,14 +109,21 @@ func (u *User) CreateMessage(mboxName string, flags []string, date time.Time, bo return nil, err } + uid := mbox.uidNext() mbox.Messages = append(mbox.Messages, &Message{ - Uid: mbox.uidNext(), + Uid: uid, Date: date, Size: uint32(len(b)), Flags: flags, Body: b, }) - return nil, nil + + return []backend.ExtensionResult{ + backend.AppendUID{ + UIDValidity: 1, + UID: uid, + }, + }, nil } func (u *User) CreateMailbox(name string, _ []backend.ExtensionOption) ([]backend.ExtensionResult, error) { diff --git a/commands/expunge.go b/commands/expunge.go index af550a4d..e34288c2 100644 --- a/commands/expunge.go +++ b/commands/expunge.go @@ -1,16 +1,30 @@ package commands import ( + "errors" + "github.com/emersion/go-imap" ) // Expunge is an EXPUNGE command, as defined in RFC 3501 section 6.4.3. -type Expunge struct{} +type Expunge struct { + // UID seqset specified for UID EXPUNGE (UIDPLUS extension). + SeqSet *imap.SeqSet +} func (cmd *Expunge) Command() *imap.Command { return &imap.Command{Name: "EXPUNGE"} } func (cmd *Expunge) Parse(fields []interface{}) error { - return nil + if len(fields) == 0 { + return nil + } + seqSet, ok := fields[0].(string) + if !ok { + return errors.New("String required as an argument") + } + var err error + cmd.SeqSet, err = imap.ParseSeqSet(seqSet) + return err } diff --git a/server/cmd_auth.go b/server/cmd_auth.go index 1f0525c9..599284ae 100644 --- a/server/cmd_auth.go +++ b/server/cmd_auth.go @@ -219,7 +219,8 @@ func (cmd *Append) Handle(conn Conn) error { return ErrNotAuthenticated } - if _, err := ctx.User.CreateMessage(cmd.Mailbox, cmd.Flags, cmd.Date, cmd.Message, nil); err != nil { + res, err := ctx.User.CreateMessage(cmd.Mailbox, cmd.Flags, cmd.Date, cmd.Message, nil) + if err != nil { if err == backend.ErrNoSuchMailbox { return ErrStatusResp(&imap.StatusResp{ Type: imap.StatusRespNo, @@ -237,5 +238,29 @@ func (cmd *Append) Handle(conn Conn) error { if ctx.Mailbox != nil && ctx.Mailbox.Name() == cmd.Mailbox { return ctx.Mailbox.Poll(true) } + + var customResp *imap.StatusResp + for _, value := range res { + switch value := value.(type) { + case backend.AppendUID: + customResp = &imap.StatusResp{ + Tag: "", + Type: imap.StatusRespOk, + Code: "APPENDUID", + Arguments: []interface{}{ + value.UIDValidity, + value.UID, + }, + Info: "APPEND completed", + } + default: + conn.Server().ErrorLog.Printf("ExtensionResult of unknown type returned by backend: %T", value) + // Returning an error here would make it look like the command failed. + } + } + if customResp != nil { + return &imap.ErrStatusResp{Resp: customResp} + } + return nil } diff --git a/server/cmd_selected.go b/server/cmd_selected.go index 90082130..c6408d13 100644 --- a/server/cmd_selected.go +++ b/server/cmd_selected.go @@ -63,6 +63,10 @@ type Expunge struct { } func (cmd *Expunge) Handle(conn Conn) error { + if cmd.SeqSet != nil { + return errors.New("Unexpected argment") + } + ctx := conn.Context() if ctx.Mailbox == nil { return ErrNoMailboxSelected @@ -75,6 +79,28 @@ func (cmd *Expunge) Handle(conn Conn) error { return err } +func (cmd *Expunge) UidHandle(conn Conn) error { + if _, ok := conn.Server().backendExts[backend.ExtUIDPLUS]; !ok { + return errors.New("Unknown command") + } + if cmd.SeqSet == nil { + return errors.New("Missing set argment") + } + + ctx := conn.Context() + if ctx.Mailbox == nil { + return ErrNoMailboxSelected + } + if ctx.MailboxReadOnly { + return ErrMailboxReadOnly + } + + _, err := ctx.Mailbox.Expunge([]backend.ExtensionOption{ + backend.ExpungeSeqSet{SeqSet: cmd.SeqSet}, + }) + return err +} + type Search struct { commands.Search } @@ -217,7 +243,7 @@ func (cmd *Copy) handle(uid bool, conn Conn) error { return ErrNoMailboxSelected } - _, err := ctx.Mailbox.CopyMessages(uid, cmd.SeqSet, cmd.Mailbox, nil) + resp, err := ctx.Mailbox.CopyMessages(uid, cmd.SeqSet, cmd.Mailbox, nil) if err != nil { if err == backend.ErrNoSuchMailbox { return ErrStatusResp(&imap.StatusResp{ @@ -228,6 +254,30 @@ func (cmd *Copy) handle(uid bool, conn Conn) error { } return err } + + var customResp *imap.StatusResp + for _, value := range resp { + switch value := value.(type) { + case backend.CopyUIDs: + customResp = &imap.StatusResp{ + Type: imap.StatusRespOk, + Code: "COPYUID", + Arguments: []interface{}{ + value.UIDValidity, + value.Source, + value.Dest, + }, + Info: "COPY completed", + } + default: + conn.Server().ErrorLog.Printf("ExtensionResult of unknown type returned by backend: %T", value) + // Returning an error here would make it look like the command failed. + } + } + if customResp != nil { + return &imap.ErrStatusResp{Resp: customResp} + } + return nil } diff --git a/server/conn.go b/server/conn.go index 5480706f..62a4afa6 100644 --- a/server/conn.go +++ b/server/conn.go @@ -184,6 +184,13 @@ func (c *conn) Capabilities() []string { } } + for _, ext := range c.Server().Backend.SupportedExtensions() { + switch ext { + case backend.ExtUIDPLUS: + caps = append(caps, "UIDPLUS") + } + } + for _, ext := range c.s.extensions { caps = append(caps, ext.Capabilities(c)...) } diff --git a/server/server.go b/server/server.go index 66d38471..78723b7f 100644 --- a/server/server.go +++ b/server/server.go @@ -88,9 +88,10 @@ type Server struct { listeners map[net.Listener]struct{} conns map[Conn]struct{} - commands map[string]HandlerFactory - auths map[string]SASLServerFactory - extensions []Extension + commands map[string]HandlerFactory + auths map[string]SASLServerFactory + extensions []Extension + backendExts map[backend.Extension]struct{} // TCP address to listen on. Addr string @@ -119,10 +120,15 @@ type Server struct { // Create a new IMAP server from an existing listener. func New(bkd backend.Backend) *Server { s := &Server{ - listeners: make(map[net.Listener]struct{}), - conns: make(map[Conn]struct{}), - Backend: bkd, - ErrorLog: log.New(os.Stderr, "imap/server: ", log.LstdFlags), + listeners: make(map[net.Listener]struct{}), + conns: make(map[Conn]struct{}), + backendExts: map[backend.Extension]struct{}{}, + Backend: bkd, + ErrorLog: log.New(os.Stderr, "imap/server: ", log.LstdFlags), + } + + for _, ext := range bkd.SupportedExtensions() { + s.backendExts[ext] = struct{}{} } s.auths = map[string]SASLServerFactory{