diff --git a/rust-lib/build.rs b/rust-lib/build.rs index 46a4b3f..6f5726e 100644 --- a/rust-lib/build.rs +++ b/rust-lib/build.rs @@ -35,8 +35,18 @@ fn main() { &invalid_local_parts, &invalid_domains, ); + create_is_email_tests(&mut content, &test_data_root); + create_valid_instantiation_tests(&mut content, &valid_local_parts, &valid_domains); + create_invalid_instantiation_tests( + &mut content, + &valid_local_parts, + &valid_domains, + &invalid_local_parts, + &invalid_domains, + ); + write!(test_file, "{}", content.trim()).unwrap(); println!("cargo:rerun-if-changed=build.rs"); println!("cargo:rerun-if-changed=resources/.test_data/valid_local_parts.txt"); @@ -245,3 +255,77 @@ generate_is_email_test!{ content.push_str("}"); } + +fn create_valid_instantiation_tests( + content: &mut String, + local_parts: &Vec, + domains: &Vec, +) { + content.push_str( + " +macro_rules! generate_test_positive_instantiation_test { + ($($case:ident: ($local_part:literal, $domain:literal),)+) => { + #[cfg(test)] + mod instantiates_valid_email_address { + use email_address_parser::EmailAddress; + use wasm_bindgen_test::*; + wasm_bindgen_test_configure!(run_in_browser); + $( + #[test] + #[wasm_bindgen_test] + fn $case() { + let address = EmailAddress::new(&$local_part, &$domain, None).unwrap(); + assert_eq!(address.get_local_part(), $local_part); + assert_eq!(address.get_domain(), $domain); + assert_eq!(format!(\"{}\", address), concat!($local_part, \"@\", $domain), \"incorrect display\"); + } + )* + } + }; +} + +generate_test_positive_instantiation_test!{ +", + ); + create_case(content, &mut 0, local_parts, domains); + + content.push_str("}\n"); +} + +fn create_invalid_instantiation_tests( + content: &mut String, + valid_local_parts: &Vec, + valid_domains: &Vec, + invalid_local_parts: &Vec, + invalid_domains: &Vec, +) { + content.push_str( + " +macro_rules! generate_test_negative_instantiation_test { + ($($case:ident: ($local_part:literal, $domain:literal),)+) => { + #[cfg(test)] + mod panics_instantiating_invalid_email_address { + use email_address_parser::EmailAddress; + use wasm_bindgen_test::*; + wasm_bindgen_test_configure!(run_in_browser); + $( + #[test] + #[wasm_bindgen_test] + fn $case() { + assert_eq!(EmailAddress::new(&$local_part, &$domain, None).is_none(), true); + } + )* + } + }; +} + +generate_test_negative_instantiation_test!{ +", + ); + let mut i = 0; + create_case(content, &mut i, invalid_local_parts, valid_domains); + create_case(content, &mut i, valid_local_parts, invalid_domains); + create_case(content, &mut i, invalid_local_parts, invalid_domains); + + content.push_str("}\n"); +} diff --git a/rust-lib/src/email_address.rs b/rust-lib/src/email_address.rs index c4c2bd1..382935e 100644 --- a/rust-lib/src/email_address.rs +++ b/rust-lib/src/email_address.rs @@ -33,45 +33,64 @@ pub struct EmailAddress { impl EmailAddress { #![warn(missing_docs)] #![warn(missing_doc_code_examples)] - // TODO: validate local part and domain - /// Instantiates a new `EmailAddress`. - /// + /// Instantiates a new `Some(EmailAddress)` for a valid local part and domain. + /// Returns `None` otherwise. + /// Ideally a `Result` should have been returned instead of `Option`. + /// However, unfortunately it does not seems to be working with wasm_bindgen (refer: https://github.com/rustwasm/wasm-bindgen/issues/1017). + /// /// Accessible from WASM. - /// - /// #Examples + /// + /// # Examples /// ``` /// use email_address_parser::EmailAddress; - /// - /// let email = EmailAddress::new("foo", "bar.com"); + /// + /// let email = EmailAddress::new("foo", "bar.com", Some(true)).unwrap(); + /// + /// assert_eq!(EmailAddress::new("foo", "-bar.com", Some(true)).is_none(), true); /// ``` - pub fn new(local_part: &str, domain: &str) -> EmailAddress { - EmailAddress { + pub fn new(local_part: &str, domain: &str, is_strict: Option) -> Option { + let is_strict = is_strict.unwrap_or_default(); + + if (is_strict && !RFC5322::parse(Rule::local_part_complete, local_part).is_ok()) + || (!is_strict && !RFC5322::parse(Rule::local_part_complete, local_part).is_ok()) + { + // return Err(format!("Invalid local part '{}'.", local_part)); + return None; + } + if (is_strict && !RFC5322::parse(Rule::domain_complete, domain).is_ok()) + || (!is_strict && !RFC5322::parse(Rule::domain_complete, domain).is_ok()) + { + // return Err(format!("Invalid domain '{}'.", domain)); + return None; + } + + Some(EmailAddress { local_part: String::from(local_part), domain: String::from(domain), - } + }) } /// Parses a given string as an email address. - /// + /// /// Accessible from WASM. - /// + /// /// Returns `Some(EmailAddress)` if the parsing is successful, else `None`. - /// #Examples + /// # Examples /// ``` /// use email_address_parser::EmailAddress; - /// + /// /// // strict parsing /// let email = EmailAddress::parse("foo@bar.com", Some(true)); /// assert!(email.is_some()); /// let email = email.unwrap(); /// assert_eq!(email.get_local_part(), "foo"); /// assert_eq!(email.get_domain(), "bar.com"); - /// + /// /// // non-strict parsing /// let email = EmailAddress::parse("\u{0d}\u{0a} \u{0d}\u{0a} test@iana.org", None); /// assert!(email.is_some()); - /// + /// /// // parsing invalid address /// let email = EmailAddress::parse("test@-iana.org", Some(true)); /// assert!(email.is_none()); @@ -112,17 +131,17 @@ impl EmailAddress { } } /// Returns the local part of the email address. - /// + /// /// Note that if you are using this library from rust, then consider using the `get_local_part` method instead. /// This returns a cloned copy of the local part string, instead of a borrowed `&str`, and exists purely for WASM interoperability. - /// - /// #Examples + /// + /// # Examples /// ``` /// use email_address_parser::EmailAddress; - /// - /// let email = EmailAddress::new("foo", "bar.com"); + /// + /// let email = EmailAddress::new("foo", "bar.com", Some(true)).unwrap(); /// assert_eq!(email.local_part(), "foo"); - /// + /// /// let email = EmailAddress::parse("foo@bar.com", Some(true)).unwrap(); /// assert_eq!(email.local_part(), "foo"); /// ``` @@ -131,17 +150,17 @@ impl EmailAddress { self.local_part.clone() } /// Returns the domain of the email address. - /// + /// /// Note that if you are using this library from rust, then consider using the `get_domain` method instead. /// This returns a cloned copy of the domain string, instead of a borrowed `&str`, and exists purely for WASM interoperability. - /// - /// #Examples + /// + /// # Examples /// ``` /// use email_address_parser::EmailAddress; - /// - /// let email = EmailAddress::new("foo", "bar.com"); + /// + /// let email = EmailAddress::new("foo", "bar.com", Some(true)).unwrap(); /// assert_eq!(email.domain(), "bar.com"); - /// + /// /// let email = EmailAddress::parse("foo@bar.com", Some(true)).unwrap(); /// assert_eq!(email.domain(), "bar.com"); /// ``` @@ -154,36 +173,34 @@ impl EmailAddress { impl EmailAddress { #![warn(missing_docs)] #![warn(missing_doc_code_examples)] - /// Returns the local part of the email address. - /// + /// /// Not accessible from WASM. - /// - /// #Examples + /// + /// # Examples /// ``` /// use email_address_parser::EmailAddress; - /// - /// let email = EmailAddress::new("foo", "bar.com"); + /// + /// let email = EmailAddress::new("foo", "bar.com", Some(true)).unwrap(); /// assert_eq!(email.get_local_part(), "foo"); - /// + /// /// let email = EmailAddress::parse("foo@bar.com", Some(true)).unwrap(); /// assert_eq!(email.get_local_part(), "foo"); /// ``` pub fn get_local_part(&self) -> &str { self.local_part.as_str() } - /// Returns the domain of the email address. - /// + /// /// Not accessible from WASM. - /// - /// #Examples + /// + /// # Examples /// ``` /// use email_address_parser::EmailAddress; - /// - /// let email = EmailAddress::new("foo", "bar.com"); + /// + /// let email = EmailAddress::new("foo", "bar.com", Some(true)).unwrap(); /// assert_eq!(email.get_domain(), "bar.com"); - /// + /// /// let email = EmailAddress::parse("foo@bar.com", Some(true)).unwrap(); /// assert_eq!(email.get_domain(), "bar.com"); /// ``` @@ -204,7 +221,7 @@ mod tests { #[test] fn email_address_instantiation_works() { - let address = EmailAddress::new("foo", "bar.com"); + let address = EmailAddress::new("foo", "bar.com", Some(true)).unwrap(); assert_eq!(address.get_local_part(), "foo"); assert_eq!(address.get_domain(), "bar.com"); assert_eq!(format!("{}", address), "foo@bar.com"); @@ -296,4 +313,11 @@ mod tests { println!("{:#?}", actual); assert_eq!(actual.is_err(), false); } + + #[test] + fn can_parse_local_part_with_space_and_quote() { + let actual = RFC5322::parse(Rule::local_part_complete, "\"test test\""); + println!("{:#?}", actual); + assert_eq!(actual.is_err(), false); + } } diff --git a/rust-lib/src/rfc5322.pest b/rust-lib/src/rfc5322.pest index 9b5ee4f..19a53e7 100644 --- a/rust-lib/src/rfc5322.pest +++ b/rust-lib/src/rfc5322.pest @@ -3,6 +3,8 @@ address_spec = { local_part ~ "@" ~ domain } local_part = @{ dot_atom | quoted_string } domain = @{ dot_atom | domain_literal } + +local_part_complete = { SOI ~ local_part ~ EOI } domain_complete = { SOI ~ domain ~ EOI } /*------------ lower level rules -------------*/ @@ -56,6 +58,9 @@ address_spec_obs = { local_part_obs ~ "@" ~ domain_obs } local_part_obs = @{ obs_local_part | dot_atom | quoted_string } domain_obs = @{ obs_domain | dot_atom | domain_literal } +obs_local_part_complete = { SOI ~ obs_local_part ~ EOI } +obs_domain_complete = { SOI ~ obs_domain ~ EOI } + obs_local_part = { FWS* ~ word ~ (CFWS* ~ "." ~ CFWS* ~ word)* } obs_domain = { CFWS* ~