From 7e8c5a19ea5efeae960e24a38091e67b6e76638f Mon Sep 17 00:00:00 2001 From: Dejan Strbac Date: Sat, 7 Jun 2025 01:09:37 +0200 Subject: [PATCH 1/6] add independent capabilities --- imapserver/capability.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/imapserver/capability.go b/imapserver/capability.go index b3e7c99b..ebed9264 100644 --- a/imapserver/capability.go +++ b/imapserver/capability.go @@ -75,6 +75,8 @@ func (c *Conn) availableCaps() []imap.Cap { imap.CapMove, imap.CapStatusSize, imap.CapBinary, + imap.CapChildren, + imap.CapID, }) } addAvailableCaps(&caps, available, []imap.Cap{ From 6c983fab58882f768e4d314444507eb59a610b3b Mon Sep 17 00:00:00 2001 From: Dejan Strbac Date: Sat, 7 Jun 2025 17:34:33 +0200 Subject: [PATCH 2/6] capability ID required separate parsing, adding it now --- imapserver/conn.go | 3 + imapserver/id.go | 154 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 imapserver/id.go diff --git a/imapserver/conn.go b/imapserver/conn.go index 291f37ec..637e62f9 100644 --- a/imapserver/conn.go +++ b/imapserver/conn.go @@ -220,6 +220,9 @@ func (c *Conn) readCommand(dec *imapwire.Decoder) error { err = c.handleLogout(dec) case "CAPABILITY": err = c.handleCapability(dec) + case "ID": + err = c.handleID(tag, dec) + sendOK = false case "STARTTLS": err = c.handleStartTLS(tag, dec) sendOK = false diff --git a/imapserver/id.go b/imapserver/id.go new file mode 100644 index 00000000..3fe5d41f --- /dev/null +++ b/imapserver/id.go @@ -0,0 +1,154 @@ +package imapserver + +import ( + "fmt" + + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/internal/imapwire" +) + +func (c *Conn) handleID(tag string, dec *imapwire.Decoder) error { + idData, err := readID(dec) + if err != nil { + return fmt.Errorf("in id: %v", err) + } + + if !dec.ExpectCRLF() { + return dec.Err() + } + + var serverIDData *imap.IDData + if idSess, ok := c.session.(SessionID); ok { + serverIDData = idSess.ID(idData) + } + + enc := newResponseEncoder(c) + defer enc.end() + enc.Atom("*").SP().Atom("ID") + + if serverIDData == nil { + enc.SP().NIL() + } else { + enc.SP().Special('(') + isFirstKey := true + if serverIDData.Name != "" { + addIDKeyValue(enc.Encoder, &isFirstKey, "name", serverIDData.Name) + } + if serverIDData.Version != "" { + addIDKeyValue(enc.Encoder, &isFirstKey, "version", serverIDData.Version) + } + if serverIDData.OS != "" { + addIDKeyValue(enc.Encoder, &isFirstKey, "os", serverIDData.OS) + } + if serverIDData.OSVersion != "" { + addIDKeyValue(enc.Encoder, &isFirstKey, "os-version", serverIDData.OSVersion) + } + if serverIDData.Vendor != "" { + addIDKeyValue(enc.Encoder, &isFirstKey, "vendor", serverIDData.Vendor) + } + if serverIDData.SupportURL != "" { + addIDKeyValue(enc.Encoder, &isFirstKey, "support-url", serverIDData.SupportURL) + } + if serverIDData.Address != "" { + addIDKeyValue(enc.Encoder, &isFirstKey, "address", serverIDData.Address) + } + if serverIDData.Date != "" { + addIDKeyValue(enc.Encoder, &isFirstKey, "date", serverIDData.Date) + } + if serverIDData.Command != "" { + addIDKeyValue(enc.Encoder, &isFirstKey, "command", serverIDData.Command) + } + if serverIDData.Arguments != "" { + addIDKeyValue(enc.Encoder, &isFirstKey, "arguments", serverIDData.Arguments) + } + if serverIDData.Environment != "" { + addIDKeyValue(enc.Encoder, &isFirstKey, "environment", serverIDData.Environment) + } + enc.Special(')') + } + + if err := enc.CRLF(); err != nil { + return err + } + + return c.writeStatusResp(tag, &imap.StatusResponse{ + Type: imap.StatusResponseTypeOK, + Text: "ID completed", + }) +} + +func readID(dec *imapwire.Decoder) (*imap.IDData, error) { + if !dec.ExpectSP() { + return nil, dec.Err() + } + + if dec.ExpectNIL() { + return nil, nil + } + + data := &imap.IDData{} + currKey := "" + err := dec.ExpectList(func() error { + var keyOrValue string + if !dec.String(&keyOrValue) { + return fmt.Errorf("in id key-val list: %v", dec.Err()) + } + + if currKey == "" { + currKey = keyOrValue + return nil + } + + switch currKey { + case "name": + data.Name = keyOrValue + case "version": + data.Version = keyOrValue + case "os": + data.OS = keyOrValue + case "os-version": + data.OSVersion = keyOrValue + case "vendor": + data.Vendor = keyOrValue + case "support-url": + data.SupportURL = keyOrValue + case "address": + data.Address = keyOrValue + case "date": + data.Date = keyOrValue + case "command": + data.Command = keyOrValue + case "arguments": + data.Arguments = keyOrValue + case "environment": + data.Environment = keyOrValue + default: + // Ignore unknown key + } + currKey = "" + + return nil + }) + + if err != nil { + return nil, err + } + + return data, nil +} + +func addIDKeyValue(enc *imapwire.Encoder, isFirstKey *bool, key, value string) { + if *isFirstKey { + enc.Quoted(key).SP().Quoted(value) + } else { + enc.SP().Quoted(key).SP().Quoted(value) + } + *isFirstKey = false +} + +// SessionID is an interface for sessions that can provide server ID information. +type SessionID interface { + // ID returns server information in response to a client ID command. + // The client's ID information is provided if available. + ID(clientID *imap.IDData) *imap.IDData +} From 025cfbc11a171728d97dd194651195ebf9859bb2 Mon Sep 17 00:00:00 2001 From: Dejan Strbac Date: Sun, 8 Jun 2025 19:01:10 +0200 Subject: [PATCH 3/6] fix: ensure response encoder is properly finalized in handleID --- imapserver/id.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/imapserver/id.go b/imapserver/id.go index 3fe5d41f..c130209a 100644 --- a/imapserver/id.go +++ b/imapserver/id.go @@ -23,7 +23,6 @@ func (c *Conn) handleID(tag string, dec *imapwire.Decoder) error { } enc := newResponseEncoder(c) - defer enc.end() enc.Atom("*").SP().Atom("ID") if serverIDData == nil { @@ -67,7 +66,9 @@ func (c *Conn) handleID(tag string, dec *imapwire.Decoder) error { enc.Special(')') } - if err := enc.CRLF(); err != nil { + err = enc.CRLF() + enc.end() + if err != nil { return err } From 94bb68afd4e505b5b4ed54a098352b637c2a2fb6 Mon Sep 17 00:00:00 2001 From: Dejan Strbac Date: Mon, 9 Jun 2025 16:59:01 +0200 Subject: [PATCH 4/6] moving CapChildren to own PR --- imapserver/capability.go | 1 - 1 file changed, 1 deletion(-) diff --git a/imapserver/capability.go b/imapserver/capability.go index 204aafb7..39a9dd3a 100644 --- a/imapserver/capability.go +++ b/imapserver/capability.go @@ -80,7 +80,6 @@ func (c *Conn) availableCaps() []imap.Cap { imap.CapMove, imap.CapStatusSize, imap.CapBinary, - imap.CapChildren, imap.CapID, }) } From bb05d235e806b2c6a8e4c98a05e248cc8ee1df00 Mon Sep 17 00:00:00 2001 From: Dejan Strbac Date: Wed, 11 Jun 2025 21:34:32 +0200 Subject: [PATCH 5/6] mkae CapID applicable to both rev1 and rev2 --- imapserver/capability.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imapserver/capability.go b/imapserver/capability.go index d592a48a..7c3306f1 100644 --- a/imapserver/capability.go +++ b/imapserver/capability.go @@ -80,7 +80,6 @@ func (c *Conn) availableCaps() []imap.Cap { imap.CapMove, imap.CapStatusSize, imap.CapBinary, - imap.CapID, }) } @@ -91,6 +90,7 @@ func (c *Conn) availableCaps() []imap.Cap { imap.CapCreateSpecialUse, imap.CapLiteralPlus, imap.CapUnauthenticate, + imap.CapID, }) } return caps From 0e418b8fbdb4f590a3749d667fd943988e39bf79 Mon Sep 17 00:00:00 2001 From: Dejan Strbac Date: Sun, 21 Sep 2025 11:05:40 +0200 Subject: [PATCH 6/6] Extend ID command on both client and server for ID forwarding in Dovecot --- id.go | 4 ++++ imapclient/id.go | 23 +++++++++++++++++++++-- imapserver/id.go | 24 +++++++++++++++++++++--- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/id.go b/id.go index de7ca0e1..eb53e0f5 100644 --- a/id.go +++ b/id.go @@ -12,4 +12,8 @@ type IDData struct { Command string Arguments string Environment string + + // Raw contains all raw key-value pairs. Standard keys are also present + // in this map. Keys are case-insensitive and are normalized to lowercase. + Raw map[string]string } diff --git a/imapclient/id.go b/imapclient/id.go index 0c10d605..66f75367 100644 --- a/imapclient/id.go +++ b/imapclient/id.go @@ -2,6 +2,7 @@ package imapclient import ( "fmt" + "strings" "github.com/emersion/go-imap/v2" "github.com/emersion/go-imap/v2/internal/imapwire" @@ -60,6 +61,18 @@ func (c *Client) ID(idData *imap.IDData) *IDCommand { if idData.Environment != "" { addIDKeyValue(enc, &isFirstKey, "environment", idData.Environment) } + if idData.Raw != nil { + stdKeys := map[string]struct{}{ + "name": {}, "version": {}, "os": {}, "os-version": {}, "vendor": {}, + "support-url": {}, "address": {}, "date": {}, "command": {}, + "arguments": {}, "environment": {}, + } + for k, v := range idData.Raw { + if _, ok := stdKeys[strings.ToLower(k)]; !ok { + addIDKeyValue(enc, &isFirstKey, k, v) + } + } + } enc.Special(')') enc.end() @@ -91,7 +104,9 @@ func (c *Client) handleID() error { } func (c *Client) readID(dec *imapwire.Decoder) (*imap.IDData, error) { - var data = imap.IDData{} + var data = imap.IDData{ + Raw: make(map[string]string), + } if !dec.ExpectSP() { return nil, dec.Err() @@ -113,7 +128,10 @@ func (c *Client) readID(dec *imapwire.Decoder) (*imap.IDData, error) { return nil } - switch currKey { + lowerKey := strings.ToLower(currKey) + data.Raw[lowerKey] = keyOrValue + + switch lowerKey { case "name": data.Name = keyOrValue case "version": @@ -138,6 +156,7 @@ func (c *Client) readID(dec *imapwire.Decoder) (*imap.IDData, error) { data.Environment = keyOrValue default: // Ignore unknown key + // Unknown key is already stored in Raw // Yahoo server sends "host" and "remote-host" keys // which are not defined in RFC 2971 } diff --git a/imapserver/id.go b/imapserver/id.go index c130209a..5997b774 100644 --- a/imapserver/id.go +++ b/imapserver/id.go @@ -2,6 +2,7 @@ package imapserver import ( "fmt" + "strings" "github.com/emersion/go-imap/v2" "github.com/emersion/go-imap/v2/internal/imapwire" @@ -63,6 +64,18 @@ func (c *Conn) handleID(tag string, dec *imapwire.Decoder) error { if serverIDData.Environment != "" { addIDKeyValue(enc.Encoder, &isFirstKey, "environment", serverIDData.Environment) } + if serverIDData.Raw != nil { + stdKeys := map[string]struct{}{ + "name": {}, "version": {}, "os": {}, "os-version": {}, "vendor": {}, + "support-url": {}, "address": {}, "date": {}, "command": {}, + "arguments": {}, "environment": {}, + } + for k, v := range serverIDData.Raw { + if _, ok := stdKeys[strings.ToLower(k)]; !ok { + addIDKeyValue(enc.Encoder, &isFirstKey, k, v) + } + } + } enc.Special(')') } @@ -87,7 +100,9 @@ func readID(dec *imapwire.Decoder) (*imap.IDData, error) { return nil, nil } - data := &imap.IDData{} + data := &imap.IDData{ + Raw: make(map[string]string), + } currKey := "" err := dec.ExpectList(func() error { var keyOrValue string @@ -100,7 +115,10 @@ func readID(dec *imapwire.Decoder) (*imap.IDData, error) { return nil } - switch currKey { + lowerKey := strings.ToLower(currKey) + data.Raw[lowerKey] = keyOrValue + + switch lowerKey { case "name": data.Name = keyOrValue case "version": @@ -124,7 +142,7 @@ func readID(dec *imapwire.Decoder) (*imap.IDData, error) { case "environment": data.Environment = keyOrValue default: - // Ignore unknown key + // Unknown key, already stored in Raw } currKey = ""