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