From e987b13a5ccd98d28fb756f1bf41427c337750c4 Mon Sep 17 00:00:00 2001 From: "Roger G. Coram" Date: Mon, 17 Oct 2022 17:34:47 +0100 Subject: [PATCH] fix: split Microsoft 365/Hotmail functionality (#1204) * fix: split Microsoft 365/Hotmail functionality `hotmail` was previously behind the `headless` feature which, if disabled, breaks the Microsoft 365 API functionality. refactor these into a `microsoft` module, leaving the `hotmail` module behind the `headless` feature and allowing the Microsoft 365 API to exist separately. * fix: explanation of Microsoft365Error Co-authored-by: Amaury <1293565+amaurym@users.noreply.github.com> Co-authored-by: Amaury <1293565+amaurym@users.noreply.github.com> --- core/src/smtp/error.rs | 11 ++- core/src/smtp/{ => microsoft}/hotmail.rs | 81 +---------------- core/src/smtp/microsoft/microsoft365.rs | 106 +++++++++++++++++++++++ core/src/smtp/microsoft/mod.rs | 3 + core/src/smtp/mod.rs | 7 +- 5 files changed, 124 insertions(+), 84 deletions(-) rename core/src/smtp/{ => microsoft}/hotmail.rs (72%) create mode 100644 core/src/smtp/microsoft/microsoft365.rs create mode 100644 core/src/smtp/microsoft/mod.rs diff --git a/core/src/smtp/error.rs b/core/src/smtp/error.rs index 2c302dfb1..808b7afcf 100644 --- a/core/src/smtp/error.rs +++ b/core/src/smtp/error.rs @@ -16,7 +16,8 @@ use super::gmail::GmailError; #[cfg(feature = "headless")] -use super::hotmail::HotmailError; +use super::microsoft::hotmail::HotmailError; +use super::microsoft::microsoft365::Microsoft365Error; use super::parser; use super::yahoo::YahooError; use crate::util::ser_with_display::ser_with_display; @@ -45,6 +46,8 @@ pub enum SmtpError { /// Error when verifying a Hotmail email via headless browser. #[cfg(feature = "headless")] HotmailError(HotmailError), + /// Error when verifying a Microsoft 365 email via HTTP request. + Microsoft365Error(Microsoft365Error), } impl From for SmtpError { @@ -78,6 +81,12 @@ impl From for SmtpError { } } +impl From for SmtpError { + fn from(e: Microsoft365Error) -> Self { + SmtpError::Microsoft365Error(e) + } +} + impl SmtpError { /// Get a human-understandable description of the error, in form of an enum /// SmtpErrorDesc. This only parses the following known errors: diff --git a/core/src/smtp/hotmail.rs b/core/src/smtp/microsoft/hotmail.rs similarity index 72% rename from core/src/smtp/hotmail.rs rename to core/src/smtp/microsoft/hotmail.rs index 85f74d90e..00a424a59 100644 --- a/core/src/smtp/hotmail.rs +++ b/core/src/smtp/microsoft/hotmail.rs @@ -23,15 +23,10 @@ use fantoccini::{ ClientBuilder, Locator, }; use futures::TryFutureExt; -use reqwest::Error as ReqwestError; use serde::Serialize; use serde_json::Map; -use super::SmtpDetails; -use crate::LOG_TARGET; -use crate::{ - smtp::http_api::create_client, util::ser_with_display::ser_with_display, CheckEmailInput, -}; +use crate::{smtp::SmtpDetails, util::ser_with_display::ser_with_display, LOG_TARGET}; #[derive(Debug, Serialize)] pub enum HotmailError { @@ -39,8 +34,6 @@ 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 { @@ -55,12 +48,6 @@ 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 @@ -151,65 +138,9 @@ 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 a HTTP request to verify if an Microsoft 365 email address exists. -/// -/// See -/// [this article]() -/// for details on the underlying principles. -/// -/// Note that a positive response from this function is (at present) considered -/// a reliable indicator that an email-address is valid. However, a negative -/// response is ambigious: the email address may or may not be valid but this -/// cannot be determined by the method outlined here. -pub async fn check_microsoft365_api( - to_email: &EmailAddress, - input: &CheckEmailInput, -) -> Result, HotmailError> { - let url = get_onedrive_url(to_email.as_ref()); - - let response = create_client(input, "microsoft365")? - .head(url) - .send() - .await?; - - log::debug!( - target: LOG_TARGET, - "[email={}] microsoft365 response: {:?}", - to_email, - response - ); - - if response.status() == 403 { - Ok(Some(SmtpDetails { - can_connect_smtp: true, - is_deliverable: true, - ..Default::default() - })) - } else { - Ok(None) - } -} - #[cfg(test)] mod tests { - use super::{check_password_recovery, get_onedrive_url}; + use super::check_password_recovery; use async_smtp::EmailAddress; use async_std::prelude::FutureExt; use std::str::FromStr; @@ -260,12 +191,4 @@ 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/microsoft/microsoft365.rs b/core/src/smtp/microsoft/microsoft365.rs new file mode 100644 index 000000000..d9d98cf5a --- /dev/null +++ b/core/src/smtp/microsoft/microsoft365.rs @@ -0,0 +1,106 @@ +// check-if-email-exists +// Copyright (C) 2018-2022 Reacher + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use async_smtp::EmailAddress; +use reqwest::Error as ReqwestError; +use serde::Serialize; + +use crate::{ + smtp::{http_api::create_client, SmtpDetails}, + util::ser_with_display::ser_with_display, + CheckEmailInput, LOG_TARGET, +}; + +#[derive(Debug, Serialize)] +pub enum Microsoft365Error { + #[serde(serialize_with = "ser_with_display")] + ReqwestError(ReqwestError), +} + +impl From for Microsoft365Error { + fn from(error: ReqwestError) -> Self { + Microsoft365Error::ReqwestError(error) + } +} + +/// 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 a HTTP request to verify if an Microsoft 365 email address exists. +/// +/// See +/// [this article]() +/// for details on the underlying principles. +/// +/// Note that a positive response from this function is (at present) considered +/// a reliable indicator that an email-address is valid. However, a negative +/// response is ambigious: the email address may or may not be valid but this +/// cannot be determined by the method outlined here. +pub async fn check_microsoft365_api( + to_email: &EmailAddress, + input: &CheckEmailInput, +) -> Result, Microsoft365Error> { + let url = get_onedrive_url(to_email.as_ref()); + + let response = create_client(input, "microsoft365")? + .head(url) + .send() + .await?; + + log::debug!( + target: LOG_TARGET, + "[email={}] microsoft365 response: {:?}", + to_email, + response + ); + + if response.status() == 403 { + Ok(Some(SmtpDetails { + can_connect_smtp: true, + is_deliverable: true, + ..Default::default() + })) + } else { + Ok(None) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[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/microsoft/mod.rs b/core/src/smtp/microsoft/mod.rs new file mode 100644 index 000000000..35cff51f9 --- /dev/null +++ b/core/src/smtp/microsoft/mod.rs @@ -0,0 +1,3 @@ +#[cfg(feature = "headless")] +pub mod hotmail; +pub mod microsoft365; diff --git a/core/src/smtp/mod.rs b/core/src/smtp/mod.rs index e0230a693..dd7597882 100644 --- a/core/src/smtp/mod.rs +++ b/core/src/smtp/mod.rs @@ -17,9 +17,8 @@ mod connect; mod error; mod gmail; -#[cfg(feature = "headless")] -mod hotmail; mod http_api; +mod microsoft; mod parser; mod yahoo; @@ -70,7 +69,7 @@ pub async fn check_smtp( .map_err(|err| err.into()); } if input.microsoft365_use_api && host_lowercase.ends_with(".mail.protection.outlook.com.") { - match hotmail::check_microsoft365_api(to_email, input).await { + match microsoft::microsoft365::check_microsoft365_api(to_email, input).await { Ok(Some(smtp_details)) => return Ok(smtp_details), // Continue in the event of an error/ambiguous result. Err(err) => { @@ -98,7 +97,7 @@ pub async fn check_smtp( // // So it seems that outlook/hotmail addresses end with `olc.protection.outlook.com.` if host_lowercase.ends_with("olc.protection.outlook.com.") { - return hotmail::check_password_recovery(to_email, webdriver) + return microsoft::hotmail::check_password_recovery(to_email, webdriver) .await .map_err(|err| err.into()); }