Skip to main content

io_email/imap/
convert.rs

1//! Conversions between IMAP wire types and the shared LCD types.
2//!
3//! Each helper returns a typed "invalid input" marker so each
4//! coroutine can fold it into its own error enum.
5
6use alloc::{
7    string::{String, ToString},
8    vec::Vec,
9};
10use core::num::NonZeroU32;
11
12use io_imap::types::{
13    core::Atom, flag::Flag as ImapFlag, mailbox::Mailbox as ImapMailbox, sequence::SequenceSet,
14};
15
16use crate::flag::types::{Flag, IanaFlag};
17
18/// `name` could not be encoded as an IMAP mailbox token.
19#[derive(Debug)]
20pub struct InvalidMailboxName(pub String);
21
22/// `ids` was empty, contained a non-`u32`, or could not assemble into
23/// a [`SequenceSet`].
24#[derive(Debug)]
25pub enum InvalidUidSet {
26    Empty,
27    Invalid(String),
28}
29
30/// Parses a shared mailbox name into an IMAP Mailbox token.
31pub fn parse_mailbox(name: &str) -> Result<ImapMailbox<'static>, InvalidMailboxName> {
32    String::from(name)
33        .try_into()
34        .map_err(|_| InvalidMailboxName(name.to_string()))
35}
36
37/// Parses a list of stringified UIDs into an IMAP [`SequenceSet`].
38pub fn parse_uids(ids: &[&str]) -> Result<SequenceSet, InvalidUidSet> {
39    if ids.is_empty() {
40        return Err(InvalidUidSet::Empty);
41    }
42
43    let uids: Vec<NonZeroU32> = ids
44        .iter()
45        .map(|s| {
46            s.parse::<NonZeroU32>()
47                .map_err(|_| InvalidUidSet::Invalid((*s).to_string()))
48        })
49        .collect::<Result<_, _>>()?;
50
51    SequenceSet::try_from(uids).map_err(|_| InvalidUidSet::Empty)
52}
53
54/// Maps a shared [`Flag`] to its IMAP wire counterpart.
55///
56/// IANA flags become the matching system flag; custom keywords pass
57/// through as Keyword atoms, with a sanitised fallback when the raw
58/// spelling is not atom-safe.
59pub fn flag_from(flag: &Flag) -> ImapFlag<'static> {
60    match flag.iana() {
61        Some(IanaFlag::Seen) => ImapFlag::Seen,
62        Some(IanaFlag::Answered) => ImapFlag::Answered,
63        Some(IanaFlag::Flagged) => ImapFlag::Flagged,
64        Some(IanaFlag::Draft) => ImapFlag::Draft,
65        Some(IanaFlag::Deleted) => ImapFlag::Deleted,
66        Some(_) => ImapFlag::keyword(
67            Atom::try_from(String::from(flag.raw()))
68                .expect("canonical IANA keyword is a valid IMAP atom"),
69        ),
70        None => match Atom::try_from(String::from(flag.raw())) {
71            Ok(atom) => ImapFlag::keyword(atom),
72            Err(_) => ImapFlag::keyword(
73                Atom::try_from(sanitise_atom(flag.raw()))
74                    .expect("sanitised atom contains only atom-safe ASCII"),
75            ),
76        },
77    }
78}
79
80/// Replaces every non-atom-safe byte with `_` so a keyword with
81/// spaces, controls or `()<>{}` survives IMAP STORE.
82fn sanitise_atom(raw: &str) -> String {
83    raw.chars()
84        .map(|c| {
85            if c.is_ascii()
86                && !c.is_control()
87                && !matches!(
88                    c,
89                    ' ' | '(' | ')' | '{' | '%' | '*' | '"' | '\\' | ']' | '\x7f'
90                )
91            {
92                c
93            } else {
94                '_'
95            }
96        })
97        .collect()
98}