@remit/web-client 0.0.193 → 0.0.194
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/web-client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.194",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Remit web client, published as composable primitives — the app shell, auth shells, and runtime config. A distributor imports what it composes and bundles it.",
|
|
6
6
|
"exports": {
|
|
@@ -2,7 +2,6 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import { describe, it } from "node:test";
|
|
3
3
|
import {
|
|
4
4
|
advanceMove,
|
|
5
|
-
appointedRole,
|
|
6
5
|
beginMove,
|
|
7
6
|
excludeFolder,
|
|
8
7
|
type FolderNode,
|
|
@@ -11,6 +10,7 @@ import {
|
|
|
11
10
|
hasChildFolders,
|
|
12
11
|
initialStage,
|
|
13
12
|
moveProgressLabel,
|
|
13
|
+
vouchedRole,
|
|
14
14
|
} from "./delete-folder.js";
|
|
15
15
|
|
|
16
16
|
const folder = (
|
|
@@ -36,13 +36,74 @@ describe("guardFolderDeletion", () => {
|
|
|
36
36
|
const guard = guardFolderDeletion(
|
|
37
37
|
target,
|
|
38
38
|
[target],
|
|
39
|
-
[{ role: "Archive", mailboxId: "arch" }],
|
|
39
|
+
[{ role: "Archive", source: "Appointed", mailboxId: "arch" }],
|
|
40
40
|
);
|
|
41
41
|
assert.equal(guard.deletable, false);
|
|
42
42
|
assert.equal(guard.reason, "role");
|
|
43
43
|
assert.match(guard.message ?? "", /Archive/);
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
+
it("blocks a folder the mail server flagged for the role", () => {
|
|
47
|
+
const target = folder({ mailboxId: "sent", fullPath: "Sent" });
|
|
48
|
+
const guard = guardFolderDeletion(
|
|
49
|
+
target,
|
|
50
|
+
[target],
|
|
51
|
+
[{ role: "Sent", source: "Flagged", mailboxId: "sent" }],
|
|
52
|
+
);
|
|
53
|
+
assert.equal(guard.deletable, false);
|
|
54
|
+
assert.equal(guard.reason, "role");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("deletes a folder whose only role claim is a name proposal", () => {
|
|
58
|
+
const target = folder({ mailboxId: "arch", fullPath: "Archive" });
|
|
59
|
+
const guard = guardFolderDeletion(
|
|
60
|
+
target,
|
|
61
|
+
[target],
|
|
62
|
+
[{ role: "Archive", source: "Proposed", mailboxId: "arch" }],
|
|
63
|
+
);
|
|
64
|
+
assert.deepEqual(guard, { deletable: true });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("deletes the name-proposed fallback a stale appointment resolves to", () => {
|
|
68
|
+
const target = folder({ mailboxId: "deleted-items", fullPath: "Deleted" });
|
|
69
|
+
const guard = guardFolderDeletion(
|
|
70
|
+
target,
|
|
71
|
+
[target],
|
|
72
|
+
[{ role: "Trash", source: "Stale", mailboxId: "deleted-items" }],
|
|
73
|
+
);
|
|
74
|
+
assert.deepEqual(guard, { deletable: true });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("blocks the flagged folder a stale appointment falls back to", () => {
|
|
78
|
+
const target = folder({
|
|
79
|
+
mailboxId: "server-trash",
|
|
80
|
+
fullPath: "Trash",
|
|
81
|
+
specialUse: ["Trash"],
|
|
82
|
+
});
|
|
83
|
+
const guard = guardFolderDeletion(
|
|
84
|
+
target,
|
|
85
|
+
[target],
|
|
86
|
+
[{ role: "Trash", source: "Stale", mailboxId: "server-trash" }],
|
|
87
|
+
);
|
|
88
|
+
assert.equal(guard.deletable, false);
|
|
89
|
+
assert.equal(guard.reason, "role");
|
|
90
|
+
assert.match(guard.message ?? "", /Trash/);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("reads the flag for the role the entry names, not any flag the folder has", () => {
|
|
94
|
+
const target = folder({
|
|
95
|
+
mailboxId: "archive",
|
|
96
|
+
fullPath: "Archive",
|
|
97
|
+
specialUse: ["Archive"],
|
|
98
|
+
});
|
|
99
|
+
const guard = guardFolderDeletion(
|
|
100
|
+
target,
|
|
101
|
+
[target],
|
|
102
|
+
[{ role: "Trash", source: "Stale", mailboxId: "archive" }],
|
|
103
|
+
);
|
|
104
|
+
assert.deepEqual(guard, { deletable: true });
|
|
105
|
+
});
|
|
106
|
+
|
|
46
107
|
it("blocks a folder that has subfolders", () => {
|
|
47
108
|
const target = folder({ mailboxId: "work", fullPath: "Work" });
|
|
48
109
|
const child = folder({ mailboxId: "sub", fullPath: "Work/Shop" });
|
|
@@ -62,14 +123,18 @@ describe("guardFolderDeletion", () => {
|
|
|
62
123
|
const guard = guardFolderDeletion(
|
|
63
124
|
target,
|
|
64
125
|
[target, other],
|
|
65
|
-
[{ role: "Archive", mailboxId: "other-box" }],
|
|
126
|
+
[{ role: "Archive", source: "Appointed", mailboxId: "other-box" }],
|
|
66
127
|
);
|
|
67
128
|
assert.deepEqual(guard, { deletable: true });
|
|
68
129
|
});
|
|
69
130
|
|
|
70
131
|
it("ignores an unfilled role appointment with no mailbox", () => {
|
|
71
132
|
const target = folder({ mailboxId: "r", fullPath: "Receipts" });
|
|
72
|
-
const guard = guardFolderDeletion(
|
|
133
|
+
const guard = guardFolderDeletion(
|
|
134
|
+
target,
|
|
135
|
+
[target],
|
|
136
|
+
[{ role: "Junk", source: "None" }],
|
|
137
|
+
);
|
|
73
138
|
assert.equal(guard.deletable, true);
|
|
74
139
|
});
|
|
75
140
|
});
|
|
@@ -106,20 +171,42 @@ describe("hasChildFolders", () => {
|
|
|
106
171
|
});
|
|
107
172
|
});
|
|
108
173
|
|
|
109
|
-
describe("
|
|
174
|
+
describe("vouchedRole", () => {
|
|
110
175
|
it("returns the role a mailbox fills", () => {
|
|
111
176
|
assert.equal(
|
|
112
|
-
|
|
177
|
+
vouchedRole({ mailboxId: "a" }, [
|
|
178
|
+
{ role: "Trash", source: "Appointed", mailboxId: "a" },
|
|
179
|
+
]),
|
|
113
180
|
"Trash",
|
|
114
181
|
);
|
|
115
182
|
});
|
|
116
183
|
|
|
117
184
|
it("returns undefined when the mailbox fills no role", () => {
|
|
118
185
|
assert.equal(
|
|
119
|
-
|
|
186
|
+
vouchedRole({ mailboxId: "a" }, [
|
|
187
|
+
{ role: "Trash", source: "Appointed", mailboxId: "b" },
|
|
188
|
+
]),
|
|
189
|
+
undefined,
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("returns undefined for a role nobody vouched for", () => {
|
|
194
|
+
assert.equal(
|
|
195
|
+
vouchedRole({ mailboxId: "a" }, [
|
|
196
|
+
{ role: "Trash", source: "Proposed", mailboxId: "a" },
|
|
197
|
+
]),
|
|
120
198
|
undefined,
|
|
121
199
|
);
|
|
122
200
|
});
|
|
201
|
+
|
|
202
|
+
it("returns the role a stale entry falls back to when the server flags it", () => {
|
|
203
|
+
assert.equal(
|
|
204
|
+
vouchedRole({ mailboxId: "a", specialUse: ["Trash"] }, [
|
|
205
|
+
{ role: "Trash", source: "Stale", mailboxId: "a" },
|
|
206
|
+
]),
|
|
207
|
+
"Trash",
|
|
208
|
+
);
|
|
209
|
+
});
|
|
123
210
|
});
|
|
124
211
|
|
|
125
212
|
describe("excludeFolder", () => {
|
package/src/lib/delete-folder.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { RemitImapFolderAppointment } from "@remit/api-http-client/types.gen.ts";
|
|
2
|
+
|
|
1
3
|
export const MOVE_BATCH_SIZE = 100;
|
|
2
4
|
|
|
3
5
|
export interface FolderNode {
|
|
@@ -5,10 +7,16 @@ export interface FolderNode {
|
|
|
5
7
|
fullPath: string;
|
|
6
8
|
hierarchyDelimiter: string;
|
|
7
9
|
messageCount: number;
|
|
10
|
+
/** RFC 6154 SPECIAL-USE flags, bare (`Trash`, not `\Trash`). */
|
|
11
|
+
specialUse?: readonly string[];
|
|
8
12
|
}
|
|
9
13
|
|
|
14
|
+
/** Where a role's answer came from. */
|
|
15
|
+
export type RoleAppointmentSource = RemitImapFolderAppointment["source"];
|
|
16
|
+
|
|
10
17
|
export interface RoleAppointment {
|
|
11
18
|
role: string;
|
|
19
|
+
source: RoleAppointmentSource;
|
|
12
20
|
mailboxId?: string | null;
|
|
13
21
|
}
|
|
14
22
|
|
|
@@ -39,18 +47,48 @@ export const hasChildFolders = (
|
|
|
39
47
|
);
|
|
40
48
|
};
|
|
41
49
|
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Sources that vouch for the mailbox they name: a person chose it, or the
|
|
52
|
+
* server flagged it. `Proposed` is a guess at a folder's name and vouches for
|
|
53
|
+
* nothing. `Reserved` is unreachable behind the inbox branch below and stays
|
|
54
|
+
* anyway — this set is a statement about evidence, not about branch order.
|
|
55
|
+
*/
|
|
56
|
+
const VOUCHED_SOURCES: readonly RoleAppointmentSource[] = [
|
|
57
|
+
"Appointed",
|
|
58
|
+
"Flagged",
|
|
59
|
+
"Reserved",
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A `Stale` entry resolves to whatever the account would have had with no
|
|
64
|
+
* appointment at all, and that fallback can be the folder the server itself
|
|
65
|
+
* flags for the role. The entry no longer carries which it was, so read the
|
|
66
|
+
* flag off the folder: the mail server's designation is unchanged by the
|
|
67
|
+
* user's own choice going missing, and nothing behind this guard re-checks it.
|
|
68
|
+
* Flag values are the bare RFC 6154 names, identical to the canonical roles.
|
|
69
|
+
*/
|
|
70
|
+
const vouchesFor = (
|
|
71
|
+
appointment: RoleAppointment,
|
|
72
|
+
folder: Pick<FolderNode, "mailboxId" | "specialUse">,
|
|
73
|
+
): boolean => {
|
|
74
|
+
if (appointment.mailboxId !== folder.mailboxId) return false;
|
|
75
|
+
if (appointment.source === "Stale")
|
|
76
|
+
return folder.specialUse?.includes(appointment.role) ?? false;
|
|
77
|
+
return VOUCHED_SOURCES.includes(appointment.source);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** The canonical role a folder is vouched for, or `undefined` when none is. */
|
|
81
|
+
export const vouchedRole = (
|
|
82
|
+
folder: Pick<FolderNode, "mailboxId" | "specialUse">,
|
|
45
83
|
appointments: readonly RoleAppointment[],
|
|
46
|
-
): string | undefined =>
|
|
47
|
-
appointments.find((a) => a.mailboxId != null && a.mailboxId === mailboxId)
|
|
48
|
-
?.role;
|
|
84
|
+
): string | undefined => appointments.find((a) => vouchesFor(a, folder))?.role;
|
|
49
85
|
|
|
50
86
|
/**
|
|
51
87
|
* Why a folder can't be deleted, or `{ deletable: true }` when it can. The
|
|
52
|
-
* inbox is reserved, a folder
|
|
53
|
-
* first, and a folder with subfolders must have them handled before it
|
|
88
|
+
* inbox is reserved, a folder somebody vouched for in a canonical role must be
|
|
89
|
+
* released first, and a folder with subfolders must have them handled before it
|
|
90
|
+
* goes. A role a folder only fills because its name reads that way is no reason
|
|
91
|
+
* to keep the folder: the message would name a remedy that does not apply.
|
|
54
92
|
*/
|
|
55
93
|
export function guardFolderDeletion(
|
|
56
94
|
folder: FolderNode,
|
|
@@ -64,7 +102,7 @@ export function guardFolderDeletion(
|
|
|
64
102
|
message: "The inbox can't be deleted.",
|
|
65
103
|
};
|
|
66
104
|
|
|
67
|
-
const role =
|
|
105
|
+
const role = vouchedRole(folder, appointments);
|
|
68
106
|
if (role)
|
|
69
107
|
return {
|
|
70
108
|
deletable: false,
|