8 "github.com/mjl-/bstore"
10 "github.com/mjl-/mox/store"
13// Changed during tests.
14var metadataMaxKeys = 1000
15var metadataMaxSize = 1000 * 1000
23// Note: We do not tie the special-use mailbox flags to a (synthetic) private
26// For registration of names, see https://www.iana.org/assignments/imap-metadata/imap-metadata.xhtml
28// Get metadata annotations, per mailbox or globally.
30// State: Authenticated and selected.
31func (c *conn) cmdGetmetadata(tag, cmd string, p *parser) {
37 var optMaxSize int64 = -1
41 if p.take("MAXSIZE") {
46 p.xerrorf("only a single maxsize option accepted")
49 } else if p.take("DEPTH") {
52 s := p.xtakelist("0", "1", "INFINITY")
54 p.xerrorf("only single depth option accepted")
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")
69 mailboxName := p.xmailbox()
70 if mailboxName != "" {
71 mailboxName = xcheckmailboxname(mailboxName, true)
75 entryNames := map[string]struct{}{}
79 entryNames[s] = struct{}{}
87 entryNames[s] = struct{}{}
91 var annotations []store.Annotation
92 longentries := -1 // Size of largest value skipped due to optMaxSize.
../rfc/5464:482
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)
100 mb := c.xmailbox(tx, mailboxName, "TRYCREATE")
101 q.FilterNonzero(store.Annotation{MailboxID: mb.ID})
104 q.SortAsc("MailboxID", "Key") // For tests.
105 err := q.ForEach(func(a store.Annotation) error {
109 if _, ok := entryNames[a.Key]; !ok {
112 case "1", "INFINITY":
113 // Go through all keys, matching depth.
114 if _, ok := entryNames[a.Key]; ok {
118 for s := range entryNames {
123 if !strings.HasPrefix(a.Key, prefix) {
126 if optDepth == "INFINITY" {
130 suffix := a.Key[len(prefix):]
131 t := strings.SplitN(suffix, "/", 2)
141 xcheckf(fmt.Errorf("%q", optDepth), "missing case for depth")
144 if optMaxSize >= 0 && int64(len(a.Value)) > optMaxSize {
145 longentries = max(longentries, len(a.Value))
147 annotations = append(annotations, a)
151 xcheckf(err, "looking up annotations")
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 {
161 fmt.Fprint(c.bw, " ")
163 astring(a.Key).writeTo(c, c.bw)
164 fmt.Fprint(c.bw, " ")
166 string0(string(a.Value)).writeTo(c, c.bw)
168 v := readerSizeSyncliteral{bytes.NewReader(a.Value), int64(len(a.Value)), true}
175 if longentries >= 0 {
176 c.bwritelinef("%s OK [METADATA LONGENTRIES %d] getmetadata done", tag, longentries)
182// Set metadata annotation, per mailbox or globally.
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.
187// State: Authenticated and selected.
188func (c *conn) cmdSetmetadata(tag, cmd string, p *parser) {
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)
201 var l []store.Annotation
203 key, isString, value := p.xmetadataKeyValue()
204 l = append(l, store.Annotation{Key: key, IsString: isString, Value: value})
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).
217 if !strings.HasPrefix(a.Key, "/private/") {
219 xuserErrorf("only /private/* entry names allowed")
222 // We also enforce that /private/vendor/ is followed by at least 2 elements.
224 if a.Key == "/private/vendor" || strings.HasPrefix(a.Key, "/private/vendor/") {
225 t := strings.SplitN(a.Key[1:], "/", 4)
227 xuserErrorf("entry names starting with /private/vendor must have at least 4 components")
232 // Store the annotations, possibly removing/inserting/updating them.
233 c.account.WithWLock(func() {
234 var changes []store.Change
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")
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.
249 var deleted []store.Annotation
252 xcheckf(err, "deleting annotation")
253 for _, oa := range deleted {
254 changes = append(changes, oa.Change(mailboxName))
262 if err == bstore.ErrAbsent {
264 xcheckf(err, "inserting annotation")
265 changes = append(changes, a.Change(mailboxName))
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))
274 xcheckf(err, "updating metadata annotation")
277 // Check for total size. We allow a total of 1000 entries, with total capacity of 1MB.
281 err := bstore.QueryTx[store.Annotation](tx).ForEach(func(a store.Annotation) error {
283 if n > metadataMaxKeys {
285 xusercodeErrorf("METADATA TOOMANY", "too many metadata entries, 1000 allowed in total")
287 size += len(a.Key) + len(a.Value)
288 if size > metadataMaxSize {
290 // mention the max total size.
291 xusercodeErrorf(fmt.Sprintf("METADATA MAXSIZE %d", metadataMaxSize), "metadata entry values too large, total maximum size is 1MB")
295 xcheckf(err, "checking metadata annotation size")