message/
address.rs

1use config::any_err;
2use dns_resolver::DomainClassification;
3use mailparsing::{Address, AddressList, EncodeHeaderValue, Mailbox};
4#[cfg(feature = "impl")]
5use mlua::{MetaMethod, UserData, UserDataFields, UserDataMethods};
6use rfc5321::{ForwardPath, ReversePath};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Eq)]
10#[serde(transparent)]
11pub struct EnvelopeAddress(String);
12
13impl EnvelopeAddress {
14    pub fn parse(text: &str) -> anyhow::Result<Self> {
15        if text.is_empty() {
16            Ok(Self::null_sender())
17        } else {
18            let fields: Vec<&str> = text.split('@').collect();
19            anyhow::ensure!(fields.len() == 2, "expected user@domain");
20            // TODO: stronger validation of local part and domain
21            let domain = &fields[1];
22            Self::validate_domain(domain)?;
23            Ok(Self(text.to_string()))
24        }
25    }
26
27    fn validate_domain(domain: &str) -> anyhow::Result<()> {
28        // Ensure that there are no port numbers in the domain portion.
29        // These are no allowed by SMTP. We don't allow them during
30        // RCPT TO in the incoming SMTP, but it is possible for
31        // addresses to be constructed in a few other code paths,
32        // so it is prudent to explicitly validate that here
33        let classified = DomainClassification::classify(domain)?;
34        if classified.has_port() {
35            anyhow::bail!("EnvelopeAddress does not allow port numbers in the domain ({domain})");
36        }
37
38        Ok(())
39    }
40
41    pub fn user(&self) -> &str {
42        match self.0.find('@') {
43            Some(at) => &self.0[..at],
44            None => "",
45        }
46    }
47
48    pub fn domain(&self) -> &str {
49        match self.0.find('@') {
50            Some(at) => &self.0[at + 1..],
51            None => "",
52        }
53    }
54
55    pub fn null_sender() -> Self {
56        Self("".to_string())
57    }
58
59    pub fn to_string(&self) -> String {
60        self.0.to_string()
61    }
62}
63
64impl TryInto<EnvelopeAddress> for &Mailbox {
65    type Error = anyhow::Error;
66    fn try_into(self) -> anyhow::Result<EnvelopeAddress> {
67        EnvelopeAddress::parse(&self.address.encode_value())
68    }
69}
70
71impl TryInto<EnvelopeAddress> for &Address {
72    type Error = anyhow::Error;
73    fn try_into(self) -> anyhow::Result<EnvelopeAddress> {
74        EnvelopeAddress::parse(&self.encode_value())
75    }
76}
77
78impl TryInto<ForwardPath> for EnvelopeAddress {
79    type Error = String;
80    fn try_into(self) -> Result<ForwardPath, Self::Error> {
81        ForwardPath::try_from(self.0.as_str())
82    }
83}
84
85impl TryInto<ReversePath> for EnvelopeAddress {
86    type Error = String;
87    fn try_into(self) -> Result<ReversePath, Self::Error> {
88        ReversePath::try_from(self.0.as_str())
89    }
90}
91
92#[cfg(feature = "impl")]
93impl UserData for EnvelopeAddress {
94    fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
95        fields.add_field_method_get("user", |_, this| Ok(this.user().to_string()));
96        fields.add_field_method_get("domain", |_, this| Ok(this.domain().to_string()));
97        fields.add_field_method_get("email", |_, this| Ok(this.0.to_string()));
98    }
99
100    fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
101        methods.add_meta_method(MetaMethod::ToString, |_, this, _: ()| {
102            Ok(this.0.to_string())
103        });
104    }
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
108pub struct HeaderAddressList(Vec<HeaderAddressEntry>);
109
110impl HeaderAddressList {
111    /// If the address list is comprised of a single entry,
112    /// returns just the email domain from that entry
113    pub fn domain(&self) -> anyhow::Result<&str> {
114        let (_local, domain) = self.single_address_cracked()?;
115        Ok(domain)
116    }
117
118    /// If the address list is comprised of a single entry,
119    /// returns just the email domain from that entry
120    pub fn user(&self) -> anyhow::Result<&str> {
121        let (user, _domain) = self.single_address_cracked()?;
122        Ok(user)
123    }
124
125    /// If the address list is comprised of a single entry,
126    /// returns just the display name portion, if any
127    pub fn name(&self) -> anyhow::Result<Option<&str>> {
128        let addr = self.single_address()?;
129        Ok(addr.name.as_deref())
130    }
131
132    pub fn email(&self) -> anyhow::Result<Option<&str>> {
133        let addr = self.single_address()?;
134        Ok(addr.address.as_deref())
135    }
136
137    /// Flattens the groups and list and returns a simple list
138    /// of addresses
139    pub fn flatten(&self) -> Vec<&HeaderAddress> {
140        let mut res = vec![];
141        for entry in &self.0 {
142            match entry {
143                HeaderAddressEntry::Address(a) => res.push(a),
144                HeaderAddressEntry::Group(group) => {
145                    for addr in &group.addresses {
146                        res.push(addr);
147                    }
148                }
149            }
150        }
151        res
152    }
153
154    pub fn single_address_cracked(&self) -> anyhow::Result<(&str, &str)> {
155        let addr = self.single_address_string()?;
156        let tuple = addr
157            .split_once('@')
158            .ok_or_else(|| anyhow::anyhow!("no @ in address"))?;
159        Ok(tuple)
160    }
161
162    pub fn single_address_string(&self) -> anyhow::Result<&str> {
163        let addr = self.single_address()?;
164        match &addr.address {
165            None => anyhow::bail!("no address"),
166            Some(addr) => Ok(addr),
167        }
168    }
169
170    pub fn single_address(&self) -> anyhow::Result<&HeaderAddress> {
171        match self.0.len() {
172            0 => anyhow::bail!("no addresses"),
173            1 => match &self.0[0] {
174                HeaderAddressEntry::Address(a) => Ok(a),
175                _ => anyhow::bail!("is not a simple address"),
176            },
177            _ => anyhow::bail!("is not a simple single address"),
178        }
179    }
180}
181
182impl From<AddressList> for HeaderAddressList {
183    fn from(input: AddressList) -> HeaderAddressList {
184        let addresses: Vec<HeaderAddressEntry> = input.0.iter().map(Into::into).collect();
185        HeaderAddressList(addresses)
186    }
187}
188
189#[cfg(feature = "impl")]
190impl UserData for HeaderAddressList {
191    fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
192        fields.add_field_method_get("user", |_, this| {
193            Ok(this.user().map_err(any_err)?.to_string())
194        });
195        fields.add_field_method_get("domain", |_, this| {
196            Ok(this.domain().map_err(any_err)?.to_string())
197        });
198        fields.add_field_method_get("email", |_, this| {
199            Ok(this.email().map_err(any_err)?.map(|s| s.to_string()))
200        });
201        fields.add_field_method_get("name", |_, this| {
202            Ok(this.name().map_err(any_err)?.map(|s| s.to_string()))
203        });
204        fields.add_field_method_get("list", |_, this| {
205            Ok(this
206                .flatten()
207                .into_iter()
208                .cloned()
209                .collect::<Vec<HeaderAddress>>())
210        });
211    }
212
213    fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
214        methods.add_meta_method(MetaMethod::ToString, |_, this, _: ()| {
215            let json = serde_json::to_string(&this.0).map_err(any_err)?;
216            Ok(json)
217        });
218    }
219}
220
221#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
222pub enum HeaderAddressEntry {
223    Address(HeaderAddress),
224    Group(AddressGroup),
225}
226
227impl From<&Address> for HeaderAddressEntry {
228    fn from(addr: &Address) -> HeaderAddressEntry {
229        match addr {
230            Address::Mailbox(mbox) => HeaderAddressEntry::Address(mbox.into()),
231            Address::Group { name, entries } => {
232                let addresses = entries.0.iter().map(Into::into).collect();
233                HeaderAddressEntry::Group(AddressGroup {
234                    name: if name.is_empty() {
235                        None
236                    } else {
237                        Some(name.to_string())
238                    },
239                    addresses,
240                })
241            }
242        }
243    }
244}
245
246#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
247pub struct HeaderAddress {
248    pub name: Option<String>,
249    pub address: Option<String>,
250}
251
252impl From<&Mailbox> for HeaderAddress {
253    fn from(mbox: &Mailbox) -> HeaderAddress {
254        Self {
255            name: mbox.name.clone(),
256            address: Some(mbox.address.encode_value().to_string()),
257        }
258    }
259}
260
261impl HeaderAddress {
262    pub fn user(&self) -> anyhow::Result<&str> {
263        let (user, _domain) = self.crack_address().map_err(any_err)?;
264        Ok(user)
265    }
266    pub fn domain(&self) -> anyhow::Result<&str> {
267        let (_user, domain) = self.crack_address().map_err(any_err)?;
268        Ok(domain)
269    }
270    pub fn email(&self) -> Option<&str> {
271        self.address.as_deref()
272    }
273    pub fn name(&self) -> Option<&str> {
274        self.name.as_deref()
275    }
276
277    pub fn crack_address(&self) -> anyhow::Result<(&str, &str)> {
278        let address = self
279            .address
280            .as_ref()
281            .ok_or_else(|| anyhow::anyhow!("no address"))?;
282
283        address
284            .split_once('@')
285            .ok_or_else(|| anyhow::anyhow!("no @ in address"))
286    }
287}
288
289#[cfg(feature = "impl")]
290impl UserData for HeaderAddress {
291    fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
292        fields.add_field_method_get("user", |_, this| {
293            Ok(this.user().map_err(any_err)?.to_string())
294        });
295        fields.add_field_method_get("domain", |_, this| {
296            Ok(this.domain().map_err(any_err)?.to_string())
297        });
298        fields.add_field_method_get("email", |_, this| Ok(this.email().map(|s| s.to_string())));
299        fields.add_field_method_get("name", |_, this| Ok(this.name().map(|s| s.to_string())));
300    }
301    fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
302        methods.add_meta_method(MetaMethod::ToString, |_, this, _: ()| {
303            let json = serde_json::to_string(&this).map_err(any_err)?;
304            Ok(json)
305        });
306    }
307}
308
309#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
310pub struct AddressGroup {
311    pub name: Option<String>,
312    pub addresses: Vec<HeaderAddress>,
313}
314
315#[cfg(test)]
316mod test {
317    use super::*;
318
319    #[test]
320    fn no_ports_in_domain() {
321        k9::snapshot!(
322            EnvelopeAddress::parse("user@example.com:2025").unwrap_err(),
323            "EnvelopeAddress does not allow port numbers in the domain (example.com:2025)"
324        );
325    }
326}