1use core::fmt;
71
72use alloc::{format, string::String, string::ToString, vec::Vec};
73
74use imap_codec::{
75 CommandCodec,
76 encode::{Encoder, Fragment},
77 fragmentizer::Fragmentizer,
78 imap_types::{
79 command::{Command, CommandBody},
80 core::{Literal, TagGenerator},
81 extensions::binary::LiteralOrLiteral8,
82 mailbox::Mailbox,
83 response::{Code, Data, StatusKind, Tagged},
84 },
85};
86use log::{debug, trace};
87use thiserror::Error;
88
89use crate::{
90 coroutine::*,
91 imap_try,
92 rfc3501::{
93 append::{ImapMessageAppendOptions, ImapMessageAppendOutput},
94 mailbox::encode_inplace,
95 },
96 send::*,
97};
98
99#[derive(Clone, Debug, Error)]
101pub enum ImapMessageAppendStreamError {
102 #[error("IMAP APPEND failed: NO {0}")]
104 No(String),
105 #[error("IMAP APPEND failed: BAD {0}")]
107 Bad(String),
108 #[error("IMAP APPEND failed: BYE {0}")]
110 Bye(String),
111 #[error("IMAP APPEND failed: server did not return a tagged response")]
113 MissingTagged,
114 #[error("IMAP APPEND failed: message source delivered fewer octets than declared")]
117 ShortMessage,
118 #[error("IMAP APPEND failed: {0}")]
120 Send(#[from] ImapSendError),
121}
122
123#[derive(Debug)]
125pub enum ImapMessageAppendStreamYield {
126 WantsRead,
128 WantsWrite(Vec<u8>),
130 WantsStream,
133}
134
135impl From<ImapYield> for ImapMessageAppendStreamYield {
136 fn from(yielded: ImapYield) -> Self {
137 match yielded {
138 ImapYield::WantsRead => Self::WantsRead,
139 ImapYield::WantsWrite(bytes) => Self::WantsWrite(bytes),
140 }
141 }
142}
143
144pub struct ImapMessageAppendStream {
146 state: State,
147 header: Option<Vec<u8>>,
148 crlf: Option<Vec<u8>>,
149 command: Command<'static>,
150 non_sync: bool,
151 stream_pending: bool,
152}
153
154impl ImapMessageAppendStream {
155 pub fn new(mut mailbox: Mailbox<'static>, len: u32, opts: ImapMessageAppendOptions) -> Self {
162 encode_inplace(&mut mailbox);
163
164 let command = Command {
169 tag: TagGenerator::new().generate(),
170 body: CommandBody::Append {
171 mailbox,
172 flags: opts.flags,
173 date: opts.date,
174 message: LiteralOrLiteral8::Literal(Literal::unvalidated_non_sync(Vec::new())),
175 },
176 };
177
178 trace!("send IMAP command {command:?}");
179
180 let fragments: Vec<Fragment> = CommandCodec::new().encode(&command).collect();
181
182 let last = fragments
187 .iter()
188 .rposition(|fragment| matches!(fragment, Fragment::Literal { .. }))
189 .expect("APPEND always encodes a message literal");
190
191 let mut header = Vec::new();
192 let mut crlf = Vec::new();
193
194 for (index, fragment) in fragments.into_iter().enumerate() {
195 match fragment {
196 Fragment::Line { data } if index < last => header.extend(data),
197 Fragment::Line { data } => crlf.extend(data),
198 Fragment::Literal { data, .. } if index < last => header.extend(data),
201 Fragment::Literal { .. } => {}
202 }
203 }
204
205 const EMPTY_LITERAL: &[u8] = b"{0+}\r\n";
209 debug_assert!(header.ends_with(EMPTY_LITERAL));
210 header.truncate(header.len() - EMPTY_LITERAL.len());
211
212 if opts.non_sync {
213 header.extend_from_slice(format!("{{{len}+}}\r\n").as_bytes());
214 } else {
215 header.extend_from_slice(format!("{{{len}}}\r\n").as_bytes());
216 }
217
218 Self {
219 state: State::WriteHeader,
220 header: Some(header),
221 crlf: Some(crlf),
222 command,
223 non_sync: opts.non_sync,
224 stream_pending: false,
225 }
226 }
227}
228
229impl ImapCoroutine for ImapMessageAppendStream {
230 type Yield = ImapMessageAppendStreamYield;
231 type Return = Result<ImapMessageAppendOutput, ImapMessageAppendStreamError>;
232
233 fn resume(
234 &mut self,
235 fragmentizer: &mut Fragmentizer,
236 arg: Option<&[u8]>,
237 ) -> ImapCoroutineState<Self::Yield, Self::Return> {
238 loop {
239 match &mut self.state {
240 State::WriteHeader => {
241 let header = self.header.take().expect("header written once");
242
243 self.state = if self.non_sync {
247 State::Stream
248 } else {
249 State::Continuation(ImapSend::receive(self.command.clone()))
250 };
251 debug!("{}", self.state);
252
253 return ImapCoroutineState::Yielded(ImapMessageAppendStreamYield::WantsWrite(
254 header,
255 ));
256 }
257 State::Continuation(recv) => {
258 let out = imap_try!(recv, fragmentizer, arg);
259
260 if let Some(bye) = out.bye {
261 let err = ImapMessageAppendStreamError::Bye(bye.text.to_string());
262 return ImapCoroutineState::Complete(Err(err));
263 }
264
265 if let Some(Tagged { body, .. }) = out.tagged {
268 let err = match body.kind {
269 StatusKind::No => {
270 ImapMessageAppendStreamError::No(body.text.to_string())
271 }
272 _ => ImapMessageAppendStreamError::Bad(body.text.to_string()),
273 };
274 return ImapCoroutineState::Complete(Err(err));
275 }
276
277 self.state = State::Stream;
278 debug!("{}", self.state);
279 }
280 State::Stream => {
281 if self.stream_pending {
282 self.stream_pending = false;
283
284 if matches!(arg, Some(&[])) {
285 let err = ImapMessageAppendStreamError::ShortMessage;
286 return ImapCoroutineState::Complete(Err(err));
287 }
288
289 self.state = State::WriteCrlf;
290 debug!("{}", self.state);
291 continue;
292 }
293
294 self.stream_pending = true;
295 return ImapCoroutineState::Yielded(ImapMessageAppendStreamYield::WantsStream);
296 }
297 State::WriteCrlf => {
298 let crlf = self.crlf.take().expect("crlf written once");
299 self.state = State::Recv(ImapSend::receive(self.command.clone()));
300 debug!("{}", self.state);
301 return ImapCoroutineState::Yielded(ImapMessageAppendStreamYield::WantsWrite(
302 crlf,
303 ));
304 }
305 State::Recv(recv) => {
306 let out = imap_try!(recv, fragmentizer, arg);
307
308 if let Some(bye) = out.bye {
309 let err = ImapMessageAppendStreamError::Bye(bye.text.to_string());
310 return ImapCoroutineState::Complete(Err(err));
311 }
312
313 let Some(Tagged { body, .. }) = out.tagged else {
314 let err = ImapMessageAppendStreamError::MissingTagged;
315 return ImapCoroutineState::Complete(Err(err));
316 };
317
318 let mut exists = None;
319
320 for data in out.data {
321 if let Data::Exists(seq) = data {
322 exists = Some(seq);
323 }
324 }
325
326 return match body.kind {
327 StatusKind::Ok => {
328 let appenduid =
329 if let Some(Code::AppendUid { uid_validity, uid }) = body.code {
330 Some((uid_validity.get(), uid.get()))
331 } else {
332 None
333 };
334 ImapCoroutineState::Complete(Ok((exists, appenduid)))
335 }
336 StatusKind::No => {
337 let err = ImapMessageAppendStreamError::No(body.text.to_string());
338 ImapCoroutineState::Complete(Err(err))
339 }
340 StatusKind::Bad => {
341 let err = ImapMessageAppendStreamError::Bad(body.text.to_string());
342 ImapCoroutineState::Complete(Err(err))
343 }
344 };
345 }
346 }
347 }
348 }
349}
350
351enum State {
352 WriteHeader,
353 Continuation(ImapSend<CommandCodec>),
354 Stream,
355 WriteCrlf,
356 Recv(ImapSend<CommandCodec>),
357}
358
359impl fmt::Display for State {
360 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
361 match self {
362 Self::WriteHeader => f.write_str("write append header"),
363 Self::Continuation(_) => f.write_str("await continuation"),
364 Self::Stream => f.write_str("stream message"),
365 Self::WriteCrlf => f.write_str("write append crlf"),
366 Self::Recv(_) => f.write_str("receive append response"),
367 }
368 }
369}
370
371#[cfg(test)]
372mod tests {
373 use core::str;
374
375 use alloc::borrow::ToOwned;
376
377 use crate::rfc3501::append_stream::*;
378
379 #[test]
380 fn sync_success_with_appenduid_returns_pair() {
381 let mut append = ImapMessageAppendStream::new(
382 "INBOX".try_into().expect("valid mailbox"),
383 15,
384 ImapMessageAppendOptions::default(),
385 );
386 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
387
388 let header = expect_wants_write(&mut append, &mut frag, None);
389 let line = str::from_utf8(&header).expect("utf8 header");
390 let tag = first_word(line).to_owned();
391 assert!(line.contains("APPEND INBOX"));
392 assert!(line.ends_with("{15}\r\n"));
393
394 expect_wants_read(&mut append, &mut frag, None);
395 expect_wants_stream(
396 &mut append,
397 &mut frag,
398 Some(b"+ Ready for literal data\r\n"),
399 );
400
401 let crlf = expect_wants_write(&mut append, &mut frag, None);
402 assert_eq!(crlf, b"\r\n");
403
404 expect_wants_read(&mut append, &mut frag, None);
405
406 let reply =
407 format!("* 42 EXISTS\r\n{tag} OK [APPENDUID 1700000000 7] APPEND completed\r\n");
408 let (exists, appenduid) =
409 expect_complete_ok(&mut append, &mut frag, Some(reply.as_bytes()));
410 assert_eq!(Some(42), exists);
411 assert_eq!(Some((1700000000, 7)), appenduid);
412 }
413
414 #[test]
415 fn non_sync_streams_without_continuation() {
416 let mut append = ImapMessageAppendStream::new(
417 "INBOX".try_into().expect("valid mailbox"),
418 15,
419 ImapMessageAppendOptions {
420 non_sync: true,
421 ..Default::default()
422 },
423 );
424 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
425
426 let header = expect_wants_write(&mut append, &mut frag, None);
427 let line = str::from_utf8(&header).expect("utf8 header");
428 let tag = first_word(line).to_owned();
429 assert!(line.ends_with("{15+}\r\n"));
430
431 expect_wants_stream(&mut append, &mut frag, None);
433 let crlf = expect_wants_write(&mut append, &mut frag, None);
434 assert_eq!(crlf, b"\r\n");
435
436 expect_wants_read(&mut append, &mut frag, None);
437
438 let reply = format!("{tag} OK APPEND completed\r\n");
439 expect_complete_ok(&mut append, &mut frag, Some(reply.as_bytes()));
440 }
441
442 #[test]
443 fn continuation_no_returns_no_error() {
444 let mut append = ImapMessageAppendStream::new(
445 "INBOX".try_into().expect("valid mailbox"),
446 15,
447 ImapMessageAppendOptions::default(),
448 );
449 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
450
451 let header = expect_wants_write(&mut append, &mut frag, None);
452 let tag = first_word(str::from_utf8(&header).expect("utf8 header")).to_owned();
453
454 expect_wants_read(&mut append, &mut frag, None);
455
456 let reply = format!("{tag} NO over quota\r\n");
457 let err = expect_complete_err(&mut append, &mut frag, Some(reply.as_bytes()));
458 let ImapMessageAppendStreamError::No(text) = err else {
459 panic!("expected ImapMessageAppendStreamError::No, got {err:?}");
460 };
461 assert_eq!(text, "over quota");
462 }
463
464 #[test]
465 fn short_stream_returns_short_message_error() {
466 let mut append = ImapMessageAppendStream::new(
467 "INBOX".try_into().expect("valid mailbox"),
468 15,
469 ImapMessageAppendOptions::default(),
470 );
471 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
472
473 let _ = expect_wants_write(&mut append, &mut frag, None);
474 expect_wants_read(&mut append, &mut frag, None);
475 expect_wants_stream(&mut append, &mut frag, Some(b"+ go\r\n"));
476
477 let err = expect_complete_err(&mut append, &mut frag, Some(&[]));
479 assert!(matches!(err, ImapMessageAppendStreamError::ShortMessage));
480 }
481
482 #[test]
483 fn bye_returns_bye_error() {
484 let mut append = ImapMessageAppendStream::new(
485 "INBOX".try_into().expect("valid mailbox"),
486 15,
487 ImapMessageAppendOptions::default(),
488 );
489 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
490
491 let _ = expect_wants_write(&mut append, &mut frag, None);
492 expect_wants_read(&mut append, &mut frag, None);
493 expect_wants_stream(&mut append, &mut frag, Some(b"+ go\r\n"));
494 let _ = expect_wants_write(&mut append, &mut frag, None);
495 expect_wants_read(&mut append, &mut frag, None);
496
497 let err = expect_complete_err(&mut append, &mut frag, Some(b"* BYE shutting down\r\n"));
498 let ImapMessageAppendStreamError::Bye(text) = err else {
499 panic!("expected ImapMessageAppendStreamError::Bye, got {err:?}");
500 };
501 assert_eq!(text, "shutting down");
502 }
503
504 fn expect_wants_write(
505 cor: &mut ImapMessageAppendStream,
506 frag: &mut Fragmentizer,
507 arg: Option<&[u8]>,
508 ) -> Vec<u8> {
509 match cor.resume(frag, arg) {
510 ImapCoroutineState::Yielded(ImapMessageAppendStreamYield::WantsWrite(bytes)) => bytes,
511 state => panic!("expected WantsWrite, got {state:?}"),
512 }
513 }
514
515 fn expect_wants_read(
516 cor: &mut ImapMessageAppendStream,
517 frag: &mut Fragmentizer,
518 arg: Option<&[u8]>,
519 ) {
520 match cor.resume(frag, arg) {
521 ImapCoroutineState::Yielded(ImapMessageAppendStreamYield::WantsRead) => {}
522 state => panic!("expected WantsRead, got {state:?}"),
523 }
524 }
525
526 fn expect_wants_stream(
527 cor: &mut ImapMessageAppendStream,
528 frag: &mut Fragmentizer,
529 arg: Option<&[u8]>,
530 ) {
531 match cor.resume(frag, arg) {
532 ImapCoroutineState::Yielded(ImapMessageAppendStreamYield::WantsStream) => {}
533 state => panic!("expected WantsStream, got {state:?}"),
534 }
535 }
536
537 fn expect_complete_ok(
538 cor: &mut ImapMessageAppendStream,
539 frag: &mut Fragmentizer,
540 arg: Option<&[u8]>,
541 ) -> ImapMessageAppendOutput {
542 match cor.resume(frag, arg) {
543 ImapCoroutineState::Complete(Ok(value)) => value,
544 state => panic!("expected Complete(Ok), got {state:?}"),
545 }
546 }
547
548 fn expect_complete_err(
549 cor: &mut ImapMessageAppendStream,
550 frag: &mut Fragmentizer,
551 arg: Option<&[u8]>,
552 ) -> ImapMessageAppendStreamError {
553 match cor.resume(frag, arg) {
554 ImapCoroutineState::Complete(Err(err)) => err,
555 state => panic!("expected Complete(Err), got {state:?}"),
556 }
557 }
558
559 fn first_word(line: &str) -> &str {
560 line.split_whitespace()
561 .next()
562 .expect("first whitespace-separated token")
563 }
564}