@bobfrankston/mailx-types 0.1.111 → 0.1.112
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/address.d.ts +54 -0
- package/address.js +138 -0
- package/groups.js +6 -5
- package/index.d.ts +2 -14
- package/index.js +6 -57
- package/list-identity.js +3 -1
- package/package.json +1 -1
- package/trust.js +6 -2
package/address.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email address parsing — the ONE place mailx reads addresses out of header
|
|
3
|
+
* values and decides whether a string is an address.
|
|
4
|
+
*
|
|
5
|
+
* 2026-10-26 — Claude Code (Opus 5.5), at Bob's direction ("fold the other
|
|
6
|
+
* address regexes into the shared functions"). Before this, about a dozen
|
|
7
|
+
* call sites each carried their own address regex: the send paths, trust,
|
|
8
|
+
* the store, list-identity, compose, the editor's link detection, contacts,
|
|
9
|
+
* groups and the CLI. They disagreed on which characters an address may
|
|
10
|
+
* hold, and the narrowest one (`[\w.+-]+@`) cut
|
|
11
|
+
* `bernardgreenberg=msn.com@groups.io` to `msn.com@groups.io` in RCPT TO.
|
|
12
|
+
* Parsing is structural here — split the header on top-level commas, take
|
|
13
|
+
* the `<…>` part or the bare entry — so no function guesses at the
|
|
14
|
+
* characters of a local part.
|
|
15
|
+
*
|
|
16
|
+
* Pure, no I/O, browser-safe.
|
|
17
|
+
*/
|
|
18
|
+
/** One parsed entry of an address header. */
|
|
19
|
+
export interface ParsedAddress {
|
|
20
|
+
/** Display name, unquoted and unescaped; "" when there is none. */
|
|
21
|
+
name: string;
|
|
22
|
+
/** The addr-spec, as written (case kept). */
|
|
23
|
+
address: string;
|
|
24
|
+
}
|
|
25
|
+
/** Split an address header on top-level commas (outside quotes and angle
|
|
26
|
+
* brackets; `\` escapes inside quotes honored). Moved here from index.ts. */
|
|
27
|
+
export declare function splitAddressHeader(value: string): string[];
|
|
28
|
+
/** Every entry of an address header that carries an addr-spec, with its
|
|
29
|
+
* display name. Entries with no address (a bare name, an empty group like
|
|
30
|
+
* `Undisclosed recipients:;`) are left out. */
|
|
31
|
+
export declare function parseAddressList(value: string): ParsedAddress[];
|
|
32
|
+
/** The first entry of an address header value, or null. */
|
|
33
|
+
export declare function parseAddress(value: string): ParsedAddress | null;
|
|
34
|
+
/** The addr-spec of every entry — what SMTP RCPT TO / MAIL FROM must carry.
|
|
35
|
+
*
|
|
36
|
+
* 2026-10-25: replaced the `/[\w.+-]+@[\w.-]+/g` scan that all three send
|
|
37
|
+
* paths carried, which omitted characters RFC 5322 allows in a local part
|
|
38
|
+
* (`= ! # $ % & ' * / ? ^ { | } ~`) and so turned
|
|
39
|
+
* `bernardgreenberg=msn.com@groups.io` into `msn.com@groups.io`. */
|
|
40
|
+
export declare function envelopeAddresses(value: string): string[];
|
|
41
|
+
/** The first addr-spec in a header value (`"Name" <a@b>`, `a@b (comment)`,
|
|
42
|
+
* or a bare `a@b`), or "". Case kept; callers that compare lowercase it. */
|
|
43
|
+
export declare function firstAddress(value: string): string;
|
|
44
|
+
/** Is `s` — a bare addr-spec, no display name — plausibly deliverable?
|
|
45
|
+
* Something before the last `@`, a dotted domain after it, and no blanks or
|
|
46
|
+
* angle brackets anywhere. The rule all five copies of
|
|
47
|
+
* `/^[^\s<>@]+@[^\s<>@]+\.[^\s<>@]+$/` encoded, written without a pattern
|
|
48
|
+
* whose domain part could be split two ways. Any character RFC 5322 allows
|
|
49
|
+
* in a local part passes. */
|
|
50
|
+
export declare function isValidAddress(s: string): boolean;
|
|
51
|
+
export declare const ADDRESS_IN_TEXT: string;
|
|
52
|
+
/** Every address-shaped run in free text. */
|
|
53
|
+
export declare function findAddressesInText(text: string): string[];
|
|
54
|
+
//# sourceMappingURL=address.d.ts.map
|
package/address.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email address parsing — the ONE place mailx reads addresses out of header
|
|
3
|
+
* values and decides whether a string is an address.
|
|
4
|
+
*
|
|
5
|
+
* 2026-10-26 — Claude Code (Opus 5.5), at Bob's direction ("fold the other
|
|
6
|
+
* address regexes into the shared functions"). Before this, about a dozen
|
|
7
|
+
* call sites each carried their own address regex: the send paths, trust,
|
|
8
|
+
* the store, list-identity, compose, the editor's link detection, contacts,
|
|
9
|
+
* groups and the CLI. They disagreed on which characters an address may
|
|
10
|
+
* hold, and the narrowest one (`[\w.+-]+@`) cut
|
|
11
|
+
* `bernardgreenberg=msn.com@groups.io` to `msn.com@groups.io` in RCPT TO.
|
|
12
|
+
* Parsing is structural here — split the header on top-level commas, take
|
|
13
|
+
* the `<…>` part or the bare entry — so no function guesses at the
|
|
14
|
+
* characters of a local part.
|
|
15
|
+
*
|
|
16
|
+
* Pure, no I/O, browser-safe.
|
|
17
|
+
*/
|
|
18
|
+
/** Split an address header on top-level commas (outside quotes and angle
|
|
19
|
+
* brackets; `\` escapes inside quotes honored). Moved here from index.ts. */
|
|
20
|
+
export function splitAddressHeader(value) {
|
|
21
|
+
const parts = [];
|
|
22
|
+
let cur = "";
|
|
23
|
+
let inAngle = false;
|
|
24
|
+
let inQuote = false;
|
|
25
|
+
for (let i = 0; i < value.length; i++) {
|
|
26
|
+
const ch = value[i];
|
|
27
|
+
if (inQuote && ch === "\\" && i + 1 < value.length) {
|
|
28
|
+
cur += ch + value[++i];
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (ch === '"' && !inAngle)
|
|
32
|
+
inQuote = !inQuote;
|
|
33
|
+
else if (ch === "<" && !inQuote)
|
|
34
|
+
inAngle = true;
|
|
35
|
+
else if (ch === ">" && !inQuote)
|
|
36
|
+
inAngle = false;
|
|
37
|
+
if (ch === "," && !inAngle && !inQuote) {
|
|
38
|
+
parts.push(cur);
|
|
39
|
+
cur = "";
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
cur += ch;
|
|
43
|
+
}
|
|
44
|
+
if (cur.trim())
|
|
45
|
+
parts.push(cur);
|
|
46
|
+
return parts.map(p => p.trim()).filter(Boolean);
|
|
47
|
+
}
|
|
48
|
+
/** Text on both sides of the last `@`: the least an addr-spec must have. */
|
|
49
|
+
function hasAddrShape(spec) {
|
|
50
|
+
const at = spec.lastIndexOf("@");
|
|
51
|
+
return at > 0 && at < spec.length - 1;
|
|
52
|
+
}
|
|
53
|
+
/** Every entry of an address header that carries an addr-spec, with its
|
|
54
|
+
* display name. Entries with no address (a bare name, an empty group like
|
|
55
|
+
* `Undisclosed recipients:;`) are left out. */
|
|
56
|
+
export function parseAddressList(value) {
|
|
57
|
+
if (!value)
|
|
58
|
+
return [];
|
|
59
|
+
const out = [];
|
|
60
|
+
for (const part of splitAddressHeader(value)) {
|
|
61
|
+
const angle = /<([^<>]*)>\s*$/.exec(part);
|
|
62
|
+
let name = "";
|
|
63
|
+
let spec;
|
|
64
|
+
if (angle) {
|
|
65
|
+
spec = angle[1].trim();
|
|
66
|
+
name = part.slice(0, angle.index).replace(/\([^()]*\)/g, "").trim();
|
|
67
|
+
if (name.length >= 2 && name.startsWith('"') && name.endsWith('"'))
|
|
68
|
+
name = name.slice(1, -1).replace(/\\(.)/g, "$1");
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// No angle brackets: an addr-spec has no unquoted blanks, so when
|
|
72
|
+
// the entry has words around it ("mailing list x@y", a sloppy
|
|
73
|
+
// "Bob bob@x.com") the address is the word that has the shape.
|
|
74
|
+
const words = part.replace(/\([^()]*\)/g, " ").trim().split(/\s+/);
|
|
75
|
+
spec = words.find(hasAddrShape) || "";
|
|
76
|
+
}
|
|
77
|
+
if (hasAddrShape(spec))
|
|
78
|
+
out.push({ name, address: spec });
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
/** The first entry of an address header value, or null. */
|
|
83
|
+
export function parseAddress(value) {
|
|
84
|
+
return parseAddressList(value)[0] || null;
|
|
85
|
+
}
|
|
86
|
+
/** The addr-spec of every entry — what SMTP RCPT TO / MAIL FROM must carry.
|
|
87
|
+
*
|
|
88
|
+
* 2026-10-25: replaced the `/[\w.+-]+@[\w.-]+/g` scan that all three send
|
|
89
|
+
* paths carried, which omitted characters RFC 5322 allows in a local part
|
|
90
|
+
* (`= ! # $ % & ' * / ? ^ { | } ~`) and so turned
|
|
91
|
+
* `bernardgreenberg=msn.com@groups.io` into `msn.com@groups.io`. */
|
|
92
|
+
export function envelopeAddresses(value) {
|
|
93
|
+
return parseAddressList(value).map(a => a.address);
|
|
94
|
+
}
|
|
95
|
+
/** The first addr-spec in a header value (`"Name" <a@b>`, `a@b (comment)`,
|
|
96
|
+
* or a bare `a@b`), or "". Case kept; callers that compare lowercase it. */
|
|
97
|
+
export function firstAddress(value) {
|
|
98
|
+
return parseAddressList(value)[0]?.address || "";
|
|
99
|
+
}
|
|
100
|
+
/** Is `s` — a bare addr-spec, no display name — plausibly deliverable?
|
|
101
|
+
* Something before the last `@`, a dotted domain after it, and no blanks or
|
|
102
|
+
* angle brackets anywhere. The rule all five copies of
|
|
103
|
+
* `/^[^\s<>@]+@[^\s<>@]+\.[^\s<>@]+$/` encoded, written without a pattern
|
|
104
|
+
* whose domain part could be split two ways. Any character RFC 5322 allows
|
|
105
|
+
* in a local part passes. */
|
|
106
|
+
export function isValidAddress(s) {
|
|
107
|
+
const t = (s || "").trim();
|
|
108
|
+
if (!t || /[\s<>]/.test(t))
|
|
109
|
+
return false;
|
|
110
|
+
const at = t.lastIndexOf("@");
|
|
111
|
+
if (at <= 0)
|
|
112
|
+
return false;
|
|
113
|
+
const local = t.slice(0, at);
|
|
114
|
+
const domain = t.slice(at + 1);
|
|
115
|
+
if (local.includes("@") || domain.includes("@"))
|
|
116
|
+
return false;
|
|
117
|
+
const dot = domain.indexOf(".");
|
|
118
|
+
return dot > 0 && dot < domain.length - 1;
|
|
119
|
+
}
|
|
120
|
+
/** Regex SOURCE for an address appearing inside free text (prose, a display
|
|
121
|
+
* name) — where there is no header structure to split on, so a pattern is
|
|
122
|
+
* the only tool. Local part: the RFC 5322 atext characters plus dots; domain:
|
|
123
|
+
* dotted labels. Each piece matches one way only (`@` and `.` are not in the
|
|
124
|
+
* classes they separate). Exported as a source string so a caller can embed
|
|
125
|
+
* it in a larger alternation (the spell checker's skip spans).
|
|
126
|
+
* (2026-10-26, Claude Code (Opus 5.5): replaces the spell checker's
|
|
127
|
+
* `[\w.+\-]+@…` and displayNameClaimsOtherAddress's `[A-Za-z0-9._%+-]+@…`.) */
|
|
128
|
+
// The lookbehind lets a match START only where a run of local-part characters
|
|
129
|
+
// starts. Without it a long run with no `@` (a pasted base64 blob) was retried
|
|
130
|
+
// from every position inside it, re-reading the rest each time: 80,000
|
|
131
|
+
// characters took 5.3 s (measured 2026-10-26). With it each run is read once.
|
|
132
|
+
const LOCAL_CHARS = String.raw `\w.!#$%&'*+\/=?^\`{|}~\-`;
|
|
133
|
+
export const ADDRESS_IN_TEXT = String.raw `(?<![${LOCAL_CHARS}])[${LOCAL_CHARS}]+@[A-Za-z0-9\-]+(?:\.[A-Za-z0-9\-]+)+`;
|
|
134
|
+
/** Every address-shaped run in free text. */
|
|
135
|
+
export function findAddressesInText(text) {
|
|
136
|
+
return (text || "").match(new RegExp(ADDRESS_IN_TEXT, "g")) || [];
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=address.js.map
|
package/groups.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* names of other groups (recursive aliasing). Cycles are detected; depth
|
|
9
9
|
* is capped at 10 nested levels.
|
|
10
10
|
*/
|
|
11
|
+
import { firstAddress } from "./address.js";
|
|
11
12
|
/** Split a comma-or-semicolon recipient string into tokens. Respects
|
|
12
13
|
* angle-bracket address blocks ("Name, Suffix <a@b>") so a comma inside
|
|
13
14
|
* the display name doesn't split the entry. */
|
|
@@ -40,15 +41,15 @@ export function splitRecipients(raw) {
|
|
|
40
41
|
* a display name)? Anything containing "@" with non-whitespace on both
|
|
41
42
|
* sides counts. */
|
|
42
43
|
export function isAddressToken(token) {
|
|
43
|
-
|
|
44
|
+
// 2026-10-26 — Claude Code (Opus 5.5): shared parser (address.ts) instead of
|
|
45
|
+
// its own patterns; "Name <a@b>" and a bare "a@b" both parse.
|
|
46
|
+
return !!firstAddress(token);
|
|
44
47
|
}
|
|
45
48
|
/** Extract the bare email address from a token. Returns the original token
|
|
46
49
|
* lowercased if it has no angle-bracket form. */
|
|
47
50
|
export function extractAddress(token) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return m[1].trim().toLowerCase();
|
|
51
|
-
return token.trim().toLowerCase();
|
|
51
|
+
// 2026-10-26 — Claude Code (Opus 5.5): shared parser (address.ts).
|
|
52
|
+
return (firstAddress(token) || token).trim().toLowerCase();
|
|
52
53
|
}
|
|
53
54
|
/** Expand a recipient string by resolving group names against `groups`.
|
|
54
55
|
* Group names take precedence: if a token matches a group name AND looks
|
package/index.d.ts
CHANGED
|
@@ -427,20 +427,6 @@ export declare function formatAddressListForHeader(list: {
|
|
|
427
427
|
name?: string;
|
|
428
428
|
address?: string;
|
|
429
429
|
}[] | undefined): string;
|
|
430
|
-
/** The addr-spec of every entry in an address header — what SMTP RCPT TO /
|
|
431
|
-
* MAIL FROM must carry. One per comma-separated entry (commas inside quotes
|
|
432
|
-
* and angle brackets don't split): the `<…>` part when there is one, else the
|
|
433
|
-
* entry itself minus any `(comment)`. Entries without an `@` are dropped.
|
|
434
|
-
*
|
|
435
|
-
* 2026-10-25 — Claude Code (Opus 5.5), at Bob's direction. Replaces the
|
|
436
|
-
* `/[\w.+-]+@[\w.-]+/g` scan that all three send paths (desktop mailx-imap,
|
|
437
|
-
* Android android-bootstrap and sync-manager) carried as copies. That class
|
|
438
|
-
* omits characters RFC 5322 allows in a local part (`= ! # $ % & ' * / ? ^ { | } ~`),
|
|
439
|
-
* so `bernardgreenberg=msn.com@groups.io` (groups.io's relay address for a
|
|
440
|
-
* member) matched as `msn.com@groups.io`: the To header was right, the
|
|
441
|
-
* envelope was wrong, and groups.io bounced "The group 'msn.com' does not
|
|
442
|
-
* exist". Taking the addr-spec whole avoids guessing at its characters. */
|
|
443
|
-
export declare function envelopeAddresses(value: string): string[];
|
|
444
430
|
/** What an SMTP send needs from a finished RFC 822 message. */
|
|
445
431
|
export interface OutgoingEnvelope {
|
|
446
432
|
/** RCPT TO: every addr-spec in To, Cc and Bcc. */
|
|
@@ -629,6 +615,8 @@ export declare function parseInvite(ics: string): ParsedInvite | null;
|
|
|
629
615
|
* case specially").
|
|
630
616
|
*/
|
|
631
617
|
export declare function inviteHasGoogleRsvp(bodyHtml: string): boolean;
|
|
618
|
+
export { splitAddressHeader, parseAddressList, parseAddress, envelopeAddresses, firstAddress, isValidAddress, ADDRESS_IN_TEXT, findAddressesInText } from "./address.js";
|
|
619
|
+
export type { ParsedAddress } from "./address.js";
|
|
632
620
|
export { readListIdentity } from "./list-identity.js";
|
|
633
621
|
export type { ListIdentity, ListHeaderLine } from "./list-identity.js";
|
|
634
622
|
//# sourceMappingURL=index.d.ts.map
|
package/index.js
CHANGED
|
@@ -221,61 +221,6 @@ export function formatAddressForHeader(name, address) {
|
|
|
221
221
|
export function formatAddressListForHeader(list) {
|
|
222
222
|
return (list || []).map(a => formatAddressForHeader(a.name, a.address || "")).join(", ");
|
|
223
223
|
}
|
|
224
|
-
/** Split an address header on top-level commas (outside quotes and angle
|
|
225
|
-
* brackets; `\` escapes inside quotes honored). */
|
|
226
|
-
function splitAddressHeader(value) {
|
|
227
|
-
const parts = [];
|
|
228
|
-
let cur = "";
|
|
229
|
-
let inAngle = false;
|
|
230
|
-
let inQuote = false;
|
|
231
|
-
for (let i = 0; i < value.length; i++) {
|
|
232
|
-
const ch = value[i];
|
|
233
|
-
if (inQuote && ch === "\\" && i + 1 < value.length) {
|
|
234
|
-
cur += ch + value[++i];
|
|
235
|
-
continue;
|
|
236
|
-
}
|
|
237
|
-
if (ch === '"' && !inAngle)
|
|
238
|
-
inQuote = !inQuote;
|
|
239
|
-
else if (ch === "<" && !inQuote)
|
|
240
|
-
inAngle = true;
|
|
241
|
-
else if (ch === ">" && !inQuote)
|
|
242
|
-
inAngle = false;
|
|
243
|
-
if (ch === "," && !inAngle && !inQuote) {
|
|
244
|
-
parts.push(cur);
|
|
245
|
-
cur = "";
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
cur += ch;
|
|
249
|
-
}
|
|
250
|
-
if (cur.trim())
|
|
251
|
-
parts.push(cur);
|
|
252
|
-
return parts.map(p => p.trim()).filter(Boolean);
|
|
253
|
-
}
|
|
254
|
-
/** The addr-spec of every entry in an address header — what SMTP RCPT TO /
|
|
255
|
-
* MAIL FROM must carry. One per comma-separated entry (commas inside quotes
|
|
256
|
-
* and angle brackets don't split): the `<…>` part when there is one, else the
|
|
257
|
-
* entry itself minus any `(comment)`. Entries without an `@` are dropped.
|
|
258
|
-
*
|
|
259
|
-
* 2026-10-25 — Claude Code (Opus 5.5), at Bob's direction. Replaces the
|
|
260
|
-
* `/[\w.+-]+@[\w.-]+/g` scan that all three send paths (desktop mailx-imap,
|
|
261
|
-
* Android android-bootstrap and sync-manager) carried as copies. That class
|
|
262
|
-
* omits characters RFC 5322 allows in a local part (`= ! # $ % & ' * / ? ^ { | } ~`),
|
|
263
|
-
* so `bernardgreenberg=msn.com@groups.io` (groups.io's relay address for a
|
|
264
|
-
* member) matched as `msn.com@groups.io`: the To header was right, the
|
|
265
|
-
* envelope was wrong, and groups.io bounced "The group 'msn.com' does not
|
|
266
|
-
* exist". Taking the addr-spec whole avoids guessing at its characters. */
|
|
267
|
-
export function envelopeAddresses(value) {
|
|
268
|
-
if (!value)
|
|
269
|
-
return [];
|
|
270
|
-
const out = [];
|
|
271
|
-
for (const part of splitAddressHeader(value)) {
|
|
272
|
-
const angle = /<([^<>]*)>\s*$/.exec(part);
|
|
273
|
-
const spec = (angle ? angle[1] : part.replace(/\([^()]*\)/g, "")).trim();
|
|
274
|
-
if (spec.includes("@"))
|
|
275
|
-
out.push(spec);
|
|
276
|
-
}
|
|
277
|
-
return out;
|
|
278
|
-
}
|
|
279
224
|
/** Build the SMTP envelope and the transmitted copy of an outgoing message.
|
|
280
225
|
* ONE implementation for every send path (desktop mailx-imap sendRaw, Android
|
|
281
226
|
* android-bootstrap and sync-manager), which each used to carry their own copy
|
|
@@ -667,8 +612,9 @@ export function displayNameClaimsOtherAddress(name, address) {
|
|
|
667
612
|
return null;
|
|
668
613
|
const real = (address || "").trim().toLowerCase();
|
|
669
614
|
// Deliberately loose: anything shaped like local@domain.tld inside the name.
|
|
670
|
-
|
|
671
|
-
|
|
615
|
+
// 2026-10-26 — Claude Code (Opus 5.5): shared free-text pattern (address.ts).
|
|
616
|
+
const m = findAddressesInText(n);
|
|
617
|
+
if (!m.length)
|
|
672
618
|
return null;
|
|
673
619
|
const realDomain = real.split("@").pop() || "";
|
|
674
620
|
for (const claimed of m) {
|
|
@@ -907,6 +853,9 @@ export function inviteHasGoogleRsvp(bodyHtml) {
|
|
|
907
853
|
return false;
|
|
908
854
|
return /calendar\.google\.com\/calendar\/event\?action=RESPOND/i.test(bodyHtml);
|
|
909
855
|
}
|
|
856
|
+
// Address parsing: one splitter, one notion of "is this an address".
|
|
857
|
+
import { splitAddressHeader, envelopeAddresses, findAddressesInText } from "./address.js";
|
|
858
|
+
export { splitAddressHeader, parseAddressList, parseAddress, envelopeAddresses, firstAddress, isValidAddress, ADDRESS_IN_TEXT, findAddressesInText } from "./address.js";
|
|
910
859
|
// Delivery address + mailing-list identity for reply's From (desktop + Android).
|
|
911
860
|
export { readListIdentity } from "./list-identity.js";
|
|
912
861
|
//# sourceMappingURL=index.js.map
|
package/list-identity.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* two cannot drift apart again. Pure: header lines and body text in, strings
|
|
21
21
|
* out — no I/O, browser-safe.
|
|
22
22
|
*/
|
|
23
|
+
import { firstAddress } from "./address.js";
|
|
23
24
|
/** Value of every occurrence of a header, unfolded, in message order. */
|
|
24
25
|
function headerValues(lines, name) {
|
|
25
26
|
const want = name.toLowerCase();
|
|
@@ -33,8 +34,9 @@ function headerValues(lines, name) {
|
|
|
33
34
|
}
|
|
34
35
|
return out;
|
|
35
36
|
}
|
|
37
|
+
/** First addr-spec in a header value, lowercased (shared parser: address.ts). */
|
|
36
38
|
function bareAddress(value) {
|
|
37
|
-
return (
|
|
39
|
+
return firstAddress(value || "").toLowerCase();
|
|
38
40
|
}
|
|
39
41
|
/** qmail/ezmlm, and groups.io after them, stamp `Delivered-To: mailing list
|
|
40
42
|
* <addr>` on the copy they fan out, to catch mail loops. It names the LIST,
|
package/package.json
CHANGED
package/trust.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* be you. Forwarded mail fails SPF constantly, and flagging that would be
|
|
29
29
|
* exactly the cry-wolf banner this is trying not to be.
|
|
30
30
|
*/
|
|
31
|
+
import { firstAddress } from "./address.js";
|
|
31
32
|
/** What the reader can vouch for, and the allowlist.jsonc key each one is
|
|
32
33
|
* written to. One table, shared by the desktop service and the Android
|
|
33
34
|
* web-service, so "trust X" cannot land in different keys on the two
|
|
@@ -67,9 +68,12 @@ function headerAll(lines, name) {
|
|
|
67
68
|
}
|
|
68
69
|
return out;
|
|
69
70
|
}
|
|
71
|
+
// 2026-10-26 — Claude Code (Opus 5.5): the addr-spec comes from the shared
|
|
72
|
+
// parser (address.ts) instead of a first-`@`-token scan. The scan took the
|
|
73
|
+
// first token holding an `@` ANYWHERE in the value, so `"ceo@bank.com"
|
|
74
|
+
// <x@evil.example>` read as ceo@bank.com — the display name, not the address.
|
|
70
75
|
function bare(addr) {
|
|
71
|
-
|
|
72
|
-
return (m ? m[0] : addr || "").toLowerCase().replace(/[.,;]+$/, "");
|
|
76
|
+
return (firstAddress(addr || "") || addr || "").toLowerCase().replace(/[.,;]+$/, "");
|
|
73
77
|
}
|
|
74
78
|
/** Do two hostnames belong to the same organisation?
|
|
75
79
|
*
|