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