apiddress 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/LICENSE +21 -0
- data/README.md +142 -0
- data/lib/apiddress/client.rb +208 -0
- data/lib/apiddress/error.rb +38 -0
- data/lib/apiddress/models.rb +180 -0
- data/lib/apiddress.rb +8 -0
- metadata +47 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 659eb249d983cd4c87e29610a1a83d659d3487fb3d49f69859553585c4ec5a65
|
|
4
|
+
data.tar.gz: 9b7ff658b157ff64f474d377732c92dedbc259cb663df0949580816925bf4168
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b35284431baef688f6588a9b93d5fd14a62b85bff8e401700d1efc93815e9dc76b39a9aa5d27284f4415a5bac5b2c1c2df713c5403d5c83d5b70dcb82d482619
|
|
7
|
+
data.tar.gz: 868c82a52672a1f6ef90f65520a76ea7360f43b4fcc4c20f313b9a817574b7ca74339fe40bb3291ffd48e5398f8c97e855138fa74e6d95b5b46b57a33fc8592d
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 APIddress
|
|
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,142 @@
|
|
|
1
|
+
# apiddress
|
|
2
|
+
|
|
3
|
+
Official Ruby SDK for the [APIddress](https://api.apiddress.com) email validation API.
|
|
4
|
+
|
|
5
|
+
- Zero runtime gem dependencies (stdlib `net/http` only)
|
|
6
|
+
- Ruby 3.0+, immutable `Data` value objects, fully type-documented
|
|
7
|
+
- Automatic retry with backoff on `429` and `5xx` (batch creation is never retried)
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
gem install apiddress
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or add to your `Gemfile`:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
gem "apiddress"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quickstart
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
require "apiddress"
|
|
25
|
+
|
|
26
|
+
client = Apiddress::Client.new(ENV["APIDDRESS_API_KEY"])
|
|
27
|
+
|
|
28
|
+
result = client.validate_email("ada@stripe.com")
|
|
29
|
+
puts result.status # => "valid"
|
|
30
|
+
puts result.score # => 0.98
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Configuration
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
client = Apiddress::Client.new(
|
|
37
|
+
"YOUR_API_KEY",
|
|
38
|
+
base_url: "https://api.apiddress.com", # default
|
|
39
|
+
timeout: 10, # per-request timeout (seconds), default 10
|
|
40
|
+
max_retries: 2, # retries on 429/5xx, default 2
|
|
41
|
+
)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
### Validate one email
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
result = client.validate_email(
|
|
50
|
+
"john@company.com",
|
|
51
|
+
check_smtp: false, # default
|
|
52
|
+
allow_role_based: true, # server default
|
|
53
|
+
)
|
|
54
|
+
# result.status: "valid" | "invalid" | "risky" | "disposable" | "unknown"
|
|
55
|
+
# result.suggestion: "john@gmail.com" for typo-like addresses, else nil
|
|
56
|
+
# result.checks: Apiddress::ValidationChecks (syntax, domain_exists, mx, smtp, ...)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
A malformed value (e.g. `"not-an-email"`) is a verdict, not an error: you get
|
|
60
|
+
`status == "invalid"` with `reason == "invalid_syntax"`.
|
|
61
|
+
|
|
62
|
+
### Validate up to 100 emails synchronously
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
response = client.validate_emails(["a@example.com", "b@example.com"])
|
|
66
|
+
puts response.count
|
|
67
|
+
response.results.each { |r| puts "#{r.email} #{r.status}" }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Batch jobs (up to 5000 emails)
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
batch = client.create_batch(
|
|
74
|
+
emails,
|
|
75
|
+
callback_url: "https://yourapp.com/webhooks/apiddress", # optional
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
done = client.wait_for_batch(
|
|
79
|
+
batch.batch_id,
|
|
80
|
+
poll_interval: 1.0, # default
|
|
81
|
+
timeout: 60.0, # default
|
|
82
|
+
)
|
|
83
|
+
puts "#{done.status} #{done.results.length}"
|
|
84
|
+
|
|
85
|
+
# Or poll yourself:
|
|
86
|
+
status = client.get_batch(batch.batch_id)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`wait_for_batch` returns the terminal state (`"completed"` or `"failed"`) —
|
|
90
|
+
check `status` before using `results`.
|
|
91
|
+
|
|
92
|
+
### Account
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
profile = client.me # plan, limits, usage
|
|
96
|
+
usage = client.usage # current month
|
|
97
|
+
may = client.usage("2026-05") # specific month
|
|
98
|
+
health = client.health # no auth required
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Error handling
|
|
102
|
+
|
|
103
|
+
Every failed request raises an `Apiddress::Error`:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
require "apiddress"
|
|
107
|
+
|
|
108
|
+
begin
|
|
109
|
+
client.validate_email("john@company.com")
|
|
110
|
+
rescue Apiddress::Error => err
|
|
111
|
+
puts err.status # 429
|
|
112
|
+
puts err.code # "quota_exceeded"
|
|
113
|
+
puts err.message # "Monthly request limit exceeded."
|
|
114
|
+
p err.details # {"requests_used"=>..., "requests_limit"=>...}
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
| `status` | `code` |
|
|
119
|
+
| -------- | ------------------- |
|
|
120
|
+
| 400 | `invalid_request` |
|
|
121
|
+
| 401 | `unauthorized` |
|
|
122
|
+
| 404 | `not_found` |
|
|
123
|
+
| 429 | `quota_exceeded` |
|
|
124
|
+
| 500 | `internal_error` |
|
|
125
|
+
| 0 | `timeout` (request or `wait_for_batch` timeout) |
|
|
126
|
+
|
|
127
|
+
## Development
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
bundle install
|
|
131
|
+
|
|
132
|
+
# Unit tests (no network required):
|
|
133
|
+
ruby test/test_client.rb
|
|
134
|
+
|
|
135
|
+
# Integration tests need a live backend:
|
|
136
|
+
APIDDRESS_BASE_URL=http://localhost:3000 APIDDRESS_API_KEY=test_key_local_dev \
|
|
137
|
+
ruby test/test_client.rb
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
MIT
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "net/http"
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Apiddress
|
|
6
|
+
# Official APIddress client (stdlib net/http, Ruby 3.0+).
|
|
7
|
+
#
|
|
8
|
+
# client = Apiddress::Client.new(ENV["APIDDRESS_API_KEY"])
|
|
9
|
+
# result = client.validate_email("ada@stripe.com")
|
|
10
|
+
# puts result.status # => "valid"
|
|
11
|
+
class Client
|
|
12
|
+
DEFAULT_BASE_URL = "https://api.apiddress.com"
|
|
13
|
+
DEFAULT_TIMEOUT = 10 # seconds
|
|
14
|
+
DEFAULT_MAX_RETRIES = 2
|
|
15
|
+
|
|
16
|
+
# @param api_key [String] Your APIddress API key (sent as x-api-key).
|
|
17
|
+
# @param base_url [String] API origin. Defaults to production.
|
|
18
|
+
# @param timeout [Integer] Per-request timeout in seconds. Default 10.
|
|
19
|
+
# @param max_retries [Integer] Retries on 429/5xx. Default 2.
|
|
20
|
+
# Batch creation is never retried.
|
|
21
|
+
def initialize(api_key, base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT, max_retries: DEFAULT_MAX_RETRIES)
|
|
22
|
+
raise ArgumentError, "Apiddress::Client: api_key must not be empty" if api_key.nil? || api_key.empty?
|
|
23
|
+
|
|
24
|
+
@api_key = api_key
|
|
25
|
+
@base_url = base_url.chomp("/")
|
|
26
|
+
@timeout = timeout
|
|
27
|
+
@max_retries = max_retries
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# -------------------------------------------------------------------------
|
|
31
|
+
# Validation
|
|
32
|
+
# -------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
# Validate a single email address. Costs 1 request.
|
|
35
|
+
#
|
|
36
|
+
# @param email [String]
|
|
37
|
+
# @param check_smtp [Boolean] Attempt an SMTP-level probe. Default false.
|
|
38
|
+
# @param allow_role_based [Boolean, nil] Treat role-based addresses as acceptable. Default nil (use server default).
|
|
39
|
+
# @return [ValidateEmailResponse]
|
|
40
|
+
def validate_email(email, check_smtp: false, allow_role_based: nil)
|
|
41
|
+
body = { email: email, check_smtp: check_smtp }
|
|
42
|
+
body[:allow_role_based] = allow_role_based unless allow_role_based.nil?
|
|
43
|
+
ValidateEmailResponse.from_hash(request(:post, "/api/v1/validate-email", body:))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Validate 1-100 email addresses synchronously. Costs one request per email.
|
|
47
|
+
#
|
|
48
|
+
# @param emails [Array<String>]
|
|
49
|
+
# @param check_smtp [Boolean]
|
|
50
|
+
# @return [BulkValidateResponse]
|
|
51
|
+
def validate_emails(emails, check_smtp: false)
|
|
52
|
+
BulkValidateResponse.from_hash(
|
|
53
|
+
request(:post, "/api/v1/validate-emails", body: { emails:, check_smtp: }),
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# -------------------------------------------------------------------------
|
|
58
|
+
# Batch jobs
|
|
59
|
+
# -------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
# Create an asynchronous batch job for 1-5000 email addresses.
|
|
62
|
+
# Never retried automatically (to avoid duplicate jobs).
|
|
63
|
+
#
|
|
64
|
+
# @param emails [Array<String>]
|
|
65
|
+
# @param callback_url [String, nil] Webhook URL invoked when the batch completes.
|
|
66
|
+
# @param check_smtp [Boolean]
|
|
67
|
+
# @return [BatchAcceptedResponse]
|
|
68
|
+
def create_batch(emails, callback_url: nil, check_smtp: false)
|
|
69
|
+
body = { emails:, check_smtp: }
|
|
70
|
+
body[:callback_url] = callback_url unless callback_url.nil?
|
|
71
|
+
BatchAcceptedResponse.from_hash(
|
|
72
|
+
request(:post, "/api/v1/batches", body:, retryable: false),
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Get the current state (and results, when completed) of a batch job.
|
|
77
|
+
#
|
|
78
|
+
# @param batch_id [String]
|
|
79
|
+
# @return [BatchStatusResponse]
|
|
80
|
+
def get_batch(batch_id)
|
|
81
|
+
BatchStatusResponse.from_hash(
|
|
82
|
+
request(:get, "/api/v1/batches/#{URI.encode_www_form_component(batch_id)}"),
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Poll a batch job until it reaches a terminal state ("completed" or "failed").
|
|
87
|
+
#
|
|
88
|
+
# Returns the final state. Raises {Error} with code +"timeout"+ if the job
|
|
89
|
+
# is still running after +timeout+ seconds.
|
|
90
|
+
#
|
|
91
|
+
# @param batch_id [String]
|
|
92
|
+
# @param poll_interval [Float] Delay between polls in seconds. Default 1.0.
|
|
93
|
+
# @param timeout [Float] Give up after this many seconds. Default 60.0.
|
|
94
|
+
# @return [BatchStatusResponse]
|
|
95
|
+
def wait_for_batch(batch_id, poll_interval: 1.0, timeout: 60.0)
|
|
96
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
97
|
+
|
|
98
|
+
loop do
|
|
99
|
+
batch = get_batch(batch_id)
|
|
100
|
+
return batch if %w[completed failed].include?(batch.status)
|
|
101
|
+
|
|
102
|
+
if Process.clock_gettime(Process::CLOCK_MONOTONIC) + poll_interval > deadline
|
|
103
|
+
raise Error.new(0, "timeout",
|
|
104
|
+
"Batch #{batch_id} did not complete within #{timeout}s (last status: #{batch.status})")
|
|
105
|
+
end
|
|
106
|
+
sleep(poll_interval)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# -------------------------------------------------------------------------
|
|
111
|
+
# Account
|
|
112
|
+
# -------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
# Get the profile (plan, limits, usage) of the authenticated API key.
|
|
115
|
+
# @return [ApiKeyProfileResponse]
|
|
116
|
+
def me
|
|
117
|
+
ApiKeyProfileResponse.from_hash(request(:get, "/api/v1/me"))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Get usage for a month ("YYYY-MM"). Defaults to the current month.
|
|
121
|
+
# @param month [String, nil]
|
|
122
|
+
# @return [UsageResponse]
|
|
123
|
+
def usage(month = nil)
|
|
124
|
+
query = month ? { month: } : nil
|
|
125
|
+
UsageResponse.from_hash(request(:get, "/api/v1/usage", query:))
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Service health check. Does not require authentication.
|
|
129
|
+
# @return [HealthResponse]
|
|
130
|
+
def health
|
|
131
|
+
HealthResponse.from_hash(request(:get, "/api/v1/health", auth: false))
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# -------------------------------------------------------------------------
|
|
135
|
+
# Transport
|
|
136
|
+
# -------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
def request(method, path, body: nil, query: nil, auth: true, retryable: true)
|
|
141
|
+
url = URI.parse(@base_url + path)
|
|
142
|
+
url.query = URI.encode_www_form(query.transform_keys(&:to_s)) if query
|
|
143
|
+
|
|
144
|
+
max_attempts = retryable ? @max_retries + 1 : 1
|
|
145
|
+
|
|
146
|
+
attempt = 0
|
|
147
|
+
loop do
|
|
148
|
+
http = Net::HTTP.new(url.host, url.port)
|
|
149
|
+
http.use_ssl = url.scheme == "https"
|
|
150
|
+
http.open_timeout = @timeout
|
|
151
|
+
http.read_timeout = @timeout
|
|
152
|
+
|
|
153
|
+
req_class = method == :get ? Net::HTTP::Get : Net::HTTP::Post
|
|
154
|
+
req = req_class.new(url.request_uri)
|
|
155
|
+
req["Accept"] = "application/json"
|
|
156
|
+
req["x-api-key"] = @api_key if auth
|
|
157
|
+
|
|
158
|
+
if body
|
|
159
|
+
req["Content-Type"] = "application/json"
|
|
160
|
+
req.body = JSON.generate(body)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
begin
|
|
164
|
+
response = http.request(req)
|
|
165
|
+
rescue Net::OpenTimeout, Net::ReadTimeout, Timeout::Error => e
|
|
166
|
+
raise Error.new(0, "timeout", "Request to #{path} timed out after #{@timeout}s")
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
http_status = response.code.to_i
|
|
170
|
+
|
|
171
|
+
if http_status >= 200 && http_status < 300
|
|
172
|
+
return JSON.parse(response.body)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
api_error = parse_error_envelope(response.body, http_status)
|
|
176
|
+
should_retry = attempt + 1 < max_attempts && (http_status == 429 || http_status >= 500)
|
|
177
|
+
|
|
178
|
+
raise api_error unless should_retry
|
|
179
|
+
|
|
180
|
+
sleep(backoff_seconds(attempt))
|
|
181
|
+
attempt += 1
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Exponential backoff with a little jitter: ~0.25s, ~0.5s, ~1s, capped at 4s.
|
|
186
|
+
def backoff_seconds(attempt)
|
|
187
|
+
[0.25 * (2**attempt), 4.0].min + rand * 0.1
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def parse_error_envelope(body, http_status)
|
|
191
|
+
code = "unknown_error"
|
|
192
|
+
message = "HTTP #{http_status}"
|
|
193
|
+
details = nil
|
|
194
|
+
|
|
195
|
+
begin
|
|
196
|
+
payload = JSON.parse(body)
|
|
197
|
+
envelope = payload["error"] || {}
|
|
198
|
+
code = envelope["code"] || code
|
|
199
|
+
message = envelope["message"] || message
|
|
200
|
+
details = envelope["details"]
|
|
201
|
+
rescue JSON::ParserError
|
|
202
|
+
# Non-JSON error body; keep defaults.
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
Error.new(http_status, code, message, details)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Apiddress
|
|
2
|
+
# Raised for any failed APIddress request.
|
|
3
|
+
#
|
|
4
|
+
# For HTTP errors, +status+ is the response status code and +code+ / +message+ /
|
|
5
|
+
# +details+ come from the API error envelope +{"error": {"code", "message", "details"}}+.
|
|
6
|
+
#
|
|
7
|
+
# For client-side failures (request timeout, wait_for_batch timeout), +status+
|
|
8
|
+
# is 0 and +code+ is +"timeout"+.
|
|
9
|
+
class Error < StandardError
|
|
10
|
+
# @return [Integer] HTTP status code, or 0 for client-side failures.
|
|
11
|
+
attr_reader :status
|
|
12
|
+
|
|
13
|
+
# @return [String] Machine-readable error code, e.g. "quota_exceeded".
|
|
14
|
+
attr_reader :code
|
|
15
|
+
|
|
16
|
+
# @return [String] Human-readable description (also the exception message).
|
|
17
|
+
attr_reader :message
|
|
18
|
+
|
|
19
|
+
# @return [Hash, nil] Extra context from the API, or nil.
|
|
20
|
+
attr_reader :details
|
|
21
|
+
|
|
22
|
+
# @param status [Integer]
|
|
23
|
+
# @param code [String]
|
|
24
|
+
# @param message [String]
|
|
25
|
+
# @param details [Hash, nil]
|
|
26
|
+
def initialize(status, code, message, details = nil)
|
|
27
|
+
super(message)
|
|
28
|
+
@status = status
|
|
29
|
+
@code = code
|
|
30
|
+
@message = message
|
|
31
|
+
@details = details
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def inspect
|
|
35
|
+
"#<Apiddress::Error status=#{status.inspect} code=#{code.inspect} message=#{message.inspect} details=#{details.inspect}>"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
module Apiddress
|
|
2
|
+
# Response models for the APIddress API.
|
|
3
|
+
#
|
|
4
|
+
# Hand-mapped 1:1 from the OpenAPI 3.1 contract (backend/openapi.yaml).
|
|
5
|
+
# Attribute names match the wire format exactly (snake_case).
|
|
6
|
+
|
|
7
|
+
# Individual checks performed on an email address.
|
|
8
|
+
ValidationChecks = Data.define(
|
|
9
|
+
:syntax,
|
|
10
|
+
:domain_exists,
|
|
11
|
+
:mx,
|
|
12
|
+
# nil when the SMTP probe was skipped or inconclusive.
|
|
13
|
+
:smtp,
|
|
14
|
+
:disposable,
|
|
15
|
+
:role_based,
|
|
16
|
+
:free_provider,
|
|
17
|
+
# nil when not determined.
|
|
18
|
+
:catch_all,
|
|
19
|
+
:typo,
|
|
20
|
+
# "low" | "medium" | "high" | nil
|
|
21
|
+
:spam_trap_risk,
|
|
22
|
+
) do
|
|
23
|
+
# @param data [Hash]
|
|
24
|
+
# @return [ValidationChecks]
|
|
25
|
+
def self.from_hash(data)
|
|
26
|
+
new(
|
|
27
|
+
syntax: data["syntax"],
|
|
28
|
+
domain_exists: data["domain_exists"],
|
|
29
|
+
mx: data["mx"],
|
|
30
|
+
smtp: data["smtp"],
|
|
31
|
+
disposable: data["disposable"],
|
|
32
|
+
role_based: data["role_based"],
|
|
33
|
+
free_provider: data["free_provider"],
|
|
34
|
+
catch_all: data["catch_all"],
|
|
35
|
+
typo: data["typo"],
|
|
36
|
+
spam_trap_risk: data["spam_trap_risk"],
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Result of validating a single email address.
|
|
42
|
+
ValidateEmailResponse = Data.define(
|
|
43
|
+
:email,
|
|
44
|
+
:normalized_email,
|
|
45
|
+
# "valid" | "invalid" | "risky" | "disposable" | "unknown"
|
|
46
|
+
:status,
|
|
47
|
+
:valid,
|
|
48
|
+
# Confidence score between 0 and 1.
|
|
49
|
+
:score,
|
|
50
|
+
# Machine-readable reason, e.g. "accepted_email", "invalid_syntax".
|
|
51
|
+
:reason,
|
|
52
|
+
# Suggested correction for typo-like addresses, or nil.
|
|
53
|
+
:suggestion,
|
|
54
|
+
:checks,
|
|
55
|
+
:provider,
|
|
56
|
+
:created_at,
|
|
57
|
+
) do
|
|
58
|
+
def self.from_hash(data)
|
|
59
|
+
new(
|
|
60
|
+
email: data["email"],
|
|
61
|
+
normalized_email: data["normalized_email"],
|
|
62
|
+
status: data["status"],
|
|
63
|
+
valid: data["valid"],
|
|
64
|
+
score: data["score"].to_f,
|
|
65
|
+
reason: data["reason"],
|
|
66
|
+
suggestion: data["suggestion"],
|
|
67
|
+
checks: ValidationChecks.from_hash(data["checks"]),
|
|
68
|
+
provider: data["provider"],
|
|
69
|
+
created_at: data["created_at"],
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Result of a synchronous bulk validation.
|
|
75
|
+
BulkValidateResponse = Data.define(:count, :results) do
|
|
76
|
+
def self.from_hash(data)
|
|
77
|
+
new(
|
|
78
|
+
count: data["count"].to_i,
|
|
79
|
+
results: data["results"].map { |item| ValidateEmailResponse.from_hash(item) },
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Returned when an asynchronous batch job is accepted (HTTP 202).
|
|
85
|
+
BatchAcceptedResponse = Data.define(
|
|
86
|
+
:batch_id,
|
|
87
|
+
# Always "pending" on acceptance.
|
|
88
|
+
:status,
|
|
89
|
+
:submitted_count,
|
|
90
|
+
:created_at,
|
|
91
|
+
) do
|
|
92
|
+
def self.from_hash(data)
|
|
93
|
+
new(
|
|
94
|
+
batch_id: data["batch_id"],
|
|
95
|
+
status: data["status"],
|
|
96
|
+
submitted_count: data["submitted_count"].to_i,
|
|
97
|
+
created_at: data["created_at"],
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Current state of an asynchronous batch job.
|
|
103
|
+
BatchStatusResponse = Data.define(
|
|
104
|
+
:batch_id,
|
|
105
|
+
# "pending" | "processing" | "completed" | "failed"
|
|
106
|
+
:status,
|
|
107
|
+
:submitted_count,
|
|
108
|
+
:processed_count,
|
|
109
|
+
:created_at,
|
|
110
|
+
:completed_at,
|
|
111
|
+
# Per-email results once the batch is completed, otherwise nil.
|
|
112
|
+
:results,
|
|
113
|
+
) do
|
|
114
|
+
def self.from_hash(data)
|
|
115
|
+
raw_results = data["results"]
|
|
116
|
+
new(
|
|
117
|
+
batch_id: data["batch_id"],
|
|
118
|
+
status: data["status"],
|
|
119
|
+
submitted_count: data["submitted_count"].to_i,
|
|
120
|
+
processed_count: data["processed_count"].to_i,
|
|
121
|
+
created_at: data["created_at"],
|
|
122
|
+
completed_at: data["completed_at"],
|
|
123
|
+
results: raw_results.nil? ? nil : raw_results.map { |item| ValidateEmailResponse.from_hash(item) },
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Profile of the authenticated API key.
|
|
129
|
+
ApiKeyProfileResponse = Data.define(
|
|
130
|
+
:id,
|
|
131
|
+
:email,
|
|
132
|
+
# "free" | "paid" | "enterprise"
|
|
133
|
+
:plan,
|
|
134
|
+
:is_active,
|
|
135
|
+
:requests_limit,
|
|
136
|
+
:requests_used,
|
|
137
|
+
:created_at,
|
|
138
|
+
) do
|
|
139
|
+
def self.from_hash(data)
|
|
140
|
+
new(
|
|
141
|
+
id: data["id"],
|
|
142
|
+
email: data["email"],
|
|
143
|
+
plan: data["plan"],
|
|
144
|
+
is_active: data["is_active"],
|
|
145
|
+
requests_limit: data["requests_limit"].to_i,
|
|
146
|
+
requests_used: data["requests_used"].to_i,
|
|
147
|
+
created_at: data["created_at"],
|
|
148
|
+
)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Monthly usage for the authenticated API key.
|
|
153
|
+
UsageResponse = Data.define(
|
|
154
|
+
# Month in YYYY-MM format.
|
|
155
|
+
:month,
|
|
156
|
+
:requests_used,
|
|
157
|
+
:requests_limit,
|
|
158
|
+
:remaining,
|
|
159
|
+
) do
|
|
160
|
+
def self.from_hash(data)
|
|
161
|
+
new(
|
|
162
|
+
month: data["month"],
|
|
163
|
+
requests_used: data["requests_used"].to_i,
|
|
164
|
+
requests_limit: data["requests_limit"].to_i,
|
|
165
|
+
remaining: data["remaining"].to_i,
|
|
166
|
+
)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Service health payload.
|
|
171
|
+
HealthResponse = Data.define(:status, :timestamp, :version) do
|
|
172
|
+
def self.from_hash(data)
|
|
173
|
+
new(
|
|
174
|
+
status: data["status"],
|
|
175
|
+
timestamp: data["timestamp"],
|
|
176
|
+
version: data["version"],
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
data/lib/apiddress.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: apiddress
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- APIddress
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Validate email addresses with the APIddress API. Stdlib net/http only,
|
|
13
|
+
Ruby 3.0+, automatic retries.
|
|
14
|
+
executables: []
|
|
15
|
+
extensions: []
|
|
16
|
+
extra_rdoc_files: []
|
|
17
|
+
files:
|
|
18
|
+
- LICENSE
|
|
19
|
+
- README.md
|
|
20
|
+
- lib/apiddress.rb
|
|
21
|
+
- lib/apiddress/client.rb
|
|
22
|
+
- lib/apiddress/error.rb
|
|
23
|
+
- lib/apiddress/models.rb
|
|
24
|
+
homepage: https://api.apiddress.com
|
|
25
|
+
licenses:
|
|
26
|
+
- MIT
|
|
27
|
+
metadata:
|
|
28
|
+
homepage_uri: https://api.apiddress.com
|
|
29
|
+
source_code_uri: https://github.com/apiddress/sdk-ruby
|
|
30
|
+
rdoc_options: []
|
|
31
|
+
require_paths:
|
|
32
|
+
- lib
|
|
33
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
34
|
+
requirements:
|
|
35
|
+
- - ">="
|
|
36
|
+
- !ruby/object:Gem::Version
|
|
37
|
+
version: '3.0'
|
|
38
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '0'
|
|
43
|
+
requirements: []
|
|
44
|
+
rubygems_version: 4.0.11
|
|
45
|
+
specification_version: 4
|
|
46
|
+
summary: Official Ruby SDK for the APIddress email validation API
|
|
47
|
+
test_files: []
|