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.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +24 -0
  3. data/Cargo.lock +2940 -0
  4. data/Cargo.toml +22 -0
  5. data/LICENSE.txt +661 -0
  6. data/NOTICE.md +56 -0
  7. data/README.md +316 -0
  8. data/exe/valid_email_checker +148 -0
  9. data/ext/valid_email_checker/Cargo.toml +27 -0
  10. data/ext/valid_email_checker/extconf.rb +26 -0
  11. data/ext/valid_email_checker/src/lib.rs +371 -0
  12. data/ext/valid_email_checker/src/nogvl.rs +64 -0
  13. data/ext/valid_email_checker/vendor/UPSTREAM.json +9 -0
  14. data/ext/valid_email_checker/vendor/check-if-email-exists/Cargo.toml +52 -0
  15. data/ext/valid_email_checker/vendor/check-if-email-exists/LICENSE.AGPL +661 -0
  16. data/ext/valid_email_checker/vendor/check-if-email-exists/LICENSE.md +11 -0
  17. data/ext/valid_email_checker/vendor/check-if-email-exists/README.md +175 -0
  18. data/ext/valid_email_checker/vendor/check-if-email-exists/src/haveibeenpwned.rs +70 -0
  19. data/ext/valid_email_checker/vendor/check-if-email-exists/src/lib.rs +281 -0
  20. data/ext/valid_email_checker/vendor/check-if-email-exists/src/misc/b2c.txt +96640 -0
  21. data/ext/valid_email_checker/vendor/check-if-email-exists/src/misc/gravatar.rs +60 -0
  22. data/ext/valid_email_checker/vendor/check-if-email-exists/src/misc/mod.rs +124 -0
  23. data/ext/valid_email_checker/vendor/check-if-email-exists/src/misc/roles.txt +944 -0
  24. data/ext/valid_email_checker/vendor/check-if-email-exists/src/mx/mod.rs +165 -0
  25. data/ext/valid_email_checker/vendor/check-if-email-exists/src/rules.json +28 -0
  26. data/ext/valid_email_checker/vendor/check-if-email-exists/src/rules.rs +105 -0
  27. data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/connect.rs +396 -0
  28. data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/error.rs +144 -0
  29. data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/gmail.rs +99 -0
  30. data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/headless.rs +82 -0
  31. data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/http_api.rs +27 -0
  32. data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/mod.rs +234 -0
  33. data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/outlook/headless.rs +181 -0
  34. data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/outlook/microsoft365.rs +109 -0
  35. data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/outlook/mod.rs +2 -0
  36. data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/parser.rs +291 -0
  37. data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/verif_method.rs +531 -0
  38. data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/yahoo/api.rs +174 -0
  39. data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/yahoo/headless.rs +188 -0
  40. data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/yahoo/mod.rs +62 -0
  41. data/ext/valid_email_checker/vendor/check-if-email-exists/src/syntax/mod.rs +199 -0
  42. data/ext/valid_email_checker/vendor/check-if-email-exists/src/syntax/normalize.rs +70 -0
  43. data/ext/valid_email_checker/vendor/check-if-email-exists/src/util/input_output.rs +353 -0
  44. data/ext/valid_email_checker/vendor/check-if-email-exists/src/util/mod.rs +20 -0
  45. data/ext/valid_email_checker/vendor/check-if-email-exists/src/util/sentry.rs +173 -0
  46. data/ext/valid_email_checker/vendor/check-if-email-exists/src/util/ser_with_display.rs +28 -0
  47. data/lib/valid_email_checker/configuration.rb +216 -0
  48. data/lib/valid_email_checker/errors.rb +32 -0
  49. data/lib/valid_email_checker/result.rb +241 -0
  50. data/lib/valid_email_checker/syntax.rb +72 -0
  51. data/lib/valid_email_checker/version.rb +5 -0
  52. data/lib/valid_email_checker.rb +217 -0
  53. metadata +118 -0
@@ -0,0 +1,188 @@
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 std::pin::Pin;
18
+ use std::vec;
19
+ use std::{thread::sleep, time::Duration};
20
+
21
+ use fantoccini::error::CmdError;
22
+ use fantoccini::Locator;
23
+ use futures::future::select_ok;
24
+ use futures::{Future, TryFutureExt};
25
+
26
+ use crate::smtp::headless::{create_headless_client, HeadlessError};
27
+ use crate::WebdriverConfig;
28
+ use crate::{smtp::SmtpDetails, LOG_TARGET};
29
+
30
+ /// Check if a Hotmail/Outlook email exists by connecting to the password
31
+ /// recovery page https://account.live.com/password/reset using a headless
32
+ /// browser. Make sure you have a WebDriver server running locally before
33
+ /// running this, or this will error.
34
+ pub async fn check_headless(
35
+ to_email: &str,
36
+ webdriver: &str,
37
+ webdriver_config: &WebdriverConfig,
38
+ ) -> Result<SmtpDetails, HeadlessError> {
39
+ let mut attempts = 0;
40
+ let max_attempts = 3;
41
+ let mut last_error = None;
42
+
43
+ while attempts < max_attempts {
44
+ attempts += 1;
45
+ tracing::debug!(
46
+ target: LOG_TARGET,
47
+ email=%to_email,
48
+ attempt=%attempts,
49
+ "Using Yahoo password recovery in headless navigator"
50
+ );
51
+
52
+ match check_headless_inner(to_email, webdriver, webdriver_config).await {
53
+ Ok(result) => return Ok(result),
54
+ Err(e) => {
55
+ last_error = Some(e);
56
+ if attempts < max_attempts {
57
+ sleep(Duration::from_secs(1));
58
+ }
59
+ }
60
+ }
61
+ }
62
+
63
+ Err(last_error.unwrap())
64
+ }
65
+
66
+ async fn check_headless_inner(
67
+ to_email: &str,
68
+ webdriver: &str,
69
+ webdriver_config: &WebdriverConfig,
70
+ ) -> Result<SmtpDetails, HeadlessError> {
71
+ let c = create_headless_client(webdriver, webdriver_config).await?;
72
+
73
+ // Navigate to Microsoft password recovery page.
74
+ c.goto("https://login.yahoo.com/forgot").await?;
75
+
76
+ // Wait for network/javascript/dom to make the input-box available
77
+ // and click it.
78
+ let input = c.wait().for_element(Locator::Id("username")).await?;
79
+ // Sometimes I get "input ... is not reachable by keyboard". Addind this
80
+ // small sleep helps.
81
+ sleep(Duration::from_millis(200));
82
+ input.send_keys(to_email).await?;
83
+
84
+ // Click on "Continue"
85
+ c.find(Locator::Css("button[name=\"verifyYid\"]"))
86
+ .await?
87
+ .click()
88
+ .await?;
89
+
90
+ // Depending on what answers we have on the account recovery page, return
91
+ // the relevant (is_deliverable, is_disabled) tuple.
92
+
93
+ // "Sorry, we don't recognise that email address or phone number".
94
+ let f1 = c
95
+ .wait()
96
+ .for_element(Locator::Css(".error-msg"))
97
+ .and_then(|_| async { Ok((false, false)) });
98
+ // "This account has been deactivated due to inactivity, but we would love to welcome you back!"
99
+ let f2 = c
100
+ .wait()
101
+ .for_element(Locator::Css(".ctx-account_is_locked"))
102
+ .and_then(|_| async { Ok((false, true)) });
103
+ // Recaptcha
104
+ let f3 = c
105
+ .wait()
106
+ .for_element(Locator::Css(".recaptcha-challenge"))
107
+ .and_then(|_| async { Ok((true, false)) });
108
+ // "Enter verification code sent to your email address"
109
+ let f4 = c
110
+ .wait()
111
+ .for_element(Locator::Id("email-verify-challenge"))
112
+ .and_then(|_| async { Ok((true, false)) });
113
+ // "Select an option to sign in to your account"
114
+ let f5 = c
115
+ .wait()
116
+ .for_element(Locator::Id("challenge-selector-challenge"))
117
+ .and_then(|_| async { Ok((true, false)) });
118
+
119
+ let vec = vec![
120
+ Box::pin(f1) as Pin<Box<dyn Future<Output = Result<(bool, bool), CmdError>> + Send>>,
121
+ Box::pin(f2),
122
+ Box::pin(f3),
123
+ Box::pin(f4),
124
+ Box::pin(f5),
125
+ ];
126
+ let ((is_deliverable, is_disabled), _) = select_ok(vec).await?;
127
+
128
+ c.close().await?;
129
+
130
+ Ok(SmtpDetails {
131
+ can_connect_smtp: true,
132
+ has_full_inbox: false,
133
+ is_catch_all: false,
134
+ is_deliverable,
135
+ is_disabled,
136
+ })
137
+ }
138
+
139
+ #[cfg(test)]
140
+ mod tests {
141
+ use crate::{initialize_crypto_provider, WebdriverConfig};
142
+
143
+ use super::check_headless;
144
+
145
+ // Ignoring this test as it requires a local process of WebDriver running on
146
+ // "http://localhost:9515". To debug the headless password recovery page,
147
+ // run chromedriver and remove the "#[ignore]".
148
+ // Also see: https://github.com/jonhoo/fantoccini
149
+ #[tokio::test]
150
+ #[ignore = "Run a webdriver server on port 9515 locally to test this"]
151
+ async fn test_yahoo_address() {
152
+ initialize_crypto_provider();
153
+ // Run 5 headless sessions with the below dummy emails.
154
+ for _ in 0..5 {
155
+ // Email does not exist.
156
+ let res = check_headless(
157
+ "test42134@yahoo.com",
158
+ "http://localhost:9515",
159
+ &WebdriverConfig::default(),
160
+ )
161
+ .await
162
+ .unwrap();
163
+ assert!(!res.is_deliverable);
164
+
165
+ // Disabled email.
166
+ let res = check_headless(
167
+ "amaury@yahoo.com",
168
+ "http://localhost:9515",
169
+ &WebdriverConfig::default(),
170
+ )
171
+ .await
172
+ .unwrap();
173
+ assert!(!res.is_deliverable);
174
+ assert!(res.is_disabled);
175
+
176
+ // OK email.
177
+ let res = check_headless(
178
+ "test2@yahoo.com",
179
+ "http://localhost:9515",
180
+ &WebdriverConfig::default(),
181
+ )
182
+ .await
183
+ .unwrap();
184
+ assert!(res.is_deliverable);
185
+ assert!(!res.is_disabled);
186
+ }
187
+ }
188
+ }
@@ -0,0 +1,62 @@
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 api;
18
+
19
+ mod headless;
20
+
21
+ use crate::util::ser_with_display::ser_with_display;
22
+ use reqwest::Error as ReqwestError;
23
+ use serde::Serialize;
24
+ use serde_json::error::Error as SerdeError;
25
+ use thiserror::Error;
26
+
27
+ pub use api::check_api;
28
+ pub use headless::check_headless;
29
+
30
+ /// Possible errors when checking Yahoo email addresses.
31
+ #[derive(Debug, Error, Serialize)]
32
+ pub enum YahooError {
33
+ /// Cannot find "acrumb" field in cookie.
34
+ #[error("Cannot find \"acrumb\" field in cookie")]
35
+ NoAcrumb,
36
+ /// Cannot find "sessionIndex" hidden input in body
37
+ #[error("Cannot find \"sessionIndex\" hidden input in body")]
38
+ NoSessionIndex,
39
+ /// Cannot find cookie in Yahoo response.
40
+ #[error("Cannot find cookie in Yahoo response")]
41
+ NoCookie,
42
+ /// Error when serializing or deserializing HTTP requests and responses.
43
+ #[serde(serialize_with = "ser_with_display")]
44
+ #[error("Error serializing or deserializing HTTP requests and responses: {0}")]
45
+ ReqwestError(ReqwestError),
46
+ /// Error when serializing or deserializing HTTP requests and responses.
47
+ #[serde(serialize_with = "ser_with_display")]
48
+ #[error("Error serializing or deserializing HTTP requests and responses: {0}")]
49
+ SerdeError(SerdeError),
50
+ }
51
+
52
+ impl From<ReqwestError> for YahooError {
53
+ fn from(error: ReqwestError) -> Self {
54
+ YahooError::ReqwestError(error)
55
+ }
56
+ }
57
+
58
+ impl From<SerdeError> for YahooError {
59
+ fn from(error: SerdeError) -> Self {
60
+ YahooError::SerdeError(error)
61
+ }
62
+ }
@@ -0,0 +1,199 @@
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 normalize;
18
+
19
+ use crate::EmailAddress;
20
+ use levenshtein::levenshtein;
21
+ use normalize::normalize_email;
22
+ use serde::{Deserialize, Serialize};
23
+ use std::str::FromStr;
24
+
25
+ /// Syntax information after parsing an email address
26
+ #[derive(Debug, Eq, PartialEq, Deserialize, Serialize)]
27
+ pub struct SyntaxDetails {
28
+ /// The email address as a async_smtp `EmailAddress`. It will be `None` if
29
+ /// the email address is ill-formed.
30
+ pub address: Option<EmailAddress>,
31
+ /// The domain name, after "@". It will be the empty string if the email
32
+ /// address if ill-formed.
33
+ pub domain: String,
34
+ /// Does the email have a valid syntax?
35
+ pub is_valid_syntax: bool,
36
+ /// The username, before "@". It will be the empty string if the email
37
+ /// address if ill-formed.
38
+ pub username: String,
39
+ /// The normalized form of `address`. It will be `None` if the email
40
+ /// address is ill-formed.
41
+ pub normalized_email: Option<String>,
42
+ pub suggestion: Option<String>,
43
+ }
44
+
45
+ impl Default for SyntaxDetails {
46
+ fn default() -> Self {
47
+ SyntaxDetails {
48
+ address: None,
49
+ domain: "".into(),
50
+ is_valid_syntax: false,
51
+ username: "".into(),
52
+ normalized_email: None,
53
+ suggestion: None,
54
+ }
55
+ }
56
+ }
57
+
58
+ /// From an `email_address` string, compute syntax information about it, such as
59
+ /// username and domain.
60
+ pub fn check_syntax(email_address: &str) -> SyntaxDetails {
61
+ let email_address = match EmailAddress::from_str(email_address) {
62
+ Ok(m) => {
63
+ if mailchecker::is_valid(email_address) {
64
+ m
65
+ } else {
66
+ return SyntaxDetails {
67
+ address: None,
68
+ domain: "".into(),
69
+ is_valid_syntax: false,
70
+ username: "".into(),
71
+ normalized_email: None,
72
+ suggestion: None,
73
+ };
74
+ }
75
+ }
76
+ _ => {
77
+ return SyntaxDetails {
78
+ address: None,
79
+ domain: "".into(),
80
+ is_valid_syntax: false,
81
+ username: "".into(),
82
+ normalized_email: None,
83
+ suggestion: None,
84
+ }
85
+ }
86
+ };
87
+
88
+ let iter: &str = email_address.as_ref();
89
+ let mut iter = iter.split('@');
90
+ let username: String = iter
91
+ .next()
92
+ .expect("We checked above that email is valid. qed.")
93
+ .into();
94
+ let domain: String = iter
95
+ .next()
96
+ .expect("We checked above that email is valid. qed.")
97
+ .into();
98
+ let normalized_email = normalize_email(&username, &domain);
99
+
100
+ SyntaxDetails {
101
+ address: Some(email_address),
102
+ domain,
103
+ is_valid_syntax: true,
104
+ username,
105
+ normalized_email: Some(normalized_email),
106
+ suggestion: None,
107
+ }
108
+ }
109
+
110
+ const MAIL_PROVIDERS: &[&str] = &[
111
+ "gmail.com",
112
+ "yahoo.com",
113
+ "outlook.com",
114
+ "hotmail.com",
115
+ "protonmail.com",
116
+ "icloud.com",
117
+ "yandex.com",
118
+ ];
119
+ // Supplies the syntax parameter with a suggestion that matches the mail domain best by levenshtein
120
+ // distance.
121
+ pub fn get_similar_mail_provider(syntax: &mut SyntaxDetails) {
122
+ for possible_provider in MAIL_PROVIDERS {
123
+ let distance = levenshtein(&syntax.domain, possible_provider);
124
+
125
+ if distance < 3 {
126
+ // Return full address
127
+ syntax.suggestion = Some(format!(
128
+ "{}@{}",
129
+ syntax.username,
130
+ String::from(*possible_provider),
131
+ ));
132
+ break;
133
+ }
134
+ }
135
+ }
136
+
137
+ #[cfg(test)]
138
+ mod tests {
139
+ use super::*;
140
+
141
+ #[test]
142
+ fn should_return_invalid_for_invalid_email() {
143
+ assert_eq!(
144
+ check_syntax("foo"),
145
+ SyntaxDetails {
146
+ address: None,
147
+ domain: "".into(),
148
+ is_valid_syntax: false,
149
+ username: "".into(),
150
+ normalized_email: None,
151
+ suggestion: None,
152
+ }
153
+ );
154
+ }
155
+
156
+ #[test]
157
+ fn should_return_invalid_for_invalid_email_with_at() {
158
+ assert_eq!(
159
+ check_syntax("foo@bar"),
160
+ SyntaxDetails {
161
+ address: None,
162
+ domain: "".into(),
163
+ is_valid_syntax: false,
164
+ username: "".into(),
165
+ normalized_email: None,
166
+ suggestion: None,
167
+ }
168
+ );
169
+ }
170
+
171
+ #[test]
172
+ fn should_work_for_valid_email() {
173
+ assert_eq!(
174
+ check_syntax("foo@bar.com"),
175
+ SyntaxDetails {
176
+ address: Some(EmailAddress::new("foo@bar.com".into()).unwrap()),
177
+ domain: "bar.com".into(),
178
+ is_valid_syntax: true,
179
+ username: "foo".into(),
180
+ normalized_email: Some("foo@bar.com".into()),
181
+ suggestion: None,
182
+ }
183
+ );
184
+ }
185
+
186
+ #[test]
187
+ fn should_suggest_a_correct_mail_if_similar() {
188
+ let mut syntax = SyntaxDetails {
189
+ address: Some(EmailAddress::new("test@gmali.com".into()).unwrap()),
190
+ domain: "gmali.com".into(),
191
+ is_valid_syntax: true,
192
+ username: "test".into(),
193
+ normalized_email: Some("test@gmali.com".into()),
194
+ suggestion: None,
195
+ };
196
+ get_similar_mail_provider(&mut syntax);
197
+ assert_eq!(syntax.suggestion, Some("test@gmail.com".to_string()))
198
+ }
199
+ }
@@ -0,0 +1,70 @@
1
+ pub fn normalize_email(username: &str, domain: &str) -> String {
2
+ match domain {
3
+ "gmail.com" | "googlemail.com" => normalize_gmail(username),
4
+ _ => format!("{username}@{domain}"),
5
+ }
6
+ }
7
+
8
+ /// Normalize a Gmail address.
9
+ ///
10
+ /// See Gmail username
11
+ /// [restrictions](https://support.google.com/mail/answer/9211434?hl=en-GB).
12
+ ///
13
+ /// - removes
14
+ /// [sub-addresses](https://support.google.com/a/users/answer/9282734?hl=en#zippy=%2Clearn-how)
15
+ /// (i.e. parts after a `+` character.)
16
+ /// - removes [dots](https://support.google.com/mail/answer/7436150).
17
+ /// - converts to lower-case.
18
+ /// - [replaces](https://support.google.com/mail/answer/10313?hl=en-GB#zippy=%2Cgetting-messages-sent-to-an-googlemailcom-address)
19
+ /// `googlemail.com` with `gmail.com`.
20
+ fn normalize_gmail(username: &str) -> String {
21
+ let username = match username.split_once('+') {
22
+ Some((username, _)) => username,
23
+ _ => username,
24
+ }
25
+ .chars()
26
+ .filter_map(|c| match c.to_ascii_lowercase() {
27
+ '.' => None,
28
+ lower => Some(lower),
29
+ })
30
+ .collect::<String>();
31
+
32
+ format!("{username}@gmail.com",)
33
+ }
34
+
35
+ #[cfg(test)]
36
+ mod tests {
37
+ use super::*;
38
+
39
+ #[test]
40
+ fn test_gmail_removes_periods() {
41
+ assert_eq!(normalize_email("a.b.c", "gmail.com"), "abc@gmail.com");
42
+ }
43
+
44
+ #[test]
45
+ fn test_gmail_removes_subaddress() {
46
+ assert_eq!(normalize_email("abc+123", "gmail.com"), "abc@gmail.com");
47
+ }
48
+
49
+ #[test]
50
+ fn test_gmail_uses_gmail_com() {
51
+ assert_eq!(normalize_email("abc", "googlemail.com"), "abc@gmail.com");
52
+ }
53
+
54
+ #[test]
55
+ fn test_gmail() {
56
+ assert_eq!(
57
+ normalize_email("A.B.C+123", "googlemail.com"),
58
+ "abc@gmail.com"
59
+ );
60
+ }
61
+
62
+ #[test]
63
+ fn test_gmail_idempotent() {
64
+ let normalized = normalize_email("A.B.C+123", "googlemail.com");
65
+
66
+ let (username, domain) = normalized.rsplit_once('@').unwrap();
67
+
68
+ assert_eq!(normalize_email(username, domain), normalized);
69
+ }
70
+ }