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 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 pub fn domain(&self) -> anyhow::Result<&str> {
98 let (_local, domain) = self.single_address_cracked()?;
99 Ok(domain)
100 }
101
102 pub fn user(&self) -> anyhow::Result<&str> {
105 let (user, _domain) = self.single_address_cracked()?;
106 Ok(user)
107 }
108
109 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 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}