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,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ require_relative "valid_email_checker/version"
6
+ require_relative "valid_email_checker/errors"
7
+ require_relative "valid_email_checker/configuration"
8
+ require_relative "valid_email_checker/syntax"
9
+ require_relative "valid_email_checker/result"
10
+
11
+ # The compiled extension. rake-compiler installs a per-version copy when
12
+ # several Ruby ABIs share a checkout, so prefer that when it is present.
13
+ begin
14
+ ruby_abi = RUBY_VERSION[/\d+\.\d+/]
15
+ require_relative "valid_email_checker/#{ruby_abi}/valid_email_checker"
16
+ rescue LoadError
17
+ require_relative "valid_email_checker/valid_email_checker"
18
+ end
19
+
20
+ # Check whether an email address really exists, without sending mail.
21
+ #
22
+ # This gem embeds the Rust crate
23
+ # {https://github.com/reacherhq/check-if-email-exists check-if-email-exists}
24
+ # and calls it in-process. A full check validates the syntax, resolves the
25
+ # domain's MX records, opens an SMTP conversation with the mail exchanger and
26
+ # asks about the recipient without ever completing a message, then reports
27
+ # whether the address is disposable, role-based or catch-all.
28
+ #
29
+ # ValidEmailChecker.check("someone@gmail.com").safe? # => true
30
+ #
31
+ # == A note on what "exists" can mean
32
+ #
33
+ # SMTP verification is a best-effort signal, not a guarantee. Results come back
34
+ # as +:unknown+ more often than people expect, for reasons that have nothing to
35
+ # do with the address:
36
+ #
37
+ # * Most residential and cloud networks block outbound port 25. Without a
38
+ # SOCKS5 +proxy+, or a host that permits port 25, expect +:unknown+.
39
+ # * Mail exchangers rate-limit, greylist and blacklist aggressively. A server
40
+ # you are unknown to may simply refuse to answer.
41
+ # * Catch-all domains accept every recipient, so an individual address there
42
+ # can never be confirmed. Those come back +:risky+.
43
+ #
44
+ # Set +from_email+ and +hello_name+ to a domain you actually control, with
45
+ # matching forward and reverse DNS, for markedly better results.
46
+ module ValidEmailChecker
47
+ # Whether this platform's network stack deadlocks when a process verifies an
48
+ # address after forking from a parent that already had.
49
+ #
50
+ # True on macOS, whose Network.framework is not fork-safe. See
51
+ # {ForkAfterVerificationError} and the README's "Forking servers" section.
52
+ FORK_UNSAFE_NETWORK_STACK = RUBY_PLATFORM.include?("darwin")
53
+
54
+ class << self
55
+ # Process-wide defaults. See {Configuration} for every setting.
56
+ #
57
+ # @return [ValidEmailChecker::Configuration]
58
+ def configuration
59
+ @configuration ||= Configuration.new
60
+ end
61
+
62
+ # Set process-wide defaults.
63
+ #
64
+ # ValidEmailChecker.configure do |config|
65
+ # config.from_email = "verify@example.com"
66
+ # config.hello_name = "example.com"
67
+ # config.timeout = 15
68
+ # end
69
+ #
70
+ # @yieldparam config [ValidEmailChecker::Configuration]
71
+ # @return [ValidEmailChecker::Configuration]
72
+ def configure
73
+ yield configuration if block_given?
74
+ configuration.validate!
75
+ end
76
+
77
+ # Restore the default configuration. Mainly useful in tests.
78
+ #
79
+ # @return [ValidEmailChecker::Configuration]
80
+ def reset_configuration!
81
+ @configuration = Configuration.new
82
+ end
83
+
84
+ # Verify one address.
85
+ #
86
+ # Performs network I/O and takes seconds. The GVL is released for the
87
+ # duration, so other Ruby threads keep running.
88
+ #
89
+ # @param email [String] the address to check
90
+ # @param options [Hash] per-call overrides; any {Configuration} setting
91
+ # @return [ValidEmailChecker::Result]
92
+ # @raise [ArgumentError] if +email+ is not a non-empty String
93
+ # @raise [ValidEmailChecker::ConfigurationError] if the options are unusable
94
+ def check(email, **options)
95
+ address = coerce_email(email)
96
+ payload = configuration.merge(options).to_native_payload
97
+ check_fork_safety!
98
+
99
+ Result.from_json(Native.check(address, JSON.generate(payload)))
100
+ end
101
+
102
+ # Verify several addresses, several at a time.
103
+ #
104
+ # This is the reason to embed the crate rather than shell out to it: the
105
+ # whole batch runs on one async runtime under a single GVL release, so
106
+ # +concurrency+ verifications are in flight at once. Results come back in
107
+ # the same order as +emails+.
108
+ #
109
+ # results = ValidEmailChecker.check_many(addresses, concurrency: 20)
110
+ # results.select(&:safe?).map(&:email)
111
+ #
112
+ # Be careful raising +concurrency+ when the addresses share a domain: you
113
+ # are opening that many SMTP connections to one mail exchanger, which is a
114
+ # good way to get rate-limited or blacklisted.
115
+ #
116
+ # @param emails [Array<String>] the addresses to check
117
+ # @param options [Hash] per-call overrides; any {Configuration} setting,
118
+ # including +concurrency+
119
+ # @return [Array<ValidEmailChecker::Result>] in the order given
120
+ def check_many(emails, **options)
121
+ list = Array(emails).map { |email| coerce_email(email) }
122
+ return [] if list.empty?
123
+
124
+ config = configuration.merge(options)
125
+ payload = config.to_native_payload
126
+ concurrency = Integer(config.concurrency)
127
+
128
+ if concurrency < 1
129
+ raise ConfigurationError, "concurrency must be at least 1, got #{config.concurrency.inspect}"
130
+ end
131
+
132
+ check_fork_safety!
133
+
134
+ json = Native.check_many(list, JSON.generate(payload), concurrency)
135
+ JSON.parse(json, symbolize_names: true).map { |data| Result.new(data) }
136
+ end
137
+
138
+ # Parse an address without touching the network.
139
+ #
140
+ # Instant and side-effect free, so it is safe to call on user input inline,
141
+ # including in a request cycle or a model validation.
142
+ #
143
+ # ValidEmailChecker.syntax("someone@gmail.co").suggestion
144
+ # # => "someone@gmail.com"
145
+ #
146
+ # Note that a suggestion is only offered for an address that parses.
147
+ # Well-known typo domains such as "gmial.com" are on upstream's blocklist
148
+ # and come back as simply invalid, with no domain and no suggestion.
149
+ #
150
+ # @param email [String]
151
+ # @return [ValidEmailChecker::Syntax]
152
+ def syntax(email)
153
+ Syntax.new(JSON.parse(Native.check_syntax(coerce_email(email)), symbolize_names: true))
154
+ end
155
+
156
+ # Whether an address is well-formed. No network access.
157
+ #
158
+ # @param email [String]
159
+ # @return [Boolean]
160
+ def syntax_valid?(email)
161
+ syntax(email).valid?
162
+ end
163
+
164
+ # Which version of the embedded crate this gem was built against.
165
+ #
166
+ # @return [Hash] with +:name+, +:version+, +:commit+, +:repository+ and
167
+ # +:license+ keys
168
+ def upstream
169
+ @upstream ||= JSON.parse(Native.upstream, symbolize_names: true).freeze
170
+ end
171
+
172
+ private
173
+
174
+ # Apple's Network.framework is not fork-safe: once a process has resolved
175
+ # and connected, a child that inherits that state deadlocks inside the
176
+ # framework on its own first connection, ignoring our timeout entirely.
177
+ #
178
+ # We cannot fix the inherited state, so we refuse the call instead of
179
+ # hanging. The check is limited to platforms where the deadlock is
180
+ # demonstrable; see {FORK_UNSAFE_NETWORK_STACK}. Set
181
+ # VALID_EMAIL_CHECKER_ALLOW_FORK=1 to bypass it and take the risk.
182
+ def check_fork_safety!
183
+ pid = Process.pid
184
+
185
+ if FORK_UNSAFE_NETWORK_STACK &&
186
+ @verified_in_pid &&
187
+ @verified_in_pid != pid &&
188
+ ENV["VALID_EMAIL_CHECKER_ALLOW_FORK"] != "1"
189
+ raise ForkAfterVerificationError, <<~MSG.strip
190
+ Refusing to verify in process #{pid}: process #{@verified_in_pid} already
191
+ verified an address before this one forked from it.
192
+
193
+ macOS's Network.framework is not fork-safe, and continuing would deadlock
194
+ here indefinitely rather than time out.
195
+
196
+ Verify only in child processes: load this gem at boot if you like, but do
197
+ not call check/check_many in a parent that will later fork (for example,
198
+ during Puma's or Unicorn's preload phase). Set
199
+ VALID_EMAIL_CHECKER_ALLOW_FORK=1 to bypass this check.
200
+ MSG
201
+ end
202
+
203
+ @verified_in_pid = pid
204
+ end
205
+
206
+ def coerce_email(email)
207
+ unless email.is_a?(String)
208
+ raise ArgumentError, "expected an email address as a String, got #{email.class}"
209
+ end
210
+
211
+ stripped = email.strip
212
+ raise ArgumentError, "expected an email address, got an empty String" if stripped.empty?
213
+
214
+ stripped
215
+ end
216
+ end
217
+ end
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: valid_email_checker
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - J. Callahan
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rb_sys
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.9'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.9'
26
+ description: |
27
+ valid_email_checker embeds the Rust crate check-if-email-exists and exposes
28
+ it to Ruby as a native extension. It validates syntax, looks up MX records,
29
+ holds an SMTP conversation with the recipient's mail exchanger, and reports
30
+ disposable, role-based and catch-all addresses -- all in-process, with no
31
+ HTTP service to run and no mail actually sent.
32
+ email:
33
+ - jcallahan@acm.org
34
+ executables:
35
+ - valid_email_checker
36
+ extensions:
37
+ - ext/valid_email_checker/extconf.rb
38
+ extra_rdoc_files: []
39
+ files:
40
+ - CHANGELOG.md
41
+ - Cargo.lock
42
+ - Cargo.toml
43
+ - LICENSE.txt
44
+ - NOTICE.md
45
+ - README.md
46
+ - exe/valid_email_checker
47
+ - ext/valid_email_checker/Cargo.toml
48
+ - ext/valid_email_checker/extconf.rb
49
+ - ext/valid_email_checker/src/lib.rs
50
+ - ext/valid_email_checker/src/nogvl.rs
51
+ - ext/valid_email_checker/vendor/UPSTREAM.json
52
+ - ext/valid_email_checker/vendor/check-if-email-exists/Cargo.toml
53
+ - ext/valid_email_checker/vendor/check-if-email-exists/LICENSE.AGPL
54
+ - ext/valid_email_checker/vendor/check-if-email-exists/LICENSE.md
55
+ - ext/valid_email_checker/vendor/check-if-email-exists/README.md
56
+ - ext/valid_email_checker/vendor/check-if-email-exists/src/haveibeenpwned.rs
57
+ - ext/valid_email_checker/vendor/check-if-email-exists/src/lib.rs
58
+ - ext/valid_email_checker/vendor/check-if-email-exists/src/misc/b2c.txt
59
+ - ext/valid_email_checker/vendor/check-if-email-exists/src/misc/gravatar.rs
60
+ - ext/valid_email_checker/vendor/check-if-email-exists/src/misc/mod.rs
61
+ - ext/valid_email_checker/vendor/check-if-email-exists/src/misc/roles.txt
62
+ - ext/valid_email_checker/vendor/check-if-email-exists/src/mx/mod.rs
63
+ - ext/valid_email_checker/vendor/check-if-email-exists/src/rules.json
64
+ - ext/valid_email_checker/vendor/check-if-email-exists/src/rules.rs
65
+ - ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/connect.rs
66
+ - ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/error.rs
67
+ - ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/gmail.rs
68
+ - ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/headless.rs
69
+ - ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/http_api.rs
70
+ - ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/mod.rs
71
+ - ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/outlook/headless.rs
72
+ - ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/outlook/microsoft365.rs
73
+ - ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/outlook/mod.rs
74
+ - ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/parser.rs
75
+ - ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/verif_method.rs
76
+ - ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/yahoo/api.rs
77
+ - ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/yahoo/headless.rs
78
+ - ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/yahoo/mod.rs
79
+ - ext/valid_email_checker/vendor/check-if-email-exists/src/syntax/mod.rs
80
+ - ext/valid_email_checker/vendor/check-if-email-exists/src/syntax/normalize.rs
81
+ - ext/valid_email_checker/vendor/check-if-email-exists/src/util/input_output.rs
82
+ - ext/valid_email_checker/vendor/check-if-email-exists/src/util/mod.rs
83
+ - ext/valid_email_checker/vendor/check-if-email-exists/src/util/sentry.rs
84
+ - ext/valid_email_checker/vendor/check-if-email-exists/src/util/ser_with_display.rs
85
+ - lib/valid_email_checker.rb
86
+ - lib/valid_email_checker/configuration.rb
87
+ - lib/valid_email_checker/errors.rb
88
+ - lib/valid_email_checker/result.rb
89
+ - lib/valid_email_checker/syntax.rb
90
+ - lib/valid_email_checker/version.rb
91
+ homepage: https://github.com/johncallahan/valid_email_checker
92
+ licenses:
93
+ - AGPL-3.0-only
94
+ metadata:
95
+ homepage_uri: https://github.com/johncallahan/valid_email_checker
96
+ source_code_uri: https://github.com/johncallahan/valid_email_checker
97
+ changelog_uri: https://github.com/johncallahan/valid_email_checker/blob/main/CHANGELOG.md
98
+ bug_tracker_uri: https://github.com/johncallahan/valid_email_checker/issues
99
+ upstream_project_uri: https://github.com/reacherhq/check-if-email-exists
100
+ rubygems_mfa_required: 'true'
101
+ rdoc_options: []
102
+ require_paths:
103
+ - lib
104
+ required_ruby_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: 3.1.0
109
+ required_rubygems_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: 3.3.11
114
+ requirements: []
115
+ rubygems_version: 3.6.9
116
+ specification_version: 4
117
+ summary: Check whether an email address really exists, without sending mail.
118
+ test_files: []