#[cfg(feature = "arbitrary")]
use arbitrary::Arbitrary;
use bounded_static::ToStatic;
use nom::{
branch::alt,
bytes::complete::tag,
combinator::{into, map, map_opt, opt},
multi::separated_list1,
sequence::tuple,
IResult,
};
#[cfg(feature = "arbitrary")]
use crate::fuzz_eq::FuzzEq;
use crate::i18n::ContainsUtf8;
use crate::imf::mailbox::{mailbox, mailbox_list_nullable, MailboxList, MailboxRef};
use crate::print::{print_seq, Formatter, Print};
use crate::text::misc_token::{phrase, Phrase};
use crate::text::whitespace::cfws;
use crate::utils::vec_filter_none_nonempty;
use eml_codec_derives::instrument_input;
#[derive(Clone, ContainsUtf8, Debug, PartialEq, ToStatic)]
#[cfg_attr(feature = "arbitrary", derive(Arbitrary, FuzzEq))]
pub struct GroupRef<'a> {
pub name: Phrase<'a>,
pub participants: Option<MailboxList<'a>>,
}
impl<'a> Print for GroupRef<'a> {
fn print(&self, fmt: &mut impl Formatter) {
self.name.print(fmt);
fmt.write_bytes(b":");
if let Some(mboxs) = &self.participants {
mboxs.print(fmt);
}
fmt.write_bytes(b";")
}
}
#[derive(Clone, ContainsUtf8, Debug, PartialEq, ToStatic)]
#[cfg_attr(feature = "arbitrary", derive(Arbitrary, FuzzEq))]
pub enum AddressRef<'a> {
Single(MailboxRef<'a>),
Many(GroupRef<'a>),
}
impl<'a> From<MailboxRef<'a>> for AddressRef<'a> {
fn from(mx: MailboxRef<'a>) -> Self {
AddressRef::Single(mx)
}
}
impl<'a> From<GroupRef<'a>> for AddressRef<'a> {
fn from(grp: GroupRef<'a>) -> Self {
AddressRef::Many(grp)
}
}
impl<'a> Print for AddressRef<'a> {
fn print(&self, fmt: &mut impl Formatter) {
match self {
AddressRef::Single(mbox) => mbox.print(fmt),
AddressRef::Many(group) => group.print(fmt),
}
}
}
pub type AddressList<'a> = Vec<AddressRef<'a>>;
impl<'a> Print for AddressList<'a> {
fn print(&self, fmt: &mut impl Formatter) {
print_seq(fmt, self, |fmt| {
fmt.write_bytes(b",");
fmt.write_fws()
})
}
}
#[instrument_input("tracing")]
pub fn address(input: &[u8]) -> IResult<&[u8], AddressRef<'_>> {
alt((into(mailbox), into(group)))(input)
}
#[instrument_input("tracing")]
pub fn group(input: &[u8]) -> IResult<&[u8], GroupRef<'_>> {
let (input, (grp_name, _, grp_list, _, _)) =
tuple((phrase, tag(":"), opt(group_list), tag(";"), opt(cfws)))(input)?;
Ok((
input,
GroupRef {
name: grp_name,
participants: grp_list.unwrap_or(None),
},
))
}
#[instrument_input("tracing")]
pub fn group_list(input: &[u8]) -> IResult<&[u8], Option<MailboxList<'_>>> {
mailbox_list_nullable(input)
}
#[instrument_input("tracing")]
pub fn address_list(input: &[u8]) -> IResult<&[u8], Vec<AddressRef<'_>>> {
map_opt(
separated_list1(
tag(","),
alt((map(address, Some), map(opt(cfws), |_| None))),
),
vec_filter_none_nonempty,
)(input)
}
#[instrument_input("tracing")]
pub fn empty_address_list(input: &[u8]) -> IResult<&[u8], Vec<AddressRef<'_>>> {
map(opt(cfws), |_| vec![])(input)
}
#[instrument_input("tracing")]
pub fn nullable_address_list(input: &[u8]) -> IResult<&[u8], Vec<AddressRef<'_>>> {
alt((address_list, empty_address_list))(input)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::imf::mailbox::{AddrSpec, Domain, LocalPart, LocalPartToken};
use crate::print::tests::print_to_vec;
use crate::text::charset::EmailCharset;
use crate::text::misc_token::{Phrase, PhraseToken, Word};
use crate::text::words::Atom;
fn address_list_parsed_printed(addrlist: &[u8], printed: &[u8], parsed: AddressList<'_>) {
assert_eq!(address_list(addrlist).unwrap(), (&b""[..], parsed.clone()));
let reprinted = print_to_vec(parsed);
assert_eq!(
String::from_utf8_lossy(&reprinted),
String::from_utf8_lossy(printed)
);
}
fn address_list_reprinted(addrlist: &[u8], printed: &[u8]) {
let (input, parsed) = address_list(addrlist).unwrap();
assert!(input.is_empty());
let reprinted = print_to_vec(parsed);
assert_eq!(
String::from_utf8_lossy(&reprinted),
String::from_utf8_lossy(printed)
);
}
#[test]
fn test_address_list() {
address_list_parsed_printed(
r#"A Group:Ed Jones <c@a.test>,joe@where.test,John <jdoe@one.test>;, Mary Smith <mary@x.test>"#.as_bytes(),
r#"A Group:Ed Jones <c@a.test>, joe@where.test, John <jdoe@one.test>;, Mary Smith <mary@x.test>"#.as_bytes(),
vec![
AddressRef::Many(GroupRef {
name: Phrase(vec![
PhraseToken::Word(Word::Atom(Atom("A"[..].into()))),
PhraseToken::Word(Word::Atom(Atom("Group"[..].into()))),
]),
participants: Some(MailboxList(vec![
MailboxRef {
name: Some(Phrase(vec![
PhraseToken::Word(Word::Atom(Atom("Ed"[..].into()))),
PhraseToken::Word(Word::Atom(Atom("Jones"[..].into()))),
])),
addrspec: AddrSpec {
local_part: LocalPart(vec![LocalPartToken::Word(Word::Atom(Atom("c"[..].into())))]),
domain: Domain::Atoms(vec![Atom("a"[..].into()), Atom("test"[..].into())]),
},
},
MailboxRef {
name: None,
addrspec: AddrSpec {
local_part: LocalPart(vec![LocalPartToken::Word(Word::Atom(Atom("joe"[..].into())))]),
domain: Domain::Atoms(vec![Atom("where"[..].into()), Atom("test"[..].into())])
},
},
MailboxRef {
name: Some(Phrase(vec![
PhraseToken::Word(Word::Atom(Atom("John"[..].into()))),
])),
addrspec: AddrSpec {
local_part: LocalPart(vec![LocalPartToken::Word(Word::Atom(Atom("jdoe"[..].into())))]),
domain: Domain::Atoms(vec![Atom("one"[..].into()), Atom("test"[..].into())])
},
},
])),
}),
AddressRef::Single(MailboxRef {
name: Some(Phrase(vec![
PhraseToken::Word(Word::Atom(Atom("Mary"[..].into()))),
PhraseToken::Word(Word::Atom(Atom("Smith"[..].into()))),
])),
addrspec: AddrSpec {
local_part: LocalPart(vec![LocalPartToken::Word(Word::Atom(Atom("mary"[..].into())))]),
domain: Domain::Atoms(vec![Atom("x"[..].into()), Atom("test"[..].into())])
},
}),
],
);
}
#[test]
fn test_address_list_obs() {
address_list_reprinted(
br#" ,,A Group:Ed Jones <c@a.test>,,,,joe@where.test,John <jdoe@one.test>;, Mary Smith <mary@x.test>,,"#,
br#"A Group:Ed Jones <c@a.test>, joe@where.test, John <jdoe@one.test>;, Mary Smith <mary@x.test>"#,
)
}
use crate::text::encoding::{EncodedWord, EncodedWordToken, QuotedChunk, QuotedWord};
use crate::text::quoted::QuotedString;
#[test]
fn test_strange_groups() {
address_list_parsed_printed(
br#""Colleagues": "James Smythe" <james@vandelay.com>;, Friends:
jane@example.com, =?UTF-8?Q?John_Sm=C3=AEth?= <john@example.com>;"#,
br#""Colleagues":"James Smythe" <james@vandelay.com>;, Friends:jane@example.com, =?UTF-8?Q?John_Sm=C3=AEth?= <john@example.com>;"#,
vec![
AddressRef::Many(GroupRef {
name: Phrase(vec![
PhraseToken::Word(Word::Quoted(QuotedString(vec!["Colleagues"[..].into()]))),
]),
participants: Some(MailboxList(vec![MailboxRef {
name: Some(Phrase(vec![
PhraseToken::Word(Word::Quoted(QuotedString(vec![
"James"[..].into(),
" "[..].into(),
"Smythe"[..].into(),
])))])),
addrspec: AddrSpec {
local_part: LocalPart(vec![LocalPartToken::Word(Word::Atom(
Atom("james"[..].into())
))]),
domain: Domain::Atoms(vec![Atom("vandelay"[..].into()), Atom("com"[..].into())]),
}
},])),
}),
AddressRef::Many(GroupRef {
name: Phrase(vec![PhraseToken::Word(Word::Atom(Atom("Friends"[..].into())))]),
participants: Some(MailboxList(vec![
MailboxRef {
name: None,
addrspec: AddrSpec {
local_part: LocalPart(vec![LocalPartToken::Word(Word::Atom(
Atom("jane"[..].into())
))]),
domain: Domain::Atoms(vec![Atom("example"[..].into()), Atom("com"[..].into())]),
}
},
MailboxRef {
name: Some(Phrase(vec![PhraseToken::Encoded(EncodedWord(vec![
EncodedWordToken::Quoted(
QuotedWord {
enc: EmailCharset::utf8(),
chunks: vec![
QuotedChunk::Safe(b"John"[..].into()),
QuotedChunk::Space,
QuotedChunk::Safe(b"Sm"[..].into()),
QuotedChunk::Encoded(vec![0xc3, 0xae]),
QuotedChunk::Safe(b"th"[..].into()),
]
}
)
]))])),
addrspec: AddrSpec {
local_part: LocalPart(vec![LocalPartToken::Word(Word::Atom(
Atom("john"[..].into())
))]),
domain: Domain::Atoms(vec![Atom("example"[..].into()), Atom("com"[..].into())]),
}
},
]))
}),
],
);
address_list_parsed_printed(
b"group:;",
b"group:;",
vec![AddressRef::Many(GroupRef {
name: Phrase(vec![PhraseToken::Word(Word::Atom(Atom("group".into())))]),
participants: None,
})],
);
address_list_parsed_printed(
b"group: \r\n ;",
b"group:;",
vec![AddressRef::Many(GroupRef {
name: Phrase(vec![PhraseToken::Word(Word::Atom(Atom("group".into())))]),
participants: None,
})],
);
}
#[test]
fn test_obs_groups() {
address_list_parsed_printed(
b"group: ,, \r\n ,,,, ;",
b"group:;",
vec![AddressRef::Many(GroupRef {
name: Phrase(vec![PhraseToken::Word(Word::Atom(Atom("group".into())))]),
participants: None,
})],
);
address_list_parsed_printed(
b"group:,;",
b"group:;",
vec![AddressRef::Many(GroupRef {
name: Phrase(vec![PhraseToken::Word(Word::Atom(Atom("group".into())))]),
participants: None,
})],
)
}
}