validates_email_format_of 1.7.2 → 1.8.2

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