Skip to main content

io_smtp/rfc5321/types/
domain.rs

1//! SMTP domain (RFC 5321 ยง4.1.2).
2//!
3//! The dotted hostname production used by greetings, hello commands
4//! and mailbox addresses.
5
6use core::fmt;
7
8use alloc::{borrow::Cow, vec::Vec};
9
10use bounded_static_derive::ToStatic;
11use chumsky::prelude::*;
12
13/// A domain name (hostname).
14#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash, ToStatic)]
15pub struct SmtpDomain<'a>(pub Cow<'a, str>);
16
17impl SmtpDomain<'_> {
18    /// Parses a domain from raw bytes, consuming the whole input.
19    pub fn parse<'a>(bytes: &'a [u8]) -> Result<SmtpDomain<'a>, Vec<Rich<'a, u8>>> {
20        parsers::domain()
21            .then_ignore(end())
22            .parse(bytes)
23            .into_result()
24    }
25}
26
27impl fmt::Display for SmtpDomain<'_> {
28    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
29        write!(f, "{}", self.0)
30    }
31}
32
33impl<'a> From<SmtpDomain<'a>> for Cow<'a, str> {
34    fn from(domain: SmtpDomain<'a>) -> Self {
35        domain.0
36    }
37}
38
39impl AsRef<str> for SmtpDomain<'_> {
40    fn as_ref(&self) -> &str {
41        self.0.as_ref()
42    }
43}
44
45pub(crate) mod parsers {
46    //! Chumsky parser for the SMTP domain.
47
48    use core::str::from_utf8;
49
50    use alloc::borrow::Cow;
51
52    use chumsky::prelude::*;
53
54    use crate::{rfc5321::SmtpDomain, utils::parsers::Extra};
55
56    /// SMTP domain parser.
57    ///
58    /// ```abnf
59    /// Domain         = sub-domain *("." sub-domain)
60    /// sub-domain     = Let-dig [Ldh-str]
61    /// Let-dig        = ALPHA / DIGIT
62    /// Ldh-str        = *( ALPHA / DIGIT / "-" ) Let-dig
63    /// ```
64    pub(crate) fn domain<'a>() -> impl Parser<'a, &'a [u8], SmtpDomain<'a>, Extra<'a>> + Clone {
65        // NOTE: sub-domain = Let-dig [Ldh-str]
66        let sub_domain = any()
67            .filter(|b: &u8| b.is_ascii_alphanumeric())
68            .then(
69                any()
70                    .filter(|b: &u8| b.is_ascii_alphanumeric() || *b == b'-')
71                    .repeated()
72                    .to_slice(),
73            )
74            .to_slice();
75
76        // NOTE: Domain = sub-domain *("." sub-domain)
77        sub_domain
78            .then(
79                just(b'.')
80                    .then(any().filter(|b: &u8| b.is_ascii_alphanumeric()))
81                    .then(
82                        any()
83                            .filter(|b: &u8| b.is_ascii_alphanumeric() || *b == b'-')
84                            .repeated()
85                            .to_slice(),
86                    )
87                    .to_slice()
88                    .repeated()
89                    .to_slice(),
90            )
91            .to_slice()
92            .try_map(|bytes: &[u8], span| {
93                from_utf8(bytes)
94                    .map_err(|_| Rich::custom(span, "invalid UTF-8 in domain"))
95                    .map(Cow::from)
96                    .map(SmtpDomain)
97            })
98            .labelled("domain")
99    }
100}