nondisposable 0.3.0 → 0.4.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.
data/lib/nondisposable.rb CHANGED
@@ -2,8 +2,11 @@
2
2
 
3
3
  require_relative "nondisposable/version"
4
4
  require_relative "nondisposable/engine"
5
+ require_relative "nondisposable/tld"
6
+ require_relative "nondisposable/suggestion"
5
7
  require_relative "nondisposable/email_validator"
6
8
  require_relative "nondisposable/domain_list_updater"
9
+ require_relative "nondisposable/tld_list_updater"
7
10
 
8
11
  module Nondisposable
9
12
  class Error < StandardError; end
@@ -30,6 +33,29 @@ module Nondisposable
30
33
  DisposableDomain.disposable?(domain.downcase)
31
34
  end
32
35
 
36
+ # Does this address end in a TLD that actually exists?
37
+ #
38
+ # A different question from #disposable?, catching a different kind of bad
39
+ # address: `user@gmail.con` is not a throwaway, it is a typo that produces an
40
+ # account nobody can ever reach. Answers true for anything we cannot judge —
41
+ # no domain, no dot, an IP literal — so it is safe to call on any input; pair
42
+ # it with a format validation if you also care about shape.
43
+ def self.valid_tld?(email)
44
+ return true unless Tld.judgeable?(email)
45
+
46
+ tld = Tld.extract(email)
47
+ return false if tld.nil? # A domain with no TLD can't end in a real one
48
+
49
+ Tld.valid?(tld) && !Tld.blocked?(tld)
50
+ end
51
+
52
+ # "someone@gmial.com" => "someone@gmail.com", or nil when we have nothing
53
+ # useful to say. Never blocks anything by itself — hand it to a user and let
54
+ # them decide. See Nondisposable::Suggestion for how it decides.
55
+ def self.suggestion_for(email)
56
+ Suggestion.for(email)
57
+ end
58
+
33
59
  class Configuration
34
60
  ON_CHECK_FAILURE_MODES = [:allow, :reject].freeze
35
61
 
@@ -40,12 +66,78 @@ module Nondisposable
40
66
  attr_accessor :check_parent_domains
41
67
  attr_reader :on_check_failure
42
68
 
69
+ # ---- TLD validity (Nondisposable::Tld) ----------------------------------
70
+
71
+ # OFF by default, and it stays off: upgrading a gem must never silently
72
+ # start rejecting addresses that were fine yesterday. Turn it on to reject
73
+ # addresses whose TLD is not in the IANA root zone — `user@gmail.con` and
74
+ # friends, which are typos rather than throwaways but produce an account
75
+ # nobody can ever reach.
76
+ attr_accessor :check_tld
77
+
78
+ # Your escape hatch from a stale snapshot. A TLD delegated after this gem's
79
+ # release is unknown to the bundled list; naming it here accepts it
80
+ # immediately, with no release to wait for. Dots optional: "app" == ".app".
81
+ attr_accessor :additional_tlds
82
+
83
+ # TLDs to refuse even though they are perfectly real. The usual reason is
84
+ # abuse economics rather than validity — the historically free Freenom set
85
+ # (%w[tk ml ga cf gq]) is the classic example.
86
+ attr_accessor :blocked_tlds
87
+
88
+ # Allowlist mode: when set, ONLY these TLDs are accepted and every other one
89
+ # is refused. nil (the default) means "any TLD in the root zone". ⚠️ This is
90
+ # a blunt instrument — `%w[es]` turns away every customer who happens to use
91
+ # a .com address. Reach for blocked_tlds first.
92
+ attr_accessor :allowed_tlds
93
+
94
+ # ---- Lookalike domains (Nondisposable::Suggestion) ----------------------
95
+
96
+ # Reject addresses one edit away from a well-known provider — `gmail.co`,
97
+ # `gmial.com`, `hotmial.com` — with a message naming the correction.
98
+ #
99
+ # OFF by default and worth leaving off unless you have thought about it: a
100
+ # suggestion is a guess about intent, and a wrong guess here stops a real
101
+ # person signing up with their real address. `Nondisposable.suggestion_for`
102
+ # is always available and blocks nothing, which is the gentler way to use
103
+ # this: show the hint, let the human decide.
104
+ attr_accessor :reject_lookalike_domains
105
+
106
+ # How many edits still count as "a typo". 1 (the default) is the distance at
107
+ # which acting on a guess is safe; at 2, genuinely different domains start
108
+ # colliding with each other. 0 disables suggestions entirely.
109
+ attr_accessor :lookalike_distance
110
+
111
+ # Providers to add to the bundled list — your own domain, a regional
112
+ # provider we missed. Adding one both makes it a suggestion candidate and,
113
+ # more importantly, stops its users being told they made a typo.
114
+ attr_accessor :additional_email_providers
115
+
116
+ # ---- Error messages -----------------------------------------------------
117
+
118
+ attr_accessor :invalid_tld_error_message, :blocked_tld_error_message
119
+ # %{suggestion} is replaced with the full corrected address.
120
+ attr_accessor :lookalike_error_message
121
+
43
122
  def initialize
44
123
  @error_message = "provider is not allowed"
45
124
  @additional_domains = []
46
125
  @excluded_domains = []
47
126
  @on_check_failure = :allow
48
127
  @check_parent_domains = true
128
+
129
+ @check_tld = false
130
+ @additional_tlds = []
131
+ @blocked_tlds = []
132
+ @allowed_tlds = nil
133
+
134
+ @reject_lookalike_domains = false
135
+ @lookalike_distance = 1
136
+ @additional_email_providers = []
137
+
138
+ @invalid_tld_error_message = "doesn't look like a real email address"
139
+ @blocked_tld_error_message = "domain ending is not allowed"
140
+ @lookalike_error_message = "looks like a typo. Did you mean %{suggestion}?"
49
141
  end
50
142
 
51
143
  # What the validator does when the disposable check itself raises
@@ -0,0 +1,37 @@
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
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.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - rameerez
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-08-09 00:00:00.000000000 Z
10
+ date: 2026-08-27 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rails
@@ -43,6 +43,8 @@ files:
43
43
  - app/models/nondisposable/disposable_domain.rb
44
44
  - context7.json
45
45
  - data/disposable_email_blocklist.conf
46
+ - data/email_providers.txt
47
+ - data/iana_tlds.txt
46
48
  - gemfiles/rails_7.2.gemfile
47
49
  - gemfiles/rails_8.0.gemfile
48
50
  - gemfiles/rails_8.1.gemfile
@@ -54,7 +56,11 @@ files:
54
56
  - lib/nondisposable/domain_list_updater.rb
55
57
  - lib/nondisposable/email_validator.rb
56
58
  - lib/nondisposable/engine.rb
59
+ - lib/nondisposable/suggestion.rb
60
+ - lib/nondisposable/tld.rb
61
+ - lib/nondisposable/tld_list_updater.rb
57
62
  - lib/nondisposable/version.rb
63
+ - lib/tasks/nondisposable.rake
58
64
  - sig/nondisposable.rbs
59
65
  homepage: https://github.com/rameerez/nondisposable
60
66
  licenses: