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,181 @@
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, thread::sleep, time::Duration};
18
+
19
+ use fantoccini::{error::CmdError, Locator};
20
+ use futures::{future::select_ok, Future, TryFutureExt};
21
+
22
+ use crate::{
23
+ smtp::{
24
+ headless::{create_headless_client, HeadlessError},
25
+ SmtpDetails,
26
+ },
27
+ WebdriverConfig, LOG_TARGET,
28
+ };
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_password_recovery(
35
+ to_email: &str,
36
+ webdriver: &str,
37
+ webdriver_config: &WebdriverConfig,
38
+ ) -> Result<SmtpDetails, HeadlessError> {
39
+ let to_email = to_email.to_string();
40
+ tracing::debug!(
41
+ target: LOG_TARGET,
42
+ email=to_email,
43
+ "Using Hotmail password recovery in headless navigator"
44
+ );
45
+
46
+ let c = create_headless_client(webdriver, webdriver_config).await?;
47
+
48
+ // Navigate to Microsoft password recovery page.
49
+ c.goto("https://account.live.com/password/reset").await?;
50
+
51
+ // Wait for network/javascript/dom to make the input-box available
52
+ // and click it.
53
+ let input = c.wait().for_element(Locator::Id("iSigninName")).await?;
54
+ // Sometimes I get "input ... is not reachable by keyboard". Addind this
55
+ // small sleep helps.
56
+ sleep(Duration::from_millis(200));
57
+ input.send_keys(to_email.as_str()).await?;
58
+
59
+ // Click on "Next"
60
+ c.find(Locator::Id("resetPwdHipAction"))
61
+ .await?
62
+ .click()
63
+ .await?;
64
+
65
+ // "Try entering your Microsoft account again. We don't recognise this one." means the account does not exist.
66
+ let f1 = c
67
+ .wait()
68
+ .for_element(Locator::Id("pMemberNameErr"))
69
+ .and_then(|_| async { Ok(false) });
70
+ // "Try entering your Microsoft account again. We don't recognise this one."
71
+ let f2 = c
72
+ .wait()
73
+ .for_element(Locator::Id("iSigninNameError"))
74
+ .and_then(|_| async { Ok(false) });
75
+ // "We need to verify your identity" means that the account exists.
76
+ let f3 = c
77
+ .wait()
78
+ .for_element(Locator::Id("iSelectProofTitle"))
79
+ .and_then(|_| async { Ok(true) });
80
+ // "Enter the code generated by your authenticator app..."
81
+ let f4 = c
82
+ .wait()
83
+ .for_element(Locator::Id("iEnterVerification"))
84
+ .and_then(|_| async { Ok(true) });
85
+
86
+ let vec = vec![
87
+ Box::pin(f1) as Pin<Box<dyn Future<Output = Result<bool, CmdError>> + Send>>,
88
+ Box::pin(f2),
89
+ Box::pin(f3),
90
+ Box::pin(f4),
91
+ ];
92
+ let (is_deliverable, _) = select_ok(vec).await?;
93
+
94
+ if is_deliverable {
95
+ tracing::debug!(
96
+ target: LOG_TARGET,
97
+ email=to_email,
98
+ exists=is_deliverable,
99
+ "Did not find error message in password recovery, email exists"
100
+ );
101
+ } else {
102
+ tracing::debug!(
103
+ target: LOG_TARGET,
104
+ email=to_email,
105
+ exists=is_deliverable,
106
+ "Found error message in password recovery, email does not exist"
107
+ );
108
+ }
109
+
110
+ c.close().await?;
111
+
112
+ tracing::debug!(
113
+ target: LOG_TARGET,
114
+ email=to_email,
115
+ exists=is_deliverable,
116
+ "Password recovery check result"
117
+ );
118
+
119
+ Ok(SmtpDetails {
120
+ can_connect_smtp: true,
121
+ has_full_inbox: false,
122
+ is_catch_all: false,
123
+ is_deliverable,
124
+ is_disabled: false,
125
+ })
126
+ }
127
+
128
+ #[cfg(test)]
129
+ mod tests {
130
+ use super::check_password_recovery;
131
+ use crate::WebdriverConfig;
132
+ use futures::future::join;
133
+
134
+ // Ignoring this test as it requires a local process of WebDriver running on
135
+ // "http://localhost:9515". To debug the headless password recovery page,
136
+ // run chromedriver and remove the "#[ignore]".
137
+ // Also see: https://github.com/jonhoo/fantoccini
138
+ #[tokio::test]
139
+ #[ignore = "Run a webdriver server locally to test this"]
140
+ async fn test_hotmail_address() {
141
+ // Run 10 headless sessions with dummy emails.
142
+ // It should not error.
143
+ for _ in 0..10 {
144
+ // This email does not exist.
145
+ let res = check_password_recovery(
146
+ "test42134@hotmail.com",
147
+ "http://localhost:9515",
148
+ &WebdriverConfig::default(),
149
+ )
150
+ .await
151
+ .unwrap();
152
+ assert!(!res.is_deliverable);
153
+
154
+ // This email does exist.
155
+ let res = check_password_recovery(
156
+ "test@hotmail.com",
157
+ "http://localhost:9515",
158
+ &WebdriverConfig::default(),
159
+ )
160
+ .await
161
+ .unwrap();
162
+ assert!(res.is_deliverable);
163
+ }
164
+ }
165
+
166
+ // This test tests that we can run 2 instances of check_password_recovery.
167
+ // This will only work with chromedriver (which supports parallel cleints),
168
+ // but will fail with geckodriver.
169
+ // ref: https://github.com/jonhoo/fantoccini/issues/111#issuecomment-727650629
170
+ #[tokio::test]
171
+ #[ignore = "Run a webdriver server locally to test this"]
172
+ async fn test_parallel() {
173
+ let webdriver_config = WebdriverConfig::default();
174
+ // This email does not exist.
175
+ let f1 = check_password_recovery("foo@bar.baz", "http://localhost:9515", &webdriver_config);
176
+ let f2 = check_password_recovery("foo@bar.baz", "http://localhost:9515", &webdriver_config);
177
+
178
+ let f = join(f1, f2).await;
179
+ assert!(f.0.is_ok(), "{:?}", f);
180
+ }
181
+ }
@@ -0,0 +1,109 @@
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::{
18
+ smtp::{http_api::create_client, SmtpDetails},
19
+ util::ser_with_display::ser_with_display,
20
+ CheckEmailInput, LOG_TARGET,
21
+ };
22
+ use async_smtp::EmailAddress;
23
+ use reqwest::Error as ReqwestError;
24
+ use serde::Serialize;
25
+ use thiserror::Error;
26
+
27
+ #[derive(Debug, Error, Serialize)]
28
+ pub enum Microsoft365Error {
29
+ #[serde(serialize_with = "ser_with_display")]
30
+ #[error("Reqwest error: {0}")]
31
+ ReqwestError(ReqwestError),
32
+ }
33
+
34
+ impl From<ReqwestError> for Microsoft365Error {
35
+ fn from(error: ReqwestError) -> Self {
36
+ Microsoft365Error::ReqwestError(error)
37
+ }
38
+ }
39
+
40
+ /// Convert an email address to its corresponding OneDrive URL.
41
+ #[allow(dead_code)]
42
+ fn get_onedrive_url(email_address: &str) -> String {
43
+ let (username, domain) = email_address
44
+ .split_once('@')
45
+ .expect("Email address syntax already validated.");
46
+ let (tenant, _) = domain
47
+ .split_once('.')
48
+ .expect("Email domain syntax already validated.");
49
+
50
+ format!(
51
+ "https://{}-my.sharepoint.com/personal/{}_{}/_layouts/15/onedrive.aspx",
52
+ tenant,
53
+ username.replace('.', "_"),
54
+ domain.replace('.', "_"),
55
+ )
56
+ }
57
+
58
+ /// Use a HTTP request to verify if an Microsoft 365 email address exists.
59
+ ///
60
+ /// See
61
+ /// [this article](<https://www.trustedsec.com/blog/achieving-passive-user-enumeration-with-onedrive/>)
62
+ /// for details on the underlying principles.
63
+ ///
64
+ /// Note that a positive response from this function is (at present) considered
65
+ /// a reliable indicator that an email-address is valid. However, a negative
66
+ /// response is ambigious: the email address may or may not be valid but this
67
+ /// cannot be determined by the method outlined here.
68
+ #[allow(dead_code)]
69
+ pub async fn check_microsoft365_api(
70
+ to_email: &EmailAddress,
71
+ input: &CheckEmailInput,
72
+ ) -> Result<Option<SmtpDetails>, Microsoft365Error> {
73
+ let url = get_onedrive_url(to_email.as_ref());
74
+
75
+ let response = create_client(input, "microsoft365")?
76
+ .head(url)
77
+ .send()
78
+ .await?;
79
+
80
+ tracing::debug!(
81
+ target: LOG_TARGET,
82
+ email = %to_email,
83
+ response = ?response,
84
+ "microsoft365 response"
85
+ );
86
+
87
+ if response.status() == 403 {
88
+ Ok(Some(SmtpDetails {
89
+ can_connect_smtp: true,
90
+ is_deliverable: true,
91
+ ..Default::default()
92
+ }))
93
+ } else {
94
+ Ok(None)
95
+ }
96
+ }
97
+
98
+ #[cfg(test)]
99
+ mod tests {
100
+ use super::*;
101
+
102
+ #[test]
103
+ fn test_onedrive_url() {
104
+ let email_address = "lightmand@acmecomputercompany.com";
105
+ let expected = "https://acmecomputercompany-my.sharepoint.com/personal/lightmand_acmecomputercompany_com/_layouts/15/onedrive.aspx";
106
+
107
+ assert_eq!(expected, get_onedrive_url(email_address));
108
+ }
109
+ }
@@ -0,0 +1,2 @@
1
+ pub mod headless;
2
+ pub mod microsoft365;
@@ -0,0 +1,291 @@
1
+ // Reacher - Email Verification
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
+ //! Parse the SMTP responses to get information about the email address.
18
+
19
+ use super::error::SmtpError;
20
+ use crate::EmailAddress;
21
+ use async_smtp::error::Error as AsyncSmtpError;
22
+
23
+ /// is_invalid checks for SMTP responses meaning that the email is invalid,
24
+ /// i.e. that the mailbox doesn't exist.
25
+ pub fn is_invalid(e: &str, email: &EmailAddress) -> bool {
26
+ // 550 Address rejected
27
+ // 550 5.1.1 : Recipient address rejected
28
+ // 550 5.1.1 : Recipient address rejected: User unknown in virtual alias table
29
+ // 550 5.1.1 <EMAIL: Recipient address rejected: User unknown in relay recipient table
30
+ e.contains("address rejected")
31
+ // 550 5.1.1 : Unrouteable address
32
+ || e.contains("unrouteable")
33
+ // 550 5.1.1 : The email account that you tried to reach does not exist
34
+ || e.contains("does not exist")
35
+ // 550 invalid address
36
+ // 550 User not local or invalid address – Relay denied
37
+ || e.contains("invalid address")
38
+ // 5.1.1 Invalid email address
39
+ || e.contains("invalid email address")
40
+ // 550 Invalid recipient
41
+ || e.contains("invalid recipient")
42
+ || e.contains("may not exist")
43
+ || e.contains("recipient invalid")
44
+ // 550 5.1.1 : Recipient rejected
45
+ || e.contains("recipient rejected")
46
+ // permanent: 5.1.1 Unknown recipient address
47
+ || e.contains("unknown recipient address")
48
+ // 554 Unknown Recipient (#5.1.1) (on @parkwayhonda.com)
49
+ || e.contains("unknown recipient")
50
+ || e.contains("undeliverable")
51
+ // 550 User unknown
52
+ // 550 5.1.1 <EMAIL> User unknown
53
+ // 550 recipient address rejected: user unknown in local recipient table
54
+ || e.contains("user unknown")
55
+ // 550 Unknown user
56
+ || e.contains("unknown user")
57
+ // 5.1.1 Recipient unknown <EMAIL>
58
+ || e.contains("recipient unknown")
59
+ // 550 5.1.1 No such user - pp
60
+ // 550 No such user here
61
+ || e.contains("no such user")
62
+ // permanent: 5.1.1 MXIN501 mailbox <EMAIL> unknown (on @virginmedia.com)
63
+ || e.contains(format!("mailbox {email} unknown").as_str())
64
+ // 550 5.1.1 : Mailbox not found
65
+ // 550 Unknown address error ‘MAILBOX NOT FOUND’
66
+ || e.contains("mailbox not found")
67
+ // 550 5.1.1 : Invalid mailbox
68
+ || e.contains("invalid mailbox")
69
+ // 550 5.1.1 Sorry, no mailbox here by that name
70
+ || e.contains("no mailbox")
71
+ // 5.2.0 No such mailbox
72
+ || e.contains("no such mailbox")
73
+ // 550 Requested action not taken: mailbox unavailable
74
+ || e.contains("mailbox unavailable")
75
+ // 5.5.0 Requested actions not taken as the mailbox is unavailable (on @etu.uca.fr)
76
+ || e.contains("mailbox is unavailable")
77
+ // 550 5.1.1 Is not a valid mailbox
78
+ || e.contains("not a valid mailbox")
79
+ // No such recipient here
80
+ || e.contains("no such recipient")
81
+ // 554 delivery error: This user doesn’t have an account
82
+ || e.contains("have an account")
83
+ // permanent: Unknown local part <USER> in <USER@flabeg.com> (on @flabeg.com)
84
+ || e.contains("unknown local part")
85
+ // 5.1.1 RCP-P1 Domain facebook.com no longer available https://www.facebook.com/postmaster/response_codes?ip=3.80.111.155#RCP-P1
86
+ || e.contains("no longer available")
87
+ // permanent: RCPT (<EMAIL>) dosn't exist (on @hgy.ooo, @stigpods.com.cn)
88
+ || e.contains("dosn't exist") // sic! typo is intentional
89
+ // 5.1.1 <EMAIL>: Email address could not be found, or was misspelled (G8) (on @biotech-calendar.com, @invoicefactoring.com)
90
+ || e.contains("could not be found")
91
+ // No such person at this address (on @aconsa.com.mx)
92
+ || e.contains("no such person")
93
+ // Callout verification failed: 550 No Such User Here (on @medipro.co.uk)
94
+ || e.contains("no such user")
95
+ // 5.1.1 <EMAIL> Address Error (on @lucidity.co.za)
96
+ || e.contains("address error")
97
+ // E-mail address is not handled by this system (on @kaimayfair.co.uk)
98
+ || e.contains("address is not handled")
99
+ // permanent: 5.1.1 recipient is not exist (on @sim.com)
100
+ || e.contains("recipient is not exist")
101
+ // permanent: 5.1.1 <EMAIL> Recipient not found. (on @4polymer.com)
102
+ || e.contains("recipient not found")
103
+ // permanent: 5.7.1 Email doesn't exist. Please forward it or send it to contact@magency.f; 5.7.1 r - gcdp 38308e7fff4ca-30344241c74si13773751fa.513 - gsmtp"
104
+ || e.contains("email doesn't exist")
105
+ // permanent: verify address failed, User not found ***@salemall.vn (on @salemall.vn)
106
+ || e.contains("verify address failed")
107
+ // transient: unable to verify user (on @computan.net)
108
+ || e.contains("unable to verify user")
109
+ // permanent: 5.1.1 Utilisateur inconnu (on @sante.fr)
110
+ || e.contains("utilisateur inconnu")
111
+ // permanent: 5.1.1
112
+ || e.contains("permanent: 5.1.1")
113
+ // permanent: 5.7.1
114
+ || e.contains("permanent: 5.7.1")
115
+ }
116
+
117
+ /// Check that the mailbox has a full inbox.
118
+ pub fn is_full_inbox(e: &str) -> bool {
119
+ e.contains("insufficient")
120
+ // https://answers.microsoft.com/en-us/outlook_com/forum/all/how-do-i-interpret-the-delivery-failure-message/2f1bf9c0-8b03-4f8f-aacc-5f6ba60a73f3
121
+ || e.contains("mailbox full")
122
+ // https://answers.microsoft.com/en-us/outlook_com/forum/all/how-do-i-interpret-the-delivery-failure-message/2f1bf9c0-8b03-4f8f-aacc-5f6ba60a73f3
123
+ || e.contains("quote exceeded")
124
+ || e.contains("over quota")
125
+ // 550 user has too many messages on the server
126
+ || e.contains("too many messages")
127
+ // transient: 4.2.2 The recipient's inbox is out of storage space. Please direct the; 4.2.2 recipient to; 4.2.2 https://support.google.com/mail/?p=OverQuotaTemp 41be03b00d2f7-801d5c00011si4287631a12.311 - gsmtp (on @gmail.com)
128
+ || e.contains("out of storage space")
129
+ }
130
+
131
+ /// Check if the email account has been disabled or blocked by the email
132
+ /// provider.
133
+ pub fn is_disabled_account(e: &str) -> bool {
134
+ // 554 The email account that you tried to reach is disabled. Learn more at https://support.google.com/mail/?p=DisabledUser"
135
+ e.contains("disabled")
136
+ // 554 delivery error: Sorry your message to <EMAIL> cannot be delivered. This account has been disabled or discontinued
137
+ || e.contains("discontinued")
138
+ //550 5.2.1 RACT MY.IP: Mailbox is inactive: <USER@hanmail.net><CRLF> (on hanmail.net)
139
+ || e.contains("inactive")
140
+ }
141
+
142
+ /// Check if the error is an IO "incomplete" error.
143
+ pub fn is_err_io_errors(e: &SmtpError) -> bool {
144
+ match e {
145
+ SmtpError::AsyncSmtpError(AsyncSmtpError::Io(err)) => err.to_string() == "incomplete",
146
+ _ => false,
147
+ }
148
+ }
149
+
150
+ /// Check if the IP is blacklisted.
151
+ pub fn is_err_ip_blacklisted(e: &SmtpError) -> bool {
152
+ let e = match e {
153
+ SmtpError::AsyncSmtpError(AsyncSmtpError::Transient(r) | AsyncSmtpError::Permanent(r)) => {
154
+ // TODO We can use .to_string() after:
155
+ // https://github.com/async-email/async-smtp/pull/53
156
+ r.message.join("; ").to_lowercase()
157
+ }
158
+ _ => {
159
+ return false;
160
+ }
161
+ };
162
+
163
+ // Permanent errors
164
+
165
+ // 5.7.1 IP address blacklisted by recipient
166
+ // 5.7.1 Service unavailable; Client host [147.75.45.223] is blacklisted. Visit https://www.sophos.com/en-us/threat-center/ip-lookup.aspx?ip=147.75.45.223 to request delisting
167
+ // 5.3.0 <EMAIL>... Mail from 147.75.45.223 rejected by Abusix blacklist (on @helsinki.fi)
168
+ e.contains("blacklist")
169
+ // Rejected because 23.129.64.213 is in a black list at b.barracudacentral.org
170
+ || e.contains("black list")
171
+ // 5.7.1 Recipient not authorized, your IP has been found on a block list
172
+ // gmx.net (mxgmx117) Nemesis ESMTP Service not available; No SMTP service; IP address is block listed.; For explanation visit https://www.gmx.net/mail/senderguidelines?c=bl (on @gmx.net, @web.de)
173
+ || e.contains("block list")
174
+ // Unable to add <EMAIL> because host 23.129.64.184 is listed on zen.spamhaus.org
175
+ // 5.7.1 Service unavailable, Client host [23.129.64.184] blocked using Spamhaus.
176
+ // 5.7.1 Email cannot be delivered. Reason: Email detected as Spam by spam filters.
177
+ || e.contains("spam")
178
+ // host 23.129.64.216 is listed at combined.mail.abusix.zone (127.0.0.12,
179
+ || e.contains("abusix")
180
+ // 5.7.1 Relaying denied. IP name possibly forged [45.154.35.252]
181
+ // 5.7.1 Relaying denied: You must check for new mail before sending mail. [23.129.64.216]
182
+ || e.contains("relaying denied")
183
+ // 5.7.1 <unknown[23.129.64.100]>: Client host rejected: Access denied
184
+ || e.contains("access denied")
185
+ // sorry, mail from your location [5.79.109.48] is administratively denied (#5.7.1)
186
+ || e.contains("administratively denied")
187
+ // 5.7.606 Access denied, banned sending IP [23.129.64.216]
188
+ || e.contains("banned")
189
+ // Blocked - see https://ipcheck.proofpoint.com/?ip=23.129.64.192
190
+ // 5.7.1 Mail from 23.129.64.183 has been blocked by Trend Micro Email Reputation Service.
191
+ || e.contains("blocked")
192
+ // Connection rejected by policy [7.3] 38206, please visit https://support.symantec.com/en_US/article.TECH246726.html for more details about this error message.
193
+ || e.contains("connection rejected")
194
+ // csi.mimecast.org Poor Reputation Sender. - https://community.mimecast.com/docs/DOC-1369#550 [6ATVl4DjOvSA6XNsWGoUFw.us31]
195
+ // Your access to this mail system has been rejected due to the sending MTA\'s poor reputation. If you believe that this failure is in error, please contact the intended recipient via alternate means.
196
+ || e.contains("poor reputation")
197
+ // JunkMail rejected - (gmail.com) [193.218.118.140]:46615 is in an RBL: http://www.barracudanetworks.com/reputation/?pr=1&ip=193.218.118.140
198
+ || e.contains("junkmail")
199
+ // mailfi01.lmco.com ESMTP 550 5.7.0 Mail from 18.234.87.196 refused by Proofpoint Reputation Services. SENDER please see and take action: https://support.proofpoint.com/dnsbl-lookup.cgi?18.234.87.196" (on @lmco.com)
200
+ || e.contains("refused by proofpoint")
201
+ // resimta-h1p-037598.sys.comcast.net resimta-h1p-037598.sys.comcast.net 5.135.185.166 found on one or more DNSBLs, see http://postmaster.comcast.net/smtp-error-codes.php#BL000001 (on @comcast.net)
202
+ || e.contains("dnsbl")
203
+ // smtp-fw-9107.amazon.com; SBRS score too low: http://www.senderbase.org/ (on @amazon.com)
204
+ || e.contains("sbrs score too low")
205
+ // https://www.spamhaus.org/sbl/query/SBLCSShttps://www.spamhaus.org/query/ip/3.238.201.74 (on @knollridges.com.ph)
206
+ || e.contains("spamhaus")
207
+
208
+ // Transient errors
209
+
210
+ // Blocked - see https://www.spamcop.net/bl.shtml?23.129.64.211
211
+ || e.contains("blocked")
212
+ // 4.7.1 <EMAIL>: Relay access denied
213
+ || e.contains("access denied")
214
+ // relay not permitted!
215
+ || e.contains("relay not permitted")
216
+ // 23.129.64.216 is not yet authorized to deliver mail from
217
+ || e.contains("not yet authorized")
218
+ }
219
+
220
+ /// Check if the IP needs a reverse DNS.
221
+ pub fn is_err_needs_rdns(e: &SmtpError) -> bool {
222
+ let e = match e {
223
+ SmtpError::AsyncSmtpError(AsyncSmtpError::Transient(r) | AsyncSmtpError::Permanent(r)) => {
224
+ // TODO We can use .to_string() after:
225
+ // https://github.com/async-email/async-smtp/pull/53
226
+ r.message.join("; ").to_lowercase()
227
+ }
228
+ _ => {
229
+ return false;
230
+ }
231
+ };
232
+
233
+ // 4.7.25 Client host rejected: cannot find your hostname, [147.75.45.223]
234
+ // 4.7.1 Client host rejected: cannot find your reverse hostname, [147.75.45.223]
235
+ // 5.7.1 Client host rejected: cannot find your reverse hostname, [23.129.64.184]
236
+ e.contains("cannot find your reverse hostname")
237
+ // You dont seem to have a reverse dns entry. Come back later. You are greylisted for 20 minutes. See http://www.fsf.org/about/systems/greylisting
238
+ || e.contains("reverse dns entry")
239
+ }
240
+
241
+ #[cfg(test)]
242
+ mod tests {
243
+
244
+ use super::{is_err_ip_blacklisted, is_invalid};
245
+ use crate::EmailAddress;
246
+ use crate::SmtpError::AsyncSmtpError;
247
+ use async_smtp::{
248
+ error::Error,
249
+ response::{Category, Code, Detail, Response, Severity},
250
+ };
251
+ use std::str::FromStr;
252
+
253
+ #[test]
254
+ fn test_is_invalid() {
255
+ let email = EmailAddress::from_str("foo@bar.baz").unwrap();
256
+
257
+ assert!(!is_invalid(
258
+ "554 5.7.1 <mta.voipdir.net[]>: Client host rejected: Access denied",
259
+ &email
260
+ ));
261
+
262
+ assert!(is_invalid(
263
+ "RCPT (***@stigpods.com.cn) dosn't exist",
264
+ &email
265
+ ));
266
+
267
+ assert!(is_invalid(
268
+ "permanent: 5.1.1 MXIN501 mailbox foo@bar.baz unknown (on @virginmedia.com)",
269
+ &email
270
+ ));
271
+ }
272
+
273
+ #[test]
274
+ fn test_is_err_ip_blacklisted() {
275
+ let err = Error::Permanent(Response::new(
276
+ Code::new(
277
+ Severity::PermanentNegativeCompletion,
278
+ Category::Information,
279
+ Detail::Zero,
280
+ ),
281
+ vec![
282
+ "gmx.net (mxgmx117) Nemesis ESMTP Service not available".to_string(),
283
+ "No SMTP service".to_string(),
284
+ "IP address is block listed.".to_string(),
285
+ "For explanation visit https://www.gmx.net/mail/senderguidelines?c=bl".to_string(),
286
+ ],
287
+ ));
288
+
289
+ assert!(is_err_ip_blacklisted(&AsyncSmtpError(err)))
290
+ }
291
+ }