Skip to main content

addr_spec/
lib.rs

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