email_address 0.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 +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:
|