@bobfrankston/rmfmail 1.2.257 → 1.2.259
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/client/app.bundle.js +12 -1
- package/client/app.bundle.js.map +2 -2
- package/client/app.js +14 -0
- package/client/app.js.map +1 -1
- package/client/app.ts +13 -0
- package/client/components/message-viewer.js +6 -1
- package/client/components/message-viewer.js.map +1 -1
- package/client/components/message-viewer.ts +7 -1
- package/client/compose/compose.bundle.js +5 -7
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/compose/harper-lint.js +10 -3
- package/client/compose/harper-lint.js.map +1 -1
- package/client/compose/harper-lint.ts +10 -3
- package/client/compose/spellcheck.js +19 -9
- package/client/compose/spellcheck.js.map +1 -1
- package/client/compose/spellcheck.ts +19 -9
- package/client/index.html +2 -1
- package/client/package.json +1 -1
- package/package.json +3 -3
- package/packages/mailx-service/index.d.ts +51 -3
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +96 -30
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +116 -32
- package/packages/mailx-service/package.json +1 -1
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-50428 → node_modules.npmglobalize-stash-39528}/.package-lock.json +0 -0
|
@@ -246,6 +246,49 @@ function calendarRowEquals(prior, fresh) {
|
|
|
246
246
|
&& (prior.htmlLink || null) === (fresh.htmlLink || null)
|
|
247
247
|
&& (prior.calendarId || "") === (fresh.calendarId || "");
|
|
248
248
|
}
|
|
249
|
+
/** Spamhaus DBL — listings are 127.0.1.2 … 127.0.1.106; 127.255.255.x is
|
|
250
|
+
* status/error. https://www.spamhaus.org/faq/section/DNSBL%20Usage */
|
|
251
|
+
const DBL_LISTINGS = {
|
|
252
|
+
2: "spam", 4: "phishing", 5: "malware", 6: "botnet C&C",
|
|
253
|
+
102: "abused legit spam", 103: "abused spammed redirector",
|
|
254
|
+
104: "abused legit phishing", 105: "abused legit malware", 106: "abused legit botnet",
|
|
255
|
+
};
|
|
256
|
+
const DBL_STATUS = {
|
|
257
|
+
"127.255.255.252": "query name malformed",
|
|
258
|
+
"127.255.255.253": "anonymous query — a DQS key is required",
|
|
259
|
+
"127.255.255.254": "query came through a public/open DNS resolver — Spamhaus refuses these (set a Spamhaus DQS key in Settings)",
|
|
260
|
+
"127.255.255.255": "query volume limit exceeded",
|
|
261
|
+
};
|
|
262
|
+
export const interpretSpamhausDbl = (ip) => {
|
|
263
|
+
const m = /^127\.0\.1\.(\d+)$/.exec(ip);
|
|
264
|
+
const code = m ? Number(m[1]) : NaN;
|
|
265
|
+
if (DBL_LISTINGS[code])
|
|
266
|
+
return { verdict: DBL_LISTINGS[code] };
|
|
267
|
+
if (m && code >= 2 && code <= 106)
|
|
268
|
+
return { verdict: "listed" };
|
|
269
|
+
return { reason: DBL_STATUS[ip] || `unrecognized answer ${ip || "(empty)"}` };
|
|
270
|
+
};
|
|
271
|
+
/** URIBL / SURBL — both answer 127.0.0.<bitmask>. In BOTH, 127.0.0.1 means
|
|
272
|
+
* "query refused" (public resolver / not permitted) and 127.0.0.255 means
|
|
273
|
+
* "blocked, too many queries" — neither is a listing. */
|
|
274
|
+
const bitmaskInterpret = (bits) => (ip) => {
|
|
275
|
+
const m = /^127\.0\.0\.(\d+)$/.exec(ip);
|
|
276
|
+
if (!m)
|
|
277
|
+
return { reason: `unrecognized answer ${ip || "(empty)"}` };
|
|
278
|
+
const v = Number(m[1]);
|
|
279
|
+
if (v === 1)
|
|
280
|
+
return { reason: "query refused — this list does not answer public DNS resolvers" };
|
|
281
|
+
if (v === 255)
|
|
282
|
+
return { reason: "query volume limit exceeded" };
|
|
283
|
+
const names = Object.keys(bits).map(Number).filter(b => v & b).map(b => bits[b]);
|
|
284
|
+
if (names.length)
|
|
285
|
+
return { verdict: `listed (${names.join(" + ")})` };
|
|
286
|
+
return { reason: `unrecognized answer ${ip}` };
|
|
287
|
+
};
|
|
288
|
+
/** URIBL multi: 2 black, 4 grey, 8 red. */
|
|
289
|
+
export const interpretUribl = bitmaskInterpret({ 2: "black", 4: "grey", 8: "red" });
|
|
290
|
+
/** SURBL multi: 8 phishing, 16 malware, 32 abuse, 64 cracked. */
|
|
291
|
+
export const interpretSurbl = bitmaskInterpret({ 8: "phishing", 16: "malware", 32: "abuse", 64: "cracked" });
|
|
249
292
|
/** How long an un-consumed compose-popout init stash survives. The popout
|
|
250
293
|
* window normally consumes it within a second of spawning; the timer only
|
|
251
294
|
* matters when the window fails to launch. */
|
|
@@ -1991,15 +2034,30 @@ export class MailxService {
|
|
|
1991
2034
|
* SURBL multi — `<d>.multi.surbl.org` mixed (ph/mw/abuse)
|
|
1992
2035
|
* URIBL multi — `<d>.multi.uribl.com` black/grey/red lists
|
|
1993
2036
|
*
|
|
2037
|
+
* A DNSBL answers in 127.0.0.0/8, and only SOME of those addresses mean
|
|
2038
|
+
* "listed" — the rest are STATUS codes. Treating any A record as a
|
|
2039
|
+
* listing is the bug that flagged e.nytimes.com as spam on 2 of 2
|
|
2040
|
+
* services (Bob 2026-08-13). From a consumer connection every DBL query
|
|
2041
|
+
* comes back `127.255.255.254` ("query via a public/open resolver"),
|
|
2042
|
+
* including for google.com and for Spamhaus's own dbltest.com, and
|
|
2043
|
+
* URIBL answers `127.0.0.1` ("query refused") the same way — so the old
|
|
2044
|
+
* code convicted every domain it looked at. Each service now gets an
|
|
2045
|
+
* interpreter that returns a verdict ONLY for a documented listing code;
|
|
2046
|
+
* anything else is "unavailable" and never reaches the banner.
|
|
2047
|
+
*
|
|
1994
2048
|
* Each lookup is bounded at 500 ms; missing/slow services are treated
|
|
1995
2049
|
* as "unknown" (don't poison the cache). Returns the aggregate plus
|
|
1996
2050
|
* the per-service detail so the UI can show "N of 3 services flag
|
|
1997
2051
|
* this domain" with the contributing source list.
|
|
1998
2052
|
*
|
|
1999
|
-
*
|
|
2000
|
-
*
|
|
2053
|
+
* Spamhaus DQS: a free personal key (https://www.spamhaus.com/free-trial/)
|
|
2054
|
+
* makes DBL answer from any resolver — queries go to
|
|
2055
|
+
* `<domain>.<key>.dbl.dq.spamhaus.net` instead. Set `spamhausDqsKey` in
|
|
2056
|
+
* Settings; without it, DBL is simply reported as unavailable rather
|
|
2057
|
+
* than guessed at.
|
|
2001
2058
|
*
|
|
2002
|
-
*
|
|
2059
|
+
* Privacy: each query leaks the bare domain to that DNSBL's
|
|
2060
|
+
* infrastructure plus the user's local resolver. Opt-in via Settings. */
|
|
2003
2061
|
async checkDomainReputation(domain) {
|
|
2004
2062
|
domain = (domain || "").toLowerCase().trim();
|
|
2005
2063
|
if (!domain)
|
|
@@ -2007,48 +2065,56 @@ export class MailxService {
|
|
|
2007
2065
|
const cached = this.reputationCache.get(domain);
|
|
2008
2066
|
if (cached && cached.expiresAt > Date.now())
|
|
2009
2067
|
return cached.result;
|
|
2010
|
-
const probe = async (service, host,
|
|
2068
|
+
const probe = async (service, host, interpret) => {
|
|
2011
2069
|
try {
|
|
2012
2070
|
const lookup = dns.resolve4(`${domain}.${host}`);
|
|
2013
2071
|
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("dnsbl-timeout")), MailxService.REPUTATION_TIMEOUT_MS));
|
|
2014
2072
|
const records = await Promise.race([lookup, timeout]);
|
|
2015
|
-
const
|
|
2016
|
-
|
|
2073
|
+
const ip = records[0] || "";
|
|
2074
|
+
const seen = interpret(ip);
|
|
2075
|
+
return "verdict" in seen
|
|
2076
|
+
? { service, status: "listed", verdict: seen.verdict }
|
|
2077
|
+
: { service, status: "unavailable", reason: seen.reason };
|
|
2017
2078
|
}
|
|
2018
2079
|
catch (e) {
|
|
2019
2080
|
const code = e?.code || "";
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
return
|
|
2081
|
+
// NXDOMAIN / no A record is the DNSBL way of saying "not listed".
|
|
2082
|
+
if (code === "ENOTFOUND" || code === "ENODATA")
|
|
2083
|
+
return { service, status: "clean" };
|
|
2084
|
+
return { service, status: "unavailable", reason: code || String(e?.message || e) };
|
|
2024
2085
|
}
|
|
2025
2086
|
};
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
// SURBL/URIBL encode multiple list memberships in a bitfield; the
|
|
2032
|
-
// distinction matters less to the end user than "how many sources
|
|
2033
|
-
// agree", so we keep a generic "listed" verdict for both.
|
|
2034
|
-
const generic = (_last) => "listed";
|
|
2087
|
+
// A DQS key routes DBL queries to Spamhaus's keyed service, which
|
|
2088
|
+
// answers from any resolver. Without one we still ask the public zone —
|
|
2089
|
+
// it works from an ISP resolver that Spamhaus doesn't classify as open.
|
|
2090
|
+
const dqsKey = String(this.getCachedSettings()?.spamhausDqsKey || "").trim();
|
|
2091
|
+
const dblHost = dqsKey ? `${dqsKey}.dbl.dq.spamhaus.net` : "dbl.spamhaus.org";
|
|
2035
2092
|
const sources = await Promise.all([
|
|
2036
|
-
probe("Spamhaus DBL",
|
|
2037
|
-
probe("SURBL", "multi.surbl.org",
|
|
2038
|
-
probe("URIBL", "multi.uribl.com",
|
|
2093
|
+
probe("Spamhaus DBL", dblHost, interpretSpamhausDbl),
|
|
2094
|
+
probe("SURBL", "multi.surbl.org", interpretSurbl),
|
|
2095
|
+
probe("URIBL", "multi.uribl.com", interpretUribl),
|
|
2039
2096
|
]);
|
|
2040
|
-
const
|
|
2041
|
-
const
|
|
2097
|
+
const listed = sources.filter((s) => s.status === "listed");
|
|
2098
|
+
const unavailable = sources.filter((s) => s.status === "unavailable");
|
|
2099
|
+
const answered = sources.filter(s => s.status !== "unavailable");
|
|
2042
2100
|
const result = {
|
|
2043
|
-
flagged:
|
|
2044
|
-
listedCount:
|
|
2045
|
-
checkedCount:
|
|
2046
|
-
sources: flagged,
|
|
2101
|
+
flagged: listed.length > 0,
|
|
2102
|
+
listedCount: listed.length,
|
|
2103
|
+
checkedCount: answered.length,
|
|
2104
|
+
sources: listed.map(s => ({ service: s.service, flagged: true, verdict: s.verdict })),
|
|
2047
2105
|
// Pick the most specific verdict if Spamhaus contributed (since
|
|
2048
2106
|
// DBL distinguishes phishing/malware/etc); otherwise generic.
|
|
2049
|
-
verdict:
|
|
2050
|
-
service:
|
|
2107
|
+
verdict: listed.find(s => s.service === "Spamhaus DBL")?.verdict || listed[0]?.verdict || "clean",
|
|
2108
|
+
service: listed.map(s => s.service).join(", ") || "Spamhaus DBL / SURBL / URIBL",
|
|
2109
|
+
unavailable: unavailable.map(s => ({ service: s.service, reason: s.reason })),
|
|
2051
2110
|
};
|
|
2111
|
+
// Say so when the feature is blind — a domain nobody could check must
|
|
2112
|
+
// not read as a domain everybody cleared. Once per cache entry (5 min),
|
|
2113
|
+
// and only when NOTHING answered, so this can't spam the log.
|
|
2114
|
+
if (answered.length === 0) {
|
|
2115
|
+
console.warn(`[reputation] ${domain}: no service could answer — `
|
|
2116
|
+
+ unavailable.map(s => `${s.service}: ${s.reason}`).join("; "));
|
|
2117
|
+
}
|
|
2052
2118
|
this.reputationCache.set(domain, { result, expiresAt: Date.now() + MailxService.REPUTATION_TTL_MS });
|
|
2053
2119
|
return result;
|
|
2054
2120
|
}
|