email_address 0.1.13 → 0.1.18
Sign up to get free protection for your applications and to get access to all the features.
- 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
|