validates_email_format_of 1.7.2 → 1.8.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.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/ci.yml +38 -7
- data/Appraisals +4 -0
- data/CHANGELOG.md +11 -0
- data/README.md +111 -0
- data/config/locales/de.yml +1 -1
- data/config/locales/en.yml +1 -1
- data/config/locales/id.yml +8 -0
- data/config/locales/it.yml +1 -1
- data/config/locales/pl.yml +1 -1
- data/config/locales/pt-BR.yml +1 -1
- data/config/locales/pt.yml +1 -1
- data/config/locales/tr.yml +8 -0
- data/gemfiles/rails_7.1.gemfile +7 -0
- data/lib/validates_email_format_of/active_model.rb +1 -1
- data/lib/validates_email_format_of/version.rb +1 -1
- data/lib/validates_email_format_of.rb +67 -21
- data/spec/spec_helper.rb +5 -3
- data/spec/validates_email_format_of_spec.rb +75 -8
- data/validates_email_format_of.gemspec +3 -2
- metadata +24 -6
- data/README.rdoc +0 -100
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2329a74ad3d6ce656ab392715c9c2cbda4c4af6fbffcce69f5e98afd8018f0ae
|
4
|
+
data.tar.gz: 4ad12aef8277878f61c32a82b231d58251fe2e674ed262a021814075e9df2b17
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b454131a1c1f7ded070a65144ccbb9a27add0efa96ce681b578d9e7497f39234b1985863327a51e2179752dc06b9586978c762accaf5daea7e600df34cf11599
|
7
|
+
data.tar.gz: 76badfe2f7d9b8b75142c2f9b369f485d5b0cd6d3db660206e120bd9eb426f1ed8bd089e58bcb9651c64e5e47ea589eef8c040021da8adb3635957e1f1e3fd27
|
data/.github/workflows/ci.yml
CHANGED
@@ -1,10 +1,8 @@
|
|
1
1
|
name: CI
|
2
2
|
|
3
3
|
on:
|
4
|
-
push
|
5
|
-
|
6
|
-
pull_request:
|
7
|
-
branches: [master]
|
4
|
+
- push
|
5
|
+
- pull_request
|
8
6
|
|
9
7
|
permissions:
|
10
8
|
contents: read
|
@@ -17,38 +15,70 @@ jobs:
|
|
17
15
|
|
18
16
|
strategy:
|
19
17
|
matrix:
|
20
|
-
ruby: ["2.6", "2.7", "3.0", "3.1"]
|
21
|
-
gemfile: ["4.2", "5.0", "5.1", "5.2", "6.0", "6.1", "7.0"]
|
18
|
+
ruby: ["2.6", "2.7", "3.0", "3.1", "3.2", "3.3"]
|
19
|
+
gemfile: ["4.2", "5.0", "5.1", "5.2", "6.0", "6.1", "7.0", "7.1"]
|
20
|
+
|
22
21
|
exclude:
|
23
22
|
- gemfile: "4.2"
|
24
23
|
ruby: "3.0"
|
25
24
|
- gemfile: "4.2"
|
26
25
|
ruby: "3.1"
|
26
|
+
- gemfile: "4.2"
|
27
|
+
ruby: "3.2"
|
28
|
+
- gemfile: "4.2"
|
29
|
+
ruby: "3.3"
|
27
30
|
- gemfile: "5.0"
|
28
31
|
ruby: "3.0"
|
29
32
|
- gemfile: "5.0"
|
30
33
|
ruby: "3.1"
|
34
|
+
- gemfile: "5.0"
|
35
|
+
ruby: "3.2"
|
36
|
+
- gemfile: "5.0"
|
37
|
+
ruby: "3.3"
|
31
38
|
- gemfile: "5.1"
|
32
39
|
ruby: "3.0"
|
33
40
|
- gemfile: "5.1"
|
34
41
|
ruby: "3.1"
|
42
|
+
- gemfile: "5.1"
|
43
|
+
ruby: "3.2"
|
44
|
+
- gemfile: "5.1"
|
45
|
+
ruby: "3.3"
|
35
46
|
- gemfile: "5.2"
|
36
47
|
ruby: "3.0"
|
37
48
|
- gemfile: "5.2"
|
38
49
|
ruby: "3.1"
|
50
|
+
- gemfile: "5.2"
|
51
|
+
ruby: "3.2"
|
52
|
+
- gemfile: "5.2"
|
53
|
+
ruby: "3.3"
|
54
|
+
- gemfile: "6.0"
|
55
|
+
ruby: "3.2"
|
56
|
+
- gemfile: "6.0"
|
57
|
+
ruby: "3.3"
|
58
|
+
- gemfile: "6.1"
|
59
|
+
ruby: "3.2"
|
60
|
+
- gemfile: "6.1"
|
61
|
+
ruby: "3.3"
|
39
62
|
- gemfile: "7.0"
|
40
63
|
ruby: "2.5"
|
41
64
|
- gemfile: "7.0"
|
42
65
|
ruby: "2.6"
|
43
66
|
- gemfile: "7.0"
|
44
67
|
ruby: "2.7"
|
68
|
+
- gemfile: "7.1"
|
69
|
+
ruby: "2.5"
|
70
|
+
- gemfile: "7.1"
|
71
|
+
ruby: "2.6"
|
72
|
+
- gemfile: "7.1"
|
73
|
+
ruby: "2.7"
|
74
|
+
|
45
75
|
|
46
76
|
env:
|
47
77
|
BUNDLE_GEMFILE: gemfiles/rails_${{ matrix.gemfile }}.gemfile
|
48
78
|
RAILS_ENV: test
|
49
79
|
|
50
80
|
steps:
|
51
|
-
- uses: actions/checkout@
|
81
|
+
- uses: actions/checkout@v4
|
52
82
|
|
53
83
|
- name: "Install Ruby ${{ matrix.ruby }}"
|
54
84
|
# To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
|
@@ -65,3 +95,4 @@ jobs:
|
|
65
95
|
|
66
96
|
- name: Run standard.rb
|
67
97
|
run: bundle exec rake standard
|
98
|
+
if: ${{ ! startsWith(matrix.ruby, '2.') }}
|
data/Appraisals
CHANGED
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,17 @@
|
|
2
2
|
|
3
3
|
## [Unreleased]
|
4
4
|
|
5
|
+
[Unreleased]: https://github.com/validates-email-format-of/validates_email_format_of/compare/v1.8.0...master
|
6
|
+
|
7
|
+
## [1.8.0]
|
8
|
+
|
9
|
+
* Add Internationalized Domain Name support - https://github.com/validates-email-format-of/validates_email_format_of/pull/103 - thanks https://github.com/sbilharz !
|
10
|
+
* Add Turkish locale - https://github.com/validates-email-format-of/validates_email_format_of/pull/101 - thanks https://github.com/@krmbzds !
|
11
|
+
* Added Indonesian locale - https://github.com/validates-email-format-of/validates_email_format_of/commit/129ebfc3a3b432b4df0334bcbdd74b1d17d765e0 - thanks https://github.com/khoerodin !
|
12
|
+
* Fix inconsistent `generate_messages` behaviour - https://github.com/validates-email-format-of/validates_email_format_of/pull/105
|
13
|
+
* ⚠️ Deprecate `:with` option - https://github.com/validates-email-format-of/validates_email_format_of/issues/42
|
14
|
+
* Require i18n >= 0.8.0 in modern Ruby versions - https://github.com/advisories/GHSA-34hf-g744-jw64
|
15
|
+
|
5
16
|
[Unreleased]: https://github.com/validates-email-format-of/validates_email_format_of/compare/v1.7.2...master
|
6
17
|
|
7
18
|
## [1.7.2]
|
data/README.md
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
# validates_email_format_of
|
2
|
+
|
3
|
+
[]( https://github.com/validates-email-format-of/validates_email_format_of/actions/workflows/ci.yml?query=branch%3Amaster)
|
4
|
+
|
5
|
+
A Ruby gem to validate email addresses against RFC 2822 and RFC 5322.
|
6
|
+
|
7
|
+
## Why this email validator?
|
8
|
+
|
9
|
+
This gem is the O.G. email validation gem for Rails. It was started back in 2006.
|
10
|
+
|
11
|
+
Why use this validator? Instead of trying to validate email addresses with one giant regular expression, this library parses addresses character by character. This lets us handle weird cases like [nested comments](https://www.rfc-editor.org/rfc/rfc5322#appendix-A.5). Gross but technically allowed.
|
12
|
+
|
13
|
+
In reality, most email validating scripts will get you where you need to go. This library just aims to go all the way.
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
Add the gem to your Gemfile with:
|
18
|
+
|
19
|
+
```sh
|
20
|
+
gem 'validates_email_format_of'
|
21
|
+
```
|
22
|
+
|
23
|
+
### Usage in a Rails app
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
class Person < ActiveRecord::Base
|
27
|
+
validates :email, :email_format => { :message => "is not looking good" }
|
28
|
+
|
29
|
+
# OR
|
30
|
+
|
31
|
+
validates_email_format_of :email, :message => "is not looking good"
|
32
|
+
end
|
33
|
+
```
|
34
|
+
|
35
|
+
You can use the included `rspec` matcher as well:
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
require "validates_email_format_of/rspec_matcher"
|
39
|
+
|
40
|
+
describe Person do
|
41
|
+
it { should validate_email_format_of(:email).with_message("is not looking good") }
|
42
|
+
end
|
43
|
+
```
|
44
|
+
|
45
|
+
### Usage without Rails
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
# Optional, if you want error messages to be in your language
|
49
|
+
ValidatesEmailFormatOf::load_i18n_locales
|
50
|
+
I18n.locale = :pl
|
51
|
+
|
52
|
+
ValidatesEmailFormatOf::validate_email_format("example@mydomain.com") # => nil
|
53
|
+
ValidatesEmailFormatOf::validate_email_format("invalid@") # => ["does not appear to be a valid email address"]
|
54
|
+
```
|
55
|
+
|
56
|
+
## Options
|
57
|
+
|
58
|
+
| Option | Type | Description |
|
59
|
+
| --- | --- | --- |
|
60
|
+
| `:message` | String | A custom error message when the email format is invalid (default is: "does not appear to be a valid email address") |
|
61
|
+
| `:check_mx` | Boolean | Check domain for a valid MX record (default is false) |
|
62
|
+
| `:check_mx_timeout` | Integer | Timeout in seconds for checking MX records before a `ResolvTimeout` is raised (default is 3). |
|
63
|
+
| `:idn` | Boolean | Allowed internationalized domain names like `test@exämple.com` and `test@пример.рф`. Otherwise only domains that have already been converted to [Punycode](https://en.wikipedia.org/wiki/Punycode) are supported. (default is true) |
|
64
|
+
| `:mx_message` | String | A custom error message when the domain does not match a valid MX record (default is: "is not routable"). Ignored unless :check_mx option is true. |
|
65
|
+
| `:local_length` |Integer | Maximum number of characters allowed in the local part (everything before the '@') (default is 64) |
|
66
|
+
| `:domain_length` | Integer | Maximum number of characters allowed in the domain part (everything after the '@') (default is 255) |
|
67
|
+
| `:generate_message` | Boolean | Return the I18n key of the error message instead of the error message itself (default is false) |
|
68
|
+
|
69
|
+
The standard ActiveModel validation options (`:on`, `:if`, `:unless`, `:allow_nil`, `:allow_blank`, etc...) all work as well when using the gem as part of a Rails application.
|
70
|
+
## Testing
|
71
|
+
|
72
|
+
You can see our [current Ruby and Rails test matrix here](.github/workflows/ci.yml).
|
73
|
+
|
74
|
+
To execute the unit tests against [all the Rails versions we support run](gemfiles/) <tt>bundle exec appraisal rspec</tt> or run against an individual version with <tt>bundle exec appraisal rails-6.0 rspec</tt>.
|
75
|
+
## Contributing
|
76
|
+
|
77
|
+
If you think we're letting some rules about valid email formats slip through the cracks, don't just update the parser. Instead, add a failing test and demonstrate that the described email address should be treated differently. A link to an appropriate RFC is the best way to do this. Then change the gem code to make the test pass.
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
describe "i_think_this_is_not_a_v@lid_email_addre.ss" do
|
81
|
+
# According to http://..., this email address IS NOT valid.
|
82
|
+
it { should have_errors_on_email.because("does not appear to be valid") }
|
83
|
+
end
|
84
|
+
|
85
|
+
describe "i_think_this_is_a_v@lid_email_addre.ss" do
|
86
|
+
# According to http://..., this email address IS valid.
|
87
|
+
it { should_not have_errors_on_email }
|
88
|
+
end
|
89
|
+
```
|
90
|
+
|
91
|
+
Yes, our Rspec syntax is that simple!
|
92
|
+
|
93
|
+
## Homepage
|
94
|
+
|
95
|
+
* https://github.com/validates-email-format-of/validates_email_format_of
|
96
|
+
|
97
|
+
## Credits
|
98
|
+
|
99
|
+
Written by Alex Dunae (dunae.ca), 2006-22.
|
100
|
+
|
101
|
+
Many thanks to the plugin's recent contributors: https://github.com/alexdunae/validates_email_format_of/contributors
|
102
|
+
|
103
|
+
Thanks to Francis Hwang (http://fhwang.net/) at Diversion Media for creating the 1.1 update.
|
104
|
+
|
105
|
+
Thanks to Travis Sinnott for creating the 1.3 update.
|
106
|
+
|
107
|
+
Thanks to Denis Ahearn at Riverock Technologies (http://www.riverocktech.com/) for creating the 1.4 update.
|
108
|
+
|
109
|
+
Thanks to George Anderson (http://github.com/george) and 'history' (http://github.com/history) for creating the 1.4.1 update.
|
110
|
+
|
111
|
+
Thanks to Isaac Betesh (https://github.com/betesh) for converting tests to Rspec and refactoring for version 1.6.0.
|
data/config/locales/de.yml
CHANGED
@@ -2,7 +2,7 @@ de:
|
|
2
2
|
activemodel: &errors
|
3
3
|
errors:
|
4
4
|
messages:
|
5
|
-
invalid_email_address: 'ist offensichtlich keine gültige
|
5
|
+
invalid_email_address: 'ist offensichtlich keine gültige EMail-Adresse'
|
6
6
|
email_address_not_routable: 'kann nicht erreicht werden'
|
7
7
|
activerecord:
|
8
8
|
<<: *errors
|
data/config/locales/en.yml
CHANGED
@@ -2,7 +2,7 @@ en:
|
|
2
2
|
activemodel: &errors
|
3
3
|
errors:
|
4
4
|
messages:
|
5
|
-
invalid_email_address: 'does not appear to be a valid
|
5
|
+
invalid_email_address: 'does not appear to be a valid email address'
|
6
6
|
email_address_not_routable: 'is not routable'
|
7
7
|
activerecord:
|
8
8
|
<<: *errors
|
data/config/locales/it.yml
CHANGED
data/config/locales/pl.yml
CHANGED
data/config/locales/pt-BR.yml
CHANGED
@@ -2,7 +2,7 @@ pt-BR:
|
|
2
2
|
activemodel: &errors
|
3
3
|
errors:
|
4
4
|
messages:
|
5
|
-
invalid_email_address: 'não parece ser um endereço de
|
5
|
+
invalid_email_address: 'não parece ser um endereço de email válido'
|
6
6
|
email_address_not_routable: 'não é acessível'
|
7
7
|
activerecord:
|
8
8
|
<<: *errors
|
data/config/locales/pt.yml
CHANGED
@@ -9,7 +9,7 @@ module ActiveModel
|
|
9
9
|
module Validations
|
10
10
|
class EmailFormatValidator < EachValidator
|
11
11
|
def validate_each(record, attribute, value)
|
12
|
-
(ValidatesEmailFormatOf.validate_email_format(value, options
|
12
|
+
(ValidatesEmailFormatOf.validate_email_format(value, options) || []).each do |error|
|
13
13
|
record.errors.add(attribute, error)
|
14
14
|
end
|
15
15
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require "validates_email_format_of/version"
|
2
|
+
require "simpleidn"
|
2
3
|
|
3
4
|
module ValidatesEmailFormatOf
|
4
5
|
def self.load_i18n_locales
|
@@ -77,7 +78,7 @@ module ValidatesEmailFormatOf
|
|
77
78
|
# > restriction on the first character is relaxed to allow either a
|
78
79
|
# > letter or a digit. Host software MUST support this more liberal
|
79
80
|
# > syntax.
|
80
|
-
DOMAIN_PART_LABEL = /\A[A-Za-z0-9][A-Za-z0-9
|
81
|
+
DOMAIN_PART_LABEL = /\A[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9]?\Z/
|
81
82
|
|
82
83
|
# From https://tools.ietf.org/id/draft-liman-tld-names-00.html#rfc.section.2
|
83
84
|
#
|
@@ -92,10 +93,12 @@ module ValidatesEmailFormatOf
|
|
92
93
|
# ld = ALPHA / DIGIT
|
93
94
|
# ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
|
94
95
|
# DIGIT = %x30-39 ; 0-9
|
95
|
-
DOMAIN_PART_TLD = /\A[A-Za-z][A-Za-z0-9
|
96
|
+
DOMAIN_PART_TLD = /\A[A-Za-z][A-Za-z0-9-]*[A-Za-z0-9]\Z/
|
96
97
|
|
97
|
-
def self.validate_email_domain(email, check_mx_timeout: 3)
|
98
|
+
def self.validate_email_domain(email, idn: true, check_mx_timeout: 3)
|
98
99
|
domain = email.to_s.downcase.match(/@(.+)/)[1]
|
100
|
+
domain = SimpleIDN.to_ascii(domain) if idn
|
101
|
+
|
99
102
|
Resolv::DNS.open do |dns|
|
100
103
|
dns.timeouts = check_mx_timeout
|
101
104
|
@mx = dns.getresources(domain, Resolv::DNS::Resource::IN::MX) + dns.getresources(domain, Resolv::DNS::Resource::IN::A)
|
@@ -119,8 +122,8 @@ module ValidatesEmailFormatOf
|
|
119
122
|
# * <tt>message</tt> - A custom error message (default is: "does not appear to be valid")
|
120
123
|
# * <tt>check_mx</tt> - Check for MX records (default is false)
|
121
124
|
# * <tt>check_mx_timeout</tt> - Timeout in seconds for checking MX records before a `ResolvTimeout` is raised (default is 3)
|
125
|
+
# * <tt>idn</tt> - Enable or disable Internationalized Domain Names (default is true)
|
122
126
|
# * <tt>mx_message</tt> - A custom error message when an MX record validation fails (default is: "is not routable.")
|
123
|
-
# * <tt>with</tt> The regex to use for validating the format of the email address (deprecated)
|
124
127
|
# * <tt>local_length</tt> Maximum number of characters allowed in the local part (default is 64)
|
125
128
|
# * <tt>domain_length</tt> Maximum number of characters allowed in the domain part (default is 255)
|
126
129
|
# * <tt>generate_message</tt> Return the I18n key of the error message instead of the error message itself (default is false)
|
@@ -128,6 +131,7 @@ module ValidatesEmailFormatOf
|
|
128
131
|
default_options = {message: options[:generate_message] ? ERROR_MESSAGE_I18N_KEY : default_message,
|
129
132
|
check_mx: false,
|
130
133
|
check_mx_timeout: 3,
|
134
|
+
idn: true,
|
131
135
|
mx_message: if options[:generate_message]
|
132
136
|
ERROR_MX_MESSAGE_I18N_KEY
|
133
137
|
else
|
@@ -154,12 +158,13 @@ module ValidatesEmailFormatOf
|
|
154
158
|
domain.reverse!
|
155
159
|
|
156
160
|
if opts.has_key?(:with) # holdover from versions <= 1.4.7
|
161
|
+
deprecation_warn(":with option is deprecated and will be removed in the next version")
|
157
162
|
return [opts[:message]] unless email&.match?(opts[:with])
|
158
163
|
else
|
159
|
-
return [opts[:message]] unless validate_local_part_syntax(local) && validate_domain_part_syntax(domain)
|
164
|
+
return [opts[:message]] unless validate_local_part_syntax(local) && validate_domain_part_syntax(domain, idn: opts[:idn])
|
160
165
|
end
|
161
166
|
|
162
|
-
if opts[:check_mx] && !validate_email_domain(email, check_mx_timeout: opts[:check_mx_timeout])
|
167
|
+
if opts[:check_mx] && !validate_email_domain(email, check_mx_timeout: opts[:check_mx_timeout], idn: opts[:idn])
|
163
168
|
return [opts[:mx_message]]
|
164
169
|
end
|
165
170
|
|
@@ -171,6 +176,19 @@ module ValidatesEmailFormatOf
|
|
171
176
|
in_quoted_string = false
|
172
177
|
comment_depth = 0
|
173
178
|
|
179
|
+
# The local part is made up of dot-atom and quoted-string joined together by "." characters
|
180
|
+
#
|
181
|
+
# https://www.rfc-editor.org/rfc/rfc5322#section-3.4.1
|
182
|
+
# > local-part = dot-atom / quoted-string / obs-local-part
|
183
|
+
#
|
184
|
+
# https://www.rfc-editor.org/rfc/rfc5322#section-3.2.3
|
185
|
+
# Both atom and dot-atom are interpreted as a single unit, comprising
|
186
|
+
# > the string of characters that make it up. Semantically, the optional
|
187
|
+
# > comments and FWS surrounding the rest of the characters are not part
|
188
|
+
# > of the atom; the atom is only the run of atext characters in an atom,
|
189
|
+
# > or the atext and "." characters in a dot-atom.
|
190
|
+
joining_atoms = true
|
191
|
+
|
174
192
|
(0..local.length - 1).each do |i|
|
175
193
|
ord = local[i].ord
|
176
194
|
|
@@ -180,6 +198,32 @@ module ValidatesEmailFormatOf
|
|
180
198
|
next
|
181
199
|
end
|
182
200
|
|
201
|
+
# double quote delimits quoted strings
|
202
|
+
if ord == 34
|
203
|
+
if in_quoted_string # leaving the quoted string
|
204
|
+
in_quoted_string = false
|
205
|
+
next
|
206
|
+
elsif joining_atoms # are we allowed to enter a quoted string?
|
207
|
+
in_quoted_string = true
|
208
|
+
joining_atoms = false
|
209
|
+
next
|
210
|
+
else
|
211
|
+
return false
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
# period indicates we want to join atoms, e.g. `aaa.bbb."ccc"@example.com
|
216
|
+
if ord == 46
|
217
|
+
return false if i.zero?
|
218
|
+
return false if joining_atoms
|
219
|
+
joining_atoms = true
|
220
|
+
next
|
221
|
+
end
|
222
|
+
|
223
|
+
joining_atoms = false
|
224
|
+
|
225
|
+
# quoted string logic must come before comment processing since a quoted string
|
226
|
+
# may contain parens, e.g. `"name(a)"@example.com`
|
183
227
|
if in_quoted_string
|
184
228
|
next if QTEXT.match?(local[i])
|
185
229
|
end
|
@@ -198,40 +242,34 @@ module ValidatesEmailFormatOf
|
|
198
242
|
end
|
199
243
|
|
200
244
|
# backslash signifies the start of a quoted pair
|
201
|
-
if ord == 92
|
202
|
-
|
245
|
+
if ord == 92
|
246
|
+
# https://www.rfc-editor.org/rfc/rfc5322#section-3.2.1
|
247
|
+
# > The only places in this specification where quoted-pair currently appears are
|
248
|
+
# > ccontent, qcontent, and in obs-dtext in section 4.
|
249
|
+
return false unless in_quoted_string || comment_depth > 0
|
203
250
|
in_quoted_pair = true
|
204
251
|
next
|
205
252
|
end
|
206
253
|
|
207
|
-
# double quote delimits quoted strings
|
208
|
-
if ord == 34
|
209
|
-
in_quoted_string = !in_quoted_string
|
210
|
-
next
|
211
|
-
end
|
212
|
-
|
213
254
|
if comment_depth > 0
|
214
255
|
next if CTEXT.match?(local[i])
|
215
256
|
elsif ATEXT.match?(local[i, 1])
|
216
257
|
next
|
217
258
|
end
|
218
259
|
|
219
|
-
# period must be followed by something
|
220
|
-
if ord == 46
|
221
|
-
return false if i == 0 || i == local.length - 1 # can't be first or last char
|
222
|
-
next unless local[i + 1].ord == 46 # can't be followed by a period
|
223
|
-
end
|
224
|
-
|
225
260
|
return false
|
226
261
|
end
|
227
262
|
|
263
|
+
return false if in_quoted_pair # unbalanced quoted pair
|
228
264
|
return false if in_quoted_string # unbalanced quotes
|
229
265
|
return false unless comment_depth.zero? # unbalanced comment parens
|
266
|
+
return false if joining_atoms # the last char we encountered was a period
|
230
267
|
|
231
268
|
true
|
232
269
|
end
|
233
270
|
|
234
|
-
def self.validate_domain_part_syntax(domain)
|
271
|
+
def self.validate_domain_part_syntax(domain, idn: true)
|
272
|
+
domain = SimpleIDN.to_ascii(domain) if idn
|
235
273
|
parts = domain.downcase.split(".", -1)
|
236
274
|
|
237
275
|
return false if parts.length <= 1 # Only one domain part
|
@@ -256,6 +294,14 @@ module ValidatesEmailFormatOf
|
|
256
294
|
return false unless DOMAIN_PART_TLD.match?(parts[-1])
|
257
295
|
true
|
258
296
|
end
|
297
|
+
|
298
|
+
def self.deprecation_warn(msg)
|
299
|
+
if defined?(ActiveSupport::Deprecation)
|
300
|
+
ActiveSupport::Deprecation.warn(msg)
|
301
|
+
else
|
302
|
+
warn
|
303
|
+
end
|
304
|
+
end
|
259
305
|
end
|
260
306
|
|
261
307
|
require "validates_email_format_of/active_model" if defined?(::ActiveModel) && !(ActiveModel::VERSION::MAJOR < 2 || (ActiveModel::VERSION::MAJOR == 2 && ActiveModel::VERSION::MINOR < 1))
|
data/spec/spec_helper.rb
CHANGED
@@ -4,7 +4,9 @@ require "pry"
|
|
4
4
|
require "byebug"
|
5
5
|
|
6
6
|
RSpec::Matchers.define :have_errors_on_email do
|
7
|
-
match do |
|
7
|
+
match do |user|
|
8
|
+
actual = user.errors.full_messages
|
9
|
+
expect(user.errors.added?(:email, ValidatesEmailFormatOf::ERROR_MESSAGE_I18N_KEY))
|
8
10
|
expect(actual).not_to be_nil, "#{actual} should not be nil"
|
9
11
|
expect(actual).not_to be_empty, "#{actual} should not be empty"
|
10
12
|
expect(actual.size).to eq(@reasons.size), "#{actual} should have #{@reasons.size} elements"
|
@@ -19,7 +21,7 @@ RSpec::Matchers.define :have_errors_on_email do
|
|
19
21
|
chain :and_because do |reason|
|
20
22
|
(@reasons ||= []) << reason
|
21
23
|
end
|
22
|
-
match_when_negated do |
|
23
|
-
expect(
|
24
|
+
match_when_negated do |user|
|
25
|
+
expect(user.errors).to(be_empty)
|
24
26
|
end
|
25
27
|
end
|
@@ -22,7 +22,7 @@ describe ValidatesEmailFormatOf do
|
|
22
22
|
ActiveModel::Name.new(self, nil, "User")
|
23
23
|
end
|
24
24
|
end
|
25
|
-
user.new(example.example_group_instance.email).tap(&:valid?)
|
25
|
+
user.new(example.example_group_instance.email).tap(&:valid?)
|
26
26
|
end
|
27
27
|
let(:options) { {} }
|
28
28
|
let(:email) { |example| example.example_group.description }
|
@@ -58,13 +58,24 @@ describe ValidatesEmailFormatOf do
|
|
58
58
|
"_somename@example.com",
|
59
59
|
# apostrophes
|
60
60
|
"test'test@example.com",
|
61
|
-
#
|
61
|
+
# punycode domain names
|
62
62
|
"test@xn--bcher-kva.ch",
|
63
63
|
"test@example.xn--0zwm56d",
|
64
|
+
|
65
|
+
# IDN domains,
|
66
|
+
"test@exämple.com",
|
67
|
+
"test@пример.рф",
|
68
|
+
"test@почта.бел",
|
69
|
+
|
64
70
|
"test@192.192.192.1",
|
65
71
|
# Allow quoted characters. Valid according to http://www.rfc-editor.org/errata_search.php?rfc=3696
|
66
72
|
'"Abc\@def"@example.com',
|
73
|
+
"\"quote\".dotatom.\"otherquote\"@example.com",
|
67
74
|
'"Quote(Only".Chars@wier.de',
|
75
|
+
"\"much.more unusual\"@example.com",
|
76
|
+
"\"very.unusual.@.unusual.com\"@example.com",
|
77
|
+
'"very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com',
|
78
|
+
'"()<>[]:,;@\"!#$%&*+-/=?^_`{}| ~ ? ^_`{}|~.a"@example.org',
|
68
79
|
'"Fred\ Bloggs"@example.com',
|
69
80
|
'"Joe.\\Blow"@example.com',
|
70
81
|
# Balanced quoted characters
|
@@ -98,6 +109,7 @@ describe ValidatesEmailFormatOf do
|
|
98
109
|
"invalid@example.com-",
|
99
110
|
"invalid-example.com",
|
100
111
|
"invalid@example.b#r.com",
|
112
|
+
"just\"not\"right@example.com",
|
101
113
|
"invalid@example.c",
|
102
114
|
"invali d@example.com",
|
103
115
|
# TLD can not be only numeric
|
@@ -131,7 +143,7 @@ describe ValidatesEmailFormatOf do
|
|
131
143
|
"(unbalancedcomment@example.com"
|
132
144
|
].each do |address|
|
133
145
|
describe address do
|
134
|
-
it { should have_errors_on_email.because("does not appear to be a valid
|
146
|
+
it { should have_errors_on_email.because("does not appear to be a valid email address") }
|
135
147
|
end
|
136
148
|
end
|
137
149
|
|
@@ -141,7 +153,7 @@ describe ValidatesEmailFormatOf do
|
|
141
153
|
it { should_not have_errors_on_email }
|
142
154
|
end
|
143
155
|
describe "#{"a" * (limit + 1)}@example.com" do
|
144
|
-
it { should have_errors_on_email.because("does not appear to be a valid
|
156
|
+
it { should have_errors_on_email.because("does not appear to be a valid email address") }
|
145
157
|
end
|
146
158
|
end
|
147
159
|
describe "when using default" do
|
@@ -158,7 +170,7 @@ describe ValidatesEmailFormatOf do
|
|
158
170
|
it { should_not have_errors_on_email }
|
159
171
|
end
|
160
172
|
describe "user@#{"a." * (limit / 2 + 1)}com" do
|
161
|
-
it { should have_errors_on_email.because("does not appear to be a valid
|
173
|
+
it { should have_errors_on_email.because("does not appear to be a valid email address") }
|
162
174
|
end
|
163
175
|
end
|
164
176
|
describe "when using default" do
|
@@ -179,6 +191,26 @@ describe ValidatesEmailFormatOf do
|
|
179
191
|
end
|
180
192
|
end
|
181
193
|
|
194
|
+
describe "when idn support is disabled" do
|
195
|
+
before(:each) do
|
196
|
+
allow(SimpleIDN).to receive(:to_ascii).never
|
197
|
+
end
|
198
|
+
let(:options) { {idn: false} }
|
199
|
+
describe "test@exämple.com" do
|
200
|
+
it { should have_errors_on_email.because("does not appear to be a valid email address") }
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
describe "when idn support is enabled" do
|
205
|
+
before(:each) do
|
206
|
+
allow(SimpleIDN).to receive(:to_ascii).once.with("exämple.com").and_return("xn--exmple-cua.com")
|
207
|
+
end
|
208
|
+
let(:options) { {idn: true} }
|
209
|
+
describe "test@exämple.com" do
|
210
|
+
it { should_not have_errors_on_email }
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
182
214
|
describe "mx record" do
|
183
215
|
domain = "example.com"
|
184
216
|
email = "valid@#{domain}"
|
@@ -216,6 +248,9 @@ describe ValidatesEmailFormatOf do
|
|
216
248
|
let(:mx_record) { [] }
|
217
249
|
describe email do
|
218
250
|
it { should have_errors_on_email.because("is not routable") }
|
251
|
+
it "adds the i18n key" do
|
252
|
+
subject.errors.added?(:email, ValidatesEmailFormatOf::ERROR_MX_MESSAGE_I18N_KEY)
|
253
|
+
end
|
219
254
|
end
|
220
255
|
describe "with a custom error message" do
|
221
256
|
let(:options) { {check_mx: true, mx_message: "There ain't no such domain!"} }
|
@@ -223,6 +258,7 @@ describe ValidatesEmailFormatOf do
|
|
223
258
|
it { should have_errors_on_email.because("There ain't no such domain!") }
|
224
259
|
end
|
225
260
|
end
|
261
|
+
|
226
262
|
describe "i18n" do
|
227
263
|
before(:each) do
|
228
264
|
allow(I18n.config).to receive(:locale).and_return(locale)
|
@@ -236,6 +272,7 @@ describe ValidatesEmailFormatOf do
|
|
236
272
|
end
|
237
273
|
end
|
238
274
|
end
|
275
|
+
|
239
276
|
describe "when not testing" do
|
240
277
|
before(:each) { allow(Resolv::DNS).to receive(:open).never }
|
241
278
|
describe "by default" do
|
@@ -258,13 +295,43 @@ describe ValidatesEmailFormatOf do
|
|
258
295
|
end
|
259
296
|
end
|
260
297
|
|
298
|
+
describe "mx record for internationalized domain" do
|
299
|
+
domain = "пример.рф"
|
300
|
+
email = "valid@#{domain}"
|
301
|
+
|
302
|
+
describe "when idn support is enabled" do
|
303
|
+
let(:dns) { double(Resolv::DNS) }
|
304
|
+
let(:options) { {check_mx: true, idn: true} }
|
305
|
+
|
306
|
+
before(:each) do
|
307
|
+
allow(Resolv::DNS).to receive(:open).and_yield(dns)
|
308
|
+
allow(dns).to receive(:"timeouts=").with(3).once
|
309
|
+
allow(dns).to receive(:getresources).with(SimpleIDN.to_ascii(domain), Resolv::DNS::Resource::IN::A).once.and_return([double])
|
310
|
+
allow(dns).to receive(:getresources).with(SimpleIDN.to_ascii(domain), Resolv::DNS::Resource::IN::MX).once.and_return([double])
|
311
|
+
end
|
312
|
+
|
313
|
+
describe email do
|
314
|
+
it { should_not have_errors_on_email }
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
describe "when idn support is disabled" do
|
319
|
+
let(:options) { {check_mx: true, idn: false} }
|
320
|
+
|
321
|
+
describe "test@пример.рф" do
|
322
|
+
let(:domain) { "exämple.com" }
|
323
|
+
it { should have_errors_on_email.because("does not appear to be a valid email address") }
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
261
328
|
describe "custom regex" do
|
262
329
|
let(:options) { {with: /[0-9]+@[0-9]+/} }
|
263
330
|
describe "012345@789" do
|
264
331
|
it { should_not have_errors_on_email }
|
265
332
|
end
|
266
333
|
describe "valid@example.com" do
|
267
|
-
it { should have_errors_on_email.because("does not appear to be a valid
|
334
|
+
it { should have_errors_on_email.because("does not appear to be a valid email address") }
|
268
335
|
end
|
269
336
|
end
|
270
337
|
|
@@ -275,7 +342,7 @@ describe ValidatesEmailFormatOf do
|
|
275
342
|
describe "present locale" do
|
276
343
|
let(:locale) { :pl }
|
277
344
|
describe "invalid@exmaple." do
|
278
|
-
it { should have_errors_on_email.because("nieprawidłowy adres
|
345
|
+
it { should have_errors_on_email.because("nieprawidłowy adres email") }
|
279
346
|
end
|
280
347
|
end
|
281
348
|
end
|
@@ -320,7 +387,7 @@ describe ValidatesEmailFormatOf do
|
|
320
387
|
ActiveModel::Name.new(self, nil, "User")
|
321
388
|
end
|
322
389
|
end
|
323
|
-
user.new(example.example_group_instance.email).tap(&:valid?)
|
390
|
+
user.new(example.example_group_instance.email).tap(&:valid?)
|
324
391
|
end
|
325
392
|
|
326
393
|
it_should_behave_like :all_specs
|
@@ -4,7 +4,7 @@ require "validates_email_format_of/version"
|
|
4
4
|
Gem::Specification.new do |s|
|
5
5
|
s.name = "validates_email_format_of"
|
6
6
|
s.version = ValidatesEmailFormatOf::VERSION
|
7
|
-
s.summary = "Validate
|
7
|
+
s.summary = "Validate email addresses against RFC 2822 and RFC 3696."
|
8
8
|
s.description = s.summary
|
9
9
|
s.authors = ["Alex Dunae", "Isaac Betesh"]
|
10
10
|
s.email = ["code@dunae.ca", "iybetesh@gmail.com"]
|
@@ -16,9 +16,10 @@ Gem::Specification.new do |s|
|
|
16
16
|
if RUBY_VERSION < "1.9.3"
|
17
17
|
s.add_dependency "i18n", "< 0.7.0"
|
18
18
|
else
|
19
|
-
s.add_dependency "i18n"
|
19
|
+
s.add_dependency "i18n", ">= 0.8.0"
|
20
20
|
end
|
21
21
|
|
22
|
+
s.add_dependency "simpleidn"
|
22
23
|
s.add_development_dependency "activemodel"
|
23
24
|
s.add_development_dependency "bundler"
|
24
25
|
s.add_development_dependency "rspec"
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: validates_email_format_of
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alex Dunae
|
@@ -9,10 +9,24 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2024-03-11 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: i18n
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ">="
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: 0.8.0
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: 0.8.0
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: simpleidn
|
16
30
|
requirement: !ruby/object:Gem::Requirement
|
17
31
|
requirements:
|
18
32
|
- - ">="
|
@@ -109,7 +123,7 @@ dependencies:
|
|
109
123
|
- - ">="
|
110
124
|
- !ruby/object:Gem::Version
|
111
125
|
version: '0'
|
112
|
-
description: Validate
|
126
|
+
description: Validate email addresses against RFC 2822 and RFC 3696.
|
113
127
|
email:
|
114
128
|
- code@dunae.ca
|
115
129
|
- iybetesh@gmail.com
|
@@ -117,6 +131,7 @@ executables: []
|
|
117
131
|
extensions: []
|
118
132
|
extra_rdoc_files: []
|
119
133
|
files:
|
134
|
+
- ".github/dependabot.yml"
|
120
135
|
- ".github/workflows/ci.yml"
|
121
136
|
- ".gitignore"
|
122
137
|
- ".rspec"
|
@@ -124,15 +139,17 @@ files:
|
|
124
139
|
- CHANGELOG.md
|
125
140
|
- Gemfile
|
126
141
|
- MIT-LICENSE
|
127
|
-
- README.
|
142
|
+
- README.md
|
128
143
|
- config/locales/de.yml
|
129
144
|
- config/locales/en.yml
|
130
145
|
- config/locales/fr.yml
|
146
|
+
- config/locales/id.yml
|
131
147
|
- config/locales/it.yml
|
132
148
|
- config/locales/ja.yml
|
133
149
|
- config/locales/pl.yml
|
134
150
|
- config/locales/pt-BR.yml
|
135
151
|
- config/locales/pt.yml
|
152
|
+
- config/locales/tr.yml
|
136
153
|
- gemfiles/rails_4.2.gemfile
|
137
154
|
- gemfiles/rails_5.0.gemfile
|
138
155
|
- gemfiles/rails_5.1.gemfile
|
@@ -140,6 +157,7 @@ files:
|
|
140
157
|
- gemfiles/rails_6.0.gemfile
|
141
158
|
- gemfiles/rails_6.1.gemfile
|
142
159
|
- gemfiles/rails_7.0.gemfile
|
160
|
+
- gemfiles/rails_7.1.gemfile
|
143
161
|
- lib/validates_email_format_of.rb
|
144
162
|
- lib/validates_email_format_of/active_model.rb
|
145
163
|
- lib/validates_email_format_of/railtie.rb
|
@@ -169,8 +187,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
169
187
|
- !ruby/object:Gem::Version
|
170
188
|
version: '0'
|
171
189
|
requirements: []
|
172
|
-
rubygems_version: 3.
|
190
|
+
rubygems_version: 3.4.21
|
173
191
|
signing_key:
|
174
192
|
specification_version: 4
|
175
|
-
summary: Validate
|
193
|
+
summary: Validate email addresses against RFC 2822 and RFC 3696.
|
176
194
|
test_files: []
|
data/README.rdoc
DELETED
@@ -1,100 +0,0 @@
|
|
1
|
-
= validates_email_format_of Gem and Rails Plugin
|
2
|
-
|
3
|
-
Validate e-mail addresses against RFC 2822 and RFC 3696.
|
4
|
-
|
5
|
-
== Installation
|
6
|
-
|
7
|
-
Installing as a gem:
|
8
|
-
|
9
|
-
gem install validates_email_format_of
|
10
|
-
|
11
|
-
Or in your Gemfile:
|
12
|
-
|
13
|
-
gem 'validates_email_format_of'
|
14
|
-
|
15
|
-
== Usage
|
16
|
-
|
17
|
-
# Rails
|
18
|
-
# I18n locales are loaded automatically.
|
19
|
-
class Person < ActiveRecord::Base
|
20
|
-
validates_email_format_of :email, :message => 'is not looking good'
|
21
|
-
# OR
|
22
|
-
validates :email, :email_format => { :message => 'is not looking good' }
|
23
|
-
end
|
24
|
-
|
25
|
-
# Now you can test your model using RSpec:
|
26
|
-
require "validates_email_format_of/rspec_matcher"
|
27
|
-
describe Person do
|
28
|
-
it { should validate_email_format_of(:email).with_message('is not looking good') }
|
29
|
-
end
|
30
|
-
|
31
|
-
# If you're not using Rails (which really means, if you're not using ActiveModel::Validations)
|
32
|
-
ValidatesEmailFormatOf::load_i18n_locales # Optional, if you want error messages to be in your language
|
33
|
-
I18n.locale = :pl # If, for example, you want Polish error messages.
|
34
|
-
ValidatesEmailFormatOf::validate_email_format("example@mydomain.com") # => nil
|
35
|
-
ValidatesEmailFormatOf::validate_email_format("invalid_because_there_is_no_at_symbol") # => ["does not appear to be a valid e-mail address"]
|
36
|
-
|
37
|
-
=== Options
|
38
|
-
|
39
|
-
:message
|
40
|
-
String. A custom error message when the email format is invalid (default is: "does not appear to be a valid e-mail address")
|
41
|
-
:check_mx
|
42
|
-
Boolean. Check domain for a valid MX record (default is false)
|
43
|
-
:check_mx_timeout
|
44
|
-
Integer. Timeout in seconds for checking MX records before a `ResolvTimeout` is raised (default is 3).
|
45
|
-
:mx_message
|
46
|
-
String. A custom error message when the domain does not match a valid MX record (default is: "is not routable"). Ignored unless :check_mx option is true.
|
47
|
-
:local_length
|
48
|
-
Maximum number of characters allowed in the local part (everything before the '@') (default is 64)
|
49
|
-
:domain_length
|
50
|
-
Maximum number of characters allowed in the domain part (everything after the '@') (default is 255)
|
51
|
-
:generate_message
|
52
|
-
Boolean. Return the I18n key of the error message instead of the error message itself (default is false)
|
53
|
-
:with
|
54
|
-
Specify a custom Regex as the valid email format.
|
55
|
-
:on, :if, :unless, :allow_nil, :allow_blank, :strict
|
56
|
-
Standard ActiveModel validation options. These work in the ActiveModel/ActiveRecord/Rails syntax only.
|
57
|
-
See http://api.rubyonrails.org/classes/ActiveModel/Validations/ClassMethods.html#method-i-validates for details.
|
58
|
-
|
59
|
-
== Testing
|
60
|
-
|
61
|
-
To execute the unit tests against [all the Rails versions we support run](gemfiles/) <tt>bundle exec appraisal rspec</tt> or run against an individual version with <tt>bundle exec appraisal rails-6.0 rspec</tt>.
|
62
|
-
|
63
|
-
Tested in Ruby 1.8.7, 1.9.2, 1.9.3, 2.0.0, 2.1.2, JRuby and REE 1.8.7.
|
64
|
-
|
65
|
-
== Contributing
|
66
|
-
|
67
|
-
If you think we're letting some rules about valid email formats slip through the cracks, don't just update the Regex.
|
68
|
-
Instead, add a failing test, and demonstrate that the described email address should be treated differently. A link to an appropriate RFC is the best way to do this.
|
69
|
-
Then change the gem code to make the test pass.
|
70
|
-
|
71
|
-
describe "i_think_this_is_not_a_v@lid_email_addre.ss" do
|
72
|
-
# According to http://..., this email address IS NOT valid.
|
73
|
-
it { should have_errors_on_email.because("does not appear to be valid") }
|
74
|
-
end
|
75
|
-
describe "i_think_this_is_a_v@lid_email_addre.ss" do
|
76
|
-
# According to http://..., this email address IS valid.
|
77
|
-
it { should_not have_errors_on_email }
|
78
|
-
end
|
79
|
-
|
80
|
-
Yes, our Rspec syntax is that simple!
|
81
|
-
|
82
|
-
== Homepage
|
83
|
-
|
84
|
-
* https://github.com/validates-email-format-of/validates_email_format_of
|
85
|
-
|
86
|
-
== Credits
|
87
|
-
|
88
|
-
Written by Alex Dunae (dunae.ca), 2006-22.
|
89
|
-
|
90
|
-
Many thanks to the plugin's recent contributors: https://github.com/alexdunae/validates_email_format_of/contributors
|
91
|
-
|
92
|
-
Thanks to Francis Hwang (http://fhwang.net/) at Diversion Media for creating the 1.1 update.
|
93
|
-
|
94
|
-
Thanks to Travis Sinnott for creating the 1.3 update.
|
95
|
-
|
96
|
-
Thanks to Denis Ahearn at Riverock Technologies (http://www.riverocktech.com/) for creating the 1.4 update.
|
97
|
-
|
98
|
-
Thanks to George Anderson (http://github.com/george) and 'history' (http://github.com/history) for creating the 1.4.1 update.
|
99
|
-
|
100
|
-
Thanks to Isaac Betesh (https://github.com/betesh) for converting tests to Rspec and refactoring for version 1.6.0.
|