1#![cfg_attr(not(feature = "std"), no_std)]
2
3pub fn validate_email(email: &str) -> bool {
5 match validate_local(email) {
6 Some(domain_start) => validate_domain(&email[domain_start..]),
7 None => false,
8 }
9}
10
11fn is_valid_non_escaped(c: char) -> bool {
15 match c {
16 '!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '/' | '=' | '?' | '^' | '_'
17 | '`' | '{' | '|' | '}' | '~' => true,
18 _ => c.is_alphanumeric(),
19 }
20}
21
22fn is_valid_quoted(c: char) -> bool {
26 match c {
27 ' ' | '@' | ',' | '[' | ']' | '.' => true,
28 _ => is_valid_non_escaped(c),
29 }
30}
31
32fn is_valid_quoted_escape(c: char) -> bool {
34 match c {
35 '\\' | '\"' => true,
36 _ => false,
37 }
38}
39
40fn is_valid_escape(c: char) -> bool {
42 match c {
43 ' ' | '@' | '\\' | '\"' | ',' | '[' | ']' => true,
44 _ => false,
45 }
46}
47
48#[derive(Eq, PartialEq, Debug)]
50enum LocalState {
51 Start,
53
54 Normal,
56
57 NormalPeriod,
59
60 Escaped,
62
63 QuotedNormal,
65
66 QuotedEscaped,
68
69 QuotedEnd,
71
72 End,
74}
75
76impl LocalState {
77 fn transition(self, c: char) -> Option<Self> {
79 match self {
80 LocalState::Start => {
81 if is_valid_non_escaped(c) {
84 return Some(LocalState::Normal);
85 }
86
87 if c == '\\' {
89 return Some(LocalState::Escaped);
90 }
91
92 if c == '\"' {
94 return Some(LocalState::QuotedNormal);
95 }
96
97 None
99 }
100 LocalState::Normal => {
101 if is_valid_non_escaped(c) {
103 return Some(LocalState::Normal);
104 }
105
106 if c == '.' {
108 return Some(LocalState::NormalPeriod);
109 }
110
111 if c == '@' {
113 return Some(LocalState::End);
114 }
115
116 if c == '\\' {
118 return Some(LocalState::Escaped);
119 }
120
121 None
123 }
124 LocalState::NormalPeriod => {
125 if is_valid_non_escaped(c) {
127 return Some(LocalState::Normal);
128 }
129
130 if c == '\\' {
132 return Some(LocalState::Escaped);
133 }
134
135 None
139 }
140 LocalState::Escaped => {
141 if is_valid_escape(c) {
143 return Some(LocalState::Normal);
144 }
145
146 None
148 }
149 LocalState::QuotedNormal => {
150 if is_valid_quoted(c) {
152 return Some(LocalState::QuotedNormal);
153 }
154
155 if c == '\"' {
157 return Some(LocalState::QuotedEnd);
158 }
159
160 if c == '\\' {
162 return Some(LocalState::QuotedEscaped);
163 }
164
165 None
167 }
168 LocalState::QuotedEscaped => {
169 if is_valid_quoted_escape(c) {
171 return Some(LocalState::QuotedNormal);
172 }
173
174 None
176 }
177 LocalState::QuotedEnd => {
178 if c == '@' {
180 return Some(LocalState::End);
181 }
182
183 None
185 }
186
187 LocalState::End => None,
189 }
190 }
191}
192
193fn validate_local(email: &str) -> Option<usize> {
195 let mut state = LocalState::Start;
196 for (i, c) in email.char_indices() {
197 if state == LocalState::End {
199 if (i - 1) > 64 {
202 return None;
203 }
204 return Some(i);
205 }
206
207 match state.transition(c) {
209 None => return None,
210 Some(new_state) => state = new_state,
211 }
212 }
213
214 None
216}
217
218#[derive(Eq, PartialEq, Debug)]
220enum DomainState {
221 Start,
223
224 Normal,
226
227 Dash,
229
230 StartDotted,
232
233 NormalDotted,
237
238 DashDotted,
240}
241
242impl DomainState {
243 fn transition(self, c: char) -> Option<Self> {
245 match self {
246 DomainState::Start => {
247 if c.is_ascii_alphanumeric() {
249 return Some(DomainState::Normal);
250 }
251
252 None
254 }
255 DomainState::Normal => {
256 if c.is_ascii_alphanumeric() {
258 return Some(DomainState::Normal);
259 }
260
261 if c == '-' {
263 return Some(DomainState::Dash);
264 }
265
266 if c == '.' {
268 return Some(DomainState::StartDotted);
269 }
270
271 None
273 }
274 DomainState::Dash => {
275 if c.is_ascii_alphanumeric() {
277 return Some(DomainState::Normal);
278 }
279
280 if c == '-' {
282 return Some(DomainState::Dash);
283 }
284
285 None
287 }
288 DomainState::StartDotted => {
289 if c.is_ascii_alphanumeric() {
291 return Some(DomainState::NormalDotted);
292 }
293
294 None
296 }
297 DomainState::NormalDotted => {
298 if c.is_ascii_alphanumeric() {
300 return Some(DomainState::NormalDotted);
301 }
302
303 if c == '-' {
305 return Some(DomainState::DashDotted);
306 }
307
308 if c == '.' {
310 return Some(DomainState::StartDotted);
311 }
312
313 None
315 }
316 DomainState::DashDotted => {
317 if c.is_ascii_alphanumeric() {
319 return Some(DomainState::NormalDotted);
320 }
321
322 if c == '-' {
324 return Some(DomainState::DashDotted);
325 }
326
327 None
329 }
330 }
331 }
332}
333
334fn validate_domain(domain: &str) -> bool {
336 if domain.len() > 255 {
338 return false;
339 }
340
341 let mut state = DomainState::Start;
342 for c in domain.chars() {
343 match state.transition(c) {
345 None => return false,
346 Some(new_state) => state = new_state,
347 }
348 }
349
350 state == DomainState::NormalDotted
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357
358 fn check(str: &str) {
360 assert!(validate_email(&str));
361 }
362
363 fn x(str: &str) {
365 assert!(!validate_email(&str));
366 }
367
368 #[test]
369 fn normal_email() {
370 check("normalemail@example.com");
371 }
372
373 #[test]
374 fn normal_plus() {
375 check("user+mailbox@example.com");
376 }
377
378 #[test]
379 fn normal_slash_eq() {
380 check("customer/department=shipping@example.com");
381 }
382
383 #[test]
384 fn normal_dollar() {
385 check("$A12345@example.com");
386 }
387
388 #[test]
389 fn normal_exclamation_percent() {
390 check("!def!xyz%abc@example.com");
391 }
392
393 #[test]
394 fn normal_underscore() {
395 check("_somename@example.com");
396 }
397
398 #[test]
399 fn normal_apostrophe_acute_accent() {
400 check("lol`'lol'@example.com");
401 }
402
403 #[test]
404 fn normal_crazy_symbols() {
405 check("!#$%&'*+-/=?^_`{|}~@example.com");
406 }
407
408 #[test]
409 fn normal_dot() {
410 check("a.name@example.com");
411 }
412
413 #[test]
414 fn escaped_at() {
415 check("Abc\\@def@example.com");
416 }
417
418 #[test]
419 fn escaped_space() {
420 check("Fred\\ Bloggs@example.com");
421 }
422
423 #[test]
424 fn escaped_backslash() {
425 check("Joe.\\\\Blow@example.com");
426 }
427
428 #[test]
429 fn all_escaped() {
430 check("\\\\\\ \\\"\\,\\[\\]@example.com");
431 }
432
433 #[test]
434 fn quoted_at() {
435 check("\"Abc@def\"@example.com");
436 }
437
438 #[test]
439 fn quoted_space() {
440 check("\"Fred Bloggs\"@example.com");
441 }
442
443 #[test]
444 fn all_quoted() {
445 check("\"this is..quoted [te,xt]\"@example.com");
446 }
447
448 #[test]
449 fn all_escaped_quoted() {
450 check("\"\\\\\\\"\"@example.com");
451 }
452
453 #[test]
454 fn almost_too_long_local() {
455 check("thisisnotaslonglocalportionofanemailaddressthatshouldberejected1@example.com");
456 }
457
458 #[test]
459 fn subdomains() {
460 check("example@sub.domain.com");
461 }
462
463 #[test]
464 fn domain_single_dash() {
465 check("example@domain-x.com");
466 }
467
468 #[test]
469 fn domain_multi_dash() {
470 check("example@domain--x.com");
471 }
472
473 #[test]
474 fn almost_long_domain() {
475 check(
476 "example@thisisalongdomainnamethatshouldberejectedifihaveimplementedthelogiccorrectlyandwillnowrepeattisisalongdomainnamethatshouldberejectedifihaveimplementedthelogiccorrectlywowwhyoneartharethismanycharactersallowedmypooridecannotrenderinonescreenalmostdone1.com",
477 );
478 }
479
480 #[test]
481 fn almost_long_email() {
482 check(
483 "thisisnotaslonglocalportionofanemailaddressthatshouldberejected1@thisisalongdomainnamethatshouldberejectedifihaveimplementedthelogiccorrectlyandwillnowrepeattisisalongdomainnamethatshouldberejectedifihaveimplementedthelogiccorrectlywowwhyoneartharethismanycharactersallowedmypooridecannotrenderinonescreenalmostdone1.com",
484 );
485 }
486
487 #[test]
490 fn start_dot() {
491 x(".example@example.com");
492 }
493
494 #[test]
495 fn double_dot() {
496 x("example..name@example.com");
497 }
498
499 #[test]
500 fn end_dot() {
501 x("example.@example.com");
502 }
503
504 #[test]
505 fn empty_local() {
506 x("@example.com");
507 }
508
509 #[test]
510 fn no_domain() {
511 x("myname");
512 }
513
514 #[test]
515 fn unescaped_quote() {
516 x("my\"name@example.com");
517 }
518
519 #[test]
520 fn things_after_quote() {
521 x("\"quoted\"abc@example.com");
522 }
523
524 #[test]
525 fn too_long_local() {
526 x("thisisasuperlonglocalportionofanemailaddressthatshouldberejected1@example.com");
527 }
528
529 #[test]
530 fn domain_start_dot() {
531 x("example@.domain.com");
532 }
533
534 #[test]
535 fn domain_end_dot() {
536 x("example@domain.com.");
537 }
538
539 #[test]
540 fn domain_with_double_dot() {
541 x("example@domain..com");
542 }
543
544 #[test]
545 fn domain_start_dash() {
546 x("example@-domain.com");
547 }
548
549 #[test]
550 fn domain_end_dash() {
551 x("example@domain-.com");
552 }
553
554 #[test]
555 fn tld_end_dash() {
556 x("example@domain.com-");
557 }
558
559 #[test]
560 fn domain_without_tld() {
561 x("example@domain");
562 }
563
564 #[test]
565 fn domain_with_only_tld() {
566 x("example@.com");
567 }
568
569 #[test]
570 fn domain_with_space() {
571 x("example@example .com");
572 }
573
574 #[test]
575 fn long_domain() {
576 x(
577 "example@thisisalongdomainnamethatshouldberejectedifihaveimplementedthelogiccorrectlyandwillnowrepeatthisisalongdomainnamethatshouldberejectedifihaveimplementedthelogiccorrectlywowwhyoneartharethismanycharactersallowedmypooridecannotrenderinonescreenalmostdone1.com",
578 );
579 }
580}