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