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