Skip to main content

io_email/m2dir/
convert.rs

1//! Shared helpers for the m2dir coroutines: mailbox path resolution,
2//! flag and address conversions, plus `paginate` shared with maildir.
3
4use alloc::{
5    string::{String, ToString},
6    vec::Vec,
7};
8use std::path::PathBuf;
9
10use chrono::DateTime;
11use io_m2dir::{
12    entry::types::M2dirEntry,
13    flag::types::M2dirFlags,
14    m2dir::types::M2dir,
15    path::M2dirPath,
16    store::{M2dirStore, M2dirStoreError},
17};
18use mail_parser::{Address as MailParserAddress, Message as ParsedMessage};
19use thiserror::Error;
20
21use crate::{
22    address::Address,
23    envelope::types::{Envelope, normalize_message_id},
24    flag::types::Flag,
25};
26
27/// Errors produced by the mailbox-path resolver.
28#[derive(Debug, Error)]
29pub enum InvalidMailboxName {
30    #[error(transparent)]
31    Store(#[from] M2dirStoreError),
32}
33
34/// Builds an [`M2dirStore`] from a shared root path.
35pub(crate) fn store_from_root(root: impl Into<PathBuf>) -> M2dirStore {
36    let path: M2dirPath = root.into().into();
37    M2dirStore::from_path(path)
38}
39
40/// Resolves a mailbox name to its on-disk m2dir under the store
41/// root; same `/`-separated convention as io-m2dir.
42pub(crate) fn resolve_mailbox(
43    root: impl Into<PathBuf>,
44    name: &str,
45) -> Result<M2dir, InvalidMailboxName> {
46    let store = store_from_root(root);
47    let path = store.resolve_folder_path(name)?;
48    Ok(M2dir::from_path(path))
49}
50
51/// One .meta/<id>.flags line to shared [`Flag`]; whitespace trimmed.
52pub(crate) fn flag_from_meta_line(line: &str) -> Flag {
53    Flag::from_raw(line.trim())
54}
55
56/// Inverse of [`flag_from_meta_line`]: one canonical line per flag.
57pub(crate) fn flag_to_meta_line(flag: &Flag) -> String {
58    flag.raw().to_string()
59}
60
61/// Shared flag slice to [`M2dirFlags`].
62pub(crate) fn flags_to_m2dir(flags: &[Flag]) -> M2dirFlags {
63    flags.iter().map(flag_to_meta_line).collect()
64}
65
66/// Folds an entry + meta flags + parsed message into an [`Envelope`];
67/// `has_attachment` is left `None` for the caller to fill in.
68pub fn envelope_from(
69    entry: &M2dirEntry,
70    meta: &M2dirFlags,
71    parsed: &ParsedMessage<'_>,
72) -> Envelope {
73    let id = entry.id().to_string();
74    let flags = meta.iter().map(flag_from_meta_line).collect();
75    let subject = parsed.subject().unwrap_or_default().to_string();
76    let from = parsed.from().map(addresses_from).unwrap_or_default();
77    let to = parsed.to().map(addresses_from).unwrap_or_default();
78    let date = parsed
79        .date()
80        .and_then(|d| DateTime::parse_from_rfc3339(&d.to_rfc3339()).ok());
81    let size = parsed.raw_message().len() as u64;
82    let message_id = parsed.message_id().and_then(normalize_message_id);
83    Envelope {
84        id,
85        message_id,
86        flags,
87        subject,
88        from,
89        to,
90        date,
91        size,
92        has_attachment: None,
93    }
94}
95
96/// 1-indexed in-memory pagination; `page_size = None` returns the
97/// full slice; size 0 or a page past the end returns empty.
98pub(crate) fn paginate<T>(items: Vec<T>, page: Option<u32>, page_size: Option<u32>) -> Vec<T> {
99    let Some(size) = page_size else {
100        return items;
101    };
102    if size == 0 {
103        return Vec::new();
104    }
105    let page = page.unwrap_or(1).max(1);
106    let skip = ((page - 1) as usize).saturating_mul(size as usize);
107    if skip >= items.len() {
108        return Vec::new();
109    }
110    items.into_iter().skip(skip).take(size as usize).collect()
111}
112
113fn addresses_from(addrs: &MailParserAddress<'_>) -> Vec<Address> {
114    addrs
115        .clone()
116        .into_list()
117        .into_iter()
118        .filter_map(|a| {
119            let email = a.address?.into_owned();
120            if email.is_empty() {
121                return None;
122            }
123            let name = a.name.map(|s| s.into_owned());
124            Some(Address { name, email })
125        })
126        .collect()
127}