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 q.FilterEqual("Expunged", false)
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.xbw, "* METADATA %s (", mailboxt(mailboxName).pack(c))
159 for i, a := range annotations {
160 if i > 0 {
161 fmt.Fprint(c.xbw, " ")
162 }
163 astring(a.Key).writeTo(c, c.xbw)
164 fmt.Fprint(c.xbw, " ")
165 if a.IsString {
166 string0(string(a.Value)).writeTo(c, c.xbw)
167 } else {
168 v := readerSizeSyncliteral{bytes.NewReader(a.Value), int64(len(a.Value)), true}
169 v.writeTo(c, c.xbw)
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 allow both /private/* and /shared/*, we store them in the same way since we
185// don't have ACL extension support yet or another mechanism for access control.
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 // ../rfc/5464:217
215 if !strings.HasPrefix(a.Key, "/private/") && !strings.HasPrefix(a.Key, "/shared/") {
216 // ../rfc/5464:346
217 xuserErrorf("only /private/* and /shared/* entry names allowed")
218 }
219
220 // We also enforce that /private/vendor/ is followed by at least 2 elements.
221 // ../rfc/5464:234
222 switch {
223 case a.Key == "/private/vendor",
224 strings.HasPrefix(a.Key, "/private/vendor/"),
225 a.Key == "/shared/vendor", strings.HasPrefix(a.Key, "/shared/vendor/"):
226
227 t := strings.SplitN(a.Key[1:], "/", 4)
228 if len(t) < 4 {
229 xuserErrorf("entry names starting with /private/vendor or /shared/vendor must have at least 4 components")
230 }
231 }
232 }
233
234 // Store the annotations, possibly removing/inserting/updating them.
235 c.account.WithWLock(func() {
236 var changes []store.Change
237 var modseq store.ModSeq
238
239 c.xdbwrite(func(tx *bstore.Tx) {
240 var mb store.Mailbox // mb.ID as 0 is used in query below.
241 if mailboxName != "" {
242 mb = c.xmailbox(tx, mailboxName, "TRYCREATE")
243 }
244
245 for _, a := range l {
246 q := bstore.QueryTx[store.Annotation](tx)
247 q.FilterNonzero(store.Annotation{Key: a.Key})
248 q.FilterEqual("MailboxID", mb.ID) // Can be zero.
249 q.FilterEqual("Expunged", false)
250 oa, err := q.Get()
251 // Nil means remove. ../rfc/5464:579
252 if err == bstore.ErrAbsent && a.Value == nil {
253 continue
254 }
255 if modseq == 0 {
256 var err error
257 modseq, err = c.account.NextModSeq(tx)
258 xcheckf(err, "get next modseq")
259 }
260 if err == bstore.ErrAbsent {
261 a.MailboxID = mb.ID
262 a.CreateSeq = modseq
263 a.ModSeq = modseq
264 err = tx.Insert(&a)
265 xcheckf(err, "inserting annotation")
266 changes = append(changes, a.Change(mailboxName))
267 } else {
268 xcheckf(err, "get metadata")
269 oa.ModSeq = modseq
270 if a.Value == nil {
271 oa.Expunged = true
272 }
273 oa.IsString = a.IsString
274 oa.Value = a.Value
275 err = tx.Update(&oa)
276 xcheckf(err, "updating metdata")
277 changes = append(changes, oa.Change(mailboxName))
278 }
279 }
280
281 c.xcheckMetadataSize(tx)
282
283 // ../rfc/7162:1335
284 if mb.ID != 0 && modseq != 0 {
285 mb.ModSeq = modseq
286 err := tx.Update(&mb)
287 xcheckf(err, "updating mailbox with modseq")
288 }
289 })
290
291 c.broadcast(changes)
292 })
293
294 c.ok(tag, cmd)
295}
296
297func (c *conn) xcheckMetadataSize(tx *bstore.Tx) {
298 // Check for total size. We allow a total of 1000 entries, with total capacity of 1MB.
299 // ../rfc/5464:383
300 var n int
301 var size int
302 err := bstore.QueryTx[store.Annotation](tx).FilterEqual("Expunged", false).ForEach(func(a store.Annotation) error {
303 n++
304 if n > metadataMaxKeys {
305 // ../rfc/5464:590
306 xusercodeErrorf("METADATA TOOMANY", "too many metadata entries, 1000 allowed in total")
307 }
308 size += len(a.Key) + len(a.Value)
309 if size > metadataMaxSize {
310 // ../rfc/5464:585 We only have a max total size limit, not per entry. We'll
311 // mention the max total size.
312 xusercodeErrorf(fmt.Sprintf("METADATA MAXSIZE %d", metadataMaxSize), "metadata entry values too large, total maximum size is 1MB")
313 }
314 return nil
315 })
316 xcheckf(err, "checking metadata annotation size")
317}
318