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