diff options
-rw-r--r-- | openpgp/Cargo.toml | 1 | ||||
-rw-r--r-- | openpgp/src/lib.rs | 2 | ||||
-rw-r--r-- | openpgp/src/packet/userid.rs | 64 |
3 files changed, 67 insertions, 0 deletions
diff --git a/openpgp/Cargo.toml b/openpgp/Cargo.toml index 320c0628..fd71bc04 100644 --- a/openpgp/Cargo.toml +++ b/openpgp/Cargo.toml @@ -26,6 +26,7 @@ base64 = "0.9.0" bzip2 = { version = "0.3.2", optional = true } failure = "0.1.2" flate2 = { version = "1.0.1", optional = true } +idna = "0.1" lalrpop-util = "0.16" lazy_static = "1.3" memsec = "0.5.4" diff --git a/openpgp/src/lib.rs b/openpgp/src/lib.rs index 7fe62790..0b27440f 100644 --- a/openpgp/src/lib.rs +++ b/openpgp/src/lib.rs @@ -69,6 +69,8 @@ extern crate time; extern crate sequoia_rfc2822 as rfc2822; #[macro_use] extern crate lazy_static; + +extern crate idna; #[macro_use] mod macros; diff --git a/openpgp/src/packet/userid.rs b/openpgp/src/packet/userid.rs index 64f76b13..1c8b3f84 100644 --- a/openpgp/src/packet/userid.rs +++ b/openpgp/src/packet/userid.rs @@ -165,6 +165,55 @@ impl UserID { None => unreachable!(), } } + + /// Returns a normalized version of the UserID's email address. + /// + /// Normalized email addresses are primarily needed when email + /// addresses are compared. + /// + /// Note: normalized email addresses are still valid email + /// addresses. + /// + /// This function normalizes an email address by doing [puny-code + /// normalization] on the domain, and lowercasing the local part in + /// the so-called [empty locale]. + /// + /// Note: this normalization procedure is the same as the + /// normalization procedure recommended by [Autocrypt]. + /// + /// [puny-code normalization]: https://tools.ietf.org/html/rfc5891.html#section-4.4 + /// [empty locale]: https://www.w3.org/International/wiki/Case_folding + /// [Autocryt]: https://autocrypt.org/level1.html#e-mail-address-canonicalization + pub fn address_normalized(&self) -> Result<Option<String>> { + match self.address() { + e @ Err(_) => e, + Ok(None) => Ok(None), + Ok(Some(address)) => { + let mut iter = address.split('@'); + let localpart = iter.next().expect("Invalid email address"); + let domain = iter.next().expect("Invalid email address"); + assert!(iter.next().is_none(), "Invalid email address"); + + // Normalize Unicode in domains. + let domain = idna::domain_to_ascii(domain) + .map_err(|e| failure::format_err!( + "punycode conversion failed: {:?}", e))?; + + // Join. + let address = format!("{}@{}", localpart, domain); + + // Convert to lowercase without tailoring, i.e. without taking + // any locale into account. See: + // + // - https://www.w3.org/International/wiki/Case_folding + // - https://doc.rust-lang.org/std/primitive.str.html#method.to_lowercase + // - http://www.unicode.org/versions/Unicode7.0.0/ch03.pdf#G33992 + let address = address.to_lowercase(); + + Ok(Some(address)) + } + } + } } impl From<UserID> for Packet { @@ -266,4 +315,19 @@ mod tests { c("huxley@old-world.org.", false, None, None, None); c("@old-world.org", false, None, None, None); } + + #[test] + fn address_normalized() { + fn c(value: &str, expected: &str) { + let u = UserID::from(value); + let got = u.address_normalized().unwrap().unwrap(); + assert_eq!(expected, got); + } + + c("Henry Ford (CEO) <henry@ford.com>", "henry@ford.com"); + c("Henry Ford (CEO) <Henry@Ford.com>", "henry@ford.com"); + c("Henry Ford (CEO) <Henry@Ford.com>", "henry@ford.com"); + c("hans@bücher.tld", "hans@xn--bcher-kva.tld"); + c("hANS@bücher.tld", "hans@xn--bcher-kva.tld"); + } } |