1use std::fmt::Display;
2use std::str::FromStr;
3
4#[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}