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.
-
#locale ⇒ Object
Returns the value of attribute locale.
-
#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(timeout = nil) ⇒ 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 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_provider ⇒ Object
-
#hosted_service? ⇒ Boolean
True if host is hosted at the provider, not a public provider host name.
-
#initialize(host_name, config = {}, locale = "en") ⇒ 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
Parsing.
-
#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 = {}, locale = "en") ⇒ Host
host name -
* host type - :email for an email host, :mx for exchanger host
85 86 87 88 89 90 91 92 |
# 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 |
Instance Attribute Details
#comment ⇒ Object
Returns the value of attribute comment.
33 34 35 |
# File 'lib/email_address/host.rb', line 33 def comment @comment end |
#config ⇒ Object
Returns the value of attribute config.
33 34 35 |
# File 'lib/email_address/host.rb', line 33 def config @config end |
#dns_name ⇒ Object
Returns the value of attribute dns_name.
33 34 35 |
# File 'lib/email_address/host.rb', line 33 def dns_name @dns_name end |
#domain_name ⇒ Object
Returns the value of attribute domain_name.
33 34 35 |
# File 'lib/email_address/host.rb', line 33 def domain_name @domain_name end |
#error_message ⇒ Object
Returns the value of attribute error_message.
33 34 35 |
# File 'lib/email_address/host.rb', line 33 def @error_message end |
#host_name ⇒ Object
Returns the value of attribute host_name.
32 33 34 |
# File 'lib/email_address/host.rb', line 32 def host_name @host_name end |
#ip_address ⇒ Object
Returns the value of attribute ip_address.
33 34 35 |
# File 'lib/email_address/host.rb', line 33 def ip_address @ip_address end |
#locale ⇒ Object
Returns the value of attribute locale.
33 34 35 |
# File 'lib/email_address/host.rb', line 33 def locale @locale end |
#provider ⇒ Object
Returns the value of attribute provider.
33 34 35 |
# File 'lib/email_address/host.rb', line 33 def provider @provider end |
#reason ⇒ Object
Returns the value of attribute reason.
33 34 35 |
# File 'lib/email_address/host.rb', line 33 def reason @reason end |
#registration_name ⇒ Object
Returns the value of attribute registration_name.
33 34 35 |
# File 'lib/email_address/host.rb', line 33 def registration_name @registration_name end |
#subdomains ⇒ Object
Returns the value of attribute subdomains.
33 34 35 |
# File 'lib/email_address/host.rb', line 33 def subdomains @subdomains end |
#tld ⇒ Object
Returns the value of attribute tld.
33 34 35 |
# File 'lib/email_address/host.rb', line 33 def tld @tld end |
#tld2 ⇒ Object
Returns the value of attribute tld2.
33 34 35 |
# File 'lib/email_address/host.rb', line 33 def tld2 @tld2 end |
Instance Method Details
#canonical ⇒ Object
The canonical host name is the simplified, DNS host name
109 110 111 |
# File 'lib/email_address/host.rb', line 109 def canonical dns_name end |
#connect(timeout = nil) ⇒ 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.
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.
474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 |
# 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 ⇒ Object
Returns a hash of the domain’s DMARC (en.wikipedia.org/wiki/DMARC) settings.
373 374 375 |
# File 'lib/email_address/host.rb', line 373 def dmarc dns_name ? txt_hash("_dmarc." + dns_name) : {} end |
#dns_a_record ⇒ Object
Returns: [official_hostname, alias_hostnames, address_family, *address_list]
328 329 330 331 332 333 |
# 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? ⇒ Boolean
True if the :dns_lookup setting is enabled
321 322 323 324 325 |
# 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) ⇒ Boolean
Does domain == rule or glob matches? (also tests the DNS (punycode) name) Requires optionally starts with a “@”.
301 302 303 304 305 306 |
# 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 ⇒ Object
The inverse of valid? – Returns nil (falsey) if valid, otherwise error message
498 499 500 |
# File 'lib/email_address/host.rb', line 498 def error valid? ? nil : @error_message end |
#exchangers ⇒ Object
Returns an array of Exchanger hosts configured in DNS. The array will be empty if none are configured.
337 338 339 340 |
# 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 |
#find_provider ⇒ Object
:nodoc:
208 209 210 211 212 213 214 215 216 217 218 219 220 |
# File 'lib/email_address/host.rb', line 208 def find_provider # :nodoc: return provider if provider Config.providers.each do |provider, config| if config[:host_match] && matches?(config[:host_match]) return set_provider(provider, config) end end return set_provider(:default) unless dns_enabled? self.provider ||= set_provider(:default) end |
#fqdn? ⇒ Boolean
Is this a fully-qualified domain name?
243 244 245 |
# File 'lib/email_address/host.rb', line 243 def fqdn? tld ? true : false end |
#fully_qualified_domain_name(host_part) ⇒ Object
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 |
# 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 |
#hosted_provider ⇒ Object
234 235 236 |
# File 'lib/email_address/host.rb', line 234 def hosted_provider Exchanger.cached(dns_name).provider end |
#hosted_service? ⇒ Boolean
True if host is hosted at the provider, not a public provider host name
201 202 203 204 205 206 |
# 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? ⇒ Boolean
247 248 249 |
# File 'lib/email_address/host.rb', line 247 def ip? !!ip_address 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”)
310 311 312 313 314 |
# 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? ⇒ Boolean
251 252 253 |
# File 'lib/email_address/host.rb', line 251 def ipv4? ip? && ip_address.include?(".") end |
#ipv6? ⇒ Boolean
255 256 257 |
# File 'lib/email_address/host.rb', line 255 def ipv6? ip? && ip_address.include?(":") end |
#localhost? ⇒ Boolean
458 459 460 461 462 |
# 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) ⇒ 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
270 271 272 273 274 275 276 277 278 279 280 281 282 |
# 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 ⇒ Object
Returns the munged version of the name, replacing everything after the initial two characters with “*****” or the configured “munge_string”.
115 116 117 |
# File 'lib/email_address/host.rb', line 115 def munge 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)
95 96 97 98 99 100 101 102 103 104 105 |
# 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 |
#parse(host) ⇒ Object
Parsing
123 124 125 126 127 128 129 130 131 132 133 |
# File 'lib/email_address/host.rb', line 123 def parse(host) # :nodoc: host = 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.
228 229 230 231 232 |
# 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) ⇒ Boolean
295 296 297 |
# 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) ⇒ Boolean
Does “example.” match any tld?
285 286 287 |
# File 'lib/email_address/host.rb', line 285 def registration_name_matches?(rule) rule == "#{registration_name}." end |
#set_error(err, reason = nil) ⇒ Object
490 491 492 493 494 495 |
# File 'lib/email_address/host.rb', line 490 def set_error(err, reason = nil) @error = err @reason = reason @error_message = Config.(err, locale) false end |
#set_provider(name, provider_config = {}) ⇒ Object
:nodoc:
222 223 224 225 |
# File 'lib/email_address/host.rb', line 222 def set_provider(name, provider_config = {}) # :nodoc: config.configure(provider_config) @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)
291 292 293 |
# File 'lib/email_address/host.rb', line 291 def tld_matches?(rule) rule.match(/\A\.(.+)\z/) && ($1 == tld || $1 == tld2) # ? true : false end |
#txt(alternate_host = nil) ⇒ Object
Returns a DNS TXT Record
343 344 345 346 347 348 349 350 351 352 353 354 355 356 |
# 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) ⇒ Object
Parses TXT record pairs into a hash
359 360 361 362 363 364 365 366 367 368 369 |
# 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 = {}) ⇒ Boolean
Returns true if the host name is valid according to the current configuration
382 383 384 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 382 def valid?(rules = {}) host_validation = rules[:host_validation] || @config[:host_validation] || :mx dns_lookup = rules[:dns_lookup] || @config[:dns_lookup] || host_validation self. = 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 == :a || host_validation == :a valid_dns? elsif dns_lookup == :mx valid_mx? else true end end |
#valid_dns? ⇒ Boolean
True if the host name has a DNS A Record
405 406 407 408 |
# 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? ⇒ Boolean
True if the host_name passes Regular Expression match and size limits.
429 430 431 432 433 434 435 436 437 438 439 |
# 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? ⇒ 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.
444 445 446 447 448 449 450 451 452 453 454 455 456 |
# 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? ⇒ Boolean
True if the host name has valid MX servers configured in DNS
411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 |
# 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 |