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 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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VerifyAnyEmail
4
+ VERSION = "1.0.0"
5
+ 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: []