Skip to main content

io_email/envelope/m2dir/
list.rs

1//! m2dir envelope-list coroutine: M2dirEntryList over the entry
2//! directory then a batched WantsFileRead for message bytes plus the
3//! .meta sidecar; headers parse via [`mail_parser`].
4//!
5//! Sorted by Date: header descending, paginated 1-indexed.
6//!
7//! # Example
8//!
9//! ```rust,ignore
10//! use io_email::envelope::m2dir::list::M2dirEnvelopeList;
11//!
12//! let envs = client.run(M2dirEnvelopeList::new(&client.root, "INBOX", Some(1), Some(50), false)?)?;
13//! ```
14
15use alloc::{collections::BTreeSet, vec::Vec};
16use core::mem;
17use std::path::PathBuf;
18
19use io_m2dir::{
20    coroutine::*,
21    entry::{
22        list::{
23            M2dirEntryList as InnerList, M2dirEntryListError as InnerErr,
24            M2dirEntryListOptions as InnerOpts,
25        },
26        types::M2dirEntry,
27    },
28    flag::types::M2dirFlags,
29    m2dir::types::M2dir,
30    path::M2dirPath,
31};
32use log::trace;
33use mail_parser::MessageParser;
34use thiserror::Error;
35
36use crate::{
37    envelope::types::Envelope,
38    m2dir::convert::{InvalidMailboxName, envelope_from, paginate, resolve_mailbox},
39};
40
41/// Errors produced by [`M2dirEnvelopeList`].
42#[derive(Debug, Error)]
43pub enum M2dirEnvelopeListError {
44    #[error(transparent)]
45    List(#[from] InnerErr),
46    #[error(transparent)]
47    InvalidMailbox(#[from] InvalidMailboxName),
48    #[error("coroutine was resumed with an M2dirArg variant it did not request")]
49    UnexpectedArg,
50    #[error("coroutine was resumed after completion")]
51    ResumedAfterDone,
52    #[error("failed to parse m2dir message at {0:?}")]
53    Parse(M2dirPath),
54}
55
56/// I/O-free coroutine listing every message in an m2dir, sorted by
57/// Date: descending then paginated.
58pub struct M2dirEnvelopeList {
59    state: State,
60    m2dir: M2dir,
61    page: Option<u32>,
62    page_size: Option<u32>,
63    with_attachment: bool,
64}
65
66impl M2dirEnvelopeList {
67    pub fn new(
68        root: impl Into<PathBuf>,
69        mailbox: &str,
70        page: Option<u32>,
71        page_size: Option<u32>,
72        with_attachment: bool,
73    ) -> Result<Self, M2dirEnvelopeListError> {
74        trace!("prepare m2dir envelope listing");
75        let m2dir = resolve_mailbox(root, mailbox)?;
76        let inner = InnerList::new(m2dir.clone(), InnerOpts::default());
77        Ok(Self {
78            state: State::Listing(inner),
79            m2dir,
80            page,
81            page_size,
82            with_attachment,
83        })
84    }
85}
86
87enum State {
88    Listing(InnerList),
89    Reading(Vec<M2dirEntry>),
90    Done,
91}
92
93/// Reads a .meta/<id>.flags file (one flag per non-empty trimmed line).
94fn parse_meta_flags(bytes: &[u8]) -> M2dirFlags {
95    let Ok(text) = core::str::from_utf8(bytes) else {
96        return M2dirFlags::default();
97    };
98    text.lines()
99        .map(str::trim)
100        .filter(|line| !line.is_empty())
101        .collect()
102}
103
104impl M2dirCoroutine for M2dirEnvelopeList {
105    type Yield = M2dirYield;
106    type Return = Result<Vec<Envelope>, M2dirEnvelopeListError>;
107
108    fn resume(&mut self, arg: Option<M2dirArg>) -> M2dirCoroutineState<Self::Yield, Self::Return> {
109        match mem::replace(&mut self.state, State::Done) {
110            State::Listing(mut inner) => match inner.resume(arg) {
111                M2dirCoroutineState::Yielded(y) => {
112                    self.state = State::Listing(inner);
113                    M2dirCoroutineState::Yielded(y)
114                }
115                M2dirCoroutineState::Complete(Ok(entries)) => {
116                    if entries.is_empty() {
117                        return M2dirCoroutineState::Complete(Ok(Vec::new()));
118                    }
119                    let mut paths: BTreeSet<M2dirPath> = BTreeSet::new();
120                    for entry in &entries {
121                        paths.insert(entry.path().clone());
122                        paths.insert(self.m2dir.flags_path(entry.id()));
123                    }
124                    self.state = State::Reading(entries);
125                    M2dirCoroutineState::Yielded(M2dirYield::WantsFileRead(paths))
126                }
127                M2dirCoroutineState::Complete(Err(err)) => {
128                    M2dirCoroutineState::Complete(Err(err.into()))
129                }
130            },
131            State::Reading(entries) => {
132                let Some(M2dirArg::FileRead(mut contents)) = arg else {
133                    self.state = State::Reading(entries);
134                    return M2dirCoroutineState::Complete(Err(
135                        M2dirEnvelopeListError::UnexpectedArg,
136                    ));
137                };
138                let parser = MessageParser::default();
139                let mut envelopes: Vec<Envelope> = Vec::with_capacity(entries.len());
140                for entry in entries {
141                    let Some(body) = contents.remove(entry.path()) else {
142                        continue;
143                    };
144                    let flags_bytes = contents
145                        .remove(&self.m2dir.flags_path(entry.id()))
146                        .unwrap_or_default();
147                    let flags = parse_meta_flags(&flags_bytes);
148                    let parsed = if self.with_attachment {
149                        parser.parse(&body)
150                    } else {
151                        parser.parse_headers(&body)
152                    };
153                    let Some(parsed) = parsed else {
154                        return M2dirCoroutineState::Complete(Err(M2dirEnvelopeListError::Parse(
155                            entry.path().clone(),
156                        )));
157                    };
158                    let mut envelope = envelope_from(&entry, &flags, &parsed);
159                    if self.with_attachment {
160                        envelope.has_attachment = Some(parsed.attachment_count() > 0);
161                    }
162                    envelopes.push(envelope);
163                }
164                envelopes.sort_by(|a, b| b.date.cmp(&a.date));
165                M2dirCoroutineState::Complete(Ok(paginate(envelopes, self.page, self.page_size)))
166            }
167            State::Done => {
168                M2dirCoroutineState::Complete(Err(M2dirEnvelopeListError::ResumedAfterDone))
169            }
170        }
171    }
172}