mirror of
https://github.com/mjl-/mox.git
synced 2025-06-28 01:08:15 +03:00
imapserver: simplify and fix logic around processing changes while opening a mailbox (with SELECT or EXAMINE)
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.
This commit is contained in:
parent
fd5167fdb3
commit
af3e9351bc
@ -276,7 +276,7 @@ func (c *conn) cmdNotify(tag, cmd string, p *parser) {
|
|||||||
select {
|
select {
|
||||||
case <-c.comm.Pending:
|
case <-c.comm.Pending:
|
||||||
overflow, changes := c.comm.Get()
|
overflow, changes := c.comm.Get()
|
||||||
c.xapplyChanges(overflow, changes, true, true)
|
c.xapplyChanges(overflow, changes, true)
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -329,7 +329,7 @@ func (c *conn) cmdxReplace(isUID bool, tag, cmd string, p *parser) {
|
|||||||
// Must update our msgseq/uids tracking with latest pending changes.
|
// Must update our msgseq/uids tracking with latest pending changes.
|
||||||
l := pendingChanges
|
l := pendingChanges
|
||||||
pendingChanges = nil
|
pendingChanges = nil
|
||||||
c.xapplyChanges(overflow, l, false, false)
|
c.xapplyChanges(overflow, l, false)
|
||||||
|
|
||||||
// If we couldn't find the message, send a NO response. We've just applied pending
|
// If we couldn't find the message, send a NO response. We've just applied pending
|
||||||
// changes, which should have expunged the absent message.
|
// changes, which should have expunged the absent message.
|
||||||
|
@ -208,7 +208,7 @@ type conn struct {
|
|||||||
baseTLSConfig *tls.Config // Base TLS config to use for handshake.
|
baseTLSConfig *tls.Config // Base TLS config to use for handshake.
|
||||||
remoteIP net.IP
|
remoteIP net.IP
|
||||||
noRequireSTARTTLS bool
|
noRequireSTARTTLS bool
|
||||||
cmd string // Currently executing, for deciding to applyChanges and logging.
|
cmd string // Currently executing, for deciding to xapplyChanges and logging.
|
||||||
cmdMetric string // Currently executing, for metrics.
|
cmdMetric string // Currently executing, for metrics.
|
||||||
cmdStart time.Time
|
cmdStart time.Time
|
||||||
ncmds int // Number of commands processed. Used to abort connection when first incoming command is unknown/invalid.
|
ncmds int // Number of commands processed. Used to abort connection when first incoming command is unknown/invalid.
|
||||||
@ -675,10 +675,14 @@ func (c *conn) xbwriteresultf(format string, args ...any) {
|
|||||||
switch c.cmd {
|
switch c.cmd {
|
||||||
case "fetch", "store", "search":
|
case "fetch", "store", "search":
|
||||||
// ../rfc/9051:5862 ../rfc/7162:2033
|
// ../rfc/9051:5862 ../rfc/7162:2033
|
||||||
|
case "select", "examine":
|
||||||
|
// We don't send changes before having confirmed opening the mailbox, to prevent
|
||||||
|
// clients from trying to interpret changes when it considers there isn't a
|
||||||
|
// selected mailbox yet.
|
||||||
default:
|
default:
|
||||||
if c.comm != nil {
|
if c.comm != nil {
|
||||||
overflow, changes := c.comm.Get()
|
overflow, changes := c.comm.Get()
|
||||||
c.xapplyChanges(overflow, changes, false, true)
|
c.xapplyChanges(overflow, changes, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.xbwritelinef(format, args...)
|
c.xbwritelinef(format, args...)
|
||||||
@ -1304,7 +1308,7 @@ func (c *conn) command() {
|
|||||||
|
|
||||||
case <-c.comm.Pending:
|
case <-c.comm.Pending:
|
||||||
overflow, changes := c.comm.Get()
|
overflow, changes := c.comm.Get()
|
||||||
c.xapplyChanges(overflow, changes, false, false)
|
c.xapplyChanges(overflow, changes, false)
|
||||||
c.xflush()
|
c.xflush()
|
||||||
|
|
||||||
case <-mox.Shutdown.Done():
|
case <-mox.Shutdown.Done():
|
||||||
@ -1770,11 +1774,9 @@ func (c *conn) xmailboxID(tx *bstore.Tx, id int64) store.Mailbox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Apply changes to our session state.
|
// Apply changes to our session state.
|
||||||
// If initial is false, updates like EXISTS and EXPUNGE are written to the client.
|
|
||||||
// If initial is true, we only apply the changes.
|
|
||||||
// Should not be called while holding locks, as changes are written to client connections, which can block.
|
// Should not be called while holding locks, as changes are written to client connections, which can block.
|
||||||
// Does not flush output.
|
// Does not flush output.
|
||||||
func (c *conn) xapplyChanges(overflow bool, changes []store.Change, initial, sendDelayed bool) {
|
func (c *conn) xapplyChanges(overflow bool, changes []store.Change, sendDelayed bool) {
|
||||||
// If more changes were generated than we can process, we send a
|
// If more changes were generated than we can process, we send a
|
||||||
// NOTIFICATIONOVERFLOW as defined in the NOTIFY extension. ../rfc/5465:712
|
// NOTIFICATIONOVERFLOW as defined in the NOTIFY extension. ../rfc/5465:712
|
||||||
if overflow {
|
if overflow {
|
||||||
@ -1786,9 +1788,6 @@ func (c *conn) xapplyChanges(overflow bool, changes []store.Change, initial, sen
|
|||||||
// NOTIFY, but we also follow this for IDLE. ../rfc/5465:717
|
// NOTIFY, but we also follow this for IDLE. ../rfc/5465:717
|
||||||
c.notify = ¬ify{}
|
c.notify = ¬ify{}
|
||||||
c.xbwritelinef("* OK [NOTIFICATIONOVERFLOW] out of sync after too many pending changes")
|
c.xbwritelinef("* OK [NOTIFICATIONOVERFLOW] out of sync after too many pending changes")
|
||||||
if !initial {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
changes = nil
|
changes = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1861,16 +1860,10 @@ func (c *conn) xapplyChanges(overflow bool, changes []store.Change, initial, sen
|
|||||||
if !ok {
|
if !ok {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if initial && !c.uidonly && c.sequence(ch.UID) > 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
c.uidAppend(ch.UID)
|
c.uidAppend(ch.UID)
|
||||||
adds = append(adds, ch)
|
adds = append(adds, ch)
|
||||||
}
|
}
|
||||||
if len(adds) > 0 {
|
if len(adds) > 0 {
|
||||||
if initial {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Write the exists, and the UID and flags as well. Hopefully the client waits for
|
// Write the exists, and the UID and flags as well. Hopefully the client waits for
|
||||||
// long enough after the EXISTS to see these messages, and doesn't request them
|
// long enough after the EXISTS to see these messages, and doesn't request them
|
||||||
// again with a FETCH.
|
// again with a FETCH.
|
||||||
@ -1900,29 +1893,17 @@ func (c *conn) xapplyChanges(overflow bool, changes []store.Change, initial, sen
|
|||||||
for _, uid := range ch.UIDs {
|
for _, uid := range ch.UIDs {
|
||||||
// With uidonly, we must always return VANISHED. ../rfc/9586:232
|
// With uidonly, we must always return VANISHED. ../rfc/9586:232
|
||||||
if c.uidonly {
|
if c.uidonly {
|
||||||
if !initial {
|
c.exists--
|
||||||
c.exists--
|
vanishedUIDs.append(uint32(uid))
|
||||||
vanishedUIDs.append(uint32(uid))
|
|
||||||
}
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var seq msgseq
|
seq := c.xsequence(uid)
|
||||||
if initial {
|
|
||||||
seq = c.sequence(uid)
|
|
||||||
if seq <= 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
seq = c.xsequence(uid)
|
|
||||||
}
|
|
||||||
c.sequenceRemove(seq, uid)
|
c.sequenceRemove(seq, uid)
|
||||||
if !initial {
|
if qresync {
|
||||||
if qresync {
|
vanishedUIDs.append(uint32(uid))
|
||||||
vanishedUIDs.append(uint32(uid))
|
} else {
|
||||||
} else {
|
c.xbwritelinef("* %d EXPUNGE", seq)
|
||||||
c.xbwritelinef("* %d EXPUNGE", seq)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !vanishedUIDs.empty() {
|
if !vanishedUIDs.empty() {
|
||||||
@ -1933,9 +1914,6 @@ func (c *conn) xapplyChanges(overflow bool, changes []store.Change, initial, sen
|
|||||||
}
|
}
|
||||||
|
|
||||||
case store.ChangeFlags:
|
case store.ChangeFlags:
|
||||||
if initial {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var modseqStr string
|
var modseqStr string
|
||||||
if condstore {
|
if condstore {
|
||||||
modseqStr = fmt.Sprintf(" MODSEQ (%d)", ch.ModSeq.Client())
|
modseqStr = fmt.Sprintf(" MODSEQ (%d)", ch.ModSeq.Client())
|
||||||
@ -1987,9 +1965,10 @@ func (c *conn) xapplyChanges(overflow bool, changes []store.Change, initial, sen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Like applyChanges, but for notify, with configurable mailboxes to notify about,
|
// xapplyChangesNotify is like xapplyChanges, but for NOTIFY, with configurable
|
||||||
// and configurable events to send, including which fetch attributes to return.
|
// mailboxes to notify about, and configurable events to send, including which
|
||||||
// All calls must go through applyChanges, for overflow handling.
|
// fetch attributes to return. All calls must go through xapplyChanges, for overflow
|
||||||
|
// handling.
|
||||||
func (c *conn) xapplyChangesNotify(changes []store.Change, sendDelayed bool) {
|
func (c *conn) xapplyChangesNotify(changes []store.Change, sendDelayed bool) {
|
||||||
if sendDelayed && len(c.notify.Delayed) > 0 {
|
if sendDelayed && len(c.notify.Delayed) > 0 {
|
||||||
changes = append(c.notify.Delayed, changes...)
|
changes = append(c.notify.Delayed, changes...)
|
||||||
@ -3240,14 +3219,13 @@ func (c *conn) cmdSelectExamine(isselect bool, tag, cmd string, p *parser) {
|
|||||||
|
|
||||||
name = xcheckmailboxname(name, true)
|
name = xcheckmailboxname(name, true)
|
||||||
|
|
||||||
var highestModSeq store.ModSeq
|
|
||||||
var highDeletedModSeq store.ModSeq
|
|
||||||
var firstUnseen msgseq = 0
|
|
||||||
var mb store.Mailbox
|
var mb store.Mailbox
|
||||||
c.account.WithRLock(func() {
|
c.account.WithRLock(func() {
|
||||||
c.xdbread(func(tx *bstore.Tx) {
|
c.xdbread(func(tx *bstore.Tx) {
|
||||||
mb = c.xmailbox(tx, name, "")
|
mb = c.xmailbox(tx, name, "")
|
||||||
|
|
||||||
|
var firstUnseen msgseq = 0
|
||||||
|
|
||||||
c.uidnext = mb.UIDNext
|
c.uidnext = mb.UIDNext
|
||||||
if c.uidonly {
|
if c.uidonly {
|
||||||
c.exists = uint32(mb.MailboxCounts.Total + mb.MailboxCounts.Deleted)
|
c.exists = uint32(mb.MailboxCounts.Total + mb.MailboxCounts.Deleted)
|
||||||
@ -3270,217 +3248,200 @@ func (c *conn) cmdSelectExamine(isselect bool, tag, cmd string, p *parser) {
|
|||||||
c.exists = uint32(len(c.uids))
|
c.exists = uint32(len(c.uids))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Condstore extension, find the highest modseq.
|
var flags string
|
||||||
|
if len(mb.Keywords) > 0 {
|
||||||
|
flags = " " + strings.Join(mb.Keywords, " ")
|
||||||
|
}
|
||||||
|
c.xbwritelinef(`* FLAGS (\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent%s)`, flags)
|
||||||
|
c.xbwritelinef(`* OK [PERMANENTFLAGS (\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent \*)] x`)
|
||||||
|
if !c.enabled[capIMAP4rev2] {
|
||||||
|
c.xbwritelinef(`* 0 RECENT`)
|
||||||
|
}
|
||||||
|
c.xbwritelinef(`* %d EXISTS`, c.exists)
|
||||||
|
if !c.enabled[capIMAP4rev2] && firstUnseen > 0 {
|
||||||
|
// ../rfc/9051:8051 ../rfc/3501:1774
|
||||||
|
c.xbwritelinef(`* OK [UNSEEN %d] x`, firstUnseen)
|
||||||
|
}
|
||||||
|
c.xbwritelinef(`* OK [UIDVALIDITY %d] x`, mb.UIDValidity)
|
||||||
|
c.xbwritelinef(`* OK [UIDNEXT %d] x`, mb.UIDNext)
|
||||||
|
c.xbwritelinef(`* LIST () "/" %s`, mailboxt(mb.Name).pack(c))
|
||||||
if c.enabled[capCondstore] {
|
if c.enabled[capCondstore] {
|
||||||
highestModSeq = mb.ModSeq
|
// ../rfc/7162:417
|
||||||
|
// ../rfc/7162-eid5055 ../rfc/7162:484 ../rfc/7162:1167
|
||||||
|
c.xbwritelinef(`* OK [HIGHESTMODSEQ %d] x`, mb.ModSeq.Client())
|
||||||
}
|
}
|
||||||
// For QRESYNC, we need to know the highest modset of deleted expunged records to
|
|
||||||
// maintain synchronization.
|
|
||||||
if c.enabled[capQresync] {
|
|
||||||
var err error
|
|
||||||
highDeletedModSeq, err = c.account.HighestDeletedModSeq(tx)
|
|
||||||
xcheckf(err, "getting highest deleted modseq")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
overflow, changes := c.comm.Get()
|
// If QRESYNC uidvalidity matches, we send any changes. ../rfc/7162:1509
|
||||||
c.xapplyChanges(overflow, changes, true, false)
|
if qruidvalidity == mb.UIDValidity {
|
||||||
|
// We send the vanished UIDs at the end, so we can easily combine the modseq
|
||||||
|
// changes and vanished UIDs that result from that, with the vanished UIDs from the
|
||||||
|
// case where we don't store enough history.
|
||||||
|
vanishedUIDs := map[store.UID]struct{}{}
|
||||||
|
|
||||||
var flags string
|
var preVanished store.UID
|
||||||
if len(mb.Keywords) > 0 {
|
var oldClientUID store.UID
|
||||||
flags = " " + strings.Join(mb.Keywords, " ")
|
// If samples of known msgseq and uid pairs are given (they must be in order), we
|
||||||
}
|
// use them to determine the earliest UID for which we send VANISHED responses.
|
||||||
c.xbwritelinef(`* FLAGS (\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent%s)`, flags)
|
// ../rfc/7162:1579
|
||||||
c.xbwritelinef(`* OK [PERMANENTFLAGS (\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent \*)] x`)
|
if qrknownSeqSet != nil {
|
||||||
if !c.enabled[capIMAP4rev2] {
|
if !qrknownSeqSet.isBasicIncreasing() {
|
||||||
c.xbwritelinef(`* 0 RECENT`)
|
xuserErrorf("QRESYNC known message sequence set must be numeric and strictly increasing")
|
||||||
}
|
}
|
||||||
c.xbwritelinef(`* %d EXISTS`, c.exists)
|
if !qrknownUIDSet.isBasicIncreasing() {
|
||||||
if !c.enabled[capIMAP4rev2] && firstUnseen > 0 {
|
xuserErrorf("QRESYNC known uid set must be numeric and strictly increasing")
|
||||||
// ../rfc/9051:8051 ../rfc/3501:1774
|
}
|
||||||
c.xbwritelinef(`* OK [UNSEEN %d] x`, firstUnseen)
|
seqiter := qrknownSeqSet.newIter()
|
||||||
}
|
uiditer := qrknownUIDSet.newIter()
|
||||||
c.xbwritelinef(`* OK [UIDVALIDITY %d] x`, mb.UIDValidity)
|
for {
|
||||||
c.xbwritelinef(`* OK [UIDNEXT %d] x`, mb.UIDNext)
|
msgseq, ok0 := seqiter.Next()
|
||||||
c.xbwritelinef(`* LIST () "/" %s`, mailboxt(mb.Name).pack(c))
|
uid, ok1 := uiditer.Next()
|
||||||
if c.enabled[capCondstore] {
|
if !ok0 && !ok1 {
|
||||||
// ../rfc/7162:417
|
break
|
||||||
// ../rfc/7162-eid5055 ../rfc/7162:484 ../rfc/7162:1167
|
} else if !ok0 || !ok1 {
|
||||||
c.xbwritelinef(`* OK [HIGHESTMODSEQ %d] x`, highestModSeq.Client())
|
xsyntaxErrorf("invalid combination of known sequence set and uid set, must be of equal length")
|
||||||
}
|
}
|
||||||
|
i := int(msgseq - 1)
|
||||||
// If QRESYNC uidvalidity matches, we send any changes. ../rfc/7162:1509
|
// Access to c.uids is safe, qrknownSeqSet and uidonly cannot both be set.
|
||||||
if qruidvalidity == mb.UIDValidity {
|
if i < 0 || i >= int(c.exists) || c.uids[i] != store.UID(uid) {
|
||||||
// We send the vanished UIDs at the end, so we can easily combine the modseq
|
if uidSearch(c.uids, store.UID(uid)) <= 0 {
|
||||||
// changes and vanished UIDs that result from that, with the vanished UIDs from the
|
// We will check this old client UID for consistency below.
|
||||||
// case where we don't store enough history.
|
oldClientUID = store.UID(uid)
|
||||||
vanishedUIDs := map[store.UID]struct{}{}
|
}
|
||||||
|
break
|
||||||
var preVanished store.UID
|
}
|
||||||
var oldClientUID store.UID
|
preVanished = store.UID(uid + 1)
|
||||||
// If samples of known msgseq and uid pairs are given (they must be in order), we
|
|
||||||
// use them to determine the earliest UID for which we send VANISHED responses.
|
|
||||||
// ../rfc/7162:1579
|
|
||||||
if qrknownSeqSet != nil {
|
|
||||||
if !qrknownSeqSet.isBasicIncreasing() {
|
|
||||||
xuserErrorf("QRESYNC known message sequence set must be numeric and strictly increasing")
|
|
||||||
}
|
|
||||||
if !qrknownUIDSet.isBasicIncreasing() {
|
|
||||||
xuserErrorf("QRESYNC known uid set must be numeric and strictly increasing")
|
|
||||||
}
|
|
||||||
seqiter := qrknownSeqSet.newIter()
|
|
||||||
uiditer := qrknownUIDSet.newIter()
|
|
||||||
for {
|
|
||||||
msgseq, ok0 := seqiter.Next()
|
|
||||||
uid, ok1 := uiditer.Next()
|
|
||||||
if !ok0 && !ok1 {
|
|
||||||
break
|
|
||||||
} else if !ok0 || !ok1 {
|
|
||||||
xsyntaxErrorf("invalid combination of known sequence set and uid set, must be of equal length")
|
|
||||||
}
|
|
||||||
i := int(msgseq - 1)
|
|
||||||
// Access to c.uids is safe, qrknownSeqSet and uidonly cannot both be set.
|
|
||||||
if i < 0 || i >= int(c.exists) || c.uids[i] != store.UID(uid) {
|
|
||||||
if uidSearch(c.uids, store.UID(uid)) <= 0 {
|
|
||||||
// We will check this old client UID for consistency below.
|
|
||||||
oldClientUID = store.UID(uid)
|
|
||||||
}
|
}
|
||||||
break
|
|
||||||
}
|
}
|
||||||
preVanished = store.UID(uid + 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We gather vanished UIDs and report them at the end. This seems OK because we
|
// We gather vanished UIDs and report them at the end. This seems OK because we
|
||||||
// already sent HIGHESTMODSEQ, and a client should know not to commit that value
|
// already sent HIGHESTMODSEQ, and a client should know not to commit that value
|
||||||
// until after it has seen the tagged OK of this command. The RFC has a remark
|
// until after it has seen the tagged OK of this command. The RFC has a remark
|
||||||
// about ordering of some untagged responses, it's not immediately clear what it
|
// about ordering of some untagged responses, it's not immediately clear what it
|
||||||
// means, but given the examples appears to allude to servers that decide to not
|
// means, but given the examples appears to allude to servers that decide to not
|
||||||
// send expunge/vanished before the tagged OK.
|
// send expunge/vanished before the tagged OK.
|
||||||
// ../rfc/7162:1340
|
// ../rfc/7162:1340
|
||||||
|
|
||||||
|
if oldClientUID > 0 {
|
||||||
|
// The client sent a UID that is now removed. This is typically fine. But we check
|
||||||
|
// that it is consistent with the modseq the client sent. If the UID already didn't
|
||||||
|
// exist at that modseq, the client may be missing some information.
|
||||||
|
q := bstore.QueryTx[store.Message](tx)
|
||||||
|
q.FilterNonzero(store.Message{MailboxID: mb.ID, UID: oldClientUID})
|
||||||
|
m, err := q.Get()
|
||||||
|
if err == nil {
|
||||||
|
// If client claims to be up to date up to and including qrmodseq, and the message
|
||||||
|
// was deleted at or before that time, we send changes from just before that
|
||||||
|
// modseq, and we send vanished for all UIDs.
|
||||||
|
if m.Expunged && qrmodseq >= m.ModSeq.Client() {
|
||||||
|
qrmodseq = m.ModSeq.Client() - 1
|
||||||
|
preVanished = 0
|
||||||
|
qrknownUIDs = nil
|
||||||
|
c.xbwritelinef("* OK [ALERT] Synchronization inconsistency in client detected. Client tried to sync with a UID that was removed at or after the MODSEQ it sent in the request. Sending all historic message removals for selected mailbox. Full synchronization recommended.")
|
||||||
|
}
|
||||||
|
} else if err != bstore.ErrAbsent {
|
||||||
|
xcheckf(err, "checking old client uid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// We are reading without account lock. Similar to when we process FETCH/SEARCH
|
|
||||||
// requests. We don't have to reverify existence of the mailbox, so we don't
|
|
||||||
// rlock, even briefly.
|
|
||||||
c.xdbread(func(tx *bstore.Tx) {
|
|
||||||
if oldClientUID > 0 {
|
|
||||||
// The client sent a UID that is now removed. This is typically fine. But we check
|
|
||||||
// that it is consistent with the modseq the client sent. If the UID already didn't
|
|
||||||
// exist at that modseq, the client may be missing some information.
|
|
||||||
q := bstore.QueryTx[store.Message](tx)
|
q := bstore.QueryTx[store.Message](tx)
|
||||||
q.FilterNonzero(store.Message{MailboxID: mb.ID, UID: oldClientUID})
|
q.FilterNonzero(store.Message{MailboxID: mb.ID})
|
||||||
m, err := q.Get()
|
// Note: we don't filter by Expunged.
|
||||||
if err == nil {
|
q.FilterGreater("ModSeq", store.ModSeqFromClient(qrmodseq))
|
||||||
// If client claims to be up to date up to and including qrmodseq, and the message
|
q.FilterLessEqual("ModSeq", mb.ModSeq)
|
||||||
// was deleted at or before that time, we send changes from just before that
|
q.FilterLess("UID", c.uidnext)
|
||||||
// modseq, and we send vanished for all UIDs.
|
q.SortAsc("ModSeq")
|
||||||
if m.Expunged && qrmodseq >= m.ModSeq.Client() {
|
err := q.ForEach(func(m store.Message) error {
|
||||||
qrmodseq = m.ModSeq.Client() - 1
|
if m.Expunged && m.UID < preVanished {
|
||||||
preVanished = 0
|
return nil
|
||||||
qrknownUIDs = nil
|
}
|
||||||
c.xbwritelinef("* OK [ALERT] Synchronization inconsistency in client detected. Client tried to sync with a UID that was removed at or after the MODSEQ it sent in the request. Sending all historic message removals for selected mailbox. Full synchronization recommended.")
|
// If known UIDs was specified, we only report about those UIDs. ../rfc/7162:1523
|
||||||
|
if qrknownUIDs != nil && !qrknownUIDs.contains(uint32(m.UID)) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if m.Expunged {
|
||||||
|
vanishedUIDs[m.UID] = struct{}{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// UIDFETCH in case of uidonly. ../rfc/9586:228
|
||||||
|
if c.uidonly {
|
||||||
|
c.xbwritelinef("* %d UIDFETCH (FLAGS %s MODSEQ (%d))", m.UID, flaglist(m.Flags, m.Keywords).pack(c), m.ModSeq.Client())
|
||||||
|
} else if msgseq := c.sequence(m.UID); msgseq > 0 {
|
||||||
|
c.xbwritelinef("* %d FETCH (UID %d FLAGS %s MODSEQ (%d))", msgseq, m.UID, flaglist(m.Flags, m.Keywords).pack(c), m.ModSeq.Client())
|
||||||
}
|
}
|
||||||
} else if err != bstore.ErrAbsent {
|
|
||||||
xcheckf(err, "checking old client uid")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
q := bstore.QueryTx[store.Message](tx)
|
|
||||||
q.FilterNonzero(store.Message{MailboxID: mb.ID})
|
|
||||||
// Note: we don't filter by Expunged.
|
|
||||||
q.FilterGreater("ModSeq", store.ModSeqFromClient(qrmodseq))
|
|
||||||
q.FilterLessEqual("ModSeq", highestModSeq)
|
|
||||||
q.FilterLess("UID", c.uidnext)
|
|
||||||
q.SortAsc("ModSeq")
|
|
||||||
err := q.ForEach(func(m store.Message) error {
|
|
||||||
if m.Expunged && m.UID < preVanished {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
})
|
||||||
// If known UIDs was specified, we only report about those UIDs. ../rfc/7162:1523
|
xcheckf(err, "listing changed messages")
|
||||||
if qrknownUIDs != nil && !qrknownUIDs.contains(uint32(m.UID)) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if m.Expunged {
|
|
||||||
vanishedUIDs[m.UID] = struct{}{}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// UIDFETCH in case of uidonly. ../rfc/9586:228
|
|
||||||
if c.uidonly {
|
|
||||||
c.xbwritelinef("* %d UIDFETCH (FLAGS %s MODSEQ (%d))", m.UID, flaglist(m.Flags, m.Keywords).pack(c), m.ModSeq.Client())
|
|
||||||
} else if msgseq := c.sequence(m.UID); msgseq > 0 {
|
|
||||||
c.xbwritelinef("* %d FETCH (UID %d FLAGS %s MODSEQ (%d))", msgseq, m.UID, flaglist(m.Flags, m.Keywords).pack(c), m.ModSeq.Client())
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
xcheckf(err, "listing changed messages")
|
|
||||||
|
|
||||||
// If we don't have enough history, we go through all UIDs and look them up, and
|
highDeletedModSeq, err := c.account.HighestDeletedModSeq(tx)
|
||||||
// add them to the vanished list if they have disappeared.
|
xcheckf(err, "getting highest deleted modseq")
|
||||||
if qrmodseq < highDeletedModSeq.Client() {
|
|
||||||
// If no "known uid set" was in the request, we substitute 1:max or the empty set.
|
|
||||||
// ../rfc/7162:1524
|
|
||||||
if qrknownUIDs == nil {
|
|
||||||
qrknownUIDs = &numSet{ranges: []numRange{{first: setNumber{number: 1}, last: &setNumber{number: uint32(c.uidnext - 1)}}}}
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.uidonly {
|
// If we don't have enough history, we go through all UIDs and look them up, and
|
||||||
// note: qrknownUIDs will not contain "*".
|
// add them to the vanished list if they have disappeared.
|
||||||
for _, r := range qrknownUIDs.xinterpretStar(func() store.UID { return 0 }).ranges {
|
if qrmodseq < highDeletedModSeq.Client() {
|
||||||
// Gather UIDs for this range.
|
// If no "known uid set" was in the request, we substitute 1:max or the empty set.
|
||||||
var uids []store.UID
|
// ../rfc/7162:1524
|
||||||
q := bstore.QueryTx[store.Message](tx)
|
if qrknownUIDs == nil {
|
||||||
q.FilterNonzero(store.Message{MailboxID: mb.ID})
|
qrknownUIDs = &numSet{ranges: []numRange{{first: setNumber{number: 1}, last: &setNumber{number: uint32(c.uidnext - 1)}}}}
|
||||||
q.FilterEqual("Expunged", false)
|
}
|
||||||
if r.last == nil {
|
|
||||||
q.FilterEqual("UID", r.first.number)
|
if c.uidonly {
|
||||||
} else {
|
// note: qrknownUIDs will not contain "*".
|
||||||
q.FilterGreaterEqual("UID", r.first.number)
|
for _, r := range qrknownUIDs.xinterpretStar(func() store.UID { return 0 }).ranges {
|
||||||
q.FilterLessEqual("UID", r.last.number)
|
// Gather UIDs for this range.
|
||||||
|
var uids []store.UID
|
||||||
|
q := bstore.QueryTx[store.Message](tx)
|
||||||
|
q.FilterNonzero(store.Message{MailboxID: mb.ID})
|
||||||
|
q.FilterEqual("Expunged", false)
|
||||||
|
if r.last == nil {
|
||||||
|
q.FilterEqual("UID", r.first.number)
|
||||||
|
} else {
|
||||||
|
q.FilterGreaterEqual("UID", r.first.number)
|
||||||
|
q.FilterLessEqual("UID", r.last.number)
|
||||||
|
}
|
||||||
|
q.SortAsc("UID")
|
||||||
|
for m, err := range q.All() {
|
||||||
|
xcheckf(err, "enumerating uids")
|
||||||
|
uids = append(uids, m.UID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find UIDs missing from the database.
|
||||||
|
iter := r.newIter()
|
||||||
|
for {
|
||||||
|
uid, ok := iter.Next()
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if uidSearch(uids, store.UID(uid)) <= 0 {
|
||||||
|
vanishedUIDs[store.UID(uid)] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
q.SortAsc("UID")
|
} else {
|
||||||
for m, err := range q.All() {
|
// Ensure it is in ascending order, no needless first/last ranges. qrknownUIDs cannot contain a star.
|
||||||
xcheckf(err, "enumerating uids")
|
iter := qrknownUIDs.newIter()
|
||||||
uids = append(uids, m.UID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find UIDs missing from the database.
|
|
||||||
iter := r.newIter()
|
|
||||||
for {
|
for {
|
||||||
uid, ok := iter.Next()
|
v, ok := iter.Next()
|
||||||
if !ok {
|
if !ok {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if uidSearch(uids, store.UID(uid)) <= 0 {
|
if c.sequence(store.UID(v)) <= 0 {
|
||||||
vanishedUIDs[store.UID(uid)] = struct{}{}
|
vanishedUIDs[store.UID(v)] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
// Ensure it is in ascending order, no needless first/last ranges. qrknownUIDs cannot contain a star.
|
|
||||||
iter := qrknownUIDs.newIter()
|
// Now that we have all vanished UIDs, send them over compactly.
|
||||||
for {
|
if len(vanishedUIDs) > 0 {
|
||||||
v, ok := iter.Next()
|
l := slices.Sorted(maps.Keys(vanishedUIDs))
|
||||||
if !ok {
|
// ../rfc/7162:1985
|
||||||
break
|
for _, s := range compactUIDSet(l).Strings(4*1024 - 32) {
|
||||||
}
|
c.xbwritelinef("* VANISHED (EARLIER) %s", s)
|
||||||
if c.sequence(store.UID(v)) <= 0 {
|
|
||||||
vanishedUIDs[store.UID(v)] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
})
|
||||||
// Now that we have all vanished UIDs, send them over compactly.
|
|
||||||
if len(vanishedUIDs) > 0 {
|
|
||||||
l := slices.Sorted(maps.Keys(vanishedUIDs))
|
|
||||||
// ../rfc/7162:1985
|
|
||||||
for _, s := range compactUIDSet(l).Strings(4*1024 - 32) {
|
|
||||||
c.xbwritelinef("* VANISHED (EARLIER) %s", s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if isselect {
|
if isselect {
|
||||||
c.xbwriteresultf("%s OK [READ-WRITE] x", tag)
|
c.xbwriteresultf("%s OK [READ-WRITE] x", tag)
|
||||||
@ -4242,7 +4203,7 @@ func (c *conn) cmdAppend(tag, cmd string, p *parser) {
|
|||||||
if c.mailboxID == mb.ID {
|
if c.mailboxID == mb.ID {
|
||||||
l := pendingChanges
|
l := pendingChanges
|
||||||
pendingChanges = nil
|
pendingChanges = nil
|
||||||
c.xapplyChanges(overflow, l, false, true)
|
c.xapplyChanges(overflow, l, true)
|
||||||
for _, a := range appends {
|
for _, a := range appends {
|
||||||
c.uidAppend(a.m.UID)
|
c.uidAppend(a.m.UID)
|
||||||
}
|
}
|
||||||
@ -4277,7 +4238,7 @@ func (c *conn) cmdIdle(tag, cmd string, p *parser) {
|
|||||||
|
|
||||||
// With NOTIFY enabled, flush all pending changes.
|
// With NOTIFY enabled, flush all pending changes.
|
||||||
if c.notify != nil && len(c.notify.Delayed) > 0 {
|
if c.notify != nil && len(c.notify.Delayed) > 0 {
|
||||||
c.xapplyChanges(false, nil, false, true)
|
c.xapplyChanges(false, nil, true)
|
||||||
c.xflush()
|
c.xflush()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4303,7 +4264,7 @@ Wait:
|
|||||||
break Wait
|
break Wait
|
||||||
case <-c.comm.Pending:
|
case <-c.comm.Pending:
|
||||||
overflow, changes := c.comm.Get()
|
overflow, changes := c.comm.Get()
|
||||||
c.xapplyChanges(overflow, changes, false, true)
|
c.xapplyChanges(overflow, changes, true)
|
||||||
c.xflush()
|
c.xflush()
|
||||||
case <-mox.Shutdown.Done():
|
case <-mox.Shutdown.Done():
|
||||||
// ../rfc/9051:5375
|
// ../rfc/9051:5375
|
||||||
|
Loading…
x
Reference in New Issue
Block a user