clean-email 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/README.md +345 -0
- data/lib/clean-email.rb +1 -0
- data/lib/clean_email/abstract_api_client.rb +59 -0
- data/lib/clean_email/active_model_validator.rb +33 -0
- data/lib/clean_email/adapters/active_record.rb +60 -0
- data/lib/clean_email/adapters/base.rb +43 -0
- data/lib/clean_email/adapters/memory.rb +30 -0
- data/lib/clean_email/configuration.rb +53 -0
- data/lib/clean_email/models/domain_cache.rb +8 -0
- data/lib/clean_email/models/email_cache.rb +8 -0
- data/lib/clean_email/reoon_client.rb +60 -0
- data/lib/clean_email/result.rb +53 -0
- data/lib/clean_email/validator.rb +88 -0
- data/lib/clean_email/version.rb +3 -0
- data/lib/clean_email/zerobounce_client.rb +71 -0
- data/lib/clean_email.rb +45 -0
- data/lib/generators/clean_email/install/install_generator.rb +32 -0
- metadata +128 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 4325a9205cc1e85f87c8791415b7845a0132c7b4a59fc87b5d1b7f62c816a34d
|
|
4
|
+
data.tar.gz: '048b1b3b60ff7b27f4500791f33961718011493a9282cc29f6a41da521dd0ad9'
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 547dba76b5a2a233a0ed2df32edd349a73ad1fde51335cf69c32db2f71d0432030d0dde6cb7995bc05b59ec53c3469e9f8b941b911e326ba5f06e9e7a916c63a
|
|
7
|
+
data.tar.gz: 822404c6745e72b017b044d2cb5605c9bb82164c0ddef7d389a79ef24e8d56bc0dce6076b943500ddfffa010e3bb4018049c8bf1853bfa71bc2ae5af9e0e488e
|
data/README.md
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
# clean-email
|
|
2
|
+
|
|
3
|
+
[](http://badge.fury.io/rb/clean-email)
|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
Ruby gem for email validation with support for multiple providers ([Reoon](https://reoon.com/email-verifier-api/), [ZeroBounce](https://www.zerobounce.net/), [AbstractAPI](https://www.abstractapi.com/email-validation-api)), with optional smart caching to minimize API usage.
|
|
7
|
+
|
|
8
|
+
Unlike SMTP-based validators, clean-email uses HTTP APIs backed by continuously updated datasets — making it faster, more reliable across all environments, and capable of detecting **disposable** and **spamtrap** addresses that SMTP handshakes simply cannot identify.
|
|
9
|
+
|
|
10
|
+
## How it works
|
|
11
|
+
|
|
12
|
+
Every validation follows this priority order:
|
|
13
|
+
|
|
14
|
+
1. **Invalid format** — malformed emails are rejected immediately, no cache, no API call
|
|
15
|
+
2. **Domain cache** — if the domain is already marked as `disposable` or `spamtrap`, it is rejected instantly without hitting the API
|
|
16
|
+
3. **Email cache** — if the full email was validated before, the cached result is returned
|
|
17
|
+
4. **Provider API** — only called when no cache is available; the result is stored for future use
|
|
18
|
+
|
|
19
|
+
The domain-level cache is the key to API savings: once `mailinator.com` is identified as disposable, any future email `@mailinator.com` is rejected without a single API call.
|
|
20
|
+
|
|
21
|
+
**No database?** The adapter is optional. Without one, every call goes straight to the provider API — no caching at all.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
Add to your `Gemfile`:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
gem "clean-email"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Then run:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
bundle install
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Setup
|
|
38
|
+
|
|
39
|
+
### Rails
|
|
40
|
+
|
|
41
|
+
Run the install generator to create the initializer and the migration:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
rails generate clean_email:install
|
|
45
|
+
rails db:migrate
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
This creates:
|
|
49
|
+
- `config/initializers/clean_email.rb` — configuration file
|
|
50
|
+
- `db/migrate/TIMESTAMP_create_clean_email_caches.rb` — tables for email and domain caching
|
|
51
|
+
|
|
52
|
+
Then configure your chosen provider in the initializer (see [Configuration](#configuration-options) below).
|
|
53
|
+
|
|
54
|
+
### Non-Rails projects
|
|
55
|
+
|
|
56
|
+
Require and configure the gem manually:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
require "clean_email"
|
|
60
|
+
|
|
61
|
+
CleanEmail.configure do |config|
|
|
62
|
+
config.provider = :reoon
|
|
63
|
+
config.reoon_api_key = ENV["REOON_API_KEY"]
|
|
64
|
+
config.adapter = MyAdapter.new # optional — see Adapter section
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Without a database
|
|
69
|
+
|
|
70
|
+
Simply omit the adapter. Every call will hit the provider API directly:
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
CleanEmail.configure do |config|
|
|
74
|
+
config.provider = :reoon
|
|
75
|
+
config.reoon_api_key = ENV["REOON_API_KEY"]
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Configuration options
|
|
80
|
+
|
|
81
|
+
| Option | Description | Default |
|
|
82
|
+
|---|---|---|
|
|
83
|
+
| `provider` | Which API to use: `:reoon`, `:zerobounce`, or `:abstractapi` | `:reoon` |
|
|
84
|
+
| `reoon_api_key` | Reoon API key. Required when `provider: :reoon`. | — |
|
|
85
|
+
| `reoon_mode` | `:quick` (faster) or `:power` (more accurate) | `:quick` |
|
|
86
|
+
| `zerobounce_api_key` | ZeroBounce API key. Required when `provider: :zerobounce`. | — |
|
|
87
|
+
| `abstractapi_key` | AbstractAPI key. Required when `provider: :abstractapi`. | — |
|
|
88
|
+
| `adapter` | An instance of `CleanEmail::Adapters::Base`. Optional. | `nil` |
|
|
89
|
+
| `blocked_statuses` | Statuses rejected by the ActiveModel/Devise validator. | `%w[invalid disposable spamtrap]` |
|
|
90
|
+
|
|
91
|
+
### Provider examples
|
|
92
|
+
|
|
93
|
+
**Reoon:**
|
|
94
|
+
```ruby
|
|
95
|
+
CleanEmail.configure do |config|
|
|
96
|
+
config.provider = :reoon
|
|
97
|
+
config.reoon_api_key = ENV["REOON_API_KEY"]
|
|
98
|
+
config.reoon_mode = :quick # or :power
|
|
99
|
+
config.adapter = CleanEmail::Adapters::ActiveRecord.new
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**ZeroBounce:**
|
|
104
|
+
```ruby
|
|
105
|
+
CleanEmail.configure do |config|
|
|
106
|
+
config.provider = :zerobounce
|
|
107
|
+
config.zerobounce_api_key = ENV["ZEROBOUNCE_API_KEY"]
|
|
108
|
+
config.adapter = CleanEmail::Adapters::ActiveRecord.new
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**AbstractAPI:**
|
|
113
|
+
```ruby
|
|
114
|
+
CleanEmail.configure do |config|
|
|
115
|
+
config.provider = :abstractapi
|
|
116
|
+
config.abstractapi_key = ENV["ABSTRACTAPI_KEY"]
|
|
117
|
+
config.adapter = CleanEmail::Adapters::ActiveRecord.new
|
|
118
|
+
end
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Usage
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
result = CleanEmail.validate("user@example.com")
|
|
125
|
+
|
|
126
|
+
result.valid? # => true / false
|
|
127
|
+
result.invalid? # => true / false
|
|
128
|
+
result.status # => "valid", "invalid", "disposable", "spamtrap", "unknown"
|
|
129
|
+
result.disposable? # => true / false
|
|
130
|
+
result.spamtrap? # => true / false
|
|
131
|
+
result.cached? # => true if result came from cache (no API call was made)
|
|
132
|
+
result.to_h # => { email:, status:, valid:, cached: }
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Or instantiate the validator directly to reuse the same instance:
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
validator = CleanEmail::Validator.new
|
|
139
|
+
|
|
140
|
+
validator.validate("a@example.com")
|
|
141
|
+
validator.validate("b@example.com")
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Normalized statuses
|
|
145
|
+
|
|
146
|
+
Regardless of which provider you use, all results are normalized to a common set of statuses:
|
|
147
|
+
|
|
148
|
+
| Status | Meaning | `valid?` |
|
|
149
|
+
|---|---|---|
|
|
150
|
+
| `valid` | Email exists and accepts messages | `true` |
|
|
151
|
+
| `invalid` | Invalid email or non-existent domain | `false` |
|
|
152
|
+
| `disposable` | Temporary / throwaway email service | `false` |
|
|
153
|
+
| `spamtrap` | Anti-spam trap address | `false` |
|
|
154
|
+
| `unknown` | Provider could not determine | `false` |
|
|
155
|
+
|
|
156
|
+
### Provider status mapping
|
|
157
|
+
|
|
158
|
+
**Reoon** → normalized:
|
|
159
|
+
|
|
160
|
+
| Reoon status | Normalized |
|
|
161
|
+
|---|---|
|
|
162
|
+
| `valid` | `valid` |
|
|
163
|
+
| `invalid` | `invalid` |
|
|
164
|
+
| `disposable` | `disposable` |
|
|
165
|
+
| `spamtrap` | `spamtrap` |
|
|
166
|
+
| `unknown` | `unknown` |
|
|
167
|
+
|
|
168
|
+
**ZeroBounce** → normalized:
|
|
169
|
+
|
|
170
|
+
| ZeroBounce status | Sub-status | Normalized |
|
|
171
|
+
|---|---|---|
|
|
172
|
+
| `valid` | — | `valid` |
|
|
173
|
+
| `invalid` | — | `invalid` |
|
|
174
|
+
| `catch-all` | — | `unknown` |
|
|
175
|
+
| `unknown` | — | `unknown` |
|
|
176
|
+
| `spamtrap` | — | `spamtrap` |
|
|
177
|
+
| `abuse` | — | `invalid` |
|
|
178
|
+
| `do_not_mail` | `disposable` | `disposable` |
|
|
179
|
+
| `do_not_mail` | other | `invalid` |
|
|
180
|
+
|
|
181
|
+
**AbstractAPI** → normalized:
|
|
182
|
+
|
|
183
|
+
| `is_disposable_email` | `deliverability` | Normalized |
|
|
184
|
+
|---|---|---|
|
|
185
|
+
| `true` | any | `disposable` |
|
|
186
|
+
| `false` | `DELIVERABLE` | `valid` |
|
|
187
|
+
| `false` | `UNDELIVERABLE` | `invalid` |
|
|
188
|
+
| `false` | `RISKY` / `UNKNOWN` | `unknown` |
|
|
189
|
+
|
|
190
|
+
## Adapter
|
|
191
|
+
|
|
192
|
+
The gem is storage-agnostic. You can implement `CleanEmail::Adapters::Base` with any backend, or use the built-in adapters.
|
|
193
|
+
|
|
194
|
+
### Built-in: ActiveRecord (Rails)
|
|
195
|
+
|
|
196
|
+
Generated automatically by `rails generate clean_email:install`. Creates two tables:
|
|
197
|
+
|
|
198
|
+
- `clean_email_email_caches` — caches full email results
|
|
199
|
+
- `clean_email_domain_caches` — caches disposable/spamtrap domains
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
config.adapter = CleanEmail::Adapters::ActiveRecord.new
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Built-in: Memory (tests and prototyping)
|
|
206
|
+
|
|
207
|
+
Data lives only for the duration of the process:
|
|
208
|
+
|
|
209
|
+
```ruby
|
|
210
|
+
config.adapter = CleanEmail::Adapters::Memory.new
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Custom adapter interface
|
|
214
|
+
|
|
215
|
+
Implement four methods:
|
|
216
|
+
|
|
217
|
+
```ruby
|
|
218
|
+
class MyAdapter < CleanEmail::Adapters::Base
|
|
219
|
+
# Persist a validated email result
|
|
220
|
+
def store_email(email, result) = raise NotImplementedError
|
|
221
|
+
# Return a CleanEmail::Result or nil
|
|
222
|
+
def find_email(email) = raise NotImplementedError
|
|
223
|
+
|
|
224
|
+
# Persist a domain marked as "disposable" or "spamtrap"
|
|
225
|
+
def store_domain(domain, status) = raise NotImplementedError
|
|
226
|
+
# Return "disposable", "spamtrap", or nil
|
|
227
|
+
def find_domain(domain) = raise NotImplementedError
|
|
228
|
+
end
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Redis example
|
|
232
|
+
|
|
233
|
+
```ruby
|
|
234
|
+
class RedisAdapter < CleanEmail::Adapters::Base
|
|
235
|
+
TTL_EMAIL = 7 * 24 * 60 * 60 # 7 days
|
|
236
|
+
TTL_DOMAIN = 30 * 24 * 60 * 60 # 30 days
|
|
237
|
+
|
|
238
|
+
def initialize(redis = Redis.current)
|
|
239
|
+
@redis = redis
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def store_email(email, result)
|
|
243
|
+
@redis.setex("email_valid:#{email}", TTL_EMAIL, result.status)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def find_email(email)
|
|
247
|
+
status = @redis.get("email_valid:#{email}")
|
|
248
|
+
return nil unless status
|
|
249
|
+
CleanEmail::Result.new(email: email, status: status)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def store_domain(domain, status)
|
|
253
|
+
@redis.setex("domain_valid:#{domain}", TTL_DOMAIN, status)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def find_domain(domain)
|
|
257
|
+
@redis.get("domain_valid:#{domain}")
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Devise / ActiveModel integration
|
|
263
|
+
|
|
264
|
+
The gem ships with an opt-in `ActiveModel` validator that can be used in any model — including Devise's `User`.
|
|
265
|
+
|
|
266
|
+
### Setup
|
|
267
|
+
|
|
268
|
+
Add `validates :email, clean_email: true` to your model:
|
|
269
|
+
|
|
270
|
+
```ruby
|
|
271
|
+
class User < ApplicationRecord
|
|
272
|
+
devise :database_authenticatable, :registerable, ...
|
|
273
|
+
|
|
274
|
+
validates :email, clean_email: true
|
|
275
|
+
end
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
The validator calls the configured provider API (with caching if an adapter is configured) and rejects any status listed in `blocked_statuses`.
|
|
279
|
+
|
|
280
|
+
The API is only called when necessary: on new records, or when the email attribute has actually changed. Saving other fields (e.g. name, avatar) does not trigger a new API call.
|
|
281
|
+
|
|
282
|
+
### Configuring which statuses are blocked
|
|
283
|
+
|
|
284
|
+
Set a global default in the initializer:
|
|
285
|
+
|
|
286
|
+
```ruby
|
|
287
|
+
CleanEmail.configure do |config|
|
|
288
|
+
config.provider = :zerobounce
|
|
289
|
+
config.zerobounce_api_key = ENV["ZEROBOUNCE_API_KEY"]
|
|
290
|
+
|
|
291
|
+
# Reject disposable and invalid emails; allow "unknown" through
|
|
292
|
+
config.blocked_statuses = %w[invalid disposable spamtrap]
|
|
293
|
+
end
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Or override per-model:
|
|
297
|
+
|
|
298
|
+
```ruby
|
|
299
|
+
# Only block outright invalid emails
|
|
300
|
+
validates :email, clean_email: { blocked_statuses: %w[invalid] }
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Custom error message
|
|
304
|
+
|
|
305
|
+
```ruby
|
|
306
|
+
validates :email, clean_email: { message: "is not accepted" }
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### API errors
|
|
310
|
+
|
|
311
|
+
If the provider API is unreachable, the validator rejects the email and logs a warning via `Rails.logger`. To change this behaviour, rescue `CleanEmail::ApiError` in a `before_validation` callback.
|
|
312
|
+
|
|
313
|
+
## Error handling
|
|
314
|
+
|
|
315
|
+
| Exception | When raised |
|
|
316
|
+
|---|---|
|
|
317
|
+
| `CleanEmail::ConfigurationError` | Required API key not set, or unknown provider |
|
|
318
|
+
| `CleanEmail::ApiError` | HTTP request failed or provider returned an invalid response |
|
|
319
|
+
|
|
320
|
+
```ruby
|
|
321
|
+
begin
|
|
322
|
+
result = CleanEmail.validate(email)
|
|
323
|
+
rescue CleanEmail::ApiError => e
|
|
324
|
+
# API failed — decide whether to reject or accept provisionally
|
|
325
|
+
Rails.logger.error("Email validation error: #{e.message}")
|
|
326
|
+
end
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## Reoon API modes
|
|
330
|
+
|
|
331
|
+
Only applicable when `provider: :reoon`:
|
|
332
|
+
|
|
333
|
+
- **`:quick`** — Fast validation based on syntax, DNS, and known lists. Recommended for real-time validation.
|
|
334
|
+
- **`:power`** — Full validation including SMTP verification. More accurate but slower. Best for batch processing.
|
|
335
|
+
|
|
336
|
+
## Development
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
bundle install
|
|
340
|
+
bundle exec rspec
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## License
|
|
344
|
+
|
|
345
|
+
MIT
|
data/lib/clean-email.rb
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require_relative "clean_email"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
require "faraday"
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
module CleanEmail
|
|
5
|
+
# Thin wrapper around the Abstract Email Validation API.
|
|
6
|
+
# https://docs.abstractapi.com/email-validation
|
|
7
|
+
class AbstractApiClient
|
|
8
|
+
BASE_URL = "https://emailreputation.abstractapi.com/v1/"
|
|
9
|
+
|
|
10
|
+
def initialize(api_key:)
|
|
11
|
+
@api_key = api_key
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @param email [String]
|
|
15
|
+
# @return [CleanEmail::Result]
|
|
16
|
+
# @raise [CleanEmail::ApiError]
|
|
17
|
+
def validate(email)
|
|
18
|
+
response = connection.get do |req|
|
|
19
|
+
req.params["api_key"] = @api_key
|
|
20
|
+
req.params["email"] = email
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
handle_response(email, response)
|
|
24
|
+
rescue Faraday::Error => e
|
|
25
|
+
raise ApiError, "AbstractAPI request failed: #{e.message}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def connection
|
|
31
|
+
@connection ||= Faraday.new(url: BASE_URL) do |f|
|
|
32
|
+
f.response :raise_error
|
|
33
|
+
f.options.timeout = 10
|
|
34
|
+
f.options.open_timeout = 5
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def handle_response(email, response)
|
|
39
|
+
body = JSON.parse(response.body)
|
|
40
|
+
status = extract_status(body)
|
|
41
|
+
Result.new(email: email, status: status, raw: body)
|
|
42
|
+
rescue JSON::ParserError => e
|
|
43
|
+
raise ApiError, "Invalid JSON from AbstractAPI: #{e.message}"
|
|
44
|
+
rescue Faraday::Error => e
|
|
45
|
+
raise ApiError, "AbstractAPI request failed: #{e.message}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def extract_status(body)
|
|
49
|
+
# Disposable check takes priority regardless of deliverability
|
|
50
|
+
return "disposable" if body.dig("is_disposable_email", "value") == true
|
|
51
|
+
|
|
52
|
+
case body["deliverability"].to_s.upcase
|
|
53
|
+
when "DELIVERABLE" then "valid"
|
|
54
|
+
when "UNDELIVERABLE" then "invalid"
|
|
55
|
+
else "unknown"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# ActiveModel/Devise integration for CleanEmail.
|
|
2
|
+
#
|
|
3
|
+
# Opt-in by adding to your model (e.g. User):
|
|
4
|
+
#
|
|
5
|
+
# validates :email, clean_email: true
|
|
6
|
+
#
|
|
7
|
+
# This calls the Reoon API (with caching if an adapter is configured) and
|
|
8
|
+
# rejects any status listed in CleanEmail.configuration.blocked_statuses.
|
|
9
|
+
#
|
|
10
|
+
# You can override blocked statuses per-validator:
|
|
11
|
+
#
|
|
12
|
+
# validates :email, clean_email: { blocked_statuses: %w[invalid] }
|
|
13
|
+
#
|
|
14
|
+
# Or customise the error message:
|
|
15
|
+
#
|
|
16
|
+
# validates :email, clean_email: { message: "is not accepted" }
|
|
17
|
+
#
|
|
18
|
+
class CleanEmailValidator < ActiveModel::EachValidator
|
|
19
|
+
def validate_each(record, attribute, value)
|
|
20
|
+
return if record.persisted? && !record.will_save_change_to_attribute?(attribute)
|
|
21
|
+
|
|
22
|
+
result = CleanEmail.validate(value.to_s)
|
|
23
|
+
|
|
24
|
+
blocked = options.fetch(:blocked_statuses, CleanEmail.configuration.blocked_statuses)
|
|
25
|
+
|
|
26
|
+
if blocked.include?(result.status)
|
|
27
|
+
record.errors.add(attribute, options[:message] || :invalid)
|
|
28
|
+
end
|
|
29
|
+
rescue CleanEmail::ApiError => e
|
|
30
|
+
record.errors.add(attribute, options[:message] || :invalid)
|
|
31
|
+
Rails.logger.warn("[CleanEmail] API error during validation: #{e.message}") if defined?(Rails)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
require_relative "base"
|
|
2
|
+
|
|
3
|
+
module CleanEmail
|
|
4
|
+
module Adapters
|
|
5
|
+
# ActiveRecord adapter. Requires the tables created by the install generator:
|
|
6
|
+
#
|
|
7
|
+
# rails generate clean_email:install
|
|
8
|
+
# rails db:migrate
|
|
9
|
+
#
|
|
10
|
+
# Then configure:
|
|
11
|
+
#
|
|
12
|
+
# CleanEmail.configure do |c|
|
|
13
|
+
# c.adapter = CleanEmail::Adapters::ActiveRecord.new
|
|
14
|
+
# end
|
|
15
|
+
class ActiveRecord < Base
|
|
16
|
+
EMAIL_MODEL = "CleanEmail::EmailCache"
|
|
17
|
+
DOMAIN_MODEL = "CleanEmail::DomainCache"
|
|
18
|
+
|
|
19
|
+
def store_email(email, result)
|
|
20
|
+
email_model.upsert(
|
|
21
|
+
{ email: email, status: result.status },
|
|
22
|
+
unique_by: :email
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def find_email(email)
|
|
27
|
+
row = email_model.find_by(email: email)
|
|
28
|
+
return nil unless row
|
|
29
|
+
Result.new(email: row.email, status: row.status)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def store_domain(domain, status)
|
|
33
|
+
domain_model.upsert(
|
|
34
|
+
{ domain: domain, status: status },
|
|
35
|
+
unique_by: :domain
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def find_domain(domain)
|
|
40
|
+
domain_model.find_by(domain: domain)&.status
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def email_model
|
|
46
|
+
Object.const_get(EMAIL_MODEL)
|
|
47
|
+
rescue NameError
|
|
48
|
+
raise ConfigurationError,
|
|
49
|
+
"#{EMAIL_MODEL} model not found. Did you run `rails generate clean_email:install` and `rails db:migrate`?"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def domain_model
|
|
53
|
+
Object.const_get(DOMAIN_MODEL)
|
|
54
|
+
rescue NameError
|
|
55
|
+
raise ConfigurationError,
|
|
56
|
+
"#{DOMAIN_MODEL} model not found. Did you run `rails generate clean_email:install` and `rails db:migrate`?"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module CleanEmail
|
|
2
|
+
module Adapters
|
|
3
|
+
# Base interface for storage adapters.
|
|
4
|
+
#
|
|
5
|
+
# Implement this in your application to plug in any storage backend
|
|
6
|
+
# (ActiveRecord, Redis, SQLite, etc.).
|
|
7
|
+
#
|
|
8
|
+
# All methods must be implemented by subclasses.
|
|
9
|
+
class Base
|
|
10
|
+
# Persist a validated email result.
|
|
11
|
+
#
|
|
12
|
+
# @param email [String] the full email address
|
|
13
|
+
# @param result [CleanEmail::Result]
|
|
14
|
+
def store_email(email, result)
|
|
15
|
+
raise NotImplementedError, "#{self.class}#store_email not implemented"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Retrieve a cached result for an email.
|
|
19
|
+
#
|
|
20
|
+
# @param email [String]
|
|
21
|
+
# @return [CleanEmail::Result, nil] nil if not cached
|
|
22
|
+
def find_email(email)
|
|
23
|
+
raise NotImplementedError, "#{self.class}#find_email not implemented"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Persist a domain marked as disposable or spamtrap.
|
|
27
|
+
#
|
|
28
|
+
# @param domain [String] e.g. "mailinator.com"
|
|
29
|
+
# @param status [String] "disposable" or "spamtrap"
|
|
30
|
+
def store_domain(domain, status)
|
|
31
|
+
raise NotImplementedError, "#{self.class}#store_domain not implemented"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Retrieve a cached domain status.
|
|
35
|
+
#
|
|
36
|
+
# @param domain [String]
|
|
37
|
+
# @return [String, nil] "disposable", "spamtrap", or nil if not cached
|
|
38
|
+
def find_domain(domain)
|
|
39
|
+
raise NotImplementedError, "#{self.class}#find_domain not implemented"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require_relative "base"
|
|
2
|
+
|
|
3
|
+
module CleanEmail
|
|
4
|
+
module Adapters
|
|
5
|
+
# In-memory adapter. Useful for tests and quick prototyping.
|
|
6
|
+
# Data is lost when the process exits.
|
|
7
|
+
class Memory < Base
|
|
8
|
+
def initialize
|
|
9
|
+
@emails = {}
|
|
10
|
+
@domains = {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def store_email(email, result)
|
|
14
|
+
@emails[email.downcase] = result
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def find_email(email)
|
|
18
|
+
@emails[email.downcase]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def store_domain(domain, status)
|
|
22
|
+
@domains[domain.downcase] = status
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def find_domain(domain)
|
|
26
|
+
@domains[domain.downcase]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module CleanEmail
|
|
2
|
+
class Configuration
|
|
3
|
+
PROVIDERS = %i[reoon zerobounce abstractapi].freeze
|
|
4
|
+
|
|
5
|
+
# Which provider to use: :reoon, :zerobounce, or :abstractapi
|
|
6
|
+
attr_accessor :provider
|
|
7
|
+
|
|
8
|
+
# Reoon-specific options
|
|
9
|
+
attr_accessor :reoon_api_key
|
|
10
|
+
attr_accessor :reoon_mode
|
|
11
|
+
|
|
12
|
+
# ZeroBounce-specific options
|
|
13
|
+
attr_accessor :zerobounce_api_key
|
|
14
|
+
|
|
15
|
+
# AbstractAPI-specific options
|
|
16
|
+
attr_accessor :abstractapi_key
|
|
17
|
+
|
|
18
|
+
# An object implementing CleanEmail::Adapters::Base
|
|
19
|
+
attr_accessor :adapter
|
|
20
|
+
|
|
21
|
+
# Statuses that cause the ActiveModel validator to reject the email.
|
|
22
|
+
# Used by `validates :email, clean_email: true` in Devise (or any model).
|
|
23
|
+
# Possible values: "invalid", "disposable", "spamtrap", "unknown"
|
|
24
|
+
attr_accessor :blocked_statuses
|
|
25
|
+
|
|
26
|
+
def initialize
|
|
27
|
+
@provider = :reoon
|
|
28
|
+
@reoon_mode = :quick
|
|
29
|
+
@adapter = nil
|
|
30
|
+
@reoon_api_key = nil
|
|
31
|
+
@zerobounce_api_key = nil
|
|
32
|
+
@abstractapi_key = nil
|
|
33
|
+
@blocked_statuses = %w[invalid disposable spamtrap]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def validate!
|
|
37
|
+
case provider.to_sym
|
|
38
|
+
when :reoon
|
|
39
|
+
raise ConfigurationError, "reoon_api_key is required" if reoon_api_key.nil? || reoon_api_key.empty?
|
|
40
|
+
when :zerobounce
|
|
41
|
+
raise ConfigurationError, "zerobounce_api_key is required" if zerobounce_api_key.nil? || zerobounce_api_key.empty?
|
|
42
|
+
when :abstractapi
|
|
43
|
+
raise ConfigurationError, "abstractapi_key is required" if abstractapi_key.nil? || abstractapi_key.empty?
|
|
44
|
+
else
|
|
45
|
+
raise ConfigurationError, "Unknown provider: #{provider}. Use one of: #{PROVIDERS.join(', ')}"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def adapter?
|
|
50
|
+
!adapter.nil?
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
require "faraday"
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
module CleanEmail
|
|
5
|
+
# Thin wrapper around the Reoon Email Verifier API.
|
|
6
|
+
# https://reoon.com/email-verifier-api/
|
|
7
|
+
class ReoonClient
|
|
8
|
+
BASE_URL = "https://emailverifier.reoon.com/api/v1/verify"
|
|
9
|
+
|
|
10
|
+
VALID_STATUSES = %w[valid invalid disposable spamtrap unknown].freeze
|
|
11
|
+
|
|
12
|
+
def initialize(api_key:, mode: :quick)
|
|
13
|
+
@api_key = api_key
|
|
14
|
+
@mode = mode.to_s # "quick" or "power"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @param email [String]
|
|
18
|
+
# @return [CleanEmail::Result]
|
|
19
|
+
# @raise [CleanEmail::ApiError]
|
|
20
|
+
def validate(email)
|
|
21
|
+
response = connection.get do |req|
|
|
22
|
+
req.params["email"] = email
|
|
23
|
+
req.params["key"] = @api_key
|
|
24
|
+
req.params["mode"] = @mode
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
handle_response(email, response)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def connection
|
|
33
|
+
@connection ||= Faraday.new(url: BASE_URL) do |f|
|
|
34
|
+
f.response :raise_error
|
|
35
|
+
f.options.timeout = 10
|
|
36
|
+
f.options.open_timeout = 5
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def handle_response(email, response)
|
|
41
|
+
body = JSON.parse(response.body)
|
|
42
|
+
|
|
43
|
+
# Reoon wraps results; extract the status field
|
|
44
|
+
status = extract_status(body)
|
|
45
|
+
|
|
46
|
+
Result.new(email: email, status: status, raw: body)
|
|
47
|
+
rescue JSON::ParserError => e
|
|
48
|
+
raise ApiError, "Invalid JSON from Reoon: #{e.message}"
|
|
49
|
+
rescue Faraday::Error => e
|
|
50
|
+
raise ApiError, "Reoon API request failed: #{e.message}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def extract_status(body)
|
|
54
|
+
# Reoon v1 response shape:
|
|
55
|
+
# { "status": "valid"|"invalid"|"disposable"|"spamtrap"|..., ... }
|
|
56
|
+
raw_status = body["status"].to_s.downcase
|
|
57
|
+
VALID_STATUSES.include?(raw_status) ? raw_status : "unknown"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module CleanEmail
|
|
2
|
+
# Wraps the outcome of an email validation check.
|
|
3
|
+
class Result
|
|
4
|
+
# Reoon statuses that make an email unusable
|
|
5
|
+
BAD_STATUSES = %w[disposable spamtrap].freeze
|
|
6
|
+
|
|
7
|
+
attr_reader :email, :status, :raw
|
|
8
|
+
|
|
9
|
+
# @param email [String] the validated email
|
|
10
|
+
# @param status [String] Reoon status: "valid", "invalid", "disposable",
|
|
11
|
+
# "spamtrap", "unknown", etc.
|
|
12
|
+
# @param raw [Hash] the full Reoon API response (may be nil for
|
|
13
|
+
# domain-level cache hits)
|
|
14
|
+
def initialize(email:, status:, raw: nil)
|
|
15
|
+
@email = email
|
|
16
|
+
@status = status
|
|
17
|
+
@raw = raw
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def valid?
|
|
21
|
+
status == "valid"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def invalid?
|
|
25
|
+
!valid?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def disposable?
|
|
29
|
+
status == "disposable"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def spamtrap?
|
|
33
|
+
status == "spamtrap"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# True when the domain alone is enough to reject (no API call needed next time)
|
|
37
|
+
def bad_domain?
|
|
38
|
+
BAD_STATUSES.include?(status)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def cached?
|
|
42
|
+
raw.nil?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def to_h
|
|
46
|
+
{ email: email, status: status, valid: valid?, cached: cached? }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def inspect
|
|
50
|
+
"#<CleanEmail::Result email=#{email.inspect} status=#{status.inspect} valid=#{valid?}>"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
module CleanEmail
|
|
2
|
+
class Validator
|
|
3
|
+
BASIC_FORMAT = /\A[^@\s]+@[^@\s]+\.[^@\s]+\z/
|
|
4
|
+
|
|
5
|
+
def initialize(config = CleanEmail.configuration)
|
|
6
|
+
@config = config
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Validate an email address.
|
|
10
|
+
#
|
|
11
|
+
# Resolution order (when an adapter is configured):
|
|
12
|
+
# 1. Reject malformed emails immediately (no cache, no API call)
|
|
13
|
+
# 2. If domain is cached as disposable/spamtrap → reject instantly
|
|
14
|
+
# 3. If full email is cached → return cached result
|
|
15
|
+
# 4. Call the configured provider API → cache result → return
|
|
16
|
+
#
|
|
17
|
+
# Without an adapter every call goes straight to the provider API.
|
|
18
|
+
#
|
|
19
|
+
# @param email [String]
|
|
20
|
+
# @return [CleanEmail::Result]
|
|
21
|
+
def validate(email)
|
|
22
|
+
@config.validate!
|
|
23
|
+
|
|
24
|
+
email = email.to_s.strip.downcase
|
|
25
|
+
|
|
26
|
+
return malformed_result(email) unless looks_like_email?(email)
|
|
27
|
+
|
|
28
|
+
return client.validate(email) unless adapter?
|
|
29
|
+
|
|
30
|
+
domain = extract_domain(email)
|
|
31
|
+
|
|
32
|
+
# 1. Domain-level cache (cheapest check)
|
|
33
|
+
if (domain_status = adapter.find_domain(domain))
|
|
34
|
+
return Result.new(email: email, status: domain_status)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# 2. Full email cache
|
|
38
|
+
if (cached = adapter.find_email(email))
|
|
39
|
+
return cached
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# 3. API call
|
|
43
|
+
result = client.validate(email)
|
|
44
|
+
|
|
45
|
+
# Cache at domain level for disposable/spamtrap — any future email
|
|
46
|
+
# from this domain gets rejected without hitting the API
|
|
47
|
+
adapter.store_domain(domain, result.status) if result.bad_domain?
|
|
48
|
+
adapter.store_email(email, result)
|
|
49
|
+
|
|
50
|
+
result
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def client
|
|
56
|
+
@client ||= case @config.provider.to_sym
|
|
57
|
+
when :reoon
|
|
58
|
+
ReoonClient.new(api_key: @config.reoon_api_key, mode: @config.reoon_mode)
|
|
59
|
+
when :zerobounce
|
|
60
|
+
ZeroBounceClient.new(api_key: @config.zerobounce_api_key)
|
|
61
|
+
when :abstractapi
|
|
62
|
+
AbstractApiClient.new(api_key: @config.abstractapi_key)
|
|
63
|
+
else
|
|
64
|
+
raise ConfigurationError, "Unknown provider: #{@config.provider}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def adapter
|
|
69
|
+
@config.adapter
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def adapter?
|
|
73
|
+
@config.adapter?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def looks_like_email?(email)
|
|
77
|
+
BASIC_FORMAT.match?(email)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def extract_domain(email)
|
|
81
|
+
email.split("@", 2).last
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def malformed_result(email)
|
|
85
|
+
Result.new(email: email, status: "invalid")
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
require "faraday"
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
module CleanEmail
|
|
5
|
+
# Thin wrapper around the ZeroBounce Email Validation API.
|
|
6
|
+
# https://www.zerobounce.net/docs/email-validation-api-quickstart/
|
|
7
|
+
class ZeroBounceClient
|
|
8
|
+
BASE_URL = "https://api.zerobounce.net/v2/validate"
|
|
9
|
+
|
|
10
|
+
# ZeroBounce status → normalized CleanEmail status
|
|
11
|
+
STATUS_MAP = {
|
|
12
|
+
"valid" => "valid",
|
|
13
|
+
"invalid" => "invalid",
|
|
14
|
+
"catch-all" => "unknown",
|
|
15
|
+
"unknown" => "unknown",
|
|
16
|
+
"spamtrap" => "spamtrap",
|
|
17
|
+
"abuse" => "invalid",
|
|
18
|
+
"do_not_mail" => "invalid"
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
def initialize(api_key:)
|
|
22
|
+
@api_key = api_key
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @param email [String]
|
|
26
|
+
# @return [CleanEmail::Result]
|
|
27
|
+
# @raise [CleanEmail::ApiError]
|
|
28
|
+
def validate(email)
|
|
29
|
+
response = connection.get do |req|
|
|
30
|
+
req.params["api_key"] = @api_key
|
|
31
|
+
req.params["email"] = email
|
|
32
|
+
req.params["ip_address"] = ""
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
handle_response(email, response)
|
|
36
|
+
rescue Faraday::Error => e
|
|
37
|
+
raise ApiError, "ZeroBounce API request failed: #{e.message}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def connection
|
|
43
|
+
@connection ||= Faraday.new(url: BASE_URL) do |f|
|
|
44
|
+
f.response :raise_error
|
|
45
|
+
f.options.timeout = 10
|
|
46
|
+
f.options.open_timeout = 5
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def handle_response(email, response)
|
|
51
|
+
body = JSON.parse(response.body)
|
|
52
|
+
status = extract_status(body)
|
|
53
|
+
Result.new(email: email, status: status, raw: body)
|
|
54
|
+
rescue JSON::ParserError => e
|
|
55
|
+
raise ApiError, "Invalid JSON from ZeroBounce: #{e.message}"
|
|
56
|
+
rescue Faraday::Error => e
|
|
57
|
+
raise ApiError, "ZeroBounce API request failed: #{e.message}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def extract_status(body)
|
|
61
|
+
raw_status = body["status"].to_s.downcase
|
|
62
|
+
|
|
63
|
+
# do_not_mail with sub_status "disposable" is a disposable address
|
|
64
|
+
if raw_status == "do_not_mail" && body["sub_status"].to_s.downcase == "disposable"
|
|
65
|
+
return "disposable"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
STATUS_MAP.fetch(raw_status, "unknown")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
data/lib/clean_email.rb
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require_relative "clean_email/version"
|
|
2
|
+
require_relative "clean_email/configuration"
|
|
3
|
+
require_relative "clean_email/result"
|
|
4
|
+
require_relative "clean_email/reoon_client"
|
|
5
|
+
require_relative "clean_email/zerobounce_client"
|
|
6
|
+
require_relative "clean_email/abstract_api_client"
|
|
7
|
+
require_relative "clean_email/validator"
|
|
8
|
+
require_relative "clean_email/adapters/base"
|
|
9
|
+
require_relative "clean_email/adapters/memory"
|
|
10
|
+
|
|
11
|
+
if defined?(ActiveRecord)
|
|
12
|
+
require_relative "clean_email/adapters/active_record"
|
|
13
|
+
require_relative "clean_email/models/email_cache"
|
|
14
|
+
require_relative "clean_email/models/domain_cache"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# ActiveModel validator (enables `validates :email, clean_email: true` in
|
|
18
|
+
# Devise models or any ActiveModel-backed class).
|
|
19
|
+
require_relative "clean_email/active_model_validator" if defined?(ActiveModel)
|
|
20
|
+
|
|
21
|
+
module CleanEmail
|
|
22
|
+
class Error < StandardError; end
|
|
23
|
+
class ConfigurationError < Error; end
|
|
24
|
+
class ApiError < Error; end
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
def configuration
|
|
28
|
+
@configuration ||= Configuration.new
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def configure
|
|
32
|
+
yield configuration
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def reset_configuration!
|
|
36
|
+
@configuration = Configuration.new
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Convenience method: CleanEmail.validate("user@example.com")
|
|
40
|
+
# Uses the globally configured validator.
|
|
41
|
+
def validate(email)
|
|
42
|
+
Validator.new.validate(email)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
require "rails/generators/active_record"
|
|
3
|
+
|
|
4
|
+
module CleanEmail
|
|
5
|
+
module Generators
|
|
6
|
+
class InstallGenerator < Rails::Generators::Base
|
|
7
|
+
include ActiveRecord::Generators::Migration
|
|
8
|
+
|
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
|
10
|
+
|
|
11
|
+
desc "Creates a CleanEmail initializer and ActiveRecord migration."
|
|
12
|
+
|
|
13
|
+
def create_initializer
|
|
14
|
+
template "initializer.rb.tt", "config/initializers/clean_email.rb"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def copy_migration
|
|
18
|
+
migration_template(
|
|
19
|
+
"migration.rb.tt",
|
|
20
|
+
"db/migrate/create_clean_email_caches.rb",
|
|
21
|
+
migration_version: migration_version
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def migration_version
|
|
28
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: clean-email
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- TapGoods
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: faraday
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rspec
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: webmock
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: simplecov
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0.22'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0.22'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rubocop-rails-omakase
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
description: |
|
|
83
|
+
Validates emails via the Reoon API while minimizing API usage through
|
|
84
|
+
storage-agnostic caching. Disposable and spamtrap domains are cached
|
|
85
|
+
at the domain level so subsequent emails from the same domain are
|
|
86
|
+
rejected instantly without an API call.
|
|
87
|
+
executables: []
|
|
88
|
+
extensions: []
|
|
89
|
+
extra_rdoc_files: []
|
|
90
|
+
files:
|
|
91
|
+
- README.md
|
|
92
|
+
- lib/clean-email.rb
|
|
93
|
+
- lib/clean_email.rb
|
|
94
|
+
- lib/clean_email/abstract_api_client.rb
|
|
95
|
+
- lib/clean_email/active_model_validator.rb
|
|
96
|
+
- lib/clean_email/adapters/active_record.rb
|
|
97
|
+
- lib/clean_email/adapters/base.rb
|
|
98
|
+
- lib/clean_email/adapters/memory.rb
|
|
99
|
+
- lib/clean_email/configuration.rb
|
|
100
|
+
- lib/clean_email/models/domain_cache.rb
|
|
101
|
+
- lib/clean_email/models/email_cache.rb
|
|
102
|
+
- lib/clean_email/reoon_client.rb
|
|
103
|
+
- lib/clean_email/result.rb
|
|
104
|
+
- lib/clean_email/validator.rb
|
|
105
|
+
- lib/clean_email/version.rb
|
|
106
|
+
- lib/clean_email/zerobounce_client.rb
|
|
107
|
+
- lib/generators/clean_email/install/install_generator.rb
|
|
108
|
+
licenses:
|
|
109
|
+
- MIT
|
|
110
|
+
metadata: {}
|
|
111
|
+
rdoc_options: []
|
|
112
|
+
require_paths:
|
|
113
|
+
- lib
|
|
114
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
115
|
+
requirements:
|
|
116
|
+
- - ">="
|
|
117
|
+
- !ruby/object:Gem::Version
|
|
118
|
+
version: '3.0'
|
|
119
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - ">="
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '0'
|
|
124
|
+
requirements: []
|
|
125
|
+
rubygems_version: 3.6.9
|
|
126
|
+
specification_version: 4
|
|
127
|
+
summary: Email validation using Reoon with smart local caching
|
|
128
|
+
test_files: []
|