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
12 changes: 10 additions & 2 deletions imapclient/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,16 @@ func readESearchResponse(dec *imapwire.Decoder) (tag string, data *imap.SearchDa
if isUID {
numKind = imapwire.NumKindUID
}
if !dec.ExpectNumSet(numKind, &data.All) {
return "", nil, dec.Err()
if dec.SP() {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grammar from RFC 4731 says that ALL must always be followed by a seq-set:

"ALL" SP sequence-set

Is a client not conforming?

if !dec.ExpectNumSet(numKind, &data.All) {
return "", nil, dec.Err()
}
} else {
if isUID {
data.All = imap.UIDSet{}
} else {
data.All = imap.SeqSet{}
}
}
if data.All.Dynamic() {
return "", nil, fmt.Errorf("imapclient: server returned a dynamic ALL number set in SEARCH response")
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
}
85 changes: 57 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 {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like this is the only place where SessionCapabilities is used? I would've expected it to affect everything, not just SEARCH.

I do think it would be nice to be able to customize supported caps per-conn, FWIW.

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,15 @@ 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)

if options.ReturnAll && data.All != nil && !isNumSetEmpty(data.All) {
enc.SP().Atom("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 +153,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 +199,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.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see where this is specified in RFC 4731? This would also be surprising to clients: they ask for something and silently get a reply without it.

RFC 4466 says that return options may be followed by optional search-mod-params, so we can't just skip the name anyways.

}
return nil
})
Expand All @@ -196,7 +217,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() {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why stop on non-spaces?

break
}
}
return nil
})
}

Expand Down Expand Up @@ -241,7 +270,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 +337,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 +346,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 +368,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)))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll never get anything other than ASCII here. I'd prefer not to introduce a new dependency with large Unicode tables just for this.

}
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
}