From 6a3d8f65e3cc42d52dcc0ad3e1001de8960f40a5 Mon Sep 17 00:00:00 2001 From: Simon Johnston Date: Tue, 24 Mar 2020 10:11:36 -0700 Subject: [PATCH] Added `is_valid_local_part` and `is_valid_domain`. Also now URI-encode addresses in `to_uri`. --- README.md | 3 ++- src/lib.rs | 68 +++++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 42fba01..316ef1e 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,8 @@ assert_eq!( * Added UTF-8 support. * Added more test cases, fixing defects in parsing. +* Method `to_uri` now supports URI encoding the address as a part of the URI. +* Added `is_valid_local_part` and `is_valid_domain` methods. **Version 0.1.0** @@ -62,6 +64,5 @@ assert_eq!( ## TODO 1. Support comments. -1. `to_uri` needs to support URI encoding. 1. Support line-feed and whitespace rules. 1. Does not parse _into_ `domain-literal` values, only does surface syntax check. diff --git a/src/lib.rs b/src/lib.rs index bc69d4e..c472eab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,7 +31,7 @@ assert_eq!( assert_eq!( email.to_uri(), - "mailto:johnstonsk@gmail.com".to_string() + "mailto:johnstonsk%40gmail.com".to_string() ); assert_eq!( @@ -372,7 +372,7 @@ impl FromStr for EmailAddress { impl EmailAddress { /// - /// Determine whether the passed string is a valid email address. Note this is equivalent to + /// Determine whether the `address` string is a valid email address. Note this is equivalent to /// the following: /// /// ```rust @@ -383,18 +383,40 @@ impl EmailAddress { /// ``` /// pub fn is_valid(address: &str) -> bool { - Self::from_str(&address).is_ok() + Self::from_str(address).is_ok() } /// - /// Return this email address formatted as a URI. + /// Determine whether the `part` string would be a valid `local-part` if it were in an + /// email address. + /// + pub fn is_valid_local_part(part: &str) -> bool { + parse_local_part(part).is_ok() + } + + /// + /// Determine whether the `part` string would be a valid `domain` if it were in an + /// email address. + /// + pub fn is_valid_domain(part: &str) -> bool { + parse_domain(part).is_ok() + } + + /// + /// Return this email address formatted as a URI. This will also URI-encode the email + /// address itself. So, `name@example.org` becomes `mailto:name%40example.org`. /// pub fn to_uri(&self) -> String { - format!("{}{}", MAILTO_URI_PREFIX, self) + let encoded = encode(&self.0); + format!("{}{}", MAILTO_URI_PREFIX, encoded) } /// - /// Return a string formatted as a display email with the user name. + /// Return a string formatted as a display email with the user name. This is commonly used + /// in email headers and other locations where a display name is associated with the + /// address. + /// + /// So, `("name@example.org", "My Name")` becomes `"My Name "`. /// pub fn to_display(&self, display_name: &str) -> String { format!("{} <{}>", display_name, self) @@ -405,6 +427,40 @@ impl EmailAddress { // Private Functions // ------------------------------------------------------------------------------------------------ +fn encode(address: &str) -> String { + let mut result = String::new(); + for c in address.chars() { + if is_uri_reserved(c) { + result.push_str(&format!("%{:02X}", c as u8)) + } else { + result.push(c); + } + } + result +} + +fn is_uri_reserved(c: char) -> bool { + c == '!' + || c == '#' + || c == '$' + || c == '%' + || c == '&' + || c == '\'' + || c == '(' + || c == ')' + || c == '*' + || c == '+' + || c == ',' + || c == '/' + || c == ':' + || c == ';' + || c == '=' + || c == '?' + || c == '@' + || c == '[' + || c == ']' +} + fn parse_address(address: &str) -> Result { let address = if address.starts_with(LT) && address.ends_with(GT) { &address[1..address.len() - 1]