email_address 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,46 @@
1
+ ################################################################################
2
+ # ActiveRecord v5.0 Custom Type
3
+ #
4
+ # 1) Register your types
5
+ #
6
+ # # config/initializers/email_address.rb
7
+ # ActiveRecord::Type.register(:email_address, EmailAddress::Address)
8
+ # ActiveRecord::Type.register(:canonical_email_address,
9
+ # EmailAddress::CanonicalEmailAddressType)
10
+ #
11
+ # 2) Define your email address columns in your model class
12
+ #
13
+ # class User < ApplicationRecord
14
+ # attribute :email, :email_address
15
+ # attribute :canonical_email, :canonical_email_address
16
+ #
17
+ # def email=(email_address)
18
+ # self[:canonical_email] = email_address
19
+ # self[:email] = email_address
20
+ # end
21
+ # end
22
+ #
23
+ # 3) Profit!
24
+ #
25
+ # user = User.new(email:"Pat.Smith+registrations@gmail.com")
26
+ # user.email #=> "pat.smith+registrations@gmail.com"
27
+ # user.canonical_email #=> "patsmith@gmail.com"
28
+ ################################################################################
29
+
30
+ class EmailAddress::CanonicalEmailAddressType < ActiveRecord::Type::Value
31
+
32
+ # From user input, setter
33
+ def cast(value)
34
+ super(EmailAddress.canonical(value))
35
+ end
36
+
37
+ # From a database value
38
+ def deserialize(value)
39
+ value && EmailAddress.normal(value)
40
+ end
41
+
42
+ # To a database value (string)
43
+ def serialize(value)
44
+ value && EmailAddress.normal(value)
45
+ end
46
+ end
@@ -1,83 +1,167 @@
1
1
  module EmailAddress
2
+ # Global configurations and for default/unknown providers. Settings are:
3
+ #
4
+ # * dns_lookup: :mx, :a, :off
5
+ # Enables DNS lookup for validation by
6
+ # :mx - DNS MX Record lookup
7
+ # :a - DNS A Record lookup (as some domains don't specify an MX incorrectly)
8
+ # :off - Do not perform DNS lookup (Test mode, network unavailable)
9
+ #
10
+ # * sha1_secret ""
11
+ # This application-level secret is appended to the email_address to compute
12
+ # the SHA1 Digest, making it unique to your application so it can't easily be
13
+ # discovered by comparing against a known list of email/sha1 pairs.
14
+ #
15
+ # For local part configuration:
16
+ # * local_downcase: true
17
+ # Downcase the local part. You probably want this for uniqueness.
18
+ # RFC says local part is case insensitive, that's a bad part.
19
+ #
20
+ # * local_fix: true,
21
+ # Make simple fixes when available, remove spaces, condense multiple punctuations
22
+ #
23
+ # * local_encoding: :ascii, :unicode,
24
+ # Enable Unicode in local part. Most mail systems do not yet support this.
25
+ # You probably want to stay with ASCII for now.
26
+ #
27
+ # * local_parse: nil, ->(local) { [mailbox, tag, comment] }
28
+ # Specify an optional lambda/Proc to parse the local part. It should return an
29
+ # array (tuple) of mailbox, tag, and comment.
30
+ #
31
+ # * local_format: :conventional, :relaxed, :redacted, :standard, Proc
32
+ # :conventional word ( puncuation{1} word )*
33
+ # :relaxed alphanum ( allowed_characters)* alphanum
34
+ # :standard RFC Compliant email addresses (anything goes!)
35
+ #
36
+ # * local_size: 1..64,
37
+ # A Range specifying the allowed size for mailbox + tags + comment
38
+ #
39
+ # * tag_separator: nil, character (+)
40
+ # Nil, or a character used to split the tag from the mailbox
41
+ #
42
+ # For the mailbox (AKA account, role), without the tag
43
+ # * mailbox_size: 1..64
44
+ # A Range specifying the allowed size for mailbox
45
+ #
46
+ # * mailbox_canonical: nil, ->(mailbox) { mailbox }
47
+ # An optional lambda/Proc taking a mailbox name, returning a canonical
48
+ # version of it. (E.G.: gmail removes '.' characters)
49
+ #
50
+ # * mailbox_validator: nil, ->(mailbox) { true }
51
+ # An optional lambda/Proc taking a mailbox name, returning true or false.
52
+ #
53
+ # * host_encoding: :punycode, :unicode,
54
+ # How to treat International Domain Names (IDN). Note that most mail and
55
+ # DNS systems do not support unicode, so punycode needs to be passed.
56
+ # :punycode Convert Unicode names to punycode representation
57
+ # :unicode Keep Unicode names as is.
58
+ #
59
+ # * host_validation:
60
+ # :mx Ensure host is configured with DNS MX records
61
+ # :a Ensure host is known to DNS (A Record)
62
+ # :syntax Validate by syntax only, no Network verification
63
+ # :connect Attempt host connection (not implemented, BAD!)
64
+ #
65
+ # * host_size: 1..253,
66
+ # A range specifying the size limit of the host part,
67
+ #
68
+ # * host_allow_ip: false,
69
+ # Allow IP address format in host: [127.0.0.1], [IPv6:::1]
70
+ #
71
+ # * address_validation: :parts, :smtp, ->(address) { true }
72
+ # Address validation policy
73
+ # :parts Validate local and host.
74
+ # :smtp Validate via SMTP (not implemented, BAD!)
75
+ # A lambda/Proc taking the address string, returning true or false
76
+ #
77
+ # * address_size: 3..254,
78
+ # A range specifying the size limit of the complete address
79
+ #
80
+ # * address_local: false,
81
+ # Allow localhost, no domain, or local subdomains.
82
+ #
83
+ # For provider rules to match to domain names and Exchanger hosts
84
+ # The value is an array of match tokens.
85
+ # * host_match: %w(.org example.com hotmail. user*@ sub.*.com)
86
+ # * exchanger_match: %w(google.com 127.0.0.1 10.9.8.0/24 ::1/64)
87
+
2
88
  class Config
3
- @options = {
4
- downcase_mailboxes: true,
5
- check_dns: true,
89
+ @config = {
90
+ dns_lookup: :mx, # :mx, :a, :off
91
+ sha1_secret: "",
92
+ munge_string: "*****",
93
+
94
+ local_downcase: true,
95
+ local_fix: true,
96
+ local_encoding: :ascii, # :ascii, :unicode,
97
+ local_parse: nil, # nil, Proc
98
+ local_format: :conventional, # :conventional, :relaxed, :redacted, :standard, Proc
99
+ local_size: 1..64,
100
+ tag_separator: '+', # nil, character
101
+ mailbox_size: 1..64, # without tag
102
+ mailbox_canonical: nil, # nil, Proc
103
+ mailbox_validator: nil, # nil, Proc
104
+
105
+ host_encoding: :punycode || :unicode,
106
+ host_validation: :mx || :a || :connect,
107
+ host_size: 1..253,
108
+ host_allow_ip: false,
109
+
110
+ address_validation: :parts, # :parts, :smtp, Proc
111
+ address_size: 3..254,
112
+ address_localhost: false,
6
113
  }
7
114
 
8
115
  @providers = {
9
- default: {
10
- domains: [],
11
- exchangers: [],
12
- tag_separator: '+',
13
- case_sensitive: false,
14
- address_size: 3..254,
15
- local_size: 1..64,
16
- domain_size: 1..253,
17
- mailbox_size: 1..64,
18
- mailbox_unicode: false,
19
- canonical_mailbox: ->(m) {m},
20
- valid_mailbox: nil, # :legible, :rfc, ->(m) {true}
21
- },
22
- aol: {
23
- registration_names: %w(aol compuserve netscape aim cs)
24
- },
25
- google: {
26
- domains: %w(gmail.com googlemail.com),
27
- exchangers: %w(google.com),
28
- local_size: 5..64,
29
- canonical_mailbox: ->(m) {m.gsub('.','')},
30
- #valid_mailbox: ->(local) { local.mailbox =~ /\A[a-z0-9][\.a-z0-9]{5,29}\z/i},
31
- },
32
- msn: {
33
- valid_mailbox: ->(m) { m =~ /\A[a-z0-9][\.\-a-z0-9]{5,29}\z/i},
34
- },
35
- yahoo: {
36
- domains: %w(yahoo ymail rocketmail),
37
- exchangers: %w(yahoodns yahoo-inc),
38
- },
116
+ aol: {
117
+ host_match: %w(aol. compuserve. netscape. aim. cs.),
118
+ },
119
+ google: {
120
+ host_match: %w(gmail.com googlemail.com),
121
+ exchanger_match: %w(google.com),
122
+ local_size: 5..64,
123
+ mailbox_canonical: ->(m) {m.gsub('.','')},
124
+ },
125
+ msn: {
126
+ host_match: %w(msn. hotmail. outlook. live.),
127
+ mailbox_validator: ->(m,t) { m =~ /\A[a-z0-9][\.\-a-z0-9]{5,29}\z/i},
128
+ },
129
+ yahoo: {
130
+ host_match: %w(yahoo. ymail. rocketmail.),
131
+ exchanger_match: %w(yahoodns yahoo-inc),
132
+ },
39
133
  }
40
134
 
41
- def self.providers
42
- @providers
135
+ # Set multiple default configuration settings
136
+ def self.configure(config={})
137
+ @config.merge!(config)
43
138
  end
44
139
 
45
- #def provider(name, defn={})
46
- # EmailAddress::Config.providers[name] = defn
47
- #end
48
-
49
- def self.options
50
- @options
140
+ def self.setting(name, *value)
141
+ name = name.to_sym
142
+ @config[name] = value.first if value.size > 0
143
+ @config[name]
51
144
  end
52
145
 
53
- def self.provider(name)
54
- @providers[:default].merge(@providers.fetch(name) { Hash.new })
146
+ # Returns the hash of Provider rules
147
+ def self.providers
148
+ @providers
55
149
  end
56
150
 
57
- class Setup
58
- attr_reader :providers
59
-
60
- def initialize
61
- @providers = {}
62
- end
63
-
64
- def do_block(&block)
65
- instance_eval(&block)
66
- end
67
-
68
- def provider(name, defn={})
69
- EmailAddress::Config.providers[name] = defn
70
- end
71
-
72
- def option(name, value)
73
- EmailAddress::Config.options[name.to_sym] = value
151
+ # Configure or lookup a provider by name.
152
+ def self.provider(name, config={})
153
+ name = name.to_sym
154
+ if config.size > 0
155
+ @providers[name] ||= @config.clone
156
+ @providers[name].merge!(config)
74
157
  end
158
+ @providers[name]
75
159
  end
76
160
 
77
- def self.setup(&block)
78
- @setup ||= Setup.new
79
- @setup.do_block(&block) if block_given?
80
- @setup
161
+ def self.all_settings(*configs)
162
+ config = @config.clone
163
+ configs.each {|c| config.merge!(c) }
164
+ config
81
165
  end
82
166
  end
83
167
  end
@@ -1,28 +1,30 @@
1
1
  ################################################################################
2
2
  # ActiveRecord v5.0 Custom Type
3
- # This class is not automatically loaded by the gem.
4
- #-------------------------------------------------------------------------------
5
- # 1) Register this type
6
3
  #
7
- # # config/initializers.types.rb
8
- # require "email_address/email_address_type"
4
+ # 1) Register your types
5
+ #
6
+ # # config/initializers/email_address.rb
9
7
  # ActiveRecord::Type.register(:email_address, EmailAddress::Address)
10
8
  # ActiveRecord::Type.register(:canonical_email_address,
11
9
  # EmailAddress::CanonicalEmailAddressType)
12
10
  #
13
11
  # 2) Define your email address columns in your model class
14
12
  #
15
- # class User < ActiveRecord::Base
13
+ # class User < ApplicationRecord
16
14
  # attribute :email, :email_address
17
- # attribute :unique_email, :canonical_email_address
15
+ # attribute :canonical_email, :canonical_email_address
16
+ #
17
+ # def email=(email_address)
18
+ # self[:canonical_email] = email_address
19
+ # self[:email] = email_address
20
+ # end
18
21
  # end
19
22
  #
20
23
  # 3) Profit!
21
24
  #
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"
25
+ # user = User.new(email:"Pat.Smith+registrations@gmail.com")
26
+ # user.email #=> "pat.smith+registrations@gmail.com"
27
+ # user.canonical_email #=> "patsmith@gmail.com"
26
28
  ################################################################################
27
29
 
28
30
  class EmailAddress::EmailAddressType < ActiveRecord::Type::Value
@@ -34,29 +36,11 @@ class EmailAddress::EmailAddressType < ActiveRecord::Type::Value
34
36
 
35
37
  # From a database value
36
38
  def deserialize(value)
37
- EmailAddress.normal(value)
39
+ value && EmailAddress.normal(value)
38
40
  end
39
41
  #
40
42
  # To a database value (string)
41
43
  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)
44
+ value && EmailAddress.normal(value)
61
45
  end
62
46
  end
@@ -20,9 +20,9 @@ module EmailAddress
20
20
  end
21
21
  end
22
22
 
23
- def initialize(host, options={})
23
+ def initialize(host, config={})
24
24
  @host = host
25
- @options = options
25
+ @config = config
26
26
  end
27
27
 
28
28
  def each(&block)
@@ -33,24 +33,13 @@ module EmailAddress
33
33
 
34
34
  # Returns the provider name based on the MX-er host names, or nil if not matched
35
35
  def provider
36
- base = EmailAddress::Config.providers[:default]
37
- EmailAddress::Config.providers.each do |name, defn|
38
- defn = base.merge(defn)
39
- self.each do |m|
40
- return name if DomainMatcher.matches?(m[:host], defn[:exchangers])
36
+ return @provider if @provider
37
+ EmailAddress::Config.providers.each do |provider, config|
38
+ if config[:exchanger_match] && self.matches?(config[:exchanger_match])
39
+ return @provider = provider
41
40
  end
42
41
  end
43
- nil
44
- end
45
-
46
- def has_dns_a_record?
47
- dns_a_record.size > 0 ? true : false
48
- end
49
-
50
- def dns_a_record
51
- @_dns_a_record ||= Socket.gethostbyname(@host)
52
- rescue SocketError # not found, but could also mean network not work
53
- @_dns_a_record ||= []
42
+ @provider = :default
54
43
  end
55
44
 
56
45
  # Returns: [["mta7.am0.yahoodns.net", "66.94.237.139", 1], ["mta5.am0.yahoodns.net", "67.195.168.230", 1], ["mta6.am0.yahoodns.net", "98.139.54.60", 1]]
@@ -66,7 +55,7 @@ module EmailAddress
66
55
 
67
56
  # Returns Array of domain names for the MX'ers, used to determine the Provider
68
57
  def domains
69
- mxers.map {|m| EmailAddress::DomainParser.new(m.first).domain_name}.sort.uniq
58
+ @_domains ||= mxers.map {|m| EmailAddress::Host.new(m.first).domain_name }.sort.uniq
70
59
  end
71
60
 
72
61
  # Returns an array of MX IP address (String) for the given email domain
@@ -74,26 +63,34 @@ module EmailAddress
74
63
  mxers.map {|m| m[1] }
75
64
  end
76
65
 
66
+ # Simple matcher, takes an array of CIDR addresses (ip/bits) and strings.
67
+ # Returns true if any MX IP matches the CIDR or host name ends in string.
68
+ # Ex: match?(%w(127.0.0.1/32 0:0:1/64 .yahoodns.net))
69
+ # Note: Your networking stack may return IPv6 addresses instead of IPv4
70
+ # when both are available. If matching on IP, be sure to include both
71
+ # IPv4 and IPv6 forms for matching for hosts running on IPv6 (like gmail).
72
+ def matches?(rules)
73
+ rules = Array(rules)
74
+ rules.each do |rule|
75
+ if rule.include?("/")
76
+ return rule if self.in_cidr?(rule)
77
+ else
78
+ self.each {|mx| return rule if mx[:host].end_with?(rule) }
79
+ end
80
+ end
81
+ false
82
+ end
83
+
77
84
  # Given a cidr (ip/bits) and ip address, returns true on match. Caches cidr object.
78
85
  def in_cidr?(cidr)
86
+ c = NetAddr::CIDR.create(cidr)
79
87
  if cidr.include?(":")
80
- in_ipv6_cidr?(cidr)
88
+ mx_ips.find { |ip| ip.include?(":") && c.matches?(ip) } ? true : false
89
+ elsif cidr.include?(".")
90
+ mx_ips.find { |ip| !ip.include?(":") && c.matches?(ip) } ? true : false
81
91
  else
82
- in_ipv4_cidr?(cidr)
92
+ false
83
93
  end
84
94
  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
-
98
95
  end
99
96
  end