1use core::{fmt, num::NonZeroU32};
96
97use alloc::{collections::BTreeMap, string::String, string::ToString, vec::Vec};
98
99use imap_codec::{
100 CommandCodec,
101 fragmentizer::Fragmentizer,
102 imap_types::{
103 command::{Command, CommandBody},
104 core::{TagGenerator, Vec1},
105 fetch::MessageDataItem,
106 flag::{Flag, StoreResponse, StoreType},
107 response::{Data, StatusKind, Tagged},
108 sequence::SequenceSet,
109 },
110};
111use log::trace;
112use thiserror::Error;
113
114use crate::{coroutine::*, imap_try, send::*};
115
116#[derive(Clone, Debug, Error)]
118pub enum ImapMessageStoreError {
119 #[error("IMAP STORE failed: NO {0}")]
121 No(String),
122 #[error("IMAP STORE failed: BAD {0}")]
124 Bad(String),
125 #[error("IMAP STORE failed: BYE {0}")]
127 Bye(String),
128 #[error("IMAP STORE failed: server did not return a tagged response")]
130 MissingTagged,
131 #[error("IMAP STORE failed: {0}")]
133 Send(#[from] ImapSendError),
134}
135
136#[derive(Clone, Debug, Default, Eq, PartialEq)]
138pub struct ImapMessageStoreOptions {
139 pub uid: bool,
141}
142
143pub struct ImapMessageStore {
145 state: State,
146}
147
148impl ImapMessageStore {
149 pub fn new(
152 sequence_set: SequenceSet,
153 kind: StoreType,
154 flags: Vec<Flag<'static>>,
155 opts: ImapMessageStoreOptions,
156 ) -> Self {
157 let command = Command {
158 tag: TagGenerator::new().generate(),
159 body: CommandBody::Store {
160 modifiers: Default::default(),
161 sequence_set,
162 kind,
163 response: StoreResponse::Answer,
164 flags,
165 uid: opts.uid,
166 },
167 };
168
169 trace!("send IMAP command {command:?}");
170
171 let state = State::Send(ImapSend::new(CommandCodec::new(), command));
172
173 Self { state }
174 }
175}
176
177impl ImapCoroutine for ImapMessageStore {
178 type Yield = ImapYield;
179 type Return =
180 Result<BTreeMap<NonZeroU32, Vec1<MessageDataItem<'static>>>, ImapMessageStoreError>;
181
182 fn resume(
183 &mut self,
184 fragmentizer: &mut Fragmentizer,
185 arg: Option<&[u8]>,
186 ) -> ImapCoroutineState<Self::Yield, Self::Return> {
187 match &mut self.state {
188 State::Send(send) => {
189 let out = imap_try!(send, fragmentizer, arg);
190
191 if let Some(bye) = out.bye {
192 let err = ImapMessageStoreError::Bye(bye.text.to_string());
193 return ImapCoroutineState::Complete(Err(err));
194 }
195
196 let Some(Tagged { body, .. }) = out.tagged else {
197 let err = ImapMessageStoreError::MissingTagged;
198 return ImapCoroutineState::Complete(Err(err));
199 };
200
201 let mut data: BTreeMap<NonZeroU32, Vec1<MessageDataItem<'static>>> =
202 BTreeMap::new();
203 for res in out.data {
204 if let Data::Fetch { seq, items } = res {
205 data.insert(seq, items);
206 }
207 }
208
209 match body.kind {
210 StatusKind::Ok => ImapCoroutineState::Complete(Ok(data)),
211 StatusKind::No => {
212 let err = ImapMessageStoreError::No(body.text.to_string());
213 ImapCoroutineState::Complete(Err(err))
214 }
215 StatusKind::Bad => {
216 let err = ImapMessageStoreError::Bad(body.text.to_string());
217 ImapCoroutineState::Complete(Err(err))
218 }
219 }
220 }
221 }
222 }
223}
224
225pub struct ImapMessageStoreSilent {
227 state: State,
228}
229
230impl ImapMessageStoreSilent {
231 pub fn new(
234 sequence_set: SequenceSet,
235 kind: StoreType,
236 flags: Vec<Flag<'static>>,
237 opts: ImapMessageStoreOptions,
238 ) -> Self {
239 let command = Command {
240 tag: TagGenerator::new().generate(),
241 body: CommandBody::Store {
242 modifiers: Default::default(),
243 sequence_set,
244 kind,
245 response: StoreResponse::Silent,
246 flags,
247 uid: opts.uid,
248 },
249 };
250
251 trace!("send IMAP command {command:?}");
252
253 let state = State::Send(ImapSend::new(CommandCodec::new(), command));
254
255 Self { state }
256 }
257}
258
259impl ImapCoroutine for ImapMessageStoreSilent {
260 type Yield = ImapYield;
261 type Return = Result<(), ImapMessageStoreError>;
262
263 fn resume(
264 &mut self,
265 fragmentizer: &mut Fragmentizer,
266 arg: Option<&[u8]>,
267 ) -> ImapCoroutineState<Self::Yield, Self::Return> {
268 match &mut self.state {
269 State::Send(send) => {
270 let out = imap_try!(send, fragmentizer, arg);
271
272 if let Some(bye) = out.bye {
273 let err = ImapMessageStoreError::Bye(bye.text.to_string());
274 return ImapCoroutineState::Complete(Err(err));
275 }
276
277 let Some(Tagged { body, .. }) = out.tagged else {
278 let err = ImapMessageStoreError::MissingTagged;
279 return ImapCoroutineState::Complete(Err(err));
280 };
281
282 match body.kind {
283 StatusKind::Ok => ImapCoroutineState::Complete(Ok(())),
284 StatusKind::No => {
285 let err = ImapMessageStoreError::No(body.text.to_string());
286 ImapCoroutineState::Complete(Err(err))
287 }
288 StatusKind::Bad => {
289 let err = ImapMessageStoreError::Bad(body.text.to_string());
290 ImapCoroutineState::Complete(Err(err))
291 }
292 }
293 }
294 }
295 }
296}
297
298enum State {
299 Send(ImapSend<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, format, vec, vec::Vec};
315
316 use crate::rfc3501::store::*;
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(
402 cor: &mut ImapMessageStore,
403 frag: &mut Fragmentizer,
404 arg: Option<&[u8]>,
405 ) -> Vec<u8> {
406 match cor.resume(frag, arg) {
407 ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
408 state => panic!("expected WantsWrite, got {state:?}"),
409 }
410 }
411
412 fn expect_wants_read_echo(cor: &mut ImapMessageStore, frag: &mut Fragmentizer) {
413 match cor.resume(frag, None) {
414 ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
415 state => panic!("expected WantsRead, got {state:?}"),
416 }
417 }
418
419 fn expect_complete_ok_echo(
420 cor: &mut ImapMessageStore,
421 frag: &mut Fragmentizer,
422 reply: &[u8],
423 ) -> BTreeMap<NonZeroU32, Vec1<MessageDataItem<'static>>> {
424 match cor.resume(frag, Some(reply)) {
425 ImapCoroutineState::Complete(Ok(value)) => value,
426 state => panic!("expected Complete(Ok), got {state:?}"),
427 }
428 }
429
430 fn expect_complete_err_echo(
431 cor: &mut ImapMessageStore,
432 frag: &mut Fragmentizer,
433 reply: &[u8],
434 ) -> ImapMessageStoreError {
435 match cor.resume(frag, Some(reply)) {
436 ImapCoroutineState::Complete(Err(err)) => err,
437 state => panic!("expected Complete(Err), got {state:?}"),
438 }
439 }
440
441 fn expect_wants_write_silent(
442 cor: &mut ImapMessageStoreSilent,
443 frag: &mut Fragmentizer,
444 arg: Option<&[u8]>,
445 ) -> Vec<u8> {
446 match cor.resume(frag, arg) {
447 ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
448 state => panic!("expected WantsWrite, got {state:?}"),
449 }
450 }
451
452 fn expect_wants_read_silent(cor: &mut ImapMessageStoreSilent, frag: &mut Fragmentizer) {
453 match cor.resume(frag, None) {
454 ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
455 state => panic!("expected WantsRead, got {state:?}"),
456 }
457 }
458
459 fn expect_complete_ok_silent(
460 cor: &mut ImapMessageStoreSilent,
461 frag: &mut Fragmentizer,
462 reply: &[u8],
463 ) {
464 match cor.resume(frag, Some(reply)) {
465 ImapCoroutineState::Complete(Ok(())) => {}
466 state => panic!("expected Complete(Ok), got {state:?}"),
467 }
468 }
469
470 fn first_word(line: &str) -> &str {
471 line.split_whitespace()
472 .next()
473 .expect("first whitespace-separated token")
474 }
475}