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