mirror of
https://github.com/mjl-/mox.git
synced 2025-06-28 01:48:15 +03:00
imapserver: implement UIDONLY extension, RFC 9586
Once clients enable this extension, commands can no longer refer to "message sequence numbers" (MSNs), but can only refer to messages with UIDs. This means both sides no longer have to carefully keep their sequence numbers in sync (error-prone), and don't have to keep track of a mapping of sequence numbers to UIDs (saves resources). With UIDONLY enabled, all FETCH responses are replaced with UIDFETCH response.
This commit is contained in:
parent
8bab38eac4
commit
507ca73b96
@ -145,7 +145,6 @@ support:
|
|||||||
- Automate DNS management, for setup and maintenance, such as DANE/DKIM key rotation
|
- Automate DNS management, for setup and maintenance, such as DANE/DKIM key rotation
|
||||||
- Config options for "transactional email domains", for which mox will only
|
- Config options for "transactional email domains", for which mox will only
|
||||||
send messages
|
send messages
|
||||||
- More IMAP extensions (UIDONLY)
|
|
||||||
- Encrypted storage of files (email messages, TLS keys), also with per account keys
|
- Encrypted storage of files (email messages, TLS keys), also with per account keys
|
||||||
- Recognize common deliverability issues and help postmasters solve them
|
- Recognize common deliverability issues and help postmasters solve them
|
||||||
- JMAP, IMAP OBJECTID extension, IMAP JMAPACCESS extension
|
- JMAP, IMAP OBJECTID extension, IMAP JMAPACCESS extension
|
||||||
|
@ -388,6 +388,42 @@ func (c *Conn) StoreFlagsClear(seqset string, silent bool, flags ...string) (unt
|
|||||||
return c.Transactf("store %s %s (%s)", seqset, item, strings.Join(flags, " "))
|
return c.Transactf("store %s %s (%s)", seqset, item, strings.Join(flags, " "))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UIDStoreFlagsSet stores a new set of flags for messages from uid set with
|
||||||
|
// the UID STORE command.
|
||||||
|
//
|
||||||
|
// If silent, no untagged responses with the updated flags will be sent by the
|
||||||
|
// server.
|
||||||
|
func (c *Conn) UIDStoreFlagsSet(seqset string, silent bool, flags ...string) (untagged []Untagged, result Result, rerr error) {
|
||||||
|
defer c.recover(&rerr)
|
||||||
|
item := "flags"
|
||||||
|
if silent {
|
||||||
|
item += ".silent"
|
||||||
|
}
|
||||||
|
return c.Transactf("uid store %s %s (%s)", seqset, item, strings.Join(flags, " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UIDStoreFlagsAdd is like UIDStoreFlagsSet, but only adds flags, leaving
|
||||||
|
// current flags on the message intact.
|
||||||
|
func (c *Conn) UIDStoreFlagsAdd(seqset string, silent bool, flags ...string) (untagged []Untagged, result Result, rerr error) {
|
||||||
|
defer c.recover(&rerr)
|
||||||
|
item := "+flags"
|
||||||
|
if silent {
|
||||||
|
item += ".silent"
|
||||||
|
}
|
||||||
|
return c.Transactf("uid store %s %s (%s)", seqset, item, strings.Join(flags, " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UIDStoreFlagsClear is like UIDStoreFlagsSet, but only removes flags, leaving
|
||||||
|
// other flags on the message intact.
|
||||||
|
func (c *Conn) UIDStoreFlagsClear(seqset string, silent bool, flags ...string) (untagged []Untagged, result Result, rerr error) {
|
||||||
|
defer c.recover(&rerr)
|
||||||
|
item := "-flags"
|
||||||
|
if silent {
|
||||||
|
item += ".silent"
|
||||||
|
}
|
||||||
|
return c.Transactf("uid store %s %s (%s)", seqset, item, strings.Join(flags, " "))
|
||||||
|
}
|
||||||
|
|
||||||
// Copy adds messages from the sequences in seqSet in the currently selected/active mailbox to dstMailbox.
|
// Copy adds messages from the sequences in seqSet in the currently selected/active mailbox to dstMailbox.
|
||||||
func (c *Conn) Copy(seqSet NumSet, dstMailbox string) (untagged []Untagged, result Result, rerr error) {
|
func (c *Conn) Copy(seqSet NumSet, dstMailbox string) (untagged []Untagged, result Result, rerr error) {
|
||||||
defer c.recover(&rerr)
|
defer c.recover(&rerr)
|
||||||
|
@ -136,6 +136,7 @@ var knownCodes = stringMap(
|
|||||||
"INPROGRESS", // ../rfc/9585:104
|
"INPROGRESS", // ../rfc/9585:104
|
||||||
"BADEVENT", "NOTIFICATIONOVERFLOW", // ../rfc/5465:1023
|
"BADEVENT", "NOTIFICATIONOVERFLOW", // ../rfc/5465:1023
|
||||||
"SERVERBUG",
|
"SERVERBUG",
|
||||||
|
"UIDREQUIRED", // ../rfc/9586:136
|
||||||
)
|
)
|
||||||
|
|
||||||
func stringMap(l ...string) map[string]struct{} {
|
func stringMap(l ...string) map[string]struct{} {
|
||||||
@ -654,14 +655,17 @@ func (c *Conn) xuntagged() Untagged {
|
|||||||
w = c.xword()
|
w = c.xword()
|
||||||
W = strings.ToUpper(w)
|
W = strings.ToUpper(w)
|
||||||
switch W {
|
switch W {
|
||||||
case "FETCH":
|
case "FETCH", "UIDFETCH":
|
||||||
if num == 0 {
|
if num == 0 {
|
||||||
c.xerrorf("invalid zero number for untagged fetch response")
|
c.xerrorf("invalid zero number for untagged fetch response")
|
||||||
}
|
}
|
||||||
c.xspace()
|
c.xspace()
|
||||||
r := c.xfetch(num)
|
attrs := c.xfetch()
|
||||||
c.xcrlf()
|
c.xcrlf()
|
||||||
return r
|
if W == "UIDFETCH" {
|
||||||
|
return UntaggedUIDFetch{num, attrs}
|
||||||
|
}
|
||||||
|
return UntaggedFetch{num, attrs}
|
||||||
|
|
||||||
case "EXPUNGE":
|
case "EXPUNGE":
|
||||||
if num == 0 {
|
if num == 0 {
|
||||||
@ -691,14 +695,14 @@ func (c *Conn) xuntagged() Untagged {
|
|||||||
|
|
||||||
// ../rfc/3501:4864 ../rfc/9051:6742
|
// ../rfc/3501:4864 ../rfc/9051:6742
|
||||||
// Already parsed: "*" SP nznumber SP "FETCH" SP
|
// Already parsed: "*" SP nznumber SP "FETCH" SP
|
||||||
func (c *Conn) xfetch(num uint32) UntaggedFetch {
|
func (c *Conn) xfetch() []FetchAttr {
|
||||||
c.xtake("(")
|
c.xtake("(")
|
||||||
attrs := []FetchAttr{c.xmsgatt1()}
|
attrs := []FetchAttr{c.xmsgatt1()}
|
||||||
for c.space() {
|
for c.space() {
|
||||||
attrs = append(attrs, c.xmsgatt1())
|
attrs = append(attrs, c.xmsgatt1())
|
||||||
}
|
}
|
||||||
c.xtake(")")
|
c.xtake(")")
|
||||||
return UntaggedFetch{num, attrs}
|
return attrs
|
||||||
}
|
}
|
||||||
|
|
||||||
// ../rfc/9051:6746
|
// ../rfc/9051:6746
|
||||||
|
@ -43,6 +43,7 @@ const (
|
|||||||
CapPreview Capability = "PREVIEW" // ../rfc/8970:114
|
CapPreview Capability = "PREVIEW" // ../rfc/8970:114
|
||||||
CapMultiSearch Capability = "MULTISEARCH" // ../rfc/7377:187
|
CapMultiSearch Capability = "MULTISEARCH" // ../rfc/7377:187
|
||||||
CapNotify Capability = "NOTIFY" // ../rfc/5465:195
|
CapNotify Capability = "NOTIFY" // ../rfc/5465:195
|
||||||
|
CapUIDOnly Capability = "UIDONLY" // ../rfc/9586:129
|
||||||
)
|
)
|
||||||
|
|
||||||
// Status is the tagged final result of a command.
|
// Status is the tagged final result of a command.
|
||||||
@ -261,6 +262,14 @@ type UntaggedFetch struct {
|
|||||||
Seq uint32
|
Seq uint32
|
||||||
Attrs []FetchAttr
|
Attrs []FetchAttr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UntaggedUIDFetch is like UntaggedFetch, but with UIDs instead of message
|
||||||
|
// sequence numbers, and returned instead of regular fetch responses when UIDONLY
|
||||||
|
// is enabled.
|
||||||
|
type UntaggedUIDFetch struct {
|
||||||
|
UID uint32
|
||||||
|
Attrs []FetchAttr
|
||||||
|
}
|
||||||
type UntaggedSearch []uint32
|
type UntaggedSearch []uint32
|
||||||
|
|
||||||
// ../rfc/7162:1101
|
// ../rfc/7162:1101
|
||||||
|
@ -7,22 +7,30 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestAppend(t *testing.T) {
|
func TestAppend(t *testing.T) {
|
||||||
|
testAppend(t, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppendUIDOnly(t *testing.T) {
|
||||||
|
testAppend(t, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAppend(t *testing.T, uidonly bool) {
|
||||||
defer mockUIDValidity()()
|
defer mockUIDValidity()()
|
||||||
|
|
||||||
tc := start(t) // note: with switchboard because this connection stays alive unlike tc2.
|
tc := start(t, uidonly) // note: with switchboard because this connection stays alive unlike tc2.
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc2 := startNoSwitchboard(t) // note: without switchboard because this connection will break during tests.
|
tc2 := startNoSwitchboard(t, uidonly) // note: without switchboard because this connection will break during tests.
|
||||||
defer tc2.closeNoWait()
|
defer tc2.closeNoWait()
|
||||||
|
|
||||||
tc3 := startNoSwitchboard(t)
|
tc3 := startNoSwitchboard(t, uidonly)
|
||||||
defer tc3.closeNoWait()
|
defer tc3.closeNoWait()
|
||||||
|
|
||||||
tc2.client.Login("mjl@mox.example", password0)
|
tc2.login("mjl@mox.example", password0)
|
||||||
tc2.client.Select("inbox")
|
tc2.client.Select("inbox")
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc.client.Select("inbox")
|
tc.client.Select("inbox")
|
||||||
tc3.client.Login("mjl@mox.example", password0)
|
tc3.login("mjl@mox.example", password0)
|
||||||
|
|
||||||
tc2.transactf("bad", "append") // Missing params.
|
tc2.transactf("bad", "append") // Missing params.
|
||||||
tc2.transactf("bad", `append inbox`) // Missing message.
|
tc2.transactf("bad", `append inbox`) // Missing message.
|
||||||
@ -30,15 +38,15 @@ func TestAppend(t *testing.T) {
|
|||||||
|
|
||||||
// Syntax error for line ending in literal causes connection abort.
|
// Syntax error for line ending in literal causes connection abort.
|
||||||
tc2.transactf("bad", "append inbox (\\Badflag) {1+}\r\nx") // Unknown flag.
|
tc2.transactf("bad", "append inbox (\\Badflag) {1+}\r\nx") // Unknown flag.
|
||||||
tc2 = startNoSwitchboard(t)
|
tc2 = startNoSwitchboard(t, uidonly)
|
||||||
defer tc2.closeNoWait()
|
defer tc2.closeNoWait()
|
||||||
tc2.client.Login("mjl@mox.example", password0)
|
tc2.login("mjl@mox.example", password0)
|
||||||
tc2.client.Select("inbox")
|
tc2.client.Select("inbox")
|
||||||
|
|
||||||
tc2.transactf("bad", "append inbox () \"bad time\" {1+}\r\nx") // Bad time.
|
tc2.transactf("bad", "append inbox () \"bad time\" {1+}\r\nx") // Bad time.
|
||||||
tc2 = startNoSwitchboard(t)
|
tc2 = startNoSwitchboard(t, uidonly)
|
||||||
defer tc2.closeNoWait()
|
defer tc2.closeNoWait()
|
||||||
tc2.client.Login("mjl@mox.example", password0)
|
tc2.login("mjl@mox.example", password0)
|
||||||
tc2.client.Select("inbox")
|
tc2.client.Select("inbox")
|
||||||
|
|
||||||
tc2.transactf("no", "append nobox (\\Seen) \" 1-Jan-2022 10:10:00 +0100\" {1}")
|
tc2.transactf("no", "append nobox (\\Seen) \" 1-Jan-2022 10:10:00 +0100\" {1}")
|
||||||
@ -52,9 +60,8 @@ func TestAppend(t *testing.T) {
|
|||||||
tc2.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("1")})
|
tc2.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("1")})
|
||||||
|
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
uid1 := imapclient.FetchUID(1)
|
|
||||||
flags := imapclient.FetchFlags{`\Seen`, "$label2", "label1"}
|
flags := imapclient.FetchFlags{`\Seen`, "$label2", "label1"}
|
||||||
tc.xuntagged(imapclient.UntaggedExists(1), imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flags}})
|
tc.xuntagged(imapclient.UntaggedExists(1), tc.untaggedFetch(1, 1, flags))
|
||||||
tc3.transactf("ok", "noop")
|
tc3.transactf("ok", "noop")
|
||||||
tc3.xuntagged() // Inbox is not selected, nothing to report.
|
tc3.xuntagged() // Inbox is not selected, nothing to report.
|
||||||
|
|
||||||
@ -69,7 +76,6 @@ func TestAppend(t *testing.T) {
|
|||||||
// Messages that we cannot parse are marked as application/octet-stream. Perhaps
|
// Messages that we cannot parse are marked as application/octet-stream. Perhaps
|
||||||
// the imap client knows how to deal with them.
|
// the imap client knows how to deal with them.
|
||||||
tc2.transactf("ok", "uid fetch 2 body")
|
tc2.transactf("ok", "uid fetch 2 body")
|
||||||
uid2 := imapclient.FetchUID(2)
|
|
||||||
xbs := imapclient.FetchBodystructure{
|
xbs := imapclient.FetchBodystructure{
|
||||||
RespAttr: "BODY",
|
RespAttr: "BODY",
|
||||||
Body: imapclient.BodyTypeBasic{
|
Body: imapclient.BodyTypeBasic{
|
||||||
@ -80,7 +86,7 @@ func TestAppend(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
tc2.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, xbs}})
|
tc2.xuntagged(tc.untaggedFetch(2, 2, xbs))
|
||||||
|
|
||||||
// Multiappend with two messages.
|
// Multiappend with two messages.
|
||||||
tc.transactf("ok", "noop") // Flush pending untagged responses.
|
tc.transactf("ok", "noop") // Flush pending untagged responses.
|
||||||
@ -91,9 +97,9 @@ func TestAppend(t *testing.T) {
|
|||||||
// Cancelled with zero-length message.
|
// Cancelled with zero-length message.
|
||||||
tc.transactf("no", "append inbox {6+}\r\ntest\r\n {0+}\r\n")
|
tc.transactf("no", "append inbox {6+}\r\ntest\r\n {0+}\r\n")
|
||||||
|
|
||||||
tclimit := startArgs(t, false, false, true, true, "limit")
|
tclimit := startArgs(t, uidonly, false, false, true, true, "limit")
|
||||||
defer tclimit.close()
|
defer tclimit.close()
|
||||||
tclimit.client.Login("limit@mox.example", password0)
|
tclimit.login("limit@mox.example", password0)
|
||||||
tclimit.client.Select("inbox")
|
tclimit.client.Select("inbox")
|
||||||
// First message of 1 byte is within limits.
|
// First message of 1 byte is within limits.
|
||||||
tclimit.transactf("ok", "append inbox (\\Seen Label1 $label2) \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
|
tclimit.transactf("ok", "append inbox (\\Seen Label1 $label2) \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
|
||||||
@ -103,7 +109,11 @@ func TestAppend(t *testing.T) {
|
|||||||
tclimit.xcode("OVERQUOTA")
|
tclimit.xcode("OVERQUOTA")
|
||||||
|
|
||||||
// Empty mailbox.
|
// Empty mailbox.
|
||||||
|
if uidonly {
|
||||||
|
tclimit.transactf("ok", `uid store 1 flags (\deleted)`)
|
||||||
|
} else {
|
||||||
tclimit.transactf("ok", `store 1 flags (\deleted)`)
|
tclimit.transactf("ok", `store 1 flags (\deleted)`)
|
||||||
|
}
|
||||||
tclimit.transactf("ok", "expunge")
|
tclimit.transactf("ok", "expunge")
|
||||||
|
|
||||||
// Multiappend with first message within quota, and second message with sync
|
// Multiappend with first message within quota, and second message with sync
|
||||||
|
@ -27,13 +27,13 @@ import (
|
|||||||
|
|
||||||
func TestAuthenticateLogin(t *testing.T) {
|
func TestAuthenticateLogin(t *testing.T) {
|
||||||
// NFD username and PRECIS-cleaned password.
|
// NFD username and PRECIS-cleaned password.
|
||||||
tc := start(t)
|
tc := start(t, false)
|
||||||
tc.client.Login("mo\u0301x@mox.example", password1)
|
tc.client.Login("mo\u0301x@mox.example", password1)
|
||||||
tc.close()
|
tc.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthenticatePlain(t *testing.T) {
|
func TestAuthenticatePlain(t *testing.T) {
|
||||||
tc := start(t)
|
tc := start(t, false)
|
||||||
|
|
||||||
tc.transactf("no", "authenticate bogus ")
|
tc.transactf("no", "authenticate bogus ")
|
||||||
tc.transactf("bad", "authenticate plain not base64...")
|
tc.transactf("bad", "authenticate plain not base64...")
|
||||||
@ -54,20 +54,20 @@ func TestAuthenticatePlain(t *testing.T) {
|
|||||||
tc.transactf("ok", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000"+password0)))
|
tc.transactf("ok", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000"+password0)))
|
||||||
tc.close()
|
tc.close()
|
||||||
|
|
||||||
tc = start(t)
|
tc = start(t, false)
|
||||||
tc.transactf("ok", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("mjl@mox.example\u0000mjl@mox.example\u0000"+password0)))
|
tc.transactf("ok", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("mjl@mox.example\u0000mjl@mox.example\u0000"+password0)))
|
||||||
tc.close()
|
tc.close()
|
||||||
|
|
||||||
// NFD username and PRECIS-cleaned password.
|
// NFD username and PRECIS-cleaned password.
|
||||||
tc = start(t)
|
tc = start(t, false)
|
||||||
tc.transactf("ok", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("mo\u0301x@mox.example\u0000mo\u0301x@mox.example\u0000"+password1)))
|
tc.transactf("ok", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("mo\u0301x@mox.example\u0000mo\u0301x@mox.example\u0000"+password1)))
|
||||||
tc.close()
|
tc.close()
|
||||||
|
|
||||||
tc = start(t)
|
tc = start(t, false)
|
||||||
tc.client.AuthenticatePlain("mjl@mox.example", password0)
|
tc.client.AuthenticatePlain("mjl@mox.example", password0)
|
||||||
tc.close()
|
tc.close()
|
||||||
|
|
||||||
tc = start(t)
|
tc = start(t, false)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc.cmdf("", "authenticate plain")
|
tc.cmdf("", "authenticate plain")
|
||||||
@ -82,7 +82,7 @@ func TestAuthenticatePlain(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLoginDisabled(t *testing.T) {
|
func TestLoginDisabled(t *testing.T) {
|
||||||
tc := start(t)
|
tc := start(t, false)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
acc, err := store.OpenAccount(pkglog, "disabled", false)
|
acc, err := store.OpenAccount(pkglog, "disabled", false)
|
||||||
@ -120,7 +120,7 @@ func TestAuthenticateSCRAMSHA256PLUS(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testAuthenticateSCRAM(t *testing.T, tls bool, method string, h func() hash.Hash) {
|
func testAuthenticateSCRAM(t *testing.T, tls bool, method string, h func() hash.Hash) {
|
||||||
tc := startArgs(t, true, tls, true, true, "mjl")
|
tc := startArgs(t, false, true, tls, true, true, "mjl")
|
||||||
tc.client.AuthenticateSCRAM(method, h, "mjl@mox.example", password0)
|
tc.client.AuthenticateSCRAM(method, h, "mjl@mox.example", password0)
|
||||||
tc.close()
|
tc.close()
|
||||||
|
|
||||||
@ -168,7 +168,7 @@ func testAuthenticateSCRAM(t *testing.T, tls bool, method string, h func() hash.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tc = startArgs(t, true, tls, true, true, "mjl")
|
tc = startArgs(t, false, true, tls, true, true, "mjl")
|
||||||
auth("no", scram.ErrInvalidProof, "mjl@mox.example", "badpass")
|
auth("no", scram.ErrInvalidProof, "mjl@mox.example", "badpass")
|
||||||
auth("no", scram.ErrInvalidProof, "mjl@mox.example", "")
|
auth("no", scram.ErrInvalidProof, "mjl@mox.example", "")
|
||||||
// todo: server aborts due to invalid username. we should probably make client continue with fake determinisitically generated salt and result in error in the end.
|
// todo: server aborts due to invalid username. we should probably make client continue with fake determinisitically generated salt and result in error in the end.
|
||||||
@ -185,7 +185,7 @@ func testAuthenticateSCRAM(t *testing.T, tls bool, method string, h func() hash.
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthenticateCRAMMD5(t *testing.T) {
|
func TestAuthenticateCRAMMD5(t *testing.T) {
|
||||||
tc := start(t)
|
tc := start(t, false)
|
||||||
|
|
||||||
tc.transactf("no", "authenticate bogus ")
|
tc.transactf("no", "authenticate bogus ")
|
||||||
tc.transactf("bad", "authenticate CRAM-MD5 not base64...")
|
tc.transactf("bad", "authenticate CRAM-MD5 not base64...")
|
||||||
@ -234,13 +234,13 @@ func TestAuthenticateCRAMMD5(t *testing.T) {
|
|||||||
tc.close()
|
tc.close()
|
||||||
|
|
||||||
// NFD username, with PRECIS-cleaned password.
|
// NFD username, with PRECIS-cleaned password.
|
||||||
tc = start(t)
|
tc = start(t, false)
|
||||||
auth("ok", "mo\u0301x@mox.example", password1)
|
auth("ok", "mo\u0301x@mox.example", password1)
|
||||||
tc.close()
|
tc.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthenticateTLSClientCert(t *testing.T) {
|
func TestAuthenticateTLSClientCert(t *testing.T) {
|
||||||
tc := startArgsMore(t, true, true, nil, nil, true, true, "mjl", nil)
|
tc := startArgsMore(t, false, true, true, nil, nil, true, true, "mjl", nil)
|
||||||
tc.transactf("no", "authenticate external ") // No TLS auth.
|
tc.transactf("no", "authenticate external ") // No TLS auth.
|
||||||
tc.close()
|
tc.close()
|
||||||
|
|
||||||
@ -263,7 +263,7 @@ func TestAuthenticateTLSClientCert(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// No preauth, explicit authenticate with TLS.
|
// No preauth, explicit authenticate with TLS.
|
||||||
tc = startArgsMore(t, true, true, nil, &clientConfig, false, true, "mjl", addClientCert)
|
tc = startArgsMore(t, false, true, true, nil, &clientConfig, false, true, "mjl", addClientCert)
|
||||||
if tc.client.Preauth {
|
if tc.client.Preauth {
|
||||||
t.Fatalf("preauthentication while not configured for tls public key")
|
t.Fatalf("preauthentication while not configured for tls public key")
|
||||||
}
|
}
|
||||||
@ -271,7 +271,7 @@ func TestAuthenticateTLSClientCert(t *testing.T) {
|
|||||||
tc.close()
|
tc.close()
|
||||||
|
|
||||||
// External with explicit username.
|
// External with explicit username.
|
||||||
tc = startArgsMore(t, true, true, nil, &clientConfig, false, true, "mjl", addClientCert)
|
tc = startArgsMore(t, false, true, true, nil, &clientConfig, false, true, "mjl", addClientCert)
|
||||||
if tc.client.Preauth {
|
if tc.client.Preauth {
|
||||||
t.Fatalf("preauthentication while not configured for tls public key")
|
t.Fatalf("preauthentication while not configured for tls public key")
|
||||||
}
|
}
|
||||||
@ -279,12 +279,12 @@ func TestAuthenticateTLSClientCert(t *testing.T) {
|
|||||||
tc.close()
|
tc.close()
|
||||||
|
|
||||||
// No preauth, also allow other mechanisms.
|
// No preauth, also allow other mechanisms.
|
||||||
tc = startArgsMore(t, true, true, nil, &clientConfig, false, true, "mjl", addClientCert)
|
tc = startArgsMore(t, false, true, true, nil, &clientConfig, false, true, "mjl", addClientCert)
|
||||||
tc.transactf("ok", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000"+password0)))
|
tc.transactf("ok", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000"+password0)))
|
||||||
tc.close()
|
tc.close()
|
||||||
|
|
||||||
// No preauth, also allow other username for same account.
|
// No preauth, also allow other username for same account.
|
||||||
tc = startArgsMore(t, true, true, nil, &clientConfig, false, true, "mjl", addClientCert)
|
tc = startArgsMore(t, false, true, true, nil, &clientConfig, false, true, "mjl", addClientCert)
|
||||||
tc.transactf("ok", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000móx@mox.example\u0000"+password0)))
|
tc.transactf("ok", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000móx@mox.example\u0000"+password0)))
|
||||||
tc.close()
|
tc.close()
|
||||||
|
|
||||||
@ -295,12 +295,12 @@ func TestAuthenticateTLSClientCert(t *testing.T) {
|
|||||||
tcheck(t, err, "set password")
|
tcheck(t, err, "set password")
|
||||||
err = acc.Close()
|
err = acc.Close()
|
||||||
tcheck(t, err, "close account")
|
tcheck(t, err, "close account")
|
||||||
tc = startArgsMore(t, true, true, nil, &clientConfig, false, true, "mjl", addClientCert)
|
tc = startArgsMore(t, false, true, true, nil, &clientConfig, false, true, "mjl", addClientCert)
|
||||||
tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000other@mox.example\u0000test1234")))
|
tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000other@mox.example\u0000test1234")))
|
||||||
tc.close()
|
tc.close()
|
||||||
|
|
||||||
// Starttls and external auth.
|
// Starttls and external auth.
|
||||||
tc = startArgsMore(t, true, false, nil, &clientConfig, false, true, "mjl", addClientCert)
|
tc = startArgsMore(t, false, true, false, nil, &clientConfig, false, true, "mjl", addClientCert)
|
||||||
tc.client.Starttls(&clientConfig)
|
tc.client.Starttls(&clientConfig)
|
||||||
tc.transactf("ok", "authenticate external =")
|
tc.transactf("ok", "authenticate external =")
|
||||||
tc.close()
|
tc.close()
|
||||||
@ -318,7 +318,7 @@ func TestAuthenticateTLSClientCert(t *testing.T) {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
mox.StartTLSSessionTicketKeyRefresher(ctx, pkglog, &serverConfig)
|
mox.StartTLSSessionTicketKeyRefresher(ctx, pkglog, &serverConfig)
|
||||||
clientConfig.ClientSessionCache = tls.NewLRUClientSessionCache(10)
|
clientConfig.ClientSessionCache = tls.NewLRUClientSessionCache(10)
|
||||||
tc = startArgsMore(t, true, true, &serverConfig, &clientConfig, false, true, "mjl", addClientCert)
|
tc = startArgsMore(t, false, true, true, &serverConfig, &clientConfig, false, true, "mjl", addClientCert)
|
||||||
if !tc.client.Preauth {
|
if !tc.client.Preauth {
|
||||||
t.Fatalf("not preauthentication while configured for tls public key")
|
t.Fatalf("not preauthentication while configured for tls public key")
|
||||||
}
|
}
|
||||||
@ -330,7 +330,7 @@ func TestAuthenticateTLSClientCert(t *testing.T) {
|
|||||||
tc.close()
|
tc.close()
|
||||||
|
|
||||||
// Authentication works with TLS resumption.
|
// Authentication works with TLS resumption.
|
||||||
tc = startArgsMore(t, true, true, &serverConfig, &clientConfig, false, true, "mjl", addClientCert)
|
tc = startArgsMore(t, false, true, true, &serverConfig, &clientConfig, false, true, "mjl", addClientCert)
|
||||||
if !tc.client.Preauth {
|
if !tc.client.Preauth {
|
||||||
t.Fatalf("not preauthentication while configured for tls public key")
|
t.Fatalf("not preauthentication while configured for tls public key")
|
||||||
}
|
}
|
||||||
|
@ -10,10 +10,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestCompress(t *testing.T) {
|
func TestCompress(t *testing.T) {
|
||||||
tc := start(t)
|
tc := start(t, false)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
|
|
||||||
tc.transactf("bad", "compress")
|
tc.transactf("bad", "compress")
|
||||||
tc.transactf("bad", "compress bogus ")
|
tc.transactf("bad", "compress bogus ")
|
||||||
@ -30,11 +30,11 @@ func TestCompress(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCompressStartTLS(t *testing.T) {
|
func TestCompressStartTLS(t *testing.T) {
|
||||||
tc := start(t)
|
tc := start(t, false)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc.client.Starttls(&tls.Config{InsecureSkipVerify: true})
|
tc.client.Starttls(&tls.Config{InsecureSkipVerify: true})
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc.client.CompressDeflate()
|
tc.client.CompressDeflate()
|
||||||
tc.client.Select("inbox")
|
tc.client.Select("inbox")
|
||||||
tc.transactf("ok", "append inbox (\\seen) {%d+}\r\n%s", len(exampleMsg), exampleMsg)
|
tc.transactf("ok", "append inbox (\\seen) {%d+}\r\n%s", len(exampleMsg), exampleMsg)
|
||||||
@ -53,7 +53,7 @@ func TestCompressBreak(t *testing.T) {
|
|||||||
// state inconsistent. We must not call into the flate writer again because due to
|
// state inconsistent. We must not call into the flate writer again because due to
|
||||||
// its broken internal state it may cause array out of bounds accesses.
|
// its broken internal state it may cause array out of bounds accesses.
|
||||||
|
|
||||||
tc := start(t)
|
tc := start(t, false)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
msg := exampleMsg
|
msg := exampleMsg
|
||||||
@ -69,7 +69,7 @@ func TestCompressBreak(t *testing.T) {
|
|||||||
text = text[n:]
|
text = text[n:]
|
||||||
}
|
}
|
||||||
|
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc.client.CompressDeflate()
|
tc.client.CompressDeflate()
|
||||||
tc.client.Select("inbox")
|
tc.client.Select("inbox")
|
||||||
tc.transactf("ok", "append inbox (\\seen) {%d+}\r\n%s", len(msg), msg)
|
tc.transactf("ok", "append inbox (\\seen) {%d+}\r\n%s", len(msg), msg)
|
||||||
|
@ -14,16 +14,24 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestCondstore(t *testing.T) {
|
func TestCondstore(t *testing.T) {
|
||||||
testCondstoreQresync(t, false)
|
testCondstoreQresync(t, false, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCondstoreUIDOnly(t *testing.T) {
|
||||||
|
testCondstoreQresync(t, false, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestQresync(t *testing.T) {
|
func TestQresync(t *testing.T) {
|
||||||
testCondstoreQresync(t, true)
|
testCondstoreQresync(t, true, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCondstoreQresync(t *testing.T, qresync bool) {
|
func TestQresyncUIDOnly(t *testing.T) {
|
||||||
|
testCondstoreQresync(t, true, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCondstoreQresync(t *testing.T, qresync, uidonly bool) {
|
||||||
defer mockUIDValidity()()
|
defer mockUIDValidity()()
|
||||||
tc := start(t)
|
tc := start(t, uidonly)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
// todo: check whether marking \seen will cause modseq to be returned in case of qresync.
|
// todo: check whether marking \seen will cause modseq to be returned in case of qresync.
|
||||||
@ -35,7 +43,7 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
|
|||||||
capability = "Qresync"
|
capability = "Qresync"
|
||||||
}
|
}
|
||||||
|
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc.client.Enable(capability)
|
tc.client.Enable(capability)
|
||||||
tc.transactf("ok", "Select inbox")
|
tc.transactf("ok", "Select inbox")
|
||||||
tc.xuntaggedOpt(false, imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(2), More: "x"}})
|
tc.xuntaggedOpt(false, imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(2), More: "x"}})
|
||||||
@ -57,6 +65,7 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
|
|||||||
tc.transactf("ok", "Uid Fetch 1:* (Flags Modseq) (Changedsince 1)")
|
tc.transactf("ok", "Uid Fetch 1:* (Flags Modseq) (Changedsince 1)")
|
||||||
tc.xuntagged()
|
tc.xuntagged()
|
||||||
|
|
||||||
|
if !uidonly {
|
||||||
// Search with modseq search criteria.
|
// Search with modseq search criteria.
|
||||||
tc.transactf("ok", "Search Modseq 0") // Zero is valid, matches all.
|
tc.transactf("ok", "Search Modseq 0") // Zero is valid, matches all.
|
||||||
tc.xsearch()
|
tc.xsearch()
|
||||||
@ -76,6 +85,7 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
|
|||||||
// esearch
|
// esearch
|
||||||
tc.transactf("ok", "Search Return (All) Modseq 123")
|
tc.transactf("ok", "Search Return (All) Modseq 123")
|
||||||
tc.xesearch(imapclient.UntaggedEsearch{})
|
tc.xesearch(imapclient.UntaggedEsearch{})
|
||||||
|
}
|
||||||
|
|
||||||
// Now we add, delete, expunge, modify some message flags and check if the
|
// 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
|
// responses are correct. We check in both a condstore-enabled and one without that
|
||||||
@ -102,15 +112,15 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
|
|||||||
tc.client.Create("otherbox", nil)
|
tc.client.Create("otherbox", nil)
|
||||||
|
|
||||||
// tc2 is a client without condstore, so no modseq responses.
|
// tc2 is a client without condstore, so no modseq responses.
|
||||||
tc2 := startNoSwitchboard(t)
|
tc2 := startNoSwitchboard(t, uidonly)
|
||||||
defer tc2.closeNoWait()
|
defer tc2.closeNoWait()
|
||||||
tc2.client.Login("mjl@mox.example", password0)
|
tc2.login("mjl@mox.example", password0)
|
||||||
tc2.client.Select("inbox")
|
tc2.client.Select("inbox")
|
||||||
|
|
||||||
// tc3 is a client with condstore, so with modseq responses.
|
// tc3 is a client with condstore, so with modseq responses.
|
||||||
tc3 := startNoSwitchboard(t)
|
tc3 := startNoSwitchboard(t, uidonly)
|
||||||
defer tc3.closeNoWait()
|
defer tc3.closeNoWait()
|
||||||
tc3.client.Login("mjl@mox.example", password0)
|
tc3.login("mjl@mox.example", password0)
|
||||||
tc3.client.Enable(capability)
|
tc3.client.Enable(capability)
|
||||||
tc3.client.Select("inbox")
|
tc3.client.Select("inbox")
|
||||||
|
|
||||||
@ -141,24 +151,26 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
|
|||||||
noflags := imapclient.FetchFlags(nil)
|
noflags := imapclient.FetchFlags(nil)
|
||||||
tc2.xuntagged(
|
tc2.xuntagged(
|
||||||
imapclient.UntaggedExists(6),
|
imapclient.UntaggedExists(6),
|
||||||
imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(4), noflags}},
|
tc2.untaggedFetch(4, 4, noflags),
|
||||||
imapclient.UntaggedFetch{Seq: 5, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags}},
|
tc2.untaggedFetch(5, 5, noflags),
|
||||||
imapclient.UntaggedFetch{Seq: 6, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags}},
|
tc2.untaggedFetch(6, 6, noflags),
|
||||||
)
|
)
|
||||||
|
|
||||||
tc3.transactf("ok", "Noop")
|
tc3.transactf("ok", "Noop")
|
||||||
tc3.xuntagged(
|
tc3.xuntagged(
|
||||||
imapclient.UntaggedExists(6),
|
imapclient.UntaggedExists(6),
|
||||||
imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(4), noflags, imapclient.FetchModSeq(clientModseq + 1)}},
|
tc3.untaggedFetch(4, 4, noflags, imapclient.FetchModSeq(clientModseq+1)),
|
||||||
imapclient.UntaggedFetch{Seq: 5, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(clientModseq + 3)}},
|
tc3.untaggedFetch(5, 5, noflags, imapclient.FetchModSeq(clientModseq+3)),
|
||||||
imapclient.UntaggedFetch{Seq: 6, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq + 4)}},
|
tc3.untaggedFetch(6, 6, noflags, imapclient.FetchModSeq(clientModseq+4)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if !uidonly {
|
||||||
mox.SetPedantic(true)
|
mox.SetPedantic(true)
|
||||||
tc.transactf("bad", `Fetch 1 Flags (Changedsince 0)`) // 0 not allowed in syntax.
|
tc.transactf("bad", `Fetch 1 Flags (Changedsince 0)`) // 0 not allowed in syntax.
|
||||||
mox.SetPedantic(false)
|
mox.SetPedantic(false)
|
||||||
|
}
|
||||||
tc.transactf("ok", "Uid fetch 1 (Flags) (Changedsince 0)")
|
tc.transactf("ok", "Uid fetch 1 (Flags) (Changedsince 0)")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(1)}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, noflags, imapclient.FetchModSeq(1)))
|
||||||
|
|
||||||
// Check highestmodseq for mailboxes.
|
// Check highestmodseq for mailboxes.
|
||||||
tc.transactf("ok", "Status inbox (highestmodseq)")
|
tc.transactf("ok", "Status inbox (highestmodseq)")
|
||||||
@ -176,54 +188,67 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
|
|||||||
|
|
||||||
clientModseq += 4
|
clientModseq += 4
|
||||||
|
|
||||||
|
if !uidonly {
|
||||||
// Check fetch modseq response and changedsince.
|
// Check fetch modseq response and changedsince.
|
||||||
tc.transactf("ok", `Fetch 1 (Modseq)`)
|
tc.transactf("ok", `Fetch 1 (Modseq)`)
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchModSeq(1)}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchModSeq(1)))
|
||||||
|
}
|
||||||
|
|
||||||
// Without modseq attribute, even with condseq enabled, there is no modseq response.
|
// 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
|
// 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`)
|
tc.transactf("ok", `Uid Fetch 1 Flags`)
|
||||||
if qresync {
|
if qresync {
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(1)}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, noflags, imapclient.FetchModSeq(1)))
|
||||||
} else {
|
} else {
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, noflags))
|
||||||
}
|
}
|
||||||
tc.transactf("ok", `Fetch 1 Flags`)
|
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags}})
|
|
||||||
|
|
||||||
|
if !uidonly {
|
||||||
|
tc.transactf("ok", `Fetch 1 Flags`)
|
||||||
|
tc.xuntagged(tc.untaggedFetch(1, 1, noflags))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !uidonly {
|
||||||
// When CHANGEDSINCE is present, MODSEQ is automatically added to the response.
|
// When CHANGEDSINCE is present, MODSEQ is automatically added to the response.
|
||||||
// ../rfc/7162:871
|
// ../rfc/7162:871
|
||||||
// ../rfc/7162:877
|
// ../rfc/7162:877
|
||||||
tc.transactf("ok", `Fetch 1 Flags (Changedsince 1)`)
|
tc.transactf("ok", `Fetch 1 Flags (Changedsince 1)`)
|
||||||
tc.xuntagged()
|
tc.xuntagged()
|
||||||
tc.transactf("ok", `Fetch 1,4 Flags (Changedsince 1)`)
|
tc.transactf("ok", `Fetch 1,4 Flags (Changedsince 1)`)
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(4), noflags, imapclient.FetchModSeq(3)}})
|
tc.xuntagged(tc.untaggedFetch(4, 4, noflags, imapclient.FetchModSeq(3)))
|
||||||
tc.transactf("ok", `Fetch 2 Flags (Changedsince 2)`)
|
tc.transactf("ok", `Fetch 2 Flags (Changedsince 2)`)
|
||||||
tc.xuntagged()
|
tc.xuntagged()
|
||||||
|
}
|
||||||
|
|
||||||
// store and uid store.
|
// store and uid store.
|
||||||
|
|
||||||
|
if !uidonly {
|
||||||
// unchangedsince 0 never passes the check. ../rfc/7162:640
|
// unchangedsince 0 never passes the check. ../rfc/7162:640
|
||||||
tc.transactf("ok", `Store 1 (Unchangedsince 0) +Flags ()`)
|
tc.transactf("ok", `Store 1 (Unchangedsince 0) +Flags ()`)
|
||||||
tc.xcodeArg(imapclient.CodeModified(xparseNumSet("1")))
|
tc.xcodeArg(imapclient.CodeModified(xparseNumSet("1")))
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(1)}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, noflags, imapclient.FetchModSeq(1)))
|
||||||
|
}
|
||||||
|
|
||||||
// Modseq is 2 for first condstore-aware-appended message, so also no match.
|
// Modseq is 2 for first condstore-aware-appended message, so also no match.
|
||||||
tc.transactf("ok", `Uid Store 4 (Unchangedsince 1) +Flags ()`)
|
tc.transactf("ok", `Uid Store 4 (Unchangedsince 1) +Flags ()`)
|
||||||
tc.xcodeArg(imapclient.CodeModified(xparseNumSet("4")))
|
tc.xcodeArg(imapclient.CodeModified(xparseNumSet("4")))
|
||||||
|
|
||||||
|
if uidonly {
|
||||||
|
tc.transactf("ok", `Uid Store 1 (Unchangedsince 1) +Flags (label1)`)
|
||||||
|
} else {
|
||||||
// Modseq is 1 for original message.
|
// Modseq is 1 for original message.
|
||||||
tc.transactf("ok", `Store 1 (Unchangedsince 1) +Flags (label1)`)
|
tc.transactf("ok", `Store 1 (Unchangedsince 1) +Flags (label1)`)
|
||||||
|
}
|
||||||
tc.xcode("") // No MODIFIED.
|
tc.xcode("") // No MODIFIED.
|
||||||
clientModseq++
|
clientModseq++
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchFlags{"label1"}, imapclient.FetchModSeq(clientModseq)}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{"label1"}, imapclient.FetchModSeq(clientModseq)))
|
||||||
tc2.transactf("ok", "Noop")
|
tc2.transactf("ok", "Noop")
|
||||||
tc2.xuntagged(
|
tc2.xuntagged(
|
||||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchFlags{"label1"}}},
|
tc2.untaggedFetch(1, 1, imapclient.FetchFlags{"label1"}),
|
||||||
)
|
)
|
||||||
tc3.transactf("ok", "Noop")
|
tc3.transactf("ok", "Noop")
|
||||||
tc3.xuntagged(
|
tc3.xuntagged(
|
||||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchFlags{"label1"}, imapclient.FetchModSeq(clientModseq)}},
|
tc3.untaggedFetch(1, 1, imapclient.FetchFlags{"label1"}, imapclient.FetchModSeq(clientModseq)),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Modify same message twice. Check that second application doesn't fail due to
|
// Modify same message twice. Check that second application doesn't fail due to
|
||||||
@ -232,21 +257,22 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
|
|||||||
clientModseq++
|
clientModseq++
|
||||||
tc.xcode("") // No MODIFIED.
|
tc.xcode("") // No MODIFIED.
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchFlags(nil), imapclient.FetchModSeq(clientModseq)}},
|
tc.untaggedFetch(1, 1, imapclient.FetchFlags(nil), imapclient.FetchModSeq(clientModseq)),
|
||||||
)
|
)
|
||||||
// We do broadcast the changes twice. Not great, but doesn't hurt. This isn't common.
|
// We do broadcast the changes twice. Not great, but doesn't hurt. This isn't common.
|
||||||
tc2.transactf("ok", "Noop")
|
tc2.transactf("ok", "Noop")
|
||||||
tc2.xuntagged(
|
tc2.xuntagged(
|
||||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchFlags(nil)}},
|
tc2.untaggedFetch(1, 1, imapclient.FetchFlags(nil)),
|
||||||
)
|
)
|
||||||
tc3.transactf("ok", "Noop")
|
tc3.transactf("ok", "Noop")
|
||||||
tc3.xuntagged(
|
tc3.xuntagged(
|
||||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchFlags(nil), imapclient.FetchModSeq(clientModseq)}},
|
tc3.untaggedFetch(1, 1, imapclient.FetchFlags(nil), imapclient.FetchModSeq(clientModseq)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if !uidonly {
|
||||||
// Modify without actually changing flags, there will be no new modseq and no broadcast.
|
// 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.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.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags(nil), imapclient.FetchModSeq(clientModseq)))
|
||||||
tc.xcode("") // No MODIFIED.
|
tc.xcode("") // No MODIFIED.
|
||||||
tc2.transactf("ok", "Noop")
|
tc2.transactf("ok", "Noop")
|
||||||
tc2.xuntagged()
|
tc2.xuntagged()
|
||||||
@ -256,10 +282,12 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
|
|||||||
// search with modseq criteria and modseq in response
|
// search with modseq criteria and modseq in response
|
||||||
tc.transactf("ok", "Search Modseq %d", clientModseq)
|
tc.transactf("ok", "Search Modseq %d", clientModseq)
|
||||||
tc.xsearchmodseq(clientModseq, 1)
|
tc.xsearchmodseq(clientModseq, 1)
|
||||||
|
}
|
||||||
|
|
||||||
tc.transactf("ok", "Uid Search Or Modseq %d Modseq %d", clientModseq, clientModseq)
|
tc.transactf("ok", "Uid Search Or Modseq %d Modseq %d", clientModseq, clientModseq)
|
||||||
tc.xsearchmodseq(clientModseq, 1)
|
tc.xsearchmodseq(clientModseq, 1)
|
||||||
|
|
||||||
|
if !uidonly {
|
||||||
// esearch
|
// esearch
|
||||||
tc.transactf("ok", "Search Return (Min Max All) 1:* Modseq %d", clientModseq)
|
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})
|
tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 1, All: esearchall0("1"), ModSeq: clientModseq})
|
||||||
@ -277,20 +305,28 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
|
|||||||
// modseq 0, the fourth was added with condstore-aware append.
|
// modseq 0, the fourth was added with condstore-aware append.
|
||||||
tc.transactf("ok", `Store 3:4 +Flags (\Deleted)`)
|
tc.transactf("ok", `Store 3:4 +Flags (\Deleted)`)
|
||||||
clientModseq++
|
clientModseq++
|
||||||
|
} else {
|
||||||
|
tc.transactf("ok", `Uid Store 3,4 +Flags (\Deleted)`)
|
||||||
|
clientModseq++
|
||||||
|
}
|
||||||
tc2.transactf("ok", "Noop")
|
tc2.transactf("ok", "Noop")
|
||||||
tc3.transactf("ok", "Noop")
|
tc3.transactf("ok", "Noop")
|
||||||
tc.transactf("ok", "Expunge")
|
tc.transactf("ok", "Expunge")
|
||||||
clientModseq++
|
clientModseq++
|
||||||
if qresync {
|
if qresync || uidonly {
|
||||||
tc.xuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("3:4")})
|
tc.xuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("3:4")})
|
||||||
} else {
|
} else {
|
||||||
tc.xuntagged(imapclient.UntaggedExpunge(3), imapclient.UntaggedExpunge(3))
|
tc.xuntagged(imapclient.UntaggedExpunge(3), imapclient.UntaggedExpunge(3))
|
||||||
}
|
}
|
||||||
tc.xcodeArg(imapclient.CodeHighestModSeq(clientModseq))
|
tc.xcodeArg(imapclient.CodeHighestModSeq(clientModseq))
|
||||||
tc2.transactf("ok", "Noop")
|
tc2.transactf("ok", "Noop")
|
||||||
|
if uidonly {
|
||||||
|
tc2.xuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("3:4")})
|
||||||
|
} else {
|
||||||
tc2.xuntagged(imapclient.UntaggedExpunge(3), imapclient.UntaggedExpunge(3))
|
tc2.xuntagged(imapclient.UntaggedExpunge(3), imapclient.UntaggedExpunge(3))
|
||||||
|
}
|
||||||
tc3.transactf("ok", "Noop")
|
tc3.transactf("ok", "Noop")
|
||||||
if qresync {
|
if qresync || uidonly {
|
||||||
tc3.xuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("3:4")})
|
tc3.xuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("3:4")})
|
||||||
} else {
|
} else {
|
||||||
tc3.xuntagged(imapclient.UntaggedExpunge(3), imapclient.UntaggedExpunge(3))
|
tc3.xuntagged(imapclient.UntaggedExpunge(3), imapclient.UntaggedExpunge(3))
|
||||||
@ -307,17 +343,20 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
|
|||||||
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(clientModseq), More: "x"}},
|
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(clientModseq), More: "x"}},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if !uidonly {
|
||||||
tc.transactf("ok", `Fetch 1:* (Modseq)`)
|
tc.transactf("ok", `Fetch 1:* (Modseq)`)
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchModSeq(8)}},
|
tc.untaggedFetch(1, 1, imapclient.FetchModSeq(8)),
|
||||||
imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2), imapclient.FetchModSeq(1)}},
|
tc.untaggedFetch(2, 2, imapclient.FetchModSeq(1)),
|
||||||
imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), imapclient.FetchModSeq(5)}},
|
tc.untaggedFetch(3, 5, imapclient.FetchModSeq(5)),
|
||||||
imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), imapclient.FetchModSeq(6)}},
|
tc.untaggedFetch(4, 6, imapclient.FetchModSeq(6)),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
// Expunged messages, with higher modseq, should not show up.
|
// Expunged messages, with higher modseq, should not show up.
|
||||||
tc.transactf("ok", "Uid Fetch 1:* (flags) (Changedsince 8)")
|
tc.transactf("ok", "Uid Fetch 1:* (flags) (Changedsince 8)")
|
||||||
tc.xuntagged()
|
tc.xuntagged()
|
||||||
|
|
||||||
|
if !uidonly {
|
||||||
// search
|
// search
|
||||||
tc.transactf("ok", "Search Modseq 8")
|
tc.transactf("ok", "Search Modseq 8")
|
||||||
tc.xsearchmodseq(8, 1)
|
tc.xsearchmodseq(8, 1)
|
||||||
@ -329,6 +368,7 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
|
|||||||
tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 1, All: esearchall0("1"), ModSeq: 8})
|
tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 1, All: esearchall0("1"), ModSeq: 8})
|
||||||
tc.transactf("ok", "Search Return (Min Max All) 1:* Modseq 9")
|
tc.transactf("ok", "Search Return (Min Max All) 1:* Modseq 9")
|
||||||
tc.xuntagged(imapclient.UntaggedEsearch{Tag: tc.client.LastTag})
|
tc.xuntagged(imapclient.UntaggedEsearch{Tag: tc.client.LastTag})
|
||||||
|
}
|
||||||
|
|
||||||
// store, cannot modify expunged messages.
|
// store, cannot modify expunged messages.
|
||||||
tc.transactf("ok", `Uid Store 3,4 (Unchangedsince %d) +Flags (label2)`, clientModseq)
|
tc.transactf("ok", `Uid Store 3,4 (Unchangedsince %d) +Flags (label2)`, clientModseq)
|
||||||
@ -350,18 +390,18 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
|
|||||||
checkCondstoreEnabled := func(fn func(xtc *testconn)) {
|
checkCondstoreEnabled := func(fn func(xtc *testconn)) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
xtc := startNoSwitchboard(t)
|
xtc := startNoSwitchboard(t, uidonly)
|
||||||
// We have modified modseq & createseq to 0 above for testing that case. Don't
|
// We have modified modseq & createseq to 0 above for testing that case. Don't
|
||||||
// trigger the consistency checker.
|
// trigger the consistency checker.
|
||||||
defer xtc.closeNoWait()
|
defer xtc.closeNoWait()
|
||||||
xtc.client.Login("mjl@mox.example", password0)
|
xtc.login("mjl@mox.example", password0)
|
||||||
fn(xtc)
|
fn(xtc)
|
||||||
tagcount++
|
tagcount++
|
||||||
label := fmt.Sprintf("l%d", tagcount)
|
label := fmt.Sprintf("l%d", tagcount)
|
||||||
tc.transactf("ok", "Store 4 Flags (%s)", label)
|
tc.transactf("ok", "Uid Store 6 Flags (%s)", label)
|
||||||
clientModseq++
|
clientModseq++
|
||||||
xtc.transactf("ok", "Noop")
|
xtc.transactf("ok", "Noop")
|
||||||
xtc.xuntagged(imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), imapclient.FetchFlags{label}, imapclient.FetchModSeq(clientModseq)}})
|
xtc.xuntagged(xtc.untaggedFetch(4, 6, imapclient.FetchFlags{label}, imapclient.FetchModSeq(clientModseq)))
|
||||||
}
|
}
|
||||||
// SELECT/EXAMINE with CONDSTORE parameter, ../rfc/7162:373
|
// SELECT/EXAMINE with CONDSTORE parameter, ../rfc/7162:373
|
||||||
checkCondstoreEnabled(func(xtc *testconn) {
|
checkCondstoreEnabled(func(xtc *testconn) {
|
||||||
@ -378,25 +418,25 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
|
|||||||
checkCondstoreEnabled(func(xtc *testconn) {
|
checkCondstoreEnabled(func(xtc *testconn) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
xtc.transactf("ok", "Select inbox")
|
xtc.transactf("ok", "Select inbox")
|
||||||
xtc.transactf("ok", "Fetch 4 (Modseq)")
|
xtc.transactf("ok", "Uid Fetch 6 (Modseq)")
|
||||||
})
|
})
|
||||||
// SEARCH with MODSEQ ../rfc/7162:377
|
// SEARCH with MODSEQ ../rfc/7162:377
|
||||||
checkCondstoreEnabled(func(xtc *testconn) {
|
checkCondstoreEnabled(func(xtc *testconn) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
xtc.transactf("ok", "Select inbox")
|
xtc.transactf("ok", "Select inbox")
|
||||||
xtc.transactf("ok", "Search 4 Modseq 1")
|
xtc.transactf("ok", "Uid Search Uid 6 Modseq 1")
|
||||||
})
|
})
|
||||||
// FETCH with CHANGEDSINCE ../rfc/7162:380
|
// FETCH with CHANGEDSINCE ../rfc/7162:380
|
||||||
checkCondstoreEnabled(func(xtc *testconn) {
|
checkCondstoreEnabled(func(xtc *testconn) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
xtc.transactf("ok", "Select inbox")
|
xtc.transactf("ok", "Select inbox")
|
||||||
xtc.transactf("ok", "Fetch 4 (Flags) (Changedsince %d)", clientModseq)
|
xtc.transactf("ok", "Uid Fetch 6 (Flags) (Changedsince %d)", clientModseq)
|
||||||
})
|
})
|
||||||
// STORE with UNCHANGEDSINCE ../rfc/7162:382
|
// STORE with UNCHANGEDSINCE ../rfc/7162:382
|
||||||
checkCondstoreEnabled(func(xtc *testconn) {
|
checkCondstoreEnabled(func(xtc *testconn) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
xtc.transactf("ok", "Select inbox")
|
xtc.transactf("ok", "Select inbox")
|
||||||
xtc.transactf("ok", "Store 4 (Unchangedsince 0) Flags ()")
|
xtc.transactf("ok", "Uid Store 6 (Unchangedsince 0) Flags ()")
|
||||||
})
|
})
|
||||||
// ENABLE CONDSTORE ../rfc/7162:384
|
// ENABLE CONDSTORE ../rfc/7162:384
|
||||||
checkCondstoreEnabled(func(xtc *testconn) {
|
checkCondstoreEnabled(func(xtc *testconn) {
|
||||||
@ -410,11 +450,12 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
|
|||||||
xtc.transactf("ok", "Enable Qresync")
|
xtc.transactf("ok", "Enable Qresync")
|
||||||
xtc.transactf("ok", "Select inbox")
|
xtc.transactf("ok", "Select inbox")
|
||||||
})
|
})
|
||||||
tc.transactf("ok", "Store 4 Flags ()")
|
|
||||||
clientModseq++
|
|
||||||
|
|
||||||
if qresync {
|
if qresync {
|
||||||
testQresync(t, tc, clientModseq)
|
tc.transactf("ok", "Uid Store 6 Flags ()")
|
||||||
|
clientModseq++
|
||||||
|
|
||||||
|
testQresync(t, tc, uidonly, clientModseq)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continue with some tests that further change the data.
|
// Continue with some tests that further change the data.
|
||||||
@ -423,31 +464,31 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
|
|||||||
tc.transactf("ok", "Select otherbox")
|
tc.transactf("ok", "Select otherbox")
|
||||||
tc2.transactf("ok", "Noop")
|
tc2.transactf("ok", "Noop")
|
||||||
tc3.transactf("ok", "Noop")
|
tc3.transactf("ok", "Noop")
|
||||||
tc.transactf("ok", "Copy 1 inbox")
|
tc.transactf("ok", "Uid Copy 1 inbox")
|
||||||
clientModseq++
|
clientModseq++
|
||||||
tc2.transactf("ok", "Noop")
|
tc2.transactf("ok", "Noop")
|
||||||
tc3.transactf("ok", "Noop")
|
tc3.transactf("ok", "Noop")
|
||||||
tc2.xuntagged(
|
tc2.xuntagged(
|
||||||
imapclient.UntaggedExists(5),
|
imapclient.UntaggedExists(5),
|
||||||
imapclient.UntaggedFetch{Seq: 5, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(7), noflags}},
|
tc2.untaggedFetch(5, 7, noflags),
|
||||||
)
|
)
|
||||||
tc3.xuntagged(
|
tc3.xuntagged(
|
||||||
imapclient.UntaggedExists(5),
|
imapclient.UntaggedExists(5),
|
||||||
imapclient.UntaggedFetch{Seq: 5, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(7), noflags, imapclient.FetchModSeq(clientModseq)}},
|
tc3.untaggedFetch(5, 7, noflags, imapclient.FetchModSeq(clientModseq)),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Then we move some messages, and check if we get expunged/vanished in original
|
// Then we move some messages, and check if we get expunged/vanished in original
|
||||||
// and untagged fetch with modseq in destination mailbox.
|
// and untagged fetch with modseq in destination mailbox.
|
||||||
// tc2o is a client without condstore, so no modseq responses.
|
// tc2o is a client without condstore, so no modseq responses.
|
||||||
tc2o := startNoSwitchboard(t)
|
tc2o := startNoSwitchboard(t, uidonly)
|
||||||
defer tc2o.closeNoWait()
|
defer tc2o.closeNoWait()
|
||||||
tc2o.client.Login("mjl@mox.example", password0)
|
tc2o.login("mjl@mox.example", password0)
|
||||||
tc2o.client.Select("otherbox")
|
tc2o.client.Select("otherbox")
|
||||||
|
|
||||||
// tc3o is a client with condstore, so with modseq responses.
|
// tc3o is a client with condstore, so with modseq responses.
|
||||||
tc3o := startNoSwitchboard(t)
|
tc3o := startNoSwitchboard(t, uidonly)
|
||||||
defer tc3o.closeNoWait()
|
defer tc3o.closeNoWait()
|
||||||
tc3o.client.Login("mjl@mox.example", password0)
|
tc3o.login("mjl@mox.example", password0)
|
||||||
tc3o.client.Enable(capability)
|
tc3o.client.Enable(capability)
|
||||||
tc3o.client.Select("otherbox")
|
tc3o.client.Select("otherbox")
|
||||||
|
|
||||||
@ -457,14 +498,21 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
|
|||||||
if qresync {
|
if qresync {
|
||||||
tc.xuntaggedOpt(false, imapclient.UntaggedVanished{UIDs: xparseNumSet("2")})
|
tc.xuntaggedOpt(false, imapclient.UntaggedVanished{UIDs: xparseNumSet("2")})
|
||||||
tc.xcodeArg(imapclient.CodeHighestModSeq(clientModseq))
|
tc.xcodeArg(imapclient.CodeHighestModSeq(clientModseq))
|
||||||
|
} else if uidonly {
|
||||||
|
tc.xuntaggedOpt(false, imapclient.UntaggedVanished{UIDs: xparseNumSet("2")})
|
||||||
|
tc.xcode("")
|
||||||
} else {
|
} else {
|
||||||
tc.xuntaggedOpt(false, imapclient.UntaggedExpunge(2))
|
tc.xuntaggedOpt(false, imapclient.UntaggedExpunge(2))
|
||||||
tc.xcode("")
|
tc.xcode("")
|
||||||
}
|
}
|
||||||
tc2.transactf("ok", "Noop")
|
tc2.transactf("ok", "Noop")
|
||||||
|
if uidonly {
|
||||||
|
tc2.xuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("2")})
|
||||||
|
} else {
|
||||||
tc2.xuntagged(imapclient.UntaggedExpunge(2))
|
tc2.xuntagged(imapclient.UntaggedExpunge(2))
|
||||||
|
}
|
||||||
tc3.transactf("ok", "Noop")
|
tc3.transactf("ok", "Noop")
|
||||||
if qresync {
|
if qresync || uidonly {
|
||||||
tc3.xuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("2")})
|
tc3.xuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("2")})
|
||||||
} else {
|
} else {
|
||||||
tc3.xuntagged(imapclient.UntaggedExpunge(2))
|
tc3.xuntagged(imapclient.UntaggedExpunge(2))
|
||||||
@ -472,12 +520,12 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
|
|||||||
tc2o.transactf("ok", "Noop")
|
tc2o.transactf("ok", "Noop")
|
||||||
tc2o.xuntagged(
|
tc2o.xuntagged(
|
||||||
imapclient.UntaggedExists(2),
|
imapclient.UntaggedExists(2),
|
||||||
imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2), noflags}},
|
tc2o.untaggedFetch(2, 2, noflags),
|
||||||
)
|
)
|
||||||
tc3o.transactf("ok", "Noop")
|
tc3o.transactf("ok", "Noop")
|
||||||
tc3o.xuntagged(
|
tc3o.xuntagged(
|
||||||
imapclient.UntaggedExists(2),
|
imapclient.UntaggedExists(2),
|
||||||
imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2), noflags, imapclient.FetchModSeq(clientModseq)}},
|
tc2o.untaggedFetch(2, 2, noflags, imapclient.FetchModSeq(clientModseq)),
|
||||||
)
|
)
|
||||||
|
|
||||||
tc2o.closeNoWait()
|
tc2o.closeNoWait()
|
||||||
@ -491,12 +539,19 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
|
|||||||
tc.transactf("ok", "Rename inbox oldbox")
|
tc.transactf("ok", "Rename inbox oldbox")
|
||||||
// todo spec: server doesn't respond with untagged responses, find rfc reference that says this is ok.
|
// todo spec: server doesn't respond with untagged responses, find rfc reference that says this is ok.
|
||||||
tc2.transactf("ok", "Noop")
|
tc2.transactf("ok", "Noop")
|
||||||
|
if uidonly {
|
||||||
|
tc2.xuntagged(
|
||||||
|
imapclient.UntaggedList{Separator: '/', Mailbox: "oldbox"},
|
||||||
|
imapclient.UntaggedVanished{UIDs: xparseNumSet("1,5:7")},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
tc2.xuntagged(
|
tc2.xuntagged(
|
||||||
imapclient.UntaggedList{Separator: '/', Mailbox: "oldbox"},
|
imapclient.UntaggedList{Separator: '/', Mailbox: "oldbox"},
|
||||||
imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1),
|
imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
tc3.transactf("ok", "Noop")
|
tc3.transactf("ok", "Noop")
|
||||||
if qresync {
|
if qresync || uidonly {
|
||||||
tc3.xuntagged(
|
tc3.xuntagged(
|
||||||
imapclient.UntaggedList{Separator: '/', Mailbox: "oldbox"},
|
imapclient.UntaggedList{Separator: '/', Mailbox: "oldbox"},
|
||||||
imapclient.UntaggedVanished{UIDs: xparseNumSet("1,5:7")},
|
imapclient.UntaggedVanished{UIDs: xparseNumSet("1,5:7")},
|
||||||
@ -512,7 +567,7 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
|
|||||||
tc.transactf("ok", "Delete otherbox")
|
tc.transactf("ok", "Delete otherbox")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testQresync(t *testing.T, tc *testconn, clientModseq int64) {
|
func testQresync(t *testing.T, tc *testconn, uidonly bool, clientModseq int64) {
|
||||||
// Vanished on non-uid fetch is not allowed. ../rfc/7162:1693
|
// Vanished on non-uid fetch is not allowed. ../rfc/7162:1693
|
||||||
tc.transactf("bad", "fetch 1:* (Flags) (Changedsince 1 Vanished)")
|
tc.transactf("bad", "fetch 1:* (Flags) (Changedsince 1 Vanished)")
|
||||||
|
|
||||||
@ -520,8 +575,8 @@ func testQresync(t *testing.T, tc *testconn, clientModseq int64) {
|
|||||||
tc.transactf("bad", "Uid Fetch 1:* (Flags) (Vanished)")
|
tc.transactf("bad", "Uid Fetch 1:* (Flags) (Vanished)")
|
||||||
|
|
||||||
// Vanished not allowed without first enabling qresync. ../rfc/7162:1697
|
// Vanished not allowed without first enabling qresync. ../rfc/7162:1697
|
||||||
xtc := startNoSwitchboard(t)
|
xtc := startNoSwitchboard(t, uidonly)
|
||||||
xtc.client.Login("mjl@mox.example", password0)
|
xtc.login("mjl@mox.example", password0)
|
||||||
xtc.transactf("ok", "Select inbox (Condstore)")
|
xtc.transactf("ok", "Select inbox (Condstore)")
|
||||||
xtc.transactf("bad", "Uid Fetch 1:* (Flags) (Changedsince 1 Vanished)")
|
xtc.transactf("bad", "Uid Fetch 1:* (Flags) (Changedsince 1 Vanished)")
|
||||||
xtc.closeNoWait()
|
xtc.closeNoWait()
|
||||||
@ -532,17 +587,17 @@ func testQresync(t *testing.T, tc *testconn, clientModseq int64) {
|
|||||||
noflags := imapclient.FetchFlags(nil)
|
noflags := imapclient.FetchFlags(nil)
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
|
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
|
||||||
imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(5)}},
|
tc.untaggedFetch(3, 5, noflags, imapclient.FetchModSeq(5)),
|
||||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(8)}},
|
tc.untaggedFetch(1, 1, noflags, imapclient.FetchModSeq(8)),
|
||||||
imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}},
|
tc.untaggedFetch(4, 6, noflags, imapclient.FetchModSeq(clientModseq)),
|
||||||
)
|
)
|
||||||
|
|
||||||
// select/examine with qresync parameters, including the various optional fields.
|
// select/examine with qresync parameters, including the various optional fields.
|
||||||
tc.transactf("ok", "Close")
|
tc.transactf("ok", "Close")
|
||||||
|
|
||||||
// Must enable qresync explicitly before using. ../rfc/7162:1446
|
// Must enable qresync explicitly before using. ../rfc/7162:1446
|
||||||
xtc = startNoSwitchboard(t)
|
xtc = startNoSwitchboard(t, uidonly)
|
||||||
xtc.client.Login("mjl@mox.example", password0)
|
xtc.login("mjl@mox.example", password0)
|
||||||
xtc.transactf("bad", "Select inbox (Qresync 1 0)")
|
xtc.transactf("bad", "Select inbox (Qresync 1 0)")
|
||||||
// Prevent triggering the consistency checker, we still have modseq/createseq at 0.
|
// Prevent triggering the consistency checker, we still have modseq/createseq at 0.
|
||||||
xtc.closeNoWait()
|
xtc.closeNoWait()
|
||||||
@ -568,11 +623,15 @@ func testQresync(t *testing.T, tc *testconn, clientModseq int64) {
|
|||||||
imapclient.UntaggedList{Separator: '/', Mailbox: "Inbox"},
|
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: "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: "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.UntaggedRecent(0),
|
||||||
imapclient.UntaggedExists(4),
|
imapclient.UntaggedExists(4),
|
||||||
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(clientModseq), More: "x"}},
|
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(clientModseq), More: "x"}},
|
||||||
}
|
}
|
||||||
|
if !uidonly {
|
||||||
|
baseUntagged = append(baseUntagged,
|
||||||
|
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UNSEEN", CodeArg: imapclient.CodeUint{Code: "UNSEEN", Num: 1}, More: "x"}},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
makeUntagged := func(l ...imapclient.Untagged) []imapclient.Untagged {
|
makeUntagged := func(l ...imapclient.Untagged) []imapclient.Untagged {
|
||||||
return slices.Concat(baseUntagged, l)
|
return slices.Concat(baseUntagged, l)
|
||||||
@ -583,9 +642,9 @@ func testQresync(t *testing.T, tc *testconn, clientModseq int64) {
|
|||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
makeUntagged(
|
makeUntagged(
|
||||||
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
|
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
|
||||||
imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(5)}},
|
tc.untaggedFetch(3, 5, noflags, imapclient.FetchModSeq(5)),
|
||||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(8)}},
|
tc.untaggedFetch(1, 1, noflags, imapclient.FetchModSeq(8)),
|
||||||
imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}},
|
tc.untaggedFetch(4, 6, noflags, imapclient.FetchModSeq(clientModseq)),
|
||||||
)...,
|
)...,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -600,9 +659,9 @@ func testQresync(t *testing.T, tc *testconn, clientModseq int64) {
|
|||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
makeUntagged(
|
makeUntagged(
|
||||||
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
|
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
|
||||||
imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(5)}},
|
tc.untaggedFetch(3, 5, noflags, imapclient.FetchModSeq(5)),
|
||||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(8)}},
|
tc.untaggedFetch(1, 1, noflags, imapclient.FetchModSeq(8)),
|
||||||
imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}},
|
tc.untaggedFetch(4, 6, noflags, imapclient.FetchModSeq(clientModseq)),
|
||||||
)...,
|
)...,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -611,9 +670,9 @@ func testQresync(t *testing.T, tc *testconn, clientModseq int64) {
|
|||||||
tc.transactf("ok", "Select inbox (Qresync (1 1 1,2,5:6))")
|
tc.transactf("ok", "Select inbox (Qresync (1 1 1,2,5:6))")
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
makeUntagged(
|
makeUntagged(
|
||||||
imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(5)}},
|
tc.untaggedFetch(3, 5, noflags, imapclient.FetchModSeq(5)),
|
||||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(8)}},
|
tc.untaggedFetch(1, 1, noflags, imapclient.FetchModSeq(8)),
|
||||||
imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}},
|
tc.untaggedFetch(4, 6, noflags, imapclient.FetchModSeq(clientModseq)),
|
||||||
)...,
|
)...,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -622,7 +681,7 @@ func testQresync(t *testing.T, tc *testconn, clientModseq int64) {
|
|||||||
tc.transactf("ok", "Select inbox (Qresync (1 1 5))")
|
tc.transactf("ok", "Select inbox (Qresync (1 1 5))")
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
makeUntagged(
|
makeUntagged(
|
||||||
imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(5)}},
|
tc.untaggedFetch(3, 5, noflags, imapclient.FetchModSeq(5)),
|
||||||
)...,
|
)...,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -646,17 +705,23 @@ func testQresync(t *testing.T, tc *testconn, clientModseq int64) {
|
|||||||
tc.transactf("ok", "Close")
|
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 1,2)))") // Not same length.
|
||||||
tc.transactf("bad", "Select inbox (Qresync (1 1 1:6 (1,2 1)))") // Not same length.
|
tc.transactf("bad", "Select inbox (Qresync (1 1 1:6 (1,2 1)))") // Not same length.
|
||||||
|
if !uidonly {
|
||||||
tc.transactf("no", "Select inbox (Qresync (1 1 1:6 (1,2 1,1)))") // Not ascending.
|
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.
|
tc.transactf("bad", "Select inbox (Qresync (1 1 1:6 (1:* 1:4)))") // Star not allowed.
|
||||||
|
|
||||||
|
if uidonly {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// With valid parameters, based on what a client would know at this stage.
|
// 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.transactf("ok", "Select inbox (Qresync (1 1 1:6 (1,3,6 1,3,6)))")
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
makeUntagged(
|
makeUntagged(
|
||||||
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
|
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
|
||||||
imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(5)}},
|
tc.untaggedFetch(3, 5, noflags, imapclient.FetchModSeq(5)),
|
||||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(8)}},
|
tc.untaggedFetch(1, 1, noflags, imapclient.FetchModSeq(8)),
|
||||||
imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}},
|
tc.untaggedFetch(4, 6, noflags, imapclient.FetchModSeq(clientModseq)),
|
||||||
)...,
|
)...,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -666,8 +731,8 @@ func testQresync(t *testing.T, tc *testconn, clientModseq int64) {
|
|||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
makeUntagged(
|
makeUntagged(
|
||||||
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
|
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
|
||||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(8)}},
|
tc.untaggedFetch(1, 1, noflags, imapclient.FetchModSeq(8)),
|
||||||
imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}},
|
tc.untaggedFetch(4, 6, noflags, imapclient.FetchModSeq(clientModseq)),
|
||||||
)...,
|
)...,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -676,7 +741,7 @@ func testQresync(t *testing.T, tc *testconn, clientModseq int64) {
|
|||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
makeUntagged(
|
makeUntagged(
|
||||||
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
|
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
|
||||||
imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}},
|
tc.untaggedFetch(4, 6, noflags, imapclient.FetchModSeq(clientModseq)),
|
||||||
)...,
|
)...,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -689,7 +754,7 @@ func testQresync(t *testing.T, tc *testconn, clientModseq int64) {
|
|||||||
makeUntagged(
|
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.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.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
|
||||||
imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}},
|
tc.untaggedFetch(4, 6, noflags, imapclient.FetchModSeq(clientModseq)),
|
||||||
)...,
|
)...,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -702,7 +767,128 @@ func testQresync(t *testing.T, tc *testconn, clientModseq int64) {
|
|||||||
makeUntagged(
|
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.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.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
|
||||||
imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}},
|
tc.untaggedFetch(4, 6, noflags, imapclient.FetchModSeq(clientModseq)),
|
||||||
)...,
|
)...,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestQresyncHistory(t *testing.T) {
|
||||||
|
testQresyncHistory(t, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQresyncHistoryUIDOnly(t *testing.T) {
|
||||||
|
testQresyncHistory(t, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testQresyncHistory(t *testing.T, uidonly bool) {
|
||||||
|
defer mockUIDValidity()()
|
||||||
|
tc := start(t, uidonly)
|
||||||
|
defer tc.close()
|
||||||
|
|
||||||
|
tc.login("mjl@mox.example", password0)
|
||||||
|
tc.client.Enable("Qresync")
|
||||||
|
tc.transactf("ok", "Append inbox {1+}\r\nx")
|
||||||
|
tc.transactf("ok", "Append inbox {1+}\r\nx") // modseq 6
|
||||||
|
tc.transactf("ok", "Append inbox {1+}\r\nx")
|
||||||
|
tc.transactf("ok", "Select inbox")
|
||||||
|
tc.client.UIDStoreFlagsAdd("1,3", true, `\Deleted`) // modseq 8
|
||||||
|
tc.client.Expunge() // modseq 9
|
||||||
|
tc.client.UIDStoreFlagsAdd("2", true, `\Seen`) // modseq 10
|
||||||
|
// We have UID 2, no more UID 1 and 3.
|
||||||
|
|
||||||
|
flags := strings.Split(`\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent`, " ")
|
||||||
|
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: 4}, More: "x"}},
|
||||||
|
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UIDVALIDITY", CodeArg: imapclient.CodeUint{Code: "UIDVALIDITY", Num: 1}, More: "x"}},
|
||||||
|
imapclient.UntaggedRecent(0),
|
||||||
|
imapclient.UntaggedExists(1),
|
||||||
|
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(10), More: "x"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
makeUntagged := func(l ...imapclient.Untagged) []imapclient.Untagged {
|
||||||
|
return slices.Concat(baseUntagged, l)
|
||||||
|
}
|
||||||
|
|
||||||
|
tc.transactf("ok", "Close")
|
||||||
|
tc.transactf("ok", "Select inbox (Qresync (1 1))")
|
||||||
|
tc.xuntagged(
|
||||||
|
makeUntagged(
|
||||||
|
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("1,3")},
|
||||||
|
tc.untaggedFetch(1, 2, imapclient.FetchFlags{`\Seen`}, imapclient.FetchModSeq(10)),
|
||||||
|
)...,
|
||||||
|
)
|
||||||
|
|
||||||
|
err := tc.account.DB.Write(ctxbg, func(tx *bstore.Tx) error {
|
||||||
|
syncState := store.SyncState{ID: 1}
|
||||||
|
err := tx.Get(&syncState)
|
||||||
|
tcheck(t, err, "get syncstate")
|
||||||
|
|
||||||
|
syncState.HighestDeletedModSeq = 9
|
||||||
|
err = tx.Update(&syncState)
|
||||||
|
tcheck(t, err, "update syncstate")
|
||||||
|
|
||||||
|
q := bstore.QueryTx[store.Message](tx)
|
||||||
|
q.FilterNonzero(store.Message{Expunged: true})
|
||||||
|
q.FilterLessEqual("ModSeq", syncState.HighestDeletedModSeq)
|
||||||
|
n, err := q.Delete()
|
||||||
|
tcheck(t, err, "delete history")
|
||||||
|
if n != 2 {
|
||||||
|
t.Fatalf("removed %d message history records, expected 2", n)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
tcheck(t, err, "db write")
|
||||||
|
|
||||||
|
// We should still get VANISHED EARLIER for 1,3, even though we don't have history for it.
|
||||||
|
tc.transactf("ok", "Close")
|
||||||
|
tc.transactf("ok", "Select inbox (Qresync (1 1))")
|
||||||
|
tc.xuntagged(
|
||||||
|
makeUntagged(
|
||||||
|
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("1,3")},
|
||||||
|
tc.untaggedFetch(1, 2, imapclient.FetchFlags{`\Seen`}, imapclient.FetchModSeq(10)),
|
||||||
|
)...,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Similar with explicit UIDs.
|
||||||
|
tc.transactf("ok", "Close")
|
||||||
|
tc.transactf("ok", "Select inbox (Qresync (1 1 1:3))")
|
||||||
|
tc.xuntagged(
|
||||||
|
makeUntagged(
|
||||||
|
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("1,3")},
|
||||||
|
tc.untaggedFetch(1, 2, imapclient.FetchFlags{`\Seen`}, imapclient.FetchModSeq(10)),
|
||||||
|
)...,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Fetch with changedsince also returns VANISHED EARLIER when we don't have history anymore.
|
||||||
|
tc.transactf("ok", "uid fetch 1:3 flags (Changedsince 10)")
|
||||||
|
tc.xuntagged() // We still have history, nothing changed.
|
||||||
|
|
||||||
|
tc.transactf("ok", "uid fetch 1:3 flags (Changedsince 9)")
|
||||||
|
tc.xuntagged(tc.untaggedFetch(1, 2, imapclient.FetchFlags{`\Seen`}, imapclient.FetchModSeq(10)))
|
||||||
|
|
||||||
|
// Missing history, but no vanished requested.
|
||||||
|
tc.transactf("ok", "uid fetch 1:4 flags (Changedsince 1)")
|
||||||
|
tc.xuntagged(
|
||||||
|
tc.untaggedFetch(1, 2, imapclient.FetchFlags{`\Seen`}, imapclient.FetchModSeq(10)),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Same, but with vanished requested.
|
||||||
|
tc.transactf("ok", "uid fetch 1:3 flags (Vanished Changedsince 10)")
|
||||||
|
tc.xuntagged() // We still have history, nothing changed.
|
||||||
|
|
||||||
|
tc.transactf("ok", "uid fetch 1:3 flags (Vanished Changedsince 9)")
|
||||||
|
tc.xuntagged(tc.untaggedFetch(1, 2, imapclient.FetchFlags{`\Seen`}, imapclient.FetchModSeq(10)))
|
||||||
|
|
||||||
|
// We return vanished for 1,3. Not for 4, since that is uidnext.
|
||||||
|
tc.transactf("ok", "uid fetch 1:4 flags (Vanished Changedsince 1)")
|
||||||
|
tc.xuntagged(
|
||||||
|
tc.untaggedFetch(1, 2, imapclient.FetchFlags{`\Seen`}, imapclient.FetchModSeq(10)),
|
||||||
|
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("1,3")},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -7,17 +7,25 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestCopy(t *testing.T) {
|
func TestCopy(t *testing.T) {
|
||||||
|
testCopy(t, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCopyUIDOnly(t *testing.T) {
|
||||||
|
testCopy(t, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCopy(t *testing.T, uidonly bool) {
|
||||||
defer mockUIDValidity()()
|
defer mockUIDValidity()()
|
||||||
tc := start(t)
|
tc := start(t, uidonly)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc2 := startNoSwitchboard(t)
|
tc2 := startNoSwitchboard(t, uidonly)
|
||||||
defer tc2.closeNoWait()
|
defer tc2.closeNoWait()
|
||||||
|
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc.client.Select("inbox")
|
tc.client.Select("inbox")
|
||||||
|
|
||||||
tc2.client.Login("mjl@mox.example", password0)
|
tc2.login("mjl@mox.example", password0)
|
||||||
tc2.client.Select("Trash")
|
tc2.client.Select("Trash")
|
||||||
|
|
||||||
tc.transactf("bad", "copy") // Missing params.
|
tc.transactf("bad", "copy") // Missing params.
|
||||||
@ -27,11 +35,14 @@ func TestCopy(t *testing.T) {
|
|||||||
// Seqs 1,2 and UIDs 3,4.
|
// Seqs 1,2 and UIDs 3,4.
|
||||||
tc.client.Append("inbox", makeAppend(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
tc.client.Append("inbox", makeAppend(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
tc.client.StoreFlagsSet("1:2", true, `\Deleted`)
|
tc.transactf("ok", `Uid Store 1:2 +Flags.Silent (\Deleted)`)
|
||||||
tc.client.Expunge()
|
tc.client.Expunge()
|
||||||
tc.client.Append("inbox", makeAppend(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
tc.client.Append("inbox", makeAppend(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
|
|
||||||
|
if uidonly {
|
||||||
|
tc.transactf("ok", "uid copy 3:* Trash")
|
||||||
|
} else {
|
||||||
tc.transactf("no", "copy 1 nonexistent")
|
tc.transactf("no", "copy 1 nonexistent")
|
||||||
tc.xcode("TRYCREATE")
|
tc.xcode("TRYCREATE")
|
||||||
tc.transactf("no", "copy 1 expungebox")
|
tc.transactf("no", "copy 1 expungebox")
|
||||||
@ -42,33 +53,33 @@ func TestCopy(t *testing.T) {
|
|||||||
tc2.transactf("ok", "noop") // Drain.
|
tc2.transactf("ok", "noop") // Drain.
|
||||||
|
|
||||||
tc.transactf("ok", "copy 1:* Trash")
|
tc.transactf("ok", "copy 1:* Trash")
|
||||||
ptr := func(v uint32) *uint32 { return &v }
|
tc.xcodeArg(imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}, To: []imapclient.NumRange{{First: 1, Last: uint32ptr(2)}}})
|
||||||
tc.xcodeArg(imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 3, Last: ptr(4)}}, To: []imapclient.NumRange{{First: 1, Last: ptr(2)}}})
|
}
|
||||||
tc2.transactf("ok", "noop")
|
tc2.transactf("ok", "noop")
|
||||||
tc2.xuntagged(
|
tc2.xuntagged(
|
||||||
imapclient.UntaggedExists(2),
|
imapclient.UntaggedExists(2),
|
||||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchFlags(nil)}},
|
tc2.untaggedFetch(1, 1, imapclient.FetchFlags(nil)),
|
||||||
imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2), imapclient.FetchFlags(nil)}},
|
tc2.untaggedFetch(2, 2, imapclient.FetchFlags(nil)),
|
||||||
)
|
)
|
||||||
|
|
||||||
tc.transactf("no", "uid copy 1,2 Trash") // No match.
|
tc.transactf("no", "uid copy 1,2 Trash") // No match.
|
||||||
tc.transactf("ok", "uid copy 4,3 Trash")
|
tc.transactf("ok", "uid copy 4,3 Trash")
|
||||||
tc.xcodeArg(imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 3, Last: ptr(4)}}, To: []imapclient.NumRange{{First: 3, Last: ptr(4)}}})
|
tc.xcodeArg(imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}, To: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}})
|
||||||
tc2.transactf("ok", "noop")
|
tc2.transactf("ok", "noop")
|
||||||
tc2.xuntagged(
|
tc2.xuntagged(
|
||||||
imapclient.UntaggedExists(4),
|
imapclient.UntaggedExists(4),
|
||||||
imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(3), imapclient.FetchFlags(nil)}},
|
tc2.untaggedFetch(3, 3, imapclient.FetchFlags(nil)),
|
||||||
imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(4), imapclient.FetchFlags(nil)}},
|
tc2.untaggedFetch(4, 4, imapclient.FetchFlags(nil)),
|
||||||
)
|
)
|
||||||
|
|
||||||
tclimit := startArgs(t, false, false, true, true, "limit")
|
tclimit := startArgs(t, uidonly, false, false, true, true, "limit")
|
||||||
defer tclimit.close()
|
defer tclimit.close()
|
||||||
tclimit.client.Login("limit@mox.example", password0)
|
tclimit.login("limit@mox.example", password0)
|
||||||
tclimit.client.Select("inbox")
|
tclimit.client.Select("inbox")
|
||||||
// First message of 1 byte is within limits.
|
// First message of 1 byte is within limits.
|
||||||
tclimit.transactf("ok", "append inbox (\\Seen Label1 $label2) \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
|
tclimit.transactf("ok", "append inbox (\\Seen Label1 $label2) \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
|
||||||
tclimit.xuntagged(imapclient.UntaggedExists(1))
|
tclimit.xuntagged(imapclient.UntaggedExists(1))
|
||||||
// Second message would take account past limit.
|
// Second message would take account past limit.
|
||||||
tclimit.transactf("no", "copy 1:* Trash")
|
tclimit.transactf("no", "uid copy 1:* Trash")
|
||||||
tclimit.xcode("OVERQUOTA")
|
tclimit.xcode("OVERQUOTA")
|
||||||
}
|
}
|
||||||
|
@ -7,14 +7,22 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestCreate(t *testing.T) {
|
func TestCreate(t *testing.T) {
|
||||||
tc := start(t)
|
testCreate(t, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateUIDOnly(t *testing.T) {
|
||||||
|
testCreate(t, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCreate(t *testing.T, uidonly bool) {
|
||||||
|
tc := start(t, uidonly)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc2 := startNoSwitchboard(t)
|
tc2 := startNoSwitchboard(t, uidonly)
|
||||||
defer tc2.closeNoWait()
|
defer tc2.closeNoWait()
|
||||||
|
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc2.client.Login("mjl@mox.example", password0)
|
tc2.login("mjl@mox.example", password0)
|
||||||
|
|
||||||
tc.transactf("no", "create inbox") // Already exists and not allowed. ../rfc/9051:1913
|
tc.transactf("no", "create inbox") // Already exists and not allowed. ../rfc/9051:1913
|
||||||
tc.transactf("no", "create Inbox") // Idem.
|
tc.transactf("no", "create Inbox") // Idem.
|
||||||
|
@ -7,18 +7,26 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestDelete(t *testing.T) {
|
func TestDelete(t *testing.T) {
|
||||||
tc := start(t)
|
testDelete(t, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteUIDOnly(t *testing.T) {
|
||||||
|
testDelete(t, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDelete(t *testing.T, uidonly bool) {
|
||||||
|
tc := start(t, uidonly)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc2 := startNoSwitchboard(t)
|
tc2 := startNoSwitchboard(t, uidonly)
|
||||||
defer tc2.closeNoWait()
|
defer tc2.closeNoWait()
|
||||||
|
|
||||||
tc3 := startNoSwitchboard(t)
|
tc3 := startNoSwitchboard(t, uidonly)
|
||||||
defer tc3.closeNoWait()
|
defer tc3.closeNoWait()
|
||||||
|
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc2.client.Login("mjl@mox.example", password0)
|
tc2.login("mjl@mox.example", password0)
|
||||||
tc3.client.Login("mjl@mox.example", password0)
|
tc3.login("mjl@mox.example", password0)
|
||||||
|
|
||||||
tc.transactf("bad", "delete") // Missing mailbox.
|
tc.transactf("bad", "delete") // Missing mailbox.
|
||||||
tc.transactf("no", "delete inbox") // Cannot delete inbox.
|
tc.transactf("no", "delete inbox") // Cannot delete inbox.
|
||||||
|
@ -57,3 +57,9 @@ func xsyntaxErrorf(format string, args ...any) {
|
|||||||
err := errors.New(errmsg)
|
err := errors.New(errmsg)
|
||||||
panic(syntaxError{"", "", errmsg, err})
|
panic(syntaxError{"", "", errmsg, err})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func xsyntaxCodeErrorf(code, format string, args ...any) {
|
||||||
|
errmsg := fmt.Sprintf(format, args...)
|
||||||
|
err := errors.New(errmsg)
|
||||||
|
panic(syntaxError{"", code, errmsg, err})
|
||||||
|
}
|
||||||
|
@ -7,17 +7,25 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestExpunge(t *testing.T) {
|
func TestExpunge(t *testing.T) {
|
||||||
|
testExpunge(t, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExpungeUIDOnly(t *testing.T) {
|
||||||
|
testExpunge(t, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testExpunge(t *testing.T, uidonly bool) {
|
||||||
defer mockUIDValidity()()
|
defer mockUIDValidity()()
|
||||||
tc := start(t)
|
tc := start(t, uidonly)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc2 := startNoSwitchboard(t)
|
tc2 := startNoSwitchboard(t, uidonly)
|
||||||
defer tc2.closeNoWait()
|
defer tc2.closeNoWait()
|
||||||
|
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc.client.Select("inbox")
|
tc.client.Select("inbox")
|
||||||
|
|
||||||
tc2.client.Login("mjl@mox.example", password0)
|
tc2.login("mjl@mox.example", password0)
|
||||||
tc2.client.Select("inbox")
|
tc2.client.Select("inbox")
|
||||||
|
|
||||||
tc.transactf("bad", "expunge leftover") // Leftover data.
|
tc.transactf("bad", "expunge leftover") // Leftover data.
|
||||||
@ -37,15 +45,23 @@ func TestExpunge(t *testing.T) {
|
|||||||
tc.transactf("ok", "expunge") // Still nothing to remove.
|
tc.transactf("ok", "expunge") // Still nothing to remove.
|
||||||
tc.xuntagged()
|
tc.xuntagged()
|
||||||
|
|
||||||
tc.client.StoreFlagsAdd("1,3", true, `\Deleted`)
|
tc.transactf("ok", `uid store 1,3 +flags.silent \Deleted`)
|
||||||
|
|
||||||
tc2.transactf("ok", "noop") // Drain.
|
tc2.transactf("ok", "noop") // Drain.
|
||||||
|
|
||||||
tc.transactf("ok", "expunge")
|
tc.transactf("ok", "expunge")
|
||||||
|
if uidonly {
|
||||||
|
tc.xuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("1,3")})
|
||||||
|
} else {
|
||||||
tc.xuntagged(imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(2))
|
tc.xuntagged(imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(2))
|
||||||
|
}
|
||||||
|
|
||||||
tc2.transactf("ok", "noop") // Drain.
|
tc2.transactf("ok", "noop") // Drain.
|
||||||
|
if uidonly {
|
||||||
|
tc2.xuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("1,3")})
|
||||||
|
} else {
|
||||||
tc2.xuntagged(imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(2))
|
tc2.xuntagged(imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(2))
|
||||||
|
}
|
||||||
|
|
||||||
tc.transactf("ok", "expunge") // Nothing to remove anymore.
|
tc.transactf("ok", "expunge") // Nothing to remove anymore.
|
||||||
tc.xuntagged()
|
tc.xuntagged()
|
||||||
@ -59,7 +75,7 @@ func TestExpunge(t *testing.T) {
|
|||||||
tc.transactf("bad", "uid expunge 1 leftover") // Leftover data.
|
tc.transactf("bad", "uid expunge 1 leftover") // Leftover data.
|
||||||
tc.transactf("bad", "uid expunge 1 leftover") // Leftover data.
|
tc.transactf("bad", "uid expunge 1 leftover") // Leftover data.
|
||||||
|
|
||||||
tc.client.StoreFlagsAdd("1,2,4", true, `\Deleted`) // Marks UID 2,4,6 as deleted.
|
tc.transactf("ok", `uid store 2,4,6 +flags.silent \Deleted`)
|
||||||
|
|
||||||
tc.transactf("ok", "uid expunge 1")
|
tc.transactf("ok", "uid expunge 1")
|
||||||
tc.xuntagged() // No match.
|
tc.xuntagged() // No match.
|
||||||
@ -67,8 +83,16 @@ func TestExpunge(t *testing.T) {
|
|||||||
tc2.transactf("ok", "noop") // Drain.
|
tc2.transactf("ok", "noop") // Drain.
|
||||||
|
|
||||||
tc.transactf("ok", "uid expunge 4:6") // Removes UID 4,6 at seqs 2,4.
|
tc.transactf("ok", "uid expunge 4:6") // Removes UID 4,6 at seqs 2,4.
|
||||||
tc.xuntagged(imapclient.UntaggedExpunge(2), imapclient.UntaggedExpunge(3))
|
if uidonly {
|
||||||
|
tc.xuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("4,6")})
|
||||||
tc2.transactf("ok", "noop")
|
} else {
|
||||||
tc.xuntagged(imapclient.UntaggedExpunge(2), imapclient.UntaggedExpunge(3))
|
tc.xuntagged(imapclient.UntaggedExpunge(2), imapclient.UntaggedExpunge(3))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tc2.transactf("ok", "noop")
|
||||||
|
if uidonly {
|
||||||
|
tc2.xuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("4,6")})
|
||||||
|
} else {
|
||||||
|
tc2.xuntagged(imapclient.UntaggedExpunge(2), imapclient.UntaggedExpunge(3))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -167,8 +167,9 @@ func (c *conn) cmdxFetch(isUID bool, tag, cmdstr string, p *parser) {
|
|||||||
c.xmailboxID(cmd.rtx, c.mailboxID)
|
c.xmailboxID(cmd.rtx, c.mailboxID)
|
||||||
|
|
||||||
// With changedSince, the client is likely asking for a small set of changes. Use a
|
// With changedSince, the client is likely asking for a small set of changes. Use a
|
||||||
// database query to trim down the uids we need to look at.
|
// database query to trim down the uids we need to look at. We need to go through
|
||||||
// ../rfc/7162:871
|
// the database for "VANISHED (EARLIER)" anyway, to see UIDs that aren't in the
|
||||||
|
// session anymore. Vanished must be used with changedSince. ../rfc/7162:871
|
||||||
if changedSince > 0 {
|
if changedSince > 0 {
|
||||||
q := bstore.QueryTx[store.Message](cmd.rtx)
|
q := bstore.QueryTx[store.Message](cmd.rtx)
|
||||||
q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
|
q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
|
||||||
@ -177,12 +178,17 @@ func (c *conn) cmdxFetch(isUID bool, tag, cmdstr string, p *parser) {
|
|||||||
q.FilterEqual("Expunged", false)
|
q.FilterEqual("Expunged", false)
|
||||||
}
|
}
|
||||||
err := q.ForEach(func(m store.Message) error {
|
err := q.ForEach(func(m store.Message) error {
|
||||||
|
if m.UID >= c.uidnext {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if isUID {
|
||||||
|
if nums.xcontainsKnownUID(m.UID, c.searchResult, func() store.UID { return c.uidnext - 1 }) {
|
||||||
if m.Expunged {
|
if m.Expunged {
|
||||||
vanishedUIDs = append(vanishedUIDs, m.UID)
|
vanishedUIDs = append(vanishedUIDs, m.UID)
|
||||||
} else if isUID {
|
} else {
|
||||||
if nums.containsUID(m.UID, c.uids, c.searchResult) {
|
|
||||||
uids = append(uids, m.UID)
|
uids = append(uids, m.UID)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
seq := c.sequence(m.UID)
|
seq := c.sequence(m.UID)
|
||||||
if seq > 0 && nums.containsSeq(seq, c.uids, c.searchResult) {
|
if seq > 0 && nums.containsSeq(seq, c.uids, c.searchResult) {
|
||||||
@ -192,31 +198,28 @@ func (c *conn) cmdxFetch(isUID bool, tag, cmdstr string, p *parser) {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
xcheckf(err, "looking up messages with changedsince")
|
xcheckf(err, "looking up messages with changedsince")
|
||||||
} else {
|
|
||||||
uids = c.xnumSetUIDs(isUID, nums)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send vanished for all missing requested UIDs. ../rfc/7162:1718
|
|
||||||
if !vanished {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// In case of vanished where we don't have the full history, we must send VANISHED
|
||||||
|
// for all uids matching nums. ../rfc/7162:1718
|
||||||
delModSeq, err := c.account.HighestDeletedModSeq(cmd.rtx)
|
delModSeq, err := c.account.HighestDeletedModSeq(cmd.rtx)
|
||||||
xcheckf(err, "looking up highest deleted modseq")
|
xcheckf(err, "looking up highest deleted modseq")
|
||||||
if changedSince >= delModSeq.Client() {
|
if !vanished || changedSince >= delModSeq.Client() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// First sort the uids we already found, for fast lookup.
|
// We'll iterate through all UIDs in the numset, and add anything that isn't
|
||||||
|
// already in uids and vanishedUIDs. First sort the uids we already found, for fast
|
||||||
|
// lookup. We'll gather new UIDs in more, so we don't break the binary search.
|
||||||
slices.Sort(vanishedUIDs)
|
slices.Sort(vanishedUIDs)
|
||||||
|
slices.Sort(uids)
|
||||||
|
|
||||||
// We'll be gathering any more vanished uids in more.
|
more := map[store.UID]struct{}{} // We'll add them at the end.
|
||||||
more := map[store.UID]struct{}{}
|
|
||||||
checkVanished := func(uid store.UID) {
|
checkVanished := func(uid store.UID) {
|
||||||
if uidSearch(c.uids, uid) <= 0 && uidSearch(vanishedUIDs, uid) <= 0 {
|
if uid < c.uidnext && uidSearch(uids, uid) <= 0 && uidSearch(vanishedUIDs, uid) <= 0 {
|
||||||
more[uid] = struct{}{}
|
more[uid] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now look through the requested uids. We may have a searchResult, handle it
|
// Now look through the requested uids. We may have a searchResult, handle it
|
||||||
// separately from a numset with potential stars, over which we can more easily
|
// separately from a numset with potential stars, over which we can more easily
|
||||||
// iterate.
|
// iterate.
|
||||||
@ -225,7 +228,8 @@ func (c *conn) cmdxFetch(isUID bool, tag, cmdstr string, p *parser) {
|
|||||||
checkVanished(uid)
|
checkVanished(uid)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
iter := nums.interpretStar(c.uids).newIter()
|
xlastUID := c.newCachedLastUID(cmd.rtx, c.mailboxID, func(xerr error) { xuserErrorf("%s", xerr) })
|
||||||
|
iter := nums.xinterpretStar(xlastUID).newIter()
|
||||||
for {
|
for {
|
||||||
num, ok := iter.Next()
|
num, ok := iter.Next()
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -235,6 +239,11 @@ func (c *conn) cmdxFetch(isUID bool, tag, cmdstr string, p *parser) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
vanishedUIDs = slices.AppendSeq(vanishedUIDs, maps.Keys(more))
|
vanishedUIDs = slices.AppendSeq(vanishedUIDs, maps.Keys(more))
|
||||||
|
slices.Sort(vanishedUIDs)
|
||||||
|
} else {
|
||||||
|
uids = c.xnumSetEval(cmd.rtx, isUID, nums)
|
||||||
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
// We are continuing without a lock, working off our snapshot of uids to process.
|
// We are continuing without a lock, working off our snapshot of uids to process.
|
||||||
|
|
||||||
@ -242,7 +251,6 @@ func (c *conn) cmdxFetch(isUID bool, tag, cmdstr string, p *parser) {
|
|||||||
if len(vanishedUIDs) > 0 {
|
if len(vanishedUIDs) > 0 {
|
||||||
// Mention all vanished UIDs in compact numset form.
|
// Mention all vanished UIDs in compact numset form.
|
||||||
// ../rfc/7162:1985
|
// ../rfc/7162:1985
|
||||||
slices.Sort(vanishedUIDs)
|
|
||||||
// No hard limit on response sizes, but clients are recommended to not send more
|
// No hard limit on response sizes, but clients are recommended to not send more
|
||||||
// than 8k. We send a more conservative max 4k.
|
// than 8k. We send a more conservative max 4k.
|
||||||
for _, s := range compactUIDSet(vanishedUIDs).Strings(4*1024 - 32) {
|
for _, s := range compactUIDSet(vanishedUIDs).Strings(4*1024 - 32) {
|
||||||
@ -260,7 +268,12 @@ func (c *conn) cmdxFetch(isUID bool, tag, cmdstr string, p *parser) {
|
|||||||
xuserErrorf("processing fetch attribute: %v", err)
|
xuserErrorf("processing fetch attribute: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UIDFETCH in case of uidonly. ../rfc/9586:181
|
||||||
|
if c.uidonly {
|
||||||
|
fmt.Fprintf(cmd.conn.xbw, "* %d UIDFETCH ", cmd.uid)
|
||||||
|
} else {
|
||||||
fmt.Fprintf(cmd.conn.xbw, "* %d FETCH ", cmd.conn.xsequence(cmd.uid))
|
fmt.Fprintf(cmd.conn.xbw, "* %d FETCH ", cmd.conn.xsequence(cmd.uid))
|
||||||
|
}
|
||||||
data.xwriteTo(cmd.conn, cmd.conn.xbw)
|
data.xwriteTo(cmd.conn, cmd.conn.xbw)
|
||||||
cmd.conn.xbw.Write([]byte("\r\n"))
|
cmd.conn.xbw.Write([]byte("\r\n"))
|
||||||
|
|
||||||
@ -426,7 +439,10 @@ func (cmd *fetchCmd) process(atts []fetchAtt) (rdata listspace, rerr error) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
data := listspace{bare("UID"), number(cmd.uid)}
|
var data listspace
|
||||||
|
if !cmd.conn.uidonly {
|
||||||
|
data = append(data, bare("UID"), number(cmd.uid))
|
||||||
|
}
|
||||||
|
|
||||||
cmd.markSeen = false
|
cmd.markSeen = false
|
||||||
cmd.needFlags = false
|
cmd.needFlags = false
|
||||||
@ -474,8 +490,11 @@ func (cmd *fetchCmd) process(atts []fetchAtt) (rdata listspace, rerr error) {
|
|||||||
func (cmd *fetchCmd) xprocessAtt(a fetchAtt) []token {
|
func (cmd *fetchCmd) xprocessAtt(a fetchAtt) []token {
|
||||||
switch a.field {
|
switch a.field {
|
||||||
case "UID":
|
case "UID":
|
||||||
// Always present.
|
// Present by default without uidonly. For uidonly, we only add it when explicitly
|
||||||
return nil
|
// requested. ../rfc/9586:184
|
||||||
|
if cmd.conn.uidonly {
|
||||||
|
return []token{bare("UID"), number(cmd.uid)}
|
||||||
|
}
|
||||||
|
|
||||||
case "ENVELOPE":
|
case "ENVELOPE":
|
||||||
_, part := cmd.xensureParsed()
|
_, part := cmd.xensureParsed()
|
||||||
|
@ -12,10 +12,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestFetch(t *testing.T) {
|
func TestFetch(t *testing.T) {
|
||||||
tc := start(t)
|
testFetch(t, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchUIDOnly(t *testing.T) {
|
||||||
|
testFetch(t, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testFetch(t *testing.T, uidonly bool) {
|
||||||
|
tc := start(t, uidonly)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc.client.Enable("imap4rev2")
|
tc.client.Enable("imap4rev2")
|
||||||
received, err := time.Parse(time.RFC3339, "2022-11-16T10:01:00+01:00")
|
received, err := time.Parse(time.RFC3339, "2022-11-16T10:01:00+01:00")
|
||||||
tc.check(err, "parse time")
|
tc.check(err, "parse time")
|
||||||
@ -82,134 +90,133 @@ func TestFetch(t *testing.T) {
|
|||||||
|
|
||||||
flagsSeen := imapclient.FetchFlags{`\Seen`}
|
flagsSeen := imapclient.FetchFlags{`\Seen`}
|
||||||
|
|
||||||
|
if !uidonly {
|
||||||
tc.transactf("ok", "fetch 1 all")
|
tc.transactf("ok", "fetch 1 all")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, date1, rfcsize1, env1, noflags}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, date1, rfcsize1, env1, noflags))
|
||||||
|
|
||||||
tc.transactf("ok", "fetch 1 fast")
|
tc.transactf("ok", "fetch 1 fast")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, date1, rfcsize1, noflags}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, date1, rfcsize1, noflags))
|
||||||
|
|
||||||
tc.transactf("ok", "fetch 1 full")
|
tc.transactf("ok", "fetch 1 full")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, date1, rfcsize1, env1, bodyxstructure1, noflags}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, date1, rfcsize1, env1, bodyxstructure1, noflags))
|
||||||
|
|
||||||
tc.transactf("ok", "fetch 1 flags")
|
tc.transactf("ok", "fetch 1 flags")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, noflags}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, noflags))
|
||||||
|
|
||||||
tc.transactf("ok", "fetch 1 bodystructure")
|
tc.transactf("ok", "fetch 1 bodystructure")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodystructure1}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, bodystructure1))
|
||||||
|
|
||||||
// Should be returned unmodified, because there is no content-transfer-encoding.
|
// Should be returned unmodified, because there is no content-transfer-encoding.
|
||||||
tc.transactf("ok", "fetch 1 binary[]")
|
tc.transactf("ok", "fetch 1 binary[]")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binary1, noflags}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, binary1, noflags))
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||||
|
|
||||||
tc.transactf("ok", "fetch 1 binary[1]")
|
tc.transactf("ok", "fetch 1 binary[1]")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binarypart1}}) // Seen flag not changed.
|
tc.xuntagged(tc.untaggedFetch(1, 1, binarypart1)) // Seen flag not changed.
|
||||||
|
|
||||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||||
tc.transactf("ok", "uid fetch 1 binary[]<1.1>")
|
tc.transactf("ok", "uid fetch 1 binary[]<1.1>")
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binarypartial1, noflags}},
|
tc.untaggedFetch(1, 1, binarypartial1, noflags),
|
||||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}}, // For UID FETCH, we get the flags during the command.
|
tc.untaggedFetch(1, 1, flagsSeen), // For UID FETCH, we get the flags during the command.
|
||||||
)
|
)
|
||||||
|
|
||||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||||
tc.transactf("ok", "fetch 1 binary[1]<1.1>")
|
tc.transactf("ok", "fetch 1 binary[1]<1.1>")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binarypartpartial1, noflags}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, binarypartpartial1, noflags))
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||||
|
|
||||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||||
tc.transactf("ok", "fetch 1 binary[]<10000.10001>")
|
tc.transactf("ok", "fetch 1 binary[]<10000.10001>")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binaryend1, noflags}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, binaryend1, noflags))
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||||
|
|
||||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||||
tc.transactf("ok", "fetch 1 binary[1]<10000.10001>")
|
tc.transactf("ok", "fetch 1 binary[1]<10000.10001>")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binarypartend1, noflags}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, binarypartend1, noflags))
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||||
|
|
||||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
|
||||||
tc.transactf("ok", "fetch 1 binary.size[]")
|
tc.transactf("ok", "fetch 1 binary.size[]")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binarysize1}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, binarysize1))
|
||||||
|
|
||||||
tc.transactf("ok", "fetch 1 binary.size[1]")
|
tc.transactf("ok", "fetch 1 binary.size[1]")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binarysizepart1}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, binarysizepart1))
|
||||||
|
|
||||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||||
tc.transactf("ok", "fetch 1 body[]")
|
tc.transactf("ok", "fetch 1 body[]")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, body1, noflags}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, body1, noflags))
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||||
tc.transactf("ok", "fetch 1 body[]<1.2>")
|
tc.transactf("ok", "fetch 1 body[]<1.2>")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodyoff1}}) // Already seen.
|
tc.xuntagged(tc.untaggedFetch(1, 1, bodyoff1)) // Already seen.
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.xuntagged() // Already seen.
|
tc.xuntagged() // Already seen.
|
||||||
|
|
||||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||||
tc.transactf("ok", "fetch 1 body[1]")
|
tc.transactf("ok", "fetch 1 body[1]")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodypart1, noflags}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, bodypart1, noflags))
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||||
|
|
||||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||||
tc.transactf("ok", "fetch 1 body[1]<1.2>")
|
tc.transactf("ok", "fetch 1 body[1]<1.2>")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, body1off1, noflags}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, body1off1, noflags))
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||||
|
|
||||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||||
tc.transactf("ok", "fetch 1 body[1]<100000.100000>")
|
tc.transactf("ok", "fetch 1 body[1]<100000.100000>")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodyend1, noflags}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, bodyend1, noflags))
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||||
|
|
||||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||||
tc.transactf("ok", "fetch 1 body[header]")
|
tc.transactf("ok", "fetch 1 body[header]")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodyheader1, noflags}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, bodyheader1, noflags))
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||||
|
|
||||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||||
tc.transactf("ok", "fetch 1 body[text]")
|
tc.transactf("ok", "fetch 1 body[text]")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodytext1, noflags}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, bodytext1, noflags))
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||||
|
|
||||||
// equivalent to body.peek[header], ../rfc/3501:3183
|
// equivalent to body.peek[header], ../rfc/3501:3183
|
||||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
|
||||||
tc.transactf("ok", "fetch 1 rfc822.header")
|
tc.transactf("ok", "fetch 1 rfc822.header")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, rfcheader1}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, rfcheader1))
|
||||||
|
|
||||||
// equivalent to body[text], ../rfc/3501:3199
|
// equivalent to body[text], ../rfc/3501:3199
|
||||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||||
tc.transactf("ok", "fetch 1 rfc822.text")
|
tc.transactf("ok", "fetch 1 rfc822.text")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, rfctext1, noflags}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, rfctext1, noflags))
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||||
|
|
||||||
// equivalent to body[], ../rfc/3501:3179
|
// equivalent to body[], ../rfc/3501:3179
|
||||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||||
tc.transactf("ok", "fetch 1 rfc822")
|
tc.transactf("ok", "fetch 1 rfc822")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, rfc1, noflags}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, rfc1, noflags))
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||||
|
|
||||||
// With PEEK, we should not get the \Seen flag.
|
// With PEEK, we should not get the \Seen flag.
|
||||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||||
tc.transactf("ok", "fetch 1 body.peek[]")
|
tc.transactf("ok", "fetch 1 body.peek[]")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, body1}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, body1))
|
||||||
|
|
||||||
tc.transactf("ok", "fetch 1 binary.peek[]")
|
tc.transactf("ok", "fetch 1 binary.peek[]")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binary1}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, binary1))
|
||||||
|
|
||||||
// HEADER.FIELDS and .NOT
|
// HEADER.FIELDS and .NOT
|
||||||
tc.transactf("ok", "fetch 1 body.peek[header.fields (date)]")
|
tc.transactf("ok", "fetch 1 body.peek[header.fields (date)]")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, dateheader1}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, dateheader1))
|
||||||
tc.transactf("ok", "fetch 1 body.peek[header.fields.not (date)]")
|
tc.transactf("ok", "fetch 1 body.peek[header.fields.not (date)]")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, nodateheader1}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, nodateheader1))
|
||||||
// For non-multipart messages, 1 means the whole message, but since it's not of
|
// For non-multipart messages, 1 means the whole message, but since it's not of
|
||||||
// type message/{rfc822,global} (a message), you can't get the message headers.
|
// type message/{rfc822,global} (a message), you can't get the message headers.
|
||||||
// ../rfc/9051:4481
|
// ../rfc/9051:4481
|
||||||
@ -217,21 +224,25 @@ func TestFetch(t *testing.T) {
|
|||||||
|
|
||||||
// MIME, part 1 for non-multipart messages is the message itself. ../rfc/9051:4481
|
// MIME, part 1 for non-multipart messages is the message itself. ../rfc/9051:4481
|
||||||
tc.transactf("ok", "fetch 1 body.peek[1.mime]")
|
tc.transactf("ok", "fetch 1 body.peek[1.mime]")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, mime1}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, mime1))
|
||||||
|
|
||||||
// Missing sequence number. ../rfc/9051:7018
|
// Missing sequence number. ../rfc/9051:7018
|
||||||
tc.transactf("bad", "fetch 2 body[]")
|
tc.transactf("bad", "fetch 2 body[]")
|
||||||
|
|
||||||
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||||
tc.transactf("ok", "fetch 1:1 body[]")
|
tc.transactf("ok", "fetch 1:1 body[]")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, body1, noflags}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, body1, noflags))
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||||
|
} else {
|
||||||
|
tc.client.UIDStoreFlagsAdd("1", true, `\Seen`)
|
||||||
|
tc.transactf("ok", "noop")
|
||||||
|
}
|
||||||
|
|
||||||
// UID fetch
|
// UID fetch
|
||||||
tc.transactf("ok", "uid fetch 1 body[]")
|
tc.transactf("ok", "uid fetch 1 body[]")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, body1}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, body1))
|
||||||
|
|
||||||
// UID fetch
|
|
||||||
tc.transactf("ok", "uid fetch 2 body[]")
|
tc.transactf("ok", "uid fetch 2 body[]")
|
||||||
tc.xuntagged()
|
tc.xuntagged()
|
||||||
|
|
||||||
@ -254,9 +265,9 @@ func TestFetch(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
tc.check(err, "get savedate")
|
tc.check(err, "get savedate")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, imapclient.FetchSaveDate{SaveDate: &saveDate}}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchSaveDate{SaveDate: &saveDate}))
|
||||||
|
|
||||||
// Test some invalid syntax.
|
// Test some invalid syntax. Also invalid for uidonly.
|
||||||
tc.transactf("bad", "fetch")
|
tc.transactf("bad", "fetch")
|
||||||
tc.transactf("bad", "fetch ")
|
tc.transactf("bad", "fetch ")
|
||||||
tc.transactf("bad", "fetch ")
|
tc.transactf("bad", "fetch ")
|
||||||
@ -279,11 +290,12 @@ func TestFetch(t *testing.T) {
|
|||||||
tc.transactf("bad", "fetch 1 body[header.fields.not ()]") // List must be non-empty.
|
tc.transactf("bad", "fetch 1 body[header.fields.not ()]") // List must be non-empty.
|
||||||
tc.transactf("bad", "fetch 1 body[mime]") // MIME must be prefixed with a number. ../rfc/9051:4497
|
tc.transactf("bad", "fetch 1 body[mime]") // MIME must be prefixed with a number. ../rfc/9051:4497
|
||||||
|
|
||||||
|
if !uidonly {
|
||||||
tc.transactf("no", "fetch 1 body[2]") // No such part.
|
tc.transactf("no", "fetch 1 body[2]") // No such part.
|
||||||
|
}
|
||||||
|
|
||||||
// Add more complex message.
|
// Add more complex message.
|
||||||
|
|
||||||
uid2 := imapclient.FetchUID(2)
|
|
||||||
bodystructure2 := imapclient.FetchBodystructure{
|
bodystructure2 := imapclient.FetchBodystructure{
|
||||||
RespAttr: "BODYSTRUCTURE",
|
RespAttr: "BODYSTRUCTURE",
|
||||||
Body: imapclient.BodyTypeMpart{
|
Body: imapclient.BodyTypeMpart{
|
||||||
@ -325,41 +337,42 @@ func TestFetch(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
tc.client.Append("inbox", makeAppendTime(nestedMessage, received))
|
tc.client.Append("inbox", makeAppendTime(nestedMessage, received))
|
||||||
tc.transactf("ok", "fetch 2 bodystructure")
|
tc.transactf("ok", "uid fetch 2 bodystructure")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
|
tc.xuntagged(tc.untaggedFetch(2, 2, bodystructure2))
|
||||||
|
|
||||||
// Multiple responses.
|
// Multiple responses.
|
||||||
|
if !uidonly {
|
||||||
tc.transactf("ok", "fetch 1:2 bodystructure")
|
tc.transactf("ok", "fetch 1:2 bodystructure")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodystructure1}}, imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, bodystructure1), tc.untaggedFetch(2, 2, bodystructure2))
|
||||||
tc.transactf("ok", "fetch 1,2 bodystructure")
|
tc.transactf("ok", "fetch 1,2 bodystructure")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodystructure1}}, imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, bodystructure1), tc.untaggedFetch(2, 2, bodystructure2))
|
||||||
tc.transactf("ok", "fetch 2:1 bodystructure")
|
tc.transactf("ok", "fetch 2:1 bodystructure")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodystructure1}}, imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, bodystructure1), tc.untaggedFetch(2, 2, bodystructure2))
|
||||||
tc.transactf("ok", "fetch 1:* bodystructure")
|
tc.transactf("ok", "fetch 1:* bodystructure")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodystructure1}}, imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, bodystructure1), tc.untaggedFetch(2, 2, bodystructure2))
|
||||||
tc.transactf("ok", "fetch *:1 bodystructure")
|
tc.transactf("ok", "fetch *:1 bodystructure")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodystructure1}}, imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, bodystructure1), tc.untaggedFetch(2, 2, bodystructure2))
|
||||||
tc.transactf("ok", "fetch *:2 bodystructure")
|
tc.transactf("ok", "fetch *:2 bodystructure")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
|
tc.xuntagged(tc.untaggedFetch(2, 2, bodystructure2))
|
||||||
|
|
||||||
tc.transactf("ok", "fetch * bodystructure") // Highest msgseq.
|
tc.transactf("ok", "fetch * bodystructure") // Highest msgseq.
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
|
tc.xuntagged(tc.untaggedFetch(2, 2, bodystructure2))
|
||||||
|
}
|
||||||
|
|
||||||
tc.transactf("ok", "uid fetch 1:* bodystructure")
|
tc.transactf("ok", "uid fetch 1:* bodystructure")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodystructure1}}, imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, bodystructure1), tc.untaggedFetch(2, 2, bodystructure2))
|
||||||
|
|
||||||
tc.transactf("ok", "uid fetch 1:2 bodystructure")
|
tc.transactf("ok", "uid fetch 1:2 bodystructure")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodystructure1}}, imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, bodystructure1), tc.untaggedFetch(2, 2, bodystructure2))
|
||||||
|
|
||||||
tc.transactf("ok", "uid fetch 1,2 bodystructure")
|
tc.transactf("ok", "uid fetch 1,2 bodystructure")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodystructure1}}, imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, bodystructure1), tc.untaggedFetch(2, 2, bodystructure2))
|
||||||
|
|
||||||
tc.transactf("ok", "uid fetch 2:2 bodystructure")
|
tc.transactf("ok", "uid fetch 2:2 bodystructure")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
|
tc.xuntagged(tc.untaggedFetch(2, 2, bodystructure2))
|
||||||
|
|
||||||
// todo: read the bodies/headers of the parts, and of the nested message.
|
// todo: read the bodies/headers of the parts, and of the nested message.
|
||||||
tc.transactf("ok", "fetch 2 body.peek[]")
|
tc.transactf("ok", "uid fetch 2 body.peek[]")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[]", Body: nestedMessage}}})
|
tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchBody{RespAttr: "BODY[]", Body: nestedMessage}))
|
||||||
|
|
||||||
part1 := tocrlf(` ... Some text appears here ...
|
part1 := tocrlf(` ... Some text appears here ...
|
||||||
|
|
||||||
@ -369,22 +382,22 @@ func TestFetch(t *testing.T) {
|
|||||||
It could have been done with explicit typing as in the
|
It could have been done with explicit typing as in the
|
||||||
next part.]
|
next part.]
|
||||||
`)
|
`)
|
||||||
tc.transactf("ok", "fetch 2 body.peek[1]")
|
tc.transactf("ok", "uid fetch 2 body.peek[1]")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[1]", Section: "1", Body: part1}}})
|
tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchBody{RespAttr: "BODY[1]", Section: "1", Body: part1}))
|
||||||
|
|
||||||
tc.transactf("no", "fetch 2 binary.peek[3]") // Only allowed on leaf parts, not multiparts.
|
tc.transactf("no", "uid fetch 2 binary.peek[3]") // Only allowed on leaf parts, not multiparts.
|
||||||
tc.transactf("no", "fetch 2 binary.peek[5]") // Only allowed on leaf parts, not messages.
|
tc.transactf("no", "uid fetch 2 binary.peek[5]") // Only allowed on leaf parts, not messages.
|
||||||
|
|
||||||
part31 := "aGVsbG8NCndvcmxkDQo=\r\n"
|
part31 := "aGVsbG8NCndvcmxkDQo=\r\n"
|
||||||
part31dec := "hello\r\nworld\r\n"
|
part31dec := "hello\r\nworld\r\n"
|
||||||
tc.transactf("ok", "fetch 2 binary.size[3.1]")
|
tc.transactf("ok", "uid fetch 2 binary.size[3.1]")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBinarySize{RespAttr: "BINARY.SIZE[3.1]", Parts: []uint32{3, 1}, Size: int64(len(part31dec))}}})
|
tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchBinarySize{RespAttr: "BINARY.SIZE[3.1]", Parts: []uint32{3, 1}, Size: int64(len(part31dec))}))
|
||||||
|
|
||||||
tc.transactf("ok", "fetch 2 body.peek[3.1]")
|
tc.transactf("ok", "uid fetch 2 body.peek[3.1]")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[3.1]", Section: "3.1", Body: part31}}})
|
tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchBody{RespAttr: "BODY[3.1]", Section: "3.1", Body: part31}))
|
||||||
|
|
||||||
tc.transactf("ok", "fetch 2 binary.peek[3.1]")
|
tc.transactf("ok", "uid fetch 2 binary.peek[3.1]")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBinary{RespAttr: "BINARY[3.1]", Parts: []uint32{3, 1}, Data: part31dec}}})
|
tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchBinary{RespAttr: "BINARY[3.1]", Parts: []uint32{3, 1}, Data: part31dec}))
|
||||||
|
|
||||||
part3 := tocrlf(`--unique-boundary-2
|
part3 := tocrlf(`--unique-boundary-2
|
||||||
Content-Type: audio/basic
|
Content-Type: audio/basic
|
||||||
@ -401,12 +414,12 @@ Content-Disposition: inline; filename=image.jpg
|
|||||||
--unique-boundary-2--
|
--unique-boundary-2--
|
||||||
|
|
||||||
`)
|
`)
|
||||||
tc.transactf("ok", "fetch 2 body.peek[3]")
|
tc.transactf("ok", "uid fetch 2 body.peek[3]")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[3]", Section: "3", Body: part3}}})
|
tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchBody{RespAttr: "BODY[3]", Section: "3", Body: part3}))
|
||||||
|
|
||||||
part2mime := "Content-type: text/plain; charset=US-ASCII\r\n"
|
part2mime := "Content-type: text/plain; charset=US-ASCII\r\n"
|
||||||
tc.transactf("ok", "fetch 2 body.peek[2.mime]")
|
tc.transactf("ok", "uid fetch 2 body.peek[2.mime]")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[2.MIME]", Section: "2.MIME", Body: part2mime}}})
|
tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchBody{RespAttr: "BODY[2.MIME]", Section: "2.MIME", Body: part2mime}))
|
||||||
|
|
||||||
part5 := tocrlf(`From: info@mox.example
|
part5 := tocrlf(`From: info@mox.example
|
||||||
To: mox <info@mox.example>
|
To: mox <info@mox.example>
|
||||||
@ -416,8 +429,8 @@ Content-Transfer-Encoding: Quoted-printable
|
|||||||
|
|
||||||
... Additional text in ISO-8859-1 goes here ...
|
... Additional text in ISO-8859-1 goes here ...
|
||||||
`)
|
`)
|
||||||
tc.transactf("ok", "fetch 2 body.peek[5]")
|
tc.transactf("ok", "uid fetch 2 body.peek[5]")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[5]", Section: "5", Body: part5}}})
|
tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchBody{RespAttr: "BODY[5]", Section: "5", Body: part5}))
|
||||||
|
|
||||||
part5header := tocrlf(`From: info@mox.example
|
part5header := tocrlf(`From: info@mox.example
|
||||||
To: mox <info@mox.example>
|
To: mox <info@mox.example>
|
||||||
@ -426,42 +439,42 @@ Content-Type: Text/plain; charset=ISO-8859-1
|
|||||||
Content-Transfer-Encoding: Quoted-printable
|
Content-Transfer-Encoding: Quoted-printable
|
||||||
|
|
||||||
`)
|
`)
|
||||||
tc.transactf("ok", "fetch 2 body.peek[5.header]")
|
tc.transactf("ok", "uid fetch 2 body.peek[5.header]")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[5.HEADER]", Section: "5.HEADER", Body: part5header}}})
|
tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchBody{RespAttr: "BODY[5.HEADER]", Section: "5.HEADER", Body: part5header}))
|
||||||
|
|
||||||
part5mime := tocrlf(`Content-Type: message/rfc822
|
part5mime := tocrlf(`Content-Type: message/rfc822
|
||||||
Content-MD5: MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=
|
Content-MD5: MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=
|
||||||
Content-Language: en,de
|
Content-Language: en,de
|
||||||
Content-Location: http://localhost
|
Content-Location: http://localhost
|
||||||
`)
|
`)
|
||||||
tc.transactf("ok", "fetch 2 body.peek[5.mime]")
|
tc.transactf("ok", "uid fetch 2 body.peek[5.mime]")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[5.MIME]", Section: "5.MIME", Body: part5mime}}})
|
tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchBody{RespAttr: "BODY[5.MIME]", Section: "5.MIME", Body: part5mime}))
|
||||||
|
|
||||||
part5text := " ... Additional text in ISO-8859-1 goes here ...\r\n"
|
part5text := " ... Additional text in ISO-8859-1 goes here ...\r\n"
|
||||||
|
|
||||||
tc.transactf("ok", "fetch 2 body.peek[5.text]")
|
tc.transactf("ok", "uid fetch 2 body.peek[5.text]")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[5.TEXT]", Section: "5.TEXT", Body: part5text}}})
|
tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchBody{RespAttr: "BODY[5.TEXT]", Section: "5.TEXT", Body: part5text}))
|
||||||
|
|
||||||
part5body := " ... Additional text in ISO-8859-1 goes here ...\r\n"
|
part5body := " ... Additional text in ISO-8859-1 goes here ...\r\n"
|
||||||
tc.transactf("ok", "fetch 2 body.peek[5.1]")
|
tc.transactf("ok", "uid fetch 2 body.peek[5.1]")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[5.1]", Section: "5.1", Body: part5body}}})
|
tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchBody{RespAttr: "BODY[5.1]", Section: "5.1", Body: part5body}))
|
||||||
|
|
||||||
// 5.1 is the part that is the sub message, but not as message/rfc822, but as part,
|
// 5.1 is the part that is the sub message, but not as message/rfc822, but as part,
|
||||||
// so we cannot request a header.
|
// so we cannot request a header.
|
||||||
tc.transactf("no", "fetch 2 body.peek[5.1.header]")
|
tc.transactf("no", "uid fetch 2 body.peek[5.1.header]")
|
||||||
|
|
||||||
// In case of EXAMINE instead of SELECT, we should not be seeing any changed \Seen flags for non-peek commands.
|
// In case of EXAMINE instead of SELECT, we should not be seeing any changed \Seen flags for non-peek commands.
|
||||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
tc.client.UIDStoreFlagsClear("1", true, `\Seen`)
|
||||||
tc.client.Unselect()
|
tc.client.Unselect()
|
||||||
tc.client.Examine("inbox")
|
tc.client.Examine("inbox")
|
||||||
|
|
||||||
// Preview
|
// Preview
|
||||||
preview := "Hello Joe, do you think we can meet at 3:30 tomorrow?"
|
preview := "Hello Joe, do you think we can meet at 3:30 tomorrow?"
|
||||||
tc.transactf("ok", "fetch 1 preview")
|
tc.transactf("ok", "uid fetch 1 preview")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, imapclient.FetchPreview{Preview: &preview}}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchPreview{Preview: &preview}))
|
||||||
|
|
||||||
tc.transactf("ok", "fetch 1 preview (lazy)")
|
tc.transactf("ok", "uid fetch 1 preview (lazy)")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, imapclient.FetchPreview{Preview: &preview}}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchPreview{Preview: &preview}))
|
||||||
|
|
||||||
// On-demand preview and saving on first request.
|
// On-demand preview and saving on first request.
|
||||||
err = tc.account.DB.Write(ctxbg, func(tx *bstore.Tx) error {
|
err = tc.account.DB.Write(ctxbg, func(tx *bstore.Tx) error {
|
||||||
@ -478,8 +491,8 @@ Content-Location: http://localhost
|
|||||||
})
|
})
|
||||||
tcheck(t, err, "remove preview from database")
|
tcheck(t, err, "remove preview from database")
|
||||||
|
|
||||||
tc.transactf("ok", "fetch 1 preview")
|
tc.transactf("ok", "uid fetch 1 preview")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, imapclient.FetchPreview{Preview: &preview}}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchPreview{Preview: &preview}))
|
||||||
m := store.Message{ID: 1}
|
m := store.Message{ID: 1}
|
||||||
err = tc.account.DB.Get(ctxbg, &m)
|
err = tc.account.DB.Get(ctxbg, &m)
|
||||||
tcheck(t, err, "get message")
|
tcheck(t, err, "get message")
|
||||||
@ -489,29 +502,38 @@ Content-Location: http://localhost
|
|||||||
t.Fatalf("got preview %q, expected %q", *m.Preview, preview+"\n")
|
t.Fatalf("got preview %q, expected %q", *m.Preview, preview+"\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
tc.transactf("bad", "fetch 1 preview (bogus)")
|
tc.transactf("bad", "uid fetch 1 preview (bogus)")
|
||||||
|
|
||||||
// Start a second session. Use it to remove the message. First session should still
|
// Start a second session. Use it to remove the message. First session should still
|
||||||
// be able to access the messages.
|
// be able to access the messages.
|
||||||
tc2 := startNoSwitchboard(t)
|
tc2 := startNoSwitchboard(t, uidonly)
|
||||||
defer tc2.closeNoWait()
|
defer tc2.closeNoWait()
|
||||||
tc2.client.Login("mjl@mox.example", password0)
|
tc2.login("mjl@mox.example", password0)
|
||||||
tc2.client.Select("inbox")
|
tc2.client.Select("inbox")
|
||||||
tc2.client.StoreFlagsSet("1", true, `\Deleted`)
|
tc2.client.UIDStoreFlagsSet("1", true, `\Deleted`)
|
||||||
tc2.client.Expunge()
|
tc2.client.Expunge()
|
||||||
tc2.client.Logout()
|
tc2.client.Logout()
|
||||||
|
|
||||||
|
if uidonly {
|
||||||
|
tc.transactf("ok", "uid fetch 1 binary[]")
|
||||||
|
tc.xuntagged(
|
||||||
|
tc.untaggedFetch(1, 1, imapclient.FetchFlags{`\Deleted`}),
|
||||||
|
imapclient.UntaggedVanished{UIDs: xparseNumSet("1")},
|
||||||
|
)
|
||||||
|
// Message no longer available in session.
|
||||||
|
} else {
|
||||||
tc.transactf("ok", "fetch 1 binary[]")
|
tc.transactf("ok", "fetch 1 binary[]")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binary1}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, binary1))
|
||||||
|
|
||||||
tc.transactf("ok", "fetch 1 body[]")
|
tc.transactf("ok", "fetch 1 body[]")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, body1}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, body1))
|
||||||
|
|
||||||
tc.transactf("ok", "fetch 1 rfc822.text")
|
tc.transactf("ok", "fetch 1 rfc822.text")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, rfctext1}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, rfctext1))
|
||||||
|
|
||||||
tc.transactf("ok", "fetch 1 rfc822")
|
tc.transactf("ok", "fetch 1 rfc822")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, rfc1}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, rfc1))
|
||||||
|
}
|
||||||
|
|
||||||
tc.client.Logout()
|
tc.client.Logout()
|
||||||
}
|
}
|
||||||
|
@ -9,14 +9,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestIdle(t *testing.T) {
|
func TestIdle(t *testing.T) {
|
||||||
tc1 := start(t)
|
tc1 := start(t, false)
|
||||||
defer tc1.close()
|
defer tc1.close()
|
||||||
|
|
||||||
tc2 := startNoSwitchboard(t)
|
tc2 := startNoSwitchboard(t, false)
|
||||||
defer tc2.closeNoWait()
|
defer tc2.closeNoWait()
|
||||||
|
|
||||||
tc1.client.Login("mjl@mox.example", password0)
|
tc1.login("mjl@mox.example", password0)
|
||||||
tc2.client.Login("mjl@mox.example", password0)
|
tc2.login("mjl@mox.example", password0)
|
||||||
|
|
||||||
tc1.transactf("ok", "select inbox")
|
tc1.transactf("ok", "select inbox")
|
||||||
tc2.transactf("ok", "select inbox")
|
tc2.transactf("ok", "select inbox")
|
||||||
|
@ -8,10 +8,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestListBasic(t *testing.T) {
|
func TestListBasic(t *testing.T) {
|
||||||
tc := start(t)
|
testListBasic(t, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListBasicUIDOnly(t *testing.T) {
|
||||||
|
testListBasic(t, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testListBasic(t *testing.T, uidonly bool) {
|
||||||
|
tc := start(t, uidonly)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
|
|
||||||
ulist := func(name string, flags ...string) imapclient.UntaggedList {
|
ulist := func(name string, flags ...string) imapclient.UntaggedList {
|
||||||
if len(flags) == 0 {
|
if len(flags) == 0 {
|
||||||
@ -59,12 +67,20 @@ func TestListBasic(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestListExtended(t *testing.T) {
|
func TestListExtended(t *testing.T) {
|
||||||
|
testListExtended(t, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListExtendedUIDOnly(t *testing.T) {
|
||||||
|
testListExtended(t, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testListExtended(t *testing.T, uidonly bool) {
|
||||||
defer mockUIDValidity()()
|
defer mockUIDValidity()()
|
||||||
|
|
||||||
tc := start(t)
|
tc := start(t, uidonly)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
|
|
||||||
ulist := func(name string, flags ...string) imapclient.UntaggedList {
|
ulist := func(name string, flags ...string) imapclient.UntaggedList {
|
||||||
if len(flags) == 0 {
|
if len(flags) == 0 {
|
||||||
|
@ -7,10 +7,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestLsub(t *testing.T) {
|
func TestLsub(t *testing.T) {
|
||||||
tc := start(t)
|
testLsub(t, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLsubUIDOnly(t *testing.T) {
|
||||||
|
testLsub(t, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLsub(t *testing.T, uidonly bool) {
|
||||||
|
tc := start(t, uidonly)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
|
|
||||||
tc.transactf("bad", "lsub") // Missing params.
|
tc.transactf("bad", "lsub") // Missing params.
|
||||||
tc.transactf("bad", `lsub ""`) // Missing param.
|
tc.transactf("bad", `lsub ""`) // Missing param.
|
||||||
|
@ -9,10 +9,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestMetadata(t *testing.T) {
|
func TestMetadata(t *testing.T) {
|
||||||
tc := start(t)
|
testMetadata(t, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMetadataUIDOnly(t *testing.T) {
|
||||||
|
testMetadata(t, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMetadata(t *testing.T, uidonly bool) {
|
||||||
|
tc := start(t, uidonly)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
|
|
||||||
tc.transactf("ok", `getmetadata "" /private/comment`)
|
tc.transactf("ok", `getmetadata "" /private/comment`)
|
||||||
tc.xuntagged()
|
tc.xuntagged()
|
||||||
@ -184,9 +192,9 @@ func TestMetadata(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Broadcast should not happen when metadata capability is not enabled.
|
// Broadcast should not happen when metadata capability is not enabled.
|
||||||
tc2 := startNoSwitchboard(t)
|
tc2 := startNoSwitchboard(t, uidonly)
|
||||||
defer tc2.closeNoWait()
|
defer tc2.closeNoWait()
|
||||||
tc2.client.Login("mjl@mox.example", password0)
|
tc2.login("mjl@mox.example", password0)
|
||||||
tc2.client.Select("inbox")
|
tc2.client.Select("inbox")
|
||||||
|
|
||||||
tc2.cmdf("", "idle")
|
tc2.cmdf("", "idle")
|
||||||
@ -255,10 +263,10 @@ func TestMetadata(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMetadataLimit(t *testing.T) {
|
func TestMetadataLimit(t *testing.T) {
|
||||||
tc := start(t)
|
tc := start(t, false)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
|
|
||||||
maxKeys, maxSize := metadataMaxKeys, metadataMaxSize
|
maxKeys, maxSize := metadataMaxKeys, metadataMaxSize
|
||||||
defer func() {
|
defer func() {
|
||||||
|
@ -7,23 +7,31 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestMove(t *testing.T) {
|
func TestMove(t *testing.T) {
|
||||||
|
testMove(t, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMoveUIDOnly(t *testing.T) {
|
||||||
|
testMove(t, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMove(t *testing.T, uidonly bool) {
|
||||||
defer mockUIDValidity()()
|
defer mockUIDValidity()()
|
||||||
tc := start(t)
|
tc := start(t, uidonly)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc2 := startNoSwitchboard(t)
|
tc2 := startNoSwitchboard(t, uidonly)
|
||||||
defer tc2.closeNoWait()
|
defer tc2.closeNoWait()
|
||||||
|
|
||||||
tc3 := startNoSwitchboard(t)
|
tc3 := startNoSwitchboard(t, uidonly)
|
||||||
defer tc3.closeNoWait()
|
defer tc3.closeNoWait()
|
||||||
|
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc.client.Select("inbox")
|
tc.client.Select("inbox")
|
||||||
|
|
||||||
tc2.client.Login("mjl@mox.example", password0)
|
tc2.login("mjl@mox.example", password0)
|
||||||
tc2.client.Select("Trash")
|
tc2.client.Select("Trash")
|
||||||
|
|
||||||
tc3.client.Login("mjl@mox.example", password0)
|
tc3.login("mjl@mox.example", password0)
|
||||||
tc3.client.Select("inbox")
|
tc3.client.Select("inbox")
|
||||||
|
|
||||||
tc.transactf("bad", "move") // Missing params.
|
tc.transactf("bad", "move") // Missing params.
|
||||||
@ -33,11 +41,14 @@ func TestMove(t *testing.T) {
|
|||||||
// Seqs 1,2 and UIDs 3,4.
|
// Seqs 1,2 and UIDs 3,4.
|
||||||
tc.client.Append("inbox", makeAppend(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
tc.client.Append("inbox", makeAppend(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
tc.client.StoreFlagsSet("1:2", true, `\Deleted`)
|
tc.client.UIDStoreFlagsSet("1:2", true, `\Deleted`)
|
||||||
tc.client.Expunge()
|
tc.client.Expunge()
|
||||||
tc.client.Append("inbox", makeAppend(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
tc.client.Append("inbox", makeAppend(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
|
|
||||||
|
if uidonly {
|
||||||
|
tc.transactf("ok", "uid move 1:* Trash")
|
||||||
|
} else {
|
||||||
tc.client.Unselect()
|
tc.client.Unselect()
|
||||||
tc.client.Examine("inbox")
|
tc.client.Examine("inbox")
|
||||||
tc.transactf("no", "move 1 Trash") // Opened readonly.
|
tc.transactf("no", "move 1 Trash") // Opened readonly.
|
||||||
@ -56,20 +67,20 @@ func TestMove(t *testing.T) {
|
|||||||
tc3.transactf("ok", "noop") // Drain.
|
tc3.transactf("ok", "noop") // Drain.
|
||||||
|
|
||||||
tc.transactf("ok", "move 1:* Trash")
|
tc.transactf("ok", "move 1:* Trash")
|
||||||
ptr := func(v uint32) *uint32 { return &v }
|
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
imapclient.UntaggedResult{Status: "OK", RespText: imapclient.RespText{Code: "COPYUID", CodeArg: imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 3, Last: ptr(4)}}, To: []imapclient.NumRange{{First: 1, Last: ptr(2)}}}, More: "moved"}},
|
imapclient.UntaggedResult{Status: "OK", RespText: imapclient.RespText{Code: "COPYUID", CodeArg: imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}, To: []imapclient.NumRange{{First: 1, Last: uint32ptr(2)}}}, More: "moved"}},
|
||||||
imapclient.UntaggedExpunge(1),
|
imapclient.UntaggedExpunge(1),
|
||||||
imapclient.UntaggedExpunge(1),
|
imapclient.UntaggedExpunge(1),
|
||||||
)
|
)
|
||||||
tc2.transactf("ok", "noop")
|
tc2.transactf("ok", "noop")
|
||||||
tc2.xuntagged(
|
tc2.xuntagged(
|
||||||
imapclient.UntaggedExists(2),
|
imapclient.UntaggedExists(2),
|
||||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchFlags(nil)}},
|
tc.untaggedFetch(1, 1, imapclient.FetchFlags(nil)),
|
||||||
imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2), imapclient.FetchFlags(nil)}},
|
tc.untaggedFetch(2, 2, imapclient.FetchFlags(nil)),
|
||||||
)
|
)
|
||||||
tc3.transactf("ok", "noop")
|
tc3.transactf("ok", "noop")
|
||||||
tc3.xuntagged(imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1))
|
tc3.xuntagged(imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1))
|
||||||
|
}
|
||||||
|
|
||||||
// UIDs 5,6
|
// UIDs 5,6
|
||||||
tc.client.Append("inbox", makeAppend(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
@ -79,17 +90,28 @@ func TestMove(t *testing.T) {
|
|||||||
|
|
||||||
tc.transactf("no", "uid move 1:4 Trash") // No match.
|
tc.transactf("no", "uid move 1:4 Trash") // No match.
|
||||||
tc.transactf("ok", "uid move 6:5 Trash")
|
tc.transactf("ok", "uid move 6:5 Trash")
|
||||||
|
if uidonly {
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
imapclient.UntaggedResult{Status: "OK", RespText: imapclient.RespText{Code: "COPYUID", CodeArg: imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 5, Last: ptr(6)}}, To: []imapclient.NumRange{{First: 3, Last: ptr(4)}}}, More: "moved"}},
|
imapclient.UntaggedResult{Status: "OK", RespText: imapclient.RespText{Code: "COPYUID", CodeArg: imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 5, Last: uint32ptr(6)}}, To: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}}, More: "moved"}},
|
||||||
|
imapclient.UntaggedVanished{UIDs: xparseNumSet("5:6")},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
tc.xuntagged(
|
||||||
|
imapclient.UntaggedResult{Status: "OK", RespText: imapclient.RespText{Code: "COPYUID", CodeArg: imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 5, Last: uint32ptr(6)}}, To: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}}, More: "moved"}},
|
||||||
imapclient.UntaggedExpunge(1),
|
imapclient.UntaggedExpunge(1),
|
||||||
imapclient.UntaggedExpunge(1),
|
imapclient.UntaggedExpunge(1),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
tc2.transactf("ok", "noop")
|
tc2.transactf("ok", "noop")
|
||||||
tc2.xuntagged(
|
tc2.xuntagged(
|
||||||
imapclient.UntaggedExists(4),
|
imapclient.UntaggedExists(4),
|
||||||
imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(3), imapclient.FetchFlags(nil)}},
|
tc2.untaggedFetch(3, 3, imapclient.FetchFlags(nil)),
|
||||||
imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(4), imapclient.FetchFlags(nil)}},
|
tc2.untaggedFetch(4, 4, imapclient.FetchFlags(nil)),
|
||||||
)
|
)
|
||||||
tc3.transactf("ok", "noop")
|
tc3.transactf("ok", "noop")
|
||||||
|
if uidonly {
|
||||||
|
tc3.xuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("5:6")})
|
||||||
|
} else {
|
||||||
tc3.xuntagged(imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1))
|
tc3.xuntagged(imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -9,15 +9,19 @@ import (
|
|||||||
"github.com/mjl-/mox/store"
|
"github.com/mjl-/mox/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ptr[T any](v T) *T {
|
func TestNotify(t *testing.T) {
|
||||||
return &v
|
testNotify(t, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNotify(t *testing.T) {
|
func TestNotifyUIDOnly(t *testing.T) {
|
||||||
|
testNotify(t, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNotify(t *testing.T, uidonly bool) {
|
||||||
defer mockUIDValidity()()
|
defer mockUIDValidity()()
|
||||||
tc := start(t)
|
tc := start(t, uidonly)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc.client.Select("inbox")
|
tc.client.Select("inbox")
|
||||||
|
|
||||||
// Check for some invalid syntax.
|
// Check for some invalid syntax.
|
||||||
@ -42,9 +46,9 @@ func TestNotify(t *testing.T) {
|
|||||||
tc.transactf("no", "Notify Set Status (Personal (unknownEvent))")
|
tc.transactf("no", "Notify Set Status (Personal (unknownEvent))")
|
||||||
tc.xcode("BADEVENT")
|
tc.xcode("BADEVENT")
|
||||||
|
|
||||||
tc2 := startNoSwitchboard(t)
|
tc2 := startNoSwitchboard(t, uidonly)
|
||||||
defer tc2.closeNoWait()
|
defer tc2.closeNoWait()
|
||||||
tc2.client.Login("mjl@mox.example", password0)
|
tc2.login("mjl@mox.example", password0)
|
||||||
tc2.client.Select("inbox")
|
tc2.client.Select("inbox")
|
||||||
|
|
||||||
var modseq uint32 = 4
|
var modseq uint32 = 4
|
||||||
@ -60,15 +64,9 @@ func TestNotify(t *testing.T) {
|
|||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
imapclient.UntaggedExists(1),
|
imapclient.UntaggedExists(1),
|
||||||
imapclient.UntaggedFetch{
|
tc.untaggedFetch(1, 1, imapclient.FetchFlags(nil)),
|
||||||
Seq: 1,
|
|
||||||
Attrs: []imapclient.FetchAttr{
|
|
||||||
imapclient.FetchUID(1),
|
|
||||||
imapclient.FetchFlags(nil),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
tc2.client.StoreFlagsAdd("1:*", true, `\Deleted`)
|
tc2.client.UIDStoreFlagsAdd("1:*", true, `\Deleted`)
|
||||||
modseq++
|
modseq++
|
||||||
tc2.client.Expunge()
|
tc2.client.Expunge()
|
||||||
modseq++
|
modseq++
|
||||||
@ -97,10 +95,7 @@ func TestNotify(t *testing.T) {
|
|||||||
modseq++
|
modseq++
|
||||||
tc.readuntagged(
|
tc.readuntagged(
|
||||||
imapclient.UntaggedExists(1),
|
imapclient.UntaggedExists(1),
|
||||||
imapclient.UntaggedFetch{
|
tc.untaggedFetchUID(1, 2,
|
||||||
Seq: 1,
|
|
||||||
Attrs: []imapclient.FetchAttr{
|
|
||||||
imapclient.FetchUID(2),
|
|
||||||
imapclient.FetchBodystructure{
|
imapclient.FetchBodystructure{
|
||||||
RespAttr: "BODYSTRUCTURE",
|
RespAttr: "BODYSTRUCTURE",
|
||||||
Body: imapclient.BodyTypeMpart{
|
Body: imapclient.BodyTypeMpart{
|
||||||
@ -134,30 +129,22 @@ func TestNotify(t *testing.T) {
|
|||||||
},
|
},
|
||||||
imapclient.FetchPreview{Preview: ptr("this is plain text.")},
|
imapclient.FetchPreview{Preview: ptr("this is plain text.")},
|
||||||
imapclient.FetchModSeq(modseq),
|
imapclient.FetchModSeq(modseq),
|
||||||
},
|
),
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Change flags.
|
// Change flags.
|
||||||
tc2.client.StoreFlagsAdd("1:*", true, `\Deleted`)
|
tc2.client.UIDStoreFlagsAdd("1:*", true, `\Deleted`)
|
||||||
modseq++
|
modseq++
|
||||||
tc.readuntagged(
|
tc.readuntagged(tc.untaggedFetch(1, 2, imapclient.FetchFlags{`\Deleted`}, imapclient.FetchModSeq(modseq)))
|
||||||
imapclient.UntaggedFetch{
|
|
||||||
Seq: 1,
|
|
||||||
Attrs: []imapclient.FetchAttr{
|
|
||||||
imapclient.FetchUID(2),
|
|
||||||
imapclient.FetchFlags{`\Deleted`},
|
|
||||||
imapclient.FetchModSeq(modseq),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Remove message.
|
// Remove message.
|
||||||
tc2.client.Expunge()
|
tc2.client.Expunge()
|
||||||
modseq++
|
modseq++
|
||||||
tc.readuntagged(
|
if uidonly {
|
||||||
imapclient.UntaggedExpunge(1),
|
tc.readuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("2")})
|
||||||
)
|
} else {
|
||||||
|
tc.readuntagged(imapclient.UntaggedExpunge(1))
|
||||||
|
}
|
||||||
|
|
||||||
// MailboxMetadataChange for mailbox annotation.
|
// MailboxMetadataChange for mailbox annotation.
|
||||||
tc2.transactf("ok", `setmetadata Archive (/private/comment "test")`)
|
tc2.transactf("ok", `setmetadata Archive (/private/comment "test")`)
|
||||||
@ -228,13 +215,7 @@ func TestNotify(t *testing.T) {
|
|||||||
modseq++
|
modseq++
|
||||||
tc.readuntagged(
|
tc.readuntagged(
|
||||||
imapclient.UntaggedExists(1),
|
imapclient.UntaggedExists(1),
|
||||||
imapclient.UntaggedFetch{
|
tc.untaggedFetchUID(1, 3, imapclient.FetchModSeq(modseq)),
|
||||||
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
|
// Next round of events must be ignored. We shouldn't get anything until we add a
|
||||||
@ -242,7 +223,7 @@ func TestNotify(t *testing.T) {
|
|||||||
tc.transactf("ok", "Notify Set (Selected None) (mailboxes testbox (messageNew messageExpunge)) (personal None)")
|
tc.transactf("ok", "Notify Set (Selected None) (mailboxes testbox (messageNew messageExpunge)) (personal None)")
|
||||||
tc2.client.Append("inbox", makeAppend(searchMsg)) // MessageNew
|
tc2.client.Append("inbox", makeAppend(searchMsg)) // MessageNew
|
||||||
modseq++
|
modseq++
|
||||||
tc2.client.StoreFlagsAdd("1:*", true, `\Deleted`) // FlagChange
|
tc2.client.UIDStoreFlagsAdd("1:*", true, `\Deleted`) // FlagChange
|
||||||
modseq++
|
modseq++
|
||||||
tc2.client.Expunge() // MessageExpunge
|
tc2.client.Expunge() // MessageExpunge
|
||||||
modseq++
|
modseq++
|
||||||
@ -275,27 +256,27 @@ func TestNotify(t *testing.T) {
|
|||||||
tc.client.Unsubscribe("other/a/b")
|
tc.client.Unsubscribe("other/a/b")
|
||||||
|
|
||||||
// Inboxes
|
// Inboxes
|
||||||
tc3 := startNoSwitchboard(t)
|
tc3 := startNoSwitchboard(t, uidonly)
|
||||||
defer tc3.closeNoWait()
|
defer tc3.closeNoWait()
|
||||||
tc3.client.Login("mjl@mox.example", password0)
|
tc3.login("mjl@mox.example", password0)
|
||||||
tc3.transactf("ok", "Notify Set (Inboxes (messageNew messageExpunge))")
|
tc3.transactf("ok", "Notify Set (Inboxes (messageNew messageExpunge))")
|
||||||
|
|
||||||
// Subscribed
|
// Subscribed
|
||||||
tc4 := startNoSwitchboard(t)
|
tc4 := startNoSwitchboard(t, uidonly)
|
||||||
defer tc4.closeNoWait()
|
defer tc4.closeNoWait()
|
||||||
tc4.client.Login("mjl@mox.example", password0)
|
tc4.login("mjl@mox.example", password0)
|
||||||
tc4.transactf("ok", "Notify Set (Subscribed (messageNew messageExpunge))")
|
tc4.transactf("ok", "Notify Set (Subscribed (messageNew messageExpunge))")
|
||||||
|
|
||||||
// Subtree
|
// Subtree
|
||||||
tc5 := startNoSwitchboard(t)
|
tc5 := startNoSwitchboard(t, uidonly)
|
||||||
defer tc5.closeNoWait()
|
defer tc5.closeNoWait()
|
||||||
tc5.client.Login("mjl@mox.example", password0)
|
tc5.login("mjl@mox.example", password0)
|
||||||
tc5.transactf("ok", "Notify Set (Subtree (Nonexistent inbox) (messageNew messageExpunge))")
|
tc5.transactf("ok", "Notify Set (Subtree (Nonexistent inbox) (messageNew messageExpunge))")
|
||||||
|
|
||||||
// Subtree-One
|
// Subtree-One
|
||||||
tc6 := startNoSwitchboard(t)
|
tc6 := startNoSwitchboard(t, uidonly)
|
||||||
defer tc6.closeNoWait()
|
defer tc6.closeNoWait()
|
||||||
tc6.client.Login("mjl@mox.example", password0)
|
tc6.login("mjl@mox.example", password0)
|
||||||
tc6.transactf("ok", "Notify Set (Subtree-One (Nonexistent Inbox/a other) (messageNew messageExpunge))")
|
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,
|
// We append to other/a/b first. It would normally come first in the notifications,
|
||||||
@ -336,7 +317,7 @@ func TestNotify(t *testing.T) {
|
|||||||
tc.client.Select("statusbox")
|
tc.client.Select("statusbox")
|
||||||
tc2.client.Append("inbox", makeAppend(searchMsg))
|
tc2.client.Append("inbox", makeAppend(searchMsg))
|
||||||
modseq++
|
modseq++
|
||||||
tc2.client.StoreFlagsSet("1", true, `\Seen`)
|
tc2.client.UIDStoreFlagsSet("*", true, `\Seen`)
|
||||||
modseq++
|
modseq++
|
||||||
tc2.client.Append("statusbox", imapclient.Append{Flags: []string{"newflag"}, Size: int64(len(searchMsg)), Data: strings.NewReader(searchMsg)})
|
tc2.client.Append("statusbox", imapclient.Append{Flags: []string{"newflag"}, Size: int64(len(searchMsg)), Data: strings.NewReader(searchMsg)})
|
||||||
modseq++
|
modseq++
|
||||||
@ -350,40 +331,32 @@ func TestNotify(t *testing.T) {
|
|||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
imapclient.UntaggedExists(2),
|
imapclient.UntaggedExists(2),
|
||||||
imapclient.UntaggedFetch{
|
tc.untaggedFetch(2, 2, imapclient.FetchFlags{"newflag"}, imapclient.FetchModSeq(modseq)),
|
||||||
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`},
|
imapclient.UntaggedFlags{`\Seen`, `\Answered`, `\Flagged`, `\Deleted`, `\Draft`, `$Forwarded`, `$Junk`, `$NotJunk`, `$Phishing`, `$MDNSent`, `newflag`},
|
||||||
)
|
)
|
||||||
|
|
||||||
tc2.client.StoreFlagsSet("2", true, `\Deleted`)
|
tc2.client.UIDStoreFlagsSet("2", true, `\Deleted`)
|
||||||
modseq++
|
modseq++
|
||||||
tc2.client.Expunge()
|
tc2.client.Expunge()
|
||||||
modseq++
|
modseq++
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
|
if uidonly {
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
imapclient.UntaggedFetch{
|
tc.untaggedFetch(2, 2, imapclient.FetchFlags{`\Deleted`}, imapclient.FetchModSeq(modseq-1)),
|
||||||
Seq: 2,
|
imapclient.UntaggedVanished{UIDs: xparseNumSet("2")},
|
||||||
Attrs: []imapclient.FetchAttr{
|
)
|
||||||
imapclient.FetchUID(2),
|
} else {
|
||||||
imapclient.FetchFlags{`\Deleted`},
|
tc.xuntagged(
|
||||||
imapclient.FetchModSeq(modseq - 1),
|
tc.untaggedFetch(2, 2, imapclient.FetchFlags{`\Deleted`}, imapclient.FetchModSeq(modseq-1)),
|
||||||
},
|
|
||||||
},
|
|
||||||
imapclient.UntaggedExpunge(2),
|
imapclient.UntaggedExpunge(2),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// With Selected-Delayed, we should get events for selected mailboxes immediately when using IDLE.
|
// With Selected-Delayed, we should get events for selected mailboxes immediately when using IDLE.
|
||||||
tc2.client.StoreFlagsSet("*", true, `\Answered`)
|
tc2.client.UIDStoreFlagsSet("*", true, `\Answered`)
|
||||||
modseq++
|
modseq++
|
||||||
|
|
||||||
tc2.client.Select("inbox")
|
tc2.client.Select("inbox")
|
||||||
tc2.client.StoreFlagsClear("1", true, `\Seen`)
|
tc2.client.UIDStoreFlagsClear("*", true, `\Seen`)
|
||||||
modseq++
|
modseq++
|
||||||
tc2.client.Select("statusbox")
|
tc2.client.Select("statusbox")
|
||||||
|
|
||||||
@ -394,14 +367,7 @@ func TestNotify(t *testing.T) {
|
|||||||
tc.conn.SetReadDeadline(time.Now().Add(3 * time.Second))
|
tc.conn.SetReadDeadline(time.Now().Add(3 * time.Second))
|
||||||
tc.cmdf("", "idle")
|
tc.cmdf("", "idle")
|
||||||
tc.readprefixline("+ ")
|
tc.readprefixline("+ ")
|
||||||
tc.readuntagged(imapclient.UntaggedFetch{
|
tc.readuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{`\Answered`}, imapclient.FetchModSeq(modseq-1)))
|
||||||
Seq: 1,
|
|
||||||
Attrs: []imapclient.FetchAttr{
|
|
||||||
imapclient.FetchUID(1),
|
|
||||||
imapclient.FetchFlags{`\Answered`},
|
|
||||||
imapclient.FetchModSeq(modseq - 1),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
tc.writelinef("done")
|
tc.writelinef("done")
|
||||||
tc.response("ok")
|
tc.response("ok")
|
||||||
tc.conn.SetReadDeadline(time.Now().Add(30 * time.Second))
|
tc.conn.SetReadDeadline(time.Now().Add(30 * time.Second))
|
||||||
@ -409,7 +375,7 @@ func TestNotify(t *testing.T) {
|
|||||||
// If any event matches, we normally return it. But NONE prevents looking further.
|
// If any event matches, we normally return it. But NONE prevents looking further.
|
||||||
tc.client.Unselect()
|
tc.client.Unselect()
|
||||||
tc.transactf("ok", "notify set (mailboxes statusbox NONE) (personal (mailboxName))")
|
tc.transactf("ok", "notify set (mailboxes statusbox NONE) (personal (mailboxName))")
|
||||||
tc2.client.StoreFlagsSet("*", true, `\Answered`) // Matches NONE, ignored.
|
tc2.client.UIDStoreFlagsSet("*", true, `\Answered`) // Matches NONE, ignored.
|
||||||
//modseq++
|
//modseq++
|
||||||
tc2.client.Create("eventbox", nil)
|
tc2.client.Create("eventbox", nil)
|
||||||
//modseq++
|
//modseq++
|
||||||
@ -425,10 +391,7 @@ func TestNotify(t *testing.T) {
|
|||||||
offset := strings.Index(searchMsg, "\r\n\r\n")
|
offset := strings.Index(searchMsg, "\r\n\r\n")
|
||||||
tc.readuntagged(
|
tc.readuntagged(
|
||||||
imapclient.UntaggedExists(2),
|
imapclient.UntaggedExists(2),
|
||||||
imapclient.UntaggedFetch{
|
tc.untaggedFetch(2, 3,
|
||||||
Seq: 2,
|
|
||||||
Attrs: []imapclient.FetchAttr{
|
|
||||||
imapclient.FetchUID(3),
|
|
||||||
imapclient.FetchBody{
|
imapclient.FetchBody{
|
||||||
RespAttr: "BODY[HEADER]",
|
RespAttr: "BODY[HEADER]",
|
||||||
Section: "HEADER",
|
Section: "HEADER",
|
||||||
@ -440,8 +403,7 @@ func TestNotify(t *testing.T) {
|
|||||||
Body: searchMsg[offset+4:],
|
Body: searchMsg[offset+4:],
|
||||||
},
|
},
|
||||||
imapclient.FetchFlags(nil),
|
imapclient.FetchFlags(nil),
|
||||||
},
|
),
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// If we encounter an error during fetch, an untagged NO is returned.
|
// If we encounter an error during fetch, an untagged NO is returned.
|
||||||
@ -457,18 +419,21 @@ func TestNotify(t *testing.T) {
|
|||||||
More: "generating notify fetch response: requested part does not exist",
|
More: "generating notify fetch response: requested part does not exist",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
imapclient.UntaggedFetch{
|
tc.untaggedFetchUID(3, 4),
|
||||||
Seq: 3,
|
|
||||||
Attrs: []imapclient.FetchAttr{
|
|
||||||
imapclient.FetchUID(4),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// When adding new tests, uncomment modseq++ lines above.
|
// When adding new tests, uncomment modseq++ lines above.
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNotifyOverflow(t *testing.T) {
|
func TestNotifyOverflow(t *testing.T) {
|
||||||
|
testNotifyOverflow(t, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNotifyOverflowUIDOnly(t *testing.T) {
|
||||||
|
testNotifyOverflow(t, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNotifyOverflow(t *testing.T, uidonly bool) {
|
||||||
orig := store.CommPendingChangesMax
|
orig := store.CommPendingChangesMax
|
||||||
store.CommPendingChangesMax = 3
|
store.CommPendingChangesMax = 3
|
||||||
defer func() {
|
defer func() {
|
||||||
@ -476,15 +441,15 @@ func TestNotifyOverflow(t *testing.T) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
defer mockUIDValidity()()
|
defer mockUIDValidity()()
|
||||||
tc := start(t)
|
tc := start(t, uidonly)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc.client.Select("inbox")
|
tc.client.Select("inbox")
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
|
|
||||||
tc2 := startNoSwitchboard(t)
|
tc2 := startNoSwitchboard(t, uidonly)
|
||||||
defer tc2.closeNoWait()
|
defer tc2.closeNoWait()
|
||||||
tc2.client.Login("mjl@mox.example", password0)
|
tc2.login("mjl@mox.example", password0)
|
||||||
tc2.client.Select("inbox")
|
tc2.client.Select("inbox")
|
||||||
|
|
||||||
// Generates 4 changes, crossing max 3.
|
// Generates 4 changes, crossing max 3.
|
||||||
@ -507,26 +472,22 @@ func TestNotifyOverflow(t *testing.T) {
|
|||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.xuntagged()
|
tc.xuntagged()
|
||||||
|
|
||||||
// Enable notify again. We won't get a notification because the message isn't yet
|
// Enable notify again. Without uidonly, we won't get a notification because the
|
||||||
// known in the session.
|
// message isn't known in the session.
|
||||||
tc.transactf("ok", "notify set (selected (messageNew messageExpunge flagChange))")
|
tc.transactf("ok", "notify set (selected (messageNew messageExpunge flagChange))")
|
||||||
tc2.client.StoreFlagsAdd("1", true, `\Seen`)
|
tc2.client.UIDStoreFlagsAdd("1", true, `\Seen`)
|
||||||
|
if uidonly {
|
||||||
|
tc.readuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{`\Seen`}))
|
||||||
|
} else {
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.xuntagged()
|
tc.xuntagged()
|
||||||
|
}
|
||||||
|
|
||||||
// Reselect to get the message visible in the session.
|
// Reselect to get the message visible in the session.
|
||||||
tc.client.Select("inbox")
|
tc.client.Select("inbox")
|
||||||
tc2.client.StoreFlagsClear("1", true, `\Seen`)
|
tc2.client.UIDStoreFlagsClear("1", true, `\Seen`)
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.xuntagged(
|
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags(nil)))
|
||||||
imapclient.UntaggedFetch{
|
|
||||||
Seq: 1,
|
|
||||||
Attrs: []imapclient.FetchAttr{
|
|
||||||
imapclient.FetchUID(1),
|
|
||||||
imapclient.FetchFlags(nil),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Trigger overflow for changes for "selected-delayed".
|
// Trigger overflow for changes for "selected-delayed".
|
||||||
store.CommPendingChangesMax = 10
|
store.CommPendingChangesMax = 10
|
||||||
@ -536,8 +497,8 @@ func TestNotifyOverflow(t *testing.T) {
|
|||||||
selectedDelayedChangesMax = delayedMax
|
selectedDelayedChangesMax = delayedMax
|
||||||
}()
|
}()
|
||||||
tc.transactf("ok", "notify set (selected-delayed (messageNew messageExpunge flagChange))")
|
tc.transactf("ok", "notify set (selected-delayed (messageNew messageExpunge flagChange))")
|
||||||
tc2.client.StoreFlagsAdd("1", true, `\Seen`)
|
tc2.client.UIDStoreFlagsAdd("1", true, `\Seen`)
|
||||||
tc2.client.StoreFlagsClear("1", true, `\Seen`)
|
tc2.client.UIDStoreFlagsClear("1", true, `\Seen`)
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
imapclient.UntaggedResult{
|
imapclient.UntaggedResult{
|
||||||
@ -550,21 +511,13 @@ func TestNotifyOverflow(t *testing.T) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Again, no new notifications until we select and enable again.
|
// Again, no new notifications until we select and enable again.
|
||||||
tc2.client.StoreFlagsAdd("1", true, `\Seen`)
|
tc2.client.UIDStoreFlagsAdd("1", true, `\Seen`)
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.xuntagged()
|
tc.xuntagged()
|
||||||
|
|
||||||
tc.client.Select("inbox")
|
tc.client.Select("inbox")
|
||||||
tc.transactf("ok", "notify set (selected-delayed (messageNew messageExpunge flagChange))")
|
tc.transactf("ok", "notify set (selected-delayed (messageNew messageExpunge flagChange))")
|
||||||
tc2.client.StoreFlagsClear("1", true, `\Seen`)
|
tc2.client.UIDStoreFlagsClear("1", true, `\Seen`)
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.xuntagged(
|
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags(nil)))
|
||||||
imapclient.UntaggedFetch{
|
|
||||||
Seq: 1,
|
|
||||||
Attrs: []imapclient.FetchAttr{
|
|
||||||
imapclient.FetchUID(1),
|
|
||||||
imapclient.FetchFlags(nil),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
@ -946,42 +946,6 @@ func (p *parser) xsearchKey() *searchKey {
|
|||||||
return sk
|
return sk
|
||||||
}
|
}
|
||||||
|
|
||||||
// hasModseq returns whether there is a modseq filter anywhere in the searchkey.
|
|
||||||
func (sk searchKey) hasModseq() bool {
|
|
||||||
if sk.clientModseq != nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
for _, e := range sk.searchKeys {
|
|
||||||
if e.hasModseq() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if sk.searchKey != nil && sk.searchKey.hasModseq() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if sk.searchKey2 != nil && sk.searchKey2.hasModseq() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Whether we need message sequence numbers to evaluate. If not, we cannot optimize
|
|
||||||
// when only MAX is requested through a reverse query.
|
|
||||||
func (sk searchKey) needSeq() bool {
|
|
||||||
for _, k := range sk.searchKeys {
|
|
||||||
if k.needSeq() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if sk.searchKey != nil && sk.searchKey.needSeq() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if sk.searchKey2 != nil && sk.searchKey2.needSeq() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return sk.seqSet != nil && !sk.seqSet.searchResult
|
|
||||||
}
|
|
||||||
|
|
||||||
// ../rfc/9051:6489 ../rfc/3501:4692
|
// ../rfc/9051:6489 ../rfc/3501:4692
|
||||||
func (p *parser) xdateDay() int {
|
func (p *parser) xdateDay() int {
|
||||||
d := p.xdigit()
|
d := p.xdigit()
|
||||||
|
@ -35,7 +35,8 @@ func (ss numSet) containsSeq(seq msgseq, uids []store.UID, searchResult []store.
|
|||||||
return ss.containsSeqCount(seq, uint32(len(uids)))
|
return ss.containsSeqCount(seq, uint32(len(uids)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// containsSeqCount returns whether seq is contained in ss, which must not be a searchResult, assuming the message count.
|
// containsSeqCount returns whether seq is contained in ss, which must not be a
|
||||||
|
// searchResult, assuming the message count.
|
||||||
func (ss numSet) containsSeqCount(seq msgseq, msgCount uint32) bool {
|
func (ss numSet) containsSeqCount(seq msgseq, msgCount uint32) bool {
|
||||||
if msgCount == 0 {
|
if msgCount == 0 {
|
||||||
return false
|
return false
|
||||||
@ -64,45 +65,14 @@ func (ss numSet) containsSeqCount(seq msgseq, msgCount uint32) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss numSet) containsUID(uid store.UID, uids []store.UID, searchResult []store.UID) bool {
|
|
||||||
if len(uids) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if ss.searchResult {
|
|
||||||
return uidSearch(searchResult, uid) > 0 && uidSearch(uids, uid) > 0
|
|
||||||
}
|
|
||||||
for _, r := range ss.ranges {
|
|
||||||
first := store.UID(r.first.number)
|
|
||||||
if r.first.star || first > uids[len(uids)-1] {
|
|
||||||
first = uids[len(uids)-1]
|
|
||||||
}
|
|
||||||
last := first
|
|
||||||
// Num in <num>:* can be larger than last, but it still matches the last...
|
|
||||||
// Similar for *:<num>. ../rfc/9051:4814
|
|
||||||
if r.last != nil {
|
|
||||||
last = store.UID(r.last.number)
|
|
||||||
if r.last.star || last > uids[len(uids)-1] {
|
|
||||||
last = uids[len(uids)-1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if first > last {
|
|
||||||
first, last = last, first
|
|
||||||
}
|
|
||||||
if uid >= first && uid <= last && uidSearch(uids, uid) > 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// containsKnownUID returns whether uid, which is known to exist, matches the numSet.
|
// containsKnownUID returns whether uid, which is known to exist, matches the numSet.
|
||||||
// highestUID must return the highest/last UID in the mailbox, or an error. A last UID must
|
// highestUID must return the highest/last UID in the mailbox, or an error. A last UID must
|
||||||
// exist, otherwise this method wouldn't have been called with a known uid.
|
// exist, otherwise this method wouldn't have been called with a known uid.
|
||||||
// highestUID is needed for interpreting UID sets like "<num>:*" where num is
|
// highestUID is needed for interpreting UID sets like "<num>:*" where num is
|
||||||
// higher than the uid to check.
|
// higher than the uid to check.
|
||||||
func (ss numSet) containsKnownUID(uid store.UID, searchResult []store.UID, highestUID func() (store.UID, error)) (bool, error) {
|
func (ss numSet) xcontainsKnownUID(uid store.UID, searchResult []store.UID, xhighestUID func() store.UID) bool {
|
||||||
if ss.searchResult {
|
if ss.searchResult {
|
||||||
return uidSearch(searchResult, uid) > 0, nil
|
return uidSearch(searchResult, uid) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, r := range ss.ranges {
|
for _, r := range ss.ranges {
|
||||||
@ -111,38 +81,61 @@ func (ss numSet) containsKnownUID(uid store.UID, searchResult []store.UID, highe
|
|||||||
// Similar for *:<num>. ../rfc/9051:4814
|
// Similar for *:<num>. ../rfc/9051:4814
|
||||||
if r.first.star {
|
if r.first.star {
|
||||||
if r.last != nil && uid >= store.UID(r.last.number) {
|
if r.last != nil && uid >= store.UID(r.last.number) {
|
||||||
return true, nil
|
return true
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
a, err = highestUID()
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
}
|
||||||
|
a = xhighestUID()
|
||||||
}
|
}
|
||||||
b := a
|
b := a
|
||||||
if r.last != nil {
|
if r.last != nil {
|
||||||
b = store.UID(r.last.number)
|
b = store.UID(r.last.number)
|
||||||
if r.last.star {
|
if r.last.star {
|
||||||
if uid >= a {
|
if uid >= a {
|
||||||
return true, nil
|
return true
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
b, err = highestUID()
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
}
|
||||||
|
b = xhighestUID()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if a > b {
|
if a > b {
|
||||||
a, b = b, a
|
a, b = b, a
|
||||||
}
|
}
|
||||||
if uid >= a && uid <= b {
|
if uid >= a && uid <= b {
|
||||||
return true, nil
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false, nil
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// xinterpretStar returns a numset that interprets stars in a uid set using
|
||||||
|
// xlastUID, returning a new uid set without stars, with increasing first/last, and
|
||||||
|
// without unneeded ranges (first.number != last.number).
|
||||||
|
// If there are no messages in the mailbox, xlastUID must return zero and the
|
||||||
|
// returned numSet will include 0.
|
||||||
|
func (s numSet) xinterpretStar(xlastUID func() store.UID) numSet {
|
||||||
|
var ns numSet
|
||||||
|
|
||||||
|
for _, r := range s.ranges {
|
||||||
|
first := r.first.number
|
||||||
|
if r.first.star {
|
||||||
|
first = uint32(xlastUID())
|
||||||
|
}
|
||||||
|
last := first
|
||||||
|
if r.last != nil {
|
||||||
|
if r.last.star {
|
||||||
|
last = uint32(xlastUID())
|
||||||
|
} else {
|
||||||
|
last = r.last.number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if first > last {
|
||||||
|
first, last = last, first
|
||||||
|
}
|
||||||
|
nr := numRange{first: setNumber{number: first}}
|
||||||
|
if first != last {
|
||||||
|
nr.last = &setNumber{number: last}
|
||||||
|
}
|
||||||
|
ns.ranges = append(ns.ranges, nr)
|
||||||
|
}
|
||||||
|
return ns
|
||||||
}
|
}
|
||||||
|
|
||||||
// contains returns whether the numset contains the number.
|
// contains returns whether the numset contains the number.
|
||||||
@ -216,38 +209,6 @@ func (ss numSet) String() string {
|
|||||||
return l[0]
|
return l[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
// interpretStar returns a numset that interprets stars in a numset, returning a new
|
|
||||||
// numset without stars with increasing first/last.
|
|
||||||
func (s numSet) interpretStar(uids []store.UID) numSet {
|
|
||||||
var ns numSet
|
|
||||||
if len(uids) == 0 {
|
|
||||||
return ns
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, r := range s.ranges {
|
|
||||||
first := r.first.number
|
|
||||||
if r.first.star || first > uint32(uids[len(uids)-1]) {
|
|
||||||
first = uint32(uids[len(uids)-1])
|
|
||||||
}
|
|
||||||
last := first
|
|
||||||
if r.last != nil {
|
|
||||||
last = r.last.number
|
|
||||||
if r.last.star || last > uint32(uids[len(uids)-1]) {
|
|
||||||
last = uint32(uids[len(uids)-1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if first > last {
|
|
||||||
first, last = last, first
|
|
||||||
}
|
|
||||||
nr := numRange{first: setNumber{number: first}}
|
|
||||||
if first != last {
|
|
||||||
nr.last = &setNumber{number: last}
|
|
||||||
}
|
|
||||||
ns.ranges = append(ns.ranges, nr)
|
|
||||||
}
|
|
||||||
return ns
|
|
||||||
}
|
|
||||||
|
|
||||||
// whether numSet only has numbers (no star/search), and is strictly increasing.
|
// whether numSet only has numbers (no star/search), and is strictly increasing.
|
||||||
func (s *numSet) isBasicIncreasing() bool {
|
func (s *numSet) isBasicIncreasing() bool {
|
||||||
if s.searchResult {
|
if s.searchResult {
|
||||||
@ -385,6 +346,40 @@ type searchKey struct {
|
|||||||
clientModseq *int64
|
clientModseq *int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Whether we need message sequence numbers to evaluate. Sequence numbers are not
|
||||||
|
// allowed with UIDONLY. And if we need sequence numbers we cannot optimize
|
||||||
|
// searching for MAX with a query in reverse order.
|
||||||
|
func (sk *searchKey) hasSequenceNumbers() bool {
|
||||||
|
for _, k := range sk.searchKeys {
|
||||||
|
if k.hasSequenceNumbers() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if sk.searchKey != nil && sk.searchKey.hasSequenceNumbers() || sk.searchKey2 != nil && sk.searchKey2.hasSequenceNumbers() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return sk.seqSet != nil && !sk.seqSet.searchResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasModseq returns whether there is a modseq filter anywhere in the searchkey.
|
||||||
|
func (sk *searchKey) hasModseq() bool {
|
||||||
|
if sk.clientModseq != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, e := range sk.searchKeys {
|
||||||
|
if e.hasModseq() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if sk.searchKey != nil && sk.searchKey.hasModseq() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if sk.searchKey2 != nil && sk.searchKey2.hasModseq() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func compactUIDSet(l []store.UID) (r numSet) {
|
func compactUIDSet(l []store.UID) (r numSet) {
|
||||||
for len(l) > 0 {
|
for len(l) > 0 {
|
||||||
e := 1
|
e := 1
|
||||||
|
@ -23,16 +23,10 @@ func TestNumSetContains(t *testing.T) {
|
|||||||
check(ss0.containsSeq(1, []store.UID{2}, []store.UID{2}))
|
check(ss0.containsSeq(1, []store.UID{2}, []store.UID{2}))
|
||||||
check(!ss0.containsSeq(1, []store.UID{2}, []store.UID{}))
|
check(!ss0.containsSeq(1, []store.UID{2}, []store.UID{}))
|
||||||
|
|
||||||
check(ss0.containsUID(1, []store.UID{1}, []store.UID{1}))
|
|
||||||
check(ss0.containsUID(2, []store.UID{1, 2, 3}, []store.UID{2}))
|
|
||||||
check(!ss0.containsUID(2, []store.UID{1, 2, 3}, []store.UID{}))
|
|
||||||
check(!ss0.containsUID(2, []store.UID{}, []store.UID{2}))
|
|
||||||
|
|
||||||
ss1 := numSet{false, []numRange{{*num(1), nil}}} // Single number 1.
|
ss1 := numSet{false, []numRange{{*num(1), nil}}} // Single number 1.
|
||||||
check(ss1.containsSeq(1, []store.UID{2}, nil))
|
check(ss1.containsSeq(1, []store.UID{2}, nil))
|
||||||
check(!ss1.containsSeq(2, []store.UID{1, 2}, nil))
|
check(!ss1.containsSeq(2, []store.UID{1, 2}, nil))
|
||||||
|
|
||||||
check(ss1.containsUID(1, []store.UID{1}, nil))
|
|
||||||
check(ss1.containsSeq(1, []store.UID{2}, nil))
|
check(ss1.containsSeq(1, []store.UID{2}, nil))
|
||||||
check(!ss1.containsSeq(2, []store.UID{1, 2}, nil))
|
check(!ss1.containsSeq(2, []store.UID{1, 2}, nil))
|
||||||
|
|
||||||
@ -44,15 +38,6 @@ func TestNumSetContains(t *testing.T) {
|
|||||||
check(ss2.containsSeq(3, []store.UID{4, 5, 6}, nil))
|
check(ss2.containsSeq(3, []store.UID{4, 5, 6}, nil))
|
||||||
check(!ss2.containsSeq(4, []store.UID{4, 5, 6}, nil))
|
check(!ss2.containsSeq(4, []store.UID{4, 5, 6}, nil))
|
||||||
|
|
||||||
check(ss2.containsUID(2, []store.UID{2}, nil))
|
|
||||||
check(!ss2.containsUID(1, []store.UID{1, 2, 3}, nil))
|
|
||||||
check(ss2.containsUID(3, []store.UID{1, 2, 3}, nil))
|
|
||||||
check(!ss2.containsUID(2, []store.UID{4, 5}, nil))
|
|
||||||
check(!ss2.containsUID(2, []store.UID{1}, nil))
|
|
||||||
|
|
||||||
check(ss2.containsUID(2, []store.UID{2, 6}, nil))
|
|
||||||
check(ss2.containsUID(6, []store.UID{2, 6}, nil))
|
|
||||||
|
|
||||||
// *:2, same as 2:*
|
// *:2, same as 2:*
|
||||||
ss3 := numSet{false, []numRange{{*star, num(2)}}}
|
ss3 := numSet{false, []numRange{{*star, num(2)}}}
|
||||||
check(ss3.containsSeq(1, []store.UID{2}, nil))
|
check(ss3.containsSeq(1, []store.UID{2}, nil))
|
||||||
@ -60,15 +45,6 @@ func TestNumSetContains(t *testing.T) {
|
|||||||
check(ss3.containsSeq(2, []store.UID{4, 5}, nil))
|
check(ss3.containsSeq(2, []store.UID{4, 5}, nil))
|
||||||
check(ss3.containsSeq(3, []store.UID{4, 5, 6}, nil))
|
check(ss3.containsSeq(3, []store.UID{4, 5, 6}, nil))
|
||||||
check(!ss3.containsSeq(4, []store.UID{4, 5, 6}, nil))
|
check(!ss3.containsSeq(4, []store.UID{4, 5, 6}, nil))
|
||||||
|
|
||||||
check(ss3.containsUID(2, []store.UID{2}, nil))
|
|
||||||
check(!ss3.containsUID(1, []store.UID{1, 2, 3}, nil))
|
|
||||||
check(ss3.containsUID(3, []store.UID{1, 2, 3}, nil))
|
|
||||||
check(!ss3.containsUID(2, []store.UID{4, 5}, nil))
|
|
||||||
check(!ss3.containsUID(2, []store.UID{1}, nil))
|
|
||||||
|
|
||||||
check(ss3.containsUID(2, []store.UID{2, 6}, nil))
|
|
||||||
check(ss3.containsUID(6, []store.UID{2, 6}, nil))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNumSetInterpret(t *testing.T) {
|
func TestNumSetInterpret(t *testing.T) {
|
||||||
@ -77,38 +53,34 @@ func TestNumSetInterpret(t *testing.T) {
|
|||||||
return p.xnumSet0(true, false)
|
return p.xnumSet0(true, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
checkEqual := func(uids []store.UID, a, s string) {
|
checkEqual := func(lastUID store.UID, a, s string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
n := parseNumSet(a).interpretStar(uids)
|
n := parseNumSet(a).xinterpretStar(func() store.UID { return lastUID })
|
||||||
ns := n.String()
|
ns := n.String()
|
||||||
if ns != s {
|
if ns != s {
|
||||||
t.Fatalf("%s != %s", ns, s)
|
t.Fatalf("%s != %s", ns, s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
checkEqual([]store.UID{}, "1:*", "")
|
checkEqual(0, "1:*", "0:1")
|
||||||
checkEqual([]store.UID{1}, "1:*", "1")
|
checkEqual(1, "1:*", "1")
|
||||||
checkEqual([]store.UID{1, 3}, "1:*", "1:3")
|
checkEqual(3, "1:*", "1:3")
|
||||||
checkEqual([]store.UID{1, 3}, "4:*", "3")
|
checkEqual(3, "4:*", "3:4")
|
||||||
checkEqual([]store.UID{1, 3}, "*:4", "3")
|
checkEqual(3, "*:4", "3:4")
|
||||||
checkEqual([]store.UID{2, 3}, "*:4", "3")
|
checkEqual(3, "*:4", "3:4")
|
||||||
checkEqual([]store.UID{2, 3}, "*:1", "1:3")
|
checkEqual(3, "*:1", "1:3")
|
||||||
checkEqual([]store.UID{2, 3}, "1:*", "1:3")
|
checkEqual(3, "1:*", "1:3")
|
||||||
checkEqual([]store.UID{1, 2, 3}, "1,2,3", "1,2,3")
|
checkEqual(3, "1,2,3", "1,2,3")
|
||||||
checkEqual([]store.UID{}, "1,2,3", "")
|
checkEqual(0, "1,2,3", "1,2,3")
|
||||||
checkEqual([]store.UID{}, "1:3", "")
|
checkEqual(0, "1:3", "1:3")
|
||||||
checkEqual([]store.UID{}, "3:1", "")
|
checkEqual(0, "3:1", "1:3")
|
||||||
|
|
||||||
iter := parseNumSet("1:3").interpretStar([]store.UID{}).newIter()
|
iter := parseNumSet("3:1").xinterpretStar(func() store.UID { return 2 }).newIter()
|
||||||
if _, ok := iter.Next(); ok {
|
|
||||||
t.Fatalf("expected immediate end for empty iter")
|
|
||||||
}
|
|
||||||
|
|
||||||
iter = parseNumSet("3:1").interpretStar([]store.UID{1, 2}).newIter()
|
|
||||||
v0, _ := iter.Next()
|
v0, _ := iter.Next()
|
||||||
v1, _ := iter.Next()
|
v1, _ := iter.Next()
|
||||||
|
v2, _ := iter.Next()
|
||||||
_, ok := iter.Next()
|
_, ok := iter.Next()
|
||||||
if v0 != 1 || v1 != 2 || ok {
|
if v0 != 1 || v1 != 2 || v2 != 3 || ok {
|
||||||
t.Fatalf("got %v %v %v, expected 1, 2, false", v0, v1, ok)
|
t.Fatalf("got %v %v %v %v, expected 1, 2, 3 false", v0, v1, v2, ok)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,10 +7,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestQuota1(t *testing.T) {
|
func TestQuota1(t *testing.T) {
|
||||||
tc := start(t)
|
tc := start(t, false)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
|
|
||||||
// We don't implement setquota.
|
// We don't implement setquota.
|
||||||
tc.transactf("bad", `setquota "" (STORAGE 123)`)
|
tc.transactf("bad", `setquota "" (STORAGE 123)`)
|
||||||
@ -35,10 +35,10 @@ func TestQuota1(t *testing.T) {
|
|||||||
tc.xuntagged(imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusDeletedStorage: 0}})
|
tc.xuntagged(imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusDeletedStorage: 0}})
|
||||||
|
|
||||||
// tclimit does have a limit.
|
// tclimit does have a limit.
|
||||||
tclimit := startArgs(t, false, false, true, true, "limit")
|
tclimit := startArgs(t, false, false, false, true, true, "limit")
|
||||||
defer tclimit.close()
|
defer tclimit.close()
|
||||||
|
|
||||||
tclimit.client.Login("limit@mox.example", password0)
|
tclimit.login("limit@mox.example", password0)
|
||||||
|
|
||||||
tclimit.transactf("ok", "getquotaroot inbox")
|
tclimit.transactf("ok", "getquotaroot inbox")
|
||||||
tclimit.xuntagged(
|
tclimit.xuntagged(
|
||||||
|
@ -6,16 +6,24 @@ import (
|
|||||||
"github.com/mjl-/mox/imapclient"
|
"github.com/mjl-/mox/imapclient"
|
||||||
)
|
)
|
||||||
|
|
||||||
// todo: check that UIDValidity is indeed updated properly.
|
|
||||||
func TestRename(t *testing.T) {
|
func TestRename(t *testing.T) {
|
||||||
tc := start(t)
|
testRename(t, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenameUIDOnly(t *testing.T) {
|
||||||
|
testRename(t, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: check that UIDValidity is indeed updated properly.
|
||||||
|
func testRename(t *testing.T, uidonly bool) {
|
||||||
|
tc := start(t, uidonly)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc2 := startNoSwitchboard(t)
|
tc2 := startNoSwitchboard(t, uidonly)
|
||||||
defer tc2.closeNoWait()
|
defer tc2.closeNoWait()
|
||||||
|
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc2.client.Login("mjl@mox.example", password0)
|
tc2.login("mjl@mox.example", password0)
|
||||||
|
|
||||||
tc.transactf("bad", "rename") // Missing parameters.
|
tc.transactf("bad", "rename") // Missing parameters.
|
||||||
tc.transactf("bad", "rename x") // Missing destination.
|
tc.transactf("bad", "rename x") // Missing destination.
|
||||||
@ -104,7 +112,7 @@ func TestRename(t *testing.T) {
|
|||||||
)
|
)
|
||||||
tc.transactf("ok", `select x/minbox`)
|
tc.transactf("ok", `select x/minbox`)
|
||||||
tc.transactf("ok", `uid fetch 1:* flags`)
|
tc.transactf("ok", `uid fetch 1:* flags`)
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchFlags{"label1"}}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{"label1"}))
|
||||||
|
|
||||||
// Renaming to new hiearchy that does not have any subscribes.
|
// Renaming to new hiearchy that does not have any subscribes.
|
||||||
tc.transactf("ok", "rename x/minbox w/w")
|
tc.transactf("ok", "rename x/minbox w/w")
|
||||||
|
@ -82,32 +82,38 @@ func (c *conn) cmdxReplace(isUID bool, tag, cmd string, p *parser) {
|
|||||||
|
|
||||||
// Resolve "*" for UID or message sequence.
|
// Resolve "*" for UID or message sequence.
|
||||||
if star {
|
if star {
|
||||||
if len(c.uids) == 0 {
|
if c.uidonly {
|
||||||
return func() { xuserErrorf("cannot use * on empty mailbox") }
|
q := bstore.QueryTx[store.Message](tx)
|
||||||
|
q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
|
||||||
|
q.FilterEqual("Expunged", false)
|
||||||
|
q.FilterLess("UID", c.uidnext)
|
||||||
|
q.SortDesc("UID")
|
||||||
|
q.Limit(1)
|
||||||
|
m, err := q.Get()
|
||||||
|
if err == bstore.ErrAbsent {
|
||||||
|
return func() { xsyntaxErrorf("cannot use * on empty mailbox") }
|
||||||
}
|
}
|
||||||
if isUID {
|
xcheckf(err, "get last message in mailbox")
|
||||||
num = uint32(c.uids[len(c.uids)-1])
|
num = uint32(m.UID)
|
||||||
|
} else if c.exists == 0 {
|
||||||
|
return func() { xsyntaxErrorf("cannot use * on empty mailbox") }
|
||||||
|
} else if isUID {
|
||||||
|
num = uint32(c.uids[c.exists-1])
|
||||||
} else {
|
} else {
|
||||||
num = uint32(len(c.uids))
|
num = uint32(c.exists)
|
||||||
}
|
}
|
||||||
star = false
|
star = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find or verify UID of message to replace.
|
// Find or verify UID of message to replace.
|
||||||
var seq msgseq
|
|
||||||
if isUID {
|
if isUID {
|
||||||
seq = c.sequence(store.UID(num))
|
uidOld = store.UID(num)
|
||||||
if seq <= 0 {
|
} else if num > c.exists {
|
||||||
return func() { xuserErrorf("unknown uid %d", num) }
|
|
||||||
}
|
|
||||||
} else if num > uint32(len(c.uids)) {
|
|
||||||
return func() { xuserErrorf("invalid msgseq") }
|
return func() { xuserErrorf("invalid msgseq") }
|
||||||
} else {
|
} else {
|
||||||
seq = msgseq(num)
|
uidOld = c.uids[int(num)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
uidOld = c.uids[int(seq)-1]
|
|
||||||
|
|
||||||
// Check the message still exists in the database. If it doesn't, it may have been
|
// Check the message still exists in the database. If it doesn't, it may have been
|
||||||
// deleted just now and we won't check the quota. We'll raise an error later on,
|
// deleted just now and we won't check the quota. We'll raise an error later on,
|
||||||
// when we are not possibly reading a sync literal and can respond with unsolicited
|
// when we are not possibly reading a sync literal and can respond with unsolicited
|
||||||
@ -115,6 +121,7 @@ func (c *conn) cmdxReplace(isUID bool, tag, cmd string, p *parser) {
|
|||||||
q := bstore.QueryTx[store.Message](tx)
|
q := bstore.QueryTx[store.Message](tx)
|
||||||
q.FilterNonzero(store.Message{MailboxID: c.mailboxID, UID: uidOld})
|
q.FilterNonzero(store.Message{MailboxID: c.mailboxID, UID: uidOld})
|
||||||
q.FilterEqual("Expunged", false)
|
q.FilterEqual("Expunged", false)
|
||||||
|
q.FilterLess("UID", c.uidnext)
|
||||||
_, err = q.Get()
|
_, err = q.Get()
|
||||||
if err == bstore.ErrAbsent {
|
if err == bstore.ErrAbsent {
|
||||||
return nil
|
return nil
|
||||||
@ -336,7 +343,7 @@ func (c *conn) cmdxReplace(isUID bool, tag, cmd string, p *parser) {
|
|||||||
c.uidAppend(nm.UID)
|
c.uidAppend(nm.UID)
|
||||||
// We send an untagged OK with APPENDUID, for sane bookkeeping in clients. ../rfc/8508:401
|
// We send an untagged OK with APPENDUID, for sane bookkeeping in clients. ../rfc/8508:401
|
||||||
c.xbwritelinef("* OK [APPENDUID %d %d] ", mbDst.UIDValidity, nm.UID)
|
c.xbwritelinef("* OK [APPENDUID %d %d] ", mbDst.UIDValidity, nm.UID)
|
||||||
c.xbwritelinef("* %d EXISTS", len(c.uids))
|
c.xbwritelinef("* %d EXISTS", c.exists)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We must return vanished instead of expunge, and also highestmodseq, when qresync
|
// We must return vanished instead of expunge, and also highestmodseq, when qresync
|
||||||
@ -345,13 +352,18 @@ func (c *conn) cmdxReplace(isUID bool, tag, cmd string, p *parser) {
|
|||||||
|
|
||||||
// Now that we are in sync with msgseq, we can find our old msgseq and say it is
|
// Now that we are in sync with msgseq, we can find our old msgseq and say it is
|
||||||
// expunged or vanished. ../rfc/7162:1900
|
// expunged or vanished. ../rfc/7162:1900
|
||||||
omsgseq := c.xsequence(om.UID)
|
var oseq msgseq
|
||||||
c.sequenceRemove(omsgseq, om.UID)
|
if c.uidonly {
|
||||||
if qresync {
|
c.exists--
|
||||||
|
} else {
|
||||||
|
oseq = c.xsequence(om.UID)
|
||||||
|
c.sequenceRemove(oseq, om.UID)
|
||||||
|
}
|
||||||
|
if qresync || c.uidonly {
|
||||||
c.xbwritelinef("* VANISHED %d", om.UID)
|
c.xbwritelinef("* VANISHED %d", om.UID)
|
||||||
// ../rfc/7162:1916
|
// ../rfc/7162:1916
|
||||||
} else {
|
} else {
|
||||||
c.xbwritelinef("* %d EXPUNGE", omsgseq)
|
c.xbwritelinef("* %d EXPUNGE", oseq)
|
||||||
}
|
}
|
||||||
c.xwriteresultf("%s OK [HIGHESTMODSEQ %d] replaced", tag, nm.ModSeq.Client())
|
c.xwriteresultf("%s OK [HIGHESTMODSEQ %d] replaced", tag, nm.ModSeq.Client())
|
||||||
}
|
}
|
||||||
|
@ -7,45 +7,79 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestReplace(t *testing.T) {
|
func TestReplace(t *testing.T) {
|
||||||
|
testReplace(t, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReplaceUIDOnly(t *testing.T) {
|
||||||
|
testReplace(t, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testReplace(t *testing.T, uidonly bool) {
|
||||||
defer mockUIDValidity()()
|
defer mockUIDValidity()()
|
||||||
|
|
||||||
tc := start(t)
|
tc := start(t, uidonly)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc2 := startNoSwitchboard(t)
|
tc2 := startNoSwitchboard(t, uidonly)
|
||||||
defer tc2.closeNoWait()
|
defer tc2.closeNoWait()
|
||||||
|
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc.client.Select("inbox")
|
tc.client.Select("inbox")
|
||||||
|
|
||||||
|
// Star not allowed on empty mailbox.
|
||||||
|
tc.transactf("bad", "uid replace * inbox {1}")
|
||||||
|
if !uidonly {
|
||||||
|
tc.transactf("bad", "replace * inbox {1}")
|
||||||
|
}
|
||||||
|
|
||||||
// Append 3 messages, remove first. Leaves msgseq 1,2 with uid 2,3.
|
// Append 3 messages, remove first. Leaves msgseq 1,2 with uid 2,3.
|
||||||
tc.client.Append("inbox", makeAppend(exampleMsg), makeAppend(exampleMsg), makeAppend(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg), makeAppend(exampleMsg), makeAppend(exampleMsg))
|
||||||
tc.client.StoreFlagsSet("1", true, `\deleted`)
|
tc.client.UIDStoreFlagsSet("1", true, `\deleted`)
|
||||||
tc.client.Expunge()
|
tc.client.Expunge()
|
||||||
|
|
||||||
tc.transactf("no", "replace 2 expungebox {1}") // Mailbox no longer exists.
|
tc.transactf("no", "uid replace 1 expungebox {1}") // Mailbox no longer exists.
|
||||||
tc.xcode("TRYCREATE")
|
tc.xcode("TRYCREATE")
|
||||||
|
|
||||||
tc2.client.Login("mjl@mox.example", password0)
|
tc2.login("mjl@mox.example", password0)
|
||||||
tc2.client.Select("inbox")
|
tc2.client.Select("inbox")
|
||||||
|
|
||||||
// Replace last message (msgseq 2, uid 3) in same mailbox.
|
// Replace last message (msgseq 2, uid 3) in same mailbox.
|
||||||
|
if uidonly {
|
||||||
|
tc.lastUntagged, tc.lastResult, tc.lastErr = tc.client.UIDReplace("3", "INBOX", makeAppend(searchMsg))
|
||||||
|
} else {
|
||||||
tc.lastUntagged, tc.lastResult, tc.lastErr = tc.client.Replace("2", "INBOX", makeAppend(searchMsg))
|
tc.lastUntagged, tc.lastResult, tc.lastErr = tc.client.Replace("2", "INBOX", makeAppend(searchMsg))
|
||||||
|
}
|
||||||
tcheck(tc.t, tc.lastErr, "read imap response")
|
tcheck(tc.t, tc.lastErr, "read imap response")
|
||||||
|
if uidonly {
|
||||||
|
tc.xuntagged(
|
||||||
|
imapclient.UntaggedResult{Status: "OK", RespText: imapclient.RespText{Code: "APPENDUID", CodeArg: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("4")}, More: ""}},
|
||||||
|
imapclient.UntaggedExists(3),
|
||||||
|
imapclient.UntaggedVanished{UIDs: xparseNumSet("3")},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
imapclient.UntaggedResult{Status: "OK", RespText: imapclient.RespText{Code: "APPENDUID", CodeArg: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("4")}, More: ""}},
|
imapclient.UntaggedResult{Status: "OK", RespText: imapclient.RespText{Code: "APPENDUID", CodeArg: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("4")}, More: ""}},
|
||||||
imapclient.UntaggedExists(3),
|
imapclient.UntaggedExists(3),
|
||||||
imapclient.UntaggedExpunge(2),
|
imapclient.UntaggedExpunge(2),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
tc.xcodeArg(imapclient.CodeHighestModSeq(8))
|
tc.xcodeArg(imapclient.CodeHighestModSeq(8))
|
||||||
|
|
||||||
// Check that other client sees Exists and Expunge.
|
// Check that other client sees Exists and Expunge.
|
||||||
tc2.transactf("ok", "noop")
|
tc2.transactf("ok", "noop")
|
||||||
|
if uidonly {
|
||||||
|
tc2.xuntagged(
|
||||||
|
imapclient.UntaggedVanished{UIDs: xparseNumSet("3")},
|
||||||
|
imapclient.UntaggedExists(2),
|
||||||
|
tc.untaggedFetch(2, 4, imapclient.FetchFlags(nil)),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
tc2.xuntagged(
|
tc2.xuntagged(
|
||||||
imapclient.UntaggedExpunge(2),
|
imapclient.UntaggedExpunge(2),
|
||||||
imapclient.UntaggedExists(2),
|
imapclient.UntaggedExists(2),
|
||||||
imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(4), imapclient.FetchFlags(nil)}},
|
tc.untaggedFetch(2, 4, imapclient.FetchFlags(nil)),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Enable qresync, replace uid 2 (msgseq 1) to different mailbox, see that we get vanished instead of expunged.
|
// Enable qresync, replace uid 2 (msgseq 1) to different mailbox, see that we get vanished instead of expunged.
|
||||||
tc.transactf("ok", "enable qresync")
|
tc.transactf("ok", "enable qresync")
|
||||||
@ -58,18 +92,38 @@ func TestReplace(t *testing.T) {
|
|||||||
)
|
)
|
||||||
tc.xcodeArg(imapclient.CodeHighestModSeq(9))
|
tc.xcodeArg(imapclient.CodeHighestModSeq(9))
|
||||||
|
|
||||||
|
// Use "*" for replacing.
|
||||||
|
tc.transactf("ok", "uid replace * inbox {1+}\r\nx")
|
||||||
|
tc.xuntagged(
|
||||||
|
imapclient.UntaggedResult{Status: "OK", RespText: imapclient.RespText{Code: "APPENDUID", CodeArg: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("6")}, More: ""}},
|
||||||
|
imapclient.UntaggedExists(3),
|
||||||
|
imapclient.UntaggedVanished{UIDs: xparseNumSet("5")},
|
||||||
|
)
|
||||||
|
if !uidonly {
|
||||||
|
tc.transactf("ok", "replace * inbox {1+}\r\ny")
|
||||||
|
tc.xuntagged(
|
||||||
|
imapclient.UntaggedResult{Status: "OK", RespText: imapclient.RespText{Code: "APPENDUID", CodeArg: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("7")}, More: ""}},
|
||||||
|
imapclient.UntaggedExists(3),
|
||||||
|
imapclient.UntaggedVanished{UIDs: xparseNumSet("6")},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Non-existent mailbox with non-synchronizing literal should consume the literal.
|
// Non-existent mailbox with non-synchronizing literal should consume the literal.
|
||||||
|
if uidonly {
|
||||||
|
tc.transactf("no", "uid replace 1 bogusbox {1+}\r\nx")
|
||||||
|
} else {
|
||||||
tc.transactf("no", "replace 1 bogusbox {1+}\r\nx")
|
tc.transactf("no", "replace 1 bogusbox {1+}\r\nx")
|
||||||
|
}
|
||||||
|
|
||||||
// Leftover data.
|
// Leftover data.
|
||||||
tc.transactf("bad", "replace 1 inbox () {6+}\r\ntest\r\n ")
|
tc.transactf("bad", "replace 1 inbox () {6+}\r\ntest\r\n ")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReplaceBigNonsyncLit(t *testing.T) {
|
func TestReplaceBigNonsyncLit(t *testing.T) {
|
||||||
tc := start(t)
|
tc := start(t, false)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc.client.Select("inbox")
|
tc.client.Select("inbox")
|
||||||
|
|
||||||
// Adding a message >1mb with non-sync literal to non-existent mailbox should abort entire connection.
|
// Adding a message >1mb with non-sync literal to non-existent mailbox should abort entire connection.
|
||||||
@ -81,20 +135,28 @@ func TestReplaceBigNonsyncLit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestReplaceQuota(t *testing.T) {
|
func TestReplaceQuota(t *testing.T) {
|
||||||
|
testReplaceQuota(t, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReplaceQuotaUIDOnly(t *testing.T) {
|
||||||
|
testReplaceQuota(t, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testReplaceQuota(t *testing.T, uidonly bool) {
|
||||||
// with quota limit
|
// with quota limit
|
||||||
tc := startArgs(t, true, false, true, true, "limit")
|
tc := startArgs(t, uidonly, true, false, true, true, "limit")
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc.client.Login("limit@mox.example", password0)
|
tc.login("limit@mox.example", password0)
|
||||||
tc.client.Select("inbox")
|
tc.client.Select("inbox")
|
||||||
tc.client.Append("inbox", makeAppend("x"))
|
tc.client.Append("inbox", makeAppend("x"))
|
||||||
|
|
||||||
// Synchronizing literal, we get failure immediately.
|
// Synchronizing literal, we get failure immediately.
|
||||||
tc.transactf("no", "replace 1 inbox {6}\r\n")
|
tc.transactf("no", "uid replace 1 inbox {6}\r\n")
|
||||||
tc.xcode("OVERQUOTA")
|
tc.xcode("OVERQUOTA")
|
||||||
|
|
||||||
// Synchronizing literal to non-existent mailbox, we get failure immediately.
|
// Synchronizing literal to non-existent mailbox, we get failure immediately.
|
||||||
tc.transactf("no", "replace 1 badbox {6}\r\n")
|
tc.transactf("no", "uid replace 1 badbox {6}\r\n")
|
||||||
tc.xcode("TRYCREATE")
|
tc.xcode("TRYCREATE")
|
||||||
|
|
||||||
buf := make([]byte, 4000, 4002)
|
buf := make([]byte, 4000, 4002)
|
||||||
@ -104,14 +166,14 @@ func TestReplaceQuota(t *testing.T) {
|
|||||||
buf = append(buf, "\r\n"...)
|
buf = append(buf, "\r\n"...)
|
||||||
|
|
||||||
// Non-synchronizing literal. We get to write our data.
|
// Non-synchronizing literal. We get to write our data.
|
||||||
tc.client.Commandf("", "replace 1 inbox ~{4000+}")
|
tc.client.Commandf("", "uid replace 1 inbox ~{4000+}")
|
||||||
_, err := tc.client.Write(buf)
|
_, err := tc.client.Write(buf)
|
||||||
tc.check(err, "write replace message")
|
tc.check(err, "write replace message")
|
||||||
tc.response("no")
|
tc.response("no")
|
||||||
tc.xcode("OVERQUOTA")
|
tc.xcode("OVERQUOTA")
|
||||||
|
|
||||||
// Non-synchronizing literal to bad mailbox.
|
// Non-synchronizing literal to bad mailbox.
|
||||||
tc.client.Commandf("", "replace 1 badbox {4000+}")
|
tc.client.Commandf("", "uid replace 1 badbox {4000+}")
|
||||||
_, err = tc.client.Write(buf)
|
_, err = tc.client.Write(buf)
|
||||||
tc.check(err, "write replace message")
|
tc.check(err, "write replace message")
|
||||||
tc.response("no")
|
tc.response("no")
|
||||||
@ -119,22 +181,30 @@ func TestReplaceQuota(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestReplaceExpunged(t *testing.T) {
|
func TestReplaceExpunged(t *testing.T) {
|
||||||
tc := start(t)
|
testReplaceExpunged(t, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReplaceExpungedUIDOnly(t *testing.T) {
|
||||||
|
testReplaceExpunged(t, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testReplaceExpunged(t *testing.T, uidonly bool) {
|
||||||
|
tc := start(t, uidonly)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc.client.Select("inbox")
|
tc.client.Select("inbox")
|
||||||
tc.client.Append("inbox", makeAppend(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
|
|
||||||
// We start the command, but don't write data yet.
|
// We start the command, but don't write data yet.
|
||||||
tc.client.Commandf("", "replace 1 inbox {4000}")
|
tc.client.Commandf("", "uid replace 1 inbox {4000}")
|
||||||
|
|
||||||
// Get in with second client and remove the message we are replacing.
|
// Get in with second client and remove the message we are replacing.
|
||||||
tc2 := startNoSwitchboard(t)
|
tc2 := startNoSwitchboard(t, uidonly)
|
||||||
defer tc2.closeNoWait()
|
defer tc2.closeNoWait()
|
||||||
tc2.client.Login("mjl@mox.example", password0)
|
tc2.login("mjl@mox.example", password0)
|
||||||
tc2.client.Select("inbox")
|
tc2.client.Select("inbox")
|
||||||
tc2.client.StoreFlagsSet("1", true, `\Deleted`)
|
tc2.client.UIDStoreFlagsSet("1", true, `\Deleted`)
|
||||||
tc2.client.Expunge()
|
tc2.client.Expunge()
|
||||||
tc2.client.Unselect()
|
tc2.client.Unselect()
|
||||||
tc2.client.Close()
|
tc2.client.Close()
|
||||||
@ -149,8 +219,15 @@ func TestReplaceExpunged(t *testing.T) {
|
|||||||
_, err := tc.client.Write(buf)
|
_, err := tc.client.Write(buf)
|
||||||
tc.check(err, "write replace message")
|
tc.check(err, "write replace message")
|
||||||
tc.response("no")
|
tc.response("no")
|
||||||
|
if uidonly {
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchFlags{`\Deleted`}}},
|
tc.untaggedFetch(1, 1, imapclient.FetchFlags{`\Deleted`}),
|
||||||
|
imapclient.UntaggedVanished{UIDs: xparseNumSet("1")},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
tc.xuntagged(
|
||||||
|
tc.untaggedFetch(1, 1, imapclient.FetchFlags{`\Deleted`}),
|
||||||
imapclient.UntaggedExpunge(1),
|
imapclient.UntaggedExpunge(1),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -107,6 +107,11 @@ func (c *conn) cmdxSearch(isUID, isE bool, tag, cmd string, p *parser) {
|
|||||||
sk.searchKeys = append(sk.searchKeys, *p.xsearchKey())
|
sk.searchKeys = append(sk.searchKeys, *p.xsearchKey())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sequence set search program must be rejected with UIDONLY enabled. ../rfc/9586:220
|
||||||
|
if c.uidonly && sk.hasSequenceNumbers() {
|
||||||
|
xsyntaxCodeErrorf("UIDREQUIRED", "cannot search message sequence numbers in search program with uidonly enabled")
|
||||||
|
}
|
||||||
|
|
||||||
// Even in case of error, we ensure search result is changed.
|
// Even in case of error, we ensure search result is changed.
|
||||||
if save {
|
if save {
|
||||||
c.searchResult = []store.UID{}
|
c.searchResult = []store.UID{}
|
||||||
@ -340,7 +345,7 @@ func (c *conn) cmdxSearch(isUID, isE bool, tag, cmd string, p *parser) {
|
|||||||
// Determine if search has a sequence set without search results. If so, we need
|
// Determine if search has a sequence set without search results. If so, we need
|
||||||
// sequence numbers for matching, and we must always go through the messages in
|
// sequence numbers for matching, and we must always go through the messages in
|
||||||
// forward order. No reverse search for MAX only.
|
// forward order. No reverse search for MAX only.
|
||||||
needSeq := (len(mailboxes) > 1 || len(mailboxes) == 1 && mailboxes[0].ID != c.mailboxID) && sk.needSeq()
|
needSeq := (len(mailboxes) > 1 || len(mailboxes) == 1 && mailboxes[0].ID != c.mailboxID) && sk.hasSequenceNumbers()
|
||||||
|
|
||||||
forward := eargs == nil || max1 == 0 || len(eargs) != 1 || needSeq
|
forward := eargs == nil || max1 == 0 || len(eargs) != 1 || needSeq
|
||||||
reverse := max1 == 1 && (len(eargs) == 1 || min1+max1 == len(eargs)) && !needSeq
|
reverse := max1 == 1 && (len(eargs) == 1 || min1+max1 == len(eargs)) && !needSeq
|
||||||
@ -352,8 +357,8 @@ func (c *conn) cmdxSearch(isUID, isE bool, tag, cmd string, p *parser) {
|
|||||||
goal := "nil"
|
goal := "nil"
|
||||||
var total uint32
|
var total uint32
|
||||||
for _, mb := range mailboxes {
|
for _, mb := range mailboxes {
|
||||||
if mb.ID == c.mailboxID {
|
if mb.ID == c.mailboxID && !c.uidonly {
|
||||||
total += uint32(len(c.uids))
|
total += c.exists
|
||||||
} else {
|
} else {
|
||||||
total += uint32(mb.Total + mb.Deleted)
|
total += uint32(mb.Total + mb.Deleted)
|
||||||
}
|
}
|
||||||
@ -370,27 +375,34 @@ func (c *conn) cmdxSearch(isUID, isE bool, tag, cmd string, p *parser) {
|
|||||||
result := Result{Mailbox: mb}
|
result := Result{Mailbox: mb}
|
||||||
|
|
||||||
msgCount := uint32(mb.MailboxCounts.Total + mb.MailboxCounts.Deleted)
|
msgCount := uint32(mb.MailboxCounts.Total + mb.MailboxCounts.Deleted)
|
||||||
if mb.ID == c.mailboxID {
|
if mb.ID == c.mailboxID && !c.uidonly {
|
||||||
msgCount = uint32(len(c.uids))
|
msgCount = c.exists
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used for interpreting UID sets with a star, like "1:*" and "10:*". Only called
|
// Used for interpreting UID sets with a star, like "1:*" and "10:*". Only called
|
||||||
// for UIDs that are higher than the number, since "10:*" evaluates to "10:5" if 5
|
// for UIDs that are higher than the number, since "10:*" evaluates to "10:5" if 5
|
||||||
// is the highest UID, and UID 5-10 would all match.
|
// is the highest UID, and UID 5-10 would all match.
|
||||||
var cachedHighestUID store.UID
|
var cachedHighestUID store.UID
|
||||||
highestUID := func() (store.UID, error) {
|
xhighestUID := func() store.UID {
|
||||||
if cachedHighestUID > 0 {
|
if cachedHighestUID > 0 {
|
||||||
return cachedHighestUID, nil
|
return cachedHighestUID
|
||||||
}
|
}
|
||||||
|
|
||||||
q := bstore.QueryTx[store.Message](tx)
|
q := bstore.QueryTx[store.Message](tx)
|
||||||
q.FilterNonzero(store.Message{MailboxID: mb.ID})
|
q.FilterNonzero(store.Message{MailboxID: mb.ID})
|
||||||
q.FilterEqual("Expunged", false)
|
q.FilterEqual("Expunged", false)
|
||||||
|
if mb.ID == c.mailboxID {
|
||||||
|
q.FilterLess("UID", c.uidnext)
|
||||||
|
}
|
||||||
q.SortDesc("UID")
|
q.SortDesc("UID")
|
||||||
q.Limit(1)
|
q.Limit(1)
|
||||||
m, err := q.Get()
|
m, err := q.Get()
|
||||||
|
if err == bstore.ErrAbsent {
|
||||||
|
xuserErrorf("cannot use * on empty mailbox")
|
||||||
|
}
|
||||||
|
xcheckf(err, "get last uid")
|
||||||
cachedHighestUID = m.UID
|
cachedHighestUID = m.UID
|
||||||
return cachedHighestUID, err
|
return cachedHighestUID
|
||||||
}
|
}
|
||||||
|
|
||||||
progressOrig := progress
|
progressOrig := progress
|
||||||
@ -403,6 +415,9 @@ func (c *conn) cmdxSearch(isUID, isE bool, tag, cmd string, p *parser) {
|
|||||||
q := bstore.QueryTx[store.Message](tx)
|
q := bstore.QueryTx[store.Message](tx)
|
||||||
q.FilterNonzero(store.Message{MailboxID: mb.ID})
|
q.FilterNonzero(store.Message{MailboxID: mb.ID})
|
||||||
q.FilterEqual("Expunged", false)
|
q.FilterEqual("Expunged", false)
|
||||||
|
if mb.ID == c.mailboxID {
|
||||||
|
q.FilterLess("UID", c.uidnext)
|
||||||
|
}
|
||||||
q.SortAsc("UID")
|
q.SortAsc("UID")
|
||||||
for m, err := range q.All() {
|
for m, err := range q.All() {
|
||||||
xcheckf(err, "list messages in mailbox")
|
xcheckf(err, "list messages in mailbox")
|
||||||
@ -416,7 +431,7 @@ func (c *conn) cmdxSearch(isUID, isE bool, tag, cmd string, p *parser) {
|
|||||||
}
|
}
|
||||||
progress++
|
progress++
|
||||||
|
|
||||||
if c.searchMatch(tx, msgCount, seq, m, *sk, bodySearch, textSearch, highestUID) {
|
if c.searchMatch(tx, msgCount, seq, m, *sk, bodySearch, textSearch, xhighestUID) {
|
||||||
result.UIDs = append(result.UIDs, m.UID)
|
result.UIDs = append(result.UIDs, m.UID)
|
||||||
result.MaxModSeq = max(result.MaxModSeq, m.ModSeq)
|
result.MaxModSeq = max(result.MaxModSeq, m.ModSeq)
|
||||||
if min1 == 1 && min1+max1 == len(eargs) {
|
if min1 == 1 && min1+max1 == len(eargs) {
|
||||||
@ -443,6 +458,9 @@ func (c *conn) cmdxSearch(isUID, isE bool, tag, cmd string, p *parser) {
|
|||||||
q.FilterNonzero(store.Message{MailboxID: mb.ID})
|
q.FilterNonzero(store.Message{MailboxID: mb.ID})
|
||||||
q.FilterEqual("Expunged", false)
|
q.FilterEqual("Expunged", false)
|
||||||
q.FilterGreater("UID", lastUID)
|
q.FilterGreater("UID", lastUID)
|
||||||
|
if mb.ID == c.mailboxID {
|
||||||
|
q.FilterLess("UID", c.uidnext)
|
||||||
|
}
|
||||||
q.SortDesc("UID")
|
q.SortDesc("UID")
|
||||||
for m, err := range q.All() {
|
for m, err := range q.All() {
|
||||||
xcheckf(err, "list messages in mailbox")
|
xcheckf(err, "list messages in mailbox")
|
||||||
@ -454,7 +472,7 @@ func (c *conn) cmdxSearch(isUID, isE bool, tag, cmd string, p *parser) {
|
|||||||
progress++
|
progress++
|
||||||
|
|
||||||
var seq msgseq // Filled in by searchMatch for messages in selected mailbox.
|
var seq msgseq // Filled in by searchMatch for messages in selected mailbox.
|
||||||
if c.searchMatch(tx, msgCount, seq, m, *sk, bodySearch, textSearch, highestUID) {
|
if c.searchMatch(tx, msgCount, seq, m, *sk, bodySearch, textSearch, xhighestUID) {
|
||||||
result.UIDs = append(result.UIDs, m.UID)
|
result.UIDs = append(result.UIDs, m.UID)
|
||||||
result.MaxModSeq = max(result.MaxModSeq, m.ModSeq)
|
result.MaxModSeq = max(result.MaxModSeq, m.ModSeq)
|
||||||
break
|
break
|
||||||
@ -483,10 +501,7 @@ func (c *conn) cmdxSearch(isUID, isE bool, tag, cmd string, p *parser) {
|
|||||||
// Old-style SEARCH response. We must spell out each number. So we may be splitting
|
// Old-style SEARCH response. We must spell out each number. So we may be splitting
|
||||||
// into multiple responses. ../rfc/9051:6809 ../rfc/3501:4833
|
// into multiple responses. ../rfc/9051:6809 ../rfc/3501:4833
|
||||||
for len(result.UIDs) > 0 {
|
for len(result.UIDs) > 0 {
|
||||||
n := len(result.UIDs)
|
n := min(100, len(result.UIDs))
|
||||||
if n > 100 {
|
|
||||||
n = 100
|
|
||||||
}
|
|
||||||
s := ""
|
s := ""
|
||||||
for _, v := range result.UIDs[:n] {
|
for _, v := range result.UIDs[:n] {
|
||||||
if !isUID {
|
if !isUID {
|
||||||
@ -516,9 +531,7 @@ func (c *conn) cmdxSearch(isUID, isE bool, tag, cmd string, p *parser) {
|
|||||||
if save {
|
if save {
|
||||||
// ../rfc/9051:3784 ../rfc/5182:13
|
// ../rfc/9051:3784 ../rfc/5182:13
|
||||||
c.searchResult = results[0].UIDs
|
c.searchResult = results[0].UIDs
|
||||||
if sanityChecks {
|
c.checkUIDs(c.searchResult, false)
|
||||||
checkUIDs(c.searchResult)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// No untagged ESEARCH response if nothing was requested. ../rfc/9051:4160
|
// No untagged ESEARCH response if nothing was requested. ../rfc/9051:4160
|
||||||
@ -591,20 +604,26 @@ type search struct {
|
|||||||
m store.Message
|
m store.Message
|
||||||
mr *store.MsgReader
|
mr *store.MsgReader
|
||||||
p *message.Part
|
p *message.Part
|
||||||
highestUID func() (store.UID, error)
|
xhighestUID func() store.UID
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *conn) searchMatch(tx *bstore.Tx, msgCount uint32, seq msgseq, m store.Message, sk searchKey, bodySearch, textSearch *store.WordSearch, highestUID func() (store.UID, error)) bool {
|
func (c *conn) searchMatch(tx *bstore.Tx, msgCount uint32, seq msgseq, m store.Message, sk searchKey, bodySearch, textSearch *store.WordSearch, xhighestUID func() store.UID) bool {
|
||||||
if m.MailboxID == c.mailboxID {
|
if m.MailboxID == c.mailboxID {
|
||||||
|
// If session doesn't know about the message yet, don't return it.
|
||||||
|
if c.uidonly {
|
||||||
|
if m.UID >= c.uidnext {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Set seq for use in evaluations.
|
||||||
seq = c.sequence(m.UID)
|
seq = c.sequence(m.UID)
|
||||||
if seq == 0 {
|
if seq == 0 {
|
||||||
// Session has not yet seen this message, and is not expecting to get a result that
|
|
||||||
// includes it.
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
s := search{c: c, tx: tx, msgCount: msgCount, seq: seq, m: m, highestUID: highestUID}
|
s := search{c: c, tx: tx, msgCount: msgCount, seq: seq, m: m, xhighestUID: xhighestUID}
|
||||||
defer func() {
|
defer func() {
|
||||||
if s.mr != nil {
|
if s.mr != nil {
|
||||||
err := s.mr.Close()
|
err := s.mr.Close()
|
||||||
@ -722,9 +741,7 @@ func (s *search) match0(sk searchKey) bool {
|
|||||||
// is likely a mistake. No mention about it in the RFC. ../rfc/7377:257
|
// is likely a mistake. No mention about it in the RFC. ../rfc/7377:257
|
||||||
xuserErrorf("cannot use search result from another mailbox")
|
xuserErrorf("cannot use search result from another mailbox")
|
||||||
}
|
}
|
||||||
match, err := sk.uidSet.containsKnownUID(s.m.UID, c.searchResult, s.highestUID)
|
return sk.uidSet.xcontainsKnownUID(s.m.UID, c.searchResult, s.xhighestUID)
|
||||||
xcheckf(err, "checking for presence in uid set")
|
|
||||||
return match
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parsed part.
|
// Parsed part.
|
||||||
|
@ -62,9 +62,17 @@ func (tc *testconn) xesearch(exp imapclient.UntaggedEsearch) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSearch(t *testing.T) {
|
func TestSearch(t *testing.T) {
|
||||||
tc := start(t)
|
testSearch(t, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSearchUIDOnly(t *testing.T) {
|
||||||
|
testSearch(t, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSearch(t *testing.T, uidonly bool) {
|
||||||
|
tc := start(t, uidonly)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc.client.Select("inbox")
|
tc.client.Select("inbox")
|
||||||
|
|
||||||
// Add 5 and delete first 4 messages. So UIDs start at 5.
|
// Add 5 and delete first 4 messages. So UIDs start at 5.
|
||||||
@ -73,7 +81,7 @@ func TestSearch(t *testing.T) {
|
|||||||
for range 5 {
|
for range 5 {
|
||||||
tc.client.Append("inbox", makeAppendTime(exampleMsg, received))
|
tc.client.Append("inbox", makeAppendTime(exampleMsg, received))
|
||||||
}
|
}
|
||||||
tc.client.StoreFlagsSet("1:4", true, `\Deleted`)
|
tc.client.UIDStoreFlagsSet("1:4", true, `\Deleted`)
|
||||||
tc.client.Expunge()
|
tc.client.Expunge()
|
||||||
|
|
||||||
received = time.Date(2022, time.January, 1, 9, 0, 0, 0, time.UTC)
|
received = time.Date(2022, time.January, 1, 9, 0, 0, 0, time.UTC)
|
||||||
@ -98,6 +106,12 @@ func TestSearch(t *testing.T) {
|
|||||||
|
|
||||||
// We now have sequence numbers 1,2,3 and UIDs 5,6,7.
|
// We now have sequence numbers 1,2,3 and UIDs 5,6,7.
|
||||||
|
|
||||||
|
if uidonly {
|
||||||
|
// We need to be selected. Not the case for ESEARCH command.
|
||||||
|
tc.client.Unselect()
|
||||||
|
tc.transactf("no", "uid search all")
|
||||||
|
tc.client.Select("inbox")
|
||||||
|
} else {
|
||||||
// We need to be selected. Not the case for ESEARCH command.
|
// We need to be selected. Not the case for ESEARCH command.
|
||||||
tc.client.Unselect()
|
tc.client.Unselect()
|
||||||
tc.transactf("no", "search all")
|
tc.transactf("no", "search all")
|
||||||
@ -105,10 +119,16 @@ func TestSearch(t *testing.T) {
|
|||||||
|
|
||||||
tc.transactf("ok", "search all")
|
tc.transactf("ok", "search all")
|
||||||
tc.xsearch(1, 2, 3)
|
tc.xsearch(1, 2, 3)
|
||||||
|
}
|
||||||
|
|
||||||
tc.transactf("ok", "uid search all")
|
tc.transactf("ok", "uid search all")
|
||||||
tc.xsearch(5, 6, 7)
|
tc.xsearch(5, 6, 7)
|
||||||
|
|
||||||
|
esearchall := func(ss string) imapclient.UntaggedEsearch {
|
||||||
|
return imapclient.UntaggedEsearch{All: esearchall0(ss)}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !uidonly {
|
||||||
tc.transactf("ok", "search answered")
|
tc.transactf("ok", "search answered")
|
||||||
tc.xsearch(3)
|
tc.xsearch(3)
|
||||||
|
|
||||||
@ -294,10 +314,6 @@ func TestSearch(t *testing.T) {
|
|||||||
)
|
)
|
||||||
inProgressPeriod = orig
|
inProgressPeriod = orig
|
||||||
|
|
||||||
esearchall := func(ss string) imapclient.UntaggedEsearch {
|
|
||||||
return imapclient.UntaggedEsearch{All: esearchall0(ss)}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do new-style ESEARCH requests with RETURN. We should get an ESEARCH response.
|
// Do new-style ESEARCH requests with RETURN. We should get an ESEARCH response.
|
||||||
tc.transactf("ok", "search return () all")
|
tc.transactf("ok", "search return () all")
|
||||||
tc.xesearch(esearchall("1:3")) // Without any options, "ALL" is implicit.
|
tc.xesearch(esearchall("1:3")) // Without any options, "ALL" is implicit.
|
||||||
@ -305,9 +321,6 @@ func TestSearch(t *testing.T) {
|
|||||||
tc.transactf("ok", "search return (min max count all) all")
|
tc.transactf("ok", "search return (min max count all) all")
|
||||||
tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 3, Count: uint32ptr(3), All: esearchall0("1:3")})
|
tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 3, Count: uint32ptr(3), All: esearchall0("1:3")})
|
||||||
|
|
||||||
tc.transactf("ok", "UID search return (min max count all) all")
|
|
||||||
tc.xesearch(imapclient.UntaggedEsearch{UID: true, Min: 5, Max: 7, Count: uint32ptr(3), All: esearchall0("5:7")})
|
|
||||||
|
|
||||||
tc.transactf("ok", "search return (min) all")
|
tc.transactf("ok", "search return (min) all")
|
||||||
tc.xesearch(imapclient.UntaggedEsearch{Min: 1})
|
tc.xesearch(imapclient.UntaggedEsearch{Min: 1})
|
||||||
|
|
||||||
@ -349,13 +362,20 @@ func TestSearch(t *testing.T) {
|
|||||||
|
|
||||||
tc.transactf("ok", "search return (min max count all) UID 5,7")
|
tc.transactf("ok", "search return (min max count all) UID 5,7")
|
||||||
tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 3, Count: uint32ptr(2), All: esearchall0("1,3")})
|
tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 3, Count: uint32ptr(2), All: esearchall0("1,3")})
|
||||||
|
}
|
||||||
|
|
||||||
|
tc.transactf("ok", "UID search return (min max count all) all")
|
||||||
|
tc.xesearch(imapclient.UntaggedEsearch{UID: true, Min: 5, Max: 7, Count: uint32ptr(3), All: esearchall0("5:7")})
|
||||||
|
|
||||||
|
if !uidonly {
|
||||||
tc.transactf("ok", "uid search return (min max count all) 1,3")
|
tc.transactf("ok", "uid search return (min max count all) 1,3")
|
||||||
tc.xesearch(imapclient.UntaggedEsearch{UID: true, Min: 5, Max: 7, Count: uint32ptr(2), All: esearchall0("5,7")})
|
tc.xesearch(imapclient.UntaggedEsearch{UID: true, Min: 5, Max: 7, Count: uint32ptr(2), All: esearchall0("5,7")})
|
||||||
|
}
|
||||||
|
|
||||||
tc.transactf("ok", "uid search return (min max count all) UID 5,7")
|
tc.transactf("ok", "uid search return (min max count all) UID 5,7")
|
||||||
tc.xesearch(imapclient.UntaggedEsearch{UID: true, Min: 5, Max: 7, Count: uint32ptr(2), All: esearchall0("5,7")})
|
tc.xesearch(imapclient.UntaggedEsearch{UID: true, Min: 5, Max: 7, Count: uint32ptr(2), All: esearchall0("5,7")})
|
||||||
|
|
||||||
|
if !uidonly {
|
||||||
tc.transactf("no", `search return () charset unknown text "mox"`)
|
tc.transactf("no", `search return () charset unknown text "mox"`)
|
||||||
tc.transactf("ok", `search return () charset us-ascii text "mox"`)
|
tc.transactf("ok", `search return () charset us-ascii text "mox"`)
|
||||||
tc.xesearch(esearchall("2:3"))
|
tc.xesearch(esearchall("2:3"))
|
||||||
@ -367,7 +387,7 @@ func TestSearch(t *testing.T) {
|
|||||||
tc.transactf("ok", "search return (save) 2")
|
tc.transactf("ok", "search return (save) 2")
|
||||||
tc.xnountagged() // ../rfc/9051:3800
|
tc.xnountagged() // ../rfc/9051:3800
|
||||||
tc.transactf("ok", "fetch $ (uid)")
|
tc.transactf("ok", "fetch $ (uid)")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6)}})
|
tc.xuntagged(tc.untaggedFetch(2, 6))
|
||||||
|
|
||||||
tc.transactf("ok", "search return (all) $")
|
tc.transactf("ok", "search return (all) $")
|
||||||
tc.xesearch(esearchall("2"))
|
tc.xesearch(esearchall("2"))
|
||||||
@ -384,12 +404,16 @@ func TestSearch(t *testing.T) {
|
|||||||
tc.transactf("ok", "search return (min save) all")
|
tc.transactf("ok", "search return (min save) all")
|
||||||
tc.xesearch(imapclient.UntaggedEsearch{Min: 1})
|
tc.xesearch(imapclient.UntaggedEsearch{Min: 1})
|
||||||
tc.transactf("ok", "fetch $ (uid)")
|
tc.transactf("ok", "fetch $ (uid)")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5)}})
|
tc.xuntagged(tc.untaggedFetch(1, 5))
|
||||||
|
}
|
||||||
|
|
||||||
// Do a seemingly old-style search command with IMAP4rev2 enabled. We'll still get ESEARCH responses.
|
// Do a seemingly old-style search command with IMAP4rev2 enabled. We'll still get ESEARCH responses.
|
||||||
tc.client.Enable("IMAP4rev2")
|
tc.client.Enable("IMAP4rev2")
|
||||||
|
|
||||||
|
if !uidonly {
|
||||||
tc.transactf("ok", `search undraft`)
|
tc.transactf("ok", `search undraft`)
|
||||||
tc.xesearch(esearchall("1:2"))
|
tc.xesearch(esearchall("1:2"))
|
||||||
|
}
|
||||||
|
|
||||||
// Long commands should be rejected, not allocating too much memory.
|
// Long commands should be rejected, not allocating too much memory.
|
||||||
lit := make([]byte, 100*1024+1)
|
lit := make([]byte, 100*1024+1)
|
||||||
@ -470,21 +494,27 @@ func esearchall0(ss string) imapclient.NumSet {
|
|||||||
return seqset
|
return seqset
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test the MULTISEARCH extension. Where we don't need to have a mailbox selected,
|
func TestSearchMultiUnselected(t *testing.T) {
|
||||||
// operating without messag sequence numbers, and return untagged esearch responses
|
testSearchMulti(t, false, false)
|
||||||
// that include the mailbox and uidvalidity.
|
|
||||||
func TestSearchMulti(t *testing.T) {
|
|
||||||
testSearchMulti(t, false)
|
|
||||||
testSearchMulti(t, true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run multisearch tests with or without a mailbox selected.
|
func TestSearchMultiSelected(t *testing.T) {
|
||||||
func testSearchMulti(t *testing.T, selected bool) {
|
testSearchMulti(t, true, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSearchMultiSelectedUIDOnly(t *testing.T) {
|
||||||
|
testSearchMulti(t, true, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the MULTISEARCH extension, with and without selected mailbx. Operating
|
||||||
|
// without messag sequence numbers, and return untagged esearch responses that
|
||||||
|
// include the mailbox and uidvalidity.
|
||||||
|
func testSearchMulti(t *testing.T, selected, uidonly bool) {
|
||||||
defer mockUIDValidity()()
|
defer mockUIDValidity()()
|
||||||
|
|
||||||
tc := start(t)
|
tc := start(t, uidonly)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc.client.Select("inbox")
|
tc.client.Select("inbox")
|
||||||
|
|
||||||
// Add 5 messages to Inbox and delete first 4 messages. So UIDs start at 5.
|
// Add 5 messages to Inbox and delete first 4 messages. So UIDs start at 5.
|
||||||
@ -492,7 +522,7 @@ func testSearchMulti(t *testing.T, selected bool) {
|
|||||||
for range 6 {
|
for range 6 {
|
||||||
tc.client.Append("inbox", makeAppendTime(exampleMsg, received))
|
tc.client.Append("inbox", makeAppendTime(exampleMsg, received))
|
||||||
}
|
}
|
||||||
tc.client.StoreFlagsSet("1:4", true, `\Deleted`)
|
tc.client.UIDStoreFlagsSet("1:4", true, `\Deleted`)
|
||||||
tc.client.Expunge()
|
tc.client.Expunge()
|
||||||
|
|
||||||
// Unselecting mailbox, esearch works in authenticated state.
|
// Unselecting mailbox, esearch works in authenticated state.
|
||||||
@ -681,12 +711,15 @@ func testSearchMulti(t *testing.T, selected bool) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Search with sequence set also for non-selected mailboxes(!). The min/max would
|
// Search with sequence set also for non-selected mailboxes(!). The min/max would
|
||||||
// get the first and last message, but the message sequence set forces a scan.
|
// get the first and last message, but the message sequence set forces a scan. Not
|
||||||
|
// allowed with UIDONLY.
|
||||||
|
if !uidonly {
|
||||||
tc.cmdf("Tag1", `Esearch In (Mailboxes Inbox) Return (Min Max) 1:*`)
|
tc.cmdf("Tag1", `Esearch In (Mailboxes Inbox) Return (Min Max) 1:*`)
|
||||||
tc.response("ok")
|
tc.response("ok")
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Min: 5, Max: 7},
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Min: 5, Max: 7},
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Search with uid set with "$highnum:*" forces getting highest uid.
|
// Search with uid set with "$highnum:*" forces getting highest uid.
|
||||||
tc.cmdf("Tag1", `Esearch In (Mailboxes Inbox) Return (Min Max) Uid *:100`)
|
tc.cmdf("Tag1", `Esearch In (Mailboxes Inbox) Return (Min Max) Uid *:100`)
|
||||||
@ -709,9 +742,9 @@ func testSearchMulti(t *testing.T, selected bool) {
|
|||||||
// with Inbox selected will not return the new message since it isn't available in
|
// with Inbox selected will not return the new message since it isn't available in
|
||||||
// the session yet. The message in Archive is returned, since there is no session
|
// the session yet. The message in Archive is returned, since there is no session
|
||||||
// limitation.
|
// limitation.
|
||||||
tc2 := startNoSwitchboard(t)
|
tc2 := startNoSwitchboard(t, uidonly)
|
||||||
defer tc2.closeNoWait()
|
defer tc2.closeNoWait()
|
||||||
tc2.client.Login("mjl@mox.example", password0)
|
tc2.login("mjl@mox.example", password0)
|
||||||
tc2.client.Append("inbox", makeAppendTime(searchMsg, received))
|
tc2.client.Append("inbox", makeAppendTime(searchMsg, received))
|
||||||
tc2.client.Append("Archive", makeAppendTime(searchMsg, received))
|
tc2.client.Append("Archive", makeAppendTime(searchMsg, received))
|
||||||
|
|
||||||
@ -722,7 +755,7 @@ func testSearchMulti(t *testing.T, selected bool) {
|
|||||||
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Count: uint32ptr(3)},
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Count: uint32ptr(3)},
|
||||||
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Archive", UIDValidity: 1, UID: true, Count: uint32ptr(2)},
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Archive", UIDValidity: 1, UID: true, Count: uint32ptr(2)},
|
||||||
imapclient.UntaggedExists(4),
|
imapclient.UntaggedExists(4),
|
||||||
imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(8), imapclient.FetchFlags(nil)}},
|
tc.untaggedFetch(4, 8, imapclient.FetchFlags(nil)),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
|
@ -8,20 +8,28 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestSelect(t *testing.T) {
|
func TestSelect(t *testing.T) {
|
||||||
testSelectExamine(t, false)
|
testSelectExamine(t, false, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExamine(t *testing.T) {
|
func TestExamine(t *testing.T) {
|
||||||
testSelectExamine(t, true)
|
testSelectExamine(t, true, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSelectUIDOnly(t *testing.T) {
|
||||||
|
testSelectExamine(t, false, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExamineUIDOnly(t *testing.T) {
|
||||||
|
testSelectExamine(t, true, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// select and examine are pretty much the same. but examine opens readonly instead of readwrite.
|
// select and examine are pretty much the same. but examine opens readonly instead of readwrite.
|
||||||
func testSelectExamine(t *testing.T, examine bool) {
|
func testSelectExamine(t *testing.T, examine, uidonly bool) {
|
||||||
defer mockUIDValidity()()
|
defer mockUIDValidity()()
|
||||||
tc := start(t)
|
tc := start(t, uidonly)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
|
|
||||||
cmd := "select"
|
cmd := "select"
|
||||||
okcode := "READ-WRITE"
|
okcode := "READ-WRITE"
|
||||||
@ -62,7 +70,11 @@ func testSelectExamine(t *testing.T, examine bool) {
|
|||||||
// Append a message. It will be reported as UNSEEN.
|
// Append a message. It will be reported as UNSEEN.
|
||||||
tc.client.Append("inbox", makeAppend(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
tc.transactf("ok", "%s inbox", cmd)
|
tc.transactf("ok", "%s inbox", cmd)
|
||||||
|
if uidonly {
|
||||||
|
tc.xuntagged(uclosed, uflags, upermflags, urecent, uexists1, uuidval1, uuidnext2, ulist)
|
||||||
|
} else {
|
||||||
tc.xuntagged(uclosed, uflags, upermflags, urecent, uunseen, uexists1, uuidval1, uuidnext2, ulist)
|
tc.xuntagged(uclosed, uflags, upermflags, urecent, uunseen, uexists1, uuidval1, uuidnext2, ulist)
|
||||||
|
}
|
||||||
tc.xcode(okcode)
|
tc.xcode(okcode)
|
||||||
|
|
||||||
// With imap4rev2, we no longer get untagged RECENT or untagged UNSEEN.
|
// With imap4rev2, we no longer get untagged RECENT or untagged UNSEEN.
|
||||||
|
@ -184,6 +184,7 @@ var serverCapabilities = strings.Join([]string{
|
|||||||
"INPROGRESS", // ../rfc/9585:101
|
"INPROGRESS", // ../rfc/9585:101
|
||||||
"MULTISEARCH", // ../rfc/7377:187
|
"MULTISEARCH", // ../rfc/7377:187
|
||||||
"NOTIFY", // ../rfc/5465:195
|
"NOTIFY", // ../rfc/5465:195
|
||||||
|
"UIDONLY", // ../rfc/9586:127
|
||||||
// "COMPRESS=DEFLATE", // ../rfc/4978, disabled for interoperability issues: The flate reader (inflate) still blocks on partial flushes, preventing progress.
|
// "COMPRESS=DEFLATE", // ../rfc/4978, disabled for interoperability issues: The flate reader (inflate) still blocks on partial flushes, preventing progress.
|
||||||
}, " ")
|
}, " ")
|
||||||
|
|
||||||
@ -244,6 +245,9 @@ type conn struct {
|
|||||||
|
|
||||||
mailboxID int64 // Only for StateSelected.
|
mailboxID int64 // Only for StateSelected.
|
||||||
readonly bool // If opened mailbox is readonly.
|
readonly bool // If opened mailbox is readonly.
|
||||||
|
uidonly bool // If uidonly is enabled, uids is empty and cannot be used.
|
||||||
|
uidnext store.UID // We don't return search/fetch/etc results for uids >= uidnext, which is updated when applying changes.
|
||||||
|
exists uint32 // Needed for uidonly, equal to len(uids) for non-uidonly sessions.
|
||||||
uids []store.UID // UIDs known in this session, sorted. todo future: store more space-efficiently, as ranges.
|
uids []store.UID // UIDs known in this session, sorted. todo future: store more space-efficiently, as ranges.
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,6 +262,7 @@ const (
|
|||||||
capCondstore capability = "CONDSTORE"
|
capCondstore capability = "CONDSTORE"
|
||||||
capQresync capability = "QRESYNC"
|
capQresync capability = "QRESYNC"
|
||||||
capMetadata capability = "METADATA"
|
capMetadata capability = "METADATA"
|
||||||
|
capUIDOnly capability = "UIDONLY"
|
||||||
)
|
)
|
||||||
|
|
||||||
type lineErr struct {
|
type lineErr struct {
|
||||||
@ -288,6 +293,10 @@ var (
|
|||||||
commandsStateSelected = stateCommands("close", "unselect", "expunge", "search", "fetch", "store", "copy", "move", "uid expunge", "uid search", "uid fetch", "uid store", "uid copy", "uid move", "replace", "uid replace", "esearch")
|
commandsStateSelected = stateCommands("close", "unselect", "expunge", "search", "fetch", "store", "copy", "move", "uid expunge", "uid search", "uid fetch", "uid store", "uid copy", "uid move", "replace", "uid replace", "esearch")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Commands that use sequence numbers. Cannot be used when UIDONLY is enabled.
|
||||||
|
// Commands like UID SEARCH have additional checks for some parameters.
|
||||||
|
var commandsSequence = stateCommands("search", "fetch", "store", "copy", "move", "replace")
|
||||||
|
|
||||||
var commands = map[string]func(c *conn, tag, cmd string, p *parser){
|
var commands = map[string]func(c *conn, tag, cmd string, p *parser){
|
||||||
// Any state.
|
// Any state.
|
||||||
"capability": (*conn).cmdCapability,
|
"capability": (*conn).cmdCapability,
|
||||||
@ -499,6 +508,8 @@ func (c *conn) unselect() {
|
|||||||
c.state = stateAuthenticated
|
c.state = stateAuthenticated
|
||||||
}
|
}
|
||||||
c.mailboxID = 0
|
c.mailboxID = 0
|
||||||
|
c.uidnext = 0
|
||||||
|
c.exists = 0
|
||||||
c.uids = nil
|
c.uids = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1343,6 +1354,11 @@ func (c *conn) command() {
|
|||||||
xserverErrorf("unrecognized command")
|
xserverErrorf("unrecognized command")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ../rfc/9586:172
|
||||||
|
if _, ok := commandsSequence[cmdlow]; ok && c.uidonly {
|
||||||
|
xsyntaxCodeErrorf("UIDREQUIRED", "cannot use message sequence numbers with uidonly")
|
||||||
|
}
|
||||||
|
|
||||||
fn(c, tag, cmd, p)
|
fn(c, tag, cmd, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1414,6 +1430,9 @@ func xmailboxPatternMatcher(ref string, patterns []string) matchStringer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *conn) sequence(uid store.UID) msgseq {
|
func (c *conn) sequence(uid store.UID) msgseq {
|
||||||
|
if c.uidonly {
|
||||||
|
panic("sequence with uidonly")
|
||||||
|
}
|
||||||
return uidSearch(c.uids, uid)
|
return uidSearch(c.uids, uid)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1435,6 +1454,9 @@ func uidSearch(uids []store.UID, uid store.UID) msgseq {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *conn) xsequence(uid store.UID) msgseq {
|
func (c *conn) xsequence(uid store.UID) msgseq {
|
||||||
|
if c.uidonly {
|
||||||
|
panic("xsequence with uidonly")
|
||||||
|
}
|
||||||
seq := c.sequence(uid)
|
seq := c.sequence(uid)
|
||||||
if seq <= 0 {
|
if seq <= 0 {
|
||||||
xserverErrorf("unknown uid %d (%w)", uid, errProtocol)
|
xserverErrorf("unknown uid %d (%w)", uid, errProtocol)
|
||||||
@ -1443,36 +1465,55 @@ func (c *conn) xsequence(uid store.UID) msgseq {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *conn) sequenceRemove(seq msgseq, uid store.UID) {
|
func (c *conn) sequenceRemove(seq msgseq, uid store.UID) {
|
||||||
|
if c.uidonly {
|
||||||
|
panic("sequenceRemove with uidonly")
|
||||||
|
}
|
||||||
i := seq - 1
|
i := seq - 1
|
||||||
if c.uids[i] != uid {
|
if c.uids[i] != uid {
|
||||||
xserverErrorf("got uid %d at msgseq %d, expected uid %d", uid, seq, c.uids[i])
|
xserverErrorf("got uid %d at msgseq %d, expected uid %d", uid, seq, c.uids[i])
|
||||||
}
|
}
|
||||||
copy(c.uids[i:], c.uids[i+1:])
|
copy(c.uids[i:], c.uids[i+1:])
|
||||||
c.uids = c.uids[:len(c.uids)-1]
|
c.uids = c.uids[:c.exists-1]
|
||||||
if sanityChecks {
|
c.exists--
|
||||||
checkUIDs(c.uids)
|
c.checkUIDs(c.uids, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add uid to session, through c.uidnext, and if uidonly isn't enabled to c.uids.
|
||||||
|
// care must be taken that pending changes are fetched while holding the account
|
||||||
|
// wlock, and applied before adding this uid, because those pending changes may
|
||||||
|
// contain another new uid that has to be added first.
|
||||||
|
func (c *conn) uidAppend(uid store.UID) {
|
||||||
|
if c.uidonly {
|
||||||
|
if uid < c.uidnext {
|
||||||
|
panic(fmt.Sprintf("new uid %d < uidnext %d", uid, c.uidnext))
|
||||||
|
}
|
||||||
|
c.exists++
|
||||||
|
c.uidnext = uid + 1
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// add uid to the session. care must be taken that pending changes are fetched
|
|
||||||
// while holding the account wlock, and applied before adding this uid, because
|
|
||||||
// those pending changes may contain another new uid that has to be added first.
|
|
||||||
func (c *conn) uidAppend(uid store.UID) msgseq {
|
|
||||||
if uidSearch(c.uids, uid) > 0 {
|
if uidSearch(c.uids, uid) > 0 {
|
||||||
xserverErrorf("uid already present (%w)", errProtocol)
|
xserverErrorf("uid already present (%w)", errProtocol)
|
||||||
}
|
}
|
||||||
if len(c.uids) > 0 && uid < c.uids[len(c.uids)-1] {
|
if c.exists > 0 && uid < c.uids[c.exists-1] {
|
||||||
xserverErrorf("new uid %d is smaller than last uid %d (%w)", uid, c.uids[len(c.uids)-1], errProtocol)
|
xserverErrorf("new uid %d is smaller than last uid %d (%w)", uid, c.uids[c.exists-1], errProtocol)
|
||||||
}
|
}
|
||||||
|
c.exists++
|
||||||
|
c.uidnext = uid + 1
|
||||||
c.uids = append(c.uids, uid)
|
c.uids = append(c.uids, uid)
|
||||||
if sanityChecks {
|
c.checkUIDs(c.uids, true)
|
||||||
checkUIDs(c.uids)
|
|
||||||
}
|
|
||||||
return msgseq(len(c.uids))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// sanity check that uids are in ascending order.
|
// sanity check that uids are in ascending order.
|
||||||
func checkUIDs(uids []store.UID) {
|
func (c *conn) checkUIDs(uids []store.UID, checkExists bool) {
|
||||||
|
if !sanityChecks {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if checkExists && uint32(len(uids)) != c.exists {
|
||||||
|
panic(fmt.Sprintf("exists %d does not match len(uids) %d", c.exists, len(c.uids)))
|
||||||
|
}
|
||||||
|
|
||||||
for i, uid := range uids {
|
for i, uid := range uids {
|
||||||
if uid == 0 || i > 0 && uid <= uids[i-1] {
|
if uid == 0 || i > 0 && uid <= uids[i-1] {
|
||||||
xserverErrorf("bad uids %v", uids)
|
xserverErrorf("bad uids %v", uids)
|
||||||
@ -1480,20 +1521,82 @@ func checkUIDs(uids []store.UID) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *conn) xnumSetUIDs(isUID bool, nums numSet) []store.UID {
|
func slicesAny[T any](l []T) []any {
|
||||||
_, uids := c.xnumSetConditionUIDs(false, true, isUID, nums)
|
r := make([]any, len(l))
|
||||||
return uids
|
for i, v := range l {
|
||||||
|
r[i] = v
|
||||||
|
}
|
||||||
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *conn) xnumSetCondition(isUID bool, nums numSet) []any {
|
// newCachedLastUID returns a method that returns the highest uid for a mailbox,
|
||||||
uidargs, _ := c.xnumSetConditionUIDs(true, false, isUID, nums)
|
// for interpretation of "*". If mailboxID is for the selected mailbox, the UIDs
|
||||||
return uidargs
|
// visible in the session are taken into account. If there is no UID, 0 is
|
||||||
|
// returned. If an error occurs, xerrfn is called, which should not return.
|
||||||
|
func (c *conn) newCachedLastUID(tx *bstore.Tx, mailboxID int64, xerrfn func(err error)) func() store.UID {
|
||||||
|
var last store.UID
|
||||||
|
var have bool
|
||||||
|
return func() store.UID {
|
||||||
|
if have {
|
||||||
|
return last
|
||||||
|
}
|
||||||
|
if c.mailboxID == mailboxID {
|
||||||
|
if c.exists == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if !c.uidonly {
|
||||||
|
return c.uids[c.exists-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
q := bstore.QueryTx[store.Message](tx)
|
||||||
|
q.FilterNonzero(store.Message{MailboxID: mailboxID})
|
||||||
|
q.FilterEqual("Expunged", false)
|
||||||
|
if c.mailboxID == mailboxID {
|
||||||
|
q.FilterLess("UID", c.uidnext)
|
||||||
|
}
|
||||||
|
q.SortDesc("UID")
|
||||||
|
q.Limit(1)
|
||||||
|
m, err := q.Get()
|
||||||
|
if err == bstore.ErrAbsent {
|
||||||
|
have = true
|
||||||
|
return last
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
xerrfn(err)
|
||||||
|
panic(err) // xerrfn should have called panic.
|
||||||
|
}
|
||||||
|
have = true
|
||||||
|
last = m.UID
|
||||||
|
return last
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *conn) xnumSetConditionUIDs(forDB, returnUIDs bool, isUID bool, nums numSet) ([]any, []store.UID) {
|
// xnumSetEval evaluates nums to uids given the current session state and messages
|
||||||
|
// in the selected mailbox. The returned UIDs are sorted, without duplicates.
|
||||||
|
func (c *conn) xnumSetEval(tx *bstore.Tx, isUID bool, nums numSet) []store.UID {
|
||||||
if nums.searchResult {
|
if nums.searchResult {
|
||||||
|
// UIDs that do not exist can be ignored.
|
||||||
|
if c.exists == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Update previously stored UIDs. Some may have been deleted.
|
// Update previously stored UIDs. Some may have been deleted.
|
||||||
// Once deleted a UID will never come back, so we'll just remove those uids.
|
// Once deleted a UID will never come back, so we'll just remove those uids.
|
||||||
|
if c.uidonly {
|
||||||
|
var uids []store.UID
|
||||||
|
if len(c.searchResult) > 0 {
|
||||||
|
q := bstore.QueryTx[store.Message](tx)
|
||||||
|
q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
|
||||||
|
q.FilterEqual("Expunged", false)
|
||||||
|
q.FilterEqual("UID", slicesAny(c.searchResult)...)
|
||||||
|
q.SortAsc("UID")
|
||||||
|
for m, err := range q.All() {
|
||||||
|
xcheckf(err, "looking up messages from search result")
|
||||||
|
uids = append(uids, m.UID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.searchResult = uids
|
||||||
|
} else {
|
||||||
o := 0
|
o := 0
|
||||||
for _, uid := range c.searchResult {
|
for _, uid := range c.searchResult {
|
||||||
if uidSearch(c.uids, uid) > 0 {
|
if uidSearch(c.uids, uid) > 0 {
|
||||||
@ -1502,53 +1605,37 @@ func (c *conn) xnumSetConditionUIDs(forDB, returnUIDs bool, isUID bool, nums num
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.searchResult = c.searchResult[:o]
|
c.searchResult = c.searchResult[:o]
|
||||||
uidargs := make([]any, len(c.searchResult))
|
|
||||||
for i, uid := range c.searchResult {
|
|
||||||
uidargs[i] = uid
|
|
||||||
}
|
|
||||||
return uidargs, c.searchResult
|
|
||||||
}
|
|
||||||
|
|
||||||
var uidargs []any
|
|
||||||
var uids []store.UID
|
|
||||||
|
|
||||||
add := func(uid store.UID) {
|
|
||||||
if forDB {
|
|
||||||
uidargs = append(uidargs, uid)
|
|
||||||
}
|
|
||||||
if returnUIDs {
|
|
||||||
uids = append(uids, uid)
|
|
||||||
}
|
}
|
||||||
|
return c.searchResult
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isUID {
|
if !isUID {
|
||||||
|
uids := map[store.UID]struct{}{}
|
||||||
|
|
||||||
// Sequence numbers that don't exist, or * on an empty mailbox, should result in a BAD response. ../rfc/9051:7018
|
// Sequence numbers that don't exist, or * on an empty mailbox, should result in a BAD response. ../rfc/9051:7018
|
||||||
for _, r := range nums.ranges {
|
for _, r := range nums.ranges {
|
||||||
var ia, ib int
|
var ia, ib int
|
||||||
if r.first.star {
|
if r.first.star {
|
||||||
if len(c.uids) == 0 {
|
if c.exists == 0 {
|
||||||
xsyntaxErrorf("invalid seqset * on empty mailbox")
|
xsyntaxErrorf("invalid seqset * on empty mailbox")
|
||||||
}
|
}
|
||||||
ia = len(c.uids) - 1
|
ia = int(c.exists) - 1
|
||||||
} else {
|
} else {
|
||||||
ia = int(r.first.number - 1)
|
ia = int(r.first.number - 1)
|
||||||
if ia >= len(c.uids) {
|
if ia >= int(c.exists) {
|
||||||
xsyntaxErrorf("msgseq %d not in mailbox", r.first.number)
|
xsyntaxErrorf("msgseq %d not in mailbox", r.first.number)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if r.last == nil {
|
if r.last == nil {
|
||||||
add(c.uids[ia])
|
uids[c.uids[ia]] = struct{}{}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.last.star {
|
if r.last.star {
|
||||||
if len(c.uids) == 0 {
|
ib = int(c.exists) - 1
|
||||||
xsyntaxErrorf("invalid seqset * on empty mailbox")
|
|
||||||
}
|
|
||||||
ib = len(c.uids) - 1
|
|
||||||
} else {
|
} else {
|
||||||
ib = int(r.last.number - 1)
|
ib = int(r.last.number - 1)
|
||||||
if ib >= len(c.uids) {
|
if ib >= int(c.exists) {
|
||||||
xsyntaxErrorf("msgseq %d not in mailbox", r.last.number)
|
xsyntaxErrorf("msgseq %d not in mailbox", r.last.number)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1556,15 +1643,39 @@ func (c *conn) xnumSetConditionUIDs(forDB, returnUIDs bool, isUID bool, nums num
|
|||||||
ia, ib = ib, ia
|
ia, ib = ib, ia
|
||||||
}
|
}
|
||||||
for _, uid := range c.uids[ia : ib+1] {
|
for _, uid := range c.uids[ia : ib+1] {
|
||||||
add(uid)
|
uids[uid] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return uidargs, uids
|
return slices.Sorted(maps.Keys(uids))
|
||||||
}
|
}
|
||||||
|
|
||||||
// UIDs that do not exist can be ignored.
|
// UIDs that do not exist can be ignored.
|
||||||
if len(c.uids) == 0 {
|
if c.exists == 0 {
|
||||||
return nil, nil
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
uids := map[store.UID]struct{}{}
|
||||||
|
|
||||||
|
if c.uidonly {
|
||||||
|
xlastUID := c.newCachedLastUID(tx, c.mailboxID, func(xerr error) { xuserErrorf("%s", xerr) })
|
||||||
|
for _, r := range nums.xinterpretStar(xlastUID).ranges {
|
||||||
|
q := bstore.QueryTx[store.Message](tx)
|
||||||
|
q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
|
||||||
|
q.FilterEqual("Expunged", false)
|
||||||
|
if r.last == nil {
|
||||||
|
q.FilterEqual("UID", r.first.number)
|
||||||
|
} else {
|
||||||
|
q.FilterGreaterEqual("UID", r.first.number)
|
||||||
|
q.FilterLessEqual("UID", r.last.number)
|
||||||
|
}
|
||||||
|
q.FilterLess("UID", c.uidnext)
|
||||||
|
q.SortAsc("UID")
|
||||||
|
for m, err := range q.All() {
|
||||||
|
xcheckf(err, "enumerating uids")
|
||||||
|
uids[m.UID] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return slices.Sorted(maps.Keys(uids))
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, r := range nums.ranges {
|
for _, r := range nums.ranges {
|
||||||
@ -1575,12 +1686,12 @@ func (c *conn) xnumSetConditionUIDs(forDB, returnUIDs bool, isUID bool, nums num
|
|||||||
|
|
||||||
uida := store.UID(r.first.number)
|
uida := store.UID(r.first.number)
|
||||||
if r.first.star {
|
if r.first.star {
|
||||||
uida = c.uids[len(c.uids)-1]
|
uida = c.uids[c.exists-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
uidb := store.UID(last.number)
|
uidb := store.UID(last.number)
|
||||||
if last.star {
|
if last.star {
|
||||||
uidb = c.uids[len(c.uids)-1]
|
uidb = c.uids[c.exists-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
if uida > uidb {
|
if uida > uidb {
|
||||||
@ -1589,7 +1700,7 @@ func (c *conn) xnumSetConditionUIDs(forDB, returnUIDs bool, isUID bool, nums num
|
|||||||
|
|
||||||
// Binary search for uida.
|
// Binary search for uida.
|
||||||
s := 0
|
s := 0
|
||||||
e := len(c.uids)
|
e := int(c.exists)
|
||||||
for s < e {
|
for s < e {
|
||||||
m := (s + e) / 2
|
m := (s + e) / 2
|
||||||
if uida < c.uids[m] {
|
if uida < c.uids[m] {
|
||||||
@ -1603,14 +1714,13 @@ func (c *conn) xnumSetConditionUIDs(forDB, returnUIDs bool, isUID bool, nums num
|
|||||||
|
|
||||||
for _, uid := range c.uids[s:] {
|
for _, uid := range c.uids[s:] {
|
||||||
if uid >= uida && uid <= uidb {
|
if uid >= uida && uid <= uidb {
|
||||||
add(uid)
|
uids[uid] = struct{}{}
|
||||||
} else if uid > uidb {
|
} else if uid > uidb {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return slices.Sorted(maps.Keys(uids))
|
||||||
return uidargs, uids
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *conn) ok(tag, cmd string) {
|
func (c *conn) ok(tag, cmd string) {
|
||||||
@ -1751,8 +1861,7 @@ func (c *conn) xapplyChanges(overflow bool, changes []store.Change, initial, sen
|
|||||||
if !ok {
|
if !ok {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
seq := c.sequence(ch.UID)
|
if initial && !c.uidonly && c.sequence(ch.UID) > 0 {
|
||||||
if seq > 0 && initial {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
c.uidAppend(ch.UID)
|
c.uidAppend(ch.UID)
|
||||||
@ -1765,16 +1874,20 @@ func (c *conn) xapplyChanges(overflow bool, changes []store.Change, initial, sen
|
|||||||
// Write the exists, and the UID and flags as well. Hopefully the client waits for
|
// Write the exists, and the UID and flags as well. Hopefully the client waits for
|
||||||
// long enough after the EXISTS to see these messages, and doesn't request them
|
// long enough after the EXISTS to see these messages, and doesn't request them
|
||||||
// again with a FETCH.
|
// again with a FETCH.
|
||||||
c.xbwritelinef("* %d EXISTS", len(c.uids))
|
c.xbwritelinef("* %d EXISTS", c.exists)
|
||||||
for _, add := range adds {
|
for _, add := range adds {
|
||||||
seq := c.xsequence(add.UID)
|
|
||||||
var modseqStr string
|
var modseqStr string
|
||||||
if condstore {
|
if condstore {
|
||||||
modseqStr = fmt.Sprintf(" MODSEQ (%d)", add.ModSeq.Client())
|
modseqStr = fmt.Sprintf(" MODSEQ (%d)", add.ModSeq.Client())
|
||||||
}
|
}
|
||||||
|
// UIDFETCH in case of uidonly. ../rfc/9586:228
|
||||||
|
if c.uidonly {
|
||||||
|
c.xbwritelinef("* %d UIDFETCH (FLAGS %s%s)", add.UID, flaglist(add.Flags, add.Keywords).pack(c), modseqStr)
|
||||||
|
} else {
|
||||||
|
seq := c.xsequence(add.UID)
|
||||||
c.xbwritelinef("* %d FETCH (UID %d FLAGS %s%s)", seq, add.UID, flaglist(add.Flags, add.Keywords).pack(c), modseqStr)
|
c.xbwritelinef("* %d FETCH (UID %d FLAGS %s%s)", seq, add.UID, flaglist(add.Flags, add.Keywords).pack(c), modseqStr)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1785,6 +1898,15 @@ func (c *conn) xapplyChanges(overflow bool, changes []store.Change, initial, sen
|
|||||||
case store.ChangeRemoveUIDs:
|
case store.ChangeRemoveUIDs:
|
||||||
var vanishedUIDs numSet
|
var vanishedUIDs numSet
|
||||||
for _, uid := range ch.UIDs {
|
for _, uid := range ch.UIDs {
|
||||||
|
// With uidonly, we must always return VANISHED. ../rfc/9586:232
|
||||||
|
if c.uidonly {
|
||||||
|
if !initial {
|
||||||
|
c.exists--
|
||||||
|
vanishedUIDs.append(uint32(uid))
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
var seq msgseq
|
var seq msgseq
|
||||||
if initial {
|
if initial {
|
||||||
seq = c.sequence(uid)
|
seq = c.sequence(uid)
|
||||||
@ -1803,7 +1925,7 @@ func (c *conn) xapplyChanges(overflow bool, changes []store.Change, initial, sen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if qresync {
|
if !vanishedUIDs.empty() {
|
||||||
// VANISHED without EARLIER. ../rfc/7162:2004
|
// VANISHED without EARLIER. ../rfc/7162:2004
|
||||||
for _, s := range vanishedUIDs.Strings(4*1024 - 32) {
|
for _, s := range vanishedUIDs.Strings(4*1024 - 32) {
|
||||||
c.xbwritelinef("* VANISHED %s", s)
|
c.xbwritelinef("* VANISHED %s", s)
|
||||||
@ -1811,16 +1933,22 @@ func (c *conn) xapplyChanges(overflow bool, changes []store.Change, initial, sen
|
|||||||
}
|
}
|
||||||
|
|
||||||
case store.ChangeFlags:
|
case store.ChangeFlags:
|
||||||
|
if initial {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var modseqStr string
|
||||||
|
if condstore {
|
||||||
|
modseqStr = fmt.Sprintf(" MODSEQ (%d)", ch.ModSeq.Client())
|
||||||
|
}
|
||||||
|
// UIDFETCH in case of uidonly. ../rfc/9586:228
|
||||||
|
if c.uidonly {
|
||||||
|
c.xbwritelinef("* %d UIDFETCH (FLAGS %s%s)", ch.UID, flaglist(ch.Flags, ch.Keywords).pack(c), modseqStr)
|
||||||
|
} else {
|
||||||
// The uid can be unknown if we just expunged it while another session marked it as deleted just before.
|
// The uid can be unknown if we just expunged it while another session marked it as deleted just before.
|
||||||
seq := c.sequence(ch.UID)
|
seq := c.sequence(ch.UID)
|
||||||
if seq <= 0 {
|
if seq <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !initial {
|
|
||||||
var modseqStr string
|
|
||||||
if condstore {
|
|
||||||
modseqStr = fmt.Sprintf(" MODSEQ (%d)", ch.ModSeq.Client())
|
|
||||||
}
|
|
||||||
c.xbwritelinef("* %d FETCH (UID %d FLAGS %s%s)", seq, ch.UID, flaglist(ch.Flags, ch.Keywords).pack(c), modseqStr)
|
c.xbwritelinef("* %d FETCH (UID %d FLAGS %s%s)", seq, ch.UID, flaglist(ch.Flags, ch.Keywords).pack(c), modseqStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1969,10 +2097,10 @@ func (c *conn) xapplyChangesNotify(changes []store.Change, sendDelayed bool) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
seq := c.uidAppend(ch.UID)
|
c.uidAppend(ch.UID)
|
||||||
|
|
||||||
// ../rfc/5465:515
|
// ../rfc/5465:515
|
||||||
c.xbwritelinef("* %d EXISTS", len(c.uids))
|
c.xbwritelinef("* %d EXISTS", c.exists)
|
||||||
|
|
||||||
// If client did not specify attributes, we'll send the defaults.
|
// If client did not specify attributes, we'll send the defaults.
|
||||||
if len(ev.FetchAtt) == 0 {
|
if len(ev.FetchAtt) == 0 {
|
||||||
@ -1982,7 +2110,12 @@ func (c *conn) xapplyChangesNotify(changes []store.Change, sendDelayed bool) {
|
|||||||
}
|
}
|
||||||
// NOTIFY does not specify the default fetch attributes to return, we send UID and
|
// NOTIFY does not specify the default fetch attributes to return, we send UID and
|
||||||
// FLAGS.
|
// FLAGS.
|
||||||
c.xbwritelinef("* %d FETCH (UID %d FLAGS %s%s)", seq, ch.UID, flaglist(ch.Flags, ch.Keywords).pack(c), modseqStr)
|
// UIDFETCH in case of uidonly. ../rfc/9586:228
|
||||||
|
if c.uidonly {
|
||||||
|
c.xbwritelinef("* %d UIDFETCH (FLAGS %s%s)", ch.UID, flaglist(ch.Flags, ch.Keywords).pack(c), modseqStr)
|
||||||
|
} else {
|
||||||
|
c.xbwritelinef("* %d FETCH (UID %d FLAGS %s%s)", c.xsequence(ch.UID), ch.UID, flaglist(ch.Flags, ch.Keywords).pack(c), modseqStr)
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1995,9 +2128,15 @@ func (c *conn) xapplyChangesNotify(changes []store.Change, sendDelayed bool) {
|
|||||||
// developer sees the message.
|
// developer sees the message.
|
||||||
c.log.Errorx("generating notify fetch response", err, slog.Int64("mailboxid", ch.MailboxID), slog.Any("uid", ch.UID))
|
c.log.Errorx("generating notify fetch response", err, slog.Int64("mailboxid", ch.MailboxID), slog.Any("uid", ch.UID))
|
||||||
c.xbwritelinef("* NO generating notify fetch response: %s", err.Error())
|
c.xbwritelinef("* NO generating notify fetch response: %s", err.Error())
|
||||||
|
// Always add UID, also for uidonly, to ensure a non-empty list.
|
||||||
data = listspace{bare("UID"), number(ch.UID)}
|
data = listspace{bare("UID"), number(ch.UID)}
|
||||||
}
|
}
|
||||||
fmt.Fprintf(cmd.conn.xbw, "* %d FETCH ", seq)
|
// UIDFETCH in case of uidonly. ../rfc/9586:228
|
||||||
|
if c.uidonly {
|
||||||
|
fmt.Fprintf(cmd.conn.xbw, "* %d UIDFETCH ", ch.UID)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(cmd.conn.xbw, "* %d FETCH ", c.xsequence(ch.UID))
|
||||||
|
}
|
||||||
func() {
|
func() {
|
||||||
defer c.xtracewrite(mlog.LevelTracedata)()
|
defer c.xtracewrite(mlog.LevelTracedata)()
|
||||||
data.xwriteTo(cmd.conn, cmd.conn.xbw)
|
data.xwriteTo(cmd.conn, cmd.conn.xbw)
|
||||||
@ -2050,6 +2189,12 @@ func (c *conn) xapplyChangesNotify(changes []store.Change, sendDelayed bool) {
|
|||||||
|
|
||||||
var vanishedUIDs numSet
|
var vanishedUIDs numSet
|
||||||
for _, uid := range ch.UIDs {
|
for _, uid := range ch.UIDs {
|
||||||
|
// With uidonly, we must always return VANISHED. ../rfc/9586:232
|
||||||
|
if c.uidonly {
|
||||||
|
c.exists--
|
||||||
|
vanishedUIDs.append(uint32(uid))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
seq := c.xsequence(uid)
|
seq := c.xsequence(uid)
|
||||||
c.sequenceRemove(seq, uid)
|
c.sequenceRemove(seq, uid)
|
||||||
@ -2059,7 +2204,7 @@ func (c *conn) xapplyChangesNotify(changes []store.Change, sendDelayed bool) {
|
|||||||
c.xbwritelinef("* %d EXPUNGE", seq)
|
c.xbwritelinef("* %d EXPUNGE", seq)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if qresync {
|
if !vanishedUIDs.empty() {
|
||||||
// VANISHED without EARLIER. ../rfc/7162:2004
|
// VANISHED without EARLIER. ../rfc/7162:2004
|
||||||
for _, s := range vanishedUIDs.Strings(4*1024 - 32) {
|
for _, s := range vanishedUIDs.Strings(4*1024 - 32) {
|
||||||
c.xbwritelinef("* VANISHED %s", s)
|
c.xbwritelinef("* VANISHED %s", s)
|
||||||
@ -2092,17 +2237,25 @@ func (c *conn) xapplyChangesNotify(changes []store.Change, sendDelayed bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The uid can be unknown if we just expunged it while another session marked it as deleted just before.
|
// The uid can be unknown if we just expunged it while another session marked it as deleted just before.
|
||||||
seq := c.sequence(ch.UID)
|
var seq msgseq
|
||||||
|
if !c.uidonly {
|
||||||
|
seq = c.sequence(ch.UID)
|
||||||
if seq <= 0 {
|
if seq <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var modseqStr string
|
var modseqStr string
|
||||||
if condstore {
|
if condstore {
|
||||||
modseqStr = fmt.Sprintf(" MODSEQ (%d)", ch.ModSeq.Client())
|
modseqStr = fmt.Sprintf(" MODSEQ (%d)", ch.ModSeq.Client())
|
||||||
}
|
}
|
||||||
// UID and FLAGS are required. ../rfc/5465:463
|
// UID and FLAGS are required. ../rfc/5465:463
|
||||||
|
// UIDFETCH in case of uidonly. ../rfc/9586:228
|
||||||
|
if c.uidonly {
|
||||||
|
c.xbwritelinef("* %d UIDFETCH (FLAGS %s%s)", ch.UID, flaglist(ch.Flags, ch.Keywords).pack(c), modseqStr)
|
||||||
|
} else {
|
||||||
c.xbwritelinef("* %d FETCH (UID %d FLAGS %s%s)", seq, ch.UID, flaglist(ch.Flags, ch.Keywords).pack(c), modseqStr)
|
c.xbwritelinef("* %d FETCH (UID %d FLAGS %s%s)", seq, ch.UID, flaglist(ch.Flags, ch.Keywords).pack(c), modseqStr)
|
||||||
|
}
|
||||||
|
|
||||||
case store.ChangeThread:
|
case store.ChangeThread:
|
||||||
continue
|
continue
|
||||||
@ -2950,6 +3103,11 @@ func (c *conn) cmdEnable(tag, cmd string, p *parser) {
|
|||||||
case capMetadata:
|
case capMetadata:
|
||||||
c.enabled[cap] = true
|
c.enabled[cap] = true
|
||||||
enabled += " " + s
|
enabled += " " + s
|
||||||
|
case capUIDOnly:
|
||||||
|
c.enabled[cap] = true
|
||||||
|
enabled += " " + s
|
||||||
|
c.uidonly = true
|
||||||
|
c.uids = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// QRESYNC enabled CONDSTORE too ../rfc/7162:1391
|
// QRESYNC enabled CONDSTORE too ../rfc/7162:1391
|
||||||
@ -3075,6 +3233,11 @@ func (c *conn) cmdSelectExamine(isselect bool, tag, cmd string, p *parser) {
|
|||||||
c.unselect()
|
c.unselect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.uidonly && qrknownSeqSet != nil {
|
||||||
|
// ../rfc/9586:255
|
||||||
|
xsyntaxCodeErrorf("UIDREQUIRED", "cannot use message sequence match data with uidonly enabled")
|
||||||
|
}
|
||||||
|
|
||||||
name = xcheckmailboxname(name, true)
|
name = xcheckmailboxname(name, true)
|
||||||
|
|
||||||
var highestModSeq store.ModSeq
|
var highestModSeq store.ModSeq
|
||||||
@ -3085,25 +3248,28 @@ func (c *conn) cmdSelectExamine(isselect bool, tag, cmd string, p *parser) {
|
|||||||
c.xdbread(func(tx *bstore.Tx) {
|
c.xdbread(func(tx *bstore.Tx) {
|
||||||
mb = c.xmailbox(tx, name, "")
|
mb = c.xmailbox(tx, name, "")
|
||||||
|
|
||||||
|
c.uidnext = mb.UIDNext
|
||||||
|
if c.uidonly {
|
||||||
|
c.exists = uint32(mb.MailboxCounts.Total + mb.MailboxCounts.Deleted)
|
||||||
|
} else {
|
||||||
|
c.uids = []store.UID{}
|
||||||
|
|
||||||
q := bstore.QueryTx[store.Message](tx)
|
q := bstore.QueryTx[store.Message](tx)
|
||||||
q.FilterNonzero(store.Message{MailboxID: mb.ID})
|
q.FilterNonzero(store.Message{MailboxID: mb.ID})
|
||||||
q.FilterEqual("Expunged", false)
|
q.FilterEqual("Expunged", false)
|
||||||
q.SortAsc("UID")
|
q.SortAsc("UID")
|
||||||
c.uids = []store.UID{}
|
|
||||||
var seq msgseq = 1
|
|
||||||
err := q.ForEach(func(m store.Message) error {
|
err := q.ForEach(func(m store.Message) error {
|
||||||
c.uids = append(c.uids, m.UID)
|
c.uids = append(c.uids, m.UID)
|
||||||
if firstUnseen == 0 && !m.Seen {
|
if firstUnseen == 0 && !m.Seen {
|
||||||
firstUnseen = seq
|
firstUnseen = msgseq(len(c.uids))
|
||||||
}
|
}
|
||||||
seq++
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if sanityChecks {
|
|
||||||
checkUIDs(c.uids)
|
|
||||||
}
|
|
||||||
xcheckf(err, "fetching uids")
|
xcheckf(err, "fetching uids")
|
||||||
|
|
||||||
|
c.exists = uint32(len(c.uids))
|
||||||
|
}
|
||||||
|
|
||||||
// Condstore extension, find the highest modseq.
|
// Condstore extension, find the highest modseq.
|
||||||
if c.enabled[capCondstore] {
|
if c.enabled[capCondstore] {
|
||||||
highestModSeq = mb.ModSeq
|
highestModSeq = mb.ModSeq
|
||||||
@ -3111,6 +3277,7 @@ func (c *conn) cmdSelectExamine(isselect bool, tag, cmd string, p *parser) {
|
|||||||
// For QRESYNC, we need to know the highest modset of deleted expunged records to
|
// For QRESYNC, we need to know the highest modset of deleted expunged records to
|
||||||
// maintain synchronization.
|
// maintain synchronization.
|
||||||
if c.enabled[capQresync] {
|
if c.enabled[capQresync] {
|
||||||
|
var err error
|
||||||
highDeletedModSeq, err = c.account.HighestDeletedModSeq(tx)
|
highDeletedModSeq, err = c.account.HighestDeletedModSeq(tx)
|
||||||
xcheckf(err, "getting highest deleted modseq")
|
xcheckf(err, "getting highest deleted modseq")
|
||||||
}
|
}
|
||||||
@ -3129,7 +3296,7 @@ func (c *conn) cmdSelectExamine(isselect bool, tag, cmd string, p *parser) {
|
|||||||
if !c.enabled[capIMAP4rev2] {
|
if !c.enabled[capIMAP4rev2] {
|
||||||
c.xbwritelinef(`* 0 RECENT`)
|
c.xbwritelinef(`* 0 RECENT`)
|
||||||
}
|
}
|
||||||
c.xbwritelinef(`* %d EXISTS`, len(c.uids))
|
c.xbwritelinef(`* %d EXISTS`, c.exists)
|
||||||
if !c.enabled[capIMAP4rev2] && firstUnseen > 0 {
|
if !c.enabled[capIMAP4rev2] && firstUnseen > 0 {
|
||||||
// ../rfc/9051:8051 ../rfc/3501:1774
|
// ../rfc/9051:8051 ../rfc/3501:1774
|
||||||
c.xbwritelinef(`* OK [UNSEEN %d] x`, firstUnseen)
|
c.xbwritelinef(`* OK [UNSEEN %d] x`, firstUnseen)
|
||||||
@ -3173,7 +3340,8 @@ func (c *conn) cmdSelectExamine(isselect bool, tag, cmd string, p *parser) {
|
|||||||
xsyntaxErrorf("invalid combination of known sequence set and uid set, must be of equal length")
|
xsyntaxErrorf("invalid combination of known sequence set and uid set, must be of equal length")
|
||||||
}
|
}
|
||||||
i := int(msgseq - 1)
|
i := int(msgseq - 1)
|
||||||
if i < 0 || i >= len(c.uids) || c.uids[i] != store.UID(uid) {
|
// Access to c.uids is safe, qrknownSeqSet and uidonly cannot both be set.
|
||||||
|
if i < 0 || i >= int(c.exists) || c.uids[i] != store.UID(uid) {
|
||||||
if uidSearch(c.uids, store.UID(uid)) <= 0 {
|
if uidSearch(c.uids, store.UID(uid)) <= 0 {
|
||||||
// We will check this old client UID for consistency below.
|
// We will check this old client UID for consistency below.
|
||||||
oldClientUID = store.UID(uid)
|
oldClientUID = store.UID(uid)
|
||||||
@ -3223,6 +3391,7 @@ func (c *conn) cmdSelectExamine(isselect bool, tag, cmd string, p *parser) {
|
|||||||
// Note: we don't filter by Expunged.
|
// Note: we don't filter by Expunged.
|
||||||
q.FilterGreater("ModSeq", store.ModSeqFromClient(qrmodseq))
|
q.FilterGreater("ModSeq", store.ModSeqFromClient(qrmodseq))
|
||||||
q.FilterLessEqual("ModSeq", highestModSeq)
|
q.FilterLessEqual("ModSeq", highestModSeq)
|
||||||
|
q.FilterLess("UID", c.uidnext)
|
||||||
q.SortAsc("ModSeq")
|
q.SortAsc("ModSeq")
|
||||||
err := q.ForEach(func(m store.Message) error {
|
err := q.ForEach(func(m store.Message) error {
|
||||||
if m.Expunged && m.UID < preVanished {
|
if m.Expunged && m.UID < preVanished {
|
||||||
@ -3236,27 +3405,59 @@ func (c *conn) cmdSelectExamine(isselect bool, tag, cmd string, p *parser) {
|
|||||||
vanishedUIDs[m.UID] = struct{}{}
|
vanishedUIDs[m.UID] = struct{}{}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
msgseq := c.sequence(m.UID)
|
// UIDFETCH in case of uidonly. ../rfc/9586:228
|
||||||
if msgseq > 0 {
|
if c.uidonly {
|
||||||
|
c.xbwritelinef("* %d UIDFETCH (FLAGS %s MODSEQ (%d))", m.UID, flaglist(m.Flags, m.Keywords).pack(c), m.ModSeq.Client())
|
||||||
|
} else if msgseq := c.sequence(m.UID); msgseq > 0 {
|
||||||
c.xbwritelinef("* %d FETCH (UID %d FLAGS %s MODSEQ (%d))", msgseq, m.UID, flaglist(m.Flags, m.Keywords).pack(c), m.ModSeq.Client())
|
c.xbwritelinef("* %d FETCH (UID %d FLAGS %s MODSEQ (%d))", msgseq, m.UID, flaglist(m.Flags, m.Keywords).pack(c), m.ModSeq.Client())
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
xcheckf(err, "listing changed messages")
|
xcheckf(err, "listing changed messages")
|
||||||
})
|
|
||||||
|
|
||||||
// Add UIDs from client's known UID set to vanished list if we don't have enough history.
|
// If we don't have enough history, we go through all UIDs and look them up, and
|
||||||
|
// add them to the vanished list if they have disappeared.
|
||||||
if qrmodseq < highDeletedModSeq.Client() {
|
if qrmodseq < highDeletedModSeq.Client() {
|
||||||
// If no known uid set was in the request, we substitute 1:max or the empty set.
|
// If no "known uid set" was in the request, we substitute 1:max or the empty set.
|
||||||
// ../rfc/7162:1524
|
// ../rfc/7162:1524
|
||||||
if qrknownUIDs == nil {
|
if qrknownUIDs == nil {
|
||||||
if len(c.uids) > 0 {
|
qrknownUIDs = &numSet{ranges: []numRange{{first: setNumber{number: 1}, last: &setNumber{number: uint32(c.uidnext - 1)}}}}
|
||||||
qrknownUIDs = &numSet{ranges: []numRange{{first: setNumber{number: 1}, last: &setNumber{number: uint32(c.uids[len(c.uids)-1])}}}}
|
|
||||||
} else {
|
|
||||||
qrknownUIDs = &numSet{}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.uidonly {
|
||||||
|
// note: qrknownUIDs will not contain "*".
|
||||||
|
for _, r := range qrknownUIDs.xinterpretStar(func() store.UID { return 0 }).ranges {
|
||||||
|
// Gather UIDs for this range.
|
||||||
|
var uids []store.UID
|
||||||
|
q := bstore.QueryTx[store.Message](tx)
|
||||||
|
q.FilterNonzero(store.Message{MailboxID: mb.ID})
|
||||||
|
q.FilterEqual("Expunged", false)
|
||||||
|
if r.last == nil {
|
||||||
|
q.FilterEqual("UID", r.first.number)
|
||||||
|
} else {
|
||||||
|
q.FilterGreaterEqual("UID", r.first.number)
|
||||||
|
q.FilterLessEqual("UID", r.last.number)
|
||||||
|
}
|
||||||
|
q.SortAsc("UID")
|
||||||
|
for m, err := range q.All() {
|
||||||
|
xcheckf(err, "enumerating uids")
|
||||||
|
uids = append(uids, m.UID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find UIDs missing from the database.
|
||||||
|
iter := r.newIter()
|
||||||
|
for {
|
||||||
|
uid, ok := iter.Next()
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if uidSearch(uids, store.UID(uid)) <= 0 {
|
||||||
|
vanishedUIDs[store.UID(uid)] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Ensure it is in ascending order, no needless first/last ranges. qrknownUIDs cannot contain a star.
|
||||||
iter := qrknownUIDs.newIter()
|
iter := qrknownUIDs.newIter()
|
||||||
for {
|
for {
|
||||||
v, ok := iter.Next()
|
v, ok := iter.Next()
|
||||||
@ -3268,6 +3469,8 @@ func (c *conn) cmdSelectExamine(isselect bool, tag, cmd string, p *parser) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Now that we have all vanished UIDs, send them over compactly.
|
// Now that we have all vanished UIDs, send them over compactly.
|
||||||
if len(vanishedUIDs) > 0 {
|
if len(vanishedUIDs) > 0 {
|
||||||
@ -4044,7 +4247,7 @@ func (c *conn) cmdAppend(tag, cmd string, p *parser) {
|
|||||||
c.uidAppend(a.m.UID)
|
c.uidAppend(a.m.UID)
|
||||||
}
|
}
|
||||||
// todo spec: with condstore/qresync, is there a mechanism to let the client know the modseq for the appended uid? in theory an untagged fetch with the modseq after the OK APPENDUID could make sense, but this probably isn't allowed.
|
// todo spec: with condstore/qresync, is there a mechanism to let the client know the modseq for the appended uid? in theory an untagged fetch with the modseq after the OK APPENDUID could make sense, but this probably isn't allowed.
|
||||||
c.xbwritelinef("* %d EXISTS", len(c.uids))
|
c.xbwritelinef("* %d EXISTS", c.exists)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ../rfc/4315:289 ../rfc/3502:236 APPENDUID
|
// ../rfc/4315:289 ../rfc/3502:236 APPENDUID
|
||||||
@ -4263,13 +4466,17 @@ func (c *conn) xexpunge(uidSet *numSet, missingMailboxOK bool) (expunged []store
|
|||||||
}
|
}
|
||||||
xcheckf(err, "get mailbox")
|
xcheckf(err, "get mailbox")
|
||||||
|
|
||||||
|
xlastUID := c.newCachedLastUID(tx, c.mailboxID, func(err error) { xuserErrorf("%s", err) })
|
||||||
|
|
||||||
qm := bstore.QueryTx[store.Message](tx)
|
qm := bstore.QueryTx[store.Message](tx)
|
||||||
qm.FilterNonzero(store.Message{MailboxID: c.mailboxID})
|
qm.FilterNonzero(store.Message{MailboxID: c.mailboxID})
|
||||||
qm.FilterEqual("Deleted", true)
|
qm.FilterEqual("Deleted", true)
|
||||||
qm.FilterEqual("Expunged", false)
|
qm.FilterEqual("Expunged", false)
|
||||||
|
qm.FilterLess("UID", c.uidnext)
|
||||||
qm.FilterFn(func(m store.Message) bool {
|
qm.FilterFn(func(m store.Message) bool {
|
||||||
// Only remove if this session knows about the message and if present in optional uidSet.
|
// Only remove if this session knows about the message and if present in optional
|
||||||
return uidSearch(c.uids, m.UID) > 0 && (uidSet == nil || uidSet.containsUID(m.UID, c.uids, c.searchResult))
|
// uidSet.
|
||||||
|
return uidSet == nil || uidSet.xcontainsKnownUID(m.UID, c.searchResult, xlastUID)
|
||||||
})
|
})
|
||||||
qm.SortAsc("UID")
|
qm.SortAsc("UID")
|
||||||
expunged, err = qm.List()
|
expunged, err = qm.List()
|
||||||
@ -4363,6 +4570,12 @@ func (c *conn) cmdxExpunge(tag, cmd string, uidSet *numSet) {
|
|||||||
var vanishedUIDs numSet
|
var vanishedUIDs numSet
|
||||||
qresync := c.enabled[capQresync]
|
qresync := c.enabled[capQresync]
|
||||||
for _, m := range expunged {
|
for _, m := range expunged {
|
||||||
|
// With uidonly, we must always return VANISHED. ../rfc/9586:210
|
||||||
|
if c.uidonly {
|
||||||
|
c.exists--
|
||||||
|
vanishedUIDs.append(uint32(m.UID))
|
||||||
|
continue
|
||||||
|
}
|
||||||
seq := c.xsequence(m.UID)
|
seq := c.xsequence(m.UID)
|
||||||
c.sequenceRemove(seq, m.UID)
|
c.sequenceRemove(seq, m.UID)
|
||||||
if qresync {
|
if qresync {
|
||||||
@ -4445,20 +4658,14 @@ func (c *conn) cmdUIDReplace(tag, cmd string, p *parser) {
|
|||||||
c.cmdxReplace(true, tag, cmd, p)
|
c.cmdxReplace(true, tag, cmd, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *conn) gatherCopyMoveUIDs(isUID bool, nums numSet) ([]store.UID, []any) {
|
func (c *conn) gatherCopyMoveUIDs(tx *bstore.Tx, isUID bool, nums numSet) []store.UID {
|
||||||
// Gather uids, then sort so we can return a consistently simple and hard to
|
// Gather uids, then sort so we can return a consistently simple and hard to
|
||||||
// misinterpret COPYUID/MOVEUID response. It seems safer to have UIDs in ascending
|
// misinterpret COPYUID/MOVEUID response. It seems safer to have UIDs in ascending
|
||||||
// order, because requested uid set of 12:10 is equal to 10:12, so if we would just
|
// order, because requested uid set of 12:10 is equal to 10:12, so if we would just
|
||||||
// echo whatever the client sends us without reordering, the client can reorder our
|
// echo whatever the client sends us without reordering, the client can reorder our
|
||||||
// response and interpret it differently than we intended.
|
// response and interpret it differently than we intended.
|
||||||
// ../rfc/9051:5072
|
// ../rfc/9051:5072
|
||||||
uids := c.xnumSetUIDs(isUID, nums)
|
return c.xnumSetEval(tx, isUID, nums)
|
||||||
slices.Sort(uids)
|
|
||||||
uidargs := make([]any, len(uids))
|
|
||||||
for i, uid := range uids {
|
|
||||||
uidargs[i] = uid
|
|
||||||
}
|
|
||||||
return uids, uidargs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy copies messages from the currently selected/active mailbox to another named
|
// Copy copies messages from the currently selected/active mailbox to another named
|
||||||
@ -4477,8 +4684,6 @@ func (c *conn) cmdxCopy(isUID bool, tag, cmd string, p *parser) {
|
|||||||
|
|
||||||
name = xcheckmailboxname(name, true)
|
name = xcheckmailboxname(name, true)
|
||||||
|
|
||||||
uids, uidargs := c.gatherCopyMoveUIDs(isUID, nums)
|
|
||||||
|
|
||||||
// Files that were created during the copy. Remove them if the operation fails.
|
// Files that were created during the copy. Remove them if the operation fails.
|
||||||
var newIDs []int64
|
var newIDs []int64
|
||||||
defer func() {
|
defer func() {
|
||||||
@ -4489,9 +4694,12 @@ func (c *conn) cmdxCopy(isUID bool, tag, cmd string, p *parser) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// UIDs to copy.
|
||||||
|
var uids []store.UID
|
||||||
|
|
||||||
var mbDst store.Mailbox
|
var mbDst store.Mailbox
|
||||||
var nkeywords int
|
var nkeywords int
|
||||||
var origUIDs, newUIDs []store.UID
|
var newUIDs []store.UID
|
||||||
var flags []store.Flags
|
var flags []store.Flags
|
||||||
var keywords [][]string
|
var keywords [][]string
|
||||||
var modseq store.ModSeq // For messages in new mailbox, assigned when first message is copied.
|
var modseq store.ModSeq // For messages in new mailbox, assigned when first message is copied.
|
||||||
@ -4500,12 +4708,15 @@ func (c *conn) cmdxCopy(isUID bool, tag, cmd string, p *parser) {
|
|||||||
|
|
||||||
c.xdbwrite(func(tx *bstore.Tx) {
|
c.xdbwrite(func(tx *bstore.Tx) {
|
||||||
mbSrc := c.xmailboxID(tx, c.mailboxID) // Validate.
|
mbSrc := c.xmailboxID(tx, c.mailboxID) // Validate.
|
||||||
|
|
||||||
mbDst = c.xmailbox(tx, name, "TRYCREATE")
|
mbDst = c.xmailbox(tx, name, "TRYCREATE")
|
||||||
if mbDst.ID == mbSrc.ID {
|
if mbDst.ID == mbSrc.ID {
|
||||||
xuserErrorf("cannot copy to currently selected mailbox")
|
xuserErrorf("cannot copy to currently selected mailbox")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(uidargs) == 0 {
|
uids = c.gatherCopyMoveUIDs(tx, isUID, nums)
|
||||||
|
|
||||||
|
if len(uids) == 0 {
|
||||||
xuserErrorf("no matching messages to copy")
|
xuserErrorf("no matching messages to copy")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4522,17 +4733,17 @@ func (c *conn) cmdxCopy(isUID bool, tag, cmd string, p *parser) {
|
|||||||
|
|
||||||
// Reserve the uids in the destination mailbox.
|
// Reserve the uids in the destination mailbox.
|
||||||
uidFirst := mbDst.UIDNext
|
uidFirst := mbDst.UIDNext
|
||||||
mbDst.UIDNext += store.UID(len(uidargs))
|
mbDst.UIDNext += store.UID(len(uids))
|
||||||
|
|
||||||
// Fetch messages from database.
|
// Fetch messages from database.
|
||||||
q := bstore.QueryTx[store.Message](tx)
|
q := bstore.QueryTx[store.Message](tx)
|
||||||
q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
|
q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
|
||||||
q.FilterEqual("UID", uidargs...)
|
q.FilterEqual("UID", slicesAny(uids)...)
|
||||||
q.FilterEqual("Expunged", false)
|
q.FilterEqual("Expunged", false)
|
||||||
xmsgs, err := q.List()
|
xmsgs, err := q.List()
|
||||||
xcheckf(err, "fetching messages")
|
xcheckf(err, "fetching messages")
|
||||||
|
|
||||||
if len(xmsgs) != len(uidargs) {
|
if len(xmsgs) != len(uids) {
|
||||||
xserverErrorf("uid and message mismatch")
|
xserverErrorf("uid and message mismatch")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4588,7 +4799,6 @@ func (c *conn) cmdxCopy(isUID bool, tag, cmd string, p *parser) {
|
|||||||
xcheckf(err, "inserting message")
|
xcheckf(err, "inserting message")
|
||||||
msgs[uid] = m
|
msgs[uid] = m
|
||||||
nmsgs[i] = m
|
nmsgs[i] = m
|
||||||
origUIDs = append(origUIDs, uid)
|
|
||||||
newUIDs = append(newUIDs, m.UID)
|
newUIDs = append(newUIDs, m.UID)
|
||||||
newMsgIDs = append(newMsgIDs, m.ID)
|
newMsgIDs = append(newMsgIDs, m.ID)
|
||||||
flags = append(flags, m.Flags)
|
flags = append(flags, m.Flags)
|
||||||
@ -4666,7 +4876,7 @@ func (c *conn) cmdxCopy(isUID bool, tag, cmd string, p *parser) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// ../rfc/9051:6881 ../rfc/4315:183
|
// ../rfc/9051:6881 ../rfc/4315:183
|
||||||
c.xwriteresultf("%s OK [COPYUID %d %s %s] copied", tag, mbDst.UIDValidity, compactUIDSet(origUIDs).String(), compactUIDSet(newUIDs).String())
|
c.xwriteresultf("%s OK [COPYUID %d %s %s] copied", tag, mbDst.UIDValidity, compactUIDSet(uids).String(), compactUIDSet(newUIDs).String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move moves messages from the currently selected/active mailbox to a named mailbox.
|
// Move moves messages from the currently selected/active mailbox to a named mailbox.
|
||||||
@ -4688,7 +4898,8 @@ func (c *conn) cmdxMove(isUID bool, tag, cmd string, p *parser) {
|
|||||||
xuserErrorf("mailbox open in read-only mode")
|
xuserErrorf("mailbox open in read-only mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
uids, uidargs := c.gatherCopyMoveUIDs(isUID, nums)
|
// UIDs to move.
|
||||||
|
var uids []store.UID
|
||||||
|
|
||||||
var mbDst store.Mailbox
|
var mbDst store.Mailbox
|
||||||
var uidFirst store.UID
|
var uidFirst store.UID
|
||||||
@ -4713,6 +4924,8 @@ func (c *conn) cmdxMove(isUID bool, tag, cmd string, p *parser) {
|
|||||||
xuserErrorf("cannot move to currently selected mailbox")
|
xuserErrorf("cannot move to currently selected mailbox")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uids = c.gatherCopyMoveUIDs(tx, isUID, nums)
|
||||||
|
|
||||||
if len(uids) == 0 {
|
if len(uids) == 0 {
|
||||||
xuserErrorf("no matching messages to move")
|
xuserErrorf("no matching messages to move")
|
||||||
}
|
}
|
||||||
@ -4727,11 +4940,11 @@ func (c *conn) cmdxMove(isUID bool, tag, cmd string, p *parser) {
|
|||||||
// Make query selecting messages to move.
|
// Make query selecting messages to move.
|
||||||
q := bstore.QueryTx[store.Message](tx)
|
q := bstore.QueryTx[store.Message](tx)
|
||||||
q.FilterNonzero(store.Message{MailboxID: mbSrc.ID})
|
q.FilterNonzero(store.Message{MailboxID: mbSrc.ID})
|
||||||
q.FilterEqual("UID", uidargs...)
|
q.FilterEqual("UID", slicesAny(uids)...)
|
||||||
q.FilterEqual("Expunged", false)
|
q.FilterEqual("Expunged", false)
|
||||||
q.SortAsc("UID")
|
q.SortAsc("UID")
|
||||||
|
|
||||||
newIDs, chl := c.xmoveMessages(tx, q, len(uidargs), modseq, &mbSrc, &mbDst)
|
newIDs, chl := c.xmoveMessages(tx, q, len(uids), modseq, &mbSrc, &mbDst)
|
||||||
changes = append(changes, chl...)
|
changes = append(changes, chl...)
|
||||||
cleanupIDs = newIDs
|
cleanupIDs = newIDs
|
||||||
})
|
})
|
||||||
@ -4748,6 +4961,13 @@ func (c *conn) cmdxMove(isUID bool, tag, cmd string, p *parser) {
|
|||||||
qresync := c.enabled[capQresync]
|
qresync := c.enabled[capQresync]
|
||||||
var vanishedUIDs numSet
|
var vanishedUIDs numSet
|
||||||
for i := range uids {
|
for i := range uids {
|
||||||
|
// With uidonly, we must always return VANISHED. ../rfc/9586:232
|
||||||
|
if c.uidonly {
|
||||||
|
c.exists--
|
||||||
|
vanishedUIDs.append(uint32(uids[i]))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
seq := c.xsequence(uids[i])
|
seq := c.xsequence(uids[i])
|
||||||
c.sequenceRemove(seq, uids[i])
|
c.sequenceRemove(seq, uids[i])
|
||||||
if qresync {
|
if qresync {
|
||||||
@ -4988,9 +5208,9 @@ func (c *conn) cmdxStore(isUID bool, tag, cmd string, p *parser) {
|
|||||||
mb = c.xmailboxID(tx, c.mailboxID) // Validate.
|
mb = c.xmailboxID(tx, c.mailboxID) // Validate.
|
||||||
origmb = mb
|
origmb = mb
|
||||||
|
|
||||||
uidargs := c.xnumSetCondition(isUID, nums)
|
uids := c.xnumSetEval(tx, isUID, nums)
|
||||||
|
|
||||||
if len(uidargs) == 0 {
|
if len(uids) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -5005,7 +5225,7 @@ func (c *conn) cmdxStore(isUID bool, tag, cmd string, p *parser) {
|
|||||||
|
|
||||||
q := bstore.QueryTx[store.Message](tx)
|
q := bstore.QueryTx[store.Message](tx)
|
||||||
q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
|
q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
|
||||||
q.FilterEqual("UID", uidargs...)
|
q.FilterEqual("UID", slicesAny(uids)...)
|
||||||
q.FilterEqual("Expunged", false)
|
q.FilterEqual("Expunged", false)
|
||||||
err := q.ForEach(func(m store.Message) error {
|
err := q.ForEach(func(m store.Message) error {
|
||||||
// Client may specify a message multiple times, but we only process it once. ../rfc/7162:823
|
// Client may specify a message multiple times, but we only process it once. ../rfc/7162:823
|
||||||
@ -5111,16 +5331,25 @@ func (c *conn) cmdxStore(isUID bool, tag, cmd string, p *parser) {
|
|||||||
// ../rfc/7162:549
|
// ../rfc/7162:549
|
||||||
if !silent || c.enabled[capCondstore] {
|
if !silent || c.enabled[capCondstore] {
|
||||||
for _, m := range updated {
|
for _, m := range updated {
|
||||||
var flags string
|
var args []string
|
||||||
if !silent {
|
if !silent {
|
||||||
flags = fmt.Sprintf(" FLAGS %s", flaglist(m.Flags, m.Keywords).pack(c))
|
args = append(args, fmt.Sprintf("FLAGS %s", flaglist(m.Flags, m.Keywords).pack(c)))
|
||||||
}
|
}
|
||||||
var modseqStr string
|
|
||||||
if c.enabled[capCondstore] {
|
if c.enabled[capCondstore] {
|
||||||
modseqStr = fmt.Sprintf(" MODSEQ (%d)", m.ModSeq.Client())
|
args = append(args, fmt.Sprintf("MODSEQ (%d)", m.ModSeq.Client()))
|
||||||
}
|
}
|
||||||
// ../rfc/9051:6749 ../rfc/3501:4869 ../rfc/7162:2490
|
// ../rfc/9051:6749 ../rfc/3501:4869 ../rfc/7162:2490
|
||||||
c.xbwritelinef("* %d FETCH (UID %d%s%s)", c.xsequence(m.UID), m.UID, flags, modseqStr)
|
// UIDFETCH in case of uidonly. ../rfc/9586:228
|
||||||
|
if c.uidonly {
|
||||||
|
// Ensure list is non-empty.
|
||||||
|
if len(args) == 0 {
|
||||||
|
args = append(args, fmt.Sprintf("UID %d", m.UID))
|
||||||
|
}
|
||||||
|
c.xbwritelinef("* %d UIDFETCH (%s)", m.UID, strings.Join(args, " "))
|
||||||
|
} else {
|
||||||
|
args = append([]string{fmt.Sprintf("UID %d", m.UID)}, args...)
|
||||||
|
c.xbwritelinef("* %d FETCH (%s)", c.xsequence(m.UID), strings.Join(args, " "))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -5138,7 +5367,12 @@ func (c *conn) cmdxStore(isUID bool, tag, cmd string, p *parser) {
|
|||||||
// Also gather UIDs or sequences for the MODIFIED response below. ../rfc/7162:571
|
// Also gather UIDs or sequences for the MODIFIED response below. ../rfc/7162:571
|
||||||
var mnums []store.UID
|
var mnums []store.UID
|
||||||
for _, m := range changed {
|
for _, m := range changed {
|
||||||
|
// UIDFETCH in case of uidonly. ../rfc/9586:228
|
||||||
|
if c.uidonly {
|
||||||
|
c.xbwritelinef("* %d UIDFETCH (FLAGS %s MODSEQ (%d))", m.UID, flaglist(m.Flags, m.Keywords).pack(c), m.ModSeq.Client())
|
||||||
|
} else {
|
||||||
c.xbwritelinef("* %d FETCH (UID %d FLAGS %s MODSEQ (%d))", c.xsequence(m.UID), m.UID, flaglist(m.Flags, m.Keywords).pack(c), m.ModSeq.Client())
|
c.xbwritelinef("* %d FETCH (UID %d FLAGS %s MODSEQ (%d))", c.xsequence(m.UID), m.UID, flaglist(m.Flags, m.Keywords).pack(c), m.ModSeq.Client())
|
||||||
|
}
|
||||||
if isUID {
|
if isUID {
|
||||||
mnums = append(mnums, m.UID)
|
mnums = append(mnums, m.UID)
|
||||||
} else {
|
} else {
|
||||||
|
@ -41,6 +41,10 @@ func init() {
|
|||||||
mox.Context = ctxbg
|
mox.Context = ctxbg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ptr[T any](v T) *T {
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
func tocrlf(s string) string {
|
func tocrlf(s string) string {
|
||||||
return strings.ReplaceAll(s, "\n", "\r\n")
|
return strings.ReplaceAll(s, "\n", "\r\n")
|
||||||
}
|
}
|
||||||
@ -170,6 +174,7 @@ type testconn struct {
|
|||||||
t *testing.T
|
t *testing.T
|
||||||
conn net.Conn
|
conn net.Conn
|
||||||
client *imapclient.Conn
|
client *imapclient.Conn
|
||||||
|
uidonly bool
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
serverConn net.Conn
|
serverConn net.Conn
|
||||||
account *store.Account
|
account *store.Account
|
||||||
@ -250,7 +255,7 @@ func tuntagged(t *testing.T, got imapclient.Untagged, dst any) {
|
|||||||
gotv := reflect.ValueOf(got)
|
gotv := reflect.ValueOf(got)
|
||||||
dstv := reflect.ValueOf(dst)
|
dstv := reflect.ValueOf(dst)
|
||||||
if gotv.Type() != dstv.Type().Elem() {
|
if gotv.Type() != dstv.Type().Elem() {
|
||||||
t.Fatalf("got %#v, expected %#v", gotv.Type(), dstv.Type().Elem())
|
t.Fatalf("got %#v, expected %#v", got, dstv.Elem().Interface())
|
||||||
}
|
}
|
||||||
dstv.Elem().Set(gotv)
|
dstv.Elem().Set(gotv)
|
||||||
}
|
}
|
||||||
@ -327,6 +332,33 @@ func (tc *testconn) waitDone() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (tc *testconn) login(username, password string) {
|
||||||
|
tc.client.Login(username, password)
|
||||||
|
if tc.uidonly {
|
||||||
|
tc.transactf("ok", "enable uidonly")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// untaggedFetch returns an imapclient.UntaggedFetch or
|
||||||
|
// imapclient.UntaggedUIDFetch, depending on whether uidonly is enabled for the
|
||||||
|
// connection.
|
||||||
|
func (tc *testconn) untaggedFetch(seq, uid uint32, attrs ...imapclient.FetchAttr) any {
|
||||||
|
if tc.uidonly {
|
||||||
|
return imapclient.UntaggedUIDFetch{UID: uid, Attrs: attrs}
|
||||||
|
}
|
||||||
|
attrs = append([]imapclient.FetchAttr{imapclient.FetchUID(uid)}, attrs...)
|
||||||
|
return imapclient.UntaggedFetch{Seq: seq, Attrs: attrs}
|
||||||
|
}
|
||||||
|
|
||||||
|
// like untaggedFetch, but with explicit UID fetch attribute in case of uidonly.
|
||||||
|
func (tc *testconn) untaggedFetchUID(seq, uid uint32, attrs ...imapclient.FetchAttr) any {
|
||||||
|
attrs = append([]imapclient.FetchAttr{imapclient.FetchUID(uid)}, attrs...)
|
||||||
|
if tc.uidonly {
|
||||||
|
return imapclient.UntaggedUIDFetch{UID: uid, Attrs: attrs}
|
||||||
|
}
|
||||||
|
return imapclient.UntaggedFetch{Seq: seq, Attrs: attrs}
|
||||||
|
}
|
||||||
|
|
||||||
func (tc *testconn) close() {
|
func (tc *testconn) close() {
|
||||||
tc.close0(true)
|
tc.close0(true)
|
||||||
}
|
}
|
||||||
@ -338,7 +370,7 @@ func (tc *testconn) closeNoWait() {
|
|||||||
func (tc *testconn) close0(waitclose bool) {
|
func (tc *testconn) close0(waitclose bool) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if unhandledPanics.Swap(0) > 0 {
|
if unhandledPanics.Swap(0) > 0 {
|
||||||
tc.t.Fatalf("handled panic in server")
|
tc.t.Fatalf("unhandled panic in server")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -388,19 +420,19 @@ func makeAppendTime(msg string, tm time.Time) imapclient.Append {
|
|||||||
|
|
||||||
var connCounter int64
|
var connCounter int64
|
||||||
|
|
||||||
func start(t *testing.T) *testconn {
|
func start(t *testing.T, uidonly bool) *testconn {
|
||||||
return startArgs(t, true, false, true, true, "mjl")
|
return startArgs(t, uidonly, true, false, true, true, "mjl")
|
||||||
}
|
}
|
||||||
|
|
||||||
func startNoSwitchboard(t *testing.T) *testconn {
|
func startNoSwitchboard(t *testing.T, uidonly bool) *testconn {
|
||||||
return startArgs(t, false, false, true, false, "mjl")
|
return startArgs(t, uidonly, false, false, true, false, "mjl")
|
||||||
}
|
}
|
||||||
|
|
||||||
const password0 = "te\u0301st \u00a0\u2002\u200a" // NFD and various unicode spaces.
|
const password0 = "te\u0301st \u00a0\u2002\u200a" // NFD and various unicode spaces.
|
||||||
const password1 = "tést " // PRECIS normalized, with NFC.
|
const password1 = "tést " // PRECIS normalized, with NFC.
|
||||||
|
|
||||||
func startArgs(t *testing.T, first, immediateTLS bool, allowLoginWithoutTLS, setPassword bool, accname string) *testconn {
|
func startArgs(t *testing.T, uidonly, first, immediateTLS bool, allowLoginWithoutTLS, setPassword bool, accname string) *testconn {
|
||||||
return startArgsMore(t, first, immediateTLS, nil, nil, allowLoginWithoutTLS, setPassword, accname, nil)
|
return startArgsMore(t, uidonly, first, immediateTLS, nil, nil, allowLoginWithoutTLS, setPassword, accname, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// namedConn wraps a conn so it can return a RemoteAddr with a non-empty name.
|
// namedConn wraps a conn so it can return a RemoteAddr with a non-empty name.
|
||||||
@ -415,7 +447,7 @@ func (c namedConn) RemoteAddr() net.Addr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// todo: the parameters and usage are too much now. change to scheme similar to smtpserver, with params in a struct, and a separate method for init and making a connection.
|
// todo: the parameters and usage are too much now. change to scheme similar to smtpserver, with params in a struct, and a separate method for init and making a connection.
|
||||||
func startArgsMore(t *testing.T, first, immediateTLS bool, serverConfig, clientConfig *tls.Config, allowLoginWithoutTLS, setPassword bool, accname string, afterInit func() error) *testconn {
|
func startArgsMore(t *testing.T, uidonly, first, immediateTLS bool, serverConfig, clientConfig *tls.Config, allowLoginWithoutTLS, setPassword bool, accname string, afterInit func() error) *testconn {
|
||||||
limitersInit() // Reset rate limiters.
|
limitersInit() // Reset rate limiters.
|
||||||
|
|
||||||
switchStop := func() {}
|
switchStop := func() {}
|
||||||
@ -506,7 +538,7 @@ func startArgsMore(t *testing.T, first, immediateTLS bool, serverConfig, clientC
|
|||||||
}()
|
}()
|
||||||
client, err := imapclient.New(connCounter, clientConn, true)
|
client, err := imapclient.New(connCounter, clientConn, true)
|
||||||
tcheck(t, err, "new client")
|
tcheck(t, err, "new client")
|
||||||
tc := &testconn{t: t, conn: clientConn, client: client, done: done, serverConn: serverConn, account: acc}
|
tc := &testconn{t: t, conn: clientConn, client: client, uidonly: uidonly, done: done, serverConn: serverConn, account: acc}
|
||||||
if first {
|
if first {
|
||||||
tc.switchStop = switchStop
|
tc.switchStop = switchStop
|
||||||
}
|
}
|
||||||
@ -542,7 +574,7 @@ func fakeCert(t *testing.T, randomkey bool) tls.Certificate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLogin(t *testing.T) {
|
func TestLogin(t *testing.T) {
|
||||||
tc := start(t)
|
tc := start(t, false)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc.transactf("bad", "login too many args")
|
tc.transactf("bad", "login too many args")
|
||||||
@ -556,11 +588,11 @@ func TestLogin(t *testing.T) {
|
|||||||
tc.transactf("ok", `login mjl@mox.example "%s"`, password0)
|
tc.transactf("ok", `login mjl@mox.example "%s"`, password0)
|
||||||
tc.close()
|
tc.close()
|
||||||
|
|
||||||
tc = start(t)
|
tc = start(t, false)
|
||||||
tc.transactf("ok", `login "mjl@mox.example" "%s"`, password0)
|
tc.transactf("ok", `login "mjl@mox.example" "%s"`, password0)
|
||||||
tc.close()
|
tc.close()
|
||||||
|
|
||||||
tc = start(t)
|
tc = start(t, false)
|
||||||
tc.transactf("ok", `login "\"\"@mox.example" "%s"`, password0)
|
tc.transactf("ok", `login "\"\"@mox.example" "%s"`, password0)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
@ -570,7 +602,7 @@ func TestLogin(t *testing.T) {
|
|||||||
|
|
||||||
// Test that commands don't work in the states they are not supposed to.
|
// Test that commands don't work in the states they are not supposed to.
|
||||||
func TestState(t *testing.T) {
|
func TestState(t *testing.T) {
|
||||||
tc := start(t)
|
tc := start(t, false)
|
||||||
|
|
||||||
notAuthenticated := []string{"starttls", "authenticate", "login"}
|
notAuthenticated := []string{"starttls", "authenticate", "login"}
|
||||||
authenticatedOrSelected := []string{"enable", "select", "examine", "create", "delete", "rename", "subscribe", "unsubscribe", "list", "namespace", "status", "append", "idle", "lsub"}
|
authenticatedOrSelected := []string{"enable", "select", "examine", "create", "delete", "rename", "subscribe", "unsubscribe", "list", "namespace", "status", "append", "idle", "lsub"}
|
||||||
@ -581,7 +613,7 @@ func TestState(t *testing.T) {
|
|||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.transactf("ok", "logout")
|
tc.transactf("ok", "logout")
|
||||||
tc.close()
|
tc.close()
|
||||||
tc = start(t)
|
tc = start(t, false)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
// Not authenticated, lots of commands not allowed.
|
// Not authenticated, lots of commands not allowed.
|
||||||
@ -599,7 +631,7 @@ func TestState(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNonIMAP(t *testing.T) {
|
func TestNonIMAP(t *testing.T) {
|
||||||
tc := start(t)
|
tc := start(t, false)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
// imap greeting has already been read, we sidestep the imapclient.
|
// imap greeting has already been read, we sidestep the imapclient.
|
||||||
@ -612,10 +644,10 @@ func TestNonIMAP(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLiterals(t *testing.T) {
|
func TestLiterals(t *testing.T) {
|
||||||
tc := start(t)
|
tc := start(t, false)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc.client.Create("tmpbox", nil)
|
tc.client.Create("tmpbox", nil)
|
||||||
|
|
||||||
tc.transactf("ok", "rename {6+}\r\ntmpbox {7+}\r\nntmpbox")
|
tc.transactf("ok", "rename {6+}\r\ntmpbox {7+}\r\nntmpbox")
|
||||||
@ -636,9 +668,17 @@ func TestLiterals(t *testing.T) {
|
|||||||
|
|
||||||
// Test longer scenario with login, lists, subscribes, status, selects, etc.
|
// Test longer scenario with login, lists, subscribes, status, selects, etc.
|
||||||
func TestScenario(t *testing.T) {
|
func TestScenario(t *testing.T) {
|
||||||
tc := start(t)
|
testScenario(t, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScenarioUIDOnly(t *testing.T) {
|
||||||
|
testScenario(t, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testScenario(t *testing.T, uidonly bool) {
|
||||||
|
tc := start(t, uidonly)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
tc.transactf("ok", `login mjl@mox.example "%s"`, password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
|
|
||||||
tc.transactf("bad", " missingcommand")
|
tc.transactf("bad", " missingcommand")
|
||||||
|
|
||||||
@ -682,6 +722,44 @@ func TestScenario(t *testing.T) {
|
|||||||
tc.check(err, "write message")
|
tc.check(err, "write message")
|
||||||
tc.response("ok")
|
tc.response("ok")
|
||||||
|
|
||||||
|
tc.transactf("ok", "uid fetch 1 all")
|
||||||
|
tc.transactf("ok", "uid fetch 1 body")
|
||||||
|
tc.transactf("ok", "uid fetch 1 binary[]")
|
||||||
|
|
||||||
|
tc.transactf("ok", `uid store 1 flags (\seen \answered)`)
|
||||||
|
tc.transactf("ok", `uid store 1 +flags ($junk)`) // should train as junk.
|
||||||
|
tc.transactf("ok", `uid store 1 -flags ($junk)`) // should retrain as non-junk.
|
||||||
|
tc.transactf("ok", `uid store 1 -flags (\seen)`) // should untrain completely.
|
||||||
|
tc.transactf("ok", `uid store 1 -flags (\answered)`)
|
||||||
|
tc.transactf("ok", `uid store 1 +flags (\answered)`)
|
||||||
|
tc.transactf("ok", `uid store 1 flags.silent (\seen \answered)`)
|
||||||
|
tc.transactf("ok", `uid store 1 -flags.silent (\answered)`)
|
||||||
|
tc.transactf("ok", `uid store 1 +flags.silent (\answered)`)
|
||||||
|
tc.transactf("bad", `uid store 1 flags (\badflag)`)
|
||||||
|
tc.transactf("ok", "noop")
|
||||||
|
|
||||||
|
tc.transactf("ok", "uid copy 1 Trash")
|
||||||
|
tc.transactf("ok", "uid copy 1 Trash")
|
||||||
|
tc.transactf("ok", "uid move 1 Trash")
|
||||||
|
|
||||||
|
tc.transactf("ok", "close")
|
||||||
|
tc.transactf("ok", "select Trash")
|
||||||
|
tc.transactf("ok", `uid store 1 flags (\deleted)`)
|
||||||
|
tc.transactf("ok", "expunge")
|
||||||
|
tc.transactf("ok", "noop")
|
||||||
|
|
||||||
|
tc.transactf("ok", `uid store 1 flags (\deleted)`)
|
||||||
|
tc.transactf("ok", "close")
|
||||||
|
tc.transactf("ok", "delete Trash")
|
||||||
|
|
||||||
|
if uidonly {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tc.transactf("ok", "create Trash")
|
||||||
|
tc.transactf("ok", "append inbox (\\seen) {%d+}\r\n%s", len(exampleMsg), exampleMsg)
|
||||||
|
tc.transactf("ok", "select inbox")
|
||||||
|
|
||||||
tc.transactf("ok", "fetch 1 all")
|
tc.transactf("ok", "fetch 1 all")
|
||||||
tc.transactf("ok", "fetch 1 body")
|
tc.transactf("ok", "fetch 1 body")
|
||||||
tc.transactf("ok", "fetch 1 binary[]")
|
tc.transactf("ok", "fetch 1 binary[]")
|
||||||
@ -714,9 +792,9 @@ func TestScenario(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMailbox(t *testing.T) {
|
func TestMailbox(t *testing.T) {
|
||||||
tc := start(t)
|
tc := start(t, false)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
|
|
||||||
invalid := []string{
|
invalid := []string{
|
||||||
"e\u0301", // é but as e + acute, not unicode-normalized
|
"e\u0301", // é but as e + acute, not unicode-normalized
|
||||||
@ -736,14 +814,14 @@ func TestMailbox(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMailboxDeleted(t *testing.T) {
|
func TestMailboxDeleted(t *testing.T) {
|
||||||
tc := start(t)
|
tc := start(t, false)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc2 := startNoSwitchboard(t)
|
tc2 := startNoSwitchboard(t, false)
|
||||||
defer tc2.closeNoWait()
|
defer tc2.closeNoWait()
|
||||||
|
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc2.client.Login("mjl@mox.example", password0)
|
tc2.login("mjl@mox.example", password0)
|
||||||
|
|
||||||
tc.client.Create("testbox", nil)
|
tc.client.Create("testbox", nil)
|
||||||
tc2.client.Select("testbox")
|
tc2.client.Select("testbox")
|
||||||
@ -759,9 +837,9 @@ func TestMailboxDeleted(t *testing.T) {
|
|||||||
tc2.transactf("no", "uid fetch 1 all")
|
tc2.transactf("no", "uid fetch 1 all")
|
||||||
tc2.transactf("no", "store 1 flags ()")
|
tc2.transactf("no", "store 1 flags ()")
|
||||||
tc2.transactf("no", "uid store 1 flags ()")
|
tc2.transactf("no", "uid store 1 flags ()")
|
||||||
tc2.transactf("bad", "copy 1 inbox") // msgseq 1 not available.
|
tc2.transactf("no", "copy 1 inbox")
|
||||||
tc2.transactf("no", "uid copy 1 inbox")
|
tc2.transactf("no", "uid copy 1 inbox")
|
||||||
tc2.transactf("bad", "move 1 inbox") // msgseq 1 not available.
|
tc2.transactf("no", "move 1 inbox")
|
||||||
tc2.transactf("no", "uid move 1 inbox")
|
tc2.transactf("no", "uid move 1 inbox")
|
||||||
|
|
||||||
tc2.transactf("ok", "unselect")
|
tc2.transactf("ok", "unselect")
|
||||||
@ -773,9 +851,9 @@ func TestMailboxDeleted(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestID(t *testing.T) {
|
func TestID(t *testing.T) {
|
||||||
tc := start(t)
|
tc := start(t, false)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
|
|
||||||
tc.transactf("ok", "id nil")
|
tc.transactf("ok", "id nil")
|
||||||
tc.xuntagged(imapclient.UntaggedID{"name": "mox", "version": moxvar.Version})
|
tc.xuntagged(imapclient.UntaggedID{"name": "mox", "version": moxvar.Version})
|
||||||
@ -787,54 +865,84 @@ func TestID(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSequence(t *testing.T) {
|
func TestSequence(t *testing.T) {
|
||||||
tc := start(t)
|
testSequence(t, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSequenceUIDOnly(t *testing.T) {
|
||||||
|
testSequence(t, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSequence(t *testing.T, uidonly bool) {
|
||||||
|
tc := start(t, uidonly)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc.client.Select("inbox")
|
tc.client.Select("inbox")
|
||||||
|
|
||||||
tc.transactf("bad", "fetch * all") // ../rfc/9051:7018
|
tc.transactf("bad", "fetch * all") // ../rfc/9051:7018
|
||||||
|
tc.transactf("bad", "fetch 1:* all")
|
||||||
|
tc.transactf("bad", "fetch 1:2 all")
|
||||||
tc.transactf("bad", "fetch 1 all") // ../rfc/9051:7018
|
tc.transactf("bad", "fetch 1 all") // ../rfc/9051:7018
|
||||||
|
|
||||||
tc.transactf("ok", "uid fetch 1 all") // non-existing messages are OK for uids.
|
tc.transactf("ok", "uid fetch 1 all") // non-existing messages are OK for uids.
|
||||||
tc.transactf("ok", "uid fetch * all") // * is like uidnext, a non-existing message.
|
tc.transactf("ok", "uid fetch * all") // * is like uidnext, a non-existing message.
|
||||||
|
|
||||||
|
tc.transactf("ok", "uid search return (save) all") // Empty result.
|
||||||
|
tc.transactf("ok", "uid fetch $ uid")
|
||||||
|
tc.xuntagged()
|
||||||
|
|
||||||
tc.client.Append("inbox", makeAppend(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
tc.client.Append("inbox", makeAppend(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
tc.transactf("ok", "fetch 2:1,1 uid") // We reorder 2:1 to 1:2, but we don't deduplicate numbers.
|
if !uidonly {
|
||||||
|
tc.transactf("ok", "fetch 2:1,1 uid") // We reorder 2:1 to 1:2, and we deduplicate numbers.
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1)}},
|
tc.untaggedFetch(1, 1),
|
||||||
imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2)}},
|
tc.untaggedFetch(2, 2),
|
||||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1)}},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
tc.transactf("ok", "uid fetch 3:* uid") // Because * is the last message, which is 2, the range becomes 3:2, which matches the last message.
|
tc.transactf("bad", "fetch 1:3 all")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2)}})
|
}
|
||||||
|
|
||||||
|
tc.transactf("ok", "uid fetch * flags")
|
||||||
|
tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchFlags(nil)))
|
||||||
|
|
||||||
|
tc.transactf("ok", "uid fetch 3:* flags") // Because * is the last message, which is 2, the range becomes 3:2, which matches the last message.
|
||||||
|
tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchFlags(nil)))
|
||||||
|
|
||||||
|
tc.transactf("ok", "uid fetch *:3 flags")
|
||||||
|
tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchFlags(nil)))
|
||||||
|
|
||||||
|
tc.transactf("ok", "uid search return (save) all") // Empty result.
|
||||||
|
tc.transactf("ok", "uid fetch $ flags")
|
||||||
|
tc.xuntagged(
|
||||||
|
tc.untaggedFetch(1, 1, imapclient.FetchFlags(nil)),
|
||||||
|
tc.untaggedFetch(2, 2, imapclient.FetchFlags(nil)),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test that a message that is expunged by another session can be read as long as a
|
// Test that a message that is expunged by another session can be read as long as a
|
||||||
// reference is held by a session. New sessions do not see the expunged message.
|
// reference is held by a session. New sessions do not see the expunged message.
|
||||||
// todo: possibly implement the additional reference counting. so far it hasn't been worth the trouble.
|
// todo: possibly implement the additional reference counting. so far it hasn't been worth the trouble.
|
||||||
func DisabledTestReference(t *testing.T) {
|
func DisabledTestReference(t *testing.T) {
|
||||||
tc := start(t)
|
tc := start(t, false)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc.client.Select("inbox")
|
tc.client.Select("inbox")
|
||||||
tc.client.Append("inbox", makeAppend(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
|
|
||||||
tc2 := startNoSwitchboard(t)
|
tc2 := startNoSwitchboard(t, false)
|
||||||
defer tc2.close()
|
defer tc2.close()
|
||||||
tc2.client.Login("mjl@mox.example", password0)
|
tc2.login("mjl@mox.example", password0)
|
||||||
tc2.client.Select("inbox")
|
tc2.client.Select("inbox")
|
||||||
|
|
||||||
tc.client.StoreFlagsSet("1", true, `\Deleted`)
|
tc.client.StoreFlagsSet("1", true, `\Deleted`)
|
||||||
tc.client.Expunge()
|
tc.client.Expunge()
|
||||||
|
|
||||||
tc3 := startNoSwitchboard(t)
|
tc3 := startNoSwitchboard(t, false)
|
||||||
defer tc3.close()
|
defer tc3.close()
|
||||||
tc3.client.Login("mjl@mox.example", password0)
|
tc3.login("mjl@mox.example", password0)
|
||||||
tc3.transactf("ok", `list "" "inbox" return (status (messages))`)
|
tc3.transactf("ok", `list "" "inbox" return (status (messages))`)
|
||||||
tc3.xuntagged(imapclient.UntaggedList{Separator: '/', Mailbox: "Inbox"}, imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 0}})
|
tc3.xuntagged(imapclient.UntaggedList{Separator: '/', Mailbox: "Inbox"}, imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 0}})
|
||||||
|
|
||||||
tc2.transactf("ok", "fetch 1 rfc822.size")
|
tc2.transactf("ok", "fetch 1 rfc822.size")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchRFC822Size(len(exampleMsg))}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchRFC822Size(len(exampleMsg))))
|
||||||
}
|
}
|
||||||
|
@ -7,22 +7,22 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestStarttls(t *testing.T) {
|
func TestStarttls(t *testing.T) {
|
||||||
tc := start(t)
|
tc := start(t, false)
|
||||||
tc.client.Starttls(&tls.Config{InsecureSkipVerify: true})
|
tc.client.Starttls(&tls.Config{InsecureSkipVerify: true})
|
||||||
tc.transactf("bad", "starttls") // TLS already active.
|
tc.transactf("bad", "starttls") // TLS already active.
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc.close()
|
tc.close()
|
||||||
|
|
||||||
tc = startArgs(t, true, true, false, true, "mjl")
|
tc = startArgs(t, false, true, true, false, true, "mjl")
|
||||||
tc.transactf("bad", "starttls") // TLS already active.
|
tc.transactf("bad", "starttls") // TLS already active.
|
||||||
tc.close()
|
tc.close()
|
||||||
|
|
||||||
tc = startArgs(t, true, false, false, true, "mjl")
|
tc = startArgs(t, false, true, false, false, true, "mjl")
|
||||||
tc.transactf("no", `login "mjl@mox.example" "%s"`, password0)
|
tc.transactf("no", `login "mjl@mox.example" "%s"`, password0)
|
||||||
tc.xcode("PRIVACYREQUIRED")
|
tc.xcode("PRIVACYREQUIRED")
|
||||||
tc.transactf("no", "authenticate PLAIN %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000"+password0)))
|
tc.transactf("no", "authenticate PLAIN %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000"+password0)))
|
||||||
tc.xcode("PRIVACYREQUIRED")
|
tc.xcode("PRIVACYREQUIRED")
|
||||||
tc.client.Starttls(&tls.Config{InsecureSkipVerify: true})
|
tc.client.Starttls(&tls.Config{InsecureSkipVerify: true})
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc.close()
|
tc.close()
|
||||||
}
|
}
|
||||||
|
@ -7,11 +7,19 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestStatus(t *testing.T) {
|
func TestStatus(t *testing.T) {
|
||||||
|
testStatus(t, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatusUIDOnly(t *testing.T) {
|
||||||
|
testStatus(t, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStatus(t *testing.T, uidonly bool) {
|
||||||
defer mockUIDValidity()()
|
defer mockUIDValidity()()
|
||||||
tc := start(t)
|
tc := start(t, uidonly)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
|
|
||||||
tc.transactf("bad", "status") // Missing param.
|
tc.transactf("bad", "status") // Missing param.
|
||||||
tc.transactf("bad", "status inbox") // Missing param.
|
tc.transactf("bad", "status inbox") // Missing param.
|
||||||
@ -53,7 +61,7 @@ func TestStatus(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
tc.client.Select("inbox")
|
tc.client.Select("inbox")
|
||||||
tc.client.StoreFlagsSet("1", true, `\Deleted`)
|
tc.client.UIDStoreFlagsSet("1", true, `\Deleted`)
|
||||||
tc.transactf("ok", "status inbox (messages uidnext uidvalidity unseen deleted size recent appendlimit)")
|
tc.transactf("ok", "status inbox (messages uidnext uidvalidity unseen deleted size recent appendlimit)")
|
||||||
tc.xuntagged(imapclient.UntaggedStatus{
|
tc.xuntagged(imapclient.UntaggedStatus{
|
||||||
Mailbox: "Inbox",
|
Mailbox: "Inbox",
|
||||||
|
@ -8,63 +8,74 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestStore(t *testing.T) {
|
func TestStore(t *testing.T) {
|
||||||
tc := start(t)
|
testStore(t, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStoreUIDOnly(t *testing.T) {
|
||||||
|
testStore(t, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStore(t *testing.T, uidonly bool) {
|
||||||
|
tc := start(t, uidonly)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc.client.Enable("imap4rev2")
|
tc.client.Enable("imap4rev2")
|
||||||
|
|
||||||
tc.client.Append("inbox", makeAppend(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
tc.client.Select("inbox")
|
tc.client.Select("inbox")
|
||||||
|
|
||||||
uid1 := imapclient.FetchUID(1)
|
|
||||||
noflags := imapclient.FetchFlags(nil)
|
noflags := imapclient.FetchFlags(nil)
|
||||||
|
|
||||||
|
if !uidonly {
|
||||||
tc.transactf("ok", "store 1 flags.silent ()")
|
tc.transactf("ok", "store 1 flags.silent ()")
|
||||||
tc.xuntagged()
|
tc.xuntagged()
|
||||||
|
}
|
||||||
|
|
||||||
tc.transactf("ok", `store 1 flags ()`)
|
tc.transactf("ok", `uid store 1 flags ()`)
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, noflags}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, noflags))
|
||||||
tc.transactf("ok", `fetch 1 flags`)
|
tc.transactf("ok", `uid fetch 1 flags`)
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, noflags}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, noflags))
|
||||||
|
|
||||||
tc.transactf("ok", `store 1 flags.silent (\Seen)`)
|
tc.transactf("ok", `uid store 1 flags.silent (\Seen)`)
|
||||||
tc.xuntagged()
|
tc.xuntagged()
|
||||||
tc.transactf("ok", `fetch 1 flags`)
|
tc.transactf("ok", `uid fetch 1 flags`)
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, imapclient.FetchFlags{`\Seen`}}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{`\Seen`}))
|
||||||
|
|
||||||
tc.transactf("ok", `store 1 flags ($Junk)`)
|
tc.transactf("ok", `uid store 1 flags ($Junk)`)
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, imapclient.FetchFlags{`$Junk`}}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{`$Junk`}))
|
||||||
tc.transactf("ok", `fetch 1 flags`)
|
tc.transactf("ok", `uid fetch 1 flags`)
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, imapclient.FetchFlags{`$Junk`}}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{`$Junk`}))
|
||||||
|
|
||||||
tc.transactf("ok", `store 1 +flags ()`)
|
tc.transactf("ok", `uid store 1 +flags ()`)
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, imapclient.FetchFlags{`$Junk`}}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{`$Junk`}))
|
||||||
tc.transactf("ok", `store 1 +flags (\Deleted)`)
|
tc.transactf("ok", `uid store 1 +flags (\Deleted)`)
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, imapclient.FetchFlags{`\Deleted`, `$Junk`}}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{`\Deleted`, `$Junk`}))
|
||||||
tc.transactf("ok", `fetch 1 flags`)
|
tc.transactf("ok", `uid fetch 1 flags`)
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, imapclient.FetchFlags{`\Deleted`, `$Junk`}}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{`\Deleted`, `$Junk`}))
|
||||||
|
|
||||||
tc.transactf("ok", `store 1 -flags \Deleted $Junk`)
|
tc.transactf("ok", `uid store 1 -flags \Deleted $Junk`)
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, noflags}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, noflags))
|
||||||
tc.transactf("ok", `fetch 1 flags`)
|
tc.transactf("ok", `uid fetch 1 flags`)
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, noflags}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, noflags))
|
||||||
|
|
||||||
|
if !uidonly {
|
||||||
tc.transactf("bad", "store 2 flags ()") // ../rfc/9051:7018
|
tc.transactf("bad", "store 2 flags ()") // ../rfc/9051:7018
|
||||||
|
}
|
||||||
|
|
||||||
tc.transactf("ok", "uid store 1 flags ()")
|
tc.transactf("ok", "uid store 1 flags ()")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, noflags}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, noflags))
|
||||||
|
|
||||||
tc.transactf("ok", "store 1 flags (new)") // New flag.
|
tc.transactf("ok", "uid store 1 flags (new)") // New flag.
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, imapclient.FetchFlags{"new"}}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{"new"}))
|
||||||
tc.transactf("ok", "store 1 flags (new new a b c)") // Duplicates are ignored.
|
tc.transactf("ok", "uid store 1 flags (new new a b c)") // Duplicates are ignored.
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, imapclient.FetchFlags{"a", "b", "c", "new"}}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{"a", "b", "c", "new"}))
|
||||||
tc.transactf("ok", "store 1 +flags (new new c d e)")
|
tc.transactf("ok", "uid store 1 +flags (new new c d e)")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, imapclient.FetchFlags{"a", "b", "c", "d", "e", "new"}}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{"a", "b", "c", "d", "e", "new"}))
|
||||||
tc.transactf("ok", "store 1 -flags (new new e a c)")
|
tc.transactf("ok", "uid store 1 -flags (new new e a c)")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, imapclient.FetchFlags{"b", "d"}}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{"b", "d"}))
|
||||||
tc.transactf("ok", "store 1 flags ($Forwarded Different)")
|
tc.transactf("ok", "uid store 1 flags ($Forwarded Different)")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, imapclient.FetchFlags{"$Forwarded", "different"}}})
|
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{"$Forwarded", "different"}))
|
||||||
|
|
||||||
tc.transactf("bad", "store") // Need numset, flags and args.
|
tc.transactf("bad", "store") // Need numset, flags and args.
|
||||||
tc.transactf("bad", "store 1") // Need flags.
|
tc.transactf("bad", "store 1") // Need flags.
|
||||||
@ -80,5 +91,5 @@ func TestStore(t *testing.T) {
|
|||||||
flags := strings.Split(`\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent a b c d different e new`, " ")
|
flags := strings.Split(`\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent a b c d different e new`, " ")
|
||||||
tc.xuntaggedOpt(false, imapclient.UntaggedFlags(flags))
|
tc.xuntaggedOpt(false, imapclient.UntaggedFlags(flags))
|
||||||
|
|
||||||
tc.transactf("no", `store 1 flags ()`) // No permission to set flags.
|
tc.transactf("no", `uid store 1 flags ()`) // No permission to set flags.
|
||||||
}
|
}
|
||||||
|
@ -7,14 +7,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestSubscribe(t *testing.T) {
|
func TestSubscribe(t *testing.T) {
|
||||||
tc := start(t)
|
tc := start(t, false)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc2 := startNoSwitchboard(t)
|
tc2 := startNoSwitchboard(t, false)
|
||||||
defer tc2.closeNoWait()
|
defer tc2.closeNoWait()
|
||||||
|
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc2.client.Login("mjl@mox.example", password0)
|
tc2.login("mjl@mox.example", password0)
|
||||||
|
|
||||||
tc.transactf("bad", "subscribe") // Missing param.
|
tc.transactf("bad", "subscribe") // Missing param.
|
||||||
tc.transactf("bad", "subscribe ") // Missing param.
|
tc.transactf("bad", "subscribe ") // Missing param.
|
||||||
|
39
imapserver/uidonly_test.go
Normal file
39
imapserver/uidonly_test.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package imapserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUIDOnly(t *testing.T) {
|
||||||
|
tc := start(t, true)
|
||||||
|
defer tc.close()
|
||||||
|
tc.login("mjl@mox.example", password0)
|
||||||
|
tc.client.Select("inbox")
|
||||||
|
|
||||||
|
tc.transactf("bad", "Fetch 1")
|
||||||
|
tc.xcode("UIDREQUIRED")
|
||||||
|
tc.transactf("bad", "Fetch 1")
|
||||||
|
tc.xcode("UIDREQUIRED")
|
||||||
|
tc.transactf("bad", "Search 1")
|
||||||
|
tc.xcode("UIDREQUIRED")
|
||||||
|
tc.transactf("bad", "Store 1 Flags ()")
|
||||||
|
tc.xcode("UIDREQUIRED")
|
||||||
|
tc.transactf("bad", "Copy 1 Archive")
|
||||||
|
tc.xcode("UIDREQUIRED")
|
||||||
|
tc.transactf("bad", "Move 1 Archive")
|
||||||
|
tc.xcode("UIDREQUIRED")
|
||||||
|
|
||||||
|
// Sequence numbers in search program.
|
||||||
|
tc.transactf("bad", "Uid Search 1")
|
||||||
|
tc.xcode("UIDREQUIRED")
|
||||||
|
|
||||||
|
// Sequence number in last qresync parameter.
|
||||||
|
tc.transactf("ok", "Enable Qresync")
|
||||||
|
tc.transactf("bad", "Select inbox (Qresync (1 5 (1,3,6 1,3,6)))")
|
||||||
|
tc.xcode("UIDREQUIRED")
|
||||||
|
tc.client.Select("inbox") // Select again.
|
||||||
|
|
||||||
|
// Breaks connection.
|
||||||
|
tc.transactf("bad", "replace 1 inbox {1+}\r\nx")
|
||||||
|
tc.xcode("UIDREQUIRED")
|
||||||
|
}
|
@ -7,10 +7,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestUnselect(t *testing.T) {
|
func TestUnselect(t *testing.T) {
|
||||||
tc := start(t)
|
testUnselect(t, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnselectUIDOnly(t *testing.T) {
|
||||||
|
testUnselect(t, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUnselect(t *testing.T, uidonly bool) {
|
||||||
|
tc := start(t, uidonly)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc.client.Select("inbox")
|
tc.client.Select("inbox")
|
||||||
|
|
||||||
tc.transactf("bad", "unselect bogus") // Leftover data.
|
tc.transactf("bad", "unselect bogus") // Leftover data.
|
||||||
@ -19,7 +27,7 @@ func TestUnselect(t *testing.T) {
|
|||||||
|
|
||||||
tc.client.Select("inbox")
|
tc.client.Select("inbox")
|
||||||
tc.client.Append("inbox", makeAppend(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
tc.client.StoreFlagsAdd("1", true, `\Deleted`)
|
tc.client.UIDStoreFlagsAdd("1", true, `\Deleted`)
|
||||||
tc.transactf("ok", "unselect")
|
tc.transactf("ok", "unselect")
|
||||||
tc.transactf("ok", "status inbox (messages)")
|
tc.transactf("ok", "status inbox (messages)")
|
||||||
tc.xuntagged(imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 1}}) // Message not removed.
|
tc.xuntagged(imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 1}}) // Message not removed.
|
||||||
|
@ -2,13 +2,19 @@ package imapserver
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mjl-/mox/imapclient"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUnsubscribe(t *testing.T) {
|
func TestUnsubscribe(t *testing.T) {
|
||||||
tc := start(t)
|
tc := start(t, false)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
|
|
||||||
|
tc2 := startNoSwitchboard(t, false)
|
||||||
|
defer tc2.closeNoWait()
|
||||||
|
tc2.login("mjl@mox.example", password0)
|
||||||
|
|
||||||
tc.transactf("bad", "unsubscribe") // Missing param.
|
tc.transactf("bad", "unsubscribe") // Missing param.
|
||||||
tc.transactf("bad", "unsubscribe ") // Missing param.
|
tc.transactf("bad", "unsubscribe ") // Missing param.
|
||||||
@ -17,9 +23,16 @@ func TestUnsubscribe(t *testing.T) {
|
|||||||
tc.transactf("no", "unsubscribe a/b") // Does not exist and is not subscribed.
|
tc.transactf("no", "unsubscribe a/b") // Does not exist and is not subscribed.
|
||||||
tc.transactf("ok", "unsubscribe expungebox") // Does not exist anymore but is still subscribed.
|
tc.transactf("ok", "unsubscribe expungebox") // Does not exist anymore but is still subscribed.
|
||||||
tc.transactf("no", "unsubscribe expungebox") // Not subscribed.
|
tc.transactf("no", "unsubscribe expungebox") // Not subscribed.
|
||||||
|
tc2.transactf("ok", "noop")
|
||||||
|
tc2.xuntagged(imapclient.UntaggedList{Flags: []string{`\NonExistent`}, Separator: '/', Mailbox: "expungebox"})
|
||||||
|
|
||||||
tc.transactf("ok", "create a/b")
|
tc.transactf("ok", "create a/b")
|
||||||
|
tc2.transactf("ok", "noop")
|
||||||
tc.transactf("ok", "unsubscribe a/b")
|
tc.transactf("ok", "unsubscribe a/b")
|
||||||
tc.transactf("ok", "unsubscribe a/b") // Can unsubscribe even if there is no subscription.
|
tc.transactf("ok", "unsubscribe a/b") // Can unsubscribe even if there is no subscription.
|
||||||
|
tc2.transactf("ok", "noop")
|
||||||
|
tc2.xuntagged(imapclient.UntaggedList{Flags: []string(nil), Separator: '/', Mailbox: "a/b"})
|
||||||
|
|
||||||
tc.transactf("ok", "subscribe a/b")
|
tc.transactf("ok", "subscribe a/b")
|
||||||
tc.transactf("ok", "unsubscribe a/b")
|
tc.transactf("ok", "unsubscribe a/b")
|
||||||
}
|
}
|
||||||
|
@ -242,7 +242,7 @@ https://www.iana.org/assignments/message-headers/message-headers.xhtml
|
|||||||
9208 Partial - IMAP QUOTA Extension
|
9208 Partial - IMAP QUOTA Extension
|
||||||
9394 Roadmap - IMAP PARTIAL Extension for Paged SEARCH and FETCH
|
9394 Roadmap - IMAP PARTIAL Extension for Paged SEARCH and FETCH
|
||||||
9585 Yes - IMAP Response Code for Command Progress Notifications
|
9585 Yes - IMAP Response Code for Command Progress Notifications
|
||||||
9586 Roadmap - IMAP Extension for Using and Returning Unique Identifiers (UIDs) Only
|
9586 Yes - IMAP Extension for Using and Returning Unique Identifiers (UIDs) Only
|
||||||
9590 Yes - IMAP Extension for Returning Mailbox METADATA in Extended LIST
|
9590 Yes - IMAP Extension for Returning Mailbox METADATA in Extended LIST
|
||||||
9698 ? - The JMAPACCESS Extension for IMAP
|
9698 ? - The JMAPACCESS Extension for IMAP
|
||||||
9738 No - IMAP MESSAGELIMIT Extension
|
9738 No - IMAP MESSAGELIMIT Extension
|
||||||
|
Loading…
x
Reference in New Issue
Block a user