mirror of
https://github.com/mjl-/mox.git
synced 2025-04-21 21:40:01 +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
41 changed files with 2405 additions and 1545 deletions
README.md
imapclient
imapserver
append_test.goauthenticate_test.gocompress_test.gocondstore_test.gocopy_test.gocreate_test.godelete_test.goerror.goexpunge_test.gofetch.gofetch_test.goidle_test.golist_test.golsub_test.gometadata_test.gomove_test.gonotify_test.goparse.goprotocol.goprotocol_test.goquota_test.gorename_test.goreplace.goreplace_test.gosearch.gosearch_test.goselectexamine_test.goserver.goserver_test.gostarttls_test.gostatus_test.gostore_test.gosubscribe_test.gouidonly_test.gounselect_test.gounsubscribe_test.go
rfc
|
@ -145,7 +145,6 @@ support:
|
|||
- Automate DNS management, for setup and maintenance, such as DANE/DKIM key rotation
|
||||
- Config options for "transactional email domains", for which mox will only
|
||||
send messages
|
||||
- More IMAP extensions (UIDONLY)
|
||||
- Encrypted storage of files (email messages, TLS keys), also with per account keys
|
||||
- Recognize common deliverability issues and help postmasters solve them
|
||||
- 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, " "))
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (c *Conn) Copy(seqSet NumSet, dstMailbox string) (untagged []Untagged, result Result, rerr error) {
|
||||
defer c.recover(&rerr)
|
||||
|
|
|
@ -136,6 +136,7 @@ var knownCodes = stringMap(
|
|||
"INPROGRESS", // ../rfc/9585:104
|
||||
"BADEVENT", "NOTIFICATIONOVERFLOW", // ../rfc/5465:1023
|
||||
"SERVERBUG",
|
||||
"UIDREQUIRED", // ../rfc/9586:136
|
||||
)
|
||||
|
||||
func stringMap(l ...string) map[string]struct{} {
|
||||
|
@ -654,14 +655,17 @@ func (c *Conn) xuntagged() Untagged {
|
|||
w = c.xword()
|
||||
W = strings.ToUpper(w)
|
||||
switch W {
|
||||
case "FETCH":
|
||||
case "FETCH", "UIDFETCH":
|
||||
if num == 0 {
|
||||
c.xerrorf("invalid zero number for untagged fetch response")
|
||||
}
|
||||
c.xspace()
|
||||
r := c.xfetch(num)
|
||||
attrs := c.xfetch()
|
||||
c.xcrlf()
|
||||
return r
|
||||
if W == "UIDFETCH" {
|
||||
return UntaggedUIDFetch{num, attrs}
|
||||
}
|
||||
return UntaggedFetch{num, attrs}
|
||||
|
||||
case "EXPUNGE":
|
||||
if num == 0 {
|
||||
|
@ -691,14 +695,14 @@ func (c *Conn) xuntagged() Untagged {
|
|||
|
||||
// ../rfc/3501:4864 ../rfc/9051:6742
|
||||
// Already parsed: "*" SP nznumber SP "FETCH" SP
|
||||
func (c *Conn) xfetch(num uint32) UntaggedFetch {
|
||||
func (c *Conn) xfetch() []FetchAttr {
|
||||
c.xtake("(")
|
||||
attrs := []FetchAttr{c.xmsgatt1()}
|
||||
for c.space() {
|
||||
attrs = append(attrs, c.xmsgatt1())
|
||||
}
|
||||
c.xtake(")")
|
||||
return UntaggedFetch{num, attrs}
|
||||
return attrs
|
||||
}
|
||||
|
||||
// ../rfc/9051:6746
|
||||
|
|
|
@ -43,6 +43,7 @@ const (
|
|||
CapPreview Capability = "PREVIEW" // ../rfc/8970:114
|
||||
CapMultiSearch Capability = "MULTISEARCH" // ../rfc/7377:187
|
||||
CapNotify Capability = "NOTIFY" // ../rfc/5465:195
|
||||
CapUIDOnly Capability = "UIDONLY" // ../rfc/9586:129
|
||||
)
|
||||
|
||||
// Status is the tagged final result of a command.
|
||||
|
@ -261,6 +262,14 @@ type UntaggedFetch struct {
|
|||
Seq uint32
|
||||
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
|
||||
|
||||
// ../rfc/7162:1101
|
||||
|
|
|
@ -7,22 +7,30 @@ import (
|
|||
)
|
||||
|
||||
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()()
|
||||
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
tc3 := startNoSwitchboard(t)
|
||||
tc3 := startNoSwitchboard(t, uidonly)
|
||||
defer tc3.closeNoWait()
|
||||
|
||||
tc2.client.Login("mjl@mox.example", password0)
|
||||
tc2.login("mjl@mox.example", password0)
|
||||
tc2.client.Select("inbox")
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
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 inbox`) // Missing message.
|
||||
|
@ -30,15 +38,15 @@ func TestAppend(t *testing.T) {
|
|||
|
||||
// Syntax error for line ending in literal causes connection abort.
|
||||
tc2.transactf("bad", "append inbox (\\Badflag) {1+}\r\nx") // Unknown flag.
|
||||
tc2 = startNoSwitchboard(t)
|
||||
tc2 = startNoSwitchboard(t, uidonly)
|
||||
defer tc2.closeNoWait()
|
||||
tc2.client.Login("mjl@mox.example", password0)
|
||||
tc2.login("mjl@mox.example", password0)
|
||||
tc2.client.Select("inbox")
|
||||
|
||||
tc2.transactf("bad", "append inbox () \"bad time\" {1+}\r\nx") // Bad time.
|
||||
tc2 = startNoSwitchboard(t)
|
||||
tc2 = startNoSwitchboard(t, uidonly)
|
||||
defer tc2.closeNoWait()
|
||||
tc2.client.Login("mjl@mox.example", password0)
|
||||
tc2.login("mjl@mox.example", password0)
|
||||
tc2.client.Select("inbox")
|
||||
|
||||
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")})
|
||||
|
||||
tc.transactf("ok", "noop")
|
||||
uid1 := imapclient.FetchUID(1)
|
||||
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.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
|
||||
// the imap client knows how to deal with them.
|
||||
tc2.transactf("ok", "uid fetch 2 body")
|
||||
uid2 := imapclient.FetchUID(2)
|
||||
xbs := imapclient.FetchBodystructure{
|
||||
RespAttr: "BODY",
|
||||
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.
|
||||
tc.transactf("ok", "noop") // Flush pending untagged responses.
|
||||
|
@ -91,9 +97,9 @@ func TestAppend(t *testing.T) {
|
|||
// Cancelled with zero-length message.
|
||||
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()
|
||||
tclimit.client.Login("limit@mox.example", password0)
|
||||
tclimit.login("limit@mox.example", password0)
|
||||
tclimit.client.Select("inbox")
|
||||
// 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")
|
||||
|
@ -103,7 +109,11 @@ func TestAppend(t *testing.T) {
|
|||
tclimit.xcode("OVERQUOTA")
|
||||
|
||||
// Empty mailbox.
|
||||
tclimit.transactf("ok", `store 1 flags (\deleted)`)
|
||||
if uidonly {
|
||||
tclimit.transactf("ok", `uid store 1 flags (\deleted)`)
|
||||
} else {
|
||||
tclimit.transactf("ok", `store 1 flags (\deleted)`)
|
||||
}
|
||||
tclimit.transactf("ok", "expunge")
|
||||
|
||||
// Multiappend with first message within quota, and second message with sync
|
||||
|
|
|
@ -27,13 +27,13 @@ import (
|
|||
|
||||
func TestAuthenticateLogin(t *testing.T) {
|
||||
// NFD username and PRECIS-cleaned password.
|
||||
tc := start(t)
|
||||
tc := start(t, false)
|
||||
tc.client.Login("mo\u0301x@mox.example", password1)
|
||||
tc.close()
|
||||
}
|
||||
|
||||
func TestAuthenticatePlain(t *testing.T) {
|
||||
tc := start(t)
|
||||
tc := start(t, false)
|
||||
|
||||
tc.transactf("no", "authenticate bogus ")
|
||||
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.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.close()
|
||||
|
||||
// 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.close()
|
||||
|
||||
tc = start(t)
|
||||
tc = start(t, false)
|
||||
tc.client.AuthenticatePlain("mjl@mox.example", password0)
|
||||
tc.close()
|
||||
|
||||
tc = start(t)
|
||||
tc = start(t, false)
|
||||
defer tc.close()
|
||||
|
||||
tc.cmdf("", "authenticate plain")
|
||||
|
@ -82,7 +82,7 @@ func TestAuthenticatePlain(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLoginDisabled(t *testing.T) {
|
||||
tc := start(t)
|
||||
tc := start(t, false)
|
||||
defer tc.close()
|
||||
|
||||
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) {
|
||||
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.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", "")
|
||||
// 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) {
|
||||
tc := start(t)
|
||||
tc := start(t, false)
|
||||
|
||||
tc.transactf("no", "authenticate bogus ")
|
||||
tc.transactf("bad", "authenticate CRAM-MD5 not base64...")
|
||||
|
@ -234,13 +234,13 @@ func TestAuthenticateCRAMMD5(t *testing.T) {
|
|||
tc.close()
|
||||
|
||||
// NFD username, with PRECIS-cleaned password.
|
||||
tc = start(t)
|
||||
tc = start(t, false)
|
||||
auth("ok", "mo\u0301x@mox.example", password1)
|
||||
tc.close()
|
||||
}
|
||||
|
||||
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.close()
|
||||
|
||||
|
@ -263,7 +263,7 @@ func TestAuthenticateTLSClientCert(t *testing.T) {
|
|||
}
|
||||
|
||||
// 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 {
|
||||
t.Fatalf("preauthentication while not configured for tls public key")
|
||||
}
|
||||
|
@ -271,7 +271,7 @@ func TestAuthenticateTLSClientCert(t *testing.T) {
|
|||
tc.close()
|
||||
|
||||
// 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 {
|
||||
t.Fatalf("preauthentication while not configured for tls public key")
|
||||
}
|
||||
|
@ -279,12 +279,12 @@ func TestAuthenticateTLSClientCert(t *testing.T) {
|
|||
tc.close()
|
||||
|
||||
// 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.close()
|
||||
|
||||
// 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.close()
|
||||
|
||||
|
@ -295,12 +295,12 @@ func TestAuthenticateTLSClientCert(t *testing.T) {
|
|||
tcheck(t, err, "set password")
|
||||
err = acc.Close()
|
||||
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.close()
|
||||
|
||||
// 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.transactf("ok", "authenticate external =")
|
||||
tc.close()
|
||||
|
@ -318,7 +318,7 @@ func TestAuthenticateTLSClientCert(t *testing.T) {
|
|||
defer cancel()
|
||||
mox.StartTLSSessionTicketKeyRefresher(ctx, pkglog, &serverConfig)
|
||||
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 {
|
||||
t.Fatalf("not preauthentication while configured for tls public key")
|
||||
}
|
||||
|
@ -330,7 +330,7 @@ func TestAuthenticateTLSClientCert(t *testing.T) {
|
|||
tc.close()
|
||||
|
||||
// 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 {
|
||||
t.Fatalf("not preauthentication while configured for tls public key")
|
||||
}
|
||||
|
|
|
@ -10,10 +10,10 @@ import (
|
|||
)
|
||||
|
||||
func TestCompress(t *testing.T) {
|
||||
tc := start(t)
|
||||
tc := start(t, false)
|
||||
defer tc.close()
|
||||
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
|
||||
tc.transactf("bad", "compress")
|
||||
tc.transactf("bad", "compress bogus ")
|
||||
|
@ -30,11 +30,11 @@ func TestCompress(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCompressStartTLS(t *testing.T) {
|
||||
tc := start(t)
|
||||
tc := start(t, false)
|
||||
defer tc.close()
|
||||
|
||||
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.Select("inbox")
|
||||
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
|
||||
// its broken internal state it may cause array out of bounds accesses.
|
||||
|
||||
tc := start(t)
|
||||
tc := start(t, false)
|
||||
defer tc.close()
|
||||
|
||||
msg := exampleMsg
|
||||
|
@ -69,7 +69,7 @@ func TestCompressBreak(t *testing.T) {
|
|||
text = text[n:]
|
||||
}
|
||||
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
tc.client.CompressDeflate()
|
||||
tc.client.Select("inbox")
|
||||
tc.transactf("ok", "append inbox (\\seen) {%d+}\r\n%s", len(msg), msg)
|
||||
|
|
|
@ -14,16 +14,24 @@ import (
|
|||
)
|
||||
|
||||
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) {
|
||||
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()()
|
||||
tc := start(t)
|
||||
tc := start(t, uidonly)
|
||||
defer tc.close()
|
||||
|
||||
// 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"
|
||||
}
|
||||
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
tc.client.Enable(capability)
|
||||
tc.transactf("ok", "Select inbox")
|
||||
tc.xuntaggedOpt(false, imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(2), More: "x"}})
|
||||
|
@ -57,25 +65,27 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
|
|||
tc.transactf("ok", "Uid Fetch 1:* (Flags Modseq) (Changedsince 1)")
|
||||
tc.xuntagged()
|
||||
|
||||
// Search with modseq search criteria.
|
||||
tc.transactf("ok", "Search Modseq 0") // Zero is valid, matches all.
|
||||
tc.xsearch()
|
||||
if !uidonly {
|
||||
// Search with modseq search criteria.
|
||||
tc.transactf("ok", "Search Modseq 0") // Zero is valid, matches all.
|
||||
tc.xsearch()
|
||||
|
||||
tc.transactf("ok", "Search Modseq 1") // Converted to zero internally.
|
||||
tc.xsearch()
|
||||
tc.transactf("ok", "Search Modseq 1") // Converted to zero internally.
|
||||
tc.xsearch()
|
||||
|
||||
tc.transactf("ok", "Search Modseq 12345")
|
||||
tc.xsearch()
|
||||
tc.transactf("ok", "Search Modseq 12345")
|
||||
tc.xsearch()
|
||||
|
||||
tc.transactf("ok", `Search Modseq "/Flags/\\Draft" All 12345`)
|
||||
tc.xsearch()
|
||||
tc.transactf("ok", `Search Modseq "/Flags/\\Draft" All 12345`)
|
||||
tc.xsearch()
|
||||
|
||||
tc.transactf("ok", `Search Or Modseq 12345 Modseq 54321`)
|
||||
tc.xsearch()
|
||||
tc.transactf("ok", `Search Or Modseq 12345 Modseq 54321`)
|
||||
tc.xsearch()
|
||||
|
||||
// esearch
|
||||
tc.transactf("ok", "Search Return (All) Modseq 123")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{})
|
||||
// esearch
|
||||
tc.transactf("ok", "Search Return (All) Modseq 123")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{})
|
||||
}
|
||||
|
||||
// Now we add, delete, expunge, modify some message flags and check if the
|
||||
// responses are correct. We check in both a condstore-enabled and one without that
|
||||
|
@ -102,15 +112,15 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
|
|||
tc.client.Create("otherbox", nil)
|
||||
|
||||
// tc2 is a client without condstore, so no modseq responses.
|
||||
tc2 := startNoSwitchboard(t)
|
||||
tc2 := startNoSwitchboard(t, uidonly)
|
||||
defer tc2.closeNoWait()
|
||||
tc2.client.Login("mjl@mox.example", password0)
|
||||
tc2.login("mjl@mox.example", password0)
|
||||
tc2.client.Select("inbox")
|
||||
|
||||
// tc3 is a client with condstore, so with modseq responses.
|
||||
tc3 := startNoSwitchboard(t)
|
||||
tc3 := startNoSwitchboard(t, uidonly)
|
||||
defer tc3.closeNoWait()
|
||||
tc3.client.Login("mjl@mox.example", password0)
|
||||
tc3.login("mjl@mox.example", password0)
|
||||
tc3.client.Enable(capability)
|
||||
tc3.client.Select("inbox")
|
||||
|
||||
|
@ -141,24 +151,26 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
|
|||
noflags := imapclient.FetchFlags(nil)
|
||||
tc2.xuntagged(
|
||||
imapclient.UntaggedExists(6),
|
||||
imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(4), noflags}},
|
||||
imapclient.UntaggedFetch{Seq: 5, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags}},
|
||||
imapclient.UntaggedFetch{Seq: 6, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags}},
|
||||
tc2.untaggedFetch(4, 4, noflags),
|
||||
tc2.untaggedFetch(5, 5, noflags),
|
||||
tc2.untaggedFetch(6, 6, noflags),
|
||||
)
|
||||
|
||||
tc3.transactf("ok", "Noop")
|
||||
tc3.xuntagged(
|
||||
imapclient.UntaggedExists(6),
|
||||
imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(4), noflags, imapclient.FetchModSeq(clientModseq + 1)}},
|
||||
imapclient.UntaggedFetch{Seq: 5, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(clientModseq + 3)}},
|
||||
imapclient.UntaggedFetch{Seq: 6, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq + 4)}},
|
||||
tc3.untaggedFetch(4, 4, noflags, imapclient.FetchModSeq(clientModseq+1)),
|
||||
tc3.untaggedFetch(5, 5, noflags, imapclient.FetchModSeq(clientModseq+3)),
|
||||
tc3.untaggedFetch(6, 6, noflags, imapclient.FetchModSeq(clientModseq+4)),
|
||||
)
|
||||
|
||||
mox.SetPedantic(true)
|
||||
tc.transactf("bad", `Fetch 1 Flags (Changedsince 0)`) // 0 not allowed in syntax.
|
||||
mox.SetPedantic(false)
|
||||
if !uidonly {
|
||||
mox.SetPedantic(true)
|
||||
tc.transactf("bad", `Fetch 1 Flags (Changedsince 0)`) // 0 not allowed in syntax.
|
||||
mox.SetPedantic(false)
|
||||
}
|
||||
tc.transactf("ok", "Uid fetch 1 (Flags) (Changedsince 0)")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(1)}})
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, noflags, imapclient.FetchModSeq(1)))
|
||||
|
||||
// Check highestmodseq for mailboxes.
|
||||
tc.transactf("ok", "Status inbox (highestmodseq)")
|
||||
|
@ -176,54 +188,67 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
|
|||
|
||||
clientModseq += 4
|
||||
|
||||
// Check fetch modseq response and changedsince.
|
||||
tc.transactf("ok", `Fetch 1 (Modseq)`)
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchModSeq(1)}})
|
||||
if !uidonly {
|
||||
// Check fetch modseq response and changedsince.
|
||||
tc.transactf("ok", `Fetch 1 (Modseq)`)
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchModSeq(1)))
|
||||
}
|
||||
|
||||
// Without modseq attribute, even with condseq enabled, there is no modseq response.
|
||||
// For QRESYNC, we must always send MODSEQ for UID FETCH commands, but not for FETCH commands. ../rfc/7162:1427
|
||||
tc.transactf("ok", `Uid Fetch 1 Flags`)
|
||||
if qresync {
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(1)}})
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, noflags, imapclient.FetchModSeq(1)))
|
||||
} 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}})
|
||||
|
||||
// When CHANGEDSINCE is present, MODSEQ is automatically added to the response.
|
||||
// ../rfc/7162:871
|
||||
// ../rfc/7162:877
|
||||
tc.transactf("ok", `Fetch 1 Flags (Changedsince 1)`)
|
||||
tc.xuntagged()
|
||||
tc.transactf("ok", `Fetch 1,4 Flags (Changedsince 1)`)
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(4), noflags, imapclient.FetchModSeq(3)}})
|
||||
tc.transactf("ok", `Fetch 2 Flags (Changedsince 2)`)
|
||||
tc.xuntagged()
|
||||
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.
|
||||
// ../rfc/7162:871
|
||||
// ../rfc/7162:877
|
||||
tc.transactf("ok", `Fetch 1 Flags (Changedsince 1)`)
|
||||
tc.xuntagged()
|
||||
tc.transactf("ok", `Fetch 1,4 Flags (Changedsince 1)`)
|
||||
tc.xuntagged(tc.untaggedFetch(4, 4, noflags, imapclient.FetchModSeq(3)))
|
||||
tc.transactf("ok", `Fetch 2 Flags (Changedsince 2)`)
|
||||
tc.xuntagged()
|
||||
}
|
||||
|
||||
// store and uid store.
|
||||
|
||||
// unchangedsince 0 never passes the check. ../rfc/7162:640
|
||||
tc.transactf("ok", `Store 1 (Unchangedsince 0) +Flags ()`)
|
||||
tc.xcodeArg(imapclient.CodeModified(xparseNumSet("1")))
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(1)}})
|
||||
if !uidonly {
|
||||
// unchangedsince 0 never passes the check. ../rfc/7162:640
|
||||
tc.transactf("ok", `Store 1 (Unchangedsince 0) +Flags ()`)
|
||||
tc.xcodeArg(imapclient.CodeModified(xparseNumSet("1")))
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, noflags, imapclient.FetchModSeq(1)))
|
||||
}
|
||||
|
||||
// Modseq is 2 for first condstore-aware-appended message, so also no match.
|
||||
tc.transactf("ok", `Uid Store 4 (Unchangedsince 1) +Flags ()`)
|
||||
tc.xcodeArg(imapclient.CodeModified(xparseNumSet("4")))
|
||||
|
||||
// Modseq is 1 for original message.
|
||||
tc.transactf("ok", `Store 1 (Unchangedsince 1) +Flags (label1)`)
|
||||
if uidonly {
|
||||
tc.transactf("ok", `Uid Store 1 (Unchangedsince 1) +Flags (label1)`)
|
||||
} else {
|
||||
// Modseq is 1 for original message.
|
||||
tc.transactf("ok", `Store 1 (Unchangedsince 1) +Flags (label1)`)
|
||||
}
|
||||
tc.xcode("") // No MODIFIED.
|
||||
clientModseq++
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchFlags{"label1"}, imapclient.FetchModSeq(clientModseq)}})
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{"label1"}, imapclient.FetchModSeq(clientModseq)))
|
||||
tc2.transactf("ok", "Noop")
|
||||
tc2.xuntagged(
|
||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchFlags{"label1"}}},
|
||||
tc2.untaggedFetch(1, 1, imapclient.FetchFlags{"label1"}),
|
||||
)
|
||||
tc3.transactf("ok", "Noop")
|
||||
tc3.xuntagged(
|
||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchFlags{"label1"}, imapclient.FetchModSeq(clientModseq)}},
|
||||
tc3.untaggedFetch(1, 1, imapclient.FetchFlags{"label1"}, imapclient.FetchModSeq(clientModseq)),
|
||||
)
|
||||
|
||||
// Modify same message twice. Check that second application doesn't fail due to
|
||||
|
@ -232,65 +257,76 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
|
|||
clientModseq++
|
||||
tc.xcode("") // No MODIFIED.
|
||||
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.
|
||||
tc2.transactf("ok", "Noop")
|
||||
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.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)),
|
||||
)
|
||||
|
||||
// Modify without actually changing flags, there will be no new modseq and no broadcast.
|
||||
tc.transactf("ok", `Store 1 (Unchangedsince %d) -Flags (label1)`, clientModseq)
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchFlags(nil), imapclient.FetchModSeq(clientModseq)}})
|
||||
tc.xcode("") // No MODIFIED.
|
||||
tc2.transactf("ok", "Noop")
|
||||
tc2.xuntagged()
|
||||
tc3.transactf("ok", "Noop")
|
||||
tc3.xuntagged()
|
||||
if !uidonly {
|
||||
// Modify without actually changing flags, there will be no new modseq and no broadcast.
|
||||
tc.transactf("ok", `Store 1 (Unchangedsince %d) -Flags (label1)`, clientModseq)
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags(nil), imapclient.FetchModSeq(clientModseq)))
|
||||
tc.xcode("") // No MODIFIED.
|
||||
tc2.transactf("ok", "Noop")
|
||||
tc2.xuntagged()
|
||||
tc3.transactf("ok", "Noop")
|
||||
tc3.xuntagged()
|
||||
|
||||
// search with modseq criteria and modseq in response
|
||||
tc.transactf("ok", "Search Modseq %d", clientModseq)
|
||||
tc.xsearchmodseq(clientModseq, 1)
|
||||
// search with modseq criteria and modseq in response
|
||||
tc.transactf("ok", "Search Modseq %d", clientModseq)
|
||||
tc.xsearchmodseq(clientModseq, 1)
|
||||
}
|
||||
|
||||
tc.transactf("ok", "Uid Search Or Modseq %d Modseq %d", clientModseq, clientModseq)
|
||||
tc.xsearchmodseq(clientModseq, 1)
|
||||
|
||||
// esearch
|
||||
tc.transactf("ok", "Search Return (Min Max All) 1:* Modseq %d", clientModseq)
|
||||
tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 1, All: esearchall0("1"), ModSeq: clientModseq})
|
||||
if !uidonly {
|
||||
// esearch
|
||||
tc.transactf("ok", "Search Return (Min Max All) 1:* Modseq %d", clientModseq)
|
||||
tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 1, All: esearchall0("1"), ModSeq: clientModseq})
|
||||
|
||||
tc.transactf("ok", "Search Return (Count) 1:* Modseq 0")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{Count: uint32ptr(6), ModSeq: clientModseq})
|
||||
tc.transactf("ok", "Search Return (Count) 1:* Modseq 0")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{Count: uint32ptr(6), ModSeq: clientModseq})
|
||||
|
||||
tc.transactf("ok", "Search Return (Min Max) 1:* Modseq 0")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 6, ModSeq: clientModseq})
|
||||
tc.transactf("ok", "Search Return (Min Max) 1:* Modseq 0")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 6, ModSeq: clientModseq})
|
||||
|
||||
tc.transactf("ok", "Search Return (Min) 1:* Modseq 0")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{Min: 1, ModSeq: clientModseq})
|
||||
tc.transactf("ok", "Search Return (Min) 1:* Modseq 0")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{Min: 1, ModSeq: clientModseq})
|
||||
|
||||
// expunge, we expunge the third and fourth messages. The third was originally with
|
||||
// modseq 0, the fourth was added with condstore-aware append.
|
||||
tc.transactf("ok", `Store 3:4 +Flags (\Deleted)`)
|
||||
clientModseq++
|
||||
// expunge, we expunge the third and fourth messages. The third was originally with
|
||||
// modseq 0, the fourth was added with condstore-aware append.
|
||||
tc.transactf("ok", `Store 3:4 +Flags (\Deleted)`)
|
||||
clientModseq++
|
||||
} else {
|
||||
tc.transactf("ok", `Uid Store 3,4 +Flags (\Deleted)`)
|
||||
clientModseq++
|
||||
}
|
||||
tc2.transactf("ok", "Noop")
|
||||
tc3.transactf("ok", "Noop")
|
||||
tc.transactf("ok", "Expunge")
|
||||
clientModseq++
|
||||
if qresync {
|
||||
if qresync || uidonly {
|
||||
tc.xuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("3:4")})
|
||||
} else {
|
||||
tc.xuntagged(imapclient.UntaggedExpunge(3), imapclient.UntaggedExpunge(3))
|
||||
}
|
||||
tc.xcodeArg(imapclient.CodeHighestModSeq(clientModseq))
|
||||
tc2.transactf("ok", "Noop")
|
||||
tc2.xuntagged(imapclient.UntaggedExpunge(3), imapclient.UntaggedExpunge(3))
|
||||
if uidonly {
|
||||
tc2.xuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("3:4")})
|
||||
} else {
|
||||
tc2.xuntagged(imapclient.UntaggedExpunge(3), imapclient.UntaggedExpunge(3))
|
||||
}
|
||||
tc3.transactf("ok", "Noop")
|
||||
if qresync {
|
||||
if qresync || uidonly {
|
||||
tc3.xuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("3:4")})
|
||||
} else {
|
||||
tc3.xuntagged(imapclient.UntaggedExpunge(3), imapclient.UntaggedExpunge(3))
|
||||
|
@ -307,28 +343,32 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
|
|||
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(clientModseq), More: "x"}},
|
||||
)
|
||||
|
||||
tc.transactf("ok", `Fetch 1:* (Modseq)`)
|
||||
tc.xuntagged(
|
||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchModSeq(8)}},
|
||||
imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2), imapclient.FetchModSeq(1)}},
|
||||
imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), imapclient.FetchModSeq(5)}},
|
||||
imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), imapclient.FetchModSeq(6)}},
|
||||
)
|
||||
if !uidonly {
|
||||
tc.transactf("ok", `Fetch 1:* (Modseq)`)
|
||||
tc.xuntagged(
|
||||
tc.untaggedFetch(1, 1, imapclient.FetchModSeq(8)),
|
||||
tc.untaggedFetch(2, 2, imapclient.FetchModSeq(1)),
|
||||
tc.untaggedFetch(3, 5, imapclient.FetchModSeq(5)),
|
||||
tc.untaggedFetch(4, 6, imapclient.FetchModSeq(6)),
|
||||
)
|
||||
}
|
||||
// Expunged messages, with higher modseq, should not show up.
|
||||
tc.transactf("ok", "Uid Fetch 1:* (flags) (Changedsince 8)")
|
||||
tc.xuntagged()
|
||||
|
||||
// search
|
||||
tc.transactf("ok", "Search Modseq 8")
|
||||
tc.xsearchmodseq(8, 1)
|
||||
tc.transactf("ok", "Search Modseq 9")
|
||||
tc.xsearch()
|
||||
if !uidonly {
|
||||
// search
|
||||
tc.transactf("ok", "Search Modseq 8")
|
||||
tc.xsearchmodseq(8, 1)
|
||||
tc.transactf("ok", "Search Modseq 9")
|
||||
tc.xsearch()
|
||||
|
||||
// esearch
|
||||
tc.transactf("ok", "Search Return (Min Max All) 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.xuntagged(imapclient.UntaggedEsearch{Tag: tc.client.LastTag})
|
||||
// esearch
|
||||
tc.transactf("ok", "Search Return (Min Max All) 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.xuntagged(imapclient.UntaggedEsearch{Tag: tc.client.LastTag})
|
||||
}
|
||||
|
||||
// store, cannot modify expunged messages.
|
||||
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)) {
|
||||
t.Helper()
|
||||
|
||||
xtc := startNoSwitchboard(t)
|
||||
xtc := startNoSwitchboard(t, uidonly)
|
||||
// We have modified modseq & createseq to 0 above for testing that case. Don't
|
||||
// trigger the consistency checker.
|
||||
defer xtc.closeNoWait()
|
||||
xtc.client.Login("mjl@mox.example", password0)
|
||||
xtc.login("mjl@mox.example", password0)
|
||||
fn(xtc)
|
||||
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++
|
||||
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
|
||||
checkCondstoreEnabled(func(xtc *testconn) {
|
||||
|
@ -378,25 +418,25 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
|
|||
checkCondstoreEnabled(func(xtc *testconn) {
|
||||
t.Helper()
|
||||
xtc.transactf("ok", "Select inbox")
|
||||
xtc.transactf("ok", "Fetch 4 (Modseq)")
|
||||
xtc.transactf("ok", "Uid Fetch 6 (Modseq)")
|
||||
})
|
||||
// SEARCH with MODSEQ ../rfc/7162:377
|
||||
checkCondstoreEnabled(func(xtc *testconn) {
|
||||
t.Helper()
|
||||
xtc.transactf("ok", "Select inbox")
|
||||
xtc.transactf("ok", "Search 4 Modseq 1")
|
||||
xtc.transactf("ok", "Uid Search Uid 6 Modseq 1")
|
||||
})
|
||||
// FETCH with CHANGEDSINCE ../rfc/7162:380
|
||||
checkCondstoreEnabled(func(xtc *testconn) {
|
||||
t.Helper()
|
||||
xtc.transactf("ok", "Select inbox")
|
||||
xtc.transactf("ok", "Fetch 4 (Flags) (Changedsince %d)", clientModseq)
|
||||
xtc.transactf("ok", "Uid Fetch 6 (Flags) (Changedsince %d)", clientModseq)
|
||||
})
|
||||
// STORE with UNCHANGEDSINCE ../rfc/7162:382
|
||||
checkCondstoreEnabled(func(xtc *testconn) {
|
||||
t.Helper()
|
||||
xtc.transactf("ok", "Select inbox")
|
||||
xtc.transactf("ok", "Store 4 (Unchangedsince 0) Flags ()")
|
||||
xtc.transactf("ok", "Uid Store 6 (Unchangedsince 0) Flags ()")
|
||||
})
|
||||
// ENABLE CONDSTORE ../rfc/7162:384
|
||||
checkCondstoreEnabled(func(xtc *testconn) {
|
||||
|
@ -410,11 +450,12 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
|
|||
xtc.transactf("ok", "Enable Qresync")
|
||||
xtc.transactf("ok", "Select inbox")
|
||||
})
|
||||
tc.transactf("ok", "Store 4 Flags ()")
|
||||
clientModseq++
|
||||
|
||||
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.
|
||||
|
@ -423,31 +464,31 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
|
|||
tc.transactf("ok", "Select otherbox")
|
||||
tc2.transactf("ok", "Noop")
|
||||
tc3.transactf("ok", "Noop")
|
||||
tc.transactf("ok", "Copy 1 inbox")
|
||||
tc.transactf("ok", "Uid Copy 1 inbox")
|
||||
clientModseq++
|
||||
tc2.transactf("ok", "Noop")
|
||||
tc3.transactf("ok", "Noop")
|
||||
tc2.xuntagged(
|
||||
imapclient.UntaggedExists(5),
|
||||
imapclient.UntaggedFetch{Seq: 5, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(7), noflags}},
|
||||
tc2.untaggedFetch(5, 7, noflags),
|
||||
)
|
||||
tc3.xuntagged(
|
||||
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
|
||||
// and untagged fetch with modseq in destination mailbox.
|
||||
// tc2o is a client without condstore, so no modseq responses.
|
||||
tc2o := startNoSwitchboard(t)
|
||||
tc2o := startNoSwitchboard(t, uidonly)
|
||||
defer tc2o.closeNoWait()
|
||||
tc2o.client.Login("mjl@mox.example", password0)
|
||||
tc2o.login("mjl@mox.example", password0)
|
||||
tc2o.client.Select("otherbox")
|
||||
|
||||
// tc3o is a client with condstore, so with modseq responses.
|
||||
tc3o := startNoSwitchboard(t)
|
||||
tc3o := startNoSwitchboard(t, uidonly)
|
||||
defer tc3o.closeNoWait()
|
||||
tc3o.client.Login("mjl@mox.example", password0)
|
||||
tc3o.login("mjl@mox.example", password0)
|
||||
tc3o.client.Enable(capability)
|
||||
tc3o.client.Select("otherbox")
|
||||
|
||||
|
@ -457,14 +498,21 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
|
|||
if qresync {
|
||||
tc.xuntaggedOpt(false, imapclient.UntaggedVanished{UIDs: xparseNumSet("2")})
|
||||
tc.xcodeArg(imapclient.CodeHighestModSeq(clientModseq))
|
||||
} else if uidonly {
|
||||
tc.xuntaggedOpt(false, imapclient.UntaggedVanished{UIDs: xparseNumSet("2")})
|
||||
tc.xcode("")
|
||||
} else {
|
||||
tc.xuntaggedOpt(false, imapclient.UntaggedExpunge(2))
|
||||
tc.xcode("")
|
||||
}
|
||||
tc2.transactf("ok", "Noop")
|
||||
tc2.xuntagged(imapclient.UntaggedExpunge(2))
|
||||
if uidonly {
|
||||
tc2.xuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("2")})
|
||||
} else {
|
||||
tc2.xuntagged(imapclient.UntaggedExpunge(2))
|
||||
}
|
||||
tc3.transactf("ok", "Noop")
|
||||
if qresync {
|
||||
if qresync || uidonly {
|
||||
tc3.xuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("2")})
|
||||
} else {
|
||||
tc3.xuntagged(imapclient.UntaggedExpunge(2))
|
||||
|
@ -472,12 +520,12 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
|
|||
tc2o.transactf("ok", "Noop")
|
||||
tc2o.xuntagged(
|
||||
imapclient.UntaggedExists(2),
|
||||
imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2), noflags}},
|
||||
tc2o.untaggedFetch(2, 2, noflags),
|
||||
)
|
||||
tc3o.transactf("ok", "Noop")
|
||||
tc3o.xuntagged(
|
||||
imapclient.UntaggedExists(2),
|
||||
imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2), noflags, imapclient.FetchModSeq(clientModseq)}},
|
||||
tc2o.untaggedFetch(2, 2, noflags, imapclient.FetchModSeq(clientModseq)),
|
||||
)
|
||||
|
||||
tc2o.closeNoWait()
|
||||
|
@ -491,12 +539,19 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
|
|||
tc.transactf("ok", "Rename inbox oldbox")
|
||||
// todo spec: server doesn't respond with untagged responses, find rfc reference that says this is ok.
|
||||
tc2.transactf("ok", "Noop")
|
||||
tc2.xuntagged(
|
||||
imapclient.UntaggedList{Separator: '/', Mailbox: "oldbox"},
|
||||
imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1),
|
||||
)
|
||||
if uidonly {
|
||||
tc2.xuntagged(
|
||||
imapclient.UntaggedList{Separator: '/', Mailbox: "oldbox"},
|
||||
imapclient.UntaggedVanished{UIDs: xparseNumSet("1,5:7")},
|
||||
)
|
||||
} else {
|
||||
tc2.xuntagged(
|
||||
imapclient.UntaggedList{Separator: '/', Mailbox: "oldbox"},
|
||||
imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1),
|
||||
)
|
||||
}
|
||||
tc3.transactf("ok", "Noop")
|
||||
if qresync {
|
||||
if qresync || uidonly {
|
||||
tc3.xuntagged(
|
||||
imapclient.UntaggedList{Separator: '/', Mailbox: "oldbox"},
|
||||
imapclient.UntaggedVanished{UIDs: xparseNumSet("1,5:7")},
|
||||
|
@ -512,7 +567,7 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
|
|||
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
|
||||
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)")
|
||||
|
||||
// Vanished not allowed without first enabling qresync. ../rfc/7162:1697
|
||||
xtc := startNoSwitchboard(t)
|
||||
xtc.client.Login("mjl@mox.example", password0)
|
||||
xtc := startNoSwitchboard(t, uidonly)
|
||||
xtc.login("mjl@mox.example", password0)
|
||||
xtc.transactf("ok", "Select inbox (Condstore)")
|
||||
xtc.transactf("bad", "Uid Fetch 1:* (Flags) (Changedsince 1 Vanished)")
|
||||
xtc.closeNoWait()
|
||||
|
@ -532,17 +587,17 @@ func testQresync(t *testing.T, tc *testconn, clientModseq int64) {
|
|||
noflags := imapclient.FetchFlags(nil)
|
||||
tc.xuntagged(
|
||||
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
|
||||
imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(5)}},
|
||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(8)}},
|
||||
imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}},
|
||||
tc.untaggedFetch(3, 5, noflags, imapclient.FetchModSeq(5)),
|
||||
tc.untaggedFetch(1, 1, noflags, imapclient.FetchModSeq(8)),
|
||||
tc.untaggedFetch(4, 6, noflags, imapclient.FetchModSeq(clientModseq)),
|
||||
)
|
||||
|
||||
// select/examine with qresync parameters, including the various optional fields.
|
||||
tc.transactf("ok", "Close")
|
||||
|
||||
// Must enable qresync explicitly before using. ../rfc/7162:1446
|
||||
xtc = startNoSwitchboard(t)
|
||||
xtc.client.Login("mjl@mox.example", password0)
|
||||
xtc = startNoSwitchboard(t, uidonly)
|
||||
xtc.login("mjl@mox.example", password0)
|
||||
xtc.transactf("bad", "Select inbox (Qresync 1 0)")
|
||||
// Prevent triggering the consistency checker, we still have modseq/createseq at 0.
|
||||
xtc.closeNoWait()
|
||||
|
@ -568,11 +623,15 @@ func testQresync(t *testing.T, tc *testconn, clientModseq int64) {
|
|||
imapclient.UntaggedList{Separator: '/', Mailbox: "Inbox"},
|
||||
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UIDNEXT", CodeArg: imapclient.CodeUint{Code: "UIDNEXT", Num: 7}, More: "x"}},
|
||||
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UIDVALIDITY", CodeArg: imapclient.CodeUint{Code: "UIDVALIDITY", Num: 1}, More: "x"}},
|
||||
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UNSEEN", CodeArg: imapclient.CodeUint{Code: "UNSEEN", Num: 1}, More: "x"}},
|
||||
imapclient.UntaggedRecent(0),
|
||||
imapclient.UntaggedExists(4),
|
||||
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(clientModseq), More: "x"}},
|
||||
}
|
||||
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 {
|
||||
return slices.Concat(baseUntagged, l)
|
||||
|
@ -583,9 +642,9 @@ func testQresync(t *testing.T, tc *testconn, clientModseq int64) {
|
|||
tc.xuntagged(
|
||||
makeUntagged(
|
||||
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
|
||||
imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(5)}},
|
||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(8)}},
|
||||
imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}},
|
||||
tc.untaggedFetch(3, 5, noflags, imapclient.FetchModSeq(5)),
|
||||
tc.untaggedFetch(1, 1, noflags, imapclient.FetchModSeq(8)),
|
||||
tc.untaggedFetch(4, 6, noflags, imapclient.FetchModSeq(clientModseq)),
|
||||
)...,
|
||||
)
|
||||
|
||||
|
@ -600,9 +659,9 @@ func testQresync(t *testing.T, tc *testconn, clientModseq int64) {
|
|||
tc.xuntagged(
|
||||
makeUntagged(
|
||||
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
|
||||
imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(5)}},
|
||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(8)}},
|
||||
imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}},
|
||||
tc.untaggedFetch(3, 5, noflags, imapclient.FetchModSeq(5)),
|
||||
tc.untaggedFetch(1, 1, noflags, imapclient.FetchModSeq(8)),
|
||||
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.xuntagged(
|
||||
makeUntagged(
|
||||
imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(5)}},
|
||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(8)}},
|
||||
imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}},
|
||||
tc.untaggedFetch(3, 5, noflags, imapclient.FetchModSeq(5)),
|
||||
tc.untaggedFetch(1, 1, noflags, imapclient.FetchModSeq(8)),
|
||||
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.xuntagged(
|
||||
makeUntagged(
|
||||
imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(5)}},
|
||||
tc.untaggedFetch(3, 5, noflags, imapclient.FetchModSeq(5)),
|
||||
)...,
|
||||
)
|
||||
|
||||
|
@ -644,19 +703,25 @@ func testQresync(t *testing.T, tc *testconn, clientModseq int64) {
|
|||
// know. But the seqs & uids must be of equal length. First try with a few combinations
|
||||
// that aren't valid. ../rfc/7162:1579
|
||||
tc.transactf("ok", "Close")
|
||||
tc.transactf("bad", "Select inbox (Qresync (1 1 1:6 (1 1,2)))") // Not same length.
|
||||
tc.transactf("bad", "Select inbox (Qresync (1 1 1:6 (1,2 1)))") // Not same length.
|
||||
tc.transactf("no", "Select inbox (Qresync (1 1 1:6 (1,2 1,1)))") // Not ascending.
|
||||
tc.transactf("bad", "Select inbox (Qresync (1 1 1:6 (1 1,2)))") // 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("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.
|
||||
tc.transactf("ok", "Select inbox (Qresync (1 1 1:6 (1,3,6 1,3,6)))")
|
||||
tc.xuntagged(
|
||||
makeUntagged(
|
||||
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
|
||||
imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5), noflags, imapclient.FetchModSeq(5)}},
|
||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(8)}},
|
||||
imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}},
|
||||
tc.untaggedFetch(3, 5, noflags, imapclient.FetchModSeq(5)),
|
||||
tc.untaggedFetch(1, 1, noflags, imapclient.FetchModSeq(8)),
|
||||
tc.untaggedFetch(4, 6, noflags, imapclient.FetchModSeq(clientModseq)),
|
||||
)...,
|
||||
)
|
||||
|
||||
|
@ -666,8 +731,8 @@ func testQresync(t *testing.T, tc *testconn, clientModseq int64) {
|
|||
tc.xuntagged(
|
||||
makeUntagged(
|
||||
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
|
||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), noflags, imapclient.FetchModSeq(8)}},
|
||||
imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}},
|
||||
tc.untaggedFetch(1, 1, noflags, imapclient.FetchModSeq(8)),
|
||||
tc.untaggedFetch(4, 6, noflags, imapclient.FetchModSeq(clientModseq)),
|
||||
)...,
|
||||
)
|
||||
|
||||
|
@ -676,7 +741,7 @@ func testQresync(t *testing.T, tc *testconn, clientModseq int64) {
|
|||
tc.xuntagged(
|
||||
makeUntagged(
|
||||
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
|
||||
imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}},
|
||||
tc.untaggedFetch(4, 6, noflags, imapclient.FetchModSeq(clientModseq)),
|
||||
)...,
|
||||
)
|
||||
|
||||
|
@ -689,7 +754,7 @@ func testQresync(t *testing.T, tc *testconn, clientModseq int64) {
|
|||
makeUntagged(
|
||||
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "ALERT", More: "Synchronization inconsistency in client detected. Client tried to sync with a UID that was removed at or after the MODSEQ it sent in the request. Sending all historic message removals for selected mailbox. Full synchronization recommended."}},
|
||||
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
|
||||
imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}},
|
||||
tc.untaggedFetch(4, 6, noflags, imapclient.FetchModSeq(clientModseq)),
|
||||
)...,
|
||||
)
|
||||
|
||||
|
@ -702,7 +767,128 @@ func testQresync(t *testing.T, tc *testconn, clientModseq int64) {
|
|||
makeUntagged(
|
||||
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "ALERT", More: "Synchronization inconsistency in client detected. Client tried to sync with a UID that was removed at or after the MODSEQ it sent in the request. Sending all historic message removals for selected mailbox. Full synchronization recommended."}},
|
||||
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
|
||||
imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6), noflags, imapclient.FetchModSeq(clientModseq)}},
|
||||
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) {
|
||||
testCopy(t, false)
|
||||
}
|
||||
|
||||
func TestCopyUIDOnly(t *testing.T) {
|
||||
testCopy(t, true)
|
||||
}
|
||||
|
||||
func testCopy(t *testing.T, uidonly bool) {
|
||||
defer mockUIDValidity()()
|
||||
tc := start(t)
|
||||
tc := start(t, uidonly)
|
||||
defer tc.close()
|
||||
|
||||
tc2 := startNoSwitchboard(t)
|
||||
tc2 := startNoSwitchboard(t, uidonly)
|
||||
defer tc2.closeNoWait()
|
||||
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
tc.client.Select("inbox")
|
||||
|
||||
tc2.client.Login("mjl@mox.example", password0)
|
||||
tc2.login("mjl@mox.example", password0)
|
||||
tc2.client.Select("Trash")
|
||||
|
||||
tc.transactf("bad", "copy") // Missing params.
|
||||
|
@ -27,48 +35,51 @@ func TestCopy(t *testing.T) {
|
|||
// Seqs 1,2 and UIDs 3,4.
|
||||
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.Append("inbox", makeAppend(exampleMsg))
|
||||
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||
|
||||
tc.transactf("no", "copy 1 nonexistent")
|
||||
tc.xcode("TRYCREATE")
|
||||
tc.transactf("no", "copy 1 expungebox")
|
||||
tc.xcode("TRYCREATE")
|
||||
if uidonly {
|
||||
tc.transactf("ok", "uid copy 3:* Trash")
|
||||
} else {
|
||||
tc.transactf("no", "copy 1 nonexistent")
|
||||
tc.xcode("TRYCREATE")
|
||||
tc.transactf("no", "copy 1 expungebox")
|
||||
tc.xcode("TRYCREATE")
|
||||
|
||||
tc.transactf("no", "copy 1 inbox") // Cannot copy to same mailbox.
|
||||
tc.transactf("no", "copy 1 inbox") // Cannot copy to same mailbox.
|
||||
|
||||
tc2.transactf("ok", "noop") // Drain.
|
||||
tc2.transactf("ok", "noop") // Drain.
|
||||
|
||||
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: ptr(4)}}, To: []imapclient.NumRange{{First: 1, Last: ptr(2)}}})
|
||||
tc.transactf("ok", "copy 1:* Trash")
|
||||
tc.xcodeArg(imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}, To: []imapclient.NumRange{{First: 1, Last: uint32ptr(2)}}})
|
||||
}
|
||||
tc2.transactf("ok", "noop")
|
||||
tc2.xuntagged(
|
||||
imapclient.UntaggedExists(2),
|
||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchFlags(nil)}},
|
||||
imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2), imapclient.FetchFlags(nil)}},
|
||||
tc2.untaggedFetch(1, 1, imapclient.FetchFlags(nil)),
|
||||
tc2.untaggedFetch(2, 2, imapclient.FetchFlags(nil)),
|
||||
)
|
||||
|
||||
tc.transactf("no", "uid copy 1,2 Trash") // No match.
|
||||
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.xuntagged(
|
||||
imapclient.UntaggedExists(4),
|
||||
imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(3), imapclient.FetchFlags(nil)}},
|
||||
imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(4), imapclient.FetchFlags(nil)}},
|
||||
tc2.untaggedFetch(3, 3, 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()
|
||||
tclimit.client.Login("limit@mox.example", password0)
|
||||
tclimit.login("limit@mox.example", password0)
|
||||
tclimit.client.Select("inbox")
|
||||
// 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.xuntagged(imapclient.UntaggedExists(1))
|
||||
// Second message would take account past limit.
|
||||
tclimit.transactf("no", "copy 1:* Trash")
|
||||
tclimit.transactf("no", "uid copy 1:* Trash")
|
||||
tclimit.xcode("OVERQUOTA")
|
||||
}
|
||||
|
|
|
@ -7,14 +7,22 @@ import (
|
|||
)
|
||||
|
||||
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()
|
||||
|
||||
tc2 := startNoSwitchboard(t)
|
||||
tc2 := startNoSwitchboard(t, uidonly)
|
||||
defer tc2.closeNoWait()
|
||||
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc2.client.Login("mjl@mox.example", password0)
|
||||
tc.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") // Idem.
|
||||
|
|
|
@ -7,18 +7,26 @@ import (
|
|||
)
|
||||
|
||||
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()
|
||||
|
||||
tc2 := startNoSwitchboard(t)
|
||||
tc2 := startNoSwitchboard(t, uidonly)
|
||||
defer tc2.closeNoWait()
|
||||
|
||||
tc3 := startNoSwitchboard(t)
|
||||
tc3 := startNoSwitchboard(t, uidonly)
|
||||
defer tc3.closeNoWait()
|
||||
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc2.client.Login("mjl@mox.example", password0)
|
||||
tc3.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
tc2.login("mjl@mox.example", password0)
|
||||
tc3.login("mjl@mox.example", password0)
|
||||
|
||||
tc.transactf("bad", "delete") // Missing mailbox.
|
||||
tc.transactf("no", "delete inbox") // Cannot delete inbox.
|
||||
|
|
|
@ -57,3 +57,9 @@ func xsyntaxErrorf(format string, args ...any) {
|
|||
err := errors.New(errmsg)
|
||||
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) {
|
||||
testExpunge(t, false)
|
||||
}
|
||||
|
||||
func TestExpungeUIDOnly(t *testing.T) {
|
||||
testExpunge(t, true)
|
||||
}
|
||||
|
||||
func testExpunge(t *testing.T, uidonly bool) {
|
||||
defer mockUIDValidity()()
|
||||
tc := start(t)
|
||||
tc := start(t, uidonly)
|
||||
defer tc.close()
|
||||
|
||||
tc2 := startNoSwitchboard(t)
|
||||
tc2 := startNoSwitchboard(t, uidonly)
|
||||
defer tc2.closeNoWait()
|
||||
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
tc.client.Select("inbox")
|
||||
|
||||
tc2.client.Login("mjl@mox.example", password0)
|
||||
tc2.login("mjl@mox.example", password0)
|
||||
tc2.client.Select("inbox")
|
||||
|
||||
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.xuntagged()
|
||||
|
||||
tc.client.StoreFlagsAdd("1,3", true, `\Deleted`)
|
||||
tc.transactf("ok", `uid store 1,3 +flags.silent \Deleted`)
|
||||
|
||||
tc2.transactf("ok", "noop") // Drain.
|
||||
|
||||
tc.transactf("ok", "expunge")
|
||||
tc.xuntagged(imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(2))
|
||||
if uidonly {
|
||||
tc.xuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("1,3")})
|
||||
} else {
|
||||
tc.xuntagged(imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(2))
|
||||
}
|
||||
|
||||
tc2.transactf("ok", "noop") // Drain.
|
||||
tc2.xuntagged(imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(2))
|
||||
if uidonly {
|
||||
tc2.xuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("1,3")})
|
||||
} else {
|
||||
tc2.xuntagged(imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(2))
|
||||
}
|
||||
|
||||
tc.transactf("ok", "expunge") // Nothing to remove anymore.
|
||||
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.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.xuntagged() // No match.
|
||||
|
@ -67,8 +83,16 @@ func TestExpunge(t *testing.T) {
|
|||
tc2.transactf("ok", "noop") // Drain.
|
||||
|
||||
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")})
|
||||
} else {
|
||||
tc.xuntagged(imapclient.UntaggedExpunge(2), imapclient.UntaggedExpunge(3))
|
||||
}
|
||||
|
||||
tc2.transactf("ok", "noop")
|
||||
tc.xuntagged(imapclient.UntaggedExpunge(2), imapclient.UntaggedExpunge(3))
|
||||
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)
|
||||
|
||||
// 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.
|
||||
// ../rfc/7162:871
|
||||
// database query to trim down the uids we need to look at. We need to go through
|
||||
// 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 {
|
||||
q := bstore.QueryTx[store.Message](cmd.rtx)
|
||||
q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
|
||||
|
@ -177,11 +178,16 @@ func (c *conn) cmdxFetch(isUID bool, tag, cmdstr string, p *parser) {
|
|||
q.FilterEqual("Expunged", false)
|
||||
}
|
||||
err := q.ForEach(func(m store.Message) error {
|
||||
if m.Expunged {
|
||||
vanishedUIDs = append(vanishedUIDs, m.UID)
|
||||
} else if isUID {
|
||||
if nums.containsUID(m.UID, c.uids, c.searchResult) {
|
||||
uids = append(uids, m.UID)
|
||||
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 {
|
||||
vanishedUIDs = append(vanishedUIDs, m.UID)
|
||||
} else {
|
||||
uids = append(uids, m.UID)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
seq := c.sequence(m.UID)
|
||||
|
@ -192,49 +198,52 @@ func (c *conn) cmdxFetch(isUID bool, tag, cmdstr string, p *parser) {
|
|||
return nil
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
delModSeq, err := c.account.HighestDeletedModSeq(cmd.rtx)
|
||||
xcheckf(err, "looking up highest deleted modseq")
|
||||
if changedSince >= delModSeq.Client() {
|
||||
return
|
||||
}
|
||||
|
||||
// First sort the uids we already found, for fast lookup.
|
||||
slices.Sort(vanishedUIDs)
|
||||
|
||||
// We'll be gathering any more vanished uids in more.
|
||||
more := map[store.UID]struct{}{}
|
||||
checkVanished := func(uid store.UID) {
|
||||
if uidSearch(c.uids, uid) <= 0 && uidSearch(vanishedUIDs, uid) <= 0 {
|
||||
more[uid] = struct{}{}
|
||||
// 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)
|
||||
xcheckf(err, "looking up highest deleted modseq")
|
||||
if !vanished || changedSince >= delModSeq.Client() {
|
||||
return
|
||||
}
|
||||
}
|
||||
// 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
|
||||
// iterate.
|
||||
if nums.searchResult {
|
||||
for _, uid := range c.searchResult {
|
||||
checkVanished(uid)
|
||||
}
|
||||
} else {
|
||||
iter := nums.interpretStar(c.uids).newIter()
|
||||
for {
|
||||
num, ok := iter.Next()
|
||||
if !ok {
|
||||
break
|
||||
|
||||
// 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(uids)
|
||||
|
||||
more := map[store.UID]struct{}{} // We'll add them at the end.
|
||||
checkVanished := func(uid store.UID) {
|
||||
if uid < c.uidnext && uidSearch(uids, uid) <= 0 && uidSearch(vanishedUIDs, uid) <= 0 {
|
||||
more[uid] = struct{}{}
|
||||
}
|
||||
checkVanished(store.UID(num))
|
||||
}
|
||||
|
||||
// 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
|
||||
// iterate.
|
||||
if nums.searchResult {
|
||||
for _, uid := range c.searchResult {
|
||||
checkVanished(uid)
|
||||
}
|
||||
} else {
|
||||
xlastUID := c.newCachedLastUID(cmd.rtx, c.mailboxID, func(xerr error) { xuserErrorf("%s", xerr) })
|
||||
iter := nums.xinterpretStar(xlastUID).newIter()
|
||||
for {
|
||||
num, ok := iter.Next()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
checkVanished(store.UID(num))
|
||||
}
|
||||
}
|
||||
vanishedUIDs = slices.AppendSeq(vanishedUIDs, maps.Keys(more))
|
||||
slices.Sort(vanishedUIDs)
|
||||
} else {
|
||||
uids = c.xnumSetEval(cmd.rtx, isUID, nums)
|
||||
}
|
||||
vanishedUIDs = slices.AppendSeq(vanishedUIDs, maps.Keys(more))
|
||||
|
||||
})
|
||||
// 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 {
|
||||
// Mention all vanished UIDs in compact numset form.
|
||||
// ../rfc/7162:1985
|
||||
slices.Sort(vanishedUIDs)
|
||||
// No hard limit on response sizes, but clients are recommended to not send more
|
||||
// than 8k. We send a more conservative max 4k.
|
||||
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)
|
||||
}
|
||||
|
||||
fmt.Fprintf(cmd.conn.xbw, "* %d FETCH ", cmd.conn.xsequence(cmd.uid))
|
||||
// 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))
|
||||
}
|
||||
data.xwriteTo(cmd.conn, cmd.conn.xbw)
|
||||
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.needFlags = false
|
||||
|
@ -474,8 +490,11 @@ func (cmd *fetchCmd) process(atts []fetchAtt) (rdata listspace, rerr error) {
|
|||
func (cmd *fetchCmd) xprocessAtt(a fetchAtt) []token {
|
||||
switch a.field {
|
||||
case "UID":
|
||||
// Always present.
|
||||
return nil
|
||||
// Present by default without uidonly. For uidonly, we only add it when explicitly
|
||||
// requested. ../rfc/9586:184
|
||||
if cmd.conn.uidonly {
|
||||
return []token{bare("UID"), number(cmd.uid)}
|
||||
}
|
||||
|
||||
case "ENVELOPE":
|
||||
_, part := cmd.xensureParsed()
|
||||
|
|
|
@ -12,10 +12,18 @@ import (
|
|||
)
|
||||
|
||||
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()
|
||||
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
tc.client.Enable("imap4rev2")
|
||||
received, err := time.Parse(time.RFC3339, "2022-11-16T10:01:00+01:00")
|
||||
tc.check(err, "parse time")
|
||||
|
@ -82,156 +90,159 @@ func TestFetch(t *testing.T) {
|
|||
|
||||
flagsSeen := imapclient.FetchFlags{`\Seen`}
|
||||
|
||||
tc.transactf("ok", "fetch 1 all")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, date1, rfcsize1, env1, noflags}})
|
||||
if !uidonly {
|
||||
tc.transactf("ok", "fetch 1 all")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, date1, rfcsize1, env1, noflags))
|
||||
|
||||
tc.transactf("ok", "fetch 1 fast")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, date1, rfcsize1, noflags}})
|
||||
tc.transactf("ok", "fetch 1 fast")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, date1, rfcsize1, noflags))
|
||||
|
||||
tc.transactf("ok", "fetch 1 full")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, date1, rfcsize1, env1, bodyxstructure1, noflags}})
|
||||
tc.transactf("ok", "fetch 1 full")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, date1, rfcsize1, env1, bodyxstructure1, noflags))
|
||||
|
||||
tc.transactf("ok", "fetch 1 flags")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, noflags}})
|
||||
tc.transactf("ok", "fetch 1 flags")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, noflags))
|
||||
|
||||
tc.transactf("ok", "fetch 1 bodystructure")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodystructure1}})
|
||||
tc.transactf("ok", "fetch 1 bodystructure")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, bodystructure1))
|
||||
|
||||
// Should be returned unmodified, because there is no content-transfer-encoding.
|
||||
tc.transactf("ok", "fetch 1 binary[]")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binary1, noflags}})
|
||||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
||||
// Should be returned unmodified, because there is no content-transfer-encoding.
|
||||
tc.transactf("ok", "fetch 1 binary[]")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, binary1, noflags))
|
||||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||
|
||||
tc.transactf("ok", "fetch 1 binary[1]")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binarypart1}}) // Seen flag not changed.
|
||||
tc.transactf("ok", "fetch 1 binary[1]")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, binarypart1)) // Seen flag not changed.
|
||||
|
||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||
tc.transactf("ok", "uid fetch 1 binary[]<1.1>")
|
||||
tc.xuntagged(
|
||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binarypartial1, noflags}},
|
||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}}, // For UID FETCH, we get the flags during the command.
|
||||
)
|
||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||
tc.transactf("ok", "uid fetch 1 binary[]<1.1>")
|
||||
tc.xuntagged(
|
||||
tc.untaggedFetch(1, 1, binarypartial1, noflags),
|
||||
tc.untaggedFetch(1, 1, flagsSeen), // For UID FETCH, we get the flags during the command.
|
||||
)
|
||||
|
||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||
tc.transactf("ok", "fetch 1 binary[1]<1.1>")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binarypartpartial1, noflags}})
|
||||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||
tc.transactf("ok", "fetch 1 binary[1]<1.1>")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, binarypartpartial1, noflags))
|
||||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||
|
||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||
tc.transactf("ok", "fetch 1 binary[]<10000.10001>")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binaryend1, noflags}})
|
||||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||
tc.transactf("ok", "fetch 1 binary[]<10000.10001>")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, binaryend1, noflags))
|
||||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||
|
||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||
tc.transactf("ok", "fetch 1 binary[1]<10000.10001>")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binarypartend1, noflags}})
|
||||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||
tc.transactf("ok", "fetch 1 binary[1]<10000.10001>")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, binarypartend1, noflags))
|
||||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||
|
||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||
tc.transactf("ok", "fetch 1 binary.size[]")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binarysize1}})
|
||||
tc.transactf("ok", "fetch 1 binary.size[]")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, binarysize1))
|
||||
|
||||
tc.transactf("ok", "fetch 1 binary.size[1]")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binarysizepart1}})
|
||||
tc.transactf("ok", "fetch 1 binary.size[1]")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, binarysizepart1))
|
||||
|
||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||
tc.transactf("ok", "fetch 1 body[]")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, body1, noflags}})
|
||||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
||||
tc.transactf("ok", "fetch 1 body[]<1.2>")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodyoff1}}) // Already seen.
|
||||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged() // Already seen.
|
||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||
tc.transactf("ok", "fetch 1 body[]")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, body1, noflags))
|
||||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||
tc.transactf("ok", "fetch 1 body[]<1.2>")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, bodyoff1)) // Already seen.
|
||||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged() // Already seen.
|
||||
|
||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||
tc.transactf("ok", "fetch 1 body[1]")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodypart1, noflags}})
|
||||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||
tc.transactf("ok", "fetch 1 body[1]")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, bodypart1, noflags))
|
||||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||
|
||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||
tc.transactf("ok", "fetch 1 body[1]<1.2>")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, body1off1, noflags}})
|
||||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||
tc.transactf("ok", "fetch 1 body[1]<1.2>")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, body1off1, noflags))
|
||||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||
|
||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||
tc.transactf("ok", "fetch 1 body[1]<100000.100000>")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodyend1, noflags}})
|
||||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||
tc.transactf("ok", "fetch 1 body[1]<100000.100000>")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, bodyend1, noflags))
|
||||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||
|
||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||
tc.transactf("ok", "fetch 1 body[header]")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodyheader1, noflags}})
|
||||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||
tc.transactf("ok", "fetch 1 body[header]")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, bodyheader1, noflags))
|
||||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||
|
||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||
tc.transactf("ok", "fetch 1 body[text]")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodytext1, noflags}})
|
||||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||
tc.transactf("ok", "fetch 1 body[text]")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, bodytext1, noflags))
|
||||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||
|
||||
// equivalent to body.peek[header], ../rfc/3501:3183
|
||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||
tc.transactf("ok", "fetch 1 rfc822.header")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, rfcheader1}})
|
||||
// equivalent to body.peek[header], ../rfc/3501:3183
|
||||
tc.transactf("ok", "fetch 1 rfc822.header")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, rfcheader1))
|
||||
|
||||
// equivalent to body[text], ../rfc/3501:3199
|
||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||
tc.transactf("ok", "fetch 1 rfc822.text")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, rfctext1, noflags}})
|
||||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
||||
// equivalent to body[text], ../rfc/3501:3199
|
||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||
tc.transactf("ok", "fetch 1 rfc822.text")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, rfctext1, noflags))
|
||||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||
|
||||
// equivalent to body[], ../rfc/3501:3179
|
||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||
tc.transactf("ok", "fetch 1 rfc822")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, rfc1, noflags}})
|
||||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
||||
// equivalent to body[], ../rfc/3501:3179
|
||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||
tc.transactf("ok", "fetch 1 rfc822")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, rfc1, noflags))
|
||||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||
|
||||
// With PEEK, we should not get the \Seen flag.
|
||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||
tc.transactf("ok", "fetch 1 body.peek[]")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, body1}})
|
||||
// With PEEK, we should not get the \Seen flag.
|
||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||
tc.transactf("ok", "fetch 1 body.peek[]")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, body1))
|
||||
|
||||
tc.transactf("ok", "fetch 1 binary.peek[]")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binary1}})
|
||||
tc.transactf("ok", "fetch 1 binary.peek[]")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, binary1))
|
||||
|
||||
// HEADER.FIELDS and .NOT
|
||||
tc.transactf("ok", "fetch 1 body.peek[header.fields (date)]")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, dateheader1}})
|
||||
tc.transactf("ok", "fetch 1 body.peek[header.fields.not (date)]")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, nodateheader1}})
|
||||
// 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.
|
||||
// ../rfc/9051:4481
|
||||
tc.transactf("no", "fetch 1 body.peek[1.header]")
|
||||
// HEADER.FIELDS and .NOT
|
||||
tc.transactf("ok", "fetch 1 body.peek[header.fields (date)]")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, dateheader1))
|
||||
tc.transactf("ok", "fetch 1 body.peek[header.fields.not (date)]")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, nodateheader1))
|
||||
// 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.
|
||||
// ../rfc/9051:4481
|
||||
tc.transactf("no", "fetch 1 body.peek[1.header]")
|
||||
|
||||
// MIME, part 1 for non-multipart messages is the message itself. ../rfc/9051:4481
|
||||
tc.transactf("ok", "fetch 1 body.peek[1.mime]")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, mime1}})
|
||||
// MIME, part 1 for non-multipart messages is the message itself. ../rfc/9051:4481
|
||||
tc.transactf("ok", "fetch 1 body.peek[1.mime]")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, mime1))
|
||||
|
||||
// Missing sequence number. ../rfc/9051:7018
|
||||
tc.transactf("bad", "fetch 2 body[]")
|
||||
// Missing sequence number. ../rfc/9051:7018
|
||||
tc.transactf("bad", "fetch 2 body[]")
|
||||
|
||||
tc.transactf("ok", "fetch 1:1 body[]")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, body1, noflags}})
|
||||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||
tc.transactf("ok", "fetch 1:1 body[]")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, body1, noflags))
|
||||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||
} else {
|
||||
tc.client.UIDStoreFlagsAdd("1", true, `\Seen`)
|
||||
tc.transactf("ok", "noop")
|
||||
}
|
||||
|
||||
// UID fetch
|
||||
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.xuntagged()
|
||||
|
||||
|
@ -254,9 +265,9 @@ func TestFetch(t *testing.T) {
|
|||
return nil
|
||||
})
|
||||
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 ")
|
||||
|
@ -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[mime]") // MIME must be prefixed with a number. ../rfc/9051:4497
|
||||
|
||||
tc.transactf("no", "fetch 1 body[2]") // No such part.
|
||||
if !uidonly {
|
||||
tc.transactf("no", "fetch 1 body[2]") // No such part.
|
||||
}
|
||||
|
||||
// Add more complex message.
|
||||
|
||||
uid2 := imapclient.FetchUID(2)
|
||||
bodystructure2 := imapclient.FetchBodystructure{
|
||||
RespAttr: "BODYSTRUCTURE",
|
||||
Body: imapclient.BodyTypeMpart{
|
||||
|
@ -325,41 +337,42 @@ func TestFetch(t *testing.T) {
|
|||
},
|
||||
}
|
||||
tc.client.Append("inbox", makeAppendTime(nestedMessage, received))
|
||||
tc.transactf("ok", "fetch 2 bodystructure")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
|
||||
tc.transactf("ok", "uid fetch 2 bodystructure")
|
||||
tc.xuntagged(tc.untaggedFetch(2, 2, bodystructure2))
|
||||
|
||||
// Multiple responses.
|
||||
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.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.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.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.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.transactf("ok", "fetch *:2 bodystructure")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
|
||||
|
||||
tc.transactf("ok", "fetch * bodystructure") // Highest msgseq.
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
|
||||
if !uidonly {
|
||||
tc.transactf("ok", "fetch 1:2 bodystructure")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, bodystructure1), tc.untaggedFetch(2, 2, bodystructure2))
|
||||
tc.transactf("ok", "fetch 1,2 bodystructure")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, bodystructure1), tc.untaggedFetch(2, 2, bodystructure2))
|
||||
tc.transactf("ok", "fetch 2:1 bodystructure")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, bodystructure1), tc.untaggedFetch(2, 2, bodystructure2))
|
||||
tc.transactf("ok", "fetch 1:* bodystructure")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, bodystructure1), tc.untaggedFetch(2, 2, bodystructure2))
|
||||
tc.transactf("ok", "fetch *:1 bodystructure")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, bodystructure1), tc.untaggedFetch(2, 2, bodystructure2))
|
||||
tc.transactf("ok", "fetch *:2 bodystructure")
|
||||
tc.xuntagged(tc.untaggedFetch(2, 2, bodystructure2))
|
||||
tc.transactf("ok", "fetch * bodystructure") // Highest msgseq.
|
||||
tc.xuntagged(tc.untaggedFetch(2, 2, bodystructure2))
|
||||
}
|
||||
|
||||
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.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.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.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.
|
||||
tc.transactf("ok", "fetch 2 body.peek[]")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[]", Body: nestedMessage}}})
|
||||
tc.transactf("ok", "uid fetch 2 body.peek[]")
|
||||
tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchBody{RespAttr: "BODY[]", Body: nestedMessage}))
|
||||
|
||||
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
|
||||
next part.]
|
||||
`)
|
||||
tc.transactf("ok", "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.transactf("ok", "uid fetch 2 body.peek[1]")
|
||||
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", "fetch 2 binary.peek[5]") // Only allowed on leaf parts, not messages.
|
||||
tc.transactf("no", "uid fetch 2 binary.peek[3]") // Only allowed on leaf parts, not multiparts.
|
||||
tc.transactf("no", "uid fetch 2 binary.peek[5]") // Only allowed on leaf parts, not messages.
|
||||
|
||||
part31 := "aGVsbG8NCndvcmxkDQo=\r\n"
|
||||
part31dec := "hello\r\nworld\r\n"
|
||||
tc.transactf("ok", "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.transactf("ok", "uid fetch 2 binary.size[3.1]")
|
||||
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.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[3.1]", Section: "3.1", Body: part31}}})
|
||||
tc.transactf("ok", "uid fetch 2 body.peek[3.1]")
|
||||
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.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBinary{RespAttr: "BINARY[3.1]", Parts: []uint32{3, 1}, Data: part31dec}}})
|
||||
tc.transactf("ok", "uid fetch 2 binary.peek[3.1]")
|
||||
tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchBinary{RespAttr: "BINARY[3.1]", Parts: []uint32{3, 1}, Data: part31dec}))
|
||||
|
||||
part3 := tocrlf(`--unique-boundary-2
|
||||
Content-Type: audio/basic
|
||||
|
@ -401,12 +414,12 @@ Content-Disposition: inline; filename=image.jpg
|
|||
--unique-boundary-2--
|
||||
|
||||
`)
|
||||
tc.transactf("ok", "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.transactf("ok", "uid fetch 2 body.peek[3]")
|
||||
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"
|
||||
tc.transactf("ok", "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.transactf("ok", "uid fetch 2 body.peek[2.mime]")
|
||||
tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchBody{RespAttr: "BODY[2.MIME]", Section: "2.MIME", Body: part2mime}))
|
||||
|
||||
part5 := tocrlf(`From: 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 ...
|
||||
`)
|
||||
tc.transactf("ok", "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.transactf("ok", "uid fetch 2 body.peek[5]")
|
||||
tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchBody{RespAttr: "BODY[5]", Section: "5", Body: part5}))
|
||||
|
||||
part5header := tocrlf(`From: 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
|
||||
|
||||
`)
|
||||
tc.transactf("ok", "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.transactf("ok", "uid fetch 2 body.peek[5.header]")
|
||||
tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchBody{RespAttr: "BODY[5.HEADER]", Section: "5.HEADER", Body: part5header}))
|
||||
|
||||
part5mime := tocrlf(`Content-Type: message/rfc822
|
||||
Content-MD5: MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=
|
||||
Content-Language: en,de
|
||||
Content-Location: http://localhost
|
||||
`)
|
||||
tc.transactf("ok", "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.transactf("ok", "uid fetch 2 body.peek[5.mime]")
|
||||
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"
|
||||
|
||||
tc.transactf("ok", "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.transactf("ok", "uid fetch 2 body.peek[5.text]")
|
||||
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"
|
||||
tc.transactf("ok", "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.transactf("ok", "uid fetch 2 body.peek[5.1]")
|
||||
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,
|
||||
// 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.
|
||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
||||
tc.client.UIDStoreFlagsClear("1", true, `\Seen`)
|
||||
tc.client.Unselect()
|
||||
tc.client.Examine("inbox")
|
||||
|
||||
// Preview
|
||||
preview := "Hello Joe, do you think we can meet at 3:30 tomorrow?"
|
||||
tc.transactf("ok", "fetch 1 preview")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, imapclient.FetchPreview{Preview: &preview}}})
|
||||
tc.transactf("ok", "uid fetch 1 preview")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchPreview{Preview: &preview}))
|
||||
|
||||
tc.transactf("ok", "fetch 1 preview (lazy)")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, imapclient.FetchPreview{Preview: &preview}}})
|
||||
tc.transactf("ok", "uid fetch 1 preview (lazy)")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchPreview{Preview: &preview}))
|
||||
|
||||
// On-demand preview and saving on first request.
|
||||
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")
|
||||
|
||||
tc.transactf("ok", "fetch 1 preview")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, imapclient.FetchPreview{Preview: &preview}}})
|
||||
tc.transactf("ok", "uid fetch 1 preview")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchPreview{Preview: &preview}))
|
||||
m := store.Message{ID: 1}
|
||||
err = tc.account.DB.Get(ctxbg, &m)
|
||||
tcheck(t, err, "get message")
|
||||
|
@ -489,29 +502,38 @@ Content-Location: http://localhost
|
|||
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
|
||||
// be able to access the messages.
|
||||
tc2 := startNoSwitchboard(t)
|
||||
tc2 := startNoSwitchboard(t, uidonly)
|
||||
defer tc2.closeNoWait()
|
||||
tc2.client.Login("mjl@mox.example", password0)
|
||||
tc2.login("mjl@mox.example", password0)
|
||||
tc2.client.Select("inbox")
|
||||
tc2.client.StoreFlagsSet("1", true, `\Deleted`)
|
||||
tc2.client.UIDStoreFlagsSet("1", true, `\Deleted`)
|
||||
tc2.client.Expunge()
|
||||
tc2.client.Logout()
|
||||
|
||||
tc.transactf("ok", "fetch 1 binary[]")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binary1}})
|
||||
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.xuntagged(tc.untaggedFetch(1, 1, binary1))
|
||||
|
||||
tc.transactf("ok", "fetch 1 body[]")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, body1}})
|
||||
tc.transactf("ok", "fetch 1 body[]")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, body1))
|
||||
|
||||
tc.transactf("ok", "fetch 1 rfc822.text")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, rfctext1}})
|
||||
tc.transactf("ok", "fetch 1 rfc822.text")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, rfctext1))
|
||||
|
||||
tc.transactf("ok", "fetch 1 rfc822")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, rfc1}})
|
||||
tc.transactf("ok", "fetch 1 rfc822")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, rfc1))
|
||||
}
|
||||
|
||||
tc.client.Logout()
|
||||
}
|
||||
|
|
|
@ -9,14 +9,14 @@ import (
|
|||
)
|
||||
|
||||
func TestIdle(t *testing.T) {
|
||||
tc1 := start(t)
|
||||
tc1 := start(t, false)
|
||||
defer tc1.close()
|
||||
|
||||
tc2 := startNoSwitchboard(t)
|
||||
tc2 := startNoSwitchboard(t, false)
|
||||
defer tc2.closeNoWait()
|
||||
|
||||
tc1.client.Login("mjl@mox.example", password0)
|
||||
tc2.client.Login("mjl@mox.example", password0)
|
||||
tc1.login("mjl@mox.example", password0)
|
||||
tc2.login("mjl@mox.example", password0)
|
||||
|
||||
tc1.transactf("ok", "select inbox")
|
||||
tc2.transactf("ok", "select inbox")
|
||||
|
|
|
@ -8,10 +8,18 @@ import (
|
|||
)
|
||||
|
||||
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()
|
||||
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
|
||||
ulist := func(name string, flags ...string) imapclient.UntaggedList {
|
||||
if len(flags) == 0 {
|
||||
|
@ -59,12 +67,20 @@ func TestListBasic(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()()
|
||||
|
||||
tc := start(t)
|
||||
tc := start(t, uidonly)
|
||||
defer tc.close()
|
||||
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
|
||||
ulist := func(name string, flags ...string) imapclient.UntaggedList {
|
||||
if len(flags) == 0 {
|
||||
|
|
|
@ -7,10 +7,18 @@ import (
|
|||
)
|
||||
|
||||
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()
|
||||
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
|
||||
tc.transactf("bad", "lsub") // Missing params.
|
||||
tc.transactf("bad", `lsub ""`) // Missing param.
|
||||
|
|
|
@ -9,10 +9,18 @@ import (
|
|||
)
|
||||
|
||||
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()
|
||||
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
|
||||
tc.transactf("ok", `getmetadata "" /private/comment`)
|
||||
tc.xuntagged()
|
||||
|
@ -184,9 +192,9 @@ func TestMetadata(t *testing.T) {
|
|||
})
|
||||
|
||||
// Broadcast should not happen when metadata capability is not enabled.
|
||||
tc2 := startNoSwitchboard(t)
|
||||
tc2 := startNoSwitchboard(t, uidonly)
|
||||
defer tc2.closeNoWait()
|
||||
tc2.client.Login("mjl@mox.example", password0)
|
||||
tc2.login("mjl@mox.example", password0)
|
||||
tc2.client.Select("inbox")
|
||||
|
||||
tc2.cmdf("", "idle")
|
||||
|
@ -255,10 +263,10 @@ func TestMetadata(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMetadataLimit(t *testing.T) {
|
||||
tc := start(t)
|
||||
tc := start(t, false)
|
||||
defer tc.close()
|
||||
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
|
||||
maxKeys, maxSize := metadataMaxKeys, metadataMaxSize
|
||||
defer func() {
|
||||
|
|
|
@ -7,23 +7,31 @@ import (
|
|||
)
|
||||
|
||||
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()()
|
||||
tc := start(t)
|
||||
tc := start(t, uidonly)
|
||||
defer tc.close()
|
||||
|
||||
tc2 := startNoSwitchboard(t)
|
||||
tc2 := startNoSwitchboard(t, uidonly)
|
||||
defer tc2.closeNoWait()
|
||||
|
||||
tc3 := startNoSwitchboard(t)
|
||||
tc3 := startNoSwitchboard(t, uidonly)
|
||||
defer tc3.closeNoWait()
|
||||
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
tc.client.Select("inbox")
|
||||
|
||||
tc2.client.Login("mjl@mox.example", password0)
|
||||
tc2.login("mjl@mox.example", password0)
|
||||
tc2.client.Select("Trash")
|
||||
|
||||
tc3.client.Login("mjl@mox.example", password0)
|
||||
tc3.login("mjl@mox.example", password0)
|
||||
tc3.client.Select("inbox")
|
||||
|
||||
tc.transactf("bad", "move") // Missing params.
|
||||
|
@ -33,43 +41,46 @@ func TestMove(t *testing.T) {
|
|||
// Seqs 1,2 and UIDs 3,4.
|
||||
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.Append("inbox", makeAppend(exampleMsg))
|
||||
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||
|
||||
tc.client.Unselect()
|
||||
tc.client.Examine("inbox")
|
||||
tc.transactf("no", "move 1 Trash") // Opened readonly.
|
||||
tc.client.Unselect()
|
||||
tc.client.Select("inbox")
|
||||
if uidonly {
|
||||
tc.transactf("ok", "uid move 1:* Trash")
|
||||
} else {
|
||||
tc.client.Unselect()
|
||||
tc.client.Examine("inbox")
|
||||
tc.transactf("no", "move 1 Trash") // Opened readonly.
|
||||
tc.client.Unselect()
|
||||
tc.client.Select("inbox")
|
||||
|
||||
tc.transactf("no", "move 1 nonexistent")
|
||||
tc.xcode("TRYCREATE")
|
||||
tc.transactf("no", "move 1 nonexistent")
|
||||
tc.xcode("TRYCREATE")
|
||||
|
||||
tc.transactf("no", "move 1 expungebox")
|
||||
tc.xcode("TRYCREATE")
|
||||
tc.transactf("no", "move 1 expungebox")
|
||||
tc.xcode("TRYCREATE")
|
||||
|
||||
tc.transactf("no", "move 1 inbox") // Cannot move to same mailbox.
|
||||
tc.transactf("no", "move 1 inbox") // Cannot move to same mailbox.
|
||||
|
||||
tc2.transactf("ok", "noop") // Drain.
|
||||
tc3.transactf("ok", "noop") // Drain.
|
||||
tc2.transactf("ok", "noop") // Drain.
|
||||
tc3.transactf("ok", "noop") // Drain.
|
||||
|
||||
tc.transactf("ok", "move 1:* Trash")
|
||||
ptr := func(v uint32) *uint32 { return &v }
|
||||
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.UntaggedExpunge(1),
|
||||
imapclient.UntaggedExpunge(1),
|
||||
)
|
||||
tc2.transactf("ok", "noop")
|
||||
tc2.xuntagged(
|
||||
imapclient.UntaggedExists(2),
|
||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchFlags(nil)}},
|
||||
imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2), imapclient.FetchFlags(nil)}},
|
||||
)
|
||||
tc3.transactf("ok", "noop")
|
||||
tc3.xuntagged(imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1))
|
||||
tc.transactf("ok", "move 1:* Trash")
|
||||
tc.xuntagged(
|
||||
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),
|
||||
)
|
||||
tc2.transactf("ok", "noop")
|
||||
tc2.xuntagged(
|
||||
imapclient.UntaggedExists(2),
|
||||
tc.untaggedFetch(1, 1, imapclient.FetchFlags(nil)),
|
||||
tc.untaggedFetch(2, 2, imapclient.FetchFlags(nil)),
|
||||
)
|
||||
tc3.transactf("ok", "noop")
|
||||
tc3.xuntagged(imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1))
|
||||
}
|
||||
|
||||
// UIDs 5,6
|
||||
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("ok", "uid move 6:5 Trash")
|
||||
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.UntaggedExpunge(1),
|
||||
imapclient.UntaggedExpunge(1),
|
||||
)
|
||||
if uidonly {
|
||||
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.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),
|
||||
)
|
||||
}
|
||||
tc2.transactf("ok", "noop")
|
||||
tc2.xuntagged(
|
||||
imapclient.UntaggedExists(4),
|
||||
imapclient.UntaggedFetch{Seq: 3, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(3), imapclient.FetchFlags(nil)}},
|
||||
imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(4), imapclient.FetchFlags(nil)}},
|
||||
tc2.untaggedFetch(3, 3, imapclient.FetchFlags(nil)),
|
||||
tc2.untaggedFetch(4, 4, imapclient.FetchFlags(nil)),
|
||||
)
|
||||
tc3.transactf("ok", "noop")
|
||||
tc3.xuntagged(imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1))
|
||||
if uidonly {
|
||||
tc3.xuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("5:6")})
|
||||
} else {
|
||||
tc3.xuntagged(imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,15 +9,19 @@ import (
|
|||
"github.com/mjl-/mox/store"
|
||||
)
|
||||
|
||||
func ptr[T any](v T) *T {
|
||||
return &v
|
||||
func TestNotify(t *testing.T) {
|
||||
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()()
|
||||
tc := start(t)
|
||||
tc := start(t, uidonly)
|
||||
defer tc.close()
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
tc.client.Select("inbox")
|
||||
|
||||
// Check for some invalid syntax.
|
||||
|
@ -42,9 +46,9 @@ func TestNotify(t *testing.T) {
|
|||
tc.transactf("no", "Notify Set Status (Personal (unknownEvent))")
|
||||
tc.xcode("BADEVENT")
|
||||
|
||||
tc2 := startNoSwitchboard(t)
|
||||
tc2 := startNoSwitchboard(t, uidonly)
|
||||
defer tc2.closeNoWait()
|
||||
tc2.client.Login("mjl@mox.example", password0)
|
||||
tc2.login("mjl@mox.example", password0)
|
||||
tc2.client.Select("inbox")
|
||||
|
||||
var modseq uint32 = 4
|
||||
|
@ -60,15 +64,9 @@ func TestNotify(t *testing.T) {
|
|||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged(
|
||||
imapclient.UntaggedExists(1),
|
||||
imapclient.UntaggedFetch{
|
||||
Seq: 1,
|
||||
Attrs: []imapclient.FetchAttr{
|
||||
imapclient.FetchUID(1),
|
||||
imapclient.FetchFlags(nil),
|
||||
},
|
||||
},
|
||||
tc.untaggedFetch(1, 1, imapclient.FetchFlags(nil)),
|
||||
)
|
||||
tc2.client.StoreFlagsAdd("1:*", true, `\Deleted`)
|
||||
tc2.client.UIDStoreFlagsAdd("1:*", true, `\Deleted`)
|
||||
modseq++
|
||||
tc2.client.Expunge()
|
||||
modseq++
|
||||
|
@ -97,67 +95,56 @@ func TestNotify(t *testing.T) {
|
|||
modseq++
|
||||
tc.readuntagged(
|
||||
imapclient.UntaggedExists(1),
|
||||
imapclient.UntaggedFetch{
|
||||
Seq: 1,
|
||||
Attrs: []imapclient.FetchAttr{
|
||||
imapclient.FetchUID(2),
|
||||
imapclient.FetchBodystructure{
|
||||
RespAttr: "BODYSTRUCTURE",
|
||||
Body: imapclient.BodyTypeMpart{
|
||||
Bodies: []any{
|
||||
imapclient.BodyTypeText{
|
||||
MediaType: "TEXT",
|
||||
MediaSubtype: "PLAIN",
|
||||
BodyFields: imapclient.BodyFields{
|
||||
Params: [][2]string{[...]string{"CHARSET", "utf-8"}},
|
||||
Octets: 21,
|
||||
},
|
||||
Lines: 1,
|
||||
Ext: &imapclient.BodyExtension1Part{},
|
||||
},
|
||||
imapclient.BodyTypeText{
|
||||
MediaType: "TEXT",
|
||||
MediaSubtype: "HTML",
|
||||
BodyFields: imapclient.BodyFields{
|
||||
Params: [][2]string{[...]string{"CHARSET", "utf-8"}},
|
||||
Octets: 15,
|
||||
},
|
||||
Lines: 1,
|
||||
Ext: &imapclient.BodyExtension1Part{},
|
||||
tc.untaggedFetchUID(1, 2,
|
||||
imapclient.FetchBodystructure{
|
||||
RespAttr: "BODYSTRUCTURE",
|
||||
Body: imapclient.BodyTypeMpart{
|
||||
Bodies: []any{
|
||||
imapclient.BodyTypeText{
|
||||
MediaType: "TEXT",
|
||||
MediaSubtype: "PLAIN",
|
||||
BodyFields: imapclient.BodyFields{
|
||||
Params: [][2]string{[...]string{"CHARSET", "utf-8"}},
|
||||
Octets: 21,
|
||||
},
|
||||
Lines: 1,
|
||||
Ext: &imapclient.BodyExtension1Part{},
|
||||
},
|
||||
MediaSubtype: "ALTERNATIVE",
|
||||
Ext: &imapclient.BodyExtensionMpart{
|
||||
Params: [][2]string{{"BOUNDARY", "x"}},
|
||||
imapclient.BodyTypeText{
|
||||
MediaType: "TEXT",
|
||||
MediaSubtype: "HTML",
|
||||
BodyFields: imapclient.BodyFields{
|
||||
Params: [][2]string{[...]string{"CHARSET", "utf-8"}},
|
||||
Octets: 15,
|
||||
},
|
||||
Lines: 1,
|
||||
Ext: &imapclient.BodyExtension1Part{},
|
||||
},
|
||||
},
|
||||
MediaSubtype: "ALTERNATIVE",
|
||||
Ext: &imapclient.BodyExtensionMpart{
|
||||
Params: [][2]string{{"BOUNDARY", "x"}},
|
||||
},
|
||||
},
|
||||
imapclient.FetchPreview{Preview: ptr("this is plain text.")},
|
||||
imapclient.FetchModSeq(modseq),
|
||||
},
|
||||
},
|
||||
imapclient.FetchPreview{Preview: ptr("this is plain text.")},
|
||||
imapclient.FetchModSeq(modseq),
|
||||
),
|
||||
)
|
||||
|
||||
// Change flags.
|
||||
tc2.client.StoreFlagsAdd("1:*", true, `\Deleted`)
|
||||
tc2.client.UIDStoreFlagsAdd("1:*", true, `\Deleted`)
|
||||
modseq++
|
||||
tc.readuntagged(
|
||||
imapclient.UntaggedFetch{
|
||||
Seq: 1,
|
||||
Attrs: []imapclient.FetchAttr{
|
||||
imapclient.FetchUID(2),
|
||||
imapclient.FetchFlags{`\Deleted`},
|
||||
imapclient.FetchModSeq(modseq),
|
||||
},
|
||||
},
|
||||
)
|
||||
tc.readuntagged(tc.untaggedFetch(1, 2, imapclient.FetchFlags{`\Deleted`}, imapclient.FetchModSeq(modseq)))
|
||||
|
||||
// Remove message.
|
||||
tc2.client.Expunge()
|
||||
modseq++
|
||||
tc.readuntagged(
|
||||
imapclient.UntaggedExpunge(1),
|
||||
)
|
||||
if uidonly {
|
||||
tc.readuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("2")})
|
||||
} else {
|
||||
tc.readuntagged(imapclient.UntaggedExpunge(1))
|
||||
}
|
||||
|
||||
// MailboxMetadataChange for mailbox annotation.
|
||||
tc2.transactf("ok", `setmetadata Archive (/private/comment "test")`)
|
||||
|
@ -228,13 +215,7 @@ func TestNotify(t *testing.T) {
|
|||
modseq++
|
||||
tc.readuntagged(
|
||||
imapclient.UntaggedExists(1),
|
||||
imapclient.UntaggedFetch{
|
||||
Seq: 1,
|
||||
Attrs: []imapclient.FetchAttr{
|
||||
imapclient.FetchUID(3),
|
||||
imapclient.FetchModSeq(modseq),
|
||||
},
|
||||
},
|
||||
tc.untaggedFetchUID(1, 3, imapclient.FetchModSeq(modseq)),
|
||||
)
|
||||
|
||||
// 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)")
|
||||
tc2.client.Append("inbox", makeAppend(searchMsg)) // MessageNew
|
||||
modseq++
|
||||
tc2.client.StoreFlagsAdd("1:*", true, `\Deleted`) // FlagChange
|
||||
tc2.client.UIDStoreFlagsAdd("1:*", true, `\Deleted`) // FlagChange
|
||||
modseq++
|
||||
tc2.client.Expunge() // MessageExpunge
|
||||
modseq++
|
||||
|
@ -275,27 +256,27 @@ func TestNotify(t *testing.T) {
|
|||
tc.client.Unsubscribe("other/a/b")
|
||||
|
||||
// Inboxes
|
||||
tc3 := startNoSwitchboard(t)
|
||||
tc3 := startNoSwitchboard(t, uidonly)
|
||||
defer tc3.closeNoWait()
|
||||
tc3.client.Login("mjl@mox.example", password0)
|
||||
tc3.login("mjl@mox.example", password0)
|
||||
tc3.transactf("ok", "Notify Set (Inboxes (messageNew messageExpunge))")
|
||||
|
||||
// Subscribed
|
||||
tc4 := startNoSwitchboard(t)
|
||||
tc4 := startNoSwitchboard(t, uidonly)
|
||||
defer tc4.closeNoWait()
|
||||
tc4.client.Login("mjl@mox.example", password0)
|
||||
tc4.login("mjl@mox.example", password0)
|
||||
tc4.transactf("ok", "Notify Set (Subscribed (messageNew messageExpunge))")
|
||||
|
||||
// Subtree
|
||||
tc5 := startNoSwitchboard(t)
|
||||
tc5 := startNoSwitchboard(t, uidonly)
|
||||
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))")
|
||||
|
||||
// Subtree-One
|
||||
tc6 := startNoSwitchboard(t)
|
||||
tc6 := startNoSwitchboard(t, uidonly)
|
||||
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))")
|
||||
|
||||
// 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")
|
||||
tc2.client.Append("inbox", makeAppend(searchMsg))
|
||||
modseq++
|
||||
tc2.client.StoreFlagsSet("1", true, `\Seen`)
|
||||
tc2.client.UIDStoreFlagsSet("*", true, `\Seen`)
|
||||
modseq++
|
||||
tc2.client.Append("statusbox", imapclient.Append{Flags: []string{"newflag"}, Size: int64(len(searchMsg)), Data: strings.NewReader(searchMsg)})
|
||||
modseq++
|
||||
|
@ -350,40 +331,32 @@ func TestNotify(t *testing.T) {
|
|||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged(
|
||||
imapclient.UntaggedExists(2),
|
||||
imapclient.UntaggedFetch{
|
||||
Seq: 2,
|
||||
Attrs: []imapclient.FetchAttr{
|
||||
imapclient.FetchUID(2),
|
||||
imapclient.FetchFlags{`newflag`},
|
||||
imapclient.FetchModSeq(modseq),
|
||||
},
|
||||
},
|
||||
tc.untaggedFetch(2, 2, imapclient.FetchFlags{"newflag"}, imapclient.FetchModSeq(modseq)),
|
||||
imapclient.UntaggedFlags{`\Seen`, `\Answered`, `\Flagged`, `\Deleted`, `\Draft`, `$Forwarded`, `$Junk`, `$NotJunk`, `$Phishing`, `$MDNSent`, `newflag`},
|
||||
)
|
||||
|
||||
tc2.client.StoreFlagsSet("2", true, `\Deleted`)
|
||||
tc2.client.UIDStoreFlagsSet("2", true, `\Deleted`)
|
||||
modseq++
|
||||
tc2.client.Expunge()
|
||||
modseq++
|
||||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged(
|
||||
imapclient.UntaggedFetch{
|
||||
Seq: 2,
|
||||
Attrs: []imapclient.FetchAttr{
|
||||
imapclient.FetchUID(2),
|
||||
imapclient.FetchFlags{`\Deleted`},
|
||||
imapclient.FetchModSeq(modseq - 1),
|
||||
},
|
||||
},
|
||||
imapclient.UntaggedExpunge(2),
|
||||
)
|
||||
if uidonly {
|
||||
tc.xuntagged(
|
||||
tc.untaggedFetch(2, 2, imapclient.FetchFlags{`\Deleted`}, imapclient.FetchModSeq(modseq-1)),
|
||||
imapclient.UntaggedVanished{UIDs: xparseNumSet("2")},
|
||||
)
|
||||
} else {
|
||||
tc.xuntagged(
|
||||
tc.untaggedFetch(2, 2, imapclient.FetchFlags{`\Deleted`}, imapclient.FetchModSeq(modseq-1)),
|
||||
imapclient.UntaggedExpunge(2),
|
||||
)
|
||||
}
|
||||
|
||||
// With Selected-Delayed, we should get events for selected mailboxes immediately when using IDLE.
|
||||
tc2.client.StoreFlagsSet("*", true, `\Answered`)
|
||||
tc2.client.UIDStoreFlagsSet("*", true, `\Answered`)
|
||||
modseq++
|
||||
|
||||
tc2.client.Select("inbox")
|
||||
tc2.client.StoreFlagsClear("1", true, `\Seen`)
|
||||
tc2.client.UIDStoreFlagsClear("*", true, `\Seen`)
|
||||
modseq++
|
||||
tc2.client.Select("statusbox")
|
||||
|
||||
|
@ -394,14 +367,7 @@ func TestNotify(t *testing.T) {
|
|||
tc.conn.SetReadDeadline(time.Now().Add(3 * time.Second))
|
||||
tc.cmdf("", "idle")
|
||||
tc.readprefixline("+ ")
|
||||
tc.readuntagged(imapclient.UntaggedFetch{
|
||||
Seq: 1,
|
||||
Attrs: []imapclient.FetchAttr{
|
||||
imapclient.FetchUID(1),
|
||||
imapclient.FetchFlags{`\Answered`},
|
||||
imapclient.FetchModSeq(modseq - 1),
|
||||
},
|
||||
})
|
||||
tc.readuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{`\Answered`}, imapclient.FetchModSeq(modseq-1)))
|
||||
tc.writelinef("done")
|
||||
tc.response("ok")
|
||||
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.
|
||||
tc.client.Unselect()
|
||||
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++
|
||||
tc2.client.Create("eventbox", nil)
|
||||
//modseq++
|
||||
|
@ -425,23 +391,19 @@ func TestNotify(t *testing.T) {
|
|||
offset := strings.Index(searchMsg, "\r\n\r\n")
|
||||
tc.readuntagged(
|
||||
imapclient.UntaggedExists(2),
|
||||
imapclient.UntaggedFetch{
|
||||
Seq: 2,
|
||||
Attrs: []imapclient.FetchAttr{
|
||||
imapclient.FetchUID(3),
|
||||
imapclient.FetchBody{
|
||||
RespAttr: "BODY[HEADER]",
|
||||
Section: "HEADER",
|
||||
Body: searchMsg[:offset+4],
|
||||
},
|
||||
imapclient.FetchBody{
|
||||
RespAttr: "BODY[TEXT]",
|
||||
Section: "TEXT",
|
||||
Body: searchMsg[offset+4:],
|
||||
},
|
||||
imapclient.FetchFlags(nil),
|
||||
tc.untaggedFetch(2, 3,
|
||||
imapclient.FetchBody{
|
||||
RespAttr: "BODY[HEADER]",
|
||||
Section: "HEADER",
|
||||
Body: searchMsg[:offset+4],
|
||||
},
|
||||
},
|
||||
imapclient.FetchBody{
|
||||
RespAttr: "BODY[TEXT]",
|
||||
Section: "TEXT",
|
||||
Body: searchMsg[offset+4:],
|
||||
},
|
||||
imapclient.FetchFlags(nil),
|
||||
),
|
||||
)
|
||||
|
||||
// If we encounter an error during fetch, an untagged NO is returned.
|
||||
|
@ -457,18 +419,21 @@ func TestNotify(t *testing.T) {
|
|||
More: "generating notify fetch response: requested part does not exist",
|
||||
},
|
||||
},
|
||||
imapclient.UntaggedFetch{
|
||||
Seq: 3,
|
||||
Attrs: []imapclient.FetchAttr{
|
||||
imapclient.FetchUID(4),
|
||||
},
|
||||
},
|
||||
tc.untaggedFetchUID(3, 4),
|
||||
)
|
||||
|
||||
// When adding new tests, uncomment modseq++ lines above.
|
||||
}
|
||||
|
||||
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
|
||||
store.CommPendingChangesMax = 3
|
||||
defer func() {
|
||||
|
@ -476,15 +441,15 @@ func TestNotifyOverflow(t *testing.T) {
|
|||
}()
|
||||
|
||||
defer mockUIDValidity()()
|
||||
tc := start(t)
|
||||
tc := start(t, uidonly)
|
||||
defer tc.close()
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
tc.client.Select("inbox")
|
||||
tc.transactf("ok", "noop")
|
||||
|
||||
tc2 := startNoSwitchboard(t)
|
||||
tc2 := startNoSwitchboard(t, uidonly)
|
||||
defer tc2.closeNoWait()
|
||||
tc2.client.Login("mjl@mox.example", password0)
|
||||
tc2.login("mjl@mox.example", password0)
|
||||
tc2.client.Select("inbox")
|
||||
|
||||
// Generates 4 changes, crossing max 3.
|
||||
|
@ -507,26 +472,22 @@ func TestNotifyOverflow(t *testing.T) {
|
|||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged()
|
||||
|
||||
// Enable notify again. We won't get a notification because the message isn't yet
|
||||
// known in the session.
|
||||
// Enable notify again. Without uidonly, we won't get a notification because the
|
||||
// message isn't known in the session.
|
||||
tc.transactf("ok", "notify set (selected (messageNew messageExpunge flagChange))")
|
||||
tc2.client.StoreFlagsAdd("1", true, `\Seen`)
|
||||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged()
|
||||
tc2.client.UIDStoreFlagsAdd("1", true, `\Seen`)
|
||||
if uidonly {
|
||||
tc.readuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{`\Seen`}))
|
||||
} else {
|
||||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged()
|
||||
}
|
||||
|
||||
// Reselect to get the message visible in the session.
|
||||
tc.client.Select("inbox")
|
||||
tc2.client.StoreFlagsClear("1", true, `\Seen`)
|
||||
tc2.client.UIDStoreFlagsClear("1", true, `\Seen`)
|
||||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged(
|
||||
imapclient.UntaggedFetch{
|
||||
Seq: 1,
|
||||
Attrs: []imapclient.FetchAttr{
|
||||
imapclient.FetchUID(1),
|
||||
imapclient.FetchFlags(nil),
|
||||
},
|
||||
},
|
||||
)
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags(nil)))
|
||||
|
||||
// Trigger overflow for changes for "selected-delayed".
|
||||
store.CommPendingChangesMax = 10
|
||||
|
@ -536,8 +497,8 @@ func TestNotifyOverflow(t *testing.T) {
|
|||
selectedDelayedChangesMax = delayedMax
|
||||
}()
|
||||
tc.transactf("ok", "notify set (selected-delayed (messageNew messageExpunge flagChange))")
|
||||
tc2.client.StoreFlagsAdd("1", true, `\Seen`)
|
||||
tc2.client.StoreFlagsClear("1", true, `\Seen`)
|
||||
tc2.client.UIDStoreFlagsAdd("1", true, `\Seen`)
|
||||
tc2.client.UIDStoreFlagsClear("1", true, `\Seen`)
|
||||
tc.transactf("ok", "noop")
|
||||
tc.xuntagged(
|
||||
imapclient.UntaggedResult{
|
||||
|
@ -550,21 +511,13 @@ func TestNotifyOverflow(t *testing.T) {
|
|||
)
|
||||
|
||||
// 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.xuntagged()
|
||||
|
||||
tc.client.Select("inbox")
|
||||
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.xuntagged(
|
||||
imapclient.UntaggedFetch{
|
||||
Seq: 1,
|
||||
Attrs: []imapclient.FetchAttr{
|
||||
imapclient.FetchUID(1),
|
||||
imapclient.FetchFlags(nil),
|
||||
},
|
||||
},
|
||||
)
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags(nil)))
|
||||
}
|
||||
|
|
|
@ -946,42 +946,6 @@ func (p *parser) xsearchKey() *searchKey {
|
|||
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
|
||||
func (p *parser) xdateDay() int {
|
||||
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)))
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if msgCount == 0 {
|
||||
return false
|
||||
|
@ -64,45 +65,14 @@ func (ss numSet) containsSeqCount(seq msgseq, msgCount uint32) bool {
|
|||
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.
|
||||
// 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.
|
||||
// highestUID is needed for interpreting UID sets like "<num>:*" where num is
|
||||
// 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 {
|
||||
return uidSearch(searchResult, uid) > 0, nil
|
||||
return uidSearch(searchResult, uid) > 0
|
||||
}
|
||||
|
||||
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
|
||||
if r.first.star {
|
||||
if r.last != nil && uid >= store.UID(r.last.number) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var err error
|
||||
a, err = highestUID()
|
||||
if err != nil {
|
||||
return false, err
|
||||
return true
|
||||
}
|
||||
a = xhighestUID()
|
||||
}
|
||||
b := a
|
||||
if r.last != nil {
|
||||
b = store.UID(r.last.number)
|
||||
if r.last.star {
|
||||
if uid >= a {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var err error
|
||||
b, err = highestUID()
|
||||
if err != nil {
|
||||
return false, err
|
||||
return true
|
||||
}
|
||||
b = xhighestUID()
|
||||
}
|
||||
}
|
||||
if a > b {
|
||||
a, b = b, a
|
||||
}
|
||||
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.
|
||||
|
@ -216,38 +209,6 @@ func (ss numSet) String() string {
|
|||
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.
|
||||
func (s *numSet) isBasicIncreasing() bool {
|
||||
if s.searchResult {
|
||||
|
@ -385,6 +346,40 @@ type searchKey struct {
|
|||
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) {
|
||||
for len(l) > 0 {
|
||||
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{}))
|
||||
|
||||
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.
|
||||
check(ss1.containsSeq(1, []store.UID{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(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(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:*
|
||||
ss3 := numSet{false, []numRange{{*star, num(2)}}}
|
||||
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(3, []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) {
|
||||
|
@ -77,38 +53,34 @@ func TestNumSetInterpret(t *testing.T) {
|
|||
return p.xnumSet0(true, false)
|
||||
}
|
||||
|
||||
checkEqual := func(uids []store.UID, a, s string) {
|
||||
checkEqual := func(lastUID store.UID, a, s string) {
|
||||
t.Helper()
|
||||
n := parseNumSet(a).interpretStar(uids)
|
||||
n := parseNumSet(a).xinterpretStar(func() store.UID { return lastUID })
|
||||
ns := n.String()
|
||||
if ns != s {
|
||||
t.Fatalf("%s != %s", ns, s)
|
||||
}
|
||||
}
|
||||
|
||||
checkEqual([]store.UID{}, "1:*", "")
|
||||
checkEqual([]store.UID{1}, "1:*", "1")
|
||||
checkEqual([]store.UID{1, 3}, "1:*", "1:3")
|
||||
checkEqual([]store.UID{1, 3}, "4:*", "3")
|
||||
checkEqual([]store.UID{1, 3}, "*:4", "3")
|
||||
checkEqual([]store.UID{2, 3}, "*:4", "3")
|
||||
checkEqual([]store.UID{2, 3}, "*:1", "1:3")
|
||||
checkEqual([]store.UID{2, 3}, "1:*", "1:3")
|
||||
checkEqual([]store.UID{1, 2, 3}, "1,2,3", "1,2,3")
|
||||
checkEqual([]store.UID{}, "1,2,3", "")
|
||||
checkEqual([]store.UID{}, "1:3", "")
|
||||
checkEqual([]store.UID{}, "3:1", "")
|
||||
checkEqual(0, "1:*", "0:1")
|
||||
checkEqual(1, "1:*", "1")
|
||||
checkEqual(3, "1:*", "1:3")
|
||||
checkEqual(3, "4:*", "3:4")
|
||||
checkEqual(3, "*:4", "3:4")
|
||||
checkEqual(3, "*:4", "3:4")
|
||||
checkEqual(3, "*:1", "1:3")
|
||||
checkEqual(3, "1:*", "1:3")
|
||||
checkEqual(3, "1,2,3", "1,2,3")
|
||||
checkEqual(0, "1,2,3", "1,2,3")
|
||||
checkEqual(0, "1:3", "1:3")
|
||||
checkEqual(0, "3:1", "1:3")
|
||||
|
||||
iter := parseNumSet("1:3").interpretStar([]store.UID{}).newIter()
|
||||
if _, ok := iter.Next(); ok {
|
||||
t.Fatalf("expected immediate end for empty iter")
|
||||
}
|
||||
|
||||
iter = parseNumSet("3:1").interpretStar([]store.UID{1, 2}).newIter()
|
||||
iter := parseNumSet("3:1").xinterpretStar(func() store.UID { return 2 }).newIter()
|
||||
v0, _ := iter.Next()
|
||||
v1, _ := iter.Next()
|
||||
v2, _ := iter.Next()
|
||||
_, ok := iter.Next()
|
||||
if v0 != 1 || v1 != 2 || ok {
|
||||
t.Fatalf("got %v %v %v, expected 1, 2, false", v0, v1, ok)
|
||||
if v0 != 1 || v1 != 2 || v2 != 3 || 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) {
|
||||
tc := start(t)
|
||||
tc := start(t, false)
|
||||
defer tc.close()
|
||||
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
|
||||
// We don't implement setquota.
|
||||
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}})
|
||||
|
||||
// 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()
|
||||
|
||||
tclimit.client.Login("limit@mox.example", password0)
|
||||
tclimit.login("limit@mox.example", password0)
|
||||
|
||||
tclimit.transactf("ok", "getquotaroot inbox")
|
||||
tclimit.xuntagged(
|
||||
|
|
|
@ -6,16 +6,24 @@ import (
|
|||
"github.com/mjl-/mox/imapclient"
|
||||
)
|
||||
|
||||
// todo: check that UIDValidity is indeed updated properly.
|
||||
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()
|
||||
|
||||
tc2 := startNoSwitchboard(t)
|
||||
tc2 := startNoSwitchboard(t, uidonly)
|
||||
defer tc2.closeNoWait()
|
||||
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc2.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
tc2.login("mjl@mox.example", password0)
|
||||
|
||||
tc.transactf("bad", "rename") // Missing parameters.
|
||||
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", `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.
|
||||
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.
|
||||
if star {
|
||||
if len(c.uids) == 0 {
|
||||
return func() { xuserErrorf("cannot use * on empty mailbox") }
|
||||
}
|
||||
if isUID {
|
||||
num = uint32(c.uids[len(c.uids)-1])
|
||||
if c.uidonly {
|
||||
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") }
|
||||
}
|
||||
xcheckf(err, "get last message in mailbox")
|
||||
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 {
|
||||
num = uint32(len(c.uids))
|
||||
num = uint32(c.exists)
|
||||
}
|
||||
star = false
|
||||
}
|
||||
|
||||
// Find or verify UID of message to replace.
|
||||
var seq msgseq
|
||||
if isUID {
|
||||
seq = c.sequence(store.UID(num))
|
||||
if seq <= 0 {
|
||||
return func() { xuserErrorf("unknown uid %d", num) }
|
||||
}
|
||||
} else if num > uint32(len(c.uids)) {
|
||||
uidOld = store.UID(num)
|
||||
} else if num > c.exists {
|
||||
return func() { xuserErrorf("invalid msgseq") }
|
||||
} 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
|
||||
// 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
|
||||
|
@ -115,6 +121,7 @@ func (c *conn) cmdxReplace(isUID bool, tag, cmd string, p *parser) {
|
|||
q := bstore.QueryTx[store.Message](tx)
|
||||
q.FilterNonzero(store.Message{MailboxID: c.mailboxID, UID: uidOld})
|
||||
q.FilterEqual("Expunged", false)
|
||||
q.FilterLess("UID", c.uidnext)
|
||||
_, err = q.Get()
|
||||
if err == bstore.ErrAbsent {
|
||||
return nil
|
||||
|
@ -336,7 +343,7 @@ func (c *conn) cmdxReplace(isUID bool, tag, cmd string, p *parser) {
|
|||
c.uidAppend(nm.UID)
|
||||
// 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("* %d EXISTS", len(c.uids))
|
||||
c.xbwritelinef("* %d EXISTS", c.exists)
|
||||
}
|
||||
|
||||
// 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
|
||||
// expunged or vanished. ../rfc/7162:1900
|
||||
omsgseq := c.xsequence(om.UID)
|
||||
c.sequenceRemove(omsgseq, om.UID)
|
||||
if qresync {
|
||||
var oseq msgseq
|
||||
if c.uidonly {
|
||||
c.exists--
|
||||
} else {
|
||||
oseq = c.xsequence(om.UID)
|
||||
c.sequenceRemove(oseq, om.UID)
|
||||
}
|
||||
if qresync || c.uidonly {
|
||||
c.xbwritelinef("* VANISHED %d", om.UID)
|
||||
// ../rfc/7162:1916
|
||||
} else {
|
||||
c.xbwritelinef("* %d EXPUNGE", omsgseq)
|
||||
c.xbwritelinef("* %d EXPUNGE", oseq)
|
||||
}
|
||||
c.xwriteresultf("%s OK [HIGHESTMODSEQ %d] replaced", tag, nm.ModSeq.Client())
|
||||
}
|
||||
|
|
|
@ -7,45 +7,79 @@ import (
|
|||
)
|
||||
|
||||
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()()
|
||||
|
||||
tc := start(t)
|
||||
tc := start(t, uidonly)
|
||||
defer tc.close()
|
||||
|
||||
tc2 := startNoSwitchboard(t)
|
||||
tc2 := startNoSwitchboard(t, uidonly)
|
||||
defer tc2.closeNoWait()
|
||||
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
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.
|
||||
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.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")
|
||||
|
||||
tc2.client.Login("mjl@mox.example", password0)
|
||||
tc2.login("mjl@mox.example", password0)
|
||||
tc2.client.Select("inbox")
|
||||
|
||||
// Replace last message (msgseq 2, uid 3) in same mailbox.
|
||||
tc.lastUntagged, tc.lastResult, tc.lastErr = tc.client.Replace("2", "INBOX", makeAppend(searchMsg))
|
||||
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))
|
||||
}
|
||||
tcheck(tc.t, tc.lastErr, "read imap response")
|
||||
tc.xuntagged(
|
||||
imapclient.UntaggedResult{Status: "OK", RespText: imapclient.RespText{Code: "APPENDUID", CodeArg: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("4")}, More: ""}},
|
||||
imapclient.UntaggedExists(3),
|
||||
imapclient.UntaggedExpunge(2),
|
||||
)
|
||||
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(
|
||||
imapclient.UntaggedResult{Status: "OK", RespText: imapclient.RespText{Code: "APPENDUID", CodeArg: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("4")}, More: ""}},
|
||||
imapclient.UntaggedExists(3),
|
||||
imapclient.UntaggedExpunge(2),
|
||||
)
|
||||
}
|
||||
tc.xcodeArg(imapclient.CodeHighestModSeq(8))
|
||||
|
||||
// Check that other client sees Exists and Expunge.
|
||||
tc2.transactf("ok", "noop")
|
||||
tc2.xuntagged(
|
||||
imapclient.UntaggedExpunge(2),
|
||||
imapclient.UntaggedExists(2),
|
||||
imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(4), imapclient.FetchFlags(nil)}},
|
||||
)
|
||||
if uidonly {
|
||||
tc2.xuntagged(
|
||||
imapclient.UntaggedVanished{UIDs: xparseNumSet("3")},
|
||||
imapclient.UntaggedExists(2),
|
||||
tc.untaggedFetch(2, 4, imapclient.FetchFlags(nil)),
|
||||
)
|
||||
} else {
|
||||
tc2.xuntagged(
|
||||
imapclient.UntaggedExpunge(2),
|
||||
imapclient.UntaggedExists(2),
|
||||
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.
|
||||
tc.transactf("ok", "enable qresync")
|
||||
|
@ -58,18 +92,38 @@ func TestReplace(t *testing.T) {
|
|||
)
|
||||
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.
|
||||
tc.transactf("no", "replace 1 bogusbox {1+}\r\nx")
|
||||
if uidonly {
|
||||
tc.transactf("no", "uid replace 1 bogusbox {1+}\r\nx")
|
||||
} else {
|
||||
tc.transactf("no", "replace 1 bogusbox {1+}\r\nx")
|
||||
}
|
||||
|
||||
// Leftover data.
|
||||
tc.transactf("bad", "replace 1 inbox () {6+}\r\ntest\r\n ")
|
||||
}
|
||||
|
||||
func TestReplaceBigNonsyncLit(t *testing.T) {
|
||||
tc := start(t)
|
||||
tc := start(t, false)
|
||||
defer tc.close()
|
||||
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
tc.client.Select("inbox")
|
||||
|
||||
// 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) {
|
||||
testReplaceQuota(t, false)
|
||||
}
|
||||
|
||||
func TestReplaceQuotaUIDOnly(t *testing.T) {
|
||||
testReplaceQuota(t, true)
|
||||
}
|
||||
|
||||
func testReplaceQuota(t *testing.T, uidonly bool) {
|
||||
// with quota limit
|
||||
tc := startArgs(t, true, false, true, true, "limit")
|
||||
tc := startArgs(t, uidonly, true, false, true, true, "limit")
|
||||
defer tc.close()
|
||||
|
||||
tc.client.Login("limit@mox.example", password0)
|
||||
tc.login("limit@mox.example", password0)
|
||||
tc.client.Select("inbox")
|
||||
tc.client.Append("inbox", makeAppend("x"))
|
||||
|
||||
// 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")
|
||||
|
||||
// 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")
|
||||
|
||||
buf := make([]byte, 4000, 4002)
|
||||
|
@ -104,14 +166,14 @@ func TestReplaceQuota(t *testing.T) {
|
|||
buf = append(buf, "\r\n"...)
|
||||
|
||||
// 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)
|
||||
tc.check(err, "write replace message")
|
||||
tc.response("no")
|
||||
tc.xcode("OVERQUOTA")
|
||||
|
||||
// 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)
|
||||
tc.check(err, "write replace message")
|
||||
tc.response("no")
|
||||
|
@ -119,22 +181,30 @@ func TestReplaceQuota(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()
|
||||
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
tc.client.Select("inbox")
|
||||
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||
|
||||
// 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.
|
||||
tc2 := startNoSwitchboard(t)
|
||||
tc2 := startNoSwitchboard(t, uidonly)
|
||||
defer tc2.closeNoWait()
|
||||
tc2.client.Login("mjl@mox.example", password0)
|
||||
tc2.login("mjl@mox.example", password0)
|
||||
tc2.client.Select("inbox")
|
||||
tc2.client.StoreFlagsSet("1", true, `\Deleted`)
|
||||
tc2.client.UIDStoreFlagsSet("1", true, `\Deleted`)
|
||||
tc2.client.Expunge()
|
||||
tc2.client.Unselect()
|
||||
tc2.client.Close()
|
||||
|
@ -149,8 +219,15 @@ func TestReplaceExpunged(t *testing.T) {
|
|||
_, err := tc.client.Write(buf)
|
||||
tc.check(err, "write replace message")
|
||||
tc.response("no")
|
||||
tc.xuntagged(
|
||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchFlags{`\Deleted`}}},
|
||||
imapclient.UntaggedExpunge(1),
|
||||
)
|
||||
if uidonly {
|
||||
tc.xuntagged(
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -107,6 +107,11 @@ func (c *conn) cmdxSearch(isUID, isE bool, tag, cmd string, p *parser) {
|
|||
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.
|
||||
if save {
|
||||
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
|
||||
// sequence numbers for matching, and we must always go through the messages in
|
||||
// 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
|
||||
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"
|
||||
var total uint32
|
||||
for _, mb := range mailboxes {
|
||||
if mb.ID == c.mailboxID {
|
||||
total += uint32(len(c.uids))
|
||||
if mb.ID == c.mailboxID && !c.uidonly {
|
||||
total += c.exists
|
||||
} else {
|
||||
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}
|
||||
|
||||
msgCount := uint32(mb.MailboxCounts.Total + mb.MailboxCounts.Deleted)
|
||||
if mb.ID == c.mailboxID {
|
||||
msgCount = uint32(len(c.uids))
|
||||
if mb.ID == c.mailboxID && !c.uidonly {
|
||||
msgCount = c.exists
|
||||
}
|
||||
|
||||
// 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
|
||||
// is the highest UID, and UID 5-10 would all match.
|
||||
var cachedHighestUID store.UID
|
||||
highestUID := func() (store.UID, error) {
|
||||
xhighestUID := func() store.UID {
|
||||
if cachedHighestUID > 0 {
|
||||
return cachedHighestUID, nil
|
||||
return cachedHighestUID
|
||||
}
|
||||
|
||||
q := bstore.QueryTx[store.Message](tx)
|
||||
q.FilterNonzero(store.Message{MailboxID: mb.ID})
|
||||
q.FilterEqual("Expunged", false)
|
||||
if mb.ID == c.mailboxID {
|
||||
q.FilterLess("UID", c.uidnext)
|
||||
}
|
||||
q.SortDesc("UID")
|
||||
q.Limit(1)
|
||||
m, err := q.Get()
|
||||
if err == bstore.ErrAbsent {
|
||||
xuserErrorf("cannot use * on empty mailbox")
|
||||
}
|
||||
xcheckf(err, "get last uid")
|
||||
cachedHighestUID = m.UID
|
||||
return cachedHighestUID, err
|
||||
return cachedHighestUID
|
||||
}
|
||||
|
||||
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.FilterNonzero(store.Message{MailboxID: mb.ID})
|
||||
q.FilterEqual("Expunged", false)
|
||||
if mb.ID == c.mailboxID {
|
||||
q.FilterLess("UID", c.uidnext)
|
||||
}
|
||||
q.SortAsc("UID")
|
||||
for m, err := range q.All() {
|
||||
xcheckf(err, "list messages in mailbox")
|
||||
|
@ -416,7 +431,7 @@ func (c *conn) cmdxSearch(isUID, isE bool, tag, cmd string, p *parser) {
|
|||
}
|
||||
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.MaxModSeq = max(result.MaxModSeq, m.ModSeq)
|
||||
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.FilterEqual("Expunged", false)
|
||||
q.FilterGreater("UID", lastUID)
|
||||
if mb.ID == c.mailboxID {
|
||||
q.FilterLess("UID", c.uidnext)
|
||||
}
|
||||
q.SortDesc("UID")
|
||||
for m, err := range q.All() {
|
||||
xcheckf(err, "list messages in mailbox")
|
||||
|
@ -454,7 +472,7 @@ func (c *conn) cmdxSearch(isUID, isE bool, tag, cmd string, p *parser) {
|
|||
progress++
|
||||
|
||||
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.MaxModSeq = max(result.MaxModSeq, m.ModSeq)
|
||||
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
|
||||
// into multiple responses. ../rfc/9051:6809 ../rfc/3501:4833
|
||||
for len(result.UIDs) > 0 {
|
||||
n := len(result.UIDs)
|
||||
if n > 100 {
|
||||
n = 100
|
||||
}
|
||||
n := min(100, len(result.UIDs))
|
||||
s := ""
|
||||
for _, v := range result.UIDs[:n] {
|
||||
if !isUID {
|
||||
|
@ -516,9 +531,7 @@ func (c *conn) cmdxSearch(isUID, isE bool, tag, cmd string, p *parser) {
|
|||
if save {
|
||||
// ../rfc/9051:3784 ../rfc/5182:13
|
||||
c.searchResult = results[0].UIDs
|
||||
if sanityChecks {
|
||||
checkUIDs(c.searchResult)
|
||||
}
|
||||
c.checkUIDs(c.searchResult, false)
|
||||
}
|
||||
|
||||
// No untagged ESEARCH response if nothing was requested. ../rfc/9051:4160
|
||||
|
@ -584,27 +597,33 @@ func (c *conn) cmdxSearch(isUID, isE bool, tag, cmd string, p *parser) {
|
|||
}
|
||||
|
||||
type search struct {
|
||||
c *conn
|
||||
tx *bstore.Tx
|
||||
msgCount uint32 // Number of messages in mailbox (or session when selected).
|
||||
seq msgseq // Can be 0, for other mailboxes than selected in case of MAX.
|
||||
m store.Message
|
||||
mr *store.MsgReader
|
||||
p *message.Part
|
||||
highestUID func() (store.UID, error)
|
||||
c *conn
|
||||
tx *bstore.Tx
|
||||
msgCount uint32 // Number of messages in mailbox (or session when selected).
|
||||
seq msgseq // Can be 0, for other mailboxes than selected in case of MAX.
|
||||
m store.Message
|
||||
mr *store.MsgReader
|
||||
p *message.Part
|
||||
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 {
|
||||
seq = c.sequence(m.UID)
|
||||
if seq == 0 {
|
||||
// Session has not yet seen this message, and is not expecting to get a result that
|
||||
// includes it.
|
||||
return false
|
||||
// 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)
|
||||
if seq == 0 {
|
||||
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() {
|
||||
if s.mr != nil {
|
||||
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
|
||||
xuserErrorf("cannot use search result from another mailbox")
|
||||
}
|
||||
match, err := sk.uidSet.containsKnownUID(s.m.UID, c.searchResult, s.highestUID)
|
||||
xcheckf(err, "checking for presence in uid set")
|
||||
return match
|
||||
return sk.uidSet.xcontainsKnownUID(s.m.UID, c.searchResult, s.xhighestUID)
|
||||
}
|
||||
|
||||
// Parsed part.
|
||||
|
|
|
@ -62,9 +62,17 @@ func (tc *testconn) xesearch(exp imapclient.UntaggedEsearch) {
|
|||
}
|
||||
|
||||
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()
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
tc.client.Select("inbox")
|
||||
|
||||
// Add 5 and delete first 4 messages. So UIDs start at 5.
|
||||
|
@ -73,7 +81,7 @@ func TestSearch(t *testing.T) {
|
|||
for range 5 {
|
||||
tc.client.Append("inbox", makeAppendTime(exampleMsg, received))
|
||||
}
|
||||
tc.client.StoreFlagsSet("1:4", true, `\Deleted`)
|
||||
tc.client.UIDStoreFlagsSet("1:4", true, `\Deleted`)
|
||||
tc.client.Expunge()
|
||||
|
||||
received = time.Date(2022, time.January, 1, 9, 0, 0, 0, time.UTC)
|
||||
|
@ -98,298 +106,314 @@ func TestSearch(t *testing.T) {
|
|||
|
||||
// We now have sequence numbers 1,2,3 and UIDs 5,6,7.
|
||||
|
||||
// We need to be selected. Not the case for ESEARCH command.
|
||||
tc.client.Unselect()
|
||||
tc.transactf("no", "search all")
|
||||
tc.client.Select("inbox")
|
||||
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.
|
||||
tc.client.Unselect()
|
||||
tc.transactf("no", "search all")
|
||||
tc.client.Select("inbox")
|
||||
|
||||
tc.transactf("ok", "search all")
|
||||
tc.xsearch(1, 2, 3)
|
||||
tc.transactf("ok", "search all")
|
||||
tc.xsearch(1, 2, 3)
|
||||
}
|
||||
|
||||
tc.transactf("ok", "uid search all")
|
||||
tc.xsearch(5, 6, 7)
|
||||
|
||||
tc.transactf("ok", "search answered")
|
||||
tc.xsearch(3)
|
||||
|
||||
tc.transactf("ok", `search bcc "bcc@mox.example"`)
|
||||
tc.xsearch(2, 3)
|
||||
|
||||
tc.transactf("ok", "search before 1-Jan-2038")
|
||||
tc.xsearch(1, 2, 3)
|
||||
tc.transactf("ok", "search before 1-Jan-2020")
|
||||
tc.xsearch() // Before is about received, not date header of message.
|
||||
|
||||
// WITHIN extension with OLDER & YOUNGER.
|
||||
tc.transactf("ok", "search older 60")
|
||||
tc.xsearch(1, 2, 3)
|
||||
tc.transactf("ok", "search younger 60")
|
||||
tc.xsearch()
|
||||
|
||||
// SAVEDATE extension.
|
||||
tc.transactf("ok", "search savedbefore %s", saveDate.Add(24*time.Hour).Format("2-Jan-2006"))
|
||||
tc.xsearch(1, 2, 3)
|
||||
tc.transactf("ok", "search savedbefore %s", saveDate.Add(-24*time.Hour).Format("2-Jan-2006"))
|
||||
tc.xsearch()
|
||||
tc.transactf("ok", "search savedon %s", saveDate.Format("2-Jan-2006"))
|
||||
tc.xsearch(1, 2, 3)
|
||||
tc.transactf("ok", "search savedon %s", saveDate.Add(-24*time.Hour).Format("2-Jan-2006"))
|
||||
tc.xsearch()
|
||||
tc.transactf("ok", "search savedsince %s", saveDate.Add(-24*time.Hour).Format("2-Jan-2006"))
|
||||
tc.xsearch(1, 2, 3)
|
||||
tc.transactf("ok", "search savedsince %s", saveDate.Add(24*time.Hour).Format("2-Jan-2006"))
|
||||
tc.xsearch()
|
||||
|
||||
tc.transactf("ok", `search body "Joe"`)
|
||||
tc.xsearch(1)
|
||||
tc.transactf("ok", `search body "Joe" body "bogus"`)
|
||||
tc.xsearch()
|
||||
tc.transactf("ok", `search body "Joe" text "Blurdybloop"`)
|
||||
tc.xsearch(1)
|
||||
tc.transactf("ok", `search body "Joe" not text "mox"`)
|
||||
tc.xsearch(1)
|
||||
tc.transactf("ok", `search body "Joe" not not body "Joe"`)
|
||||
tc.xsearch(1)
|
||||
tc.transactf("ok", `search body "this is plain text"`)
|
||||
tc.xsearch(2, 3)
|
||||
tc.transactf("ok", `search body "this is html"`)
|
||||
tc.xsearch(2, 3)
|
||||
|
||||
tc.transactf("ok", `search cc "xcc@mox.example"`)
|
||||
tc.xsearch(2, 3)
|
||||
|
||||
tc.transactf("ok", `search deleted`)
|
||||
tc.xsearch(3)
|
||||
|
||||
tc.transactf("ok", `search flagged`)
|
||||
tc.xsearch(3)
|
||||
|
||||
tc.transactf("ok", `search from "foobar@Blurdybloop.example"`)
|
||||
tc.xsearch(1)
|
||||
|
||||
tc.transactf("ok", `search keyword $Forwarded`)
|
||||
tc.xsearch(3)
|
||||
|
||||
tc.transactf("ok", `search keyword Custom1`)
|
||||
tc.xsearch(3)
|
||||
|
||||
tc.transactf("ok", `search keyword custom2`)
|
||||
tc.xsearch(3)
|
||||
|
||||
tc.transactf("ok", `search new`)
|
||||
tc.xsearch() // New requires a message to be recent. We pretend all messages are not recent.
|
||||
|
||||
tc.transactf("ok", `search old`)
|
||||
tc.xsearch(1, 2, 3)
|
||||
|
||||
tc.transactf("ok", `search on 1-Jan-2022`)
|
||||
tc.xsearch(2, 3)
|
||||
|
||||
tc.transactf("ok", `search recent`)
|
||||
tc.xsearch()
|
||||
|
||||
tc.transactf("ok", `search seen`)
|
||||
tc.xsearch(3)
|
||||
|
||||
tc.transactf("ok", `search since 1-Jan-2020`)
|
||||
tc.xsearch(1, 2, 3)
|
||||
|
||||
tc.transactf("ok", `search subject "afternoon"`)
|
||||
tc.xsearch(1)
|
||||
|
||||
tc.transactf("ok", `search text "Joe"`)
|
||||
tc.xsearch(1)
|
||||
|
||||
tc.transactf("ok", `search to "mooch@owatagu.siam.edu.example"`)
|
||||
tc.xsearch(1)
|
||||
|
||||
tc.transactf("ok", `search unanswered`)
|
||||
tc.xsearch(1, 2)
|
||||
|
||||
tc.transactf("ok", `search undeleted`)
|
||||
tc.xsearch(1, 2)
|
||||
|
||||
tc.transactf("ok", `search unflagged`)
|
||||
tc.xsearch(1, 2)
|
||||
|
||||
tc.transactf("ok", `search unkeyword $Junk`)
|
||||
tc.xsearch(1, 2)
|
||||
|
||||
tc.transactf("ok", `search unkeyword custom1`)
|
||||
tc.xsearch(1, 2)
|
||||
|
||||
tc.transactf("ok", `search unseen`)
|
||||
tc.xsearch(1, 2)
|
||||
|
||||
tc.transactf("ok", `search draft`)
|
||||
tc.xsearch(3)
|
||||
|
||||
tc.transactf("ok", `search header "subject" "afternoon"`)
|
||||
tc.xsearch(1)
|
||||
|
||||
tc.transactf("ok", `search larger 1`)
|
||||
tc.xsearch(1, 2, 3)
|
||||
|
||||
tc.transactf("ok", `search not text "mox"`)
|
||||
tc.xsearch(1)
|
||||
|
||||
tc.transactf("ok", `search or seen unseen`)
|
||||
tc.xsearch(1, 2, 3)
|
||||
|
||||
tc.transactf("ok", `search or unseen seen`)
|
||||
tc.xsearch(1, 2, 3)
|
||||
|
||||
tc.transactf("ok", `search sentbefore 8-Feb-1994`)
|
||||
tc.xsearch(1)
|
||||
|
||||
tc.transactf("ok", `search senton 7-Feb-1994`)
|
||||
tc.xsearch(1)
|
||||
|
||||
tc.transactf("ok", `search sentsince 6-Feb-1994`)
|
||||
tc.xsearch(1, 2, 3)
|
||||
|
||||
tc.transactf("ok", `search smaller 9999999`)
|
||||
tc.xsearch(1, 2, 3)
|
||||
|
||||
tc.transactf("ok", `search uid 1`)
|
||||
tc.xsearch()
|
||||
|
||||
tc.transactf("ok", `search uid 5`)
|
||||
tc.xsearch(1)
|
||||
|
||||
tc.transactf("ok", `search or larger 1000000 smaller 1`)
|
||||
tc.xsearch()
|
||||
|
||||
tc.transactf("ok", `search undraft`)
|
||||
tc.xsearch(1, 2)
|
||||
|
||||
tc.transactf("no", `search charset unknown text "mox"`)
|
||||
tc.transactf("ok", `search charset us-ascii text "mox"`)
|
||||
tc.xsearch(2, 3)
|
||||
tc.transactf("ok", `search charset utf-8 text "mox"`)
|
||||
tc.xsearch(2, 3)
|
||||
|
||||
// Check for properly formed INPROGRESS response code.
|
||||
orig := inProgressPeriod
|
||||
inProgressPeriod = 0
|
||||
tc.cmdf("tag1", "search undraft")
|
||||
tc.response("ok")
|
||||
|
||||
inprogress := func(cur, goal uint32) imapclient.UntaggedResult {
|
||||
return imapclient.UntaggedResult{
|
||||
Status: "OK",
|
||||
RespText: imapclient.RespText{
|
||||
Code: "INPROGRESS",
|
||||
CodeArg: imapclient.CodeInProgress{Tag: "tag1", Current: &cur, Goal: &goal},
|
||||
More: "still searching",
|
||||
},
|
||||
}
|
||||
}
|
||||
tc.xuntagged(
|
||||
imapclient.UntaggedSearch([]uint32{1, 2}),
|
||||
// Due to inProgressPeriod 0, we get an inprogress response for each message in the mailbox.
|
||||
inprogress(0, 3),
|
||||
inprogress(1, 3),
|
||||
inprogress(2, 3),
|
||||
)
|
||||
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.
|
||||
tc.transactf("ok", "search return () all")
|
||||
tc.xesearch(esearchall("1:3")) // Without any options, "ALL" is implicit.
|
||||
if !uidonly {
|
||||
tc.transactf("ok", "search answered")
|
||||
tc.xsearch(3)
|
||||
|
||||
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.transactf("ok", `search bcc "bcc@mox.example"`)
|
||||
tc.xsearch(2, 3)
|
||||
|
||||
tc.transactf("ok", "search before 1-Jan-2038")
|
||||
tc.xsearch(1, 2, 3)
|
||||
tc.transactf("ok", "search before 1-Jan-2020")
|
||||
tc.xsearch() // Before is about received, not date header of message.
|
||||
|
||||
// WITHIN extension with OLDER & YOUNGER.
|
||||
tc.transactf("ok", "search older 60")
|
||||
tc.xsearch(1, 2, 3)
|
||||
tc.transactf("ok", "search younger 60")
|
||||
tc.xsearch()
|
||||
|
||||
// SAVEDATE extension.
|
||||
tc.transactf("ok", "search savedbefore %s", saveDate.Add(24*time.Hour).Format("2-Jan-2006"))
|
||||
tc.xsearch(1, 2, 3)
|
||||
tc.transactf("ok", "search savedbefore %s", saveDate.Add(-24*time.Hour).Format("2-Jan-2006"))
|
||||
tc.xsearch()
|
||||
tc.transactf("ok", "search savedon %s", saveDate.Format("2-Jan-2006"))
|
||||
tc.xsearch(1, 2, 3)
|
||||
tc.transactf("ok", "search savedon %s", saveDate.Add(-24*time.Hour).Format("2-Jan-2006"))
|
||||
tc.xsearch()
|
||||
tc.transactf("ok", "search savedsince %s", saveDate.Add(-24*time.Hour).Format("2-Jan-2006"))
|
||||
tc.xsearch(1, 2, 3)
|
||||
tc.transactf("ok", "search savedsince %s", saveDate.Add(24*time.Hour).Format("2-Jan-2006"))
|
||||
tc.xsearch()
|
||||
|
||||
tc.transactf("ok", `search body "Joe"`)
|
||||
tc.xsearch(1)
|
||||
tc.transactf("ok", `search body "Joe" body "bogus"`)
|
||||
tc.xsearch()
|
||||
tc.transactf("ok", `search body "Joe" text "Blurdybloop"`)
|
||||
tc.xsearch(1)
|
||||
tc.transactf("ok", `search body "Joe" not text "mox"`)
|
||||
tc.xsearch(1)
|
||||
tc.transactf("ok", `search body "Joe" not not body "Joe"`)
|
||||
tc.xsearch(1)
|
||||
tc.transactf("ok", `search body "this is plain text"`)
|
||||
tc.xsearch(2, 3)
|
||||
tc.transactf("ok", `search body "this is html"`)
|
||||
tc.xsearch(2, 3)
|
||||
|
||||
tc.transactf("ok", `search cc "xcc@mox.example"`)
|
||||
tc.xsearch(2, 3)
|
||||
|
||||
tc.transactf("ok", `search deleted`)
|
||||
tc.xsearch(3)
|
||||
|
||||
tc.transactf("ok", `search flagged`)
|
||||
tc.xsearch(3)
|
||||
|
||||
tc.transactf("ok", `search from "foobar@Blurdybloop.example"`)
|
||||
tc.xsearch(1)
|
||||
|
||||
tc.transactf("ok", `search keyword $Forwarded`)
|
||||
tc.xsearch(3)
|
||||
|
||||
tc.transactf("ok", `search keyword Custom1`)
|
||||
tc.xsearch(3)
|
||||
|
||||
tc.transactf("ok", `search keyword custom2`)
|
||||
tc.xsearch(3)
|
||||
|
||||
tc.transactf("ok", `search new`)
|
||||
tc.xsearch() // New requires a message to be recent. We pretend all messages are not recent.
|
||||
|
||||
tc.transactf("ok", `search old`)
|
||||
tc.xsearch(1, 2, 3)
|
||||
|
||||
tc.transactf("ok", `search on 1-Jan-2022`)
|
||||
tc.xsearch(2, 3)
|
||||
|
||||
tc.transactf("ok", `search recent`)
|
||||
tc.xsearch()
|
||||
|
||||
tc.transactf("ok", `search seen`)
|
||||
tc.xsearch(3)
|
||||
|
||||
tc.transactf("ok", `search since 1-Jan-2020`)
|
||||
tc.xsearch(1, 2, 3)
|
||||
|
||||
tc.transactf("ok", `search subject "afternoon"`)
|
||||
tc.xsearch(1)
|
||||
|
||||
tc.transactf("ok", `search text "Joe"`)
|
||||
tc.xsearch(1)
|
||||
|
||||
tc.transactf("ok", `search to "mooch@owatagu.siam.edu.example"`)
|
||||
tc.xsearch(1)
|
||||
|
||||
tc.transactf("ok", `search unanswered`)
|
||||
tc.xsearch(1, 2)
|
||||
|
||||
tc.transactf("ok", `search undeleted`)
|
||||
tc.xsearch(1, 2)
|
||||
|
||||
tc.transactf("ok", `search unflagged`)
|
||||
tc.xsearch(1, 2)
|
||||
|
||||
tc.transactf("ok", `search unkeyword $Junk`)
|
||||
tc.xsearch(1, 2)
|
||||
|
||||
tc.transactf("ok", `search unkeyword custom1`)
|
||||
tc.xsearch(1, 2)
|
||||
|
||||
tc.transactf("ok", `search unseen`)
|
||||
tc.xsearch(1, 2)
|
||||
|
||||
tc.transactf("ok", `search draft`)
|
||||
tc.xsearch(3)
|
||||
|
||||
tc.transactf("ok", `search header "subject" "afternoon"`)
|
||||
tc.xsearch(1)
|
||||
|
||||
tc.transactf("ok", `search larger 1`)
|
||||
tc.xsearch(1, 2, 3)
|
||||
|
||||
tc.transactf("ok", `search not text "mox"`)
|
||||
tc.xsearch(1)
|
||||
|
||||
tc.transactf("ok", `search or seen unseen`)
|
||||
tc.xsearch(1, 2, 3)
|
||||
|
||||
tc.transactf("ok", `search or unseen seen`)
|
||||
tc.xsearch(1, 2, 3)
|
||||
|
||||
tc.transactf("ok", `search sentbefore 8-Feb-1994`)
|
||||
tc.xsearch(1)
|
||||
|
||||
tc.transactf("ok", `search senton 7-Feb-1994`)
|
||||
tc.xsearch(1)
|
||||
|
||||
tc.transactf("ok", `search sentsince 6-Feb-1994`)
|
||||
tc.xsearch(1, 2, 3)
|
||||
|
||||
tc.transactf("ok", `search smaller 9999999`)
|
||||
tc.xsearch(1, 2, 3)
|
||||
|
||||
tc.transactf("ok", `search uid 1`)
|
||||
tc.xsearch()
|
||||
|
||||
tc.transactf("ok", `search uid 5`)
|
||||
tc.xsearch(1)
|
||||
|
||||
tc.transactf("ok", `search or larger 1000000 smaller 1`)
|
||||
tc.xsearch()
|
||||
|
||||
tc.transactf("ok", `search undraft`)
|
||||
tc.xsearch(1, 2)
|
||||
|
||||
tc.transactf("no", `search charset unknown text "mox"`)
|
||||
tc.transactf("ok", `search charset us-ascii text "mox"`)
|
||||
tc.xsearch(2, 3)
|
||||
tc.transactf("ok", `search charset utf-8 text "mox"`)
|
||||
tc.xsearch(2, 3)
|
||||
|
||||
// Check for properly formed INPROGRESS response code.
|
||||
orig := inProgressPeriod
|
||||
inProgressPeriod = 0
|
||||
tc.cmdf("tag1", "search undraft")
|
||||
tc.response("ok")
|
||||
|
||||
inprogress := func(cur, goal uint32) imapclient.UntaggedResult {
|
||||
return imapclient.UntaggedResult{
|
||||
Status: "OK",
|
||||
RespText: imapclient.RespText{
|
||||
Code: "INPROGRESS",
|
||||
CodeArg: imapclient.CodeInProgress{Tag: "tag1", Current: &cur, Goal: &goal},
|
||||
More: "still searching",
|
||||
},
|
||||
}
|
||||
}
|
||||
tc.xuntagged(
|
||||
imapclient.UntaggedSearch([]uint32{1, 2}),
|
||||
// Due to inProgressPeriod 0, we get an inprogress response for each message in the mailbox.
|
||||
inprogress(0, 3),
|
||||
inprogress(1, 3),
|
||||
inprogress(2, 3),
|
||||
)
|
||||
inProgressPeriod = orig
|
||||
|
||||
// Do new-style ESEARCH requests with RETURN. We should get an ESEARCH response.
|
||||
tc.transactf("ok", "search return () all")
|
||||
tc.xesearch(esearchall("1:3")) // Without any options, "ALL" is implicit.
|
||||
|
||||
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.transactf("ok", "search return (min) all")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{Min: 1})
|
||||
|
||||
tc.transactf("ok", "search return (min) 3")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{Min: 3})
|
||||
|
||||
tc.transactf("ok", "search return (min) NOT all")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{}) // Min not present if no match.
|
||||
|
||||
tc.transactf("ok", "search return (max) all")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{Max: 3})
|
||||
|
||||
tc.transactf("ok", "search return (max) 1")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{Max: 1})
|
||||
|
||||
tc.transactf("ok", "search return (max) not all")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{}) // Max not present if no match.
|
||||
|
||||
tc.transactf("ok", "search return (min max) all")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 3})
|
||||
|
||||
tc.transactf("ok", "search return (min max) 1")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 1})
|
||||
|
||||
tc.transactf("ok", "search return (min max) not all")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{})
|
||||
|
||||
tc.transactf("ok", "search return (all) not all")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{}) // All not present if no match.
|
||||
|
||||
tc.transactf("ok", "search return (min max all) not all")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{})
|
||||
|
||||
tc.transactf("ok", "search return (min max all count) not all")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{Count: uint32ptr(0)})
|
||||
|
||||
tc.transactf("ok", "search return (min max count all) 1,3")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 3, Count: uint32ptr(2), All: esearchall0("1,3")})
|
||||
|
||||
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.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.xesearch(imapclient.UntaggedEsearch{Min: 1})
|
||||
|
||||
tc.transactf("ok", "search return (min) 3")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{Min: 3})
|
||||
|
||||
tc.transactf("ok", "search return (min) NOT all")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{}) // Min not present if no match.
|
||||
|
||||
tc.transactf("ok", "search return (max) all")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{Max: 3})
|
||||
|
||||
tc.transactf("ok", "search return (max) 1")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{Max: 1})
|
||||
|
||||
tc.transactf("ok", "search return (max) not all")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{}) // Max not present if no match.
|
||||
|
||||
tc.transactf("ok", "search return (min max) all")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 3})
|
||||
|
||||
tc.transactf("ok", "search return (min max) 1")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 1})
|
||||
|
||||
tc.transactf("ok", "search return (min max) not all")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{})
|
||||
|
||||
tc.transactf("ok", "search return (all) not all")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{}) // All not present if no match.
|
||||
|
||||
tc.transactf("ok", "search return (min max all) not all")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{})
|
||||
|
||||
tc.transactf("ok", "search return (min max all count) not all")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{Count: uint32ptr(0)})
|
||||
|
||||
tc.transactf("ok", "search return (min max count all) 1,3")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 3, Count: uint32ptr(2), All: esearchall0("1,3")})
|
||||
|
||||
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.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")})
|
||||
if !uidonly {
|
||||
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.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.transactf("no", `search return () charset unknown text "mox"`)
|
||||
tc.transactf("ok", `search return () charset us-ascii text "mox"`)
|
||||
tc.xesearch(esearchall("2:3"))
|
||||
tc.transactf("ok", `search return () charset utf-8 text "mox"`)
|
||||
tc.xesearch(esearchall("2:3"))
|
||||
if !uidonly {
|
||||
tc.transactf("no", `search return () charset unknown text "mox"`)
|
||||
tc.transactf("ok", `search return () charset us-ascii text "mox"`)
|
||||
tc.xesearch(esearchall("2:3"))
|
||||
tc.transactf("ok", `search return () charset utf-8 text "mox"`)
|
||||
tc.xesearch(esearchall("2:3"))
|
||||
|
||||
tc.transactf("bad", `search return (unknown) all`)
|
||||
tc.transactf("bad", `search return (unknown) all`)
|
||||
|
||||
tc.transactf("ok", "search return (save) 2")
|
||||
tc.xnountagged() // ../rfc/9051:3800
|
||||
tc.transactf("ok", "fetch $ (uid)")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(6)}})
|
||||
tc.transactf("ok", "search return (save) 2")
|
||||
tc.xnountagged() // ../rfc/9051:3800
|
||||
tc.transactf("ok", "fetch $ (uid)")
|
||||
tc.xuntagged(tc.untaggedFetch(2, 6))
|
||||
|
||||
tc.transactf("ok", "search return (all) $")
|
||||
tc.xesearch(esearchall("2"))
|
||||
tc.transactf("ok", "search return (all) $")
|
||||
tc.xesearch(esearchall("2"))
|
||||
|
||||
tc.transactf("ok", "search return (save) $")
|
||||
tc.xnountagged()
|
||||
tc.transactf("ok", "search return (save) $")
|
||||
tc.xnountagged()
|
||||
|
||||
tc.transactf("ok", "search return (save all) all")
|
||||
tc.xesearch(esearchall("1:3"))
|
||||
tc.transactf("ok", "search return (save all) all")
|
||||
tc.xesearch(esearchall("1:3"))
|
||||
|
||||
tc.transactf("ok", "search return (all save) all")
|
||||
tc.xesearch(esearchall("1:3"))
|
||||
tc.transactf("ok", "search return (all save) all")
|
||||
tc.xesearch(esearchall("1:3"))
|
||||
|
||||
tc.transactf("ok", "search return (min save) all")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{Min: 1})
|
||||
tc.transactf("ok", "fetch $ (uid)")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(5)}})
|
||||
tc.transactf("ok", "search return (min save) all")
|
||||
tc.xesearch(imapclient.UntaggedEsearch{Min: 1})
|
||||
tc.transactf("ok", "fetch $ (uid)")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 5))
|
||||
}
|
||||
|
||||
// Do a seemingly old-style search command with IMAP4rev2 enabled. We'll still get ESEARCH responses.
|
||||
tc.client.Enable("IMAP4rev2")
|
||||
tc.transactf("ok", `search undraft`)
|
||||
tc.xesearch(esearchall("1:2"))
|
||||
|
||||
if !uidonly {
|
||||
tc.transactf("ok", `search undraft`)
|
||||
tc.xesearch(esearchall("1:2"))
|
||||
}
|
||||
|
||||
// Long commands should be rejected, not allocating too much memory.
|
||||
lit := make([]byte, 100*1024+1)
|
||||
|
@ -470,21 +494,27 @@ func esearchall0(ss string) imapclient.NumSet {
|
|||
return seqset
|
||||
}
|
||||
|
||||
// Test the MULTISEARCH extension. Where we don't need to have a mailbox selected,
|
||||
// operating without messag sequence numbers, and return untagged esearch responses
|
||||
// that include the mailbox and uidvalidity.
|
||||
func TestSearchMulti(t *testing.T) {
|
||||
testSearchMulti(t, false)
|
||||
testSearchMulti(t, true)
|
||||
func TestSearchMultiUnselected(t *testing.T) {
|
||||
testSearchMulti(t, false, false)
|
||||
}
|
||||
|
||||
// Run multisearch tests with or without a mailbox selected.
|
||||
func testSearchMulti(t *testing.T, selected bool) {
|
||||
func TestSearchMultiSelected(t *testing.T) {
|
||||
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()()
|
||||
|
||||
tc := start(t)
|
||||
tc := start(t, uidonly)
|
||||
defer tc.close()
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
tc.client.Select("inbox")
|
||||
|
||||
// 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 {
|
||||
tc.client.Append("inbox", makeAppendTime(exampleMsg, received))
|
||||
}
|
||||
tc.client.StoreFlagsSet("1:4", true, `\Deleted`)
|
||||
tc.client.UIDStoreFlagsSet("1:4", true, `\Deleted`)
|
||||
tc.client.Expunge()
|
||||
|
||||
// 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
|
||||
// get the first and last message, but the message sequence set forces a scan.
|
||||
tc.cmdf("Tag1", `Esearch In (Mailboxes Inbox) Return (Min Max) 1:*`)
|
||||
tc.response("ok")
|
||||
tc.xuntagged(
|
||||
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Min: 5, Max: 7},
|
||||
)
|
||||
// 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.response("ok")
|
||||
tc.xuntagged(
|
||||
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Min: 5, Max: 7},
|
||||
)
|
||||
}
|
||||
|
||||
// Search with uid set with "$highnum:*" forces getting highest uid.
|
||||
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
|
||||
// the session yet. The message in Archive is returned, since there is no session
|
||||
// limitation.
|
||||
tc2 := startNoSwitchboard(t)
|
||||
tc2 := startNoSwitchboard(t, uidonly)
|
||||
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("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: "Archive", UIDValidity: 1, UID: true, Count: uint32ptr(2)},
|
||||
imapclient.UntaggedExists(4),
|
||||
imapclient.UntaggedFetch{Seq: 4, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(8), imapclient.FetchFlags(nil)}},
|
||||
tc.untaggedFetch(4, 8, imapclient.FetchFlags(nil)),
|
||||
)
|
||||
} else {
|
||||
tc.xuntagged(
|
||||
|
|
|
@ -8,20 +8,28 @@ import (
|
|||
)
|
||||
|
||||
func TestSelect(t *testing.T) {
|
||||
testSelectExamine(t, false)
|
||||
testSelectExamine(t, false, false)
|
||||
}
|
||||
|
||||
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.
|
||||
func testSelectExamine(t *testing.T, examine bool) {
|
||||
func testSelectExamine(t *testing.T, examine, uidonly bool) {
|
||||
defer mockUIDValidity()()
|
||||
tc := start(t)
|
||||
tc := start(t, uidonly)
|
||||
defer tc.close()
|
||||
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
|
||||
cmd := "select"
|
||||
okcode := "READ-WRITE"
|
||||
|
@ -62,7 +70,11 @@ func testSelectExamine(t *testing.T, examine bool) {
|
|||
// Append a message. It will be reported as UNSEEN.
|
||||
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||
tc.transactf("ok", "%s inbox", cmd)
|
||||
tc.xuntagged(uclosed, uflags, upermflags, urecent, uunseen, uexists1, uuidval1, uuidnext2, ulist)
|
||||
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.xcode(okcode)
|
||||
|
||||
// With imap4rev2, we no longer get untagged RECENT or untagged UNSEEN.
|
||||
|
|
|
@ -184,6 +184,7 @@ var serverCapabilities = strings.Join([]string{
|
|||
"INPROGRESS", // ../rfc/9585:101
|
||||
"MULTISEARCH", // ../rfc/7377:187
|
||||
"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.
|
||||
}, " ")
|
||||
|
||||
|
@ -244,6 +245,9 @@ type conn struct {
|
|||
|
||||
mailboxID int64 // Only for StateSelected.
|
||||
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.
|
||||
}
|
||||
|
||||
|
@ -258,6 +262,7 @@ const (
|
|||
capCondstore capability = "CONDSTORE"
|
||||
capQresync capability = "QRESYNC"
|
||||
capMetadata capability = "METADATA"
|
||||
capUIDOnly capability = "UIDONLY"
|
||||
)
|
||||
|
||||
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")
|
||||
)
|
||||
|
||||
// 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){
|
||||
// Any state.
|
||||
"capability": (*conn).cmdCapability,
|
||||
|
@ -499,6 +508,8 @@ func (c *conn) unselect() {
|
|||
c.state = stateAuthenticated
|
||||
}
|
||||
c.mailboxID = 0
|
||||
c.uidnext = 0
|
||||
c.exists = 0
|
||||
c.uids = nil
|
||||
}
|
||||
|
||||
|
@ -1343,6 +1354,11 @@ func (c *conn) 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)
|
||||
}
|
||||
|
||||
|
@ -1414,6 +1430,9 @@ func xmailboxPatternMatcher(ref string, patterns []string) matchStringer {
|
|||
}
|
||||
|
||||
func (c *conn) sequence(uid store.UID) msgseq {
|
||||
if c.uidonly {
|
||||
panic("sequence with uidonly")
|
||||
}
|
||||
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 {
|
||||
if c.uidonly {
|
||||
panic("xsequence with uidonly")
|
||||
}
|
||||
seq := c.sequence(uid)
|
||||
if seq <= 0 {
|
||||
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) {
|
||||
if c.uidonly {
|
||||
panic("sequenceRemove with uidonly")
|
||||
}
|
||||
i := seq - 1
|
||||
if c.uids[i] != uid {
|
||||
xserverErrorf("got uid %d at msgseq %d, expected uid %d", uid, seq, c.uids[i])
|
||||
}
|
||||
copy(c.uids[i:], c.uids[i+1:])
|
||||
c.uids = c.uids[:len(c.uids)-1]
|
||||
if sanityChecks {
|
||||
checkUIDs(c.uids)
|
||||
}
|
||||
c.uids = c.uids[:c.exists-1]
|
||||
c.exists--
|
||||
c.checkUIDs(c.uids, true)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// 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
|
||||
}
|
||||
|
||||
if uidSearch(c.uids, uid) > 0 {
|
||||
xserverErrorf("uid already present (%w)", errProtocol)
|
||||
}
|
||||
if len(c.uids) > 0 && uid < c.uids[len(c.uids)-1] {
|
||||
xserverErrorf("new uid %d is smaller than last uid %d (%w)", uid, c.uids[len(c.uids)-1], errProtocol)
|
||||
if c.exists > 0 && uid < c.uids[c.exists-1] {
|
||||
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)
|
||||
if sanityChecks {
|
||||
checkUIDs(c.uids)
|
||||
}
|
||||
return msgseq(len(c.uids))
|
||||
c.checkUIDs(c.uids, true)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if uid == 0 || i > 0 && uid <= uids[i-1] {
|
||||
xserverErrorf("bad uids %v", uids)
|
||||
|
@ -1480,75 +1521,121 @@ func checkUIDs(uids []store.UID) {
|
|||
}
|
||||
}
|
||||
|
||||
func (c *conn) xnumSetUIDs(isUID bool, nums numSet) []store.UID {
|
||||
_, uids := c.xnumSetConditionUIDs(false, true, isUID, nums)
|
||||
return uids
|
||||
func slicesAny[T any](l []T) []any {
|
||||
r := make([]any, len(l))
|
||||
for i, v := range l {
|
||||
r[i] = v
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (c *conn) xnumSetCondition(isUID bool, nums numSet) []any {
|
||||
uidargs, _ := c.xnumSetConditionUIDs(true, false, isUID, nums)
|
||||
return uidargs
|
||||
}
|
||||
|
||||
func (c *conn) xnumSetConditionUIDs(forDB, returnUIDs bool, isUID bool, nums numSet) ([]any, []store.UID) {
|
||||
if nums.searchResult {
|
||||
// Update previously stored UIDs. Some may have been deleted.
|
||||
// Once deleted a UID will never come back, so we'll just remove those uids.
|
||||
o := 0
|
||||
for _, uid := range c.searchResult {
|
||||
if uidSearch(c.uids, uid) > 0 {
|
||||
c.searchResult[o] = uid
|
||||
o++
|
||||
// newCachedLastUID returns a method that returns the highest uid for a mailbox,
|
||||
// for interpretation of "*". If mailboxID is for the selected mailbox, the UIDs
|
||||
// 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]
|
||||
}
|
||||
}
|
||||
c.searchResult = c.searchResult[:o]
|
||||
uidargs := make([]any, len(c.searchResult))
|
||||
for i, uid := range c.searchResult {
|
||||
uidargs[i] = uid
|
||||
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)
|
||||
}
|
||||
return uidargs, c.searchResult
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
var uidargs []any
|
||||
var uids []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 {
|
||||
// UIDs that do not exist can be ignored.
|
||||
if c.exists == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
add := func(uid store.UID) {
|
||||
if forDB {
|
||||
uidargs = append(uidargs, uid)
|
||||
}
|
||||
if returnUIDs {
|
||||
uids = append(uids, uid)
|
||||
// Update previously stored UIDs. Some may have been deleted.
|
||||
// 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
|
||||
for _, uid := range c.searchResult {
|
||||
if uidSearch(c.uids, uid) > 0 {
|
||||
c.searchResult[o] = uid
|
||||
o++
|
||||
}
|
||||
}
|
||||
c.searchResult = c.searchResult[:o]
|
||||
}
|
||||
return c.searchResult
|
||||
}
|
||||
|
||||
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
|
||||
for _, r := range nums.ranges {
|
||||
var ia, ib int
|
||||
if r.first.star {
|
||||
if len(c.uids) == 0 {
|
||||
if c.exists == 0 {
|
||||
xsyntaxErrorf("invalid seqset * on empty mailbox")
|
||||
}
|
||||
ia = len(c.uids) - 1
|
||||
ia = int(c.exists) - 1
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
}
|
||||
if r.last == nil {
|
||||
add(c.uids[ia])
|
||||
uids[c.uids[ia]] = struct{}{}
|
||||
continue
|
||||
}
|
||||
|
||||
if r.last.star {
|
||||
if len(c.uids) == 0 {
|
||||
xsyntaxErrorf("invalid seqset * on empty mailbox")
|
||||
}
|
||||
ib = len(c.uids) - 1
|
||||
ib = int(c.exists) - 1
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -1556,15 +1643,39 @@ func (c *conn) xnumSetConditionUIDs(forDB, returnUIDs bool, isUID bool, nums num
|
|||
ia, ib = ib, ia
|
||||
}
|
||||
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.
|
||||
if len(c.uids) == 0 {
|
||||
return nil, nil
|
||||
if c.exists == 0 {
|
||||
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 {
|
||||
|
@ -1575,12 +1686,12 @@ func (c *conn) xnumSetConditionUIDs(forDB, returnUIDs bool, isUID bool, nums num
|
|||
|
||||
uida := store.UID(r.first.number)
|
||||
if r.first.star {
|
||||
uida = c.uids[len(c.uids)-1]
|
||||
uida = c.uids[c.exists-1]
|
||||
}
|
||||
|
||||
uidb := store.UID(last.number)
|
||||
if last.star {
|
||||
uidb = c.uids[len(c.uids)-1]
|
||||
uidb = c.uids[c.exists-1]
|
||||
}
|
||||
|
||||
if uida > uidb {
|
||||
|
@ -1589,7 +1700,7 @@ func (c *conn) xnumSetConditionUIDs(forDB, returnUIDs bool, isUID bool, nums num
|
|||
|
||||
// Binary search for uida.
|
||||
s := 0
|
||||
e := len(c.uids)
|
||||
e := int(c.exists)
|
||||
for s < e {
|
||||
m := (s + e) / 2
|
||||
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:] {
|
||||
if uid >= uida && uid <= uidb {
|
||||
add(uid)
|
||||
uids[uid] = struct{}{}
|
||||
} else if uid > uidb {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return uidargs, uids
|
||||
return slices.Sorted(maps.Keys(uids))
|
||||
}
|
||||
|
||||
func (c *conn) ok(tag, cmd string) {
|
||||
|
@ -1751,8 +1861,7 @@ func (c *conn) xapplyChanges(overflow bool, changes []store.Change, initial, sen
|
|||
if !ok {
|
||||
break
|
||||
}
|
||||
seq := c.sequence(ch.UID)
|
||||
if seq > 0 && initial {
|
||||
if initial && !c.uidonly && c.sequence(ch.UID) > 0 {
|
||||
continue
|
||||
}
|
||||
c.uidAppend(ch.UID)
|
||||
|
@ -1765,15 +1874,19 @@ 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
|
||||
// long enough after the EXISTS to see these messages, and doesn't request them
|
||||
// again with a FETCH.
|
||||
c.xbwritelinef("* %d EXISTS", len(c.uids))
|
||||
c.xbwritelinef("* %d EXISTS", c.exists)
|
||||
for _, add := range adds {
|
||||
seq := c.xsequence(add.UID)
|
||||
var modseqStr string
|
||||
if condstore {
|
||||
modseqStr = fmt.Sprintf(" MODSEQ (%d)", add.ModSeq.Client())
|
||||
}
|
||||
|
||||
c.xbwritelinef("* %d FETCH (UID %d FLAGS %s%s)", seq, add.UID, flaglist(add.Flags, add.Keywords).pack(c), modseqStr)
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
@ -1785,6 +1898,15 @@ func (c *conn) xapplyChanges(overflow bool, changes []store.Change, initial, sen
|
|||
case store.ChangeRemoveUIDs:
|
||||
var vanishedUIDs numSet
|
||||
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
|
||||
if initial {
|
||||
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
|
||||
for _, s := range vanishedUIDs.Strings(4*1024 - 32) {
|
||||
c.xbwritelinef("* VANISHED %s", s)
|
||||
|
@ -1811,15 +1933,21 @@ func (c *conn) xapplyChanges(overflow bool, changes []store.Change, initial, sen
|
|||
}
|
||||
|
||||
case store.ChangeFlags:
|
||||
// The uid can be unknown if we just expunged it while another session marked it as deleted just before.
|
||||
seq := c.sequence(ch.UID)
|
||||
if seq <= 0 {
|
||||
if initial {
|
||||
continue
|
||||
}
|
||||
if !initial {
|
||||
var modseqStr string
|
||||
if condstore {
|
||||
modseqStr = fmt.Sprintf(" MODSEQ (%d)", ch.ModSeq.Client())
|
||||
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.
|
||||
seq := c.sequence(ch.UID)
|
||||
if seq <= 0 {
|
||||
continue
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
seq := c.uidAppend(ch.UID)
|
||||
c.uidAppend(ch.UID)
|
||||
|
||||
// ../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 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
|
||||
// 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
|
||||
}
|
||||
|
||||
|
@ -1995,9 +2128,15 @@ func (c *conn) xapplyChangesNotify(changes []store.Change, sendDelayed bool) {
|
|||
// developer sees the message.
|
||||
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())
|
||||
// Always add UID, also for uidonly, to ensure a non-empty list.
|
||||
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() {
|
||||
defer c.xtracewrite(mlog.LevelTracedata)()
|
||||
data.xwriteTo(cmd.conn, cmd.conn.xbw)
|
||||
|
@ -2050,6 +2189,12 @@ func (c *conn) xapplyChangesNotify(changes []store.Change, sendDelayed bool) {
|
|||
|
||||
var vanishedUIDs numSet
|
||||
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)
|
||||
c.sequenceRemove(seq, uid)
|
||||
|
@ -2059,7 +2204,7 @@ func (c *conn) xapplyChangesNotify(changes []store.Change, sendDelayed bool) {
|
|||
c.xbwritelinef("* %d EXPUNGE", seq)
|
||||
}
|
||||
}
|
||||
if qresync {
|
||||
if !vanishedUIDs.empty() {
|
||||
// VANISHED without EARLIER. ../rfc/7162:2004
|
||||
for _, s := range vanishedUIDs.Strings(4*1024 - 32) {
|
||||
c.xbwritelinef("* VANISHED %s", s)
|
||||
|
@ -2092,9 +2237,12 @@ 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.
|
||||
seq := c.sequence(ch.UID)
|
||||
if seq <= 0 {
|
||||
continue
|
||||
var seq msgseq
|
||||
if !c.uidonly {
|
||||
seq = c.sequence(ch.UID)
|
||||
if seq <= 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
var modseqStr string
|
||||
|
@ -2102,7 +2250,12 @@ func (c *conn) xapplyChangesNotify(changes []store.Change, sendDelayed bool) {
|
|||
modseqStr = fmt.Sprintf(" MODSEQ (%d)", ch.ModSeq.Client())
|
||||
}
|
||||
// UID and FLAGS are required. ../rfc/5465:463
|
||||
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)", seq, ch.UID, flaglist(ch.Flags, ch.Keywords).pack(c), modseqStr)
|
||||
}
|
||||
|
||||
case store.ChangeThread:
|
||||
continue
|
||||
|
@ -2950,6 +3103,11 @@ func (c *conn) cmdEnable(tag, cmd string, p *parser) {
|
|||
case capMetadata:
|
||||
c.enabled[cap] = true
|
||||
enabled += " " + s
|
||||
case capUIDOnly:
|
||||
c.enabled[cap] = true
|
||||
enabled += " " + s
|
||||
c.uidonly = true
|
||||
c.uids = nil
|
||||
}
|
||||
}
|
||||
// QRESYNC enabled CONDSTORE too ../rfc/7162:1391
|
||||
|
@ -3075,6 +3233,11 @@ func (c *conn) cmdSelectExamine(isselect bool, tag, cmd string, p *parser) {
|
|||
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)
|
||||
|
||||
var highestModSeq store.ModSeq
|
||||
|
@ -3085,24 +3248,27 @@ func (c *conn) cmdSelectExamine(isselect bool, tag, cmd string, p *parser) {
|
|||
c.xdbread(func(tx *bstore.Tx) {
|
||||
mb = c.xmailbox(tx, name, "")
|
||||
|
||||
q := bstore.QueryTx[store.Message](tx)
|
||||
q.FilterNonzero(store.Message{MailboxID: mb.ID})
|
||||
q.FilterEqual("Expunged", false)
|
||||
q.SortAsc("UID")
|
||||
c.uids = []store.UID{}
|
||||
var seq msgseq = 1
|
||||
err := q.ForEach(func(m store.Message) error {
|
||||
c.uids = append(c.uids, m.UID)
|
||||
if firstUnseen == 0 && !m.Seen {
|
||||
firstUnseen = seq
|
||||
}
|
||||
seq++
|
||||
return nil
|
||||
})
|
||||
if sanityChecks {
|
||||
checkUIDs(c.uids)
|
||||
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.FilterNonzero(store.Message{MailboxID: mb.ID})
|
||||
q.FilterEqual("Expunged", false)
|
||||
q.SortAsc("UID")
|
||||
err := q.ForEach(func(m store.Message) error {
|
||||
c.uids = append(c.uids, m.UID)
|
||||
if firstUnseen == 0 && !m.Seen {
|
||||
firstUnseen = msgseq(len(c.uids))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
xcheckf(err, "fetching uids")
|
||||
|
||||
c.exists = uint32(len(c.uids))
|
||||
}
|
||||
xcheckf(err, "fetching uids")
|
||||
|
||||
// Condstore extension, find the highest modseq.
|
||||
if c.enabled[capCondstore] {
|
||||
|
@ -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
|
||||
// maintain synchronization.
|
||||
if c.enabled[capQresync] {
|
||||
var err error
|
||||
highDeletedModSeq, err = c.account.HighestDeletedModSeq(tx)
|
||||
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] {
|
||||
c.xbwritelinef(`* 0 RECENT`)
|
||||
}
|
||||
c.xbwritelinef(`* %d EXISTS`, len(c.uids))
|
||||
c.xbwritelinef(`* %d EXISTS`, c.exists)
|
||||
if !c.enabled[capIMAP4rev2] && firstUnseen > 0 {
|
||||
// ../rfc/9051:8051 ../rfc/3501:1774
|
||||
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")
|
||||
}
|
||||
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 {
|
||||
// We will check this old client UID for consistency below.
|
||||
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.
|
||||
q.FilterGreater("ModSeq", store.ModSeqFromClient(qrmodseq))
|
||||
q.FilterLessEqual("ModSeq", highestModSeq)
|
||||
q.FilterLess("UID", c.uidnext)
|
||||
q.SortAsc("ModSeq")
|
||||
err := q.ForEach(func(m store.Message) error {
|
||||
if m.Expunged && m.UID < preVanished {
|
||||
|
@ -3236,38 +3405,72 @@ func (c *conn) cmdSelectExamine(isselect bool, tag, cmd string, p *parser) {
|
|||
vanishedUIDs[m.UID] = struct{}{}
|
||||
return nil
|
||||
}
|
||||
msgseq := c.sequence(m.UID)
|
||||
if msgseq > 0 {
|
||||
// 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 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())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
xcheckf(err, "listing changed messages")
|
||||
})
|
||||
|
||||
// Add UIDs from client's known UID set to vanished list if we don't have enough history.
|
||||
if qrmodseq < highDeletedModSeq.Client() {
|
||||
// If no known uid set was in the request, we substitute 1:max or the empty set.
|
||||
// ../rfc/7162:1524
|
||||
if qrknownUIDs == nil {
|
||||
if len(c.uids) > 0 {
|
||||
qrknownUIDs = &numSet{ranges: []numRange{{first: setNumber{number: 1}, last: &setNumber{number: uint32(c.uids[len(c.uids)-1])}}}}
|
||||
// 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 no "known uid set" was in the request, we substitute 1:max or the empty set.
|
||||
// ../rfc/7162:1524
|
||||
if qrknownUIDs == nil {
|
||||
qrknownUIDs = &numSet{ranges: []numRange{{first: setNumber{number: 1}, last: &setNumber{number: uint32(c.uidnext - 1)}}}}
|
||||
}
|
||||
|
||||
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 {
|
||||
qrknownUIDs = &numSet{}
|
||||
// Ensure it is in ascending order, no needless first/last ranges. qrknownUIDs cannot contain a star.
|
||||
iter := qrknownUIDs.newIter()
|
||||
for {
|
||||
v, ok := iter.Next()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if c.sequence(store.UID(v)) <= 0 {
|
||||
vanishedUIDs[store.UID(v)] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
iter := qrknownUIDs.newIter()
|
||||
for {
|
||||
v, ok := iter.Next()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if c.sequence(store.UID(v)) <= 0 {
|
||||
vanishedUIDs[store.UID(v)] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Now that we have all vanished UIDs, send them over compactly.
|
||||
if len(vanishedUIDs) > 0 {
|
||||
|
@ -4044,7 +4247,7 @@ func (c *conn) cmdAppend(tag, cmd string, p *parser) {
|
|||
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.
|
||||
c.xbwritelinef("* %d EXISTS", len(c.uids))
|
||||
c.xbwritelinef("* %d EXISTS", c.exists)
|
||||
}
|
||||
|
||||
// ../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")
|
||||
|
||||
xlastUID := c.newCachedLastUID(tx, c.mailboxID, func(err error) { xuserErrorf("%s", err) })
|
||||
|
||||
qm := bstore.QueryTx[store.Message](tx)
|
||||
qm.FilterNonzero(store.Message{MailboxID: c.mailboxID})
|
||||
qm.FilterEqual("Deleted", true)
|
||||
qm.FilterEqual("Expunged", false)
|
||||
qm.FilterLess("UID", c.uidnext)
|
||||
qm.FilterFn(func(m store.Message) bool {
|
||||
// Only remove if this session knows about the message and if present in optional uidSet.
|
||||
return uidSearch(c.uids, m.UID) > 0 && (uidSet == nil || uidSet.containsUID(m.UID, c.uids, c.searchResult))
|
||||
// Only remove if this session knows about the message and if present in optional
|
||||
// uidSet.
|
||||
return uidSet == nil || uidSet.xcontainsKnownUID(m.UID, c.searchResult, xlastUID)
|
||||
})
|
||||
qm.SortAsc("UID")
|
||||
expunged, err = qm.List()
|
||||
|
@ -4363,6 +4570,12 @@ func (c *conn) cmdxExpunge(tag, cmd string, uidSet *numSet) {
|
|||
var vanishedUIDs numSet
|
||||
qresync := c.enabled[capQresync]
|
||||
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)
|
||||
c.sequenceRemove(seq, m.UID)
|
||||
if qresync {
|
||||
|
@ -4445,20 +4658,14 @@ func (c *conn) cmdUIDReplace(tag, cmd string, p *parser) {
|
|||
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
|
||||
// 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
|
||||
// echo whatever the client sends us without reordering, the client can reorder our
|
||||
// response and interpret it differently than we intended.
|
||||
// ../rfc/9051:5072
|
||||
uids := c.xnumSetUIDs(isUID, nums)
|
||||
slices.Sort(uids)
|
||||
uidargs := make([]any, len(uids))
|
||||
for i, uid := range uids {
|
||||
uidargs[i] = uid
|
||||
}
|
||||
return uids, uidargs
|
||||
return c.xnumSetEval(tx, isUID, nums)
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
uids, uidargs := c.gatherCopyMoveUIDs(isUID, nums)
|
||||
|
||||
// Files that were created during the copy. Remove them if the operation fails.
|
||||
var newIDs []int64
|
||||
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 nkeywords int
|
||||
var origUIDs, newUIDs []store.UID
|
||||
var newUIDs []store.UID
|
||||
var flags []store.Flags
|
||||
var keywords [][]string
|
||||
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) {
|
||||
mbSrc := c.xmailboxID(tx, c.mailboxID) // Validate.
|
||||
|
||||
mbDst = c.xmailbox(tx, name, "TRYCREATE")
|
||||
if mbDst.ID == mbSrc.ID {
|
||||
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")
|
||||
}
|
||||
|
||||
|
@ -4522,17 +4733,17 @@ func (c *conn) cmdxCopy(isUID bool, tag, cmd string, p *parser) {
|
|||
|
||||
// Reserve the uids in the destination mailbox.
|
||||
uidFirst := mbDst.UIDNext
|
||||
mbDst.UIDNext += store.UID(len(uidargs))
|
||||
mbDst.UIDNext += store.UID(len(uids))
|
||||
|
||||
// Fetch messages from database.
|
||||
q := bstore.QueryTx[store.Message](tx)
|
||||
q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
|
||||
q.FilterEqual("UID", uidargs...)
|
||||
q.FilterEqual("UID", slicesAny(uids)...)
|
||||
q.FilterEqual("Expunged", false)
|
||||
xmsgs, err := q.List()
|
||||
xcheckf(err, "fetching messages")
|
||||
|
||||
if len(xmsgs) != len(uidargs) {
|
||||
if len(xmsgs) != len(uids) {
|
||||
xserverErrorf("uid and message mismatch")
|
||||
}
|
||||
|
||||
|
@ -4588,7 +4799,6 @@ func (c *conn) cmdxCopy(isUID bool, tag, cmd string, p *parser) {
|
|||
xcheckf(err, "inserting message")
|
||||
msgs[uid] = m
|
||||
nmsgs[i] = m
|
||||
origUIDs = append(origUIDs, uid)
|
||||
newUIDs = append(newUIDs, m.UID)
|
||||
newMsgIDs = append(newMsgIDs, m.ID)
|
||||
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
|
||||
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.
|
||||
|
@ -4688,7 +4898,8 @@ func (c *conn) cmdxMove(isUID bool, tag, cmd string, p *parser) {
|
|||
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 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")
|
||||
}
|
||||
|
||||
uids = c.gatherCopyMoveUIDs(tx, isUID, nums)
|
||||
|
||||
if len(uids) == 0 {
|
||||
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.
|
||||
q := bstore.QueryTx[store.Message](tx)
|
||||
q.FilterNonzero(store.Message{MailboxID: mbSrc.ID})
|
||||
q.FilterEqual("UID", uidargs...)
|
||||
q.FilterEqual("UID", slicesAny(uids)...)
|
||||
q.FilterEqual("Expunged", false)
|
||||
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...)
|
||||
cleanupIDs = newIDs
|
||||
})
|
||||
|
@ -4748,6 +4961,13 @@ func (c *conn) cmdxMove(isUID bool, tag, cmd string, p *parser) {
|
|||
qresync := c.enabled[capQresync]
|
||||
var vanishedUIDs numSet
|
||||
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])
|
||||
c.sequenceRemove(seq, uids[i])
|
||||
if qresync {
|
||||
|
@ -4988,9 +5208,9 @@ func (c *conn) cmdxStore(isUID bool, tag, cmd string, p *parser) {
|
|||
mb = c.xmailboxID(tx, c.mailboxID) // Validate.
|
||||
origmb = mb
|
||||
|
||||
uidargs := c.xnumSetCondition(isUID, nums)
|
||||
uids := c.xnumSetEval(tx, isUID, nums)
|
||||
|
||||
if len(uidargs) == 0 {
|
||||
if len(uids) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -5005,7 +5225,7 @@ func (c *conn) cmdxStore(isUID bool, tag, cmd string, p *parser) {
|
|||
|
||||
q := bstore.QueryTx[store.Message](tx)
|
||||
q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
|
||||
q.FilterEqual("UID", uidargs...)
|
||||
q.FilterEqual("UID", slicesAny(uids)...)
|
||||
q.FilterEqual("Expunged", false)
|
||||
err := q.ForEach(func(m store.Message) error {
|
||||
// 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
|
||||
if !silent || c.enabled[capCondstore] {
|
||||
for _, m := range updated {
|
||||
var flags string
|
||||
var args []string
|
||||
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] {
|
||||
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
|
||||
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
|
||||
var mnums []store.UID
|
||||
for _, m := range changed {
|
||||
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())
|
||||
// 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())
|
||||
}
|
||||
if isUID {
|
||||
mnums = append(mnums, m.UID)
|
||||
} else {
|
||||
|
|
|
@ -41,6 +41,10 @@ func init() {
|
|||
mox.Context = ctxbg
|
||||
}
|
||||
|
||||
func ptr[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
||||
func tocrlf(s string) string {
|
||||
return strings.ReplaceAll(s, "\n", "\r\n")
|
||||
}
|
||||
|
@ -170,6 +174,7 @@ type testconn struct {
|
|||
t *testing.T
|
||||
conn net.Conn
|
||||
client *imapclient.Conn
|
||||
uidonly bool
|
||||
done chan struct{}
|
||||
serverConn net.Conn
|
||||
account *store.Account
|
||||
|
@ -250,7 +255,7 @@ func tuntagged(t *testing.T, got imapclient.Untagged, dst any) {
|
|||
gotv := reflect.ValueOf(got)
|
||||
dstv := reflect.ValueOf(dst)
|
||||
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)
|
||||
}
|
||||
|
@ -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() {
|
||||
tc.close0(true)
|
||||
}
|
||||
|
@ -338,7 +370,7 @@ func (tc *testconn) closeNoWait() {
|
|||
func (tc *testconn) close0(waitclose bool) {
|
||||
defer func() {
|
||||
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
|
||||
|
||||
func start(t *testing.T) *testconn {
|
||||
return startArgs(t, true, false, true, true, "mjl")
|
||||
func start(t *testing.T, uidonly bool) *testconn {
|
||||
return startArgs(t, uidonly, true, false, true, true, "mjl")
|
||||
}
|
||||
|
||||
func startNoSwitchboard(t *testing.T) *testconn {
|
||||
return startArgs(t, false, false, true, false, "mjl")
|
||||
func startNoSwitchboard(t *testing.T, uidonly bool) *testconn {
|
||||
return startArgs(t, uidonly, false, false, true, false, "mjl")
|
||||
}
|
||||
|
||||
const password0 = "te\u0301st \u00a0\u2002\u200a" // NFD and various unicode spaces.
|
||||
const password1 = "tést " // PRECIS normalized, with NFC.
|
||||
|
||||
func startArgs(t *testing.T, first, immediateTLS bool, allowLoginWithoutTLS, setPassword bool, accname string) *testconn {
|
||||
return startArgsMore(t, first, immediateTLS, nil, nil, allowLoginWithoutTLS, setPassword, accname, nil)
|
||||
func startArgs(t *testing.T, uidonly, first, immediateTLS bool, allowLoginWithoutTLS, setPassword bool, accname string) *testconn {
|
||||
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.
|
||||
|
@ -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.
|
||||
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.
|
||||
|
||||
switchStop := func() {}
|
||||
|
@ -506,7 +538,7 @@ func startArgsMore(t *testing.T, first, immediateTLS bool, serverConfig, clientC
|
|||
}()
|
||||
client, err := imapclient.New(connCounter, clientConn, true)
|
||||
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 {
|
||||
tc.switchStop = switchStop
|
||||
}
|
||||
|
@ -542,7 +574,7 @@ func fakeCert(t *testing.T, randomkey bool) tls.Certificate {
|
|||
}
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
tc := start(t)
|
||||
tc := start(t, false)
|
||||
defer tc.close()
|
||||
|
||||
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.close()
|
||||
|
||||
tc = start(t)
|
||||
tc = start(t, false)
|
||||
tc.transactf("ok", `login "mjl@mox.example" "%s"`, password0)
|
||||
tc.close()
|
||||
|
||||
tc = start(t)
|
||||
tc = start(t, false)
|
||||
tc.transactf("ok", `login "\"\"@mox.example" "%s"`, password0)
|
||||
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.
|
||||
func TestState(t *testing.T) {
|
||||
tc := start(t)
|
||||
tc := start(t, false)
|
||||
|
||||
notAuthenticated := []string{"starttls", "authenticate", "login"}
|
||||
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", "logout")
|
||||
tc.close()
|
||||
tc = start(t)
|
||||
tc = start(t, false)
|
||||
defer tc.close()
|
||||
|
||||
// Not authenticated, lots of commands not allowed.
|
||||
|
@ -599,7 +631,7 @@ func TestState(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNonIMAP(t *testing.T) {
|
||||
tc := start(t)
|
||||
tc := start(t, false)
|
||||
defer tc.close()
|
||||
|
||||
// imap greeting has already been read, we sidestep the imapclient.
|
||||
|
@ -612,10 +644,10 @@ func TestNonIMAP(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLiterals(t *testing.T) {
|
||||
tc := start(t)
|
||||
tc := start(t, false)
|
||||
defer tc.close()
|
||||
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
tc.client.Create("tmpbox", nil)
|
||||
|
||||
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.
|
||||
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()
|
||||
tc.transactf("ok", `login mjl@mox.example "%s"`, password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
|
||||
tc.transactf("bad", " missingcommand")
|
||||
|
||||
|
@ -682,6 +722,44 @@ func TestScenario(t *testing.T) {
|
|||
tc.check(err, "write message")
|
||||
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 body")
|
||||
tc.transactf("ok", "fetch 1 binary[]")
|
||||
|
@ -714,9 +792,9 @@ func TestScenario(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMailbox(t *testing.T) {
|
||||
tc := start(t)
|
||||
tc := start(t, false)
|
||||
defer tc.close()
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
|
||||
invalid := []string{
|
||||
"e\u0301", // é but as e + acute, not unicode-normalized
|
||||
|
@ -736,14 +814,14 @@ func TestMailbox(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMailboxDeleted(t *testing.T) {
|
||||
tc := start(t)
|
||||
tc := start(t, false)
|
||||
defer tc.close()
|
||||
|
||||
tc2 := startNoSwitchboard(t)
|
||||
tc2 := startNoSwitchboard(t, false)
|
||||
defer tc2.closeNoWait()
|
||||
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc2.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
tc2.login("mjl@mox.example", password0)
|
||||
|
||||
tc.client.Create("testbox", nil)
|
||||
tc2.client.Select("testbox")
|
||||
|
@ -759,9 +837,9 @@ func TestMailboxDeleted(t *testing.T) {
|
|||
tc2.transactf("no", "uid fetch 1 all")
|
||||
tc2.transactf("no", "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("bad", "move 1 inbox") // msgseq 1 not available.
|
||||
tc2.transactf("no", "move 1 inbox")
|
||||
tc2.transactf("no", "uid move 1 inbox")
|
||||
|
||||
tc2.transactf("ok", "unselect")
|
||||
|
@ -773,9 +851,9 @@ func TestMailboxDeleted(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestID(t *testing.T) {
|
||||
tc := start(t)
|
||||
tc := start(t, false)
|
||||
defer tc.close()
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
|
||||
tc.transactf("ok", "id nil")
|
||||
tc.xuntagged(imapclient.UntaggedID{"name": "mox", "version": moxvar.Version})
|
||||
|
@ -787,54 +865,84 @@ func TestID(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()
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
tc.client.Select("inbox")
|
||||
|
||||
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("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.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.
|
||||
tc.xuntagged(
|
||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1)}},
|
||||
imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2)}},
|
||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1)}},
|
||||
)
|
||||
tc.transactf("ok", "uid search return (save) all") // Empty result.
|
||||
tc.transactf("ok", "uid fetch $ uid")
|
||||
tc.xuntagged()
|
||||
|
||||
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.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2)}})
|
||||
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||
if !uidonly {
|
||||
tc.transactf("ok", "fetch 2:1,1 uid") // We reorder 2:1 to 1:2, and we deduplicate numbers.
|
||||
tc.xuntagged(
|
||||
tc.untaggedFetch(1, 1),
|
||||
tc.untaggedFetch(2, 2),
|
||||
)
|
||||
|
||||
tc.transactf("bad", "fetch 1:3 all")
|
||||
}
|
||||
|
||||
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
|
||||
// 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.
|
||||
func DisabledTestReference(t *testing.T) {
|
||||
tc := start(t)
|
||||
tc := start(t, false)
|
||||
defer tc.close()
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
tc.client.Select("inbox")
|
||||
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||
|
||||
tc2 := startNoSwitchboard(t)
|
||||
tc2 := startNoSwitchboard(t, false)
|
||||
defer tc2.close()
|
||||
tc2.client.Login("mjl@mox.example", password0)
|
||||
tc2.login("mjl@mox.example", password0)
|
||||
tc2.client.Select("inbox")
|
||||
|
||||
tc.client.StoreFlagsSet("1", true, `\Deleted`)
|
||||
tc.client.Expunge()
|
||||
|
||||
tc3 := startNoSwitchboard(t)
|
||||
tc3 := startNoSwitchboard(t, false)
|
||||
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.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")
|
||||
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) {
|
||||
tc := start(t)
|
||||
tc := start(t, false)
|
||||
tc.client.Starttls(&tls.Config{InsecureSkipVerify: true})
|
||||
tc.transactf("bad", "starttls") // TLS already active.
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
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.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.xcode("PRIVACYREQUIRED")
|
||||
tc.transactf("no", "authenticate PLAIN %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000"+password0)))
|
||||
tc.xcode("PRIVACYREQUIRED")
|
||||
tc.client.Starttls(&tls.Config{InsecureSkipVerify: true})
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
tc.close()
|
||||
}
|
||||
|
|
|
@ -7,11 +7,19 @@ import (
|
|||
)
|
||||
|
||||
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()()
|
||||
tc := start(t)
|
||||
tc := start(t, uidonly)
|
||||
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 inbox") // Missing param.
|
||||
|
@ -53,7 +61,7 @@ func TestStatus(t *testing.T) {
|
|||
})
|
||||
|
||||
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.xuntagged(imapclient.UntaggedStatus{
|
||||
Mailbox: "Inbox",
|
||||
|
|
|
@ -8,63 +8,74 @@ import (
|
|||
)
|
||||
|
||||
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()
|
||||
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
tc.client.Enable("imap4rev2")
|
||||
|
||||
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||
tc.client.Select("inbox")
|
||||
|
||||
uid1 := imapclient.FetchUID(1)
|
||||
noflags := imapclient.FetchFlags(nil)
|
||||
|
||||
tc.transactf("ok", "store 1 flags.silent ()")
|
||||
if !uidonly {
|
||||
tc.transactf("ok", "store 1 flags.silent ()")
|
||||
tc.xuntagged()
|
||||
}
|
||||
|
||||
tc.transactf("ok", `uid store 1 flags ()`)
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, noflags))
|
||||
tc.transactf("ok", `uid fetch 1 flags`)
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, noflags))
|
||||
|
||||
tc.transactf("ok", `uid store 1 flags.silent (\Seen)`)
|
||||
tc.xuntagged()
|
||||
tc.transactf("ok", `uid fetch 1 flags`)
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{`\Seen`}))
|
||||
|
||||
tc.transactf("ok", `store 1 flags ()`)
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, noflags}})
|
||||
tc.transactf("ok", `fetch 1 flags`)
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, noflags}})
|
||||
tc.transactf("ok", `uid store 1 flags ($Junk)`)
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{`$Junk`}))
|
||||
tc.transactf("ok", `uid fetch 1 flags`)
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{`$Junk`}))
|
||||
|
||||
tc.transactf("ok", `store 1 flags.silent (\Seen)`)
|
||||
tc.xuntagged()
|
||||
tc.transactf("ok", `fetch 1 flags`)
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, imapclient.FetchFlags{`\Seen`}}})
|
||||
tc.transactf("ok", `uid store 1 +flags ()`)
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{`$Junk`}))
|
||||
tc.transactf("ok", `uid store 1 +flags (\Deleted)`)
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{`\Deleted`, `$Junk`}))
|
||||
tc.transactf("ok", `uid fetch 1 flags`)
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{`\Deleted`, `$Junk`}))
|
||||
|
||||
tc.transactf("ok", `store 1 flags ($Junk)`)
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, imapclient.FetchFlags{`$Junk`}}})
|
||||
tc.transactf("ok", `fetch 1 flags`)
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, imapclient.FetchFlags{`$Junk`}}})
|
||||
tc.transactf("ok", `uid store 1 -flags \Deleted $Junk`)
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, noflags))
|
||||
tc.transactf("ok", `uid fetch 1 flags`)
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, noflags))
|
||||
|
||||
tc.transactf("ok", `store 1 +flags ()`)
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, imapclient.FetchFlags{`$Junk`}}})
|
||||
tc.transactf("ok", `store 1 +flags (\Deleted)`)
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, imapclient.FetchFlags{`\Deleted`, `$Junk`}}})
|
||||
tc.transactf("ok", `fetch 1 flags`)
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, imapclient.FetchFlags{`\Deleted`, `$Junk`}}})
|
||||
|
||||
tc.transactf("ok", `store 1 -flags \Deleted $Junk`)
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, noflags}})
|
||||
tc.transactf("ok", `fetch 1 flags`)
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, noflags}})
|
||||
|
||||
tc.transactf("bad", "store 2 flags ()") // ../rfc/9051:7018
|
||||
if !uidonly {
|
||||
tc.transactf("bad", "store 2 flags ()") // ../rfc/9051:7018
|
||||
}
|
||||
|
||||
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.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, imapclient.FetchFlags{"new"}}})
|
||||
tc.transactf("ok", "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.transactf("ok", "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.transactf("ok", "store 1 -flags (new new e a c)")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, imapclient.FetchFlags{"b", "d"}}})
|
||||
tc.transactf("ok", "store 1 flags ($Forwarded Different)")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, imapclient.FetchFlags{"$Forwarded", "different"}}})
|
||||
tc.transactf("ok", "uid store 1 flags (new)") // New flag.
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{"new"}))
|
||||
tc.transactf("ok", "uid store 1 flags (new new a b c)") // Duplicates are ignored.
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{"a", "b", "c", "new"}))
|
||||
tc.transactf("ok", "uid store 1 +flags (new new c d e)")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{"a", "b", "c", "d", "e", "new"}))
|
||||
tc.transactf("ok", "uid store 1 -flags (new new e a c)")
|
||||
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{"b", "d"}))
|
||||
tc.transactf("ok", "uid store 1 flags ($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 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`, " ")
|
||||
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) {
|
||||
tc := start(t)
|
||||
tc := start(t, false)
|
||||
defer tc.close()
|
||||
|
||||
tc2 := startNoSwitchboard(t)
|
||||
tc2 := startNoSwitchboard(t, false)
|
||||
defer tc2.closeNoWait()
|
||||
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc2.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
tc2.login("mjl@mox.example", password0)
|
||||
|
||||
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) {
|
||||
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()
|
||||
|
||||
tc.client.Login("mjl@mox.example", password0)
|
||||
tc.login("mjl@mox.example", password0)
|
||||
tc.client.Select("inbox")
|
||||
|
||||
tc.transactf("bad", "unselect bogus") // Leftover data.
|
||||
|
@ -19,7 +27,7 @@ func TestUnselect(t *testing.T) {
|
|||
|
||||
tc.client.Select("inbox")
|
||||
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", "status inbox (messages)")
|
||||
tc.xuntagged(imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 1}}) // Message not removed.
|
||||
|
|
|
@ -2,13 +2,19 @@ package imapserver
|
|||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mjl-/mox/imapclient"
|
||||
)
|
||||
|
||||
func TestUnsubscribe(t *testing.T) {
|
||||
tc := start(t)
|
||||
tc := start(t, false)
|
||||
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.
|
||||
|
@ -17,9 +23,16 @@ func TestUnsubscribe(t *testing.T) {
|
|||
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("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")
|
||||
tc2.transactf("ok", "noop")
|
||||
tc.transactf("ok", "unsubscribe a/b")
|
||||
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", "unsubscribe a/b")
|
||||
}
|
||||
|
|
|
@ -242,7 +242,7 @@ https://www.iana.org/assignments/message-headers/message-headers.xhtml
|
|||
9208 Partial - IMAP QUOTA Extension
|
||||
9394 Roadmap - IMAP PARTIAL Extension for Paged SEARCH and FETCH
|
||||
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
|
||||
9698 ? - The JMAPACCESS Extension for IMAP
|
||||
9738 No - IMAP MESSAGELIMIT Extension
|
||||
|
|
Loading…
Reference in a new issue