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
data/NOTICE.md ADDED
@@ -0,0 +1,56 @@
1
+ # Notices and licensing
2
+
3
+ ## What this gem embeds
4
+
5
+ `valid_email_checker` is a wrapper. All of the actual email verification is
6
+ done by the Rust crate **`check-if-email-exists`**, by Reacher, a verbatim copy
7
+ of which is included in this repository at:
8
+
9
+ ext/valid_email_checker/vendor/check-if-email-exists/
10
+
11
+ That copy is the upstream `core` package. Only two lines of its `Cargo.toml`
12
+ differ from upstream — `publish` and `readme` — and the reason is recorded in a
13
+ comment there. The exact upstream version and commit are recorded in
14
+ `ext/valid_email_checker/vendor/UPSTREAM.json` and are also readable at runtime:
15
+
16
+ ```ruby
17
+ ValidEmailChecker.upstream
18
+ # => {name: "check-if-email-exists", version: "0.11.7", commit: "81da93e...", ...}
19
+ ```
20
+
21
+ * Upstream project: https://github.com/reacherhq/check-if-email-exists
22
+ * Upstream copyright: Copyright (C) 2018-2023 Reacher
23
+
24
+ ## License — please read this before deploying
25
+
26
+ Upstream `check-if-email-exists` is offered under a **dual license**:
27
+
28
+ 1. **AGPL-3.0**, or
29
+ 2. a **commercial license** sold by Reacher at https://reacher.email/pricing
30
+
31
+ Because this gem embeds and links that code, the combined work is a derivative
32
+ of it. This gem is therefore distributed under the **AGPL-3.0** (see
33
+ `LICENSE.txt`), and the AGPL's terms apply to your use of it.
34
+
35
+ The consequence most people need to know about is **AGPL section 13**. If you
36
+ run a modified version of this software and let users interact with it over a
37
+ network, you must offer those users the corresponding source code of your
38
+ modified version. Unlike the GPL, for AGPL-covered code this obligation is
39
+ triggered by providing network access, not only by distributing binaries — so
40
+ using this gem inside a public-facing web application is exactly the case the
41
+ clause is written for.
42
+
43
+ Nothing here is legal advice. If the AGPL does not suit how you intend to
44
+ deploy, buy the commercial license from Reacher; that is precisely the reason
45
+ the dual-license option exists. Note that a commercial license covers the
46
+ upstream crate, and you would also need to arrange terms for this wrapper.
47
+
48
+ ## Third-party Rust dependencies
49
+
50
+ Building the extension compiles the upstream crate's dependency tree
51
+ (`tokio`, `reqwest`, `rustls`, `hickory-resolver`, `mailchecker` and others),
52
+ each under its own license, predominantly MIT and Apache-2.0. To produce a
53
+ full report:
54
+
55
+ cargo install cargo-license
56
+ cargo license --manifest-path ext/valid_email_checker/Cargo.toml
data/README.md ADDED
@@ -0,0 +1,316 @@
1
+ # valid_email_checker
2
+
3
+ Check whether an email address really exists — without sending any mail.
4
+
5
+ This gem embeds the Rust crate
6
+ [`check-if-email-exists`](https://github.com/reacherhq/check-if-email-exists)
7
+ and calls it in-process through a native extension. There is no HTTP service to
8
+ run and no API key to buy. A full check will:
9
+
10
+ - validate the syntax, and suggest a correction for a near-miss domain;
11
+ - resolve the domain's MX records;
12
+ - open an SMTP conversation with the mail exchanger and ask about the recipient,
13
+ without ever completing a message;
14
+ - report whether the address is disposable, role-based, catch-all, disabled, or
15
+ over quota.
16
+
17
+ > **Licensing:** the embedded crate is AGPL-3.0, so this gem is too. If you plan
18
+ > to use it in a network service, read [NOTICE.md](NOTICE.md) first — AGPL
19
+ > section 13 will apply to you. Upstream sells a commercial license for exactly
20
+ > this situation.
21
+
22
+ ## Install
23
+
24
+ The gem compiles the embedded Rust crate at install time, so you need a Rust
25
+ toolchain (see [rustup.rs](https://rustup.rs)):
26
+
27
+ ```console
28
+ $ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
29
+ $ gem install valid_email_checker
30
+ ```
31
+
32
+ Or in a `Gemfile`:
33
+
34
+ ```ruby
35
+ gem "valid_email_checker"
36
+ ```
37
+
38
+ A cold build takes a few minutes — the dependency tree includes `tokio`,
39
+ `rustls` and `reqwest`. Requires Ruby >= 3.1 and Rust >= 1.75.
40
+
41
+ ## Usage
42
+
43
+ ```ruby
44
+ require "valid_email_checker"
45
+
46
+ result = ValidEmailChecker.check("someone@gmail.com")
47
+
48
+ result.reachable # => :safe
49
+ result.safe? # => true
50
+ result.deliverable? # => true
51
+ result.catch_all? # => false
52
+ result.disposable? # => false
53
+ result.mx_records # => ["gmail-smtp-in.l.google.com.", ...]
54
+ result.duration # => 1.83
55
+ ```
56
+
57
+ ### The four verdicts
58
+
59
+ `#reachable` is the headline answer. Everything else is supporting detail.
60
+
61
+ | Verdict | Meaning | Predicate |
62
+ | ---------- | ------- | --------- |
63
+ | `:safe` | The address exists and has no quality problems. | `safe?` |
64
+ | `:risky` | It exists, but it is catch-all, disposable, role-based, or the inbox is full. Mail may bounce. | `risky?` |
65
+ | `:invalid` | Malformed, or the domain has no MX records, or the server rejected the recipient. Do not send. | `invalid?` |
66
+ | `:unknown` | No usable answer. Says nothing about the address — see below. | `unknown?` |
67
+
68
+ `#valid?` is an alias for `safe?`, deliberately strict. Whether `:risky` or
69
+ `:unknown` is good enough is a policy decision, so make it explicitly:
70
+
71
+ ```ruby
72
+ case result.reachable
73
+ when :safe then accept
74
+ when :risky then accept_with_warning unless result.disposable?
75
+ when :invalid then reject
76
+ when :unknown then queue_for_retry
77
+ end
78
+ ```
79
+
80
+ ### Please read this: why you will see `:unknown`
81
+
82
+ SMTP verification is a best-effort signal, not a guarantee, and `:unknown` is
83
+ common for reasons that have nothing to do with the address:
84
+
85
+ - **Outbound port 25 is blocked** on most residential networks and by most
86
+ cloud providers (AWS, GCP, Azure, Heroku, Fly). Without a SOCKS5 proxy or a
87
+ host that permits port 25, essentially every check returns `:unknown`.
88
+ - **Mail exchangers are defensive.** They greylist, rate-limit, and blacklist.
89
+ A server that has never seen your IP may simply refuse to answer.
90
+ - **Reputation matters.** Set `from_email` and `hello_name` to a domain you
91
+ actually control, with matching forward and reverse DNS. This makes a large
92
+ difference in practice.
93
+
94
+ `#error` and `#error_description` say what went wrong:
95
+
96
+ ```ruby
97
+ result.error # => {type: "AsyncSmtpError", message: "transient: ..."}
98
+ result.error_description # => "IpBlacklisted" (or "NeedsRDNS", ...)
99
+ result.timed_out? # => false
100
+ ```
101
+
102
+ Also note that **catch-all domains cannot be verified per-address** — the
103
+ server accepts every recipient, so those come back `:risky`. That is a property
104
+ of the domain, not a shortcoming of the check.
105
+
106
+ ### Syntax only, no network
107
+
108
+ Instant and side-effect free, so it is safe inline in a request cycle or a
109
+ model validation:
110
+
111
+ ```ruby
112
+ ValidEmailChecker.syntax_valid?("someone@example.com") # => true
113
+
114
+ syntax = ValidEmailChecker.syntax("someone@gmail.co")
115
+ syntax.valid? # => true
116
+ syntax.suggestion # => "someone@gmail.com"
117
+ syntax.domain # => "gmail.co"
118
+
119
+ # Provider quirks removed — Gmail ignores dots and "+" tags:
120
+ ValidEmailChecker.syntax("Some.One+news@gmail.com").normalized_email
121
+ # => "someone@gmail.com"
122
+ ```
123
+
124
+ A suggestion is only offered for an address that parses. Notorious typo domains
125
+ such as `gmial.com` are on upstream's blocklist, so they come back plainly
126
+ invalid with no domain and no suggestion.
127
+
128
+ ### Checking many addresses
129
+
130
+ `check_many` runs the whole batch on one async runtime with a single GVL
131
+ release, so `concurrency` verifications are in flight at once. Results come
132
+ back in the order given.
133
+
134
+ ```ruby
135
+ results = ValidEmailChecker.check_many(addresses, concurrency: 20)
136
+ results.select(&:safe?).map(&:email)
137
+ ```
138
+
139
+ This is the main reason to embed the crate rather than shell out to it: on this
140
+ machine, four addresses at `concurrency: 4` took 4.0s versus 13.5s serially.
141
+
142
+ Be careful raising `concurrency` when the addresses share a domain — you are
143
+ opening that many simultaneous SMTP connections to one mail exchanger, which is
144
+ a good way to get rate-limited or blacklisted.
145
+
146
+ ### Threading
147
+
148
+ `check` and `check_many` release the GVL for the duration of the verification,
149
+ so other Ruby threads keep running. Calling them from a thread pool is fine.
150
+ The async runtime is created per call rather than cached, so there is no shared
151
+ runtime to contend on.
152
+
153
+ ### Forking servers
154
+
155
+ **Do not verify an address in a process that will later fork.** Loading the gem
156
+ is fine; calling `check` or `check_many` is not.
157
+
158
+ On macOS, Apple's `Network.framework` is not fork-safe. The first verification
159
+ in a process initializes XPC and dispatch state for the system resolver, and a
160
+ child that inherits that state deadlocks inside the framework on its own first
161
+ connection — permanently, ignoring the configured timeout. Rather than let that
162
+ look like a slow mail server, the gem tracks which process verified and raises
163
+ `ValidEmailChecker::ForkAfterVerificationError` instead:
164
+
165
+ ```ruby
166
+ ValidEmailChecker.check("someone@example.com") # in the parent
167
+ fork { ValidEmailChecker.check("other@example.com") }
168
+ # => ValidEmailChecker::ForkAfterVerificationError
169
+ ```
170
+
171
+ In practice this costs nothing, because the normal arrangement is already the
172
+ safe one: let Puma or Unicorn preload your app, and verify only inside workers.
173
+ Only a verification during the preload phase itself — a boot-time health check,
174
+ say — will trip it. Syntax checks are unaffected, since they touch no sockets.
175
+
176
+ The guard applies on macOS only, and can be lifted with
177
+ `VALID_EMAIL_CHECKER_ALLOW_FORK=1` if you have reason to believe your platform
178
+ is unaffected.
179
+
180
+ ## Configuration
181
+
182
+ Set process-wide defaults:
183
+
184
+ ```ruby
185
+ ValidEmailChecker.configure do |config|
186
+ config.from_email = "verify@example.com" # MAIL FROM: — use a domain you own
187
+ config.hello_name = "example.com" # EHLO — ditto, with matching rDNS
188
+ config.timeout = 15 # seconds, per address
189
+ config.smtp_port = 587 # if 25 is blocked
190
+ end
191
+ ```
192
+
193
+ Any setting can be overridden per call:
194
+
195
+ ```ruby
196
+ ValidEmailChecker.check("someone@example.com", timeout: 5, retries: 2)
197
+ ```
198
+
199
+ | Option | Default | Notes |
200
+ | ------ | ------- | ----- |
201
+ | `from_email` | `"reacher@gmail.com"` | Address for `MAIL FROM:`. Some servers check it. |
202
+ | `hello_name` | `"gmail.com"` | Name for `EHLO`. Should be a domain you own. |
203
+ | `smtp_port` | `25` | Try `587` or `2525` where 25 is blocked. |
204
+ | `smtp_timeout` | `nil` | Seconds, per SMTP connection. |
205
+ | `retries` | `1` | Total SMTP connections. `2` can get past greylisting. |
206
+ | `timeout` | `30` | Seconds, wall-clock ceiling per address. `nil` to disable. |
207
+ | `concurrency` | `10` | For `check_many` only. |
208
+ | `proxy` | `nil` | `{host:, port:, username:, password:, timeout:}`, SOCKS5. |
209
+ | `check_gravatar` | `false` | Adds an HTTP round trip. |
210
+ | `haveibeenpwned_api_key` | `nil` | Enables the breach lookup. |
211
+ | `yahoo` | `:api` | `:api`, `:smtp` or `:headless`. |
212
+ | `hotmail_b2c` | `:smtp` | `:smtp` or `:headless`. |
213
+ | `webdriver_addr` | `nil` | Required by `:headless`, e.g. `"http://localhost:9515"`. |
214
+ | `backend_name` | `"valid_email_checker"` | Recorded in the result's debug section. |
215
+
216
+ Bad options raise `ValidEmailChecker::ConfigurationError` before any network
217
+ access happens.
218
+
219
+ ### Timeouts
220
+
221
+ `timeout` is a wall-clock ceiling per address, defaulting to 30 seconds.
222
+ Upstream has no such ceiling; this gem adds one so a single unresponsive mail
223
+ server cannot block a caller indefinitely.
224
+
225
+ A timed-out result comes back as `:unknown` with `timed_out?` true. It carries
226
+ no partial detail — no MX records, no SMTP findings — because the verification
227
+ is abandoned wholesale. If you want the MX result independently of the SMTP
228
+ stage, that is a good reason to keep the ceiling generous.
229
+
230
+ ### Proxies
231
+
232
+ Where port 25 is blocked, route through a SOCKS5 proxy:
233
+
234
+ ```ruby
235
+ ValidEmailChecker.configure do |config|
236
+ config.proxy = { host: "proxy.example.com", port: 1080, username: "u", password: "p" }
237
+ end
238
+ ```
239
+
240
+ ### Yahoo and Hotmail
241
+
242
+ Two providers need special handling, and this gem's defaults differ from
243
+ upstream's so that a fresh install works with nothing else running: Yahoo
244
+ defaults to `:api` and consumer Hotmail/Outlook to `:smtp`. Upstream defaults
245
+ both to `:headless`, which drives the provider's password-recovery page through
246
+ a WebDriver. To opt into that, run `chromedriver` and point the gem at it:
247
+
248
+ ```ruby
249
+ ValidEmailChecker.configure do |config|
250
+ config.yahoo = :headless
251
+ config.hotmail_b2c = :headless
252
+ config.webdriver_addr = "http://localhost:9515"
253
+ end
254
+ ```
255
+
256
+ ## Command line
257
+
258
+ ```console
259
+ $ valid_email_checker someone@gmail.com info@example.com
260
+ safe someone@gmail.com
261
+ risky info@example.com (role account, catch-all)
262
+
263
+ $ valid_email_checker --syntax-only someone@gmail.co
264
+ valid someone@gmail.co (suggestion: someone@gmail.com)
265
+
266
+ $ valid_email_checker --json --timeout 10 someone@gmail.com
267
+ [{"input": "someone@gmail.com", "is_reachable": "safe", ...}]
268
+
269
+ $ valid_email_checker --stdin --concurrency 20 < addresses.txt
270
+ ```
271
+
272
+ Exit status is `0` when every address came back `:safe`, `1` when any was not,
273
+ and `2` for a usage or configuration error. Run `--help` for all options.
274
+
275
+ ## The raw upstream result
276
+
277
+ Every field the crate produces is available verbatim, under `:syntax`, `:mx`,
278
+ `:smtp`, `:misc` and `:debug`:
279
+
280
+ ```ruby
281
+ result.to_h[:smtp] # => {can_connect_smtp: true, is_catch_all: false, ...}
282
+ result.to_h[:debug] # => {backend_name: "...", start_time: "...", ...}
283
+ ```
284
+
285
+ Note that when a stage fails, the crate replaces that section with
286
+ `{error: {...}}` instead of the fields it normally holds. The predicate methods
287
+ on `Result` already tolerate this; code reading `to_h` directly should too.
288
+
289
+ ## Development
290
+
291
+ ```console
292
+ $ bundle install
293
+ $ bundle exec rake compile # build the extension
294
+ $ bundle exec rake test # offline tests only
295
+ $ bundle exec rake test_network # also the tests needing DNS and port 25
296
+ ```
297
+
298
+ Tests that reach the network are skipped by default, since most networks block
299
+ outbound port 25 — including, most likely, yours.
300
+
301
+ To update the embedded crate to upstream's latest commit (or a tag, e.g.
302
+ `rake "vendor:update[v0.11.8]"`):
303
+
304
+ ```console
305
+ $ bundle exec rake vendor:update
306
+ ```
307
+
308
+ This replaces `ext/valid_email_checker/vendor/check-if-email-exists/` with a
309
+ fresh copy of upstream's `core` package, re-applies the two `Cargo.toml` changes
310
+ noted in [NOTICE.md](NOTICE.md), updates
311
+ `ext/valid_email_checker/vendor/UPSTREAM.json` and refreshes `Cargo.lock`.
312
+
313
+ ## License
314
+
315
+ AGPL-3.0-only. See [LICENSE.txt](LICENSE.txt) and, importantly,
316
+ [NOTICE.md](NOTICE.md).
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "optparse"
5
+ require "valid_email_checker"
6
+
7
+ options = {}
8
+ format = :text
9
+ syntax_only = false
10
+
11
+ parser = OptionParser.new do |opts|
12
+ opts.banner = <<~BANNER
13
+ Check whether email addresses exist, without sending mail.
14
+
15
+ Usage: valid_email_checker [options] EMAIL [EMAIL...]
16
+ valid_email_checker [options] --stdin < addresses.txt
17
+
18
+ BANNER
19
+
20
+ opts.on("--stdin", "Read addresses from stdin, one per line") do
21
+ options[:stdin] = true
22
+ end
23
+
24
+ opts.on("--syntax-only", "Only parse the addresses; no network access") do
25
+ syntax_only = true
26
+ end
27
+
28
+ opts.on("--json", "Emit the full upstream result as JSON") { format = :json }
29
+
30
+ opts.on("--from-email EMAIL", "Address for the MAIL FROM: command") do |value|
31
+ options[:from_email] = value
32
+ end
33
+
34
+ opts.on("--hello-name NAME", "Name for the EHLO command; use a domain you own") do |value|
35
+ options[:hello_name] = value
36
+ end
37
+
38
+ opts.on("--smtp-port PORT", Integer, "SMTP port (default 25)") do |value|
39
+ options[:smtp_port] = value
40
+ end
41
+
42
+ opts.on("--timeout SECONDS", Float, "Per-address wall-clock budget (default 30)") do |value|
43
+ options[:timeout] = value
44
+ end
45
+
46
+ opts.on("--retries N", Integer, "Total SMTP connections per address (default 1)") do |value|
47
+ options[:retries] = value
48
+ end
49
+
50
+ opts.on("--concurrency N", Integer, "Addresses to verify at once (default 10)") do |value|
51
+ options[:concurrency] = value
52
+ end
53
+
54
+ opts.on("--proxy HOST:PORT", "SOCKS5 proxy, needed where port 25 is blocked") do |value|
55
+ host, port = value.rpartition(":").values_at(0, 2)
56
+ if host.empty? || port.to_i.zero?
57
+ warn "error: --proxy expects HOST:PORT, got #{value.inspect}"
58
+ exit 2
59
+ end
60
+ options[:proxy] = { host: host, port: port.to_i }
61
+ end
62
+
63
+ opts.on("--gravatar", "Also look up a Gravatar for each address") do
64
+ options[:check_gravatar] = true
65
+ end
66
+
67
+ opts.on("-v", "--version", "Print versions and exit") do
68
+ upstream = ValidEmailChecker.upstream
69
+ puts "valid_email_checker #{ValidEmailChecker::VERSION}"
70
+ puts "#{upstream[:name]} #{upstream[:version]} (#{upstream[:commit][0, 12]})"
71
+ exit 0
72
+ end
73
+
74
+ opts.on("-h", "--help", "Print this message and exit") do
75
+ puts opts
76
+ exit 0
77
+ end
78
+ end
79
+
80
+ parser.parse!
81
+
82
+ emails = options.delete(:stdin) ? $stdin.read.split("\n") : ARGV
83
+ emails = emails.map(&:strip).reject(&:empty?)
84
+
85
+ if emails.empty?
86
+ warn parser.help
87
+ exit 2
88
+ end
89
+
90
+ # Exit status is meant for scripting: 0 only when every address came back
91
+ # clearly good, 1 when any was invalid or could not be determined.
92
+ def report_text(result)
93
+ marker = case result.reachable
94
+ when :safe then "safe "
95
+ when :risky then "risky "
96
+ when :invalid then "invalid"
97
+ else "unknown"
98
+ end
99
+
100
+ notes = []
101
+ notes << "catch-all" if result.catch_all?
102
+ notes << "disposable" if result.disposable?
103
+ notes << "role account" if result.role_account?
104
+ notes << "full inbox" if result.full_inbox?
105
+ notes << "disabled" if result.disabled?
106
+ notes << "timed out" if result.timed_out?
107
+ notes << "suggestion: #{result.syntax.suggestion}" if result.syntax.suggestion?
108
+ if result.error? && !result.timed_out?
109
+ notes << "#{result.error[:type]}: #{result.error[:message]}".lines.first.to_s.strip
110
+ end
111
+
112
+ line = "#{marker} #{result.email}"
113
+ line += " (#{notes.join(", ")})" unless notes.empty?
114
+ puts line
115
+ end
116
+
117
+ begin
118
+ if syntax_only
119
+ results = emails.map { |email| [email, ValidEmailChecker.syntax(email)] }
120
+
121
+ if format == :json
122
+ puts JSON.pretty_generate(results.map { |email, s| { input: email }.merge(s.to_h) })
123
+ else
124
+ results.each do |email, s|
125
+ suffix = s.suggestion? ? " (suggestion: #{s.suggestion})" : ""
126
+ puts "#{s.valid? ? "valid " : "invalid"} #{email}#{suffix}"
127
+ end
128
+ end
129
+
130
+ exit results.all? { |_, s| s.valid? } ? 0 : 1
131
+ end
132
+
133
+ results = ValidEmailChecker.check_many(emails, **options)
134
+
135
+ if format == :json
136
+ puts JSON.pretty_generate(results.map(&:to_h))
137
+ else
138
+ results.each { |result| report_text(result) }
139
+ end
140
+
141
+ exit results.all?(&:safe?) ? 0 : 1
142
+ rescue ValidEmailChecker::ConfigurationError, ArgumentError => e
143
+ warn "error: #{e.message}"
144
+ exit 2
145
+ rescue Interrupt
146
+ warn "interrupted"
147
+ exit 130
148
+ end
@@ -0,0 +1,27 @@
1
+ [package]
2
+ name = "valid_email_checker"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ rust-version = "1.75"
6
+ publish = false
7
+ license = "AGPL-3.0-only"
8
+ description = "Ruby native extension wrapping the embedded check-if-email-exists crate"
9
+
10
+ [lib]
11
+ name = "valid_email_checker"
12
+ crate-type = ["cdylib"]
13
+
14
+ [dependencies]
15
+ magnus = "0.8"
16
+ rb-sys = "0.9"
17
+ serde = { version = "1.0", features = ["derive"] }
18
+ serde_json = "1.0"
19
+ futures = "0.3"
20
+ tokio = { version = "1", features = ["rt-multi-thread", "time"] }
21
+
22
+ # The upstream project, embedded in this gem under vendor/. Pinned by the
23
+ # commit recorded in vendor/UPSTREAM.json; refresh with `rake vendor:update`.
24
+ check-if-email-exists = { path = "vendor/check-if-email-exists" }
25
+
26
+ # Note: build profiles live in the workspace root Cargo.toml, not here --
27
+ # cargo ignores `[profile.*]` in a workspace member.
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+ require "rb_sys/mkmf"
5
+
6
+ # Compiling the embedded crate pulls in tokio, rustls and reqwest, so a cold
7
+ # build takes a few minutes. Fail with a clear message rather than a cryptic
8
+ # one if the toolchain is missing.
9
+ unless find_executable("cargo")
10
+ abort <<~MSG
11
+
12
+ valid_email_checker needs a Rust toolchain to build.
13
+
14
+ It embeds the `check-if-email-exists` crate and compiles it during
15
+ installation. Install Rust with:
16
+
17
+ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
18
+
19
+ or see https://rustup.rs, then reinstall this gem.
20
+
21
+ MSG
22
+ end
23
+
24
+ create_rust_makefile("valid_email_checker/valid_email_checker") do |r|
25
+ r.profile = ENV.fetch("VALID_EMAIL_CHECKER_PROFILE", "release").to_sym
26
+ end