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