1use core::{fmt, num::NonZeroU32};
73
74use alloc::{string::String, string::ToString, vec, vec::Vec};
75
76use imap_codec::{
77 CommandCodec, ResponseCodec,
78 encode::Encoder,
79 fragmentizer::{FragmentInfo, Fragmentizer},
80 imap_types::{
81 command::{Command, CommandBody},
82 core::TagGenerator,
83 fetch::{MacroOrMessageDataItemNames, MessageDataItemName},
84 response::{Response, Status, StatusKind},
85 sequence::{SeqOrUid, SequenceSet},
86 },
87};
88use log::{debug, trace};
89use thiserror::Error;
90
91use crate::coroutine::*;
92
93#[derive(Clone, Debug, Error)]
95pub enum ImapMessageFetchStreamError {
96 #[error("IMAP FETCH failed: NO {0}")]
98 No(String),
99 #[error("IMAP FETCH failed: BAD {0}")]
101 Bad(String),
102 #[error("IMAP FETCH failed: BYE {0}")]
104 Bye(String),
105 #[error("IMAP FETCH failed: server did not return a tagged response")]
107 MissingTagged,
108 #[error("IMAP FETCH failed: stream ended before the declared body length")]
111 ShortBody,
112 #[error("IMAP FETCH failed: unexpected literal in response trailer")]
115 UnexpectedLiteral,
116}
117
118#[derive(Debug)]
120pub enum ImapMessageFetchStreamYield {
121 WantsRead,
123 WantsWrite(Vec<u8>),
125 BodyChunk(Vec<u8>),
128 WantsStream {
131 len: u32,
133 },
134}
135
136pub struct ImapMessageFetchStream {
138 state: State,
139 command: Option<Vec<u8>>,
140 pending: Vec<u8>,
141 remaining: u32,
142 stream_pending: bool,
143 codec: ResponseCodec,
144}
145
146impl ImapMessageFetchStream {
147 pub fn new(id: NonZeroU32, uid: bool) -> Self {
150 let command = Command {
151 tag: TagGenerator::new().generate(),
152 body: CommandBody::Fetch {
153 sequence_set: SequenceSet::from(SeqOrUid::from(id)),
154 macro_or_item_names: MacroOrMessageDataItemNames::MessageDataItemNames(vec![
155 MessageDataItemName::BodyExt {
156 section: None,
157 partial: None,
158 peek: true,
159 },
160 ]),
161 uid,
162 modifiers: Vec::new(),
163 },
164 };
165
166 trace!("send IMAP command {command:?}");
167
168 let command = CommandCodec::new().encode(&command).dump();
169
170 Self {
171 state: State::SendCommand,
172 command: Some(command),
173 pending: Vec::new(),
174 remaining: 0,
175 stream_pending: false,
176 codec: ResponseCodec::new(),
177 }
178 }
179}
180
181impl ImapCoroutine for ImapMessageFetchStream {
182 type Yield = ImapMessageFetchStreamYield;
183 type Return = Result<(), ImapMessageFetchStreamError>;
184
185 fn resume(
186 &mut self,
187 fragmentizer: &mut Fragmentizer,
188 mut arg: Option<&[u8]>,
189 ) -> ImapCoroutineState<Self::Yield, Self::Return> {
190 loop {
191 match self.state {
192 State::SendCommand => {
193 let command = self.command.take().expect("command sent once");
194 self.state = State::Header;
195 debug!("{}", self.state);
196 return ImapCoroutineState::Yielded(ImapMessageFetchStreamYield::WantsWrite(
197 command,
198 ));
199 }
200 State::Header => {
201 if let Some(bytes) = arg.take() {
202 if bytes.is_empty() {
203 let err = ImapMessageFetchStreamError::MissingTagged;
204 return ImapCoroutineState::Complete(Err(err));
205 }
206 self.pending.extend_from_slice(bytes);
207 }
208
209 loop {
210 let Some(nl) = self.pending.iter().position(|&b| b == b'\n') else {
211 return ImapCoroutineState::Yielded(
212 ImapMessageFetchStreamYield::WantsRead,
213 );
214 };
215
216 let line: Vec<u8> = self.pending.drain(..=nl).collect();
217 fragmentizer.enqueue_bytes(&line);
218
219 match fragmentizer.progress() {
220 Some(FragmentInfo::Line {
223 announcement: Some(announcement),
224 ..
225 }) => {
226 self.remaining = announcement.length;
227 self.state = State::Stream;
228 debug!("{}", self.state);
229 break;
230 }
231 Some(FragmentInfo::Line {
235 announcement: None, ..
236 }) => {
237 if let Some(result) = self.decode_terminal(fragmentizer) {
238 return result;
239 }
240 }
241 _ => {}
242 }
243 }
244 }
245 State::Stream => {
246 if self.remaining == 0 {
247 fragmentizer.skip_message();
250 self.state = State::Trailer;
251 debug!("{}", self.state);
252 continue;
253 }
254
255 if !self.pending.is_empty() {
256 let take = (self.remaining as usize).min(self.pending.len());
257 let chunk: Vec<u8> = self.pending.drain(..take).collect();
258 self.remaining -= take as u32;
259 return ImapCoroutineState::Yielded(
260 ImapMessageFetchStreamYield::BodyChunk(chunk),
261 );
262 }
263
264 if self.stream_pending {
265 self.stream_pending = false;
266 if matches!(arg.take(), Some(&[])) {
267 let err = ImapMessageFetchStreamError::ShortBody;
268 return ImapCoroutineState::Complete(Err(err));
269 }
270 self.remaining = 0;
271 continue;
272 }
273
274 self.stream_pending = true;
275 return ImapCoroutineState::Yielded(ImapMessageFetchStreamYield::WantsStream {
276 len: self.remaining,
277 });
278 }
279 State::Trailer => {
280 if let Some(bytes) = arg.take() {
281 if bytes.is_empty() {
282 let err = ImapMessageFetchStreamError::MissingTagged;
283 return ImapCoroutineState::Complete(Err(err));
284 }
285 self.pending.extend_from_slice(bytes);
286 }
287
288 loop {
289 let Some(nl) = self.pending.iter().position(|&b| b == b'\n') else {
290 return ImapCoroutineState::Yielded(
291 ImapMessageFetchStreamYield::WantsRead,
292 );
293 };
294
295 let line: Vec<u8> = self.pending.drain(..=nl).collect();
296 fragmentizer.enqueue_bytes(&line);
297
298 match fragmentizer.progress() {
299 Some(FragmentInfo::Line {
300 announcement: Some(_),
301 ..
302 }) => {
303 let err = ImapMessageFetchStreamError::UnexpectedLiteral;
304 return ImapCoroutineState::Complete(Err(err));
305 }
306 Some(FragmentInfo::Line {
310 announcement: None, ..
311 }) => {
312 if let Some(result) = self.decode_terminal(fragmentizer) {
313 return result;
314 }
315 }
316 _ => {}
317 }
318 }
319 }
320 }
321 }
322 }
323}
324
325impl ImapMessageFetchStream {
326 fn decode_terminal(
332 &self,
333 fragmentizer: &Fragmentizer,
334 ) -> Option<
335 ImapCoroutineState<ImapMessageFetchStreamYield, Result<(), ImapMessageFetchStreamError>>,
336 > {
337 match fragmentizer.decode_message(&self.codec) {
338 Ok(Response::Status(Status::Tagged(tagged))) => {
339 let text = tagged.body.text.to_string();
340 let result = match tagged.body.kind {
341 StatusKind::Ok => Ok(()),
342 StatusKind::No => Err(ImapMessageFetchStreamError::No(text)),
343 StatusKind::Bad => Err(ImapMessageFetchStreamError::Bad(text)),
344 };
345 Some(ImapCoroutineState::Complete(result))
346 }
347 Ok(Response::Status(Status::Bye(bye))) => {
348 let err = ImapMessageFetchStreamError::Bye(bye.text.to_string());
349 Some(ImapCoroutineState::Complete(Err(err)))
350 }
351 _ => None,
352 }
353 }
354}
355
356#[derive(Clone, Copy)]
357enum State {
358 SendCommand,
359 Header,
360 Stream,
361 Trailer,
362}
363
364impl fmt::Display for State {
365 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
366 match self {
367 Self::SendCommand => f.write_str("send fetch command"),
368 Self::Header => f.write_str("parse fetch header"),
369 Self::Stream => f.write_str("stream body"),
370 Self::Trailer => f.write_str("parse fetch trailer"),
371 }
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use core::str;
378
379 use alloc::{borrow::ToOwned, format};
380
381 use crate::rfc3501::fetch_stream::*;
382
383 #[test]
384 fn streams_body_in_one_read() {
385 let mut cor = ImapMessageFetchStream::new(NonZeroU32::new(1).unwrap(), true);
386 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
387
388 let cmd = expect_wants_write(&mut cor, &mut frag, None);
389 let line = str::from_utf8(&cmd).expect("utf8 command");
390 let tag = first_word(line).to_owned();
391 assert!(line.contains("UID FETCH 1 BODY.PEEK[]"));
392
393 expect_wants_read(&mut cor, &mut frag, None);
394
395 let reply = format!("* 1 FETCH (BODY[] {{5}}\r\nhello)\r\n{tag} OK FETCH completed\r\n");
397 let chunk = expect_body_chunk(&mut cor, &mut frag, Some(reply.as_bytes()));
398 assert_eq!(chunk, b"hello");
399
400 expect_complete_ok(&mut cor, &mut frag, None);
403 }
404
405 #[test]
406 fn streams_body_via_wants_stream() {
407 let mut cor = ImapMessageFetchStream::new(NonZeroU32::new(9).unwrap(), false);
408 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
409
410 let cmd = expect_wants_write(&mut cor, &mut frag, None);
411 let line = str::from_utf8(&cmd).expect("utf8 command");
412 let tag = first_word(line).to_owned();
413 assert!(line.contains("FETCH 9 BODY.PEEK[]"));
414 assert!(!line.contains("UID"));
415
416 expect_wants_read(&mut cor, &mut frag, None);
417
418 let len = expect_wants_stream(&mut cor, &mut frag, Some(b"* 9 FETCH (BODY[] {12}\r\n"));
420 assert_eq!(len, 12);
421
422 expect_wants_read(&mut cor, &mut frag, None);
425
426 let reply = format!(")\r\n{tag} OK FETCH completed\r\n");
427 expect_complete_ok(&mut cor, &mut frag, Some(reply.as_bytes()));
428 }
429
430 #[test]
431 fn partial_body_in_header_read_chunks_then_streams() {
432 let mut cor = ImapMessageFetchStream::new(NonZeroU32::new(1).unwrap(), true);
433 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
434
435 let cmd = expect_wants_write(&mut cor, &mut frag, None);
436 let tag = first_word(str::from_utf8(&cmd).expect("utf8 command")).to_owned();
437
438 expect_wants_read(&mut cor, &mut frag, None);
439
440 let chunk = expect_body_chunk(&mut cor, &mut frag, Some(b"* 1 FETCH (BODY[] {5}\r\nhel"));
442 assert_eq!(chunk, b"hel");
443
444 let len = expect_wants_stream(&mut cor, &mut frag, None);
446 assert_eq!(len, 2);
447
448 expect_wants_read(&mut cor, &mut frag, None);
449
450 let reply = format!(")\r\n{tag} OK done\r\n");
451 expect_complete_ok(&mut cor, &mut frag, Some(reply.as_bytes()));
452 }
453
454 #[test]
455 fn missing_message_returns_ok_without_body() {
456 let mut cor = ImapMessageFetchStream::new(NonZeroU32::new(7).unwrap(), true);
457 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
458
459 let cmd = expect_wants_write(&mut cor, &mut frag, None);
460 let tag = first_word(str::from_utf8(&cmd).expect("utf8 command")).to_owned();
461
462 expect_wants_read(&mut cor, &mut frag, None);
463
464 let reply = format!("{tag} OK FETCH completed\r\n");
466 expect_complete_ok(&mut cor, &mut frag, Some(reply.as_bytes()));
467 }
468
469 #[test]
470 fn tagged_no_returns_no_error() {
471 let mut cor = ImapMessageFetchStream::new(NonZeroU32::new(7).unwrap(), true);
472 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
473
474 let cmd = expect_wants_write(&mut cor, &mut frag, None);
475 let tag = first_word(str::from_utf8(&cmd).expect("utf8 command")).to_owned();
476
477 expect_wants_read(&mut cor, &mut frag, None);
478
479 let reply = format!("{tag} NO mailbox not selected\r\n");
480 let err = expect_complete_err(&mut cor, &mut frag, Some(reply.as_bytes()));
481 let ImapMessageFetchStreamError::No(text) = err else {
482 panic!("expected ImapMessageFetchStreamError::No, got {err:?}");
483 };
484 assert_eq!(text, "mailbox not selected");
485 }
486
487 #[test]
488 fn short_stream_returns_short_body() {
489 let mut cor = ImapMessageFetchStream::new(NonZeroU32::new(1).unwrap(), true);
490 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
491
492 let _ = expect_wants_write(&mut cor, &mut frag, None);
493 expect_wants_read(&mut cor, &mut frag, None);
494 let _ = expect_wants_stream(&mut cor, &mut frag, Some(b"* 1 FETCH (BODY[] {12}\r\n"));
495
496 let err = expect_complete_err(&mut cor, &mut frag, Some(&[]));
498 assert!(matches!(err, ImapMessageFetchStreamError::ShortBody));
499 }
500
501 fn expect_wants_write(
502 cor: &mut ImapMessageFetchStream,
503 frag: &mut Fragmentizer,
504 arg: Option<&[u8]>,
505 ) -> Vec<u8> {
506 match cor.resume(frag, arg) {
507 ImapCoroutineState::Yielded(ImapMessageFetchStreamYield::WantsWrite(bytes)) => bytes,
508 state => panic!("expected WantsWrite, got {state:?}"),
509 }
510 }
511
512 fn expect_wants_read(
513 cor: &mut ImapMessageFetchStream,
514 frag: &mut Fragmentizer,
515 arg: Option<&[u8]>,
516 ) {
517 match cor.resume(frag, arg) {
518 ImapCoroutineState::Yielded(ImapMessageFetchStreamYield::WantsRead) => {}
519 state => panic!("expected WantsRead, got {state:?}"),
520 }
521 }
522
523 fn expect_body_chunk(
524 cor: &mut ImapMessageFetchStream,
525 frag: &mut Fragmentizer,
526 arg: Option<&[u8]>,
527 ) -> Vec<u8> {
528 match cor.resume(frag, arg) {
529 ImapCoroutineState::Yielded(ImapMessageFetchStreamYield::BodyChunk(bytes)) => bytes,
530 state => panic!("expected BodyChunk, got {state:?}"),
531 }
532 }
533
534 fn expect_wants_stream(
535 cor: &mut ImapMessageFetchStream,
536 frag: &mut Fragmentizer,
537 arg: Option<&[u8]>,
538 ) -> u32 {
539 match cor.resume(frag, arg) {
540 ImapCoroutineState::Yielded(ImapMessageFetchStreamYield::WantsStream { len }) => len,
541 state => panic!("expected WantsStream, got {state:?}"),
542 }
543 }
544
545 fn expect_complete_ok(
546 cor: &mut ImapMessageFetchStream,
547 frag: &mut Fragmentizer,
548 arg: Option<&[u8]>,
549 ) {
550 match cor.resume(frag, arg) {
551 ImapCoroutineState::Complete(Ok(())) => {}
552 state => panic!("expected Complete(Ok), got {state:?}"),
553 }
554 }
555
556 fn expect_complete_err(
557 cor: &mut ImapMessageFetchStream,
558 frag: &mut Fragmentizer,
559 arg: Option<&[u8]>,
560 ) -> ImapMessageFetchStreamError {
561 match cor.resume(frag, arg) {
562 ImapCoroutineState::Complete(Err(err)) => err,
563 state => panic!("expected Complete(Err), got {state:?}"),
564 }
565 }
566
567 fn first_word(line: &str) -> &str {
568 line.split_whitespace()
569 .next()
570 .expect("first whitespace-separated token")
571 }
572}