1#![doc = include_str!("../README.md")]
2#![cfg_attr(feature = "nightly", feature(test))]
3
4mod ascii;
5mod parser;
6mod unicode;
7
8use std::{
9 fmt::{self, Write},
10 str::FromStr,
11};
12
13pub use parser::ParseError;
14use parser::{is_ascii_control_and_not_htab, is_not_atext, is_not_dtext, Parser};
15
16fn quote(value: &str) -> String {
17 ascii::escape!(value, b'\\', b'"' | b' ' | b'\t')
18}
19
20#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
105pub struct AddrSpec {
106 local_part: String,
107 domain: String,
108 #[cfg(feature = "literals")]
109 literal: bool,
110}
111
112impl AddrSpec {
113 #[inline]
131 pub fn normalize<Address>(address: Address) -> Result<String, ParseError>
132 where
133 Address: AsRef<str>,
134 {
135 Ok(address.as_ref().parse::<Self>()?.to_string())
136 }
137
138 pub fn new<LocalPart, Domain>(local_part: LocalPart, domain: Domain) -> Result<Self, ParseError>
141 where
142 LocalPart: AsRef<str>,
143 Domain: AsRef<str>,
144 {
145 Self::new_impl(local_part.as_ref(), domain.as_ref(), false)
146 }
147
148 #[cfg(feature = "literals")]
151 pub fn with_literal<LocalPart, Domain>(
152 local_part: LocalPart,
153 domain: Domain,
154 ) -> Result<Self, ParseError>
155 where
156 LocalPart: AsRef<str>,
157 Domain: AsRef<str>,
158 {
159 Self::new_impl(local_part.as_ref(), domain.as_ref(), true)
160 }
161
162 fn new_impl(local_part: &str, domain: &str, literal: bool) -> Result<Self, ParseError> {
163 if let Some(index) = local_part.find(is_ascii_control_and_not_htab) {
164 return Err(ParseError("invalid character in local part", index));
165 }
166
167 if literal {
168 if let Some(index) = domain.find(is_not_dtext) {
169 return Err(ParseError("invalid character in literal domain", index));
170 }
171 } else {
172 let mut parser = Parser::new(domain);
175 parser.parse_dot_atom("empty label in domain")?;
176 parser.check_end("invalid character in domain")?;
177 }
178 Ok(Self {
179 local_part: unicode::normalize(local_part),
180 domain: unicode::normalize(domain),
181 #[cfg(feature = "literals")]
182 literal,
183 })
184 }
185
186 #[inline]
200 pub unsafe fn new_unchecked<LocalPart, Domain>(local_part: LocalPart, domain: Domain) -> Self
201 where
202 LocalPart: Into<String>,
203 Domain: Into<String>,
204 {
205 Self::new_unchecked_impl(local_part.into(), domain.into(), false)
206 }
207
208 #[cfg(feature = "literals")]
222 #[inline]
223 pub unsafe fn with_literal_unchecked<LocalPart, Domain>(
224 local_part: LocalPart,
225 domain: Domain,
226 ) -> Self
227 where
228 LocalPart: Into<String>,
229 Domain: Into<String>,
230 {
231 Self::new_unchecked_impl(local_part.into(), domain.into(), true)
232 }
233
234 #[allow(unused_variables)]
235 unsafe fn new_unchecked_impl(local_part: String, domain: String, literal: bool) -> Self {
236 Self {
237 local_part,
238 domain,
239 #[cfg(feature = "literals")]
240 literal,
241 }
242 }
243
244 #[inline]
246 pub fn local_part(&self) -> &str {
247 &self.local_part
248 }
249
250 #[inline]
252 pub fn domain(&self) -> &str {
253 &self.domain
254 }
255
256 #[inline]
258 pub fn is_quoted(&self) -> bool {
259 self.local_part()
260 .split('.')
261 .any(|s| s.is_empty() || s.contains(is_not_atext))
262 }
263
264 #[inline]
266 pub fn is_literal(&self) -> bool {
267 #[cfg(feature = "literals")]
268 return self.literal;
269 #[cfg(not(feature = "literals"))]
270 return false;
271 }
272
273 #[inline]
275 pub fn into_parts(self) -> (String, String) {
276 (self.local_part, self.domain)
277 }
278
279 pub fn into_serialized_parts(self) -> (String, String) {
285 match (self.is_quoted(), self.is_literal()) {
288 (false, false) => (self.local_part, self.domain),
289 (true, false) => (
290 ["\"", "e(self.local_part()), "\""].concat(),
291 self.domain,
292 ),
293 (false, true) => (self.local_part, ["[", &self.domain, "]"].concat()),
294 (true, true) => (
295 ["\"", "e(self.local_part()), "\""].concat(),
296 ["[", &self.domain, "]"].concat(),
297 ),
298 }
299 }
300}
301
302impl fmt::Display for AddrSpec {
303 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
304 if !self.is_quoted() {
305 formatter.write_str(self.local_part())?;
306 } else {
307 formatter.write_char('"')?;
308 for chr in quote(self.local_part()).chars() {
309 formatter.write_char(chr)?;
310 }
311 formatter.write_char('"')?;
312 }
313
314 formatter.write_char('@')?;
315
316 if !self.is_literal() {
319 formatter.write_str(self.domain())?;
320 } else {
321 formatter.write_char('[')?;
322 for chr in self.domain().chars() {
323 formatter.write_char(chr)?;
324 }
325 formatter.write_char(']')?;
326 }
327
328 Ok(())
329 }
330}
331
332impl FromStr for AddrSpec {
333 type Err = ParseError;
334
335 #[inline]
336 fn from_str(address: &str) -> Result<Self, Self::Err> {
337 Parser::new(address).parse()
338 }
339}
340
341#[cfg(feature = "serde")]
342use serde::{Deserialize, Deserializer, Serialize, Serializer};
343
344#[cfg(feature = "serde")]
345impl Serialize for AddrSpec {
346 #[inline]
347 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
348 where
349 S: Serializer,
350 {
351 serializer.serialize_str(self.to_string().as_str())
352 }
353}
354
355#[cfg(feature = "serde")]
356impl<'de> Deserialize<'de> for AddrSpec {
357 #[inline]
358 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
359 where
360 D: Deserializer<'de>,
361 {
362 String::deserialize(deserializer)?
363 .parse()
364 .map_err(serde::de::Error::custom)
365 }
366}
367
368#[cfg(feature = "email_address")]
369use email_address::EmailAddress;
370
371#[cfg(feature = "email_address")]
372impl From<EmailAddress> for AddrSpec {
373 #[inline]
374 fn from(val: EmailAddress) -> Self {
375 AddrSpec::from_str(val.as_str()).unwrap()
376 }
377}
378
379#[cfg(feature = "email_address")]
380impl From<AddrSpec> for EmailAddress {
381 #[inline]
382 fn from(val: AddrSpec) -> Self {
383 EmailAddress::new_unchecked(val.to_string())
384 }
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390
391 #[test]
392 fn test_addr_spec_from_str() {
393 let addr_spec = AddrSpec::from_str("jdoe@machine.example").unwrap();
394 assert_eq!(addr_spec.local_part(), "jdoe");
395 assert_eq!(addr_spec.domain(), "machine.example");
396 assert_eq!(addr_spec.to_string(), "jdoe@machine.example");
397 }
398
399 #[cfg(feature = "white-spaces")]
400 #[test]
401 fn test_addr_spec_from_str_with_white_space_before_local_part() {
402 let addr_spec = AddrSpec::from_str(" jdoe@machine.example").unwrap();
403 assert_eq!(addr_spec.local_part(), "jdoe");
404 assert_eq!(addr_spec.domain(), "machine.example");
405 assert_eq!(addr_spec.to_string(), "jdoe@machine.example");
406 }
407
408 #[cfg(feature = "white-spaces")]
409 #[test]
410 fn test_addr_spec_from_str_with_white_space_before_at() {
411 let addr_spec = AddrSpec::from_str("jdoe @machine.example").unwrap();
412 assert_eq!(addr_spec.local_part(), "jdoe");
413 assert_eq!(addr_spec.domain(), "machine.example");
414 assert_eq!(addr_spec.to_string(), "jdoe@machine.example");
415 }
416
417 #[cfg(feature = "white-spaces")]
418 #[test]
419 fn test_addr_spec_from_str_with_white_space_after_at() {
420 let addr_spec = AddrSpec::from_str("jdoe@ machine.example").unwrap();
421 assert_eq!(addr_spec.local_part(), "jdoe");
422 assert_eq!(addr_spec.domain(), "machine.example");
423 assert_eq!(addr_spec.to_string(), "jdoe@machine.example");
424 }
425
426 #[cfg(feature = "white-spaces")]
427 #[test]
428 fn test_addr_spec_from_str_with_white_space_after_domain() {
429 let addr_spec = AddrSpec::from_str("jdoe@machine.example ").unwrap();
430 assert_eq!(addr_spec.local_part(), "jdoe");
431 assert_eq!(addr_spec.domain(), "machine.example");
432 assert_eq!(addr_spec.to_string(), "jdoe@machine.example");
433 }
434
435 #[cfg(feature = "comments")]
436 #[test]
437 fn test_addr_spec_from_str_with_comments_before_local_part() {
438 let addr_spec = AddrSpec::from_str("(John Doe)jdoe@machine.example").unwrap();
439 assert_eq!(addr_spec.local_part(), "jdoe");
440 assert_eq!(addr_spec.domain(), "machine.example");
441 assert_eq!(addr_spec.to_string(), "jdoe@machine.example");
442 }
443
444 #[cfg(feature = "comments")]
445 #[test]
446 fn test_addr_spec_from_str_with_comments_before_at() {
447 let addr_spec = AddrSpec::from_str("jdoe(John Doe)@machine.example").unwrap();
448 assert_eq!(addr_spec.local_part(), "jdoe");
449 assert_eq!(addr_spec.domain(), "machine.example");
450 assert_eq!(addr_spec.to_string(), "jdoe@machine.example");
451 }
452
453 #[cfg(feature = "comments")]
454 #[test]
455 fn test_addr_spec_from_str_with_comments_after_at() {
456 let addr_spec = AddrSpec::from_str("jdoe@(John Doe)machine.example").unwrap();
457 assert_eq!(addr_spec.local_part(), "jdoe");
458 assert_eq!(addr_spec.domain(), "machine.example");
459 assert_eq!(addr_spec.to_string(), "jdoe@machine.example");
460 }
461
462 #[cfg(feature = "comments")]
463 #[test]
464 fn test_addr_spec_from_str_with_comments_after_domain() {
465 let addr_spec = AddrSpec::from_str("jdoe@machine.example(John Doe)").unwrap();
466 assert_eq!(addr_spec.local_part(), "jdoe");
467 assert_eq!(addr_spec.domain(), "machine.example");
468 assert_eq!(addr_spec.to_string(), "jdoe@machine.example");
469 }
470
471 #[cfg(feature = "comments")]
472 #[test]
473 fn test_addr_spec_from_str_with_nested_comments_before_local_part() {
474 let addr_spec =
475 AddrSpec::from_str("(John Doe (The Adventurer))jdoe@machine.example").unwrap();
476 assert_eq!(addr_spec.local_part(), "jdoe");
477 assert_eq!(addr_spec.domain(), "machine.example");
478 assert_eq!(addr_spec.to_string(), "jdoe@machine.example");
479 }
480
481 #[cfg(feature = "comments")]
482 #[test]
483 fn test_addr_spec_from_str_with_nested_comments_before_at() {
484 let addr_spec =
485 AddrSpec::from_str("jdoe(John Doe (The Adventurer))@machine.example").unwrap();
486 assert_eq!(addr_spec.local_part(), "jdoe");
487 assert_eq!(addr_spec.domain(), "machine.example");
488 assert_eq!(addr_spec.to_string(), "jdoe@machine.example");
489 }
490
491 #[cfg(feature = "comments")]
492 #[test]
493 fn test_addr_spec_from_str_with_nested_comments_after_at() {
494 let addr_spec =
495 AddrSpec::from_str("jdoe@(John Doe (The Adventurer))machine.example").unwrap();
496 assert_eq!(addr_spec.local_part(), "jdoe");
497 assert_eq!(addr_spec.domain(), "machine.example");
498 assert_eq!(addr_spec.to_string(), "jdoe@machine.example");
499 }
500
501 #[cfg(feature = "comments")]
502 #[test]
503 fn test_addr_spec_from_str_with_nested_comments_after_domain() {
504 let addr_spec =
505 AddrSpec::from_str("jdoe@machine.example(John Doe (The Adventurer))").unwrap();
506 assert_eq!(addr_spec.local_part(), "jdoe");
507 assert_eq!(addr_spec.domain(), "machine.example");
508 assert_eq!(addr_spec.to_string(), "jdoe@machine.example");
509 }
510
511 #[test]
512 fn test_addr_spec_from_str_with_empty_labels() {
513 let addr_spec = AddrSpec::from_str("\"..\"@machine.example").unwrap();
514 assert_eq!(addr_spec.local_part(), "..");
515 assert_eq!(addr_spec.domain(), "machine.example");
516 assert_eq!(addr_spec.to_string(), "\"..\"@machine.example");
517 }
518
519 #[test]
520 fn test_addr_spec_from_str_with_quote() {
521 let addr_spec = AddrSpec::from_str("\"jdoe\"@machine.example").unwrap();
522 assert_eq!(addr_spec.local_part(), "jdoe");
523 assert_eq!(addr_spec.domain(), "machine.example");
524 assert_eq!(addr_spec.to_string(), "jdoe@machine.example");
525 }
526
527 #[test]
528 fn test_addr_spec_from_str_with_escape_and_quote() {
529 let addr_spec = AddrSpec::from_str("\"jdoe\\\"\"@machine.example").unwrap();
530 assert_eq!(addr_spec.local_part(), "jdoe\"");
531 assert_eq!(addr_spec.domain(), "machine.example");
532 assert_eq!(addr_spec.to_string(), "\"jdoe\\\"\"@machine.example");
533 }
534
535 #[test]
536 fn test_addr_spec_from_str_with_white_space_escape_and_quote() {
537 let addr_spec = AddrSpec::from_str("\"jdoe\\ \"@machine.example").unwrap();
538 assert_eq!(addr_spec.local_part(), "jdoe ");
539 assert_eq!(addr_spec.domain(), "machine.example");
540 assert_eq!(addr_spec.to_string(), "\"jdoe\\ \"@machine.example");
541 }
542
543 #[cfg(not(feature = "white-spaces"))]
544 #[test]
545 fn test_addr_spec_from_str_with_white_spaces_and_white_space_escape_and_quote() {
546 assert_eq!(
547 AddrSpec::from_str("\"jdoe \\ \"@machine.example").unwrap_err(),
548 ParseError("invalid character in quoted local part", 5)
549 );
550 }
551
552 #[cfg(feature = "white-spaces")]
553 #[test]
554 fn test_addr_spec_from_str_with_white_spaces_and_white_space_escape_and_quote() {
555 let addr_spec = AddrSpec::from_str("\"jdoe \\ \"@machine.example").unwrap();
556 assert_eq!(addr_spec.local_part(), "jdoe ");
557 assert_eq!(addr_spec.domain(), "machine.example");
558 assert_eq!(addr_spec.to_string(), "\"jdoe\\ \"@machine.example");
559 }
560
561 #[cfg(feature = "literals")]
562 #[test]
563 fn test_addr_spec_from_str_with_domain_literal() {
564 let addr_spec = AddrSpec::from_str("jdoe@[machine.example]").unwrap();
565 assert_eq!(addr_spec.local_part(), "jdoe");
566 assert_eq!(addr_spec.domain(), "machine.example");
567 assert_eq!(addr_spec.to_string(), "jdoe@[machine.example]");
568 }
569
570 #[cfg(feature = "literals")]
571 #[test]
572 fn test_addr_spec_from_str_with_escape_and_domain_literal() {
573 let addr_spec = AddrSpec::from_str("\"jdoe\"@[machine.example]").unwrap();
574 assert_eq!(addr_spec.local_part(), "jdoe");
575 assert_eq!(addr_spec.domain(), "machine.example");
576 assert_eq!(addr_spec.to_string(), "jdoe@[machine.example]");
577 }
578
579 #[test]
580 fn test_addr_spec_from_str_with_unicode() {
581 let addr_spec = AddrSpec::from_str("😄😄😄@😄😄😄").unwrap();
582 assert_eq!(addr_spec.local_part(), "😄😄😄");
583 assert_eq!(addr_spec.domain(), "😄😄😄");
584 assert_eq!(addr_spec.to_string(), "😄😄😄@😄😄😄");
585 }
586
587 #[test]
588 fn test_addr_spec_from_str_with_escape_and_unicode() {
589 let addr_spec = AddrSpec::from_str("\"😄😄😄\"@😄😄😄").unwrap();
590 assert_eq!(addr_spec.local_part(), "😄😄😄");
591 assert_eq!(addr_spec.domain(), "😄😄😄");
592 assert_eq!(addr_spec.to_string(), "😄😄😄@😄😄😄");
593 }
594
595 #[test]
596 fn test_addr_spec_from_str_with_escape_and_unicode_and_quote() {
597 let addr_spec = AddrSpec::from_str("\"😄😄😄\\\"\"@😄😄😄").unwrap();
598 assert_eq!(addr_spec.local_part(), "😄😄😄\"");
599 assert_eq!(addr_spec.domain(), "😄😄😄");
600 assert_eq!(addr_spec.to_string(), "\"😄😄😄\\\"\"@😄😄😄");
601 }
602
603 #[test]
604 #[cfg(feature = "literals")]
605 fn test_addr_spec_from_str_with_escape_and_unicode_and_domain_literal() {
606 let addr_spec = AddrSpec::from_str("\"😄😄😄\"@[😄😄😄]").unwrap();
607 assert_eq!(addr_spec.local_part(), "😄😄😄");
608 assert_eq!(addr_spec.domain(), "😄😄😄");
609 assert_eq!(addr_spec.to_string(), "😄😄😄@[😄😄😄]");
610 }
611}
612
613#[cfg(all(test, feature = "nightly"))]
614mod benches {
615 extern crate test;
616
617 use super::*;
618
619 mod addr_spec {
620 use super::*;
621
622 #[bench]
623 fn bench_trivial(b: &mut test::Bencher) {
624 b.iter(|| {
625 let address = AddrSpec::from_str("test@example.com").unwrap();
626 assert_eq!(address.local_part(), "test");
627 assert_eq!(address.domain(), "example.com");
628 assert_eq!(address.to_string().as_str(), "test@example.com");
629 });
630 }
631
632 #[bench]
633 fn bench_quoted_local_part(b: &mut test::Bencher) {
634 b.iter(|| {
635 let address = AddrSpec::from_str("\"test\"@example.com").unwrap();
636 assert_eq!(address.local_part(), "test");
637 assert_eq!(address.domain(), "example.com");
638 assert_eq!(address.to_string().as_str(), "test@example.com");
639 });
640 }
641
642 #[cfg(feature = "literals")]
643 #[bench]
644 fn bench_literal_domain(b: &mut test::Bencher) {
645 b.iter(|| {
646 let address = AddrSpec::from_str("test@[example.com]").unwrap();
647 assert_eq!(address.local_part(), "test");
648 assert_eq!(address.domain(), "example.com");
649 assert_eq!(address.to_string().as_str(), "test@[example.com]");
650 });
651 }
652
653 #[cfg(feature = "literals")]
654 #[bench]
655 fn bench_full(b: &mut test::Bencher) {
656 b.iter(|| {
657 let address = AddrSpec::from_str("\"test\"@[example.com]").unwrap();
658 assert_eq!(address.local_part(), "test");
659 assert_eq!(address.domain(), "example.com");
660 assert_eq!(address.to_string().as_str(), "test@[example.com]");
661 });
662 }
663 }
664
665 #[cfg(feature = "email_address")]
666 mod email_address {
667 use super::*;
668
669 use ::email_address::EmailAddress;
670
671 #[bench]
672 fn bench_trivial(b: &mut test::Bencher) {
673 b.iter(|| {
674 let address = EmailAddress::from_str("test@example.com").unwrap();
675 assert_eq!(address.local_part(), "test");
676 assert_eq!(address.domain(), "example.com");
677 assert_eq!(address.to_string().as_str(), "test@example.com");
678 });
679 }
680
681 #[bench]
682 fn bench_quoted_local_part(b: &mut test::Bencher) {
683 b.iter(|| {
684 let address = EmailAddress::from_str("\"test\"@example.com").unwrap();
685 assert_eq!(address.local_part(), "\"test\"");
686 assert_eq!(address.domain(), "example.com");
687 assert_eq!(address.to_string().as_str(), "\"test\"@example.com");
688 });
689 }
690
691 #[cfg(feature = "literals")]
692 #[bench]
693 fn bench_literal_domain(b: &mut test::Bencher) {
694 b.iter(|| {
695 let address = EmailAddress::from_str("test@[example.com]").unwrap();
696 assert_eq!(address.local_part(), "test");
697 assert_eq!(address.domain(), "[example.com]");
698 assert_eq!(address.to_string().as_str(), "test@[example.com]");
699 });
700 }
701
702 #[cfg(feature = "literals")]
703 #[bench]
704 fn bench_full(b: &mut test::Bencher) {
705 b.iter(|| {
706 let address = EmailAddress::from_str("\"test\"@[example.com]").unwrap();
707 assert_eq!(address.local_part(), "\"test\"");
708 assert_eq!(address.domain(), "[example.com]");
709 assert_eq!(address.to_string().as_str(), "\"test\"@[example.com]");
710 });
711 }
712 }
713
714 #[bench]
718 fn bench_addr_spec_regexp(b: &mut test::Bencher) {
719 use regex::Regex;
720
721 let regex = Regex::new(r#"^(?:"(.*)"|([^@]+))@(?:\[(.*)\]|(.*))$"#).unwrap();
722 b.iter(|| {
723 {
724 let captures = regex.captures("test@example.com").unwrap();
725 assert_eq!(
726 unsafe {
727 AddrSpec::new_unchecked(
728 captures.get(2).unwrap().as_str(),
729 captures.get(4).unwrap().as_str(),
730 )
731 }
732 .to_string()
733 .as_str(),
734 "test@example.com"
735 );
736 }
737 AddrSpec::from_str("test@example.com").unwrap();
738 {
739 let captures = regex.captures("\"test\"@example.com").unwrap();
740 assert_eq!(
741 unsafe {
742 AddrSpec::new_unchecked(
743 captures.get(1).unwrap().as_str(),
744 captures.get(4).unwrap().as_str(),
745 )
746 }
747 .to_string()
748 .as_str(),
749 "test@example.com"
750 );
751 }
752 #[cfg(feature = "literals")]
753 {
754 let captures = regex.captures("test@[example.com]").unwrap();
755 assert_eq!(
756 unsafe {
757 AddrSpec::with_literal_unchecked(
758 captures.get(2).unwrap().as_str(),
759 captures.get(3).unwrap().as_str(),
760 )
761 }
762 .to_string()
763 .as_str(),
764 "test@[example.com]"
765 );
766 }
767 #[cfg(feature = "literals")]
768 {
769 let captures = regex.captures("\"test\"@[example.com]").unwrap();
770 assert_eq!(
771 unsafe {
772 AddrSpec::with_literal_unchecked(
773 captures.get(1).unwrap().as_str(),
774 captures.get(3).unwrap().as_str(),
775 )
776 }
777 .to_string()
778 .as_str(),
779 "test@[example.com]"
780 );
781 }
782 });
783 }
784}