1package imapserver
2
3import (
4 "fmt"
5 "slices"
6 "strings"
7
8 "github.com/mjl-/bstore"
9
10 "github.com/mjl-/mox/store"
11)
12
13// Max number of pending changes for selected-delayed mailbox before we write a
14// NOTIFICATIONOVERFLOW message, flush changes and stop gathering more changes.
15// Changed during tests.
16var selectedDelayedChangesMax = 1000
17
18// notify represents a configuration as passed to the notify command.
19type notify struct {
20 // "NOTIFY NONE" results in an empty list, matching no events.
21 EventGroups []eventGroup
22
23 // Changes for the selected mailbox in case of SELECTED-DELAYED, when we don't send
24 // events asynchrously. These must still be processed later on for their
25 // ChangeRemoveUIDs, to erase expunged message files. At the end of a command (e.g.
26 // NOOP) or immediately upon IDLE we will send untagged responses for these
27 // changes. If the connection breaks, we still process the ChangeRemoveUIDs.
28 Delayed []store.Change
29}
30
31// match checks if an event for a mailbox id/name (optional depending on type)
32// should be turned into a notification to the client.
33func (n notify) match(c *conn, xtxfn func() *bstore.Tx, mailboxID int64, mailbox string, kind eventKind) (mailboxSpecifier, notifyEvent, bool) {
34 // We look through the event groups, and won't stop looking until we've found a
35 // confirmation the event should be notified. ../rfc/5465:756
36
37 // Non-message-related events are only matched by non-"selected" mailbox
38 // specifiers. ../rfc/5465:268
39 // If you read the mailboxes matching paragraph in isolation, you would think only
40 // "SELECTED" and "SELECTED-DELAYED" can match events for the selected mailbox. But
41 // a few other places hint that that only applies to message events, not to mailbox
42 // events, such as subscriptions and mailbox metadata changes. With a strict
43 // interpretation, clients couldn't request notifications for such events for the
44 // selection mailbox. ../rfc/5465:752
45
46 for _, eg := range n.EventGroups {
47 switch eg.MailboxSpecifier.Kind {
48 case mbspecSelected, mbspecSelectedDelayed: // ../rfc/5465:800
49 if mailboxID != c.mailboxID || !slices.Contains(messageEventKinds, kind) {
50 continue
51 }
52 for _, ev := range eg.Events {
53 if eventKind(ev.Kind) == kind {
54 return eg.MailboxSpecifier, ev, true
55 }
56 }
57 // We can only have a single selected for notify, so no point in continuing the search.
58 return mailboxSpecifier{}, notifyEvent{}, false
59
60 default:
61 // The selected mailbox can only match for non-message events for specifiers other
62 // than "selected"/"selected-delayed".
63 if c.mailboxID == mailboxID && slices.Contains(messageEventKinds, kind) {
64 continue
65 }
66 }
67
68 var match bool
69 Match:
70 switch eg.MailboxSpecifier.Kind {
71 case mbspecPersonal: // ../rfc/5465:817
72 match = true
73
74 case mbspecInboxes: // ../rfc/5465:822
75 if mailbox == "Inbox" || strings.HasPrefix(mailbox, "Inbox/") {
76 match = true
77 break Match
78 }
79
80 if mailbox == "" {
81 break Match
82 }
83
84 // Include mailboxes we may deliver to based on destinations, or based on rulesets,
85 // not including deliveries for mailing lists.
86 conf, _ := c.account.Conf()
87 for _, dest := range conf.Destinations {
88 if dest.Mailbox == mailbox {
89 match = true
90 break Match
91 }
92
93 for _, rs := range dest.Rulesets {
94 if rs.ListAllowDomain == "" && rs.Mailbox == mailbox {
95 match = true
96 break Match
97 }
98 }
99 }
100
101 case mbspecSubscribed: // ../rfc/5465:831
102 sub := store.Subscription{Name: mailbox}
103 err := xtxfn().Get(&sub)
104 if err != bstore.ErrAbsent {
105 xcheckf(err, "lookup subscription")
106 }
107 match = err == nil
108
109 case mbspecSubtree: // ../rfc/5465:847
110 for _, name := range eg.MailboxSpecifier.Mailboxes {
111 if mailbox == name || strings.HasPrefix(mailbox, name+"/") {
112 match = true
113 break
114 }
115 }
116
117 case mbspecSubtreeOne: // ../rfc/7377:274
118 ntoken := len(strings.Split(mailbox, "/"))
119 for _, name := range eg.MailboxSpecifier.Mailboxes {
120 if mailbox == name || (strings.HasPrefix(mailbox, name+"/") && len(strings.Split(name, "/"))+1 == ntoken) {
121 match = true
122 break
123 }
124 }
125
126 case mbspecMailboxes: // ../rfc/5465:853
127 match = slices.Contains(eg.MailboxSpecifier.Mailboxes, mailbox)
128
129 default:
130 panic("missing case for " + string(eg.MailboxSpecifier.Kind))
131 }
132
133 if !match {
134 continue
135 }
136
137 // NONE is the signal we shouldn't return events for this mailbox. ../rfc/5465:455
138 if len(eg.Events) == 0 {
139 break
140 }
141
142 // If event kind matches, we will be notifying about this change. If not, we'll
143 // look again at next mailbox specifiers.
144 for _, ev := range eg.Events {
145 if eventKind(ev.Kind) == kind {
146 return eg.MailboxSpecifier, ev, true
147 }
148 }
149 }
150 return mailboxSpecifier{}, notifyEvent{}, false
151}
152
153// Notify enables continuous notifications from the server to the client, without
154// the client issuing an IDLE command. The mailboxes and events to notify about are
155// specified in the account. When notify is enabled, instead of being blocked
156// waiting for a command from the client, we also wait for events from the account,
157// and send events about it.
158//
159// State: Authenticated and selected.
160func (c *conn) cmdNotify(tag, cmd string, p *parser) {
161 // Command: ../rfc/5465:203
162 // Request syntax: ../rfc/5465:923
163
164 p.xspace()
165
166 // NONE indicates client doesn't want any events, also not the "normal" events
167 // without notify. ../rfc/5465:234
168 // ../rfc/5465:930
169 if p.take("NONE") {
170 p.xempty()
171
172 // If we have delayed changes for the selected mailbox, we are no longer going to
173 // notify about them. The client can't know anymore whether messages still exist,
174 // and trying to read them can cause errors if the messages have been expunged and
175 // erased.
176 var changes []store.Change
177 if c.notify != nil {
178 changes = c.notify.Delayed
179 }
180 c.notify = &notify{}
181 c.flushChanges(changes)
182
183 c.ok(tag, cmd)
184 return
185 }
186
187 var n notify
188 var status bool
189
190 // ../rfc/5465:926
191 p.xtake("SET")
192 p.xspace()
193 if p.take("STATUS") {
194 status = true
195 p.xspace()
196 }
197 for {
198 eg := p.xeventGroup()
199 n.EventGroups = append(n.EventGroups, eg)
200 if !p.space() {
201 break
202 }
203 }
204 p.xempty()
205
206 for _, eg := range n.EventGroups {
207 var hasNew, hasExpunge, hasFlag, hasAnnotation bool
208 for _, ev := range eg.Events {
209 switch eventKind(ev.Kind) {
210 case eventMessageNew:
211 hasNew = true
212 case eventMessageExpunge:
213 hasExpunge = true
214 case eventFlagChange:
215 hasFlag = true
216 case eventMailboxName, eventSubscriptionChange, eventMailboxMetadataChange, eventServerMetadataChange:
217 // Nothing special.
218 default: // Including eventAnnotationChange.
219 hasAnnotation = true // Ineffective, we don't implement message annotations yet.
220 // Result must be NO instead of BAD, and we must include BADEVENT and the events we
221 // support. ../rfc/5465:343
222 // ../rfc/5465:1033
223 xusercodeErrorf("BADEVENT (MessageNew MessageExpunge FlagChange MailboxName SubscriptionChange MailboxMetadataChange ServerMetadataChange)", "unimplemented event %s", ev.Kind)
224 }
225 }
226 if hasNew != hasExpunge {
227 // ../rfc/5465:443 ../rfc/5465:987
228 xsyntaxErrorf("MessageNew and MessageExpunge must be specified together")
229 }
230 if (hasFlag || hasAnnotation) && !hasNew {
231 // ../rfc/5465:439
232 xsyntaxErrorf("FlagChange and/or AnnotationChange requires MessageNew and MessageExpunge")
233 }
234 }
235
236 for _, eg := range n.EventGroups {
237 for i, name := range eg.MailboxSpecifier.Mailboxes {
238 eg.MailboxSpecifier.Mailboxes[i] = xcheckmailboxname(name, true)
239 }
240 }
241
242 // Only one selected/selected-delay mailbox filter is allowed. ../rfc/5465:779
243 // Only message events are allowed for selected/selected-delayed. ../rfc/5465:796
244 var haveSelected bool
245 for _, eg := range n.EventGroups {
246 switch eg.MailboxSpecifier.Kind {
247 case mbspecSelected, mbspecSelectedDelayed:
248 if haveSelected {
249 xsyntaxErrorf("cannot have multiple selected/selected-delayed mailbox filters")
250 }
251 haveSelected = true
252
253 // Only events from message-event are allowed with selected mailbox specifiers.
254 // ../rfc/5465:977
255 for _, ev := range eg.Events {
256 if !slices.Contains(messageEventKinds, eventKind(ev.Kind)) {
257 xsyntaxErrorf("selected/selected-delayed is only allowed with message events, not %s", ev.Kind)
258 }
259 }
260 }
261 }
262
263 // We must apply any changes for delayed select. ../rfc/5465:248
264 if c.notify != nil {
265 delayed := c.notify.Delayed
266 c.notify.Delayed = nil
267 c.xapplyChangesNotify(delayed, true)
268 }
269
270 if status {
271 var statuses []string
272
273 // Flush new pending changes before we read the current state from the database.
274 // Don't allow any concurrent changes for a consistent snapshot.
275 c.account.WithRLock(func() {
276 select {
277 case <-c.comm.Pending:
278 overflow, changes := c.comm.Get()
279 c.xapplyChanges(overflow, changes, true)
280 default:
281 }
282
283 c.xdbread(func(tx *bstore.Tx) {
284 // Send STATUS responses for all matching mailboxes. ../rfc/5465:271
285 q := bstore.QueryTx[store.Mailbox](tx)
286 q.FilterEqual("Expunged", false)
287 q.SortAsc("Name")
288 for mb, err := range q.All() {
289 xcheckf(err, "list mailboxes for status")
290
291 if mb.ID == c.mailboxID {
292 continue
293 }
294 _, _, ok := n.match(c, func() *bstore.Tx { return tx }, mb.ID, mb.Name, eventMessageNew)
295 if !ok {
296 continue
297 }
298
299 list := listspace{
300 bare("MESSAGES"), number(mb.MessageCountIMAP()),
301 bare("UIDNEXT"), number(mb.UIDNext),
302 bare("UIDVALIDITY"), number(mb.UIDValidity),
303 // Unseen is not mentioned for STATUS, but clients are able to parse it due to
304 // FlagChange, and it will be useful to have.
305 bare("UNSEEN"), number(mb.MailboxCounts.Unseen),
306 }
307 if c.enabled[capCondstore] || c.enabled[capQresync] {
308 list = append(list, bare("HIGHESTMODSEQ"), number(mb.ModSeq))
309 }
310
311 status := fmt.Sprintf("* STATUS %s %s", mailboxt(mb.Name).pack(c), list.pack(c))
312 statuses = append(statuses, status)
313 }
314 })
315 })
316
317 // Write outside of db transaction and lock.
318 for _, s := range statuses {
319 c.xbwritelinef("%s", s)
320 }
321 }
322
323 // We replace the previous notify config. ../rfc/5465:245
324 c.notify = &n
325
326 // Writing OK will flush any other pending changes for the account according to the
327 // new filters.
328 c.ok(tag, cmd)
329}
330