diff options
Diffstat (limited to 'vendor/github.com/emersion/go-imap/search.go')
-rw-r--r-- | vendor/github.com/emersion/go-imap/search.go | 371 |
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 +} |