email_address 0.1.2 → 0.2.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.
@@ -1,6 +1,6 @@
1
- require 'simpleidn'
2
- require 'resolv'
3
- require 'netaddr'
1
+ require "simpleidn"
2
+ require "resolv"
3
+ require "net/smtp"
4
4
 
5
5
  module EmailAddress
6
6
  ##############################################################################
@@ -32,16 +32,16 @@ module EmailAddress
32
32
  class Host
33
33
  attr_reader :host_name
34
34
  attr_accessor :dns_name, :domain_name, :registration_name,
35
- :tld, :tld2, :subdomains, :ip_address, :config, :provider,
36
- :comment
35
+ :tld, :tld2, :subdomains, :ip_address, :config, :provider,
36
+ :comment, :error_message, :reason, :locale
37
37
  MAX_HOST_LENGTH = 255
38
38
 
39
39
  # Sometimes, you just need a Regexp...
40
- DNS_HOST_REGEX = / [\p{L}\p{N}]+ (?: (?: \-{1,2} | \.) [\p{L}\p{N}]+ )*/x
40
+ DNS_HOST_REGEX = / [\p{L}\p{N}]+ (?: (?: -{1,2} | \.) [\p{L}\p{N}]+ )*/x
41
41
 
42
42
  # The IPv4 and IPv6 were lifted from Resolv::IPv?::Regex and tweaked to not
43
43
  # \A...\z anchor at the edges.
44
- IPv6_HOST_REGEX = /\[IPv6:
44
+ IPV6_HOST_REGEX = /\[IPv6:
45
45
  (?: (?:(?x-mi:
46
46
  (?:[0-9A-Fa-f]{1,4}:){7}
47
47
  [0-9A-Fa-f]{1,4}
@@ -60,7 +60,7 @@ module EmailAddress
60
60
  (?: \d+)\.(?: \d+)\.(?: \d+)\.(?: \d+)
61
61
  )))\]/ix
62
62
 
63
- IPv4_HOST_REGEX = /\[((?x-mi:0
63
+ IPV4_HOST_REGEX = /\[((?x-mi:0
64
64
  |1(?:[0-9][0-9]?)?
65
65
  |2(?:[0-4][0-9]?|5[0-5]?|[6-9])?
66
66
  |[3-9][0-9]?))\.((?x-mi:0
@@ -79,49 +79,50 @@ module EmailAddress
79
79
 
80
80
  # Matches Host forms: DNS name, IPv4, or IPv6 formats
81
81
  STANDARD_HOST_REGEX = /\A (?: #{DNS_HOST_REGEX}
82
- | #{IPv4_HOST_REGEX} | #{IPv6_HOST_REGEX}) \z/ix
82
+ | #{IPV4_HOST_REGEX} | #{IPV6_HOST_REGEX}) \z/ix
83
83
 
84
84
  # host name -
85
85
  # * host type - :email for an email host, :mx for exchanger host
86
- def initialize(host_name, config={})
87
- @original = host_name ||= ''
86
+ def initialize(host_name, config = {}, locale = "en")
87
+ @original = host_name ||= ""
88
+ @locale = locale
88
89
  config[:host_type] ||= :email
89
- @config = config
90
+ @config = config.is_a?(Hash) ? Config.new(config) : config
91
+ @error = @error_message = nil
90
92
  parse(host_name)
91
93
  end
92
94
 
93
95
  # Returns the String representation of the host name (or IP)
94
96
  def name
95
- if self.ipv4?
96
- "[#{self.ip_address}]"
97
- elsif self.ipv6?
98
- "[IPv6:#{self.ip_address}]"
97
+ if ipv4?
98
+ "[#{ip_address}]"
99
+ elsif ipv6?
100
+ "[IPv6:#{ip_address}]"
99
101
  elsif @config[:host_encoding] && @config[:host_encoding] == :unicode
100
- ::SimpleIDN.to_unicode(self.host_name)
102
+ ::SimpleIDN.to_unicode(host_name)
101
103
  else
102
- self.dns_name
104
+ dns_name
103
105
  end
104
106
  end
105
- alias :to_s :name
107
+ alias_method :to_s, :name
106
108
 
107
109
  # The canonical host name is the simplified, DNS host name
108
110
  def canonical
109
- self.dns_name
111
+ dns_name
110
112
  end
111
113
 
112
114
  # Returns the munged version of the name, replacing everything after the
113
115
  # initial two characters with "*****" or the configured "munge_string".
114
116
  def munge
115
- self.host_name.sub(/\A(.{1,2}).*/) { |m| $1 + @config[:munge_string] }
117
+ host_name.sub(/\A(.{1,2}).*/) { |m| $1 + @config[:munge_string] }
116
118
  end
117
119
 
118
120
  ############################################################################
119
121
  # Parsing
120
122
  ############################################################################
121
123
 
122
-
123
124
  def parse(host) # :nodoc:
124
- host = self.parse_comment(host)
125
+ host = parse_comment(host)
125
126
 
126
127
  if host =~ /\A\[IPv6:(.+)\]/i
127
128
  self.ip_address = $1
@@ -143,62 +144,96 @@ module EmailAddress
143
144
  end
144
145
 
145
146
  def host_name=(name)
146
- @host_name = name = name.strip.downcase.gsub(' ', '').gsub(/\(.*\)/, '')
147
- @dns_name = ::SimpleIDN.to_ascii(self.host_name)
147
+ name = fully_qualified_domain_name(name.downcase)
148
+ @host_name = name
149
+ if @config[:host_remove_spaces]
150
+ @host_name = @host_name.delete(" ")
151
+ end
152
+ @dns_name = if /[^[:ascii:]]/.match?(host_name)
153
+ ::SimpleIDN.to_ascii(host_name)
154
+ else
155
+ host_name
156
+ end
148
157
 
149
158
  # Subdomain only (root@localhost)
150
- if name.index('.').nil?
159
+ if name.index(".").nil?
151
160
  self.subdomains = name
152
161
 
153
162
  # Split sub.domain from .tld: *.com, *.xx.cc, *.cc
154
163
  elsif name =~ /\A(.+)\.(\w{3,10})\z/ ||
155
- name =~ /\A(.+)\.(\w{1,3}\.\w\w)\z/ ||
156
- name =~ /\A(.+)\.(\w\w)\z/
164
+ name =~ /\A(.+)\.(\w{1,3}\.\w\w)\z/ ||
165
+ name =~ /\A(.+)\.(\w\w)\z/
157
166
 
158
167
  sub_and_domain, self.tld2 = [$1, $2] # sub+domain, com || co.uk
159
- self.tld = self.tld2.sub(/\A.+\./, '') # co.uk => uk
168
+ self.tld = tld2.sub(/\A.+\./, "") # co.uk => uk
160
169
  if sub_and_domain =~ /\A(.+)\.(.+)\z/ # is subdomain? sub.example [.tld2]
161
- self.subdomains = $1
170
+ self.subdomains = $1
162
171
  self.registration_name = $2
163
172
  else
164
173
  self.registration_name = sub_and_domain
165
- #self.domain_name = sub_and_domain + '.' + self.tld2
174
+ # self.domain_name = sub_and_domain + '.' + self.tld2
166
175
  end
167
- self.domain_name = self.registration_name + '.' + self.tld2
168
- self.find_provider
176
+ self.domain_name = registration_name + "." + tld2
177
+ find_provider
178
+ else # Bad format
179
+ self.subdomains = self.tld = self.tld2 = ""
180
+ self.domain_name = self.registration_name = name
169
181
  end
170
182
  end
171
183
 
172
- def find_provider # :nodoc:
173
- return self.provider if self.provider
174
-
175
- EmailAddress::Config.providers.each do |provider, config|
176
- if config[:host_match] && self.matches?(config[:host_match])
177
- return self.set_provider(provider, config)
184
+ def fully_qualified_domain_name(host_part)
185
+ dn = @config[:address_fqdn_domain]
186
+ if !dn
187
+ if (host_part.nil? || host_part <= " ") && @config[:host_local]
188
+ "localhost"
189
+ else
190
+ host_part
178
191
  end
192
+ elsif host_part.nil? || host_part <= " "
193
+ dn
194
+ elsif !host_part.include?(".")
195
+ host_part + "." + dn
196
+ else
197
+ host_part
179
198
  end
199
+ end
200
+
201
+ # True if host is hosted at the provider, not a public provider host name
202
+ def hosted_service?
203
+ return false unless registration_name
204
+ find_provider
205
+ return false unless config[:host_match]
206
+ !matches?(config[:host_match])
207
+ end
180
208
 
181
- return self.set_provider(:default) unless self.dns_enabled?
209
+ def find_provider # :nodoc:
210
+ return provider if provider
182
211
 
183
- provider = self.exchangers.provider
184
- if provider != :default
185
- self.set_provider(provider,
186
- EmailAddress::Config.provider(provider))
212
+ Config.providers.each do |provider, config|
213
+ if config[:host_match] && matches?(config[:host_match])
214
+ return set_provider(provider, config)
215
+ end
187
216
  end
188
217
 
189
- self.provider ||= self.set_provider(:default)
218
+ return set_provider(:default) unless dns_enabled?
219
+
220
+ self.provider ||= set_provider(:default)
190
221
  end
191
222
 
192
- def set_provider(name, provider_config={}) # :nodoc:
193
- self.config = EmailAddress::Config.all_settings(provider_config, @config)
194
- self.provider = name
223
+ def set_provider(name, provider_config = {}) # :nodoc:
224
+ config.configure(provider_config)
225
+ @provider = name
195
226
  end
196
227
 
197
228
  # Returns a hash of the parts of the host name after parsing.
198
229
  def parts
199
- { host_name:self.host_name, dns_name:self.dns_name, subdomain:self.subdomains,
200
- registration_name:self.registration_name, domain_name:self.domain_name,
201
- tld2:self.tld2, tld:self.tld, ip_address:self.ip_address }
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
202
237
  end
203
238
 
204
239
  ############################################################################
@@ -207,19 +242,19 @@ module EmailAddress
207
242
 
208
243
  # Is this a fully-qualified domain name?
209
244
  def fqdn?
210
- self.tld ? true : false
245
+ tld ? true : false
211
246
  end
212
247
 
213
248
  def ip?
214
- self.ip_address.nil? ? false : true
249
+ !!ip_address
215
250
  end
216
251
 
217
252
  def ipv4?
218
- self.ip? && self.ip_address.include?(".")
253
+ ip? && ip_address.include?(".")
219
254
  end
220
255
 
221
256
  def ipv6?
222
- self.ip? && self.ip_address.include?(":")
257
+ ip? && ip_address.include?(":")
223
258
  end
224
259
 
225
260
  ############################################################################
@@ -237,25 +272,25 @@ module EmailAddress
237
272
  rules = Array(rules)
238
273
  return false if rules.empty?
239
274
  rules.each do |rule|
240
- return rule if rule == self.domain_name || rule == self.dns_name
275
+ return rule if rule == domain_name || rule == dns_name
241
276
  return rule if registration_name_matches?(rule)
242
277
  return rule if tld_matches?(rule)
243
278
  return rule if domain_matches?(rule)
244
279
  return rule if self.provider && provider_matches?(rule)
245
- return rule if self.ip_matches?(rule)
280
+ return rule if ip_matches?(rule)
246
281
  end
247
282
  false
248
283
  end
249
284
 
250
285
  # Does "example." match any tld?
251
286
  def registration_name_matches?(rule)
252
- self.registration_name + '.' == rule ? true : false
287
+ rule == "#{registration_name}."
253
288
  end
254
289
 
255
290
  # Does "sub.example.com" match ".com" and ".example.com" top level names?
256
291
  # Matches TLD (uk) or TLD2 (co.uk)
257
292
  def tld_matches?(rule)
258
- rule.match(/\A\.(.+)\z/) && ($1 == self.tld || $1 == self.tld2) ? true : false
293
+ rule.match(/\A\.(.+)\z/) && ($1 == tld || $1 == tld2) # ? true : false
259
294
  end
260
295
 
261
296
  def provider_matches?(rule)
@@ -266,24 +301,17 @@ module EmailAddress
266
301
  # Requires optionally starts with a "@".
267
302
  def domain_matches?(rule)
268
303
  rule = $1 if rule =~ /\A@(.+)/
269
- return rule if File.fnmatch?(rule, self.domain_name)
270
- return rule if File.fnmatch?(rule, self.dns_name)
304
+ return rule if domain_name && File.fnmatch?(rule, domain_name)
305
+ return rule if dns_name && File.fnmatch?(rule, dns_name)
271
306
  false
272
307
  end
273
308
 
274
309
  # True if the host is an IP Address form, and that address matches
275
310
  # the passed CIDR string ("10.9.8.0/24" or "2001:..../64")
276
311
  def ip_matches?(cidr)
277
- return false unless self.ip_address
278
- return cidr if !cidr.include?("/") && cidr == self.ip_address
279
-
280
- c = NetAddr::CIDR.create(cidr)
281
- if cidr.include?(":") && self.ip_address.include?(":")
282
- return cidr if c.matches?(self.ip_address)
283
- elsif cidr.include?(".") && self.ip_address.include?(".")
284
- return cidr if c.matches?(self.ip_address)
285
- end
286
- false
312
+ return false unless ip_address
313
+ net = IPAddr.new(cidr)
314
+ net.include?(IPAddr.new(ip_address))
287
315
  end
288
316
 
289
317
  ############################################################################
@@ -292,45 +320,50 @@ module EmailAddress
292
320
 
293
321
  # True if the :dns_lookup setting is enabled
294
322
  def dns_enabled?
295
- EmailAddress::Config.setting(:dns_lookup).equal?(:off) ? false : true
296
- end
297
-
298
- # True if the host name has a DNS A Record
299
- def has_dns_a_record?
300
- dns_a_record.size > 0 ? true : false
323
+ return false if @config[:dns_lookup] == :off
324
+ return false if @config[:host_validation] == :syntax
325
+ true
301
326
  end
302
327
 
303
328
  # Returns: [official_hostname, alias_hostnames, address_family, *address_list]
304
329
  def dns_a_record
305
- @_dns_a_record ||= Socket.gethostbyname(self.dns_name)
330
+ @_dns_a_record = "0.0.0.0" if @config[:dns_lookup] == :off
331
+ @_dns_a_record ||= Addrinfo.getaddrinfo(dns_name, 80) # Port 80 for A rec, 25 for MX
306
332
  rescue SocketError # not found, but could also mean network not work
307
333
  @_dns_a_record ||= []
308
334
  end
309
335
 
310
- # Returns an array of EmailAddress::Exchanger hosts configured in DNS.
336
+ # Returns an array of Exchanger hosts configured in DNS.
311
337
  # The array will be empty if none are configured.
312
338
  def exchangers
313
- return nil if @config[:host_type] != :email || !self.dns_enabled?
314
- @_exchangers ||= EmailAddress::Exchanger.cached(self.dns_name)
339
+ # return nil if @config[:host_type] != :email || !self.dns_enabled?
340
+ @_exchangers ||= Exchanger.cached(dns_name, @config)
315
341
  end
316
342
 
317
343
  # Returns a DNS TXT Record
318
- def txt(alternate_host=nil)
344
+ def txt(alternate_host = nil)
345
+ return nil unless dns_enabled?
319
346
  Resolv::DNS.open do |dns|
320
- records = dns.getresources(alternate_host || self.dns_name,
321
- Resolv::DNS::Resource::IN::TXT)
347
+ dns.timeouts = @config[:dns_timeout] if @config[:dns_timeout]
348
+ records = begin
349
+ dns.getresources(alternate_host || dns_name,
350
+ Resolv::DNS::Resource::IN::TXT)
351
+ rescue Resolv::ResolvTimeout
352
+ []
353
+ end
354
+
322
355
  records.empty? ? nil : records.map(&:data).join(" ")
323
356
  end
324
357
  end
325
358
 
326
359
  # Parses TXT record pairs into a hash
327
- def txt_hash(alternate_host=nil)
360
+ def txt_hash(alternate_host = nil)
328
361
  fields = {}
329
- record = self.txt(alternate_host)
362
+ record = txt(alternate_host)
330
363
  return fields unless record
331
364
 
332
365
  record.split(/\s*;\s*/).each do |pair|
333
- (n,v) = pair.split(/\s*=\s*/)
366
+ (n, v) = pair.split(/\s*=\s*/)
334
367
  fields[n.to_sym] = v
335
368
  end
336
369
  fields
@@ -339,7 +372,7 @@ module EmailAddress
339
372
  # Returns a hash of the domain's DMARC (https://en.wikipedia.org/wiki/DMARC)
340
373
  # settings.
341
374
  def dmarc
342
- self.dns_name ? self.txt_hash("_dmarc." + self.dns_name) : {}
375
+ dns_name ? txt_hash("_dmarc." + dns_name) : {}
343
376
  end
344
377
 
345
378
  ############################################################################
@@ -347,34 +380,113 @@ module EmailAddress
347
380
  ############################################################################
348
381
 
349
382
  # Returns true if the host name is valid according to the current configuration
350
- def valid?(rule=@config[:dns_lookup]||:mx)
351
- if self.provider != :default # well known
383
+ def valid?(rules = {})
384
+ host_validation = rules[:host_validation] || @config[:host_validation] || :mx
385
+ dns_lookup = rules[:dns_lookup] || host_validation
386
+ self.error_message = nil
387
+ if ip_address
388
+ valid_ip?
389
+ elsif !valid_format?
390
+ false
391
+ elsif dns_lookup == :connect
392
+ valid_mx? && connect
393
+ elsif dns_lookup == :mx
394
+ valid_mx?
395
+ elsif dns_lookup == :a
396
+ valid_dns?
397
+ else
352
398
  true
353
- elsif self.ip_address
354
- @config[:host_allow_ip] && self.valid_ip?
355
- elsif rule == :mx
356
- self.exchangers.mx_ips.size > 0
357
- elsif rule == :a
358
- self.has_dns_a_record?
359
- elsif rule == :off
360
- self.to_s.size <= MAX_HOST_LENGTH
399
+ end
400
+ end
401
+
402
+ # True if the host name has a DNS A Record
403
+ def valid_dns?
404
+ return true unless dns_enabled?
405
+ bool = dns_a_record.size > 0 || set_error(:domain_unknown)
406
+ if localhost? && !@config[:host_local]
407
+ bool = set_error(:domain_no_localhost)
408
+ end
409
+ bool
410
+ end
411
+
412
+ # True if the host name has valid MX servers configured in DNS
413
+ def valid_mx?
414
+ return true unless dns_enabled?
415
+ if exchangers.nil?
416
+ set_error(:domain_unknown)
417
+ elsif exchangers.mx_ips.size > 0
418
+ if localhost? && !@config[:host_local]
419
+ set_error(:domain_no_localhost)
420
+ else
421
+ true
422
+ end
423
+ elsif @config[:dns_timeout].nil? && valid_dns?
424
+ set_error(:domain_does_not_accept_email)
361
425
  else
362
- false
426
+ set_error(:domain_unknown)
427
+ end
428
+ end
429
+
430
+ # True if the host_name passes Regular Expression match and size limits.
431
+ def valid_format?
432
+ if host_name =~ CANONICAL_HOST_REGEX && to_s.size <= MAX_HOST_LENGTH
433
+ return true if localhost?
434
+ return true if host_name.include?(".") # require FQDN
363
435
  end
436
+ set_error(:domain_invalid)
364
437
  end
365
438
 
366
439
  # Returns true if the IP address given in that form of the host name
367
440
  # is a potentially valid IP address. It does not check if the address
368
441
  # is reachable.
369
442
  def valid_ip?
370
- if self.ip_address.nil?
371
- false
372
- elsif self.ip_address.include?(":")
373
- self.ip_address =~ Resolv::IPv6::Regex
374
- elsif self.ip_address.include?(".")
375
- self.ip_address =~ Resolv::IPv4::Regex
443
+ if !@config[:host_allow_ip]
444
+ bool = set_error(:ip_address_forbidden)
445
+ elsif ip_address.include?(":")
446
+ bool = ip_address.match(Resolv::IPv6::Regex) ? true : set_error(:ipv6_address_invalid)
447
+ elsif ip_address.include?(".")
448
+ bool = ip_address.match(Resolv::IPv4::Regex) ? true : set_error(:ipv4_address_invalid)
449
+ end
450
+ if bool && (localhost? && !@config[:host_local])
451
+ bool = set_error(:ip_address_no_localhost)
452
+ end
453
+ bool
454
+ end
455
+
456
+ def localhost?
457
+ return true if host_name == "localhost"
458
+ return false unless ip_address
459
+ IPAddr.new(ip_address).loopback?
460
+ end
461
+
462
+ # Connects to host to test it can receive email. This should NOT be performed
463
+ # as an email address check, but is provided to assist in problem resolution.
464
+ # If you abuse this, you *could* be blocked by the ESP.
465
+ def connect
466
+ smtp = Net::SMTP.new(host_name || ip_address)
467
+ smtp.start(@config[:helo_name] || "localhost")
468
+ smtp.finish
469
+ true
470
+ rescue Net::SMTPFatalError => e
471
+ set_error(:server_not_available, e.to_s)
472
+ rescue SocketError => e
473
+ set_error(:server_not_available, e.to_s)
474
+ ensure
475
+ if smtp&.started?
476
+ smtp.finish
376
477
  end
377
478
  end
378
479
 
480
+ def set_error(err, reason = nil)
481
+ @error = err
482
+ @reason = reason
483
+ @error_message = Config.error_message(err, locale)
484
+ false
485
+ end
486
+
487
+ # The inverse of valid? -- Returns nil (falsey) if valid, otherwise error message
488
+ def error
489
+ valid? ? nil : @error_message
490
+ end
379
491
  end
380
492
  end