From d2bd46462cd05318517648a77e05205f641e5574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodolphe=20Br=C3=A9ard?= Date: Mon, 31 Jul 2023 22:49:21 +0200 Subject: [PATCH] Support IDN --- Cargo.toml | 1 + README.md | 4 ++++ src/address.rs | 24 ++++++++++++++++++++---- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6196ee8..e3138be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,4 +12,5 @@ anyhow = { version = "1.0.71", default-features = false, features = ["std"] } clap = { version = "4.3.11", default-features = false, features = ["derive", "std"] } data-encoding = { version = "2.4.0", default-features = false, features = ["std"] } hmac = { version = "0.12.1", default-features = false } +idna = { version = "0.4.0", default-features = false, features = ["std"] } sha2 = { version = "0.10.7", default-features = false, features = ["std", "asm"] } diff --git a/README.md b/README.md index bc55432..135c155 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,10 @@ No, this project is based on the filter API used by OpenSMTPD. However, if someone implemented it in the exact same way for any other MTA, the progressive web app should work. +### Does it supports IDN? + +Yes, internationalized domain names (IDN) are supported. You can specify domain names either using valid UTF-8 or Punycode ([RFC 3492](https://datatracker.ietf.org/doc/html/rfc3492)). + ### What about key rotation? Rotating the key would mean that all previously generated addresses for this local part would suddenly be invalid. Therefore, the key associated with a local part must not change. diff --git a/src/address.rs b/src/address.rs index 85d47f7..5fe90bc 100644 --- a/src/address.rs +++ b/src/address.rs @@ -17,9 +17,10 @@ impl CodedAddress { pub fn parse(s: &str, separator: char) -> Result { let (local_part, domain) = split_local_part(s); ensure!(!local_part.is_empty(), "{s}: local part cannot be empty"); - if let Some(dom) = &domain { + let domain = domain.map(|dom| -> Result { ensure!(!dom.is_empty(), "{s}: domain cannot be empty"); - } + Ok(idna::domain_to_ascii(&dom)?) + }).transpose()?; let parts: Vec<&str> = local_part.split(separator).collect(); let local_part = parts[0].to_string(); ensure!(!local_part.is_empty(), "{s}: local part cannot be empty"); @@ -108,9 +109,10 @@ impl FromStr for KeyedAddress { let (address, key_b64) = ksplit.unwrap(); let (local_part, domain) = split_local_part(address); ensure!(!local_part.is_empty(), "{s}: local part cannot be empty"); - if let Some(dom) = &domain { + let domain = domain.map(|dom| -> Result { ensure!(!dom.is_empty(), "{s}: domain cannot be empty"); - } + Ok(idna::domain_to_ascii(&dom)?) + }).transpose()?; let key = BASE64.decode(key_b64.as_bytes())?; ensure!(!key.is_empty(), "{s}: key cannot be empty"); Ok(Self { @@ -346,6 +348,20 @@ mod tests { assert_eq!(addr_1, addr_2); } + #[test] + fn cmp_addr_types_idna_1() { + let addr_1 = KeyedAddress::from_str("test@mél.example.org:3d74YQqk").unwrap(); + let addr_2 = CodedAddress::parse("test@xn--ml-bja.example.org", '+').unwrap(); + assert_eq!(addr_1, addr_2); + } + + #[test] + fn cmp_addr_types_idna_2() { + let addr_1 = KeyedAddress::from_str("test@xn--ml-bja.example.org:3d74YQqk").unwrap(); + let addr_2 = CodedAddress::parse("test@mél.example.org", '+').unwrap(); + assert_eq!(addr_1, addr_2); + } + #[test] fn cmp_addr_types_with_domain_ne() { let addr_1 = KeyedAddress::from_str("test@example.org:3d74YQqk").unwrap();