Skip to content

Commit 88d09e3

Browse files
committed
add is_valid_email_address.rs
1 parent 28dda98 commit 88d09e3

File tree

3 files changed

+311
-0
lines changed

3 files changed

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

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)