mirror of
https://github.com/mjl-/mox.git
synced 2025-06-27 23:08:14 +03:00

NOTIFY is like IDLE, but where IDLE watches just the selected mailbox, NOTIFY can watch all mailboxes. With NOTIFY, a client can also ask a server to immediately return configurable fetch attributes for new messages, e.g. a message preview, certain header fields, or simply the entire message. Mild testing with evolution and fairemail.
571 lines
20 KiB
Go
571 lines
20 KiB
Go
package imapserver
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/mjl-/mox/imapclient"
|
|
"github.com/mjl-/mox/store"
|
|
)
|
|
|
|
func ptr[T any](v T) *T {
|
|
return &v
|
|
}
|
|
|
|
func TestNotify(t *testing.T) {
|
|
defer mockUIDValidity()()
|
|
tc := start(t)
|
|
defer tc.close()
|
|
tc.client.Login("mjl@mox.example", password0)
|
|
tc.client.Select("inbox")
|
|
|
|
// Check for some invalid syntax.
|
|
tc.transactf("bad", "Notify")
|
|
tc.transactf("bad", "Notify bogus")
|
|
tc.transactf("bad", "Notify None ") // Trailing space.
|
|
tc.transactf("bad", "Notify Set")
|
|
tc.transactf("bad", "Notify Set ")
|
|
tc.transactf("bad", "Notify Set Status")
|
|
tc.transactf("bad", "Notify Set Status ()") // Empty list.
|
|
tc.transactf("bad", "Notify Set Status (UnknownSpecifier (messageNew))")
|
|
tc.transactf("bad", "Notify Set Status (Personal messageNew)") // Missing list around events.
|
|
tc.transactf("bad", "Notify Set Status (Personal (messageNew) )") // Trailing space.
|
|
tc.transactf("bad", "Notify Set Status (Personal (messageNew)) ") // Trailing space.
|
|
|
|
tc.transactf("bad", "Notify Set Status (Selected (mailboxName))") // MailboxName not allowed on Selected.
|
|
tc.transactf("bad", "Notify Set Status (Selected (messageNew))") // MessageNew must come with MessageExpunge.
|
|
tc.transactf("bad", "Notify Set Status (Selected (flagChange))") // flagChange must come with MessageNew and MessageExpunge.
|
|
tc.transactf("bad", "Notify Set Status (Selected (mailboxName)) (Selected-Delayed (mailboxName))") // Duplicate selected.
|
|
tc.transactf("no", "Notify Set Status (Selected (annotationChange))") // We don't implement annotation change.
|
|
tc.xcode("BADEVENT")
|
|
tc.transactf("no", "Notify Set Status (Personal (unknownEvent))")
|
|
tc.xcode("BADEVENT")
|
|
|
|
tc2 := startNoSwitchboard(t)
|
|
defer tc2.closeNoWait()
|
|
tc2.client.Login("mjl@mox.example", password0)
|
|
tc2.client.Select("inbox")
|
|
|
|
var modseq uint32 = 4
|
|
|
|
// Check that we don't get pending changes when we set "notify none". We first make
|
|
// changes that we drain with noop. Then add new pending changes and execute
|
|
// "notify none". Server should still process changes to the message sequence
|
|
// numbers of the selected mailbox.
|
|
tc2.client.Append("inbox", makeAppend(searchMsg)) // Results in exists and fetch.
|
|
modseq++
|
|
tc2.client.Append("Junk", makeAppend(searchMsg)) // Not selected, not mentioned.
|
|
modseq++
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedExists(1),
|
|
imapclient.UntaggedFetch{
|
|
Seq: 1,
|
|
Attrs: []imapclient.FetchAttr{
|
|
imapclient.FetchUID(1),
|
|
imapclient.FetchFlags(nil),
|
|
},
|
|
},
|
|
)
|
|
tc2.client.StoreFlagsAdd("1:*", true, `\Deleted`)
|
|
modseq++
|
|
tc2.client.Expunge()
|
|
modseq++
|
|
tc.transactf("ok", "Notify None")
|
|
tc.xuntagged() // No untagged responses for delete/expunge.
|
|
|
|
// Enable notify, will first result in a the pending changes, then status.
|
|
tc.transactf("ok", "Notify Set Status (Selected (messageNew (Uid Modseq Bodystructure Preview) messageExpunge flagChange)) (personal (messageNew messageExpunge flagChange mailboxName subscriptionChange mailboxMetadataChange serverMetadataChange))")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(modseq), More: "after condstore-enabling command"}},
|
|
// note: no status for Inbox since it is selected.
|
|
imapclient.UntaggedStatus{Mailbox: "Drafts", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 0, imapclient.StatusUIDNext: 1, imapclient.StatusUIDValidity: 1, imapclient.StatusUnseen: 0, imapclient.StatusHighestModSeq: 2}},
|
|
imapclient.UntaggedStatus{Mailbox: "Sent", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 0, imapclient.StatusUIDNext: 1, imapclient.StatusUIDValidity: 1, imapclient.StatusUnseen: 0, imapclient.StatusHighestModSeq: 2}},
|
|
imapclient.UntaggedStatus{Mailbox: "Archive", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 0, imapclient.StatusUIDNext: 1, imapclient.StatusUIDValidity: 1, imapclient.StatusUnseen: 0, imapclient.StatusHighestModSeq: 2}},
|
|
imapclient.UntaggedStatus{Mailbox: "Trash", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 0, imapclient.StatusUIDNext: 1, imapclient.StatusUIDValidity: 1, imapclient.StatusUnseen: 0, imapclient.StatusHighestModSeq: 2}},
|
|
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)}},
|
|
)
|
|
|
|
// Selecting the mailbox again results in a refresh of the message sequence
|
|
// numbers, with the deleted message gone (it wasn't acknowledged yet due to
|
|
// "notify none").
|
|
tc.client.Select("inbox")
|
|
|
|
// Add message, should result in EXISTS and FETCH with the configured attributes.
|
|
tc2.client.Append("inbox", makeAppend(searchMsg))
|
|
modseq++
|
|
tc.readuntagged(
|
|
imapclient.UntaggedExists(1),
|
|
imapclient.UntaggedFetch{
|
|
Seq: 1,
|
|
Attrs: []imapclient.FetchAttr{
|
|
imapclient.FetchUID(2),
|
|
imapclient.FetchBodystructure{
|
|
RespAttr: "BODYSTRUCTURE",
|
|
Body: imapclient.BodyTypeMpart{
|
|
Bodies: []any{
|
|
imapclient.BodyTypeText{
|
|
MediaType: "TEXT",
|
|
MediaSubtype: "PLAIN",
|
|
BodyFields: imapclient.BodyFields{
|
|
Params: [][2]string{[...]string{"CHARSET", "utf-8"}},
|
|
Octets: 21,
|
|
},
|
|
Lines: 1,
|
|
Ext: &imapclient.BodyExtension1Part{},
|
|
},
|
|
imapclient.BodyTypeText{
|
|
MediaType: "TEXT",
|
|
MediaSubtype: "HTML",
|
|
BodyFields: imapclient.BodyFields{
|
|
Params: [][2]string{[...]string{"CHARSET", "utf-8"}},
|
|
Octets: 15,
|
|
},
|
|
Lines: 1,
|
|
Ext: &imapclient.BodyExtension1Part{},
|
|
},
|
|
},
|
|
MediaSubtype: "ALTERNATIVE",
|
|
Ext: &imapclient.BodyExtensionMpart{
|
|
Params: [][2]string{{"BOUNDARY", "x"}},
|
|
},
|
|
},
|
|
},
|
|
imapclient.FetchPreview{Preview: ptr("this is plain text.")},
|
|
imapclient.FetchModSeq(modseq),
|
|
},
|
|
},
|
|
)
|
|
|
|
// Change flags.
|
|
tc2.client.StoreFlagsAdd("1:*", true, `\Deleted`)
|
|
modseq++
|
|
tc.readuntagged(
|
|
imapclient.UntaggedFetch{
|
|
Seq: 1,
|
|
Attrs: []imapclient.FetchAttr{
|
|
imapclient.FetchUID(2),
|
|
imapclient.FetchFlags{`\Deleted`},
|
|
imapclient.FetchModSeq(modseq),
|
|
},
|
|
},
|
|
)
|
|
|
|
// Remove message.
|
|
tc2.client.Expunge()
|
|
modseq++
|
|
tc.readuntagged(
|
|
imapclient.UntaggedExpunge(1),
|
|
)
|
|
|
|
// MailboxMetadataChange for mailbox annotation.
|
|
tc2.transactf("ok", `setmetadata Archive (/private/comment "test")`)
|
|
modseq++
|
|
tc.readuntagged(
|
|
imapclient.UntaggedMetadataKeys{Mailbox: "Archive", Keys: []string{"/private/comment"}},
|
|
)
|
|
|
|
// MailboxMetadataChange also for the selected Inbox.
|
|
tc2.transactf("ok", `setmetadata Inbox (/private/comment "test")`)
|
|
modseq++
|
|
tc.readuntagged(
|
|
imapclient.UntaggedMetadataKeys{Mailbox: "Inbox", Keys: []string{"/private/comment"}},
|
|
)
|
|
|
|
// ServerMetadataChange for server annotation.
|
|
tc2.transactf("ok", `setmetadata "" (/private/vendor/other/x "test")`)
|
|
modseq++
|
|
tc.readuntagged(
|
|
imapclient.UntaggedMetadataKeys{Mailbox: "", Keys: []string{"/private/vendor/other/x"}},
|
|
)
|
|
|
|
// SubscriptionChange for new subscription.
|
|
tc2.client.Subscribe("doesnotexist")
|
|
tc.readuntagged(
|
|
imapclient.UntaggedList{Mailbox: "doesnotexist", Separator: '/', Flags: []string{`\Subscribed`, `\NonExistent`}},
|
|
)
|
|
|
|
// SubscriptionChange for removed subscription.
|
|
tc2.client.Unsubscribe("doesnotexist")
|
|
tc.readuntagged(
|
|
imapclient.UntaggedList{Mailbox: "doesnotexist", Separator: '/', Flags: []string{`\NonExistent`}},
|
|
)
|
|
|
|
// SubscriptionChange for selected mailbox.
|
|
tc2.client.Unsubscribe("Inbox")
|
|
tc2.client.Subscribe("Inbox")
|
|
tc.readuntagged(
|
|
imapclient.UntaggedList{Mailbox: "Inbox", Separator: '/'},
|
|
imapclient.UntaggedList{Mailbox: "Inbox", Separator: '/', Flags: []string{`\Subscribed`}},
|
|
)
|
|
|
|
// MailboxName for creating mailbox.
|
|
tc2.client.Create("newbox", nil)
|
|
modseq++
|
|
tc.readuntagged(
|
|
imapclient.UntaggedList{Mailbox: "newbox", Separator: '/', Flags: []string{`\Subscribed`}},
|
|
)
|
|
|
|
// MailboxName for renaming mailbox.
|
|
tc2.client.Rename("newbox", "oldbox")
|
|
modseq++
|
|
tc.readuntagged(
|
|
imapclient.UntaggedList{Mailbox: "oldbox", Separator: '/', OldName: "newbox"},
|
|
)
|
|
|
|
// MailboxName for deleting mailbox.
|
|
tc2.client.Delete("oldbox")
|
|
modseq++
|
|
tc.readuntagged(
|
|
imapclient.UntaggedList{Mailbox: "oldbox", Separator: '/', Flags: []string{`\NonExistent`}},
|
|
)
|
|
|
|
// Add message again to check for modseq. First set notify again with fewer fetch
|
|
// attributes for simpler checking.
|
|
tc.transactf("ok", "Notify Set (personal (messageNew messageExpunge flagChange mailboxName subscriptionChange mailboxMetadataChange serverMetadataChange)) (Selected (messageNew (Uid Modseq) messageExpunge flagChange))")
|
|
tc2.client.Append("inbox", makeAppend(searchMsg))
|
|
modseq++
|
|
tc.readuntagged(
|
|
imapclient.UntaggedExists(1),
|
|
imapclient.UntaggedFetch{
|
|
Seq: 1,
|
|
Attrs: []imapclient.FetchAttr{
|
|
imapclient.FetchUID(3),
|
|
imapclient.FetchModSeq(modseq),
|
|
},
|
|
},
|
|
)
|
|
|
|
// Next round of events must be ignored. We shouldn't get anything until we add a
|
|
// message to "testbox".
|
|
tc.transactf("ok", "Notify Set (Selected None) (mailboxes testbox (messageNew messageExpunge)) (personal None)")
|
|
tc2.client.Append("inbox", makeAppend(searchMsg)) // MessageNew
|
|
modseq++
|
|
tc2.client.StoreFlagsAdd("1:*", true, `\Deleted`) // FlagChange
|
|
modseq++
|
|
tc2.client.Expunge() // MessageExpunge
|
|
modseq++
|
|
tc2.transactf("ok", `setmetadata Archive (/private/comment "test2")`) // MailboxMetadataChange
|
|
modseq++
|
|
tc2.transactf("ok", `setmetadata "" (/private/vendor/other/x "test2")`) // ServerMetadataChange
|
|
modseq++
|
|
tc2.client.Subscribe("doesnotexist2") // SubscriptionChange
|
|
tc2.client.Unsubscribe("doesnotexist2") // SubscriptionChange
|
|
tc2.client.Create("newbox2", nil) // MailboxName
|
|
modseq++
|
|
tc2.client.Rename("newbox2", "oldbox2") // MailboxName
|
|
modseq++
|
|
tc2.client.Delete("oldbox2") // MailboxName
|
|
modseq++
|
|
// Now trigger receiving a notification.
|
|
tc2.client.Create("testbox", nil) // MailboxName
|
|
modseq++
|
|
tc2.client.Append("testbox", makeAppend(searchMsg)) // MessageNew
|
|
modseq++
|
|
tc.readuntagged(
|
|
imapclient.UntaggedStatus{Mailbox: "testbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 1, imapclient.StatusUIDNext: 2, imapclient.StatusUnseen: 1, imapclient.StatusHighestModSeq: int64(modseq)}},
|
|
)
|
|
|
|
// Test filtering per mailbox specifier. We create two mailboxes.
|
|
tc.client.Create("inbox/a/b", nil)
|
|
modseq++
|
|
tc.client.Create("other/a/b", nil)
|
|
modseq++
|
|
tc.client.Unsubscribe("other/a/b")
|
|
|
|
// Inboxes
|
|
tc3 := startNoSwitchboard(t)
|
|
defer tc3.closeNoWait()
|
|
tc3.client.Login("mjl@mox.example", password0)
|
|
tc3.transactf("ok", "Notify Set (Inboxes (messageNew messageExpunge))")
|
|
|
|
// Subscribed
|
|
tc4 := startNoSwitchboard(t)
|
|
defer tc4.closeNoWait()
|
|
tc4.client.Login("mjl@mox.example", password0)
|
|
tc4.transactf("ok", "Notify Set (Subscribed (messageNew messageExpunge))")
|
|
|
|
// Subtree
|
|
tc5 := startNoSwitchboard(t)
|
|
defer tc5.closeNoWait()
|
|
tc5.client.Login("mjl@mox.example", password0)
|
|
tc5.transactf("ok", "Notify Set (Subtree (Nonexistent inbox) (messageNew messageExpunge))")
|
|
|
|
// Subtree-One
|
|
tc6 := startNoSwitchboard(t)
|
|
defer tc6.closeNoWait()
|
|
tc6.client.Login("mjl@mox.example", password0)
|
|
tc6.transactf("ok", "Notify Set (Subtree-One (Nonexistent Inbox/a other) (messageNew messageExpunge))")
|
|
|
|
// We append to other/a/b first. It would normally come first in the notifications,
|
|
// but we check we only get the second event.
|
|
tc2.client.Append("other/a/b", makeAppend(searchMsg))
|
|
modseq++
|
|
tc2.client.Append("inbox/a/b", makeAppend(searchMsg))
|
|
modseq++
|
|
|
|
// No highestmodseq, these connections don't have CONDSTORE enabled.
|
|
tc3.readuntagged(
|
|
imapclient.UntaggedStatus{Mailbox: "Inbox/a/b", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 1, imapclient.StatusUIDNext: 2, imapclient.StatusUnseen: 1}},
|
|
)
|
|
tc4.readuntagged(
|
|
imapclient.UntaggedStatus{Mailbox: "Inbox/a/b", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 1, imapclient.StatusUIDNext: 2, imapclient.StatusUnseen: 1}},
|
|
)
|
|
tc5.readuntagged(
|
|
imapclient.UntaggedStatus{Mailbox: "Inbox/a/b", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 1, imapclient.StatusUIDNext: 2, imapclient.StatusUnseen: 1}},
|
|
)
|
|
tc6.readuntagged(
|
|
imapclient.UntaggedStatus{Mailbox: "Inbox/a/b", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 1, imapclient.StatusUIDNext: 2, imapclient.StatusUnseen: 1}},
|
|
)
|
|
|
|
// Test for STATUS events on non-selected mailbox for message events.
|
|
tc.transactf("ok", "notify set (personal (messageNew messageExpunge flagChange))")
|
|
tc.client.Unselect()
|
|
tc2.client.Create("statusbox", nil)
|
|
modseq++
|
|
tc2.client.Append("statusbox", makeAppend(searchMsg))
|
|
modseq++
|
|
tc.readuntagged(
|
|
imapclient.UntaggedStatus{Mailbox: "statusbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 1, imapclient.StatusUIDNext: 2, imapclient.StatusUnseen: 1, imapclient.StatusHighestModSeq: int64(modseq)}},
|
|
)
|
|
|
|
// With Selected-Delayed, we only get the events for the selected mailbox for
|
|
// explicit commands. We still get other events.
|
|
tc.transactf("ok", "notify set (selected-delayed (messageNew messageExpunge flagChange)) (personal (messageNew messageExpunge flagChange))")
|
|
tc.client.Select("statusbox")
|
|
tc2.client.Append("inbox", makeAppend(searchMsg))
|
|
modseq++
|
|
tc2.client.StoreFlagsSet("1", true, `\Seen`)
|
|
modseq++
|
|
tc2.client.Append("statusbox", imapclient.Append{Flags: []string{"newflag"}, Size: int64(len(searchMsg)), Data: strings.NewReader(searchMsg)})
|
|
modseq++
|
|
tc2.client.Select("statusbox")
|
|
|
|
tc.readuntagged(
|
|
imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 1, imapclient.StatusUIDNext: 6, imapclient.StatusUnseen: 1, imapclient.StatusHighestModSeq: int64(modseq - 2)}},
|
|
imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusUIDValidity: 1, imapclient.StatusUnseen: 0, imapclient.StatusHighestModSeq: int64(modseq - 1)}},
|
|
)
|
|
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedExists(2),
|
|
imapclient.UntaggedFetch{
|
|
Seq: 2,
|
|
Attrs: []imapclient.FetchAttr{
|
|
imapclient.FetchUID(2),
|
|
imapclient.FetchFlags{`newflag`},
|
|
imapclient.FetchModSeq(modseq),
|
|
},
|
|
},
|
|
imapclient.UntaggedFlags{`\Seen`, `\Answered`, `\Flagged`, `\Deleted`, `\Draft`, `$Forwarded`, `$Junk`, `$NotJunk`, `$Phishing`, `$MDNSent`, `newflag`},
|
|
)
|
|
|
|
tc2.client.StoreFlagsSet("2", true, `\Deleted`)
|
|
modseq++
|
|
tc2.client.Expunge()
|
|
modseq++
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedFetch{
|
|
Seq: 2,
|
|
Attrs: []imapclient.FetchAttr{
|
|
imapclient.FetchUID(2),
|
|
imapclient.FetchFlags{`\Deleted`},
|
|
imapclient.FetchModSeq(modseq - 1),
|
|
},
|
|
},
|
|
imapclient.UntaggedExpunge(2),
|
|
)
|
|
|
|
// With Selected-Delayed, we should get events for selected mailboxes immediately when using IDLE.
|
|
tc2.client.StoreFlagsSet("*", true, `\Answered`)
|
|
modseq++
|
|
|
|
tc2.client.Select("inbox")
|
|
tc2.client.StoreFlagsClear("1", true, `\Seen`)
|
|
modseq++
|
|
tc2.client.Select("statusbox")
|
|
|
|
tc.readuntagged(
|
|
imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusUIDValidity: 1, imapclient.StatusUnseen: 1, imapclient.StatusHighestModSeq: int64(modseq)}},
|
|
)
|
|
|
|
tc.conn.SetReadDeadline(time.Now().Add(3 * time.Second))
|
|
tc.cmdf("", "idle")
|
|
tc.readprefixline("+ ")
|
|
tc.readuntagged(imapclient.UntaggedFetch{
|
|
Seq: 1,
|
|
Attrs: []imapclient.FetchAttr{
|
|
imapclient.FetchUID(1),
|
|
imapclient.FetchFlags{`\Answered`},
|
|
imapclient.FetchModSeq(modseq - 1),
|
|
},
|
|
})
|
|
tc.writelinef("done")
|
|
tc.response("ok")
|
|
tc.conn.SetReadDeadline(time.Now().Add(30 * time.Second))
|
|
|
|
// If any event matches, we normally return it. But NONE prevents looking further.
|
|
tc.client.Unselect()
|
|
tc.transactf("ok", "notify set (mailboxes statusbox NONE) (personal (mailboxName))")
|
|
tc2.client.StoreFlagsSet("*", true, `\Answered`) // Matches NONE, ignored.
|
|
//modseq++
|
|
tc2.client.Create("eventbox", nil)
|
|
//modseq++
|
|
tc.readuntagged(
|
|
imapclient.UntaggedList{Mailbox: "eventbox", Separator: '/', Flags: []string{`\Subscribed`}},
|
|
)
|
|
|
|
// Check we can return message contents.
|
|
tc.transactf("ok", "notify set (selected (messageNew (body[header] body[text]) messageExpunge))")
|
|
tc.client.Select("statusbox")
|
|
tc2.client.Append("statusbox", makeAppend(searchMsg))
|
|
// modseq++
|
|
offset := strings.Index(searchMsg, "\r\n\r\n")
|
|
tc.readuntagged(
|
|
imapclient.UntaggedExists(2),
|
|
imapclient.UntaggedFetch{
|
|
Seq: 2,
|
|
Attrs: []imapclient.FetchAttr{
|
|
imapclient.FetchUID(3),
|
|
imapclient.FetchBody{
|
|
RespAttr: "BODY[HEADER]",
|
|
Section: "HEADER",
|
|
Body: searchMsg[:offset+4],
|
|
},
|
|
imapclient.FetchBody{
|
|
RespAttr: "BODY[TEXT]",
|
|
Section: "TEXT",
|
|
Body: searchMsg[offset+4:],
|
|
},
|
|
imapclient.FetchFlags(nil),
|
|
},
|
|
},
|
|
)
|
|
|
|
// If we encounter an error during fetch, an untagged NO is returned.
|
|
// We ask for the 2nd part of a message, and we add a message with just 1 part.
|
|
tc.transactf("ok", "notify set (selected (messageNew (body[2]) messageExpunge))")
|
|
tc2.client.Append("statusbox", makeAppend(exampleMsg))
|
|
// modseq++
|
|
tc.readuntagged(
|
|
imapclient.UntaggedExists(3),
|
|
imapclient.UntaggedResult{
|
|
Status: "NO",
|
|
RespText: imapclient.RespText{
|
|
More: "generating notify fetch response: requested part does not exist",
|
|
},
|
|
},
|
|
imapclient.UntaggedFetch{
|
|
Seq: 3,
|
|
Attrs: []imapclient.FetchAttr{
|
|
imapclient.FetchUID(4),
|
|
},
|
|
},
|
|
)
|
|
|
|
// When adding new tests, uncomment modseq++ lines above.
|
|
}
|
|
|
|
func TestNotifyOverflow(t *testing.T) {
|
|
orig := store.CommPendingChangesMax
|
|
store.CommPendingChangesMax = 3
|
|
defer func() {
|
|
store.CommPendingChangesMax = orig
|
|
}()
|
|
|
|
defer mockUIDValidity()()
|
|
tc := start(t)
|
|
defer tc.close()
|
|
tc.client.Login("mjl@mox.example", password0)
|
|
tc.client.Select("inbox")
|
|
tc.transactf("ok", "noop")
|
|
|
|
tc2 := startNoSwitchboard(t)
|
|
defer tc2.closeNoWait()
|
|
tc2.client.Login("mjl@mox.example", password0)
|
|
tc2.client.Select("inbox")
|
|
|
|
// Generates 4 changes, crossing max 3.
|
|
tc2.client.Append("inbox", makeAppend(searchMsg))
|
|
tc2.client.Append("inbox", makeAppend(searchMsg))
|
|
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedResult{
|
|
Status: "OK",
|
|
RespText: imapclient.RespText{
|
|
Code: "NOTIFICATIONOVERFLOW",
|
|
More: "out of sync after too many pending changes",
|
|
},
|
|
},
|
|
)
|
|
|
|
// Won't be getting any more notifications until we enable them again with NOTIFY.
|
|
tc2.client.Append("inbox", makeAppend(searchMsg))
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged()
|
|
|
|
// Enable notify again. We won't get a notification because the message isn't yet
|
|
// known in the session.
|
|
tc.transactf("ok", "notify set (selected (messageNew messageExpunge flagChange))")
|
|
tc2.client.StoreFlagsAdd("1", true, `\Seen`)
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged()
|
|
|
|
// Reselect to get the message visible in the session.
|
|
tc.client.Select("inbox")
|
|
tc2.client.StoreFlagsClear("1", true, `\Seen`)
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedFetch{
|
|
Seq: 1,
|
|
Attrs: []imapclient.FetchAttr{
|
|
imapclient.FetchUID(1),
|
|
imapclient.FetchFlags(nil),
|
|
},
|
|
},
|
|
)
|
|
|
|
// Trigger overflow for changes for "selected-delayed".
|
|
store.CommPendingChangesMax = 10
|
|
delayedMax := selectedDelayedChangesMax
|
|
selectedDelayedChangesMax = 1
|
|
defer func() {
|
|
selectedDelayedChangesMax = delayedMax
|
|
}()
|
|
tc.transactf("ok", "notify set (selected-delayed (messageNew messageExpunge flagChange))")
|
|
tc2.client.StoreFlagsAdd("1", true, `\Seen`)
|
|
tc2.client.StoreFlagsClear("1", true, `\Seen`)
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedResult{
|
|
Status: "OK",
|
|
RespText: imapclient.RespText{
|
|
Code: "NOTIFICATIONOVERFLOW",
|
|
More: "out of sync after too many pending changes for selected mailbox",
|
|
},
|
|
},
|
|
)
|
|
|
|
// Again, no new notifications until we select and enable again.
|
|
tc2.client.StoreFlagsAdd("1", true, `\Seen`)
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged()
|
|
|
|
tc.client.Select("inbox")
|
|
tc.transactf("ok", "notify set (selected-delayed (messageNew messageExpunge flagChange))")
|
|
tc2.client.StoreFlagsClear("1", true, `\Seen`)
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedFetch{
|
|
Seq: 1,
|
|
Attrs: []imapclient.FetchAttr{
|
|
imapclient.FetchUID(1),
|
|
imapclient.FetchFlags(nil),
|
|
},
|
|
},
|
|
)
|
|
}
|