95

I am doing email validation in Rails with:

validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i

Also, I do HTML5 validation in the frontend but email addresses like

[email protected]
[email protected]

still are valid. What am I missing?

7
  • 1
    Those are valid emails Commented Jul 27, 2016 at 11:17
  • @FrederickCheung: apparently, they aren't: isemail.info/..abc%40gmail.com Commented Jul 27, 2016 at 11:57
  • @SergioTulentsev cool didn't know about that site. I can receive email sent to such an address, so some implementations would appear to ignore that restriction. Commented Jul 27, 2016 at 12:00
  • 1
    @FrederickCheung: checked with gmail, doesn't work. :) Commented Jul 27, 2016 at 12:03
  • @SergioTulentsev I tested with gmail too - can hardly complain when non spec behaviour is inconsistent though. Commented Jul 27, 2016 at 12:04

14 Answers 14

278

I use the constant built into URI in the standard ruby library

validates :email, format: { with: URI::MailTo::EMAIL_REGEXP } 

warning - this considers a@b to be a valid email address

Sign up to request clarification or add additional context in comments.

12 Comments

Attention! I tried this using a wrong email !('fakeemail@gmailcom' =~ URI::MailTo::EMAIL_REGEXP).nil? it should returns false but it returns true which is not correct
fakeemail@gmailcom is a valid email, check here en.wikipedia.org/wiki/Email_address#Examples
This method does not check domain
If there is a period, but nothing after it, the regex will return nil. If there is something after the period or no period, it will pass. "This requirement is a willful violation of RFC 5322, which defines a syntax for e-mail addresses that is simultaneously too strict (before the "@" character), too vague (after the "@" character), and too lax (allowing comments, whitespace characters, and quoted strings in manners unfamiliar to most users) to be of practical use here." html.spec.whatwg.org/multipage/input.html#valid-e-mail-address
@medBouzid because fakeemail@gmailcom is in valid email format. I would check the domain next.
|
40

If you use the Devise gem already in your app, it might be opportune to use

email =~ Devise.email_regexp

...which also means different places of the app use the same validation.

If you want to add it as an attribute validator to your ActiveRecord model:

validates :email, format: { with: Devise.email_regexp }

6 Comments

The Device regex is quite simple: /\A[^@\s]+@[^@\s]+\z/ The following email addresses are not detected as invalid: info@example,com, info@example, info,[email protected]
I use validates_format_of :email, :with => /\A[^@,\s]+@[^@,\s]+\.[^@,\s]+\z/ in my model now. The Regex is the one from Devise, but modified to deny the examples above.
Yes this looks cool to me. validates :email, format: { with: Devise.email_regexp, message: "invalid email" }
This will not check if the email is valid, it will just check if your string contains @ char and there is no whitespace. # Email regex used to validate email formats. It asserts that there are no # @ symbols or whitespaces in either the localpart or the domain, and that # there is a single @ symbol separating the localpart and the domain. mattr_accessor :email_regexp @@email_regexp = /\A[^@\s]+@[^@\s]+\z/
if you're using devise already, your user model should already be doing this
|
34

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('[email protected]')

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 <[email protected]> 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 <[email protected]>')
a.address
=> "[email protected]"

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.

6 Comments

interesting approach - though this will also accept values like 'Mikel Lindsaar (My email address) <[email protected]>' with the display_name and comments - which probably isn't what you always want, but you can probably get the parsed address alone (a.address) and overwrite the attribute with that value
It doesn't detect the following as invalid: info@example,com, info@example, info,[email protected]
I'm pretty sure those are all RFC2822-compliant addresses. I've edited my answer to clarify.
I'm unable to get any errors using Mail::Address.new('fake'). Don't tell me the email address fake is RFC compliant.
I think it is, it would be to the user 'fake' on the local system. You probably want to make sure domain is something you like reasonably well.
|
15

Here is the new rails way to do email validation:

validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, on: :create }

Refer to the Rails validations doc.

1 Comment

Always is good idea to scroll to bottom. Only one point against is it accepts foo,[email protected]. Not a problem for my situation.
9

@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" <[email protected]>')
=> #<Mail::Address:70343691638900 Address: |Jon Doe <[email protected]>| >
>> a.address
=> "[email protected]"
>> 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

So I use the above for "pretty email" validation and the https://github.com/balexand/email_validator for standard email validation.

1 Comment

Mail::Address.new('abc') is for mail to the local network. Check the valid_email2 gem. Really, send a test email.
8

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

Hope it helps!

2 Comments

Care to show how this would be called in the User model?
It's been a while since I looked at this, but I believe it should just be validates :email, email: true
5

Try validates_email_format_of gem.

4 Comments

That's a nice, simple gem. Thanks for pointing that one out. I'd much rather use this than inline regex.
How could I use this library as simple function returning boolean value? Docs shows only usage with error message, but I'd like to avoid throwing error.
It's not maintained, and there's several false positive issues not being addressed at all. Use wisely.
I glanced quickly at this gem. Other than maintenance concerns, it's doing a DNS resolutions step. Personally, I do not like the idea of having a network dependency from a validator on a model.
5

Is better to follow Rails documentation:

https://guides.rubyonrails.org/active_record_validations.html#custom-validators

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

4 Comments

Very restrictive (and not even close to the "real" email regex, which is a horrible abuse of regexes :)
@DaveNewton Your comment is not adding anything to it. Share with us the correct regex/way, please. :)
Here's what it adds: (1) It's a restrictive regex, e.g., it does not cover many real-world email cases, and (2) hints there's an actual reasonably-complete regex, e.g., stackoverflow.com/q/20771794/438992. As the regex is a page long I cannot include it in a comment. For all but the simplest cases (which, fortunately, include a lot of email addresses) a regex is not the best approach.
the guide was updated edgeguides.rubyonrails.org/… it uses URI::MailTo::EMAIL_REGEXP now
4

You can try this

VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i

Comments

4

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:

https://www.youtube.com/watch?v=xxX81WmXjPg

1 Comment

while this works for sign up it would be too complex for simple data collection, e.g. website contact forms
3

try this.

validates_format_of  :email, :with => /^[\+A-Z0-9\._%-]+@([A-Z0-9-]+\.)+[A-Z]{2,4}$/i

Comments

3
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
!("[email protected]" =~ VALID_EMAIL_REGEX).nil?

1 Comment

Very restrictive (and not even close to the "real" email regex, which is a horrible abuse of regexes :)
3

Please, be aware that for now email_validator gem does not have complex validation rules, but only:

/[^\s]@[^\s]/

https://github.com/balexand/email_validator/blob/master/lib/email_validator.rb#L13

Argumentation is in https://medium.com/hackernoon/the-100-correct-way-to-validate-email-addresses-7c4818f24643

Comments

1

I had the same issue, and it was fixed with a simple one-line gem

gem 'validates_email_format_of'

in my model I added

validates :email, email_format: { message: 'Invalid email format' }

I hope this helps any newbie's like myself :)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.