valid_email2 7.0.13 → 7.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.
@@ -8,9 +8,12 @@ module ValidEmail2
8
8
  class Address
9
9
  attr_accessor :address
10
10
 
11
- PROHIBITED_DOMAIN_CHARACTERS_REGEX = /[+!_\/\s'#`]/
11
+ PROHIBITED_DOMAIN_CHARACTERS_REGEX = /[+!_\/\s'#`|]/
12
12
  DEFAULT_RECIPIENT_DELIMITER = '+'
13
13
  DOT_DELIMITER = '.'
14
+ HYPHEN_DELIMITER = '-'
15
+ MAX_DOMAIN_LENGTH = 253
16
+ MAX_LABEL_LENGTH = 63
14
17
 
15
18
  def self.prohibited_domain_characters_regex
16
19
  @prohibited_domain_characters_regex ||= PROHIBITED_DOMAIN_CHARACTERS_REGEX
@@ -52,13 +55,13 @@ module ValidEmail2
52
55
  def valid_domain?
53
56
  domain = address.domain
54
57
  return false if domain.nil?
58
+ return false if domain =~ self.class.prohibited_domain_characters_regex
59
+ return false if domain.length > MAX_DOMAIN_LENGTH
55
60
 
56
- domain !~ self.class.prohibited_domain_characters_regex &&
57
- domain.include?('.') &&
58
- !domain.include?('..') &&
59
- !domain.start_with?('.') &&
60
- !domain.start_with?('-') &&
61
- !domain.include?('-.')
61
+ labels = domain.split(DOT_DELIMITER, -1)
62
+ return false if labels.length < 2
63
+
64
+ labels.all? { |label| valid_domain_label?(label) }
62
65
  end
63
66
 
64
67
  def valid_address?
@@ -82,11 +85,11 @@ module ValidEmail2
82
85
  end
83
86
 
84
87
  def disposable_domain?
85
- domain_is_in?(address.domain, ValidEmail2.disposable_emails)
88
+ valid? && domain_is_in?(address.domain, ValidEmail2.disposable_emails)
86
89
  end
87
90
 
88
91
  def allow_listed?
89
- domain_is_in?(address.domain, ValidEmail2.allow_list)
92
+ valid? && domain_is_in?(address.domain, ValidEmail2.allow_list)
90
93
  end
91
94
 
92
95
  def deny_listed?
@@ -109,6 +112,14 @@ module ValidEmail2
109
112
 
110
113
  private
111
114
 
115
+ # An RFC 1035 label: 1-63 octets, and it may not begin or end with a hyphen.
116
+ def valid_domain_label?(label)
117
+ !label.empty? &&
118
+ label.length <= MAX_LABEL_LENGTH &&
119
+ !label.start_with?(HYPHEN_DELIMITER) &&
120
+ !label.end_with?(HYPHEN_DELIMITER)
121
+ end
122
+
112
123
  def disposable_mx_server?
113
124
  mx_server_is_in?(ValidEmail2.disposable_emails)
114
125
  end
@@ -64,7 +64,8 @@ module ValidEmail2
64
64
 
65
65
  def resolv_config
66
66
  config = Resolv::DNS::Config.default_config_hash
67
- config[:nameserver] = @dns_nameserver if @dns_nameserver
67
+ # REM: resolv gem 0.6.1 on linux can return frozen hash
68
+ config = config.merge(nameserver: @dns_nameserver) if @dns_nameserver
68
69
  config
69
70
  end
70
71
  end
@@ -6,7 +6,16 @@ require "active_model/validations"
6
6
  module ValidEmail2
7
7
  class EmailValidator < ActiveModel::EachValidator
8
8
  def default_options
9
- { disposable: false, mx: false, strict_mx: false, disallow_subaddressing: false, multiple: false, dns_timeout: 5, dns_nameserver: nil }
9
+ {
10
+ disposable: false,
11
+ mx: false,
12
+ strict_mx: false,
13
+ disallow_subaddressing: false,
14
+ multiple: false,
15
+ dns_timeout: 5,
16
+ dns_nameserver: nil,
17
+ specific_error_messages: false
18
+ }
10
19
  end
11
20
 
12
21
  def validate_each(record, attribute, value)
@@ -19,39 +28,39 @@ module ValidEmail2
19
28
  error(record, attribute) && return unless addresses.all?(&:valid?)
20
29
 
21
30
  if options[:disallow_dotted]
22
- error(record, attribute) && return if addresses.any?(&:dotted?)
31
+ error(record, attribute, :disallow_dotted) && return if addresses.any?(&:dotted?)
23
32
  end
24
33
 
25
34
  if options[:disallow_subaddressing]
26
- error(record, attribute) && return if addresses.any?(&:subaddressed?)
35
+ error(record, attribute, :disallow_subaddressing) && return if addresses.any?(&:subaddressed?)
27
36
  end
28
37
 
29
38
  if options[:disposable]
30
- error(record, attribute) && return if addresses.any?(&:disposable?)
39
+ error(record, attribute, :disposable) && return if addresses.any?(&:disposable?)
31
40
  end
32
41
 
33
42
  if options[:disposable_domain]
34
- error(record, attribute) && return if addresses.any?(&:disposable_domain?)
43
+ error(record, attribute, :disposable_domain) && return if addresses.any?(&:disposable_domain?)
35
44
  end
36
45
 
37
46
  if options[:disposable_with_allow_list]
38
- error(record, attribute) && return if addresses.any? { |address| address.disposable? && !address.allow_listed? }
47
+ error(record, attribute, :disposable_with_allow_list) && return if addresses.any? { |address| address.disposable? && !address.allow_listed? }
39
48
  end
40
49
 
41
50
  if options[:disposable_domain_with_allow_list]
42
- error(record, attribute) && return if addresses.any? { |address| address.disposable_domain? && !address.allow_listed? }
51
+ error(record, attribute, :disposable_domain_with_allow_list) && return if addresses.any? { |address| address.disposable_domain? && !address.allow_listed? }
43
52
  end
44
53
 
45
54
  if options[:deny_list]
46
- error(record, attribute) && return if addresses.any?(&:deny_listed?)
55
+ error(record, attribute, :deny_list) && return if addresses.any?(&:deny_listed?)
47
56
  end
48
57
 
49
58
  if options[:mx]
50
- error(record, attribute) && return unless addresses.all?(&:valid_mx?)
59
+ error(record, attribute, :mx) && return unless addresses.all?(&:valid_mx?)
51
60
  end
52
61
 
53
62
  if options[:strict_mx]
54
- error(record, attribute) && return unless addresses.all?(&:valid_strict_mx?)
63
+ error(record, attribute, :strict_mx) && return unless addresses.all?(&:valid_strict_mx?)
55
64
  end
56
65
  end
57
66
 
@@ -67,10 +76,11 @@ module ValidEmail2
67
76
  email_list.reject(&:empty?)
68
77
  end
69
78
 
70
- def error(record, attribute)
79
+ def error(record, attribute, error_key = :invalid)
71
80
  message = options[:message].respond_to?(:call) ? options[:message].call : options[:message]
81
+ message ||= options[:specific_error_messages] ? error_key : :invalid
72
82
 
73
- record.errors.add(attribute, message || :invalid)
83
+ record.errors.add(attribute, message)
74
84
  end
75
85
  end
76
86
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal:true
2
2
 
3
3
  module ValidEmail2
4
- VERSION = "7.0.13"
4
+ VERSION = "7.1.0"
5
5
  end
@@ -16,6 +16,7 @@ allow_listed_emails = %w[
16
16
  duck.com mozmail.com dralias.com 8alias.com 8shield.net
17
17
  mailinblack.com anonaddy.com anonaddy.me addy.io privaterelay.appleid.com appleid.com
18
18
  net.ua kommespaeter.de alpenjodel.de my.id web.id directbox.com embarqmail.com
19
+ unlimit.com reallyfast.info
19
20
  ]
20
21
 
21
22
  existing_emails = File.open("config/disposable_email_domains.txt") { |f| f.read.split("\n") }
@@ -26,7 +27,7 @@ remote_emails = [
26
27
  ].flat_map do |url|
27
28
  resp = Net::HTTP.get_response(URI.parse(url))
28
29
 
29
- resp.body.split("\n").flatten
30
+ resp.body.split("\n").flatten.map(&:downcase)
30
31
  end
31
32
 
32
33
  deny_listed_tlds = %w[me ml id]
data/spec/address_spec.rb CHANGED
@@ -38,6 +38,11 @@ describe ValidEmail2::Address do
38
38
  expect(address.valid?).to be false
39
39
  end
40
40
 
41
+ it "is invalid if email contains a pipe" do
42
+ address = described_class.new("foo@gmai|.com")
43
+ expect(address.valid?).to be false
44
+ end
45
+
41
46
  it "is invalid if it contains Japanese characters" do
42
47
  address = described_class.new("あいうえお@example.com")
43
48
  expect(address.valid?).to be false
@@ -83,6 +88,11 @@ describe ValidEmail2::Address do
83
88
  address = described_class.new("foo@example.com")
84
89
  expect(address.disposable_domain?).to eq false
85
90
  end
91
+
92
+ it "is false if the email cannot be parsed" do
93
+ address = described_class.new('"><test>')
94
+ expect(address.disposable_domain?).to eq false
95
+ end
86
96
  end
87
97
 
88
98
  context "when the disposable domain has subdomains" do
data/spec/spec_helper.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  $:.unshift File.expand_path("../lib",__FILE__)
2
+ require "bundler/setup"
3
+ require "rspec"
2
4
  require "valid_email2"
3
5
  require "debug"
4
6
 
@@ -7,6 +7,10 @@ class TestUser < TestModel
7
7
  validates :email, 'valid_email_2/email': true
8
8
  end
9
9
 
10
+ class TestUserSpecificErrorMessage < TestModel
11
+ validates :email, 'valid_email_2/email': { disallow_dotted: true, specific_error_messages: true }
12
+ end
13
+
10
14
  class TestUserDotted < TestModel
11
15
  validates :email, 'valid_email_2/email': { disallow_dotted: true }
12
16
  end
@@ -171,6 +175,48 @@ describe ValidEmail2 do
171
175
  user = TestUser.new(email: "foo@example.com#")
172
176
  expect(user.valid?).to be_falsey
173
177
  end
178
+
179
+ describe "domain label structure" do
180
+ # A domain label may not begin or end with a hyphen and is limited to 63
181
+ # octets; the whole domain is limited to 253. These rules apply to every
182
+ # label, not only the first/last position of the domain string.
183
+ long_label = "a" * 64
184
+ long_domain = (["aa"] + ["a"] * 126).join(".") # 254 chars, all-valid labels
185
+
186
+ {
187
+ "a leading hyphen on the first label" => "foo@-example.com",
188
+ "a leading hyphen on a later label" => "foo@sub.-example.com",
189
+ "a leading hyphen on a deeply nested label" => "foo@a.b.-c.example.com",
190
+ "a trailing hyphen on a non-final label" => "foo@example-.com",
191
+ "a trailing hyphen on the final label" => "foo@example.com-",
192
+ "a bare hyphen label" => "foo@example.-.com",
193
+ "a label longer than 63 octets" => "foo@#{long_label}.com",
194
+ "a later label longer than 63 octets" => "foo@example.#{long_label}.com",
195
+ "a domain longer than 253 octets" => "foo@#{long_domain}"
196
+ }.each do |description, email|
197
+ it "is invalid with #{description}" do
198
+ expect(TestUser.new(email: email).valid?).to be_falsey
199
+ end
200
+ end
201
+
202
+ full_label = "a" * 63
203
+ max_domain = (["a"] * 127).join(".") # exactly 253 chars
204
+
205
+ {
206
+ "an internal hyphen" => "foo@ex-ample.com",
207
+ "hyphens on several internal labels" => "foo@f-o-o.example.com",
208
+ "a double internal hyphen" => "foo@a--b.example.com",
209
+ "a punycode (IDNA ACE) label" => "foo@xn--bcher-kva.com",
210
+ "a label beginning with a digit" => "foo@3m.com",
211
+ "a 63 octet label at the limit" => "foo@#{full_label}.com",
212
+ "a later 63 octet label" => "foo@example.#{full_label}.com",
213
+ "a domain at the 253 octet limit" => "foo@#{max_domain}"
214
+ }.each do |description, email|
215
+ it "is valid with #{description}" do
216
+ expect(TestUser.new(email: email).valid?).to be_truthy
217
+ end
218
+ end
219
+ end
174
220
  end
175
221
 
176
222
  describe "with disposable validation" do
@@ -418,6 +464,7 @@ describe ValidEmail2 do
418
464
  it "is invalid when address cotains dots" do
419
465
  user = TestUserDotted.new(email: "john.doe@gmail.com")
420
466
  expect(user.valid?).to be_falsey
467
+ expect(user.errors.full_messages).to include("Email is invalid")
421
468
  end
422
469
  end
423
470
 
@@ -433,6 +480,30 @@ describe ValidEmail2 do
433
480
  end
434
481
  end
435
482
 
483
+ context "when detail error messages are enabled" do
484
+ describe "with custom error message" do
485
+ it "supports settings a custom error message" do
486
+ with_translations(:en, { errors: { messages: { disallow_dotted: "dotted message" } } }) do
487
+ user = TestUserSpecificErrorMessage.new(email: "john.doe@gmail.com")
488
+ user.valid?
489
+
490
+ expect(user.errors.full_messages).to include("Email dotted message")
491
+ end
492
+ end
493
+ end
494
+ end
495
+
496
+ def with_translations(locale, translations)
497
+ original_backend = I18n.backend
498
+
499
+ I18n.backend = I18n::Backend::KeyValue.new({}, true)
500
+ I18n.backend.store_translations locale, translations
501
+
502
+ yield
503
+ ensure
504
+ I18n.backend = original_backend
505
+ end
506
+
436
507
  context "when message is present" do
437
508
  describe "with custom error message" do
438
509
  it "supports settings a custom error message" do
data/valid_email2.gemspec CHANGED
@@ -19,8 +19,8 @@ Gem::Specification.new do |spec|
19
19
 
20
20
  spec.required_ruby_version = ">= 3.1.0"
21
21
 
22
- spec.add_development_dependency "bundler", "~> 2.0"
23
- spec.add_development_dependency "rake", "~> 12.3"
22
+ spec.add_development_dependency "bundler", "> 2.0"
23
+ spec.add_development_dependency "rake", "> 12.3"
24
24
  spec.add_development_dependency "securerandom", "0.3.1" # https://github.com/micke/valid_email2/actions/runs/13325070756/job/37216488894
25
25
  spec.add_development_dependency "rspec", "~> 3.5"
26
26
  spec.add_development_dependency "rspec-benchmark", "~> 0.6"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: valid_email2
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.0.13
4
+ version: 7.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Micke Lisinge
@@ -13,28 +13,28 @@ dependencies:
13
13
  name: bundler
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - "~>"
16
+ - - ">"
17
17
  - !ruby/object:Gem::Version
18
18
  version: '2.0'
19
19
  type: :development
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
- - - "~>"
23
+ - - ">"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '2.0'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: rake
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
- - - "~>"
30
+ - - ">"
31
31
  - !ruby/object:Gem::Version
32
32
  version: '12.3'
33
33
  type: :development
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - "~>"
37
+ - - ">"
38
38
  - !ruby/object:Gem::Version
39
39
  version: '12.3'
40
40
  - !ruby/object:Gem::Dependency
@@ -205,7 +205,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
205
205
  - !ruby/object:Gem::Version
206
206
  version: '0'
207
207
  requirements: []
208
- rubygems_version: 3.6.8
208
+ rubygems_version: 4.0.19
209
209
  specification_version: 4
210
210
  summary: ActiveModel validation for email. Including MX lookup and disposable email
211
211
  deny list