diff --git a/imapserver/condstore_test.go b/imapserver/condstore_test.go index e00b88a..9f4965e 100644 --- a/imapserver/condstore_test.go +++ b/imapserver/condstore_test.go @@ -37,12 +37,12 @@ func testCondstoreQresync(t *testing.T, qresync bool) { tc.client.Login("mjl@mox.example", password0) tc.client.Enable(capability) tc.transactf("ok", "Select inbox") - tc.xuntaggedOpt(false, imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(1), More: "x"}}) + tc.xuntaggedOpt(false, imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(2), More: "x"}}) // First some tests without any messages. tc.transactf("ok", "Status inbox (Highestmodseq)") - tc.xuntagged(imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusHighestModSeq: 1}}) + tc.xuntagged(imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusHighestModSeq: 2}}) // No messages, no matches. tc.transactf("ok", "Uid Fetch 1:* (Flags) (Changedsince 12345)") @@ -111,12 +111,13 @@ func testCondstoreQresync(t *testing.T, qresync bool) { tc3.client.Enable(capability) tc3.client.Select("inbox") - var clientModseq int64 = 1 // We track the client-side modseq for inbox. Not a store.ModSeq. + var clientModseq int64 = 2 // We track the client-side modseq for inbox. Not a store.ModSeq. // Add messages to: inbox, otherbox, inbox, inbox. // We have these messages in order of modseq: 2+1 in inbox, 1 in otherbox, 2 in inbox. // The original two in inbox appear to have modseq 1 (with 0 stored in the database). - // The ones we insert below will start with modseq 2. So we'll have modseq 1-5. + // Creation of otherbox got modseq 2. + // The ones we insert below will start with modseq 3. So we'll have messages with modseq 1 and 3-6. tc.transactf("ok", "Append inbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx") tc.xuntagged(imapclient.UntaggedExists(4)) tc.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UID: 4}) @@ -154,23 +155,23 @@ func testCondstoreQresync(t *testing.T, qresync bool) { tc.transactf("bad", `Fetch 1 Flags (Changedsince 0)`) // 0 not allowed in syntax. mox.SetPedantic(false) tc.transactf("ok", "Uid fetch 1 (Flags) (Changedsince 0)") - tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(clientModseq)}}) - - clientModseq += 4 // Four messages, over two mailboxes, modseq is per account. + tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(1)}}) // Check highestmodseq for mailboxes. tc.transactf("ok", "Status inbox (highestmodseq)") - tc.xuntagged(imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusHighestModSeq: clientModseq}}) + tc.xuntagged(imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusHighestModSeq: clientModseq + 4}}) tc.transactf("ok", "Status otherbox (highestmodseq)") - tc.xuntagged(imapclient.UntaggedStatus{Mailbox: "otherbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusHighestModSeq: 3}}) + tc.xuntagged(imapclient.UntaggedStatus{Mailbox: "otherbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusHighestModSeq: clientModseq + 2}}) // Check highestmodseq when we select. tc.transactf("ok", "Examine otherbox") - tc.xuntaggedOpt(false, imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(3), More: "x"}}) + tc.xuntaggedOpt(false, imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(clientModseq + 2), More: "x"}}) tc.transactf("ok", "Select inbox") - tc.xuntaggedOpt(false, imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(clientModseq), More: "x"}}) + tc.xuntaggedOpt(false, imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(clientModseq + 4), More: "x"}}) + + clientModseq += 4 // Check fetch modseq response and changedsince. tc.transactf("ok", `Fetch 1 (Modseq)`) @@ -193,7 +194,7 @@ func testCondstoreQresync(t *testing.T, qresync bool) { tc.transactf("ok", `Fetch 1 Flags (Changedsince 1)`) tc.xuntagged() tc.transactf("ok", `Fetch 1,4 Flags (Changedsince 1)`) - tc.xuntagged(imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(4), noflags, imapclient.FetchModSeq(2)}}) + tc.xuntagged(imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(4), noflags, imapclient.FetchModSeq(3)}}) tc.transactf("ok", `Fetch 2 Flags (Changedsince 2)`) tc.xuntagged() @@ -308,25 +309,25 @@ func testCondstoreQresync(t *testing.T, qresync bool) { tc.transactf("ok", `Fetch 1:* (Modseq)`) tc.xuntagged( - imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchModSeq(7)}}, + imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchModSeq(8)}}, imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2), imapclient.FetchModSeq(1)}}, - imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), imapclient.FetchModSeq(4)}}, - imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), imapclient.FetchModSeq(5)}}, + imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), imapclient.FetchModSeq(5)}}, + imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), imapclient.FetchModSeq(6)}}, ) // Expunged messages, with higher modseq, should not show up. - tc.transactf("ok", "Uid Fetch 1:* (flags) (Changedsince 7)") + tc.transactf("ok", "Uid Fetch 1:* (flags) (Changedsince 8)") tc.xuntagged() // search - tc.transactf("ok", "Search Modseq 7") - tc.xsearchmodseq(7, 1) tc.transactf("ok", "Search Modseq 8") + tc.xsearchmodseq(8, 1) + tc.transactf("ok", "Search Modseq 9") tc.xsearch() // esearch - tc.transactf("ok", "Search Return (Min Max All) 1:* Modseq 7") - tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 1, All: esearchall0("1"), ModSeq: 7}) tc.transactf("ok", "Search Return (Min Max All) 1:* Modseq 8") + tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 1, All: esearchall0("1"), ModSeq: 8}) + tc.transactf("ok", "Search Return (Min Max All) 1:* Modseq 9") tc.xuntagged(imapclient.UntaggedEsearch{Correlator: tc.client.LastTag}) // store, cannot modify expunged messages. @@ -543,8 +544,8 @@ func testQresync(t *testing.T, tc *testconn, clientModseq int64) { noflags := imapclient.FetchFlags(nil) tc.xuntagged( imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")}, - imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(4)}}, - imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(7)}}, + imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(5)}}, + imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(8)}}, imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}}, ) @@ -596,8 +597,8 @@ func testQresync(t *testing.T, tc *testconn, clientModseq int64) { tc.xuntagged( makeUntagged( imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")}, - imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(4)}}, - imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(7)}}, + imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(5)}}, + imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(8)}}, imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}}, )..., ) @@ -613,8 +614,8 @@ func testQresync(t *testing.T, tc *testconn, clientModseq int64) { tc.xuntagged( makeUntagged( imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")}, - imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(4)}}, - imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(7)}}, + imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(5)}}, + imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(8)}}, imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}}, )..., ) @@ -624,8 +625,8 @@ func testQresync(t *testing.T, tc *testconn, clientModseq int64) { tc.transactf("ok", "Select inbox (Qresync (1 1 1,2,5:6))") tc.xuntagged( makeUntagged( - imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(4)}}, - imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(7)}}, + imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(5)}}, + imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(8)}}, imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}}, )..., ) @@ -635,7 +636,7 @@ func testQresync(t *testing.T, tc *testconn, clientModseq int64) { tc.transactf("ok", "Select inbox (Qresync (1 1 5))") tc.xuntagged( makeUntagged( - imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(4)}}, + imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(5)}}, )..., ) @@ -667,8 +668,8 @@ func testQresync(t *testing.T, tc *testconn, clientModseq int64) { tc.xuntagged( makeUntagged( imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")}, - imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(4)}}, - imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(7)}}, + imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(5)}}, + imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(8)}}, imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}}, )..., ) @@ -679,13 +680,13 @@ func testQresync(t *testing.T, tc *testconn, clientModseq int64) { tc.xuntagged( makeUntagged( imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")}, - imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(7)}}, + imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(8)}}, imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}}, )..., ) tc.transactf("ok", "Close") - tc.transactf("ok", "Select inbox (Qresync (1 8 (1,3,6 1,3,6)))") + tc.transactf("ok", "Select inbox (Qresync (1 9 (1,3,6 1,3,6)))") tc.xuntagged( makeUntagged( imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")}, @@ -697,7 +698,7 @@ func testQresync(t *testing.T, tc *testconn, clientModseq int64) { // since that time. Server detects this, sends full vanished history and continues // working with modseq changed to 1 before the expunged uid. tc.transactf("ok", "Close") - tc.transactf("ok", "Select inbox (Qresync (1 9 (1,3,6 1,3,6)))") + tc.transactf("ok", "Select inbox (Qresync (1 10 (1,3,6 1,3,6)))") tc.xuntagged( makeUntagged( imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "ALERT", More: "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."}}, diff --git a/imapserver/fetch.go b/imapserver/fetch.go index 9f23866..e550919 100644 --- a/imapserver/fetch.go +++ b/imapserver/fetch.go @@ -238,8 +238,9 @@ func (c *conn) cmdxFetch(isUID bool, tag, cmdstr string, p *parser) { } var zeromc store.MailboxCounts - if cmd.deltaCounts != zeromc { + if cmd.deltaCounts != zeromc || cmd.modseq != 0 { mb.Add(cmd.deltaCounts) // Unseen/Unread will be <= 0. + mb.ModSeq = cmd.modseq err := tx.Update(&mb) xcheckf(err, "updating mailbox counts") cmd.changes = append(cmd.changes, mb.ChangeCounts()) diff --git a/imapserver/metadata.go b/imapserver/metadata.go index 7c01f32..5b82a27 100644 --- a/imapserver/metadata.go +++ b/imapserver/metadata.go @@ -232,6 +232,7 @@ func (c *conn) cmdSetmetadata(tag, cmd string, p *parser) { // Store the annotations, possibly removing/inserting/updating them. c.account.WithWLock(func() { var changes []store.Change + var modseq store.ModSeq c.xdbwrite(func(tx *bstore.Tx) { var mb store.Mailbox // mb.ID as 0 is used in query below. @@ -256,7 +257,15 @@ func (c *conn) cmdSetmetadata(tag, cmd string, p *parser) { continue } + if modseq == 0 { + var err error + modseq, err = c.account.NextModSeq(tx) + xcheckf(err, "get next modseq") + } + a.MailboxID = mb.ID + a.ModSeq = modseq + a.CreateSeq = modseq oa, err := q.Get() if err == bstore.ErrAbsent { @@ -266,9 +275,7 @@ func (c *conn) cmdSetmetadata(tag, cmd string, p *parser) { continue } xcheckf(err, "looking up existing annotation for entry name") - if oa.IsString != a.IsString || (oa.Value == nil) != (a.Value == nil) || !bytes.Equal(oa.Value, a.Value) { - changes = append(changes, a.Change(mailboxName)) - } + changes = append(changes, a.Change(mailboxName)) oa.Value = a.Value err = tx.Update(&oa) xcheckf(err, "updating metadata annotation") @@ -293,6 +300,13 @@ func (c *conn) cmdSetmetadata(tag, cmd string, p *parser) { return nil }) xcheckf(err, "checking metadata annotation size") + + // ../rfc/7162:1335 + if mb.ID != 0 && modseq != 0 { + mb.ModSeq = modseq + err := tx.Update(&mb) + xcheckf(err, "updating mailbox with modseq") + } }) c.broadcast(changes) diff --git a/imapserver/server.go b/imapserver/server.go index bbe5a25..a1a0633 100644 --- a/imapserver/server.go +++ b/imapserver/server.go @@ -674,19 +674,6 @@ func (c *conn) xreadliteral(size int64, sync bool) []byte { return buf } -func (c *conn) xhighestModSeq(tx *bstore.Tx, mailboxID int64) store.ModSeq { - qms := bstore.QueryTx[store.Message](tx) - qms.FilterNonzero(store.Message{MailboxID: mailboxID}) - qms.SortDesc("ModSeq") - qms.Limit(1) - m, err := qms.Get() - if err == bstore.ErrAbsent { - return store.ModSeq(0) - } - xcheckf(err, "looking up highest modseq for mailbox") - return m.ModSeq -} - var cleanClose struct{} // Sentinel value for panic/recover indicating clean close of connection. // serve handles a single IMAP connection on nc. @@ -2461,15 +2448,16 @@ func (c *conn) xensureCondstore(tx *bstore.Tx) { if c.mailboxID <= 0 { return } - var modseq store.ModSeq - if tx != nil { - modseq = c.xhighestModSeq(tx, c.mailboxID) - } else { + + var mb store.Mailbox + if tx == nil { c.xdbread(func(tx *bstore.Tx) { - modseq = c.xhighestModSeq(tx, c.mailboxID) + mb = c.xmailboxID(tx, c.mailboxID) }) + } else { + mb = c.xmailboxID(tx, c.mailboxID) } - c.bwritelinef("* OK [HIGHESTMODSEQ %d] after condstore-enabling command", modseq.Client()) + c.bwritelinef("* OK [HIGHESTMODSEQ %d] after condstore-enabling command", mb.ModSeq.Client()) } } @@ -2591,7 +2579,7 @@ func (c *conn) cmdSelectExamine(isselect bool, tag, cmd string, p *parser) { // Condstore extension, find the highest modseq. if c.enabled[capCondstore] { - highestModSeq = c.xhighestModSeq(tx, mb.ID) + highestModSeq = mb.ModSeq } // For QRESYNC, we need to know the highest modset of deleted expunged records to // maintain synchronization. @@ -2935,6 +2923,8 @@ func (c *conn) cmdRename(tag, cmd string, p *parser) { c.xdbwrite(func(tx *bstore.Tx) { srcMB := c.xmailbox(tx, src, "NONEXISTENT") + var modseq store.ModSeq + // Inbox is very special. Unlike other mailboxes, its children are not moved. And // unlike a regular move, its messages are moved to a newly created mailbox. We do // indeed create a new destination mailbox and actually move the messages. @@ -2952,19 +2942,21 @@ func (c *conn) cmdRename(tag, cmd string, p *parser) { uidval, err := c.account.NextUIDValidity(tx) xcheckf(err, "next uid validity") + modseq, err = c.account.NextModSeq(tx) + xcheckf(err, "assigning next modseq") + dstMB := store.Mailbox{ Name: dst, UIDValidity: uidval, UIDNext: 1, Keywords: srcMB.Keywords, + ModSeq: modseq, + CreateSeq: modseq, HaveCounts: true, } err = tx.Insert(&dstMB) xcheckf(err, "create new destination mailbox") - modseq, err := c.account.NextModSeq(tx) - xcheckf(err, "assigning next modseq") - changes = make([]store.Change, 2) // Placeholders filled in below. // Move existing messages, with their ID's and on-disk files intact, to the new @@ -3007,6 +2999,7 @@ func (c *conn) cmdRename(tag, cmd string, p *parser) { err = tx.Update(&dstMB) xcheckf(err, "updating uidnext and counts in destination mailbox") + srcMB.ModSeq = modseq err = tx.Update(&srcMB) xcheckf(err, "updating counts for inbox") @@ -3021,12 +3014,14 @@ func (c *conn) cmdRename(tag, cmd string, p *parser) { for i := range annotations { annotations[i].ID = 0 annotations[i].MailboxID = dstMB.ID + annotations[i].ModSeq = modseq + annotations[i].CreateSeq = modseq err := tx.Insert(&annotations[i]) xcheckf(err, "copy annotation to destination mailbox") } changes[0] = store.ChangeRemoveUIDs{MailboxID: srcMB.ID, UIDs: oldUIDs, ModSeq: modseq} - changes[1] = store.ChangeAddMailbox{Mailbox: dstMB, Flags: dstFlags} + changes[1] = store.ChangeAddMailbox{Mailbox: dstMB, Flags: dstFlags, ModSeq: modseq} // changes[2:...] are ChangeAddUIDs changes = append(changes, srcMB.ChangeCounts(), dstMB.ChangeCounts()) for _, a := range annotations { @@ -3038,7 +3033,7 @@ func (c *conn) cmdRename(tag, cmd string, p *parser) { var notExists, alreadyExists bool var err error - changes, _, notExists, alreadyExists, err = c.account.MailboxRename(tx, srcMB, dst) + changes, _, notExists, alreadyExists, err = c.account.MailboxRename(tx, srcMB, dst, &modseq) if notExists { // ../rfc/9051:5140 xusercodeErrorf("NONEXISTENT", "%s", err) @@ -3265,7 +3260,7 @@ func (c *conn) xstatusLine(tx *bstore.Tx, mb store.Mailbox, attrs []string) stri status = append(status, A, "NIL") case "HIGHESTMODSEQ": // ../rfc/7162:366 - status = append(status, A, fmt.Sprintf("%d", c.xhighestModSeq(tx, mb.ID).Client())) + status = append(status, A, fmt.Sprintf("%d", mb.ModSeq.Client())) case "DELETED-STORAGE": // ../rfc/9208:394 // How much storage space could be reclaimed by expunging messages with the @@ -3660,7 +3655,7 @@ func (c *conn) xexpunge(uidSet *numSet, missingMailboxOK bool) (remove []store.M xcheckf(err, "listing messages to delete") if len(remove) == 0 { - highestModSeq = c.xhighestModSeq(tx, c.mailboxID) + highestModSeq = mb.ModSeq return } @@ -3668,6 +3663,7 @@ func (c *conn) xexpunge(uidSet *numSet, missingMailboxOK bool) (remove []store.M modseq, err = c.account.NextModSeq(tx) xcheckf(err, "assigning next modseq") highestModSeq = modseq + mb.ModSeq = modseq removeIDs := make([]int64, len(remove)) anyIDs := make([]any, len(remove)) @@ -3944,6 +3940,11 @@ func (c *conn) cmdxCopy(isUID bool, tag, cmd string, p *parser) { var err error modseq, err = c.account.NextModSeq(tx) xcheckf(err, "assigning next modseq") + mbSrc.ModSeq = modseq + mbDst.ModSeq = modseq + + err = tx.Update(&mbSrc) + xcheckf(err, "updating source mailbox for modseq") // Reserve the uids in the destination mailbox. uidFirst := mbDst.UIDNext @@ -4133,6 +4134,8 @@ func (c *conn) cmdxMove(isUID bool, tag, cmd string, p *parser) { var err error modseq, err = c.account.NextModSeq(tx) xcheckf(err, "assigning next modseq") + mbSrc.ModSeq = modseq + mbDst.ModSeq = modseq // Update existing record with new UID and MailboxID in database for messages. We // add a new but expunged record again in the original/source mailbox, for qresync. @@ -4202,7 +4205,7 @@ func (c *conn) cmdxMove(isUID bool, tag, cmd string, p *parser) { } err = tx.Update(&mbSrc) - xcheckf(err, "updating source mailbox counts") + xcheckf(err, "updating source mailbox counts and modseq") err = tx.Update(&mbDst) xcheckf(err, "updating destination mailbox for uids, keywords and counts") @@ -4416,7 +4419,8 @@ func (c *conn) cmdxStore(isUID bool, tag, cmd string, p *parser) { }) xcheckf(err, "storing flags in messages") - if mb.MailboxCounts != origmb.MailboxCounts { + if mb.MailboxCounts != origmb.MailboxCounts || modseq != 0 { + mb.ModSeq = modseq err := tx.Update(&mb) xcheckf(err, "updating mailbox counts") diff --git a/import.go b/import.go index 32f50ab..42aec56 100644 --- a/import.go +++ b/import.go @@ -298,7 +298,7 @@ func importctl(ctx context.Context, ctl *ctl, mbox bool) { a.WithWLock(func() { // Ensure mailbox exists. var mb store.Mailbox - mb, changes, err = a.MailboxEnsure(tx, mailbox, true, store.SpecialUse{}) + mb, changes, err = a.MailboxEnsure(tx, mailbox, true, store.SpecialUse{}, &modseq) ctl.xcheck(err, "ensuring mailbox exists") // We ensure keywords in messages make it to the mailbox as well. diff --git a/main.go b/main.go index 3a700e7..42854d6 100644 --- a/main.go +++ b/main.go @@ -3404,7 +3404,7 @@ open, or is not running. // Reassign UIDs, going per mailbox. We assign starting at 1, only changing the // message if it isn't already at the intended UID. Doing it in this order ensures // we don't get into trouble with duplicate UIDs for a mailbox. We assign a new - // modseq. Not strictly needed, for doesn't hurt. + // modseq. Not strictly needed, but doesn't hurt. modseq, err := a.NextModSeq(tx) xcheckf(err, "assigning next modseq") @@ -3429,7 +3429,7 @@ open, or is not running. return fmt.Errorf("reading through messages: %v", err) } - // Now update the uidnext and uidvalidity for each mailbox. + // Now update the uidnext, uidvalidity and modseq for each mailbox. err = bstore.QueryTx[store.Mailbox](tx).ForEach(func(mb store.Mailbox) error { // Assign each mailbox a completely new uidvalidity. uidvalidity, err := a.NextUIDValidity(tx) @@ -3449,6 +3449,7 @@ open, or is not running. mb.UIDValidity = uidvalidity } mb.UIDNext = uidlasts[mb.ID] + 1 + mb.ModSeq = modseq if err := tx.Update(&mb); err != nil { return fmt.Errorf("updating uidvalidity and uidnext for mailbox: %v", err) } diff --git a/store/account.go b/store/account.go index 0f91d19..f7eff1c 100644 --- a/store/account.go +++ b/store/account.go @@ -212,6 +212,11 @@ type Mailbox struct { // lower case (for JMAP), sorted. Keywords []string + // ModSeq matches that of last message (including deleted), or changes + // to mailbox such as after metadata changes. + ModSeq ModSeq + CreateSeq ModSeq + HaveCounts bool // Whether MailboxCounts have been initialized. MailboxCounts // Statistics about messages, kept up to date whenever a change happens. } @@ -230,11 +235,14 @@ type Annotation struct { IsString bool // If true, the value is a string instead of bytes. Value []byte + + ModSeq ModSeq + CreateSeq ModSeq } // Change returns a broadcastable change for the annotation. func (a Annotation) Change(mailboxName string) ChangeAnnotation { - return ChangeAnnotation{a.MailboxID, mailboxName, a.Key} + return ChangeAnnotation{a.MailboxID, mailboxName, a.Key, a.ModSeq} } // MailboxCounts tracks statistics about messages for a mailbox. @@ -293,7 +301,7 @@ func (mb *Mailbox) CalculateCounts(tx *bstore.Tx) (mc MailboxCounts, err error) // ChangeSpecialUse returns a change for special-use flags, for broadcasting to // other connections. func (mb Mailbox) ChangeSpecialUse() ChangeMailboxSpecialUse { - return ChangeMailboxSpecialUse{mb.ID, mb.Name, mb.SpecialUse} + return ChangeMailboxSpecialUse{mb.ID, mb.Name, mb.SpecialUse, mb.ModSeq} } // ChangeKeywords returns a change with new keywords for a mailbox (e.g. after @@ -872,8 +880,16 @@ type Account struct { } type Upgrade struct { - ID byte - Threads byte // 0: None, 1: Adding MessageID's completed, 2: Adding ThreadID's completed. + ID byte + Threads byte // 0: None, 1: Adding MessageID's completed, 2: Adding ThreadID's completed. + MailboxModSeq bool // Whether mailboxes have been assigned modseqs. +} + +// upgradeInit is the value to for new account database, that don't need any upgrading. +var upgradeInit = Upgrade{ + ID: 1, // Singleton. + Threads: 2, + MailboxModSeq: true, } // InitialUIDValidity returns a UIDValidity used for initializing an account. @@ -1039,6 +1055,56 @@ func OpenAccountDB(log mlog.Log, accountDir, accountName string) (a *Account, re if err != nil { return nil, fmt.Errorf("checking message threading: %v", err) } + + // Ensure all mailboxes have a modseq based on highest modseq message in each + // mailbox, and a creatseq. + if !up.MailboxModSeq { + log.Debug("upgrade: adding modseq to each mailbox") + err := acc.DB.Write(context.TODO(), func(tx *bstore.Tx) error { + var modseq ModSeq + + mbl, err := bstore.QueryTx[Mailbox](tx).List() + if err != nil { + return fmt.Errorf("listing mailboxes: %v", err) + } + for _, mb := range mbl { + // Get current highest modseq of message in account. + qms := bstore.QueryTx[Message](tx) + qms.FilterNonzero(Message{MailboxID: mb.ID}) + qms.SortDesc("ModSeq") + qms.Limit(1) + m, err := qms.Get() + if err == nil { + mb.ModSeq = ModSeq(m.ModSeq.Client()) + } else if err == bstore.ErrAbsent { + if modseq == 0 { + modseq, err = acc.NextModSeq(tx) + if err != nil { + return fmt.Errorf("get next mod seq for mailbox without messages: %v", err) + } + } + mb.ModSeq = modseq + } else { + return fmt.Errorf("looking up highest modseq for mailbox: %v", err) + } + mb.CreateSeq = 1 + if err := tx.Update(&mb); err != nil { + return fmt.Errorf("updating mailbox with modseq: %v", err) + } + } + + up.MailboxModSeq = true + if err := tx.Update(&up); err != nil { + return fmt.Errorf("marking upgrade done: %v", err) + } + + return nil + }) + if err != nil { + return nil, fmt.Errorf("upgrade: adding modseq to each mailbox: %v", err) + } + } + if up.Threads == 2 { close(acc.threadsCompleted) return acc, nil @@ -1072,7 +1138,7 @@ func OpenAccountDB(log mlog.Log, accountDir, accountName string) (a *Account, re } }() - err := upgradeThreads(mox.Shutdown, log, acc, &up) + err := upgradeThreads(mox.Shutdown, log, acc, up) if err != nil { a.threadsErr = err log.Errorx("upgrading account for threading, aborted", err, slog.String("account", a.Name)) @@ -1103,7 +1169,7 @@ func initAccount(db *bstore.DB) error { return db.Write(context.TODO(), func(tx *bstore.Tx) error { uidvalidity := InitialUIDValidity() - if err := tx.Insert(&Upgrade{ID: 1, Threads: 2}); err != nil { + if err := tx.Insert(&upgradeInit); err != nil { return err } if err := tx.Insert(&DiskUsage{ID: 1}); err != nil { @@ -1113,6 +1179,11 @@ func initAccount(db *bstore.DB) error { return err } + modseq, err := nextModSeq(tx) + if err != nil { + return fmt.Errorf("get next modseq: %v", err) + } + if len(mox.Conf.Static.DefaultMailboxes) > 0 { // Deprecated in favor of InitialMailboxes. defaultMailboxes := mox.Conf.Static.DefaultMailboxes @@ -1124,7 +1195,7 @@ func initAccount(db *bstore.DB) error { mailboxes = append(mailboxes, name) } for _, name := range mailboxes { - mb := Mailbox{Name: name, UIDValidity: uidvalidity, UIDNext: 1, HaveCounts: true} + mb := Mailbox{Name: name, UIDValidity: uidvalidity, UIDNext: 1, ModSeq: modseq, CreateSeq: modseq, HaveCounts: true} if strings.HasPrefix(name, "Archive") { mb.Archive = true } else if strings.HasPrefix(name, "Drafts") { @@ -1151,7 +1222,7 @@ func initAccount(db *bstore.DB) error { } add := func(name string, use SpecialUse) error { - mb := Mailbox{Name: name, UIDValidity: uidvalidity, UIDNext: 1, SpecialUse: use, HaveCounts: true} + mb := Mailbox{Name: name, UIDValidity: uidvalidity, UIDNext: 1, SpecialUse: use, ModSeq: modseq, CreateSeq: modseq, HaveCounts: true} if err := tx.Insert(&mb); err != nil { return fmt.Errorf("creating mailbox: %w", err) } @@ -1230,8 +1301,10 @@ func (a *Account) Close() error { // - Incorrect total message size. // - Message with UID >= mailbox uid next. // - Mailbox uidvalidity >= account uid validity. -// - ModSeq > 0, CreateSeq > 0, CreateSeq <= ModSeq. +// - Mailbox ModSeq > 0, CreateSeq > 0, CreateSeq <= ModSeq, and Modseq >= highest message ModSeq. +// - Message ModSeq > 0, CreateSeq > 0, CreateSeq <= ModSeq. // - All messages have a nonzero ThreadID, and no cycles in ThreadParentID, and parent messages the same ThreadParentIDs tail. +// - Annotations must have ModSeq > 0, CreateSeq > 0, ModSeq >= CreateSeq. func (a *Account) CheckConsistency() error { var uidErrors []string // With a limit, could be many. var modseqErrors []string // With limit. @@ -1256,10 +1329,39 @@ func (a *Account) CheckConsistency() error { errmsg := fmt.Sprintf("mailbox %q (id %d) has uidvalidity %d >= account next uidvalidity %d", mb.Name, mb.ID, mb.UIDValidity, nuv.Next) errors = append(errors, errmsg) } + + if mb.ModSeq == 0 || mb.CreateSeq == 0 || mb.CreateSeq > mb.ModSeq { + errmsg := fmt.Sprintf("mailbox %q (id %d) has invalid modseq %d or createseq %d, both must be > 0 and createseq <= modseq", mb.Name, mb.ID, mb.ModSeq, mb.CreateSeq) + errors = append(errors, errmsg) + return nil + } + m, err := bstore.QueryTx[Message](tx).FilterNonzero(Message{MailboxID: mb.ID}).SortDesc("ModSeq").Limit(1).Get() + if err == bstore.ErrAbsent { + return nil + } else if err != nil { + return fmt.Errorf("get message with highest modseq for mailbox: %v", err) + } else if mb.ModSeq < m.ModSeq { + errmsg := fmt.Sprintf("mailbox %q (id %d) has modseq %d < highest message modseq is %d", mb.Name, mb.ID, mb.ModSeq, m.ModSeq) + errors = append(errors, errmsg) + } return nil }) if err != nil { - return fmt.Errorf("listing mailboxes: %v", err) + return fmt.Errorf("checking mailboxes: %v", err) + } + + err = bstore.QueryTx[Annotation](tx).ForEach(func(a Annotation) error { + if a.ModSeq == 0 || a.CreateSeq == 0 || a.CreateSeq > a.ModSeq { + errmsg := fmt.Sprintf("annotation %d in mailbox %q (id %d) has invalid modseq %d or createseq %d, both must be > 0 and modseq >= createseq", a.ID, mailboxes[a.MailboxID].Name, a.MailboxID, a.ModSeq, a.CreateSeq) + errors = append(errors, errmsg) + } else if a.MailboxID > 0 && mailboxes[a.MailboxID].ModSeq < a.ModSeq { + errmsg := fmt.Sprintf("annotation %d in mailbox %q (id %d) has invalid modseq %d > mailbox modseq %d", a.ID, mailboxes[a.MailboxID].Name, a.MailboxID, a.ModSeq, mailboxes[a.MailboxID].ModSeq) + errors = append(errors, errmsg) + } + return nil + }) + if err != nil { + return fmt.Errorf("checking mailbox annotations: %v", err) } counts := map[int64]MailboxCounts{} @@ -1379,6 +1481,10 @@ func (a *Account) NextUIDValidity(tx *bstore.Tx) (uint32, error) { // NextModSeq returns the next modification sequence, which is global per account, // over all types. func (a *Account) NextModSeq(tx *bstore.Tx) (ModSeq, error) { + return nextModSeq(tx) +} + +func nextModSeq(tx *bstore.Tx) (ModSeq, error) { v := SyncState{ID: 1} if err := tx.Get(&v); err == bstore.ErrAbsent { // We start assigning from modseq 2. Modseq 0 is not usable, so returned as 1, so @@ -1453,6 +1559,17 @@ func (a *Account) DeliverMessage(log mlog.Log, tx *bstore.Tx, m *Message, msgFil } m.UID = mb.UIDNext mb.UIDNext++ + if m.CreateSeq == 0 || m.ModSeq == 0 { + modseq, err := a.NextModSeq(tx) + if err != nil { + return fmt.Errorf("assigning next modseq: %w", err) + } + m.CreateSeq = modseq + m.ModSeq = modseq + } else if m.ModSeq < mb.ModSeq { + return fmt.Errorf("cannot deliver message with modseq %d < mailbox modseq %d", m.ModSeq, mb.ModSeq) + } + mb.ModSeq = m.ModSeq if err := tx.Update(&mb); err != nil { return fmt.Errorf("updating mailbox nextuid: %w", err) } @@ -1498,14 +1615,6 @@ func (a *Account) DeliverMessage(log mlog.Log, tx *bstore.Tx, m *Message, msgFil if m.MailboxDestinedID != 0 && m.MailboxDestinedID == m.MailboxOrigID { m.MailboxDestinedID = 0 } - if m.CreateSeq == 0 || m.ModSeq == 0 { - modseq, err := a.NextModSeq(tx) - if err != nil { - return fmt.Errorf("assigning next modseq: %w", err) - } - m.CreateSeq = modseq - m.ModSeq = modseq - } if part != nil && m.MessageID == "" && m.SubjectBase == "" { m.PrepareThreading(log, part) @@ -1733,9 +1842,11 @@ func (a *Account) Subjectpass(email string) (key string, err error) { // The leaf mailbox is created with special-use flags, taking the flags away from // other mailboxes, and reflecting that in the returned changes. // +// Modseq is used, and initialized if 0, for created mailboxes. +// // Caller must hold account wlock. // Caller must propagate changes if any. -func (a *Account) MailboxEnsure(tx *bstore.Tx, name string, subscribe bool, specialUse SpecialUse) (mb Mailbox, changes []Change, rerr error) { +func (a *Account) MailboxEnsure(tx *bstore.Tx, name string, subscribe bool, specialUse SpecialUse, modseq *ModSeq) (mb Mailbox, changes []Change, rerr error) { if norm.NFC.String(name) != name { return Mailbox{}, nil, fmt.Errorf("mailbox name not normalized") } @@ -1775,10 +1886,18 @@ func (a *Account) MailboxEnsure(tx *bstore.Tx, name string, subscribe bool, spec if err != nil { return Mailbox{}, nil, fmt.Errorf("next uid validity: %v", err) } + if *modseq == 0 { + *modseq, err = a.NextModSeq(tx) + if err != nil { + return Mailbox{}, nil, fmt.Errorf("next modseq: %v", err) + } + } mb = Mailbox{ Name: p, UIDValidity: uidval, UIDNext: 1, + ModSeq: *modseq, + CreateSeq: *modseq, HaveCounts: true, } err = tx.Insert(&mb) @@ -1796,7 +1915,7 @@ func (a *Account) MailboxEnsure(tx *bstore.Tx, name string, subscribe bool, spec } flags = []string{`\Subscribed`} } - changes = append(changes, ChangeAddMailbox{mb, flags}) + changes = append(changes, ChangeAddMailbox{mb, flags, *modseq}) } // Clear any special-use flags from existing mailboxes and assign them to this mailbox. @@ -1820,10 +1939,11 @@ func (a *Account) MailboxEnsure(tx *bstore.Tx, name string, subscribe bool, spec } p := fn(&xmb) *p = false + xmb.ModSeq = *modseq if err := tx.Update(&xmb); err != nil { qerr = fmt.Errorf("clearing special-use flag: %v", err) } else { - changes = append(changes, ChangeMailboxSpecialUse{xmb.ID, xmb.Name, xmb.SpecialUse}) + changes = append(changes, xmb.ChangeSpecialUse()) } } clearSpecialUse(specialUse.Archive, func(xmb *Mailbox) *bool { return &xmb.Archive }) @@ -1836,10 +1956,11 @@ func (a *Account) MailboxEnsure(tx *bstore.Tx, name string, subscribe bool, spec } mb.SpecialUse = specialUse + mb.ModSeq = *modseq if err := tx.Update(&mb); err != nil { return Mailbox{}, nil, fmt.Errorf("setting special-use flag for new mailbox: %v", err) } - changes = append(changes, ChangeMailboxSpecialUse{mb.ID, mb.Name, mb.SpecialUse}) + changes = append(changes, mb.ChangeSpecialUse()) } return mb, changes, nil } @@ -2015,12 +2136,17 @@ func (a *Account) DeliverMailbox(log mlog.Log, mailbox string, m *Message, msgFi return ErrOverQuota } - mb, chl, err := a.MailboxEnsure(tx, mailbox, true, SpecialUse{}) + modseq := m.ModSeq + mb, chl, err := a.MailboxEnsure(tx, mailbox, true, SpecialUse{}, &modseq) if err != nil { return fmt.Errorf("ensuring mailbox: %w", err) } m.MailboxID = mb.ID m.MailboxOrigID = mb.ID + if m.ModSeq == 0 && modseq != 0 { + m.ModSeq = modseq + m.CreateSeq = modseq + } // Update count early, DeliverMessage will update mb too and we don't want to fetch // it again before updating. @@ -2136,6 +2262,7 @@ func (a *Account) rejectsRemoveMessages(ctx context.Context, log mlog.Log, tx *b if err != nil { return nil, fmt.Errorf("assign next modseq: %w", err) } + mb.ModSeq = modseq // Expunge the messages. qx := bstore.QueryTx[Message](tx) @@ -2655,6 +2782,7 @@ func (a *Account) SendLimitReached(tx *bstore.Tx, recipients []smtp.Path) (msgli func (a *Account) MailboxCreate(tx *bstore.Tx, name string, specialUse SpecialUse) (changes []Change, created []string, exists bool, rerr error) { elems := strings.Split(name, "/") var p string + var modseq ModSeq for i, elem := range elems { if i > 0 { p += "/" @@ -2670,7 +2798,7 @@ func (a *Account) MailboxCreate(tx *bstore.Tx, name string, specialUse SpecialUs } continue } - _, nchanges, err := a.MailboxEnsure(tx, p, true, specialUse) + _, nchanges, err := a.MailboxEnsure(tx, p, true, specialUse, &modseq) if err != nil { return nil, nil, false, fmt.Errorf("ensuring mailbox exists: %v", err) } @@ -2684,7 +2812,7 @@ func (a *Account) MailboxCreate(tx *bstore.Tx, name string, specialUse SpecialUs // destination, and any children of mbsrc and the destination. // // Names must be normalized and cannot be Inbox. -func (a *Account) MailboxRename(tx *bstore.Tx, mbsrc Mailbox, dst string) (changes []Change, isInbox, notExists, alreadyExists bool, rerr error) { +func (a *Account) MailboxRename(tx *bstore.Tx, mbsrc Mailbox, dst string, modseq *ModSeq) (changes []Change, isInbox, notExists, alreadyExists bool, rerr error) { if mbsrc.Name == "Inbox" || dst == "Inbox" { return nil, true, false, false, fmt.Errorf("inbox cannot be renamed") } @@ -2716,6 +2844,12 @@ func (a *Account) MailboxRename(tx *bstore.Tx, mbsrc Mailbox, dst string) (chang if err != nil { return nil, false, false, false, fmt.Errorf("next uid validity: %v", err) } + if *modseq == 0 { + *modseq, err = a.NextModSeq(tx) + if err != nil { + return nil, false, false, false, fmt.Errorf("get next modseq: %v", err) + } + } // Ensure parent mailboxes for the destination paths exist. var parent string @@ -2730,12 +2864,15 @@ func (a *Account) MailboxRename(tx *bstore.Tx, mbsrc Mailbox, dst string) (chang if ok { continue } + omb := mb mb = Mailbox{ ID: omb.ID, Name: parent, UIDValidity: uidval, UIDNext: 1, + ModSeq: *modseq, + CreateSeq: *modseq, HaveCounts: true, } if err := tx.Insert(&mb); err != nil { @@ -2746,7 +2883,7 @@ func (a *Account) MailboxRename(tx *bstore.Tx, mbsrc Mailbox, dst string) (chang return nil, false, false, false, fmt.Errorf("creating subscription for %q: %v", parent, err) } } - changes = append(changes, ChangeAddMailbox{Mailbox: mb, Flags: []string{`\Subscribed`}}) + changes = append(changes, ChangeAddMailbox{mb, []string{`\Subscribed`}, *modseq}) } // Process src mailboxes, renaming them to dst. @@ -2762,6 +2899,7 @@ func (a *Account) MailboxRename(tx *bstore.Tx, mbsrc Mailbox, dst string) (chang srcmb.Name = dstName srcmb.UIDValidity = uidval + srcmb.ModSeq = *modseq if err := tx.Update(&srcmb); err != nil { return nil, false, false, false, fmt.Errorf("renaming mailbox: %v", err) } @@ -2770,7 +2908,7 @@ func (a *Account) MailboxRename(tx *bstore.Tx, mbsrc Mailbox, dst string) (chang if tx.Get(&Subscription{Name: dstName}) == nil { dstFlags = []string{`\Subscribed`} } - changes = append(changes, ChangeRenameMailbox{MailboxID: srcmb.ID, OldName: srcName, NewName: dstName, Flags: dstFlags}) + changes = append(changes, ChangeRenameMailbox{srcmb.ID, srcName, dstName, dstFlags, *modseq}) } // If we renamed e.g. a/b to a/b/c/d, and a/b/c to a/b/c/d/c, we'll have to recreate a/b and a/b/c. @@ -2781,6 +2919,8 @@ func (a *Account) MailboxRename(tx *bstore.Tx, mbsrc Mailbox, dst string) (chang UIDValidity: uidval, UIDNext: 1, Name: xsrc, + ModSeq: *modseq, + CreateSeq: *modseq, HaveCounts: true, } if err := tx.Insert(&mb); err != nil { @@ -2811,6 +2951,11 @@ func (a *Account) MailboxDelete(ctx context.Context, log mlog.Log, tx *bstore.Tx // todo jmap: instead of completely deleting a mailbox and its messages, we need to mark them all as expunged. + modseq, err := a.NextModSeq(tx) + if err != nil { + return nil, nil, false, fmt.Errorf("get next modseq: %v", err) + } + qm := bstore.QueryTx[Message](tx) qm.FilterNonzero(Message{MailboxID: mailbox.ID}) remove, err := qm.List() @@ -2873,7 +3018,7 @@ func (a *Account) MailboxDelete(ctx context.Context, log mlog.Log, tx *bstore.Tx if err := tx.Delete(&Mailbox{ID: mailbox.ID}); err != nil { return nil, nil, false, fmt.Errorf("removing mailbox: %v", err) } - return []Change{ChangeRemoveMailbox{MailboxID: mailbox.ID, Name: mailbox.Name}}, removeMessageIDs, false, nil + return []Change{ChangeRemoveMailbox{mailbox.ID, mailbox.Name, modseq}}, removeMessageIDs, false, nil } // CheckMailboxName checks if name is valid, returning an INBOX-normalized name. diff --git a/store/account_test.go b/store/account_test.go index e2e85b0..945cc53 100644 --- a/store/account_test.go +++ b/store/account_test.go @@ -72,7 +72,6 @@ func TestMailbox(t *testing.T) { m.ThreadMuted = true m.ThreadCollapsed = true var mbsent Mailbox - mbrejects := Mailbox{Name: "Rejects", UIDValidity: 1, UIDNext: 1, HaveCounts: true} mreject := m mconsumed := Message{ Received: m.Received, @@ -102,6 +101,9 @@ func TestMailbox(t *testing.T) { err = tx.Update(&mbsent) tcheck(t, err, "update mbsent") + modseq, err := acc.NextModSeq(tx) + tcheck(t, err, "get next modseq") + mbrejects := Mailbox{Name: "Rejects", UIDValidity: 1, UIDNext: 1, ModSeq: modseq, CreateSeq: modseq, HaveCounts: true} err = tx.Insert(&mbrejects) tcheck(t, err, "insert rejects mailbox") mreject.MailboxID = mbrejects.ID @@ -166,20 +168,21 @@ func TestMailbox(t *testing.T) { t.Fatalf("same key for different address") } + var modseq ModSeq acc.WithWLock(func() { err := acc.DB.Write(ctxbg, func(tx *bstore.Tx) error { - _, _, err := acc.MailboxEnsure(tx, "Testbox", true, SpecialUse{}) + _, _, err := acc.MailboxEnsure(tx, "Testbox", true, SpecialUse{}, &modseq) return err }) tcheck(t, err, "ensure mailbox exists") err = acc.DB.Read(ctxbg, func(tx *bstore.Tx) error { - _, _, err := acc.MailboxEnsure(tx, "Testbox", true, SpecialUse{}) + _, _, err := acc.MailboxEnsure(tx, "Testbox", true, SpecialUse{}, &modseq) return err }) tcheck(t, err, "ensure mailbox exists") err = acc.DB.Write(ctxbg, func(tx *bstore.Tx) error { - _, _, err := acc.MailboxEnsure(tx, "Testbox2", false, SpecialUse{}) + _, _, err := acc.MailboxEnsure(tx, "Testbox2", false, SpecialUse{}, &modseq) tcheck(t, err, "create mailbox") exists, err := acc.MailboxExists(tx, "Testbox2") diff --git a/store/state.go b/store/state.go index ff3a02a..2c0b5bb 100644 --- a/store/state.go +++ b/store/state.go @@ -61,12 +61,14 @@ type ChangeThread struct { type ChangeRemoveMailbox struct { MailboxID int64 Name string + ModSeq ModSeq } // ChangeAddMailbox is sent for a newly created mailbox. type ChangeAddMailbox struct { Mailbox Mailbox Flags []string // For flags like \Subscribed. + ModSeq ModSeq } // ChangeRenameMailbox is sent for a rename mailbox. @@ -75,6 +77,7 @@ type ChangeRenameMailbox struct { OldName string NewName string Flags []string + ModSeq ModSeq } // ChangeAddSubscription is sent for an added subscription to a mailbox. @@ -95,6 +98,7 @@ type ChangeMailboxSpecialUse struct { MailboxID int64 MailboxName string SpecialUse SpecialUse + ModSeq ModSeq } // ChangeMailboxKeywords is sent when keywords are changed for a mailbox. For @@ -111,6 +115,7 @@ type ChangeAnnotation struct { MailboxID int64 // Can be zero, meaning global (per-account) annotation. MailboxName string // Empty for global (per-account) annotation. Key string // Also called "entry name", e.g. "/private/comment". + ModSeq ModSeq } var switchboardBusy atomic.Bool diff --git a/store/threads.go b/store/threads.go index 86d647a..b2c1d25 100644 --- a/store/threads.go +++ b/store/threads.go @@ -121,7 +121,7 @@ func assignParent(m *Message, pm Message, updateSeen bool) { // are made in transactions of batchSize changes. The total number of updated // messages is returned. // -// ModSeq is not changed. Calles should bump the uid validity of the mailboxes +// ModSeq is not changed. Callers should bump the uid validity of the mailboxes // to propagate the changes to IMAP clients. func (a *Account) ResetThreading(ctx context.Context, log mlog.Log, batchSize int, clearIDs bool) (int, error) { // todo: should this send Change events for ThreadMuted and ThreadCollapsed? worth it? @@ -725,7 +725,7 @@ func lookupThreadMessageSubject(tx *bstore.Tx, m Message, subjectBase string) (* return &tm, nil } -func upgradeThreads(ctx context.Context, log mlog.Log, acc *Account, up *Upgrade) error { +func upgradeThreads(ctx context.Context, log mlog.Log, acc *Account, up Upgrade) error { log = log.With(slog.String("account", acc.Name)) if up.Threads == 0 { @@ -742,9 +742,16 @@ func upgradeThreads(ctx context.Context, log mlog.Log, acc *Account, up *Upgrade return fmt.Errorf("resetting message threading fields: %v", err) } - up.Threads = 1 - if err := acc.DB.Update(ctx, up); err != nil { - up.Threads = 0 + // Must refresh up, it may have been modified by another upgrade progress. + err = acc.DB.Write(ctx, func(tx *bstore.Tx) error { + up = Upgrade{ID: up.ID} + if err := tx.Get(&up); err != nil { + return err + } + up.Threads = 1 + return tx.Update(&up) + }) + if err != nil { return fmt.Errorf("saving upgrade process while upgrading account to threads storage, step 1/2: %w", err) } log.Info("upgrading account for threading, step 1/2: completed", slog.Duration("duration", time.Since(t0)), slog.Int("messages", total)) @@ -762,11 +769,20 @@ func upgradeThreads(ctx context.Context, log mlog.Log, acc *Account, up *Upgrade if err := acc.AssignThreads(ctx, log, nil, 1, batchSize, io.Discard); err != nil { return fmt.Errorf("upgrading to threads storage, step 2/2: %w", err) } - up.Threads = 2 - if err := acc.DB.Update(ctx, up); err != nil { - up.Threads = 1 + + // Must refresh up, it may have been modified by another upgrade progress. + err := acc.DB.Write(ctx, func(tx *bstore.Tx) error { + up = Upgrade{ID: up.ID} + if err := tx.Get(&up); err != nil { + return err + } + up.Threads = 2 + return tx.Update(&up) + }) + if err != nil { return fmt.Errorf("saving upgrade process for thread storage, step 2/2: %w", err) } + log.Info("upgrading account for threading, step 2/2: completed", slog.Duration("duration", time.Since(t0))) } diff --git a/webaccount/import.go b/webaccount/import.go index 664017b..158c4b1 100644 --- a/webaccount/import.go +++ b/webaccount/import.go @@ -463,10 +463,19 @@ func importMessages(ctx context.Context, log mlog.Log, token string, acc *store. if err == bstore.ErrAbsent { uidvalidity, err := acc.NextUIDValidity(tx) ximportcheckf(err, "finding next uid validity") + + if modseq == 0 { + var err error + modseq, err = acc.NextModSeq(tx) + ximportcheckf(err, "assigning next modseq") + } + mb = store.Mailbox{ Name: p, UIDValidity: uidvalidity, UIDNext: 1, + ModSeq: modseq, + CreateSeq: modseq, HaveCounts: true, // Do not assign special-use flags. This existing account probably already has such mailboxes. } @@ -477,7 +486,7 @@ func importMessages(ctx context.Context, log mlog.Log, token string, acc *store. err := tx.Insert(&store.Subscription{Name: p}) ximportcheckf(err, "subscribing to imported mailbox") } - changes = append(changes, store.ChangeAddMailbox{Mailbox: mb, Flags: []string{`\Subscribed`}}) + changes = append(changes, store.ChangeAddMailbox{Mailbox: mb, Flags: []string{`\Subscribed`}, ModSeq: modseq}) } else if err != nil { ximportcheckf(err, "creating mailbox %s (aborting)", p) } diff --git a/webmail/api.go b/webmail/api.go index 729b4eb..def5ceb 100644 --- a/webmail/api.go +++ b/webmail/api.go @@ -424,8 +424,7 @@ func (w Webmail) MessageCompose(ctx context.Context, m ComposeMessage, mailboxID var modseq store.ModSeq // Only set if needed. if m.DraftMessageID > 0 { - var nchanges []store.Change - modseq, nchanges = xops.MessageDeleteTx(ctx, log, tx, acc, []int64{m.DraftMessageID}, modseq) + nchanges := xops.MessageDeleteTx(ctx, log, tx, acc, []int64{m.DraftMessageID}, &modseq) changes = append(changes, nchanges...) // On-disk file is removed after lock. } @@ -1030,8 +1029,7 @@ func (w Webmail) MessageSubmit(ctx context.Context, m SubmitMessage) { }() xdbwrite(ctx, acc, func(tx *bstore.Tx) { if m.DraftMessageID > 0 { - var nchanges []store.Change - modseq, nchanges = xops.MessageDeleteTx(ctx, log, tx, acc, []int64{m.DraftMessageID}, modseq) + nchanges := xops.MessageDeleteTx(ctx, log, tx, acc, []int64{m.DraftMessageID}, &modseq) changes = append(changes, nchanges...) // On-disk file is removed after lock. } @@ -1048,13 +1046,23 @@ func (w Webmail) MessageSubmit(ctx context.Context, m SubmitMessage) { rm.Notjunk = true } if rm.Flags != oflags { - modseq, err = acc.NextModSeq(tx) - xcheckf(ctx, err, "next modseq") + if modseq == 0 { + modseq, err = acc.NextModSeq(tx) + xcheckf(ctx, err, "next modseq") + } rm.ModSeq = modseq err := tx.Update(&rm) xcheckf(ctx, err, "updating flags of replied/forwarded message") changes = append(changes, rm.ChangeFlags(oflags)) + // Update modseq of mailbox of replied/forwarded message. + rmb := store.Mailbox{ID: rm.MailboxID} + err = tx.Get(&rmb) + xcheckf(ctx, err, "get mailbox of replied/forwarded message for modseq update") + rmb.ModSeq = modseq + err = tx.Update(&rmb) + xcheckf(ctx, err, "update modseqo of mailbox of replied/forwarded message") + err = acc.RetrainMessages(ctx, log, tx, []store.Message{rm}, false) xcheckf(ctx, err, "retraining messages after reply/forward") } @@ -1075,8 +1083,7 @@ func (w Webmail) MessageSubmit(ctx context.Context, m SubmitMessage) { err = q.IDs(&msgIDs) xcheckf(ctx, err, "listing messages in thread to archive") if len(msgIDs) > 0 { - var nchanges []store.Change - modseq, nchanges = xops.MessageMoveTx(ctx, log, acc, tx, msgIDs, mbArchive, modseq) + nchanges := xops.MessageMoveTx(ctx, log, acc, tx, msgIDs, mbArchive, &modseq) changes = append(changes, nchanges...) } } @@ -1299,6 +1306,8 @@ func (Webmail) MailboxEmpty(ctx context.Context, mailboxID int64) { xcheckf(ctx, errors.New("no messages in mailbox"), "emptying mailbox") } + mb.ModSeq = modseq + // Remove Recipients. anyIDs := make([]any, len(expunged)) for i, m := range expunged { @@ -1364,7 +1373,8 @@ func (Webmail) MailboxRename(ctx context.Context, mailboxID int64, newName strin mbsrc := xmailboxID(ctx, tx, mailboxID) var err error var isInbox, notExists, alreadyExists bool - changes, isInbox, notExists, alreadyExists, err = acc.MailboxRename(tx, mbsrc, newName) + var modseq store.ModSeq + changes, isInbox, notExists, alreadyExists, err = acc.MailboxRename(tx, mbsrc, newName, &modseq) if isInbox || notExists || alreadyExists { xcheckuserf(ctx, err, "renaming mailbox") } @@ -1485,6 +1495,9 @@ func (Webmail) MailboxSetSpecialUse(ctx context.Context, mb store.Mailbox) { xdbwrite(ctx, acc, func(tx *bstore.Tx) { xmb := xmailboxID(ctx, tx, mb.ID) + modseq, err := acc.NextModSeq(tx) + xcheckf(ctx, err, "get next modseq") + // We only allow a single mailbox for each flag (JMAP requirement). So for any flag // we set, we clear it for the mailbox(es) that had it, if any. clearPrevious := func(clear bool, specialUse string) { @@ -1496,7 +1509,7 @@ func (Webmail) MailboxSetSpecialUse(ctx context.Context, mb store.Mailbox) { q.FilterNotEqual("ID", mb.ID) q.FilterEqual(specialUse, true) q.Gather(&ombl) - _, err := q.UpdateField(specialUse, false) + _, err := q.UpdateFields(map[string]any{specialUse: false, "ModSeq": modseq}) xcheckf(ctx, err, "updating previous special-use mailboxes") for _, omb := range ombl { @@ -1510,7 +1523,8 @@ func (Webmail) MailboxSetSpecialUse(ctx context.Context, mb store.Mailbox) { clearPrevious(mb.Trash, "Trash") xmb.SpecialUse = mb.SpecialUse - err := tx.Update(&xmb) + xmb.ModSeq = modseq + err = tx.Update(&xmb) xcheckf(ctx, err, "updating special-use flags for mailbox") changes = append(changes, xmb.ChangeSpecialUse()) }) diff --git a/webmail/api.json b/webmail/api.json index 45b1f27..f657548 100644 --- a/webmail/api.json +++ b/webmail/api.json @@ -1665,6 +1665,20 @@ "string" ] }, + { + "Name": "ModSeq", + "Docs": "ModSeq matches that of last message (including deleted), or changes to mailbox such as after metadata changes.", + "Typewords": [ + "ModSeq" + ] + }, + { + "Name": "CreateSeq", + "Docs": "", + "Typewords": [ + "ModSeq" + ] + }, { "Name": "HaveCounts", "Docs": "Whether MailboxCounts have been initialized.", @@ -2974,6 +2988,13 @@ "Typewords": [ "string" ] + }, + { + "Name": "ModSeq", + "Docs": "", + "Typewords": [ + "ModSeq" + ] } ] }, @@ -3022,6 +3043,13 @@ "[]", "string" ] + }, + { + "Name": "ModSeq", + "Docs": "", + "Typewords": [ + "ModSeq" + ] } ] }, @@ -3104,6 +3132,13 @@ "Typewords": [ "SpecialUse" ] + }, + { + "Name": "ModSeq", + "Docs": "", + "Typewords": [ + "ModSeq" + ] } ] }, diff --git a/webmail/api.ts b/webmail/api.ts index 1b8f24a..92d5e49 100644 --- a/webmail/api.ts +++ b/webmail/api.ts @@ -197,6 +197,8 @@ export interface Mailbox { Sent: boolean Trash: boolean Keywords?: string[] | null // Keywords as used in messages. Storing a non-system keyword for a message automatically adds it to this list. Used in the IMAP FLAGS response. Only "atoms" are allowed (IMAP syntax), keywords are case-insensitive, only stored in lower case (for JMAP), sorted. + ModSeq: ModSeq // ModSeq matches that of last message (including deleted), or changes to mailbox such as after metadata changes. + CreateSeq: ModSeq HaveCounts: boolean // Whether MailboxCounts have been initialized. Total: number // Total number of messages, excluding \Deleted. For JMAP. Deleted: number // Number of messages with \Deleted flag. Used for IMAP message count that includes messages with \Deleted. @@ -460,6 +462,7 @@ export interface ChangeMsgThread { export interface ChangeMailboxRemove { MailboxID: number Name: string + ModSeq: ModSeq } // ChangeMailboxAdd indicates a new mailbox was added, initially without any messages. @@ -474,6 +477,7 @@ export interface ChangeMailboxRename { OldName: string NewName: string Flags?: string[] | null + ModSeq: ModSeq } // ChangeMailboxCounts set new total and unseen message counts for a mailbox. @@ -492,6 +496,7 @@ export interface ChangeMailboxSpecialUse { MailboxID: number MailboxName: string SpecialUse: SpecialUse + ModSeq: ModSeq } // SpecialUse identifies a specific role for a mailbox, used by clients to @@ -608,7 +613,7 @@ export const types: TypenameMap = { "SubmitMessage": {"Name":"SubmitMessage","Docs":"","Fields":[{"Name":"From","Docs":"","Typewords":["string"]},{"Name":"To","Docs":"","Typewords":["[]","string"]},{"Name":"Cc","Docs":"","Typewords":["[]","string"]},{"Name":"Bcc","Docs":"","Typewords":["[]","string"]},{"Name":"ReplyTo","Docs":"","Typewords":["string"]},{"Name":"Subject","Docs":"","Typewords":["string"]},{"Name":"TextBody","Docs":"","Typewords":["string"]},{"Name":"Attachments","Docs":"","Typewords":["[]","File"]},{"Name":"ForwardAttachments","Docs":"","Typewords":["ForwardAttachments"]},{"Name":"IsForward","Docs":"","Typewords":["bool"]},{"Name":"ResponseMessageID","Docs":"","Typewords":["int64"]},{"Name":"UserAgent","Docs":"","Typewords":["string"]},{"Name":"RequireTLS","Docs":"","Typewords":["nullable","bool"]},{"Name":"FutureRelease","Docs":"","Typewords":["nullable","timestamp"]},{"Name":"ArchiveThread","Docs":"","Typewords":["bool"]},{"Name":"ArchiveReferenceMailboxID","Docs":"","Typewords":["int64"]},{"Name":"DraftMessageID","Docs":"","Typewords":["int64"]}]}, "File": {"Name":"File","Docs":"","Fields":[{"Name":"Filename","Docs":"","Typewords":["string"]},{"Name":"DataURI","Docs":"","Typewords":["string"]}]}, "ForwardAttachments": {"Name":"ForwardAttachments","Docs":"","Fields":[{"Name":"MessageID","Docs":"","Typewords":["int64"]},{"Name":"Paths","Docs":"","Typewords":["[]","[]","int32"]}]}, - "Mailbox": {"Name":"Mailbox","Docs":"","Fields":[{"Name":"ID","Docs":"","Typewords":["int64"]},{"Name":"Name","Docs":"","Typewords":["string"]},{"Name":"UIDValidity","Docs":"","Typewords":["uint32"]},{"Name":"UIDNext","Docs":"","Typewords":["UID"]},{"Name":"Archive","Docs":"","Typewords":["bool"]},{"Name":"Draft","Docs":"","Typewords":["bool"]},{"Name":"Junk","Docs":"","Typewords":["bool"]},{"Name":"Sent","Docs":"","Typewords":["bool"]},{"Name":"Trash","Docs":"","Typewords":["bool"]},{"Name":"Keywords","Docs":"","Typewords":["[]","string"]},{"Name":"HaveCounts","Docs":"","Typewords":["bool"]},{"Name":"Total","Docs":"","Typewords":["int64"]},{"Name":"Deleted","Docs":"","Typewords":["int64"]},{"Name":"Unread","Docs":"","Typewords":["int64"]},{"Name":"Unseen","Docs":"","Typewords":["int64"]},{"Name":"Size","Docs":"","Typewords":["int64"]}]}, + "Mailbox": {"Name":"Mailbox","Docs":"","Fields":[{"Name":"ID","Docs":"","Typewords":["int64"]},{"Name":"Name","Docs":"","Typewords":["string"]},{"Name":"UIDValidity","Docs":"","Typewords":["uint32"]},{"Name":"UIDNext","Docs":"","Typewords":["UID"]},{"Name":"Archive","Docs":"","Typewords":["bool"]},{"Name":"Draft","Docs":"","Typewords":["bool"]},{"Name":"Junk","Docs":"","Typewords":["bool"]},{"Name":"Sent","Docs":"","Typewords":["bool"]},{"Name":"Trash","Docs":"","Typewords":["bool"]},{"Name":"Keywords","Docs":"","Typewords":["[]","string"]},{"Name":"ModSeq","Docs":"","Typewords":["ModSeq"]},{"Name":"CreateSeq","Docs":"","Typewords":["ModSeq"]},{"Name":"HaveCounts","Docs":"","Typewords":["bool"]},{"Name":"Total","Docs":"","Typewords":["int64"]},{"Name":"Deleted","Docs":"","Typewords":["int64"]},{"Name":"Unread","Docs":"","Typewords":["int64"]},{"Name":"Unseen","Docs":"","Typewords":["int64"]},{"Name":"Size","Docs":"","Typewords":["int64"]}]}, "RecipientSecurity": {"Name":"RecipientSecurity","Docs":"","Fields":[{"Name":"STARTTLS","Docs":"","Typewords":["SecurityResult"]},{"Name":"MTASTS","Docs":"","Typewords":["SecurityResult"]},{"Name":"DNSSEC","Docs":"","Typewords":["SecurityResult"]},{"Name":"DANE","Docs":"","Typewords":["SecurityResult"]},{"Name":"RequireTLS","Docs":"","Typewords":["SecurityResult"]}]}, "Settings": {"Name":"Settings","Docs":"","Fields":[{"Name":"ID","Docs":"","Typewords":["uint8"]},{"Name":"Signature","Docs":"","Typewords":["string"]},{"Name":"Quoting","Docs":"","Typewords":["Quoting"]},{"Name":"ShowAddressSecurity","Docs":"","Typewords":["bool"]},{"Name":"ShowHTML","Docs":"","Typewords":["bool"]},{"Name":"NoShowShortcuts","Docs":"","Typewords":["bool"]},{"Name":"ShowHeaders","Docs":"","Typewords":["[]","string"]}]}, "Ruleset": {"Name":"Ruleset","Docs":"","Fields":[{"Name":"SMTPMailFromRegexp","Docs":"","Typewords":["string"]},{"Name":"MsgFromRegexp","Docs":"","Typewords":["string"]},{"Name":"VerifiedDomain","Docs":"","Typewords":["string"]},{"Name":"HeadersRegexp","Docs":"","Typewords":["{}","string"]},{"Name":"IsForward","Docs":"","Typewords":["bool"]},{"Name":"ListAllowDomain","Docs":"","Typewords":["string"]},{"Name":"AcceptRejectsToMailbox","Docs":"","Typewords":["string"]},{"Name":"Mailbox","Docs":"","Typewords":["string"]},{"Name":"Comment","Docs":"","Typewords":["string"]},{"Name":"VerifiedDNSDomain","Docs":"","Typewords":["Domain"]},{"Name":"ListAllowDNSDomain","Docs":"","Typewords":["Domain"]}]}, @@ -627,11 +632,11 @@ export const types: TypenameMap = { "ChangeMsgRemove": {"Name":"ChangeMsgRemove","Docs":"","Fields":[{"Name":"MailboxID","Docs":"","Typewords":["int64"]},{"Name":"UIDs","Docs":"","Typewords":["[]","UID"]},{"Name":"ModSeq","Docs":"","Typewords":["ModSeq"]}]}, "ChangeMsgFlags": {"Name":"ChangeMsgFlags","Docs":"","Fields":[{"Name":"MailboxID","Docs":"","Typewords":["int64"]},{"Name":"UID","Docs":"","Typewords":["UID"]},{"Name":"ModSeq","Docs":"","Typewords":["ModSeq"]},{"Name":"Mask","Docs":"","Typewords":["Flags"]},{"Name":"Flags","Docs":"","Typewords":["Flags"]},{"Name":"Keywords","Docs":"","Typewords":["[]","string"]}]}, "ChangeMsgThread": {"Name":"ChangeMsgThread","Docs":"","Fields":[{"Name":"MessageIDs","Docs":"","Typewords":["[]","int64"]},{"Name":"Muted","Docs":"","Typewords":["bool"]},{"Name":"Collapsed","Docs":"","Typewords":["bool"]}]}, - "ChangeMailboxRemove": {"Name":"ChangeMailboxRemove","Docs":"","Fields":[{"Name":"MailboxID","Docs":"","Typewords":["int64"]},{"Name":"Name","Docs":"","Typewords":["string"]}]}, + "ChangeMailboxRemove": {"Name":"ChangeMailboxRemove","Docs":"","Fields":[{"Name":"MailboxID","Docs":"","Typewords":["int64"]},{"Name":"Name","Docs":"","Typewords":["string"]},{"Name":"ModSeq","Docs":"","Typewords":["ModSeq"]}]}, "ChangeMailboxAdd": {"Name":"ChangeMailboxAdd","Docs":"","Fields":[{"Name":"Mailbox","Docs":"","Typewords":["Mailbox"]}]}, - "ChangeMailboxRename": {"Name":"ChangeMailboxRename","Docs":"","Fields":[{"Name":"MailboxID","Docs":"","Typewords":["int64"]},{"Name":"OldName","Docs":"","Typewords":["string"]},{"Name":"NewName","Docs":"","Typewords":["string"]},{"Name":"Flags","Docs":"","Typewords":["[]","string"]}]}, + "ChangeMailboxRename": {"Name":"ChangeMailboxRename","Docs":"","Fields":[{"Name":"MailboxID","Docs":"","Typewords":["int64"]},{"Name":"OldName","Docs":"","Typewords":["string"]},{"Name":"NewName","Docs":"","Typewords":["string"]},{"Name":"Flags","Docs":"","Typewords":["[]","string"]},{"Name":"ModSeq","Docs":"","Typewords":["ModSeq"]}]}, "ChangeMailboxCounts": {"Name":"ChangeMailboxCounts","Docs":"","Fields":[{"Name":"MailboxID","Docs":"","Typewords":["int64"]},{"Name":"MailboxName","Docs":"","Typewords":["string"]},{"Name":"Total","Docs":"","Typewords":["int64"]},{"Name":"Deleted","Docs":"","Typewords":["int64"]},{"Name":"Unread","Docs":"","Typewords":["int64"]},{"Name":"Unseen","Docs":"","Typewords":["int64"]},{"Name":"Size","Docs":"","Typewords":["int64"]}]}, - "ChangeMailboxSpecialUse": {"Name":"ChangeMailboxSpecialUse","Docs":"","Fields":[{"Name":"MailboxID","Docs":"","Typewords":["int64"]},{"Name":"MailboxName","Docs":"","Typewords":["string"]},{"Name":"SpecialUse","Docs":"","Typewords":["SpecialUse"]}]}, + "ChangeMailboxSpecialUse": {"Name":"ChangeMailboxSpecialUse","Docs":"","Fields":[{"Name":"MailboxID","Docs":"","Typewords":["int64"]},{"Name":"MailboxName","Docs":"","Typewords":["string"]},{"Name":"SpecialUse","Docs":"","Typewords":["SpecialUse"]},{"Name":"ModSeq","Docs":"","Typewords":["ModSeq"]}]}, "SpecialUse": {"Name":"SpecialUse","Docs":"","Fields":[{"Name":"Archive","Docs":"","Typewords":["bool"]},{"Name":"Draft","Docs":"","Typewords":["bool"]},{"Name":"Junk","Docs":"","Typewords":["bool"]},{"Name":"Sent","Docs":"","Typewords":["bool"]},{"Name":"Trash","Docs":"","Typewords":["bool"]}]}, "ChangeMailboxKeywords": {"Name":"ChangeMailboxKeywords","Docs":"","Fields":[{"Name":"MailboxID","Docs":"","Typewords":["int64"]},{"Name":"MailboxName","Docs":"","Typewords":["string"]},{"Name":"Keywords","Docs":"","Typewords":["[]","string"]}]}, "UID": {"Name":"UID","Docs":"","Values":null}, diff --git a/webmail/msg.js b/webmail/msg.js index 13b5b60..8ebe86a 100644 --- a/webmail/msg.js +++ b/webmail/msg.js @@ -309,7 +309,7 @@ var api; "SubmitMessage": { "Name": "SubmitMessage", "Docs": "", "Fields": [{ "Name": "From", "Docs": "", "Typewords": ["string"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Cc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Bcc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "ReplyTo", "Docs": "", "Typewords": ["string"] }, { "Name": "Subject", "Docs": "", "Typewords": ["string"] }, { "Name": "TextBody", "Docs": "", "Typewords": ["string"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["[]", "File"] }, { "Name": "ForwardAttachments", "Docs": "", "Typewords": ["ForwardAttachments"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ResponseMessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "UserAgent", "Docs": "", "Typewords": ["string"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["nullable", "bool"] }, { "Name": "FutureRelease", "Docs": "", "Typewords": ["nullable", "timestamp"] }, { "Name": "ArchiveThread", "Docs": "", "Typewords": ["bool"] }, { "Name": "ArchiveReferenceMailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "DraftMessageID", "Docs": "", "Typewords": ["int64"] }] }, "File": { "Name": "File", "Docs": "", "Fields": [{ "Name": "Filename", "Docs": "", "Typewords": ["string"] }, { "Name": "DataURI", "Docs": "", "Typewords": ["string"] }] }, "ForwardAttachments": { "Name": "ForwardAttachments", "Docs": "", "Fields": [{ "Name": "MessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Paths", "Docs": "", "Typewords": ["[]", "[]", "int32"] }] }, - "Mailbox": { "Name": "Mailbox", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "UIDValidity", "Docs": "", "Typewords": ["uint32"] }, { "Name": "UIDNext", "Docs": "", "Typewords": ["UID"] }, { "Name": "Archive", "Docs": "", "Typewords": ["bool"] }, { "Name": "Draft", "Docs": "", "Typewords": ["bool"] }, { "Name": "Junk", "Docs": "", "Typewords": ["bool"] }, { "Name": "Sent", "Docs": "", "Typewords": ["bool"] }, { "Name": "Trash", "Docs": "", "Typewords": ["bool"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "HaveCounts", "Docs": "", "Typewords": ["bool"] }, { "Name": "Total", "Docs": "", "Typewords": ["int64"] }, { "Name": "Deleted", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unread", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unseen", "Docs": "", "Typewords": ["int64"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }] }, + "Mailbox": { "Name": "Mailbox", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "UIDValidity", "Docs": "", "Typewords": ["uint32"] }, { "Name": "UIDNext", "Docs": "", "Typewords": ["UID"] }, { "Name": "Archive", "Docs": "", "Typewords": ["bool"] }, { "Name": "Draft", "Docs": "", "Typewords": ["bool"] }, { "Name": "Junk", "Docs": "", "Typewords": ["bool"] }, { "Name": "Sent", "Docs": "", "Typewords": ["bool"] }, { "Name": "Trash", "Docs": "", "Typewords": ["bool"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "ModSeq", "Docs": "", "Typewords": ["ModSeq"] }, { "Name": "CreateSeq", "Docs": "", "Typewords": ["ModSeq"] }, { "Name": "HaveCounts", "Docs": "", "Typewords": ["bool"] }, { "Name": "Total", "Docs": "", "Typewords": ["int64"] }, { "Name": "Deleted", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unread", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unseen", "Docs": "", "Typewords": ["int64"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }] }, "RecipientSecurity": { "Name": "RecipientSecurity", "Docs": "", "Fields": [{ "Name": "STARTTLS", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "MTASTS", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DNSSEC", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DANE", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["SecurityResult"] }] }, "Settings": { "Name": "Settings", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["uint8"] }, { "Name": "Signature", "Docs": "", "Typewords": ["string"] }, { "Name": "Quoting", "Docs": "", "Typewords": ["Quoting"] }, { "Name": "ShowAddressSecurity", "Docs": "", "Typewords": ["bool"] }, { "Name": "ShowHTML", "Docs": "", "Typewords": ["bool"] }, { "Name": "NoShowShortcuts", "Docs": "", "Typewords": ["bool"] }, { "Name": "ShowHeaders", "Docs": "", "Typewords": ["[]", "string"] }] }, "Ruleset": { "Name": "Ruleset", "Docs": "", "Fields": [{ "Name": "SMTPMailFromRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "MsgFromRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "VerifiedDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "HeadersRegexp", "Docs": "", "Typewords": ["{}", "string"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ListAllowDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "AcceptRejectsToMailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Mailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Comment", "Docs": "", "Typewords": ["string"] }, { "Name": "VerifiedDNSDomain", "Docs": "", "Typewords": ["Domain"] }, { "Name": "ListAllowDNSDomain", "Docs": "", "Typewords": ["Domain"] }] }, @@ -328,11 +328,11 @@ var api; "ChangeMsgRemove": { "Name": "ChangeMsgRemove", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "UIDs", "Docs": "", "Typewords": ["[]", "UID"] }, { "Name": "ModSeq", "Docs": "", "Typewords": ["ModSeq"] }] }, "ChangeMsgFlags": { "Name": "ChangeMsgFlags", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "UID", "Docs": "", "Typewords": ["UID"] }, { "Name": "ModSeq", "Docs": "", "Typewords": ["ModSeq"] }, { "Name": "Mask", "Docs": "", "Typewords": ["Flags"] }, { "Name": "Flags", "Docs": "", "Typewords": ["Flags"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }] }, "ChangeMsgThread": { "Name": "ChangeMsgThread", "Docs": "", "Fields": [{ "Name": "MessageIDs", "Docs": "", "Typewords": ["[]", "int64"] }, { "Name": "Muted", "Docs": "", "Typewords": ["bool"] }, { "Name": "Collapsed", "Docs": "", "Typewords": ["bool"] }] }, - "ChangeMailboxRemove": { "Name": "ChangeMailboxRemove", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }] }, + "ChangeMailboxRemove": { "Name": "ChangeMailboxRemove", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "ModSeq", "Docs": "", "Typewords": ["ModSeq"] }] }, "ChangeMailboxAdd": { "Name": "ChangeMailboxAdd", "Docs": "", "Fields": [{ "Name": "Mailbox", "Docs": "", "Typewords": ["Mailbox"] }] }, - "ChangeMailboxRename": { "Name": "ChangeMailboxRename", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "OldName", "Docs": "", "Typewords": ["string"] }, { "Name": "NewName", "Docs": "", "Typewords": ["string"] }, { "Name": "Flags", "Docs": "", "Typewords": ["[]", "string"] }] }, + "ChangeMailboxRename": { "Name": "ChangeMailboxRename", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "OldName", "Docs": "", "Typewords": ["string"] }, { "Name": "NewName", "Docs": "", "Typewords": ["string"] }, { "Name": "Flags", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "ModSeq", "Docs": "", "Typewords": ["ModSeq"] }] }, "ChangeMailboxCounts": { "Name": "ChangeMailboxCounts", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "MailboxName", "Docs": "", "Typewords": ["string"] }, { "Name": "Total", "Docs": "", "Typewords": ["int64"] }, { "Name": "Deleted", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unread", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unseen", "Docs": "", "Typewords": ["int64"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }] }, - "ChangeMailboxSpecialUse": { "Name": "ChangeMailboxSpecialUse", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "MailboxName", "Docs": "", "Typewords": ["string"] }, { "Name": "SpecialUse", "Docs": "", "Typewords": ["SpecialUse"] }] }, + "ChangeMailboxSpecialUse": { "Name": "ChangeMailboxSpecialUse", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "MailboxName", "Docs": "", "Typewords": ["string"] }, { "Name": "SpecialUse", "Docs": "", "Typewords": ["SpecialUse"] }, { "Name": "ModSeq", "Docs": "", "Typewords": ["ModSeq"] }] }, "SpecialUse": { "Name": "SpecialUse", "Docs": "", "Fields": [{ "Name": "Archive", "Docs": "", "Typewords": ["bool"] }, { "Name": "Draft", "Docs": "", "Typewords": ["bool"] }, { "Name": "Junk", "Docs": "", "Typewords": ["bool"] }, { "Name": "Sent", "Docs": "", "Typewords": ["bool"] }, { "Name": "Trash", "Docs": "", "Typewords": ["bool"] }] }, "ChangeMailboxKeywords": { "Name": "ChangeMailboxKeywords", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "MailboxName", "Docs": "", "Typewords": ["string"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }] }, "UID": { "Name": "UID", "Docs": "", "Values": null }, diff --git a/webmail/text.js b/webmail/text.js index d8507d0..eb5d587 100644 --- a/webmail/text.js +++ b/webmail/text.js @@ -309,7 +309,7 @@ var api; "SubmitMessage": { "Name": "SubmitMessage", "Docs": "", "Fields": [{ "Name": "From", "Docs": "", "Typewords": ["string"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Cc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Bcc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "ReplyTo", "Docs": "", "Typewords": ["string"] }, { "Name": "Subject", "Docs": "", "Typewords": ["string"] }, { "Name": "TextBody", "Docs": "", "Typewords": ["string"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["[]", "File"] }, { "Name": "ForwardAttachments", "Docs": "", "Typewords": ["ForwardAttachments"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ResponseMessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "UserAgent", "Docs": "", "Typewords": ["string"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["nullable", "bool"] }, { "Name": "FutureRelease", "Docs": "", "Typewords": ["nullable", "timestamp"] }, { "Name": "ArchiveThread", "Docs": "", "Typewords": ["bool"] }, { "Name": "ArchiveReferenceMailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "DraftMessageID", "Docs": "", "Typewords": ["int64"] }] }, "File": { "Name": "File", "Docs": "", "Fields": [{ "Name": "Filename", "Docs": "", "Typewords": ["string"] }, { "Name": "DataURI", "Docs": "", "Typewords": ["string"] }] }, "ForwardAttachments": { "Name": "ForwardAttachments", "Docs": "", "Fields": [{ "Name": "MessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Paths", "Docs": "", "Typewords": ["[]", "[]", "int32"] }] }, - "Mailbox": { "Name": "Mailbox", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "UIDValidity", "Docs": "", "Typewords": ["uint32"] }, { "Name": "UIDNext", "Docs": "", "Typewords": ["UID"] }, { "Name": "Archive", "Docs": "", "Typewords": ["bool"] }, { "Name": "Draft", "Docs": "", "Typewords": ["bool"] }, { "Name": "Junk", "Docs": "", "Typewords": ["bool"] }, { "Name": "Sent", "Docs": "", "Typewords": ["bool"] }, { "Name": "Trash", "Docs": "", "Typewords": ["bool"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "HaveCounts", "Docs": "", "Typewords": ["bool"] }, { "Name": "Total", "Docs": "", "Typewords": ["int64"] }, { "Name": "Deleted", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unread", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unseen", "Docs": "", "Typewords": ["int64"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }] }, + "Mailbox": { "Name": "Mailbox", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "UIDValidity", "Docs": "", "Typewords": ["uint32"] }, { "Name": "UIDNext", "Docs": "", "Typewords": ["UID"] }, { "Name": "Archive", "Docs": "", "Typewords": ["bool"] }, { "Name": "Draft", "Docs": "", "Typewords": ["bool"] }, { "Name": "Junk", "Docs": "", "Typewords": ["bool"] }, { "Name": "Sent", "Docs": "", "Typewords": ["bool"] }, { "Name": "Trash", "Docs": "", "Typewords": ["bool"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "ModSeq", "Docs": "", "Typewords": ["ModSeq"] }, { "Name": "CreateSeq", "Docs": "", "Typewords": ["ModSeq"] }, { "Name": "HaveCounts", "Docs": "", "Typewords": ["bool"] }, { "Name": "Total", "Docs": "", "Typewords": ["int64"] }, { "Name": "Deleted", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unread", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unseen", "Docs": "", "Typewords": ["int64"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }] }, "RecipientSecurity": { "Name": "RecipientSecurity", "Docs": "", "Fields": [{ "Name": "STARTTLS", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "MTASTS", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DNSSEC", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DANE", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["SecurityResult"] }] }, "Settings": { "Name": "Settings", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["uint8"] }, { "Name": "Signature", "Docs": "", "Typewords": ["string"] }, { "Name": "Quoting", "Docs": "", "Typewords": ["Quoting"] }, { "Name": "ShowAddressSecurity", "Docs": "", "Typewords": ["bool"] }, { "Name": "ShowHTML", "Docs": "", "Typewords": ["bool"] }, { "Name": "NoShowShortcuts", "Docs": "", "Typewords": ["bool"] }, { "Name": "ShowHeaders", "Docs": "", "Typewords": ["[]", "string"] }] }, "Ruleset": { "Name": "Ruleset", "Docs": "", "Fields": [{ "Name": "SMTPMailFromRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "MsgFromRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "VerifiedDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "HeadersRegexp", "Docs": "", "Typewords": ["{}", "string"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ListAllowDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "AcceptRejectsToMailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Mailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Comment", "Docs": "", "Typewords": ["string"] }, { "Name": "VerifiedDNSDomain", "Docs": "", "Typewords": ["Domain"] }, { "Name": "ListAllowDNSDomain", "Docs": "", "Typewords": ["Domain"] }] }, @@ -328,11 +328,11 @@ var api; "ChangeMsgRemove": { "Name": "ChangeMsgRemove", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "UIDs", "Docs": "", "Typewords": ["[]", "UID"] }, { "Name": "ModSeq", "Docs": "", "Typewords": ["ModSeq"] }] }, "ChangeMsgFlags": { "Name": "ChangeMsgFlags", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "UID", "Docs": "", "Typewords": ["UID"] }, { "Name": "ModSeq", "Docs": "", "Typewords": ["ModSeq"] }, { "Name": "Mask", "Docs": "", "Typewords": ["Flags"] }, { "Name": "Flags", "Docs": "", "Typewords": ["Flags"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }] }, "ChangeMsgThread": { "Name": "ChangeMsgThread", "Docs": "", "Fields": [{ "Name": "MessageIDs", "Docs": "", "Typewords": ["[]", "int64"] }, { "Name": "Muted", "Docs": "", "Typewords": ["bool"] }, { "Name": "Collapsed", "Docs": "", "Typewords": ["bool"] }] }, - "ChangeMailboxRemove": { "Name": "ChangeMailboxRemove", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }] }, + "ChangeMailboxRemove": { "Name": "ChangeMailboxRemove", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "ModSeq", "Docs": "", "Typewords": ["ModSeq"] }] }, "ChangeMailboxAdd": { "Name": "ChangeMailboxAdd", "Docs": "", "Fields": [{ "Name": "Mailbox", "Docs": "", "Typewords": ["Mailbox"] }] }, - "ChangeMailboxRename": { "Name": "ChangeMailboxRename", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "OldName", "Docs": "", "Typewords": ["string"] }, { "Name": "NewName", "Docs": "", "Typewords": ["string"] }, { "Name": "Flags", "Docs": "", "Typewords": ["[]", "string"] }] }, + "ChangeMailboxRename": { "Name": "ChangeMailboxRename", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "OldName", "Docs": "", "Typewords": ["string"] }, { "Name": "NewName", "Docs": "", "Typewords": ["string"] }, { "Name": "Flags", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "ModSeq", "Docs": "", "Typewords": ["ModSeq"] }] }, "ChangeMailboxCounts": { "Name": "ChangeMailboxCounts", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "MailboxName", "Docs": "", "Typewords": ["string"] }, { "Name": "Total", "Docs": "", "Typewords": ["int64"] }, { "Name": "Deleted", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unread", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unseen", "Docs": "", "Typewords": ["int64"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }] }, - "ChangeMailboxSpecialUse": { "Name": "ChangeMailboxSpecialUse", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "MailboxName", "Docs": "", "Typewords": ["string"] }, { "Name": "SpecialUse", "Docs": "", "Typewords": ["SpecialUse"] }] }, + "ChangeMailboxSpecialUse": { "Name": "ChangeMailboxSpecialUse", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "MailboxName", "Docs": "", "Typewords": ["string"] }, { "Name": "SpecialUse", "Docs": "", "Typewords": ["SpecialUse"] }, { "Name": "ModSeq", "Docs": "", "Typewords": ["ModSeq"] }] }, "SpecialUse": { "Name": "SpecialUse", "Docs": "", "Fields": [{ "Name": "Archive", "Docs": "", "Typewords": ["bool"] }, { "Name": "Draft", "Docs": "", "Typewords": ["bool"] }, { "Name": "Junk", "Docs": "", "Typewords": ["bool"] }, { "Name": "Sent", "Docs": "", "Typewords": ["bool"] }, { "Name": "Trash", "Docs": "", "Typewords": ["bool"] }] }, "ChangeMailboxKeywords": { "Name": "ChangeMailboxKeywords", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "MailboxName", "Docs": "", "Typewords": ["string"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }] }, "UID": { "Name": "UID", "Docs": "", "Values": null }, diff --git a/webmail/view_test.go b/webmail/view_test.go index bafc115..d8db0ea 100644 --- a/webmail/view_test.go +++ b/webmail/view_test.go @@ -385,7 +385,7 @@ func TestView(t *testing.T) { var chmbrename ChangeMailboxRename getChanges(&chmbrename) tcompare(t, chmbrename, ChangeMailboxRename{ - ChangeRenameMailbox: store.ChangeRenameMailbox{MailboxID: chmbadd.Mailbox.ID, OldName: "Newbox", NewName: "Newbox2", Flags: nil}, + ChangeRenameMailbox: store.ChangeRenameMailbox{MailboxID: chmbadd.Mailbox.ID, OldName: "Newbox", NewName: "Newbox2", Flags: nil, ModSeq: 13}, }) // ChangeMailboxSpecialUse @@ -393,10 +393,10 @@ func TestView(t *testing.T) { var chmbspecialuseOld, chmbspecialuseNew ChangeMailboxSpecialUse getChanges(&chmbspecialuseOld, &chmbspecialuseNew) tcompare(t, chmbspecialuseOld, ChangeMailboxSpecialUse{ - ChangeMailboxSpecialUse: store.ChangeMailboxSpecialUse{MailboxID: archive.ID, MailboxName: "Archive", SpecialUse: store.SpecialUse{}}, + ChangeMailboxSpecialUse: store.ChangeMailboxSpecialUse{MailboxID: archive.ID, MailboxName: "Archive", SpecialUse: store.SpecialUse{}, ModSeq: 14}, }) tcompare(t, chmbspecialuseNew, ChangeMailboxSpecialUse{ - ChangeMailboxSpecialUse: store.ChangeMailboxSpecialUse{MailboxID: chmbadd.Mailbox.ID, MailboxName: "Newbox2", SpecialUse: store.SpecialUse{Archive: true}}, + ChangeMailboxSpecialUse: store.ChangeMailboxSpecialUse{MailboxID: chmbadd.Mailbox.ID, MailboxName: "Newbox2", SpecialUse: store.SpecialUse{Archive: true}, ModSeq: 14}, }) // ChangeMailboxRemove @@ -404,7 +404,7 @@ func TestView(t *testing.T) { var chmbremove ChangeMailboxRemove getChanges(&chmbremove) tcompare(t, chmbremove, ChangeMailboxRemove{ - ChangeRemoveMailbox: store.ChangeRemoveMailbox{MailboxID: chmbadd.Mailbox.ID, Name: "Newbox2"}, + ChangeRemoveMailbox: store.ChangeRemoveMailbox{MailboxID: chmbadd.Mailbox.ID, Name: "Newbox2", ModSeq: 15}, }) // ChangeMsgAdd diff --git a/webmail/webmail.js b/webmail/webmail.js index 2463a05..1b86140 100644 --- a/webmail/webmail.js +++ b/webmail/webmail.js @@ -309,7 +309,7 @@ var api; "SubmitMessage": { "Name": "SubmitMessage", "Docs": "", "Fields": [{ "Name": "From", "Docs": "", "Typewords": ["string"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Cc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Bcc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "ReplyTo", "Docs": "", "Typewords": ["string"] }, { "Name": "Subject", "Docs": "", "Typewords": ["string"] }, { "Name": "TextBody", "Docs": "", "Typewords": ["string"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["[]", "File"] }, { "Name": "ForwardAttachments", "Docs": "", "Typewords": ["ForwardAttachments"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ResponseMessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "UserAgent", "Docs": "", "Typewords": ["string"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["nullable", "bool"] }, { "Name": "FutureRelease", "Docs": "", "Typewords": ["nullable", "timestamp"] }, { "Name": "ArchiveThread", "Docs": "", "Typewords": ["bool"] }, { "Name": "ArchiveReferenceMailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "DraftMessageID", "Docs": "", "Typewords": ["int64"] }] }, "File": { "Name": "File", "Docs": "", "Fields": [{ "Name": "Filename", "Docs": "", "Typewords": ["string"] }, { "Name": "DataURI", "Docs": "", "Typewords": ["string"] }] }, "ForwardAttachments": { "Name": "ForwardAttachments", "Docs": "", "Fields": [{ "Name": "MessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Paths", "Docs": "", "Typewords": ["[]", "[]", "int32"] }] }, - "Mailbox": { "Name": "Mailbox", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "UIDValidity", "Docs": "", "Typewords": ["uint32"] }, { "Name": "UIDNext", "Docs": "", "Typewords": ["UID"] }, { "Name": "Archive", "Docs": "", "Typewords": ["bool"] }, { "Name": "Draft", "Docs": "", "Typewords": ["bool"] }, { "Name": "Junk", "Docs": "", "Typewords": ["bool"] }, { "Name": "Sent", "Docs": "", "Typewords": ["bool"] }, { "Name": "Trash", "Docs": "", "Typewords": ["bool"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "HaveCounts", "Docs": "", "Typewords": ["bool"] }, { "Name": "Total", "Docs": "", "Typewords": ["int64"] }, { "Name": "Deleted", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unread", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unseen", "Docs": "", "Typewords": ["int64"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }] }, + "Mailbox": { "Name": "Mailbox", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "UIDValidity", "Docs": "", "Typewords": ["uint32"] }, { "Name": "UIDNext", "Docs": "", "Typewords": ["UID"] }, { "Name": "Archive", "Docs": "", "Typewords": ["bool"] }, { "Name": "Draft", "Docs": "", "Typewords": ["bool"] }, { "Name": "Junk", "Docs": "", "Typewords": ["bool"] }, { "Name": "Sent", "Docs": "", "Typewords": ["bool"] }, { "Name": "Trash", "Docs": "", "Typewords": ["bool"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "ModSeq", "Docs": "", "Typewords": ["ModSeq"] }, { "Name": "CreateSeq", "Docs": "", "Typewords": ["ModSeq"] }, { "Name": "HaveCounts", "Docs": "", "Typewords": ["bool"] }, { "Name": "Total", "Docs": "", "Typewords": ["int64"] }, { "Name": "Deleted", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unread", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unseen", "Docs": "", "Typewords": ["int64"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }] }, "RecipientSecurity": { "Name": "RecipientSecurity", "Docs": "", "Fields": [{ "Name": "STARTTLS", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "MTASTS", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DNSSEC", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DANE", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["SecurityResult"] }] }, "Settings": { "Name": "Settings", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["uint8"] }, { "Name": "Signature", "Docs": "", "Typewords": ["string"] }, { "Name": "Quoting", "Docs": "", "Typewords": ["Quoting"] }, { "Name": "ShowAddressSecurity", "Docs": "", "Typewords": ["bool"] }, { "Name": "ShowHTML", "Docs": "", "Typewords": ["bool"] }, { "Name": "NoShowShortcuts", "Docs": "", "Typewords": ["bool"] }, { "Name": "ShowHeaders", "Docs": "", "Typewords": ["[]", "string"] }] }, "Ruleset": { "Name": "Ruleset", "Docs": "", "Fields": [{ "Name": "SMTPMailFromRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "MsgFromRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "VerifiedDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "HeadersRegexp", "Docs": "", "Typewords": ["{}", "string"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ListAllowDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "AcceptRejectsToMailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Mailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Comment", "Docs": "", "Typewords": ["string"] }, { "Name": "VerifiedDNSDomain", "Docs": "", "Typewords": ["Domain"] }, { "Name": "ListAllowDNSDomain", "Docs": "", "Typewords": ["Domain"] }] }, @@ -328,11 +328,11 @@ var api; "ChangeMsgRemove": { "Name": "ChangeMsgRemove", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "UIDs", "Docs": "", "Typewords": ["[]", "UID"] }, { "Name": "ModSeq", "Docs": "", "Typewords": ["ModSeq"] }] }, "ChangeMsgFlags": { "Name": "ChangeMsgFlags", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "UID", "Docs": "", "Typewords": ["UID"] }, { "Name": "ModSeq", "Docs": "", "Typewords": ["ModSeq"] }, { "Name": "Mask", "Docs": "", "Typewords": ["Flags"] }, { "Name": "Flags", "Docs": "", "Typewords": ["Flags"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }] }, "ChangeMsgThread": { "Name": "ChangeMsgThread", "Docs": "", "Fields": [{ "Name": "MessageIDs", "Docs": "", "Typewords": ["[]", "int64"] }, { "Name": "Muted", "Docs": "", "Typewords": ["bool"] }, { "Name": "Collapsed", "Docs": "", "Typewords": ["bool"] }] }, - "ChangeMailboxRemove": { "Name": "ChangeMailboxRemove", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }] }, + "ChangeMailboxRemove": { "Name": "ChangeMailboxRemove", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "ModSeq", "Docs": "", "Typewords": ["ModSeq"] }] }, "ChangeMailboxAdd": { "Name": "ChangeMailboxAdd", "Docs": "", "Fields": [{ "Name": "Mailbox", "Docs": "", "Typewords": ["Mailbox"] }] }, - "ChangeMailboxRename": { "Name": "ChangeMailboxRename", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "OldName", "Docs": "", "Typewords": ["string"] }, { "Name": "NewName", "Docs": "", "Typewords": ["string"] }, { "Name": "Flags", "Docs": "", "Typewords": ["[]", "string"] }] }, + "ChangeMailboxRename": { "Name": "ChangeMailboxRename", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "OldName", "Docs": "", "Typewords": ["string"] }, { "Name": "NewName", "Docs": "", "Typewords": ["string"] }, { "Name": "Flags", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "ModSeq", "Docs": "", "Typewords": ["ModSeq"] }] }, "ChangeMailboxCounts": { "Name": "ChangeMailboxCounts", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "MailboxName", "Docs": "", "Typewords": ["string"] }, { "Name": "Total", "Docs": "", "Typewords": ["int64"] }, { "Name": "Deleted", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unread", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unseen", "Docs": "", "Typewords": ["int64"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }] }, - "ChangeMailboxSpecialUse": { "Name": "ChangeMailboxSpecialUse", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "MailboxName", "Docs": "", "Typewords": ["string"] }, { "Name": "SpecialUse", "Docs": "", "Typewords": ["SpecialUse"] }] }, + "ChangeMailboxSpecialUse": { "Name": "ChangeMailboxSpecialUse", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "MailboxName", "Docs": "", "Typewords": ["string"] }, { "Name": "SpecialUse", "Docs": "", "Typewords": ["SpecialUse"] }, { "Name": "ModSeq", "Docs": "", "Typewords": ["ModSeq"] }] }, "SpecialUse": { "Name": "SpecialUse", "Docs": "", "Fields": [{ "Name": "Archive", "Docs": "", "Typewords": ["bool"] }, { "Name": "Draft", "Docs": "", "Typewords": ["bool"] }, { "Name": "Junk", "Docs": "", "Typewords": ["bool"] }, { "Name": "Sent", "Docs": "", "Typewords": ["bool"] }, { "Name": "Trash", "Docs": "", "Typewords": ["bool"] }] }, "ChangeMailboxKeywords": { "Name": "ChangeMailboxKeywords", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "MailboxName", "Docs": "", "Typewords": ["string"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }] }, "UID": { "Name": "UID", "Docs": "", "Values": null }, diff --git a/webops/xops.go b/webops/xops.go index 24ff241..b301b2d 100644 --- a/webops/xops.go +++ b/webops/xops.go @@ -61,7 +61,8 @@ func (x XOps) MessageDelete(ctx context.Context, log mlog.Log, acc *store.Accoun var changes []store.Change x.DBWrite(ctx, acc, func(tx *bstore.Tx) { - _, changes = x.MessageDeleteTx(ctx, log, tx, acc, messageIDs, 0) + var modseq store.ModSeq + changes = x.MessageDeleteTx(ctx, log, tx, acc, messageIDs, &modseq) }) store.BroadcastChanges(acc, changes) @@ -74,7 +75,7 @@ func (x XOps) MessageDelete(ctx context.Context, log mlog.Log, acc *store.Accoun } } -func (x XOps) MessageDeleteTx(ctx context.Context, log mlog.Log, tx *bstore.Tx, acc *store.Account, messageIDs []int64, modseq store.ModSeq) (store.ModSeq, []store.Change) { +func (x XOps) MessageDeleteTx(ctx context.Context, log mlog.Log, tx *bstore.Tx, acc *store.Account, messageIDs []int64, modseq *store.ModSeq) []store.Change { removeChanges := map[int64]store.ChangeRemoveUIDs{} changes := make([]store.Change, 0, len(messageIDs)+1) // n remove, 1 mailbox counts @@ -86,8 +87,15 @@ func (x XOps) MessageDeleteTx(ctx context.Context, log mlog.Log, tx *bstore.Tx, m := x.messageID(ctx, tx, mid) totalSize += m.Size + if *modseq == 0 { + var err error + *modseq, err = acc.NextModSeq(tx) + x.Checkf(ctx, err, "assigning next modseq") + } + if m.MailboxID != mb.ID { if mb.ID != 0 { + mb.ModSeq = *modseq err := tx.Update(&mb) x.Checkf(ctx, err, "updating mailbox counts") changes = append(changes, mb.ChangeCounts()) @@ -102,24 +110,21 @@ func (x XOps) MessageDeleteTx(ctx context.Context, log mlog.Log, tx *bstore.Tx, mb.Sub(m.MailboxCounts()) - if modseq == 0 { - modseq, err = acc.NextModSeq(tx) - x.Checkf(ctx, err, "assigning next modseq") - } m.Expunged = true - m.ModSeq = modseq + m.ModSeq = *modseq err = tx.Update(&m) x.Checkf(ctx, err, "marking message as expunged") ch := removeChanges[m.MailboxID] ch.UIDs = append(ch.UIDs, m.UID) ch.MailboxID = m.MailboxID - ch.ModSeq = modseq + ch.ModSeq = *modseq removeChanges[m.MailboxID] = ch remove = append(remove, m) } if mb.ID != 0 { + mb.ModSeq = *modseq err := tx.Update(&mb) x.Checkf(ctx, err, "updating count in mailbox") changes = append(changes, mb.ChangeCounts()) @@ -144,7 +149,7 @@ func (x XOps) MessageDeleteTx(ctx context.Context, log mlog.Log, tx *bstore.Tx, changes = append(changes, ch) } - return modseq, changes + return changes } func (x XOps) MessageFlagsAdd(ctx context.Context, log mlog.Log, acc *store.Account, messageIDs []int64, flaglist []string) { @@ -162,8 +167,14 @@ func (x XOps) MessageFlagsAdd(ctx context.Context, log mlog.Log, acc *store.Acco for _, mid := range messageIDs { m := x.messageID(ctx, tx, mid) + if modseq == 0 { + modseq, err = acc.NextModSeq(tx) + x.Checkf(ctx, err, "assigning next modseq") + } + if mb.ID != m.MailboxID { if mb.ID != 0 { + mb.ModSeq = modseq err := tx.Update(&mb) x.Checkf(ctx, err, "updating mailbox") if mb.MailboxCounts != origmb.MailboxCounts { @@ -189,10 +200,6 @@ func (x XOps) MessageFlagsAdd(ctx context.Context, log mlog.Log, acc *store.Acco continue } - if modseq == 0 { - modseq, err = acc.NextModSeq(tx) - x.Checkf(ctx, err, "assigning next modseq") - } m.ModSeq = modseq err = tx.Update(&m) x.Checkf(ctx, err, "updating message") @@ -202,6 +209,7 @@ func (x XOps) MessageFlagsAdd(ctx context.Context, log mlog.Log, acc *store.Acco } if mb.ID != 0 { + mb.ModSeq = modseq err := tx.Update(&mb) x.Checkf(ctx, err, "updating mailbox") if mb.MailboxCounts != origmb.MailboxCounts { @@ -235,8 +243,14 @@ func (x XOps) MessageFlagsClear(ctx context.Context, log mlog.Log, acc *store.Ac for _, mid := range messageIDs { m := x.messageID(ctx, tx, mid) + if modseq == 0 { + modseq, err = acc.NextModSeq(tx) + x.Checkf(ctx, err, "assigning next modseq") + } + if mb.ID != m.MailboxID { if mb.ID != 0 { + mb.ModSeq = modseq err := tx.Update(&mb) x.Checkf(ctx, err, "updating counts for mailbox") if mb.MailboxCounts != origmb.MailboxCounts { @@ -259,10 +273,6 @@ func (x XOps) MessageFlagsClear(ctx context.Context, log mlog.Log, acc *store.Ac continue } - if modseq == 0 { - modseq, err = acc.NextModSeq(tx) - x.Checkf(ctx, err, "assigning next modseq") - } m.ModSeq = modseq err = tx.Update(&m) x.Checkf(ctx, err, "updating message") @@ -272,6 +282,7 @@ func (x XOps) MessageFlagsClear(ctx context.Context, log mlog.Log, acc *store.Ac } if mb.ID != 0 { + mb.ModSeq = modseq err := tx.Update(&mb) x.Checkf(ctx, err, "updating keywords in mailbox") if mb.MailboxCounts != origmb.MailboxCounts { @@ -333,6 +344,7 @@ func (x XOps) MailboxesMarkRead(ctx context.Context, log mlog.Log, acc *store.Ac x.Checkf(ctx, err, "listing messages to mark as read") if have { + mb.ModSeq = modseq err := tx.Update(&mb) x.Checkf(ctx, err, "updating mailbox") changes = append(changes, mb.ChangeCounts()) @@ -366,14 +378,15 @@ func (x XOps) MessageMove(ctx context.Context, log mlog.Log, acc *store.Account, return } - _, changes = x.MessageMoveTx(ctx, log, acc, tx, messageIDs, mbDst, 0) + var modseq store.ModSeq + changes = x.MessageMoveTx(ctx, log, acc, tx, messageIDs, mbDst, &modseq) }) store.BroadcastChanges(acc, changes) }) } -func (x XOps) MessageMoveTx(ctx context.Context, log mlog.Log, acc *store.Account, tx *bstore.Tx, messageIDs []int64, mbDst store.Mailbox, modseq store.ModSeq) (store.ModSeq, []store.Change) { +func (x XOps) MessageMoveTx(ctx context.Context, log mlog.Log, acc *store.Account, tx *bstore.Tx, messageIDs []int64, mbDst store.Mailbox, modseq *store.ModSeq) []store.Change { retrain := make([]store.Message, 0, len(messageIDs)) removeChanges := map[int64]store.ChangeRemoveUIDs{} // n adds, 1 remove, 2 mailboxcounts, optimistic and at least for a single message. @@ -384,12 +397,19 @@ func (x XOps) MessageMoveTx(ctx context.Context, log mlog.Log, acc *store.Accoun keywords := map[string]struct{}{} now := time.Now() + var err error + if *modseq == 0 { + *modseq, err = acc.NextModSeq(tx) + x.Checkf(ctx, err, "assigning next modseq") + } + for _, mid := range messageIDs { m := x.messageID(ctx, tx, mid) // We may have loaded this mailbox in the previous iteration of this loop. if m.MailboxID != mbSrc.ID { if mbSrc.ID != 0 { + mbSrc.ModSeq = *modseq err := tx.Update(&mbSrc) x.Checkf(ctx, err, "updating source mailbox counts") changes = append(changes, mbSrc.ChangeCounts()) @@ -402,15 +422,9 @@ func (x XOps) MessageMoveTx(ctx context.Context, log mlog.Log, acc *store.Accoun x.Checkuserf(ctx, errors.New("already in destination mailbox"), "moving message") } - var err error - if modseq == 0 { - modseq, err = acc.NextModSeq(tx) - x.Checkf(ctx, err, "assigning next modseq") - } - ch := removeChanges[m.MailboxID] ch.UIDs = append(ch.UIDs, m.UID) - ch.ModSeq = modseq + ch.ModSeq = *modseq ch.MailboxID = m.MailboxID removeChanges[m.MailboxID] = ch @@ -418,7 +432,7 @@ func (x XOps) MessageMoveTx(ctx context.Context, log mlog.Log, acc *store.Accoun om := m om.PrepareExpunge() om.ID = 0 // Assign new ID. - om.ModSeq = modseq + om.ModSeq = *modseq mbSrc.Sub(m.MailboxCounts()) @@ -435,7 +449,7 @@ func (x XOps) MessageMoveTx(ctx context.Context, log mlog.Log, acc *store.Accoun m.Seen = false } m.UID = mbDst.UIDNext - m.ModSeq = modseq + m.ModSeq = *modseq mbDst.UIDNext++ m.JunkFlagsForMailbox(mbDst, conf) m.SaveDate = &now @@ -456,8 +470,9 @@ func (x XOps) MessageMoveTx(ctx context.Context, log mlog.Log, acc *store.Accoun } } - err := tx.Update(&mbSrc) - x.Checkf(ctx, err, "updating source mailbox counts") + mbSrc.ModSeq = *modseq + err = tx.Update(&mbSrc) + x.Checkf(ctx, err, "updating source mailbox counts and modseq") changes = append(changes, mbSrc.ChangeCounts(), mbDst.ChangeCounts()) @@ -468,8 +483,9 @@ func (x XOps) MessageMoveTx(ctx context.Context, log mlog.Log, acc *store.Accoun changes = append(changes, mbDst.ChangeKeywords()) } + mbDst.ModSeq = *modseq err = tx.Update(&mbDst) - x.Checkf(ctx, err, "updating mailbox with uidnext") + x.Checkf(ctx, err, "updating destination mailbox with uidnext and modseq") err = acc.RetrainMessages(ctx, log, tx, retrain, false) x.Checkf(ctx, err, "retraining messages after move") @@ -484,7 +500,7 @@ func (x XOps) MessageMoveTx(ctx context.Context, log mlog.Log, acc *store.Accoun changes = append(changes, ch) } - return modseq, changes + return changes } func isText(p message.Part) bool {