mirror of
https://github.com/mjl-/mox.git
synced 2025-06-28 01:08:15 +03:00

We were first getting UIDs in a transaction with a lock. Then getting the changes and processing them in a special way. And then processing for qresync in a new transaction. The special processing of changes is now gone, it seems to have skipped adding/removing uids to the session, which can't be correct. The new approach is just using a lock and transaction and process the whole opening of the mailbox, and not processing any changes as part of the open, and getting rid of the special "initial" mode processing a mailbox.
330 lines
10 KiB
Go
330 lines
10 KiB
Go
package imapserver
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/mjl-/bstore"
|
|
|
|
"github.com/mjl-/mox/store"
|
|
)
|
|
|
|
// Max number of pending changes for selected-delayed mailbox before we write a
|
|
// NOTIFICATIONOVERFLOW message, flush changes and stop gathering more changes.
|
|
// Changed during tests.
|
|
var selectedDelayedChangesMax = 1000
|
|
|
|
// notify represents a configuration as passed to the notify command.
|
|
type notify struct {
|
|
// "NOTIFY NONE" results in an empty list, matching no events.
|
|
EventGroups []eventGroup
|
|
|
|
// Changes for the selected mailbox in case of SELECTED-DELAYED, when we don't send
|
|
// events asynchrously. These must still be processed later on for their
|
|
// ChangeRemoveUIDs, to erase expunged message files. At the end of a command (e.g.
|
|
// NOOP) or immediately upon IDLE we will send untagged responses for these
|
|
// changes. If the connection breaks, we still process the ChangeRemoveUIDs.
|
|
Delayed []store.Change
|
|
}
|
|
|
|
// match checks if an event for a mailbox id/name (optional depending on type)
|
|
// should be turned into a notification to the client.
|
|
func (n notify) match(c *conn, xtxfn func() *bstore.Tx, mailboxID int64, mailbox string, kind eventKind) (mailboxSpecifier, notifyEvent, bool) {
|
|
// We look through the event groups, and won't stop looking until we've found a
|
|
// confirmation the event should be notified. ../rfc/5465:756
|
|
|
|
// Non-message-related events are only matched by non-"selected" mailbox
|
|
// specifiers. ../rfc/5465:268
|
|
// If you read the mailboxes matching paragraph in isolation, you would think only
|
|
// "SELECTED" and "SELECTED-DELAYED" can match events for the selected mailbox. But
|
|
// a few other places hint that that only applies to message events, not to mailbox
|
|
// events, such as subscriptions and mailbox metadata changes. With a strict
|
|
// interpretation, clients couldn't request notifications for such events for the
|
|
// selection mailbox. ../rfc/5465:752
|
|
|
|
for _, eg := range n.EventGroups {
|
|
switch eg.MailboxSpecifier.Kind {
|
|
case mbspecSelected, mbspecSelectedDelayed: // ../rfc/5465:800
|
|
if mailboxID != c.mailboxID || !slices.Contains(messageEventKinds, kind) {
|
|
continue
|
|
}
|
|
for _, ev := range eg.Events {
|
|
if eventKind(ev.Kind) == kind {
|
|
return eg.MailboxSpecifier, ev, true
|
|
}
|
|
}
|
|
// We can only have a single selected for notify, so no point in continuing the search.
|
|
return mailboxSpecifier{}, notifyEvent{}, false
|
|
|
|
default:
|
|
// The selected mailbox can only match for non-message events for specifiers other
|
|
// than "selected"/"selected-delayed".
|
|
if c.mailboxID == mailboxID && slices.Contains(messageEventKinds, kind) {
|
|
continue
|
|
}
|
|
}
|
|
|
|
var match bool
|
|
Match:
|
|
switch eg.MailboxSpecifier.Kind {
|
|
case mbspecPersonal: // ../rfc/5465:817
|
|
match = true
|
|
|
|
case mbspecInboxes: // ../rfc/5465:822
|
|
if mailbox == "Inbox" || strings.HasPrefix(mailbox, "Inbox/") {
|
|
match = true
|
|
break Match
|
|
}
|
|
|
|
if mailbox == "" {
|
|
break Match
|
|
}
|
|
|
|
// Include mailboxes we may deliver to based on destinations, or based on rulesets,
|
|
// not including deliveries for mailing lists.
|
|
conf, _ := c.account.Conf()
|
|
for _, dest := range conf.Destinations {
|
|
if dest.Mailbox == mailbox {
|
|
match = true
|
|
break Match
|
|
}
|
|
|
|
for _, rs := range dest.Rulesets {
|
|
if rs.ListAllowDomain == "" && rs.Mailbox == mailbox {
|
|
match = true
|
|
break Match
|
|
}
|
|
}
|
|
}
|
|
|
|
case mbspecSubscribed: // ../rfc/5465:831
|
|
sub := store.Subscription{Name: mailbox}
|
|
err := xtxfn().Get(&sub)
|
|
if err != bstore.ErrAbsent {
|
|
xcheckf(err, "lookup subscription")
|
|
}
|
|
match = err == nil
|
|
|
|
case mbspecSubtree: // ../rfc/5465:847
|
|
for _, name := range eg.MailboxSpecifier.Mailboxes {
|
|
if mailbox == name || strings.HasPrefix(mailbox, name+"/") {
|
|
match = true
|
|
break
|
|
}
|
|
}
|
|
|
|
case mbspecSubtreeOne: // ../rfc/7377:274
|
|
ntoken := len(strings.Split(mailbox, "/"))
|
|
for _, name := range eg.MailboxSpecifier.Mailboxes {
|
|
if mailbox == name || (strings.HasPrefix(mailbox, name+"/") && len(strings.Split(name, "/"))+1 == ntoken) {
|
|
match = true
|
|
break
|
|
}
|
|
}
|
|
|
|
case mbspecMailboxes: // ../rfc/5465:853
|
|
match = slices.Contains(eg.MailboxSpecifier.Mailboxes, mailbox)
|
|
|
|
default:
|
|
panic("missing case for " + string(eg.MailboxSpecifier.Kind))
|
|
}
|
|
|
|
if !match {
|
|
continue
|
|
}
|
|
|
|
// NONE is the signal we shouldn't return events for this mailbox. ../rfc/5465:455
|
|
if len(eg.Events) == 0 {
|
|
break
|
|
}
|
|
|
|
// If event kind matches, we will be notifying about this change. If not, we'll
|
|
// look again at next mailbox specifiers.
|
|
for _, ev := range eg.Events {
|
|
if eventKind(ev.Kind) == kind {
|
|
return eg.MailboxSpecifier, ev, true
|
|
}
|
|
}
|
|
}
|
|
return mailboxSpecifier{}, notifyEvent{}, false
|
|
}
|
|
|
|
// Notify enables continuous notifications from the server to the client, without
|
|
// the client issuing an IDLE command. The mailboxes and events to notify about are
|
|
// specified in the account. When notify is enabled, instead of being blocked
|
|
// waiting for a command from the client, we also wait for events from the account,
|
|
// and send events about it.
|
|
//
|
|
// State: Authenticated and selected.
|
|
func (c *conn) cmdNotify(tag, cmd string, p *parser) {
|
|
// Command: ../rfc/5465:203
|
|
// Request syntax: ../rfc/5465:923
|
|
|
|
p.xspace()
|
|
|
|
// NONE indicates client doesn't want any events, also not the "normal" events
|
|
// without notify. ../rfc/5465:234
|
|
// ../rfc/5465:930
|
|
if p.take("NONE") {
|
|
p.xempty()
|
|
|
|
// If we have delayed changes for the selected mailbox, we are no longer going to
|
|
// notify about them. The client can't know anymore whether messages still exist,
|
|
// and trying to read them can cause errors if the messages have been expunged and
|
|
// erased.
|
|
var changes []store.Change
|
|
if c.notify != nil {
|
|
changes = c.notify.Delayed
|
|
}
|
|
c.notify = ¬ify{}
|
|
c.flushChanges(changes)
|
|
|
|
c.ok(tag, cmd)
|
|
return
|
|
}
|
|
|
|
var n notify
|
|
var status bool
|
|
|
|
// ../rfc/5465:926
|
|
p.xtake("SET")
|
|
p.xspace()
|
|
if p.take("STATUS") {
|
|
status = true
|
|
p.xspace()
|
|
}
|
|
for {
|
|
eg := p.xeventGroup()
|
|
n.EventGroups = append(n.EventGroups, eg)
|
|
if !p.space() {
|
|
break
|
|
}
|
|
}
|
|
p.xempty()
|
|
|
|
for _, eg := range n.EventGroups {
|
|
var hasNew, hasExpunge, hasFlag, hasAnnotation bool
|
|
for _, ev := range eg.Events {
|
|
switch eventKind(ev.Kind) {
|
|
case eventMessageNew:
|
|
hasNew = true
|
|
case eventMessageExpunge:
|
|
hasExpunge = true
|
|
case eventFlagChange:
|
|
hasFlag = true
|
|
case eventMailboxName, eventSubscriptionChange, eventMailboxMetadataChange, eventServerMetadataChange:
|
|
// Nothing special.
|
|
default: // Including eventAnnotationChange.
|
|
hasAnnotation = true // Ineffective, we don't implement message annotations yet.
|
|
// Result must be NO instead of BAD, and we must include BADEVENT and the events we
|
|
// support. ../rfc/5465:343
|
|
// ../rfc/5465:1033
|
|
xusercodeErrorf("BADEVENT (MessageNew MessageExpunge FlagChange MailboxName SubscriptionChange MailboxMetadataChange ServerMetadataChange)", "unimplemented event %s", ev.Kind)
|
|
}
|
|
}
|
|
if hasNew != hasExpunge {
|
|
// ../rfc/5465:443 ../rfc/5465:987
|
|
xsyntaxErrorf("MessageNew and MessageExpunge must be specified together")
|
|
}
|
|
if (hasFlag || hasAnnotation) && !hasNew {
|
|
// ../rfc/5465:439
|
|
xsyntaxErrorf("FlagChange and/or AnnotationChange requires MessageNew and MessageExpunge")
|
|
}
|
|
}
|
|
|
|
for _, eg := range n.EventGroups {
|
|
for i, name := range eg.MailboxSpecifier.Mailboxes {
|
|
eg.MailboxSpecifier.Mailboxes[i] = xcheckmailboxname(name, true)
|
|
}
|
|
}
|
|
|
|
// Only one selected/selected-delay mailbox filter is allowed. ../rfc/5465:779
|
|
// Only message events are allowed for selected/selected-delayed. ../rfc/5465:796
|
|
var haveSelected bool
|
|
for _, eg := range n.EventGroups {
|
|
switch eg.MailboxSpecifier.Kind {
|
|
case mbspecSelected, mbspecSelectedDelayed:
|
|
if haveSelected {
|
|
xsyntaxErrorf("cannot have multiple selected/selected-delayed mailbox filters")
|
|
}
|
|
haveSelected = true
|
|
|
|
// Only events from message-event are allowed with selected mailbox specifiers.
|
|
// ../rfc/5465:977
|
|
for _, ev := range eg.Events {
|
|
if !slices.Contains(messageEventKinds, eventKind(ev.Kind)) {
|
|
xsyntaxErrorf("selected/selected-delayed is only allowed with message events, not %s", ev.Kind)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// We must apply any changes for delayed select. ../rfc/5465:248
|
|
if c.notify != nil {
|
|
delayed := c.notify.Delayed
|
|
c.notify.Delayed = nil
|
|
c.xapplyChangesNotify(delayed, true)
|
|
}
|
|
|
|
if status {
|
|
var statuses []string
|
|
|
|
// Flush new pending changes before we read the current state from the database.
|
|
// Don't allow any concurrent changes for a consistent snapshot.
|
|
c.account.WithRLock(func() {
|
|
select {
|
|
case <-c.comm.Pending:
|
|
overflow, changes := c.comm.Get()
|
|
c.xapplyChanges(overflow, changes, true)
|
|
default:
|
|
}
|
|
|
|
c.xdbread(func(tx *bstore.Tx) {
|
|
// Send STATUS responses for all matching mailboxes. ../rfc/5465:271
|
|
q := bstore.QueryTx[store.Mailbox](tx)
|
|
q.FilterEqual("Expunged", false)
|
|
q.SortAsc("Name")
|
|
for mb, err := range q.All() {
|
|
xcheckf(err, "list mailboxes for status")
|
|
|
|
if mb.ID == c.mailboxID {
|
|
continue
|
|
}
|
|
_, _, ok := n.match(c, func() *bstore.Tx { return tx }, mb.ID, mb.Name, eventMessageNew)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
list := listspace{
|
|
bare("MESSAGES"), number(mb.MessageCountIMAP()),
|
|
bare("UIDNEXT"), number(mb.UIDNext),
|
|
bare("UIDVALIDITY"), number(mb.UIDValidity),
|
|
// Unseen is not mentioned for STATUS, but clients are able to parse it due to
|
|
// FlagChange, and it will be useful to have.
|
|
bare("UNSEEN"), number(mb.MailboxCounts.Unseen),
|
|
}
|
|
if c.enabled[capCondstore] || c.enabled[capQresync] {
|
|
list = append(list, bare("HIGHESTMODSEQ"), number(mb.ModSeq))
|
|
}
|
|
|
|
status := fmt.Sprintf("* STATUS %s %s", mailboxt(mb.Name).pack(c), list.pack(c))
|
|
statuses = append(statuses, status)
|
|
}
|
|
})
|
|
})
|
|
|
|
// Write outside of db transaction and lock.
|
|
for _, s := range statuses {
|
|
c.xbwritelinef("%s", s)
|
|
}
|
|
}
|
|
|
|
// We replace the previous notify config. ../rfc/5465:245
|
|
c.notify = &n
|
|
|
|
// Writing OK will flush any other pending changes for the account according to the
|
|
// new filters.
|
|
c.ok(tag, cmd)
|
|
}
|