@bobfrankston/rmfmail 1.2.289 → 1.2.291
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/.commitmsg +31 -12
- package/TODO.md +1 -1
- package/client/android-bootstrap.bundle.js +35 -3
- package/client/android-bootstrap.bundle.js.map +2 -2
- package/client/app.bundle.js +1 -1
- package/client/app.bundle.js.map +2 -2
- package/client/components/message-viewer.js +5 -1
- package/client/components/message-viewer.js.map +1 -1
- package/client/components/message-viewer.ts +6 -2
- package/docs/spam-false-positives.md +34 -0
- package/npmchanges.md +57 -0
- package/package.json +5 -5
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-settings/docs/spam-false-positives.md +34 -0
- package/packages/mailx-settings/package.json +1 -1
- package/packages/mailx-store/package.json +1 -1
- package/packages/mailx-store/store.d.ts.map +1 -1
- package/packages/mailx-store/store.js +15 -5
- package/packages/mailx-store/store.js.map +1 -1
- package/packages/mailx-store/store.ts +15 -5
- package/packages/mailx-store-web/package.json +1 -1
- package/packages/mailx-types/package.json +1 -1
- package/packages/mailx-types/trust.d.ts +11 -0
- package/packages/mailx-types/trust.d.ts.map +1 -1
- package/packages/mailx-types/trust.js +50 -1
- package/packages/mailx-types/trust.js.map +1 -1
- package/packages/mailx-types/trust.ts +62 -1
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-53712 → node_modules.npmglobalize-stash-27960}/.package-lock.json +0 -0
|
@@ -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. */
|
|
@@ -133,6 +141,15 @@ const ASSOCIATION_RULES: RegExp[] = [
|
|
|
133
141
|
/^SEM_URIBL/,
|
|
134
142
|
/^SPAMHAUS_/,
|
|
135
143
|
/^RCVD_IN_(SBL|XBL|PBL|BL_SPAMCOP|SORBS|BRBL|VALIDITY|MSPIKE_[LZ])/,
|
|
144
|
+
// Blocklist rules that happen to live under other prefixes. KAM_*URIBL_*
|
|
145
|
+
// ("Body contains URI listed in PCCC WILD RBL", 9.0) and SH_HBL_EMAILS
|
|
146
|
+
// ("Email address listed in email blocklist", 8.0) are the same kind of
|
|
147
|
+
// evidence as URIBL_SBL — somebody else's list, about a host or address
|
|
148
|
+
// the message mentions — and they were the bulk of what still cautioned a
|
|
149
|
+
// proved sender: a Business Insider newsletter, an MIT cancer-center
|
|
150
|
+
// invitation, a campaign mailing. KAM_INFOUSMEBIZ judges the TLD a domain
|
|
151
|
+
// lives in, which is guilt by neighbourhood.
|
|
152
|
+
/^KAM_(BODY_|FROM_)?URIBL/, /^SH_HBL/, /^KAM_INFOUSMEBIZ$/,
|
|
136
153
|
];
|
|
137
154
|
|
|
138
155
|
/** The shape of mass mailing: bulk-detection networks, marketing markup, list
|
|
@@ -151,6 +168,10 @@ const BULK_RULES: RegExp[] = [
|
|
|
151
168
|
/^URIBL_(GREY|CSS|BLOCKED|CT_SURBL)/, /^SH_BODYURI/,
|
|
152
169
|
/^HEADER_FROM_DIFFERENT_DOMAINS$/, // every ESP on earth trips this
|
|
153
170
|
/^MPART_ALT_DIFF/, /^TVD_/, /^T_REMOTE_IMAGE$/, /^UNPARSEABLE_RELAY$/,
|
|
171
|
+
// Branded ESP click-trackers. Not open redirectors (URI_PHP_REDIR and
|
|
172
|
+
// friends stay unclassified): these are the sending platform's own
|
|
173
|
+
// tracking domain, which is mass-mail machinery by definition.
|
|
174
|
+
/^GB_AWSTRACK/, /AWSTRACK/, /^GB_SENDGRID/, /^GB_MAILCHIMP/,
|
|
154
175
|
];
|
|
155
176
|
|
|
156
177
|
/** Passes, whitelistings, and Bayes' own opinion — informational, and in
|
|
@@ -333,9 +354,28 @@ function analyzeServerSpam(input: TrustInput): ServerSpamAnalysis {
|
|
|
333
354
|
// of a report is not evidence the rule was free.
|
|
334
355
|
const scored = (r: SpamRule) => !(r.score <= 0);
|
|
335
356
|
const present = (cls: RuleClass) => rules.some(r => r.cls === cls && scored(r));
|
|
357
|
+
|
|
358
|
+
// An unknown rule keeps a proved sender at caution only if it could have
|
|
359
|
+
// flagged the message BY ITSELF. Presence was too crude: an IEEE
|
|
360
|
+
// conference announcement (Bob 2026-08-28) scored 4.5 of 4.5 on DCC 3.0 +
|
|
361
|
+
// marketing 1.0 + 1.5 for GB_AWSTRACK_REDIR — Amazon SES's click-tracker,
|
|
362
|
+
// which every bulk mailer on earth routes through — and the 1.5 alone
|
|
363
|
+
// pushed a DMARC-proved announcement into "check this message before
|
|
364
|
+
// acting on it". Meanwhile KAM_BENEFICIARY, the case this rule exists for,
|
|
365
|
+
// is worth 10.0 on its own.
|
|
366
|
+
//
|
|
367
|
+
// The comparison is against the SERVER'S OWN threshold, so this adds no
|
|
368
|
+
// constant to tune: "enough to condemn" already has a number, and it is
|
|
369
|
+
// the one the server used. Unscored rules (no X-Spam-Report) make the sum
|
|
370
|
+
// unknown, and unknown stays cautious.
|
|
371
|
+
const unknown = rules.filter(r => r.cls === "other" && scored(r));
|
|
372
|
+
const unknownWeight = unknown.reduce((sum, r) => sum + r.score, 0);
|
|
373
|
+
const unknownCouldFlagAlone = !unknown.length ? false
|
|
374
|
+
: !Number.isFinite(unknownWeight) || !Number.isFinite(threshold) ? true
|
|
375
|
+
: unknownWeight >= threshold;
|
|
336
376
|
const kind = (dmarc.authFailures.length || present("forgery")) ? "forgery"
|
|
337
377
|
: present("association") ? "association"
|
|
338
|
-
:
|
|
378
|
+
: unknownCouldFlagAlone ? "unclassified"
|
|
339
379
|
: rules.length ? "bulk"
|
|
340
380
|
: "unclassified";
|
|
341
381
|
|
|
@@ -424,6 +464,16 @@ function serverSpamVerdict(input: TrustInput): TrustFinding {
|
|
|
424
464
|
const a = analyzeServerSpam(input);
|
|
425
465
|
if (!a || !a.flagged) return null;
|
|
426
466
|
|
|
467
|
+
// Mail from a sender the reader has approved, whose identity is proved,
|
|
468
|
+
// and which nothing accuses of forgery: say nothing at all. The server's
|
|
469
|
+
// score answers "is this bulk", the reader has already answered "is it
|
|
470
|
+
// wanted", and repeating the first after the second is how a banner turns
|
|
471
|
+
// into furniture. (Bob 2026-08-28, having just clicked "Always:
|
|
472
|
+
// *@icecer.com" on a conference announcement: "I accepted the server so
|
|
473
|
+
// shouldn't this be considered safe?")
|
|
474
|
+
const trustedAndProved = input.senderTrusted && a.proved;
|
|
475
|
+
if (trustedAndProved && a.kind !== "forgery" && !a.authFailures.length) return null;
|
|
476
|
+
|
|
427
477
|
const numbers = Number.isFinite(a.score) && Number.isFinite(a.threshold)
|
|
428
478
|
? `SpamAssassin score ${a.score} of ${a.threshold}`
|
|
429
479
|
: "flagged by SpamAssassin";
|
|
@@ -459,6 +509,13 @@ function serverSpamVerdict(input: TrustInput): TrustFinding {
|
|
|
459
509
|
};
|
|
460
510
|
|
|
461
511
|
const proof = a.proved ? `${provedNote(a)} ` : "";
|
|
512
|
+
if (input.senderTrusted && !a.proved)
|
|
513
|
+
return {
|
|
514
|
+
id: "server-spam-verdict",
|
|
515
|
+
severity: "caution",
|
|
516
|
+
text: "This claims to be a sender you trust, but nothing proves it is.",
|
|
517
|
+
detail: `No DMARC pass, no valid signature from the sending domain, and it passed through hosts your server does not trust. ${numbers}${why}`,
|
|
518
|
+
};
|
|
462
519
|
return {
|
|
463
520
|
id: "server-spam-verdict",
|
|
464
521
|
severity: "caution",
|
|
@@ -742,6 +799,9 @@ export interface SpamScore {
|
|
|
742
799
|
* listed in the Spamhaus SBL blocklist)". A number alone is unactionable;
|
|
743
800
|
* this can be judged in a second. */
|
|
744
801
|
reasons: string[];
|
|
802
|
+
/** The reader approved this sender AND the sender is proved: the score is
|
|
803
|
+
* still true, and no longer worth showing. The viewer hides the chip. */
|
|
804
|
+
trusted: boolean;
|
|
745
805
|
}
|
|
746
806
|
|
|
747
807
|
export function spamScoreOf(input: TrustInput): SpamScore {
|
|
@@ -754,6 +814,7 @@ export function spamScoreOf(input: TrustInput): SpamScore {
|
|
|
754
814
|
kind: a.kind,
|
|
755
815
|
proved: a.proved,
|
|
756
816
|
provedBy: a.provedBy,
|
|
817
|
+
trusted: !!input.senderTrusted && a.proved && a.kind !== "forgery" && !a.authFailures.length,
|
|
757
818
|
reasons: a.reasons,
|
|
758
819
|
};
|
|
759
820
|
}
|