@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/.smartconfig.json +5 -2
- package/dist_ts/00_commitinfo_data.js +3 -3
- package/dist_ts/classes.jmapclient.d.ts +43 -13
- package/dist_ts/classes.jmapclient.js +326 -149
- package/dist_ts/classes.jmapserver.d.ts +154 -84
- package/dist_ts/classes.jmapserver.js +1788 -660
- package/dist_ts/classes.memorymailbackend.d.ts +109 -0
- package/dist_ts/classes.memorymailbackend.js +670 -0
- package/dist_ts/index.d.ts +2 -0
- package/dist_ts/index.js +3 -1
- package/dist_ts/interfaces.backend.d.ts +349 -0
- package/dist_ts/interfaces.backend.js +13 -0
- package/npmextra.json +4 -2
- package/package.json +8 -7
- package/readme.hints.md +25 -14
- package/readme.md +149 -18
- package/ts/00_commitinfo_data.ts +2 -2
- package/ts/classes.jmapclient.ts +414 -153
- package/ts/classes.jmapserver.ts +2389 -761
- package/ts/classes.memorymailbackend.ts +850 -0
- package/ts/index.ts +2 -0
- package/ts/interfaces.backend.ts +413 -0
package/ts/index.ts
CHANGED
|
@@ -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
|
+
}
|