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