Skip to content
Closed
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
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 for UID FETCH with ChangedSince
}

// FetchItemBodyStructure contains FETCH options for the body structure.
Expand Down
29 changes: 28 additions & 1 deletion imapclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -896,6 +896,11 @@ func (c *Client) readResponseData(typ string) error {
}
case "NOMODSEQ":
// ignore
case "NOTIFICATIONOVERFLOW":
// Server has disabled NOTIFY due to overflow (RFC 5465 section 5.8)
if cmd := findPendingCmdByType[*NotifyCommand](c); cmd != nil {
cmd.handleOverflow()
}
default: // [SP 1*<any TEXT-CHAR except "]">]
if c.dec.SP() {
c.dec.DiscardUntilByte(']')
Expand Down Expand Up @@ -973,6 +978,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 @@ -1179,14 +1189,31 @@ type UnilateralDataMailbox struct {
//
// The handler will be invoked in an arbitrary goroutine.
//
// These handlers are important when using the NOTIFY command , as the server
// will send unsolicited STATUS, FETCH, and EXPUNGE responses for mailbox
// events.
//
// See Options.UnilateralDataHandler.
type UnilateralDataHandler struct {
Expunge func(seqNum uint32)
Mailbox func(data *UnilateralDataMailbox)
Fetch func(msg *FetchMessageData)

// requires ENABLE METADATA or ENABLE SERVER-METADATA
// Requires ENABLE METADATA or ENABLE SERVER-METADATA.
Metadata func(mailbox string, entries []string)

// Called when the server sends an unsolicited STATUS response.
//
// Commonly used with NOTIFY to receive mailbox status updates
// for non-selected mailboxes (RFC 5465).
Status func(data *imap.StatusData)

// Called when the server sends an untagged VANISHED response.
//
// Requires QRESYNC extension (RFC 4551/7162). The parameter earlier
// indicates whether this response covers earlier expunges (true for
// SELECT QRESYNC responses, false for UID FETCH VANISHED responses).
Vanished func(uids imap.UIDSet, earlier bool)
}

// command is an interface for IMAP commands.
Expand Down
1 change: 1 addition & 0 deletions imapclient/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ func newMemClientServerPair(t *testing.T) (net.Conn, io.Closer) {
Caps: imap.CapSet{
imap.CapIMAP4rev1: {},
imap.CapIMAP4rev2: {},
imap.CapNotify: {},
},
})

Expand Down
2 changes: 1 addition & 1 deletion imapclient/enable.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func (c *Client) Enable(caps ...imap.Cap) *EnableCommand {
// extensions we support here
for _, name := range caps {
switch name {
case imap.CapIMAP4rev2, imap.CapUTF8Accept, imap.CapMetadata, imap.CapMetadataServer:
case imap.CapIMAP4rev2, imap.CapUTF8Accept, imap.CapMetadata, imap.CapMetadataServer, imap.CapQResync, imap.CapCondStore:
// ok
default:
done := make(chan error)
Expand Down
6 changes: 5 additions & 1 deletion imapclient/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ func (c *Client) Fetch(numSet imap.NumSet, options *imap.FetchOptions) *FetchCom
enc.SP().NumSet(numSet).SP()
writeFetchItems(enc.Encoder, numKind, options)
if options.ChangedSince != 0 {
enc.SP().Special('(').Atom("CHANGEDSINCE").SP().ModSeq(options.ChangedSince).Special(')')
enc.SP().Special('(').Atom("CHANGEDSINCE").SP().ModSeq(options.ChangedSince)
if options.Vanished {
enc.SP().Atom("VANISHED")
}
enc.Special(')')
}
enc.end()
return cmd
Expand Down
141 changes: 141 additions & 0 deletions imapclient/notify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package imapclient

import (
"sync/atomic"

"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/internal/imapwire"
)

// Notify sends a NOTIFY command (RFC 5465).
//
// The NOTIFY command allows clients to request server-push notifications
// for mailbox events like new messages, expunges, flag changes, etc.
//
// When NOTIFY SET is active, the server may send unsolicited responses at any
// time (STATUS, FETCH, EXPUNGE, LIST responses). These unsolicited responses
// are delivered via the UnilateralDataHandler callbacks set in
// imapclient.Options.
//
// When the server sends an untagged OK [NOTIFICATIONOVERFLOW] response, the
// Overflow() channel on the returned NotifyCommand will be closed. This
// indicates the server has disabled all notifications and the client should
// re-issue the NOTIFY command if needed.
//
// This requires support for the NOTIFY extension.
func (c *Client) Notify(options *imap.NotifyOptions) (*NotifyCommand, error) {
cmd := &NotifyCommand{
options: options,
overflow: make(chan struct{}),
}
enc := c.beginCommand("NOTIFY", cmd)
encodeNotifyOptions(enc.Encoder, options)
enc.end()

if err := cmd.Wait(); err != nil {
return nil, err
}

return cmd, nil
}

// encodeNotifyOptions encodes NOTIFY command options to the encoder.
func encodeNotifyOptions(enc *imapwire.Encoder, options *imap.NotifyOptions) {
if options == nil || len(options.Items) == 0 {
// NOTIFY NONE - disable all notifications
enc.SP().Atom("NONE")
} else {
// NOTIFY SET
enc.SP().Atom("SET")

if options.STATUS {
enc.SP().List(1, func(i int) {
enc.Atom("STATUS")
})
}

// Encode each notify item
for _, item := range options.Items {
// Validate the item before encoding
if item.MailboxSpec == "" && len(item.Mailboxes) == 0 {
// Skip invalid items - this shouldn't happen with properly constructed NotifyOptions
continue
}

enc.SP().List(1, func(i int) {
// Encode mailbox specification
if item.MailboxSpec != "" {
enc.Atom(string(item.MailboxSpec))
} else if len(item.Mailboxes) > 0 {
if item.Subtree {
enc.Atom("SUBTREE").SP()
}
// Encode mailbox list
enc.List(len(item.Mailboxes), func(j int) {
enc.Mailbox(item.Mailboxes[j])
})
}

// Encode events
if len(item.Events) > 0 {
enc.SP().List(len(item.Events), func(j int) {
enc.Atom(string(item.Events[j]))
})
}
})
}
}
}

// NotifyNone sends a NOTIFY NONE command to disable all notifications.
func (c *Client) NotifyNone() error {
_, err := c.Notify(nil)
return err
}

// NotifyCommand is a NOTIFY command.
//
// When NOTIFY SET is active (options != nil), the server may send unsolicited
// responses at any time. These responses are delivered via UnilateralDataHandler
// (see Options.UnilateralDataHandler).
//
// The Overflow() channel can be monitored to detect when the server sends an
// untagged OK [NOTIFICATIONOVERFLOW] response, indicating that notifications
// shall no longer be delivered.
type NotifyCommand struct {
commandBase

options *imap.NotifyOptions
overflow chan struct{}
closed atomic.Bool
}

// Wait blocks until the NOTIFY command has completed.
func (cmd *NotifyCommand) Wait() error {
return cmd.wait()
}

// Overflow returns a channel that is closed when the server sends a
// NOTIFICATIONOVERFLOW response code. This indicates the server has disabled
// notifications and the client should re-issue the NOTIFY command if needed.
//
// The channel is nil if NOTIFY NONE was sent (no notifications active).
func (cmd *NotifyCommand) Overflow() <-chan struct{} {
if cmd.options == nil || len(cmd.options.Items) == 0 {
return nil
}
return cmd.overflow
}

// Close disables the NOTIFY monitoring by calling it an internal close.
// This is called internally when NOTIFICATIONOVERFLOW is received.
func (cmd *NotifyCommand) close() {
if cmd.closed.Swap(true) {
return
}
close(cmd.overflow)
}

func (cmd *NotifyCommand) handleOverflow() {
cmd.close()
}
Loading