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 }
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 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 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}