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