@wtfalch/email 0.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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/dist/mailbox/client.d.ts +85 -0
  3. package/dist/mailbox/client.js +201 -0
  4. package/dist/mailbox/drafts.d.ts +52 -0
  5. package/dist/mailbox/drafts.js +134 -0
  6. package/dist/mailbox/errors.d.ts +45 -0
  7. package/dist/mailbox/errors.js +88 -0
  8. package/dist/mailbox/fake/index.d.ts +34 -0
  9. package/dist/mailbox/fake/index.js +85 -0
  10. package/dist/mailbox/fake/mailbox.d.ts +65 -0
  11. package/dist/mailbox/fake/mailbox.js +402 -0
  12. package/dist/mailbox/fake/sample.d.ts +11 -0
  13. package/dist/mailbox/fake/sample.js +85 -0
  14. package/dist/mailbox/identities.d.ts +4 -0
  15. package/dist/mailbox/identities.js +18 -0
  16. package/dist/mailbox/index.d.ts +27 -0
  17. package/dist/mailbox/index.js +17 -0
  18. package/dist/mailbox/mail.css +451 -0
  19. package/dist/mailbox/mailboxes.d.ts +33 -0
  20. package/dist/mailbox/mailboxes.js +107 -0
  21. package/dist/mailbox/push.d.ts +40 -0
  22. package/dist/mailbox/push.js +127 -0
  23. package/dist/mailbox/react/Composer.d.ts +37 -0
  24. package/dist/mailbox/react/Composer.js +64 -0
  25. package/dist/mailbox/react/Mail.d.ts +8 -0
  26. package/dist/mailbox/react/Mail.js +149 -0
  27. package/dist/mailbox/react/MailboxTree.d.ts +14 -0
  28. package/dist/mailbox/react/MailboxTree.js +52 -0
  29. package/dist/mailbox/react/ThreadList.d.ts +37 -0
  30. package/dist/mailbox/react/ThreadList.js +41 -0
  31. package/dist/mailbox/react/ThreadView.d.ts +33 -0
  32. package/dist/mailbox/react/ThreadView.js +80 -0
  33. package/dist/mailbox/react/context.d.ts +11 -0
  34. package/dist/mailbox/react/context.js +28 -0
  35. package/dist/mailbox/react/hooks.d.ts +46 -0
  36. package/dist/mailbox/react/hooks.js +127 -0
  37. package/dist/mailbox/react/index.d.ts +24 -0
  38. package/dist/mailbox/react/index.js +18 -0
  39. package/dist/mailbox/search.d.ts +20 -0
  40. package/dist/mailbox/search.js +18 -0
  41. package/dist/mailbox/submit.d.ts +35 -0
  42. package/dist/mailbox/submit.js +150 -0
  43. package/dist/mailbox/thread.d.ts +61 -0
  44. package/dist/mailbox/thread.js +153 -0
  45. package/dist/mailbox/threads.d.ts +44 -0
  46. package/dist/mailbox/threads.js +156 -0
  47. package/dist/mailbox/types.d.ts +233 -0
  48. package/dist/mailbox/types.js +8 -0
  49. package/dist/mailbox/uri.d.ts +17 -0
  50. package/dist/mailbox/uri.js +26 -0
  51. package/dist/postmaster/apply.d.ts +62 -0
  52. package/dist/postmaster/apply.js +192 -0
  53. package/dist/postmaster/client.d.ts +127 -0
  54. package/dist/postmaster/client.js +235 -0
  55. package/dist/postmaster/index.d.ts +33 -0
  56. package/dist/postmaster/index.js +33 -0
  57. package/dist/postmaster/instance.d.ts +37 -0
  58. package/dist/postmaster/instance.js +21 -0
  59. package/dist/postmaster/load.d.ts +12 -0
  60. package/dist/postmaster/load.js +34 -0
  61. package/dist/postmaster/mailboxes.d.ts +23 -0
  62. package/dist/postmaster/mailboxes.js +35 -0
  63. package/dist/postmaster/objects.d.ts +47 -0
  64. package/dist/postmaster/objects.js +167 -0
  65. package/dist/postmaster/overview.d.ts +177 -0
  66. package/dist/postmaster/overview.js +112 -0
  67. package/dist/postmaster/react/controls.d.ts +18 -0
  68. package/dist/postmaster/react/controls.js +71 -0
  69. package/dist/postmaster/react/index.d.ts +13 -0
  70. package/dist/postmaster/react/index.js +12 -0
  71. package/dist/postmaster/react/panel.d.ts +42 -0
  72. package/dist/postmaster/react/panel.js +104 -0
  73. package/dist/postmaster/react/types.d.ts +10 -0
  74. package/dist/postmaster/react/types.js +1 -0
  75. package/dist/postmaster/writes.d.ts +62 -0
  76. package/dist/postmaster/writes.js +131 -0
  77. package/package.json +91 -0
@@ -0,0 +1,27 @@
1
+ /**
2
+ * `@wtfalch/mail` — a JMAP mail client core.
3
+ *
4
+ * Everything takes a `MailClient` and returns this package's own shapes. The
5
+ * JMAP wire, and the `jmap-jam` client that speaks it, stay inside.
6
+ */
7
+ export { MailClient, MAIL_CAPABILITY, SUBMISSION_CAPABILITY } from './client.ts';
8
+ export type { MailClientOptions } from './client.ts';
9
+ export { MailError, toMailError } from './errors.ts';
10
+ export type { ProblemDetails } from './errors.ts';
11
+ export { buildTree, findRole, mailboxes, walk } from './mailboxes.ts';
12
+ export type { FlatMailbox } from './mailboxes.ts';
13
+ export { listThreads, queryThreads, summarise } from './threads.ts';
14
+ export type { ListOptions } from './threads.ts';
15
+ export { DEFAULT_MAX_BODY_BYTES, thread, toMessage } from './thread.ts';
16
+ export type { RawMessage, ThreadOptions } from './thread.ts';
17
+ export { search } from './search.ts';
18
+ export type { SearchOptions } from './search.ts';
19
+ export { identities } from './identities.ts';
20
+ export { attribution, formatAddress, forwardDraft, forwardIntroduction, forwardSubject, quoteText, replyDraft, replySubject, } from './drafts.ts';
21
+ export type { ForwardOptions, ReplyOptions } from './drafts.ts';
22
+ export { draftToEmail, send, sentPatch } from './submit.ts';
23
+ export type { SendOptions } from './submit.ts';
24
+ export { push } from './push.ts';
25
+ export type { PushHandle, PushOptions } from './push.ts';
26
+ export { expandTemplate } from './uri.ts';
27
+ export type { Attachment, ChangedState, Draft, DraftAttachment, EmailAddress, MailFilter, MailIdentity, MailSession, MailSort, MailboxNode, MailboxRole, Message, Sent, ThreadDetail, ThreadPage, ThreadSummary, } from './types.ts';
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `@wtfalch/mail` — a JMAP mail client core.
3
+ *
4
+ * Everything takes a `MailClient` and returns this package's own shapes. The
5
+ * JMAP wire, and the `jmap-jam` client that speaks it, stay inside.
6
+ */
7
+ export { MailClient, MAIL_CAPABILITY, SUBMISSION_CAPABILITY } from "./client.js";
8
+ export { MailError, toMailError } from "./errors.js";
9
+ export { buildTree, findRole, mailboxes, walk } from "./mailboxes.js";
10
+ export { listThreads, queryThreads, summarise } from "./threads.js";
11
+ export { DEFAULT_MAX_BODY_BYTES, thread, toMessage } from "./thread.js";
12
+ export { search } from "./search.js";
13
+ export { identities } from "./identities.js";
14
+ export { attribution, formatAddress, forwardDraft, forwardIntroduction, forwardSubject, quoteText, replyDraft, replySubject, } from "./drafts.js";
15
+ export { draftToEmail, send, sentPatch } from "./submit.js";
16
+ export { push } from "./push.js";
17
+ export { expandTemplate } from "./uri.js";
@@ -0,0 +1,451 @@
1
+ /* The mail client's own layout. Everything visual comes from
2
+ `@wtfalch/design`'s tokens; nothing here defines a colour, a radius or a
3
+ space of its own. Import it after the design stylesheet. */
4
+
5
+ .mail {
6
+ display: flex;
7
+ flex-direction: column;
8
+ height: 100%;
9
+ min-height: 0;
10
+ background: var(--bg);
11
+ color: var(--text);
12
+ font-size: var(--text-sm);
13
+ }
14
+
15
+ .mail-bar {
16
+ display: flex;
17
+ align-items: center;
18
+ gap: var(--space-3);
19
+ padding: var(--space-3);
20
+ border-bottom: var(--border-width) solid var(--border);
21
+ flex: none;
22
+ }
23
+
24
+ .mail-search {
25
+ flex: 1 1 auto;
26
+ min-width: 0;
27
+ }
28
+ .mail-panes {
29
+ flex: 1 1 auto;
30
+ min-height: 0;
31
+ }
32
+
33
+ /* ---------------------------------------------------------------- mailboxes */
34
+ .mailboxes {
35
+ height: 100%;
36
+ background: var(--panel);
37
+ }
38
+ .mailboxes-quiet,
39
+ .threads-quiet,
40
+ .message-quiet {
41
+ margin: 0;
42
+ padding: var(--space-4);
43
+ color: var(--muted);
44
+ font-size: var(--text-xs);
45
+ }
46
+
47
+ .mailboxes-list {
48
+ list-style: none;
49
+ margin: 0;
50
+ padding: 0;
51
+ }
52
+ .mailboxes-list .mailboxes-list {
53
+ padding-left: var(--space-3);
54
+ }
55
+
56
+ .mailbox {
57
+ display: flex;
58
+ align-items: center;
59
+ gap: var(--space-2);
60
+ width: 100%;
61
+ padding: var(--space-2) var(--space-3);
62
+ padding-left: calc(var(--space-3) + var(--depth, 0) * var(--space-3));
63
+ border: none;
64
+ background: transparent;
65
+ color: var(--muted);
66
+ font: inherit;
67
+ font-size: var(--text-sm);
68
+ text-align: left;
69
+ cursor: pointer;
70
+ }
71
+
72
+ .mailbox:hover {
73
+ background: var(--panel-2);
74
+ color: var(--text);
75
+ }
76
+ .mailbox:focus-visible {
77
+ outline: none;
78
+ box-shadow: var(--focus-ring);
79
+ }
80
+ .mailbox.on {
81
+ background: var(--panel-2);
82
+ color: var(--text);
83
+ }
84
+ /* Unread is weight, so it survives a theme, a projector and a reader who
85
+ cannot tell the colours apart. */
86
+ .mailbox.unread {
87
+ color: var(--text);
88
+ font-weight: var(--weight-strong);
89
+ }
90
+
91
+ .mailbox-icon {
92
+ flex: none;
93
+ width: var(--space-4);
94
+ height: var(--space-4);
95
+ }
96
+ .mailbox-name {
97
+ flex: 1 1 auto;
98
+ min-width: 0;
99
+ overflow: hidden;
100
+ text-overflow: ellipsis;
101
+ white-space: nowrap;
102
+ }
103
+ .mailbox-count {
104
+ flex: none;
105
+ font-size: var(--text-2xs);
106
+ font-variant-numeric: tabular-nums;
107
+ }
108
+
109
+ /* ------------------------------------------------------------------ threads */
110
+ .threads {
111
+ display: flex;
112
+ flex-direction: column;
113
+ height: 100%;
114
+ min-height: 0;
115
+ background: var(--panel);
116
+ }
117
+ .threads-scroll {
118
+ flex: 1 1 auto;
119
+ }
120
+ .threads-list {
121
+ list-style: none;
122
+ margin: 0;
123
+ padding: 0;
124
+ }
125
+ .threads-pager {
126
+ flex: none;
127
+ padding: 0 var(--space-3);
128
+ border-top: var(--border-width) solid var(--border);
129
+ }
130
+
131
+ .thread-row {
132
+ display: grid;
133
+ gap: var(--space-1);
134
+ width: 100%;
135
+ padding: var(--space-3);
136
+ border: none;
137
+ border-bottom: var(--border-width) solid var(--border);
138
+ background: transparent;
139
+ color: var(--muted);
140
+ font: inherit;
141
+ text-align: left;
142
+ cursor: pointer;
143
+ }
144
+
145
+ .thread-row:hover {
146
+ background: var(--panel-2);
147
+ }
148
+ .thread-row:focus-visible {
149
+ outline: none;
150
+ box-shadow: var(--focus-ring);
151
+ }
152
+ .thread-row.on {
153
+ background: var(--panel-2);
154
+ }
155
+ .thread-row.unread {
156
+ color: var(--text);
157
+ }
158
+ .thread-row.unread .thread-subject-text {
159
+ font-weight: var(--weight-strong);
160
+ }
161
+
162
+ .thread-top {
163
+ display: flex;
164
+ align-items: center;
165
+ gap: var(--space-2);
166
+ min-width: 0;
167
+ }
168
+ .thread-who {
169
+ flex: 1 1 auto;
170
+ min-width: 0;
171
+ }
172
+ .thread-nobody {
173
+ color: var(--muted);
174
+ font-style: italic;
175
+ }
176
+ .thread-when {
177
+ flex: none;
178
+ font-size: var(--text-2xs);
179
+ color: var(--muted);
180
+ font-variant-numeric: tabular-nums;
181
+ }
182
+
183
+ .thread-subject {
184
+ display: flex;
185
+ align-items: baseline;
186
+ gap: var(--space-2);
187
+ min-width: 0;
188
+ }
189
+ .thread-dot {
190
+ flex: none;
191
+ color: var(--accent);
192
+ }
193
+ .thread-subject-text {
194
+ flex: 1 1 auto;
195
+ min-width: 0;
196
+ overflow: hidden;
197
+ text-overflow: ellipsis;
198
+ white-space: nowrap;
199
+ }
200
+ .thread-count {
201
+ flex: none;
202
+ padding: 0 var(--space-1);
203
+ border: var(--border-width) solid var(--border);
204
+ border-radius: var(--radius-pill);
205
+ font-size: var(--text-2xs);
206
+ color: var(--muted);
207
+ }
208
+
209
+ .thread-preview {
210
+ color: var(--muted);
211
+ font-size: var(--text-xs);
212
+ overflow: hidden;
213
+ text-overflow: ellipsis;
214
+ white-space: nowrap;
215
+ }
216
+
217
+ .thread-marks {
218
+ display: flex;
219
+ gap: var(--space-1);
220
+ flex-wrap: wrap;
221
+ }
222
+
223
+ /* --------------------------------------------------------------- one thread */
224
+ .thread {
225
+ display: flex;
226
+ flex-direction: column;
227
+ height: 100%;
228
+ min-height: 0;
229
+ }
230
+ .thread-head {
231
+ flex: none;
232
+ padding: var(--space-4);
233
+ border-bottom: var(--border-width) solid var(--border);
234
+ }
235
+ .thread-title {
236
+ margin: 0;
237
+ font-size: var(--text-lg);
238
+ font-weight: var(--weight-strong);
239
+ }
240
+ .thread-meta {
241
+ margin: var(--space-1) 0 0;
242
+ color: var(--muted);
243
+ font-size: var(--text-xs);
244
+ }
245
+ .thread-body {
246
+ flex: 1 1 auto;
247
+ }
248
+
249
+ .messages {
250
+ list-style: none;
251
+ margin: 0;
252
+ padding: 0;
253
+ }
254
+
255
+ .message {
256
+ border-bottom: var(--border-width) solid var(--border);
257
+ }
258
+ .message-head {
259
+ display: flex;
260
+ align-items: center;
261
+ gap: var(--space-2);
262
+ padding: 0 var(--space-4);
263
+ }
264
+
265
+ .message-toggle {
266
+ display: flex;
267
+ align-items: center;
268
+ gap: var(--space-3);
269
+ flex: 1 1 auto;
270
+ min-width: 0;
271
+ padding: var(--space-3) 0;
272
+ border: none;
273
+ background: transparent;
274
+ color: inherit;
275
+ font: inherit;
276
+ text-align: left;
277
+ cursor: pointer;
278
+ }
279
+
280
+ .message-toggle:focus-visible {
281
+ outline: none;
282
+ box-shadow: var(--focus-ring);
283
+ }
284
+ .message-when {
285
+ flex: none;
286
+ margin-left: auto;
287
+ color: var(--muted);
288
+ font-size: var(--text-2xs);
289
+ }
290
+ .message-nobody {
291
+ color: var(--muted);
292
+ font-style: italic;
293
+ }
294
+
295
+ .message-body {
296
+ padding: 0 var(--space-4) var(--space-4);
297
+ }
298
+ .message-lines {
299
+ margin-bottom: var(--space-3);
300
+ }
301
+ .message-line {
302
+ margin: 0;
303
+ color: var(--muted);
304
+ font-size: var(--text-xs);
305
+ display: flex;
306
+ gap: var(--space-2);
307
+ }
308
+ .message-line-label {
309
+ min-width: var(--space-6);
310
+ }
311
+
312
+ /* Plain-text mail carries its own line breaks and its own quote markers.
313
+ Reflowing it destroys both, so it is shown as written. */
314
+ .message-text,
315
+ .message-source {
316
+ margin: 0;
317
+ white-space: pre-wrap;
318
+ overflow-wrap: anywhere;
319
+ font-family: inherit;
320
+ font-size: var(--text-sm);
321
+ line-height: var(--line-height);
322
+ color: var(--text);
323
+ }
324
+
325
+ .message-source {
326
+ margin-top: var(--space-2);
327
+ padding: var(--space-3);
328
+ border-radius: var(--radius);
329
+ background: var(--panel-2);
330
+ font-family: var(--font-mono);
331
+ font-size: var(--text-xs);
332
+ max-height: 20rem;
333
+ overflow: auto;
334
+ }
335
+
336
+ .message-html {
337
+ display: grid;
338
+ gap: var(--space-2);
339
+ justify-items: start;
340
+ }
341
+
342
+ .attachments {
343
+ list-style: none;
344
+ display: flex;
345
+ flex-wrap: wrap;
346
+ gap: var(--space-2);
347
+ margin: var(--space-4) 0 0;
348
+ padding: 0;
349
+ }
350
+
351
+ .attachment {
352
+ display: inline-flex;
353
+ align-items: center;
354
+ gap: var(--space-2);
355
+ padding: var(--space-2) var(--space-3);
356
+ border: var(--border-width) solid var(--border);
357
+ border-radius: var(--radius);
358
+ background: var(--panel-2);
359
+ color: var(--text);
360
+ font: inherit;
361
+ font-size: var(--text-xs);
362
+ cursor: pointer;
363
+ }
364
+
365
+ .attachment:hover:not(:disabled) {
366
+ border-color: var(--accent);
367
+ }
368
+ .attachment:focus-visible {
369
+ outline: none;
370
+ box-shadow: var(--focus-ring);
371
+ }
372
+ .attachment:disabled {
373
+ cursor: default;
374
+ color: var(--muted);
375
+ }
376
+ .attachment-icon {
377
+ flex: none;
378
+ width: var(--space-4);
379
+ height: var(--space-4);
380
+ color: var(--muted);
381
+ }
382
+ .attachment-size {
383
+ color: var(--muted);
384
+ }
385
+
386
+ /* ----------------------------------------------------------------- composer */
387
+ .composer {
388
+ display: grid;
389
+ gap: var(--space-3);
390
+ }
391
+
392
+ .composer-addresses {
393
+ display: flex;
394
+ flex-wrap: wrap;
395
+ align-items: center;
396
+ gap: var(--space-1);
397
+ padding: var(--space-1);
398
+ border: var(--border-width) solid var(--border);
399
+ border-radius: var(--radius);
400
+ background: var(--control);
401
+ }
402
+
403
+ .composer-addresses:focus-within {
404
+ border-color: var(--accent);
405
+ }
406
+
407
+ /* The input is a seam in the row of chips rather than a box of its own: two
408
+ nested boxes make the field look broken at every width. */
409
+ .composer-address-input {
410
+ flex: 1 1 8rem;
411
+ min-width: 6rem;
412
+ border: none;
413
+ background: transparent;
414
+ box-shadow: none;
415
+ }
416
+
417
+ .composer-address-input:focus {
418
+ outline: none;
419
+ box-shadow: none;
420
+ }
421
+
422
+ .composer-files {
423
+ list-style: none;
424
+ margin: 0;
425
+ padding: 0;
426
+ display: grid;
427
+ gap: var(--space-1);
428
+ }
429
+ .composer-files li {
430
+ display: flex;
431
+ align-items: center;
432
+ gap: var(--space-2);
433
+ }
434
+ .composer-file {
435
+ font-size: var(--text-xs);
436
+ }
437
+
438
+ .composer-actions {
439
+ display: flex;
440
+ align-items: center;
441
+ gap: var(--space-3);
442
+ }
443
+ .composer-hint {
444
+ color: var(--muted);
445
+ font-size: var(--text-xs);
446
+ }
447
+ .composer-error {
448
+ margin: 0;
449
+ color: var(--bad);
450
+ font-size: var(--text-xs);
451
+ }
@@ -0,0 +1,33 @@
1
+ import type { MailClient } from './client.ts';
2
+ import type { MailboxNode, MailboxRole } from './types.ts';
3
+ /** A mailbox as the server returns it, before the tree is built. */
4
+ export type FlatMailbox = {
5
+ id: string;
6
+ name: string;
7
+ parentId: string | null;
8
+ role: MailboxRole | null;
9
+ sortOrder: number;
10
+ totalEmails: number;
11
+ unreadEmails: number;
12
+ totalThreads: number;
13
+ unreadThreads: number;
14
+ isSubscribed: boolean;
15
+ };
16
+ /**
17
+ * Build the mailbox tree from the flat list the server returns.
18
+ *
19
+ * Two shapes the server can legally produce, and one it should not, all have
20
+ * to come out as a usable tree:
21
+ *
22
+ * - a mailbox whose parent is not in the list (the account may hide it, or
23
+ * the list may be a filtered subset) becomes a root, so it stays visible;
24
+ * - a cycle, which RFC 8621 forbids but nothing on the wire prevents, is
25
+ * broken by only visiting each mailbox once.
26
+ */
27
+ export declare function buildTree(list: readonly FlatMailbox[]): readonly MailboxNode[];
28
+ /** Walk a tree depth-first, parents before children. */
29
+ export declare function walk(nodes: readonly MailboxNode[]): Generator<MailboxNode>;
30
+ /** The first mailbox with a given role, e.g. `drafts`. */
31
+ export declare function findRole(nodes: readonly MailboxNode[], role: MailboxRole): MailboxNode | undefined;
32
+ /** Every mailbox in the account, as a tree. */
33
+ export declare function mailboxes(client: MailClient): Promise<readonly MailboxNode[]>;
@@ -0,0 +1,107 @@
1
+ import { guard } from "./errors.js";
2
+ /** RFC 8621 §2: order by `sortOrder`, and break ties by name. */
3
+ function compare(a, b) {
4
+ if (a.sortOrder !== b.sortOrder)
5
+ return a.sortOrder - b.sortOrder;
6
+ const left = a.name.toLowerCase();
7
+ const right = b.name.toLowerCase();
8
+ if (left !== right)
9
+ return left < right ? -1 : 1;
10
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
11
+ }
12
+ /**
13
+ * Build the mailbox tree from the flat list the server returns.
14
+ *
15
+ * Two shapes the server can legally produce, and one it should not, all have
16
+ * to come out as a usable tree:
17
+ *
18
+ * - a mailbox whose parent is not in the list (the account may hide it, or
19
+ * the list may be a filtered subset) becomes a root, so it stays visible;
20
+ * - a cycle, which RFC 8621 forbids but nothing on the wire prevents, is
21
+ * broken by only visiting each mailbox once.
22
+ */
23
+ export function buildTree(list) {
24
+ const byId = new Map(list.map((mailbox) => [mailbox.id, mailbox]));
25
+ const childrenOf = new Map();
26
+ for (const mailbox of list) {
27
+ const parentId = mailbox.parentId !== null && byId.has(mailbox.parentId) ? mailbox.parentId : null;
28
+ const siblings = childrenOf.get(parentId);
29
+ if (siblings)
30
+ siblings.push(mailbox);
31
+ else
32
+ childrenOf.set(parentId, [mailbox]);
33
+ }
34
+ for (const siblings of childrenOf.values())
35
+ siblings.sort(compare);
36
+ const visited = new Set();
37
+ const build = (mailbox, ancestry) => {
38
+ visited.add(mailbox.id);
39
+ const path = [...ancestry, mailbox.name];
40
+ const children = (childrenOf.get(mailbox.id) ?? [])
41
+ .filter((child) => !visited.has(child.id))
42
+ .map((child) => build(child, path));
43
+ return {
44
+ id: mailbox.id,
45
+ name: mailbox.name,
46
+ parentId: mailbox.parentId,
47
+ role: mailbox.role,
48
+ sortOrder: mailbox.sortOrder,
49
+ totalEmails: mailbox.totalEmails,
50
+ unreadEmails: mailbox.unreadEmails,
51
+ totalThreads: mailbox.totalThreads,
52
+ unreadThreads: mailbox.unreadThreads,
53
+ isSubscribed: mailbox.isSubscribed,
54
+ path,
55
+ children,
56
+ };
57
+ };
58
+ const roots = (childrenOf.get(null) ?? []).map((mailbox) => build(mailbox, []));
59
+ // Anything a cycle kept out of the tree is attached at the root rather than
60
+ // dropped: a mailbox the caller cannot see is worse than one shown flat.
61
+ // `visited` is re-read as the loop goes, because building one stranded
62
+ // mailbox pulls the rest of its cycle in as children.
63
+ const stranded = [];
64
+ for (const mailbox of [...list].sort(compare)) {
65
+ if (visited.has(mailbox.id))
66
+ continue;
67
+ stranded.push(build(mailbox, []));
68
+ }
69
+ return [...roots, ...stranded];
70
+ }
71
+ /** Walk a tree depth-first, parents before children. */
72
+ export function* walk(nodes) {
73
+ for (const node of nodes) {
74
+ yield node;
75
+ yield* walk(node.children);
76
+ }
77
+ }
78
+ /** The first mailbox with a given role, e.g. `drafts`. */
79
+ export function findRole(nodes, role) {
80
+ for (const node of walk(nodes))
81
+ if (node.role === role)
82
+ return node;
83
+ return undefined;
84
+ }
85
+ const MAILBOX_PROPERTIES = [
86
+ 'id',
87
+ 'name',
88
+ 'parentId',
89
+ 'role',
90
+ 'sortOrder',
91
+ 'totalEmails',
92
+ 'unreadEmails',
93
+ 'totalThreads',
94
+ 'unreadThreads',
95
+ 'isSubscribed',
96
+ ];
97
+ /** Every mailbox in the account, as a tree. */
98
+ export async function mailboxes(client) {
99
+ const { jam, accountId } = await client.connect();
100
+ return guard('mailboxes', async () => {
101
+ const [result] = await jam.api.Mailbox.get({
102
+ accountId,
103
+ properties: MAILBOX_PROPERTIES,
104
+ });
105
+ return buildTree(result.list);
106
+ });
107
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Push, as server-sent events.
3
+ *
4
+ * RFC 8620 §7.3 puts the event source behind the same authentication as the
5
+ * API, and the browser `EventSource` cannot send an `Authorization` header —
6
+ * it has no way to set headers at all. `jmap-jam`'s `connectEventSource`
7
+ * therefore only works against a server that accepts a cookie or a token in
8
+ * the query string, which is not what a bearer token is.
9
+ *
10
+ * So the stream is read with `fetch`, which takes the same header every other
11
+ * request carries, and the handful of lines of SSE framing are parsed here.
12
+ * This works unchanged in a browser and in Node.
13
+ */
14
+ import type { MailClient } from './client.ts';
15
+ import type { ChangedState } from './types.ts';
16
+ export type PushOptions = {
17
+ /** Which types to be told about. `*` is everything. */
18
+ types?: readonly string[] | '*';
19
+ /** Seconds between the server's keep-alive pings. */
20
+ ping?: number;
21
+ /** Called when the connection drops and again when it comes back, so a UI
22
+ * can say it is stale. */
23
+ onConnectionChange?: (connected: boolean) => void;
24
+ /** Called for an error that did not stop the retry loop. */
25
+ onError?: (error: unknown) => void;
26
+ };
27
+ export type PushHandle = {
28
+ /** Stop listening. The connection closes and no handler runs again. */
29
+ close: () => void;
30
+ };
31
+ /**
32
+ * Watch for changes on the account.
33
+ *
34
+ * `handler` is called with the changed state strings for this account; a
35
+ * value that differs from the one a previous read returned means that type
36
+ * has changed and its list should be refetched. The stream reconnects on its
37
+ * own with a backoff, so the handle only has to be closed when the caller is
38
+ * finished with it.
39
+ */
40
+ export declare function push(client: MailClient, handler: (changed: ChangedState) => void, options?: PushOptions): PushHandle;