Skip to main content

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}