use nom::{ IResult, branch::alt, bytes::complete::tag, character::complete::satisfy, combinator::{into,map,opt,recognize}, multi::{separated_list1, many0}, sequence::{delimited,pair,preceded,terminated,tuple}, }; use crate::model::{MailboxRef, AddrSpec}; use crate::misc_token::{phrase, word}; use crate::whitespace::{cfws, fws, is_obs_no_ws_ctl}; use crate::words::{atom, dot_atom}; use crate::quoted::quoted_string; /// Mailbox /// /// ```abnf /// mailbox = name-addr / addr-spec /// ``` pub fn mailbox(input: &str) -> IResult<&str, MailboxRef> { alt((name_addr, into(addr_spec)))(input) } /// Name of the email address /// /// ```abnf /// name-addr = [display-name] angle-addr /// ``` fn name_addr(input: &str) -> IResult<&str, MailboxRef> { let (input, name) = opt(phrase)(input)?; let (input, mut mbox) = angle_addr(input)?; mbox.name = name; Ok((input, mbox)) } /// Enclosed addr-spec with < and > /// /// ```abnf /// angle-addr = [CFWS] "<" addr-spec ">" [CFWS] / /// obs-angle-addr /// ``` pub fn angle_addr(input: &str) -> IResult<&str, MailboxRef> { delimited( tuple((opt(cfws), tag("<"), opt(obs_route))), into(addr_spec), pair(tag(">"), opt(cfws)), )(input) } /// obs-route = obs-domain-list ":" fn obs_route(input: &str) -> IResult<&str, Vec> { terminated(obs_domain_list, tag(":"))(input) } /// ```abnf /// obs-domain-list = *(CFWS / ",") "@" domain /// *("," [CFWS] ["@" domain]) /// ``` fn obs_domain_list(input: &str) -> IResult<&str, Vec> { //@FIXME complexity is O(n) in term of domains here. let (input, head) = preceded(pair(many0(alt((recognize(cfws), tag(",")))), tag("@")), domain_part)(input)?; let (input, mut rest) = obs_domain_list_rest(input)?; rest.insert(0, head); Ok(("", rest)) } fn obs_domain_list_rest(input: &str) -> IResult<&str, Vec> { map( many0(preceded( pair(tag(","), opt(cfws)), opt(preceded(tag("@"), domain_part)), )), |v: Vec>| v.into_iter().flatten().collect() )(input) } /// Add-spec /// /// ```abnf /// addr-spec = local-part "@" domain /// ``` pub fn addr_spec(input: &str) -> IResult<&str, AddrSpec> { let (input, (local, _, domain)) = tuple((local_part, tag("@"), domain_part))(input)?; Ok((input, AddrSpec { local_part: local, domain: domain, })) } /// Local part /// /// ```abnf /// local-part = dot-atom / quoted-string / obs-local-part /// ``` fn local_part(input: &str) -> IResult<&str, String> { alt((into(dot_atom), quoted_string, obs_local_part))(input) } /// obs-local-part = word *("." word) fn obs_local_part(input: &str) -> IResult<&str, String> { map(recognize(separated_list1(tag("."), word)), |s| s.into())(input) } /// Domain /// /// ```abnf /// domain = dot-atom / domain-literal / obs-domain /// ``` pub fn domain_part(input: &str) -> IResult<&str, String> { alt((into(dot_atom), domain_litteral, obs_domain))(input) } /// obs-domain = atom *("." atom) fn obs_domain(input: &str) -> IResult<&str, String> { map(recognize(separated_list1(tag("."), atom)), |s| s.into())(input) } /// Domain litteral /// /// ```abnf /// domain-literal = [CFWS] "[" *([FWS] dtext) [FWS] "]" [CFWS] /// ``` fn domain_litteral(input: &str) -> IResult<&str, String> { delimited( pair(opt(cfws), tag("[")), inner_domain_litteral, pair(tag("]"), opt(cfws)) )(input) } fn inner_domain_litteral(input: &str) -> IResult<&str, String> { let (input, (cvec, maybe_wsp)) = pair(many0(pair(opt(fws), satisfy(is_dtext))), opt(fws))(input)?; let mut domain = cvec.iter().fold( String::with_capacity(16), |mut acc, (maybe_wsp, c)| { if let Some(wsp) = maybe_wsp { acc.push(*wsp); } acc.push(*c); acc }); if let Some(wsp) = maybe_wsp { domain.push(wsp); } Ok((input, domain)) } fn is_strict_dtext(c: char) -> bool { (c >= '\x21' && c <= '\x5A') || (c >= '\x5E' && c <= '\x7E') || !c.is_ascii() } /// Is domain text /// /// ```abnf /// dtext = %d33-90 / ; Printable US-ASCII /// %d94-126 / ; characters not including /// obs-dtext ; "[", "]", or "\" /// obs-dtext = obs-NO-WS-CTL / quoted-pair /// ``` pub fn is_dtext(c: char) -> bool { is_strict_dtext(c) || is_obs_no_ws_ctl(c) //@FIXME does not support quoted pair yet while RFC requires it } #[cfg(test)] mod tests { use super::*; #[test] fn test_addr_spec() { assert_eq!(addr_spec("alice@example.com"), Ok(("", AddrSpec{local_part: "alice".into(), domain: "example.com".into() }))); assert_eq!(addr_spec("jsmith@[192.168.2.1]"), Ok(("", AddrSpec{local_part: "jsmith".into(), domain: "192.168.2.1".into() }))); assert_eq!(addr_spec("jsmith@[IPv6:2001:db8::1]"), Ok(("", AddrSpec{local_part: "jsmith".into(), domain: "IPv6:2001:db8::1".into() }))); // UTF-8 assert_eq!(addr_spec("用户@例子.广告"), Ok(("", AddrSpec{local_part: "用户".into(), domain: "例子.广告".into()}))); // ASCII Edge cases assert_eq!( addr_spec("user+mailbox/department=shipping@example.com"), Ok(("", AddrSpec{local_part: "user+mailbox/department=shipping".into(), domain: "example.com".into()}))); assert_eq!( addr_spec("!#$%&'*+-/=?^_`.{|}~@example.com"), Ok(("", AddrSpec{local_part: "!#$%&'*+-/=?^_`.{|}~".into(), domain: "example.com".into()}))); assert_eq!( addr_spec(r#""Abc@def"@example.com"#), Ok(("", AddrSpec{local_part: "Abc@def".into(), domain: "example.com".into()}))); assert_eq!(addr_spec(r#""Fred\ Bloggs"@example.com"#), Ok(("", AddrSpec{local_part: "Fred Bloggs".into(), domain: "example.com".into()}))); assert_eq!(addr_spec(r#""Joe.\\Blow"@example.com"#), Ok(("", AddrSpec{local_part: r#"Joe.\Blow"#.into(), domain: "example.com".into()}))); } #[test] fn test_mailbox() { assert_eq!(mailbox(r#""Joe Q. Public" "#), Ok(("", MailboxRef { name: Some("Joe Q. Public".into()), addrspec: AddrSpec { local_part: "john.q.public".into(), domain: "example.com".into(), } }))); assert_eq!(mailbox(r#"Mary Smith "#), Ok(("", MailboxRef { name: Some("Mary Smith".into()), addrspec: AddrSpec { local_part: "mary".into(), domain: "x.test".into(), } }))); assert_eq!(mailbox(r#"jdoe@example.org"#), Ok(("", MailboxRef { name: None, addrspec: AddrSpec { local_part: "jdoe".into(), domain: "example.org".into(), } }))); assert_eq!(mailbox(r#"Who? "#), Ok(("", MailboxRef { name: Some("Who?".into()), addrspec: AddrSpec { local_part: "one".into(), domain: "y.test".into(), } }))); assert_eq!(mailbox(r#""#), Ok(("", MailboxRef { name: None, addrspec: AddrSpec { local_part: "boss".into(), domain: "nil.test".into(), } }))); assert_eq!(mailbox(r#""Giant; \"Big\" Box" "#), Ok(("", MailboxRef { name: Some(r#"Giant; "Big" Box"#.into()), addrspec: AddrSpec { local_part: "sysservices".into(), domain: "example.net".into(), } }))); } #[test] fn test_obs_domain_list() { assert_eq!(obs_domain_list(r#"(shhh it's coming) , (not yet) @33+4.com,,,, ,,,, (again) @example.com,@yep.com,@a,@b,,,@c"#), Ok(("", vec!["33+4.com".into(), "example.com".into(), "yep.com".into(), "a".into(), "b".into(), "c".into()])) ); } }