validates_email_format_of 1.4.7 → 1.5.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.
@@ -2,14 +2,11 @@
2
2
  module ValidatesEmailFormatOf
3
3
  require 'resolv'
4
4
 
5
- VERSION = '1.4.7'
5
+ VERSION = '1.5.0'
6
6
 
7
7
  MessageScope = defined?(ActiveModel) ? :activemodel : :activerecord
8
8
 
9
- LocalPartSpecialChars = Regexp.escape('!#$%&\'*-/=?+-^_`{|}~')
10
- LocalPartUnquoted = '([[:alnum:]' + LocalPartSpecialChars + ']+[\.]+)*[[:alnum:]' + LocalPartSpecialChars + '+]+'
11
- LocalPartQuoted = '\"([[:alnum:]' + LocalPartSpecialChars + '\.]|\\\\[\x00-\xFF])*\"'
12
- Regex = Regexp.new('\A(' + LocalPartUnquoted + '|' + LocalPartQuoted + '+)@(((\w+\-+[^_])|(\w+\.[a-z0-9-]*))*([a-z0-9\-\.]{1,63})\.[a-z]{2,6}(?:\.[a-z]{2,6})?\Z)', Regexp::EXTENDED | Regexp::IGNORECASE, 'n')
9
+ LocalPartSpecialChars = /[\!\#\$\%\&\'\*\-\/\=\?\+\-\^\_\`\{\|\}\~]/
13
10
 
14
11
  def self.validate_email_domain(email)
15
12
  domain = email.match(/\@(.+)/)[1]
@@ -26,58 +23,112 @@ module ValidatesEmailFormatOf
26
23
  # * <tt>message</tt> - A custom error message (default is: "does not appear to be valid")
27
24
  # * <tt>check_mx</tt> - Check for MX records (default is false)
28
25
  # * <tt>mx_message</tt> - A custom error message when an MX record validation fails (default is: "is not routable.")
29
- # * <tt>with</tt> The regex to use for validating the format of the email address (default is ValidatesEmailFormatOf::Regex)</tt>
26
+ # * <tt>with</tt> The regex to use for validating the format of the email address (deprecated)
30
27
  # * <tt>local_length</tt> Maximum number of characters allowed in the local part (default is 64)
31
28
  # * <tt>domain_length</tt> Maximum number of characters allowed in the domain part (default is 255)
32
29
  def self.validate_email_format(email, options={})
33
30
  default_options = { :message => I18n.t(:invalid_email_address, :scope => [MessageScope, :errors, :messages], :default => 'does not appear to be valid'),
34
31
  :check_mx => false,
35
32
  :mx_message => I18n.t(:email_address_not_routable, :scope => [MessageScope, :errors, :messages], :default => 'is not routable'),
36
- :with => ValidatesEmailFormatOf::Regex ,
37
33
  :domain_length => 255,
38
34
  :local_length => 64
39
35
  }
40
36
  opts = options.merge(default_options) {|key, old, new| old} # merge the default options into the specified options, retaining all specified options
41
37
 
42
- email.strip! if email
38
+ email = email.strip if email
43
39
 
44
- # local part max is 64 chars, domain part max is 255 chars
45
- # TODO: should this decode escaped entities before counting?
46
40
  begin
47
41
  domain, local = email.reverse.split('@', 2)
48
42
  rescue
49
43
  return [ opts[:message] ]
50
44
  end
51
45
 
52
- unless email =~ opts[:with] and not email =~ /\.\./ and domain.length <= opts[:domain_length] and local.length <= opts[:local_length]
53
- return [ opts[:message] ]
46
+ # need local and domain parts
47
+ return [ opts[:message] ] unless local and domain
48
+
49
+ # check lengths
50
+ return [ opts[:message] ] unless domain.length <= opts[:domain_length] and local.length <= opts[:local_length]
51
+
52
+ local.reverse!
53
+ domain.reverse!
54
+
55
+ if opts.has_key?(:with) # holdover from versions <= 1.4.7
56
+ return [ opts[:message] ] unless email =~ opts[:with]
57
+ else
58
+ return [ opts[:message] ] unless self.validate_local_part_syntax(local) and self.validate_domain_part_syntax(domain)
54
59
  end
55
60
 
56
- if opts[:check_mx] and !ValidatesEmailFormatOf::validate_email_domain(email)
61
+ if opts[:check_mx] and !self.validate_email_domain(email)
57
62
  return [ opts[:mx_message] ]
58
63
  end
59
64
 
60
- local.reverse!
61
-
62
- # check for proper escaping
63
-
64
- if local[0] == '"'
65
- local.gsub!(/\A\"|\"\Z/, '')
66
- escaped = false
67
- local.each_char do |c|
68
- if escaped
69
- escaped = false
70
- elsif c == '"' # can't have a double quote without a preceding backslash
71
- return [ opts[:mx_message] ]
72
- else
73
- escaped = c == '\\'
74
- end
75
- end
76
-
77
- return [ opts[:mx_message] ] if escaped
65
+ return nil # represents no validation errors
66
+ end
67
+
68
+
69
+ def self.validate_local_part_syntax(local)
70
+ in_quoted_pair = false
71
+ in_quoted_string = false
72
+
73
+ (0..local.length-1).each do |i|
74
+ ord = local[i].ord
75
+
76
+ # accept anything if it's got a backslash before it
77
+ if in_quoted_pair
78
+ in_quoted_pair = false
79
+ next
80
+ end
81
+
82
+ # backslash signifies the start of a quoted pair
83
+ if ord == 92 and i < local.length - 1
84
+ return false if not in_quoted_string # must be in quoted string per http://www.rfc-editor.org/errata_search.php?rfc=3696
85
+ in_quoted_pair = true
86
+ next
87
+ end
88
+
89
+ # double quote delimits quoted strings
90
+ if ord == 34
91
+ in_quoted_string = !in_quoted_string
92
+ next
93
+ end
94
+
95
+ next if local[i] =~ /[[:alnum:]]/
96
+ next if local[i] =~ LocalPartSpecialChars
97
+
98
+ # period must be followed by something
99
+ if ord == 46
100
+ return false if i == 0 or i == local.length - 1 # can't be first or last char
101
+ next unless local[i+1].ord == 46 # can't be followed by a period
78
102
  end
79
103
 
80
- return nil # represents no validation errors
104
+ return false
105
+ end
106
+
107
+ return false if in_quoted_string # unbalanced quotes
108
+
109
+ return true
110
+ end
111
+
112
+ def self.validate_domain_part_syntax(domain)
113
+ parts = domain.downcase.split('.', -1)
114
+
115
+ return false if parts.length <= 1 # Only one domain part
116
+
117
+ # Empty parts (double period) or invalid chars
118
+ return false if parts.any? {
119
+ |part|
120
+ part.nil? or
121
+ part.empty? or
122
+ not part =~ /\A[[:alnum:]\-]+\Z/ or
123
+ part[0] == '-' or part[-1] == '-' # hyphen at beginning or end of part
124
+ }
125
+
126
+ # ipv4
127
+ return true if parts.length == 4 and parts.all? { |part| part =~ /\A[0-9]+\Z/ and part.to_i.between?(0, 255) }
128
+
129
+ return false if parts[-1].length < 2 or not parts[-1] =~ /[a-z\-]/ # TLD is too short or does not contain a char or hyphen
130
+
131
+ return true
81
132
  end
82
133
 
83
134
  module Validations
@@ -98,7 +149,7 @@ module ValidatesEmailFormatOf
98
149
  # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
99
150
  # method, proc or string should return or evaluate to a true or false value.
100
151
  # * <tt>unless</tt> - See <tt>:if</tt>
101
- def validates_email_format_of(*attr_names)
152
+ def validates_email_format_of(*attr_names)
102
153
  options = { :on => :save,
103
154
  :allow_nil => false,
104
155
  :allow_blank => false }
@@ -50,6 +50,10 @@ class ValidatesEmailFormatOfTest < TEST_CASE
50
50
  '_somename@example.com',
51
51
  # apostrophes
52
52
  "test'test@example.com",
53
+ # international domain names
54
+ 'test@xn--bcher-kva.ch',
55
+ 'test@example.xn--0zwm56d',
56
+ 'test@192.192.192.1'
53
57
  ].each do |email|
54
58
  assert_valid(email)
55
59
  end
@@ -64,18 +68,20 @@ class ValidatesEmailFormatOfTest < TEST_CASE
64
68
  # period can not appear twice consecutively in local part
65
69
  'invali..d@example.com',
66
70
  # should not allow underscores in domain names
67
- 'invalid@ex_mple.com',
68
- 'invalid@e..example.com',
69
- 'invalid@p-t..example.com',
70
- 'invalid@example.com.',
71
- 'invalid@example.com_',
72
- 'invalid@example.com-',
73
- 'invalid-example.com',
74
- 'invalid@example.b#r.com',
75
- 'invalid@example.c',
76
- 'invali d@example.com',
71
+ 'invalid@ex_mple.com',
72
+ 'invalid@e..example.com',
73
+ 'invalid@p-t..example.com',
74
+ 'invalid@example.com.',
75
+ 'invalid@example.com_',
76
+ 'invalid@example.com-',
77
+ 'invalid-example.com',
78
+ 'invalid@example.b#r.com',
79
+ 'invalid@example.c',
80
+ 'invali d@example.com',
81
+ # TLD can not be only numeric
82
+ 'invalid@example.123',
77
83
  # unclosed quote
78
- "\"a-17180061943-10618354-1993365053",
84
+ "\"a-17180061943-10618354-1993365053@example.com",
79
85
  # too many special chars used to cause the regexp to hang
80
86
  "-+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++@foo",
81
87
  'invalidexample.com',
@@ -84,27 +90,30 @@ class ValidatesEmailFormatOfTest < TEST_CASE
84
90
  'local@sub.#domain.com',
85
91
  # one at a time
86
92
  "foo@example.com\nexample@gmail.com",
87
- 'invalid@example.'].each do |email|
93
+ 'invalid@example.',
94
+ "\"foo\\\\\"\"@bar.com",
95
+ "foo@mail.com\r\nfoo@mail.com"
96
+ ].each do |email|
88
97
  assert_invalid(email)
89
98
  end
90
99
  end
91
-
100
+
92
101
  # from http://www.rfc-editor.org/errata_search.php?rfc=3696
93
102
  def test_should_allow_quoted_characters
94
- ['"Abc\@def"@example.com',
103
+ ['"Abc\@def"@example.com',
95
104
  '"Fred\ Bloggs"@example.com',
96
105
  '"Joe.\\Blow"@example.com',
97
106
  ].each do |email|
98
107
  assert_valid(email)
99
108
  end
100
109
  end
101
-
110
+
102
111
  def test_should_required_balanced_quoted_characters
103
112
  assert_valid(%!"example\\\\\\""@example.com!)
104
113
  assert_valid(%!"example\\\\"@example.com!)
105
114
  assert_invalid(%!"example\\\\""example.com!)
106
115
  end
107
-
116
+
108
117
  # from http://tools.ietf.org/html/rfc3696, page 5
109
118
  # corrected in http://www.rfc-editor.org/errata_search.php?rfc=3696
110
119
  def test_should_not_allow_escaped_characters_without_quotes
@@ -123,21 +132,25 @@ class ValidatesEmailFormatOfTest < TEST_CASE
123
132
  assert_invalid(email)
124
133
  end
125
134
  end
126
-
135
+
127
136
  def test_overriding_length_checks
128
137
  assert_not_nil ValidatesEmailFormatOf::validate_email_format('valid@example.com', :local_length => 1)
129
138
  assert_not_nil ValidatesEmailFormatOf::validate_email_format('valid@example.com', :domain_length => 1)
130
139
  end
131
140
 
141
+ def test_validating_with_custom_regexp
142
+ assert_nil ValidatesEmailFormatOf::validate_email_format('012345@789', :with => /[0-9]+\@[0-9]+/)
143
+ end
144
+
132
145
  def test_should_respect_validate_on_option
133
146
  p = create_person(:email => @valid_email)
134
147
  save_passes(p)
135
-
148
+
136
149
  # we only asked to validate on :create so this should fail
137
150
  assert p.update_attributes(:email => @invalid_email)
138
151
  assert_equal @invalid_email, p.email
139
152
  end
140
-
153
+
141
154
  def test_should_allow_custom_error_message
142
155
  p = create_person(:email => @invalid_email)
143
156
  save_fails(p)
@@ -151,7 +164,7 @@ class ValidatesEmailFormatOfTest < TEST_CASE
151
164
  def test_should_allow_nil
152
165
  p = create_person(:email => nil)
153
166
  save_passes(p)
154
-
167
+
155
168
  p = PersonForbidNil.new(:email => nil)
156
169
  save_fails(p)
157
170
  end
@@ -170,7 +183,7 @@ class ValidatesEmailFormatOfTest < TEST_CASE
170
183
  pmx = MxRecord.new(:email => 'test@code.dunae.ca')
171
184
  save_passes(pmx)
172
185
  end
173
-
186
+
174
187
  def test_shorthand
175
188
  if ActiveRecord::VERSION::MAJOR >= 3
176
189
  s = Shorthand.new(:email => 'invalid')
@@ -184,15 +197,20 @@ class ValidatesEmailFormatOfTest < TEST_CASE
184
197
  end
185
198
  end
186
199
 
200
+ def test_frozen_string
201
+ assert_valid(" #{@valid_email} ".freeze)
202
+ assert_invalid(" #{@invalid_email} ".freeze)
203
+ end
204
+
187
205
  protected
188
206
  def create_person(params)
189
207
  Person.new(params)
190
208
  end
191
-
209
+
192
210
  def assert_valid(email)
193
211
  assert_nil ValidatesEmailFormatOf::validate_email_format(email)
194
212
  end
195
-
213
+
196
214
  def assert_invalid(email)
197
215
  err = ValidatesEmailFormatOf::validate_email_format(email)
198
216
  assert_equal 1, err.size
@@ -207,7 +225,7 @@ class ValidatesEmailFormatOfTest < TEST_CASE
207
225
  assert_nil p.errors.on(:email)
208
226
  end
209
227
  end
210
-
228
+
211
229
  def save_fails(p, email = '')
212
230
  assert !p.valid?, " #{email} should fail"
213
231
  assert !p.save
metadata CHANGED
@@ -1,28 +1,24 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: validates_email_format_of
3
- version: !ruby/object:Gem::Version
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.5.0
4
5
  prerelease:
5
- version: 1.4.7
6
6
  platform: ruby
7
- authors:
7
+ authors:
8
8
  - Alex Dunae
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
-
13
- date: 2011-05-13 00:00:00 Z
12
+ date: 2011-09-07 00:00:00.000000000Z
14
13
  dependencies: []
15
-
16
14
  description: Validate e-mail addresses against RFC 2822 and RFC 3696.
17
15
  email: code@dunae.ca
18
16
  executables: []
19
-
20
17
  extensions: []
21
-
22
- extra_rdoc_files:
18
+ extra_rdoc_files:
23
19
  - README.rdoc
24
20
  - MIT-LICENSE
25
- files:
21
+ files:
26
22
  - MIT-LICENSE
27
23
  - init.rb
28
24
  - rakefile.rb
@@ -36,33 +32,31 @@ files:
36
32
  - test/fixtures/people.yml
37
33
  homepage: https://github.com/alexdunae/validates_email_format_of
38
34
  licenses: []
39
-
40
35
  post_install_message:
41
- rdoc_options:
36
+ rdoc_options:
42
37
  - --title
43
38
  - validates_email_format_of
44
- require_paths:
39
+ require_paths:
45
40
  - lib
46
- required_ruby_version: !ruby/object:Gem::Requirement
41
+ required_ruby_version: !ruby/object:Gem::Requirement
47
42
  none: false
48
- requirements:
49
- - - ">="
50
- - !ruby/object:Gem::Version
51
- version: "0"
52
- required_rubygems_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
48
  none: false
54
- requirements:
55
- - - ">="
56
- - !ruby/object:Gem::Version
57
- version: "0"
49
+ requirements:
50
+ - - ! '>='
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
58
53
  requirements: []
59
-
60
54
  rubyforge_project:
61
- rubygems_version: 1.8.1
55
+ rubygems_version: 1.8.5
62
56
  signing_key:
63
57
  specification_version: 3
64
58
  summary: Validate e-mail addresses against RFC 2822 and RFC 3696.
65
- test_files:
59
+ test_files:
66
60
  - test/fixtures/person.rb
67
61
  - test/schema.rb
68
62
  - test/test_helper.rb