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