@remit/web-client 0.0.303 → 0.0.304

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.
@@ -0,0 +1,191 @@
1
+ import type {
2
+ RemitImapOutboxMessageResponse,
3
+ RemitImapThreadMessageResponse,
4
+ } from "@remit/api-http-client/types.gen.ts";
5
+ import {
6
+ DAY,
7
+ HOUR,
8
+ type MailWorld,
9
+ MINUTE,
10
+ mailboxIdFor,
11
+ mailWorld,
12
+ outboxMessage,
13
+ threadRow,
14
+ WORK,
15
+ } from "./mail-world";
16
+
17
+ export const INBOX = mailboxIdFor(WORK, "Inbox");
18
+
19
+ export const LUNCH_SUBJECT = "Lunch Thursday?";
20
+ export const LUNCH_BODY = "Are we still on for Thursday? I can do 12:30.";
21
+ export const LONG_SUBJECT = "Billing migration owners";
22
+ export const TURNS_SUBJECT = "Offsite venue";
23
+ export const SCANS_SUBJECT = "Scans";
24
+ export const MISSPELT_DRAFT =
25
+ "<p>Ths report is redy today, and the notes are attachd.</p>";
26
+ export const PLAIN_DRAFT = [
27
+ "Thanks, that works for me.",
28
+ "",
29
+ "| Region | Total |",
30
+ "| --- | --- |",
31
+ "| EMEA | 412 |",
32
+ ].join("\n");
33
+
34
+ const longBody = [
35
+ "Longer context, so the pane has more message than it has room for.",
36
+ ...Array.from(
37
+ { length: 14 },
38
+ (_, index) =>
39
+ `Point ${index + 1}. The billing migration touches the export path, the dunning schedule and the invoice numbering, and each of those has a different owner today.`,
40
+ ),
41
+ ].join("\n\n");
42
+
43
+ const longDraft = [
44
+ "<p>That works. Before Thursday, three things I want written down:</p>",
45
+ ...Array.from(
46
+ { length: 24 },
47
+ (_, index) =>
48
+ `<p>Point ${index + 1}. Whoever owns the dunning mail after self-serve ships also owns the invoice numbering, and today those are two different people on two different rotas.</p>`,
49
+ ),
50
+ ].join("");
51
+
52
+ const conversations: RemitImapThreadMessageResponse[] = [
53
+ threadRow({
54
+ id: "lunch",
55
+ accountId: WORK,
56
+ fromName: "Ada Lovelace",
57
+ fromEmail: "ada@acme.example",
58
+ subject: LUNCH_SUBJECT,
59
+ snippet: LUNCH_BODY,
60
+ category: "personal",
61
+ ago: 30 * MINUTE,
62
+ }),
63
+ threadRow({
64
+ id: "billing",
65
+ accountId: WORK,
66
+ fromName: "Priya Raman",
67
+ fromEmail: "priya@acme.example",
68
+ subject: LONG_SUBJECT,
69
+ snippet:
70
+ "Longer context, so the pane has more message than it has room for.",
71
+ category: "personal",
72
+ ago: 3 * HOUR,
73
+ }),
74
+ threadRow({
75
+ id: "venue-1",
76
+ inThread: "venue",
77
+ accountId: WORK,
78
+ role: "Archive",
79
+ fromName: "Linus Berg",
80
+ fromEmail: "linus@acme.example",
81
+ subject: TURNS_SUBJECT,
82
+ snippet: "Two options for the offsite: the boathouse or the old mill.",
83
+ category: "personal",
84
+ ago: 3 * DAY,
85
+ isRead: true,
86
+ }),
87
+ threadRow({
88
+ id: "venue-2",
89
+ inThread: "venue",
90
+ accountId: WORK,
91
+ role: "Sent",
92
+ fromName: "Alice Tan",
93
+ fromEmail: "alice.tan@acme.example",
94
+ subject: `Re: ${TURNS_SUBJECT}`,
95
+ snippet: "The boathouse, if it still has the long table.",
96
+ category: "personal",
97
+ ago: 2 * DAY,
98
+ isRead: true,
99
+ }),
100
+ threadRow({
101
+ id: "venue-3",
102
+ inThread: "venue",
103
+ accountId: WORK,
104
+ fromName: "Linus Berg",
105
+ fromEmail: "linus@acme.example",
106
+ subject: `Re: ${TURNS_SUBJECT}`,
107
+ snippet: "It does. Shall I book it for the 14th?",
108
+ category: "personal",
109
+ ago: 4 * HOUR,
110
+ }),
111
+ threadRow({
112
+ id: "scans",
113
+ accountId: WORK,
114
+ fromName: "Office Scanner",
115
+ fromEmail: "scanner@acme.example",
116
+ subject: SCANS_SUBJECT,
117
+ snippet: "",
118
+ category: "automated",
119
+ ago: 5 * HOUR,
120
+ hasAttachment: true,
121
+ }),
122
+ ];
123
+
124
+ const drafts: RemitImapOutboxMessageResponse[] = [
125
+ {
126
+ ...outboxMessage({
127
+ id: "misspelt",
128
+ accountId: WORK,
129
+ to: "ada@acme.example",
130
+ subject: "Report",
131
+ body: "Ths report is redy today, and the notes are attachd.",
132
+ status: "draft",
133
+ ago: 5 * MINUTE,
134
+ }),
135
+ htmlBody: MISSPELT_DRAFT,
136
+ },
137
+ outboxMessage({
138
+ id: "plain",
139
+ accountId: WORK,
140
+ to: "ada@acme.example",
141
+ subject: "Re: Q3 planning",
142
+ body: PLAIN_DRAFT,
143
+ status: "draft",
144
+ ago: 6 * MINUTE,
145
+ }),
146
+ {
147
+ ...outboxMessage({
148
+ id: "long-reply",
149
+ accountId: WORK,
150
+ to: "ada@acme.example",
151
+ subject: `Re: ${LUNCH_SUBJECT}`,
152
+ body: "That works.",
153
+ status: "draft",
154
+ ago: 7 * MINUTE,
155
+ }),
156
+ htmlBody: longDraft,
157
+ },
158
+ ];
159
+
160
+ export const composeWorld = (overrides: Partial<MailWorld> = {}): MailWorld => {
161
+ const base = mailWorld();
162
+ return {
163
+ ...base,
164
+ accounts: base.accounts.map((account) =>
165
+ account.accountId === WORK
166
+ ? { ...account, composeLanguages: ["en", "nl", "de"] }
167
+ : account,
168
+ ),
169
+ threads: [...base.threads, ...conversations],
170
+ outbox: [...base.outbox, ...drafts],
171
+ bodies: { "msg-billing": longBody },
172
+ ...overrides,
173
+ };
174
+ };
175
+
176
+ export const withoutSmtp = (world: MailWorld): MailWorld => ({
177
+ ...world,
178
+ accounts: world.accounts.map((account) => ({
179
+ ...account,
180
+ smtpEnabled: false,
181
+ })),
182
+ });
183
+
184
+ export const messageUrl = (id: string, mailboxId: string = INBOX): string =>
185
+ `/mail/${mailboxId}/thread-${id}/msg-${id}`;
186
+
187
+ export const threadUrl = (
188
+ threadId: string,
189
+ messageId: string,
190
+ mailboxId: string = INBOX,
191
+ ): string => `/mail/${mailboxId}/thread-${threadId}/msg-${messageId}`;
@@ -2,19 +2,30 @@ import type {
2
2
  AccountDetailOperationsUpdateAccountResponse,
3
3
  AddressDetailOperationsUpdateAddressResponse,
4
4
  AddressOperationsSearchAddressesResponse,
5
+ CalendarEventOperationsListCalendarEventsResponse,
6
+ CalendarOperationsListCalendarsResponse,
7
+ CalendarSuggestionOperationsListCalendarSuggestionsResponse,
5
8
  FilterOperationsListFiltersResponse,
6
9
  LabelOperationsListLabelsResponse,
7
10
  MailboxDetailOperationsRenameMailboxResponse,
8
11
  MailboxOperationsListMailboxesResponse,
9
12
  MeOperationsListQuarantineResponse,
10
13
  MeOperationsListVipSuggestionsResponse,
14
+ MessageBulkOperationsDeleteMessagesData,
15
+ MessageBulkOperationsDeleteMessagesResponse,
16
+ MessageBulkOperationsMoveMessagesData,
17
+ MessageBulkOperationsMoveMessagesResponse,
11
18
  MessageBulkOperationsUpdateFlagsResponse,
12
19
  MessageCalendarSuggestionOperationsListMessageCalendarSuggestionsResponse,
20
+ OrganizeOperationsPreviewOrganizeResponse,
13
21
  OutboxOperationsListOutboxMessagesResponse,
14
22
  RemitImapConfigDescriptionResponse,
23
+ RemitImapCreateOutboxMessageInput,
15
24
  RemitImapDescribeMessageResponse,
25
+ RemitImapOutboxMessageResponse,
16
26
  RemitImapThreadMessageResponse,
17
27
  RemitImapThreadSearchResponse,
28
+ RemitImapUpdateOutboxMessageInput,
18
29
  SemanticSearchOperationsSemanticSearchResponse,
19
30
  SyncOperationsGetSyncStatusResponse,
20
31
  SyncOperationsTriggerSyncResponse,
@@ -32,11 +43,45 @@ export interface MailHandlerOptions {
32
43
  withholdCounts?: boolean;
33
44
  pageSize?: number;
34
45
  holdLaterPages?: boolean;
46
+ draftSave?: "saves" | "refused";
47
+ send?: "sends" | "holds";
48
+ search?: "answers" | "holds";
49
+ config?: "answers" | "holds";
35
50
  }
36
51
 
37
52
  const contentPath = (row: RemitImapThreadMessageResponse): string =>
38
53
  `/content/accounts/${row.accountConfigId}/${row.accountId}/messages/${row.messageId}/parts/1`;
39
54
 
55
+ const attachmentOnly = (row: RemitImapThreadMessageResponse): boolean =>
56
+ row.hasAttachment && !row.snippet;
57
+
58
+ const bodyPartsOf = (
59
+ row: RemitImapThreadMessageResponse,
60
+ ): RemitImapDescribeMessageResponse["bodyParts"] =>
61
+ attachmentOnly(row)
62
+ ? [
63
+ {
64
+ bodyPartId: `part-${row.messageId}`,
65
+ mediaType: "APPLICATION",
66
+ mediaSubtype: "pdf",
67
+ sizeOctets: 48_000,
68
+ isMultipart: false,
69
+ disposition: "attachment",
70
+ dispositionFilename: "scan.pdf",
71
+ contentUrl: contentPath(row),
72
+ },
73
+ ]
74
+ : [
75
+ {
76
+ bodyPartId: `part-${row.messageId}`,
77
+ mediaType: "TEXT",
78
+ mediaSubtype: "plain",
79
+ sizeOctets: (row.snippet ?? "").length,
80
+ isMultipart: false,
81
+ contentUrl: contentPath(row),
82
+ },
83
+ ];
84
+
40
85
  const describe = (
41
86
  row: RemitImapThreadMessageResponse,
42
87
  ): RemitImapDescribeMessageResponse => ({
@@ -71,16 +116,7 @@ const describe = (
71
116
  senderTrust: row.senderTrust,
72
117
  },
73
118
  flags: row.isRead ? ["\\Seen"] : [],
74
- bodyParts: [
75
- {
76
- bodyPartId: `part-${row.messageId}`,
77
- mediaType: "TEXT",
78
- mediaSubtype: "plain",
79
- sizeOctets: (row.snippet ?? "").length,
80
- isMultipart: false,
81
- contentUrl: contentPath(row),
82
- },
83
- ],
119
+ bodyParts: bodyPartsOf(row),
84
120
  references: [],
85
121
  });
86
122
 
@@ -167,11 +203,23 @@ export const mailHandlers = (
167
203
  ),
168
204
  );
169
205
 
206
+ const outbox = new Map(
207
+ world.outbox.map((message) => [message.outboxMessageId, message]),
208
+ );
209
+ let draftsCreated = 0;
210
+ const refuseSave = () =>
211
+ HttpResponse.json(
212
+ { status: 409, message: "The mail server did not accept the draft." },
213
+ { status: 409 },
214
+ );
215
+
170
216
  return [
171
217
  http.get(`${API}/config`, () =>
172
- HttpResponse.json<RemitImapConfigDescriptionResponse>(
173
- makeConfig(world.accounts),
174
- ),
218
+ options.config === "holds"
219
+ ? pending()
220
+ : HttpResponse.json<RemitImapConfigDescriptionResponse>(
221
+ makeConfig(world.accounts),
222
+ ),
175
223
  ),
176
224
  http.patch(
177
225
  `${API}/accounts/:accountId/mailboxes/:mailboxId`,
@@ -277,11 +325,38 @@ export const mailHandlers = (
277
325
  failureCount: 0,
278
326
  }),
279
327
  ),
328
+ http.post(`${API}/messages/delete`, async ({ request }) => {
329
+ const { messageIds } =
330
+ (await request.json()) as MessageBulkOperationsDeleteMessagesData["body"];
331
+ return HttpResponse.json<MessageBulkOperationsDeleteMessagesResponse>({
332
+ successCount: messageIds.length,
333
+ failureCount: 0,
334
+ });
335
+ }),
336
+ http.post(`${API}/messages/move`, async ({ request }) => {
337
+ const { messageIds } =
338
+ (await request.json()) as MessageBulkOperationsMoveMessagesData["body"];
339
+ return HttpResponse.json<MessageBulkOperationsMoveMessagesResponse>({
340
+ successCount: messageIds.length,
341
+ failureCount: 0,
342
+ });
343
+ }),
344
+ http.post(`${API}/accounts/:accountId/organize/preview`, ({ params }) => {
345
+ const matched = world.threads.filter(
346
+ (row) => row.accountId === params.accountId,
347
+ );
348
+ return HttpResponse.json<OrganizeOperationsPreviewOrganizeResponse>({
349
+ matchedCount: matched.length,
350
+ messageIds: matched.map((row) => row.messageId),
351
+ });
352
+ }),
280
353
  http.get(
281
354
  "/content/accounts/:accountConfigId/:accountId/messages/:messageId/parts/*",
282
355
  ({ params }) =>
283
356
  HttpResponse.text(
284
- byMessageId.get(String(params.messageId))?.snippet ?? "",
357
+ world.bodies[String(params.messageId)] ??
358
+ byMessageId.get(String(params.messageId))?.snippet ??
359
+ "",
285
360
  ),
286
361
  ),
287
362
  http.get(`${API}/threads/:threadId/messages`, ({ params }) =>
@@ -300,21 +375,25 @@ export const mailHandlers = (
300
375
  );
301
376
  if (options.holdLaterPages && params.has("continuationToken"))
302
377
  return pending();
378
+ if (searching && options.search === "holds") return pending();
303
379
  return HttpResponse.json(listing(narrow(scope, params), params, options));
304
380
  }),
305
381
  http.get(
306
382
  `${API}/mailboxes/:mailboxId/threads/search`,
307
- ({ request, params }) =>
308
- HttpResponse.json(
383
+ ({ request, params }) => {
384
+ const search = new URL(request.url).searchParams;
385
+ if (search.has("query") && options.search === "holds") return pending();
386
+ return HttpResponse.json(
309
387
  listing(
310
388
  narrow(
311
389
  world.threads.filter((row) => row.mailboxId === params.mailboxId),
312
- new URL(request.url).searchParams,
390
+ search,
313
391
  ),
314
- new URL(request.url).searchParams,
392
+ search,
315
393
  options,
316
394
  ),
317
- ),
395
+ );
396
+ },
318
397
  ),
319
398
  http.get(`${API}/mailboxes/:mailboxId/threads`, ({ params }) =>
320
399
  HttpResponse.json<ThreadOperationsListThreadsResponse>({
@@ -325,14 +404,108 @@ export const mailHandlers = (
325
404
  ),
326
405
  http.get(`${API}/outbox`, () =>
327
406
  HttpResponse.json<OutboxOperationsListOutboxMessagesResponse>({
328
- items: world.outbox,
407
+ items: [...outbox.values()],
329
408
  }),
330
409
  ),
331
- http.get(`${API}/search/semantic`, () =>
332
- HttpResponse.json<SemanticSearchOperationsSemanticSearchResponse>({
410
+ http.post(`${API}/outbox`, async ({ request }) => {
411
+ if (options.draftSave === "refused") return refuseSave();
412
+ const input = (await request.json()) as RemitImapCreateOutboxMessageInput;
413
+ draftsCreated += 1;
414
+ const account = world.accounts.find(
415
+ (candidate) => candidate.accountId === input.accountId,
416
+ );
417
+ const created: RemitImapOutboxMessageResponse = {
418
+ outboxMessageId: `out-created-${draftsCreated}`,
419
+ accountId: input.accountId,
420
+ fromAddress: account?.email ?? "",
421
+ fromName: account?.displayName,
422
+ toAddresses: input.toAddresses,
423
+ ccAddresses: input.ccAddresses ?? [],
424
+ bccAddresses: input.bccAddresses ?? [],
425
+ subject: input.subject,
426
+ textBody: input.textBody,
427
+ htmlBody: input.htmlBody,
428
+ references: input.references ?? [],
429
+ status: "draft",
430
+ createdAt: Date.UTC(2026, 8, 24, 13, 0),
431
+ updatedAt: Date.UTC(2026, 8, 24, 13, 0),
432
+ attachments: [],
433
+ };
434
+ outbox.set(created.outboxMessageId, created);
435
+ return HttpResponse.json<RemitImapOutboxMessageResponse>(created);
436
+ }),
437
+ http.get(`${API}/outbox/:outboxMessageId`, ({ params }) => {
438
+ const message = outbox.get(String(params.outboxMessageId));
439
+ if (!message)
440
+ return HttpResponse.json(
441
+ { status: 404, message: "Outbox message not found" },
442
+ { status: 404 },
443
+ );
444
+ return HttpResponse.json<RemitImapOutboxMessageResponse>(message);
445
+ }),
446
+ http.patch(
447
+ `${API}/outbox/:outboxMessageId`,
448
+ async ({ params, request }) => {
449
+ if (options.draftSave === "refused") return refuseSave();
450
+ const id = String(params.outboxMessageId);
451
+ const current = outbox.get(id);
452
+ if (!current)
453
+ return HttpResponse.json(
454
+ { status: 404, message: "Outbox message not found" },
455
+ { status: 404 },
456
+ );
457
+ const input =
458
+ (await request.json()) as RemitImapUpdateOutboxMessageInput;
459
+ const { attachmentIds: _attachmentIds, ...fields } = input;
460
+ const updated: RemitImapOutboxMessageResponse = {
461
+ ...current,
462
+ ...fields,
463
+ status: "draft",
464
+ };
465
+ outbox.set(id, updated);
466
+ return HttpResponse.json<RemitImapOutboxMessageResponse>(updated);
467
+ },
468
+ ),
469
+ http.post(`${API}/outbox/:outboxMessageId/send`, ({ params }) => {
470
+ if (options.send === "holds") return pending();
471
+ const id = String(params.outboxMessageId);
472
+ const current = outbox.get(id);
473
+ if (!current)
474
+ return HttpResponse.json(
475
+ { status: 404, message: "Outbox message not found" },
476
+ { status: 404 },
477
+ );
478
+ const queued: RemitImapOutboxMessageResponse = {
479
+ ...current,
480
+ status: "queued",
481
+ };
482
+ outbox.set(id, queued);
483
+ return HttpResponse.json<RemitImapOutboxMessageResponse>(queued);
484
+ }),
485
+ http.delete(`${API}/outbox/:outboxMessageId`, ({ params }) => {
486
+ outbox.delete(String(params.outboxMessageId));
487
+ return new HttpResponse(null, { status: 204 });
488
+ }),
489
+ http.get(`${API}/calendars`, () =>
490
+ HttpResponse.json<CalendarOperationsListCalendarsResponse>({ items: [] }),
491
+ ),
492
+ http.get(`${API}/calendar-events`, () =>
493
+ HttpResponse.json<CalendarEventOperationsListCalendarEventsResponse>({
333
494
  items: [],
334
495
  }),
335
496
  ),
497
+ http.get(`${API}/calendar-suggestions`, () =>
498
+ HttpResponse.json<CalendarSuggestionOperationsListCalendarSuggestionsResponse>(
499
+ { items: [] },
500
+ ),
501
+ ),
502
+ http.get(`${API}/search/semantic`, () =>
503
+ options.search === "holds"
504
+ ? pending()
505
+ : HttpResponse.json<SemanticSearchOperationsSemanticSearchResponse>({
506
+ items: [],
507
+ }),
508
+ ),
336
509
  http.get(`${API}/me/quarantine`, () =>
337
510
  HttpResponse.json<MeOperationsListQuarantineResponse>({
338
511
  entries: world.quarantine,
@@ -26,11 +26,12 @@ export interface MailWorld {
26
26
  vipSuggestions: RemitImapVipSuggestionEntry[];
27
27
  addresses: RemitImapAddressResponse[];
28
28
  systemUpdate: RemitImapSystemUpdateResponse;
29
+ bodies: Record<string, string>;
29
30
  }
30
31
 
31
- const MINUTE = 60_000;
32
- const HOUR = 60 * MINUTE;
33
- const DAY = 24 * HOUR;
32
+ export const MINUTE = 60_000;
33
+ export const HOUR = 60 * MINUTE;
34
+ export const DAY = 24 * HOUR;
34
35
  export const NOW = Date.UTC(2026, 8, 24, 13, 0);
35
36
 
36
37
  const ROLES = [
@@ -93,8 +94,9 @@ const accounts = [
93
94
  accountWithFolders(WORK, "alice.tan@acme.example", "Work"),
94
95
  ];
95
96
 
96
- interface ThreadSeed {
97
+ export interface ThreadSeed {
97
98
  id: string;
99
+ inThread?: string;
98
100
  accountId: string;
99
101
  role?: RemitImapCanonicalMailboxRole;
100
102
  fromName: string;
@@ -108,10 +110,10 @@ interface ThreadSeed {
108
110
  hasAttachment?: boolean;
109
111
  }
110
112
 
111
- const thread = (seed: ThreadSeed): RemitImapThreadMessageResponse =>
113
+ export const threadRow = (seed: ThreadSeed): RemitImapThreadMessageResponse =>
112
114
  makeThreadMessage({
113
115
  messageId: `msg-${seed.id}`,
114
- threadId: `thread-${seed.id}`,
116
+ threadId: `thread-${seed.inThread ?? seed.id}`,
115
117
  threadMessageId: `tm-${seed.id}`,
116
118
  accountId: seed.accountId,
117
119
  mailboxId: mailboxIdFor(seed.accountId, seed.role ?? "Inbox"),
@@ -129,7 +131,7 @@ const thread = (seed: ThreadSeed): RemitImapThreadMessageResponse =>
129
131
  });
130
132
 
131
133
  const inboxThreads: RemitImapThreadMessageResponse[] = [
132
- thread({
134
+ threadRow({
133
135
  id: "q3-planning",
134
136
  accountId: WORK,
135
137
  fromName: "Ada Lovelace",
@@ -140,7 +142,7 @@ const inboxThreads: RemitImapThreadMessageResponse[] = [
140
142
  ago: 20 * MINUTE,
141
143
  starred: true,
142
144
  }),
143
- thread({
145
+ threadRow({
144
146
  id: "lease",
145
147
  accountId: PERSONAL,
146
148
  fromName: "Amara Okafor",
@@ -152,7 +154,7 @@ const inboxThreads: RemitImapThreadMessageResponse[] = [
152
154
  hasAttachment: true,
153
155
  starred: true,
154
156
  }),
155
- thread({
157
+ threadRow({
156
158
  id: "dinner",
157
159
  accountId: PERSONAL,
158
160
  fromName: "Grace Hopper",
@@ -163,7 +165,7 @@ const inboxThreads: RemitImapThreadMessageResponse[] = [
163
165
  ago: 5 * HOUR,
164
166
  isRead: true,
165
167
  }),
166
- thread({
168
+ threadRow({
167
169
  id: "invoice",
168
170
  accountId: WORK,
169
171
  fromName: "Billing",
@@ -175,7 +177,7 @@ const inboxThreads: RemitImapThreadMessageResponse[] = [
175
177
  isRead: true,
176
178
  starred: true,
177
179
  }),
178
- thread({
180
+ threadRow({
179
181
  id: "shipping",
180
182
  accountId: PERSONAL,
181
183
  fromName: "Parcel Service",
@@ -185,7 +187,7 @@ const inboxThreads: RemitImapThreadMessageResponse[] = [
185
187
  category: "transactional",
186
188
  ago: 9 * HOUR,
187
189
  }),
188
- thread({
190
+ threadRow({
189
191
  id: "weekly-digest",
190
192
  accountId: PERSONAL,
191
193
  fromName: "The Weekly",
@@ -195,7 +197,7 @@ const inboxThreads: RemitImapThreadMessageResponse[] = [
195
197
  category: "newsletter",
196
198
  ago: DAY,
197
199
  }),
198
- thread({
200
+ threadRow({
199
201
  id: "sale",
200
202
  accountId: PERSONAL,
201
203
  fromName: "Outdoor Store",
@@ -205,7 +207,7 @@ const inboxThreads: RemitImapThreadMessageResponse[] = [
205
207
  category: "marketing",
206
208
  ago: DAY + 3 * HOUR,
207
209
  }),
208
- thread({
210
+ threadRow({
209
211
  id: "mention",
210
212
  accountId: WORK,
211
213
  fromName: "Chat",
@@ -215,7 +217,7 @@ const inboxThreads: RemitImapThreadMessageResponse[] = [
215
217
  category: "social",
216
218
  ago: DAY + 5 * HOUR,
217
219
  }),
218
- thread({
220
+ threadRow({
219
221
  id: "build",
220
222
  accountId: WORK,
221
223
  fromName: "CI",
@@ -226,7 +228,7 @@ const inboxThreads: RemitImapThreadMessageResponse[] = [
226
228
  ago: 2 * DAY,
227
229
  isRead: true,
228
230
  }),
229
- thread({
231
+ threadRow({
230
232
  id: "talk-outline",
231
233
  accountId: PERSONAL,
232
234
  role: "Archive",
@@ -242,7 +244,7 @@ const inboxThreads: RemitImapThreadMessageResponse[] = [
242
244
  ];
243
245
 
244
246
  const serverDrafts: RemitImapThreadMessageResponse[] = [
245
- thread({
247
+ threadRow({
246
248
  id: "draft-talk",
247
249
  accountId: PERSONAL,
248
250
  role: "Drafts",
@@ -376,7 +378,7 @@ export const newsletterBacklog = (
376
378
  count: number,
377
379
  ): RemitImapThreadMessageResponse[] =>
378
380
  Array.from({ length: count }, (_, index) =>
379
- thread({
381
+ threadRow({
380
382
  id: `digest-${index}`,
381
383
  accountId: PERSONAL,
382
384
  fromName: "The Weekly",
@@ -388,6 +390,29 @@ export const newsletterBacklog = (
388
390
  }),
389
391
  );
390
392
 
393
+ export const senderAddress = (
394
+ row: RemitImapThreadMessageResponse,
395
+ ): RemitImapAddressResponse => {
396
+ const email = (row.fromEmail ?? "").toLowerCase();
397
+ const [localPart = "", domain = ""] = email.split("@");
398
+ return {
399
+ addressId: `addr-${email}`,
400
+ accountConfigId: row.accountConfigId,
401
+ displayName: row.fromName,
402
+ localPart,
403
+ domain,
404
+ normalizedEmail: email,
405
+ flags: {},
406
+ inboundCount: 1,
407
+ outboundCount: 0,
408
+ replyCount: 0,
409
+ lastInboundAt: row.sentDate,
410
+ lastReplyAt: 0,
411
+ createdAt: row.sentDate,
412
+ updatedAt: row.sentDate,
413
+ };
414
+ };
415
+
391
416
  export const mailWorld = (overrides: Partial<MailWorld> = {}): MailWorld => ({
392
417
  accounts,
393
418
  mailboxes: (overrides.accounts ?? accounts).flatMap((account) =>
@@ -403,5 +428,6 @@ export const mailWorld = (overrides: Partial<MailWorld> = {}): MailWorld => ({
403
428
  check: { status: "disabled" },
404
429
  run: null,
405
430
  },
431
+ bodies: {},
406
432
  ...overrides,
407
433
  });