# Contains the actual logic behind the plugin. class ValidatesEmailVeracityOf # Defines a server contains methods used to retrieve information from it. class Server attr_accessor :name def to_s #:nodoc: name end def initialize(name = '') self.name = name end end # Defines a domain and contains methods used to retrieve information from it such # as mail exchange and address server information. class Domain require 'resolv' require 'timeout' attr_accessor :name # Creates a new Domain object, optionally accepts a domain as an argument. # ==== Example # Domain.new('gmail.com').exchange_servers # => ["ms1.google.com", # "ms2.google.com", ...] def initialize(name = '') self.name = name end def to_s #:nodoc: name end # Returns an array of server objects for address server the domain's A record, if # the domain does not exist, it will return an empty array. If it times out, nil # is returned. # ==== Options # * *timeout* # - Sets a time (in seconds) that the method will time out and return nil. The # default is two. def address_servers(options = {}) servers_in :address, options end # Returns an array of server objects for each exchange server in the domain's MX # record, if the domain does not exist, it will return an empty array. If it times # out, nil is returned. # ==== Options # * *timeout* # - Sets a time (in seconds) that the method will time out and return nil. The # default is two. def exchange_servers(options = {}) servers_in :exchange, options end protected # Returns an array of server objects from the domain using the specified method. # If the domain does not exist an empty array is returned. If the process times # out, nil is returned. # ==== Arguments # * *record* # - Either :exchange to return mail exchange servers (MX) or # :address to return primary address servers (A) # ==== Options # * *timeout* # - Sets a time (in seconds) that the method will time out and return nil. The # default is two. def servers_in(record, options = {}) type = case record.to_s.downcase when 'exchange' : Resolv::DNS::Resource::IN::MX when 'address' : Resolv::DNS::Resource::IN::A end st = Timeout::timeout(options.fetch(:timeout, 2)) do Resolv::DNS.new.getresources(name, type).inject([]) do |servers, s| servers << Server.new(s.send(record).to_s) end end rescue Timeout::Error nil end end # Defines an email address and contains methods to perform things needed in order # to validate it. class EmailAddress attr_accessor :address # Creates a new EmailAddress object, optionally accepts an email address as an # argument. # ==== Example # EmailAddress.new('heycarsten@gmail.com').domain # => "gmail.com" def initialize(email = '') self.address = email end # Domains that we know have mail servers such as gmail.com, aol.com and # yahoo.com. def self.known_domains %w[ aol.com gmail.com hotmail.com mac.com msn.com rogers.com sympatico.ca yahoo.com ] end # Checks the email's domain against any invalid domains passed in the options # hash. This is useful when you don't want addresses from providers such as # dodgeit.com. # ==== Options # * *invalid_domains* # - An array of strings that indicate invalid domain names. # ==== Example # EmailAddress.new('carsten@dodgeit.com').domain_is_valid?(:invalid_domains => ['dodgeit.com']) # => false def domain_is_valid?(options = {}) configuration = { :invalid_domains => nil }.update(options) return true unless configuration[:invalid_domains] unless configuration[:invalid_domains].is_a?(Array) raise ArgumentError, 'invalid_domains must be an Array' end !configuration[:invalid_domains].include?(domain.name.downcase) end # Returns the domain portion of the email address. # ==== Example # EmailAddress.new('heycarsten@gmail.com').domain # => "gmail.com" def domain Domain.new((address.split('@')[1] || '').strip) end # Verifies the email address for well-formedness against a well-known pattern. # Note that it will not verifiy all RFC 2822 valid addresses. def pattern_is_valid? address =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i end # Checks if the email address' domain has any servers in it's mail exchange (MX) # or address (A) records. If it does then true is returned, otherwise false is # returned. If the lookup times out, it will return false (or nil if the # :fail_on_timeout option is specified.) Additionally the secondary (A record) # lookup can be turned off (if your really picky) by passing in the option # :mx_only => true. # ==== Options # * *mx_only* # - The domain is only checked for the presence of mail exchange servers, the # address record is ignored. # * *timeout* # - Time (in seconds) before the domain lookup is skipped. Default is two. # * *fail_on_timeout* # - Causes validation to fail if a timeout occurs. def domain_has_servers?(options = {}) return true if EmailAddress.known_domains.include?(domain.name.downcase) servers = [] servers << domain.exchange_servers(options) servers << domain.address_servers(options) if !options[:mx_only] servers.flatten! if (servers.size - servers.nitems) > 0 options.fetch(:fail_on_timeout, true) ? nil : true else !servers.empty? end end end end