Skip to main content

email_message/
address.rs

1//! RFC 5322 mailbox, group, address, and address-list values.
2//!
3//! Parsing accepts either a single address item or a comma-separated list,
4//! depending on the target type. Rendering produces UTF-8-direct display names;
5//! use `email-message-wire` when RFC 2047 header encoding is required.
6
7use std::fmt::Display;
8use std::str::FromStr;
9use std::sync::OnceLock;
10
11use crate::email::{EmailAddress, EmailAddressParseError};
12
13static ADDRESS_PARSER: OnceLock<mail_parser::MessageParser> = OnceLock::new();
14
15/// A mailbox address with optional display name.
16#[derive(Clone, Debug, PartialEq, Eq, Hash)]
17pub struct Mailbox {
18    name: Option<String>,
19    email: EmailAddress,
20}
21
22impl Mailbox {
23    /// Returns the optional display name.
24    #[must_use]
25    pub fn name(&self) -> Option<&str> {
26        self.name.as_deref()
27    }
28
29    /// Returns the mailbox email address.
30    #[must_use]
31    pub const fn email(&self) -> &EmailAddress {
32        &self.email
33    }
34}
35
36impl From<EmailAddress> for Mailbox {
37    fn from(email: EmailAddress) -> Self {
38        Self { name: None, email }
39    }
40}
41
42impl From<(String, EmailAddress)> for Mailbox {
43    fn from((name, email): (String, EmailAddress)) -> Self {
44        Self {
45            name: Some(name),
46            email,
47        }
48    }
49}
50
51impl From<(Option<String>, EmailAddress)> for Mailbox {
52    fn from((name, email): (Option<String>, EmailAddress)) -> Self {
53        Self { name, email }
54    }
55}
56
57/// Errors returned when parsing a [`Mailbox`] or [`MailboxList`].
58#[derive(Debug, thiserror::Error)]
59#[non_exhaustive]
60pub enum MailboxParseError {
61    /// The input contained zero or multiple address items.
62    #[error("expected a single mailbox, found {found} address item(s)")]
63    ExpectedSingleMailbox {
64        /// Number of address items found.
65        found: usize,
66    },
67    /// The single address item was a group rather than a mailbox.
68    #[error("expected mailbox but found group")]
69    UnexpectedAddressKind,
70    /// A mailbox list contained at least one group.
71    #[error("mailbox list contains group entries")]
72    ContainsGroupEntry,
73    /// The address parsing backend failed.
74    #[error("mailbox parse backend failed")]
75    Backend {
76        /// The underlying backend error.
77        #[source]
78        source: AddressBackendError,
79    },
80}
81
82impl FromStr for Mailbox {
83    type Err = MailboxParseError;
84
85    fn from_str(s: &str) -> Result<Self, Self::Err> {
86        if let Ok(email) = EmailAddress::from_str(s) {
87            return Ok(Self::from(email));
88        }
89
90        let addresses =
91            parse_address_items(s).map_err(|source| MailboxParseError::Backend { source })?;
92        if addresses.len() != 1 {
93            return Err(MailboxParseError::ExpectedSingleMailbox {
94                found: addresses.len(),
95            });
96        }
97
98        match addresses.into_iter().next() {
99            Some(Address::Mailbox(mailbox)) => Ok(mailbox),
100            _ => Err(MailboxParseError::UnexpectedAddressKind),
101        }
102    }
103}
104
105impl TryFrom<&str> for Mailbox {
106    type Error = MailboxParseError;
107
108    /// Parses a single mailbox from a string slice.
109    ///
110    /// ```rust
111    /// use email_message::Mailbox;
112    ///
113    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
114    /// let mailbox = Mailbox::try_from("Mary Smith <mary@x.test>")?;
115    /// assert_eq!(mailbox.name(), Some("Mary Smith"));
116    /// assert_eq!(mailbox.email().as_str(), "mary@x.test");
117    /// # Ok(())
118    /// # }
119    /// ```
120    ///
121    /// # Errors
122    ///
123    /// Returns [`MailboxParseError`] unless the input contains exactly one
124    /// mailbox and no group.
125    fn try_from(value: &str) -> Result<Self, Self::Error> {
126        Self::from_str(value)
127    }
128}
129
130/// Renders the mailbox as `"display name" <email>` or just `email` if no
131/// display name is set.
132///
133/// The output is **UTF-8-direct**: a non-ASCII display name is emitted
134/// verbatim (e.g. `"José" <jose@example.com>`). This is the right shape
135/// for HTTP-API consumers (Postmark, Resend, Mailgun, Loops) which
136/// JSON-encode UTF-8 strings natively.
137///
138/// **Do not use the result directly as an RFC 5322 header value.** SMTP
139/// headers are 7-bit and require RFC 2047 encoded-word wrapping for
140/// non-ASCII display names; the wire renderer
141/// (`email_message_wire::render_rfc822`) applies that encoding
142/// separately. Routing `Mailbox::to_string()` straight into a `From:`
143/// or `To:` header would emit a malformed RFC 5322 line.
144impl Display for Mailbox {
145    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146        match self.name() {
147            Some(name) => {
148                write_quoted(name, f)?;
149                f.write_str(" <")?;
150                self.email.fmt(f)?;
151                f.write_str(">")
152            }
153            None => self.email.fmt(f),
154        }
155    }
156}
157
158#[cfg(feature = "serde")]
159#[derive(serde::Deserialize, PartialEq, Eq)]
160#[serde(rename_all = "snake_case")]
161enum AddressKind {
162    Mailbox,
163    Group,
164}
165
166#[cfg(feature = "schemars")]
167fn mailbox_typed_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
168    let email = generator.subschema_for::<EmailAddress>();
169    schemars::json_schema!({
170        "type": "object",
171        "properties": {
172            "type": {"const": "mailbox"},
173            "name": {"type": ["string", "null"]},
174            "email": email
175        },
176        "required": ["type", "email"]
177    })
178}
179
180#[cfg(feature = "serde")]
181impl serde::Serialize for Mailbox {
182    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
183    where
184        S: serde::Serializer,
185    {
186        use serde::ser::SerializeStruct as _;
187
188        let len = 2 + usize::from(self.name.is_some());
189        let mut value = serializer.serialize_struct("Mailbox", len)?;
190        value.serialize_field("type", "mailbox")?;
191        if let Some(name) = &self.name {
192            value.serialize_field("name", name)?;
193        }
194        value.serialize_field("email", &self.email)?;
195        value.end()
196    }
197}
198
199#[cfg(feature = "serde")]
200impl<'de> serde::Deserialize<'de> for Mailbox {
201    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
202    where
203        D: serde::Deserializer<'de>,
204    {
205        #[derive(serde::Deserialize)]
206        struct RawMailbox {
207            #[serde(rename = "type")]
208            type_: AddressKind,
209            #[serde(default)]
210            name: Option<String>,
211            email: EmailAddress,
212        }
213
214        fn from_raw<E>(raw: RawMailbox) -> Result<Mailbox, E>
215        where
216            E: serde::de::Error,
217        {
218            if raw.type_ != AddressKind::Mailbox {
219                return Err(E::custom("expected mailbox address type"));
220            }
221            Ok(Mailbox {
222                name: raw.name,
223                email: raw.email,
224            })
225        }
226
227        #[cfg(feature = "rfc5322-string-compat")]
228        {
229            // A bespoke `Visitor` (rather than `#[serde(untagged)]`) so
230            // typed-shape errors keep their field-level provenance, e.g.
231            // `missing field "type"` instead of the generic "did not match
232            // any variant" produced by `untagged`.
233            struct MailboxVisitor;
234
235            impl<'de> serde::de::Visitor<'de> for MailboxVisitor {
236                type Value = Mailbox;
237
238                fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
239                    f.write_str("a typed mailbox object or an RFC 5322 mailbox string")
240                }
241
242                fn visit_str<E>(self, value: &str) -> Result<Mailbox, E>
243                where
244                    E: serde::de::Error,
245                {
246                    value.parse().map_err(E::custom)
247                }
248
249                fn visit_string<E>(self, value: String) -> Result<Mailbox, E>
250                where
251                    E: serde::de::Error,
252                {
253                    value.parse().map_err(E::custom)
254                }
255
256                fn visit_map<A>(self, map: A) -> Result<Mailbox, A::Error>
257                where
258                    A: serde::de::MapAccess<'de>,
259                {
260                    let raw = <RawMailbox as serde::Deserialize<'de>>::deserialize(
261                        serde::de::value::MapAccessDeserializer::new(map),
262                    )?;
263                    from_raw(raw)
264                }
265            }
266
267            deserializer.deserialize_any(MailboxVisitor)
268        }
269
270        #[cfg(not(feature = "rfc5322-string-compat"))]
271        from_raw(RawMailbox::deserialize(deserializer)?)
272    }
273}
274
275#[cfg(feature = "schemars")]
276impl schemars::JsonSchema for Mailbox {
277    fn schema_name() -> std::borrow::Cow<'static, str> {
278        "Mailbox".into()
279    }
280
281    fn schema_id() -> std::borrow::Cow<'static, str> {
282        concat!(module_path!(), "::Mailbox").into()
283    }
284
285    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
286        let typed = mailbox_typed_schema(generator);
287
288        #[cfg(feature = "rfc5322-string-compat")]
289        {
290            schemars::json_schema!({
291                "oneOf": [
292                    typed,
293                    {
294                        "type": "string",
295                        "description": "RFC 5322 mailbox string"
296                    }
297                ]
298            })
299        }
300
301        #[cfg(not(feature = "rfc5322-string-compat"))]
302        typed
303    }
304}
305
306#[cfg(feature = "arbitrary")]
307impl<'a> arbitrary::Arbitrary<'a> for Mailbox {
308    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
309        let email = EmailAddress::arbitrary(u)?;
310        if bool::arbitrary(u)? {
311            let name = format!("User {}", u8::arbitrary(u)?);
312            Ok(Self {
313                name: Some(name),
314                email,
315            })
316        } else {
317            Ok(Self { name: None, email })
318        }
319    }
320}
321
322/// A named address group containing mailbox members.
323#[derive(Clone, Debug, PartialEq, Eq, Hash)]
324pub struct Group {
325    name: String,
326    members: Vec<Mailbox>,
327}
328
329impl Group {
330    /// Returns the group display name.
331    #[must_use]
332    pub fn name(&self) -> &str {
333        self.name.as_str()
334    }
335
336    /// Returns group members.
337    #[must_use]
338    pub fn members(&self) -> &[Mailbox] {
339        self.members.as_slice()
340    }
341}
342
343/// Errors returned when parsing a [`Group`].
344#[derive(Debug, thiserror::Error)]
345#[non_exhaustive]
346pub enum GroupParseError {
347    /// The input contained zero or multiple address items.
348    #[error("expected a single group, found {found} address item(s)")]
349    ExpectedSingleGroup {
350        /// Number of address items found.
351        found: usize,
352    },
353    /// The single address item was a mailbox rather than a group.
354    #[error("expected group but found mailbox")]
355    UnexpectedAddressKind,
356    /// The address parsing backend failed.
357    #[error("group parse backend failed")]
358    Backend {
359        /// The underlying backend error.
360        #[source]
361        source: AddressBackendError,
362    },
363}
364
365impl FromStr for Group {
366    type Err = GroupParseError;
367
368    fn from_str(s: &str) -> Result<Self, Self::Err> {
369        let addresses =
370            parse_address_items(s).map_err(|source| GroupParseError::Backend { source })?;
371        if addresses.len() != 1 {
372            return Err(GroupParseError::ExpectedSingleGroup {
373                found: addresses.len(),
374            });
375        }
376
377        match addresses.into_iter().next() {
378            Some(Address::Group(group)) => Ok(group),
379            _ => Err(GroupParseError::UnexpectedAddressKind),
380        }
381    }
382}
383
384impl TryFrom<&str> for Group {
385    type Error = GroupParseError;
386
387    /// Parses a single group from a string slice.
388    ///
389    /// ```rust
390    /// use email_message::Group;
391    ///
392    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
393    /// let group = Group::try_from("Undisclosed recipients:;")?;
394    /// assert_eq!(group.name(), "Undisclosed recipients");
395    /// assert!(group.members().is_empty());
396    /// # Ok(())
397    /// # }
398    /// ```
399    ///
400    /// # Errors
401    ///
402    /// Returns [`GroupParseError`] unless the input contains exactly one group.
403    fn try_from(value: &str) -> Result<Self, Self::Error> {
404        Self::from_str(value)
405    }
406}
407
408/// Renders the group as `"display name": member1, member2, ...;`.
409///
410/// Same UTF-8-direct caveat as [`Display for Mailbox`]: suitable for
411/// HTTP-API consumers, not directly safe as an RFC 5322 header value.
412impl Display for Group {
413    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
414        write_quoted(self.name(), f)?;
415        f.write_str(":")?;
416        for (idx, member) in self.members().iter().enumerate() {
417            if idx > 0 {
418                f.write_str(", ")?;
419            }
420            member.fmt(f)?;
421        }
422        f.write_str(";")
423    }
424}
425
426#[cfg(feature = "schemars")]
427fn group_typed_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
428    let members = <Vec<Mailbox> as schemars::JsonSchema>::json_schema(generator);
429    schemars::json_schema!({
430        "type": "object",
431        "properties": {
432            "type": {"const": "group"},
433            "name": {"type": "string"},
434            "members": members
435        },
436        "required": ["type", "name"]
437    })
438}
439
440#[cfg(feature = "serde")]
441impl serde::Serialize for Group {
442    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
443    where
444        S: serde::Serializer,
445    {
446        use serde::ser::SerializeStruct as _;
447
448        let len = 2 + usize::from(!self.members.is_empty());
449        let mut value = serializer.serialize_struct("Group", len)?;
450        value.serialize_field("type", "group")?;
451        value.serialize_field("name", &self.name)?;
452        if !self.members.is_empty() {
453            value.serialize_field("members", &self.members)?;
454        }
455        value.end()
456    }
457}
458
459#[cfg(feature = "serde")]
460impl<'de> serde::Deserialize<'de> for Group {
461    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
462    where
463        D: serde::Deserializer<'de>,
464    {
465        #[derive(serde::Deserialize)]
466        struct RawGroup {
467            #[serde(rename = "type")]
468            type_: AddressKind,
469            name: String,
470            #[serde(default)]
471            members: Vec<Mailbox>,
472        }
473
474        fn from_raw<E>(raw: RawGroup) -> Result<Group, E>
475        where
476            E: serde::de::Error,
477        {
478            if raw.type_ != AddressKind::Group {
479                return Err(E::custom("expected group address type"));
480            }
481            Ok(Group {
482                name: raw.name,
483                members: raw.members,
484            })
485        }
486
487        #[cfg(feature = "rfc5322-string-compat")]
488        {
489            struct GroupVisitor;
490
491            impl<'de> serde::de::Visitor<'de> for GroupVisitor {
492                type Value = Group;
493
494                fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
495                    f.write_str("a typed group object or an RFC 5322 group string")
496                }
497
498                fn visit_str<E>(self, value: &str) -> Result<Group, E>
499                where
500                    E: serde::de::Error,
501                {
502                    value.parse().map_err(E::custom)
503                }
504
505                fn visit_string<E>(self, value: String) -> Result<Group, E>
506                where
507                    E: serde::de::Error,
508                {
509                    value.parse().map_err(E::custom)
510                }
511
512                fn visit_map<A>(self, map: A) -> Result<Group, A::Error>
513                where
514                    A: serde::de::MapAccess<'de>,
515                {
516                    let raw = <RawGroup as serde::Deserialize<'de>>::deserialize(
517                        serde::de::value::MapAccessDeserializer::new(map),
518                    )?;
519                    from_raw(raw)
520                }
521            }
522
523            deserializer.deserialize_any(GroupVisitor)
524        }
525
526        #[cfg(not(feature = "rfc5322-string-compat"))]
527        from_raw(RawGroup::deserialize(deserializer)?)
528    }
529}
530
531#[cfg(feature = "schemars")]
532impl schemars::JsonSchema for Group {
533    fn schema_name() -> std::borrow::Cow<'static, str> {
534        "Group".into()
535    }
536
537    fn schema_id() -> std::borrow::Cow<'static, str> {
538        concat!(module_path!(), "::Group").into()
539    }
540
541    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
542        let typed = group_typed_schema(generator);
543
544        #[cfg(feature = "rfc5322-string-compat")]
545        {
546            schemars::json_schema!({
547                "oneOf": [
548                    typed,
549                    {
550                        "type": "string",
551                        "description": "RFC 5322 group string"
552                    }
553                ]
554            })
555        }
556
557        #[cfg(not(feature = "rfc5322-string-compat"))]
558        typed
559    }
560}
561
562#[cfg(feature = "arbitrary")]
563impl<'a> arbitrary::Arbitrary<'a> for Group {
564    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
565        let member_count = usize::from(u.int_in_range::<u8>(0..=3)?);
566        let mut members = Vec::with_capacity(member_count);
567        for _ in 0..member_count {
568            members.push(Mailbox::arbitrary(u)?);
569        }
570        Ok(Self {
571            name: format!("Group {}", u8::arbitrary(u)?),
572            members,
573        })
574    }
575}
576
577/// A single address item: either a mailbox or a group.
578///
579/// Deliberately *not* `#[non_exhaustive]`. RFC 5322 §3.4 closes the
580/// address grammar to exactly `mailbox / group`; the kernel cannot
581/// honestly add a third variant without an RFC update. The
582/// derive-required exhaustive `match` lets downstream callers branch
583/// on every variant without an `_ =>` arm, useful when an extension
584/// crate wants type-safe coverage of the address space.
585#[derive(Clone, Debug, PartialEq, Eq, Hash)]
586pub enum Address {
587    /// A single mailbox.
588    Mailbox(Mailbox),
589    /// A named group of mailboxes.
590    Group(Group),
591}
592
593impl From<Mailbox> for Address {
594    fn from(value: Mailbox) -> Self {
595        Self::Mailbox(value)
596    }
597}
598
599impl From<Group> for Address {
600    fn from(value: Group) -> Self {
601        Self::Group(value)
602    }
603}
604
605impl Address {
606    /// Returns the mailbox entries represented by this address item.
607    pub fn mailboxes(&self) -> impl Iterator<Item = &Mailbox> {
608        match self {
609            Self::Mailbox(mailbox) => std::slice::from_ref(mailbox).iter(),
610            Self::Group(group) => group.members().iter(),
611        }
612    }
613}
614
615/// Errors returned when parsing an [`Address`] or [`AddressList`].
616#[derive(Debug, thiserror::Error)]
617#[non_exhaustive]
618pub enum AddressParseError {
619    /// The input contained zero or multiple address items.
620    #[error("expected a single address, found {found} address item(s)")]
621    ExpectedSingleAddress {
622        /// Number of address items found.
623        found: usize,
624    },
625    /// The address parsing backend failed.
626    #[error("address parse backend failed")]
627    Backend {
628        /// The underlying backend error.
629        #[source]
630        source: AddressBackendError,
631    },
632}
633
634impl FromStr for Address {
635    type Err = AddressParseError;
636
637    fn from_str(s: &str) -> Result<Self, Self::Err> {
638        if let Ok(email) = EmailAddress::from_str(s) {
639            return Ok(Self::Mailbox(Mailbox::from(email)));
640        }
641
642        let addresses =
643            parse_address_items(s).map_err(|source| AddressParseError::Backend { source })?;
644        if addresses.len() != 1 {
645            return Err(AddressParseError::ExpectedSingleAddress {
646                found: addresses.len(),
647            });
648        }
649
650        addresses
651            .into_iter()
652            .next()
653            .ok_or(AddressParseError::ExpectedSingleAddress { found: 0 })
654    }
655}
656
657impl TryFrom<&str> for Address {
658    type Error = AddressParseError;
659
660    /// Parses a single address (mailbox or group) from a string slice.
661    ///
662    /// ```rust
663    /// use email_message::Address;
664    ///
665    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
666    /// let address = Address::try_from("jdoe@one.test")?;
667    /// assert_eq!(address.to_string(), "jdoe@one.test");
668    /// # Ok(())
669    /// # }
670    /// ```
671    ///
672    /// # Errors
673    ///
674    /// Returns [`AddressParseError`] unless the input contains exactly one
675    /// mailbox or group.
676    fn try_from(value: &str) -> Result<Self, Self::Error> {
677        Self::from_str(value)
678    }
679}
680
681/// Forwards to the underlying [`Display for Mailbox`] or
682/// [`Display for Group`]; same UTF-8-direct caveat applies.
683impl Display for Address {
684    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
685        match self {
686            Self::Mailbox(mailbox) => mailbox.fmt(f),
687            Self::Group(group) => group.fmt(f),
688        }
689    }
690}
691
692#[cfg(feature = "serde")]
693impl serde::Serialize for Address {
694    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
695    where
696        S: serde::Serializer,
697    {
698        match self {
699            Self::Mailbox(mailbox) => serde::Serialize::serialize(mailbox, serializer),
700            Self::Group(group) => serde::Serialize::serialize(group, serializer),
701        }
702    }
703}
704
705#[cfg(feature = "serde")]
706impl<'de> serde::Deserialize<'de> for Address {
707    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
708    where
709        D: serde::Deserializer<'de>,
710    {
711        #[derive(serde::Deserialize)]
712        #[serde(tag = "type", rename_all = "snake_case")]
713        enum RawAddress {
714            Mailbox {
715                #[serde(default)]
716                name: Option<String>,
717                email: EmailAddress,
718            },
719            Group {
720                name: String,
721                #[serde(default)]
722                members: Vec<Mailbox>,
723            },
724        }
725
726        fn from_raw(raw: RawAddress) -> Address {
727            match raw {
728                RawAddress::Mailbox { name, email } => Address::Mailbox(Mailbox { name, email }),
729                RawAddress::Group { name, members } => Address::Group(Group { name, members }),
730            }
731        }
732
733        #[cfg(feature = "rfc5322-string-compat")]
734        {
735            struct AddressVisitor;
736
737            impl<'de> serde::de::Visitor<'de> for AddressVisitor {
738                type Value = Address;
739
740                fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
741                    f.write_str("a typed address object or an RFC 5322 address string")
742                }
743
744                fn visit_str<E>(self, value: &str) -> Result<Address, E>
745                where
746                    E: serde::de::Error,
747                {
748                    value.parse().map_err(E::custom)
749                }
750
751                fn visit_string<E>(self, value: String) -> Result<Address, E>
752                where
753                    E: serde::de::Error,
754                {
755                    value.parse().map_err(E::custom)
756                }
757
758                fn visit_map<A>(self, map: A) -> Result<Address, A::Error>
759                where
760                    A: serde::de::MapAccess<'de>,
761                {
762                    let raw = <RawAddress as serde::Deserialize<'de>>::deserialize(
763                        serde::de::value::MapAccessDeserializer::new(map),
764                    )?;
765                    Ok(from_raw(raw))
766                }
767            }
768
769            deserializer.deserialize_any(AddressVisitor)
770        }
771
772        #[cfg(not(feature = "rfc5322-string-compat"))]
773        Ok(from_raw(RawAddress::deserialize(deserializer)?))
774    }
775}
776
777#[cfg(feature = "schemars")]
778impl schemars::JsonSchema for Address {
779    fn schema_name() -> std::borrow::Cow<'static, str> {
780        "Address".into()
781    }
782
783    fn schema_id() -> std::borrow::Cow<'static, str> {
784        concat!(module_path!(), "::Address").into()
785    }
786
787    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
788        let mailbox = mailbox_typed_schema(generator);
789        let group = group_typed_schema(generator);
790
791        #[cfg(feature = "rfc5322-string-compat")]
792        {
793            schemars::json_schema!({
794                "oneOf": [
795                    mailbox,
796                    group,
797                    {
798                        "type": "string",
799                        "description": "RFC 5322 address string"
800                    }
801                ]
802            })
803        }
804
805        #[cfg(not(feature = "rfc5322-string-compat"))]
806        schemars::json_schema!({"oneOf": [mailbox, group]})
807    }
808}
809
810#[cfg(feature = "arbitrary")]
811impl<'a> arbitrary::Arbitrary<'a> for Address {
812    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
813        if bool::arbitrary(u)? {
814            Ok(Self::Mailbox(Mailbox::arbitrary(u)?))
815        } else {
816            Ok(Self::Group(Group::arbitrary(u)?))
817        }
818    }
819}
820
821macro_rules! impl_address_collection {
822    ($(#[$meta:meta])* $name:ident, $item:ty, $error:ty, $parse_fn:expr) => {
823        $(#[$meta])*
824        #[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
825        pub struct $name {
826            items: Vec<$item>,
827        }
828
829        #[cfg(feature = "schemars")]
830        impl schemars::JsonSchema for $name {
831            fn inline_schema() -> bool {
832                true
833            }
834
835            fn schema_name() -> std::borrow::Cow<'static, str> {
836                stringify!($name).into()
837            }
838
839            fn schema_id() -> std::borrow::Cow<'static, str> {
840                concat!(module_path!(), "::", stringify!($name)).into()
841            }
842
843            fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
844                let typed = <Vec<$item> as schemars::JsonSchema>::json_schema(generator);
845
846                #[cfg(feature = "rfc5322-string-compat")]
847                {
848                    schemars::json_schema!({
849                        "oneOf": [
850                            typed,
851                            {
852                                "type": "string",
853                                "description": "RFC 5322 comma-separated address list string"
854                            }
855                        ]
856                    })
857                }
858
859                #[cfg(not(feature = "rfc5322-string-compat"))]
860                typed
861            }
862        }
863
864        impl $name {
865            /// Returns the number of items in the list.
866            #[must_use]
867            pub fn len(&self) -> usize {
868                self.items.len()
869            }
870
871            /// Returns `true` if the list contains no items.
872            #[must_use]
873            pub fn is_empty(&self) -> bool {
874                self.items.is_empty()
875            }
876
877            /// Returns an iterator over the items.
878            pub fn iter(&self) -> std::slice::Iter<'_, $item> {
879                self.items.iter()
880            }
881
882            /// Returns a mutable iterator over the items.
883            pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, $item> {
884                self.items.iter_mut()
885            }
886
887            /// Returns the items as a slice.
888            #[must_use]
889            pub fn as_slice(&self) -> &[$item] {
890                self.items.as_slice()
891            }
892
893            /// Consumes the list and returns its item vector.
894            #[must_use]
895            pub fn into_vec(self) -> Vec<$item> {
896                self.items
897            }
898        }
899
900        impl From<Vec<$item>> for $name {
901            fn from(items: Vec<$item>) -> Self {
902                Self { items }
903            }
904        }
905
906        impl From<$name> for Vec<$item> {
907            fn from(value: $name) -> Self {
908                value.items
909            }
910        }
911
912        impl FromStr for $name {
913            type Err = $error;
914
915            fn from_str(s: &str) -> Result<Self, Self::Err> {
916                let items = ($parse_fn)(s)?;
917                Ok(Self { items })
918            }
919        }
920
921        impl TryFrom<&str> for $name {
922            type Error = $error;
923
924            /// Parses a list from a string slice.
925            fn try_from(value: &str) -> Result<Self, Self::Error> {
926                Self::from_str(value)
927            }
928        }
929
930        impl IntoIterator for $name {
931            type Item = $item;
932            type IntoIter = std::vec::IntoIter<$item>;
933
934            fn into_iter(self) -> Self::IntoIter {
935                self.items.into_iter()
936            }
937        }
938
939        impl<'a> IntoIterator for &'a $name {
940            type Item = &'a $item;
941            type IntoIter = std::slice::Iter<'a, $item>;
942
943            fn into_iter(self) -> Self::IntoIter {
944                self.items.iter()
945            }
946        }
947
948        impl<'a> IntoIterator for &'a mut $name {
949            type Item = &'a mut $item;
950            type IntoIter = std::slice::IterMut<'a, $item>;
951
952            fn into_iter(self) -> Self::IntoIter {
953                self.items.iter_mut()
954            }
955        }
956
957        impl AsRef<[$item]> for $name {
958            fn as_ref(&self) -> &[$item] {
959                self.items.as_slice()
960            }
961        }
962
963        impl std::iter::FromIterator<$item> for $name {
964            fn from_iter<T: IntoIterator<Item = $item>>(iter: T) -> Self {
965                Self {
966                    items: iter.into_iter().collect(),
967                }
968            }
969        }
970
971        impl Extend<$item> for $name {
972            fn extend<T: IntoIterator<Item = $item>>(&mut self, iter: T) {
973                self.items.extend(iter);
974            }
975        }
976
977        impl Display for $name {
978            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
979                for (idx, item) in self.items.iter().enumerate() {
980                    if idx > 0 {
981                        f.write_str(", ")?;
982                    }
983                    item.fmt(f)?;
984                }
985                Ok(())
986            }
987        }
988
989        #[cfg(feature = "serde")]
990        impl serde::Serialize for $name {
991            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
992            where
993                S: serde::Serializer,
994            {
995                serde::Serialize::serialize(&self.items, serializer)
996            }
997        }
998
999        #[cfg(feature = "serde")]
1000        impl<'de> serde::Deserialize<'de> for $name {
1001            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1002            where
1003                D: serde::Deserializer<'de>,
1004            {
1005                #[cfg(feature = "rfc5322-string-compat")]
1006                {
1007                    struct CollectionVisitor;
1008
1009                    impl<'de> serde::de::Visitor<'de> for CollectionVisitor {
1010                        type Value = $name;
1011
1012                        fn expecting(
1013                            &self,
1014                            f: &mut std::fmt::Formatter<'_>,
1015                        ) -> std::fmt::Result {
1016                            f.write_str(concat!(
1017                                "a ",
1018                                stringify!($name),
1019                                " array or an RFC 5322 list string",
1020                            ))
1021                        }
1022
1023                        fn visit_str<E>(self, value: &str) -> Result<$name, E>
1024                        where
1025                            E: serde::de::Error,
1026                        {
1027                            value.parse().map_err(E::custom)
1028                        }
1029
1030                        fn visit_string<E>(self, value: String) -> Result<$name, E>
1031                        where
1032                            E: serde::de::Error,
1033                        {
1034                            value.parse().map_err(E::custom)
1035                        }
1036
1037                        fn visit_seq<A>(self, mut seq: A) -> Result<$name, A::Error>
1038                        where
1039                            A: serde::de::SeqAccess<'de>,
1040                        {
1041                            let mut items = Vec::with_capacity(seq.size_hint().unwrap_or(0));
1042                            while let Some(item) = seq.next_element::<$item>()? {
1043                                items.push(item);
1044                            }
1045                            Ok($name { items })
1046                        }
1047                    }
1048
1049                    deserializer.deserialize_any(CollectionVisitor)
1050                }
1051
1052                #[cfg(not(feature = "rfc5322-string-compat"))]
1053                {
1054                    let items = <Vec<$item> as serde::Deserialize>::deserialize(deserializer)?;
1055                    Ok(Self { items })
1056                }
1057            }
1058        }
1059
1060        #[cfg(feature = "arbitrary")]
1061        impl<'a> arbitrary::Arbitrary<'a> for $name {
1062            fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
1063                let len = usize::from(u.int_in_range::<u8>(0..=4)?);
1064                let mut items = Vec::with_capacity(len);
1065                for _ in 0..len {
1066                    items.push(<$item>::arbitrary(u)?);
1067                }
1068                Ok(Self { items })
1069            }
1070        }
1071    };
1072}
1073
1074impl_address_collection!(
1075    /// A parsed list of address items.
1076    ///
1077    /// This is used instead of `Vec<Address>` for `FromStr`, because Rust's orphan
1078    /// rules do not allow implementing foreign traits for foreign types.
1079    AddressList,
1080    Address,
1081    AddressParseError,
1082    |s| parse_address_items(s).map_err(|source| AddressParseError::Backend { source })
1083);
1084
1085impl<'a> TryFrom<Vec<&'a str>> for AddressList {
1086    type Error = AddressParseError;
1087
1088    fn try_from(value: Vec<&'a str>) -> Result<Self, Self::Error> {
1089        value
1090            .into_iter()
1091            .map(Address::from_str)
1092            .collect::<Result<Vec<_>, _>>()
1093            .map(Self::from)
1094    }
1095}
1096
1097impl<'a> TryFrom<&'a [&'a str]> for AddressList {
1098    type Error = AddressParseError;
1099
1100    fn try_from(value: &'a [&'a str]) -> Result<Self, Self::Error> {
1101        value
1102            .iter()
1103            .copied()
1104            .map(Address::from_str)
1105            .collect::<Result<Vec<_>, _>>()
1106            .map(Self::from)
1107    }
1108}
1109
1110impl<'a> TryFrom<Vec<&'a str>> for MailboxList {
1111    type Error = MailboxParseError;
1112
1113    fn try_from(value: Vec<&'a str>) -> Result<Self, Self::Error> {
1114        value
1115            .into_iter()
1116            .map(Mailbox::from_str)
1117            .collect::<Result<Vec<_>, _>>()
1118            .map(Self::from)
1119    }
1120}
1121
1122impl<'a> TryFrom<&'a [&'a str]> for MailboxList {
1123    type Error = MailboxParseError;
1124
1125    fn try_from(value: &'a [&'a str]) -> Result<Self, Self::Error> {
1126        value
1127            .iter()
1128            .copied()
1129            .map(Mailbox::from_str)
1130            .collect::<Result<Vec<_>, _>>()
1131            .map(Self::from)
1132    }
1133}
1134
1135impl_address_collection!(
1136    /// A parsed list of mailbox items.
1137    ///
1138    /// Group entries are rejected when parsing into `MailboxList`.
1139    MailboxList,
1140    Mailbox,
1141    MailboxParseError,
1142    |s| {
1143        let addresses = parse_address_items(s).map_err(|source| MailboxParseError::Backend { source })?;
1144        let mut items = Vec::with_capacity(addresses.len());
1145
1146        for address in addresses {
1147            match address {
1148                Address::Mailbox(mailbox) => items.push(mailbox),
1149                Address::Group(_) => return Err(MailboxParseError::ContainsGroupEntry),
1150            }
1151        }
1152
1153        Ok(items)
1154    }
1155);
1156
1157/// Maximum byte length accepted by the address-list parser before
1158/// rejecting outright. 64 KiB is far above any realistic header value
1159///, RFC 5322 caps physical lines at 998 bytes; even a header folded
1160/// across hundreds of continuation lines stays well under this. The
1161/// cap exists to prevent the `format!("To: {input}\r\n\r\n")`
1162/// allocation amplification on adversarial multi-megabyte input.
1163pub const MAX_ADDRESS_INPUT_BYTES: usize = 64 * 1024;
1164
1165/// Low-level failures reported by the address parsing backend.
1166#[derive(Debug, thiserror::Error)]
1167#[non_exhaustive]
1168pub enum AddressBackendError {
1169    /// The input contains a raw carriage return or line feed.
1170    #[error("address input contains raw newline characters")]
1171    InputContainsRawNewlines,
1172    /// The input exceeds [`MAX_ADDRESS_INPUT_BYTES`].
1173    #[error("address input is {len} bytes, exceeding maximum of {max}")]
1174    #[non_exhaustive]
1175    InputTooLong {
1176        /// Actual input length in bytes.
1177        len: usize,
1178        /// Maximum accepted input length in bytes.
1179        max: usize,
1180    },
1181    /// The backend could not parse the synthetic address header.
1182    #[error("failed to parse address header")]
1183    HeaderParse,
1184    /// The parsed header did not contain address data.
1185    #[error("parsed header did not contain address data")]
1186    MissingAddress,
1187    /// A mailbox did not contain an `addr-spec`.
1188    #[error("mailbox is missing addr-spec")]
1189    MissingAddrSpec,
1190    /// A mailbox contained an invalid `addr-spec`.
1191    #[error("invalid addr-spec `{input}`")]
1192    InvalidAddrSpec {
1193        /// Invalid `addr-spec` text.
1194        input: String,
1195        /// Underlying email-address validation error.
1196        #[source]
1197        source: EmailAddressParseError,
1198    },
1199    /// A group member did not contain an `addr-spec`.
1200    #[error("group member at index {index} is missing addr-spec")]
1201    GroupMemberMissingAddrSpec {
1202        /// Zero-based index of the invalid member.
1203        index: usize,
1204    },
1205    /// A group member contained an invalid `addr-spec`.
1206    #[error("invalid group member addr-spec `{input}` at index {index}")]
1207    InvalidGroupMemberAddrSpec {
1208        /// Zero-based index of the invalid member.
1209        index: usize,
1210        /// Invalid `addr-spec` text.
1211        input: String,
1212        /// Underlying email-address validation error.
1213        #[source]
1214        source: EmailAddressParseError,
1215    },
1216    /// A parsed group did not contain a name.
1217    #[error("group is missing a name")]
1218    GroupMissingName,
1219}
1220
1221fn write_quoted(value: &str, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1222    f.write_str("\"")?;
1223    for ch in value.chars() {
1224        if ch == '\\' || ch == '"' {
1225            f.write_str("\\")?;
1226        }
1227        f.write_str(ch.encode_utf8(&mut [0; 4]))?;
1228    }
1229    f.write_str("\"")
1230}
1231
1232/// Parse-side address-list extractor.
1233///
1234/// # Byte discipline at the parser
1235///
1236/// This parser deliberately accepts more than the message-level gate
1237/// rejects. Specifically: it rejects raw CR / LF (which would let an
1238/// attacker inject a new header line at the parser layer) and
1239/// inputs over [`MAX_ADDRESS_INPUT_BYTES`]; it does **not** reject
1240/// NUL or other non-tab ASCII control characters in display-name
1241/// content.
1242///
1243/// The asymmetry is intentional. The kernel's stricter byte-
1244/// discipline lives at [`crate::Message::validate_basic`] and fires
1245/// when an outbound `OutboundMessage` is built; inbound parsing is
1246/// best-effort and used in forensic / archival / replay workflows
1247/// where rejecting BEL / VT / ESC in display names from real-world
1248/// malformed-but-recoverable mail loses information. A `Mailbox`
1249/// carrying questionable bytes is fine *as a parsed value*; it
1250/// cannot reach an outbound wire renderer because the message-level
1251/// gate catches it first.
1252///
1253/// Callers handing a `Mailbox.name()` directly to a logging sink or
1254/// non-validated downstream consumer are responsible for their own
1255/// byte-discipline check.
1256fn parse_address_items(input: &str) -> Result<Vec<Address>, AddressBackendError> {
1257    if input.len() > MAX_ADDRESS_INPUT_BYTES {
1258        return Err(AddressBackendError::InputTooLong {
1259            len: input.len(),
1260            max: MAX_ADDRESS_INPUT_BYTES,
1261        });
1262    }
1263    if input.contains('\r') || input.contains('\n') {
1264        return Err(AddressBackendError::InputContainsRawNewlines);
1265    }
1266
1267    let raw = format!("To: {input}\r\n\r\n");
1268    let parser =
1269        ADDRESS_PARSER.get_or_init(|| mail_parser::MessageParser::new().with_address_headers());
1270    let message = parser
1271        .parse_headers(raw.as_bytes())
1272        .ok_or(AddressBackendError::HeaderParse)?;
1273    let parsed = message.to().ok_or(AddressBackendError::MissingAddress)?;
1274
1275    match parsed {
1276        mail_parser::Address::List(list) => list
1277            .iter()
1278            .map(convert_mailbox)
1279            .map(|result| result.map(Address::Mailbox))
1280            .collect(),
1281        // mail_parser switches the whole header to the `Group` shape as soon
1282        // as any group syntax appears, and wraps flat mailboxes that appear
1283        // before/between/after named groups into a synthetic
1284        // `Group { name: None, ... }`. Flatten those back to `Mailbox`
1285        // entries so a mixed header like
1286        // `alice@example.com, Team: bob@team.com;, dave@example.com`
1287        // produces three items in order rather than a parse error.
1288        mail_parser::Address::Group(groups) => {
1289            let mut items = Vec::with_capacity(groups.len());
1290            for group in groups {
1291                if group.name.is_some() {
1292                    items.push(Address::Group(convert_group(group)?));
1293                } else {
1294                    for addr in &group.addresses {
1295                        items.push(Address::Mailbox(convert_mailbox(addr)?));
1296                    }
1297                }
1298            }
1299            Ok(items)
1300        }
1301    }
1302}
1303
1304fn convert_mailbox(value: &mail_parser::Addr<'_>) -> Result<Mailbox, AddressBackendError> {
1305    let raw_email = value
1306        .address()
1307        .ok_or(AddressBackendError::MissingAddrSpec)?;
1308    let email = EmailAddress::from_str(raw_email).map_err(|source| {
1309        AddressBackendError::InvalidAddrSpec {
1310            input: raw_email.to_owned(),
1311            source,
1312        }
1313    })?;
1314
1315    Ok(match value.name() {
1316        Some(name) => Mailbox::from((name.to_owned(), email)),
1317        None => Mailbox::from(email),
1318    })
1319}
1320
1321fn convert_group(value: &mail_parser::Group<'_>) -> Result<Group, AddressBackendError> {
1322    let name = value
1323        .name
1324        .as_deref()
1325        .ok_or(AddressBackendError::GroupMissingName)?
1326        .to_owned();
1327    let mut members = Vec::with_capacity(value.addresses.len());
1328    for (index, member) in value.addresses.iter().enumerate() {
1329        let mailbox = convert_mailbox(member).map_err(|error| match error {
1330            AddressBackendError::MissingAddrSpec => {
1331                AddressBackendError::GroupMemberMissingAddrSpec { index }
1332            }
1333            AddressBackendError::InvalidAddrSpec { input, source } => {
1334                AddressBackendError::InvalidGroupMemberAddrSpec {
1335                    index,
1336                    input,
1337                    source,
1338                }
1339            }
1340            other => other,
1341        })?;
1342        members.push(mailbox);
1343    }
1344
1345    Ok(Group { name, members })
1346}
1347
1348#[cfg(test)]
1349mod tests {
1350    use super::*;
1351
1352    #[test]
1353    fn mailbox_from_str_accepts_rfc_examples() {
1354        let parsed = "Mary Smith <mary@x.test>".parse::<Mailbox>();
1355        assert!(parsed.is_ok(), "expected valid mailbox");
1356
1357        let parsed = "jdoe@one.test".parse::<Mailbox>();
1358        assert!(parsed.is_ok(), "expected valid mailbox");
1359    }
1360
1361    #[test]
1362    fn mailbox_from_str_rejects_group() {
1363        let parsed = "Undisclosed recipients:;".parse::<Mailbox>();
1364        assert!(matches!(
1365            parsed,
1366            Err(MailboxParseError::UnexpectedAddressKind)
1367        ));
1368    }
1369
1370    #[test]
1371    fn group_from_str_accepts_rfc_examples() {
1372        let parsed =
1373            "A Group:Ed Jones <c@a.test>,joe@where.test,John <jdoe@one.test>;".parse::<Group>();
1374        assert!(parsed.is_ok(), "expected valid group");
1375
1376        let parsed = "Undisclosed recipients:;".parse::<Group>();
1377        assert!(parsed.is_ok(), "expected valid group");
1378    }
1379
1380    #[test]
1381    fn address_list_roundtrip() {
1382        let list = "Mary Smith <mary@x.test>, jdoe@one.test"
1383            .parse::<AddressList>()
1384            .expect("address list should parse");
1385        let rendered = list.to_string();
1386        let reparsed = rendered
1387            .parse::<AddressList>()
1388            .expect("rendered address list should parse");
1389        assert_eq!(reparsed.as_slice(), list.as_slice());
1390    }
1391
1392    #[test]
1393    fn mailbox_from_str_rejects_input_with_raw_newline() {
1394        let parsed = "Mary Smith <mary@x.test>\nBcc: victim@example.com".parse::<Mailbox>();
1395        assert!(matches!(
1396            parsed,
1397            Err(MailboxParseError::Backend {
1398                source: AddressBackendError::InputContainsRawNewlines,
1399            })
1400        ));
1401    }
1402}