email_address 0.0.3 → 0.1.0

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.
@@ -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