1use std::fmt::Display;
2use std::str::FromStr;
3
4#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
25#[derive(Clone, Debug, PartialEq, Eq, Hash)]
26pub struct EmailAddress {
27 value: String,
28}
29
30impl EmailAddress {
31 #[must_use]
33 pub fn as_str(&self) -> &str {
34 self.value.as_str()
35 }
36}
37
38impl Display for EmailAddress {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 f.write_str(self.as_str())
41 }
42}
43
44impl AsRef<str> for EmailAddress {
45 fn as_ref(&self) -> &str {
46 self.as_str()
47 }
48}
49
50impl From<EmailAddress> for String {
51 fn from(value: EmailAddress) -> Self {
52 value.value
53 }
54}
55
56#[derive(Debug, thiserror::Error)]
57#[error(transparent)]
58pub struct EmailAddressParseError(#[from] addr_spec::ParseError);
59
60impl FromStr for EmailAddress {
61 type Err = EmailAddressParseError;
62
63 fn from_str(s: &str) -> Result<Self, Self::Err> {
64 let parsed = addr_spec::AddrSpec::from_str(s)?;
65 let is_literal = parsed.is_literal();
71 let (local, domain) = parsed.into_serialized_parts();
72 let value = if is_literal {
73 format!("{local}@{domain}")
74 } else {
75 format!("{local}@{}", domain.to_ascii_lowercase())
76 };
77 Ok(Self { value })
78 }
79}
80
81impl TryFrom<&str> for EmailAddress {
82 type Error = EmailAddressParseError;
83
84 fn try_from(value: &str) -> Result<Self, Self::Error> {
93 Self::from_str(value)
94 }
95}
96
97#[cfg(feature = "serde")]
98impl serde::Serialize for EmailAddress {
99 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
100 where
101 S: serde::Serializer,
102 {
103 serializer.serialize_str(self.as_str())
104 }
105}
106
107#[cfg(feature = "serde")]
108impl<'de> serde::Deserialize<'de> for EmailAddress {
109 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
110 where
111 D: serde::Deserializer<'de>,
112 {
113 let value = String::deserialize(deserializer)?;
114 value.parse().map_err(serde::de::Error::custom)
115 }
116}
117
118#[cfg(feature = "arbitrary")]
119impl<'a> arbitrary::Arbitrary<'a> for EmailAddress {
120 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
121 let local = u64::arbitrary(u)?;
122 let domain = u32::arbitrary(u)?;
123 format!("user{local}@domain{domain}.test")
124 .parse()
125 .map_err(|_| arbitrary::Error::IncorrectFormat)
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::EmailAddress;
132
133 const RFC_VALID_EMAILS: &[&str] = &[
134 "jdoe@one.test",
135 "simple@example.com",
136 "very.common@example.com",
137 "disposable.style.email.with+symbol@example.com",
138 "other.email-with-hyphen@example.com",
139 "fully-qualified-domain@example.com",
140 "user.name+tag+sorting@example.com",
141 "x@example.com",
142 "example-indeed@strange-example.com",
143 "admin@mailserver1",
144 "example@s.example",
145 "\"john..doe\"@example.org",
146 "mailhost!username@example.org",
147 "user%example.com@example.org",
148 ];
149
150 const INVALID_EMAILS: &[&str] = &[
151 "plainaddress",
152 "@missing-local.org",
153 "A@b@c@example.com",
154 "john..doe@example.org",
155 "john.doe@example..org",
156 "john.doe.@example.org",
157 ".john.doe@example.org",
158 ];
159
160 #[test]
161 fn email_from_str_accepts_rfc_examples() {
162 for input in RFC_VALID_EMAILS {
163 let parsed = input.parse::<EmailAddress>();
164 assert!(parsed.is_ok(), "expected valid email: {input}");
165 }
166 }
167
168 #[test]
169 fn email_from_str_rejects_invalid_examples() {
170 for input in INVALID_EMAILS {
171 let parsed = input.parse::<EmailAddress>();
172 assert!(parsed.is_err(), "expected invalid email: {input}");
173 }
174 }
175
176 #[test]
181 fn email_from_str_accepts_ipv4_literal_domain() {
182 let parsed = "user@[192.168.1.1]".parse::<EmailAddress>();
183 assert!(parsed.is_ok(), "expected IPv4 literal to parse: {parsed:?}");
184 }
185
186 #[test]
187 fn email_from_str_accepts_ipv6_literal_domain() {
188 let parsed = "user@[IPv6:fe80::1]".parse::<EmailAddress>();
189 assert!(parsed.is_ok(), "expected IPv6 literal to parse: {parsed:?}");
190 }
191
192 #[test]
196 fn email_domain_is_case_folded_for_eq_and_hash() {
197 let a: EmailAddress = "User.Name@Example.COM".parse().unwrap();
198 let b: EmailAddress = "User.Name@example.com".parse().unwrap();
199 assert_eq!(a, b);
200 assert_eq!(a.as_str(), "User.Name@example.com");
201 assert_eq!(b.as_str(), "User.Name@example.com");
202
203 use std::collections::HashSet;
205 let mut set: HashSet<EmailAddress> = HashSet::new();
206 set.insert(a);
207 assert!(set.contains(&b));
208 }
209
210 #[test]
212 fn email_local_part_case_is_preserved() {
213 let upper: EmailAddress = "John.Doe@example.com".parse().unwrap();
214 let lower: EmailAddress = "john.doe@example.com".parse().unwrap();
215 assert_ne!(upper, lower);
216 assert_eq!(upper.as_str(), "John.Doe@example.com");
217 assert_eq!(lower.as_str(), "john.doe@example.com");
218 }
219
220 #[test]
223 fn email_ipv6_literal_domain_is_not_case_folded() {
224 let parsed: EmailAddress = "user@[IPv6:Fe80::1]".parse().unwrap();
225 assert!(parsed.as_str().contains("IPv6"));
231 }
232}