@wtfalch/email 0.4.0 → 0.5.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.
@@ -262,7 +262,62 @@ export class FakeMailbox {
262
262
  this.#threads.set(threadId, [id]);
263
263
  created[key] = { id, blobId: `blob-${id}`, threadId, size: 512 };
264
264
  }
265
- return { accountId: ACCOUNT, oldState: '1', newState: '2', created, notCreated: null };
265
+ /* The patch language, enough of it for what `mutate.ts` sends:
266
+ `keywords/$seen`, a whole `mailboxIds` replacement, and
267
+ `mailboxIds/<id>` removal. A `null` value deletes rather than sets
268
+ false, which is RFC 8620 §5.3's rule and the one a fake gets wrong
269
+ by treating the patch as a merge -- `keywords: { $seen: false }`
270
+ reads as unread everywhere in this package, so a fake that stores
271
+ the false would pass a test the real server fails. */
272
+ const update = (args.update ?? {});
273
+ const updated = {};
274
+ const notUpdated = {};
275
+ for (const [id, patch] of Object.entries(update)) {
276
+ const email = this.#emails.find((one) => one.id === id);
277
+ if (!email) {
278
+ notUpdated[id] = { type: 'notFound' };
279
+ continue;
280
+ }
281
+ for (const [path, value] of Object.entries(patch)) {
282
+ if (path === 'mailboxIds') {
283
+ email.mailboxIds = { ...value };
284
+ }
285
+ else if (path.startsWith('mailboxIds/')) {
286
+ const boxId = unpointer(path.slice('mailboxIds/'.length));
287
+ const next = { ...email.mailboxIds };
288
+ if (value === null)
289
+ delete next[boxId];
290
+ else
291
+ next[boxId] = true;
292
+ email.mailboxIds = next;
293
+ }
294
+ else if (path.startsWith('keywords/')) {
295
+ const keyword = unpointer(path.slice('keywords/'.length));
296
+ const next = { ...email.keywords };
297
+ if (value === null)
298
+ delete next[keyword];
299
+ else
300
+ next[keyword] = true;
301
+ email.keywords = next;
302
+ }
303
+ else if (path === 'keywords') {
304
+ email.keywords = { ...value };
305
+ }
306
+ else {
307
+ notUpdated[id] = { type: 'invalidPatch', description: `cannot patch ${path}` };
308
+ }
309
+ }
310
+ updated[id] = null;
311
+ }
312
+ return {
313
+ accountId: ACCOUNT,
314
+ oldState: '1',
315
+ newState: '2',
316
+ created,
317
+ notCreated: null,
318
+ updated,
319
+ notUpdated: Object.keys(notUpdated).length > 0 ? notUpdated : null,
320
+ };
266
321
  }
267
322
  case 'EmailSubmission/set': {
268
323
  const create = (args.create ?? {});
@@ -376,6 +431,11 @@ export class FakeMailbox {
376
431
  return out;
377
432
  }
378
433
  }
434
+ /** RFC 6901 §3 in reverse: one patch-key segment, unescaped. A mailbox id is
435
+ * the server's to choose, so a key built from one may carry `~0` or `~1`. */
436
+ function unpointer(segment) {
437
+ return segment.replaceAll('~1', '/').replaceAll('~0', '~');
438
+ }
379
439
  /** RFC 8620 §3.7's pointer, with the `*` that maps over a list. */
380
440
  function pointer(value, path) {
381
441
  let current = value;
@@ -1,11 +1,2 @@
1
1
  import type { SeedThread } from './mailbox.ts';
2
- /**
3
- * A plausible inbox.
4
- *
5
- * Chosen to put every state a list view has to draw on the screen at once:
6
- * unread and read, a thread with several messages, one with an attachment,
7
- * one flagged, one from a machine with no display name, and one subject long
8
- * enough to need truncating. A sample that is five tidy one-line messages
9
- * demonstrates a component that has never met mail.
10
- */
11
2
  export declare const SAMPLE: SeedThread[];
@@ -2,11 +2,26 @@
2
2
  * A plausible inbox.
3
3
  *
4
4
  * Chosen to put every state a list view has to draw on the screen at once:
5
- * unread and read, a thread with several messages, one with an attachment,
6
- * one flagged, one from a machine with no display name, and one subject long
7
- * enough to need truncating. A sample that is five tidy one-line messages
8
- * demonstrates a component that has never met mail.
5
+ * unread and read, a thread with several messages, one with several *unread*
6
+ * messages, one with an attachment, several with attachments of different
7
+ * kinds, one flagged, one from a machine with no display name, one with no
8
+ * subject at all, one whose sender has no display name, one subject long
9
+ * enough to need truncating and one preview that is a single word. A sample
10
+ * that is five tidy one-line messages demonstrates a component that has never
11
+ * met mail.
12
+ *
13
+ * **It is long on purpose.** A list of four rows cannot show whether a row is
14
+ * the right height, whether the date groups earn their space, whether the
15
+ * scroll region behaves or whether the pager is reachable. Sixteen
16
+ * conversations spread over a fortnight can, and that is the shortest sample
17
+ * that can: relative time collapses at an hour, a day, a week and a year, and
18
+ * a fixture with nothing older than yesterday exercises one of the four.
19
+ *
20
+ * Times are minutes before the fake's fixed "now", so every screenshot of
21
+ * this is the same screenshot.
9
22
  */
23
+ const HOUR = 60;
24
+ const DAY = 24 * HOUR;
10
25
  export const SAMPLE = [
11
26
  {
12
27
  mailbox: 'mb-inbox',
@@ -69,6 +84,187 @@ export const SAMPLE = [
69
84
  },
70
85
  ],
71
86
  },
87
+ /* Two people going back and forth, none of it read: the case where the row
88
+ has to say "4" and the mailbox count has to say one, not four. */
89
+ {
90
+ mailbox: 'mb-inbox',
91
+ messages: [
92
+ {
93
+ from: { name: 'Mary Somerville', email: 'mary@example.org' },
94
+ subject: 'The Difference Engine chapter',
95
+ text: 'I have marked up the proofs. Three passages need a decision from you before Friday.',
96
+ agoMinutes: 5 * HOUR,
97
+ },
98
+ {
99
+ from: { name: 'Mary Somerville', email: 'mary@example.org' },
100
+ subject: 'Re: The Difference Engine chapter',
101
+ text: 'Ignore the second passage — I have resolved it myself.',
102
+ agoMinutes: 4 * HOUR,
103
+ },
104
+ {
105
+ from: { name: 'Charles Babbage', email: 'charles@example.com' },
106
+ subject: 'Re: The Difference Engine chapter',
107
+ text: 'The third passage is the one that matters. I would rather we said nothing than said it vaguely.',
108
+ agoMinutes: 3 * HOUR,
109
+ },
110
+ {
111
+ from: { name: 'Mary Somerville', email: 'mary@example.org' },
112
+ subject: 'Re: The Difference Engine chapter',
113
+ text: 'Agreed. Ada, you have the casting vote.',
114
+ agoMinutes: 2 * HOUR,
115
+ },
116
+ ],
117
+ },
118
+ /* A message with no subject, which is a real thing that arrives and a real
119
+ way to make a list view draw an empty line. */
120
+ {
121
+ mailbox: 'mb-inbox',
122
+ messages: [
123
+ {
124
+ from: { name: 'Sophie Germain', email: 'sophie@example.fr' },
125
+ subject: '',
126
+ text: 'Yes.',
127
+ agoMinutes: 7 * HOUR,
128
+ seen: true,
129
+ },
130
+ ],
131
+ },
132
+ {
133
+ mailbox: 'mb-inbox',
134
+ messages: [
135
+ {
136
+ from: { name: 'The Royal Society', email: 'events@royalsociety.example.org' },
137
+ subject: 'Invitation: demonstration of the analytical engine, 14 October',
138
+ text: 'You are invited to a demonstration. Papers are attached. Please reply with numbers by the end of the month.',
139
+ agoMinutes: 9 * HOUR,
140
+ attachments: [
141
+ { name: 'programme.pdf', type: 'application/pdf', size: 214_882 },
142
+ { name: 'directions.png', type: 'image/png', size: 48_120 },
143
+ ],
144
+ },
145
+ ],
146
+ },
147
+ {
148
+ mailbox: 'mb-inbox',
149
+ messages: [
150
+ {
151
+ from: { name: 'Augustus De Morgan', email: 'augustus@example.ac.uk' },
152
+ subject: 'A correction, and an apology for the correction',
153
+ text: 'The recurrence in your note 7 is right and my objection was not. I have written to the editor withdrawing it.',
154
+ agoMinutes: 26 * HOUR,
155
+ seen: true,
156
+ },
157
+ {
158
+ from: { name: 'Ada Lovelace', email: 'ada@example.com' },
159
+ subject: 'Re: A correction, and an apology for the correction',
160
+ text: 'No apology needed. The objection made the note better.',
161
+ agoMinutes: 25 * HOUR,
162
+ seen: true,
163
+ },
164
+ ],
165
+ },
166
+ /* Only an HTML body, which the reading pane refuses to render and says so.
167
+ Its own row so the state is reachable from the list rather than only
168
+ from a unit test. */
169
+ {
170
+ mailbox: 'mb-inbox',
171
+ messages: [
172
+ {
173
+ from: { email: 'campaigns@marketing.example.com' },
174
+ subject: 'Your weekly digest, beautifully formatted',
175
+ text: '',
176
+ html: '<html><body><h1>This week</h1><p>Rendered nowhere, on purpose.</p></body></html>',
177
+ agoMinutes: 30 * HOUR,
178
+ },
179
+ ],
180
+ },
181
+ {
182
+ mailbox: 'mb-inbox',
183
+ messages: [
184
+ {
185
+ from: { name: 'William King', email: 'william@example.com' },
186
+ subject: 'Ockham Park, the weekend',
187
+ text: 'The house is free from Friday. Bring the tables if you must, but not the mill.',
188
+ agoMinutes: 2 * DAY,
189
+ seen: true,
190
+ },
191
+ ],
192
+ },
193
+ {
194
+ mailbox: 'mb-inbox',
195
+ messages: [
196
+ {
197
+ from: { name: 'Michael Faraday', email: 'michael@example.org' },
198
+ subject: 'Re: your question about the coil',
199
+ text: 'It is the rate of change, not the field. I have drawn it out, which is quicker than the sentence.',
200
+ agoMinutes: 3 * DAY,
201
+ seen: true,
202
+ attachments: [{ name: 'coil.svg', type: 'image/svg+xml', size: 4_902 }],
203
+ },
204
+ ],
205
+ },
206
+ {
207
+ mailbox: 'mb-inbox',
208
+ messages: [
209
+ {
210
+ from: { email: 'security-noreply@accounts.example.net' },
211
+ subject: 'New sign-in from an unrecognised device',
212
+ text: 'A new sign-in was recorded. If this was you, no action is needed.',
213
+ agoMinutes: 4 * DAY,
214
+ seen: true,
215
+ },
216
+ ],
217
+ },
218
+ {
219
+ mailbox: 'mb-inbox',
220
+ messages: [
221
+ {
222
+ from: { name: 'Anne Isabella Byron', email: 'annabella@example.com' },
223
+ subject: 'Sunday',
224
+ text: 'Come at four. There is nothing to discuss and I would like to discuss it at length.',
225
+ agoMinutes: 6 * DAY,
226
+ seen: true,
227
+ flagged: true,
228
+ },
229
+ ],
230
+ },
231
+ {
232
+ mailbox: 'mb-inbox',
233
+ messages: [
234
+ {
235
+ from: { name: 'Joseph-Marie Jacquard', email: 'joseph@example.fr' },
236
+ subject: 'On the punched card mechanism, its failure modes in humid weather, and what I propose to do about the third of them before the demonstration',
237
+ text: 'The cards swell. Everything else in the list follows from the cards swelling, so I have started there.',
238
+ agoMinutes: 9 * DAY,
239
+ seen: true,
240
+ },
241
+ ],
242
+ },
243
+ {
244
+ mailbox: 'mb-inbox',
245
+ messages: [
246
+ {
247
+ from: { name: 'Charles Wheatstone', email: 'charles.w@example.org' },
248
+ subject: 'Telegraph trial — results',
249
+ text: 'Twelve miles, no repeater, legible throughout. The full log is attached.',
250
+ agoMinutes: 14 * DAY,
251
+ seen: true,
252
+ attachments: [{ name: 'trial-log.csv', type: 'text/csv', size: 11_204 }],
253
+ },
254
+ ],
255
+ },
256
+ {
257
+ mailbox: 'mb-inbox',
258
+ messages: [
259
+ {
260
+ from: { name: 'Ada Lovelace', email: 'ada@example.com' },
261
+ subject: 'Notes to myself: the Bernoulli table',
262
+ text: 'Start from the recurrence, not the closed form. The closed form is where the last three attempts went wrong.',
263
+ agoMinutes: 400 * DAY,
264
+ seen: true,
265
+ },
266
+ ],
267
+ },
72
268
  {
73
269
  mailbox: 'mb-sent',
74
270
  messages: [
@@ -82,4 +278,28 @@ export const SAMPLE = [
82
278
  },
83
279
  ],
84
280
  },
281
+ {
282
+ mailbox: 'mb-sent',
283
+ messages: [
284
+ {
285
+ from: { name: 'Ada Lovelace', email: 'ada@example.com' },
286
+ to: [{ name: 'Mary Somerville', email: 'mary@example.org' }],
287
+ subject: 'Re: The Difference Engine chapter',
288
+ text: 'Give me until tomorrow on the third passage.',
289
+ agoMinutes: 90,
290
+ seen: true,
291
+ },
292
+ ],
293
+ },
294
+ {
295
+ mailbox: 'mb-junk',
296
+ messages: [
297
+ {
298
+ from: { email: 'winner@lottery.example' },
299
+ subject: 'YOU HAVE BEEN SELECTED',
300
+ text: 'Reply with your bank details to claim your prize.',
301
+ agoMinutes: 8 * HOUR,
302
+ },
303
+ ],
304
+ },
85
305
  ];
@@ -21,6 +21,8 @@ export { attribution, formatAddress, forwardDraft, forwardIntroduction, forwardS
21
21
  export type { ForwardOptions, ReplyOptions } from './drafts.ts';
22
22
  export { draftToEmail, send, sentPatch } from './submit.ts';
23
23
  export type { SendOptions } from './submit.ts';
24
+ export { moveThread, moveToRole, removeFromMailbox, setFlagged, setRead, } from './mutate.ts';
25
+ export type { MoveOptions } from './mutate.ts';
24
26
  export { push } from './push.ts';
25
27
  export type { PushHandle, PushOptions } from './push.ts';
26
28
  export { expandTemplate } from './uri.ts';
@@ -13,5 +13,6 @@ export { search } from "./search.js";
13
13
  export { identities } from "./identities.js";
14
14
  export { attribution, formatAddress, forwardDraft, forwardIntroduction, forwardSubject, quoteText, replyDraft, replySubject, } from "./drafts.js";
15
15
  export { draftToEmail, send, sentPatch } from "./submit.js";
16
+ export { moveThread, moveToRole, removeFromMailbox, setFlagged, setRead, } from "./mutate.js";
16
17
  export { push } from "./push.js";
17
18
  export { expandTemplate } from "./uri.js";