validates_email_format_of 1.7.2 → 1.8.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6220bc1f3cb06d9af63e0bcf8ccfcc6e1dc795a59a5c08cd817ac921a057585b
4
- data.tar.gz: 6c0f414549308510ad6434c71b3a1d0cabd88c4fc18cb3a3fb02135b862f3f5b
3
+ metadata.gz: 11c21b2a28ee1659e6727cc106aaf912487adf9b519eceeebd68f3dbb1750b88
4
+ data.tar.gz: 8db0acd0c16c49e5042640bf159aa75caad8f75b899a5df91527d66c899012aa
5
5
  SHA512:
6
- metadata.gz: 91e1f3eb18440c304f144d81b346134d9c6d7bc747e43fe464edeebb9e3a02e972e7fa4b4283d7b9eed6e4556f37a578d11bb4a9bb0328e93f5f2f2658625a30
7
- data.tar.gz: 2e3c8cc64f8d860c0fb1e7771197942901a4fae28b166376b613bbef6262275003bbad62b23e244a3a9887ecb99c5f1dad7bc3f3b973150beaa5774782baed2d
6
+ metadata.gz: c3fcec1122983e8bbcff3e8a388260d040851467dbb89d6a1157e38ab59fbe908c3b8c69793ac2166ee1cedaf1d78d0c6613fc4ac8c450fb523c45fda1821b59
7
+ data.tar.gz: dbc08e3633edf9d254e80d6614795a36f4c806046aa6dafb1c0aa46e9aa729f19701fbf533be9253138d119b35c8203fdea1a64a87c6c364e0e53ded94e23a7c
@@ -0,0 +1,6 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "github-actions"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "monthly"
@@ -1,10 +1,7 @@
1
1
  name: CI
2
2
 
3
3
  on:
4
- push:
5
- branches: [master]
6
- pull_request:
7
- branches: [master]
4
+ - pull_request
8
5
 
9
6
  permissions:
10
7
  contents: read
@@ -17,38 +14,78 @@ jobs:
17
14
 
18
15
  strategy:
19
16
  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"]
17
+ ruby: ["2.6", "2.7", "3.0", "3.1", "3.2", "3.3", "jruby-head"]
18
+ gemfile: ["4.2", "5.0", "5.1", "5.2", "6.0", "6.1", "7.0", "7.1"]
19
+
22
20
  exclude:
23
21
  - gemfile: "4.2"
24
22
  ruby: "3.0"
25
23
  - gemfile: "4.2"
26
24
  ruby: "3.1"
25
+ - gemfile: "4.2"
26
+ ruby: "3.2"
27
+ - gemfile: "4.2"
28
+ ruby: "3.3"
29
+ - gemfile: "4.2"
30
+ ruby: "jruby-head"
27
31
  - gemfile: "5.0"
28
32
  ruby: "3.0"
29
33
  - gemfile: "5.0"
30
34
  ruby: "3.1"
35
+ - gemfile: "5.0"
36
+ ruby: "3.2"
37
+ - gemfile: "5.0"
38
+ ruby: "3.3"
39
+ - gemfile: "5.0"
40
+ ruby: "jruby-head"
31
41
  - gemfile: "5.1"
32
42
  ruby: "3.0"
33
43
  - gemfile: "5.1"
34
44
  ruby: "3.1"
45
+ - gemfile: "5.1"
46
+ ruby: "3.2"
47
+ - gemfile: "5.1"
48
+ ruby: "3.3"
49
+ - gemfile: "5.1"
50
+ ruby: "jruby-head"
35
51
  - gemfile: "5.2"
36
52
  ruby: "3.0"
37
53
  - gemfile: "5.2"
38
54
  ruby: "3.1"
55
+ - gemfile: "5.2"
56
+ ruby: "3.2"
57
+ - gemfile: "5.2"
58
+ ruby: "3.3"
59
+ - gemfile: "5.2"
60
+ ruby: "jruby-head"
61
+ - gemfile: "6.0"
62
+ ruby: "3.2"
63
+ - gemfile: "6.0"
64
+ ruby: "3.3"
65
+ - gemfile: "6.1"
66
+ ruby: "3.2"
67
+ - gemfile: "6.1"
68
+ ruby: "3.3"
39
69
  - gemfile: "7.0"
40
70
  ruby: "2.5"
41
71
  - gemfile: "7.0"
42
72
  ruby: "2.6"
43
73
  - gemfile: "7.0"
44
74
  ruby: "2.7"
75
+ - gemfile: "7.1"
76
+ ruby: "2.5"
77
+ - gemfile: "7.1"
78
+ ruby: "2.6"
79
+ - gemfile: "7.1"
80
+ ruby: "2.7"
81
+
45
82
 
46
83
  env:
47
84
  BUNDLE_GEMFILE: gemfiles/rails_${{ matrix.gemfile }}.gemfile
48
85
  RAILS_ENV: test
49
86
 
50
87
  steps:
51
- - uses: actions/checkout@v3
88
+ - uses: actions/checkout@v4
52
89
 
53
90
  - name: "Install Ruby ${{ matrix.ruby }}"
54
91
  # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
@@ -65,3 +102,4 @@ jobs:
65
102
 
66
103
  - name: Run standard.rb
67
104
  run: bundle exec rake standard
105
+ if: ${{ ! startsWith(matrix.ruby, '2.') }}
data/Appraisals CHANGED
@@ -1,4 +1,8 @@
1
1
  # run `bundle exec appraisal install` after making changes here
2
+ appraise "rails-7.1" do
3
+ gem "rails", "~> 7.1"
4
+ end
5
+
2
6
  appraise "rails-7.0" do
3
7
  gem "rails", "~> 7.0"
4
8
  end
data/CHANGELOG.md CHANGED
@@ -2,13 +2,37 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
- [Unreleased]: https://github.com/validates-email-format-of/validates_email_format_of/compare/v1.7.2...master
5
+ [Unreleased]: https://github.com/validates-email-format-of/validates_email_format_of/compare/v1.8.2...master
6
+
7
+ ## [1.8.2]
8
+
9
+ * Improve German translations - https://github.com/validates-email-format-of/validates_email_format_of/pull/111
10
+
11
+ [1.8.2]: https://github.com/validates-email-format-of/validates_email_format_of/compare/v1.8.1...1.8.2
12
+
13
+ ## [1.8.1]
14
+
15
+ * Fix IDN->Punycode conversion when domain names start with periods - https://github.com/validates-email-format-of/validates_email_format_of/issues/109
16
+ * Add jruby to test matrix - https://github.com/validates-email-format-of/validates_email_format_of/pull/108
17
+
18
+ [1.8.1]: https://github.com/validates-email-format-of/validates_email_format_of/compare/v1.8.0...1.8.1
19
+
20
+ ## [1.8.0]
21
+
22
+ * Add Internationalized Domain Name support - https://github.com/validates-email-format-of/validates_email_format_of/pull/103 - thanks https://github.com/sbilharz !
23
+ * Add Turkish locale - https://github.com/validates-email-format-of/validates_email_format_of/pull/101 - thanks https://github.com/@krmbzds !
24
+ * Added Indonesian locale - https://github.com/validates-email-format-of/validates_email_format_of/commit/129ebfc3a3b432b4df0334bcbdd74b1d17d765e0 - thanks https://github.com/khoerodin !
25
+ * Fix inconsistent `generate_messages` behaviour - https://github.com/validates-email-format-of/validates_email_format_of/pull/105
26
+ * ⚠️ Deprecate `:with` option - https://github.com/validates-email-format-of/validates_email_format_of/issues/42
27
+ * Require i18n >= 0.8.0 in modern Ruby versions - https://github.com/advisories/GHSA-34hf-g744-jw64
28
+
29
+ [1.8.0]: https://github.com/validates-email-format-of/validates_email_format_of/compare/v1.7.2...1.8.0
6
30
 
7
31
  ## [1.7.2]
8
32
 
9
33
  * Fix regression that disallowed domains starting with number - https://github.com/validates-email-format-of/validates_email_format_of/issues/88
10
34
 
11
- [Unreleased]: https://github.com/validates-email-format-of/validates_email_format_of/compare/v1.7.1...v1.7.2
35
+ [1.7.2]: https://github.com/validates-email-format-of/validates_email_format_of/compare/v1.7.1...v1.7.2
12
36
 
13
37
  ## [1.7.1] (3 Aug 2022)
14
38
 
data/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # validates_email_format_of
2
+
3
+ [![Build Status](https://github.com/validates-email-format-of/validates_email_format_of/actions/workflows/ci.yml/badge.svg)]( 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, with optional domain name lookups.
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
+ ValidatesEmailFormatOf::validate_email_format("example@mydomain.com") # => nil
49
+ ValidatesEmailFormatOf::validate_email_format("invalid@") # => ["does not appear to be a valid email address"]
50
+
51
+ # Optional, if you want error messages to be in your language
52
+ ValidatesEmailFormatOf::load_i18n_locales
53
+ I18n.locale = :pl
54
+
55
+ ValidatesEmailFormatOf::validate_email_format("invalid@") # => ["nieprawidłowy adres email"]
56
+ ```
57
+
58
+ ## Internationalized Domain Names (IDN) and Punycode
59
+
60
+ As of v1.8.0, this gem can validate email addresses using internationalized domain names (like `test@пример.рф`) as well as domains that have already been converted to Punycode code (like `test@xn--test@-3weu6azakd.xn--p1ai`).
61
+
62
+ If you would like to forbid internationalized domains, you can pass the `idn: false` option. Punycode is always accepted.
63
+
64
+
65
+ ## Options
66
+
67
+ | Option | Type | Description |
68
+ | --- | --- | --- |
69
+ | `:message` | String | A custom error message when the email format is invalid (default is: "does not appear to be a valid email address") |
70
+ | `:check_mx` | Boolean | Check domain for a valid MX record (default is false) |
71
+ | `:check_mx_timeout` | Integer | Timeout in seconds for checking MX records before a `ResolvTimeout` is raised (default is 3). |
72
+ | `: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) |
73
+ | `: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. |
74
+ | `:local_length` |Integer | Maximum number of characters allowed in the local part (everything before the '@') (default is 64) |
75
+ | `:domain_length` | Integer | Maximum number of characters allowed in the domain part (everything after the '@') (default is 255) |
76
+ | `:generate_message` | Boolean | Return the I18n key of the error message instead of the error message itself (default is false) |
77
+
78
+ 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.
79
+ ## Testing
80
+
81
+ The gem is tested against Rails 4.2 and onward across a bunch of Ruby versions including jruby. You can see our [current Ruby and Rails test matrix here](.github/workflows/ci.yml).
82
+
83
+ 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>.
84
+ ## Contributing
85
+
86
+ 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.
87
+
88
+ ```ruby
89
+ describe "i_think_this_is_not_a_v@lid_email_addre.ss" do
90
+ # According to http://..., this email address IS NOT valid.
91
+ it { should have_errors_on_email.because("does not appear to be valid") }
92
+ end
93
+
94
+ describe "i_think_this_is_a_v@lid_email_addre.ss" do
95
+ # According to http://..., this email address IS valid.
96
+ it { should_not have_errors_on_email }
97
+ end
98
+ ```
99
+
100
+ Yes, our Rspec syntax is that simple!
101
+
102
+ ## Homepage
103
+
104
+ * https://github.com/validates-email-format-of/validates_email_format_of
105
+
106
+ ## Credits
107
+
108
+ Written by [Alex Dunae](https://dunae.ca), 2006-24.
109
+
110
+ Many thanks to the plugin's recent contributors: https://github.com/alexdunae/validates_email_format_of/contributors
111
+
112
+ Thanks to [Francis Hwang](http://fhwang.net/) at Diversion Media for creating the 1.1 update.
113
+
114
+ Thanks to Travis Sinnott for creating the 1.3 update.
115
+
116
+ Thanks to Denis Ahearn at [Riverock Technologies](http://www.riverocktech.com/) for creating the 1.4 update.
117
+
118
+ Thanks to [George Anderson](http://github.com/george) and ['history'](http://github.com/history) for creating the 1.4.1 update.
119
+
120
+ Thanks to [Isaac Betesh](https://github.com/betesh) for converting tests to Rspec and refactoring for version 1.6.0.
@@ -2,7 +2,7 @@ de:
2
2
  activemodel: &errors
3
3
  errors:
4
4
  messages:
5
- invalid_email_address: 'ist offensichtlich keine gültige E-Mail-Adresse'
5
+ invalid_email_address: 'ist anscheinend keine gültige E-Mail-Adresse'
6
6
  email_address_not_routable: 'kann nicht erreicht werden'
7
7
  activerecord:
8
8
  <<: *errors
@@ -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 e-mail address'
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
@@ -0,0 +1,8 @@
1
+ id:
2
+ activemodel: &errors
3
+ errors:
4
+ messages:
5
+ invalid_email_address: 'tampaknya bukan alamat email yang valid'
6
+ email_address_not_routable: 'tidak dapat dirutekan'
7
+ activerecord:
8
+ <<: *errors
@@ -2,7 +2,7 @@ it:
2
2
  activemodel: &errors
3
3
  errors:
4
4
  messages:
5
- invalid_email_address: 'non sembra un indirizzo e-mail valido'
5
+ invalid_email_address: 'non sembra un indirizzo email valido'
6
6
  email_address_not_routable: 'dominio non raggiungibile'
7
7
  activerecord:
8
8
  <<: *errors
@@ -2,7 +2,7 @@ pl:
2
2
  activemodel: &errors
3
3
  errors:
4
4
  messages:
5
- invalid_email_address: 'nieprawidłowy adres e-mail'
5
+ invalid_email_address: 'nieprawidłowy adres email'
6
6
  email_address_not_routable: 'jest nieosiągalny'
7
7
  activerecord:
8
8
  <<: *errors
@@ -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 e-mail válido'
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
@@ -2,7 +2,7 @@ pt:
2
2
  activemodel: &errors
3
3
  errors:
4
4
  messages:
5
- invalid_email_address: 'não parece ser um endereço de e-mail válido'
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
@@ -0,0 +1,8 @@
1
+ tr:
2
+ activemodel: &errors
3
+ errors:
4
+ messages:
5
+ invalid_email_address: 'geçerli bir eposta adresi değil'
6
+ email_address_not_routable: 'ulaşılabilir değil'
7
+ activerecord:
8
+ <<: *errors
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "http://rubygems.org"
4
+
5
+ gem "rails", "~> 7.1"
6
+
7
+ gemspec path: "../"
@@ -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.merge(generate_message: true)) || []).each do |error|
12
+ (ValidatesEmailFormatOf.validate_email_format(value, options) || []).each do |error|
13
13
  record.errors.add(attribute, error)
14
14
  end
15
15
  end
@@ -1,3 +1,3 @@
1
1
  module ValidatesEmailFormatOf
2
- VERSION = "1.7.2"
2
+ VERSION = "1.8.2"
3
3
  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\-]*[A-Za-z0-9]?\Z/
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\-]*[A-Za-z0-9]\Z/
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,41 +242,35 @@ module ValidatesEmailFormatOf
198
242
  end
199
243
 
200
244
  # backslash signifies the start of a quoted pair
201
- if ord == 92 && i < local.length - 1
202
- return false if !in_quoted_string # must be in quoted string per http://www.rfc-editor.org/errata_search.php?rfc=3696
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)
235
272
  parts = domain.downcase.split(".", -1)
273
+ parts.map! { |part| SimpleIDN.to_ascii(part) } if idn
236
274
 
237
275
  return false if parts.length <= 1 # Only one domain part
238
276
 
@@ -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(msg)
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
@@ -1,10 +1,10 @@
1
1
  require "active_model"
2
2
  require "active_support"
3
- require "pry"
4
- require "byebug"
5
3
 
6
4
  RSpec::Matchers.define :have_errors_on_email do
7
- match do |actual|
5
+ match do |user|
6
+ actual = user.errors.full_messages
7
+ expect(user.errors.added?(:email, ValidatesEmailFormatOf::ERROR_MESSAGE_I18N_KEY))
8
8
  expect(actual).not_to be_nil, "#{actual} should not be nil"
9
9
  expect(actual).not_to be_empty, "#{actual} should not be empty"
10
10
  expect(actual.size).to eq(@reasons.size), "#{actual} should have #{@reasons.size} elements"
@@ -19,7 +19,7 @@ RSpec::Matchers.define :have_errors_on_email do
19
19
  chain :and_because do |reason|
20
20
  (@reasons ||= []) << reason
21
21
  end
22
- match_when_negated do |actual|
23
- expect(actual).to(be_empty)
22
+ match_when_negated do |user|
23
+ expect(user.errors).to(be_empty)
24
24
  end
25
25
  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?).errors.full_messages
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
- # international domain names
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
@@ -128,10 +140,11 @@ describe ValidatesEmailFormatOf do
128
140
  "\nnewline@example.com",
129
141
  " spacesbefore@example.com",
130
142
  "spacesafter@example.com ",
131
- "(unbalancedcomment@example.com"
143
+ "(unbalancedcomment@example.com",
144
+ "help@.example.co.uk" # TLD can not start with a period
132
145
  ].each do |address|
133
146
  describe address do
134
- it { should have_errors_on_email.because("does not appear to be a valid e-mail address") }
147
+ it { should have_errors_on_email.because("does not appear to be a valid email address") }
135
148
  end
136
149
  end
137
150
 
@@ -141,7 +154,7 @@ describe ValidatesEmailFormatOf do
141
154
  it { should_not have_errors_on_email }
142
155
  end
143
156
  describe "#{"a" * (limit + 1)}@example.com" do
144
- it { should have_errors_on_email.because("does not appear to be a valid e-mail address") }
157
+ it { should have_errors_on_email.because("does not appear to be a valid email address") }
145
158
  end
146
159
  end
147
160
  describe "when using default" do
@@ -158,7 +171,7 @@ describe ValidatesEmailFormatOf do
158
171
  it { should_not have_errors_on_email }
159
172
  end
160
173
  describe "user@#{"a." * (limit / 2 + 1)}com" do
161
- it { should have_errors_on_email.because("does not appear to be a valid e-mail address") }
174
+ it { should have_errors_on_email.because("does not appear to be a valid email address") }
162
175
  end
163
176
  end
164
177
  describe "when using default" do
@@ -179,6 +192,27 @@ describe ValidatesEmailFormatOf do
179
192
  end
180
193
  end
181
194
 
195
+ describe "when idn support is disabled" do
196
+ before(:each) do
197
+ allow(SimpleIDN).to receive(:to_ascii).never
198
+ end
199
+ let(:options) { {idn: false} }
200
+ describe "test@exämple.com" do
201
+ it { should have_errors_on_email.because("does not appear to be a valid email address") }
202
+ end
203
+ end
204
+
205
+ describe "when idn support is enabled" do
206
+ before(:each) do
207
+ expect(SimpleIDN).to receive(:to_ascii).with("exämple").and_return("xn--exmple-cua")
208
+ expect(SimpleIDN).to receive(:to_ascii).with("com").and_return("com")
209
+ end
210
+ let(:options) { {idn: true} }
211
+ describe "test@exämple.com" do
212
+ it { should_not have_errors_on_email }
213
+ end
214
+ end
215
+
182
216
  describe "mx record" do
183
217
  domain = "example.com"
184
218
  email = "valid@#{domain}"
@@ -216,6 +250,9 @@ describe ValidatesEmailFormatOf do
216
250
  let(:mx_record) { [] }
217
251
  describe email do
218
252
  it { should have_errors_on_email.because("is not routable") }
253
+ it "adds the i18n key" do
254
+ subject.errors.added?(:email, ValidatesEmailFormatOf::ERROR_MX_MESSAGE_I18N_KEY)
255
+ end
219
256
  end
220
257
  describe "with a custom error message" do
221
258
  let(:options) { {check_mx: true, mx_message: "There ain't no such domain!"} }
@@ -223,6 +260,7 @@ describe ValidatesEmailFormatOf do
223
260
  it { should have_errors_on_email.because("There ain't no such domain!") }
224
261
  end
225
262
  end
263
+
226
264
  describe "i18n" do
227
265
  before(:each) do
228
266
  allow(I18n.config).to receive(:locale).and_return(locale)
@@ -236,6 +274,7 @@ describe ValidatesEmailFormatOf do
236
274
  end
237
275
  end
238
276
  end
277
+
239
278
  describe "when not testing" do
240
279
  before(:each) { allow(Resolv::DNS).to receive(:open).never }
241
280
  describe "by default" do
@@ -258,13 +297,43 @@ describe ValidatesEmailFormatOf do
258
297
  end
259
298
  end
260
299
 
300
+ describe "mx record for internationalized domain" do
301
+ domain = "пример.рф"
302
+ email = "valid@#{domain}"
303
+
304
+ describe "when idn support is enabled" do
305
+ let(:dns) { double(Resolv::DNS) }
306
+ let(:options) { {check_mx: true, idn: true} }
307
+
308
+ before(:each) do
309
+ allow(Resolv::DNS).to receive(:open).and_yield(dns)
310
+ allow(dns).to receive(:"timeouts=").with(3).once
311
+ allow(dns).to receive(:getresources).with(SimpleIDN.to_ascii(domain), Resolv::DNS::Resource::IN::A).once.and_return([double])
312
+ allow(dns).to receive(:getresources).with(SimpleIDN.to_ascii(domain), Resolv::DNS::Resource::IN::MX).once.and_return([double])
313
+ end
314
+
315
+ describe email do
316
+ it { should_not have_errors_on_email }
317
+ end
318
+ end
319
+
320
+ describe "when idn support is disabled" do
321
+ let(:options) { {check_mx: true, idn: false} }
322
+
323
+ describe "test@пример.рф" do
324
+ let(:domain) { "exämple.com" }
325
+ it { should have_errors_on_email.because("does not appear to be a valid email address") }
326
+ end
327
+ end
328
+ end
329
+
261
330
  describe "custom regex" do
262
331
  let(:options) { {with: /[0-9]+@[0-9]+/} }
263
332
  describe "012345@789" do
264
333
  it { should_not have_errors_on_email }
265
334
  end
266
335
  describe "valid@example.com" do
267
- it { should have_errors_on_email.because("does not appear to be a valid e-mail address") }
336
+ it { should have_errors_on_email.because("does not appear to be a valid email address") }
268
337
  end
269
338
  end
270
339
 
@@ -275,7 +344,7 @@ describe ValidatesEmailFormatOf do
275
344
  describe "present locale" do
276
345
  let(:locale) { :pl }
277
346
  describe "invalid@exmaple." do
278
- it { should have_errors_on_email.because("nieprawidłowy adres e-mail") }
347
+ it { should have_errors_on_email.because("nieprawidłowy adres email") }
279
348
  end
280
349
  end
281
350
  end
@@ -320,7 +389,7 @@ describe ValidatesEmailFormatOf do
320
389
  ActiveModel::Name.new(self, nil, "User")
321
390
  end
322
391
  end
323
- user.new(example.example_group_instance.email).tap(&:valid?).errors.full_messages
392
+ user.new(example.example_group_instance.email).tap(&:valid?)
324
393
  end
325
394
 
326
395
  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 e-mail addresses against RFC 2822 and RFC 3696."
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,13 +16,13 @@ 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"
25
26
  s.add_development_dependency "standard"
26
27
  s.add_development_dependency "appraisal"
27
- s.add_development_dependency "pry-byebug"
28
28
  end
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.7.2
4
+ version: 1.8.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Dunae
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2022-08-08 00:00:00.000000000 Z
12
+ date: 2024-04-12 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: i18n
@@ -17,22 +17,22 @@ dependencies:
17
17
  requirements:
18
18
  - - ">="
19
19
  - !ruby/object:Gem::Version
20
- version: '0'
20
+ version: 0.8.0
21
21
  type: :runtime
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
25
  - - ">="
26
26
  - !ruby/object:Gem::Version
27
- version: '0'
27
+ version: 0.8.0
28
28
  - !ruby/object:Gem::Dependency
29
- name: activemodel
29
+ name: simpleidn
30
30
  requirement: !ruby/object:Gem::Requirement
31
31
  requirements:
32
32
  - - ">="
33
33
  - !ruby/object:Gem::Version
34
34
  version: '0'
35
- type: :development
35
+ type: :runtime
36
36
  prerelease: false
37
37
  version_requirements: !ruby/object:Gem::Requirement
38
38
  requirements:
@@ -40,7 +40,7 @@ dependencies:
40
40
  - !ruby/object:Gem::Version
41
41
  version: '0'
42
42
  - !ruby/object:Gem::Dependency
43
- name: bundler
43
+ name: activemodel
44
44
  requirement: !ruby/object:Gem::Requirement
45
45
  requirements:
46
46
  - - ">="
@@ -54,7 +54,7 @@ dependencies:
54
54
  - !ruby/object:Gem::Version
55
55
  version: '0'
56
56
  - !ruby/object:Gem::Dependency
57
- name: rspec
57
+ name: bundler
58
58
  requirement: !ruby/object:Gem::Requirement
59
59
  requirements:
60
60
  - - ">="
@@ -68,7 +68,7 @@ dependencies:
68
68
  - !ruby/object:Gem::Version
69
69
  version: '0'
70
70
  - !ruby/object:Gem::Dependency
71
- name: standard
71
+ name: rspec
72
72
  requirement: !ruby/object:Gem::Requirement
73
73
  requirements:
74
74
  - - ">="
@@ -82,7 +82,7 @@ dependencies:
82
82
  - !ruby/object:Gem::Version
83
83
  version: '0'
84
84
  - !ruby/object:Gem::Dependency
85
- name: appraisal
85
+ name: standard
86
86
  requirement: !ruby/object:Gem::Requirement
87
87
  requirements:
88
88
  - - ">="
@@ -96,7 +96,7 @@ dependencies:
96
96
  - !ruby/object:Gem::Version
97
97
  version: '0'
98
98
  - !ruby/object:Gem::Dependency
99
- name: pry-byebug
99
+ name: appraisal
100
100
  requirement: !ruby/object:Gem::Requirement
101
101
  requirements:
102
102
  - - ">="
@@ -109,7 +109,7 @@ dependencies:
109
109
  - - ">="
110
110
  - !ruby/object:Gem::Version
111
111
  version: '0'
112
- description: Validate e-mail addresses against RFC 2822 and RFC 3696.
112
+ description: Validate email addresses against RFC 2822 and RFC 3696.
113
113
  email:
114
114
  - code@dunae.ca
115
115
  - iybetesh@gmail.com
@@ -117,6 +117,7 @@ executables: []
117
117
  extensions: []
118
118
  extra_rdoc_files: []
119
119
  files:
120
+ - ".github/dependabot.yml"
120
121
  - ".github/workflows/ci.yml"
121
122
  - ".gitignore"
122
123
  - ".rspec"
@@ -124,15 +125,17 @@ files:
124
125
  - CHANGELOG.md
125
126
  - Gemfile
126
127
  - MIT-LICENSE
127
- - README.rdoc
128
+ - README.md
128
129
  - config/locales/de.yml
129
130
  - config/locales/en.yml
130
131
  - config/locales/fr.yml
132
+ - config/locales/id.yml
131
133
  - config/locales/it.yml
132
134
  - config/locales/ja.yml
133
135
  - config/locales/pl.yml
134
136
  - config/locales/pt-BR.yml
135
137
  - config/locales/pt.yml
138
+ - config/locales/tr.yml
136
139
  - gemfiles/rails_4.2.gemfile
137
140
  - gemfiles/rails_5.0.gemfile
138
141
  - gemfiles/rails_5.1.gemfile
@@ -140,6 +143,7 @@ files:
140
143
  - gemfiles/rails_6.0.gemfile
141
144
  - gemfiles/rails_6.1.gemfile
142
145
  - gemfiles/rails_7.0.gemfile
146
+ - gemfiles/rails_7.1.gemfile
143
147
  - lib/validates_email_format_of.rb
144
148
  - lib/validates_email_format_of/active_model.rb
145
149
  - lib/validates_email_format_of/railtie.rb
@@ -169,8 +173,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
169
173
  - !ruby/object:Gem::Version
170
174
  version: '0'
171
175
  requirements: []
172
- rubygems_version: 3.1.6
176
+ rubygems_version: 3.3.26
173
177
  signing_key:
174
178
  specification_version: 4
175
- summary: Validate e-mail addresses against RFC 2822 and RFC 3696.
179
+ summary: Validate email addresses against RFC 2822 and RFC 3696.
176
180
  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.