diff --git a/cli/README.md b/cli/README.md index ba3115fa0a..d11a9a635c 100644 --- a/cli/README.md +++ b/cli/README.md @@ -62,6 +62,10 @@ OPTIONS: --yahoo-use-api For Yahoo email addresses, use Yahoo's API instead of connecting directly to their SMTP servers [env: YAHOO_USE_API=] [default: true] + + --gmail-use-api + For Gmail email addresses, use Gmail's API instead of connecting directly to their SMTP + servers [env: GMAIL_USE_API=] [default: false] ``` **💡 PRO TIP:** To show debug logs when running the binary, run: diff --git a/cli/src/main.rs b/cli/src/main.rs index 5a9ea8bf70..6e00d92f1f 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -58,6 +58,11 @@ pub struct Cli { #[clap(long, env, default_value = "true", parse(try_from_str))] pub yahoo_use_api: bool, + /// For Gmail email addresses, use Gmail's API instead of connecting + /// directly to their SMTP servers. + #[clap(long, env, default_value = "false", parse(try_from_str))] + pub gmail_use_api: bool, + /// Whether to check if a gravatar image is existing for the given email. #[clap(long, env, default_value = "false", parse(try_from_str))] pub check_gravatar: bool, @@ -81,6 +86,7 @@ async fn main() -> Result<(), Box> { .set_hello_name(CONF.hello_name.clone()) .set_smtp_port(CONF.smtp_port) .set_yahoo_use_api(CONF.yahoo_use_api) + .set_gmail_use_api(CONF.gmail_use_api) .set_check_gravatar(CONF.check_gravatar); if let Some(proxy_host) = &CONF.proxy_host { input.set_proxy(CheckEmailInputProxy { diff --git a/core/src/smtp/connect.rs b/core/src/smtp/connect.rs index 3092397cc3..e93f0bd523 100644 --- a/core/src/smtp/connect.rs +++ b/core/src/smtp/connect.rs @@ -352,6 +352,7 @@ pub async fn check_smtp_with_retry( #[cfg(feature = "headless")] Err(SmtpError::HotmailError(_)) => result, Err(SmtpError::YahooError(_)) => result, + Err(SmtpError::GmailError(_)) => result, // Only retry if the SMTP error was unknown. Err(err) if err.get_description().is_none() => { if count <= 1 { diff --git a/core/src/smtp/error.rs b/core/src/smtp/error.rs index 8abadb5c30..2c302dfb10 100644 --- a/core/src/smtp/error.rs +++ b/core/src/smtp/error.rs @@ -14,6 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +use super::gmail::GmailError; #[cfg(feature = "headless")] use super::hotmail::HotmailError; use super::parser; @@ -39,6 +40,8 @@ pub enum SmtpError { TimeoutError(future::TimeoutError), /// Error when verifying a Yahoo email via HTTP requests. YahooError(YahooError), + /// Error when verifying a Gmail email via a HTTP request. + GmailError(GmailError), /// Error when verifying a Hotmail email via headless browser. #[cfg(feature = "headless")] HotmailError(HotmailError), @@ -62,6 +65,12 @@ impl From for SmtpError { } } +impl From for SmtpError { + fn from(e: GmailError) -> Self { + SmtpError::GmailError(e) + } +} + #[cfg(feature = "headless")] impl From for SmtpError { fn from(e: HotmailError) -> Self { diff --git a/core/src/smtp/gmail.rs b/core/src/smtp/gmail.rs new file mode 100644 index 0000000000..5980a39c61 --- /dev/null +++ b/core/src/smtp/gmail.rs @@ -0,0 +1,92 @@ +// 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 super::SmtpDetails; +use crate::util::{ + constants::LOG_TARGET, input_output::CheckEmailInput, ser_with_display::ser_with_display, +}; +use async_smtp::EmailAddress; +use reqwest::Error as ReqwestError; +use serde::Serialize; +use std::fmt; + +const GLXU_PAGE: &str = "https://mail.google.com/mail/gxlu"; + +/// Possible errors when checking Gmail email addresses. +#[derive(Debug, Serialize)] +pub enum GmailError { + /// Error when serializing or deserializing HTTP requests and responses. + #[serde(serialize_with = "ser_with_display")] + ReqwestError(ReqwestError), +} + +impl fmt::Display for GmailError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +impl From for GmailError { + fn from(error: ReqwestError) -> Self { + GmailError::ReqwestError(error) + } +} + +/// Helper function to create a reqwest client, with optional proxy. +fn create_client(input: &CheckEmailInput) -> Result { + if let Some(proxy) = &input.proxy { + log::debug!( + target: LOG_TARGET, + "[email={}] Using proxy socks://{}:{} for gmail API", + input.to_email, + proxy.host, + proxy.port + ); + + let proxy = reqwest::Proxy::all(&format!("socks5://{}:{}", proxy.host, proxy.port))?; + reqwest::Client::builder().proxy(proxy).build() + } else { + Ok(reqwest::Client::new()) + } +} + +/// Use HTTP request to verify if a Gmail email address exists. +/// See: +pub async fn check_gmail( + to_email: &EmailAddress, + input: &CheckEmailInput, +) -> Result { + let response = create_client(input)? + .head(GLXU_PAGE) + .query(&[("email", to_email)]) + .send() + .await?; + + let email_exists = response.headers().contains_key("Set-Cookie"); + + log::debug!( + target: LOG_TARGET, + "[email={}] gmail response: {:?}", + to_email, + response + ); + + Ok(SmtpDetails { + can_connect_smtp: true, + is_deliverable: email_exists, + ..Default::default() + }) +} diff --git a/core/src/smtp/mod.rs b/core/src/smtp/mod.rs index dd725fe67b..2f973beeca 100644 --- a/core/src/smtp/mod.rs +++ b/core/src/smtp/mod.rs @@ -16,6 +16,7 @@ mod connect; mod error; +mod gmail; #[cfg(feature = "headless")] mod hotmail; mod parser; @@ -62,6 +63,13 @@ pub async fn check_smtp( .await .map_err(|err| err.into()); } + if input.gmail_use_api + && (host_lowercase.contains("gmail") || host_lowercase.contains("googlemail")) + { + return gmail::check_gmail(to_email, input) + .await + .map_err(|err| err.into()); + } #[cfg(feature = "headless")] if let Some(webdriver) = &input.hotmail_use_headless { if host_lowercase.contains("outlook") { diff --git a/core/src/util/input_output.rs b/core/src/util/input_output.rs index 9b21cc8739..9e668e3db0 100644 --- a/core/src/util/input_output.rs +++ b/core/src/util/input_output.rs @@ -90,6 +90,11 @@ pub struct CheckEmailInput { /// /// Defaults to true. pub yahoo_use_api: bool, + /// For Gmail email addresses, use Gmail's API instead of connecting + /// directly to their SMTP servers. + /// + /// Defaults to false. + pub gmail_use_api: bool, // Whether to check if a gravatar image is existing for the given email. // // Defaults to false @@ -126,6 +131,7 @@ impl Default for CheckEmailInput { smtp_security: SmtpSecurity::Opportunistic, smtp_timeout: None, yahoo_use_api: true, + gmail_use_api: false, check_gravatar: false, retries: 2, } @@ -234,6 +240,13 @@ impl CheckEmailInput { self } + /// Set whether to use Gmail's API or connecting directly to their SMTP + /// servers. Defaults to false. + pub fn set_gmail_use_api(&mut self, use_api: bool) -> &mut CheckEmailInput { + self.gmail_use_api = use_api; + self + } + /// Whether to check if a gravatar image is existing for the given email. /// Defaults to false. pub fn set_check_gravatar(&mut self, check_gravatar: bool) -> &mut CheckEmailInput {