diff --git a/cli/src/main.rs b/cli/src/main.rs index 6e00d92f1f..50283747eb 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -63,6 +63,11 @@ pub struct Cli { #[clap(long, env, default_value = "false", parse(try_from_str))] pub gmail_use_api: bool, + /// For Outlook/Office 365 email addresses, use Outlook's API instead of + /// connecting directly to their SMTP servers. + #[clap(long, env, default_value = "false", parse(try_from_str))] + pub outlook_use_api: bool, + /// Whether to check if a gravatar image is existing for the given email. #[clap(long, env, default_value = "false", parse(try_from_str))] pub check_gravatar: bool, @@ -87,6 +92,7 @@ async fn main() -> Result<(), Box> { .set_smtp_port(CONF.smtp_port) .set_yahoo_use_api(CONF.yahoo_use_api) .set_gmail_use_api(CONF.gmail_use_api) + .set_outlook_use_api(CONF.outlook_use_api) .set_check_gravatar(CONF.check_gravatar); if let Some(proxy_host) = &CONF.proxy_host { input.set_proxy(CheckEmailInputProxy { diff --git a/core/src/smtp/hotmail.rs b/core/src/smtp/hotmail.rs index 2819738da2..d8c5a449e2 100644 --- a/core/src/smtp/hotmail.rs +++ b/core/src/smtp/hotmail.rs @@ -23,12 +23,15 @@ use fantoccini::{ ClientBuilder, Locator, }; use futures::TryFutureExt; +use reqwest::Error as ReqwestError; use serde::Serialize; use serde_json::Map; use super::SmtpDetails; -use crate::util::ser_with_display::ser_with_display; use crate::LOG_TARGET; +use crate::{ + smtp::http_api::create_client, util::ser_with_display::ser_with_display, CheckEmailInput, +}; #[derive(Debug, Serialize)] pub enum HotmailError { @@ -36,6 +39,8 @@ pub enum HotmailError { Cmd(CmdError), #[serde(serialize_with = "ser_with_display")] NewSession(NewSessionError), + #[serde(serialize_with = "ser_with_display")] + ReqwestError(ReqwestError), } impl From for HotmailError { @@ -50,6 +55,12 @@ impl From for HotmailError { } } +impl From for HotmailError { + fn from(error: ReqwestError) -> Self { + HotmailError::ReqwestError(error) + } +} + /// Check if a Hotmail/Outlook email exists by connecting to the password /// recovery page https://account.live.com/password/reset using a headless /// browser. Make sure you have a WebDriver server running locally before @@ -140,9 +151,56 @@ pub async fn check_password_recovery( }) } +/// Convert an email address to its corresponding OneDrive URL. +fn get_onedrive_url(email_address: &str) -> String { + let (username, domain) = email_address + .split_once('@') + .expect("Email address syntax already validated."); + let (tenant, _) = domain + .split_once('.') + .expect("Email domain syntax already validated."); + + format!( + "https://{}-my.sharepoint.com/personal/{}_{}/_layouts/15/onedrive.aspx", + tenant, + username.replace('.', "_"), + domain.replace('.', "_"), + ) +} + +/// Use HTTP request to verify if an Outlook email address exists. +/// See: +pub async fn check_outlook_api( + to_email: &EmailAddress, + input: &CheckEmailInput, +) -> Result { + let url = get_onedrive_url(to_email.as_ref()); + + let response = create_client(input, "outlook")? + .head(url) + .query(&[("email", to_email)]) + .send() + .await?; + + let email_exists = response.status() == 403; + + log::debug!( + target: LOG_TARGET, + "[email={}] outlook response: {:?}", + to_email, + response + ); + + Ok(SmtpDetails { + can_connect_smtp: true, + is_deliverable: email_exists, + ..Default::default() + }) +} + #[cfg(test)] mod tests { - use super::check_password_recovery; + use super::{check_password_recovery, get_onedrive_url}; use async_smtp::EmailAddress; use async_std::prelude::FutureExt; use std::str::FromStr; @@ -193,4 +251,12 @@ mod tests { let f = f1.try_join(f2).await; assert!(f.is_ok(), "{:?}", f); } + + #[test] + fn test_onedrive_url() { + let email_address = "lightmand@acmecomputercompany.com"; + let expected = "https://acmecomputercompany-my.sharepoint.com/personal/lightmand_acmecomputercompany_com/_layouts/15/onedrive.aspx"; + + assert_eq!(expected, get_onedrive_url(email_address)); + } } diff --git a/core/src/smtp/mod.rs b/core/src/smtp/mod.rs index 20d14f856d..be01c569b6 100644 --- a/core/src/smtp/mod.rs +++ b/core/src/smtp/mod.rs @@ -69,6 +69,11 @@ pub async fn check_smtp( .await .map_err(|err| err.into()); } + if input.outlook_use_api && host_lowercase.ends_with(".outlook.com.") { + return hotmail::check_outlook_api(to_email, input) + .await + .map_err(|err| err.into()); + } #[cfg(feature = "headless")] if let Some(webdriver) = &input.hotmail_use_headless { if host_lowercase.contains("outlook") { diff --git a/core/src/util/input_output.rs b/core/src/util/input_output.rs index 9e668e3db0..b894fb2e2d 100644 --- a/core/src/util/input_output.rs +++ b/core/src/util/input_output.rs @@ -95,6 +95,11 @@ pub struct CheckEmailInput { /// /// Defaults to false. pub gmail_use_api: bool, + /// For Outlook/Office 365 email addresses, use Outlook's API instead of + /// connecting directly to their SMTP servers. + /// + /// Defaults to false. + pub outlook_use_api: bool, // Whether to check if a gravatar image is existing for the given email. // // Defaults to false @@ -132,6 +137,7 @@ impl Default for CheckEmailInput { smtp_timeout: None, yahoo_use_api: true, gmail_use_api: false, + outlook_use_api: false, check_gravatar: false, retries: 2, } @@ -247,6 +253,13 @@ impl CheckEmailInput { self } + /// Set whether to use Outlook's API or connecting directly to their SMTP + /// servers. Defaults to false. + pub fn set_outlook_use_api(&mut self, use_api: bool) -> &mut CheckEmailInput { + self.outlook_use_api = use_api; + self + } + /// Whether to check if a gravatar image is existing for the given email. /// Defaults to false. pub fn set_check_gravatar(&mut self, check_gravatar: bool) -> &mut CheckEmailInput {