implement the imap metadata extension, rfc 5464

this allows setting per-mailbox and per-server annotations (metadata). we have
a fixed maximum for total number of annotations (1000) and their total size
(1000000 bytes). this size isn't held against the regular quota for simplicity.
we send unsolicited metadata responses when a connection is in the idle
command and a change to a metadata item is made.

we currently only implement the /private/ namespace.  we should implement the
/shared/ namespace, for mox-global metadata annotations.  only the admin should
be able to configure those, probably through the config file, cli, or admin web
interface.

for issue #290
This commit is contained in:
Mechiel Lukkien
2025-02-17 22:44:51 +01:00
parent 9dff879164
commit f30c44eddb
17 changed files with 820 additions and 41 deletions

View File

@ -427,6 +427,50 @@ func (c *Conn) xuntagged() Untagged {
c.xcrlf()
return r
case "METADATA":
// ../rfc/5464:807
c.xspace()
mailbox := c.xastring()
c.xspace()
if !c.take('(') {
// Unsolicited form, with only annotation keys, not values.
var keys []string
for {
key := c.xastring()
keys = append(keys, key)
if !c.space() {
break
}
}
c.xcrlf()
return UntaggedMetadataKeys{mailbox, keys}
}
// Form with values, in response to GETMETADATA command.
r := UntaggedMetadataAnnotations{Mailbox: mailbox}
for {
key := c.xastring()
c.xspace()
var value []byte
var isString bool
if c.take('~') {
value = c.xliteral()
} else {
value = []byte(c.xstring())
isString = true
// note: the abnf also allows nstring, but that only makes sense when the
// production rule is used in the setmetadata command. ../rfc/5464:831
}
r.Annotations = append(r.Annotations, Annotation{key, isString, value})
if c.take(')') {
break
}
c.xspace()
}
c.xcrlf()
return r
case "NAMESPACE":
// ../rfc/9051:6778
c.xspace()

View File

@ -10,27 +10,29 @@ import (
type Capability string
const (
CapIMAP4rev1 Capability = "IMAP4rev1"
CapIMAP4rev2 Capability = "IMAP4rev2"
CapLoginDisabled Capability = "LOGINDISABLED"
CapStarttls Capability = "STARTTLS"
CapAuthPlain Capability = "AUTH=PLAIN"
CapLiteralPlus Capability = "LITERAL+"
CapLiteralMinus Capability = "LITERAL-"
CapIdle Capability = "IDLE"
CapNamespace Capability = "NAMESPACE"
CapBinary Capability = "BINARY"
CapUnselect Capability = "UNSELECT"
CapUidplus Capability = "UIDPLUS"
CapEsearch Capability = "ESEARCH"
CapEnable Capability = "ENABLE"
CapSave Capability = "SAVE"
CapListExtended Capability = "LIST-EXTENDED"
CapSpecialUse Capability = "SPECIAL-USE"
CapMove Capability = "MOVE"
CapUTF8Only Capability = "UTF8=ONLY"
CapUTF8Accept Capability = "UTF8=ACCEPT"
CapID Capability = "ID" // ../rfc/2971:80
CapIMAP4rev1 Capability = "IMAP4rev1"
CapIMAP4rev2 Capability = "IMAP4rev2"
CapLoginDisabled Capability = "LOGINDISABLED"
CapStarttls Capability = "STARTTLS"
CapAuthPlain Capability = "AUTH=PLAIN"
CapLiteralPlus Capability = "LITERAL+"
CapLiteralMinus Capability = "LITERAL-"
CapIdle Capability = "IDLE"
CapNamespace Capability = "NAMESPACE"
CapBinary Capability = "BINARY"
CapUnselect Capability = "UNSELECT"
CapUidplus Capability = "UIDPLUS"
CapEsearch Capability = "ESEARCH"
CapEnable Capability = "ENABLE"
CapSave Capability = "SAVE"
CapListExtended Capability = "LIST-EXTENDED"
CapSpecialUse Capability = "SPECIAL-USE"
CapMove Capability = "MOVE"
CapUTF8Only Capability = "UTF8=ONLY"
CapUTF8Accept Capability = "UTF8=ACCEPT"
CapID Capability = "ID" // ../rfc/2971:80
CapMetadata Capability = "METADATA" // ../rfc/5464:124
CapMetadataServer Capability = "METADATA-SERVER" // ../rfc/5464:124
)
// Status is the tagged final result of a command.
@ -227,6 +229,27 @@ type UntaggedStatus struct {
Attrs map[StatusAttr]int64 // Upper case status attributes.
}
// ../rfc/5464:716 Unsolicited response, indicating an annotation has changed.
type UntaggedMetadataKeys struct {
Mailbox string // Empty means not specific to mailbox.
// Keys that have changed. To get values (or determine absence), the server must be
// queried.
Keys []string
}
type Annotation struct {
Key string
IsString bool
Value []byte
}
// ../rfc/5464:683
type UntaggedMetadataAnnotations struct {
Mailbox string // Empty means not specific to mailbox.
Annotations []Annotation
}
// ../rfc/9051:7059 ../9208:712
type StatusAttr string