addr_spec/
lib.rs

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/// Address specification as defined in [RFC
21/// 5322](https://tools.ietf.org/html/rfc5322#section-3.4.1) with UTF-8 support
22/// as defined in [RFC 6532](https://tools.ietf.org/html/rfc6532).
23///
24/// Both the local part and the domain are normalized using the
25/// [NFC](https://unicode.org/reports/tr15/#Norm_Forms) as recommended in
26/// [Section 3.1, RFC 6532](https://tools.ietf.org/html/rfc6532#section-3.1).
27/// Address strings built using this crate work well for unique, UTF-8
28/// identifiers.
29///
30/// # Examples
31///
32/// ```
33/// use std::str::FromStr;
34///
35/// use addr_spec::AddrSpec;
36///
37/// let addr_spec = AddrSpec::from_str("test@example.com").unwrap();
38/// assert_eq!(addr_spec.local_part(), "test");
39/// assert_eq!(addr_spec.domain(), "example.com");
40/// assert_eq!(addr_spec.is_literal(), false);
41/// assert_eq!(addr_spec.to_string(), "test@example.com");
42/// ```
43///
44/// Quoted local parts will be unescaped if possible:
45///
46/// ```
47/// use std::str::FromStr;
48///
49/// use addr_spec::AddrSpec;
50///
51/// let addr_spec = AddrSpec::from_str(r#""test"@example.com"#).unwrap();
52/// assert_eq!(addr_spec.local_part(), "test");
53/// assert_eq!(addr_spec.domain(), "example.com");
54/// assert_eq!(addr_spec.is_literal(), false);
55/// assert_eq!(addr_spec.to_string(), "test@example.com");
56/// ```
57///
58/// Literal domains are also supported:
59///
60/// ```
61/// use std::str::FromStr;
62///
63/// use addr_spec::AddrSpec;
64///
65/// #[cfg(feature = "literals")]
66/// {
67///     let addr_spec = AddrSpec::from_str("test@[IPv6:2001:db8::1]").unwrap();
68///     assert_eq!(addr_spec.local_part(), "test");
69///     assert_eq!(addr_spec.domain(), "IPv6:2001:db8::1");
70///     assert_eq!(addr_spec.is_literal(), true);
71///     assert_eq!(addr_spec.to_string(), "test@[IPv6:2001:db8::1]");
72/// }
73/// ```
74///
75/// You can also create an address specification from its parts:
76///
77/// ```
78/// use addr_spec::AddrSpec;
79///
80/// let addr_spec = AddrSpec::new("test", "example.com").unwrap();
81/// assert_eq!(addr_spec.local_part(), "test");
82/// assert_eq!(addr_spec.domain(), "example.com");
83/// assert_eq!(addr_spec.is_literal(), false);
84/// assert_eq!(addr_spec.to_string(), "test@example.com");
85/// ```
86///
87/// If you want to just normalize an address, you can use the `normalize`
88/// function:
89///
90/// ```
91/// use addr_spec::AddrSpec;
92///
93/// assert_eq!(
94///     &AddrSpec::normalize("\"test\"@example.com").unwrap(),
95///     "test@example.com"
96/// );
97/// ```
98///
99/// # References
100///
101/// - [RFC 5322](https://tools.ietf.org/html/rfc5322#section-3.4.1)
102/// - [RFC 6531](https://tools.ietf.org/html/rfc6531)
103/// - [RFC 6532](https://tools.ietf.org/html/rfc6532)
104#[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    /// Normalizes the address.
114    ///
115    /// This is a convenience function that parses the address and then
116    /// serializes it again.
117    ///
118    /// It is equivalent to `address.parse::<AddrSpec>()?.to_string()`.
119    ///
120    /// # Examples
121    ///
122    /// ```
123    /// use addr_spec::AddrSpec;
124    ///
125    /// assert_eq!(
126    ///     &AddrSpec::normalize("\"test\"@example.com").unwrap(),
127    ///     "test@example.com"
128    /// );
129    /// ```
130    #[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    /// Creates a new address specification. This will validate the local part
139    /// and domain and perform NFC-normalization.
140    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    /// Creates a new address specification with a literal domain. This will
149    /// validate the local part and domain and perform NFC-normalization.
150    #[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            // We use the parser here since parsing dot atoms is a pure
173            // operation (i.e. independent of any features).
174            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    /// Creates a new address specification without performing any validation or
187    /// normalization.
188    ///
189    /// # Safety
190    ///
191    /// This function is unsafe because it does not validate nor normalize the
192    /// local part or domain. If the local part or domain contains invalid
193    /// characters or is not NFC-normalized, the resulting address specification
194    /// will be invalid.
195    ///
196    /// Only use this function if you are sure that the local part and domain
197    /// are valid and NFC-normalized. This is typically the case if you are
198    /// getting them from a trusted source.
199    #[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    /// Creates a new address specification with a domain literal without
209    /// performing any validation or normalization.
210    ///
211    /// # Safety
212    ///
213    /// This function is unsafe because it does not validate nor normalize the
214    /// local part or domain. If the local part or domain contains invalid
215    /// characters or is not NFC-normalized, the resulting address specification
216    /// will be invalid.
217    ///
218    /// Only use this function if you are sure that the local part and domain
219    /// are valid and NFC-normalized. This is typically the case if you are
220    /// getting them from a trusted source.
221    #[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    /// Returns the local part of the address.
245    #[inline]
246    pub fn local_part(&self) -> &str {
247        &self.local_part
248    }
249
250    /// Returns the domain of the address.
251    #[inline]
252    pub fn domain(&self) -> &str {
253        &self.domain
254    }
255
256    /// Returns whether the local part is quoted.
257    #[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    /// Returns whether the domain is literal.
265    #[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    /// Returns the local part and domain of the address.
274    #[inline]
275    pub fn into_parts(self) -> (String, String) {
276        (self.local_part, self.domain)
277    }
278
279    /// Returns serialized versions of the local part and domain of the address.
280    ///
281    /// This is useful if you need to transport the address specification over
282    /// line-based protocols such as SMTP and need to ensure that the local part
283    /// and domain fit on a single line or require folding white-spaces.
284    pub fn into_serialized_parts(self) -> (String, String) {
285        // Note literals will be optimized away by the compiler if the feature
286        // is disabled.
287        match (self.is_quoted(), self.is_literal()) {
288            (false, false) => (self.local_part, self.domain),
289            (true, false) => (
290                ["\"", &quote(self.local_part()), "\""].concat(),
291                self.domain,
292            ),
293            (false, true) => (self.local_part, ["[", &self.domain, "]"].concat()),
294            (true, true) => (
295                ["\"", &quote(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        // Note literals will be optimized away by the compiler if the feature
317        // is disabled.
318        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    // Sanity check that the regex is actually slower than the hand-written
715    // parser. The regex below is a fairly simple one, but should still slower
716    // than the hand-written parser.
717    #[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}