#![cfg_attr(not(feature = "std"), no_std)]
pub fn validate_email(email: &str) -> bool {
match validate_local(email) {
Some(domain_start) => validate_domain(&email[domain_start..]),
None => false,
}
}
fn is_valid_non_escaped(c: char) -> bool {
match c {
'!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '/' | '=' | '?' | '^' | '_'
| '`' | '{' | '|' | '}' | '~' => true,
_ => c.is_alphanumeric(),
}
}
fn is_valid_quoted(c: char) -> bool {
match c {
' ' | '@' | ',' | '[' | ']' | '.' => true,
_ => is_valid_non_escaped(c),
}
}
fn is_valid_quoted_escape(c: char) -> bool {
match c {
'\\' | '\"' => true,
_ => false,
}
}
fn is_valid_escape(c: char) -> bool {
match c {
' ' | '@' | '\\' | '\"' | ',' | '[' | ']' => true,
_ => false,
}
}
#[derive(Eq, PartialEq, Debug)]
enum LocalState {
Start,
Normal,
NormalPeriod,
Escaped,
QuotedNormal,
QuotedEscaped,
QuotedEnd,
End,
}
impl LocalState {
fn transition(self, c: char) -> Option<Self> {
match self {
LocalState::Start => {
if is_valid_non_escaped(c) {
return Some(LocalState::Normal);
}
if c == '\\' {
return Some(LocalState::Escaped);
}
if c == '\"' {
return Some(LocalState::QuotedNormal);
}
None
}
LocalState::Normal => {
if is_valid_non_escaped(c) {
return Some(LocalState::Normal);
}
if c == '.' {
return Some(LocalState::NormalPeriod);
}
if c == '@' {
return Some(LocalState::End);
}
if c == '\\' {
return Some(LocalState::Escaped);
}
None
}
LocalState::NormalPeriod => {
if is_valid_non_escaped(c) {
return Some(LocalState::Normal);
}
if c == '\\' {
return Some(LocalState::Escaped);
}
None
}
LocalState::Escaped => {
if is_valid_escape(c) {
return Some(LocalState::Normal);
}
None
}
LocalState::QuotedNormal => {
if is_valid_quoted(c) {
return Some(LocalState::QuotedNormal);
}
if c == '\"' {
return Some(LocalState::QuotedEnd);
}
if c == '\\' {
return Some(LocalState::QuotedEscaped);
}
None
}
LocalState::QuotedEscaped => {
if is_valid_quoted_escape(c) {
return Some(LocalState::QuotedNormal);
}
None
}
LocalState::QuotedEnd => {
if c == '@' {
return Some(LocalState::End);
}
None
}
LocalState::End => None,
}
}
}
fn validate_local(email: &str) -> Option<usize> {
let mut state = LocalState::Start;
for (i, c) in email.char_indices() {
if state == LocalState::End {
if (i - 1) > 64 {
return None;
}
return Some(i);
}
match state.transition(c) {
None => return None,
Some(new_state) => state = new_state,
}
}
None
}
#[derive(Eq, PartialEq, Debug)]
enum DomainState {
Start,
Normal,
Dash,
StartDotted,
NormalDotted,
DashDotted,
}
impl DomainState {
fn transition(self, c: char) -> Option<Self> {
match self {
DomainState::Start => {
if c.is_ascii_alphanumeric() {
return Some(DomainState::Normal);
}
None
}
DomainState::Normal => {
if c.is_ascii_alphanumeric() {
return Some(DomainState::Normal);
}
if c == '-' {
return Some(DomainState::Dash);
}
if c == '.' {
return Some(DomainState::StartDotted);
}
None
}
DomainState::Dash => {
if c.is_ascii_alphanumeric() {
return Some(DomainState::Normal);
}
if c == '-' {
return Some(DomainState::Dash);
}
None
}
DomainState::StartDotted => {
if c.is_ascii_alphanumeric() {
return Some(DomainState::NormalDotted);
}
None
}
DomainState::NormalDotted => {
if c.is_ascii_alphanumeric() {
return Some(DomainState::NormalDotted);
}
if c == '-' {
return Some(DomainState::DashDotted);
}
if c == '.' {
return Some(DomainState::StartDotted);
}
None
}
DomainState::DashDotted => {
if c.is_ascii_alphanumeric() {
return Some(DomainState::NormalDotted);
}
if c == '-' {
return Some(DomainState::DashDotted);
}
None
}
}
}
}
fn validate_domain(domain: &str) -> bool {
if domain.len() > 255 {
return false;
}
let mut state = DomainState::Start;
for c in domain.chars() {
match state.transition(c) {
None => return false,
Some(new_state) => state = new_state,
}
}
state == DomainState::NormalDotted
}
#[cfg(test)]
mod tests {
use super::*;
fn check(str: &str) {
assert!(validate_email(&str));
}
fn x(str: &str) {
assert!(!validate_email(&str));
}
#[test]
fn normal_email() {
check("normalemail@example.com");
}
#[test]
fn normal_plus() {
check("user+mailbox@example.com");
}
#[test]
fn normal_slash_eq() {
check("customer/department=shipping@example.com");
}
#[test]
fn normal_dollar() {
check("$A12345@example.com");
}
#[test]
fn normal_exclamation_percent() {
check("!def!xyz%abc@example.com");
}
#[test]
fn normal_underscore() {
check("_somename@example.com");
}
#[test]
fn normal_apostrophe_acute_accent() {
check("lol`'lol'@example.com");
}
#[test]
fn normal_crazy_symbols() {
check("!#$%&'*+-/=?^_`{|}~@example.com");
}
#[test]
fn normal_dot() {
check("a.name@example.com");
}
#[test]
fn escaped_at() {
check("Abc\\@def@example.com");
}
#[test]
fn escaped_space() {
check("Fred\\ Bloggs@example.com");
}
#[test]
fn escaped_backslash() {
check("Joe.\\\\Blow@example.com");
}
#[test]
fn all_escaped() {
check("\\\\\\ \\\"\\,\\[\\]@example.com");
}
#[test]
fn quoted_at() {
check("\"Abc@def\"@example.com");
}
#[test]
fn quoted_space() {
check("\"Fred Bloggs\"@example.com");
}
#[test]
fn all_quoted() {
check("\"this is..quoted [te,xt]\"@example.com");
}
#[test]
fn all_escaped_quoted() {
check("\"\\\\\\\"\"@example.com");
}
#[test]
fn almost_too_long_local() {
check("thisisnotaslonglocalportionofanemailaddressthatshouldberejected1@example.com");
}
#[test]
fn subdomains() {
check("example@sub.domain.com");
}
#[test]
fn domain_single_dash() {
check("example@domain-x.com");
}
#[test]
fn domain_multi_dash() {
check("example@domain--x.com");
}
#[test]
fn almost_long_domain() {
check(
"example@thisisalongdomainnamethatshouldberejectedifihaveimplementedthelogiccorrectlyandwillnowrepeattisisalongdomainnamethatshouldberejectedifihaveimplementedthelogiccorrectlywowwhyoneartharethismanycharactersallowedmypooridecannotrenderinonescreenalmostdone1.com",
);
}
#[test]
fn almost_long_email() {
check(
"thisisnotaslonglocalportionofanemailaddressthatshouldberejected1@thisisalongdomainnamethatshouldberejectedifihaveimplementedthelogiccorrectlyandwillnowrepeattisisalongdomainnamethatshouldberejectedifihaveimplementedthelogiccorrectlywowwhyoneartharethismanycharactersallowedmypooridecannotrenderinonescreenalmostdone1.com",
);
}
#[test]
fn start_dot() {
x(".example@example.com");
}
#[test]
fn double_dot() {
x("example..name@example.com");
}
#[test]
fn end_dot() {
x("example.@example.com");
}
#[test]
fn empty_local() {
x("@example.com");
}
#[test]
fn no_domain() {
x("myname");
}
#[test]
fn unescaped_quote() {
x("my\"name@example.com");
}
#[test]
fn things_after_quote() {
x("\"quoted\"abc@example.com");
}
#[test]
fn too_long_local() {
x("thisisasuperlonglocalportionofanemailaddressthatshouldberejected1@example.com");
}
#[test]
fn domain_start_dot() {
x("example@.domain.com");
}
#[test]
fn domain_end_dot() {
x("example@domain.com.");
}
#[test]
fn domain_with_double_dot() {
x("example@domain..com");
}
#[test]
fn domain_start_dash() {
x("example@-domain.com");
}
#[test]
fn domain_end_dash() {
x("example@domain-.com");
}
#[test]
fn tld_end_dash() {
x("example@domain.com-");
}
#[test]
fn domain_without_tld() {
x("example@domain");
}
#[test]
fn domain_with_only_tld() {
x("example@.com");
}
#[test]
fn domain_with_space() {
x("example@example .com");
}
#[test]
fn long_domain() {
x(
"example@thisisalongdomainnamethatshouldberejectedifihaveimplementedthelogiccorrectlyandwillnowrepeatthisisalongdomainnamethatshouldberejectedifihaveimplementedthelogiccorrectlywowwhyoneartharethismanycharactersallowedmypooridecannotrenderinonescreenalmostdone1.com",
);
}
}