norm_email/
lib.rs

1#[macro_use]
2extern crate lazy_static;
3
4use std::convert::From;
5use std::fmt::Display;
6use std::fmt::Formatter;
7use trust_dns_resolver::config::*;
8use trust_dns_resolver::Resolver as MxResolver;
9
10use anyhow::Result as AResult;
11
12mod providers;
13
14use providers::Provider;
15use providers::PROVIDERS;
16
17#[derive(Debug)]
18pub struct MxRecord {
19    pub priority: u16,
20    pub host: String,
21}
22
23impl From<(u16, String)> for MxRecord {
24    fn from(a: (u16, String)) -> Self {
25        Self {
26            priority: a.0,
27            host: a.1,
28        }
29    }
30}
31
32impl Display for MxRecord {
33    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
34        write!(f, "Priority: {} Host: {}", self.priority, self.host)
35    }
36}
37
38pub struct LookupResult {
39    pub address: String,
40    pub normalized_address: String,
41    pub mailbox_provider: Option<String>,
42    pub mx_records: Vec<MxRecord>,
43}
44
45pub struct Normalizer {
46    resolver: trust_dns_resolver::Resolver,
47}
48
49use thiserror::Error;
50
51#[derive(Error, Debug)]
52pub enum NormalizerError {
53    #[error("Invalid {email:?} contains more than one @")]
54    InvalidEmailAt { email: String },
55}
56
57impl Default for Normalizer {
58    fn default() -> Self {
59        Self::new()
60    }
61}
62
63impl Normalizer {
64    pub fn new() -> Normalizer {
65        let opts = ResolverOpts {
66            ndots: 0,
67            ..ResolverOpts::default()
68        };
69        Normalizer {
70            resolver: MxResolver::new(ResolverConfig::google(), opts).unwrap(),
71        }
72    }
73
74    pub fn mx_records(&self, domain_name: &str) -> AResult<Vec<MxRecord>> {
75        let mx_records = self.resolver.mx_lookup(domain_name)?;
76        Ok(mx_records
77            .iter()
78            .map(|mx| (mx.preference(), mx.exchange().to_string()).into())
79            .collect::<Vec<MxRecord>>())
80    }
81
82    pub fn lookup_provider(mx_records: &[MxRecord]) -> Option<&'static Provider> {
83        for MxRecord { priority, host } in mx_records {
84            for &p in PROVIDERS.iter() {
85                for domain in p.mx_domains.iter() {
86                    let doted_domain = format!("{}{}", domain, '.');
87                    if host.ends_with(&doted_domain) {
88                        return Some(p);
89                    }
90                }
91            }
92        }
93        None
94    }
95
96    pub fn normalize(&self, email_address: &str) -> AResult<LookupResult> {
97        let (mut local, mut domain) = Normalizer::get_local_and_domain(email_address)?;
98        let mx_records = self.mx_records(&domain)?;
99        if let Some(provider) = Normalizer::lookup_provider(&mx_records[..]) {
100            let normalized_address;
101            if provider
102                .rules
103                .contains(providers::Rules::LocalPartAsHostName)
104            {
105                let (new_local_part, new_domain_part) =
106                    Normalizer::local_part_as_hostname(&local, &domain);
107                local = new_local_part;
108                domain = new_domain_part;
109                // normalized_address = format!("{}@{}", new_local_part, new_domain_part);
110            }
111
112            if provider.rules.contains(providers::Rules::DashAddressing) {
113                let local_parts = local.split('-').collect::<Vec<_>>();
114                if let Some(lp) = local_parts.first() {
115                    local = lp.to_string();
116                }
117            }
118
119            if provider.rules.contains(providers::Rules::PlusAddressing) {
120                let local_parts = local.split('+').collect::<Vec<_>>();
121                if let Some(lp) = local_parts.first() {
122                    local = lp.to_string();
123                }
124            }
125
126            if provider.rules.contains(providers::Rules::StripPeriods) {
127                let new_local = local.replace(".", "");
128                local = new_local;
129            }
130
131            normalized_address = format!("{}@{}", local, domain);
132            Result::Ok(LookupResult {
133                mailbox_provider: Some(provider.name.clone()),
134                mx_records,
135                address: email_address.into(),
136                normalized_address,
137            })
138        } else {
139            Result::Ok(LookupResult {
140                mailbox_provider: None,
141                mx_records,
142                address: email_address.into(),
143                normalized_address: email_address.into(),
144            })
145        }
146    }
147
148    pub fn get_local_and_domain(email_address: &str) -> Result<(String, String), NormalizerError> {
149        let parts = email_address.split('@').collect::<Vec<_>>();
150        if parts.len() != 2 {
151            Err(NormalizerError::InvalidEmailAt {
152                email: email_address.to_string(),
153            })
154        } else {
155            Result::Ok((
156                parts[0].to_string().to_lowercase(),
157                parts[1].to_string().to_lowercase(),
158            ))
159        }
160    }
161
162    pub fn local_part_as_hostname(local_part: &str, domain_part: &str) -> (String, String) {
163        let mut local_part_inner = local_part.to_string();
164        let mut domain_part_inner = domain_part.to_string();
165        let domain_splits = domain_part.split('.').collect::<Vec<_>>();
166        if domain_splits.len() > 2 {
167            local_part_inner = domain_splits[0].to_string();
168            domain_part_inner = domain_splits[1..].join(".");
169        }
170
171        (local_part_inner, domain_part_inner)
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::Normalizer;
178    use uuid::Uuid;
179
180    #[test]
181    fn mx_records() {
182        use super::Normalizer;
183        let n = Normalizer::new();
184        let records = n.mx_records("gmail.com").unwrap();
185        // for (a, b) in records {
186        //     println!("{} {}", a, b)
187        // }
188        assert!(true)
189    }
190
191    #[test]
192    fn lookup_provider() {
193        use super::Normalizer;
194        let n = Normalizer::new();
195        let provider =
196            Normalizer::lookup_provider(&n.mx_records("gmail.com").unwrap()[..]).unwrap();
197        // println!("{:?}", provider);
198        assert!(true)
199    }
200
201    #[test]
202    fn apple() {
203        let local = Uuid::new_v4();
204        let domain = "icloud.com";
205
206        let normalizer = Normalizer::new();
207        let result = normalizer
208            .normalize(&format!("{}+test@{}", local, domain))
209            .unwrap();
210        assert_eq!(result.normalized_address, format!("{}@{}", local, domain));
211    }
212
213    #[test]
214    fn gmail() {
215        let local = Uuid::new_v4();
216        let domain = "gmail.com";
217        let normalizer = Normalizer::new();
218        let result = normalizer
219            .normalize(&format!("{}+test@{}", local, domain))
220            .unwrap();
221        println!("{:?}", result.mx_records);
222        println!("{:?}", result.mailbox_provider);
223        assert_eq!(result.normalized_address, format!("{}@{}", local, domain));
224    }
225
226    #[test]
227    fn fastmail() {
228        let local = Uuid::new_v4();
229        let domain = "fastmail.com";
230        let normalizer = Normalizer::new();
231        let result = normalizer
232            .normalize(&format!("{}+test@{}", local, domain))
233            .unwrap();
234        println!("{:?}", result.mx_records);
235        println!("{:?}", result.mailbox_provider);
236        assert_eq!(result.normalized_address, format!("{}@{}", local, domain));
237    }
238
239    #[test]
240    fn fastmail_second() {
241        let local = Uuid::new_v4();
242        let domain_local = Uuid::new_v4();
243        let domain = "fastmail.com";
244        let normalizer = Normalizer::new();
245        let result = normalizer
246            .normalize(&format!("{}@{}.{}", local, domain_local, domain))
247            .unwrap();
248        println!("{:?}", result.mx_records);
249        println!("{:?}", result.mailbox_provider);
250        assert_eq!(
251            result.normalized_address,
252            format!("{}@{}", domain_local, domain)
253        );
254    }
255
256    #[test]
257    fn yahoo() {
258        let normalizer = Normalizer::new();
259        let result = normalizer.normalize("a.b.c.d+tag@yahoo.com").unwrap();
260        assert_eq!("a.b.c.d+tag@yahoo.com", result.normalized_address);
261    }
262
263    #[test]
264    fn yahoo_second() {
265        let normalizer = Normalizer::new();
266        let result = normalizer.normalize("a-b.c-tag@yahoo.ro").unwrap();
267        assert_eq!("a@yahoo.ro", result.normalized_address);
268    }
269
270    #[test]
271    fn microsoft_first() {
272        let normalizer = Normalizer::new();
273        let result = normalizer.normalize("a.b.c+tag@outlook.com").unwrap();
274        assert_eq!("a.b.c@outlook.com", result.normalized_address);
275    }
276
277    #[test]
278    fn microsoft_second() {
279        let normalizer = Normalizer::new();
280        let result = normalizer.normalize("a.b.c-tag@outlook.com").unwrap();
281        assert_eq!("a.b.c-tag@outlook.com", result.normalized_address);
282    }
283}