@push.rocks/smartjmap 1.0.0 → 2.0.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 +4 -2
- package/dist_ts/00_commitinfo_data.js +3 -3
- package/dist_ts/classes.jmapclient.d.ts +21 -0
- package/dist_ts/classes.jmapclient.js +37 -1
- package/dist_ts/classes.jmapserver.d.ts +111 -86
- package/dist_ts/classes.jmapserver.js +1250 -644
- 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 +330 -0
- package/dist_ts/interfaces.backend.js +13 -0
- package/npmextra.json +4 -2
- package/package.json +4 -2
- package/readme.hints.md +25 -14
- package/readme.md +116 -16
- package/ts/00_commitinfo_data.ts +2 -2
- package/ts/classes.jmapclient.ts +58 -0
- package/ts/classes.jmapserver.ts +1548 -752
- package/ts/classes.memorymailbackend.ts +850 -0
- package/ts/index.ts +2 -0
- package/ts/interfaces.backend.ts +392 -0
package/dist_ts/index.d.ts
CHANGED
package/dist_ts/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
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';
|
|
3
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
5
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxjQUFjLHlCQUF5QixDQUFDO0FBQ3hDLGNBQWMseUJBQXlCLENBQUM7QUFDeEMsY0FBYyxnQ0FBZ0MsQ0FBQztBQUMvQyxjQUFjLHlCQUF5QixDQUFDIn0=
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import type { IJmapEmailAddress, IJmapEmailBodyPart, IJmapEmailBodyValue } from './classes.jmapclient.js';
|
|
2
|
+
/**
|
|
3
|
+
* The authenticated principal a JmapServer `authenticate` hook resolves from an
|
|
4
|
+
* incoming Request. The backend maps a principal to a JMAP account via
|
|
5
|
+
* `resolveAccount`.
|
|
6
|
+
*/
|
|
7
|
+
export type TJmapPrincipal = {
|
|
8
|
+
/** Stable identifier of the authenticated user, e.g. a username or token subject. */
|
|
9
|
+
username: string;
|
|
10
|
+
};
|
|
11
|
+
/** Account metadata returned by `IJmapMailBackend.resolveAccount`. */
|
|
12
|
+
export interface IJmapAccountInfo {
|
|
13
|
+
/** The JMAP accountId used in method calls and URL templates. */
|
|
14
|
+
accountId: string;
|
|
15
|
+
/** Display name of the account, shown in the session object. */
|
|
16
|
+
name: string;
|
|
17
|
+
/** Defaults to true when omitted. */
|
|
18
|
+
isPersonal?: boolean;
|
|
19
|
+
/** Defaults to false when omitted. */
|
|
20
|
+
isReadOnly?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** A Mailbox record (RFC 8621 §2) as stored/served by a backend. */
|
|
23
|
+
export interface IJmapMailboxRecord {
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
role: string | null;
|
|
27
|
+
parentId: string | null;
|
|
28
|
+
sortOrder: number;
|
|
29
|
+
totalEmails: number;
|
|
30
|
+
unreadEmails: number;
|
|
31
|
+
totalThreads: number;
|
|
32
|
+
unreadThreads: number;
|
|
33
|
+
/** Defaults to true when omitted. */
|
|
34
|
+
isSubscribed?: boolean;
|
|
35
|
+
/** Defaults to an all-permissive rights object when omitted. */
|
|
36
|
+
myRights?: Record<string, boolean>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* An Email record (RFC 8621 §4) as stored/served by a backend — the full
|
|
40
|
+
* superset of properties the server can project from. `bodyValues` must hold
|
|
41
|
+
* the complete body values; the server applies the Email/get fetch flags and
|
|
42
|
+
* `maxBodyValueBytes` truncation on top.
|
|
43
|
+
*/
|
|
44
|
+
export interface IJmapEmailRecord {
|
|
45
|
+
id: string;
|
|
46
|
+
blobId: string;
|
|
47
|
+
threadId: string;
|
|
48
|
+
mailboxIds: Record<string, boolean>;
|
|
49
|
+
keywords: Record<string, boolean>;
|
|
50
|
+
size: number;
|
|
51
|
+
receivedAt: string;
|
|
52
|
+
sentAt: string | null;
|
|
53
|
+
messageId?: string[] | null;
|
|
54
|
+
inReplyTo?: string[] | null;
|
|
55
|
+
references?: string[] | null;
|
|
56
|
+
sender?: IJmapEmailAddress[] | null;
|
|
57
|
+
subject: string | null;
|
|
58
|
+
from: IJmapEmailAddress[] | null;
|
|
59
|
+
to: IJmapEmailAddress[] | null;
|
|
60
|
+
cc: IJmapEmailAddress[] | null;
|
|
61
|
+
bcc: IJmapEmailAddress[] | null;
|
|
62
|
+
replyTo: IJmapEmailAddress[] | null;
|
|
63
|
+
hasAttachment: boolean;
|
|
64
|
+
preview: string;
|
|
65
|
+
bodyValues: Record<string, IJmapEmailBodyValue>;
|
|
66
|
+
textBody: IJmapEmailBodyPart[];
|
|
67
|
+
htmlBody: IJmapEmailBodyPart[];
|
|
68
|
+
attachments: IJmapEmailBodyPart[];
|
|
69
|
+
}
|
|
70
|
+
/** A Thread record (RFC 8621 §3): emailIds sorted oldest first. */
|
|
71
|
+
export interface IJmapThreadRecord {
|
|
72
|
+
id: string;
|
|
73
|
+
emailIds: string[];
|
|
74
|
+
}
|
|
75
|
+
/** A stored blob: raw bytes plus the content type given at upload/creation. */
|
|
76
|
+
export interface IJmapBlobRecord {
|
|
77
|
+
data: Uint8Array;
|
|
78
|
+
type: string;
|
|
79
|
+
name?: string;
|
|
80
|
+
}
|
|
81
|
+
/** An Identity record (RFC 8621 §6). */
|
|
82
|
+
export interface IJmapIdentityRecord {
|
|
83
|
+
id: string;
|
|
84
|
+
name: string;
|
|
85
|
+
email: string;
|
|
86
|
+
mayDelete: boolean;
|
|
87
|
+
}
|
|
88
|
+
/** An EmailSubmission record (RFC 8621 §7) as returned by `submitEmail`. */
|
|
89
|
+
export interface IJmapSubmissionRecord {
|
|
90
|
+
id: string;
|
|
91
|
+
emailId: string;
|
|
92
|
+
identityId: string;
|
|
93
|
+
undoStatus: string;
|
|
94
|
+
sendAt: string;
|
|
95
|
+
}
|
|
96
|
+
/** SMTP-style envelope handed to the backend on submission (RFC 8621 §7). */
|
|
97
|
+
export interface IJmapEnvelope {
|
|
98
|
+
mailFrom: {
|
|
99
|
+
email: string;
|
|
100
|
+
};
|
|
101
|
+
rcptTo: Array<{
|
|
102
|
+
email: string;
|
|
103
|
+
}>;
|
|
104
|
+
}
|
|
105
|
+
/** Everything the backend needs to actually send a message. */
|
|
106
|
+
export interface IJmapSubmissionInput {
|
|
107
|
+
emailId: string;
|
|
108
|
+
identityId: string;
|
|
109
|
+
/** Envelope from the client, or derived by the server from the email's addresses. */
|
|
110
|
+
envelope: IJmapEnvelope;
|
|
111
|
+
/** The raw RFC 5322 payload (the email's blob) — the backend performs the actual sending. */
|
|
112
|
+
message: Uint8Array;
|
|
113
|
+
}
|
|
114
|
+
/** Standard result shape for the /get-style backend reads. */
|
|
115
|
+
export interface IJmapGetResult<T> {
|
|
116
|
+
/** Backend-owned opaque state string for the collection at read time. */
|
|
117
|
+
state: string;
|
|
118
|
+
list: T[];
|
|
119
|
+
notFound: string[];
|
|
120
|
+
}
|
|
121
|
+
/** Result shape for the /changes-style backend reads (RFC 8620 §5.2). */
|
|
122
|
+
export interface IJmapChanges {
|
|
123
|
+
oldState: string;
|
|
124
|
+
newState: string;
|
|
125
|
+
hasMoreChanges: boolean;
|
|
126
|
+
created: string[];
|
|
127
|
+
updated: string[];
|
|
128
|
+
destroyed: string[];
|
|
129
|
+
}
|
|
130
|
+
/** The Email/query filter subset a backend must support (RFC 8621 §4.4.1). */
|
|
131
|
+
export interface IJmapEmailFilter {
|
|
132
|
+
inMailbox?: string;
|
|
133
|
+
/** Case-insensitive match in subject, body text, or any address field. */
|
|
134
|
+
text?: string;
|
|
135
|
+
subject?: string;
|
|
136
|
+
from?: string;
|
|
137
|
+
to?: string;
|
|
138
|
+
/** receivedAt strictly before this UTC date. */
|
|
139
|
+
before?: string;
|
|
140
|
+
/** receivedAt at or after this UTC date. */
|
|
141
|
+
after?: string;
|
|
142
|
+
hasKeyword?: string;
|
|
143
|
+
notKeyword?: string;
|
|
144
|
+
}
|
|
145
|
+
/** A query sort comparator; the server only forwards supported properties. */
|
|
146
|
+
export interface IJmapComparator {
|
|
147
|
+
property: string;
|
|
148
|
+
isAscending?: boolean;
|
|
149
|
+
}
|
|
150
|
+
/** Options passed to `queryEmails`; the server has already validated them. */
|
|
151
|
+
export interface IJmapEmailQueryOptions {
|
|
152
|
+
filter?: IJmapEmailFilter | null;
|
|
153
|
+
/** Only `receivedAt` comparators are forwarded by the server. */
|
|
154
|
+
sort?: IJmapComparator[];
|
|
155
|
+
/** Zero-based offset into the full result list. */
|
|
156
|
+
position?: number;
|
|
157
|
+
limit?: number | null;
|
|
158
|
+
/** When true the result must include `total`. */
|
|
159
|
+
calculateTotal?: boolean;
|
|
160
|
+
}
|
|
161
|
+
export interface IJmapEmailQueryResult {
|
|
162
|
+
queryState: string;
|
|
163
|
+
ids: string[];
|
|
164
|
+
position: number;
|
|
165
|
+
/** Present when `calculateTotal` was requested. */
|
|
166
|
+
total?: number;
|
|
167
|
+
}
|
|
168
|
+
/** An Email creation spec (server-validated Email/set create arguments). */
|
|
169
|
+
export interface IJmapEmailCreate {
|
|
170
|
+
mailboxIds: Record<string, boolean>;
|
|
171
|
+
keywords?: Record<string, boolean>;
|
|
172
|
+
from?: IJmapEmailAddress[] | null;
|
|
173
|
+
to?: IJmapEmailAddress[] | null;
|
|
174
|
+
cc?: IJmapEmailAddress[] | null;
|
|
175
|
+
bcc?: IJmapEmailAddress[] | null;
|
|
176
|
+
replyTo?: IJmapEmailAddress[] | null;
|
|
177
|
+
sender?: IJmapEmailAddress[] | null;
|
|
178
|
+
messageId?: string[] | null;
|
|
179
|
+
inReplyTo?: string[] | null;
|
|
180
|
+
references?: string[] | null;
|
|
181
|
+
subject?: string | null;
|
|
182
|
+
sentAt?: string | null;
|
|
183
|
+
receivedAt?: string;
|
|
184
|
+
bodyValues?: Record<string, {
|
|
185
|
+
value: string;
|
|
186
|
+
}>;
|
|
187
|
+
textBody?: IJmapEmailBodyPart[];
|
|
188
|
+
htmlBody?: IJmapEmailBodyPart[];
|
|
189
|
+
/** Attachment body parts referencing previously uploaded blobs via `blobId`. */
|
|
190
|
+
attachments?: IJmapEmailBodyPart[];
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* An Email update, normalized by the server from RFC 8620 §5.3 patches:
|
|
194
|
+
* full replacements and per-key patches never overlap for the same property
|
|
195
|
+
* (the server rejects such patches before calling the backend).
|
|
196
|
+
*/
|
|
197
|
+
export interface IJmapEmailUpdate {
|
|
198
|
+
/** Full replacement of the keywords object. */
|
|
199
|
+
keywords?: Record<string, boolean>;
|
|
200
|
+
/** Full replacement of the mailboxIds object. */
|
|
201
|
+
mailboxIds?: Record<string, boolean>;
|
|
202
|
+
/** Per-keyword patch: true adds, null removes. */
|
|
203
|
+
keywordsPatch?: Record<string, true | null>;
|
|
204
|
+
/** Per-mailbox patch: true adds, null removes. */
|
|
205
|
+
mailboxIdsPatch?: Record<string, true | null>;
|
|
206
|
+
}
|
|
207
|
+
/** A JMAP SetError (RFC 8620 §5.3). */
|
|
208
|
+
export interface IJmapSetError {
|
|
209
|
+
type: string;
|
|
210
|
+
description?: string;
|
|
211
|
+
properties?: string[];
|
|
212
|
+
}
|
|
213
|
+
/** The batch handed to `setEmails`; entries are already server-validated. */
|
|
214
|
+
export interface IJmapEmailSetRequest {
|
|
215
|
+
/** Keyed by client creation id. */
|
|
216
|
+
create?: Record<string, IJmapEmailCreate>;
|
|
217
|
+
/** Keyed by email id. */
|
|
218
|
+
update?: Record<string, IJmapEmailUpdate>;
|
|
219
|
+
destroy?: string[];
|
|
220
|
+
}
|
|
221
|
+
/** The outcome of a `setEmails` batch; `created` carries the full new records. */
|
|
222
|
+
export interface IJmapEmailSetResult {
|
|
223
|
+
oldState: string;
|
|
224
|
+
newState: string;
|
|
225
|
+
created: Record<string, IJmapEmailRecord>;
|
|
226
|
+
notCreated: Record<string, IJmapSetError>;
|
|
227
|
+
updated: string[];
|
|
228
|
+
notUpdated: Record<string, IJmapSetError>;
|
|
229
|
+
destroyed: string[];
|
|
230
|
+
notDestroyed: Record<string, IJmapSetError>;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* A change notification the backend emits when data mutates — both for
|
|
234
|
+
* mutations performed through the JMAP server and for out-of-band mutations
|
|
235
|
+
* (e.g. new mail arriving via an IMAP bridge). `changed` maps JMAP type names
|
|
236
|
+
* ('Email', 'Mailbox', 'Thread', ...) to the new opaque state string.
|
|
237
|
+
*/
|
|
238
|
+
export interface IJmapStateChange {
|
|
239
|
+
accountId: string;
|
|
240
|
+
changed: Record<string, string>;
|
|
241
|
+
}
|
|
242
|
+
export type TJmapChangeListener = (change: IJmapStateChange) => void;
|
|
243
|
+
/**
|
|
244
|
+
* Error a backend may throw from set-style operations to control the JMAP
|
|
245
|
+
* SetError `type` the server reports (e.g. 'notFound', 'invalidProperties').
|
|
246
|
+
* Any other thrown error is mapped to a generic SetError.
|
|
247
|
+
*/
|
|
248
|
+
export declare class JmapBackendError extends Error {
|
|
249
|
+
type: string;
|
|
250
|
+
constructor(type: string, description?: string);
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* The storage contract `JmapServer` dispatches into. Implement this against a
|
|
254
|
+
* real store (SQL, MongoDB, ...) to expose it as a JMAP server; the bundled
|
|
255
|
+
* `MemoryMailBackend` is the in-memory reference implementation.
|
|
256
|
+
*
|
|
257
|
+
* All state strings are backend-owned opaque strings: the server never
|
|
258
|
+
* interprets them beyond equality comparison and echoing them to clients.
|
|
259
|
+
*/
|
|
260
|
+
export interface IJmapMailBackend {
|
|
261
|
+
/**
|
|
262
|
+
* Maps an authenticated principal to its JMAP account, or null when the
|
|
263
|
+
* principal has no account (the server then responds 403).
|
|
264
|
+
*/
|
|
265
|
+
resolveAccount(principal: TJmapPrincipal): Promise<IJmapAccountInfo | null>;
|
|
266
|
+
/**
|
|
267
|
+
* Returns mailboxes by id (`ids: null` returns all mailboxes of the
|
|
268
|
+
* account) together with the current Mailbox collection state. Counter
|
|
269
|
+
* fields (totalEmails, unreadEmails, ...) must be accurate at read time.
|
|
270
|
+
*/
|
|
271
|
+
getMailboxes(accountId: string, ids: string[] | null): Promise<IJmapGetResult<IJmapMailboxRecord>>;
|
|
272
|
+
/**
|
|
273
|
+
* Returns mailbox changes since the given state, or null when the state is
|
|
274
|
+
* unknown/too old (the server then answers `cannotCalculateChanges`).
|
|
275
|
+
* When `maxChanges` is given, the result may be partial with
|
|
276
|
+
* `hasMoreChanges: true` and an intermediate `newState` to resume from.
|
|
277
|
+
*/
|
|
278
|
+
getMailboxChanges(accountId: string, sinceState: string, maxChanges?: number): Promise<IJmapChanges | null>;
|
|
279
|
+
/**
|
|
280
|
+
* Returns emails by id together with the current Email collection state.
|
|
281
|
+
* `properties` is a projection hint (null/undefined = all): backends may
|
|
282
|
+
* use it to avoid loading bodies, but returning full records is always
|
|
283
|
+
* correct — the server applies the final projection itself.
|
|
284
|
+
*/
|
|
285
|
+
getEmails(accountId: string, ids: string[], properties?: string[] | null): Promise<IJmapGetResult<IJmapEmailRecord>>;
|
|
286
|
+
/**
|
|
287
|
+
* Runs an Email query: apply the filter, sort, then slice by
|
|
288
|
+
* position/limit. `total` must be included when `calculateTotal` is set.
|
|
289
|
+
*/
|
|
290
|
+
queryEmails(accountId: string, options: IJmapEmailQueryOptions): Promise<IJmapEmailQueryResult>;
|
|
291
|
+
/**
|
|
292
|
+
* Returns email changes since the given state, or null when the state is
|
|
293
|
+
* unknown/too old (the server then answers `cannotCalculateChanges`).
|
|
294
|
+
*/
|
|
295
|
+
getEmailChanges(accountId: string, sinceState: string, maxChanges?: number): Promise<IJmapChanges | null>;
|
|
296
|
+
/**
|
|
297
|
+
* Applies an Email/set batch: creates (assigning ids, blobIds, and thread
|
|
298
|
+
* ids), updates (keyword/mailbox replacements and patches), and destroys.
|
|
299
|
+
* Per-item failures go into the not* maps; the whole batch must never
|
|
300
|
+
* throw for item-level problems. Must bump the Email state exactly when
|
|
301
|
+
* something changed and report old/new state.
|
|
302
|
+
*/
|
|
303
|
+
setEmails(accountId: string, request: IJmapEmailSetRequest): Promise<IJmapEmailSetResult>;
|
|
304
|
+
/**
|
|
305
|
+
* Returns threads by id (each thread lists its emailIds oldest first)
|
|
306
|
+
* together with the current Thread collection state.
|
|
307
|
+
*/
|
|
308
|
+
getThreads(accountId: string, ids: string[]): Promise<IJmapGetResult<IJmapThreadRecord>>;
|
|
309
|
+
/** Stores an uploaded blob and returns its id and byte size (RFC 8620 §6.1). */
|
|
310
|
+
uploadBlob(accountId: string, data: Uint8Array, type: string): Promise<{
|
|
311
|
+
blobId: string;
|
|
312
|
+
size: number;
|
|
313
|
+
}>;
|
|
314
|
+
/** Returns a blob's bytes and metadata, or null when unknown. */
|
|
315
|
+
getBlob(accountId: string, blobId: string): Promise<IJmapBlobRecord | null>;
|
|
316
|
+
/** Returns the sending identities of the account with their collection state. */
|
|
317
|
+
getIdentities(accountId: string): Promise<IJmapGetResult<IJmapIdentityRecord>>;
|
|
318
|
+
/**
|
|
319
|
+
* Accepts an email for sending: the server has already resolved the
|
|
320
|
+
* envelope and fetched the raw RFC 5322 payload — the backend performs (or
|
|
321
|
+
* queues) the actual delivery and returns the submission record.
|
|
322
|
+
*/
|
|
323
|
+
submitEmail(accountId: string, submission: IJmapSubmissionInput): Promise<IJmapSubmissionRecord>;
|
|
324
|
+
/**
|
|
325
|
+
* Subscribes to change notifications so the server can emit StateChange
|
|
326
|
+
* pushes — including for mutations that happen outside a JMAP request.
|
|
327
|
+
* Returns an unsubscribe function; the server always calls it on teardown.
|
|
328
|
+
*/
|
|
329
|
+
subscribeToChanges(listener: TJmapChangeListener): () => void;
|
|
330
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error a backend may throw from set-style operations to control the JMAP
|
|
3
|
+
* SetError `type` the server reports (e.g. 'notFound', 'invalidProperties').
|
|
4
|
+
* Any other thrown error is mapped to a generic SetError.
|
|
5
|
+
*/
|
|
6
|
+
export class JmapBackendError extends Error {
|
|
7
|
+
constructor(type, description) {
|
|
8
|
+
super(description ?? type);
|
|
9
|
+
this.type = type;
|
|
10
|
+
this.name = 'JmapBackendError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW50ZXJmYWNlcy5iYWNrZW5kLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvaW50ZXJmYWNlcy5iYWNrZW5kLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQXdRQTs7OztHQUlHO0FBQ0gsTUFBTSxPQUFPLGdCQUFpQixTQUFRLEtBQUs7SUFDekMsWUFBbUIsSUFBWSxFQUFFLFdBQW9CO1FBQ25ELEtBQUssQ0FBQyxXQUFXLElBQUksSUFBSSxDQUFDLENBQUM7UUFEVixTQUFJLEdBQUosSUFBSSxDQUFRO1FBRTdCLElBQUksQ0FBQyxJQUFJLEdBQUcsa0JBQWtCLENBQUM7SUFDakMsQ0FBQztDQUNGIn0=
|
package/npmextra.json
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
"githost": "code.foss.global",
|
|
6
6
|
"gitscope": "push.rocks",
|
|
7
7
|
"gitrepo": "smartjmap",
|
|
8
|
-
"shortDescription": "JMAP client for mail (RFC 8620/8621)",
|
|
9
|
-
"description": "A TypeScript JMAP client for reading, sending, and watching mail
|
|
8
|
+
"shortDescription": "JMAP client and server core for mail (RFC 8620/8621)",
|
|
9
|
+
"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.",
|
|
10
10
|
"npmPackagename": "@push.rocks/smartjmap",
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"projectDomain": "push.rocks",
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
"json",
|
|
22
22
|
"http",
|
|
23
23
|
"client",
|
|
24
|
+
"server",
|
|
25
|
+
"deno",
|
|
24
26
|
"typescript",
|
|
25
27
|
"event-driven",
|
|
26
28
|
"push",
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@push.rocks/smartjmap",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "A TypeScript JMAP client for reading, sending, and watching mail
|
|
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": {
|
|
7
7
|
".": "./dist_ts/index.js"
|
|
8
8
|
},
|
|
@@ -54,6 +54,8 @@
|
|
|
54
54
|
"json",
|
|
55
55
|
"http",
|
|
56
56
|
"client",
|
|
57
|
+
"server",
|
|
58
|
+
"deno",
|
|
57
59
|
"typescript",
|
|
58
60
|
"event-driven",
|
|
59
61
|
"push",
|
package/readme.hints.md
CHANGED
|
@@ -2,33 +2,44 @@
|
|
|
2
2
|
|
|
3
3
|
Design notes for @push.rocks/smartjmap.
|
|
4
4
|
|
|
5
|
+
## Architecture (2.x)
|
|
6
|
+
|
|
7
|
+
- Three layers: `JmapClient` (consumer), `JmapServer` (protocol core), `IJmapMailBackend` (storage contract, `ts/interfaces.backend.ts`). The server owns everything protocol-shaped (request envelope, limits, back-references, projection, bodyValue fetch flags, patch normalization, envelope derivation, SSE); the backend owns storage, ids, and state strings (opaque to the server, compared only for equality).
|
|
8
|
+
- `MemoryMailBackend` is the reference backend and the default, keeping the no-options `JmapServer` an offline test helper. Accounts are keyed by username (`accountId = account_<username>`) and auto-created on first reference, so `resolveAccount` never fails for the default auth registry.
|
|
9
|
+
- Auth is two-step: `authenticate(request) -> TJmapPrincipal | null` (custom hook replaces the built-in Basic/Bearer registry entirely), then `backend.resolveAccount(principal)` (null -> 403). `addUser`/`addBearerToken` only feed the default registry.
|
|
10
|
+
- Email/set update patches are normalized server-side into `IJmapEmailUpdate` (full `keywords`/`mailboxIds` replacements vs `keywordsPatch`/`mailboxIdsPatch` per-key patches, mutually exclusive per property) so real backends can apply patches natively (e.g. MongoDB `$set`/`$unset`). Emails are otherwise immutable per RFC 8621 §4.6; any other patched property is rejected.
|
|
11
|
+
- `submitEmail` hands the backend the resolved envelope (client-supplied or derived from from/to/cc/bcc) plus the raw RFC 5322 payload from the email's blob; the app performs actual delivery.
|
|
12
|
+
|
|
5
13
|
## HTTP layer
|
|
6
14
|
|
|
7
|
-
- Zero runtime dependencies: global `fetch` (Node.js 18+ floor; developed against @types/node 25).
|
|
8
|
-
- SSE is parsed by hand over a fetch body stream (~50 lines in `runEventStream`). The global `EventSource` was rejected deliberately: it is only default-enabled from Node 22.3 and, more importantly, the WHATWG EventSource API cannot send the `Authorization` header JMAP requires.
|
|
15
|
+
- Zero runtime dependencies: global `fetch`/`Request`/`Response`/`ReadableStream` (Node.js 18+ floor; developed against @types/node 25). `fetchHandler(request)` is the primary server API and is runtime-agnostic (Deno/Bun-mountable, covered by tests that use bare Request objects); `start(port)` wraps it in node:http, buffering request bodies (capped at maxSizeUpload + 1 -> 413) and piping response streams to the socket, cancelling the stream reader when the client disconnects.
|
|
16
|
+
- Client SSE is parsed by hand over a fetch body stream (~50 lines in `runEventStream`). The global `EventSource` was rejected deliberately: it is only default-enabled from Node 22.3 and, more importantly, the WHATWG EventSource API cannot send the `Authorization` header JMAP requires.
|
|
17
|
+
- Server SSE is a `ReadableStream` Response; per-stream state (ping interval timer, `types` filter, closeafter flag, last-pushed state per type) is registered in a set and torn down on stream cancel, `closeafter=state` delivery, enqueue failure, `stop()`, and stream-setup failure (a throwing `backend.subscribeToChanges` unwinds the ping timer and registry entry before the request 500s). The backend change-feed subscription is created when the first stream connects and dropped when the last one closes, so an idle server holds no listener.
|
|
18
|
+
- Push dedupe: a mutation may be reported twice (backend change feed fires during the request + the server pushes after mutating requests for backends whose feed only covers out-of-band changes). Streams track the last state pushed per type and skip repeats.
|
|
9
19
|
|
|
10
|
-
## Watching semantics
|
|
20
|
+
## Watching semantics (client)
|
|
11
21
|
|
|
12
22
|
- `connect()`: session GET → capability/account validation → mailbox resolution (role match on the lowercased mailbox name, e.g. 'INBOX' → role 'inbox', falling back to case-insensitive name match) → Email state snapshot → 'connected' → initial sweep (Email/query + Email/get, emitted oldest-first) → event stream.
|
|
13
23
|
- The Email state is snapshotted *before* the initial sweep and `fetchDeltas()` runs once right after the SSE stream is established, so mail arriving between connect and stream establishment is not lost; `seenEmailIds` deduplicates the overlap.
|
|
14
24
|
- Event stream errors/end → silent fallback to `Email/changes` polling on `pollIntervalMs`. `cannotCalculateChanges` → full re-query resync.
|
|
15
25
|
- `connect()` emits 'error' instead of throwing (mirrors smartimap). Unlike smartimap, the same instance may call `connect()` again — JMAP is stateless HTTP.
|
|
16
|
-
- `disconnect()` aborts the SSE fetch and clears the poll timer; the connect-failure path also tears both down, so no handles leak either way.
|
|
26
|
+
- `disconnect()` aborts the SSE fetch and clears the poll timer; the connect-failure path also tears both down, so no handles leak either way. The teardown-at-connect-entry, abort-in-catch, last-stream-wins, and guarded done-path pieces in `runEventStream` are all deliberate lifecycle fixes — keep them.
|
|
17
27
|
|
|
18
|
-
## RFC subset covered (
|
|
28
|
+
## RFC subset covered (2.x)
|
|
19
29
|
|
|
20
|
-
- RFC 8620: session resource, method calls/responses, `error` method responses, RFC 7807 problem details, `#creationId` back-references
|
|
21
|
-
- RFC 8621: Mailbox/get(+query
|
|
30
|
+
- RFC 8620: session resource, method calls/responses, `error` method responses, RFC 7807 problem details, enforced limits (`limit` request errors, `requestTooLarge` method errors), `using` validation (`unknownCapability`), `#creationId` back-references and full `ResultReference` `{resultOf, name, path}` resolution with `/*` array expansion, request/response `createdIds` maps, upload/download endpoints, event source (§7.3: `types`, `closeafter=state`, `ping` keepalives).
|
|
31
|
+
- RFC 8621: Mailbox/get(+query, +changes), Thread/get, Email/get (projection, bodyValue fetch flags, `maxBodyValueBytes`), Email/query (flat FilterCondition subset + receivedAt sort + position/limit/calculateTotal), Email/changes (incl. maxChanges), Email/set (create/update with §5.3 patches for keywords/mailboxIds/destroy, `ifInState`), Identity/get, EmailSubmission/set (envelope derivation, onSuccessUpdateEmail/onSuccessDestroyEmail with the implicit Email/set response).
|
|
22
32
|
|
|
23
|
-
## Known gaps
|
|
33
|
+
## Known gaps / phase-1 exclusions
|
|
24
34
|
|
|
25
|
-
- No
|
|
26
|
-
- No
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
35
|
+
- No `Email/queryChanges` (explicit `cannotCalculateChanges` error, not unknownMethod — clients like the bundled one re-query), no `Mailbox/set`, no `Email/copy`/`Email/import`/`Email/parse`, no `SearchSnippet/*`, no `VacationResponse/*`, no `PushSubscription` (§7.2) — the §7.3 event source is the only push channel.
|
|
36
|
+
- No `FilterOperator` trees (AND/OR/NOT → `unsupportedFilter`) and no `anchor` pagination (→ `invalidArguments`); Email sorting is `receivedAt` only, Mailbox sorting `name`/`sortOrder` only.
|
|
37
|
+
- EmailSubmission has no state tracking (`newState` is constant '0', updates/destroys are rejected with `forbidden`) and no EmailSubmission/get|changes.
|
|
38
|
+
- Session `state` and `sessionState` are the constant '0': the session object never changes at runtime for a given account.
|
|
39
|
+
- Memory backend simplifications: thread ids derive from normalized subjects (reply/forward prefixes stripped), not References headers — emails with an empty subject get their own thread; raw RFC 5322 blobs render headers + text body only (attachments stay separate blobs, not MIME-encoded into the raw message); date filters compare ISO strings lexicographically, assuming normalized UTC ('Z') timestamps.
|
|
40
|
+
- Initial-sweep race (client): an email created between the Email/query and Email/get of the sweep is picked up by the following delta fetch, not the sweep itself (dedupe prevents double emission).
|
|
30
41
|
|
|
31
42
|
## By-design unbounded state (accepted)
|
|
32
43
|
|
|
33
44
|
- Client: `seenEmailIds` retains every emitted email id for the instance lifetime — required for dedupe across reconnects and sweep/delta overlap; on a very long-lived, high-volume connection this grows without a pruning hook. Recreate the client to reset it.
|
|
34
|
-
-
|
|
45
|
+
- MemoryMailBackend (in-memory helper, not production storage): the per-account email and mailbox change logs grow one entry per mutation (needed by the /changes methods), destroyed emails leave their blobs and thread-subject mappings in place, and uploaded blobs are never expired. The node wrapper buffers request bodies in memory up to maxSizeUpload + 1 bytes before dispatch (test-helper convenience; the JMAP per-endpoint limits are enforced inside `fetchHandler`).
|