eml-codec 0.4.0

Email enCOder DECoder in Rust. Support Internet Message Format and MIME (RFC 822, 5322, 2045, 2046, 2047, 2048, 2049, 6532).
Documentation
#[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()
        })
    }
}

/// Address (section 3.4 of RFC5322)
///
/// ```abnf
///    address         =   mailbox / group
/// ```
#[instrument_input("tracing")]
pub fn address(input: &[u8]) -> IResult<&[u8], AddressRef<'_>> {
    alt((into(mailbox), into(group)))(input)
}

/// Group
///
/// ```abnf
///    group           =   display-name ":" [group-list] ";" [CFWS]
///    display-name    =   phrase
/// ```
#[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),
        },
    ))
}

/// Group list
///
/// ```abnf
///    group-list      =   mailbox-list / CFWS / obs-group-list
///    obs-group-list  =   1*([CFWS] ",") [CFWS]
/// ```
#[instrument_input("tracing")]
pub fn group_list(input: &[u8]) -> IResult<&[u8], Option<MailboxList<'_>>> {
    mailbox_list_nullable(input)
}

/// Address list
///
/// ```abnf
///   address-list    =   (address *("," address)) / obs-addr-list
///   obs-addr-list   =   *([CFWS] ",") address *("," [address / CFWS])
/// ```
#[instrument_input("tracing")]
pub fn address_list(input: &[u8]) -> IResult<&[u8], Vec<AddressRef<'_>>> {
    // NOTE: should we try to recover from individually broken addresses?
    // (see e.g. identification::nullable_msg_list)
    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,
            })],
        )
    }
}