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,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
|
+
}
|