1package imapserver
2
3import (
4 "fmt"
5 "path"
6 "sort"
7 "strings"
8
9 "github.com/mjl-/bstore"
10 "github.com/mjl-/mox/store"
11)
12
13// LIST command, for listing mailboxes with various attributes, including about subscriptions and children.
14// We don't have flags Marked, Unmarked, NoSelect and NoInferiors and we don't have REMOTE mailboxes.
15//
16// State: Authenticated and selected.
17func (c *conn) cmdList(tag, cmd string, p *parser) {
18 // Command: ../rfc/9051:2224 ../rfc/6154:144 ../rfc/5258:193 ../rfc/3501:2191
19 // Examples: ../rfc/9051:2755 ../rfc/6154:347 ../rfc/5258:679 ../rfc/3501:2359
20
21 // Request syntax: ../rfc/9051:6600 ../rfc/6154:478 ../rfc/5258:1095 ../rfc/3501:4793
22 p.xspace()
23 var isExtended bool
24 var listSubscribed bool
25 var listRecursive bool
26 if p.take("(") {
27 // ../rfc/9051:6633
28 isExtended = true
29 selectOptions := map[string]bool{}
30 var nbase int
31 for !p.take(")") {
32 if len(selectOptions) > 0 {
33 p.xspace()
34 }
35 w := p.xatom()
36 W := strings.ToUpper(w)
37 switch W {
38 case "REMOTE":
39 case "RECURSIVEMATCH":
40 listRecursive = true
41 case "SUBSCRIBED":
42 nbase++
43 listSubscribed = true
44 default:
45 // ../rfc/9051:2398
46 xsyntaxErrorf("bad list selection option %q", w)
47 }
48 // Duplicates must be accepted. ../rfc/9051:2399
49 selectOptions[W] = true
50 }
51 if listRecursive && nbase == 0 {
52 // ../rfc/9051:6640
53 xsyntaxErrorf("cannot have RECURSIVEMATCH selection option without other (base) selection option")
54 }
55 p.xspace()
56 }
57 reference := p.xmailbox()
58 p.xspace()
59 patterns, isList := p.xmboxOrPat()
60 isExtended = isExtended || isList
61 var retSubscribed, retChildren bool
62 var retStatusAttrs []string
63 if p.take(" RETURN (") {
64 isExtended = true
65 // ../rfc/9051:6613 ../rfc/9051:6915 ../rfc/9051:7072 ../rfc/9051:6821 ../rfc/5819:95
66 n := 0
67 for !p.take(")") {
68 if n > 0 {
69 p.xspace()
70 }
71 n++
72 w := p.xatom()
73 W := strings.ToUpper(w)
74 switch W {
75 case "SUBSCRIBED":
76 retSubscribed = true
77 case "CHILDREN":
78 // ../rfc/3348:44
79 retChildren = true
80 case "SPECIAL-USE":
81 // ../rfc/6154:478
82 // We always include special-use mailbox flags. Mac OS X Mail 16.0 (sept 2023) does
83 // not ask for the flags, but does use them when given. ../rfc/6154:146
84 case "STATUS":
85 // ../rfc/9051:7072 ../rfc/5819:181
86 p.xspace()
87 p.xtake("(")
88 retStatusAttrs = []string{p.xstatusAtt()}
89 for p.take(" ") {
90 retStatusAttrs = append(retStatusAttrs, p.xstatusAtt())
91 }
92 p.xtake(")")
93 default:
94 // ../rfc/9051:2398
95 xsyntaxErrorf("bad list return option %q", w)
96 }
97 }
98 }
99 p.xempty()
100
101 if !isExtended && reference == "" && patterns[0] == "" {
102 // ../rfc/9051:2277 ../rfc/3501:2221
103 c.bwritelinef(`* LIST () "/" ""`)
104 c.ok(tag, cmd)
105 return
106 }
107
108 if isExtended {
109 // ../rfc/9051:2286
110 n := make([]string, 0, len(patterns))
111 for _, p := range patterns {
112 if p != "" {
113 n = append(n, p)
114 }
115 }
116 patterns = n
117 }
118 re := xmailboxPatternMatcher(reference, patterns)
119 var responseLines []string
120
121 c.account.WithRLock(func() {
122 c.xdbread(func(tx *bstore.Tx) {
123 type info struct {
124 mailbox *store.Mailbox
125 subscribed bool
126 }
127 names := map[string]info{}
128 hasSubscribedChild := map[string]bool{}
129 hasChild := map[string]bool{}
130 var nameList []string
131
132 q := bstore.QueryTx[store.Mailbox](tx)
133 err := q.ForEach(func(mb store.Mailbox) error {
134 names[mb.Name] = info{mailbox: &mb}
135 nameList = append(nameList, mb.Name)
136 for p := path.Dir(mb.Name); p != "."; p = path.Dir(p) {
137 hasChild[p] = true
138 }
139 return nil
140 })
141 xcheckf(err, "listing mailboxes")
142
143 qs := bstore.QueryTx[store.Subscription](tx)
144 err = qs.ForEach(func(sub store.Subscription) error {
145 info, ok := names[sub.Name]
146 info.subscribed = true
147 names[sub.Name] = info
148 if !ok {
149 nameList = append(nameList, sub.Name)
150 }
151 for p := path.Dir(sub.Name); p != "."; p = path.Dir(p) {
152 hasSubscribedChild[p] = true
153 }
154 return nil
155 })
156 xcheckf(err, "listing subscriptions")
157
158 sort.Strings(nameList) // For predictable order in tests.
159
160 for _, name := range nameList {
161 if !re.MatchString(name) {
162 continue
163 }
164 info := names[name]
165
166 var flags listspace
167 var extended listspace
168 if listRecursive && hasSubscribedChild[name] {
169 extended = listspace{bare("CHILDINFO"), listspace{dquote("SUBSCRIBED")}}
170 }
171 if listSubscribed && info.subscribed {
172 flags = append(flags, bare(`\Subscribed`))
173 if info.mailbox == nil {
174 flags = append(flags, bare(`\NonExistent`))
175 }
176 }
177 if (info.mailbox == nil || listSubscribed) && flags == nil && extended == nil {
178 continue
179 }
180
181 if retChildren {
182 var f string
183 if hasChild[name] {
184 f = `\HasChildren`
185 } else {
186 f = `\HasNoChildren`
187 }
188 flags = append(flags, bare(f))
189 }
190 if !listSubscribed && retSubscribed && info.subscribed {
191 flags = append(flags, bare(`\Subscribed`))
192 }
193 if info.mailbox != nil {
194 if info.mailbox.Archive {
195 flags = append(flags, bare(`\Archive`))
196 }
197 if info.mailbox.Draft {
198 flags = append(flags, bare(`\Drafts`))
199 }
200 if info.mailbox.Junk {
201 flags = append(flags, bare(`\Junk`))
202 }
203 if info.mailbox.Sent {
204 flags = append(flags, bare(`\Sent`))
205 }
206 if info.mailbox.Trash {
207 flags = append(flags, bare(`\Trash`))
208 }
209 }
210
211 var extStr string
212 if extended != nil {
213 extStr = " " + extended.pack(c)
214 }
215 line := fmt.Sprintf(`* LIST %s "/" %s%s`, flags.pack(c), astring(c.encodeMailbox(name)).pack(c), extStr)
216 responseLines = append(responseLines, line)
217
218 if retStatusAttrs != nil && info.mailbox != nil {
219 responseLines = append(responseLines, c.xstatusLine(tx, *info.mailbox, retStatusAttrs))
220 }
221 }
222 })
223 })
224
225 for _, line := range responseLines {
226 c.bwritelinef("%s", line)
227 }
228 c.ok(tag, cmd)
229}
230