class EmailAddress::Host

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

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

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.

Constants

CANONICAL_HOST_REGEX

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

DNS_HOST_REGEX

Sometimes, you just need a Regexp…

IPV4_HOST_REGEX
IPV6_HOST_REGEX

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

MAX_HOST_LENGTH
STANDARD_HOST_REGEX

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

Attributes

comment[RW]
config[RW]
dns_name[RW]
domain_name[RW]
error_message[RW]
host_name[R]
ip_address[RW]
locale[RW]
provider[RW]
reason[RW]
registration_name[RW]
subdomains[RW]
tld[RW]
tld2[RW]

Public Class Methods

new(host_name, config = {}, locale = "en") click to toggle source

host name -

* host type - :email for an email host, :mx for exchanger host
# File lib/email_address/host.rb, line 85
def initialize(host_name, config = {}, locale = "en")
  @original = host_name ||= ""
  @locale = locale
  config[:host_type] ||= :email
  @config = config.is_a?(Hash) ? Config.new(config) : config
  @error = @error_message = nil
  parse(host_name)
end

Public Instance Methods

canonical() click to toggle source

The canonical host name is the simplified, DNS host name

# File lib/email_address/host.rb, line 109
def canonical
  dns_name
end
connect(timeout = nil) click to toggle source

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.

timeout is the number of seconds to wait before timing out the request and returns false as the connection was unsuccessful.

> NOTE: As of Ruby 3.1, Net::SMTP was moved from the standard library to the > ‘net-smtp’ gem. In order to avoid adding that dependency for this experimental > feature, please add the gem to your Gemfile and require it to use this feature.

# File lib/email_address/host.rb, line 474
def connect(timeout = nil)
  smtp = Net::SMTP.new(host_name || ip_address)
  smtp.open_timeout = timeout || @config[:host_timeout]
  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)
rescue Net::OpenTimeout => e
  set_error(:server_not_available, e.to_s)
ensure
  smtp.finish if smtp&.started?
end
dmarc() click to toggle source

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

# File lib/email_address/host.rb, line 373
def dmarc
  dns_name ? txt_hash("_dmarc." + dns_name) : {}
end
dns_a_record() click to toggle source

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

# File lib/email_address/host.rb, line 328
def dns_a_record
  @_dns_a_record = "0.0.0.0" if @config[:dns_lookup] == :off
  @_dns_a_record ||= Addrinfo.getaddrinfo(dns_name, 80) # Port 80 for A rec, 25 for MX
rescue SocketError # not found, but could also mean network not work
  @_dns_a_record ||= []
end
dns_enabled?() click to toggle source

True if the :dns_lookup setting is enabled

# File lib/email_address/host.rb, line 321
def dns_enabled?
  return false if @config[:dns_lookup] == :off
  return false if @config[:host_validation] == :syntax
  true
end
domain_matches?(rule) click to toggle source

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

# File lib/email_address/host.rb, line 301
def domain_matches?(rule)
  rule = $1 if rule =~ /\A@(.+)/
  return rule if domain_name && File.fnmatch?(rule, domain_name)
  return rule if dns_name && File.fnmatch?(rule, dns_name)
  false
end
error() click to toggle source

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

# File lib/email_address/host.rb, line 498
def error
  valid? ? nil : @error_message
end
exchangers() click to toggle source

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

# File lib/email_address/host.rb, line 337
def exchangers
  # return nil if @config[:host_type] != :email || !self.dns_enabled?
  @_exchangers ||= Exchanger.cached(dns_name, @config)
end
fqdn?() click to toggle source

Is this a fully-qualified domain name?

# File lib/email_address/host.rb, line 243
def fqdn?
  tld ? true : false
end
fully_qualified_domain_name(host_part) click to toggle source
# File lib/email_address/host.rb, line 183
def fully_qualified_domain_name(host_part)
  dn = @config[:address_fqdn_domain]
  if !dn
    if (host_part.nil? || host_part <= " ") && @config[:host_local] && @config[:host_auto_append]
      "localhost"
    else
      host_part
    end
  elsif host_part.nil? || host_part <= " "
    dn
  elsif !host_part.include?(".")
    host_part + "." + dn
  else
    host_part
  end
end
host_name=(name) click to toggle source
# File lib/email_address/host.rb, line 145
def host_name=(name)
  name = fully_qualified_domain_name(name.downcase)
  @host_name = name
  if @config[:host_remove_spaces]
    @host_name = @host_name.delete(" ")
  end
  @dns_name = if /[^[:ascii:]]/.match?(host_name)
    ::SimpleIDN.to_ascii(host_name)
  else
    host_name
  end

  # Subdomain only (root@localhost)
  if name.index(".").nil?
    self.subdomains = name

  # Split sub.domain from .tld: *.com, *.xx.cc, *.cc
  elsif name =~ /\A(.+)\.(\w{3,10})\z/ ||
      name =~ /\A(.+)\.(\w{1,3}\.\w\w)\z/ ||
      name =~ /\A(.+)\.(\w\w)\z/

    sub_and_domain, self.tld2 = [$1, $2] # sub+domain, com || co.uk
    self.tld = tld2.sub(/\A.+\./, "") # co.uk => uk
    if sub_and_domain =~ /\A(.+)\.(.+)\z/ # is subdomain? sub.example [.tld2]
      self.subdomains = $1
      self.registration_name = $2
    else
      self.registration_name = sub_and_domain
      # self.domain_name = sub_and_domain + '.' + self.tld2
    end
    self.domain_name = registration_name + "." + tld2
    find_provider
  else # Bad format
    self.subdomains = self.tld = self.tld2 = ""
    self.domain_name = self.registration_name = name
  end
end
hosted_provider() click to toggle source
# File lib/email_address/host.rb, line 234
def hosted_provider
  Exchanger.cached(dns_name).provider
end
hosted_service?() click to toggle source

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

# File lib/email_address/host.rb, line 201
def hosted_service?
  return false unless registration_name
  find_provider
  return false unless config[:host_match]
  !matches?(config[:host_match])
end
ip?() click to toggle source
# File lib/email_address/host.rb, line 247
def ip?
  !!ip_address
end
ip_matches?(cidr) click to toggle source

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

# File lib/email_address/host.rb, line 310
def ip_matches?(cidr)
  return false unless ip_address
  net = IPAddr.new(cidr)
  net.include?(IPAddr.new(ip_address))
end
ipv4?() click to toggle source
# File lib/email_address/host.rb, line 251
def ipv4?
  ip? && ip_address.include?(".")
end
ipv6?() click to toggle source
# File lib/email_address/host.rb, line 255
def ipv6?
  ip? && ip_address.include?(":")
end
localhost?() click to toggle source
# File lib/email_address/host.rb, line 458
def localhost?
  return true if host_name == "localhost"
  return false unless ip_address
  IPAddr.new(ip_address).loopback?
end
matches?(rules) click to toggle source

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

# File lib/email_address/host.rb, line 270
def matches?(rules)
  rules = Array(rules)
  return false if rules.empty?
  rules.each do |rule|
    return rule if rule == domain_name || rule == 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 ip_matches?(rule)
  end
  false
end
munge() click to toggle source

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

# File lib/email_address/host.rb, line 115
def munge
  host_name.sub(/\A(.{1,2}).*/) { |m| $1 + @config[:munge_string] }
end
name() click to toggle source

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

# File lib/email_address/host.rb, line 95
def name
  if ipv4?
    "[#{ip_address}]"
  elsif ipv6?
    "[IPv6:#{ip_address}]"
  elsif @config[:host_encoding] && @config[:host_encoding] == :unicode
    ::SimpleIDN.to_unicode(host_name)
  else
    dns_name
  end
end
Also aliased as: to_s
parts() click to toggle source

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

# File lib/email_address/host.rb, line 228
def parts
  {host_name: host_name, dns_name: dns_name, subdomain: subdomains,
   registration_name: registration_name, domain_name: domain_name,
   tld2: tld2, tld: tld, ip_address: ip_address}
end
provider_matches?(rule) click to toggle source
# File lib/email_address/host.rb, line 295
def provider_matches?(rule)
  rule.to_s =~ /\A[\w\-]*\z/ && self.provider && self.provider == rule.to_sym
end
registration_name_matches?(rule) click to toggle source

Does “example.” match any tld?

# File lib/email_address/host.rb, line 285
def registration_name_matches?(rule)
  rule == "#{registration_name}."
end
set_error(err, reason = nil) click to toggle source
# File lib/email_address/host.rb, line 490
def set_error(err, reason = nil)
  @error = err
  @reason = reason
  @error_message = Config.error_message(err, locale)
  false
end
tld_matches?(rule) click to toggle source

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

# File lib/email_address/host.rb, line 291
def tld_matches?(rule)
  rule.match(/\A\.(.+)\z/) && ($1 == tld || $1 == tld2) # ? true : false
end
to_s()
Alias for: name
txt(alternate_host = nil) click to toggle source

Returns a DNS TXT Record

# File lib/email_address/host.rb, line 343
def txt(alternate_host = nil)
  return nil unless dns_enabled?
  Resolv::DNS.open do |dns|
    dns.timeouts = @config[:dns_timeout] if @config[:dns_timeout]
    records = begin
      dns.getresources(alternate_host || dns_name,
        Resolv::DNS::Resource::IN::TXT)
    rescue Resolv::ResolvTimeout
      []
    end

    records.empty? ? nil : records.map(&:data).join(" ")
  end
end
txt_hash(alternate_host = nil) click to toggle source

Parses TXT record pairs into a hash

# File lib/email_address/host.rb, line 359
def txt_hash(alternate_host = nil)
  fields = {}
  record = 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 = {}) click to toggle source

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

# File lib/email_address/host.rb, line 382
def valid?(rules = {})
  host_validation = rules[:host_validation] || @config[:host_validation] || :mx
  dns_lookup = rules[:dns_lookup] || host_validation
  self.error_message = nil
  if host_name && !host_name.empty? && !@config[:host_size].include?(host_name.size)
    return set_error(:invalid_host)
  end
  if 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?() click to toggle source

True if the host name has a DNS A Record

# File lib/email_address/host.rb, line 405
def valid_dns?
  return true unless dns_enabled?
  dns_a_record.size > 0 || set_error(:domain_unknown)
end
valid_format?() click to toggle source

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

# File lib/email_address/host.rb, line 429
def valid_format?
  if host_name =~ CANONICAL_HOST_REGEX && to_s.size <= MAX_HOST_LENGTH
    if localhost?
      return @config[:host_local] ? true : set_error(:domain_no_localhost)
    end

    return true if !@config[:host_fqdn]
    return true if host_name.include?(".") # require FQDN
  end
  set_error(:domain_invalid)
end
valid_ip?() click to toggle source

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.

# File lib/email_address/host.rb, line 444
def valid_ip?
  if !@config[:host_allow_ip]
    bool = set_error(:ip_address_forbidden)
  elsif ip_address.include?(":")
    bool = ip_address.match(Resolv::IPv6::Regex) ? true : set_error(:ipv6_address_invalid)
  elsif ip_address.include?(".")
    bool = ip_address.match(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?() click to toggle source

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

# File lib/email_address/host.rb, line 411
def valid_mx?
  return true unless dns_enabled?
  if exchangers.nil?
    set_error(:domain_unknown)
  elsif exchangers.mx_ips.size > 0
    if localhost? && !@config[:host_local]
      set_error(:domain_no_localhost)
    else
      true
    end
  elsif @config[:dns_timeout].nil? && valid_dns?
    set_error(:domain_does_not_accept_email)
  else
    set_error(:domain_unknown)
  end
end