|
| 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 | + |
| 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 | +} |
0 commit comments