Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ go 1.18
require (
github.com/emersion/go-message v0.18.1
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43
golang.org/x/text v0.14.0
)
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
Expand Down
9 changes: 9 additions & 0 deletions imapserver/capability.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,12 @@ func addAvailableCaps(caps *[]imap.Cap, available imap.CapSet, l []imap.Cap) {
}
}
}

func (c *Conn) availableCapsSet() imap.CapSet {
caps := c.availableCaps()
capSet := make(imap.CapSet)
for _, cap := range caps {
capSet[cap] = struct{}{}
}
return capSet
}
87 changes: 59 additions & 28 deletions imapserver/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/internal"
"github.com/emersion/go-imap/v2/internal/imapwire"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)

func (c *Conn) handleSearch(tag string, dec *imapwire.Decoder, numKind NumKind) error {
Expand Down Expand Up @@ -85,7 +87,22 @@ func (c *Conn) handleSearch(tag string, dec *imapwire.Decoder, numKind NumKind)
return err
}

if c.enabled.Has(imap.CapIMAP4rev2) || extended {
var supportsESEARCH bool
if capSession, ok := c.session.(SessionCapabilities); ok {
sessionCaps := capSession.GetCapabilities()
supportsESEARCH = sessionCaps.Has(imap.CapESearch) || sessionCaps.Has(imap.CapIMAP4rev2)
} else {
availableCaps := c.availableCaps()
for _, cap := range availableCaps {
if cap == imap.CapESearch || cap == imap.CapIMAP4rev2 {
supportsESEARCH = true
break
}
}
}

// Use ESEARCH format only if session supports it AND client used extended syntax
if supportsESEARCH && extended {
return c.writeESearch(tag, data, &options, numKind)
} else {
return c.writeSearch(data.All)
Expand All @@ -98,15 +115,17 @@ func (c *Conn) writeESearch(tag string, data *imap.SearchData, options *imap.Sea

enc.Atom("*").SP().Atom("ESEARCH")
if tag != "" {
enc.SP().Special('(').Atom("TAG").SP().Atom(tag).Special(')')
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)
// When there is no result, we need to send an ESEARCH response with no sequence set after ALL.
if options.ReturnAll {
enc.SP().Atom("ALL")
if data.All != nil && !isNumSetEmpty(data.All) {
enc.SP().NumSet(data.All)
}
}
if options.ReturnMin && data.Min > 0 {
enc.SP().Atom("MIN").SP().Number(data.Min)
Expand Down Expand Up @@ -136,24 +155,28 @@ func (c *Conn) writeSearch(numSet imap.NumSet) error {
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)

if numSet != nil {
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)
}
}
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 (dynamic set?)")
}
}
if !ok {
return fmt.Errorf("imapserver: failed to enumerate message numbers in SEARCH response")
}

return enc.CRLF()
}

Expand All @@ -178,7 +201,7 @@ func readSearchReturnOpts(dec *imapwire.Decoder, options *imap.SearchOptions) er
case "SAVE":
options.ReturnSave = true
default:
return newClientBugError("unknown SEARCH RETURN option")
// RFC 4731: A server MUST ignore any unrecognized return options.
}
return nil
})
Expand All @@ -196,7 +219,15 @@ func readSearchKey(criteria *imap.SearchCriteria, dec *imapwire.Decoder) error {
return readSearchKeyWithAtom(criteria, dec, key)
}
return dec.ExpectList(func() error {
return readSearchKey(criteria, dec)
for {
if err := readSearchKey(criteria, dec); err != nil {
return err
}
if !dec.SP() {
break
}
}
return nil
})
}

Expand Down Expand Up @@ -241,7 +272,7 @@ func readSearchKeyWithAtom(criteria *imap.SearchCriteria, dec *imapwire.Decoder,
return dec.Err()
}
criteria.Header = append(criteria.Header, imap.SearchCriteriaHeaderField{
Key: strings.Title(strings.ToLower(key)),
Key: cases.Title(language.English).String(strings.ToLower(key)),
Value: value,
})
case "HEADER":
Expand Down Expand Up @@ -308,7 +339,7 @@ func readSearchKeyWithAtom(criteria *imap.SearchCriteria, dec *imapwire.Decoder,
}
var not imap.SearchCriteria
if err := readSearchKey(&not, dec); err != nil {
return nil
return err
}
criteria.Not = append(criteria.Not, not)
case "OR":
Expand All @@ -317,13 +348,13 @@ func readSearchKeyWithAtom(criteria *imap.SearchCriteria, dec *imapwire.Decoder,
}
var or [2]imap.SearchCriteria
if err := readSearchKey(&or[0], dec); err != nil {
return nil
return err
}
if !dec.ExpectSP() {
return dec.Err()
}
if err := readSearchKey(&or[1], dec); err != nil {
return nil
return err
}
criteria.Or = append(criteria.Or, or)
case "$":
Expand All @@ -339,5 +370,5 @@ func readSearchKeyWithAtom(criteria *imap.SearchCriteria, dec *imapwire.Decoder,
}

func searchKeyFlag(key string) imap.Flag {
return imap.Flag("\\" + strings.Title(strings.ToLower(key)))
return imap.Flag("\\" + cases.Title(language.English).String(strings.ToLower(key)))
}
11 changes: 11 additions & 0 deletions imapserver/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,14 @@ type SessionAppendLimit interface {
// this server in an APPEND command.
AppendLimit() uint32
}

// SessionCapabilities is an IMAP session which can provide its current
// capabilities for capability filtering.
type SessionCapabilities interface {
Session

// GetCapabilities returns the session-specific capabilities.
// This allows sessions to filter capabilities based on client behavior
// or other session-specific factors.
GetCapabilities() imap.CapSet
}