email_assessor 0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/lisinge/email_assessor.png?branch=master)](https://travis-ci.org/lisinge/email_assessor)
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/email_assessor.png)](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
|