imap metadata extension: allow keys in the /shared/ namespace too

not just /private. /shared/ is the more commonly implemented namespace, because
it is easier te implement: you don't need per-user/account storage of metadata.
i initially approached it from the other direction: we don't have a mechanism
to share metadata with other accounts, so everything is private, and i assumed
that would be what a user would prefer. but email clients make the decisions,
and they'll likely try the /shared/ namespace.
This commit is contained in:
Mechiel Lukkien
2025-02-23 20:19:07 +01:00
parent 463e801909
commit 2809136451
3 changed files with 17 additions and 12 deletions

View File

@ -181,8 +181,8 @@ func (c *conn) cmdGetmetadata(tag, cmd string, p *parser) {
// Set metadata annotation, per mailbox or globally.
//
// We only implement private annotations, not shared annotations. We don't
// currently have a mechanism for determining if the user should have access.
// We allow both /private/* and /shared/*, we store them in the same way since we
// don't have ACL extension support yet or another mechanism for access control.
//
// State: Authenticated and selected.
func (c *conn) cmdSetmetadata(tag, cmd string, p *parser) {
@ -211,20 +211,22 @@ func (c *conn) cmdSetmetadata(tag, cmd string, p *parser) {
// Additional checks on entry names.
for _, a := range l {
// We only allow /private/* entry names, so check early and fail if we see anything
// else (the only other option is /shared/* at this moment).
// ../rfc/5464:217
if !strings.HasPrefix(a.Key, "/private/") {
if !strings.HasPrefix(a.Key, "/private/") && !strings.HasPrefix(a.Key, "/shared/") {
// ../rfc/5464:346
xuserErrorf("only /private/* entry names allowed")
xuserErrorf("only /private/* and /shared/* entry names allowed")
}
// We also enforce that /private/vendor/ is followed by at least 2 elements.
// ../rfc/5464:234
if a.Key == "/private/vendor" || strings.HasPrefix(a.Key, "/private/vendor/") {
switch {
case a.Key == "/private/vendor",
strings.HasPrefix(a.Key, "/private/vendor/"),
a.Key == "/shared/vendor", strings.HasPrefix(a.Key, "/shared/vendor/"):
t := strings.SplitN(a.Key[1:], "/", 4)
if len(t) < 4 {
xuserErrorf("entry names starting with /private/vendor must have at least 4 components")
xuserErrorf("entry names starting with /private/vendor or /shared/vendor must have at least 4 components")
}
}
}

View File

@ -31,16 +31,18 @@ func TestMetadata(t *testing.T) {
},
})
tc.transactf("ok", `setmetadata Inbox (/shared/comment "share")`)
tc.transactf("ok", `getmetadata inbox (/private/comment /private/unknown /shared/comment)`)
tc.xuntagged(imapclient.UntaggedMetadataAnnotations{
Mailbox: "Inbox",
Annotations: []imapclient.Annotation{
{Key: "/private/comment", IsString: true, Value: []byte("mailbox value")},
{Key: "/shared/comment", IsString: true, Value: []byte("share")},
},
})
tc.transactf("no", `setmetadata doesnotexist (/private/comment "test")`) // Bad mailbox.
tc.transactf("no", `setmetadata Inbox (/shared/comment "")`) // /shared/ not implemented.
tc.transactf("no", `setmetadata Inbox (/badprefix/comment "")`)
tc.transactf("no", `setmetadata Inbox (/private/vendor "")`) // /*/vendor must have more components.
tc.transactf("no", `setmetadata Inbox (/private/vendor/stillbad "")`) // /*/vendor must have more components.
@ -131,7 +133,7 @@ func TestMetadata(t *testing.T) {
},
})
// Same as previous, but ask for everything below /.
tc.transactf("ok", `getmetadata (depth infinity) inbox (/)`)
tc.transactf("ok", `getmetadata (depth infinity) inbox ("")`)
tc.xuntagged(imapclient.UntaggedMetadataAnnotations{
Mailbox: "Inbox",
Annotations: []imapclient.Annotation{
@ -142,6 +144,7 @@ func TestMetadata(t *testing.T) {
{Key: "/private/another", IsString: true, Value: []byte("longer")},
{Key: "/private/comment", IsString: false, Value: []byte("test")},
{Key: "/private/vendor/a/b", IsString: true, Value: []byte("")},
{Key: "/shared/comment", IsString: true, Value: []byte("share")},
},
})

View File

@ -229,8 +229,8 @@ type Annotation struct {
// Can be zero, indicates global (per-account) annotation.
MailboxID int64 `bstore:"ref Mailbox,unique MailboxID+Key"`
// "Entry name", always starts with "/private/". Stored lower-case, comparisons
// must be done case-insensitively.
// "Entry name", always starts with "/private/" or "/shared/". Stored lower-case,
// comparisons must be done case-insensitively.
Key string `bstore:"nonzero"`
IsString bool // If true, the value is a string instead of bytes.