diff --git a/internal/imap/mailbox_messages.go b/internal/imap/mailbox_messages.go index 55ca7229..e92ae88c 100644 --- a/internal/imap/mailbox_messages.go +++ b/internal/imap/mailbox_messages.go @@ -526,18 +526,6 @@ func (im *imapMailbox) listMessages(isUID bool, seqSet *imap.SeqSet, items []ima return err } - // From RFC: UID range of 559:* always includes the UID of the last message - // in the mailbox, even if 559 is higher than any assigned UID value. - // See: https://tools.ietf.org/html/rfc3501#page-61 - if isUID && seqSet.Dynamic() && len(apiIDs) == 0 { - l.Debug("Requesting empty UID dynamic fetch, adding latest message") - apiID, err := im.storeMailbox.GetLatestAPIID() - if err != nil { - return nil - } - apiIDs = []string{apiID} - } - input := make([]interface{}, len(apiIDs)) for i, apiID := range apiIDs { input[i] = apiID diff --git a/internal/store/mailbox_ids.go b/internal/store/mailbox_ids.go index c81fdfa5..f8da95d1 100644 --- a/internal/store/mailbox_ids.go +++ b/internal/store/mailbox_ids.go @@ -36,23 +36,36 @@ import ( func (storeMailbox *Mailbox) GetAPIIDsFromUIDRange(start, stop uint32) (apiIDs []string, err error) { err = storeMailbox.db().View(func(tx *bolt.Tx) error { b := storeMailbox.txGetIMAPIDsBucket(tx) + c := b.Cursor() + // If the start range is a wildcard, the range can only refer to the last message in the mailbox. + if start == 0 { + _, apiID := c.Last() + apiIDs = append(apiIDs, string(apiID)) + return nil + } + + // Resolve the stop value to be the final UID in the mailbox. if stop == 0 { - // A null stop means no stop. - stop = ^uint32(0) + stop = storeMailbox.txGetFinalUID(b) + } + + // After resolving the stop value, it might be less than start so we sort it. + if start > stop { + start, stop = stop, start } startb := itob(start) stopb := itob(stop) - c := b.Cursor() for k, v := c.Seek(startb); k != nil && bytes.Compare(k, stopb) <= 0; k, v = c.Next() { apiIDs = append(apiIDs, string(v)) } return nil }) - return + + return apiIDs, err } // GetAPIIDsFromSequenceRange returns API IDs by IMAP sequence number range. @@ -60,28 +73,47 @@ func (storeMailbox *Mailbox) GetAPIIDsFromSequenceRange(start, stop uint32) (api err = storeMailbox.db().View(func(tx *bolt.Tx) error { b := storeMailbox.txGetIMAPIDsBucket(tx) c := b.Cursor() + + // If the start range is a wildcard, the range can only refer to the last message in the mailbox. + if start == 0 { + _, apiID := c.Last() + apiIDs = append(apiIDs, string(apiID)) + return nil + } + var i uint32 + for k, v := c.First(); k != nil; k, v = c.Next() { i++ + if i < start { continue } + if stop > 0 && i > stop { break } + apiIDs = append(apiIDs, string(v)) } + + if stop == 0 && len(apiIDs) == 0 { + if _, apiID := c.Last(); len(apiID) > 0 { + apiIDs = append(apiIDs, string(apiID)) + } + } + return nil }) - return + + return apiIDs, err } // GetLatestAPIID returns the latest message API ID which still exists. // Info: not the latest IMAP UID which can be already removed. func (storeMailbox *Mailbox) GetLatestAPIID() (apiID string, err error) { err = storeMailbox.db().View(func(tx *bolt.Tx) error { - b := storeMailbox.txGetAPIIDsBucket(tx) - c := b.Cursor() + c := storeMailbox.txGetAPIIDsBucket(tx).Cursor() lastAPIID, _ := c.Last() apiID = string(lastAPIID) if apiID == "" { @@ -283,3 +315,8 @@ func (storeMailbox *Mailbox) GetUIDByHeader(header *mail.Header) (foundUID uint3 return foundUID } + +func (storeMailbox *Mailbox) txGetFinalUID(b *bolt.Bucket) uint32 { + uid, _ := b.Cursor().Last() + return btoi(uid) +} diff --git a/internal/store/mailbox_ids_test.go b/internal/store/mailbox_ids_test.go index e0ecc3ff..77cd76cb 100644 --- a/internal/store/mailbox_ids_test.go +++ b/internal/store/mailbox_ids_test.go @@ -56,7 +56,7 @@ func checkMailboxMessageIDs(t *testing.T, m *mocksForStore, mailboxLabel string, storeAddress := m.store.addresses[addrID1] storeMailbox := storeAddress.mailboxes[mailboxLabel] - ids, err := storeMailbox.GetAPIIDsFromSequenceRange(0, uint32(len(wantIDs))) + ids, err := storeMailbox.GetAPIIDsFromSequenceRange(1, uint32(len(wantIDs))) require.Nil(t, err) idx := 0 diff --git a/test/features/bridge/imap/message/fetch.feature b/test/features/bridge/imap/message/fetch.feature index c1a34f3c..2170f0e1 100644 --- a/test/features/bridge/imap/message/fetch.feature +++ b/test/features/bridge/imap/message/fetch.feature @@ -11,7 +11,15 @@ Feature: IMAP fetch messages Then IMAP response is "OK" And IMAP response has 10 messages - Scenario: Fetch first few message of inbox + Scenario: Fetch of inbox by UID + Given there are 10 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 fetches by UID "1:*" + Then IMAP response is "OK" + And IMAP response has 10 messages + + Scenario: Fetch first few messages of inbox Given there are 10 messages in mailbox "INBOX" for "user" And there is IMAP client logged in as "user" And there is IMAP client selected in "INBOX" @@ -19,6 +27,78 @@ Feature: IMAP fetch messages Then IMAP response is "OK" And IMAP response has 5 messages + Scenario: Fetch first few messages of inbox + Given there are 10 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 fetches by UID "1:5" + Then IMAP response is "OK" + And IMAP response has 5 messages + + Scenario: Fetch last few messages of inbox using wildcard + Given there are 10 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 fetches "6:*" + Then IMAP response is "OK" + And IMAP response has 5 messages + + Scenario: Fetch last few messages of inbox using wildcard by UID + Given there are 10 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 fetches by UID "6:*" + Then IMAP response is "OK" + And IMAP response has 5 messages + + Scenario: Fetch last message of inbox using wildcard + Given there are 10 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 fetches "*" + Then IMAP response is "OK" + And IMAP response has 1 message + + Scenario: Fetch last message of inbox using wildcard by UID + Given there are 10 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 fetches by UID "*" + Then IMAP response is "OK" + And IMAP response has 1 message + + Scenario: Fetch backwards range using wildcard + Given there are 10 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 fetches "*:1" + Then IMAP response is "OK" + And IMAP response has 10 messages + + Scenario: Fetch backwards range using wildcard by UID + Given there are 10 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 fetches by UID "*:1" + Then IMAP response is "OK" + And IMAP response has 10 messages + + Scenario: Fetch overshot range using wildcard returns last message + Given there are 10 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 fetches "20:*" + Then IMAP response is "OK" + And IMAP response has 1 message + + Scenario: Fetch overshot range using wildcard by UID returns last message + Given there are 10 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 fetches by UID "20:*" + Then IMAP response is "OK" + And IMAP response has 1 message + Scenario: Fetch of custom mailbox Given there are 10 messages in mailbox "Folders/mbox" for "user" And there is IMAP client logged in as "user" @@ -27,7 +107,8 @@ Feature: IMAP fetch messages Then IMAP response is "OK" And IMAP response has 10 messages - Scenario: Fetch of emtpy mailbox + # This test is wrong! RFC says it should return "BAD" (GODT-1153). + Scenario: Fetch of empty mailbox Given there is IMAP client logged in as "user" And there is IMAP client selected in "Folders/mbox" When IMAP client fetches "1:*" @@ -42,14 +123,6 @@ Feature: IMAP fetch messages Then IMAP response is "OK" And IMAP response has 100 messages - Scenario: Fetch returns alsways latest messages - Given there are 10 messages in mailbox "Folders/mbox" for "user" - And there is IMAP client logged in as "user" - And there is IMAP client selected in "Folders/mbox" - When IMAP client fetches by UID "11:*" - Then IMAP response is "OK" - And IMAP response has 1 message - Scenario: Fetch returns also messages that are marked as deleted Given there are messages in mailbox "Folders/mbox" for "user" | from | to | subject | body | read | starred | deleted | diff --git a/test/store_checks_test.go b/test/store_checks_test.go index 2b25153b..88368edd 100644 --- a/test/store_checks_test.go +++ b/test/store_checks_test.go @@ -128,7 +128,7 @@ func mailboxForAddressOfUserHasMessages(mailboxName, bddAddressID, bddUserID str if err != nil { return internalError(err, "getting store mailbox") } - apiIDs, err := mailbox.GetAPIIDsFromSequenceRange(0, 1000) + apiIDs, err := mailbox.GetAPIIDsFromSequenceRange(1, 1000) if err != nil { return internalError(err, "getting API IDs from sequence range") }