1use core::{fmt, num::NonZeroU32};
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, SelectParameter},
54 core::TagGenerator,
55 mailbox::Mailbox,
56 response::{Code, Data, StatusBody, StatusKind, Tagged},
57 sequence::SequenceSet,
58 },
59};
60use log::trace;
61use thiserror::Error;
62
63use crate::{
64 coroutine::*,
65 imap_try,
66 rfc3501::{
67 mailbox::encode_inplace,
68 select::{SelectData, SelectFetch},
69 },
70 send::*,
71};
72
73pub type ExamineData = SelectData;
75pub type ExamineFetch = SelectFetch;
77
78#[derive(Clone, Debug, Error)]
80pub enum ImapMailboxExamineError {
81 #[error("IMAP EXAMINE failed: NO {0}")]
82 No(String),
83 #[error("IMAP EXAMINE failed: BAD {0}")]
84 Bad(String),
85 #[error("IMAP EXAMINE failed: BYE {0}")]
86 Bye(String),
87
88 #[error("IMAP EXAMINE failed: server did not return a tagged response")]
89 MissingTagged,
90
91 #[error("IMAP EXAMINE failed: {0}")]
92 Send(#[from] SendImapCommandError),
93}
94
95#[derive(Clone, Debug, Default, Eq, PartialEq)]
97pub struct ImapMailboxExamineOptions {
98 pub parameters: Vec<SelectParameter>,
100}
101
102pub struct ImapMailboxExamine {
104 state: State,
105}
106
107impl ImapMailboxExamine {
108 pub fn new(mut mailbox: Mailbox<'static>, opts: ImapMailboxExamineOptions) -> Self {
109 encode_inplace(&mut mailbox);
110
111 let command = Command {
112 tag: TagGenerator::new().generate(),
113 body: CommandBody::Examine {
114 mailbox,
115 parameters: opts.parameters,
116 },
117 };
118
119 trace!("send IMAP command {command:?}");
120
121 let state = State::Send(SendImapCommand::new(CommandCodec::new(), command));
122
123 Self { state }
124 }
125}
126
127impl ImapCoroutine for ImapMailboxExamine {
128 type Yield = ImapYield;
129 type Return = Result<ExamineData, ImapMailboxExamineError>;
130
131 fn resume(
132 &mut self,
133 fragmentizer: &mut Fragmentizer,
134 arg: Option<&[u8]>,
135 ) -> ImapCoroutineState<Self::Yield, Self::Return> {
136 loop {
137 trace!("examine: {}", self.state);
138
139 match &mut self.state {
140 State::Send(send) => {
141 let out = imap_try!(send, fragmentizer, arg);
142
143 if let Some(bye) = out.bye {
144 let err = ImapMailboxExamineError::Bye(bye.text.to_string());
145 return ImapCoroutineState::Complete(Err(err));
146 }
147
148 let Some(Tagged { body, .. }) = out.tagged else {
149 let err = ImapMailboxExamineError::MissingTagged;
150 return ImapCoroutineState::Complete(Err(err));
151 };
152
153 let mut output = ExamineData::default();
154
155 for data in out.data {
156 match data {
157 Data::Flags(flags) => output.flags = Some(flags),
158 Data::Exists(count) => output.exists = Some(count),
159 Data::Recent(count) => output.recent = Some(count),
160 Data::Fetch { seq, items } => {
161 output.changed.push(ExamineFetch { seq, items });
162 }
163 Data::Vanished {
164 earlier,
165 known_uids,
166 } if earlier => {
167 output.vanished_earlier.extend(expand_uid_set(&known_uids));
168 }
169 _ => {}
170 }
171 }
172
173 for StatusBody { kind, code, .. } in out.untagged {
174 if let StatusKind::Ok = kind {
175 match code {
176 Some(Code::Unseen(seq)) => output.unseen = Some(seq),
177 Some(Code::PermanentFlags(flags)) => {
178 output.permanent_flags = Some(flags)
179 }
180 Some(Code::UidNext(uid)) => output.uid_next = Some(uid),
181 Some(Code::UidValidity(uid)) => output.uid_validity = Some(uid),
182 Some(Code::HighestModSeq(modseq)) => {
183 output.highest_mod_seq = Some(modseq.get());
184 }
185 _ => {}
186 }
187 }
188 }
189
190 return match body.kind {
191 StatusKind::Ok => ImapCoroutineState::Complete(Ok(output)),
192 StatusKind::No => {
193 let err = ImapMailboxExamineError::No(body.text.to_string());
194 ImapCoroutineState::Complete(Err(err))
195 }
196 StatusKind::Bad => {
197 let err = ImapMailboxExamineError::Bad(body.text.to_string());
198 ImapCoroutineState::Complete(Err(err))
199 }
200 };
201 }
202 }
203 }
204 }
205}
206
207enum State {
208 Send(SendImapCommand<CommandCodec>),
209}
210
211impl fmt::Display for State {
212 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213 match self {
214 Self::Send(_) => f.write_str("send examine"),
215 }
216 }
217}
218
219fn expand_uid_set(uid_set: &SequenceSet) -> Vec<NonZeroU32> {
222 let max = NonZeroU32::new(u32::MAX).unwrap();
223 uid_set.iter(max).collect()
224}
225
226#[cfg(test)]
227mod tests {
228 use core::str;
229
230 use alloc::borrow::ToOwned;
231
232 use super::*;
233
234 #[test]
235 fn success_collects_response() {
236 let mut examine = ImapMailboxExamine::new(
237 "INBOX".try_into().expect("valid mailbox"),
238 ImapMailboxExamineOptions::default(),
239 );
240 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
241
242 let bytes = expect_wants_write(&mut examine, &mut frag, None);
243 let line = str::from_utf8(&bytes).expect("utf8 command");
244 let tag = first_word(line).to_owned();
245 assert!(line.contains("EXAMINE INBOX"));
246
247 expect_wants_read(&mut examine, &mut frag);
248
249 let reply = format!(
250 "* FLAGS (\\Seen)\r\n\
251 * 42 EXISTS\r\n\
252 * 7 RECENT\r\n\
253 * OK [UIDVALIDITY 1700] uid validity\r\n\
254 {tag} OK [READ-ONLY] EXAMINE completed\r\n",
255 );
256 let data = expect_complete_ok(&mut examine, &mut frag, reply.as_bytes());
257 assert_eq!(Some(42), data.exists);
258 assert_eq!(Some(7), data.recent);
259 assert_eq!(1700, data.uid_validity.expect("uid validity").get());
260 assert!(data.flags.is_some());
261 }
262
263 #[test]
264 fn tagged_no_returns_no_error() {
265 let mut examine = ImapMailboxExamine::new(
266 "INBOX".try_into().expect("valid mailbox"),
267 ImapMailboxExamineOptions::default(),
268 );
269 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
270
271 let bytes = expect_wants_write(&mut examine, &mut frag, None);
272 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
273
274 expect_wants_read(&mut examine, &mut frag);
275
276 let reply = format!("{tag} NO mailbox does not exist\r\n");
277 let err = expect_complete_err(&mut examine, &mut frag, reply.as_bytes());
278 let ImapMailboxExamineError::No(text) = err else {
279 panic!("expected ImapMailboxExamineError::No, got {err:?}");
280 };
281 assert_eq!(text, "mailbox does not exist");
282 }
283
284 #[test]
285 fn tagged_bad_returns_bad_error() {
286 let mut examine = ImapMailboxExamine::new(
287 "INBOX".try_into().expect("valid mailbox"),
288 ImapMailboxExamineOptions::default(),
289 );
290 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
291
292 let bytes = expect_wants_write(&mut examine, &mut frag, None);
293 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
294
295 expect_wants_read(&mut examine, &mut frag);
296
297 let reply = format!("{tag} BAD EXAMINE syntax error\r\n");
298 let err = expect_complete_err(&mut examine, &mut frag, reply.as_bytes());
299 let ImapMailboxExamineError::Bad(text) = err else {
300 panic!("expected ImapMailboxExamineError::Bad, got {err:?}");
301 };
302 assert_eq!(text, "EXAMINE syntax error");
303 }
304
305 #[test]
306 fn bye_returns_bye_error() {
307 let mut examine = ImapMailboxExamine::new(
308 "INBOX".try_into().expect("valid mailbox"),
309 ImapMailboxExamineOptions::default(),
310 );
311 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
312
313 let _ = expect_wants_write(&mut examine, &mut frag, None);
314 expect_wants_read(&mut examine, &mut frag);
315
316 let err = expect_complete_err(&mut examine, &mut frag, b"* BYE going down\r\n");
317 let ImapMailboxExamineError::Bye(text) = err else {
318 panic!("expected ImapMailboxExamineError::Bye, got {err:?}");
319 };
320 assert_eq!(text, "going down");
321 }
322
323 fn expect_wants_write(
326 cor: &mut ImapMailboxExamine,
327 frag: &mut Fragmentizer,
328 arg: Option<&[u8]>,
329 ) -> Vec<u8> {
330 match cor.resume(frag, arg) {
331 ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
332 state => panic!("expected WantsWrite, got {state:?}"),
333 }
334 }
335
336 fn expect_wants_read(cor: &mut ImapMailboxExamine, frag: &mut Fragmentizer) {
337 match cor.resume(frag, None) {
338 ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
339 state => panic!("expected WantsRead, got {state:?}"),
340 }
341 }
342
343 fn expect_complete_ok(
344 cor: &mut ImapMailboxExamine,
345 frag: &mut Fragmentizer,
346 reply: &[u8],
347 ) -> ExamineData {
348 match cor.resume(frag, Some(reply)) {
349 ImapCoroutineState::Complete(Ok(value)) => value,
350 state => panic!("expected Complete(Ok), got {state:?}"),
351 }
352 }
353
354 fn expect_complete_err(
355 cor: &mut ImapMailboxExamine,
356 frag: &mut Fragmentizer,
357 reply: &[u8],
358 ) -> ImapMailboxExamineError {
359 match cor.resume(frag, Some(reply)) {
360 ImapCoroutineState::Complete(Err(err)) => err,
361 state => panic!("expected Complete(Err), got {state:?}"),
362 }
363 }
364
365 fn first_word(line: &str) -> &str {
366 line.split_whitespace()
367 .next()
368 .expect("first whitespace-separated token")
369 }
370}