@remit/data-ports 0.0.38 → 0.0.39
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/account-settings.ts +1 -0
- package/src/folder-role.test.ts +151 -0
- package/src/folder-role.ts +159 -38
- package/src/interfaces/mailbox-special-use.ts +11 -0
package/package.json
CHANGED
package/src/account-settings.ts
CHANGED
|
@@ -70,6 +70,7 @@ export const accountSettingRegistry = {
|
|
|
70
70
|
[AccountSettingName.MailboxDisplayName]: StringSettingSchema,
|
|
71
71
|
[AccountSettingName.MailboxMuted]: MutedFlagSettingSchema,
|
|
72
72
|
[AccountSettingName.FolderRoleAppointment]: StringSettingSchema,
|
|
73
|
+
[AccountSettingName.FolderRoleAppointmentLabel]: StringSettingSchema,
|
|
73
74
|
/**
|
|
74
75
|
* Deprecated tombstone: superseded by `FolderRoleAppointment` (RFC 032
|
|
75
76
|
* exclusive-folder-appointment, #976). Kept only so `baseSettingName`
|
package/src/folder-role.test.ts
CHANGED
|
@@ -2,9 +2,15 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import { describe, it } from "node:test";
|
|
3
3
|
import { CanonicalMailboxRole, MailboxSpecialUse } from "@remit/domain-enums";
|
|
4
4
|
import {
|
|
5
|
+
composeFolderRoleAppointmentLabelName,
|
|
6
|
+
composeFolderRoleAppointmentName,
|
|
7
|
+
meetsTrashAssurance,
|
|
8
|
+
parseFolderRoleAppointmentLabelName,
|
|
9
|
+
parseFolderRoleAppointmentName,
|
|
5
10
|
type RoleMailboxCandidate,
|
|
6
11
|
resolveConfirmedMailboxForRole,
|
|
7
12
|
resolveMailboxForRole,
|
|
13
|
+
resolveRoleForAccount,
|
|
8
14
|
} from "./folder-role.js";
|
|
9
15
|
|
|
10
16
|
const mailbox = (
|
|
@@ -165,3 +171,148 @@ describe("resolveConfirmedMailboxForRole", () => {
|
|
|
165
171
|
);
|
|
166
172
|
});
|
|
167
173
|
});
|
|
174
|
+
|
|
175
|
+
describe("resolveRoleForAccount", () => {
|
|
176
|
+
it("answers none when nothing names a Trash folder", () => {
|
|
177
|
+
// #887 Done item 4: an account whose folders happen to include a Dutch
|
|
178
|
+
// "Prullenbak" has no Trash reader may act on — no flag, no appointment,
|
|
179
|
+
// and "prullenbak" is not a hint.
|
|
180
|
+
const mailboxes = [
|
|
181
|
+
mailbox("mb-inbox", "INBOX"),
|
|
182
|
+
mailbox("mb-werk", "Werk"),
|
|
183
|
+
mailbox("mb-prullenbak", "Prullenbak"),
|
|
184
|
+
];
|
|
185
|
+
assert.deepEqual(
|
|
186
|
+
resolveRoleForAccount(CanonicalMailboxRole.Trash, mailboxes),
|
|
187
|
+
{ kind: "none" },
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("proposes a folder named Trash, and that is not enough to empty it", () => {
|
|
192
|
+
// #887 Done item 1: the name is a guess. It files a delete somewhere
|
|
193
|
+
// retrievable; it never authorises an expunge.
|
|
194
|
+
const mailboxes = [
|
|
195
|
+
mailbox("mb-inbox", "INBOX"),
|
|
196
|
+
mailbox("mb-werk", "Werk"),
|
|
197
|
+
mailbox("mb-trash", "Trash"),
|
|
198
|
+
];
|
|
199
|
+
const resolution = resolveRoleForAccount(
|
|
200
|
+
CanonicalMailboxRole.Trash,
|
|
201
|
+
mailboxes,
|
|
202
|
+
);
|
|
203
|
+
assert.deepEqual(resolution, {
|
|
204
|
+
kind: "proposed",
|
|
205
|
+
mailbox: mailboxes[2],
|
|
206
|
+
});
|
|
207
|
+
assert.equal(meetsTrashAssurance(resolution, "confirmed"), false);
|
|
208
|
+
assert.equal(meetsTrashAssurance(resolution, "resolved"), true);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("names the appointment that went stale, and what took its place", () => {
|
|
212
|
+
const mailboxes = [
|
|
213
|
+
mailbox("mb-inbox", "INBOX"),
|
|
214
|
+
mailbox("mb-trash", "[Gmail]/Trash", [MailboxSpecialUse.Trash]),
|
|
215
|
+
];
|
|
216
|
+
const resolution = resolveRoleForAccount(
|
|
217
|
+
CanonicalMailboxRole.Trash,
|
|
218
|
+
mailboxes,
|
|
219
|
+
"mb-rubbish-deleted-by-apple-mail",
|
|
220
|
+
);
|
|
221
|
+
assert.deepEqual(resolution, {
|
|
222
|
+
kind: "appointment_stale",
|
|
223
|
+
appointedMailboxId: "mb-rubbish-deleted-by-apple-mail",
|
|
224
|
+
fallback: { kind: "flagged", mailbox: mailboxes[1] },
|
|
225
|
+
});
|
|
226
|
+
assert.equal(meetsTrashAssurance(resolution, "confirmed"), false);
|
|
227
|
+
assert.equal(meetsTrashAssurance(resolution, "resolved"), false);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("tags the appointment the user made and the flag the server set apart", () => {
|
|
231
|
+
const mailboxes = [
|
|
232
|
+
mailbox("mb-inbox", "INBOX"),
|
|
233
|
+
mailbox("mb-trash", "[Gmail]/Trash", [MailboxSpecialUse.Trash]),
|
|
234
|
+
];
|
|
235
|
+
assert.equal(
|
|
236
|
+
resolveRoleForAccount(CanonicalMailboxRole.Trash, mailboxes).kind,
|
|
237
|
+
"flagged",
|
|
238
|
+
);
|
|
239
|
+
assert.equal(
|
|
240
|
+
resolveRoleForAccount(CanonicalMailboxRole.Trash, mailboxes, "mb-inbox")
|
|
241
|
+
.kind,
|
|
242
|
+
"appointed",
|
|
243
|
+
);
|
|
244
|
+
assert.equal(
|
|
245
|
+
resolveRoleForAccount(CanonicalMailboxRole.Inbox, mailboxes).kind,
|
|
246
|
+
"reserved",
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe("the adapters over resolveRoleForAccount", () => {
|
|
252
|
+
const mailboxes = [
|
|
253
|
+
mailbox("mb-inbox", "INBOX"),
|
|
254
|
+
mailbox("mb-trash", "[Gmail]/Trash", [MailboxSpecialUse.Trash]),
|
|
255
|
+
];
|
|
256
|
+
|
|
257
|
+
it("still hands a stale appointment the flagged mailbox", () => {
|
|
258
|
+
// The refusal that makes this return null lands with the surface that can
|
|
259
|
+
// tell the user why (S3). Until then, both adapters answer exactly as they
|
|
260
|
+
// did before the union existed.
|
|
261
|
+
assert.equal(
|
|
262
|
+
resolveConfirmedMailboxForRole(
|
|
263
|
+
CanonicalMailboxRole.Trash,
|
|
264
|
+
mailboxes,
|
|
265
|
+
"mb-gone",
|
|
266
|
+
)?.mailboxId,
|
|
267
|
+
"mb-trash",
|
|
268
|
+
);
|
|
269
|
+
assert.equal(
|
|
270
|
+
resolveMailboxForRole(CanonicalMailboxRole.Trash, mailboxes, "mb-gone")
|
|
271
|
+
?.mailboxId,
|
|
272
|
+
"mb-trash",
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("keeps the name guess out of the confirmed answer", () => {
|
|
277
|
+
const unflagged = [
|
|
278
|
+
mailbox("mb-inbox", "INBOX"),
|
|
279
|
+
mailbox("mb-bak", "Trash"),
|
|
280
|
+
];
|
|
281
|
+
assert.equal(
|
|
282
|
+
resolveConfirmedMailboxForRole(CanonicalMailboxRole.Trash, unflagged),
|
|
283
|
+
null,
|
|
284
|
+
);
|
|
285
|
+
assert.equal(
|
|
286
|
+
resolveMailboxForRole(CanonicalMailboxRole.Trash, unflagged)?.mailboxId,
|
|
287
|
+
"mb-bak",
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe("the appointment name and its label sibling", () => {
|
|
293
|
+
it("never lets the label row parse as an appointment", () => {
|
|
294
|
+
// The label is display only. If the appointment parser matched it, a path
|
|
295
|
+
// string would be read back as a mailboxId and resolution would follow it.
|
|
296
|
+
const label = composeFolderRoleAppointmentLabelName(
|
|
297
|
+
"acc-1",
|
|
298
|
+
CanonicalMailboxRole.Trash,
|
|
299
|
+
);
|
|
300
|
+
assert.equal(parseFolderRoleAppointmentName(label), undefined);
|
|
301
|
+
assert.deepEqual(parseFolderRoleAppointmentLabelName(label), {
|
|
302
|
+
accountId: "acc-1",
|
|
303
|
+
role: CanonicalMailboxRole.Trash,
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("never lets an appointment row parse as a label", () => {
|
|
308
|
+
const appointment = composeFolderRoleAppointmentName(
|
|
309
|
+
"acc-1",
|
|
310
|
+
CanonicalMailboxRole.Trash,
|
|
311
|
+
);
|
|
312
|
+
assert.equal(parseFolderRoleAppointmentLabelName(appointment), undefined);
|
|
313
|
+
assert.deepEqual(parseFolderRoleAppointmentName(appointment), {
|
|
314
|
+
accountId: "acc-1",
|
|
315
|
+
role: CanonicalMailboxRole.Trash,
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
});
|
package/src/folder-role.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
MailboxSpecialUse,
|
|
5
5
|
} from "@remit/domain-enums";
|
|
6
6
|
import {
|
|
7
|
+
type AccountSettingNameValue,
|
|
7
8
|
composeSettingName,
|
|
8
9
|
SETTING_NAME_SEPARATOR,
|
|
9
10
|
} from "./account-settings.js";
|
|
@@ -37,22 +38,52 @@ export const composeFolderRoleAppointmentName = (
|
|
|
37
38
|
);
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
41
|
+
* The stored `AccountSetting` name holding the path one role's appointed
|
|
42
|
+
* mailbox had at the moment it was appointed. A sibling row rather than a field
|
|
43
|
+
* on the appointment, so "never read by resolution" is structural: the
|
|
44
|
+
* repository composes only the appointment name and cannot reach this one.
|
|
44
45
|
*/
|
|
45
|
-
export const
|
|
46
|
+
export const composeFolderRoleAppointmentLabelName = (
|
|
47
|
+
accountId: string,
|
|
48
|
+
role: CanonicalMailboxRoleValue,
|
|
49
|
+
): string =>
|
|
50
|
+
composeSettingName(
|
|
51
|
+
AccountSettingName.FolderRoleAppointmentLabel,
|
|
52
|
+
`${accountId}${SETTING_NAME_SEPARATOR}${role}`,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const parseFolderRoleTarget = (
|
|
56
|
+
base: AccountSettingNameValue,
|
|
46
57
|
name: string,
|
|
47
58
|
): { accountId: string; role: CanonicalMailboxRoleValue } | undefined => {
|
|
48
|
-
const [
|
|
49
|
-
if (
|
|
59
|
+
const [candidate, ...rest] = name.split(SETTING_NAME_SEPARATOR);
|
|
60
|
+
if (candidate !== base) return undefined;
|
|
50
61
|
const role = rest[rest.length - 1];
|
|
51
62
|
const accountId = rest.slice(0, -1).join(SETTING_NAME_SEPARATOR);
|
|
52
63
|
if (!accountId || !role || !isCanonicalRole(role)) return undefined;
|
|
53
64
|
return { accountId, role };
|
|
54
65
|
};
|
|
55
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Split a stored appointment name back into its two-part target. Unlike the
|
|
69
|
+
* single-target composites (`MailboxRole#<id>`), this setting composes two ids
|
|
70
|
+
* after the base, so it parses the suffix itself rather than reusing
|
|
71
|
+
* `targetIdOf`.
|
|
72
|
+
*/
|
|
73
|
+
export const parseFolderRoleAppointmentName = (
|
|
74
|
+
name: string,
|
|
75
|
+
): { accountId: string; role: CanonicalMailboxRoleValue } | undefined =>
|
|
76
|
+
parseFolderRoleTarget(AccountSettingName.FolderRoleAppointment, name);
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The label row's counterpart, so the config-wide settings batch can recover
|
|
80
|
+
* the recorded path for a role the same way it recovers the appointment.
|
|
81
|
+
*/
|
|
82
|
+
export const parseFolderRoleAppointmentLabelName = (
|
|
83
|
+
name: string,
|
|
84
|
+
): { accountId: string; role: CanonicalMailboxRoleValue } | undefined =>
|
|
85
|
+
parseFolderRoleTarget(AccountSettingName.FolderRoleAppointmentLabel, name);
|
|
86
|
+
|
|
56
87
|
/**
|
|
57
88
|
* RFC 6154 SPECIAL-USE flag per role. Inbox has no SPECIAL-USE flag (RFC 3501
|
|
58
89
|
* reserves the name itself); a role with no entry here is matched by name hint
|
|
@@ -125,38 +156,55 @@ export interface RoleMailboxCandidate extends MailboxNameCandidate {
|
|
|
125
156
|
}
|
|
126
157
|
|
|
127
158
|
/**
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
159
|
+
* What an account with no appointment for the role resolves to, and equally
|
|
160
|
+
* what a stale appointment falls back to — the fallback can never itself be an
|
|
161
|
+
* appointment, stale or otherwise.
|
|
162
|
+
*/
|
|
163
|
+
export type UnappointedRoleResolution<T> =
|
|
164
|
+
| { kind: "flagged"; mailbox: T }
|
|
165
|
+
| { kind: "reserved"; mailbox: T }
|
|
166
|
+
| { kind: "proposed"; mailbox: T }
|
|
167
|
+
| { kind: "none" };
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* What resolving a role for an account answered, and on what evidence. Total:
|
|
171
|
+
* there is a member for every outcome, so a caller that must weigh the evidence
|
|
172
|
+
* — Empty Trash may only expunge what somebody designated — reads the tag
|
|
173
|
+
* instead of inferring it from a `null` that means four different things.
|
|
134
174
|
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
175
|
+
* `appointment_stale` is the case a nullable answer cannot express: the user
|
|
176
|
+
* appointed a mailbox the account no longer holds. It carries the id they chose
|
|
177
|
+
* so a surface can offer the repair, and the resolution that would have applied
|
|
178
|
+
* had they appointed nothing.
|
|
138
179
|
*/
|
|
139
|
-
export
|
|
180
|
+
export type RoleResolution<T> =
|
|
181
|
+
| { kind: "appointed"; mailbox: T }
|
|
182
|
+
| UnappointedRoleResolution<T>
|
|
183
|
+
| {
|
|
184
|
+
kind: "appointment_stale";
|
|
185
|
+
appointedMailboxId: string;
|
|
186
|
+
fallback: UnappointedRoleResolution<T>;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const resolveWithoutAppointment = <T extends RoleMailboxCandidate>(
|
|
140
190
|
role: CanonicalMailboxRoleValue,
|
|
141
191
|
mailboxes: readonly T[],
|
|
142
|
-
|
|
143
|
-
): T | null => {
|
|
144
|
-
if (appointedMailboxId) {
|
|
145
|
-
const appointed = mailboxes.find((m) => m.mailboxId === appointedMailboxId);
|
|
146
|
-
if (appointed) return appointed;
|
|
147
|
-
}
|
|
148
|
-
|
|
192
|
+
): UnappointedRoleResolution<T> => {
|
|
149
193
|
const specialUse = ROLE_SPECIAL_USE[role];
|
|
150
194
|
if (specialUse) {
|
|
151
195
|
const flagged = mailboxes.find((m) => m.specialUse?.includes(specialUse));
|
|
152
|
-
if (flagged) return flagged;
|
|
196
|
+
if (flagged) return { kind: "flagged", mailbox: flagged };
|
|
153
197
|
}
|
|
154
198
|
|
|
155
199
|
if (role === CanonicalMailboxRole.Inbox) {
|
|
156
|
-
|
|
200
|
+
const inbox = mailboxes.find((m) => m.fullPath.toUpperCase() === "INBOX");
|
|
201
|
+
return inbox ? { kind: "reserved", mailbox: inbox } : { kind: "none" };
|
|
157
202
|
}
|
|
158
203
|
|
|
159
|
-
|
|
204
|
+
const hints = ROLE_NAME_HINTS[role];
|
|
205
|
+
if (!hints) return { kind: "none" };
|
|
206
|
+
const proposed = resolveMailboxByLeafName(mailboxes, hints);
|
|
207
|
+
return proposed ? { kind: "proposed", mailbox: proposed } : { kind: "none" };
|
|
160
208
|
};
|
|
161
209
|
|
|
162
210
|
/**
|
|
@@ -173,23 +221,96 @@ export const resolveConfirmedMailboxForRole = <T extends RoleMailboxCandidate>(
|
|
|
173
221
|
* The last of those is a guess: a folder named `Deleted` is not evidence that
|
|
174
222
|
* the user means it as Trash. Use it only where being wrong misfiles a message
|
|
175
223
|
* a user can move back — never where it destroys mail.
|
|
224
|
+
*/
|
|
225
|
+
export const resolveRoleForAccount = <T extends RoleMailboxCandidate>(
|
|
226
|
+
role: CanonicalMailboxRoleValue,
|
|
227
|
+
mailboxes: readonly T[],
|
|
228
|
+
appointedMailboxId?: string,
|
|
229
|
+
): RoleResolution<T> => {
|
|
230
|
+
if (!appointedMailboxId) return resolveWithoutAppointment(role, mailboxes);
|
|
231
|
+
|
|
232
|
+
const appointed = mailboxes.find((m) => m.mailboxId === appointedMailboxId);
|
|
233
|
+
if (appointed) return { kind: "appointed", mailbox: appointed };
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
kind: "appointment_stale",
|
|
237
|
+
appointedMailboxId,
|
|
238
|
+
fallback: resolveWithoutAppointment(role, mailboxes),
|
|
239
|
+
};
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* How much evidence a Trash verb demands. `confirmed` is what an expunge
|
|
244
|
+
* requires: somebody designated this folder, either the user or the server.
|
|
245
|
+
* `resolved` additionally accepts the name guess, for filing mail somewhere the
|
|
246
|
+
* user can retrieve it from. Trash only, deliberately — no other role and no
|
|
247
|
+
* other verb weighs its evidence, and a matrix over eight roles would be five
|
|
248
|
+
* gates nobody asked for.
|
|
249
|
+
*/
|
|
250
|
+
export type TrashAssuranceLevel = "confirmed" | "resolved";
|
|
251
|
+
|
|
252
|
+
export const meetsTrashAssurance = <T>(
|
|
253
|
+
resolution: RoleResolution<T>,
|
|
254
|
+
level: TrashAssuranceLevel,
|
|
255
|
+
): boolean => {
|
|
256
|
+
switch (resolution.kind) {
|
|
257
|
+
case "appointed":
|
|
258
|
+
case "flagged":
|
|
259
|
+
return true;
|
|
260
|
+
case "proposed":
|
|
261
|
+
return level === "resolved";
|
|
262
|
+
default:
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const withoutStale = <T>(
|
|
268
|
+
resolution: RoleResolution<T>,
|
|
269
|
+
): { kind: "appointed"; mailbox: T } | UnappointedRoleResolution<T> =>
|
|
270
|
+
resolution.kind === "appointment_stale" ? resolution.fallback : resolution;
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The mailbox a role is CONFIRMED to hold: the one the user appointed, or the
|
|
274
|
+
* one the server flagged (RFC 6154). No name guessing — `null` here means
|
|
275
|
+
* nobody has said which folder this is, only that a folder happens to be named
|
|
276
|
+
* something plausible. An operation that destroys mail resolves through this
|
|
277
|
+
* and refuses when it comes back empty; `resolveMailboxForRole` adds the guess
|
|
278
|
+
* on top for the operations where being wrong only misfiles a message.
|
|
176
279
|
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
280
|
+
* An appointment naming a mailbox this account does not hold — deleted,
|
|
281
|
+
* renamed, or carried in from elsewhere — is stale, and falls through to the
|
|
282
|
+
* flag as if unset.
|
|
179
283
|
*/
|
|
180
|
-
export const
|
|
284
|
+
export const resolveConfirmedMailboxForRole = <T extends RoleMailboxCandidate>(
|
|
181
285
|
role: CanonicalMailboxRoleValue,
|
|
182
286
|
mailboxes: readonly T[],
|
|
183
287
|
appointedMailboxId?: string,
|
|
184
288
|
): T | null => {
|
|
185
|
-
const
|
|
186
|
-
role,
|
|
187
|
-
mailboxes,
|
|
188
|
-
appointedMailboxId,
|
|
289
|
+
const resolution = withoutStale(
|
|
290
|
+
resolveRoleForAccount(role, mailboxes, appointedMailboxId),
|
|
189
291
|
);
|
|
190
|
-
|
|
292
|
+
switch (resolution.kind) {
|
|
293
|
+
case "appointed":
|
|
294
|
+
case "flagged":
|
|
295
|
+
case "reserved":
|
|
296
|
+
return resolution.mailbox;
|
|
297
|
+
default:
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
};
|
|
191
301
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
302
|
+
/**
|
|
303
|
+
* The role's mailbox including the name guess: `null` only when nothing
|
|
304
|
+
* matches at all, so the caller says the role has no folder rather than picking
|
|
305
|
+
* one.
|
|
306
|
+
*/
|
|
307
|
+
export const resolveMailboxForRole = <T extends RoleMailboxCandidate>(
|
|
308
|
+
role: CanonicalMailboxRoleValue,
|
|
309
|
+
mailboxes: readonly T[],
|
|
310
|
+
appointedMailboxId?: string,
|
|
311
|
+
): T | null => {
|
|
312
|
+
const resolution = withoutStale(
|
|
313
|
+
resolveRoleForAccount(role, mailboxes, appointedMailboxId),
|
|
314
|
+
);
|
|
315
|
+
return resolution.kind === "none" ? null : resolution.mailbox;
|
|
195
316
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RoleResolution } from "../folder-role.js";
|
|
1
2
|
import type {
|
|
2
3
|
MailboxSpecialUseItem,
|
|
3
4
|
MailboxSpecialUseValue,
|
|
@@ -38,6 +39,16 @@ export interface IMailboxSpecialUseRepository {
|
|
|
38
39
|
findConfirmedTrashMailbox(
|
|
39
40
|
accountId: string,
|
|
40
41
|
): Promise<{ mailboxId: string; fullPath: string } | null>;
|
|
42
|
+
/**
|
|
43
|
+
* Trash with the evidence attached, for the two verbs that weigh it: a
|
|
44
|
+
* delete files mail somewhere retrievable, an Empty Trash destroys it, and
|
|
45
|
+
* they refuse on different grounds. `null` cannot tell them apart — an
|
|
46
|
+
* appointment naming a folder that is gone is a different answer from no
|
|
47
|
+
* folder at all, and only this read distinguishes them.
|
|
48
|
+
*/
|
|
49
|
+
resolveTrashRole(
|
|
50
|
+
accountId: string,
|
|
51
|
+
): Promise<RoleResolution<{ mailboxId: string; fullPath: string }>>;
|
|
41
52
|
findArchiveMailbox(
|
|
42
53
|
accountId: string,
|
|
43
54
|
): Promise<{ mailboxId: string; fullPath: string } | null>;
|