package bql import ( "net/mail" "fmt" "io" "strings" // TODO think about caching only in bql stdregexp "regexp" "pkg.jfrech.com/brief/docket" "pkg.jfrech.com/brief/humaninterface" "pkg.jfrech.com/brief/internal/headersemantics" "pkg.jfrech.com/brief/sem" ) type Sorting int const ( UNSORTED = Sorting(iota) DATE MODTIME // TODO use "pkg.jfrech.com/brief/internal/tiesort" (see msgs/message_meta.go: SortMessages) //DATE_PREFER_INBOX // TODO Any other ideas? ) type Query struct { Unoptimised BQL Context // TODO Rename "s/SortBy/Sorting/"? SortBy Sorting } func (q Query) String() string { return q.Unoptimised.String() } // context must not be nil func Compile(context *Context, raw []string) (*Query, error) { if context == nil { panic("context must not be nil") } q, err := Parse(raw) if err != nil { return nil, err } return &Query{ Unoptimised: q, Context: *context, }, nil } // header is recommended to be lazy and QuickHeader-infused. // body is recommended to be lazy. func (q *Query) Match(header headersemantics.Header, dkt docket.Docket, body io.Reader) (matched bool, err error) { return inefficientmatch(&q.Context, q.Unoptimised, dkt, header, body) } // TODO: The query does not get compiled: profile! // NOTE: searchedfor currently operates lazily even in the recognition of verbs, i.e. (and date=never wakka) is a valid query never yielding. // TODO: Due to client.QuickHeader, move queries involving cached values to the front so they may fail early. func inefficientmatch(bqlcontext *Context, q BQL, dkt docket.Docket, header headersemantics.Header, body io.Reader) (bool, error) { // "cq" means "**c**oncrete **q**uery" switch cq := q.(type) { default: panic("unreachable") // TODO: Hard design decision: potentially reveals deep insights into one's psyche. case BQLEmpty: return true, nil case BQLQry: // TODO hacky switch cq.Raw { case "everything": return true, nil case "transfer-status=outgoing": ts, _ := dkt.TransferStatus() return ts == sem.TransferStatusOutgoing, nil case "transfer-status-indeterminate": switch ts, _ := dkt.TransferStatus(); ts { case sem.TransferStatusSent, sem.TransferStatusRetrieved: return false, nil case "": return false, nil default: return true, nil } case "mailbox=INBOX": return dkt.Mailbox() == sem.INBOX, nil case "hot": return dkt.IsHot(), nil } addressMentioned := func(get func(headersemantics.Header) ([]*mail.Address, error)) func(string) (bool, error) { return func(raw string) (bool, error) { addresses, err := get(header) if err != nil { return false, err } want := bqlcontext.LookupAddress(raw) if want == nil { return false, fmt.Errorf("invalid address: %q", raw) } for _, address := range addresses { if want.Address == address.Address { return true, nil } } return false, nil } } addressRe := func(get func(headersemantics.Header) ([]*mail.Address, error)) func(string) (bool, error) { return func(raw string) (bool, error) { // TODO cache compiled regular expressions re, err := stdregexp.Compile(`(?i)`+raw) if err != nil { return false, fmt.Errorf("%w: addressRe: %v", ErrBadVerb, err) } addresses, err := get(header) if err != nil { return false, err } b := new(strings.Builder) for j, address := range addresses { if j > 0 { b.WriteString(", ") } b.WriteString(address.String()) } return re.MatchString(b.String()), nil } } for prefix, action := range map[string]func(string) (bool, error) { "date=": func(raw string) (bool, error) { ts, err := humaninterface.Parse(bqlcontext.Now, raw) if err != nil { return false, err } t, _ := headersemantics.GetDate(header) return ts.Includes(t), nil }, "initiator=": addressMentioned(headersemantics.GetInitiators), "initiator~": addressRe(headersemantics.GetInitiators), "from=": addressMentioned(headersemantics.GetFroms), "from~": addressRe(headersemantics.GetFroms), "to=": addressMentioned(headersemantics.GetTos), "to~": addressRe(headersemantics.GetTos), "recipient=": addressMentioned(headersemantics.GetRecipients), "recipient~": addressRe(headersemantics.GetRecipients), "subject~": func(raw string) (bool, error) { // TODO cache compiled regular expressions re, err := stdregexp.Compile(`(?i)`+raw) if err != nil { return false, fmt.Errorf("%w: subject~: %v", ErrBadVerb, err) } subject, err := headersemantics.GetSubject(header) if err != nil { return false, err } return re.MatchString(subject), nil }, } { if strings.HasPrefix(cq.Raw, prefix) { return action(strings.TrimPrefix(cq.Raw, prefix)) } } return false, fmt.Errorf("%w: unknown verb: %q", ErrBadVerb, cq.Raw) // (not x y) means (and (not x) (not y)). // One could say "not is neither". case BQLNot: for _, q := range cq.Qs { yes, err := inefficientmatch(bqlcontext, q, dkt, header, body) if err != nil { return false, err } if yes { return false, nil } } return true, nil case BQLAnd: for _, q := range cq.Qs { yes, err := inefficientmatch(bqlcontext, q, dkt, header, body) if err != nil { return false, err } if !yes { return false, nil } } return true, nil case BQLOr: for _, q := range cq.Qs { yes, err := inefficientmatch(bqlcontext, q, dkt, header, body) if err != nil { return false, err } if yes { return true, nil } } return false, nil } }