diff --git a/internal/state/snapshot_messages.go b/internal/state/snapshot_messages.go index 47944895..6a8f10bc 100644 --- a/internal/state/snapshot_messages.go +++ b/internal/state/snapshot_messages.go @@ -322,6 +322,14 @@ func (list *snapMsgList) resolveSeqInterval(seqSet []command.SeqRange) ([]SeqInt } func (list *snapMsgList) resolveUIDInterval(seqSet []command.SeqRange) ([]UIDInterval, error) { + // An empty mailbox contains no UIDs, so any UID set matches nothing. + // Resolving against the empty snapshot would report ErrNoSuchMessage + // instead, turning e.g. a UID SEARCH on an empty mailbox into a NO reply; + // RFC 3501 requires non-existent UIDs to be ignored without error. + if list.len() == 0 { + return nil, nil + } + res := make([]UIDInterval, 0, len(seqSet)) for _, uidRange := range seqSet { diff --git a/tests/search_test.go b/tests/search_test.go index 344cb597..d505da15 100644 --- a/tests/search_test.go +++ b/tests/search_test.go @@ -564,6 +564,28 @@ func TestSearchUID(t *testing.T) { }) } +func TestSearchUIDEmptyMailbox(t *testing.T) { + runOneToOneTestWithAuth(t, defaultServerOptions(t), func(c *testConnection, s *testSession) { + c.C(`A001 select inbox`).OK("A001") + + // RFC 3501 section 6.4.8: "A non-existent unique identifier is + // ignored without any error message generated." In an empty mailbox + // every UID is non-existent, so a UID key matches nothing; it must + // not turn the SEARCH into a NO reply. + c.C(`A002 uid search uid 1`) + c.S("* SEARCH") + c.OK("A002") + + c.C(`A003 search uid 1,3:5`) + c.S("* SEARCH") + c.OK("A003") + + c.C(`A004 uid search uid 1:*`) + c.S("* SEARCH") + c.OK("A004") + }) +} + func TestSearchUIDAfterDelete(t *testing.T) { runOneToOneTestWithData(t, defaultServerOptions(t), func(c *testConnection, s *testSession, mbox string, mboxID imap.MailboxID) { // Remove some messages to ensure sequence doesn't match uid