Implement broader search functionality
This commit is contained in:
parent
0769190180
commit
b437cef2ab
2 changed files with 163 additions and 26 deletions
|
@ -399,37 +399,12 @@ func listMessages(conn *imapclient.Client, mbox *MailboxStatus, page, messagesPe
|
|||
return msgs, nil
|
||||
}
|
||||
|
||||
func searchCriteriaHeader(k, v string) *imap.SearchCriteria {
|
||||
return &imap.SearchCriteria{
|
||||
Header: map[string][]string{
|
||||
k: []string{v},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func searchCriteriaOr(criteria ...*imap.SearchCriteria) *imap.SearchCriteria {
|
||||
or := criteria[0]
|
||||
for _, c := range criteria[1:] {
|
||||
or = &imap.SearchCriteria{
|
||||
Or: [][2]*imap.SearchCriteria{{or, c}},
|
||||
}
|
||||
}
|
||||
return or
|
||||
}
|
||||
|
||||
func searchMessages(conn *imapclient.Client, mboxName, query string, page, messagesPerPage int) (msgs []IMAPMessage, total int, err error) {
|
||||
if err := ensureMailboxSelected(conn, mboxName); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// TODO: full-text search on demand (can be slow)
|
||||
//criteria := &imap.SearchCriteria{Text: []string{query}}
|
||||
criteria := searchCriteriaOr(
|
||||
searchCriteriaHeader("From", query),
|
||||
searchCriteriaHeader("To", query),
|
||||
searchCriteriaHeader("Cc", query),
|
||||
searchCriteriaHeader("Subject", query),
|
||||
)
|
||||
criteria := PrepareSearch(query)
|
||||
nums, err := conn.Search(criteria)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("UID SEARCH failed: %v", err)
|
||||
|
|
162
plugins/base/search.go
Normal file
162
plugins/base/search.go
Normal file
|
@ -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
|
||||
}
|
Loading…
Reference in a new issue