ticketdive.com
メールアドレス入力フォームの
バリデーション仕様
Next.js production bundle を取得・整形し、zod スキーマと React Hook Form の振る舞いを特定。
対象: /account/email と /signup。実機検証を含む。
調査の目的と結論
ticketdive.com (Next.js / SPA) の「メールアドレス入力フォーム」が submit 時にどんなバリデーションを行っているかを、 production の JS chunk を静的解析し、ブラウザ上で実機検証して確定させる。
メール検証は /signup と /account/email で別実装。共通して zod スキーマで 必須・形式・+拒否 を行い、
/signup はさらに 176 件のドメイン typo ブラックリスト と blur 時の入力値エコー UX を持つ。
ドメイン照合は完全一致 (Levenshtein 等のあいまい一致は無し)。
検証対象と手法
対象
| ページ | ファイル | 役割 |
|---|---|---|
/account/email |
pages/account/email-a5b27c8de29b72b3.js |
ログイン後のメールアドレス変更 |
/signup |
pages/signup-df663788b5bbdc5e.js |
新規会員登録 |
手法
- Bundle 取得該当ページを読み込んだ際に
<script src>から各 page chunk を列挙し、curl で取得。 - 整形
prettier --parser babelで minified を可読化。 - 静的解析zod schema・
register()・onBlur・refine の引数を読み取り。 - 実機検証本番フォームに既知の typo / 大文字 /
+入りメールを入力し、表示されるエラー文言と DOM 状態を確認。
/account/email の仕様
zod スキーマ (再整形)
email: z.string()
.min(1, t("form.required")) // 必須
.email( t("form.emailValid")) // RFC 風の形式チェック
.refine(e => !e.includes("+"), // ★ "+" を 1 文字でも含むと NG
{ message: t("form.emailValid") }),
password: z.string().min(1, t("form.required")),
verificationCode: z.string()
.length(6, t("form.verificationCodeLength"))
.regex(/^\d{6}$/, t("form.verificationCodeFormat")),
HTML 属性と検証タイミング
- type
"text"(type="email"ではない)。inputMode="email"とautoComplete="email"はソフトキーボード等のヒントのみ。 - HTML5
pattern[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}$が付与されるが、送信はonClick: handleSubmit()経由で native form submit ではない。よって pattern 制約検証は走らない (実機でVALID.User@Example.COMも通過することを確認)。 - 検証タイミングreact-hook-form のデフォルト
mode: onSubmit。入力中・blur 時は何も走らない。 - 典型エラー文言必須 → 「必須項目です」 / 形式・
+→ 「有効なメールアドレスを入力してください」。
実機検証 (個人情報マスク済み)
| 入力値 | 結果 | 備考 |
|---|---|---|
test+demo@example.com |
Rejected | refine による "+" 拒否が発火 |
VALID.User@Example.COM |
Passed | 大文字混じり可。HTML pattern は実質効いていない |
test@gmial.com |
Passed | このフォームに typo ブラックリストは無い |
/signup の仕様 (本題)
signup ページにはメール検証が 2 系統ある。①は submit 時の zod refine、②は blur 時の純粋な UX (typo 検出ではない再確認プロンプト)。
① submit 時 — typo ブラックリスト
固定リスト z をモジュール初期化時に Object.values({...}).flat() でフラット化し、refine で ローカル部以降を完全一致 比較する。
const z = Object.values({
icloud: [/* @icluod.com, @icloud.cmo, ... 30件 */],
gmail: [/* @gamil.com, @gmial.com, ... 31件 */],
docomo: [/* @docmo.ne.jp, @docom.ne.jp, ... 29件 */],
ezweb: [/* @ezwbe.ne.jp, @ezweb.ne.jo, ... 29件 */],
softbank: [/* @i.softabnk.jp, @i.softbakn.jp, ... 27件 */],
yahoo: [/* @yhoo.co.jp, @yaho.co.jp, ... 30件 */],
}).flat();
email: z.string()
.min(1, "form.required")
.email("form.validEmail")
.refine(e => {
const idx = e.lastIndexOf("@");
const domain = idx === -1 ? "" : e.slice(idx).toLowerCase();
return !z.includes(domain) && !e.includes("+");
}, { message: "form.validEmail" });
- 照合方式完全一致のみ。Levenshtein 距離やキーボード近傍などの fuzzy match は無し。リストに無い typo (例:
@gaamail.com) は通過する。 - 正規化
toLowerCase()のみ。前後空白の trim はしない。 - エラー文言typo ヒット時も「有効なメールアドレスを入力してください」(
form.validEmail)。ユーザーには typo として識別できる文言ではない。
② blur 時 — 入力値エコー UX
ここがユーザー体感での 「メールアドレスに誤りがないか確認してください。」 の出所。バリデーションではなく、入力した値をそのまま赤色で再描画する確認プロンプトである。
const [R, setR] = useState("");
<Input
{...register("email", {
onBlur: e => setR(e.currentTarget.value),
})}
type="email"
...
/>
{R.trim().length > 0 && (
<Stack>
<Text fontWeight={600}>
{t("form.emailConfirmationNote")} {/* ← 「メールアドレスに誤りがないか確認してください。」 */}
</Text>
<Text color="red1" fontWeight={600}>
{R} {/* ← 入力値を赤で再表示 */}
</Text>
</Stack>
)}
- 発火条件email 入力欄を blur した時。中身が空白以外なら必ず表示。
- typo 検出ではない正しい
foo@gmail.comでも、ブラックリスト外の typofoo@gaamail.comでも、同じプロンプトが出る。 - 表示位置入力欄の下、
error表示とは別の領域。
sample@gaamail.com を入力 → blur 直後。
「メールアドレスに誤りがないか確認してください。」+ 入力値の赤字再表示が出る。
これは typo 検出ではなく無条件で発火する確認 UX。
このエコー UX は「typo を検出して指摘している」ように見える。実際は単に再描画しているだけなので、
@gaamail.com 等のブラックリスト外 typo は submit すれば普通に通る。
ドメイン typo ブラックリスト全件
signup chunk に含まれる固定リスト。先頭の @ は省略表記。合計 176 件。
icloud.com 30 entries
- @icluod.com
- @icloud.cmo
- @iclud.com
- @iclod.com
- @icloud.co
- @icloud.ocm
- @iclooud.com
- @icloud.con
- @iclou.com
- @ilcoud.com
- @icloud.xom
- @iclodu.com
- @icloud.vom
- @icloud.clm
- @ickoud.com
- @icloud.comn
- @icloud.cok
- @icloud.dom
- @iciood.com
- @icloud.come
- @iccloud.com
- @iclpud.com
- @icloud.cop
- @icloud.cam
- @icloud.coj
- @icloud.oc
- @icloid.com
- @iclous.com
- @icloud.cim
- @iclous.cow
gmail.com 31 entries
- @gamil.com
- @gmial.com
- @gmaill.com
- @gmail.cmo
- @gmail.co
- @gail.com
- @gmai.com
- @gmali.com
- @gml.com
- @gmaol.com
- @gmail.con
- @gmaio.com
- @gimail.com
- @gamil.con
- @gmail.xom
- @gmqil.com
- @gmaik.com
- @gmail.vom
- @gnail.com
- @gmail.clm
- @gmsil.com
- @gmaul.com
- @gmail.coj
- @gma.com
- @gmail.ocm
- @gmal.com
- @gmail.cim
- @gmall.com
- @gmail.cok
- @hmail.com
- @gmail.cow
docomo.ne.jp 29 entries
- @docmo.ne.jp
- @dcom.ne.jp
- @docmoo.ne.jp
- @docom.ne.jp
- @odcomo.ne.jp
- @docoom.ne.jp
- @doco.ne.jp
- @doocmo.ne.jp
- @dokomo.ne.jp
- @dkcomo.ne.jp
- @focomo.ne.jp
- @docpomo.ne.jp
- @docomone.jp
- @docomo.me.jp
- @docom0.ne.jp
- @docomo.n.jp
- @docomo.ne.jo
- @docomo.ne.j
- @docomo.ne.kp
- @docomo.ne.ip
- @docomo.ne.jpp
- @docomo.be.jp
- @docomo.fe.jp
- @docomo.ge.jp
- @docomo.he.jp
- @docomo.nr.jp
- @docomo.ne.jl
- @docomo.ne.ji
- @docomo.ne.jm
ezweb.ne.jp 29 entries
- @ezwbe.ne.jp
- @ezweb.ne.jo
- @ezweb.ne.j
- @ezweb.ne.jpp
- @ezweb.nw.jp
- @ezweb.ne.kp
- @ezweb.me.jp
- @ezweb.ne.ip
- @ezweb.ne.ji
- @ezweb.ne.jm
- @ezweb.de.jp
- @ezweb.ne.hp
- @ezweb.nr.jp
- @ezweb.ne.lp
- @ezweb.ne.jl
- @ezweb.ne.jlp
- @ezweb.he.jp
- @ezweb.be.jp
- @ezwweb.ne.jp
- @ezwbeb.ne.jp
- @ezeweb.ne.jp
- @ezeb.ne.jp
- @ezwebb.ne.jp
- @ezwrb.ne.jp
- @ezweb.n.ep
- @ezweb.nf.jp
- @ezweb.ne.hjp
- @ezweb.ne.jph
- @ezweb.ne.pj
i.softbank.jp 27 entries
- @i.softabnk.jp
- @i.softbakn.jp
- @i.sofbank.jp
- @i.softbak.jp
- @i.softbanl.jp
- @i.softnank.jp
- @i.softbamk.jp
- @i.softbanj.jp
- @i.softbanm.jp
- @i.softbank.jo
- @i.softbank.jl
- @i.softbankk.jp
- @i.sofrbank.jp
- @i.softtbank.jp
- @i.softbank.jpj
- @i.softbank.kp
- @i.softbnak.jp
- @i.softbajk.jp
- @i.softbank.j
- @i.softbank.ip
- @i.softbank.jpp
- @i.softbnk.jp
- @i.softdank.jp
- @i.softgank.jp
- @i.softban.jp
- @i.softbannk.jp
- @i.softbank.jm
yahoo.co.jp 30 entries
- @yhoo.co.jp
- @yaho.co.jp
- @yaoho.co.jp
- @yahho.co.jp
- @yahoo.oc.jp
- @yahoo.cp.jp
- @yahoo.cl.jp
- @yahoo.co.kp
- @yahoo.co.ip
- @yahoo.co.jo
- @yahoo.co.j
- @yahoo.coo.jp
- @yahoo.co.jpp
- @yhaoo.co.jp
- @yahoo.co.jl
- @yahoo.co.ji
- @yahoo.co.jm
- @yajoo.co.jp
- @yahio.co.jp
- @yahoi.co.jp
- @yahop.co.jp
- @yahoo.co.pj
- @yaoo.co.jp
- @ahoo.co.jp
- @yahoo.c.jp
- @yahoo.co.lj
- @yahoo.co.jpn
- @yauoo.co.jp
- @yahhoo.co.jp
- @yahoo.com.jp
/account/email と /signup の差分
| 項目 | /signup | /account/email |
|---|---|---|
必須 (zod .min(1)) | Yes | Yes |
形式 (zod .email()) | Yes | Yes |
"+" 拒否 | Yes | Yes |
| ドメイン typo ブラックリスト | Yes (176 件) | No |
| blur 時の入力値エコー | Yes | No |
HTML5 pattern 属性 | Yes (実効性なし) | Yes (実効性なし) |
input type | "email" | "text" |
| 追加要件 | — | 現在のパスワード + 6 桁認証コード |
signup だけが ① 既存ユーザーの大半を占めるであろう 6 大ドメインを守るために typo リストを持ち、② 入力値エコーで目視確認も促す。 account/email 変更時はすでに本人確認 (パスワード + 認証コード) があるため、typo の救済より 「本人の意図した変更を尊重する」 方針と読み取れる。
結論と所感
- signup の typo 対策は固定リスト依存キーボード隣接エラー (
@gmaul.com等) や.comのミス (.cmo,.con等) はカバーされているが、リストに無い typo (例:@gaamail.com) は素通りする。 - UX とロジックの分離が浅いblur 時の「メールアドレスに誤りがないか確認してください。」は 常に 出るプロンプトであり、ユーザーは typo 検出と誤解しやすい。
- エラー文言の使い回し形式エラー /
+拒否 / typo ヒットがすべてform.validEmail= 「有効なメールアドレスを入力してください」。原因切り分けが難しく、サポート問い合わせの粒度を上げにくい。 +拒否は両ページ共通foo+tag@gmail.com系のエイリアスはサインアップ・変更ともに不可。RFC 上は valid。意図的な制約と推測 (重複登録抑止 / 配信都合)。- HTML
patternは装飾的送信は JS でハンドルしているため pattern 制約検証は事実上効かない。大文字や+を含む入力でも HTML5 レベルではエラーにならない。
fuzzy match (Levenshtein 距離 1〜2) を追加すれば現在のリスト依存の盲点 (@gaamail.com 等) を一気にカバーできる。同時に「形式」「typo」「+」のエラー文言を分けると、ユーザーが入力ミスを修正しやすい。