Skip to content

Commit 6d19878

Browse files
committed
Add support for email formats
1 parent fc9e687 commit 6d19878

File tree

4 files changed

+102
-12
lines changed

4 files changed

+102
-12
lines changed

formats/formats-test-suite.spec.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,6 @@ type Test = {
2525
};
2626

2727
const skip = new Set<string>([
28-
// Not supported
29-
"|draft2020-12|email.json",
30-
"|draft2019-09|email.json",
31-
"|draft7|email.json",
32-
"|draft6|email.json",
33-
"|draft4|email.json",
34-
35-
// Not supported
36-
"|draft2020-12|idn-email.json",
37-
"|draft2019-09|idn-email.json",
38-
"|draft7|idn-email.json",
39-
4028
// Uses UTS #46 rather than RFC 589[0-5]
4129
"|draft2020-12|idn-hostname.json|validation of internationalized host names|contains illegal char U+302E Hangul single dot tone mark",
4230
"|draft2019-09|idn-hostname.json|validation of internationalized host names|contains illegal char U+302E Hangul single dot tone mark",

formats/handlers/email.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
const alpha = `[a-zA-Z]`;
2+
const hexdig = `[\\da-fA-F]`;
3+
4+
// Printable US-ASCII characters not including specials.
5+
const atext = `[\\w!#$%&'*+\\-/=?^\`{|}~]`;
6+
const atom = `${atext}+`;
7+
const dotString = `${atom}(?:\\.${atom})*`;
8+
9+
// Any ASCII graphic or space without blackslash-quoting except double-quote and the backslash itself.
10+
const qtextSMTP = `[\\x20-\\x21\\x23-\\x5B\\x5D-\\x7E]`;
11+
// backslash followed by any ASCII graphic or space
12+
const quotedPairSMTP = `\\\\[\\x20-\\x7E]`;
13+
const qcontentSMTP = `(?:${qtextSMTP}|${quotedPairSMTP})`;
14+
const quotedString = `"${qcontentSMTP}*"`;
15+
16+
const localPart = `(?:${dotString}|${quotedString})`; // MAY be case-sensitive
17+
18+
const letDig = `(?:${alpha}|\\d)`;
19+
const ldhStr = `(?:${letDig}|-)*${letDig}`;
20+
const subDomain = `${letDig}${ldhStr}?`;
21+
const domain = `${subDomain}(?:\\.${subDomain})*`;
22+
23+
const decOctet = `(?:\\d|[1-9]\\d|1\\d\\d|2[0-4]\\d|25[0-5])`;
24+
const ipv4Address = `${decOctet}\\.${decOctet}\\.${decOctet}\\.${decOctet}`;
25+
26+
const h16 = `${hexdig}{1,4}`;
27+
const ls32 = `(?:${h16}:${h16}|${ipv4Address})`;
28+
const ipv6Address = `(?:(?:${h16}:){6}${ls32}|::(?:${h16}:){5}${ls32}|(?:${h16})?::(?:${h16}:){4}${ls32}|(?:(?:${h16}:){0,1}${h16})?::(?:${h16}:){3}${ls32}|(?:(?:${h16}:){0,2}${h16})?::(?:${h16}:){2}${ls32}|(?:(?:${h16}:){0,3}${h16})?::(?:${h16}:){1}${ls32}|(?:(?:${h16}:){0,4}${h16})?::${ls32}|(?:(?:${h16}:){0,5}${h16})?::${h16}|(?:(?:${h16}:){0,6}${h16})?::)`;
29+
const ipv6AddressLiteral = `IPv6:${ipv6Address}`;
30+
31+
const dcontent = `[\\x21-\\x5A\\x5E-\\x7E]`; // Printable US-ASCII excluding "[", "\", "]"
32+
const generalAddressLiteral = `${ldhStr}:${dcontent}+`;
33+
34+
const addressLiteral = `\\[(?:${ipv4Address}|${ipv6AddressLiteral}|${generalAddressLiteral})]`;
35+
36+
const mailbox = `${localPart}@(?:${domain}|${addressLiteral})`;
37+
38+
export const isEmail = RegExp.prototype.test.bind(new RegExp(`^${mailbox}$`));
39+
40+
export default {
41+
id: "https://json-schema.org/format/email",
42+
handler: (email) => typeof email !== "string" || isEmail(email)
43+
};

formats/handlers/idn-email.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { isIdnHostname } from "./idn-hostname.js";
2+
3+
4+
const ucschar = `[\\u{A0}-\\u{D7FF}\\u{F900}-\\u{FDCF}\\u{FDF0}-\\u{FFEF}\\u{10000}-\\u{1FFFD}\\u{20000}-\\u{2FFFD}\\u{30000}-\\u{3FFFD}\\u{40000}-\\u{4FFFD}\\u{50000}-\\u{5FFFD}\\u{60000}-\\u{6FFFD}\\u{70000}-\\u{7FFFD}\\u{80000}-\\u{8FFFD}\\u{90000}-\\u{9FFFD}\\u{A0000}-\\u{AFFFD}\\u{B0000}-\\u{BFFFD}\\u{C0000}-\\u{CFFFD}\\u{D0000}-\\u{DFFFD}\\u{E1000}-\\u{EFFFD}]`;
5+
6+
const alpha = `[a-zA-Z]`;
7+
const hexdig = `[\\da-fA-F]`;
8+
9+
// Printable US-ASCII characters not including specials.
10+
const atext = `(?:[\\w!#$%&'*+\\-/=?^\`{|}~]|${ucschar})`;
11+
const atom = `${atext}+`;
12+
const dotString = `${atom}(?:\\.${atom})*`;
13+
14+
// Any ASCII graphic or space without blackslash-quoting except double-quote and the backslash itself.
15+
const qtextSMTP = `(?:[\\x20-\\x21\\x23-\\x5B\\x5D-\\x7E]|${ucschar})`;
16+
// backslash followed by any ASCII graphic or space
17+
const quotedPairSMTP = `\\\\[\\x20-\\x7E]`;
18+
const qcontentSMTP = `(?:${qtextSMTP}|${quotedPairSMTP})`;
19+
const quotedString = `"${qcontentSMTP}*"`;
20+
21+
const localPart = `(?:${dotString}|${quotedString})`; // MAY be case-sensitive
22+
23+
const letDig = `(?:${alpha}|\\d)`;
24+
const ldhStr = `(?:${letDig}|-)*${letDig}`;
25+
const letDigUcs = `(?:${alpha}|\\d|${ucschar})`;
26+
const ldhStrUcs = `(?:${letDigUcs}|-)*${letDigUcs}`;
27+
const subDomain = `${letDigUcs}${ldhStrUcs}?`;
28+
const domain = `${subDomain}(?:\\.${subDomain})*`;
29+
30+
const decOctet = `(?:\\d|[1-9]\\d|1\\d\\d|2[0-4]\\d|25[0-5])`;
31+
const ipv4Address = `${decOctet}\\.${decOctet}\\.${decOctet}\\.${decOctet}`;
32+
33+
const h16 = `${hexdig}{1,4}`;
34+
const ls32 = `(?:${h16}:${h16}|${ipv4Address})`;
35+
const ipv6Address = `(?:(?:${h16}:){6}${ls32}|::(?:${h16}:){5}${ls32}|(?:${h16})?::(?:${h16}:){4}${ls32}|(?:(?:${h16}:){0,1}${h16})?::(?:${h16}:){3}${ls32}|(?:(?:${h16}:){0,2}${h16})?::(?:${h16}:){2}${ls32}|(?:(?:${h16}:){0,3}${h16})?::(?:${h16}:){1}${ls32}|(?:(?:${h16}:){0,4}${h16})?::${ls32}|(?:(?:${h16}:){0,5}${h16})?::${h16}|(?:(?:${h16}:){0,6}${h16})?::)`;
36+
const ipv6AddressLiteral = `IPv6:${ipv6Address}`;
37+
38+
const dcontent = `[\\x21-\\x5A\\x5E-\\x7E]`; // Printable US-ASCII excluding "[", "\", "]"
39+
const generalAddressLiteral = `${ldhStr}:${dcontent}+`;
40+
41+
const addressLiteral = `\\[(?:${ipv4Address}|${ipv6AddressLiteral}|${generalAddressLiteral})\\]`;
42+
43+
const mailbox = `(?<localPart>${localPart})@(?:(?<ip>${addressLiteral})|(?<domain>${domain}))`;
44+
45+
const mailboxPattern = new RegExp(`^${mailbox}$`, "u");
46+
export const isIdnEmail = (email) => {
47+
const parsedEmail = mailboxPattern.exec(email)?.groups;
48+
49+
return !!parsedEmail && (!parsedEmail.domain || isIdnHostname(parsedEmail.domain));
50+
};
51+
52+
export default {
53+
id: "https://json-schema.org/format/idn-email",
54+
handler: (email) => typeof email !== "string" || isIdnEmail(email)
55+
};

formats/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import dateTime from "./handlers/date-time.js";
44
import date from "./handlers/date.js";
55
import time from "./handlers/time.js";
66
import duration from "./handlers/duration.js";
7+
import email from "./handlers/email.js";
8+
import idnEmail from "./handlers/idn-email.js";
79
import hostname from "./handlers/hostname.js";
810
import idnHostname from "./handlers/idn-hostname.js";
911
import ipv4 from "./handlers/ipv4.js";
@@ -23,6 +25,8 @@ addFormat(dateTime);
2325
addFormat(date);
2426
addFormat(time);
2527
addFormat(duration);
28+
addFormat(email);
29+
addFormat(idnEmail);
2630
addFormat(hostname);
2731
addFormat(idnHostname);
2832
addFormat(ipv4);

0 commit comments

Comments
 (0)