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