mirror of
https://github.com/chatmail/core.git
synced 2026-04-21 07:26:29 +03:00
There are providers in the provider database that do not have servers specified. For such providers default list should be tried just like when configuring unknown providers.
373 lines
10 KiB
Rust
373 lines
10 KiB
Rust
//! [Provider database](https://providers.delta.chat/) module.
|
|
|
|
pub(crate) mod data;
|
|
|
|
use anyhow::Result;
|
|
use deltachat_contact_tools::EmailAddress;
|
|
use hickory_resolver::{config, AsyncResolver, TokioAsyncResolver};
|
|
|
|
use crate::config::Config;
|
|
use crate::context::Context;
|
|
use crate::provider::data::{PROVIDER_DATA, PROVIDER_IDS};
|
|
|
|
/// 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(())
|
|
}
|
|
}
|