use emailaddr::{
Buffer, DomainLiteralPolicy, DomainOptions, DomainPart, DomainUnicodePolicy, EmailAddr, Limits,
LocalOptions, LocalPart, Options, Relax, SmtpUtf8Policy, verify_ascii_dns_domain,
verify_ascii_domain_part, verify_ascii_local_part, verify_email_addr_with_options,
verify_local_part,
};
#[cfg(any(feature = "alloc", feature = "std"))]
use emailaddr::{MAX_EMAIL_ADDR_LENGTH, verify_ascii_email_addr, verify_email_addr};
#[cfg(any(feature = "alloc", feature = "std"))]
use std::{borrow::Cow, rc::Rc, sync::Arc};
#[cfg(feature = "serde")]
fn assert_serde<T>()
where
T: serde_core::Serialize + for<'de> serde_core::Deserialize<'de>,
{
}
#[cfg(feature = "serde")]
fn assert_serialize<T>()
where
T: serde_core::Serialize,
{
}
#[cfg(all(feature = "serde", any(feature = "alloc", feature = "std")))]
fn assert_deserialize<'de, T>()
where
T: serde_core::Deserialize<'de>,
{
}
#[cfg(all(feature = "arbitrary", any(feature = "alloc", feature = "std")))]
fn assert_arbitrary<'a, T>()
where
T: arbitrary::Arbitrary<'a>,
{
}
#[cfg(all(feature = "quickcheck", any(feature = "alloc", feature = "std")))]
fn assert_quickcheck<T>()
where
T: quickcheck::Arbitrary,
{
}
#[test]
fn validates_common_addresses() {
let addr = EmailAddr::try_from_ascii_str("user.name+tag@example.com").unwrap();
assert_eq!(addr.as_str(), "user.name+tag@example.com");
assert_eq!(addr.local_part().as_inner(), &"user.name+tag");
assert_eq!(addr.domain_part().as_inner(), &"example.com");
assert!(!addr.is_domain_literal());
let quoted = EmailAddr::try_from_ascii_str("\"user name\"@example.com").unwrap();
assert_eq!(quoted.local_part().as_inner(), &"\"user name\"");
let quoted_at = EmailAddr::try_from_ascii_str("\"a@b\"@example.com").unwrap();
assert_eq!(quoted_at.local_part().as_inner(), &"\"a@b\"");
let display_like =
EmailAddr::try_from_ascii_str("\"User <user@example.com>\"@example.com").unwrap();
assert_eq!(
display_like.local_part().as_inner(),
&"\"User <user@example.com>\""
);
}
#[test]
fn rejects_rfc5322_display_names_and_address_lists() {
for input in [
"User <user@example.com>",
"With, Comma <a@example.net>",
"a@example.net, b@example.net",
] {
assert!(EmailAddr::try_from_ascii_str(input).is_err(), "{input}");
}
}
#[test]
fn supports_unsized_borrowed_wrappers() {
let addr: &EmailAddr<str> = EmailAddr::try_from_ascii_str("user.name+tag@example.com").unwrap();
assert_eq!(addr.as_inner(), "user.name+tag@example.com");
assert_eq!(addr.local_part_ref().as_inner(), "user.name+tag");
assert_eq!(addr.domain_part_ref().as_inner(), "example.com");
assert_eq!(addr.parts_ref().0.as_inner(), "user.name+tag");
assert_eq!(addr.parts_ref().1.as_inner(), "example.com");
let addr_value: EmailAddr<&str> = addr.as_ref();
assert_eq!(addr_value.as_inner(), &"user.name+tag@example.com");
let bytes: &EmailAddr<[u8]> = addr.as_bytes_addr();
assert_eq!(bytes.as_inner(), b"user.name+tag@example.com");
assert_eq!(bytes.as_str_addr().as_inner(), "user.name+tag@example.com");
let bytes = EmailAddr::<[u8]>::try_from_ascii_bytes(b"user@example.com").unwrap();
assert_eq!(bytes.as_str_addr().as_inner(), "user@example.com");
let local: &LocalPart<str> = LocalPart::try_from_ascii_str("first.last").unwrap();
assert_eq!(local.as_inner(), "first.last");
assert_eq!(local.as_bytes().as_inner(), b"first.last");
assert_eq!(
LocalPart::<[u8]>::try_from_ascii_bytes(b"first.last")
.unwrap()
.as_str()
.as_inner(),
"first.last"
);
let domain: &DomainPart<str> = DomainPart::try_from_ascii_str("example.com").unwrap();
assert_eq!(domain.as_inner(), "example.com");
assert_eq!(domain.as_bytes().as_inner(), b"example.com");
assert_eq!(
DomainPart::<[u8]>::try_from_ascii_bytes(b"example.com")
.unwrap()
.as_str()
.as_inner(),
"example.com"
);
}
#[test]
fn validates_domain_literals() {
let ipv4 = EmailAddr::try_from_ascii_str("user@[127.0.0.1]").unwrap();
assert!(ipv4.is_domain_literal());
assert_eq!(ipv4.domain_part().as_inner(), &"[127.0.0.1]");
let ipv6 = EmailAddr::try_from_ascii_str("user@[IPv6:::1]").unwrap();
assert!(ipv6.is_domain_literal());
assert_eq!(ipv6.domain_part().as_inner(), &"[IPv6:::1]");
let ipv6_lower = EmailAddr::try_from_ascii_str("user@[ipv6:::1]").unwrap();
assert!(ipv6_lower.is_domain_literal());
assert_eq!(ipv6_lower.domain_part().as_inner(), &"[ipv6:::1]");
let ipv6_mixed = EmailAddr::try_from_ascii_str("user@[IpV6:::1]").unwrap();
assert!(ipv6_mixed.is_domain_literal());
assert_eq!(ipv6_mixed.domain_part().as_inner(), &"[IpV6:::1]");
let general = EmailAddr::try_from_ascii_str("user@[TAG:payload]").unwrap();
assert!(general.is_domain_literal());
assert_eq!(general.domain_part().as_inner(), &"[TAG:payload]");
}
#[test]
fn supports_parse_options_for_domain_literals() {
let literal = "user@[127.0.0.1]";
let default = EmailAddr::<Buffer, Relax>::parse_with_options(literal, Options::new()).unwrap();
assert!(default.is_domain_literal());
let forbidden = Options::new().with_domain(DomainOptions::new().without_domain_literals());
assert!(EmailAddr::<Buffer, Relax>::parse_with_options(literal, forbidden).is_err());
assert!(verify_email_addr_with_options(literal.as_bytes(), forbidden).is_err());
let explicit =
Options::new().with_domain(DomainOptions::new().with_literals(DomainLiteralPolicy::Forbid));
assert!(EmailAddr::<Buffer, Relax>::parse_with_options(literal, explicit).is_err());
}
#[test]
fn converts_strict_email_addr_into_relaxed_email_addr() {
let strict = EmailAddr::<Buffer>::try_from("user@example.com").unwrap();
let relaxed: EmailAddr<Buffer, Relax> = strict.into();
assert_eq!(relaxed.as_str(), "user@example.com");
assert_eq!(relaxed.parts(), ("user", "example.com"));
}
#[test]
fn supports_parse_options_for_minimum_dns_labels() {
let single = "ted.backer@gmail";
assert!(EmailAddr::<Buffer, Relax>::parse_with_options(single, Options::new()).is_ok());
let require_tld = Options::new().with_domain(DomainOptions::new().with_required_tld());
assert!(EmailAddr::<Buffer, Relax>::parse_with_options(single, require_tld).is_err());
assert!(verify_email_addr_with_options(single.as_bytes(), require_tld).is_err());
let multi = "ted.backer@gmail.com";
assert!(EmailAddr::<Buffer, Relax>::parse_with_options(multi, require_tld).is_ok());
let three_labels = Options::new().with_domain(DomainOptions::new().with_minimum_dns_labels(3));
assert!(
EmailAddr::<Buffer, Relax>::parse_with_options("user@example.com", three_labels).is_err()
);
assert!(
EmailAddr::<Buffer, Relax>::parse_with_options("user@mail.example.com", three_labels).is_ok()
);
}
#[test]
fn supports_parse_options_for_smtp_utf8_local_parts() {
let input = "用户@example.com";
let allowed = EmailAddr::<Buffer, Relax>::parse_with_options(input, Options::new()).unwrap();
assert_eq!(allowed.local_part(), "用户");
let forbidden =
Options::new().with_local(LocalOptions::new().with_smtp_utf8(SmtpUtf8Policy::Forbid));
assert!(EmailAddr::<Buffer, Relax>::parse_with_options(input, forbidden).is_err());
assert!(verify_email_addr_with_options(input.as_bytes(), forbidden).is_err());
}
#[test]
fn supports_parse_options_for_non_standard_utf8_domains() {
let input = "👋@💌.kz";
let default = EmailAddr::<Buffer, Relax>::parse_with_options(input, Options::new());
#[cfg(any(feature = "alloc", feature = "std"))]
assert_ne!(default.unwrap().as_str(), input);
#[cfg(not(any(feature = "alloc", feature = "std")))]
assert!(default.is_err());
let options = Options::new()
.with_domain(DomainOptions::new().with_unicode(DomainUnicodePolicy::NonStandardUtf8));
let addr = EmailAddr::<Buffer, Relax>::parse_with_options(input, options).unwrap();
assert_eq!(addr.as_str(), input);
assert_eq!(addr.local_part(), "👋");
assert_eq!(addr.domain_part(), "💌.kz");
assert!(EmailAddr::<Buffer, Relax>::parse_with_options("user@xn--55555577.💌", options).is_err());
assert!(EmailAddr::<Buffer, Relax>::parse_with_options("user@xn--é.example", options).is_err());
assert!(EmailAddr::<Buffer, Relax>::parse_with_options("user@ab--é.example", options).is_err());
assert!(
EmailAddr::<Buffer, Relax>::parse_with_options("user@é.123.xn--9dbne9b", options).is_err()
);
let ascii_only =
Options::new().with_domain(DomainOptions::new().with_unicode(DomainUnicodePolicy::AsciiOnly));
assert!(EmailAddr::<Buffer, Relax>::parse_with_options("user@测试.中国", ascii_only).is_err());
}
#[cfg(any(feature = "alloc", feature = "std"))]
#[test]
fn normalizes_unicode_domains_for_owned_storage() {
let addr: EmailAddr<String> = "user@测试.中国".parse().unwrap();
assert_eq!(addr.as_str(), "user@xn--0zwm56d.xn--fiqs8s");
assert_eq!(addr.local_part().as_inner(), &"user");
assert_eq!(addr.domain_part().as_inner(), &"xn--0zwm56d.xn--fiqs8s");
let stack = EmailAddr::<Buffer>::try_from("user@测试.中国").unwrap();
assert_eq!(stack.as_str(), "user@xn--0zwm56d.xn--fiqs8s");
assert!(verify_email_addr("user@测试.中国".as_bytes()).is_ok());
}
#[cfg(any(feature = "alloc", feature = "std"))]
#[test]
fn validates_ascii_alabels_through_idna() {
let valid_input = "user@xn--0zwm56d.xn--fiqs8s";
let valid = EmailAddr::<String>::try_from(valid_input).unwrap();
assert_eq!(valid.as_str(), "user@xn--0zwm56d.xn--fiqs8s");
assert!(EmailAddr::<&str>::try_from(valid_input).is_ok());
assert!(EmailAddr::<&[u8]>::try_from(valid_input.as_bytes()).is_ok());
assert!(EmailAddr::try_from_ascii_str(valid_input).is_ok());
assert!(EmailAddr::try_from_ascii_bytes(valid_input.as_bytes()).is_ok());
assert!(verify_email_addr(b"user@xn--0zwm56d.xn--fiqs8s").is_ok());
let invalid = "user@xn--55555577.com";
assert!(EmailAddr::<&str>::try_from(invalid).is_err());
assert!(EmailAddr::<&[u8]>::try_from(invalid.as_bytes()).is_err());
assert!(EmailAddr::try_from_ascii_str(invalid).is_err());
assert!(EmailAddr::try_from_ascii_bytes(invalid.as_bytes()).is_err());
assert!(EmailAddr::<String>::try_from(invalid).is_err());
assert!(EmailAddr::<Buffer>::try_from(invalid).is_err());
assert!(verify_email_addr(invalid.as_bytes()).is_err());
}
#[cfg(any(feature = "alloc", feature = "std"))]
#[test]
fn ignores_alabel_like_text_inside_address_literals() {
let literal = "user@[TAG:a.xn--payload]";
let string = EmailAddr::<String>::try_from(literal).unwrap();
assert_eq!(string.as_str(), literal);
assert!(EmailAddr::<&str>::try_from(literal).is_ok());
assert!(EmailAddr::<&[u8]>::try_from(literal.as_bytes()).is_ok());
assert!(EmailAddr::<Buffer>::try_from(literal).is_ok());
assert!(verify_email_addr(literal.as_bytes()).is_ok());
}
#[cfg(any(feature = "alloc", feature = "std"))]
#[test]
fn validates_domain_part_ascii_alabels_through_idna() {
let valid = "xn--0zwm56d.xn--fiqs8s";
assert!(DomainPart::<str>::try_from_ascii_str(valid).is_ok());
assert!(DomainPart::<[u8]>::try_from_ascii_bytes(valid.as_bytes()).is_ok());
assert!(DomainPart::<&str>::try_from(valid).is_ok());
assert!(DomainPart::<&[u8]>::try_from(valid.as_bytes()).is_ok());
assert!(verify_ascii_dns_domain(valid.as_bytes()).is_ok());
assert!(verify_ascii_domain_part(valid.as_bytes()).is_ok());
let invalid = "xn--55555577.com";
assert!(DomainPart::<str>::try_from_ascii_str(invalid).is_err());
assert!(DomainPart::<[u8]>::try_from_ascii_bytes(invalid.as_bytes()).is_err());
assert!(DomainPart::<&str>::try_from(invalid).is_err());
assert!(DomainPart::<&[u8]>::try_from(invalid.as_bytes()).is_err());
assert!(verify_ascii_dns_domain(invalid.as_bytes()).is_err());
assert!(verify_ascii_domain_part(invalid.as_bytes()).is_err());
let literal = "[TAG:a.xn--payload]";
assert!(DomainPart::<str>::try_from_ascii_str(literal).is_ok());
assert!(DomainPart::<[u8]>::try_from_ascii_bytes(literal.as_bytes()).is_ok());
assert!(DomainPart::<&str>::try_from(literal).is_ok());
assert!(DomainPart::<&[u8]>::try_from(literal.as_bytes()).is_ok());
assert!(verify_ascii_domain_part(literal.as_bytes()).is_ok());
}
#[cfg(any(feature = "alloc", feature = "std"))]
#[test]
fn preserves_ordinary_ascii_labels_next_to_idna_labels() {
let mixed = "ab--cd.xn--0zwm56d";
assert!(verify_ascii_dns_domain(mixed.as_bytes()).is_ok());
assert!(verify_ascii_domain_part(mixed.as_bytes()).is_ok());
assert!(DomainPart::<str>::try_from_ascii_str(mixed).is_ok());
assert!(EmailAddr::<&str>::try_from("user@ab--cd.xn--0zwm56d").is_ok());
let ascii: EmailAddr<String> = EmailAddr::try_from("user@ab--cd.xn--0zwm56d").unwrap();
assert_eq!(ascii.as_str(), "user@ab--cd.xn--0zwm56d");
let unicode: EmailAddr<String> = EmailAddr::try_from("user@ab--cd.测试").unwrap();
assert_eq!(unicode.as_str(), "user@ab--cd.xn--0zwm56d");
}
#[cfg(any(feature = "alloc", feature = "std"))]
#[test]
fn rejects_idna_labels_with_decoded_hyphen_violations() {
let invalid_alabel_domain = "xn----bga.com";
assert!(verify_ascii_dns_domain(invalid_alabel_domain.as_bytes()).is_err());
assert!(verify_ascii_domain_part(invalid_alabel_domain.as_bytes()).is_err());
assert!(DomainPart::<str>::try_from_ascii_str(invalid_alabel_domain).is_err());
assert!(DomainPart::<[u8]>::try_from_ascii_bytes(invalid_alabel_domain.as_bytes()).is_err());
let invalid_alabel_addr = "user@xn----bga.com";
assert!(EmailAddr::<&str>::try_from(invalid_alabel_addr).is_err());
assert!(EmailAddr::<&[u8]>::try_from(invalid_alabel_addr.as_bytes()).is_err());
assert!(EmailAddr::<String>::try_from(invalid_alabel_addr).is_err());
assert!(EmailAddr::<Cow<'_, str>>::try_from(invalid_alabel_addr).is_err());
assert!(EmailAddr::<Vec<u8>>::try_from(invalid_alabel_addr.as_bytes()).is_err());
assert!(EmailAddr::<Buffer>::try_from(invalid_alabel_addr).is_err());
assert!(verify_email_addr(invalid_alabel_addr.as_bytes()).is_err());
for input in ["user@-é.com", "user@é-.com"] {
assert!(EmailAddr::<String>::try_from(input).is_err(), "{input}");
assert!(
EmailAddr::<Cow<'_, str>>::try_from(input).is_err(),
"{input}"
);
assert!(EmailAddr::<Buffer>::try_from(input).is_err(), "{input}");
assert!(verify_email_addr(input.as_bytes()).is_err(), "{input}");
}
}
#[cfg(any(feature = "alloc", feature = "std"))]
#[test]
fn rejects_mixed_bidi_domains_after_idna_normalization() {
let rtl_alabel_domain = "123.xn--9dbne9b";
assert!(verify_ascii_dns_domain(rtl_alabel_domain.as_bytes()).is_err());
assert!(verify_ascii_domain_part(rtl_alabel_domain.as_bytes()).is_err());
assert!(DomainPart::<str>::try_from_ascii_str(rtl_alabel_domain).is_err());
for input in ["user@123.שלום", "user@123.xn--9dbne9b"] {
assert!(EmailAddr::<String>::try_from(input).is_err(), "{input}");
assert!(
EmailAddr::<Cow<'_, str>>::try_from(input).is_err(),
"{input}"
);
assert!(EmailAddr::<Buffer>::try_from(input).is_err(), "{input}");
assert!(verify_email_addr(input.as_bytes()).is_err(), "{input}");
}
}
#[cfg(not(any(feature = "alloc", feature = "std")))]
#[test]
fn borrowed_constructors_reject_ascii_alabels_without_idna() {
let valid_alabel = "user@xn--0zwm56d.xn--fiqs8s";
assert!(EmailAddr::<&str>::try_from(valid_alabel).is_err());
assert!(EmailAddr::<&[u8]>::try_from(valid_alabel.as_bytes()).is_err());
assert!(EmailAddr::try_from_ascii_str(valid_alabel).is_err());
assert!(EmailAddr::try_from_ascii_bytes(valid_alabel.as_bytes()).is_err());
let literal = "user@[TAG:a.xn--payload]";
assert!(EmailAddr::<&str>::try_from(literal).is_ok());
assert!(EmailAddr::<&[u8]>::try_from(literal.as_bytes()).is_ok());
assert!(EmailAddr::try_from_ascii_str(literal).is_ok());
assert!(EmailAddr::try_from_ascii_bytes(literal.as_bytes()).is_ok());
}
#[cfg(not(any(feature = "alloc", feature = "std")))]
#[test]
fn domain_part_rejects_ascii_alabels_without_idna() {
let valid_alabel = "xn--0zwm56d.xn--fiqs8s";
assert!(DomainPart::<str>::try_from_ascii_str(valid_alabel).is_err());
assert!(DomainPart::<[u8]>::try_from_ascii_bytes(valid_alabel.as_bytes()).is_err());
assert!(DomainPart::<&str>::try_from(valid_alabel).is_err());
assert!(DomainPart::<&[u8]>::try_from(valid_alabel.as_bytes()).is_err());
assert!(verify_ascii_dns_domain(valid_alabel.as_bytes()).is_err());
assert!(verify_ascii_domain_part(valid_alabel.as_bytes()).is_err());
let invalid_alabel = "xn--55555577.com";
assert!(DomainPart::<str>::try_from_ascii_str(invalid_alabel).is_err());
assert!(DomainPart::<[u8]>::try_from_ascii_bytes(invalid_alabel.as_bytes()).is_err());
assert!(DomainPart::<&str>::try_from(invalid_alabel).is_err());
assert!(DomainPart::<&[u8]>::try_from(invalid_alabel.as_bytes()).is_err());
assert!(verify_ascii_dns_domain(invalid_alabel.as_bytes()).is_err());
assert!(verify_ascii_domain_part(invalid_alabel.as_bytes()).is_err());
let invalid_hyphen_alabel = "xn----bga.com";
assert!(DomainPart::<str>::try_from_ascii_str(invalid_hyphen_alabel).is_err());
assert!(DomainPart::<[u8]>::try_from_ascii_bytes(invalid_hyphen_alabel.as_bytes()).is_err());
assert!(DomainPart::<&str>::try_from(invalid_hyphen_alabel).is_err());
assert!(DomainPart::<&[u8]>::try_from(invalid_hyphen_alabel.as_bytes()).is_err());
assert!(verify_ascii_dns_domain(invalid_hyphen_alabel.as_bytes()).is_err());
assert!(verify_ascii_domain_part(invalid_hyphen_alabel.as_bytes()).is_err());
let literal = "[TAG:a.xn--payload]";
assert!(DomainPart::<str>::try_from_ascii_str(literal).is_ok());
assert!(DomainPart::<[u8]>::try_from_ascii_bytes(literal.as_bytes()).is_ok());
assert!(DomainPart::<&str>::try_from(literal).is_ok());
assert!(DomainPart::<&[u8]>::try_from(literal.as_bytes()).is_ok());
assert!(verify_ascii_domain_part(literal.as_bytes()).is_ok());
}
#[cfg(any(feature = "alloc", feature = "std"))]
#[test]
fn enforces_length_after_domain_normalization() {
let unicode_label = "a".repeat(63);
let input = format!("u@{unicode_label}.{unicode_label}");
assert!(input.len() > MAX_EMAIL_ADDR_LENGTH);
let addr = EmailAddr::<String>::try_from(input.as_str()).unwrap();
let ascii_label = "a".repeat(63);
assert_eq!(addr.as_str(), format!("u@{ascii_label}.{ascii_label}"));
}
#[cfg(any(feature = "alloc", feature = "std"))]
#[test]
fn supports_smtp_utf8_local_parts() {
let addr = EmailAddr::<String>::try_from("用户@example.com").unwrap();
assert_eq!(addr.as_str(), "用户@example.com");
assert_eq!(addr.local_part().as_inner(), &"用户");
let emoji = EmailAddr::<String>::try_from("I❤️CHOCOLATE@example.com").unwrap();
assert_eq!(emoji.local_part().as_inner(), &"I❤️CHOCOLATE");
let lower = "İⅢ@example.com".to_lowercase();
let combining = EmailAddr::<String>::try_from(lower.as_str()).unwrap();
assert_eq!(combining.as_str(), lower);
let quoted = EmailAddr::<String>::try_from("\"用户 name\"@example.com").unwrap();
assert_eq!(quoted.local_part().as_inner(), &"\"用户 name\"");
assert!(verify_email_addr("用户@example.com".as_bytes()).is_ok());
assert!(verify_ascii_email_addr("用户@example.com".as_bytes()).is_err());
}
#[test]
fn validates_parts_directly() {
let local = LocalPart::try_from_ascii_str("first.last").unwrap();
assert_eq!(local.as_inner(), "first.last");
assert!(verify_ascii_local_part(b"first..last").is_err());
assert!(verify_local_part("用户.name".as_bytes()).is_ok());
let domain = DomainPart::try_from_ascii_str("example.com").unwrap();
assert_eq!(domain.as_inner(), "example.com");
assert!(verify_ascii_domain_part(b"example.123").is_ok());
assert!(verify_ascii_dns_domain(b"example.123").is_ok());
assert!(verify_ascii_domain_part(b"example_com").is_err());
assert!(verify_ascii_dns_domain(b"example_com").is_err());
assert!(verify_ascii_domain_part(b"example.com.").is_err());
assert!(verify_ascii_domain_part(b"[ipv6:::1]").is_ok());
assert!(verify_ascii_domain_part(b"[IpV6:not-ip]").is_err());
}
#[test]
fn rejects_invalid_addresses() {
for input in [
"",
"missing-at.example.com",
"@example.com",
"user@",
"user..name@example.com",
".user@example.com",
"user.@example.com",
"user@example..com",
"user@example_com",
"user@example.com.",
"user@-example.com",
"user@example-.com",
"\"unterminated@example.com",
"\"bad\\\n\"@example.com",
"user@[999.0.0.1]",
"user@[IPv6:not-ip]",
"user@[ipv6:not-ip]",
"user@[IpV6:not-ip]",
"user@[-TAG:payload]",
"user@[TAG-:payload]",
] {
assert!(EmailAddr::try_from_ascii_str(input).is_err(), "{input}");
}
}
#[cfg(any(feature = "alloc", feature = "std"))]
#[test]
fn supports_storage_backends() {
let string: EmailAddr<String> = "user@example.com".parse().unwrap();
let arc: EmailAddr<Arc<str>> = EmailAddr::try_from("user@example.com").unwrap();
let rc: EmailAddr<Rc<str>> = EmailAddr::try_from("user@example.com").unwrap();
let boxed: EmailAddr<Box<str>> = EmailAddr::try_from("user@example.com").unwrap();
let cow: EmailAddr<Cow<'_, str>> = EmailAddr::try_from("user@example.com").unwrap();
let bytes: EmailAddr<Vec<u8>> = EmailAddr::try_from(b"user@example.com".as_slice()).unwrap();
let stack: EmailAddr<Buffer> = EmailAddr::try_from("user@example.com").unwrap();
assert_eq!(string.as_str(), arc.as_str());
assert_eq!(arc.as_str(), rc.as_str());
assert_eq!(rc.as_str(), boxed.as_str());
assert_eq!(boxed.as_str(), cow.as_str());
assert_eq!(bytes.as_bytes(), stack.as_bytes());
}
#[cfg(feature = "zeroize")]
#[test]
fn supports_zeroize_for_stack_storage() {
fn assert_zeroize<T: ?Sized + zeroize::Zeroize>() {}
assert_zeroize::<Buffer>();
assert_zeroize::<EmailAddr<Buffer>>();
assert_zeroize::<EmailAddr<[u8]>>();
assert_zeroize::<LocalPart<[u8]>>();
assert_zeroize::<DomainPart<[u8]>>();
let mut addr: EmailAddr<Buffer> = EmailAddr::try_from("user@example.com").unwrap();
assert_eq!(addr.as_bytes(), b"user@example.com");
zeroize::Zeroize::zeroize(&mut addr);
assert_eq!(addr.as_inner().as_bytes(), b"");
}
#[cfg(all(feature = "zeroize", any(feature = "alloc", feature = "std")))]
#[test]
fn supports_zeroize_for_owned_storage() {
fn assert_zeroize<T: ?Sized + zeroize::Zeroize>() {}
assert_zeroize::<EmailAddr<String>>();
let mut addr: EmailAddr<String> = "user@example.com".parse().unwrap();
assert_eq!(addr.as_str(), "user@example.com");
zeroize::Zeroize::zeroize(&mut addr);
assert!(addr.as_inner().is_empty());
}
#[cfg(any(
feature = "bytes_1",
feature = "smallvec_1",
feature = "smol_str_0_3",
feature = "tinyvec_1",
feature = "triomphe_0_1",
))]
#[test]
fn supports_optional_storage_backends() {
#[cfg(feature = "smol_str_0_3")]
{
let addr: EmailAddr<smol_str_0_3::SmolStr> = EmailAddr::try_from("user@example.com").unwrap();
assert_eq!(addr.as_str(), "user@example.com");
}
#[cfg(feature = "triomphe_0_1")]
{
let text: EmailAddr<triomphe_0_1::Arc<str>> = EmailAddr::try_from("user@example.com").unwrap();
let bytes: EmailAddr<triomphe_0_1::Arc<[u8]>> =
EmailAddr::try_from(b"user@example.com".as_slice()).unwrap();
assert_eq!(text.as_str(), "user@example.com");
assert_eq!(bytes.as_bytes(), b"user@example.com");
}
#[cfg(feature = "bytes_1")]
{
let addr: EmailAddr<bytes_1::Bytes> = EmailAddr::try_from("user@example.com").unwrap();
assert_eq!(addr.as_bytes(), b"user@example.com");
let owned = bytes_1::Bytes::copy_from_slice(b"user@example.com");
let addr = EmailAddr::<bytes_1::Bytes>::try_from(owned).unwrap();
assert_eq!(addr.as_bytes(), b"user@example.com");
}
#[cfg(feature = "tinyvec_1")]
{
let addr: EmailAddr<tinyvec_1::TinyVec<[u8; 32]>> =
EmailAddr::try_from("user@example.com").unwrap();
assert_eq!(addr.as_bytes(), b"user@example.com");
}
#[cfg(feature = "smallvec_1")]
{
let addr: EmailAddr<smallvec_1::SmallVec<[u8; 32]>> =
EmailAddr::try_from("user@example.com").unwrap();
assert_eq!(addr.as_bytes(), b"user@example.com");
}
}
#[cfg(all(feature = "arbitrary", any(feature = "alloc", feature = "std")))]
#[test]
fn supports_arbitrary_for_storage_backends() {
assert_arbitrary::<EmailAddr<Buffer>>();
assert_arbitrary::<EmailAddr<String>>();
assert_arbitrary::<EmailAddr<Vec<u8>>>();
assert_arbitrary::<EmailAddr<Box<str>>>();
assert_arbitrary::<EmailAddr<Box<[u8]>>>();
assert_arbitrary::<EmailAddr<Rc<str>>>();
assert_arbitrary::<EmailAddr<Rc<[u8]>>>();
assert_arbitrary::<EmailAddr<Arc<str>>>();
assert_arbitrary::<EmailAddr<Arc<[u8]>>>();
assert_arbitrary::<EmailAddr<Cow<'_, str>>>();
assert_arbitrary::<EmailAddr<Cow<'_, [u8]>>>();
#[cfg(feature = "smol_str_0_3")]
assert_arbitrary::<EmailAddr<smol_str_0_3::SmolStr>>();
#[cfg(feature = "triomphe_0_1")]
{
assert_arbitrary::<EmailAddr<triomphe_0_1::Arc<str>>>();
assert_arbitrary::<EmailAddr<triomphe_0_1::Arc<[u8]>>>();
}
#[cfg(feature = "bytes_1")]
assert_arbitrary::<EmailAddr<bytes_1::Bytes>>();
#[cfg(feature = "tinyvec_1")]
assert_arbitrary::<EmailAddr<tinyvec_1::TinyVec<[u8; 32]>>>();
#[cfg(feature = "smallvec_1")]
assert_arbitrary::<EmailAddr<smallvec_1::SmallVec<[u8; 32]>>>();
}
#[cfg(all(feature = "quickcheck", any(feature = "alloc", feature = "std")))]
#[test]
fn supports_quickcheck_for_storage_backends() {
assert_quickcheck::<EmailAddr<Buffer>>();
assert_quickcheck::<EmailAddr<String>>();
assert_quickcheck::<EmailAddr<Vec<u8>>>();
assert_quickcheck::<EmailAddr<Box<str>>>();
assert_quickcheck::<EmailAddr<Box<[u8]>>>();
assert_quickcheck::<EmailAddr<Rc<str>>>();
assert_quickcheck::<EmailAddr<Rc<[u8]>>>();
assert_quickcheck::<EmailAddr<Arc<str>>>();
assert_quickcheck::<EmailAddr<Arc<[u8]>>>();
assert_quickcheck::<EmailAddr<Cow<'static, str>>>();
assert_quickcheck::<EmailAddr<Cow<'static, [u8]>>>();
#[cfg(feature = "smol_str_0_3")]
assert_quickcheck::<EmailAddr<smol_str_0_3::SmolStr>>();
#[cfg(feature = "triomphe_0_1")]
{
assert_quickcheck::<EmailAddr<triomphe_0_1::Arc<str>>>();
assert_quickcheck::<EmailAddr<triomphe_0_1::Arc<[u8]>>>();
}
#[cfg(feature = "bytes_1")]
assert_quickcheck::<EmailAddr<bytes_1::Bytes>>();
#[cfg(feature = "tinyvec_1")]
assert_quickcheck::<EmailAddr<tinyvec_1::TinyVec<[u8; 32]>>>();
#[cfg(feature = "smallvec_1")]
assert_quickcheck::<EmailAddr<smallvec_1::SmallVec<[u8; 32]>>>();
}
#[cfg(any(feature = "alloc", feature = "std"))]
#[test]
fn enforces_length_limits() {
let local = "a".repeat(64);
let valid = format!("{local}@example.com");
assert!(EmailAddr::<String>::try_from(valid).is_ok());
let local = "a".repeat(65);
let invalid = format!("{local}@example.com");
assert!(EmailAddr::<String>::try_from(invalid).is_err());
let long = format!("{}@{}", "a".repeat(64), "b".repeat(MAX_EMAIL_ADDR_LENGTH));
assert!(EmailAddr::<String>::try_from(long).is_err());
let long_utf8 = format!("{}@example.com", "用".repeat(90));
assert!(long_utf8.len() > MAX_EMAIL_ADDR_LENGTH);
assert!(EmailAddr::<String>::try_from(long_utf8).is_err());
}
#[test]
fn supports_parse_options_for_long_local_parts() {
let input = "reply+2a907e&3uofr1&&99cd5c22c2ca5b23655799316a8d8eb2dd83c3c487612cb9b9a00bf13f13afe2@mg1.substack.com";
assert!(EmailAddr::<Buffer, Relax>::parse_with_options(input, Options::new()).is_err());
let options = Options::new().with_limits(Limits::new().with_max_local_part_len(128));
let addr: EmailAddr<Buffer, Relax> =
EmailAddr::<Buffer, Relax>::parse_with_options(input, options).unwrap();
assert_eq!(
addr.local_part(),
"reply+2a907e&3uofr1&&99cd5c22c2ca5b23655799316a8d8eb2dd83c3c487612cb9b9a00bf13f13afe2"
);
assert_eq!(addr.domain_part(), "mg1.substack.com");
assert!(LocalPart::try_from_str(addr.local_part()).is_err());
#[cfg(any(feature = "alloc", feature = "std"))]
{
let string = EmailAddr::<String, Relax>::parse_with_options(input, options).unwrap();
assert_eq!(string.as_str(), input);
}
}
#[test]
fn parse_options_do_not_launder_relaxed_values_as_strict_parts() {
let options = Options::new()
.with_domain(DomainOptions::new().with_unicode(DomainUnicodePolicy::NonStandardUtf8));
let addr = EmailAddr::<Buffer, Relax>::parse_with_options("👋@💌.kz", options).unwrap();
assert_eq!(addr.parts(), ("👋", "💌.kz"));
assert!(DomainPart::try_from_ascii_str(addr.domain_part()).is_err());
}
#[cfg(feature = "serde")]
#[test]
fn supports_serde_core_for_stack_storage() {
assert_serde::<EmailAddr<Buffer>>();
assert_serialize::<LocalPart<&str>>();
assert_serialize::<DomainPart<&str>>();
assert_serde::<Options>();
assert_serde::<LocalOptions>();
assert_serde::<DomainOptions>();
assert_serde::<Limits>();
assert_serde::<SmtpUtf8Policy>();
assert_serde::<DomainLiteralPolicy>();
assert_serde::<DomainUnicodePolicy>();
}
#[cfg(feature = "serde")]
#[test]
fn supports_serde_core_for_options() {
use core::fmt;
use serde_core::{
Deserialize, Serialize, Serializer,
de::{
DeserializeSeed, Deserializer, MapAccess, SeqAccess, Visitor,
value::{Error as DeError, MapDeserializer, StrDeserializer},
},
ser::{Error as SerErrorTrait, Impossible, SerializeStruct},
};
use std::{string::String, vec::Vec};
#[derive(Debug, PartialEq, Eq)]
enum Token {
Str(String),
U8(u8),
U64(u64),
Struct {
name: &'static str,
fields: Vec<(&'static str, Token)>,
},
}
#[derive(Debug)]
struct SerError(String);
impl fmt::Display for SerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for SerError {}
impl SerErrorTrait for SerError {
fn custom<T>(msg: T) -> Self
where
T: fmt::Display,
{
Self(msg.to_string())
}
}
struct TokenSerializer {
human: bool,
}
struct TokenStructSerializer {
human: bool,
name: &'static str,
fields: Vec<(&'static str, Token)>,
}
macro_rules! reject_serialize {
($($name:ident($($arg:ident: $ty:ty),*) -> $ret:ty;)*) => {
$(
fn $name(self, $($arg: $ty),*) -> Result<$ret, Self::Error> {
let _ = ($($arg),*);
Err(<Self::Error as SerErrorTrait>::custom("unsupported token"))
}
)*
};
}
impl Serializer for TokenSerializer {
type Ok = Token;
type Error = SerError;
type SerializeSeq = Impossible<Token, SerError>;
type SerializeTuple = Impossible<Token, SerError>;
type SerializeTupleStruct = Impossible<Token, SerError>;
type SerializeTupleVariant = Impossible<Token, SerError>;
type SerializeMap = Impossible<Token, SerError>;
type SerializeStruct = TokenStructSerializer;
type SerializeStructVariant = Impossible<Token, SerError>;
fn is_human_readable(&self) -> bool {
self.human
}
fn serialize_str(self, value: &str) -> Result<Self::Ok, Self::Error> {
Ok(Token::Str(value.to_owned()))
}
fn serialize_u8(self, value: u8) -> Result<Self::Ok, Self::Error> {
Ok(Token::U8(value))
}
fn serialize_u64(self, value: u64) -> Result<Self::Ok, Self::Error> {
Ok(Token::U64(value))
}
fn serialize_struct(
self,
name: &'static str,
len: usize,
) -> Result<Self::SerializeStruct, Self::Error> {
Ok(TokenStructSerializer {
human: self.human,
name,
fields: Vec::with_capacity(len),
})
}
fn collect_str<T>(self, value: &T) -> Result<Self::Ok, Self::Error>
where
T: ?Sized + fmt::Display,
{
Ok(Token::Str(value.to_string()))
}
reject_serialize! {
serialize_bool(value: bool) -> Token;
serialize_i8(value: i8) -> Token;
serialize_i16(value: i16) -> Token;
serialize_i32(value: i32) -> Token;
serialize_i64(value: i64) -> Token;
serialize_u16(value: u16) -> Token;
serialize_u32(value: u32) -> Token;
serialize_f32(value: f32) -> Token;
serialize_f64(value: f64) -> Token;
serialize_char(value: char) -> Token;
serialize_bytes(value: &[u8]) -> Token;
}
fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
Err(<Self::Error as SerErrorTrait>::custom("unsupported token"))
}
fn serialize_some<T>(self, _value: &T) -> Result<Self::Ok, Self::Error>
where
T: ?Sized + Serialize,
{
Err(<Self::Error as SerErrorTrait>::custom("unsupported token"))
}
fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
Err(<Self::Error as SerErrorTrait>::custom("unsupported token"))
}
fn serialize_unit_struct(self, _name: &'static str) -> Result<Self::Ok, Self::Error> {
Err(<Self::Error as SerErrorTrait>::custom("unsupported token"))
}
fn serialize_unit_variant(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
) -> Result<Self::Ok, Self::Error> {
Err(<Self::Error as SerErrorTrait>::custom("unsupported token"))
}
fn serialize_newtype_struct<T>(
self,
_name: &'static str,
_value: &T,
) -> Result<Self::Ok, Self::Error>
where
T: ?Sized + Serialize,
{
Err(<Self::Error as SerErrorTrait>::custom("unsupported token"))
}
fn serialize_newtype_variant<T>(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
_value: &T,
) -> Result<Self::Ok, Self::Error>
where
T: ?Sized + Serialize,
{
Err(<Self::Error as SerErrorTrait>::custom("unsupported token"))
}
fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
Err(<Self::Error as SerErrorTrait>::custom("unsupported token"))
}
fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple, Self::Error> {
Err(<Self::Error as SerErrorTrait>::custom("unsupported token"))
}
fn serialize_tuple_struct(
self,
_name: &'static str,
_len: usize,
) -> Result<Self::SerializeTupleStruct, Self::Error> {
Err(<Self::Error as SerErrorTrait>::custom("unsupported token"))
}
fn serialize_tuple_variant(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
_len: usize,
) -> Result<Self::SerializeTupleVariant, Self::Error> {
Err(<Self::Error as SerErrorTrait>::custom("unsupported token"))
}
fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
Err(<Self::Error as SerErrorTrait>::custom("unsupported token"))
}
fn serialize_struct_variant(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
_len: usize,
) -> Result<Self::SerializeStructVariant, Self::Error> {
Err(<Self::Error as SerErrorTrait>::custom("unsupported token"))
}
}
impl SerializeStruct for TokenStructSerializer {
type Ok = Token;
type Error = SerError;
fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<(), Self::Error>
where
T: ?Sized + Serialize,
{
let value = value.serialize(TokenSerializer { human: self.human })?;
self.fields.push((key, value));
Ok(())
}
fn end(self) -> Result<Self::Ok, Self::Error> {
Ok(Token::Struct {
name: self.name,
fields: self.fields,
})
}
}
struct BinaryU8Deserializer(u8);
impl<'de> Deserializer<'de> for BinaryU8Deserializer {
type Error = DeError;
fn is_human_readable(&self) -> bool {
false
}
fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
visitor.visit_u8(self.0)
}
fn deserialize_u8<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
visitor.visit_u8(self.0)
}
serde_core::forward_to_deserialize_any! {
bool i8 i16 i32 i64 u16 u32 u64 f32 f64 char str string bytes byte_buf
option unit unit_struct newtype_struct seq tuple tuple_struct map struct
enum identifier ignored_any
}
}
struct BinaryU64Deserializer(u64);
impl<'de> Deserializer<'de> for BinaryU64Deserializer {
type Error = DeError;
fn is_human_readable(&self) -> bool {
false
}
fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
visitor.visit_u64(self.0)
}
fn deserialize_u8<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
visitor.visit_u64(self.0)
}
fn deserialize_u64<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
visitor.visit_u64(self.0)
}
serde_core::forward_to_deserialize_any! {
bool i8 i16 i32 i64 u16 u32 f32 f64 char str string bytes byte_buf
option unit unit_struct newtype_struct seq tuple tuple_struct map struct
enum identifier ignored_any
}
}
struct HumanBytesDeserializer<'a>(&'a [u8]);
impl<'de> Deserializer<'de> for HumanBytesDeserializer<'_> {
type Error = DeError;
fn is_human_readable(&self) -> bool {
true
}
fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
visitor.visit_bytes(self.0)
}
fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
visitor.visit_bytes(self.0)
}
fn deserialize_bytes<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
visitor.visit_bytes(self.0)
}
serde_core::forward_to_deserialize_any! {
bool i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 char string byte_buf
option unit unit_struct newtype_struct seq tuple tuple_struct map struct
enum identifier ignored_any
}
}
enum BinaryValue {
U8(u8),
U64(u64),
Map(Vec<(&'static str, BinaryValue)>),
Seq(Vec<BinaryValue>),
}
struct BinaryMapAccess {
entries: std::vec::IntoIter<(&'static str, BinaryValue)>,
value: Option<BinaryValue>,
}
impl<'de> MapAccess<'de> for BinaryMapAccess {
type Error = DeError;
fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Self::Error>
where
K: DeserializeSeed<'de>,
{
let Some((key, value)) = self.entries.next() else {
return Ok(None);
};
self.value = Some(value);
seed
.deserialize(StrDeserializer::<DeError>::new(key))
.map(Some)
}
fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Self::Error>
where
V: DeserializeSeed<'de>,
{
seed.deserialize(self.value.take().expect("map value follows map key"))
}
}
struct BinarySeqAccess {
values: std::vec::IntoIter<BinaryValue>,
}
impl<'de> SeqAccess<'de> for BinarySeqAccess {
type Error = DeError;
fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>, Self::Error>
where
T: DeserializeSeed<'de>,
{
self
.values
.next()
.map(|value| seed.deserialize(value))
.transpose()
}
}
impl<'de> Deserializer<'de> for BinaryValue {
type Error = DeError;
fn is_human_readable(&self) -> bool {
false
}
fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
match self {
Self::U8(value) => visitor.visit_u8(value),
Self::U64(value) => visitor.visit_u64(value),
Self::Map(entries) => visitor.visit_map(BinaryMapAccess {
entries: entries.into_iter(),
value: None,
}),
Self::Seq(values) => visitor.visit_seq(BinarySeqAccess {
values: values.into_iter(),
}),
}
}
fn deserialize_u8<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
self.deserialize_any(visitor)
}
fn deserialize_u64<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
self.deserialize_any(visitor)
}
fn deserialize_map<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
self.deserialize_any(visitor)
}
fn deserialize_struct<V>(
self,
_name: &'static str,
_fields: &'static [&'static str],
visitor: V,
) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
self.deserialize_any(visitor)
}
serde_core::forward_to_deserialize_any! {
bool i8 i16 i32 i64 u16 u32 f32 f64 char str string bytes byte_buf
option unit unit_struct newtype_struct seq tuple tuple_struct enum
identifier ignored_any
}
}
fn binary_local() -> BinaryValue {
BinaryValue::Map(vec![("smtp_utf8", BinaryValue::U8(1))])
}
fn binary_domain() -> BinaryValue {
BinaryValue::Map(vec![
("minimum_dns_labels", BinaryValue::U64(2)),
("literals", BinaryValue::U8(0)),
("unicode", BinaryValue::U8(2)),
])
}
fn binary_limits() -> BinaryValue {
BinaryValue::Map(vec![("max_local_part_len", BinaryValue::U64(128))])
}
assert_eq!(
SmtpUtf8Policy::Forbid
.serialize(TokenSerializer { human: true })
.unwrap(),
Token::Str("forbid".to_owned())
);
assert_eq!(
SmtpUtf8Policy::Allow
.serialize(TokenSerializer { human: false })
.unwrap(),
Token::U8(0)
);
assert_eq!(
DomainLiteralPolicy::Forbid
.serialize(TokenSerializer { human: false })
.unwrap(),
Token::U8(1)
);
assert_eq!(
DomainUnicodePolicy::NonStandardUtf8
.serialize(TokenSerializer { human: true })
.unwrap(),
Token::Str("raw".to_owned())
);
assert_eq!(
DomainUnicodePolicy::NonStandardUtf8
.serialize(TokenSerializer { human: false })
.unwrap(),
Token::U8(2)
);
let human = DomainUnicodePolicy::deserialize(StrDeserializer::<DeError>::new("ascii")).unwrap();
assert_eq!(human, DomainUnicodePolicy::AsciiOnly);
let human_alias =
DomainUnicodePolicy::deserialize(StrDeserializer::<DeError>::new("ascii_only")).unwrap();
assert_eq!(human_alias, DomainUnicodePolicy::AsciiOnly);
let raw_alias =
DomainUnicodePolicy::deserialize(StrDeserializer::<DeError>::new("non_standard_utf8")).unwrap();
assert_eq!(raw_alias, DomainUnicodePolicy::NonStandardUtf8);
let binary = DomainUnicodePolicy::deserialize(BinaryU8Deserializer(1)).unwrap();
assert_eq!(binary, DomainUnicodePolicy::Idna);
let smtp_binary = SmtpUtf8Policy::deserialize(BinaryU64Deserializer(1)).unwrap();
assert_eq!(smtp_binary, SmtpUtf8Policy::Forbid);
let literal_bytes = DomainLiteralPolicy::deserialize(HumanBytesDeserializer(b"forbid")).unwrap();
assert_eq!(literal_bytes, DomainLiteralPolicy::Forbid);
assert!(DomainLiteralPolicy::deserialize(HumanBytesDeserializer(b"\xff")).is_err());
assert!(DomainUnicodePolicy::deserialize(BinaryU64Deserializer(300)).is_err());
assert!(DomainLiteralPolicy::deserialize(BinaryU8Deserializer(9)).is_err());
let options = Options::new()
.with_local(LocalOptions::new().with_smtp_utf8(SmtpUtf8Policy::Forbid))
.with_domain(
DomainOptions::new()
.with_minimum_dns_labels(2)
.with_unicode(DomainUnicodePolicy::NonStandardUtf8),
)
.with_limits(Limits::new().with_max_local_part_len(128));
let token = options.serialize(TokenSerializer { human: true }).unwrap();
assert!(matches!(
token,
Token::Struct {
name: "Options",
..
}
));
let options_binary = bincode::serialize(&options).unwrap();
assert_eq!(
bincode::deserialize::<Options>(&options_binary).unwrap(),
options
);
let local_binary = bincode::serialize(&options.local()).unwrap();
assert_eq!(
bincode::deserialize::<LocalOptions>(&local_binary).unwrap(),
options.local()
);
let domain_binary = bincode::serialize(&options.domain()).unwrap();
assert_eq!(
bincode::deserialize::<DomainOptions>(&domain_binary).unwrap(),
options.domain()
);
let limits_binary = bincode::serialize(&options.limits()).unwrap();
assert_eq!(
bincode::deserialize::<Limits>(&limits_binary).unwrap(),
options.limits()
);
let sequence_options = Options::deserialize(BinaryValue::Seq(vec![
BinaryValue::Seq(vec![BinaryValue::U8(1)]),
BinaryValue::Seq(vec![
BinaryValue::U64(2),
BinaryValue::U8(0),
BinaryValue::U8(2),
]),
BinaryValue::Seq(vec![BinaryValue::U64(128)]),
]))
.unwrap();
assert_eq!(sequence_options, options);
assert!(Options::deserialize(BinaryValue::Seq(vec![])).is_err());
assert!(
Options::deserialize(BinaryValue::Seq(vec![BinaryValue::Seq(vec![
BinaryValue::U8(1)
])]))
.is_err()
);
assert!(LocalOptions::deserialize(BinaryValue::Seq(vec![])).is_err());
assert!(DomainOptions::deserialize(BinaryValue::Seq(vec![BinaryValue::U64(2)])).is_err());
assert!(
DomainOptions::deserialize(BinaryValue::Seq(vec![
BinaryValue::U64(2),
BinaryValue::U8(0)
]))
.is_err()
);
assert!(Limits::deserialize(BinaryValue::Seq(vec![])).is_err());
let map_options = Options::deserialize(BinaryValue::Map(vec![
("local", binary_local()),
("domain", binary_domain()),
("limits", binary_limits()),
]))
.unwrap();
assert_eq!(map_options, options);
assert_eq!(
LocalOptions::deserialize(binary_local()).unwrap(),
options.local()
);
assert_eq!(
DomainOptions::deserialize(binary_domain()).unwrap(),
options.domain()
);
assert_eq!(
Limits::deserialize(binary_limits()).unwrap(),
options.limits()
);
assert!(Options::deserialize(BinaryValue::Map(vec![])).is_err());
assert!(Options::deserialize(BinaryValue::Map(vec![("local", binary_local())])).is_err());
assert!(LocalOptions::deserialize(BinaryValue::Map(vec![])).is_err());
assert!(
DomainOptions::deserialize(BinaryValue::Map(vec![
("minimum_dns_labels", BinaryValue::U64(2)),
("literals", BinaryValue::U8(0)),
]))
.is_err()
);
assert!(Limits::deserialize(BinaryValue::Map(vec![])).is_err());
let empty = core::iter::empty::<(&str, StrDeserializer<'_, DeError>)>();
let default_options = Options::deserialize(MapDeserializer::new(empty)).unwrap();
assert_eq!(default_options, Options::new());
let domain = DomainOptions::deserialize(MapDeserializer::new(
[("unicode", StrDeserializer::<DeError>::new("raw"))].into_iter(),
))
.unwrap();
assert_eq!(domain.minimum_dns_labels(), 1);
assert_eq!(domain.literals(), DomainLiteralPolicy::Allow);
assert_eq!(domain.unicode(), DomainUnicodePolicy::NonStandardUtf8);
let default_local = LocalOptions::deserialize(MapDeserializer::new(core::iter::empty::<(
&str,
StrDeserializer<'_, DeError>,
)>()))
.unwrap();
assert_eq!(default_local, LocalOptions::new());
let default_limits = Limits::deserialize(MapDeserializer::new(core::iter::empty::<(
&str,
StrDeserializer<'_, DeError>,
)>()))
.unwrap();
assert_eq!(default_limits, Limits::new());
}
#[cfg(feature = "serde")]
#[test]
fn supports_human_readable_config_formats_for_options() {
let options = Options::new()
.with_local(LocalOptions::new().with_smtp_utf8(SmtpUtf8Policy::Forbid))
.with_domain(
DomainOptions::new()
.with_minimum_dns_labels(2)
.with_literals(DomainLiteralPolicy::Forbid)
.with_unicode(DomainUnicodePolicy::NonStandardUtf8),
)
.with_limits(Limits::new().with_max_local_part_len(128));
let json = serde_json::to_string(&options).unwrap();
assert!(json.contains("\"smtp_utf8\":\"forbid\""));
assert!(json.contains("\"unicode\":\"raw\""));
assert_eq!(serde_json::from_str::<Options>(&json).unwrap(), options);
assert_eq!(
serde_json::from_str::<DomainOptions>(r#"{"unicode":"non_standard_utf8"}"#)
.unwrap()
.unicode(),
DomainUnicodePolicy::NonStandardUtf8
);
let toml_doc = toml::to_string(&options).unwrap();
assert!(toml_doc.contains("smtp_utf8 = \"forbid\""));
assert!(toml_doc.contains("unicode = \"raw\""));
assert_eq!(toml::from_str::<Options>(&toml_doc).unwrap(), options);
assert_eq!(
toml::from_str::<DomainOptions>("unicode = \"ascii_only\"")
.unwrap()
.unicode(),
DomainUnicodePolicy::AsciiOnly
);
let yaml = yaml_serde::to_string(&options).unwrap();
assert!(yaml.contains("smtp_utf8: forbid"));
assert!(yaml.contains("unicode: raw"));
assert_eq!(yaml_serde::from_str::<Options>(&yaml).unwrap(), options);
assert_eq!(
yaml_serde::from_str::<DomainOptions>("unicode: raw_utf8")
.unwrap()
.unicode(),
DomainUnicodePolicy::NonStandardUtf8
);
}
#[cfg(feature = "serde")]
#[test]
fn rejects_invalid_options_config_fields() {
for input in [
"[]",
r#"{"unknown":{}}"#,
r#"{"local":{},"local":{}}"#,
r#"{"domain":{},"domain":{}}"#,
r#"{"limits":{},"limits":{}}"#,
] {
assert!(serde_json::from_str::<Options>(input).is_err(), "{input}");
}
for input in [
"[]",
r#"{"unknown":"allow"}"#,
r#"{"smtp_utf8":"allow","smtp_utf8":"forbid"}"#,
] {
assert!(
serde_json::from_str::<LocalOptions>(input).is_err(),
"{input}"
);
}
for input in [
"[]",
r#"{"unknown":"allow"}"#,
r#"{"minimum_dns_labels":1,"minimum_dns_labels":2}"#,
r#"{"literals":"allow","literals":"forbid"}"#,
r#"{"unicode":"idna","unicode":"raw"}"#,
] {
assert!(
serde_json::from_str::<DomainOptions>(input).is_err(),
"{input}"
);
}
for input in [
"[]",
r#"{"unknown":64}"#,
r#"{"max_local_part_len":64,"max_local_part_len":128}"#,
] {
assert!(serde_json::from_str::<Limits>(input).is_err(), "{input}");
}
}
#[cfg(feature = "clap")]
#[test]
fn supports_clap_for_options() {
use clap::Parser;
#[derive(Debug, Parser)]
struct Cli {
#[command(flatten)]
options: Options,
}
let default = Cli::try_parse_from(["emailaddr-test"]).unwrap();
assert_eq!(default.options, Options::new());
let cli = Cli::try_parse_from([
"emailaddr-test",
"--email-local-smtp-utf8",
"forbid",
"--email-domain-minimum-dns-labels",
"2",
"--email-domain-literals",
"forbid",
"--email-domain-unicode",
"raw",
"--email-limits-max-local-part-len",
"128",
])
.unwrap();
assert_eq!(cli.options.local().smtp_utf8(), SmtpUtf8Policy::Forbid);
assert_eq!(cli.options.domain().minimum_dns_labels(), 2);
assert_eq!(cli.options.domain().literals(), DomainLiteralPolicy::Forbid);
assert_eq!(
cli.options.domain().unicode(),
DomainUnicodePolicy::NonStandardUtf8
);
assert_eq!(cli.options.limits().max_local_part_len(), 128);
let alias_cli = Cli::try_parse_from([
"emailaddr-test",
"--email-domain-unicode",
"non_standard_utf8",
])
.unwrap();
assert_eq!(
alias_cli.options.domain().unicode(),
DomainUnicodePolicy::NonStandardUtf8
);
}
#[cfg(feature = "serde")]
#[test]
fn supports_serde_core_for_custom_string_storage() {
struct CustomStr(&'static str);
impl AsRef<str> for CustomStr {
fn as_ref(&self) -> &str {
self.0
}
}
impl emailaddr::EmailAddrSerdeStorage for CustomStr {
fn as_valid_str(&self) -> &str {
self.as_ref()
}
}
assert_serialize::<EmailAddr<CustomStr>>();
assert_serialize::<LocalPart<CustomStr>>();
assert_serialize::<DomainPart<CustomStr>>();
}
#[cfg(all(feature = "serde", any(feature = "alloc", feature = "std")))]
#[test]
fn supports_serde_core_for_owned_storage() {
assert_serde::<EmailAddr<String>>();
assert_serde::<EmailAddr<Vec<u8>>>();
assert_serde::<EmailAddr<Box<str>>>();
assert_serde::<EmailAddr<Box<[u8]>>>();
assert_serde::<EmailAddr<Rc<str>>>();
assert_serde::<EmailAddr<Rc<[u8]>>>();
assert_serde::<EmailAddr<Arc<str>>>();
assert_serde::<EmailAddr<Arc<[u8]>>>();
assert_serialize::<EmailAddr<Cow<'static, str>>>();
assert_serialize::<EmailAddr<Cow<'static, [u8]>>>();
assert_deserialize::<'static, EmailAddr<Cow<'static, [u8]>>>();
#[cfg(feature = "smol_str_0_3")]
assert_serde::<EmailAddr<smol_str_0_3::SmolStr>>();
#[cfg(feature = "triomphe_0_1")]
{
assert_serde::<EmailAddr<triomphe_0_1::Arc<str>>>();
assert_serde::<EmailAddr<triomphe_0_1::Arc<[u8]>>>();
}
#[cfg(feature = "bytes_1")]
assert_serde::<EmailAddr<bytes_1::Bytes>>();
#[cfg(feature = "tinyvec_1")]
assert_serde::<EmailAddr<tinyvec_1::TinyVec<[u8; 32]>>>();
#[cfg(feature = "smallvec_1")]
assert_serde::<EmailAddr<smallvec_1::SmallVec<[u8; 32]>>>();
}
#[cfg(all(feature = "serde", any(feature = "alloc", feature = "std")))]
#[test]
fn serde_deserialization_rejects_malformed_ascii_alabels() {
use serde_core::{
Deserialize,
de::value::{Error, StrDeserializer},
};
let valid = StrDeserializer::<Error>::new("user@xn--0zwm56d.xn--fiqs8s");
assert!(EmailAddr::<String>::deserialize(valid).is_ok());
let invalid = StrDeserializer::<Error>::new("user@xn--55555577.com");
assert!(EmailAddr::<String>::deserialize(invalid).is_err());
let invalid_buffer = StrDeserializer::<Error>::new("user@xn--55555577.com");
assert!(EmailAddr::<Buffer>::deserialize(invalid_buffer).is_err());
let valid_box_bytes = StrDeserializer::<Error>::new("user@example.com");
assert!(EmailAddr::<Box<[u8]>>::deserialize(valid_box_bytes).is_ok());
let valid_cow_bytes = StrDeserializer::<Error>::new("user@example.com");
assert!(EmailAddr::<Cow<'_, [u8]>>::deserialize(valid_cow_bytes).is_ok());
let invalid_hyphen = StrDeserializer::<Error>::new("user@xn----bga.com");
assert!(EmailAddr::<String>::deserialize(invalid_hyphen).is_err());
let invalid_hyphen_buffer = StrDeserializer::<Error>::new("user@xn----bga.com");
assert!(EmailAddr::<Buffer>::deserialize(invalid_hyphen_buffer).is_err());
let invalid_bidi = StrDeserializer::<Error>::new("user@123.xn--9dbne9b");
assert!(EmailAddr::<String>::deserialize(invalid_bidi).is_err());
let invalid_bidi_buffer = StrDeserializer::<Error>::new("user@123.xn--9dbne9b");
assert!(EmailAddr::<Buffer>::deserialize(invalid_bidi_buffer).is_err());
let valid_buffer = StrDeserializer::<Error>::new("user@xn--0zwm56d.xn--fiqs8s");
assert!(EmailAddr::<Buffer>::deserialize(valid_buffer).is_ok());
let literal = StrDeserializer::<Error>::new("user@[TAG:a.xn--payload]");
assert!(EmailAddr::<String>::deserialize(literal).is_ok());
}
#[cfg(all(feature = "serde", not(any(feature = "alloc", feature = "std"))))]
#[test]
fn serde_only_buffer_deserialization_rejects_ascii_alabels() {
use serde_core::{
Deserialize,
de::value::{Error, StrDeserializer},
};
let invalid = StrDeserializer::<Error>::new("user@xn--55555577.com");
assert!(EmailAddr::<Buffer>::deserialize(invalid).is_err());
let valid_alabel = StrDeserializer::<Error>::new("user@xn--0zwm56d.xn--fiqs8s");
assert!(EmailAddr::<Buffer>::deserialize(valid_alabel).is_err());
let literal = StrDeserializer::<Error>::new("user@[TAG:a.xn--payload]");
assert!(EmailAddr::<Buffer>::deserialize(literal).is_ok());
}