mox/imapserver/list.go
Mechiel Lukkien 8c2814df89
imapserver: fix returning special-use mailbox "\Drafts" instead of "\Draft"
related to issue #66 by x8x, though this doesn't fix that (macos mail doesn't
yet request the special-use flags).
2023-09-23 14:50:02 +02:00

228 lines
5.7 KiB
Go

package imapserver
import (
"fmt"
"path/filepath"
"sort"
"strings"
"github.com/mjl-/bstore"
"github.com/mjl-/mox/store"
)
// LIST command, for listing mailboxes with various attributes, including about subscriptions and children.
// We don't have flags Marked, Unmarked, NoSelect and NoInferiors and we don't have REMOTE mailboxes.
//
// State: Authenticated and selected.
func (c *conn) cmdList(tag, cmd string, p *parser) {
// Command: ../rfc/9051:2224 ../rfc/6154:144 ../rfc/5258:193 ../rfc/3501:2191
// Examples: ../rfc/9051:2755 ../rfc/6154:347 ../rfc/5258:679 ../rfc/3501:2359
// Request syntax: ../rfc/9051:6600 ../rfc/6154:478 ../rfc/5258:1095 ../rfc/3501:4793
p.xspace()
var isExtended bool
var listSubscribed bool
var listRecursive bool
if p.take("(") {
// ../rfc/9051:6633
isExtended = true
selectOptions := map[string]bool{}
var nbase int
for !p.take(")") {
if len(selectOptions) > 0 {
p.xspace()
}
w := p.xatom()
W := strings.ToUpper(w)
switch W {
case "REMOTE":
case "RECURSIVEMATCH":
listRecursive = true
case "SUBSCRIBED":
nbase++
listSubscribed = true
default:
// ../rfc/9051:2398
xsyntaxErrorf("bad list selection option %q", w)
}
// Duplicates must be accepted. ../rfc/9051:2399
selectOptions[W] = true
}
if listRecursive && nbase == 0 {
// ../rfc/9051:6640
xsyntaxErrorf("cannot have RECURSIVEMATCH selection option without other (base) selection option")
}
p.xspace()
}
reference := p.xmailbox()
p.xspace()
patterns, isList := p.xmboxOrPat()
isExtended = isExtended || isList
var retSubscribed, retChildren, retSpecialUse bool
var retStatusAttrs []string
if p.take(" RETURN (") {
isExtended = true
// ../rfc/9051:6613 ../rfc/9051:6915 ../rfc/9051:7072 ../rfc/9051:6821 ../rfc/5819:95
n := 0
for !p.take(")") {
if n > 0 {
p.xspace()
}
n++
w := p.xatom()
W := strings.ToUpper(w)
switch W {
case "SUBSCRIBED":
retSubscribed = true
case "CHILDREN":
// ../rfc/3348:44
retChildren = true
case "SPECIAL-USE":
// ../rfc/6154:478
retSpecialUse = true
case "STATUS":
// ../rfc/9051:7072 ../rfc/5819:181
p.xspace()
p.xtake("(")
retStatusAttrs = []string{p.xstatusAtt()}
for p.take(" ") {
retStatusAttrs = append(retStatusAttrs, p.xstatusAtt())
}
p.xtake(")")
default:
// ../rfc/9051:2398
xsyntaxErrorf("bad list return option %q", w)
}
}
}
p.xempty()
if !isExtended && reference == "" && patterns[0] == "" {
// ../rfc/9051:2277 ../rfc/3501:2221
c.bwritelinef(`* LIST () "/" ""`)
c.ok(tag, cmd)
return
}
if isExtended {
// ../rfc/9051:2286
n := make([]string, 0, len(patterns))
for _, p := range patterns {
if p != "" {
n = append(n, p)
}
}
patterns = n
}
re := xmailboxPatternMatcher(reference, patterns)
var responseLines []string
c.account.WithRLock(func() {
c.xdbread(func(tx *bstore.Tx) {
type info struct {
mailbox *store.Mailbox
subscribed bool
}
names := map[string]info{}
hasSubscribedChild := map[string]bool{}
hasChild := map[string]bool{}
var nameList []string
q := bstore.QueryTx[store.Mailbox](tx)
err := q.ForEach(func(mb store.Mailbox) error {
names[mb.Name] = info{mailbox: &mb}
nameList = append(nameList, mb.Name)
for p := filepath.Dir(mb.Name); p != "."; p = filepath.Dir(p) {
hasChild[p] = true
}
return nil
})
xcheckf(err, "listing mailboxes")
qs := bstore.QueryTx[store.Subscription](tx)
err = qs.ForEach(func(sub store.Subscription) error {
info, ok := names[sub.Name]
info.subscribed = true
names[sub.Name] = info
if !ok {
nameList = append(nameList, sub.Name)
}
for p := filepath.Dir(sub.Name); p != "."; p = filepath.Dir(p) {
hasSubscribedChild[p] = true
}
return nil
})
xcheckf(err, "listing subscriptions")
sort.Strings(nameList) // For predictable order in tests.
for _, name := range nameList {
if !re.MatchString(name) {
continue
}
info := names[name]
var flags listspace
var extended listspace
if listRecursive && hasSubscribedChild[name] {
extended = listspace{bare("CHILDINFO"), listspace{dquote("SUBSCRIBED")}}
}
if listSubscribed && info.subscribed {
flags = append(flags, bare(`\Subscribed`))
if info.mailbox == nil {
flags = append(flags, bare(`\NonExistent`))
}
}
if (info.mailbox == nil || listSubscribed) && flags == nil && extended == nil {
continue
}
if retChildren {
var f string
if hasChild[name] {
f = `\HasChildren`
} else {
f = `\HasNoChildren`
}
flags = append(flags, bare(f))
}
if !listSubscribed && retSubscribed && info.subscribed {
flags = append(flags, bare(`\Subscribed`))
}
if retSpecialUse && info.mailbox != nil {
if info.mailbox.Archive {
flags = append(flags, bare(`\Archive`))
}
if info.mailbox.Draft {
flags = append(flags, bare(`\Drafts`))
}
if info.mailbox.Junk {
flags = append(flags, bare(`\Junk`))
}
if info.mailbox.Sent {
flags = append(flags, bare(`\Sent`))
}
if info.mailbox.Trash {
flags = append(flags, bare(`\Trash`))
}
}
var extStr string
if extended != nil {
extStr = " " + extended.pack(c)
}
line := fmt.Sprintf(`* LIST %s "/" %s%s`, flags.pack(c), astring(name).pack(c), extStr)
responseLines = append(responseLines, line)
if retStatusAttrs != nil && info.mailbox != nil {
responseLines = append(responseLines, c.xstatusLine(tx, *info.mailbox, retStatusAttrs))
}
}
})
})
for _, line := range responseLines {
c.bwritelinef("%s", line)
}
c.ok(tag, cmd)
}