valid_email2 3.4.0 → 4.0.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.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal:true
2
+
1
3
  require "valid_email2"
2
4
  require "resolv"
3
5
  require "mail"
@@ -7,8 +9,8 @@ module ValidEmail2
7
9
  attr_accessor :address
8
10
 
9
11
  PROHIBITED_DOMAIN_CHARACTERS_REGEX = /[+!_\/\s'`]/
10
- DEFAULT_RECIPIENT_DELIMITER = '+'.freeze
11
- DOT_DELIMITER = '.'.freeze
12
+ DEFAULT_RECIPIENT_DELIMITER = '+'
13
+ DOT_DELIMITER = '.'
12
14
 
13
15
  def self.prohibited_domain_characters_regex
14
16
  @prohibited_domain_characters_regex ||= PROHIBITED_DOMAIN_CHARACTERS_REGEX
@@ -18,9 +20,10 @@ module ValidEmail2
18
20
  @prohibited_domain_characters_regex = val
19
21
  end
20
22
 
21
- def initialize(address)
23
+ def initialize(address, dns_timeout = 5)
22
24
  @parse_error = false
23
25
  @raw_address = address
26
+ @dns_timeout = dns_timeout
24
27
 
25
28
  begin
26
29
  @address = Mail::Address.new(address)
@@ -35,22 +38,27 @@ module ValidEmail2
35
38
  return @valid unless @valid.nil?
36
39
  return false if @parse_error
37
40
 
38
- @valid = begin
39
- if address.domain && address.address == @raw_address
40
- domain = address.domain
41
-
42
- domain !~ self.class.prohibited_domain_characters_regex &&
43
- domain.include?('.') &&
44
- !domain.include?('..') &&
45
- !domain.start_with?('.') &&
46
- !domain.start_with?('-') &&
47
- !domain.include?('-.') &&
48
- !address.local.include?('..') &&
49
- !address.local.end_with?('.')
50
- else
51
- false
52
- end
53
- end
41
+ @valid = address.domain &&
42
+ address.address == @raw_address &&
43
+ valid_domain? &&
44
+ valid_address?
45
+ end
46
+
47
+ def valid_domain?
48
+ domain = address.domain
49
+
50
+ domain !~ self.class.prohibited_domain_characters_regex &&
51
+ domain.include?('.') &&
52
+ !domain.include?('..') &&
53
+ !domain.start_with?('.') &&
54
+ !domain.start_with?('-') &&
55
+ !domain.include?('-.')
56
+ end
57
+
58
+ def valid_address?
59
+ !address.local.include?('..') &&
60
+ !address.local.end_with?('.') &&
61
+ !address.local.start_with?('.')
54
62
  end
55
63
 
56
64
  def dotted?
@@ -84,6 +92,12 @@ module ValidEmail2
84
92
  def valid_mx?
85
93
  return false unless valid?
86
94
 
95
+ mx_or_a_servers.any?
96
+ end
97
+
98
+ def valid_strict_mx?
99
+ return false unless valid?
100
+
87
101
  mx_servers.any?
88
102
  end
89
103
 
@@ -96,12 +110,13 @@ module ValidEmail2
96
110
  i = address_domain.index('.')
97
111
  return false unless i
98
112
 
99
- return domain_list.include?(address_domain[(i+1)..-1])
113
+ domain_list.include?(address_domain[(i + 1)..-1])
100
114
  end
101
115
 
102
116
  def mx_server_is_in?(domain_list)
103
117
  mx_servers.any? { |mx_server|
104
118
  return false unless mx_server.respond_to?(:exchange)
119
+
105
120
  mx_server = mx_server.exchange.to_s
106
121
 
107
122
  domain_list.any? { |domain|
@@ -118,7 +133,14 @@ module ValidEmail2
118
133
 
119
134
  def mx_servers
120
135
  @mx_servers ||= Resolv::DNS.open do |dns|
121
- mx_servers = dns.getresources(address.domain, Resolv::DNS::Resource::IN::MX)
136
+ dns.timeouts = @dns_timeout
137
+ dns.getresources(address.domain, Resolv::DNS::Resource::IN::MX)
138
+ end
139
+ end
140
+
141
+ def mx_or_a_servers
142
+ @mx_or_a_servers ||= Resolv::DNS.open do |dns|
143
+ dns.timeouts = @dns_timeout
122
144
  (mx_servers.any? && mx_servers) ||
123
145
  dns.getresources(address.domain, Resolv::DNS::Resource::IN::A)
124
146
  end
@@ -5,15 +5,14 @@ require "active_model/validations"
5
5
  module ValidEmail2
6
6
  class EmailValidator < ActiveModel::EachValidator
7
7
  def default_options
8
- { regex: true, disposable: false, mx: false, disallow_subaddressing: false, multiple: false }
8
+ { regex: true, disposable: false, mx: false, strict_mx: false, disallow_subaddressing: false, multiple: false, dns_timeout: 5 }
9
9
  end
10
10
 
11
11
  def validate_each(record, attribute, value)
12
12
  return unless value.present?
13
13
  options = default_options.merge(self.options)
14
14
 
15
- value_spitted = options[:multiple] ? value.split(',').map(&:strip) : [value]
16
- addresses = value_spitted.map { |v| ValidEmail2::Address.new(v) }
15
+ addresses = sanitized_values(value).map { |v| ValidEmail2::Address.new(v, options[:dns_timeout]) }
17
16
 
18
17
  error(record, attribute) && return unless addresses.all?(&:valid?)
19
18
 
@@ -37,6 +36,10 @@ module ValidEmail2
37
36
  error(record, attribute) && return if addresses.any? { |address| address.disposable? && !address.whitelisted? }
38
37
  end
39
38
 
39
+ if options[:disposable_domain_with_whitelist]
40
+ error(record, attribute) && return if addresses.any? { |address| address.disposable_domain? && !address.whitelisted? }
41
+ end
42
+
40
43
  if options[:blacklist]
41
44
  error(record, attribute) && return if addresses.any?(&:blacklisted?)
42
45
  end
@@ -44,6 +47,22 @@ module ValidEmail2
44
47
  if options[:mx]
45
48
  error(record, attribute) && return unless addresses.all?(&:valid_mx?)
46
49
  end
50
+
51
+ if options[:strict_mx]
52
+ error(record, attribute) && return unless addresses.all?(&:valid_strict_mx?)
53
+ end
54
+ end
55
+
56
+ def sanitized_values(input)
57
+ options = default_options.merge(self.options)
58
+
59
+ if options[:multiple]
60
+ email_list = input.is_a?(Array) ? input : input.split(',')
61
+ else
62
+ email_list = [input]
63
+ end
64
+
65
+ email_list.reject(&:empty?).map(&:strip)
47
66
  end
48
67
 
49
68
  def error(record, attribute)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal:true
2
+
1
3
  module ValidEmail2
2
- VERSION = "3.4.0"
4
+ VERSION = "4.0.0"
3
5
  end
@@ -8,7 +8,7 @@ require "net/http"
8
8
  whitelisted_emails = %w(
9
9
  onet.pl poczta.onet.pl fastmail.fm hushmail.com
10
10
  hush.ai hush.com hushmail.me naver.com qq.com example.com
11
- yandex.net gmx.com gmx.es
11
+ yandex.net gmx.com gmx.es webdesignspecialist.com.au vp.com
12
12
  )
13
13
 
14
14
  existing_emails = File.open("config/disposable_email_domains.txt") { |f| f.read.split("\n") }
@@ -19,6 +19,10 @@ class TestUserMX < TestModel
19
19
  validates :email, 'valid_email_2/email': { mx: true }
20
20
  end
21
21
 
22
+ class TestUserStrictMX < TestModel
23
+ validates :email, 'valid_email_2/email': { strict_mx: true }
24
+ end
25
+
22
26
  class TestUserDisallowDisposable < TestModel
23
27
  validates :email, 'valid_email_2/email': { disposable: true }
24
28
  end
@@ -31,6 +35,10 @@ class TestUserDisallowDisposableWithWhitelist < TestModel
31
35
  validates :email, 'valid_email_2/email': { disposable_with_whitelist: true }
32
36
  end
33
37
 
38
+ class TestUserDisallowDisposableDomainWithWhitelist < TestModel
39
+ validates :email, 'valid_email_2/email': { disposable_domain_with_whitelist: true }
40
+ end
41
+
34
42
  class TestUserDisallowBlacklisted < TestModel
35
43
  validates :email, 'valid_email_2/email': { blacklist: true }
36
44
  end
@@ -92,6 +100,16 @@ describe ValidEmail2 do
92
100
  expect(user.valid?).to be_falsey
93
101
  end
94
102
 
103
+ it "is invalid if the address starts with a dot" do
104
+ user = TestUser.new(email: ".foo@bar.com")
105
+ expect(user.valid?).to be_falsey
106
+ end
107
+
108
+ it "is invalid if the local part of the address ends with a dot" do
109
+ user = TestUser.new(email: "foo.@bar.com")
110
+ expect(user.valid?).to be_falsey
111
+ end
112
+
95
113
  it "is invalid if the email contains emoticons" do
96
114
  user = TestUser.new(email: "foo🙈@gmail.com")
97
115
  expect(user.valid?).to be_falsy
@@ -170,8 +188,18 @@ describe ValidEmail2 do
170
188
  let(:whitelist_domain) { disposable_domain }
171
189
  let(:whitelist_file_path) { "config/whitelisted_email_domains.yml" }
172
190
 
191
+ # Some of the specs below need to explictly set the whitelist var or it
192
+ # may be cached to an empty set
193
+ def set_whitelist
194
+ ValidEmail2.instance_variable_set(
195
+ :@whitelist,
196
+ ValidEmail2.send(:load_if_exists, ValidEmail2::WHITELIST_FILE)
197
+ )
198
+ end
199
+
173
200
  after do
174
201
  FileUtils.rm(whitelist_file_path, force: true)
202
+ set_whitelist
175
203
  end
176
204
 
177
205
  it "is invalid if the domain is disposable and not in the whitelist" do
@@ -181,9 +209,22 @@ describe ValidEmail2 do
181
209
 
182
210
  it "is valid if the domain is disposable but in the whitelist" do
183
211
  File.open(whitelist_file_path, "w") { |f| f.write [whitelist_domain].to_yaml }
212
+ set_whitelist
184
213
  user = TestUserDisallowDisposableWithWhitelist.new(email: "foo@#{whitelist_domain}")
214
+ expect(user.valid?).to be_truthy
215
+ end
216
+
217
+ it "is invalid if the domain is a disposable_domain and not in the whitelist" do
218
+ user = TestUserDisallowDisposableDomainWithWhitelist.new(email: "foo@#{whitelist_domain}")
185
219
  expect(user.valid?).to be_falsey
186
220
  end
221
+
222
+ it "is valid if the domain is a disposable_domain but in the whitelist" do
223
+ File.open(whitelist_file_path, "w") { |f| f.write [whitelist_domain].to_yaml }
224
+ set_whitelist
225
+ user = TestUserDisallowDisposableDomainWithWhitelist.new(email: "foo@#{whitelist_domain}")
226
+ expect(user.valid?).to be_truthy
227
+ end
187
228
  end
188
229
  end
189
230
 
@@ -216,6 +257,23 @@ describe ValidEmail2 do
216
257
  end
217
258
  end
218
259
 
260
+ describe "with strict mx validation" do
261
+ it "is valid if mx records are found" do
262
+ user = TestUserStrictMX.new(email: "foo@gmail.com")
263
+ expect(user.valid?).to be_truthy
264
+ end
265
+
266
+ it "is invalid if A records are found but no mx records are found" do
267
+ user = TestUserStrictMX.new(email: "foo@ghs.google.com")
268
+ expect(user.valid?).to be_falsey
269
+ end
270
+
271
+ it "is invalid if no mx records are found" do
272
+ user = TestUserStrictMX.new(email: "foo@subdomain.gmail.com")
273
+ expect(user.valid?).to be_falsey
274
+ end
275
+ end
276
+
219
277
  describe "with dotted validation" do
220
278
  it "is valid when address does not contain dots" do
221
279
  user = TestUserDotted.new(email: "johndoe@gmail.com")
@@ -254,6 +312,11 @@ describe ValidEmail2 do
254
312
  expect(user.valid?).to be_truthy
255
313
  end
256
314
 
315
+ it "tests each address from an array" do
316
+ user = TestUserMultiple.new(email: %w[foo@gmail.com bar@gmail.com])
317
+ expect(user.valid?).to be_truthy
318
+ end
319
+
257
320
  context 'when one address is invalid' do
258
321
  it "fails for all" do
259
322
  user = TestUserMultiple.new(email: "foo@gmail.com, bar@123")
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: valid_email2
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.4.0
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Micke Lisinge
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-09-11 00:00:00.000000000 Z
11
+ date: 2021-06-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -157,7 +157,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
157
157
  - !ruby/object:Gem::Version
158
158
  version: '0'
159
159
  requirements: []
160
- rubygems_version: 3.1.2
160
+ rubygems_version: 3.2.3
161
161
  signing_key:
162
162
  specification_version: 4
163
163
  summary: ActiveModel validation for email. Including MX lookup and disposable email