valid_email_checker 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +24 -0
- data/Cargo.lock +2940 -0
- data/Cargo.toml +22 -0
- data/LICENSE.txt +661 -0
- data/NOTICE.md +56 -0
- data/README.md +316 -0
- data/exe/valid_email_checker +148 -0
- data/ext/valid_email_checker/Cargo.toml +27 -0
- data/ext/valid_email_checker/extconf.rb +26 -0
- data/ext/valid_email_checker/src/lib.rs +371 -0
- data/ext/valid_email_checker/src/nogvl.rs +64 -0
- data/ext/valid_email_checker/vendor/UPSTREAM.json +9 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/Cargo.toml +52 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/LICENSE.AGPL +661 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/LICENSE.md +11 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/README.md +175 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/haveibeenpwned.rs +70 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/lib.rs +281 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/misc/b2c.txt +96640 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/misc/gravatar.rs +60 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/misc/mod.rs +124 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/misc/roles.txt +944 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/mx/mod.rs +165 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/rules.json +28 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/rules.rs +105 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/connect.rs +396 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/error.rs +144 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/gmail.rs +99 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/headless.rs +82 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/http_api.rs +27 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/mod.rs +234 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/outlook/headless.rs +181 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/outlook/microsoft365.rs +109 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/outlook/mod.rs +2 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/parser.rs +291 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/verif_method.rs +531 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/yahoo/api.rs +174 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/yahoo/headless.rs +188 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/smtp/yahoo/mod.rs +62 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/syntax/mod.rs +199 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/syntax/normalize.rs +70 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/util/input_output.rs +353 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/util/mod.rs +20 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/util/sentry.rs +173 -0
- data/ext/valid_email_checker/vendor/check-if-email-exists/src/util/ser_with_display.rs +28 -0
- data/lib/valid_email_checker/configuration.rb +216 -0
- data/lib/valid_email_checker/errors.rb +32 -0
- data/lib/valid_email_checker/result.rb +241 -0
- data/lib/valid_email_checker/syntax.rb +72 -0
- data/lib/valid_email_checker/version.rb +5 -0
- data/lib/valid_email_checker.rb +217 -0
- metadata +118 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ValidEmailChecker
|
|
4
|
+
# Verification settings.
|
|
5
|
+
#
|
|
6
|
+
# Set process-wide defaults with {ValidEmailChecker.configure}, and override
|
|
7
|
+
# any of them per call by passing the same keys to {ValidEmailChecker.check}.
|
|
8
|
+
#
|
|
9
|
+
# Timeouts are expressed in seconds here, the way Ruby's own APIs do; they
|
|
10
|
+
# are converted to milliseconds on the way into Rust.
|
|
11
|
+
class Configuration
|
|
12
|
+
# Strategies the native extension understands, per provider.
|
|
13
|
+
YAHOO_METHODS = %i[api headless smtp].freeze
|
|
14
|
+
HOTMAIL_B2C_METHODS = %i[smtp headless].freeze
|
|
15
|
+
|
|
16
|
+
# Address used in the `MAIL FROM:` command. Upstream's default is an
|
|
17
|
+
# unused address owned by the Reacher project; set this to one you control,
|
|
18
|
+
# since some mail exchangers check it.
|
|
19
|
+
DEFAULT_FROM_EMAIL = "reacher@gmail.com"
|
|
20
|
+
|
|
21
|
+
# Name used in the `EHLO` command. Should be a domain you own, with
|
|
22
|
+
# matching forward and reverse DNS, or many servers will refuse to talk.
|
|
23
|
+
DEFAULT_HELLO_NAME = "gmail.com"
|
|
24
|
+
|
|
25
|
+
# Wall-clock ceiling for verifying one address. Upstream has no overall
|
|
26
|
+
# timeout; we default to one so a single unresponsive mail server cannot
|
|
27
|
+
# block a caller forever. Set to +nil+ to remove the ceiling.
|
|
28
|
+
DEFAULT_TIMEOUT = 30
|
|
29
|
+
|
|
30
|
+
# How many addresses {ValidEmailChecker.check_many} verifies at once.
|
|
31
|
+
DEFAULT_CONCURRENCY = 10
|
|
32
|
+
|
|
33
|
+
# Address used in the `MAIL FROM:` SMTP command.
|
|
34
|
+
attr_accessor :from_email
|
|
35
|
+
# Name used in the `EHLO` SMTP command.
|
|
36
|
+
attr_accessor :hello_name
|
|
37
|
+
# Port to connect to. Usually 25; try 587 or 2525 if 25 is blocked.
|
|
38
|
+
attr_accessor :smtp_port
|
|
39
|
+
# Timeout for one SMTP connection, in seconds. +nil+ for none.
|
|
40
|
+
attr_accessor :smtp_timeout
|
|
41
|
+
# Total number of SMTP connections to attempt. 2 can get past greylisting.
|
|
42
|
+
attr_accessor :retries
|
|
43
|
+
# Whether to look up a Gravatar for the address. Adds a HTTP round trip.
|
|
44
|
+
attr_accessor :check_gravatar
|
|
45
|
+
# API key enabling the HaveIBeenPwned breach lookup.
|
|
46
|
+
attr_accessor :haveibeenpwned_api_key
|
|
47
|
+
# SOCKS5 proxy, as a Hash of +:host+, +:port+ and optionally +:username+,
|
|
48
|
+
# +:password+, +:timeout+ (seconds).
|
|
49
|
+
attr_accessor :proxy
|
|
50
|
+
# WebDriver endpoint, used only by the +:headless+ strategies.
|
|
51
|
+
attr_accessor :webdriver_addr
|
|
52
|
+
# Identifier recorded in the result's debug section.
|
|
53
|
+
attr_accessor :backend_name
|
|
54
|
+
# Wall-clock ceiling per address, in seconds. +nil+ for none.
|
|
55
|
+
attr_accessor :timeout
|
|
56
|
+
# Strategy for Yahoo addresses: +:api+, +:headless+ or +:smtp+.
|
|
57
|
+
attr_accessor :yahoo
|
|
58
|
+
# Strategy for consumer Hotmail/Outlook addresses: +:smtp+ or +:headless+.
|
|
59
|
+
attr_accessor :hotmail_b2c
|
|
60
|
+
# Default concurrency for {ValidEmailChecker.check_many}.
|
|
61
|
+
attr_accessor :concurrency
|
|
62
|
+
|
|
63
|
+
# Keys accepted by {#initialize} and by per-call overrides.
|
|
64
|
+
KEYS = %i[
|
|
65
|
+
from_email hello_name smtp_port smtp_timeout retries check_gravatar
|
|
66
|
+
haveibeenpwned_api_key proxy webdriver_addr backend_name timeout
|
|
67
|
+
yahoo hotmail_b2c concurrency
|
|
68
|
+
].freeze
|
|
69
|
+
|
|
70
|
+
def initialize(**overrides)
|
|
71
|
+
@from_email = DEFAULT_FROM_EMAIL
|
|
72
|
+
@hello_name = DEFAULT_HELLO_NAME
|
|
73
|
+
@smtp_port = 25
|
|
74
|
+
@smtp_timeout = nil
|
|
75
|
+
@retries = 1
|
|
76
|
+
@check_gravatar = false
|
|
77
|
+
@haveibeenpwned_api_key = nil
|
|
78
|
+
@proxy = nil
|
|
79
|
+
@webdriver_addr = nil
|
|
80
|
+
@backend_name = "valid_email_checker"
|
|
81
|
+
@timeout = DEFAULT_TIMEOUT
|
|
82
|
+
# Defaults that differ from upstream so the gem works out of the box with
|
|
83
|
+
# no WebDriver process running. Pass :headless to match upstream.
|
|
84
|
+
@yahoo = :api
|
|
85
|
+
@hotmail_b2c = :smtp
|
|
86
|
+
@concurrency = DEFAULT_CONCURRENCY
|
|
87
|
+
|
|
88
|
+
apply(overrides)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# A copy of this configuration with +overrides+ applied. Used for per-call
|
|
92
|
+
# options, so that a call never mutates the global configuration.
|
|
93
|
+
def merge(overrides)
|
|
94
|
+
return self if overrides.nil? || overrides.empty?
|
|
95
|
+
|
|
96
|
+
dup.tap { |config| config.send(:apply, overrides) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def to_h
|
|
100
|
+
KEYS.to_h { |key| [key, public_send(key)] }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# The payload handed to the native extension.
|
|
104
|
+
#
|
|
105
|
+
# Deliberately excludes +concurrency+, which is a Ruby-side concern: the
|
|
106
|
+
# Rust options struct rejects unknown fields, which is what catches typos
|
|
107
|
+
# in caller-supplied option names.
|
|
108
|
+
def to_native_payload
|
|
109
|
+
validate!
|
|
110
|
+
|
|
111
|
+
{
|
|
112
|
+
from_email: from_email,
|
|
113
|
+
hello_name: hello_name,
|
|
114
|
+
smtp_port: smtp_port,
|
|
115
|
+
smtp_timeout_ms: seconds_to_ms(smtp_timeout),
|
|
116
|
+
retries: retries,
|
|
117
|
+
check_gravatar: check_gravatar,
|
|
118
|
+
haveibeenpwned_api_key: haveibeenpwned_api_key,
|
|
119
|
+
proxy: proxy_payload,
|
|
120
|
+
webdriver_addr: webdriver_addr,
|
|
121
|
+
backend_name: backend_name,
|
|
122
|
+
timeout_ms: seconds_to_ms(timeout),
|
|
123
|
+
yahoo: yahoo&.to_sym,
|
|
124
|
+
hotmail_b2c: hotmail_b2c&.to_sym
|
|
125
|
+
}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def validate!
|
|
129
|
+
unless yahoo.nil? || YAHOO_METHODS.include?(yahoo.to_sym)
|
|
130
|
+
raise ConfigurationError,
|
|
131
|
+
"yahoo must be one of #{YAHOO_METHODS.inspect}, got #{yahoo.inspect}"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
unless hotmail_b2c.nil? || HOTMAIL_B2C_METHODS.include?(hotmail_b2c.to_sym)
|
|
135
|
+
raise ConfigurationError,
|
|
136
|
+
"hotmail_b2c must be one of #{HOTMAIL_B2C_METHODS.inspect}, got #{hotmail_b2c.inspect}"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
if (yahoo&.to_sym == :headless || hotmail_b2c&.to_sym == :headless) && webdriver_addr.nil?
|
|
140
|
+
raise ConfigurationError,
|
|
141
|
+
"the :headless strategy needs a running WebDriver; set webdriver_addr " \
|
|
142
|
+
"(commonly \"http://localhost:9515\" for chromedriver)"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
validate_proxy!
|
|
146
|
+
|
|
147
|
+
[[:timeout, timeout], [:smtp_timeout, smtp_timeout]].each do |name, value|
|
|
148
|
+
next if value.nil?
|
|
149
|
+
raise ConfigurationError, "#{name} must be positive, got #{value.inspect}" unless value.positive?
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
unless (1..65_535).cover?(smtp_port.to_i)
|
|
153
|
+
raise ConfigurationError, "smtp_port must be between 1 and 65535, got #{smtp_port.inspect}"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
raise ConfigurationError, "retries must be at least 1, got #{retries.inspect}" if retries.to_i < 1
|
|
157
|
+
|
|
158
|
+
self
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
private
|
|
162
|
+
|
|
163
|
+
def apply(overrides)
|
|
164
|
+
overrides.each do |key, value|
|
|
165
|
+
unless KEYS.include?(key.to_sym)
|
|
166
|
+
raise ConfigurationError,
|
|
167
|
+
"unknown option #{key.inspect}; valid options are #{KEYS.inspect}"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
public_send(:"#{key}=", value)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
self
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def validate_proxy!
|
|
177
|
+
return if proxy.nil?
|
|
178
|
+
|
|
179
|
+
unless proxy.respond_to?(:to_h)
|
|
180
|
+
raise ConfigurationError, "proxy must be a Hash of :host and :port, got #{proxy.inspect}"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
hash = proxy.to_h
|
|
184
|
+
allowed = %i[host port username password timeout]
|
|
185
|
+
unknown = hash.keys.map(&:to_sym) - allowed
|
|
186
|
+
unless unknown.empty?
|
|
187
|
+
raise ConfigurationError, "unknown proxy keys #{unknown.inspect}; valid keys are #{allowed.inspect}"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
host = hash[:host] || hash["host"]
|
|
191
|
+
port = hash[:port] || hash["port"]
|
|
192
|
+
raise ConfigurationError, "proxy requires a :host" if host.nil? || host.to_s.empty?
|
|
193
|
+
raise ConfigurationError, "proxy requires a :port" if port.nil?
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def proxy_payload
|
|
197
|
+
return nil if proxy.nil?
|
|
198
|
+
|
|
199
|
+
hash = proxy.to_h.transform_keys(&:to_sym)
|
|
200
|
+
|
|
201
|
+
{
|
|
202
|
+
host: hash[:host].to_s,
|
|
203
|
+
port: hash[:port].to_i,
|
|
204
|
+
username: hash[:username],
|
|
205
|
+
password: hash[:password],
|
|
206
|
+
timeout_ms: seconds_to_ms(hash[:timeout])
|
|
207
|
+
}
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def seconds_to_ms(seconds)
|
|
211
|
+
return nil if seconds.nil?
|
|
212
|
+
|
|
213
|
+
(Float(seconds) * 1000).round
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ValidEmailChecker
|
|
4
|
+
# Base class for errors specific to this gem.
|
|
5
|
+
#
|
|
6
|
+
# Note that bad arguments raise Ruby's own ::ArgumentError rather than
|
|
7
|
+
# something under this namespace, so that a plain `rescue ArgumentError`
|
|
8
|
+
# behaves the way it does everywhere else.
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
|
|
11
|
+
# Raised when options are not usable: an unknown provider strategy, a proxy
|
|
12
|
+
# missing a host, a non-positive timeout, and so on.
|
|
13
|
+
#
|
|
14
|
+
# Raised before any network access happens, so nothing has been sent when
|
|
15
|
+
# you see this.
|
|
16
|
+
class ConfigurationError < Error; end
|
|
17
|
+
|
|
18
|
+
# Raised on macOS when a process tries to verify an address after forking
|
|
19
|
+
# from a parent that had already verified one.
|
|
20
|
+
#
|
|
21
|
+
# Apple's Network.framework is not fork-safe. The first verification in a
|
|
22
|
+
# process initializes XPC and dispatch state for the system resolver, and a
|
|
23
|
+
# child inherits that state in a form it cannot use: its next connection
|
|
24
|
+
# attempt deadlocks inside the framework and never returns, not even when the
|
|
25
|
+
# configured timeout expires.
|
|
26
|
+
#
|
|
27
|
+
# We cannot repair the inherited state, so this is raised in place of a hang
|
|
28
|
+
# that would otherwise be indistinguishable from a very slow mail server.
|
|
29
|
+
# See the "Forking servers" section of the README for how to avoid it --
|
|
30
|
+
# in short, do not verify in a parent process before forking.
|
|
31
|
+
class ForkAfterVerificationError < Error; end
|
|
32
|
+
end
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ValidEmailChecker
|
|
4
|
+
# The outcome of verifying one address.
|
|
5
|
+
#
|
|
6
|
+
# This wraps the JSON document the embedded Rust crate produces. The
|
|
7
|
+
# predicate methods cover the questions callers usually have; anything not
|
|
8
|
+
# given a method is still reachable through {#to_h}, which holds the upstream
|
|
9
|
+
# structure verbatim under +:syntax+, +:mx+, +:smtp+, +:misc+ and +:debug+.
|
|
10
|
+
#
|
|
11
|
+
# result = ValidEmailChecker.check("someone@gmail.com")
|
|
12
|
+
# result.reachable # => :safe
|
|
13
|
+
# result.safe? # => true
|
|
14
|
+
# result.disposable? # => false
|
|
15
|
+
# result.to_h[:smtp] # => {can_connect_smtp: true, ...}
|
|
16
|
+
class Result
|
|
17
|
+
# The four verdicts the crate can reach, in decreasing order of confidence
|
|
18
|
+
# that mail will arrive.
|
|
19
|
+
REACHABILITY = %i[safe risky invalid unknown].freeze
|
|
20
|
+
|
|
21
|
+
# @return [Hash] the raw upstream result, with symbol keys
|
|
22
|
+
attr_reader :to_h
|
|
23
|
+
|
|
24
|
+
def initialize(data)
|
|
25
|
+
@to_h = data.freeze
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @return [ValidEmailChecker::Result]
|
|
29
|
+
def self.from_json(json)
|
|
30
|
+
new(JSON.parse(json, symbolize_names: true))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return [String] the address that was checked, as it was given
|
|
34
|
+
def email
|
|
35
|
+
to_h[:input].to_s
|
|
36
|
+
end
|
|
37
|
+
alias input email
|
|
38
|
+
|
|
39
|
+
# How confident the crate is that mail sent here would arrive.
|
|
40
|
+
#
|
|
41
|
+
# @return [Symbol] +:safe+, +:risky+, +:invalid+ or +:unknown+
|
|
42
|
+
def reachable
|
|
43
|
+
value = to_h[:is_reachable]
|
|
44
|
+
value.nil? ? :unknown : value.to_sym
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Mail should arrive, and the address has no quality problems.
|
|
48
|
+
def safe?
|
|
49
|
+
reachable == :safe
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The address exists but is flagged: catch-all, disposable, role-based, or
|
|
53
|
+
# a full inbox. Mail may arrive, or may bounce.
|
|
54
|
+
def risky?
|
|
55
|
+
reachable == :risky
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# The address does not exist or is malformed. Do not send here.
|
|
59
|
+
def invalid?
|
|
60
|
+
reachable == :invalid
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The mail server gave no usable answer. Common when port 25 is blocked
|
|
64
|
+
# outbound, the server greylists, or the IP is blacklisted -- check
|
|
65
|
+
# {#error} before concluding anything about the address.
|
|
66
|
+
def unknown?
|
|
67
|
+
reachable == :unknown
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Whether the address is safe to send to.
|
|
71
|
+
#
|
|
72
|
+
# This is deliberately strict: only +:safe+ counts. Treating +:risky+ or
|
|
73
|
+
# +:unknown+ as usable is a policy decision, so make it explicitly with
|
|
74
|
+
# {#reachable} rather than relying on a looser default here.
|
|
75
|
+
def valid?
|
|
76
|
+
safe?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @return [ValidEmailChecker::Syntax]
|
|
80
|
+
def syntax
|
|
81
|
+
@syntax ||= Syntax.new(to_h[:syntax])
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# --- SMTP findings -------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
# @return [Boolean] the mail exchanger accepted the recipient
|
|
87
|
+
def deliverable?
|
|
88
|
+
smtp[:is_deliverable] == true
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# @return [Boolean] we completed an SMTP conversation with the server
|
|
92
|
+
def can_connect_smtp?
|
|
93
|
+
smtp[:can_connect_smtp] == true
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# @return [Boolean] the domain accepts mail for every address, so an
|
|
97
|
+
# individual address cannot be confirmed to exist
|
|
98
|
+
def catch_all?
|
|
99
|
+
smtp[:is_catch_all] == true
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @return [Boolean] the mailbox is over quota
|
|
103
|
+
def full_inbox?
|
|
104
|
+
smtp[:has_full_inbox] == true
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# @return [Boolean] the provider has blocked or deactivated the mailbox
|
|
108
|
+
def disabled?
|
|
109
|
+
smtp[:is_disabled] == true
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# --- Domain and address metadata ----------------------------------------
|
|
113
|
+
|
|
114
|
+
# @return [Boolean] the domain has MX records
|
|
115
|
+
def accepts_mail?
|
|
116
|
+
mx[:accepts_mail] == true
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# @return [Array<String>] the mail exchangers found for the domain
|
|
120
|
+
def mx_records
|
|
121
|
+
Array(mx[:records])
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# @return [Boolean] the address belongs to a known throwaway-mail provider
|
|
125
|
+
def disposable?
|
|
126
|
+
misc[:is_disposable] == true
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# @return [Boolean] the address is a shared mailbox such as info@ or
|
|
130
|
+
# support@, rather than a person
|
|
131
|
+
def role_account?
|
|
132
|
+
misc[:is_role_account] == true
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# @return [Boolean] the address is a consumer rather than a business one
|
|
136
|
+
def b2c?
|
|
137
|
+
misc[:is_b2c] == true
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# @return [String, nil] the Gravatar image URL, when +check_gravatar+ was
|
|
141
|
+
# enabled and one exists
|
|
142
|
+
def gravatar_url
|
|
143
|
+
misc[:gravatar_url]
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Whether the address appears in a HaveIBeenPwned breach.
|
|
147
|
+
#
|
|
148
|
+
# @return [Boolean, nil] nil when no +haveibeenpwned_api_key+ was set, so
|
|
149
|
+
# the lookup never ran
|
|
150
|
+
def pwned?
|
|
151
|
+
misc[:haveibeenpwned]
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# --- Errors and timing ---------------------------------------------------
|
|
155
|
+
|
|
156
|
+
# The reason a stage could not complete, if any.
|
|
157
|
+
#
|
|
158
|
+
# Present mainly on +:unknown+ results. The +:type+ is the upstream error
|
|
159
|
+
# variant, or +"Timeout"+ when the call hit its wall-clock budget.
|
|
160
|
+
#
|
|
161
|
+
# @return [Hash, nil] with +:type+ and +:message+ keys
|
|
162
|
+
def error
|
|
163
|
+
to_h[:error] || smtp_error || mx_error || misc_error
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# @return [Boolean] whether anything went wrong during verification
|
|
167
|
+
def error?
|
|
168
|
+
!error.nil?
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# @return [Boolean] whether the wall-clock budget ran out. The address was
|
|
172
|
+
# neither confirmed nor rejected; retrying later may succeed.
|
|
173
|
+
def timed_out?
|
|
174
|
+
error&.dig(:type).to_s == "Timeout"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# A hint about a recoverable cause of an +:unknown+ result, such as the
|
|
178
|
+
# sending IP being blacklisted or lacking a reverse DNS record.
|
|
179
|
+
#
|
|
180
|
+
# @return [String, nil]
|
|
181
|
+
def error_description
|
|
182
|
+
to_h.dig(:smtp, :description)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# @return [Float, nil] how long verification took, in seconds
|
|
186
|
+
def duration
|
|
187
|
+
value = to_h.dig(:debug, :duration)
|
|
188
|
+
return nil if value.nil?
|
|
189
|
+
|
|
190
|
+
value[:secs].to_i + (value[:nanos].to_i / 1_000_000_000.0)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# --- Plumbing ------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
def [](key)
|
|
196
|
+
to_h[key.to_sym]
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def to_json(*args)
|
|
200
|
+
to_h.to_json(*args)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def inspect
|
|
204
|
+
"#<#{self.class} #{email.inspect} #{reachable}#{error? ? " error=#{error[:type].inspect}" : ""}>"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
private
|
|
208
|
+
|
|
209
|
+
# The upstream result replaces a section with +{error: ...}+ when that
|
|
210
|
+
# stage failed, so every reader has to tolerate the section being an error
|
|
211
|
+
# rather than the details it normally holds.
|
|
212
|
+
def section(name)
|
|
213
|
+
value = to_h[name]
|
|
214
|
+
value.is_a?(Hash) && !value.key?(:error) ? value : {}
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def smtp
|
|
218
|
+
section(:smtp)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def mx
|
|
222
|
+
section(:mx)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def misc
|
|
226
|
+
section(:misc)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def smtp_error
|
|
230
|
+
to_h.dig(:smtp, :error)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def mx_error
|
|
234
|
+
to_h.dig(:mx, :error)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def misc_error
|
|
238
|
+
to_h.dig(:misc, :error)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ValidEmailChecker
|
|
4
|
+
# The outcome of parsing an address, with no network access involved.
|
|
5
|
+
#
|
|
6
|
+
# Returned on its own by {ValidEmailChecker.syntax}, and reachable as
|
|
7
|
+
# {Result#syntax} after a full verification.
|
|
8
|
+
class Syntax
|
|
9
|
+
# @return [Hash] the raw data as the Rust crate reported it
|
|
10
|
+
attr_reader :to_h
|
|
11
|
+
|
|
12
|
+
def initialize(data)
|
|
13
|
+
@to_h = (data || {}).freeze
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @return [Boolean] whether the address parses as a well-formed address
|
|
17
|
+
def valid?
|
|
18
|
+
to_h[:is_valid_syntax] == true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [String, nil] the address, or nil if it did not parse
|
|
22
|
+
def address
|
|
23
|
+
to_h[:address]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [String] the part before the "@", or "" for an unparseable address
|
|
27
|
+
def username
|
|
28
|
+
to_h[:username].to_s
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [String] the part after the "@", or "" for an unparseable address
|
|
32
|
+
def domain
|
|
33
|
+
to_h[:domain].to_s
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @return [String, nil] the address with provider-specific quirks removed,
|
|
37
|
+
# such as Gmail's dots and "+" tags
|
|
38
|
+
def normalized_email
|
|
39
|
+
to_h[:normalized_email]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# A likely correction when the domain looks like a typo of a well-known
|
|
43
|
+
# provider, e.g. "gmail.co" -> "gmail.com".
|
|
44
|
+
#
|
|
45
|
+
# Only offered for an address that parses: notorious typo domains such as
|
|
46
|
+
# "gmial.com" are on upstream's blocklist, so they come back invalid with
|
|
47
|
+
# no domain to compare against and hence no suggestion.
|
|
48
|
+
#
|
|
49
|
+
# @return [String, nil]
|
|
50
|
+
def suggestion
|
|
51
|
+
to_h[:suggestion]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @return [Boolean] whether a likely typo correction is available
|
|
55
|
+
def suggestion?
|
|
56
|
+
!suggestion.nil?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def [](key)
|
|
60
|
+
to_h[key.to_sym]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def to_json(*args)
|
|
64
|
+
to_h.to_json(*args)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def inspect
|
|
68
|
+
"#<#{self.class} #{address.inspect} valid=#{valid?}" \
|
|
69
|
+
"#{suggestion? ? " suggestion=#{suggestion.inspect}" : ""}>"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|