Skip to content

Commit

Permalink
fix: split Microsoft 365/Hotmail functionality (#1204)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
PsypherPunk and amaury1093 committed Oct 17, 2022
1 parent 5d3c49f commit e987b13
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 84 deletions.
11 changes: 10 additions & 1 deletion core/src/smtp/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<SocksError> for SmtpError {
Expand Down Expand Up @@ -78,6 +81,12 @@ impl From<HotmailError> for SmtpError {
}
}

impl From<Microsoft365Error> 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:
Expand Down
81 changes: 2 additions & 79 deletions core/src/smtp/hotmail.rs → core/src/smtp/microsoft/hotmail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,17 @@ 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 {
#[serde(serialize_with = "ser_with_display")]
Cmd(CmdError),
#[serde(serialize_with = "ser_with_display")]
NewSession(NewSessionError),
#[serde(serialize_with = "ser_with_display")]
ReqwestError(ReqwestError),
}

impl From<CmdError> for HotmailError {
Expand All @@ -55,12 +48,6 @@ impl From<NewSessionError> for HotmailError {
}
}

impl From<ReqwestError> 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
Expand Down Expand Up @@ -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](<https://www.trustedsec.com/blog/achieving-passive-user-enumeration-with-onedrive/>)
/// 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<Option<SmtpDetails>, 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;
Expand Down Expand Up @@ -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));
}
}
106 changes: 106 additions & 0 deletions core/src/smtp/microsoft/microsoft365.rs
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.

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<ReqwestError> 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](<https://www.trustedsec.com/blog/achieving-passive-user-enumeration-with-onedrive/>)
/// 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<Option<SmtpDetails>, 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));
}
}
3 changes: 3 additions & 0 deletions core/src/smtp/microsoft/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#[cfg(feature = "headless")]
pub mod hotmail;
pub mod microsoft365;
7 changes: 3 additions & 4 deletions core/src/smtp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,8 @@
mod connect;
mod error;
mod gmail;
#[cfg(feature = "headless")]
mod hotmail;
mod http_api;
mod microsoft;
mod parser;
mod yahoo;

Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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());
}
Expand Down

0 comments on commit e987b13

Please sign in to comment.