Skip to content

Commit 284439b

Browse files
committed
add is_valid_email_address.rs
1 parent 28dda98 commit 284439b

File tree

3 files changed

+258
-0
lines changed

3 files changed

+258
-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: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
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 has_bad_brackets(domain: &str) -> bool {
107+
let (starts_with, ends_with) = (domain.starts_with('['), domain.ends_with(']'));
108+
starts_with != ends_with
109+
}
110+
111+
pub fn is_valid_ip(domain: &str) -> bool {
112+
is_valid_ipv4(domain) || is_valid_ipv6(domain)
113+
}
114+
115+
fn is_local_part_valid(local_part: &str) -> bool {
116+
if local_part.len() > MAX_LOCAL_PART_LENGTH as usize {
117+
return false;
118+
}
119+
120+
if !has_valid_quotes(local_part) {
121+
return false;
122+
}
123+
124+
if !contains_legal_local_characters(local_part) {
125+
return false;
126+
}
127+
128+
if has_bad_dots(local_part) {
129+
return false;
130+
}
131+
132+
true
133+
}
134+
135+
fn is_domain_valid(domain: &str) -> bool {
136+
if domain.len() > MAX_DOMAIN_LENGTH as usize {
137+
return false;
138+
}
139+
140+
if !contains_legal_domain_characters(domain) {
141+
return false;
142+
}
143+
144+
if has_bad_dots(domain) {
145+
return false;
146+
}
147+
148+
if has_bad_brackets(domain) {
149+
return false;
150+
}
151+
152+
if domain.starts_with('[')
153+
&& domain.ends_with(']')
154+
&& !is_valid_ip(&domain[1..domain.len() - 1])
155+
{
156+
return false;
157+
}
158+
159+
if domain.starts_with('-') || domain.ends_with('-') {
160+
return false;
161+
}
162+
163+
true
164+
}
165+
166+
/// Follows email address rules as listed:
167+
/// https://en.wikipedia.org/wiki/Email_address#Examples
168+
pub fn is_valid_email_address(input_email_address: &str) -> bool {
169+
let email_address = clear_quotes(input_email_address);
170+
171+
let parts: Vec<String> = email_address
172+
.split('@')
173+
.map(|x| x.to_string())
174+
.collect::<Vec<String>>();
175+
176+
// (1) ensure there is only one '@' symbol in the address
177+
if parts.len() != 2 {
178+
return false;
179+
}
180+
181+
let (local_part, domain): (String, String) = (parts[0].clone(), parts[1].clone());
182+
183+
if !is_local_part_valid(&local_part) {
184+
return false;
185+
}
186+
187+
if !is_domain_valid(&domain) {
188+
return false;
189+
}
190+
191+
true
192+
}
193+
194+
#[cfg(test)]
195+
mod tests {
196+
use crate::string::is_valid_email_address::is_valid_email_address;
197+
198+
macro_rules! test_is_valid_email_address {
199+
($($name:ident: $inputs:expr,)*) => {
200+
$(
201+
#[test]
202+
fn $name() {
203+
let (s, expected) = $inputs;
204+
assert_eq!(is_valid_email_address(s), expected);
205+
}
206+
)*
207+
}
208+
}
209+
210+
test_is_valid_email_address! {
211+
basic: ("[email protected]", true),
212+
basic_2: ("[email protected]", true),
213+
cases: ("[email protected]", true),
214+
one_letter_local: ("[email protected]", true),
215+
long_email_subdomains: ("[email protected]", true),
216+
tags: ("[email protected]", true),
217+
slashes: ("name/[email protected]", true),
218+
no_tld: ("admin@example", true),
219+
tld: ("[email protected]", true),
220+
quotes_with_space: ("\" \"@example.org", true),
221+
quoted_double_dot: ("\"john..doe\"@example.org", true),
222+
host_route: ("[email protected]", true),
223+
quoted_non_letters: (r#""very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com"#, true),
224+
percent_symbol: ("user%[email protected]", true),
225+
local_end_symbol: ("[email protected]", true),
226+
ip_address: ("postmaster@[123.123.123.123]", true),
227+
ip_address_2: ("postmaster@[255.255.255.255]", true),
228+
other_ip: ("postmaster@[IPv6:2001:0db8:85a3:0000:0000:8a2e:0370:7334]", true),
229+
begin_with_underscore: ("_test@[IPv6:2001:0db8:85a3:0000:0000:8a2e:0370:7334]", true),
230+
valid_ipv6: ("example@[IPv6:2001:db8:3333:4444:5555:6666:7777:8888]", true),
231+
small_ipv6: ("test@[IPv6:0:0:0:0:0:0:0:0]", true),
232+
233+
no_closing_bracket: ("postmaster@[", false),
234+
empty_brackets: ("example@[]", false),
235+
another_invalid_example: ("test@[1234]", false),
236+
empty_parts: ("x@[IPv6:1000:1000:1000:1000:1000:1000::1000]", false),
237+
wrong_ip_address: ("postmaster@[1234.123.123.123]", false),
238+
too_long_ipv4: ("wrong.ip@[123.123.123.123.123.123.123.123]", false),
239+
missing_closing: ("example@[1.1.1.1", false),
240+
missing_closing_ipv6: ("test@[IPv6:1000:1000:1000:1000:1000:1000:1000:1000", false),
241+
no_ipv6_at_start: ("test@[1234:2001:0db8:85a3:0000:0000:8a2e:0370:7334]", false),
242+
too_long_ipv6: ("test@[IPv6:1234:2001:0db8:85a3:0000:0000:8a2e:0370:7334", false),
243+
invalid_ipv4: ("example@[123.123.123.123.123]", false),
244+
bad_ip_address: ("postmaster@[hello.255.255.255]", false),
245+
barely_invalid: ("example@[255.255.255.256]", false),
246+
no_at: ("abc.example.com", false),
247+
multiple_ats: ("a@b@[email protected]", false),
248+
bad_local_characters: ("a\"b(c)d,e:f;g<h>i[j\\k][email protected]", false),
249+
bad_local_string: ("just\"not\"[email protected]", false),
250+
bad_backslash: ("this is\"not\\[email protected]", false),
251+
escaped_backslash: ("this\\ still\\\"not\\\\[email protected]", false),
252+
long_local_part: ("1234567890123456789012345678901234567890123456789012345678901234+x@example.com", false),
253+
domain_underscore: ("i.like.underscores@but_they_are_not_allowed_in_this_part", false),
254+
}
255+
}

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)