@remit/data-ports 0.0.30 → 0.0.32
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/display-name.test.ts +101 -0
- package/src/display-name.ts +40 -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.32",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
"types": "./src/account-settings.ts",
|
|
26
26
|
"default": "./src/account-settings.ts"
|
|
27
27
|
},
|
|
28
|
+
"./display-name": {
|
|
29
|
+
"types": "./src/display-name.ts",
|
|
30
|
+
"default": "./src/display-name.ts"
|
|
31
|
+
},
|
|
28
32
|
"./wellknown": {
|
|
29
33
|
"types": "./src/wellknown.ts",
|
|
30
34
|
"default": "./src/wellknown.ts"
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { storedDisplayName } from "./display-name.js";
|
|
4
|
+
|
|
5
|
+
const OWN = "matthijs@ischen.nl";
|
|
6
|
+
const SPOOFED = "aramirez@secresaludguaviare.gov.co";
|
|
7
|
+
|
|
8
|
+
const emptied: ReadonlyArray<readonly [string, string]> = [
|
|
9
|
+
["the incident", "matthijs@ischen.nl"],
|
|
10
|
+
["quoted", '"matthijs@ischen.nl"'],
|
|
11
|
+
["angle-bracketed", "<matthijs@ischen.nl>"],
|
|
12
|
+
["BATV-wrapped", "prvs=0068b51f37=matthijs@ischen.nl"],
|
|
13
|
+
["upper case", "MATTHIJS@ISCHEN.NL"],
|
|
14
|
+
["a subdomain apart", "matthijs@mail.ischen.nl"],
|
|
15
|
+
["separated by a tab", "\tmatthijs@ischen.nl"],
|
|
16
|
+
["separated by a no-break space", " matthijs@ischen.nl"],
|
|
17
|
+
["separated by a zero-width space", "matthijs@ischen.nl"],
|
|
18
|
+
["a non-ASCII local part", "Özcan@example.com"],
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const stripped: ReadonlyArray<readonly [string, string, string]> = [
|
|
22
|
+
["parenthesised", "Support (support@acme.com)", "Support"],
|
|
23
|
+
["angle-bracketed", "Matthijs <matthijs@ischen.nl>", "Matthijs"],
|
|
24
|
+
["comma-separated", "matthijs@ischen.nl, team", "team"],
|
|
25
|
+
["semicolon-separated", "Team; matthijs@ischen.nl", "Team"],
|
|
26
|
+
["colon-separated", "Reply to: matthijs@ischen.nl", "Reply to"],
|
|
27
|
+
["leading and parenthesised", "(matthijs@ischen.nl) Support", "Support"],
|
|
28
|
+
["mid-name", "Support <matthijs@ischen.nl> Team", "Support Team"],
|
|
29
|
+
["with a trailing verb", "matthijs@ischen.nl wrote", "wrote"],
|
|
30
|
+
[
|
|
31
|
+
"quoted around a name",
|
|
32
|
+
'Bob "The Builder" <bob@acme.com>',
|
|
33
|
+
'Bob "The Builder"',
|
|
34
|
+
],
|
|
35
|
+
["a tab away from the name", "Support\tmatthijs@ischen.nl", "Support"],
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const kept: ReadonlyArray<readonly [string, string]> = [
|
|
39
|
+
["an ordinary human name", "Matthijs van Henten"],
|
|
40
|
+
["a name with a full stop", "Dr. M. van Henten"],
|
|
41
|
+
["a company name", "ING Bank N.V."],
|
|
42
|
+
["a bare word", "Newsletter"],
|
|
43
|
+
["an empty name", ""],
|
|
44
|
+
["a name with an at-sign but no domain", "me @ home"],
|
|
45
|
+
["a social handle", "@matthijs"],
|
|
46
|
+
["a version string", "release 2.11"],
|
|
47
|
+
["a domain with no local part", "ischen.nl"],
|
|
48
|
+
["a single-letter tld", "a@b.c"],
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
describe("a display name that claims to be an address", () => {
|
|
52
|
+
for (const [what, name] of emptied) {
|
|
53
|
+
it(`empties ${what}`, () => {
|
|
54
|
+
assert.equal(storedDisplayName(name, SPOOFED), "");
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const [what, name, remainder] of stripped) {
|
|
59
|
+
it(`keeps what is left after ${what}`, () => {
|
|
60
|
+
assert.equal(storedDisplayName(name, SPOOFED), remainder);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const [what, name] of kept) {
|
|
65
|
+
it(`leaves ${what} alone`, () => {
|
|
66
|
+
assert.equal(storedDisplayName(name, OWN), name);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
it("keeps the address it labels", () => {
|
|
71
|
+
assert.equal(storedDisplayName(OWN, OWN), OWN);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("keeps its own address inside a longer name", () => {
|
|
75
|
+
const name = "Matthijs <matthijs@ischen.nl>";
|
|
76
|
+
assert.equal(storedDisplayName(name, OWN), name);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("folds case beyond ASCII on both sides", () => {
|
|
80
|
+
assert.equal(
|
|
81
|
+
storedDisplayName("Özcan@example.com", "özcan@example.com"),
|
|
82
|
+
"Özcan@example.com",
|
|
83
|
+
);
|
|
84
|
+
assert.equal(
|
|
85
|
+
storedDisplayName("ÖZCAN@EXAMPLE.COM", "özcan@example.com"),
|
|
86
|
+
"ÖZCAN@EXAMPLE.COM",
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("removes any address when the envelope carried none", () => {
|
|
91
|
+
assert.equal(storedDisplayName(`Support ${OWN}`, undefined), "Support");
|
|
92
|
+
assert.equal(storedDisplayName("Matthijs", undefined), "Matthijs");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("removes a second address and keeps its own", () => {
|
|
96
|
+
assert.equal(
|
|
97
|
+
storedDisplayName(`${OWN} on behalf of ${SPOOFED}`, OWN),
|
|
98
|
+
`${OWN} on behalf of`,
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const EMBEDDED_ADDRESS =
|
|
2
|
+
/[^\s@<>()[\],;:"'\\]+@[^\s@<>()[\],;:"'\\]+\.[^\s@<>()[\],;:"'\\.]{2,}/gu;
|
|
3
|
+
|
|
4
|
+
const DELIMITER_PAIR_LEFT_EMPTY = /[<([{"']\s*[>)\]}"']/gu;
|
|
5
|
+
|
|
6
|
+
const LEADING_OR_TRAILING_SEPARATORS = /^[\s,;:|/\\-]+|[\s,;:|/\\-]+$/gu;
|
|
7
|
+
|
|
8
|
+
const tidyAfterAddressRemoval = (text: string): string =>
|
|
9
|
+
text
|
|
10
|
+
.replace(DELIMITER_PAIR_LEFT_EMPTY, " ")
|
|
11
|
+
.replace(/\s+/gu, " ")
|
|
12
|
+
.replace(LEADING_OR_TRAILING_SEPARATORS, "")
|
|
13
|
+
.trim();
|
|
14
|
+
|
|
15
|
+
const carriesAddressOtherThan = (
|
|
16
|
+
text: string,
|
|
17
|
+
ownAddress: string | undefined,
|
|
18
|
+
): boolean => {
|
|
19
|
+
for (const [address] of text.matchAll(EMBEDDED_ADDRESS)) {
|
|
20
|
+
if (address.toLowerCase() !== ownAddress) return true;
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const storedDisplayName = (
|
|
26
|
+
displayName: string | undefined,
|
|
27
|
+
normalizedEmail: string | undefined,
|
|
28
|
+
): string => {
|
|
29
|
+
if (!displayName) return "";
|
|
30
|
+
const ownAddress = normalizedEmail?.toLowerCase();
|
|
31
|
+
if (!carriesAddressOtherThan(displayName, ownAddress)) return displayName;
|
|
32
|
+
|
|
33
|
+
const remainder = tidyAfterAddressRemoval(
|
|
34
|
+
displayName.replace(EMBEDDED_ADDRESS, (address) =>
|
|
35
|
+
address.toLowerCase() === ownAddress ? address : " ",
|
|
36
|
+
),
|
|
37
|
+
);
|
|
38
|
+
if (!remainder || carriesAddressOtherThan(remainder, ownAddress)) return "";
|
|
39
|
+
return remainder;
|
|
40
|
+
};
|