JMAP (JSON Meta Application Protocol) is a modern, efficient protocol for synchronising mail, contacts, and calendars. It replaces older protocols like IMAP with a stateless, JSON-based API designed for fast, reliable client-server communication. JMAP is defined in RFC 8620 (core) and RFC 8621 (mail).
jmap-kit is a typed JavaScript/TypeScript library for building JMAP clients. It handles:
.well-known/jmap, session state tracking, staleness detectionThe library is organised around these core concepts:
yarn add jmap-kit
The library does not bundle an HTTP client. Instead, you provide a Transport implementation that handles HTTP GET and POST requests, including authentication.
A Transport must implement two methods:
type Transport = {
get: <T>(url: string | URL, options?: TransportRequestOptions) => Promise<T>;
post: <T>(url: string | URL, options?: TransportRequestOptions) => Promise<T>;
};
TransportRequestOptions includes:
| Property | Type | Description |
|---|---|---|
headers |
Headers |
Additional HTTP headers |
responseType |
"json" | "blob" |
Expected response type (default: "json") |
signal |
AbortSignal |
For cancelling the request |
body |
string | Blob | ArrayBuffer | File |
Request body (POST only) |
The repository includes an example fetch-based transport in examples/basic/ for reference. This example is not part of the published library — you must provide your own Transport implementation.
Here is a minimal example using the Fetch API with bearer token authentication:
import type { Transport, TransportRequestOptions } from "jmap-kit";
function createTransport(bearerToken: string): Transport {
async function request<T>(method: string, url: string | URL, options: TransportRequestOptions = {}): Promise<T> {
const headers = new Headers(options.headers);
headers.set("Authorization", `Bearer ${bearerToken}`);
const response = await fetch(url.toString(), {
method,
headers,
body: "body" in options ? options.body : undefined,
signal: options.signal,
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
if (options.responseType === "blob") {
return (await response.blob()) as T;
}
return (await response.json()) as T;
}
return {
get: <T>(url: string | URL, options?: TransportRequestOptions) => request<T>("GET", url, options),
post: <T>(url: string | URL, options?: TransportRequestOptions) => request<T>("POST", url, options),
};
}
For custom authentication schemes or different HTTP libraries, implement the Transport interface directly. See Customisation for a detailed guide on transport requirements and error handling.
import { JMAPClient } from "jmap-kit";
const client = new JMAPClient(transport, {
hostname: "api.example.com",
// port: 443, // default
// logger: ..., // optional, see Customisation
// emitter: ..., // optional, see Customisation
});
The hostname and optional port tell the client where to discover the JMAP session. The default port is 443.
await client.connect();
connect() performs session discovery by fetching https://{hostname}:{port}/.well-known/jmap. The server returns a session object containing:
After connecting, the client validates capability data in the session against any schemas provided by registered capabilities, strips invalid non-Core capabilities, and configures itself according to the server's capabilities (e.g., concurrency limits, maximum request sizes). See Capabilities for details.
connect() is idempotent — calling it while a connection is in progress returns the same promise. It throws if called while disconnecting.
Before making method calls for a particular data type, register the corresponding capability:
import { EmailCapability } from "jmap-kit";
client.registerCapabilities(EmailCapability);
The Core capability is always registered automatically. See Capabilities for the full list of built-in capabilities.
await client.disconnect();
Disconnecting aborts all in-flight requests, waits for them to settle, and clears the session state. Like connect(), it is idempotent.
The client tracks its state via client.connectionStatus:
| Status | Description |
|---|---|
"disconnected" |
Not connected (initial state) |
"connecting" |
Session discovery in progress |
"connected" |
Session established, ready to send requests |
"disconnecting" |
Aborting in-flight requests and clearing state |
The following methods are chainable and may be called while the client is disconnected:
const client = new JMAPClient(transport, { hostname: "api.example.com" })
.withHostname("other.example.com")
.withPort(8443)
.withHeaders({ "X-Custom": "value" })
.withLogger(myLogger)
.withEmitter(myEmitter);
Both connect() and disconnect() are idempotent:
connect() while already connecting returns the same promise (no duplicate network requests)disconnect() while already disconnecting returns the same promisedisconnect() while already disconnected is a no-opWhen disconnect() is called:
AbortController"disconnected"The client respects server-defined concurrency limits from the core capabilities:
maxConcurrentRequests (default: 4 if not connected)maxConcurrentUpload (default: 4 if not connected)Excess requests and uploads are automatically queued and sent as earlier operations complete.