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