Support IDN

This commit is contained in:
Rodolphe Bréard 2023-07-31 22:49:21 +02:00
parent e6b20d7ef5
commit d2bd46462c
3 changed files with 25 additions and 4 deletions

View file

@ -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"] }

View file

@ -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.

View file

@ -17,9 +17,10 @@ impl CodedAddress {
pub fn parse(s: &str, separator: char) -> Result<Self> {
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<String> {
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<String> {
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();