@push.rocks/smartjmap 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.smartconfig.json +1 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.jmapclient.d.ts +22 -13
- package/dist_ts/classes.jmapclient.js +294 -153
- package/dist_ts/classes.jmapserver.d.ts +49 -4
- package/dist_ts/classes.jmapserver.js +852 -330
- package/dist_ts/interfaces.backend.d.ts +19 -0
- package/dist_ts/interfaces.backend.js +1 -1
- package/package.json +5 -6
- package/readme.md +36 -5
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.jmapclient.ts +360 -157
- package/ts/classes.jmapserver.ts +1271 -439
- package/ts/interfaces.backend.ts +21 -0
|
@@ -111,6 +111,17 @@ export interface IJmapSubmissionInput {
|
|
|
111
111
|
/** The raw RFC 5322 payload (the email's blob) — the backend performs the actual sending. */
|
|
112
112
|
message: Uint8Array;
|
|
113
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Authenticated, admitted upload handed to a streaming backend. The backend
|
|
116
|
+
* must reserve quota before reading `stream`, consume it through EOF, and
|
|
117
|
+
* abort its storage operation when `signal` fires.
|
|
118
|
+
*/
|
|
119
|
+
export interface IJmapBlobUploadStream {
|
|
120
|
+
stream: ReadableStream<Uint8Array>;
|
|
121
|
+
size: number;
|
|
122
|
+
type: string;
|
|
123
|
+
signal: AbortSignal;
|
|
124
|
+
}
|
|
114
125
|
/** Standard result shape for the /get-style backend reads. */
|
|
115
126
|
export interface IJmapGetResult<T> {
|
|
116
127
|
/** Backend-owned opaque state string for the collection at read time. */
|
|
@@ -311,6 +322,14 @@ export interface IJmapMailBackend {
|
|
|
311
322
|
blobId: string;
|
|
312
323
|
size: number;
|
|
313
324
|
}>;
|
|
325
|
+
/**
|
|
326
|
+
* Optional streaming upload path. When present, JmapServer requires a valid
|
|
327
|
+
* Content-Length and never buffers the request before invoking this method.
|
|
328
|
+
*/
|
|
329
|
+
uploadBlobStream?(accountId: string, upload: IJmapBlobUploadStream): Promise<{
|
|
330
|
+
blobId: string;
|
|
331
|
+
size: number;
|
|
332
|
+
}>;
|
|
314
333
|
/** Returns a blob's bytes and metadata, or null when unknown. */
|
|
315
334
|
getBlob(accountId: string, blobId: string): Promise<IJmapBlobRecord | null>;
|
|
316
335
|
/** Returns the sending identities of the account with their collection state. */
|
|
@@ -10,4 +10,4 @@ export class JmapBackendError extends Error {
|
|
|
10
10
|
this.name = 'JmapBackendError';
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
13
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW50ZXJmYWNlcy5iYWNrZW5kLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvaW50ZXJmYWNlcy5iYWNrZW5kLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQW9SQTs7OztHQUlHO0FBQ0gsTUFBTSxPQUFPLGdCQUFpQixTQUFRLEtBQUs7SUFDekMsWUFBbUIsSUFBWSxFQUFFLFdBQW9CO1FBQ25ELEtBQUssQ0FBQyxXQUFXLElBQUksSUFBSSxDQUFDLENBQUM7UUFEVixTQUFJLEdBQUosSUFBSSxDQUFRO1FBRTdCLElBQUksQ0FBQyxJQUFJLEdBQUcsa0JBQWtCLENBQUM7SUFDakMsQ0FBQztDQUNGIn0=
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@push.rocks/smartjmap",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A TypeScript JMAP toolkit (RFC 8620/8621): an event-driven mail client for reading, sending, and watching mail, plus an embeddable JMAP server core with pluggable storage.",
|
|
6
6
|
"exports": {
|
|
@@ -12,12 +12,11 @@
|
|
|
12
12
|
"author": "Task Venture Capital GmbH <hello@task.vc>",
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"devDependencies": {
|
|
15
|
-
"@git.zone/tsbuild": "^4.4.
|
|
16
|
-
"@git.zone/tsrun": "^2.0.
|
|
17
|
-
"@git.zone/tstest": "^3.6.
|
|
18
|
-
"@types/node": "^
|
|
15
|
+
"@git.zone/tsbuild": "^4.4.2",
|
|
16
|
+
"@git.zone/tsrun": "^2.0.5",
|
|
17
|
+
"@git.zone/tstest": "^3.6.6",
|
|
18
|
+
"@types/node": "^26.1.1"
|
|
19
19
|
},
|
|
20
|
-
"dependencies": {},
|
|
21
20
|
"repository": {
|
|
22
21
|
"type": "git",
|
|
23
22
|
"url": "git+https://code.foss.global/push.rocks/smartjmap.git"
|
package/readme.md
CHANGED
|
@@ -4,10 +4,10 @@ A TypeScript JMAP toolkit (RFC 8620/8621): an event-driven mail client for readi
|
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Install `@push.rocks/smartjmap` with pnpm:
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
|
-
pnpm
|
|
10
|
+
pnpm add @push.rocks/smartjmap
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
The package has zero runtime dependencies — it uses the global `fetch`, `Request`/`Response`, and `ReadableStream` available in Node.js 18+ (and in Deno/Bun for the server core).
|
|
@@ -144,8 +144,16 @@ const message = await jmapClient.getEmail(messages[0].id);
|
|
|
144
144
|
// Convenience: set the $seen keyword
|
|
145
145
|
await jmapClient.markSeen(message.id);
|
|
146
146
|
|
|
147
|
+
// Mark unread, toggle flagged, move, or permanently destroy
|
|
148
|
+
await jmapClient.markUnseen(message.id);
|
|
149
|
+
await jmapClient.setFlagged(message.id, true);
|
|
150
|
+
await jmapClient.setMailboxes(message.id, ['archive-mailbox-id']);
|
|
151
|
+
|
|
147
152
|
// Replace the full keywords object
|
|
148
153
|
await jmapClient.setKeywords(message.id, { $seen: true, $flagged: true });
|
|
154
|
+
|
|
155
|
+
// Permanently destroy the email after any other mutations are complete
|
|
156
|
+
await jmapClient.destroyEmail(message.id);
|
|
149
157
|
```
|
|
150
158
|
|
|
151
159
|
### Sending Mail
|
|
@@ -220,6 +228,17 @@ await jmapClient.disconnect();
|
|
|
220
228
|
|
|
221
229
|
`JmapServer` handles the protocol; storage is behind the `IJmapMailBackend` interface. Endpoints served: the session resource (`/.well-known/jmap` and `/jmap/session`), the API (`/jmap/api`), uploads (`/jmap/upload/{accountId}`), downloads (`/jmap/download/{accountId}/{blobId}/{name}?type=`), and StateChange pushes via SSE (`/jmap/eventsource`, RFC 8620 §7.3 with `types`, `closeafter=state`, and `ping` keepalive support).
|
|
222
230
|
|
|
231
|
+
Production consumers that rely on bounded streaming uploads can verify the
|
|
232
|
+
server surface before starting a listener:
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
import { JMAP_SERVER_STREAMING_API_VERSION } from '@push.rocks/smartjmap';
|
|
236
|
+
|
|
237
|
+
if (JMAP_SERVER_STREAMING_API_VERSION !== 1) {
|
|
238
|
+
throw new Error('This application requires smartjmap streaming server API v1.');
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
223
242
|
### The Offline Test Server
|
|
224
243
|
|
|
225
244
|
With no options, `JmapServer` uses a fresh `MemoryMailBackend` plus a static credential registry — an offline JMAP server for tests. Seed users on the server (auth) and mail on the backend (storage):
|
|
@@ -287,13 +306,19 @@ The `authenticate` option replaces the built-in Basic/Bearer registry entirely.
|
|
|
287
306
|
import { JmapServer } from '@push.rocks/smartjmap';
|
|
288
307
|
|
|
289
308
|
const jmapServer = new JmapServer({
|
|
309
|
+
authenticationChallenges: ['Bearer realm="jmap"'],
|
|
290
310
|
authenticate: async (request: Request) => {
|
|
291
|
-
const
|
|
311
|
+
const match = request.headers.get('authorization')?.match(/^Bearer\s+(.+)$/i);
|
|
312
|
+
const token = match?.[1];
|
|
292
313
|
return token === 'sesame' ? { username: 'appuser' } : null;
|
|
293
314
|
},
|
|
294
315
|
});
|
|
295
316
|
```
|
|
296
317
|
|
|
318
|
+
Set `authenticationChallenges` to the exact schemes accepted by a custom
|
|
319
|
+
authenticator. The built-in registry advertises both Bearer and Basic by
|
|
320
|
+
default.
|
|
321
|
+
|
|
297
322
|
### Implementing a Real Backend
|
|
298
323
|
|
|
299
324
|
Implement `IJmapMailBackend` against your own store to serve real mail. All state strings are backend-owned opaque strings. The contract (all methods async except `subscribeToChanges`, which returns its unsubscribe function synchronously):
|
|
@@ -307,7 +332,7 @@ Implement `IJmapMailBackend` against your own store to serve real mail. All stat
|
|
|
307
332
|
| `getEmailChanges(accountId, sinceState, maxChanges?)` | `Email/changes` (return null for `cannotCalculateChanges`) |
|
|
308
333
|
| `setEmails(accountId, request)` | `Email/set` (creates, normalized keyword/mailbox updates, destroys) |
|
|
309
334
|
| `getThreads(accountId, ids)` | `Thread/get` |
|
|
310
|
-
| `uploadBlob(accountId, data, type)` / `getBlob(accountId, blobId)` | upload
|
|
335
|
+
| `uploadBlob(accountId, data, type)` / optional `uploadBlobStream(accountId, upload)` / `getBlob(accountId, blobId)` | buffered or streaming upload, download endpoints, and raw-message access |
|
|
311
336
|
| `getIdentities(accountId)` | `Identity/get` |
|
|
312
337
|
| `submitEmail(accountId, submission)` | `EmailSubmission/set` — receives the resolved envelope plus the raw RFC 5322 payload; your backend performs the actual sending |
|
|
313
338
|
| `subscribeToChanges(listener)` | StateChange pushes — the server's only push feed, so notify on all mutations, including those made through JMAP requests, not just out-of-band changes |
|
|
@@ -325,9 +350,15 @@ const jmapServer = new JmapServer({
|
|
|
325
350
|
|
|
326
351
|
`MemoryMailBackend` is the reference implementation — a readable starting point for the expected semantics.
|
|
327
352
|
|
|
353
|
+
When `uploadBlobStream` is implemented, the server authenticates and admits the
|
|
354
|
+
request before reading, requires `Content-Length`, and passes `stream`, exact
|
|
355
|
+
`size`, media `type`, and an abort `signal`. The backend must consume exactly
|
|
356
|
+
`size` bytes, reserve quota before reading the stream, reject short or oversized
|
|
357
|
+
input, honor cancellation, and remove partial storage on failure.
|
|
358
|
+
|
|
328
359
|
### What the Server Advertises (and What It Doesn't)
|
|
329
360
|
|
|
330
|
-
The session object advertises `urn:ietf:params:jmap:core`, `urn:ietf:params:jmap:mail`, and `urn:ietf:params:jmap:submission`. The core limits are enforced, not just advertised (exported as `JMAP_SERVER_LIMITS`): `maxSizeRequest` 10 MB, `maxCallsInRequest` 16, `maxObjectsInGet`/`maxObjectsInSet` 500, `maxSizeUpload` 50 MB, and `
|
|
361
|
+
The session object advertises `urn:ietf:params:jmap:core`, `urn:ietf:params:jmap:mail`, and `urn:ietf:params:jmap:submission`. The core limits are enforced, not just advertised (exported as `JMAP_SERVER_LIMITS`): `maxSizeRequest` 10 MB, `maxCallsInRequest` 16, `maxObjectsInGet`/`maxObjectsInSet` 500, `maxSizeUpload` 50 MB, `maxConcurrentRequests` 4, and per-principal `maxConcurrentUpload` 4 — violations produce request-level `limit` errors, `requestTooLarge` method errors, or upload problem details. Constructor `limits` override advertised limits; `maxConcurrentUploadServer` adds a process-wide upload ceiling, and `mapUploadError` maps storage failures to application-specific RFC 7807 responses. `maxDelayedSend` is 0 (no delayed send) and `mayCreateTopLevelMailbox` is false (no `Mailbox/set`).
|
|
331
362
|
|
|
332
363
|
The request layer implements `using` validation (`unknownCapability`), `#`-prefixed back-references with full `ResultReference` `{ resultOf, name, path }` JSON-pointer resolution including `/*` array expansion, client-provided `createdIds` maps, and `unknownMethod`/`invalidArguments`/`serverFail` error responses.
|
|
333
364
|
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@push.rocks/smartjmap',
|
|
6
|
-
version: '2.
|
|
6
|
+
version: '2.1.0',
|
|
7
7
|
description: 'A TypeScript JMAP toolkit (RFC 8620/8621): an event-driven mail client for reading, sending, and watching mail, plus an embeddable JMAP server core with pluggable storage.'
|
|
8
8
|
}
|