aboutsummaryrefslogtreecommitdiffstats
path: root/worker/maildir/search.go
diff options
context:
space:
mode:
Diffstat (limited to 'worker/maildir/search.go')
-rw-r--r--worker/maildir/search.go286
1 files changed, 7 insertions, 279 deletions
diff --git a/worker/maildir/search.go b/worker/maildir/search.go
index c1ea4de2..cf954753 100644
--- a/worker/maildir/search.go
+++ b/worker/maildir/search.go
@@ -2,114 +2,17 @@ package maildir
import (
"context"
- "io"
- "net/textproto"
"runtime"
- "strings"
"sync"
- "time"
- "unicode"
- "github.com/emersion/go-maildir"
-
- "git.sr.ht/~sircmpwn/getopt"
-
- "git.sr.ht/~rjarry/aerc/lib"
- "git.sr.ht/~rjarry/aerc/lib/parse"
"git.sr.ht/~rjarry/aerc/log"
- "git.sr.ht/~rjarry/aerc/models"
+ "git.sr.ht/~rjarry/aerc/worker/lib"
+ "git.sr.ht/~rjarry/aerc/worker/types"
)
-type searchCriteria struct {
- Header textproto.MIMEHeader
- Body []string
- Text []string
-
- WithFlags []maildir.Flag
- WithoutFlags []maildir.Flag
-
- startDate, endDate time.Time
-}
-
-func parseSearch(args []string) (*searchCriteria, error) {
- criteria := &searchCriteria{Header: make(textproto.MIMEHeader)}
-
- opts, optind, err := getopt.Getopts(args, "rux:X:bat:H:f:c:d:")
- if err != nil {
- return nil, err
- }
- body := false
- text := false
- for _, opt := range opts {
- switch opt.Option {
- case 'r':
- criteria.WithFlags = append(criteria.WithFlags, maildir.FlagSeen)
- case 'u':
- criteria.WithoutFlags = append(criteria.WithoutFlags, maildir.FlagSeen)
- case 'x':
- criteria.WithFlags = append(criteria.WithFlags, getParsedFlag(opt.Value))
- case 'X':
- criteria.WithoutFlags = append(criteria.WithoutFlags, getParsedFlag(opt.Value))
- case 'H':
- if strings.Contains(opt.Value, ": ") {
- HeaderValue := strings.SplitN(opt.Value, ": ", 2)
- criteria.Header.Add(HeaderValue[0], HeaderValue[1])
- } else {
- log.Errorf("Header is not given properly, must be given in format `Header: Value`")
- continue
- }
- case 'f':
- criteria.Header.Add("From", opt.Value)
- case 't':
- criteria.Header.Add("To", opt.Value)
- case 'c':
- criteria.Header.Add("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() {
- criteria.startDate = start
- }
- if !end.IsZero() {
- criteria.endDate = end
- }
- }
- }
- switch {
- case text:
- criteria.Text = args[optind:]
- case body:
- criteria.Body = args[optind:]
- default:
- for _, arg := range args[optind:] {
- criteria.Header.Add("Subject", arg)
- }
- }
- return criteria, nil
-}
-
-func getParsedFlag(name string) maildir.Flag {
- var f maildir.Flag
- switch strings.ToLower(name) {
- case "seen":
- f = maildir.FlagSeen
- case "answered":
- f = maildir.FlagReplied
- case "flagged":
- f = maildir.FlagFlagged
- }
- return f
-}
+func (w *Worker) search(ctx context.Context, criteria *types.SearchCriteria) ([]uint32, error) {
+ requiredParts := lib.GetRequiredParts(criteria)
-func (w *Worker) search(ctx context.Context, criteria *searchCriteria) ([]uint32, error) {
- requiredParts := getRequiredParts(criteria)
w.worker.Debugf("Required parts bitmask for search: %b", requiredParts)
keys, err := w.c.UIDs(*w.selected)
@@ -152,187 +55,12 @@ func (w *Worker) search(ctx context.Context, criteria *searchCriteria) ([]uint32
}
// Execute the search criteria for the given key, returns true if search succeeded
-func (w *Worker) searchKey(key uint32, criteria *searchCriteria,
- parts MsgParts,
+func (w *Worker) searchKey(key uint32, criteria *types.SearchCriteria,
+ parts lib.MsgParts,
) (bool, error) {
message, err := w.c.Message(*w.selected, key)
if err != nil {
return false, err
}
-
- // setup parts of the message to use in the search
- // this is so that we try to minimise reading unnecessary parts
- var (
- flags []maildir.Flag
- header *models.MessageInfo
- body string
- all string
- )
-
- if parts&FLAGS > 0 {
- flags, err = message.Flags()
- if err != nil {
- return false, err
- }
- }
- if parts&HEADER > 0 || parts&DATE > 0 {
- header, err = message.MessageInfo()
- if err != nil {
- return false, err
- }
- }
- if parts&BODY > 0 {
- // TODO: select which part to search, maybe look for text/plain
- mi, err := message.MessageInfo()
- if err != nil {
- return false, err
- }
- path := lib.FindFirstNonMultipart(mi.BodyStructure, nil)
- reader, err := message.NewBodyPartReader(path)
- if err != nil {
- return false, err
- }
- bytes, err := io.ReadAll(reader)
- if err != nil {
- return false, err
- }
- body = string(bytes)
- }
- if parts&ALL > 0 {
- reader, err := message.NewReader()
- if err != nil {
- return false, err
- }
- defer reader.Close()
- bytes, err := io.ReadAll(reader)
- if err != nil {
- return false, err
- }
- all = string(bytes)
- }
-
- // now search through the criteria
- // implicit AND at the moment so fail fast
- if criteria.Header != nil {
- for k, v := range criteria.Header {
- headerValue := header.RFC822Headers.Get(k)
- for _, text := range v {
- if !containsSmartCase(headerValue, text) {
- return false, nil
- }
- }
- }
- }
- if criteria.Body != nil {
- for _, searchTerm := range criteria.Body {
- if !containsSmartCase(body, searchTerm) {
- return false, nil
- }
- }
- }
- if criteria.Text != nil {
- for _, searchTerm := range criteria.Text {
- if !containsSmartCase(all, searchTerm) {
- return false, nil
- }
- }
- }
- if criteria.WithFlags != nil {
- for _, searchFlag := range criteria.WithFlags {
- if !containsFlag(flags, searchFlag) {
- return false, nil
- }
- }
- }
- if criteria.WithoutFlags != nil {
- for _, searchFlag := range criteria.WithoutFlags {
- if containsFlag(flags, searchFlag) {
- return false, nil
- }
- }
- }
- if parts&DATE > 0 {
- if date, err := header.RFC822Headers.Date(); err != nil {
- w.worker.Errorf("Failed to get date from header: %v", err)
- } else {
- if !criteria.startDate.IsZero() {
- if date.Before(criteria.startDate) {
- return false, nil
- }
- }
- if !criteria.endDate.IsZero() {
- if date.After(criteria.endDate) {
- return false, nil
- }
- }
- }
- }
- return true, nil
-}
-
-// Returns true if searchFlag appears in flags
-func containsFlag(flags []maildir.Flag, searchFlag maildir.Flag) bool {
- match := false
- for _, flag := range flags {
- if searchFlag == flag {
- match = true
- }
- }
- return match
-}
-
-// Smarter version of strings.Contains for searching.
-// Is case-insensitive unless substr contains an upper case character
-func containsSmartCase(s string, substr string) bool {
- if hasUpper(substr) {
- return strings.Contains(s, substr)
- }
- return strings.Contains(strings.ToLower(s), strings.ToLower(substr))
-}
-
-func hasUpper(s string) bool {
- for _, r := range s {
- if unicode.IsUpper(r) {
- return true
- }
- }
- return false
-}
-
-// The parts of a message, kind of
-type MsgParts int
-
-const NONE MsgParts = 0
-const (
- FLAGS MsgParts = 1 << iota
- HEADER
- DATE
- BODY
- ALL
-)
-
-// Returns a bitmask of the parts of the message required to be loaded for the
-// given criteria
-func getRequiredParts(criteria *searchCriteria) MsgParts {
- required := NONE
- if len(criteria.Header) > 0 {
- required |= HEADER
- }
- if !criteria.startDate.IsZero() || !criteria.endDate.IsZero() {
- required |= DATE
- }
- if criteria.Body != nil && len(criteria.Body) > 0 {
- required |= BODY
- }
- if criteria.Text != nil && len(criteria.Text) > 0 {
- required |= ALL
- }
- if criteria.WithFlags != nil && len(criteria.WithFlags) > 0 {
- required |= FLAGS
- }
- if criteria.WithoutFlags != nil && len(criteria.WithoutFlags) > 0 {
- required |= FLAGS
- }
-
- return required
+ return lib.SearchMessage(message, criteria, parts)
}