RFC 5321/5322に沿ったEmail正規表現を書く
はじめに
後述しますが、RFCに厳密に沿ったバリデーションはお勧めしません。
以下の正規表現が最も一般的なemail正規表現で、HTML仕様書にも記載されています。
/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
RFC 5321とRFC 5322の正規表現ネタバレ
RFC 5321 Regex
/^(?=.{1,256}$)(?=(?:[^@]{1,64})@)(?!.*\.\.)(?<local>(?:[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[^"\\]|\\[\s\S]){0,62}"))@(?<domain>[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+|\[(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3}|IPv6:[0-9a-fA-F:]+)\])$/
RFC 5322 Regex
/^(?=.{1,998}$)(?:\s|\((?:[^()\\]|\\[\s\S])*(?:\((?:[^()\\]|\\[\s\S])*\)(?:[^()\\]|\\[\s\S])*)*\))*(?<local>"(?:[^"\\]|\\[\s\S]){0,62}"(?:\."(?:[^"\\]|\\[\s\S]){0,62}")*|"(?:[^"\\]|\\[\s\S]){0,62}"(?:\.[a-zA-Z0-9!#$%&'*/=?^_`{|}~+-]{1,64}(?:\.[a-zA-Z0-9!#$%&'*/=?^_`{|}~+-]{1,64})*)+|[a-zA-Z0-9!#$%&'*/=?^_`{|}~+-]{1,64}(?:\.[a-zA-Z0-9!#$%&'*/=?^_`{|}~+-]{1,64})*(?:\."(?:[^"\\]|\\[\s\S]){0,62}")+|[a-zA-Z0-9!#$%&'*/=?^_`{|}~+-]{1,64}(?:(?:\s|\((?:[^()\\]|\\[\s\S])*(?:\((?:[^()\\]|\\[\s\S])*\)(?:[^()\\]|\\[\s\S])*)*\))*\.(?:\s|\((?:[^()\\]|\\[\s\S])*(?:\((?:[^()\\]|\\[\s\S])*\)(?:[^()\\]|\\[\s\S])*)*\))*[a-zA-Z0-9!#$%&'*/=?^_`{|}~+-]{1,64})*)(?:\s|\((?:[^()\\]|\\[\s\S])*(?:\((?:[^()\\]|\\[\s\S])*\)(?:[^()\\]|\\[\s\S])*)*\))*@(?:\s|\((?:[^()\\]|\\[\s\S])*(?:\((?:[^()\\]|\\[\s\S])*\)(?:[^()\\]|\\[\s\S])*)*\))*(?<domain>[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:(?:\s|\((?:[^()\\]|\\[\s\S])*(?:\((?:[^()\\]|\\[\s\S])*\)(?:[^()\\]|\\[\s\S])*)*\))*\.(?:\s|\((?:[^()\\]|\\[\s\S])*(?:\((?:[^()\\]|\\[\s\S])*\)(?:[^()\\]|\\[\s\S])*)*\))*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+|\[(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3}|IPv6:[0-9a-fA-F:]+)\])(?:\s|\((?:[^()\\]|\\[\s\S])*(?:\((?:[^()\\]|\\[\s\S])*\)(?:[^()\\]|\\[\s\S])*)*\))*$/
2. 一般的なemail正規表現(HTML仕様)
HTML仕様書の「4.10.5.1.5 Email state (type=email)」に、メールアドレスの正規表現が定義されています。
/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
ブラウザなどもこの仕様に基づいてバリデーションを行っているため、おそらく世界で最も広く使われているemailの正規表現と言えるでしょう。
HTML仕様がRFCに違反している理由
しかし、HTML仕様書には以下のような記述があります:
この要件は、RFC 5322 への意図的な違反です。RFC 5322 はEメールアドレスの構文を定義していますが、その内容は("@"文字より前は)厳格すぎると同時に、("@"文字より後は)曖昧すぎ、さらに(ほとんどのユーザーには馴染みのない方法でのコメント、空白文字、引用符付き文字列を許可するという点で)緩すぎるため、ここで実用的に用いるには適していません。
つまり、HTML仕様は意図的にRFC 5322に違反しています。
馴染みのない構文や厳格すぎる要件を設けているため、実用的には使いにくいということです。
しかし、今回はタイトル通りRFCに厳密に沿ったバリデーションを作りたいので、何がいけないのかを詳しくみていきましょう。
3. メールアドレスRFCの基礎知識
メールアドレスRFCの歴史
メールアドレスのフォーマットは、以下の流れで標準化されてきました:
- RFC 822(1982年): 初期のメッセージフォーマット標準
- RFC 2822(2001年): RFC 822の近代化版
- RFC 5322(2008年): 現行のメッセージフォーマット標準
本記事では、現行のRFC 5322とRFC 5321(SMTPプロトコル)の2つを扱います。
RFC 5322とRFC 5321:定義している範囲の違い
メールアドレスに関する2つの主要なRFCは、それぞれ異なる側面を定義しています:
RFC 5322(メッセージフォーマット)
- メールの「内容」、特にヘッダーのフォーマットを定義
-
To:,From:などのヘッダーに記述されるメールアドレスの「構文」を定義 - より柔軟で表現力のある構文を許可(quoted-string、コメントなど)
RFC 5321(SMTPプロトコル)
- メールの「転送」プロトコル(SMTP)を定義
-
MAIL FROM:,RCPT TO:コマンドで使用されるアドレスの構文を定義 - DNS解決可能なドメイン名のみを許可
重要な違い
フォーマットとして正しいこと(RFC 5322)と、実際に転送できること(RFC 5321)は異なります。
例えば、user@example!com はRFC 5322の構文上は有効ですが、RFC 5321ではDNS解決が必要なため転送できません。そのため、RFC 5321の方がより厳格な制約を持っています。
4. RFC 5321準拠の正規表現を作る
RFC 5321は、SMTPで実際に転送できるメールアドレスの形式を定義しています。HTML仕様の正規表現をベースに、段階的にRFC 5321に準拠させていきます。
基本構造の理解
RFC 5321のメールアドレスは、以下の基本構造を持ちます:
local-part "@" domain
-
local-part: 最大64文字 -
domain: 最大255文字 - パス全体: 最大256文字
local-partの実装
RFC 5321のセクション4.1.2では、local-part は dot-atom / quoted-string と定義されています。
dot-atom形式
dot-atom は、RFC 5322の atext を使用できます。atext には以下の文字が含まれます:
- 英数字:
A-Z,a-z,0-9 - 記号: ! # $ % & ' * + - / = ? ^ _` { | } ~
ドット(.)で区切ることができますが、連続するドットや先頭・末尾のドットは許可されません。
実は + を使ったエイリアス機能(例: user+alias@example.com)はRFCで定義されていないGmailなどが独自に拡張した機能です
dot-atom の正規表現:
[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*
quoted-string形式
RFC 5321では、RFC 5322で定義されている quoted-string(引用符付き文字列)もサポートしています。これにより、通常は許可されない文字を含めることができます:
"user"@example.com
"user.name"@example.com
"user@domain"@example.com
quoted-string の正規表現:
"(?:[^"\\]|\\[\s\S]){0,62}"
RFC 5321の local-part は、これらを組み合わせた形式になります:
(?:[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[^"\\]|\\[\s\S]){0,62}")
domainの実装(DNS制約)
RFC 5321のdomainは、DNSで解決可能な名前でなければなりません。RFC 1035に従い、以下の制約があります:
- 使用可能な文字: 英数字(
A-Z,a-z,0-9)とハイフン(-)のみ - ハイフンは先頭と末尾には使用できない
- 各ラベルは最大63文字
- ドメイン全体は最大255文字
- 最低1つのドット(
.)が必要(TLDが必要)
正規表現では以下のように表現できます:
[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+
domain-literalの実装(IPアドレス)
RFC 5321では、DNSの代わりにIPアドレスを直接指定する domain-literal も許可されています:
user@[192.168.0.1]
user@[IPv6:2001:db8::1]
これを正規表現で表現すると:
\[(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3}|IPv6:[0-9a-fA-F:]+)\]
連続ドットの禁止
連続するドット(..)は許可されないため、これも先読みアサーションで除外します:
(?!.*\.\.)
長さ制限
RFC 5321では、パス全体の長さが最大256文字、local-partが最大64文字と定義されています。これは <user@example.com> のような形式を含むため、実質的にはメールアドレス自体は254文字程度が上限になります。
長さ制限は、正規表現の先頭で先読みアサーション(lookahead)を使用して実装します:
(?=.{1,256}$)(?=(?:[^@]{1,64})@)
RFC 5321の最終的な正規表現
これらを組み合わせると、RFC 5321準拠の正規表現が完成します:
/^(?=.{1,256}$)(?=(?:[^@]{1,64})@)(?!.*\.\.)(?<local>(?:[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[^"\\]|\\[\s\S]){0,62}"))@(?<domain>[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+|\[(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3}|IPv6:[0-9a-fA-F:]+)\])$/
5. RFC 5322準拠の正規表現を作る
RFC 5322は、メールヘッダーに記述されるメールアドレスのフォーマットを定義しており、RFC 5321よりも柔軟な構文を許可しています。
quoted-stringの実装
RFC 5322の最大の特徴は、local-part に quoted-string(引用符付き文字列)を使用できることです。これにより、通常は許可されない文字(スペース、ドット、アットマークなど)を含めることができます。
quoted-stringの例
"user name"@example.com
"user..dots"@example.com
"user@domain"@example.com
quoted-stringの中では、バックスラッシュ(\)でエスケープができます:
"user\"quote"@example.com
"user\\backslash"@example.com
quoted-stringの正規表現:
"(?:[^"\\]|\\[\s\S]){0,62}"
mixed形式(quoted-stringとatextの混在)
RFC 5322では、quoted-stringとatextを . で連結した混在形式も許可されています:
"first".last@example.com
first."last"@example.com
この柔軟性により、local-partの定義は複雑になります。以下の4つのパターンをサポートする必要があります:
-
"quoted"."quoted"...- quoted-stringのみ -
"quoted".atext.atext...- quoted-stringで開始、atextで終了 -
atext.atext..."quoted"- atextで開始、quoted-stringで終了 -
atext.atext...- atextのみ
これらを正規表現で表現すると:
(?<local>"(?:[^"\\]|\\[\s\S]){0,62}"(?:\."(?:[^"\\]|\\[\s\S]){0,62}")*|"(?:[^"\\]|\\[\s\S]){0,62}"(?:\.[a-zA-Z0-9!#$%&'*/=?^_`{|}~+-]{1,64}(?:\.[a-zA-Z0-9!#$%&'*/=?^_`{|}~+-]{1,64})*)+|[a-zA-Z0-9!#$%&'*/=?^_`{|}~+-]{1,64}(?:\.[a-zA-Z0-9!#$%&'*/=?^_`{|}~+-]{1,64})*(?:\."(?:[^"\\]|\\[\s\S]){0,62}")+|[a-zA-Z0-9!#$%&'*/=?^_`{|}~+-]{1,64}(?:(?:\s|\((?:[^()\\]|\\[\s\S])*(?:\((?:[^()\\]|\\[\s\S])*\)(?:[^()\\]|\\[\s\S])*)*\))*\.(?:\s|\((?:[^()\\]|\\[\s\S])*(?:\((?:[^()\\]|\\[\s\S])*\)(?:[^()\\]|\\[\s\S])*)*\))*[a-zA-Z0-9!#$%&'*/=?^_`{|}~+-]{1,64})*)
コメント対応
RFC 5322では、メールアドレスの様々な箇所にコメントや空白を挿入できます:
user(comment)@example.com
(comment)user@example.com
user@(comment)example.com
user@example.(comment)com
user @ example.com
コメントは丸括弧(())で囲まれ、ネストも可能です。また、コメント内ではバックスラッシュ(\)でエスケープができます。
コメントの正規表現:
(?:\s|\((?:[^()\\]|\\[\s\S])*(?:\((?:[^()\\]|\\[\s\S])*\)(?:[^()\\]|\\[\s\S])*)*\))*
これを @ の前後、local-partとdomainの周り、さらにdomainのドット(.)の前後に配置します。
domainにコメント対応を追加した例:
(?<domain>[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:(?:\s|\((?:[^()\\]|\\[\s\S])*(?:\((?:[^()\\]|\\[\s\S])*\)(?:[^()\\]|\\[\s\S])*)*\))*\.(?:\s|\((?:[^()\\]|\\[\s\S])*(?:\((?:[^()\\]|\\[\s\S])*\)(?:[^()\\]|\\[\s\S])*)*\))*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+|\[(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3}|IPv6:[0-9a-fA-F:]+)\])
長さ制限の調整
RFC 5322では、RFC 5321のようなパス全体の長さ制限はありませんが、実装上の制限として998文字が推奨されています。
(?=.{1,998}$)
RFC 5322の最終的な正規表現
これらを組み合わせると、RFC 5322準拠の正規表現が完成します
/^(?=.{1,998}$)(?:\s|\((?:[^()\\]|\\[\s\S])*(?:\((?:[^()\\]|\\[\s\S])*\)(?:[^()\\]|\\[\s\S])*)*\))*(?<local>"(?:[^"\\]|\\[\s\S]){0,62}"(?:\."(?:[^"\\]|\\[\s\S]){0,62}")*|"(?:[^"\\]|\\[\s\S]){0,62}"(?:\.[a-zA-Z0-9!#$%&'*/=?^_`{|}~+-]{1,64}(?:\.[a-zA-Z0-9!#$%&'*/=?^_`{|}~+-]{1,64})*)+|[a-zA-Z0-9!#$%&'*/=?^_`{|}~+-]{1,64}(?:\.[a-zA-Z0-9!#$%&'*/=?^_`{|}~+-]{1,64})*(?:\."(?:[^"\\]|\\[\s\S]){0,62}")+|[a-zA-Z0-9!#$%&'*/=?^_`{|}~+-]{1,64}(?:(?:\s|\((?:[^()\\]|\\[\s\S])*(?:\((?:[^()\\]|\\[\s\S])*\)(?:[^()\\]|\\[\s\S])*)*\))*\.(?:\s|\((?:[^()\\]|\\[\s\S])*(?:\((?:[^()\\]|\\[\s\S])*\)(?:[^()\\]|\\[\s\S])*)*\))*[a-zA-Z0-9!#$%&'*/=?^_`{|}~+-]{1,64})*)(?:\s|\((?:[^()\\]|\\[\s\S])*(?:\((?:[^()\\]|\\[\s\S])*\)(?:[^()\\]|\\[\s\S])*)*\))*@(?:\s|\((?:[^()\\]|\\[\s\S])*(?:\((?:[^()\\]|\\[\s\S])*\)(?:[^()\\]|\\[\s\S])*)*\))*(?<domain>[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:(?:\s|\((?:[^()\\]|\\[\s\S])*(?:\((?:[^()\\]|\\[\s\S])*\)(?:[^()\\]|\\[\s\S])*)*\))*\.(?:\s|\((?:[^()\\]|\\[\s\S])*(?:\((?:[^()\\]|\\[\s\S])*\)(?:[^()\\]|\\[\s\S])*)*\))*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+|\[(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3}|IPv6:[0-9a-fA-F:]+)\])(?:\s|\((?:[^()\\]|\\[\s\S])*(?:\((?:[^()\\]|\\[\s\S])*\)(?:[^()\\]|\\[\s\S])*)*\))*$/
テスト結果
実装した正規表現に対して、RFC仕様に基づいたテストを実行した結果、全てのテストケースが合格しました
$ bun test src/test/email.test.ts
bun test v1.3.1
42 pass
0 fail
193 expect() calls
Ran 42 tests across 1 file. [19.00ms]
test code
import { describe, expect, it } from "bun:test";
export type ValidationLevel = "rfc5321" | "rfc5322";
interface ValidationOptions {
level: ValidationLevel;
}
const EMAIL_PATTERNS: Record<ValidationLevel, RegExp> = {
rfc5321:
/^(?=.{1,256}$)(?=(?:[^@]{1,64})@)(?!.*\.\.)(?<local>(?:[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[^"\\]|\\[\s\S]){0,62}"))@(?<domain>[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+|\[(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3}|IPv6:[0-9a-fA-F:]+)\])$/,
rfc5322:
/^(?=.{1,998}$)(?:\s|\((?:[^()\\]|\\[\s\S])*(?:\((?:[^()\\]|\\[\s\S])*\)(?:[^()\\]|\\[\s\S])*)*\))*(?<local>"(?:[^"\\]|\\[\s\S]){0,62}"(?:\."(?:[^"\\]|\\[\s\S]){0,62}")*|"(?:[^"\\]|\\[\s\S]){0,62}"(?:\.[a-zA-Z0-9!#$%&'*/=?^_`{|}~+-]{1,64}(?:\.[a-zA-Z0-9!#$%&'*/=?^_`{|}~+-]{1,64})*)+|[a-zA-Z0-9!#$%&'*/=?^_`{|}~+-]{1,64}(?:\.[a-zA-Z0-9!#$%&'*/=?^_`{|}~+-]{1,64})*(?:\."(?:[^"\\]|\\[\s\S]){0,62}")+|[a-zA-Z0-9!#$%&'*/=?^_`{|}~+-]{1,64}(?:(?:\s|\((?:[^()\\]|\\[\s\S])*(?:\((?:[^()\\]|\\[\s\S])*\)(?:[^()\\]|\\[\s\S])*)*\))*\.(?:\s|\((?:[^()\\]|\\[\s\S])*(?:\((?:[^()\\]|\\[\s\S])*\)(?:[^()\\]|\\[\s\S])*)*\))*[a-zA-Z0-9!#$%&'*/=?^_`{|}~+-]{1,64})*)(?:\s|\((?:[^()\\]|\\[\s\S])*(?:\((?:[^()\\]|\\[\s\S])*\)(?:[^()\\]|\\[\s\S])*)*\))*@(?:\s|\((?:[^()\\]|\\[\s\S])*(?:\((?:[^()\\]|\\[\s\S])*\)(?:[^()\\]|\\[\s\S])*)*\))*(?<domain>[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:(?:\s|\((?:[^()\\]|\\[\s\S])*(?:\((?:[^()\\]|\\[\s\S])*\)(?:[^()\\]|\\[\s\S])*)*\))*\.(?:\s|\((?:[^()\\]|\\[\s\S])*(?:\((?:[^()\\]|\\[\s\S])*\)(?:[^()\\]|\\[\s\S])*)*\))*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+|\[(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3}|IPv6:[0-9a-fA-F:]+)\])(?:\s|\((?:[^()\\]|\\[\s\S])*(?:\((?:[^()\\]|\\[\s\S])*\)(?:[^()\\]|\\[\s\S])*)*\))*$/,
} as const;
export const validateEmail = (email: string, options: ValidationOptions) => {
const { level } = options;
const pattern = EMAIL_PATTERNS[level];
const match = pattern.exec(email);
return {
valid: match !== null,
parts: match?.groups
? {
local: match.groups.local,
domain: match.groups.domain,
}
: undefined,
};
};
describe("email validation", () => {
describe("rfc5321 level", () => {
it("validates with different RFC compliance levels", () => {
expect(
validateEmail("test@example.com", { level: "rfc5321" }).valid,
).toBeTruthy();
});
it("RFC 5321 allows RFC 5322 special characters in local-part but not in domain", () => {
expect(
validateEmail("user@example.com", { level: "rfc5321" }).valid,
).toBeTruthy();
expect(
validateEmail("user+tag@example.com", { level: "rfc5321" }).valid,
).toBeTruthy();
expect(
validateEmail("user.name@example.com", { level: "rfc5321" }).valid,
).toBeTruthy();
expect(
validateEmail("user!test@example.com", { level: "rfc5321" }).valid,
).toBeTruthy();
expect(
validateEmail("user#test@example.com", { level: "rfc5321" }).valid,
).toBeTruthy();
expect(
validateEmail("user@exam!ple.com", { level: "rfc5321" }).valid,
).toBeFalsy();
expect(
validateEmail("user@exam#ple.com", { level: "rfc5321" }).valid,
).toBeFalsy();
expect(
validateEmail("user@localhost", { level: "rfc5321" }).valid,
).toBeFalsy();
});
it("validates domain name rules more strictly (RFC 1035/5321)", () => {
expect(
validateEmail("user@-example.com", { level: "rfc5321" }).valid,
).toBeFalsy();
expect(
validateEmail("user@example-.com", { level: "rfc5321" }).valid,
).toBeFalsy();
expect(
validateEmail("user@example.com-", { level: "rfc5321" }).valid,
).toBeFalsy();
const longLabel = "a".repeat(64);
expect(
validateEmail(`user@${longLabel}.com`, { level: "rfc5321" }).valid,
).toBeFalsy();
const longDomain = `user@example.${"a".repeat(250)}.com`;
expect(validateEmail(longDomain, { level: "rfc5321" }).valid).toBeFalsy();
const exactLabel = "a".repeat(63);
expect(
validateEmail(`user@${exactLabel}.com`, { level: "rfc5321" }).valid,
).toBeTruthy();
expect(
validateEmail("user@123.example.com", { level: "rfc5321" }).valid,
).toBeTruthy();
});
it("handles domain literals (IP addresses) correctly based on RFC 5321/5322 level", () => {
expect(
validateEmail("user@[192.168.0.1]", { level: "rfc5321" }).valid,
).toBeTruthy();
expect(
validateEmail("user@[IPv6:2001:db8::1]", { level: "rfc5321" }).valid,
).toBeTruthy();
});
it("RFC 5321: path length (max 256 octets)", () => {
const d63 = "a".repeat(63);
const domain249 = `${d63}.${d63}.${d63}.${"a".repeat(57)}`;
expect(domain249.length).toBe(249);
const local5 = "a".repeat(5);
expect(
validateEmail(`${local5}@${domain249}`, { level: "rfc5321" }).valid,
).toBeTruthy();
const local6 = "a".repeat(6);
expect(
validateEmail(`${local6}@${domain249}`, { level: "rfc5321" }).valid,
).toBeTruthy();
const local7 = "a".repeat(7);
expect(
validateEmail(`${local7}@${domain249}`, { level: "rfc5321" }).valid,
).toBeFalsy();
});
it("RFC 5321: local-part length (max 64 octets)", () => {
const local64 = "a".repeat(64);
expect(
validateEmail(`${local64}@example.com`, { level: "rfc5321" }).valid,
).toBeTruthy();
const local65 = "a".repeat(65);
expect(
validateEmail(`${local65}@example.com`, { level: "rfc5321" }).valid,
).toBeFalsy();
const local32dot31 = `${"a".repeat(32)}.${"a".repeat(31)}`;
expect(local32dot31.length).toBe(64);
expect(
validateEmail(`${local32dot31}@example.com`, { level: "rfc5321" }).valid,
).toBeTruthy();
const local33dot31 = `${"a".repeat(33)}.${"a".repeat(31)}`;
expect(local33dot31.length).toBe(65);
expect(
validateEmail(`${local33dot31}@example.com`, { level: "rfc5321" }).valid,
).toBeFalsy();
});
it("all levels reject consecutive dots", () => {
expect(
validateEmail("user..name@example.com", { level: "rfc5321" }).valid,
).toBeFalsy();
});
it("levels with length checks enforce 998 char total limit", () => {
const veryLongEmail = `${"a".repeat(990)}@example.com`;
expect(
validateEmail(veryLongEmail, { level: "rfc5321" }).valid,
).toBeFalsy();
});
it("RFC 5321/5322: handles domain literals correctly", () => {
expect(
validateEmail("user@[192.168.1.1]", { level: "rfc5321" }).valid,
).toBeTruthy();
expect(
validateEmail("user@[IPv6:2001:db8::1]", { level: "rfc5321" }).valid,
).toBeTruthy();
});
it("RFC 1035: should reject domain labels starting or ending with a hyphen", () => {
expect(
validateEmail("user@-example.com", { level: "rfc5321" }).valid,
).toBeFalsy();
expect(
validateEmail("user@example-.com", { level: "rfc5321" }).valid,
).toBeFalsy();
});
it("rejects strings with invalid control characters on stricter levels", () => {
expect(
validateEmail("user\n@example.com", { level: "rfc5321" }).valid,
).toBeFalsy();
expect(
validateEmail("user@example.com\n", { level: "rfc5321" }).valid,
).toBeFalsy();
expect(
validateEmail("\nuser@example.com", { level: "rfc5321" }).valid,
).toBeFalsy();
expect(
validateEmail("user@exam\nple.com", { level: "rfc5321" }).valid,
).toBeFalsy();
expect(
validateEmail("user\r@example.com", { level: "rfc5321" }).valid,
).toBeFalsy();
expect(
validateEmail("user@example.com\r", { level: "rfc5321" }).valid,
).toBeFalsy();
expect(
validateEmail("\ruser@example.com", { level: "rfc5321" }).valid,
).toBeFalsy();
expect(
validateEmail("user@exam\rple.com", { level: "rfc5321" }).valid,
).toBeFalsy();
});
it("rejects malformed domain structures across all levels", () => {
expect(validateEmail("user@", { level: "rfc5321" }).valid).toBeFalsy();
expect(validateEmail("user@.", { level: "rfc5321" }).valid).toBeFalsy();
expect(validateEmail("user@..", { level: "rfc5321" }).valid).toBeFalsy();
expect(validateEmail("user@...", { level: "rfc5321" }).valid).toBeFalsy();
expect(
validateEmail("user@.domain.example.com", { level: "rfc5321" }).valid,
).toBeFalsy();
expect(
validateEmail("user@domain..example.com", { level: "rfc5321" }).valid,
).toBeFalsy();
});
it("rejects completely invalid strings across all levels", () => {
expect(
validateEmail("not-an-email-at-all", { level: "rfc5321" }).valid,
).toBeFalsy();
expect(validateEmail("12345", { level: "rfc5321" }).valid).toBeFalsy();
expect(
validateEmail("!@#$%^&*()", { level: "rfc5321" }).valid,
).toBeFalsy();
expect(
validateEmail("random string with spaces", { level: "rfc5321" }).valid,
).toBeFalsy();
expect(
validateEmail("just-text", { level: "rfc5321" }).valid,
).toBeFalsy();
expect(
validateEmail("<script>alert('xss')</script>", { level: "rfc5321" })
.valid,
).toBeFalsy();
expect(
validateEmail("../../../etc/passwd", { level: "rfc5321" }).valid,
).toBeFalsy();
expect(
validateEmail("SELECT * FROM users", { level: "rfc5321" }).valid,
).toBeFalsy();
expect(validateEmail("null", { level: "rfc5321" }).valid).toBeFalsy();
expect(
validateEmail("undefined", { level: "rfc5321" }).valid,
).toBeFalsy();
expect(validateEmail("true", { level: "rfc5321" }).valid).toBeFalsy();
expect(validateEmail("false", { level: "rfc5321" }).valid).toBeFalsy();
expect(validateEmail("{}", { level: "rfc5321" }).valid).toBeFalsy();
expect(validateEmail("[]", { level: "rfc5321" }).valid).toBeFalsy();
expect(validateEmail("0", { level: "rfc5321" }).valid).toBeFalsy();
expect(validateEmail("NaN", { level: "rfc5321" }).valid).toBeFalsy();
});
it("rejects email-like but invalid strings across all levels", () => {
expect(
validateEmail("no-at-sign.example.com", { level: "rfc5321" }).valid,
).toBeFalsy();
expect(
validateEmail("multiple@@at@signs.example.com", { level: "rfc5321" })
.valid,
).toBeFalsy();
expect(
validateEmail("@no-local-part.example.com", { level: "rfc5321" }).valid,
).toBeFalsy();
expect(
validateEmail("no-domain@", { level: "rfc5321" }).valid,
).toBeFalsy();
expect(
validateEmail("..consecutive@example.com", { level: "rfc5321" }).valid,
).toBeFalsy();
expect(
validateEmail("trailing..dots@example.com", { level: "rfc5321" }).valid,
).toBeFalsy();
expect(
validateEmail("local@.leading-domain.example.com", {
level: "rfc5321",
}).valid,
).toBeFalsy();
expect(
validateEmail("local@trailing-dot.example.com.", { level: "rfc5321" })
.valid,
).toBeFalsy();
expect(
validateEmail("local@domain..consecutive.example.com", {
level: "rfc5321",
}).valid,
).toBeFalsy();
expect(
validateEmail("user@@example.com", { level: "rfc5321" }).valid,
).toBeFalsy();
expect(
validateEmail("@@@example.com", { level: "rfc5321" }).valid,
).toBeFalsy();
expect(validateEmail("user@@@", { level: "rfc5321" }).valid).toBeFalsy();
expect(
validateEmail("@domain.example.com", { level: "rfc5321" }).valid,
).toBeFalsy();
});
it("handles Punycode (IDN) addresses", () => {
const punycodeEmail = "user@xn--bcher-kva.com";
expect(
validateEmail(punycodeEmail, { level: "rfc5321" }).valid,
).toBeTruthy();
});
it("RFC 5321: supports quoted-string in local-part", () => {
expect(
validateEmail('"user"@example.com', { level: "rfc5321" }).valid,
).toBeTruthy();
expect(
validateEmail('"user.name"@example.com', { level: "rfc5321" }).valid,
).toBeTruthy();
expect(
validateEmail('"user@domain"@example.com', { level: "rfc5321" }).valid,
).toBeTruthy();
expect(
validateEmail('"user with space"@example.com', { level: "rfc5321" }).valid,
).toBeTruthy();
});
it("RFC 5321: rejects leading/trailing dots in dot-atom local-part", () => {
expect(
validateEmail(".user@example.com", { level: "rfc5321" }).valid,
).toBeFalsy();
expect(
validateEmail("user.@example.com", { level: "rfc5321" }).valid,
).toBeFalsy();
});
it("RFC 5321: rejects comments", () => {
expect(
validateEmail("(comment)user@example.com", { level: "rfc5321" }).valid,
).toBeFalsy();
expect(
validateEmail("user(comment)@example.com", { level: "rfc5321" }).valid,
).toBeFalsy();
expect(
validateEmail("user@(comment)example.com", { level: "rfc5321" }).valid,
).toBeFalsy();
expect(
validateEmail("user@example.com(comment)", { level: "rfc5321" }).valid,
).toBeFalsy();
expect(
validateEmail("user@(comment)[127.0.0.1]", { level: "rfc5321" }).valid,
).toBeFalsy();
});
});
describe("rfc5322 level", () => {
it("validates with different RFC compliance levels", () => {
expect(
validateEmail("test@example.com", { level: "rfc5322" }).valid,
).toBeTruthy();
});
it("RFC 5322 accepts all special characters from RFC", () => {
const rfc5322ValidChars = "!#$%&'*+/=?^_`{|}~-";
expect(
validateEmail(`user${rfc5322ValidChars[0]}test@example.com`, {
level: "rfc5322",
}).valid,
).toBeTruthy();
expect(
validateEmail(`user${rfc5322ValidChars[1]}test@example.com`, {
level: "rfc5322",
}).valid,
).toBeTruthy();
expect(
validateEmail(`user${rfc5322ValidChars[2]}test@example.com`, {
level: "rfc5322",
}).valid,
).toBeTruthy();
expect(
validateEmail(`user${rfc5322ValidChars[3]}test@example.com`, {
level: "rfc5322",
}).valid,
).toBeTruthy();
expect(
validateEmail(`user${rfc5322ValidChars[4]}test@example.com`, {
level: "rfc5322",
}).valid,
).toBeTruthy();
expect(
validateEmail(`user${rfc5322ValidChars[5]}test@example.com`, {
level: "rfc5322",
}).valid,
).toBeTruthy();
expect(
validateEmail(`user${rfc5322ValidChars[6]}test@example.com`, {
level: "rfc5322",
}).valid,
).toBeTruthy();
expect(
validateEmail(`user${rfc5322ValidChars[7]}test@example.com`, {
level: "rfc5322",
}).valid,
).toBeTruthy();
expect(
validateEmail(`user${rfc5322ValidChars[8]}test@example.com`, {
level: "rfc5322",
}).valid,
).toBeTruthy();
expect(
validateEmail(`user${rfc5322ValidChars[9]}test@example.com`, {
level: "rfc5322",
}).valid,
).toBeTruthy();
expect(
validateEmail(`user${rfc5322ValidChars[10]}test@example.com`, {
level: "rfc5322",
}).valid,
).toBeTruthy();
expect(
validateEmail(`user${rfc5322ValidChars[11]}test@example.com`, {
level: "rfc5322",
}).valid,
).toBeTruthy();
expect(
validateEmail(`user${rfc5322ValidChars[12]}test@example.com`, {
level: "rfc5322",
}).valid,
).toBeTruthy();
expect(
validateEmail(`user${rfc5322ValidChars[13]}test@example.com`, {
level: "rfc5322",
}).valid,
).toBeTruthy();
expect(
validateEmail(`user${rfc5322ValidChars[14]}test@example.com`, {
level: "rfc5322",
}).valid,
).toBeTruthy();
expect(
validateEmail(`user${rfc5322ValidChars[15]}test@example.com`, {
level: "rfc5322",
}).valid,
).toBeTruthy();
expect(
validateEmail(`user${rfc5322ValidChars[16]}test@example.com`, {
level: "rfc5322",
}).valid,
).toBeTruthy();
});
it("all levels reject consecutive dots", () => {
expect(
validateEmail("user..name@example.com", { level: "rfc5322" }).valid,
).toBeFalsy();
});
it("all levels reject leading/trailing dots in local part", () => {
expect(
validateEmail(".user@example.com", { level: "rfc5322" }).valid,
).toBeFalsy();
expect(
validateEmail("user.@example.com", { level: "rfc5322" }).valid,
).toBeFalsy();
});
it("all levels respect local part length limit of 64 chars", () => {
const longLocal = `${"a".repeat(65)}@example.com`;
expect(validateEmail(longLocal, { level: "rfc5322" }).valid).toBeFalsy();
});
it("levels with length checks enforce 998 char total limit", () => {
const veryLongEmail = `${"a".repeat(990)}@example.com`;
expect(
validateEmail(veryLongEmail, { level: "rfc5322" }).valid,
).toBeFalsy();
});
it("handles quoted strings correctly based on RFC 5322 level", () => {
expect(
validateEmail('"user with space"@example.com', { level: "rfc5322" })
.valid,
).toBeTruthy();
expect(
validateEmail('"user..dots"@example.com', { level: "rfc5322" }).valid,
).toBeTruthy();
expect(
validateEmail('".leadingdot"@example.com', { level: "rfc5322" }).valid,
).toBeTruthy();
expect(
validateEmail('"trailingdot."@example.com', { level: "rfc5322" }).valid,
).toBeTruthy();
expect(
validateEmail('"user\\"quote"@example.com', { level: "rfc5322" }).valid,
).toBeTruthy();
expect(
validateEmail('"user\\\\backslashes"@example.com', { level: "rfc5322" })
.valid,
).toBeTruthy();
});
it("handles domain literals (IP addresses) correctly based on RFC 5321/5322 level", () => {
expect(
validateEmail("user@[192.168.0.1]", { level: "rfc5322" }).valid,
).toBeTruthy();
expect(
validateEmail("user@[IPv6:2001:db8::1]", { level: "rfc5322" }).valid,
).toBeTruthy();
});
it("handles comments correctly based on RFC 5322 level", () => {
expect(
validateEmail("user(comment)@example.com", { level: "rfc5322" }).valid,
).toBeTruthy();
expect(
validateEmail("user@(comment)example.com", { level: "rfc5322" }).valid,
).toBeTruthy();
expect(
validateEmail("(comment)user@example.com", { level: "rfc5322" }).valid,
).toBeTruthy();
const parts = validateEmail("user(comment)@example.com", {
level: "rfc5322",
});
expect(parts.valid).toBeTruthy();
const parts2 = validateEmail("user@(comment)example.com", {
level: "rfc5322",
});
expect(parts2.valid).toBeTruthy();
expect(parts2.parts?.domain).toBe("example.com");
});
it("RFC 5322: handles quoted strings with special characters", () => {
expect(
validateEmail('"@+;,"@example.com', { level: "rfc5322" }).valid,
).toBeTruthy();
expect(
validateEmail('"test@test"@example.com', { level: "rfc5322" }).valid,
).toBeTruthy();
expect(
validateEmail('"a\\ b"@example.com', { level: "rfc5322" }).valid,
).toBeTruthy();
});
it("RFC 5322: empty quoted string in local-part is valid", () => {
expect(
validateEmail('""@example.com', { level: "rfc5322" }).valid,
).toBeTruthy();
});
it("RFC 5322: handles various comment placements", () => {
expect(
validateEmail("(comment)user@example.com", { level: "rfc5322" }).valid,
).toBeTruthy();
expect(
validateEmail("user(comment)@example.com", { level: "rfc5322" }).valid,
).toBeTruthy();
expect(
validateEmail("user@(comment)example.com", { level: "rfc5322" }).valid,
).toBeTruthy();
expect(
validateEmail("user@example.com(comment)", { level: "rfc5322" }).valid,
).toBeTruthy();
expect(
validateEmail("user@(comment)[127.0.0.1]", { level: "rfc5322" }).valid,
).toBeTruthy();
});
it("RFC 5322: handles nested comments and comments around dots", () => {
expect(
validateEmail("user(comment(nested))@example.com", {
level: "rfc5322",
}).valid,
).toBeTruthy();
expect(
validateEmail("first.(comment)last@example.com", { level: "rfc5322" })
.valid,
).toBeTruthy();
expect(
validateEmail("user@example.(comment)com", { level: "rfc5322" }).valid,
).toBeTruthy();
});
it("RFC 5322: handles comments with quoted characters", () => {
expect(
validateEmail("user(a\\(b\\)c)@example.com", { level: "rfc5322" })
.valid,
).toBeTruthy();
});
it("RFC 5321/5322: handles domain literals correctly", () => {
expect(
validateEmail("user@[192.168.1.1]", { level: "rfc5322" }).valid,
).toBeTruthy();
expect(
validateEmail("user@[IPv6:2001:db8::1]", { level: "rfc5322" }).valid,
).toBeTruthy();
});
it("RFC 5322 obsolete: handles mixed quoted and unquoted local-parts", () => {
expect(
validateEmail('"first".last@example.com', { level: "rfc5322" }).valid,
).toBeTruthy();
expect(
validateEmail('first."last"@example.com', { level: "rfc5322" }).valid,
).toBeTruthy();
});
it("RFC 5322: handles extra whitespace around @", () => {
expect(
validateEmail("user @ example.com", { level: "rfc5322" }).valid,
).toBeTruthy();
});
it("RFC 5322: should handle comments within the domain part", () => {
expect(
validateEmail("user@example.(comment)com", { level: "rfc5322" }).valid,
).toBeTruthy();
});
it("RFC 5322: should handle whitespace around the '@' symbol", () => {
expect(
validateEmail("user @example.com", { level: "rfc5322" }).valid,
).toBeTruthy();
expect(
validateEmail("user@ example.com", { level: "rfc5322" }).valid,
).toBeTruthy();
expect(
validateEmail("user @ example.com", { level: "rfc5322" }).valid,
).toBeTruthy();
});
it("rejects strings with invalid control characters on stricter levels", () => {
expect(
validateEmail("user\n@example.com", { level: "rfc5322" }).valid,
).toBeFalsy();
expect(
validateEmail("user@example.com\n", { level: "rfc5322" }).valid,
).toBeFalsy();
expect(
validateEmail("\nuser@example.com", { level: "rfc5322" }).valid,
).toBeFalsy();
expect(
validateEmail("user@exam\nple.com", { level: "rfc5322" }).valid,
).toBeFalsy();
expect(
validateEmail("user\r@example.com", { level: "rfc5322" }).valid,
).toBeFalsy();
expect(
validateEmail("user@example.com\r", { level: "rfc5322" }).valid,
).toBeFalsy();
expect(
validateEmail("\ruser@example.com", { level: "rfc5322" }).valid,
).toBeFalsy();
expect(
validateEmail("user@exam\rple.com", { level: "rfc5322" }).valid,
).toBeFalsy();
});
it("rejects emails with excessive length violations on levels with length checks", () => {
const tooLongLocal = `${"a".repeat(65)}@example.com`;
expect(
validateEmail(tooLongLocal, { level: "rfc5322" }).valid,
).toBeFalsy();
const tooLongTotal = `${"a".repeat(990)}@example.com`;
expect(
validateEmail(tooLongTotal, { level: "rfc5322" }).valid,
).toBeFalsy();
});
it("rejects malformed domain structures across all levels", () => {
expect(validateEmail("user@", { level: "rfc5322" }).valid).toBeFalsy();
expect(validateEmail("user@.", { level: "rfc5322" }).valid).toBeFalsy();
expect(validateEmail("user@..", { level: "rfc5322" }).valid).toBeFalsy();
expect(validateEmail("user@...", { level: "rfc5322" }).valid).toBeFalsy();
expect(
validateEmail("user@.domain.example.com", { level: "rfc5322" }).valid,
).toBeFalsy();
expect(
validateEmail("user@domain..example.com", { level: "rfc5322" }).valid,
).toBeFalsy();
});
it("rejects completely invalid strings across all levels", () => {
expect(
validateEmail("not-an-email-at-all", { level: "rfc5322" }).valid,
).toBeFalsy();
expect(validateEmail("12345", { level: "rfc5322" }).valid).toBeFalsy();
expect(
validateEmail("!@#$%^&*()", { level: "rfc5322" }).valid,
).toBeFalsy();
expect(
validateEmail("random string with spaces", { level: "rfc5322" }).valid,
).toBeFalsy();
expect(
validateEmail("just-text", { level: "rfc5322" }).valid,
).toBeFalsy();
expect(
validateEmail("<script>alert('xss')</script>", { level: "rfc5322" })
.valid,
).toBeFalsy();
expect(
validateEmail("../../../etc/passwd", { level: "rfc5322" }).valid,
).toBeFalsy();
expect(
validateEmail("SELECT * FROM users", { level: "rfc5322" }).valid,
).toBeFalsy();
expect(validateEmail("null", { level: "rfc5322" }).valid).toBeFalsy();
expect(
validateEmail("undefined", { level: "rfc5322" }).valid,
).toBeFalsy();
expect(validateEmail("true", { level: "rfc5322" }).valid).toBeFalsy();
expect(validateEmail("false", { level: "rfc5322" }).valid).toBeFalsy();
expect(validateEmail("{}", { level: "rfc5322" }).valid).toBeFalsy();
expect(validateEmail("[]", { level: "rfc5322" }).valid).toBeFalsy();
expect(validateEmail("0", { level: "rfc5322" }).valid).toBeFalsy();
expect(validateEmail("NaN", { level: "rfc5322" }).valid).toBeFalsy();
});
it("rejects email-like but invalid strings across all levels", () => {
expect(
validateEmail("no-at-sign.example.com", { level: "rfc5322" }).valid,
).toBeFalsy();
expect(
validateEmail("multiple@@at@signs.example.com", { level: "rfc5322" })
.valid,
).toBeFalsy();
expect(
validateEmail("@no-local-part.example.com", { level: "rfc5322" }).valid,
).toBeFalsy();
expect(
validateEmail("no-domain@", { level: "rfc5322" }).valid,
).toBeFalsy();
expect(
validateEmail("..consecutive@example.com", { level: "rfc5322" }).valid,
).toBeFalsy();
expect(
validateEmail("trailing..dots@example.com", { level: "rfc5322" }).valid,
).toBeFalsy();
expect(
validateEmail("local@.leading-domain.example.com", {
level: "rfc5322",
}).valid,
).toBeFalsy();
expect(
validateEmail("local@trailing-dot.example.com.", { level: "rfc5322" })
.valid,
).toBeFalsy();
expect(
validateEmail("local@domain..consecutive.example.com", {
level: "rfc5322",
}).valid,
).toBeFalsy();
expect(
validateEmail("user@@example.com", { level: "rfc5322" }).valid,
).toBeFalsy();
expect(
validateEmail("@@@example.com", { level: "rfc5322" }).valid,
).toBeFalsy();
expect(validateEmail("user@@@", { level: "rfc5322" }).valid).toBeFalsy();
expect(
validateEmail("@domain.example.com", { level: "rfc5322" }).valid,
).toBeFalsy();
});
});
});
テストケースには以下のような項目が含まれています:
- 基本的なメールアドレスの検証
-
atextの特殊文字対応(!#$%&'*+/=?^_`{|}~-) - quoted-string対応(RFC 5322のみ)
- コメント対応(RFC 5322のみ)
- domain-literal(IPアドレス)対応
- DNS制約の検証(RFC 5321)
- 長さ制限の検証
- エラーケースの検証(連続ドット、不正なドメインなど)
7. RFC 5321とRFC 5322の比較
実装した2つの正規表現の主な違いをまとめます:
| 項目 | RFC 5321 | RFC 5322 |
|---|---|---|
| local-partの形式 |
dot-atom + quoted-string
|
dot-atom + quoted-string + 混在形式 |
| domainの制約 | DNS準拠(英数字とハイフンのみ) | 構文上は柔軟(実際にはDNS制約推奨) |
| コメント | 許可しない |
@ の前後、ドットの前後に配置可能 |
| 最大長 | 256文字 | 998文字(推奨) |
8. まとめ
本記事では、RFC 5321とRFC 5322に準拠したメールアドレスの正規表現を段階的に構築しました。
繰り返しになりますが、実際のアプリケーションでメールアドレスのバリデーションを行う場合は、記事の冒頭で紹介したHTML仕様の正規表現の使用をお勧めします。
/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
本記事で紹介したRFC準拠の正規表現は、あくまでRFC仕様の理解と学習を目的としています。実際のプロダクション環境では、用途に応じて適切なバリデーション方法を選択することが重要です。
Discussion