@remit/data-ports 0.0.32 → 0.0.34
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 +9 -1
- package/src/interfaces/address.ts +3 -0
- package/src/mailbox-name.test.ts +70 -0
- package/src/mailbox-name.ts +54 -0
- package/src/mailbox-role.ts +46 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/data-ports",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.34",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -25,10 +25,18 @@
|
|
|
25
25
|
"types": "./src/account-settings.ts",
|
|
26
26
|
"default": "./src/account-settings.ts"
|
|
27
27
|
},
|
|
28
|
+
"./mailbox-role": {
|
|
29
|
+
"types": "./src/mailbox-role.ts",
|
|
30
|
+
"default": "./src/mailbox-role.ts"
|
|
31
|
+
},
|
|
28
32
|
"./display-name": {
|
|
29
33
|
"types": "./src/display-name.ts",
|
|
30
34
|
"default": "./src/display-name.ts"
|
|
31
35
|
},
|
|
36
|
+
"./mailbox-name": {
|
|
37
|
+
"types": "./src/mailbox-name.ts",
|
|
38
|
+
"default": "./src/mailbox-name.ts"
|
|
39
|
+
},
|
|
32
40
|
"./wellknown": {
|
|
33
41
|
"types": "./src/wellknown.ts",
|
|
34
42
|
"default": "./src/wellknown.ts"
|
|
@@ -11,6 +11,9 @@ import type {
|
|
|
11
11
|
export interface IAddressRepository {
|
|
12
12
|
createAddress(input: CreateAddressInput): Promise<AddressItem>;
|
|
13
13
|
upsertAddress(input: CreateAddressInput): Promise<AddressItem>;
|
|
14
|
+
upsertCorrespondentAddress(input: CreateAddressInput): Promise<AddressItem>;
|
|
15
|
+
upsertJunkAddress(input: CreateAddressInput): Promise<AddressItem>;
|
|
16
|
+
reconcileJunkOnlyForMessage(messageId: string): Promise<void>;
|
|
14
17
|
getAddress(accountConfigId: string, addressId: string): Promise<AddressItem>;
|
|
15
18
|
getAddress(
|
|
16
19
|
accountConfigId: string,
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { resolveMailboxByLeafName } from "./mailbox-name.js";
|
|
4
|
+
|
|
5
|
+
const JUNK_NAMES = ["junk", "spam", "junk e-mail", "junk email", "bulk mail"];
|
|
6
|
+
|
|
7
|
+
const mailbox = (
|
|
8
|
+
mailboxId: string,
|
|
9
|
+
fullPath: string,
|
|
10
|
+
hierarchyDelimiter = "/",
|
|
11
|
+
) => ({ mailboxId, fullPath, hierarchyDelimiter });
|
|
12
|
+
|
|
13
|
+
describe("resolveMailboxByLeafName", () => {
|
|
14
|
+
it("resolves a Junk folder nested under INBOX", () => {
|
|
15
|
+
// The layout that broke report-spam's name fallback: an INBOX-prefixed
|
|
16
|
+
// namespace where Junk is `INBOX/Spam`, which never equals "spam".
|
|
17
|
+
const found = resolveMailboxByLeafName(
|
|
18
|
+
[
|
|
19
|
+
mailbox("m-inbox", "INBOX"),
|
|
20
|
+
mailbox("m-spam", "INBOX/Spam"),
|
|
21
|
+
mailbox("m-sent", "INBOX/Sent"),
|
|
22
|
+
],
|
|
23
|
+
JUNK_NAMES,
|
|
24
|
+
);
|
|
25
|
+
assert.equal(found?.mailboxId, "m-spam");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("resolves under a non-slash delimiter and a non-INBOX prefix", () => {
|
|
29
|
+
const found = resolveMailboxByLeafName(
|
|
30
|
+
[mailbox("m-junk", "Mail.Junk E-mail", ".")],
|
|
31
|
+
JUNK_NAMES,
|
|
32
|
+
);
|
|
33
|
+
assert.equal(found?.mailboxId, "m-junk");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("treats a flat namespace with no delimiter as its own leaf", () => {
|
|
37
|
+
const found = resolveMailboxByLeafName(
|
|
38
|
+
[mailbox("m-junk", "Junk", "")],
|
|
39
|
+
JUNK_NAMES,
|
|
40
|
+
);
|
|
41
|
+
assert.equal(found?.mailboxId, "m-junk");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("prefers the shallowest match over a deeper one", () => {
|
|
45
|
+
const found = resolveMailboxByLeafName(
|
|
46
|
+
[
|
|
47
|
+
mailbox("m-buried", "Archive/2019/Spam"),
|
|
48
|
+
mailbox("m-real", "INBOX/Junk"),
|
|
49
|
+
],
|
|
50
|
+
JUNK_NAMES,
|
|
51
|
+
);
|
|
52
|
+
assert.equal(found?.mailboxId, "m-real");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("prefers the better name at equal depth", () => {
|
|
56
|
+
const found = resolveMailboxByLeafName(
|
|
57
|
+
[mailbox("m-bulk", "INBOX/Bulk Mail"), mailbox("m-junk", "INBOX/Junk")],
|
|
58
|
+
JUNK_NAMES,
|
|
59
|
+
);
|
|
60
|
+
assert.equal(found?.mailboxId, "m-junk");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("answers null when no folder carries a conventional name", () => {
|
|
64
|
+
const found = resolveMailboxByLeafName(
|
|
65
|
+
[mailbox("m-inbox", "INBOX"), mailbox("m-work", "INBOX/Work")],
|
|
66
|
+
JUNK_NAMES,
|
|
67
|
+
);
|
|
68
|
+
assert.equal(found, null);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface MailboxNameCandidate {
|
|
2
|
+
mailboxId: string;
|
|
3
|
+
fullPath: string;
|
|
4
|
+
hierarchyDelimiter: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// A flat mailbox namespace reports no delimiter at all — ImapFlow gives `""`
|
|
8
|
+
// for a NIL LIST delimiter — and splitting on "" returns single characters, so
|
|
9
|
+
// the path is its own leaf.
|
|
10
|
+
const segments = (mailbox: MailboxNameCandidate): string[] =>
|
|
11
|
+
mailbox.hierarchyDelimiter.length === 0
|
|
12
|
+
? [mailbox.fullPath]
|
|
13
|
+
: mailbox.fullPath.split(mailbox.hierarchyDelimiter);
|
|
14
|
+
|
|
15
|
+
const rank = (
|
|
16
|
+
mailbox: MailboxNameCandidate,
|
|
17
|
+
names: readonly string[],
|
|
18
|
+
): number | null => {
|
|
19
|
+
const parts = segments(mailbox);
|
|
20
|
+
const leaf = parts[parts.length - 1] ?? mailbox.fullPath;
|
|
21
|
+
const nameIndex = names.indexOf(leaf.toLowerCase());
|
|
22
|
+
if (nameIndex < 0) return null;
|
|
23
|
+
// Depth outranks the name: a "Spam" buried under Trash or an archive must
|
|
24
|
+
// never beat the account's real "Junk" one level up.
|
|
25
|
+
return parts.length * names.length + nameIndex;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A special-use folder by conventional name, for servers that advertise no
|
|
30
|
+
* special-use flag for it. Matches the folder's own leaf segment against its
|
|
31
|
+
* mailbox's own hierarchy delimiter, so it resolves at any depth under any
|
|
32
|
+
* prefix (`INBOX/Spam`, `Mail.Junk E-mail`, `[Gmail]/Spam`) without knowing
|
|
33
|
+
* which prefixes a server uses. `names` is ordered best-first and must be
|
|
34
|
+
* lower case.
|
|
35
|
+
*/
|
|
36
|
+
export const resolveMailboxByLeafName = <T extends MailboxNameCandidate>(
|
|
37
|
+
mailboxes: T[],
|
|
38
|
+
names: readonly string[],
|
|
39
|
+
): T | null => {
|
|
40
|
+
let best: { mailbox: T; rank: number } | null = null;
|
|
41
|
+
for (const mailbox of mailboxes) {
|
|
42
|
+
const candidate = rank(mailbox, names);
|
|
43
|
+
if (candidate === null) continue;
|
|
44
|
+
if (
|
|
45
|
+
!best ||
|
|
46
|
+
candidate < best.rank ||
|
|
47
|
+
(candidate === best.rank &&
|
|
48
|
+
mailbox.fullPath.localeCompare(best.mailbox.fullPath) < 0)
|
|
49
|
+
) {
|
|
50
|
+
best = { mailbox, rank: candidate };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return best?.mailbox ?? null;
|
|
54
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { MailboxSpecialUse } from "@remit/domain-enums";
|
|
2
|
+
import { resolveMailboxByLeafName } from "./mailbox-name.js";
|
|
3
|
+
|
|
4
|
+
export const JUNK_FOLDER_NAMES: readonly string[] = [
|
|
5
|
+
"junk",
|
|
6
|
+
"spam",
|
|
7
|
+
"junk e-mail",
|
|
8
|
+
"junk email",
|
|
9
|
+
"bulk mail",
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
export const TRASH_FOLDER_NAMES: readonly string[] = [
|
|
13
|
+
"trash",
|
|
14
|
+
"deleted items",
|
|
15
|
+
"deleted",
|
|
16
|
+
"bin",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export interface MailboxRole {
|
|
20
|
+
readonly fullPath: string;
|
|
21
|
+
readonly hierarchyDelimiter?: string;
|
|
22
|
+
readonly specialUse?: readonly string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const carriesRole = (
|
|
26
|
+
mailbox: MailboxRole,
|
|
27
|
+
specialUse: string,
|
|
28
|
+
namesWhenServerDoesNotSaySo: readonly string[],
|
|
29
|
+
): boolean =>
|
|
30
|
+
mailbox.specialUse?.includes(specialUse) === true ||
|
|
31
|
+
resolveMailboxByLeafName(
|
|
32
|
+
[
|
|
33
|
+
{
|
|
34
|
+
mailboxId: "",
|
|
35
|
+
fullPath: mailbox.fullPath,
|
|
36
|
+
hierarchyDelimiter: mailbox.hierarchyDelimiter ?? "",
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
namesWhenServerDoesNotSaySo,
|
|
40
|
+
) !== null;
|
|
41
|
+
|
|
42
|
+
export const isJunkMailbox = (mailbox: MailboxRole): boolean =>
|
|
43
|
+
carriesRole(mailbox, MailboxSpecialUse.Junk, JUNK_FOLDER_NAMES);
|
|
44
|
+
|
|
45
|
+
export const isTrashMailbox = (mailbox: MailboxRole): boolean =>
|
|
46
|
+
carriesRole(mailbox, MailboxSpecialUse.Trash, TRASH_FOLDER_NAMES);
|