package imapserver import ( "fmt" "strings" "time" "github.com/emersion/go-imap/v2" "github.com/emersion/go-imap/v2/internal" "github.com/emersion/go-imap/v2/internal/imapwire" ) func (c *Conn) handleSearch(tag string, dec *imapwire.Decoder, numKind NumKind) error { if !dec.ExpectSP() { return dec.Err() } var ( atom string options imap.SearchOptions extended bool ) if maybeReadSearchKeyAtom(dec, &atom) && strings.EqualFold(atom, "RETURN") { if err := readSearchReturnOpts(dec, &options); err != nil { return fmt.Errorf("in search-return-opts: %w", err) } if !dec.ExpectSP() { return dec.Err() } extended = true atom = "" maybeReadSearchKeyAtom(dec, &atom) } if strings.EqualFold(atom, "CHARSET") { var charset string if !dec.ExpectSP() || !dec.ExpectAString(&charset) || !dec.ExpectSP() { return dec.Err() } switch strings.ToUpper(charset) { case "US-ASCII", "UTF-8": // nothing to do default: return &imap.Error{ Type: imap.StatusResponseTypeNo, Code: imap.ResponseCodeBadCharset, // TODO: return list of supported charsets Text: "Only US-ASCII and UTF-8 are supported SEARCH charsets", } } atom = "" maybeReadSearchKeyAtom(dec, &atom) } var criteria imap.SearchCriteria for { var err error if atom != "" { err = readSearchKeyWithAtom(&criteria, dec, atom) atom = "" } else { err = readSearchKey(&criteria, dec) } if err != nil { return fmt.Errorf("in search-key: %w", err) } if !dec.SP() { break } } if !dec.ExpectCRLF() { return dec.Err() } if err := c.checkState(imap.ConnStateSelected); err != nil { return err } // If no return option is specified, ALL is assumed if !options.ReturnMin && !options.ReturnMax && !options.ReturnAll && !options.ReturnCount { options.ReturnAll = true } data, err := c.session.Search(numKind, &criteria, &options) if err != nil { return err } if c.enabled.Has(imap.CapIMAP4rev2) || extended { return c.writeESearch(tag, data, &options, numKind) } else { return c.writeSearch(data.All) } } func (c *Conn) writeESearch(tag string, data *imap.SearchData, options *imap.SearchOptions, numKind NumKind) error { enc := newResponseEncoder(c) defer enc.end() enc.Atom("*").SP().Atom("ESEARCH") if tag != "" { enc.SP().Special('(').Atom("TAG").SP().String(tag).Special(')') } if numKind == NumKindUID { enc.SP().Atom("UID") } // When there is no result, we need to send an ESEARCH response with no ALL // keyword if options.ReturnAll && !isNumSetEmpty(data.All) { enc.SP().Atom("ALL").SP().NumSet(data.All) } if options.ReturnMin && data.Min > 0 { enc.SP().Atom("MIN").SP().Number(data.Min) } if options.ReturnMax && data.Max > 0 { enc.SP().Atom("MAX").SP().Number(data.Max) } if options.ReturnCount { enc.SP().Atom("COUNT").SP().Number(data.Count) } return enc.CRLF() } func isNumSetEmpty(numSet imap.NumSet) bool { switch numSet := numSet.(type) { case imap.SeqSet: return len(numSet) == 0 case imap.UIDSet: return len(numSet) == 0 default: panic("unknown imap.NumSet type") } } func (c *Conn) writeSearch(numSet imap.NumSet) error { enc := newResponseEncoder(c) defer enc.end() enc.Atom("*").SP().Atom("SEARCH") var ok bool switch numSet := numSet.(type) { case imap.SeqSet: var nums []uint32 nums, ok = numSet.Nums() for _, num := range nums { enc.SP().Number(num) } case imap.UIDSet: var uids []imap.UID uids, ok = numSet.Nums() for _, uid := range uids { enc.SP().UID(uid) } } if !ok { return fmt.Errorf("imapserver: failed to enumerate message numbers in SEARCH response") } return enc.CRLF() } func readSearchReturnOpts(dec *imapwire.Decoder, options *imap.SearchOptions) error { if !dec.ExpectSP() { return dec.Err() } return dec.ExpectList(func() error { var name string if !dec.ExpectAtom(&name) { return dec.Err() } switch strings.ToUpper(name) { case "MIN": options.ReturnMin = true case "MAX": options.ReturnMax = true case "ALL": options.ReturnAll = true case "COUNT": options.ReturnCount = true case "SAVE": options.ReturnSave = true default: return newClientBugError("unknown SEARCH RETURN option") } return nil }) } func maybeReadSearchKeyAtom(dec *imapwire.Decoder, ptr *string) bool { return dec.Func(ptr, func(ch byte) bool { return ch == '*' || imapwire.IsAtomChar(ch) }) } func readSearchKey(criteria *imap.SearchCriteria, dec *imapwire.Decoder) error { var key string if maybeReadSearchKeyAtom(dec, &key) { return readSearchKeyWithAtom(criteria, dec, key) } return dec.ExpectList(func() error { return readSearchKey(criteria, dec) }) } func readSearchKeyWithAtom(criteria *imap.SearchCriteria, dec *imapwire.Decoder, key string) error { key = strings.ToUpper(key) switch key { case "ALL": // nothing to do case "UID": var uidSet imap.UIDSet if !dec.ExpectSP() || !dec.ExpectUIDSet(&uidSet) { return dec.Err() } criteria.UID = append(criteria.UID, uidSet) case "ANSWERED", "DELETED", "DRAFT", "FLAGGED", "RECENT", "SEEN": criteria.Flag = append(criteria.Flag, searchKeyFlag(key)) case "UNANSWERED", "UNDELETED", "UNDRAFT", "UNFLAGGED", "UNSEEN": notKey := strings.TrimPrefix(key, "UN") criteria.NotFlag = append(criteria.NotFlag, searchKeyFlag(notKey)) case "NEW": criteria.Flag = append(criteria.Flag, internal.FlagRecent) criteria.NotFlag = append(criteria.NotFlag, imap.FlagSeen) case "OLD": criteria.NotFlag = append(criteria.NotFlag, internal.FlagRecent) case "KEYWORD", "UNKEYWORD": if !dec.ExpectSP() { return dec.Err() } flag, err := internal.ExpectFlag(dec) if err != nil { return err } switch key { case "KEYWORD": criteria.Flag = append(criteria.Flag, flag) case "UNKEYWORD": criteria.NotFlag = append(criteria.NotFlag, flag) } case "BCC", "CC", "FROM", "SUBJECT", "TO": var value string if !dec.ExpectSP() || !dec.ExpectAString(&value) { return dec.Err() } criteria.Header = append(criteria.Header, imap.SearchCriteriaHeaderField{ Key: strings.Title(strings.ToLower(key)), Value: value, }) case "HEADER": var key, value string if !dec.ExpectSP() || !dec.ExpectAString(&key) || !dec.ExpectSP() || !dec.ExpectAString(&value) { return dec.Err() } criteria.Header = append(criteria.Header, imap.SearchCriteriaHeaderField{ Key: key, Value: value, }) case "SINCE", "BEFORE", "ON", "SENTSINCE", "SENTBEFORE", "SENTON": if !dec.ExpectSP() { return dec.Err() } t, err := internal.ExpectDate(dec) if err != nil { return err } var dateCriteria imap.SearchCriteria switch key { case "SINCE": dateCriteria.Since = t case "BEFORE": dateCriteria.Before = t case "ON": dateCriteria.Since = t dateCriteria.Before = t.Add(24 * time.Hour) case "SENTSINCE": dateCriteria.SentSince = t case "SENTBEFORE": dateCriteria.SentBefore = t case "SENTON": dateCriteria.SentSince = t dateCriteria.SentBefore = t.Add(24 * time.Hour) } criteria.And(&dateCriteria) case "BODY": var body string if !dec.ExpectSP() || !dec.ExpectAString(&body) { return dec.Err() } criteria.Body = append(criteria.Body, body) case "TEXT": var text string if !dec.ExpectSP() || !dec.ExpectAString(&text) { return dec.Err() } criteria.Text = append(criteria.Text, text) case "LARGER", "SMALLER": var n int64 if !dec.ExpectSP() || !dec.ExpectNumber64(&n) { return dec.Err() } switch key { case "LARGER": criteria.And(&imap.SearchCriteria{Larger: n}) case "SMALLER": criteria.And(&imap.SearchCriteria{Smaller: n}) } case "NOT": if !dec.ExpectSP() { return dec.Err() } var not imap.SearchCriteria if err := readSearchKey(¬, dec); err != nil { return err } criteria.Not = append(criteria.Not, not) case "OR": if !dec.ExpectSP() { return dec.Err() } var or [2]imap.SearchCriteria if err := readSearchKey(&or[0], dec); err != nil { return err } if !dec.ExpectSP() { return dec.Err() } if err := readSearchKey(&or[1], dec); err != nil { return err } criteria.Or = append(criteria.Or, or) case "$": criteria.UID = append(criteria.UID, imap.SearchRes()) default: seqSet, err := imapwire.ParseSeqSet(key) if err != nil { return err } criteria.SeqNum = append(criteria.SeqNum, seqSet) } return nil } func searchKeyFlag(key string) imap.Flag { return imap.Flag("\\" + strings.Title(strings.ToLower(key))) }