email_assessor 0.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 +7 -0
- data/.gitignore +18 -0
- data/.travis.yml +14 -0
- data/CHANGELOG.md +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +108 -0
- data/Rakefile +10 -0
- data/email_assessor.gemspec +30 -0
- data/gemfiles/activemodel3.gemfile +5 -0
- data/gemfiles/activemodel4.gemfile +5 -0
- data/lib/email_assessor.rb +32 -0
- data/lib/email_assessor/address.rb +52 -0
- data/lib/email_assessor/email_validator.rb +34 -0
- data/lib/email_assessor/version.rb +3 -0
- data/pull_mailchecker_emails.rb +21 -0
- data/spec/email_assessor_spec.rb +102 -0
- data/spec/spec_helper.rb +14 -0
- data/vendor/blacklisted_domains.txt +1 -0
- data/vendor/disposable_domains.txt +1896 -0
- metadata +135 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9f9f5a08d4182ed68bdc232439f615339e0f5158
|
4
|
+
data.tar.gz: 76309795d2dad14078647902e21191fc90af9934
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 01529449a629cec7b20880b109574d9cc90d4ea1d8a3bd82908a09a82d66a7c5fbf98510246167ab9eab1cdb40e869873b276e78ea41606386b92a2bf8af87b5
|
7
|
+
data.tar.gz: 683e9ac09cd34379d909558672886f044d5277bda2573e9c0278b1233993731195d555278072b54d914e5f64ac64cf4d003c3613272116da511d573b20d220ac
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright for portions of this project are held by Micke Lisinge, 2013, as part of valid_email2. All other copyright are held by Michael Wolfe Millard, 2016.
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
# ValidEmail2
|
2
|
+
[](https://travis-ci.org/lisinge/email_assessor)
|
3
|
+
[](http://badge.fury.io/rb/email_assessor)
|
4
|
+
|
5
|
+
A fork of of the wonderful [ValidEmail2](https://github.com/lisinge/valid_email2) by [Micke Lisinge](https://github.com/lisinge).
|
6
|
+
|
7
|
+
ValidEmail2:
|
8
|
+
|
9
|
+
* Validates emails with the help of the `mail` gem instead of some clunky regexp.
|
10
|
+
* Aditionally validates that the domain has a MX record.
|
11
|
+
* Optionally validates against a static [list of disposable email services](vendor/disposable_domains.txt).
|
12
|
+
|
13
|
+
|
14
|
+
### Why?
|
15
|
+
|
16
|
+
ValidEmail2 offers very comprehensive email validation, but it has a few pitfalls.
|
17
|
+
|
18
|
+
For starters, it loads the entire list of blacklisted/disposable email domains into memory from a YAML file. In a never ending battle against spam, loading such an extremely large (and ever-growing) array into memory is far from ideal. Instead, EmailAssessor reads a text file line-by-line.
|
19
|
+
|
20
|
+
Another pitfall is that subdomains are able to bypass the disposable and blacklist checks in ValidEmail2. EmailAssessor checks if a given domain *ends* with a blacklisted/disposable domain, preventing subdomains from masking an email that would otherwise be considered invalid.
|
21
|
+
|
22
|
+
|
23
|
+
## Installation
|
24
|
+
|
25
|
+
Add this line to your application's Gemfile:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
gem "email_assessor"
|
29
|
+
```
|
30
|
+
|
31
|
+
And then execute:
|
32
|
+
|
33
|
+
$ bundle
|
34
|
+
|
35
|
+
Or install it yourself as:
|
36
|
+
|
37
|
+
$ gem install email_assessor
|
38
|
+
|
39
|
+
## Usage
|
40
|
+
|
41
|
+
### Use with ActiveModel
|
42
|
+
|
43
|
+
If you just want to validate that it is a valid email address:
|
44
|
+
```ruby
|
45
|
+
class User < ActiveRecord::Base
|
46
|
+
validates :email, presence: true, email: true
|
47
|
+
end
|
48
|
+
```
|
49
|
+
|
50
|
+
To validate that the domain has a MX record:
|
51
|
+
```ruby
|
52
|
+
validates :email, email: { mx: true }
|
53
|
+
```
|
54
|
+
|
55
|
+
To validate that the domain is not a disposable email:
|
56
|
+
```ruby
|
57
|
+
validates :email, email: { disposable: true }
|
58
|
+
```
|
59
|
+
|
60
|
+
To validate that the domain is not blacklisted (under vendor/blacklisted_domains.txt):
|
61
|
+
```ruby
|
62
|
+
validates :email, email: { blacklist: true }
|
63
|
+
```
|
64
|
+
|
65
|
+
All together:
|
66
|
+
```ruby
|
67
|
+
validates :email, email: { mx: true, disposable: true }
|
68
|
+
```
|
69
|
+
|
70
|
+
> Note that this gem will let an empty email pass through so you will need to
|
71
|
+
> add `presence: true` if you require an email
|
72
|
+
|
73
|
+
### Use without ActiveModel
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
address = EmailAssessor::Address.new("lisinge@gmail.com")
|
77
|
+
address.valid? => true
|
78
|
+
address.disposable? => false
|
79
|
+
address.valid_mx? => true
|
80
|
+
```
|
81
|
+
|
82
|
+
### Test environment
|
83
|
+
|
84
|
+
If you are validating `mx` then your specs will fail without an internet connection.
|
85
|
+
It is a good idea to stub out that validation in your test environment.
|
86
|
+
Do so by adding this in your `spec_helper`:
|
87
|
+
```ruby
|
88
|
+
config.before(:each) do
|
89
|
+
allow_any_instance_of(EmailAssessor::Address).to receive(:valid_mx?) { true }
|
90
|
+
end
|
91
|
+
```
|
92
|
+
|
93
|
+
## Requirements
|
94
|
+
|
95
|
+
This gem requires Rails 3.2 or 4.0. It is tested against both versions using:
|
96
|
+
* Ruby-1.9
|
97
|
+
* Ruby-2.0
|
98
|
+
* Ruby-2.1
|
99
|
+
* Ruby-2.2
|
100
|
+
* JRuby-1.9
|
101
|
+
|
102
|
+
## Contributing
|
103
|
+
|
104
|
+
1. Fork it
|
105
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
106
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
107
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
108
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "email_assessor/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "email_assessor"
|
8
|
+
spec.version = EmailAssessor::VERSION
|
9
|
+
spec.summary = "Advanced ActiveModel email validation"
|
10
|
+
spec.description = "ActiveModel email validation with MX lookups, domain blacklisting and disposable email-domain blocking"
|
11
|
+
|
12
|
+
spec.license = "MIT"
|
13
|
+
|
14
|
+
spec.author = "Michael Wolfe Millard"
|
15
|
+
spec.email = "wolfemm.development@gmail.com"
|
16
|
+
spec.homepage = "https://github.com/wolfemm/email_assessor"
|
17
|
+
|
18
|
+
spec.files = `git ls-files`.split($/)
|
19
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
20
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
21
|
+
spec.require_paths = ["lib"]
|
22
|
+
|
23
|
+
spec.required_ruby_version = ">= 1.9.3"
|
24
|
+
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
26
|
+
spec.add_development_dependency "rake"
|
27
|
+
spec.add_development_dependency "rspec", "~> 2.14.1"
|
28
|
+
spec.add_runtime_dependency "mail", "~> 2.5"
|
29
|
+
spec.add_runtime_dependency "activemodel", ">= 3.2"
|
30
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require "email_assessor/email_validator"
|
2
|
+
|
3
|
+
module EmailAssessor
|
4
|
+
DISPOSABLE_DOMAINS_FILE = File.expand_path("../../vendor/disposable_domains.txt", __FILE__)
|
5
|
+
BLACKLISTED_DOMAINS_FILE = File.expand_path("../../vendor/blacklisted_domains.txt", __FILE__)
|
6
|
+
|
7
|
+
def self.domain_is_disposable?(domain)
|
8
|
+
domain_in_file?(domain, DISPOSABLE_DOMAINS_FILE)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.domain_is_blacklisted?(domain)
|
12
|
+
domain_in_file?(domain, BLACKLISTED_DOMAINS_FILE)
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def self.domain_in_file?(domain, filename)
|
18
|
+
return false unless File.exists?(filename)
|
19
|
+
|
20
|
+
domain = domain.downcase
|
21
|
+
domain_matched = false
|
22
|
+
|
23
|
+
File.open(filename).each do |line|
|
24
|
+
if domain.end_with?(line.chomp)
|
25
|
+
domain_matched = true
|
26
|
+
break
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
domain_matched
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require "email_assessor"
|
2
|
+
require "resolv"
|
3
|
+
require "mail"
|
4
|
+
|
5
|
+
module EmailAssessor
|
6
|
+
class Address
|
7
|
+
attr_accessor :address
|
8
|
+
|
9
|
+
def initialize(address)
|
10
|
+
@parse_error = false
|
11
|
+
@raw_address = address
|
12
|
+
|
13
|
+
begin
|
14
|
+
@address = Mail::Address.new(address)
|
15
|
+
rescue Mail::Field::ParseError
|
16
|
+
@parse_error = true
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def valid?
|
21
|
+
return false if @parse_error
|
22
|
+
|
23
|
+
if address.domain && address.address == @raw_address
|
24
|
+
domain = address.domain
|
25
|
+
# Valid address needs to have a dot in the domain
|
26
|
+
!!domain.match(/\./) && !domain.match(/\.{2,}/) && domain.match(/[a-z]\Z/i)
|
27
|
+
else
|
28
|
+
false
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def disposable?
|
33
|
+
valid? && EmailAssessor.domain_is_disposable?(address.domain)
|
34
|
+
end
|
35
|
+
|
36
|
+
def blacklisted?
|
37
|
+
valid? && EmailAssessor.domain_is_blacklisted?(address.domain)
|
38
|
+
end
|
39
|
+
|
40
|
+
def valid_mx?
|
41
|
+
return false unless valid?
|
42
|
+
|
43
|
+
mx = []
|
44
|
+
|
45
|
+
Resolv::DNS.open do |dns|
|
46
|
+
mx.concat dns.getresources(address.domain, Resolv::DNS::Resource::IN::MX)
|
47
|
+
end
|
48
|
+
|
49
|
+
mx.any?
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require "email_assessor/address"
|
2
|
+
require "active_model"
|
3
|
+
require "active_model/validations"
|
4
|
+
|
5
|
+
class EmailValidator < ActiveModel::EachValidator
|
6
|
+
def default_options
|
7
|
+
{ regex: true, disposable: false, mx: false }
|
8
|
+
end
|
9
|
+
|
10
|
+
def validate_each(record, attribute, value)
|
11
|
+
return unless value.present?
|
12
|
+
options = default_options.merge(self.options)
|
13
|
+
|
14
|
+
address = EmailAssessor::Address.new(value)
|
15
|
+
|
16
|
+
error(record, attribute) && return unless address.valid?
|
17
|
+
|
18
|
+
if options[:disposable]
|
19
|
+
error(record, attribute) && return if address.disposable?
|
20
|
+
end
|
21
|
+
|
22
|
+
if options[:blacklist]
|
23
|
+
error(record, attribute) && return if address.blacklisted?
|
24
|
+
end
|
25
|
+
|
26
|
+
if options[:mx]
|
27
|
+
error(record, attribute) && return unless address.valid_mx?
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def error(record, attribute)
|
32
|
+
record.errors.add(attribute, options[:message] || :invalid)
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
|
5
|
+
require "json"
|
6
|
+
require "net/http"
|
7
|
+
|
8
|
+
whitelisted_domains = %w(poczta.onet.pl fastmail.fm hushmail.com naver.com)
|
9
|
+
|
10
|
+
existing_domains = File.readlines("vendor/disposable_domains.txt")
|
11
|
+
|
12
|
+
url = "https://raw.githubusercontent.com/FGRibreau/mailchecker/master/list.json"
|
13
|
+
resp = Net::HTTP.get_response(URI.parse(url))
|
14
|
+
|
15
|
+
remote_domains = JSON.parse(resp.body).flatten - whitelisted_domains
|
16
|
+
|
17
|
+
puts "New domains found: #{(remote_domains - existing_domains).join(', ')}"
|
18
|
+
|
19
|
+
result_domains = (existing_domains + remote_domains).map { |domain| domain.strip.downcase }.uniq.sort
|
20
|
+
|
21
|
+
File.open("vendor/disposable_domains.txt", "w") { |f| f.write result_domains.join("\n") }
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
class TestUser < TestModel
|
4
|
+
validates :email, email: true
|
5
|
+
end
|
6
|
+
|
7
|
+
class TestUserMX < TestModel
|
8
|
+
validates :email, email: { mx: true }
|
9
|
+
end
|
10
|
+
|
11
|
+
class TestUserDisallowDisposable < TestModel
|
12
|
+
validates :email, email: { disposable: true }
|
13
|
+
end
|
14
|
+
|
15
|
+
class TestUserDisallowBlacklisted < TestModel
|
16
|
+
validates :email, email: { blacklist: true }
|
17
|
+
end
|
18
|
+
|
19
|
+
describe EmailAssessor do
|
20
|
+
describe "basic validation" do
|
21
|
+
subject(:user) { TestUser.new(email: "") }
|
22
|
+
|
23
|
+
it "should be valid when email is empty" do
|
24
|
+
user.valid?.should be_true
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should not be valid when domain is missing" do
|
28
|
+
user = TestUser.new(email: "foo")
|
29
|
+
user.valid?.should be_false
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should be invalid when email is malformed" do
|
33
|
+
user = TestUser.new(email: "foo@bar")
|
34
|
+
user.valid?.should be_false
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should be invalid when email contains a trailing symbol" do
|
38
|
+
user = TestUser.new(email: "foo@bar.com/")
|
39
|
+
user.valid?.should be_false
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should be invalid if Mail::AddressListsParser raises exception" do
|
43
|
+
user = TestUser.new(email: "foo@gmail.com")
|
44
|
+
Mail::Address.stub(:new).and_raise(Mail::Field::ParseError.new(nil, nil, nil))
|
45
|
+
user.valid?.should be_false
|
46
|
+
end
|
47
|
+
|
48
|
+
it "shouldn't be valid if the domain constains consecutives dots" do
|
49
|
+
user = TestUser.new(email: "foo@bar..com")
|
50
|
+
user.valid?.should be_false
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "disposable domains" do
|
55
|
+
let(:disposable_domain) { disposable_domain = File.open(described_class::DISPOSABLE_DOMAINS_FILE, &:readline) }
|
56
|
+
|
57
|
+
it "should be valid when email is not in the list of disposable domains" do
|
58
|
+
user = TestUserDisallowDisposable.new(email: "foo@gmail.com")
|
59
|
+
user.valid?.should be_true
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should be invalid when email is in the list of disposable domains" do
|
63
|
+
user = TestUserDisallowDisposable.new(email: "foo@#{disposable_domain}")
|
64
|
+
user.valid?.should be_false
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should be invalid when email is in the list of disposable domains regardless of subdomain" do
|
68
|
+
user = TestUserDisallowDisposable.new(email: "foo@abc123.#{disposable_domain}")
|
69
|
+
user.valid?.should be_false
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "blacklisted domains" do
|
74
|
+
let(:blacklisted_domain) { File.open(described_class::BLACKLISTED_DOMAINS_FILE, &:readline) }
|
75
|
+
it "should be valid when email domain is not in the blacklist" do
|
76
|
+
user = TestUserDisallowBlacklisted.new(email: "foo@gmail.com")
|
77
|
+
user.valid?.should be_true
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should be invalid when email domain is in the blacklist" do
|
81
|
+
user = TestUserDisallowBlacklisted.new(email: "foo@#{blacklisted_domain}")
|
82
|
+
user.valid?.should be_false
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should be invalid when email domain is in the blacklist regardless of subdomain" do
|
86
|
+
user = TestUserDisallowBlacklisted.new(email: "foo@abc123.#{blacklisted_domain}")
|
87
|
+
user.valid?.should be_false
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe "mx lookup" do
|
92
|
+
it "should be valid if mx records are found" do
|
93
|
+
user = TestUserMX.new(email: "foo@gmail.com")
|
94
|
+
user.valid?.should be_true
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should be invalid if no mx records are found" do
|
98
|
+
user = TestUserMX.new(email: "foo@subdomain.gmail.com")
|
99
|
+
user.valid?.should be_false
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|