@remit/data-ports 0.0.31 → 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 +1 -1
- package/src/display-name.test.ts +0 -13
- package/src/display-name.ts +13 -59
package/package.json
CHANGED
package/src/display-name.test.ts
CHANGED
|
@@ -5,9 +5,6 @@ import { storedDisplayName } from "./display-name.js";
|
|
|
5
5
|
const OWN = "matthijs@ischen.nl";
|
|
6
6
|
const SPOOFED = "aramirez@secresaludguaviare.gov.co";
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
* The name is a stranger's address and nothing else, so nothing survives it.
|
|
10
|
-
*/
|
|
11
8
|
const emptied: ReadonlyArray<readonly [string, string]> = [
|
|
12
9
|
["the incident", "matthijs@ischen.nl"],
|
|
13
10
|
["quoted", '"matthijs@ischen.nl"'],
|
|
@@ -21,11 +18,6 @@ const emptied: ReadonlyArray<readonly [string, string]> = [
|
|
|
21
18
|
["a non-ASCII local part", "Özcan@example.com"],
|
|
22
19
|
];
|
|
23
20
|
|
|
24
|
-
/**
|
|
25
|
-
* The name says something besides the address, and that something is what its
|
|
26
|
-
* recipient knows the sender by. Removing it to remove the address destroys
|
|
27
|
-
* real text on a live instance.
|
|
28
|
-
*/
|
|
29
21
|
const stripped: ReadonlyArray<readonly [string, string, string]> = [
|
|
30
22
|
["parenthesised", "Support (support@acme.com)", "Support"],
|
|
31
23
|
["angle-bracketed", "Matthijs <matthijs@ischen.nl>", "Matthijs"],
|
|
@@ -84,11 +76,6 @@ describe("a display name that claims to be an address", () => {
|
|
|
84
76
|
assert.equal(storedDisplayName(name, OWN), name);
|
|
85
77
|
});
|
|
86
78
|
|
|
87
|
-
/**
|
|
88
|
-
* SQLite folds `A-Z` and stops; JS folds the whole of Unicode. A rule spelled
|
|
89
|
-
* once in SQL and once in TypeScript disagrees exactly here, and the
|
|
90
|
-
* disagreement rewrites a name on a live database.
|
|
91
|
-
*/
|
|
92
79
|
it("folds case beyond ASCII on both sides", () => {
|
|
93
80
|
assert.equal(
|
|
94
81
|
storedDisplayName("Özcan@example.com", "özcan@example.com"),
|
package/src/display-name.ts
CHANGED
|
@@ -1,86 +1,40 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A display name that claims to be an email address (issue #826).
|
|
3
|
-
*
|
|
4
|
-
* The name on an envelope address is free text its sender chooses; the address
|
|
5
|
-
* is verifiable. A spam envelope of
|
|
6
|
-
* `matthijs@ischen.nl <aramirez@secresaludguaviare.gov.co>` therefore labels a
|
|
7
|
-
* stranger's address with the reader's own, and every surface that shows a name
|
|
8
|
-
* — the message list, the message header, contact autocomplete — repeats the
|
|
9
|
-
* claim. On 2026-08-18 autocomplete offered such a row back and private mail
|
|
10
|
-
* left the instance.
|
|
11
|
-
*
|
|
12
|
-
* The rule is one function because it has to hold in two places at once: the
|
|
13
|
-
* harvest that decides what to store, and the repair that rewrites what is
|
|
14
|
-
* already stored. Two spellings of it disagree at the edges — SQL `lower()`
|
|
15
|
-
* folds ASCII only where JS folds all of Unicode — and every disagreement
|
|
16
|
-
* rewrites a name on a live database that holds the only copy of it.
|
|
17
|
-
*
|
|
18
|
-
* What comes out is the name minus the claim, not the empty string. A sender
|
|
19
|
-
* signing itself `Support (support@acme.com)` from `noreply@acme.com` is
|
|
20
|
-
* ordinary mail, and `Support` is the name its recipient knows it by; deleting
|
|
21
|
-
* that to remove the address takes real text off a live instance. Only the
|
|
22
|
-
* address is removed, with the punctuation that was holding it, and a name that
|
|
23
|
-
* is nothing but the claim comes back empty.
|
|
24
|
-
*
|
|
25
|
-
* An address the name shares with the address it labels is not a claim about
|
|
26
|
-
* anyone else, so it stays — `Matthijs <matthijs@ischen.nl>` on
|
|
27
|
-
* `matthijs@ischen.nl` is the same information twice.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
1
|
const EMBEDDED_ADDRESS =
|
|
31
2
|
/[^\s@<>()[\],;:"'\\]+@[^\s@<>()[\],;:"'\\]+\.[^\s@<>()[\],;:"'\\.]{2,}/gu;
|
|
32
3
|
|
|
33
|
-
|
|
34
|
-
const EMPTIED_PAIR = /[<([{"']\s*[>)\]}"']/gu;
|
|
4
|
+
const DELIMITER_PAIR_LEFT_EMPTY = /[<([{"']\s*[>)\]}"']/gu;
|
|
35
5
|
|
|
36
|
-
|
|
37
|
-
* What is left holding the address once it is gone: the separators around it.
|
|
38
|
-
* Brackets and quotes are not trimmed here — EMPTIED_PAIR has already taken the
|
|
39
|
-
* ones that were wrapping the address, and a name can legitimately end in one.
|
|
40
|
-
*/
|
|
41
|
-
const EDGE_SEPARATORS = /^[\s,;:|/\\-]+|[\s,;:|/\\-]+$/gu;
|
|
6
|
+
const LEADING_OR_TRAILING_SEPARATORS = /^[\s,;:|/\\-]+|[\s,;:|/\\-]+$/gu;
|
|
42
7
|
|
|
43
|
-
const
|
|
8
|
+
const tidyAfterAddressRemoval = (text: string): string =>
|
|
44
9
|
text
|
|
45
|
-
.replace(
|
|
10
|
+
.replace(DELIMITER_PAIR_LEFT_EMPTY, " ")
|
|
46
11
|
.replace(/\s+/gu, " ")
|
|
47
|
-
.replace(
|
|
12
|
+
.replace(LEADING_OR_TRAILING_SEPARATORS, "")
|
|
48
13
|
.trim();
|
|
49
14
|
|
|
50
|
-
|
|
51
|
-
* `normalizedEmail` absent means the envelope carried no address this name
|
|
52
|
-
* could be describing, so any address in the name is a claim about nobody.
|
|
53
|
-
*/
|
|
54
|
-
const claimsAnotherAddress = (
|
|
15
|
+
const carriesAddressOtherThan = (
|
|
55
16
|
text: string,
|
|
56
|
-
|
|
17
|
+
ownAddress: string | undefined,
|
|
57
18
|
): boolean => {
|
|
58
19
|
for (const [address] of text.matchAll(EMBEDDED_ADDRESS)) {
|
|
59
|
-
if (address.toLowerCase() !==
|
|
20
|
+
if (address.toLowerCase() !== ownAddress) return true;
|
|
60
21
|
}
|
|
61
22
|
return false;
|
|
62
23
|
};
|
|
63
24
|
|
|
64
|
-
/**
|
|
65
|
-
* The display name as it should be stored, for both the harvest and the repair.
|
|
66
|
-
* Unchanged when the name claims nothing.
|
|
67
|
-
*/
|
|
68
25
|
export const storedDisplayName = (
|
|
69
26
|
displayName: string | undefined,
|
|
70
27
|
normalizedEmail: string | undefined,
|
|
71
28
|
): string => {
|
|
72
29
|
if (!displayName) return "";
|
|
73
|
-
const
|
|
74
|
-
if (!
|
|
30
|
+
const ownAddress = normalizedEmail?.toLowerCase();
|
|
31
|
+
if (!carriesAddressOtherThan(displayName, ownAddress)) return displayName;
|
|
75
32
|
|
|
76
|
-
const remainder =
|
|
33
|
+
const remainder = tidyAfterAddressRemoval(
|
|
77
34
|
displayName.replace(EMBEDDED_ADDRESS, (address) =>
|
|
78
|
-
address.toLowerCase() ===
|
|
35
|
+
address.toLowerCase() === ownAddress ? address : " ",
|
|
79
36
|
),
|
|
80
37
|
);
|
|
81
|
-
|
|
82
|
-
// backstop rather than a case: nothing that still claims another address is
|
|
83
|
-
// ever stored, whatever the shape it was hiding in.
|
|
84
|
-
if (!remainder || claimsAnotherAddress(remainder, own)) return "";
|
|
38
|
+
if (!remainder || carriesAddressOtherThan(remainder, ownAddress)) return "";
|
|
85
39
|
return remainder;
|
|
86
40
|
};
|