1use core::fmt;
46
47use alloc::{string::String, string::ToString, vec::Vec};
48
49use imap_codec::{
50 CommandCodec,
51 fragmentizer::Fragmentizer,
52 imap_types::{
53 command::{Command, CommandBody},
54 core::{QuotedChar, TagGenerator},
55 flag::FlagNameAttribute,
56 mailbox::{ListMailbox, Mailbox},
57 response::{Data, StatusKind, Tagged},
58 },
59};
60use log::trace;
61use thiserror::Error;
62
63use crate::{
64 coroutine::*,
65 imap_try,
66 rfc3501::mailbox::{decode_inplace, encode_inplace},
67 send::*,
68};
69
70pub type ImapMailboxListing = Vec<(
72 Mailbox<'static>,
73 Option<QuotedChar>,
74 Vec<FlagNameAttribute<'static>>,
75)>;
76
77#[derive(Clone, Debug, Error)]
79pub enum ImapMailboxListError {
80 #[error("IMAP LIST failed: NO {0}")]
81 No(String),
82 #[error("IMAP LIST failed: BAD {0}")]
83 Bad(String),
84 #[error("IMAP LIST failed: BYE {0}")]
85 Bye(String),
86
87 #[error("IMAP LIST failed: server did not return a tagged response")]
88 MissingTagged,
89
90 #[error("IMAP LIST failed: {0}")]
91 Send(#[from] SendImapCommandError),
92}
93
94pub struct ImapMailboxList {
96 state: State,
97}
98
99impl ImapMailboxList {
100 pub fn new(mut reference: Mailbox<'static>, mailbox_wildcard: ListMailbox<'static>) -> Self {
101 encode_inplace(&mut reference);
102
103 let command = Command {
104 tag: TagGenerator::new().generate(),
105 body: CommandBody::List {
106 reference,
107 mailbox_wildcard,
108 },
109 };
110
111 trace!("send IMAP command {command:?}");
112
113 let state = State::Send(SendImapCommand::new(CommandCodec::new(), command));
114
115 Self { state }
116 }
117}
118
119impl ImapCoroutine for ImapMailboxList {
120 type Yield = ImapYield;
121 type Return = Result<ImapMailboxListing, ImapMailboxListError>;
122
123 fn resume(
124 &mut self,
125 fragmentizer: &mut Fragmentizer,
126 arg: Option<&[u8]>,
127 ) -> ImapCoroutineState<Self::Yield, Self::Return> {
128 loop {
129 trace!("list: {}", self.state);
130
131 match &mut self.state {
132 State::Send(send) => {
133 let out = imap_try!(send, fragmentizer, arg);
134
135 if let Some(bye) = out.bye {
136 let err = ImapMailboxListError::Bye(bye.text.to_string());
137 return ImapCoroutineState::Complete(Err(err));
138 }
139
140 let Some(Tagged { body, .. }) = out.tagged else {
141 let err = ImapMailboxListError::MissingTagged;
142 return ImapCoroutineState::Complete(Err(err));
143 };
144
145 let mut mailboxes = Vec::new();
146 for data in out.data {
147 if let Data::List {
148 items,
149 delimiter,
150 mailbox,
151 } = data
152 {
153 let mut mailbox = mailbox;
154 decode_inplace(&mut mailbox);
155 mailboxes.push((mailbox, delimiter, items));
156 }
157 }
158
159 return match body.kind {
160 StatusKind::Ok => ImapCoroutineState::Complete(Ok(mailboxes)),
161 StatusKind::No => {
162 let err = ImapMailboxListError::No(body.text.to_string());
163 ImapCoroutineState::Complete(Err(err))
164 }
165 StatusKind::Bad => {
166 let err = ImapMailboxListError::Bad(body.text.to_string());
167 ImapCoroutineState::Complete(Err(err))
168 }
169 };
170 }
171 }
172 }
173 }
174}
175
176enum State {
177 Send(SendImapCommand<CommandCodec>),
178}
179
180impl fmt::Display for State {
181 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182 match self {
183 Self::Send(_) => f.write_str("send list"),
184 }
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use core::str;
191
192 use alloc::{borrow::ToOwned, vec::Vec};
193
194 use super::*;
195
196 #[test]
197 fn success_returns_rows() {
198 let reference: Mailbox = "".try_into().expect("valid reference");
199 let pattern: ListMailbox = "*".try_into().expect("valid pattern");
200 let mut list = ImapMailboxList::new(reference, pattern);
201 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
202
203 let bytes = expect_wants_write(&mut list, &mut frag, None);
204 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
205
206 expect_wants_read(&mut list, &mut frag);
207
208 let reply = format!(
209 "* LIST (\\HasNoChildren) \"/\" INBOX\r\n\
210 * LIST (\\HasNoChildren) \"/\" Archive\r\n\
211 {tag} OK LIST completed\r\n",
212 );
213 let rows = expect_complete_ok(&mut list, &mut frag, reply.as_bytes());
214 assert_eq!(2, rows.len());
215 }
216
217 #[test]
218 fn tagged_no_returns_no_error() {
219 let mut list = ImapMailboxList::new(
220 "".try_into().expect("valid reference"),
221 "*".try_into().expect("valid pattern"),
222 );
223 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
224
225 let bytes = expect_wants_write(&mut list, &mut frag, None);
226 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
227
228 expect_wants_read(&mut list, &mut frag);
229
230 let reply = format!("{tag} NO not allowed\r\n");
231 let err = expect_complete_err(&mut list, &mut frag, reply.as_bytes());
232 let ImapMailboxListError::No(text) = err else {
233 panic!("expected ImapMailboxListError::No, got {err:?}");
234 };
235 assert_eq!(text, "not allowed");
236 }
237
238 #[test]
239 fn bye_returns_bye_error() {
240 let mut list = ImapMailboxList::new(
241 "".try_into().expect("valid reference"),
242 "*".try_into().expect("valid pattern"),
243 );
244 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
245
246 let _ = expect_wants_write(&mut list, &mut frag, None);
247 expect_wants_read(&mut list, &mut frag);
248
249 let err = expect_complete_err(&mut list, &mut frag, b"* BYE going down\r\n");
250 let ImapMailboxListError::Bye(text) = err else {
251 panic!("expected ImapMailboxListError::Bye, got {err:?}");
252 };
253 assert_eq!(text, "going down");
254 }
255
256 fn expect_wants_write(
259 cor: &mut ImapMailboxList,
260 frag: &mut Fragmentizer,
261 arg: Option<&[u8]>,
262 ) -> Vec<u8> {
263 match cor.resume(frag, arg) {
264 ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
265 state => panic!("expected WantsWrite, got {state:?}"),
266 }
267 }
268
269 fn expect_wants_read(cor: &mut ImapMailboxList, frag: &mut Fragmentizer) {
270 match cor.resume(frag, None) {
271 ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
272 state => panic!("expected WantsRead, got {state:?}"),
273 }
274 }
275
276 fn expect_complete_ok(
277 cor: &mut ImapMailboxList,
278 frag: &mut Fragmentizer,
279 reply: &[u8],
280 ) -> ImapMailboxListing {
281 match cor.resume(frag, Some(reply)) {
282 ImapCoroutineState::Complete(Ok(value)) => value,
283 state => panic!("expected Complete(Ok), got {state:?}"),
284 }
285 }
286
287 fn expect_complete_err(
288 cor: &mut ImapMailboxList,
289 frag: &mut Fragmentizer,
290 reply: &[u8],
291 ) -> ImapMailboxListError {
292 match cor.resume(frag, Some(reply)) {
293 ImapCoroutineState::Complete(Err(err)) => err,
294 state => panic!("expected Complete(Err), got {state:?}"),
295 }
296 }
297
298 fn first_word(line: &str) -> &str {
299 line.split_whitespace()
300 .next()
301 .expect("first whitespace-separated token")
302 }
303}