message/
address.rs

1#[cfg(feature = "impl")]
2use config::any_err;
3use mailparsing::{AddrSpec, Address, AddressList, EncodeHeaderValue, Mailbox};
4#[cfg(feature = "impl")]
5use mlua::{MetaMethod, UserData, UserDataFields, UserDataMethods};
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9pub struct HeaderAddressList(Vec<HeaderAddressEntry>);
10
11impl HeaderAddressList {
12    /// If the address list is comprised of a single entry,
13    /// returns just the email domain from that entry
14    pub fn domain(&self) -> anyhow::Result<&str> {
15        let addr = self.single_address()?;
16        addr.domain()
17    }
18
19    /// If the address list is comprised of a single entry,
20    /// returns just the email domain from that entry
21    pub fn user(&self) -> anyhow::Result<&str> {
22        let addr = self.single_address()?;
23        addr.user()
24    }
25
26    /// If the address list is comprised of a single entry,
27    /// returns just the display name portion, if any
28    pub fn name(&self) -> anyhow::Result<Option<&str>> {
29        let addr = self.single_address()?;
30        Ok(addr.name.as_deref())
31    }
32
33    pub fn email(&self) -> anyhow::Result<Option<String>> {
34        let addr = self.single_address()?;
35        Ok(addr.email())
36    }
37
38    /// Flattens the groups and list and returns a simple list
39    /// of addresses
40    pub fn flatten(&self) -> Vec<&HeaderAddress> {
41        let mut res = vec![];
42        for entry in &self.0 {
43            match entry {
44                HeaderAddressEntry::Address(a) => res.push(a),
45                HeaderAddressEntry::Group(group) => {
46                    for addr in &group.addresses {
47                        res.push(addr);
48                    }
49                }
50            }
51        }
52        res
53    }
54
55    pub fn single_address(&self) -> anyhow::Result<&HeaderAddress> {
56        match self.0.len() {
57            0 => anyhow::bail!("no addresses"),
58            1 => match &self.0[0] {
59                HeaderAddressEntry::Address(a) => Ok(a),
60                _ => anyhow::bail!("is not a simple address"),
61            },
62            _ => anyhow::bail!("is not a simple single address"),
63        }
64    }
65}
66
67impl From<AddressList> for HeaderAddressList {
68    fn from(input: AddressList) -> HeaderAddressList {
69        let addresses: Vec<HeaderAddressEntry> = input.0.iter().map(Into::into).collect();
70        HeaderAddressList(addresses)
71    }
72}
73
74#[cfg(feature = "impl")]
75impl UserData for HeaderAddressList {
76    fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
77        fields.add_field_method_get("user", |_, this| {
78            Ok(this.user().map_err(any_err)?.to_string())
79        });
80        fields.add_field_method_get("domain", |_, this| {
81            Ok(this.domain().map_err(any_err)?.to_string())
82        });
83        fields.add_field_method_get("email", |_, this| Ok(this.email().map_err(any_err)?));
84        fields.add_field_method_get("name", |_, this| {
85            Ok(this.name().map_err(any_err)?.map(|s| s.to_string()))
86        });
87        fields.add_field_method_get("list", |_, this| {
88            Ok(this
89                .flatten()
90                .into_iter()
91                .cloned()
92                .collect::<Vec<HeaderAddress>>())
93        });
94    }
95
96    fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
97        methods.add_meta_method(MetaMethod::ToString, |_, this, _: ()| {
98            let json = serde_json::to_string(&this.0).map_err(any_err)?;
99            Ok(json)
100        });
101    }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
105pub enum HeaderAddressEntry {
106    Address(HeaderAddress),
107    Group(AddressGroup),
108}
109
110impl From<&Address> for HeaderAddressEntry {
111    fn from(addr: &Address) -> HeaderAddressEntry {
112        match addr {
113            Address::Mailbox(mbox) => HeaderAddressEntry::Address(mbox.into()),
114            Address::Group { name, entries } => {
115                let addresses = entries.0.iter().map(Into::into).collect();
116                HeaderAddressEntry::Group(AddressGroup {
117                    name: if name.is_empty() {
118                        None
119                    } else {
120                        Some(name.clone())
121                    },
122                    addresses,
123                })
124            }
125        }
126    }
127}
128
129/// Wire format for JSON serialization of HeaderAddress.
130/// This preserves the original `{name, address}` JSON shape for
131/// backwards compatibility with existing consumers.
132#[derive(Debug, Clone, Serialize, Deserialize)]
133struct HeaderAddressWire {
134    pub name: Option<String>,
135    pub address: Option<String>,
136}
137
138impl From<HeaderAddress> for HeaderAddressWire {
139    fn from(addr: HeaderAddress) -> Self {
140        let address = addr.email();
141        Self {
142            name: addr.name,
143            address,
144        }
145    }
146}
147
148impl TryFrom<HeaderAddressWire> for HeaderAddress {
149    type Error = anyhow::Error;
150
151    fn try_from(wire: HeaderAddressWire) -> anyhow::Result<Self> {
152        let (user, domain) = match &wire.address {
153            Some(email) => {
154                let parsed = AddrSpec::parse(email)?;
155                (Some(parsed.local_part), Some(parsed.domain))
156            }
157            None => (None, None),
158        };
159        Ok(Self {
160            name: wire.name,
161            user,
162            domain,
163        })
164    }
165}
166
167#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
168#[serde(try_from = "HeaderAddressWire", into = "HeaderAddressWire")]
169pub struct HeaderAddress {
170    pub name: Option<String>,
171    pub user: Option<String>,
172    pub domain: Option<String>,
173}
174
175impl From<&Mailbox> for HeaderAddress {
176    fn from(mbox: &Mailbox) -> HeaderAddress {
177        Self {
178            name: mbox.name.clone(),
179            user: Some(mbox.address.local_part.clone()),
180            domain: Some(mbox.address.domain.clone()),
181        }
182    }
183}
184
185impl HeaderAddress {
186    pub fn user(&self) -> anyhow::Result<&str> {
187        self.user
188            .as_deref()
189            .ok_or_else(|| anyhow::anyhow!("no address"))
190    }
191    pub fn domain(&self) -> anyhow::Result<&str> {
192        self.domain
193            .as_deref()
194            .ok_or_else(|| anyhow::anyhow!("no address"))
195    }
196    pub fn email(&self) -> Option<String> {
197        match (&self.user, &self.domain) {
198            (Some(user), Some(domain)) => {
199                Some(AddrSpec::new(user, domain).encode_value().to_string())
200            }
201            _ => None,
202        }
203    }
204    pub fn name(&self) -> Option<&str> {
205        self.name.as_deref()
206    }
207}
208
209#[cfg(feature = "impl")]
210impl UserData for HeaderAddress {
211    fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
212        fields.add_field_method_get("user", |_, this| {
213            Ok(this.user().map_err(any_err)?.to_string())
214        });
215        fields.add_field_method_get("domain", |_, this| {
216            Ok(this.domain().map_err(any_err)?.to_string())
217        });
218        fields.add_field_method_get("email", |_, this| Ok(this.email()));
219        fields.add_field_method_get("name", |_, this| Ok(this.name().map(|s| s.to_string())));
220    }
221    fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
222        methods.add_meta_method(MetaMethod::ToString, |_, this, _: ()| {
223            let json = serde_json::to_string(&this).map_err(any_err)?;
224            Ok(json)
225        });
226    }
227}
228
229#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
230pub struct AddressGroup {
231    pub name: Option<String>,
232    pub addresses: Vec<HeaderAddress>,
233}
234
235#[cfg(test)]
236mod test {
237    use super::*;
238    use mailparsing::Parser;
239
240    /// Test the unusual "info@"@example.com form via HeaderAddressList
241    #[test]
242    fn header_address_list_with_at_in_local_part() {
243        // Parse the header value that contains the unusual email address
244        let header_value = "\"info@\"@example.com";
245        let addr_list = Parser::parse_address_list_header(header_value.as_bytes())
246            .expect("failed to parse address list");
247
248        // Convert to HeaderAddressList
249        let header_addr_list: HeaderAddressList = addr_list.into();
250
251        // user() now returns the decoded local part (without quotes)
252        let user = header_addr_list.user().expect("failed to get user");
253        k9::assert_equal!(user, "info@");
254        let domain = header_addr_list.domain().expect("failed to get domain");
255        k9::assert_equal!(domain, "example.com");
256
257        let addr = header_addr_list
258            .single_address()
259            .expect("expected single address");
260
261        // HeaderAddress stores the decoded local part
262        k9::assert_equal!(addr.user().unwrap(), "info@");
263        k9::assert_equal!(addr.domain().unwrap(), "example.com");
264        // email() re-encodes with quoting as needed
265        k9::assert_equal!(addr.email().unwrap(), "\"info@\"@example.com");
266    }
267
268    /// Test JSON serialization roundtrip preserves the {name, address} shape
269    #[test]
270    fn header_address_serde_roundtrip() {
271        let addr = HeaderAddress {
272            name: Some("Test User".into()),
273            user: Some("test".into()),
274            domain: Some("example.com".into()),
275        };
276
277        let json = serde_json::to_string(&addr).unwrap();
278        k9::assert_equal!(json, r#"{"name":"Test User","address":"test@example.com"}"#);
279
280        let roundtripped: HeaderAddress = serde_json::from_str(&json).unwrap();
281        k9::assert_equal!(roundtripped, addr);
282    }
283
284    /// Test JSON roundtrip with a quoted local part
285    #[test]
286    fn header_address_serde_roundtrip_quoted() {
287        let addr = HeaderAddress {
288            name: None,
289            user: Some("info@".into()),
290            domain: Some("example.com".into()),
291        };
292
293        let json = serde_json::to_string(&addr).unwrap();
294        k9::assert_equal!(json, r#"{"name":null,"address":"\"info@\"@example.com"}"#);
295
296        let roundtripped: HeaderAddress = serde_json::from_str(&json).unwrap();
297        k9::assert_equal!(roundtripped, addr);
298    }
299}