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