Skip to content
無料の100クレジットで検証を始める
Qualisend
すべての記事
エンジニアリング / 2026年5月12日

Railsでメールアドレスを検証する方法

約12分で読めます

Qualisend team
Railsでメールアドレスを検証する方法

Railsでメールアドレスを検証しようとすると、Active Recordのおかげで簡単に済んだように見えます。validates :emailと書き、テストが通るのを確認して、次の作業へ進みます。しかしその1行が答えているのは、本来の検証が問う3つの問いのうち最初の1つだけです。アドレスの形式は正しいか。そのドメインはメールを受信できるか。そのメールボックスは実際に存在するか。Railsは1つ目にはすっきりした答えを用意していますが、残りの2つには何も用意していません。このガイドでは、各レイヤーをRailsらしいバリデーションとして組み立てます。formatルールと、ActiveModel::EachValidatorを継承したカスタムバリデーター2つです。あわせて、フレームワークの守備範囲がどこまでかも示します。Rubyのメールアドレス検証ガイドの続きにあたる内容で、Railsは同じ標準ライブラリの部品を、宣言的なバリデーションAPIで包んでいます。

結論#

構文にはvalidates :email, format: { with: URI::MailTo::EMAIL_REGEXP }、MXルックアップには標準ライブラリresolvを使ったカスタムバリデーター、SMTPによるメールボックスの確認には検証APIを使います。1つの属性に3つのバリデーションを、コストの低い順に並べます。それぞれ、より低コストなレイヤーがすでに失敗していれば処理を飛ばします。メールボックスを調べるために、アプリからSMTPセッションを開くのはやめましょう。多くのホストではアウトバウンドのポート25がブロックされており、結果は送信IPのレピュテーションやグレーリスティングに左右されます。これを自前で再実装したくはないはずです。Railsには、bailキーワードを持つフレームワークと異なる点が1つあります。Active Modelは宣言したすべてのバリデーターを実行してエラーを集めるので、処理の打ち切りはフラグで指定するものではなく、自分で加えるガードになります。各レイヤーは、前のレイヤーよりも低コストでアドレスを除外します。アドレスを有効と判断できるのは、最後のレイヤーだけです。

レイヤー1:validatesルールによる形式の検証#

レイヤー1はRailsの担当範囲で、正規表現を自分で書く必要もありません。Railsがすでに読み込んでいるRubyのuriライブラリには、十分にテストされたパターンがURI::MailTo::EMAIL_REGEXPとして含まれています。Stack Overflowから貼り付けたものではなく、これをformatバリデーションに渡します。

app/models/user.rb

class User < ApplicationRecord
  validates :email,
            presence: true,
            format: { with: URI::MailTo::EMAIL_REGEXP }
end

この定数は、Rubyガイドでも使っているものです。すでに\Aと\zでアンカーされているため文字列全体を検証し、意図的に緩めに作られています。WHATWG/HTML5の定義に従っているのでRFC 5322より緩く、ドメインにドットを要求しないためjane@localhostも問題なく受け入れます。これは欠点ではなく利点です。レイヤー1の役目は、入力ミスをその場で拾うことで、RFCの解釈を争うことではありません。争っても役には立ちません。

大切なのは、Railsの守備範囲がどこまでかを知ることです。formatルール、スキャフォールド、email_fieldヘルパーなど、Railsに組み込まれたメールアドレス関連の機能はすべて、形式だけを検証します。DNSを解決することも、メールサーバーに接続することもないため、noreply-9f2x@gmail.com、typo@gmial.com、sales@company-that-folded.comはすべて通過します。この3つはいずれも配信できません。Railsには、到達可否のチェックがまったく用意されていません。

レイヤー2:ドメインはメールを受信できるか#

ここからがRailsの用意していないレイヤーですが、後から追加するのは簡単です。メールの経路がないドメインは誰宛てのメールも受け付けられないため、MXルックアップを1回行うだけで、存在しないドメインや会社名のつづり間違い、架空のTLDを除外できます。Railsのバリデーターは、ActiveModel::EachValidatorを継承してvalidate_eachを実装しただけのクラスです。そこでresolvライブラリのMXルックアップを、そのクラスで包みます。

app/validators/deliverable_domain_validator.rb

require "resolv"

class DeliverableDomainValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    return if value.blank? || record.errors[attribute].present?

    domain = value.rpartition("@").last
    return if domain.present? && mail_route?(domain)

    record.errors.add(attribute, options[:message] || "can't receive email")
  end

  private

  def mail_route?(domain)
    records = Resolv::DNS.open do |dns|
      dns.getresources(domain, Resolv::DNS::Resource::IN::MX)
    end
    !records.empty?
  rescue Resolv::ResolvError, Resolv::ResolvTimeout
    false
  end
end

Resolv::DNS.openはリゾルバーを渡し、ブロックが終了すると閉じます。getresourcesはMXレコードの配列を返します。ドメインがMXを公開していない場合、またはドメイン自体が存在しない場合は空になり、これがまさに「経路なし」のケースです。もう1つの失敗パターンはrescueで処理します。Resolv::ResolvTimeoutはResolv::ResolvErrorのサブクラスではないので、応答の遅いネームサーバーをきれいにやり過ごしたいなら、両方を指定してください。モデルへの組み込みは、推測されるキーで行います。Railsはdeliverable_domainをキャメルケースのDeliverableDomainValidatorに変換し、Zeitwerkがapp/validatorsから自動で読み込んだクラスを見つけます。

validates :email,
          presence: true,
          format: { with: URI::MailTo::EMAIL_REGEXP },
          deliverable_domain: true

return if ... record.errors[attribute].present?というガードがこの仕組みの肝です。Active Modelは宣言したすべてのバリデーターを実行してエラーを集約するため、bailはありません。ただしバリデーションは宣言した順に実行されるので、このバリデーターが動く時点では、formatの失敗がすでにrecord.errors[:email]に入っています。ガードがDNSルックアップをまるごと飛ばします。形式が不正なアドレスのために、ネームサーバーへ問い合わせることはありません。

レイヤー3:メールボックスは実際に存在するか#

レイヤー1と2でできるのは、アドレスを除外することだけです。ドメインに完璧なMXレコードがあっても、手元のアドレスにメールボックスがないことはあります。noreply-9f2x@gmail.comは、構文として有効でメールの経路も実在しますが、作成されたことのないメールボックスです。特定のメールボックスを確認するには、SMTPの配信時のやり取りが必要です。メールホストに接続してRCPT TOを発行し、応答を読み取り、何も送らずに切断します。実際にはもう少し複雑で、すべてのアドレスを受け入れて単純なプローブを無効にするキャッチオールドメインも含めたパイプライン全体は、メールアドレス検証の仕組みで解説しています。

Rubyのnet/smtpを使えば、このやり取りは問題なく開始できます。問題はコードではなく、ネットワークです。アプリケーションサーバーからSMTPプローブを実行すると、3つの問題が起きます。ほとんどのクラウドプロバイダーはアウトバウンドのポート25を完全にブロックしています。結果は接続元のIPのレピュテーションに左右されます。受信サーバーは見慣れない送信元をグレーリスティングしたりレート制限したりします。そのため、手元のノートPCで動くプローブが、本番では黙って失敗するか、ブラックリストに載る原因になります。このレイヤーこそ、外部に任せる価値があります。

カスタムバリデーターからQualisendを呼び出す#

検証APIも、結局はもう1つのバリデーターにすぎないので、処理を任せても構成はすっきり保てます。Qualisendの検証エンドポイントは、構文、DNS、SMTPによるメールボックスプローブというパイプライン全体を、レピュテーションを管理したインフラから実行します。高速なチェックによる判定はすぐに返り、プローブがまだ実行中の間は、最終的なメールボックスの判定を取得するためのジョブIDも返します。単純なJSONのPOSTなので、標準ライブラリのnet/httpだけで済み、gemは不要です。キーはハードコードせず、環境変数から読み取ります。

app/validators/deliverable_email_validator.rb

require "net/http"
require "json"
require "uri"

class DeliverableEmailValidator < ActiveModel::EachValidator
  VERIFY_URL = URI("https://app.qualisend.com/api/v1/verify")

  def validate_each(record, attribute, value)
    return if value.blank? || record.errors[attribute].present?

    result = verify(value)
    return if result.nil? # our outage shouldn't block a real signup

    if result["status"] == "undeliverable"
      record.errors.add(attribute, options[:message] || "appears to be undeliverable")
    end
  end

  private

  def verify(email)
    request = Net::HTTP::Post.new(VERIFY_URL)
    request["Authorization"] = "Bearer #{ENV.fetch('QUALISEND_API_KEY')}"
    request["Content-Type"]  = "application/json"
    request.body = JSON.generate(email: email)

    response = Net::HTTP.start(
      VERIFY_URL.hostname, VERIFY_URL.port,
      use_ssl: true, open_timeout: 5, read_timeout: 10
    ) { |http| http.request(request) }

    return nil unless response.is_a?(Net::HTTPSuccess)

    JSON.parse(response.body)["result"]
  rescue StandardError
    nil # unreachable or slow — treat as unknown and re-verify later
  end
end

リクエストの形式とスコープ付きキーの発行方法は、APIリファレンスで確認できます。レスポンスにはjob_idが含まれ、resultオブジェクトが入っています。

{
  "job_id": "8d1f0c2e-5b7a-4c3e-9f61-2a4b6c8d0e13",
  "result": {
    "status": "unknown",
    "score": 61,
    "smtp": "pending",
    "reason": null,
    "sub_flags": { "role": false, "disposable": false, "catch_all": false }
  }
}

statusはdeliverable、risky、undeliverable、unknownのいずれかです。上のバリデーターが拒否するのはundeliverableだけで、riskyとunknownは通します。判断の難しいアドレスをフォームのエラーにせず、後続の処理でscoreによる制御や、disposableなどのsub_flagsによる分岐を決められるようにするためです。リクエスト自体が失敗した場合、verifyはnilを返し、バリデーターはエラーを追加しません。構文とMXのレイヤーはすでにローカルで実行済みなので、緩めるのをネットワークのレイヤーだけにすることで、一時的な障害が本物のサインアップを妨げないようにしています。上のJSONは契約ではなく、形の目安として見てください。全フィールドの一覧は開発者向けドキュメントにあります。

smtpフィールドは、その判定がどこまで最終的なものかを示します。cachedまたはskippedなら、resultはすでに最終結果です。pendingの場合は、メールボックスプローブがまだ実行中です。即時の判定は高速なチェックによるもので、最終的な判定はGET https://app.qualisend.com/api/v1/jobs/{job_id}?include=resultsで取得できます。バリデーターの中でその完了を待たないでください。更新のために/verifyをもう一度呼び出すのも避けてください。job_idを保存し、バックグラウンドジョブで数秒ごとにこのエンドポイントをポーリングします。ジョブのstatusがcompletedになったら、resultsにあるそのアドレスのstatusに基づいて処理します。

3つのレイヤーを組み合わせてRailsでメールアドレスを検証する#

3つのバリデーションを属性に、コストの低い順で宣言します。宣言順がそのまま実行順で、より低コストなレイヤーがすでに失敗していれば各ガードが処理を飛ばすので、組み合わせたルールは、実行される順序のまま上から下へ読めます。

app/models/user.rb

class User < ApplicationRecord
  validates :email,
            presence: true,
            format: { with: URI::MailTo::EMAIL_REGEXP },
            deliverable_domain: true,
            deliverable_email: true
end

これでuser.saveがAPIに到達するのは、構文とMXのチェックに通ったアドレスだけになります。入力ミスはformatで失敗し、ネームサーバーへの往復もAPIクレジットの消費も発生しません。存在しないドメインは、ネットワーク呼び出しの前にdeliverable_domainで失敗します。user.valid?はチェーン全体を実行し、user.errors[:email]には、エラーを出したレイヤーのメッセージが入ります。検証を確実にするのはフレームワークではなく、この順序です。Ruby版と同じ構成を、手でつないだメソッドの代わりに宣言的なバリデーションで表現したものです。

これらは通常のバリデーションなので、標準のオプションがすべて使えます。ネットワークを使う2つのレイヤーにはif: :email_changed?を付けて対象を絞ります。そうすれば、関係のないuser.update(name: ...)のたびに、検証済みのアドレスに対してDNSルックアップが再実行されたりクレジットが再び消費されたりすることはありません。同期的に実行するのは、サインアップ時の高速なローカルレイヤーと、APIの即時の判定までにとどめてください。それより重い処理はバックグラウンドジョブの役目です。リアルタイムのパターンはサーバーレスのサインアップガイドに、バッチ側の処理はメールリストのクリーニング方法にまとめています。

よくある質問

01Railsには、メールアドレスが配信可能か確認する組み込みの方法がありますか?

ありません。URI::MailTo::EMAIL_REGEXPを使ったformatバリデーション、スキャフォールド、email_fieldヘルパーなど、Railsに組み込まれたメールアドレス関連の機能は、どれも形式しか検証しません。DNSの解決もメールサーバーへの接続も行わないため、架空のドメインや存在しないメールボックスも通過します。Railsにはどちらも用意されていないので、到達可否の確認には、MXルックアップとSMTPによるメールボックスの確認を、カスタムバリデーターとして追加してください。

02Railsでカスタムのメールアドレスバリデーターを書くにはどうすればよいですか?

ActiveModel::EachValidatorを継承してvalidate_each(record, attribute, value)を実装し、値が検証に失敗したらrecord.errors.add(attribute, message)を呼び出します。クラスはapp/validatorsに置いてZeitwerkに自動で読み込ませ、推測されるキーで指定します。たとえばDeliverableDomainValidatorはdeliverable_domain: trueとして組み込めます。他のバリデーションと同じオプションが使えるため、if:、on:、カスタムのmessage:もそのまま機能します。

03APIを使わずに、Railsでメールボックスの存在を確認できますか?

一部は可能です。標準ライブラリのresolvを使えば、ドメインがメールを受け付けることを確認でき、カスタムバリデーターの中から存在しないドメインを無料で除外できます。メールボックスの確認にはSMTPのやり取りが必要です。net/smtpで開始することはできますが、アプリサーバーから実行すべきではありません。アウトバウンドのポート25は広くブロックされており、結果は送信IPのレピュテーションに左右されるからです。この最後のレイヤーは、そのために構築されたレピュテーション管理済みのインフラから、検証サービスが実行します。

04メールアドレスの検証は、サインアップ時とリストのクリーニング時のどちらで行うべきですか?

両方で行いますが、深さが異なります。サインアップ時には、構文とMXのレイヤーを同期的に実行します。リクエストをブロックしても支障のない速さで、即座にフィードバックを返せるからです。ここではAPIの即時の判定にも基づいて処理します。より深いバッチ検証は、既存リストのクリーニングのために取っておきます。そこでは応答速度よりも徹底さが重要だからです。ネットワークを使うバリデーターをif: :email_changed?で絞り込めば、通常の更新で、すでに確認したアドレスを再検証せずに済みます。


メールボックスのレイヤーを追加してみませんか。APIリファレンスには、/verifyエンドポイントとスコープ付きキー、コピー&ペースト用のスニペットがあります。または無料メールチェッカーにアドレスを入力すると、構文上は完璧な文字列が配信不可と判定される様子を確認できます。

送信者の信頼を、守る。

最初のリストを数分でクリーニング。100クレジット無料、クレジットカード登録は不要です。

今すぐ始める