email_address 0.0.3 → 0.1.0
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 +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
|