Skip to content
Open
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
4 changes: 4 additions & 0 deletions id.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
23 changes: 21 additions & 2 deletions imapclient/id.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package imapclient

import (
"fmt"
"strings"

"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/internal/imapwire"
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand All @@ -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":
Expand All @@ -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
}
Expand Down
1 change: 1 addition & 0 deletions imapserver/capability.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions imapserver/conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
173 changes: 173 additions & 0 deletions imapserver/id.go
Original file line number Diff line number Diff line change
@@ -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
}