Skip to content

Commit 2a6eb8e

Browse files
committed
imapserver: implement support for NOTIFY
1 parent 504d330 commit 2a6eb8e

File tree

5 files changed

+708
-0
lines changed

5 files changed

+708
-0
lines changed

imapserver/capability.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ func (c *Conn) availableCaps() []imap.Cap {
9393
imap.CapCreateSpecialUse,
9494
imap.CapLiteralPlus,
9595
imap.CapUnauthenticate,
96+
imap.CapNotify,
9697
})
9798

9899
if appendLimitSession, ok := c.session.(SessionAppendLimit); ok {

imapserver/conn.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@ func (c *Conn) serve() {
153153
if _, ok := c.session.(SessionUnauthenticate); !ok && caps.Has(imap.CapUnauthenticate) {
154154
panic("imapserver: server advertises UNAUTHENTICATE but session doesn't support it")
155155
}
156+
if _, ok := c.session.(SessionNotify); !ok && caps.Has(imap.CapNotify) {
157+
panic("imapserver: server advertises NOTIFY but session doesn't support it")
158+
}
156159

157160
c.state = imap.ConnStateNotAuthenticated
158161
statusType := imap.StatusResponseTypeOK
@@ -253,6 +256,8 @@ func (c *Conn) readCommand(dec *imapwire.Decoder) error {
253256
err = c.handleNamespace(dec)
254257
case "IDLE":
255258
err = c.handleIdle(dec)
259+
case "NOTIFY":
260+
err = c.handleNotify(dec)
256261
case "SELECT", "EXAMINE":
257262
err = c.handleSelect(tag, dec, name == "EXAMINE")
258263
sendOK = false

imapserver/notify.go

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
package imapserver
2+
3+
import (
4+
"strings"
5+
6+
"github.com/emersion/go-imap/v2"
7+
"github.com/emersion/go-imap/v2/internal/imapwire"
8+
)
9+
10+
func (c *Conn) handleNotify(dec *imapwire.Decoder) error {
11+
options, err := readNotifyOptions(dec)
12+
if err != nil {
13+
return err
14+
}
15+
16+
if err := c.checkState(imap.ConnStateAuthenticated); err != nil {
17+
return err
18+
}
19+
20+
session, ok := c.session.(SessionNotify)
21+
if !ok {
22+
return &imap.Error{
23+
Type: imap.StatusResponseTypeBad,
24+
Text: "NOTIFY not supported",
25+
}
26+
}
27+
28+
w := &UpdateWriter{conn: c, allowExpunge: true}
29+
return session.Notify(w, options)
30+
}
31+
32+
// readNotifyOptions parses the NOTIFY command arguments from the decoder.
33+
// Returns nil options for NOTIFY NONE, or populated options for NOTIFY SET.
34+
func readNotifyOptions(dec *imapwire.Decoder) (*imap.NotifyOptions, error) {
35+
if !dec.ExpectSP() {
36+
return nil, dec.Err()
37+
}
38+
39+
// Check for NONE or SET
40+
var atom string
41+
if !dec.ExpectAtom(&atom) {
42+
return nil, dec.Err()
43+
}
44+
45+
atom = strings.ToUpper(atom)
46+
if atom == "NONE" {
47+
// NOTIFY NONE - disable all notifications
48+
if !dec.ExpectCRLF() {
49+
return nil, dec.Err()
50+
}
51+
return nil, nil
52+
} else if atom == "SET" {
53+
// NOTIFY SET - set notifications
54+
options := &imap.NotifyOptions{}
55+
56+
// Parse items until we hit CRLF
57+
for {
58+
// We need at least a space before each item
59+
if !dec.SP() {
60+
return nil, &imap.Error{
61+
Type: imap.StatusResponseTypeBad,
62+
Text: "Expected SP after SET or between items",
63+
}
64+
}
65+
66+
// Parse a list
67+
isList, err := dec.List(func() error {
68+
// First element in the list: check if it's STATUS or a mailbox spec
69+
var firstAtom string
70+
if dec.Atom(&firstAtom) {
71+
firstAtom = strings.ToUpper(firstAtom)
72+
if firstAtom == "STATUS" {
73+
// This is the STATUS parameter
74+
options.STATUS = true
75+
return nil
76+
}
77+
78+
// It's a mailbox spec or SUBTREE, parse as a notify item
79+
item, err := parseNotifyItemFromAtom(dec, firstAtom)
80+
if err != nil {
81+
return err
82+
}
83+
options.Items = append(options.Items, *item)
84+
return nil
85+
}
86+
87+
// Not an atom, try to parse as mailbox list
88+
item, err := parseNotifyItemMailboxList(dec)
89+
if err != nil {
90+
return err
91+
}
92+
options.Items = append(options.Items, *item)
93+
return nil
94+
})
95+
if err != nil {
96+
return nil, err
97+
}
98+
if !isList {
99+
return nil, &imap.Error{
100+
Type: imap.StatusResponseTypeBad,
101+
Text: "Expected list",
102+
}
103+
}
104+
105+
// Check if we're done (CRLF)
106+
if dec.CRLF() {
107+
break
108+
}
109+
}
110+
111+
if len(options.Items) == 0 && !options.STATUS {
112+
return nil, &imap.Error{
113+
Type: imap.StatusResponseTypeBad,
114+
Text: "NOTIFY SET requires at least one mailbox specification",
115+
}
116+
}
117+
118+
return options, nil
119+
} else {
120+
return nil, &imap.Error{
121+
Type: imap.StatusResponseTypeBad,
122+
Text: "Expected NONE or SET",
123+
}
124+
}
125+
}
126+
127+
// parseNotifyItemFromAtom parses a notify item that starts with an atom (mailbox spec or SUBTREE)
128+
func parseNotifyItemFromAtom(dec *imapwire.Decoder, firstAtom string) (*imap.NotifyItem, error) {
129+
item := &imap.NotifyItem{}
130+
131+
switch firstAtom {
132+
case "SELECTED", "SELECTED-DELAYED", "PERSONAL", "INBOXES", "SUBSCRIBED":
133+
item.MailboxSpec = imap.NotifyMailboxSpec(firstAtom)
134+
135+
// Check for optional events list
136+
if dec.SP() {
137+
err := dec.ExpectList(func() error {
138+
return readNotifyEvent(dec, item)
139+
})
140+
if err != nil {
141+
return nil, err
142+
}
143+
}
144+
return item, nil
145+
146+
case "SUBTREE":
147+
// SUBTREE mailbox-list [event-list]
148+
item.Subtree = true
149+
150+
if !dec.ExpectSP() {
151+
return nil, dec.Err()
152+
}
153+
154+
// Read mailbox list
155+
err := dec.ExpectList(func() error {
156+
var mailbox string
157+
if !dec.ExpectMailbox(&mailbox) {
158+
return dec.Err()
159+
}
160+
item.Mailboxes = append(item.Mailboxes, mailbox)
161+
return nil
162+
})
163+
if err != nil {
164+
return nil, err
165+
}
166+
167+
// Check for optional events list
168+
if dec.SP() {
169+
err := dec.ExpectList(func() error {
170+
return readNotifyEvent(dec, item)
171+
})
172+
if err != nil {
173+
return nil, err
174+
}
175+
}
176+
return item, nil
177+
178+
default:
179+
return nil, &imap.Error{
180+
Type: imap.StatusResponseTypeBad,
181+
Text: "Invalid mailbox specification: " + firstAtom,
182+
}
183+
}
184+
}
185+
186+
// parseNotifyItemMailboxList parses a notify item that starts with a mailbox list
187+
func parseNotifyItemMailboxList(dec *imapwire.Decoder) (*imap.NotifyItem, error) {
188+
item := &imap.NotifyItem{}
189+
190+
// We're already inside a list, so we need to see if the first element is a mailbox or a list
191+
// The decoder is positioned at the start of the list content
192+
// Try to parse as a nested mailbox list
193+
isList, err := dec.List(func() error {
194+
var mailbox string
195+
if !dec.ExpectMailbox(&mailbox) {
196+
return dec.Err()
197+
}
198+
item.Mailboxes = append(item.Mailboxes, mailbox)
199+
return nil
200+
})
201+
if err != nil {
202+
return nil, err
203+
}
204+
if !isList {
205+
return nil, &imap.Error{
206+
Type: imap.StatusResponseTypeBad,
207+
Text: "Expected mailbox list",
208+
}
209+
}
210+
211+
// Check for optional events list
212+
if dec.SP() {
213+
err := dec.ExpectList(func() error {
214+
return readNotifyEvent(dec, item)
215+
})
216+
if err != nil {
217+
return nil, err
218+
}
219+
}
220+
221+
return item, nil
222+
}
223+
224+
func readNotifyEvent(dec *imapwire.Decoder, item *imap.NotifyItem) error {
225+
var event string
226+
if !dec.ExpectAtom(&event) {
227+
return dec.Err()
228+
}
229+
230+
// Validate event name
231+
switch imap.NotifyEvent(event) {
232+
case imap.NotifyEventMessageNew,
233+
imap.NotifyEventMessageExpunge,
234+
imap.NotifyEventFlagChange,
235+
imap.NotifyEventAnnotationChange,
236+
imap.NotifyEventMailboxName,
237+
imap.NotifyEventSubscriptionChange,
238+
imap.NotifyEventMailboxMetadataChange,
239+
imap.NotifyEventServerMetadataChange:
240+
item.Events = append(item.Events, imap.NotifyEvent(event))
241+
return nil
242+
default:
243+
return &imap.Error{
244+
Type: imap.StatusResponseTypeBad,
245+
Text: "Unknown NOTIFY event: " + event,
246+
}
247+
}
248+
}

0 commit comments

Comments
 (0)