Skip to content

Commit a0dfec5

Browse files
committed
imapclient: initial VANISHED support
1 parent 17771fb commit a0dfec5

File tree

7 files changed

+93
-4
lines changed

7 files changed

+93
-4
lines changed

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 for UID FETCH with ChangedSince
2425
}
2526

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

imapclient/client.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -973,6 +973,11 @@ func (c *Client) readResponseData(typ string) error {
973973
return c.handleFetch(num)
974974
case "EXPUNGE":
975975
return c.handleExpunge(num)
976+
case "VANISHED":
977+
if !c.dec.ExpectSP() {
978+
return c.dec.Err()
979+
}
980+
return c.handleVanished()
976981
case "SEARCH":
977982
return c.handleSearch()
978983
case "ESEARCH":
@@ -1187,6 +1192,13 @@ type UnilateralDataHandler struct {
11871192

11881193
// requires ENABLE METADATA or ENABLE SERVER-METADATA
11891194
Metadata func(mailbox string, entries []string)
1195+
1196+
// Called when the server sends an untagged VANISHED response.
1197+
//
1198+
// Requires QRESYNC extension (RFC 4551/7162). The parameter earlier
1199+
// indicates whether this response covers earlier expunges (true for
1200+
// SELECT QRESYNC responses, false for UID FETCH VANISHED responses).
1201+
Vanished func(uids imap.UIDSet, earlier bool)
11901202
}
11911203

11921204
// command is an interface for IMAP commands.

imapclient/enable.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ func (c *Client) Enable(caps ...imap.Cap) *EnableCommand {
1414
// extensions we support here
1515
for _, name := range caps {
1616
switch name {
17-
case imap.CapIMAP4rev2, imap.CapUTF8Accept, imap.CapMetadata, imap.CapMetadataServer:
17+
case imap.CapIMAP4rev2, imap.CapUTF8Accept, imap.CapMetadata, imap.CapMetadataServer, imap.CapQResync, imap.CapCondStore:
1818
// ok
1919
default:
2020
done := make(chan error)

imapclient/fetch.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ func (c *Client) Fetch(numSet imap.NumSet, options *imap.FetchOptions) *FetchCom
3434
enc.SP().NumSet(numSet).SP()
3535
writeFetchItems(enc.Encoder, numKind, options)
3636
if options.ChangedSince != 0 {
37-
enc.SP().Special('(').Atom("CHANGEDSINCE").SP().ModSeq(options.ChangedSince).Special(')')
37+
enc.SP().Special('(').Atom("CHANGEDSINCE").SP().ModSeq(options.ChangedSince)
38+
if options.Vanished {
39+
enc.SP().Atom("VANISHED")
40+
}
41+
enc.Special(')')
3842
}
3943
enc.end()
4044
return cmd

imapclient/select.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,25 @@ func (c *Client) Select(mailbox string, options *imap.SelectOptions) *SelectComm
1717
cmd := &SelectCommand{mailbox: mailbox}
1818
enc := c.beginCommand(cmdName, cmd)
1919
enc.SP().Mailbox(mailbox)
20-
if options != nil && options.CondStore {
21-
enc.SP().Special('(').Atom("CONDSTORE").Special(')')
20+
if options != nil {
21+
if options.QResync != nil {
22+
// QRESYNC implies CONDSTORE
23+
enc.SP().Special('(').Atom("QRESYNC").SP()
24+
enc.Special('(')
25+
enc.Number(options.QResync.UIDValidity).SP().ModSeq(options.QResync.ModSeq)
26+
if options.QResync.KnownUIDs != nil {
27+
enc.SP().NumSet(*options.QResync.KnownUIDs)
28+
if options.QResync.SeqMatchData != nil {
29+
enc.SP().Special('(')
30+
enc.NumSet(options.QResync.SeqMatchData.KnownSeqSet).SP()
31+
enc.NumSet(options.QResync.SeqMatchData.KnownUIDSet)
32+
enc.Special(')')
33+
}
34+
}
35+
enc.Special(')').Special(')')
36+
} else if options.CondStore {
37+
enc.SP().Special('(').Atom("CONDSTORE").Special(')')
38+
}
2239
}
2340
enc.end()
2441
return cmd

imapclient/vanished.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package imapclient
2+
3+
import (
4+
"github.com/emersion/go-imap/v2"
5+
)
6+
7+
func (c *Client) handleVanished() error {
8+
var earlier bool
9+
if c.dec.Special('(') {
10+
var atom string
11+
if !c.dec.ExpectAtom(&atom) || atom != "EARLIER" || !c.dec.ExpectSpecial(')') {
12+
return c.dec.Err()
13+
}
14+
earlier = true
15+
if !c.dec.ExpectSP() {
16+
return c.dec.Err()
17+
}
18+
}
19+
20+
var uids imap.UIDSet
21+
if !c.dec.ExpectUIDSet(&uids) {
22+
return c.dec.Err()
23+
}
24+
25+
// Check if this is part of a SELECT command response
26+
cmd := findPendingCmdByType[*SelectCommand](c)
27+
if cmd != nil {
28+
cmd.data.VanishedUIDs = uids
29+
} else if handler := c.options.unilateralDataHandler().Vanished; handler != nil {
30+
handler(uids, earlier)
31+
}
32+
33+
return nil
34+
}

select.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,23 @@ package imap
44
type SelectOptions struct {
55
ReadOnly bool
66
CondStore bool // requires CONDSTORE
7+
8+
// QRESYNC parameters (requires QRESYNC extension, RFC 5162)
9+
QResync *SelectQResyncOptions
10+
}
11+
12+
// SelectQResyncOptions contains QRESYNC parameters for SELECT.
13+
type SelectQResyncOptions struct {
14+
UIDValidity uint32
15+
ModSeq uint64
16+
KnownUIDs *UIDSet // optional
17+
SeqMatchData *SelectSeqMatchData // optional
18+
}
19+
20+
// SelectSeqMatchData contains sequence match data for QRESYNC.
21+
type SelectSeqMatchData struct {
22+
KnownSeqSet SeqSet
23+
KnownUIDSet UIDSet
724
}
825

926
// SelectData is the data returned by a SELECT command.
@@ -28,4 +45,8 @@ type SelectData struct {
2845
List *ListData // requires IMAP4rev2
2946

3047
HighestModSeq uint64 // requires CONDSTORE
48+
49+
// UIDs of messages that were expunged.
50+
// Requires QRESYNC extension (RFC 4551/7162).
51+
VanishedUIDs UIDSet // requires QRESYNC
3152
}

0 commit comments

Comments
 (0)