deltachat/
provider.rs

1//! [Provider database](https://providers.delta.chat/) module.
2
3pub(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/// Provider status according to manual testing.
17#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
18#[repr(u8)]
19pub enum Status {
20    /// Provider is known to be working with Delta Chat.
21    Ok = 1,
22
23    /// Provider works with Delta Chat, but requires some preparation,
24    /// such as changing the settings in the web interface.
25    Preparation = 2,
26
27    /// Provider is known not to work with Delta Chat.
28    Broken = 3,
29}
30
31/// Server protocol.
32#[derive(Debug, Display, PartialEq, Eq, Copy, Clone, FromPrimitive, ToPrimitive)]
33#[repr(u8)]
34pub enum Protocol {
35    /// SMTP protocol.
36    Smtp = 1,
37
38    /// IMAP protocol.
39    Imap = 2,
40}
41
42/// Socket security.
43#[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    /// Unspecified socket security, select automatically.
59    #[default]
60    Automatic = 0,
61
62    /// TLS connection.
63    Ssl = 1,
64
65    /// STARTTLS connection.
66    Starttls = 2,
67
68    /// No TLS, plaintext connection.
69    Plain = 3,
70}
71
72/// Pattern used to construct login usernames from email addresses.
73#[derive(Debug, PartialEq, Eq, Clone)]
74#[repr(u8)]
75pub enum UsernamePattern {
76    /// Whole email is used as username.
77    Email = 1,
78
79    /// Part of address before `@` is used as username.
80    Emaillocalpart = 2,
81}
82
83/// Type of OAuth 2 authorization.
84#[derive(Debug, PartialEq, Eq)]
85#[repr(u8)]
86pub enum Oauth2Authorizer {
87    /// Yandex.
88    Yandex = 1,
89
90    /// Gmail.
91    Gmail = 2,
92}
93
94/// Email server endpoint.
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct Server {
97    /// Server protocol, e.g. SMTP or IMAP.
98    pub protocol: Protocol,
99
100    /// Port security, e.g. TLS or STARTTLS.
101    pub socket: Socket,
102
103    /// Server host.
104    pub hostname: &'static str,
105
106    /// Server port.
107    pub port: u16,
108
109    /// Pattern used to construct login usernames from email addresses.
110    pub username_pattern: UsernamePattern,
111}
112
113/// Pair of key and value for default configuration.
114#[derive(Debug, PartialEq, Eq)]
115pub struct ConfigDefault {
116    /// Configuration variable name.
117    pub key: Config,
118
119    /// Configuration variable value.
120    pub value: &'static str,
121}
122
123/// Provider database entry.
124#[derive(Debug, PartialEq, Eq)]
125pub struct Provider {
126    /// Unique ID, corresponding to provider database filename.
127    pub id: &'static str,
128
129    /// Provider status according to manual testing.
130    pub status: Status,
131
132    /// Hint to be shown to the user on the login screen.
133    pub before_login_hint: &'static str,
134
135    /// Hint to be added to the device chat after provider configuration.
136    pub after_login_hint: &'static str,
137
138    /// URL of the page with provider overview.
139    pub overview_page: &'static str,
140
141    /// List of provider servers.
142    pub server: &'static [Server],
143
144    /// Default configuration values to set when provider is configured.
145    pub config_defaults: Option<&'static [ConfigDefault]>,
146
147    /// Type of OAuth 2 authorization if provider supports it.
148    pub oauth2_authorizer: Option<Oauth2Authorizer>,
149
150    /// Options with good defaults.
151    pub opt: ProviderOptions,
152}
153
154/// Provider options with good defaults.
155#[derive(Debug, PartialEq, Eq)]
156pub struct ProviderOptions {
157    /// True if provider is known to use use proper,
158    /// not self-signed certificates.
159    pub strict_tls: bool,
160
161    /// Maximum number of recipients the provider allows to send a single email to.
162    pub max_smtp_rcpt_to: Option<u16>,
163
164    /// Move messages to the Trash folder instead of marking them "\Deleted".
165    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
178/// Get resolver to query MX records.
179///
180/// We first try to read the system's resolver from `/etc/resolv.conf`.
181/// This does not work at least on some Androids, therefore we fallback
182/// to the default `ResolverConfig` which uses eg. to google's `8.8.8.8` or `8.8.4.4`.
183fn 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
194/// Returns provider for the given an e-mail address.
195///
196/// Returns an error if provided address is not valid.
197pub 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
208/// Returns provider for the given domain.
209///
210/// This function looks up domain in offline database first. If not
211/// found, it queries MX record for the domain and looks up offline
212/// database for MX domains.
213pub 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
231/// Finds a provider in offline database based on domain.
232pub 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            // Wildcard domain pattern.
237            //
238            // For example, `suffix` is ".hermes.radio" for "*.hermes.radio" pattern.
239            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
250/// Finds a provider based on MX record for the given domain.
251///
252/// For security reasons, only Gmail can be configured this way.
253pub 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            // MX lookup is limited to Gmail for security reasons
272            continue;
273        }
274
275        if provider_domain_pattern.starts_with('*') {
276            // Skip wildcard patterns.
277            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
295/// Returns a provider with the given ID from the database.
296pub 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}