use nom::{
IResult, Parser,
branch::alt,
bytes::complete::take_while,
character::complete::{char, one_of, satisfy},
combinator::{eof, map, opt, recognize},
multi::{fold_many0, many0, many1, separated_list0},
sequence::{delimited, pair, preceded, separated_pair, terminated},
};
use super::{rfc2234, rfc5336};
fn no_ws_ctl(input: &str) -> IResult<&str, char> {
satisfy(|c| matches!(u32::from(c), 1..=8 | 11 | 12 | 14..=31 | 127)).parse(input)
}
fn text(input: &str) -> IResult<&str, char> {
satisfy(|c| matches!(u32::from(c), 1..=9 | 11 | 12 | 14..=127)).parse(input)
}
fn quoted_pair(input: &str) -> IResult<&str, char> {
preceded(char('\\'), text).parse(input)
}
pub(super) fn fws(input: &str) -> IResult<&str, Option<char>> {
terminated(opt(rfc2234::wsp), take_while(|c| c == ' ' || c == '\t')).parse(input)
}
pub(super) fn cfws(input: &str) -> IResult<&str, Option<char>> {
fws(input)
}
pub(super) fn atext(input: &str) -> IResult<&str, char> {
alt((
rfc2234::alpha,
rfc2234::digit,
one_of("!#$%&'*+-/=?^_`{|}~"),
rfc5336::utf8_non_ascii,
))
.parse(input)
}
pub(super) fn atom(input: &str) -> IResult<&str, String> {
preceded(cfws, map(recognize(many1(atext)), str::to_owned)).parse(input)
}
pub(super) fn dot_atom(input: &str) -> IResult<&str, String> {
preceded(cfws, dot_atom_text).parse(input)
}
pub(super) fn dot_atom_text(input: &str) -> IResult<&str, String> {
map(
recognize(pair(many1(atext), many0(pair(char('.'), many1(atext))))),
str::to_owned,
)
.parse(input)
}
fn qtext(input: &str) -> IResult<&str, char> {
alt((
satisfy(|c| matches!(u32::from(c), 33 | 35..=91 | 93..=126)),
no_ws_ctl,
))
.parse(input)
}
pub(super) fn qcontent(input: &str) -> IResult<&str, char> {
alt((qtext, quoted_pair, rfc5336::utf8_non_ascii)).parse(input)
}
fn quoted_string(input: &str) -> IResult<&str, String> {
delimited(
rfc2234::dquote,
fold_many0(pair(fws, qcontent), String::new, |mut acc, (ws, c)| {
if let Some(ws_char) = ws {
acc.push(ws_char);
}
acc.push(c);
acc
}),
preceded(fws, rfc2234::dquote),
)
.parse(input)
}
fn word(input: &str) -> IResult<&str, String> {
alt((quoted_string, atom)).parse(input)
}
fn phrase(input: &str) -> IResult<&str, String> {
alt((obs_phrase, map(many1(word), |words| words.join(" ")))).parse(input)
}
pub(crate) fn mailbox(input: &str) -> IResult<&str, (Option<String>, (String, String))> {
delimited(
take_while(char::is_whitespace),
alt((name_addr, map(addr_spec, |addr| (None, addr)))),
(take_while(char::is_whitespace), eof),
)
.parse(input)
}
fn name_addr(input: &str) -> IResult<&str, (Option<String>, (String, String))> {
pair(opt(display_name), angle_addr).parse(input)
}
fn angle_addr(input: &str) -> IResult<&str, (String, String)> {
delimited((cfws, char('<')), addr_spec, (char('>'), cfws)).parse(input)
}
fn display_name(input: &str) -> IResult<&str, String> {
phrase(input)
}
#[allow(clippy::type_complexity)]
pub(crate) fn mailbox_list(input: &str) -> IResult<&str, Vec<(Option<String>, (String, String))>> {
terminated(
separated_list0(
delimited(
take_while(char::is_whitespace),
char(','),
take_while(char::is_whitespace),
),
alt((name_addr, map(addr_spec, |addr| (None, addr)))),
),
eof,
)
.parse(input)
}
pub(super) fn addr_spec(input: &str) -> IResult<&str, (String, String)> {
separated_pair(local_part, char('@'), domain).parse(input)
}
pub(super) fn local_part(input: &str) -> IResult<&str, String> {
alt((dot_atom, quoted_string, obs_local_part)).parse(input)
}
pub(super) fn domain(input: &str) -> IResult<&str, String> {
alt((dot_atom, obs_domain)).parse(input)
}
fn obs_phrase(input: &str) -> IResult<&str, String> {
map(
pair(
word,
many0(pair(
opt(fws),
alt((word, map(char('.'), |c| c.to_string()))),
)),
),
|(mut result, rest)| {
for (ws, part) in rest {
if ws.flatten().is_some() {
result.push(' ');
}
result.push_str(&part);
}
result
},
)
.parse(input)
}
pub(super) fn obs_local_part(input: &str) -> IResult<&str, String> {
map(
pair(word, many0(pair(char('.'), word))),
|(mut result, rest)| {
for (_, word) in rest {
result.push('.');
result.push_str(&word);
}
result
},
)
.parse(input)
}
pub(super) fn obs_domain(input: &str) -> IResult<&str, String> {
map(
pair(atom, many0(pair(char('.'), atom))),
|(mut result, rest)| {
for (_, atom) in rest {
result.push('.');
result.push_str(&atom);
}
result
},
)
.parse(input)
}