email_address 0.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 +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +67 -0
- data/Rakefile +5 -0
- data/email_address.gemspec +25 -0
- data/lib/email_address.rb +145 -0
- data/lib/email_address/address.rb +4 -0
- data/lib/email_address/config.rb +4 -0
- data/lib/email_address/esp.rb +4 -0
- data/lib/email_address/host.rb +6 -0
- data/lib/email_address/local.rb +4 -0
- data/lib/email_address/providers/default.rb +8 -0
- data/lib/email_address/providers/google.rb +8 -0
- data/lib/email_address/version.rb +3 -0
- data/lib/email_providers/address.rb +102 -0
- data/lib/email_providers/config.rb +36 -0
- data/lib/email_providers/factory.rb +17 -0
- data/lib/email_providers/host.rb +87 -0
- data/lib/email_providers/mail_exchanger.rb +60 -0
- data/lib/email_providers/mailbox.rb +44 -0
- data/lib/email_providers/providers/default.rb +55 -0
- data/lib/email_providers/providers/google.rb +27 -0
- data/lib/email_providers/version.rb +3 -0
- data/test/email_address.rb +52 -0
- data/test/email_address/address.rb +16 -0
- data/test/email_address/config.rb +13 -0
- data/test/email_address/host.rb +29 -0
- data/test/email_address/mail_exchanger.rb +9 -0
- data/test/test_helper.rb +9 -0
- metadata +125 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 225554c1b1abf4ead2900a7209b55ec7babeaefe
|
4
|
+
data.tar.gz: 4de853f82e80be5e9e6ce843a16fd2dc3f4a3255
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 11b73a88096b1553dac4aa7979a1a7385c568ae8d5c2dca268c88bf1fd2827aa77bb76e3a412a6cc85d1f5d9983f470a1a6b1840df73d428fbfdae76277bfe9a
|
7
|
+
data.tar.gz: 97b3f3497d00346404b94dbfde915bbbcf1dae41bcbc6d7fc98a4e0dc0072245b57597ca4939295e6db47fc2700ed935add5f8019519389950288f2501ad7bc6
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Allen Fair
|
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,67 @@
|
|
1
|
+
# EmailAddress
|
2
|
+
|
3
|
+
The EmailAddress library is an _opinionated_ email address handler and
|
4
|
+
validator.
|
5
|
+
|
6
|
+
So you have an email address input by a user. Do you want to validate
|
7
|
+
it, check it for uniqueness, or mine statistics on all your addresses?
|
8
|
+
Then the email_address gem is for you!
|
9
|
+
|
10
|
+
Opininated? Yes. By default, this does not support RFC822 specification
|
11
|
+
because it allows addresses that should never exist for real people.
|
12
|
+
By limiting email addresses to a subset of standardly used ones, you can
|
13
|
+
remove false positives in validation, and help the email community
|
14
|
+
evolve into a friendlier place. I like standards as much as the next
|
15
|
+
person, but enough is enough!
|
16
|
+
|
17
|
+
Why my opinion? I've been working with email and such since the late
|
18
|
+
1990's. I would like to see modern practices applied to email addresses.
|
19
|
+
These rules really do apply to most (without real statistics, I'll claim
|
20
|
+
99.9+%) usage in the 21st Century. They may not be for everyone, you may
|
21
|
+
need strict adherence to these standards for historical reasons, but I
|
22
|
+
bet you'll wish you could support this one instead!
|
23
|
+
|
24
|
+
## Installation
|
25
|
+
|
26
|
+
Add this line to your application's Gemfile:
|
27
|
+
|
28
|
+
gem 'email_address'
|
29
|
+
|
30
|
+
And then execute:
|
31
|
+
|
32
|
+
$ bundle
|
33
|
+
|
34
|
+
Or install it yourself as:
|
35
|
+
|
36
|
+
$ gem install email_address
|
37
|
+
|
38
|
+
## Usage
|
39
|
+
|
40
|
+
Inspect your email address string by creating an instance of
|
41
|
+
EmailAddress:
|
42
|
+
|
43
|
+
email = EmailAddress.new("user@example.com")
|
44
|
+
|
45
|
+
You can see if it validates as an opinionated address:
|
46
|
+
|
47
|
+
email.valid?
|
48
|
+
|
49
|
+
This runs the following checks you can do yourself:
|
50
|
+
|
51
|
+
email.valid_format?
|
52
|
+
email.valid_domain?
|
53
|
+
email.valid_user?
|
54
|
+
email.disposable_email?
|
55
|
+
email.spam_trap?
|
56
|
+
|
57
|
+
Of course, the last couple tests can't be published, so you can provide
|
58
|
+
a callback to check them yourself if you need.
|
59
|
+
|
60
|
+
|
61
|
+
## Contributing
|
62
|
+
|
63
|
+
1. Fork it
|
64
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
65
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
66
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
67
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'email_address/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "email_address"
|
8
|
+
spec.version = EmailAddress::VERSION
|
9
|
+
spec.authors = ["Allen Fair"]
|
10
|
+
spec.email = ["allen.fair@gmail.com"]
|
11
|
+
spec.description = %q{The EmailAddress library is an _opinionated_ email address handler and
|
12
|
+
validator.}
|
13
|
+
spec.summary = %q{EmailAddress checks on validates an acceptable set of email addresses.}
|
14
|
+
spec.homepage = "https://github.com/afair/email_address"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files`.split($/)
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
23
|
+
spec.add_development_dependency "rake"
|
24
|
+
spec.add_development_dependency "simpleidn"
|
25
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
#require "email_address/version"
|
2
|
+
#require "email_address/config"
|
3
|
+
#require "email_address/host"
|
4
|
+
#require "email_address/address"
|
5
|
+
require 'simpleidn'
|
6
|
+
|
7
|
+
class EmailAddress
|
8
|
+
attr_reader :address, :local, :account, :tag, :comment,
|
9
|
+
:domain, :subdomains, :domain_name, :base_domain, :top_level_domain
|
10
|
+
|
11
|
+
def initialize(email)
|
12
|
+
self.address = email
|
13
|
+
end
|
14
|
+
|
15
|
+
##############################################################################
|
16
|
+
# Basic email address: local@domain
|
17
|
+
# Only supporting FQDN's (Fully Qualified Domain Names)?
|
18
|
+
# Length: Up to 254 characters
|
19
|
+
##############################################################################
|
20
|
+
def address=(email)
|
21
|
+
@address = email.strip
|
22
|
+
(local_name, domain_name) = @address.split('@')
|
23
|
+
self.domain = domain_name
|
24
|
+
self.local = local_name
|
25
|
+
@address
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_s
|
29
|
+
[local, domain].join('@')
|
30
|
+
end
|
31
|
+
|
32
|
+
##############################################################################
|
33
|
+
# Domain Parsing
|
34
|
+
# Parts: subdomains.basedomain.top-level-domain
|
35
|
+
# IPv6/IPv6: [128.0.0.1], [IPv6:2001:db8:1ff::a0b:dbd0]
|
36
|
+
# Comments: (comment)example.com, example.com(comment)
|
37
|
+
# Internationalized: Unicode to Punycode
|
38
|
+
# Length: up to 255 characters
|
39
|
+
##############################################################################
|
40
|
+
def domain=(host_name)
|
41
|
+
host_name ||= ''
|
42
|
+
@domain = host_name.strip.downcase
|
43
|
+
parse_domain
|
44
|
+
@domain
|
45
|
+
end
|
46
|
+
|
47
|
+
def parse_domain
|
48
|
+
@subdomains = @base_domain = @domain_name = @top_level_domain = ''
|
49
|
+
# Patterns: *.com, *.xx.cc, *.cc
|
50
|
+
if @domain =~ /\A(.+)\.(\w{3,10})\z/ || @domain =~ /\A(.+)\.(\w{1,3}\.\w\w)\z/ || @domain =~ /\A(.+)\.(\w\w)\z/
|
51
|
+
@top_level_domain = $2;
|
52
|
+
sld = $1 # Second level domain
|
53
|
+
if sld =~ /\A(.+)\.(.+)\z/ # is subdomain? sub.example [.tld]
|
54
|
+
@subdomains = $1
|
55
|
+
@base_domain = $2
|
56
|
+
else
|
57
|
+
@subdomains = ""
|
58
|
+
@base_domain = sld
|
59
|
+
end
|
60
|
+
@domain_name = @base_domain + '.' + @top_level_domain
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def dns_hostname
|
65
|
+
@dns_hostname ||= SimpleIDN.to_ascii(domain)
|
66
|
+
end
|
67
|
+
|
68
|
+
##############################################################################
|
69
|
+
# Parsing id provider-dependent, but RFC allows:
|
70
|
+
# A-Z a-z 0-9 . ! # $ % ' * + - / = ? ^ _ { | } ~
|
71
|
+
# Quoted: space ( ) , : ; < > @ [ ]
|
72
|
+
# Quoted-Backslash-Escaped: \ "
|
73
|
+
# Quote local part or dot-separated sub-parts x."y".z
|
74
|
+
# (comment)mailbox | mailbox(comment)
|
75
|
+
# 8-bit/UTF-8: allowed but mail-system defined
|
76
|
+
# RFC 5321 also warns that "a host that expects to receive mail SHOULD avoid defining mailboxes where the Local-part requires (or uses) the Quoted-string form".
|
77
|
+
# Postmaster: must always be case-insensitive
|
78
|
+
# Case: sensitive, but usually treated as equivalent
|
79
|
+
# Local Parts: comment, account tag
|
80
|
+
# Length: upt o 64 cgaracters
|
81
|
+
##############################################################################
|
82
|
+
def local=(local)
|
83
|
+
local ||= ''
|
84
|
+
@local = local.strip.downcase
|
85
|
+
@account = parse_comment(@local)
|
86
|
+
(@account, @tag) = @account.split(tag_separator)
|
87
|
+
@tag ||= ''
|
88
|
+
|
89
|
+
@local
|
90
|
+
end
|
91
|
+
|
92
|
+
def parse_comment(local)
|
93
|
+
if local =~ /\A\((.+?)\)(.+)\z/
|
94
|
+
(@comment, local) = [$1, $2]
|
95
|
+
elsif @local =~ /\A(.+)\((.+?)\)\z/
|
96
|
+
(@comment, local) = [$1, $2]
|
97
|
+
else
|
98
|
+
@comment = '';
|
99
|
+
end
|
100
|
+
local
|
101
|
+
end
|
102
|
+
|
103
|
+
##############################################################################
|
104
|
+
# Provider-Specific Settings
|
105
|
+
##############################################################################
|
106
|
+
|
107
|
+
def provider
|
108
|
+
# @provider ||= EmailProviders::Default.new
|
109
|
+
'unknown'
|
110
|
+
end
|
111
|
+
|
112
|
+
def tag_separator
|
113
|
+
'+'
|
114
|
+
end
|
115
|
+
|
116
|
+
def case_sensitive_local
|
117
|
+
false
|
118
|
+
end
|
119
|
+
|
120
|
+
# Returns the unique address as simplified account@hostname
|
121
|
+
def unique_address
|
122
|
+
"#{account}@#{dns_hostname}".downcase
|
123
|
+
end
|
124
|
+
|
125
|
+
# Letters, numbers, period (no start) 6-30chars
|
126
|
+
def user_pattern
|
127
|
+
/\A[a-z0-9][\.a-z0-9]{0,29}\z/i
|
128
|
+
end
|
129
|
+
|
130
|
+
##############################################################################
|
131
|
+
# Validations -- Eventually a provider-sepecific check
|
132
|
+
##############################################################################
|
133
|
+
def valid?
|
134
|
+
return false unless @local =~ user_pattern
|
135
|
+
return false unless provider # .valid_domain
|
136
|
+
true
|
137
|
+
end
|
138
|
+
|
139
|
+
def valid_format?
|
140
|
+
return false unless @local.match(user_pattern)
|
141
|
+
return false unless @host.valid_format?
|
142
|
+
true
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module EmailAddress
|
2
|
+
|
3
|
+
# EmailAddress::Address - Inspects a Email Address.
|
4
|
+
#
|
5
|
+
# * hostname - Everything to the rigth of the @
|
6
|
+
# * mailbox - Everything to the left of the @
|
7
|
+
#
|
8
|
+
class Address
|
9
|
+
attr_reader :address, :mailbox, :host, :account, :tags
|
10
|
+
|
11
|
+
def initialize(mailbox, host_object)
|
12
|
+
self.mailbox = mailbox
|
13
|
+
@host = host_object
|
14
|
+
end
|
15
|
+
|
16
|
+
def mailbox=(mailbox)
|
17
|
+
@mailbox = mailbox.strip.downcase
|
18
|
+
(@account, @tags) = @mailbox.split(tag_separator)
|
19
|
+
@mailbox
|
20
|
+
end
|
21
|
+
|
22
|
+
def address=(address)
|
23
|
+
@address = address.strip
|
24
|
+
(mailbox_name, host_name) = @address.split(/\@/)
|
25
|
+
return unless host_part
|
26
|
+
|
27
|
+
@mailbox_name = mailbox_name
|
28
|
+
@host = EmailAddress::Host.new(host_part)
|
29
|
+
@mailbox = host.provider_mailbox(mailbox_part, @host)
|
30
|
+
@address
|
31
|
+
end
|
32
|
+
|
33
|
+
def provider
|
34
|
+
'unknown'
|
35
|
+
end
|
36
|
+
|
37
|
+
def tag_separator
|
38
|
+
'+'
|
39
|
+
end
|
40
|
+
|
41
|
+
def case_sensitive_mailbox
|
42
|
+
false
|
43
|
+
end
|
44
|
+
|
45
|
+
# Letters, numbers, period (no start) 6-30chars
|
46
|
+
def user_pattern
|
47
|
+
/\A[a-z0-9][\.a-z0-9]{5,29}\z/i
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns the unique address as simplified account@hostname
|
51
|
+
def unique_address
|
52
|
+
"#{account}@#{dns_hostname}"
|
53
|
+
end
|
54
|
+
|
55
|
+
def valid?
|
56
|
+
return false unless @mailbox.valid?
|
57
|
+
return false unless @host.valid?
|
58
|
+
true
|
59
|
+
end
|
60
|
+
|
61
|
+
def valid_format?
|
62
|
+
return false unless @mailbox.match(user_pattern)
|
63
|
+
return false unless @host.valid_format?
|
64
|
+
true
|
65
|
+
end
|
66
|
+
|
67
|
+
############################################################################
|
68
|
+
# Host Deletation: domain parts
|
69
|
+
############################################################################
|
70
|
+
|
71
|
+
# Returns the fully-qualified host name (everything to the right of the @).
|
72
|
+
def hostname
|
73
|
+
host.host
|
74
|
+
end
|
75
|
+
|
76
|
+
# Returns the host without any subdomains (domain.tld(
|
77
|
+
def domain_name
|
78
|
+
host.domain_name
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns the Top-Level-Domain parts (after domain): com, co.jp
|
82
|
+
def tld
|
83
|
+
host.tld
|
84
|
+
end
|
85
|
+
|
86
|
+
# Returns the registration name without subdomains or TLD.
|
87
|
+
def base_domain
|
88
|
+
host.base_domain
|
89
|
+
end
|
90
|
+
|
91
|
+
# Returns any given subdomains of the domain name
|
92
|
+
def subdomains
|
93
|
+
host.subdomains
|
94
|
+
end
|
95
|
+
|
96
|
+
# Returns an ASCII name for DNS lookup, Punycode for Unicode domains.
|
97
|
+
def dns_hostname
|
98
|
+
host.dns_hostname
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module EmailAddress
|
2
|
+
class Config
|
3
|
+
|
4
|
+
# EmailAddress::Config.add_provider(:google, domain_names:["gmail.com", "googlemail.com", "google.com"])
|
5
|
+
def self.add_provider(provider, matches={})
|
6
|
+
@pmatch ||= []
|
7
|
+
@pmatch << matches
|
8
|
+
end
|
9
|
+
|
10
|
+
# EmailAddress::Config.config do .... end
|
11
|
+
def self.setup(&block)
|
12
|
+
@config = Config::DSL.new(&block)
|
13
|
+
@config.instance_eval(&block)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.get
|
17
|
+
@config
|
18
|
+
end
|
19
|
+
|
20
|
+
class DSL
|
21
|
+
attr_accessor :provider_matching_rules
|
22
|
+
def add_provider(provider, matches={})
|
23
|
+
puts provider, matches
|
24
|
+
@provider_matching_rules ||= []
|
25
|
+
@provider_matching_rules << {provider:provider, matches:matches}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
#EmailAddress::Config.setup do
|
33
|
+
# add_provider :google, domain_names:["gmail.com", "googlemail.com", "google.com"]
|
34
|
+
#end
|
35
|
+
|
36
|
+
#puts EmailAddress::Config.provider_matching_rules
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module EmailAddress
|
2
|
+
|
3
|
+
# EmailAddress::Address - Inspects a Email Address.
|
4
|
+
#
|
5
|
+
# Format: mailbox@hostname
|
6
|
+
#
|
7
|
+
class Factory
|
8
|
+
attr_reader :mailbox, :host
|
9
|
+
|
10
|
+
def address(address)
|
11
|
+
(@mailbox, host) = address.strip.split(/\@/)
|
12
|
+
return unless host
|
13
|
+
@host = EmailAddress::Host.new(host)
|
14
|
+
@mailbox = @host.provider_address(@mailbox)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# EmailAddress::Address
|
2
|
+
# EmailAddress::Host
|
3
|
+
# EmailAddress::MailExchanger
|
4
|
+
# EmailAddress::Config
|
5
|
+
# EmailAddress::EspMapping
|
6
|
+
# EmailAddress::Esp::Base, Yahoo, Msn, ...
|
7
|
+
|
8
|
+
require 'simpleidn'
|
9
|
+
|
10
|
+
module EmailAddress
|
11
|
+
|
12
|
+
# EmailAddress::Host handles mail host properties of an email address
|
13
|
+
# The host is typically the data to the right of the @ in the address
|
14
|
+
# and consists of:
|
15
|
+
#
|
16
|
+
# * hostname - full name of DNS host with the MX record.
|
17
|
+
# * domain_name - generally, the name and TLD without subdomain
|
18
|
+
# * base_domain - the identity name of the domain name, without the tld
|
19
|
+
# * tld (top-level-domain), like .com, .co.jp, .com.xx, etc.
|
20
|
+
# * subdomain - optional name of server/service under the domain
|
21
|
+
# * esp (email service provider) a name of the provider: yahoo, msn, etc.
|
22
|
+
# * dns_hostname - Converted hostname Unicode to Punycode
|
23
|
+
|
24
|
+
class Host
|
25
|
+
attr_reader :host, :domain_name, :tld, :base_domain, :subdomains, :dns_hostname
|
26
|
+
|
27
|
+
def initialize(host)
|
28
|
+
host.gsub!(/\A.*@/, '')
|
29
|
+
host.downcase!
|
30
|
+
self.host = host
|
31
|
+
end
|
32
|
+
|
33
|
+
def address(mailbox)
|
34
|
+
# Determine EmailAddress::Provider::Xxxx
|
35
|
+
EmailAddress::Address.new(mailbox, self)
|
36
|
+
end
|
37
|
+
|
38
|
+
def host=(host)
|
39
|
+
@host = host
|
40
|
+
# Patterns: *.com, *.xx.cc, *.cc
|
41
|
+
if @host =~ /(.+)\.(\w{3,10})\z/ || @host =~ /(.+)\.(\w{1,3}\.\w\w)\z/ || @host =~ /(.+)\.(\w\w)\z/
|
42
|
+
@tld = $2;
|
43
|
+
sld = $1 # Second level domain
|
44
|
+
if @sld =~ /(.+)\.(.+)$/ # is subdomain?
|
45
|
+
@subdomains = $1
|
46
|
+
@base_domain = $2
|
47
|
+
else
|
48
|
+
@subdomains = ""
|
49
|
+
@base_domain = sld
|
50
|
+
end
|
51
|
+
@domain_name = @base_domain + '.' + @tld
|
52
|
+
@dns_hostname = SimpleIDN.to_ascii(@host)
|
53
|
+
@host
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Resets the host to the domain name, dropping any subdomain
|
58
|
+
def drop_subdomain!
|
59
|
+
self.hostname = domain_name
|
60
|
+
end
|
61
|
+
|
62
|
+
def valid?
|
63
|
+
return false unless valid_format?
|
64
|
+
return false unless valid_mx?
|
65
|
+
true
|
66
|
+
end
|
67
|
+
|
68
|
+
def valid_format?
|
69
|
+
Host.valid_format?(@host)
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.valid_format?(host)
|
73
|
+
return false unless host.match(/\A([0-9a-z\-]{1,63}\.)+[a-z0-9\-]{2,15}\z/)
|
74
|
+
return false unless host.length <= 253
|
75
|
+
true
|
76
|
+
end
|
77
|
+
|
78
|
+
def valid_mx?
|
79
|
+
Host.valid_mx?(@host)
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.valid_mx?(host)
|
83
|
+
true
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'resolv'
|
2
|
+
require 'netaddr'
|
3
|
+
require 'socket'
|
4
|
+
|
5
|
+
class MailExchanger
|
6
|
+
cattr_accessor :domains, :lookups
|
7
|
+
|
8
|
+
def self.valid_mx?(domain)
|
9
|
+
dns_a_record_exists?(domain) || mxers(domain).size > 0
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.dns_a_record_exists?(domain)
|
13
|
+
@dns_a_record ||= {}
|
14
|
+
@dns_a_record[domain] = false
|
15
|
+
if Socket.gethostbyname(domain)
|
16
|
+
return @dns_a_record[domain] = true
|
17
|
+
end
|
18
|
+
rescue SocketError # not found
|
19
|
+
@dns_a_record[domain] = false
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns DNS A record results for the domain as: [[domain, ip, 0],...]
|
23
|
+
def self.domain_hosts(domain)
|
24
|
+
@domain_hosts ||= {}
|
25
|
+
@domain_hosts[domain] = []
|
26
|
+
res = TCPSocket.gethostbyname(domain)
|
27
|
+
res = res.slice(3, res.size)
|
28
|
+
res.each { |r| @domain_hosts[domain] << [domain, r, 0] }
|
29
|
+
@domain_hosts[domain]
|
30
|
+
|
31
|
+
rescue SocketError
|
32
|
+
return []
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns: [["mta7.am0.yahoodns.net", "66.94.237.139", 1], ["mta5.am0.yahoodns.net", "67.195.168.230", 1], ["mta6.am0.yahoodns.net", "98.139.54.60", 1]]
|
36
|
+
# If not found, returns []
|
37
|
+
def self.mxers(domain)
|
38
|
+
@domains ||= {}
|
39
|
+
return @domains[domain] if @domains.key?(domain)
|
40
|
+
@lookups = @lookups ? @lookups + 1 : 1
|
41
|
+
mx = nil
|
42
|
+
mxs = Resolv::DNS.open do |dns|
|
43
|
+
ress = dns.getresources domain, Resolv::DNS::Resource::IN::MX
|
44
|
+
ress.map { |r| [r.exchange.to_s, IPSocket::getaddress(r.exchange.to_s), r.preference] }
|
45
|
+
end
|
46
|
+
@domains[domain] = mxs
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns an array of MX IP address (String) for the given email domain
|
50
|
+
def self.mx_ips(domain)
|
51
|
+
mxers(domain).map {|m| m[1] }
|
52
|
+
end
|
53
|
+
|
54
|
+
# Given a cidr (ip/bits) and ip address, returns true on match. Caches cidr object.
|
55
|
+
def self.in_cidr?(cidr, ip)
|
56
|
+
@cidrs ||= {}
|
57
|
+
@cidrs[cidr] ||= NetAddr::CIDR.create(cidr)
|
58
|
+
@cidrs[cidr].matches?(ip)
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module EmailAddress
|
2
|
+
|
3
|
+
# EmailAddress::Mailbox - Left side of the @
|
4
|
+
#
|
5
|
+
# * mailbox - Everything to the left of the @
|
6
|
+
# * account - part of the mailbox typically sent to a user
|
7
|
+
# * tags - Address tags appended to the account for tracking
|
8
|
+
#
|
9
|
+
class Mailbox
|
10
|
+
attr_reader :mailbox, :account, :tags, :provider
|
11
|
+
|
12
|
+
def initialize(mailbox, mail_provider=nil)
|
13
|
+
@provider = mail_provider
|
14
|
+
@mailbox = mailbox
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
@mailbox
|
19
|
+
end
|
20
|
+
|
21
|
+
def mailbox=(mailbox)
|
22
|
+
@mailbox = mailbox.strip.downcase
|
23
|
+
(@account, @tags) = @mailbox.split(@provider.tag_separator)
|
24
|
+
@mailbox
|
25
|
+
end
|
26
|
+
|
27
|
+
def valid?
|
28
|
+
return false unless provider.valid?(mailbox)
|
29
|
+
true
|
30
|
+
end
|
31
|
+
|
32
|
+
def valid_format?
|
33
|
+
return false unless provider.valid_format?(mailbox)
|
34
|
+
#return false unless @mailbox =~ /\A\w[\w\.\-\+\']*\z/
|
35
|
+
true
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns true if the email account is a standard reserved address
|
39
|
+
def reserved?
|
40
|
+
%Q(postmaster abuse).include?(account)
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module EmailAddress
|
2
|
+
module Providers
|
3
|
+
class Default
|
4
|
+
def initialize(address)
|
5
|
+
@mailbox = mailbox
|
6
|
+
end
|
7
|
+
|
8
|
+
def account(mailbox)
|
9
|
+
mailbox
|
10
|
+
end
|
11
|
+
|
12
|
+
def provider
|
13
|
+
'default'
|
14
|
+
end
|
15
|
+
|
16
|
+
def tag_separator
|
17
|
+
'+'
|
18
|
+
end
|
19
|
+
|
20
|
+
def case_sensitive_mailbox
|
21
|
+
false
|
22
|
+
end
|
23
|
+
|
24
|
+
def max_domain_length
|
25
|
+
253
|
26
|
+
end
|
27
|
+
|
28
|
+
def max_email_length
|
29
|
+
254
|
30
|
+
end
|
31
|
+
|
32
|
+
# Letters, numbers, period (no start) 6-30chars
|
33
|
+
def max_mailbox_length
|
34
|
+
64
|
35
|
+
end
|
36
|
+
|
37
|
+
# Letters, numbers, period (no start) 6-30chars
|
38
|
+
def user_pattern
|
39
|
+
/\A[a-z0-9][\.\'a-z0-9]{5,29}\z/i
|
40
|
+
end
|
41
|
+
|
42
|
+
def valid?
|
43
|
+
return false unless valid_format?
|
44
|
+
end
|
45
|
+
|
46
|
+
def valid_format?
|
47
|
+
return false if mailbox.length > max_mailbox_length
|
48
|
+
return false if address.length > max_email_length
|
49
|
+
return false unless mailbox.to_s.match(user_pattern)
|
50
|
+
true
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module EmailAddress
|
2
|
+
module Providers
|
3
|
+
class Google < Default
|
4
|
+
def account
|
5
|
+
account.gsub(/\./. '')
|
6
|
+
end
|
7
|
+
|
8
|
+
def provider
|
9
|
+
'google'
|
10
|
+
end
|
11
|
+
|
12
|
+
def tag_separator
|
13
|
+
'+'
|
14
|
+
end
|
15
|
+
|
16
|
+
def case_sensitive_mailbox
|
17
|
+
false
|
18
|
+
end
|
19
|
+
|
20
|
+
# Letters, numbers, period (no start) 6-30chars
|
21
|
+
def user_pattern
|
22
|
+
/\A[a-z0-9][\.a-z0-9]{5,29}\z/i
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require_relative 'test_helper'
|
3
|
+
|
4
|
+
class TestEmailAddress < MiniTest::Unit::TestCase
|
5
|
+
def test_address
|
6
|
+
a = EmailAddress.new('user@example.com')
|
7
|
+
assert_equal a.local, 'user'
|
8
|
+
assert_equal a.tag, ''
|
9
|
+
assert_equal a.comment, ''
|
10
|
+
assert_equal a.domain, 'example.com'
|
11
|
+
assert_equal a.subdomains, ''
|
12
|
+
assert_equal a.base_domain, 'example'
|
13
|
+
assert_equal a.dns_hostname, 'example.com'
|
14
|
+
assert_equal a.top_level_domain, 'com'
|
15
|
+
assert_equal a.to_s, 'user@example.com'
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_foreign_address
|
19
|
+
a = EmailAddress.new("user@sub.example.co.jp")
|
20
|
+
assert_equal a.domain, "sub.example.co.jp"
|
21
|
+
assert_equal a.subdomains, "sub"
|
22
|
+
assert_equal a.domain_name, "example.co.jp"
|
23
|
+
assert_equal a.base_domain, "example"
|
24
|
+
assert_equal a.top_level_domain, "co.jp"
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_address_tag
|
28
|
+
a = EmailAddress.new('user+etc@example.com')
|
29
|
+
assert_equal a.account, 'user'
|
30
|
+
assert_equal a.tag, 'etc'
|
31
|
+
assert_equal a.unique_address, 'user@example.com'
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_address_comment
|
35
|
+
a = EmailAddress.new('(comment)user@example.com')
|
36
|
+
assert_equal a.comment, 'comment'
|
37
|
+
assert_equal a.account, 'user'
|
38
|
+
assert_equal a.unique_address, 'user@example.com'
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_user_validation
|
42
|
+
a = EmailAddress.new("user@example.co.jp")
|
43
|
+
assert a.valid? == true
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_unicode_domain
|
47
|
+
a = EmailAddress.new("User@København.eu")
|
48
|
+
assert_equal a.dns_hostname, 'xn--kbenhavn-54a.eu'
|
49
|
+
assert_equal a.unique_address, 'user@xn--kbenhavn-54a.eu'
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
#encoding: utf-8
|
2
|
+
require_relative '../test_helper'
|
3
|
+
|
4
|
+
class TestAddress < MiniTest::Unit::TestCase
|
5
|
+
def test_address
|
6
|
+
a = EmailAddress.new("User+tag@example.com")
|
7
|
+
assert_equal "user", a.account
|
8
|
+
assert_equal "user+tag", a.local
|
9
|
+
assert_equal "user@example.com", a.unique_address
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_unicode_user
|
13
|
+
a = EmailAddress.new("å@example.com")
|
14
|
+
assert_equal false, a.valid_format?
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
#encoding: utf-8
|
2
|
+
require_relative '../test_helper'
|
3
|
+
#require 'minitest/autorun'
|
4
|
+
|
5
|
+
class TestConfig < MiniTest::Unit::TestCase
|
6
|
+
def test_config
|
7
|
+
EmailAddress::Config.setup do
|
8
|
+
add_provider :google, domain_names: %w(gmail.com googlemail.com google.com)
|
9
|
+
end
|
10
|
+
assert_equal EmailAddress::Config.get.provider_matching_rules.first[:provider], :google
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require_relative '../test_helper'
|
3
|
+
|
4
|
+
|
5
|
+
class TestHost < MiniTest::Unit::TestCase
|
6
|
+
def test_host
|
7
|
+
a = EmailAddress::Host.new("example.com")
|
8
|
+
assert_equal "example.com", a.host
|
9
|
+
assert_equal "example.com", a.domain_name
|
10
|
+
assert_equal "example", a.base_domain
|
11
|
+
assert_equal ".com", a.tld
|
12
|
+
assert_equal "", a.subdomains
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_foreign_host
|
16
|
+
a = EmailAddress::Host.new("yahoo.co.jp")
|
17
|
+
assert_equal "yahoo.co.jp", a.host
|
18
|
+
assert_equal "yahoo.co.jp", a.domain_name
|
19
|
+
assert_equal "yahoo", a.base_domain
|
20
|
+
assert_equal "co.jp", a.tld
|
21
|
+
assert_equal "", a.subdomains
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_unicode_host
|
25
|
+
a = EmailAddress::Host.new("å.com")
|
26
|
+
assert_equal "xn--5ca.com", a.dns_hostname
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: email_address
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Allen Fair
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-01-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: simpleidn
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: |-
|
56
|
+
The EmailAddress library is an _opinionated_ email address handler and
|
57
|
+
validator.
|
58
|
+
email:
|
59
|
+
- allen.fair@gmail.com
|
60
|
+
executables: []
|
61
|
+
extensions: []
|
62
|
+
extra_rdoc_files: []
|
63
|
+
files:
|
64
|
+
- ".gitignore"
|
65
|
+
- Gemfile
|
66
|
+
- LICENSE.txt
|
67
|
+
- README.md
|
68
|
+
- Rakefile
|
69
|
+
- email_address.gemspec
|
70
|
+
- lib/email_address.rb
|
71
|
+
- lib/email_address/address.rb
|
72
|
+
- lib/email_address/config.rb
|
73
|
+
- lib/email_address/esp.rb
|
74
|
+
- lib/email_address/host.rb
|
75
|
+
- lib/email_address/local.rb
|
76
|
+
- lib/email_address/providers/default.rb
|
77
|
+
- lib/email_address/providers/google.rb
|
78
|
+
- lib/email_address/version.rb
|
79
|
+
- lib/email_providers/address.rb
|
80
|
+
- lib/email_providers/config.rb
|
81
|
+
- lib/email_providers/factory.rb
|
82
|
+
- lib/email_providers/host.rb
|
83
|
+
- lib/email_providers/mail_exchanger.rb
|
84
|
+
- lib/email_providers/mailbox.rb
|
85
|
+
- lib/email_providers/providers/default.rb
|
86
|
+
- lib/email_providers/providers/google.rb
|
87
|
+
- lib/email_providers/version.rb
|
88
|
+
- test/email_address.rb
|
89
|
+
- test/email_address/address.rb
|
90
|
+
- test/email_address/config.rb
|
91
|
+
- test/email_address/host.rb
|
92
|
+
- test/email_address/mail_exchanger.rb
|
93
|
+
- test/test_helper.rb
|
94
|
+
homepage: https://github.com/afair/email_address
|
95
|
+
licenses:
|
96
|
+
- MIT
|
97
|
+
metadata: {}
|
98
|
+
post_install_message:
|
99
|
+
rdoc_options: []
|
100
|
+
require_paths:
|
101
|
+
- lib
|
102
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
107
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - ">="
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '0'
|
112
|
+
requirements: []
|
113
|
+
rubyforge_project:
|
114
|
+
rubygems_version: 2.2.0.rc.1
|
115
|
+
signing_key:
|
116
|
+
specification_version: 4
|
117
|
+
summary: EmailAddress checks on validates an acceptable set of email addresses.
|
118
|
+
test_files:
|
119
|
+
- test/email_address.rb
|
120
|
+
- test/email_address/address.rb
|
121
|
+
- test/email_address/config.rb
|
122
|
+
- test/email_address/host.rb
|
123
|
+
- test/email_address/mail_exchanger.rb
|
124
|
+
- test/test_helper.rb
|
125
|
+
has_rdoc:
|