Skip to main content

eml_codec/imf/
address.rs

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/// Address (section 3.4 of RFC5322)
77///
78/// ```abnf
79///    address         =   mailbox / group
80/// ```
81#[instrument_input("tracing")]
82pub fn address(input: &[u8]) -> IResult<&[u8], AddressRef<'_>> {
83    alt((into(mailbox), into(group)))(input)
84}
85
86/// Group
87///
88/// ```abnf
89///    group           =   display-name ":" [group-list] ";" [CFWS]
90///    display-name    =   phrase
91/// ```
92#[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/// Group list
107///
108/// ```abnf
109///    group-list      =   mailbox-list / CFWS / obs-group-list
110///    obs-group-list  =   1*([CFWS] ",") [CFWS]
111/// ```
112#[instrument_input("tracing")]
113pub fn group_list(input: &[u8]) -> IResult<&[u8], Option<MailboxList<'_>>> {
114    mailbox_list_nullable(input)
115}
116
117/// Address list
118///
119/// ```abnf
120///   address-list    =   (address *("," address)) / obs-addr-list
121///   obs-addr-list   =   *([CFWS] ",") address *("," [address / CFWS])
122/// ```
123#[instrument_input("tracing")]
124pub fn address_list(input: &[u8]) -> IResult<&[u8], Vec<AddressRef<'_>>> {
125    // NOTE: should we try to recover from individually broken addresses?
126    // (see e.g. identification::nullable_msg_list)
127    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}