From 0ba2faa4b5eca96e98cfea0141187e2bbf731dc5 Mon Sep 17 00:00:00 2001 From: Dejan Strbac Date: Fri, 6 Jun 2025 15:52:56 +0200 Subject: [PATCH 1/3] imapserver: advertise APPENDLIMIT capability --- cmd/imapmemserver/main.go | 5 +++-- imapserver/capability.go | 10 ++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/cmd/imapmemserver/main.go b/cmd/imapmemserver/main.go index 91423a64..ebd15d4c 100644 --- a/cmd/imapmemserver/main.go +++ b/cmd/imapmemserver/main.go @@ -68,8 +68,9 @@ func main() { return memServer.NewSession(), nil, nil }, Caps: imap.CapSet{ - imap.CapIMAP4rev1: {}, - imap.CapIMAP4rev2: {}, + imap.CapIMAP4rev1: {}, + imap.CapIMAP4rev2: {}, + imap.Cap("APPENDLIMIT=104857600"): {}, }, TLSConfig: tlsConfig, InsecureAuth: insecureAuth, diff --git a/imapserver/capability.go b/imapserver/capability.go index 4fc43d6a..87553a64 100644 --- a/imapserver/capability.go +++ b/imapserver/capability.go @@ -1,6 +1,8 @@ package imapserver import ( + "fmt" + "github.com/emersion/go-imap/v2" "github.com/emersion/go-imap/v2/internal/imapwire" ) @@ -85,6 +87,14 @@ func (c *Conn) availableCaps() []imap.Cap { // Capabilities which require backend support and apply to both // IMAP4rev1 and IMAP4rev2 + if limit, ok := available.AppendLimit(); ok { + if limit != nil { + caps = append(caps, imap.Cap(fmt.Sprintf("APPENDLIMIT=%d", *limit))) + } else { + caps = append(caps, imap.CapAppendLimit) + } + } + addAvailableCaps(&caps, available, []imap.Cap{ imap.CapCreateSpecialUse, imap.CapLiteralPlus, From b85b1c9a02bbb199986cd00f093edb59a3acf940 Mon Sep 17 00:00:00 2001 From: Dejan Strbac Date: Mon, 9 Jun 2025 16:55:51 +0200 Subject: [PATCH 2/3] move appendlimit to sessionAppendLimit --- cmd/imapmemserver/main.go | 5 ++-- imapserver/capability.go | 9 ++++++- imapserver/imapmemserver/server.go | 19 ++++++++++++++ imapserver/imapmemserver/session.go | 40 ++++++++++++++++++++++++++++- imapserver/session.go | 15 +++++++++++ imapserver/status.go | 8 ++++++ 6 files changed, 91 insertions(+), 5 deletions(-) diff --git a/cmd/imapmemserver/main.go b/cmd/imapmemserver/main.go index ebd15d4c..91423a64 100644 --- a/cmd/imapmemserver/main.go +++ b/cmd/imapmemserver/main.go @@ -68,9 +68,8 @@ func main() { return memServer.NewSession(), nil, nil }, Caps: imap.CapSet{ - imap.CapIMAP4rev1: {}, - imap.CapIMAP4rev2: {}, - imap.Cap("APPENDLIMIT=104857600"): {}, + imap.CapIMAP4rev1: {}, + imap.CapIMAP4rev2: {}, }, TLSConfig: tlsConfig, InsecureAuth: insecureAuth, diff --git a/imapserver/capability.go b/imapserver/capability.go index 87553a64..da8964d8 100644 --- a/imapserver/capability.go +++ b/imapserver/capability.go @@ -87,7 +87,14 @@ func (c *Conn) availableCaps() []imap.Cap { // Capabilities which require backend support and apply to both // IMAP4rev1 and IMAP4rev2 - if limit, ok := available.AppendLimit(); ok { + if appendLimitSession, ok := c.session.(SessionAppendLimit); ok { + if appendLimitSession.DiscloseLimit() { + limit := appendLimitSession.AppendLimit() + caps = append(caps, imap.Cap(fmt.Sprintf("APPENDLIMIT=%d", limit))) + } else { + caps = append(caps, imap.CapAppendLimit) + } + } else if limit, ok := available.AppendLimit(); ok { if limit != nil { caps = append(caps, imap.Cap(fmt.Sprintf("APPENDLIMIT=%d", *limit))) } else { diff --git a/imapserver/imapmemserver/server.go b/imapserver/imapmemserver/server.go index e31453a3..df655910 100644 --- a/imapserver/imapmemserver/server.go +++ b/imapserver/imapmemserver/server.go @@ -47,6 +47,25 @@ type serverSession struct { } var _ imapserver.Session = (*serverSession)(nil) +var _ imapserver.SessionAppendLimit = (*serverSession)(nil) + +// AppendLimit implements the SessionAppendLimit interface. +func (sess *serverSession) AppendLimit() uint32 { + if sess.UserSession != nil { + return sess.UserSession.AppendLimit() + } + // Default value for unauthenticated sessions + return 104857600 // 100 MiB +} + +// DiscloseLimit implements the SessionAppendLimit interface. +func (sess *serverSession) DiscloseLimit() bool { + if sess.UserSession != nil { + return sess.UserSession.DiscloseLimit() + } + // Default for unauthenticated sessions - true means we show the limit + return true +} func (sess *serverSession) Login(username, password string) error { u := sess.server.user(username) diff --git a/imapserver/imapmemserver/session.go b/imapserver/imapmemserver/session.go index 70e9d2f8..1ec32402 100644 --- a/imapserver/imapmemserver/session.go +++ b/imapserver/imapmemserver/session.go @@ -17,13 +17,35 @@ type ( type UserSession struct { *user // immutable *mailbox // may be nil + + // appendLimit is the maximum size in bytes that can be uploaded to this server + // in an APPEND command + appendLimit uint32 + + // discloseLimit indicates whether the append limit should be advertised in the + // CAPABILITY response + discloseLimit bool } var _ imapserver.SessionIMAP4rev2 = (*UserSession)(nil) +var _ imapserver.SessionAppendLimit = (*UserSession)(nil) // NewUserSession creates a new user session. func NewUserSession(user *User) *UserSession { - return &UserSession{user: user} + return &UserSession{ + user: user, + appendLimit: 104857600, // 100 MiB default + discloseLimit: true, // By default, disclose the limit in CAPABILITY + } +} + +// NewUserSessionWithAppendLimit creates a new user session with a custom append limit. +func NewUserSessionWithAppendLimit(user *User, appendLimit uint32, discloseLimit bool) *UserSession { + return &UserSession{ + user: user, + appendLimit: appendLimit, + discloseLimit: discloseLimit, + } } func (sess *UserSession) Close() error { @@ -138,3 +160,19 @@ func (sess *UserSession) Idle(w *imapserver.UpdateWriter, stop <-chan struct{}) } return sess.mailbox.Idle(w, stop) } + +// AppendLimit implements the SessionAppendLimit interface. +// It returns the maximum size in bytes that can be uploaded to this server in an APPEND command. +func (sess *UserSession) AppendLimit() uint32 { + // If appendLimit is not set (0), return a default large value + if sess.appendLimit == 0 { + return 104857600 // 100 MiB default + } + return sess.appendLimit +} + +// DiscloseLimit implements the SessionAppendLimit interface. +// It indicates whether the append limit should be advertised in the CAPABILITY response. +func (sess *UserSession) DiscloseLimit() bool { + return sess.discloseLimit +} diff --git a/imapserver/session.go b/imapserver/session.go index eb774382..4f4a3ddb 100644 --- a/imapserver/session.go +++ b/imapserver/session.go @@ -114,3 +114,18 @@ type SessionUnauthenticate interface { // Authenticated state Unauthenticate() error } + +// SessionAppendLimit is an IMAP session which has the same APPEND limit for +// all mailboxes. +type SessionAppendLimit interface { + Session + + // AppendLimit returns the maximum size in bytes that can be uploaded to this server + // in an APPEND command. + AppendLimit() uint32 + + // DiscloseLimit indicates whether the limit should be advertised in the CAPABILITY + // response. If false, only "APPENDLIMIT" will be listed, without the actual limit. + // If true, "APPENDLIMIT=" will be listed. + DiscloseLimit() bool +} diff --git a/imapserver/status.go b/imapserver/status.go index b2b5feb6..db502823 100644 --- a/imapserver/status.go +++ b/imapserver/status.go @@ -45,6 +45,14 @@ func (c *Conn) handleStatus(dec *imapwire.Decoder) error { return err } + // Check if AppendLimit was requested and the session supports it + if options.AppendLimit && data.AppendLimit == nil { + if appendLimitSession, ok := c.session.(SessionAppendLimit); ok { + limit := appendLimitSession.AppendLimit() + data.AppendLimit = &limit + } + } + return c.writeStatus(data, &options) } From f615554563feb3c677edd0265bd55261aeb4d8f2 Mon Sep 17 00:00:00 2001 From: Dejan Strbac Date: Mon, 9 Jun 2025 19:36:05 +0200 Subject: [PATCH 3/3] imapserver: remove redundant AppendLimit check in handleStatus --- imapserver/status.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/imapserver/status.go b/imapserver/status.go index db502823..b2b5feb6 100644 --- a/imapserver/status.go +++ b/imapserver/status.go @@ -45,14 +45,6 @@ func (c *Conn) handleStatus(dec *imapwire.Decoder) error { return err } - // Check if AppendLimit was requested and the session supports it - if options.AppendLimit && data.AppendLimit == nil { - if appendLimitSession, ok := c.session.(SessionAppendLimit); ok { - limit := appendLimitSession.AppendLimit() - data.AppendLimit = &limit - } - } - return c.writeStatus(data, &options) }