Browse Source

Implement broader search functionality

master
Drew DeVault 3 years ago
parent
commit
b437cef2ab
2 changed files with 163 additions and 26 deletions
  1. +1
    -26
      plugins/base/imap.go
  2. +162
    -0
      plugins/base/search.go

+ 1
- 26
plugins/base/imap.go View File

@@ -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
- 0
plugins/base/search.go View 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…
Cancel
Save