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 }