Validate an email address in Ruby
Ruby's standard library validates email syntax and checks MX records with zero gems — here's the code, plus where an API actually earns its place.
Contents
You want to validate an email in Ruby, and you are one search away from a 6,000-character regex someone pasted into a gist in 2011. Put it down. Ruby's standard library ships a maintained email regex and a DNS resolver, so you can check both the shape of an address and whether its domain can receive mail without adding a single gem. Here's the code, then the honest line about where it stops.
Syntax: the stdlib already has a regex
Ruby's uri library exposes URI::MailTo::EMAIL_REGEXP, an anchored pattern maintained by Ruby core alongside the rest of the URI parser. You do not write it, you do not paste it, and it gets fixed upstream when RFC edge cases surface.
require 'uri'
URI::MailTo::EMAIL_REGEXP.match?('you@example.com') # => true
URI::MailTo::EMAIL_REGEXP.match?('you@@example.com') # => false
URI::MailTo::EMAIL_REGEXP.match?('not-an-email') # => false
match? returns a boolean and skips allocating a MatchData object, so it's the right call when all you want is a yes or no. The pattern is anchored (\A...\z), so it validates the whole string, not a substring — no need to bolt on your own anchors.
That's syntax done. For plenty of forms this is genuinely all you need: reject the typos, accept the rest, move on. But a syntactically perfect address can still point at a domain that has never received mail in its life. For that, you ask DNS.
Deliverability shape: MX lookup with Resolv
The resolv library — also stdlib — resolves DNS records without shelling out to dig or pulling a networking gem. An MX (mail exchange) record is how a domain advertises the servers that accept its email; if a domain has none, nothing is going to be delivered there. (If MX records are new to you, we wrote up what an MX record is.)
require 'resolv'
def domain_has_mx?(domain)
Resolv::DNS.open do |dns|
records = dns.getresources(domain, Resolv::DNS::Resource::IN::MX)
records.any?
end
end
domain_has_mx?('gmail.com') # => true
domain_has_mx?('example.invalid') # => false
getresources returns an array of MX records; any? is enough to answer "can this domain receive mail at all?". Wrap the two checks together and you have a stdlib-only validator:
require 'uri'
require 'resolv'
def deliverable_shape?(email)
return false unless URI::MailTo::EMAIL_REGEXP.match?(email)
domain = email.split('@', 2).last
Resolv::DNS.open do |dns|
dns.getresources(domain, Resolv::DNS::Resource::IN::MX).any?
end
end
deliverable_shape?('you@gmail.com') # => true
deliverable_shape?('you@example.invalid') # => false
Zero gems. Syntax plus a live MX lookup covers the two failures that matter most: malformed input and dead domains.
When a gem earns its place: truemail
If you want those two checks bundled with configuration, retries, and a whitelist/blacklist, the truemail gem does it cleanly.
require 'truemail'
Truemail.configure do |config|
config.verifier_email = 'verifier@yourdomain.com'
end
Truemail.validate('recipient@example.com', with: :mx).result.success # => true
with: :regex runs syntax only; with: :mx adds the DNS check you just wrote by hand. truemail also offers with: :smtp, which opens a connection to the mail server to probe the mailbox — be honest with yourself about that one: major providers rate-limit or refuse it, results are inconsistent, and it can get your sending IP flagged. Syntax and MX are the reliable tier.
What syntax and MX still can't tell you
Here's the ceiling on everything above. A valid, MX-backed address can still be a burner from a disposable-email service, a role account like info@ or sales@ that no human reads, or a free-provider throwaway. Regex and DNS cannot see any of that — those judgments need lists that someone keeps current.
That's the layer Boundstone's API adds. The same syntax and MX you'd run yourself, plus disposable and role classification, returned over one HTTP contract you can call identically from a Rails controller, a Sidekiq job, or a serverless function.
require 'net/http'
require 'json'
require 'uri'
uri = URI('https://api.boundstone.io/v1/verify/email')
req = Net::HTTP::Post.new(uri)
req['Authorization'] = 'Bearer bs_live_YOUR_KEY'
req['Content-Type'] = 'application/json'
req.body = JSON.generate(email: 'you@example.com')
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body)
data['mx_found'] # => true
data['disposable'] # => false
data['role_account'] # => false
data['checks']['performed'] # => ["syntax", "mx", "disposable_list", "role_list"]
data['checks']['not_performed'] # => ["smtp_mailbox", "catch_all"]
Every response carries checks.performed and checks.not_performed, and the second array is the point. Boundstone does not do smtp_mailbox (opening an SMTP connection to confirm the box exists) or catch_all detection. Plenty of services claim both; the results are unreliable and often blocked, so we list them as not performed rather than dress them up. A pass from us means exactly the four checks in performed ran — no more. The response also flags free_provider, though that flag isn't counted as one of the checks.
Want to eyeball an address before writing any Ruby? Paste one into the keyless email validator.
The short version
- Syntax:
URI::MailTo::EMAIL_REGEXP.match?(email)from stdlib. No gem. - Domain can receive mail: a
Resolv::DNSMX lookup. Also stdlib. - Those two cover most real validation. Ship them first.
- Want it bundled:
truemailwith:regex/:mx. Skip:smtp. - Want disposable/role plus one contract across your services: the API — and read
checks.not_performedso you know exactly what a "valid" did and did not verify.
Working in more than one language? The same approach in Python is here.