email_address 0.1.13 → 0.1.18
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 +1 -2
- data/README.md +42 -3
- data/email_address.gemspec +1 -0
- data/lib/email_address.rb +17 -18
- data/lib/email_address/active_record_validator.rb +2 -2
- data/lib/email_address/address.rb +94 -100
- data/lib/email_address/canonical_email_address_type.rb +14 -12
- data/lib/email_address/config.rb +69 -47
- data/lib/email_address/email_address_type.rb +15 -13
- data/lib/email_address/exchanger.rb +29 -30
- data/lib/email_address/host.rb +123 -125
- data/lib/email_address/local.rb +7 -7
- data/lib/email_address/version.rb +1 -1
- data/test/email_address/test_address.rb +5 -0
- data/test/email_address/test_host.rb +29 -31
- data/test/test_aliasing.rb +54 -0
- metadata +8 -6
@@ -29,20 +29,22 @@
|
|
29
29
|
# user.canonical_email #=> "patsmith@gmail.com"
|
30
30
|
################################################################################
|
31
31
|
|
32
|
-
|
32
|
+
module EmailAddress
|
33
|
+
class CanonicalEmailAddressType < ActiveRecord::Type::Value
|
33
34
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
35
|
+
# From user input, setter
|
36
|
+
def cast(value)
|
37
|
+
super(Address.new(value).canonical)
|
38
|
+
end
|
38
39
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
40
|
+
# From a database value
|
41
|
+
def deserialize(value)
|
42
|
+
value && Address.new(value).normal
|
43
|
+
end
|
43
44
|
|
44
|
-
|
45
|
-
|
46
|
-
|
45
|
+
# To a database value (string)
|
46
|
+
def serialize(value)
|
47
|
+
value && Address.new(value).normal
|
48
|
+
end
|
47
49
|
end
|
48
50
|
end
|
data/lib/email_address/config.rb
CHANGED
@@ -97,67 +97,67 @@ module EmailAddress
|
|
97
97
|
# * exchanger_match: %w(google.com 127.0.0.1 10.9.8.0/24 ::1/64)
|
98
98
|
#
|
99
99
|
|
100
|
-
require
|
100
|
+
require "yaml"
|
101
101
|
|
102
102
|
class Config
|
103
103
|
@config = {
|
104
|
-
dns_lookup:
|
105
|
-
dns_timeout:
|
106
|
-
sha1_secret:
|
107
|
-
munge_string:
|
108
|
-
|
109
|
-
local_downcase:
|
110
|
-
local_fix:
|
111
|
-
local_encoding:
|
112
|
-
local_parse:
|
113
|
-
local_format:
|
114
|
-
local_size:
|
115
|
-
tag_separator:
|
116
|
-
mailbox_size:
|
117
|
-
mailbox_canonical:
|
118
|
-
mailbox_validator:
|
119
|
-
|
120
|
-
host_encoding:
|
121
|
-
host_validation:
|
122
|
-
host_size:
|
123
|
-
host_allow_ip:
|
104
|
+
dns_lookup: :mx, # :mx, :a, :off
|
105
|
+
dns_timeout: nil,
|
106
|
+
sha1_secret: "",
|
107
|
+
munge_string: "*****",
|
108
|
+
|
109
|
+
local_downcase: true,
|
110
|
+
local_fix: false,
|
111
|
+
local_encoding: :ascii, # :ascii, :unicode,
|
112
|
+
local_parse: nil, # nil, Proc
|
113
|
+
local_format: :conventional, # :conventional, :relaxed, :redacted, :standard, Proc
|
114
|
+
local_size: 1..64,
|
115
|
+
tag_separator: "+", # nil, character
|
116
|
+
mailbox_size: 1..64, # without tag
|
117
|
+
mailbox_canonical: nil, # nil, Proc
|
118
|
+
mailbox_validator: nil, # nil, Proc
|
119
|
+
|
120
|
+
host_encoding: :punycode || :unicode,
|
121
|
+
host_validation: :mx || :a || :connect || :syntax,
|
122
|
+
host_size: 1..253,
|
123
|
+
host_allow_ip: false,
|
124
124
|
host_remove_spaces: false,
|
125
|
-
host_local:
|
125
|
+
host_local: false,
|
126
126
|
|
127
127
|
address_validation: :parts, # :parts, :smtp, Proc
|
128
|
-
address_size:
|
129
|
-
address_fqdn_domain: nil
|
128
|
+
address_size: 3..254,
|
129
|
+
address_fqdn_domain: nil # Fully Qualified Domain Name = [host].[domain.tld]
|
130
130
|
}
|
131
131
|
|
132
132
|
# 2018-04: AOL and Yahoo now under "oath.com", owned by Verizon. Keeping separate for now
|
133
133
|
@providers = {
|
134
134
|
aol: {
|
135
|
-
host_match:
|
135
|
+
host_match: %w[aol. compuserve. netscape. aim. cs.]
|
136
136
|
},
|
137
137
|
google: {
|
138
|
-
host_match:
|
139
|
-
exchanger_match:
|
140
|
-
local_size:
|
138
|
+
host_match: %w[gmail.com googlemail.com],
|
139
|
+
exchanger_match: %w[google.com googlemail.com],
|
140
|
+
local_size: 5..64,
|
141
141
|
local_private_size: 1..64, # When hostname not in host_match (private label)
|
142
|
-
mailbox_canonical: ->(m) {m.
|
142
|
+
mailbox_canonical: ->(m) { m.delete(".") }
|
143
143
|
},
|
144
144
|
msn: {
|
145
|
-
host_match:
|
146
|
-
|
145
|
+
host_match: %w[msn. hotmail. outlook. live.],
|
146
|
+
exchanger_match: %w[outlook.com],
|
147
|
+
mailbox_validator: ->(m, t) { m =~ /\A\w[\-\w]*(?:\.[\-\w]+)*\z/i }
|
147
148
|
},
|
148
149
|
yahoo: {
|
149
|
-
host_match:
|
150
|
-
exchanger_match:
|
151
|
-
}
|
150
|
+
host_match: %w[yahoo. ymail. rocketmail.],
|
151
|
+
exchanger_match: %w[yahoodns yahoo-inc]
|
152
|
+
}
|
152
153
|
}
|
153
154
|
|
154
|
-
|
155
155
|
# Loads messages: {"en"=>{"email_address"=>{"invalid_address"=>"Invalid Email Address",...}}}
|
156
156
|
# Rails/I18n gem: t(email_address.error, scope: "email_address")
|
157
|
-
@errors = YAML.load_file(File.dirname(__FILE__)+"/messages.yaml")
|
157
|
+
@errors = YAML.load_file(File.dirname(__FILE__) + "/messages.yaml")
|
158
158
|
|
159
159
|
# Set multiple default configuration settings
|
160
|
-
def self.configure(config={})
|
160
|
+
def self.configure(config = {})
|
161
161
|
@config.merge!(config)
|
162
162
|
end
|
163
163
|
|
@@ -168,34 +168,56 @@ module EmailAddress
|
|
168
168
|
end
|
169
169
|
|
170
170
|
# Returns the hash of Provider rules
|
171
|
-
|
172
|
-
|
171
|
+
class << self
|
172
|
+
attr_reader :providers
|
173
173
|
end
|
174
174
|
|
175
175
|
# Configure or lookup a provider by name.
|
176
|
-
def self.provider(name, config={})
|
176
|
+
def self.provider(name, config = {})
|
177
177
|
name = name.to_sym
|
178
178
|
if config.size > 0
|
179
|
-
@providers[name]
|
180
|
-
@providers[name].merge!(config)
|
179
|
+
@providers[name.to_sym] = config
|
181
180
|
end
|
182
181
|
@providers[name]
|
183
182
|
end
|
184
183
|
|
185
|
-
def self.error_message(name, locale="en")
|
184
|
+
def self.error_message(name, locale = "en")
|
186
185
|
@errors[locale]["email_address"][name.to_s] || name.to_s
|
187
186
|
end
|
188
187
|
|
189
188
|
# Customize your own error message text.
|
190
|
-
def self.error_messages(hash=
|
191
|
-
|
192
|
-
|
189
|
+
def self.error_messages(hash = {}, locale = "en", *extra)
|
190
|
+
hash = extra.first if extra.first.is_a? Hash
|
191
|
+
unless hash.empty?
|
192
|
+
@errors[locale]["email_address"] = @errors[locale]["email_address"].merge(hash)
|
193
|
+
end
|
194
|
+
@errors[locale]["email_address"]
|
193
195
|
end
|
194
196
|
|
195
197
|
def self.all_settings(*configs)
|
196
198
|
config = @config.clone
|
197
|
-
configs.each {|c| config.merge!(c) }
|
199
|
+
configs.each { |c| config.merge!(c) }
|
198
200
|
config
|
199
201
|
end
|
202
|
+
|
203
|
+
def initialize(overrides = {})
|
204
|
+
@config = Config.all_settings(overrides)
|
205
|
+
end
|
206
|
+
|
207
|
+
def []=(setting, value)
|
208
|
+
@config[setting.to_sym] = value
|
209
|
+
end
|
210
|
+
|
211
|
+
def [](setting)
|
212
|
+
@config[setting.to_sym]
|
213
|
+
end
|
214
|
+
|
215
|
+
def configure(settings)
|
216
|
+
@config = @config.merge(settings)
|
217
|
+
end
|
218
|
+
|
219
|
+
def to_h
|
220
|
+
@config
|
221
|
+
end
|
200
222
|
end
|
201
223
|
end
|
@@ -29,20 +29,22 @@
|
|
29
29
|
# user.canonical_email #=> "patsmith@gmail.com"
|
30
30
|
################################################################################
|
31
31
|
|
32
|
-
|
32
|
+
module EmailAddress
|
33
|
+
class EmailAddressType < ActiveRecord::Type::Value
|
33
34
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
35
|
+
# From user input, setter
|
36
|
+
def cast(value)
|
37
|
+
super(Address.new(value).normal)
|
38
|
+
end
|
38
39
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
40
|
+
# From a database value
|
41
|
+
def deserialize(value)
|
42
|
+
value && Address.new(value).normal
|
43
|
+
end
|
44
|
+
|
45
|
+
# To a database value (string)
|
46
|
+
def serialize(value)
|
47
|
+
value && Address.new(value).normal
|
48
|
+
end
|
47
49
|
end
|
48
50
|
end
|
@@ -1,16 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
3
|
+
require "resolv"
|
4
|
+
require "netaddr"
|
5
|
+
require "socket"
|
6
6
|
|
7
7
|
module EmailAddress
|
8
8
|
class Exchanger
|
9
9
|
include Enumerable
|
10
10
|
|
11
|
-
def self.cached(host, config={})
|
11
|
+
def self.cached(host, config = {})
|
12
12
|
@host_cache ||= {}
|
13
|
-
@cache_size ||= ENV[
|
13
|
+
@cache_size ||= ENV["EMAIL_ADDRESS_CACHE_SIZE"].to_i || 100
|
14
14
|
if @host_cache.has_key?(host)
|
15
15
|
o = @host_cache.delete(host)
|
16
16
|
@host_cache[host] = o # LRU cache, move to end
|
@@ -22,22 +22,24 @@ module EmailAddress
|
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
25
|
-
def initialize(host, config={})
|
25
|
+
def initialize(host, config = {})
|
26
26
|
@host = host
|
27
|
-
@config = config
|
27
|
+
@config = config.is_a?(Hash) ? Config.new(config) : config
|
28
|
+
@dns_disabled = @config[:host_validation] == :syntax || @config[:dns_lookup] == :off
|
28
29
|
end
|
29
30
|
|
30
31
|
def each(&block)
|
32
|
+
return if @dns_disabled
|
31
33
|
mxers.each do |m|
|
32
|
-
yield({host:m[0], ip:m[1], priority:m[2]})
|
34
|
+
yield({host: m[0], ip: m[1], priority: m[2]})
|
33
35
|
end
|
34
36
|
end
|
35
37
|
|
36
38
|
# Returns the provider name based on the MX-er host names, or nil if not matched
|
37
39
|
def provider
|
38
40
|
return @provider if defined? @provider
|
39
|
-
|
40
|
-
if config[:exchanger_match] &&
|
41
|
+
Config.providers.each do |provider, config|
|
42
|
+
if config[:exchanger_match] && matches?(config[:exchanger_match])
|
41
43
|
return @provider = provider
|
42
44
|
end
|
43
45
|
end
|
@@ -50,40 +52,37 @@ module EmailAddress
|
|
50
52
|
# may not find provider by MX name or IP. I'm not sure about the "0.0.0.0" ip, it should
|
51
53
|
# be good in this context, but in "listen" context it means "all bound IP's"
|
52
54
|
def mxers
|
53
|
-
return [["example.com", "0.0.0.0", 1]] if @
|
54
|
-
@mxers ||= Resolv::DNS.open
|
55
|
+
return [["example.com", "0.0.0.0", 1]] if @dns_disabled
|
56
|
+
@mxers ||= Resolv::DNS.open { |dns|
|
55
57
|
dns.timeouts = @config[:dns_timeout] if @config[:dns_timeout]
|
56
58
|
|
57
59
|
ress = begin
|
58
60
|
dns.getresources(@host, Resolv::DNS::Resource::IN::MX)
|
59
|
-
|
60
|
-
|
61
|
+
rescue Resolv::ResolvTimeout
|
62
|
+
[]
|
61
63
|
end
|
62
64
|
|
63
|
-
records = ress.map
|
64
|
-
|
65
|
-
|
66
|
-
[r.exchange.to_s, IPSocket::getaddress(r.exchange.to_s), r.preference]
|
67
|
-
else
|
68
|
-
nil
|
69
|
-
end
|
70
|
-
rescue SocketError # not found, but could also mean network not work or it could mean one record doesn't resolve an address
|
71
|
-
nil
|
65
|
+
records = ress.map { |r|
|
66
|
+
if r.exchange.to_s > " "
|
67
|
+
[r.exchange.to_s, IPSocket.getaddress(r.exchange.to_s), r.preference]
|
72
68
|
end
|
73
|
-
|
69
|
+
}
|
74
70
|
records.compact
|
75
|
-
|
71
|
+
}
|
72
|
+
# not found, but could also mean network not work or it could mean one record doesn't resolve an address
|
73
|
+
rescue SocketError
|
74
|
+
[["example.com", "0.0.0.0", 1]]
|
76
75
|
end
|
77
76
|
|
78
77
|
# Returns Array of domain names for the MX'ers, used to determine the Provider
|
79
78
|
def domains
|
80
|
-
@_domains ||= mxers.map {|m|
|
79
|
+
@_domains ||= mxers.map { |m| Host.new(m.first).domain_name }.sort.uniq
|
81
80
|
end
|
82
81
|
|
83
82
|
# Returns an array of MX IP address (String) for the given email domain
|
84
83
|
def mx_ips
|
85
|
-
return ["0.0.0.0"] if @
|
86
|
-
mxers.map {|m| m[1] }
|
84
|
+
return ["0.0.0.0"] if @dns_disabled
|
85
|
+
mxers.map { |m| m[1] }
|
87
86
|
end
|
88
87
|
|
89
88
|
# Simple matcher, takes an array of CIDR addresses (ip/bits) and strings.
|
@@ -96,9 +95,9 @@ module EmailAddress
|
|
96
95
|
rules = Array(rules)
|
97
96
|
rules.each do |rule|
|
98
97
|
if rule.include?("/")
|
99
|
-
return rule if
|
98
|
+
return rule if in_cidr?(rule)
|
100
99
|
else
|
101
|
-
|
100
|
+
each { |mx| return rule if mx[:host].end_with?(rule) }
|
102
101
|
end
|
103
102
|
end
|
104
103
|
false
|
data/lib/email_address/host.rb
CHANGED
@@ -1,8 +1,7 @@
|
|
1
|
-
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require 'net/smtp'
|
1
|
+
require "simpleidn"
|
2
|
+
require "resolv"
|
3
|
+
require "netaddr"
|
4
|
+
require "net/smtp"
|
6
5
|
|
7
6
|
module EmailAddress
|
8
7
|
##############################################################################
|
@@ -34,12 +33,12 @@ module EmailAddress
|
|
34
33
|
class Host
|
35
34
|
attr_reader :host_name
|
36
35
|
attr_accessor :dns_name, :domain_name, :registration_name,
|
37
|
-
|
38
|
-
|
36
|
+
:tld, :tld2, :subdomains, :ip_address, :config, :provider,
|
37
|
+
:comment, :error_message, :reason
|
39
38
|
MAX_HOST_LENGTH = 255
|
40
39
|
|
41
40
|
# Sometimes, you just need a Regexp...
|
42
|
-
DNS_HOST_REGEX
|
41
|
+
DNS_HOST_REGEX = / [\p{L}\p{N}]+ (?: (?: \-{1,2} | \.) [\p{L}\p{N}]+ )*/x
|
43
42
|
|
44
43
|
# The IPv4 and IPv6 were lifted from Resolv::IPv?::Regex and tweaked to not
|
45
44
|
# \A...\z anchor at the edges.
|
@@ -85,46 +84,45 @@ module EmailAddress
|
|
85
84
|
|
86
85
|
# host name -
|
87
86
|
# * host type - :email for an email host, :mx for exchanger host
|
88
|
-
def initialize(host_name, config={})
|
89
|
-
@original
|
87
|
+
def initialize(host_name, config = {})
|
88
|
+
@original = host_name ||= ""
|
90
89
|
config[:host_type] ||= :email
|
91
|
-
@config
|
92
|
-
@error
|
90
|
+
@config = config.is_a?(Hash) ? Config.new(config) : config
|
91
|
+
@error = @error_message = nil
|
93
92
|
parse(host_name)
|
94
93
|
end
|
95
94
|
|
96
95
|
# Returns the String representation of the host name (or IP)
|
97
96
|
def name
|
98
|
-
if
|
99
|
-
"[#{
|
100
|
-
elsif
|
101
|
-
"[IPv6:#{
|
97
|
+
if ipv4?
|
98
|
+
"[#{ip_address}]"
|
99
|
+
elsif ipv6?
|
100
|
+
"[IPv6:#{ip_address}]"
|
102
101
|
elsif @config[:host_encoding] && @config[:host_encoding] == :unicode
|
103
|
-
::SimpleIDN.to_unicode(
|
102
|
+
::SimpleIDN.to_unicode(host_name)
|
104
103
|
else
|
105
|
-
|
104
|
+
dns_name
|
106
105
|
end
|
107
106
|
end
|
108
|
-
alias
|
107
|
+
alias to_s name
|
109
108
|
|
110
109
|
# The canonical host name is the simplified, DNS host name
|
111
110
|
def canonical
|
112
|
-
|
111
|
+
dns_name
|
113
112
|
end
|
114
113
|
|
115
114
|
# Returns the munged version of the name, replacing everything after the
|
116
115
|
# initial two characters with "*****" or the configured "munge_string".
|
117
116
|
def munge
|
118
|
-
|
117
|
+
host_name.sub(/\A(.{1,2}).*/) { |m| $1 + @config[:munge_string] }
|
119
118
|
end
|
120
119
|
|
121
120
|
############################################################################
|
122
121
|
# Parsing
|
123
122
|
############################################################################
|
124
123
|
|
125
|
-
|
126
124
|
def parse(host) # :nodoc:
|
127
|
-
host =
|
125
|
+
host = parse_comment(host)
|
128
126
|
|
129
127
|
if host =~ /\A\[IPv6:(.+)\]/i
|
130
128
|
self.ip_address = $1
|
@@ -149,34 +147,34 @@ module EmailAddress
|
|
149
147
|
name = fully_qualified_domain_name(name.downcase)
|
150
148
|
@host_name = name
|
151
149
|
if @config[:host_remove_spaces]
|
152
|
-
@host_name = @host_name.
|
150
|
+
@host_name = @host_name.delete(" ")
|
153
151
|
end
|
154
|
-
|
155
|
-
|
152
|
+
@dns_name = if /[^[:ascii:]]/.match(host_name)
|
153
|
+
::SimpleIDN.to_ascii(host_name)
|
156
154
|
else
|
157
|
-
|
155
|
+
host_name
|
158
156
|
end
|
159
157
|
|
160
158
|
# Subdomain only (root@localhost)
|
161
|
-
if name.index(
|
159
|
+
if name.index(".").nil?
|
162
160
|
self.subdomains = name
|
163
161
|
|
164
162
|
# Split sub.domain from .tld: *.com, *.xx.cc, *.cc
|
165
163
|
elsif name =~ /\A(.+)\.(\w{3,10})\z/ ||
|
166
|
-
|
167
|
-
|
164
|
+
name =~ /\A(.+)\.(\w{1,3}\.\w\w)\z/ ||
|
165
|
+
name =~ /\A(.+)\.(\w\w)\z/
|
168
166
|
|
169
167
|
sub_and_domain, self.tld2 = [$1, $2] # sub+domain, com || co.uk
|
170
|
-
self.tld =
|
168
|
+
self.tld = tld2.sub(/\A.+\./, "") # co.uk => uk
|
171
169
|
if sub_and_domain =~ /\A(.+)\.(.+)\z/ # is subdomain? sub.example [.tld2]
|
172
|
-
self.subdomains
|
170
|
+
self.subdomains = $1
|
173
171
|
self.registration_name = $2
|
174
172
|
else
|
175
173
|
self.registration_name = sub_and_domain
|
176
|
-
#self.domain_name = sub_and_domain + '.' + self.tld2
|
174
|
+
# self.domain_name = sub_and_domain + '.' + self.tld2
|
177
175
|
end
|
178
|
-
self.domain_name =
|
179
|
-
|
176
|
+
self.domain_name = registration_name + "." + tld2
|
177
|
+
find_provider
|
180
178
|
else # Bad format
|
181
179
|
self.subdomains = self.tld = self.tld2 = ""
|
182
180
|
self.domain_name = self.registration_name = name
|
@@ -187,7 +185,7 @@ module EmailAddress
|
|
187
185
|
dn = @config[:address_fqdn_domain]
|
188
186
|
if !dn
|
189
187
|
if (host_part.nil? || host_part <= " ") && @config[:host_local]
|
190
|
-
|
188
|
+
"localhost"
|
191
189
|
else
|
192
190
|
host_part
|
193
191
|
end
|
@@ -205,39 +203,37 @@ module EmailAddress
|
|
205
203
|
return false unless registration_name
|
206
204
|
find_provider
|
207
205
|
return false unless config[:host_match]
|
208
|
-
!
|
206
|
+
!matches?(config[:host_match])
|
209
207
|
end
|
210
208
|
|
211
209
|
def find_provider # :nodoc:
|
212
|
-
return
|
210
|
+
return provider if provider
|
213
211
|
|
214
|
-
|
215
|
-
if config[:host_match] &&
|
216
|
-
return
|
212
|
+
Config.providers.each do |provider, config|
|
213
|
+
if config[:host_match] && matches?(config[:host_match])
|
214
|
+
return set_provider(provider, config)
|
217
215
|
end
|
218
216
|
end
|
219
217
|
|
220
|
-
return
|
218
|
+
return set_provider(:default) unless dns_enabled?
|
221
219
|
|
222
|
-
provider
|
223
|
-
if provider != :default
|
224
|
-
self.set_provider(provider,
|
225
|
-
EmailAddress::Config.provider(provider))
|
226
|
-
end
|
227
|
-
|
228
|
-
self.provider ||= self.set_provider(:default)
|
220
|
+
self.provider ||= set_provider(:default)
|
229
221
|
end
|
230
222
|
|
231
|
-
def set_provider(name, provider_config={}) # :nodoc:
|
232
|
-
|
233
|
-
|
223
|
+
def set_provider(name, provider_config = {}) # :nodoc:
|
224
|
+
config.configure(provider_config)
|
225
|
+
@provider = name
|
234
226
|
end
|
235
227
|
|
236
228
|
# Returns a hash of the parts of the host name after parsing.
|
237
229
|
def parts
|
238
|
-
{
|
239
|
-
|
240
|
-
|
230
|
+
{host_name: host_name, dns_name: dns_name, subdomain: subdomains,
|
231
|
+
registration_name: registration_name, domain_name: domain_name,
|
232
|
+
tld2: tld2, tld: tld, ip_address: ip_address,}
|
233
|
+
end
|
234
|
+
|
235
|
+
def hosted_provider
|
236
|
+
Exchanger.cached(dns_name).provider
|
241
237
|
end
|
242
238
|
|
243
239
|
############################################################################
|
@@ -246,19 +242,19 @@ module EmailAddress
|
|
246
242
|
|
247
243
|
# Is this a fully-qualified domain name?
|
248
244
|
def fqdn?
|
249
|
-
|
245
|
+
tld ? true : false
|
250
246
|
end
|
251
247
|
|
252
248
|
def ip?
|
253
|
-
|
249
|
+
ip_address.nil? ? false : true
|
254
250
|
end
|
255
251
|
|
256
252
|
def ipv4?
|
257
|
-
|
253
|
+
ip? && ip_address.include?(".")
|
258
254
|
end
|
259
255
|
|
260
256
|
def ipv6?
|
261
|
-
|
257
|
+
ip? && ip_address.include?(":")
|
262
258
|
end
|
263
259
|
|
264
260
|
############################################################################
|
@@ -276,25 +272,25 @@ module EmailAddress
|
|
276
272
|
rules = Array(rules)
|
277
273
|
return false if rules.empty?
|
278
274
|
rules.each do |rule|
|
279
|
-
return rule if rule ==
|
275
|
+
return rule if rule == domain_name || rule == dns_name
|
280
276
|
return rule if registration_name_matches?(rule)
|
281
277
|
return rule if tld_matches?(rule)
|
282
278
|
return rule if domain_matches?(rule)
|
283
279
|
return rule if self.provider && provider_matches?(rule)
|
284
|
-
return rule if
|
280
|
+
return rule if ip_matches?(rule)
|
285
281
|
end
|
286
282
|
false
|
287
283
|
end
|
288
284
|
|
289
285
|
# Does "example." match any tld?
|
290
286
|
def registration_name_matches?(rule)
|
291
|
-
"#{
|
287
|
+
rule == "#{registration_name}."
|
292
288
|
end
|
293
289
|
|
294
290
|
# Does "sub.example.com" match ".com" and ".example.com" top level names?
|
295
291
|
# Matches TLD (uk) or TLD2 (co.uk)
|
296
292
|
def tld_matches?(rule)
|
297
|
-
rule.match(/\A\.(.+)\z/) && ($1 ==
|
293
|
+
rule.match(/\A\.(.+)\z/) && ($1 == tld || $1 == tld2) ? true : false
|
298
294
|
end
|
299
295
|
|
300
296
|
def provider_matches?(rule)
|
@@ -305,20 +301,20 @@ module EmailAddress
|
|
305
301
|
# Requires optionally starts with a "@".
|
306
302
|
def domain_matches?(rule)
|
307
303
|
rule = $1 if rule =~ /\A@(.+)/
|
308
|
-
return rule if File.fnmatch?(rule,
|
309
|
-
return rule if File.fnmatch?(rule,
|
304
|
+
return rule if domain_name && File.fnmatch?(rule, domain_name)
|
305
|
+
return rule if dns_name && File.fnmatch?(rule, dns_name)
|
310
306
|
false
|
311
307
|
end
|
312
308
|
|
313
309
|
# True if the host is an IP Address form, and that address matches
|
314
310
|
# the passed CIDR string ("10.9.8.0/24" or "2001:..../64")
|
315
311
|
def ip_matches?(cidr)
|
316
|
-
return false unless
|
317
|
-
return cidr if !cidr.include?("/") && cidr ==
|
318
|
-
if cidr.include?(":") &&
|
319
|
-
return cidr if NetAddr::IPv6Net.parse(cidr).contains(NetAddr::IPv6.parse(
|
320
|
-
elsif cidr.include?(".") &&
|
321
|
-
return cidr if NetAddr::IPv4Net.parse(cidr).contains(NetAddr::IPv4.parse(
|
312
|
+
return false unless ip_address
|
313
|
+
return cidr if !cidr.include?("/") && cidr == ip_address
|
314
|
+
if cidr.include?(":") && ip_address.include?(":")
|
315
|
+
return cidr if NetAddr::IPv6Net.parse(cidr).contains(NetAddr::IPv6.parse(ip_address))
|
316
|
+
elsif cidr.include?(".") && ip_address.include?(".")
|
317
|
+
return cidr if NetAddr::IPv4Net.parse(cidr).contains(NetAddr::IPv4.parse(ip_address))
|
322
318
|
end
|
323
319
|
false
|
324
320
|
end
|
@@ -329,33 +325,36 @@ module EmailAddress
|
|
329
325
|
|
330
326
|
# True if the :dns_lookup setting is enabled
|
331
327
|
def dns_enabled?
|
332
|
-
[:
|
328
|
+
return false if @config[:dns_lookup] == :off
|
329
|
+
return false if @config[:host_validation] == :syntax
|
330
|
+
true
|
333
331
|
end
|
334
332
|
|
335
333
|
# Returns: [official_hostname, alias_hostnames, address_family, *address_list]
|
336
334
|
def dns_a_record
|
337
335
|
@_dns_a_record = "0.0.0.0" if @config[:dns_lookup] == :off
|
338
|
-
@_dns_a_record ||= Socket.gethostbyname(
|
336
|
+
@_dns_a_record ||= Socket.gethostbyname(dns_name)
|
339
337
|
rescue SocketError # not found, but could also mean network not work
|
340
338
|
@_dns_a_record ||= []
|
341
339
|
end
|
342
340
|
|
343
|
-
# Returns an array of
|
341
|
+
# Returns an array of Exchanger hosts configured in DNS.
|
344
342
|
# The array will be empty if none are configured.
|
345
343
|
def exchangers
|
346
|
-
#return nil if @config[:host_type] != :email || !self.dns_enabled?
|
347
|
-
@_exchangers ||=
|
344
|
+
# return nil if @config[:host_type] != :email || !self.dns_enabled?
|
345
|
+
@_exchangers ||= Exchanger.cached(dns_name, @config)
|
348
346
|
end
|
349
347
|
|
350
348
|
# Returns a DNS TXT Record
|
351
|
-
def txt(alternate_host=nil)
|
349
|
+
def txt(alternate_host = nil)
|
350
|
+
return nil unless dns_enabled?
|
352
351
|
Resolv::DNS.open do |dns|
|
353
352
|
dns.timeouts = @config[:dns_timeout] if @config[:dns_timeout]
|
354
353
|
records = begin
|
355
|
-
dns.getresources(alternate_host ||
|
356
|
-
|
357
|
-
|
358
|
-
|
354
|
+
dns.getresources(alternate_host || dns_name,
|
355
|
+
Resolv::DNS::Resource::IN::TXT)
|
356
|
+
rescue Resolv::ResolvTimeout
|
357
|
+
[]
|
359
358
|
end
|
360
359
|
|
361
360
|
records.empty? ? nil : records.map(&:data).join(" ")
|
@@ -363,13 +362,13 @@ module EmailAddress
|
|
363
362
|
end
|
364
363
|
|
365
364
|
# Parses TXT record pairs into a hash
|
366
|
-
def txt_hash(alternate_host=nil)
|
365
|
+
def txt_hash(alternate_host = nil)
|
367
366
|
fields = {}
|
368
|
-
record =
|
367
|
+
record = txt(alternate_host)
|
369
368
|
return fields unless record
|
370
369
|
|
371
370
|
record.split(/\s*;\s*/).each do |pair|
|
372
|
-
(n,v) = pair.split(/\s*=\s*/)
|
371
|
+
(n, v) = pair.split(/\s*=\s*/)
|
373
372
|
fields[n.to_sym] = v
|
374
373
|
end
|
375
374
|
fields
|
@@ -378,7 +377,7 @@ module EmailAddress
|
|
378
377
|
# Returns a hash of the domain's DMARC (https://en.wikipedia.org/wiki/DMARC)
|
379
378
|
# settings.
|
380
379
|
def dmarc
|
381
|
-
|
380
|
+
dns_name ? txt_hash("_dmarc." + dns_name) : {}
|
382
381
|
end
|
383
382
|
|
384
383
|
############################################################################
|
@@ -386,13 +385,13 @@ module EmailAddress
|
|
386
385
|
############################################################################
|
387
386
|
|
388
387
|
# Returns true if the host name is valid according to the current configuration
|
389
|
-
def valid?(rules={})
|
388
|
+
def valid?(rules = {})
|
390
389
|
host_validation = rules[:host_validation] || @config[:host_validation] || :mx
|
391
|
-
dns_lookup
|
390
|
+
dns_lookup = rules[:dns_lookup] || host_validation
|
392
391
|
self.error_message = nil
|
393
|
-
if
|
392
|
+
if ip_address
|
394
393
|
valid_ip?
|
395
|
-
elsif !
|
394
|
+
elsif !valid_format?
|
396
395
|
false
|
397
396
|
elsif dns_lookup == :connect
|
398
397
|
valid_mx? && connect
|
@@ -407,8 +406,9 @@ module EmailAddress
|
|
407
406
|
|
408
407
|
# True if the host name has a DNS A Record
|
409
408
|
def valid_dns?
|
409
|
+
return true unless dns_enabled?
|
410
410
|
bool = dns_a_record.size > 0 || set_error(:domain_unknown)
|
411
|
-
if
|
411
|
+
if localhost? && !@config[:host_local]
|
412
412
|
bool = set_error(:domain_no_localhost)
|
413
413
|
end
|
414
414
|
bool
|
@@ -416,10 +416,11 @@ module EmailAddress
|
|
416
416
|
|
417
417
|
# True if the host name has valid MX servers configured in DNS
|
418
418
|
def valid_mx?
|
419
|
-
|
419
|
+
return true unless dns_enabled?
|
420
|
+
if exchangers.nil?
|
420
421
|
set_error(:domain_unknown)
|
421
|
-
elsif
|
422
|
-
if
|
422
|
+
elsif exchangers.mx_ips.size > 0
|
423
|
+
if localhost? && !@config[:host_local]
|
423
424
|
set_error(:domain_no_localhost)
|
424
425
|
else
|
425
426
|
true
|
@@ -433,9 +434,9 @@ module EmailAddress
|
|
433
434
|
|
434
435
|
# True if the host_name passes Regular Expression match and size limits.
|
435
436
|
def valid_format?
|
436
|
-
if
|
437
|
+
if host_name =~ CANONICAL_HOST_REGEX && to_s.size <= MAX_HOST_LENGTH
|
437
438
|
return true if localhost?
|
438
|
-
return true if
|
439
|
+
return true if host_name.include?(".") # require FQDN
|
439
440
|
end
|
440
441
|
set_error(:domain_invalid)
|
441
442
|
end
|
@@ -444,12 +445,12 @@ module EmailAddress
|
|
444
445
|
# is a potentially valid IP address. It does not check if the address
|
445
446
|
# is reachable.
|
446
447
|
def valid_ip?
|
447
|
-
if
|
448
|
+
if !@config[:host_allow_ip]
|
448
449
|
bool = set_error(:ip_address_forbidden)
|
449
|
-
elsif
|
450
|
-
bool =
|
451
|
-
elsif
|
452
|
-
bool =
|
450
|
+
elsif ip_address.include?(":")
|
451
|
+
bool = ip_address.match(Resolv::IPv6::Regex) ? true : set_error(:ipv6_address_invalid)
|
452
|
+
elsif ip_address.include?(".")
|
453
|
+
bool = ip_address.match(Resolv::IPv4::Regex) ? true : set_error(:ipv4_address_invalid)
|
453
454
|
end
|
454
455
|
if bool && (localhost? && !@config[:host_local])
|
455
456
|
bool = set_error(:ip_address_no_localhost)
|
@@ -458,20 +459,20 @@ module EmailAddress
|
|
458
459
|
end
|
459
460
|
|
460
461
|
def localhost?
|
461
|
-
if
|
462
|
+
if ip_address
|
462
463
|
rel =
|
463
|
-
if
|
464
|
-
NetAddr::IPv6Net.parse(""+"::1").rel(
|
465
|
-
NetAddr::IPv6Net.parse(
|
464
|
+
if ip_address.include?(":")
|
465
|
+
NetAddr::IPv6Net.parse("" + "::1").rel(
|
466
|
+
NetAddr::IPv6Net.parse(ip_address)
|
466
467
|
)
|
467
468
|
else
|
468
|
-
NetAddr::IPv4Net.parse(""+"127.0.0.0/8").rel(
|
469
|
-
NetAddr::IPv4Net.parse(
|
469
|
+
NetAddr::IPv4Net.parse("" + "127.0.0.0/8").rel(
|
470
|
+
NetAddr::IPv4Net.parse(ip_address)
|
470
471
|
)
|
471
472
|
end
|
472
473
|
!rel.nil? && rel >= 0
|
473
474
|
else
|
474
|
-
|
475
|
+
host_name == "localhost"
|
475
476
|
end
|
476
477
|
end
|
477
478
|
|
@@ -479,33 +480,30 @@ module EmailAddress
|
|
479
480
|
# as an email address check, but is provided to assist in problem resolution.
|
480
481
|
# If you abuse this, you *could* be blocked by the ESP.
|
481
482
|
def connect
|
482
|
-
|
483
|
-
|
484
|
-
|
483
|
+
smtp = Net::SMTP.new(host_name || ip_address)
|
484
|
+
smtp.start(@config[:helo_name] || "localhost")
|
485
|
+
smtp.finish
|
486
|
+
true
|
487
|
+
rescue Net::SMTPFatalError => e
|
488
|
+
set_error(:server_not_available, e.to_s)
|
489
|
+
rescue SocketError => e
|
490
|
+
set_error(:server_not_available, e.to_s)
|
491
|
+
ensure
|
492
|
+
if smtp&.started?
|
485
493
|
smtp.finish
|
486
|
-
true
|
487
|
-
rescue Net::SMTPFatalError => e
|
488
|
-
set_error(:server_not_available, e.to_s)
|
489
|
-
rescue SocketError => e
|
490
|
-
set_error(:server_not_available, e.to_s)
|
491
|
-
ensure
|
492
|
-
if smtp && smtp.started?
|
493
|
-
smtp.finish
|
494
|
-
end
|
495
494
|
end
|
496
495
|
end
|
497
496
|
|
498
|
-
def set_error(err, reason=nil)
|
499
|
-
@error
|
500
|
-
@reason
|
501
|
-
@error_message =
|
497
|
+
def set_error(err, reason = nil)
|
498
|
+
@error = err
|
499
|
+
@reason = reason
|
500
|
+
@error_message = Config.error_message(err)
|
502
501
|
false
|
503
502
|
end
|
504
503
|
|
505
504
|
# The inverse of valid? -- Returns nil (falsey) if valid, otherwise error message
|
506
505
|
def error
|
507
|
-
|
506
|
+
valid? ? nil : @error_message
|
508
507
|
end
|
509
|
-
|
510
508
|
end
|
511
509
|
end
|