validates_email_format_of 1.4.7 → 1.5.0

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