From 1909ceed677c5353490e4ccdb139a85ada8bafb6 Mon Sep 17 00:00:00 2001 From: Michal Horejsek Date: Wed, 20 Jan 2021 13:16:27 +0100 Subject: [PATCH] Support of UID EXPUNGE --- internal/imap/mailbox.go | 15 +++- internal/imap/store.go | 2 +- internal/imap/uidplus/extension.go | 68 +++++++++++++------ internal/store/mailbox_message.go | 25 ++++++- .../bridge/imap/message/delete.feature | 13 ++++ unreleased.md | 1 + 6 files changed, 101 insertions(+), 23 deletions(-) diff --git a/internal/imap/mailbox.go b/internal/imap/mailbox.go index a64f5c66..cfb8d9b5 100644 --- a/internal/imap/mailbox.go +++ b/internal/imap/mailbox.go @@ -191,7 +191,20 @@ func (im *imapMailbox) Expunge() error { im.user.backend.setUpdatesBeBlocking(im.user.currentAddressLowercase, im.name, operationDeleteMessage) defer im.user.backend.unsetUpdatesBeBlocking(im.user.currentAddressLowercase, im.name, operationDeleteMessage) - return im.storeMailbox.RemoveDeleted() + return im.storeMailbox.RemoveDeleted(nil) +} + +// UIDExpunge permanently removes messages that have the \Deleted flag set +// and UID passed from SeqSet from the currently selected mailbox. +func (im *imapMailbox) UIDExpunge(seqSet *imap.SeqSet) error { + im.user.backend.setUpdatesBeBlocking(im.user.currentAddressLowercase, im.name, operationDeleteMessage) + defer im.user.backend.unsetUpdatesBeBlocking(im.user.currentAddressLowercase, im.name, operationDeleteMessage) + + messageIDs, err := im.apiIDsFromSeqSet(true, seqSet) + if err != nil || len(messageIDs) == 0 { + return err + } + return im.storeMailbox.RemoveDeleted(messageIDs) } func (im *imapMailbox) ListQuotas() ([]string, error) { diff --git a/internal/imap/store.go b/internal/imap/store.go index 6273d105..fec7cae9 100644 --- a/internal/imap/store.go +++ b/internal/imap/store.go @@ -89,7 +89,7 @@ type storeMailboxProvider interface { MarkMessagesDeleted(apiID []string) error MarkMessagesUndeleted(apiID []string) error ImportMessage(msg *pmapi.Message, body []byte, labelIDs []string) error - RemoveDeleted() error + RemoveDeleted(apiIDs []string) error } type storeMessageProvider interface { diff --git a/internal/imap/uidplus/extension.go b/internal/imap/uidplus/extension.go index e3f574c2..f3d1df39 100644 --- a/internal/imap/uidplus/extension.go +++ b/internal/imap/uidplus/extension.go @@ -116,40 +116,70 @@ func (os *OrderedSeq) String() string { // UIDExpunge implements server.Handler but Bridge is not supporting // UID EXPUNGE with specific UIDs. + +type UIDExpungeMailbox interface { + Expunge() error + UIDExpunge(*imap.SeqSet) error +} + type UIDExpunge struct { - expunge *server.Expunge + SeqSet *imap.SeqSet } func newUIDExpunge() *UIDExpunge { - return &UIDExpunge{expunge: &server.Expunge{}} + return &UIDExpunge{} } func (e *UIDExpunge) Parse(fields []interface{}) error { - if len(fields) < 1 { - return e.expunge.Parse(fields) + if len(fields) == 0 { + return nil // It could be regular EXPUNGE without arguments. + } + if len(fields) > 1 { + return errors.New("too many arguments") } - // RFC4315#section-2.1 - // The UID EXPUNGE command permanently removes all messages that both - // have the \Deleted flag set and have a UID that is included in the - // specified sequence set from the currently selected mailbox. If a - // message either does not have the \Deleted flag set or has a UID - // that is not included in the specified sequence set, it is not - // affected. - // - // Current implementation supports only deletion of all messages - // marked as deleted. It will probably need mailbox interface change: - // ExpungeUIDs(seqSet). Not sure how to combine with original - // e.expunge.Handle(). - return errors.New("UID EXPUNGE with UIDs is not supported") + seqset, ok := fields[0].(string) + if !ok { + return errors.New("sequence set must be an atom") + } + var err error + e.SeqSet, err = imap.ParseSeqSet(seqset) + return err } func (e *UIDExpunge) Handle(conn server.Conn) error { - return e.expunge.Handle(conn) + mailbox, err := e.getMailbox(conn) + if err != nil { + return err + } + return mailbox.Expunge() } func (e *UIDExpunge) UidHandle(conn server.Conn) error { //nolint[golint] - return e.expunge.Handle(conn) + if e.SeqSet == nil { + return errors.New("missing sequence set") + } + mailbox, err := e.getMailbox(conn) + if err != nil { + return err + } + return mailbox.UIDExpunge(e.SeqSet) +} + +func (e *UIDExpunge) getMailbox(conn server.Conn) (UIDExpungeMailbox, error) { + ctx := conn.Context() + if ctx.Mailbox == nil { + return nil, server.ErrNoMailboxSelected + } + if ctx.MailboxReadOnly { + return nil, server.ErrMailboxReadOnly + } + + mailbox, ok := ctx.Mailbox.(UIDExpungeMailbox) + if !ok { + return nil, errors.New("UID EXPUNGE is not implemented") + } + return mailbox, nil } type extension struct{} diff --git a/internal/store/mailbox_message.go b/internal/store/mailbox_message.go index e0443387..3913b740 100644 --- a/internal/store/mailbox_message.go +++ b/internal/store/mailbox_message.go @@ -208,14 +208,35 @@ func (storeMailbox *Mailbox) MarkMessagesUndeleted(apiIDs []string) error { // If the mailbox is All Mail or All Sent, it does nothing. // If the mailbox is Trash or Spam and message is not in any other mailbox, messages is deleted. // In all other cases the message is only removed from the mailbox. -func (storeMailbox *Mailbox) RemoveDeleted() error { +// If nil is passed, all messages with \Deleted flag are removed. +// In other cases only messages with \Deleted flag and included in the passed list. +func (storeMailbox *Mailbox) RemoveDeleted(apiIDs []string) error { storeMailbox.log.Trace("Deleting messages") - apiIDs, err := storeMailbox.GetDeletedAPIIDs() + deletedAPIIDs, err := storeMailbox.GetDeletedAPIIDs() if err != nil { return err } + if apiIDs == nil { + apiIDs = deletedAPIIDs + } else { + filteredAPIIDs := []string{} + for _, apiID := range apiIDs { + found := false + for _, deletedAPIID := range deletedAPIIDs { + if apiID == deletedAPIID { + found = true + break + } + } + if found { + filteredAPIIDs = append(filteredAPIIDs, apiID) + } + } + apiIDs = filteredAPIIDs + } + if len(apiIDs) == 0 { storeMailbox.log.Debug("List to expunge is empty") return nil diff --git a/test/features/bridge/imap/message/delete.feature b/test/features/bridge/imap/message/delete.feature index 3c0e204c..efea2ca9 100644 --- a/test/features/bridge/imap/message/delete.feature +++ b/test/features/bridge/imap/message/delete.feature @@ -99,3 +99,16 @@ Feature: IMAP remove messages from mailbox And there is IMAP client selected in "All Mail" When IMAP client marks message seq "1" as deleted Then IMAP response is "IMAP error: NO operation not allowed for 'All Mail' folder" + + Scenario: Expunge specific message only + Given there are 5 messages in mailbox "INBOX" for "user" + And there is IMAP client logged in as "user" + And there is IMAP client selected in "INBOX" + When IMAP client marks message seq "1" as deleted + Then IMAP response is "OK" + When IMAP client marks message seq "2" as deleted + Then IMAP response is "OK" + When IMAP client sends command "UID EXPUNGE 1" + Then IMAP response is "OK" + And mailbox "INBOX" for "user" has 4 messages + And message "2" in "INBOX" for "user" is marked as deleted diff --git a/unreleased.md b/unreleased.md index 1d8fe0b1..532ea07c 100644 --- a/unreleased.md +++ b/unreleased.md @@ -13,6 +13,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/) * GODT-804 Added GUI notification on silent update installed (promt to restart). * GODT-275 Added option to disable autoupdates in settings (default autoupdate is enabled). * GODT-874 Added manual triggers to Updater module. +* GODT-851 Added support of UID EXPUNGE. ### Changed * GODT-97 Don't log errors caused by SELECT "".