npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@byspec/email

v0.1.0

Published

Spec-anchored email address test cases — RFC 5321 / RFC 5322

Downloads

90

Readme

@byspec/email

Spec-anchored email address test cases for validators and parsers.

Each case provides a string input, machine-readable metadata pointing back to the exact specification clause that governs it, and a character position identifying where any violation occurs. Consuming libraries use these cases to verify they handle every email edge case — and to know precisely which ones they are deliberately skipping.


Installation

npm install --save-dev @byspec/email

Design contract

  • All inputs are strings. The library is designed for testing email validators and parsers, not for generating addresses. Your validator receives a string; these cases provide the strings worth testing.

  • meta.position is the 0-based character index where the violation occurs. For valid cases it is always -1. For invalid cases it points to the first offending character in the full address string (including the @ separator offset for domain-side errors).

  • What a specific library rejects may differ from what this package marks invalid. Many validators allow lenient forms (e.g. quoted local parts, Punycode domains) that others reject. Use this package to enumerate the spec-defined cases; decide in your own tests which ones apply to your library's strictness level.

  • Each scenario is independent. Import only what your domain requires.


Scenarios

| Scenario | Named export | Description | |---|---|---| | valid | valid | RFC 5321 / RFC 5322 conformant addresses that every validator should accept | | invalid-local-part | invalidLocalPart | Addresses whose local part (before @) violates RFC 5321 §4.1.2 | | invalid-domain | invalidDomain | Addresses whose domain (after @) violates RFC 5321 §4.1.2 or RFC 1035 §2.3.4 |

valid

| Rule | Input example | Note | |---|---|---| | simple-address | [email protected] | | | local-part-with-dot | [email protected] | | | local-part-with-plus | [email protected] | Sub-addressing / tagged mailboxes | | local-part-with-hyphen | [email protected] | | | local-part-with-underscore | [email protected] | | | local-part-with-digits | [email protected] | | | case-insensitive-domain | [email protected] | Domain is case-insensitive per §2.4 | | subdomain | [email protected] | | | multi-label-tld | [email protected] | | | quoted-local-part | "quoted local"@example.com | Quoted strings allow spaces | | single-char-local-part | [email protected] | Minimal valid form | | punycode-domain | [email protected] | IDN in ACE/Punycode form |

invalid-local-part

| Rule | Input example | Position | |---|---|---| | local-part-must-not-be-empty | @example.com | 0 | | local-part-cannot-start-with-dot | [email protected] | 0 | | local-part-cannot-end-with-dot | [email protected] | 4 | | local-part-cannot-have-consecutive-dots | [email protected] | 3 | | unquoted-local-part-cannot-contain-space | user [email protected] | 4 |

invalid-domain

| Rule | Input example | Position | Part | |---|---|---|---| | domain-must-not-be-empty | user@ | 5 | domain | | domain-cannot-start-with-dot | [email protected] | 5 | domain | | domain-cannot-end-with-dot | user@example. | 12 | domain | | domain-cannot-have-consecutive-dots | [email protected] | 12 | domain | | domain-label-cannot-contain-underscore | user@exam_ple.com | 9 | domain | | tld-must-not-be-all-numeric | [email protected] | 13 | tld |


Usage

Validating against a single scenario

import { invalidLocalPart } from "@byspec/email";

for (const c of invalidLocalPart.cases) {
  const result = myValidator(c.input);
  if (result.valid !== false) {
    console.error(
      `FAIL [${c.meta.rule}] input="${c.input}" should have been rejected at position ${c.meta.position}`
    );
  }
}

Iterating all scenarios at once

import { scenarios } from "@byspec/email";

for (const scenario of scenarios) {
  for (const c of scenario.cases) {
    const result = myValidator(c.input);
    const expectValid = c.meta.scenario === "valid";

    if (result.valid !== expectValid) {
      console.error(
        `FAIL [${scenario.name} / ${c.meta.rule}] input="${c.input}"`
      );
    }
  }
}

Filtering cases by tag

The filterCases utility lets you select cases across scenarios by tag, rule, part, or scenario name without writing .filter() manually.

import { filterCases, scenarios } from "@byspec/email";

// Only common, everyday address patterns
const commonCases = filterCases(scenarios, { tags: ["common"] });

// All domain-side invalid cases
const domainErrors = filterCases(scenarios, { part: "domain" });

// Quoted local part cases only
const quotedCases = filterCases(scenarios, { tags: ["quoted"] });

// Tagged (plus-sign) and IDN cases — OR logic
const specialCases = filterCases(scenarios, { anyTag: ["tagged", "idn"] });

// All cases for one scenario by name
const validOnly = filterCases(scenarios, { scenario: "valid" });

// A specific rule
const noEmptyDomain = filterCases(scenarios, { rule: "domain-must-not-be-empty" });

Filtering cases by rule — direct array approach

The cases array is a plain array, so .filter() and .find() work directly too:

import { invalidDomain } from "@byspec/email";

const tldErrors = invalidDomain.cases.filter(c => c.meta.part === "tld");
const emptyDomain = invalidDomain.cases.find(c => c.meta.rule === "domain-must-not-be-empty");

Handling cases where your validator intentionally diverges

import { invalidLocalPart } from "@byspec/email";

// A lenient validator that permits quoted local parts — skip those cases
const quotedRules = new Set(["quoted-local-part"]);

for (const c of invalidLocalPart.cases) {
  if (quotedRules.has(c.meta.rule)) continue; // known divergence

  const result = myLenientValidator(c.input);
  expect(result.valid).toBe(false);
}

filterCases(scenarios, opts?)

import { filterCases } from "@byspec/email";

Parameters

| Option | Type | Logic | Description | |---|---|---|---| | scenario | string | exact match | Filter by meta.scenario name | | rule | string | exact match | Filter by meta.rule identifier | | part | string | exact match | Filter by meta.part"local", "domain", "tld", or "full" | | tags | string[] | ALL must match | Case must carry every tag listed | | anyTag | string \| string[] | ANY must match | Case must carry at least one of the tags |

Omit opts (or pass {}) to get all cases across all provided scenarios.


Tags

Every case carries meta.tags?: string[]. The full tag vocabulary:

| Tag | Meaning | |---|---| | common | Typical everyday email pattern | | minimal | Shortest valid or invalid representative | | quoted | Local part uses RFC 5321 quoted-string form | | tagged | Local part uses + sub-addressing | | case-sensitive | Case-handling behaviour under test | | idn | Internationalised domain (Punycode / IDNA) | | long-local | Local part at or near the 64-octet limit | | long-domain | Domain at or near the 255-octet limit | | long-label | A single domain label at or near 63 octets |


TypeScript

Types are exported from the main entry point:

import type { EmailCase, EmailScenario, EmailMeta, EmailTag, FilterOptions }
  from "@byspec/email";

Key types:

interface EmailMeta {
  scenario: string;   // e.g. "invalid-domain"
  spec: string;       // e.g. "RFC 5321 §4.1.2"
  rule: string;       // e.g. "domain-cannot-end-with-dot"
  position: number;   // 0-based index of the violation; -1 for valid cases
  part: "local" | "domain" | "tld" | "full";
  tags?: EmailTag[];  // cross-cutting classification
  note?: string;      // human explanation for tricky or counter-intuitive cases
}

interface EmailCase {
  input: string;      // always a string — feed to your validator
  meta: EmailMeta;
}

interface FilterOptions {
  scenario?: string;
  rule?: string;
  part?: "local" | "domain" | "tld" | "full";
  tags?: EmailTag[];
  anyTag?: EmailTag | EmailTag[];
}

Specification references