imapserver: implement rfc 9590, returning metadata in the extended list command

only with "return" including "metadata". so clients can quickly get certain
metadata (eg for display, such as a color) for mailboxes.

this also adds a protocol token type "mailboxt" that properly encodes to utf7
if required.
This commit is contained in:
Mechiel Lukkien
2025-02-23 22:12:18 +01:00
parent 2809136451
commit 0ed820e3b0
8 changed files with 120 additions and 24 deletions

View File

@ -164,12 +164,13 @@ var authFailDelay = time.Second // After authentication failure.
// WITHIN: ../rfc/5032
// NAMESPACE: ../rfc/2342
// COMPRESS=DEFLATE: ../rfc/4978
// LIST-METADATA: ../rfc/9590
//
// We always announce support for SCRAM PLUS-variants, also on connections without
// TLS. The client should not be selecting PLUS variants on non-TLS connections,
// instead opting to do the bare SCRAM variant without indicating the server claims
// to support the PLUS variant (skipping the server downgrade detection check).
const serverCapabilities = "IMAP4rev2 IMAP4rev1 ENABLE LITERAL+ IDLE SASL-IR BINARY UNSELECT UIDPLUS ESEARCH SEARCHRES MOVE UTF8=ACCEPT LIST-EXTENDED SPECIAL-USE CREATE-SPECIAL-USE LIST-STATUS AUTH=SCRAM-SHA-256-PLUS AUTH=SCRAM-SHA-256 AUTH=SCRAM-SHA-1-PLUS AUTH=SCRAM-SHA-1 AUTH=CRAM-MD5 ID APPENDLIMIT=9223372036854775807 CONDSTORE QRESYNC STATUS=SIZE QUOTA QUOTA=RES-STORAGE METADATA SAVEDATE WITHIN NAMESPACE COMPRESS=DEFLATE"
const serverCapabilities = "IMAP4rev2 IMAP4rev1 ENABLE LITERAL+ IDLE SASL-IR BINARY UNSELECT UIDPLUS ESEARCH SEARCHRES MOVE UTF8=ACCEPT LIST-EXTENDED SPECIAL-USE CREATE-SPECIAL-USE LIST-STATUS AUTH=SCRAM-SHA-256-PLUS AUTH=SCRAM-SHA-256 AUTH=SCRAM-SHA-1-PLUS AUTH=SCRAM-SHA-1 AUTH=CRAM-MD5 ID APPENDLIMIT=9223372036854775807 CONDSTORE QRESYNC STATUS=SIZE QUOTA QUOTA=RES-STORAGE METADATA SAVEDATE WITHIN NAMESPACE COMPRESS=DEFLATE LIST-METADATA"
type conn struct {
cid int64
@ -441,13 +442,6 @@ func (c *conn) utf8strings() bool {
return c.enabled[capIMAP4rev2] || c.enabled[capUTF8Accept]
}
func (c *conn) encodeMailbox(s string) string {
if c.utf8strings() {
return s
}
return utf7encode(s)
}
func (c *conn) xdbwrite(fn func(tx *bstore.Tx)) {
err := c.account.DB.Write(context.TODO(), func(tx *bstore.Tx) error {
fn(tx)
@ -1686,22 +1680,22 @@ func (c *conn) applyChanges(changes []store.Change, initial bool) {
// unrecognized \NonExistent and interpret this as a newly created mailbox, while
// the goal was to remove it...
if c.enabled[capIMAP4rev2] {
c.bwritelinef(`* LIST (\NonExistent) "/" %s`, astring(c.encodeMailbox(ch.Name)).pack(c))
c.bwritelinef(`* LIST (\NonExistent) "/" %s`, mailboxt(ch.Name).pack(c))
}
case store.ChangeAddMailbox:
c.bwritelinef(`* LIST (%s) "/" %s`, strings.Join(ch.Flags, " "), astring(c.encodeMailbox(ch.Mailbox.Name)).pack(c))
c.bwritelinef(`* LIST (%s) "/" %s`, strings.Join(ch.Flags, " "), mailboxt(ch.Mailbox.Name).pack(c))
case store.ChangeRenameMailbox:
// OLDNAME only with IMAP4rev2 or NOTIFY ../rfc/9051:2726 ../rfc/5465:628
var oldname string
if c.enabled[capIMAP4rev2] {
oldname = fmt.Sprintf(` ("OLDNAME" (%s))`, string0(c.encodeMailbox(ch.OldName)).pack(c))
oldname = fmt.Sprintf(` ("OLDNAME" (%s))`, mailboxt(ch.OldName).pack(c))
}
c.bwritelinef(`* LIST (%s) "/" %s%s`, strings.Join(ch.Flags, " "), astring(c.encodeMailbox(ch.NewName)).pack(c), oldname)
c.bwritelinef(`* LIST (%s) "/" %s%s`, strings.Join(ch.Flags, " "), mailboxt(ch.NewName).pack(c), oldname)
case store.ChangeAddSubscription:
c.bwritelinef(`* LIST (%s) "/" %s`, strings.Join(append([]string{`\Subscribed`}, ch.Flags...), " "), astring(c.encodeMailbox(ch.Name)).pack(c))
c.bwritelinef(`* LIST (%s) "/" %s`, strings.Join(append([]string{`\Subscribed`}, ch.Flags...), " "), mailboxt(ch.Name).pack(c))
case store.ChangeAnnotation:
// ../rfc/5464:807 ../rfc/5464:788
c.bwritelinef(`* METADATA %s %s`, astring(c.encodeMailbox(ch.MailboxName)).pack(c), astring(ch.Key).pack(c))
c.bwritelinef(`* METADATA %s %s`, mailboxt(ch.MailboxName).pack(c), astring(ch.Key).pack(c))
default:
panic(fmt.Sprintf("internal error, missing case for %#v", change))
}
@ -2607,7 +2601,7 @@ func (c *conn) cmdSelectExamine(isselect bool, tag, cmd string, p *parser) {
}
c.bwritelinef(`* OK [UIDVALIDITY %d] x`, mb.UIDValidity)
c.bwritelinef(`* OK [UIDNEXT %d] x`, mb.UIDNext)
c.bwritelinef(`* LIST () "/" %s`, astring(c.encodeMailbox(mb.Name)).pack(c))
c.bwritelinef(`* LIST () "/" %s`, mailboxt(mb.Name).pack(c))
if c.enabled[capCondstore] {
// ../rfc/7162:417
// ../rfc/7162-eid5055 ../rfc/7162:484 ../rfc/7162:1167
@ -2842,9 +2836,9 @@ func (c *conn) cmdCreate(tag, cmd string, p *parser) {
var oldname string
// OLDNAME only with IMAP4rev2 or NOTIFY ../rfc/9051:2726 ../rfc/5465:628
if c.enabled[capIMAP4rev2] && n == name && name != origName && !(name == "Inbox" || strings.HasPrefix(name, "Inbox/")) {
oldname = fmt.Sprintf(` ("OLDNAME" (%s))`, string0(c.encodeMailbox(origName)).pack(c))
oldname = fmt.Sprintf(` ("OLDNAME" (%s))`, mailboxt(origName).pack(c))
}
c.bwritelinef(`* LIST (\Subscribed) "/" %s%s`, astring(c.encodeMailbox(n)).pack(c), oldname)
c.bwritelinef(`* LIST (\Subscribed) "/" %s%s`, mailboxt(n).pack(c), oldname)
}
c.ok(tag, cmd)
}
@ -3152,7 +3146,7 @@ func (c *conn) cmdLsub(tag, cmd string, p *parser) {
continue
}
have[name] = true
line := fmt.Sprintf(`* LSUB () "/" %s`, astring(c.encodeMailbox(name)).pack(c))
line := fmt.Sprintf(`* LSUB () "/" %s`, mailboxt(name).pack(c))
lines = append(lines, line)
}
@ -3167,7 +3161,7 @@ func (c *conn) cmdLsub(tag, cmd string, p *parser) {
if have[mb.Name] || !subscribedKids[mb.Name] || !re.MatchString(mb.Name) {
return nil
}
line := fmt.Sprintf(`* LSUB (\NoSelect) "/" %s`, astring(c.encodeMailbox(mb.Name)).pack(c))
line := fmt.Sprintf(`* LSUB (\NoSelect) "/" %s`, mailboxt(mb.Name).pack(c))
lines = append(lines, line)
return nil
})
@ -3274,7 +3268,7 @@ func (c *conn) xstatusLine(tx *bstore.Tx, mb store.Mailbox, attrs []string) stri
xsyntaxErrorf("unknown attribute %q", a)
}
}
return fmt.Sprintf("* STATUS %s (%s)", astring(c.encodeMailbox(mb.Name)).pack(c), strings.Join(status, " "))
return fmt.Sprintf("* STATUS %s (%s)", mailboxt(mb.Name).pack(c), strings.Join(status, " "))
}
func flaglist(fl store.Flags, keywords []string) listspace {