use crate::code::generate_code; use anyhow::{ensure, Error, Result}; use data_encoding::{BASE32_NOPAD, BASE64}; use std::hash::{Hash, Hasher}; use std::str::FromStr; #[derive(Clone, Debug, Eq, PartialEq)] pub struct CodedAddress { local_part: String, sub_addr: Option, code: Vec, domain: Option, separator: char, } 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"); 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"); let sub_addr = if parts.len() >= 2 { Some(parts[1].to_string()) } else { None }; let code = if parts.len() >= 3 { BASE32_NOPAD .decode(parts[2].to_uppercase().as_bytes()) .unwrap_or_default() } else { Vec::new() }; Ok(Self { local_part, sub_addr, code, domain, separator, }) } } #[derive(Clone, Debug)] pub struct KeyedAddress { local_part: String, domain: Option, key: Vec, } impl KeyedAddress { pub fn check_code(&self, addr: &CodedAddress) -> bool { if addr.local_part.is_empty() || self.key.is_empty() { return false; } match &addr.sub_addr { Some(sub_addr) => { if !sub_addr.is_empty() { addr.code == generate_code(&addr.local_part, addr.separator, sub_addr, &self.key) } else { false } } None => false, } } } impl PartialEq for KeyedAddress { fn eq(&self, other: &Self) -> bool { self.local_part == other.local_part && self.domain == other.domain } } impl PartialEq for KeyedAddress { fn eq(&self, other: &CodedAddress) -> bool { if let Some(domain_k) = &self.domain { if let Some(domain_c) = &other.domain { if domain_k != domain_c { return false; } } } self.local_part == other.local_part } } impl Eq for KeyedAddress {} impl Hash for KeyedAddress { fn hash(&self, state: &mut H) { self.local_part.hash(state); self.domain.hash(state); } } impl FromStr for KeyedAddress { type Err = Error; fn from_str(s: &str) -> Result { let ksplit = s.rsplit_once(crate::KEY_SEPARATOR); ensure!(ksplit.is_some(), "{s}: key separator not found"); 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"); 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"); ensure!( crate::ALLOWED_KEY_SIZES.contains(&key.len()), "{s}: key length must be either 128 or 256 bits" ); Ok(Self { local_part, domain, key, }) } } fn split_local_part(s: &str) -> (String, Option) { match s.rsplit_once('@') { Some((local_part, domain)) => (local_part.to_string(), Some(domain.to_string())), None => (s.to_string(), None), } } #[cfg(test)] mod tests { use super::{CodedAddress, KeyedAddress}; use std::str::FromStr; #[test] fn parse_coded_addr_empty_email() { assert!(CodedAddress::parse("", '+').is_err()); } #[test] fn parse_coded_addr_empty_local() { assert!(CodedAddress::parse("@example.com", '+').is_err()); } #[test] fn parse_coded_addr_empty_domain() { assert!(CodedAddress::parse("derp@", '+').is_err()); } #[test] fn parse_keyed_addr_empty_email() { assert!(KeyedAddress::from_str("").is_err()); } #[test] fn parse_keyed_addr_empty_local() { assert!(KeyedAddress::from_str("@example.com").is_err()); } #[test] fn parse_keyed_addr_empty_domain() { assert!(KeyedAddress::from_str("derp@").is_err()); } #[test] fn parse_keyed_addr_invalid_key_length() { assert!(KeyedAddress::from_str("test:bAXk1r7mgJY=").is_err()); } #[test] fn parse_valid_coded_addr_with_domain() { let addr_str = "a+test+orsxg5a@example.org"; let addr = CodedAddress::parse(addr_str, '+'); assert!(addr.is_ok(), "unable to parse {addr_str}: {addr:?}"); let addr = addr.unwrap(); assert_eq!(addr.local_part, "a"); assert_eq!(addr.sub_addr, Some("test".to_string())); assert_eq!(addr.code, b"test"); assert_eq!(addr.domain, Some("example.org".to_string())); } #[test] fn parse_valid_coded_addr_with_invalid_base32() { let addr_str = "a+test+invalid@example.org"; let addr = CodedAddress::parse(addr_str, '+'); assert!(addr.is_ok(), "unable to parse {addr_str}: {addr:?}"); let addr = addr.unwrap(); assert_eq!(addr.local_part, "a"); assert_eq!(addr.sub_addr, Some("test".to_string())); assert_eq!(addr.code, Vec::new()); assert_eq!(addr.domain, Some("example.org".to_string())); } #[test] fn parse_valid_coded_addr_without_domain() { let addr_str = "local.part+test+orsxg5a"; let addr = CodedAddress::parse(addr_str, '+'); assert!(addr.is_ok(), "unable to parse {addr_str}: {addr:?}"); let addr = addr.unwrap(); assert_eq!(addr.local_part, "local.part"); assert_eq!(addr.sub_addr, Some("test".to_string())); assert_eq!(addr.code, b"test"); assert!(addr.domain.is_none()); } #[test] fn parse_valid_coded_addr_alt_sep() { let addr_str = "local-part.test.orsxg5a@example.org"; let addr = CodedAddress::parse(addr_str, '.'); assert!(addr.is_ok(), "unable to parse {addr_str}: {addr:?}"); let addr = addr.unwrap(); assert_eq!(addr.local_part, "local-part"); assert_eq!(addr.sub_addr, Some("test".to_string())); assert_eq!(addr.code, b"test"); assert_eq!(addr.domain, Some("example.org".to_string())); } #[test] fn parse_valid_coded_addr_without_sub_addr() { let addr_str = "local.part@example.org"; let addr = CodedAddress::parse(addr_str, '+'); assert!(addr.is_ok(), "unable to parse {addr_str}: {addr:?}"); let addr = addr.unwrap(); assert_eq!(addr.local_part, "local.part"); assert!(addr.sub_addr.is_none()); assert!(addr.code.is_empty()); assert_eq!(addr.domain, Some("example.org".to_string())); } #[test] fn parse_valid_keyed_addr_with_domain() { let addr_str = "a@example.org:11voiefK5PgCX5F1TTcuoQ=="; let addr = KeyedAddress::from_str(addr_str); assert!(addr.is_ok(), "unable to parse {addr_str}: {addr:?}"); let addr = addr.unwrap(); assert_eq!(addr.local_part, "a"); assert_eq!(addr.domain, Some("example.org".to_string())); assert_eq!( addr.key, vec![ 0xd7, 0x5b, 0xe8, 0x89, 0xe7, 0xca, 0xe4, 0xf8, 0x02, 0x5f, 0x91, 0x75, 0x4d, 0x37, 0x2e, 0xa1 ] ); } #[test] fn parse_valid_keyed_addr_without_domain() { let addr_str = "local.part:sbW22xNStWKro4nnMKmG3A=="; let addr = KeyedAddress::from_str(addr_str); assert!(addr.is_ok(), "unable to parse {addr_str}: {addr:?}"); let addr = addr.unwrap(); assert_eq!(addr.local_part, "local.part"); assert_eq!(addr.domain, None); assert_eq!( addr.key, vec![ 0xb1, 0xb5, 0xb6, 0xdb, 0x13, 0x52, 0xb5, 0x62, 0xab, 0xa3, 0x89, 0xe7, 0x30, 0xa9, 0x86, 0xdc ] ); } #[test] fn keyed_addr_empty_address() { let res = KeyedAddress::from_str(""); assert!(res.is_err()); } #[test] fn keyed_addr_empty_base64() { let res = KeyedAddress::from_str("a:"); assert!(res.is_err()); } #[test] fn keyed_addr_invalid_base64() { let res = KeyedAddress::from_str("a:uh2kv%j3"); assert!(res.is_err()); } #[test] fn cmp_coded_addr_with_domain_eq() { let addr_1 = CodedAddress::parse("test+test+orsxg5a@example.org", '+').unwrap(); let addr_2 = CodedAddress::parse("test+test+orsxg5a@example.org", '+').unwrap(); assert_eq!(addr_1, addr_2); } #[test] fn cmp_coded_addr_without_domain_eq() { let addr_1 = CodedAddress::parse("test+test+orsxg5a", '+').unwrap(); let addr_2 = CodedAddress::parse("test+test+orsxg5a", '+').unwrap(); assert_eq!(addr_1, addr_2); } #[test] fn cmp_coded_addr_with_domain_ne() { let addr_1 = CodedAddress::parse("test+test+orsxg5a@example.org", '+').unwrap(); let addr_2 = CodedAddress::parse("test2+test+orsxg5a@example.org", '+').unwrap(); assert_ne!(addr_1, addr_2); } #[test] fn cmp_coded_addr_without_domain_ne() { let addr_1 = CodedAddress::parse("test+test+orsxg5a", '+').unwrap(); let addr_2 = CodedAddress::parse("test2+test+orsxg5a", '+').unwrap(); assert_ne!(addr_1, addr_2); } #[test] fn cmp_keyed_addr_with_domain_eq() { let addr_1 = KeyedAddress::from_str("test@example.org:gkJfLlKa2OLYItm4JD6p3hEF1kWH8LFtMK0rra8A2SQ=") .unwrap(); let addr_2 = KeyedAddress::from_str("test@example.org:11voiefK5PgCX5F1TTcuoQ==").unwrap(); assert_eq!(addr_1, addr_2); } #[test] fn cmp_keyed_addr_without_domain_eq() { let addr_1 = KeyedAddress::from_str("test:gkJfLlKa2OLYItm4JD6p3hEF1kWH8LFtMK0rra8A2SQ=").unwrap(); let addr_2 = KeyedAddress::from_str("test:11voiefK5PgCX5F1TTcuoQ==").unwrap(); assert_eq!(addr_1, addr_2); } #[test] fn cmp_keyed_addr_with_domain_ne() { let addr_1 = KeyedAddress::from_str("test@example.org:sbW22xNStWKro4nnMKmG3A==").unwrap(); let addr_2 = KeyedAddress::from_str("test2@example.org:sbW22xNStWKro4nnMKmG3A==").unwrap(); assert_ne!(addr_1, addr_2); } #[test] fn cmp_keyed_addr_without_domain_ne() { let addr_1 = KeyedAddress::from_str("test:gkJfLlKa2OLYItm4JD6p3hEF1kWH8LFtMK0rra8A2SQ=").unwrap(); let addr_2 = KeyedAddress::from_str("test2:gkJfLlKa2OLYItm4JD6p3hEF1kWH8LFtMK0rra8A2SQ=").unwrap(); assert_ne!(addr_1, addr_2); } #[test] fn cmp_addr_types_with_domain_eq() { let addr_1 = KeyedAddress::from_str("test@example.org:sbW22xNStWKro4nnMKmG3A==").unwrap(); let addr_2 = CodedAddress::parse("test+test+orsxg5a@example.org", '+').unwrap(); assert_eq!(addr_1, addr_2); } #[test] fn cmp_addr_types_without_domain_eq() { let addr_1 = KeyedAddress::from_str("test:sbW22xNStWKro4nnMKmG3A==").unwrap(); let addr_2 = CodedAddress::parse("test+test+orsxg5a", '+').unwrap(); assert_eq!(addr_1, addr_2); } #[test] fn cmp_addr_types_without_sub_addr() { let addr_1 = KeyedAddress::from_str("test@example.org:sbW22xNStWKro4nnMKmG3A==").unwrap(); let addr_2 = CodedAddress::parse("test@example.org", '+').unwrap(); assert_eq!(addr_1, addr_2); } #[test] fn cmp_addr_types_idna_1() { let addr_1 = KeyedAddress::from_str("test@mél.example.org:sbW22xNStWKro4nnMKmG3A==").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:sbW22xNStWKro4nnMKmG3A==").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:sbW22xNStWKro4nnMKmG3A==").unwrap(); let addr_2 = CodedAddress::parse("test+test+orsxg5a@example.com", '+').unwrap(); assert_ne!(addr_1, addr_2); } #[test] fn cmp_addr_types_without_domain_ne() { let addr_1 = KeyedAddress::from_str("test:sbW22xNStWKro4nnMKmG3A==").unwrap(); let addr_2 = CodedAddress::parse("test2+test+orsxg5a", '+').unwrap(); assert_ne!(addr_1, addr_2); } }