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::log::warn;
14use crate::provider::data::{PROVIDER_DATA, PROVIDER_IDS};
15
16#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
18#[repr(u8)]
19pub enum Status {
20 Ok = 1,
22
23 Preparation = 2,
26
27 Broken = 3,
29}
30
31#[derive(Debug, Display, PartialEq, Eq, Copy, Clone, FromPrimitive, ToPrimitive)]
33#[repr(u8)]
34pub enum Protocol {
35 Smtp = 1,
37
38 Imap = 2,
40}
41
42#[derive(
44 Debug,
45 Default,
46 Display,
47 PartialEq,
48 Eq,
49 Copy,
50 Clone,
51 FromPrimitive,
52 ToPrimitive,
53 Serialize,
54 Deserialize,
55)]
56#[repr(u8)]
57pub enum Socket {
58 #[default]
60 Automatic = 0,
61
62 Ssl = 1,
64
65 Starttls = 2,
67
68 Plain = 3,
70}
71
72#[derive(Debug, PartialEq, Eq, Clone)]
74#[repr(u8)]
75pub enum UsernamePattern {
76 Email = 1,
78
79 Emaillocalpart = 2,
81}
82
83#[derive(Debug, PartialEq, Eq)]
85#[repr(u8)]
86pub enum Oauth2Authorizer {
87 Yandex = 1,
89
90 Gmail = 2,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct Server {
97 pub protocol: Protocol,
99
100 pub socket: Socket,
102
103 pub hostname: &'static str,
105
106 pub port: u16,
108
109 pub username_pattern: UsernamePattern,
111}
112
113#[derive(Debug, PartialEq, Eq)]
115pub struct ConfigDefault {
116 pub key: Config,
118
119 pub value: &'static str,
121}
122
123#[derive(Debug, PartialEq, Eq)]
125pub struct Provider {
126 pub id: &'static str,
128
129 pub status: Status,
131
132 pub before_login_hint: &'static str,
134
135 pub after_login_hint: &'static str,
137
138 pub overview_page: &'static str,
140
141 pub server: &'static [Server],
143
144 pub config_defaults: Option<&'static [ConfigDefault]>,
146
147 pub oauth2_authorizer: Option<Oauth2Authorizer>,
149
150 pub opt: ProviderOptions,
152}
153
154#[derive(Debug, PartialEq, Eq)]
156pub struct ProviderOptions {
157 pub strict_tls: bool,
160
161 pub max_smtp_rcpt_to: Option<u16>,
163
164 pub delete_to_trash: bool,
166}
167
168impl ProviderOptions {
169 const fn new() -> Self {
170 Self {
171 strict_tls: true,
172 max_smtp_rcpt_to: None,
173 delete_to_trash: false,
174 }
175 }
176}
177
178fn get_resolver() -> Result<TokioResolver> {
184 if let Ok(resolver) = TokioResolver::builder_tokio() {
185 return Ok(resolver.build());
186 }
187 let resolver = Resolver::builder_with_config(
188 config::ResolverConfig::default(),
189 TokioConnectionProvider::default(),
190 );
191 Ok(resolver.build())
192}
193
194pub async fn get_provider_info_by_addr(
198 context: &Context,
199 addr: &str,
200 skip_mx: bool,
201) -> Result<Option<&'static Provider>> {
202 let addr = EmailAddress::new(addr)?;
203
204 let provider = get_provider_info(context, &addr.domain, skip_mx).await;
205 Ok(provider)
206}
207
208pub async fn get_provider_info(
214 context: &Context,
215 domain: &str,
216 skip_mx: bool,
217) -> Option<&'static Provider> {
218 if let Some(provider) = get_provider_by_domain(domain) {
219 return Some(provider);
220 }
221
222 if !skip_mx {
223 if let Some(provider) = get_provider_by_mx(context, domain).await {
224 return Some(provider);
225 }
226 }
227
228 None
229}
230
231pub fn get_provider_by_domain(domain: &str) -> Option<&'static Provider> {
233 let domain = domain.to_lowercase();
234 for (pattern, provider) in PROVIDER_DATA {
235 if let Some(suffix) = pattern.strip_prefix('*') {
236 if domain.ends_with(suffix) {
240 return Some(provider);
241 }
242 } else if pattern == domain {
243 return Some(provider);
244 }
245 }
246
247 None
248}
249
250pub async fn get_provider_by_mx(context: &Context, domain: &str) -> Option<&'static Provider> {
254 let Ok(resolver) = get_resolver() else {
255 warn!(context, "Cannot get a resolver to check MX records.");
256 return None;
257 };
258
259 let mut fqdn: String = domain.to_string();
260 if !fqdn.ends_with('.') {
261 fqdn.push('.');
262 }
263
264 let Ok(mx_domains) = resolver.mx_lookup(fqdn).await else {
265 warn!(context, "Cannot resolve MX records for {domain:?}.");
266 return None;
267 };
268
269 for (provider_domain_pattern, provider) in PROVIDER_DATA {
270 if provider.id != "gmail" {
271 continue;
273 }
274
275 if provider_domain_pattern.starts_with('*') {
276 continue;
278 }
279
280 let provider_fqdn = provider_domain_pattern.to_string() + ".";
281 let provider_fqdn_dot = ".".to_string() + &provider_fqdn;
282
283 for mx_domain in mx_domains.iter() {
284 let mx_domain = mx_domain.exchange().to_lowercase().to_utf8();
285
286 if mx_domain == provider_fqdn || mx_domain.ends_with(&provider_fqdn_dot) {
287 return Some(provider);
288 }
289 }
290 }
291
292 None
293}
294
295pub fn get_provider_by_id(id: &str) -> Option<&'static Provider> {
297 if let Some(provider) = PROVIDER_IDS.get(id) {
298 Some(provider)
299 } else {
300 None
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307 use crate::test_utils::TestContext;
308
309 #[test]
310 fn test_get_provider_by_domain_unexistant() {
311 let provider = get_provider_by_domain("unexistant.org");
312 assert!(provider.is_none());
313 }
314
315 #[test]
316 fn test_get_provider_by_domain_mixed_case() {
317 let provider = get_provider_by_domain("nAUta.Cu").unwrap();
318 assert!(provider.status == Status::Ok);
319 }
320
321 #[test]
322 fn test_get_provider_by_domain() {
323 let addr = "nauta.cu";
324 let provider = get_provider_by_domain(addr).unwrap();
325 assert!(provider.status == Status::Ok);
326 let server = &provider.server[0];
327 assert_eq!(server.protocol, Protocol::Imap);
328 assert_eq!(server.socket, Socket::Starttls);
329 assert_eq!(server.hostname, "imap.nauta.cu");
330 assert_eq!(server.port, 143);
331 assert_eq!(server.username_pattern, UsernamePattern::Email);
332 let server = &provider.server[1];
333 assert_eq!(server.protocol, Protocol::Smtp);
334 assert_eq!(server.socket, Socket::Starttls);
335 assert_eq!(server.hostname, "smtp.nauta.cu");
336 assert_eq!(server.port, 25);
337 assert_eq!(server.username_pattern, UsernamePattern::Email);
338
339 let provider = get_provider_by_domain("gmail.com").unwrap();
340 assert!(provider.status == Status::Preparation);
341 assert!(!provider.before_login_hint.is_empty());
342 assert!(!provider.overview_page.is_empty());
343
344 let provider = get_provider_by_domain("googlemail.com").unwrap();
345 assert!(provider.status == Status::Preparation);
346 }
347
348 #[test]
349 fn test_get_provider_by_id() {
350 let provider = get_provider_by_id("gmail").unwrap();
351 assert!(provider.id == "gmail");
352 }
353
354 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
355 async fn test_get_provider_info() {
356 let t = TestContext::new().await;
357 assert!(get_provider_info(&t, "", false).await.is_none());
358 assert!(get_provider_info(&t, "google.com", false).await.unwrap().id == "gmail");
359 assert!(get_provider_info(&t, "example@google.com", false)
360 .await
361 .is_none());
362 }
363
364 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
365 async fn test_get_provider_info_by_addr() -> Result<()> {
366 let t = TestContext::new().await;
367 assert!(get_provider_info_by_addr(&t, "google.com", false)
368 .await
369 .is_err());
370 assert!(
371 get_provider_info_by_addr(&t, "example@google.com", false)
372 .await?
373 .unwrap()
374 .id
375 == "gmail"
376 );
377 Ok(())
378 }
379
380 #[test]
381 fn test_get_resolver() -> Result<()> {
382 assert!(get_resolver().is_ok());
383 Ok(())
384 }
385}