Class: EmailAddress::Host

Inherits:
Object
  • Object
show all
Defined in:
lib/email_address/host.rb

Overview

The EmailAddress Host is found on the right-hand side of the “@” symbol. It can be:

  • Host name (domain name with optional subdomain)

  • International Domain Name, in Unicode (Display) or Punycode (DNS) format

  • IP Address format, either IPv4 or IPv6, enclosed in square brackets. This is not Conventionally supported, but is part of the specification.

  • It can contain an optional comment, enclosed in parenthesis, either at beginning or ending of the host name. This is not well defined, so it not supported here, expect to parse it off, if found.

For matching and query capabilities, the host name is parsed into these parts (with example data for “subdomain.example.co.uk”):

  • host_name: “subdomain.example.co.uk”

  • dns_name: punycode(“subdomain.example.co.uk”)

  • subdomain: “subdomain”

  • registration_name: “example”

  • domain_name: “example.co.uk”

  • tld: “uk”

  • tld2: “co.uk” (the 1 or 2 term TLD we could guess)

  • ip_address: nil or “ipaddress” used in [ipaddress] syntax

The provider (Email Service Provider or ESP) is looked up according to the provider configuration rules, setting the config attribute to values of that provider.

Constant Summary collapse

MAX_HOST_LENGTH =
255
DNS_HOST_REGEX =

Sometimes, you just need a Regexp…

/ [\p{L}\p{N}]+ (?: (?: \-{1,2} | \.) [\p{L}\p{N}]+ )*/x
IPv6_HOST_REGEX =

The IPv4 and IPv6 were lifted from Resolv::IPv?::Regex and tweaked to not A…z anchor at the edges.

/\[IPv6:
(?: (?:(?x-mi:
(?:[0-9A-Fa-f]{1,4}:){7}
   [0-9A-Fa-f]{1,4}
)) |
(?:(?x-mi:
(?: (?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::
(?: (?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)
)) |
(?:(?x-mi:
(?: (?:[0-9A-Fa-f]{1,4}:){6,6})
(?: \d+)\.(?: \d+)\.(?: \d+)\.(?: \d+)
)) |
(?:(?x-mi:
(?: (?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::
(?: (?:[0-9A-Fa-f]{1,4}:)*)
(?: \d+)\.(?: \d+)\.(?: \d+)\.(?: \d+)
)))\]/ix
IPv4_HOST_REGEX =
/\[((?x-mi:0
|1(?:[0-9][0-9]?)?
|2(?:[0-4][0-9]?|5[0-5]?|[6-9])?
|[3-9][0-9]?))\.((?x-mi:0
|1(?:[0-9][0-9]?)?
|2(?:[0-4][0-9]?|5[0-5]?|[6-9])?
|[3-9][0-9]?))\.((?x-mi:0
|1(?:[0-9][0-9]?)?
|2(?:[0-4][0-9]?|5[0-5]?|[6-9])?
|[3-9][0-9]?))\.((?x-mi:0
|1(?:[0-9][0-9]?)?
|2(?:[0-4][0-9]?|5[0-5]?|[6-9])?
|[3-9][0-9]?))\]/x
CANONICAL_HOST_REGEX =

Matches conventional host name and punycode: domain.tld, x–punycode.tld

/\A #{DNS_HOST_REGEX} \z/x
STANDARD_HOST_REGEX =

Matches Host forms: DNS name, IPv4, or IPv6 formats

/\A (?: #{DNS_HOST_REGEX}
| #{IPv4_HOST_REGEX} | #{IPv6_HOST_REGEX}) \z/ix

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(host_name, config = {}) ⇒ Host

host name -

* host type - :email for an email host, :mx for exchanger host


88
89
90
91
92
93
94
# File 'lib/email_address/host.rb', line 88

def initialize(host_name, config={})
  @original            = host_name ||= ''
  config[:host_type] ||= :email
  @config              = config
  @error               = @error_message = nil
  parse(host_name)
end

Instance Attribute Details

#commentObject

Returns the value of attribute comment.



36
37
38
# File 'lib/email_address/host.rb', line 36

def comment
  @comment
end

#configObject

Returns the value of attribute config.



36
37
38
# File 'lib/email_address/host.rb', line 36

def config
  @config
end

#dns_nameObject

Returns the value of attribute dns_name.



36
37
38
# File 'lib/email_address/host.rb', line 36

def dns_name
  @dns_name
end

#domain_nameObject

Returns the value of attribute domain_name.



36
37
38
# File 'lib/email_address/host.rb', line 36

def domain_name
  @domain_name
end

#error_messageObject

Returns the value of attribute error_message.



36
37
38
# File 'lib/email_address/host.rb', line 36

def error_message
  @error_message
end

#host_nameObject

Returns the value of attribute host_name.



35
36
37
# File 'lib/email_address/host.rb', line 35

def host_name
  @host_name
end

#ip_addressObject

Returns the value of attribute ip_address.



36
37
38
# File 'lib/email_address/host.rb', line 36

def ip_address
  @ip_address
end

#providerObject

Returns the value of attribute provider.



36
37
38
# File 'lib/email_address/host.rb', line 36

def provider
  @provider
end

#reasonObject

Returns the value of attribute reason.



36
37
38
# File 'lib/email_address/host.rb', line 36

def reason
  @reason
end

#registration_nameObject

Returns the value of attribute registration_name.



36
37
38
# File 'lib/email_address/host.rb', line 36

def registration_name
  @registration_name
end

#subdomainsObject

Returns the value of attribute subdomains.



36
37
38
# File 'lib/email_address/host.rb', line 36

def subdomains
  @subdomains
end

#tldObject

Returns the value of attribute tld.



36
37
38
# File 'lib/email_address/host.rb', line 36

def tld
  @tld
end

#tld2Object

Returns the value of attribute tld2.



36
37
38
# File 'lib/email_address/host.rb', line 36

def tld2
  @tld2
end

Instance Method Details

#canonicalObject

The canonical host name is the simplified, DNS host name



111
112
113
# File 'lib/email_address/host.rb', line 111

def canonical
  self.dns_name
end

#connectObject

Connects to host to test it can receive email. This should NOT be performed as an email address check, but is provided to assist in problem resolution. If you abuse this, you could be blocked by the ESP.



477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
# File 'lib/email_address/host.rb', line 477

def connect
  begin
    smtp = Net::SMTP.new(self.host_name || self.ip_address)
    smtp.start(@config[:helo_name] || 'localhost')
    smtp.finish
    true
  rescue Net::SMTPFatalError => e
    set_error(:server_not_available, e.to_s)
  rescue SocketError => e
    set_error(:server_not_available, e.to_s)
  ensure
    if smtp && smtp.started?
      smtp.finish
    end
  end
end

#dmarcObject

Returns a hash of the domain’s DMARC (en.wikipedia.org/wiki/DMARC) settings.



376
377
378
# File 'lib/email_address/host.rb', line 376

def dmarc
  self.dns_name ? self.txt_hash("_dmarc." + self.dns_name) : {}
end

#dns_a_recordObject

Returns: [official_hostname, alias_hostnames, address_family, *address_list]



338
339
340
341
342
343
# File 'lib/email_address/host.rb', line 338

def dns_a_record
  @_dns_a_record = "0.0.0.0" if @config[:dns_lookup] == :off
  @_dns_a_record ||= Socket.gethostbyname(self.dns_name)
rescue SocketError # not found, but could also mean network not work
  @_dns_a_record ||= []
end

#dns_enabled?Boolean

True if the :dns_lookup setting is enabled

Returns:

  • (Boolean)


333
334
335
# File 'lib/email_address/host.rb', line 333

def dns_enabled?
  [:mx, :a].include?(EmailAddress::Config.setting(:host_validation))
end

#domain_matches?(rule) ⇒ Boolean

Does domain == rule or glob matches? (also tests the DNS (punycode) name) Requires optionally starts with a “@”.

Returns:

  • (Boolean)


306
307
308
309
310
311
# File 'lib/email_address/host.rb', line 306

def domain_matches?(rule)
  rule = $1 if rule =~ /\A@(.+)/
  return rule if File.fnmatch?(rule, self.domain_name)
  return rule if File.fnmatch?(rule, self.dns_name)
  false
end

#errorObject

The inverse of valid? – Returns nil (falsey) if valid, otherwise error message



502
503
504
# File 'lib/email_address/host.rb', line 502

def error
  self.valid? ? nil : @error_message
end

#exchangersObject

Returns an array of EmailAddress::Exchanger hosts configured in DNS. The array will be empty if none are configured.



347
348
349
350
# File 'lib/email_address/host.rb', line 347

def exchangers
  #return nil if @config[:host_type] != :email || !self.dns_enabled?
  @_exchangers ||= EmailAddress::Exchanger.cached(self.dns_name, @config)
end

#find_providerObject

:nodoc:



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/email_address/host.rb', line 211

def find_provider # :nodoc:
  return self.provider if self.provider

  EmailAddress::Config.providers.each do |provider, config|
    if config[:host_match] && self.matches?(config[:host_match])
      return self.set_provider(provider, config)
    end
  end

  return self.set_provider(:default) unless self.dns_enabled?

  provider = self.exchangers.provider
  if provider != :default
    self.set_provider(provider,
      EmailAddress::Config.provider(provider))
  end

  self.provider ||= self.set_provider(:default)
end

#fqdn?Boolean

Is this a fully-qualified domain name?

Returns:

  • (Boolean)


248
249
250
# File 'lib/email_address/host.rb', line 248

def fqdn?
  self.tld ? true : false
end

#fully_qualified_domain_name(host_part) ⇒ Object



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/email_address/host.rb', line 186

def fully_qualified_domain_name(host_part)
  dn = @config[:address_fqdn_domain]
  if !dn
    if (host_part.nil? || host_part <= " ") && @config[:host_local]
      'localhost'
    else
      host_part
    end
  elsif host_part.nil? || host_part <= " "
    dn
  elsif !host_part.include?(".")
    host_part + "." + dn
  else
    host_part
  end
end

#hosted_service?Boolean

True if host is hosted at the provider, not a public provider host name

Returns:

  • (Boolean)


204
205
206
207
208
209
# File 'lib/email_address/host.rb', line 204

def hosted_service?
  return false unless registration_name
  find_provider
  return false unless config[:host_match]
  ! matches?(config[:host_match])
end

#ip?Boolean

Returns:

  • (Boolean)


252
253
254
# File 'lib/email_address/host.rb', line 252

def ip?
  self.ip_address.nil? ? false : true
end

#ip_matches?(cidr) ⇒ Boolean

True if the host is an IP Address form, and that address matches the passed CIDR string (“10.9.8.0/24” or “2001:.…/64”)

Returns:

  • (Boolean)


315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/email_address/host.rb', line 315

def ip_matches?(cidr)
  return false unless self.ip_address
  return cidr if !cidr.include?("/") && cidr == self.ip_address

  c = NetAddr::CIDR.create(cidr)
  if cidr.include?(":") && self.ip_address.include?(":")
    return cidr if c.matches?(self.ip_address)
  elsif cidr.include?(".") && self.ip_address.include?(".")
    return cidr if c.matches?(self.ip_address)
  end
  false
end

#ipv4?Boolean

Returns:

  • (Boolean)


256
257
258
# File 'lib/email_address/host.rb', line 256

def ipv4?
  self.ip? && self.ip_address.include?(".")
end

#ipv6?Boolean

Returns:

  • (Boolean)


260
261
262
# File 'lib/email_address/host.rb', line 260

def ipv6?
  self.ip? && self.ip_address.include?(":")
end

#localhost?Boolean

Returns:

  • (Boolean)


456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
# File 'lib/email_address/host.rb', line 456

def localhost?
  if self.ip_address
    rel =
      if self.ip_address.include?(":")
        NetAddr::IPv6Net.parse(""+"::1").rel(
          NetAddr::IPv6Net.parse(self.ip_address)
        )
      else
        NetAddr::IPv4Net.parse(""+"127.0.0.0/8").rel(
          NetAddr::IPv4Net.parse(self.ip_address)
        )
      end
    !rel.nil? && rel >= 0
  else
    self.host_name == 'localhost'
  end
end

#matches?(rules) ⇒ Boolean

Takes a email address string, returns true if it matches a rule Rules of the follow formats are evaluated:

  • “example.” => registration name

  • “.com” => top-level domain name

  • “google” => email service provider designation

  • “@goog*.com” => Glob match

  • IPv4 or IPv6 or CIDR Address

Returns:

  • (Boolean)


275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/email_address/host.rb', line 275

def matches?(rules)
  rules = Array(rules)
  return false if rules.empty?
  rules.each do |rule|
    return rule if rule == self.domain_name || rule == self.dns_name
    return rule if registration_name_matches?(rule)
    return rule if tld_matches?(rule)
    return rule if domain_matches?(rule)
    return rule if self.provider && provider_matches?(rule)
    return rule if self.ip_matches?(rule)
  end
  false
end

#mungeObject

Returns the munged version of the name, replacing everything after the initial two characters with “*****” or the configured “munge_string”.



117
118
119
# File 'lib/email_address/host.rb', line 117

def munge
  self.host_name.sub(/\A(.{1,2}).*/) { |m| $1 + @config[:munge_string] }
end

#nameObject Also known as: to_s

Returns the String representation of the host name (or IP)



97
98
99
100
101
102
103
104
105
106
107
# File 'lib/email_address/host.rb', line 97

def name
  if self.ipv4?
    "[#{self.ip_address}]"
  elsif self.ipv6?
    "[IPv6:#{self.ip_address}]"
  elsif @config[:host_encoding] && @config[:host_encoding] == :unicode
    ::SimpleIDN.to_unicode(self.host_name)
  else
    self.dns_name
  end
end

#parse(host) ⇒ Object

:nodoc:



126
127
128
129
130
131
132
133
134
135
136
# File 'lib/email_address/host.rb', line 126

def parse(host) # :nodoc:
  host = self.parse_comment(host)

  if host =~ /\A\[IPv6:(.+)\]/i
    self.ip_address = $1
  elsif host =~ /\A\[(\d{1,3}(\.\d{1,3}){3})\]/ # IPv4
    self.ip_address = $1
  else
    self.host_name = host
  end
end

#parse_comment(host) ⇒ Object

:nodoc:



138
139
140
141
142
143
144
145
146
# File 'lib/email_address/host.rb', line 138

def parse_comment(host) # :nodoc:
  if host =~ /\A\((.+?)\)(.+)/ # (comment)domain.tld
    self.comment, host = $1, $2
  end
  if host =~ /\A(.+)\((.+?)\)\z/ # domain.tld(comment)
    host, self.comment = $1, $2
  end
  host
end

#partsObject

Returns a hash of the parts of the host name after parsing.



237
238
239
240
241
# File 'lib/email_address/host.rb', line 237

def parts
  { host_name:self.host_name, dns_name:self.dns_name, subdomain:self.subdomains,
    registration_name:self.registration_name, domain_name:self.domain_name,
    tld2:self.tld2, tld:self.tld, ip_address:self.ip_address }
end

#provider_matches?(rule) ⇒ Boolean

Returns:

  • (Boolean)


300
301
302
# File 'lib/email_address/host.rb', line 300

def provider_matches?(rule)
  rule.to_s =~ /\A[\w\-]*\z/ && self.provider && self.provider == rule.to_sym
end

#registration_name_matches?(rule) ⇒ Boolean

Does “example.” match any tld?

Returns:

  • (Boolean)


290
291
292
# File 'lib/email_address/host.rb', line 290

def registration_name_matches?(rule)
  self.registration_name + '.' == rule ? true : false
end

#set_error(err, reason = nil) ⇒ Object



494
495
496
497
498
499
# File 'lib/email_address/host.rb', line 494

def set_error(err, reason=nil)
  @error         = err
  @reason        = reason
  @error_message = EmailAddress::Config.error_message(err)
  false
end

#set_provider(name, provider_config = {}) ⇒ Object

:nodoc:



231
232
233
234
# File 'lib/email_address/host.rb', line 231

def set_provider(name, provider_config={}) # :nodoc:
  self.config = EmailAddress::Config.all_settings(provider_config, @config)
  self.provider = name
end

#tld_matches?(rule) ⇒ Boolean

Does “sub.example.com” match “.com” and “.example.com” top level names? Matches TLD (uk) or TLD2 (co.uk)

Returns:

  • (Boolean)


296
297
298
# File 'lib/email_address/host.rb', line 296

def tld_matches?(rule)
  rule.match(/\A\.(.+)\z/) && ($1 == self.tld || $1 == self.tld2) ? true : false
end

#txt(alternate_host = nil) ⇒ Object

Returns a DNS TXT Record



353
354
355
356
357
358
359
# File 'lib/email_address/host.rb', line 353

def txt(alternate_host=nil)
  Resolv::DNS.open do |dns|
    records = dns.getresources(alternate_host || self.dns_name,
                     Resolv::DNS::Resource::IN::TXT)
    records.empty? ? nil : records.map(&:data).join(" ")
  end
end

#txt_hash(alternate_host = nil) ⇒ Object

Parses TXT record pairs into a hash



362
363
364
365
366
367
368
369
370
371
372
# File 'lib/email_address/host.rb', line 362

def txt_hash(alternate_host=nil)
  fields = {}
  record = self.txt(alternate_host)
  return fields unless record

  record.split(/\s*;\s*/).each do |pair|
    (n,v) = pair.split(/\s*=\s*/)
    fields[n.to_sym] = v
  end
  fields
end

#valid?(rules = {}) ⇒ Boolean

Returns true if the host name is valid according to the current configuration

Returns:

  • (Boolean)


385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
# File 'lib/email_address/host.rb', line 385

def valid?(rules={})
  host_validation = rules[:host_validation] || @config[:host_validation] || :mx
  dns_lookup      = rules[:dns_lookup] || host_validation
  self.error_message = nil
  if self.ip_address
    valid_ip?
  elsif ! valid_format?
    false
  elsif dns_lookup == :connect
    valid_mx? && connect
  elsif dns_lookup == :mx
    valid_mx?
  elsif dns_lookup == :a
    valid_dns?
  else
    true
  end
end

#valid_dns?Boolean

True if the host name has a DNS A Record

Returns:

  • (Boolean)


405
406
407
408
409
410
411
# File 'lib/email_address/host.rb', line 405

def valid_dns?
  bool = dns_a_record.size > 0 || set_error(:domain_unknown)
  if self.localhost? && !@config[:host_local]
    bool = set_error(:domain_no_localhost)
  end
  bool
end

#valid_format?Boolean

True if the host_name passes Regular Expression match and size limits.

Returns:

  • (Boolean)


431
432
433
434
435
436
437
# File 'lib/email_address/host.rb', line 431

def valid_format?
  if self.host_name =~ CANONICAL_HOST_REGEX && self.to_s.size <= MAX_HOST_LENGTH
    return true if localhost?
    return true if self.host_name.include?(".") # require FQDN
  end
  set_error(:domain_invalid)
end

#valid_ip?Boolean

Returns true if the IP address given in that form of the host name is a potentially valid IP address. It does not check if the address is reachable.

Returns:

  • (Boolean)


442
443
444
445
446
447
448
449
450
451
452
453
454
# File 'lib/email_address/host.rb', line 442

def valid_ip?
  if ! @config[:host_allow_ip]
    bool = set_error(:ip_address_forbidden)
  elsif self.ip_address.include?(":")
    bool = self.ip_address =~ Resolv::IPv6::Regex ? true : set_error(:ipv6_address_invalid)
  elsif self.ip_address.include?(".")
    bool = self.ip_address =~ Resolv::IPv4::Regex ? true : set_error(:ipv4_address_invalid)
  end
  if bool && (localhost? && !@config[:host_local])
    bool = set_error(:ip_address_no_localhost)
  end
  bool
end

#valid_mx?Boolean

True if the host name has valid MX servers configured in DNS

Returns:

  • (Boolean)


414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
# File 'lib/email_address/host.rb', line 414

def valid_mx?
  if self.exchangers.nil?
    set_error(:domain_unknown)
  elsif self.exchangers.mx_ips.size > 0
    if self.localhost? && !@config[:host_local]
      set_error(:domain_no_localhost)
    else
      true
    end
  elsif valid_dns?
    set_error(:domain_does_not_accept_email)
  else
    set_error(:domain_unknown)
  end
end