summaryrefslogtreecommitdiffstats
path: root/openpgp
diff options
context:
space:
mode:
authorNeal H. Walfield <neal@pep.foundation>2019-05-06 11:02:51 +0200
committerNeal H. Walfield <neal@pep.foundation>2019-05-07 12:18:32 +0200
commitabffff28039cafcb076063790cf7458d5e3d0e2c (patch)
tree4d467c0dd7e422ae7d58a23d1626054c9962f302 /openpgp
parentf865d4fe68983d8197e95559636f45a1985df61a (diff)
openpgp: Add email address normalization
- Provide a function to return a normalized email address, which is appropriate when comparing email addresses for equality.
Diffstat (limited to 'openpgp')
-rw-r--r--openpgp/Cargo.toml1
-rw-r--r--openpgp/src/lib.rs2
-rw-r--r--openpgp/src/packet/userid.rs64
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");
+ }
}