1pub(crate) mod data;
4
5use anyhow::Result;
6use deltachat_contact_tools::EmailAddress;
7use hickory_resolver::{config, Resolver, TokioResolver};
8use serde::{Deserialize, Serialize};
9
10use crate::config::Config;
11use crate::context::Context;
12use crate::provider::data::{PROVIDER_DATA, PROVIDER_IDS};
13
14#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
16#[repr(u8)]
17pub enum Status {
18 Ok = 1,
20
21 Preparation = 2,
24
25 Broken = 3,
27}
28
29#[derive(Debug, Display, PartialEq, Eq, Copy, Clone, FromPrimitive, ToPrimitive)]
31#[repr(u8)]
32pub enum Protocol {
33 Smtp = 1,
35
36 Imap = 2,
38}
39
40#[derive(
42 Debug,
43 Default,
44 Display,
45 PartialEq,
46 Eq,
47 Copy,
48 Clone,
49 FromPrimitive,
50 ToPrimitive,
51 Serialize,
52 Deserialize,
53)]
54#[repr(u8)]
55pub enum Socket {
56 #[default]
58 Automatic = 0,
59
60 Ssl = 1,
62
63 Starttls = 2,
65
66 Plain = 3,
68}
69
70#[derive(Debug, PartialEq, Eq, Clone)]
72#[repr(u8)]
73pub enum UsernamePattern {
74 Email = 1,
76
77 Emaillocalpart = 2,
79}
80
81#[derive(Debug, PartialEq, Eq)]
83#[repr(u8)]
84pub enum Oauth2Authorizer {
85 Yandex = 1,
87
88 Gmail = 2,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct Server {
95 pub protocol: Protocol,
97
98 pub socket: Socket,
100
101 pub hostname: &'static str,
103
104 pub port: u16,
106
107 pub username_pattern: UsernamePattern,
109}
110
111#[derive(Debug, PartialEq, Eq)]
113pub struct ConfigDefault {
114 pub key: Config,
116
117 pub value: &'static str,
119}
120
121#[derive(Debug, PartialEq, Eq)]
123pub struct Provider {
124 pub id: &'static str,
126
127 pub status: Status,
129
130 pub before_login_hint: &'static str,
132
133 pub after_login_hint: &'static str,
135
136 pub overview_page: &'static str,
138
139 pub server: &'static [Server],
141
142 pub config_defaults: Option<&'static [ConfigDefault]>,
144
145 pub oauth2_authorizer: Option<Oauth2Authorizer>,
147
148 pub opt: ProviderOptions,
150}
151
152#[derive(Debug, PartialEq, Eq)]
154pub struct ProviderOptions {
155 pub strict_tls: bool,
158
159 pub max_smtp_rcpt_to: Option<u16>,
161
162 pub delete_to_trash: bool,
164}
165
166impl ProviderOptions {
167 const fn new() -> Self {
168 Self {
169 strict_tls: true,
170 max_smtp_rcpt_to: None,
171 delete_to_trash: false,
172 }
173 }
174}
175
176fn get_resolver() -> Result<TokioResolver> {
182 if let Ok(resolver) = Resolver::tokio_from_system_conf() {
183 return Ok(resolver);
184 }
185 let resolver = Resolver::tokio(
186 config::ResolverConfig::default(),
187 config::ResolverOpts::default(),
188 );
189 Ok(resolver)
190}
191
192pub async fn get_provider_info_by_addr(
196 context: &Context,
197 addr: &str,
198 skip_mx: bool,
199) -> Result<Option<&'static Provider>> {
200 let addr = EmailAddress::new(addr)?;
201
202 let provider = get_provider_info(context, &addr.domain, skip_mx).await;
203 Ok(provider)
204}
205
206pub async fn get_provider_info(
212 context: &Context,
213 domain: &str,
214 skip_mx: bool,
215) -> Option<&'static Provider> {
216 if let Some(provider) = get_provider_by_domain(domain) {
217 return Some(provider);
218 }
219
220 if !skip_mx {
221 if let Some(provider) = get_provider_by_mx(context, domain).await {
222 return Some(provider);
223 }
224 }
225
226 None
227}
228
229pub fn get_provider_by_domain(domain: &str) -> Option<&'static Provider> {
231 let domain = domain.to_lowercase();
232 for (pattern, provider) in PROVIDER_DATA {
233 if let Some(suffix) = pattern.strip_prefix('*') {
234 if domain.ends_with(suffix) {
238 return Some(provider);
239 }
240 } else if pattern == domain {
241 return Some(provider);
242 }
243 }
244
245 None
246}
247
248pub async fn get_provider_by_mx(context: &Context, domain: &str) -> Option<&'static Provider> {
252 let Ok(resolver) = get_resolver() else {
253 warn!(context, "Cannot get a resolver to check MX records.");
254 return None;
255 };
256
257 let mut fqdn: String = domain.to_string();
258 if !fqdn.ends_with('.') {
259 fqdn.push('.');
260 }
261
262 let Ok(mx_domains) = resolver.mx_lookup(fqdn).await else {
263 warn!(context, "Cannot resolve MX records for {domain:?}.");
264 return None;
265 };
266
267 for (provider_domain_pattern, provider) in PROVIDER_DATA {
268 if provider.id != "gmail" {
269 continue;
271 }
272
273 if provider_domain_pattern.starts_with('*') {
274 continue;
276 }
277
278 let provider_fqdn = provider_domain_pattern.to_string() + ".";
279 let provider_fqdn_dot = ".".to_string() + &provider_fqdn;
280
281 for mx_domain in mx_domains.iter() {
282 let mx_domain = mx_domain.exchange().to_lowercase().to_utf8();
283
284 if mx_domain == provider_fqdn || mx_domain.ends_with(&provider_fqdn_dot) {
285 return Some(provider);
286 }
287 }
288 }
289
290 None
291}
292
293pub fn get_provider_by_id(id: &str) -> Option<&'static Provider> {
295 if let Some(provider) = PROVIDER_IDS.get(id) {
296 Some(provider)
297 } else {
298 None
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305 use crate::test_utils::TestContext;
306
307 #[test]
308 fn test_get_provider_by_domain_unexistant() {
309 let provider = get_provider_by_domain("unexistant.org");
310 assert!(provider.is_none());
311 }
312
313 #[test]
314 fn test_get_provider_by_domain_mixed_case() {
315 let provider = get_provider_by_domain("nAUta.Cu").unwrap();
316 assert!(provider.status == Status::Ok);
317 }
318
319 #[test]
320 fn test_get_provider_by_domain() {
321 let addr = "nauta.cu";
322 let provider = get_provider_by_domain(addr).unwrap();
323 assert!(provider.status == Status::Ok);
324 let server = &provider.server[0];
325 assert_eq!(server.protocol, Protocol::Imap);
326 assert_eq!(server.socket, Socket::Starttls);
327 assert_eq!(server.hostname, "imap.nauta.cu");
328 assert_eq!(server.port, 143);
329 assert_eq!(server.username_pattern, UsernamePattern::Email);
330 let server = &provider.server[1];
331 assert_eq!(server.protocol, Protocol::Smtp);
332 assert_eq!(server.socket, Socket::Starttls);
333 assert_eq!(server.hostname, "smtp.nauta.cu");
334 assert_eq!(server.port, 25);
335 assert_eq!(server.username_pattern, UsernamePattern::Email);
336
337 let provider = get_provider_by_domain("gmail.com").unwrap();
338 assert!(provider.status == Status::Preparation);
339 assert!(!provider.before_login_hint.is_empty());
340 assert!(!provider.overview_page.is_empty());
341
342 let provider = get_provider_by_domain("googlemail.com").unwrap();
343 assert!(provider.status == Status::Preparation);
344 }
345
346 #[test]
347 fn test_get_provider_by_id() {
348 let provider = get_provider_by_id("gmail").unwrap();
349 assert!(provider.id == "gmail");
350 }
351
352 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
353 async fn test_get_provider_info() {
354 let t = TestContext::new().await;
355 assert!(get_provider_info(&t, "", false).await.is_none());
356 assert!(get_provider_info(&t, "google.com", false).await.unwrap().id == "gmail");
357 assert!(get_provider_info(&t, "example@google.com", false)
358 .await
359 .is_none());
360 }
361
362 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
363 async fn test_get_provider_info_by_addr() -> Result<()> {
364 let t = TestContext::new().await;
365 assert!(get_provider_info_by_addr(&t, "google.com", false)
366 .await
367 .is_err());
368 assert!(
369 get_provider_info_by_addr(&t, "example@google.com", false)
370 .await?
371 .unwrap()
372 .id
373 == "gmail"
374 );
375 Ok(())
376 }
377
378 #[test]
379 fn test_get_resolver() -> Result<()> {
380 assert!(get_resolver().is_ok());
381 Ok(())
382 }
383}