はじめに
通常のサインアップと言えば「登録 → 確認メール送信 → メール内リンクをクリック」というフローが定番ですが、最近はこれがだんだんつらくなってきました。
- 迷惑メール行きでユーザーが気づかない
- メール遅延でオンボーディングが途切れる
- 間違ったアドレスに大量送信して bounce が増える → ドメイン評価が下がる
そこで、メールを一通も送らずに「配信可能なメールアドレスかどうか」を検証してからサインアップを通す構成に変えてみたのでメモします。
やりたいこと
いわゆる Email Verification API を使って、入力されたメールアドレスに対して以下のチェックを行います。
- Syntax チェック: RFC 準拠かどうか
- Domain チェック: DNS lookup でドメインが存在するか
- MX レコード: メールサーバーが設定されているか
- SMTP チェック:
RCPT TOを投げたときにサーバーが受け付けるか
自前で SMTP セッションを書くこともできますが、実運用では Hunter / ZeroBounce などの API を叩くだけにするのが現実的です。 API のレスポンスはだいたい valid / invalid / catch-all / unknown といったステータスで返ってきます。
User モデルのカラム構成
User モデルには、メール検証用のカラムを追加しました。
# users テーブル
t.string :email_status, null: false, default: "unknown"
t.datetime :email_verified_at
t.jsonb :email_verification_meta, null: false, default: {}
-
email_status:"valid","invalid","catch_all","unknown"など -
email_verified_at: API 的に「OK」と判断できた時创 -
email_verification_meta: 外部 API のレスポンスをそのまま保存
Devise 利用時は :confirmable を外し、email_status == "valid" かつ email_verified_at が入っているかどうかで有効ユーザーかを判定しるようにしています。
EmailVerificationService の実装例
Service Object で外部 API をラップします。
class EmailVerificationService
Result = Struct.new(
:status,
:mx_found,
:is_catch_all,
:raw,
keyword_init: true
)
def self.verify(email)
new(email).verify
end
def initialize(email)
@address = email.to_s.strip
end
def verify
return invalid_result unless valid_syntax?
api_result = call_verification_api(@address)
Result.new(
status: map_status(api_result["status"]),
mx_found: api_result["mx_found"],
is_catch_all: api_result["status"] == "catch-all",
raw: api_result
)
end
private
def valid_syntax?
@address.match?(URI::MailTo::EMAIL_REGEXP)
end
def invalid_result
Result.new(status: "invalid", mx_found: false, is_catch_all: false, raw: {})
end
def call_verification_api(email)
# Faraday / HTTParty などで ZeroBounce や Hunter の API を叩く
end
def map_status(third_party_status)
case third_party_status
when "valid" then "valid"
when "invalid" then "invalid"
when "catch-all" then "catch_all"
else "unknown"
end
end
end
外部サービス側の API は、ZeroBounce のように valid / invalid / catch-all / unknown と MX や did_you_mean などを返してくれます。
サインアップフローへの組み込み
RegistrationsController(もしくは Devise のカスタムコントローラ)で、ざっくり以下のような流れです。
- フォームから
emailを受け取る -
EmailVerificationService.verify(email)を実行 -
status == "invalid"の場合は User を作らずにバリデーションエラー -
status == "valid"の場合のみ User を作成し、email_verified_atをセット -
catch_all/unknownはプロダクトのポリシーに合わせて許可 / 警告 / 再入力を選ぶ
フロント側で blur 時に Ajax で /email_verifications を叩き、リアルタイムで「OK / NG」を表示するようにすると UX 的にも分かりやすかったです。
まとめ
- 確認メールに頻らずに、サインアップ時点で deliverable なメールアドレスかどうかをチェックできる
- 間違ったアドレスや捨てアドレスを入口で引けるので、後続のメール配信エラーが減る
- メール送信数自体も減るので、評価
「確認メールを送ること=メールアドレス検証」という前提を一度外してみると、Rails でも意外とんとを罫いに置き換えられるので、同じような課題を感じている方の参考になれば幸いです。