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