Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
* develop:
  Bump version to `2.2.3`
  Fix regexp for numeric domains (fixes #72)
  Add `EmailValidator::Error` class, raise `EmailValidator::Error` when invalid `mode`
  Add checks for double dash in domain
  Fix specs for numeric-only domains labels
  Add checks for numeric-only TLDs in tests
  Add tests to ensure that `regexp` returns expected value
  Bump y18n from 3.2.1 to 3.2.2
  Add failing test: strict mode should accept numbers only domains
  Update .travis.yml
  • Loading branch information
karlwilbur committed Apr 5, 2021
2 parents 9aab0b9 + c69066d commit 4b24282
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 40 deletions.
4 changes: 4 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
language: ruby
arch:
- amd64
- ppc64le

rvm:
- 2.4.7
- 2.4.10
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ This file is used to list changes made in `email_validator`.
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## 2.2.3 (2021-04-05)

* [karlwilbur] - Fix regexp for numeric domains (fixes [#72](https://github.com/K-and-R/email_validator/issues/72))
- [delphaber] - Add checks for numeric-only domains in tests (should be considered valid)
- [karlwilbur] - Fix specs for numeric-only domains labels (should be considered valid)
- [karlwilbur] - Add checks for numeric-only TLDs in tests (should be considered invalid)
- [karlwilbur] - Add tests to ensure that `regexp` returns expected value
- [karlwilbur] - Add checks for double dash in domain (should be considered invalid)
- [karlwilbur] - Add `EmailValidator::Error` class, raise `EmailValidator::Error` when invalid `mode`

## 2.2.2 (2020-12-10)

* [karlwilbur] - Fix includes for `:rfc` and `:strict` modes from `Gemfile`
Expand Down
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ by setting `require_fqdn: true` or by enabling `:strict` checking:
validates :my_email_attribute, email: {mode: :strict, require_fqdn: true}
```

You can also limit to a single domain (e.g: this might help if, for example, you
have separate `User` and `AdminUser` models and want to ensure that `AdminUser`
emails are on a specific domain):
You can also limit to a single domain (e.g: you have separate `User` and
`AdminUser` models and want to ensure that `AdminUser` emails are on a specific
domain):

```ruby
validates :my_email_attribute, email: {domain: 'example.com'}
Expand Down Expand Up @@ -166,6 +166,10 @@ EmailValidator.valid?('narf@somehost') # boolean false
EmailValidator.invalid?('narf@somehost', require_fqdn: false) # boolean true
```

_NB: Enabling strict mode (`mode: :strict`) enables `require_fqdn`
(`require_fqdn: true`), overridding any `require_fqdn: false` while
`mode: :strict` is set._

### Requiring a specific domain

```ruby
Expand Down
2 changes: 1 addition & 1 deletion email_validator.gemspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Gem::Specification.new do |s|
s.name = 'email_validator'
s.version = '2.2.2'
s.version = '2.2.3'
s.authors = ['Brian Alexander', 'Karl Wilbur']
s.summary = 'An email validator for Rails 3+.'
s.description = 'An email validator for Rails 3+. See homepage for details: http://github.com/K-and-R/email_validator'
Expand Down
51 changes: 35 additions & 16 deletions lib/email_validator.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Based on work from http://thelucid.com/2010/01/08/sexy-validation-in-edge-rails-rails-3/

# EmailValidator class
class EmailValidator < ActiveModel::EachValidator
# rubocop:disable Style/ClassVars
@@default_options = {
Expand All @@ -9,6 +11,13 @@ class EmailValidator < ActiveModel::EachValidator
}
# rubocop:enable Style/ClassVars

# EmailValidator::Error class
class Error < StandardError
def initialize(msg = 'EmailValidator error')
super
end
end

class << self
def default_options
@@default_options
Expand All @@ -35,8 +44,11 @@ def regexp(options = {})
loose_regexp(options)
when :rfc
rfc_regexp(options)
else
when :strict
options[:require_fqdn] = true
strict_regexp(options)
else
fail EmailValidator::Error, "Validation mode '#{options[:mode]}' is not supported by EmailValidator"
end
end

Expand Down Expand Up @@ -81,31 +93,38 @@ def address_literal
end

def host_label_pattern
"#{label_is_correct_length}" \
"#{label_contains_no_more_than_one_consecutive_hyphen}" \
"#{alnum}(?:#{alnumhy}{,61}#{alnum})?"
end

# splitting this up into separate regex pattern for performance; let's not
# try the "contains" pattern unless we have to
def domain_label_pattern
'(?=[^.]{1,63}(?:\.|$))' \
'(?:' \
"#{alpha}" \
"|#{domain_label_starts_with_a_letter_pattern}" \
"|#{domain_label_ends_with_a_letter_pattern}" \
"|#{domain_label_contains_a_letter_pattern}" \
')'
"#{host_label_pattern}\\.#{tld_label_pattern}"
end

# While, techincally, TLDs can be numeric-only, this is not allowed by ICANN
# Ref: ICANN Application Guidebook for new TLDs (June 2012)
# says the following starting at page 64:
#
# > The ASCII label must consist entirely of letters (alphabetic characters a-z)
#
# -- https://newgtlds.icann.org/en/applicants/agb/guidebook-full-04jun12-en.pdf
def tld_label_pattern
"#{alpha}{1,64}"
end

def domain_label_starts_with_a_letter_pattern
"#{alpha}#{alnumhy}{,61}#{alnum}"
def label_is_correct_length
'(?=[^.]{1,63}(?:\.|$))'
end

def domain_label_ends_with_a_letter_pattern
"#{alnum}#{alnumhy}{,61}#{alpha}"
def domain_part_is_correct_length
'(?=.{1,255}$)'
end

def domain_label_contains_a_letter_pattern
"(?:[[:digit:]])(?:[[:digit:]]|-)*#{alpha}#{alnumhy}*#{alnum}"
def label_contains_no_more_than_one_consecutive_hyphen
'(?!.*?--.*$)'
end

def atom_char
Expand All @@ -122,11 +141,11 @@ def local_part_pattern
def domain_part_pattern(options)
return options[:domain].sub(/\./, '\.') if options[:domain].present?
return fqdn_pattern if options[:require_fqdn]
"(?=.{1,255}$)(?:#{address_literal}|(?:#{host_label_pattern}\\.)*#{domain_label_pattern})"
"#{domain_part_is_correct_length}(?:#{address_literal}|(?:#{host_label_pattern}\\.)*#{tld_label_pattern})"
end

def fqdn_pattern
"(?=.{1,255}$)(?:#{host_label_pattern}\\.)*#{domain_label_pattern}\\.#{domain_label_pattern}"
"(?=.{1,255}$)(?:#{host_label_pattern}\\.)*#{domain_label_pattern}"
end

private
Expand Down
70 changes: 53 additions & 17 deletions spec/email_validator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,8 @@ class DefaultUserWithMessage < TestModel
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]'
'[email protected]',
'[email protected]'
]).flatten.each do |email|
context 'when using defaults' do
it "'#{email}' should be valid" do
Expand Down Expand Up @@ -289,20 +290,20 @@ class DefaultUserWithMessage < TestModel
end

context 'when in `:strict` mode' do
it "'#{email}' should not be valid" do
expect(StrictUser.new(:email => email)).not_to be_valid
it "'#{email}' should be valid" do
expect(StrictUser.new(:email => email)).to be_valid
end

it "'#{email}' should not be valid using EmailValidator.valid?" do
expect(described_class).not_to be_valid(email, :mode => :strict)
it "'#{email}' should be valid using EmailValidator.valid?" do
expect(described_class).to be_valid(email, :mode => :strict)
end

it "'#{email}' should be invalid using EmailValidator.invalid?" do
expect(described_class).to be_invalid(email, :mode => :strict)
it "'#{email}' should be not invalid using EmailValidator.invalid?" do
expect(described_class).not_to be_invalid(email, :mode => :strict)
end

it "'#{email}' should not match the regexp" do
expect(!!(email.strip =~ described_class.regexp(:mode => :strict))).to be(false)
it "'#{email}' should match the regexp" do
expect(!!(email.strip =~ described_class.regexp(:mode => :strict))).to be(true)
end
end

Expand Down Expand Up @@ -476,6 +477,7 @@ class DefaultUserWithMessage < TestModel
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'the-local-part-is-invalid-if-it-is-longer-than-sixty-four-characters@sld.dev',
"domain-too-long@t#{".#{'o' * 63}" * 5}.long",
"[email protected]<script>alert('hello')</script>"
Expand Down Expand Up @@ -703,7 +705,8 @@ class DefaultUserWithMessage < TestModel
]}).concat([
'[email protected]',
'[email protected]',
'[email protected]'
'[email protected]',
'[email protected]'
]).flatten.each do |email|
context 'when using defaults' do
it "#{email.strip} in a model should be valid" do
Expand Down Expand Up @@ -781,23 +784,24 @@ class DefaultUserWithMessage < TestModel
end
end

# Strict mode enables `require_fqdn` anyway
context 'when in `:strict` mode' do
let(:opts) { { :require_fqdn => false, :mode => :strict } }

it 'is valid' do
expect(NonFqdnStrictUser.new(:email => email)).to be_valid
it 'is not valid' do
expect(NonFqdnStrictUser.new(:email => email)).not_to be_valid
end

it 'is valid using EmailValidator.valid?' do
expect(described_class).to be_valid(email, opts)
it 'is not valid using EmailValidator.valid?' do
expect(described_class).not_to be_valid(email, opts)
end

it 'is not invalid using EmailValidator.invalid?' do
expect(described_class).not_to be_invalid(email, opts)
it 'is invalid using EmailValidator.invalid?' do
expect(described_class).to be_invalid(email, opts)
end

it 'matches the regexp' do
expect(!!(email =~ described_class.regexp(opts))).to be(true)
expect(!!(email =~ described_class.regexp(opts))).to be(false)
end
end

Expand Down Expand Up @@ -1012,4 +1016,36 @@ class DefaultUserWithMessage < TestModel
end
end
end

context 'with regexp' do
it 'returns a regexp when asked' do
expect(described_class.regexp).to be_a(Regexp)
end

it 'returns a strict regexp when asked' do
expect(described_class.regexp(:mode => :strict)).to be_a(Regexp)
end

it 'returns a RFC regexp when asked' do
expect(described_class.regexp(:mode => :rfc)).to be_a(Regexp)
end

it 'has different regexp for strict and loose' do
expect(described_class.regexp(:mode => :strict)).not_to eq(described_class.regexp(:mode => :loose))
end

it 'has different regexp for RFC and loose' do
expect(described_class.regexp(:mode => :rfc)).not_to eq(described_class.regexp(:mode => :loose))
end

it 'has different regexp for RFC and strict' do
expect(described_class.regexp(:mode => :rfc)).not_to eq(described_class.regexp(:mode => :strict))
end
end

context 'with invalid `:mode`' do
it 'raises an error' do
expect { described_class.regexp(:mode => :invalid) }.to raise_error(EmailValidator::Error)
end
end
end
6 changes: 3 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1409,9 +1409,9 @@ wrappy@1:
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=

y18n@^3.2.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
integrity sha1-bRX7qITAhnnA136I53WegR4H+kE=
version "3.2.2"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.2.tgz#85c901bd6470ce71fc4bb723ad209b70f7f28696"
integrity sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==

yallist@^2.1.2:
version "2.1.2"
Expand Down

0 comments on commit 4b24282

Please sign in to comment.