#![warn(
unknown_lints,
// ---------- Stylistic
absolute_paths_not_starting_with_crate,
elided_lifetimes_in_paths,
explicit_outlives_requirements,
macro_use_extern_crate,
nonstandard_style, /* group */
noop_method_call,
rust_2018_idioms,
single_use_lifetimes,
trivial_casts,
trivial_numeric_casts,
// ---------- Future
future_incompatible, /* group */
rust_2021_compatibility, /* group */
// ---------- Public
missing_debug_implementations,
missing_docs,
unreachable_pub,
// ---------- Unsafe
unsafe_code,
unsafe_op_in_unsafe_fn,
// ---------- Unused
unused, /* group */
)]
#![deny(
// ---------- Public
exported_private_dependencies,
// ---------- Deprecated
anonymous_parameters,
bare_trait_objects,
ellipsis_inclusive_range_patterns,
// ---------- Unsafe
deref_nullptr,
drop_bounds,
dyn_drop,
)]
#[cfg(feature = "serde_support")]
use serde::{Deserialize, Serialize, Serializer};
use std::fmt::{Debug, Display, Formatter};
use std::hash::Hash;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq)]
pub enum Error {
InvalidCharacter,
MissingSeparator,
LocalPartEmpty,
LocalPartTooLong,
DomainEmpty,
DomainTooLong,
SubDomainEmpty,
SubDomainTooLong,
DomainTooFew,
DomainInvalidSeparator,
UnbalancedQuotes,
InvalidComment,
InvalidIPAddress,
UnsupportedDomainLiteral,
UnsupportedDisplayName,
MissingDisplayName,
MissingEndBracket,
}
#[derive(Debug, Copy, Clone)]
pub struct Options {
pub minimum_sub_domains: usize,
pub allow_domain_literal: bool,
pub allow_display_text: bool,
}
#[derive(Debug, Clone)]
pub struct EmailAddress(String);
const LOCAL_PART_MAX_LENGTH: usize = 64;
const DOMAIN_MAX_LENGTH: usize = 254;
const SUB_DOMAIN_MAX_LENGTH: usize = 63;
#[allow(dead_code)]
const CR: char = '\r';
#[allow(dead_code)]
const LF: char = '\n';
const SP: char = ' ';
const HTAB: char = '\t';
const ESC: char = '\\';
const AT: char = '@';
const DOT: char = '.';
const DQUOTE: char = '"';
const LBRACKET: char = '[';
const RBRACKET: char = ']';
#[allow(dead_code)]
const LPAREN: char = '(';
#[allow(dead_code)]
const RPAREN: char = ')';
const DISPLAY_SEP: &str = " <";
const DISPLAY_START: char = '<';
const DISPLAY_END: char = '>';
const MAILTO_URI_PREFIX: &str = "mailto:";
impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Error::InvalidCharacter => write!(f, "Invalid character."),
Error::LocalPartEmpty => write!(f, "Local part is empty."),
Error::LocalPartTooLong => write!(
f,
"Local part is too long. Length limit: {}",
LOCAL_PART_MAX_LENGTH
),
Error::DomainEmpty => write!(f, "Domain is empty."),
Error::DomainTooLong => {
write!(f, "Domain is too long. Length limit: {}", DOMAIN_MAX_LENGTH)
}
Error::SubDomainEmpty => write!(f, "A sub-domain is empty."),
Error::SubDomainTooLong => write!(
f,
"A sub-domain is too long. Length limit: {}",
SUB_DOMAIN_MAX_LENGTH
),
Error::MissingSeparator => write!(f, "Missing separator character '{}'.", AT),
Error::DomainTooFew => write!(f, "Too few parts in the domain"),
Error::DomainInvalidSeparator => {
write!(f, "Invalid placement of the domain separator '{:?}", DOT)
}
Error::InvalidIPAddress => write!(f, "Invalid IP Address specified for domain."),
Error::UnbalancedQuotes => write!(f, "Quotes around the local-part are unbalanced."),
Error::InvalidComment => write!(f, "A comment was badly formed."),
Error::UnsupportedDomainLiteral => write!(f, "Domain literals are not supported."),
Error::UnsupportedDisplayName => write!(f, "Display names are not supported."),
Error::MissingDisplayName => write!(
f,
"Display name was not supplied, but email starts with '<'."
),
Error::MissingEndBracket => write!(f, "Terminating '>' is missing."),
}
}
}
impl std::error::Error for Error {}
impl<T> From<Error> for std::result::Result<T, Error> {
fn from(err: Error) -> Self {
Err(err)
}
}
impl Default for Options {
fn default() -> Self {
Self {
minimum_sub_domains: Default::default(),
allow_domain_literal: true,
allow_display_text: true,
}
}
}
impl Options {
#[inline(always)]
pub const fn with_minimum_sub_domains(self, min: usize) -> Self {
Self {
minimum_sub_domains: min,
..self
}
}
#[inline(always)]
pub const fn with_no_minimum_sub_domains(self) -> Self {
Self {
minimum_sub_domains: 0,
..self
}
}
#[inline(always)]
pub const fn with_required_tld(self) -> Self {
Self {
minimum_sub_domains: 2,
..self
}
}
#[inline(always)]
pub const fn with_domain_literal(self) -> Self {
Self {
allow_domain_literal: true,
..self
}
}
#[inline(always)]
pub const fn without_domain_literal(self) -> Self {
Self {
allow_domain_literal: false,
..self
}
}
#[inline(always)]
pub const fn with_display_text(self) -> Self {
Self {
allow_display_text: true,
..self
}
}
#[inline(always)]
pub const fn without_display_text(self) -> Self {
Self {
allow_display_text: false,
..self
}
}
}
impl Display for EmailAddress {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl PartialEq for EmailAddress {
fn eq(&self, other: &Self) -> bool {
let (left, right) = split_at(&self.0).unwrap();
let (other_left, other_right) = split_at(&other.0).unwrap();
left.eq(other_left) && right.eq_ignore_ascii_case(other_right)
}
}
impl Eq for EmailAddress {}
impl Hash for EmailAddress {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.hash(state);
}
}
impl FromStr for EmailAddress {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
parse_address(s, Default::default())
}
}
impl From<EmailAddress> for String {
fn from(email: EmailAddress) -> Self {
email.0
}
}
impl AsRef<str> for EmailAddress {
fn as_ref(&self) -> &str {
&self.0
}
}
#[cfg(feature = "serde_support")]
impl Serialize for EmailAddress {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.0)
}
}
#[cfg(feature = "serde_support")]
impl<'de> Deserialize<'de> for EmailAddress {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::{Error, Unexpected, Visitor};
struct EmailAddressVisitor;
impl Visitor<'_> for EmailAddressVisitor {
type Value = EmailAddress;
fn expecting(&self, fmt: &mut Formatter<'_>) -> std::fmt::Result {
fmt.write_str("string containing a valid email address")
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: Error,
{
EmailAddress::from_str(s).map_err(|err| {
let exp = format!("{}", err);
Error::invalid_value(Unexpected::Str(s), &exp.as_ref())
})
}
}
deserializer.deserialize_str(EmailAddressVisitor)
}
}
impl EmailAddress {
pub fn new_unchecked<S>(address: S) -> Self
where
S: Into<String>,
{
Self(address.into())
}
pub fn parse_with_options(address: &str, options: Options) -> Result<Self, Error> {
parse_address(address, options)
}
pub fn is_valid(address: &str) -> bool {
Self::from_str(address).is_ok()
}
pub fn is_valid_local_part(part: &str) -> bool {
parse_local_part(part, Default::default()).is_ok()
}
pub fn is_valid_domain(part: &str) -> bool {
parse_domain(part, Default::default()).is_ok()
}
pub fn to_uri(&self) -> String {
let encoded = encode(&self.0);
format!("{}{}", MAILTO_URI_PREFIX, encoded)
}
pub fn to_display(&self, display_name: &str) -> String {
format!("{} <{}>", display_name, self)
}
pub fn local_part(&self) -> &str {
let (local, _, _) = split_parts(&self.0).unwrap();
local
}
pub fn display_part(&self) -> &str {
let (_, _, display) = split_parts(&self.0).unwrap();
display
}
pub fn email(&self) -> String {
let (local, domain, _) = split_parts(&self.0).unwrap();
format!("{}{AT}{}", local, domain)
}
pub fn domain(&self) -> &str {
let (_, domain, _) = split_parts(&self.0).unwrap();
domain
}
pub fn as_str(&self) -> &str {
self.as_ref()
}
}
fn encode(address: &str) -> String {
let mut result = String::new();
for c in address.chars() {
if is_uri_reserved(c) {
result.push_str(&format!("%{:02X}", c as u8))
} else {
result.push(c);
}
}
result
}
fn is_uri_reserved(c: char) -> bool {
c == '!'
|| c == '#'
|| c == '$'
|| c == '%'
|| c == '&'
|| c == '\''
|| c == '('
|| c == ')'
|| c == '*'
|| c == '+'
|| c == ','
|| c == '/'
|| c == ':'
|| c == ';'
|| c == '='
|| c == '?'
|| c == '['
|| c == ']'
}
fn parse_address(address: &str, options: Options) -> Result<EmailAddress, Error> {
let (local_part, domain, display) = split_parts(address)?;
match (
display.is_empty(),
local_part.starts_with(DISPLAY_START),
options.allow_display_text,
) {
(false, _, false) => Err(Error::UnsupportedDisplayName),
(true, true, true) => Err(Error::MissingDisplayName),
(true, true, false) => Err(Error::InvalidCharacter),
_ => {
parse_local_part(local_part, options)?;
parse_domain(domain, options)?;
Ok(EmailAddress(address.to_owned()))
}
}
}
fn split_parts(address: &str) -> Result<(&str, &str, &str), Error> {
let (display, email) = split_display_email(address)?;
let (local_part, domain) = split_at(email)?;
Ok((local_part, domain, display))
}
fn split_display_email(text: &str) -> Result<(&str, &str), Error> {
match text.rsplit_once(DISPLAY_SEP) {
None => Ok(("", text)),
Some((left, right)) => {
let right = right.trim();
if !right.ends_with(DISPLAY_END) {
Err(Error::MissingEndBracket)
} else {
let email = &right[0..right.len() - 1];
let display_name = left.trim();
Ok((display_name, email))
}
}
}
}
fn split_at(address: &str) -> Result<(&str, &str), Error> {
match address.rsplit_once(AT) {
None => Error::MissingSeparator.into(),
Some(left_right) => Ok(left_right),
}
}
fn parse_local_part(part: &str, _: Options) -> Result<(), Error> {
if part.is_empty() {
Error::LocalPartEmpty.into()
} else if part.len() > LOCAL_PART_MAX_LENGTH {
Error::LocalPartTooLong.into()
} else if part.starts_with(DQUOTE) && part.ends_with(DQUOTE) {
if part.len() <= 2 {
Error::LocalPartEmpty.into()
} else {
parse_quoted_local_part(&part[1..part.len() - 1])
}
} else {
parse_unquoted_local_part(part)
}
}
fn parse_quoted_local_part(part: &str) -> Result<(), Error> {
if is_qcontent(part) {
Ok(())
} else {
Error::InvalidCharacter.into()
}
}
fn parse_unquoted_local_part(part: &str) -> Result<(), Error> {
if is_dot_atom_text(part) {
Ok(())
} else {
Error::InvalidCharacter.into()
}
}
fn parse_domain(part: &str, options: Options) -> Result<(), Error> {
if part.is_empty() {
Error::DomainEmpty.into()
} else if part.len() > DOMAIN_MAX_LENGTH {
Error::DomainTooLong.into()
} else if part.starts_with(LBRACKET) && part.ends_with(RBRACKET) {
if options.allow_domain_literal {
parse_literal_domain(&part[1..part.len() - 1])
} else {
Error::UnsupportedDomainLiteral.into()
}
} else {
parse_text_domain(part, options)
}
}
fn parse_text_domain(part: &str, options: Options) -> Result<(), Error> {
let mut sub_domains = 0;
for sub_part in part.split(DOT) {
if sub_part.is_empty() {
return Error::SubDomainEmpty.into();
}
if !sub_part.starts_with(char::is_alphanumeric) {
return Error::InvalidCharacter.into();
}
if !sub_part.ends_with(char::is_alphanumeric) {
return Error::InvalidCharacter.into();
}
if sub_part.len() > SUB_DOMAIN_MAX_LENGTH {
return Error::SubDomainTooLong.into();
}
if !is_atom(sub_part) {
return Error::InvalidCharacter.into();
}
sub_domains += 1;
}
if sub_domains < options.minimum_sub_domains {
Error::DomainTooFew.into()
} else {
Ok(())
}
}
fn parse_literal_domain(part: &str) -> Result<(), Error> {
if part.chars().all(is_dtext_char) {
return Ok(());
}
Error::InvalidCharacter.into()
}
fn is_atext(c: char) -> bool {
c.is_alphanumeric()
|| c == '!'
|| c == '#'
|| c == '$'
|| c == '%'
|| c == '&'
|| c == '\''
|| c == '*'
|| c == '+'
|| c == '-'
|| c == '/'
|| c == '='
|| c == '?'
|| c == '^'
|| c == '_'
|| c == '`'
|| c == '{'
|| c == '|'
|| c == '}'
|| c == '~'
|| is_utf8_non_ascii(c)
}
fn is_utf8_non_ascii(c: char) -> bool {
let bytes = (c as u32).to_be_bytes();
match (bytes[0], bytes[1], bytes[2], bytes[3]) {
(0x00, 0x00, 0xC2..=0xDF, 0x80..=0xBF) => true,
(0x00, 0xE0, 0xA0..=0xBF, 0x80..=0xBF) => true,
(0x00, 0xE1..=0xEC, 0x80..=0xBF, 0x80..=0xBF) => true,
(0x00, 0xED, 0x80..=0x9F, 0x80..=0xBF) => true,
(0x00, 0xEE..=0xEF, 0x80..=0xBF, 0x80..=0xBF) => true,
(0xF0, 0x90..=0xBF, 0x80..=0xBF, 0x80..=0xBF) => true,
(0xF1..=0xF3, 0x80..=0xBF, 0x80..=0xBF, 0x80..=0xBF) => true,
(0xF4, 0x80..=0x8F, 0x80..=0xBF, 0x80..=0xBF) => true,
_ => false,
}
}
fn is_atom(s: &str) -> bool {
!s.is_empty() && s.chars().all(is_atext)
}
fn is_dot_atom_text(s: &str) -> bool {
s.split(DOT).all(is_atom)
}
fn is_vchar(c: char) -> bool {
('\x21'..='\x7E').contains(&c)
}
fn is_wsp(c: char) -> bool {
c == SP || c == HTAB
}
fn is_qtext_char(c: char) -> bool {
c == '\x21'
|| ('\x23'..='\x5B').contains(&c)
|| ('\x5D'..='\x7E').contains(&c)
|| is_utf8_non_ascii(c)
}
fn is_qcontent(s: &str) -> bool {
let mut char_iter = s.chars();
while let Some(c) = &char_iter.next() {
if c == &ESC {
match char_iter.next() {
Some(c2) if is_vchar(c2) => (),
_ => return false,
}
} else if !(is_wsp(*c) || is_qtext_char(*c)) {
return false;
}
}
true
}
fn is_dtext_char(c: char) -> bool {
('\x21'..='\x5A').contains(&c) || ('\x5E'..='\x7E').contains(&c) || is_utf8_non_ascii(c)
}
#[cfg(feature = "serde_support")]
#[cfg(test)]
mod serde_tests {
use super::*;
use claims::{assert_err_eq, assert_ok, assert_ok_eq};
use serde::de::{Error as _, Unexpected};
use serde_assert::{Deserializer, Serializer, Token};
#[test]
fn test_serialize() {
let email = assert_ok!(EmailAddress::from_str("simple@example.com"));
let serializer = Serializer::builder().build();
assert_ok_eq!(
email.serialize(&serializer),
[Token::Str("simple@example.com".to_owned())]
);
}
#[test]
fn test_deserialize() {
let mut deserializer =
Deserializer::builder([Token::Str("simple@example.com".to_owned())]).build();
let email = assert_ok!(EmailAddress::from_str("simple@example.com"));
assert_ok_eq!(EmailAddress::deserialize(&mut deserializer), email);
}
#[test]
fn test_deserialize_invalid_value() {
let mut deserializer =
Deserializer::builder([Token::Str("Abc.example.com".to_owned())]).build();
assert_err_eq!(
EmailAddress::deserialize(&mut deserializer),
serde_assert::de::Error::invalid_value(
Unexpected::Str("Abc.example.com"),
&"Missing separator character '@'."
)
);
}
#[test]
fn test_deserialize_invalid_type() {
let mut deserializer = Deserializer::builder([Token::U64(42)]).build();
assert_err_eq!(
EmailAddress::deserialize(&mut deserializer),
serde_assert::de::Error::invalid_type(
Unexpected::Unsigned(42),
&"string containing a valid email address"
)
);
}
#[test]
fn test_serde_roundtrip() {
let email = assert_ok!(EmailAddress::from_str("simple@example.com"));
let serializer = Serializer::builder().build();
let mut deserializer =
Deserializer::builder(assert_ok!(email.serialize(&serializer))).build();
assert_ok_eq!(EmailAddress::deserialize(&mut deserializer), email);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn is_valid(address: &str, test_case: Option<&str>) {
if let Some(test_case) = test_case {
println!(">> test case: {}", test_case);
println!(" <{}>", address);
} else {
println!(">> <{}>", address);
}
assert!(EmailAddress::is_valid(address));
}
fn valid_with_options(address: &str, options: Options, test_case: Option<&str>) {
if let Some(test_case) = test_case {
println!(">> test case: {}", test_case);
println!(" <{}>", address);
} else {
println!(">> <{}>", address);
}
assert!(EmailAddress::parse_with_options(address, options).is_ok());
}
#[test]
fn test_good_examples_from_wikipedia_01() {
is_valid("simple@example.com", None);
}
#[test]
fn test_good_examples_from_wikipedia_02() {
is_valid("very.common@example.com", None);
}
#[test]
fn test_good_examples_from_wikipedia_03() {
is_valid("disposable.style.email.with+symbol@example.com", None);
}
#[test]
fn test_good_examples_from_wikipedia_04() {
is_valid("other.email-with-hyphen@example.com", None);
}
#[test]
fn test_good_examples_from_wikipedia_05() {
is_valid("fully-qualified-domain@example.com", None);
}
#[test]
fn test_good_examples_from_wikipedia_06() {
is_valid(
"user.name+tag+sorting@example.com",
Some(" may go to user.name@example.com inbox depending on mail server"),
);
}
#[test]
fn test_good_examples_from_wikipedia_07() {
is_valid("x@example.com", Some("one-letter local-part"));
}
#[test]
fn test_good_examples_from_wikipedia_08() {
is_valid("example-indeed@strange-example.com", None);
}
#[test]
fn test_good_examples_from_wikipedia_09() {
is_valid(
"admin@mailserver1",
Some("local domain name with no TLD, although ICANN highly discourages dotless email addresses")
);
}
#[test]
fn test_good_examples_from_wikipedia_10() {
is_valid(
"example@s.example",
Some("see the List of Internet top-level domains"),
);
}
#[test]
fn test_good_examples_from_wikipedia_11() {
is_valid("\" \"@example.org", Some("space between the quotes"));
}
#[test]
fn test_good_examples_from_wikipedia_12() {
is_valid("\"john..doe\"@example.org", Some("quoted double dot"));
}
#[test]
fn test_good_examples_from_wikipedia_13() {
is_valid(
"mailhost!username@example.org",
Some("bangified host route used for uucp mailers"),
);
}
#[test]
fn test_good_examples_from_wikipedia_14() {
is_valid(
"user%example.com@example.org",
Some("% escaped mail route to user@example.com via example.org"),
);
}
#[test]
fn test_good_examples_from_wikipedia_15() {
is_valid("jsmith@[192.168.2.1]", None);
}
#[test]
fn test_good_examples_from_wikipedia_16() {
is_valid("jsmith@[IPv6:2001:db8::1]", None);
}
#[test]
fn test_good_examples_from_wikipedia_17() {
is_valid("user+mailbox/department=shipping@example.com", None);
}
#[test]
fn test_good_examples_from_wikipedia_18() {
is_valid("!#$%&'*+-/=?^_`.{|}~@example.com", None);
}
#[test]
fn test_good_examples_from_wikipedia_19() {
is_valid("\"Abc@def\"@example.com", None);
}
#[test]
fn test_good_examples_from_wikipedia_20() {
is_valid("\"Joe.\\\\Blow\"@example.com", None);
}
#[test]
fn test_good_examples_from_wikipedia_21() {
is_valid("用户@例子.广告", Some("Chinese"));
}
#[test]
fn test_good_examples_from_wikipedia_22() {
is_valid("अजय@डाटा.भारत", Some("Hindi"));
}
#[test]
fn test_good_examples_from_wikipedia_23() {
is_valid("квіточка@пошта.укр", Some("Ukranian"));
}
#[test]
fn test_good_examples_from_wikipedia_24() {
is_valid("θσερ@εχαμπλε.ψομ", Some("Greek"));
}
#[test]
fn test_good_examples_from_wikipedia_25() {
is_valid("Dörte@Sörensen.example.com", Some("German"));
}
#[test]
fn test_good_examples_from_wikipedia_26() {
is_valid("коля@пример.рф", Some("Russian"));
}
#[test]
fn test_good_examples_01() {
valid_with_options(
"foo@example.com",
Options {
minimum_sub_domains: 2,
..Default::default()
},
Some("minimum sub domains"),
);
}
#[test]
fn test_good_examples_02() {
valid_with_options(
"email@[127.0.0.256]",
Options {
allow_domain_literal: true,
..Default::default()
},
Some("minimum sub domains"),
);
}
#[test]
fn test_good_examples_03() {
valid_with_options(
"email@[2001:db8::12345]",
Options {
allow_domain_literal: true,
..Default::default()
},
Some("minimum sub domains"),
);
}
#[test]
fn test_good_examples_04() {
valid_with_options(
"email@[2001:db8:0:0:0:0:1]",
Options {
allow_domain_literal: true,
..Default::default()
},
Some("minimum sub domains"),
);
}
#[test]
fn test_good_examples_05() {
valid_with_options(
"email@[::ffff:127.0.0.256]",
Options {
allow_domain_literal: true,
..Default::default()
},
Some("minimum sub domains"),
);
}
#[test]
fn test_good_examples_06() {
valid_with_options(
"email@[2001:dg8::1]",
Options {
allow_domain_literal: true,
..Default::default()
},
Some("minimum sub domains"),
);
}
#[test]
fn test_good_examples_07() {
valid_with_options(
"email@[2001:dG8:0:0:0:0:0:1]",
Options {
allow_domain_literal: true,
..Default::default()
},
Some("minimum sub domains"),
);
}
#[test]
fn test_good_examples_08() {
valid_with_options(
"email@[::fTzF:127.0.0.1]",
Options {
allow_domain_literal: true,
..Default::default()
},
Some("minimum sub domains"),
);
}
#[test]
fn test_to_strings() {
let email = EmailAddress::from_str("коля@пример.рф").unwrap();
assert_eq!(String::from(email.clone()), String::from("коля@пример.рф"));
assert_eq!(email.to_string(), String::from("коля@пример.рф"));
assert_eq!(email.as_ref(), "коля@пример.рф");
}
#[test]
fn test_to_display() {
let email = EmailAddress::from_str("коля@пример.рф").unwrap();
assert_eq!(
email.to_display("коля"),
String::from("коля <коля@пример.рф>")
);
}
#[test]
fn test_touri() {
let email = EmailAddress::from_str("коля@пример.рф").unwrap();
assert_eq!(email.to_uri(), String::from("mailto:коля@пример.рф"));
}
fn expect(address: &str, error: Error, test_case: Option<&str>) {
if let Some(test_case) = test_case {
println!(">> test case: {}", test_case);
println!(" <{}>, expecting {:?}", address, error);
} else {
println!(">> <{}>, expecting {:?}", address, error);
}
assert_eq!(EmailAddress::from_str(address), error.into());
}
fn expect_with_options(address: &str, options: Options, error: Error, test_case: Option<&str>) {
if let Some(test_case) = test_case {
println!(">> test case: {}", test_case);
println!(" <{}>, expecting {:?}", address, error);
} else {
println!(">> <{}>, expecting {:?}", address, error);
}
assert_eq!(
EmailAddress::parse_with_options(address, options),
error.into()
);
}
#[test]
fn test_bad_examples_from_wikipedia_00() {
expect(
"Abc.example.com",
Error::MissingSeparator,
Some("no @ character"),
);
}
#[test]
fn test_bad_examples_from_wikipedia_01() {
expect(
"A@b@c@example.com",
Error::InvalidCharacter,
Some("only one @ is allowed outside quotation marks"),
);
}
#[test]
fn test_bad_examples_from_wikipedia_02() {
expect(
"a\"b(c)d,e:f;g<h>i[j\\k]l@example.com",
Error::InvalidCharacter,
Some("none of the special characters in this local-part are allowed outside quotation marks")
);
}
#[test]
fn test_bad_examples_from_wikipedia_03() {
expect(
"just\"not\"right@example.com",
Error::InvalidCharacter,
Some(
"quoted strings must be dot separated or the only element making up the local-part",
),
);
}
#[test]
fn test_bad_examples_from_wikipedia_04() {
expect(
"this is\"not\\allowed@example.com",
Error::InvalidCharacter,
Some("spaces, quotes, and backslashes may only exist when within quoted strings and preceded by a backslash")
);
}
#[test]
fn test_bad_examples_from_wikipedia_05() {
expect(
"this\\ still\"not\\allowed@example.com",
Error::InvalidCharacter,
Some("even if escaped (preceded by a backslash), spaces, quotes, and backslashes must still be contained by quotes")
);
}
#[test]
fn test_bad_examples_from_wikipedia_06() {
expect(
"1234567890123456789012345678901234567890123456789012345678901234+x@example.com",
Error::LocalPartTooLong,
Some("local part is longer than 64 characters"),
);
}
#[test]
fn test_bad_example_01() {
expect(
"foo@example.v1234567890123456789012345678901234567890123456789012345678901234v.com",
Error::SubDomainTooLong,
Some("domain part is longer than 64 characters"),
);
}
#[test]
fn test_bad_example_02() {
expect(
"@example.com",
Error::LocalPartEmpty,
Some("local-part is empty"),
);
}
#[test]
fn test_bad_example_03() {
expect(
"\"\"@example.com",
Error::LocalPartEmpty,
Some("local-part is empty"),
);
expect(
"\"@example.com",
Error::LocalPartEmpty,
Some("local-part is empty"),
);
}
#[test]
fn test_bad_example_04() {
expect("simon@", Error::DomainEmpty, Some("domain is empty"));
}
#[test]
fn test_bad_example_05() {
expect(
"example@invalid-.com",
Error::InvalidCharacter,
Some("domain label ends with hyphen"),
);
}
#[test]
fn test_bad_example_06() {
expect(
"example@-invalid.com",
Error::InvalidCharacter,
Some("domain label starts with hyphen"),
);
}
#[test]
fn test_bad_example_07() {
expect(
"example@invalid.com-",
Error::InvalidCharacter,
Some("domain label starts ends hyphen"),
);
}
#[test]
fn test_bad_example_08() {
expect(
"example@inv-.alid-.com",
Error::InvalidCharacter,
Some("subdomain label ends hyphen"),
);
}
#[test]
fn test_bad_example_09() {
expect(
"example@-inv.alid-.com",
Error::InvalidCharacter,
Some("subdomain label starts hyphen"),
);
}
#[test]
fn test_bad_example_10() {
expect(
"example@-.com",
Error::InvalidCharacter,
Some("domain label is hyphen"),
);
}
#[test]
fn test_bad_example_11() {
expect(
"example@-",
Error::InvalidCharacter,
Some("domain label is hyphen"),
);
}
#[test]
fn test_bad_example_12() {
expect(
"example@-abc",
Error::InvalidCharacter,
Some("domain label starts with hyphen"),
);
}
#[test]
fn test_bad_example_13() {
expect(
"example@abc-",
Error::InvalidCharacter,
Some("domain label ends with hyphen"),
);
}
#[test]
fn test_bad_example_14() {
expect(
"example@.com",
Error::SubDomainEmpty,
Some("subdomain label is empty"),
);
}
#[test]
fn test_bad_example_15() {
expect_with_options(
"foo@localhost",
Options::default().with_minimum_sub_domains(2),
Error::DomainTooFew,
Some("too few domains"),
);
}
#[test]
fn test_bad_example_16() {
expect_with_options(
"foo@a.b.c.d.e.f.g.h.i",
Options::default().with_minimum_sub_domains(10),
Error::DomainTooFew,
Some("too few domains"),
);
}
#[test]
fn test_bad_example_17() {
expect_with_options(
"email@[127.0.0.256]",
Options::default().without_domain_literal(),
Error::UnsupportedDomainLiteral,
Some("unsupported domain literal (1)"),
);
}
#[test]
fn test_bad_example_18() {
expect_with_options(
"email@[2001:db8::12345]",
Options::default().without_domain_literal(),
Error::UnsupportedDomainLiteral,
Some("unsupported domain literal (2)"),
);
}
#[test]
fn test_bad_example_19() {
expect_with_options(
"email@[2001:db8:0:0:0:0:1]",
Options::default().without_domain_literal(),
Error::UnsupportedDomainLiteral,
Some("unsupported domain literal (3)"),
);
}
#[test]
fn test_bad_example_20() {
expect_with_options(
"email@[::ffff:127.0.0.256]",
Options::default().without_domain_literal(),
Error::UnsupportedDomainLiteral,
Some("unsupported domain literal (4)"),
);
}
fn is_send<T: Send>() {}
fn is_sync<T: Sync>() {}
#[test]
fn test_error_traits() {
is_send::<Error>();
is_sync::<Error>();
}
#[test]
fn test_parse_trimmed() {
let email = EmailAddress::parse_with_options(
" Simons Email <simon@example.com> ",
Options::default(),
)
.unwrap();
assert_eq!(email.display_part(), "Simons Email");
assert_eq!(email.email(), "simon@example.com");
}
#[test]
fn test_parse_display_name() {
let email = EmailAddress::parse_with_options(
"Simons Email <simon@example.com>",
Options::default(),
)
.unwrap();
assert_eq!(email.display_part(), "Simons Email");
assert_eq!(email.email(), "simon@example.com");
assert_eq!(email.local_part(), "simon");
assert_eq!(email.domain(), "example.com");
}
#[test]
fn test_parse_display_empty_name() {
expect(
"<simon@example.com>",
Error::MissingDisplayName,
Some("missing display name"),
);
}
#[test]
fn test_parse_display_empty_name_2() {
expect_with_options(
"<simon@example.com>",
Options::default().without_display_text(),
Error::InvalidCharacter,
Some("without display text '<' is invalid"),
);
}
#[test]
fn test_parse_display_name_unsupported() {
expect_with_options(
"Simons Email <simon@example.com>",
Options::default().without_display_text(),
Error::UnsupportedDisplayName,
Some("unsupported display name (1)"),
);
}
#[test]
fn test_missing_tld() {
EmailAddress::parse_with_options("simon@localhost", Options::default()).unwrap();
EmailAddress::parse_with_options(
"simon@localhost",
Options::default().with_no_minimum_sub_domains(),
)
.unwrap();
expect_with_options(
"simon@localhost",
Options::default().with_required_tld(),
Error::DomainTooFew,
Some("too few domain segments"),
);
}
#[test]
fn test_eq_name_case_sensitive_local() {
let email = EmailAddress::new_unchecked("simon@example.com");
assert_eq!(email, EmailAddress::new_unchecked("simon@example.com"));
assert_ne!(email, EmailAddress::new_unchecked("Simon@example.com"));
assert_ne!(email, EmailAddress::new_unchecked("simoN@example.com"));
}
#[test]
fn test_eq_name_case_insensitive_domain() {
let email = EmailAddress::new_unchecked("simon@example.com");
assert_eq!(email, EmailAddress::new_unchecked("simon@Example.com"));
assert_eq!(email, EmailAddress::new_unchecked("simon@example.COM"));
}
#[test]
fn test_utf8_non_ascii() {
assert!(!is_utf8_non_ascii('A'));
assert!(!is_utf8_non_ascii('§'));
assert!(!is_utf8_non_ascii('�'));
assert!(!is_utf8_non_ascii('\u{0F40}'));
assert!(is_utf8_non_ascii('\u{C2B0}'));
}
}