@push.rocks/smartjmap 1.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/ts/index.ts CHANGED
@@ -1,2 +1,4 @@
1
1
  export * from './classes.jmapclient.js';
2
+ export * from './interfaces.backend.js';
3
+ export * from './classes.memorymailbackend.js';
2
4
  export * from './classes.jmapserver.js';
@@ -0,0 +1,413 @@
1
+ import type {
2
+ IJmapEmailAddress,
3
+ IJmapEmailBodyPart,
4
+ IJmapEmailBodyValue,
5
+ } from './classes.jmapclient.js';
6
+
7
+ /**
8
+ * The authenticated principal a JmapServer `authenticate` hook resolves from an
9
+ * incoming Request. The backend maps a principal to a JMAP account via
10
+ * `resolveAccount`.
11
+ */
12
+ export type TJmapPrincipal = {
13
+ /** Stable identifier of the authenticated user, e.g. a username or token subject. */
14
+ username: string;
15
+ };
16
+
17
+ /** Account metadata returned by `IJmapMailBackend.resolveAccount`. */
18
+ export interface IJmapAccountInfo {
19
+ /** The JMAP accountId used in method calls and URL templates. */
20
+ accountId: string;
21
+ /** Display name of the account, shown in the session object. */
22
+ name: string;
23
+ /** Defaults to true when omitted. */
24
+ isPersonal?: boolean;
25
+ /** Defaults to false when omitted. */
26
+ isReadOnly?: boolean;
27
+ }
28
+
29
+ /** A Mailbox record (RFC 8621 §2) as stored/served by a backend. */
30
+ export interface IJmapMailboxRecord {
31
+ id: string;
32
+ name: string;
33
+ role: string | null;
34
+ parentId: string | null;
35
+ sortOrder: number;
36
+ totalEmails: number;
37
+ unreadEmails: number;
38
+ totalThreads: number;
39
+ unreadThreads: number;
40
+ /** Defaults to true when omitted. */
41
+ isSubscribed?: boolean;
42
+ /** Defaults to an all-permissive rights object when omitted. */
43
+ myRights?: Record<string, boolean>;
44
+ }
45
+
46
+ /**
47
+ * An Email record (RFC 8621 §4) as stored/served by a backend — the full
48
+ * superset of properties the server can project from. `bodyValues` must hold
49
+ * the complete body values; the server applies the Email/get fetch flags and
50
+ * `maxBodyValueBytes` truncation on top.
51
+ */
52
+ export interface IJmapEmailRecord {
53
+ id: string;
54
+ blobId: string;
55
+ threadId: string;
56
+ mailboxIds: Record<string, boolean>;
57
+ keywords: Record<string, boolean>;
58
+ size: number;
59
+ receivedAt: string;
60
+ sentAt: string | null;
61
+ messageId?: string[] | null;
62
+ inReplyTo?: string[] | null;
63
+ references?: string[] | null;
64
+ sender?: IJmapEmailAddress[] | null;
65
+ subject: string | null;
66
+ from: IJmapEmailAddress[] | null;
67
+ to: IJmapEmailAddress[] | null;
68
+ cc: IJmapEmailAddress[] | null;
69
+ bcc: IJmapEmailAddress[] | null;
70
+ replyTo: IJmapEmailAddress[] | null;
71
+ hasAttachment: boolean;
72
+ preview: string;
73
+ bodyValues: Record<string, IJmapEmailBodyValue>;
74
+ textBody: IJmapEmailBodyPart[];
75
+ htmlBody: IJmapEmailBodyPart[];
76
+ attachments: IJmapEmailBodyPart[];
77
+ }
78
+
79
+ /** A Thread record (RFC 8621 §3): emailIds sorted oldest first. */
80
+ export interface IJmapThreadRecord {
81
+ id: string;
82
+ emailIds: string[];
83
+ }
84
+
85
+ /** A stored blob: raw bytes plus the content type given at upload/creation. */
86
+ export interface IJmapBlobRecord {
87
+ data: Uint8Array;
88
+ type: string;
89
+ name?: string;
90
+ }
91
+
92
+ /** An Identity record (RFC 8621 §6). */
93
+ export interface IJmapIdentityRecord {
94
+ id: string;
95
+ name: string;
96
+ email: string;
97
+ mayDelete: boolean;
98
+ }
99
+
100
+ /** An EmailSubmission record (RFC 8621 §7) as returned by `submitEmail`. */
101
+ export interface IJmapSubmissionRecord {
102
+ id: string;
103
+ emailId: string;
104
+ identityId: string;
105
+ undoStatus: string;
106
+ sendAt: string;
107
+ }
108
+
109
+ /** SMTP-style envelope handed to the backend on submission (RFC 8621 §7). */
110
+ export interface IJmapEnvelope {
111
+ mailFrom: { email: string };
112
+ rcptTo: Array<{ email: string }>;
113
+ }
114
+
115
+ /** Everything the backend needs to actually send a message. */
116
+ export interface IJmapSubmissionInput {
117
+ emailId: string;
118
+ identityId: string;
119
+ /** Envelope from the client, or derived by the server from the email's addresses. */
120
+ envelope: IJmapEnvelope;
121
+ /** The raw RFC 5322 payload (the email's blob) — the backend performs the actual sending. */
122
+ message: Uint8Array;
123
+ }
124
+
125
+ /**
126
+ * Authenticated, admitted upload handed to a streaming backend. The backend
127
+ * must reserve quota before reading `stream`, consume it through EOF, and
128
+ * abort its storage operation when `signal` fires.
129
+ */
130
+ export interface IJmapBlobUploadStream {
131
+ stream: ReadableStream<Uint8Array>;
132
+ size: number;
133
+ type: string;
134
+ signal: AbortSignal;
135
+ }
136
+
137
+ /** Standard result shape for the /get-style backend reads. */
138
+ export interface IJmapGetResult<T> {
139
+ /** Backend-owned opaque state string for the collection at read time. */
140
+ state: string;
141
+ list: T[];
142
+ notFound: string[];
143
+ }
144
+
145
+ /** Result shape for the /changes-style backend reads (RFC 8620 §5.2). */
146
+ export interface IJmapChanges {
147
+ oldState: string;
148
+ newState: string;
149
+ hasMoreChanges: boolean;
150
+ created: string[];
151
+ updated: string[];
152
+ destroyed: string[];
153
+ }
154
+
155
+ /** The Email/query filter subset a backend must support (RFC 8621 §4.4.1). */
156
+ export interface IJmapEmailFilter {
157
+ inMailbox?: string;
158
+ /** Case-insensitive match in subject, body text, or any address field. */
159
+ text?: string;
160
+ subject?: string;
161
+ from?: string;
162
+ to?: string;
163
+ /** receivedAt strictly before this UTC date. */
164
+ before?: string;
165
+ /** receivedAt at or after this UTC date. */
166
+ after?: string;
167
+ hasKeyword?: string;
168
+ notKeyword?: string;
169
+ }
170
+
171
+ /** A query sort comparator; the server only forwards supported properties. */
172
+ export interface IJmapComparator {
173
+ property: string;
174
+ isAscending?: boolean;
175
+ }
176
+
177
+ /** Options passed to `queryEmails`; the server has already validated them. */
178
+ export interface IJmapEmailQueryOptions {
179
+ filter?: IJmapEmailFilter | null;
180
+ /** Only `receivedAt` comparators are forwarded by the server. */
181
+ sort?: IJmapComparator[];
182
+ /** Zero-based offset into the full result list. */
183
+ position?: number;
184
+ limit?: number | null;
185
+ /** When true the result must include `total`. */
186
+ calculateTotal?: boolean;
187
+ }
188
+
189
+ export interface IJmapEmailQueryResult {
190
+ queryState: string;
191
+ ids: string[];
192
+ position: number;
193
+ /** Present when `calculateTotal` was requested. */
194
+ total?: number;
195
+ }
196
+
197
+ /** An Email creation spec (server-validated Email/set create arguments). */
198
+ export interface IJmapEmailCreate {
199
+ mailboxIds: Record<string, boolean>;
200
+ keywords?: Record<string, boolean>;
201
+ from?: IJmapEmailAddress[] | null;
202
+ to?: IJmapEmailAddress[] | null;
203
+ cc?: IJmapEmailAddress[] | null;
204
+ bcc?: IJmapEmailAddress[] | null;
205
+ replyTo?: IJmapEmailAddress[] | null;
206
+ sender?: IJmapEmailAddress[] | null;
207
+ messageId?: string[] | null;
208
+ inReplyTo?: string[] | null;
209
+ references?: string[] | null;
210
+ subject?: string | null;
211
+ sentAt?: string | null;
212
+ receivedAt?: string;
213
+ bodyValues?: Record<string, { value: string }>;
214
+ textBody?: IJmapEmailBodyPart[];
215
+ htmlBody?: IJmapEmailBodyPart[];
216
+ /** Attachment body parts referencing previously uploaded blobs via `blobId`. */
217
+ attachments?: IJmapEmailBodyPart[];
218
+ }
219
+
220
+ /**
221
+ * An Email update, normalized by the server from RFC 8620 §5.3 patches:
222
+ * full replacements and per-key patches never overlap for the same property
223
+ * (the server rejects such patches before calling the backend).
224
+ */
225
+ export interface IJmapEmailUpdate {
226
+ /** Full replacement of the keywords object. */
227
+ keywords?: Record<string, boolean>;
228
+ /** Full replacement of the mailboxIds object. */
229
+ mailboxIds?: Record<string, boolean>;
230
+ /** Per-keyword patch: true adds, null removes. */
231
+ keywordsPatch?: Record<string, true | null>;
232
+ /** Per-mailbox patch: true adds, null removes. */
233
+ mailboxIdsPatch?: Record<string, true | null>;
234
+ }
235
+
236
+ /** A JMAP SetError (RFC 8620 §5.3). */
237
+ export interface IJmapSetError {
238
+ type: string;
239
+ description?: string;
240
+ properties?: string[];
241
+ }
242
+
243
+ /** The batch handed to `setEmails`; entries are already server-validated. */
244
+ export interface IJmapEmailSetRequest {
245
+ /** Keyed by client creation id. */
246
+ create?: Record<string, IJmapEmailCreate>;
247
+ /** Keyed by email id. */
248
+ update?: Record<string, IJmapEmailUpdate>;
249
+ destroy?: string[];
250
+ }
251
+
252
+ /** The outcome of a `setEmails` batch; `created` carries the full new records. */
253
+ export interface IJmapEmailSetResult {
254
+ oldState: string;
255
+ newState: string;
256
+ created: Record<string, IJmapEmailRecord>;
257
+ notCreated: Record<string, IJmapSetError>;
258
+ updated: string[];
259
+ notUpdated: Record<string, IJmapSetError>;
260
+ destroyed: string[];
261
+ notDestroyed: Record<string, IJmapSetError>;
262
+ }
263
+
264
+ /**
265
+ * A change notification the backend emits when data mutates — both for
266
+ * mutations performed through the JMAP server and for out-of-band mutations
267
+ * (e.g. new mail arriving via an IMAP bridge). `changed` maps JMAP type names
268
+ * ('Email', 'Mailbox', 'Thread', ...) to the new opaque state string.
269
+ */
270
+ export interface IJmapStateChange {
271
+ accountId: string;
272
+ changed: Record<string, string>;
273
+ }
274
+
275
+ export type TJmapChangeListener = (change: IJmapStateChange) => void;
276
+
277
+ /**
278
+ * Error a backend may throw from set-style operations to control the JMAP
279
+ * SetError `type` the server reports (e.g. 'notFound', 'invalidProperties').
280
+ * Any other thrown error is mapped to a generic SetError.
281
+ */
282
+ export class JmapBackendError extends Error {
283
+ constructor(public type: string, description?: string) {
284
+ super(description ?? type);
285
+ this.name = 'JmapBackendError';
286
+ }
287
+ }
288
+
289
+ /**
290
+ * The storage contract `JmapServer` dispatches into. Implement this against a
291
+ * real store (SQL, MongoDB, ...) to expose it as a JMAP server; the bundled
292
+ * `MemoryMailBackend` is the in-memory reference implementation.
293
+ *
294
+ * All state strings are backend-owned opaque strings: the server never
295
+ * interprets them beyond equality comparison and echoing them to clients.
296
+ */
297
+ export interface IJmapMailBackend {
298
+ /**
299
+ * Maps an authenticated principal to its JMAP account, or null when the
300
+ * principal has no account (the server then responds 403).
301
+ */
302
+ resolveAccount(principal: TJmapPrincipal): Promise<IJmapAccountInfo | null>;
303
+
304
+ /**
305
+ * Returns mailboxes by id (`ids: null` returns all mailboxes of the
306
+ * account) together with the current Mailbox collection state. Counter
307
+ * fields (totalEmails, unreadEmails, ...) must be accurate at read time.
308
+ */
309
+ getMailboxes(
310
+ accountId: string,
311
+ ids: string[] | null
312
+ ): Promise<IJmapGetResult<IJmapMailboxRecord>>;
313
+
314
+ /**
315
+ * Returns mailbox changes since the given state, or null when the state is
316
+ * unknown/too old (the server then answers `cannotCalculateChanges`).
317
+ * When `maxChanges` is given, the result may be partial with
318
+ * `hasMoreChanges: true` and an intermediate `newState` to resume from.
319
+ */
320
+ getMailboxChanges(
321
+ accountId: string,
322
+ sinceState: string,
323
+ maxChanges?: number
324
+ ): Promise<IJmapChanges | null>;
325
+
326
+ /**
327
+ * Returns emails by id together with the current Email collection state.
328
+ * `properties` is a projection hint (null/undefined = all): backends may
329
+ * use it to avoid loading bodies, but returning full records is always
330
+ * correct — the server applies the final projection itself.
331
+ */
332
+ getEmails(
333
+ accountId: string,
334
+ ids: string[],
335
+ properties?: string[] | null
336
+ ): Promise<IJmapGetResult<IJmapEmailRecord>>;
337
+
338
+ /**
339
+ * Runs an Email query: apply the filter, sort, then slice by
340
+ * position/limit. `total` must be included when `calculateTotal` is set.
341
+ */
342
+ queryEmails(
343
+ accountId: string,
344
+ options: IJmapEmailQueryOptions
345
+ ): Promise<IJmapEmailQueryResult>;
346
+
347
+ /**
348
+ * Returns email changes since the given state, or null when the state is
349
+ * unknown/too old (the server then answers `cannotCalculateChanges`).
350
+ */
351
+ getEmailChanges(
352
+ accountId: string,
353
+ sinceState: string,
354
+ maxChanges?: number
355
+ ): Promise<IJmapChanges | null>;
356
+
357
+ /**
358
+ * Applies an Email/set batch: creates (assigning ids, blobIds, and thread
359
+ * ids), updates (keyword/mailbox replacements and patches), and destroys.
360
+ * Per-item failures go into the not* maps; the whole batch must never
361
+ * throw for item-level problems. Must bump the Email state exactly when
362
+ * something changed and report old/new state.
363
+ */
364
+ setEmails(
365
+ accountId: string,
366
+ request: IJmapEmailSetRequest
367
+ ): Promise<IJmapEmailSetResult>;
368
+
369
+ /**
370
+ * Returns threads by id (each thread lists its emailIds oldest first)
371
+ * together with the current Thread collection state.
372
+ */
373
+ getThreads(accountId: string, ids: string[]): Promise<IJmapGetResult<IJmapThreadRecord>>;
374
+
375
+ /** Stores an uploaded blob and returns its id and byte size (RFC 8620 §6.1). */
376
+ uploadBlob(
377
+ accountId: string,
378
+ data: Uint8Array,
379
+ type: string
380
+ ): Promise<{ blobId: string; size: number }>;
381
+
382
+ /**
383
+ * Optional streaming upload path. When present, JmapServer requires a valid
384
+ * Content-Length and never buffers the request before invoking this method.
385
+ */
386
+ uploadBlobStream?(
387
+ accountId: string,
388
+ upload: IJmapBlobUploadStream
389
+ ): Promise<{ blobId: string; size: number }>;
390
+
391
+ /** Returns a blob's bytes and metadata, or null when unknown. */
392
+ getBlob(accountId: string, blobId: string): Promise<IJmapBlobRecord | null>;
393
+
394
+ /** Returns the sending identities of the account with their collection state. */
395
+ getIdentities(accountId: string): Promise<IJmapGetResult<IJmapIdentityRecord>>;
396
+
397
+ /**
398
+ * Accepts an email for sending: the server has already resolved the
399
+ * envelope and fetched the raw RFC 5322 payload — the backend performs (or
400
+ * queues) the actual delivery and returns the submission record.
401
+ */
402
+ submitEmail(
403
+ accountId: string,
404
+ submission: IJmapSubmissionInput
405
+ ): Promise<IJmapSubmissionRecord>;
406
+
407
+ /**
408
+ * Subscribes to change notifications so the server can emit StateChange
409
+ * pushes — including for mutations that happen outside a JMAP request.
410
+ * Returns an unsubscribe function; the server always calls it on teardown.
411
+ */
412
+ subscribeToChanges(listener: TJmapChangeListener): () => void;
413
+ }