Skip to content

Commit b75e766

Browse files
authored
Merge branch 'sora' into condstore
2 parents 55b3fb2 + 1506e71 commit b75e766

File tree

17 files changed

+807
-33
lines changed

17 files changed

+807
-33
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ go 1.18
55
require (
66
github.com/emersion/go-message v0.18.1
77
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43
8+
golang.org/x/text v0.14.0
89
)

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
2727
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
2828
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
2929
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
30+
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
3031
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
3132
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
3233
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=

id.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,8 @@ type IDData struct {
1212
Command string
1313
Arguments string
1414
Environment string
15+
16+
// Raw contains all raw key-value pairs. Standard keys are also present
17+
// in this map. Keys are case-insensitive and are normalized to lowercase.
18+
Raw map[string]string
1519
}

imapclient/client.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,8 @@ func (c *Client) setCaps(caps imap.CapSet) {
345345

346346
c.mutex.Lock()
347347
c.caps = caps
348+
quotedUTF8 := c.caps.Has(imap.CapIMAP4rev2) || c.enabled.Has(imap.CapUTF8Accept)
349+
c.dec.QuotedUTF8 = quotedUTF8
348350
c.mutex.Unlock()
349351
}
350352

imapclient/enable.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ func (c *Client) handleEnabled() error {
4343
for name := range caps {
4444
c.enabled[name] = struct{}{}
4545
}
46+
47+
quotedUTF8 := c.caps.Has(imap.CapIMAP4rev2) || c.enabled.Has(imap.CapUTF8Accept)
48+
c.dec.QuotedUTF8 = quotedUTF8
4649
c.mutex.Unlock()
4750

4851
if cmd := findPendingCmdByType[*EnableCommand](c); cmd != nil {

imapclient/id.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package imapclient
22

33
import (
44
"fmt"
5+
"strings"
56

67
"github.com/emersion/go-imap/v2"
78
"github.com/emersion/go-imap/v2/internal/imapwire"
@@ -60,6 +61,18 @@ func (c *Client) ID(idData *imap.IDData) *IDCommand {
6061
if idData.Environment != "" {
6162
addIDKeyValue(enc, &isFirstKey, "environment", idData.Environment)
6263
}
64+
if idData.Raw != nil {
65+
stdKeys := map[string]struct{}{
66+
"name": {}, "version": {}, "os": {}, "os-version": {}, "vendor": {},
67+
"support-url": {}, "address": {}, "date": {}, "command": {},
68+
"arguments": {}, "environment": {},
69+
}
70+
for k, v := range idData.Raw {
71+
if _, ok := stdKeys[strings.ToLower(k)]; !ok {
72+
addIDKeyValue(enc, &isFirstKey, k, v)
73+
}
74+
}
75+
}
6376

6477
enc.Special(')')
6578
enc.end()
@@ -91,7 +104,9 @@ func (c *Client) handleID() error {
91104
}
92105

93106
func (c *Client) readID(dec *imapwire.Decoder) (*imap.IDData, error) {
94-
var data = imap.IDData{}
107+
var data = imap.IDData{
108+
Raw: make(map[string]string),
109+
}
95110

96111
if !dec.ExpectSP() {
97112
return nil, dec.Err()
@@ -113,7 +128,10 @@ func (c *Client) readID(dec *imapwire.Decoder) (*imap.IDData, error) {
113128
return nil
114129
}
115130

116-
switch currKey {
131+
lowerKey := strings.ToLower(currKey)
132+
data.Raw[lowerKey] = keyOrValue
133+
134+
switch lowerKey {
117135
case "name":
118136
data.Name = keyOrValue
119137
case "version":
@@ -138,6 +156,7 @@ func (c *Client) readID(dec *imapwire.Decoder) (*imap.IDData, error) {
138156
data.Environment = keyOrValue
139157
default:
140158
// Ignore unknown key
159+
// Unknown key is already stored in Raw
141160
// Yahoo server sends "host" and "remote-host" keys
142161
// which are not defined in RFC 2971
143162
}

imapclient/select.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func (c *Client) handleFlags() error {
6969
c.mutex.Lock()
7070
if c.state == imap.ConnStateSelected {
7171
c.mailbox = c.mailbox.copy()
72-
c.mailbox.PermanentFlags = flags
72+
c.mailbox.Flags = flags
7373
}
7474
c.mutex.Unlock()
7575

imapserver/capability.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ func (c *Conn) availableCaps() []imap.Cap {
9595
imap.CapUnauthenticate,
9696
imap.CapCondStore,
9797
imap.CapQResync,
98+
imap.CapSort,
99+
imap.CapSortDisplay,
100+
imap.CapESort,
101+
imap.CapID,
98102
})
99103

100104
if appendLimitSession, ok := c.session.(SessionAppendLimit); ok {

imapserver/conn.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,15 @@ func (c *Conn) serve() {
179179
dec.MaxSize = maxCommandSize
180180
dec.CheckBufferedLiteralFunc = c.checkBufferedLiteral
181181

182+
c.mutex.Lock()
183+
// IMAP4rev2 is automatically enabled when advertised in capabilities
184+
// UTF8=ACCEPT must be explicitly enabled
185+
imap4rev2Available := c.server.options.caps().Has(imap.CapIMAP4rev2)
186+
utf8AcceptEnabled := c.enabled.Has(imap.CapUTF8Accept)
187+
quotedUTF8 := imap4rev2Available || utf8AcceptEnabled
188+
c.mutex.Unlock()
189+
dec.QuotedUTF8 = quotedUTF8
190+
182191
if c.state == imap.ConnStateLogout || dec.EOF() {
183192
break
184193
}
@@ -194,6 +203,17 @@ func (c *Conn) serve() {
194203
}
195204

196205
func (c *Conn) readCommand(dec *imapwire.Decoder) error {
206+
for {
207+
if dec.EOF() {
208+
return nil
209+
}
210+
211+
if dec.ExpectCRLF() {
212+
continue
213+
}
214+
break
215+
}
216+
197217
var tag, name string
198218
if !dec.ExpectAtom(&tag) || !dec.ExpectSP() || !dec.ExpectAtom(&name) {
199219
return fmt.Errorf("in command: %w", dec.Err())
@@ -220,6 +240,9 @@ func (c *Conn) readCommand(dec *imapwire.Decoder) error {
220240
err = c.handleLogout(dec)
221241
case "CAPABILITY":
222242
err = c.handleCapability(dec)
243+
case "ID":
244+
err = c.handleID(tag, dec)
245+
sendOK = false
223246
case "STARTTLS":
224247
err = c.handleStartTLS(tag, dec)
225248
sendOK = false
@@ -276,6 +299,8 @@ func (c *Conn) readCommand(dec *imapwire.Decoder) error {
276299
err = c.handleMove(dec, numKind)
277300
case "SEARCH", "UID SEARCH":
278301
err = c.handleSearch(tag, dec, numKind)
302+
case "SORT", "UID SORT":
303+
err = c.handleSort(tag, dec, numKind)
279304
default:
280305
if c.state == imap.ConnStateNotAuthenticated {
281306
// Don't allow a single unknown command before authentication to
@@ -492,7 +517,11 @@ type responseEncoder struct {
492517

493518
func newResponseEncoder(conn *Conn) *responseEncoder {
494519
conn.mutex.Lock()
495-
quotedUTF8 := conn.enabled.Has(imap.CapIMAP4rev2) || conn.enabled.Has(imap.CapUTF8Accept)
520+
// IMAP4rev2 is automatically enabled when advertised in capabilities
521+
// UTF8=ACCEPT must be explicitly enabled
522+
imap4rev2Available := conn.server.options.caps().Has(imap.CapIMAP4rev2)
523+
utf8AcceptEnabled := conn.enabled.Has(imap.CapUTF8Accept)
524+
quotedUTF8 := imap4rev2Available || utf8AcceptEnabled
496525
conn.mutex.Unlock()
497526

498527
wireEnc := imapwire.NewEncoder(conn.bw, imapwire.ConnSideServer)

imapserver/id.go

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package imapserver
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/emersion/go-imap/v2"
8+
"github.com/emersion/go-imap/v2/internal/imapwire"
9+
)
10+
11+
func (c *Conn) handleID(tag string, dec *imapwire.Decoder) error {
12+
idData, err := readID(dec)
13+
if err != nil {
14+
return fmt.Errorf("in id: %v", err)
15+
}
16+
17+
if !dec.ExpectCRLF() {
18+
return dec.Err()
19+
}
20+
21+
var serverIDData *imap.IDData
22+
if idSess, ok := c.session.(SessionID); ok {
23+
serverIDData = idSess.ID(idData)
24+
}
25+
26+
enc := newResponseEncoder(c)
27+
enc.Atom("*").SP().Atom("ID")
28+
29+
if serverIDData == nil {
30+
enc.SP().NIL()
31+
} else {
32+
enc.SP().Special('(')
33+
isFirstKey := true
34+
if serverIDData.Name != "" {
35+
addIDKeyValue(enc.Encoder, &isFirstKey, "name", serverIDData.Name)
36+
}
37+
if serverIDData.Version != "" {
38+
addIDKeyValue(enc.Encoder, &isFirstKey, "version", serverIDData.Version)
39+
}
40+
if serverIDData.OS != "" {
41+
addIDKeyValue(enc.Encoder, &isFirstKey, "os", serverIDData.OS)
42+
}
43+
if serverIDData.OSVersion != "" {
44+
addIDKeyValue(enc.Encoder, &isFirstKey, "os-version", serverIDData.OSVersion)
45+
}
46+
if serverIDData.Vendor != "" {
47+
addIDKeyValue(enc.Encoder, &isFirstKey, "vendor", serverIDData.Vendor)
48+
}
49+
if serverIDData.SupportURL != "" {
50+
addIDKeyValue(enc.Encoder, &isFirstKey, "support-url", serverIDData.SupportURL)
51+
}
52+
if serverIDData.Address != "" {
53+
addIDKeyValue(enc.Encoder, &isFirstKey, "address", serverIDData.Address)
54+
}
55+
if serverIDData.Date != "" {
56+
addIDKeyValue(enc.Encoder, &isFirstKey, "date", serverIDData.Date)
57+
}
58+
if serverIDData.Command != "" {
59+
addIDKeyValue(enc.Encoder, &isFirstKey, "command", serverIDData.Command)
60+
}
61+
if serverIDData.Arguments != "" {
62+
addIDKeyValue(enc.Encoder, &isFirstKey, "arguments", serverIDData.Arguments)
63+
}
64+
if serverIDData.Environment != "" {
65+
addIDKeyValue(enc.Encoder, &isFirstKey, "environment", serverIDData.Environment)
66+
}
67+
if serverIDData.Raw != nil {
68+
stdKeys := map[string]struct{}{
69+
"name": {}, "version": {}, "os": {}, "os-version": {}, "vendor": {},
70+
"support-url": {}, "address": {}, "date": {}, "command": {},
71+
"arguments": {}, "environment": {},
72+
}
73+
for k, v := range serverIDData.Raw {
74+
if _, ok := stdKeys[strings.ToLower(k)]; !ok {
75+
addIDKeyValue(enc.Encoder, &isFirstKey, k, v)
76+
}
77+
}
78+
}
79+
enc.Special(')')
80+
}
81+
82+
err = enc.CRLF()
83+
enc.end()
84+
if err != nil {
85+
return err
86+
}
87+
88+
return c.writeStatusResp(tag, &imap.StatusResponse{
89+
Type: imap.StatusResponseTypeOK,
90+
Text: "ID completed",
91+
})
92+
}
93+
94+
func readID(dec *imapwire.Decoder) (*imap.IDData, error) {
95+
if !dec.ExpectSP() {
96+
return nil, dec.Err()
97+
}
98+
99+
if dec.ExpectNIL() {
100+
return nil, nil
101+
}
102+
103+
data := &imap.IDData{
104+
Raw: make(map[string]string),
105+
}
106+
currKey := ""
107+
err := dec.ExpectList(func() error {
108+
var keyOrValue string
109+
if !dec.String(&keyOrValue) {
110+
return fmt.Errorf("in id key-val list: %v", dec.Err())
111+
}
112+
113+
if currKey == "" {
114+
currKey = keyOrValue
115+
return nil
116+
}
117+
118+
lowerKey := strings.ToLower(currKey)
119+
data.Raw[lowerKey] = keyOrValue
120+
121+
switch lowerKey {
122+
case "name":
123+
data.Name = keyOrValue
124+
case "version":
125+
data.Version = keyOrValue
126+
case "os":
127+
data.OS = keyOrValue
128+
case "os-version":
129+
data.OSVersion = keyOrValue
130+
case "vendor":
131+
data.Vendor = keyOrValue
132+
case "support-url":
133+
data.SupportURL = keyOrValue
134+
case "address":
135+
data.Address = keyOrValue
136+
case "date":
137+
data.Date = keyOrValue
138+
case "command":
139+
data.Command = keyOrValue
140+
case "arguments":
141+
data.Arguments = keyOrValue
142+
case "environment":
143+
data.Environment = keyOrValue
144+
default:
145+
// Unknown key, already stored in Raw
146+
}
147+
currKey = ""
148+
149+
return nil
150+
})
151+
152+
if err != nil {
153+
return nil, err
154+
}
155+
156+
return data, nil
157+
}
158+
159+
func addIDKeyValue(enc *imapwire.Encoder, isFirstKey *bool, key, value string) {
160+
if *isFirstKey {
161+
enc.Quoted(key).SP().Quoted(value)
162+
} else {
163+
enc.SP().Quoted(key).SP().Quoted(value)
164+
}
165+
*isFirstKey = false
166+
}
167+
168+
// SessionID is an interface for sessions that can provide server ID information.
169+
type SessionID interface {
170+
// ID returns server information in response to a client ID command.
171+
// The client's ID information is provided if available.
172+
ID(clientID *imap.IDData) *imap.IDData
173+
}

0 commit comments

Comments
 (0)