├── .bundle └── config ├── .gitignore ├── .rspec ├── .travis.yml ├── .yardopts ├── CHANGELOG.md ├── Gemfile ├── MIT-LICENSE ├── README.md ├── Rakefile ├── gemfiles ├── Gemfile-rails.4.2.x ├── Gemfile-rails.5.0.x └── Gemfile-rails.5.1.x ├── lib ├── validates_email.rb └── validates_email │ ├── email_validator.rb │ └── version.rb ├── spec ├── person.rb ├── spec_helper.rb └── validates_email_spec.rb └── spectator-validates_email.gemspec /.bundle/config: -------------------------------------------------------------------------------- 1 | --- {} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.tmproj 3 | tmtags 4 | .rvmrc 5 | pkg 6 | .yardoc 7 | doc 8 | *.gem 9 | *.lock 10 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2 4 | - 2.3 5 | - 2.4 6 | - 2.5 7 | gemfile: 8 | - Gemfile 9 | - gemfiles/Gemfile-rails.4.2.x 10 | - gemfiles/Gemfile-rails.5.0.x 11 | - gemfiles/Gemfile-rails.5.1.x 12 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --quiet 2 | lib/**/*.rb 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.1.0 / 2019-10-10 2 | 3 | ## Added 4 | 5 | - Add support for longer TLDs (packrat386 in [#10](https://github.com/spectator/validates_email/pull/10)) 6 | 7 | [Compare v1.0.0...v1.1.0](https://github.com/spectator/validates_email/compare/v1.0.0...v1.1.0) 8 | 9 | # 1.0.0 / 2019-07-10 10 | 11 | ## Added 12 | 13 | - Removed support for old Ruby versions (< 2.2) and Rails (< 4.2) [#9](https://github.com/spectator/validates_email/pull/9) 14 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem 'activemodel', '~> 5.2.0' 6 | 7 | group :test do 8 | gem 'rspec' 9 | gem 'sqlite3' 10 | gem 'rake' 11 | end 12 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 [name of plugin creator] 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gem 2 | Version](https://badge.fury.io/rb/spectator-validates_email.png)](http://badge.fury.io/rb/spectator-validates_email) 3 | [![Build 4 | Status](https://secure.travis-ci.org/spectator/validates_email.png?branch=master)](http://travis-ci.org/spectator/validates_email) 5 | [![Dependency 6 | Status](https://gemnasium.com/spectator/validates_email.png?travis)](https://gemnasium.com/spectator/validates_email) 7 | 8 | validates_email 9 | =============== 10 | 11 | validates_email is a Rails plugin that validates email addresses against RFC 2822 and RFC 3696 12 | 13 | Installation 14 | ------------ 15 | 16 | rails plugin install git://github.com/spectator/validates_email.git 17 | 18 | or 19 | 20 | gem 'spectator-validates_email', require: 'validates_email' 21 | 22 | Usage 23 | ----- 24 | 25 | class User < ActiveRecord::Base 26 | validates :primary_email, email: true 27 | end 28 | 29 | As well as any other Rails validation this one has the same triggers, such as `:on`, `:if`, `:unless`, `:allow_blank`, and `:allow_nil`. 30 | 31 | Also, you can pass your own custom error message. 32 | 33 | class User < ActiveRecord::Base 34 | validates :primary_email, email: { message: 'is not an email address' } 35 | end 36 | 37 | If you like to check MX Records for email, you can use `:mx` option. 38 | 39 | class User < ActiveRecord::Base 40 | validates :primary_email, email: { mx: true } 41 | end 42 | 43 | And if you like to check MX Records with fallback to A record, use `:a_fallback` option. 44 | 45 | class User < ActiveRecord::Base 46 | validates :primary_email, email: { mx: { a_fallback: true } } 47 | end 48 | 49 | I18n 50 | ---- 51 | 52 | If you don't specify your own error message, then ActiveRecord's `:invalid` error message will be used to show the error. 53 | 54 | If do check MX Records, then you have to specify your own error message or add it to your traslations: 55 | 56 | activerecord: 57 | errors: 58 | messages: 59 | mx_invalid: "is not valid" 60 | 61 | Credits 62 | ------- 63 | 64 | Most of the code were taken from Alex Dunae (dunae.ca) plugin (see http://github.com/alexdunae/validates_email_format_of/) and adopted for Rails 3 playing around with Rails 3 beta, so pass all beers to him. 65 | 66 | Contributors 67 | ------------ 68 | 69 | * Petr Blaho 70 | * Christian Eichhorn 71 | * Alexander Zubkov 72 | * Daniel Naves de Carvalho 73 | * Aidan Coyle 74 | 75 | How to contribute 76 | ----------------- 77 | 78 | * Fork 79 | * Make changes 80 | * Write specs 81 | * Do pull request 82 | 83 | Testing 84 | ------- 85 | 86 | To execute unit tests run `rake spec` within plugin folder. 87 | 88 | The unit tests for this plugin use an in-memory sqlite3 database. 89 | 90 | Notes 91 | ----- 92 | 93 | Compatible with the following ruby versions: 94 | 95 | * Ruby 2.2.x 96 | * Ruby 2.3.x 97 | * Ruby 2.4.x 98 | * Ruby 2.5.x 99 | 100 | Compatible with the following Rails versions: 101 | 102 | * Rails 4.2.x 103 | * Rails 5.0.x 104 | * Rails 5.1.x 105 | * Rails 5.2.x 106 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | 4 | require "rspec" 5 | require "rspec/core/rake_task" 6 | 7 | RSpec::Core::RakeTask.new(:spec) 8 | 9 | task :default => :spec 10 | -------------------------------------------------------------------------------- /gemfiles/Gemfile-rails.4.2.x: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'spectator-validates_email', path: '..', require: 'validates_email' 4 | gem 'activemodel', '~> 4.2.0' 5 | 6 | group :test do 7 | gem 'rspec' 8 | gem 'sqlite3' 9 | gem 'rake' 10 | end 11 | -------------------------------------------------------------------------------- /gemfiles/Gemfile-rails.5.0.x: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'spectator-validates_email', path: '..', require: 'validates_email' 4 | gem 'activemodel', '~> 5.0.0' 5 | 6 | group :test do 7 | gem 'rspec' 8 | gem 'sqlite3' 9 | gem 'rake' 10 | end 11 | -------------------------------------------------------------------------------- /gemfiles/Gemfile-rails.5.1.x: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'spectator-validates_email', path: '..', require: 'validates_email' 4 | gem 'activemodel', '~> 5.1.0' 5 | 6 | group :test do 7 | gem 'rspec' 8 | gem 'sqlite3' 9 | gem 'rake' 10 | end 11 | -------------------------------------------------------------------------------- /lib/validates_email.rb: -------------------------------------------------------------------------------- 1 | require "validates_email/email_validator" 2 | -------------------------------------------------------------------------------- /lib/validates_email/email_validator.rb: -------------------------------------------------------------------------------- 1 | # Email validation class which uses Rails 4 ActiveModel 2 | # validation mechanism. 3 | # 4 | class EmailValidator < ActiveModel::EachValidator 5 | 6 | LocalPartSpecialChars = Regexp.escape('!#$%&\'*-/=?+-^_`{|}~') 7 | LocalPartUnquoted = '(([[:alnum:]' + LocalPartSpecialChars + ']+[\.\+]+))*[[:alnum:]' + LocalPartSpecialChars + '+]+' 8 | LocalPartQuoted = '\"(([[:alnum:]' + LocalPartSpecialChars + '\.\+]*|(\\\\[\x00-\xFF]))*)\"' 9 | Regex = Regexp.new('^((' + LocalPartUnquoted + ')|(' + LocalPartQuoted + ')+)@(((\w+\-+[^_])|(\w+\.[^_]))*([a-z0-9-]{1,63})\.[a-z]{2,63}(?:\.[a-z]{2,63})?$)', Regexp::EXTENDED | Regexp::IGNORECASE, 'n') 10 | 11 | # Validates email address. 12 | # If MX fallback was also requested, it will check if email is valid 13 | # first, and only after that will go to MX fallback. 14 | # 15 | # @example 16 | # class User < ActiveRecord::Base 17 | # validates :primary_email, :email => { :mx => { :a_fallback => true } } 18 | # end 19 | # 20 | def validate_each(record, attribute, value) 21 | if validates_email_format(value) 22 | if options[:mx] && !validates_email_domain(value, options[:mx]) 23 | record.errors.add(attribute, (options[:mx_message] || I18n.t(:mx_invalid, scope: [:activerecord, :errors, :messages]))) 24 | end 25 | else 26 | record.errors.add(attribute, (options[:message] || I18n.t(:invalid, scope: [:activerecord, :errors, :messages]))) 27 | end 28 | end 29 | 30 | private 31 | 32 | # Checks email if it's valid by rules defined in `Regex`. 33 | # 34 | # @param [String] A string with email. Local part of email is max 64 chars, 35 | # domain part is max 255 chars. 36 | # 37 | # @return [Boolean] True or false. 38 | # 39 | def validates_email_format(email) 40 | # TODO: should this decode escaped entities before counting? 41 | begin 42 | local, domain = email.split('@', 2) 43 | rescue NoMethodError 44 | return false 45 | end 46 | 47 | begin 48 | email =~ Regex and not email =~ /\.\./ and domain.length <= 255 and local.length <= 64 49 | rescue Encoding::CompatibilityError 50 | # RFC 2822 and RFC 3696 don't support UTF-8 characters in email address, 51 | # so Regexp is in ASCII-8BIT encoding, which raises this exception when 52 | # you try email address with unicode characters in it. 53 | return false 54 | end 55 | end 56 | 57 | # Checks email is its domain is valid. Fallbacks to A record if requested. 58 | # 59 | # @param [String] A string with email. 60 | # @param [Hash] A hash of options, which tells whether to use A fallback or 61 | # or not. Additional options can be also passed. 62 | # 63 | # @return [Integer, nil] In general, it's true or false. 64 | # 65 | def validates_email_domain(email, options) 66 | require 'resolv' 67 | a_fallback = options.is_a?(Hash) ? options[:a_fallback] : false 68 | domain = email.match(/\@(.+)/)[1] 69 | Resolv::DNS.open do |dns| 70 | @mx = dns.getresources(domain, Resolv::DNS::Resource::IN::MX) 71 | @mx.push(*dns.getresources(domain, Resolv::DNS::Resource::IN::A)) if a_fallback 72 | end 73 | @mx.size > 0 ? true : false 74 | end 75 | 76 | end 77 | -------------------------------------------------------------------------------- /lib/validates_email/version.rb: -------------------------------------------------------------------------------- 1 | module ValidatesEmail 2 | VERSION = "1.1.0" 3 | end 4 | -------------------------------------------------------------------------------- /spec/person.rb: -------------------------------------------------------------------------------- 1 | class GenericPerson 2 | include ActiveModel::Validations 3 | attr_accessor :primary_email 4 | 5 | def initialize(attributes = {}) 6 | attributes.each do |name, value| 7 | send("#{name}=", value) 8 | end 9 | end 10 | end 11 | 12 | class Person < GenericPerson 13 | validates :primary_email, email: true 14 | end 15 | 16 | class PersonMessage < GenericPerson 17 | validates :primary_email, 18 | email: { message: 'fails with custom message' } 19 | end 20 | 21 | class PersonMX < GenericPerson 22 | validates :primary_email, 23 | email: { mx: true } 24 | end 25 | 26 | class PersonMXA < GenericPerson 27 | validates :primary_email, 28 | email: { mx: { a_fallback: true } } 29 | end 30 | 31 | class PersonMXMessage < GenericPerson 32 | validates :primary_email, 33 | email: { mx: true, 34 | mx_message: 'fails with custom mx message' } 35 | end 36 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'active_model' 2 | require 'rspec' 3 | 4 | require 'validates_email' 5 | require 'person' 6 | -------------------------------------------------------------------------------- /spec/validates_email_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'spec_helper' 3 | 4 | describe EmailValidator do 5 | 6 | context "w/o mx fallback" do 7 | it "allows valid emails" do 8 | [ 9 | 'valid@example.com', 10 | 'Valid@test.example.com', 11 | 'valid+valid123@test.example.com', 12 | 'valid_valid123@test.example.com', 13 | 'valid-valid+123@test.example.co.uk', 14 | 'valid-valid+1.23@test.example.com.au', 15 | 'valid@example.co.uk', 16 | 'v@example.com', 17 | 'valid@example.ca', 18 | 'valid_@example.com', 19 | 'valid123.456@example.org', 20 | 'valid123.456@example.travel', 21 | 'valid123.456@example.museum', 22 | 'valid@example.mobi', 23 | 'valid@example.info', 24 | 'valid-@example.com', 25 | # from RFC 3696, page 6 26 | 'customer/department=shipping@example.com', 27 | '$A12345@example.com', 28 | '!def!xyz%abc@example.com', 29 | '_somename@example.com', 30 | # apostrophes 31 | "test'test@example.com", 32 | # .sch.uk 33 | 'valid@example.w-dash.sch.uk', 34 | # long TLD 35 | 'valid@valid.helsinki' 36 | ].each do |email| 37 | person = Person.new(:primary_email => email) 38 | person.should be_valid(email) 39 | end 40 | end 41 | 42 | # From http://www.rfc-editor.org/errata_search.php?rfc=3696 43 | it "allows quoted characters" do 44 | [ 45 | '"Abc\@def"@example.com', 46 | '"Fred\ Bloggs"@example.com', 47 | '"Joe.\\Blow"@example.com' 48 | ].each do |email| 49 | person = Person.new(:primary_email => email) 50 | person.should be_valid(email) 51 | end 52 | end 53 | 54 | it "doesn't allow invalid emails" do 55 | [ 56 | 'invalid@example-com', 57 | # period can not start local part 58 | '.invalid@example.com', 59 | # period can not end local part 60 | 'invalid.@example.com', 61 | # period can not appear twice consecutively in local part 62 | 'invali..d@example.com', 63 | # should not allow underscores in domain names 64 | 'invalid@ex_mple.com', 65 | 'invalid@example.com.', 66 | 'invalid@example.com_', 67 | 'invalid@example.com-', 68 | 'invalid-example.com', 69 | 'invalid@example.b#r.com', 70 | 'invalid@example.c', 71 | 'invali d@example.com', 72 | 'invalidexample.com', 73 | 'invalid@example.', 74 | 'чебурашка@kremlin.ru' 75 | ].each do |email| 76 | person = Person.new(:primary_email => email) 77 | person.should_not be_valid(email) 78 | end 79 | end 80 | 81 | it "doesn't raise exception for emails with UTF-8 characters" do 82 | person = Person.new(:primary_email => 'чебурашка@kremlin.ru') 83 | lambda { 84 | person.valid? 85 | }.should_not raise_error(Encoding::CompatibilityError) 86 | end 87 | 88 | # From http://tools.ietf.org/html/rfc3696, page 5 89 | # Corrected in http://www.rfc-editor.org/errata_search.php?rfc=3696 90 | it "doesn't allow escaped characters without quotes" do 91 | [ 92 | 'Fred\ Bloggs_@example.com', 93 | 'Abc\@def+@example.com', 94 | 'Joe.\\Blow@example.com' 95 | ].each do |email| 96 | person = Person.new(:primary_email => email) 97 | person.should_not be_valid(email) 98 | end 99 | end 100 | 101 | it "doesn't allow long emails" do 102 | [ 103 | 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@example.com', 104 | 'test@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com' 105 | ].each do |email| 106 | person = Person.new(:primary_email => email) 107 | person.should_not be_valid(email) 108 | end 109 | end 110 | end 111 | 112 | context "w/ MX fallback" do 113 | it "allows valid email" do 114 | email = "test@gmail.com" 115 | person = PersonMX.new(:primary_email => email) 116 | person.should be_valid(email) 117 | end 118 | 119 | it "doesn't allow invalid email" do 120 | email = "test@example.com" 121 | person = PersonMX.new(:primary_email => email) 122 | person.should_not be_valid(email) 123 | end 124 | 125 | it "doesn't validate mx with invalid email" do 126 | email = "testexample.com" 127 | lambda { 128 | person = PersonMX.new(:primary_email => email) 129 | person.should_not be_valid(email) 130 | }.should_not raise_error 131 | end 132 | end 133 | 134 | context "w/ A record MX fallback" do 135 | it "allows valid email" do 136 | email = "test@gmail.com" 137 | person = PersonMXA.new(:primary_email => email) 138 | person.should be_valid(email) 139 | end 140 | 141 | it "allows valid email with fallback to A" do 142 | email = "test@example.com" 143 | person = PersonMXA.new(:primary_email => email) 144 | person.should be_valid(email) 145 | end 146 | end 147 | 148 | context "w/ custom error messages" do 149 | it "allows custom error message" do 150 | email = "example.com" 151 | person = PersonMessage.new(:primary_email => email) 152 | person.should_not be_valid(email) 153 | person.errors[:primary_email].should eql(["fails with custom message"]) 154 | end 155 | 156 | it "allows custom error message for mx fallback" do 157 | email = "test@example.com" 158 | person = PersonMXMessage.new(:primary_email => email) 159 | person.should_not be_valid(email) 160 | person.errors[:primary_email].should eql(["fails with custom mx message"]) 161 | end 162 | end 163 | 164 | if ActiveModel::VERSION::MAJOR >= 5 165 | context "w/ details" do 166 | it "allows error message" do 167 | email = "example.com" 168 | person = PersonMessage.new(:primary_email => email) 169 | person.should_not be_valid(email) 170 | person.errors.details[:primary_email].should eql([{:error=>"fails with custom message"}]) 171 | end 172 | end 173 | end 174 | end 175 | -------------------------------------------------------------------------------- /spectator-validates_email.gemspec: -------------------------------------------------------------------------------- 1 | require File.expand_path("../lib/validates_email/version", __FILE__) 2 | 3 | Gem::Specification.new do |s| 4 | s.name = "spectator-validates_email" 5 | s.version = ValidatesEmail::VERSION 6 | s.platform = Gem::Platform::RUBY 7 | s.authors = ["Yury Velikanau"] 8 | s.date = ["2010-10-24"] 9 | s.email = ["yury.velikanau@gmail.com"] 10 | s.homepage = "http://github.com/spectator/validates_email" 11 | s.summary = "Rails plugin to validate email addresses" 12 | s.description = "Rails plugin to validate email addresses against RFC 2822 and RFC 3696" 13 | 14 | s.required_rubygems_version = ">= 1.3.6" 15 | 16 | s.files = Dir["{lib}/**/*.rb", "MIT-LICENSE", "*.rdoc"] 17 | s.require_path = 'lib' 18 | 19 | s.add_dependency "activemodel", ">= 4.2.0" 20 | end 21 | --------------------------------------------------------------------------------