email_address 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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