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,371 @@
|
|
|
1
|
+
//! Native extension backing the `valid_email_checker` gem.
|
|
2
|
+
//!
|
|
3
|
+
//! This is a thin bridge onto the embedded `check-if-email-exists` crate under
|
|
4
|
+
//! `vendor/`. Everything crossing the boundary is JSON: Ruby hands down an
|
|
5
|
+
//! options object, and gets back the upstream `CheckEmailOutput` serialized
|
|
6
|
+
//! verbatim. Keeping the seam at JSON means the gem exposes the upstream
|
|
7
|
+
//! result shape without this file having to mirror every upstream struct.
|
|
8
|
+
|
|
9
|
+
mod nogvl;
|
|
10
|
+
|
|
11
|
+
use std::collections::HashMap;
|
|
12
|
+
use std::time::Duration;
|
|
13
|
+
|
|
14
|
+
use check_if_email_exists::smtp::verif_method::{
|
|
15
|
+
EverythingElseVerifMethod, GmailVerifMethod, HotmailB2BVerifMethod, HotmailB2CVerifMethod,
|
|
16
|
+
MimecastVerifMethod, ProofpointVerifMethod, VerifMethod, VerifMethodSmtpConfig,
|
|
17
|
+
YahooVerifMethod, DEFAULT_PROXY_ID,
|
|
18
|
+
};
|
|
19
|
+
use check_if_email_exists::syntax::{check_syntax, get_similar_mail_provider};
|
|
20
|
+
use check_if_email_exists::{check_email, CheckEmailInput, CheckEmailInputProxy};
|
|
21
|
+
use futures::stream::{self, StreamExt};
|
|
22
|
+
use magnus::{function, prelude::*, Error, Ruby};
|
|
23
|
+
use serde::Deserialize;
|
|
24
|
+
|
|
25
|
+
use nogvl::nogvl;
|
|
26
|
+
|
|
27
|
+
const UPSTREAM_JSON: &str = include_str!("../vendor/UPSTREAM.json");
|
|
28
|
+
|
|
29
|
+
/// Upstream's own defaults, restated here so the gem behaves identically to
|
|
30
|
+
/// `check-if-email-exists` when the caller configures nothing.
|
|
31
|
+
const DEFAULT_FROM_EMAIL: &str = "reacher@gmail.com";
|
|
32
|
+
const DEFAULT_HELLO_NAME: &str = "gmail.com";
|
|
33
|
+
const DEFAULT_WEBDRIVER_ADDR: &str = "http://localhost:9515";
|
|
34
|
+
const DEFAULT_BACKEND_NAME: &str = "valid_email_checker";
|
|
35
|
+
const DEFAULT_SMTP_PORT: u16 = 25;
|
|
36
|
+
const DEFAULT_RETRIES: usize = 1;
|
|
37
|
+
|
|
38
|
+
/// How to verify addresses at providers that offer more than one strategy.
|
|
39
|
+
#[derive(Debug, Clone, Copy, PartialEq, Deserialize)]
|
|
40
|
+
#[serde(rename_all = "lowercase")]
|
|
41
|
+
enum ProviderMethod {
|
|
42
|
+
/// Talk to the provider's SMTP server.
|
|
43
|
+
Smtp,
|
|
44
|
+
/// Use the provider's own HTTP endpoint (Yahoo only).
|
|
45
|
+
Api,
|
|
46
|
+
/// Drive the provider's password-recovery page through a WebDriver.
|
|
47
|
+
Headless,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
51
|
+
struct ProxyOptions {
|
|
52
|
+
host: String,
|
|
53
|
+
port: u16,
|
|
54
|
+
#[serde(default)]
|
|
55
|
+
username: Option<String>,
|
|
56
|
+
#[serde(default)]
|
|
57
|
+
password: Option<String>,
|
|
58
|
+
#[serde(default)]
|
|
59
|
+
timeout_ms: Option<u64>,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
impl From<ProxyOptions> for CheckEmailInputProxy {
|
|
63
|
+
fn from(p: ProxyOptions) -> Self {
|
|
64
|
+
CheckEmailInputProxy {
|
|
65
|
+
host: p.host,
|
|
66
|
+
port: p.port,
|
|
67
|
+
username: p.username,
|
|
68
|
+
password: p.password,
|
|
69
|
+
timeout_ms: p.timeout_ms,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Every field is optional so that Ruby can pass `nil` for anything it has no
|
|
75
|
+
/// opinion about; defaults are resolved here rather than in serde, which keeps
|
|
76
|
+
/// the "unset" and "explicitly nil" cases identical.
|
|
77
|
+
#[derive(Debug, Clone, Default, Deserialize)]
|
|
78
|
+
#[serde(default, deny_unknown_fields)]
|
|
79
|
+
struct Options {
|
|
80
|
+
from_email: Option<String>,
|
|
81
|
+
hello_name: Option<String>,
|
|
82
|
+
smtp_port: Option<u16>,
|
|
83
|
+
smtp_timeout_ms: Option<u64>,
|
|
84
|
+
retries: Option<usize>,
|
|
85
|
+
check_gravatar: Option<bool>,
|
|
86
|
+
haveibeenpwned_api_key: Option<String>,
|
|
87
|
+
proxy: Option<ProxyOptions>,
|
|
88
|
+
webdriver_addr: Option<String>,
|
|
89
|
+
backend_name: Option<String>,
|
|
90
|
+
timeout_ms: Option<u64>,
|
|
91
|
+
yahoo: Option<ProviderMethod>,
|
|
92
|
+
hotmail_b2c: Option<ProviderMethod>,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
impl Options {
|
|
96
|
+
/// Build the upstream input for one address.
|
|
97
|
+
///
|
|
98
|
+
/// Note the provider defaults: unlike upstream, Yahoo defaults to its HTTP
|
|
99
|
+
/// API and Hotmail B2C to SMTP, so that a freshly installed gem works
|
|
100
|
+
/// without a WebDriver running. Pass `yahoo: :headless` /
|
|
101
|
+
/// `hotmail_b2c: :headless` to opt back into upstream's behaviour.
|
|
102
|
+
fn to_input(&self, email: &str) -> Result<CheckEmailInput, String> {
|
|
103
|
+
let mut proxies = HashMap::new();
|
|
104
|
+
let proxy_id = match &self.proxy {
|
|
105
|
+
Some(proxy) => {
|
|
106
|
+
proxies.insert(DEFAULT_PROXY_ID.to_string(), proxy.clone().into());
|
|
107
|
+
Some(DEFAULT_PROXY_ID.to_string())
|
|
108
|
+
}
|
|
109
|
+
None => None,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
let smtp_config = VerifMethodSmtpConfig {
|
|
113
|
+
from_email: self
|
|
114
|
+
.from_email
|
|
115
|
+
.clone()
|
|
116
|
+
.unwrap_or_else(|| DEFAULT_FROM_EMAIL.to_string()),
|
|
117
|
+
hello_name: self
|
|
118
|
+
.hello_name
|
|
119
|
+
.clone()
|
|
120
|
+
.unwrap_or_else(|| DEFAULT_HELLO_NAME.to_string()),
|
|
121
|
+
proxy: proxy_id,
|
|
122
|
+
smtp_port: self.smtp_port.unwrap_or(DEFAULT_SMTP_PORT),
|
|
123
|
+
smtp_timeout: self.smtp_timeout_ms.map(Duration::from_millis),
|
|
124
|
+
retries: self.retries.unwrap_or(DEFAULT_RETRIES),
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
let yahoo = match self.yahoo.unwrap_or(ProviderMethod::Api) {
|
|
128
|
+
ProviderMethod::Api => YahooVerifMethod::Api,
|
|
129
|
+
ProviderMethod::Headless => YahooVerifMethod::Headless,
|
|
130
|
+
ProviderMethod::Smtp => YahooVerifMethod::Smtp(smtp_config.clone()),
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
let hotmailb2c = match self.hotmail_b2c.unwrap_or(ProviderMethod::Smtp) {
|
|
134
|
+
ProviderMethod::Smtp => HotmailB2CVerifMethod::Smtp(smtp_config.clone()),
|
|
135
|
+
ProviderMethod::Headless => HotmailB2CVerifMethod::Headless,
|
|
136
|
+
ProviderMethod::Api => {
|
|
137
|
+
return Err("hotmail_b2c does not support :api; use :smtp or :headless".to_string())
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
let verif_method = VerifMethod {
|
|
142
|
+
proxies,
|
|
143
|
+
gmail: GmailVerifMethod::Smtp(smtp_config.clone()),
|
|
144
|
+
hotmailb2b: HotmailB2BVerifMethod::Smtp(smtp_config.clone()),
|
|
145
|
+
hotmailb2c,
|
|
146
|
+
mimecast: MimecastVerifMethod::Smtp(smtp_config.clone()),
|
|
147
|
+
proofpoint: ProofpointVerifMethod::Smtp(smtp_config.clone()),
|
|
148
|
+
yahoo,
|
|
149
|
+
everything_else: EverythingElseVerifMethod::Smtp(smtp_config),
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
verif_method
|
|
153
|
+
.validate_proxies()
|
|
154
|
+
.map_err(|e| format!("invalid proxy configuration: {e}"))?;
|
|
155
|
+
|
|
156
|
+
Ok(CheckEmailInput {
|
|
157
|
+
to_email: email.to_string(),
|
|
158
|
+
verif_method,
|
|
159
|
+
check_gravatar: self.check_gravatar.unwrap_or(false),
|
|
160
|
+
haveibeenpwned_api_key: self.haveibeenpwned_api_key.clone(),
|
|
161
|
+
webdriver_addr: self
|
|
162
|
+
.webdriver_addr
|
|
163
|
+
.clone()
|
|
164
|
+
.unwrap_or_else(|| DEFAULT_WEBDRIVER_ADDR.to_string()),
|
|
165
|
+
webdriver_config: Default::default(),
|
|
166
|
+
backend_name: self
|
|
167
|
+
.backend_name
|
|
168
|
+
.clone()
|
|
169
|
+
.unwrap_or_else(|| DEFAULT_BACKEND_NAME.to_string()),
|
|
170
|
+
sentry_dsn: None,
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/// A tokio runtime is built per call rather than cached in a static.
|
|
176
|
+
///
|
|
177
|
+
/// Runtime creation costs well under a millisecond against verifications that
|
|
178
|
+
/// take seconds, and building it on demand means there is no cached runtime to
|
|
179
|
+
/// be left with dead worker threads if the process forks.
|
|
180
|
+
///
|
|
181
|
+
/// Note that this does not make verification fork-safe on its own: the system
|
|
182
|
+
/// network stack has its own inherited state, which the Ruby side guards
|
|
183
|
+
/// against. See `ValidEmailChecker::ForkAfterVerificationError`.
|
|
184
|
+
fn runtime() -> Result<tokio::runtime::Runtime, String> {
|
|
185
|
+
tokio::runtime::Builder::new_multi_thread()
|
|
186
|
+
.worker_threads(2)
|
|
187
|
+
.enable_all()
|
|
188
|
+
.build()
|
|
189
|
+
.map_err(|e| format!("could not start the async runtime: {e}"))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/// Serialize one verification, substituting a synthetic record when the
|
|
193
|
+
/// wall-clock budget runs out.
|
|
194
|
+
///
|
|
195
|
+
/// The timeout result keeps the same top-level shape as a real one — an
|
|
196
|
+
/// `input` and an `is_reachable` of `unknown` — so Ruby can wrap every entry
|
|
197
|
+
/// in the same `Result` object and callers do not need a separate code path.
|
|
198
|
+
async fn check_one_to_json(input: CheckEmailInput, timeout: Option<Duration>) -> String {
|
|
199
|
+
let email = input.to_email.clone();
|
|
200
|
+
|
|
201
|
+
let output = match timeout {
|
|
202
|
+
Some(duration) => match tokio::time::timeout(duration, check_email(&input)).await {
|
|
203
|
+
Ok(output) => output,
|
|
204
|
+
Err(_) => {
|
|
205
|
+
return serde_json::json!({
|
|
206
|
+
"input": email,
|
|
207
|
+
"is_reachable": "unknown",
|
|
208
|
+
"error": {
|
|
209
|
+
"type": "Timeout",
|
|
210
|
+
"message": format!("verification timed out after {}ms", duration.as_millis()),
|
|
211
|
+
},
|
|
212
|
+
})
|
|
213
|
+
.to_string()
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
None => check_email(&input).await,
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
serde_json::to_string(&output)
|
|
220
|
+
.unwrap_or_else(|e| panic!("upstream result for {email} could not be serialized: {e}"))
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
fn parse_options(ruby: &Ruby, options_json: String) -> Result<Options, Error> {
|
|
224
|
+
serde_json::from_str::<Options>(&options_json)
|
|
225
|
+
.map_err(|e| Error::new(ruby.exception_arg_error(), format!("invalid options: {e}")))
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
fn config_error(ruby: &Ruby, message: String) -> Error {
|
|
229
|
+
Error::new(ruby.exception_arg_error(), message)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/// Re-raise a panic from the verification thread as a Ruby exception, now that
|
|
233
|
+
/// we hold the GVL again.
|
|
234
|
+
fn panic_to_error(ruby: &Ruby, payload: Box<dyn std::any::Any + Send>) -> Error {
|
|
235
|
+
let detail = if let Some(s) = payload.downcast_ref::<&str>() {
|
|
236
|
+
(*s).to_string()
|
|
237
|
+
} else if let Some(s) = payload.downcast_ref::<String>() {
|
|
238
|
+
s.clone()
|
|
239
|
+
} else {
|
|
240
|
+
"unknown panic".to_string()
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
Error::new(
|
|
244
|
+
ruby.exception_runtime_error(),
|
|
245
|
+
format!("valid_email_checker: the native verifier panicked: {detail}"),
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/// Syntax-only check. Pure CPU work, no network, so the GVL stays held.
|
|
250
|
+
///
|
|
251
|
+
/// Upstream fills in the typo `suggestion` from a separate call, which
|
|
252
|
+
/// `check_email` only reaches on its failure paths. We always populate it,
|
|
253
|
+
/// since a caller asking about syntax alone is exactly who wants "did you mean
|
|
254
|
+
/// gmail.com?".
|
|
255
|
+
fn rb_check_syntax(ruby: &Ruby, email: String) -> Result<String, Error> {
|
|
256
|
+
let mut details = check_syntax(&email);
|
|
257
|
+
get_similar_mail_provider(&mut details);
|
|
258
|
+
|
|
259
|
+
// That helper matches any provider within a levenshtein distance of 3,
|
|
260
|
+
// which includes a distance of 0 -- so a correct domain "suggests" itself.
|
|
261
|
+
// Upstream only calls it on failure paths, where that is harmless noise.
|
|
262
|
+
// We call it on every syntax check, so drop anything that is not actually
|
|
263
|
+
// a correction.
|
|
264
|
+
let suggests_same_domain = details
|
|
265
|
+
.suggestion
|
|
266
|
+
.as_deref()
|
|
267
|
+
.and_then(|suggestion| suggestion.rsplit_once('@'))
|
|
268
|
+
.map(|(_, domain)| domain == details.domain)
|
|
269
|
+
.unwrap_or(false);
|
|
270
|
+
|
|
271
|
+
if suggests_same_domain {
|
|
272
|
+
details.suggestion = None;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
serde_json::to_string(&details).map_err(|e| {
|
|
276
|
+
Error::new(
|
|
277
|
+
ruby.exception_runtime_error(),
|
|
278
|
+
format!("could not serialize syntax details: {e}"),
|
|
279
|
+
)
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/// Verify a single address. Returns the upstream result as a JSON string.
|
|
284
|
+
fn rb_check(ruby: &Ruby, email: String, options_json: String) -> Result<String, Error> {
|
|
285
|
+
let options = parse_options(ruby, options_json)?;
|
|
286
|
+
let timeout = options.timeout_ms.map(Duration::from_millis);
|
|
287
|
+
let input = options
|
|
288
|
+
.to_input(&email)
|
|
289
|
+
.map_err(|e| config_error(ruby, e))?;
|
|
290
|
+
|
|
291
|
+
let work = move || {
|
|
292
|
+
let runtime = runtime()?;
|
|
293
|
+
Ok(runtime.block_on(check_one_to_json(input, timeout)))
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
nogvl(work)
|
|
297
|
+
.map_err(|payload| panic_to_error(ruby, payload))?
|
|
298
|
+
.map_err(|e| config_error(ruby, e))
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/// Verify several addresses concurrently on a single runtime.
|
|
302
|
+
///
|
|
303
|
+
/// This is the main reason to embed the crate rather than shell out: one
|
|
304
|
+
/// GVL release and one runtime cover the whole batch, and `concurrency`
|
|
305
|
+
/// verifications proceed at once. Results come back in the order the emails
|
|
306
|
+
/// were given, regardless of which finished first.
|
|
307
|
+
fn rb_check_many(
|
|
308
|
+
ruby: &Ruby,
|
|
309
|
+
emails: Vec<String>,
|
|
310
|
+
options_json: String,
|
|
311
|
+
concurrency: usize,
|
|
312
|
+
) -> Result<String, Error> {
|
|
313
|
+
let options = parse_options(ruby, options_json)?;
|
|
314
|
+
let timeout = options.timeout_ms.map(Duration::from_millis);
|
|
315
|
+
let concurrency = concurrency.max(1);
|
|
316
|
+
|
|
317
|
+
let inputs = emails
|
|
318
|
+
.iter()
|
|
319
|
+
.map(|email| options.to_input(email))
|
|
320
|
+
.collect::<Result<Vec<_>, _>>()
|
|
321
|
+
.map_err(|e| config_error(ruby, e))?;
|
|
322
|
+
|
|
323
|
+
let work = move || {
|
|
324
|
+
let runtime = runtime()?;
|
|
325
|
+
|
|
326
|
+
let results = runtime.block_on(async move {
|
|
327
|
+
let mut results: Vec<(usize, String)> = stream::iter(inputs.into_iter().enumerate())
|
|
328
|
+
.map(|(index, input)| async move {
|
|
329
|
+
(index, check_one_to_json(input, timeout).await)
|
|
330
|
+
})
|
|
331
|
+
.buffer_unordered(concurrency)
|
|
332
|
+
.collect()
|
|
333
|
+
.await;
|
|
334
|
+
|
|
335
|
+
results.sort_by_key(|(index, _)| *index);
|
|
336
|
+
results
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// Each element is already a JSON document, so splice them into an
|
|
340
|
+
// array rather than round-tripping through serde_json::Value.
|
|
341
|
+
let joined = results
|
|
342
|
+
.into_iter()
|
|
343
|
+
.map(|(_, json)| json)
|
|
344
|
+
.collect::<Vec<_>>()
|
|
345
|
+
.join(",");
|
|
346
|
+
|
|
347
|
+
Ok(format!("[{joined}]"))
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
nogvl(work)
|
|
351
|
+
.map_err(|payload| panic_to_error(ruby, payload))?
|
|
352
|
+
.map_err(|e| config_error(ruby, e))
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/// Provenance of the embedded crate, as JSON.
|
|
356
|
+
fn rb_upstream() -> &'static str {
|
|
357
|
+
UPSTREAM_JSON
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
#[magnus::init]
|
|
361
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
362
|
+
let parent = ruby.define_module("ValidEmailChecker")?;
|
|
363
|
+
let module = parent.define_module("Native")?;
|
|
364
|
+
|
|
365
|
+
module.define_singleton_method("check_syntax", function!(rb_check_syntax, 1))?;
|
|
366
|
+
module.define_singleton_method("check", function!(rb_check, 2))?;
|
|
367
|
+
module.define_singleton_method("check_many", function!(rb_check_many, 3))?;
|
|
368
|
+
module.define_singleton_method("upstream", function!(rb_upstream, 0))?;
|
|
369
|
+
|
|
370
|
+
Ok(())
|
|
371
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
//! Releasing the GVL around blocking verification work.
|
|
2
|
+
//!
|
|
3
|
+
//! Verifying an email address is network-bound: it does DNS lookups and then
|
|
4
|
+
//! holds an SMTP conversation, which routinely takes seconds. Holding Ruby's
|
|
5
|
+
//! global VM lock for that whole time would freeze every other thread in the
|
|
6
|
+
//! process, so we hand the work to `rb_thread_call_without_gvl`.
|
|
7
|
+
//!
|
|
8
|
+
//! magnus does not wrap that function (it is on its explicit "not yet
|
|
9
|
+
//! wrapped" list), so we reach for the raw binding in `rb-sys`.
|
|
10
|
+
|
|
11
|
+
use std::ffi::c_void;
|
|
12
|
+
use std::panic::{catch_unwind, AssertUnwindSafe};
|
|
13
|
+
|
|
14
|
+
/// Run `func` with the GVL released.
|
|
15
|
+
///
|
|
16
|
+
/// A panic inside `func` is caught rather than allowed to unwind across the
|
|
17
|
+
/// `extern "C"` boundary, which would be undefined behaviour. It is returned
|
|
18
|
+
/// to the caller so it can be re-raised once the GVL is held again.
|
|
19
|
+
pub fn nogvl<F, R>(func: F) -> std::thread::Result<R>
|
|
20
|
+
where
|
|
21
|
+
F: FnOnce() -> R,
|
|
22
|
+
{
|
|
23
|
+
struct Args<F, R> {
|
|
24
|
+
func: Option<F>,
|
|
25
|
+
result: Option<std::thread::Result<R>>,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
unsafe extern "C" fn call<F, R>(arg: *mut c_void) -> *mut c_void
|
|
29
|
+
where
|
|
30
|
+
F: FnOnce() -> R,
|
|
31
|
+
{
|
|
32
|
+
// Safety: `arg` is the `&mut Args` we pass below. Ruby invokes this
|
|
33
|
+
// callback exactly once, on this same thread, before
|
|
34
|
+
// `rb_thread_call_without_gvl` returns, so the reference is live and
|
|
35
|
+
// uniquely held for the duration.
|
|
36
|
+
let args = unsafe { &mut *(arg as *mut Args<F, R>) };
|
|
37
|
+
let func = args
|
|
38
|
+
.func
|
|
39
|
+
.take()
|
|
40
|
+
.expect("nogvl callback invoked more than once");
|
|
41
|
+
args.result = Some(catch_unwind(AssertUnwindSafe(func)));
|
|
42
|
+
std::ptr::null_mut()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let mut args: Args<F, R> = Args {
|
|
46
|
+
func: Some(func),
|
|
47
|
+
result: None,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// We deliberately pass no unblocking function. The alternative is to let
|
|
51
|
+
// Ruby interrupt the thread, but the actual work happens on tokio worker
|
|
52
|
+
// threads that would keep running, so there would be nothing to cancel.
|
|
53
|
+
// Instead, callers bound the blocking region with a wall-clock timeout.
|
|
54
|
+
unsafe {
|
|
55
|
+
rb_sys::rb_thread_call_without_gvl(
|
|
56
|
+
Some(call::<F, R>),
|
|
57
|
+
(&mut args) as *mut Args<F, R> as *mut c_void,
|
|
58
|
+
None,
|
|
59
|
+
std::ptr::null_mut(),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
args.result.expect("nogvl callback never ran")
|
|
64
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "check-if-email-exists",
|
|
3
|
+
"version": "0.11.7",
|
|
4
|
+
"commit": "81da93e8a419f601c05e1bcf4a197d25eb0e349d",
|
|
5
|
+
"repository": "https://github.com/reacherhq/check-if-email-exists",
|
|
6
|
+
"license": "AGPL-3.0-only OR Commercial",
|
|
7
|
+
"vendored_path": "ext/valid_email_checker/vendor/check-if-email-exists",
|
|
8
|
+
"notes": "Verbatim copy of the upstream `core` package (the check-if-email-exists library crate). Only Cargo.toml's `publish` and `readme` keys were changed; see the comment in that file."
|
|
9
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "check-if-email-exists"
|
|
3
|
+
version = "0.11.7"
|
|
4
|
+
authors = ["Amaury <amaury@reacher.email>"]
|
|
5
|
+
categories = ["email"]
|
|
6
|
+
description = "Check if an email address exists without sending any email"
|
|
7
|
+
documentation = "http://docs.rs/check-if-email-exists"
|
|
8
|
+
edition = "2018"
|
|
9
|
+
keywords = ["email", "smtp", "mx", "verification"]
|
|
10
|
+
license = "AGPL-3.0"
|
|
11
|
+
# Vendored into the valid_email_checker gem. Two lines differ from upstream:
|
|
12
|
+
# `publish` is false (this copy must never be pushed to crates.io) and `readme`
|
|
13
|
+
# points at the copy of README.md alongside this file rather than `../`.
|
|
14
|
+
publish = false
|
|
15
|
+
readme = "README.md"
|
|
16
|
+
repository = "https://github.com/reacherhq/check-if-email-exists"
|
|
17
|
+
|
|
18
|
+
[dependencies]
|
|
19
|
+
anyhow = "1.0"
|
|
20
|
+
async-recursion = "1.0.5"
|
|
21
|
+
async-smtp = { version = "0.9.1", features = ["runtime-tokio"] }
|
|
22
|
+
chrono = { version = "0.4.31", features = ["serde"] }
|
|
23
|
+
config = "0.14"
|
|
24
|
+
derive_builder = "0.20"
|
|
25
|
+
fast-socks5 = "0.9"
|
|
26
|
+
fantoccini = { version = "0.21.2", default-features = false, features = [
|
|
27
|
+
"rustls-tls",
|
|
28
|
+
] }
|
|
29
|
+
futures = { version = "0.3.30" }
|
|
30
|
+
hickory-proto = "0.24.0"
|
|
31
|
+
hickory-resolver = "0.24.0"
|
|
32
|
+
levenshtein = "1.0.5"
|
|
33
|
+
log = "0.4.26"
|
|
34
|
+
mailchecker = "6.0.11"
|
|
35
|
+
md5 = "0.7.0"
|
|
36
|
+
once_cell = "1.21.1"
|
|
37
|
+
rand = { version = "0.8.5", features = ["small_rng"] }
|
|
38
|
+
regex = "1.11.1"
|
|
39
|
+
reqwest = { version = "0.12.15", default-features = false, features = [
|
|
40
|
+
"json",
|
|
41
|
+
"rustls-tls",
|
|
42
|
+
] }
|
|
43
|
+
rustls = { version = "0.23", features = ["ring"] }
|
|
44
|
+
sentry = { version = "0.36", default-features = false, features = [
|
|
45
|
+
"reqwest",
|
|
46
|
+
"rustls",
|
|
47
|
+
], optional = true }
|
|
48
|
+
serde = { version = "1.0.214", features = ["derive"] }
|
|
49
|
+
serde_json = "1.0.133"
|
|
50
|
+
thiserror = "2.0"
|
|
51
|
+
tokio = { version = "1.40.0", features = ["rt-multi-thread", "macros"] }
|
|
52
|
+
tracing = "0.1.40"
|