mirror of
https://github.com/mjl-/mox.git
synced 2025-07-13 10:54:36 +03:00
imapserver: implement NOTIFY extension from RFC 5465
NOTIFY is like IDLE, but where IDLE watches just the selected mailbox, NOTIFY can watch all mailboxes. With NOTIFY, a client can also ask a server to immediately return configurable fetch attributes for new messages, e.g. a message preview, certain header fields, or simply the entire message. Mild testing with evolution and fairemail.
This commit is contained in:
@ -259,6 +259,13 @@ type MailboxCounts struct {
|
||||
Size int64 // Number of bytes for all messages.
|
||||
}
|
||||
|
||||
// MessageCountIMAP returns the total message count for use in IMAP. In IMAP,
|
||||
// message marked \Deleted are included, in JMAP they those messages are not
|
||||
// visible at all.
|
||||
func (mc MailboxCounts) MessageCountIMAP() uint32 {
|
||||
return uint32(mc.Total + mc.Deleted)
|
||||
}
|
||||
|
||||
func (mc MailboxCounts) String() string {
|
||||
return fmt.Sprintf("%d total, %d deleted, %d unread, %d unseen, size %d bytes", mc.Total, mc.Deleted, mc.Unread, mc.Unseen, mc.Size)
|
||||
}
|
||||
@ -601,13 +608,13 @@ func (m Message) MailboxCounts() (mc MailboxCounts) {
|
||||
return
|
||||
}
|
||||
|
||||
func (m Message) ChangeAddUID() ChangeAddUID {
|
||||
return ChangeAddUID{m.MailboxID, m.UID, m.ModSeq, m.Flags, m.Keywords}
|
||||
func (m Message) ChangeAddUID(mb Mailbox) ChangeAddUID {
|
||||
return ChangeAddUID{m.MailboxID, m.UID, m.ModSeq, m.Flags, m.Keywords, mb.MessageCountIMAP(), uint32(mb.MailboxCounts.Unseen)}
|
||||
}
|
||||
|
||||
func (m Message) ChangeFlags(orig Flags) ChangeFlags {
|
||||
func (m Message) ChangeFlags(orig Flags, mb Mailbox) ChangeFlags {
|
||||
mask := m.Flags.Changed(orig)
|
||||
return ChangeFlags{MailboxID: m.MailboxID, UID: m.UID, ModSeq: m.ModSeq, Mask: mask, Flags: m.Flags, Keywords: m.Keywords}
|
||||
return ChangeFlags{m.MailboxID, m.UID, m.ModSeq, mask, m.Flags, m.Keywords, mb.UIDValidity, uint32(mb.MailboxCounts.Unseen)}
|
||||
}
|
||||
|
||||
func (m Message) ChangeThread() ChangeThread {
|
||||
@ -2884,7 +2891,7 @@ func (a *Account) DeliverMailbox(log mlog.Log, mailbox string, m *Message, msgFi
|
||||
}
|
||||
|
||||
changes = append(changes, chl...)
|
||||
changes = append(changes, m.ChangeAddUID(), mb.ChangeCounts())
|
||||
changes = append(changes, m.ChangeAddUID(mb), mb.ChangeCounts())
|
||||
if nmbkeywords != len(mb.Keywords) {
|
||||
changes = append(changes, mb.ChangeKeywords())
|
||||
}
|
||||
@ -2992,7 +2999,7 @@ func (a *Account) MessageRemove(log mlog.Log, tx *bstore.Tx, modseq ModSeq, mb *
|
||||
}
|
||||
}
|
||||
|
||||
return ChangeRemoveUIDs{mb.ID, uids, modseq, ids}, mb.ChangeCounts(), nil
|
||||
return ChangeRemoveUIDs{mb.ID, uids, modseq, ids, mb.UIDNext, mb.MessageCountIMAP(), uint32(mb.MailboxCounts.Unseen)}, mb.ChangeCounts(), nil
|
||||
}
|
||||
|
||||
// TidyRejectsMailbox removes old reject emails, and returns whether there is space for a new delivery.
|
||||
|
@ -14,6 +14,11 @@ import (
|
||||
"github.com/mjl-/mox/mox-"
|
||||
)
|
||||
|
||||
// CommPendingChangesMax is the maximum number of changes kept for a Comm before
|
||||
// registering a notification overflow and flushing changes. Variable because set
|
||||
// to low value during tests.
|
||||
var CommPendingChangesMax = 10000
|
||||
|
||||
var (
|
||||
register = make(chan *Comm)
|
||||
unregister = make(chan *Comm)
|
||||
@ -48,6 +53,10 @@ type ChangeAddUID struct {
|
||||
ModSeq ModSeq
|
||||
Flags Flags // System flags.
|
||||
Keywords []string // Other flags.
|
||||
|
||||
// For IMAP NOTIFY.
|
||||
MessageCountIMAP uint32
|
||||
Unseen uint32
|
||||
}
|
||||
|
||||
func (c ChangeAddUID) ChangeModSeq() ModSeq { return c.ModSeq }
|
||||
@ -58,6 +67,11 @@ type ChangeRemoveUIDs struct {
|
||||
UIDs []UID // Must be in increasing UID order, for IMAP.
|
||||
ModSeq ModSeq
|
||||
MsgIDs []int64 // Message.ID, for erasing, order does not necessarily correspond with UIDs!
|
||||
|
||||
// For IMAP NOTIFY.
|
||||
UIDNext UID
|
||||
MessageCountIMAP uint32
|
||||
Unseen uint32
|
||||
}
|
||||
|
||||
func (c ChangeRemoveUIDs) ChangeModSeq() ModSeq { return c.ModSeq }
|
||||
@ -70,6 +84,10 @@ type ChangeFlags struct {
|
||||
Mask Flags // Which flags are actually modified.
|
||||
Flags Flags // New flag values. All are set, not just mask.
|
||||
Keywords []string // Non-system/well-known flags/keywords/labels.
|
||||
|
||||
// For IMAP NOTIFY.
|
||||
UIDValidity uint32
|
||||
Unseen uint32
|
||||
}
|
||||
|
||||
func (c ChangeFlags) ChangeModSeq() ModSeq { return c.ModSeq }
|
||||
@ -113,12 +131,20 @@ func (c ChangeRenameMailbox) ChangeModSeq() ModSeq { return c.ModSeq }
|
||||
|
||||
// ChangeAddSubscription is sent for an added subscription to a mailbox.
|
||||
type ChangeAddSubscription struct {
|
||||
Name string
|
||||
Flags []string // For additional IMAP flags like \NonExistent.
|
||||
MailboxName string
|
||||
ListFlags []string // For additional IMAP flags like \NonExistent.
|
||||
}
|
||||
|
||||
func (c ChangeAddSubscription) ChangeModSeq() ModSeq { return -1 }
|
||||
|
||||
// ChangeRemoveSubscription is sent for a removed subscription of a mailbox.
|
||||
type ChangeRemoveSubscription struct {
|
||||
MailboxName string
|
||||
ListFlags []string // For additional IMAP flags like \NonExistent.
|
||||
}
|
||||
|
||||
func (c ChangeRemoveSubscription) ChangeModSeq() ModSeq { return -1 }
|
||||
|
||||
// ChangeMailboxCounts is sent when the number of total/deleted/unseen/unread messages changes.
|
||||
type ChangeMailboxCounts struct {
|
||||
MailboxID int64
|
||||
@ -327,11 +353,9 @@ func switchboard(stopc, donec chan struct{}, cleanc chan map[*Account][]int64) {
|
||||
// possibly queue messages for cleaning. No need to take a lock, the caller does
|
||||
// not use the comm anymore.
|
||||
for _, ch := range c.changes {
|
||||
rem, ok := ch.(ChangeRemoveUIDs)
|
||||
if !ok {
|
||||
continue
|
||||
if rem, ok := ch.(ChangeRemoveUIDs); ok {
|
||||
decreaseEraseRefs(c.acc, rem.MsgIDs...)
|
||||
}
|
||||
decreaseEraseRefs(c.acc, rem.MsgIDs...)
|
||||
}
|
||||
|
||||
delete(regs[c.acc], c)
|
||||
@ -381,14 +405,31 @@ func switchboard(stopc, donec chan struct{}, cleanc chan map[*Account][]int64) {
|
||||
for c := range regs[acc] {
|
||||
// Do not send the broadcaster back their own changes. chReq.comm is nil if not
|
||||
// originating from a comm, so won't match in that case.
|
||||
// Relevant for IMAP IDLE, and NOTIFY ../rfc/5465:428
|
||||
if c == chReq.comm {
|
||||
continue
|
||||
}
|
||||
|
||||
var overflow bool
|
||||
c.Lock()
|
||||
c.changes = append(c.changes, chReq.changes...)
|
||||
if len(c.changes)+len(chReq.changes) > CommPendingChangesMax {
|
||||
c.overflow = true
|
||||
overflow = true
|
||||
} else {
|
||||
c.changes = append(c.changes, chReq.changes...)
|
||||
}
|
||||
c.Unlock()
|
||||
|
||||
// In case of overflow, we didn't add the pending changes to the comm, so we must
|
||||
// decrease references again.
|
||||
if overflow {
|
||||
for _, ch := range chReq.changes {
|
||||
if rem, ok := ch.(ChangeRemoveUIDs); ok {
|
||||
decreaseEraseRefs(acc, rem.MsgIDs...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case c.Pending <- struct{}{}:
|
||||
default:
|
||||
@ -463,6 +504,9 @@ type Comm struct {
|
||||
|
||||
sync.Mutex
|
||||
changes []Change
|
||||
// Set if too many changes were queued, cleared when changes are retrieved. While
|
||||
// in overflow, no new changes are added.
|
||||
overflow bool
|
||||
}
|
||||
|
||||
// Register starts a Comm for the account. Unregister must be called.
|
||||
@ -491,13 +535,16 @@ func (c *Comm) Broadcast(ch []Change) {
|
||||
}
|
||||
|
||||
// Get retrieves all pending changes. If no changes are pending a nil or empty list
|
||||
// is returned.
|
||||
func (c *Comm) Get() []Change {
|
||||
// is returned. If too many changes were pending, overflow is true, and this Comm
|
||||
// stopped getting new changes. The caller should usually return an error to its
|
||||
// connection. Even with overflow, changes may still be non-empty. On
|
||||
// ChangeRemoveUIDs, the RemovalSeen must still be called by the caller.
|
||||
func (c *Comm) Get() (overflow bool, changes []Change) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
l := c.changes
|
||||
c.changes = nil
|
||||
return l
|
||||
overflow, changes = c.overflow, c.changes
|
||||
c.overflow, c.changes = false, nil
|
||||
return
|
||||
}
|
||||
|
||||
// RemovalSeen must be called by consumers when they have applied the removal to
|
||||
|
Reference in New Issue
Block a user