From 36deb1f13160b80e90fce33449bea42b976c7ad2 Mon Sep 17 00:00:00 2001 From: PsypherPunk Date: Tue, 4 Oct 2022 13:47:07 +0100 Subject: [PATCH] feat: additional Gmail validation - check the validity of `gmail.com`/`googlemail.com` email addresses via the method outlined [here](https://blog.0day.rocks/abusing-gmail-to-get-previously-unlisted-e-mail-addresses-41544b62b2). - run only via the `--gmail-use-api`/`gmail_use_api` flags (defaulting to `false`.) relates #937 --- cli/README.md | 4 ++ cli/src/main.rs | 8 ++- core/src/smtp/connect.rs | 1 + core/src/smtp/error.rs | 9 ++++ core/src/smtp/gmail.rs | 92 +++++++++++++++++++++++++++++++++++ core/src/smtp/mod.rs | 8 +++ core/src/util/input_output.rs | 13 +++++ 7 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 core/src/smtp/gmail.rs diff --git a/cli/README.md b/cli/README.md index 8bda86bc6a..56e19939d9 100644 --- a/cli/README.md +++ b/cli/README.md @@ -59,6 +59,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 a21278ea6f..116a2e56b0 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, + /// The email to check. pub to_email: String, } @@ -76,7 +81,8 @@ async fn main() -> Result<(), Box> { .set_from_email(CONF.from_email.clone()) .set_hello_name(CONF.hello_name.clone()) .set_smtp_port(CONF.smtp_port) - .set_yahoo_use_api(CONF.yahoo_use_api); + .set_yahoo_use_api(CONF.yahoo_use_api) + .set_gmail_use_api(CONF.gmail_use_api); if let Some(proxy_host) = &CONF.proxy_host { input.set_proxy(CheckEmailInputProxy { host: proxy_host.clone(), 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 48fe8d9049..dacf81a5b1 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, /// For Hotmail/Outlook email addresses, use a headless navigator /// connecting to the password recovery page instead of the SMTP server. /// This assumes you have a WebDriver compatible process running, then pass @@ -122,6 +127,7 @@ impl Default for CheckEmailInput { smtp_security: SmtpSecurity::Opportunistic, smtp_timeout: None, yahoo_use_api: true, + gmail_use_api: false, retries: 2, } } @@ -229,6 +235,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 + } + /// Set whether or not to use a headless navigator to navigate to Hotmail's /// password recovery page to check if an email exists. If set to /// `Some()`, this endpoint must point to a WebDriver process,