mod connect;
mod error;
mod gmail;
mod http_api;
mod outlook;
mod parser;
mod yahoo;
use std::default::Default;
use async_smtp::EmailAddress;
use serde::{Deserialize, Serialize};
use trust_dns_proto::rr::Name;
use crate::{util::input_output::CheckEmailInput, LOG_TARGET};
use connect::check_smtp_with_retry;
pub use error::*;
use self::{
gmail::is_gmail,
outlook::{is_microsoft365, is_outlook},
yahoo::is_yahoo,
};
#[derive(Debug, Default, Deserialize, Serialize)]
pub struct SmtpDetails {
pub can_connect_smtp: bool,
pub has_full_inbox: bool,
pub is_catch_all: bool,
pub is_deliverable: bool,
pub is_disabled: bool,
}
pub async fn check_smtp(
to_email: &EmailAddress,
host: &Name,
port: u16,
domain: &str,
input: &CheckEmailInput,
) -> Result<SmtpDetails, SmtpError> {
let host_lowercase = host.to_lowercase().to_string();
if input
.skipped_domains
.iter()
.any(|d| host_lowercase.contains(d))
{
return Err(SmtpError::SkippedDomain(format!(
"Reacher currently cannot verify emails from @{domain}"
)));
}
if input.yahoo_use_api && is_yahoo(&host_lowercase) {
return yahoo::check_yahoo(to_email, input)
.await
.map_err(|err| err.into());
}
if input.gmail_use_api && is_gmail(&host_lowercase) {
return gmail::check_gmail(to_email, input)
.await
.map_err(|err| err.into());
}
if input.microsoft365_use_api && is_microsoft365(&host_lowercase) {
match outlook::microsoft365::check_microsoft365_api(to_email, input).await {
Ok(Some(smtp_details)) => return Ok(smtp_details),
Err(err) => {
log::debug!(
target: LOG_TARGET,
"[email={}] microsoft365 error: {:?}",
to_email,
err,
);
}
_ => {}
}
}
#[cfg(feature = "headless")]
if let Some(webdriver) = &input.hotmail_use_headless {
if is_outlook(&host_lowercase) {
return outlook::hotmail::check_password_recovery(to_email, webdriver)
.await
.map_err(|err| err.into());
}
}
check_smtp_with_retry(to_email, host, port, domain, input, input.retries).await
}
#[cfg(test)]
mod tests {
use super::{check_smtp, CheckEmailInput, SmtpError};
use async_smtp::EmailAddress;
use std::{str::FromStr, time::Duration};
use tokio::runtime::Runtime;
use trust_dns_proto::rr::Name;
#[test]
fn should_timeout() {
let runtime = Runtime::new().unwrap();
let to_email = EmailAddress::from_str("foo@gmail.com").unwrap();
let host = Name::from_str("gmail.com").unwrap();
let mut input = CheckEmailInput::default();
input.set_smtp_timeout(Some(Duration::from_millis(1)));
let res = runtime.block_on(check_smtp(&to_email, &host, 25, "gmail.com", &input));
match res {
Err(SmtpError::TimeoutError(_)) => (),
_ => panic!("check_smtp did not time out"),
}
}
#[test]
fn should_skip_domains() {
let runtime = Runtime::new().unwrap();
let to_email = EmailAddress::from_str("foo@icloud.com").unwrap();
let host = Name::from_str("mx01.mail.icloud.com.").unwrap();
let mut input = CheckEmailInput::default();
input.set_skipped_domains(vec![".mail.icloud.com.".into()]);
let res = runtime.block_on(check_smtp(&to_email, &host, 25, "icloud.com", &input));
match res {
Err(SmtpError::SkippedDomain(_)) => (),
r => panic!("{:?}", r),
}
}
}