From 94524828a1ab60e0006221bdafaa796f53caf88d Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Thu, 30 Oct 2025 14:48:18 +0100 Subject: [PATCH 1/2] imapserver: add SupportedCaps --- cmd/imapmemserver/main.go | 6 +-- imapclient/client_test.go | 6 +-- imapserver/capability.go | 2 +- imapserver/conn.go | 6 +-- imapserver/select.go | 2 +- imapserver/server.go | 77 +++++++++++++++++++++++++++++++-------- imapserver/status.go | 2 +- 7 files changed, 74 insertions(+), 27 deletions(-) diff --git a/cmd/imapmemserver/main.go b/cmd/imapmemserver/main.go index 781e1c34..997e38a3 100644 --- a/cmd/imapmemserver/main.go +++ b/cmd/imapmemserver/main.go @@ -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, diff --git a/imapclient/client_test.go b/imapclient/client_test.go index 9e5c206f..0890647c 100644 --- a/imapclient/client_test.go +++ b/imapclient/client_test.go @@ -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, }, }) diff --git a/imapserver/capability.go b/imapserver/capability.go index 37da104b..56fe421d 100644 --- a/imapserver/capability.go +++ b/imapserver/capability.go @@ -28,7 +28,7 @@ func (c *Conn) handleCapability(dec *imapwire.Decoder) error { // Some extensions (e.g. SASL-IR, ENABLE) don't require backend support and // thus are always enabled. func (c *Conn) availableCaps() []imap.Cap { - available := c.server.options.caps() + available := c.server.options.caps().set() var caps []imap.Cap addAvailableCaps(&caps, available, []imap.Cap{ diff --git a/imapserver/conn.go b/imapserver/conn.go index 291f37ec..34024e41 100644 --- a/imapserver/conn.go +++ b/imapserver/conn.go @@ -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") } @@ -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", @@ -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) diff --git a/imapserver/select.go b/imapserver/select.go index 3535fe73..614c8251 100644 --- a/imapserver/select.go +++ b/imapserver/select.go @@ -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 } diff --git a/imapserver/server.go b/imapserver/server.go index fd6eff1f..462986e1 100644 --- a/imapserver/server.go +++ b/imapserver/server.go @@ -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. @@ -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 @@ -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. @@ -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{ diff --git a/imapserver/status.go b/imapserver/status.go index b2b5feb6..eba80fa9 100644 --- a/imapserver/status.go +++ b/imapserver/status.go @@ -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", From 4dbfef3c184a6adef2b894745bd880e64d5eb056 Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Thu, 30 Oct 2025 14:51:45 +0100 Subject: [PATCH 2/2] wip --- imapserver/capability.go | 52 ++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/imapserver/capability.go b/imapserver/capability.go index 56fe421d..3b72b743 100644 --- a/imapserver/capability.go +++ b/imapserver/capability.go @@ -28,18 +28,18 @@ func (c *Conn) handleCapability(dec *imapwire.Decoder) error { // Some extensions (e.g. SASL-IR, ENABLE) don't require backend support and // thus are always enabled. func (c *Conn) availableCaps() []imap.Cap { - available := c.server.options.caps().set() + 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, @@ -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{ @@ -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) } }