mirror of
https://github.com/hanzoai/mail.git
synced 2026-09-23 23:53:02 +00:00
Fork of github.com/mjl-/mox at 5656ca6d1b
(upstream v0.0.15 plus 55 commits).
Rename the module path in go.mod and the import paths in 268 Go files. Also
update the five scripts that "go install" the module (genwebsite.sh,
docker-release.sh, apidiff.sh, test-upgrade.sh, Dockerfile.release), which
would otherwise build upstream rather than this fork.
Add NOTICE recording the upstream project, author, license, and fork point,
and stating which license covers which files: MIT for the mox source and our
changes, MPL-2.0 for publicsuffix/public_suffix_list.txt alone, and the
vendored dependencies' own licenses under licenses/. Embed NOTICE so
"mail licenses" prints the lineage from the running binary. LICENSE.MIT,
LICENSE.MPLv2.0 and the licenses/ tree are preserved verbatim; no code is
relicensed.
The mox command name, config keys, data directory layout and website content
are unchanged. LLM.md records the fork point, the rename, what was left alone,
and the steps to stand the server up.
Co-authored-by: Hanzo Dev <[email protected]>
330 lines
10 KiB
Go
330 lines
10 KiB
Go
package imapserver
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/mjl-/bstore"
|
|
|
|
"github.com/hanzoai/mail/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)
|
|
}
|