spamscanner 7.0.8 → 7.0.10
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 +52 -22
- 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 +49 -19
- package/dist/esm/cli.js.map +3 -3
- package/dist/esm/index.js +49 -19
- package/dist/esm/index.js.map +3 -3
- package/dist/types/parse.d.ts +11 -0
- 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,7 +2115,7 @@ 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);
|
|
2119
2120
|
if (!target) {
|
|
2120
2121
|
break;
|
|
@@ -3233,6 +3234,7 @@ var MACRO_EXTENSIONS = /* @__PURE__ */ new Set(["docm", "dotm", "xlsm", "xltm",
|
|
|
3233
3234
|
var DOCUMENT_EXTENSIONS = /* @__PURE__ */ new Set(["pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "rtf", "jpg", "jpeg", "png", "gif", "zip", "csv", "odt", "ods", "html", "htm", "mp3", "mp4", "mov", "wav", "eml", "msg", "xml", "json"]);
|
|
3234
3235
|
var TEMPORARY_EXTENSIONS = /* @__PURE__ */ new Set(["tmp"]);
|
|
3235
3236
|
var BIDI_CONTROLS = characterClass([[8234, 8238], [8294, 8297]], "u");
|
|
3237
|
+
var ALL_BIDI_CONTROLS = characterClass([[8234, 8238], [8294, 8297]], "gu");
|
|
3236
3238
|
var ARCHIVE_TYPES = /* @__PURE__ */ new Set(["zip", "rar", "7z", "gz", "bz2", "xz", "cab", "iso", "tar", "zst", "lzh", "ace", "arj"]);
|
|
3237
3239
|
function sniff(buffer) {
|
|
3238
3240
|
if (!Buffer.isBuffer(buffer) || buffer.length < 4) {
|
|
@@ -3416,7 +3418,16 @@ function shorten(name, max = 100) {
|
|
|
3416
3418
|
return characters.length <= max ? name : `${characters.slice(0, max - 41).join("")}\u2026${characters.slice(-40).join("")}`;
|
|
3417
3419
|
}
|
|
3418
3420
|
function extensionOf(name) {
|
|
3419
|
-
const base = trimEndOf(String(name).toLowerCase().split(/[/\\]/).pop(), ". ");
|
|
3421
|
+
const base = trimEndOf(String(name).toLowerCase().replace(ALL_BIDI_CONTROLS, "").split(/[/\\]/).pop(), ". ");
|
|
3422
|
+
const whole = lastExtension(base);
|
|
3423
|
+
const file = base.replace(/^[a-z]:/, "");
|
|
3424
|
+
const colon = file.indexOf(":");
|
|
3425
|
+
if (colon !== -1 && /^(?::?\$data)?$/.test(file.slice(colon + 1))) {
|
|
3426
|
+
return lastExtension(trimEndOf(file.slice(0, colon), ". "));
|
|
3427
|
+
}
|
|
3428
|
+
return whole;
|
|
3429
|
+
}
|
|
3430
|
+
function lastExtension(base) {
|
|
3420
3431
|
const dot = base.lastIndexOf(".");
|
|
3421
3432
|
return dot === -1 ? "" : base.slice(dot + 1).trim();
|
|
3422
3433
|
}
|
|
@@ -3432,8 +3443,13 @@ function oleHasMacros(buffer) {
|
|
|
3432
3443
|
return false;
|
|
3433
3444
|
}
|
|
3434
3445
|
var PDF_ACTIVE = linear(String.raw`\/(?:JavaScript|JS|Launch)\b`);
|
|
3435
|
-
var
|
|
3436
|
-
|
|
3446
|
+
var escaped = (character) => {
|
|
3447
|
+
const hex = character.codePointAt(0).toString(16);
|
|
3448
|
+
return `(?:${character}|#${hex[0]}[${hex[1].toLowerCase()}${hex[1].toUpperCase()}])`;
|
|
3449
|
+
};
|
|
3450
|
+
var WORD_ESCAPE = String.raw`#(?:3\d|4[1-9a-fA-F]|5[\daA]|5[fF]|6[1-9a-fA-F]|7[\daA])`;
|
|
3451
|
+
var PDF_ACTIVE_ESCAPED = new RegExp(`/(?:${["JavaScript", "JS", "Launch"].map((name) => [...name].map((character) => escaped(character)).join("")).join("|")})(?![\\w]|${WORD_ESCAPE})`);
|
|
3452
|
+
var pdfActive = (text) => PDF_ACTIVE.test(text) || PDF_ACTIVE_ESCAPED.test(text);
|
|
3437
3453
|
function inspectAttachments(attachments = []) {
|
|
3438
3454
|
const findings = [];
|
|
3439
3455
|
for (const attachment of attachments) {
|
|
@@ -3491,7 +3507,7 @@ function inspectAttachments(attachments = []) {
|
|
|
3491
3507
|
add("macro", `Attachment "${name}" is an Office document with a VBA macro project`, { extension });
|
|
3492
3508
|
}
|
|
3493
3509
|
} else if (detected?.type === "pdf" || extension === "pdf") {
|
|
3494
|
-
if (
|
|
3510
|
+
if (pdfActive(content.toString("latin1"))) {
|
|
3495
3511
|
add("pdf_active", `PDF "${name}" contains JavaScript or a launch action`, { extension: "pdf" });
|
|
3496
3512
|
}
|
|
3497
3513
|
} else if (detected?.type === "rtf" && /\\object\b|\\objdata\b|\\objupdate\b/.test(content.toString("latin1"))) {
|
|
@@ -3539,7 +3555,7 @@ function boundAddressField(line, room) {
|
|
|
3539
3555
|
const limit = Math.min(line.length, MAX_ADDRESS_FIELD, room);
|
|
3540
3556
|
let end = start;
|
|
3541
3557
|
let expecting = "";
|
|
3542
|
-
let
|
|
3558
|
+
let escaped2 = false;
|
|
3543
3559
|
let separators = 0;
|
|
3544
3560
|
let groups = 0;
|
|
3545
3561
|
let index = start;
|
|
@@ -3548,8 +3564,8 @@ function boundAddressField(line, room) {
|
|
|
3548
3564
|
if ((chr === "," || chr === ";") && ++separators > MAX_ADDRESS_SEPARATORS) {
|
|
3549
3565
|
break;
|
|
3550
3566
|
}
|
|
3551
|
-
if (
|
|
3552
|
-
|
|
3567
|
+
if (escaped2) {
|
|
3568
|
+
escaped2 = false;
|
|
3553
3569
|
} else if (expecting !== "" && chr === expecting) {
|
|
3554
3570
|
expecting = "";
|
|
3555
3571
|
end = chr === ";" ? index + 1 : end;
|
|
@@ -3557,7 +3573,7 @@ function boundAddressField(line, room) {
|
|
|
3557
3573
|
expecting = ADDRESS_OPERATORS.get(chr);
|
|
3558
3574
|
end = chr === "," || chr === ";" ? index : end;
|
|
3559
3575
|
} else if (expecting === '"' && chr === "\\") {
|
|
3560
|
-
|
|
3576
|
+
escaped2 = true;
|
|
3561
3577
|
}
|
|
3562
3578
|
if (chr === ":" && expecting === ";" && ++groups > MAX_ADDRESS_GROUPS) {
|
|
3563
3579
|
break;
|
|
@@ -3700,9 +3716,14 @@ var SIGNATURE_FIELDS = /* @__PURE__ */ new Set(["dkim-signature", "arc-message-s
|
|
|
3700
3716
|
var AUTH_FIELDS = /* @__PURE__ */ new Set([...KEPT_FIELDS, "arc-seal", "arc-message-signature"]);
|
|
3701
3717
|
var MAX_SIGNATURES = 10;
|
|
3702
3718
|
var MAX_DKIM_SIGNATURES = 8;
|
|
3719
|
+
function isAttachedMessage(attachment) {
|
|
3720
|
+
const type = String(attachment.contentType).toLowerCase();
|
|
3721
|
+
return type === "message/rfc822" || type === "message/global" || /\.(?:eml|u8msg)$/i.test(String(attachment.filename));
|
|
3722
|
+
}
|
|
3703
3723
|
var MAX_NESTED_DEPTH = 3;
|
|
3704
3724
|
var MAX_ATTACHMENTS = 100;
|
|
3705
3725
|
var MAX_NESTED_BYTES = 25 * 1024 * 1024;
|
|
3726
|
+
var MAX_ATTACHED_MESSAGES = 100;
|
|
3706
3727
|
var PARSE_TIMEOUT = 6e4;
|
|
3707
3728
|
async function simpleParse(raw, { timeout = PARSE_TIMEOUT, parser = simpleParser } = {}) {
|
|
3708
3729
|
const input = Readable.from([finishable(unfoldHeaders(raw))], { objectMode: false, autoDestroy: false });
|
|
@@ -3760,18 +3781,18 @@ function headerValue2(line) {
|
|
|
3760
3781
|
let key = null;
|
|
3761
3782
|
let inKey = false;
|
|
3762
3783
|
let quoted = false;
|
|
3763
|
-
let
|
|
3784
|
+
let escaped2 = false;
|
|
3764
3785
|
let value = "";
|
|
3765
3786
|
for (const chr of line.slice(line.indexOf(":") + 1).replaceAll(/\r?\n[ \t]*/g, " ")) {
|
|
3766
3787
|
if (inKey && chr === "=") {
|
|
3767
3788
|
key = value.trim().toLowerCase();
|
|
3768
3789
|
inKey = false;
|
|
3769
3790
|
value = "";
|
|
3770
|
-
} else if (inKey ||
|
|
3791
|
+
} else if (inKey || escaped2) {
|
|
3771
3792
|
value += chr;
|
|
3772
|
-
|
|
3793
|
+
escaped2 = false;
|
|
3773
3794
|
} else if (chr === "\\") {
|
|
3774
|
-
|
|
3795
|
+
escaped2 = true;
|
|
3775
3796
|
} else if (chr === '"') {
|
|
3776
3797
|
quoted = !quoted;
|
|
3777
3798
|
} else if (chr === ";" && !quoted) {
|
|
@@ -3819,7 +3840,7 @@ function unfoldHeaders(raw) {
|
|
|
3819
3840
|
if (blank && type.boundary) {
|
|
3820
3841
|
boundaries.push(Buffer2.from(type.boundary));
|
|
3821
3842
|
}
|
|
3822
|
-
head = blank &&
|
|
3843
|
+
head = blank && ["message/rfc822", "message/global"].includes(type.value) && ["", "7bit", "8bit", "binary"].includes(encoding) && first.get("content-disposition")?.value === "inline";
|
|
3823
3844
|
};
|
|
3824
3845
|
for (let start = 0; start < raw.length; ) {
|
|
3825
3846
|
const lf = raw.indexOf(10, start);
|
|
@@ -3956,6 +3977,7 @@ async function expandAttachments(attachments = [], errors = [], parse = parseMes
|
|
|
3956
3977
|
let budget = MAX_NESTED_BYTES;
|
|
3957
3978
|
let nested = 0;
|
|
3958
3979
|
let tooMany = false;
|
|
3980
|
+
let opened = 0;
|
|
3959
3981
|
const visit = async (list, prefix, depth) => {
|
|
3960
3982
|
for (const attachment of list) {
|
|
3961
3983
|
if (prefix) {
|
|
@@ -3972,17 +3994,22 @@ async function expandAttachments(attachments = [], errors = [], parse = parseMes
|
|
|
3972
3994
|
const filename = prefix ? `${prefix}/${name}` : name || "attached message";
|
|
3973
3995
|
all.push(prefix ? { ...attachment, filename: `${prefix}/${name}` } : attachment);
|
|
3974
3996
|
const { content } = attachment;
|
|
3975
|
-
const attached =
|
|
3997
|
+
const attached = isAttachedMessage(attachment);
|
|
3976
3998
|
if (attached && Buffer2.isBuffer(content) && depth >= MAX_NESTED_DEPTH) {
|
|
3977
3999
|
unopened.push({ filename, reason: `nested more than ${MAX_NESTED_DEPTH} deep` });
|
|
3978
4000
|
continue;
|
|
3979
4001
|
}
|
|
4002
|
+
if (attached && Buffer2.isBuffer(content) && opened >= MAX_ATTACHED_MESSAGES) {
|
|
4003
|
+
unopened.push({ filename, reason: `more than ${MAX_ATTACHED_MESSAGES} attached messages` });
|
|
4004
|
+
continue;
|
|
4005
|
+
}
|
|
3980
4006
|
if (attached && Buffer2.isBuffer(content) && content.length > budget) {
|
|
3981
4007
|
unopened.push({ filename, reason: `past ${MAX_NESTED_BYTES / 1024 / 1024} MB of attached messages` });
|
|
3982
4008
|
continue;
|
|
3983
4009
|
}
|
|
3984
4010
|
if (attached && Buffer2.isBuffer(content)) {
|
|
3985
4011
|
budget -= content.length;
|
|
4012
|
+
opened++;
|
|
3986
4013
|
let inner;
|
|
3987
4014
|
try {
|
|
3988
4015
|
inner = await parseWithin(content, parse, deadline);
|
|
@@ -5668,9 +5695,12 @@ ${visibleText(html.slice(0, 1e5)).text}`)) {
|
|
|
5668
5695
|
hit("SEXTORTION_SUBJECT", 2, "Subject used by sextortion and account takeover scams, without a demand for payment", "SCAM");
|
|
5669
5696
|
}
|
|
5670
5697
|
const markup = html.slice(0, 1e5);
|
|
5698
|
+
const readable = readableText(`${subject}
|
|
5699
|
+
${text.slice(0, 5e4)}
|
|
5700
|
+
${visibleText(withoutHelp(markup)).text}`);
|
|
5671
5701
|
if (isInjection(`${subject}
|
|
5672
5702
|
${text.slice(0, 5e4)}
|
|
5673
|
-
${withoutHelp(markup)}`) || attributeText(markup).some(({ text: text2, start, end, help }) => (!help || isOrder(text2)) && isInjection(unquoted(text2), AI_ACTION_OBJECT, () => shownNear(markup, start, end)))) {
|
|
5703
|
+
${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)))) {
|
|
5674
5704
|
hit("PROMPT_INJECTION", 3, "Contains instructions addressed to an AI filter", "SPAM");
|
|
5675
5705
|
}
|
|
5676
5706
|
if (session.resolvedClientHostname?.endsWith(".outbound.protection.outlook.com")) {
|
|
@@ -7048,7 +7078,7 @@ function scoreResults(results, options) {
|
|
|
7048
7078
|
var package_default = {
|
|
7049
7079
|
name: "spamscanner",
|
|
7050
7080
|
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.",
|
|
7051
|
-
version: "7.0.
|
|
7081
|
+
version: "7.0.10",
|
|
7052
7082
|
author: "Forward Email (https://forwardemail.net)",
|
|
7053
7083
|
bin: {
|
|
7054
7084
|
spamscanner: "./dist/esm/cli.js"
|
|
@@ -9098,7 +9128,7 @@ var SpamScanner = class {
|
|
|
9098
9128
|
message: `Attachment "${filename}" contains ${result.viruses.join(", ")}`
|
|
9099
9129
|
});
|
|
9100
9130
|
viruses.push(found);
|
|
9101
|
-
if (
|
|
9131
|
+
if (isAttachedMessage(attachment)) {
|
|
9102
9132
|
carriers.set(found, attachment.filename || "attached message");
|
|
9103
9133
|
}
|
|
9104
9134
|
}
|