zino_core/validation/validator/
email.rs

1use self::InvalidEmail::*;
2use super::Validator;
3use crate::LazyLock;
4use regex::Regex;
5use std::{fmt, net::IpAddr, str::FromStr};
6
7/// A validator for the email address.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub struct EmailValidator;
10
11/// An error for the email address validation.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum InvalidEmail {
14    /// The value is empty.
15    Empty,
16    /// The `@` symbol is missing.
17    MissingAt,
18    /// The user info is too long.
19    UserLengthExceeded,
20    /// Invalid user.
21    InvalidUser,
22    /// The domain info is too long.
23    DomainLengthExceeded,
24    /// Invalid domain.
25    InvalidDomain,
26}
27
28impl fmt::Display for InvalidEmail {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        match self {
31            Empty => write!(f, "value is empty"),
32            MissingAt => write!(f, "value is missing `@`"),
33            UserLengthExceeded => {
34                write!(f, "user length exceeded maximum of 64 characters")
35            }
36            InvalidUser => write!(f, "user contains unexpected characters"),
37            DomainLengthExceeded => {
38                write!(f, "domain length exceeded maximum of 255 characters")
39            }
40            InvalidDomain => write!(f, "domain contains unexpected characters"),
41        }
42    }
43}
44
45impl std::error::Error for InvalidEmail {}
46
47impl Validator<str> for EmailValidator {
48    type Error = InvalidEmail;
49
50    fn validate(&self, data: &str) -> Result<(), Self::Error> {
51        if data.is_empty() {
52            return Err(Empty);
53        }
54
55        let (user, domain) = data.split_once('@').ok_or(MissingAt)?;
56        if user.len() > 64 {
57            return Err(UserLengthExceeded);
58        }
59        if !EMAIL_USER_PATTERN.is_match(user) {
60            return Err(InvalidUser);
61        }
62        if domain.len() > 255 {
63            return Err(DomainLengthExceeded);
64        }
65        if !EMAIL_DOMAIN_PATTERN.is_match(domain)
66            && domain
67                .strip_prefix('[')
68                .and_then(|s| s.strip_suffix(']'))
69                .and_then(|s| IpAddr::from_str(s).ok())
70                .is_none()
71        {
72            return Err(InvalidDomain);
73        }
74        Ok(())
75    }
76}
77
78/// Regex for the email user.
79static EMAIL_USER_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
80    Regex::new(r"(?i-u)^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+\z")
81        .expect("fail to create a regex for the email user")
82});
83
84/// Regex for the email domain.
85static EMAIL_DOMAIN_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
86    Regex::new(
87        r"(?i-u)^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$",
88    )
89    .expect("fail to create a regex for the email domain")
90});