package imapserver import ( "fmt" "strings" "testing" "github.com/mjl-/bstore" "github.com/mjl-/mox/imapclient" "github.com/mjl-/mox/mox-" "github.com/mjl-/mox/store" ) func TestCondstore(t *testing.T) { testCondstoreQresync(t, false) } func TestQresync(t *testing.T) { testCondstoreQresync(t, true) } func testCondstoreQresync(t *testing.T, qresync bool) { defer mockUIDValidity()() tc := start(t) defer tc.close() // todo: check whether marking \seen will cause modseq to be returned in case of qresync. // Check basic requirements of CONDSTORE. capability := "Condstore" if qresync { capability = "Qresync" } 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"}}) // First some tests without any messages. tc.transactf("ok", "Status inbox (Highestmodseq)") tc.xuntagged(imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[string]int64{"HIGHESTMODSEQ": 1}}) // No messages, no matches. tc.transactf("ok", "Uid Fetch 1:* (Flags) (Changedsince 12345)") tc.xuntagged() // Also no messages with modseq 1, which we internally turn into modseq 0. tc.transactf("ok", "Uid Fetch 1:* (Flags) (Changedsince 1)") tc.xuntagged() // Also try with modseq attribute. tc.transactf("ok", "Uid Fetch 1:* (Flags Modseq) (Changedsince 1)") tc.xuntagged() // Search with modseq search criteria. tc.transactf("ok", "Search Modseq 0") // Zero is valid, matches all. tc.xsearch() tc.transactf("ok", "Search Modseq 1") // Converted to zero internally. tc.xsearch() tc.transactf("ok", "Search Modseq 12345") tc.xsearch() tc.transactf("ok", `Search Modseq "/Flags/\\Draft" All 12345`) tc.xsearch() tc.transactf("ok", `Search Or Modseq 12345 Modseq 54321`) tc.xsearch() // esearch tc.transactf("ok", "Search Return (All) Modseq 123") tc.xesearch(imapclient.UntaggedEsearch{}) // Now we add, delete, expunge, modify some message flags and check if the // responses are correct. We check in both a condstore-enabled and one without that // we get the correct notifications. // First we add 3 messages as if they were added before we implemented CONDSTORE. // Later on, we'll update the second, and delete the third, leaving the first // unmodified. Those messages have modseq 0 in the database. We use append for // convenience, then adjust the records in the database. // We have a workaround below to prevent triggering the consistency checker. tc.transactf("ok", "Append inbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx") tc.transactf("ok", "Append inbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx") tc.transactf("ok", "Append inbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx") _, err := bstore.QueryDB[store.Message](ctxbg, tc.account.DB).UpdateFields(map[string]any{ "ModSeq": 0, "CreateSeq": 0, }) tcheck(t, err, "clearing modseq from messages") err = tc.account.DB.Update(ctxbg, &store.SyncState{ID: 1, LastModSeq: 1}) tcheck(t, err, "resetting modseq state") tc.client.Create("otherbox") // tc2 is a client without condstore, so no modseq responses. tc2 := startNoSwitchboard(t) defer tc2.close() tc2.client.Login("mjl@mox.example", password0) tc2.client.Select("inbox") // tc3 is a client with condstore, so with modseq responses. tc3 := startNoSwitchboard(t) defer tc3.close() tc3.client.Login("mjl@mox.example", password0) tc3.client.Enable(capability) tc3.client.Select("inbox") var clientModseq int64 = 1 // 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. 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}) tc.transactf("ok", "Append otherbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx") tc.xuntagged() tc.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 2, UID: 1}) tc.transactf("ok", "Append inbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx") tc.xuntagged(imapclient.UntaggedExists(5)) tc.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UID: 5}) tc.transactf("ok", "Append inbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx") tc.xuntagged(imapclient.UntaggedExists(6)) tc.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UID: 6}) tc2.transactf("ok", "Noop") noflags := imapclient.FetchFlags(nil) tc2.xuntagged( imapclient.UntaggedExists(6), imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(4), noflags}}, imapclient.UntaggedFetch{Seq: 5, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags}}, imapclient.UntaggedFetch{Seq: 6, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags}}, ) tc3.transactf("ok", "Noop") tc3.xuntagged( imapclient.UntaggedExists(6), imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(4), noflags, imapclient.FetchModSeq(clientModseq + 1)}}, imapclient.UntaggedFetch{Seq: 5, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(clientModseq + 3)}}, imapclient.UntaggedFetch{Seq: 6, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq + 4)}}, ) mox.SetPedantic(true) 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. // Check highestmodseq for mailboxes. tc.transactf("ok", "Status inbox (highestmodseq)") tc.xuntagged(imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[string]int64{"HIGHESTMODSEQ": clientModseq}}) tc.transactf("ok", "Status otherbox (highestmodseq)") tc.xuntagged(imapclient.UntaggedStatus{Mailbox: "otherbox", Attrs: map[string]int64{"HIGHESTMODSEQ": 3}}) // 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.transactf("ok", "Select inbox") tc.xuntaggedOpt(false, imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(clientModseq), More: "x"}}) // Check fetch modseq response and changedsince. tc.transactf("ok", `Fetch 1 (Modseq)`) tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchModSeq(1)}}) // Without modseq attribute, even with condseq enabled, there is no modseq response. // For QRESYNC, we must always send MODSEQ for UID FETCH commands, but not for FETCH commands. ../rfc/7162:1427 tc.transactf("ok", `Uid Fetch 1 Flags`) if qresync { tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(1)}}) } else { tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags}}) } tc.transactf("ok", `Fetch 1 Flags`) tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags}}) // When CHANGEDSINCE is present, MODSEQ is automatically added to the response. // ../rfc/7162:871 // ../rfc/7162:877 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.transactf("ok", `Fetch 2 Flags (Changedsince 2)`) tc.xuntagged() // store and uid store. // unchangedsince 0 never passes the check. ../rfc/7162:640 tc.transactf("ok", `Store 1 (Unchangedsince 0) +Flags ()`) tc.xcodeArg(imapclient.CodeModified(xparseNumSet("1"))) tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(1)}}) // Modseq is 2 for first condstore-aware-appended message, so also no match. tc.transactf("ok", `Uid Store 4 (Unchangedsince 1) +Flags ()`) tc.xcodeArg(imapclient.CodeModified(xparseNumSet("4"))) // Modseq is 1 for original message. tc.transactf("ok", `Store 1 (Unchangedsince 1) +Flags (label1)`) tc.xcode("") // No MODIFIED. clientModseq++ tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchFlags{"label1"}, imapclient.FetchModSeq(clientModseq)}}) tc2.transactf("ok", "Noop") tc2.xuntagged( imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchFlags{"label1"}}}, ) tc3.transactf("ok", "Noop") tc3.xuntagged( imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchFlags{"label1"}, imapclient.FetchModSeq(clientModseq)}}, ) // Modify same message twice. Check that second application doesn't fail due to // modseq change made in the first application. ../rfc/7162:823 tc.transactf("ok", `Uid Store 1,1 (Unchangedsince %d) -Flags (label1)`, clientModseq) clientModseq++ tc.xcode("") // No MODIFIED. tc.xuntagged( imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchFlags(nil), imapclient.FetchModSeq(clientModseq)}}, ) // We do broadcast the changes twice. Not great, but doesn't hurt. This isn't common. tc2.transactf("ok", "Noop") tc2.xuntagged( imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchFlags(nil)}}, ) tc3.transactf("ok", "Noop") tc3.xuntagged( imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchFlags(nil), imapclient.FetchModSeq(clientModseq)}}, ) // Modify without actually changing flags, there will be no new modseq and no broadcast. tc.transactf("ok", `Store 1 (Unchangedsince %d) -Flags (label1)`, clientModseq) tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchFlags(nil), imapclient.FetchModSeq(clientModseq)}}) tc.xcode("") // No MODIFIED. tc2.transactf("ok", "Noop") tc2.xuntagged() tc3.transactf("ok", "Noop") tc3.xuntagged() // search with modseq criteria and modseq in response tc.transactf("ok", "Search Modseq %d", clientModseq) tc.xsearchmodseq(clientModseq, 1) tc.transactf("ok", "Uid Search Or Modseq %d Modseq %d", clientModseq, clientModseq) tc.xsearchmodseq(clientModseq, 1) // esearch tc.transactf("ok", "Search Return (Min Max All) 1:* Modseq %d", clientModseq) tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 1, All: esearchall0("1"), ModSeq: clientModseq}) uint32ptr := func(v uint32) *uint32 { return &v } tc.transactf("ok", "Search Return (Count) 1:* Modseq 0") tc.xesearch(imapclient.UntaggedEsearch{Count: uint32ptr(6), ModSeq: clientModseq}) tc.transactf("ok", "Search Return (Min Max) 1:* Modseq 0") tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 6, ModSeq: clientModseq}) tc.transactf("ok", "Search Return (Min) 1:* Modseq 0") tc.xesearch(imapclient.UntaggedEsearch{Min: 1, ModSeq: clientModseq}) // expunge, we expunge the third and fourth messages. The third was originally with // modseq 0, the fourth was added with condstore-aware append. tc.transactf("ok", `Store 3:4 +Flags (\Deleted)`) clientModseq++ tc2.transactf("ok", "Noop") tc3.transactf("ok", "Noop") tc.transactf("ok", "Expunge") clientModseq++ if qresync { tc.xuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("3:4")}) } else { tc.xuntagged(imapclient.UntaggedExpunge(3), imapclient.UntaggedExpunge(3)) } tc.xcodeArg(imapclient.CodeHighestModSeq(clientModseq)) tc2.transactf("ok", "Noop") tc2.xuntagged(imapclient.UntaggedExpunge(3), imapclient.UntaggedExpunge(3)) tc3.transactf("ok", "Noop") if qresync { tc3.xuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("3:4")}) } else { tc3.xuntagged(imapclient.UntaggedExpunge(3), imapclient.UntaggedExpunge(3)) } // Again after expunge: status, select, conditional store/fetch/search tc.transactf("ok", "Status inbox (Highestmodseq Messages Unseen Deleted)") tc.xuntagged(imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[string]int64{"MESSAGES": 4, "UNSEEN": 4, "DELETED": 0, "HIGHESTMODSEQ": clientModseq}}) tc.transactf("ok", "Close") tc.transactf("ok", "Select inbox") tc.xuntaggedOpt(false, imapclient.UntaggedExists(4), imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(clientModseq), More: "x"}}, ) tc.transactf("ok", `Fetch 1:* (Modseq)`) tc.xuntagged( imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchModSeq(7)}}, 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)}}, ) // Expunged messages, with higher modseq, should not show up. tc.transactf("ok", "Uid Fetch 1:* (flags) (Changedsince 7)") tc.xuntagged() // search tc.transactf("ok", "Search Modseq 7") tc.xsearchmodseq(7, 1) tc.transactf("ok", "Search Modseq 8") 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.xuntagged(imapclient.UntaggedEsearch{Correlator: tc.client.LastTag}) // store, cannot modify expunged messages. tc.transactf("ok", `Uid Store 3,4 (Unchangedsince %d) +Flags (label2)`, clientModseq) tc.xuntagged() tc.xcode("") // Not MODIFIED. tc.transactf("ok", `Uid Store 3,4 +Flags (label2)`) tc.xuntagged() tc.xcode("") // Not MODIFIED. // Check all condstore-enabling commands (and their syntax), ../rfc/7162:368 // We start a new connection, do the thing that should enable condstore, then // change flags of a message in another connection, do a noop in the new connection // which should result in an untagged fetch that includes modseq, the indicator // that condstore was indeed enabled. It's a bit complicated, but i don't think // there is a clearly specified mechanism to find out which capabilities are // enabled at any point. var tagcount int checkCondstoreEnabled := func(fn func(xtc *testconn)) { t.Helper() xtc := startNoSwitchboard(t) // We have modified modseq & createseq to 0 above for testing that case. Don't // trigger the consistency checker. store.CheckConsistencyOnClose = false defer func() { xtc.close() store.CheckConsistencyOnClose = true }() xtc.client.Login("mjl@mox.example", password0) fn(xtc) tagcount++ label := fmt.Sprintf("l%d", tagcount) tc.transactf("ok", "Store 4 Flags (%s)", label) clientModseq++ xtc.transactf("ok", "Noop") xtc.xuntagged(imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), imapclient.FetchFlags{label}, imapclient.FetchModSeq(clientModseq)}}) } // SELECT/EXAMINE with CONDSTORE parameter, ../rfc/7162:373 checkCondstoreEnabled(func(xtc *testconn) { t.Helper() xtc.transactf("ok", "Select inbox (Condstore)") }) // STATUS with HIGHESTMODSEQ attribute, ../rfc/7162:375 checkCondstoreEnabled(func(xtc *testconn) { t.Helper() xtc.transactf("ok", "Status otherbox (Highestmodseq)") xtc.transactf("ok", "Select inbox") }) // FETCH with MODSEQ ../rfc/7162:377 checkCondstoreEnabled(func(xtc *testconn) { t.Helper() xtc.transactf("ok", "Select inbox") xtc.transactf("ok", "Fetch 4 (Modseq)") }) // SEARCH with MODSEQ ../rfc/7162:377 checkCondstoreEnabled(func(xtc *testconn) { t.Helper() xtc.transactf("ok", "Select inbox") xtc.transactf("ok", "Search 4 Modseq 1") }) // FETCH with CHANGEDSINCE ../rfc/7162:380 checkCondstoreEnabled(func(xtc *testconn) { t.Helper() xtc.transactf("ok", "Select inbox") xtc.transactf("ok", "Fetch 4 (Flags) (Changedsince %d)", clientModseq) }) // STORE with UNCHANGEDSINCE ../rfc/7162:382 checkCondstoreEnabled(func(xtc *testconn) { t.Helper() xtc.transactf("ok", "Select inbox") xtc.transactf("ok", "Store 4 (Unchangedsince 0) Flags ()") }) // ENABLE CONDSTORE ../rfc/7162:384 checkCondstoreEnabled(func(xtc *testconn) { t.Helper() xtc.transactf("ok", "Enable Condstore") xtc.transactf("ok", "Select inbox") }) // ENABLE QRESYNC ../rfc/7162:1390 checkCondstoreEnabled(func(xtc *testconn) { t.Helper() xtc.transactf("ok", "Enable Qresync") xtc.transactf("ok", "Select inbox") }) tc.transactf("ok", "Store 4 Flags ()") clientModseq++ if qresync { testQresync(t, tc, clientModseq) } // Continue with some tests that further change the data. // First we copy messages to a new mailbox, and check we get new modseq for those // messages. tc.transactf("ok", "Select otherbox") tc2.transactf("ok", "Noop") tc3.transactf("ok", "Noop") tc.transactf("ok", "Copy 1 inbox") clientModseq++ tc2.transactf("ok", "Noop") tc3.transactf("ok", "Noop") tc2.xuntagged( imapclient.UntaggedExists(5), imapclient.UntaggedFetch{Seq: 5, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(7), noflags}}, ) tc3.xuntagged( imapclient.UntaggedExists(5), imapclient.UntaggedFetch{Seq: 5, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(7), noflags, imapclient.FetchModSeq(clientModseq)}}, ) // Then we move some messages, and check if we get expunged/vanished in original // and untagged fetch with modseq in destination mailbox. // tc2o is a client without condstore, so no modseq responses. tc2o := startNoSwitchboard(t) defer tc2o.close() tc2o.client.Login("mjl@mox.example", password0) tc2o.client.Select("otherbox") // tc3o is a client with condstore, so with modseq responses. tc3o := startNoSwitchboard(t) defer tc3o.close() tc3o.client.Login("mjl@mox.example", password0) tc3o.client.Enable(capability) tc3o.client.Select("otherbox") tc.transactf("ok", "Select inbox") tc.transactf("ok", "Uid Move 2:4 otherbox") // Only UID 2, because UID 3 and 4 have already been expunged. clientModseq++ if qresync { tc.xuntaggedOpt(false, imapclient.UntaggedVanished{UIDs: xparseNumSet("2")}) tc.xcodeArg(imapclient.CodeHighestModSeq(clientModseq)) } else { tc.xuntaggedOpt(false, imapclient.UntaggedExpunge(2)) tc.xcode("") } tc2.transactf("ok", "Noop") tc2.xuntagged(imapclient.UntaggedExpunge(2)) tc3.transactf("ok", "Noop") if qresync { tc3.xuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("2")}) } else { tc3.xuntagged(imapclient.UntaggedExpunge(2)) } tc2o.transactf("ok", "Noop") tc2o.xuntagged( imapclient.UntaggedExists(2), imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2), noflags}}, ) tc3o.transactf("ok", "Noop") tc3o.xuntagged( imapclient.UntaggedExists(2), imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2), noflags, imapclient.FetchModSeq(clientModseq)}}, ) // Restore valid modseq/createseq for the consistency checker. _, err = bstore.QueryDB[store.Message](ctxbg, tc.account.DB).FilterEqual("CreateSeq", int64(0)).UpdateNonzero(store.Message{CreateSeq: 2}) tcheck(t, err, "updating modseq/createseq to valid values") _, err = bstore.QueryDB[store.Message](ctxbg, tc.account.DB).FilterEqual("ModSeq", int64(0)).UpdateNonzero(store.Message{ModSeq: 2}) tcheck(t, err, "updating modseq/createseq to valid values") tc2o.close() tc2o = nil tc3o.close() tc3o = nil // Then we rename inbox, which is special because it moves messages away instead of // actually moving the mailbox. The mailbox stays and is cleared, so we check if we // get expunged/vanished messages. tc.transactf("ok", "Rename inbox oldbox") // todo spec: server doesn't respond with untagged responses, find rfc reference that says this is ok. tc2.transactf("ok", "Noop") tc2.xuntagged( imapclient.UntaggedList{Separator: '/', Mailbox: "oldbox"}, imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1), ) tc3.transactf("ok", "Noop") if qresync { tc3.xuntagged( imapclient.UntaggedList{Separator: '/', Mailbox: "oldbox"}, imapclient.UntaggedVanished{UIDs: xparseNumSet("1,5:7")}, ) } else { tc3.xuntagged( imapclient.UntaggedList{Separator: '/', Mailbox: "oldbox"}, imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1), ) } // Then we delete otherbox (we cannot delete inbox). We don't keep any history for removed mailboxes, so not actually a special case. tc.transactf("ok", "Delete otherbox") } func testQresync(t *testing.T, tc *testconn, clientModseq int64) { // Vanished on non-uid fetch is not allowed. ../rfc/7162:1693 tc.transactf("bad", "fetch 1:* (Flags) (Changedsince 1 Vanished)") // Vanished without changedsince is not allowed. ../rfc/7162:1701 tc.transactf("bad", "Uid Fetch 1:* (Flags) (Vanished)") // Vanished not allowed without first enabling qresync. ../rfc/7162:1697 xtc := startNoSwitchboard(t) xtc.client.Login("mjl@mox.example", password0) xtc.transactf("ok", "Select inbox (Condstore)") xtc.transactf("bad", "Uid Fetch 1:* (Flags) (Changedsince 1 Vanished)") // Prevent triggering the consistency checker, we still have modseq/createseq at 0. store.CheckConsistencyOnClose = false xtc.close() store.CheckConsistencyOnClose = true xtc = nil // Check that we get proper vanished responses. tc.transactf("ok", "Uid Fetch 1:* (Flags) (Changedsince 1 Vanished)") 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: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}}, ) // select/examine with qresync parameters, including the various optional fields. tc.transactf("ok", "Close") // Must enable qresync explicitly before using. ../rfc/7162:1446 xtc = startNoSwitchboard(t) xtc.client.Login("mjl@mox.example", password0) xtc.transactf("bad", "Select inbox (Qresync 1 0)") // Prevent triggering the consistency checker, we still have modseq/createseq at 0. store.CheckConsistencyOnClose = false xtc.close() store.CheckConsistencyOnClose = true xtc = nil tc.transactf("bad", "Select inbox (Qresync (0 1))") // Both args must be > 0. tc.transactf("bad", "Select inbox (Qresync (1 0))") // Both args must be > 0. tc.transactf("bad", "Select inbox (Qresync)") // Two args are minimum. tc.transactf("bad", "Select inbox (Qresync (1))") // Two args are minimum. tc.transactf("bad", "Select inbox (Qresync (1 1 1:*))") // Known UIDs, * not allowed. tc.transactf("bad", "Select inbox (Qresync (1 1 1:6 (1:* 1:6)))") // Known seqset cannot have *. tc.transactf("bad", "Select inbox (Qresync (1 1 1:6 (1:6 1:*)))") // Known uidset cannot have *. tc.transactf("bad", "Select inbox (Qresync (1 1) qresync (1 1))") // Duplicate qresync. flags := strings.Split(`\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent l1 l2 l3 l4 l5 l6 l7 l8 label1`, " ") permflags := strings.Split(`\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent \*`, " ") uflags := imapclient.UntaggedFlags(flags) upermflags := imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "PERMANENTFLAGS", CodeArg: imapclient.CodeList{Code: "PERMANENTFLAGS", Args: permflags}, More: "x"}} baseUntagged := []imapclient.Untagged{ uflags, upermflags, imapclient.UntaggedList{Separator: '/', Mailbox: "Inbox"}, imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UIDNEXT", CodeArg: imapclient.CodeUint{Code: "UIDNEXT", Num: 7}, More: "x"}}, imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UIDVALIDITY", CodeArg: imapclient.CodeUint{Code: "UIDVALIDITY", Num: 1}, More: "x"}}, imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UNSEEN", CodeArg: imapclient.CodeUint{Code: "UNSEEN", Num: 1}, More: "x"}}, imapclient.UntaggedRecent(0), imapclient.UntaggedExists(4), imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(clientModseq), More: "x"}}, } makeUntagged := func(l ...imapclient.Untagged) []imapclient.Untagged { return append(append([]imapclient.Untagged{}, baseUntagged...), l...) } // uidvalidity 1, highest known modseq 1, sends full current state. tc.transactf("ok", "Select inbox (Qresync (1 1))") 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: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}}, )..., ) // Uidvalidity mismatch, server will not send any changes, so it's just a regular open. tc.transactf("ok", "Close") tc.transactf("ok", "Select inbox (Qresync (2 1))") tc.xuntagged(baseUntagged...) // We can tell which UIDs we know. First, send broader range then exist, should work. tc.transactf("ok", "Close") tc.transactf("ok", "Select inbox (Qresync (1 1 1:7))") 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: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}}, )..., ) // Now send just the ones that exist. We won't get the vanished messages. tc.transactf("ok", "Close") 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: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}}, )..., ) // We'll only get updates for UIDs we specify. tc.transactf("ok", "Close") 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)}}, )..., ) // We'll only get updates for UIDs we specify. ../rfc/7162:1523 tc.transactf("ok", "Close") tc.transactf("ok", "Select inbox (Qresync (1 1 3))") tc.xuntagged( makeUntagged( imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3")}, )..., ) // If we specify the latest modseq, we'll get no changes. tc.transactf("ok", "Close") tc.transactf("ok", "Select inbox (Qresync (1 %d))", clientModseq) tc.xuntagged(baseUntagged...) // We can provide our own seqs & uids, and have server determine which uids we // know. But the seqs & uids must be of equal length. First try with a few combinations // that aren't valid. ../rfc/7162:1579 tc.transactf("ok", "Close") tc.transactf("bad", "Select inbox (Qresync (1 1 1:6 (1 1,2)))") // Not same length. tc.transactf("bad", "Select inbox (Qresync (1 1 1:6 (1,2 1)))") // Not same length. tc.transactf("no", "Select inbox (Qresync (1 1 1:6 (1,2 1,1)))") // Not ascending. tc.transactf("bad", "Select inbox (Qresync (1 1 1:6 (1:* 1:4)))") // Star not allowed. // With valid parameters, based on what a client would know at this stage. tc.transactf("ok", "Select inbox (Qresync (1 1 1:6 (1,3,6 1,3,6)))") 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: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}}, )..., ) // The 3rd parameter is optional, try without. tc.transactf("ok", "Close") tc.transactf("ok", "Select inbox (Qresync (1 5 (1,3,6 1,3,6)))") 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: 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.xuntagged( makeUntagged( imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")}, imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}}, )..., ) // Client will claim a highestmodseq but then include uids that have been removed // 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.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."}}, imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")}, imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}}, )..., ) // Client will claim a highestmodseq but then include uids that have been removed // 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 18 (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."}}, imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")}, imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}}, )..., ) }