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