Skip to content

Commit 7db3eba

Browse files
committed
imapclient: implement support for NOTIFY
Client–server tests run only with dovecot; the imapmemserver doesn't support NOTIFY.
1 parent 5d38391 commit 7db3eba

File tree

6 files changed

+931
-1
lines changed

6 files changed

+931
-1
lines changed

imapclient/client.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,10 @@ func (c *Client) readResponseData(typ string) error {
896896
}
897897
case "NOMODSEQ":
898898
// ignore
899+
case "NOTIFICATIONOVERFLOW":
900+
if handler := c.options.unilateralDataHandler().NotificationOverflow; handler != nil {
901+
handler()
902+
}
899903
default: // [SP 1*<any TEXT-CHAR except "]">]
900904
if c.dec.SP() {
901905
c.dec.DiscardUntilByte(']')
@@ -1179,14 +1183,29 @@ type UnilateralDataMailbox struct {
11791183
//
11801184
// The handler will be invoked in an arbitrary goroutine.
11811185
//
1186+
// These handlers are important when using the IDLE or NOTIFY commands, as the
1187+
// server will send unsolicited STATUS, FETCH, and EXPUNGE responses for
1188+
// mailbox events.
1189+
//
11821190
// See Options.UnilateralDataHandler.
11831191
type UnilateralDataHandler struct {
11841192
Expunge func(seqNum uint32)
11851193
Mailbox func(data *UnilateralDataMailbox)
11861194
Fetch func(msg *FetchMessageData)
11871195

1188-
// requires ENABLE METADATA or ENABLE SERVER-METADATA
1196+
// Requires ENABLE METADATA or ENABLE SERVER-METADATA.
11891197
Metadata func(mailbox string, entries []string)
1198+
1199+
// Called when the server sends an unsolicited STATUS response.
1200+
//
1201+
// Commonly used with NOTIFY to receive mailbox status updates
1202+
// for non-selected mailboxes (RFC 5465).
1203+
Status func(data *imap.StatusData)
1204+
1205+
// Called when the server sends NOTIFICATIONOVERFLOW (RFC 5465).
1206+
//
1207+
// Indicates the server has disabled all NOTIFY notifications.
1208+
NotificationOverflow func()
11901209
}
11911210

11921211
// command is an interface for IMAP commands.

imapclient/notify.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package imapclient
2+
3+
import (
4+
"fmt"
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 NOTIFICATIONOVERFLOW, the NotificationOverflow callback
21+
// in UnilateralDataHandler will be called (if set).
22+
func (c *Client) Notify(options *imap.NotifyOptions) (*NotifyCommand, error) {
23+
cmd := &NotifyCommand{}
24+
enc := c.beginCommand("NOTIFY", cmd)
25+
if err := encodeNotifyOptions(enc.Encoder, options); err != nil {
26+
enc.end()
27+
return nil, err
28+
}
29+
enc.end()
30+
31+
if err := cmd.Wait(); err != nil {
32+
return nil, err
33+
}
34+
35+
return cmd, nil
36+
}
37+
38+
// encodeNotifyOptions encodes NOTIFY command options to the encoder.
39+
func encodeNotifyOptions(enc *imapwire.Encoder, options *imap.NotifyOptions) error {
40+
if options == nil || len(options.Items) == 0 {
41+
// NOTIFY NONE: disable all notifications.
42+
enc.SP().Atom("NONE")
43+
return nil
44+
}
45+
46+
enc.SP().Atom("SET")
47+
48+
if options.Status {
49+
enc.SP().List(1, func(i int) {
50+
enc.Atom("STATUS")
51+
})
52+
}
53+
54+
for _, item := range options.Items {
55+
if item.MailboxSpec == "" && len(item.Mailboxes) == 0 {
56+
return fmt.Errorf("invalid NOTIFY item: must specify either MailboxSpec or Mailboxes")
57+
}
58+
59+
enc.SP().List(1, func(_ int) {
60+
if item.MailboxSpec != "" {
61+
enc.Atom(string(item.MailboxSpec))
62+
} else {
63+
// len(item.Mailboxes) > 0, as per the check above.
64+
if item.Subtree {
65+
enc.Atom("SUBTREE").SP()
66+
}
67+
enc.List(len(item.Mailboxes), func(j int) {
68+
enc.Mailbox(item.Mailboxes[j])
69+
})
70+
}
71+
72+
if len(item.Events) > 0 {
73+
enc.SP().List(len(item.Events), func(j int) {
74+
enc.Atom(string(item.Events[j]))
75+
})
76+
}
77+
})
78+
79+
}
80+
81+
return nil
82+
}
83+
84+
// NotifyCommand is a NOTIFY command.
85+
//
86+
// When NOTIFY SET is active, the server may send unsolicited responses at any
87+
// time. These responses are delivered via UnilateralDataHandler
88+
// (see Options.UnilateralDataHandler).
89+
//
90+
// If the server sends NOTIFICATIONOVERFLOW, the NotificationOverflow callback
91+
// in UnilateralDataHandler will be called (if set).
92+
type NotifyCommand struct {
93+
commandBase
94+
}
95+
96+
// Wait blocks until the NOTIFY command has completed.
97+
func (cmd *NotifyCommand) Wait() error {
98+
return cmd.wait()
99+
}

0 commit comments

Comments
 (0)