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