1package imapserver
2
3import (
4 "bytes"
5 "fmt"
6 "strings"
7
8 "github.com/mjl-/bstore"
9
10 "github.com/mjl-/mox/store"
11)
12
13// Changed during tests.
14var metadataMaxKeys = 1000
15var metadataMaxSize = 1000 * 1000
16
17// Metadata errata:
18// ../rfc/5464:183 ../rfc/5464-eid1691
19// ../rfc/5464:564 ../rfc/5464-eid1692
20// ../rfc/5464:494 ../rfc/5464-eid2785 ../rfc/5464-eid2786
21// ../rfc/5464:698 ../rfc/5464-eid3868
22
23// Note: We do not tie the special-use mailbox flags to a (synthetic) private
24// per-mailbox annotation. ../rfc/6154:303
25
26// For registration of names, see https://www.iana.org/assignments/imap-metadata/imap-metadata.xhtml
27
28// Get metadata annotations, per mailbox or globally.
29//
30// State: Authenticated and selected.
31func (c *conn) cmdGetmetadata(tag, cmd string, p *parser) {
32 // Command: ../rfc/5464:412
33
34 // Request syntax: ../rfc/5464:792
35
36 p.xspace()
37 var optMaxSize int64 = -1
38 var optDepth string
39 if p.take("(") {
40 for {
41 if p.take("MAXSIZE") {
42 // ../rfc/5464:804
43 p.xspace()
44 v := p.xnumber()
45 if optMaxSize >= 0 {
46 p.xerrorf("only a single maxsize option accepted")
47 }
48 optMaxSize = int64(v)
49 } else if p.take("DEPTH") {
50 // ../rfc/5464:823
51 p.xspace()
52 s := p.xtakelist("0", "1", "INFINITY")
53 if optDepth != "" {
54 p.xerrorf("only single depth option accepted")
55 }
56 optDepth = s
57 } else {
58 // ../rfc/5464:800 We are not doing anything further parsing for future extensions.
59 p.xerrorf("unknown option for getmetadata, expected maxsize or depth")
60 }
61
62 if p.take(")") {
63 break
64 }
65 p.xspace()
66 }
67 p.xspace()
68 }
69 mailboxName := p.xmailbox()
70 if mailboxName != "" {
71 mailboxName = xcheckmailboxname(mailboxName, true)
72 }
73 p.xspace()
74 // Entries ../rfc/5464:768
75 entryNames := map[string]struct{}{}
76 if p.take("(") {
77 for {
78 s := p.xmetadataKey()
79 entryNames[s] = struct{}{}
80 if p.take(")") {
81 break
82 }
83 p.xtake(" ")
84 }
85 } else {
86 s := p.xmetadataKey()
87 entryNames[s] = struct{}{}
88 }
89 p.xempty()
90
91 var annotations []store.Annotation
92 longentries := -1 // Size of largest value skipped due to optMaxSize. ../rfc/5464:482
93
94 c.account.WithRLock(func() {
95 c.xdbread(func(tx *bstore.Tx) {
96 q := bstore.QueryTx[store.Annotation](tx)
97 if mailboxName == "" {
98 q.FilterEqual("MailboxID", 0)
99 } else {
100 mb := c.xmailbox(tx, mailboxName, "TRYCREATE")
101 q.FilterNonzero(store.Annotation{MailboxID: mb.ID})
102 }
103
104 q.SortAsc("MailboxID", "Key") // For tests.
105 err := q.ForEach(func(a store.Annotation) error {
106 // ../rfc/5464:516
107 switch optDepth {
108 case "", "0":
109 if _, ok := entryNames[a.Key]; !ok {
110 return nil
111 }
112 case "1", "INFINITY":
113 // Go through all keys, matching depth.
114 if _, ok := entryNames[a.Key]; ok {
115 break
116 }
117 var match bool
118 for s := range entryNames {
119 prefix := s
120 if s != "/" {
121 prefix += "/"
122 }
123 if !strings.HasPrefix(a.Key, prefix) {
124 continue
125 }
126 if optDepth == "INFINITY" {
127 match = true
128 break
129 }
130 suffix := a.Key[len(prefix):]
131 t := strings.SplitN(suffix, "/", 2)
132 if len(t) == 1 {
133 match = true
134 break
135 }
136 }
137 if !match {
138 return nil
139 }
140 default:
141 xcheckf(fmt.Errorf("%q", optDepth), "missing case for depth")
142 }
143
144 if optMaxSize >= 0 && int64(len(a.Value)) > optMaxSize {
145 longentries = max(longentries, len(a.Value))
146 } else {
147 annotations = append(annotations, a)
148 }
149 return nil
150 })
151 xcheckf(err, "looking up annotations")
152 })
153 })
154
155 // Response syntax: ../rfc/5464:807 ../rfc/5464:778
156 // We can only send untagged responses when we have any matches.
157 if len(annotations) > 0 {
158 fmt.Fprintf(c.bw, "* METADATA %s (", astring(mailboxName).pack(c))
159 for i, a := range annotations {
160 if i > 0 {
161 fmt.Fprint(c.bw, " ")
162 }
163 astring(a.Key).writeTo(c, c.bw)
164 fmt.Fprint(c.bw, " ")
165 if a.IsString {
166 string0(string(a.Value)).writeTo(c, c.bw)
167 } else {
168 v := readerSizeSyncliteral{bytes.NewReader(a.Value), int64(len(a.Value)), true}
169 v.writeTo(c, c.bw)
170 }
171 }
172 c.bwritelinef(")")
173 }
174
175 if longentries >= 0 {
176 c.bwritelinef("%s OK [METADATA LONGENTRIES %d] getmetadata done", tag, longentries)
177 } else {
178 c.ok(tag, cmd)
179 }
180}
181
182// Set metadata annotation, per mailbox or globally.
183//
184// We only implement private annotations, not shared annotations. We don't
185// currently have a mechanism for determining if the user should have access.
186//
187// State: Authenticated and selected.
188func (c *conn) cmdSetmetadata(tag, cmd string, p *parser) {
189 // Command: ../rfc/5464:547
190
191 // Request syntax: ../rfc/5464:826
192
193 p.xspace()
194 mailboxName := p.xmailbox()
195 // Empty name means a global (per-account) annotation, not for a mailbox.
196 if mailboxName != "" {
197 mailboxName = xcheckmailboxname(mailboxName, true)
198 }
199 p.xspace()
200 p.xtake("(")
201 var l []store.Annotation
202 for {
203 key, isString, value := p.xmetadataKeyValue()
204 l = append(l, store.Annotation{Key: key, IsString: isString, Value: value})
205 if p.take(")") {
206 break
207 }
208 p.xspace()
209 }
210 p.xempty()
211
212 // Additional checks on entry names.
213 for _, a := range l {
214 // We only allow /private/* entry names, so check early and fail if we see anything
215 // else (the only other option is /shared/* at this moment).
216 // ../rfc/5464:217
217 if !strings.HasPrefix(a.Key, "/private/") {
218 // ../rfc/5464:346
219 xuserErrorf("only /private/* entry names allowed")
220 }
221
222 // We also enforce that /private/vendor/ is followed by at least 2 elements.
223 // ../rfc/5464:234
224 if a.Key == "/private/vendor" || strings.HasPrefix(a.Key, "/private/vendor/") {
225 t := strings.SplitN(a.Key[1:], "/", 4)
226 if len(t) < 4 {
227 xuserErrorf("entry names starting with /private/vendor must have at least 4 components")
228 }
229 }
230 }
231
232 // Store the annotations, possibly removing/inserting/updating them.
233 c.account.WithWLock(func() {
234 var changes []store.Change
235
236 c.xdbwrite(func(tx *bstore.Tx) {
237 var mb store.Mailbox // mb.ID as 0 is used in query below.
238 if mailboxName != "" {
239 mb = c.xmailbox(tx, mailboxName, "TRYCREATE")
240 }
241
242 for _, a := range l {
243 q := bstore.QueryTx[store.Annotation](tx)
244 q.FilterNonzero(store.Annotation{Key: a.Key})
245 q.FilterEqual("MailboxID", mb.ID) // Can be zero.
246
247 // Nil means remove. ../rfc/5464:579
248 if a.Value == nil {
249 var deleted []store.Annotation
250 q.Gather(&deleted)
251 _, err := q.Delete()
252 xcheckf(err, "deleting annotation")
253 for _, oa := range deleted {
254 changes = append(changes, oa.Change(mailboxName))
255 }
256 continue
257 }
258
259 a.MailboxID = mb.ID
260
261 oa, err := q.Get()
262 if err == bstore.ErrAbsent {
263 err = tx.Insert(&a)
264 xcheckf(err, "inserting annotation")
265 changes = append(changes, a.Change(mailboxName))
266 continue
267 }
268 xcheckf(err, "looking up existing annotation for entry name")
269 if oa.IsString != a.IsString || (oa.Value == nil) != (a.Value == nil) || !bytes.Equal(oa.Value, a.Value) {
270 changes = append(changes, a.Change(mailboxName))
271 }
272 oa.Value = a.Value
273 err = tx.Update(&oa)
274 xcheckf(err, "updating metadata annotation")
275 }
276
277 // Check for total size. We allow a total of 1000 entries, with total capacity of 1MB.
278 // ../rfc/5464:383
279 var n int
280 var size int
281 err := bstore.QueryTx[store.Annotation](tx).ForEach(func(a store.Annotation) error {
282 n++
283 if n > metadataMaxKeys {
284 // ../rfc/5464:590
285 xusercodeErrorf("METADATA TOOMANY", "too many metadata entries, 1000 allowed in total")
286 }
287 size += len(a.Key) + len(a.Value)
288 if size > metadataMaxSize {
289 // ../rfc/5464:585 We only have a max total size limit, not per entry. We'll
290 // mention the max total size.
291 xusercodeErrorf(fmt.Sprintf("METADATA MAXSIZE %d", metadataMaxSize), "metadata entry values too large, total maximum size is 1MB")
292 }
293 return nil
294 })
295 xcheckf(err, "checking metadata annotation size")
296 })
297
298 c.broadcast(changes)
299 })
300
301 c.ok(tag, cmd)
302}
303