2020-10-23 15:45:00 +00:00
|
|
|
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
|
|
|
|
)
|
2021-07-05 09:19:07 +00:00
|
|
|
if colon+1 < len(buf) && buf[colon+1] == byte('"') {
|
2020-10-23 15:45:00 +00:00
|
|
|
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
|
|
|
|
}
|