nondisposable 0.1.0 → 0.3.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 +4 -4
- data/.simplecov +35 -0
- data/AGENTS.md +5 -0
- data/Appraisals +20 -0
- data/CHANGELOG.md +32 -1
- data/CLAUDE.md +5 -0
- data/README.md +59 -1
- data/Rakefile +10 -1
- data/app/models/nondisposable/disposable_domain.rb +42 -2
- data/context7.json +4 -0
- data/data/disposable_email_blocklist.conf +8201 -0
- data/gemfiles/rails_7.2.gemfile +18 -0
- data/gemfiles/rails_8.0.gemfile +18 -0
- data/gemfiles/rails_8.1.gemfile +18 -0
- data/lib/generators/nondisposable/install_generator.rb +2 -2
- data/lib/generators/nondisposable/templates/create_nondisposable_disposable_domains.rb.erb +9 -0
- data/lib/generators/nondisposable/templates/nondisposable.rb +9 -0
- data/lib/nondisposable/domain_list_updater.rb +98 -21
- data/lib/nondisposable/email_validator.rb +6 -2
- data/lib/nondisposable/engine.rb +0 -4
- data/lib/nondisposable/version.rb +1 -1
- data/lib/nondisposable.rb +34 -4
- metadata +12 -7
- data/lib/nondisposable/railtie.rb +0 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 691fa38ec02a652cd53aa77bd8fac7513553bfd5b0dac8ba0e224be41ff58fdd
|
|
4
|
+
data.tar.gz: f145924d4cb664331fc215ae0e562f2e4110ff04bf501ea52fddf5f7089de0f9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 97ce0386481ec1e70fa828ce3a18b0b2dce4a19855b34660c2abc782042d14512da0c77a7f09b6687a4f602a52d5c5d045ea8f9d9fa8f0e68a5123dbe067b36a
|
|
7
|
+
data.tar.gz: df2aaf590f850fe10fe9ee588861630721679f4bd2ae3bfb43a8cd6aab904bba2069cced44ce8fb60644e0f6ca21d679f3cf9593d1e90592a6f675029fcf8e0e
|
data/.simplecov
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SimpleCov configuration file (auto-loaded before test suite)
|
|
4
|
+
# This keeps test_helper.rb clean and follows best practices
|
|
5
|
+
|
|
6
|
+
SimpleCov.start do
|
|
7
|
+
# Use SimpleFormatter for terminal-only output (no HTML generation)
|
|
8
|
+
formatter SimpleCov::Formatter::SimpleFormatter
|
|
9
|
+
|
|
10
|
+
# Track coverage for the lib directory (gem source code)
|
|
11
|
+
add_filter "/test/"
|
|
12
|
+
|
|
13
|
+
# Track the lib and app directories
|
|
14
|
+
track_files "{lib,app}/**/*.rb"
|
|
15
|
+
|
|
16
|
+
# Enable branch coverage for more detailed metrics
|
|
17
|
+
enable_coverage :branch
|
|
18
|
+
|
|
19
|
+
# Set minimum coverage threshold to prevent coverage regression
|
|
20
|
+
minimum_coverage line: 90, branch: 90
|
|
21
|
+
|
|
22
|
+
# Disambiguate parallel test runs
|
|
23
|
+
command_name "Job #{ENV['TEST_ENV_NUMBER']}" if ENV['TEST_ENV_NUMBER']
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Print coverage summary to terminal after tests complete
|
|
27
|
+
SimpleCov.at_exit do
|
|
28
|
+
SimpleCov.result.format!
|
|
29
|
+
puts "\n" + "=" * 60
|
|
30
|
+
puts "COVERAGE SUMMARY"
|
|
31
|
+
puts "=" * 60
|
|
32
|
+
puts "Line Coverage: #{SimpleCov.result.covered_percent.round(2)}%"
|
|
33
|
+
puts "Branch Coverage: #{SimpleCov.result.coverage_statistics[:branch]&.percent&.round(2) || 'N/A'}%"
|
|
34
|
+
puts "=" * 60
|
|
35
|
+
end
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to AI Agents (like OpenAI's Codex, Cursor Agent, Claude Code, etc) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
Please go ahead and read the full context for this project at `.cursor/rules/0-overview.mdc` and `.cursor/rules/1-quality.mdc` now. Also read the README for a good overview of the project.
|
data/Appraisals
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Note: Rails < 7.2 is not compatible with Ruby 3.4
|
|
4
|
+
# (Logger became a bundled gem in Ruby 3.4, and only Rails 7.2+ handles this)
|
|
5
|
+
# See: https://stdgems.org/logger/
|
|
6
|
+
|
|
7
|
+
# Test against Rails 7.2 (minimum version compatible with Ruby 3.4)
|
|
8
|
+
appraise "rails-7.2" do
|
|
9
|
+
gem "rails", "~> 7.2.0"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Test against Rails 8.0
|
|
13
|
+
appraise "rails-8.0" do
|
|
14
|
+
gem "rails", "~> 8.0.0"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Test against Rails 8.1 (latest)
|
|
18
|
+
appraise "rails-8.1" do
|
|
19
|
+
gem "rails", "~> 8.1.2"
|
|
20
|
+
end
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,35 @@
|
|
|
1
|
-
## [
|
|
1
|
+
## [0.3.0] - 2026-08-09
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- **Bundled seed list**: the gem now ships a snapshot of the upstream blocklist (8,201 domains) and `DomainListUpdater.seed` populates the table from it when (and only when) the table is empty. The install generator's migration seeds automatically, and `DomainListUpdater.update` falls back to seeding when the remote fetch fails against an empty table. This closes the fresh-install fail-open window where every signup passed until the first successful remote update.
|
|
6
|
+
- **Parent-domain matching** (`config.check_parent_domains`, default `true`): an email at `x.tempmail.com` is now blocked when `tempmail.com` is on the list. The check walks up to 3 parent labels and never matches bare TLDs. Set to `false` to restore exact-only matching.
|
|
7
|
+
- **Configurable failure mode** (`config.on_check_failure`, default `:allow`): controls what happens when the disposable check itself raises (e.g. database hiccup). `:allow` lets the record through with a loud error log (availability-first); `:reject` preserves the previous behavior and error message. Invalid values raise `ArgumentError`.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **Behavior change**: a broken check no longer rejects signups by default (previously any `StandardError` during validation added an error to the record). Set `config.on_check_failure = :reject` to keep the old fail-closed behavior.
|
|
12
|
+
- **Behavior change**: subdomains of listed domains are now blocked by default (see parent-domain matching above). Set `config.check_parent_domains = false` for the old exact-only behavior.
|
|
13
|
+
- `DomainListUpdater` now uses 10s open/read timeouts on the fetch (previously Net::HTTP's 60s defaults) so a hung connection can't block the update job.
|
|
14
|
+
- Domain list updates use a single bulk `insert_all` instead of per-row `create`, and report any rows skipped by the unique index instead of silently swallowing them.
|
|
15
|
+
- Downloaded domains are normalized before insert: whitespace/CR stripped, blank lines dropped, case-insensitive dedupe. `additional_domains` and `excluded_domains` now match case-insensitively everywhere.
|
|
16
|
+
- An exact entry in `excluded_domains` always wins, so a specific subdomain can be allowlisted under a listed parent domain.
|
|
17
|
+
- `Nondisposable.configuration` is lazily initialized: apps without an initializer no longer fail every validation with "cannot check if it's disposable".
|
|
18
|
+
|
|
19
|
+
### Removed
|
|
20
|
+
|
|
21
|
+
- Dead `require 'open-uri'` in the domain list updater.
|
|
22
|
+
|
|
23
|
+
## [0.2.1] - 2026-01-17
|
|
24
|
+
|
|
25
|
+
- Add `[nondisposable]` prefix to all logger calls for better log identification
|
|
26
|
+
|
|
27
|
+
## [0.2.0] - 2026-01-16
|
|
28
|
+
|
|
29
|
+
- Fixed `NoMethodError` when email is `"@"` or malformed (empty domain after split)
|
|
30
|
+
- Removed non-existent asset references from engine (`nondisposable/application.css` and `.js`)
|
|
31
|
+
- Removed buggy `railtie.rb` file that attempted to include a class instead of a module
|
|
32
|
+
- Added comprehensive Minitest 6 test suite with 256 tests and 90%+ line / branch coverage
|
|
2
33
|
|
|
3
34
|
## [0.1.0] - 2024-09-25
|
|
4
35
|
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
Please go ahead and read the full context for this project at `.cursor/rules/0-overview.mdc` and `.cursor/rules/1-quality.mdc` now. Also read the README for a good overview of the project.
|
data/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# 🗑️ `nondisposable` - Block disposable email addresses from signing up to your Rails app
|
|
2
2
|
|
|
3
|
+
[](https://badge.fury.io/rb/nondisposable) [](https://github.com/rameerez/nondisposable/actions)
|
|
4
|
+
|
|
5
|
+
> [!TIP]
|
|
6
|
+
> **🚀 Ship your next Rails app 10x faster!** I've built **[RailsFast](https://railsfast.com/?ref=nondisposable)**, a production-ready Rails boilerplate template that comes with everything you need to launch a software business in days, not weeks. Go [check it out](https://railsfast.com/?ref=nondisposable)!
|
|
7
|
+
|
|
3
8
|
`nondisposable` is a Ruby gem that prevents users from signing up to your Rails app with disposable email addresses.
|
|
4
9
|
|
|
5
10
|
Simply add to your User model:
|
|
@@ -38,7 +43,7 @@ This will create the necessary migration file, initializer, and a job for schedu
|
|
|
38
43
|
rails db:migrate
|
|
39
44
|
```
|
|
40
45
|
|
|
41
|
-
|
|
46
|
+
The migration also seeds the table from a snapshot of the disposable-domain list bundled with the gem, so your app is protected immediately. To fetch the very latest list, run:
|
|
42
47
|
|
|
43
48
|
```ruby
|
|
44
49
|
Nondisposable::DomainListUpdater.update
|
|
@@ -89,9 +94,28 @@ Nondisposable.configure do |config|
|
|
|
89
94
|
config.error_message = "provider is not allowed. Please use a non-disposable email address."
|
|
90
95
|
config.additional_domains = ['custom-disposable-domain.com']
|
|
91
96
|
config.excluded_domains = ['false-positive-domain.com']
|
|
97
|
+
|
|
98
|
+
# What to do when the disposable check itself fails (e.g. database hiccup):
|
|
99
|
+
# :allow - let the signup through and log an error (availability-first, default)
|
|
100
|
+
# :reject - block the signup with a validation error (fail closed)
|
|
101
|
+
config.on_check_failure = :allow
|
|
102
|
+
|
|
103
|
+
# Also match parent domains: an email at x.tempmail.com is blocked when
|
|
104
|
+
# tempmail.com is on the list. Set to false for exact matches only.
|
|
105
|
+
config.check_parent_domains = true
|
|
92
106
|
end
|
|
93
107
|
```
|
|
94
108
|
|
|
109
|
+
#### Parent domain matching
|
|
110
|
+
|
|
111
|
+
With `check_parent_domains` enabled (the default since 0.3.0), `user@x.tempmail.com` is blocked when `tempmail.com` is on the list. The check walks up to 3 parent labels (`a.b.c.tempmail.com` → `b.c.tempmail.com` → `c.tempmail.com` → `tempmail.com`) and never matches against bare TLDs like `com`. All candidates are checked in a single indexed query.
|
|
112
|
+
|
|
113
|
+
This is a deliberately minimal, dependency-free approximation of "registrable domain" matching: the gem doesn't ship a full [public suffix list](https://publicsuffix.org), so it can't tell that `co.uk` is a public suffix — which is harmless in practice, because public suffixes don't appear on the blocklist. If you need to allowlist a specific subdomain under a listed parent, add the exact subdomain to `excluded_domains`; an exact exclusion always wins. Excluding a parent domain also unblocks its subdomains.
|
|
114
|
+
|
|
115
|
+
#### Failure mode
|
|
116
|
+
|
|
117
|
+
By default (`on_check_failure = :allow`), if the disposable check raises — say, the database is briefly unavailable — the signup goes through and an error is logged. This is availability-first: a broken check should not lock everyone out of signup. If you'd rather fail closed (reject signups whenever the check cannot run, as versions before 0.3.0 did), set `config.on_check_failure = :reject`.
|
|
118
|
+
|
|
95
119
|
### Direct Check
|
|
96
120
|
|
|
97
121
|
You can also check if an email is disposable directly:
|
|
@@ -109,6 +133,8 @@ To manually update the list of disposable domains, run:
|
|
|
109
133
|
Nondisposable::DomainListUpdater.update
|
|
110
134
|
```
|
|
111
135
|
|
|
136
|
+
The fetch uses 10-second open/read timeouts and replaces the whole table atomically. If the remote fetch fails and your table is empty (e.g. a fresh install without network access), the updater automatically seeds from the blocklist snapshot bundled with the gem so you're never left unprotected; you can also trigger that explicitly with `Nondisposable::DomainListUpdater.seed` (it only seeds an empty table, and never overwrites existing data).
|
|
137
|
+
|
|
112
138
|
It's important you keep your disposable domain list up to date. `nondisposable` will read from the latest version of the [`disposable-email-domains`](https://github.com/disposable-email-domains/disposable-email-domains) list, which is typically updated every few days.
|
|
113
139
|
|
|
114
140
|
For this, `nondisposable` provides you with an Active Job (`DisposableEmailDomainListUpdateJob`) that you can use to schedule daily updates. How you do that, exactly, depends on the queueing system you're using.
|
|
@@ -122,6 +148,38 @@ production:
|
|
|
122
148
|
schedule: every day at 3am US/Pacific
|
|
123
149
|
```
|
|
124
150
|
|
|
151
|
+
## Troubleshooting
|
|
152
|
+
|
|
153
|
+
### SSL certificate verify failed (unable to get certificate CRL)
|
|
154
|
+
|
|
155
|
+
If you see this error when running `Nondisposable::DomainListUpdater.update`:
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
SSL_connect returned=1 errno=0 peeraddr=[::1]:10011 state=error: certificate verify failed (unable to get certificate CRL) (OpenSSL::SSL::SSLError)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
This is **not** a bug in `nondisposable`. It's a known incompatibility between OpenSSL 3.6.0 and older versions of Ruby's `openssl` gem (3.3.0 and earlier).
|
|
162
|
+
|
|
163
|
+
The fix is to update the `openssl` gem to version 3.3.1 or later **in your Rails project**.
|
|
164
|
+
|
|
165
|
+
Add this to your Rails' project `Gemfile`:
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
gem "openssl", "~> 3.3.2"
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Then run:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
bundle install
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
This issue is unlikely to occur in production, it's mostly a development-only issue. It's likely that the exact same codebase fails in development but works fine in production. It only occurs when you have OpenSSL 3.6.0 system-wide AND something intercepting HTTPS traffic (like Cursor's proxy). Users in production or using a regular terminal won't experience it.
|
|
178
|
+
|
|
179
|
+
This issue is more likely to occur if you're running your Rails console from within certain IDEs (like Cursor) that intercept HTTPS traffic through a local proxy. The updated `openssl` gem properly handles certificate verification in these environments.
|
|
180
|
+
|
|
181
|
+
For more details, see the [Ruby openssl gem issue](https://github.com/ruby/openssl/issues/949).
|
|
182
|
+
|
|
125
183
|
## Development
|
|
126
184
|
|
|
127
185
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/Rakefile
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "bundler/gem_tasks"
|
|
4
|
-
|
|
4
|
+
require "rake/testtask"
|
|
5
|
+
|
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
|
7
|
+
t.libs << "lib"
|
|
8
|
+
t.libs << "test"
|
|
9
|
+
t.test_files = FileList['test/**/*_test.rb']
|
|
10
|
+
t.warning = false
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
task default: %i[test]
|
|
@@ -4,12 +4,52 @@ module Nondisposable
|
|
|
4
4
|
class DisposableDomain < ApplicationRecord
|
|
5
5
|
validates :name, presence: true, uniqueness: { case_sensitive: false }
|
|
6
6
|
|
|
7
|
+
# How many leading labels the parent-domain walk strips when
|
|
8
|
+
# check_parent_domains is enabled: user@a.b.c.tempmail.com is checked as
|
|
9
|
+
# a.b.c.tempmail.com, b.c.tempmail.com, c.tempmail.com and tempmail.com.
|
|
10
|
+
#
|
|
11
|
+
# Trade-off (documented instead of pulling in a full public-suffix-list
|
|
12
|
+
# dependency): the walk never queries single-label suffixes (bare TLDs like
|
|
13
|
+
# "com"), but without a PSL it cannot tell that multi-label suffixes like
|
|
14
|
+
# "co.uk" are public — those candidates are harmless unless a public suffix
|
|
15
|
+
# is deliberately added to the list. Conversely, a throwaway address nested
|
|
16
|
+
# more than 3 labels below a listed domain escapes the walk; blocklisted
|
|
17
|
+
# domains are registrable domains, so 3 levels covers practical abuse.
|
|
18
|
+
PARENT_MATCH_DEPTH = 3
|
|
19
|
+
|
|
7
20
|
class << self
|
|
8
21
|
def disposable?(domain)
|
|
9
22
|
return false if domain.blank?
|
|
23
|
+
|
|
10
24
|
domain = domain.to_s.downcase
|
|
11
|
-
Nondisposable.configuration
|
|
12
|
-
|
|
25
|
+
config = Nondisposable.configuration
|
|
26
|
+
candidates = match_candidates(domain)
|
|
27
|
+
|
|
28
|
+
additional = config.additional_domains.map { |d| d.to_s.downcase }
|
|
29
|
+
return true if (candidates & additional).any? # rubocop compat: Array#intersect? needs Ruby >= 3.1, gem supports 3.0
|
|
30
|
+
|
|
31
|
+
excluded = config.excluded_domains.map { |d| d.to_s.downcase }
|
|
32
|
+
# An exact exclusion always wins, so a specific subdomain can be
|
|
33
|
+
# allowlisted even when a parent domain is on the list.
|
|
34
|
+
return false if excluded.include?(domain)
|
|
35
|
+
|
|
36
|
+
where(name: candidates - excluded).exists?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The exact domain plus up to PARENT_MATCH_DEPTH parent domains,
|
|
40
|
+
# never descending below two labels (bare TLDs are never candidates).
|
|
41
|
+
def match_candidates(domain)
|
|
42
|
+
return [domain] unless Nondisposable.configuration.check_parent_domains
|
|
43
|
+
|
|
44
|
+
labels = domain.split('.')
|
|
45
|
+
candidates = [domain]
|
|
46
|
+
1.upto(PARENT_MATCH_DEPTH) do |depth|
|
|
47
|
+
parent = labels.drop(depth)
|
|
48
|
+
break if parent.size < 2
|
|
49
|
+
|
|
50
|
+
candidates << parent.join('.')
|
|
51
|
+
end
|
|
52
|
+
candidates
|
|
13
53
|
end
|
|
14
54
|
end
|
|
15
55
|
|
data/context7.json
ADDED