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