Skip to content

Commit 43d6868

Browse files
committed
Merge migadu/condstore: add ID, CONDSTORE, QRESYNC, SORT, ENABLE extensions
Incorporates server-side support from upstream PR emersion#690: - ID (RFC 2971): SessionID interface, handleID command - CONDSTORE (RFC 7162): HIGHESTMODSEQ, UNCHANGEDSINCE, MODSEQ in FETCH/STORE/SEARCH/SELECT - QRESYNC (RFC 7162): QRESYNC SELECT params, VANISHED responses, VanishedWriter - SORT (RFC 5256): SessionSort interface, ESORT support - ENABLE: patched to accept CONDSTORE/QRESYNC
2 parents 66d8f5a + ab7be97 commit 43d6868

37 files changed

+1943
-173
lines changed

capability.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,13 @@ func (set CapSet) Has(c Cap) bool {
130130
if c == CapLiteralMinus && set.has(CapLiteralPlus) {
131131
return true
132132
}
133-
if c == CapCondStore && set.has(CapQResync) {
133+
134+
// IMAP4rev2 implies QRESYNC, which in turn implies CONDSTORE.
135+
isQResync := set.has(CapQResync) || set.has(CapIMAP4rev2)
136+
if c == CapQResync && isQResync {
137+
return true
138+
}
139+
if c == CapCondStore && isQResync {
134140
return true
135141
}
136142
if c == CapUTF8Accept && set.has(CapUTF8Only) {

fetch.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ type FetchOptions struct {
2121
ModSeq bool // requires CONDSTORE
2222

2323
ChangedSince uint64 // requires CONDSTORE
24+
Vanished bool // requires QRESYNC, only valid for UID FETCH
2425
}
2526

2627
// FetchItemBodyStructure contains FETCH options for the body structure.

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.2
77
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6
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: 45 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -154,16 +154,15 @@ type Client struct {
154154
decCh chan struct{}
155155
decErr error
156156

157-
mutex sync.Mutex
158-
state imap.ConnState
159-
caps imap.CapSet
160-
enabled imap.CapSet
161-
pendingCapCh chan struct{}
162-
mailbox *SelectedMailbox
163-
cmdTag uint64
164-
pendingCmds []command
165-
contReqs []continuationRequest
166-
closed bool
157+
mutex sync.Mutex
158+
state imap.ConnState
159+
caps imap.CapSet
160+
enabled imap.CapSet
161+
mailbox *SelectedMailbox
162+
cmdTag uint64
163+
pendingCmds []command
164+
contReqs []continuationRequest
165+
closed bool
167166
}
168167

169168
// New creates a new IMAP client.
@@ -319,58 +318,35 @@ func (c *Client) Caps() imap.CapSet {
319318

320319
c.mutex.Lock()
321320
caps := c.caps
322-
capCh := c.pendingCapCh
323321
c.mutex.Unlock()
324322

325323
if caps != nil {
326324
return caps
327325
}
328326

329-
if capCh == nil {
330-
capCmd := c.Capability()
331-
capCh := make(chan struct{})
332-
go func() {
333-
capCmd.Wait()
334-
close(capCh)
335-
}()
336-
c.mutex.Lock()
337-
c.pendingCapCh = capCh
338-
c.mutex.Unlock()
339-
}
340-
341-
timer := time.NewTimer(respReadTimeout)
342-
defer timer.Stop()
343-
select {
344-
case <-timer.C:
327+
capCmd := c.Capability()
328+
caps, err := capCmd.Wait()
329+
if err != nil {
345330
return nil
346-
case <-capCh:
347-
// ok
348331
}
349-
350-
// TODO: this is racy if caps are reset before we get the reply
351-
c.mutex.Lock()
352-
defer c.mutex.Unlock()
353-
return c.caps
332+
return caps
354333
}
355334

356335
func (c *Client) setCaps(caps imap.CapSet) {
357336
// If the capabilities are being reset, request the updated capabilities
358337
// from the server
359-
var capCh chan struct{}
360338
if caps == nil {
361-
capCh = make(chan struct{})
362-
363339
// We need to send the CAPABILITY command in a separate goroutine:
364340
// setCaps might be called with Client.encMutex locked
365341
go func() {
366342
c.Capability().Wait()
367-
close(capCh)
368343
}()
369344
}
370345

371346
c.mutex.Lock()
372347
c.caps = caps
373-
c.pendingCapCh = capCh
348+
quotedUTF8 := c.caps.Has(imap.CapIMAP4rev2) || c.enabled.Has(imap.CapUTF8Accept)
349+
c.dec.QuotedUTF8 = quotedUTF8
374350
c.mutex.Unlock()
375351
}
376352

@@ -986,6 +962,11 @@ func (c *Client) readResponseData(typ string) error {
986962
return c.handleFetch(num)
987963
case "EXPUNGE":
988964
return c.handleExpunge(num)
965+
case "VANISHED":
966+
if !c.dec.ExpectSP() {
967+
return c.dec.Err()
968+
}
969+
return c.handleVanished()
989970
case "SEARCH":
990971
return c.handleSearch()
991972
case "ESEARCH":
@@ -1026,6 +1007,28 @@ func (c *Client) readResponseData(typ string) error {
10261007
return nil
10271008
}
10281009

1010+
func (c *Client) handleVanished() error {
1011+
var data imap.VanishedData
1012+
isParen := c.dec.Special('(')
1013+
if isParen {
1014+
var tag string
1015+
if !c.dec.ExpectAtom(&tag) || !c.dec.ExpectSpecial(')') {
1016+
return c.dec.Err()
1017+
}
1018+
data.Earlier = strings.ToUpper(tag) == "EARLIER"
1019+
}
1020+
1021+
if !c.dec.ExpectSP() || !c.dec.ExpectUIDSet(&data.UIDs) {
1022+
return c.dec.Err()
1023+
}
1024+
1025+
if handler := c.options.unilateralDataHandler().Vanished; handler != nil {
1026+
handler(&data)
1027+
}
1028+
1029+
return nil
1030+
}
1031+
10291032
// WaitGreeting waits for the server's initial greeting.
10301033
func (c *Client) WaitGreeting() error {
10311034
select {
@@ -1202,6 +1205,9 @@ type UnilateralDataHandler struct {
12021205
Mailbox func(data *UnilateralDataMailbox)
12031206
Fetch func(msg *FetchMessageData)
12041207

1208+
// requires ENABLE QRESYNC
1209+
Vanished func(data *imap.VanishedData)
1210+
12051211
// Requires ENABLE METADATA or ENABLE SERVER-METADATA.
12061212
Metadata func(mailbox string, entries []string)
12071213

imapclient/client_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ func newMemClientServerPair(t *testing.T) (net.Conn, io.Closer) {
102102
Caps: imap.CapSet{
103103
imap.CapIMAP4rev1: {},
104104
imap.CapIMAP4rev2: {},
105+
imap.CapCondStore: {},
106+
imap.CapQResync: {},
105107
},
106108
})
107109

@@ -184,6 +186,15 @@ func newClientServerPairWithOptions(t *testing.T, initialState imap.ConnState, o
184186
}
185187
}
186188

189+
// Enable CONDSTORE for Dovecot tests (required for CONDSTORE features)
190+
if useDovecot && initialState >= imap.ConnStateAuthenticated {
191+
if client.Caps().Has(imap.CapCondStore) {
192+
if _, err := client.Enable(imap.CapCondStore).Wait(); err != nil {
193+
t.Logf("Failed to enable CONDSTORE: %v", err)
194+
}
195+
}
196+
}
197+
187198
// Turn on debug logs after we're done initializing the test
188199
debugWriter.Swap(os.Stderr)
189200

0 commit comments

Comments
 (0)