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};
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[repr(u8)]
pub enum Status {
Ok = 1,
Preparation = 2,
Broken = 3,
}
#[derive(Debug, Display, PartialEq, Eq, Copy, Clone, FromPrimitive, ToPrimitive)]
#[repr(u8)]
pub enum Protocol {
Smtp = 1,
Imap = 2,
}
#[derive(Debug, Default, Display, PartialEq, Eq, Copy, Clone, FromPrimitive, ToPrimitive)]
#[repr(u8)]
pub enum Socket {
#[default]
Automatic = 0,
Ssl = 1,
Starttls = 2,
Plain = 3,
}
#[derive(Debug, PartialEq, Eq, Clone)]
#[repr(u8)]
pub enum UsernamePattern {
Email = 1,
Emaillocalpart = 2,
}
#[derive(Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum Oauth2Authorizer {
Yandex = 1,
Gmail = 2,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Server {
pub protocol: Protocol,
pub socket: Socket,
pub hostname: &'static str,
pub port: u16,
pub username_pattern: UsernamePattern,
}
#[derive(Debug, PartialEq, Eq)]
pub struct ConfigDefault {
pub key: Config,
pub value: &'static str,
}
#[derive(Debug, PartialEq, Eq)]
pub struct Provider {
pub id: &'static str,
pub status: Status,
pub before_login_hint: &'static str,
pub after_login_hint: &'static str,
pub overview_page: &'static str,
pub server: &'static [Server],
pub config_defaults: Option<&'static [ConfigDefault]>,
pub oauth2_authorizer: Option<Oauth2Authorizer>,
pub opt: ProviderOptions,
}
#[derive(Debug, PartialEq, Eq)]
pub struct ProviderOptions {
pub strict_tls: bool,
pub max_smtp_rcpt_to: Option<u16>,
pub delete_to_trash: bool,
}
impl ProviderOptions {
const fn new() -> Self {
Self {
strict_tls: true,
max_smtp_rcpt_to: None,
delete_to_trash: false,
}
}
}
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)
}
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)
}
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
}
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('*') {
if domain.ends_with(suffix) {
return Some(provider);
}
} else if pattern == domain {
return Some(provider);
}
}
None
}
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" {
continue;
}
if provider_domain_pattern.starts_with('*') {
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
}
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 {
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(())
}
}