Skip to main content

bare_types/net/
email.rs

1//! Email address type for network programming.
2//!
3//! This module provides a type-safe abstraction for email addresses,
4//! ensuring compliance with RFC 5322 email address specifications.
5//!
6//! # RFC 5322 Email Address Rules
7//!
8//! According to [RFC 5322 §3.4.1](https://datatracker.ietf.org/doc/html/rfc5322#section-3.4.1):
9//!
10//! - Total length: up to 254 characters (RFC 5321 §4.5.3.1.1)
11//! - Local part: up to 64 characters (RFC 5321 §4.5.3.1.1)
12//! - Domain part: up to 255 characters (RFC 5321 §4.5.3.1.1)
13//! - Must contain exactly one @ symbol
14//! - Local part can contain: letters, digits, and special characters (! # $ % & ' * + - / = ? ^ _ { | } ~) and backtick (`` ` ``)
15//! - Local part can contain dots (.), but not at the start or end, and not consecutively
16//! - Local part can be quoted with double quotes for special characters
17//! - Domain part follows RFC 1035 domain name rules
18//! - Domain part is case-insensitive (stored in lowercase)
19//! - Local part is case-sensitive (preserved as-is)
20//!
21//! # Examples
22//!
23//! ```rust
24//! use bare_types::net::Email;
25//!
26//! // Create an email
27//! let email = Email::new("user@example.com")?;
28//!
29//! // Get the local part
30//! assert_eq!(email.local_part(), "user");
31//!
32//! // Get the domain part
33//! assert_eq!(email.domain_part(), "example.com");
34//!
35//! // Get the string representation
36//! assert_eq!(email.as_str(), "user@example.com");
37//!
38//! // Parse from string
39//! let email: Email = "user@example.com".parse()?;
40//! # Ok::<(), bare_types::net::EmailError>(())
41//! ```
42
43use 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/// Error type for email validation.
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
55#[non_exhaustive]
56pub enum EmailError {
57    /// Empty email
58    ///
59    /// The provided string is empty. Email addresses must contain at least one character.
60    Empty,
61    /// Email exceeds maximum length of 254 characters
62    ///
63    /// According to RFC 5321, email addresses must not exceed 254 characters.
64    /// This variant contains the actual length of the provided email.
65    TooLong(usize),
66    /// Missing @ symbol
67    ///
68    /// Email addresses must contain exactly one @ symbol separating the local and domain parts.
69    MissingAtSymbol,
70    /// Multiple @ symbols
71    ///
72    /// Email addresses must contain exactly one @ symbol.
73    MultipleAtSymbols,
74    /// Empty local part
75    ///
76    /// The local part (before @) is empty.
77    EmptyLocalPart,
78    /// Empty domain part
79    ///
80    /// The domain part (after @) is empty.
81    EmptyDomain,
82    /// Local part exceeds maximum length of 64 characters
83    ///
84    /// According to RFC 5321, the local part must not exceed 64 characters.
85    /// This variant contains the actual length of the local part.
86    LocalPartTooLong(usize),
87    /// Invalid character in local part
88
89    /// The local part contains an invalid character.
90    /// This variant contains the invalid character.
91    InvalidLocalPartChar(char),
92    /// Local part starts with a dot
93    ///
94    /// The local part cannot start with a dot.
95    LocalPartStartsWithDot,
96    /// Local part ends with a dot
97    ///
98    /// The local part cannot end with a dot.
99    LocalPartEndsWithDot,
100    /// Local part contains consecutive dots
101    ///
102    /// The local part cannot contain consecutive dots.
103    LocalPartConsecutiveDots,
104    /// Invalid domain part
105    ///
106    /// The domain part is invalid according to RFC 1035.
107    /// This variant contains the domain validation error.
108    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/// An email address.
146///
147/// This type provides type-safe email addresses with RFC 5322 validation.
148/// It uses the newtype pattern with `#[repr(transparent)]` for zero-cost abstraction.
149///
150/// # Invariants
151///
152/// - Total length is 1-254 characters
153/// - Local part is 1-64 characters
154/// - Domain part follows RFC 1035 domain name rules
155/// - Contains exactly one @ symbol
156/// - Domain part is stored in lowercase for case-insensitive comparison
157/// - Local part is case-sensitive (preserved as-is)
158///
159/// # Examples
160///
161/// ```rust
162/// use bare_types::net::Email;
163///
164/// // Create an email
165/// let email = Email::new("user@example.com")?;
166///
167/// // Access the string representation
168/// assert_eq!(email.as_str(), "user@example.com");
169///
170/// // Get the local part
171/// assert_eq!(email.local_part(), "user");
172///
173/// // Get the domain part
174/// assert_eq!(email.domain_part(), "example.com");
175///
176/// // Parse from string
177/// let email: Email = "user@example.com".parse()?;
178/// # Ok::<(), bare_types::net::EmailError>(())
179/// ```
180#[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        // Generate local part (1-20 characters)
194        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        // Generate domain part (1-3 labels)
211        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        // Combine local and domain
236        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    /// Creates a new email address from a string.
253    ///
254    /// # Errors
255    ///
256    /// Returns `EmailError` if the string does not comply with RFC 5322.
257    ///
258    /// # Examples
259    ///
260    /// ```rust
261    /// use bare_types::net::Email;
262    ///
263    /// let email = Email::new("user@example.com")?;
264    /// assert_eq!(email.as_str(), "user@example.com");
265    /// # Ok::<(), bare_types::net::EmailError>(())
266    /// ```
267    #[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    /// Checks if a character is valid in the local part of an email address.
353    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    /// Returns the email address as a string slice.
380    ///
381    /// # Examples
382    ///
383    /// ```rust
384    /// use bare_types::net::Email;
385    ///
386    /// let email = Email::new("user@example.com").unwrap();
387    /// assert_eq!(email.as_str(), "user@example.com");
388    /// ```
389    #[must_use]
390    #[inline]
391    pub fn as_str(&self) -> &str {
392        &self.0
393    }
394
395    /// Returns a reference to the underlying `heapless::String`.
396    ///
397    /// # Examples
398    ///
399    /// ```rust
400    /// use bare_types::net::Email;
401    ///
402    /// let email = Email::new("user@example.com").unwrap();
403    /// let inner: &heapless::String<254> = email.as_inner();
404    /// assert_eq!(inner.as_str(), "user@example.com");
405    /// ```
406    #[must_use]
407    #[inline]
408    pub const fn as_inner(&self) -> &heapless::String<254> {
409        &self.0
410    }
411
412    /// Consumes this email and returns the underlying string.
413    ///
414    /// # Examples
415    ///
416    /// ```rust
417    /// use bare_types::net::Email;
418    ///
419    /// let email = Email::new("user@example.com").unwrap();
420    /// let inner = email.into_inner();
421    /// assert_eq!(inner.as_str(), "user@example.com");
422    /// ```
423    #[must_use]
424    #[inline]
425    pub fn into_inner(self) -> heapless::String<254> {
426        self.0
427    }
428
429    /// Returns the local part of the email address (before @).
430    ///
431    /// # Examples
432    ///
433    /// ```rust
434    /// use bare_types::net::Email;
435    ///
436    /// let email = Email::new("user@example.com").unwrap();
437    /// assert_eq!(email.local_part(), "user");
438    /// ```
439    #[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    /// Returns the domain part of the email address (after @).
449    ///
450    /// # Examples
451    ///
452    /// ```rust
453    /// use bare_types::net::Email;
454    ///
455    /// let email = Email::new("user@example.com").unwrap();
456    /// assert_eq!(email.domain_part(), "example.com");
457    /// ```
458    #[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}