Update: I just found the valid_email2 gem which looks pretty great.
Don't use a regular expression for email address validation. It's a trap. There are way more valid email address formats than you'll think of. However! The mail gem (it's required by ActionMailer, so you have it) will parse email addresses — with a proper parser — for you:
require 'mail'
a = Mail::Address.new('foo@example.com')
This will throw a Mail::Field::ParseError if it's a non-compliant email address. (We're not getting into things like doing an MX address lookup or anything.)
If you want the good ol' Rails validator experience, you can make app/models/concerns/email_validatable.rb:
require 'mail'
module EmailValidatable
extend ActiveSupport::Concern
class EmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
begin
a = Mail::Address.new(value)
rescue Mail::Field::ParseError
record.errors[attribute] << (options[:message] || "is not an email")
end
end
end
end
and then in your model, you can:
include EmailValidatable
validates :email, email: true
As Iwo Dziechciarow's comment below mentions, this passes anything that's a valid "To:" address through. So something like Foo Bar <foo.bar@example.com> is valid. This might be a problem for you, it might not; it really is a valid address, after all.
If you do want just the address portion of it:
a = Mail::Address.new('Foo Bar <foobar@example.com>')
a.address
=> "foobar@example.com"
As Björn Weinbrenne notes below, there are way more valid RFC2822 addresses than you may expect (I'm quite sure all of the addresses listed there are compliant, and may receive mail depending system configurations) — this is why I don't recommend trying a regex, but using a compliant parser.
If you really care whether you can send email to an address then your best bet — by far — is to actually send a message with a verification link.
@Nate Thank you so much for putting this answer together. I did not realize email validation had so many nuances until I looked at your code snippet.
I noticed that the current mail gem: mail-2.6.5 doesn't throw an error for an email of "abc". Examples:
>> a = Mail::Address.new('abc')
=> #<Mail::Address:70343701196060 Address: |abc| >
>> a.address # this is weird
=> "abc"
>> a = Mail::Address.new('"Jon Doe" <jon@doe.com>')
=> #<Mail::Address:70343691638900 Address: |Jon Doe <jon@doe.com>| >
>> a.address
=> "jon@doe.com"
>> a.display_name
=> "Jon Doe"
>> Mail::Address.new('"Jon Doe <jon')
Mail::Field::ParseError: Mail::AddressList can not parse |"Jon Doe <jon|
Reason was: Only able to parse up to "Jon Doe <jon
from (irb):3:in `new'
from (irb):3
>>
It does throw Mail::Field::ParseError errors for "Jon Doe <jon which is great. I believe will check for the simple "abc pattern" also.
In app/models/concerns/pretty_email_validatable.rb:
require 'mail'
module PrettyEmailValidatable
extend ActiveSupport::Concern
class PrettyEmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
begin
a = Mail::Address.new(value)
rescue Mail::Field::ParseError
record.errors[attribute] << (options[:message] || "is not an email")
end
# regexp from http://guides.rubyonrails.org/active_record_validations.html
value = a.address
unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
record.errors[attribute] << (options[:message] || "is not an email")
end
end
end
end
and then in your model, you can:
include PrettyEmailValidatable
validates :pretty_email, email: true
If anybody else is very TDD focused: I wanted something that I could write tests against and improve upon later if needed, without tying the tests to another model.
Building off of Nate and tongueroo's code (Thanks Nate and tongueroo!), this was done in Rails 5, Ruby 2.4.1. Here's what I threw into app/validators/email_validator.rb:
require 'mail'
class EmailValidator < ActiveModel::EachValidator
def add_error(record, attribute)
record.errors.add(attribute, (options[:message] || "is not a valid email address"))
end
def validate_each(record, attribute, value)
begin
a = Mail::Address.new(value)
rescue Mail::Field::ParseError
add_error(record, attribute)
end
# regexp from http://guides.rubyonrails.org/active_record_validations.html
value = a.address unless a.nil?
unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
add_error(record, attribute)
end
end
end
And this is by no means comprehensive, but here's what I threw into spec/validators/email_validator_spec.rb:
require 'rails_helper'
RSpec.describe EmailValidator do
subject do
Class.new do
include ActiveModel::Validations
attr_accessor :email
validates :email, email: true
end.new
end
context 'when the email address is valid' do
let(:email) { Faker::Internet.email }
it 'allows the input' do
subject.email = email
expect(subject).to be_valid
end
end
context 'when the email address is invalid' do
let(:invalid_message) { 'is not a valid email address' }
it 'invalidates the input' do
subject.email = 'not_valid@'
expect(subject).not_to be_valid
end
it 'alerts the consumer' do
subject.email = 'notvalid'
subject.valid?
expect(subject.errors[:email]).to include(invalid_message)
end
end
end
The simple answer is: Don't use a regexp. There are too many edge cases and false negatives and false positives. Check for an @ sign and send a mail to the address to validate it:
class EmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
record.errors[attribute] << (options[:message] || "is not an email")
end
end
end
class Person < ApplicationRecord
validates :email, presence: true, email: true
end