@remit/web-client 0.0.303 → 0.0.305
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/package.json +1 -1
- package/src/components/compose/compose.stories.tsx +374 -0
- package/src/components/layout/ComposeFab.tsx +2 -15
- package/src/components/layout/mail-shell.stories.tsx +142 -0
- package/src/components/mail/detail-surface.stories.tsx +171 -0
- package/src/components/mail/mail-pickers.stories.tsx +82 -0
- package/src/components/mail/search.stories.tsx +280 -0
- package/src/components/mail/selection-wizard.stories.tsx +160 -0
- package/src/components/onboarding/OnboardingWizard.flow.test.ts +222 -2
- package/src/components/onboarding/OnboardingWizard.tsx +262 -15
- package/src/components/onboarding/onboarding.stories.tsx +90 -0
- package/src/components/settings/AccountServices.render.test.ts +4 -1
- package/src/components/settings/accounts.stories.tsx +5 -0
- package/src/mocks/story-frame/compose-world.ts +191 -0
- package/src/mocks/story-frame/mail-handlers.ts +195 -22
- package/src/mocks/story-frame/mail-world.ts +44 -18
- package/src/routes/settings/-accounts.test.ts +70 -2
- package/src/routes/settings/accounts.tsx +87 -7
|
@@ -458,3 +458,93 @@ export const SyncStalled: Story = {
|
|
|
458
458
|
);
|
|
459
459
|
},
|
|
460
460
|
};
|
|
461
|
+
|
|
462
|
+
const OUTLOOK = makeAccount({
|
|
463
|
+
accountId: "acc-outlook",
|
|
464
|
+
email: "alice@outlook.com",
|
|
465
|
+
authType: "oauthMicrosoft",
|
|
466
|
+
syncedServices: ["Calendar"],
|
|
467
|
+
grantedScopes: ["https://graph.microsoft.com/Calendars.Read"],
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
const withOutlook = mailWorld({
|
|
471
|
+
accounts: [...mailWorld().accounts, OUTLOOK],
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
export const MicrosoftServiceChoice: Story = {
|
|
475
|
+
play: async ({ canvasElement }) => {
|
|
476
|
+
const canvas = within(canvasElement);
|
|
477
|
+
await toMicrosoft(canvas);
|
|
478
|
+
await expect(canvas.getByRole("checkbox", { name: /^Mail/ })).toBeChecked();
|
|
479
|
+
await expect(
|
|
480
|
+
canvas.getByRole("checkbox", { name: /^Calendar/ }),
|
|
481
|
+
).toBeChecked();
|
|
482
|
+
},
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
export const MicrosoftNothingPicked: Story = {
|
|
486
|
+
play: async ({ canvasElement }) => {
|
|
487
|
+
const canvas = within(canvasElement);
|
|
488
|
+
const signIn = await toMicrosoft(canvas);
|
|
489
|
+
await userEvent.click(canvas.getByRole("checkbox", { name: /^Mail/ }));
|
|
490
|
+
await userEvent.click(canvas.getByRole("checkbox", { name: /^Calendar/ }));
|
|
491
|
+
await userEvent.click(signIn);
|
|
492
|
+
await expect(await canvas.findByRole("alert")).toHaveTextContent(
|
|
493
|
+
"Pick at least one",
|
|
494
|
+
);
|
|
495
|
+
await expect(signIn).toBeEnabled();
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
export const MicrosoftGranted: Story = {
|
|
500
|
+
args: { url: `/settings/accounts?connected=${OUTLOOK.accountId}` },
|
|
501
|
+
parameters: handlers({}, withOutlook),
|
|
502
|
+
play: async ({ canvasElement }) => {
|
|
503
|
+
const canvas = within(canvasElement);
|
|
504
|
+
await expect(
|
|
505
|
+
await canvas.findByText(`Connected ${OUTLOOK.email}`),
|
|
506
|
+
).toBeVisible();
|
|
507
|
+
await expect(canvas.getByText("Access granted — syncing")).toBeVisible();
|
|
508
|
+
await expect(
|
|
509
|
+
canvas.getByRole("button", { name: "Continue" }),
|
|
510
|
+
).toBeEnabled();
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
export const MicrosoftScopeNotGranted: Story = {
|
|
515
|
+
args: {
|
|
516
|
+
url: "/settings/accounts?oauthError=scope_not_granted&oauthEmail=bob%40outlook.com&missingServices=Calendar",
|
|
517
|
+
},
|
|
518
|
+
parameters: handlers({}, mailWorld()),
|
|
519
|
+
play: async ({ canvasElement }) => {
|
|
520
|
+
const canvas = within(canvasElement);
|
|
521
|
+
await expect(
|
|
522
|
+
await canvas.findByText(/did not grant access to Calendar/),
|
|
523
|
+
).toBeVisible();
|
|
524
|
+
await expect(canvas.getByLabelText("Email address (optional)")).toHaveValue(
|
|
525
|
+
"bob@outlook.com",
|
|
526
|
+
);
|
|
527
|
+
await expect(
|
|
528
|
+
canvas.getByRole("button", { name: "Sign in with Microsoft" }),
|
|
529
|
+
).toBeEnabled();
|
|
530
|
+
},
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
export const MicrosoftReconnectScopeNotGranted: Story = {
|
|
534
|
+
args: {
|
|
535
|
+
url: `/settings/accounts?oauthError=scope_not_granted&oauthEmail=${encodeURIComponent(OUTLOOK.email)}&missingServices=Calendar`,
|
|
536
|
+
},
|
|
537
|
+
parameters: handlers({}, withOutlook),
|
|
538
|
+
play: async ({ canvasElement }) => {
|
|
539
|
+
const canvas = within(canvasElement);
|
|
540
|
+
await expect(
|
|
541
|
+
await canvas.findByText(`Reconnect ${OUTLOOK.email}`),
|
|
542
|
+
).toBeVisible();
|
|
543
|
+
await expect(
|
|
544
|
+
canvas.getByText(/did not grant access to Calendar/),
|
|
545
|
+
).toBeVisible();
|
|
546
|
+
await expect(canvas.getByLabelText("Email address (optional)")).toHaveValue(
|
|
547
|
+
OUTLOOK.email,
|
|
548
|
+
);
|
|
549
|
+
},
|
|
550
|
+
};
|
|
@@ -130,7 +130,10 @@ describe("AccountServices", () => {
|
|
|
130
130
|
const mounted = await mount(outlook());
|
|
131
131
|
|
|
132
132
|
mounted.click(switchFor(mounted, "calendar"));
|
|
133
|
-
await mounted.waitFor(
|
|
133
|
+
await mounted.waitFor(
|
|
134
|
+
() => patches().length > 0 && switchFor(mounted, "calendar").disabled,
|
|
135
|
+
"the save to start",
|
|
136
|
+
);
|
|
134
137
|
|
|
135
138
|
assert.equal(checked(mounted, "calendar"), "true");
|
|
136
139
|
assert.equal(switchFor(mounted, "mail").disabled, true);
|
|
@@ -199,6 +199,11 @@ export const AccountsOauthSuccess: Story = {
|
|
|
199
199
|
args: { url: "/settings/accounts?connected=acc-personal" },
|
|
200
200
|
play: async ({ canvasElement }) => {
|
|
201
201
|
const canvas = within(canvasElement);
|
|
202
|
+
await expect(await canvas.findByText(/^Connected /)).toBeVisible();
|
|
203
|
+
await expect(canvas.getByText(/Access granted/)).toBeVisible();
|
|
204
|
+
await userEvent.click(
|
|
205
|
+
canvas.getByRole("button", { name: /Go to inbox|Continue/ }),
|
|
206
|
+
);
|
|
202
207
|
await expect(
|
|
203
208
|
await canvas.findByText("Account connected successfully."),
|
|
204
209
|
).toBeVisible();
|
|
@@ -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
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
390
|
+
search,
|
|
313
391
|
),
|
|
314
|
-
|
|
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:
|
|
407
|
+
items: [...outbox.values()],
|
|
329
408
|
}),
|
|
330
409
|
),
|
|
331
|
-
http.
|
|
332
|
-
|
|
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,
|