Skip to main content

agentis_pay_shared/types/
email.rs

1use serde::Deserialize;
2
3#[derive(Debug, Clone, Deserialize)]
4pub struct Email(String);
5
6impl std::ops::Deref for Email {
7    type Target = String;
8
9    fn deref(&self) -> &Self::Target {
10        &self.0
11    }
12}
13
14impl Email {
15    #[must_use]
16    pub fn format_masked(&self) -> String {
17        let (username, domain) = self.0.split_once('@').expect("checked on parse");
18
19        let first = username.chars().next().unwrap_or_default();
20        let last = username.chars().last().unwrap_or_default();
21
22        format!("{first}**{last}@{domain}")
23    }
24
25    #[must_use]
26    pub fn domain(&self) -> &str {
27        let (_username, domain) = self.0.split_once('@').expect("checked on parse");
28        domain
29    }
30}
31
32impl std::str::FromStr for Email {
33    type Err = &'static str;
34
35    fn from_str(s: &str) -> Result<Self, Self::Err> {
36        REGEX
37            .is_match(&s.to_lowercase())
38            .then(|| Email(s.to_owned()))
39            .ok_or("Email couldn't be parsed")
40    }
41}
42
43static REGEX: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
44    regex::Regex::new(
45        r"(?x)
46            ^ # from the start
47            (
48             [a-z0-9_+]      # exactly one letter, number, underscore or plus sign
49             (               # followed by a combination of
50              [a-z0-9_+.\-]* #  any number of letter, number, underscore, plus sign, dot or dash
51              [a-z0-9_+]     #  followed by one letter, number, underscore or plus sign
52             )?              # which can be omitted or happen exactly once.
53            )
54            @                # then a single @ sign
55            (
56             [a-z0-9]+       # one or more letter or number
57             (               # followed by a combination of
58              [\-\.]{1}      #  one dash or dot
59              [a-z0-9]+      #  followed one or more letter or number
60             )*              # which can happen any number of times.
61             \.              # then we need a single dot
62             [a-z]{2,8}      # to be followed by two to eight letters.
63            )
64            $ # the end
65        ",
66    )
67    .expect("Email regex couldn't be compiled")
68});
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn email_parsing() {
76        assert!("sunsi.lucas@gmail.com".parse::<Email>().is_ok());
77        assert!("luizfilipester@gmail.com".parse::<Email>().is_ok());
78        assert!("sunsi-filipester@pm.me".parse::<Email>().is_ok());
79        assert!("lucas.filipester-sunsi@pm.me".parse::<Email>().is_ok());
80        assert!("john@ipe.ventures".parse::<Email>().is_ok());
81
82        assert!("foo@bar.com".parse::<Email>().is_ok());
83        assert!("foo.bar42@c.com".parse::<Email>().is_ok());
84        assert!("42@c.com".parse::<Email>().is_ok());
85        assert!("f@42.co".parse::<Email>().is_ok());
86        assert!("foo@4-2.team".parse::<Email>().is_ok());
87        assert!("foo_bar@bar.com".parse::<Email>().is_ok());
88        assert!("_bar@bar.com".parse::<Email>().is_ok());
89        assert!("foo_@bar.com".parse::<Email>().is_ok());
90        assert!("foo+bar@bar.com".parse::<Email>().is_ok());
91        assert!("+bar@bar.com".parse::<Email>().is_ok());
92        assert!("foo+@bar.com".parse::<Email>().is_ok());
93        assert!("foo.lastname@bar.com".parse::<Email>().is_ok());
94        assert!("matheus-sampaiof@hotmail.com".parse::<Email>().is_ok());
95
96        assert!("email@example.com".parse::<Email>().is_ok());
97        assert!("firstname.lastname@example.com".parse::<Email>().is_ok());
98        assert!("email@subdomain.example.com".parse::<Email>().is_ok());
99        assert!("firstname+lastname@example.com".parse::<Email>().is_ok());
100        assert!("1234567890@example.com".parse::<Email>().is_ok());
101        assert!("email@example-one.com".parse::<Email>().is_ok());
102        assert!("_______@example.com".parse::<Email>().is_ok());
103        assert!("email@example.name".parse::<Email>().is_ok());
104        assert!("email@example.museum".parse::<Email>().is_ok());
105        assert!("email@example.co.jp".parse::<Email>().is_ok());
106        assert!("firstname-lastname@example.com".parse::<Email>().is_ok());
107
108        assert!("foo at bar.com".parse::<Email>().is_err());
109        assert!(".x@c.com".parse::<Email>().is_err());
110        assert!("x.@c.com".parse::<Email>().is_err());
111
112        assert!("email@123.123.123.123".parse::<Email>().is_err());
113        assert!("\"email\"@example.com".parse::<Email>().is_err());
114        assert!("-foo@bar.com".parse::<Email>().is_err());
115        assert!("foo-@bar.com".parse::<Email>().is_err());
116
117        assert!("34790194043".parse::<Email>().is_err());
118        assert!("not an email".parse::<Email>().is_err());
119        assert!("luiz @ bipa.app".parse::<Email>().is_err());
120        assert!("luizatbipa.app".parse::<Email>().is_err());
121        assert!("luiz@bipa..app".parse::<Email>().is_err());
122    }
123
124    #[test]
125    fn format_masked() -> Result<(), &'static str> {
126        let email = "john.doe@example.com".parse::<Email>()?;
127        assert_eq!(email.format_masked(), "j**e@example.com");
128
129        let email = "a@b.com".parse::<Email>()?;
130        assert_eq!(email.format_masked(), "a**a@b.com");
131
132        let email = "very.long.email.address@example.co.uk".parse::<Email>()?;
133        assert_eq!(email.format_masked(), "v**s@example.co.uk");
134
135        let email = "short@mail.com".parse::<Email>()?;
136        assert_eq!(email.format_masked(), "s**t@mail.com");
137
138        let email = "x@domain.com".parse::<Email>()?;
139        assert_eq!(email.format_masked(), "x**x@domain.com");
140
141        let email = "first.middle.last@long-domain.com".parse::<Email>()?;
142        assert_eq!(email.format_masked(), "f**t@long-domain.com");
143
144        let email = "user@example-domain.com".parse::<Email>()?;
145        assert_eq!(email.format_masked(), "u**r@example-domain.com");
146
147        let email = "test@multi-part-domain.co.uk".parse::<Email>()?;
148        assert_eq!(email.format_masked(), "t**t@multi-part-domain.co.uk");
149        Ok(())
150    }
151}