email_address 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -42,6 +42,10 @@ module EmailAddress
42
42
  @providers
43
43
  end
44
44
 
45
+ #def provider(name, defn={})
46
+ # EmailAddress::Config.providers[name] = defn
47
+ #end
48
+
45
49
  def self.options
46
50
  @options
47
51
  end
@@ -1,11 +1,14 @@
1
1
  module EmailAddress
2
2
  ##############################################################################
3
+ #
4
+ # DEPRECATING... See EmailAddress::Matcher now
5
+ #
3
6
  # DomainMatcher - Matches a domain to a set of patterns
4
- #
7
+ #
5
8
  # Match Patterns
6
9
  # hostname sub.domain.tld
7
10
  # domain domain.tld
8
- # registration domain
11
+ # registration domain
9
12
  # tld .tld, .domain.tld
10
13
  ##############################################################################
11
14
  class DomainMatcher
@@ -38,7 +41,7 @@ module EmailAddress
38
41
  def rule_matches?(rule)
39
42
  rule.downcase!
40
43
  @host_name == rule || registration_name_matches?(rule) ||
41
- domain_matches?(rule) || tld_matches?(rule)
44
+ domain_matches?(rule) || tld_matches?(rule) || glob_matches?(rule)
42
45
  end
43
46
 
44
47
  def list_matches?(list)
@@ -46,6 +49,11 @@ module EmailAddress
46
49
  false
47
50
  end
48
51
 
52
+ # Matches a rule as a glob match, with * and ? characters
53
+ def glob_matches?(rule)
54
+ File.fnmatch?(rule, @host_name)
55
+ end
56
+
49
57
  # Does "sub.example.com" match "example" registration name
50
58
  def registration_name_matches?(rule)
51
59
  rule.match(/\A(\w+)\z/) && @host.registration_name == rule.downcase ? true : false
@@ -58,7 +66,7 @@ module EmailAddress
58
66
 
59
67
  # Does "sub.example.com" match ".com" and ".example.com" top level names?
60
68
  def tld_matches?(rule)
61
- rule.match(/\A\..+\z/) &&
69
+ rule.match(/\A\..+\z/) &&
62
70
  ( @host_name[-rule.size, rule.size] == rule.downcase || ".#{@host_name}" == rule) \
63
71
  ? true : false
64
72
  end
@@ -60,9 +60,7 @@ module EmailAddress
60
60
  # Returns provider based on configured domain name matches, or nil if unmatched
61
61
  # For best results, # consider the Exchanger.provider result as well.
62
62
  def provider
63
- base = EmailAddress::Config.providers[:default]
64
63
  EmailAddress::Config.providers.each do |name, defn|
65
- defn = base.merge(defn)
66
64
  return name if EmailAddress::DomainMatcher.matches?(@host_name, defn[:domains])
67
65
  end
68
66
  nil
@@ -0,0 +1,62 @@
1
+ ################################################################################
2
+ # ActiveRecord v5.0 Custom Type
3
+ # This class is not automatically loaded by the gem.
4
+ #-------------------------------------------------------------------------------
5
+ # 1) Register this type
6
+ #
7
+ # # config/initializers.types.rb
8
+ # require "email_address/email_address_type"
9
+ # ActiveRecord::Type.register(:email_address, EmailAddress::Address)
10
+ # ActiveRecord::Type.register(:canonical_email_address,
11
+ # EmailAddress::CanonicalEmailAddressType)
12
+ #
13
+ # 2) Define your email address columns in your model class
14
+ #
15
+ # class User < ActiveRecord::Base
16
+ # attribute :email, :email_address
17
+ # attribute :unique_email, :canonical_email_address
18
+ # end
19
+ #
20
+ # 3) Profit!
21
+ #
22
+ # user = User.new(email:"Pat.Smith+registrations@gmail.com",
23
+ # unique_email:"Pat.Smith+registrations@gmail.com")
24
+ # user.email #=> "pat.smith+registrations@gmail.com"
25
+ # user.unique_email #=> "patsmith@gmail.com"
26
+ ################################################################################
27
+
28
+ class EmailAddress::EmailAddressType < ActiveRecord::Type::Value
29
+
30
+ # From user input, setter
31
+ def cast(value)
32
+ super(EmailAddress.normal(value))
33
+ end
34
+
35
+ # From a database value
36
+ def deserialize(value)
37
+ EmailAddress.normal(value)
38
+ end
39
+ #
40
+ # To a database value (string)
41
+ def serialize(value)
42
+ EmailAddress.normal(value)
43
+ end
44
+ end
45
+
46
+ class CanonicalEmailAddressType < EmailAddress::EmailAddressType
47
+
48
+ # From user input, setter
49
+ def cast(value)
50
+ super(EmailAddress.canonical(value))
51
+ end
52
+
53
+ # From a database value
54
+ def deserialize(value)
55
+ EmailAddress.canonical(value)
56
+ end
57
+
58
+ # To a database value (string)
59
+ def serialize(value)
60
+ EmailAddress.canonical(value)
61
+ end
62
+ end
@@ -6,6 +6,20 @@ module EmailAddress
6
6
  class Exchanger
7
7
  include Enumerable
8
8
 
9
+ def self.cached(host)
10
+ @host_cache ||= {}
11
+ @cache_size ||= ENV['EMAIL_ADDRESS_CACHE_SIZE'].to_i || 100
12
+ if @host_cache.has_key?(host)
13
+ o = @host_cache.delete(host)
14
+ @host_cache[host] = o # LRU cache, move to end
15
+ elsif @host_cache.size >= @cache_size
16
+ @host_cache.delete(@host_cache.keys.first)
17
+ @host_cache[host] = new(host)
18
+ else
19
+ @host_cache[host] = new(host)
20
+ end
21
+ end
22
+
9
23
  def initialize(host, options={})
10
24
  @host = host
11
25
  @options = options
@@ -46,6 +60,8 @@ module EmailAddress
46
60
  ress = dns.getresources(@host, Resolv::DNS::Resource::IN::MX)
47
61
  ress.map { |r| [r.exchange.to_s, IPSocket::getaddress(r.exchange.to_s), r.preference] }
48
62
  end
63
+ rescue SocketError # not found, but could also mean network not work
64
+ @_dns_a_record ||= []
49
65
  end
50
66
 
51
67
  # Returns Array of domain names for the MX'ers, used to determine the Provider
@@ -55,13 +71,29 @@ module EmailAddress
55
71
 
56
72
  # Returns an array of MX IP address (String) for the given email domain
57
73
  def mx_ips
58
- mxers(domain).map {|m| m[1] }
74
+ mxers.map {|m| m[1] }
59
75
  end
60
76
 
61
77
  # Given a cidr (ip/bits) and ip address, returns true on match. Caches cidr object.
62
78
  def in_cidr?(cidr)
63
- @cidr ||= NetAddr::CIDR.create(cidr)
64
- mx_ips.first { |ip| @cider.matches?(ip) } ? true : false
79
+ if cidr.include?(":")
80
+ in_ipv6_cidr?(cidr)
81
+ else
82
+ in_ipv4_cidr?(cidr)
83
+ end
65
84
  end
85
+
86
+ private
87
+
88
+ def in_ipv4_cidr?(cidr)
89
+ cidr = NetAddr::CIDR.create(cidr)
90
+ mx_ips.find { |ip| !ip.include?(":") && cidr.matches?(ip) } ? true : false
91
+ end
92
+
93
+ def in_ipv6_cidr?(cidr)
94
+ cidr = NetAddr::CIDR.create(cidr)
95
+ mx_ips.find { |ip| ip.include?(":") && cidr.matches?(ip) } ? true : false
96
+ end
97
+
66
98
  end
67
99
  end
@@ -22,10 +22,11 @@ module EmailAddress
22
22
  # host name -
23
23
  # * full domain name after @ for email types
24
24
  # * fully-qualified domain name
25
- # host type -
25
+ # host type -
26
26
  # :email - email address domain
27
27
  # :mx - email exchanger domain
28
28
  def initialize(host_name, host_type=:email)
29
+ host_name||= ''
29
30
  @host_name = host_name.downcase
30
31
  @host_type = host_type
31
32
  parse_host(@host_name)
@@ -34,7 +35,8 @@ module EmailAddress
34
35
  def to_s
35
36
  @host_name
36
37
  end
37
-
38
+ alias :name :to_s
39
+
38
40
  def parse_host(host)
39
41
  @parser = EmailAddress::DomainParser.new(host)
40
42
  @parts = @parser.parts
@@ -57,7 +59,7 @@ module EmailAddress
57
59
 
58
60
  def exchanger
59
61
  return nil unless @host_type == :email
60
- @exchanger = EmailAddress::Exchanger.new(@host_name)
62
+ @exchanger = EmailAddress::Exchanger.cached(self.dns_host_name)
61
63
  end
62
64
 
63
65
  def provider
@@ -67,10 +69,35 @@ module EmailAddress
67
69
  end
68
70
  @provider ||= :unknown
69
71
  end
70
-
72
+
71
73
  def matches?(*names)
72
74
  DomainMatcher.matches?(@host_name, names.flatten)
73
75
  end
74
76
 
77
+ def txt(alternate_host=nil)
78
+ Resolv::DNS.open do |dns|
79
+ records = dns.getresources(alternate_host || self.dns_host_name,
80
+ Resolv::DNS::Resource::IN::TXT)
81
+ records.empty? ? nil : records.map(&:data).join(" ")
82
+ end
83
+ end
84
+
85
+ # Parses TXT record pairs into a hash
86
+ def txt_hash(alternate_host=nil)
87
+ fields = {}
88
+ record = self.txt(alternate_host)
89
+ return fields unless record
90
+
91
+ record.split(/\s*;\s*/).each do |pair|
92
+ (n,v) = pair.split(/\s*=\s*/)
93
+ fields[n.to_sym] = v
94
+ end
95
+ fields
96
+ end
97
+
98
+ def dmarc
99
+ self.txt_hash("_dmarc." + self.dns_host_name)
100
+ end
101
+
75
102
  end
76
103
  end
@@ -2,7 +2,7 @@ module EmailAddress
2
2
  ##############################################################################
3
3
  # EmailAddress Local part consists of
4
4
  # - comments
5
- # - mailbox
5
+ # - mailbox
6
6
  # - tag
7
7
  #-----------------------------------------------------------------------------
8
8
  # Parsing id provider-dependent, but RFC allows:
@@ -12,7 +12,7 @@ module EmailAddress
12
12
  # Quote local part or dot-separated sub-parts x."y".z
13
13
  # (comment)mailbox | mailbox(comment)
14
14
  # 8-bit/UTF-8: allowed but mail-system defined
15
- # RFC 5321 also warns that "a host that expects to receive mail SHOULD avoid
15
+ # RFC 5321 also warns that "a host that expects to receive mail SHOULD avoid
16
16
  # defining mailboxes where the Local-part requires (or uses) the Quoted-string form".
17
17
  # Postmaster: must always be case-insensitive
18
18
  # Case: sensitive, but usually treated as equivalent
@@ -24,12 +24,13 @@ module EmailAddress
24
24
  ROLE_NAMES = %w(info marketing sales support abuse noc security postmaster
25
25
  hostmaster usenet news webmaster www uucp ftp)
26
26
 
27
- def initialize(local, provider=nil)
28
- @provider = EmailAddress::Config.provider(provider || :default)
27
+ def initialize(local, host=nil)
28
+ @provider = EmailAddress::Config.provider(host ? host.provider : :default)
29
29
  parse(local)
30
30
  end
31
31
 
32
32
  def parse(local)
33
+ local ||= ''
33
34
  @local = local =~ /\A"(.)"\z/ ? $1 : local
34
35
  @local.gsub!(/\\(.)/, '\1') # Unescape
35
36
  @local.downcase! unless @provider[:case_sensitive]
@@ -70,7 +71,7 @@ module EmailAddress
70
71
 
71
72
  def format(m)
72
73
  m = m.gsub(/([\\\"])/, '\\\1') # Escape \ and "
73
- if m =~ /[ \"\(\),:'<>@\[\\\]]/ # Space and "(),:;<>@[\]
74
+ if m =~ /[ \"\(\),:<>@\[\\\]]/ # Space and "(),:;<>@[\]
74
75
  m = %Q("#{m}")
75
76
  end
76
77
  m
@@ -97,5 +98,10 @@ module EmailAddress
97
98
  def role?
98
99
  ROLE_NAMES.include?(@mailbox)
99
100
  end
101
+
102
+ # Mailbox with trailing numbers removed
103
+ def root_name
104
+ canonical =~ /\A(.+?)\d+\z/ ? $1 : canonical
105
+ end
100
106
  end
101
107
  end
@@ -0,0 +1,119 @@
1
+ module EmailAddress
2
+ ##############################################################################
3
+ # Matcher - Allows matching of an email address against a list of matching
4
+ # tokens.
5
+ #
6
+ # Match Patterns
7
+ # * Top-Level-Domain: .org
8
+ # * Domain Name: example.com
9
+ # * Registration Name: hotmail. (matches any TLD)
10
+ # * Domain Glob: *.exampl?.com
11
+ # * Provider Name: google
12
+ # * Mailbox Name or Glob: user00*@
13
+ # * Address or Glob: postmaster@domain*.com
14
+ # * Provider or Registration: msn (?? Possible combo for either match?)
15
+ #
16
+ # Usage:
17
+ # m = EmailAddress::Matcher.new(".org example.com hotmail. google user*@ root@*.com")
18
+ # m.include?("pat@example.com")
19
+ ##############################################################################
20
+
21
+ class Matcher
22
+ attr_reader :rules, :email
23
+
24
+ def self.matches?(rule, email)
25
+ EmailAddress::Matcher.new(rule).matches?(email)
26
+ end
27
+
28
+ def initialize(rules=[], empty_rules_return=false)
29
+ self.rules = rules
30
+ @empty_rules_return = empty_rules_return
31
+ end
32
+
33
+ def rules=(r)
34
+ @rules = r.is_a?(Array) ? r : r.split(/\s+/)
35
+ @rules = @rules.map(&:downcase)
36
+ end
37
+
38
+ def email=(e)
39
+ e = EmailAddress.new(e) if e.is_a?(String)
40
+ if e.is_a?(EmailAddress::Address)
41
+ @email_address = e
42
+ @email = e.normalize
43
+ @mailbox = e.mailbox
44
+ @domain = e.host.name
45
+ @domain_parts = e.host.parts
46
+ @provider = e.provider
47
+ elsif e.is_a?(Hash)
48
+ @email = e[:email]
49
+ @mailbox = e[:mailbox]
50
+ @domain = e[:domain]
51
+ @domain_parts = EmailAddress::DomainParser.new(@domain).parts
52
+ @provider = e[:provider]
53
+ end
54
+ end
55
+
56
+ # Takes a email address string, returns true if it matches a rule
57
+ def include?(email_address)
58
+ self.email = email_address
59
+ return @empty_rules_return if @rules.empty?
60
+ @rules.each do |rule|
61
+ return true if registration_name_matches?(rule)
62
+ return true if tld_matches?(rule)
63
+ return true if provider_matches?(rule)
64
+ return true if domain_matches?(rule)
65
+ return true if email_matches?(rule)
66
+ return true if @email_address && ip_cidr_matches?(rule)
67
+ end
68
+ false
69
+ end
70
+
71
+ # Does "example." match any tld?
72
+ def registration_name_matches?(rule)
73
+ @domain_parts[:registration_name]+'.' == rule ? true : false
74
+ end
75
+
76
+ # Does "sub.example.com" match ".com" and ".example.com" top level names?
77
+ def tld_matches?(rule)
78
+ rule.match(/\A\..+\z/) &&
79
+ ( @domain[-rule.size, rule.size] == rule || ".#{@domain}" == rule) \
80
+ ? true : false
81
+ end
82
+
83
+ def provider_matches?(rule)
84
+ rule =~ /\A[\w\-]*\z/ && self.provider == rule.to_sym
85
+ end
86
+
87
+ def provider
88
+ @provider ||= EmailAddress::Config.providers.each do |prov, defn|
89
+ if defn.has_key?(:domains) && !defn[:domains].empty?
90
+ defn[:domains].each do |d|
91
+ if domain_matches?(d) || registration_name_matches?(d)
92
+ return @provider = prov
93
+ end
94
+ end
95
+ end
96
+ end
97
+ @provider ||= :unknown
98
+ end
99
+
100
+ # Does domain == rule or glob matches?
101
+ def domain_matches?(rule, domain=@domain)
102
+ return false if rule.include?("@")
103
+ domain == rule || File.fnmatch?(rule, domain)
104
+ end
105
+
106
+ # Does "root@*.com" match "root@example.com" domain name
107
+ def email_matches?(rule)
108
+ return false unless rule.include?("@")
109
+ @email == rule || File.fnmatch?(rule, @email)
110
+ end
111
+
112
+ # Does an IP of mail exchanger for "sub.example.com" match "xxx.xx.xx.xx/xx"?
113
+ def ip_cidr_matches?(rule)
114
+ return false unless rule.match(/\A\d.+\/\d+\z/) && @email_address.host.exchanger
115
+ @email_address.host.exchanger.in_cidr?(rule) ? true : false
116
+ end
117
+
118
+ end
119
+ end
@@ -2,6 +2,8 @@ module EmailAddress
2
2
  class Validator
3
3
  LEGIBLE_LOCAL_REGEX = /\A[a-z0-9]+(([\.\-\_\'\+][a-z0-9]+)+)?\z/
4
4
  DOT_ATOM_REGEX = /[\w\!\#\$\%\&\'\*\+\-\/\=\?\^\`\{\|\}\~]/
5
+ ERRORS = %i(bad_syntax bad_email bad_mailbox bad_domain bad_exchanger bad_recipient)
6
+ attr_reader :error
5
7
 
6
8
  def self.validate(address, options={})
7
9
  EmailAddress::Validator.new(address, options).valid?
@@ -13,25 +15,29 @@ module EmailAddress
13
15
  @host = address.host
14
16
  @options = options
15
17
  @rules = EmailAddress::Config.provider(@host.provider)
16
- @errors = []
18
+ @error = nil
17
19
  end
18
20
 
21
+ # Returns true if email address seems valid, false otherwise.
22
+ # For error, call #error method to get error code (symbol).
19
23
  def valid?
20
24
  return false unless valid_sizes?
21
25
  if @rules[:valid_mailbox] && ! @rules[:valid_mailbox].call(@local.to_s)
22
26
  #p ["VALIDATOR", @local.to_s, @rules[:valid_mailbox]]
23
- return invalid(:mailbox_validator)
27
+ return invalid(:bad_mailbox)
24
28
  else
25
29
  return false unless valid_local?
26
30
  end
27
- return invalid(:mx) unless valid_mx? || (valid_dns? && @options[:allow_dns_a])
31
+ if EmailAddress::Config.options[:check_dns]
32
+ return invalid(:bad_exchanger) unless valid_mx? || (valid_dns? && @options[:allow_dns_a])
33
+ end
28
34
  true
29
35
  end
30
36
 
31
37
  def mailbox_validator(v)
32
38
  return true unless v
33
39
  if v.is_a?(Proc)
34
- return invalid(:mailbox_proc) unless @rules[:valid_mailbox].call(@local)
40
+ return invalid(:bad_mailbox) unless @rules[:valid_mailbox].call(@local)
35
41
  elsif v == :legible
36
42
  return legible?
37
43
  elsif v == :rfc
@@ -56,19 +62,19 @@ module EmailAddress
56
62
  end
57
63
 
58
64
  def valid_sizes?
59
- return invalid(:address_size) unless @rules[:address_size].include?(@address.to_s.size)
60
- return invalid(:domain_size ) unless @rules[:domain_size ].include?(@host.to_s.size)
61
- return invalid(:local_size ) unless @rules[:local_size ].include?(@local.to_s.size)
62
- return invalid(:mailbox_size) unless @rules[:mailbox_size].include?(@local.mailbox.size)
65
+ return invalid(:bad_email) unless @rules[:address_size].include?(@address.to_s.size)
66
+ return invalid(:bad_mailbox) unless @rules[:local_size ].include?(@local.to_s.size)
67
+ return invalid(:bad_mailbox) unless @rules[:mailbox_size].include?(@local.mailbox.size)
68
+ return invalid(:bad_domain ) unless @rules[:domain_size ].include?(@host.to_s.size)
63
69
  true
64
70
  end
65
71
 
66
72
  def valid_local?
67
- return invalid(:mailbox) unless valid_local_part?(@local.mailbox)
68
- return invalid(:comment) unless @local.comment.empty? || valid_local_part?(@local.comment)
73
+ return invalid(:bad_mailbox) unless valid_local_part?(@local.mailbox)
74
+ return invalid(:bad_mailbox) unless @local.comment.empty? || valid_local_part?(@local.comment)
69
75
  if @local.tag
70
76
  @local.tag.split(@rules[:tag_separator]).each do |t|
71
- return invalid(:tag, t) unless valid_local_part?(t)
77
+ return invalid(:bad_mailbox, t) unless valid_local_part?(t)
72
78
  end
73
79
  end
74
80
  true
@@ -81,7 +87,7 @@ module EmailAddress
81
87
 
82
88
 
83
89
  def invalid(reason, *info)
84
- @errors << reason
90
+ @error = reason
85
91
  #p "INVALID ----> #{reason} for #{@local.to_s}@#{@host.to_s} #{info.inspect}"
86
92
  false
87
93
  end