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 🙏

© 2024 – Pkg Stats / Ryan Hefner

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:

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 API fetch 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 API Response.

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) => ...));