aboutsummaryrefslogblamecommitdiff
path: root/plugins/base/search.go
blob: 4d8cef80740e9092ceb2634c4f4dfd2bc0a7da89 (plain) (tree)

































































































































































                                                                                              
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
}