@bobfrankston/mailx-types 0.1.71 → 0.1.76
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/index.d.ts +1 -0
- package/index.js +1 -0
- package/log-changes.d.ts +29 -0
- package/log-changes.js +42 -0
- package/package.json +1 -1
- package/trust.d.ts +11 -0
- package/trust.js +18 -0
package/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { CONTACT_RULES } from "./contact-rules.js";
|
|
7
7
|
export { assessMessageTrust, spamScoreOf } from "./trust.js";
|
|
8
|
+
export { logIfChanged, clearLogState } from "./log-changes.js";
|
|
8
9
|
export type { TrustFinding, TrustInput, SpamScore } from "./trust.js";
|
|
9
10
|
export type { MailxApi } from "./mailx-api.js";
|
|
10
11
|
export { addContactsDenylistEntry, addContactsPreferredEntry, type PreferredContactEntry, type CloudReadFn, type CloudWriteFn, } from "./contacts-config.js";
|
package/index.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// junk-contact filtering on every platform.
|
|
9
9
|
export { CONTACT_RULES } from "./contact-rules.js";
|
|
10
10
|
export { assessMessageTrust, spamScoreOf } from "./trust.js";
|
|
11
|
+
export { logIfChanged, clearLogState } from "./log-changes.js";
|
|
11
12
|
// Shared contacts.jsonc mutations — one implementation for desktop + Android.
|
|
12
13
|
export { addContactsDenylistEntry, addContactsPreferredEntry, } from "./contacts-config.js";
|
|
13
14
|
// Group-name expansion for recipient fields. Lets users type a group name
|
package/log-changes.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log a line only when it says something new.
|
|
3
|
+
*
|
|
4
|
+
* Sync walks ~90 folders every cycle, and a folder in a bad state produces the
|
|
5
|
+
* SAME line every time — "reconcile refused — would delete 250/250 (100%)"
|
|
6
|
+
* once per folder per cycle, forever. On Android each of those is an HTTP
|
|
7
|
+
* request to the log server, and in the file the real news is buried under
|
|
8
|
+
* hundreds of identical repeats (Bob 2026-08-29: "are we overlogging").
|
|
9
|
+
*
|
|
10
|
+
* Deleting the lines is the wrong fix — a refused reconcile is a genuine
|
|
11
|
+
* anomaly and the log is where it surfaces. What is worth reading is the
|
|
12
|
+
* CHANGE: the first time a folder enters that state, and every time the
|
|
13
|
+
* numbers move. So the same key + same text is dropped, a changed text is
|
|
14
|
+
* printed, and a state that persists is re-stated once an hour so a standing
|
|
15
|
+
* problem cannot become invisible.
|
|
16
|
+
*
|
|
17
|
+
* Keyed by caller-supplied string (account + folder + which check), because
|
|
18
|
+
* two folders in the same state are two facts, not one.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* @param key identifies the thing being described — "acct/folder:reconcile"
|
|
22
|
+
* @param line the message; identical text for the same key is suppressed
|
|
23
|
+
* @param emit where to write (defaults to console.log; pass a logger in libraries)
|
|
24
|
+
* @returns whether the line was emitted, so callers can count what they skipped
|
|
25
|
+
*/
|
|
26
|
+
export declare function logIfChanged(key: string, line: string, emit?: (msg: string) => void): boolean;
|
|
27
|
+
/** Forget a key — call when the condition clears, so its return is news again. */
|
|
28
|
+
export declare function clearLogState(key: string): void;
|
|
29
|
+
//# sourceMappingURL=log-changes.d.ts.map
|
package/log-changes.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log a line only when it says something new.
|
|
3
|
+
*
|
|
4
|
+
* Sync walks ~90 folders every cycle, and a folder in a bad state produces the
|
|
5
|
+
* SAME line every time — "reconcile refused — would delete 250/250 (100%)"
|
|
6
|
+
* once per folder per cycle, forever. On Android each of those is an HTTP
|
|
7
|
+
* request to the log server, and in the file the real news is buried under
|
|
8
|
+
* hundreds of identical repeats (Bob 2026-08-29: "are we overlogging").
|
|
9
|
+
*
|
|
10
|
+
* Deleting the lines is the wrong fix — a refused reconcile is a genuine
|
|
11
|
+
* anomaly and the log is where it surfaces. What is worth reading is the
|
|
12
|
+
* CHANGE: the first time a folder enters that state, and every time the
|
|
13
|
+
* numbers move. So the same key + same text is dropped, a changed text is
|
|
14
|
+
* printed, and a state that persists is re-stated once an hour so a standing
|
|
15
|
+
* problem cannot become invisible.
|
|
16
|
+
*
|
|
17
|
+
* Keyed by caller-supplied string (account + folder + which check), because
|
|
18
|
+
* two folders in the same state are two facts, not one.
|
|
19
|
+
*/
|
|
20
|
+
/** How long an unchanged line stays suppressed before being restated. */
|
|
21
|
+
const RESTATE_AFTER_MS = 60 * 60 * 1000;
|
|
22
|
+
const lastSeen = new Map();
|
|
23
|
+
/**
|
|
24
|
+
* @param key identifies the thing being described — "acct/folder:reconcile"
|
|
25
|
+
* @param line the message; identical text for the same key is suppressed
|
|
26
|
+
* @param emit where to write (defaults to console.log; pass a logger in libraries)
|
|
27
|
+
* @returns whether the line was emitted, so callers can count what they skipped
|
|
28
|
+
*/
|
|
29
|
+
export function logIfChanged(key, line, emit = console.log) {
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
const prev = lastSeen.get(key);
|
|
32
|
+
if (prev && prev.line === line && now - prev.at < RESTATE_AFTER_MS)
|
|
33
|
+
return false;
|
|
34
|
+
lastSeen.set(key, { line, at: now });
|
|
35
|
+
emit(prev && prev.line === line ? `${line} (unchanged for an hour)` : line);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
/** Forget a key — call when the condition clears, so its return is news again. */
|
|
39
|
+
export function clearLogState(key) {
|
|
40
|
+
lastSeen.delete(key);
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=log-changes.js.map
|
package/package.json
CHANGED
package/trust.d.ts
CHANGED
|
@@ -56,6 +56,14 @@ export interface TrustInput {
|
|
|
56
56
|
ownAddresses: string[];
|
|
57
57
|
/** Sanitized HTML body, for the link-construct checks. */
|
|
58
58
|
bodyHtml: string;
|
|
59
|
+
/** The reader has approved this sender or its domain in allowlist.jsonc —
|
|
60
|
+
* "Always: *@icecer.com" in the remote-content banner, or "Trust all
|
|
61
|
+
* senders at ‹domain›" in the address menu. It answers the only question
|
|
62
|
+
* a spam score cannot: is this mail WANTED. It never silences forgery
|
|
63
|
+
* evidence, and it counts only on a message whose sender was actually
|
|
64
|
+
* proved — otherwise the list would be a switch a forger could flip by
|
|
65
|
+
* typing a trusted address into From. */
|
|
66
|
+
senderTrusted?: boolean;
|
|
59
67
|
/** Plain-text body, for the character-level checks. Separate from the HTML
|
|
60
68
|
* because markup legitimately contains things prose does not, and the
|
|
61
69
|
* zero-width test would trip over an entity or an attribute value. */
|
|
@@ -105,6 +113,9 @@ export interface SpamScore {
|
|
|
105
113
|
* listed in the Spamhaus SBL blocklist)". A number alone is unactionable;
|
|
106
114
|
* this can be judged in a second. */
|
|
107
115
|
reasons: string[];
|
|
116
|
+
/** The reader approved this sender AND the sender is proved: the score is
|
|
117
|
+
* still true, and no longer worth showing. The viewer hides the chip. */
|
|
118
|
+
trusted: boolean;
|
|
108
119
|
}
|
|
109
120
|
export declare function spamScoreOf(input: TrustInput): SpamScore;
|
|
110
121
|
//# sourceMappingURL=trust.d.ts.map
|
package/trust.js
CHANGED
|
@@ -354,6 +354,16 @@ function serverSpamVerdict(input) {
|
|
|
354
354
|
const a = analyzeServerSpam(input);
|
|
355
355
|
if (!a || !a.flagged)
|
|
356
356
|
return null;
|
|
357
|
+
// Mail from a sender the reader has approved, whose identity is proved,
|
|
358
|
+
// and which nothing accuses of forgery: say nothing at all. The server's
|
|
359
|
+
// score answers "is this bulk", the reader has already answered "is it
|
|
360
|
+
// wanted", and repeating the first after the second is how a banner turns
|
|
361
|
+
// into furniture. (Bob 2026-08-28, having just clicked "Always:
|
|
362
|
+
// *@icecer.com" on a conference announcement: "I accepted the server so
|
|
363
|
+
// shouldn't this be considered safe?")
|
|
364
|
+
const trustedAndProved = input.senderTrusted && a.proved;
|
|
365
|
+
if (trustedAndProved && a.kind !== "forgery" && !a.authFailures.length)
|
|
366
|
+
return null;
|
|
357
367
|
const numbers = Number.isFinite(a.score) && Number.isFinite(a.threshold)
|
|
358
368
|
? `SpamAssassin score ${a.score} of ${a.threshold}`
|
|
359
369
|
: "flagged by SpamAssassin";
|
|
@@ -385,6 +395,13 @@ function serverSpamVerdict(input) {
|
|
|
385
395
|
detail: `${numbers}${why}${bayes}`,
|
|
386
396
|
};
|
|
387
397
|
const proof = a.proved ? `${provedNote(a)} ` : "";
|
|
398
|
+
if (input.senderTrusted && !a.proved)
|
|
399
|
+
return {
|
|
400
|
+
id: "server-spam-verdict",
|
|
401
|
+
severity: "caution",
|
|
402
|
+
text: "This claims to be a sender you trust, but nothing proves it is.",
|
|
403
|
+
detail: `No DMARC pass, no valid signature from the sending domain, and it passed through hosts your server does not trust. ${numbers}${why}`,
|
|
404
|
+
};
|
|
388
405
|
return {
|
|
389
406
|
id: "server-spam-verdict",
|
|
390
407
|
severity: "caution",
|
|
@@ -658,6 +675,7 @@ export function spamScoreOf(input) {
|
|
|
658
675
|
kind: a.kind,
|
|
659
676
|
proved: a.proved,
|
|
660
677
|
provedBy: a.provedBy,
|
|
678
|
+
trusted: !!input.senderTrusted && a.proved && a.kind !== "forgery" && !a.authFailures.length,
|
|
661
679
|
reasons: a.reasons,
|
|
662
680
|
};
|
|
663
681
|
}
|