aboutsummaryrefslogtreecommitdiffstats
path: root/worker/notmuch/search.go
diff options
context:
space:
mode:
authorRobin Jarry <robin@jarry.cc>2023-10-17 15:31:09 +0200
committerRobin Jarry <robin@jarry.cc>2023-10-28 19:24:59 +0200
commit8464b373851142b0becaaa10db34df3559b2b62e (patch)
treedd785d6c717f1e620c63252348d8add991587a57 /worker/notmuch/search.go
parent57088312fdd8e602a084bd5736a0e22a34be9ec0 (diff)
downloadaerc-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/notmuch/search.go')
-rw-r--r--worker/notmuch/search.go143
1 files changed, 87 insertions, 56 deletions
diff --git a/worker/notmuch/search.go b/worker/notmuch/search.go
index e1428b45..b3592d41 100644
--- a/worker/notmuch/search.go
+++ b/worker/notmuch/search.go
@@ -4,18 +4,18 @@
package notmuch
import (
- "strconv"
- "strings"
+ "fmt"
- "git.sr.ht/~rjarry/aerc/log"
- "git.sr.ht/~sircmpwn/getopt"
+ "git.sr.ht/~rjarry/aerc/models"
+ "git.sr.ht/~rjarry/aerc/worker/types"
+ "git.sr.ht/~rjarry/go-opt"
)
type queryBuilder struct {
s string
}
-func (q *queryBuilder) add(s string) {
+func (q *queryBuilder) and(s string) {
if len(s) == 0 {
return
}
@@ -25,62 +25,93 @@ func (q *queryBuilder) add(s string) {
q.s += "(" + s + ")"
}
-func translate(args []string) (string, error) {
- if len(args) == 0 {
- return "", nil
- }
- var qb queryBuilder
- opts, optind, err := getopt.Getopts(args, "rux:X:bat:H:f:c:d:")
- if err != nil {
- // if error occurs here, don't fail
- log.Errorf("getopts failed: %v", err)
- return strings.Join(args[1:], ""), nil
- }
- body := false
- for _, opt := range opts {
- switch opt.Option {
- case 'r':
- qb.add("not tag:unread")
- case 'u':
- qb.add("tag:unread")
- case 'x':
- qb.add(getParsedFlag(opt.Value))
- case 'X':
- qb.add("not " + getParsedFlag(opt.Value))
- case 'H':
- // TODO
- case 'f':
- qb.add("from:" + opt.Value)
- case 't':
- qb.add("to:" + opt.Value)
- case 'c':
- qb.add("cc:" + opt.Value)
- case 'a':
- // TODO
- case 'b':
- body = true
- case 'd':
- qb.add("date:" + strconv.Quote(opt.Value))
+func (q *queryBuilder) or(s string) {
+ if len(s) == 0 {
+ return
+ }
+ if len(q.s) != 0 {
+ q.s += " or "
+ }
+ q.s += "(" + s + ")"
+}
+
+func translate(crit *types.SearchCriteria) string {
+ if crit == nil {
+ return ""
+ }
+ var base queryBuilder
+
+ // recipients
+ var from queryBuilder
+ for _, f := range crit.From {
+ from.or("from:" + opt.QuoteArg(f))
+ }
+ if from.s != "" {
+ base.and(from.s)
+ }
+
+ var to queryBuilder
+ for _, t := range crit.To {
+ to.or("to:" + opt.QuoteArg(t))
+ }
+ if to.s != "" {
+ base.and(to.s)
+ }
+
+ var cc queryBuilder
+ for _, c := range crit.Cc {
+ cc.or("cc:" + opt.QuoteArg(c))
+ }
+ if cc.s != "" {
+ base.and(cc.s)
+ }
+
+ // flags
+ for _, f := range []models.Flags{models.SeenFlag, models.AnsweredFlag, models.FlaggedFlag} {
+ if crit.WithFlags.Has(f) {
+ base.and(getParsedFlag(f, false))
+ }
+ if crit.WithoutFlags.Has(f) {
+ base.and(getParsedFlag(f, true))
}
}
+
+ // dates
switch {
- case body:
- qb.add("body:" + strconv.Quote(strings.Join(args[optind:], " ")))
- default:
- qb.add(strings.Join(args[optind:], " "))
+ case !crit.StartDate.IsZero() && !crit.EndDate.IsZero():
+ base.and(fmt.Sprintf("date:@%d..@%d",
+ crit.StartDate.Unix(), crit.EndDate.Unix()))
+ case !crit.StartDate.IsZero():
+ base.and(fmt.Sprintf("date:@%d..", crit.StartDate.Unix()))
+ case !crit.EndDate.IsZero():
+ base.and(fmt.Sprintf("date:..@%d", crit.EndDate.Unix()))
+ }
+
+ // other terms
+ if crit.Terms != "" {
+ if crit.SearchBody {
+ base.and("body:" + opt.QuoteArg(crit.Terms))
+ } else {
+ base.and(crit.Terms)
+ }
}
- return qb.s, nil
+
+ return base.s
}
-func getParsedFlag(name string) string {
- switch strings.ToLower(name) {
- case "answered":
- return "tag:replied"
- case "seen":
- return "(not tag:unread)"
- case "flagged":
- return "tag:flagged"
- default:
- return name
+func getParsedFlag(flag models.Flags, inverse bool) string {
+ name := ""
+ switch flag {
+ case models.AnsweredFlag:
+ name = "tag:replied"
+ case models.SeenFlag:
+ name = "tag:unread"
+ inverse = !inverse
+ case models.FlaggedFlag:
+ name = "tag:flagged"
+ }
+ if inverse {
+ name = "not " + name
}
+ return name
}