- Notifications
You must be signed in to change notification settings - Fork1
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.
License
htunnicliff/jmap-jam
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
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 theWeb Fetch API andES Modules.
Jam adheres to the following IETF standards:
Jam works in any environment that supports theWeb Fetch API andES Modules, including Node.js (>=18
) and the browser.
Use as a package:
npm install jmap-jam
Use in the browser:
<scripttype="module">importJamClientfrom"https://your-preferred-cdn.com/jmap-jam@<version>";</script>
To initialize a client, provide the session URL for a JMAP server to connect to, as well as a bearer token for authenticating requests.
importJamClientfrom"jmap-jam";constjam=newJamClient({sessionUrl:"https://jmap.example.com/.well-known/jmap",bearerToken:"super-secret-token",});
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 moreinvocations (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 canreference 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)
Here's what a single request looks like with Jam:
constjam=newJamClient({ ...});// Using convenience methodsconst[mailboxes]=awaitjam.api.Mailbox.get({accountId:"123"});// Using a plain requestconst[mailboxes]=awaitjam.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 theapi
property.
Or, as seen in the example, requests can be made without convenience methods by using therequest
method directly.
Both methods of sending requests have strongly typed responses and can be used interchangeably.
Though JMAP examples often show multiple method calls being used in a single request, see theNotes 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, userequestMany
.
constjam=newJamClient({ ...});constaccountId='<account-id>';constmailboxId='<mailbox-id>';const[{ emails},meta]=awaitjam.requestMany((t)=>{// Get the first 10 email IDs in the mailboxconstemailIds=t.Email.query({ accountId,filter:{inMailbox:mailboxId,},limit:10,});// Get the emails with those IDsconstemails=t.Email.get({ accountId,ids:emailIds.$ref("/ids"),// Using a result referenceproperties:["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" ] ]}
Thet
argument used in therequestMany
callback is aProxy that lets "invocation drafts" be definedbefore they are assembled into an actual JMAP request sent to the server.
To create aresult reference between invocations, use the$ref
method on the invocation draft to be referenced.
When making requests, you can pass an optionaloptions
object as the second argument torequest
,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.
Convenience methods,request
, andrequestMany
all return a two-item tuple that contains the response data and metadata.
const[mailboxes,meta]=awaitjam.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
.
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.
Jam provides types for JMAP methods, arguments, and responses as described in theJMAP andJMAP Mail RFCs.
Allconvenience methods,request
, andrequestMany
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 asproperties
to filter fields in a response, the response type will be narrowed to exclude fields that were not included.
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 |
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 |
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.
importJamClientfrom"jmap-jam";constjam=newJamClient({bearerToken:"<bearer-token>",sessionUrl:"<server-session-url>",});
A client instance requires both abearerToken
andsessionUrl
in order to make authenticated requests.
Upon constructing a client, Jam will immediately dispatch a request for asession from the server. This session will be used for all subsequent requests.
A convenience pattern for making individual JMAP requests that uses therequest
method under the hood.
const[mailboxes]=awaitjam.api.Mailbox.get({ accountId,});const[emails]=awaitjam.api.Email.get({ accountId,ids:["email-123"],properties:["subject"],});
Send a standard JMAP request.
const[mailboxes]=awaitjam.request(["Mailbox/get",{ accountId}]);const[emails]=awaitjam.request(["Email/get",{ accountId,ids:["email-123"],properties:["subject"],},]);
Send a JMAP request with multiple method calls.
const[{ emailIds, emails}]=awaitjam.requestMany((r)=>{constemailIds=r.Email.query({ accountId,filter:{inMailbox:mailboxId,},});constemails=r.Email.get({ accountId,ids:emailIds.$ref("/ids"),properties:["id","htmlBody"],});return{ emailIds, emails};});
Each item created within arequestMany
callback is an instance ofInvocationDraft
. Internally, it keeps track of the invocation that was defined for use when the request is finalized and sent.
The important part ofInvocationDraft
is that each draft exposes a method$ref
that can be used to create aresult reference between invocations.
To create a result reference, call$ref
with a JSON pointer at the field that will receive the reference.
TheemailIds.$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" ] ]}
Get the client's current session.
constsession=awaitjam.session;
Get the ID of the primary mail account for the current session.
constaccountId=awaitjam.getPrimaryAccount();
Initiate a fetch request to upload a blob.
constdata=awaitjam.uploadBlob(accountId,newBlob(["hello world"],{type:"text/plain"}));console.log(data);// =>// {// accountId: "account-abcd",// blobId: "blob-123",// type: "text/plain",// size: 152,// }
Intiate a fetch request to download a specific blob. Downloading a blob requires both aMIME type and file name, since JMAP server implementations are not required to store this information.
If the JMAP server sets aContent-Type
header in its response, it will use the value provided inmimeType
.
If the JMAP server sets aContent-Disposition
header in its response, it will use the value provided infileName
.
constresponse=awaitjam.downloadBlob({ accountId,blobId:'blob-123'mimeType:'image/png'fileName:'photo.png'});constblob=awaitresponse.blob();// or response.arrayBuffer()// or response.text()// ...etc
Connect to a JMAP event source usingServer-Sent Events.
constsse=awaitjam.connectEventSource({types:"*",// or ["Mailbox", "Email", ...]ping:5000,// ping interval in millisecondscloseafter:"no",// or "state"});sse.addEventListener("message",(event)=> ...));sse.addEventListener("error",(event)=> ...));sse.addEventListener("close",(event)=> ...));
About
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.