#![doc = include_str!("../README.md")]
#![cfg_attr(feature = "nightly", feature(test, min_specialization))]
mod ascii;
mod parser;
mod unicode;
use std::{
fmt::{self, Write},
str::FromStr,
};
pub use parser::ParseError;
use parser::{is_ascii_control_and_not_htab, is_not_atext, is_not_dtext, Parser};
fn quote(value: &str) -> String {
ascii::escape!(value, b'\\', b'"' | b' ' | b'\t')
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub struct AddrSpec {
local_part: String,
domain: String,
#[cfg(feature = "literals")]
literal: bool,
}
impl AddrSpec {
#[inline]
pub fn normalize<Address>(address: Address) -> Result<String, ParseError>
where
Address: AsRef<str>,
{
Ok(address.as_ref().parse::<Self>()?.to_string())
}
pub fn new<LocalPart, Domain>(local_part: LocalPart, domain: Domain) -> Result<Self, ParseError>
where
LocalPart: AsRef<str>,
Domain: AsRef<str>,
{
Self::new_impl(local_part.as_ref(), domain.as_ref(), false)
}
#[cfg(feature = "literals")]
pub fn with_literal<LocalPart, Domain>(
local_part: LocalPart,
domain: Domain,
) -> Result<Self, ParseError>
where
LocalPart: AsRef<str>,
Domain: AsRef<str>,
{
Self::new_impl(local_part.as_ref(), domain.as_ref(), true)
}
fn new_impl(local_part: &str, domain: &str, literal: bool) -> Result<Self, ParseError> {
if let Some(index) = local_part.find(is_ascii_control_and_not_htab) {
return Err(ParseError("invalid character in local part", index));
}
if literal {
if let Some(index) = domain.find(is_not_dtext) {
return Err(ParseError("invalid character in literal domain", index));
}
} else {
let mut parser = Parser::new(domain);
parser.parse_dot_atom("empty label in domain")?;
parser.check_end("invalid character in domain")?;
}
Ok(Self {
local_part: unicode::normalize(local_part),
domain: unicode::normalize(domain),
#[cfg(feature = "literals")]
literal,
})
}
#[inline]
pub unsafe fn new_unchecked<LocalPart, Domain>(local_part: LocalPart, domain: Domain) -> Self
where
LocalPart: Into<String>,
Domain: Into<String>,
{
Self::new_unchecked_impl(local_part.into(), domain.into(), false)
}
#[cfg(feature = "literals")]
#[inline]
pub unsafe fn with_literal_unchecked<LocalPart, Domain>(
local_part: LocalPart,
domain: Domain,
) -> Self
where
LocalPart: Into<String>,
Domain: Into<String>,
{
Self::new_unchecked_impl(local_part.into(), domain.into(), true)
}
#[allow(unused_variables)]
unsafe fn new_unchecked_impl(local_part: String, domain: String, literal: bool) -> Self {
Self {
local_part,
domain,
#[cfg(feature = "literals")]
literal,
}
}
#[inline]
pub fn local_part(&self) -> &str {
&self.local_part
}
#[inline]
pub fn domain(&self) -> &str {
&self.domain
}
#[inline]
pub fn is_quoted(&self) -> bool {
self.local_part()
.split('.')
.any(|s| s.is_empty() || s.contains(is_not_atext))
}
#[inline]
pub fn is_literal(&self) -> bool {
#[cfg(feature = "literals")]
return self.literal;
#[cfg(not(feature = "literals"))]
return false;
}
#[inline]
pub fn into_parts(self) -> (String, String) {
(self.local_part, self.domain)
}
pub fn into_serialized_parts(self) -> (String, String) {
match (self.is_quoted(), self.is_literal()) {
(false, false) => (self.local_part, self.domain),
(true, false) => (
["\"", "e(self.local_part()), "\""].concat(),
self.domain,
),
(false, true) => (self.local_part, ["[", &self.domain, "]"].concat()),
(true, true) => (
["\"", "e(self.local_part()), "\""].concat(),
["[", &self.domain, "]"].concat(),
),
}
}
}
#[cfg(feature = "nightly")]
impl ToString for AddrSpec {
fn to_string(&self) -> String {
match (self.is_quoted(), self.is_literal()) {
(false, false) => [self.local_part(), "@", self.domain()].concat(),
(true, false) => ["\"", "e(self.local_part()), "\"@", self.domain()].concat(),
(false, true) => [self.local_part(), "@[", self.domain(), "]"].concat(),
(true, true) => ["\"", "e(self.local_part()), "\"@[", self.domain(), "]"].concat(),
}
}
}
impl fmt::Display for AddrSpec {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
if !self.is_quoted() {
formatter.write_str(self.local_part())?;
} else {
formatter.write_char('"')?;
for chr in quote(self.local_part()).chars() {
formatter.write_char(chr)?;
}
formatter.write_char('"')?;
}
formatter.write_char('@')?;
if !self.is_literal() {
formatter.write_str(self.domain())?;
} else {
formatter.write_char('[')?;
for chr in self.domain().chars() {
formatter.write_char(chr)?;
}
formatter.write_char(']')?;
}
Ok(())
}
}
impl FromStr for AddrSpec {
type Err = ParseError;
#[inline]
fn from_str(address: &str) -> Result<Self, Self::Err> {
Parser::new(address).parse()
}
}
#[cfg(feature = "serde")]
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[cfg(feature = "serde")]
impl Serialize for AddrSpec {
#[inline]
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_str())
}
}
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for AddrSpec {
#[inline]
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
String::deserialize(deserializer)?
.parse()
.map_err(serde::de::Error::custom)
}
}
#[cfg(feature = "email_address")]
use email_address::EmailAddress;
#[cfg(feature = "email_address")]
impl From<EmailAddress> for AddrSpec {
#[inline]
fn from(val: EmailAddress) -> Self {
AddrSpec::from_str(val.as_str()).unwrap()
}
}
#[cfg(feature = "email_address")]
impl From<AddrSpec> for EmailAddress {
#[inline]
fn from(val: AddrSpec) -> Self {
EmailAddress::new_unchecked(val.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_addr_spec_from_str() {
let addr_spec = AddrSpec::from_str("jdoe@machine.example").unwrap();
assert_eq!(addr_spec.local_part(), "jdoe");
assert_eq!(addr_spec.domain(), "machine.example");
assert_eq!(addr_spec.to_string(), "jdoe@machine.example");
}
#[cfg(feature = "white-spaces")]
#[test]
fn test_addr_spec_from_str_with_white_space_before_local_part() {
let addr_spec = AddrSpec::from_str(" jdoe@machine.example").unwrap();
assert_eq!(addr_spec.local_part(), "jdoe");
assert_eq!(addr_spec.domain(), "machine.example");
assert_eq!(addr_spec.to_string(), "jdoe@machine.example");
}
#[cfg(feature = "white-spaces")]
#[test]
fn test_addr_spec_from_str_with_white_space_before_at() {
let addr_spec = AddrSpec::from_str("jdoe @machine.example").unwrap();
assert_eq!(addr_spec.local_part(), "jdoe");
assert_eq!(addr_spec.domain(), "machine.example");
assert_eq!(addr_spec.to_string(), "jdoe@machine.example");
}
#[cfg(feature = "white-spaces")]
#[test]
fn test_addr_spec_from_str_with_white_space_after_at() {
let addr_spec = AddrSpec::from_str("jdoe@ machine.example").unwrap();
assert_eq!(addr_spec.local_part(), "jdoe");
assert_eq!(addr_spec.domain(), "machine.example");
assert_eq!(addr_spec.to_string(), "jdoe@machine.example");
}
#[cfg(feature = "white-spaces")]
#[test]
fn test_addr_spec_from_str_with_white_space_after_domain() {
let addr_spec = AddrSpec::from_str("jdoe@machine.example ").unwrap();
assert_eq!(addr_spec.local_part(), "jdoe");
assert_eq!(addr_spec.domain(), "machine.example");
assert_eq!(addr_spec.to_string(), "jdoe@machine.example");
}
#[cfg(feature = "comments")]
#[test]
fn test_addr_spec_from_str_with_comments_before_local_part() {
let addr_spec = AddrSpec::from_str("(John Doe)jdoe@machine.example").unwrap();
assert_eq!(addr_spec.local_part(), "jdoe");
assert_eq!(addr_spec.domain(), "machine.example");
assert_eq!(addr_spec.to_string(), "jdoe@machine.example");
}
#[cfg(feature = "comments")]
#[test]
fn test_addr_spec_from_str_with_comments_before_at() {
let addr_spec = AddrSpec::from_str("jdoe(John Doe)@machine.example").unwrap();
assert_eq!(addr_spec.local_part(), "jdoe");
assert_eq!(addr_spec.domain(), "machine.example");
assert_eq!(addr_spec.to_string(), "jdoe@machine.example");
}
#[cfg(feature = "comments")]
#[test]
fn test_addr_spec_from_str_with_comments_after_at() {
let addr_spec = AddrSpec::from_str("jdoe@(John Doe)machine.example").unwrap();
assert_eq!(addr_spec.local_part(), "jdoe");
assert_eq!(addr_spec.domain(), "machine.example");
assert_eq!(addr_spec.to_string(), "jdoe@machine.example");
}
#[cfg(feature = "comments")]
#[test]
fn test_addr_spec_from_str_with_comments_after_domain() {
let addr_spec = AddrSpec::from_str("jdoe@machine.example(John Doe)").unwrap();
assert_eq!(addr_spec.local_part(), "jdoe");
assert_eq!(addr_spec.domain(), "machine.example");
assert_eq!(addr_spec.to_string(), "jdoe@machine.example");
}
#[cfg(feature = "comments")]
#[test]
fn test_addr_spec_from_str_with_nested_comments_before_local_part() {
let addr_spec =
AddrSpec::from_str("(John Doe (The Adventurer))jdoe@machine.example").unwrap();
assert_eq!(addr_spec.local_part(), "jdoe");
assert_eq!(addr_spec.domain(), "machine.example");
assert_eq!(addr_spec.to_string(), "jdoe@machine.example");
}
#[cfg(feature = "comments")]
#[test]
fn test_addr_spec_from_str_with_nested_comments_before_at() {
let addr_spec =
AddrSpec::from_str("jdoe(John Doe (The Adventurer))@machine.example").unwrap();
assert_eq!(addr_spec.local_part(), "jdoe");
assert_eq!(addr_spec.domain(), "machine.example");
assert_eq!(addr_spec.to_string(), "jdoe@machine.example");
}
#[cfg(feature = "comments")]
#[test]
fn test_addr_spec_from_str_with_nested_comments_after_at() {
let addr_spec =
AddrSpec::from_str("jdoe@(John Doe (The Adventurer))machine.example").unwrap();
assert_eq!(addr_spec.local_part(), "jdoe");
assert_eq!(addr_spec.domain(), "machine.example");
assert_eq!(addr_spec.to_string(), "jdoe@machine.example");
}
#[cfg(feature = "comments")]
#[test]
fn test_addr_spec_from_str_with_nested_comments_after_domain() {
let addr_spec =
AddrSpec::from_str("jdoe@machine.example(John Doe (The Adventurer))").unwrap();
assert_eq!(addr_spec.local_part(), "jdoe");
assert_eq!(addr_spec.domain(), "machine.example");
assert_eq!(addr_spec.to_string(), "jdoe@machine.example");
}
#[test]
fn test_addr_spec_from_str_with_empty_labels() {
let addr_spec = AddrSpec::from_str("\"..\"@machine.example").unwrap();
assert_eq!(addr_spec.local_part(), "..");
assert_eq!(addr_spec.domain(), "machine.example");
assert_eq!(addr_spec.to_string(), "\"..\"@machine.example");
}
#[test]
fn test_addr_spec_from_str_with_quote() {
let addr_spec = AddrSpec::from_str("\"jdoe\"@machine.example").unwrap();
assert_eq!(addr_spec.local_part(), "jdoe");
assert_eq!(addr_spec.domain(), "machine.example");
assert_eq!(addr_spec.to_string(), "jdoe@machine.example");
}
#[test]
fn test_addr_spec_from_str_with_escape_and_quote() {
let addr_spec = AddrSpec::from_str("\"jdoe\\\"\"@machine.example").unwrap();
assert_eq!(addr_spec.local_part(), "jdoe\"");
assert_eq!(addr_spec.domain(), "machine.example");
assert_eq!(addr_spec.to_string(), "\"jdoe\\\"\"@machine.example");
}
#[test]
fn test_addr_spec_from_str_with_white_space_escape_and_quote() {
let addr_spec = AddrSpec::from_str("\"jdoe\\ \"@machine.example").unwrap();
assert_eq!(addr_spec.local_part(), "jdoe ");
assert_eq!(addr_spec.domain(), "machine.example");
assert_eq!(addr_spec.to_string(), "\"jdoe\\ \"@machine.example");
}
#[cfg(not(feature = "white-spaces"))]
#[test]
fn test_addr_spec_from_str_with_white_spaces_and_white_space_escape_and_quote() {
assert_eq!(
AddrSpec::from_str("\"jdoe \\ \"@machine.example").unwrap_err(),
ParseError("invalid character in quoted local part", 5)
);
}
#[cfg(feature = "white-spaces")]
#[test]
fn test_addr_spec_from_str_with_white_spaces_and_white_space_escape_and_quote() {
let addr_spec = AddrSpec::from_str("\"jdoe \\ \"@machine.example").unwrap();
assert_eq!(addr_spec.local_part(), "jdoe ");
assert_eq!(addr_spec.domain(), "machine.example");
assert_eq!(addr_spec.to_string(), "\"jdoe\\ \"@machine.example");
}
#[cfg(feature = "literals")]
#[test]
fn test_addr_spec_from_str_with_domain_literal() {
let addr_spec = AddrSpec::from_str("jdoe@[machine.example]").unwrap();
assert_eq!(addr_spec.local_part(), "jdoe");
assert_eq!(addr_spec.domain(), "machine.example");
assert_eq!(addr_spec.to_string(), "jdoe@[machine.example]");
}
#[cfg(feature = "literals")]
#[test]
fn test_addr_spec_from_str_with_escape_and_domain_literal() {
let addr_spec = AddrSpec::from_str("\"jdoe\"@[machine.example]").unwrap();
assert_eq!(addr_spec.local_part(), "jdoe");
assert_eq!(addr_spec.domain(), "machine.example");
assert_eq!(addr_spec.to_string(), "jdoe@[machine.example]");
}
#[test]
fn test_addr_spec_from_str_with_unicode() {
let addr_spec = AddrSpec::from_str("😄😄😄@😄😄😄").unwrap();
assert_eq!(addr_spec.local_part(), "😄😄😄");
assert_eq!(addr_spec.domain(), "😄😄😄");
assert_eq!(addr_spec.to_string(), "😄😄😄@😄😄😄");
}
#[test]
fn test_addr_spec_from_str_with_escape_and_unicode() {
let addr_spec = AddrSpec::from_str("\"😄😄😄\"@😄😄😄").unwrap();
assert_eq!(addr_spec.local_part(), "😄😄😄");
assert_eq!(addr_spec.domain(), "😄😄😄");
assert_eq!(addr_spec.to_string(), "😄😄😄@😄😄😄");
}
#[test]
fn test_addr_spec_from_str_with_escape_and_unicode_and_quote() {
let addr_spec = AddrSpec::from_str("\"😄😄😄\\\"\"@😄😄😄").unwrap();
assert_eq!(addr_spec.local_part(), "😄😄😄\"");
assert_eq!(addr_spec.domain(), "😄😄😄");
assert_eq!(addr_spec.to_string(), "\"😄😄😄\\\"\"@😄😄😄");
}
#[test]
#[cfg(feature = "literals")]
fn test_addr_spec_from_str_with_escape_and_unicode_and_domain_literal() {
let addr_spec = AddrSpec::from_str("\"😄😄😄\"@[😄😄😄]").unwrap();
assert_eq!(addr_spec.local_part(), "😄😄😄");
assert_eq!(addr_spec.domain(), "😄😄😄");
assert_eq!(addr_spec.to_string(), "😄😄😄@[😄😄😄]");
}
}
#[cfg(all(test, feature = "nightly"))]
mod benches {
extern crate test;
use super::*;
mod addr_spec {
use super::*;
#[bench]
fn bench_trivial(b: &mut test::Bencher) {
b.iter(|| {
let address = AddrSpec::from_str("test@example.com").unwrap();
assert_eq!(address.local_part(), "test");
assert_eq!(address.domain(), "example.com");
assert_eq!(address.to_string().as_str(), "test@example.com");
});
}
#[bench]
fn bench_quoted_local_part(b: &mut test::Bencher) {
b.iter(|| {
let address = AddrSpec::from_str("\"test\"@example.com").unwrap();
assert_eq!(address.local_part(), "test");
assert_eq!(address.domain(), "example.com");
assert_eq!(address.to_string().as_str(), "test@example.com");
});
}
#[cfg(feature = "literals")]
#[bench]
fn bench_literal_domain(b: &mut test::Bencher) {
b.iter(|| {
let address = AddrSpec::from_str("test@[example.com]").unwrap();
assert_eq!(address.local_part(), "test");
assert_eq!(address.domain(), "example.com");
assert_eq!(address.to_string().as_str(), "test@[example.com]");
});
}
#[cfg(feature = "literals")]
#[bench]
fn bench_full(b: &mut test::Bencher) {
b.iter(|| {
let address = AddrSpec::from_str("\"test\"@[example.com]").unwrap();
assert_eq!(address.local_part(), "test");
assert_eq!(address.domain(), "example.com");
assert_eq!(address.to_string().as_str(), "test@[example.com]");
});
}
}
#[cfg(feature = "email_address")]
mod email_address {
use super::*;
use ::email_address::EmailAddress;
#[bench]
fn bench_trivial(b: &mut test::Bencher) {
b.iter(|| {
let address = EmailAddress::from_str("test@example.com").unwrap();
assert_eq!(address.local_part(), "test");
assert_eq!(address.domain(), "example.com");
assert_eq!(address.to_string().as_str(), "test@example.com");
});
}
#[bench]
fn bench_quoted_local_part(b: &mut test::Bencher) {
b.iter(|| {
let address = EmailAddress::from_str("\"test\"@example.com").unwrap();
assert_eq!(address.local_part(), "\"test\"");
assert_eq!(address.domain(), "example.com");
assert_eq!(address.to_string().as_str(), "\"test\"@example.com");
});
}
#[cfg(feature = "literals")]
#[bench]
fn bench_literal_domain(b: &mut test::Bencher) {
b.iter(|| {
let address = EmailAddress::from_str("test@[example.com]").unwrap();
assert_eq!(address.local_part(), "test");
assert_eq!(address.domain(), "[example.com]");
assert_eq!(address.to_string().as_str(), "test@[example.com]");
});
}
#[cfg(feature = "literals")]
#[bench]
fn bench_full(b: &mut test::Bencher) {
b.iter(|| {
let address = EmailAddress::from_str("\"test\"@[example.com]").unwrap();
assert_eq!(address.local_part(), "\"test\"");
assert_eq!(address.domain(), "[example.com]");
assert_eq!(address.to_string().as_str(), "\"test\"@[example.com]");
});
}
}
#[bench]
fn bench_addr_spec_regexp(b: &mut test::Bencher) {
use regex::Regex;
let regex = Regex::new(r#"^(?:"(.*)"|([^@]+))@(?:\[(.*)\]|(.*))$"#).unwrap();
b.iter(|| {
{
let captures = regex.captures("test@example.com").unwrap();
assert_eq!(
unsafe {
AddrSpec::new_unchecked(
captures.get(2).unwrap().as_str(),
captures.get(4).unwrap().as_str(),
)
}
.to_string()
.as_str(),
"test@example.com"
);
}
AddrSpec::from_str("test@example.com").unwrap();
{
let captures = regex.captures("\"test\"@example.com").unwrap();
assert_eq!(
unsafe {
AddrSpec::new_unchecked(
captures.get(1).unwrap().as_str(),
captures.get(4).unwrap().as_str(),
)
}
.to_string()
.as_str(),
"test@example.com"
);
}
#[cfg(feature = "literals")]
{
let captures = regex.captures("test@[example.com]").unwrap();
assert_eq!(
unsafe {
AddrSpec::with_literal_unchecked(
captures.get(2).unwrap().as_str(),
captures.get(3).unwrap().as_str(),
)
}
.to_string()
.as_str(),
"test@[example.com]"
);
}
#[cfg(feature = "literals")]
{
let captures = regex.captures("\"test\"@[example.com]").unwrap();
assert_eq!(
unsafe {
AddrSpec::with_literal_unchecked(
captures.get(1).unwrap().as_str(),
captures.get(3).unwrap().as_str(),
)
}
.to_string()
.as_str(),
"test@[example.com]"
);
}
});
}
}