@remit/data-ports 0.0.39 → 0.0.40
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/errors.ts +6 -11
- package/src/folder-role.test.ts +23 -6
- package/src/folder-role.ts +61 -18
- package/src/interfaces/mailbox-special-use.ts +0 -8
package/package.json
CHANGED
package/src/errors.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
CanonicalMailboxRoleValue,
|
|
3
|
+
FolderRoleUnresolvedReason,
|
|
4
|
+
} from "./folder-role.js";
|
|
5
|
+
|
|
6
|
+
export type { FolderRoleUnresolvedReason };
|
|
2
7
|
|
|
3
8
|
/**
|
|
4
9
|
* The half of an error a client may read: a stable `code` it branches on and
|
|
@@ -51,16 +56,6 @@ export class ConflictError extends HTTPError {
|
|
|
51
56
|
public statusCode = 409;
|
|
52
57
|
}
|
|
53
58
|
|
|
54
|
-
/**
|
|
55
|
-
* Why a canonical role names no folder this action may act on. `none`: the
|
|
56
|
-
* account has no candidate at all. `stale`: the folder the user appointed is
|
|
57
|
-
* gone from the server. `unconfirmed`: a folder matches by name, but nobody —
|
|
58
|
-
* neither the user nor the server's own flag — ever said it holds the role.
|
|
59
|
-
* These three and no others; a target that is merely unsettled is a different
|
|
60
|
-
* refusal under its own code.
|
|
61
|
-
*/
|
|
62
|
-
export type FolderRoleUnresolvedReason = "none" | "stale" | "unconfirmed";
|
|
63
|
-
|
|
64
59
|
/**
|
|
65
60
|
* A destructive action refused because the role it needs is unresolved. The
|
|
66
61
|
* client reads `code` and words its prompt from `details` — never from the
|
package/src/folder-role.test.ts
CHANGED
|
@@ -254,17 +254,17 @@ describe("the adapters over resolveRoleForAccount", () => {
|
|
|
254
254
|
mailbox("mb-trash", "[Gmail]/Trash", [MailboxSpecialUse.Trash]),
|
|
255
255
|
];
|
|
256
256
|
|
|
257
|
-
it("
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
257
|
+
it("withholds a stale appointment's fallback from the confirmed answer only", () => {
|
|
258
|
+
// A vanished appointment is not confirmation of anything, so the expunge
|
|
259
|
+
// gate sees nothing. Filing mail still works: the fallback is a fine place
|
|
260
|
+
// to put a message the user can move back.
|
|
261
261
|
assert.equal(
|
|
262
262
|
resolveConfirmedMailboxForRole(
|
|
263
263
|
CanonicalMailboxRole.Trash,
|
|
264
264
|
mailboxes,
|
|
265
265
|
"mb-gone",
|
|
266
|
-
)
|
|
267
|
-
|
|
266
|
+
),
|
|
267
|
+
null,
|
|
268
268
|
);
|
|
269
269
|
assert.equal(
|
|
270
270
|
resolveMailboxForRole(CanonicalMailboxRole.Trash, mailboxes, "mb-gone")
|
|
@@ -273,6 +273,23 @@ describe("the adapters over resolveRoleForAccount", () => {
|
|
|
273
273
|
);
|
|
274
274
|
});
|
|
275
275
|
|
|
276
|
+
it("keeps a stale appointment on the flag, never on a folder named like one", () => {
|
|
277
|
+
// The confirmed adapter is not built on top of the other one: were it,
|
|
278
|
+
// null-on-stale would drop the seven non-Trash roles past their
|
|
279
|
+
// SPECIAL-USE flag and into the name hint, and a folder somebody called
|
|
280
|
+
// "Sent" would start collecting this account's sent mail.
|
|
281
|
+
const sent = [
|
|
282
|
+
mailbox("mb-inbox", "INBOX"),
|
|
283
|
+
mailbox("mb-sent-copy", "Sent"),
|
|
284
|
+
mailbox("mb-sent", "[Gmail]/Sent Mail", [MailboxSpecialUse.Sent]),
|
|
285
|
+
];
|
|
286
|
+
assert.equal(
|
|
287
|
+
resolveMailboxForRole(CanonicalMailboxRole.Sent, sent, "mb-gone")
|
|
288
|
+
?.mailboxId,
|
|
289
|
+
"mb-sent",
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
|
|
276
293
|
it("keeps the name guess out of the confirmed answer", () => {
|
|
277
294
|
const unflagged = [
|
|
278
295
|
mailbox("mb-inbox", "INBOX"),
|
package/src/folder-role.ts
CHANGED
|
@@ -150,6 +150,20 @@ export const FOLDER_ROLES_SETTINGS_PATH = "Settings › Folder roles";
|
|
|
150
150
|
*/
|
|
151
151
|
export const NO_TRASH_FOLDER_REASON = `This account has no Trash folder, so nothing was deleted. Appoint one under ${FOLDER_ROLES_SETTINGS_PATH}, then try again.`;
|
|
152
152
|
|
|
153
|
+
/**
|
|
154
|
+
* The folder somebody chose as this account's Trash is no longer on the mail
|
|
155
|
+
* server. Reader will not quietly file mail into whatever it would have picked
|
|
156
|
+
* had nobody chosen: the user's choice went missing and only they can replace
|
|
157
|
+
* it.
|
|
158
|
+
*/
|
|
159
|
+
export const STALE_TRASH_FOLDER_REASON = `The folder appointed as this account's Trash is no longer on the mail server, so nothing was deleted. Appoint one under ${FOLDER_ROLES_SETTINGS_PATH}, then try again.`;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* A folder matches the Trash name hint and nothing else — enough to file a
|
|
163
|
+
* delete somewhere retrievable, never enough to expunge a whole folder.
|
|
164
|
+
*/
|
|
165
|
+
export const UNCONFIRMED_TRASH_FOLDER_REASON = `Nobody has confirmed which folder is this account's Trash, so it was not emptied. Appoint one under ${FOLDER_ROLES_SETTINGS_PATH}, then try again.`;
|
|
166
|
+
|
|
153
167
|
/** The minimal mailbox shape role resolution reads. */
|
|
154
168
|
export interface RoleMailboxCandidate extends MailboxNameCandidate {
|
|
155
169
|
specialUse?: readonly string[];
|
|
@@ -249,25 +263,49 @@ export const resolveRoleForAccount = <T extends RoleMailboxCandidate>(
|
|
|
249
263
|
*/
|
|
250
264
|
export type TrashAssuranceLevel = "confirmed" | "resolved";
|
|
251
265
|
|
|
252
|
-
|
|
266
|
+
/**
|
|
267
|
+
* Why a canonical role names no folder an action may act on. `none`: the
|
|
268
|
+
* account has no candidate at all. `stale`: the folder the user appointed is
|
|
269
|
+
* gone from the server. `unconfirmed`: a folder matches by name, but nobody —
|
|
270
|
+
* neither the user nor the server's own flag — ever said it holds the role.
|
|
271
|
+
* These three and no others; a target that is merely unsettled is a different
|
|
272
|
+
* refusal under its own code.
|
|
273
|
+
*/
|
|
274
|
+
export type FolderRoleUnresolvedReason = "none" | "stale" | "unconfirmed";
|
|
275
|
+
|
|
276
|
+
export type TrashGateOutcome<T> =
|
|
277
|
+
| { allowed: true; mailbox: T }
|
|
278
|
+
| { allowed: false; reason: FolderRoleUnresolvedReason };
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* The Trash folder a verb at this assurance level may act on, or which of the
|
|
282
|
+
* three reasons stops it. One switch: the delete, the expunge and the count
|
|
283
|
+
* reported before one all decide here, so no two of them can answer a user
|
|
284
|
+
* differently about the same account.
|
|
285
|
+
*/
|
|
286
|
+
export const trashMailboxAt = <T>(
|
|
253
287
|
resolution: RoleResolution<T>,
|
|
254
288
|
level: TrashAssuranceLevel,
|
|
255
|
-
):
|
|
289
|
+
): TrashGateOutcome<T> => {
|
|
256
290
|
switch (resolution.kind) {
|
|
257
291
|
case "appointed":
|
|
258
292
|
case "flagged":
|
|
259
|
-
return true;
|
|
293
|
+
return { allowed: true, mailbox: resolution.mailbox };
|
|
260
294
|
case "proposed":
|
|
261
|
-
return level === "resolved"
|
|
295
|
+
return level === "resolved"
|
|
296
|
+
? { allowed: true, mailbox: resolution.mailbox }
|
|
297
|
+
: { allowed: false, reason: "unconfirmed" };
|
|
298
|
+
case "appointment_stale":
|
|
299
|
+
return { allowed: false, reason: "stale" };
|
|
262
300
|
default:
|
|
263
|
-
return false;
|
|
301
|
+
return { allowed: false, reason: "none" };
|
|
264
302
|
}
|
|
265
303
|
};
|
|
266
304
|
|
|
267
|
-
const
|
|
305
|
+
export const meetsTrashAssurance = <T>(
|
|
268
306
|
resolution: RoleResolution<T>,
|
|
269
|
-
|
|
270
|
-
|
|
307
|
+
level: TrashAssuranceLevel,
|
|
308
|
+
): boolean => trashMailboxAt(resolution, level).allowed;
|
|
271
309
|
|
|
272
310
|
/**
|
|
273
311
|
* The mailbox a role is CONFIRMED to hold: the one the user appointed, or the
|
|
@@ -278,17 +316,16 @@ const withoutStale = <T>(
|
|
|
278
316
|
* on top for the operations where being wrong only misfiles a message.
|
|
279
317
|
*
|
|
280
318
|
* An appointment naming a mailbox this account does not hold — deleted,
|
|
281
|
-
* renamed, or carried in from elsewhere — is stale, and
|
|
282
|
-
*
|
|
319
|
+
* renamed, or carried in from elsewhere — is stale, and answers `null` here
|
|
320
|
+
* too: the fallback is the folder the user did not choose, which is fine to
|
|
321
|
+
* file mail into and no basis at all for destroying it.
|
|
283
322
|
*/
|
|
284
323
|
export const resolveConfirmedMailboxForRole = <T extends RoleMailboxCandidate>(
|
|
285
324
|
role: CanonicalMailboxRoleValue,
|
|
286
325
|
mailboxes: readonly T[],
|
|
287
326
|
appointedMailboxId?: string,
|
|
288
327
|
): T | null => {
|
|
289
|
-
const resolution =
|
|
290
|
-
resolveRoleForAccount(role, mailboxes, appointedMailboxId),
|
|
291
|
-
);
|
|
328
|
+
const resolution = resolveRoleForAccount(role, mailboxes, appointedMailboxId);
|
|
292
329
|
switch (resolution.kind) {
|
|
293
330
|
case "appointed":
|
|
294
331
|
case "flagged":
|
|
@@ -302,15 +339,21 @@ export const resolveConfirmedMailboxForRole = <T extends RoleMailboxCandidate>(
|
|
|
302
339
|
/**
|
|
303
340
|
* The role's mailbox including the name guess: `null` only when nothing
|
|
304
341
|
* matches at all, so the caller says the role has no folder rather than picking
|
|
305
|
-
* one.
|
|
342
|
+
* one. A stale appointment answers with its fallback — every role but Trash
|
|
343
|
+
* files mail on this read, and refusing them all would be a behaviour change
|
|
344
|
+
* with no surface to explain it.
|
|
345
|
+
*
|
|
346
|
+
* Built on `resolveRoleForAccount` directly, never on the confirmed adapter: a
|
|
347
|
+
* stale appointment must still reach the `\Sent` flag here, not fall past it
|
|
348
|
+
* into a folder that merely reads like one.
|
|
306
349
|
*/
|
|
307
350
|
export const resolveMailboxForRole = <T extends RoleMailboxCandidate>(
|
|
308
351
|
role: CanonicalMailboxRoleValue,
|
|
309
352
|
mailboxes: readonly T[],
|
|
310
353
|
appointedMailboxId?: string,
|
|
311
354
|
): T | null => {
|
|
312
|
-
const resolution =
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
return
|
|
355
|
+
const resolution = resolveRoleForAccount(role, mailboxes, appointedMailboxId);
|
|
356
|
+
const effective =
|
|
357
|
+
resolution.kind === "appointment_stale" ? resolution.fallback : resolution;
|
|
358
|
+
return effective.kind === "none" ? null : effective.mailbox;
|
|
316
359
|
};
|
|
@@ -31,14 +31,6 @@ export interface IMailboxSpecialUseRepository {
|
|
|
31
31
|
findTrashMailbox(
|
|
32
32
|
accountId: string,
|
|
33
33
|
): Promise<{ mailboxId: string; fullPath: string } | null>;
|
|
34
|
-
/**
|
|
35
|
-
* Trash without the name guess: the appointment or the \Trash flag, nothing
|
|
36
|
-
* else. Empty Trash expunges whatever this returns, and a folder merely
|
|
37
|
-
* named `Deleted` is not consent to destroy its contents.
|
|
38
|
-
*/
|
|
39
|
-
findConfirmedTrashMailbox(
|
|
40
|
-
accountId: string,
|
|
41
|
-
): Promise<{ mailboxId: string; fullPath: string } | null>;
|
|
42
34
|
/**
|
|
43
35
|
* Trash with the evidence attached, for the two verbs that weigh it: a
|
|
44
36
|
* delete files mail somewhere retrievable, an Empty Trash destroys it, and
|