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.
-
#reason ⇒ Object
Returns the value of attribute reason.
-
#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.
-
#connect ⇒ Object
Connects to host to test it can receive email.
-
#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?.
- #fully_qualified_domain_name(host_part) ⇒ Object
-
#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
- #localhost? ⇒ 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, reason = nil) ⇒ 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?(rules = {}) ⇒ 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
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
#comment ⇒ Object
Returns the value of attribute comment.
36 37 38 |
# File 'lib/email_address/host.rb', line 36 def comment @comment end |
#config ⇒ Object
Returns the value of attribute config.
36 37 38 |
# File 'lib/email_address/host.rb', line 36 def config @config end |
#dns_name ⇒ Object
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_name ⇒ Object
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_message ⇒ Object
Returns the value of attribute error_message.
36 37 38 |
# File 'lib/email_address/host.rb', line 36 def @error_message end |
#host_name ⇒ Object
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_address ⇒ Object
Returns the value of attribute ip_address.
36 37 38 |
# File 'lib/email_address/host.rb', line 36 def ip_address @ip_address end |
#provider ⇒ Object
Returns the value of attribute provider.
36 37 38 |
# File 'lib/email_address/host.rb', line 36 def provider @provider end |
#reason ⇒ Object
Returns the value of attribute reason.
36 37 38 |
# File 'lib/email_address/host.rb', line 36 def reason @reason end |
#registration_name ⇒ Object
Returns the value of attribute registration_name.
36 37 38 |
# File 'lib/email_address/host.rb', line 36 def registration_name @registration_name end |
#subdomains ⇒ Object
Returns the value of attribute subdomains.
36 37 38 |
# File 'lib/email_address/host.rb', line 36 def subdomains @subdomains end |
#tld ⇒ Object
Returns the value of attribute tld.
36 37 38 |
# File 'lib/email_address/host.rb', line 36 def tld @tld end |
#tld2 ⇒ Object
Returns the value of attribute tld2.
36 37 38 |
# File 'lib/email_address/host.rb', line 36 def tld2 @tld2 end |
Instance Method Details
#canonical ⇒ Object
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 |
#connect ⇒ Object
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 |
#dmarc ⇒ Object
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_record ⇒ Object
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
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 “@”.
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 |
#error ⇒ Object
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 |
#exchangers ⇒ Object
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_provider ⇒ Object
: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?
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
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
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”)
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
256 257 258 |
# File 'lib/email_address/host.rb', line 256 def ipv4? self.ip? && self.ip_address.include?(".") end |
#ipv6? ⇒ Boolean
260 261 262 |
# File 'lib/email_address/host.rb', line 260 def ipv6? self.ip? && self.ip_address.include?(":") end |
#localhost? ⇒ 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
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 |
#munge ⇒ Object
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 |
#name ⇒ Object 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 |
#parts ⇒ Object
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
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?
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.(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)
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
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. = 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
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.
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.
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
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 |