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

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



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

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]



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

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)


302
303
304
# File 'lib/email_address/host.rb', line 302

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)


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

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

#exchangersObject

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



320
321
322
323
# File 'lib/email_address/host.rb', line 320

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

#find_providerObject

:nodoc:



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/email_address/host.rb', line 180

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)


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

def fqdn?
  self.tld ? true : false
end

#has_dns_a_record?Boolean

True if the host name has a DNS A Record

Returns:

  • (Boolean)


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

def has_dns_a_record?
  dns_a_record.size > 0 ? true : false
end

#hosted_service?Boolean

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

Returns:

  • (Boolean)


173
174
175
176
177
178
# File 'lib/email_address/host.rb', line 173

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)


221
222
223
# File 'lib/email_address/host.rb', line 221

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)


284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/email_address/host.rb', line 284

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)


225
226
227
# File 'lib/email_address/host.rb', line 225

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

#ipv6?Boolean

Returns:

  • (Boolean)


229
230
231
# File 'lib/email_address/host.rb', line 229

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)


244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/email_address/host.rb', line 244

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.



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

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)


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

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)


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

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

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

:nodoc:



200
201
202
203
# File 'lib/email_address/host.rb', line 200

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)


265
266
267
# File 'lib/email_address/host.rb', line 265

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



326
327
328
329
330
331
332
# File 'lib/email_address/host.rb', line 326

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



335
336
337
338
339
340
341
342
343
344
345
# File 'lib/email_address/host.rb', line 335

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)


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

def valid?(rule=@config[:dns_lookup]||:mx)
  if self.provider != :default # well known
    true
  elsif self.ip_address
    @config[:host_allow_ip] && self.valid_ip?
  elsif rule == :mx
    self.exchangers.mx_ips.size > 0
  elsif rule == :a
    self.has_dns_a_record?
  elsif rule == :off
    self.to_s.size <= MAX_HOST_LENGTH
  else
    false
  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)


377
378
379
380
381
382
383
384
385
# File 'lib/email_address/host.rb', line 377

def valid_ip?
  if self.ip_address.nil?
    false
  elsif self.ip_address.include?(":")
    self.ip_address =~ Resolv::IPv6::Regex
  elsif self.ip_address.include?(".")
    self.ip_address =~ Resolv::IPv4::Regex
  end
end