imapserver: fix spurious test failure due to recently added account consistency check

By removing message file while holding the account wlock. We were seeing
messages that weren't removed yet.
This commit is contained in:
Mechiel Lukkien 2025-02-26 18:33:01 +01:00
parent f235b6ad83
commit d7bd50b5a5
No known key found for this signature in database

View File

@ -3752,22 +3752,9 @@ func (c *conn) cmdClose(tag, cmd string, p *parser) {
// Request syntax: ../rfc/9051:6476 ../rfc/3501:4679 // Request syntax: ../rfc/9051:6476 ../rfc/3501:4679
p.xempty() p.xempty()
if c.readonly { if !c.readonly {
c.unselect() c.xexpunge(nil, true)
c.ok(tag, cmd)
return
} }
remove, _ := c.xexpunge(nil, true)
defer func() {
for _, m := range remove {
p := c.account.MessagePath(m.ID)
err := os.Remove(p)
c.xsanity(err, "removing message file for expunge for close")
}
}()
c.unselect() c.unselect()
c.ok(tag, cmd) c.ok(tag, cmd)
} }
@ -3775,13 +3762,13 @@ func (c *conn) cmdClose(tag, cmd string, p *parser) {
// expunge messages marked for deletion in currently selected/active mailbox. // expunge messages marked for deletion in currently selected/active mailbox.
// if uidSet is not nil, only messages matching the set are deleted. // if uidSet is not nil, only messages matching the set are deleted.
// //
// messages that have been marked expunged from the database are returned, but the // messages that have been marked expunged from the database are returned and
// corresponding files still have to be removed. // have already been removed.
// //
// the highest modseq in the mailbox is returned, typically associated with the // the highest modseq in the mailbox is returned, typically associated with the
// removal of the messages, but if no messages were expunged the current latest max // removal of the messages, but if no messages were expunged the current latest max
// modseq for the mailbox is returned. // modseq for the mailbox is returned.
func (c *conn) xexpunge(uidSet *numSet, missingMailboxOK bool) (remove []store.Message, highestModSeq store.ModSeq) { func (c *conn) xexpunge(uidSet *numSet, missingMailboxOK bool) (removed []store.Message, highestModSeq store.ModSeq) {
var modseq store.ModSeq var modseq store.ModSeq
c.account.WithWLock(func() { c.account.WithWLock(func() {
@ -3806,10 +3793,10 @@ func (c *conn) xexpunge(uidSet *numSet, missingMailboxOK bool) (remove []store.M
return uidSearch(c.uids, m.UID) > 0 && (uidSet == nil || uidSet.containsUID(m.UID, c.uids, c.searchResult)) return uidSearch(c.uids, m.UID) > 0 && (uidSet == nil || uidSet.containsUID(m.UID, c.uids, c.searchResult))
}) })
qm.SortAsc("UID") qm.SortAsc("UID")
remove, err = qm.List() removed, err = qm.List()
xcheckf(err, "listing messages to delete") xcheckf(err, "listing messages to delete")
if len(remove) == 0 { if len(removed) == 0 {
highestModSeq = mb.ModSeq highestModSeq = mb.ModSeq
return return
} }
@ -3820,17 +3807,17 @@ func (c *conn) xexpunge(uidSet *numSet, missingMailboxOK bool) (remove []store.M
highestModSeq = modseq highestModSeq = modseq
mb.ModSeq = modseq mb.ModSeq = modseq
removeIDs := make([]int64, len(remove)) removeIDs := make([]int64, len(removed))
anyIDs := make([]any, len(remove)) anyIDs := make([]any, len(removed))
var totalSize int64 var totalSize int64
for i, m := range remove { for i, m := range removed {
removeIDs[i] = m.ID removeIDs[i] = m.ID
anyIDs[i] = m.ID anyIDs[i] = m.ID
mb.Sub(m.MailboxCounts()) mb.Sub(m.MailboxCounts())
totalSize += m.Size totalSize += m.Size
// Update "remove", because RetrainMessage below will save the message. // Update "remove", because RetrainMessage below will save the message.
remove[i].Expunged = true removed[i].Expunged = true
remove[i].ModSeq = modseq removed[i].ModSeq = modseq
} }
qmr := bstore.QueryTx[store.Recipient](tx) qmr := bstore.QueryTx[store.Recipient](tx)
qmr.FilterEqual("MessageID", anyIDs...) qmr.FilterEqual("MessageID", anyIDs...)
@ -3853,19 +3840,19 @@ func (c *conn) xexpunge(uidSet *numSet, missingMailboxOK bool) (remove []store.M
// Mark expunged messages as not needing training, then retrain them, so if they // Mark expunged messages as not needing training, then retrain them, so if they
// were trained, they get untrained. // were trained, they get untrained.
for i := range remove { for i := range removed {
remove[i].Junk = false removed[i].Junk = false
remove[i].Notjunk = false removed[i].Notjunk = false
} }
err = c.account.RetrainMessages(context.TODO(), c.log, tx, remove, true) err = c.account.RetrainMessages(context.TODO(), c.log, tx, removed, true)
xcheckf(err, "untraining expunged messages") xcheckf(err, "untraining expunged messages")
}) })
// Broadcast changes to other connections. We may not have actually removed any // Broadcast changes to other connections. We may not have actually removed any
// messages, so take care not to send an empty update. // messages, so take care not to send an empty update.
if len(remove) > 0 { if len(removed) > 0 {
ouids := make([]store.UID, len(remove)) ouids := make([]store.UID, len(removed))
for i, m := range remove { for i, m := range removed {
ouids[i] = m.UID ouids[i] = m.UID
} }
changes := []store.Change{ changes := []store.Change{
@ -3874,8 +3861,14 @@ func (c *conn) xexpunge(uidSet *numSet, missingMailboxOK bool) (remove []store.M
} }
c.broadcast(changes) c.broadcast(changes)
} }
for _, m := range removed {
p := c.account.MessagePath(m.ID)
err := os.Remove(p)
c.xsanity(err, "removing message file for expunge")
}
}) })
return remove, highestModSeq return removed, highestModSeq
} }
// Unselect is similar to close in that it closes the currently active mailbox, but // Unselect is similar to close in that it closes the currently active mailbox, but
@ -3935,20 +3928,12 @@ func (c *conn) cmdUIDExpunge(tag, cmd string, p *parser) {
func (c *conn) cmdxExpunge(tag, cmd string, uidSet *numSet) { func (c *conn) cmdxExpunge(tag, cmd string, uidSet *numSet) {
// Command: ../rfc/9051:3687 ../rfc/3501:2695 // Command: ../rfc/9051:3687 ../rfc/3501:2695
remove, highestModSeq := c.xexpunge(uidSet, false) removed, highestModSeq := c.xexpunge(uidSet, false)
defer func() {
for _, m := range remove {
p := c.account.MessagePath(m.ID)
err := os.Remove(p)
c.xsanity(err, "removing message file for expunge")
}
}()
// Response syntax: ../rfc/9051:6742 ../rfc/3501:4864 // Response syntax: ../rfc/9051:6742 ../rfc/3501:4864
var vanishedUIDs numSet var vanishedUIDs numSet
qresync := c.enabled[capQresync] qresync := c.enabled[capQresync]
for _, m := range remove { for _, m := range removed {
seq := c.xsequence(m.UID) seq := c.xsequence(m.UID)
c.sequenceRemove(seq, m.UID) c.sequenceRemove(seq, m.UID)
if qresync { if qresync {