verifyanyemail 1.0.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/LICENSE +21 -0
- data/README.md +211 -0
- data/lib/verifyanyemail/client.rb +225 -0
- data/lib/verifyanyemail/error.rb +75 -0
- data/lib/verifyanyemail/verification_result.rb +172 -0
- data/lib/verifyanyemail/version.rb +5 -0
- data/lib/verifyanyemail.rb +17 -0
- metadata +57 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c39cf4c4e8d73c11bd349377e9a56314ad84c6c589174447a69113450eb55204
|
|
4
|
+
data.tar.gz: 2b8bed40efde8bfcf7bc6864cf378d2f6533fbf2adbc14a58adcefbe2b205340
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 9b3294f4c0964921462269bb46e7346d780c7f27988d664567d08b1deadfe7bddb6a0ba6672298f776a64412e667c9c6617a58028ddd2e55107f2221d1bec08f
|
|
7
|
+
data.tar.gz: 85071ddf7a3d4a249223826e676d543dc607b0a112f1935c1c58aa45fd8b4ae7231c92b01d33091fc1fba9bf32d6e97e3c8464a880c363dda1d50b8b3143ad35
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 VerifyAnyEmail
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# VerifyAnyEmail Ruby SDK
|
|
2
|
+
|
|
3
|
+
Official Ruby client for the [VerifyAnyEmail](https://verifyany.email) email
|
|
4
|
+
verification API — real-time single and bulk verification, catch-all /
|
|
5
|
+
disposable / role detection, and signed batch webhooks.
|
|
6
|
+
|
|
7
|
+
- **Zero runtime dependencies** — built on the Ruby standard library
|
|
8
|
+
(`net/http`, `json`, `openssl`, `uri`). No Faraday, no ActiveSupport.
|
|
9
|
+
- **Ruby 2.7+**
|
|
10
|
+
- HTTPS with Bearer authentication and a real request timeout.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
Install the gem directly:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
gem install verifyanyemail
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Or add it to your `Gemfile`:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
gem "verifyanyemail"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
then:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
bundle install
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
require "verifyanyemail"
|
|
36
|
+
|
|
37
|
+
client = VerifyAnyEmail::Client.new(ENV["VAE_API_KEY"])
|
|
38
|
+
|
|
39
|
+
result = client.verify("name@example.com")
|
|
40
|
+
|
|
41
|
+
result.status # => "deliverable"
|
|
42
|
+
result.sub_status # => "valid_mailbox"
|
|
43
|
+
result.score # => 98
|
|
44
|
+
result.safe_to_send? # => true
|
|
45
|
+
result.deliverable? # => true
|
|
46
|
+
result.suggestion # => nil (or a "did you mean" domain)
|
|
47
|
+
result.to_h # => the raw result Hash
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Configuration
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
client = VerifyAnyEmail::Client.new(
|
|
54
|
+
ENV["VAE_API_KEY"],
|
|
55
|
+
base_url: "https://api.verifyany.email", # default
|
|
56
|
+
timeout: 30 # seconds, default
|
|
57
|
+
)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Verify options
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
# DNS-only (skip the live SMTP probe):
|
|
64
|
+
client.verify("name@example.com", smtp_probe: false)
|
|
65
|
+
|
|
66
|
+
# Skip catch-all detection:
|
|
67
|
+
client.verify("name@example.com", catch_all_detection: false)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The returned `VerifyAnyEmail::VerificationResult` exposes:
|
|
71
|
+
|
|
72
|
+
| Reader | Description |
|
|
73
|
+
| --------------------- | ------------------------------------------------------ |
|
|
74
|
+
| `email` | Address as submitted |
|
|
75
|
+
| `normalized` | Normalized address |
|
|
76
|
+
| `account` / `domain` | Local part / domain |
|
|
77
|
+
| `status` | `deliverable` \| `undeliverable` \| `risky` \| `unknown` |
|
|
78
|
+
| `sub_status` | Finer reason (e.g. `valid_mailbox`, `catch_all`) |
|
|
79
|
+
| `score` | Confidence 0–100 |
|
|
80
|
+
| `suggestion` | "Did you mean" domain correction |
|
|
81
|
+
| `safe_to_send` | Whether sending is recommended |
|
|
82
|
+
| `checks` | Hash of individual checks |
|
|
83
|
+
| `duration_ms` | Verification time in ms |
|
|
84
|
+
| `checked_at` | ISO-8601 timestamp |
|
|
85
|
+
| `to_h` | The raw result Hash |
|
|
86
|
+
|
|
87
|
+
Boolean helpers: `deliverable?`, `undeliverable?`, `risky?`, `unknown?`,
|
|
88
|
+
`safe_to_send?`, plus per-check helpers `syntax?`, `has_mx_records?`,
|
|
89
|
+
`null_mx?`, `smtp_connectable?`, `mailbox_exists?`, `catch_all?`,
|
|
90
|
+
`role_based?`, `disposable?`, `free_provider?`, `possible_typo?`, `gibberish?`.
|
|
91
|
+
|
|
92
|
+
## Batch verification
|
|
93
|
+
|
|
94
|
+
Submit a list for asynchronous processing, then poll for status/results (or
|
|
95
|
+
receive a signed `batch.completed` webhook).
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
# 1. Submit
|
|
99
|
+
resp = client.verify_batch(
|
|
100
|
+
["a@example.com", "b@example.com"],
|
|
101
|
+
name: "newsletter-2026-07",
|
|
102
|
+
webhook_url: "https://yourapp.com/webhooks/vae" # optional
|
|
103
|
+
)
|
|
104
|
+
batch_id = resp.dig("batch", "id")
|
|
105
|
+
|
|
106
|
+
# 2. Poll status
|
|
107
|
+
status = client.get_batch(batch_id)
|
|
108
|
+
status.dig("batch", "status") # => "queued" | "processing" | "completed" | "failed"
|
|
109
|
+
status.dig("batch", "processed") # => 1
|
|
110
|
+
status.dig("batch", "summary") # => { "deliverable" => .., "undeliverable" => .., ... }
|
|
111
|
+
|
|
112
|
+
# 3. Fetch results (paginated)
|
|
113
|
+
page = client.get_batch_results(batch_id, page: 1, page_size: 100)
|
|
114
|
+
page["total"] # => 2
|
|
115
|
+
page["results"] # => [ { "email" => ..., "status" => ..., "score" => .. }, ... ]
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Webhooks
|
|
119
|
+
|
|
120
|
+
When a batch finishes, VerifyAnyEmail POSTs a `batch.completed` event to your
|
|
121
|
+
`webhookUrl` with an `X-VAE-Signature: sha256=<hmac>` header. Verify it against
|
|
122
|
+
your webhook secret (dashboard → Settings) using the class method — it performs
|
|
123
|
+
a length-safe, constant-time comparison and tolerates the `sha256=` prefix.
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
raw_body = request.body.read # the EXACT raw bytes — do not re-serialize
|
|
127
|
+
signature = request.env["HTTP_X_VAE_SIGNATURE"]
|
|
128
|
+
secret = ENV["VAE_WEBHOOK_SECRET"]
|
|
129
|
+
|
|
130
|
+
if VerifyAnyEmail::Client.verify_webhook_signature(raw_body, signature, secret)
|
|
131
|
+
event = JSON.parse(raw_body)
|
|
132
|
+
# event["event"] => "batch.completed"
|
|
133
|
+
# event["data"] => { "batchId" => ..., "total" => .., "deliverable" => .., ... }
|
|
134
|
+
# ... enqueue processing, then return 2xx ...
|
|
135
|
+
else
|
|
136
|
+
# reject: return 400/401
|
|
137
|
+
end
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Rails controller snippet
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
class WebhooksController < ApplicationController
|
|
144
|
+
# Signature is computed over the raw body, so skip CSRF and read it verbatim.
|
|
145
|
+
skip_before_action :verify_authenticity_token
|
|
146
|
+
|
|
147
|
+
def vae
|
|
148
|
+
raw = request.raw_post
|
|
149
|
+
ok = VerifyAnyEmail::Client.verify_webhook_signature(
|
|
150
|
+
raw,
|
|
151
|
+
request.headers["X-VAE-Signature"],
|
|
152
|
+
Rails.application.credentials.dig(:vae, :webhook_secret)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return head(:unauthorized) unless ok
|
|
156
|
+
|
|
157
|
+
event = JSON.parse(raw)
|
|
158
|
+
ProcessBatchCompletedJob.perform_later(event["data"]) if event["event"] == "batch.completed"
|
|
159
|
+
head :ok
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Test mode (0 credits)
|
|
165
|
+
|
|
166
|
+
Any address at `@test.verifyany.email` returns a deterministic result without
|
|
167
|
+
charging a credit or probing a real mailbox — handy for CI and local dev:
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
client.verify("deliverable@test.verifyany.email").status # => "deliverable"
|
|
171
|
+
client.verify("undeliverable@test.verifyany.email").status # => "undeliverable"
|
|
172
|
+
client.verify("risky@test.verifyany.email").status # => "risky"
|
|
173
|
+
client.verify("unknown@test.verifyany.email").status # => "unknown"
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Error handling
|
|
177
|
+
|
|
178
|
+
Every non-2xx response raises `VerifyAnyEmail::Error`, which carries the HTTP
|
|
179
|
+
`status`, the machine-readable `code` (`error` field from the JSON body), and a
|
|
180
|
+
human-readable `message`.
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
begin
|
|
184
|
+
client.verify("name@example.com")
|
|
185
|
+
rescue VerifyAnyEmail::Error => e
|
|
186
|
+
e.status # => 402
|
|
187
|
+
e.code # => "insufficient_credits"
|
|
188
|
+
e.message # => "You have run out of credits."
|
|
189
|
+
|
|
190
|
+
case e.status
|
|
191
|
+
when 401 then # missing/invalid API key (also e.unauthorized?)
|
|
192
|
+
when 402 then # insufficient credits (also e.payment_required?)
|
|
193
|
+
when 413 then # batch too large for plan
|
|
194
|
+
when 429 then # rate limited (also e.rate_limited?)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Network/transport failures also raise `VerifyAnyEmail::Error` with
|
|
200
|
+
`code == "network_error"`.
|
|
201
|
+
|
|
202
|
+
## Keep your API key server-side
|
|
203
|
+
|
|
204
|
+
Your API key is a secret. Use this SDK only from server-side code (Rails
|
|
205
|
+
controllers, background jobs, scripts). Never embed it in a browser, mobile
|
|
206
|
+
app, or any client shipped to end users — anyone who obtains the key can spend
|
|
207
|
+
your credits. Load it from an environment variable or your secrets manager.
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
|
|
211
|
+
[MIT](LICENSE) © 2026 VerifyAnyEmail
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "openssl"
|
|
6
|
+
require "uri"
|
|
7
|
+
|
|
8
|
+
module VerifyAnyEmail
|
|
9
|
+
# HTTP client for the VerifyAnyEmail API.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# client = VerifyAnyEmail::Client.new(ENV["VAE_API_KEY"])
|
|
13
|
+
# result = client.verify("name@example.com")
|
|
14
|
+
# result.deliverable? # => true
|
|
15
|
+
class Client
|
|
16
|
+
# Default API base URL.
|
|
17
|
+
DEFAULT_BASE_URL = "https://api.verifyany.email"
|
|
18
|
+
|
|
19
|
+
# Default per-request timeout, in seconds.
|
|
20
|
+
DEFAULT_TIMEOUT = 30
|
|
21
|
+
|
|
22
|
+
# @return [String] Base URL (no trailing slash).
|
|
23
|
+
attr_reader :base_url
|
|
24
|
+
|
|
25
|
+
# @return [Integer, Float] Request timeout in seconds.
|
|
26
|
+
attr_reader :timeout
|
|
27
|
+
|
|
28
|
+
# @param api_key [String] Your API key (`vae_...`).
|
|
29
|
+
# @param base_url [String] Override the API base URL.
|
|
30
|
+
# @param timeout [Integer, Float] Open/read timeout in seconds.
|
|
31
|
+
# @raise [ArgumentError] when api_key is missing.
|
|
32
|
+
def initialize(api_key, base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT)
|
|
33
|
+
raise ArgumentError, "api_key is required" if api_key.nil? || api_key.to_s.empty?
|
|
34
|
+
|
|
35
|
+
@api_key = api_key
|
|
36
|
+
@base_url = base_url.to_s.sub(%r{/+\z}, "")
|
|
37
|
+
@timeout = timeout
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Verify a single email address.
|
|
41
|
+
#
|
|
42
|
+
# @param email [String] The address to verify.
|
|
43
|
+
# @param smtp_probe [Boolean, nil] Set false to skip the live SMTP probe.
|
|
44
|
+
# @param catch_all_detection [Boolean, nil] Set false to skip catch-all detection.
|
|
45
|
+
# @return [VerifyAnyEmail::VerificationResult]
|
|
46
|
+
# @raise [VerifyAnyEmail::Error] on any non-2xx response.
|
|
47
|
+
def verify(email, smtp_probe: nil, catch_all_detection: nil)
|
|
48
|
+
body = { "email" => email }
|
|
49
|
+
body["smtpProbe"] = smtp_probe unless smtp_probe.nil?
|
|
50
|
+
body["catchAllDetection"] = catch_all_detection unless catch_all_detection.nil?
|
|
51
|
+
|
|
52
|
+
response = request(:post, "/v1/verify", body: body)
|
|
53
|
+
VerificationResult.new(response["result"] || {})
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Submit a batch of addresses for asynchronous verification.
|
|
57
|
+
#
|
|
58
|
+
# @param emails [Array<String>] Addresses to verify (at least one).
|
|
59
|
+
# @param name [String, nil] Optional label for the batch.
|
|
60
|
+
# @param webhook_url [String, nil] Optional signed `batch.completed` callback URL.
|
|
61
|
+
# @return [Hash] Parsed response, e.g. `{ "ok" => true, "batch" => { "id" => ... } }`.
|
|
62
|
+
# @raise [VerifyAnyEmail::Error] on any non-2xx response.
|
|
63
|
+
def verify_batch(emails, name: nil, webhook_url: nil)
|
|
64
|
+
body = { "emails" => Array(emails) }
|
|
65
|
+
body["name"] = name unless name.nil?
|
|
66
|
+
body["webhookUrl"] = webhook_url unless webhook_url.nil?
|
|
67
|
+
|
|
68
|
+
request(:post, "/v1/verify/batch", body: body)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Fetch the status and summary of a batch.
|
|
72
|
+
#
|
|
73
|
+
# @param id [String] The batch id.
|
|
74
|
+
# @return [Hash] Parsed response.
|
|
75
|
+
# @raise [VerifyAnyEmail::Error] on any non-2xx response.
|
|
76
|
+
def get_batch(id)
|
|
77
|
+
request(:get, "/v1/verify/batch/#{escape(id)}")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Fetch a page of batch results.
|
|
81
|
+
#
|
|
82
|
+
# @param id [String] The batch id.
|
|
83
|
+
# @param page [Integer] 1-based page number.
|
|
84
|
+
# @param page_size [Integer] Results per page (1-1000).
|
|
85
|
+
# @return [Hash] Parsed response with `page`, `pageSize`, `total`, `results`.
|
|
86
|
+
# @raise [VerifyAnyEmail::Error] on any non-2xx response.
|
|
87
|
+
def get_batch_results(id, page: 1, page_size: 100)
|
|
88
|
+
query = { "page" => page, "pageSize" => page_size }
|
|
89
|
+
request(:get, "/v1/verify/batch/#{escape(id)}/results", query: query)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Verify a webhook signature using a length-safe, constant-time comparison.
|
|
93
|
+
#
|
|
94
|
+
# @param raw_body [String] The exact raw request body bytes.
|
|
95
|
+
# @param signature_header [String, nil] Value of the `X-VAE-Signature` header
|
|
96
|
+
# (with or without the `sha256=` prefix).
|
|
97
|
+
# @param secret [String] Your webhook secret.
|
|
98
|
+
# @return [Boolean] true when the signature is valid.
|
|
99
|
+
def self.verify_webhook_signature(raw_body, signature_header, secret)
|
|
100
|
+
return false if signature_header.nil? || secret.nil? || secret.to_s.empty?
|
|
101
|
+
|
|
102
|
+
provided = signature_header.to_s.strip
|
|
103
|
+
provided = provided.sub(/\Asha256=/, "")
|
|
104
|
+
|
|
105
|
+
expected = OpenSSL::HMAC.hexdigest(
|
|
106
|
+
OpenSSL::Digest.new("sha256"),
|
|
107
|
+
secret.to_s,
|
|
108
|
+
raw_body.to_s
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
secure_compare(expected, provided)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Constant-time string comparison that is safe when lengths differ.
|
|
115
|
+
#
|
|
116
|
+
# Uses OpenSSL.fixed_length_secure_compare when available (Ruby 2.6+),
|
|
117
|
+
# falling back to a manual constant-time XOR comparison.
|
|
118
|
+
#
|
|
119
|
+
# @param a [String]
|
|
120
|
+
# @param b [String]
|
|
121
|
+
# @return [Boolean]
|
|
122
|
+
def self.secure_compare(a, b)
|
|
123
|
+
a = a.to_s
|
|
124
|
+
b = b.to_s
|
|
125
|
+
|
|
126
|
+
if OpenSSL.respond_to?(:fixed_length_secure_compare)
|
|
127
|
+
# fixed_length_secure_compare raises when lengths differ, so guard first.
|
|
128
|
+
return false unless a.bytesize == b.bytesize
|
|
129
|
+
|
|
130
|
+
begin
|
|
131
|
+
return OpenSSL.fixed_length_secure_compare(a, b)
|
|
132
|
+
rescue StandardError
|
|
133
|
+
# Fall through to the manual comparison below.
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
return false unless a.bytesize == b.bytesize
|
|
138
|
+
|
|
139
|
+
diff = 0
|
|
140
|
+
a.bytes.each_with_index do |byte, i|
|
|
141
|
+
diff |= byte ^ b.getbyte(i)
|
|
142
|
+
end
|
|
143
|
+
diff.zero?
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
private
|
|
147
|
+
|
|
148
|
+
# Perform an HTTP request and return the parsed JSON body.
|
|
149
|
+
#
|
|
150
|
+
# @raise [VerifyAnyEmail::Error]
|
|
151
|
+
def request(method, path, body: nil, query: nil)
|
|
152
|
+
uri = URI.join(@base_url + "/", path.sub(%r{\A/+}, ""))
|
|
153
|
+
uri.query = URI.encode_www_form(query) if query && !query.empty?
|
|
154
|
+
|
|
155
|
+
req = build_request(method, uri, body)
|
|
156
|
+
|
|
157
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
158
|
+
http.use_ssl = (uri.scheme == "https")
|
|
159
|
+
http.open_timeout = @timeout
|
|
160
|
+
http.read_timeout = @timeout
|
|
161
|
+
http.write_timeout = @timeout if http.respond_to?(:write_timeout=)
|
|
162
|
+
|
|
163
|
+
response =
|
|
164
|
+
begin
|
|
165
|
+
http.request(req)
|
|
166
|
+
rescue StandardError => e
|
|
167
|
+
raise Error.new("HTTP request failed: #{e.message}", code: "network_error")
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
handle_response(response)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# @return [Net::HTTPRequest]
|
|
174
|
+
def build_request(method, uri, body)
|
|
175
|
+
klass =
|
|
176
|
+
case method
|
|
177
|
+
when :get then Net::HTTP::Get
|
|
178
|
+
when :post then Net::HTTP::Post
|
|
179
|
+
when :delete then Net::HTTP::Delete
|
|
180
|
+
else
|
|
181
|
+
raise ArgumentError, "Unsupported HTTP method: #{method}"
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
req = klass.new(uri)
|
|
185
|
+
req["Authorization"] = "Bearer #{@api_key}"
|
|
186
|
+
req["Accept"] = "application/json"
|
|
187
|
+
req["User-Agent"] = "verifyanyemail-ruby/#{VerifyAnyEmail::VERSION}"
|
|
188
|
+
|
|
189
|
+
unless body.nil?
|
|
190
|
+
req["Content-Type"] = "application/json"
|
|
191
|
+
req.body = JSON.generate(body)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
req
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Parse the response body, raising {Error} on non-2xx.
|
|
198
|
+
#
|
|
199
|
+
# @return [Hash]
|
|
200
|
+
def handle_response(response)
|
|
201
|
+
status = response.code.to_i
|
|
202
|
+
raw = response.body.to_s
|
|
203
|
+
|
|
204
|
+
parsed =
|
|
205
|
+
if raw.empty?
|
|
206
|
+
nil
|
|
207
|
+
else
|
|
208
|
+
begin
|
|
209
|
+
JSON.parse(raw)
|
|
210
|
+
rescue JSON::ParserError
|
|
211
|
+
nil
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
return parsed || {} if status >= 200 && status < 300
|
|
216
|
+
|
|
217
|
+
raise Error.from_response(status, parsed, raw)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# @return [String] URL-escaped path segment.
|
|
221
|
+
def escape(value)
|
|
222
|
+
URI.encode_www_form_component(value.to_s)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VerifyAnyEmail
|
|
4
|
+
# Raised for any non-success API response, transport failure, or invalid usage.
|
|
5
|
+
#
|
|
6
|
+
# When the API returns a JSON error body of the shape
|
|
7
|
+
# `{ "error": "<code>", "message": "<human message>" }`, the {#code} and
|
|
8
|
+
# {#message} readers expose those fields and {#status} carries the HTTP
|
|
9
|
+
# status code. Notable statuses:
|
|
10
|
+
#
|
|
11
|
+
# 401 - Missing or invalid API key
|
|
12
|
+
# 402 - Insufficient credits
|
|
13
|
+
# 413 - Batch too large for your plan
|
|
14
|
+
# 429 - Rate limited / concurrent batch limit reached
|
|
15
|
+
class Error < StandardError
|
|
16
|
+
# @return [Integer, nil] HTTP status code, when the error came from a response.
|
|
17
|
+
attr_reader :status
|
|
18
|
+
|
|
19
|
+
# @return [String, nil] Machine-readable error code from the JSON body (`error`).
|
|
20
|
+
attr_reader :code
|
|
21
|
+
|
|
22
|
+
# @return [Hash, nil] The raw parsed JSON body of the error response, if any.
|
|
23
|
+
attr_reader :body
|
|
24
|
+
|
|
25
|
+
# @param message [String] Human-readable message.
|
|
26
|
+
# @param status [Integer, nil] HTTP status code.
|
|
27
|
+
# @param code [String, nil] Machine-readable error code.
|
|
28
|
+
# @param body [Hash, nil] Parsed JSON body.
|
|
29
|
+
def initialize(message = nil, status: nil, code: nil, body: nil)
|
|
30
|
+
@status = status
|
|
31
|
+
@code = code
|
|
32
|
+
@body = body
|
|
33
|
+
super(message || code || "VerifyAnyEmail::Error")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# True when the failure is due to a missing or invalid API key (HTTP 401).
|
|
37
|
+
# @return [Boolean]
|
|
38
|
+
def unauthorized?
|
|
39
|
+
status == 401
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# True when the account has insufficient credits (HTTP 402).
|
|
43
|
+
# @return [Boolean]
|
|
44
|
+
def payment_required?
|
|
45
|
+
status == 402
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# True when the request was rate limited (HTTP 429).
|
|
49
|
+
# @return [Boolean]
|
|
50
|
+
def rate_limited?
|
|
51
|
+
status == 429
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Build an {Error} from an HTTP status and a parsed (or unparsed) body.
|
|
55
|
+
#
|
|
56
|
+
# @param status [Integer]
|
|
57
|
+
# @param parsed [Hash, nil] Parsed JSON body (expects `error`/`message` keys).
|
|
58
|
+
# @param raw_body [String, nil] Raw response body, used as a fallback message.
|
|
59
|
+
# @return [VerifyAnyEmail::Error]
|
|
60
|
+
def self.from_response(status, parsed, raw_body = nil)
|
|
61
|
+
code = nil
|
|
62
|
+
message = nil
|
|
63
|
+
|
|
64
|
+
if parsed.is_a?(Hash)
|
|
65
|
+
code = parsed["error"] || parsed[:error]
|
|
66
|
+
message = parsed["message"] || parsed[:message]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
message ||= raw_body
|
|
70
|
+
message = "HTTP #{status}" if message.nil? || message.to_s.empty?
|
|
71
|
+
|
|
72
|
+
new(message, status: status, code: code, body: (parsed.is_a?(Hash) ? parsed : nil))
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VerifyAnyEmail
|
|
4
|
+
# Immutable wrapper around a single verification `result` object returned by
|
|
5
|
+
# `POST /v1/verify`.
|
|
6
|
+
#
|
|
7
|
+
# Access the raw hash with {#to_h}, or use the reader methods below. Boolean
|
|
8
|
+
# helpers such as {#deliverable?} classify the {#status} field.
|
|
9
|
+
class VerificationResult
|
|
10
|
+
# @return [Hash] The raw parsed result hash (string keys).
|
|
11
|
+
attr_reader :raw
|
|
12
|
+
|
|
13
|
+
# @param data [Hash] The `result` object from the API response.
|
|
14
|
+
def initialize(data)
|
|
15
|
+
@raw = data.is_a?(Hash) ? data : {}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @return [String, nil] The address exactly as submitted.
|
|
19
|
+
def email
|
|
20
|
+
@raw["email"]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [String, nil] Normalized (lowercased/trimmed) address.
|
|
24
|
+
def normalized
|
|
25
|
+
@raw["normalized"]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @return [String, nil] Local part (before the @).
|
|
29
|
+
def account
|
|
30
|
+
@raw["account"]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return [String, nil] Domain part (after the @).
|
|
34
|
+
def domain
|
|
35
|
+
@raw["domain"]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @return [String, nil] One of: deliverable, undeliverable, risky, unknown.
|
|
39
|
+
def status
|
|
40
|
+
@raw["status"]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @return [String, nil] Finer-grained reason (e.g. valid_mailbox, catch_all).
|
|
44
|
+
def sub_status
|
|
45
|
+
@raw["subStatus"]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @return [Integer, nil] Confidence score, 0-100.
|
|
49
|
+
def score
|
|
50
|
+
@raw["score"]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @return [Hash] The `checks` sub-object (string keys), or {} when absent.
|
|
54
|
+
def checks
|
|
55
|
+
@raw["checks"] || {}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @return [String, nil] "Did you mean" domain correction, when available.
|
|
59
|
+
def suggestion
|
|
60
|
+
@raw["suggestion"]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @return [Boolean, nil] Whether the API recommends sending to this address.
|
|
64
|
+
def safe_to_send
|
|
65
|
+
@raw["safeToSend"]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @return [Integer, nil] Wall-clock verification time in milliseconds.
|
|
69
|
+
def duration_ms
|
|
70
|
+
@raw["durationMs"]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @return [String, nil] ISO-8601 timestamp of when the check ran.
|
|
74
|
+
def checked_at
|
|
75
|
+
@raw["checkedAt"]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @return [Boolean] status == "deliverable"
|
|
79
|
+
def deliverable?
|
|
80
|
+
status == "deliverable"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @return [Boolean] status == "undeliverable"
|
|
84
|
+
def undeliverable?
|
|
85
|
+
status == "undeliverable"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @return [Boolean] status == "risky"
|
|
89
|
+
def risky?
|
|
90
|
+
status == "risky"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @return [Boolean] status == "unknown"
|
|
94
|
+
def unknown?
|
|
95
|
+
status == "unknown"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# @return [Boolean] Convenience alias for the `safeToSend` flag.
|
|
99
|
+
def safe_to_send?
|
|
100
|
+
safe_to_send == true
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# --- Individual check accessors -----------------------------------------
|
|
104
|
+
|
|
105
|
+
# @return [Boolean, nil]
|
|
106
|
+
def syntax?
|
|
107
|
+
checks["syntax"]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @return [Boolean, nil]
|
|
111
|
+
def has_mx_records?
|
|
112
|
+
checks["hasMxRecords"]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# @return [Boolean, nil]
|
|
116
|
+
def null_mx?
|
|
117
|
+
checks["nullMx"]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# @return [Boolean, nil]
|
|
121
|
+
def smtp_connectable?
|
|
122
|
+
checks["smtpConnectable"]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# @return [Boolean, nil]
|
|
126
|
+
def mailbox_exists?
|
|
127
|
+
checks["mailboxExists"]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# @return [Boolean, nil]
|
|
131
|
+
def catch_all?
|
|
132
|
+
checks["catchAll"]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# @return [Boolean, nil]
|
|
136
|
+
def role_based?
|
|
137
|
+
checks["roleBased"]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# @return [Boolean, nil]
|
|
141
|
+
def disposable?
|
|
142
|
+
checks["disposable"]
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# @return [Boolean, nil]
|
|
146
|
+
def free_provider?
|
|
147
|
+
checks["freeProvider"]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# @return [Boolean, nil]
|
|
151
|
+
def possible_typo?
|
|
152
|
+
checks["possibleTypo"]
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# @return [Boolean, nil]
|
|
156
|
+
def gibberish?
|
|
157
|
+
checks["gibberish"]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# @return [Hash] The raw underlying hash.
|
|
161
|
+
def to_h
|
|
162
|
+
@raw
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# @return [String]
|
|
166
|
+
def to_s
|
|
167
|
+
"#<VerifyAnyEmail::VerificationResult #{normalized || email.inspect} " \
|
|
168
|
+
"status=#{status.inspect} score=#{score.inspect}>"
|
|
169
|
+
end
|
|
170
|
+
alias inspect to_s
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "verifyanyemail/version"
|
|
4
|
+
require_relative "verifyanyemail/error"
|
|
5
|
+
require_relative "verifyanyemail/verification_result"
|
|
6
|
+
require_relative "verifyanyemail/client"
|
|
7
|
+
|
|
8
|
+
# Ruby SDK for the VerifyAnyEmail API (https://verifyany.email).
|
|
9
|
+
#
|
|
10
|
+
# @example Quick start
|
|
11
|
+
# require "verifyanyemail"
|
|
12
|
+
#
|
|
13
|
+
# client = VerifyAnyEmail::Client.new(ENV["VAE_API_KEY"])
|
|
14
|
+
# result = client.verify("name@example.com")
|
|
15
|
+
# puts result.status
|
|
16
|
+
module VerifyAnyEmail
|
|
17
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: verifyanyemail
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- VerifyAnyEmail
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-07-21 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: 'Official Ruby client for VerifyAnyEmail: real-time single and bulk email
|
|
14
|
+
verification, catch-all/disposable/role detection, and signed batch webhooks. Built
|
|
15
|
+
on the Ruby standard library (net/http) with no runtime dependencies.'
|
|
16
|
+
email:
|
|
17
|
+
- support@verifyany.email
|
|
18
|
+
executables: []
|
|
19
|
+
extensions: []
|
|
20
|
+
extra_rdoc_files: []
|
|
21
|
+
files:
|
|
22
|
+
- LICENSE
|
|
23
|
+
- README.md
|
|
24
|
+
- lib/verifyanyemail.rb
|
|
25
|
+
- lib/verifyanyemail/client.rb
|
|
26
|
+
- lib/verifyanyemail/error.rb
|
|
27
|
+
- lib/verifyanyemail/verification_result.rb
|
|
28
|
+
- lib/verifyanyemail/version.rb
|
|
29
|
+
homepage: https://verifyany.email
|
|
30
|
+
licenses:
|
|
31
|
+
- MIT
|
|
32
|
+
metadata:
|
|
33
|
+
homepage_uri: https://verifyany.email
|
|
34
|
+
source_code_uri: https://github.com/verifyanyemail/verifyanyemail-ruby
|
|
35
|
+
changelog_uri: https://github.com/verifyanyemail/verifyanyemail-ruby/blob/main/CHANGELOG.md
|
|
36
|
+
bug_tracker_uri: https://github.com/verifyanyemail/verifyanyemail-ruby/issues
|
|
37
|
+
documentation_uri: https://verifyany.email/docs
|
|
38
|
+
post_install_message:
|
|
39
|
+
rdoc_options: []
|
|
40
|
+
require_paths:
|
|
41
|
+
- lib
|
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '2.7'
|
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
48
|
+
requirements:
|
|
49
|
+
- - ">="
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '0'
|
|
52
|
+
requirements: []
|
|
53
|
+
rubygems_version: 3.4.19
|
|
54
|
+
signing_key:
|
|
55
|
+
specification_version: 4
|
|
56
|
+
summary: Ruby SDK for the VerifyAnyEmail email verification API.
|
|
57
|
+
test_files: []
|