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/capability.go b/imapserver/capability.go index 37da104b..a9b0657f 100644 --- a/imapserver/capability.go +++ b/imapserver/capability.go @@ -93,6 +93,7 @@ func (c *Conn) availableCaps() []imap.Cap { imap.CapCreateSpecialUse, imap.CapLiteralPlus, imap.CapUnauthenticate, + imap.CapID, }) if appendLimitSession, ok := c.session.(SessionAppendLimit); ok { 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..5997b774 --- /dev/null +++ b/imapserver/id.go @@ -0,0 +1,173 @@ +package imapserver + +import ( + "fmt" + "strings" + + "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) + 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) + } + 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(')') + } + + err = enc.CRLF() + enc.end() + if 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{ + Raw: make(map[string]string), + } + 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 + } + + lowerKey := strings.ToLower(currKey) + data.Raw[lowerKey] = keyOrValue + + switch lowerKey { + 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: + // Unknown key, already stored in Raw + } + 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 +}