1package imapserver
2
3import (
4 "fmt"
5 "strings"
6 "testing"
7
8 "github.com/mjl-/bstore"
9
10 "github.com/mjl-/mox/imapclient"
11 "github.com/mjl-/mox/mox-"
12 "github.com/mjl-/mox/store"
13 "slices"
14)
15
16func TestCondstore(t *testing.T) {
17 testCondstoreQresync(t, false)
18}
19
20func TestQresync(t *testing.T) {
21 testCondstoreQresync(t, true)
22}
23
24func testCondstoreQresync(t *testing.T, qresync bool) {
25 defer mockUIDValidity()()
26 tc := start(t)
27 defer tc.close()
28
29 // todo: check whether marking \seen will cause modseq to be returned in case of qresync.
30
31 // Check basic requirements of CONDSTORE.
32
33 capability := "Condstore"
34 if qresync {
35 capability = "Qresync"
36 }
37
38 tc.client.Login("mjl@mox.example", password0)
39 tc.client.Enable(capability)
40 tc.transactf("ok", "Select inbox")
41 tc.xuntaggedOpt(false, imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(2), More: "x"}})
42
43 // First some tests without any messages.
44
45 tc.transactf("ok", "Status inbox (Highestmodseq)")
46 tc.xuntagged(imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusHighestModSeq: 2}})
47
48 // No messages, no matches.
49 tc.transactf("ok", "Uid Fetch 1:* (Flags) (Changedsince 12345)")
50 tc.xuntagged()
51
52 // Also no messages with modseq 1, which we internally turn into modseq 0.
53 tc.transactf("ok", "Uid Fetch 1:* (Flags) (Changedsince 1)")
54 tc.xuntagged()
55
56 // Also try with modseq attribute.
57 tc.transactf("ok", "Uid Fetch 1:* (Flags Modseq) (Changedsince 1)")
58 tc.xuntagged()
59
60 // Search with modseq search criteria.
61 tc.transactf("ok", "Search Modseq 0") // Zero is valid, matches all.
62 tc.xsearch()
63
64 tc.transactf("ok", "Search Modseq 1") // Converted to zero internally.
65 tc.xsearch()
66
67 tc.transactf("ok", "Search Modseq 12345")
68 tc.xsearch()
69
70 tc.transactf("ok", `Search Modseq "/Flags/\\Draft" All 12345`)
71 tc.xsearch()
72
73 tc.transactf("ok", `Search Or Modseq 12345 Modseq 54321`)
74 tc.xsearch()
75
76 // esearch
77 tc.transactf("ok", "Search Return (All) Modseq 123")
78 tc.xesearch(imapclient.UntaggedEsearch{})
79
80 // Now we add, delete, expunge, modify some message flags and check if the
81 // responses are correct. We check in both a condstore-enabled and one without that
82 // we get the correct notifications.
83
84 // First we add 3 messages as if they were added before we implemented CONDSTORE.
85 // Later on, we'll update the second, and delete the third, leaving the first
86 // unmodified. Those messages have modseq 0 in the database. We use append for
87 // convenience, then adjust the records in the database.
88 // We have a workaround below to prevent triggering the consistency checker.
89 tc.account.SetSkipMessageModSeqZeroCheck(true)
90 defer tc.account.SetSkipMessageModSeqZeroCheck(false)
91 tc.transactf("ok", "Append inbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
92 tc.transactf("ok", "Append inbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
93 tc.transactf("ok", "Append inbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
94 _, err := bstore.QueryDB[store.Message](ctxbg, tc.account.DB).UpdateFields(map[string]any{
95 "ModSeq": 0,
96 "CreateSeq": 0,
97 })
98 tcheck(t, err, "clearing modseq from messages")
99 err = tc.account.DB.Update(ctxbg, &store.SyncState{ID: 1, LastModSeq: 1})
100 tcheck(t, err, "resetting modseq state")
101
102 tc.client.Create("otherbox", nil)
103
104 // tc2 is a client without condstore, so no modseq responses.
105 tc2 := startNoSwitchboard(t)
106 defer tc2.closeNoWait()
107 tc2.client.Login("mjl@mox.example", password0)
108 tc2.client.Select("inbox")
109
110 // tc3 is a client with condstore, so with modseq responses.
111 tc3 := startNoSwitchboard(t)
112 defer tc3.closeNoWait()
113 tc3.client.Login("mjl@mox.example", password0)
114 tc3.client.Enable(capability)
115 tc3.client.Select("inbox")
116
117 var clientModseq int64 = 2 // We track the client-side modseq for inbox. Not a store.ModSeq.
118
119 // Add messages to: inbox, otherbox, inbox, inbox.
120 // We have these messages in order of modseq: 2+1 in inbox, 1 in otherbox, 2 in inbox.
121 // The original two in inbox appear to have modseq 1 (with 0 stored in the database).
122 // Creation of otherbox got modseq 2.
123 // The ones we insert below will start with modseq 3. So we'll have messages with modseq 1 and 3-6.
124 tc.transactf("ok", "Append inbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
125 tc.xuntagged(imapclient.UntaggedExists(4))
126 tc.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("4")})
127
128 tc.transactf("ok", "Append otherbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
129 tc.xuntagged()
130 tc.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 3, UIDs: xparseUIDRange("1")})
131
132 tc.transactf("ok", "Append inbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
133 tc.xuntagged(imapclient.UntaggedExists(5))
134 tc.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("5")})
135
136 tc.transactf("ok", "Append inbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
137 tc.xuntagged(imapclient.UntaggedExists(6))
138 tc.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("6")})
139
140 tc2.transactf("ok", "Noop")
141 noflags := imapclient.FetchFlags(nil)
142 tc2.xuntagged(
143 imapclient.UntaggedExists(6),
144 imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(4), noflags}},
145 imapclient.UntaggedFetch{Seq: 5, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags}},
146 imapclient.UntaggedFetch{Seq: 6, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags}},
147 )
148
149 tc3.transactf("ok", "Noop")
150 tc3.xuntagged(
151 imapclient.UntaggedExists(6),
152 imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(4), noflags, imapclient.FetchModSeq(clientModseq + 1)}},
153 imapclient.UntaggedFetch{Seq: 5, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(clientModseq + 3)}},
154 imapclient.UntaggedFetch{Seq: 6, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq + 4)}},
155 )
156
157 mox.SetPedantic(true)
158 tc.transactf("bad", `Fetch 1 Flags (Changedsince 0)`) // 0 not allowed in syntax.
159 mox.SetPedantic(false)
160 tc.transactf("ok", "Uid fetch 1 (Flags) (Changedsince 0)")
161 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(1)}})
162
163 // Check highestmodseq for mailboxes.
164 tc.transactf("ok", "Status inbox (highestmodseq)")
165 tc.xuntagged(imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusHighestModSeq: clientModseq + 4}})
166
167 tc.transactf("ok", "Status otherbox (highestmodseq)")
168 tc.xuntagged(imapclient.UntaggedStatus{Mailbox: "otherbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusHighestModSeq: clientModseq + 2}})
169
170 // Check highestmodseq when we select.
171 tc.transactf("ok", "Examine otherbox")
172 tc.xuntaggedOpt(false, imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(clientModseq + 2), More: "x"}})
173
174 tc.transactf("ok", "Select inbox")
175 tc.xuntaggedOpt(false, imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(clientModseq + 4), More: "x"}})
176
177 clientModseq += 4
178
179 // Check fetch modseq response and changedsince.
180 tc.transactf("ok", `Fetch 1 (Modseq)`)
181 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchModSeq(1)}})
182
183 // Without modseq attribute, even with condseq enabled, there is no modseq response.
184 // For QRESYNC, we must always send MODSEQ for UID FETCH commands, but not for FETCH commands. ../rfc/7162:1427
185 tc.transactf("ok", `Uid Fetch 1 Flags`)
186 if qresync {
187 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(1)}})
188 } else {
189 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags}})
190 }
191 tc.transactf("ok", `Fetch 1 Flags`)
192 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags}})
193
194 // When CHANGEDSINCE is present, MODSEQ is automatically added to the response.
195 // ../rfc/7162:871
196 // ../rfc/7162:877
197 tc.transactf("ok", `Fetch 1 Flags (Changedsince 1)`)
198 tc.xuntagged()
199 tc.transactf("ok", `Fetch 1,4 Flags (Changedsince 1)`)
200 tc.xuntagged(imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(4), noflags, imapclient.FetchModSeq(3)}})
201 tc.transactf("ok", `Fetch 2 Flags (Changedsince 2)`)
202 tc.xuntagged()
203
204 // store and uid store.
205
206 // unchangedsince 0 never passes the check. ../rfc/7162:640
207 tc.transactf("ok", `Store 1 (Unchangedsince 0) +Flags ()`)
208 tc.xcodeArg(imapclient.CodeModified(xparseNumSet("1")))
209 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(1)}})
210
211 // Modseq is 2 for first condstore-aware-appended message, so also no match.
212 tc.transactf("ok", `Uid Store 4 (Unchangedsince 1) +Flags ()`)
213 tc.xcodeArg(imapclient.CodeModified(xparseNumSet("4")))
214
215 // Modseq is 1 for original message.
216 tc.transactf("ok", `Store 1 (Unchangedsince 1) +Flags (label1)`)
217 tc.xcode("") // No MODIFIED.
218 clientModseq++
219 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchFlags{"label1"}, imapclient.FetchModSeq(clientModseq)}})
220 tc2.transactf("ok", "Noop")
221 tc2.xuntagged(
222 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchFlags{"label1"}}},
223 )
224 tc3.transactf("ok", "Noop")
225 tc3.xuntagged(
226 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchFlags{"label1"}, imapclient.FetchModSeq(clientModseq)}},
227 )
228
229 // Modify same message twice. Check that second application doesn't fail due to
230 // modseq change made in the first application. ../rfc/7162:823
231 tc.transactf("ok", `Uid Store 1,1 (Unchangedsince %d) -Flags (label1)`, clientModseq)
232 clientModseq++
233 tc.xcode("") // No MODIFIED.
234 tc.xuntagged(
235 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchFlags(nil), imapclient.FetchModSeq(clientModseq)}},
236 )
237 // We do broadcast the changes twice. Not great, but doesn't hurt. This isn't common.
238 tc2.transactf("ok", "Noop")
239 tc2.xuntagged(
240 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchFlags(nil)}},
241 )
242 tc3.transactf("ok", "Noop")
243 tc3.xuntagged(
244 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchFlags(nil), imapclient.FetchModSeq(clientModseq)}},
245 )
246
247 // Modify without actually changing flags, there will be no new modseq and no broadcast.
248 tc.transactf("ok", `Store 1 (Unchangedsince %d) -Flags (label1)`, clientModseq)
249 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchFlags(nil), imapclient.FetchModSeq(clientModseq)}})
250 tc.xcode("") // No MODIFIED.
251 tc2.transactf("ok", "Noop")
252 tc2.xuntagged()
253 tc3.transactf("ok", "Noop")
254 tc3.xuntagged()
255
256 // search with modseq criteria and modseq in response
257 tc.transactf("ok", "Search Modseq %d", clientModseq)
258 tc.xsearchmodseq(clientModseq, 1)
259
260 tc.transactf("ok", "Uid Search Or Modseq %d Modseq %d", clientModseq, clientModseq)
261 tc.xsearchmodseq(clientModseq, 1)
262
263 // esearch
264 tc.transactf("ok", "Search Return (Min Max All) 1:* Modseq %d", clientModseq)
265 tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 1, All: esearchall0("1"), ModSeq: clientModseq})
266
267 tc.transactf("ok", "Search Return (Count) 1:* Modseq 0")
268 tc.xesearch(imapclient.UntaggedEsearch{Count: uint32ptr(6), ModSeq: clientModseq})
269
270 tc.transactf("ok", "Search Return (Min Max) 1:* Modseq 0")
271 tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 6, ModSeq: clientModseq})
272
273 tc.transactf("ok", "Search Return (Min) 1:* Modseq 0")
274 tc.xesearch(imapclient.UntaggedEsearch{Min: 1, ModSeq: clientModseq})
275
276 // expunge, we expunge the third and fourth messages. The third was originally with
277 // modseq 0, the fourth was added with condstore-aware append.
278 tc.transactf("ok", `Store 3:4 +Flags (\Deleted)`)
279 clientModseq++
280 tc2.transactf("ok", "Noop")
281 tc3.transactf("ok", "Noop")
282 tc.transactf("ok", "Expunge")
283 clientModseq++
284 if qresync {
285 tc.xuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("3:4")})
286 } else {
287 tc.xuntagged(imapclient.UntaggedExpunge(3), imapclient.UntaggedExpunge(3))
288 }
289 tc.xcodeArg(imapclient.CodeHighestModSeq(clientModseq))
290 tc2.transactf("ok", "Noop")
291 tc2.xuntagged(imapclient.UntaggedExpunge(3), imapclient.UntaggedExpunge(3))
292 tc3.transactf("ok", "Noop")
293 if qresync {
294 tc3.xuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("3:4")})
295 } else {
296 tc3.xuntagged(imapclient.UntaggedExpunge(3), imapclient.UntaggedExpunge(3))
297 }
298
299 // Again after expunge: status, select, conditional store/fetch/search
300 tc.transactf("ok", "Status inbox (Highestmodseq Messages Unseen Deleted)")
301 tc.xuntagged(imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 4, imapclient.StatusUnseen: 4, imapclient.StatusDeleted: 0, imapclient.StatusHighestModSeq: clientModseq}})
302
303 tc.transactf("ok", "Close")
304 tc.transactf("ok", "Select inbox")
305 tc.xuntaggedOpt(false,
306 imapclient.UntaggedExists(4),
307 imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(clientModseq), More: "x"}},
308 )
309
310 tc.transactf("ok", `Fetch 1:* (Modseq)`)
311 tc.xuntagged(
312 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchModSeq(8)}},
313 imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2), imapclient.FetchModSeq(1)}},
314 imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), imapclient.FetchModSeq(5)}},
315 imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), imapclient.FetchModSeq(6)}},
316 )
317 // Expunged messages, with higher modseq, should not show up.
318 tc.transactf("ok", "Uid Fetch 1:* (flags) (Changedsince 8)")
319 tc.xuntagged()
320
321 // search
322 tc.transactf("ok", "Search Modseq 8")
323 tc.xsearchmodseq(8, 1)
324 tc.transactf("ok", "Search Modseq 9")
325 tc.xsearch()
326
327 // esearch
328 tc.transactf("ok", "Search Return (Min Max All) 1:* Modseq 8")
329 tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 1, All: esearchall0("1"), ModSeq: 8})
330 tc.transactf("ok", "Search Return (Min Max All) 1:* Modseq 9")
331 tc.xuntagged(imapclient.UntaggedEsearch{Tag: tc.client.LastTag})
332
333 // store, cannot modify expunged messages.
334 tc.transactf("ok", `Uid Store 3,4 (Unchangedsince %d) +Flags (label2)`, clientModseq)
335 tc.xuntagged()
336 tc.xcode("") // Not MODIFIED.
337 tc.transactf("ok", `Uid Store 3,4 +Flags (label2)`)
338 tc.xuntagged()
339 tc.xcode("") // Not MODIFIED.
340
341 // Check all condstore-enabling commands (and their syntax), ../rfc/7162:368
342
343 // We start a new connection, do the thing that should enable condstore, then
344 // change flags of a message in another connection, do a noop in the new connection
345 // which should result in an untagged fetch that includes modseq, the indicator
346 // that condstore was indeed enabled. It's a bit complicated, but i don't think
347 // there is a clearly specified mechanism to find out which capabilities are
348 // enabled at any point.
349 var tagcount int
350 checkCondstoreEnabled := func(fn func(xtc *testconn)) {
351 t.Helper()
352
353 xtc := startNoSwitchboard(t)
354 // We have modified modseq & createseq to 0 above for testing that case. Don't
355 // trigger the consistency checker.
356 defer xtc.closeNoWait()
357 xtc.client.Login("mjl@mox.example", password0)
358 fn(xtc)
359 tagcount++
360 label := fmt.Sprintf("l%d", tagcount)
361 tc.transactf("ok", "Store 4 Flags (%s)", label)
362 clientModseq++
363 xtc.transactf("ok", "Noop")
364 xtc.xuntagged(imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), imapclient.FetchFlags{label}, imapclient.FetchModSeq(clientModseq)}})
365 }
366 // SELECT/EXAMINE with CONDSTORE parameter, ../rfc/7162:373
367 checkCondstoreEnabled(func(xtc *testconn) {
368 t.Helper()
369 xtc.transactf("ok", "Select inbox (Condstore)")
370 })
371 // STATUS with HIGHESTMODSEQ attribute, ../rfc/7162:375
372 checkCondstoreEnabled(func(xtc *testconn) {
373 t.Helper()
374 xtc.transactf("ok", "Status otherbox (Highestmodseq)")
375 xtc.transactf("ok", "Select inbox")
376 })
377 // FETCH with MODSEQ ../rfc/7162:377
378 checkCondstoreEnabled(func(xtc *testconn) {
379 t.Helper()
380 xtc.transactf("ok", "Select inbox")
381 xtc.transactf("ok", "Fetch 4 (Modseq)")
382 })
383 // SEARCH with MODSEQ ../rfc/7162:377
384 checkCondstoreEnabled(func(xtc *testconn) {
385 t.Helper()
386 xtc.transactf("ok", "Select inbox")
387 xtc.transactf("ok", "Search 4 Modseq 1")
388 })
389 // FETCH with CHANGEDSINCE ../rfc/7162:380
390 checkCondstoreEnabled(func(xtc *testconn) {
391 t.Helper()
392 xtc.transactf("ok", "Select inbox")
393 xtc.transactf("ok", "Fetch 4 (Flags) (Changedsince %d)", clientModseq)
394 })
395 // STORE with UNCHANGEDSINCE ../rfc/7162:382
396 checkCondstoreEnabled(func(xtc *testconn) {
397 t.Helper()
398 xtc.transactf("ok", "Select inbox")
399 xtc.transactf("ok", "Store 4 (Unchangedsince 0) Flags ()")
400 })
401 // ENABLE CONDSTORE ../rfc/7162:384
402 checkCondstoreEnabled(func(xtc *testconn) {
403 t.Helper()
404 xtc.transactf("ok", "Enable Condstore")
405 xtc.transactf("ok", "Select inbox")
406 })
407 // ENABLE QRESYNC ../rfc/7162:1390
408 checkCondstoreEnabled(func(xtc *testconn) {
409 t.Helper()
410 xtc.transactf("ok", "Enable Qresync")
411 xtc.transactf("ok", "Select inbox")
412 })
413 tc.transactf("ok", "Store 4 Flags ()")
414 clientModseq++
415
416 if qresync {
417 testQresync(t, tc, clientModseq)
418 }
419
420 // Continue with some tests that further change the data.
421 // First we copy messages to a new mailbox, and check we get new modseq for those
422 // messages.
423 tc.transactf("ok", "Select otherbox")
424 tc2.transactf("ok", "Noop")
425 tc3.transactf("ok", "Noop")
426 tc.transactf("ok", "Copy 1 inbox")
427 clientModseq++
428 tc2.transactf("ok", "Noop")
429 tc3.transactf("ok", "Noop")
430 tc2.xuntagged(
431 imapclient.UntaggedExists(5),
432 imapclient.UntaggedFetch{Seq: 5, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(7), noflags}},
433 )
434 tc3.xuntagged(
435 imapclient.UntaggedExists(5),
436 imapclient.UntaggedFetch{Seq: 5, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(7), noflags, imapclient.FetchModSeq(clientModseq)}},
437 )
438
439 // Then we move some messages, and check if we get expunged/vanished in original
440 // and untagged fetch with modseq in destination mailbox.
441 // tc2o is a client without condstore, so no modseq responses.
442 tc2o := startNoSwitchboard(t)
443 defer tc2o.closeNoWait()
444 tc2o.client.Login("mjl@mox.example", password0)
445 tc2o.client.Select("otherbox")
446
447 // tc3o is a client with condstore, so with modseq responses.
448 tc3o := startNoSwitchboard(t)
449 defer tc3o.closeNoWait()
450 tc3o.client.Login("mjl@mox.example", password0)
451 tc3o.client.Enable(capability)
452 tc3o.client.Select("otherbox")
453
454 tc.transactf("ok", "Select inbox")
455 tc.transactf("ok", "Uid Move 2:4 otherbox") // Only UID 2, because UID 3 and 4 have already been expunged.
456 clientModseq++
457 if qresync {
458 tc.xuntaggedOpt(false, imapclient.UntaggedVanished{UIDs: xparseNumSet("2")})
459 tc.xcodeArg(imapclient.CodeHighestModSeq(clientModseq))
460 } else {
461 tc.xuntaggedOpt(false, imapclient.UntaggedExpunge(2))
462 tc.xcode("")
463 }
464 tc2.transactf("ok", "Noop")
465 tc2.xuntagged(imapclient.UntaggedExpunge(2))
466 tc3.transactf("ok", "Noop")
467 if qresync {
468 tc3.xuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("2")})
469 } else {
470 tc3.xuntagged(imapclient.UntaggedExpunge(2))
471 }
472 tc2o.transactf("ok", "Noop")
473 tc2o.xuntagged(
474 imapclient.UntaggedExists(2),
475 imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2), noflags}},
476 )
477 tc3o.transactf("ok", "Noop")
478 tc3o.xuntagged(
479 imapclient.UntaggedExists(2),
480 imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2), noflags, imapclient.FetchModSeq(clientModseq)}},
481 )
482
483 tc2o.closeNoWait()
484 tc2o = nil
485 tc3o.closeNoWait()
486 tc3o = nil
487
488 // Then we rename inbox, which is special because it moves messages away instead of
489 // actually moving the mailbox. The mailbox stays and is cleared, so we check if we
490 // get expunged/vanished messages.
491 tc.transactf("ok", "Rename inbox oldbox")
492 // todo spec: server doesn't respond with untagged responses, find rfc reference that says this is ok.
493 tc2.transactf("ok", "Noop")
494 tc2.xuntagged(
495 imapclient.UntaggedList{Separator: '/', Mailbox: "oldbox"},
496 imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1),
497 )
498 tc3.transactf("ok", "Noop")
499 if qresync {
500 tc3.xuntagged(
501 imapclient.UntaggedList{Separator: '/', Mailbox: "oldbox"},
502 imapclient.UntaggedVanished{UIDs: xparseNumSet("1,5:7")},
503 )
504 } else {
505 tc3.xuntagged(
506 imapclient.UntaggedList{Separator: '/', Mailbox: "oldbox"},
507 imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1),
508 )
509 }
510
511 // Then we delete otherbox (we cannot delete inbox). We don't keep any history for removed mailboxes, so not actually a special case.
512 tc.transactf("ok", "Delete otherbox")
513}
514
515func testQresync(t *testing.T, tc *testconn, clientModseq int64) {
516 // Vanished on non-uid fetch is not allowed. ../rfc/7162:1693
517 tc.transactf("bad", "fetch 1:* (Flags) (Changedsince 1 Vanished)")
518
519 // Vanished without changedsince is not allowed. ../rfc/7162:1701
520 tc.transactf("bad", "Uid Fetch 1:* (Flags) (Vanished)")
521
522 // Vanished not allowed without first enabling qresync. ../rfc/7162:1697
523 xtc := startNoSwitchboard(t)
524 xtc.client.Login("mjl@mox.example", password0)
525 xtc.transactf("ok", "Select inbox (Condstore)")
526 xtc.transactf("bad", "Uid Fetch 1:* (Flags) (Changedsince 1 Vanished)")
527 xtc.closeNoWait()
528 xtc = nil
529
530 // Check that we get proper vanished responses.
531 tc.transactf("ok", "Uid Fetch 1:* (Flags) (Changedsince 1 Vanished)")
532 noflags := imapclient.FetchFlags(nil)
533 tc.xuntagged(
534 imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
535 imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(5)}},
536 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(8)}},
537 imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}},
538 )
539
540 // select/examine with qresync parameters, including the various optional fields.
541 tc.transactf("ok", "Close")
542
543 // Must enable qresync explicitly before using. ../rfc/7162:1446
544 xtc = startNoSwitchboard(t)
545 xtc.client.Login("mjl@mox.example", password0)
546 xtc.transactf("bad", "Select inbox (Qresync 1 0)")
547 // Prevent triggering the consistency checker, we still have modseq/createseq at 0.
548 xtc.closeNoWait()
549 xtc = nil
550
551 tc.transactf("bad", "Select inbox (Qresync (0 1))") // Both args must be > 0.
552 tc.transactf("bad", "Select inbox (Qresync (1 0))") // Both args must be > 0.
553 tc.transactf("bad", "Select inbox (Qresync)") // Two args are minimum.
554 tc.transactf("bad", "Select inbox (Qresync (1))") // Two args are minimum.
555 tc.transactf("bad", "Select inbox (Qresync (1 1 1:*))") // Known UIDs, * not allowed.
556 tc.transactf("bad", "Select inbox (Qresync (1 1 1:6 (1:* 1:6)))") // Known seqset cannot have *.
557 tc.transactf("bad", "Select inbox (Qresync (1 1 1:6 (1:6 1:*)))") // Known uidset cannot have *.
558 tc.transactf("bad", "Select inbox (Qresync (1 1) qresync (1 1))") // Duplicate qresync.
559
560 flags := strings.Split(`\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent l1 l2 l3 l4 l5 l6 l7 l8 label1`, " ")
561 permflags := strings.Split(`\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent \*`, " ")
562 uflags := imapclient.UntaggedFlags(flags)
563 upermflags := imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "PERMANENTFLAGS", CodeArg: imapclient.CodeList{Code: "PERMANENTFLAGS", Args: permflags}, More: "x"}}
564
565 baseUntagged := []imapclient.Untagged{
566 uflags,
567 upermflags,
568 imapclient.UntaggedList{Separator: '/', Mailbox: "Inbox"},
569 imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UIDNEXT", CodeArg: imapclient.CodeUint{Code: "UIDNEXT", Num: 7}, More: "x"}},
570 imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UIDVALIDITY", CodeArg: imapclient.CodeUint{Code: "UIDVALIDITY", Num: 1}, More: "x"}},
571 imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UNSEEN", CodeArg: imapclient.CodeUint{Code: "UNSEEN", Num: 1}, More: "x"}},
572 imapclient.UntaggedRecent(0),
573 imapclient.UntaggedExists(4),
574 imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(clientModseq), More: "x"}},
575 }
576
577 makeUntagged := func(l ...imapclient.Untagged) []imapclient.Untagged {
578 return slices.Concat(baseUntagged, l)
579 }
580
581 // uidvalidity 1, highest known modseq 1, sends full current state.
582 tc.transactf("ok", "Select inbox (Qresync (1 1))")
583 tc.xuntagged(
584 makeUntagged(
585 imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
586 imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(5)}},
587 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(8)}},
588 imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}},
589 )...,
590 )
591
592 // Uidvalidity mismatch, server will not send any changes, so it's just a regular open.
593 tc.transactf("ok", "Close")
594 tc.transactf("ok", "Select inbox (Qresync (2 1))")
595 tc.xuntagged(baseUntagged...)
596
597 // We can tell which UIDs we know. First, send broader range then exist, should work.
598 tc.transactf("ok", "Close")
599 tc.transactf("ok", "Select inbox (Qresync (1 1 1:7))")
600 tc.xuntagged(
601 makeUntagged(
602 imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
603 imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(5)}},
604 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(8)}},
605 imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}},
606 )...,
607 )
608
609 // Now send just the ones that exist. We won't get the vanished messages.
610 tc.transactf("ok", "Close")
611 tc.transactf("ok", "Select inbox (Qresync (1 1 1,2,5:6))")
612 tc.xuntagged(
613 makeUntagged(
614 imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(5)}},
615 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(8)}},
616 imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}},
617 )...,
618 )
619
620 // We'll only get updates for UIDs we specify.
621 tc.transactf("ok", "Close")
622 tc.transactf("ok", "Select inbox (Qresync (1 1 5))")
623 tc.xuntagged(
624 makeUntagged(
625 imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(5)}},
626 )...,
627 )
628
629 // We'll only get updates for UIDs we specify. ../rfc/7162:1523
630 tc.transactf("ok", "Close")
631 tc.transactf("ok", "Select inbox (Qresync (1 1 3))")
632 tc.xuntagged(
633 makeUntagged(
634 imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3")},
635 )...,
636 )
637
638 // If we specify the latest modseq, we'll get no changes.
639 tc.transactf("ok", "Close")
640 tc.transactf("ok", "Select inbox (Qresync (1 %d))", clientModseq)
641 tc.xuntagged(baseUntagged...)
642
643 // We can provide our own seqs & uids, and have server determine which uids we
644 // know. But the seqs & uids must be of equal length. First try with a few combinations
645 // that aren't valid. ../rfc/7162:1579
646 tc.transactf("ok", "Close")
647 tc.transactf("bad", "Select inbox (Qresync (1 1 1:6 (1 1,2)))") // Not same length.
648 tc.transactf("bad", "Select inbox (Qresync (1 1 1:6 (1,2 1)))") // Not same length.
649 tc.transactf("no", "Select inbox (Qresync (1 1 1:6 (1,2 1,1)))") // Not ascending.
650 tc.transactf("bad", "Select inbox (Qresync (1 1 1:6 (1:* 1:4)))") // Star not allowed.
651
652 // With valid parameters, based on what a client would know at this stage.
653 tc.transactf("ok", "Select inbox (Qresync (1 1 1:6 (1,3,6 1,3,6)))")
654 tc.xuntagged(
655 makeUntagged(
656 imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
657 imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(5)}},
658 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(8)}},
659 imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}},
660 )...,
661 )
662
663 // The 3rd parameter is optional, try without.
664 tc.transactf("ok", "Close")
665 tc.transactf("ok", "Select inbox (Qresync (1 5 (1,3,6 1,3,6)))")
666 tc.xuntagged(
667 makeUntagged(
668 imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
669 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(8)}},
670 imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}},
671 )...,
672 )
673
674 tc.transactf("ok", "Close")
675 tc.transactf("ok", "Select inbox (Qresync (1 9 (1,3,6 1,3,6)))")
676 tc.xuntagged(
677 makeUntagged(
678 imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
679 imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}},
680 )...,
681 )
682
683 // Client will claim a highestmodseq but then include uids that have been removed
684 // since that time. Server detects this, sends full vanished history and continues
685 // working with modseq changed to 1 before the expunged uid.
686 tc.transactf("ok", "Close")
687 tc.transactf("ok", "Select inbox (Qresync (1 10 (1,3,6 1,3,6)))")
688 tc.xuntagged(
689 makeUntagged(
690 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."}},
691 imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
692 imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}},
693 )...,
694 )
695
696 // Client will claim a highestmodseq but then include uids that have been removed
697 // since that time. Server detects this, sends full vanished history and continues
698 // working with modseq changed to 1 before the expunged uid.
699 tc.transactf("ok", "Close")
700 tc.transactf("ok", "Select inbox (Qresync (1 18 (1,3,6 1,3,6)))")
701 tc.xuntagged(
702 makeUntagged(
703 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."}},
704 imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
705 imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}},
706 )...,
707 )
708}
709