truelist-rails 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 +261 -0
- data/lib/generators/truelist/install_generator.rb +26 -0
- data/lib/generators/truelist/templates/initializer.rb +26 -0
- data/lib/truelist/client.rb +125 -0
- data/lib/truelist/configuration.rb +23 -0
- data/lib/truelist/railtie.rb +11 -0
- data/lib/truelist/result.rb +51 -0
- data/lib/truelist/validators/deliverable_validator.rb +25 -0
- data/lib/truelist/version.rb +5 -0
- data/lib/truelist.rb +34 -0
- metadata +87 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 8d9bf2a865512806998948b308b302c98a30edab2919c23ad3bd6c3b22d93191
|
|
4
|
+
data.tar.gz: 382931bfc3208ad233801783b070d92c998d529f6d7bdc7863b8292422471b4d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: '085a171c764b96fc40dbfb188f4391fca2e4be34195cbf98f9ef6f297a9642e955f035dd8a6b533e04f9d9bc165a5ee9698ae6817448b9c7ea5c3a295021d006'
|
|
7
|
+
data.tar.gz: e178b7747e44a6efcc31565f42fda5d0d2e23dd4922dcb4d0978dd6602190d978492b62ece467dfe63c8704c95390793777361e7566cdad16ff445b4f6249270
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Truelist
|
|
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,261 @@
|
|
|
1
|
+
# truelist-rails
|
|
2
|
+
|
|
3
|
+
[](https://truelist.io/pricing)
|
|
4
|
+
Email validation for Rails, powered by [Truelist.io](https://truelist.io).
|
|
5
|
+
|
|
6
|
+
[](https://badge.fury.io/rb/truelist-rails)
|
|
7
|
+
[](https://github.com/Truelist-io-Email-Validation/truelist-rails/actions/workflows/ci.yml)
|
|
8
|
+
|
|
9
|
+
Validate email deliverability in your Rails models with a single line:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
validates :email, deliverable: true
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Truelist checks whether an email address actually exists and can receive mail, catching typos, disposable addresses, and invalid mailboxes before they hit your database.
|
|
16
|
+
|
|
17
|
+
> **Start free** — 100 validations + 10 enhanced credits, no credit card required.
|
|
18
|
+
> [Get your API key →](https://app.truelist.io/signup?utm_source=github&utm_medium=readme&utm_campaign=free-plan&utm_content=truelist-rails)
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
Add to your Gemfile:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
gem "truelist-rails"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Then run:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
bundle install
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
### 1. Configure your API key
|
|
37
|
+
|
|
38
|
+
Set the `TRUELIST_API_KEY` environment variable, or run the install generator:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
rails generate truelist:install
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This creates `config/initializers/truelist.rb` where you can configure the gem.
|
|
45
|
+
|
|
46
|
+
### 2. Add the validator
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
class User < ApplicationRecord
|
|
50
|
+
validates :email, presence: true, deliverable: true
|
|
51
|
+
end
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
That's it. Invalid emails will now fail validation with a clear error message.
|
|
55
|
+
|
|
56
|
+
## Configuration
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
Truelist.configure do |config|
|
|
60
|
+
# Your Truelist API key (required).
|
|
61
|
+
# Defaults to ENV["TRUELIST_API_KEY"].
|
|
62
|
+
config.api_key = ENV["TRUELIST_API_KEY"]
|
|
63
|
+
|
|
64
|
+
# API base URL. Change only for testing or proxying.
|
|
65
|
+
config.base_url = "https://api.truelist.io"
|
|
66
|
+
|
|
67
|
+
# Request timeout in seconds.
|
|
68
|
+
config.timeout = 10
|
|
69
|
+
|
|
70
|
+
# When true, raises Truelist::Error on API failures.
|
|
71
|
+
# When false (default), returns an "unknown" result on errors,
|
|
72
|
+
# allowing the validation to pass gracefully.
|
|
73
|
+
config.raise_on_error = false
|
|
74
|
+
|
|
75
|
+
# Whether "accept_all" emails (domains that accept all addresses) pass validation.
|
|
76
|
+
config.allow_risky = true
|
|
77
|
+
|
|
78
|
+
# Optional cache store for validation results.
|
|
79
|
+
# Accepts any Rails-compatible cache store.
|
|
80
|
+
config.cache_store = Rails.cache
|
|
81
|
+
|
|
82
|
+
# How long to cache validation results (in seconds).
|
|
83
|
+
config.cache_ttl = 1.hour
|
|
84
|
+
end
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Validator Options
|
|
88
|
+
|
|
89
|
+
### Basic usage
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
validates :email, deliverable: true
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Reject accept_all emails
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
validates :email, deliverable: { allow_risky: false }
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Custom error message
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
validates :email, deliverable: { message: "is not a valid email address" }
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Combine options
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
validates :email, deliverable: { allow_risky: false, message: "doesn't look right" }
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Use with other validators
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
validates :email, presence: true,
|
|
117
|
+
format: { with: URI::MailTo::EMAIL_REGEXP },
|
|
118
|
+
deliverable: true
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The `deliverable` validator skips blank values, so pair it with `presence: true` if the field is required.
|
|
122
|
+
|
|
123
|
+
## Working with Results Directly
|
|
124
|
+
|
|
125
|
+
Use the client to validate emails outside of model validations:
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
result = Truelist.validate("user@example.com")
|
|
129
|
+
|
|
130
|
+
result.state # => "ok", "email_invalid", "accept_all", or "unknown"
|
|
131
|
+
result.sub_state # => "email_ok", "is_disposable", "is_role", etc.
|
|
132
|
+
result.valid? # => true when state is "ok" (or "accept_all" with allow_risky)
|
|
133
|
+
result.invalid? # => true when state is "email_invalid"
|
|
134
|
+
result.accept_all? # => true when state is "accept_all"
|
|
135
|
+
result.unknown? # => true when state is "unknown"
|
|
136
|
+
|
|
137
|
+
result.email # => the validated email address
|
|
138
|
+
result.suggestion # => suggested correction, if available
|
|
139
|
+
result.domain # => email domain
|
|
140
|
+
result.canonical # => local part of the email
|
|
141
|
+
result.mx_record # => MX record for the domain
|
|
142
|
+
result.first_name # => first name, if available
|
|
143
|
+
result.last_name # => last name, if available
|
|
144
|
+
result.verified_at # => timestamp of verification
|
|
145
|
+
result.disposable? # => whether it's a disposable/temporary address (sub_state)
|
|
146
|
+
result.role? # => whether it's a role address (sub_state)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Account Info
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
client = Truelist::Client.new
|
|
153
|
+
account = client.account
|
|
154
|
+
|
|
155
|
+
account["email"] # => "team@company.com"
|
|
156
|
+
account["name"] # => "Team Lead"
|
|
157
|
+
account["uuid"] # => "a3828d19-..."
|
|
158
|
+
account["account"]["payment_plan"] # => "pro"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Sub-states
|
|
162
|
+
|
|
163
|
+
| Sub-state | Meaning |
|
|
164
|
+
|-----------|---------|
|
|
165
|
+
| `email_ok` | Email is valid and deliverable |
|
|
166
|
+
| `is_disposable` | Disposable/temporary email |
|
|
167
|
+
| `is_role` | Role-based address (info@, admin@) |
|
|
168
|
+
| `failed_smtp_check` | SMTP check failed |
|
|
169
|
+
| `failed_mx_check` | Domain has no mail server |
|
|
170
|
+
| `failed_spam_trap` | Known spam trap address |
|
|
171
|
+
| `failed_no_mailbox` | Mailbox does not exist |
|
|
172
|
+
| `failed_greylisted` | Server temporarily rejected (greylisting) |
|
|
173
|
+
| `failed_syntax_check` | Email format is invalid |
|
|
174
|
+
| `unknown_error` | Could not determine status |
|
|
175
|
+
|
|
176
|
+
## Caching
|
|
177
|
+
|
|
178
|
+
Enable caching to avoid redundant API calls for recently validated emails:
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
Truelist.configure do |config|
|
|
182
|
+
config.cache_store = Rails.cache
|
|
183
|
+
config.cache_ttl = 1.hour
|
|
184
|
+
end
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The cache key is based on the lowercase, stripped email address. Any Rails-compatible cache store works (Redis, Memcached, file store, etc.).
|
|
188
|
+
|
|
189
|
+
## Error Handling
|
|
190
|
+
|
|
191
|
+
By default, API errors (timeouts, rate limits, server errors) return an `unknown` result, allowing validation to pass. This prevents your forms from breaking when the API is unreachable.
|
|
192
|
+
|
|
193
|
+
To raise exceptions instead:
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
Truelist.configure do |config|
|
|
197
|
+
config.raise_on_error = true
|
|
198
|
+
end
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Exception classes:
|
|
202
|
+
|
|
203
|
+
- `Truelist::Error` -- base error class
|
|
204
|
+
- `Truelist::ApiError` -- unexpected API responses
|
|
205
|
+
- `Truelist::AuthenticationError` -- invalid API key (401)
|
|
206
|
+
- `Truelist::RateLimitError` -- rate limit exceeded (429)
|
|
207
|
+
|
|
208
|
+
## Testing
|
|
209
|
+
|
|
210
|
+
Stub the API in your tests to avoid real HTTP calls. With WebMock:
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
# spec/support/truelist.rb
|
|
214
|
+
RSpec.configure do |config|
|
|
215
|
+
config.before do
|
|
216
|
+
stub_request(:post, "https://api.truelist.io/api/v1/verify_inline")
|
|
217
|
+
.with(query: hash_including(email: /.+/))
|
|
218
|
+
.to_return(
|
|
219
|
+
status: 200,
|
|
220
|
+
body: {
|
|
221
|
+
emails: [{
|
|
222
|
+
address: "user@example.com",
|
|
223
|
+
email_state: "ok",
|
|
224
|
+
email_sub_state: "email_ok"
|
|
225
|
+
}]
|
|
226
|
+
}.to_json,
|
|
227
|
+
headers: { "Content-Type" => "application/json" }
|
|
228
|
+
)
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Or stub at the client level:
|
|
234
|
+
|
|
235
|
+
```ruby
|
|
236
|
+
allow(Truelist::Client).to receive(:new).and_return(
|
|
237
|
+
instance_double(Truelist::Client, validate: Truelist::Result.new(email: "user@example.com", state: "ok"))
|
|
238
|
+
)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Requirements
|
|
242
|
+
|
|
243
|
+
- Ruby >= 3.0
|
|
244
|
+
- Rails >= 7.0 (ActiveModel / ActiveSupport)
|
|
245
|
+
|
|
246
|
+
## Development
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
git clone https://github.com/Truelist-io-Email-Validation/truelist-rails.git
|
|
250
|
+
cd truelist-rails
|
|
251
|
+
bundle install
|
|
252
|
+
bundle exec rspec
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
## Getting Started
|
|
257
|
+
|
|
258
|
+
Sign up for a [free Truelist account](https://app.truelist.io/signup?utm_source=github&utm_medium=readme&utm_campaign=free-plan&utm_content=truelist-rails) to get your API key. The free plan includes 100 validations and 10 enhanced credits — no credit card required.
|
|
259
|
+
## License
|
|
260
|
+
|
|
261
|
+
Released under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Truelist
|
|
4
|
+
module Generators
|
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
|
6
|
+
source_root File.expand_path('templates', __dir__)
|
|
7
|
+
|
|
8
|
+
desc 'Creates a Truelist initializer in config/initializers'
|
|
9
|
+
|
|
10
|
+
def create_initializer
|
|
11
|
+
template 'initializer.rb', 'config/initializers/truelist.rb'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def show_instructions
|
|
15
|
+
say ''
|
|
16
|
+
say 'Truelist initializer created at config/initializers/truelist.rb', :green
|
|
17
|
+
say ''
|
|
18
|
+
say 'Next steps:'
|
|
19
|
+
say ' 1. Set your API key via TRUELIST_API_KEY environment variable'
|
|
20
|
+
say ' or uncomment the api_key line in the initializer'
|
|
21
|
+
say ' 2. Add `validates :email, deliverable: true` to your models'
|
|
22
|
+
say ''
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Truelist.configure do |config|
|
|
4
|
+
# Your Truelist API key. Defaults to ENV["TRUELIST_API_KEY"].
|
|
5
|
+
# config.api_key = ENV["TRUELIST_API_KEY"]
|
|
6
|
+
|
|
7
|
+
# API base URL (change only for testing/proxying).
|
|
8
|
+
# config.base_url = "https://api.truelist.io"
|
|
9
|
+
|
|
10
|
+
# Request timeout in seconds.
|
|
11
|
+
# config.timeout = 10
|
|
12
|
+
|
|
13
|
+
# When true, raises Truelist::Error on API failures.
|
|
14
|
+
# When false (default), returns an "unknown" result on errors.
|
|
15
|
+
# config.raise_on_error = false
|
|
16
|
+
|
|
17
|
+
# Whether "accept_all" emails pass validation.
|
|
18
|
+
# config.allow_risky = true
|
|
19
|
+
|
|
20
|
+
# Optional cache store for validation results.
|
|
21
|
+
# Uses any Rails-compatible cache store (e.g., Rails.cache).
|
|
22
|
+
# config.cache_store = Rails.cache
|
|
23
|
+
|
|
24
|
+
# How long to cache validation results.
|
|
25
|
+
# config.cache_ttl = 1.hour
|
|
26
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'json'
|
|
6
|
+
|
|
7
|
+
module Truelist
|
|
8
|
+
class Client
|
|
9
|
+
def initialize(config: Truelist.configuration)
|
|
10
|
+
@config = config
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def validate(email)
|
|
14
|
+
cached = read_cache(email)
|
|
15
|
+
return cached if cached
|
|
16
|
+
|
|
17
|
+
result = perform_request(email)
|
|
18
|
+
write_cache(email, result) unless result.unknown?
|
|
19
|
+
result
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def account
|
|
23
|
+
uri = URI("#{@config.base_url}/me")
|
|
24
|
+
http = build_http(uri)
|
|
25
|
+
|
|
26
|
+
request = Net::HTTP::Get.new(uri)
|
|
27
|
+
request['Authorization'] = "Bearer #{@config.api_key!}"
|
|
28
|
+
request['Accept'] = 'application/json'
|
|
29
|
+
|
|
30
|
+
response = http.request(request)
|
|
31
|
+
handle_status!(response)
|
|
32
|
+
JSON.parse(response.body)
|
|
33
|
+
rescue Truelist::AuthenticationError
|
|
34
|
+
raise
|
|
35
|
+
rescue StandardError => e
|
|
36
|
+
raise e if @config.raise_on_error
|
|
37
|
+
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def perform_request(email)
|
|
44
|
+
uri = URI("#{@config.base_url}/api/v1/verify_inline")
|
|
45
|
+
uri.query = URI.encode_www_form(email: email)
|
|
46
|
+
http = build_http(uri)
|
|
47
|
+
|
|
48
|
+
request = Net::HTTP::Post.new(uri)
|
|
49
|
+
request['Authorization'] = "Bearer #{@config.api_key!}"
|
|
50
|
+
request['Accept'] = 'application/json'
|
|
51
|
+
|
|
52
|
+
response = http.request(request)
|
|
53
|
+
handle_response(email, response)
|
|
54
|
+
rescue Truelist::AuthenticationError
|
|
55
|
+
raise
|
|
56
|
+
rescue StandardError => e
|
|
57
|
+
handle_error(email, e)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def build_http(uri)
|
|
61
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
62
|
+
http.use_ssl = uri.scheme == 'https'
|
|
63
|
+
http.open_timeout = @config.timeout
|
|
64
|
+
http.read_timeout = @config.timeout
|
|
65
|
+
http
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def handle_status!(response)
|
|
69
|
+
case response.code.to_i
|
|
70
|
+
when 200 then nil
|
|
71
|
+
when 401
|
|
72
|
+
raise Truelist::AuthenticationError, 'Invalid API key. Check your Truelist API key configuration.'
|
|
73
|
+
else
|
|
74
|
+
raise Truelist::ApiError, "API returned #{response.code}: #{response.body}"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def handle_response(email, response)
|
|
79
|
+
case response.code.to_i
|
|
80
|
+
when 200
|
|
81
|
+
parse_success(email, response.body)
|
|
82
|
+
when 401
|
|
83
|
+
raise Truelist::AuthenticationError, 'Invalid API key. Check your Truelist API key configuration.'
|
|
84
|
+
when 429
|
|
85
|
+
handle_error(email, Truelist::RateLimitError.new('Rate limit exceeded'))
|
|
86
|
+
else
|
|
87
|
+
handle_error(email, Truelist::ApiError.new("API returned #{response.code}: #{response.body}"))
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def parse_success(email, body)
|
|
92
|
+
d = JSON.parse(body).dig('emails', 0) || {}
|
|
93
|
+
Result.new(
|
|
94
|
+
email: d['address'] || email, state: d['email_state'] || 'unknown',
|
|
95
|
+
sub_state: d['email_sub_state'], suggestion: d['did_you_mean'],
|
|
96
|
+
domain: d['domain'], canonical: d['canonical'], mx_record: d['mx_record'],
|
|
97
|
+
first_name: d['first_name'], last_name: d['last_name'], verified_at: d['verified_at']
|
|
98
|
+
)
|
|
99
|
+
rescue JSON::ParserError => e
|
|
100
|
+
handle_error(email, e)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def handle_error(email, error)
|
|
104
|
+
raise error if @config.raise_on_error
|
|
105
|
+
|
|
106
|
+
Result.new(email: email, state: 'unknown', error: true)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def cache_key(email)
|
|
110
|
+
"truelist:validation:#{email.downcase.strip}"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def read_cache(email)
|
|
114
|
+
return nil unless @config.cache_store
|
|
115
|
+
|
|
116
|
+
@config.cache_store.read(cache_key(email))
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def write_cache(email, result)
|
|
120
|
+
return unless @config.cache_store
|
|
121
|
+
|
|
122
|
+
@config.cache_store.write(cache_key(email), result, expires_in: @config.cache_ttl)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Truelist
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :api_key, :base_url, :timeout, :raise_on_error,
|
|
6
|
+
:allow_risky, :cache_store, :cache_ttl
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@api_key = ENV.fetch('TRUELIST_API_KEY', nil)
|
|
10
|
+
@base_url = 'https://api.truelist.io'
|
|
11
|
+
@timeout = 10
|
|
12
|
+
@raise_on_error = false
|
|
13
|
+
@allow_risky = true
|
|
14
|
+
@cache_store = nil
|
|
15
|
+
@cache_ttl = 3600 # 1 hour in seconds
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def api_key!
|
|
19
|
+
api_key || raise(Truelist::AuthenticationError,
|
|
20
|
+
'Truelist API key is not configured. Set TRUELIST_API_KEY or use Truelist.configure.')
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Truelist
|
|
4
|
+
class Result
|
|
5
|
+
attr_reader :email, :state, :sub_state, :suggestion, :domain, :canonical,
|
|
6
|
+
:mx_record, :first_name, :last_name, :verified_at, :error
|
|
7
|
+
|
|
8
|
+
def initialize(email:, state:, sub_state: nil, suggestion: nil, domain: nil, canonical: nil,
|
|
9
|
+
mx_record: nil, first_name: nil, last_name: nil, verified_at: nil, error: false)
|
|
10
|
+
@email = email
|
|
11
|
+
@state = state.to_s
|
|
12
|
+
@sub_state = sub_state&.to_s
|
|
13
|
+
@suggestion = suggestion
|
|
14
|
+
@domain = domain
|
|
15
|
+
@canonical = canonical
|
|
16
|
+
@mx_record = mx_record
|
|
17
|
+
@first_name = first_name
|
|
18
|
+
@last_name = last_name
|
|
19
|
+
@verified_at = verified_at
|
|
20
|
+
@error = error
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def valid?
|
|
24
|
+
state == 'ok' || (state == 'accept_all' && Truelist.configuration.allow_risky)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def invalid?
|
|
28
|
+
state == 'email_invalid'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def accept_all?
|
|
32
|
+
state == 'accept_all'
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def unknown?
|
|
36
|
+
state == 'unknown'
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def disposable?
|
|
40
|
+
sub_state == 'is_disposable'
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def role?
|
|
44
|
+
sub_state == 'is_role'
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def error?
|
|
48
|
+
@error
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class DeliverableValidator < ActiveModel::EachValidator
|
|
4
|
+
def validate_each(record, attribute, value)
|
|
5
|
+
return if value.blank?
|
|
6
|
+
|
|
7
|
+
result = Truelist::Client.new.validate(value)
|
|
8
|
+
|
|
9
|
+
allow_risky = if options.key?(:allow_risky)
|
|
10
|
+
options[:allow_risky]
|
|
11
|
+
else
|
|
12
|
+
Truelist.configuration.allow_risky
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Fail open for transient errors (timeouts, 500s) so forms still work when API is down
|
|
16
|
+
return if result.error?
|
|
17
|
+
|
|
18
|
+
deliverable = result.state == 'ok' || (result.state == 'accept_all' && allow_risky) || result.unknown?
|
|
19
|
+
|
|
20
|
+
return if deliverable
|
|
21
|
+
|
|
22
|
+
message = options[:message] || :invalid_email
|
|
23
|
+
record.errors.add(attribute, message)
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/truelist.rb
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'truelist/version'
|
|
4
|
+
require_relative 'truelist/configuration'
|
|
5
|
+
require_relative 'truelist/result'
|
|
6
|
+
require_relative 'truelist/client'
|
|
7
|
+
|
|
8
|
+
module Truelist
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
class ApiError < Error; end
|
|
11
|
+
class AuthenticationError < Error; end
|
|
12
|
+
class RateLimitError < Error; end
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
def configuration
|
|
16
|
+
@configuration ||= Configuration.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def configure
|
|
20
|
+
yield(configuration)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def reset_configuration!
|
|
24
|
+
@configuration = Configuration.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def validate(email)
|
|
28
|
+
Client.new.validate(email)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
require_relative 'truelist/railtie' if defined?(Rails::Railtie)
|
|
34
|
+
require_relative 'truelist/validators/deliverable_validator'
|
metadata
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: truelist-rails
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Truelist
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-04-08 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: activemodel
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '7.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '7.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: activesupport
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '7.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '7.0'
|
|
41
|
+
description: Rails integration for the Truelist.io email validation API. Validate
|
|
42
|
+
email deliverability with a simple ActiveModel validator.
|
|
43
|
+
email:
|
|
44
|
+
- support@truelist.io
|
|
45
|
+
executables: []
|
|
46
|
+
extensions: []
|
|
47
|
+
extra_rdoc_files: []
|
|
48
|
+
files:
|
|
49
|
+
- LICENSE
|
|
50
|
+
- README.md
|
|
51
|
+
- lib/generators/truelist/install_generator.rb
|
|
52
|
+
- lib/generators/truelist/templates/initializer.rb
|
|
53
|
+
- lib/truelist.rb
|
|
54
|
+
- lib/truelist/client.rb
|
|
55
|
+
- lib/truelist/configuration.rb
|
|
56
|
+
- lib/truelist/railtie.rb
|
|
57
|
+
- lib/truelist/result.rb
|
|
58
|
+
- lib/truelist/validators/deliverable_validator.rb
|
|
59
|
+
- lib/truelist/version.rb
|
|
60
|
+
homepage: https://github.com/Truelist-io-Email-Validation/truelist-rails
|
|
61
|
+
licenses:
|
|
62
|
+
- MIT
|
|
63
|
+
metadata:
|
|
64
|
+
homepage_uri: https://github.com/Truelist-io-Email-Validation/truelist-rails
|
|
65
|
+
source_code_uri: https://github.com/Truelist-io-Email-Validation/truelist-rails
|
|
66
|
+
changelog_uri: https://github.com/Truelist-io-Email-Validation/truelist-rails/blob/main/CHANGELOG.md
|
|
67
|
+
rubygems_mfa_required: 'true'
|
|
68
|
+
post_install_message:
|
|
69
|
+
rdoc_options: []
|
|
70
|
+
require_paths:
|
|
71
|
+
- lib
|
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
73
|
+
requirements:
|
|
74
|
+
- - ">="
|
|
75
|
+
- !ruby/object:Gem::Version
|
|
76
|
+
version: '3.0'
|
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
requirements: []
|
|
83
|
+
rubygems_version: 3.4.10
|
|
84
|
+
signing_key:
|
|
85
|
+
specification_version: 4
|
|
86
|
+
summary: Email validation for Rails, powered by Truelist.io
|
|
87
|
+
test_files: []
|