email_address_parser/
email_address.rs

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