JMAP conformance test suite for RFC 8620 (JMAP Core) and RFC 8621 (JMAP Mail). Written in TypeScript, runs on Node.js 22+ or bun. No external test framework — uses a custom lightweight test registry and runner.
Using bun:
bun run ./src/cli.ts
# Typical usage
bun run ./src/cli.ts -c config-stalwart.json -f
bun run ./src/cli.ts -c config-stalwart.json -f --filter 'email/query*'
bun run ./src/cli.ts -c config-stalwart.json -f --filter 'copy,blob' --fail-onlyCLI flags: -c <config> (required), -f (force-clean), -o <path> (JSON output file), --filter <pattern> (glob or substring, comma-separated), --verbose, --fail-only.
Exit codes: 0 = all required tests pass, 1 = required failures, 2 = fatal error.
src/
├── cli.ts # Entry point, arg parsing
├── client/ # JMAP HTTP client, session parsing, transport
├── runner/
│ ├── test-runner.ts # Orchestration: connect → clean → seed → run → teardown → report
│ ├── test-registry.ts # Test registration, glob filtering
│ └── test-context.ts # Shared context object with assertion helpers
├── helpers/
│ └── smee.ts # smee.io webhook proxy for push tests
├── setup/
│ ├── clean-account.ts # Wipe account before run
│ ├── seed-data.ts # Create test mailboxes, emails, blobs
│ └── teardown.ts # Remove seeded data after run
├── reporter/
│ ├── console-reporter.ts # Colored terminal output (PASS/FAIL/WARN/SKIP)
│ └── json-reporter.ts # Structured JSON report
├── types/
│ ├── config.ts # Config schema and runtime validation
│ ├── jmap-core.ts # RFC 8620 type definitions
│ ├── jmap-mail.ts # RFC 8621 type definitions
│ └── report.ts # TestResult, TestReport interfaces
└── tests/ # ~300 tests across 42 files in 10 categories
├── core/ # Session, echo, request errors, method errors, result references
├── binary/ # Upload, download, blob copy
├── email/ # Get, changes, query (filters/sort/paging), set, copy, import, parse
├── mailbox/ # Get, changes, query, queryChanges, set
├── thread/ # Get, changes
├── identity/ # Get, changes, set
├── submission/ # EmailSubmission set (send, envelope, onSuccess)
├── vacation/ # VacationResponse get/set
├── push/ # PushSubscription CRUD, EventSource
└── search-snippet/ # SearchSnippet get
Every test file uses defineTests():
defineTests({ rfc: "RFC8621", section: "4.4", category: "email" }, [
{
id: "query-filter-from", // becomes testId "email/query-filter-from"
name: "Email/query filter from matches sender",
required: true, // default; false = recommended (SHOULD) behavior
runIf: (ctx) => ..., // optional: return true to run, or string reason to skip
fn: async (ctx) => {
const result = await ctx.client.call("Email/query", { ... });
ctx.assertEqual(result.ids.length, 1);
},
},
]);Test ID format: {category}/{id} — e.g., email/query-filter-from. The --filter flag matches against this.
required: true(default) — RFC MUST behavior. Failures are FAIL (red) and affect exit code.required: false— RFC SHOULD behavior. Failures are WARN (yellow) and don't affect exit code.
Tests use runIf to declare preconditions. Return true to run, or a string skip reason:
runIf: (ctx) =>
!ctx.config.users.secondary ? "No secondary user configured"
: ctx.identityIds.length === 0 ? "No identities available"
: true,Common skip conditions:
- No secondary user → submission tests skip
- smee.io unreachable → push callback tests skip
- No cross-account access → Email/copy, Blob/copy tests skip
The TestContext provides:
client/secondaryClient— JMAP clients for primary/secondary usersaccountId/crossAccountId— account IDsroleMailboxes—{ inbox: "id", drafts: "id", sent: "id", ... }mailboxIds—{ folderA: "id", folderB: "id", child1: "id", child2: "id" }emailIds— seeded email IDs keyed by name (e.g.,"plain-simple","thread-reply-2")blobIds— uploaded blob IDsidentityIds,identityEmail,secondaryEmailsmeeChannel— smee.io webhook for push tests- 20+ assertion helpers:
assertEqual,assertTruthy,assertIncludes,assertGreaterThan, etc.
- Connect — Fetch JMAP session, discover accounts and capabilities
- Clean — Remove existing test data (requires
-fflag if account not empty) - Seed — Create 4 mailboxes, ~22 emails, 2 blobs, discover identities
- Run — Execute tests sequentially, record pass/fail/skip with timing
- Teardown — Destroy all seeded data
- Report — Output JSON report (stdout or file) and console summary
Config files live in the project root (e.g., config-stalwart.json, config-fastmail.json):
{
"sessionUrl": "https://server/.well-known/jmap",
"users": {
"primary": { "username": "user1", "password": "pass" },
"secondary": { "username": "user2", "password": "pass" }
},
"authMethod": "basic",
"timeout": 30000,
"serverInfo": "Stalwart v0.11"
}users.secondary is optional — enables EmailSubmission tests (sending email between accounts).
- Test names for MUST behavior say "MUST" in the name. SHOULD tests say "SHOULD".
- Same-account
/copyis invalid per spec — cross-account tests usectx.crossAccountId. - Tests clean up after themselves (destroy created objects in
finallyblocks). - No
filter: nullin Email/query calls — omit the property instead (some servers reject null). - HTTP status code assertions use range checks (
>= 400 && < 500), not specific codes. - The
JmapMethodErrorclass wraps JMAP method-level errors thrown byclient.call().
rfc8620.txtandrfc8621.txt- RFC text for JMAP Core and JMAP Mail.rfc-assumptions.md— Comprehensive list of RFC interpretation decisions and open questionsfailure-analysis.md— Categorized analysis of test failures against Stalwart