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