8 "github.com/mjl-/mox/imapclient"
9 "github.com/mjl-/mox/store"
12func TestNotify(t *testing.T) {
16func TestNotifyUIDOnly(t *testing.T) {
20func testNotify(t *testing.T, uidonly bool) {
21 defer mockUIDValidity()()
22 tc := start(t, uidonly)
24 tc.login("mjl@mox.example", password0)
25 tc.client.Select("inbox")
27 // Check for some invalid syntax.
28 tc.transactf("bad", "Notify")
29 tc.transactf("bad", "Notify bogus")
30 tc.transactf("bad", "Notify None ") // Trailing space.
31 tc.transactf("bad", "Notify Set")
32 tc.transactf("bad", "Notify Set ")
33 tc.transactf("bad", "Notify Set Status")
34 tc.transactf("bad", "Notify Set Status ()") // Empty list.
35 tc.transactf("bad", "Notify Set Status (UnknownSpecifier (messageNew))")
36 tc.transactf("bad", "Notify Set Status (Personal messageNew)") // Missing list around events.
37 tc.transactf("bad", "Notify Set Status (Personal (messageNew) )") // Trailing space.
38 tc.transactf("bad", "Notify Set Status (Personal (messageNew)) ") // Trailing space.
40 tc.transactf("bad", "Notify Set Status (Selected (mailboxName))") // MailboxName not allowed on Selected.
41 tc.transactf("bad", "Notify Set Status (Selected (messageNew))") // MessageNew must come with MessageExpunge.
42 tc.transactf("bad", "Notify Set Status (Selected (flagChange))") // flagChange must come with MessageNew and MessageExpunge.
43 tc.transactf("bad", "Notify Set Status (Selected (mailboxName)) (Selected-Delayed (mailboxName))") // Duplicate selected.
44 tc.transactf("no", "Notify Set Status (Selected (annotationChange))") // We don't implement annotation change.
45 tc.xcode(imapclient.CodeBadEvent{"MessageNew", "MessageExpunge", "FlagChange", "MailboxName", "SubscriptionChange", "MailboxMetadataChange", "ServerMetadataChange"})
46 tc.transactf("no", "Notify Set Status (Personal (unknownEvent))")
47 tc.xcode(imapclient.CodeBadEvent{"MessageNew", "MessageExpunge", "FlagChange", "MailboxName", "SubscriptionChange", "MailboxMetadataChange", "ServerMetadataChange"})
49 tc2 := startNoSwitchboard(t, uidonly)
50 defer tc2.closeNoWait()
51 tc2.login("mjl@mox.example", password0)
52 tc2.client.Select("inbox")
56 // Check that we don't get pending changes when we set "notify none". We first make
57 // changes that we drain with noop. Then add new pending changes and execute
58 // "notify none". Server should still process changes to the message sequence
59 // numbers of the selected mailbox.
60 tc2.client.Append("inbox", makeAppend(searchMsg)) // Results in exists and fetch.
62 tc2.client.Append("Junk", makeAppend(searchMsg)) // Not selected, not mentioned.
64 tc.transactf("ok", "noop")
66 imapclient.UntaggedExists(1),
67 tc.untaggedFetch(1, 1, imapclient.FetchFlags(nil)),
69 tc2.client.UIDStoreFlagsAdd("1:*", true, `\Deleted`)
73 tc.transactf("ok", "Notify None")
74 tc.xuntagged() // No untagged responses for delete/expunge.
76 // Enable notify, will first result in a the pending changes, then status.
77 tc.transactf("ok", "Notify Set Status (Selected (messageNew (Uid Modseq Bodystructure Preview) messageExpunge flagChange)) (personal (messageNew messageExpunge flagChange mailboxName subscriptionChange mailboxMetadataChange serverMetadataChange))")
79 imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeHighestModSeq(modseq), Text: "after condstore-enabling command"},
80 // note: no status for Inbox since it is selected.
81 imapclient.UntaggedStatus{Mailbox: "Drafts", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 0, imapclient.StatusUIDNext: 1, imapclient.StatusUIDValidity: 1, imapclient.StatusUnseen: 0, imapclient.StatusHighestModSeq: 2}},
82 imapclient.UntaggedStatus{Mailbox: "Sent", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 0, imapclient.StatusUIDNext: 1, imapclient.StatusUIDValidity: 1, imapclient.StatusUnseen: 0, imapclient.StatusHighestModSeq: 2}},
83 imapclient.UntaggedStatus{Mailbox: "Archive", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 0, imapclient.StatusUIDNext: 1, imapclient.StatusUIDValidity: 1, imapclient.StatusUnseen: 0, imapclient.StatusHighestModSeq: 2}},
84 imapclient.UntaggedStatus{Mailbox: "Trash", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 0, imapclient.StatusUIDNext: 1, imapclient.StatusUIDValidity: 1, imapclient.StatusUnseen: 0, imapclient.StatusHighestModSeq: 2}},
85 imapclient.UntaggedStatus{Mailbox: "Junk", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 1, imapclient.StatusUIDNext: 2, imapclient.StatusUIDValidity: 1, imapclient.StatusUnseen: 1, imapclient.StatusHighestModSeq: int64(modseq - 2)}},
88 // Selecting the mailbox again results in a refresh of the message sequence
89 // numbers, with the deleted message gone (it wasn't acknowledged yet due to
91 tc.client.Select("inbox")
93 // Add message, should result in EXISTS and FETCH with the configured attributes.
94 tc2.client.Append("inbox", makeAppend(searchMsg))
97 imapclient.UntaggedExists(1),
98 tc.untaggedFetchUID(1, 2,
99 imapclient.FetchBodystructure{
100 RespAttr: "BODYSTRUCTURE",
101 Body: imapclient.BodyTypeMpart{
103 imapclient.BodyTypeText{
105 MediaSubtype: "PLAIN",
106 BodyFields: imapclient.BodyFields{
107 Params: [][2]string{[...]string{"CHARSET", "utf-8"}},
111 Ext: &imapclient.BodyExtension1Part{
112 Disposition: ptr((*string)(nil)),
113 DispositionParams: ptr([][2]string(nil)),
114 Language: ptr([]string(nil)),
115 Location: ptr((*string)(nil)),
118 imapclient.BodyTypeText{
120 MediaSubtype: "HTML",
121 BodyFields: imapclient.BodyFields{
122 Params: [][2]string{[...]string{"CHARSET", "utf-8"}},
126 Ext: &imapclient.BodyExtension1Part{
127 Disposition: ptr((*string)(nil)),
128 DispositionParams: ptr([][2]string(nil)),
129 Language: ptr([]string(nil)),
130 Location: ptr((*string)(nil)),
134 MediaSubtype: "ALTERNATIVE",
135 Ext: &imapclient.BodyExtensionMpart{
136 Params: [][2]string{{"BOUNDARY", "x"}},
137 Disposition: ptr((*string)(nil)), // Present but nil.
138 DispositionParams: ptr([][2]string(nil)),
139 Language: ptr([]string(nil)),
140 Location: ptr((*string)(nil)),
144 imapclient.FetchPreview{Preview: ptr("this is plain text.")},
145 imapclient.FetchModSeq(modseq),
150 tc2.client.UIDStoreFlagsAdd("1:*", true, `\Deleted`)
152 tc.readuntagged(tc.untaggedFetch(1, 2, imapclient.FetchFlags{`\Deleted`}, imapclient.FetchModSeq(modseq)))
158 tc.readuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("2")})
160 tc.readuntagged(imapclient.UntaggedExpunge(1))
163 // MailboxMetadataChange for mailbox annotation.
164 tc2.transactf("ok", `setmetadata Archive (/private/comment "test")`)
167 imapclient.UntaggedMetadataKeys{Mailbox: "Archive", Keys: []string{"/private/comment"}},
170 // MailboxMetadataChange also for the selected Inbox.
171 tc2.transactf("ok", `setmetadata Inbox (/private/comment "test")`)
174 imapclient.UntaggedMetadataKeys{Mailbox: "Inbox", Keys: []string{"/private/comment"}},
177 // ServerMetadataChange for server annotation.
178 tc2.transactf("ok", `setmetadata "" (/private/vendor/other/x "test")`)
181 imapclient.UntaggedMetadataKeys{Mailbox: "", Keys: []string{"/private/vendor/other/x"}},
184 // SubscriptionChange for new subscription.
185 tc2.client.Subscribe("doesnotexist")
187 imapclient.UntaggedList{Mailbox: "doesnotexist", Separator: '/', Flags: []string{`\Subscribed`, `\NonExistent`}},
190 // SubscriptionChange for removed subscription.
191 tc2.client.Unsubscribe("doesnotexist")
193 imapclient.UntaggedList{Mailbox: "doesnotexist", Separator: '/', Flags: []string{`\NonExistent`}},
196 // SubscriptionChange for selected mailbox.
197 tc2.client.Unsubscribe("Inbox")
198 tc2.client.Subscribe("Inbox")
200 imapclient.UntaggedList{Mailbox: "Inbox", Separator: '/'},
201 imapclient.UntaggedList{Mailbox: "Inbox", Separator: '/', Flags: []string{`\Subscribed`}},
204 // MailboxName for creating mailbox.
205 tc2.client.Create("newbox", nil)
208 imapclient.UntaggedList{Mailbox: "newbox", Separator: '/', Flags: []string{`\Subscribed`}},
211 // MailboxName for renaming mailbox.
212 tc2.client.Rename("newbox", "oldbox")
215 imapclient.UntaggedList{Mailbox: "oldbox", Separator: '/', OldName: "newbox"},
218 // MailboxName for deleting mailbox.
219 tc2.client.Delete("oldbox")
222 imapclient.UntaggedList{Mailbox: "oldbox", Separator: '/', Flags: []string{`\NonExistent`}},
225 // Add message again to check for modseq. First set notify again with fewer fetch
226 // attributes for simpler checking.
227 tc.transactf("ok", "Notify Set (personal (messageNew messageExpunge flagChange mailboxName subscriptionChange mailboxMetadataChange serverMetadataChange)) (Selected (messageNew (Uid Modseq) messageExpunge flagChange))")
228 tc2.client.Append("inbox", makeAppend(searchMsg))
231 imapclient.UntaggedExists(1),
232 tc.untaggedFetchUID(1, 3, imapclient.FetchModSeq(modseq)),
235 // Next round of events must be ignored. We shouldn't get anything until we add a
236 // message to "testbox".
237 tc.transactf("ok", "Notify Set (Selected None) (mailboxes testbox (messageNew messageExpunge)) (personal None)")
238 tc2.client.Append("inbox", makeAppend(searchMsg)) // MessageNew
240 tc2.client.UIDStoreFlagsAdd("1:*", true, `\Deleted`) // FlagChange
242 tc2.client.Expunge() // MessageExpunge
244 tc2.transactf("ok", `setmetadata Archive (/private/comment "test2")`) // MailboxMetadataChange
246 tc2.transactf("ok", `setmetadata "" (/private/vendor/other/x "test2")`) // ServerMetadataChange
248 tc2.client.Subscribe("doesnotexist2") // SubscriptionChange
249 tc2.client.Unsubscribe("doesnotexist2") // SubscriptionChange
250 tc2.client.Create("newbox2", nil) // MailboxName
252 tc2.client.Rename("newbox2", "oldbox2") // MailboxName
254 tc2.client.Delete("oldbox2") // MailboxName
256 // Now trigger receiving a notification.
257 tc2.client.Create("testbox", nil) // MailboxName
259 tc2.client.Append("testbox", makeAppend(searchMsg)) // MessageNew
262 imapclient.UntaggedStatus{Mailbox: "testbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 1, imapclient.StatusUIDNext: 2, imapclient.StatusUnseen: 1, imapclient.StatusHighestModSeq: int64(modseq)}},
265 // Test filtering per mailbox specifier. We create two mailboxes.
266 tc.client.Create("inbox/a/b", nil)
268 tc.client.Create("other/a/b", nil)
270 tc.client.Unsubscribe("other/a/b")
273 tc3 := startNoSwitchboard(t, uidonly)
274 defer tc3.closeNoWait()
275 tc3.login("mjl@mox.example", password0)
276 tc3.transactf("ok", "Notify Set (Inboxes (messageNew messageExpunge))")
279 tc4 := startNoSwitchboard(t, uidonly)
280 defer tc4.closeNoWait()
281 tc4.login("mjl@mox.example", password0)
282 tc4.transactf("ok", "Notify Set (Subscribed (messageNew messageExpunge))")
285 tc5 := startNoSwitchboard(t, uidonly)
286 defer tc5.closeNoWait()
287 tc5.login("mjl@mox.example", password0)
288 tc5.transactf("ok", "Notify Set (Subtree (Nonexistent inbox) (messageNew messageExpunge))")
291 tc6 := startNoSwitchboard(t, uidonly)
292 defer tc6.closeNoWait()
293 tc6.login("mjl@mox.example", password0)
294 tc6.transactf("ok", "Notify Set (Subtree-One (Nonexistent Inbox/a other) (messageNew messageExpunge))")
296 // We append to other/a/b first. It would normally come first in the notifications,
297 // but we check we only get the second event.
298 tc2.client.Append("other/a/b", makeAppend(searchMsg))
300 tc2.client.Append("inbox/a/b", makeAppend(searchMsg))
303 // No highestmodseq, these connections don't have CONDSTORE enabled.
305 imapclient.UntaggedStatus{Mailbox: "Inbox/a/b", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 1, imapclient.StatusUIDNext: 2, imapclient.StatusUnseen: 1}},
308 imapclient.UntaggedStatus{Mailbox: "Inbox/a/b", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 1, imapclient.StatusUIDNext: 2, imapclient.StatusUnseen: 1}},
311 imapclient.UntaggedStatus{Mailbox: "Inbox/a/b", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 1, imapclient.StatusUIDNext: 2, imapclient.StatusUnseen: 1}},
314 imapclient.UntaggedStatus{Mailbox: "Inbox/a/b", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 1, imapclient.StatusUIDNext: 2, imapclient.StatusUnseen: 1}},
317 // Test for STATUS events on non-selected mailbox for message events.
318 tc.transactf("ok", "notify set (personal (messageNew messageExpunge flagChange))")
320 tc2.client.Create("statusbox", nil)
322 tc2.client.Append("statusbox", makeAppend(searchMsg))
325 imapclient.UntaggedStatus{Mailbox: "statusbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 1, imapclient.StatusUIDNext: 2, imapclient.StatusUnseen: 1, imapclient.StatusHighestModSeq: int64(modseq)}},
328 // With Selected-Delayed, we only get the events for the selected mailbox for
329 // explicit commands. We still get other events.
330 tc.transactf("ok", "notify set (selected-delayed (messageNew messageExpunge flagChange)) (personal (messageNew messageExpunge flagChange))")
331 tc.client.Select("statusbox")
332 tc2.client.Append("inbox", makeAppend(searchMsg))
334 tc2.client.UIDStoreFlagsSet("*", true, `\Seen`)
336 tc2.client.Append("statusbox", imapclient.Append{Flags: []string{"newflag"}, Size: int64(len(searchMsg)), Data: strings.NewReader(searchMsg)})
338 tc2.client.Select("statusbox")
341 imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 1, imapclient.StatusUIDNext: 6, imapclient.StatusUnseen: 1, imapclient.StatusHighestModSeq: int64(modseq - 2)}},
342 imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusUIDValidity: 1, imapclient.StatusUnseen: 0, imapclient.StatusHighestModSeq: int64(modseq - 1)}},
345 tc.transactf("ok", "noop")
347 imapclient.UntaggedExists(2),
348 tc.untaggedFetch(2, 2, imapclient.FetchFlags{"newflag"}, imapclient.FetchModSeq(modseq)),
349 imapclient.UntaggedFlags{`\Seen`, `\Answered`, `\Flagged`, `\Deleted`, `\Draft`, `$Forwarded`, `$Junk`, `$NotJunk`, `$Phishing`, `$MDNSent`, `newflag`},
352 tc2.client.UIDStoreFlagsSet("2", true, `\Deleted`)
356 tc.transactf("ok", "noop")
359 tc.untaggedFetch(2, 2, imapclient.FetchFlags{`\Deleted`}, imapclient.FetchModSeq(modseq-1)),
360 imapclient.UntaggedVanished{UIDs: xparseNumSet("2")},
364 tc.untaggedFetch(2, 2, imapclient.FetchFlags{`\Deleted`}, imapclient.FetchModSeq(modseq-1)),
365 imapclient.UntaggedExpunge(2),
369 // With Selected-Delayed, we should get events for selected mailboxes immediately when using IDLE.
370 tc2.client.UIDStoreFlagsSet("*", true, `\Answered`)
372 tc2.client.Select("inbox")
373 tc2.client.UIDStoreFlagsClear("*", true, `\Seen`)
375 tc2.client.Select("statusbox")
378 imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusUIDValidity: 1, imapclient.StatusUnseen: 1, imapclient.StatusHighestModSeq: int64(modseq)}},
381 tc.conn.SetReadDeadline(time.Now().Add(3 * time.Second))
383 tc.readprefixline("+ ")
384 tc.readuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{`\Answered`}, imapclient.FetchModSeq(modseq-1)))
385 tc.writelinef("done")
387 tc.conn.SetReadDeadline(time.Now().Add(30 * time.Second))
389 // If any event matches, we normally return it. But NONE prevents looking further.
391 tc.transactf("ok", "notify set (mailboxes statusbox NONE) (personal (mailboxName))")
392 tc2.client.UIDStoreFlagsSet("*", true, `\Answered`) // Matches NONE, ignored.
394 tc2.client.Create("eventbox", nil)
397 imapclient.UntaggedList{Mailbox: "eventbox", Separator: '/', Flags: []string{`\Subscribed`}},
400 // Check we can return message contents.
401 tc.transactf("ok", "notify set (selected (messageNew (body[header] body[text]) messageExpunge))")
402 tc.client.Select("statusbox")
403 tc2.client.Append("statusbox", makeAppend(searchMsg))
405 offset := strings.Index(searchMsg, "\r\n\r\n")
407 imapclient.UntaggedExists(2),
408 tc.untaggedFetch(2, 3,
409 imapclient.FetchBody{
410 RespAttr: "BODY[HEADER]",
412 Body: searchMsg[:offset+4],
414 imapclient.FetchBody{
415 RespAttr: "BODY[TEXT]",
417 Body: searchMsg[offset+4:],
419 imapclient.FetchFlags(nil),
423 // If we encounter an error during fetch, an untagged NO is returned.
424 // We ask for the 2nd part of a message, and we add a message with just 1 part.
425 tc.transactf("ok", "notify set (selected (messageNew (body[2]) messageExpunge))")
426 tc2.client.Append("statusbox", makeAppend(exampleMsg))
429 imapclient.UntaggedExists(3),
430 imapclient.UntaggedResult{Status: "NO", Text: "generating notify fetch response: requested part does not exist"},
431 tc.untaggedFetchUID(3, 4),
434 // When adding new tests, uncomment modseq++ lines above.
437func TestNotifyOverflow(t *testing.T) {
438 testNotifyOverflow(t, false)
441func TestNotifyOverflowUIDOnly(t *testing.T) {
442 testNotifyOverflow(t, true)
445func testNotifyOverflow(t *testing.T, uidonly bool) {
446 orig := store.CommPendingChangesMax
447 store.CommPendingChangesMax = 3
449 store.CommPendingChangesMax = orig
452 defer mockUIDValidity()()
453 tc := start(t, uidonly)
455 tc.login("mjl@mox.example", password0)
456 tc.client.Select("inbox")
457 tc.transactf("ok", "noop")
459 tc2 := startNoSwitchboard(t, uidonly)
460 defer tc2.closeNoWait()
461 tc2.login("mjl@mox.example", password0)
462 tc2.client.Select("inbox")
464 // Generates 4 changes, crossing max 3.
465 tc2.client.Append("inbox", makeAppend(searchMsg))
466 tc2.client.Append("inbox", makeAppend(searchMsg))
468 tc.transactf("ok", "noop")
469 tc.xuntagged(imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeWord("NOTIFICATIONOVERFLOW"), Text: "out of sync after too many pending changes"})
471 // Won't be getting any more notifications until we enable them again with NOTIFY.
472 tc2.client.Append("inbox", makeAppend(searchMsg))
473 tc.transactf("ok", "noop")
476 // Enable notify again. Without uidonly, we won't get a notification because the
477 // message isn't known in the session.
478 tc.transactf("ok", "notify set (selected (messageNew messageExpunge flagChange))")
479 tc2.client.UIDStoreFlagsAdd("1", true, `\Seen`)
481 tc.readuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{`\Seen`}))
483 tc.transactf("ok", "noop")
487 // Reselect to get the message visible in the session.
488 tc.client.Select("inbox")
489 tc2.client.UIDStoreFlagsClear("1", true, `\Seen`)
490 tc.transactf("ok", "noop")
491 tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags(nil)))
493 // Trigger overflow for changes for "selected-delayed".
494 store.CommPendingChangesMax = 10
495 delayedMax := selectedDelayedChangesMax
496 selectedDelayedChangesMax = 1
498 selectedDelayedChangesMax = delayedMax
500 tc.transactf("ok", "notify set (selected-delayed (messageNew messageExpunge flagChange))")
501 tc2.client.UIDStoreFlagsAdd("1", true, `\Seen`)
502 tc2.client.UIDStoreFlagsClear("1", true, `\Seen`)
503 tc.transactf("ok", "noop")
504 tc.xuntagged(imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeWord("NOTIFICATIONOVERFLOW"), Text: "out of sync after too many pending changes for selected mailbox"})
506 // Again, no new notifications until we select and enable again.
507 tc2.client.UIDStoreFlagsAdd("1", true, `\Seen`)
508 tc.transactf("ok", "noop")
511 tc.client.Select("inbox")
512 tc.transactf("ok", "notify set (selected-delayed (messageNew messageExpunge flagChange))")
513 tc2.client.UIDStoreFlagsClear("1", true, `\Seen`)
514 tc.transactf("ok", "noop")
515 tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags(nil)))