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