From 313e803fdd4f4e5800a37bf3e14a911c41ee488b Mon Sep 17 00:00:00 2001 From: Michal Horejsek Date: Wed, 22 Apr 2020 14:14:24 +0200 Subject: [PATCH] Implement new SearchCriteria from latest go-imap --- go.sum | 1 + internal/imap/mailbox_messages.go | 196 ++++++++++------------ test/features/imap/message/search.feature | 77 +++++++-- test/store_setup_test.go | 5 + 4 files changed, 156 insertions(+), 123 deletions(-) diff --git a/go.sum b/go.sum index 7cc0bae..99cd1e7 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,7 @@ github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a h1:bM github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a/go.mod h1:ikgISoP7pRAolqsVP64yMteJa2FIpS6ju88eBT6K1yQ= github.com/emersion/go-imap-idle v0.0.0-20190519112320-2704abd7050e h1:L7bswVJZcf2YHofgom49oFRwVqmBj/qZqDy9/SJpZMY= github.com/emersion/go-imap-idle v0.0.0-20190519112320-2704abd7050e/go.mod h1:o14zPKCmEH5WC1vU5SdPoZGgNvQx7zzKSnxPQlobo78= +github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w= github.com/emersion/go-imap-quota v0.0.0-20171113212021-e883a2bc54d6 h1:CQ9z5Gk5HBUI8+kM5XNZXUoAv8RF1GnXmYrN4OkDvZU= github.com/emersion/go-imap-quota v0.0.0-20171113212021-e883a2bc54d6/go.mod h1:iApyhIQBiU4XFyr+3kdJyyGqle82TbQyuP2o+OZHrV0= github.com/emersion/go-imap-quota v0.0.0-20200423100218-dcfd1b7d2b41 h1:z5lDGnSURauBEDdNLj3o0+HogVYKQCGeY3Anl/xyRfU= diff --git a/internal/imap/mailbox_messages.go b/internal/imap/mailbox_messages.go index 787645c..e832bf3 100644 --- a/internal/imap/mailbox_messages.go +++ b/internal/imap/mailbox_messages.go @@ -170,20 +170,15 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria) return nil, err } - var apiIDsFromUID []string if criteria.Uid != nil { - if apiIDs, err := im.apiIDsFromSeqSet(true, criteria.Uid); err == nil { - apiIDsFromUID = append(apiIDsFromUID, apiIDs...) + apiIDsByUID, err := im.apiIDsFromSeqSet(true, criteria.Uid) + if err != nil { + return nil, err } + apiIDs = arrayIntersection(apiIDs, apiIDsByUID) } - // Apply filters. for _, apiID := range apiIDs { - // Filter on UIDs. - if len(apiIDsFromUID) > 0 && !isStringInList(apiIDsFromUID, apiID) { - continue - } - // Get message. storeMessage, err := im.storeMailbox.GetMessage(apiID) if err != nil { @@ -192,79 +187,7 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria) } m := storeMessage.Message() - // Filter addresses. - /*if criteria.From != "" && !addressMatch([]*mail.Address{m.Sender}, criteria.From) { - continue - } - if criteria.To != "" && !addressMatch(m.ToList, criteria.To) { - continue - } - if criteria.Cc != "" && !addressMatch(m.CCList, criteria.Cc) { - continue - } - if criteria.Bcc != "" && !addressMatch(m.BCCList, criteria.Bcc) { - continue - }*/ - - // Filter strings. - /*if criteria.Subject != "" && !strings.Contains(strings.ToLower(m.Subject), strings.ToLower(criteria.Subject)) { - continue - } - if criteria.Keyword != "" && !hasKeyword(m, criteria.Keyword) { - continue - } - if criteria.Unkeyword != "" && hasKeyword(m, criteria.Unkeyword) { - continue - } - if criteria.Header[0] != "" { - h := message.GetHeader(m) - if val := h.Get(criteria.Header[0]); val == "" { - continue // Field is not in header. - } else if criteria.Header[1] != "" && !strings.Contains(strings.ToLower(val), strings.ToLower(criteria.Header[1])) { - continue // Field is in header, second criteria is non-zero and field value not matched (case insensitive). - } - } - - // Filter flags. - if criteria.Flagged && !isStringInList(m.LabelIDs, pmapi.StarredLabel) { - continue - } - if criteria.Unflagged && isStringInList(m.LabelIDs, pmapi.StarredLabel) { - continue - } - if criteria.Seen && m.Unread == 1 { - continue - } - if criteria.Unseen && m.Unread == 0 { - continue - } - if criteria.Deleted { - continue - } - // if criteria.Undeleted { // All messages matches this criteria } - if criteria.Draft && (m.Has(pmapi.FlagSent) || m.Has(pmapi.FlagReceived)) { - continue - } - if criteria.Undraft && !(m.Has(pmapi.FlagSent) || m.Has(pmapi.FlagReceived)) { - continue - } - if criteria.Answered && !(m.Has(pmapi.FlagReplied) || m.Has(pmapi.FlagRepliedAll)) { - continue - } - if criteria.Unanswered && (m.Has(pmapi.FlagReplied) || m.Has(pmapi.FlagRepliedAll)) { - continue - } - if criteria.Recent && m.Has(pmapi.FlagOpened) { // opened means not recent - continue - } - if criteria.Old && !m.Has(pmapi.FlagOpened) { - continue - } - if criteria.New && !(!m.Has(pmapi.FlagOpened) && m.Unread == 1) { - continue - }*/ - - // Filter internal date. + // Filter by time. if !criteria.Before.IsZero() { if truncated := criteria.Before.Truncate(24 * time.Hour); m.Time > truncated.Unix() { continue @@ -275,15 +198,8 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria) continue } } - /*if !criteria.On.IsZero() { - truncated := criteria.On.Truncate(24 * time.Hour) - if m.Time < truncated.Unix() || m.Time > truncated.Add(24*time.Hour).Unix() { - continue - } - }*/ - if !(criteria.SentBefore.IsZero() && criteria.SentSince.IsZero() /*&& criteria.SentOn.IsZero()*/) { + if !criteria.SentBefore.IsZero() || !criteria.SentSince.IsZero() { if t, err := m.Header.Date(); err == nil && !t.IsZero() { - // Filter header date. if !criteria.SentBefore.IsZero() { if truncated := criteria.SentBefore.Truncate(24 * time.Hour); t.Unix() > truncated.Unix() { continue @@ -294,16 +210,81 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria) continue } } - /*if !criteria.SentOn.IsZero() { - truncated := criteria.SentOn.Truncate(24 * time.Hour) - if t.Unix() < truncated.Unix() || t.Unix() > truncated.Add(24*time.Hour).Unix() { - continue + } + } + + // Filter by headers. + header := message.GetHeader(m) + headerMatch := true + for criteriaKey, criteriaValues := range criteria.Header { + for _, criteriaValue := range criteriaValues { + if criteriaValue == "" { + continue + } + switch criteriaKey { + case "From": + headerMatch = addressMatch([]*mail.Address{m.Sender}, criteriaValue) + case "To": + headerMatch = addressMatch(m.ToList, criteriaValue) + case "Cc": + headerMatch = addressMatch(m.CCList, criteriaValue) + case "Bcc": + headerMatch = addressMatch(m.BCCList, criteriaValue) + default: + if messageValue := header.Get(criteriaKey); messageValue == "" { + headerMatch = false // Field is not in header. + } else if !strings.Contains(strings.ToLower(messageValue), strings.ToLower(criteriaValue)) { + headerMatch = false // Field is in header but value not matched (case insensitive). } - }*/ + } + if !headerMatch { + break + } + } + if !headerMatch { + break + } + } + if !headerMatch { + continue + } + + // Filter by flags. + messageFlagsMap := make(map[string]bool) + if isStringInList(m.LabelIDs, pmapi.StarredLabel) { + messageFlagsMap[imap.FlaggedFlag] = true + } + if m.Unread == 0 { + messageFlagsMap[imap.SeenFlag] = true + } + if m.Has(pmapi.FlagReplied) || m.Has(pmapi.FlagRepliedAll) { + messageFlagsMap[imap.AnsweredFlag] = true + } + if m.Has(pmapi.FlagSent) || m.Has(pmapi.FlagReceived) { + messageFlagsMap[imap.DraftFlag] = true + } + if !m.Has(pmapi.FlagOpened) { + messageFlagsMap[imap.RecentFlag] = true + } + + flagMatch := true + for _, flag := range criteria.WithFlags { + if !messageFlagsMap[flag] { + flagMatch = false + break } } + for _, flag := range criteria.WithoutFlags { + if messageFlagsMap[flag] { + flagMatch = false + break + } + } + if !flagMatch { + continue + } - // Filter size (only if size was already calculated). + // Filter by size (only if size was already calculated). if m.Size > 0 { if criteria.Larger != 0 && m.Size <= int64(criteria.Larger) { continue @@ -452,13 +433,17 @@ func (im *imapMailbox) apiIDsFromSeqSet(uid bool, seqSet *imap.SeqSet) ([]string return apiIDs, nil } -func isAddressInList(addrs []*mail.Address, query string) bool { //nolint[deadcode] - for _, addr := range addrs { - if strings.Contains(addr.Address, query) || strings.Contains(addr.Name, query) { - return true +func arrayIntersection(a, b []string) (c []string) { + m := make(map[string]bool) + for _, item := range a { + m[item] = true + } + for _, item := range b { + if _, ok := m[item]; ok { + c = append(c, item) } } - return false + return } func isStringInList(list []string, s string) bool { @@ -478,12 +463,3 @@ func addressMatch(addresses []*mail.Address, criteria string) bool { } return false } - -func hasKeyword(m *pmapi.Message, keyword string) bool { - for _, v := range message.GetHeader(m) { - if strings.Contains(strings.ToLower(strings.Join(v, " ")), strings.ToLower(keyword)) { - return true - } - } - return false -} diff --git a/test/features/imap/message/search.feature b/test/features/imap/message/search.feature index 3570905..473777f 100644 --- a/test/features/imap/message/search.feature +++ b/test/features/imap/message/search.feature @@ -2,28 +2,79 @@ Feature: IMAP search messages Background: Given there is connected user "user" Given there are messages in mailbox "INBOX" for "user" - | from | to | subject | body | - | john.doe@mail.com | user@pm.me | foo | hello | - | jane.doe@mail.com | name@pm.me | bar | world | + | from | to | cc | subject | read | starred | body | + | john.doe@email.com | user@pm.me | | foo | false | false | hello | + | jane.doe@email.com | user@pm.me | name@pm.me | bar | true | true | world | + | jane.doe@email.com | name@pm.me | | baz | true | false | bye | And there is IMAP client logged in as "user" And there is IMAP client selected in "INBOX" - Scenario: Search by subject - When IMAP client searches for "SUBJECT foo" + Scenario: Search by Sequence numbers + When IMAP client searches for "1" + Then IMAP response is "OK" + And IMAP response contains "SEARCH 1[^0-9]*$" + + Scenario: Search by UID + When IMAP client searches for "UID 2" Then IMAP response is "OK" - And IMAP response has 1 message + And IMAP response contains "SEARCH 2[^0-9]*$" - Scenario: Search by text - When IMAP client searches for "TEXT world" + Scenario: Search by Sequence numbers and UID + When IMAP client searches for "1 UID 1" Then IMAP response is "OK" - And IMAP response has 1 message + And IMAP response contains "SEARCH 1[^0-9]*$" - Scenario: Search by from + Scenario: Search by Sequence numbers and UID without match + When IMAP client searches for "1 UID 2" + Then IMAP response is "OK" + And IMAP response contains "SEARCH[^0-9]*$" + + Scenario: Search by Subject + When IMAP client searches for "SUBJECT foo" + Then IMAP response is "OK" + And IMAP response contains "SEARCH 3[^0-9]*$" + + Scenario: Search by From When IMAP client searches for "FROM jane.doe@email.com" Then IMAP response is "OK" - And IMAP response has 1 message + And IMAP response contains "SEARCH 1 2[^0-9]*$" - Scenario: Search by to + Scenario: Search by To When IMAP client searches for "TO user@pm.me" Then IMAP response is "OK" - And IMAP response has 1 message + And IMAP response contains "SEARCH 2 3[^0-9]*$" + + Scenario: Search by CC + When IMAP client searches for "CC name@pm.me" + Then IMAP response is "OK" + And IMAP response contains "SEARCH 2[^0-9]*$" + + Scenario: Search flagged messages + When IMAP client searches for "FLAGGED" + Then IMAP response is "OK" + And IMAP response contains "SEARCH 2[^0-9]*$" + + Scenario: Search not flagged messages + When IMAP client searches for "UNFLAGGED" + Then IMAP response is "OK" + And IMAP response contains "SEARCH 1 3[^0-9]*$" + + Scenario: Search seen messages + When IMAP client searches for "SEEN" + Then IMAP response is "OK" + And IMAP response contains "SEARCH 1 2[^0-9]*$" + + Scenario: Search unseen messages + When IMAP client searches for "UNSEEN" + Then IMAP response is "OK" + And IMAP response contains "SEARCH 3[^0-9]*$" + + Scenario: Search recent messages + When IMAP client searches for "RECENT" + Then IMAP response is "OK" + And IMAP response contains "SEARCH 1 2 3[^0-9]*$" + + Scenario: Search by more criterias + When IMAP client searches for "SUBJECT baz TO name@pm.me SEEN UNFLAGGED" + Then IMAP response is "OK" + And IMAP response contains "SEARCH 1[^0-9]*$" diff --git a/test/store_setup_test.go b/test/store_setup_test.go index 8607dec..04578c1 100644 --- a/test/store_setup_test.go +++ b/test/store_setup_test.go @@ -97,6 +97,11 @@ func thereAreMessagesInMailboxesForAddressOfUser(mailboxNames, bddAddressID, bdd message.ToList = []*mail.Address{{ Address: ctx.EnsureAddress(account.Username(), cell.Value), }} + case "cc": + message.AddressID = ctx.EnsureAddressID(account.Username(), cell.Value) + message.CCList = []*mail.Address{{ + Address: ctx.EnsureAddress(account.Username(), cell.Value), + }} case "subject": message.Subject = cell.Value case "body":