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#[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
37impl 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#[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 #[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 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 #[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 #[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 #[doc(hidden)]
237 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(getter))]
238 pub fn domain(&self) -> String {
239 self.domain.clone()
240 }
241
242 #[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 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 pub fn get_local_part(&self) -> &str {
319 self.local_part.as_str()
320 }
321 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}