email_assessor 0.4.13 → 0.7.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: e89762778af30f339adc30aaf6cec13e2d31ab19
4
- data.tar.gz: c955f0aa4424bb6b83070baa7aea8e3f92eee78b
2
+ SHA256:
3
+ metadata.gz: 28e8db711b1571f9f105f0e341b8798bf557edcdd21bfa9af93b97a2fc6e7989
4
+ data.tar.gz: 697a161767368eed35a1cf12eaca2fc9c129b6e27f4ab48a4bd07362597a678c
5
5
  SHA512:
6
- metadata.gz: 0e0671fd2e0aa2d3c8abacc0e11a949a71e9107445a0fa77e5377e3cbd6638217351e09eccb79d0abce202fc700bb38484b617c222d4f2e79633a93cf1979d13
7
- data.tar.gz: b25571d50af7636c25670c1758d932c52db506759c2624c5c4e3ce917dc4db20aba60cec4005f2b1957b043d9a0fc6ed363b0c4de71548533ae5d4ef0cdbb090
6
+ metadata.gz: ac12ac0944a707a72b3d7f507b016c0d4dad6cd3cd924c1f229178e8dd806cff1e2bdf4eca887566c2271c15ea4a9210f02b2f6e24e26d768c85e47e73104fdb
7
+ data.tar.gz: fd148a7343b485d42f320c0e7fcc2265a636f2973faacf3e928d5315c39e8b909c970809c6ebd07b3d929484a2239b33be3c7b2bb38ef0aeb0bd3648e2288db4
@@ -23,7 +23,7 @@ Gem::Specification.new do |spec|
23
23
 
24
24
  spec.required_ruby_version = ">= 2.4.0"
25
25
 
26
- spec.add_development_dependency "bundler", "~> 1.3"
26
+ spec.add_development_dependency "bundler", "~> 2.0"
27
27
  spec.add_development_dependency "rake"
28
28
  spec.add_development_dependency "rspec", "~> 3.4"
29
29
  spec.add_runtime_dependency "mail", "~> 2.5"
@@ -7,6 +7,82 @@ module EmailAssessor
7
7
  class Address
8
8
  attr_accessor :address
9
9
 
10
+ PROHIBITED_DOMAIN_PREFIXES = [
11
+ '.',
12
+ '-',
13
+ ].freeze
14
+
15
+ PROHIBITED_DOMAIN_CONTENT = [
16
+ '+',
17
+ '!',
18
+ '_',
19
+ '/',
20
+ ' ',
21
+ '..',
22
+ '-.',
23
+ "'",
24
+ ].freeze
25
+
26
+ PROHIBITED_DOMAIN_SUFFIXES = [
27
+ # none
28
+ ].freeze
29
+
30
+ PROHIBITED_LOCAL_PREFIXES = [
31
+ '.',
32
+ ].freeze
33
+
34
+ PROHIBITED_LOCAL_CONTENT = [
35
+ '..',
36
+ ].freeze
37
+
38
+ PROHIBITED_LOCAL_SUFFIXES = [
39
+ '.',
40
+ ].freeze
41
+
42
+ class << self
43
+ def prohibited_domain_regex
44
+ @prohibited_domain_content_regex ||= make_regex(
45
+ prefixes: PROHIBITED_DOMAIN_PREFIXES,
46
+ content: PROHIBITED_DOMAIN_CONTENT,
47
+ suffixes: PROHIBITED_DOMAIN_SUFFIXES
48
+ )
49
+ end
50
+
51
+ def prohibited_local_regex
52
+ @prohibited_local_content_regex ||= make_regex(
53
+ prefixes: PROHIBITED_LOCAL_PREFIXES,
54
+ content: PROHIBITED_LOCAL_CONTENT,
55
+ suffixes: PROHIBITED_LOCAL_SUFFIXES
56
+ )
57
+ end
58
+
59
+ private
60
+
61
+ def make_regex(prefixes: nil, content: nil, suffixes: nil)
62
+ parts = []
63
+
64
+ unless prefixes.nil?
65
+ prefixes.each do |prefix|
66
+ parts << "\\A#{Regexp.escape(prefix)}"
67
+ end
68
+ end
69
+
70
+ unless content.nil?
71
+ content.each do |prefix|
72
+ parts << Regexp.escape(prefix)
73
+ end
74
+ end
75
+
76
+ unless suffixes.nil?
77
+ suffixes.each do |prefix|
78
+ parts << "#{Regexp.escape(prefix)}\\z"
79
+ end
80
+ end
81
+
82
+ Regexp.new(parts.join("|"), Regexp::IGNORECASE)
83
+ end
84
+ end
85
+
10
86
  def initialize(address)
11
87
  @parse_error = false
12
88
  @raw_address = address
@@ -19,18 +95,19 @@ module EmailAssessor
19
95
  end
20
96
 
21
97
  def valid?
98
+ return @valid unless @valid.nil?
22
99
  return false if @parse_error
23
100
 
24
- if address.domain && address.address == @raw_address
25
- domain = address.domain
101
+ @valid =
102
+ if address.domain && address.address == @raw_address
103
+ domain = address.domain
26
104
 
27
- domain.match?(%r{\.}) && # Valid address domain must contain a period
28
- !domain.match?(%r{\.{2,}}) && # Valid address domain cannot have consecutive periods
29
- !domain.match?(%r{\A\.}) && # Valid address domain cannot start with a period
30
- domain.match?(%r{[a-z]\z}i) # Valid address domain must end with letters
31
- else
32
- false
33
- end
105
+ domain.include?('.') &&
106
+ !domain.match?(self.class.prohibited_domain_regex) &&
107
+ !address.local.match?(self.class.prohibited_local_regex)
108
+ else
109
+ false
110
+ end
34
111
  end
35
112
 
36
113
  def disposable?
@@ -42,15 +119,24 @@ module EmailAssessor
42
119
  end
43
120
 
44
121
  def valid_mx?
45
- return false unless valid?
122
+ valid? && mx_servers.any?
123
+ end
46
124
 
47
- mx = []
125
+ def mx_server_is_in?(domain_list_file)
126
+ mx_servers.any? do |mx_server|
127
+ return false unless mx_server.respond_to?(:exchange)
128
+ mx_server = mx_server.exchange.to_s
48
129
 
49
- Resolv::DNS.open do |dns|
50
- mx.concat dns.getresources(address.domain, Resolv::DNS::Resource::IN::MX)
130
+ EmailAssessor.domain_in_file?(mx_server, domain_list_file)
51
131
  end
132
+ end
52
133
 
53
- mx.any?
134
+ def mx_servers
135
+ @mx_servers ||= Resolv::DNS.open do |dns|
136
+ mx_servers = dns.getresources(address.domain, Resolv::DNS::Resource::IN::MX)
137
+ (mx_servers.any? && mx_servers) ||
138
+ dns.getresources(address.domain, Resolv::DNS::Resource::IN::A)
139
+ end
54
140
  end
55
141
  end
56
142
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module EmailAssessor
3
- VERSION = "0.4.13"
3
+ VERSION = "0.7.1"
4
4
  end
@@ -13,14 +13,14 @@ module EmailAssessor
13
13
  domain_in_file?(domain, BLACKLISTED_DOMAINS_FILE_NAME)
14
14
  end
15
15
 
16
- protected
17
-
18
16
  def self.domain_in_file?(domain, file_name)
19
17
  file_name ||= ""
20
18
  domain = domain.downcase
21
19
 
22
- # Using String#end_with? here would lead to unexpected quirks and false positives.
23
- # For instance, hotmail.com is valid but tmail.com is not.
24
- File.foreach(file_name).any? { |line| domain.match?(%r{\A(?:.+\.)*?#{line.chomp}\z}i) }
20
+ File.foreach(file_name, chomp: true).any? do |line|
21
+ # String#end_with? is used as a cheaper initial check but due to potential false positives
22
+ # (hotmail.com is valid but tmail.com is not) regex is also necessary.
23
+ domain.end_with?(line) && domain.match?(%r{\A(?:.*\.)?#{line}\z}i)
24
+ end
25
25
  end
26
26
  end
@@ -5,16 +5,17 @@ require "yaml"
5
5
 
6
6
  require "json"
7
7
  require "net/http"
8
+ require "set"
8
9
 
9
10
  whitelisted_domains = %w(poczta.onet.pl fastmail.fm hushmail.com naver.com qq.com nus.edu.sg)
10
11
 
11
12
  existing_domains = File.readlines("vendor/disposable_domains.txt")
12
13
 
13
- url = "https://raw.githubusercontent.com/FGRibreau/mailchecker/master/list.json"
14
+ url = "https://raw.githubusercontent.com/FGRibreau/mailchecker/master/list.txt"
14
15
  resp = Net::HTTP.get_response(URI.parse(url))
15
16
 
16
- remote_domains = JSON.parse(resp.body).flatten - whitelisted_domains
17
+ remote_domains = (resp.body.split("\n")) - whitelisted_domains
17
18
 
18
- result_domains = (existing_domains + remote_domains).map { |domain| domain.strip.downcase }.uniq.sort
19
+ result_domains = SortedSet.new((existing_domains + remote_domains).map! { |domain| domain.strip.downcase })
19
20
 
20
- File.open("vendor/disposable_domains.txt", "w") { |f| f.write result_domains.join("\n") }
21
+ File.open("vendor/disposable_domains.txt", "w") { |f| f.write result_domains.to_a.join("\n") }
@@ -32,55 +32,107 @@ describe EmailAssessor do
32
32
  describe "basic validation" do
33
33
  subject(:user) { plain_user }
34
34
 
35
- it "should be valid when email is empty" do
35
+ it "is valid when email is empty" do
36
36
  is_expected.to be_valid
37
37
  end
38
38
 
39
- it "should not be valid when domain is missing" do
39
+ it "is invalid if the address starts with a dot" do
40
+ user = TestUser.new(email: ".foo@bar.com")
41
+ expect(user.valid?).to be_falsey
42
+ end
43
+
44
+ it "is invalid if the address contains consecutive dots" do
45
+ user = TestUser.new(email: "foo..bar@gmail.com")
46
+ expect(user.valid?).to be_falsey
47
+ end
48
+
49
+ it "is invalid if the address ends with a dot" do
50
+ user = TestUser.new(email: "foo.@bar.com")
51
+ expect(user.valid?).to be_falsey
52
+ end
53
+
54
+ it "is invalid when domain is missing" do
40
55
  user.email = "foo@.com"
41
56
  is_expected.to be_invalid
42
57
  end
43
58
 
44
- it "should be invalid when email is malformed" do
59
+ it "is invalid when domain starts with a dot" do
60
+ user.email = "foo@.example.com"
61
+ is_expected.to be_invalid
62
+ end
63
+
64
+ it "is invalid when email is malformed" do
45
65
  user.email = "foo@bar"
46
66
  is_expected.to be_invalid
47
67
  end
48
68
 
49
- it "should be invalid when email contains a trailing symbol" do
69
+ it "is invalid when email contains a trailing symbol" do
50
70
  user.email = "foo@bar.com/"
51
71
  is_expected.to be_invalid
52
72
  end
53
73
 
54
- it "should be invalid if Mail::AddressListsParser raises exception" do
74
+ it "is invalid if Mail::AddressListsParser raises exception" do
55
75
  allow(Mail::Address).to receive(:new).and_raise(Mail::Field::ParseError.new(nil, nil, nil))
56
76
  user.email = "foo@gmail.com"
57
77
  is_expected.to be_invalid
58
78
  end
59
79
 
60
- it "shouldn't be valid if the domain constains consecutives dots" do
80
+ it "is invalid if the domain constains consecutives dots" do
61
81
  user.email = "foo@bar..com"
62
82
  is_expected.to be_invalid
63
83
  end
84
+
85
+ it "is invalid if the domain contains emoticons" do
86
+ user.email = "foo🙈@gmail.com"
87
+ is_expected.to be_invalid
88
+ end
89
+
90
+ it "is invalid if the domain contains spaces" do
91
+ user.email = "user@gmail .com"
92
+ is_expected.to be_invalid
93
+ end
94
+
95
+ it "is invalid if the domain contains '.@'" do
96
+ user.email = "foo.@gmail.com"
97
+ expect(user.valid?).to be_falsy
98
+ end
99
+
100
+ it "is invalid if the domain begins with a hyphen" do
101
+ user.email = "foo@-gmail.com"
102
+ expect(user.valid?).to be_falsy
103
+ end
104
+
105
+ it "is invalid if the domain name ends with a hyphen" do
106
+ user.email = "foo@gmail-.com"
107
+ expect(user.valid?).to be_falsy
108
+ end
109
+
110
+ %w[+ _ ! / \ '].each do |invalid_character|
111
+ it "is invalid if domain contains a \"#{invalid_character}\" character" do
112
+ user.email = "foo@google#{invalid_character}yahoo.com"
113
+ is_expected.to be_invalid
114
+ end
115
+ end
64
116
  end
65
117
 
66
118
  describe "disposable domains" do
67
119
  subject(:user) { disposable_user }
68
120
 
69
- it "should be valid when email is not in the list of disposable domains" do
121
+ it "is valid when email is not in the list of disposable domains" do
70
122
  is_expected.to be_valid
71
123
  end
72
124
 
73
- it "should be invalid when email is in the list of disposable domains" do
125
+ it "is invalid when email is in the list of disposable domains" do
74
126
  user.email = "foo@#{disposable_domain}"
75
127
  is_expected.to be_invalid
76
128
  end
77
129
 
78
- it "should be invalid when email is in the list of disposable domains regardless of case" do
130
+ it "is invalid when email is in the list of disposable domains regardless of case" do
79
131
  user.email = "foo@#{disposable_domain.upcase}"
80
132
  is_expected.to be_invalid
81
133
  end
82
134
 
83
- it "should be invalid when email is in the list of disposable domains regardless of subdomain" do
135
+ it "is invalid when email is in the list of disposable domains regardless of subdomain" do
84
136
  user.email = "foo@abc123.#{disposable_domain}"
85
137
  is_expected.to be_invalid
86
138
  end
@@ -89,21 +141,21 @@ describe EmailAssessor do
89
141
  describe "blacklisted domains" do
90
142
  subject(:user) { blacklist_user }
91
143
 
92
- it "should be valid when email domain is not in the blacklist" do
144
+ it "is valid when email domain is not in the blacklist" do
93
145
  is_expected.to be_valid
94
146
  end
95
147
 
96
- it "should be invalid when email domain is in the blacklist" do
148
+ it "is invalid when email domain is in the blacklist" do
97
149
  user.email = "foo@#{blacklisted_domain}"
98
150
  is_expected.to be_invalid
99
151
  end
100
152
 
101
- it "should be invalid when email is in the blacklist regardless of case" do
153
+ it "is invalid when email is in the blacklist regardless of case" do
102
154
  user.email = "foo@#{blacklisted_domain.upcase}"
103
155
  is_expected.to be_invalid
104
156
  end
105
157
 
106
- it "should be invalid when email domain is in the blacklist regardless of subdomain" do
158
+ it "is invalid when email domain is in the blacklist regardless of subdomain" do
107
159
  user.email = "foo@abc123.#{blacklisted_domain}"
108
160
  is_expected.to be_invalid
109
161
  end
@@ -112,11 +164,11 @@ describe EmailAssessor do
112
164
  describe "mx lookup" do
113
165
  subject(:user) { mx_user }
114
166
 
115
- it "should be valid if mx records are found" do
167
+ it "is valid if mx records are found" do
116
168
  is_expected.to be_valid
117
169
  end
118
170
 
119
- it "should be invalid if no mx records are found" do
171
+ it "is invalid if no mx records are found" do
120
172
  user.email = "foo@subdomain.gmail.com"
121
173
  is_expected.to be_invalid
122
174
  end