@remit/data-ports 0.0.34 → 0.0.35
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 +5 -1
- package/src/folder-role.test.ts +167 -0
- package/src/folder-role.ts +195 -0
- package/src/interfaces/mailbox-special-use.ts +17 -3
- package/src/mailbox-name.ts +1 -1
- package/src/mailbox-role.ts +29 -30
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/data-ports",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.35",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
"types": "./src/display-name.ts",
|
|
34
34
|
"default": "./src/display-name.ts"
|
|
35
35
|
},
|
|
36
|
+
"./folder-role": {
|
|
37
|
+
"types": "./src/folder-role.ts",
|
|
38
|
+
"default": "./src/folder-role.ts"
|
|
39
|
+
},
|
|
36
40
|
"./mailbox-name": {
|
|
37
41
|
"types": "./src/mailbox-name.ts",
|
|
38
42
|
"default": "./src/mailbox-name.ts"
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { CanonicalMailboxRole, MailboxSpecialUse } from "@remit/domain-enums";
|
|
4
|
+
import {
|
|
5
|
+
type RoleMailboxCandidate,
|
|
6
|
+
resolveConfirmedMailboxForRole,
|
|
7
|
+
resolveMailboxForRole,
|
|
8
|
+
} from "./folder-role.js";
|
|
9
|
+
|
|
10
|
+
const mailbox = (
|
|
11
|
+
mailboxId: string,
|
|
12
|
+
fullPath: string,
|
|
13
|
+
specialUse: readonly string[] = [],
|
|
14
|
+
hierarchyDelimiter = "/",
|
|
15
|
+
): RoleMailboxCandidate => ({
|
|
16
|
+
mailboxId,
|
|
17
|
+
fullPath,
|
|
18
|
+
hierarchyDelimiter,
|
|
19
|
+
specialUse,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("resolveMailboxForRole precedence", () => {
|
|
23
|
+
const mailboxes = [
|
|
24
|
+
mailbox("mb-inbox", "INBOX"),
|
|
25
|
+
mailbox("mb-spam", "INBOX/Spam"),
|
|
26
|
+
mailbox("mb-junk", "Junk", [MailboxSpecialUse.Junk]),
|
|
27
|
+
mailbox("mb-project", "INBOX/Project X"),
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
it("puts the user's appointment above both the flag and the name", () => {
|
|
31
|
+
assert.equal(
|
|
32
|
+
resolveMailboxForRole(CanonicalMailboxRole.Junk, mailboxes, "mb-project")
|
|
33
|
+
?.mailboxId,
|
|
34
|
+
"mb-project",
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("takes the server's SPECIAL-USE flag when nothing is appointed", () => {
|
|
39
|
+
assert.equal(
|
|
40
|
+
resolveMailboxForRole(CanonicalMailboxRole.Junk, mailboxes)?.mailboxId,
|
|
41
|
+
"mb-junk",
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("falls back to the name only when no flag is present", () => {
|
|
46
|
+
const unflagged = mailboxes.filter((m) => m.mailboxId !== "mb-junk");
|
|
47
|
+
assert.equal(
|
|
48
|
+
resolveMailboxForRole(CanonicalMailboxRole.Junk, unflagged)?.mailboxId,
|
|
49
|
+
"mb-spam",
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("reads the leaf against the mailbox's own delimiter, not a hardcoded slash", () => {
|
|
54
|
+
const dotted = [
|
|
55
|
+
mailbox("mb-inbox", "INBOX", [], "."),
|
|
56
|
+
mailbox("mb-trash", "INBOX.Deleted Items", [], "."),
|
|
57
|
+
];
|
|
58
|
+
assert.equal(
|
|
59
|
+
resolveMailboxForRole(CanonicalMailboxRole.Trash, dotted)?.mailboxId,
|
|
60
|
+
"mb-trash",
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("matches the reserved INBOX name for Inbox", () => {
|
|
65
|
+
assert.equal(
|
|
66
|
+
resolveMailboxForRole(CanonicalMailboxRole.Inbox, mailboxes)?.mailboxId,
|
|
67
|
+
"mb-inbox",
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("returns null rather than guessing when nothing matches", () => {
|
|
72
|
+
assert.equal(
|
|
73
|
+
resolveMailboxForRole(CanonicalMailboxRole.Archive, mailboxes),
|
|
74
|
+
null,
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("re-proposes when the appointment names a mailbox that is gone", () => {
|
|
79
|
+
assert.equal(
|
|
80
|
+
resolveMailboxForRole(
|
|
81
|
+
CanonicalMailboxRole.Junk,
|
|
82
|
+
mailboxes,
|
|
83
|
+
"mb-deleted-last-week",
|
|
84
|
+
)?.mailboxId,
|
|
85
|
+
"mb-junk",
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("resolveMailboxForRole on [Gmail]/Trash beside a top-level Bin", () => {
|
|
91
|
+
// #837: joining Trash to the leaf-name resolver made depth outrank the name,
|
|
92
|
+
// so a top-level `Bin` beat `[Gmail]/Trash` and deletes landed in the wrong
|
|
93
|
+
// folder. `bin` is no longer a hint — Gmail flags both \Trash — so the name
|
|
94
|
+
// rule reaches `[Gmail]/Trash` and nothing reaches `Bin`.
|
|
95
|
+
const mailboxes = [
|
|
96
|
+
mailbox("mb-inbox", "INBOX"),
|
|
97
|
+
mailbox("mb-bin", "Bin"),
|
|
98
|
+
mailbox("mb-gmail-trash", "[Gmail]/Trash"),
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
it("proposes [Gmail]/Trash, never the folder called Bin", () => {
|
|
102
|
+
assert.equal(
|
|
103
|
+
resolveMailboxForRole(CanonicalMailboxRole.Trash, mailboxes)?.mailboxId,
|
|
104
|
+
"mb-gmail-trash",
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("resolves to whichever folder the user appointed", () => {
|
|
109
|
+
assert.equal(
|
|
110
|
+
resolveMailboxForRole(CanonicalMailboxRole.Trash, mailboxes, "mb-bin")
|
|
111
|
+
?.mailboxId,
|
|
112
|
+
"mb-bin",
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("still takes the SPECIAL-USE flag over the name when nothing is appointed", () => {
|
|
117
|
+
const flagged = [
|
|
118
|
+
mailbox("mb-inbox", "INBOX"),
|
|
119
|
+
mailbox("mb-bin", "Bin"),
|
|
120
|
+
mailbox("mb-gmail-trash", "[Gmail]/Trash", [MailboxSpecialUse.Trash]),
|
|
121
|
+
];
|
|
122
|
+
assert.equal(
|
|
123
|
+
resolveMailboxForRole(CanonicalMailboxRole.Trash, flagged)?.mailboxId,
|
|
124
|
+
"mb-gmail-trash",
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("resolveConfirmedMailboxForRole", () => {
|
|
130
|
+
// What Empty Trash resolves through: an expunge may only touch a folder
|
|
131
|
+
// somebody actually designated (audit #841).
|
|
132
|
+
const mailboxes = [
|
|
133
|
+
mailbox("mb-inbox", "INBOX"),
|
|
134
|
+
mailbox("mb-deleted", "Deleted"),
|
|
135
|
+
mailbox("mb-gmail-trash", "[Gmail]/Trash"),
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
it("refuses a folder that only matches by name", () => {
|
|
139
|
+
assert.equal(
|
|
140
|
+
resolveConfirmedMailboxForRole(CanonicalMailboxRole.Trash, mailboxes),
|
|
141
|
+
null,
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("takes the appointment over the folder the name rule would have picked", () => {
|
|
146
|
+
assert.equal(
|
|
147
|
+
resolveConfirmedMailboxForRole(
|
|
148
|
+
CanonicalMailboxRole.Trash,
|
|
149
|
+
mailboxes,
|
|
150
|
+
"mb-gmail-trash",
|
|
151
|
+
)?.mailboxId,
|
|
152
|
+
"mb-gmail-trash",
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("takes the server flag when nothing is appointed", () => {
|
|
157
|
+
const flagged = [
|
|
158
|
+
mailbox("mb-deleted", "Deleted"),
|
|
159
|
+
mailbox("mb-trash", "[Gmail]/Trash", [MailboxSpecialUse.Trash]),
|
|
160
|
+
];
|
|
161
|
+
assert.equal(
|
|
162
|
+
resolveConfirmedMailboxForRole(CanonicalMailboxRole.Trash, flagged)
|
|
163
|
+
?.mailboxId,
|
|
164
|
+
"mb-trash",
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AccountSettingName,
|
|
3
|
+
CanonicalMailboxRole,
|
|
4
|
+
MailboxSpecialUse,
|
|
5
|
+
} from "@remit/domain-enums";
|
|
6
|
+
import {
|
|
7
|
+
composeSettingName,
|
|
8
|
+
SETTING_NAME_SEPARATOR,
|
|
9
|
+
} from "./account-settings.js";
|
|
10
|
+
import {
|
|
11
|
+
type MailboxNameCandidate,
|
|
12
|
+
resolveMailboxByLeafName,
|
|
13
|
+
} from "./mailbox-name.js";
|
|
14
|
+
|
|
15
|
+
export type CanonicalMailboxRoleValue =
|
|
16
|
+
(typeof CanonicalMailboxRole)[keyof typeof CanonicalMailboxRole];
|
|
17
|
+
|
|
18
|
+
/** The fixed anchor set, in the RFC's canonical display order. */
|
|
19
|
+
export const CANONICAL_ROLES: readonly CanonicalMailboxRoleValue[] =
|
|
20
|
+
Object.values(CanonicalMailboxRole);
|
|
21
|
+
|
|
22
|
+
const isCanonicalRole = (value: string): value is CanonicalMailboxRoleValue =>
|
|
23
|
+
(CANONICAL_ROLES as readonly string[]).includes(value);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The stored `AccountSetting` name holding one role's appointment. Composed
|
|
27
|
+
* here rather than at either reader, so the backend that writes it and the
|
|
28
|
+
* repository that reads it can never disagree about the key.
|
|
29
|
+
*/
|
|
30
|
+
export const composeFolderRoleAppointmentName = (
|
|
31
|
+
accountId: string,
|
|
32
|
+
role: CanonicalMailboxRoleValue,
|
|
33
|
+
): string =>
|
|
34
|
+
composeSettingName(
|
|
35
|
+
AccountSettingName.FolderRoleAppointment,
|
|
36
|
+
`${accountId}${SETTING_NAME_SEPARATOR}${role}`,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Split a stored appointment name back into its two-part target. Unlike the
|
|
41
|
+
* single-target composites (`MailboxRole#<id>`), this setting composes two ids
|
|
42
|
+
* after the base, so it parses the suffix itself rather than reusing
|
|
43
|
+
* `targetIdOf`.
|
|
44
|
+
*/
|
|
45
|
+
export const parseFolderRoleAppointmentName = (
|
|
46
|
+
name: string,
|
|
47
|
+
): { accountId: string; role: CanonicalMailboxRoleValue } | undefined => {
|
|
48
|
+
const [base, ...rest] = name.split(SETTING_NAME_SEPARATOR);
|
|
49
|
+
if (base !== AccountSettingName.FolderRoleAppointment) return undefined;
|
|
50
|
+
const role = rest[rest.length - 1];
|
|
51
|
+
const accountId = rest.slice(0, -1).join(SETTING_NAME_SEPARATOR);
|
|
52
|
+
if (!accountId || !role || !isCanonicalRole(role)) return undefined;
|
|
53
|
+
return { accountId, role };
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* RFC 6154 SPECIAL-USE flag per role. Inbox has no SPECIAL-USE flag (RFC 3501
|
|
58
|
+
* reserves the name itself); a role with no entry here is matched by name hint
|
|
59
|
+
* only.
|
|
60
|
+
*/
|
|
61
|
+
export const ROLE_SPECIAL_USE: Partial<
|
|
62
|
+
Record<CanonicalMailboxRoleValue, string>
|
|
63
|
+
> = {
|
|
64
|
+
[CanonicalMailboxRole.Drafts]: MailboxSpecialUse.Drafts,
|
|
65
|
+
[CanonicalMailboxRole.Sent]: MailboxSpecialUse.Sent,
|
|
66
|
+
[CanonicalMailboxRole.Archive]: MailboxSpecialUse.Archive,
|
|
67
|
+
[CanonicalMailboxRole.Junk]: MailboxSpecialUse.Junk,
|
|
68
|
+
[CanonicalMailboxRole.Trash]: MailboxSpecialUse.Trash,
|
|
69
|
+
[CanonicalMailboxRole.All]: MailboxSpecialUse.All,
|
|
70
|
+
[CanonicalMailboxRole.Flagged]: MailboxSpecialUse.Flagged,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Weak name hints, ordered best-first and lower case: the last resort for a
|
|
75
|
+
* provider that advertises no SPECIAL-USE and whose user has appointed
|
|
76
|
+
* nothing. Deliberately small — a guess that seeds a proposal, never a
|
|
77
|
+
* substitute for server truth or for what the user chose.
|
|
78
|
+
*/
|
|
79
|
+
export const ROLE_NAME_HINTS: Partial<
|
|
80
|
+
Record<CanonicalMailboxRoleValue, readonly string[]>
|
|
81
|
+
> = {
|
|
82
|
+
[CanonicalMailboxRole.Drafts]: ["drafts", "draft", "concepten"],
|
|
83
|
+
[CanonicalMailboxRole.Sent]: [
|
|
84
|
+
"sent",
|
|
85
|
+
"sent items",
|
|
86
|
+
"sent messages",
|
|
87
|
+
"sent mail",
|
|
88
|
+
],
|
|
89
|
+
[CanonicalMailboxRole.Archive]: ["archive", "archives", "all mail"],
|
|
90
|
+
[CanonicalMailboxRole.Junk]: [
|
|
91
|
+
"junk",
|
|
92
|
+
"spam",
|
|
93
|
+
"junk e-mail",
|
|
94
|
+
"junk email",
|
|
95
|
+
"bulk mail",
|
|
96
|
+
],
|
|
97
|
+
// No bare "deleted" and no "bin": both are ordinary folder names a user
|
|
98
|
+
// keeps mail in, and a wrong guess here files deletes into a folder the
|
|
99
|
+
// user never meant as Trash. Gmail's en-GB "Bin" and its "[Gmail]/Trash"
|
|
100
|
+
// both advertise \Trash, so the flag already covers them (#837).
|
|
101
|
+
[CanonicalMailboxRole.Trash]: ["trash", "deleted items", "deleted messages"],
|
|
102
|
+
[CanonicalMailboxRole.All]: ["all mail", "all"],
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The pane where a user appoints a folder to a role, named as it is labelled.
|
|
107
|
+
* Every refusal that asks for an appointment points here, in one wording — the
|
|
108
|
+
* settings screen is titled "Folder roles", and copies of this sentence had
|
|
109
|
+
* already drifted to "Folders".
|
|
110
|
+
*/
|
|
111
|
+
export const FOLDER_ROLES_SETTINGS_PATH = "Settings › Folder roles";
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Why an operation that files or destroys mail was refused. A delete, an
|
|
115
|
+
* Empty Trash and the count the handler reports before one all act on the
|
|
116
|
+
* folder the user appointed or the server flagged, and all refuse in these
|
|
117
|
+
* words — one sentence, so two surfaces cannot tell a user two things about
|
|
118
|
+
* one account.
|
|
119
|
+
*/
|
|
120
|
+
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.`;
|
|
121
|
+
|
|
122
|
+
/** The minimal mailbox shape role resolution reads. */
|
|
123
|
+
export interface RoleMailboxCandidate extends MailboxNameCandidate {
|
|
124
|
+
specialUse?: readonly string[];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The mailbox a role is CONFIRMED to hold: the one the user appointed, or the
|
|
129
|
+
* one the server flagged (RFC 6154). No name guessing — `null` here means
|
|
130
|
+
* nobody has said which folder this is, only that a folder happens to be named
|
|
131
|
+
* something plausible. An operation that destroys mail resolves through this
|
|
132
|
+
* and refuses when it comes back empty; `resolveMailboxForRole` adds the guess
|
|
133
|
+
* on top for the operations where being wrong only misfiles a message.
|
|
134
|
+
*
|
|
135
|
+
* An appointment naming a mailbox this account does not hold — deleted,
|
|
136
|
+
* renamed, or carried in from elsewhere — is stale, and falls through to the
|
|
137
|
+
* flag as if unset.
|
|
138
|
+
*/
|
|
139
|
+
export const resolveConfirmedMailboxForRole = <T extends RoleMailboxCandidate>(
|
|
140
|
+
role: CanonicalMailboxRoleValue,
|
|
141
|
+
mailboxes: readonly T[],
|
|
142
|
+
appointedMailboxId?: string,
|
|
143
|
+
): T | null => {
|
|
144
|
+
if (appointedMailboxId) {
|
|
145
|
+
const appointed = mailboxes.find((m) => m.mailboxId === appointedMailboxId);
|
|
146
|
+
if (appointed) return appointed;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const specialUse = ROLE_SPECIAL_USE[role];
|
|
150
|
+
if (specialUse) {
|
|
151
|
+
const flagged = mailboxes.find((m) => m.specialUse?.includes(specialUse));
|
|
152
|
+
if (flagged) return flagged;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (role === CanonicalMailboxRole.Inbox) {
|
|
156
|
+
return mailboxes.find((m) => m.fullPath.toUpperCase() === "INBOX") ?? null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return null;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The single mailbox that holds a canonical role for an account (RFC 032
|
|
164
|
+
* exclusive-folder-appointment, #976), in one precedence order every caller
|
|
165
|
+
* shares:
|
|
166
|
+
*
|
|
167
|
+
* 1. the mailbox the user appointed;
|
|
168
|
+
* 2. the server's own SPECIAL-USE flag (RFC 6154) — language-independent truth;
|
|
169
|
+
* 3. the reserved `INBOX` name, for the Inbox role only (RFC 3501);
|
|
170
|
+
* 4. a conventional name, matched on the folder's own leaf segment so it
|
|
171
|
+
* resolves at any depth under any prefix.
|
|
172
|
+
*
|
|
173
|
+
* The last of those is a guess: a folder named `Deleted` is not evidence that
|
|
174
|
+
* the user means it as Trash. Use it only where being wrong misfiles a message
|
|
175
|
+
* a user can move back — never where it destroys mail.
|
|
176
|
+
*
|
|
177
|
+
* `null` when nothing matches: the role has no folder, and the caller says so
|
|
178
|
+
* rather than picking one.
|
|
179
|
+
*/
|
|
180
|
+
export const resolveMailboxForRole = <T extends RoleMailboxCandidate>(
|
|
181
|
+
role: CanonicalMailboxRoleValue,
|
|
182
|
+
mailboxes: readonly T[],
|
|
183
|
+
appointedMailboxId?: string,
|
|
184
|
+
): T | null => {
|
|
185
|
+
const confirmed = resolveConfirmedMailboxForRole(
|
|
186
|
+
role,
|
|
187
|
+
mailboxes,
|
|
188
|
+
appointedMailboxId,
|
|
189
|
+
);
|
|
190
|
+
if (confirmed) return confirmed;
|
|
191
|
+
|
|
192
|
+
const hints = ROLE_NAME_HINTS[role];
|
|
193
|
+
if (!hints) return null;
|
|
194
|
+
return resolveMailboxByLeafName(mailboxes, hints);
|
|
195
|
+
};
|
|
@@ -3,6 +3,13 @@ import type {
|
|
|
3
3
|
MailboxSpecialUseValue,
|
|
4
4
|
} from "../types.js";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Special-use entries, plus the per-role lookup every special-folder operation
|
|
8
|
+
* goes through. The `find<Role>Mailbox` reads answer "which folder holds this
|
|
9
|
+
* role for this account", in the one precedence order `resolveMailboxForRole`
|
|
10
|
+
* defines: the user's appointment first (RFC 032 exclusive-folder-appointment,
|
|
11
|
+
* #976), then the server's SPECIAL-USE flag, then a conventional name.
|
|
12
|
+
*/
|
|
6
13
|
export interface IMailboxSpecialUseRepository {
|
|
7
14
|
create(
|
|
8
15
|
mailboxId: string,
|
|
@@ -14,16 +21,23 @@ export interface IMailboxSpecialUseRepository {
|
|
|
14
21
|
): Promise<MailboxSpecialUseItem[]>;
|
|
15
22
|
listByMailboxId(mailboxId: string): Promise<MailboxSpecialUseItem[]>;
|
|
16
23
|
deleteByMailboxId(mailboxId: string): Promise<number>;
|
|
17
|
-
|
|
24
|
+
findInboxMailbox(
|
|
18
25
|
accountId: string,
|
|
19
|
-
specialUse: MailboxSpecialUseValue,
|
|
20
26
|
): Promise<{ mailboxId: string; fullPath: string } | null>;
|
|
21
|
-
|
|
27
|
+
findSentMailbox(
|
|
22
28
|
accountId: string,
|
|
23
29
|
): Promise<{ mailboxId: string; fullPath: string } | null>;
|
|
24
30
|
findTrashMailbox(
|
|
25
31
|
accountId: string,
|
|
26
32
|
): Promise<{ mailboxId: string; fullPath: string } | null>;
|
|
33
|
+
/**
|
|
34
|
+
* Trash without the name guess: the appointment or the \Trash flag, nothing
|
|
35
|
+
* else. Empty Trash expunges whatever this returns, and a folder merely
|
|
36
|
+
* named `Deleted` is not consent to destroy its contents.
|
|
37
|
+
*/
|
|
38
|
+
findConfirmedTrashMailbox(
|
|
39
|
+
accountId: string,
|
|
40
|
+
): Promise<{ mailboxId: string; fullPath: string } | null>;
|
|
27
41
|
findArchiveMailbox(
|
|
28
42
|
accountId: string,
|
|
29
43
|
): Promise<{ mailboxId: string; fullPath: string } | null>;
|
package/src/mailbox-name.ts
CHANGED
|
@@ -34,7 +34,7 @@ const rank = (
|
|
|
34
34
|
* lower case.
|
|
35
35
|
*/
|
|
36
36
|
export const resolveMailboxByLeafName = <T extends MailboxNameCandidate>(
|
|
37
|
-
mailboxes: T[],
|
|
37
|
+
mailboxes: readonly T[],
|
|
38
38
|
names: readonly string[],
|
|
39
39
|
): T | null => {
|
|
40
40
|
let best: { mailbox: T; rank: number } | null = null;
|
package/src/mailbox-role.ts
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { CanonicalMailboxRole } from "@remit/domain-enums";
|
|
2
|
+
import { ROLE_NAME_HINTS, resolveMailboxForRole } from "./folder-role.js";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
]
|
|
4
|
+
/**
|
|
5
|
+
* The conventional names for a role, taken from the one table every
|
|
6
|
+
* special-folder lookup shares (`folder-role.ts`). Exported so the SQLite junk
|
|
7
|
+
* repair can spell the same rule; a second list here is how `INBOX/Sent` and
|
|
8
|
+
* `INBOX/Spam` came to be resolvable in one place and not another.
|
|
9
|
+
*/
|
|
10
|
+
export const JUNK_FOLDER_NAMES: readonly string[] =
|
|
11
|
+
ROLE_NAME_HINTS[CanonicalMailboxRole.Junk] ?? [];
|
|
11
12
|
|
|
12
|
-
export const TRASH_FOLDER_NAMES: readonly string[] =
|
|
13
|
-
|
|
14
|
-
"deleted items",
|
|
15
|
-
"deleted",
|
|
16
|
-
"bin",
|
|
17
|
-
];
|
|
13
|
+
export const TRASH_FOLDER_NAMES: readonly string[] =
|
|
14
|
+
ROLE_NAME_HINTS[CanonicalMailboxRole.Trash] ?? [];
|
|
18
15
|
|
|
19
16
|
export interface MailboxRole {
|
|
20
17
|
readonly fullPath: string;
|
|
@@ -22,25 +19,27 @@ export interface MailboxRole {
|
|
|
22
19
|
readonly specialUse?: readonly string[];
|
|
23
20
|
}
|
|
24
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Whether one mailbox, considered alone, carries a role: its SPECIAL-USE flag
|
|
24
|
+
* or its conventional name. A single mailbox carries no account context, so
|
|
25
|
+
* this cannot see the user's appointment — a caller holding an accountId asks
|
|
26
|
+
* `IMailboxSpecialUseRepository` instead, which does.
|
|
27
|
+
*/
|
|
25
28
|
const carriesRole = (
|
|
26
29
|
mailbox: MailboxRole,
|
|
27
|
-
|
|
28
|
-
namesWhenServerDoesNotSaySo: readonly string[],
|
|
30
|
+
role: (typeof CanonicalMailboxRole)[keyof typeof CanonicalMailboxRole],
|
|
29
31
|
): boolean =>
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
],
|
|
39
|
-
namesWhenServerDoesNotSaySo,
|
|
40
|
-
) !== null;
|
|
32
|
+
resolveMailboxForRole(role, [
|
|
33
|
+
{
|
|
34
|
+
mailboxId: "",
|
|
35
|
+
fullPath: mailbox.fullPath,
|
|
36
|
+
hierarchyDelimiter: mailbox.hierarchyDelimiter ?? "",
|
|
37
|
+
specialUse: mailbox.specialUse,
|
|
38
|
+
},
|
|
39
|
+
]) !== null;
|
|
41
40
|
|
|
42
41
|
export const isJunkMailbox = (mailbox: MailboxRole): boolean =>
|
|
43
|
-
carriesRole(mailbox,
|
|
42
|
+
carriesRole(mailbox, CanonicalMailboxRole.Junk);
|
|
44
43
|
|
|
45
44
|
export const isTrashMailbox = (mailbox: MailboxRole): boolean =>
|
|
46
|
-
carriesRole(mailbox,
|
|
45
|
+
carriesRole(mailbox, CanonicalMailboxRole.Trash);
|