valid_email2 3.4.0 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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