Skip to content

Commit 1377251

Browse files
committed
imapclient: implement support for NOTIFY
1 parent 17771fb commit 1377251

File tree

5 files changed

+607
-1
lines changed

5 files changed

+607
-1
lines changed

imapclient/client.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,11 @@ func (c *Client) readResponseData(typ string) error {
896896
}
897897
case "NOMODSEQ":
898898
// ignore
899+
case "NOTIFICATIONOVERFLOW":
900+
// Server has disabled NOTIFY due to overflow (RFC 5465 section 5.8)
901+
if cmd := findPendingCmdByType[*NotifyCommand](c); cmd != nil {
902+
cmd.handleOverflow()
903+
}
899904
default: // [SP 1*<any TEXT-CHAR except "]">]
900905
if c.dec.SP() {
901906
c.dec.DiscardUntilByte(']')
@@ -1179,14 +1184,24 @@ type UnilateralDataMailbox struct {
11791184
//
11801185
// The handler will be invoked in an arbitrary goroutine.
11811186
//
1187+
// These handlers are important when using the NOTIFY command , as the server
1188+
// will send unsolicited STATUS, FETCH, and EXPUNGE responses for mailbox
1189+
// events.
1190+
//
11821191
// See Options.UnilateralDataHandler.
11831192
type UnilateralDataHandler struct {
11841193
Expunge func(seqNum uint32)
11851194
Mailbox func(data *UnilateralDataMailbox)
11861195
Fetch func(msg *FetchMessageData)
11871196

1188-
// requires ENABLE METADATA or ENABLE SERVER-METADATA
1197+
// Requires ENABLE METADATA or ENABLE SERVER-METADATA.
11891198
Metadata func(mailbox string, entries []string)
1199+
1200+
// Called when the server sends an unsolicited STATUS response.
1201+
//
1202+
// Commonly used with NOTIFY to receive mailbox status updates
1203+
// for non-selected mailboxes (RFC 5465).
1204+
Status func(data *imap.StatusData)
11901205
}
11911206

11921207
// command is an interface for IMAP commands.

imapclient/notify.go

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
package imapclient
2+
3+
import (
4+
"sync/atomic"
5+
6+
"github.com/emersion/go-imap/v2"
7+
"github.com/emersion/go-imap/v2/internal/imapwire"
8+
)
9+
10+
// Notify sends a NOTIFY command (RFC 5465).
11+
//
12+
// The NOTIFY command allows clients to request server-push notifications
13+
// for mailbox events like new messages, expunges, flag changes, etc.
14+
//
15+
// When NOTIFY SET is active, the server may send unsolicited responses at any
16+
// time (STATUS, FETCH, EXPUNGE, LIST responses). These unsolicited responses
17+
// are delivered via the UnilateralDataHandler callbacks set in
18+
// imapclient.Options.
19+
//
20+
// When the server sends an untagged OK [NOTIFICATIONOVERFLOW] response, the
21+
// Overflow() channel on the returned NotifyCommand will be closed. This
22+
// indicates the server has disabled all notifications and the client should
23+
// re-issue the NOTIFY command if needed.
24+
//
25+
// This requires support for the NOTIFY extension.
26+
//
27+
// Example:
28+
//
29+
// handler := &imapclient.UnilateralDataHandler{
30+
// Fetch: func(msg *imapclient.FetchMessageData) {
31+
// fmt.Printf("New message: UID %d\n", msg.UID)
32+
// },
33+
// Mailbox: func(data *imapclient.UnilateralDataMailbox) {
34+
// if data.NumMessages != nil {
35+
// fmt.Printf("Mailbox now has %d messages\n", *data.NumMessages)
36+
// }
37+
// },
38+
// Status: func(data *imap.StatusData) {
39+
// fmt.Printf("Status update for %s: %d messages\n", data.Mailbox, *data.NumMessages)
40+
// },
41+
// }
42+
// options := &imapclient.Options{UnilateralDataHandler: handler}
43+
// client, _ := imapclient.DialTLS("imap.example.org:993", options)
44+
//
45+
// notifyCmd, err := client.Notify(&imap.NotifyOptions{
46+
// Items: []imap.NotifyItem{{
47+
// MailboxSpec: imap.NotifyMailboxSpecSelected,
48+
// Events: []imap.NotifyEvent{
49+
// imap.NotifyEventMessageNew,
50+
// imap.NotifyEventMessageExpunge,
51+
// },
52+
// }},
53+
// })
54+
// if err != nil {
55+
// log.Fatal(err)
56+
// }
57+
//
58+
// // Monitor for overflow
59+
// <-notifyCmd.Overflow()
60+
// log.Println("NOTIFICATIONOVERFLOW received, re-issuing NOTIFY...")
61+
func (c *Client) Notify(options *imap.NotifyOptions) (*NotifyCommand, error) {
62+
cmd := &NotifyCommand{
63+
options: options,
64+
overflow: make(chan struct{}),
65+
}
66+
enc := c.beginCommand("NOTIFY", cmd)
67+
encodeNotifyOptions(enc.Encoder, options)
68+
enc.end()
69+
70+
if err := cmd.Wait(); err != nil {
71+
return nil, err
72+
}
73+
74+
return cmd, nil
75+
}
76+
77+
// encodeNotifyOptions encodes NOTIFY command options to the encoder.
78+
func encodeNotifyOptions(enc *imapwire.Encoder, options *imap.NotifyOptions) {
79+
if options == nil || len(options.Items) == 0 {
80+
// NOTIFY NONE - disable all notifications
81+
enc.SP().Atom("NONE")
82+
} else {
83+
// NOTIFY SET
84+
enc.SP().Atom("SET")
85+
86+
if options.STATUS {
87+
enc.SP().List(1, func(i int) {
88+
enc.Atom("STATUS")
89+
})
90+
}
91+
92+
// Encode each notify item
93+
for _, item := range options.Items {
94+
// Validate the item before encoding
95+
if item.MailboxSpec == "" && len(item.Mailboxes) == 0 {
96+
// Skip invalid items - this shouldn't happen with properly constructed NotifyOptions
97+
continue
98+
}
99+
100+
enc.SP().List(1, func(i int) {
101+
// Encode mailbox specification
102+
if item.MailboxSpec != "" {
103+
enc.Atom(string(item.MailboxSpec))
104+
} else if len(item.Mailboxes) > 0 {
105+
if item.Subtree {
106+
enc.Atom("SUBTREE").SP()
107+
}
108+
// Encode mailbox list
109+
enc.List(len(item.Mailboxes), func(j int) {
110+
enc.Mailbox(item.Mailboxes[j])
111+
})
112+
}
113+
114+
// Encode events
115+
if len(item.Events) > 0 {
116+
enc.SP().List(len(item.Events), func(j int) {
117+
enc.Atom(string(item.Events[j]))
118+
})
119+
}
120+
})
121+
}
122+
}
123+
}
124+
125+
// NotifyNone sends a NOTIFY NONE command to disable all notifications.
126+
func (c *Client) NotifyNone() error {
127+
_, err := c.Notify(nil)
128+
return err
129+
}
130+
131+
// NotifyCommand is a NOTIFY command.
132+
//
133+
// When NOTIFY SET is active (options != nil), the server may send unsolicited
134+
// responses at any time. These responses are delivered via UnilateralDataHandler
135+
// (see Options.UnilateralDataHandler).
136+
//
137+
// The Overflow() channel can be monitored to detect when the server sends an
138+
// untagged OK [NOTIFICATIONOVERFLOW] response, indicating that notifications
139+
// shall no longer be delivered.
140+
type NotifyCommand struct {
141+
commandBase
142+
143+
options *imap.NotifyOptions
144+
overflow chan struct{}
145+
closed atomic.Bool
146+
}
147+
148+
// Wait blocks until the NOTIFY command has completed.
149+
func (cmd *NotifyCommand) Wait() error {
150+
return cmd.wait()
151+
}
152+
153+
// Overflow returns a channel that is closed when the server sends a
154+
// NOTIFICATIONOVERFLOW response code. This indicates the server has disabled
155+
// notifications and the client should re-issue the NOTIFY command if needed.
156+
//
157+
// The channel is nil if NOTIFY NONE was sent (no notifications active).
158+
func (cmd *NotifyCommand) Overflow() <-chan struct{} {
159+
if cmd.options == nil || len(cmd.options.Items) == 0 {
160+
return nil
161+
}
162+
return cmd.overflow
163+
}
164+
165+
// Close disables the NOTIFY monitoring by calling it an internal close.
166+
// This is called internally when NOTIFICATIONOVERFLOW is received.
167+
func (cmd *NotifyCommand) close() {
168+
if cmd.closed.Swap(true) {
169+
return
170+
}
171+
close(cmd.overflow)
172+
}
173+
174+
func (cmd *NotifyCommand) handleOverflow() {
175+
cmd.close()
176+
}

0 commit comments

Comments
 (0)