1package imapserver
2
3import (
4 "fmt"
5 "testing"
6 "time"
7
8 "github.com/mjl-/mox/imapclient"
9)
10
11func TestMetadata(t *testing.T) {
12 testMetadata(t, false)
13}
14
15func TestMetadataUIDOnly(t *testing.T) {
16 testMetadata(t, true)
17}
18
19func testMetadata(t *testing.T, uidonly bool) {
20 tc := start(t, uidonly)
21 defer tc.close()
22
23 tc.login("mjl@mox.example", password0)
24
25 tc.transactf("ok", `getmetadata "" /private/comment`)
26 tc.xuntagged()
27
28 tc.transactf("ok", `getmetadata inbox (/private/comment)`)
29 tc.xuntagged()
30
31 tc.transactf("ok", `setmetadata "" (/PRIVATE/COMMENT "global value")`)
32 tc.transactf("ok", `setmetadata inbox (/private/comment "mailbox value")`)
33
34 tc.transactf("ok", `create metabox`)
35 tc.transactf("ok", `setmetadata metabox (/private/comment "mailbox value")`)
36 tc.transactf("ok", `setmetadata metabox (/shared/comment "mailbox value")`)
37 tc.transactf("ok", `setmetadata metabox (/shared/comment nil)`) // Remove.
38 tc.transactf("ok", `delete metabox`) // Delete mailbox with live and expunged metadata.
39
40 tc.transactf("no", `setmetadata expungebox (/private/comment "mailbox value")`)
41 tc.xcodeWord("TRYCREATE")
42
43 tc.transactf("ok", `getmetadata "" ("/private/comment")`)
44 tc.xuntagged(imapclient.UntaggedMetadataAnnotations{
45 Mailbox: "",
46 Annotations: []imapclient.Annotation{
47 {Key: "/private/comment", IsString: true, Value: []byte("global value")},
48 },
49 })
50
51 tc.transactf("ok", `setmetadata Inbox (/shared/comment "share")`)
52
53 tc.transactf("ok", `getmetadata inbox (/private/comment /private/unknown /shared/comment)`)
54 tc.xuntagged(imapclient.UntaggedMetadataAnnotations{
55 Mailbox: "Inbox",
56 Annotations: []imapclient.Annotation{
57 {Key: "/private/comment", IsString: true, Value: []byte("mailbox value")},
58 {Key: "/shared/comment", IsString: true, Value: []byte("share")},
59 },
60 })
61
62 tc.transactf("no", `setmetadata doesnotexist (/private/comment "test")`) // Bad mailbox.
63 tc.transactf("no", `setmetadata Inbox (/badprefix/comment "")`)
64 tc.transactf("no", `setmetadata Inbox (/private/vendor "")`) // /*/vendor must have more components.
65 tc.transactf("no", `setmetadata Inbox (/private/vendor/stillbad "")`) // /*/vendor must have more components.
66 tc.transactf("ok", `setmetadata Inbox (/private/vendor/a/b "")`)
67 tc.transactf("bad", `setmetadata Inbox (/private/no* "")`)
68 tc.transactf("bad", `setmetadata Inbox (/private/no%% "")`)
69 tc.transactf("bad", `setmetadata Inbox (/private/notrailingslash/ "")`)
70 tc.transactf("bad", `setmetadata Inbox (/private//nodupslash "")`)
71 tc.transactf("bad", "setmetadata Inbox (/private/\001 \"\")")
72 tc.transactf("bad", "setmetadata Inbox (/private/\u007f \"\")")
73 tc.transactf("bad", `getmetadata (depth 0 depth 0) inbox (/private/a)`) // Duplicate option.
74 tc.transactf("bad", `getmetadata (depth badvalue) inbox (/private/a)`)
75 tc.transactf("bad", `getmetadata (maxsize invalid) inbox (/private/a)`)
76 tc.transactf("bad", `getmetadata (badoption) inbox (/private/a)`)
77
78 // Update existing annotation by key.
79 tc.transactf("ok", `setmetadata "" (/PRIVATE/COMMENT "global updated")`)
80 tc.transactf("ok", `setmetadata inbox (/private/comment "mailbox updated")`)
81 tc.transactf("ok", `getmetadata "" (/private/comment)`)
82 tc.xuntagged(imapclient.UntaggedMetadataAnnotations{
83 Mailbox: "",
84 Annotations: []imapclient.Annotation{
85 {Key: "/private/comment", IsString: true, Value: []byte("global updated")},
86 },
87 })
88 tc.transactf("ok", `getmetadata inbox (/private/comment)`)
89 tc.xuntagged(imapclient.UntaggedMetadataAnnotations{
90 Mailbox: "Inbox",
91 Annotations: []imapclient.Annotation{
92 {Key: "/private/comment", IsString: true, Value: []byte("mailbox updated")},
93 },
94 })
95
96 // Delete annotation with nil value.
97 tc.transactf("ok", `setmetadata "" (/private/comment nil)`)
98 tc.transactf("ok", `setmetadata inbox (/private/comment nil)`)
99 tc.transactf("ok", `getmetadata "" (/private/comment)`)
100 tc.xuntagged()
101 tc.transactf("ok", `getmetadata inbox (/private/comment)`)
102 tc.xuntagged()
103
104 // Create a literal8 value, not a string.
105 tc.transactf("ok", "setmetadata inbox (/private/comment ~{4+}\r\ntest)")
106 tc.transactf("ok", `getmetadata inbox (/private/comment)`)
107 tc.xuntagged(imapclient.UntaggedMetadataAnnotations{
108 Mailbox: "Inbox",
109 Annotations: []imapclient.Annotation{
110 {Key: "/private/comment", IsString: false, Value: []byte("test")},
111 },
112 })
113
114 // Request with a maximum size, we don't get anything larger.
115 tc.transactf("ok", `setmetadata inbox (/private/another "longer")`)
116 tc.transactf("ok", `getmetadata (maxsize 4) inbox (/private/comment /private/another)`)
117 tc.xcode(imapclient.CodeMetadataLongEntries(6))
118 tc.xuntagged(imapclient.UntaggedMetadataAnnotations{
119 Mailbox: "Inbox",
120 Annotations: []imapclient.Annotation{
121 {Key: "/private/comment", IsString: false, Value: []byte("test")},
122 },
123 })
124
125 // Request with various depth values.
126 tc.transactf("ok", `setmetadata inbox (/private/a "x" /private/a/b "x" /private/a/b/c "x" /private/a/b/c/d "x")`)
127 tc.transactf("ok", `getmetadata (depth 0) inbox (/private/a)`)
128 tc.xuntagged(imapclient.UntaggedMetadataAnnotations{
129 Mailbox: "Inbox",
130 Annotations: []imapclient.Annotation{
131 {Key: "/private/a", IsString: true, Value: []byte("x")},
132 },
133 })
134 tc.transactf("ok", `getmetadata (depth 1) inbox (/private/a)`)
135 tc.xuntagged(imapclient.UntaggedMetadataAnnotations{
136 Mailbox: "Inbox",
137 Annotations: []imapclient.Annotation{
138 {Key: "/private/a", IsString: true, Value: []byte("x")},
139 {Key: "/private/a/b", IsString: true, Value: []byte("x")},
140 },
141 })
142 tc.transactf("ok", `getmetadata (depth infinity) inbox (/private/a)`)
143 tc.xuntagged(imapclient.UntaggedMetadataAnnotations{
144 Mailbox: "Inbox",
145 Annotations: []imapclient.Annotation{
146 {Key: "/private/a", IsString: true, Value: []byte("x")},
147 {Key: "/private/a/b", IsString: true, Value: []byte("x")},
148 {Key: "/private/a/b/c", IsString: true, Value: []byte("x")},
149 {Key: "/private/a/b/c/d", IsString: true, Value: []byte("x")},
150 },
151 })
152 // Same as previous, but ask for everything below /.
153 tc.transactf("ok", `getmetadata (depth infinity) inbox ("")`)
154 tc.xuntagged(imapclient.UntaggedMetadataAnnotations{
155 Mailbox: "Inbox",
156 Annotations: []imapclient.Annotation{
157 {Key: "/private/a", IsString: true, Value: []byte("x")},
158 {Key: "/private/a/b", IsString: true, Value: []byte("x")},
159 {Key: "/private/a/b/c", IsString: true, Value: []byte("x")},
160 {Key: "/private/a/b/c/d", IsString: true, Value: []byte("x")},
161 {Key: "/private/another", IsString: true, Value: []byte("longer")},
162 {Key: "/private/comment", IsString: false, Value: []byte("test")},
163 {Key: "/private/vendor/a/b", IsString: true, Value: []byte("")},
164 {Key: "/shared/comment", IsString: true, Value: []byte("share")},
165 },
166 })
167
168 // Deleting a mailbox with an annotation should work and annotations should not
169 // come back when recreating mailbox.
170 tc.transactf("ok", "create testbox")
171 tc.transactf("ok", `setmetadata testbox (/private/a "x")`)
172 tc.transactf("ok", "delete testbox")
173 tc.transactf("ok", "create testbox")
174 tc.transactf("ok", `getmetadata testbox (/private/a)`)
175 tc.xuntagged()
176
177 // When renaming mailbox, annotations must be copied to destination mailbox.
178 tc.transactf("ok", "rename inbox newbox")
179 tc.transactf("ok", `getmetadata newbox (/private/a)`)
180 tc.xuntagged(imapclient.UntaggedMetadataAnnotations{
181 Mailbox: "newbox",
182 Annotations: []imapclient.Annotation{
183 {Key: "/private/a", IsString: true, Value: []byte("x")},
184 },
185 })
186 tc.transactf("ok", `getmetadata inbox (/private/a)`)
187 tc.xuntagged(imapclient.UntaggedMetadataAnnotations{
188 Mailbox: "Inbox",
189 Annotations: []imapclient.Annotation{
190 {Key: "/private/a", IsString: true, Value: []byte("x")},
191 },
192 })
193
194 // Broadcast should not happen when metadata capability is not enabled.
195 tc2 := startNoSwitchboard(t, uidonly)
196 defer tc2.closeNoWait()
197 tc2.login("mjl@mox.example", password0)
198 tc2.client.Select("inbox")
199
200 tc2.cmdf("", "idle")
201 tc2.readprefixline("+ ")
202 done := make(chan error)
203 go func() {
204 defer func() {
205 x := recover()
206 if x != nil {
207 done <- fmt.Errorf("%v", x)
208 }
209 }()
210 untagged, _ := tc2.client.ReadUntagged()
211 var exists imapclient.UntaggedExists
212 tuntagged(tc2.t, untagged, &exists)
213 tc2.writelinef("done")
214 tc2.response("ok")
215 done <- nil
216 }()
217
218 // Should not cause idle to return.
219 tc.transactf("ok", `setmetadata inbox (/private/a "y")`)
220 // Cause to return.
221 tc.transactf("ok", "append inbox {4+}\r\ntest")
222
223 timer := time.NewTimer(time.Second)
224 defer timer.Stop()
225 select {
226 case err := <-done:
227 tc.check(err, "idle")
228 case <-timer.C:
229 t.Fatalf("idle did not finish")
230 }
231
232 // Broadcast should happen when metadata capability is enabled.
233 tc2.client.Enable(imapclient.CapMetadata)
234 tc2.cmdf("", "idle")
235 tc2.readprefixline("+ ")
236 done = make(chan error)
237 go func() {
238 defer func() {
239 x := recover()
240 if x != nil {
241 done <- fmt.Errorf("%v", x)
242 }
243 }()
244 untagged, _ := tc2.client.ReadUntagged()
245 var metadataKeys imapclient.UntaggedMetadataKeys
246 tuntagged(tc2.t, untagged, &metadataKeys)
247 tc2.writelinef("done")
248 tc2.response("ok")
249 done <- nil
250 }()
251
252 // Should cause idle to return.
253 tc.transactf("ok", `setmetadata inbox (/private/a "z")`)
254
255 timer = time.NewTimer(time.Second)
256 defer timer.Stop()
257 select {
258 case err := <-done:
259 tc.check(err, "idle")
260 case <-timer.C:
261 t.Fatalf("idle did not finish")
262 }
263}
264
265func TestMetadataLimit(t *testing.T) {
266 tc := start(t, false)
267 defer tc.close()
268
269 tc.login("mjl@mox.example", password0)
270
271 maxKeys, maxSize := metadataMaxKeys, metadataMaxSize
272 defer func() {
273 metadataMaxKeys = maxKeys
274 metadataMaxSize = maxSize
275 }()
276 metadataMaxKeys = 10
277 metadataMaxSize = 1000
278
279 // Reach max total size limit.
280 buf := make([]byte, metadataMaxSize+1)
281 for i := range buf {
282 buf[i] = 'x'
283 }
284 tc.cmdf("", "setmetadata inbox (/private/large ~{%d+}", len(buf))
285 tc.client.Write(buf)
286 tc.client.Writelinef(")")
287 tc.response("no")
288 tc.xcode(imapclient.CodeMetadataMaxSize(metadataMaxSize))
289
290 // Reach limit for max number.
291 for i := 1; i <= metadataMaxKeys; i++ {
292 tc.transactf("ok", `setmetadata inbox (/private/key%d "test")`, i)
293 }
294 tc.transactf("no", `setmetadata inbox (/private/toomany "test")`)
295 tc.xcode(imapclient.CodeMetadataTooMany{})
296}
297