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
8 changes: 7 additions & 1 deletion capability.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,13 @@ func (set CapSet) Has(c Cap) bool {
if c == CapLiteralMinus && set.has(CapLiteralPlus) {
return true
}
if c == CapCondStore && set.has(CapQResync) {

// IMAP4rev2 implies QRESYNC, which in turn implies CONDSTORE.
isQResync := set.has(CapQResync) || set.has(CapIMAP4rev2)
if c == CapQResync && isQResync {
return true
}
if c == CapCondStore && isQResync {
return true
}
if c == CapUTF8Accept && set.has(CapUTF8Only) {
Expand Down
1 change: 1 addition & 0 deletions fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type FetchOptions struct {
ModSeq bool // requires CONDSTORE

ChangedSince uint64 // requires CONDSTORE
Vanished bool // requires QRESYNC, only valid for UID FETCH
}

// FetchItemBodyStructure contains FETCH options for the body structure.
Expand Down
82 changes: 43 additions & 39 deletions imapclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,16 +154,15 @@ type Client struct {
decCh chan struct{}
decErr error

mutex sync.Mutex
state imap.ConnState
caps imap.CapSet
enabled imap.CapSet
pendingCapCh chan struct{}
mailbox *SelectedMailbox
cmdTag uint64
pendingCmds []command
contReqs []continuationRequest
closed bool
mutex sync.Mutex
state imap.ConnState
caps imap.CapSet
enabled imap.CapSet
mailbox *SelectedMailbox
cmdTag uint64
pendingCmds []command
contReqs []continuationRequest
closed bool
}

// New creates a new IMAP client.
Expand Down Expand Up @@ -319,58 +318,33 @@ func (c *Client) Caps() imap.CapSet {

c.mutex.Lock()
caps := c.caps
capCh := c.pendingCapCh
c.mutex.Unlock()

if caps != nil {
return caps
}

if capCh == nil {
capCmd := c.Capability()
capCh := make(chan struct{})
go func() {
capCmd.Wait()
close(capCh)
}()
c.mutex.Lock()
c.pendingCapCh = capCh
c.mutex.Unlock()
}

timer := time.NewTimer(respReadTimeout)
defer timer.Stop()
select {
case <-timer.C:
capCmd := c.Capability()
caps, err := capCmd.Wait()
if err != nil {
return nil
case <-capCh:
// ok
}

// TODO: this is racy if caps are reset before we get the reply
c.mutex.Lock()
defer c.mutex.Unlock()
return c.caps
return caps
}

func (c *Client) setCaps(caps imap.CapSet) {
// If the capabilities are being reset, request the updated capabilities
// from the server
var capCh chan struct{}
if caps == nil {
capCh = make(chan struct{})

// We need to send the CAPABILITY command in a separate goroutine:
// setCaps might be called with Client.encMutex locked
go func() {
c.Capability().Wait()
close(capCh)
}()
}

c.mutex.Lock()
c.caps = caps
c.pendingCapCh = capCh
c.mutex.Unlock()
}

Expand Down Expand Up @@ -973,6 +947,11 @@ func (c *Client) readResponseData(typ string) error {
return c.handleFetch(num)
case "EXPUNGE":
return c.handleExpunge(num)
case "VANISHED":
if !c.dec.ExpectSP() {
return c.dec.Err()
}
return c.handleVanished()
case "SEARCH":
return c.handleSearch()
case "ESEARCH":
Expand Down Expand Up @@ -1013,6 +992,28 @@ func (c *Client) readResponseData(typ string) error {
return nil
}

func (c *Client) handleVanished() error {
var data imap.VanishedData
isParen := c.dec.Special('(')
if isParen {
var tag string
if !c.dec.ExpectAtom(&tag) || !c.dec.ExpectSpecial(')') {
return c.dec.Err()
}
data.Earlier = strings.ToUpper(tag) == "EARLIER"
}

if !c.dec.ExpectSP() || !c.dec.ExpectUIDSet(&data.UIDs) {
return c.dec.Err()
}

if handler := c.options.unilateralDataHandler().Vanished; handler != nil {
handler(&data)
}

return nil
}

// WaitGreeting waits for the server's initial greeting.
func (c *Client) WaitGreeting() error {
select {
Expand Down Expand Up @@ -1185,6 +1186,9 @@ type UnilateralDataHandler struct {
Mailbox func(data *UnilateralDataMailbox)
Fetch func(msg *FetchMessageData)

// requires ENABLE QRESYNC
Vanished func(data *imap.VanishedData)

// requires ENABLE METADATA or ENABLE SERVER-METADATA
Metadata func(mailbox string, entries []string)
}
Expand Down
11 changes: 11 additions & 0 deletions imapclient/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ func newMemClientServerPair(t *testing.T) (net.Conn, io.Closer) {
Caps: imap.CapSet{
imap.CapIMAP4rev1: {},
imap.CapIMAP4rev2: {},
imap.CapCondStore: {},
imap.CapQResync: {},
},
})

Expand Down Expand Up @@ -178,6 +180,15 @@ func newClientServerPair(t *testing.T, initialState imap.ConnState) (*imapclient
}
}

// Enable CONDSTORE for Dovecot tests (required for CONDSTORE features)
if useDovecot && initialState >= imap.ConnStateAuthenticated {
if client.Caps().Has(imap.CapCondStore) {
if _, err := client.Enable(imap.CapCondStore).Wait(); err != nil {
t.Logf("Failed to enable CONDSTORE: %v", err)
}
}
}

// Turn on debug logs after we're done initializing the test
debugWriter.Swap(os.Stderr)

Expand Down
Loading