Guides /

Rails

RB model validator

A custom validator on your User model. It runs on create only and adds a normal error to email.

The code

1 Keep the key on the server

MAILVET_KEY=mv_live_your_key

Set it wherever your server reads its environment. Never put it in client code.

2 Write the validator

app/validators/mailvet_validator.rb

require "json"
require "net/http"

class MailvetValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    return if value.blank?

    check = lookup(value)
    return if check.nil? # a signup shouldn't die because of us

    hint = check["did_you_mean"]
    if hint.present?
      local = value.rpartition("@").first
      record.errors.add(attribute, "looks mistyped. Did you mean #{local}@#{hint}?")
    elsif check["verdict"] == "invalid"
      record.errors.add(attribute, "can't receive mail")
    elsif check["verdict"] == "risky" && check["disposable"]
      record.errors.add(attribute, "is a throwaway address. Please use a permanent one")
    end
  end

  private

  def lookup(email)
    uri = URI("https://mailvet.dev/v1/check")
    uri.query = URI.encode_www_form(email: email)
    headers = { "Authorization" => "Bearer #{ENV.fetch("MAILVET_KEY")}" }

    res = Net::HTTP.start(uri.host, uri.port, use_ssl: true,
                          open_timeout: 2, read_timeout: 5) do |http|
      http.get(uri.request_uri, headers)
    end
    return JSON.parse(res.body) if res.is_a?(Net::HTTPSuccess)

    Rails.logger.warn("mailvet: HTTP #{res.code}")
    nil
  rescue StandardError => e
    Rails.logger.warn("mailvet: no answer: #{e.message}")
    nil
  end
end

3 Add it to the model

app/models/user.rb

class User < ApplicationRecord
  validates :email, presence: true
  validates :email, mailvet: true, on: :create
end

What to do with the answer

did_you_mean, invalid and disposable add an error to email, so save returns false and your form shows it. Role addresses and a risky from a DNS failure add nothing.

  • valid Let it through.
  • risky Read disposable. If it’s true, reject. If false, it’s a role address or DNS couldn’t be reached, so let it through.
  • invalid Bad format, or the domain has no mail server. Reject.

did_you_mean is empty or holds a domain, such as gmail.com for gamil.com. It doesn’t change the verdict. A known typo can come back valid because squatters give those domains a mail server, so check it first. See the docs for every field.

If MailVet is down, the signup goes through

lookup rescues timeouts, network errors, a missing key and bad JSON, and returns nil on non-2xx answers. The validator then returns without adding an error, so the record saves.

A 401 or a 429 takes the same path, so a bad key or a used-up quota means addresses pass unchecked. Watch your logs for it.

Watch out

  • on: :create keeps existing users safe. If their domain gets listed later, they can still save their profile.
  • Rails validates on every save attempt. A signup that fails on another field and retries spends another check.

Sources

Compared with these on November 5, 2026. If they’ve changed since, trust them over this page.

Other guides

Get API key