8 "github.com/mjl-/bstore"
10 "github.com/mjl-/mox/store"
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
18// notify represents a configuration as passed to the notify command.
20 // "NOTIFY NONE" results in an empty list, matching no events.
21 EventGroups []eventGroup
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
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
37 // Non-message-related events are only matched by non-"selected" mailbox
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
46 for _, eg := range n.EventGroups {
47 switch eg.MailboxSpecifier.Kind {
49 if mailboxID != c.mailboxID || !slices.Contains(messageEventKinds, kind) {
52 for _, ev := range eg.Events {
53 if eventKind(ev.Kind) == kind {
54 return eg.MailboxSpecifier, ev, true
57 // We can only have a single selected for notify, so no point in continuing the search.
58 return mailboxSpecifier{}, notifyEvent{}, false
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) {
70 switch eg.MailboxSpecifier.Kind {
75 if mailbox == "Inbox" || strings.HasPrefix(mailbox, "Inbox/") {
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 {
93 for _, rs := range dest.Rulesets {
94 if rs.ListAllowDomain == "" && rs.Mailbox == mailbox {
102 sub := store.Subscription{Name: mailbox}
103 err := xtxfn().Get(&sub)
104 if err != bstore.ErrAbsent {
105 xcheckf(err, "lookup subscription")
110 for _, name := range eg.MailboxSpecifier.Mailboxes {
111 if mailbox == name || strings.HasPrefix(mailbox, name+"/") {
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) {
127 match = slices.Contains(eg.MailboxSpecifier.Mailboxes, mailbox)
130 panic("missing case for " + string(eg.MailboxSpecifier.Kind))
138 if len(eg.Events) == 0 {
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
150 return mailboxSpecifier{}, notifyEvent{}, false
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.
159// State: Authenticated and selected.
160func (c *conn) cmdNotify(tag, cmd string, p *parser) {
166 // NONE indicates client doesn't want any events, also not the "normal" events
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
176 var changes []store.Change
178 changes = c.notify.Delayed
181 c.flushChanges(changes)
193 if p.take("STATUS") {
198 eg := p.xeventGroup()
199 n.EventGroups = append(n.EventGroups, eg)
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:
212 case eventMessageExpunge:
214 case eventFlagChange:
216 case eventMailboxName, eventSubscriptionChange, eventMailboxMetadataChange, eventServerMetadataChange:
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
223 xusercodeErrorf("BADEVENT (MessageNew MessageExpunge FlagChange MailboxName SubscriptionChange MailboxMetadataChange ServerMetadataChange)", "unimplemented event %s", ev.Kind)
226 if hasNew != hasExpunge {
228 xsyntaxErrorf("MessageNew and MessageExpunge must be specified together")
230 if (hasFlag || hasAnnotation) && !hasNew {
232 xsyntaxErrorf("FlagChange and/or AnnotationChange requires MessageNew and MessageExpunge")
236 for _, eg := range n.EventGroups {
237 for i, name := range eg.MailboxSpecifier.Mailboxes {
238 eg.MailboxSpecifier.Mailboxes[i] = xcheckmailboxname(name, true)
244 var haveSelected bool
245 for _, eg := range n.EventGroups {
246 switch eg.MailboxSpecifier.Kind {
247 case mbspecSelected, mbspecSelectedDelayed:
249 xsyntaxErrorf("cannot have multiple selected/selected-delayed mailbox filters")
253 // Only events from message-event are allowed with selected mailbox specifiers.
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)
265 delayed := c.notify.Delayed
266 c.notify.Delayed = nil
267 c.xapplyChangesNotify(delayed, true)
271 var statuses []string
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() {
277 case <-c.comm.Pending:
278 overflow, changes := c.comm.Get()
279 c.xapplyChanges(overflow, changes, true)
283 c.xdbread(func(tx *bstore.Tx) {
285 q := bstore.QueryTx[store.Mailbox](tx)
286 q.FilterEqual("Expunged", false)
288 for mb, err := range q.All() {
289 xcheckf(err, "list mailboxes for status")
291 if mb.ID == c.mailboxID {
294 _, _, ok := n.match(c, func() *bstore.Tx { return tx }, mb.ID, mb.Name, eventMessageNew)
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),
307 if c.enabled[capCondstore] || c.enabled[capQresync] {
308 list = append(list, bare("HIGHESTMODSEQ"), number(mb.ModSeq))
311 status := fmt.Sprintf("* STATUS %s %s", mailboxt(mb.Name).pack(c), list.pack(c))
312 statuses = append(statuses, status)
317 // Write outside of db transaction and lock.
318 for _, s := range statuses {
319 c.xbwritelinef("%s", s)
326 // Writing OK will flush any other pending changes for the account according to the