1use core::{fmt, num::NonZeroU32};
95
96use alloc::{collections::BTreeMap, string::String, string::ToString, vec::Vec};
97
98use imap_codec::{
99 CommandCodec,
100 fragmentizer::Fragmentizer,
101 imap_types::{
102 command::{Command, CommandBody},
103 core::{TagGenerator, Vec1},
104 fetch::MessageDataItem,
105 flag::{Flag, StoreResponse, StoreType},
106 response::{Data, StatusKind, Tagged},
107 sequence::SequenceSet,
108 },
109};
110use log::trace;
111use thiserror::Error;
112
113use crate::{coroutine::*, imap_try, send::*};
114
115#[derive(Clone, Debug, Error)]
117pub enum ImapMessageStoreError {
118 #[error("IMAP STORE failed: NO {0}")]
119 No(String),
120 #[error("IMAP STORE failed: BAD {0}")]
121 Bad(String),
122 #[error("IMAP STORE failed: BYE {0}")]
123 Bye(String),
124
125 #[error("IMAP STORE failed: server did not return a tagged response")]
126 MissingTagged,
127
128 #[error("IMAP STORE failed: {0}")]
129 Send(#[from] SendImapCommandError),
130}
131
132#[derive(Clone, Debug, Default, Eq, PartialEq)]
134pub struct ImapMessageStoreOptions {
135 pub uid: bool,
137}
138
139pub struct ImapMessageStore {
141 state: State,
142}
143
144impl ImapMessageStore {
145 pub fn new(
146 sequence_set: SequenceSet,
147 kind: StoreType,
148 flags: Vec<Flag<'static>>,
149 opts: ImapMessageStoreOptions,
150 ) -> Self {
151 let command = Command {
152 tag: TagGenerator::new().generate(),
153 body: CommandBody::Store {
154 modifiers: Default::default(),
155 sequence_set,
156 kind,
157 response: StoreResponse::Answer,
158 flags,
159 uid: opts.uid,
160 },
161 };
162
163 trace!("send IMAP command {command:?}");
164
165 let state = State::Send(SendImapCommand::new(CommandCodec::new(), command));
166
167 Self { state }
168 }
169}
170
171impl ImapCoroutine for ImapMessageStore {
172 type Yield = ImapYield;
173 type Return =
174 Result<BTreeMap<NonZeroU32, Vec1<MessageDataItem<'static>>>, ImapMessageStoreError>;
175
176 fn resume(
177 &mut self,
178 fragmentizer: &mut Fragmentizer,
179 arg: Option<&[u8]>,
180 ) -> ImapCoroutineState<Self::Yield, Self::Return> {
181 loop {
182 trace!("store: {}", self.state);
183
184 match &mut self.state {
185 State::Send(send) => {
186 let out = imap_try!(send, fragmentizer, arg);
187
188 if let Some(bye) = out.bye {
189 let err = ImapMessageStoreError::Bye(bye.text.to_string());
190 return ImapCoroutineState::Complete(Err(err));
191 }
192
193 let Some(Tagged { body, .. }) = out.tagged else {
194 let err = ImapMessageStoreError::MissingTagged;
195 return ImapCoroutineState::Complete(Err(err));
196 };
197
198 let mut data: BTreeMap<NonZeroU32, Vec1<MessageDataItem<'static>>> =
199 BTreeMap::new();
200 for res in out.data {
201 if let Data::Fetch { seq, items } = res {
202 data.insert(seq, items);
203 }
204 }
205
206 return match body.kind {
207 StatusKind::Ok => ImapCoroutineState::Complete(Ok(data)),
208 StatusKind::No => {
209 let err = ImapMessageStoreError::No(body.text.to_string());
210 ImapCoroutineState::Complete(Err(err))
211 }
212 StatusKind::Bad => {
213 let err = ImapMessageStoreError::Bad(body.text.to_string());
214 ImapCoroutineState::Complete(Err(err))
215 }
216 };
217 }
218 }
219 }
220 }
221}
222
223pub struct ImapMessageStoreSilent {
225 state: State,
226}
227
228impl ImapMessageStoreSilent {
229 pub fn new(
230 sequence_set: SequenceSet,
231 kind: StoreType,
232 flags: Vec<Flag<'static>>,
233 opts: ImapMessageStoreOptions,
234 ) -> Self {
235 let command = Command {
236 tag: TagGenerator::new().generate(),
237 body: CommandBody::Store {
238 modifiers: Default::default(),
239 sequence_set,
240 kind,
241 response: StoreResponse::Silent,
242 flags,
243 uid: opts.uid,
244 },
245 };
246
247 trace!("send IMAP command {command:?}");
248
249 let state = State::Send(SendImapCommand::new(CommandCodec::new(), command));
250
251 Self { state }
252 }
253}
254
255impl ImapCoroutine for ImapMessageStoreSilent {
256 type Yield = ImapYield;
257 type Return = Result<(), ImapMessageStoreError>;
258
259 fn resume(
260 &mut self,
261 fragmentizer: &mut Fragmentizer,
262 arg: Option<&[u8]>,
263 ) -> ImapCoroutineState<Self::Yield, Self::Return> {
264 loop {
265 trace!("store silent: {}", self.state);
266
267 match &mut self.state {
268 State::Send(send) => {
269 let out = imap_try!(send, fragmentizer, arg);
270
271 if let Some(bye) = out.bye {
272 let err = ImapMessageStoreError::Bye(bye.text.to_string());
273 return ImapCoroutineState::Complete(Err(err));
274 }
275
276 let Some(Tagged { body, .. }) = out.tagged else {
277 let err = ImapMessageStoreError::MissingTagged;
278 return ImapCoroutineState::Complete(Err(err));
279 };
280
281 return match body.kind {
282 StatusKind::Ok => ImapCoroutineState::Complete(Ok(())),
283 StatusKind::No => {
284 let err = ImapMessageStoreError::No(body.text.to_string());
285 ImapCoroutineState::Complete(Err(err))
286 }
287 StatusKind::Bad => {
288 let err = ImapMessageStoreError::Bad(body.text.to_string());
289 ImapCoroutineState::Complete(Err(err))
290 }
291 };
292 }
293 }
294 }
295 }
296}
297
298enum State {
299 Send(SendImapCommand<CommandCodec>),
300}
301
302impl fmt::Display for State {
303 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304 match self {
305 Self::Send(_) => f.write_str("send store"),
306 }
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use core::str;
313
314 use alloc::{borrow::ToOwned, vec, vec::Vec};
315
316 use super::*;
317
318 fn flags() -> Vec<Flag<'static>> {
319 vec![Flag::Seen]
320 }
321
322 #[test]
323 fn echo_success_returns_map() {
324 let mut store = ImapMessageStore::new(
325 "1".try_into().expect("valid sequence set"),
326 StoreType::Add,
327 flags(),
328 ImapMessageStoreOptions::default(),
329 );
330 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
331
332 let bytes = expect_wants_write_echo(&mut store, &mut frag, None);
333 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
334
335 expect_wants_read_echo(&mut store, &mut frag);
336
337 let reply = format!("* 1 FETCH (FLAGS (\\Seen))\r\n{tag} OK STORE completed\r\n");
338 let map = expect_complete_ok_echo(&mut store, &mut frag, reply.as_bytes());
339 assert_eq!(1, map.len());
340 }
341
342 #[test]
343 fn echo_uid_variant_sends_uid_store() {
344 let mut store = ImapMessageStore::new(
345 "42".try_into().expect("valid sequence set"),
346 StoreType::Add,
347 flags(),
348 ImapMessageStoreOptions { uid: true },
349 );
350 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
351
352 let bytes = expect_wants_write_echo(&mut store, &mut frag, None);
353 let line = str::from_utf8(&bytes).expect("utf8 command");
354 assert!(line.contains("UID STORE 42 "));
355 }
356
357 #[test]
358 fn silent_success_returns_ok() {
359 let mut store = ImapMessageStoreSilent::new(
360 "1".try_into().expect("valid sequence set"),
361 StoreType::Add,
362 flags(),
363 ImapMessageStoreOptions::default(),
364 );
365 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
366
367 let bytes = expect_wants_write_silent(&mut store, &mut frag, None);
368 let line = str::from_utf8(&bytes).expect("utf8 command");
369 let tag = first_word(line).to_owned();
370 assert!(line.contains("STORE 1 +FLAGS.SILENT "));
371
372 expect_wants_read_silent(&mut store, &mut frag);
373
374 let reply = format!("{tag} OK STORE completed\r\n");
375 expect_complete_ok_silent(&mut store, &mut frag, reply.as_bytes());
376 }
377
378 #[test]
379 fn echo_tagged_no_returns_no_error() {
380 let mut store = ImapMessageStore::new(
381 "1".try_into().expect("valid sequence set"),
382 StoreType::Add,
383 flags(),
384 ImapMessageStoreOptions::default(),
385 );
386 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
387
388 let bytes = expect_wants_write_echo(&mut store, &mut frag, None);
389 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
390
391 expect_wants_read_echo(&mut store, &mut frag);
392
393 let reply = format!("{tag} NO mailbox is read-only\r\n");
394 let err = expect_complete_err_echo(&mut store, &mut frag, reply.as_bytes());
395 let ImapMessageStoreError::No(text) = err else {
396 panic!("expected ImapMessageStoreError::No, got {err:?}");
397 };
398 assert_eq!(text, "mailbox is read-only");
399 }
400
401 fn expect_wants_write_echo(
404 cor: &mut ImapMessageStore,
405 frag: &mut Fragmentizer,
406 arg: Option<&[u8]>,
407 ) -> Vec<u8> {
408 match cor.resume(frag, arg) {
409 ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
410 state => panic!("expected WantsWrite, got {state:?}"),
411 }
412 }
413
414 fn expect_wants_read_echo(cor: &mut ImapMessageStore, frag: &mut Fragmentizer) {
415 match cor.resume(frag, None) {
416 ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
417 state => panic!("expected WantsRead, got {state:?}"),
418 }
419 }
420
421 fn expect_complete_ok_echo(
422 cor: &mut ImapMessageStore,
423 frag: &mut Fragmentizer,
424 reply: &[u8],
425 ) -> BTreeMap<NonZeroU32, Vec1<MessageDataItem<'static>>> {
426 match cor.resume(frag, Some(reply)) {
427 ImapCoroutineState::Complete(Ok(value)) => value,
428 state => panic!("expected Complete(Ok), got {state:?}"),
429 }
430 }
431
432 fn expect_complete_err_echo(
433 cor: &mut ImapMessageStore,
434 frag: &mut Fragmentizer,
435 reply: &[u8],
436 ) -> ImapMessageStoreError {
437 match cor.resume(frag, Some(reply)) {
438 ImapCoroutineState::Complete(Err(err)) => err,
439 state => panic!("expected Complete(Err), got {state:?}"),
440 }
441 }
442
443 fn expect_wants_write_silent(
444 cor: &mut ImapMessageStoreSilent,
445 frag: &mut Fragmentizer,
446 arg: Option<&[u8]>,
447 ) -> Vec<u8> {
448 match cor.resume(frag, arg) {
449 ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
450 state => panic!("expected WantsWrite, got {state:?}"),
451 }
452 }
453
454 fn expect_wants_read_silent(cor: &mut ImapMessageStoreSilent, frag: &mut Fragmentizer) {
455 match cor.resume(frag, None) {
456 ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
457 state => panic!("expected WantsRead, got {state:?}"),
458 }
459 }
460
461 fn expect_complete_ok_silent(
462 cor: &mut ImapMessageStoreSilent,
463 frag: &mut Fragmentizer,
464 reply: &[u8],
465 ) {
466 match cor.resume(frag, Some(reply)) {
467 ImapCoroutineState::Complete(Ok(())) => {}
468 state => panic!("expected Complete(Ok), got {state:?}"),
469 }
470 }
471
472 fn first_word(line: &str) -> &str {
473 line.split_whitespace()
474 .next()
475 .expect("first whitespace-separated token")
476 }
477}