1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
//! [Provider database](https://providers.delta.chat/) module.

mod data;

use anyhow::Result;
use hickory_resolver::{config, AsyncResolver, TokioAsyncResolver};

use crate::config::Config;
use crate::context::Context;
use crate::provider::data::{PROVIDER_DATA, PROVIDER_IDS};
use crate::tools::EmailAddress;

/// Provider status according to manual testing.
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[repr(u8)]
pub enum Status {
    /// Provider is known to be working with Delta Chat.
    Ok = 1,

    /// Provider works with Delta Chat, but requires some preparation,
    /// such as changing the settings in the web interface.
    Preparation = 2,

    /// Provider is known not to work with Delta Chat.
    Broken = 3,
}

/// Server protocol.
#[derive(Debug, Display, PartialEq, Eq, Copy, Clone, FromPrimitive, ToPrimitive)]
#[repr(u8)]
pub enum Protocol {
    /// SMTP protocol.
    Smtp = 1,

    /// IMAP protocol.
    Imap = 2,
}

/// Socket security.
#[derive(Debug, Default, Display, PartialEq, Eq, Copy, Clone, FromPrimitive, ToPrimitive)]
#[repr(u8)]
pub enum Socket {
    /// Unspecified socket security, select automatically.
    #[default]
    Automatic = 0,

    /// TLS connection.
    Ssl = 1,

    /// STARTTLS connection.
    Starttls = 2,

    /// No TLS, plaintext connection.
    Plain = 3,
}

/// Pattern used to construct login usernames from email addresses.
#[derive(Debug, PartialEq, Eq, Clone)]
#[repr(u8)]
pub enum UsernamePattern {
    /// Whole email is used as username.
    Email = 1,

    /// Part of address before `@` is used as username.
    Emaillocalpart = 2,
}

/// Type of OAuth 2 authorization.
#[derive(Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum Oauth2Authorizer {
    /// Yandex.
    Yandex = 1,

    /// Gmail.
    Gmail = 2,
}

/// Email server endpoint.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Server {
    /// Server protocol, e.g. SMTP or IMAP.
    pub protocol: Protocol,

    /// Port security, e.g. TLS or STARTTLS.
    pub socket: Socket,

    /// Server host.
    pub hostname: &'static str,

    /// Server port.
    pub port: u16,

    /// Pattern used to construct login usernames from email addresses.
    pub username_pattern: UsernamePattern,
}

/// Pair of key and value for default configuration.
#[derive(Debug, PartialEq, Eq)]
pub struct ConfigDefault {
    /// Configuration variable name.
    pub key: Config,

    /// Configuration variable value.
    pub value: &'static str,
}

/// Provider database entry.
#[derive(Debug, PartialEq, Eq)]
pub struct Provider {
    /// Unique ID, corresponding to provider database filename.
    pub id: &'static str,

    /// Provider status according to manual testing.
    pub status: Status,

    /// Hint to be shown to the user on the login screen.
    pub before_login_hint: &'static str,

    /// Hint to be added to the device chat after provider configuration.
    pub after_login_hint: &'static str,

    /// URL of the page with provider overview.
    pub overview_page: &'static str,

    /// List of provider servers.
    pub server: &'static [Server],

    /// Default configuration values to set when provider is configured.
    pub config_defaults: Option<&'static [ConfigDefault]>,

    /// Type of OAuth 2 authorization if provider supports it.
    pub oauth2_authorizer: Option<Oauth2Authorizer>,

    /// Options with good defaults.
    pub opt: ProviderOptions,
}

/// Provider options with good defaults.
#[derive(Debug, PartialEq, Eq)]
pub struct ProviderOptions {
    /// True if provider is known to use use proper,
    /// not self-signed certificates.
    pub strict_tls: bool,

    /// Maximum number of recipients the provider allows to send a single email to.
    pub max_smtp_rcpt_to: Option<u16>,

    /// Move messages to the Trash folder instead of marking them "\Deleted".
    pub delete_to_trash: bool,
}

impl ProviderOptions {
    const fn new() -> Self {
        Self {
            strict_tls: true,
            max_smtp_rcpt_to: None,
            delete_to_trash: false,
        }
    }
}

/// Get resolver to query MX records.
///
/// We first try to read the system's resolver from `/etc/resolv.conf`.
/// This does not work at least on some Androids, therefore we fallback
/// to the default `ResolverConfig` which uses eg. to google's `8.8.8.8` or `8.8.4.4`.
fn get_resolver() -> Result<TokioAsyncResolver> {
    if let Ok(resolver) = AsyncResolver::tokio_from_system_conf() {
        return Ok(resolver);
    }
    let resolver = AsyncResolver::tokio(
        config::ResolverConfig::default(),
        config::ResolverOpts::default(),
    );
    Ok(resolver)
}

/// Returns provider for the given an e-mail address.
///
/// Returns an error if provided address is not valid.
pub async fn get_provider_info_by_addr(
    context: &Context,
    addr: &str,
    skip_mx: bool,
) -> Result<Option<&'static Provider>> {
    let addr = EmailAddress::new(addr)?;

    let provider = get_provider_info(context, &addr.domain, skip_mx).await;
    Ok(provider)
}

/// Returns provider for the given domain.
///
/// This function looks up domain in offline database first. If not
/// found, it queries MX record for the domain and looks up offline
/// database for MX domains.
pub async fn get_provider_info(
    context: &Context,
    domain: &str,
    skip_mx: bool,
) -> Option<&'static Provider> {
    if let Some(provider) = get_provider_by_domain(domain) {
        return Some(provider);
    }

    if !skip_mx {
        if let Some(provider) = get_provider_by_mx(context, domain).await {
            return Some(provider);
        }
    }

    None
}

/// Finds a provider in offline database based on domain.
pub fn get_provider_by_domain(domain: &str) -> Option<&'static Provider> {
    let domain = domain.to_lowercase();
    for (pattern, provider) in PROVIDER_DATA {
        if let Some(suffix) = pattern.strip_prefix('*') {
            // Wildcard domain pattern.
            //
            // For example, `suffix` is ".hermes.radio" for "*.hermes.radio" pattern.
            if domain.ends_with(suffix) {
                return Some(provider);
            }
        } else if pattern == domain {
            return Some(provider);
        }
    }

    None
}

/// Finds a provider based on MX record for the given domain.
///
/// For security reasons, only Gmail can be configured this way.
pub async fn get_provider_by_mx(context: &Context, domain: &str) -> Option<&'static Provider> {
    let Ok(resolver) = get_resolver() else {
        warn!(context, "Cannot get a resolver to check MX records.");
        return None;
    };

    let mut fqdn: String = domain.to_string();
    if !fqdn.ends_with('.') {
        fqdn.push('.');
    }

    let Ok(mx_domains) = resolver.mx_lookup(fqdn).await else {
        warn!(context, "Cannot resolve MX records for {domain:?}.");
        return None;
    };

    for (provider_domain_pattern, provider) in PROVIDER_DATA {
        if provider.id != "gmail" {
            // MX lookup is limited to Gmail for security reasons
            continue;
        }

        if provider_domain_pattern.starts_with('*') {
            // Skip wildcard patterns.
            continue;
        }

        let provider_fqdn = provider_domain_pattern.to_string() + ".";
        let provider_fqdn_dot = ".".to_string() + &provider_fqdn;

        for mx_domain in mx_domains.iter() {
            let mx_domain = mx_domain.exchange().to_lowercase().to_utf8();

            if mx_domain == provider_fqdn || mx_domain.ends_with(&provider_fqdn_dot) {
                return Some(provider);
            }
        }
    }

    None
}

/// Returns a provider with the given ID from the database.
pub fn get_provider_by_id(id: &str) -> Option<&'static Provider> {
    if let Some(provider) = PROVIDER_IDS.get(id) {
        Some(provider)
    } else {
        None
    }
}

#[cfg(test)]
mod tests {
    #![allow(clippy::indexing_slicing)]

    use super::*;
    use crate::test_utils::TestContext;

    #[test]
    fn test_get_provider_by_domain_unexistant() {
        let provider = get_provider_by_domain("unexistant.org");
        assert!(provider.is_none());
    }

    #[test]
    fn test_get_provider_by_domain_mixed_case() {
        let provider = get_provider_by_domain("nAUta.Cu").unwrap();
        assert!(provider.status == Status::Ok);
    }

    #[test]
    fn test_get_provider_by_domain() {
        let addr = "nauta.cu";
        let provider = get_provider_by_domain(addr).unwrap();
        assert!(provider.status == Status::Ok);
        let server = &provider.server[0];
        assert_eq!(server.protocol, Protocol::Imap);
        assert_eq!(server.socket, Socket::Starttls);
        assert_eq!(server.hostname, "imap.nauta.cu");
        assert_eq!(server.port, 143);
        assert_eq!(server.username_pattern, UsernamePattern::Email);
        let server = &provider.server[1];
        assert_eq!(server.protocol, Protocol::Smtp);
        assert_eq!(server.socket, Socket::Starttls);
        assert_eq!(server.hostname, "smtp.nauta.cu");
        assert_eq!(server.port, 25);
        assert_eq!(server.username_pattern, UsernamePattern::Email);

        let provider = get_provider_by_domain("gmail.com").unwrap();
        assert!(provider.status == Status::Preparation);
        assert!(!provider.before_login_hint.is_empty());
        assert!(!provider.overview_page.is_empty());

        let provider = get_provider_by_domain("googlemail.com").unwrap();
        assert!(provider.status == Status::Preparation);
    }

    #[test]
    fn test_get_provider_by_id() {
        let provider = get_provider_by_id("gmail").unwrap();
        assert!(provider.id == "gmail");
    }

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_get_provider_info() {
        let t = TestContext::new().await;
        assert!(get_provider_info(&t, "", false).await.is_none());
        assert!(get_provider_info(&t, "google.com", false).await.unwrap().id == "gmail");
        assert!(get_provider_info(&t, "example@google.com", false)
            .await
            .is_none());
    }

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_get_provider_info_by_addr() -> Result<()> {
        let t = TestContext::new().await;
        assert!(get_provider_info_by_addr(&t, "google.com", false)
            .await
            .is_err());
        assert!(
            get_provider_info_by_addr(&t, "example@google.com", false)
                .await?
                .unwrap()
                .id
                == "gmail"
        );
        Ok(())
    }

    #[test]
    fn test_get_resolver() -> Result<()> {
        assert!(get_resolver().is_ok());
        Ok(())
    }
}