# Validate an email address in Ruby

_2026-11-25 · Boundstone (https://boundstone.io/blog/validate-email-ruby)_


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.

```ruby
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](/blog/what-is-an-mx-record).)

```ruby
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:

```ruby
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](https://rubygems.org/gems/truemail) gem does it cleanly.

```ruby
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.

```ruby
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](/tools/email-validator).

## The short version

- Syntax: `URI::MailTo::EMAIL_REGEXP.match?(email)` from stdlib. No gem.
- Domain can receive mail: a `Resolv::DNS` MX lookup. Also stdlib.
- Those two cover most real validation. Ship them first.
- Want it bundled: `truemail` with `:regex`/`:mx`. Skip `:smtp`.
- Want disposable/role plus one contract across your services: the API — and read `checks.not_performed` so you know exactly what a "valid" did and did not verify.

Working in more than one language? The same approach in [Python is here](/blog/validate-email-python).
