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