Skip to main content

email_message/
email.rs

1use std::fmt::Display;
2use std::str::FromStr;
3
4/// A validated RFC 5322 `addr-spec` email address.
5#[derive(Clone, Debug, PartialEq, Eq, Hash)]
6pub struct Email {
7    value: String,
8}
9
10#[derive(Debug, thiserror::Error)]
11#[error(transparent)]
12pub struct EmailParseError(#[from] addr_spec::ParseError);
13
14impl Email {
15    pub fn as_str(&self) -> &str {
16        self.value.as_str()
17    }
18}
19
20impl Display for Email {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        f.write_str(self.as_str())
23    }
24}
25
26impl AsRef<str> for Email {
27    fn as_ref(&self) -> &str {
28        self.as_str()
29    }
30}
31
32impl FromStr for Email {
33    type Err = EmailParseError;
34
35    fn from_str(s: &str) -> Result<Self, Self::Err> {
36        Ok(Self {
37            value: addr_spec::AddrSpec::normalize(s)?,
38        })
39    }
40}
41
42#[cfg(test)]
43mod tests {
44    use super::Email;
45
46    const RFC_VALID_EMAILS: &[&str] = &[
47        "jdoe@one.test",
48        "simple@example.com",
49        "very.common@example.com",
50        "disposable.style.email.with+symbol@example.com",
51        "other.email-with-hyphen@example.com",
52        "fully-qualified-domain@example.com",
53        "user.name+tag+sorting@example.com",
54        "x@example.com",
55        "example-indeed@strange-example.com",
56        "admin@mailserver1",
57        "example@s.example",
58        "\"john..doe\"@example.org",
59        "mailhost!username@example.org",
60        "user%example.com@example.org",
61    ];
62
63    const INVALID_EMAILS: &[&str] = &[
64        "plainaddress",
65        "@missing-local.org",
66        "A@b@c@example.com",
67        "john..doe@example.org",
68        "john.doe@example..org",
69        "john.doe.@example.org",
70        ".john.doe@example.org",
71    ];
72
73    #[test]
74    fn email_from_str_accepts_rfc_examples() {
75        for input in RFC_VALID_EMAILS {
76            let parsed = input.parse::<Email>();
77            assert!(parsed.is_ok(), "expected valid email: {input}");
78        }
79    }
80
81    #[test]
82    fn email_from_str_rejects_invalid_examples() {
83        for input in INVALID_EMAILS {
84            let parsed = input.parse::<Email>();
85            assert!(parsed.is_err(), "expected invalid email: {input}");
86        }
87    }
88}