Skip to main content

email_message/
address.rs

1use std::fmt::Display;
2use std::str::FromStr;
3use std::sync::OnceLock;
4
5use crate::email::{EmailAddress, EmailAddressParseError};
6
7static ADDRESS_PARSER: OnceLock<mail_parser::MessageParser> = OnceLock::new();
8
9/// A mailbox address with optional display name.
10#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
11#[derive(Clone, Debug, PartialEq, Eq, Hash)]
12pub struct Mailbox {
13    name: Option<String>,
14    email: EmailAddress,
15}
16
17impl Mailbox {
18    /// Returns the optional display name.
19    #[must_use]
20    pub fn name(&self) -> Option<&str> {
21        self.name.as_deref()
22    }
23
24    /// Returns the mailbox email address.
25    #[must_use]
26    pub const fn email(&self) -> &EmailAddress {
27        &self.email
28    }
29}
30
31impl From<EmailAddress> for Mailbox {
32    fn from(email: EmailAddress) -> Self {
33        Self { name: None, email }
34    }
35}
36
37impl From<(String, EmailAddress)> for Mailbox {
38    fn from((name, email): (String, EmailAddress)) -> Self {
39        Self {
40            name: Some(name),
41            email,
42        }
43    }
44}
45
46impl From<(Option<String>, EmailAddress)> for Mailbox {
47    fn from((name, email): (Option<String>, EmailAddress)) -> Self {
48        Self { name, email }
49    }
50}
51
52#[derive(Debug, thiserror::Error)]
53#[non_exhaustive]
54pub enum MailboxParseError {
55    #[error("expected a single mailbox, found {found} address item(s)")]
56    ExpectedSingleMailbox { found: usize },
57    #[error("expected mailbox but found group")]
58    UnexpectedAddressKind,
59    #[error("mailbox list contains group entries")]
60    ContainsGroupEntry,
61    #[error("mailbox parse backend failed")]
62    Backend {
63        #[source]
64        source: AddressBackendError,
65    },
66}
67
68impl FromStr for Mailbox {
69    type Err = MailboxParseError;
70
71    fn from_str(s: &str) -> Result<Self, Self::Err> {
72        if let Ok(email) = EmailAddress::from_str(s) {
73            return Ok(Self::from(email));
74        }
75
76        let addresses =
77            parse_address_items(s).map_err(|source| MailboxParseError::Backend { source })?;
78        if addresses.len() != 1 {
79            return Err(MailboxParseError::ExpectedSingleMailbox {
80                found: addresses.len(),
81            });
82        }
83
84        match addresses.into_iter().next() {
85            Some(Address::Mailbox(mailbox)) => Ok(mailbox),
86            _ => Err(MailboxParseError::UnexpectedAddressKind),
87        }
88    }
89}
90
91impl TryFrom<&str> for Mailbox {
92    type Error = MailboxParseError;
93
94    /// Parses a single mailbox from a string slice.
95    ///
96    /// ```rust
97    /// use email_message::Mailbox;
98    ///
99    /// let mailbox = Mailbox::try_from("Mary Smith <mary@x.test>").unwrap();
100    /// assert_eq!(mailbox.name(), Some("Mary Smith"));
101    /// assert_eq!(mailbox.email().as_str(), "mary@x.test");
102    /// ```
103    fn try_from(value: &str) -> Result<Self, Self::Error> {
104        Self::from_str(value)
105    }
106}
107
108/// Renders the mailbox as `"display name" <email>` or just `email` if no
109/// display name is set.
110///
111/// The output is **UTF-8-direct**: a non-ASCII display name is emitted
112/// verbatim (e.g. `"José" <jose@example.com>`). This is the right shape
113/// for HTTP-API consumers (Postmark, Resend, Mailgun, Loops) which
114/// JSON-encode UTF-8 strings natively.
115///
116/// **Do not use the result directly as an RFC 5322 header value.** SMTP
117/// headers are 7-bit and require RFC 2047 encoded-word wrapping for
118/// non-ASCII display names; the wire renderer
119/// (`email_message_wire::render_rfc822`) applies that encoding
120/// separately. Routing `Mailbox::to_string()` straight into a `From:`
121/// or `To:` header would emit a malformed RFC 5322 line.
122impl Display for Mailbox {
123    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124        match self.name() {
125            Some(name) => {
126                write_quoted(name, f)?;
127                f.write_str(" <")?;
128                self.email.fmt(f)?;
129                f.write_str(">")
130            }
131            None => self.email.fmt(f),
132        }
133    }
134}
135
136#[cfg(feature = "serde")]
137impl serde::Serialize for Mailbox {
138    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
139    where
140        S: serde::Serializer,
141    {
142        serializer.serialize_str(&self.to_string())
143    }
144}
145
146#[cfg(feature = "serde")]
147impl<'de> serde::Deserialize<'de> for Mailbox {
148    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
149    where
150        D: serde::Deserializer<'de>,
151    {
152        let value = String::deserialize(deserializer)?;
153        value.parse().map_err(serde::de::Error::custom)
154    }
155}
156
157#[cfg(feature = "arbitrary")]
158impl<'a> arbitrary::Arbitrary<'a> for Mailbox {
159    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
160        let email = EmailAddress::arbitrary(u)?;
161        if bool::arbitrary(u)? {
162            let name = format!("User {}", u8::arbitrary(u)?);
163            Ok(Self {
164                name: Some(name),
165                email,
166            })
167        } else {
168            Ok(Self { name: None, email })
169        }
170    }
171}
172
173/// A named address group containing mailbox members.
174#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
175#[derive(Clone, Debug, PartialEq, Eq, Hash)]
176pub struct Group {
177    name: String,
178    members: Vec<Mailbox>,
179}
180
181impl Group {
182    /// Returns the group display name.
183    #[must_use]
184    pub fn name(&self) -> &str {
185        self.name.as_str()
186    }
187
188    /// Returns group members.
189    #[must_use]
190    pub fn members(&self) -> &[Mailbox] {
191        self.members.as_slice()
192    }
193}
194
195#[derive(Debug, thiserror::Error)]
196#[non_exhaustive]
197pub enum GroupParseError {
198    #[error("expected a single group, found {found} address item(s)")]
199    ExpectedSingleGroup { found: usize },
200    #[error("expected group but found mailbox")]
201    UnexpectedAddressKind,
202    #[error("group parse backend failed")]
203    Backend {
204        #[source]
205        source: AddressBackendError,
206    },
207}
208
209impl FromStr for Group {
210    type Err = GroupParseError;
211
212    fn from_str(s: &str) -> Result<Self, Self::Err> {
213        let addresses =
214            parse_address_items(s).map_err(|source| GroupParseError::Backend { source })?;
215        if addresses.len() != 1 {
216            return Err(GroupParseError::ExpectedSingleGroup {
217                found: addresses.len(),
218            });
219        }
220
221        match addresses.into_iter().next() {
222            Some(Address::Group(group)) => Ok(group),
223            _ => Err(GroupParseError::UnexpectedAddressKind),
224        }
225    }
226}
227
228impl TryFrom<&str> for Group {
229    type Error = GroupParseError;
230
231    /// Parses a single group from a string slice.
232    ///
233    /// ```rust
234    /// use email_message::Group;
235    ///
236    /// let group = Group::try_from("Undisclosed recipients:;").unwrap();
237    /// assert_eq!(group.name(), "Undisclosed recipients");
238    /// assert!(group.members().is_empty());
239    /// ```
240    fn try_from(value: &str) -> Result<Self, Self::Error> {
241        Self::from_str(value)
242    }
243}
244
245/// Renders the group as `"display name": member1, member2, ...;`.
246///
247/// Same UTF-8-direct caveat as [`Display for Mailbox`]: suitable for
248/// HTTP-API consumers, not directly safe as an RFC 5322 header value.
249impl Display for Group {
250    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251        write_quoted(self.name(), f)?;
252        f.write_str(":")?;
253        for (idx, member) in self.members().iter().enumerate() {
254            if idx > 0 {
255                f.write_str(", ")?;
256            }
257            member.fmt(f)?;
258        }
259        f.write_str(";")
260    }
261}
262
263#[cfg(feature = "serde")]
264impl serde::Serialize for Group {
265    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
266    where
267        S: serde::Serializer,
268    {
269        serializer.serialize_str(&self.to_string())
270    }
271}
272
273#[cfg(feature = "serde")]
274impl<'de> serde::Deserialize<'de> for Group {
275    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
276    where
277        D: serde::Deserializer<'de>,
278    {
279        let value = String::deserialize(deserializer)?;
280        value.parse().map_err(serde::de::Error::custom)
281    }
282}
283
284#[cfg(feature = "arbitrary")]
285impl<'a> arbitrary::Arbitrary<'a> for Group {
286    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
287        let member_count = usize::from(u.int_in_range::<u8>(0..=3)?);
288        let mut members = Vec::with_capacity(member_count);
289        for _ in 0..member_count {
290            members.push(Mailbox::arbitrary(u)?);
291        }
292        Ok(Self {
293            name: format!("Group {}", u8::arbitrary(u)?),
294            members,
295        })
296    }
297}
298
299/// A single address item: either a mailbox or a group.
300///
301/// Deliberately *not* `#[non_exhaustive]`. RFC 5322 §3.4 closes the
302/// address grammar to exactly `mailbox / group`; the kernel cannot
303/// honestly add a third variant without an RFC update. The
304/// derive-required exhaustive `match` lets downstream callers branch
305/// on every variant without an `_ =>` arm, useful when an extension
306/// crate wants type-safe coverage of the address space.
307#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
308#[derive(Clone, Debug, PartialEq, Eq, Hash)]
309pub enum Address {
310    Mailbox(Mailbox),
311    Group(Group),
312}
313
314impl From<Mailbox> for Address {
315    fn from(value: Mailbox) -> Self {
316        Self::Mailbox(value)
317    }
318}
319
320impl From<Group> for Address {
321    fn from(value: Group) -> Self {
322        Self::Group(value)
323    }
324}
325
326impl Address {
327    /// Returns the mailbox entries represented by this address item.
328    pub fn mailboxes(&self) -> impl Iterator<Item = &Mailbox> {
329        match self {
330            Self::Mailbox(mailbox) => std::slice::from_ref(mailbox).iter(),
331            Self::Group(group) => group.members().iter(),
332        }
333    }
334}
335
336#[derive(Debug, thiserror::Error)]
337#[non_exhaustive]
338pub enum AddressParseError {
339    #[error("expected a single address, found {found} address item(s)")]
340    ExpectedSingleAddress { found: usize },
341    #[error("address parse backend failed")]
342    Backend {
343        #[source]
344        source: AddressBackendError,
345    },
346}
347
348impl FromStr for Address {
349    type Err = AddressParseError;
350
351    fn from_str(s: &str) -> Result<Self, Self::Err> {
352        if let Ok(email) = EmailAddress::from_str(s) {
353            return Ok(Self::Mailbox(Mailbox::from(email)));
354        }
355
356        let addresses =
357            parse_address_items(s).map_err(|source| AddressParseError::Backend { source })?;
358        if addresses.len() != 1 {
359            return Err(AddressParseError::ExpectedSingleAddress {
360                found: addresses.len(),
361            });
362        }
363
364        addresses
365            .into_iter()
366            .next()
367            .ok_or(AddressParseError::ExpectedSingleAddress { found: 0 })
368    }
369}
370
371impl TryFrom<&str> for Address {
372    type Error = AddressParseError;
373
374    /// Parses a single address (mailbox or group) from a string slice.
375    ///
376    /// ```rust
377    /// use email_message::Address;
378    ///
379    /// let address = Address::try_from("jdoe@one.test").unwrap();
380    /// assert_eq!(address.to_string(), "jdoe@one.test");
381    /// ```
382    fn try_from(value: &str) -> Result<Self, Self::Error> {
383        Self::from_str(value)
384    }
385}
386
387/// Forwards to the underlying [`Display for Mailbox`] or
388/// [`Display for Group`]; same UTF-8-direct caveat applies.
389impl Display for Address {
390    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
391        match self {
392            Self::Mailbox(mailbox) => mailbox.fmt(f),
393            Self::Group(group) => group.fmt(f),
394        }
395    }
396}
397
398#[cfg(feature = "serde")]
399impl serde::Serialize for Address {
400    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
401    where
402        S: serde::Serializer,
403    {
404        serializer.serialize_str(&self.to_string())
405    }
406}
407
408#[cfg(feature = "serde")]
409impl<'de> serde::Deserialize<'de> for Address {
410    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
411    where
412        D: serde::Deserializer<'de>,
413    {
414        let value = String::deserialize(deserializer)?;
415        value.parse().map_err(serde::de::Error::custom)
416    }
417}
418
419#[cfg(feature = "arbitrary")]
420impl<'a> arbitrary::Arbitrary<'a> for Address {
421    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
422        if bool::arbitrary(u)? {
423            Ok(Self::Mailbox(Mailbox::arbitrary(u)?))
424        } else {
425            Ok(Self::Group(Group::arbitrary(u)?))
426        }
427    }
428}
429
430macro_rules! impl_address_collection {
431    ($(#[$meta:meta])* $name:ident, $item:ty, $error:ty, $parse_fn:expr) => {
432        $(#[$meta])*
433        #[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
434        #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
435        pub struct $name {
436            items: Vec<$item>,
437        }
438
439        impl $name {
440            #[must_use]
441            pub fn len(&self) -> usize {
442                self.items.len()
443            }
444
445            #[must_use]
446            pub fn is_empty(&self) -> bool {
447                self.items.is_empty()
448            }
449
450            pub fn iter(&self) -> std::slice::Iter<'_, $item> {
451                self.items.iter()
452            }
453
454            pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, $item> {
455                self.items.iter_mut()
456            }
457
458            #[must_use]
459            pub fn as_slice(&self) -> &[$item] {
460                self.items.as_slice()
461            }
462
463            #[must_use]
464            pub fn into_vec(self) -> Vec<$item> {
465                self.items
466            }
467        }
468
469        impl From<Vec<$item>> for $name {
470            fn from(items: Vec<$item>) -> Self {
471                Self { items }
472            }
473        }
474
475        impl From<$name> for Vec<$item> {
476            fn from(value: $name) -> Self {
477                value.items
478            }
479        }
480
481        impl FromStr for $name {
482            type Err = $error;
483
484            fn from_str(s: &str) -> Result<Self, Self::Err> {
485                let items = ($parse_fn)(s)?;
486                Ok(Self { items })
487            }
488        }
489
490        impl TryFrom<&str> for $name {
491            type Error = $error;
492
493            /// Parses a list from a string slice.
494            fn try_from(value: &str) -> Result<Self, Self::Error> {
495                Self::from_str(value)
496            }
497        }
498
499        impl IntoIterator for $name {
500            type Item = $item;
501            type IntoIter = std::vec::IntoIter<$item>;
502
503            fn into_iter(self) -> Self::IntoIter {
504                self.items.into_iter()
505            }
506        }
507
508        impl<'a> IntoIterator for &'a $name {
509            type Item = &'a $item;
510            type IntoIter = std::slice::Iter<'a, $item>;
511
512            fn into_iter(self) -> Self::IntoIter {
513                self.items.iter()
514            }
515        }
516
517        impl<'a> IntoIterator for &'a mut $name {
518            type Item = &'a mut $item;
519            type IntoIter = std::slice::IterMut<'a, $item>;
520
521            fn into_iter(self) -> Self::IntoIter {
522                self.items.iter_mut()
523            }
524        }
525
526        impl AsRef<[$item]> for $name {
527            fn as_ref(&self) -> &[$item] {
528                self.items.as_slice()
529            }
530        }
531
532        impl std::iter::FromIterator<$item> for $name {
533            fn from_iter<T: IntoIterator<Item = $item>>(iter: T) -> Self {
534                Self {
535                    items: iter.into_iter().collect(),
536                }
537            }
538        }
539
540        impl Extend<$item> for $name {
541            fn extend<T: IntoIterator<Item = $item>>(&mut self, iter: T) {
542                self.items.extend(iter);
543            }
544        }
545
546        impl Display for $name {
547            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
548                for (idx, item) in self.items.iter().enumerate() {
549                    if idx > 0 {
550                        f.write_str(", ")?;
551                    }
552                    item.fmt(f)?;
553                }
554                Ok(())
555            }
556        }
557
558        #[cfg(feature = "serde")]
559        impl serde::Serialize for $name {
560            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
561            where
562                S: serde::Serializer,
563            {
564                serializer.serialize_str(&self.to_string())
565            }
566        }
567
568        #[cfg(feature = "serde")]
569        impl<'de> serde::Deserialize<'de> for $name {
570            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
571            where
572                D: serde::Deserializer<'de>,
573            {
574                let value = String::deserialize(deserializer)?;
575                value.parse().map_err(serde::de::Error::custom)
576            }
577        }
578
579        #[cfg(feature = "arbitrary")]
580        impl<'a> arbitrary::Arbitrary<'a> for $name {
581            fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
582                let len = usize::from(u.int_in_range::<u8>(0..=4)?);
583                let mut items = Vec::with_capacity(len);
584                for _ in 0..len {
585                    items.push(<$item>::arbitrary(u)?);
586                }
587                Ok(Self { items })
588            }
589        }
590    };
591}
592
593impl_address_collection!(
594    /// A parsed list of address items.
595    ///
596    /// This is used instead of `Vec<Address>` for `FromStr`, because Rust's orphan
597    /// rules do not allow implementing foreign traits for foreign types.
598    AddressList,
599    Address,
600    AddressParseError,
601    |s| parse_address_items(s).map_err(|source| AddressParseError::Backend { source })
602);
603
604impl<'a> TryFrom<Vec<&'a str>> for AddressList {
605    type Error = AddressParseError;
606
607    fn try_from(value: Vec<&'a str>) -> Result<Self, Self::Error> {
608        value
609            .into_iter()
610            .map(Address::from_str)
611            .collect::<Result<Vec<_>, _>>()
612            .map(Self::from)
613    }
614}
615
616impl<'a> TryFrom<&'a [&'a str]> for AddressList {
617    type Error = AddressParseError;
618
619    fn try_from(value: &'a [&'a str]) -> Result<Self, Self::Error> {
620        value
621            .iter()
622            .copied()
623            .map(Address::from_str)
624            .collect::<Result<Vec<_>, _>>()
625            .map(Self::from)
626    }
627}
628
629impl<'a> TryFrom<Vec<&'a str>> for MailboxList {
630    type Error = MailboxParseError;
631
632    fn try_from(value: Vec<&'a str>) -> Result<Self, Self::Error> {
633        value
634            .into_iter()
635            .map(Mailbox::from_str)
636            .collect::<Result<Vec<_>, _>>()
637            .map(Self::from)
638    }
639}
640
641impl<'a> TryFrom<&'a [&'a str]> for MailboxList {
642    type Error = MailboxParseError;
643
644    fn try_from(value: &'a [&'a str]) -> Result<Self, Self::Error> {
645        value
646            .iter()
647            .copied()
648            .map(Mailbox::from_str)
649            .collect::<Result<Vec<_>, _>>()
650            .map(Self::from)
651    }
652}
653
654impl_address_collection!(
655    /// A parsed list of mailbox items.
656    ///
657    /// Group entries are rejected when parsing into `MailboxList`.
658    MailboxList,
659    Mailbox,
660    MailboxParseError,
661    |s| {
662        let addresses = parse_address_items(s).map_err(|source| MailboxParseError::Backend { source })?;
663        let mut items = Vec::with_capacity(addresses.len());
664
665        for address in addresses {
666            match address {
667                Address::Mailbox(mailbox) => items.push(mailbox),
668                Address::Group(_) => return Err(MailboxParseError::ContainsGroupEntry),
669            }
670        }
671
672        Ok(items)
673    }
674);
675
676/// Maximum byte length accepted by the address-list parser before
677/// rejecting outright. 64 KiB is far above any realistic header value
678///, RFC 5322 caps physical lines at 998 bytes; even a header folded
679/// across hundreds of continuation lines stays well under this. The
680/// cap exists to prevent the `format!("To: {input}\r\n\r\n")`
681/// allocation amplification on adversarial multi-megabyte input.
682pub const MAX_ADDRESS_INPUT_BYTES: usize = 64 * 1024;
683
684#[derive(Debug, thiserror::Error)]
685#[non_exhaustive]
686pub enum AddressBackendError {
687    #[error("address input contains raw newline characters")]
688    InputContainsRawNewlines,
689    #[error("address input is {len} bytes, exceeding maximum of {max}")]
690    #[non_exhaustive]
691    InputTooLong { len: usize, max: usize },
692    #[error("failed to parse address header")]
693    HeaderParse,
694    #[error("parsed header did not contain address data")]
695    MissingAddress,
696    #[error("mailbox is missing addr-spec")]
697    MissingAddrSpec,
698    #[error("invalid addr-spec `{input}`")]
699    InvalidAddrSpec {
700        input: String,
701        #[source]
702        source: EmailAddressParseError,
703    },
704    #[error("group member at index {index} is missing addr-spec")]
705    GroupMemberMissingAddrSpec { index: usize },
706    #[error("invalid group member addr-spec `{input}` at index {index}")]
707    InvalidGroupMemberAddrSpec {
708        index: usize,
709        input: String,
710        #[source]
711        source: EmailAddressParseError,
712    },
713    #[error("group is missing a name")]
714    GroupMissingName,
715}
716
717fn write_quoted(value: &str, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
718    f.write_str("\"")?;
719    for ch in value.chars() {
720        if ch == '\\' || ch == '"' {
721            f.write_str("\\")?;
722        }
723        f.write_str(ch.encode_utf8(&mut [0; 4]))?;
724    }
725    f.write_str("\"")
726}
727
728/// Parse-side address-list extractor.
729///
730/// # Byte discipline at the parser
731///
732/// This parser deliberately accepts more than the message-level gate
733/// rejects. Specifically: it rejects raw CR / LF (which would let an
734/// attacker inject a new header line at the parser layer) and
735/// inputs over [`MAX_ADDRESS_INPUT_BYTES`]; it does **not** reject
736/// NUL or other non-tab ASCII control characters in display-name
737/// content.
738///
739/// The asymmetry is intentional. The kernel's stricter byte-
740/// discipline lives at [`crate::Message::validate_basic`] and fires
741/// when an outbound `OutboundMessage` is built; inbound parsing is
742/// best-effort and used in forensic / archival / replay workflows
743/// where rejecting BEL / VT / ESC in display names from real-world
744/// malformed-but-recoverable mail loses information. A `Mailbox`
745/// carrying questionable bytes is fine *as a parsed value*; it
746/// cannot reach an outbound wire renderer because the message-level
747/// gate catches it first.
748///
749/// Callers handing a `Mailbox.name()` directly to a logging sink or
750/// non-validated downstream consumer are responsible for their own
751/// byte-discipline check.
752fn parse_address_items(input: &str) -> Result<Vec<Address>, AddressBackendError> {
753    if input.len() > MAX_ADDRESS_INPUT_BYTES {
754        return Err(AddressBackendError::InputTooLong {
755            len: input.len(),
756            max: MAX_ADDRESS_INPUT_BYTES,
757        });
758    }
759    if input.contains('\r') || input.contains('\n') {
760        return Err(AddressBackendError::InputContainsRawNewlines);
761    }
762
763    let raw = format!("To: {input}\r\n\r\n");
764    let parser =
765        ADDRESS_PARSER.get_or_init(|| mail_parser::MessageParser::new().with_address_headers());
766    let message = parser
767        .parse_headers(raw.as_bytes())
768        .ok_or(AddressBackendError::HeaderParse)?;
769    let parsed = message.to().ok_or(AddressBackendError::MissingAddress)?;
770
771    match parsed {
772        mail_parser::Address::List(list) => list
773            .iter()
774            .map(convert_mailbox)
775            .map(|result| result.map(Address::Mailbox))
776            .collect(),
777        // mail_parser switches the whole header to the `Group` shape as soon
778        // as any group syntax appears, and wraps flat mailboxes that appear
779        // before/between/after named groups into a synthetic
780        // `Group { name: None, ... }`. Flatten those back to `Mailbox`
781        // entries so a mixed header like
782        // `alice@example.com, Team: bob@team.com;, dave@example.com`
783        // produces three items in order rather than a parse error.
784        mail_parser::Address::Group(groups) => {
785            let mut items = Vec::with_capacity(groups.len());
786            for group in groups {
787                if group.name.is_some() {
788                    items.push(Address::Group(convert_group(group)?));
789                } else {
790                    for addr in group.addresses.iter() {
791                        items.push(Address::Mailbox(convert_mailbox(addr)?));
792                    }
793                }
794            }
795            Ok(items)
796        }
797    }
798}
799
800fn convert_mailbox(value: &mail_parser::Addr<'_>) -> Result<Mailbox, AddressBackendError> {
801    let raw_email = value
802        .address()
803        .ok_or(AddressBackendError::MissingAddrSpec)?;
804    let email = EmailAddress::from_str(raw_email).map_err(|source| {
805        AddressBackendError::InvalidAddrSpec {
806            input: raw_email.to_owned(),
807            source,
808        }
809    })?;
810
811    Ok(match value.name() {
812        Some(name) => Mailbox::from((name.to_owned(), email)),
813        None => Mailbox::from(email),
814    })
815}
816
817fn convert_group(value: &mail_parser::Group<'_>) -> Result<Group, AddressBackendError> {
818    let name = value
819        .name
820        .as_deref()
821        .ok_or(AddressBackendError::GroupMissingName)?
822        .to_owned();
823    let mut members = Vec::with_capacity(value.addresses.len());
824    for (index, member) in value.addresses.iter().enumerate() {
825        let mailbox = convert_mailbox(member).map_err(|error| match error {
826            AddressBackendError::MissingAddrSpec => {
827                AddressBackendError::GroupMemberMissingAddrSpec { index }
828            }
829            AddressBackendError::InvalidAddrSpec { input, source } => {
830                AddressBackendError::InvalidGroupMemberAddrSpec {
831                    index,
832                    input,
833                    source,
834                }
835            }
836            other => other,
837        })?;
838        members.push(mailbox);
839    }
840
841    Ok(Group { name, members })
842}
843
844#[cfg(test)]
845mod tests {
846    use super::*;
847
848    #[test]
849    fn mailbox_from_str_accepts_rfc_examples() {
850        let parsed = "Mary Smith <mary@x.test>".parse::<Mailbox>();
851        assert!(parsed.is_ok(), "expected valid mailbox");
852
853        let parsed = "jdoe@one.test".parse::<Mailbox>();
854        assert!(parsed.is_ok(), "expected valid mailbox");
855    }
856
857    #[test]
858    fn mailbox_from_str_rejects_group() {
859        let parsed = "Undisclosed recipients:;".parse::<Mailbox>();
860        assert!(matches!(
861            parsed,
862            Err(MailboxParseError::UnexpectedAddressKind)
863        ));
864    }
865
866    #[test]
867    fn group_from_str_accepts_rfc_examples() {
868        let parsed =
869            "A Group:Ed Jones <c@a.test>,joe@where.test,John <jdoe@one.test>;".parse::<Group>();
870        assert!(parsed.is_ok(), "expected valid group");
871
872        let parsed = "Undisclosed recipients:;".parse::<Group>();
873        assert!(parsed.is_ok(), "expected valid group");
874    }
875
876    #[test]
877    fn address_list_roundtrip() {
878        let list = "Mary Smith <mary@x.test>, jdoe@one.test"
879            .parse::<AddressList>()
880            .expect("address list should parse");
881        let rendered = list.to_string();
882        let reparsed = rendered
883            .parse::<AddressList>()
884            .expect("rendered address list should parse");
885        assert_eq!(reparsed.as_slice(), list.as_slice());
886    }
887
888    #[test]
889    fn mailbox_from_str_rejects_input_with_raw_newline() {
890        let parsed = "Mary Smith <mary@x.test>\nBcc: victim@example.com".parse::<Mailbox>();
891        assert!(matches!(
892            parsed,
893            Err(MailboxParseError::Backend {
894                source: AddressBackendError::InputContainsRawNewlines,
895            })
896        ));
897    }
898}