Compare commits

...

3 Commits

Author SHA1 Message Date
Paolo Barbolini
e347cf9ae2 fix 2025-07-05 18:09:14 +02:00
Paolo Barbolini
4c738085e5 clippy 2025-07-05 17:37:35 +02:00
Paolo Barbolini
b752170bd8 refactor: replace chumsky with nom 2025-07-05 17:24:42 +02:00
6 changed files with 194 additions and 183 deletions

85
Cargo.lock generated
View File

@@ -17,18 +17,6 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "ahash"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy 0.7.35",
]
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.3" version = "1.1.3"
@@ -38,12 +26,6 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]] [[package]]
name = "anes" name = "anes"
version = "0.1.6" version = "0.1.6"
@@ -458,16 +440,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chumsky"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9"
dependencies = [
"hashbrown",
"stacker",
]
[[package]] [[package]]
name = "ciborium" name = "ciborium"
version = "0.2.2" version = "0.2.2"
@@ -1044,16 +1016,6 @@ dependencies = [
"crunchy", "crunchy",
] ]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
"allocator-api2",
]
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.4.0" version = "0.4.0"
@@ -1331,7 +1293,6 @@ dependencies = [
"async-trait", "async-trait",
"base64", "base64",
"boring", "boring",
"chumsky",
"criterion", "criterion",
"ed25519-dalek", "ed25519-dalek",
"email-encoding", "email-encoding",
@@ -1765,7 +1726,7 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [ dependencies = [
"zerocopy 0.8.24", "zerocopy",
] ]
[[package]] [[package]]
@@ -1820,15 +1781,6 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "psm"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f58e5423e24c18cc840e1c98370b3993c6649cd1678b4d24318bcf0a083cbe88"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.40" version = "1.0.40"
@@ -2288,19 +2240,6 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "stacker"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "601f9201feb9b09c00266478bf459952b9ef9a6b94edb2f21eba14ab681a60a9"
dependencies = [
"cc",
"cfg-if",
"libc",
"psm",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.6.1" version = "2.6.1"
@@ -2977,33 +2916,13 @@ dependencies = [
"synstructure", "synstructure",
] ]
[[package]]
name = "zerocopy"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"zerocopy-derive 0.7.35",
]
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.24" version = "0.8.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879"
dependencies = [ dependencies = [
"zerocopy-derive 0.8.24", "zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
] ]
[[package]] [[package]]

View File

@@ -20,7 +20,7 @@ maintenance = { status = "actively-developed" }
[dependencies] [dependencies]
email_address = { version = "0.2.1", default-features = false } email_address = { version = "0.2.1", default-features = false }
chumsky = "0.9" nom = "8"
idna = "1" idna = "1"
## tracing support ## tracing support
@@ -40,7 +40,6 @@ serde = { version = "1.0.110", features = ["derive"], optional = true }
serde_json = { version = "1", optional = true } serde_json = { version = "1", optional = true }
# smtp-transport # smtp-transport
nom = { version = "8", optional = true }
hostname = { version = "0.4", optional = true } # feature hostname = { version = "0.4", optional = true } # feature
socket2 = { version = "0.6", optional = true } socket2 = { version = "0.6", optional = true }
url = { version = "2.4", optional = true } url = { version = "2.4", optional = true }
@@ -107,7 +106,7 @@ mime03 = ["dep:mime"]
file-transport = ["dep:uuid", "tokio1_crate?/fs", "tokio1_crate?/io-util"] file-transport = ["dep:uuid", "tokio1_crate?/fs", "tokio1_crate?/io-util"]
file-transport-envelope = ["serde", "dep:serde_json", "file-transport"] file-transport-envelope = ["serde", "dep:serde_json", "file-transport"]
sendmail-transport = ["tokio1_crate?/process", "tokio1_crate?/io-util", "async-std?/unstable"] sendmail-transport = ["tokio1_crate?/process", "tokio1_crate?/io-util", "async-std?/unstable"]
smtp-transport = ["dep:base64", "dep:nom", "dep:socket2", "dep:url", "dep:percent-encoding", "tokio1_crate?/rt", "tokio1_crate?/time", "tokio1_crate?/net"] smtp-transport = ["dep:base64", "dep:socket2", "dep:url", "dep:percent-encoding", "tokio1_crate?/rt", "tokio1_crate?/time", "tokio1_crate?/net"]
pool = ["dep:futures-util"] pool = ["dep:futures-util"]

View File

@@ -3,30 +3,34 @@
//! //!
//! [RFC2234]: https://datatracker.ietf.org/doc/html/rfc2234 //! [RFC2234]: https://datatracker.ietf.org/doc/html/rfc2234
use chumsky::{error::Cheap, prelude::*}; use nom::{
branch::alt,
character::complete::{char, satisfy},
IResult, Parser,
};
// 6.1 Core Rules // 6.1 Core Rules
// https://datatracker.ietf.org/doc/html/rfc2234#section-6.1 // https://datatracker.ietf.org/doc/html/rfc2234#section-6.1
// ALPHA = %x41-5A / %x61-7A ; A-Z / a-z // ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
pub(super) fn alpha() -> impl Parser<char, char, Error = Cheap<char>> { pub(super) fn alpha(input: &str) -> IResult<&str, char> {
filter(|c: &char| c.is_ascii_alphabetic()) satisfy(|c| c.is_ascii_alphabetic()).parse(input)
} }
// DIGIT = %x30-39 // DIGIT = %x30-39
// ; 0-9 // ; 0-9
pub(super) fn digit() -> impl Parser<char, char, Error = Cheap<char>> { pub(super) fn digit(input: &str) -> IResult<&str, char> {
filter(|c: &char| c.is_ascii_digit()) satisfy(|c| c.is_ascii_digit()).parse(input)
} }
// DQUOTE = %x22 // DQUOTE = %x22
// ; " (Double Quote) // ; " (Double Quote)
pub(super) fn dquote() -> impl Parser<char, char, Error = Cheap<char>> { pub(super) fn dquote(input: &str) -> IResult<&str, char> {
just('"') char('"').parse(input)
} }
// WSP = SP / HTAB // WSP = SP / HTAB
// ; white space // ; white space
pub(super) fn wsp() -> impl Parser<char, char, Error = Cheap<char>> { pub(super) fn wsp(input: &str) -> IResult<&str, char> {
choice((just(' '), just('\t'))) alt((char(' '), char('\t'))).parse(input)
} }

View File

@@ -3,7 +3,14 @@
//! //!
//! [RFC2822]: https://datatracker.ietf.org/doc/html/rfc2822 //! [RFC2822]: https://datatracker.ietf.org/doc/html/rfc2822
use chumsky::{error::Cheap, prelude::*}; use nom::{
branch::alt,
character::complete::{char, satisfy},
combinator::{eof, map, opt},
multi::{fold_many0, fold_many1, many0, many1, separated_list0},
sequence::{delimited, pair, preceded, terminated},
IResult, Parser,
};
use super::{rfc2234, rfc5336}; use super::{rfc2234, rfc5336};
@@ -15,8 +22,8 @@ use super::{rfc2234, rfc5336};
// %d12 / ; carriage return, line feed, // %d12 / ; carriage return, line feed,
// %d14-31 / ; and white space characters // %d14-31 / ; and white space characters
// %d127 // %d127
fn no_ws_ctl() -> impl Parser<char, char, Error = Cheap<char>> { fn no_ws_ctl(input: &str) -> IResult<&str, char> {
filter(|c| matches!(u32::from(*c), 1..=8 | 11 | 12 | 14..=31 | 127)) satisfy(|c| matches!(u32::from(c), 1..=8 | 11 | 12 | 14..=31 | 127)).parse(input)
} }
// text = %d1-9 / ; Characters excluding CR and LF // text = %d1-9 / ; Characters excluding CR and LF
@@ -24,16 +31,16 @@ fn no_ws_ctl() -> impl Parser<char, char, Error = Cheap<char>> {
// %d12 / // %d12 /
// %d14-127 / // %d14-127 /
// obs-text // obs-text
fn text() -> impl Parser<char, char, Error = Cheap<char>> { fn text(input: &str) -> IResult<&str, char> {
filter(|c| matches!(u32::from(*c), 1..=9 | 11 | 12 | 14..=127)) satisfy(|c| matches!(u32::from(c), 1..=9 | 11 | 12 | 14..=127)).parse(input)
} }
// 3.2.2. Quoted characters // 3.2.2. Quoted characters
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.2 // https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.2
// quoted-pair = ("\" text) / obs-qp // quoted-pair = ("\" text) / obs-qp
fn quoted_pair() -> impl Parser<char, char, Error = Cheap<char>> { fn quoted_pair(input: &str) -> IResult<&str, char> {
just('\\').ignore_then(text()) preceded(char('\\'), text).parse(input)
} }
// 3.2.3. Folding white space and comments // 3.2.3. Folding white space and comments
@@ -41,17 +48,19 @@ fn quoted_pair() -> impl Parser<char, char, Error = Cheap<char>> {
// FWS = ([*WSP CRLF] 1*WSP) / ; Folding white space // FWS = ([*WSP CRLF] 1*WSP) / ; Folding white space
// obs-FWS // obs-FWS
pub(super) fn fws() -> impl Parser<char, Option<char>, Error = Cheap<char>> { pub(super) fn fws(input: &str) -> IResult<&str, Option<char>> {
rfc2234::wsp() map(
.or_not() pair(opt(rfc2234::wsp), many0(rfc2234::wsp)),
.then_ignore(rfc2234::wsp().ignored().repeated()) |(first, _rest)| first,
)
.parse(input)
} }
// CFWS = *([FWS] comment) (([FWS] comment) / FWS) // CFWS = *([FWS] comment) (([FWS] comment) / FWS)
pub(super) fn cfws() -> impl Parser<char, Option<char>, Error = Cheap<char>> { pub(super) fn cfws(input: &str) -> IResult<&str, Option<char>> {
// TODO: comment are not currently supported, so for now a cfws is // TODO: comment are not currently supported, so for now a cfws is
// the same as a fws. // the same as a fws.
fws() fws(input)
} }
// 3.2.4. Atom // 3.2.4. Atom
@@ -68,13 +77,13 @@ pub(super) fn cfws() -> impl Parser<char, Option<char>, Error = Cheap<char>> {
// "`" / "{" / // "`" / "{" /
// "|" / "}" / // "|" / "}" /
// "~" // "~"
pub(super) fn atext() -> impl Parser<char, char, Error = Cheap<char>> { pub(super) fn atext(input: &str) -> IResult<&str, char> {
choice(( alt((
rfc2234::alpha(), rfc2234::alpha,
rfc2234::digit(), rfc2234::digit,
filter(|c| { satisfy(|c| {
matches!( matches!(
*c, c,
'!' | '#' '!' | '#'
| '$' | '$'
| '%' | '%'
@@ -96,29 +105,64 @@ pub(super) fn atext() -> impl Parser<char, char, Error = Cheap<char>> {
) )
}), }),
// also allow non ASCII UTF8 chars // also allow non ASCII UTF8 chars
rfc5336::utf8_non_ascii(), rfc5336::utf8_non_ascii,
)) ))
.parse(input)
} }
// atom = [CFWS] 1*atext [CFWS] // atom = [CFWS] 1*atext [CFWS]
pub(super) fn atom() -> impl Parser<char, Vec<char>, Error = Cheap<char>> { pub(super) fn atom(input: &str) -> IResult<&str, String> {
cfws().chain(atext().repeated().at_least(1)) map(
pair(
cfws,
fold_many1(atext, String::new, |mut acc, c| {
acc.push(c);
acc
}),
),
|(cfws, mut chars)| {
if let Some(cfws) = cfws {
chars.insert(0, cfws);
}
chars
},
)
.parse(input)
} }
// dot-atom = [CFWS] dot-atom-text [CFWS] // dot-atom = [CFWS] dot-atom-text [CFWS]
pub(super) fn dot_atom() -> impl Parser<char, Vec<char>, Error = Cheap<char>> { pub(super) fn dot_atom(input: &str) -> IResult<&str, String> {
cfws().chain(dot_atom_text()) map(pair(cfws, dot_atom_text), |(_cfws, text)| text).parse(input)
} }
// dot-atom-text = 1*atext *("." 1*atext) // dot-atom-text = 1*atext *("." 1*atext)
pub(super) fn dot_atom_text() -> impl Parser<char, Vec<char>, Error = Cheap<char>> { pub(super) fn dot_atom_text(input: &str) -> IResult<&str, String> {
atext().repeated().at_least(1).chain( map(
just('.') pair(
.chain(atext().repeated().at_least(1)) fold_many1(atext, String::new, |mut acc, c| {
.repeated() acc.push(c);
.at_least(1) acc
.flatten(), }),
many0(map(
pair(
char('.'),
fold_many1(atext, String::new, |mut acc, c| {
acc.push(c);
acc
}),
),
|(dot, chars)| format!("{dot}{chars}"),
)),
),
|(first, rest)| {
let mut result = first;
for part in rest {
result.push_str(&part);
}
result
},
) )
.parse(input)
} }
// 3.2.5. Quoted strings // 3.2.5. Quoted strings
@@ -129,122 +173,168 @@ pub(super) fn dot_atom_text() -> impl Parser<char, Vec<char>, Error = Cheap<char
// %d33 / ; The rest of the US-ASCII // %d33 / ; The rest of the US-ASCII
// %d35-91 / ; characters not including "\" // %d35-91 / ; characters not including "\"
// %d93-126 ; or the quote character // %d93-126 ; or the quote character
fn qtext() -> impl Parser<char, char, Error = Cheap<char>> { fn qtext(input: &str) -> IResult<&str, char> {
choice(( alt((
filter(|c| matches!(u32::from(*c), 33 | 35..=91 | 93..=126)), satisfy(|c| matches!(u32::from(c), 33 | 35..=91 | 93..=126)),
no_ws_ctl(), no_ws_ctl,
)) ))
.parse(input)
} }
// qcontent = qtext / quoted-pair // qcontent = qtext / quoted-pair
pub(super) fn qcontent() -> impl Parser<char, char, Error = Cheap<char>> { pub(super) fn qcontent(input: &str) -> IResult<&str, char> {
choice((qtext(), quoted_pair(), rfc5336::utf8_non_ascii())) alt((qtext, quoted_pair, rfc5336::utf8_non_ascii)).parse(input)
} }
// quoted-string = [CFWS] // quoted-string = [CFWS]
// DQUOTE *([FWS] qcontent) [FWS] DQUOTE // DQUOTE *([FWS] qcontent) [FWS] DQUOTE
// [CFWS] // [CFWS]
fn quoted_string() -> impl Parser<char, Vec<char>, Error = Cheap<char>> { fn quoted_string(input: &str) -> IResult<&str, String> {
rfc2234::dquote() map(
.ignore_then(fws().chain(qcontent()).repeated().flatten()) delimited(
.then_ignore(text::whitespace()) rfc2234::dquote,
.then_ignore(rfc2234::dquote()) fold_many0(
map(pair(fws, qcontent), |(_fws, c)| c),
String::new,
|mut acc, c| {
acc.push(c);
acc
},
),
preceded(many0(satisfy(char::is_whitespace)), rfc2234::dquote),
),
|s| s,
)
.parse(input)
} }
// 3.2.6. Miscellaneous tokens // 3.2.6. Miscellaneous tokens
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.6 // https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.6
// word = atom / quoted-string // word = atom / quoted-string
fn word() -> impl Parser<char, Vec<char>, Error = Cheap<char>> { fn word(input: &str) -> IResult<&str, String> {
choice((quoted_string(), atom())) alt((quoted_string, atom)).parse(input)
} }
// phrase = 1*word / obs-phrase // phrase = 1*word / obs-phrase
fn phrase() -> impl Parser<char, Vec<char>, Error = Cheap<char>> { fn phrase(input: &str) -> IResult<&str, String> {
choice((obs_phrase(), word().repeated().at_least(1).flatten())) alt((obs_phrase, map(many1(word), |words| words.join(" ")))).parse(input)
} }
// 3.4. Address Specification // 3.4. Address Specification
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.4 // https://datatracker.ietf.org/doc/html/rfc2822#section-3.4
// mailbox = name-addr / addr-spec // mailbox = name-addr / addr-spec
pub(crate) fn mailbox() -> impl Parser<char, (Option<String>, (String, String)), Error = Cheap<char>> pub(crate) fn mailbox(input: &str) -> IResult<&str, (Option<String>, (String, String))> {
{ terminated(alt((name_addr, map(addr_spec, |addr| (None, addr)))), eof).parse(input)
choice((name_addr(), addr_spec().map(|addr| (None, addr))))
.padded()
.then_ignore(end())
} }
// name-addr = [display-name] angle-addr // name-addr = [display-name] angle-addr
fn name_addr() -> impl Parser<char, (Option<String>, (String, String)), Error = Cheap<char>> { fn name_addr(input: &str) -> IResult<&str, (Option<String>, (String, String))> {
display_name().collect().or_not().then(angle_addr()) pair(opt(display_name), angle_addr).parse(input)
} }
// angle-addr = [CFWS] "<" addr-spec ">" [CFWS] / obs-angle-addr // angle-addr = [CFWS] "<" addr-spec ">" [CFWS] / obs-angle-addr
fn angle_addr() -> impl Parser<char, (String, String), Error = Cheap<char>> { fn angle_addr(input: &str) -> IResult<&str, (String, String)> {
addr_spec() delimited((cfws, char('<')), addr_spec, (char('>'), cfws)).parse(input)
.delimited_by(just('<').ignored(), just('>').ignored())
.padded()
} }
// display-name = phrase // display-name = phrase
fn display_name() -> impl Parser<char, Vec<char>, Error = Cheap<char>> { fn display_name(input: &str) -> IResult<&str, String> {
phrase() phrase(input)
} }
// mailbox-list = (mailbox *("," mailbox)) / obs-mbox-list // mailbox-list = (mailbox *("," mailbox)) / obs-mbox-list
pub(crate) fn mailbox_list( #[allow(clippy::type_complexity)]
) -> impl Parser<char, Vec<(Option<String>, (String, String))>, Error = Cheap<char>> { pub(crate) fn mailbox_list(input: &str) -> IResult<&str, Vec<(Option<String>, (String, String))>> {
choice((name_addr(), addr_spec().map(|addr| (None, addr)))) terminated(
.separated_by(just(',').padded()) separated_list0(
.then_ignore(end()) delimited(
many0(satisfy(char::is_whitespace)),
char(','),
many0(satisfy(char::is_whitespace)),
),
alt((name_addr, map(addr_spec, |addr| (None, addr)))),
),
eof,
)
.parse(input)
} }
// 3.4.1. Addr-spec specification // 3.4.1. Addr-spec specification
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.4.1 // https://datatracker.ietf.org/doc/html/rfc2822#section-3.4.1
// addr-spec = local-part "@" domain // addr-spec = local-part "@" domain
pub(super) fn addr_spec() -> impl Parser<char, (String, String), Error = Cheap<char>> { pub(super) fn addr_spec(input: &str) -> IResult<&str, (String, String)> {
local_part() pair(terminated(local_part, char('@')), domain).parse(input)
.collect()
.then_ignore(just('@'))
.then(domain().collect())
} }
// local-part = dot-atom / quoted-string / obs-local-part // local-part = dot-atom / quoted-string / obs-local-part
pub(super) fn local_part() -> impl Parser<char, Vec<char>, Error = Cheap<char>> { pub(super) fn local_part(input: &str) -> IResult<&str, String> {
choice((dot_atom(), quoted_string(), obs_local_part())) alt((dot_atom, quoted_string, obs_local_part)).parse(input)
} }
// domain = dot-atom / domain-literal / obs-domain // domain = dot-atom / domain-literal / obs-domain
pub(super) fn domain() -> impl Parser<char, Vec<char>, Error = Cheap<char>> { pub(super) fn domain(input: &str) -> IResult<&str, String> {
// NOTE: omitting domain-literal since it may never be used // NOTE: omitting domain-literal since it may never be used
choice((dot_atom(), obs_domain())) alt((dot_atom, obs_domain)).parse(input)
} }
// 4.1. Miscellaneous obsolete tokens // 4.1. Miscellaneous obsolete tokens
// https://datatracker.ietf.org/doc/html/rfc2822#section-4.1 // https://datatracker.ietf.org/doc/html/rfc2822#section-4.1
// obs-phrase = word *(word / "." / CFWS) // obs-phrase = word *(word / "." / CFWS)
fn obs_phrase() -> impl Parser<char, Vec<char>, Error = Cheap<char>> { fn obs_phrase(input: &str) -> IResult<&str, String> {
// NOTE: the CFWS is already captured by the word, no need to add // NOTE: the CFWS is already captured by the word, no need to add
// it there. // it there.
word().chain( map(
choice((word(), just('.').repeated().exactly(1))) pair(word, many0(alt((word, map(char('.'), |c| c.to_string()))))),
.repeated() |(first, rest)| {
.flatten(), let mut result = first;
for part in rest {
result.push_str(&part);
}
result
},
) )
.parse(input)
} }
// 4.4. Obsolete Addressing // 4.4. Obsolete Addressing
// https://datatracker.ietf.org/doc/html/rfc2822#section-4.4 // https://datatracker.ietf.org/doc/html/rfc2822#section-4.4
// obs-local-part = word *("." word) // obs-local-part = word *("." word)
pub(super) fn obs_local_part() -> impl Parser<char, Vec<char>, Error = Cheap<char>> { pub(super) fn obs_local_part(input: &str) -> IResult<&str, String> {
word().chain(just('.').chain(word()).repeated().flatten()) map(
pair(
word,
many0(map(pair(char('.'), word), |(dot, w)| format!("{dot}{w}"))),
),
|(first, rest)| {
let mut result = first;
for part in rest {
result.push_str(&part);
}
result
},
)
.parse(input)
} }
// obs-domain = atom *("." atom) // obs-domain = atom *("." atom)
pub(super) fn obs_domain() -> impl Parser<char, Vec<char>, Error = Cheap<char>> { pub(super) fn obs_domain(input: &str) -> IResult<&str, String> {
atom().chain(just('.').chain(atom()).repeated().flatten()) map(
pair(
atom,
many0(map(pair(char('.'), atom), |(dot, a)| format!("{dot}{a}"))),
),
|(first, rest)| {
let mut result = first;
for part in rest {
result.push_str(&part);
}
result
},
)
.parse(input)
} }

View File

@@ -3,7 +3,7 @@
//! //!
//! [RFC5336]: https://datatracker.ietf.org/doc/html/rfc5336 //! [RFC5336]: https://datatracker.ietf.org/doc/html/rfc5336
use chumsky::{error::Cheap, prelude::*}; use nom::{character::complete::satisfy, IResult, Parser};
// 3.3. Extended Mailbox Address Syntax // 3.3. Extended Mailbox Address Syntax
// https://datatracker.ietf.org/doc/html/rfc5336#section-3.3 // https://datatracker.ietf.org/doc/html/rfc5336#section-3.3
@@ -12,6 +12,6 @@ use chumsky::{error::Cheap, prelude::*};
// UTF8-2 = <See Section 4 of RFC 3629> // UTF8-2 = <See Section 4 of RFC 3629>
// UTF8-3 = <See Section 4 of RFC 3629> // UTF8-3 = <See Section 4 of RFC 3629>
// UTF8-4 = <See Section 4 of RFC 3629> // UTF8-4 = <See Section 4 of RFC 3629>
pub(super) fn utf8_non_ascii() -> impl Parser<char, char, Error = Cheap<char>> { pub(super) fn utf8_non_ascii(input: &str) -> IResult<&str, char> {
filter(|c: &char| c.len_utf8() > 1) satisfy(|c| c.len_utf8() > 1).parse(input)
} }

View File

@@ -5,7 +5,6 @@ use std::{
str::FromStr, str::FromStr,
}; };
use chumsky::prelude::*;
use email_encoding::headers::writer::EmailWriter; use email_encoding::headers::writer::EmailWriter;
use super::parsers; use super::parsers;
@@ -114,7 +113,7 @@ impl FromStr for Mailbox {
type Err = AddressError; type Err = AddressError;
fn from_str(src: &str) -> Result<Mailbox, Self::Err> { fn from_str(src: &str) -> Result<Mailbox, Self::Err> {
let (name, (user, domain)) = parsers::mailbox().parse(src).map_err(|_errs| { let (_rest, (name, (user, domain))) = parsers::mailbox(src).map_err(|_errs| {
// TODO: improve error management // TODO: improve error management
AddressError::InvalidInput AddressError::InvalidInput
})?; })?;
@@ -345,7 +344,7 @@ impl FromStr for Mailboxes {
fn from_str(src: &str) -> Result<Self, Self::Err> { fn from_str(src: &str) -> Result<Self, Self::Err> {
let mut mailboxes = Vec::new(); let mut mailboxes = Vec::new();
let parsed_mailboxes = parsers::mailbox_list().parse(src).map_err(|_errs| { let (_rest, parsed_mailboxes) = parsers::mailbox_list(src).map_err(|_errs| {
// TODO: improve error management // TODO: improve error management
AddressError::InvalidInput AddressError::InvalidInput
})?; })?;