alps/plugins/base/search.go
2020-10-23 11:45:00 -04:00

162 lines
3.9 KiB
Go

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
}