Skip to content
Draft
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
6 changes: 3 additions & 3 deletions cmd/imapmemserver/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,9 @@ func main() {
NewSession: func(conn *imapserver.Conn) (imapserver.Session, *imapserver.GreetingData, error) {
return memServer.NewSession(), nil, nil
},
Caps: imap.CapSet{
imap.CapIMAP4rev1: {},
imap.CapIMAP4rev2: {},
Caps: &imapserver.SupportedCaps{
IMAP4rev1: true,
IMAP4rev2: true,
},
TLSConfig: tlsConfig,
InsecureAuth: insecureAuth,
Expand Down
6 changes: 3 additions & 3 deletions imapclient/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,9 @@ func newMemClientServerPair(t *testing.T) (net.Conn, io.Closer) {
Certificates: []tls.Certificate{cert},
},
InsecureAuth: true,
Caps: imap.CapSet{
imap.CapIMAP4rev1: {},
imap.CapIMAP4rev2: {},
Caps: &imapserver.SupportedCaps{
IMAP4rev1: true,
IMAP4rev2: true,
},
})

Expand Down
50 changes: 25 additions & 25 deletions imapserver/capability.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ func (c *Conn) availableCaps() []imap.Cap {
available := c.server.options.caps()

var caps []imap.Cap
addAvailableCaps(&caps, available, []imap.Cap{
imap.CapIMAP4rev2,
imap.CapIMAP4rev1,
addAvailableCaps(&caps, map[imap.Cap]bool{
imap.CapIMAP4rev2: available.IMAP4rev1,
imap.CapIMAP4rev1: available.IMAP4rev2,
})
if len(caps) == 0 {
panic("imapserver: must support at least IMAP4rev1 or IMAP4rev2")
}

if available.Has(imap.CapIMAP4rev1) {
if available.IMAP4rev1 {
caps = append(caps, []imap.Cap{
imap.CapSASLIR,
imap.CapLiteralMinus,
Expand All @@ -60,7 +60,7 @@ func (c *Conn) availableCaps() []imap.Cap {
caps = append(caps, imap.CapLoginDisabled)
}
if c.state == imap.ConnStateAuthenticated || c.state == imap.ConnStateSelected {
if available.Has(imap.CapIMAP4rev1) {
if available.IMAP4rev1 {
// IMAP4rev1-specific capabilities that don't require backend
// support and are not applicable to IMAP4rev2
caps = append(caps, []imap.Cap{
Expand All @@ -72,42 +72,42 @@ func (c *Conn) availableCaps() []imap.Cap {

// IMAP4rev1-specific capabilities which require backend support
// and are not applicable to IMAP4rev2
addAvailableCaps(&caps, available, []imap.Cap{
imap.CapNamespace,
imap.CapUIDPlus,
imap.CapESearch,
imap.CapSearchRes,
imap.CapListExtended,
imap.CapListStatus,
imap.CapMove,
imap.CapStatusSize,
imap.CapBinary,
imap.CapChildren,
addAvailableCaps(&caps, map[imap.Cap]bool{
imap.CapNamespace: available.Namespace,
imap.CapUIDPlus: available.UIDPlus,
imap.CapESearch: available.ESearch,
//imap.CapSearchRes: available.SearchRes,
imap.CapListExtended: available.ListExtended,
imap.CapListStatus: available.ListStatus,
imap.CapMove: available.Move,
imap.CapStatusSize: available.StatusSize,
//imap.CapBinary: available.Binary,
//imap.CapChildren: available.Children,
})
}

// Capabilities which require backend support and apply to both
// IMAP4rev1 and IMAP4rev2
addAvailableCaps(&caps, available, []imap.Cap{
imap.CapSpecialUse,
imap.CapCreateSpecialUse,
imap.CapLiteralPlus,
imap.CapUnauthenticate,
addAvailableCaps(&caps, map[imap.Cap]bool{
imap.CapSpecialUse: available.SpecialUse,
imap.CapCreateSpecialUse: available.CreateSpecialUse,
imap.CapLiteralPlus: available.LiteralPlus,
imap.CapUnauthenticate: available.Unauthenticate,
})

if appendLimitSession, ok := c.session.(SessionAppendLimit); ok {
limit := appendLimitSession.AppendLimit()
caps = append(caps, imap.Cap(fmt.Sprintf("APPENDLIMIT=%d", limit)))
} else {
addAvailableCaps(&caps, available, []imap.Cap{imap.CapAppendLimit})
addAvailableCaps(&caps, map[imap.Cap]bool{imap.CapAppendLimit: available.AppendLimit})
}
}
return caps
}

func addAvailableCaps(caps *[]imap.Cap, available imap.CapSet, l []imap.Cap) {
for _, c := range l {
if available.Has(c) {
func addAvailableCaps(caps *[]imap.Cap, available map[imap.Cap]bool) {
for c, ok := range available {
if ok {
*caps = append(*caps, c)
}
}
Expand Down
6 changes: 3 additions & 3 deletions imapserver/conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ func (c *Conn) serve() {
}
}()

caps := c.server.options.caps()
caps := c.server.options.caps().set()
if _, ok := c.session.(SessionIMAP4rev2); !ok && caps.Has(imap.CapIMAP4rev2) {
panic("imapserver: server advertises IMAP4rev2 but session doesn't support it")
}
Expand Down Expand Up @@ -401,7 +401,7 @@ func (c *Conn) checkBufferedLiteral(size int64, nonSync bool) error {
}

func (c *Conn) acceptLiteral(size int64, nonSync bool) error {
if nonSync && size > 4096 && !c.server.options.caps().Has(imap.CapLiteralPlus) {
if nonSync && size > 4096 && !c.server.options.caps().set().Has(imap.CapLiteralPlus) {
return &imap.Error{
Type: imap.StatusResponseTypeBad,
Text: "Non-synchronizing literals are limited to 4096 bytes",
Expand Down Expand Up @@ -595,7 +595,7 @@ func (w *UpdateWriter) WriteNumMessages(n uint32) error {

// WriteNumRecent writes an RECENT response (not used in IMAP4rev2, will be ignored).
func (w *UpdateWriter) WriteNumRecent(n uint32) error {
if w.conn.enabled.Has(imap.CapIMAP4rev2) || !w.conn.server.options.caps().Has(imap.CapIMAP4rev1) {
if w.conn.enabled.Has(imap.CapIMAP4rev2) || !w.conn.server.options.caps().set().Has(imap.CapIMAP4rev1) {
return nil
}
return w.conn.writeObsoleteRecent(n)
Expand Down
2 changes: 1 addition & 1 deletion imapserver/select.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func (c *Conn) handleSelect(tag string, dec *imapwire.Decoder, readOnly bool) er
if err := c.writeExists(data.NumMessages); err != nil {
return err
}
if !c.enabled.Has(imap.CapIMAP4rev2) && c.server.options.caps().Has(imap.CapIMAP4rev1) {
if !c.enabled.Has(imap.CapIMAP4rev2) && c.server.options.caps().set().Has(imap.CapIMAP4rev1) {
if err := c.writeObsoleteRecent(data.NumRecent); err != nil {
return err
}
Expand Down
77 changes: 62 additions & 15 deletions imapserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,64 @@ type Logger interface {
Printf(format string, args ...interface{})
}

// SupportedCaps describes capabilities supported by the server.
type SupportedCaps struct {
// IMAP protocol version
IMAP4rev1 bool // RFC 3501
IMAP4rev2 bool // RFC 9051

// Capabilities which are part of IMAP4rev2 and need to be explicitly
// enabled by IMAP4rev1-only servers
Namespace bool // RFC 2342
UIDPlus bool // RFC 4315
ESearch bool // RFC 4731
SearchRes bool // RFC 5182
ListExtended bool // RFC 5258
ListStatus bool // RFC 5819
Move bool // RFC 6851
StatusSize bool // RFC 8438
Binary bool // RFC 3516
Children bool // RFC 3348

// Capabilities which need to be explicitly enabled on both IMAP4rev1 and
// IMAP4rev2 servers
SpecialUse bool // RFC 6154
CreateSpecialUse bool // RFC 6154
LiteralPlus bool // RFC 7888
Unauthenticate bool // RFC 8437
AppendLimit bool // RFC 7889
}

func (caps *SupportedCaps) set() imap.CapSet {
m := map[imap.Cap]bool{
imap.CapIMAP4rev1: caps.IMAP4rev1,
imap.CapIMAP4rev2: caps.IMAP4rev2,
imap.CapNamespace: caps.Namespace,
imap.CapUIDPlus: caps.UIDPlus,
imap.CapESearch: caps.ESearch,
imap.CapSearchRes: caps.SearchRes,
imap.CapListExtended: caps.ListExtended,
imap.CapListStatus: caps.ListStatus,
imap.CapMove: caps.Move,
imap.CapStatusSize: caps.StatusSize,
imap.CapBinary: caps.Binary,
imap.CapChildren: caps.Children,
imap.CapSpecialUse: caps.SpecialUse,
imap.CapCreateSpecialUse: caps.CreateSpecialUse,
imap.CapLiteralPlus: caps.LiteralPlus,
imap.CapUnauthenticate: caps.Unauthenticate,
imap.CapAppendLimit: caps.AppendLimit,
}

set := make(imap.CapSet, len(m))
for name, ok := range m {
if ok {
set[name] = struct{}{}
}
}
return set
}

// Options contains server options.
//
// The only required field is NewSession.
Expand All @@ -29,18 +87,7 @@ type Options struct {
NewSession func(*Conn) (Session, *GreetingData, error)
// Supported capabilities. If nil, only IMAP4rev1 is advertised. This set
// must contain at least IMAP4rev1 or IMAP4rev2.
//
// The following capabilities are part of IMAP4rev2 and need to be
// explicitly enabled by IMAP4rev1-only servers:
//
// - NAMESPACE
// - UIDPLUS
// - ESEARCH
// - LIST-EXTENDED
// - LIST-STATUS
// - MOVE
// - STATUS=SIZE
Caps imap.CapSet
Caps *SupportedCaps
// Logger is a logger to print error messages. If nil, log.Default is used.
Logger Logger
// TLSConfig is a TLS configuration for STARTTLS. If nil, STARTTLS is
Expand Down Expand Up @@ -68,11 +115,11 @@ func (options *Options) wrapReadWriter(rw io.ReadWriter) io.ReadWriter {
}
}

func (options *Options) caps() imap.CapSet {
func (options *Options) caps() *SupportedCaps {
if options.Caps != nil {
return options.Caps
}
return imap.CapSet{imap.CapIMAP4rev1: {}}
return &SupportedCaps{IMAP4rev1: true}
}

// Server is an IMAP server.
Expand All @@ -89,7 +136,7 @@ type Server struct {

// New creates a new server.
func New(options *Options) *Server {
if caps := options.caps(); !caps.Has(imap.CapIMAP4rev2) && !caps.Has(imap.CapIMAP4rev1) {
if caps := options.caps().set(); !caps.Has(imap.CapIMAP4rev2) && !caps.Has(imap.CapIMAP4rev1) {
panic("imapserver: at least IMAP4rev1 must be supported")
}
return &Server{
Expand Down
2 changes: 1 addition & 1 deletion imapserver/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func (c *Conn) handleStatus(dec *imapwire.Decoder) error {
return dec.Err()
}

if options.NumRecent && !c.server.options.caps().Has(imap.CapIMAP4rev1) {
if options.NumRecent && !c.server.options.caps().set().Has(imap.CapIMAP4rev1) {
return &imap.Error{
Type: imap.StatusResponseTypeBad,
Text: "Unknown STATUS data item",
Expand Down