spamscanner 7.0.7 → 7.0.9
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/dist/cjs/arf.cjs +1 -1
- package/dist/cjs/arf.cjs.map +2 -2
- package/dist/cjs/index.cjs +77 -29
- package/dist/cjs/index.cjs.map +3 -3
- package/dist/esm/arf.js +1 -1
- package/dist/esm/arf.js.map +2 -2
- package/dist/esm/cli.js +72 -25
- package/dist/esm/cli.js.map +3 -3
- package/dist/esm/index.js +74 -26
- package/dist/esm/index.js.map +3 -3
- package/dist/types/index.d.ts +13 -3
- package/dist/types/parse.d.ts +12 -1
- package/dist/types/tokenizer.d.ts +3 -2
- package/package.json +1 -1
package/dist/esm/index.js
CHANGED
|
@@ -2102,11 +2102,12 @@ function gatewayTarget(url) {
|
|
|
2102
2102
|
}
|
|
2103
2103
|
if (host === "urldefense.com" && url.pathname.startsWith("/v3/__")) {
|
|
2104
2104
|
const rest = url.href.slice(url.href.indexOf("/v3/__") + 6);
|
|
2105
|
-
const end = rest.
|
|
2105
|
+
const end = rest.lastIndexOf("__;");
|
|
2106
2106
|
return end === -1 ? null : rest.slice(0, end);
|
|
2107
2107
|
}
|
|
2108
2108
|
return null;
|
|
2109
2109
|
}
|
|
2110
|
+
var MAX_GATEWAYS = 16;
|
|
2110
2111
|
function unwrapLink(address2) {
|
|
2111
2112
|
let url;
|
|
2112
2113
|
try {
|
|
@@ -2114,8 +2115,11 @@ function unwrapLink(address2) {
|
|
|
2114
2115
|
} catch {
|
|
2115
2116
|
return null;
|
|
2116
2117
|
}
|
|
2117
|
-
for (let depth = 0; depth <
|
|
2118
|
+
for (let depth = 0; depth < MAX_GATEWAYS; depth++) {
|
|
2118
2119
|
const target = gatewayTarget(url);
|
|
2120
|
+
if (!target) {
|
|
2121
|
+
break;
|
|
2122
|
+
}
|
|
2119
2123
|
let next;
|
|
2120
2124
|
try {
|
|
2121
2125
|
next = new URL(target);
|
|
@@ -3429,8 +3433,13 @@ function oleHasMacros(buffer) {
|
|
|
3429
3433
|
return false;
|
|
3430
3434
|
}
|
|
3431
3435
|
var PDF_ACTIVE = linear(String.raw`\/(?:JavaScript|JS|Launch)\b`);
|
|
3432
|
-
var
|
|
3433
|
-
|
|
3436
|
+
var escaped = (character) => {
|
|
3437
|
+
const hex = character.codePointAt(0).toString(16);
|
|
3438
|
+
return `(?:${character}|#${hex[0]}[${hex[1].toLowerCase()}${hex[1].toUpperCase()}])`;
|
|
3439
|
+
};
|
|
3440
|
+
var WORD_ESCAPE = String.raw`#(?:3\d|4[1-9a-fA-F]|5[\daA]|5[fF]|6[1-9a-fA-F]|7[\daA])`;
|
|
3441
|
+
var PDF_ACTIVE_ESCAPED = new RegExp(`/(?:${["JavaScript", "JS", "Launch"].map((name) => [...name].map((character) => escaped(character)).join("")).join("|")})(?![\\w]|${WORD_ESCAPE})`);
|
|
3442
|
+
var pdfActive = (text) => PDF_ACTIVE.test(text) || PDF_ACTIVE_ESCAPED.test(text);
|
|
3434
3443
|
function inspectAttachments(attachments = []) {
|
|
3435
3444
|
const findings = [];
|
|
3436
3445
|
for (const attachment of attachments) {
|
|
@@ -3488,7 +3497,7 @@ function inspectAttachments(attachments = []) {
|
|
|
3488
3497
|
add("macro", `Attachment "${name}" is an Office document with a VBA macro project`, { extension });
|
|
3489
3498
|
}
|
|
3490
3499
|
} else if (detected?.type === "pdf" || extension === "pdf") {
|
|
3491
|
-
if (
|
|
3500
|
+
if (pdfActive(content.toString("latin1"))) {
|
|
3492
3501
|
add("pdf_active", `PDF "${name}" contains JavaScript or a launch action`, { extension: "pdf" });
|
|
3493
3502
|
}
|
|
3494
3503
|
} else if (detected?.type === "rtf" && /\\object\b|\\objdata\b|\\objupdate\b/.test(content.toString("latin1"))) {
|
|
@@ -3536,7 +3545,7 @@ function boundAddressField(line, room) {
|
|
|
3536
3545
|
const limit = Math.min(line.length, MAX_ADDRESS_FIELD, room);
|
|
3537
3546
|
let end = start;
|
|
3538
3547
|
let expecting = "";
|
|
3539
|
-
let
|
|
3548
|
+
let escaped2 = false;
|
|
3540
3549
|
let separators = 0;
|
|
3541
3550
|
let groups = 0;
|
|
3542
3551
|
let index = start;
|
|
@@ -3545,8 +3554,8 @@ function boundAddressField(line, room) {
|
|
|
3545
3554
|
if ((chr === "," || chr === ";") && ++separators > MAX_ADDRESS_SEPARATORS) {
|
|
3546
3555
|
break;
|
|
3547
3556
|
}
|
|
3548
|
-
if (
|
|
3549
|
-
|
|
3557
|
+
if (escaped2) {
|
|
3558
|
+
escaped2 = false;
|
|
3550
3559
|
} else if (expecting !== "" && chr === expecting) {
|
|
3551
3560
|
expecting = "";
|
|
3552
3561
|
end = chr === ";" ? index + 1 : end;
|
|
@@ -3554,7 +3563,7 @@ function boundAddressField(line, room) {
|
|
|
3554
3563
|
expecting = ADDRESS_OPERATORS.get(chr);
|
|
3555
3564
|
end = chr === "," || chr === ";" ? index : end;
|
|
3556
3565
|
} else if (expecting === '"' && chr === "\\") {
|
|
3557
|
-
|
|
3566
|
+
escaped2 = true;
|
|
3558
3567
|
}
|
|
3559
3568
|
if (chr === ":" && expecting === ";" && ++groups > MAX_ADDRESS_GROUPS) {
|
|
3560
3569
|
break;
|
|
@@ -3697,9 +3706,14 @@ var SIGNATURE_FIELDS = /* @__PURE__ */ new Set(["dkim-signature", "arc-message-s
|
|
|
3697
3706
|
var AUTH_FIELDS = /* @__PURE__ */ new Set([...KEPT_FIELDS, "arc-seal", "arc-message-signature"]);
|
|
3698
3707
|
var MAX_SIGNATURES = 10;
|
|
3699
3708
|
var MAX_DKIM_SIGNATURES = 8;
|
|
3709
|
+
function isAttachedMessage(attachment) {
|
|
3710
|
+
const type = String(attachment.contentType).toLowerCase();
|
|
3711
|
+
return type === "message/rfc822" || type === "message/global" || /\.(?:eml|u8msg)$/i.test(String(attachment.filename));
|
|
3712
|
+
}
|
|
3700
3713
|
var MAX_NESTED_DEPTH = 3;
|
|
3701
3714
|
var MAX_ATTACHMENTS = 100;
|
|
3702
3715
|
var MAX_NESTED_BYTES = 25 * 1024 * 1024;
|
|
3716
|
+
var MAX_ATTACHED_MESSAGES = 100;
|
|
3703
3717
|
var PARSE_TIMEOUT = 6e4;
|
|
3704
3718
|
async function simpleParse(raw, { timeout = PARSE_TIMEOUT, parser = simpleParser } = {}) {
|
|
3705
3719
|
const input = Readable.from([finishable(unfoldHeaders(raw))], { objectMode: false, autoDestroy: false });
|
|
@@ -3757,18 +3771,18 @@ function headerValue2(line) {
|
|
|
3757
3771
|
let key = null;
|
|
3758
3772
|
let inKey = false;
|
|
3759
3773
|
let quoted = false;
|
|
3760
|
-
let
|
|
3774
|
+
let escaped2 = false;
|
|
3761
3775
|
let value = "";
|
|
3762
3776
|
for (const chr of line.slice(line.indexOf(":") + 1).replaceAll(/\r?\n[ \t]*/g, " ")) {
|
|
3763
3777
|
if (inKey && chr === "=") {
|
|
3764
3778
|
key = value.trim().toLowerCase();
|
|
3765
3779
|
inKey = false;
|
|
3766
3780
|
value = "";
|
|
3767
|
-
} else if (inKey ||
|
|
3781
|
+
} else if (inKey || escaped2) {
|
|
3768
3782
|
value += chr;
|
|
3769
|
-
|
|
3783
|
+
escaped2 = false;
|
|
3770
3784
|
} else if (chr === "\\") {
|
|
3771
|
-
|
|
3785
|
+
escaped2 = true;
|
|
3772
3786
|
} else if (chr === '"') {
|
|
3773
3787
|
quoted = !quoted;
|
|
3774
3788
|
} else if (chr === ";" && !quoted) {
|
|
@@ -3816,7 +3830,7 @@ function unfoldHeaders(raw) {
|
|
|
3816
3830
|
if (blank && type.boundary) {
|
|
3817
3831
|
boundaries.push(Buffer2.from(type.boundary));
|
|
3818
3832
|
}
|
|
3819
|
-
head = blank &&
|
|
3833
|
+
head = blank && ["message/rfc822", "message/global"].includes(type.value) && ["", "7bit", "8bit", "binary"].includes(encoding) && first.get("content-disposition")?.value === "inline";
|
|
3820
3834
|
};
|
|
3821
3835
|
for (let start = 0; start < raw.length; ) {
|
|
3822
3836
|
const lf = raw.indexOf(10, start);
|
|
@@ -3947,25 +3961,45 @@ async function parseWithin(raw, parse = parseMessage, deadline = Date.now() + PA
|
|
|
3947
3961
|
}
|
|
3948
3962
|
}
|
|
3949
3963
|
}
|
|
3950
|
-
async function expandAttachments(attachments = [], errors = [], parse = parseMessage) {
|
|
3964
|
+
async function expandAttachments(attachments = [], errors = [], parse = parseMessage, unopened = []) {
|
|
3951
3965
|
const all = [];
|
|
3952
3966
|
const deadline = Date.now() + PARSE_TIMEOUT;
|
|
3953
3967
|
let budget = MAX_NESTED_BYTES;
|
|
3954
3968
|
let nested = 0;
|
|
3969
|
+
let tooMany = false;
|
|
3970
|
+
let opened = 0;
|
|
3955
3971
|
const visit = async (list, prefix, depth) => {
|
|
3956
3972
|
for (const attachment of list) {
|
|
3957
3973
|
if (prefix) {
|
|
3958
3974
|
if (nested >= MAX_ATTACHMENTS) {
|
|
3975
|
+
if (!tooMany) {
|
|
3976
|
+
tooMany = true;
|
|
3977
|
+
unopened.push({ filename: prefix, reason: `more than ${MAX_ATTACHMENTS} attachments in attached messages` });
|
|
3978
|
+
}
|
|
3959
3979
|
return;
|
|
3960
3980
|
}
|
|
3961
3981
|
nested++;
|
|
3962
3982
|
}
|
|
3963
3983
|
const name = attachment.filename || (prefix ? "unnamed attachment" : "");
|
|
3984
|
+
const filename = prefix ? `${prefix}/${name}` : name || "attached message";
|
|
3964
3985
|
all.push(prefix ? { ...attachment, filename: `${prefix}/${name}` } : attachment);
|
|
3965
3986
|
const { content } = attachment;
|
|
3966
|
-
const attached =
|
|
3967
|
-
if (attached &&
|
|
3987
|
+
const attached = isAttachedMessage(attachment);
|
|
3988
|
+
if (attached && Buffer2.isBuffer(content) && depth >= MAX_NESTED_DEPTH) {
|
|
3989
|
+
unopened.push({ filename, reason: `nested more than ${MAX_NESTED_DEPTH} deep` });
|
|
3990
|
+
continue;
|
|
3991
|
+
}
|
|
3992
|
+
if (attached && Buffer2.isBuffer(content) && opened >= MAX_ATTACHED_MESSAGES) {
|
|
3993
|
+
unopened.push({ filename, reason: `more than ${MAX_ATTACHED_MESSAGES} attached messages` });
|
|
3994
|
+
continue;
|
|
3995
|
+
}
|
|
3996
|
+
if (attached && Buffer2.isBuffer(content) && content.length > budget) {
|
|
3997
|
+
unopened.push({ filename, reason: `past ${MAX_NESTED_BYTES / 1024 / 1024} MB of attached messages` });
|
|
3998
|
+
continue;
|
|
3999
|
+
}
|
|
4000
|
+
if (attached && Buffer2.isBuffer(content)) {
|
|
3968
4001
|
budget -= content.length;
|
|
4002
|
+
opened++;
|
|
3969
4003
|
let inner;
|
|
3970
4004
|
try {
|
|
3971
4005
|
inner = await parseWithin(content, parse, deadline);
|
|
@@ -5651,9 +5685,12 @@ ${visibleText(html.slice(0, 1e5)).text}`)) {
|
|
|
5651
5685
|
hit("SEXTORTION_SUBJECT", 2, "Subject used by sextortion and account takeover scams, without a demand for payment", "SCAM");
|
|
5652
5686
|
}
|
|
5653
5687
|
const markup = html.slice(0, 1e5);
|
|
5688
|
+
const readable = readableText(`${subject}
|
|
5689
|
+
${text.slice(0, 5e4)}
|
|
5690
|
+
${visibleText(withoutHelp(markup)).text}`);
|
|
5654
5691
|
if (isInjection(`${subject}
|
|
5655
5692
|
${text.slice(0, 5e4)}
|
|
5656
|
-
${withoutHelp(markup)}`) || attributeText(markup).some(({ text: text2, start, end, help }) => (!help || isOrder(text2)) && isInjection(unquoted(text2), AI_ACTION_OBJECT, () => shownNear(markup, start, end)))) {
|
|
5693
|
+
${withoutHelp(markup)}`) || isInjection(readable) || attributeText(markup).some(({ text: text2, start, end, help }) => (!help || isOrder(text2)) && isInjection(unquoted(text2), AI_ACTION_OBJECT, () => shownNear(markup, start, end)))) {
|
|
5657
5694
|
hit("PROMPT_INJECTION", 3, "Contains instructions addressed to an AI filter", "SPAM");
|
|
5658
5695
|
}
|
|
5659
5696
|
if (session.resolvedClientHostname?.endsWith(".outbound.protection.outlook.com")) {
|
|
@@ -7031,7 +7068,7 @@ function scoreResults(results, options) {
|
|
|
7031
7068
|
var package_default = {
|
|
7032
7069
|
name: "spamscanner",
|
|
7033
7070
|
description: "Spam, phishing and malware filter for email in any language: a trainable classifier, link and attachment checks, SPF/DKIM/DMARC, and an optional AI second opinion from any LLM, including local Ollama models. Works as a library, CLI, Postfix milter or HTTP API.",
|
|
7034
|
-
version: "7.0.
|
|
7071
|
+
version: "7.0.9",
|
|
7035
7072
|
author: "Forward Email (https://forwardemail.net)",
|
|
7036
7073
|
bin: {
|
|
7037
7074
|
spamscanner: "./dist/esm/cli.js"
|
|
@@ -8833,14 +8870,16 @@ var SpamScanner = class {
|
|
|
8833
8870
|
* a header field ("Name: value", or an mbox "From " line) is read as the
|
|
8834
8871
|
* body of a message with no header, so its text is the input; other
|
|
8835
8872
|
* input mailparser cannot read gives a message with no text. Other errors
|
|
8836
|
-
* are thrown.
|
|
8873
|
+
* are thrown. `error` is the parser's limit the message exceeds, so that
|
|
8874
|
+
* only its first parts were read (as scan() reports it, see parseError),
|
|
8875
|
+
* or null.
|
|
8837
8876
|
* @param {Buffer|string|Uint8Array|AsyncIterable} source
|
|
8838
|
-
* @returns {Promise<{raw: Buffer, mail: object}>}
|
|
8877
|
+
* @returns {Promise<{raw: Buffer, mail: object, error: string|null}>}
|
|
8839
8878
|
*/
|
|
8840
8879
|
async parse(source) {
|
|
8841
8880
|
const raw = await toBuffer(source);
|
|
8842
|
-
const { mail } = await parseWithin(withHeader(raw));
|
|
8843
|
-
return { raw, mail };
|
|
8881
|
+
const { mail, error } = await parseWithin(withHeader(raw));
|
|
8882
|
+
return { raw, mail, error: error ?? null };
|
|
8844
8883
|
}
|
|
8845
8884
|
/**
|
|
8846
8885
|
* Classifier features of a parsed message.
|
|
@@ -9079,7 +9118,7 @@ var SpamScanner = class {
|
|
|
9079
9118
|
message: `Attachment "${filename}" contains ${result.viruses.join(", ")}`
|
|
9080
9119
|
});
|
|
9081
9120
|
viruses.push(found);
|
|
9082
|
-
if (
|
|
9121
|
+
if (isAttachedMessage(attachment)) {
|
|
9083
9122
|
carriers.set(found, attachment.filename || "attached message");
|
|
9084
9123
|
}
|
|
9085
9124
|
}
|
|
@@ -9200,7 +9239,8 @@ ${visibleText(html.slice(0, config.maxLength * 2)).text}`, { maxLength: config.m
|
|
|
9200
9239
|
mixed: Math.max(plainObfuscation.mixed, shownObfuscation.mixed),
|
|
9201
9240
|
styled: plainObfuscation.styled || shownObfuscation.styled
|
|
9202
9241
|
};
|
|
9203
|
-
const
|
|
9242
|
+
const unopened = [];
|
|
9243
|
+
const attached = await expandAttachments(mail.attachments, parseErrors, void 0, unopened);
|
|
9204
9244
|
const attachments = config.attachments === false ? [] : inspectAttachments(attached).filter((item) => config.macros !== false || !["macro", "pdf_active", "rtf_object"].includes(item.type)).map((item) => finding(item));
|
|
9205
9245
|
const [links, authentication, checkedReputation, dnsbl, virusScan, toxicity, nsfw] = await Promise.all([
|
|
9206
9246
|
this.checkLinks(checkedLinks, config, extracted.hosts, fromDomain(mail)),
|
|
@@ -9321,6 +9361,13 @@ ${visibleText(html.slice(0, config.maxLength * 2)).text}`, { maxLength: config.m
|
|
|
9321
9361
|
// scanned, as {filename, reason}: the same on every try, so not a
|
|
9322
9362
|
// failure.
|
|
9323
9363
|
virusScanSkipped: virusScan.skipped,
|
|
9364
|
+
// Attached messages not opened, past the limits attached messages
|
|
9365
|
+
// are read within (nested more than three deep, more than 100
|
|
9366
|
+
// attachments in them, or more than 25 MB of them), as
|
|
9367
|
+
// {filename, reason}: their attachments were not inspected. Not
|
|
9368
|
+
// scored (forwarded messages can be nested so), for a caller that
|
|
9369
|
+
// needs a message read whole, e.g. to block what it carries.
|
|
9370
|
+
unopenedAttachments: unopened,
|
|
9324
9371
|
// Whether the message has more text than phishing.maxLength, whose
|
|
9325
9372
|
// links were not read.
|
|
9326
9373
|
linksTruncated,
|
|
@@ -9369,6 +9416,7 @@ export {
|
|
|
9369
9416
|
scoreResults,
|
|
9370
9417
|
segmentWords,
|
|
9371
9418
|
spamHeaders,
|
|
9372
|
-
train
|
|
9419
|
+
train,
|
|
9420
|
+
unwrapLink
|
|
9373
9421
|
};
|
|
9374
9422
|
//# sourceMappingURL=index.js.map
|