aboutsummaryrefslogtreecommitdiff
path: root/plugins/base/search.go
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/base/search.go')
-rw-r--r--plugins/base/search.go162
1 files changed, 162 insertions, 0 deletions
diff --git a/plugins/base/search.go b/plugins/base/search.go
new file mode 100644
index 0000000..4d8cef8
--- /dev/null
+++ b/plugins/base/search.go
@@ -0,0 +1,162 @@
+package alpsbase
+
+import (
+ "bufio"
+ "bytes"
+ "net/textproto"
+ "strings"
+
+ "github.com/emersion/go-imap"
+)
+
+func searchCriteriaHeader(k, v string) *imap.SearchCriteria {
+ return &imap.SearchCriteria{
+ Header: map[string][]string{
+ k: []string{v},
+ },
+ }
+}
+
+func searchCriteriaOr(criteria ...*imap.SearchCriteria) *imap.SearchCriteria {
+ if criteria[0] == nil {
+ criteria = criteria[1:]
+ }
+ or := criteria[0]
+ for _, c := range criteria[1:] {
+ or = &imap.SearchCriteria{
+ Or: [][2]*imap.SearchCriteria{{or, c}},
+ }
+ }
+ return or
+}
+
+func searchCriteriaAnd(criteria ...*imap.SearchCriteria) *imap.SearchCriteria {
+ if criteria[0] == nil {
+ criteria = criteria[1:]
+ }
+ and := criteria[0]
+ for _, c := range criteria[1:] {
+ // TODO: Maybe pitch the AND and OR functions to go-imap upstream
+ if c.Header != nil {
+ if and.Header == nil {
+ and.Header = make(textproto.MIMEHeader)
+ }
+
+ for key, value := range c.Header {
+ if _, ok := and.Header[key]; !ok {
+ and.Header[key] = nil
+ }
+ and.Header[key] = append(and.Header[key], value...)
+ }
+ }
+ and.Body = append(and.Body, c.Body...)
+ and.Text = append(and.Text, c.Text...)
+ and.WithFlags = append(and.WithFlags, c.WithFlags...)
+ and.WithoutFlags = append(and.WithoutFlags, c.WithoutFlags...)
+ // TODO: Merge more things
+ }
+ return and
+}
+
+// Splits search up into the longest string of non-functional parts and
+// functional parts
+//
+// Input: hello world foo:bar baz trains:"are cool"
+// Output: ["hello world", "foo:bar", "baz", "trains:are cool"]
+func splitSearchTokens(buf []byte, eof bool) (int, []byte, error) {
+ if len(buf) == 0 {
+ return 0, nil, nil
+ }
+
+ if buf[0] == ' ' {
+ return 1, nil, nil
+ }
+
+ colon := bytes.IndexByte(buf, byte(':'))
+ if colon == -1 && eof {
+ return len(buf), buf, nil
+ } else if colon == -1 {
+ return 0, nil, nil
+ } else {
+ space := bytes.LastIndexByte(buf[:colon], byte(' '))
+ if space != -1 {
+ return space, buf[:space], nil
+ }
+
+ var (
+ terminator int
+ quoted bool
+ )
+ if colon + 1 < len(buf) && buf[colon+1] == byte('"') {
+ terminator = bytes.IndexByte(buf[colon+2:], byte('"'))
+ terminator += colon + 3
+ quoted = true
+ } else {
+ terminator = bytes.IndexByte(buf[colon:], byte(' '))
+ terminator += colon
+ }
+
+ if terminator == -1 {
+ return 0, nil, nil
+ } else if terminator == -1 && eof {
+ terminator = len(buf)
+ }
+
+ if quoted {
+ trimmed := append(buf[:colon+1], buf[colon+2:terminator-1]...)
+ return terminator, trimmed, nil
+ }
+
+ return terminator, buf[:terminator], nil
+ }
+}
+
+// TODO: Document search functionality somewhere
+func PrepareSearch(terms string) *imap.SearchCriteria {
+ // XXX: If Migadu's IMAP servers can learn a better Full-Text Search then
+ // we can probably start matching on the message bodies by default (gated
+ // behind some kind of flag, perhaps)
+ var criteria *imap.SearchCriteria
+
+ scanner := bufio.NewScanner(strings.NewReader(terms))
+ scanner.Split(splitSearchTokens)
+
+ for scanner.Scan() {
+ term := scanner.Text()
+ if !strings.ContainsRune(term, ':') {
+ criteria = searchCriteriaAnd(
+ criteria,
+ searchCriteriaOr(
+ searchCriteriaHeader("From", term),
+ searchCriteriaHeader("To", term),
+ searchCriteriaHeader("Cc", term),
+ searchCriteriaHeader("Subject", term),
+ ),
+ )
+ } else {
+ parts := strings.SplitN(term, ":", 2)
+ key, value := parts[0], parts[1]
+ switch strings.ToLower(key) {
+ case "from":
+ criteria = searchCriteriaAnd(
+ criteria, searchCriteriaHeader("From", value))
+ case "to":
+ criteria = searchCriteriaAnd(
+ criteria, searchCriteriaHeader("To", value))
+ case "cc":
+ criteria = searchCriteriaAnd(
+ criteria, searchCriteriaHeader("Cc", value))
+ case "subject":
+ criteria = searchCriteriaAnd(
+ criteria, searchCriteriaHeader("Subject", value))
+ case "body":
+ criteria = searchCriteriaAnd(
+ criteria, &imap.SearchCriteria{Body: []string{value}})
+ default:
+ continue
+ }
+ }
+ }
+
+ return criteria
+}