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


86
87
88
89
90
91
# File 'lib/email_address/host.rb', line 86

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

Instance Attribute Details

#commentObject

Returns the value of attribute comment.



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

def comment
  @comment
end

#configObject

Returns the value of attribute config.



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

def config
  @config
end

#dns_nameObject

Returns the value of attribute dns_name.



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

def dns_name
  @dns_name
end

#domain_nameObject

Returns the value of attribute domain_name.



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

def domain_name
  @domain_name
end

#error_messageObject

Returns the value of attribute error_message.



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

def error_message
  @error_message
end

#host_nameObject

Returns the value of attribute host_name.



33
34
35
# File 'lib/email_address/host.rb', line 33

def host_name
  @host_name
end

#ip_addressObject

Returns the value of attribute ip_address.



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

def ip_address
  @ip_address
end

#providerObject

Returns the value of attribute provider.



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

def provider
  @provider
end

#registration_nameObject

Returns the value of attribute registration_name.



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

def registration_name
  @registration_name
end

#subdomainsObject

Returns the value of attribute subdomains.



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

def subdomains
  @subdomains
end

#tldObject

Returns the value of attribute tld.



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

def tld
  @tld
end

#tld2Object

Returns the value of attribute tld2.



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

def tld2
  @tld2
end

Instance Method Details

#canonicalObject

The canonical host name is the simplified, DNS host name



108
109
110
# File 'lib/email_address/host.rb', line 108

def canonical
  self.dns_name
end

#dmarcObject

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



354
355
356
# File 'lib/email_address/host.rb', line 354

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]



317
318
319
320
321
# File 'lib/email_address/host.rb', line 317

def dns_a_record
  @_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)


312
313
314
# File 'lib/email_address/host.rb', line 312

def dns_enabled?
  EmailAddress::Config.setting(:dns_lookup).equal?(:off) ? false : true
end

#domain_matches?(rule) ⇒ Boolean

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

Returns:

  • (Boolean)


285
286
287
288
289
290
# File 'lib/email_address/host.rb', line 285

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



381
382
383
# File 'lib/email_address/host.rb', line 381

def error
  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.



325
326
327
328
# File 'lib/email_address/host.rb', line 325

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

#find_providerObject

:nodoc:



190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/email_address/host.rb', line 190

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)


227
228
229
# File 'lib/email_address/host.rb', line 227

def fqdn?
  self.tld ? true : false
end

#hosted_service?Boolean

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

Returns:

  • (Boolean)


183
184
185
186
187
188
# File 'lib/email_address/host.rb', line 183

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)


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

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)


294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/email_address/host.rb', line 294

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)


235
236
237
# File 'lib/email_address/host.rb', line 235

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

#ipv6?Boolean

Returns:

  • (Boolean)


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

def ipv6?
  self.ip? && self.ip_address.include?(":")
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)


254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/email_address/host.rb', line 254

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”.



114
115
116
# File 'lib/email_address/host.rb', line 114

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)



94
95
96
97
98
99
100
101
102
103
104
# File 'lib/email_address/host.rb', line 94

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:



123
124
125
126
127
128
129
130
131
132
133
# File 'lib/email_address/host.rb', line 123

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:



135
136
137
138
139
140
141
142
143
# File 'lib/email_address/host.rb', line 135

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.



216
217
218
219
220
# File 'lib/email_address/host.rb', line 216

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)


279
280
281
# File 'lib/email_address/host.rb', line 279

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)


269
270
271
# File 'lib/email_address/host.rb', line 269

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

#set_error(err) ⇒ Object



423
424
425
426
# File 'lib/email_address/host.rb', line 423

def set_error(err)
  @error_message = EmailAddress::Config.error_messages.fetch(err) { err }
  false
end

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

:nodoc:



210
211
212
213
# File 'lib/email_address/host.rb', line 210

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)


275
276
277
# File 'lib/email_address/host.rb', line 275

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



331
332
333
334
335
336
337
# File 'lib/email_address/host.rb', line 331

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



340
341
342
343
344
345
346
347
348
349
350
# File 'lib/email_address/host.rb', line 340

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?(rule = @config[:dns_lookup]||:mx) ⇒ Boolean

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

Returns:

  • (Boolean)


363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
# File 'lib/email_address/host.rb', line 363

def valid?(rule=@config[:dns_lookup]||:mx)
  self.error_message = nil
  if self.ip_address
    valid_ip?
  elsif ! valid_format?
    false
  elsif rule == :mx
    valid_mx?
  elsif rule == :a
    valid_dns?
  elsif rule == :off
    true
  else
    set_error(:domain_bad_validation_rule)
  end
end

#valid_dns?Boolean

True if the host name has a DNS A Record

Returns:

  • (Boolean)


386
387
388
# File 'lib/email_address/host.rb', line 386

def valid_dns?
  dns_a_record.size > 0 || set_error(:domain_unknown)
end

#valid_format?Boolean

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

Returns:

  • (Boolean)


402
403
404
405
406
407
408
# File 'lib/email_address/host.rb', line 402

def valid_format?
  if self.host_name =~ CANONICAL_HOST_REGEX && self.to_s.size <= MAX_HOST_LENGTH
    true
  else
    set_error(:domain_invalid)
  end
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)


413
414
415
416
417
418
419
420
421
# File 'lib/email_address/host.rb', line 413

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

#valid_mx?Boolean

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

Returns:

  • (Boolean)


391
392
393
394
395
396
397
398
399
# File 'lib/email_address/host.rb', line 391

def valid_mx?
  if self.exchangers.mx_ips.size > 0
    true
  elsif valid_dns?
    set_error(:domain_does_not_accept_email)
  else
    set_error(:domain_unknown)
  end
end