Skip to content

Commit 4c85189

Browse files
committed
add is_valid_email_address.rs
1 parent 28dda98 commit 4c85189

File tree

3 files changed

+256
-0
lines changed

3 files changed

+256
-0
lines changed

DIRECTORY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,7 @@
314314
* [Burrows Wheeler Transform](https://github.com/TheAlgorithms/Rust/blob/master/src/string/burrows_wheeler_transform.rs)
315315
* [Duval Algorithm](https://github.com/TheAlgorithms/Rust/blob/master/src/string/duval_algorithm.rs)
316316
* [Hamming Distance](https://github.com/TheAlgorithms/Rust/blob/master/src/string/hamming_distance.rs)
317+
* [Is Valid Email Address](https://github.com/TheAlgorithms/Rust/blob/master/src/string/is_valid_email_address.rs)
317318
* [Isomorphism](https://github.com/TheAlgorithms/Rust/blob/master/src/string/isomorphism.rs)
318319
* [Jaro Winkler Distance](https://github.com/TheAlgorithms/Rust/blob/master/src/string/jaro_winkler_distance.rs)
319320
* [Knuth Morris Pratt](https://github.com/TheAlgorithms/Rust/blob/master/src/string/knuth_morris_pratt.rs)

src/string/is_valid_email_address.rs

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
const MAX_LOCAL_PART_LENGTH: u32 = 64;
2+
const MAX_DOMAIN_LENGTH: u32 = 255;
3+
const CHARACTERS: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890";
4+
const SYMBOLS: &str = "!#$%&'*+-/=?^_`{|}~";
5+
const HEX_CHARACTERS: &str = "0123456789abcdef";
6+
7+
fn clear_quotes(input: &str) -> String {
8+
let mut quote_count = input.starts_with('"') as u32;
9+
let mut new_local_part: String = String::from(input.chars().next().unwrap());
10+
for i in 1..input.len() {
11+
if input.chars().nth(i).unwrap() == '"' && input.chars().nth(i - 1).unwrap() != '\\' {
12+
quote_count += 1;
13+
14+
if !new_local_part.starts_with('"') && quote_count != 1 {
15+
new_local_part.push('"');
16+
}
17+
}
18+
19+
if quote_count % 2 == 0 {
20+
new_local_part.push(input.chars().nth(i).unwrap());
21+
}
22+
}
23+
24+
new_local_part
25+
}
26+
27+
fn has_bad_dots(part: &str) -> bool {
28+
part.starts_with('.') || part.ends_with('.') || part.contains("..")
29+
}
30+
31+
fn contains_legal_local_characters(local_part: &str) -> bool {
32+
for item in local_part.chars() {
33+
if !CHARACTERS.contains(item) && !SYMBOLS.contains(item) && item != '.' && item != '"' {
34+
return false;
35+
}
36+
}
37+
true
38+
}
39+
40+
fn contains_legal_domain_characters(domain: &str) -> bool {
41+
for item in domain.chars() {
42+
if !CHARACTERS.contains(item) && item != '-' && item != '.' && !"[]:".contains(item) {
43+
return false;
44+
}
45+
}
46+
true
47+
}
48+
49+
fn has_valid_quotes(part: &str) -> bool {
50+
for i in 1..part.len() {
51+
if part.chars().nth(i - 1).unwrap() == '"' && part.chars().nth(i).unwrap() == '"' {
52+
let proceeding_quote = part.chars().nth(i + 1);
53+
if proceeding_quote.is_some() && proceeding_quote.unwrap() != '.' {
54+
return false;
55+
}
56+
}
57+
}
58+
true
59+
}
60+
61+
fn is_valid_ipv4(ip: &str) -> bool {
62+
let parts = ip
63+
.split('.')
64+
.map(|x| x.to_string())
65+
.collect::<Vec<String>>();
66+
if parts.len() != 4 {
67+
return false;
68+
}
69+
70+
for value in parts {
71+
let value = value.parse::<u32>();
72+
if value.is_err() || value.unwrap() > 255 {
73+
return false;
74+
}
75+
}
76+
77+
true
78+
}
79+
80+
fn is_valid_ipv6(ip: &str) -> bool {
81+
let parts = ip
82+
.split(':')
83+
.map(|x| x.to_string())
84+
.collect::<Vec<String>>();
85+
86+
// we check for the length 9 here as the IPv6 counts as one of the parts
87+
if parts.len() != 9 || parts.first().unwrap() != "IPv6" {
88+
return false;
89+
}
90+
91+
for value in &parts[1..parts.len()] {
92+
if value.is_empty() || value.len() > 4 {
93+
return false;
94+
}
95+
96+
for chr in value.chars() {
97+
if !HEX_CHARACTERS.contains(chr.to_ascii_lowercase()) {
98+
return false;
99+
}
100+
}
101+
}
102+
103+
true
104+
}
105+
106+
fn is_valid_ip(domain: &str) -> bool {
107+
if domain.len() < 2 {
108+
return true;
109+
}
110+
111+
if domain.starts_with('[') {
112+
if !domain.ends_with(']') {
113+
return false;
114+
}
115+
116+
let ip = &domain[1..domain.len() - 1];
117+
return is_valid_ipv4(ip) || is_valid_ipv6(ip);
118+
}
119+
120+
true
121+
}
122+
123+
fn is_local_part_valid(local_part: &str) -> bool {
124+
if local_part.len() > MAX_LOCAL_PART_LENGTH as usize {
125+
return false;
126+
}
127+
128+
if !has_valid_quotes(local_part) {
129+
return false;
130+
}
131+
132+
if !contains_legal_local_characters(local_part) {
133+
return false;
134+
}
135+
136+
if has_bad_dots(local_part) {
137+
return false;
138+
}
139+
140+
true
141+
}
142+
143+
fn is_domain_valid(domain: &str) -> bool {
144+
if domain.len() > MAX_DOMAIN_LENGTH as usize {
145+
return false;
146+
}
147+
148+
if !contains_legal_domain_characters(domain) {
149+
return false;
150+
}
151+
152+
if has_bad_dots(domain) {
153+
return false;
154+
}
155+
156+
if !is_valid_ip(domain) {
157+
return false;
158+
}
159+
160+
if domain.starts_with('-') || domain.ends_with('-') {
161+
return false;
162+
}
163+
164+
true
165+
}
166+
167+
/// Follows email address rules as listed:
168+
/// https://en.wikipedia.org/wiki/Email_address#Examples
169+
pub fn is_valid_email_address(input_email_address: &str) -> bool {
170+
let email_address = clear_quotes(input_email_address);
171+
172+
let parts: Vec<String> = email_address
173+
.split('@')
174+
.map(|x| x.to_string())
175+
.collect::<Vec<String>>();
176+
177+
// (1) ensure there is only one '@' symbol in the address
178+
if parts.len() != 2 {
179+
return false;
180+
}
181+
182+
let (local_part, domain): (String, String) = (parts[0].clone(), parts[1].clone());
183+
184+
if !is_local_part_valid(&local_part) {
185+
return false;
186+
}
187+
188+
if !is_domain_valid(&domain) {
189+
return false;
190+
}
191+
192+
true
193+
}
194+
195+
#[cfg(test)]
196+
mod tests {
197+
use crate::string::is_valid_email_address::is_valid_email_address;
198+
199+
macro_rules! test_is_valid_email_address {
200+
($($name:ident: $inputs:expr,)*) => {
201+
$(
202+
#[test]
203+
fn $name() {
204+
let (s, expected) = $inputs;
205+
assert_eq!(is_valid_email_address(s), expected);
206+
}
207+
)*
208+
}
209+
}
210+
211+
test_is_valid_email_address! {
212+
basic: ("[email protected]", true),
213+
basic_2: ("[email protected]", true),
214+
cases: ("[email protected]", true),
215+
one_letter_local: ("[email protected]", true),
216+
long_email_subdomains: ("[email protected]", true),
217+
tags: ("[email protected]", true),
218+
slashes: ("name/[email protected]", true),
219+
no_tld: ("admin@example", true),
220+
tld: ("[email protected]", true),
221+
quotes_with_space: ("\" \"@example.org", true),
222+
quoted_double_dot: ("\"john..doe\"@example.org", true),
223+
host_route: ("[email protected]", true),
224+
quoted_non_letters: (r#""very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com"#, true),
225+
percent_symbol: ("user%[email protected]", true),
226+
local_end_symbol: ("[email protected]", true),
227+
ip_address: ("postmaster@[123.123.123.123]", true),
228+
ip_address_2: ("postmaster@[255.255.255.255]", true),
229+
other_ip: ("postmaster@[IPv6:2001:0db8:85a3:0000:0000:8a2e:0370:7334]", true),
230+
begin_with_underscore: ("_test@[IPv6:2001:0db8:85a3:0000:0000:8a2e:0370:7334]", true),
231+
valid_ipv6: ("example@[IPv6:2001:db8:3333:4444:5555:6666:7777:8888]", true),
232+
small_ipv6: ("test@[IPv6:0:0:0:0:0:0:0:0]", true),
233+
234+
empty_parts: ("x@[IPv6:1000:1000:1000:1000:1000:1000::1000]", false),
235+
wrong_ip_address: ("postmaster@[1234.123.123.123]", false),
236+
too_long_ipv4: ("wrong.ip@[123.123.123.123.123.123.123.123]", false),
237+
missing_closing: ("example@[1.1.1.1", false),
238+
missing_closing_ipv6: ("test@[IPv6:1000:1000:1000:1000:1000:1000:1000:1000", false),
239+
no_ipv6_at_start: ("test@[1234:2001:0db8:85a3:0000:0000:8a2e:0370:7334]", false),
240+
too_long_ipv6: ("test@[IPv6:1234:2001:0db8:85a3:0000:0000:8a2e:0370:7334", false),
241+
invalid_ipv4: ("example@[123.123.123.123.123]", false),
242+
bad_ip_address: ("postmaster@[hello.255.255.255]", false),
243+
barely_invalid: ("example@[255.255.255.256]", false),
244+
no_at: ("abc.example.com", false),
245+
multiple_ats: ("a@b@[email protected]", false),
246+
bad_local_characters: ("a\"b(c)d,e:f;g<h>i[j\\k][email protected]", false),
247+
bad_local_string: ("just\"not\"[email protected]", false),
248+
bad_backslash: ("this is\"not\\[email protected]", false),
249+
escaped_backslash: ("this\\ still\\\"not\\\\[email protected]", false),
250+
long_local_part: ("1234567890123456789012345678901234567890123456789012345678901234+x@example.com", false),
251+
domain_underscore: ("i.like.underscores@but_they_are_not_allowed_in_this_part", false),
252+
}
253+
}

src/string/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ mod boyer_moore_search;
55
mod burrows_wheeler_transform;
66
mod duval_algorithm;
77
mod hamming_distance;
8+
mod is_valid_email_address;
89
mod isomorphism;
910
mod jaro_winkler_distance;
1011
mod knuth_morris_pratt;
@@ -31,6 +32,7 @@ pub use self::burrows_wheeler_transform::{
3132
};
3233
pub use self::duval_algorithm::duval_algorithm;
3334
pub use self::hamming_distance::hamming_distance;
35+
pub use self::is_valid_email_address::is_valid_email_address;
3436
pub use self::isomorphism::is_isomorphic;
3537
pub use self::jaro_winkler_distance::jaro_winkler_distance;
3638
pub use self::knuth_morris_pratt::knuth_morris_pratt;

0 commit comments

Comments
 (0)