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.
data/Rakefile CHANGED
@@ -1,8 +1,5 @@
1
1
  require "bundler/gem_tasks"
2
2
 
3
- task :default do
4
- sh "ruby test/email_address/*"
5
- end
6
3
  require "bundler/gem_tasks"
7
4
  require "bundler/setup"
8
5
  require 'rake/testtask'
@@ -10,14 +7,12 @@ require 'rake/testtask'
10
7
  task :default => :test
11
8
 
12
9
  desc "Run the Test Suite, toot suite"
13
- task :test do
14
- sh "find test -name 'test*rb' -exec ruby {} \\;"
10
+ Rake::TestTask.new do |t|
11
+ t.libs << "test"
12
+ t.pattern = "test/**/test_*.rb"
15
13
  end
16
14
 
17
15
  desc "Open and IRB Console with the gem loaded"
18
16
  task :console do
19
- sh "bundle exec irb -Ilib -I . -r email_address"
20
- #require 'irb'
21
- #ARGV.clear
22
- #IRB.start
17
+ sh "bundle exec irb -Ilib -I . -r active_record -r email_address"
23
18
  end
@@ -8,9 +8,8 @@ Gem::Specification.new do |spec|
8
8
  spec.version = EmailAddress::VERSION
9
9
  spec.authors = ["Allen Fair"]
10
10
  spec.email = ["allen.fair@gmail.com"]
11
- spec.description = %q{The EmailAddress library is an _opinionated_ email address handler and
12
- validator.}
13
- spec.summary = %q{EmailAddress checks on validates an acceptable set of email addresses.}
11
+ spec.description = %q{The EmailAddress Gem to work with and validate email addresses.}
12
+ spec.summary = %q{This gem provides a ruby language library for working with and validating email addresses. By default, it validates against conventional usage, the format preferred for user email addresses. It can be configured to validate against RFC “Standard” formats, common email service provider formats, and perform DNS validation.}
14
13
  spec.homepage = "https://github.com/afair/email_address"
15
14
  spec.license = "MIT"
16
15
 
@@ -19,9 +18,14 @@ validator.}
19
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
19
  spec.require_paths = ["lib"]
21
20
 
22
- spec.add_development_dependency "bundler", "~> 1.3"
23
- spec.add_development_dependency "activemodel", "~> 4.2"
24
21
  spec.add_development_dependency "rake"
22
+ spec.add_development_dependency "minitest", "~> 5.8.3"
23
+ spec.add_development_dependency "bundler", "~> 1.3"
24
+ spec.add_development_dependency "activerecord", "~> 5.0.0.beta1" if RUBY_PLATFORM != 'java'
25
+ spec.add_development_dependency "activerecord", "~> 4.2.5" if RUBY_PLATFORM == 'java'
26
+ spec.add_development_dependency "sqlite3" if RUBY_PLATFORM != 'java'
27
+ spec.add_development_dependency "activerecord-jdbcsqlite3-adapter" if RUBY_PLATFORM == 'java'
28
+ spec.add_development_dependency "codeclimate-test-reporter"
25
29
  spec.add_dependency "simpleidn"
26
30
  spec.add_dependency "netaddr"
27
31
  end
data/lib/email_address.rb CHANGED
@@ -1,42 +1,73 @@
1
- require "email_address/address"
2
- require "email_address/config"
3
- require "email_address/domain_matcher"
4
- require "email_address/domain_parser"
5
- require "email_address/exchanger"
6
- require "email_address/host"
7
- require "email_address/local"
8
- require "email_address/matcher"
9
- require "email_address/validator"
10
- require "email_address/version"
11
- require "email_address/active_record_validator" if defined?(ActiveModel)
12
1
 
13
2
  module EmailAddress
14
3
 
4
+ require "email_address/config"
5
+ require "email_address/exchanger"
6
+ require "email_address/host"
7
+ require "email_address/local"
8
+ require "email_address/address"
9
+ require "email_address/version"
10
+ require "email_address/active_record_validator" if defined?(ActiveModel)
11
+ if defined?(ActiveRecord) && ::ActiveRecord::VERSION::MAJOR >= 5
12
+ require "email_address/email_address_type"
13
+ require "email_address/canonical_email_address_type"
14
+ end
15
+
15
16
  # Creates an instance of this email address.
16
17
  # This is a short-cut to Email::Address::Address.new
17
- def self.new(email_address)
18
- EmailAddress::Address.new(email_address)
18
+ def self.new(email_address, config={})
19
+ EmailAddress::Address.new(email_address, config)
20
+ end
21
+
22
+ # Given an email address, this returns true if the email validates, false otherwise
23
+ def self.valid?(email_address, config={})
24
+ self.new(email_address, config).valid?
25
+ end
26
+
27
+ # Given an email address, this returns nil if the email validates,
28
+ # or a string with a small error message otherwise
29
+ def self.error(email_address, config={})
30
+ self.new(email_address, config).error
19
31
  end
20
32
 
21
- # Given an email address, this return true if the email validates, false otherwise
22
- def self.valid?(email_address, options={})
23
- self.new(email_address).valid?(options)
33
+ # Shortcut to normalize the given email address in the given format
34
+ def self.normal(email_address, config={})
35
+ EmailAddress::Address.new(email_address, config).to_s
24
36
  end
25
37
 
26
38
  # Shortcut to normalize the given email address
27
- def self.normal(email_address)
28
- EmailAddress::Address.new(email_address).normalize
39
+ def self.redact(email_address, config={})
40
+ EmailAddress::Address.new(email_address, config).redact
41
+ end
42
+
43
+ # Shortcut to munge the given email address for web publishing
44
+ # returns ma_____@do_____.com
45
+ def self.munge(email_address, config={})
46
+ EmailAddress::Address.new(email_address, config).munge
47
+ end
48
+
49
+ def self.new_redacted(email_address, config={})
50
+ EmailAddress::Address.new(EmailAddress::Address.new(email_address, config).redact)
51
+ end
52
+
53
+ # Returns the Canonical form of the email address. This form is what should
54
+ # be considered unique for an email account, lower case, and no address tags.
55
+ def self.canonical(email_address, config={})
56
+ EmailAddress::Address.new(email_address, config).canonical
29
57
  end
30
58
 
31
- def self.new_normal(email_address)
32
- EmailAddress::Address.new(EmailAddress::Address.new(email_address).normalize)
59
+ def self.new_canonical(email_address, config={})
60
+ EmailAddress::Address.new(EmailAddress::Address.new(email_address, config).canonical, config)
33
61
  end
34
62
 
35
- def self.canonical(email_address)
36
- EmailAddress::Address.new(email_address).normalize
63
+ # Returns the Reference form of the email address, defined as the MD5
64
+ # digest of the Canonical form.
65
+ def self.reference(email_address, config={})
66
+ EmailAddress::Address.new(email_address, config).reference
37
67
  end
38
68
 
39
- def self.new_canonical(email_address)
40
- EmailAddress::Address.new(EmailAddress::Address.new(email_address).canonical)
69
+ # Does the email address match any of the given rules
70
+ def self.matches?(email_address, rules, config={})
71
+ EmailAddress::Address.new(email_address, config).matches?(rules)
41
72
  end
42
73
  end
@@ -8,10 +8,10 @@ module EmailAddress
8
8
  # validates_with EmailAddress::ActiveRecordValidator, field: :name
9
9
  #
10
10
  # Options:
11
- # field: email,
12
- # fields: [:email1, :email2]
13
- # Default field:
14
- # :email or :email_address (first found)
11
+ # * field: email,
12
+ # * fields: [:email1, :email2]
13
+ #
14
+ # Default field: :email or :email_address (first found)
15
15
  #
16
16
  class ActiveRecordValidator < ActiveModel::Validator
17
17
 
@@ -23,7 +23,7 @@ module EmailAddress
23
23
  if @opt[:fields]
24
24
  @opt[:fields].each {|f| validate_email(r, f) }
25
25
  elsif @opt[:field]
26
- validate_email(r, opt[:field])
26
+ validate_email(r, @opt[:field])
27
27
  elsif r.respond_to? :email
28
28
  validate_email(r, :email)
29
29
  elsif r.respond_to? :email_address
@@ -2,116 +2,162 @@ require 'digest/sha1'
2
2
  require 'digest/md5'
3
3
 
4
4
  module EmailAddress
5
+ # Implements the Email Address container, which hold the Local
6
+ # (EmailAddress::Local) and Host (Email::AddressHost) parts.
5
7
  class Address
6
8
  include Comparable
9
+ attr_accessor :original, :local, :host, :config, :error
10
+
11
+ CONVENTIONAL_REGEX = /\A#{::EmailAddress::Local::CONVENTIONAL_MAILBOX_WITHIN}
12
+ @#{::EmailAddress::Host::DNS_HOST_REGEX}\z/x
13
+ STANDARD_REGEX = /\A#{::EmailAddress::Local::STANDARD_LOCAL_WITHIN}
14
+ @#{::EmailAddress::Host::DNS_HOST_REGEX}\z/x
7
15
 
8
16
  # Given an email address of the form "local@hostname", this sets up the
9
17
  # instance, and initializes the address to the "normalized" format of the
10
18
  # address. The original string is available in the #original method.
11
- def initialize(email_address)
12
- @original = email_address
13
- (local, host) = email_address.split('@', 2)
14
- @host = EmailAddress::Host.new(host)
15
- @local = EmailAddress::Local.new(local||@address, @host)
16
- end
17
-
18
- # Returns the Email::Address::Host to inspect the host name of the address
19
- def host
20
- @host
21
- end
22
-
23
- # Returns the EmailAddress::local to inspect the data to the left of the @
24
- # Use the #left method to access the full string
25
- def local
26
- @local
27
- end
19
+ def initialize(email_address, config={})
20
+ email_address.strip! if email_address
21
+ @original = email_address
22
+ email_address||= ""
23
+ if lh = email_address.match(/(.+)@(.+)/)
24
+ (_, local, host) = lh.to_a
25
+ else
26
+ (local, host) = [email_address, '']
27
+ end
28
+ @host = EmailAddress::Host.new(host, config)
29
+ @config = @host.config
30
+ @local = EmailAddress::Local.new(local, @config)
31
+ end
32
+
33
+ ############################################################################
34
+ # Local Part (left of @) access
35
+ # * local: Access full local part instance
36
+ # * left: everything on the left of @
37
+ # * mailbox: parsed mailbox or email account name
38
+ # * tag: address tag (mailbox+tag)
39
+ ############################################################################
28
40
 
29
41
  # Everything to the left of the @ in the address, called the local part.
30
42
  def left
31
- local.to_s
43
+ self.local.to_s
32
44
  end
33
45
 
34
46
  # Returns the mailbox portion of the local port, with no tags. Usually, this
35
47
  # can be considered the user account or role account names. Some systems
36
48
  # employ dynamic email addresses which don't have the same meaning.
37
49
  def mailbox
38
- @local.mailbox
39
- end
40
-
41
- # Returns the host name, the part to the right of the @ sign.
42
- def host_name
43
- @host.host_name
50
+ self.local.mailbox
44
51
  end
45
- alias :right :host_name
46
52
 
47
53
  # Returns the tag part of the local address, or nil if not given.
48
54
  def tag
49
- @local.tag
55
+ self.local.tag
50
56
  end
51
57
 
52
58
  # Retuns any comments parsed from the local part of the email address.
53
59
  # This is retained for inspection after construction, even if it is
54
60
  # removed from the normalized email address.
55
61
  def comment
56
- @local.comment
62
+ self.local.comment
57
63
  end
58
64
 
65
+ ############################################################################
66
+ # Host Part (right of @) access
67
+ # * host: Access full local part instance (alias: right)
68
+ # * hostname: everything on the right of @
69
+ # * provider: determined email service provider
70
+ ############################################################################
71
+
72
+ # Returns the host name, the part to the right of the @ sign.
73
+ def host_name
74
+ @host.host_name
75
+ end
76
+ alias :right :host_name
77
+ alias :hostname :host_name
78
+
59
79
  # Returns the ESP (Email Service Provider) or ISP name derived
60
80
  # using the provider configuration rules.
61
81
  def provider
62
82
  @host.provider
63
83
  end
64
84
 
65
- # Returns the string representation of the normalized email address.
66
- def to_s
67
- normalize
68
- end
85
+ ############################################################################
86
+ # Address methods
87
+ ############################################################################
69
88
 
70
- # The original email address in the request (unmodified).
71
- def original
72
- @original
89
+ # Returns the string representation of the normalized email address.
90
+ def normal
91
+ if !@original
92
+ @original
93
+ elsif self.local.to_s.size == 0
94
+ ""
95
+ elsif self.host.to_s.size == 0
96
+ self.local.to_s
97
+ else
98
+ "#{self.local.to_s}@#{self.host.to_s}"
99
+ end
73
100
  end
101
+ alias :to_s :normal
74
102
 
75
- # Returns the normailed email address according to the provider
76
- # and system normalization rules. Ususally this downcases the address,
77
- # removes spaces and comments, but includes any tags.
78
- def normal
79
- [@local.normalize, @host.normalize].join('@')
103
+ def inspect
104
+ "#<EmailAddress::Address:0x#{self.object_id.to_s(16)} address=\"#{self.to_s}\">"
80
105
  end
81
- alias :normalize :normal
82
106
 
83
107
  # Returns the canonical email address according to the provider
84
108
  # uniqueness rules. Usually, this downcases the address, removes
85
109
  # spaves and comments and tags, and any extraneous part of the address
86
110
  # not considered a unique account by the provider.
87
111
  def canonical
88
- [@local.canonical, @host.canonical].join('@')
112
+ [self.local.canonical, @host.canonical].join('@')
113
+ end
114
+
115
+ # True if the given address is already in it's canonical form.
116
+ def canonical?
117
+ self.canonical == self.to_s
118
+ end
119
+
120
+ # Returns the redacted form of the address
121
+ # This format is defined by this libaray, and may change as usage increases.
122
+ def redact
123
+ return self.to_s if self.local.redacted?
124
+ %Q({#{self.sha1}}@#{self.host.to_s})
125
+ end
126
+
127
+ # True if the address is already in the redacted state.
128
+ def redacted?
129
+ self.local.redacted?
130
+ end
131
+
132
+ # Returns the munged form of the address, by default "mailbox@domain.tld"
133
+ # returns "ma*****@do*****".
134
+ def munge
135
+ [self.local.munge, self.host.munge].join("@")
89
136
  end
90
- alias :uniq :canonical
91
- alias :canonicalize :canonical
92
137
 
93
138
  # Returns and MD5 of the canonical address form. Some cross-system systems
94
139
  # use the email address MD5 instead of the actual address to refer to the
95
140
  # same shared user identity without exposing the actual address when it
96
141
  # is not known in common.
97
- def md5
98
- Digest::MD5.hexdigest(canonical)
99
- end
100
-
101
- def canonical_md5
142
+ def reference
102
143
  Digest::MD5.hexdigest(self.canonical)
103
144
  end
145
+ alias :md5 :reference
104
146
 
105
147
  # This returns the SHA1 digest (in a hex string) of the canonical email
106
148
  # address. See #md5 for more background.
107
149
  def sha1
108
- Digest::SHA1.hexdigest(canonical)
150
+ Digest::SHA1.hexdigest(canonical + @config[:sha1_secret])
109
151
  end
110
152
 
153
+ #---------------------------------------------------------------------------
154
+ # Comparisons & Matching
155
+ #---------------------------------------------------------------------------
156
+
111
157
  # Equal matches the normalized version of each address. Use the Threequal to check
112
158
  # for match on canonical or redacted versions of addresses
113
159
  def ==(other_email)
114
- normalize == other_email.normalize
160
+ self.to_s == other_email.to_s
115
161
  end
116
162
  alias :eql? :==
117
163
  alias :equal? :==
@@ -119,43 +165,77 @@ module EmailAddress
119
165
  # Return the <=> or CMP comparison operator result (-1, 0, +1) on the comparison
120
166
  # of this addres with another, using the canonical or redacted forms.
121
167
  def same_as?(other_email)
122
- canonical == other_email.canonical ||
123
- redact == other_email.canonical || canonical == other_email.redact
168
+ if other_email.is_a?(String)
169
+ other_email = EmailAddress::Address.new(other_email)
170
+ end
171
+
172
+ self.canonical == other_email.canonical ||
173
+ self.redact == other_email.canonical ||
174
+ self.canonical == other_email.redact
124
175
  end
125
176
  alias :include? :same_as?
126
177
 
127
178
  # Return the <=> or CMP comparison operator result (-1, 0, +1) on the comparison
128
179
  # of this addres with another, using the normalized form.
129
180
  def <=>(other_email)
130
- normalize <=> other_email.normalize
181
+ self.to_s <=> other_email.to_s
131
182
  end
132
183
 
133
- # Redact the address for storage. To protect the user's privacy,
134
- # use this when you don't want to store a real email, only a fingerprint.
135
- # Given the original address, you can match the original with this method.
136
- # This returns the SHA1 of the canonical address (no tags, no gmail dots)
137
- # at the original host. The host is part of the digest part, but also
138
- # retained for verification and domain maintenance.
139
- def redact
140
- [sha1, @host.canonical].join('@')
141
- end
184
+ # Address matches one of these Matcher rule patterns
185
+ def matches?(*rules)
186
+ rules.flatten!
187
+ match = self.local.matches?(rules)
188
+ match ||= self.host.matches?(rules)
189
+ return match if match
142
190
 
143
- def redacted?
144
- @local.to_s =~ /\A[0-9a-f]{40}\z/ ? true : false
191
+ # Does "root@*.com" match "root@example.com" domain name
192
+ rules.each do |r|
193
+ if r =~ /.+@.+/
194
+ return r if File.fnmatch?(r, self.to_s)
195
+ end
196
+ end
197
+ false
145
198
  end
146
199
 
200
+ #---------------------------------------------------------------------------
201
+ # Validation
202
+ #---------------------------------------------------------------------------
203
+
147
204
  # Returns true if this address is considered valid according to the format
148
205
  # configured for its provider, It test the normalized form.
149
206
  def valid?(options={})
150
- EmailAddress::Validator.validate(self, options)
207
+ self.error = nil
208
+ unless self.local.valid?
209
+ self.error = "Invalid Mailbox"
210
+ return false
211
+ end
212
+ unless self.host.valid?
213
+ self.error = "Invalid Host"
214
+ return false
215
+ end
216
+ if @config[:address_size] && !@config[:address_size].include?(self.to_s.size)
217
+ self.error = "Exceeds size"
218
+ return false
219
+ end
220
+ if @config[:address_validation].is_a?(Proc)
221
+ unless @config[:address_validation].call(self.to_s)
222
+ self.error = "Not allowed"
223
+ return false
224
+ end
225
+ else
226
+ return false unless self.local.valid?
227
+ return false unless self.host.valid?
228
+ end
229
+ if !@config[:address_local] && !self.hostname.include?(".")
230
+ self.error = "Incomplete Domain"
231
+ return false
232
+ end
233
+ true
234
+ end
235
+
236
+ def error
237
+ self.valid? ? nil : @error
151
238
  end
152
239
 
153
- # Returns an array of error messages generated from the validation process via
154
- # the #valid? method.
155
- def errors(options={})
156
- v = EmailAddress::Validator.new(self, options)
157
- v.valid?
158
- v.errors
159
- end
160
240
  end
161
241
  end