Skip to content

Commit 89f10ef

Browse files
committed
add is_valid_email_address.rs
1 parent 28dda98 commit 89f10ef

File tree

3 files changed

+306
-0
lines changed

3 files changed

+306
-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: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
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 != '.' {
43+
return false;
44+
}
45+
}
46+
true
47+
}
48+
49+
fn is_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_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_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+
pub fn is_valid_ip(domain: &str) -> bool {
107+
is_ipv4(domain) || is_ipv6(domain)
108+
}
109+
110+
fn is_valid_local_part(local_part: &str) -> bool {
111+
if local_part.len() > MAX_LOCAL_PART_LENGTH as usize {
112+
return false;
113+
}
114+
115+
if !is_valid_quotes(local_part) {
116+
return false;
117+
}
118+
119+
if !contains_legal_local_characters(local_part) {
120+
return false;
121+
}
122+
123+
if has_bad_dots(local_part) {
124+
return false;
125+
}
126+
127+
true
128+
}
129+
130+
fn is_valid_domain(domain: &str) -> bool {
131+
if domain.starts_with('[') && domain.ends_with(']') {
132+
return is_valid_ip(&domain[1..domain.len() - 1]);
133+
}
134+
135+
if domain.len() > MAX_DOMAIN_LENGTH as usize {
136+
return false;
137+
}
138+
139+
if !contains_legal_domain_characters(domain) {
140+
return false;
141+
}
142+
143+
if has_bad_dots(domain) {
144+
return false;
145+
}
146+
147+
if domain.starts_with('[') || domain.ends_with(']') {
148+
return false;
149+
}
150+
151+
if domain.starts_with('-') || domain.ends_with('-') {
152+
return false;
153+
}
154+
155+
true
156+
}
157+
158+
/// Follows email address rules as listed:
159+
/// https://en.wikipedia.org/wiki/Email_address#Examples
160+
pub fn is_valid_email_address(input_email_address: &str) -> bool {
161+
let email_address = clear_quotes(input_email_address);
162+
163+
let parts: Vec<String> = email_address
164+
.split('@')
165+
.map(|x| x.to_string())
166+
.collect::<Vec<String>>();
167+
168+
// (1) ensure there is only one '@' symbol in the address
169+
if parts.len() != 2 {
170+
return false;
171+
}
172+
173+
let (local_part, domain): (String, String) = (parts[0].clone(), parts[1].clone());
174+
175+
if !is_valid_local_part(&local_part) {
176+
return false;
177+
}
178+
179+
if !is_valid_domain(&domain) {
180+
return false;
181+
}
182+
183+
true
184+
}
185+
186+
#[cfg(test)]
187+
mod tests {
188+
use crate::string::is_valid_email_address::is_valid_email_address;
189+
190+
macro_rules! test_is_valid_email_address {
191+
($($name:ident: $inputs:expr,)*) => {
192+
$(
193+
#[test]
194+
fn $name() {
195+
let (s, expected) = $inputs;
196+
assert_eq!(is_valid_email_address(s), expected);
197+
}
198+
)*
199+
}
200+
}
201+
202+
macro_rules! test_is_ipv4 {
203+
($($name:ident: $inputs:expr,)*) => {
204+
$(
205+
#[test]
206+
fn $name() {
207+
let (s, expected) = $inputs;
208+
assert_eq!(crate::string::is_valid_email_address::is_ipv4(s), expected);
209+
}
210+
)*
211+
}
212+
}
213+
214+
macro_rules! test_is_ipv6 {
215+
($($name:ident: $inputs:expr,)*) => {
216+
$(
217+
#[test]
218+
fn $name() {
219+
let (s, expected) = $inputs;
220+
assert_eq!(crate::string::is_valid_email_address::is_ipv6(s), expected);
221+
}
222+
)*
223+
}
224+
}
225+
226+
test_is_valid_email_address! {
227+
basic: ("[email protected]", true),
228+
basic_2: ("[email protected]", true),
229+
cases: ("[email protected]", true),
230+
one_letter_local: ("[email protected]", true),
231+
long_email_subdomains: ("[email protected]", true),
232+
tags: ("[email protected]", true),
233+
slashes: ("name/[email protected]", true),
234+
no_tld: ("admin@example", true),
235+
tld: ("[email protected]", true),
236+
quotes_with_space: ("\" \"@example.org", true),
237+
quoted_double_dot: ("\"john..doe\"@example.org", true),
238+
host_route: ("[email protected]", true),
239+
quoted_non_letters: (r#""very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com"#, true),
240+
percent_symbol: ("user%[email protected]", true),
241+
local_end_symbol: ("[email protected]", true),
242+
ip_address: ("postmaster@[123.123.123.123]", true),
243+
ip_address_2: ("postmaster@[255.255.255.255]", true),
244+
other_ip: ("postmaster@[IPv6:2001:0db8:85a3:0000:0000:8a2e:0370:7334]", true),
245+
begin_with_underscore: ("_test@[IPv6:2001:0db8:85a3:0000:0000:8a2e:0370:7334]", true),
246+
valid_ipv6: ("example@[IPv6:2001:db8:3333:4444:5555:6666:7777:8888]", true),
247+
small_ipv6: ("test@[IPv6:0:0:0:0:0:0:0:0]", true),
248+
249+
no_closing_bracket: ("postmaster@[", false),
250+
empty_brackets: ("example@[]", false),
251+
another_invalid_example: ("test@[1234]", false),
252+
empty_parts: ("x@[IPv6:1000:1000:1000:1000:1000:1000::1000]", false),
253+
wrong_ip_address: ("postmaster@[1234.123.123.123]", false),
254+
too_long_ipv4: ("wrong.ip@[123.123.123.123.123.123.123.123]", false),
255+
missing_closing: ("example@[1.1.1.1", false),
256+
missing_closing_ipv6: ("test@[IPv6:1000:1000:1000:1000:1000:1000:1000:1000", false),
257+
no_ipv6_at_start: ("test@[1234:2001:0db8:85a3:0000:0000:8a2e:0370:7334]", false),
258+
too_long_ipv6: ("test@[IPv6:1234:2001:0db8:85a3:0000:0000:8a2e:0370:7334", false),
259+
invalid_ipv4: ("example@[123.123.123.123.123]", false),
260+
bad_ip_address: ("postmaster@[hello.255.255.255]", false),
261+
barely_invalid: ("example@[255.255.255.256]", false),
262+
no_at: ("abc.example.com", false),
263+
multiple_ats: ("a@b@[email protected]", false),
264+
bad_local_characters: ("a\"b(c)d,e:f;g<h>i[j\\k][email protected]", false),
265+
bad_local_string: ("just\"not\"[email protected]", false),
266+
bad_backslash: ("this is\"not\\[email protected]", false),
267+
escaped_backslash: ("this\\ still\\\"not\\\\[email protected]", false),
268+
long_local_part: ("1234567890123456789012345678901234567890123456789012345678901234+x@example.com", false),
269+
domain_underscore: ("i.like.underscores@but_they_are_not_allowed_in_this_part", false),
270+
}
271+
272+
test_is_ipv4! {
273+
standard: ("100.100.100.100", true),
274+
two_digit: ("10.10.10.10", true),
275+
one_digit: ("9.9.9.9", true),
276+
extreme_high: ("255.255.255.255", true),
277+
extreme_low: ("0.0.0.0", true),
278+
mixed_length: ("255.10.0.125", true),
279+
280+
invalid: ("255.255.255.256", false),
281+
missing_part: ("123.123.123", false),
282+
empty_part: ("123.123.123.", false),
283+
empty_begin: (".2.21.25", false),
284+
invalid_characters: ("123.123.123.a", false),
285+
too_long: ("123.123.123.1234", false),
286+
no_dots: ("123", false),
287+
null_ipv4: ("", false),
288+
}
289+
290+
test_is_ipv6! {
291+
regular: ("IPv6:1000:1000:1000:1000:1000:1000:1000:1000", true),
292+
mixed_lengths: ("IPv6:2001:db8:3333:4444:5555:6666:7777:8888", true),
293+
short: ("IPv6:0:0:0:0:0:0:0:0", true),
294+
295+
bad_case: ("Ipv6:1000:1000:1000:1000:1000:1000:1000:1000", false),
296+
no_starting_ivp6: ("1234:2001:0db8:85a3:0000:0000:8a2e:0370:7334", false),
297+
invalid_length: ("IPv6:1:1:1:1:1:1:1:12345", false),
298+
empty_value: ("IPv6:100:100:100:100::100:100:100", false),
299+
invalid_character: ("IPv6:100:100:100:100g:100:100:100:100", false),
300+
no_colons: ("IPv6", false),
301+
null: ("", false),
302+
}
303+
}

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)