Rubyでのメールアドレス検証には、1つのメソッド名の裏に3つの仕事が隠れています。 多くのチュートリアルは正規表現を示し、それに通れば有効なアドレスとみなします。 しかしパターンに一致するのは、文字列の形が正しいことの証明にすぎず、メールが実際に届くことまでは保証しません。 本来の検証は層になっています。 負荷の軽い構文チェック、ドメインのメールサーバーを調べるDNSルックアップ、そして実際のメールボックスに対するSMTPプローブです。 Rubyの標準ライブラリだけで、gemなしで最初の2つを実装できます。 3つ目はネットワークの問題であり、外部に任せる価値があります。 このガイドでは、各層を動くコードで組み立て、標準ライブラリの限界がどこにあるのかを示します。
結論#
構文チェックにはuriライブラリのURI::MailTo::EMAIL_REGEXP、MXルックアップにはresolvライブラリのResolv::DNS、SMTPによるメールボックスの確認には検証APIを使います。
コストの低い順に実行し、いずれかの層で結果が確定した時点で打ち切ります。
自社アプリからnet/smtpでメールボックスをプローブするのは避けてください。
ほとんどのホストでは外向きのポート25がブロックされています。
さらに、結果は送信元IPのレピュテーションに左右され、グレーリスティングへの対応も自前で再実装したくない部分です。
各層は、前の層よりも低いコストでアドレスを除外できます。
ただし、アドレスが有効だと判断できるのは最後の層だけです。
この最後の層の穴を埋めるために存在するのが、メールアドレス検証です。
第1層:構文#
正規表現を使うのはこの層だけです。しかもRubyでは、自分で書く必要すらありません。
標準ライブラリのuriには、十分にテストされたパターンがURI::MailTo::EMAIL_REGEXPとして用意されています。
Stack Overflowから貼り付けたパターンではなく、こちらを使ってください。
require "uri"
def valid_syntax?(email)
email.is_a?(String) &&
email.length <= 320 &&
email.match?(URI::MailTo::EMAIL_REGEXP)
end
valid_syntax?("jane@example.com") # => true
valid_syntax?("not-an-email") # => false
valid_syntax?("a@@b.com") # => false
この定数について、知っておくべき点が2つあります。
1つ目は、すでに\Aと\zでアンカーされていることです。
そのためmatch?は部分文字列ではなく文字列全体を判定し、自分でラップしたりアンカーを付けたりする必要はありません。
2つ目は、意図的に寛容な作りになっていることです。
WHATWG/HTML5の有効なメールアドレスの定義に従っており、RFC 5322よりも緩く、たとえばドメインにドットを要求しないためjane@localhostも受け付けます。
これは欠点ではなく仕様です。
第1層の役割は、入力欄での打ち間違いを見つけることであり、RFCの細部を議論することではありません。
議論したところで、役には立ちません。
320文字の上限は念のための保険です。
それより長い実在のアドレスはありませんし、巨大な文字列を後続の処理に渡すより早めに弾くほうが低コストです。
ここを通過したことは「確認する価値がある」という意味であり、「有効」という意味ではありません。
第2層:ドメインはメールを受信できるか#
ここでRubyの標準ライブラリが活躍します。
メールの経路がないドメインは、誰宛てのメールも受け付けられません。
そのためMXルックアップを1回行うだけで、存在しないドメイン、社名のスペルミス、架空のTLDを除外できます。
resolvライブラリを使えば、gemなしでレコードを解決できます。
require "resolv"
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
mail_route?("gmail.com") # => true
mail_route?("company-that-folded.com") # => false
Resolv::DNS.openはリゾルバーを渡し、ブロックの終了時に閉じます。
getresourcesはMXレコードの配列を返します。
ドメインがMXを公開していない場合も、ドメイン自体が存在しない場合も空の配列になり、これがまさに検出したい「経路なし」のケースです。
rescueはもう1つの失敗パターン、つまりリゾルバーのエラーやタイムアウトを処理します。
Resolv::ResolvTimeoutはResolv::ResolvErrorのサブクラスではない点に注意してください。
動作の遅いネームサーバーで呼び出し全体が例外で落ちないようにするには、両方を指定する必要があります。
可否だけでなく実際のメールホストが必要な場合は、ログに残したり優先度を確認したりできます。
各レコードはexchangeとpreferenceを持つので、後者でソートします。
def mail_hosts(domain)
records = Resolv::DNS.open do |dns|
dns.getresources(domain, Resolv::DNS::Resource::IN::MX)
end
records.sort_by(&:preference).map { |mx| mx.exchange.to_s }
# => ["gmail-smtp-in.l.google.com", "alt1.gmail-smtp-in.l.google.com", ...]
rescue Resolv::ResolvError, Resolv::ResolvTimeout
[]
end
一部のドメインは、暗黙のMXとしてAレコードでメールを受け付けます。
このエッジケースに対応する場合は、MXが空だったときにgetresources(domain, Resolv::DNS::Resource::IN::A)へフォールバックします。
実在するアドレスの大半では、MXのチェックが適切なフィルターです。
第3層:メールボックスは実在するか#
第1層と第2層でできるのは、アドレスを除外することだけです。
ドメインが正しいMXレコードを公開していても、手元のアドレスにメールボックスがあるとは限りません。
noreply-9f2x@gmail.comは、有効なメール経路を持つドメイン上の構文的に正しいアドレスですが、作成されたことのないメールボックスです。
特定のメールボックスの存在を確認するには、SMTPの配送時のやり取りが必要です。
メールホストに接続してRCPT TOを発行し、応答を読み、何も送らずに切断します。
実際にはそれほど単純ではありません。
すべてのアドレスを受け付けて単純なプローブを無効にするキャッチオールドメインなども含め、全体の流れはメールアドレス検証の仕組みで解説しています。
Rubyの標準ライブラリのnet/smtpを使えば、このやり取りを簡単に開始できます。
問題はコードではなくネットワークです。
アプリケーションサーバーからSMTPプローブを実行すると、3つの問題が起こります。
ほとんどのクラウドプロバイダーが外向きのポート25を完全にブロックしています。
結果は接続元IPのレピュテーションに左右されます。
受信サーバーは見慣れない送信者をグレーリスティングしたりレート制限したりします。
そのため、手元のノートPCでは動くプローブが、本番では何も言わずに失敗したり、ブラックリストに載ったりします。
この層は、外部に任せる価値があります。
APIで完全なチェックを行う#
Qualisendの検証エンドポイントは、構文、DNS、SMTPによるメールボックスのプローブという一連の処理を、そのために構築されたレピュテーション管理済みのインフラから実行します。
高速なチェックによる判定がすぐに返り、プローブの実行中は最終的なメールボックスの判定を取得するためのジョブIDも返ります。
単純なJSONのPOSTなので、標準ライブラリのnet/httpだけで、gemなしで呼び出せます。
キーはハードコードせず、環境変数から読み込んでください。
require "net/http"
require "json"
require "uri"
VERIFY_URL = URI("https://app.qualisend.com/api/v1/verify")
# A timeout, 402 (out of credits), 429 (rate limited) or 5xx: no verdict right now.
class VerifyUnavailable < StandardError; end
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) do |http|
http.request(request)
end
if %w[402 429].include?(response.code) || response.is_a?(Net::HTTPServerError)
raise VerifyUnavailable, "Qualisend responded #{response.code}"
end
raise "Qualisend responded #{response.code}" unless response.is_a?(Net::HTTPSuccess)
JSON.parse(response.body, symbolize_names: true)
rescue Net::OpenTimeout, Net::ReadTimeout => e
raise VerifyUnavailable, e.message
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に応じて処理します。
body = verify("jane@example.com")
result = body[:result]
result[:status] # "deliverable" | "risky" | "undeliverable" | "unknown"
result[:score] # confidence, 0–100
result[:reason] # machine-readable reason, or nil
result[:sub_flags] # { role:, disposable:, catch_all:, ... }
result[:smtp] # "pending" while the mailbox probe runs, else "cached" | "skipped"
body[:job_id] # where the final verdict lands when smtp is "pending"
result[:smtp]が"cached"または"skipped"の場合、resultはすでに最終結果です。
"pending"の場合は、メールボックスのプローブがまだ実行中で、すぐに返った判定は高速なチェックによるものです。
最終的な判定はGET https://app.qualisend.com/api/v1/jobs/{job_id}で取得できます。
更新のために/verifyをもう一度呼ばないでください。
job_idを保存し、ジョブが完了するまでバックグラウンドジョブから数秒おきにこのエンドポイントをポーリングします。
net/httpなら依存関係が増えないため、バックグラウンドジョブや小規模なサービスで便利です。
アプリがすでにFaradayやHTTPartyを使っているなら、同じリクエストをさらに数行短く書けます。読み取るエンベロープは同じです。
Rubyでメールアドレスを検証する完全な方法#
3つの層をコストの低い順につなぎ、いずれかで結果が確定した時点で止めます。
def validate_email(email)
return { status: "undeliverable", reason: "invalid_email" } unless valid_syntax?(email)
domain = email.rpartition("@").last
return { status: "undeliverable", reason: "invalid_domain" } unless mail_route?(domain)
verify(email)[:result] # deliverable | risky | undeliverable | unknown
rescue VerifyUnavailable
{ status: "unknown", reason: "unknown" } # fail open: let it through, re-verify later
end
rpartition("@")は最後の@で分割するので、引用符付きのローカル部のような変則的なアドレスでも、常に本物のドメインを取り出せます。
ローカルの2つの層はコストがかからず、不要なアドレスの大半をすぐに除外できます。
APIが動くのは、ネットワークの往復に見合うアドレスだけです。
検証を確実にするのは言語ではなく、この順序です。
同じ構成は、このガイドのNode.js版とPython版にも見られます。
Railsなら、全体をモデルのカスタムバリデーションに組み込めるので、不正なアドレスがデータベースに到達しません。
class User < ApplicationRecord
validate :email_must_be_deliverable
private
def email_must_be_deliverable
return if email.blank?
verdict = validate_email(email)
errors.add(:email, "doesn't look deliverable") if verdict[:status] == "undeliverable"
end
end
サインアップ時のチェックは、フェイルオープンにしてください。
validate_emailはタイムアウト、402、429、5xxをunknownに変換します。
そのため、APIの障害、レート制限、クレジット残高の不足があっても、実在の顧客を締め出すことはありません。
該当するアドレスは、あとで再検証してください。
同期処理の範囲は、高速なローカルの2つの層と、APIから即座に返る判定までにとどめます。 それより時間のかかる処理は、バックグラウンドジョブに回してください。 とはいえ中核は素のRubyなので、Sinatraのルートでも、rakeタスクでも、使い捨てのスクリプトでも同じように動きます。 成果を生むのは層構造です。
よくある質問
01gemなしでRubyのメールアドレス検証はできますか?
3つの層のうち2つはできます。構文はuriライブラリのURI::MailTo::EMAIL_REGEXP、MXレコードの解決はresolvライブラリのResolv::DNSで処理でき、どちらもRubyに同梱されているため、構文とドメインのチェックには何もインストールする必要はありません。3つ目のメールボックス自体の確認にはSMTPプローブが必要ですが、これは自社のサーバーから実行すべきではありません。その部分は検証サービスに任せられます。
02URI::MailTo::EMAIL_REGEXPだけで十分ですか?
第1層のチェックとしては適切で、自作の正規表現よりはるかに確実です。ただし検証できるのはアドレスの形であり、到達可能かどうかではありません。DNSを解決せず、メールサーバーにも接続しません。また設計上寛容で、jane@localhostも受け付けます。通過しても「メールアドレスの形をしている」という意味であって、「配信できる」という意味ではありません。アドレスを信頼する前に、MXルックアップとメールボックスの確認を組み合わせてください。
03メールボックスの存在確認にnet/smtpを使わないのはなぜですか?
net/smtpでSMTPのやり取りを開始することはできますが、アプリケーションサーバーから実行するのは不安定でリスクもあります。ほとんどのホストが外向きのポート25をブロックしていること、結果が送信元IPのレピュテーションに左右されること、そして一定量のプローブを行うとレート制限を受けたりブラックリストに載ったりすることが理由です。検証APIなら、まさにその目的で構築された、レピュテーション管理済みのインフラからプローブを実行します。
04メールアドレスの検証は、サインアップ時とリストのクリーニング時のどちらで行うべきですか?
どちらでも行いますが、深さが異なります。サインアップ時は、構文チェックとMXルックアップを同期的に実行します。これらはリクエストをブロックしても問題ない速さで、すぐにフィードバックを返せます。APIから即座に返る判定も、その場で利用します。より深く時間のかかる処理は、リストのクリーニングやバックオフィスのジョブに回してください。実際の流れは、サーバーレスのサインアップガイドで最初から最後まで確認できます。
メールボックスの層を追加する準備はできましたか。APIリファレンスには、/verifyエンドポイント、スコープ付きキー、そのままコピーできるスニペットがあります。
あるいは、無料メールチェッカーにアドレスを貼り付けて、構文上は完璧な文字列が配信不可と判定される様子を確かめてください。



