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