Query MX records during provider autoconfiguration

Previously MX records were queried only for OAuth 2 configuration and
did not affect the list of servers tried. User was required to manually
configure the servers for Google Workspace (former GSuite) domains.

Now MX records are queried during configuration. If provider is found in
offline database, its ID, corresponding to the filename, is saved as
`configured_provider`.

`configured_provider` is also set during database migration if email
address uses the domain from the provider database, but no MX querying
is done.
This commit is contained in:
Alexander Krotov
2020-12-27 03:49:11 +03:00
committed by link2xt
parent 6edff503aa
commit f4c8ffca4c
13 changed files with 367 additions and 165 deletions

View File

@@ -3,8 +3,8 @@
mod data;
use crate::config::Config;
use crate::dc_tools::EmailAddress;
use crate::provider::data::{PROVIDER_DATA, PROVIDER_UPDATED};
use crate::provider::data::{PROVIDER_DATA, PROVIDER_IDS, PROVIDER_UPDATED};
use async_std_resolver::{config, resolver};
use chrono::{NaiveDateTime, NaiveTime};
#[derive(Debug, Display, Copy, Clone, PartialEq, FromPrimitive, ToPrimitive)]
@@ -68,6 +68,8 @@ pub struct ConfigDefault {
#[derive(Debug)]
pub struct Provider {
/// Unique ID, corresponding to provider database filename.
pub id: &'static str,
pub status: Status,
pub before_login_hint: &'static str,
pub after_login_hint: &'static str,
@@ -79,20 +81,84 @@ pub struct Provider {
pub oauth2_authorizer: Option<Oauth2Authorizer>,
}
pub fn get_provider_info(addr: &str) -> Option<&Provider> {
let domain = match addr.parse::<EmailAddress>() {
Ok(addr) => addr.domain,
Err(_err) => return None,
}
.to_lowercase();
/// 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.
///
/// For compatibility, email address can be passed to this function
/// instead of the domain.
pub async fn get_provider_info(domain: &str) -> Option<&'static Provider> {
let domain = domain.rsplitn(2, '@').next()?;
if let Some(provider) = PROVIDER_DATA.get(domain.as_str()) {
if let Some(provider) = get_provider_by_domain(domain) {
return Some(provider);
}
if let Some(provider) = get_provider_by_mx(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> {
if let Some(provider) = PROVIDER_DATA.get(domain.to_lowercase().as_str()) {
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(domain: impl AsRef<str>) -> Option<&'static Provider> {
if let Ok(resolver) = resolver(
config::ResolverConfig::default(),
config::ResolverOpts::default(),
)
.await
{
let mut fqdn: String = String::from(domain.as_ref());
if !fqdn.ends_with('.') {
fqdn.push('.');
}
if let Ok(mx_domains) = resolver.mx_lookup(fqdn).await {
for (provider_domain, provider) in PROVIDER_DATA.iter() {
if provider.id != "gmail" {
// MX lookup is limited to Gmail for security reasons
continue;
}
let provider_fqdn = provider_domain.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
}
}
// returns update timestamp in seconds, UTC, compatible for comparison with time() and database times
pub fn get_provider_update_timestamp() -> i64 {
NaiveDateTime::new(*PROVIDER_UPDATED, NaiveTime::from_hms(0, 0, 0)).timestamp_millis() / 1_000
@@ -107,24 +173,21 @@ mod tests {
use chrono::NaiveDate;
#[test]
fn test_get_provider_info_unexistant() {
let provider = get_provider_info("user@unexistant.org");
fn test_get_provider_by_domain_unexistant() {
let provider = get_provider_by_domain("unexistant.org");
assert!(provider.is_none());
}
#[test]
fn test_get_provider_info_mixed_case() {
let provider = get_provider_info("uSer@nAUta.Cu").unwrap();
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_info() {
let provider = get_provider_info("nauta.cu"); // this is no email address
assert!(provider.is_none());
let addr = "user@nauta.cu";
let provider = get_provider_info(addr).unwrap();
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);
@@ -139,15 +202,30 @@ mod tests {
assert_eq!(server.port, 25);
assert_eq!(server.username_pattern, UsernamePattern::EMAIL);
let provider = get_provider_info("user@gmail.com").unwrap();
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_info("user@googlemail.com").unwrap();
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");
}
#[async_std::test]
async fn test_get_provider_info() {
assert!(get_provider_info("").await.is_none());
assert!(get_provider_info("google.com").await.unwrap().id == "gmail");
// get_provider_info() accepts email addresses for backwards compatibility
assert!(get_provider_info("example@google.com").await.unwrap().id == "gmail");
}
#[test]
fn test_get_provider_update_timestamp() {
let timestamp_past = NaiveDateTime::new(