nondisposable 0.3.0 → 0.6.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/CHANGELOG.md +73 -0
- data/README.md +305 -3
- data/Rakefile +5 -0
- data/app/models/nondisposable/disposable_domain.rb +53 -34
- data/app/models/nondisposable/source_snapshot.rb +66 -0
- data/data/email_providers.txt +276 -0
- data/data/iana_tlds.txt +1439 -0
- data/data/known_typo_domains.txt +99 -0
- data/data/never_disposable.txt +480 -0
- data/data/privacy_relays.txt +42 -0
- data/data/public_suffix_list.dat +16571 -0
- data/gemfiles/rails_7.2.gemfile +1 -0
- data/gemfiles/rails_8.0.gemfile +1 -0
- data/gemfiles/rails_8.1.gemfile +1 -0
- data/lib/generators/nondisposable/install_generator.rb +1 -0
- data/lib/generators/nondisposable/templates/create_nondisposable_source_snapshots.rb.erb +42 -0
- data/lib/generators/nondisposable/templates/nondisposable.rb +67 -1
- data/lib/generators/nondisposable/upgrade_generator.rb +43 -0
- data/lib/nondisposable/confirmable_email.rb +75 -0
- data/lib/nondisposable/domain.rb +238 -0
- data/lib/nondisposable/domain_list_updater.rb +510 -88
- data/lib/nondisposable/email_validator.rb +101 -3
- data/lib/nondisposable/known_typos.rb +64 -0
- data/lib/nondisposable/precedence.rb +209 -0
- data/lib/nondisposable/public_suffix.rb +147 -0
- data/lib/nondisposable/public_suffix_list_updater.rb +88 -0
- data/lib/nondisposable/source.rb +233 -0
- data/lib/nondisposable/source_fetcher.rb +69 -0
- data/lib/nondisposable/source_merge.rb +127 -0
- data/lib/nondisposable/suggestion.rb +336 -0
- data/lib/nondisposable/tld.rb +190 -0
- data/lib/nondisposable/tld_list_updater.rb +101 -0
- data/lib/nondisposable/version.rb +1 -1
- data/lib/nondisposable.rb +153 -6
- data/lib/tasks/nondisposable.rake +66 -0
- metadata +24 -2
data/lib/nondisposable.rb
CHANGED
|
@@ -2,8 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "nondisposable/version"
|
|
4
4
|
require_relative "nondisposable/engine"
|
|
5
|
+
require_relative "nondisposable/domain"
|
|
6
|
+
require_relative "nondisposable/public_suffix"
|
|
7
|
+
require_relative "nondisposable/precedence"
|
|
8
|
+
require_relative "nondisposable/source"
|
|
9
|
+
require_relative "nondisposable/source_merge"
|
|
10
|
+
require_relative "nondisposable/source_fetcher"
|
|
11
|
+
require_relative "nondisposable/tld"
|
|
12
|
+
require_relative "nondisposable/known_typos"
|
|
13
|
+
require_relative "nondisposable/suggestion"
|
|
14
|
+
require_relative "nondisposable/confirmable_email"
|
|
5
15
|
require_relative "nondisposable/email_validator"
|
|
6
16
|
require_relative "nondisposable/domain_list_updater"
|
|
17
|
+
require_relative "nondisposable/tld_list_updater"
|
|
18
|
+
require_relative "nondisposable/public_suffix_list_updater"
|
|
7
19
|
|
|
8
20
|
module Nondisposable
|
|
9
21
|
class Error < StandardError; end
|
|
@@ -24,28 +36,150 @@ module Nondisposable
|
|
|
24
36
|
end
|
|
25
37
|
|
|
26
38
|
def self.disposable?(email)
|
|
27
|
-
return false if email.nil? || !email.include?('@')
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
39
|
+
return false if email.nil? || !email.to_s.include?('@')
|
|
40
|
+
# Unicode form: DisposableDomain canonicalises it itself, and needs the
|
|
41
|
+
# unencoded spelling to match rows stored before 0.5.0.
|
|
42
|
+
domain = Domain.from_email(email, unicode: true)
|
|
43
|
+
return false if domain.empty?
|
|
44
|
+
DisposableDomain.disposable?(domain)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Does this address end in a TLD that actually exists?
|
|
48
|
+
#
|
|
49
|
+
# A different question from #disposable?, catching a different kind of bad
|
|
50
|
+
# address: `user@gmail.con` is not a throwaway, it is a typo that produces an
|
|
51
|
+
# account nobody can ever reach. Answers true for anything we cannot judge —
|
|
52
|
+
# no domain, no dot, an IP literal — so it is safe to call on any input; pair
|
|
53
|
+
# it with a format validation if you also care about shape.
|
|
54
|
+
def self.valid_tld?(email)
|
|
55
|
+
return true unless Tld.judgeable?(email)
|
|
56
|
+
|
|
57
|
+
tld = Tld.extract(email)
|
|
58
|
+
return false if tld.nil? # A domain with no TLD can't end in a real one
|
|
59
|
+
|
|
60
|
+
Tld.valid?(tld) && !Tld.blocked?(tld)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# "someone@gmial.com" => "someone@gmail.com", or nil when we have nothing
|
|
64
|
+
# useful to say. Never blocks anything by itself — hand it to a user and let
|
|
65
|
+
# them decide. See Nondisposable::Suggestion for how it decides.
|
|
66
|
+
def self.suggestion_for(email)
|
|
67
|
+
Suggestion.for(email)
|
|
31
68
|
end
|
|
32
69
|
|
|
33
70
|
class Configuration
|
|
34
71
|
ON_CHECK_FAILURE_MODES = [:allow, :reject].freeze
|
|
72
|
+
LOOKALIKE_MODES = [false, true, :confirmable].freeze
|
|
35
73
|
|
|
36
74
|
attr_accessor :error_message, :additional_domains, :excluded_domains
|
|
37
75
|
# Whether to also match parent domains: with the default true, an email at
|
|
38
|
-
# x.tempmail.com is blocked when tempmail.com is on the list (checks
|
|
39
|
-
#
|
|
76
|
+
# x.tempmail.com is blocked when tempmail.com is on the list (checks
|
|
77
|
+
# every parent down to the registrable domain, never a public suffix).
|
|
40
78
|
attr_accessor :check_parent_domains
|
|
41
79
|
attr_reader :on_check_failure
|
|
42
80
|
|
|
81
|
+
# ---- Blocklist sources (Nondisposable::Source) --------------------------
|
|
82
|
+
|
|
83
|
+
# Where the list of disposable domains comes from: an ordered list of
|
|
84
|
+
# Nondisposable::Source objects (or Hashes of their attributes). The
|
|
85
|
+
# default is the single list this gem has always used, so upgrading changes
|
|
86
|
+
# nothing. Nondisposable::Sources::RECOMMENDED adds a second, independent
|
|
87
|
+
# primary. Needs the nondisposable_source_snapshots table
|
|
88
|
+
# (`rails g nondisposable:upgrade`); without it only the default is used.
|
|
89
|
+
attr_accessor :sources
|
|
90
|
+
|
|
91
|
+
# How many independent ORIGINS must list a domain before a :corroborating
|
|
92
|
+
# source's entry is blocked. Copies don't count: see Nondisposable::Source.
|
|
93
|
+
attr_accessor :corroboration_threshold
|
|
94
|
+
|
|
95
|
+
# Privacy relays (Apple Hide My Email, DuckDuckGo, Firefox Relay,
|
|
96
|
+
# SimpleLogin, addy.io — data/privacy_relays.txt) forward to a real,
|
|
97
|
+
# long-lived inbox. false (the default) ALLOWS them even when an upstream
|
|
98
|
+
# list carries them; true REFUSES them even when none does.
|
|
99
|
+
attr_accessor :block_privacy_relays
|
|
100
|
+
|
|
101
|
+
# ---- TLD validity (Nondisposable::Tld) ----------------------------------
|
|
102
|
+
|
|
103
|
+
# OFF by default, and it stays off: upgrading a gem must never silently
|
|
104
|
+
# start rejecting addresses that were fine yesterday. Turn it on to reject
|
|
105
|
+
# addresses whose TLD is not in the IANA root zone — `user@gmail.con` and
|
|
106
|
+
# friends, which are typos rather than throwaways but produce an account
|
|
107
|
+
# nobody can ever reach.
|
|
108
|
+
attr_accessor :check_tld
|
|
109
|
+
|
|
110
|
+
# Your escape hatch from a stale snapshot. A TLD delegated after this gem's
|
|
111
|
+
# release is unknown to the bundled list; naming it here accepts it
|
|
112
|
+
# immediately, with no release to wait for. Dots optional: "app" == ".app".
|
|
113
|
+
attr_accessor :additional_tlds
|
|
114
|
+
|
|
115
|
+
# TLDs to refuse even though they are perfectly real. The usual reason is
|
|
116
|
+
# abuse economics rather than validity — the historically free Freenom set
|
|
117
|
+
# (%w[tk ml ga cf gq]) is the classic example.
|
|
118
|
+
attr_accessor :blocked_tlds
|
|
119
|
+
|
|
120
|
+
# Allowlist mode: when set, ONLY these TLDs are accepted and every other one
|
|
121
|
+
# is refused. nil (the default) means "any TLD in the root zone". ⚠️ This is
|
|
122
|
+
# a blunt instrument — `%w[es]` turns away every customer who happens to use
|
|
123
|
+
# a .com address. Reach for blocked_tlds first.
|
|
124
|
+
attr_accessor :allowed_tlds
|
|
125
|
+
|
|
126
|
+
# ---- Lookalike domains (Nondisposable::Suggestion) ----------------------
|
|
127
|
+
|
|
128
|
+
# Flag addresses one edit away from a well-known provider — `gmail.co`,
|
|
129
|
+
# `gmial.com`, `hotmial.com` — with a message naming the correction.
|
|
130
|
+
#
|
|
131
|
+
# false (default) never flag; Nondisposable.suggestion_for still works
|
|
132
|
+
# :confirmable flag them. Reviewed typos (data/known_typo_domains.txt) are
|
|
133
|
+
# refused outright; every other lookalike is a guess, and the
|
|
134
|
+
# user can confirm the address as typed and pass — see
|
|
135
|
+
# Nondisposable::ConfirmableEmail.
|
|
136
|
+
# true the same as :confirmable. Before 0.5.0 it refused every
|
|
137
|
+
# lookalike, including real organisations' domains (ge.com,
|
|
138
|
+
# bol.com, one.com…) one edit from a provider.
|
|
139
|
+
#
|
|
140
|
+
# Worth leaving off unless your form can show the confirmation: without it a
|
|
141
|
+
# user at a domain that merely looks like a typo has no way through.
|
|
142
|
+
attr_reader :reject_lookalike_domains
|
|
143
|
+
|
|
144
|
+
# How many edits still count as "a typo". 1 (the default) is the distance at
|
|
145
|
+
# which acting on a guess is safe; at 2, genuinely different domains start
|
|
146
|
+
# colliding with each other. 0 disables suggestions entirely.
|
|
147
|
+
attr_accessor :lookalike_distance
|
|
148
|
+
|
|
149
|
+
# Providers to add to the bundled list — your own domain, a regional
|
|
150
|
+
# provider we missed. Adding one both makes it a suggestion candidate and,
|
|
151
|
+
# more importantly, stops its users being told they made a typo.
|
|
152
|
+
attr_accessor :additional_email_providers
|
|
153
|
+
|
|
154
|
+
# ---- Error messages -----------------------------------------------------
|
|
155
|
+
|
|
156
|
+
attr_accessor :invalid_tld_error_message, :blocked_tld_error_message
|
|
157
|
+
# %{suggestion} is replaced with the full corrected address.
|
|
158
|
+
attr_accessor :lookalike_error_message
|
|
159
|
+
|
|
43
160
|
def initialize
|
|
44
161
|
@error_message = "provider is not allowed"
|
|
45
162
|
@additional_domains = []
|
|
46
163
|
@excluded_domains = []
|
|
47
164
|
@on_check_failure = :allow
|
|
48
165
|
@check_parent_domains = true
|
|
166
|
+
|
|
167
|
+
@sources = Nondisposable::Sources::DEFAULT
|
|
168
|
+
@corroboration_threshold = 2
|
|
169
|
+
@block_privacy_relays = false
|
|
170
|
+
|
|
171
|
+
@check_tld = false
|
|
172
|
+
@additional_tlds = []
|
|
173
|
+
@blocked_tlds = []
|
|
174
|
+
@allowed_tlds = nil
|
|
175
|
+
|
|
176
|
+
@reject_lookalike_domains = false
|
|
177
|
+
@lookalike_distance = 1
|
|
178
|
+
@additional_email_providers = []
|
|
179
|
+
|
|
180
|
+
@invalid_tld_error_message = "doesn't look like a real email address"
|
|
181
|
+
@blocked_tld_error_message = "domain ending is not allowed"
|
|
182
|
+
@lookalike_error_message = "looks like a typo. Did you mean %{suggestion}?"
|
|
49
183
|
end
|
|
50
184
|
|
|
51
185
|
# What the validator does when the disposable check itself raises
|
|
@@ -60,5 +194,18 @@ module Nondisposable
|
|
|
60
194
|
|
|
61
195
|
@on_check_failure = mode
|
|
62
196
|
end
|
|
197
|
+
|
|
198
|
+
# Strings are accepted for config loaded from YAML/ENV ("confirmable",
|
|
199
|
+
# "true"); anything else raises, so a typo in an initializer fails at boot
|
|
200
|
+
# instead of silently meaning "on".
|
|
201
|
+
def reject_lookalike_domains=(mode)
|
|
202
|
+
mode = false if mode.nil?
|
|
203
|
+
mode = { 'true' => true, 'false' => false }.fetch(mode, mode.to_sym) if mode.is_a?(String)
|
|
204
|
+
unless LOOKALIKE_MODES.include?(mode)
|
|
205
|
+
raise ArgumentError, "reject_lookalike_domains must be one of #{LOOKALIKE_MODES.map(&:inspect).join(', ')} (got #{mode.inspect})"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
@reject_lookalike_domains = mode
|
|
209
|
+
end
|
|
63
210
|
end
|
|
64
211
|
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :nondisposable do
|
|
4
|
+
namespace :tlds do
|
|
5
|
+
desc "Refresh the vendored IANA TLD snapshot (maintenance task — run in a gem checkout before a release)"
|
|
6
|
+
task :update do
|
|
7
|
+
# Deliberately NOT `require "nondisposable"`: that loads the Engine, which
|
|
8
|
+
# needs Rails, which a bare `rake` in a gem checkout doesn't have. Nothing
|
|
9
|
+
# this task touches reads configuration — only `Tld.valid?`/`blocked?` do,
|
|
10
|
+
# and it calls neither.
|
|
11
|
+
require_relative "../nondisposable/tld"
|
|
12
|
+
require_relative "../nondisposable/tld_list_updater"
|
|
13
|
+
|
|
14
|
+
before = begin
|
|
15
|
+
Nondisposable::Tld.all.size
|
|
16
|
+
rescue StandardError
|
|
17
|
+
0
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
count = Nondisposable::TldListUpdater.update
|
|
21
|
+
|
|
22
|
+
if count.nil?
|
|
23
|
+
abort "[nondisposable] TLD list unchanged. See the error above."
|
|
24
|
+
else
|
|
25
|
+
added = count - before
|
|
26
|
+
change =
|
|
27
|
+
if added.positive? then "+#{added}"
|
|
28
|
+
elsif added.negative? then added.to_s
|
|
29
|
+
else "no change"
|
|
30
|
+
end
|
|
31
|
+
puts "[nondisposable] TLD snapshot now holds #{count} entries (#{change})."
|
|
32
|
+
puts "[nondisposable] IANA version: #{Nondisposable::Tld.version}"
|
|
33
|
+
puts "[nondisposable] Commit data/iana_tlds.txt if it changed."
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
namespace :nondisposable do
|
|
40
|
+
namespace :psl do
|
|
41
|
+
desc "Refresh the vendored Public Suffix List snapshot (maintenance task — run in a gem checkout before a release)"
|
|
42
|
+
task :update do
|
|
43
|
+
# Same reasoning as tlds:update: no Rails here. The parser normalizes
|
|
44
|
+
# rules through Domain, so it comes along.
|
|
45
|
+
require_relative "../nondisposable/domain"
|
|
46
|
+
require_relative "../nondisposable/public_suffix"
|
|
47
|
+
require_relative "../nondisposable/public_suffix_list_updater"
|
|
48
|
+
|
|
49
|
+
before = begin
|
|
50
|
+
Nondisposable::PublicSuffix.rule_count
|
|
51
|
+
rescue StandardError
|
|
52
|
+
0
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
count = Nondisposable::PublicSuffixListUpdater.update
|
|
56
|
+
|
|
57
|
+
if count.nil?
|
|
58
|
+
abort "[nondisposable] Public Suffix List unchanged. See the error above."
|
|
59
|
+
else
|
|
60
|
+
puts "[nondisposable] PSL snapshot now holds #{count} rules (was #{before})."
|
|
61
|
+
puts "[nondisposable] PSL version: #{Nondisposable::PublicSuffix.version}"
|
|
62
|
+
puts "[nondisposable] Commit data/public_suffix_list.dat if it changed."
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: nondisposable
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rameerez
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-10-17 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: rails
|
|
@@ -41,20 +41,42 @@ files:
|
|
|
41
41
|
- README.md
|
|
42
42
|
- Rakefile
|
|
43
43
|
- app/models/nondisposable/disposable_domain.rb
|
|
44
|
+
- app/models/nondisposable/source_snapshot.rb
|
|
44
45
|
- context7.json
|
|
45
46
|
- data/disposable_email_blocklist.conf
|
|
47
|
+
- data/email_providers.txt
|
|
48
|
+
- data/iana_tlds.txt
|
|
49
|
+
- data/known_typo_domains.txt
|
|
50
|
+
- data/never_disposable.txt
|
|
51
|
+
- data/privacy_relays.txt
|
|
52
|
+
- data/public_suffix_list.dat
|
|
46
53
|
- gemfiles/rails_7.2.gemfile
|
|
47
54
|
- gemfiles/rails_8.0.gemfile
|
|
48
55
|
- gemfiles/rails_8.1.gemfile
|
|
49
56
|
- lib/generators/nondisposable/install_generator.rb
|
|
50
57
|
- lib/generators/nondisposable/templates/create_nondisposable_disposable_domains.rb.erb
|
|
58
|
+
- lib/generators/nondisposable/templates/create_nondisposable_source_snapshots.rb.erb
|
|
51
59
|
- lib/generators/nondisposable/templates/disposable_email_domain_list_update_job.rb
|
|
52
60
|
- lib/generators/nondisposable/templates/nondisposable.rb
|
|
61
|
+
- lib/generators/nondisposable/upgrade_generator.rb
|
|
53
62
|
- lib/nondisposable.rb
|
|
63
|
+
- lib/nondisposable/confirmable_email.rb
|
|
64
|
+
- lib/nondisposable/domain.rb
|
|
54
65
|
- lib/nondisposable/domain_list_updater.rb
|
|
55
66
|
- lib/nondisposable/email_validator.rb
|
|
56
67
|
- lib/nondisposable/engine.rb
|
|
68
|
+
- lib/nondisposable/known_typos.rb
|
|
69
|
+
- lib/nondisposable/precedence.rb
|
|
70
|
+
- lib/nondisposable/public_suffix.rb
|
|
71
|
+
- lib/nondisposable/public_suffix_list_updater.rb
|
|
72
|
+
- lib/nondisposable/source.rb
|
|
73
|
+
- lib/nondisposable/source_fetcher.rb
|
|
74
|
+
- lib/nondisposable/source_merge.rb
|
|
75
|
+
- lib/nondisposable/suggestion.rb
|
|
76
|
+
- lib/nondisposable/tld.rb
|
|
77
|
+
- lib/nondisposable/tld_list_updater.rb
|
|
57
78
|
- lib/nondisposable/version.rb
|
|
79
|
+
- lib/tasks/nondisposable.rake
|
|
58
80
|
- sig/nondisposable.rbs
|
|
59
81
|
homepage: https://github.com/rameerez/nondisposable
|
|
60
82
|
licenses:
|