agentis_pay_shared/types/
email.rs1use 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}