message/
address.rs

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