update modseq when changing mailbox/server metadata, and also for specialuse changes, and keep track of modseq for mailboxes

i added the metadata extension to the imapserver recently. then i wondered how
a client would efficiently find changed metadata. turns out the qresync rfc
mentions that metadata changes should set a new modseq on the mailbox.
shouldn't be hard, except that we were not explicitly keeping track of modseqs
per mailbox. we only kept them for messages, and we were just looking up the
latest message modseq when we needed the modseq (we keep db entries for
expunged messages, so this worked out fine). that approach isn't enough
anymore. so know we keep track of modseq & createseq for mailboxes, just as for
messages. and we also track modseq/createseq for annotations. there's a good
chance jmap is going to need it.

this also adds consistency checks for modseq/createseq on mailboxes and
annotations to the account storage. it helped spot cases i missed where the
values need to be updated.
This commit is contained in:
Mechiel Lukkien 2025-02-22 22:44:15 +01:00
parent 7c7473ef0e
commit 9f3cb7340b
No known key found for this signature in database
19 changed files with 443 additions and 174 deletions

View File

@ -37,12 +37,12 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
tc.client.Login("mjl@mox.example", password0) tc.client.Login("mjl@mox.example", password0)
tc.client.Enable(capability) tc.client.Enable(capability)
tc.transactf("ok", "Select inbox") 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. // First some tests without any messages.
tc.transactf("ok", "Status inbox (Highestmodseq)") 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. // No messages, no matches.
tc.transactf("ok", "Uid Fetch 1:* (Flags) (Changedsince 12345)") 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.Enable(capability)
tc3.client.Select("inbox") 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. // 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. // 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 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.transactf("ok", "Append inbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
tc.xuntagged(imapclient.UntaggedExists(4)) tc.xuntagged(imapclient.UntaggedExists(4))
tc.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UID: 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. tc.transactf("bad", `Fetch 1 Flags (Changedsince 0)`) // 0 not allowed in syntax.
mox.SetPedantic(false) mox.SetPedantic(false)
tc.transactf("ok", "Uid fetch 1 (Flags) (Changedsince 0)") 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)}}) tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(1)}})
clientModseq += 4 // Four messages, over two mailboxes, modseq is per account.
// Check highestmodseq for mailboxes. // Check highestmodseq for mailboxes.
tc.transactf("ok", "Status inbox (highestmodseq)") 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.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. // Check highestmodseq when we select.
tc.transactf("ok", "Examine otherbox") 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.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. // Check fetch modseq response and changedsince.
tc.transactf("ok", `Fetch 1 (Modseq)`) 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.transactf("ok", `Fetch 1 Flags (Changedsince 1)`)
tc.xuntagged() tc.xuntagged()
tc.transactf("ok", `Fetch 1,4 Flags (Changedsince 1)`) 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.transactf("ok", `Fetch 2 Flags (Changedsince 2)`)
tc.xuntagged() tc.xuntagged()
@ -308,25 +309,25 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
tc.transactf("ok", `Fetch 1:* (Modseq)`) tc.transactf("ok", `Fetch 1:* (Modseq)`)
tc.xuntagged( 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: 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: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), imapclient.FetchModSeq(5)}},
imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), 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. // 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() tc.xuntagged()
// search // search
tc.transactf("ok", "Search Modseq 7")
tc.xsearchmodseq(7, 1)
tc.transactf("ok", "Search Modseq 8") tc.transactf("ok", "Search Modseq 8")
tc.xsearchmodseq(8, 1)
tc.transactf("ok", "Search Modseq 9")
tc.xsearch() tc.xsearch()
// esearch // 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.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}) tc.xuntagged(imapclient.UntaggedEsearch{Correlator: tc.client.LastTag})
// store, cannot modify expunged messages. // store, cannot modify expunged messages.
@ -543,8 +544,8 @@ func testQresync(t *testing.T, tc *testconn, clientModseq int64) {
noflags := imapclient.FetchFlags(nil) noflags := imapclient.FetchFlags(nil)
tc.xuntagged( tc.xuntagged(
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")}, 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: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(5)}},
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)}}, 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( tc.xuntagged(
makeUntagged( makeUntagged(
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")}, 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: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(5)}},
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)}}, 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( tc.xuntagged(
makeUntagged( makeUntagged(
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")}, 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: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(5)}},
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)}}, 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.transactf("ok", "Select inbox (Qresync (1 1 1,2,5:6))")
tc.xuntagged( tc.xuntagged(
makeUntagged( 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)}},
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)}}, 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.transactf("ok", "Select inbox (Qresync (1 1 5))")
tc.xuntagged( tc.xuntagged(
makeUntagged( 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( tc.xuntagged(
makeUntagged( makeUntagged(
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")}, 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: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(5)}},
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)}}, 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( tc.xuntagged(
makeUntagged( makeUntagged(
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")}, 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)}}, imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}},
)..., )...,
) )
tc.transactf("ok", "Close") 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( tc.xuntagged(
makeUntagged( makeUntagged(
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")}, 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 // since that time. Server detects this, sends full vanished history and continues
// working with modseq changed to 1 before the expunged uid. // working with modseq changed to 1 before the expunged uid.
tc.transactf("ok", "Close") 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( tc.xuntagged(
makeUntagged( 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."}}, 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."}},

View File

@ -238,8 +238,9 @@ func (c *conn) cmdxFetch(isUID bool, tag, cmdstr string, p *parser) {
} }
var zeromc store.MailboxCounts 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.Add(cmd.deltaCounts) // Unseen/Unread will be <= 0.
mb.ModSeq = cmd.modseq
err := tx.Update(&mb) err := tx.Update(&mb)
xcheckf(err, "updating mailbox counts") xcheckf(err, "updating mailbox counts")
cmd.changes = append(cmd.changes, mb.ChangeCounts()) cmd.changes = append(cmd.changes, mb.ChangeCounts())

View File

@ -232,6 +232,7 @@ func (c *conn) cmdSetmetadata(tag, cmd string, p *parser) {
// Store the annotations, possibly removing/inserting/updating them. // Store the annotations, possibly removing/inserting/updating them.
c.account.WithWLock(func() { c.account.WithWLock(func() {
var changes []store.Change var changes []store.Change
var modseq store.ModSeq
c.xdbwrite(func(tx *bstore.Tx) { c.xdbwrite(func(tx *bstore.Tx) {
var mb store.Mailbox // mb.ID as 0 is used in query below. 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 continue
} }
if modseq == 0 {
var err error
modseq, err = c.account.NextModSeq(tx)
xcheckf(err, "get next modseq")
}
a.MailboxID = mb.ID a.MailboxID = mb.ID
a.ModSeq = modseq
a.CreateSeq = modseq
oa, err := q.Get() oa, err := q.Get()
if err == bstore.ErrAbsent { if err == bstore.ErrAbsent {
@ -266,9 +275,7 @@ func (c *conn) cmdSetmetadata(tag, cmd string, p *parser) {
continue continue
} }
xcheckf(err, "looking up existing annotation for entry name") 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 oa.Value = a.Value
err = tx.Update(&oa) err = tx.Update(&oa)
xcheckf(err, "updating metadata annotation") xcheckf(err, "updating metadata annotation")
@ -293,6 +300,13 @@ func (c *conn) cmdSetmetadata(tag, cmd string, p *parser) {
return nil return nil
}) })
xcheckf(err, "checking metadata annotation size") 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) c.broadcast(changes)

View File

@ -674,19 +674,6 @@ func (c *conn) xreadliteral(size int64, sync bool) []byte {
return buf 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. var cleanClose struct{} // Sentinel value for panic/recover indicating clean close of connection.
// serve handles a single IMAP connection on nc. // serve handles a single IMAP connection on nc.
@ -2461,15 +2448,16 @@ func (c *conn) xensureCondstore(tx *bstore.Tx) {
if c.mailboxID <= 0 { if c.mailboxID <= 0 {
return return
} }
var modseq store.ModSeq
if tx != nil { var mb store.Mailbox
modseq = c.xhighestModSeq(tx, c.mailboxID) if tx == nil {
} else {
c.xdbread(func(tx *bstore.Tx) { 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. // Condstore extension, find the highest modseq.
if c.enabled[capCondstore] { 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 // For QRESYNC, we need to know the highest modset of deleted expunged records to
// maintain synchronization. // maintain synchronization.
@ -2935,6 +2923,8 @@ func (c *conn) cmdRename(tag, cmd string, p *parser) {
c.xdbwrite(func(tx *bstore.Tx) { c.xdbwrite(func(tx *bstore.Tx) {
srcMB := c.xmailbox(tx, src, "NONEXISTENT") srcMB := c.xmailbox(tx, src, "NONEXISTENT")
var modseq store.ModSeq
// Inbox is very special. Unlike other mailboxes, its children are not moved. And // 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 // 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. // 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) uidval, err := c.account.NextUIDValidity(tx)
xcheckf(err, "next uid validity") xcheckf(err, "next uid validity")
modseq, err = c.account.NextModSeq(tx)
xcheckf(err, "assigning next modseq")
dstMB := store.Mailbox{ dstMB := store.Mailbox{
Name: dst, Name: dst,
UIDValidity: uidval, UIDValidity: uidval,
UIDNext: 1, UIDNext: 1,
Keywords: srcMB.Keywords, Keywords: srcMB.Keywords,
ModSeq: modseq,
CreateSeq: modseq,
HaveCounts: true, HaveCounts: true,
} }
err = tx.Insert(&dstMB) err = tx.Insert(&dstMB)
xcheckf(err, "create new destination mailbox") 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. changes = make([]store.Change, 2) // Placeholders filled in below.
// Move existing messages, with their ID's and on-disk files intact, to the new // 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) err = tx.Update(&dstMB)
xcheckf(err, "updating uidnext and counts in destination mailbox") xcheckf(err, "updating uidnext and counts in destination mailbox")
srcMB.ModSeq = modseq
err = tx.Update(&srcMB) err = tx.Update(&srcMB)
xcheckf(err, "updating counts for inbox") xcheckf(err, "updating counts for inbox")
@ -3021,12 +3014,14 @@ func (c *conn) cmdRename(tag, cmd string, p *parser) {
for i := range annotations { for i := range annotations {
annotations[i].ID = 0 annotations[i].ID = 0
annotations[i].MailboxID = dstMB.ID annotations[i].MailboxID = dstMB.ID
annotations[i].ModSeq = modseq
annotations[i].CreateSeq = modseq
err := tx.Insert(&annotations[i]) err := tx.Insert(&annotations[i])
xcheckf(err, "copy annotation to destination mailbox") xcheckf(err, "copy annotation to destination mailbox")
} }
changes[0] = store.ChangeRemoveUIDs{MailboxID: srcMB.ID, UIDs: oldUIDs, ModSeq: modseq} 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[2:...] are ChangeAddUIDs
changes = append(changes, srcMB.ChangeCounts(), dstMB.ChangeCounts()) changes = append(changes, srcMB.ChangeCounts(), dstMB.ChangeCounts())
for _, a := range annotations { for _, a := range annotations {
@ -3038,7 +3033,7 @@ func (c *conn) cmdRename(tag, cmd string, p *parser) {
var notExists, alreadyExists bool var notExists, alreadyExists bool
var err error 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 { if notExists {
// ../rfc/9051:5140 // ../rfc/9051:5140
xusercodeErrorf("NONEXISTENT", "%s", err) 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") status = append(status, A, "NIL")
case "HIGHESTMODSEQ": case "HIGHESTMODSEQ":
// ../rfc/7162:366 // ../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": case "DELETED-STORAGE":
// ../rfc/9208:394 // ../rfc/9208:394
// How much storage space could be reclaimed by expunging messages with the // 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") xcheckf(err, "listing messages to delete")
if len(remove) == 0 { if len(remove) == 0 {
highestModSeq = c.xhighestModSeq(tx, c.mailboxID) highestModSeq = mb.ModSeq
return return
} }
@ -3668,6 +3663,7 @@ func (c *conn) xexpunge(uidSet *numSet, missingMailboxOK bool) (remove []store.M
modseq, err = c.account.NextModSeq(tx) modseq, err = c.account.NextModSeq(tx)
xcheckf(err, "assigning next modseq") xcheckf(err, "assigning next modseq")
highestModSeq = modseq highestModSeq = modseq
mb.ModSeq = modseq
removeIDs := make([]int64, len(remove)) removeIDs := make([]int64, len(remove))
anyIDs := make([]any, 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 var err error
modseq, err = c.account.NextModSeq(tx) modseq, err = c.account.NextModSeq(tx)
xcheckf(err, "assigning next modseq") 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. // Reserve the uids in the destination mailbox.
uidFirst := mbDst.UIDNext uidFirst := mbDst.UIDNext
@ -4133,6 +4134,8 @@ func (c *conn) cmdxMove(isUID bool, tag, cmd string, p *parser) {
var err error var err error
modseq, err = c.account.NextModSeq(tx) modseq, err = c.account.NextModSeq(tx)
xcheckf(err, "assigning next modseq") xcheckf(err, "assigning next modseq")
mbSrc.ModSeq = modseq
mbDst.ModSeq = modseq
// Update existing record with new UID and MailboxID in database for messages. We // 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. // 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) err = tx.Update(&mbSrc)
xcheckf(err, "updating source mailbox counts") xcheckf(err, "updating source mailbox counts and modseq")
err = tx.Update(&mbDst) err = tx.Update(&mbDst)
xcheckf(err, "updating destination mailbox for uids, keywords and counts") 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") 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) err := tx.Update(&mb)
xcheckf(err, "updating mailbox counts") xcheckf(err, "updating mailbox counts")

View File

@ -298,7 +298,7 @@ func importctl(ctx context.Context, ctl *ctl, mbox bool) {
a.WithWLock(func() { a.WithWLock(func() {
// Ensure mailbox exists. // Ensure mailbox exists.
var mb store.Mailbox 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") ctl.xcheck(err, "ensuring mailbox exists")
// We ensure keywords in messages make it to the mailbox as well. // We ensure keywords in messages make it to the mailbox as well.

View File

@ -3404,7 +3404,7 @@ open, or is not running.
// Reassign UIDs, going per mailbox. We assign starting at 1, only changing the // 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 // 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 // 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) modseq, err := a.NextModSeq(tx)
xcheckf(err, "assigning next modseq") xcheckf(err, "assigning next modseq")
@ -3429,7 +3429,7 @@ open, or is not running.
return fmt.Errorf("reading through messages: %v", err) 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 { err = bstore.QueryTx[store.Mailbox](tx).ForEach(func(mb store.Mailbox) error {
// Assign each mailbox a completely new uidvalidity. // Assign each mailbox a completely new uidvalidity.
uidvalidity, err := a.NextUIDValidity(tx) uidvalidity, err := a.NextUIDValidity(tx)
@ -3449,6 +3449,7 @@ open, or is not running.
mb.UIDValidity = uidvalidity mb.UIDValidity = uidvalidity
} }
mb.UIDNext = uidlasts[mb.ID] + 1 mb.UIDNext = uidlasts[mb.ID] + 1
mb.ModSeq = modseq
if err := tx.Update(&mb); err != nil { if err := tx.Update(&mb); err != nil {
return fmt.Errorf("updating uidvalidity and uidnext for mailbox: %v", err) return fmt.Errorf("updating uidvalidity and uidnext for mailbox: %v", err)
} }

View File

@ -212,6 +212,11 @@ type Mailbox struct {
// lower case (for JMAP), sorted. // lower case (for JMAP), sorted.
Keywords []string 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. HaveCounts bool // Whether MailboxCounts have been initialized.
MailboxCounts // Statistics about messages, kept up to date whenever a change happens. 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. IsString bool // If true, the value is a string instead of bytes.
Value []byte Value []byte
ModSeq ModSeq
CreateSeq ModSeq
} }
// Change returns a broadcastable change for the annotation. // Change returns a broadcastable change for the annotation.
func (a Annotation) Change(mailboxName string) ChangeAnnotation { 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. // 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 // ChangeSpecialUse returns a change for special-use flags, for broadcasting to
// other connections. // other connections.
func (mb Mailbox) ChangeSpecialUse() ChangeMailboxSpecialUse { 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 // ChangeKeywords returns a change with new keywords for a mailbox (e.g. after
@ -872,8 +880,16 @@ type Account struct {
} }
type Upgrade struct { type Upgrade struct {
ID byte ID byte
Threads byte // 0: None, 1: Adding MessageID's completed, 2: Adding ThreadID's completed. 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. // 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 { if err != nil {
return nil, fmt.Errorf("checking message threading: %v", err) 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 { if up.Threads == 2 {
close(acc.threadsCompleted) close(acc.threadsCompleted)
return acc, nil 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 { if err != nil {
a.threadsErr = err a.threadsErr = err
log.Errorx("upgrading account for threading, aborted", err, slog.String("account", a.Name)) 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 { return db.Write(context.TODO(), func(tx *bstore.Tx) error {
uidvalidity := InitialUIDValidity() uidvalidity := InitialUIDValidity()
if err := tx.Insert(&Upgrade{ID: 1, Threads: 2}); err != nil { if err := tx.Insert(&upgradeInit); err != nil {
return err return err
} }
if err := tx.Insert(&DiskUsage{ID: 1}); err != nil { if err := tx.Insert(&DiskUsage{ID: 1}); err != nil {
@ -1113,6 +1179,11 @@ func initAccount(db *bstore.DB) error {
return err return err
} }
modseq, err := nextModSeq(tx)
if err != nil {
return fmt.Errorf("get next modseq: %v", err)
}
if len(mox.Conf.Static.DefaultMailboxes) > 0 { if len(mox.Conf.Static.DefaultMailboxes) > 0 {
// Deprecated in favor of InitialMailboxes. // Deprecated in favor of InitialMailboxes.
defaultMailboxes := mox.Conf.Static.DefaultMailboxes defaultMailboxes := mox.Conf.Static.DefaultMailboxes
@ -1124,7 +1195,7 @@ func initAccount(db *bstore.DB) error {
mailboxes = append(mailboxes, name) mailboxes = append(mailboxes, name)
} }
for _, name := range mailboxes { 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") { if strings.HasPrefix(name, "Archive") {
mb.Archive = true mb.Archive = true
} else if strings.HasPrefix(name, "Drafts") { } else if strings.HasPrefix(name, "Drafts") {
@ -1151,7 +1222,7 @@ func initAccount(db *bstore.DB) error {
} }
add := func(name string, use SpecialUse) 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 { if err := tx.Insert(&mb); err != nil {
return fmt.Errorf("creating mailbox: %w", err) return fmt.Errorf("creating mailbox: %w", err)
} }
@ -1230,8 +1301,10 @@ func (a *Account) Close() error {
// - Incorrect total message size. // - Incorrect total message size.
// - Message with UID >= mailbox uid next. // - Message with UID >= mailbox uid next.
// - Mailbox uidvalidity >= account uid validity. // - 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. // - 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 { func (a *Account) CheckConsistency() error {
var uidErrors []string // With a limit, could be many. var uidErrors []string // With a limit, could be many.
var modseqErrors []string // With limit. 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) 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) 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 return nil
}) })
if err != 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{} 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, // NextModSeq returns the next modification sequence, which is global per account,
// over all types. // over all types.
func (a *Account) NextModSeq(tx *bstore.Tx) (ModSeq, error) { func (a *Account) NextModSeq(tx *bstore.Tx) (ModSeq, error) {
return nextModSeq(tx)
}
func nextModSeq(tx *bstore.Tx) (ModSeq, error) {
v := SyncState{ID: 1} v := SyncState{ID: 1}
if err := tx.Get(&v); err == bstore.ErrAbsent { if err := tx.Get(&v); err == bstore.ErrAbsent {
// We start assigning from modseq 2. Modseq 0 is not usable, so returned as 1, so // 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 m.UID = mb.UIDNext
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 { if err := tx.Update(&mb); err != nil {
return fmt.Errorf("updating mailbox nextuid: %w", err) 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 { if m.MailboxDestinedID != 0 && m.MailboxDestinedID == m.MailboxOrigID {
m.MailboxDestinedID = 0 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 == "" { if part != nil && m.MessageID == "" && m.SubjectBase == "" {
m.PrepareThreading(log, part) 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 // The leaf mailbox is created with special-use flags, taking the flags away from
// other mailboxes, and reflecting that in the returned changes. // 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 hold account wlock.
// Caller must propagate changes if any. // 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 { if norm.NFC.String(name) != name {
return Mailbox{}, nil, fmt.Errorf("mailbox name not normalized") 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 { if err != nil {
return Mailbox{}, nil, fmt.Errorf("next uid validity: %v", err) 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{ mb = Mailbox{
Name: p, Name: p,
UIDValidity: uidval, UIDValidity: uidval,
UIDNext: 1, UIDNext: 1,
ModSeq: *modseq,
CreateSeq: *modseq,
HaveCounts: true, HaveCounts: true,
} }
err = tx.Insert(&mb) err = tx.Insert(&mb)
@ -1796,7 +1915,7 @@ func (a *Account) MailboxEnsure(tx *bstore.Tx, name string, subscribe bool, spec
} }
flags = []string{`\Subscribed`} 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. // 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 := fn(&xmb)
*p = false *p = false
xmb.ModSeq = *modseq
if err := tx.Update(&xmb); err != nil { if err := tx.Update(&xmb); err != nil {
qerr = fmt.Errorf("clearing special-use flag: %v", err) qerr = fmt.Errorf("clearing special-use flag: %v", err)
} else { } 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 }) 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.SpecialUse = specialUse
mb.ModSeq = *modseq
if err := tx.Update(&mb); err != nil { if err := tx.Update(&mb); err != nil {
return Mailbox{}, nil, fmt.Errorf("setting special-use flag for new mailbox: %v", err) 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 return mb, changes, nil
} }
@ -2015,12 +2136,17 @@ func (a *Account) DeliverMailbox(log mlog.Log, mailbox string, m *Message, msgFi
return ErrOverQuota 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 { if err != nil {
return fmt.Errorf("ensuring mailbox: %w", err) return fmt.Errorf("ensuring mailbox: %w", err)
} }
m.MailboxID = mb.ID m.MailboxID = mb.ID
m.MailboxOrigID = 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 // Update count early, DeliverMessage will update mb too and we don't want to fetch
// it again before updating. // it again before updating.
@ -2136,6 +2262,7 @@ func (a *Account) rejectsRemoveMessages(ctx context.Context, log mlog.Log, tx *b
if err != nil { if err != nil {
return nil, fmt.Errorf("assign next modseq: %w", err) return nil, fmt.Errorf("assign next modseq: %w", err)
} }
mb.ModSeq = modseq
// Expunge the messages. // Expunge the messages.
qx := bstore.QueryTx[Message](tx) 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) { func (a *Account) MailboxCreate(tx *bstore.Tx, name string, specialUse SpecialUse) (changes []Change, created []string, exists bool, rerr error) {
elems := strings.Split(name, "/") elems := strings.Split(name, "/")
var p string var p string
var modseq ModSeq
for i, elem := range elems { for i, elem := range elems {
if i > 0 { if i > 0 {
p += "/" p += "/"
@ -2670,7 +2798,7 @@ func (a *Account) MailboxCreate(tx *bstore.Tx, name string, specialUse SpecialUs
} }
continue continue
} }
_, nchanges, err := a.MailboxEnsure(tx, p, true, specialUse) _, nchanges, err := a.MailboxEnsure(tx, p, true, specialUse, &modseq)
if err != nil { if err != nil {
return nil, nil, false, fmt.Errorf("ensuring mailbox exists: %v", err) 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. // destination, and any children of mbsrc and the destination.
// //
// Names must be normalized and cannot be Inbox. // 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" { if mbsrc.Name == "Inbox" || dst == "Inbox" {
return nil, true, false, false, fmt.Errorf("inbox cannot be renamed") 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 { if err != nil {
return nil, false, false, false, fmt.Errorf("next uid validity: %v", err) 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. // Ensure parent mailboxes for the destination paths exist.
var parent string var parent string
@ -2730,12 +2864,15 @@ func (a *Account) MailboxRename(tx *bstore.Tx, mbsrc Mailbox, dst string) (chang
if ok { if ok {
continue continue
} }
omb := mb omb := mb
mb = Mailbox{ mb = Mailbox{
ID: omb.ID, ID: omb.ID,
Name: parent, Name: parent,
UIDValidity: uidval, UIDValidity: uidval,
UIDNext: 1, UIDNext: 1,
ModSeq: *modseq,
CreateSeq: *modseq,
HaveCounts: true, HaveCounts: true,
} }
if err := tx.Insert(&mb); err != nil { 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) 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. // 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.Name = dstName
srcmb.UIDValidity = uidval srcmb.UIDValidity = uidval
srcmb.ModSeq = *modseq
if err := tx.Update(&srcmb); err != nil { if err := tx.Update(&srcmb); err != nil {
return nil, false, false, false, fmt.Errorf("renaming mailbox: %v", err) 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 { if tx.Get(&Subscription{Name: dstName}) == nil {
dstFlags = []string{`\Subscribed`} 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. // 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, UIDValidity: uidval,
UIDNext: 1, UIDNext: 1,
Name: xsrc, Name: xsrc,
ModSeq: *modseq,
CreateSeq: *modseq,
HaveCounts: true, HaveCounts: true,
} }
if err := tx.Insert(&mb); err != nil { 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. // 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 := bstore.QueryTx[Message](tx)
qm.FilterNonzero(Message{MailboxID: mailbox.ID}) qm.FilterNonzero(Message{MailboxID: mailbox.ID})
remove, err := qm.List() 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 { if err := tx.Delete(&Mailbox{ID: mailbox.ID}); err != nil {
return nil, nil, false, fmt.Errorf("removing mailbox: %v", err) 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. // CheckMailboxName checks if name is valid, returning an INBOX-normalized name.

View File

@ -72,7 +72,6 @@ func TestMailbox(t *testing.T) {
m.ThreadMuted = true m.ThreadMuted = true
m.ThreadCollapsed = true m.ThreadCollapsed = true
var mbsent Mailbox var mbsent Mailbox
mbrejects := Mailbox{Name: "Rejects", UIDValidity: 1, UIDNext: 1, HaveCounts: true}
mreject := m mreject := m
mconsumed := Message{ mconsumed := Message{
Received: m.Received, Received: m.Received,
@ -102,6 +101,9 @@ func TestMailbox(t *testing.T) {
err = tx.Update(&mbsent) err = tx.Update(&mbsent)
tcheck(t, err, "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) err = tx.Insert(&mbrejects)
tcheck(t, err, "insert rejects mailbox") tcheck(t, err, "insert rejects mailbox")
mreject.MailboxID = mbrejects.ID mreject.MailboxID = mbrejects.ID
@ -166,20 +168,21 @@ func TestMailbox(t *testing.T) {
t.Fatalf("same key for different address") t.Fatalf("same key for different address")
} }
var modseq ModSeq
acc.WithWLock(func() { acc.WithWLock(func() {
err := acc.DB.Write(ctxbg, func(tx *bstore.Tx) error { 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 return err
}) })
tcheck(t, err, "ensure mailbox exists") tcheck(t, err, "ensure mailbox exists")
err = acc.DB.Read(ctxbg, func(tx *bstore.Tx) error { 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 return err
}) })
tcheck(t, err, "ensure mailbox exists") tcheck(t, err, "ensure mailbox exists")
err = acc.DB.Write(ctxbg, func(tx *bstore.Tx) error { 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") tcheck(t, err, "create mailbox")
exists, err := acc.MailboxExists(tx, "Testbox2") exists, err := acc.MailboxExists(tx, "Testbox2")

View File

@ -61,12 +61,14 @@ type ChangeThread struct {
type ChangeRemoveMailbox struct { type ChangeRemoveMailbox struct {
MailboxID int64 MailboxID int64
Name string Name string
ModSeq ModSeq
} }
// ChangeAddMailbox is sent for a newly created mailbox. // ChangeAddMailbox is sent for a newly created mailbox.
type ChangeAddMailbox struct { type ChangeAddMailbox struct {
Mailbox Mailbox Mailbox Mailbox
Flags []string // For flags like \Subscribed. Flags []string // For flags like \Subscribed.
ModSeq ModSeq
} }
// ChangeRenameMailbox is sent for a rename mailbox. // ChangeRenameMailbox is sent for a rename mailbox.
@ -75,6 +77,7 @@ type ChangeRenameMailbox struct {
OldName string OldName string
NewName string NewName string
Flags []string Flags []string
ModSeq ModSeq
} }
// ChangeAddSubscription is sent for an added subscription to a mailbox. // ChangeAddSubscription is sent for an added subscription to a mailbox.
@ -95,6 +98,7 @@ type ChangeMailboxSpecialUse struct {
MailboxID int64 MailboxID int64
MailboxName string MailboxName string
SpecialUse SpecialUse SpecialUse SpecialUse
ModSeq ModSeq
} }
// ChangeMailboxKeywords is sent when keywords are changed for a mailbox. For // 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. MailboxID int64 // Can be zero, meaning global (per-account) annotation.
MailboxName string // Empty for global (per-account) annotation. MailboxName string // Empty for global (per-account) annotation.
Key string // Also called "entry name", e.g. "/private/comment". Key string // Also called "entry name", e.g. "/private/comment".
ModSeq ModSeq
} }
var switchboardBusy atomic.Bool var switchboardBusy atomic.Bool

View File

@ -121,7 +121,7 @@ func assignParent(m *Message, pm Message, updateSeen bool) {
// are made in transactions of batchSize changes. The total number of updated // are made in transactions of batchSize changes. The total number of updated
// messages is returned. // 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. // to propagate the changes to IMAP clients.
func (a *Account) ResetThreading(ctx context.Context, log mlog.Log, batchSize int, clearIDs bool) (int, error) { 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? // 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 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)) log = log.With(slog.String("account", acc.Name))
if up.Threads == 0 { 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) return fmt.Errorf("resetting message threading fields: %v", err)
} }
up.Threads = 1 // Must refresh up, it may have been modified by another upgrade progress.
if err := acc.DB.Update(ctx, up); err != nil { err = acc.DB.Write(ctx, func(tx *bstore.Tx) error {
up.Threads = 0 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) 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)) 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 { 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) return fmt.Errorf("upgrading to threads storage, step 2/2: %w", err)
} }
up.Threads = 2
if err := acc.DB.Update(ctx, up); err != nil { // Must refresh up, it may have been modified by another upgrade progress.
up.Threads = 1 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) 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))) log.Info("upgrading account for threading, step 2/2: completed", slog.Duration("duration", time.Since(t0)))
} }

View File

@ -463,10 +463,19 @@ func importMessages(ctx context.Context, log mlog.Log, token string, acc *store.
if err == bstore.ErrAbsent { if err == bstore.ErrAbsent {
uidvalidity, err := acc.NextUIDValidity(tx) uidvalidity, err := acc.NextUIDValidity(tx)
ximportcheckf(err, "finding next uid validity") 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{ mb = store.Mailbox{
Name: p, Name: p,
UIDValidity: uidvalidity, UIDValidity: uidvalidity,
UIDNext: 1, UIDNext: 1,
ModSeq: modseq,
CreateSeq: modseq,
HaveCounts: true, HaveCounts: true,
// Do not assign special-use flags. This existing account probably already has such mailboxes. // 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}) err := tx.Insert(&store.Subscription{Name: p})
ximportcheckf(err, "subscribing to imported mailbox") 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 { } else if err != nil {
ximportcheckf(err, "creating mailbox %s (aborting)", p) ximportcheckf(err, "creating mailbox %s (aborting)", p)
} }

View File

@ -424,8 +424,7 @@ func (w Webmail) MessageCompose(ctx context.Context, m ComposeMessage, mailboxID
var modseq store.ModSeq // Only set if needed. var modseq store.ModSeq // Only set if needed.
if m.DraftMessageID > 0 { if m.DraftMessageID > 0 {
var nchanges []store.Change nchanges := xops.MessageDeleteTx(ctx, log, tx, acc, []int64{m.DraftMessageID}, &modseq)
modseq, nchanges = xops.MessageDeleteTx(ctx, log, tx, acc, []int64{m.DraftMessageID}, modseq)
changes = append(changes, nchanges...) changes = append(changes, nchanges...)
// On-disk file is removed after lock. // 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) { xdbwrite(ctx, acc, func(tx *bstore.Tx) {
if m.DraftMessageID > 0 { if m.DraftMessageID > 0 {
var nchanges []store.Change nchanges := xops.MessageDeleteTx(ctx, log, tx, acc, []int64{m.DraftMessageID}, &modseq)
modseq, nchanges = xops.MessageDeleteTx(ctx, log, tx, acc, []int64{m.DraftMessageID}, modseq)
changes = append(changes, nchanges...) changes = append(changes, nchanges...)
// On-disk file is removed after lock. // On-disk file is removed after lock.
} }
@ -1048,13 +1046,23 @@ func (w Webmail) MessageSubmit(ctx context.Context, m SubmitMessage) {
rm.Notjunk = true rm.Notjunk = true
} }
if rm.Flags != oflags { if rm.Flags != oflags {
modseq, err = acc.NextModSeq(tx) if modseq == 0 {
xcheckf(ctx, err, "next modseq") modseq, err = acc.NextModSeq(tx)
xcheckf(ctx, err, "next modseq")
}
rm.ModSeq = modseq rm.ModSeq = modseq
err := tx.Update(&rm) err := tx.Update(&rm)
xcheckf(ctx, err, "updating flags of replied/forwarded message") xcheckf(ctx, err, "updating flags of replied/forwarded message")
changes = append(changes, rm.ChangeFlags(oflags)) 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) err = acc.RetrainMessages(ctx, log, tx, []store.Message{rm}, false)
xcheckf(ctx, err, "retraining messages after reply/forward") 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) err = q.IDs(&msgIDs)
xcheckf(ctx, err, "listing messages in thread to archive") xcheckf(ctx, err, "listing messages in thread to archive")
if len(msgIDs) > 0 { if len(msgIDs) > 0 {
var nchanges []store.Change nchanges := xops.MessageMoveTx(ctx, log, acc, tx, msgIDs, mbArchive, &modseq)
modseq, nchanges = xops.MessageMoveTx(ctx, log, acc, tx, msgIDs, mbArchive, modseq)
changes = append(changes, nchanges...) 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") xcheckf(ctx, errors.New("no messages in mailbox"), "emptying mailbox")
} }
mb.ModSeq = modseq
// Remove Recipients. // Remove Recipients.
anyIDs := make([]any, len(expunged)) anyIDs := make([]any, len(expunged))
for i, m := range 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) mbsrc := xmailboxID(ctx, tx, mailboxID)
var err error var err error
var isInbox, notExists, alreadyExists bool 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 { if isInbox || notExists || alreadyExists {
xcheckuserf(ctx, err, "renaming mailbox") 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) { xdbwrite(ctx, acc, func(tx *bstore.Tx) {
xmb := xmailboxID(ctx, tx, mb.ID) 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 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. // we set, we clear it for the mailbox(es) that had it, if any.
clearPrevious := func(clear bool, specialUse string) { 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.FilterNotEqual("ID", mb.ID)
q.FilterEqual(specialUse, true) q.FilterEqual(specialUse, true)
q.Gather(&ombl) 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") xcheckf(ctx, err, "updating previous special-use mailboxes")
for _, omb := range ombl { for _, omb := range ombl {
@ -1510,7 +1523,8 @@ func (Webmail) MailboxSetSpecialUse(ctx context.Context, mb store.Mailbox) {
clearPrevious(mb.Trash, "Trash") clearPrevious(mb.Trash, "Trash")
xmb.SpecialUse = mb.SpecialUse xmb.SpecialUse = mb.SpecialUse
err := tx.Update(&xmb) xmb.ModSeq = modseq
err = tx.Update(&xmb)
xcheckf(ctx, err, "updating special-use flags for mailbox") xcheckf(ctx, err, "updating special-use flags for mailbox")
changes = append(changes, xmb.ChangeSpecialUse()) changes = append(changes, xmb.ChangeSpecialUse())
}) })

View File

@ -1665,6 +1665,20 @@
"string" "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", "Name": "HaveCounts",
"Docs": "Whether MailboxCounts have been initialized.", "Docs": "Whether MailboxCounts have been initialized.",
@ -2974,6 +2988,13 @@
"Typewords": [ "Typewords": [
"string" "string"
] ]
},
{
"Name": "ModSeq",
"Docs": "",
"Typewords": [
"ModSeq"
]
} }
] ]
}, },
@ -3022,6 +3043,13 @@
"[]", "[]",
"string" "string"
] ]
},
{
"Name": "ModSeq",
"Docs": "",
"Typewords": [
"ModSeq"
]
} }
] ]
}, },
@ -3104,6 +3132,13 @@
"Typewords": [ "Typewords": [
"SpecialUse" "SpecialUse"
] ]
},
{
"Name": "ModSeq",
"Docs": "",
"Typewords": [
"ModSeq"
]
} }
] ]
}, },

View File

@ -197,6 +197,8 @@ export interface Mailbox {
Sent: boolean Sent: boolean
Trash: 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. 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. HaveCounts: boolean // Whether MailboxCounts have been initialized.
Total: number // Total number of messages, excluding \Deleted. For JMAP. 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. 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 { export interface ChangeMailboxRemove {
MailboxID: number MailboxID: number
Name: string Name: string
ModSeq: ModSeq
} }
// ChangeMailboxAdd indicates a new mailbox was added, initially without any messages. // ChangeMailboxAdd indicates a new mailbox was added, initially without any messages.
@ -474,6 +477,7 @@ export interface ChangeMailboxRename {
OldName: string OldName: string
NewName: string NewName: string
Flags?: string[] | null Flags?: string[] | null
ModSeq: ModSeq
} }
// ChangeMailboxCounts set new total and unseen message counts for a mailbox. // ChangeMailboxCounts set new total and unseen message counts for a mailbox.
@ -492,6 +496,7 @@ export interface ChangeMailboxSpecialUse {
MailboxID: number MailboxID: number
MailboxName: string MailboxName: string
SpecialUse: SpecialUse SpecialUse: SpecialUse
ModSeq: ModSeq
} }
// SpecialUse identifies a specific role for a mailbox, used by clients to // 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"]}]}, "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"]}]}, "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"]}]}, "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"]}]}, "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"]}]}, "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"]}]}, "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"]}]}, "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"]}]}, "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"]}]}, "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"]}]}, "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"]}]}, "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"]}]}, "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"]}]}, "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}, "UID": {"Name":"UID","Docs":"","Values":null},

View File

@ -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"] }] }, "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"] }] }, "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"] }] }, "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"] }] }, "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"] }] }, "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"] }] }, "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"] }] }, "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"] }] }, "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"] }] }, "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"] }] }, "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"] }] }, "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"] }] }, "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"] }] }, "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 }, "UID": { "Name": "UID", "Docs": "", "Values": null },

View File

@ -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"] }] }, "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"] }] }, "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"] }] }, "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"] }] }, "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"] }] }, "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"] }] }, "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"] }] }, "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"] }] }, "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"] }] }, "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"] }] }, "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"] }] }, "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"] }] }, "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"] }] }, "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 }, "UID": { "Name": "UID", "Docs": "", "Values": null },

View File

@ -385,7 +385,7 @@ func TestView(t *testing.T) {
var chmbrename ChangeMailboxRename var chmbrename ChangeMailboxRename
getChanges(&chmbrename) getChanges(&chmbrename)
tcompare(t, chmbrename, ChangeMailboxRename{ 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 // ChangeMailboxSpecialUse
@ -393,10 +393,10 @@ func TestView(t *testing.T) {
var chmbspecialuseOld, chmbspecialuseNew ChangeMailboxSpecialUse var chmbspecialuseOld, chmbspecialuseNew ChangeMailboxSpecialUse
getChanges(&chmbspecialuseOld, &chmbspecialuseNew) getChanges(&chmbspecialuseOld, &chmbspecialuseNew)
tcompare(t, chmbspecialuseOld, ChangeMailboxSpecialUse{ 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{ 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 // ChangeMailboxRemove
@ -404,7 +404,7 @@ func TestView(t *testing.T) {
var chmbremove ChangeMailboxRemove var chmbremove ChangeMailboxRemove
getChanges(&chmbremove) getChanges(&chmbremove)
tcompare(t, chmbremove, ChangeMailboxRemove{ 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 // ChangeMsgAdd

View File

@ -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"] }] }, "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"] }] }, "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"] }] }, "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"] }] }, "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"] }] }, "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"] }] }, "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"] }] }, "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"] }] }, "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"] }] }, "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"] }] }, "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"] }] }, "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"] }] }, "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"] }] }, "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 }, "UID": { "Name": "UID", "Docs": "", "Values": null },

View File

@ -61,7 +61,8 @@ func (x XOps) MessageDelete(ctx context.Context, log mlog.Log, acc *store.Accoun
var changes []store.Change var changes []store.Change
x.DBWrite(ctx, acc, func(tx *bstore.Tx) { 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) 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{} removeChanges := map[int64]store.ChangeRemoveUIDs{}
changes := make([]store.Change, 0, len(messageIDs)+1) // n remove, 1 mailbox counts 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) m := x.messageID(ctx, tx, mid)
totalSize += m.Size 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 m.MailboxID != mb.ID {
if mb.ID != 0 { if mb.ID != 0 {
mb.ModSeq = *modseq
err := tx.Update(&mb) err := tx.Update(&mb)
x.Checkf(ctx, err, "updating mailbox counts") x.Checkf(ctx, err, "updating mailbox counts")
changes = append(changes, mb.ChangeCounts()) 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()) mb.Sub(m.MailboxCounts())
if modseq == 0 {
modseq, err = acc.NextModSeq(tx)
x.Checkf(ctx, err, "assigning next modseq")
}
m.Expunged = true m.Expunged = true
m.ModSeq = modseq m.ModSeq = *modseq
err = tx.Update(&m) err = tx.Update(&m)
x.Checkf(ctx, err, "marking message as expunged") x.Checkf(ctx, err, "marking message as expunged")
ch := removeChanges[m.MailboxID] ch := removeChanges[m.MailboxID]
ch.UIDs = append(ch.UIDs, m.UID) ch.UIDs = append(ch.UIDs, m.UID)
ch.MailboxID = m.MailboxID ch.MailboxID = m.MailboxID
ch.ModSeq = modseq ch.ModSeq = *modseq
removeChanges[m.MailboxID] = ch removeChanges[m.MailboxID] = ch
remove = append(remove, m) remove = append(remove, m)
} }
if mb.ID != 0 { if mb.ID != 0 {
mb.ModSeq = *modseq
err := tx.Update(&mb) err := tx.Update(&mb)
x.Checkf(ctx, err, "updating count in mailbox") x.Checkf(ctx, err, "updating count in mailbox")
changes = append(changes, mb.ChangeCounts()) 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) 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) { 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 { for _, mid := range messageIDs {
m := x.messageID(ctx, tx, mid) 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 != m.MailboxID {
if mb.ID != 0 { if mb.ID != 0 {
mb.ModSeq = modseq
err := tx.Update(&mb) err := tx.Update(&mb)
x.Checkf(ctx, err, "updating mailbox") x.Checkf(ctx, err, "updating mailbox")
if mb.MailboxCounts != origmb.MailboxCounts { if mb.MailboxCounts != origmb.MailboxCounts {
@ -189,10 +200,6 @@ func (x XOps) MessageFlagsAdd(ctx context.Context, log mlog.Log, acc *store.Acco
continue continue
} }
if modseq == 0 {
modseq, err = acc.NextModSeq(tx)
x.Checkf(ctx, err, "assigning next modseq")
}
m.ModSeq = modseq m.ModSeq = modseq
err = tx.Update(&m) err = tx.Update(&m)
x.Checkf(ctx, err, "updating message") 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 { if mb.ID != 0 {
mb.ModSeq = modseq
err := tx.Update(&mb) err := tx.Update(&mb)
x.Checkf(ctx, err, "updating mailbox") x.Checkf(ctx, err, "updating mailbox")
if mb.MailboxCounts != origmb.MailboxCounts { 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 { for _, mid := range messageIDs {
m := x.messageID(ctx, tx, mid) 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 != m.MailboxID {
if mb.ID != 0 { if mb.ID != 0 {
mb.ModSeq = modseq
err := tx.Update(&mb) err := tx.Update(&mb)
x.Checkf(ctx, err, "updating counts for mailbox") x.Checkf(ctx, err, "updating counts for mailbox")
if mb.MailboxCounts != origmb.MailboxCounts { if mb.MailboxCounts != origmb.MailboxCounts {
@ -259,10 +273,6 @@ func (x XOps) MessageFlagsClear(ctx context.Context, log mlog.Log, acc *store.Ac
continue continue
} }
if modseq == 0 {
modseq, err = acc.NextModSeq(tx)
x.Checkf(ctx, err, "assigning next modseq")
}
m.ModSeq = modseq m.ModSeq = modseq
err = tx.Update(&m) err = tx.Update(&m)
x.Checkf(ctx, err, "updating message") 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 { if mb.ID != 0 {
mb.ModSeq = modseq
err := tx.Update(&mb) err := tx.Update(&mb)
x.Checkf(ctx, err, "updating keywords in mailbox") x.Checkf(ctx, err, "updating keywords in mailbox")
if mb.MailboxCounts != origmb.MailboxCounts { 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") x.Checkf(ctx, err, "listing messages to mark as read")
if have { if have {
mb.ModSeq = modseq
err := tx.Update(&mb) err := tx.Update(&mb)
x.Checkf(ctx, err, "updating mailbox") x.Checkf(ctx, err, "updating mailbox")
changes = append(changes, mb.ChangeCounts()) changes = append(changes, mb.ChangeCounts())
@ -366,14 +378,15 @@ func (x XOps) MessageMove(ctx context.Context, log mlog.Log, acc *store.Account,
return 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) 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)) retrain := make([]store.Message, 0, len(messageIDs))
removeChanges := map[int64]store.ChangeRemoveUIDs{} removeChanges := map[int64]store.ChangeRemoveUIDs{}
// n adds, 1 remove, 2 mailboxcounts, optimistic and at least for a single message. // 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{}{} keywords := map[string]struct{}{}
now := time.Now() 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 { for _, mid := range messageIDs {
m := x.messageID(ctx, tx, mid) m := x.messageID(ctx, tx, mid)
// We may have loaded this mailbox in the previous iteration of this loop. // We may have loaded this mailbox in the previous iteration of this loop.
if m.MailboxID != mbSrc.ID { if m.MailboxID != mbSrc.ID {
if mbSrc.ID != 0 { if mbSrc.ID != 0 {
mbSrc.ModSeq = *modseq
err := tx.Update(&mbSrc) err := tx.Update(&mbSrc)
x.Checkf(ctx, err, "updating source mailbox counts") x.Checkf(ctx, err, "updating source mailbox counts")
changes = append(changes, mbSrc.ChangeCounts()) 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") 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 := removeChanges[m.MailboxID]
ch.UIDs = append(ch.UIDs, m.UID) ch.UIDs = append(ch.UIDs, m.UID)
ch.ModSeq = modseq ch.ModSeq = *modseq
ch.MailboxID = m.MailboxID ch.MailboxID = m.MailboxID
removeChanges[m.MailboxID] = ch removeChanges[m.MailboxID] = ch
@ -418,7 +432,7 @@ func (x XOps) MessageMoveTx(ctx context.Context, log mlog.Log, acc *store.Accoun
om := m om := m
om.PrepareExpunge() om.PrepareExpunge()
om.ID = 0 // Assign new ID. om.ID = 0 // Assign new ID.
om.ModSeq = modseq om.ModSeq = *modseq
mbSrc.Sub(m.MailboxCounts()) 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.Seen = false
} }
m.UID = mbDst.UIDNext m.UID = mbDst.UIDNext
m.ModSeq = modseq m.ModSeq = *modseq
mbDst.UIDNext++ mbDst.UIDNext++
m.JunkFlagsForMailbox(mbDst, conf) m.JunkFlagsForMailbox(mbDst, conf)
m.SaveDate = &now m.SaveDate = &now
@ -456,8 +470,9 @@ func (x XOps) MessageMoveTx(ctx context.Context, log mlog.Log, acc *store.Accoun
} }
} }
err := tx.Update(&mbSrc) mbSrc.ModSeq = *modseq
x.Checkf(ctx, err, "updating source mailbox counts") err = tx.Update(&mbSrc)
x.Checkf(ctx, err, "updating source mailbox counts and modseq")
changes = append(changes, mbSrc.ChangeCounts(), mbDst.ChangeCounts()) 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()) changes = append(changes, mbDst.ChangeKeywords())
} }
mbDst.ModSeq = *modseq
err = tx.Update(&mbDst) 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) err = acc.RetrainMessages(ctx, log, tx, retrain, false)
x.Checkf(ctx, err, "retraining messages after move") 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) changes = append(changes, ch)
} }
return modseq, changes return changes
} }
func isText(p message.Part) bool { func isText(p message.Part) bool {