email_address 0.0.3 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +10 -0
- data/Gemfile +0 -1
- data/README.md +451 -197
- data/Rakefile +4 -9
- data/email_address.gemspec +9 -5
- data/lib/email_address.rb +55 -24
- data/lib/email_address/active_record_validator.rb +5 -5
- data/lib/email_address/address.rb +152 -72
- data/lib/email_address/canonical_email_address_type.rb +46 -0
- data/lib/email_address/config.rb +148 -64
- data/lib/email_address/email_address_type.rb +15 -31
- data/lib/email_address/exchanger.rb +31 -34
- data/lib/email_address/host.rb +327 -51
- data/lib/email_address/local.rb +304 -52
- data/lib/email_address/version.rb +1 -1
- data/test/activerecord/test_ar.rb +22 -0
- data/test/activerecord/user.rb +71 -0
- data/test/email_address/test_address.rb +53 -27
- data/test/email_address/test_config.rb +23 -8
- data/test/email_address/test_exchanger.rb +22 -10
- data/test/email_address/test_host.rb +47 -6
- data/test/email_address/test_local.rb +80 -16
- data/test/test_email_address.rb +38 -4
- data/test/test_helper.rb +7 -5
- metadata +68 -34
- data/lib/email_address/domain_matcher.rb +0 -98
- data/lib/email_address/domain_parser.rb +0 -69
- data/lib/email_address/matcher.rb +0 -119
- data/lib/email_address/validator.rb +0 -141
- data/test/email_address/test_domain_matcher.rb +0 -21
- data/test/email_address/test_domain_parser.rb +0 -29
- data/test/email_address/test_matcher.rb +0 -44
- data/test/email_address/test_validator.rb +0 -16
@@ -0,0 +1,46 @@
|
|
1
|
+
################################################################################
|
2
|
+
# ActiveRecord v5.0 Custom Type
|
3
|
+
#
|
4
|
+
# 1) Register your types
|
5
|
+
#
|
6
|
+
# # config/initializers/email_address.rb
|
7
|
+
# ActiveRecord::Type.register(:email_address, EmailAddress::Address)
|
8
|
+
# ActiveRecord::Type.register(:canonical_email_address,
|
9
|
+
# EmailAddress::CanonicalEmailAddressType)
|
10
|
+
#
|
11
|
+
# 2) Define your email address columns in your model class
|
12
|
+
#
|
13
|
+
# class User < ApplicationRecord
|
14
|
+
# attribute :email, :email_address
|
15
|
+
# attribute :canonical_email, :canonical_email_address
|
16
|
+
#
|
17
|
+
# def email=(email_address)
|
18
|
+
# self[:canonical_email] = email_address
|
19
|
+
# self[:email] = email_address
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# 3) Profit!
|
24
|
+
#
|
25
|
+
# user = User.new(email:"Pat.Smith+registrations@gmail.com")
|
26
|
+
# user.email #=> "pat.smith+registrations@gmail.com"
|
27
|
+
# user.canonical_email #=> "patsmith@gmail.com"
|
28
|
+
################################################################################
|
29
|
+
|
30
|
+
class EmailAddress::CanonicalEmailAddressType < ActiveRecord::Type::Value
|
31
|
+
|
32
|
+
# From user input, setter
|
33
|
+
def cast(value)
|
34
|
+
super(EmailAddress.canonical(value))
|
35
|
+
end
|
36
|
+
|
37
|
+
# From a database value
|
38
|
+
def deserialize(value)
|
39
|
+
value && EmailAddress.normal(value)
|
40
|
+
end
|
41
|
+
|
42
|
+
# To a database value (string)
|
43
|
+
def serialize(value)
|
44
|
+
value && EmailAddress.normal(value)
|
45
|
+
end
|
46
|
+
end
|
data/lib/email_address/config.rb
CHANGED
@@ -1,83 +1,167 @@
|
|
1
1
|
module EmailAddress
|
2
|
+
# Global configurations and for default/unknown providers. Settings are:
|
3
|
+
#
|
4
|
+
# * dns_lookup: :mx, :a, :off
|
5
|
+
# Enables DNS lookup for validation by
|
6
|
+
# :mx - DNS MX Record lookup
|
7
|
+
# :a - DNS A Record lookup (as some domains don't specify an MX incorrectly)
|
8
|
+
# :off - Do not perform DNS lookup (Test mode, network unavailable)
|
9
|
+
#
|
10
|
+
# * sha1_secret ""
|
11
|
+
# This application-level secret is appended to the email_address to compute
|
12
|
+
# the SHA1 Digest, making it unique to your application so it can't easily be
|
13
|
+
# discovered by comparing against a known list of email/sha1 pairs.
|
14
|
+
#
|
15
|
+
# For local part configuration:
|
16
|
+
# * local_downcase: true
|
17
|
+
# Downcase the local part. You probably want this for uniqueness.
|
18
|
+
# RFC says local part is case insensitive, that's a bad part.
|
19
|
+
#
|
20
|
+
# * local_fix: true,
|
21
|
+
# Make simple fixes when available, remove spaces, condense multiple punctuations
|
22
|
+
#
|
23
|
+
# * local_encoding: :ascii, :unicode,
|
24
|
+
# Enable Unicode in local part. Most mail systems do not yet support this.
|
25
|
+
# You probably want to stay with ASCII for now.
|
26
|
+
#
|
27
|
+
# * local_parse: nil, ->(local) { [mailbox, tag, comment] }
|
28
|
+
# Specify an optional lambda/Proc to parse the local part. It should return an
|
29
|
+
# array (tuple) of mailbox, tag, and comment.
|
30
|
+
#
|
31
|
+
# * local_format: :conventional, :relaxed, :redacted, :standard, Proc
|
32
|
+
# :conventional word ( puncuation{1} word )*
|
33
|
+
# :relaxed alphanum ( allowed_characters)* alphanum
|
34
|
+
# :standard RFC Compliant email addresses (anything goes!)
|
35
|
+
#
|
36
|
+
# * local_size: 1..64,
|
37
|
+
# A Range specifying the allowed size for mailbox + tags + comment
|
38
|
+
#
|
39
|
+
# * tag_separator: nil, character (+)
|
40
|
+
# Nil, or a character used to split the tag from the mailbox
|
41
|
+
#
|
42
|
+
# For the mailbox (AKA account, role), without the tag
|
43
|
+
# * mailbox_size: 1..64
|
44
|
+
# A Range specifying the allowed size for mailbox
|
45
|
+
#
|
46
|
+
# * mailbox_canonical: nil, ->(mailbox) { mailbox }
|
47
|
+
# An optional lambda/Proc taking a mailbox name, returning a canonical
|
48
|
+
# version of it. (E.G.: gmail removes '.' characters)
|
49
|
+
#
|
50
|
+
# * mailbox_validator: nil, ->(mailbox) { true }
|
51
|
+
# An optional lambda/Proc taking a mailbox name, returning true or false.
|
52
|
+
#
|
53
|
+
# * host_encoding: :punycode, :unicode,
|
54
|
+
# How to treat International Domain Names (IDN). Note that most mail and
|
55
|
+
# DNS systems do not support unicode, so punycode needs to be passed.
|
56
|
+
# :punycode Convert Unicode names to punycode representation
|
57
|
+
# :unicode Keep Unicode names as is.
|
58
|
+
#
|
59
|
+
# * host_validation:
|
60
|
+
# :mx Ensure host is configured with DNS MX records
|
61
|
+
# :a Ensure host is known to DNS (A Record)
|
62
|
+
# :syntax Validate by syntax only, no Network verification
|
63
|
+
# :connect Attempt host connection (not implemented, BAD!)
|
64
|
+
#
|
65
|
+
# * host_size: 1..253,
|
66
|
+
# A range specifying the size limit of the host part,
|
67
|
+
#
|
68
|
+
# * host_allow_ip: false,
|
69
|
+
# Allow IP address format in host: [127.0.0.1], [IPv6:::1]
|
70
|
+
#
|
71
|
+
# * address_validation: :parts, :smtp, ->(address) { true }
|
72
|
+
# Address validation policy
|
73
|
+
# :parts Validate local and host.
|
74
|
+
# :smtp Validate via SMTP (not implemented, BAD!)
|
75
|
+
# A lambda/Proc taking the address string, returning true or false
|
76
|
+
#
|
77
|
+
# * address_size: 3..254,
|
78
|
+
# A range specifying the size limit of the complete address
|
79
|
+
#
|
80
|
+
# * address_local: false,
|
81
|
+
# Allow localhost, no domain, or local subdomains.
|
82
|
+
#
|
83
|
+
# For provider rules to match to domain names and Exchanger hosts
|
84
|
+
# The value is an array of match tokens.
|
85
|
+
# * host_match: %w(.org example.com hotmail. user*@ sub.*.com)
|
86
|
+
# * exchanger_match: %w(google.com 127.0.0.1 10.9.8.0/24 ::1/64)
|
87
|
+
|
2
88
|
class Config
|
3
|
-
@
|
4
|
-
|
5
|
-
|
89
|
+
@config = {
|
90
|
+
dns_lookup: :mx, # :mx, :a, :off
|
91
|
+
sha1_secret: "",
|
92
|
+
munge_string: "*****",
|
93
|
+
|
94
|
+
local_downcase: true,
|
95
|
+
local_fix: true,
|
96
|
+
local_encoding: :ascii, # :ascii, :unicode,
|
97
|
+
local_parse: nil, # nil, Proc
|
98
|
+
local_format: :conventional, # :conventional, :relaxed, :redacted, :standard, Proc
|
99
|
+
local_size: 1..64,
|
100
|
+
tag_separator: '+', # nil, character
|
101
|
+
mailbox_size: 1..64, # without tag
|
102
|
+
mailbox_canonical: nil, # nil, Proc
|
103
|
+
mailbox_validator: nil, # nil, Proc
|
104
|
+
|
105
|
+
host_encoding: :punycode || :unicode,
|
106
|
+
host_validation: :mx || :a || :connect,
|
107
|
+
host_size: 1..253,
|
108
|
+
host_allow_ip: false,
|
109
|
+
|
110
|
+
address_validation: :parts, # :parts, :smtp, Proc
|
111
|
+
address_size: 3..254,
|
112
|
+
address_localhost: false,
|
6
113
|
}
|
7
114
|
|
8
115
|
@providers = {
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
domains: %w(gmail.com googlemail.com),
|
27
|
-
exchangers: %w(google.com),
|
28
|
-
local_size: 5..64,
|
29
|
-
canonical_mailbox: ->(m) {m.gsub('.','')},
|
30
|
-
#valid_mailbox: ->(local) { local.mailbox =~ /\A[a-z0-9][\.a-z0-9]{5,29}\z/i},
|
31
|
-
},
|
32
|
-
msn: {
|
33
|
-
valid_mailbox: ->(m) { m =~ /\A[a-z0-9][\.\-a-z0-9]{5,29}\z/i},
|
34
|
-
},
|
35
|
-
yahoo: {
|
36
|
-
domains: %w(yahoo ymail rocketmail),
|
37
|
-
exchangers: %w(yahoodns yahoo-inc),
|
38
|
-
},
|
116
|
+
aol: {
|
117
|
+
host_match: %w(aol. compuserve. netscape. aim. cs.),
|
118
|
+
},
|
119
|
+
google: {
|
120
|
+
host_match: %w(gmail.com googlemail.com),
|
121
|
+
exchanger_match: %w(google.com),
|
122
|
+
local_size: 5..64,
|
123
|
+
mailbox_canonical: ->(m) {m.gsub('.','')},
|
124
|
+
},
|
125
|
+
msn: {
|
126
|
+
host_match: %w(msn. hotmail. outlook. live.),
|
127
|
+
mailbox_validator: ->(m,t) { m =~ /\A[a-z0-9][\.\-a-z0-9]{5,29}\z/i},
|
128
|
+
},
|
129
|
+
yahoo: {
|
130
|
+
host_match: %w(yahoo. ymail. rocketmail.),
|
131
|
+
exchanger_match: %w(yahoodns yahoo-inc),
|
132
|
+
},
|
39
133
|
}
|
40
134
|
|
41
|
-
|
42
|
-
|
135
|
+
# Set multiple default configuration settings
|
136
|
+
def self.configure(config={})
|
137
|
+
@config.merge!(config)
|
43
138
|
end
|
44
139
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
def self.options
|
50
|
-
@options
|
140
|
+
def self.setting(name, *value)
|
141
|
+
name = name.to_sym
|
142
|
+
@config[name] = value.first if value.size > 0
|
143
|
+
@config[name]
|
51
144
|
end
|
52
145
|
|
53
|
-
|
54
|
-
|
146
|
+
# Returns the hash of Provider rules
|
147
|
+
def self.providers
|
148
|
+
@providers
|
55
149
|
end
|
56
150
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
@providers
|
62
|
-
|
63
|
-
|
64
|
-
def do_block(&block)
|
65
|
-
instance_eval(&block)
|
66
|
-
end
|
67
|
-
|
68
|
-
def provider(name, defn={})
|
69
|
-
EmailAddress::Config.providers[name] = defn
|
70
|
-
end
|
71
|
-
|
72
|
-
def option(name, value)
|
73
|
-
EmailAddress::Config.options[name.to_sym] = value
|
151
|
+
# Configure or lookup a provider by name.
|
152
|
+
def self.provider(name, config={})
|
153
|
+
name = name.to_sym
|
154
|
+
if config.size > 0
|
155
|
+
@providers[name] ||= @config.clone
|
156
|
+
@providers[name].merge!(config)
|
74
157
|
end
|
158
|
+
@providers[name]
|
75
159
|
end
|
76
160
|
|
77
|
-
def self.
|
78
|
-
|
79
|
-
|
80
|
-
|
161
|
+
def self.all_settings(*configs)
|
162
|
+
config = @config.clone
|
163
|
+
configs.each {|c| config.merge!(c) }
|
164
|
+
config
|
81
165
|
end
|
82
166
|
end
|
83
167
|
end
|
@@ -1,28 +1,30 @@
|
|
1
1
|
################################################################################
|
2
2
|
# ActiveRecord v5.0 Custom Type
|
3
|
-
# This class is not automatically loaded by the gem.
|
4
|
-
#-------------------------------------------------------------------------------
|
5
|
-
# 1) Register this type
|
6
3
|
#
|
7
|
-
#
|
8
|
-
#
|
4
|
+
# 1) Register your types
|
5
|
+
#
|
6
|
+
# # config/initializers/email_address.rb
|
9
7
|
# ActiveRecord::Type.register(:email_address, EmailAddress::Address)
|
10
8
|
# ActiveRecord::Type.register(:canonical_email_address,
|
11
9
|
# EmailAddress::CanonicalEmailAddressType)
|
12
10
|
#
|
13
11
|
# 2) Define your email address columns in your model class
|
14
12
|
#
|
15
|
-
# class User <
|
13
|
+
# class User < ApplicationRecord
|
16
14
|
# attribute :email, :email_address
|
17
|
-
# attribute :
|
15
|
+
# attribute :canonical_email, :canonical_email_address
|
16
|
+
#
|
17
|
+
# def email=(email_address)
|
18
|
+
# self[:canonical_email] = email_address
|
19
|
+
# self[:email] = email_address
|
20
|
+
# end
|
18
21
|
# end
|
19
22
|
#
|
20
23
|
# 3) Profit!
|
21
24
|
#
|
22
|
-
# user = User.new(email:"Pat.Smith+registrations@gmail.com"
|
23
|
-
#
|
24
|
-
# user.
|
25
|
-
# user.unique_email #=> "patsmith@gmail.com"
|
25
|
+
# user = User.new(email:"Pat.Smith+registrations@gmail.com")
|
26
|
+
# user.email #=> "pat.smith+registrations@gmail.com"
|
27
|
+
# user.canonical_email #=> "patsmith@gmail.com"
|
26
28
|
################################################################################
|
27
29
|
|
28
30
|
class EmailAddress::EmailAddressType < ActiveRecord::Type::Value
|
@@ -34,29 +36,11 @@ class EmailAddress::EmailAddressType < ActiveRecord::Type::Value
|
|
34
36
|
|
35
37
|
# From a database value
|
36
38
|
def deserialize(value)
|
37
|
-
EmailAddress.normal(value)
|
39
|
+
value && EmailAddress.normal(value)
|
38
40
|
end
|
39
41
|
#
|
40
42
|
# To a database value (string)
|
41
43
|
def serialize(value)
|
42
|
-
EmailAddress.normal(value)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
class CanonicalEmailAddressType < EmailAddress::EmailAddressType
|
47
|
-
|
48
|
-
# From user input, setter
|
49
|
-
def cast(value)
|
50
|
-
super(EmailAddress.canonical(value))
|
51
|
-
end
|
52
|
-
|
53
|
-
# From a database value
|
54
|
-
def deserialize(value)
|
55
|
-
EmailAddress.canonical(value)
|
56
|
-
end
|
57
|
-
|
58
|
-
# To a database value (string)
|
59
|
-
def serialize(value)
|
60
|
-
EmailAddress.canonical(value)
|
44
|
+
value && EmailAddress.normal(value)
|
61
45
|
end
|
62
46
|
end
|
@@ -20,9 +20,9 @@ module EmailAddress
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
-
def initialize(host,
|
23
|
+
def initialize(host, config={})
|
24
24
|
@host = host
|
25
|
-
@
|
25
|
+
@config = config
|
26
26
|
end
|
27
27
|
|
28
28
|
def each(&block)
|
@@ -33,24 +33,13 @@ module EmailAddress
|
|
33
33
|
|
34
34
|
# Returns the provider name based on the MX-er host names, or nil if not matched
|
35
35
|
def provider
|
36
|
-
|
37
|
-
EmailAddress::Config.providers.each do |
|
38
|
-
|
39
|
-
|
40
|
-
return name if DomainMatcher.matches?(m[:host], defn[:exchangers])
|
36
|
+
return @provider if @provider
|
37
|
+
EmailAddress::Config.providers.each do |provider, config|
|
38
|
+
if config[:exchanger_match] && self.matches?(config[:exchanger_match])
|
39
|
+
return @provider = provider
|
41
40
|
end
|
42
41
|
end
|
43
|
-
|
44
|
-
end
|
45
|
-
|
46
|
-
def has_dns_a_record?
|
47
|
-
dns_a_record.size > 0 ? true : false
|
48
|
-
end
|
49
|
-
|
50
|
-
def dns_a_record
|
51
|
-
@_dns_a_record ||= Socket.gethostbyname(@host)
|
52
|
-
rescue SocketError # not found, but could also mean network not work
|
53
|
-
@_dns_a_record ||= []
|
42
|
+
@provider = :default
|
54
43
|
end
|
55
44
|
|
56
45
|
# 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]]
|
@@ -66,7 +55,7 @@ module EmailAddress
|
|
66
55
|
|
67
56
|
# Returns Array of domain names for the MX'ers, used to determine the Provider
|
68
57
|
def domains
|
69
|
-
mxers.map {|m| EmailAddress::
|
58
|
+
@_domains ||= mxers.map {|m| EmailAddress::Host.new(m.first).domain_name }.sort.uniq
|
70
59
|
end
|
71
60
|
|
72
61
|
# Returns an array of MX IP address (String) for the given email domain
|
@@ -74,26 +63,34 @@ module EmailAddress
|
|
74
63
|
mxers.map {|m| m[1] }
|
75
64
|
end
|
76
65
|
|
66
|
+
# Simple matcher, takes an array of CIDR addresses (ip/bits) and strings.
|
67
|
+
# Returns true if any MX IP matches the CIDR or host name ends in string.
|
68
|
+
# Ex: match?(%w(127.0.0.1/32 0:0:1/64 .yahoodns.net))
|
69
|
+
# Note: Your networking stack may return IPv6 addresses instead of IPv4
|
70
|
+
# when both are available. If matching on IP, be sure to include both
|
71
|
+
# IPv4 and IPv6 forms for matching for hosts running on IPv6 (like gmail).
|
72
|
+
def matches?(rules)
|
73
|
+
rules = Array(rules)
|
74
|
+
rules.each do |rule|
|
75
|
+
if rule.include?("/")
|
76
|
+
return rule if self.in_cidr?(rule)
|
77
|
+
else
|
78
|
+
self.each {|mx| return rule if mx[:host].end_with?(rule) }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
false
|
82
|
+
end
|
83
|
+
|
77
84
|
# Given a cidr (ip/bits) and ip address, returns true on match. Caches cidr object.
|
78
85
|
def in_cidr?(cidr)
|
86
|
+
c = NetAddr::CIDR.create(cidr)
|
79
87
|
if cidr.include?(":")
|
80
|
-
|
88
|
+
mx_ips.find { |ip| ip.include?(":") && c.matches?(ip) } ? true : false
|
89
|
+
elsif cidr.include?(".")
|
90
|
+
mx_ips.find { |ip| !ip.include?(":") && c.matches?(ip) } ? true : false
|
81
91
|
else
|
82
|
-
|
92
|
+
false
|
83
93
|
end
|
84
94
|
end
|
85
|
-
|
86
|
-
private
|
87
|
-
|
88
|
-
def in_ipv4_cidr?(cidr)
|
89
|
-
cidr = NetAddr::CIDR.create(cidr)
|
90
|
-
mx_ips.find { |ip| !ip.include?(":") && cidr.matches?(ip) } ? true : false
|
91
|
-
end
|
92
|
-
|
93
|
-
def in_ipv6_cidr?(cidr)
|
94
|
-
cidr = NetAddr::CIDR.create(cidr)
|
95
|
-
mx_ips.find { |ip| ip.include?(":") && cidr.matches?(ip) } ? true : false
|
96
|
-
end
|
97
|
-
|
98
95
|
end
|
99
96
|
end
|