mod yahoo;
use super::util::{constants::LOG_TARGET, input_output::CheckEmailInput};
use crate::util::ser_with_display::ser_with_display;
use async_smtp::{
smtp::{
client::net::NetworkStream, commands::*, error::Error as AsyncSmtpError,
extension::ClientId,
},
ClientSecurity, EmailAddress, SmtpClient, SmtpTransport,
};
use fast_socks5::{
client::{Config, Socks5Stream},
Result, SocksError,
};
use rand::{distributions::Alphanumeric, Rng};
use serde::Serialize;
use std::str::FromStr;
use std::time::Duration;
use trust_dns_proto::rr::Name;
use yahoo::YahooError;
#[derive(Debug, 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,
}
impl Default for SmtpDetails {
fn default() -> Self {
SmtpDetails {
can_connect_smtp: false,
has_full_inbox: false,
is_catch_all: false,
is_deliverable: false,
is_disabled: false,
}
}
}
#[derive(Debug, Serialize)]
#[serde(tag = "type", content = "message")]
pub enum SmtpError {
#[serde(serialize_with = "ser_with_display")]
SocksError(SocksError),
#[serde(serialize_with = "ser_with_display")]
SmtpError(AsyncSmtpError),
YahooError(YahooError),
}
impl From<AsyncSmtpError> for SmtpError {
fn from(error: AsyncSmtpError) -> Self {
SmtpError::SmtpError(error)
}
}
impl From<SocksError> for SmtpError {
fn from(error: SocksError) -> Self {
SmtpError::SocksError(error)
}
}
impl From<YahooError> for SmtpError {
fn from(error: YahooError) -> Self {
SmtpError::YahooError(error)
}
}
macro_rules! try_smtp (
($res: expr, $client: ident, $host: expr, $port: expr) => ({
if let Err(err) = $res {
log::debug!(target: LOG_TARGET, "Closing {}:{}, because of error '{}'.", $host, $port, err);
$client.close().await?;
return Err(err.into());
}
})
);
async fn connect_to_host(
host: &Name,
port: u16,
input: &CheckEmailInput,
) -> Result<SmtpTransport, SmtpError> {
let mut smtp_client =
SmtpClient::with_security((host.to_utf8().as_ref(), port), ClientSecurity::None)
.await?
.hello_name(ClientId::Domain(input.hello_name.clone()))
.timeout(Some(Duration::new(30, 0)))
.into_transport();
log::debug!(target: LOG_TARGET, "Connecting to {}:{}", host, port);
if let Some(proxy) = &input.proxy {
let stream = Socks5Stream::connect(
(proxy.host.as_ref(), proxy.port),
host.to_utf8(),
port,
Config::default(),
)
.await?;
try_smtp!(
smtp_client
.connect_with_stream(NetworkStream::Socks5Stream(stream))
.await,
smtp_client,
host,
port
);
} else {
try_smtp!(smtp_client.connect().await, smtp_client, host, port);
}
let from_email = EmailAddress::from_str(input.from_email.as_ref()).unwrap_or_else(|_| {
log::warn!(
"Inputted from_email \"{}\" is not a valid email, using \"user@example.org\" instead",
input.from_email
);
EmailAddress::from_str("user@example.org").expect("This is a valid email. qed.")
});
try_smtp!(
smtp_client
.command(MailCommand::new(Some(from_email.clone()), vec![],))
.await,
smtp_client,
host,
port
);
Ok(smtp_client)
}
struct Deliverability {
has_full_inbox: bool,
is_deliverable: bool,
is_disabled: bool,
}
async fn email_deliverable(
smtp_client: &mut SmtpTransport,
to_email: &EmailAddress,
) -> Result<Deliverability, SmtpError> {
match smtp_client
.command(RcptCommand::new(to_email.clone(), vec![]))
.await
{
Ok(response) => match response.first_line() {
Some(message) => {
let is_deliverable = message.contains("2.1.5");
Ok(Deliverability {
has_full_inbox: false,
is_deliverable,
is_disabled: false,
})
}
None => Err(SmtpError::SmtpError(AsyncSmtpError::Client(
"No response on RCPT command",
))),
},
Err(err) => {
let err_string = err.to_string().to_lowercase();
if err_string.contains("disabled")
|| err_string.contains("discontinued")
{
return Ok(Deliverability {
has_full_inbox: false,
is_deliverable: false,
is_disabled: true,
});
}
if err_string.contains("full")
|| err_string.contains("insufficient")
|| err_string.contains("over quota")
|| err_string.contains("space")
|| err_string.contains("too many messages")
{
return Ok(Deliverability {
has_full_inbox: true,
is_deliverable: false,
is_disabled: false,
});
}
if err_string
.contains("the user you are trying to contact is receiving mail at a rate that")
{
return Ok(Deliverability {
has_full_inbox: false,
is_deliverable: true,
is_disabled: false,
});
}
if err_string.contains("address rejected")
|| err_string.contains("unrouteable")
|| err_string.contains("does not exist")
|| err_string.contains("invalid address")
|| err_string.contains("invalid recipient")
|| err_string.contains("may not exist")
|| err_string.contains("recipient invalid")
|| err_string.contains("recipient rejected")
|| err_string.contains("undeliverable")
|| err_string.contains("user unknown")
|| err_string.contains("unknown user")
|| err_string.contains("recipient unknown")
|| err_string.contains("no such user")
|| err_string.contains("not found")
|| err_string.contains("invalid mailbox")
|| err_string.contains("no mailbox")
|| err_string.contains("mailbox unavailable")
|| err_string.contains("not a valid mailbox")
|| err_string.contains("have an account")
{
return Ok(Deliverability {
has_full_inbox: false,
is_deliverable: false,
is_disabled: false,
});
}
Err(SmtpError::SmtpError(err))
}
}
}
async fn smtp_is_catch_all(
smtp_client: &mut SmtpTransport,
domain: &str,
) -> Result<bool, SmtpError> {
let random_email = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(15)
.collect::<String>();
let random_email = EmailAddress::new(format!("{}@{}", random_email, domain));
email_deliverable(
smtp_client,
&random_email.expect("Email is correctly constructed. qed."),
)
.await
.map(|deliverability| deliverability.is_deliverable)
}
pub async fn check_smtp(
to_email: &EmailAddress,
host: &Name,
port: u16,
domain: &str,
input: &CheckEmailInput,
) -> Result<SmtpDetails, SmtpError> {
if input.yahoo_use_api && domain.to_lowercase().contains("yahoo") {
return yahoo::check_yahoo(to_email).await.map_err(|err| err.into());
}
let mut smtp_client = connect_to_host(host, port, input).await?;
let is_catch_all = smtp_is_catch_all(&mut smtp_client, domain)
.await
.unwrap_or(false);
let deliverability = email_deliverable(&mut smtp_client, to_email).await?;
smtp_client.close().await?;
Ok(SmtpDetails {
can_connect_smtp: true,
has_full_inbox: deliverability.has_full_inbox,
is_catch_all,
is_deliverable: deliverability.is_deliverable,
is_disabled: deliverability.is_disabled,
})
}