jmap-jam
v0.7.0
Published
A JMAP client library for Node.js and the browser.
Downloads
19
Readme
Jam is a tiny (~2kb gzipped), strongly-typed JMAP client with zero runtime dependencies. It has friendly, fluent APIs that make working with JMAP a breeze.
Jam is compatible with environments that support the Web Fetch API and ES Modules.
Jam adheres to the following IETF standards:
Table of Contents
Installation
Jam works in any environment that supports the Web Fetch API and ES Modules, including Node.js (>=18
) and the browser.
Use as a package:
npm install jmap-jam
Use in the browser:
<script type="module">
import JamClient from "https://your-preferred-cdn.com/jmap-jam@<version>";
</script>
Getting Started
To initialize a client, provide the session URL for a JMAP server to connect to, as well as a bearer token for authenticating requests.
import JamClient from "jmap-jam";
const jam = new JamClient({
sessionUrl: "https://jmap.example.com/.well-known/jmap",
bearerToken: "super-secret-token",
});
Making Requests
JMAP is a meta protocol that makes performing multiple, dependent operations on a server more efficient by accepting batches of them in a single HTTP request.
A request is made up of one or more invocations (also known as method calls) that each specify a method, arguments, and a method call ID (an arbitrary string chosen by the requester). Method calls can reference each other with this ID, allowing for complex requests to be made.
To learn more about requests in JMAP, see the following resources:
- JMAP Guides (JMAP website)
- Standard Methods and Naming Conventions (RFC 8620 § 5)
- Entities and Methods for Mail (RFC 8621)
Individual Requests
Here's what a single request looks like with Jam:
const jam = new JamClient({ ... });
// Using convenience methods
const [mailboxes] = await jam.api.Mailbox.get({ accountId: "123" });
// Using a plain request
const [mailboxes] = await jam.request(["Mailbox/get",{ accountId: "123" }]);
Both of these methods output the same JMAP request:
{
"using": ["urn:ietf:params:jmap:mail"],
"methodCalls": [
[
"Mailbox/get", // <------------ Method name
{ "accountId": "123" }, // <--- Arguments
"r1" // <------------- Method call ID (autogenerated)
]
]
}
Convenience methods for available JMAP entities (e.g. Email, Mailbox, Thread) are available through the api
property.
Or, as seen in the example, requests can be made without convenience methods by using the request
method directly.
Both methods of sending requests have strongly typed responses and can be used interchangeably.
Multiple Requests
Though JMAP examples often show multiple method calls being used in a single request, see the Notes on Concurrency section for information about why a single method call per request can sometimes be preferred.
To send multiple method calls in a single request, use requestMany
.
const jam = new JamClient({ ... });
const accountId = '<account-id>';
const mailboxId = '<mailbox-id>';
const [{ emails }, meta] = await jam.requestMany((t) => {
// Get the first 10 email IDs in the mailbox
const emailIds = t.Email.query({
accountId,
filter: {
inMailbox: mailboxId,
},
limit: 10,
});
// Get the emails with those IDs
const emails = t.Email.get({
accountId,
ids: emailIds.$ref("/ids"), // Using a result reference
properties: ["id", "htmlBody"],
});
return { emailIds, emails };
});
This produces the following JMAP request:
{
"using": ["urn:ietf:params:jmap:mail"],
"methodCalls": [
[
"Email/query",
{
"accountId": "<account-id>",
"filter": {
"inMailbox": "<mailbox-id>"
}
},
"emailIds"
],
[
"Email/get",
{
"accountId": "<account-id>",
"#ids": {
"name": "Email/query",
"resultOf": "emailIds",
"path": "/ids"
},
"properties": ["id", "htmlBody"]
},
"emails"
]
]
}
The t
argument used in the requestMany
callback is a Proxy that lets "invocation drafts" be defined before they are assembled into an actual JMAP request sent to the server.
To create a result reference between invocations, use the $ref
method on the invocation draft to be referenced.
Request Options
When making requests, you can pass an optional options
object as the second argument to request
, requestMany
, or any of the convenience methods. This object accepts the following properties:
fetchInit
- An object that will be passed to the Fetch APIfetch
method as the second argument. This can be used to set headers, change the HTTP method, etc.createdIds
- A object containing client-specified creation IDs mapped to IDs the server assigned when each record was successfully created.using
- An array of additional JMAP capabilities to include when making the request.
Response Metadata
Convenience methods, request
, and requestMany
all return a two-item tuple that contains the response data and metadata.
const [mailboxes, meta] = await jam.api.Mailbox.get({ accountId: "123" });
const { sessionState, createdIds, response } = meta;
The meta object contains the following properties:
sessionState
- The current session state.createdIds
- A map of method call IDs to the IDs of any objects created by the server in response to the request.response
- The actual Fetch APIResponse
.
Notes on Concurrency
RFC 8620 § 3.10: Method calls within a single request MUST be executed in order [by the server]. However, method calls from different concurrent API requests may be interleaved. This means that the data on the server may change between two method calls within a single API request.
JMAP supports passing multiple method calls in a single request, but it is important to remember that each method call will be executed in sequence, not concurrently.
TypeScript
Jam provides types for JMAP methods, arguments, and responses as described in the JMAP and JMAP Mail RFCs.
All convenience methods, request
, and requestMany
will reveal autosuggested types for method names (e.g. Email/get
), the arguments for that method, and the appropriate response.
Many response types will infer from arguments. For example, when using an argument field such as properties
to filter fields in a response, the response type will be narrowed to exclude fields that were not included.
Capabilities
Jam has strongly-typed support for the following JMAP capabilities:
| Entity | Capability Identifier |
| ---------------- | --------------------------------------- |
| Core | urn:ietf:params:jmap:core
|
| Mailbox | urn:ietf:params:jmap:mail
|
| Thread | urn:ietf:params:jmap:mail
|
| Email | urn:ietf:params:jmap:mail
|
| SearchSnippet | urn:ietf:params:jmap:mail
|
| Identity | urn:ietf:params:jmap:submission
|
| EmailSubmission | urn:ietf:params:jmap:submission
|
| VacationResponse | urn:ietf:params:jmap:vacationresponse
|
API Reference
JamClient
JamClient
is Jam's primary entrypoint. To use Jam, import and construct an instance.
The class can be imported by name or using default import syntax.
import JamClient from "jmap-jam";
const jam = new JamClient({
bearerToken: "<bearer-token>",
sessionUrl: "<server-session-url>",
});
A client instance requires both a bearerToken
and sessionUrl
in order to make authenticated requests.
Upon constructing a client, Jam will immediately dispatch a request for a session from the server. This session will be used for all subsequent requests.
api.<entity>.<operation>()
A convenience pattern for making individual JMAP requests that uses the request
method under the hood.
const [mailboxes] = await jam.api.Mailbox.get({
accountId,
});
const [emails] = await jam.api.Email.get({
accountId,
ids: ["email-123"],
properties: ["subject"],
});
request()
Send a standard JMAP request.
const [mailboxes] = await jam.request(["Mailbox/get", { accountId }]);
const [emails] = await jam.request([
"Email/get",
{
accountId,
ids: ["email-123"],
properties: ["subject"],
},
]);
requestMany()
Send a JMAP request with multiple method calls.
const [{ emailIds, emails }] = await jam.requestMany((r) => {
const emailIds = r.Email.query({
accountId,
filter: {
inMailbox: mailboxId,
},
});
const emails = r.Email.get({
accountId,
ids: emailIds.$ref("/ids"),
properties: ["id", "htmlBody"],
});
return { emailIds, emails };
});
$ref()
Each item created within a requestMany
callback is an instance of InvocationDraft
. Internally, it keeps track of the invocation that was defined for use when the request is finalized and sent.
The important part of InvocationDraft
is that each draft exposes a method $ref
that can be used to create a result reference between invocations.
To create a result reference, call $ref
with a JSON pointer at the field that will receive the reference.
The emailIds.$ref("/ids")
call in the previous code block will be transformed into this valid JMAP result reference before the request is sent:
{
"using": ["urn:ietf:params:jmap:mail"],
"methodCalls": [
[
"Email/query",
{
"accountId": "<account-id>",
"filter": {
"inMailbox": "<mailbox-id>"
}
},
"emailIds"
],
[
"Email/get",
{
"accountId": "<account-id>",
// Result reference created here
"#ids": {
"name": "Email/query",
"resultOf": "emailIds",
"path": "/ids"
},
"properties": ["id", "htmlBody"]
},
"emails"
]
]
}
session
Get the client's current session.
const session = await jam.session;
getPrimaryAccount()
Get the ID of the primary mail account for the current session.
const accountId = await jam.getPrimaryAccount();
uploadBlob()
Initiate a fetch request to upload a blob.
const data = await jam.uploadBlob(
accountId,
new Blob(["hello world"], { type: "text/plain" })
);
console.log(data); // =>
// {
// accountId: "account-abcd",
// blobId: "blob-123",
// type: "text/plain",
// size: 152,
// }
downloadBlob()
Intiate a fetch request to download a specific blob. Downloading a blob requires both a MIME type and file name, since JMAP server implementations are not required to store this information.
If the JMAP server sets a Content-Type
header in its response, it will use the value provided in mimeType
.
If the JMAP server sets a Content-Disposition
header in its response, it will use the value provided in fileName
.
const response = await jam.downloadBlob({
accountId,
blobId: 'blob-123'
mimeType: 'image/png'
fileName: 'photo.png'
});
const blob = await response.blob();
// or response.arrayBuffer()
// or response.text()
// ...etc
connectEventSource()
Connect to a JMAP event source using Server-Sent Events.
const sse = await jam.connectEventSource({
types: "*", // or ["Mailbox", "Email", ...]
ping: 5000, // ping interval in milliseconds
closeafter: "no", // or "state"
});
sse.addEventListener("message", (event) => ...));
sse.addEventListener("error", (event) => ...));
sse.addEventListener("close", (event) => ...));