From 28091364514efe0080aabad2acfdacffa14cb38f Mon Sep 17 00:00:00 2001 From: Mechiel Lukkien Date: Sun, 23 Feb 2025 20:19:07 +0100 Subject: [PATCH] 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. --- imapserver/metadata.go | 18 ++++++++++-------- imapserver/metadata_test.go | 7 +++++-- store/account.go | 4 ++-- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/imapserver/metadata.go b/imapserver/metadata.go index 5b82a27..dbfbe42 100644 --- a/imapserver/metadata.go +++ b/imapserver/metadata.go @@ -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") } } } diff --git a/imapserver/metadata_test.go b/imapserver/metadata_test.go index 87c0de8..1144b7b 100644 --- a/imapserver/metadata_test.go +++ b/imapserver/metadata_test.go @@ -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")}, }, }) diff --git a/store/account.go b/store/account.go index f7eff1c..804c8e3 100644 --- a/store/account.go +++ b/store/account.go @@ -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.