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