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,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"