aboutsummaryrefslogtreecommitdiff
path: root/vendor/github.com/emersion/go-imap/search.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/emersion/go-imap/search.go')
-rw-r--r--vendor/github.com/emersion/go-imap/search.go371
1 files changed, 371 insertions, 0 deletions
diff --git a/vendor/github.com/emersion/go-imap/search.go b/vendor/github.com/emersion/go-imap/search.go
new file mode 100644
index 0000000..0ecb24d
--- /dev/null
+++ b/vendor/github.com/emersion/go-imap/search.go
@@ -0,0 +1,371 @@
+package imap
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "net/textproto"
+ "strings"
+ "time"
+)
+
+func maybeString(mystery interface{}) string {
+ if s, ok := mystery.(string); ok {
+ return s
+ }
+ return ""
+}
+
+func convertField(f interface{}, charsetReader func(io.Reader) io.Reader) string {
+ // An IMAP string contains only 7-bit data, no need to decode it
+ if s, ok := f.(string); ok {
+ return s
+ }
+
+ // If no charset is provided, getting directly the string is faster
+ if charsetReader == nil {
+ if stringer, ok := f.(fmt.Stringer); ok {
+ return stringer.String()
+ }
+ }
+
+ // Not a string, it must be a literal
+ l, ok := f.(Literal)
+ if !ok {
+ return ""
+ }
+
+ var r io.Reader = l
+ if charsetReader != nil {
+ if dec := charsetReader(r); dec != nil {
+ r = dec
+ }
+ }
+
+ b := make([]byte, l.Len())
+ if _, err := io.ReadFull(r, b); err != nil {
+ return ""
+ }
+ return string(b)
+}
+
+func popSearchField(fields []interface{}) (interface{}, []interface{}, error) {
+ if len(fields) == 0 {
+ return nil, nil, errors.New("imap: no enough fields for search key")
+ }
+ return fields[0], fields[1:], nil
+}
+
+// SearchCriteria is a search criteria. A message matches the criteria if and
+// only if it matches each one of its fields.
+type SearchCriteria struct {
+ SeqNum *SeqSet // Sequence number is in sequence set
+ Uid *SeqSet // UID is in sequence set
+
+ // Time and timezone are ignored
+ Since time.Time // Internal date is since this date
+ Before time.Time // Internal date is before this date
+ SentSince time.Time // Date header field is since this date
+ SentBefore time.Time // Date header field is before this date
+
+ Header textproto.MIMEHeader // Each header field value is present
+ Body []string // Each string is in the body
+ Text []string // Each string is in the text (header + body)
+
+ WithFlags []string // Each flag is present
+ WithoutFlags []string // Each flag is not present
+
+ Larger uint32 // Size is larger than this number
+ Smaller uint32 // Size is smaller than this number
+
+ Not []*SearchCriteria // Each criteria doesn't match
+ Or [][2]*SearchCriteria // Each criteria pair has at least one match of two
+}
+
+// NewSearchCriteria creates a new search criteria.
+func NewSearchCriteria() *SearchCriteria {
+ return &SearchCriteria{Header: make(textproto.MIMEHeader)}
+}
+
+func (c *SearchCriteria) parseField(fields []interface{}, charsetReader func(io.Reader) io.Reader) ([]interface{}, error) {
+ if len(fields) == 0 {
+ return nil, nil
+ }
+
+ f := fields[0]
+ fields = fields[1:]
+
+ if subfields, ok := f.([]interface{}); ok {
+ return fields, c.ParseWithCharset(subfields, charsetReader)
+ }
+
+ key, ok := f.(string)
+ if !ok {
+ return nil, fmt.Errorf("imap: invalid search criteria field type: %T", f)
+ }
+ key = strings.ToUpper(key)
+
+ var err error
+ switch key {
+ case "ALL":
+ // Nothing to do
+ case "ANSWERED", "DELETED", "DRAFT", "FLAGGED", "RECENT", "SEEN":
+ c.WithFlags = append(c.WithFlags, CanonicalFlag("\\"+key))
+ case "BCC", "CC", "FROM", "SUBJECT", "TO":
+ if f, fields, err = popSearchField(fields); err != nil {
+ return nil, err
+ }
+ if c.Header == nil {
+ c.Header = make(textproto.MIMEHeader)
+ }
+ c.Header.Add(key, convertField(f, charsetReader))
+ case "BEFORE":
+ if f, fields, err = popSearchField(fields); err != nil {
+ return nil, err
+ } else if t, err := time.Parse(DateLayout, maybeString(f)); err != nil {
+ return nil, err
+ } else if c.Before.IsZero() || t.Before(c.Before) {
+ c.Before = t
+ }
+ case "BODY":
+ if f, fields, err = popSearchField(fields); err != nil {
+ return nil, err
+ } else {
+ c.Body = append(c.Body, convertField(f, charsetReader))
+ }
+ case "HEADER":
+ var f1, f2 interface{}
+ if f1, fields, err = popSearchField(fields); err != nil {
+ return nil, err
+ } else if f2, fields, err = popSearchField(fields); err != nil {
+ return nil, err
+ } else {
+ if c.Header == nil {
+ c.Header = make(textproto.MIMEHeader)
+ }
+ c.Header.Add(maybeString(f1), convertField(f2, charsetReader))
+ }
+ case "KEYWORD":
+ if f, fields, err = popSearchField(fields); err != nil {
+ return nil, err
+ } else {
+ c.WithFlags = append(c.WithFlags, CanonicalFlag(maybeString(f)))
+ }
+ case "LARGER":
+ if f, fields, err = popSearchField(fields); err != nil {
+ return nil, err
+ } else if n, err := ParseNumber(f); err != nil {
+ return nil, err
+ } else if c.Larger == 0 || n > c.Larger {
+ c.Larger = n
+ }
+ case "NEW":
+ c.WithFlags = append(c.WithFlags, RecentFlag)
+ c.WithoutFlags = append(c.WithoutFlags, SeenFlag)
+ case "NOT":
+ not := new(SearchCriteria)
+ if fields, err = not.parseField(fields, charsetReader); err != nil {
+ return nil, err
+ }
+ c.Not = append(c.Not, not)
+ case "OLD":
+ c.WithoutFlags = append(c.WithoutFlags, RecentFlag)
+ case "ON":
+ if f, fields, err = popSearchField(fields); err != nil {
+ return nil, err
+ } else if t, err := time.Parse(DateLayout, maybeString(f)); err != nil {
+ return nil, err
+ } else {
+ c.Since = t
+ c.Before = t.Add(24 * time.Hour)
+ }
+ case "OR":
+ c1, c2 := new(SearchCriteria), new(SearchCriteria)
+ if fields, err = c1.parseField(fields, charsetReader); err != nil {
+ return nil, err
+ } else if fields, err = c2.parseField(fields, charsetReader); err != nil {
+ return nil, err
+ }
+ c.Or = append(c.Or, [2]*SearchCriteria{c1, c2})
+ case "SENTBEFORE":
+ if f, fields, err = popSearchField(fields); err != nil {
+ return nil, err
+ } else if t, err := time.Parse(DateLayout, maybeString(f)); err != nil {
+ return nil, err
+ } else if c.SentBefore.IsZero() || t.Before(c.SentBefore) {
+ c.SentBefore = t
+ }
+ case "SENTON":
+ if f, fields, err = popSearchField(fields); err != nil {
+ return nil, err
+ } else if t, err := time.Parse(DateLayout, maybeString(f)); err != nil {
+ return nil, err
+ } else {
+ c.SentSince = t
+ c.SentBefore = t.Add(24 * time.Hour)
+ }
+ case "SENTSINCE":
+ if f, fields, err = popSearchField(fields); err != nil {
+ return nil, err
+ } else if t, err := time.Parse(DateLayout, maybeString(f)); err != nil {
+ return nil, err
+ } else if c.SentSince.IsZero() || t.After(c.SentSince) {
+ c.SentSince = t
+ }
+ case "SINCE":
+ if f, fields, err = popSearchField(fields); err != nil {
+ return nil, err
+ } else if t, err := time.Parse(DateLayout, maybeString(f)); err != nil {
+ return nil, err
+ } else if c.Since.IsZero() || t.After(c.Since) {
+ c.Since = t
+ }
+ case "SMALLER":
+ if f, fields, err = popSearchField(fields); err != nil {
+ return nil, err
+ } else if n, err := ParseNumber(f); err != nil {
+ return nil, err
+ } else if c.Smaller == 0 || n < c.Smaller {
+ c.Smaller = n
+ }
+ case "TEXT":
+ if f, fields, err = popSearchField(fields); err != nil {
+ return nil, err
+ } else {
+ c.Text = append(c.Text, convertField(f, charsetReader))
+ }
+ case "UID":
+ if f, fields, err = popSearchField(fields); err != nil {
+ return nil, err
+ } else if c.Uid, err = ParseSeqSet(maybeString(f)); err != nil {
+ return nil, err
+ }
+ case "UNANSWERED", "UNDELETED", "UNDRAFT", "UNFLAGGED", "UNSEEN":
+ unflag := strings.TrimPrefix(key, "UN")
+ c.WithoutFlags = append(c.WithoutFlags, CanonicalFlag("\\"+unflag))
+ case "UNKEYWORD":
+ if f, fields, err = popSearchField(fields); err != nil {
+ return nil, err
+ } else {
+ c.WithoutFlags = append(c.WithoutFlags, CanonicalFlag(maybeString(f)))
+ }
+ default: // Try to parse a sequence set
+ if c.SeqNum, err = ParseSeqSet(key); err != nil {
+ return nil, err
+ }
+ }
+
+ return fields, nil
+}
+
+// ParseWithCharset parses a search criteria from the provided fields.
+// charsetReader is an optional function that converts from the fields charset
+// to UTF-8.
+func (c *SearchCriteria) ParseWithCharset(fields []interface{}, charsetReader func(io.Reader) io.Reader) error {
+ for len(fields) > 0 {
+ var err error
+ if fields, err = c.parseField(fields, charsetReader); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// Format formats search criteria to fields. UTF-8 is used.
+func (c *SearchCriteria) Format() []interface{} {
+ var fields []interface{}
+
+ if c.SeqNum != nil {
+ fields = append(fields, c.SeqNum)
+ }
+ if c.Uid != nil {
+ fields = append(fields, RawString("UID"), c.Uid)
+ }
+
+ if !c.Since.IsZero() && !c.Before.IsZero() && c.Before.Sub(c.Since) == 24*time.Hour {
+ fields = append(fields, RawString("ON"), searchDate(c.Since))
+ } else {
+ if !c.Since.IsZero() {
+ fields = append(fields, RawString("SINCE"), searchDate(c.Since))
+ }
+ if !c.Before.IsZero() {
+ fields = append(fields, RawString("BEFORE"), searchDate(c.Before))
+ }
+ }
+ if !c.SentSince.IsZero() && !c.SentBefore.IsZero() && c.SentBefore.Sub(c.SentSince) == 24*time.Hour {
+ fields = append(fields, RawString("SENTON"), searchDate(c.SentSince))
+ } else {
+ if !c.SentSince.IsZero() {
+ fields = append(fields, RawString("SENTSINCE"), searchDate(c.SentSince))
+ }
+ if !c.SentBefore.IsZero() {
+ fields = append(fields, RawString("SENTBEFORE"), searchDate(c.SentBefore))
+ }
+ }
+
+ for key, values := range c.Header {
+ var prefields []interface{}
+ switch key {
+ case "Bcc", "Cc", "From", "Subject", "To":
+ prefields = []interface{}{RawString(strings.ToUpper(key))}
+ default:
+ prefields = []interface{}{RawString("HEADER"), key}
+ }
+ for _, value := range values {
+ fields = append(fields, prefields...)
+ fields = append(fields, value)
+ }
+ }
+
+ for _, value := range c.Body {
+ fields = append(fields, RawString("BODY"), value)
+ }
+ for _, value := range c.Text {
+ fields = append(fields, RawString("TEXT"), value)
+ }
+
+ for _, flag := range c.WithFlags {
+ var subfields []interface{}
+ switch flag {
+ case AnsweredFlag, DeletedFlag, DraftFlag, FlaggedFlag, RecentFlag, SeenFlag:
+ subfields = []interface{}{RawString(strings.ToUpper(strings.TrimPrefix(flag, "\\")))}
+ default:
+ subfields = []interface{}{RawString("KEYWORD"), RawString(flag)}
+ }
+ fields = append(fields, subfields...)
+ }
+ for _, flag := range c.WithoutFlags {
+ var subfields []interface{}
+ switch flag {
+ case AnsweredFlag, DeletedFlag, DraftFlag, FlaggedFlag, SeenFlag:
+ subfields = []interface{}{RawString("UN" + strings.ToUpper(strings.TrimPrefix(flag, "\\")))}
+ case RecentFlag:
+ subfields = []interface{}{RawString("OLD")}
+ default:
+ subfields = []interface{}{RawString("UNKEYWORD"), RawString(flag)}
+ }
+ fields = append(fields, subfields...)
+ }
+
+ if c.Larger > 0 {
+ fields = append(fields, RawString("LARGER"), c.Larger)
+ }
+ if c.Smaller > 0 {
+ fields = append(fields, RawString("SMALLER"), c.Smaller)
+ }
+
+ for _, not := range c.Not {
+ fields = append(fields, RawString("NOT"), not.Format())
+ }
+
+ for _, or := range c.Or {
+ fields = append(fields, RawString("OR"), or[0].Format(), or[1].Format())
+ }
+
+ // Not a single criteria given, add ALL criteria as fallback
+ if len(fields) == 0 {
+ fields = append(fields, RawString("ALL"))
+ }
+
+ return fields
+}