1package imapserver
2
3import (
4 "testing"
5
6 "github.com/mjl-/mox/imapclient"
7 "github.com/mjl-/mox/store"
8)
9
10func TestListBasic(t *testing.T) {
11 tc := start(t)
12 defer tc.close()
13
14 tc.client.Login("mjl@mox.example", password0)
15
16 ulist := func(name string, flags ...string) imapclient.UntaggedList {
17 if len(flags) == 0 {
18 flags = nil
19 }
20 return imapclient.UntaggedList{Flags: flags, Separator: '/', Mailbox: name}
21 }
22
23 tc.last(tc.client.List("INBOX"))
24 tc.xuntagged(ulist("Inbox"))
25
26 tc.last(tc.client.List("Inbox"))
27 tc.xuntagged(ulist("Inbox"))
28
29 tc.last(tc.client.List("expungebox"))
30 tc.xuntagged()
31
32 tc.last(tc.client.List("%"))
33 tc.xuntagged(ulist("Archive", `\Archive`), ulist("Drafts", `\Drafts`), ulist("Inbox"), ulist("Junk", `\Junk`), ulist("Sent", `\Sent`), ulist("Trash", `\Trash`))
34
35 tc.last(tc.client.List("*"))
36 tc.xuntagged(ulist("Archive", `\Archive`), ulist("Drafts", `\Drafts`), ulist("Inbox"), ulist("Junk", `\Junk`), ulist("Sent", `\Sent`), ulist("Trash", `\Trash`))
37
38 tc.last(tc.client.List("A*"))
39 tc.xuntagged(ulist("Archive", `\Archive`))
40
41 tc.client.Create("Inbox/todo", nil)
42
43 tc.last(tc.client.List("Inbox*"))
44 tc.xuntagged(ulist("Inbox"), ulist("Inbox/todo"))
45
46 tc.last(tc.client.List("Inbox/%"))
47 tc.xuntagged(ulist("Inbox/todo"))
48
49 tc.last(tc.client.List("Inbox/*"))
50 tc.xuntagged(ulist("Inbox/todo"))
51
52 // Leading full INBOX is turned into Inbox, so mailbox matches.
53 tc.last(tc.client.List("INBOX/*"))
54 tc.xuntagged(ulist("Inbox/todo"))
55
56 // No match because we are only touching various casings of the full "INBOX".
57 tc.last(tc.client.List("INBO*"))
58 tc.xuntagged()
59}
60
61func TestListExtended(t *testing.T) {
62 defer mockUIDValidity()()
63
64 tc := start(t)
65 defer tc.close()
66
67 tc.client.Login("mjl@mox.example", password0)
68
69 ulist := func(name string, flags ...string) imapclient.UntaggedList {
70 if len(flags) == 0 {
71 flags = nil
72 }
73 return imapclient.UntaggedList{Flags: flags, Separator: '/', Mailbox: name}
74 }
75
76 uidvals := map[string]uint32{}
77 use := store.DefaultInitialMailboxes.SpecialUse
78 for _, name := range []string{"Inbox", use.Archive, use.Draft, use.Junk, use.Sent, use.Trash} {
79 uidvals[name] = 1
80 }
81 for _, name := range store.DefaultInitialMailboxes.Regular {
82 uidvals[name] = 1
83 }
84 var uidvalnext uint32 = 3
85 uidval := func(name string) uint32 {
86 v, ok := uidvals[name]
87 if !ok {
88 v = uidvalnext
89 uidvals[name] = v
90 uidvalnext++
91 }
92 return v
93 }
94
95 ustatus := func(name string) imapclient.UntaggedStatus {
96 attrs := map[imapclient.StatusAttr]int64{
97 imapclient.StatusMessages: 0,
98 imapclient.StatusUIDNext: 1,
99 imapclient.StatusUIDValidity: int64(uidval(name)),
100 imapclient.StatusUnseen: 0,
101 imapclient.StatusDeleted: 0,
102 imapclient.StatusSize: 0,
103 imapclient.StatusRecent: 0,
104 imapclient.StatusAppendLimit: 0,
105 }
106 return imapclient.UntaggedStatus{Mailbox: name, Attrs: attrs}
107 }
108
109 const (
110 Fsubscribed = `\Subscribed`
111 Fhaschildren = `\HasChildren`
112 Fhasnochildren = `\HasNoChildren`
113 Fnonexistent = `\NonExistent`
114 Farchive = `\Archive`
115 Fdraft = `\Drafts`
116 Fjunk = `\Junk`
117 Fsent = `\Sent`
118 Ftrash = `\Trash`
119 )
120
121 // untaggedlist with flags subscribed and hasnochildren
122 xlist := func(name string, flags ...string) imapclient.UntaggedList {
123 flags = append([]string{Fhasnochildren, Fsubscribed}, flags...)
124 return ulist(name, flags...)
125 }
126
127 xchildlist := func(name string, flags ...string) imapclient.UntaggedList {
128 u := ulist(name, flags...)
129 comp := imapclient.TaggedExtComp{String: "SUBSCRIBED"}
130 u.Extended = []imapclient.MboxListExtendedItem{{Tag: "CHILDINFO", Val: imapclient.TaggedExtVal{Comp: &comp}}}
131 return u
132 }
133
134 tc.last(tc.client.ListFull(false, "INBOX"))
135 tc.xuntagged(xlist("Inbox"), ustatus("Inbox"))
136
137 tc.last(tc.client.ListFull(false, "Inbox"))
138 tc.xuntagged(xlist("Inbox"), ustatus("Inbox"))
139
140 tc.last(tc.client.ListFull(false, "%"))
141 tc.xuntagged(xlist("Archive", Farchive), ustatus("Archive"), xlist("Drafts", Fdraft), ustatus("Drafts"), xlist("Inbox"), ustatus("Inbox"), xlist("Junk", Fjunk), ustatus("Junk"), xlist("Sent", Fsent), ustatus("Sent"), xlist("Trash", Ftrash), ustatus("Trash"))
142
143 tc.last(tc.client.ListFull(false, "*"))
144 tc.xuntagged(xlist("Archive", Farchive), ustatus("Archive"), xlist("Drafts", Fdraft), ustatus("Drafts"), xlist("Inbox"), ustatus("Inbox"), xlist("Junk", Fjunk), ustatus("Junk"), xlist("Sent", Fsent), ustatus("Sent"), xlist("Trash", Ftrash), ustatus("Trash"))
145
146 tc.last(tc.client.ListFull(false, "A*"))
147 tc.xuntagged(xlist("Archive", Farchive), ustatus("Archive"))
148
149 tc.last(tc.client.ListFull(false, "A*", "Junk"))
150 tc.xuntagged(xlist("Archive", Farchive), ustatus("Archive"), xlist("Junk", Fjunk), ustatus("Junk"))
151
152 tc.client.Create("Inbox/todo", nil)
153
154 tc.last(tc.client.ListFull(false, "Inbox*"))
155 tc.xuntagged(ulist("Inbox", Fhaschildren, Fsubscribed), ustatus("Inbox"), xlist("Inbox/todo"), ustatus("Inbox/todo"))
156
157 tc.last(tc.client.ListFull(false, "Inbox/%"))
158 tc.xuntagged(xlist("Inbox/todo"), ustatus("Inbox/todo"))
159
160 tc.last(tc.client.ListFull(false, "Inbox/*"))
161 tc.xuntagged(xlist("Inbox/todo"), ustatus("Inbox/todo"))
162
163 // Leading full INBOX is turned into Inbox, so mailbox matches.
164 tc.last(tc.client.ListFull(false, "INBOX/*"))
165 tc.xuntagged(xlist("Inbox/todo"), ustatus("Inbox/todo"))
166
167 // No match because we are only touching various casings of the full "INBOX".
168 tc.last(tc.client.ListFull(false, "INBO*"))
169 tc.xuntagged()
170
171 tc.last(tc.client.ListFull(true, "Inbox"))
172 tc.xuntagged(xchildlist("Inbox", Fsubscribed, Fhaschildren), ustatus("Inbox"))
173
174 tc.client.Unsubscribe("Inbox")
175 tc.last(tc.client.ListFull(true, "Inbox"))
176 tc.xuntagged(xchildlist("Inbox", Fhaschildren), ustatus("Inbox"))
177
178 tc.client.Delete("Inbox/todo") // Still subscribed.
179 tc.last(tc.client.ListFull(true, "Inbox"))
180 tc.xuntagged(xchildlist("Inbox", Fhasnochildren), ustatus("Inbox"))
181
182 // Simple extended list without RETURN options.
183 tc.transactf("ok", `list "" ("inbox")`)
184 tc.xuntagged(ulist("Inbox"))
185
186 tc.transactf("ok", `list () "" ("inbox") return ()`)
187 tc.xuntagged(ulist("Inbox"))
188
189 tc.transactf("ok", `list "" ("inbox") return ()`)
190 tc.xuntagged(ulist("Inbox"))
191
192 tc.transactf("ok", `list () "" ("inbox")`)
193 tc.xuntagged(ulist("Inbox"))
194
195 tc.transactf("ok", `list (remote) "" ("inbox")`)
196 tc.xuntagged(ulist("Inbox"))
197
198 tc.transactf("ok", `list (remote) "" "/inbox"`)
199 tc.xuntagged()
200
201 tc.transactf("ok", `list (remote) "/inbox" ""`)
202 tc.xuntagged()
203
204 tc.transactf("ok", `list (remote) "inbox" ""`)
205 tc.xuntagged()
206
207 tc.transactf("ok", `list (remote) "inbox" "a"`)
208 tc.xuntagged()
209
210 tc.client.Create("inbox/a", nil)
211 tc.transactf("ok", `list (remote) "inbox" "a"`)
212 tc.xuntagged(ulist("Inbox/a"))
213
214 tc.client.Subscribe("x")
215 tc.transactf("ok", `list (subscribed) "" x return (subscribed)`)
216 tc.xuntagged(imapclient.UntaggedList{Flags: []string{`\Subscribed`, `\NonExistent`}, Separator: '/', Mailbox: "x"})
217
218 tc.transactf("bad", `list (recursivematch) "" "*"`) // Cannot have recursivematch without a base selection option like subscribed.
219 tc.transactf("bad", `list (recursivematch remote) "" "*"`) // "remote" is not a base selection option.
220 tc.transactf("bad", `list (unknown) "" "*"`) // Unknown selection options must result in BAD.
221 tc.transactf("bad", `list () "" "*" return (unknown)`) // Unknown return options must result in BAD.
222
223 // Return metadata.
224 tc.transactf("ok", `setmetadata inbox (/private/comment "y")`)
225 tc.transactf("ok", `list () "" ("inbox") return (metadata (/private/comment /shared/comment))`)
226 tc.xuntagged(
227 ulist("Inbox"),
228 imapclient.UntaggedMetadataAnnotations{
229 Mailbox: "Inbox",
230 Annotations: []imapclient.Annotation{
231 {Key: "/private/comment", IsString: true, Value: []byte("y")},
232 {Key: "/shared/comment"},
233 },
234 },
235 )
236
237 tc.transactf("bad", `list () "" ("inbox") return (metadata ())`) // Metadata list must be non-empty.
238 tc.transactf("bad", `list () "" ("inbox") return (metadata (/shared/comment "/private/comment" ))`) // Extra space.
239}
240