mirror of
https://github.com/mjl-/mox.git
synced 2025-06-28 02:28:15 +03:00
830 lines
28 KiB
Go
830 lines
28 KiB
Go
package imapserver
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/mjl-/mox/imapclient"
|
|
)
|
|
|
|
var searchMsg = strings.ReplaceAll(`Date: Mon, 1 Jan 2022 10:00:00 +0100 (CEST)
|
|
From: mjl <mjl@mox.example>
|
|
Subject: mox
|
|
To: mox <mox@mox.example>
|
|
Cc: <xcc@mox.example>
|
|
Bcc: <bcc@mox.example>
|
|
Reply-To: <noreply@mox.example>
|
|
Message-Id: <123@mox.example>
|
|
MIME-Version: 1.0
|
|
Content-Type: multipart/alternative; boundary=x
|
|
|
|
--x
|
|
Content-Type: text/plain; charset=utf-8
|
|
|
|
this is plain text.
|
|
|
|
--x
|
|
Content-Type: text/html; charset=utf-8
|
|
|
|
this is html.
|
|
|
|
--x--
|
|
`, "\n", "\r\n")
|
|
|
|
func uint32ptr(v uint32) *uint32 {
|
|
return &v
|
|
}
|
|
|
|
func (tc *testconn) xsearch(nums ...uint32) {
|
|
tc.t.Helper()
|
|
|
|
tc.xuntagged(imapclient.UntaggedSearch(nums))
|
|
}
|
|
|
|
func (tc *testconn) xsearchmodseq(modseq int64, nums ...uint32) {
|
|
tc.t.Helper()
|
|
|
|
if len(nums) == 0 {
|
|
tc.xnountagged()
|
|
return
|
|
}
|
|
tc.xuntagged(imapclient.UntaggedSearchModSeq{Nums: nums, ModSeq: modseq})
|
|
}
|
|
|
|
func (tc *testconn) xesearch(exp imapclient.UntaggedEsearch) {
|
|
tc.t.Helper()
|
|
|
|
exp.Tag = tc.client.LastTag
|
|
tc.xuntagged(exp)
|
|
}
|
|
|
|
func TestSearch(t *testing.T) {
|
|
tc := start(t)
|
|
defer tc.close()
|
|
tc.client.Login("mjl@mox.example", password0)
|
|
tc.client.Select("inbox")
|
|
|
|
// Add 5 and delete first 4 messages. So UIDs start at 5.
|
|
received := time.Date(2020, time.January, 1, 10, 0, 0, 0, time.UTC)
|
|
saveDate := time.Now()
|
|
for range 5 {
|
|
tc.client.Append("inbox", makeAppendTime(exampleMsg, received))
|
|
}
|
|
tc.client.StoreFlagsSet("1:4", true, `\Deleted`)
|
|
tc.client.Expunge()
|
|
|
|
received = time.Date(2022, time.January, 1, 9, 0, 0, 0, time.UTC)
|
|
tc.client.Append("inbox", makeAppendTime(searchMsg, received))
|
|
|
|
received = time.Date(2022, time.January, 1, 9, 0, 0, 0, time.UTC)
|
|
mostFlags := []string{
|
|
`\Deleted`,
|
|
`\Seen`,
|
|
`\Answered`,
|
|
`\Flagged`,
|
|
`\Draft`,
|
|
`$Forwarded`,
|
|
`$Junk`,
|
|
`$Notjunk`,
|
|
`$Phishing`,
|
|
`$MDNSent`,
|
|
`custom1`,
|
|
`Custom2`,
|
|
}
|
|
tc.client.Append("inbox", imapclient.Append{Flags: mostFlags, Received: &received, Size: int64(len(searchMsg)), Data: strings.NewReader(searchMsg)})
|
|
|
|
// 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")
|
|
|
|
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.
|
|
|
|
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", "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")})
|
|
|
|
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"))
|
|
|
|
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 (all) $")
|
|
tc.xesearch(esearchall("2"))
|
|
|
|
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 (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)}})
|
|
|
|
// 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"))
|
|
|
|
// Long commands should be rejected, not allocating too much memory.
|
|
lit := make([]byte, 100*1024+1)
|
|
for i := range lit {
|
|
lit[i] = 'x'
|
|
}
|
|
writeTextLit := func(n int, expok bool) {
|
|
_, err := fmt.Fprintf(tc.client, " TEXT ")
|
|
tcheck(t, err, "write text")
|
|
|
|
_, err = fmt.Fprintf(tc.client, "{%d}\r\n", n)
|
|
tcheck(t, err, "write literal size")
|
|
line, err := tc.client.Readline()
|
|
tcheck(t, err, "read line")
|
|
if expok && !strings.HasPrefix(line, "+") {
|
|
tcheck(t, fmt.Errorf("no continuation after writing size: %s", line), "sending literal")
|
|
} else if !expok && !strings.HasPrefix(line, "x0 BAD [TOOBIG]") {
|
|
tcheck(t, fmt.Errorf("got line %s", line), "expected TOOBIG error")
|
|
}
|
|
if !expok {
|
|
return
|
|
}
|
|
_, err = tc.client.Write(lit[:n])
|
|
tcheck(t, err, "write literal data")
|
|
}
|
|
|
|
// More than 100k for a literal.
|
|
_, err := fmt.Fprintf(tc.client, "x0 uid search")
|
|
tcheck(t, err, "write start of uit search")
|
|
writeTextLit(100*1024+1, false)
|
|
|
|
// More than 1mb total for literals.
|
|
_, err = fmt.Fprintf(tc.client, "x0 uid search")
|
|
tcheck(t, err, "write start of uit search")
|
|
for range 10 {
|
|
writeTextLit(100*1024, true)
|
|
}
|
|
writeTextLit(1, false)
|
|
|
|
// More than 1000 literals.
|
|
_, err = fmt.Fprintf(tc.client, "x0 uid search")
|
|
tcheck(t, err, "write start of uit search")
|
|
for range 1000 {
|
|
writeTextLit(1, true)
|
|
}
|
|
writeTextLit(1, false)
|
|
}
|
|
|
|
// esearchall makes an UntaggedEsearch response with All set, for comparisons.
|
|
func esearchall0(ss string) imapclient.NumSet {
|
|
seqset := imapclient.NumSet{}
|
|
for _, rs := range strings.Split(ss, ",") {
|
|
t := strings.Split(rs, ":")
|
|
if len(t) > 2 {
|
|
panic("bad seqset")
|
|
}
|
|
var first uint32
|
|
var last *uint32
|
|
if t[0] != "*" {
|
|
v, err := strconv.ParseUint(t[0], 10, 32)
|
|
if err != nil {
|
|
panic("parse first")
|
|
}
|
|
first = uint32(v)
|
|
}
|
|
if len(t) == 2 {
|
|
if t[1] != "*" {
|
|
v, err := strconv.ParseUint(t[1], 10, 32)
|
|
if err != nil {
|
|
panic("parse last")
|
|
}
|
|
u := uint32(v)
|
|
last = &u
|
|
}
|
|
}
|
|
seqset.Ranges = append(seqset.Ranges, imapclient.NumRange{First: first, Last: last})
|
|
}
|
|
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)
|
|
}
|
|
|
|
// Run multisearch tests with or without a mailbox selected.
|
|
func testSearchMulti(t *testing.T, selected bool) {
|
|
defer mockUIDValidity()()
|
|
|
|
tc := start(t)
|
|
defer tc.close()
|
|
tc.client.Login("mjl@mox.example", password0)
|
|
tc.client.Select("inbox")
|
|
|
|
// Add 5 messages to Inbox and delete first 4 messages. So UIDs start at 5.
|
|
received := time.Date(2020, time.January, 1, 10, 0, 0, 0, time.UTC)
|
|
for range 6 {
|
|
tc.client.Append("inbox", makeAppendTime(exampleMsg, received))
|
|
}
|
|
tc.client.StoreFlagsSet("1:4", true, `\Deleted`)
|
|
tc.client.Expunge()
|
|
|
|
// Unselecting mailbox, esearch works in authenticated state.
|
|
if !selected {
|
|
tc.client.Unselect()
|
|
}
|
|
|
|
received = time.Date(2022, time.January, 1, 9, 0, 0, 0, time.UTC)
|
|
tc.client.Append("inbox", makeAppendTime(searchMsg, received))
|
|
|
|
received = time.Date(2022, time.January, 1, 9, 0, 0, 0, time.UTC)
|
|
mostFlags := []string{
|
|
`\Deleted`,
|
|
`\Seen`,
|
|
`\Answered`,
|
|
`\Flagged`,
|
|
`\Draft`,
|
|
`$Forwarded`,
|
|
`$Junk`,
|
|
`$Notjunk`,
|
|
`$Phishing`,
|
|
`$MDNSent`,
|
|
`custom1`,
|
|
`Custom2`,
|
|
}
|
|
tc.client.Append("Archive", imapclient.Append{Flags: mostFlags, Received: &received, Size: int64(len(searchMsg)), Data: strings.NewReader(searchMsg)})
|
|
|
|
// We now have sequence numbers 1,2,3 and UIDs 5,6,7 in Inbox, and UID 1 in Archive.
|
|
|
|
// Basic esearch with mailboxes.
|
|
tc.cmdf("Tag1", `Esearch In (Personal) Return () All`)
|
|
tc.response("ok")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, All: esearchall0("5:7")},
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Archive", UIDValidity: 1, UID: true, All: esearchall0("1")},
|
|
)
|
|
|
|
// Again, but with progress information.
|
|
orig := inProgressPeriod
|
|
inProgressPeriod = 0
|
|
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.cmdf("Tag1", `Esearch In (Personal) Return () All`)
|
|
tc.response("ok")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, All: esearchall0("5:7")},
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Archive", UIDValidity: 1, UID: true, All: esearchall0("1")},
|
|
inprogress(0, 4),
|
|
inprogress(1, 4),
|
|
inprogress(2, 4),
|
|
inprogress(3, 4),
|
|
)
|
|
inProgressPeriod = orig
|
|
|
|
// Explicit mailboxes listed, including non-existent one that is ignored,
|
|
// duplicates are ignored as well.
|
|
tc.cmdf("Tag1", `Esearch In (Mailboxes (INBOX Archive Archive)) Return (Min Max Count All) All`)
|
|
tc.response("ok")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Min: 5, Max: 7, Count: uint32ptr(3), All: esearchall0("5:7")},
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Archive", UIDValidity: 1, UID: true, Min: 1, Max: 1, Count: uint32ptr(1), All: esearchall0("1")},
|
|
)
|
|
|
|
// No response if none of the mailboxes exist.
|
|
tc.cmdf("Tag1", `Esearch In (Mailboxes bogus Mailboxes (nonexistent)) Return (Min Max Count All) All`)
|
|
tc.response("ok")
|
|
tc.xuntagged()
|
|
|
|
// Inboxes evaluates to just inbox on new account. We'll add more mailboxes
|
|
// matching "inboxes" later on.
|
|
tc.cmdf("Tag1", `Esearch In (Inboxes) Return () All`)
|
|
tc.response("ok")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, All: esearchall0("5:7")},
|
|
)
|
|
|
|
// Subscribed is set for created mailboxes by default.
|
|
tc.cmdf("Tag1", `Esearch In (Subscribed) Return (Max) All`)
|
|
tc.response("ok")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Max: 7},
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Archive", UIDValidity: 1, UID: true, Max: 1},
|
|
)
|
|
|
|
// Asking for max does a reverse search.
|
|
tc.cmdf("Tag1", `Esearch In (Personal) Return (Max) All`)
|
|
tc.response("ok")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Max: 7},
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Archive", UIDValidity: 1, UID: true, Max: 1},
|
|
)
|
|
|
|
// Min stops early.
|
|
tc.cmdf("Tag1", `Esearch In (Personal) Return (Min) All`)
|
|
tc.response("ok")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Min: 5},
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Archive", UIDValidity: 1, UID: true, Min: 1},
|
|
)
|
|
|
|
// Min and max do forward and reverse search, stopping early.
|
|
tc.cmdf("Tag1", `Esearch In (Personal) Return (Min Max) All`)
|
|
tc.response("ok")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Min: 5, Max: 7},
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Archive", UIDValidity: 1, UID: true, Min: 1, Max: 1},
|
|
)
|
|
|
|
if selected {
|
|
// With only 1 inbox, we can use SAVE with Inboxes. Can't anymore when we have multiple.
|
|
tc.transactf("ok", `Esearch In (Inboxes) Return (Save) All`)
|
|
tc.xuntagged()
|
|
|
|
// Using search result ($) works with selected mailbox.
|
|
tc.cmdf("Tag1", `Esearch In (Selected) Return () $`)
|
|
tc.response("ok")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, All: esearchall0("5:7")},
|
|
)
|
|
} else {
|
|
// Cannot use "selected" if we are not in selected state.
|
|
tc.transactf("bad", `Esearch In (Selected) Return () All`)
|
|
}
|
|
|
|
// Add more "inboxes", and other mailboxes for testing "subtree" and "subtree-one".
|
|
more := []string{
|
|
"Inbox/Sub1",
|
|
"Inbox/Sub2",
|
|
"Inbox/Sub2/SubA",
|
|
"Inbox/Sub2/SubB",
|
|
"Other",
|
|
"Other/Sub1", // sub1@mox.example in config.
|
|
"Other/Sub2",
|
|
"Other/Sub2/SubA", // ruleset for sub2@mox.example in config.
|
|
"Other/Sub2/SubB",
|
|
"List", // ruleset for a mailing list
|
|
}
|
|
for _, name := range more {
|
|
tc.client.Create(name, nil)
|
|
tc.client.Append(name, makeAppendTime(exampleMsg, received))
|
|
}
|
|
|
|
// Cannot use SAVE with multiple mailboxes that match.
|
|
tc.transactf("bad", `Esearch In (Inboxes) Return (Save) All`)
|
|
|
|
// "inboxes" includes everything below Inbox, and also anything that we might
|
|
// deliver to based on account addresses and rulesets, but not mailing lists.
|
|
tc.cmdf("Tag1", `Esearch In (Inboxes) Return () All`)
|
|
tc.response("ok")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, All: esearchall0("5:7")},
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox/Sub1", UIDValidity: 3, UID: true, All: esearchall0("1")},
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox/Sub2", UIDValidity: 4, UID: true, All: esearchall0("1")},
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox/Sub2/SubA", UIDValidity: 5, UID: true, All: esearchall0("1")},
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox/Sub2/SubB", UIDValidity: 6, UID: true, All: esearchall0("1")},
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Other/Sub1", UIDValidity: 8, UID: true, All: esearchall0("1")},
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Other/Sub2/SubA", UIDValidity: 10, UID: true, All: esearchall0("1")},
|
|
)
|
|
|
|
// subtree
|
|
tc.cmdf("Tag1", `Esearch In (Subtree Other) Return () All`)
|
|
tc.response("ok")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Other", UIDValidity: 7, UID: true, All: esearchall0("1")},
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Other/Sub1", UIDValidity: 8, UID: true, All: esearchall0("1")},
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Other/Sub2", UIDValidity: 9, UID: true, All: esearchall0("1")},
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Other/Sub2/SubA", UIDValidity: 10, UID: true, All: esearchall0("1")},
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Other/Sub2/SubB", UIDValidity: 11, UID: true, All: esearchall0("1")},
|
|
)
|
|
|
|
// subtree-one
|
|
tc.cmdf("Tag1", `Esearch In (Subtree-One Other) Return () All`)
|
|
tc.response("ok")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Other", UIDValidity: 7, UID: true, All: esearchall0("1")},
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Other/Sub1", UIDValidity: 8, UID: true, All: esearchall0("1")},
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Other/Sub2", UIDValidity: 9, UID: true, All: esearchall0("1")},
|
|
)
|
|
|
|
// 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},
|
|
)
|
|
|
|
// Search with uid set with "$highnum:*" forces getting highest uid.
|
|
tc.cmdf("Tag1", `Esearch In (Mailboxes Inbox) Return (Min Max) Uid *:100`)
|
|
tc.response("ok")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Min: 7, Max: 7},
|
|
)
|
|
tc.cmdf("Tag1", `Esearch In (Mailboxes Inbox) Return (Min Max) Uid 100:*`)
|
|
tc.response("ok")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Min: 7, Max: 7},
|
|
)
|
|
tc.cmdf("Tag1", `Esearch In (Mailboxes Inbox) Return (Min Max) Uid 1:*`)
|
|
tc.response("ok")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Min: 5, Max: 7},
|
|
)
|
|
|
|
// We use another session to add a new message to Inbox and to Archive. Searching
|
|
// 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)
|
|
defer tc2.closeNoWait()
|
|
tc2.client.Login("mjl@mox.example", password0)
|
|
tc2.client.Append("inbox", makeAppendTime(searchMsg, received))
|
|
tc2.client.Append("Archive", makeAppendTime(searchMsg, received))
|
|
|
|
tc.cmdf("Tag1", `Esearch In (Mailboxes (Inbox Archive)) Return (Count) All`)
|
|
tc.response("ok")
|
|
if selected {
|
|
tc.xuntagged(
|
|
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)}},
|
|
)
|
|
} else {
|
|
tc.xuntagged(
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Count: uint32ptr(4)},
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Archive", UIDValidity: 1, UID: true, Count: uint32ptr(2)},
|
|
)
|
|
}
|
|
|
|
if selected {
|
|
// Saving a search result, and then using it with another mailbox results in error.
|
|
tc.transactf("ok", `Esearch In (Mailboxes Inbox) Return (Save) All`)
|
|
tc.transactf("no", `Esearch In (Mailboxes Archive) Return () $`)
|
|
} else {
|
|
tc.transactf("bad", `Esearch In (Inboxes) Return (Save) All`) // Need a selected mailbox with SAVE.
|
|
tc.transactf("no", `Esearch In (Inboxes) Return () $`) // Cannot use saved result with non-selected mailbox.
|
|
}
|
|
|
|
tc.transactf("bad", `Esearch In () Return () All`) // Missing values for "IN"-list.
|
|
tc.transactf("bad", `Esearch In (Bogus) Return () All`) // Bogus word for "IN".
|
|
tc.transactf("bad", `Esearch In ("Selected") Return () All`) // IN-words can't be quoted.
|
|
tc.transactf("bad", `Esearch In (Selected-Delayed) Return () All`) // From NOTIFY, not in ESEARCH.
|
|
tc.transactf("bad", `Esearch In (Subtree-One) Return () All`) // After subtree-one we need a list.
|
|
tc.transactf("bad", `Esearch In (Subtree-One ) Return () All`) // After subtree-one we need a list.
|
|
tc.transactf("bad", `Esearch In (Subtree-One (Test) ) Return () All`) // Bogus space.
|
|
|
|
if !selected {
|
|
return
|
|
}
|
|
// From now on, we are in selected state.
|
|
|
|
tc.cmdf("Tag1", `Esearch In (Selected) Return () All`)
|
|
tc.response("ok")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, All: esearchall0("5:8")},
|
|
)
|
|
|
|
// Testing combinations of SAVE with MIN/MAX/others ../rfc/9051:4100
|
|
tc.transactf("ok", `Esearch In (Selected) Return (Save) All`)
|
|
tc.xuntagged()
|
|
|
|
tc.cmdf("Tag1", `Esearch In (Selected) Return () $`)
|
|
tc.response("ok")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, All: esearchall0("5:8")},
|
|
)
|
|
|
|
// Inbox happens to be the selected mailbox, so OK.
|
|
tc.cmdf("Tag1", `Esearch In (Mailboxes Inbox) Return () $`)
|
|
tc.response("ok")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, All: esearchall0("5:8")},
|
|
)
|
|
|
|
// Non-selected mailboxes aren't allowed to use the saved result.
|
|
tc.transactf("no", `Esearch In (Mailboxes Archive) Return () $`)
|
|
tc.transactf("no", `Esearch In (Mailboxes Archive) Return () uid $`)
|
|
|
|
tc.cmdf("Tag1", `Esearch In (Selected) Return (Save Min Max) All`)
|
|
tc.response("ok")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Min: 5, Max: 8},
|
|
)
|
|
tc.cmdf("Tag1", `Esearch In (Selected) Return () $`)
|
|
tc.response("ok")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, All: esearchall0("5,8")},
|
|
)
|
|
|
|
tc.cmdf("Tag1", `Esearch In (Selected) Return (Save Min) All`)
|
|
tc.response("ok")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Min: 5},
|
|
)
|
|
|
|
tc.cmdf("Tag1", `Esearch In (Selected) Return () $`)
|
|
tc.response("ok")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, All: esearchall0("5")},
|
|
)
|
|
|
|
tc.cmdf("Tag1", `Esearch In (Selected) Return (Save Max) All`)
|
|
tc.response("ok")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Max: 8},
|
|
)
|
|
|
|
tc.cmdf("Tag1", `Esearch In (Selected) Return () $`)
|
|
tc.response("ok")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, All: esearchall0("8")},
|
|
)
|
|
|
|
tc.cmdf("Tag1", `Esearch In (Selected) Return (Save Min Max Count) All`)
|
|
tc.response("ok")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, Min: 5, Max: 8, Count: uint32ptr(4)},
|
|
)
|
|
|
|
tc.cmdf("Tag1", `Esearch In (Selected) Return () $`)
|
|
tc.response("ok")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedEsearch{Tag: "Tag1", Mailbox: "Inbox", UIDValidity: 1, UID: true, All: esearchall0("5:8")},
|
|
)
|
|
}
|