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,353 @@
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::misc::{MiscDetails, MiscError};
18
+ use crate::mx::{MxDetails, MxError};
19
+ use crate::smtp::verif_method::VerifMethod;
20
+ use crate::smtp::{SmtpDebug, SmtpDetails, SmtpError, SmtpErrorDesc};
21
+ use crate::syntax::SyntaxDetails;
22
+ use crate::util::ser_with_display::ser_with_display;
23
+ use async_smtp::EmailAddress as AsyncSmtpEmailAddress;
24
+ use chrono::{DateTime, Utc};
25
+ use derive_builder::Builder;
26
+ use serde::{ser::SerializeMap, Deserialize, Serialize, Serializer};
27
+ use std::fmt::Display;
28
+ use std::str::FromStr;
29
+ use std::time::{Duration, SystemTime};
30
+
31
+ /// Wrapper around the `EmailAddress` from `async_smtp` to allow for
32
+ /// serialization and deserialization.
33
+ #[derive(Debug, Eq, PartialEq, Clone)]
34
+ pub struct EmailAddress(AsyncSmtpEmailAddress);
35
+
36
+ impl Serialize for EmailAddress {
37
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
38
+ where
39
+ S: Serializer,
40
+ {
41
+ ser_with_display(&self.0, serializer)
42
+ }
43
+ }
44
+
45
+ impl<'de> Deserialize<'de> for EmailAddress {
46
+ fn deserialize<D>(deserializer: D) -> Result<EmailAddress, D::Error>
47
+ where
48
+ D: serde::Deserializer<'de>,
49
+ {
50
+ let s = String::deserialize(deserializer)?;
51
+ Ok(EmailAddress(
52
+ AsyncSmtpEmailAddress::from_str(&s).map_err(serde::de::Error::custom)?,
53
+ ))
54
+ }
55
+ }
56
+
57
+ impl Display for EmailAddress {
58
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59
+ self.0.fmt(f)
60
+ }
61
+ }
62
+
63
+ impl FromStr for EmailAddress {
64
+ type Err = anyhow::Error;
65
+
66
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
67
+ Ok(EmailAddress(AsyncSmtpEmailAddress::from_str(s)?))
68
+ }
69
+ }
70
+
71
+ impl EmailAddress {
72
+ pub fn new(email: String) -> Result<Self, anyhow::Error> {
73
+ Ok(EmailAddress(AsyncSmtpEmailAddress::new(email)?))
74
+ }
75
+
76
+ pub fn into_inner(self) -> AsyncSmtpEmailAddress {
77
+ self.0
78
+ }
79
+ }
80
+
81
+ impl AsRef<AsyncSmtpEmailAddress> for EmailAddress {
82
+ fn as_ref(&self) -> &AsyncSmtpEmailAddress {
83
+ &self.0
84
+ }
85
+ }
86
+
87
+ impl AsRef<str> for EmailAddress {
88
+ fn as_ref(&self) -> &str {
89
+ self.0.as_ref()
90
+ }
91
+ }
92
+
93
+ /// Perform the email verification via a specified proxy. The usage of a proxy
94
+ /// is optional.
95
+ #[derive(Debug, Default, Clone, Deserialize, PartialEq, Serialize)]
96
+ pub struct CheckEmailInputProxy {
97
+ /// Use the specified SOCKS5 proxy host to perform email verification.
98
+ pub host: String,
99
+ /// Use the specified SOCKS5 proxy port to perform email verification.
100
+ pub port: u16,
101
+ /// Username to pass to proxy authentication.
102
+ pub username: Option<String>,
103
+ /// Password to pass to proxy authentication.
104
+ pub password: Option<String>,
105
+ /// The timeout for the connection to the proxy, in ms.
106
+ pub timeout_ms: Option<u64>,
107
+ }
108
+
109
+ /// Builder pattern for the input argument into the main `email_exists`
110
+ /// function.
111
+ #[derive(Builder, Debug, Clone, Deserialize, Serialize)]
112
+ #[builder(default)]
113
+ pub struct CheckEmailInput {
114
+ /// The email to validate.
115
+ pub to_email: String,
116
+
117
+ pub verif_method: VerifMethod,
118
+
119
+ /// Whether to check if a gravatar image is existing for the given email.
120
+ /// Adds a bit of latency to the verification process.
121
+ ///
122
+ /// Defaults to false.
123
+ pub check_gravatar: bool,
124
+ /// Check if a the email address is present in HaveIBeenPwned API.
125
+ /// If the api_key is filled, HaveIBeenPwned API is checked
126
+ pub haveibeenpwned_api_key: Option<String>,
127
+
128
+ /// The WebDriver address to use for headless verifications.
129
+ ///
130
+ /// Defaults to http://localhost:9515.
131
+ pub webdriver_addr: String,
132
+ /// The WebDriver configuration to use for headless verifications.
133
+ ///
134
+ /// Defaults to the default WebdriverConfig.
135
+ pub webdriver_config: WebdriverConfig,
136
+ /// Identifier for the service currently running Reacher. We recommend
137
+ /// setting this to an unique identifier of the server where Reacher is
138
+ /// installed on.
139
+ ///
140
+ /// Defaults to "backend-dev".
141
+ pub backend_name: String,
142
+ /// Sentry DSN to send errors to Sentry.
143
+ ///
144
+ /// Defaults to None.
145
+ pub sentry_dsn: Option<String>,
146
+ }
147
+
148
+ impl Default for CheckEmailInput {
149
+ fn default() -> Self {
150
+ CheckEmailInput {
151
+ to_email: "".into(),
152
+ verif_method: VerifMethod::default(),
153
+ check_gravatar: false,
154
+ haveibeenpwned_api_key: None,
155
+ webdriver_addr: "http://localhost:9515".into(),
156
+ webdriver_config: WebdriverConfig::default(),
157
+ backend_name: "backend-dev".into(),
158
+ sentry_dsn: None,
159
+ }
160
+ }
161
+ }
162
+
163
+ /// An enum to describe how confident we are that the recipient address is
164
+ /// real.
165
+ #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
166
+ #[serde(rename_all = "lowercase")]
167
+ pub enum Reachable {
168
+ /// The email is safe to send.
169
+ Safe,
170
+ /// The email address appears to exist, but has quality issues that may
171
+ /// result in low engagement or a bounce. Emails are classified as risky
172
+ /// when one of the following happens:
173
+ /// - catch-all email,
174
+ /// - disposable email,
175
+ /// - role-based address,
176
+ /// - full inbox.
177
+ Risky,
178
+ /// Emails that don't exist or are syntactically incorrect. Do not send to
179
+ /// these emails.
180
+ Invalid,
181
+ /// We're unable to get a valid response from the recipient's email server.
182
+ Unknown,
183
+ }
184
+
185
+ /// Details about the email verification used for debugging.
186
+ #[derive(Debug, Deserialize, Serialize)]
187
+ pub struct DebugDetails {
188
+ /// Identifier for the service currently running Reacher.
189
+ pub backend_name: String,
190
+ /// The time when the email verification started.
191
+ pub start_time: DateTime<Utc>,
192
+ /// The time when the email verification ended.
193
+ pub end_time: DateTime<Utc>,
194
+ /// The duration of the email verification.
195
+ pub duration: Duration,
196
+ /// Details about the email verification used for debugging.
197
+ pub smtp: SmtpDebug,
198
+ }
199
+
200
+ impl Default for DebugDetails {
201
+ fn default() -> Self {
202
+ Self {
203
+ start_time: SystemTime::now().into(),
204
+ end_time: SystemTime::now().into(),
205
+ duration: Duration::default(),
206
+ smtp: SmtpDebug::default(),
207
+ backend_name: "backend-dev".into(),
208
+ }
209
+ }
210
+ }
211
+
212
+ /// The result of the [check_email](check_email) function.
213
+ #[derive(Debug)]
214
+ pub struct CheckEmailOutput {
215
+ /// Input by the user.
216
+ pub input: String,
217
+ pub is_reachable: Reachable,
218
+ /// Misc details about the email address.
219
+ pub misc: Result<MiscDetails, MiscError>,
220
+ /// Details about the MX host.
221
+ pub mx: Result<MxDetails, MxError>,
222
+ /// Details about the SMTP responses of the email.
223
+ pub smtp: Result<SmtpDetails, SmtpError>,
224
+ /// Details about the email address.
225
+ pub syntax: SyntaxDetails,
226
+ /// Details about the email verification used for debugging.
227
+ pub debug: DebugDetails,
228
+ }
229
+
230
+ impl Default for CheckEmailOutput {
231
+ fn default() -> Self {
232
+ CheckEmailOutput {
233
+ input: String::default(),
234
+ is_reachable: Reachable::Unknown,
235
+ misc: Ok(MiscDetails::default()),
236
+ mx: Ok(MxDetails::default()),
237
+ smtp: Ok(SmtpDetails::default()),
238
+ syntax: SyntaxDetails::default(),
239
+ debug: DebugDetails::default(),
240
+ }
241
+ }
242
+ }
243
+
244
+ // Implement a custom serialize.
245
+ impl Serialize for CheckEmailOutput {
246
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
247
+ where
248
+ S: Serializer,
249
+ {
250
+ // This is just used internally to get the nested error field.
251
+ #[derive(Serialize)]
252
+ struct MyError<E> {
253
+ error: E,
254
+ // We add an optional "description" field when relevant, given by
255
+ // the `get_description` on SmtpError.
256
+ #[serde(skip_serializing_if = "Option::is_none")]
257
+ description: Option<SmtpErrorDesc>,
258
+ }
259
+
260
+ let mut map = serializer.serialize_map(Some(1))?;
261
+ map.serialize_entry("input", &self.input)?;
262
+ map.serialize_entry("is_reachable", &self.is_reachable)?;
263
+ match &self.misc {
264
+ Ok(t) => map.serialize_entry("misc", &t)?,
265
+ Err(error) => map.serialize_entry(
266
+ "misc",
267
+ &MyError {
268
+ error,
269
+ description: None,
270
+ },
271
+ )?,
272
+ }
273
+ match &self.mx {
274
+ Ok(t) => map.serialize_entry("mx", &t)?,
275
+ Err(error) => map.serialize_entry(
276
+ "mx",
277
+ &MyError {
278
+ error,
279
+ description: None,
280
+ },
281
+ )?,
282
+ }
283
+ match &self.smtp {
284
+ Ok(t) => map.serialize_entry("smtp", &t)?,
285
+ Err(error) => map.serialize_entry(
286
+ "smtp",
287
+ &MyError {
288
+ error,
289
+ description: error.get_description(),
290
+ },
291
+ )?,
292
+ }
293
+ map.serialize_entry("syntax", &self.syntax)?;
294
+ map.serialize_entry("debug", &self.debug)?;
295
+ map.end()
296
+ }
297
+ }
298
+
299
+ #[derive(Builder, Clone, Debug, Deserialize, Serialize, Default)]
300
+ pub struct WebdriverConfig {
301
+ pub binary: Option<String>,
302
+ }
303
+
304
+ #[cfg(test)]
305
+ mod tests {
306
+ use super::{CheckEmailOutput, DebugDetails};
307
+ use async_smtp::response::{Category, Code, Detail, Response, Severity};
308
+
309
+ #[test]
310
+ fn should_serialize_correctly() {
311
+ // create a dummy CheckEmailOutput, with a given message as a transient
312
+ // SMTP error.
313
+ fn dummy_response_with_message(m: &str) -> CheckEmailOutput {
314
+ let r = Response::new(
315
+ Code {
316
+ severity: Severity::TransientNegativeCompletion,
317
+ category: Category::MailSystem,
318
+ detail: Detail::Zero,
319
+ },
320
+ vec![m.to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()],
321
+ );
322
+
323
+ CheckEmailOutput {
324
+ input: "foo".to_string(),
325
+ is_reachable: super::Reachable::Unknown,
326
+ misc: Ok(super::MiscDetails::default()),
327
+ mx: Ok(super::MxDetails::default()),
328
+ syntax: super::SyntaxDetails::default(),
329
+ smtp: Err(super::SmtpError::AsyncSmtpError(r.into())),
330
+ debug: DebugDetails::default(),
331
+ }
332
+ }
333
+
334
+ let res = dummy_response_with_message("blacklist");
335
+ let actual = serde_json::to_string(&res).unwrap();
336
+ // Make sure the `description` is present with IpBlacklisted.
337
+ let expected = r#""smtp":{"error":{"type":"AsyncSmtpError","message":"transient: blacklist; 8BITMIME; SIZE 42"},"description":"IpBlacklisted"}"#;
338
+ assert!(actual.contains(expected));
339
+
340
+ let res =
341
+ dummy_response_with_message("Client host rejected: cannot find your reverse hostname");
342
+ let actual = serde_json::to_string(&res).unwrap();
343
+ // Make sure the `description` is present with NeedsRDNs.
344
+ let expected = r#"smtp":{"error":{"type":"AsyncSmtpError","message":"transient: Client host rejected: cannot find your reverse hostname; 8BITMIME; SIZE 42"},"description":"NeedsRDNS"}"#;
345
+ assert!(actual.contains(expected));
346
+
347
+ let res = dummy_response_with_message("foobar");
348
+ let actual = serde_json::to_string(&res).unwrap();
349
+ // Make sure the `description` is NOT present.
350
+ let expected = r#""smtp":{"error":{"type":"AsyncSmtpError","message":"transient: foobar; 8BITMIME; SIZE 42"}}"#;
351
+ assert!(actual.contains(expected));
352
+ }
353
+ }
@@ -0,0 +1,20 @@
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
+ pub mod input_output;
18
+ #[cfg(feature = "sentry")]
19
+ pub mod sentry;
20
+ pub mod ser_with_display;
@@ -0,0 +1,173 @@
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
+ //! Helper functions to send events to Sentry.
18
+ //!
19
+ //! This module also contains functions that check if the error's given by
20
+ //! `check-if-email-exists` are known errors, in which case we don't log them
21
+ //! to Sentry.
22
+
23
+ use crate::misc::MiscError;
24
+ use crate::mx::MxError;
25
+ use crate::LOG_TARGET;
26
+ use crate::{smtp::SmtpError, CheckEmailOutput};
27
+ use async_smtp::error::Error as AsyncSmtpError;
28
+ use sentry::protocol::{Event, Exception, Level, Values};
29
+ use thiserror::Error;
30
+ use tracing::{debug, info};
31
+
32
+ const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
33
+
34
+ /// Setup Sentry.
35
+ pub fn setup_sentry(sentry_dsn: &str) -> sentry::ClientInitGuard {
36
+ // Use an empty string if we don't have any env variable for sentry. Sentry
37
+ // will just silently ignore.
38
+ let sentry = sentry::init(sentry_dsn);
39
+ if sentry.is_enabled() {
40
+ info!(target: LOG_TARGET, "Sentry is successfully set up.")
41
+ }
42
+
43
+ sentry
44
+ }
45
+
46
+ #[derive(Debug, Error)]
47
+ enum SentryError<'a> {
48
+ #[error("MiscError: {0}")]
49
+ Misc(&'a MiscError),
50
+ #[error("MxError: {0}")]
51
+ Mx(&'a MxError),
52
+ #[error("SmtpError: {0}")]
53
+ Smtp(&'a SmtpError),
54
+ }
55
+
56
+ impl<'a> SentryError<'a> {
57
+ /// Get the error type to be passed into Sentry's Exception `ty` field.
58
+ fn get_exception_type(&self) -> String {
59
+ match self {
60
+ SentryError::Misc(_) => "MiscError".into(),
61
+ SentryError::Mx(_) => "MxError".into(),
62
+ SentryError::Smtp(_) => "SmtpError".into(),
63
+ }
64
+ }
65
+ }
66
+
67
+ /// Helper function to send an Error event to Sentry. We redact all sensitive
68
+ /// info before sending to Sentry, by removing all instances of `username`.
69
+ fn error(err: SentryError, result: &CheckEmailOutput, backend_name: &str) {
70
+ let exception_value = redact(format!("{err:?}").as_str(), &result.syntax.username);
71
+ debug!(target: LOG_TARGET, "Sending error to Sentry: {}", exception_value);
72
+
73
+ let exception = Exception {
74
+ ty: err.get_exception_type(),
75
+ value: Some(exception_value),
76
+ ..Default::default()
77
+ };
78
+
79
+ sentry::capture_event(Event {
80
+ exception: Values {
81
+ values: vec![exception],
82
+ },
83
+ level: Level::Error,
84
+ environment: Some("production".into()),
85
+ release: Some(format!("v{}", CARGO_PKG_VERSION).into()),
86
+ message: Some(format!("{result:#?}")),
87
+ server_name: Some(backend_name.to_string().into()),
88
+ transaction: Some(format!("check_email:{}", result.syntax.domain)),
89
+ ..Default::default()
90
+ });
91
+ }
92
+
93
+ /// Function to replace all usernames from email, and replace them with
94
+ /// `***@domain.com` for privacy reasons.
95
+ fn redact(input: &str, username: &str) -> String {
96
+ input.replace(username, "***")
97
+ }
98
+
99
+ /// Check if the message contains known SMTP Transient errors.
100
+ fn skip_smtp_transient_errors(message: &[String]) -> bool {
101
+ let first_line = message[0].to_lowercase();
102
+
103
+ // 4.3.2 Please try again later
104
+ first_line.contains("try again") ||
105
+ // Temporary local problem - please try later
106
+ first_line.contains("try later")
107
+ }
108
+
109
+ /// Checks if the output from `check-if-email-exists` has a known error, in
110
+ /// which case we don't log to Sentry to avoid spamming it.
111
+ pub fn log_unknown_errors(result: &CheckEmailOutput, backend_name: &str) {
112
+ match (&result.misc, &result.mx, &result.smtp) {
113
+ (Err(err), _, _) => {
114
+ // We log all misc errors.
115
+ error(SentryError::Misc(err), result, backend_name);
116
+ }
117
+ (_, Err(err), _) => {
118
+ // We log all mx errors.
119
+ error(SentryError::Mx(err), result, backend_name);
120
+ }
121
+ (_, _, Err(err)) if err.get_description().is_some() => {
122
+ // If the SMTP error is known, we don't track it in Sentry.
123
+ }
124
+ (_, _, Err(SmtpError::AsyncSmtpError(AsyncSmtpError::Transient(response))))
125
+ if skip_smtp_transient_errors(&response.message) =>
126
+ {
127
+ // If the SMTP error is transient and known, we don't track it in
128
+ // Sentry, just log it locally.
129
+ debug!(target: LOG_TARGET,
130
+ "Transient error: {}",
131
+ redact(
132
+ response.message[0].as_str(),
133
+ result.syntax.username.as_str()
134
+ )
135
+ );
136
+ }
137
+ (_, _, Err(err)) => {
138
+ // If it's a SMTP error we didn't catch above, we log to
139
+ // Sentry, to be able to debug them better. We don't want to
140
+ // spam Sentry and log all instances of the error, hence the
141
+ // `count` check.
142
+ error(SentryError::Smtp(err), result, backend_name);
143
+ }
144
+ // If everything is ok, we just return the result.
145
+ (Ok(_), Ok(_), Ok(_)) => {}
146
+ }
147
+ }
148
+
149
+ #[cfg(test)]
150
+ mod tests {
151
+ use super::redact;
152
+
153
+ #[test]
154
+ fn test_redact() {
155
+ assert_eq!("***@gmail.com", redact("someone@gmail.com", "someone"));
156
+ assert_eq!(
157
+ "my email is ***@gmail.com.",
158
+ redact("my email is someone@gmail.com.", "someone")
159
+ );
160
+ assert_eq!(
161
+ "my email is ***@gmail.com., I repeat, my email is ***@gmail.com.",
162
+ redact(
163
+ "my email is someone@gmail.com., I repeat, my email is someone@gmail.com.",
164
+ "someone"
165
+ )
166
+ );
167
+ assert_eq!(
168
+ "*** @ gmail . com",
169
+ redact("someone @ gmail . com", "someone")
170
+ );
171
+ assert_eq!("*** is here.", redact("someone is here.", "someone"));
172
+ }
173
+ }
@@ -0,0 +1,28 @@
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 serde::Serializer;
18
+ use std::fmt::Display;
19
+
20
+ /// Implement the `Serialize` trait for types that are `Display`.
21
+ /// https://stackoverflow.com/questions/58103801/serialize-using-the-display-trait
22
+ pub fn ser_with_display<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
23
+ where
24
+ T: Display,
25
+ S: Serializer,
26
+ {
27
+ serializer.collect_str(value)
28
+ }