From a3f581433fb8a26d4cd509a886c1e0ecbb94b066 Mon Sep 17 00:00:00 2001
From: Amaury <1293565+amaury1093@users.noreply.github.com>
Date: Wed, 4 Dec 2024 23:30:23 +0100
Subject: [PATCH 1/6] feat(core): Update async-smtp to 0.9
---
Cargo.lock | 49 ++---------
backend/Cargo.toml | 2 +-
core/Cargo.toml | 9 +-
core/src/rules.rs | 18 ++++
core/src/smtp/connect.rs | 156 +++++++++++-----------------------
core/src/smtp/error.rs | 54 ++++++++----
core/src/smtp/gmail.rs | 2 +-
core/src/smtp/mod.rs | 17 ++--
core/src/smtp/parser.rs | 19 +++--
core/src/syntax/mod.rs | 2 +-
core/src/util/input_output.rs | 118 +++++++++++++++----------
core/src/util/sentry.rs | 4 +-
12 files changed, 210 insertions(+), 240 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index cf494cba5..dcfcba65a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -335,18 +335,6 @@ dependencies = [
"pin-project-lite",
]
-[[package]]
-name = "async-native-tls"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d57d4cec3c647232e1094dc013546c0b33ce785d8aeb251e1f20dfaf8a9a13fe"
-dependencies = [
- "native-tls",
- "thiserror",
- "tokio",
- "url",
-]
-
[[package]]
name = "async-reactor-trait"
version = "1.1.0"
@@ -372,24 +360,17 @@ dependencies = [
[[package]]
name = "async-smtp"
-version = "0.6.0"
+version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2ade89127f9e0d44f9e83cf574d499060005cd45b7dc76be89c0167487fe8edd"
+checksum = "00d1f1a16e5abad3ada9f1f23dbc2f354b138121b90533381be62dada6cbf40a"
dependencies = [
- "async-native-tls",
- "async-trait",
+ "anyhow",
"base64 0.13.1",
- "bufstream",
- "fast-socks5 0.8.2",
"futures",
"hostname",
"log",
"nom",
"pin-project",
- "pin-utils",
- "serde",
- "serde_derive",
- "serde_json",
"thiserror",
"tokio",
]
@@ -528,12 +509,6 @@ dependencies = [
"piper",
]
-[[package]]
-name = "bufstream"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8"
-
[[package]]
name = "bumpalo"
version = "3.16.0"
@@ -577,14 +552,14 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
name = "check-if-email-exists"
version = "0.10.0"
dependencies = [
- "async-native-tls",
+ "anyhow",
"async-recursion",
"async-smtp",
"chrono",
"config",
"derive_builder 0.20.2",
"fantoccini",
- "fast-socks5 0.9.6",
+ "fast-socks5",
"futures",
"hickory-proto",
"hickory-resolver",
@@ -602,6 +577,7 @@ dependencies = [
"serde_json",
"thiserror",
"tokio",
+ "tokio-socks",
"tracing",
]
@@ -1274,19 +1250,6 @@ dependencies = [
"webdriver",
]
-[[package]]
-name = "fast-socks5"
-version = "0.8.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "961ce1761191c157145a8c9f0c3ceabecd3a729d65c9a8d443674eaee3420f7e"
-dependencies = [
- "anyhow",
- "log",
- "thiserror",
- "tokio",
- "tokio-stream",
-]
-
[[package]]
name = "fast-socks5"
version = "0.9.6"
diff --git a/backend/Cargo.toml b/backend/Cargo.toml
index ff1d51dc9..7ce3794a0 100644
--- a/backend/Cargo.toml
+++ b/backend/Cargo.toml
@@ -7,7 +7,7 @@ publish = false
[dependencies]
anyhow = "1.0"
-async-smtp = "0.6"
+async-smtp = { version = "0.9.1", features = ["runtime-tokio"] }
check-if-email-exists = { path = "../core", features = ["sentry"] }
config = "0.14"
csv = "1.3.0"
diff --git a/core/Cargo.toml b/core/Cargo.toml
index 5f63915e7..2246bfbab 100644
--- a/core/Cargo.toml
+++ b/core/Cargo.toml
@@ -13,9 +13,9 @@ readme = "../README.md"
repository = "https://github.com/reacherhq/check-if-email-exists"
[dependencies]
-async-native-tls = { version = "0.4", default-features = false }
+anyhow = "1.0"
async-recursion = "1.0.5"
-async-smtp = { version = "0.6.0", features = ["socks5"] }
+async-smtp = { version = "0.9.1", features = ["runtime-tokio"] }
chrono = { version = "0.4.31", features = ["serde"] }
config = "0.14"
derive_builder = "0.20"
@@ -37,7 +37,6 @@ sentry = { version = "0.23", optional = true }
serde = { version = "1.0.214", features = ["derive"] }
serde_json = "1.0.133"
thiserror = "1.0"
-tracing = "0.1.40"
-
-[dev-dependencies]
tokio = { version = "1.40.0" }
+tokio-socks = "0.5"
+tracing = "0.1.40"
diff --git a/core/src/rules.rs b/core/src/rules.rs
index f578cdabc..30c209126 100644
--- a/core/src/rules.rs
+++ b/core/src/rules.rs
@@ -85,3 +85,21 @@ pub fn has_rule(domain: &str, host: &str, rule: &Rule) -> bool {
|| does_mx_have_rule(host, rule)
|| does_mx_suffix_have_rule(host, rule)
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn should_skip_catch_all() {
+ assert_eq!(
+ true,
+ has_rule("gmail.com", "alt4.aspmx.l.google.com.", &Rule::SkipCatchAll)
+ );
+
+ assert_eq!(
+ true,
+ has_rule("domain.com", ".antispamcloud.com.", &Rule::SkipCatchAll)
+ )
+ }
+}
diff --git a/core/src/smtp/connect.rs b/core/src/smtp/connect.rs
index a7c85000c..5d21ff67a 100644
--- a/core/src/smtp/connect.rs
+++ b/core/src/smtp/connect.rs
@@ -14,25 +14,29 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-use async_native_tls::TlsConnector;
use async_recursion::async_recursion;
-use async_smtp::{
- smtp::{commands::*, extension::ClientId, ServerAddress, Socks5Config},
- ClientTlsParameters, EmailAddress, SmtpClient, SmtpTransport,
-};
+use async_smtp::commands::{MailCommand, RcptCommand};
+use async_smtp::extension::ClientId;
+use async_smtp::{SmtpClient, SmtpTransport};
use rand::rngs::SmallRng;
use rand::{distributions::Alphanumeric, Rng, SeedableRng};
use std::iter;
use std::str::FromStr;
-use std::time::Duration;
+use tokio::io::{AsyncBufRead, AsyncRead, AsyncWrite, BufStream};
+use tokio::net::TcpStream;
+use tokio_socks::tcp::Socks5Stream;
use super::parser;
use super::{SmtpDetails, SmtpError};
-use crate::LOG_TARGET;
use crate::{
rules::{has_rule, Rule},
util::input_output::CheckEmailInput,
};
+use crate::{EmailAddress, LOG_TARGET};
+
+// Define a new trait that combines AsyncRead, AsyncWrite, and Unpin
+trait AsyncReadWrite: AsyncRead + AsyncWrite + Unpin + Send {}
+impl AsyncReadWrite for T {}
/// Try to send an smtp command, close and return Err if fails.
macro_rules! try_smtp (
@@ -40,85 +44,46 @@ macro_rules! try_smtp (
if let Err(err) = $res {
log::debug!(target: LOG_TARGET, "[email={}] Closing [host={}:{}], because of error '{:?}'.", $to_email, $host, $port, err);
// Try to close the connection, but ignore if there's an error.
- let _ = $client.close().await;
+ let _ = $client.quit().await;
- return Err(SmtpError::SmtpError(err));
+ return Err(SmtpError::AsyncSmtpError(err));
}
})
);
/// Attempt to connect to host via SMTP, and return SMTP client on success.
async fn connect_to_host(
- domain: &str,
host: &str,
port: u16,
input: &CheckEmailInput,
-) -> Result {
- let smtp_timeout = if let Some(t) = input.smtp_timeout {
- if has_rule(domain, host, &Rule::SmtpTimeout45s) {
- let duration = t.max(Duration::from_secs(45));
- log::debug!(
- target: LOG_TARGET,
- "[email={}] Bumping SMTP timeout to {duration:?} because of rule",
- input.to_email,
- );
- Some(duration)
- } else {
- input.smtp_timeout
- }
- } else {
- None
- };
-
+) -> Result>>, SmtpError> {
// hostname verification fails if it ends with '.', for example, using
// SOCKS5 proxies we can `io: incomplete` error.
let host = host.trim_end_matches('.').to_string();
- let security = {
- let tls_params: ClientTlsParameters = ClientTlsParameters::new(
- host.clone(),
- TlsConnector::new()
- .use_sni(true)
- .danger_accept_invalid_certs(true)
- .danger_accept_invalid_hostnames(true),
- );
-
- input.smtp_security.to_client_security(tls_params)
- };
-
- let mut smtp_client = SmtpClient::with_security(
- ServerAddress {
- host: host.clone(),
- port,
- },
- security,
- )
- .hello_name(ClientId::Domain(input.hello_name.clone()))
- .timeout(smtp_timeout);
-
- if let Some(proxy) = &input.proxy {
- let socks5_config = match (&proxy.username, &proxy.password) {
- (Some(username), Some(password)) => Socks5Config::new_with_user_pass(
- proxy.host.clone(),
- proxy.port,
- username.clone(),
- password.clone(),
- ),
- _ => Socks5Config::new(proxy.host.clone(), proxy.port),
+ let smtp_client = SmtpClient::new().hello_name(ClientId::Domain(input.hello_name.clone()));
+
+ let stream: BufStream> = if let Some(proxy) = &input.proxy {
+ let target_addr = format!("{}:{}", host, port);
+ let socks_stream = match (&proxy.username, &proxy.password) {
+ (Some(username), Some(password)) => {
+ Socks5Stream::connect_with_password(
+ (proxy.host.as_ref(), proxy.port),
+ target_addr,
+ username,
+ password,
+ )
+ .await?
+ }
+ _ => Socks5Stream::connect((proxy.host.as_ref(), proxy.port), target_addr).await?,
};
+ BufStream::new(Box::new(socks_stream) as Box)
+ } else {
+ let tcp_stream = TcpStream::connect(format!("{}:{}", host, port)).await?;
+ BufStream::new(Box::new(tcp_stream) as Box)
+ };
- smtp_client = smtp_client.use_socks5(socks5_config);
- }
-
- let mut smtp_transport = smtp_client.into_transport();
-
- try_smtp!(
- smtp_transport.connect().await,
- smtp_transport,
- input.to_email,
- host,
- port
- );
+ let mut smtp_transport = SmtpTransport::new(smtp_client, stream).await?;
// "MAIL FROM: user@example.org"
let from_email = EmailAddress::from_str(input.from_email.as_ref()).unwrap_or_else(|_| {
@@ -130,7 +95,8 @@ async fn connect_to_host(
});
try_smtp!(
smtp_transport
- .command(MailCommand::new(Some(from_email), vec![],))
+ .get_mut()
+ .command(MailCommand::new(Some(from_email.into_inner()), vec![],))
.await,
smtp_transport,
input.to_email,
@@ -154,14 +120,14 @@ struct Deliverability {
/// Check if `to_email` exists on host SMTP server. This is the core logic of
/// this tool.
-async fn email_deliverable(
- smtp_transport: &mut SmtpTransport,
+async fn email_deliverable(
+ smtp_transport: &mut SmtpTransport,
to_email: &EmailAddress,
) -> Result {
// "RCPT TO: "
- // FIXME Do not clone `to_email`?
match smtp_transport
- .command(RcptCommand::new(to_email.clone(), vec![]))
+ .get_mut()
+ .command(RcptCommand::new(to_email.clone().into_inner(), vec![]))
.await
{
Ok(_) => {
@@ -228,14 +194,14 @@ async fn email_deliverable(
}
// Return all unparsable errors,.
- Err(SmtpError::SmtpError(err))
+ Err(SmtpError::AsyncSmtpError(err))
}
}
}
/// Verify the existence of a catch-all on the domain.
-async fn smtp_is_catch_all(
- smtp_transport: &mut SmtpTransport,
+async fn smtp_is_catch_all(
+ smtp_transport: &mut SmtpTransport,
domain: &str,
host: &str,
input: &CheckEmailInput,
@@ -276,7 +242,7 @@ async fn create_smtp_future(
) -> Result<(bool, Deliverability), SmtpError> {
// FIXME If the SMTP is not connectable, we should actually return an
// Ok(SmtpDetails { can_connect_smtp: false, ... }).
- let mut smtp_transport = connect_to_host(domain, host, port, input).await?;
+ let mut smtp_transport = connect_to_host(host, port, input).await?;
let is_catch_all = smtp_is_catch_all(&mut smtp_transport, domain, host, input)
.await
@@ -304,8 +270,8 @@ async fn create_smtp_future(
input.to_email
);
- let _ = smtp_transport.close().await;
- smtp_transport = connect_to_host(domain, host, port, input).await?;
+ let _ = smtp_transport.quit().await;
+ smtp_transport = connect_to_host(host, port, input).await?;
result = email_deliverable(&mut smtp_transport, to_email).await;
}
}
@@ -313,7 +279,10 @@ async fn create_smtp_future(
result?
};
- smtp_transport.close().await.map_err(SmtpError::SmtpError)?;
+ smtp_transport
+ .quit()
+ .await
+ .map_err(SmtpError::AsyncSmtpError)?;
Ok((is_catch_all, deliverability))
}
@@ -394,26 +363,3 @@ pub async fn check_smtp_with_retry(
_ => result,
}
}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[tokio::test]
- async fn should_skip_catch_all() {
- let smtp_client = SmtpClient::new("gmail.com".into());
- let mut smtp_transport = smtp_client.into_transport();
-
- let r = smtp_is_catch_all(
- &mut smtp_transport,
- "gmail.com",
- "alt4.aspmx.l.google.com.",
- &CheckEmailInput::default(),
- )
- .await;
-
- assert!(!smtp_transport.is_connected()); // We shouldn't connect to google servers.
- assert!(r.is_ok());
- assert!(!r.unwrap())
- }
-}
diff --git a/core/src/smtp/error.rs b/core/src/smtp/error.rs
index 27e1bab63..6e6b8d264 100644
--- a/core/src/smtp/error.rs
+++ b/core/src/smtp/error.rs
@@ -15,29 +15,20 @@
// along with this program. If not, see .
use super::gmail::GmailError;
-
use super::headless::HeadlessError;
use super::outlook::microsoft365::Microsoft365Error;
use super::parser;
use super::yahoo::YahooError;
use crate::util::ser_with_display::ser_with_display;
-use async_smtp::smtp::error::Error as AsyncSmtpError;
-use fast_socks5::SocksError;
+use async_smtp::error::Error as AsyncSmtpError;
use serde::Serialize;
+use std::time::Duration;
use thiserror::Error;
/// Error occured connecting to this email server via SMTP.
#[derive(Debug, Error, Serialize)]
#[serde(tag = "type", content = "message")]
pub enum SmtpError {
- /// Error if we're using a SOCKS5 proxy.
- #[serde(serialize_with = "ser_with_display")]
- #[error("SOCKS5 error: {0}")]
- SocksError(SocksError),
- /// Error when communicating with SMTP server.
- #[serde(serialize_with = "ser_with_display")]
- #[error("SMTP error: {0}")]
- SmtpError(AsyncSmtpError),
/// Error when verifying a Yahoo email via HTTP requests.
#[error("Yahoo error: {0}")]
YahooError(YahooError),
@@ -50,12 +41,21 @@ pub enum SmtpError {
/// Error when verifying a Microsoft 365 email via HTTP request.
#[error("Microsoft 365 API error: {0}")]
Microsoft365Error(Microsoft365Error),
-}
-
-impl From for SmtpError {
- fn from(e: SocksError) -> Self {
- SmtpError::SocksError(e)
- }
+ /// Error from async-smtp crate.
+ #[error("SMTP error: {0}")]
+ #[serde(serialize_with = "ser_with_display")]
+ AsyncSmtpError(AsyncSmtpError),
+ /// I/O error.
+ #[error("I/O error: {0}")]
+ #[serde(serialize_with = "ser_with_display")]
+ IOError(std::io::Error),
+ /// Timeout error.
+ #[error("Timeout error: {0:?}")]
+ Timeout(Duration),
+ /// Socks5 proxy error.
+ #[error("Socks5 error: {0}")]
+ #[serde(serialize_with = "ser_with_display")]
+ Socks5(tokio_socks::Error),
}
impl From for SmtpError {
@@ -82,6 +82,24 @@ impl From for SmtpError {
}
}
+impl From for SmtpError {
+ fn from(e: AsyncSmtpError) -> Self {
+ SmtpError::AsyncSmtpError(e)
+ }
+}
+
+impl From for SmtpError {
+ fn from(e: std::io::Error) -> Self {
+ SmtpError::IOError(e)
+ }
+}
+
+impl From for SmtpError {
+ fn from(e: tokio_socks::Error) -> Self {
+ SmtpError::Socks5(e)
+ }
+}
+
impl SmtpError {
/// Get a human-understandable description of the error, in form of an enum
/// SmtpErrorDesc. This only parses the following known errors:
@@ -89,7 +107,7 @@ impl SmtpError {
/// - IP needs reverse DNS
pub fn get_description(&self) -> Option {
match self {
- SmtpError::SmtpError(_) => {
+ SmtpError::AsyncSmtpError(_) => {
if parser::is_err_ip_blacklisted(self) {
Some(SmtpErrorDesc::IpBlacklisted)
} else if parser::is_err_needs_rdns(self) {
diff --git a/core/src/smtp/gmail.rs b/core/src/smtp/gmail.rs
index 221d2cafb..6769b156a 100644
--- a/core/src/smtp/gmail.rs
+++ b/core/src/smtp/gmail.rs
@@ -15,12 +15,12 @@
// along with this program. If not, see .
use super::SmtpDetails;
+use crate::EmailAddress;
use crate::LOG_TARGET;
use crate::{
smtp::http_api::create_client,
util::{input_output::CheckEmailInput, ser_with_display::ser_with_display},
};
-use async_smtp::EmailAddress;
use reqwest::Error as ReqwestError;
use serde::Serialize;
use thiserror::Error;
diff --git a/core/src/smtp/mod.rs b/core/src/smtp/mod.rs
index 14c208ec9..b319b4d9c 100644
--- a/core/src/smtp/mod.rs
+++ b/core/src/smtp/mod.rs
@@ -24,23 +24,21 @@ mod outlook;
mod parser;
mod yahoo;
-use std::default::Default;
-
-use async_smtp::EmailAddress;
-use hickory_proto::rr::Name;
-use serde::{Deserialize, Serialize};
-
+use crate::EmailAddress;
use crate::{
util::input_output::CheckEmailInput, GmailVerifMethod, HotmailB2CVerifMethod, YahooVerifMethod,
};
use connect::check_smtp_with_retry;
-pub use error::*;
+use hickory_proto::rr::Name;
+use serde::{Deserialize, Serialize};
+use std::default::Default;
pub use self::{
gmail::is_gmail,
outlook::{is_hotmail, is_hotmail_b2b, is_hotmail_b2c},
yahoo::is_yahoo,
};
+pub use error::*;
#[derive(Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct SmtpConnection {
@@ -163,7 +161,8 @@ pub async fn check_smtp(
mod tests {
use super::{check_smtp, SmtpConnection, SmtpError};
use crate::CheckEmailInputBuilder;
- use async_smtp::{smtp::error::Error, EmailAddress};
+ use crate::EmailAddress;
+ use async_smtp::error::Error;
use hickory_proto::rr::Name;
use std::{str::FromStr, time::Duration};
use tokio::runtime::Runtime;
@@ -192,7 +191,7 @@ mod tests {
})
);
match res {
- Err(SmtpError::SmtpError(Error::Io(_))) => (), // ErrorKind == Timeout
+ Err(SmtpError::AsyncSmtpError(Error::Io(_))) => (), // ErrorKind == Timeout
_ => panic!("check_smtp did not time out"),
}
}
diff --git a/core/src/smtp/parser.rs b/core/src/smtp/parser.rs
index 0d9c60b96..311f28b58 100644
--- a/core/src/smtp/parser.rs
+++ b/core/src/smtp/parser.rs
@@ -17,7 +17,8 @@
//! Parse the SMTP responses to get information about the email address.
use super::error::SmtpError;
-use async_smtp::{smtp::error::Error as AsyncSmtpError, EmailAddress};
+use crate::EmailAddress;
+use async_smtp::error::Error as AsyncSmtpError;
/// is_invalid checks for SMTP responses meaning that the email is invalid,
/// i.e. that the mailbox doesn't exist.
@@ -127,7 +128,7 @@ pub fn is_disabled_account(e: &str) -> bool {
/// Check if the error is an IO "incomplete" error.
pub fn is_err_io_errors(e: &SmtpError) -> bool {
match e {
- SmtpError::SmtpError(AsyncSmtpError::Io(err)) => err.to_string() == "incomplete",
+ SmtpError::AsyncSmtpError(AsyncSmtpError::Io(err)) => err.to_string() == "incomplete",
_ => false,
}
}
@@ -135,7 +136,7 @@ pub fn is_err_io_errors(e: &SmtpError) -> bool {
/// Check if the IP is blacklisted.
pub fn is_err_ip_blacklisted(e: &SmtpError) -> bool {
let e = match e {
- SmtpError::SmtpError(AsyncSmtpError::Transient(r) | AsyncSmtpError::Permanent(r)) => {
+ SmtpError::AsyncSmtpError(AsyncSmtpError::Transient(r) | AsyncSmtpError::Permanent(r)) => {
// TODO We can use .to_string() after:
// https://github.com/async-email/async-smtp/pull/53
r.message.join("; ").to_lowercase()
@@ -205,7 +206,7 @@ pub fn is_err_ip_blacklisted(e: &SmtpError) -> bool {
/// Check if the IP needs a reverse DNS.
pub fn is_err_needs_rdns(e: &SmtpError) -> bool {
let e = match e {
- SmtpError::SmtpError(AsyncSmtpError::Transient(r) | AsyncSmtpError::Permanent(r)) => {
+ SmtpError::AsyncSmtpError(AsyncSmtpError::Transient(r) | AsyncSmtpError::Permanent(r)) => {
// TODO We can use .to_string() after:
// https://github.com/async-email/async-smtp/pull/53
r.message.join("; ").to_lowercase()
@@ -227,11 +228,11 @@ pub fn is_err_needs_rdns(e: &SmtpError) -> bool {
mod tests {
use super::{is_err_ip_blacklisted, is_invalid};
- use crate::SmtpError::SmtpError;
+ use crate::EmailAddress;
+ use crate::SmtpError::AsyncSmtpError;
use async_smtp::{
- smtp::error::Error,
- smtp::response::{Category, Code, Detail, Response, Severity},
- EmailAddress,
+ error::Error,
+ response::{Category, Code, Detail, Response, Severity},
};
use std::str::FromStr;
@@ -271,6 +272,6 @@ mod tests {
],
));
- assert!(is_err_ip_blacklisted(&SmtpError(err)))
+ assert!(is_err_ip_blacklisted(&AsyncSmtpError(err)))
}
}
diff --git a/core/src/syntax/mod.rs b/core/src/syntax/mod.rs
index 4c5b8f2b0..fedac42e9 100644
--- a/core/src/syntax/mod.rs
+++ b/core/src/syntax/mod.rs
@@ -16,7 +16,7 @@
mod normalize;
-use async_smtp::EmailAddress;
+use crate::EmailAddress;
use levenshtein::levenshtein;
use normalize::normalize_email;
use serde::{Deserialize, Serialize};
diff --git a/core/src/util/input_output.rs b/core/src/util/input_output.rs
index 0eaa145b4..14f782fab 100644
--- a/core/src/util/input_output.rs
+++ b/core/src/util/input_output.rs
@@ -14,18 +14,80 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-use std::str::FromStr;
-use std::time::{Duration, SystemTime};
-
-use async_smtp::{ClientSecurity, ClientTlsParameters};
-use chrono::{DateTime, Utc};
-use derive_builder::Builder;
-use serde::{ser::SerializeMap, Deserialize, Serialize, Serializer};
-
use crate::misc::{MiscDetails, MiscError};
use crate::mx::{MxDetails, MxError};
use crate::smtp::{SmtpDebug, SmtpDetails, SmtpError, SmtpErrorDesc};
use crate::syntax::SyntaxDetails;
+use crate::util::ser_with_display::ser_with_display;
+use async_smtp::EmailAddress as AsyncSmtpEmailAddress;
+use chrono::{DateTime, Utc};
+use derive_builder::Builder;
+use serde::{ser::SerializeMap, Deserialize, Serialize, Serializer};
+use std::fmt::Display;
+use std::str::FromStr;
+use std::time::{Duration, SystemTime};
+
+/// Wrapper around the `EmailAddress` from `async_smtp` to allow for
+/// serialization and deserialization.
+#[derive(Debug, Eq, PartialEq, Clone)]
+pub struct EmailAddress(AsyncSmtpEmailAddress);
+
+impl Serialize for EmailAddress {
+ fn serialize(&self, serializer: S) -> Result
+ where
+ S: Serializer,
+ {
+ ser_with_display(&self.0, serializer)
+ }
+}
+
+impl<'de> Deserialize<'de> for EmailAddress {
+ fn deserialize(deserializer: D) -> Result
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let s = String::deserialize(deserializer)?;
+ Ok(EmailAddress(
+ AsyncSmtpEmailAddress::from_str(&s).map_err(serde::de::Error::custom)?,
+ ))
+ }
+}
+
+impl Display for EmailAddress {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+impl FromStr for EmailAddress {
+ type Err = anyhow::Error;
+
+ fn from_str(s: &str) -> Result {
+ Ok(EmailAddress(AsyncSmtpEmailAddress::from_str(s)?))
+ }
+}
+
+impl EmailAddress {
+ pub fn new(email: String) -> Result {
+ Ok(EmailAddress(AsyncSmtpEmailAddress::new(email)?))
+ }
+
+ pub fn into_inner(self) -> AsyncSmtpEmailAddress {
+ self.0
+ }
+}
+
+impl AsRef for EmailAddress {
+ fn as_ref(&self) -> &AsyncSmtpEmailAddress {
+ &self.0
+ }
+}
+
+impl AsRef for EmailAddress {
+ fn as_ref(&self) -> &str {
+ self.0.as_ref()
+ }
+}
/// Perform the email verification via a specified proxy. The usage of a proxy
/// is optional.
@@ -41,37 +103,6 @@ pub struct CheckEmailInputProxy {
pub password: Option,
}
-/// Define how to apply TLS to a SMTP client connection. Will be converted into
-/// async_smtp::ClientSecurity.
-#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
-pub enum SmtpSecurity {
- /// Insecure connection only (for testing purposes).
- None,
- /// Start with insecure connection and use `STARTTLS` when available.
- Opportunistic,
- /// Start with insecure connection and require `STARTTLS`.
- Required,
- /// Use TLS wrapped connection.
- Wrapper,
-}
-
-impl Default for SmtpSecurity {
- fn default() -> Self {
- Self::Opportunistic
- }
-}
-
-impl SmtpSecurity {
- pub fn to_client_security(self, tls_params: ClientTlsParameters) -> ClientSecurity {
- match self {
- Self::None => ClientSecurity::None,
- Self::Opportunistic => ClientSecurity::Opportunistic(tls_params),
- Self::Required => ClientSecurity::Required(tls_params),
- Self::Wrapper => ClientSecurity::Wrapper(tls_params),
- }
- }
-}
-
/// Select how to verify Yahoo emails.
#[derive(Debug, Clone, Copy, Default, PartialEq, Deserialize, Serialize)]
pub enum YahooVerifMethod {
@@ -228,10 +259,6 @@ pub struct CheckEmailInput {
///
/// Defaults to 1.
pub retries: usize,
- /// How to apply TLS to a SMTP client connection.
- ///
- /// Defaults to Opportunistic.
- pub smtp_security: SmtpSecurity,
/// The WebDriver address to use for headless verifications.
///
/// Defaults to http://localhost:9515.
@@ -256,7 +283,6 @@ impl Default for CheckEmailInput {
hello_name: "gmail.com".into(),
proxy: None,
smtp_port: 25,
- smtp_security: SmtpSecurity::default(),
smtp_timeout: None,
yahoo_verif_method: YahooVerifMethod::default(),
gmail_verif_method: GmailVerifMethod::default(),
@@ -411,7 +437,7 @@ impl Serialize for CheckEmailOutput {
#[cfg(test)]
mod tests {
use super::{CheckEmailOutput, DebugDetails};
- use async_smtp::smtp::response::{Category, Code, Detail, Response, Severity};
+ use async_smtp::response::{Category, Code, Detail, Response, Severity};
#[test]
fn should_serialize_correctly() {
@@ -433,7 +459,7 @@ mod tests {
misc: Ok(super::MiscDetails::default()),
mx: Ok(super::MxDetails::default()),
syntax: super::SyntaxDetails::default(),
- smtp: Err(super::SmtpError::SmtpError(r.into())),
+ smtp: Err(super::SmtpError::AsyncSmtpError(r.into())),
debug: DebugDetails::default(),
}
}
diff --git a/core/src/util/sentry.rs b/core/src/util/sentry.rs
index 391cf2761..7aeab6516 100644
--- a/core/src/util/sentry.rs
+++ b/core/src/util/sentry.rs
@@ -24,7 +24,7 @@ use crate::misc::MiscError;
use crate::mx::MxError;
use crate::LOG_TARGET;
use crate::{smtp::SmtpError, CheckEmailOutput};
-use async_smtp::smtp::error::Error as AsyncSmtpError;
+use async_smtp::error::Error as AsyncSmtpError;
use sentry::protocol::{Event, Exception, Level, Values};
use thiserror::Error;
use tracing::{debug, info};
@@ -121,7 +121,7 @@ pub fn log_unknown_errors(result: &CheckEmailOutput, backend_name: &str) {
(_, _, Err(err)) if err.get_description().is_some() => {
// If the SMTP error is known, we don't track it in Sentry.
}
- (_, _, Err(SmtpError::SmtpError(AsyncSmtpError::Transient(response))))
+ (_, _, Err(SmtpError::AsyncSmtpError(AsyncSmtpError::Transient(response))))
if skip_smtp_transient_errors(&response.message) =>
{
// If the SMTP error is transient and known, we don't track it in
From b5c5be61ef28270dc8eeff28376af0c90d368a79 Mon Sep 17 00:00:00 2001
From: Amaury <1293565+amaury1093@users.noreply.github.com>
Date: Wed, 4 Dec 2024 23:38:51 +0100
Subject: [PATCH 2/6] prettify
---
Cargo.lock | 15 ------------
core/Cargo.toml | 1 -
core/src/lib.rs | 8 +++----
core/src/smtp/connect.rs | 50 +++++++++++++++++++++++-----------------
4 files changed, 33 insertions(+), 41 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index dcfcba65a..dfc1e9468 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -559,7 +559,6 @@ dependencies = [
"config",
"derive_builder 0.20.2",
"fantoccini",
- "fast-socks5",
"futures",
"hickory-proto",
"hickory-resolver",
@@ -1250,20 +1249,6 @@ dependencies = [
"webdriver",
]
-[[package]]
-name = "fast-socks5"
-version = "0.9.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f89f36d4ee12370d30d57b16c7e190950a1a916e7dbbb5fd5a412f5ef913fe84"
-dependencies = [
- "anyhow",
- "async-trait",
- "log",
- "thiserror",
- "tokio",
- "tokio-stream",
-]
-
[[package]]
name = "fast_chemail"
version = "0.9.6"
diff --git a/core/Cargo.toml b/core/Cargo.toml
index 2246bfbab..d6c8b431c 100644
--- a/core/Cargo.toml
+++ b/core/Cargo.toml
@@ -21,7 +21,6 @@ config = "0.14"
derive_builder = "0.20"
fantoccini = { version = "0.21.2" }
futures = { version = "0.3.30" }
-fast-socks5 = "0.9.2"
hickory-proto = "0.24.0"
hickory-resolver = "0.24.0"
levenshtein = "1.0.5"
diff --git a/core/src/lib.rs b/core/src/lib.rs
index 5aaa2cdfd..23619ff59 100644
--- a/core/src/lib.rs
+++ b/core/src/lib.rs
@@ -23,13 +23,13 @@
//! - Email deliverability: Is an email sent to this address deliverable?
//! - Syntax validation. Is the address syntactically valid?
//! - DNS records validation. Does the domain of the email address have valid
-//! MX DNS records?
+//! MX DNS records?
//! - Disposable email address (DEA) validation. Is the address provided by a
-//! known disposable email address provider?
+//! known disposable email address provider?
//! - SMTP server validation. Can the mail exchanger of the email address
-//! domain be contacted successfully?
+//! domain be contacted successfully?
//! - Mailbox disabled. Has this email address been disabled by the email
-//! provider?
+//! provider?
//! - Full inbox. Is the inbox of this mailbox full?
//! - Catch-all address. Is this email address a catch-all address?
//!
diff --git a/core/src/smtp/connect.rs b/core/src/smtp/connect.rs
index 5d21ff67a..8b74fbed4 100644
--- a/core/src/smtp/connect.rs
+++ b/core/src/smtp/connect.rs
@@ -42,7 +42,13 @@ impl AsyncReadWrite for T {}
macro_rules! try_smtp (
($res: expr, $client: ident, $to_email: expr, $host: expr, $port: expr) => ({
if let Err(err) = $res {
- log::debug!(target: LOG_TARGET, "[email={}] Closing [host={}:{}], because of error '{:?}'.", $to_email, $host, $port, err);
+ log::debug!(
+ target: LOG_TARGET,
+ "[email={}] Closing [host={}:{}], because of error '{:?}'.",
+ $to_email,
+ $host,
+ $port,err,
+ );
// Try to close the connection, but ignore if there's an error.
let _ = $client.quit().await;
@@ -63,24 +69,27 @@ async fn connect_to_host(
let smtp_client = SmtpClient::new().hello_name(ClientId::Domain(input.hello_name.clone()));
- let stream: BufStream> = if let Some(proxy) = &input.proxy {
- let target_addr = format!("{}:{}", host, port);
- let socks_stream = match (&proxy.username, &proxy.password) {
- (Some(username), Some(password)) => {
- Socks5Stream::connect_with_password(
- (proxy.host.as_ref(), proxy.port),
- target_addr,
- username,
- password,
- )
- .await?
- }
- _ => Socks5Stream::connect((proxy.host.as_ref(), proxy.port), target_addr).await?,
- };
- BufStream::new(Box::new(socks_stream) as Box)
- } else {
- let tcp_stream = TcpStream::connect(format!("{}:{}", host, port)).await?;
- BufStream::new(Box::new(tcp_stream) as Box)
+ let stream: BufStream> = match &input.proxy {
+ Some(proxy) => {
+ let target_addr = format!("{}:{}", host, port);
+ let socks_stream = match (&proxy.username, &proxy.password) {
+ (Some(username), Some(password)) => {
+ Socks5Stream::connect_with_password(
+ (proxy.host.as_ref(), proxy.port),
+ target_addr,
+ username,
+ password,
+ )
+ .await?
+ }
+ _ => Socks5Stream::connect((proxy.host.as_ref(), proxy.port), target_addr).await?,
+ };
+ BufStream::new(Box::new(socks_stream) as Box)
+ }
+ None => {
+ let tcp_stream = TcpStream::connect(format!("{}:{}", host, port)).await?;
+ BufStream::new(Box::new(tcp_stream) as Box)
+ }
};
let mut smtp_transport = SmtpTransport::new(smtp_client, stream).await?;
@@ -218,8 +227,7 @@ async fn smtp_is_catch_all(
// Create a random 15-char alphanumerical string.
let mut rng = SmallRng::from_entropy();
- let random_email: String = iter::repeat(())
- .map(|()| rng.sample(Alphanumeric))
+ let random_email: String = iter::repeat_with(|| rng.sample(Alphanumeric))
.map(char::from)
.take(15)
.collect();
From adda56472a93565f516c0d5700a896d7f41b6f54 Mon Sep 17 00:00:00 2001
From: Amaury <1293565+amaury1093@users.noreply.github.com>
Date: Wed, 4 Dec 2024 23:46:05 +0100
Subject: [PATCH 3/6] fix test
---
core/Cargo.toml | 2 +-
core/src/smtp/gmail.rs | 6 ++----
core/src/util/input_output.rs | 6 +++---
3 files changed, 6 insertions(+), 8 deletions(-)
diff --git a/core/Cargo.toml b/core/Cargo.toml
index d6c8b431c..2e1548f6d 100644
--- a/core/Cargo.toml
+++ b/core/Cargo.toml
@@ -36,6 +36,6 @@ sentry = { version = "0.23", optional = true }
serde = { version = "1.0.214", features = ["derive"] }
serde_json = "1.0.133"
thiserror = "1.0"
-tokio = { version = "1.40.0" }
+tokio = { version = "1.40.0", features = ["rt-multi-thread", "macros"] }
tokio-socks = "0.5"
tracing = "0.1.40"
diff --git a/core/src/smtp/gmail.rs b/core/src/smtp/gmail.rs
index 6769b156a..916099527 100644
--- a/core/src/smtp/gmail.rs
+++ b/core/src/smtp/gmail.rs
@@ -78,11 +78,9 @@ pub fn is_gmail(host: &str) -> bool {
#[cfg(test)]
mod tests {
- use std::str::FromStr;
-
- use crate::CheckEmailInputBuilder;
-
use super::*;
+ use crate::CheckEmailInputBuilder;
+ use std::str::FromStr;
#[tokio::test]
#[ignore] // ref: https://github.com/reacherhq/check-if-email-exists/issues/1431
diff --git a/core/src/util/input_output.rs b/core/src/util/input_output.rs
index 14f782fab..f92bbf45a 100644
--- a/core/src/util/input_output.rs
+++ b/core/src/util/input_output.rs
@@ -467,20 +467,20 @@ mod tests {
let res = dummy_response_with_message("blacklist");
let actual = serde_json::to_string(&res).unwrap();
// Make sure the `description` is present with IpBlacklisted.
- let expected = r#""smtp":{"error":{"type":"SmtpError","message":"transient: blacklist"},"description":"IpBlacklisted"}"#;
+ let expected = r#""smtp":{"error":{"type":"AsyncSmtpError","message":"transient: blacklist; 8BITMIME; SIZE 42"},"description":"IpBlacklisted"}"#;
assert!(actual.contains(expected));
let res =
dummy_response_with_message("Client host rejected: cannot find your reverse hostname");
let actual = serde_json::to_string(&res).unwrap();
// Make sure the `description` is present with NeedsRDNs.
- let expected = r#"smtp":{"error":{"type":"SmtpError","message":"transient: Client host rejected: cannot find your reverse hostname"},"description":"NeedsRDNS"}"#;
+ let expected = r#"smtp":{"error":{"type":"AsyncSmtpError","message":"transient: Client host rejected: cannot find your reverse hostname; 8BITMIME; SIZE 42"},"description":"NeedsRDNS"}"#;
assert!(actual.contains(expected));
let res = dummy_response_with_message("foobar");
let actual = serde_json::to_string(&res).unwrap();
// Make sure the `description` is NOT present.
- let expected = r#""smtp":{"error":{"type":"SmtpError","message":"transient: foobar"}}"#;
+ let expected = r#""smtp":{"error":{"type":"AsyncSmtpError","message":"transient: foobar; 8BITMIME; SIZE 42"}}"#;
assert!(actual.contains(expected));
}
}
From 3a13b7c9e6849ac9ce6797fbe262a2fc5c1e8994 Mon Sep 17 00:00:00 2001
From: Amaury <1293565+amaury1093@users.noreply.github.com>
Date: Wed, 4 Dec 2024 23:54:43 +0100
Subject: [PATCH 4/6] add smtp_timeout
---
core/src/smtp/connect.rs | 13 ++++++++++++-
core/src/smtp/mod.rs | 3 +--
core/src/util/input_output.rs | 4 +++-
3 files changed, 16 insertions(+), 4 deletions(-)
diff --git a/core/src/smtp/connect.rs b/core/src/smtp/connect.rs
index 8b74fbed4..bb1424783 100644
--- a/core/src/smtp/connect.rs
+++ b/core/src/smtp/connect.rs
@@ -305,7 +305,18 @@ async fn check_smtp_without_retry(
input: &CheckEmailInput,
) -> Result {
let fut = create_smtp_future(to_email, host, port, domain, input);
- let (is_catch_all, deliverability) = fut.await?;
+
+ let (is_catch_all, deliverability) = match input.smtp_timeout {
+ Some(smtp_timeout) => {
+ let timeout = tokio::time::timeout(smtp_timeout, fut);
+
+ match timeout.await {
+ Ok(result) => result?,
+ Err(_) => return Err(SmtpError::Timeout(smtp_timeout)),
+ }
+ }
+ None => fut.await?,
+ };
Ok(SmtpDetails {
can_connect_smtp: true,
diff --git a/core/src/smtp/mod.rs b/core/src/smtp/mod.rs
index b319b4d9c..6ee92f65d 100644
--- a/core/src/smtp/mod.rs
+++ b/core/src/smtp/mod.rs
@@ -162,7 +162,6 @@ mod tests {
use super::{check_smtp, SmtpConnection, SmtpError};
use crate::CheckEmailInputBuilder;
use crate::EmailAddress;
- use async_smtp::error::Error;
use hickory_proto::rr::Name;
use std::{str::FromStr, time::Duration};
use tokio::runtime::Runtime;
@@ -191,7 +190,7 @@ mod tests {
})
);
match res {
- Err(SmtpError::AsyncSmtpError(Error::Io(_))) => (), // ErrorKind == Timeout
+ Err(SmtpError::Timeout(_)) => (), // ErrorKind == Timeout
_ => panic!("check_smtp did not time out"),
}
}
diff --git a/core/src/util/input_output.rs b/core/src/util/input_output.rs
index f92bbf45a..04f90771c 100644
--- a/core/src/util/input_output.rs
+++ b/core/src/util/input_output.rs
@@ -226,7 +226,9 @@ pub struct CheckEmailInput {
/// Defaults to 25.
pub smtp_port: u16,
/// Add timeout for the SMTP verification step. Set to None if you don't
- /// want to use a timeout.
+ /// want to use a timeout. This timeout is per SMTP connection. For
+ /// instance, if you set the number of retries to 2, then the total time
+ /// for the SMTP verification step can be up to 2 * `smtp_timeout`.
///
/// Defaults to None.
pub smtp_timeout: Option,
From 5d76fe70314d2b442613d63a0791f0847dc99f20 Mon Sep 17 00:00:00 2001
From: Amaury <1293565+amaury1093@users.noreply.github.com>
Date: Thu, 5 Dec 2024 00:21:47 +0100
Subject: [PATCH 5/6] Fix bug
---
Cargo.lock | 16 +++++++++++++++-
backend/src/http/v0/check_email/post.rs | 4 +++-
core/Cargo.toml | 2 +-
core/src/smtp/connect.rs | 23 ++++++++++++++++-------
core/src/smtp/error.rs | 10 +++++-----
5 files changed, 40 insertions(+), 15 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index dfc1e9468..ad6648306 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -559,6 +559,7 @@ dependencies = [
"config",
"derive_builder 0.20.2",
"fantoccini",
+ "fast-socks5",
"futures",
"hickory-proto",
"hickory-resolver",
@@ -576,7 +577,6 @@ dependencies = [
"serde_json",
"thiserror",
"tokio",
- "tokio-socks",
"tracing",
]
@@ -1249,6 +1249,20 @@ dependencies = [
"webdriver",
]
+[[package]]
+name = "fast-socks5"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f89f36d4ee12370d30d57b16c7e190950a1a916e7dbbb5fd5a412f5ef913fe84"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "log",
+ "thiserror",
+ "tokio",
+ "tokio-stream",
+]
+
[[package]]
name = "fast_chemail"
version = "0.9.6"
diff --git a/backend/src/http/v0/check_email/post.rs b/backend/src/http/v0/check_email/post.rs
index 7f774b0d7..9da6865ef 100644
--- a/backend/src/http/v0/check_email/post.rs
+++ b/backend/src/http/v0/check_email/post.rs
@@ -62,7 +62,9 @@ impl CheckEmailRequest {
.or_else(|| config.proxy.as_ref())
.cloned(),
smtp_timeout: config.smtp_timeout.map(Duration::from_secs),
- smtp_port: self.smtp_port.unwrap_or_default(),
+ smtp_port: self
+ .smtp_port
+ .unwrap_or_else(|| CheckEmailInput::default().smtp_port),
sentry_dsn: config.sentry_dsn.clone(),
..Default::default()
}
diff --git a/core/Cargo.toml b/core/Cargo.toml
index 2e1548f6d..0e9ea3a19 100644
--- a/core/Cargo.toml
+++ b/core/Cargo.toml
@@ -19,6 +19,7 @@ async-smtp = { version = "0.9.1", features = ["runtime-tokio"] }
chrono = { version = "0.4.31", features = ["serde"] }
config = "0.14"
derive_builder = "0.20"
+fast-socks5 = "0.9"
fantoccini = { version = "0.21.2" }
futures = { version = "0.3.30" }
hickory-proto = "0.24.0"
@@ -37,5 +38,4 @@ serde = { version = "1.0.214", features = ["derive"] }
serde_json = "1.0.133"
thiserror = "1.0"
tokio = { version = "1.40.0", features = ["rt-multi-thread", "macros"] }
-tokio-socks = "0.5"
tracing = "0.1.40"
diff --git a/core/src/smtp/connect.rs b/core/src/smtp/connect.rs
index bb1424783..f3dc2fad9 100644
--- a/core/src/smtp/connect.rs
+++ b/core/src/smtp/connect.rs
@@ -18,13 +18,14 @@ use async_recursion::async_recursion;
use async_smtp::commands::{MailCommand, RcptCommand};
use async_smtp::extension::ClientId;
use async_smtp::{SmtpClient, SmtpTransport};
+use fast_socks5::client::Config;
+use fast_socks5::{client::Socks5Stream, Result};
use rand::rngs::SmallRng;
use rand::{distributions::Alphanumeric, Rng, SeedableRng};
use std::iter;
use std::str::FromStr;
use tokio::io::{AsyncBufRead, AsyncRead, AsyncWrite, BufStream};
use tokio::net::TcpStream;
-use tokio_socks::tcp::Socks5Stream;
use super::parser;
use super::{SmtpDetails, SmtpError};
@@ -66,23 +67,31 @@ async fn connect_to_host(
// hostname verification fails if it ends with '.', for example, using
// SOCKS5 proxies we can `io: incomplete` error.
let host = host.trim_end_matches('.').to_string();
-
let smtp_client = SmtpClient::new().hello_name(ClientId::Domain(input.hello_name.clone()));
let stream: BufStream> = match &input.proxy {
Some(proxy) => {
- let target_addr = format!("{}:{}", host, port);
let socks_stream = match (&proxy.username, &proxy.password) {
(Some(username), Some(password)) => {
Socks5Stream::connect_with_password(
(proxy.host.as_ref(), proxy.port),
- target_addr,
- username,
- password,
+ host.clone(),
+ port,
+ username.clone(),
+ password.clone(),
+ Config::default(),
+ )
+ .await?
+ }
+ _ => {
+ Socks5Stream::connect(
+ (proxy.host.as_ref(), proxy.port),
+ host.clone(),
+ port,
+ Config::default(),
)
.await?
}
- _ => Socks5Stream::connect((proxy.host.as_ref(), proxy.port), target_addr).await?,
};
BufStream::new(Box::new(socks_stream) as Box)
}
diff --git a/core/src/smtp/error.rs b/core/src/smtp/error.rs
index 6e6b8d264..2dad4d440 100644
--- a/core/src/smtp/error.rs
+++ b/core/src/smtp/error.rs
@@ -52,10 +52,10 @@ pub enum SmtpError {
/// Timeout error.
#[error("Timeout error: {0:?}")]
Timeout(Duration),
- /// Socks5 proxy error.
- #[error("Socks5 error: {0}")]
+ /// SOCKS5 proxy error.
+ #[error("SOCKS5 error: {0}")]
#[serde(serialize_with = "ser_with_display")]
- Socks5(tokio_socks::Error),
+ Socks5(fast_socks5::SocksError),
}
impl From for SmtpError {
@@ -94,8 +94,8 @@ impl From for SmtpError {
}
}
-impl From for SmtpError {
- fn from(e: tokio_socks::Error) -> Self {
+impl From for SmtpError {
+ fn from(e: fast_socks5::SocksError) -> Self {
SmtpError::Socks5(e)
}
}
From b058e9e0ae1329832ef88aefc1023978a73a1d15 Mon Sep 17 00:00:00 2001
From: Amaury <1293565+amaury1093@users.noreply.github.com>
Date: Thu, 5 Dec 2024 00:42:13 +0100
Subject: [PATCH 6/6] improve code
---
core/src/smtp/connect.rs | 107 ++++++++++++++++++---------------------
core/src/smtp/error.rs | 12 +++++
2 files changed, 60 insertions(+), 59 deletions(-)
diff --git a/core/src/smtp/connect.rs b/core/src/smtp/connect.rs
index f3dc2fad9..904a3670b 100644
--- a/core/src/smtp/connect.rs
+++ b/core/src/smtp/connect.rs
@@ -46,9 +46,7 @@ macro_rules! try_smtp (
log::debug!(
target: LOG_TARGET,
"[email={}] Closing [host={}:{}], because of error '{:?}'.",
- $to_email,
- $host,
- $port,err,
+ $to_email, $host, $port,err,
);
// Try to close the connection, but ignore if there's an error.
let _ = $client.quit().await;
@@ -58,67 +56,65 @@ macro_rules! try_smtp (
})
);
-/// Attempt to connect to host via SMTP, and return SMTP client on success.
-async fn connect_to_host(
+/// Connect to an SMTP host and return the configured client transport.
+async fn connect_to_smtp_host(
host: &str,
port: u16,
input: &CheckEmailInput,
) -> Result>>, SmtpError> {
// hostname verification fails if it ends with '.', for example, using
// SOCKS5 proxies we can `io: incomplete` error.
- let host = host.trim_end_matches('.').to_string();
+ let clean_host = host.trim_end_matches('.').to_string();
let smtp_client = SmtpClient::new().hello_name(ClientId::Domain(input.hello_name.clone()));
let stream: BufStream> = match &input.proxy {
Some(proxy) => {
- let socks_stream = match (&proxy.username, &proxy.password) {
- (Some(username), Some(password)) => {
+ let socks_stream =
+ if let (Some(username), Some(password)) = (&proxy.username, &proxy.password) {
Socks5Stream::connect_with_password(
(proxy.host.as_ref(), proxy.port),
- host.clone(),
+ clean_host.clone(),
port,
username.clone(),
password.clone(),
Config::default(),
)
.await?
- }
- _ => {
+ } else {
Socks5Stream::connect(
(proxy.host.as_ref(), proxy.port),
- host.clone(),
+ clean_host.clone(),
port,
Config::default(),
)
.await?
- }
- };
+ };
BufStream::new(Box::new(socks_stream) as Box)
}
None => {
- let tcp_stream = TcpStream::connect(format!("{}:{}", host, port)).await?;
+ let tcp_stream = TcpStream::connect(format!("{}:{}", clean_host, port)).await?;
BufStream::new(Box::new(tcp_stream) as Box)
}
};
let mut smtp_transport = SmtpTransport::new(smtp_client, stream).await?;
- // "MAIL FROM: user@example.org"
- let from_email = EmailAddress::from_str(input.from_email.as_ref()).unwrap_or_else(|_| {
+ // Set "MAIL FROM"
+ let from_email = EmailAddress::from_str(&input.from_email).unwrap_or_else(|_| {
log::warn!(
- "Inputted from_email \"{}\" is not a valid email, using \"user@example.org\" instead",
+ "Invalid 'from_email' provided: '{}'. Using default: 'user@example.org'",
input.from_email
);
- EmailAddress::from_str("user@example.org").expect("This is a valid email. qed.")
+ EmailAddress::from_str("user@example.org").expect("Default email is valid")
});
try_smtp!(
smtp_transport
.get_mut()
- .command(MailCommand::new(Some(from_email.into_inner()), vec![],))
+ .command(MailCommand::new(Some(from_email.into_inner()), vec![]))
.await,
smtp_transport,
input.to_email,
- host,
+ clean_host,
port
);
@@ -136,43 +132,39 @@ struct Deliverability {
is_disabled: bool,
}
-/// Check if `to_email` exists on host SMTP server. This is the core logic of
-/// this tool.
-async fn email_deliverable(
+/// Checks deliverability of a target email address using the provided SMTP transport.
+async fn check_email_deliverability(
smtp_transport: &mut SmtpTransport,
to_email: &EmailAddress,
) -> Result {
- // "RCPT TO: "
match smtp_transport
.get_mut()
.command(RcptCommand::new(to_email.clone().into_inner(), vec![]))
.await
{
- Ok(_) => {
- // According to RFC 5321, `RCPT TO` command succeeds with 250 and
- // 251 codes only (no 3xx codes at all):
- // https://tools.ietf.org/html/rfc5321#page-56
- //
- // Where the 251 code is used for forwarding, which is not our case,
- // because we always deliver to the SMTP server hosting the address
- // itself.
- //
- // So, if `response.is_positive()` (which is a condition for
- // returning `Ok` from the `command()` method above), then delivery
- // succeeds, accordingly to RFC 5321.
- Ok(Deliverability {
- has_full_inbox: false,
- is_deliverable: true, // response.is_positive()
- is_disabled: false,
- })
- }
+ // According to RFC 5321, `RCPT TO` command succeeds with 250 and
+ // 251 codes only (no 3xx codes at all):
+ // https://tools.ietf.org/html/rfc5321#page-56
+ //
+ // Where the 251 code is used for forwarding, which is not our case,
+ // because we always deliver to the SMTP server hosting the address
+ // itself.
+ //
+ // So, if `response.is_positive()` (which is a condition for
+ // returning `Ok` from the `command()` method above), then delivery
+ // succeeds, accordingly to RFC 5321.
+ Ok(_) => Ok(Deliverability {
+ has_full_inbox: false,
+ is_deliverable: true, // response.is_positive()
+ is_disabled: false,
+ }),
Err(err) => {
// We cast to lowercase, because our matched strings below are all
// lowercase.
let err_string = err.to_string().to_lowercase();
// Check if the email account has been disabled or blocked.
- if parser::is_disabled_account(err_string.as_str()) {
+ if parser::is_disabled_account(&err_string) {
return Ok(Deliverability {
has_full_inbox: false,
is_deliverable: false,
@@ -181,7 +173,7 @@ async fn email_deliverable(
}
// Check if the email account has a full inbox.
- if parser::is_full_inbox(err_string.as_str()) {
+ if parser::is_full_inbox(&err_string) {
return Ok(Deliverability {
has_full_inbox: true,
is_deliverable: false,
@@ -203,7 +195,7 @@ async fn email_deliverable(
}
// Check that the mailbox doesn't exist.
- if parser::is_invalid(err_string.as_str(), to_email) {
+ if parser::is_invalid(&err_string, to_email) {
return Ok(Deliverability {
has_full_inbox: false,
is_deliverable: false,
@@ -217,14 +209,13 @@ async fn email_deliverable(
}
}
-/// Verify the existence of a catch-all on the domain.
+/// Checks if the domain has a catch-all email setup.
async fn smtp_is_catch_all(
smtp_transport: &mut SmtpTransport,
domain: &str,
host: &str,
input: &CheckEmailInput,
) -> Result {
- // Skip catch-all check for known providers.
if has_rule(domain, host, &Rule::SkipCatchAll) {
log::debug!(
target: LOG_TARGET,
@@ -240,16 +231,14 @@ async fn smtp_is_catch_all(
.map(char::from)
.take(15)
.collect();
- let random_email = EmailAddress::new(format!("{random_email}@{domain}"));
+ let random_email = EmailAddress::new(format!("{}@{}", random_email, domain))?;
- email_deliverable(
- smtp_transport,
- &random_email.expect("Email is correctly constructed. qed."),
- )
- .await
- .map(|deliverability| deliverability.is_deliverable)
+ check_email_deliverability(smtp_transport, &random_email)
+ .await
+ .map(|result| result.is_deliverable)
}
+/// Creates an SMTP future for email verification.
async fn create_smtp_future(
to_email: &EmailAddress,
host: &str,
@@ -259,7 +248,7 @@ async fn create_smtp_future(
) -> Result<(bool, Deliverability), SmtpError> {
// FIXME If the SMTP is not connectable, we should actually return an
// Ok(SmtpDetails { can_connect_smtp: false, ... }).
- let mut smtp_transport = connect_to_host(host, port, input).await?;
+ let mut smtp_transport = connect_to_smtp_host(host, port, input).await?;
let is_catch_all = smtp_is_catch_all(&mut smtp_transport, domain, host, input)
.await
@@ -271,7 +260,7 @@ async fn create_smtp_future(
is_disabled: false,
}
} else {
- let mut result = email_deliverable(&mut smtp_transport, to_email).await;
+ let mut result = check_email_deliverability(&mut smtp_transport, to_email).await;
// Some SMTP servers automatically close the connection after an error,
// so we should reconnect to perform a next command.
@@ -288,8 +277,8 @@ async fn create_smtp_future(
);
let _ = smtp_transport.quit().await;
- smtp_transport = connect_to_host(host, port, input).await?;
- result = email_deliverable(&mut smtp_transport, to_email).await;
+ smtp_transport = connect_to_smtp_host(host, port, input).await?;
+ result = check_email_deliverability(&mut smtp_transport, to_email).await;
}
}
diff --git a/core/src/smtp/error.rs b/core/src/smtp/error.rs
index 2dad4d440..1d4631f26 100644
--- a/core/src/smtp/error.rs
+++ b/core/src/smtp/error.rs
@@ -56,6 +56,12 @@ pub enum SmtpError {
#[error("SOCKS5 error: {0}")]
#[serde(serialize_with = "ser_with_display")]
Socks5(fast_socks5::SocksError),
+ /// Anyhow error.
+ /// This is a catch-all error type for any error that can't be categorized
+ /// into the above types.
+ #[error("Anyhow error: {0}")]
+ #[serde(serialize_with = "ser_with_display")]
+ AnyhowError(anyhow::Error),
}
impl From for SmtpError {
@@ -100,6 +106,12 @@ impl From for SmtpError {
}
}
+impl From for SmtpError {
+ fn from(e: anyhow::Error) -> Self {
+ SmtpError::AnyhowError(e)
+ }
+}
+
impl SmtpError {
/// Get a human-understandable description of the error, in form of an enum
/// SmtpErrorDesc. This only parses the following known errors: