|
|
@@ -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 |
|
|
|
} |