Class: EmailAddress::Host
- Inherits:
-
Object
- Object
- EmailAddress::Host
- 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
-
#comment ⇒ Object
Returns the value of attribute comment.
-
#config ⇒ Object
Returns the value of attribute config.
-
#dns_name ⇒ Object
Returns the value of attribute dns_name.
-
#domain_name ⇒ Object
Returns the value of attribute domain_name.
-
#error_message ⇒ Object
Returns the value of attribute error_message.
-
#host_name ⇒ Object
Returns the value of attribute host_name.
-
#ip_address ⇒ Object
Returns the value of attribute ip_address.
-
#provider ⇒ Object
Returns the value of attribute provider.
-
#registration_name ⇒ Object
Returns the value of attribute registration_name.
-
#subdomains ⇒ Object
Returns the value of attribute subdomains.
-
#tld ⇒ Object
Returns the value of attribute tld.
-
#tld2 ⇒ Object
Returns the value of attribute tld2.
Instance Method Summary collapse
-
#canonical ⇒ Object
The canonical host name is the simplified, DNS host name.
-
#dmarc ⇒ Object
Returns a hash of the domain’s DMARC (en.wikipedia.org/wiki/DMARC) settings.
-
#dns_a_record ⇒ Object
Returns: [official_hostname, alias_hostnames, address_family, *address_list].
-
#dns_enabled? ⇒ Boolean
True if the :dns_lookup setting is enabled.
-
#domain_matches?(rule) ⇒ Boolean
Does domain == rule or glob matches? (also tests the DNS (punycode) name) Requires optionally starts with a “@”.
-
#error ⇒ Object
The inverse of valid? – Returns nil (falsey) if valid, otherwise error message.
-
#exchangers ⇒ Object
Returns an array of EmailAddress::Exchanger hosts configured in DNS.
-
#find_provider ⇒ Object
:nodoc:.
-
#fqdn? ⇒ Boolean
Is this a fully-qualified domain name?.
-
#hosted_service? ⇒ Boolean
True if host is hosted at the provider, not a public provider host name.
-
#initialize(host_name, config = {}) ⇒ Host
constructor
host name - * host type - :email for an email host, :mx for exchanger host.
- #ip? ⇒ Boolean
-
#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”).
- #ipv4? ⇒ Boolean
- #ipv6? ⇒ Boolean
-
#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.
-
#munge ⇒ Object
Returns the munged version of the name, replacing everything after the initial two characters with “*****” or the configured “munge_string”.
-
#name ⇒ Object
(also: #to_s)
Returns the String representation of the host name (or IP).
-
#parse(host) ⇒ Object
:nodoc:.
-
#parse_comment(host) ⇒ Object
:nodoc:.
-
#parts ⇒ Object
Returns a hash of the parts of the host name after parsing.
- #provider_matches?(rule) ⇒ Boolean
-
#registration_name_matches?(rule) ⇒ Boolean
Does “example.” match any tld?.
- #set_error(err) ⇒ Object
-
#set_provider(name, provider_config = {}) ⇒ Object
:nodoc:.
-
#tld_matches?(rule) ⇒ Boolean
Does “sub.example.com” match “.com” and “.example.com” top level names? Matches TLD (uk) or TLD2 (co.uk).
-
#txt(alternate_host = nil) ⇒ Object
Returns a DNS TXT Record.
-
#txt_hash(alternate_host = nil) ⇒ Object
Parses TXT record pairs into a hash.
-
#valid?(rule = @config[:dns_lookup]||:mx) ⇒ Boolean
Returns true if the host name is valid according to the current configuration.
-
#valid_dns? ⇒ Boolean
True if the host name has a DNS A Record.
-
#valid_format? ⇒ Boolean
True if the host_name passes Regular Expression match and size limits.
-
#valid_ip? ⇒ Boolean
Returns true if the IP address given in that form of the host name is a potentially valid IP address.
-
#valid_mx? ⇒ Boolean
True if the host name has valid MX servers configured in DNS.
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
#comment ⇒ Object
Returns the value of attribute comment.
34 35 36 |
# File 'lib/email_address/host.rb', line 34 def comment @comment end |
#config ⇒ Object
Returns the value of attribute config.
34 35 36 |
# File 'lib/email_address/host.rb', line 34 def config @config end |
#dns_name ⇒ Object
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_name ⇒ Object
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_message ⇒ Object
Returns the value of attribute error_message.
34 35 36 |
# File 'lib/email_address/host.rb', line 34 def @error_message end |
#host_name ⇒ Object
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_address ⇒ Object
Returns the value of attribute ip_address.
34 35 36 |
# File 'lib/email_address/host.rb', line 34 def ip_address @ip_address end |
#provider ⇒ Object
Returns the value of attribute provider.
34 35 36 |
# File 'lib/email_address/host.rb', line 34 def provider @provider end |
#registration_name ⇒ Object
Returns the value of attribute registration_name.
34 35 36 |
# File 'lib/email_address/host.rb', line 34 def registration_name @registration_name end |
#subdomains ⇒ Object
Returns the value of attribute subdomains.
34 35 36 |
# File 'lib/email_address/host.rb', line 34 def subdomains @subdomains end |
#tld ⇒ Object
Returns the value of attribute tld.
34 35 36 |
# File 'lib/email_address/host.rb', line 34 def tld @tld end |
#tld2 ⇒ Object
Returns the value of attribute tld2.
34 35 36 |
# File 'lib/email_address/host.rb', line 34 def tld2 @tld2 end |
Instance Method Details
#canonical ⇒ Object
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 |
#dmarc ⇒ Object
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_record ⇒ Object
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
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 “@”.
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 |
#error ⇒ Object
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 |
#exchangers ⇒ Object
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_provider ⇒ Object
: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?
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
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
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”)
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
235 236 237 |
# File 'lib/email_address/host.rb', line 235 def ipv4? self.ip? && self.ip_address.include?(".") end |
#ipv6? ⇒ 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
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 |
#munge ⇒ Object
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 |
#name ⇒ Object 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 |
#parts ⇒ Object
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
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?
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..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)
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
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. = 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
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.
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.
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
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 |