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