Type-safe, zero-copy, validated email addresses with generic storage, IDNA domain support, and no_std/no-alloc compatibility.
emailaddr provides validated email address types with a storage model similar
to hostaddr:
EmailAddr<S = str>stores a validated full address using caller-chosen storage.LocalPart<S = str>validates dot-atom and quoted-string local-parts.DomainPart<S = str>validates RFC 5321 domain-parts and bracketed address literals.Bufferstores a full validated address on the stack forno_std/ no-allocation use.
With the default std feature, SMTPUTF8 local-parts are accepted and Unicode
domain-parts are normalized to IDNA/punycode. The verify_ascii_* helpers
remain strict ASCII validators.
[dependencies]
emailaddr = "1"# #[cfg(any(feature = "alloc", feature = "std"))]
# {
use emailaddr::EmailAddr;
let addr: EmailAddr<String> = "user.name@example.com".parse().unwrap();
assert_eq!(addr.local_part().as_inner(), &"user.name");
assert_eq!(addr.domain_part().as_inner(), &"example.com");
let quoted = EmailAddr::try_from_ascii_str("\"user name\"@example.com").unwrap();
assert_eq!(quoted.local_part().as_inner(), &"\"user name\"");
let idn: EmailAddr<String> = "user@测试.中国".parse().unwrap();
assert_eq!(idn.as_str(), "user@xn--0zwm56d.xn--fiqs8s");
let smtp_utf8: EmailAddr<String> = "用户@example.com".parse().unwrap();
assert_eq!(smtp_utf8.local_part().as_inner(), &"用户");
# }use emailaddr::{DomainPart, EmailAddr, LocalPart};
let addr: &EmailAddr<str> = EmailAddr::try_from_ascii_str("user@example.com").unwrap();
assert_eq!(addr.as_inner(), "user@example.com");
assert_eq!(addr.local_part_ref().as_inner(), "user");
assert_eq!(addr.domain_part_ref().as_inner(), "example.com");
let bytes: &EmailAddr<[u8]> = addr.as_bytes_addr();
assert_eq!(bytes.as_str_addr().as_inner(), "user@example.com");
let local: &LocalPart<str> = LocalPart::try_from_ascii_str("user").unwrap();
let domain: &DomainPart<str> = DomainPart::try_from_ascii_str("example.com").unwrap();
assert_eq!(local.as_bytes().as_inner(), b"user");
assert_eq!(domain.as_bytes().as_inner(), b"example.com");# #[cfg(any(feature = "alloc", feature = "std"))]
# {
use emailaddr::{Buffer, EmailAddr};
use std::sync::Arc;
let owned: EmailAddr<String> = "user@example.com".parse().unwrap();
let shared: EmailAddr<Arc<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!(owned.as_str(), shared.as_str());
assert_eq!(bytes.as_bytes(), stack.as_bytes());
# }Allocating parses report whether the original storage was retained or the
address was normalized into [Buffer]. The parser fully validates IDNA
A-labels; canonical A-labels whose normalized bytes are unchanged remain
Original, while Unicode domains, Unicode dot separators, and non-canonical
A-labels are reported as Normalized:
# #[cfg(any(feature = "alloc", feature = "std"))]
# {
use emailaddr::{EmailAddr, ParseEmailAddrOutput};
let output = EmailAddr::<&str>::try_from_str("user@测试.中国").unwrap();
match output {
ParseEmailAddrOutput::Original(_) => unreachable!("Unicode domains normalize"),
ParseEmailAddrOutput::Normalized(addr) => {
assert_eq!(addr.as_str(), "user@xn--0zwm56d.xn--fiqs8s");
}
}
# }For example, a canonical A-label keeps its borrowed storage:
# #[cfg(any(feature = "alloc", feature = "std"))]
# {
use emailaddr::{EmailAddr, ParseEmailAddrOutput};
let output = EmailAddr::<&str>::try_from_str("user@xn--0zwm56d.xn--fiqs8s").unwrap();
assert!(matches!(output, ParseEmailAddrOutput::Original(_)));
# }# #[cfg(any(feature = "alloc", feature = "std"))]
# {
use emailaddr::{
verify_ascii_domain_part,
verify_ascii_email_addr,
verify_ascii_local_part,
verify_email_addr,
};
assert!(verify_ascii_email_addr(b"user@example.com").is_ok());
assert!(verify_email_addr("user@测试.中国".as_bytes()).is_ok());
assert!(verify_email_addr("用户@example.com".as_bytes()).is_ok());
assert!(verify_ascii_local_part(b"user.name").is_ok());
assert!(verify_ascii_domain_part(b"[IPv6:::1]").is_ok());
assert!(verify_ascii_email_addr(b"user..name@example.com").is_err());
assert!(verify_ascii_email_addr(b"user@example_com").is_err());
assert!(verify_ascii_email_addr(b"user@example.com.").is_err());
# }Default parsing stays RFC-compatible. [Options] adds application policy and
explicit non-standard relaxations without changing the default constructors.
parse_with_options is available on EmailAddr<S, Relax>, so relaxed parsing
is visible in the type while ordinary EmailAddr<S> remains strict.
use emailaddr::{
Buffer,
DomainOptions,
DomainUnicodePolicy,
EmailAddr,
Limits,
LocalOptions,
Options,
Relax,
SmtpUtf8Policy,
};
let require_tld = Options::new()
.with_domain(DomainOptions::new().with_required_tld());
assert!(EmailAddr::<Buffer, Relax>::parse_with_options("ted.backer@gmail", require_tld).is_err());
let long_local = Options::new()
.with_limits(Limits::new().with_max_local_part_len(128));
assert!(EmailAddr::<Buffer, Relax>::parse_with_options(
"reply+2a907e&3uofr1&&99cd5c22c2ca5b23655799316a8d8eb2dd83c3c487612cb9b9a00bf13f13afe2@mg1.substack.com",
long_local,
).is_ok());
let ascii_local = Options::new()
.with_local(LocalOptions::new().with_smtp_utf8(SmtpUtf8Policy::Forbid));
assert!(EmailAddr::<Buffer, Relax>::parse_with_options("用户@example.com", ascii_local).is_err());
let no_literals = Options::new()
.with_domain(DomainOptions::new().without_domain_literals());
assert!(EmailAddr::<Buffer, Relax>::parse_with_options("user@[127.0.0.1]", no_literals).is_err());
let raw_domain = Options::new()
.with_domain(DomainOptions::new().with_unicode(DomainUnicodePolicy::NonStandardUtf8));
let addr = EmailAddr::<Buffer, Relax>::parse_with_options("👋@💌.kz", raw_domain).unwrap();
assert_eq!(addr.as_str(), "👋@💌.kz");DomainUnicodePolicy::NonStandardUtf8 preserves raw UTF-8 DNS labels as
application data. It is not SMTP/DNS-compatible IDNA normalization.
The DomainUnicodePolicy::as_str() values, CLI values, and human-readable
serde values for this policy are ascii, idna, and raw.
Human-readable serde formats such as JSON, YAML, and TOML serialize option
enums with those same string values, so configuration files can use fields like:
[domain]
unicode = "raw"
literals = "forbid"
minimum_dns_labels = 2| Feature | Description |
|---|---|
std (default) |
Standard library support and IDNA domain normalization |
alloc |
Allocation support without std, including IDNA domain normalization |
serde |
serde_core-backed serialization for addresses and parts; strict EmailAddr and parsing options also support deserialization, while relaxed EmailAddr, LocalPart, and DomainPart are serialize-only |
clap |
clap::Args / ValueEnum support for parsing options; CLI flags only, no environment-variable wiring |
bytes / bytes_1 |
bytes::Bytes storage support |
smallvec / smallvec_1 |
smallvec::SmallVec byte storage support |
smol_str / smol_str_0_3 |
smol_str::SmolStr storage support |
tinyvec / tinyvec_1 |
tinyvec::TinyVec byte storage support |
triomphe / triomphe_0_1 |
triomphe::Arc<str> and triomphe::Arc<[u8]> storage support |
arbitrary |
Invariant-preserving arbitrary::Arbitrary impls for owned storage backends |
quickcheck |
Invariant-preserving quickcheck::Arbitrary impls for owned storage backends |
Enable alloc or std for IDNA-normalizing strict EmailAddr deserialization;
serde-only/no-alloc deserialization accepts structural ASCII forms and rejects
A-label domains requiring IDNA validation. EmailAddr<S, Relax>, LocalPart,
and DomainPart remain serialize-only.
Human-readable Serde uses the validated address string and stable policy names
(allow, forbid, ascii, idna, and raw). Binary Serde representations
are not stable wire formats; support is limited to round trips within the
current crate version. Strict EmailAddr values with supported storage and
Options can deserialize; EmailAddr<S, Relax>, LocalPart, and DomainPart
only serialize.
The parser property suite runs 5,000 QuickCheck cases by default. To reproduce the extended review run, use:
QUICKCHECK_TESTS=50000 cargo test --features quickcheck --test parser_properties
emailaddr validates the address forms relevant to an address value:
- RFC 5321 SMTP mailbox syntax and length limits
- RFC 5322
addr-speclocal-part/domain token grammar where it overlaps with mailbox addresses - RFC 6531 SMTPUTF8 local-parts
- RFC 6532 UTF-8 header character model for address tokens
- RFC 1123 host label digit relaxation for domains
- RFC 4291 IPv6 address literals
- RFC 5890 IDNA domain normalization
- RFC 3629 UTF-8 validity for SMTPUTF8 input
- RFC 5234 ABNF-derived parser behavior
- RFC 3696 application-level email length guidance
This crate does not parse full message headers, display names, groups, comments, or SMTP commands; it validates and normalizes address values.
emailaddr is dual-licensed under your choice of the MIT license or the Apache
License (Version 2.0).
See LICENSE-APACHE, LICENSE-MIT for details.
Copyright (c) 2026 Al Liu.