Skip to main content

io_smtp/rfc5321/types/
atom.rs

1//! SMTP atom (RFC 5321 ยง4.1.2).
2//!
3//! The bare unquoted string production used by local parts and ESMTP
4//! parameter keywords.
5
6use core::{fmt, ops::Deref};
7
8use alloc::{borrow::Cow, vec::Vec};
9
10use bounded_static_derive::ToStatic;
11use chumsky::prelude::*;
12
13/// An SMTP atom.
14#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash, ToStatic)]
15pub struct SmtpAtom<'a>(pub(crate) Cow<'a, str>);
16
17impl SmtpAtom<'_> {
18    /// Parses an atom from raw bytes, consuming the whole input.
19    pub fn parse<'a>(bytes: &'a [u8]) -> Result<SmtpAtom<'a>, Vec<Rich<'a, u8>>> {
20        parsers::atom()
21            .then_ignore(end())
22            .parse(bytes)
23            .into_result()
24    }
25}
26
27impl fmt::Display for SmtpAtom<'_> {
28    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
29        write!(f, "{}", self.0)
30    }
31}
32
33impl<'a> Deref for SmtpAtom<'a> {
34    type Target = str;
35
36    fn deref(&self) -> &Self::Target {
37        self.0.as_ref()
38    }
39}
40
41pub(crate) mod parsers {
42    //! Chumsky parser for the SMTP atom.
43
44    use core::str::from_utf8;
45
46    use alloc::borrow::Cow;
47
48    use chumsky::prelude::*;
49
50    use crate::{rfc5321::SmtpAtom, utils::parsers::Extra};
51
52    /// SMTP atom parser.
53    ///
54    /// ```abnf
55    /// Atom           = 1*atext
56    /// atext          = ALPHA / DIGIT /
57    ///                  "!" / "#" / "$" / "%" / "&" / "'" / "*" /
58    ///                  "+" / "-" / "/" / "=" / "?" / "^" / "_" /
59    ///                  "`" / "{" / "|" / "}" / "~"
60    /// ```
61    pub(crate) fn atom<'a>() -> impl Parser<'a, &'a [u8], SmtpAtom<'a>, Extra<'a>> + Clone {
62        any()
63            .filter(|b| match b {
64                b if b.is_ascii_alphanumeric() => true,
65                b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' => true,
66                b'+' | b'-' | b'/' | b'=' | b'?' | b'^' | b'_' => true,
67                b'`' | b'{' | b'|' | b'}' | b'~' => true,
68                _ => false,
69            })
70            .repeated()
71            .at_least(1)
72            .to_slice()
73            .try_map(|bytes: &[u8], span| {
74                from_utf8(bytes)
75                    .map_err(|_| Rich::custom(span, "invalid UTF-8 in atom"))
76                    .map(Cow::from)
77                    .map(SmtpAtom)
78            })
79            .labelled("atom")
80    }
81}