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