372 lines
10 KiB
Go
372 lines
10 KiB
Go
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
|
|
}
|