1use core::{fmt, num::NonZeroU32};
55
56use alloc::{string::String, string::ToString, vec::Vec};
57
58use imap_codec::{
59 CommandCodec,
60 fragmentizer::Fragmentizer,
61 imap_types::{
62 command::{Command, CommandBody},
63 core::{Charset, TagGenerator, Vec1},
64 extensions::sort::SortCriterion,
65 response::{Data, StatusKind, Tagged},
66 search::SearchKey,
67 },
68};
69use log::trace;
70use thiserror::Error;
71
72use crate::{coroutine::*, imap_try, send::*};
73
74#[derive(Clone, Debug, Error)]
76pub enum ImapMailboxSortError {
77 #[error("IMAP SORT failed: NO {0}")]
78 No(String),
79 #[error("IMAP SORT failed: BAD {0}")]
80 Bad(String),
81 #[error("IMAP SORT failed: BYE {0}")]
82 Bye(String),
83
84 #[error("IMAP SORT failed: server did not return a tagged response")]
85 MissingTagged,
86 #[error("IMAP SORT failed: server did not return any data")]
87 MissingData,
88
89 #[error("IMAP SORT failed: {0}")]
90 Send(#[from] SendImapCommandError),
91}
92
93#[derive(Clone, Debug, Default, Eq, PartialEq)]
95pub struct ImapMailboxSortOptions {
96 pub uid: bool,
98}
99
100pub struct ImapMailboxSort {
102 state: State,
103}
104
105impl ImapMailboxSort {
106 pub fn new(
107 sort_criteria: Vec1<SortCriterion>,
108 search_criteria: Vec1<SearchKey<'static>>,
109 opts: ImapMailboxSortOptions,
110 ) -> Self {
111 let command = Command {
112 tag: TagGenerator::new().generate(),
113 body: CommandBody::Sort {
114 sort_criteria,
115 charset: Charset::try_from("UTF-8").expect("UTF-8 is a valid charset"),
116 search_criteria,
117 uid: opts.uid,
118 },
119 };
120
121 trace!("send IMAP command {command:?}");
122
123 let state = State::Send(SendImapCommand::new(CommandCodec::new(), command));
124
125 Self { state }
126 }
127}
128
129impl ImapCoroutine for ImapMailboxSort {
130 type Yield = ImapYield;
131 type Return = Result<Vec<NonZeroU32>, ImapMailboxSortError>;
132
133 fn resume(
134 &mut self,
135 fragmentizer: &mut Fragmentizer,
136 arg: Option<&[u8]>,
137 ) -> ImapCoroutineState<Self::Yield, Self::Return> {
138 loop {
139 trace!("sort: {}", self.state);
140
141 match &mut self.state {
142 State::Send(send) => {
143 let out = imap_try!(send, fragmentizer, arg);
144
145 if let Some(bye) = out.bye {
146 let err = ImapMailboxSortError::Bye(bye.text.to_string());
147 return ImapCoroutineState::Complete(Err(err));
148 }
149
150 let Some(Tagged { body, .. }) = out.tagged else {
151 let err = ImapMailboxSortError::MissingTagged;
152 return ImapCoroutineState::Complete(Err(err));
153 };
154
155 let mut ids = None;
156 for data in out.data {
157 if let Data::Sort(sort_ids, _) = data {
158 ids = Some(sort_ids);
159 }
160 }
161
162 return match body.kind {
163 StatusKind::Ok => match ids {
164 Some(ids) => ImapCoroutineState::Complete(Ok(ids)),
165 None => {
166 ImapCoroutineState::Complete(Err(ImapMailboxSortError::MissingData))
167 }
168 },
169 StatusKind::No => {
170 let err = ImapMailboxSortError::No(body.text.to_string());
171 ImapCoroutineState::Complete(Err(err))
172 }
173 StatusKind::Bad => {
174 let err = ImapMailboxSortError::Bad(body.text.to_string());
175 ImapCoroutineState::Complete(Err(err))
176 }
177 };
178 }
179 }
180 }
181 }
182}
183
184enum State {
185 Send(SendImapCommand<CommandCodec>),
186}
187
188impl fmt::Display for State {
189 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190 match self {
191 Self::Send(_) => f.write_str("send sort"),
192 }
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use core::str;
199
200 use alloc::{borrow::ToOwned, vec, vec::Vec};
201
202 use super::*;
203
204 fn sort_criteria() -> Vec1<SortCriterion> {
205 Vec1::try_from(vec![SortCriterion {
206 reverse: false,
207 key: imap_codec::imap_types::extensions::sort::SortKey::Arrival,
208 }])
209 .expect("one sort criterion")
210 }
211
212 fn search_criteria() -> Vec1<SearchKey<'static>> {
213 Vec1::try_from(vec![SearchKey::All]).expect("one search criterion")
214 }
215
216 #[test]
217 fn success_returns_ids() {
218 let mut sort = ImapMailboxSort::new(
219 sort_criteria(),
220 search_criteria(),
221 ImapMailboxSortOptions::default(),
222 );
223 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
224
225 let bytes = expect_wants_write(&mut sort, &mut frag, None);
226 let line = str::from_utf8(&bytes).expect("utf8 command");
227 let tag = first_word(line).to_owned();
228 assert!(line.contains("SORT "));
229
230 expect_wants_read(&mut sort, &mut frag);
231
232 let reply = format!("* SORT 3 1 2\r\n{tag} OK SORT completed\r\n");
233 let ids = expect_complete_ok(&mut sort, &mut frag, reply.as_bytes());
234 assert_eq!(3, ids.len());
235 }
236
237 #[test]
238 fn uid_variant_sends_uid_sort() {
239 let mut sort = ImapMailboxSort::new(
240 sort_criteria(),
241 search_criteria(),
242 ImapMailboxSortOptions { uid: true },
243 );
244 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
245
246 let bytes = expect_wants_write(&mut sort, &mut frag, None);
247 let line = str::from_utf8(&bytes).expect("utf8 command");
248 assert!(line.contains("UID SORT "));
249 }
250
251 #[test]
252 fn missing_data_returns_missing_data_error() {
253 let mut sort = ImapMailboxSort::new(
254 sort_criteria(),
255 search_criteria(),
256 ImapMailboxSortOptions::default(),
257 );
258 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
259
260 let bytes = expect_wants_write(&mut sort, &mut frag, None);
261 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
262
263 expect_wants_read(&mut sort, &mut frag);
264
265 let reply = format!("{tag} OK SORT completed\r\n");
266 let err = expect_complete_err(&mut sort, &mut frag, reply.as_bytes());
267 assert!(matches!(err, ImapMailboxSortError::MissingData));
268 }
269
270 #[test]
271 fn tagged_no_returns_no_error() {
272 let mut sort = ImapMailboxSort::new(
273 sort_criteria(),
274 search_criteria(),
275 ImapMailboxSortOptions::default(),
276 );
277 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
278
279 let bytes = expect_wants_write(&mut sort, &mut frag, None);
280 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
281
282 expect_wants_read(&mut sort, &mut frag);
283
284 let reply = format!("{tag} NO no mailbox selected\r\n");
285 let err = expect_complete_err(&mut sort, &mut frag, reply.as_bytes());
286 let ImapMailboxSortError::No(text) = err else {
287 panic!("expected ImapMailboxSortError::No, got {err:?}");
288 };
289 assert_eq!(text, "no mailbox selected");
290 }
291
292 fn expect_wants_write(
295 cor: &mut ImapMailboxSort,
296 frag: &mut Fragmentizer,
297 arg: Option<&[u8]>,
298 ) -> Vec<u8> {
299 match cor.resume(frag, arg) {
300 ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
301 state => panic!("expected WantsWrite, got {state:?}"),
302 }
303 }
304
305 fn expect_wants_read(cor: &mut ImapMailboxSort, frag: &mut Fragmentizer) {
306 match cor.resume(frag, None) {
307 ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
308 state => panic!("expected WantsRead, got {state:?}"),
309 }
310 }
311
312 fn expect_complete_ok(
313 cor: &mut ImapMailboxSort,
314 frag: &mut Fragmentizer,
315 reply: &[u8],
316 ) -> Vec<NonZeroU32> {
317 match cor.resume(frag, Some(reply)) {
318 ImapCoroutineState::Complete(Ok(value)) => value,
319 state => panic!("expected Complete(Ok), got {state:?}"),
320 }
321 }
322
323 fn expect_complete_err(
324 cor: &mut ImapMailboxSort,
325 frag: &mut Fragmentizer,
326 reply: &[u8],
327 ) -> ImapMailboxSortError {
328 match cor.resume(frag, Some(reply)) {
329 ImapCoroutineState::Complete(Err(err)) => err,
330 state => panic!("expected Complete(Err), got {state:?}"),
331 }
332 }
333
334 fn first_word(line: &str) -> &str {
335 line.split_whitespace()
336 .next()
337 .expect("first whitespace-separated token")
338 }
339}