344 lines
8.3 KiB
Go
344 lines
8.3 KiB
Go
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)))
|
|
}
|