email_address_parser/email_address.rs
1use crate::nom_parser;
2#[cfg(target_arch = "wasm32")]
3extern crate console_error_panic_hook;
4use std::fmt;
5use std::hash::Hash;
6use std::str::FromStr;
7#[cfg(target_arch = "wasm32")]
8use wasm_bindgen::prelude::*;
9
10/// Options for parsing.
11///
12/// There is only one available option so far `is_lax` which can be set to
13/// `true` or `false` to enable/disable obsolete parts parsing.
14/// The default is `false`.
15#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
16#[derive(Debug,Clone)]
17pub struct ParsingOptions {
18 pub is_lax: bool,
19}
20
21#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
22impl ParsingOptions {
23 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(constructor))]
24 pub fn new(is_lax: bool) -> ParsingOptions {
25 ParsingOptions { is_lax }
26 }
27}
28
29impl Default for ParsingOptions {
30 fn default() -> Self {
31 ParsingOptions::new(false)
32 }
33}
34
35/// Allows conversion from string slices (&str) to EmailAddress using the FromStr trait.
36/// This wraps around `EmailAddress::parse` using the default `ParsingOptions`.
37///
38/// # Examples
39/// ```
40/// use email_address_parser::EmailAddress;
41/// use std::str::FromStr;
42///
43/// const input_address : &str = "string@slice.com";
44///
45/// let myaddr : EmailAddress = input_address.parse().expect("could not parse str into EmailAddress");
46/// let myotheraddr = EmailAddress::from_str(input_address).expect("could create EmailAddress from str");
47///
48/// assert_eq!(myaddr, myotheraddr);
49/// ```
50impl FromStr for EmailAddress {
51 type Err = fmt::Error;
52
53 fn from_str(s: &str) -> Result<Self, Self::Err> {
54 let opts = ParsingOptions::default();
55 if let Some(email) = EmailAddress::parse(s, Some(opts)) {
56 Ok(email)
57 } else {
58 Err(fmt::Error)
59 }
60 }
61}
62
63/// Email address struct.
64///
65/// # Examples
66/// ```
67/// use email_address_parser::EmailAddress;
68///
69/// assert!(EmailAddress::parse("foo@-bar.com", None).is_none());
70/// let email = EmailAddress::parse("foo@bar.com", None);
71/// assert!(email.is_some());
72/// let email = email.unwrap();
73/// assert_eq!(email.get_local_part(), "foo");
74/// assert_eq!(email.get_domain(), "bar.com");
75/// assert_eq!(format!("{}", email), "foo@bar.com");
76/// ```
77#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
78#[derive(Clone, Debug, PartialEq, Eq, Hash)]
79pub struct EmailAddress {
80 local_part: String,
81 domain: String,
82}
83
84#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
85impl EmailAddress {
86 #![warn(missing_docs)]
87 #![warn(rustdoc::missing_doc_code_examples)]
88
89 /// This is a WASM wrapper over EmailAddress::new that panics.
90 /// If you are using this lib from Rust then consider using EmailAddress::new.
91 ///
92 /// # Examples
93 /// ```
94 /// use email_address_parser::EmailAddress;
95 ///
96 /// let email = EmailAddress::_new("foo", "bar.com", None);
97 /// ```
98 ///
99 /// # Panics
100 ///
101 /// This method panics if the local part or domain is invalid.
102 ///
103 /// ```rust,should_panic,ignore-wasm32
104 /// use email_address_parser::EmailAddress;
105 ///
106 /// EmailAddress::_new("foo", "-bar.com", None);
107 /// ```
108 #[doc(hidden)]
109 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(constructor))]
110 pub fn _new(local_part: &str, domain: &str, options: Option<ParsingOptions>) -> EmailAddress {
111 #[cfg(target_arch = "wasm32")]
112 console_error_panic_hook::set_once();
113 match EmailAddress::new(local_part, domain, options) {
114 Ok(instance) => instance,
115 Err(message) => panic!("{}", message),
116 }
117 }
118
119 /// Parses a given string as an email address.
120 ///
121 /// Accessible from WASM.
122 ///
123 /// Returns `Some(EmailAddress)` if the parsing is successful, else `None`.
124 /// # Examples
125 /// ```
126 /// use email_address_parser::*;
127 ///
128 /// // strict parsing
129 /// let email = EmailAddress::parse("foo@bar.com", None);
130 /// assert!(email.is_some());
131 /// let email = email.unwrap();
132 /// assert_eq!(email.get_local_part(), "foo");
133 /// assert_eq!(email.get_domain(), "bar.com");
134 ///
135 /// // non-strict parsing
136 /// let email = EmailAddress::parse("\u{0d}\u{0a} \u{0d}\u{0a} test@iana.org", Some(ParsingOptions::new(true)));
137 /// assert!(email.is_some());
138 ///
139 /// // parsing invalid address
140 /// let email = EmailAddress::parse("test@-iana.org", Some(ParsingOptions::new(true)));
141 /// assert!(email.is_none());
142 /// let email = EmailAddress::parse("test@-iana.org", Some(ParsingOptions::new(true)));
143 /// assert!(email.is_none());
144 /// let email = EmailAddress::parse("test", Some(ParsingOptions::new(true)));
145 /// assert!(email.is_none());
146 /// let email = EmailAddress::parse("test", Some(ParsingOptions::new(true)));
147 /// assert!(email.is_none());
148 /// ```
149 pub fn parse(input: &str, options: Option<ParsingOptions>) -> Option<EmailAddress> {
150 let (local_part, domain) = EmailAddress::parse_core(input, options)?;
151 Some(EmailAddress {
152 local_part: String::from(local_part),
153 domain: String::from(domain),
154 })
155 }
156 /// Validates if the given `input` string is an email address or not.
157 ///
158 /// Returns `true` if the `input` is valid, `false` otherwise.
159 /// Unlike the `parse` method, it does not instantiate an `EmailAddress`.
160 /// # Examples
161 /// ```
162 /// use email_address_parser::*;
163 ///
164 /// // strict validation
165 /// assert!(EmailAddress::is_valid("foo@bar.com", None));
166 ///
167 /// // non-strict validation
168 /// assert!(EmailAddress::is_valid("\u{0d}\u{0a} \u{0d}\u{0a} test@iana.org", Some(ParsingOptions::new(true))));
169 ///
170 /// // invalid address
171 /// assert!(!EmailAddress::is_valid("test@-iana.org", Some(ParsingOptions::new(true))));
172 /// assert!(!EmailAddress::is_valid("test@-iana.org", Some(ParsingOptions::new(true))));
173 /// assert!(!EmailAddress::is_valid("test", Some(ParsingOptions::new(true))));
174 /// assert!(!EmailAddress::is_valid("test", Some(ParsingOptions::new(true))));
175 /// ```
176 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = "isValid"))]
177 pub fn is_valid(input: &str, options: Option<ParsingOptions>) -> bool {
178 EmailAddress::parse_core(input, options).is_some()
179 }
180
181 /// Returns the local part of the email address.
182 ///
183 /// Note that if you are using this library from rust, then consider using the `get_local_part` method instead.
184 /// This returns a cloned copy of the local part string, instead of a borrowed `&str`, and exists purely for WASM interoperability.
185 ///
186 /// # Examples
187 /// ```
188 /// use email_address_parser::EmailAddress;
189 ///
190 /// let email = EmailAddress::new("foo", "bar.com", None).unwrap();
191 /// assert_eq!(email.localPart(), "foo");
192 ///
193 /// let email = EmailAddress::parse("foo@bar.com", None).unwrap();
194 /// assert_eq!(email.localPart(), "foo");
195 /// ```
196 #[doc(hidden)]
197 #[allow(non_snake_case)]
198 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(getter))]
199 pub fn localPart(&self) -> String {
200 self.local_part.clone()
201 }
202
203 /// Returns the domain of the email address.
204 ///
205 /// Note that if you are using this library from rust, then consider using the `get_domain` method instead.
206 /// This returns a cloned copy of the domain string, instead of a borrowed `&str`, and exists purely for WASM interoperability.
207 ///
208 /// # Examples
209 /// ```
210 /// use email_address_parser::EmailAddress;
211 ///
212 /// let email = EmailAddress::new("foo", "bar.com", None).unwrap();
213 /// assert_eq!(email.domain(), "bar.com");
214 ///
215 /// let email = EmailAddress::parse("foo@bar.com", None).unwrap();
216 /// assert_eq!(email.domain(), "bar.com");
217 /// ```
218 #[doc(hidden)]
219 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(getter))]
220 pub fn domain(&self) -> String {
221 self.domain.clone()
222 }
223
224 /// Returns the formatted EmailAddress.
225 /// This exists purely for WASM interoperability.
226 #[doc(hidden)]
227 #[allow(non_snake_case)]
228 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip_typescript))]
229 pub fn toString(&self) -> String {
230 format!("{}@{}", self.local_part, self.domain)
231 }
232
233 fn parse_core<'i>(
234 input: &'i str,
235 options: Option<ParsingOptions>,
236 ) -> Option<(&'i str, &'i str)> {
237 let options = options.unwrap_or_default();
238 nom_parser::parse_address(input, options.is_lax)
239 }
240}
241
242impl EmailAddress {
243 #![warn(missing_docs)]
244 #![warn(rustdoc::missing_doc_code_examples)]
245
246 /// Instantiates a new `Some(EmailAddress)` for a valid local part and domain.
247 /// Returns `Err` otherwise.
248 ///
249 /// # Examples
250 /// ```
251 /// use email_address_parser::EmailAddress;
252 ///
253 /// let email = EmailAddress::new("foo", "bar.com", None).unwrap();
254 ///
255 /// assert_eq!(EmailAddress::new("foo", "-bar.com", None).is_err(), true);
256 /// ```
257 pub fn new(
258 local_part: &str,
259 domain: &str,
260 options: Option<ParsingOptions>,
261 ) -> Result<EmailAddress, String> {
262 match EmailAddress::parse(&format!("{}@{}", local_part, domain), options.clone()) {
263 Some(email_address) => Ok(email_address),
264 None => {
265 if !options.unwrap_or_default().is_lax {
266 return Err(format!("Invalid local part '{}'.", local_part));
267 }
268 Ok(EmailAddress {
269 local_part: String::from(local_part),
270 domain: String::from(domain),
271 })
272 }
273 }
274 }
275
276 /// Returns the local part of the email address.
277 ///
278 /// Not accessible from WASM.
279 ///
280 /// # Examples
281 /// ```
282 /// use email_address_parser::EmailAddress;
283 ///
284 /// let email = EmailAddress::new("foo", "bar.com", None).unwrap();
285 /// assert_eq!(email.get_local_part(), "foo");
286 ///
287 /// let email = EmailAddress::parse("foo@bar.com", None).unwrap();
288 /// assert_eq!(email.get_local_part(), "foo");
289 /// ```
290 pub fn get_local_part(&self) -> &str {
291 self.local_part.as_str()
292 }
293 /// Returns the domain of the email address.
294 ///
295 /// Not accessible from WASM.
296 ///
297 /// # Examples
298 /// ```
299 /// use email_address_parser::EmailAddress;
300 ///
301 /// let email = EmailAddress::new("foo", "bar.com", None).unwrap();
302 /// assert_eq!(email.get_domain(), "bar.com");
303 ///
304 /// let email = EmailAddress::parse("foo@bar.com", None).unwrap();
305 /// assert_eq!(email.get_domain(), "bar.com");
306 /// ```
307 pub fn get_domain(&self) -> &str {
308 self.domain.as_str()
309 }
310}
311
312impl fmt::Display for EmailAddress {
313 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
314 formatter.write_fmt(format_args!("{}@{}", self.local_part, self.domain))
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[test]
323 fn email_address_instantiation_works() {
324 let address = EmailAddress::new("foo", "bar.com", None).unwrap();
325 assert_eq!(address.get_local_part(), "foo");
326 assert_eq!(address.get_domain(), "bar.com");
327 assert_eq!(format!("{}", address), "foo@bar.com");
328 }
329
330 #[test]
331 fn email_address_supports_equality_checking() {
332 let foo_at_bar_dot_com = EmailAddress::new("foo", "bar.com", None).unwrap();
333 let foo_at_bar_dot_com_2 = EmailAddress::new("foo", "bar.com", None).unwrap();
334 let foob_at_ar_dot_com = EmailAddress::new("foob", "ar.com", None).unwrap();
335
336 assert_eq!(foo_at_bar_dot_com, foo_at_bar_dot_com);
337 assert_eq!(foo_at_bar_dot_com, foo_at_bar_dot_com_2);
338 assert_ne!(foo_at_bar_dot_com, foob_at_ar_dot_com);
339 assert_ne!(foo_at_bar_dot_com_2, foob_at_ar_dot_com);
340 }
341
342 #[test]
343 fn domain_rule_does_not_parse_dash_google_dot_com() {
344 assert_eq!(nom_parser::test_parse_domain_complete("-google.com"), false);
345 }
346
347 #[test]
348 fn domain_rule_does_not_parse_dash_google_dot_com_obs() {
349 assert_eq!(nom_parser::test_parse_domain_obs("-google.com"), false);
350 }
351
352 #[test]
353 fn domain_rule_does_not_parse_dash_google_dash_dot_com() {
354 assert_eq!(nom_parser::test_parse_domain_complete("-google-.com"), false);
355 }
356
357 #[test]
358 fn domain_rule_parses_google_dash_dot_com() {
359 assert_eq!(nom_parser::test_parse_domain_complete("google-.com"), false);
360 }
361
362 #[test]
363 fn domain_complete_punycode_domain() {
364 assert_eq!(
365 nom_parser::test_parse_domain_complete("xn--masekowski-d0b.pl"),
366 true
367 );
368 }
369
370 #[test]
371 fn can_parse_deprecated_local_part() {
372 assert_eq!(nom_parser::test_parse_local_part_obs("\"test\".\"test\""), true);
373 }
374
375 #[test]
376 fn can_parse_email_with_deprecated_local_part() {
377 assert_eq!(
378 nom_parser::test_parse_address_obs("\"test\".\"test\"@iana.org"),
379 true
380 );
381 }
382
383 #[test]
384 fn can_parse_domain_with_space() {
385 assert_eq!(nom_parser::test_parse_domain_obs(" iana .com"), true);
386 let actual = EmailAddress::parse("test@ iana .com", Some(ParsingOptions::new(true)));
387 assert_eq!(actual.is_some(), true, "test@ iana .com");
388 }
389
390 #[test]
391 fn can_parse_email_with_cfws_near_at() {
392 let email = " test @iana.org";
393 let actual = EmailAddress::parse(&email, None);
394 println!("{:#?}", actual);
395 assert_eq!(format!("{}", actual.unwrap()), email);
396 }
397
398 #[test]
399 fn can_parse_email_with_crlf() {
400 let email = "\u{0d}\u{0a} test@iana.org";
401 let actual = EmailAddress::parse(&email, Some(ParsingOptions::new(true)));
402 println!("{:#?}", actual);
403 assert_eq!(format!("{}", actual.unwrap()), email);
404 }
405
406 #[test]
407 fn can_parse_local_part_with_space() {
408 assert_eq!(nom_parser::test_parse_address_obs("test . test@iana.org"), true);
409 }
410
411 #[test]
412 fn can_parse_domain_with_bel() {
413 assert_eq!(
414 nom_parser::test_parse_domain_literal("[RFC-5322-\u{07}-domain-literal]"),
415 true
416 );
417 }
418
419 #[test]
420 fn can_parse_local_part_with_space_and_quote() {
421 assert_eq!(nom_parser::test_parse_local_part_complete("\"test test\""), true);
422 }
423
424 #[test]
425 fn can_parse_idn() {
426 assert_eq!(nom_parser::test_parse_domain_complete("bücher.com"), true);
427 }
428
429 #[test]
430 fn parsing_empty_local_part_and_domain() {
431 let actual = EmailAddress::parse("@", Some(ParsingOptions::new(true)));
432 assert_eq!(actual.is_none(), true, "expected none");
433 let actual = EmailAddress::new("", "", Some(ParsingOptions::new(false)));
434 assert_eq!(actual.is_err(), true, "expected error");
435 let actual = EmailAddress::new("", "", Some(ParsingOptions::new(true)));
436 assert_eq!(actual.is_ok(), true, "expected ok");
437 let actual = actual.unwrap();
438 assert_eq!(actual.domain, "");
439 assert_eq!(actual.local_part, "");
440 }
441}