Files
zeekayandHanzo Dev 15909c7b79 Rename Go module path to github.com/hanzoai/mail
Fork of github.com/mjl-/mox at 5656ca6d1b
(upstream v0.0.15 plus 55 commits).

Rename the module path in go.mod and the import paths in 268 Go files. Also
update the five scripts that "go install" the module (genwebsite.sh,
docker-release.sh, apidiff.sh, test-upgrade.sh, Dockerfile.release), which
would otherwise build upstream rather than this fork.

Add NOTICE recording the upstream project, author, license, and fork point,
and stating which license covers which files: MIT for the mox source and our
changes, MPL-2.0 for publicsuffix/public_suffix_list.txt alone, and the
vendored dependencies' own licenses under licenses/. Embed NOTICE so
"mail licenses" prints the lineage from the running binary. LICENSE.MIT,
LICENSE.MPLv2.0 and the licenses/ tree are preserved verbatim; no code is
relicensed.

The mox command name, config keys, data directory layout and website content
are unchanged. LLM.md records the fork point, the rename, what was left alone,
and the steps to stand the server up.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 23:16:19 -07:00

272 lines
6.9 KiB
Go

package imapserver
import (
"bytes"
"fmt"
"sort"
"strings"
"github.com/mjl-/bstore"
"github.com/hanzoai/mail/mox-"
"github.com/hanzoai/mail/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 bool
var retStatusAttrs []string
var retMetadata []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
// We always include special-use mailbox flags. Mac OS X Mail 16.0 (sept 2023) does
// not ask for the flags, but does use them when given. ../rfc/6154:146
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(")")
case "METADATA":
// ../rfc/9590:167
p.xspace()
p.xtake("(")
for {
s := p.xmetadataKey()
retMetadata = append(retMetadata, s)
if !p.space() {
break
}
}
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.xbwritelinef(`* 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
var respMetadata []concatspace
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)
q.FilterEqual("Expunged", false)
err := q.ForEach(func(mb store.Mailbox) error {
names[mb.Name] = info{mailbox: &mb}
nameList = append(nameList, mb.Name)
for p := mox.ParentMailboxName(mb.Name); p != ""; p = mox.ParentMailboxName(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 := mox.ParentMailboxName(sub.Name); p != ""; p = mox.ParentMailboxName(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 info.mailbox != nil {
add := func(b bool, v string) {
if b {
flags = append(flags, bare(v))
}
}
mb := info.mailbox
add(mb.Archive, `\Archive`)
add(mb.Draft, `\Drafts`)
add(mb.Junk, `\Junk`)
add(mb.Sent, `\Sent`)
add(mb.Trash, `\Trash`)
}
var extStr string
if extended != nil {
extStr = " " + extended.pack(c)
}
line := fmt.Sprintf(`* LIST %s "/" %s%s`, flags.pack(c), mailboxt(name).pack(c), extStr)
responseLines = append(responseLines, line)
if retStatusAttrs != nil && info.mailbox != nil {
responseLines = append(responseLines, c.xstatusLine(tx, *info.mailbox, retStatusAttrs))
}
// ../rfc/9590:101
if info.mailbox != nil && len(retMetadata) > 0 {
var meta listspace
for _, k := range retMetadata {
q := bstore.QueryTx[store.Annotation](tx)
q.FilterNonzero(store.Annotation{MailboxID: info.mailbox.ID, Key: k})
q.FilterEqual("Expunged", false)
a, err := q.Get()
var v token
if err == bstore.ErrAbsent {
v = nilt
} else {
xcheckf(err, "get annotation")
if a.IsString {
v = string0(string(a.Value))
} else {
v = readerSizeSyncliteral{bytes.NewReader(a.Value), int64(len(a.Value)), true}
}
}
meta = append(meta, astring(k), v)
}
line := concatspace{bare("*"), bare("METADATA"), mailboxt(info.mailbox.Name), meta}
respMetadata = append(respMetadata, line)
}
}
})
})
for _, line := range responseLines {
c.xbwritelinef("%s", line)
}
for _, meta := range respMetadata {
meta.xwriteTo(c, c.xbw)
c.xbwritelinef("")
}
c.ok(tag, cmd)
}