1use core::fmt;
44use core::str::FromStr;
45
46#[cfg(feature = "serde")]
47use serde::{Deserialize, Serialize};
48
49#[cfg(feature = "zeroize")]
50use zeroize::Zeroize;
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
55#[non_exhaustive]
56pub enum EmailError {
57 Empty,
61 TooLong(usize),
66 MissingAtSymbol,
70 MultipleAtSymbols,
74 EmptyLocalPart,
78 EmptyDomain,
82 LocalPartTooLong(usize),
87 InvalidLocalPartChar(char),
92 LocalPartStartsWithDot,
96 LocalPartEndsWithDot,
100 LocalPartConsecutiveDots,
104 InvalidDomain,
109}
110
111impl fmt::Display for EmailError {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 match self {
114 Self::Empty => write!(f, "email cannot be empty"),
115 Self::TooLong(len) => write!(
116 f,
117 "email exceeds maximum length of 254 characters (got {len})"
118 ),
119 Self::MissingAtSymbol => write!(f, "email must contain an @ symbol"),
120 Self::MultipleAtSymbols => write!(f, "email must contain exactly one @ symbol"),
121 Self::EmptyLocalPart => write!(f, "email local part cannot be empty"),
122 Self::EmptyDomain => write!(f, "email domain part cannot be empty"),
123 Self::LocalPartTooLong(len) => write!(
124 f,
125 "email local part exceeds maximum length of 64 characters (got {len})"
126 ),
127 Self::InvalidLocalPartChar(c) => {
128 write!(f, "email local part contains invalid character '{c}'")
129 }
130 Self::LocalPartStartsWithDot => {
131 write!(f, "email local part cannot start with a dot")
132 }
133 Self::LocalPartEndsWithDot => write!(f, "email local part cannot end with a dot"),
134 Self::LocalPartConsecutiveDots => {
135 write!(f, "email local part cannot contain consecutive dots")
136 }
137 Self::InvalidDomain => write!(f, "email domain part is invalid"),
138 }
139 }
140}
141
142#[cfg(feature = "std")]
143impl std::error::Error for EmailError {}
144
145#[repr(transparent)]
181#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
182#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
183#[cfg_attr(feature = "zeroize", derive(Zeroize))]
184pub struct Email(heapless::String<254>);
185
186#[cfg(feature = "arbitrary")]
187impl<'a> arbitrary::Arbitrary<'a> for Email {
188 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
189 const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz";
190 const DIGITS: &[u8] = b"0123456789";
191 const SPECIAL: &[u8] = b"!#$%&'*+-/=?^_`{|}~";
192
193 let local_len = 1 + (u8::arbitrary(u)? % 20).min(19);
195 let mut local = heapless::String::<64>::new();
196
197 for _ in 0..local_len {
198 let byte = u8::arbitrary(u)?;
199 let c = match byte % 4 {
200 0 => ALPHABET[(byte % 26) as usize] as char,
201 1 => DIGITS[(byte % 10) as usize] as char,
202 2 => SPECIAL[byte as usize % SPECIAL.len()] as char,
203 _ => '.',
204 };
205 local
206 .push(c)
207 .map_err(|_| arbitrary::Error::IncorrectFormat)?;
208 }
209
210 let label_count = 1 + (u8::arbitrary(u)? % 3);
212 let mut domain = heapless::String::<253>::new();
213
214 for label_idx in 0..label_count {
215 let label_len = 1 + (u8::arbitrary(u)? % 20).min(19);
216
217 for _ in 0..label_len {
218 let byte = u8::arbitrary(u)?;
219 let c = match byte % 2 {
220 0 => ALPHABET[(byte % 26) as usize] as char,
221 _ => DIGITS[(byte % 10) as usize] as char,
222 };
223 domain
224 .push(c)
225 .map_err(|_| arbitrary::Error::IncorrectFormat)?;
226 }
227
228 if label_idx < label_count - 1 {
229 domain
230 .push('.')
231 .map_err(|_| arbitrary::Error::IncorrectFormat)?;
232 }
233 }
234
235 let mut email = heapless::String::<254>::new();
237 email
238 .push_str(&local)
239 .map_err(|_| arbitrary::Error::IncorrectFormat)?;
240 email
241 .push('@')
242 .map_err(|_| arbitrary::Error::IncorrectFormat)?;
243 email
244 .push_str(&domain)
245 .map_err(|_| arbitrary::Error::IncorrectFormat)?;
246
247 Ok(Self(email))
248 }
249}
250
251impl Email {
252 #[allow(clippy::missing_panics_doc)]
268 pub fn new(s: &str) -> Result<Self, EmailError> {
269 if s.is_empty() {
270 return Err(EmailError::Empty);
271 }
272
273 if s.len() > 254 {
274 return Err(EmailError::TooLong(s.len()));
275 }
276
277 let at_count = s.chars().filter(|&c| c == '@').count();
278 if at_count == 0 {
279 return Err(EmailError::MissingAtSymbol);
280 }
281 if at_count > 1 {
282 return Err(EmailError::MultipleAtSymbols);
283 }
284
285 let (local_part, domain_part) = s.split_once('@').expect("at_count > 0");
286
287 if local_part.is_empty() {
288 return Err(EmailError::EmptyLocalPart);
289 }
290
291 if domain_part.is_empty() {
292 return Err(EmailError::EmptyDomain);
293 }
294
295 if local_part.len() > 64 {
296 return Err(EmailError::LocalPartTooLong(local_part.len()));
297 }
298
299 let mut local_validated = heapless::String::<64>::new();
300 let mut last_char: Option<char> = None;
301
302 for c in local_part.chars() {
303 if !Self::is_valid_local_char(c) {
304 return Err(EmailError::InvalidLocalPartChar(c));
305 }
306
307 if c == '.' {
308 if last_char.is_none() {
309 return Err(EmailError::LocalPartStartsWithDot);
310 }
311 if last_char == Some('.') {
312 return Err(EmailError::LocalPartConsecutiveDots);
313 }
314 }
315
316 last_char = Some(c);
317 local_validated
318 .push(c)
319 .map_err(|_| EmailError::LocalPartTooLong(64))?;
320 }
321
322 if local_part.ends_with('.') {
323 return Err(EmailError::LocalPartEndsWithDot);
324 }
325
326 let mut domain_validated = heapless::String::<253>::new();
327 for c in domain_part.chars() {
328 domain_validated
329 .push(c.to_ascii_lowercase())
330 .map_err(|_| EmailError::TooLong(253))?;
331 }
332
333 let total_len = local_validated.len() + 1 + domain_validated.len();
334 if total_len > 254 {
335 return Err(EmailError::TooLong(total_len));
336 }
337
338 let mut inner = heapless::String::<254>::new();
339 inner
340 .push_str(&local_validated)
341 .map_err(|_| EmailError::TooLong(total_len))?;
342 inner
343 .push('@')
344 .map_err(|_| EmailError::TooLong(total_len))?;
345 inner
346 .push_str(&domain_validated)
347 .map_err(|_| EmailError::TooLong(total_len))?;
348
349 Ok(Self(inner))
350 }
351
352 const fn is_valid_local_char(c: char) -> bool {
354 c.is_ascii_alphanumeric()
355 || matches!(
356 c,
357 '!' | '#'
358 | '$'
359 | '%'
360 | '&'
361 | '\''
362 | '*'
363 | '+'
364 | '-'
365 | '/'
366 | '='
367 | '?'
368 | '^'
369 | '_'
370 | '`'
371 | '{'
372 | '|'
373 | '}'
374 | '~'
375 | '.'
376 )
377 }
378
379 #[must_use]
390 #[inline]
391 pub fn as_str(&self) -> &str {
392 &self.0
393 }
394
395 #[must_use]
407 #[inline]
408 pub const fn as_inner(&self) -> &heapless::String<254> {
409 &self.0
410 }
411
412 #[must_use]
424 #[inline]
425 pub fn into_inner(self) -> heapless::String<254> {
426 self.0
427 }
428
429 #[must_use]
440 #[allow(clippy::missing_panics_doc)]
441 pub fn local_part(&self) -> &str {
442 self.as_str()
443 .split_once('@')
444 .map(|(local, _)| local)
445 .expect("email always contains @")
446 }
447
448 #[must_use]
459 #[allow(clippy::missing_panics_doc)]
460 pub fn domain_part(&self) -> &str {
461 self.as_str()
462 .split_once('@')
463 .map(|(_, domain)| domain)
464 .expect("email always contains @")
465 }
466}
467
468impl TryFrom<&str> for Email {
469 type Error = EmailError;
470
471 fn try_from(s: &str) -> Result<Self, Self::Error> {
472 Self::new(s)
473 }
474}
475
476impl From<Email> for heapless::String<254> {
477 fn from(email: Email) -> Self {
478 email.0
479 }
480}
481
482impl FromStr for Email {
483 type Err = EmailError;
484
485 fn from_str(s: &str) -> Result<Self, Self::Err> {
486 Self::new(s)
487 }
488}
489
490impl fmt::Display for Email {
491 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
492 write!(f, "{}", self.0)
493 }
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499
500 #[test]
501 fn test_new_valid_email() {
502 assert!(Email::new("user@example.com").is_ok());
503 assert!(Email::new("user.name@example.com").is_ok());
504 assert!(Email::new("user+tag@example.com").is_ok());
505 assert!(Email::new("user-name@example.com").is_ok());
506 assert!(Email::new("user_name@example.com").is_ok());
507 }
508
509 #[test]
510 fn test_empty_email() {
511 assert_eq!(Email::new(""), Err(EmailError::Empty));
512 }
513
514 #[test]
515 fn test_too_long_email() {
516 let long = format!("{}@example.com", "a".repeat(200));
517 assert_eq!(Email::new(&long), Err(EmailError::LocalPartTooLong(200)));
518 }
519
520 #[test]
521 fn test_total_too_long_email() {
522 let long = format!("user@{}.example.com", "a".repeat(250));
523 assert_eq!(Email::new(&long), Err(EmailError::TooLong(long.len())));
524 }
525
526 #[test]
527 fn test_missing_at_symbol() {
528 assert_eq!(
529 Email::new("userexample.com"),
530 Err(EmailError::MissingAtSymbol)
531 );
532 }
533
534 #[test]
535 fn test_multiple_at_symbols() {
536 assert_eq!(
537 Email::new("user@@example.com"),
538 Err(EmailError::MultipleAtSymbols)
539 );
540 assert_eq!(
541 Email::new("user@exa@mple.com"),
542 Err(EmailError::MultipleAtSymbols)
543 );
544 }
545
546 #[test]
547 fn test_empty_local_part() {
548 assert_eq!(Email::new("@example.com"), Err(EmailError::EmptyLocalPart));
549 }
550
551 #[test]
552 fn test_empty_domain_part() {
553 assert_eq!(Email::new("user@"), Err(EmailError::EmptyDomain));
554 }
555
556 #[test]
557 fn test_local_part_too_long() {
558 let long_local = "a".repeat(65);
559 assert_eq!(
560 Email::new(&format!("{long_local}@example.com")),
561 Err(EmailError::LocalPartTooLong(65))
562 );
563 }
564
565 #[test]
566 fn test_invalid_local_part_char() {
567 assert_eq!(
568 Email::new("user name@example.com"),
569 Err(EmailError::InvalidLocalPartChar(' '))
570 );
571 assert_eq!(
572 Email::new("user,name@example.com"),
573 Err(EmailError::InvalidLocalPartChar(','))
574 );
575 }
576
577 #[test]
578 fn test_local_part_starts_with_dot() {
579 assert_eq!(
580 Email::new(".user@example.com"),
581 Err(EmailError::LocalPartStartsWithDot)
582 );
583 }
584
585 #[test]
586 fn test_local_part_ends_with_dot() {
587 assert_eq!(
588 Email::new("user.@example.com"),
589 Err(EmailError::LocalPartEndsWithDot)
590 );
591 }
592
593 #[test]
594 fn test_local_part_consecutive_dots() {
595 assert_eq!(
596 Email::new("user..name@example.com"),
597 Err(EmailError::LocalPartConsecutiveDots)
598 );
599 }
600
601 #[test]
602 fn test_as_str() {
603 let email = Email::new("user@example.com").unwrap();
604 assert_eq!(email.as_str(), "user@example.com");
605 }
606
607 #[test]
608 fn test_as_inner() {
609 let email = Email::new("user@example.com").unwrap();
610 let inner = email.as_inner();
611 assert_eq!(inner.as_str(), "user@example.com");
612 }
613
614 #[test]
615 fn test_into_inner() {
616 let email = Email::new("user@example.com").unwrap();
617 let inner = email.into_inner();
618 assert_eq!(inner.as_str(), "user@example.com");
619 }
620
621 #[test]
622 fn test_local_part() {
623 let email = Email::new("user@example.com").unwrap();
624 assert_eq!(email.local_part(), "user");
625 }
626
627 #[test]
628 fn test_domain_part() {
629 let email = Email::new("user@example.com").unwrap();
630 assert_eq!(email.domain_part(), "example.com");
631 }
632
633 #[test]
634 fn test_try_from_str() {
635 let email = Email::try_from("user@example.com").unwrap();
636 assert_eq!(email.as_str(), "user@example.com");
637 }
638
639 #[test]
640 fn test_from_email_to_string() {
641 let email = Email::new("user@example.com").unwrap();
642 let inner: heapless::String<254> = email.into();
643 assert_eq!(inner.as_str(), "user@example.com");
644 }
645
646 #[test]
647 fn test_from_str() {
648 let email: Email = "user@example.com".parse().unwrap();
649 assert_eq!(email.as_str(), "user@example.com");
650 }
651
652 #[test]
653 fn test_from_str_invalid() {
654 assert!("".parse::<Email>().is_err());
655 assert!("userexample.com".parse::<Email>().is_err());
656 assert!("@example.com".parse::<Email>().is_err());
657 assert!("user@".parse::<Email>().is_err());
658 }
659
660 #[test]
661 fn test_display() {
662 let email = Email::new("user@example.com").unwrap();
663 assert_eq!(format!("{email}"), "user@example.com");
664 }
665
666 #[test]
667 fn test_equality() {
668 let email1 = Email::new("user@example.com").unwrap();
669 let email2 = Email::new("user@example.com").unwrap();
670 let email3 = Email::new("user2@example.com").unwrap();
671
672 assert_eq!(email1, email2);
673 assert_ne!(email1, email3);
674 }
675
676 #[test]
677 fn test_ordering() {
678 let email1 = Email::new("a@example.com").unwrap();
679 let email2 = Email::new("b@example.com").unwrap();
680
681 assert!(email1 < email2);
682 }
683
684 #[test]
685 fn test_clone() {
686 let email = Email::new("user@example.com").unwrap();
687 let email2 = email.clone();
688 assert_eq!(email, email2);
689 }
690
691 #[test]
692 fn test_special_characters() {
693 assert!(Email::new("user+tag@example.com").is_ok());
694 assert!(Email::new("user!name@example.com").is_ok());
695 assert!(Email::new("user#name@example.com").is_ok());
696 assert!(Email::new("user$name@example.com").is_ok());
697 assert!(Email::new("user%name@example.com").is_ok());
698 assert!(Email::new("user&name@example.com").is_ok());
699 assert!(Email::new("user'name@example.com").is_ok());
700 assert!(Email::new("user*name@example.com").is_ok());
701 assert!(Email::new("user-name@example.com").is_ok());
702 assert!(Email::new("user/name@example.com").is_ok());
703 assert!(Email::new("user=name@example.com").is_ok());
704 assert!(Email::new("user?name@example.com").is_ok());
705 assert!(Email::new("user^name@example.com").is_ok());
706 assert!(Email::new("user_name@example.com").is_ok());
707 assert!(Email::new("user`name@example.com").is_ok());
708 assert!(Email::new("user{name@example.com").is_ok());
709 assert!(Email::new("user|name@example.com").is_ok());
710 assert!(Email::new("user}name@example.com").is_ok());
711 assert!(Email::new("user~name@example.com").is_ok());
712 }
713
714 #[test]
715 fn test_domain_case_insensitive() {
716 let email1 = Email::new("user@Example.COM").unwrap();
717 let email2 = Email::new("user@example.com").unwrap();
718 assert_eq!(email1, email2);
719 assert_eq!(email1.as_str(), "user@example.com");
720 }
721
722 #[test]
723 fn test_local_part_case_sensitive() {
724 let email1 = Email::new("User@example.com").unwrap();
725 let email2 = Email::new("user@example.com").unwrap();
726 assert_ne!(email1, email2);
727 assert_eq!(email1.as_str(), "User@example.com");
728 }
729
730 #[test]
731 fn test_maximum_local_part_length() {
732 let local = "a".repeat(64);
733 let email = Email::new(&format!("{local}@example.com")).unwrap();
734 assert_eq!(email.local_part().len(), 64);
735 }
736
737 #[test]
738 fn test_maximum_email_length() {
739 let local = "a".repeat(64);
740 let domain = format!("{}.{}", "b".repeat(63), "c".repeat(63));
741 let email = Email::new(&format!("{local}@{domain}")).unwrap();
742 assert_eq!(email.as_str().len(), 64 + 1 + 127);
743 }
744
745 #[test]
746 fn test_error_display() {
747 assert_eq!(format!("{}", EmailError::Empty), "email cannot be empty");
748 assert_eq!(
749 format!("{}", EmailError::TooLong(300)),
750 "email exceeds maximum length of 254 characters (got 300)"
751 );
752 assert_eq!(
753 format!("{}", EmailError::MissingAtSymbol),
754 "email must contain an @ symbol"
755 );
756 assert_eq!(
757 format!("{}", EmailError::MultipleAtSymbols),
758 "email must contain exactly one @ symbol"
759 );
760 assert_eq!(
761 format!("{}", EmailError::EmptyLocalPart),
762 "email local part cannot be empty"
763 );
764 assert_eq!(
765 format!("{}", EmailError::EmptyDomain),
766 "email domain part cannot be empty"
767 );
768 assert_eq!(
769 format!("{}", EmailError::LocalPartTooLong(70)),
770 "email local part exceeds maximum length of 64 characters (got 70)"
771 );
772 assert_eq!(
773 format!("{}", EmailError::InvalidLocalPartChar(' ')),
774 "email local part contains invalid character ' '"
775 );
776 assert_eq!(
777 format!("{}", EmailError::LocalPartStartsWithDot),
778 "email local part cannot start with a dot"
779 );
780 assert_eq!(
781 format!("{}", EmailError::LocalPartEndsWithDot),
782 "email local part cannot end with a dot"
783 );
784 assert_eq!(
785 format!("{}", EmailError::LocalPartConsecutiveDots),
786 "email local part cannot contain consecutive dots"
787 );
788 assert_eq!(
789 format!("{}", EmailError::InvalidDomain),
790 "email domain part is invalid"
791 );
792 }
793
794 #[test]
795 fn test_single_character_local() {
796 assert!(Email::new("a@example.com").is_ok());
797 assert!(Email::new("1@example.com").is_ok());
798 assert!(Email::new("!@example.com").is_ok());
799 }
800
801 #[test]
802 fn test_single_character_domain() {
803 assert!(Email::new("user@com").is_ok());
804 }
805
806 #[test]
807 fn test_dots_in_local_part() {
808 assert!(Email::new("user.name@example.com").is_ok());
809 assert!(Email::new("u.n.a.m.e@example.com").is_ok());
810 assert!(Email::new("user.name.last@example.com").is_ok());
811 }
812
813 #[test]
814 fn test_digits_in_local_part() {
815 assert!(Email::new("user123@example.com").is_ok());
816 assert!(Email::new("123user@example.com").is_ok());
817 assert!(Email::new("123@example.com").is_ok());
818 }
819
820 #[test]
821 fn test_hash() {
822 use core::hash::Hash;
823 use core::hash::Hasher;
824
825 #[derive(Default)]
826 struct SimpleHasher(u64);
827
828 impl Hasher for SimpleHasher {
829 fn finish(&self) -> u64 {
830 self.0
831 }
832
833 fn write(&mut self, bytes: &[u8]) {
834 for byte in bytes {
835 self.0 = self.0.wrapping_mul(31).wrapping_add(*byte as u64);
836 }
837 }
838 }
839
840 let email1 = Email::new("user@example.com").unwrap();
841 let email2 = Email::new("user@example.com").unwrap();
842 let email3 = Email::new("user2@example.com").unwrap();
843
844 let mut hasher1 = SimpleHasher::default();
845 let mut hasher2 = SimpleHasher::default();
846 let mut hasher3 = SimpleHasher::default();
847
848 email1.hash(&mut hasher1);
849 email2.hash(&mut hasher2);
850 email3.hash(&mut hasher3);
851
852 assert_eq!(hasher1.finish(), hasher2.finish());
853 assert_ne!(hasher1.finish(), hasher3.finish());
854 }
855
856 #[test]
857 fn test_debug() {
858 let email = Email::new("user@example.com").unwrap();
859 assert_eq!(format!("{:?}", email), "Email(\"user@example.com\")");
860 }
861
862 #[test]
863 fn test_from_into_inner_roundtrip() {
864 let email = Email::new("user@example.com").unwrap();
865 let inner: heapless::String<254> = email.into();
866 let email2 = Email::new(inner.as_str()).unwrap();
867 assert_eq!(email2.as_str(), "user@example.com");
868 }
869}