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