valid_email_checker 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +24 -0
- data/Cargo.lock +2940 -0
- data/Cargo.toml +22 -0
- data/LICENSE.txt +661 -0
- data/NOTICE.md +56 -0
- data/README.md +316 -0
- data/exe/valid_email_checker +148 -0
- data/ext/valid_email_checker/Cargo.toml +27 -0
- data/ext/valid_email_checker/extconf.rb +26 -0
- data/ext/valid_email_checker/src/lib.rs +371 -0
- data/ext/valid_email_checker/src/nogvl.rs +64 -0
- data/ext/valid_email_checker/vendor/UPSTREAM.json +9 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/Cargo.toml +52 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/LICENSE.AGPL +661 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/LICENSE.md +11 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/README.md +175 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/haveibeenpwned.rs +70 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/lib.rs +281 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/misc/b2c.txt +96640 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/misc/gravatar.rs +60 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/misc/mod.rs +124 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/misc/roles.txt +944 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/mx/mod.rs +165 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/rules.json +28 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/rules.rs +105 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/connect.rs +396 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/error.rs +144 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/gmail.rs +99 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/headless.rs +82 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/http_api.rs +27 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/mod.rs +234 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/outlook/headless.rs +181 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/outlook/microsoft365.rs +109 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/outlook/mod.rs +2 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/parser.rs +291 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/verif_method.rs +531 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/yahoo/api.rs +174 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/yahoo/headless.rs +188 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/yahoo/mod.rs +62 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/syntax/mod.rs +199 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/syntax/normalize.rs +70 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/util/input_output.rs +353 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/util/mod.rs +20 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/util/sentry.rs +173 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/util/ser_with_display.rs +28 -0
- data/lib/valid_email_checker/configuration.rb +216 -0
- data/lib/valid_email_checker/errors.rb +32 -0
- data/lib/valid_email_checker/result.rb +241 -0
- data/lib/valid_email_checker/syntax.rb +72 -0
- data/lib/valid_email_checker/version.rb +5 -0
- data/lib/valid_email_checker.rb +217 -0
- metadata +118 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// check-if-email-exists
|
|
2
|
+
// Copyright (C) 2018-2023 Reacher
|
|
3
|
+
|
|
4
|
+
// This program is free software: you can redistribute it and/or modify
|
|
5
|
+
// it under the terms of the GNU Affero General Public License as published
|
|
6
|
+
// by the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
// (at your option) any later version.
|
|
8
|
+
|
|
9
|
+
// This program is distributed in the hope that it will be useful,
|
|
10
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
// GNU Affero General Public License for more details.
|
|
13
|
+
|
|
14
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
use super::gmail::GmailError;
|
|
18
|
+
use super::headless::HeadlessError;
|
|
19
|
+
use super::outlook::microsoft365::Microsoft365Error;
|
|
20
|
+
use super::parser;
|
|
21
|
+
use super::yahoo::YahooError;
|
|
22
|
+
use crate::util::ser_with_display::ser_with_display;
|
|
23
|
+
use async_smtp::error::Error as AsyncSmtpError;
|
|
24
|
+
use serde::Serialize;
|
|
25
|
+
use std::time::Duration;
|
|
26
|
+
use thiserror::Error;
|
|
27
|
+
|
|
28
|
+
/// Error occurred connecting to this email server via SMTP.
|
|
29
|
+
#[derive(Debug, Error, Serialize)]
|
|
30
|
+
#[serde(tag = "type", content = "message")]
|
|
31
|
+
pub enum SmtpError {
|
|
32
|
+
/// Error when verifying a Yahoo email via HTTP requests.
|
|
33
|
+
#[error("Yahoo error: {0}")]
|
|
34
|
+
YahooError(YahooError),
|
|
35
|
+
/// Error when verifying a Gmail email via a HTTP request.
|
|
36
|
+
#[error("Gmail error: {0}")]
|
|
37
|
+
GmailError(GmailError),
|
|
38
|
+
/// Error when verifying a Hotmail email via headless browser.
|
|
39
|
+
#[error("Headless verification error: {0}")]
|
|
40
|
+
HeadlessError(HeadlessError),
|
|
41
|
+
/// Error when verifying a Microsoft 365 email via HTTP request.
|
|
42
|
+
#[error("Microsoft 365 API error: {0}")]
|
|
43
|
+
Microsoft365Error(Microsoft365Error),
|
|
44
|
+
/// Error from async-smtp crate.
|
|
45
|
+
#[error("SMTP error: {0}")]
|
|
46
|
+
#[serde(serialize_with = "ser_with_display")]
|
|
47
|
+
AsyncSmtpError(AsyncSmtpError),
|
|
48
|
+
/// I/O error.
|
|
49
|
+
#[error("I/O error: {0}")]
|
|
50
|
+
#[serde(serialize_with = "ser_with_display")]
|
|
51
|
+
IOError(std::io::Error),
|
|
52
|
+
/// Timeout error.
|
|
53
|
+
#[error("Timeout error: {0:?}")]
|
|
54
|
+
Timeout(Duration),
|
|
55
|
+
/// SOCKS5 proxy error.
|
|
56
|
+
#[error("SOCKS5 error: {0}")]
|
|
57
|
+
#[serde(serialize_with = "ser_with_display")]
|
|
58
|
+
Socks5(fast_socks5::SocksError),
|
|
59
|
+
/// Anyhow error.
|
|
60
|
+
/// This is a catch-all error type for any error that can't be categorized
|
|
61
|
+
/// into the above types.
|
|
62
|
+
#[error("Anyhow error: {0}")]
|
|
63
|
+
#[serde(serialize_with = "ser_with_display")]
|
|
64
|
+
AnyhowError(anyhow::Error),
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
impl From<YahooError> for SmtpError {
|
|
68
|
+
fn from(e: YahooError) -> Self {
|
|
69
|
+
SmtpError::YahooError(e)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
impl From<GmailError> for SmtpError {
|
|
74
|
+
fn from(e: GmailError) -> Self {
|
|
75
|
+
SmtpError::GmailError(e)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
impl From<HeadlessError> for SmtpError {
|
|
80
|
+
fn from(e: HeadlessError) -> Self {
|
|
81
|
+
SmtpError::HeadlessError(e)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
impl From<Microsoft365Error> for SmtpError {
|
|
86
|
+
fn from(e: Microsoft365Error) -> Self {
|
|
87
|
+
SmtpError::Microsoft365Error(e)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
impl From<AsyncSmtpError> for SmtpError {
|
|
92
|
+
fn from(e: AsyncSmtpError) -> Self {
|
|
93
|
+
SmtpError::AsyncSmtpError(e)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
impl From<std::io::Error> for SmtpError {
|
|
98
|
+
fn from(e: std::io::Error) -> Self {
|
|
99
|
+
SmtpError::IOError(e)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
impl From<fast_socks5::SocksError> for SmtpError {
|
|
104
|
+
fn from(e: fast_socks5::SocksError) -> Self {
|
|
105
|
+
SmtpError::Socks5(e)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
impl From<anyhow::Error> for SmtpError {
|
|
110
|
+
fn from(e: anyhow::Error) -> Self {
|
|
111
|
+
SmtpError::AnyhowError(e)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
impl SmtpError {
|
|
116
|
+
/// Get a human-understandable description of the error, in form of an enum
|
|
117
|
+
/// SmtpErrorDesc. This only parses the following known errors:
|
|
118
|
+
/// - IP blacklisted
|
|
119
|
+
/// - IP needs reverse DNS
|
|
120
|
+
pub fn get_description(&self) -> Option<SmtpErrorDesc> {
|
|
121
|
+
match self {
|
|
122
|
+
SmtpError::AsyncSmtpError(_) => {
|
|
123
|
+
if parser::is_err_ip_blacklisted(self) {
|
|
124
|
+
Some(SmtpErrorDesc::IpBlacklisted)
|
|
125
|
+
} else if parser::is_err_needs_rdns(self) {
|
|
126
|
+
Some(SmtpErrorDesc::NeedsRDNS)
|
|
127
|
+
} else {
|
|
128
|
+
None
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
_ => None,
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#[derive(Debug, Serialize)]
|
|
137
|
+
/// SmtpErrorDesc describes a description of which category the error belongs
|
|
138
|
+
/// to.
|
|
139
|
+
pub enum SmtpErrorDesc {
|
|
140
|
+
/// The IP is blacklisted.
|
|
141
|
+
IpBlacklisted,
|
|
142
|
+
/// The IP needs a reverse DNS entry.
|
|
143
|
+
NeedsRDNS,
|
|
144
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// check-if-email-exists
|
|
2
|
+
// Copyright (C) 2018-2023 Reacher
|
|
3
|
+
|
|
4
|
+
// This program is free software: you can redistribute it and/or modify
|
|
5
|
+
// it under the terms of the GNU Affero General Public License as published
|
|
6
|
+
// by the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
// (at your option) any later version.
|
|
8
|
+
|
|
9
|
+
// This program is distributed in the hope that it will be useful,
|
|
10
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
// GNU Affero General Public License for more details.
|
|
13
|
+
|
|
14
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
use super::SmtpDetails;
|
|
18
|
+
use crate::EmailAddress;
|
|
19
|
+
use crate::LOG_TARGET;
|
|
20
|
+
use crate::{
|
|
21
|
+
smtp::http_api::create_client,
|
|
22
|
+
util::{input_output::CheckEmailInput, ser_with_display::ser_with_display},
|
|
23
|
+
};
|
|
24
|
+
use reqwest::Error as ReqwestError;
|
|
25
|
+
use serde::Serialize;
|
|
26
|
+
use thiserror::Error;
|
|
27
|
+
|
|
28
|
+
const GLXU_PAGE: &str = "https://mail.google.com/mail/gxlu";
|
|
29
|
+
|
|
30
|
+
/// Possible errors when checking Gmail email addresses.
|
|
31
|
+
#[derive(Debug, Error, Serialize)]
|
|
32
|
+
pub enum GmailError {
|
|
33
|
+
/// Error when serializing or deserializing HTTP requests and responses.
|
|
34
|
+
#[serde(serialize_with = "ser_with_display")]
|
|
35
|
+
#[error("Error serializing or deserializing HTTP requests and responses: {0}")]
|
|
36
|
+
ReqwestError(ReqwestError),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
impl From<ReqwestError> for GmailError {
|
|
40
|
+
fn from(error: ReqwestError) -> Self {
|
|
41
|
+
GmailError::ReqwestError(error)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#[allow(dead_code)]
|
|
46
|
+
/// Use HTTP request to verify if a Gmail email address exists.
|
|
47
|
+
/// See: <https://blog.0day.rocks/abusing-gmail-to-get-previously-unlisted-e-mail-addresses-41544b62b2>
|
|
48
|
+
///
|
|
49
|
+
/// This method doesn't work anymore, as Google has patched the vulnerability,
|
|
50
|
+
/// but it's kept here for historical reasons.
|
|
51
|
+
#[deprecated]
|
|
52
|
+
pub async fn check_gmail_via_api(
|
|
53
|
+
to_email: &EmailAddress,
|
|
54
|
+
input: &CheckEmailInput,
|
|
55
|
+
) -> Result<SmtpDetails, GmailError> {
|
|
56
|
+
let response = create_client(input, "gmail")?
|
|
57
|
+
.head(GLXU_PAGE)
|
|
58
|
+
.query(&[("email", to_email)])
|
|
59
|
+
.send()
|
|
60
|
+
.await?;
|
|
61
|
+
|
|
62
|
+
let email_exists = response.headers().contains_key("Set-Cookie");
|
|
63
|
+
|
|
64
|
+
tracing::debug!(
|
|
65
|
+
target: LOG_TARGET,
|
|
66
|
+
email = %to_email,
|
|
67
|
+
response = ?response,
|
|
68
|
+
"Gmail API response"
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
Ok(SmtpDetails {
|
|
72
|
+
can_connect_smtp: true,
|
|
73
|
+
is_deliverable: email_exists,
|
|
74
|
+
..Default::default()
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
#[cfg(test)]
|
|
79
|
+
mod tests {
|
|
80
|
+
use super::*;
|
|
81
|
+
use crate::CheckEmailInputBuilder;
|
|
82
|
+
use std::str::FromStr;
|
|
83
|
+
|
|
84
|
+
#[tokio::test]
|
|
85
|
+
#[ignore] // ref: https://github.com/reacherhq/check-if-email-exists/issues/1431
|
|
86
|
+
async fn should_return_is_deliverable_true() {
|
|
87
|
+
let to_email = EmailAddress::from_str("someone@gmail.com").unwrap();
|
|
88
|
+
let input = CheckEmailInputBuilder::default()
|
|
89
|
+
.to_email("someone@gmail.com".to_owned())
|
|
90
|
+
.build()
|
|
91
|
+
.unwrap();
|
|
92
|
+
|
|
93
|
+
#[allow(deprecated)]
|
|
94
|
+
let smtp_details = check_gmail_via_api(&to_email, &input).await;
|
|
95
|
+
|
|
96
|
+
assert!(smtp_details.is_ok());
|
|
97
|
+
assert!(smtp_details.unwrap().is_deliverable);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// check-if-email-exists
|
|
2
|
+
// Copyright (C) 2018-2023 Reacher
|
|
3
|
+
|
|
4
|
+
// This program is free software: you can redistribute it and/or modify
|
|
5
|
+
// it under the terms of the GNU Affero General Public License as published
|
|
6
|
+
// by the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
// (at your option) any later version.
|
|
8
|
+
|
|
9
|
+
// This program is distributed in the hope that it will be useful,
|
|
10
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
// GNU Affero General Public License for more details.
|
|
13
|
+
|
|
14
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
use crate::{util::ser_with_display::ser_with_display, WebdriverConfig};
|
|
18
|
+
use fantoccini::{
|
|
19
|
+
error::{CmdError, NewSessionError},
|
|
20
|
+
Client, ClientBuilder,
|
|
21
|
+
};
|
|
22
|
+
use serde::Serialize;
|
|
23
|
+
use serde_json::Map;
|
|
24
|
+
use thiserror::Error;
|
|
25
|
+
|
|
26
|
+
#[derive(Debug, Error, Serialize)]
|
|
27
|
+
pub enum HeadlessError {
|
|
28
|
+
#[serde(serialize_with = "ser_with_display")]
|
|
29
|
+
#[error("Fantoccini error: {0}")]
|
|
30
|
+
Fantoccini(#[from] std::io::Error),
|
|
31
|
+
#[serde(serialize_with = "ser_with_display")]
|
|
32
|
+
#[error("Cmd error: {0}")]
|
|
33
|
+
Cmd(#[from] CmdError),
|
|
34
|
+
#[serde(serialize_with = "ser_with_display")]
|
|
35
|
+
#[error("New session error: {0}")]
|
|
36
|
+
NewSession(#[from] NewSessionError),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
pub async fn create_headless_client(
|
|
40
|
+
webdriver: &str,
|
|
41
|
+
webdriver_config: &WebdriverConfig,
|
|
42
|
+
) -> Result<Client, HeadlessError> {
|
|
43
|
+
let mut opts = serde_json::json!({
|
|
44
|
+
"args": [
|
|
45
|
+
"--headless=new", "--disable-gpu", "--disable-dev-shm-usage",
|
|
46
|
+
// Running in a Docker container, I run into the following error:
|
|
47
|
+
// Failed to move to new namespace: PID namespaces supported, Network namespace supported, but failed: errno = Operation not permitted
|
|
48
|
+
// In searching around I found a few different workarounds:
|
|
49
|
+
// - Enable namespaces: https://github.com/jessfraz/dockerfiles/issues/65#issuecomment-266532289
|
|
50
|
+
// - Run it with a custom seccomp: https://github.com/jessfraz/dockerfiles/issues/65#issuecomment-217214671
|
|
51
|
+
// - Run with --no-sandbox: https://github.com/karma-runner/karma-chrome-launcher/issues/125#issuecomment-312668593
|
|
52
|
+
// For now I went with the --no-sandbox.
|
|
53
|
+
//
|
|
54
|
+
// TODO Look into security implications...
|
|
55
|
+
"--no-sandbox",
|
|
56
|
+
// From https://github.com/chromium-for-lambda/chromium-binaries/tree/b23e11c2f2859b177fd08fe50a0826c17652d846?tab=readme-ov-file#installation-via-a-lambda-layer
|
|
57
|
+
"--use-gl=angle", "--use-angle=swiftshader", "--single-process", "--no-zygote",
|
|
58
|
+
// Disable anything that might consume memory
|
|
59
|
+
"--window-size=800x600",
|
|
60
|
+
"--disable-extensions",
|
|
61
|
+
"--disable-software-rasterizer",
|
|
62
|
+
"--disable-dev-shm-usage",
|
|
63
|
+
"--disable-background-networking",
|
|
64
|
+
"--js-flags=\"--max-old-space-size=256\"",
|
|
65
|
+
]
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if let Some(binary) = &webdriver_config.binary {
|
|
69
|
+
opts["binary"] = serde_json::json!(binary);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let mut caps = Map::new();
|
|
73
|
+
caps.insert("goog:chromeOptions".to_string(), opts);
|
|
74
|
+
|
|
75
|
+
// Connect to WebDriver instance that is listening on `webdriver`
|
|
76
|
+
let c = ClientBuilder::rustls()?
|
|
77
|
+
.capabilities(caps)
|
|
78
|
+
.connect(webdriver)
|
|
79
|
+
.await?;
|
|
80
|
+
|
|
81
|
+
Ok(c)
|
|
82
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// check-if-email-exists
|
|
2
|
+
// Copyright (C) 2018-2023 Reacher
|
|
3
|
+
|
|
4
|
+
// This program is free software: you can redistribute it and/or modify
|
|
5
|
+
// it under the terms of the GNU Affero General Public License as published
|
|
6
|
+
// by the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
// (at your option) any later version.
|
|
8
|
+
|
|
9
|
+
// This program is distributed in the hope that it will be useful,
|
|
10
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
// GNU Affero General Public License for more details.
|
|
13
|
+
|
|
14
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
use crate::util::input_output::CheckEmailInput;
|
|
18
|
+
use reqwest::Error as ReqwestError;
|
|
19
|
+
|
|
20
|
+
/// Helper function to create a reqwest client, with optional proxy.
|
|
21
|
+
pub fn create_client(
|
|
22
|
+
_input: &CheckEmailInput,
|
|
23
|
+
_api_name: &str,
|
|
24
|
+
) -> Result<reqwest::Client, ReqwestError> {
|
|
25
|
+
// TODO: Allow proxying for HTTP API requests.
|
|
26
|
+
Ok(reqwest::Client::new())
|
|
27
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// check-if-email-exists
|
|
2
|
+
// Copyright (C) 2018-2023 Reacher
|
|
3
|
+
|
|
4
|
+
// This program is free software: you can redistribute it and/or modify
|
|
5
|
+
// it under the terms of the GNU Affero General Public License as published
|
|
6
|
+
// by the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
// (at your option) any later version.
|
|
8
|
+
|
|
9
|
+
// This program is distributed in the hope that it will be useful,
|
|
10
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
// GNU Affero General Public License for more details.
|
|
13
|
+
|
|
14
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
mod connect;
|
|
18
|
+
mod error;
|
|
19
|
+
mod gmail;
|
|
20
|
+
mod headless;
|
|
21
|
+
mod http_api;
|
|
22
|
+
mod outlook;
|
|
23
|
+
mod parser;
|
|
24
|
+
pub mod verif_method;
|
|
25
|
+
mod yahoo;
|
|
26
|
+
|
|
27
|
+
use crate::util::input_output::CheckEmailInput;
|
|
28
|
+
use crate::EmailAddress;
|
|
29
|
+
use connect::check_smtp_with_retry;
|
|
30
|
+
use hickory_proto::rr::Name;
|
|
31
|
+
use serde::{Deserialize, Serialize};
|
|
32
|
+
use std::default::Default;
|
|
33
|
+
use verif_method::{
|
|
34
|
+
EmailProvider, EverythingElseVerifMethod, GmailVerifMethod, HotmailB2BVerifMethod,
|
|
35
|
+
HotmailB2CVerifMethod, VerifMethodSmtp, VerifMethodSmtpConfig, YahooVerifMethod,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
pub use crate::mx::{is_gmail, is_hotmail, is_hotmail_b2b, is_hotmail_b2c, is_yahoo};
|
|
39
|
+
pub use error::*;
|
|
40
|
+
|
|
41
|
+
#[derive(Debug, Deserialize, Serialize)]
|
|
42
|
+
pub struct SmtpDebugVerifMethodSmtp {
|
|
43
|
+
/// The host we connected to via SMTP.
|
|
44
|
+
pub host: String,
|
|
45
|
+
/// The proxy used for the SMTP connection.
|
|
46
|
+
pub verif_method: VerifMethodSmtpConfig,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
#[derive(Debug, Default, Deserialize, Serialize)]
|
|
50
|
+
#[serde(tag = "type")]
|
|
51
|
+
pub enum SmtpDebugVerifMethod {
|
|
52
|
+
/// Email verification was done via SMTP.
|
|
53
|
+
Smtp(SmtpDebugVerifMethodSmtp),
|
|
54
|
+
/// Email verification was done via an HTTP API.
|
|
55
|
+
Api,
|
|
56
|
+
/// Email verification was done via a headless browser.
|
|
57
|
+
Headless,
|
|
58
|
+
/// Email verification was skipped.
|
|
59
|
+
#[default]
|
|
60
|
+
Skipped,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Details that we gathered from connecting to this email via SMTP
|
|
64
|
+
#[derive(Debug, Default, Deserialize, Serialize)]
|
|
65
|
+
pub struct SmtpDetails {
|
|
66
|
+
/// Are we able to connect to the SMTP server?
|
|
67
|
+
pub can_connect_smtp: bool,
|
|
68
|
+
/// Is this email account's inbox full?
|
|
69
|
+
pub has_full_inbox: bool,
|
|
70
|
+
/// Does this domain have a catch-all email address?
|
|
71
|
+
pub is_catch_all: bool,
|
|
72
|
+
/// Can we send an email to this address?
|
|
73
|
+
pub is_deliverable: bool,
|
|
74
|
+
/// Is the email blocked or disabled by the provider?
|
|
75
|
+
pub is_disabled: bool,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// Debug information on how the SMTP verification went.
|
|
79
|
+
#[derive(Debug, Default, Deserialize, Serialize)]
|
|
80
|
+
pub struct SmtpDebug {
|
|
81
|
+
/// The verification method used for the email.
|
|
82
|
+
pub verif_method: SmtpDebugVerifMethod,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// Get all email details we can from one single `EmailAddress`, without
|
|
86
|
+
/// retries.
|
|
87
|
+
pub async fn check_smtp(
|
|
88
|
+
to_email: &EmailAddress,
|
|
89
|
+
host: &Name,
|
|
90
|
+
domain: &str,
|
|
91
|
+
input: &CheckEmailInput,
|
|
92
|
+
) -> (Result<SmtpDetails, SmtpError>, SmtpDebug) {
|
|
93
|
+
let host_str = host.to_string();
|
|
94
|
+
let to_email_str = to_email.to_string();
|
|
95
|
+
let email_provider = EmailProvider::from_mx_host(&host_str);
|
|
96
|
+
|
|
97
|
+
// Handle all non-SMTP verifications first, and return early. For the rest,
|
|
98
|
+
// we'll use SMTP, and return the config.
|
|
99
|
+
let smtp_verif_method_config = match &email_provider {
|
|
100
|
+
EmailProvider::HotmailB2C => match &input.verif_method.hotmailb2c {
|
|
101
|
+
HotmailB2CVerifMethod::Headless => {
|
|
102
|
+
return (
|
|
103
|
+
outlook::headless::check_password_recovery(
|
|
104
|
+
&to_email_str,
|
|
105
|
+
&input.webdriver_addr,
|
|
106
|
+
&input.webdriver_config,
|
|
107
|
+
)
|
|
108
|
+
.await
|
|
109
|
+
.map_err(Into::into),
|
|
110
|
+
SmtpDebug {
|
|
111
|
+
verif_method: SmtpDebugVerifMethod::Headless,
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
HotmailB2CVerifMethod::Smtp(c) => c,
|
|
116
|
+
},
|
|
117
|
+
EmailProvider::Yahoo => match &input.verif_method.yahoo {
|
|
118
|
+
YahooVerifMethod::Api => {
|
|
119
|
+
return (
|
|
120
|
+
yahoo::check_api(&to_email_str, input)
|
|
121
|
+
.await
|
|
122
|
+
.map_err(Into::into),
|
|
123
|
+
SmtpDebug {
|
|
124
|
+
verif_method: SmtpDebugVerifMethod::Api,
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
YahooVerifMethod::Headless => {
|
|
129
|
+
return (
|
|
130
|
+
yahoo::check_headless(
|
|
131
|
+
&to_email_str,
|
|
132
|
+
&input.webdriver_addr,
|
|
133
|
+
&input.webdriver_config,
|
|
134
|
+
)
|
|
135
|
+
.await
|
|
136
|
+
.map_err(Into::into),
|
|
137
|
+
SmtpDebug {
|
|
138
|
+
verif_method: SmtpDebugVerifMethod::Headless,
|
|
139
|
+
},
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
YahooVerifMethod::Smtp(c) => c,
|
|
143
|
+
},
|
|
144
|
+
EmailProvider::Gmail => match &input.verif_method.gmail {
|
|
145
|
+
GmailVerifMethod::Smtp(c) => c,
|
|
146
|
+
},
|
|
147
|
+
EmailProvider::HotmailB2B => match &input.verif_method.hotmailb2b {
|
|
148
|
+
HotmailB2BVerifMethod::Smtp(c) => c,
|
|
149
|
+
},
|
|
150
|
+
EmailProvider::Mimecast => match &input.verif_method.mimecast {
|
|
151
|
+
verif_method::MimecastVerifMethod::Smtp(c) => c,
|
|
152
|
+
},
|
|
153
|
+
EmailProvider::Proofpoint => match &input.verif_method.proofpoint {
|
|
154
|
+
verif_method::ProofpointVerifMethod::Smtp(c) => c,
|
|
155
|
+
},
|
|
156
|
+
EmailProvider::EverythingElse => match &input.verif_method.everything_else {
|
|
157
|
+
EverythingElseVerifMethod::Smtp(c) => c,
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
.clone();
|
|
161
|
+
|
|
162
|
+
// TODO: There's surely a way to not clone here.
|
|
163
|
+
let verif_method = VerifMethodSmtp::new(
|
|
164
|
+
smtp_verif_method_config.clone(),
|
|
165
|
+
input.verif_method.get_proxy(email_provider).cloned(),
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
(
|
|
169
|
+
check_smtp_with_retry(
|
|
170
|
+
to_email,
|
|
171
|
+
&host_str,
|
|
172
|
+
domain,
|
|
173
|
+
&verif_method,
|
|
174
|
+
verif_method.config.retries,
|
|
175
|
+
)
|
|
176
|
+
.await,
|
|
177
|
+
SmtpDebug {
|
|
178
|
+
verif_method: SmtpDebugVerifMethod::Smtp(SmtpDebugVerifMethodSmtp {
|
|
179
|
+
host: host_str,
|
|
180
|
+
verif_method: smtp_verif_method_config,
|
|
181
|
+
}),
|
|
182
|
+
},
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#[cfg(test)]
|
|
187
|
+
mod tests {
|
|
188
|
+
use super::*;
|
|
189
|
+
use crate::smtp::verif_method::GmailVerifMethod;
|
|
190
|
+
use crate::smtp::verif_method::VerifMethod;
|
|
191
|
+
use crate::smtp::verif_method::VerifMethodSmtpConfig;
|
|
192
|
+
use crate::CheckEmailInputBuilder;
|
|
193
|
+
use crate::EmailAddress;
|
|
194
|
+
use hickory_proto::rr::Name;
|
|
195
|
+
use std::{str::FromStr, time::Duration};
|
|
196
|
+
use tokio::runtime::Runtime;
|
|
197
|
+
|
|
198
|
+
#[test]
|
|
199
|
+
fn should_timeout() {
|
|
200
|
+
let runtime = Runtime::new().unwrap();
|
|
201
|
+
|
|
202
|
+
let to_email = EmailAddress::from_str("foo@gmail.com").unwrap();
|
|
203
|
+
let host = Name::from_str("alt4.aspmx.l.google.com.").unwrap();
|
|
204
|
+
let input = CheckEmailInputBuilder::default()
|
|
205
|
+
.to_email("foo@gmail.com".into())
|
|
206
|
+
.verif_method(VerifMethod {
|
|
207
|
+
gmail: GmailVerifMethod::Smtp(VerifMethodSmtpConfig {
|
|
208
|
+
smtp_timeout: Some(Duration::from_millis(1)),
|
|
209
|
+
retries: 1,
|
|
210
|
+
..Default::default()
|
|
211
|
+
}),
|
|
212
|
+
..Default::default()
|
|
213
|
+
})
|
|
214
|
+
.build()
|
|
215
|
+
.unwrap();
|
|
216
|
+
|
|
217
|
+
let (res, smtp_debug) = runtime.block_on(check_smtp(&to_email, &host, "gmail.com", &input));
|
|
218
|
+
match smtp_debug.verif_method {
|
|
219
|
+
SmtpDebugVerifMethod::Smtp(SmtpDebugVerifMethodSmtp { host, verif_method }) => {
|
|
220
|
+
assert_eq!(host, "alt4.aspmx.l.google.com.");
|
|
221
|
+
assert_eq!(verif_method.smtp_port, 25);
|
|
222
|
+
assert_eq!(verif_method.smtp_timeout, Some(Duration::from_millis(1)));
|
|
223
|
+
assert_eq!(verif_method.retries, 1);
|
|
224
|
+
assert_eq!(verif_method.proxy, None);
|
|
225
|
+
}
|
|
226
|
+
_ => panic!("Expected SmtpDebugVerifMethod::Smtp"),
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
match res {
|
|
230
|
+
Err(SmtpError::Timeout(_)) => (), // ErrorKind == Timeout
|
|
231
|
+
_ => panic!("check_smtp did not time out"),
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|