valid_email2: MX-aware email validation for Rails
ActiveModel validation for email. Including MX lookup and disposable email blocklist
At a glance
- What is it?
- valid_email2 is a Ruby gem that validates email addresses through the `mail` gem instead of a regular expression, then optionally checks that the domain has MX records, is not on a disposable address list and is not subaddressed. It is aimed at Rails applications that need to reject domains which cannot receive mail, at the cost of a DNS lookup on the validation path.
- Who is it for?
- valid_email2 is a good fit for a Rails application that is losing signups to typo domains and needs to reject disposable mailboxes, since it turns those two failures into a single validator option. It is the wrong dependency when email validation must stay offline, when you cannot add a DNS lookup to a request path, or when your team wants zero runtime gem dependencies on the `mail` gem.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 3 days ago.
- What is it written in?
- Mainly Ruby, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Email validation that parses instead of pattern matching
Most email validation in Ruby has been a regular expression, and the reasoning behind valid_email2 is stated plainly in its documentation: other gems and libraries validate addresses using what it calls clunky regexps, so this project uses the `mail` gem to parse the address instead. That is the whole architectural difference between the two approaches. A regex asks whether a string matches a shape. A parser asks whether the component parts form an address that a mail client could send to.
On top of parsing, the gem adds checks that a shape matcher cannot perform. A domain can be syntactically perfect and still be unable to receive mail, because it has no MX record and no A record fallback. The gem can look that up. A domain can also be perfectly real and still be a throwaway mailbox from a disposable address service, which is why the project ships a static list of disposable email services and lets you check against it.
The second motivation is narrower. The documentation says the author saw a need to validate that an address is not coming from a disposable email provider. That is a product decision rather than a technical one, and it explains the shape of the option list, where the disposable checks are first class options rather than a plugin.
The project describes itself as production ready and states that it is used in several production apps. It is a small, focused gem: an ActiveModel validator, a `ValidEmail2::Address` object for direct use, and a set of YAML config files in the config directory.
MX lookups, timeouts and nameservers are the parts you must configure
The DNS behaviour is the part with operational consequences. The validator can be asked to check that a domain has an MX record or an A record, which is the `mx` option, or to check strictly for an MX record, which is `strict_mx`. Both default to a five second timeout for DNS lookups. That default is generous for a request handler, so if your validations run inside a web request you will probably want to lower it with the `dns_timeout` option.
Lookups use the default `Resolv::DNS` nameservers unless you override them, and the `dns_nameserver` option takes a list of addresses. People running inside containers or on hosts with an unusual resolver configuration need this, and the option exists precisely because the default nameservers are not always reachable from every environment.
The disposable checks work in two flavours. `disposable` checks both the domain and the MX server, while `disposable_domain` checks the domain only and skips the MX server lookup. Both variants have allow list counterparts: `disposable_with_allow_list` and `disposable_domain_with_allow_list`, which consult `config/allow_listed_email_domains.yml` so that a customer domain can be exempted even if it appears on the disposable list. A separate `deny_list` option checks the domain against `config/deny_listed_email_domains.yml`.
Two further options tighten the shape check. `disallow_subaddressing` rejects addresses that use the plus tag convention described in RFC5233, and `disallow_dotted` rejects an address with a dot anywhere before the @ sign. Both are off unless you ask for them.
The disposable list itself lives in a plain text file at config/disposable_email_domains.txt, and the repository also contains pull_mailchecker_emails.rb, a script for pulling in address data.
Adding valid_email2 to a Rails application
Installation is a Gemfile line plus a bundle run. The documented steps are:
gem "valid_email2"and then:
bundleA standalone install, outside Bundler, is:
gem install valid_email2With the gem in place, the validator is used through the `valid_email_2/email` key. The minimal form inside a model is:
class User < ActiveRecord::Base
validates :email, presence: true, 'valid_email_2/email': true
endRequiring the MX check is a single extra option:
validates :email, 'valid_email_2/email': { mx: true }And the strict variant, which insists on an MX record rather than accepting an A record fallback, is:
validates :email, 'valid_email_2/email': { strict_mx: true }Combining the checks you actually care about is done in one option hash:
validates :email, 'valid_email_2/email': { mx: true, disposable: true, disallow_subaddressing: true}The validator key is deliberately namespaced, and the documentation explains why in its upgrade notes for version 2.0: other gems also define an `email` validator, which can cause Rails to pick the wrong one and accept addresses it should reject. The namespaced key removes that ambiguity.
You can also use the gem without ActiveModel, which is the path to take in a plain Ruby service. The documented example is:
address = ValidEmail2::Address.new("[email protected]")
address.valid? => true
address.disposable? => false
address.valid_mx? => true
address.valid_strict_mx? => true
address.subaddressed? => falseFor anything beyond these methods, the documentation points to lib/valid_email2/address.rb as the reference for other APIs. Multibyte characters are not accepted by default, and the documentation shows how to permit them explicitly:
ValidEmail2::Address.permitted_multibyte_characters_regex = /[ÆæØøÅåÄäÖöÞþÐð]/The allow list and deny list are yours to edit
Two YAML files in the config directory drive the list based checks, and both are plain lists of domains. The deny list file is documented like this:
# config/deny_listed_email_domains.yml
- denied1.example.com
- denied2.example.comThe allow list is the counterpart and lives at config/allow_listed_email_domains.yml. Because the files are plain YAML in your own repository, you can commit them and change them without touching the gem, which is the practical way to handle a customer who insists on using a domain that happens to sit on the disposable list.
Validation messages can also be customised. A per check message is passed inline:
validates :email, 'valid_email_2/email': { message: "is not a valid email" }And if you want a distinct error for each type of failure, the documentation shows a translations file such as en.yml with one key per check:
# set custom validation messages for each type of check in translations ie en.yml
errors:
messages:
disallow_dotted: "contains a dot"
disallow_subaddressing: "..."
disposable: "..."
disposable_domain: "..."
disposable_with_allow_list: "..."
disposable_domain_with_allow_list: "..."
deny_list: "..."
mx: "..."
strict_mx: "..."That approach is enabled with the `specific_error_messages` option, combined with the check you want:
validates :email, 'valid_email_2/email': { disallow_dotted: true, specific_error_messages: true }One more option exists for forms that accept several addresses in a single field:
validates :email, 'valid_email_2/email': { multiple: true }An empty email passes, and MX checks break offline test suites
There are two traps that the documentation flags, and both will bite a new user.
The first is that the gem lets an empty email pass through. The note in the documentation is short and direct: you need to add `presence: true` if you require an email. The example model in the README does exactly that, which is why it reads `validates :email, presence: true, 'valid_email_2/email': true`. If you copy only the validator option and forget presence, blank strings become valid submissions.
The second is the test environment. If you validate `mx`, your specs fail without an internet connection, because the validator performs a live DNS lookup. The documentation recommends stubbing the validation in your spec_helper:
config.before(:each) do
allow_any_instance_of(ValidEmail2::Address).to receive_messages(
valid_mx?: true,
valid_strict_mx?: true,
mx_server_is_in?: false
)
endThis is worth thinking about beyond tests. Any DNS lookup on a validation path adds latency and a dependency on the network, and the default five second timeout means a slow or unreachable resolver can stall a request for the length of that timeout. The `mx` option also accepts an A record fallback while `strict_mx` does not, so choosing the stricter one rejects domains that resolve but have no MX entry. Both behaviours are deliberate and documented, but neither is invisible.
A subtler failure mode is the disposable check itself. The list is static, so a disposable provider added after the list was last updated will pass validation, and a provider that has been bought and repurposed may still be blocked. The maintenance of that file is a manual task that the gem does not handle for you.
Config renames are the upgrade cost, and the licence is MIT
The upgrade notes in the documentation are mostly about file names, which tells you where the compatibility risk sits. In version 3.0.0 the config files moved out of the vendor directory and were renamed: `vendor/blacklist.yml` became `config/blacklisted_email_domains.yml` and `vendor/whitelist.yml` became `config/whitelisted_email_domains.yml`. The same release also improved the `disposable` validation with an `mx` check, and the documentation points back at the test stub for that reason.
Version 5.3.0 renamed the files again, this time to the current names: `config/blacklisted_email_domains.yml` became `config/deny_listed_email_domains.yml` and `config/whitelisted_email_domains.yml` became `config/allow_listed_email_domains.yml`. Fresh installs need no action, but anyone upgrading has to rename the files by hand. The documentation also warns that future versions may make similar changes, which is a fair warning for a project that has already done it twice.
Version 2.0.0 addressed the validator naming conflict described earlier, and the notes say the deprecation of the unqualified `email` validator began there.
The licence is MIT, which for a Rails application means you can use the gem in a proprietary product without any obligation to publish your own code or to contribute changes upstream. The only attribution requirement is retaining the copyright and licence notice in distributed copies of the gem itself.
On maintenance, the repository is not archived and the last push was on 2026-08-23, which is also the date of the v7.1.0 release. The two releases before it, v7.0.15 and v7.0.14, both landed on 2026-02-22 within minutes of each other. The changelog lives in CHANGELOG.md, and the project uses release-please, with a release-please-config.json and a manifest at the top level of the repository. Requirements are stated loosely as currently supported Ruby and Rails versions, with the build matrix in the CI workflow as the authoritative list, and the project also ships gemfiles/ for testing against multiple dependency sets and a .rubocop.yml with a rubocop_todo.yml for lint exceptions.
When a regex is the better answer
valid_email2 is not the only way to validate an email field, and the difference in approach is worth weighing before you add a DNS dependency to your model.
A regular expression validator is offline, instantaneous and predictable. It cannot tell you that a domain lacks MX records, and it cannot block disposable providers, but it also cannot hang for five seconds or fail when the resolver is down. If your form is a newsletter signup where a typo in the domain costs nothing and you would rather accept the address and let the confirmation bounce, a pattern match is the simpler tool.
The alternative inside the gem itself is to drop the DNS options and keep only the parsing. `ValidEmail2::Address#valid?` gives you a syntactic check built on the `mail` gem, with no network involved, and you can add the disposable check, which for the `disposable_domain` variant is a lookup in a local text file rather than a DNS query. That combination keeps the parsing benefit and drops the latency, at the price of not catching undeliverable domains.
The `mail` gem dependency is the other trade to consider. The gem pulls in a parser and a DNS stack to do work that a regex does inline. For an application that already depends on `mail` through ActionMailer, that weight is close to zero. For a small service that validates a handful of addresses, it is not.
The disposable checks also deserve a realistic framing. They are useful against casual signups and free mailbox abuse. They are not an identity system, they do not detect throwaway providers that are not on the list, and the documentation does not claim otherwise.
Editorial conclusion
valid_email2 is a good fit for a Rails application that is losing signups to typo domains and needs to reject disposable mailboxes, since it turns those two failures into a single validator option. It is the wrong dependency when email validation must stay offline, when you cannot add a DNS lookup to a request path, or when your team wants zero runtime gem dependencies on the `mail` gem. Before adopting it, confirm that `dns_timeout` and `dns_nameserver` fit your hosting environment, that your test suite stubs `valid_mx?` and `valid_strict_mx?` as the spec_helper example shows, and that your config files use the current `config/deny_listed_email_domains.yml` and `config/allow_listed_email_domains.yml` names.
Frequently asked questions
How do I install valid_email2 in a Rails app?
Add `gem "valid_email2"` to your Gemfile and run `bundle`. Outside Bundler the documented command is `gem install valid_email2`.
How do I make valid_email2 check that a domain has an MX record?
Use the `mx` option for an MX record or A record check, and `strict_mx` for an MX record only. Both default to a five second DNS timeout that can be changed with the `dns_timeout` option, and the nameservers can be replaced with `dns_nameserver`.
Does valid_email2 block disposable email providers?
Yes, through the `disposable` option, which checks the domain and the MX server, or `disposable_domain`, which checks the domain only. The disposable domains are listed in config/disposable_email_domains.txt, and allow list variants can exempt specific domains from config/allow_listed_email_domains.yml.
Why do my valid_email2 specs fail without an internet connection?
The `mx` check performs a live DNS lookup. The documentation recommends stubbing `valid_mx?`, `valid_strict_mx?` and `mx_server_is_in?` in a `config.before(:each)` block in your spec_helper.
Does valid_email2 reject subaddressed addresses such as [email protected]?
Only when you ask for it. The `disallow_subaddressing` option rejects addresses using the plus tag convention from RFC5233, and `disallow_dotted` rejects an address with a dot anywhere before the @ sign.
What changed in the valid_email2 config file names in v5.3.0?
config/blacklisted_email_domains.yml was renamed to config/deny_listed_email_domains.yml and config/whitelisted_email_domains.yml to config/allow_listed_email_domains.yml. Fresh installs need no change, but upgrades have to rename the files.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/micke-valid-email2)Was this useful?