diff options
author | Robin Jarry <robin@jarry.cc> | 2023-10-17 15:31:09 +0200 |
---|---|---|
committer | Robin Jarry <robin@jarry.cc> | 2023-10-28 19:24:59 +0200 |
commit | 8464b373851142b0becaaa10db34df3559b2b62e (patch) | |
tree | dd785d6c717f1e620c63252348d8add991587a57 /worker/jmap | |
parent | 57088312fdd8e602a084bd5736a0e22a34be9ec0 (diff) | |
download | aerc-8464b373851142b0becaaa10db34df3559b2b62e.tar.gz |
search: use a common api for all workers
Define a SearchCriteria structure. Update the FetchDirectoryContents,
FetchDirectoryThreaded and SearchDirectory worker messages to include
this SearchCriteria structure instead of a []string slice.
Parse the search arguments in a single place into a SearchCriteria
structure and use it to search/filter via the message store.
Update all workers to use that new API. Clarify the man page indicating
that notmuch supports searching with aerc's syntax and also with notmuch
specific syntax.
getopt is no longer needed, remove it from go.mod.
NB: to support more complex search filters in JMAP, we need to use an
email.Filter interface. Since GOB does not support encoding/decoding
interfaces, store the raw SearchCriteria and []SortCriterion values in
the cached FolderContents. Translate them to JMAP API objects when
sending an email.Query request to the server.
Signed-off-by: Robin Jarry <robin@jarry.cc>
Reviewed-by: Koni Marti <koni.marti@gmail.com>
Tested-by: Moritz Poldrack <moritz@poldrack.dev>
Tested-by: Inwit <inwit@sindominio.net>
Diffstat (limited to 'worker/jmap')
-rw-r--r-- | worker/jmap/cache/cache.go | 33 | ||||
-rw-r--r-- | worker/jmap/cache/folder_contents.go | 17 | ||||
-rw-r--r-- | worker/jmap/directories.go | 39 | ||||
-rw-r--r-- | worker/jmap/push.go | 4 | ||||
-rw-r--r-- | worker/jmap/search.go | 137 |
5 files changed, 138 insertions, 92 deletions
diff --git a/worker/jmap/cache/cache.go b/worker/jmap/cache/cache.go index 249ed0e9..6d815177 100644 --- a/worker/jmap/cache/cache.go +++ b/worker/jmap/cache/cache.go @@ -4,10 +4,12 @@ import ( "errors" "os" "path" + "strings" "git.sr.ht/~rjarry/aerc/lib/xdg" "git.sr.ht/~rjarry/aerc/log" "github.com/syndtr/goleveldb/leveldb" + "github.com/syndtr/goleveldb/leveldb/util" ) type JMAPCache struct { @@ -74,3 +76,34 @@ func (c *JMAPCache) delete(key string) error { } panic("jmap cache with no backend") } + +func (c *JMAPCache) purge(prefix string) error { + switch { + case c.file != nil: + txn, err := c.file.OpenTransaction() + if err != nil { + return err + } + iter := txn.NewIterator(util.BytesPrefix([]byte(prefix)), nil) + for iter.Next() { + err = txn.Delete(iter.Key(), nil) + if err != nil { + break + } + } + iter.Release() + if err != nil { + txn.Discard() + return err + } + return txn.Commit() + case c.mem != nil: + for key := range c.mem { + if strings.HasPrefix(key, prefix) { + delete(c.mem, key) + } + } + return nil + } + panic("jmap cache with no backend") +} diff --git a/worker/jmap/cache/folder_contents.go b/worker/jmap/cache/folder_contents.go index 6c6a7d80..46a36607 100644 --- a/worker/jmap/cache/folder_contents.go +++ b/worker/jmap/cache/folder_contents.go @@ -3,26 +3,32 @@ package cache import ( "reflect" + "git.sr.ht/~rjarry/aerc/log" + "git.sr.ht/~rjarry/aerc/worker/types" "git.sr.ht/~rockorager/go-jmap" - "git.sr.ht/~rockorager/go-jmap/mail/email" ) type FolderContents struct { MailboxID jmap.ID QueryState string - Filter *email.FilterCondition - Sort []*email.SortComparator + Filter *types.SearchCriteria + Sort []*types.SortCriterion MessageIDs []jmap.ID } func (c *JMAPCache) GetFolderContents(mailboxId jmap.ID) (*FolderContents, error) { - buf, err := c.get(folderContentsKey(mailboxId)) + key := folderContentsKey(mailboxId) + buf, err := c.get(key) if err != nil { return nil, err } m := new(FolderContents) err = unmarshal(buf, m) if err != nil { + log.Debugf("cache format has changed, purging foldercontents") + if e := c.purge("foldercontents/"); e != nil { + log.Errorf("foldercontents cache purge: %s", e) + } return nil, err } return m, nil @@ -45,7 +51,7 @@ func folderContentsKey(mailboxId jmap.ID) string { } func (f *FolderContents) NeedsRefresh( - filter *email.FilterCondition, sort []*email.SortComparator, + filter *types.SearchCriteria, sort []*types.SortCriterion, ) bool { if f.QueryState == "" || f.Filter == nil || len(f.Sort) != len(sort) { return true @@ -56,6 +62,5 @@ func (f *FolderContents) NeedsRefresh( return true } } - return !reflect.DeepEqual(filter, f.Filter) } diff --git a/worker/jmap/directories.go b/worker/jmap/directories.go index 0fa3d898..bc47e691 100644 --- a/worker/jmap/directories.go +++ b/worker/jmap/directories.go @@ -126,25 +126,16 @@ func (w *JMAPWorker) handleFetchDirectoryContents(msg *types.FetchDirectoryConte if err != nil { contents = &cache.FolderContents{ MailboxID: w.selectedMbox, - Filter: &email.FilterCondition{}, } } - filter, err := parseSearch(msg.FilterCriteria) - if err != nil { - return err - } - filter.InMailbox = w.selectedMbox - - sort := translateSort(msg.SortCriteria) - - if contents.NeedsRefresh(filter, sort) { + if contents.NeedsRefresh(msg.Filter, msg.SortCriteria) { var req jmap.Request req.Invoke(&email.Query{ Account: w.accountId, - Filter: filter, - Sort: sort, + Filter: w.translateSearch(w.selectedMbox, msg.Filter), + Sort: translateSort(msg.SortCriteria), }) resp, err := w.Do(&req) if err != nil { @@ -154,8 +145,8 @@ func (w *JMAPWorker) handleFetchDirectoryContents(msg *types.FetchDirectoryConte for _, inv := range resp.Responses { switch r := inv.Args.(type) { case *email.QueryResponse: - contents.Sort = sort - contents.Filter = filter + contents.Sort = msg.SortCriteria + contents.Filter = msg.Filter contents.QueryState = r.QueryState contents.MessageIDs = r.IDs canCalculateChanges = r.CanCalculateChanges @@ -193,27 +184,9 @@ func (w *JMAPWorker) handleFetchDirectoryContents(msg *types.FetchDirectoryConte func (w *JMAPWorker) handleSearchDirectory(msg *types.SearchDirectory) error { var req jmap.Request - filter, err := parseSearch(msg.Argv) - if err != nil { - return err - } - if w.selectedMbox == "" { - // all mail virtual folder: display all but trash and spam - var mboxes []jmap.ID - if id, ok := w.roles[mailbox.RoleJunk]; ok { - mboxes = append(mboxes, id) - } - if id, ok := w.roles[mailbox.RoleTrash]; ok { - mboxes = append(mboxes, id) - } - filter.InMailboxOtherThan = mboxes - } else { - filter.InMailbox = w.selectedMbox - } - req.Invoke(&email.Query{ Account: w.accountId, - Filter: filter, + Filter: w.translateSearch(w.selectedMbox, msg.Criteria), }) resp, err := w.Do(&req) diff --git a/worker/jmap/push.go b/worker/jmap/push.go index 320fee4f..34a90ca4 100644 --- a/worker/jmap/push.go +++ b/worker/jmap/push.go @@ -127,8 +127,8 @@ func (w *JMAPWorker) refresh(newState jmap.TypeState) error { } callID = req.Invoke(&email.QueryChanges{ Account: w.accountId, - Filter: contents.Filter, - Sort: contents.Sort, + Filter: w.translateSearch(id, contents.Filter), + Sort: translateSort(contents.Sort), SinceQueryState: contents.QueryState, }) queryChangesCalls[callID] = id diff --git a/worker/jmap/search.go b/worker/jmap/search.go index 17b5ca11..0101ce9b 100644 --- a/worker/jmap/search.go +++ b/worker/jmap/search.go @@ -1,63 +1,98 @@ package jmap import ( - "strings" - - "git.sr.ht/~rjarry/aerc/lib/parse" - "git.sr.ht/~rjarry/aerc/log" + "git.sr.ht/~rjarry/aerc/worker/types" + "git.sr.ht/~rockorager/go-jmap" "git.sr.ht/~rockorager/go-jmap/mail/email" - "git.sr.ht/~sircmpwn/getopt" + "git.sr.ht/~rockorager/go-jmap/mail/mailbox" ) -func parseSearch(args []string) (*email.FilterCondition, error) { - f := new(email.FilterCondition) - if len(args) == 0 { - return f, nil - } - - opts, optind, err := getopt.Getopts(args, "rubax:X:t:H:f:c:d:") - if err != nil { - return nil, err - } - body := false - text := false - for _, opt := range opts { - switch opt.Option { - case 'r': - f.HasKeyword = "$seen" - case 'u': - f.NotKeyword = "$seen" - case 'f': - f.From = opt.Value - case 't': - f.To = opt.Value - case 'c': - f.Cc = opt.Value - case 'b': - body = true - case 'a': - text = true - case 'd': - start, end, err := parse.DateRange(opt.Value) - if err != nil { - log.Errorf("failed to parse start date: %v", err) - continue - } - if !start.IsZero() { - f.After = &start - } - if !end.IsZero() { - f.Before = &end - } +func (w *JMAPWorker) translateSearch( + mbox jmap.ID, criteria *types.SearchCriteria, +) email.Filter { + cond := new(email.FilterCondition) + + if mbox == "" { + // all mail virtual folder: display all but trash and spam + var mboxes []jmap.ID + if id, ok := w.roles[mailbox.RoleJunk]; ok { + mboxes = append(mboxes, id) + } + if id, ok := w.roles[mailbox.RoleTrash]; ok { + mboxes = append(mboxes, id) } + cond.InMailboxOtherThan = mboxes + } else { + cond.InMailbox = mbox + } + if criteria == nil { + return cond + } + + // dates + if !criteria.StartDate.IsZero() { + cond.After = &criteria.StartDate } + if !criteria.EndDate.IsZero() { + cond.Before = &criteria.EndDate + } + + // general search terms switch { - case text: - f.Text = strings.Join(args[optind:], " ") - case body: - f.Body = strings.Join(args[optind:], " ") + case criteria.SearchAll: + cond.Text = criteria.Terms + case criteria.SearchBody: + cond.Body = criteria.Terms default: - f.Subject = strings.Join(args[optind:], " ") + cond.Subject = criteria.Terms + } + + filter := &email.FilterOperator{Operator: jmap.OperatorAND} + filter.Conditions = append(filter.Conditions, cond) + + // keywords/flags + for kw := range flagsToKeywords(criteria.WithFlags) { + filter.Conditions = append(filter.Conditions, + &email.FilterCondition{HasKeyword: kw}) + } + for kw := range flagsToKeywords(criteria.WithoutFlags) { + filter.Conditions = append(filter.Conditions, + &email.FilterCondition{NotKeyword: kw}) } - return f, nil + + // recipients + addrs := &email.FilterOperator{ + Operator: jmap.OperatorOR, + } + for _, from := range criteria.From { + addrs.Conditions = append(addrs.Conditions, + &email.FilterCondition{From: from}) + } + for _, to := range criteria.To { + addrs.Conditions = append(addrs.Conditions, + &email.FilterCondition{To: to}) + } + for _, cc := range criteria.Cc { + addrs.Conditions = append(addrs.Conditions, + &email.FilterCondition{Cc: cc}) + } + if len(addrs.Conditions) > 0 { + filter.Conditions = append(filter.Conditions, addrs) + } + + // specific headers + headers := &email.FilterOperator{ + Operator: jmap.OperatorAND, + } + for h, values := range criteria.Headers { + for _, v := range values { + headers.Conditions = append(headers.Conditions, + &email.FilterCondition{Header: []string{h, v}}) + } + } + if len(headers.Conditions) > 0 { + filter.Conditions = append(filter.Conditions, headers) + } + + return filter } |