1#[cfg(feature = "arbitrary")]
2use arbitrary::Arbitrary;
3use bounded_static::ToStatic;
4use nom::{
5 branch::alt,
6 bytes::complete::tag,
7 combinator::{into, map, map_opt, opt},
8 multi::separated_list1,
9 sequence::tuple,
10 IResult,
11};
12
13#[cfg(feature = "arbitrary")]
14use crate::fuzz_eq::FuzzEq;
15use crate::i18n::ContainsUtf8;
16use crate::imf::mailbox::{mailbox, mailbox_list_nullable, MailboxList, MailboxRef};
17use crate::print::{print_seq, Formatter, Print};
18use crate::text::misc_token::{phrase, Phrase};
19use crate::text::whitespace::cfws;
20use crate::utils::vec_filter_none_nonempty;
21use eml_codec_derives::instrument_input;
22
23#[derive(Clone, ContainsUtf8, Debug, PartialEq, ToStatic)]
24#[cfg_attr(feature = "arbitrary", derive(Arbitrary, FuzzEq))]
25pub struct GroupRef<'a> {
26 pub name: Phrase<'a>,
27 pub participants: Option<MailboxList<'a>>,
28}
29impl<'a> Print for GroupRef<'a> {
30 fn print(&self, fmt: &mut impl Formatter) {
31 self.name.print(fmt);
32 fmt.write_bytes(b":");
33 if let Some(mboxs) = &self.participants {
34 mboxs.print(fmt);
35 }
36 fmt.write_bytes(b";")
37 }
38}
39
40#[derive(Clone, ContainsUtf8, Debug, PartialEq, ToStatic)]
41#[cfg_attr(feature = "arbitrary", derive(Arbitrary, FuzzEq))]
42pub enum AddressRef<'a> {
43 Single(MailboxRef<'a>),
44 Many(GroupRef<'a>),
45}
46impl<'a> From<MailboxRef<'a>> for AddressRef<'a> {
47 fn from(mx: MailboxRef<'a>) -> Self {
48 AddressRef::Single(mx)
49 }
50}
51impl<'a> From<GroupRef<'a>> for AddressRef<'a> {
52 fn from(grp: GroupRef<'a>) -> Self {
53 AddressRef::Many(grp)
54 }
55}
56impl<'a> Print for AddressRef<'a> {
57 fn print(&self, fmt: &mut impl Formatter) {
58 match self {
59 AddressRef::Single(mbox) => mbox.print(fmt),
60 AddressRef::Many(group) => group.print(fmt),
61 }
62 }
63}
64
65pub type AddressList<'a> = Vec<AddressRef<'a>>;
66
67impl<'a> Print for AddressList<'a> {
68 fn print(&self, fmt: &mut impl Formatter) {
69 print_seq(fmt, self, |fmt| {
70 fmt.write_bytes(b",");
71 fmt.write_fws()
72 })
73 }
74}
75
76#[instrument_input("tracing")]
82pub fn address(input: &[u8]) -> IResult<&[u8], AddressRef<'_>> {
83 alt((into(mailbox), into(group)))(input)
84}
85
86#[instrument_input("tracing")]
93pub fn group(input: &[u8]) -> IResult<&[u8], GroupRef<'_>> {
94 let (input, (grp_name, _, grp_list, _, _)) =
95 tuple((phrase, tag(":"), opt(group_list), tag(";"), opt(cfws)))(input)?;
96
97 Ok((
98 input,
99 GroupRef {
100 name: grp_name,
101 participants: grp_list.unwrap_or(None),
102 },
103 ))
104}
105
106#[instrument_input("tracing")]
113pub fn group_list(input: &[u8]) -> IResult<&[u8], Option<MailboxList<'_>>> {
114 mailbox_list_nullable(input)
115}
116
117#[instrument_input("tracing")]
124pub fn address_list(input: &[u8]) -> IResult<&[u8], Vec<AddressRef<'_>>> {
125 map_opt(
128 separated_list1(
129 tag(","),
130 alt((map(address, Some), map(opt(cfws), |_| None))),
131 ),
132 vec_filter_none_nonempty,
133 )(input)
134}
135
136#[instrument_input("tracing")]
137pub fn empty_address_list(input: &[u8]) -> IResult<&[u8], Vec<AddressRef<'_>>> {
138 map(opt(cfws), |_| vec![])(input)
139}
140
141#[instrument_input("tracing")]
142pub fn nullable_address_list(input: &[u8]) -> IResult<&[u8], Vec<AddressRef<'_>>> {
143 alt((address_list, empty_address_list))(input)
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use crate::imf::mailbox::{AddrSpec, Domain, LocalPart, LocalPartToken};
150 use crate::print::tests::print_to_vec;
151 use crate::text::charset::EmailCharset;
152 use crate::text::misc_token::{Phrase, PhraseToken, Word};
153 use crate::text::words::Atom;
154
155 fn address_list_parsed_printed(addrlist: &[u8], printed: &[u8], parsed: AddressList<'_>) {
156 assert_eq!(address_list(addrlist).unwrap(), (&b""[..], parsed.clone()));
157 let reprinted = print_to_vec(parsed);
158 assert_eq!(
159 String::from_utf8_lossy(&reprinted),
160 String::from_utf8_lossy(printed)
161 );
162 }
163
164 fn address_list_reprinted(addrlist: &[u8], printed: &[u8]) {
165 let (input, parsed) = address_list(addrlist).unwrap();
166 assert!(input.is_empty());
167 let reprinted = print_to_vec(parsed);
168 assert_eq!(
169 String::from_utf8_lossy(&reprinted),
170 String::from_utf8_lossy(printed)
171 );
172 }
173
174 #[test]
175 fn test_address_list() {
176 address_list_parsed_printed(
177 r#"A Group:Ed Jones <c@a.test>,joe@where.test,John <jdoe@one.test>;, Mary Smith <mary@x.test>"#.as_bytes(),
178 r#"A Group:Ed Jones <c@a.test>, joe@where.test, John <jdoe@one.test>;, Mary Smith <mary@x.test>"#.as_bytes(),
179 vec![
180 AddressRef::Many(GroupRef {
181 name: Phrase(vec![
182 PhraseToken::Word(Word::Atom(Atom("A"[..].into()))),
183 PhraseToken::Word(Word::Atom(Atom("Group"[..].into()))),
184 ]),
185 participants: Some(MailboxList(vec![
186 MailboxRef {
187 name: Some(Phrase(vec![
188 PhraseToken::Word(Word::Atom(Atom("Ed"[..].into()))),
189 PhraseToken::Word(Word::Atom(Atom("Jones"[..].into()))),
190 ])),
191 addrspec: AddrSpec {
192 local_part: LocalPart(vec![LocalPartToken::Word(Word::Atom(Atom("c"[..].into())))]),
193 domain: Domain::Atoms(vec![Atom("a"[..].into()), Atom("test"[..].into())]),
194 },
195 },
196 MailboxRef {
197 name: None,
198 addrspec: AddrSpec {
199 local_part: LocalPart(vec![LocalPartToken::Word(Word::Atom(Atom("joe"[..].into())))]),
200 domain: Domain::Atoms(vec![Atom("where"[..].into()), Atom("test"[..].into())])
201 },
202 },
203 MailboxRef {
204 name: Some(Phrase(vec![
205 PhraseToken::Word(Word::Atom(Atom("John"[..].into()))),
206 ])),
207 addrspec: AddrSpec {
208 local_part: LocalPart(vec![LocalPartToken::Word(Word::Atom(Atom("jdoe"[..].into())))]),
209 domain: Domain::Atoms(vec![Atom("one"[..].into()), Atom("test"[..].into())])
210 },
211 },
212 ])),
213 }),
214 AddressRef::Single(MailboxRef {
215 name: Some(Phrase(vec![
216 PhraseToken::Word(Word::Atom(Atom("Mary"[..].into()))),
217 PhraseToken::Word(Word::Atom(Atom("Smith"[..].into()))),
218 ])),
219 addrspec: AddrSpec {
220 local_part: LocalPart(vec![LocalPartToken::Word(Word::Atom(Atom("mary"[..].into())))]),
221 domain: Domain::Atoms(vec![Atom("x"[..].into()), Atom("test"[..].into())])
222 },
223 }),
224 ],
225 );
226 }
227
228 #[test]
229 fn test_address_list_obs() {
230 address_list_reprinted(
231 br#" ,,A Group:Ed Jones <c@a.test>,,,,joe@where.test,John <jdoe@one.test>;, Mary Smith <mary@x.test>,,"#,
232 br#"A Group:Ed Jones <c@a.test>, joe@where.test, John <jdoe@one.test>;, Mary Smith <mary@x.test>"#,
233 )
234 }
235
236 use crate::text::encoding::{EncodedWord, EncodedWordToken, QuotedChunk, QuotedWord};
237 use crate::text::quoted::QuotedString;
238
239 #[test]
240 fn test_strange_groups() {
241 address_list_parsed_printed(
242 br#""Colleagues": "James Smythe" <james@vandelay.com>;, Friends:
243 jane@example.com, =?UTF-8?Q?John_Sm=C3=AEth?= <john@example.com>;"#,
244 br#""Colleagues":"James Smythe" <james@vandelay.com>;, Friends:jane@example.com, =?UTF-8?Q?John_Sm=C3=AEth?= <john@example.com>;"#,
245 vec![
246 AddressRef::Many(GroupRef {
247 name: Phrase(vec![
248 PhraseToken::Word(Word::Quoted(QuotedString(vec!["Colleagues"[..].into()]))),
249 ]),
250 participants: Some(MailboxList(vec![MailboxRef {
251 name: Some(Phrase(vec![
252 PhraseToken::Word(Word::Quoted(QuotedString(vec![
253 "James"[..].into(),
254 " "[..].into(),
255 "Smythe"[..].into(),
256 ])))])),
257 addrspec: AddrSpec {
258 local_part: LocalPart(vec![LocalPartToken::Word(Word::Atom(
259 Atom("james"[..].into())
260 ))]),
261 domain: Domain::Atoms(vec![Atom("vandelay"[..].into()), Atom("com"[..].into())]),
262 }
263 },])),
264 }),
265 AddressRef::Many(GroupRef {
266 name: Phrase(vec![PhraseToken::Word(Word::Atom(Atom("Friends"[..].into())))]),
267 participants: Some(MailboxList(vec![
268 MailboxRef {
269 name: None,
270 addrspec: AddrSpec {
271 local_part: LocalPart(vec![LocalPartToken::Word(Word::Atom(
272 Atom("jane"[..].into())
273 ))]),
274 domain: Domain::Atoms(vec![Atom("example"[..].into()), Atom("com"[..].into())]),
275 }
276 },
277 MailboxRef {
278 name: Some(Phrase(vec![PhraseToken::Encoded(EncodedWord(vec![
279 EncodedWordToken::Quoted(
280 QuotedWord {
281 enc: EmailCharset::utf8(),
282 chunks: vec![
283 QuotedChunk::Safe(b"John"[..].into()),
284 QuotedChunk::Space,
285 QuotedChunk::Safe(b"Sm"[..].into()),
286 QuotedChunk::Encoded(vec![0xc3, 0xae]),
287 QuotedChunk::Safe(b"th"[..].into()),
288 ]
289 }
290 )
291 ]))])),
292 addrspec: AddrSpec {
293 local_part: LocalPart(vec![LocalPartToken::Word(Word::Atom(
294 Atom("john"[..].into())
295 ))]),
296 domain: Domain::Atoms(vec![Atom("example"[..].into()), Atom("com"[..].into())]),
297 }
298 },
299 ]))
300 }),
301 ],
302 );
303
304 address_list_parsed_printed(
305 b"group:;",
306 b"group:;",
307 vec![AddressRef::Many(GroupRef {
308 name: Phrase(vec![PhraseToken::Word(Word::Atom(Atom("group".into())))]),
309 participants: None,
310 })],
311 );
312
313 address_list_parsed_printed(
314 b"group: \r\n ;",
315 b"group:;",
316 vec![AddressRef::Many(GroupRef {
317 name: Phrase(vec![PhraseToken::Word(Word::Atom(Atom("group".into())))]),
318 participants: None,
319 })],
320 );
321 }
322
323 #[test]
324 fn test_obs_groups() {
325 address_list_parsed_printed(
326 b"group: ,, \r\n ,,,, ;",
327 b"group:;",
328 vec![AddressRef::Many(GroupRef {
329 name: Phrase(vec![PhraseToken::Word(Word::Atom(Atom("group".into())))]),
330 participants: None,
331 })],
332 );
333
334 address_list_parsed_printed(
335 b"group:,;",
336 b"group:;",
337 vec![AddressRef::Many(GroupRef {
338 name: Phrase(vec![PhraseToken::Word(Word::Atom(Atom("group".into())))]),
339 participants: None,
340 })],
341 )
342 }
343}