spamscanner 7.0.4 → 7.0.5
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 +245 -11
- package/dist/cjs/arf.cjs.map +3 -3
- package/dist/cjs/index.cjs +858 -408
- package/dist/cjs/index.cjs.map +4 -4
- package/dist/esm/arf.js +246 -12
- package/dist/esm/arf.js.map +3 -3
- package/dist/esm/cli.js +993 -444
- package/dist/esm/cli.js.map +4 -4
- package/dist/esm/index.js +835 -383
- package/dist/esm/index.js.map +4 -4
- package/dist/types/arf.d.ts +4 -4
- package/dist/types/attachments.d.ts +3 -1
- package/dist/types/auth.d.ts +32 -2
- package/dist/types/milter.d.ts +7 -1
- package/dist/types/model.d.ts +3 -2
- package/dist/types/parse.d.ts +66 -4
- package/dist/types/reputation.d.ts +2 -1
- package/dist/types/server.d.ts +17 -0
- package/dist/types/spamd.d.ts +2 -0
- package/package.json +1 -1
package/dist/cjs/index.cjs
CHANGED
|
@@ -3642,16 +3642,19 @@ function zipEntries(buffer, limit = 2e3) {
|
|
|
3642
3642
|
records.push(i);
|
|
3643
3643
|
}
|
|
3644
3644
|
}
|
|
3645
|
-
const
|
|
3646
|
-
|
|
3645
|
+
const state = {
|
|
3646
|
+
entries: [],
|
|
3647
|
+
limit,
|
|
3648
|
+
read: /* @__PURE__ */ new Set(),
|
|
3649
|
+
names: MAX_NAME_BYTES
|
|
3650
|
+
};
|
|
3647
3651
|
for (const eocd of records) {
|
|
3648
3652
|
const offset = directoryOf(buffer, eocd);
|
|
3649
|
-
if (offset !== -1
|
|
3650
|
-
|
|
3651
|
-
readDirectory(buffer, offset, limit - entries.length, entries);
|
|
3653
|
+
if (offset !== -1) {
|
|
3654
|
+
readDirectory(buffer, offset, state);
|
|
3652
3655
|
}
|
|
3653
3656
|
}
|
|
3654
|
-
return entries;
|
|
3657
|
+
return state.entries;
|
|
3655
3658
|
}
|
|
3656
3659
|
function directoryOf(buffer, eocd) {
|
|
3657
3660
|
let offset = buffer.readUInt32LE(eocd + 16);
|
|
@@ -3677,18 +3680,32 @@ function directoryOf(buffer, eocd) {
|
|
|
3677
3680
|
}
|
|
3678
3681
|
return -1;
|
|
3679
3682
|
}
|
|
3680
|
-
function readDirectory(buffer, offset,
|
|
3681
|
-
|
|
3683
|
+
function readDirectory(buffer, offset, state) {
|
|
3684
|
+
while (state.entries.length < state.limit && isDirectory(buffer, offset) && !state.read.has(offset)) {
|
|
3685
|
+
state.read.add(offset);
|
|
3682
3686
|
const flags = buffer.readUInt16LE(offset + 8);
|
|
3683
3687
|
const size = buffer.readUInt32LE(offset + 24);
|
|
3684
3688
|
const nameLength = buffer.readUInt16LE(offset + 28);
|
|
3685
3689
|
const extraLength = buffer.readUInt16LE(offset + 30);
|
|
3686
3690
|
const commentLength = buffer.readUInt16LE(offset + 32);
|
|
3687
|
-
const
|
|
3688
|
-
|
|
3691
|
+
const encoding = flags & 2048 ? "utf8" : "latin1";
|
|
3692
|
+
const start = offset + 46;
|
|
3693
|
+
const end = Math.min(buffer.length, start + nameLength);
|
|
3694
|
+
const kept = Math.min(end - start, MAX_NAME_LENGTH);
|
|
3695
|
+
state.names -= kept;
|
|
3696
|
+
if (state.names < 0) {
|
|
3697
|
+
return;
|
|
3698
|
+
}
|
|
3699
|
+
const half = MAX_NAME_LENGTH / 2;
|
|
3700
|
+
const name = end - start > MAX_NAME_LENGTH ? `${buffer.toString(encoding, start, start + half)}\u2026${buffer.toString(encoding, end - half, end)}` : buffer.toString(encoding, start, end);
|
|
3701
|
+
state.entries.push({ name, encrypted: (flags & 1) === 1, size });
|
|
3689
3702
|
offset += 46 + nameLength + extraLength + commentLength;
|
|
3690
3703
|
}
|
|
3691
3704
|
}
|
|
3705
|
+
function shorten(name, max = 100) {
|
|
3706
|
+
const characters = [...name];
|
|
3707
|
+
return characters.length <= max ? name : `${characters.slice(0, max - 41).join("")}\u2026${characters.slice(-40).join("")}`;
|
|
3708
|
+
}
|
|
3692
3709
|
function extensionOf(name) {
|
|
3693
3710
|
const base = trimEndOf(String(name).toLowerCase().split(/[/\\]/).pop(), ". ");
|
|
3694
3711
|
const dot = base.lastIndexOf(".");
|
|
@@ -3747,7 +3764,7 @@ function inspectAttachments(attachments = []) {
|
|
|
3747
3764
|
}
|
|
3748
3765
|
const inner = entries.filter((entry) => EXECUTABLE_EXTENSIONS.has(extensionOf(entry.name)) && !(office && OFFICE_EMBEDDED.has(extensionOf(entry.name))));
|
|
3749
3766
|
const details = { extension: extensionOf(inner[0]?.name ?? ""), extensions: [...new Set(inner.map((entry) => extensionOf(entry.name)))] };
|
|
3750
|
-
const listed = inner.slice(0, 3).map((entry) => entry.name.split("/").pop()).join(", ");
|
|
3767
|
+
const listed = inner.slice(0, 3).map((entry) => shorten(entry.name.split("/").pop())).join(", ");
|
|
3751
3768
|
if (inner.some((entry) => !SCRIPT_EXTENSIONS.has(extensionOf(entry.name)))) {
|
|
3752
3769
|
add("executable_in_archive", `Archive "${name}" contains ${inner.length === 1 ? "a program" : `${inner.length} programs`} (${listed})`, details);
|
|
3753
3770
|
} else if (inner.length > 0) {
|
|
@@ -3775,7 +3792,7 @@ function inspectAttachments(attachments = []) {
|
|
|
3775
3792
|
}
|
|
3776
3793
|
return findings;
|
|
3777
3794
|
}
|
|
3778
|
-
var import_node_buffer, EXECUTABLE_EXTENSIONS, RISKY_EXTENSIONS, SCRIPT_EXTENSIONS, MACRO_EXTENSIONS, DOCUMENT_EXTENSIONS, TEMPORARY_EXTENSIONS, BIDI_CONTROLS, ARCHIVE_TYPES, ZIP_FAMILY, OFFICE_TYPES, OFFICE_EMBEDDED, ENTRIES, DIRECTORY, isDirectory, VBA_NAMES, PDF_ACTIVE, PDF_ESCAPED_NAME, decodePdfNames;
|
|
3795
|
+
var import_node_buffer, EXECUTABLE_EXTENSIONS, RISKY_EXTENSIONS, SCRIPT_EXTENSIONS, MACRO_EXTENSIONS, DOCUMENT_EXTENSIONS, TEMPORARY_EXTENSIONS, BIDI_CONTROLS, ARCHIVE_TYPES, ZIP_FAMILY, OFFICE_TYPES, OFFICE_EMBEDDED, ENTRIES, MAX_NAME_LENGTH, MAX_NAME_BYTES, DIRECTORY, isDirectory, VBA_NAMES, PDF_ACTIVE, PDF_ESCAPED_NAME, decodePdfNames;
|
|
3779
3796
|
var init_attachments = __esm({
|
|
3780
3797
|
"src/attachments.js"() {
|
|
3781
3798
|
import_node_buffer = require("node:buffer");
|
|
@@ -3931,6 +3948,8 @@ var init_attachments = __esm({
|
|
|
3931
3948
|
OFFICE_TYPES = /* @__PURE__ */ new Set(["docx", "xlsx", "pptx"]);
|
|
3932
3949
|
OFFICE_EMBEDDED = MACRO_EXTENSIONS;
|
|
3933
3950
|
ENTRIES = /* @__PURE__ */ new WeakMap();
|
|
3951
|
+
MAX_NAME_LENGTH = 1024;
|
|
3952
|
+
MAX_NAME_BYTES = 1024 * 1024;
|
|
3934
3953
|
DIRECTORY = 33639248;
|
|
3935
3954
|
isDirectory = (buffer, offset) => offset >= 0 && offset + 46 <= buffer.length && buffer.readUInt32LE(offset) === DIRECTORY;
|
|
3936
3955
|
VBA_NAMES = /* @__PURE__ */ new Set(["vba", "_vba_project", "_vba_project_cur", "macros"]);
|
|
@@ -3940,6 +3959,487 @@ var init_attachments = __esm({
|
|
|
3940
3959
|
}
|
|
3941
3960
|
});
|
|
3942
3961
|
|
|
3962
|
+
// src/parse.js
|
|
3963
|
+
function boundAddressField(line, room) {
|
|
3964
|
+
const start = line.indexOf(":") + 1;
|
|
3965
|
+
const limit = Math.min(line.length, MAX_ADDRESS_FIELD, room);
|
|
3966
|
+
let end = start;
|
|
3967
|
+
let expecting = "";
|
|
3968
|
+
let escaped = false;
|
|
3969
|
+
let separators = 0;
|
|
3970
|
+
let groups = 0;
|
|
3971
|
+
let index = start;
|
|
3972
|
+
for (; index < limit; index++) {
|
|
3973
|
+
const chr = line[index];
|
|
3974
|
+
if ((chr === "," || chr === ";") && ++separators > MAX_ADDRESS_SEPARATORS) {
|
|
3975
|
+
break;
|
|
3976
|
+
}
|
|
3977
|
+
if (escaped) {
|
|
3978
|
+
escaped = false;
|
|
3979
|
+
} else if (expecting !== "" && chr === expecting) {
|
|
3980
|
+
expecting = "";
|
|
3981
|
+
end = chr === ";" ? index + 1 : end;
|
|
3982
|
+
} else if (expecting === "" && ADDRESS_OPERATORS.has(chr)) {
|
|
3983
|
+
expecting = ADDRESS_OPERATORS.get(chr);
|
|
3984
|
+
end = chr === "," || chr === ";" ? index : end;
|
|
3985
|
+
} else if (expecting === '"' && chr === "\\") {
|
|
3986
|
+
escaped = true;
|
|
3987
|
+
}
|
|
3988
|
+
if (chr === ":" && expecting === ";" && ++groups > MAX_ADDRESS_GROUPS) {
|
|
3989
|
+
break;
|
|
3990
|
+
}
|
|
3991
|
+
}
|
|
3992
|
+
return index === line.length ? line : line.slice(0, end).trimEnd();
|
|
3993
|
+
}
|
|
3994
|
+
function boundAddressLines(lines2, budget, only = null) {
|
|
3995
|
+
const cut = /* @__PURE__ */ new Set();
|
|
3996
|
+
const bounded = lines2.map((entry) => {
|
|
3997
|
+
if (only ? !only.has(entry.key) : !ADDRESS_FIELDS.has(entry.key) && !entry.key.startsWith("list-")) {
|
|
3998
|
+
return entry;
|
|
3999
|
+
}
|
|
4000
|
+
const line = boundAddressField(entry.line, budget.room);
|
|
4001
|
+
budget.room = Math.max(budget.room - line.length, 0);
|
|
4002
|
+
if (line === entry.line) {
|
|
4003
|
+
return entry;
|
|
4004
|
+
}
|
|
4005
|
+
cut.add(entry.key);
|
|
4006
|
+
return { ...entry, line };
|
|
4007
|
+
});
|
|
4008
|
+
return { lines: bounded, cut };
|
|
4009
|
+
}
|
|
4010
|
+
function boundedForAuth(raw) {
|
|
4011
|
+
const ends = [raw.indexOf("\n\n"), raw.indexOf("\n\r\n")].filter((index) => index !== -1);
|
|
4012
|
+
const end = ends.length > 0 ? Math.min(...ends) + 1 : raw.length;
|
|
4013
|
+
const body = raw.subarray(ends.length > 0 ? raw.indexOf(10, end) + 1 : raw.length);
|
|
4014
|
+
const text = raw.subarray(0, end).toString("latin1");
|
|
4015
|
+
let { length } = text;
|
|
4016
|
+
while (length > 0 && (text[length - 1] === "\n" || text[length - 1] === "\r")) {
|
|
4017
|
+
length--;
|
|
4018
|
+
}
|
|
4019
|
+
const fields = [];
|
|
4020
|
+
let folds = 0;
|
|
4021
|
+
let work = 0;
|
|
4022
|
+
for (let start = 0; start <= length; ) {
|
|
4023
|
+
let stop = text.indexOf("\n", start);
|
|
4024
|
+
stop = stop === -1 || stop > length ? length : stop;
|
|
4025
|
+
const lineEnd = stop > start && text[stop - 1] === "\r" ? stop - 1 : stop;
|
|
4026
|
+
const last = fields.at(-1);
|
|
4027
|
+
if (last && /\s/.test(text[start])) {
|
|
4028
|
+
last.end = lineEnd;
|
|
4029
|
+
last.folds++;
|
|
4030
|
+
folds++;
|
|
4031
|
+
work += last.folds;
|
|
4032
|
+
} else {
|
|
4033
|
+
fields.push({ start, end: lineEnd, folds: 0 });
|
|
4034
|
+
}
|
|
4035
|
+
start = stop + 1;
|
|
4036
|
+
}
|
|
4037
|
+
let signatures = 0;
|
|
4038
|
+
let dkim = 0;
|
|
4039
|
+
for (const field of fields) {
|
|
4040
|
+
field.line = text.slice(field.start, field.end);
|
|
4041
|
+
const colon = field.line.indexOf(":");
|
|
4042
|
+
field.key = (colon === -1 ? field.line : field.line.slice(0, colon)).trim().toLowerCase();
|
|
4043
|
+
field.dropped = field.key === "dkim-signature" && ++dkim > MAX_DKIM_SIGNATURES;
|
|
4044
|
+
if (SIGNATURE_FIELDS.has(field.key) && !field.dropped) {
|
|
4045
|
+
signatures += field.line.split(":").length - 1;
|
|
4046
|
+
}
|
|
4047
|
+
}
|
|
4048
|
+
const read = dkim > MAX_DKIM_SIGNATURES ? fields.filter((field) => !field.dropped) : fields;
|
|
4049
|
+
work += (folds + signatures) * read.length;
|
|
4050
|
+
const bounded = boundAddressLines(read, { room: MAX_ADDRESS_TOTAL }, AUTH_ADDRESS_FIELDS);
|
|
4051
|
+
let kept = bounded.lines;
|
|
4052
|
+
let fromCut = bounded.cut.has("from");
|
|
4053
|
+
if (work > MAX_AUTH_WORK) {
|
|
4054
|
+
const counts = /* @__PURE__ */ new Map();
|
|
4055
|
+
kept = kept.filter((field) => {
|
|
4056
|
+
counts.set(field.key, (counts.get(field.key) || 0) + 1);
|
|
4057
|
+
const keep = AUTH_FIELDS.has(field.key) && counts.get(field.key) <= (SIGNATURE_FIELDS.has(field.key) ? MAX_SIGNATURES : MAX_REPEATED);
|
|
4058
|
+
fromCut ||= field.key === "from" && !keep;
|
|
4059
|
+
return keep;
|
|
4060
|
+
}).map((field) => {
|
|
4061
|
+
const line = field.line.replaceAll(/\r?\n(?=\s)/g, "");
|
|
4062
|
+
return { ...field, line: ADDRESS_FIELDS.has(field.key) ? line : line.slice(0, MAX_FIELD) };
|
|
4063
|
+
});
|
|
4064
|
+
} else if (bounded.cut.size === 0 && read === fields) {
|
|
4065
|
+
return { raw, fromCut };
|
|
4066
|
+
}
|
|
4067
|
+
const header = kept.map((field) => field.line).join("\r\n");
|
|
4068
|
+
return { raw: import_node_buffer2.Buffer.concat([import_node_buffer2.Buffer.from(`${header}\r
|
|
4069
|
+
\r
|
|
4070
|
+
`, "latin1"), body]), fromCut };
|
|
4071
|
+
}
|
|
4072
|
+
async function simpleParse(raw, { timeout = PARSE_TIMEOUT, parser = import_mailparser.simpleParser } = {}) {
|
|
4073
|
+
const input = import_node_stream.Readable.from([finishable(unfoldHeaders(raw))], { objectMode: false, autoDestroy: false });
|
|
4074
|
+
input.on("error", () => {
|
|
4075
|
+
});
|
|
4076
|
+
const { pipe } = input;
|
|
4077
|
+
input.pipe = function(parser2, options) {
|
|
4078
|
+
parser2.splitter?.on("error", (error) => input.destroy(error));
|
|
4079
|
+
return pipe.call(this, parser2, options);
|
|
4080
|
+
};
|
|
4081
|
+
let timer;
|
|
4082
|
+
try {
|
|
4083
|
+
return await Promise.race([
|
|
4084
|
+
parser(input, PARSER_OPTIONS),
|
|
4085
|
+
new Promise((resolve, reject) => {
|
|
4086
|
+
timer = setTimeout(() => {
|
|
4087
|
+
const error = Object.assign(new Error(`Message not parsed within ${timeout} ms`), { code: "ETIMEDOUT" });
|
|
4088
|
+
input.destroy(error);
|
|
4089
|
+
reject(error);
|
|
4090
|
+
}, timeout);
|
|
4091
|
+
})
|
|
4092
|
+
]);
|
|
4093
|
+
} finally {
|
|
4094
|
+
clearTimeout(timer);
|
|
4095
|
+
input.destroy();
|
|
4096
|
+
}
|
|
4097
|
+
}
|
|
4098
|
+
async function parseMessage(raw, options = {}) {
|
|
4099
|
+
const buffer = import_node_buffer2.Buffer.isBuffer(raw) ? raw : import_node_buffer2.Buffer.from(raw);
|
|
4100
|
+
const mail = await simpleParse(buffer, options);
|
|
4101
|
+
const added = buffer.at(-1) === 10 ? 2 : 3;
|
|
4102
|
+
for (const key of ["text", "html"]) {
|
|
4103
|
+
if (typeof mail[key] === "string") {
|
|
4104
|
+
mail[key] = mail[key].replace(new RegExp(`\\n{0,${added}}$`), "");
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
const tail = import_node_buffer2.Buffer.from("\r\n".repeat(added));
|
|
4108
|
+
const last = mail.attachments?.at(-1);
|
|
4109
|
+
const encoding = String(last?.headers?.get?.("content-transfer-encoding") ?? "7bit").trim().toLowerCase();
|
|
4110
|
+
const own3 = import_node_buffer2.Buffer.isBuffer(last?.content) && last.content.length >= tail.length && last.content.subarray(-tail.length).equals(tail) ? last.content.subarray(0, -tail.length) : null;
|
|
4111
|
+
if (own3 && ["7bit", "8bit", "binary"].includes(encoding) && own3.length <= buffer.length && buffer.subarray(buffer.length - own3.length).equals(own3)) {
|
|
4112
|
+
last.content = own3;
|
|
4113
|
+
last.size = own3.length;
|
|
4114
|
+
last.checksum = (0, import_node_crypto.createHash)("md5").update(own3).digest("hex");
|
|
4115
|
+
}
|
|
4116
|
+
return mail;
|
|
4117
|
+
}
|
|
4118
|
+
function finishable(raw) {
|
|
4119
|
+
const buffer = import_node_buffer2.Buffer.isBuffer(raw) ? raw : import_node_buffer2.Buffer.from(raw);
|
|
4120
|
+
return import_node_buffer2.Buffer.concat([buffer, import_node_buffer2.Buffer.from(buffer.at(-1) === 10 ? "\r\n\r\n" : "\r\n\r\n\r\n")]);
|
|
4121
|
+
}
|
|
4122
|
+
function headerValue2(line) {
|
|
4123
|
+
const parameters = /* @__PURE__ */ new Map();
|
|
4124
|
+
let key = null;
|
|
4125
|
+
let inKey = false;
|
|
4126
|
+
let quoted = false;
|
|
4127
|
+
let escaped = false;
|
|
4128
|
+
let value = "";
|
|
4129
|
+
for (const chr of line.slice(line.indexOf(":") + 1).replaceAll(/\r?\n[ \t]*/g, " ")) {
|
|
4130
|
+
if (inKey && chr === "=") {
|
|
4131
|
+
key = value.trim().toLowerCase();
|
|
4132
|
+
inKey = false;
|
|
4133
|
+
value = "";
|
|
4134
|
+
} else if (inKey || escaped) {
|
|
4135
|
+
value += chr;
|
|
4136
|
+
escaped = false;
|
|
4137
|
+
} else if (chr === "\\") {
|
|
4138
|
+
escaped = true;
|
|
4139
|
+
} else if (chr === '"') {
|
|
4140
|
+
quoted = !quoted;
|
|
4141
|
+
} else if (chr === ";" && !quoted) {
|
|
4142
|
+
parameters.set(key, value.trim());
|
|
4143
|
+
inKey = true;
|
|
4144
|
+
value = "";
|
|
4145
|
+
} else {
|
|
4146
|
+
value += chr;
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
4149
|
+
if (!inKey) {
|
|
4150
|
+
parameters.set(key, value.trim());
|
|
4151
|
+
}
|
|
4152
|
+
return { value: parameters.get(null).toLowerCase(), boundary: parameters.get("boundary") ?? "" };
|
|
4153
|
+
}
|
|
4154
|
+
function delimiter(raw, start, stop, boundary) {
|
|
4155
|
+
const rest = start + 2 + boundary.length;
|
|
4156
|
+
if (stop - start < boundary.length + 2 || stop - rest > 4 || raw[start] !== 45 || raw[start + 1] !== 45 || !raw.subarray(start + 2, rest).equals(boundary)) {
|
|
4157
|
+
return 0;
|
|
4158
|
+
}
|
|
4159
|
+
if (raw[rest] === 13 || raw[rest] === 10) {
|
|
4160
|
+
return 1;
|
|
4161
|
+
}
|
|
4162
|
+
const tail = raw.subarray(rest, stop).toString("latin1");
|
|
4163
|
+
return /^-{0,2}$|^--\r?\n$|^--\r$/.test(tail) ? 2 : 0;
|
|
4164
|
+
}
|
|
4165
|
+
function unfoldHeaders(raw) {
|
|
4166
|
+
const boundaries = [];
|
|
4167
|
+
const unfold = [];
|
|
4168
|
+
let head = true;
|
|
4169
|
+
let fields = [];
|
|
4170
|
+
const endHead = (blank) => {
|
|
4171
|
+
unfold.push(...fields.filter((field) => field.folds > MAX_FOLDS));
|
|
4172
|
+
const first = /* @__PURE__ */ new Map();
|
|
4173
|
+
for (const field of fields) {
|
|
4174
|
+
const name = raw.toString("latin1", field.start, Math.min(field.end, field.start + 32));
|
|
4175
|
+
const key = name.slice(0, name.indexOf(":")).trim().toLowerCase();
|
|
4176
|
+
if (["content-type", "content-transfer-encoding", "content-disposition"].includes(key) && !first.has(key)) {
|
|
4177
|
+
first.set(key, headerValue2(raw.toString("latin1", field.start, field.end)));
|
|
4178
|
+
}
|
|
4179
|
+
}
|
|
4180
|
+
fields = [];
|
|
4181
|
+
const type = first.get("content-type") ?? { value: "", boundary: "" };
|
|
4182
|
+
const encoding = (first.get("content-transfer-encoding")?.value ?? "").replaceAll(/\(.*\)/g, "").trim();
|
|
4183
|
+
if (blank && type.boundary) {
|
|
4184
|
+
boundaries.push(import_node_buffer2.Buffer.from(type.boundary));
|
|
4185
|
+
}
|
|
4186
|
+
head = blank && type.value === "message/rfc822" && ["", "7bit", "8bit", "binary"].includes(encoding) && first.get("content-disposition")?.value === "inline";
|
|
4187
|
+
};
|
|
4188
|
+
for (let start = 0; start < raw.length; ) {
|
|
4189
|
+
const lf = raw.indexOf(10, start);
|
|
4190
|
+
const stop = lf === -1 ? raw.length : lf + 1;
|
|
4191
|
+
const end = lf === -1 ? stop : raw[lf - 1] === 13 ? lf - 1 : lf;
|
|
4192
|
+
const kind = boundaries.length > 0 ? delimiter(raw, start, stop, boundaries.at(-1)) : 0;
|
|
4193
|
+
if (kind !== 0) {
|
|
4194
|
+
if (head) {
|
|
4195
|
+
endHead(false);
|
|
4196
|
+
}
|
|
4197
|
+
head = kind === 1;
|
|
4198
|
+
if (kind === 2) {
|
|
4199
|
+
boundaries.pop();
|
|
4200
|
+
}
|
|
4201
|
+
} else if (head && end === start) {
|
|
4202
|
+
endHead(true);
|
|
4203
|
+
} else if (head && fields.length > 0 && (raw[start] === 32 || raw[start] === 9)) {
|
|
4204
|
+
fields.at(-1).end = end;
|
|
4205
|
+
fields.at(-1).folds++;
|
|
4206
|
+
} else if (head) {
|
|
4207
|
+
fields.push({ start, end, folds: 0 });
|
|
4208
|
+
}
|
|
4209
|
+
start = stop;
|
|
4210
|
+
}
|
|
4211
|
+
unfold.push(...fields.filter((field) => field.folds > MAX_FOLDS));
|
|
4212
|
+
if (unfold.length === 0) {
|
|
4213
|
+
return raw;
|
|
4214
|
+
}
|
|
4215
|
+
const chunks = [];
|
|
4216
|
+
let copied = 0;
|
|
4217
|
+
for (const field of unfold) {
|
|
4218
|
+
chunks.push(raw.subarray(copied, field.start), import_node_buffer2.Buffer.from(raw.toString("latin1", field.start, field.end).replaceAll(/\r?\n[ \t]*/g, " "), "latin1"));
|
|
4219
|
+
copied = field.end;
|
|
4220
|
+
}
|
|
4221
|
+
chunks.push(raw.subarray(copied));
|
|
4222
|
+
return import_node_buffer2.Buffer.concat(chunks);
|
|
4223
|
+
}
|
|
4224
|
+
function upToLine(raw, offset) {
|
|
4225
|
+
return raw.subarray(0, Math.max(raw.lastIndexOf(10, offset - 1) + 1, 0));
|
|
4226
|
+
}
|
|
4227
|
+
function fewerParts(raw) {
|
|
4228
|
+
let offset = -1;
|
|
4229
|
+
for (let count = 0; count < MAX_PART_LINES; count++) {
|
|
4230
|
+
offset = raw.indexOf("\n--", offset + 1);
|
|
4231
|
+
if (offset === -1) {
|
|
4232
|
+
return upToLine(raw, Math.floor(raw.length / 2));
|
|
4233
|
+
}
|
|
4234
|
+
}
|
|
4235
|
+
return raw.subarray(0, offset + 1);
|
|
4236
|
+
}
|
|
4237
|
+
function smallerHeader(raw) {
|
|
4238
|
+
const text = raw.toString("latin1");
|
|
4239
|
+
const blank = /\r?\n\r?\n/.exec(text);
|
|
4240
|
+
const end = blank ? blank.index : text.length;
|
|
4241
|
+
const fields = [];
|
|
4242
|
+
let start = 0;
|
|
4243
|
+
while (start < end) {
|
|
4244
|
+
let stop = text.indexOf("\n", start);
|
|
4245
|
+
stop = stop === -1 || stop > end ? end : stop;
|
|
4246
|
+
const last = fields.at(-1);
|
|
4247
|
+
const whole = text.slice(start, stop).replace(/\r$/, "");
|
|
4248
|
+
if (last && (whole.startsWith(" ") || whole.startsWith(" "))) {
|
|
4249
|
+
const line = last.address ? whole : whole.slice(0, MAX_LINE);
|
|
4250
|
+
if (last.address || last.size + line.length <= MAX_FIELD) {
|
|
4251
|
+
last.lines.push(line);
|
|
4252
|
+
last.size += line.length;
|
|
4253
|
+
}
|
|
4254
|
+
} else {
|
|
4255
|
+
const name = whole.slice(0, whole.indexOf(":")).trim().toLowerCase();
|
|
4256
|
+
const address2 = ADDRESS_FIELDS.has(name) || name.startsWith("list-");
|
|
4257
|
+
const line = address2 ? whole : whole.slice(0, MAX_LINE);
|
|
4258
|
+
fields.push({
|
|
4259
|
+
name,
|
|
4260
|
+
address: address2,
|
|
4261
|
+
lines: [line],
|
|
4262
|
+
size: line.length
|
|
4263
|
+
});
|
|
4264
|
+
}
|
|
4265
|
+
start = stop + 1;
|
|
4266
|
+
}
|
|
4267
|
+
let fromCut = false;
|
|
4268
|
+
for (const field of fields.filter((field2) => field2.address)) {
|
|
4269
|
+
const line = field.lines.join("\r\n");
|
|
4270
|
+
const bounded = boundAddressField(line, Number.POSITIVE_INFINITY);
|
|
4271
|
+
fromCut ||= field.name === "from" && bounded !== line;
|
|
4272
|
+
field.lines = [bounded];
|
|
4273
|
+
field.size = bounded.length;
|
|
4274
|
+
}
|
|
4275
|
+
const total = fields.reduce((sum, field) => sum + field.size, 0);
|
|
4276
|
+
const counts = /* @__PURE__ */ new Map();
|
|
4277
|
+
const kept = total > MAX_HEADER ? fields.filter((field) => {
|
|
4278
|
+
counts.set(field.name, (counts.get(field.name) || 0) + 1);
|
|
4279
|
+
const keep = KEPT_FIELDS.has(field.name) && counts.get(field.name) <= MAX_REPEATED;
|
|
4280
|
+
fromCut ||= field.name === "from" && !keep;
|
|
4281
|
+
return keep;
|
|
4282
|
+
}) : fields;
|
|
4283
|
+
const header = kept.flatMap((field) => field.lines).join("\r\n");
|
|
4284
|
+
return { raw: import_node_buffer2.Buffer.concat([import_node_buffer2.Buffer.from(`${header}\r
|
|
4285
|
+
\r
|
|
4286
|
+
`, "latin1"), raw.subarray(blank ? blank.index + blank[0].length : raw.length)]), fromCut };
|
|
4287
|
+
}
|
|
4288
|
+
async function parseWithin(raw, parse = parseMessage, deadline = Date.now() + PARSE_TIMEOUT) {
|
|
4289
|
+
let input = raw;
|
|
4290
|
+
let reason = null;
|
|
4291
|
+
let fromCut = false;
|
|
4292
|
+
for (; ; ) {
|
|
4293
|
+
try {
|
|
4294
|
+
const mail = await parse(input, { timeout: Math.max(1, deadline - Date.now()) });
|
|
4295
|
+
if (fromCut && mail.from) {
|
|
4296
|
+
mail.from.truncated = true;
|
|
4297
|
+
}
|
|
4298
|
+
return { mail, error: reason };
|
|
4299
|
+
} catch (error) {
|
|
4300
|
+
const limit = error.code === "EMAXLEN" && [TOO_MANY_PARTS, HEADER_TOO_LARGE].includes(error.message) ? error.message : null;
|
|
4301
|
+
let shorter = input;
|
|
4302
|
+
if (limit === TOO_MANY_PARTS) {
|
|
4303
|
+
shorter = fewerParts(input);
|
|
4304
|
+
} else if (limit === HEADER_TOO_LARGE) {
|
|
4305
|
+
const smaller = smallerHeader(input);
|
|
4306
|
+
shorter = smaller.raw.length < input.length ? smaller.raw : upToLine(input, MAX_PREFIX_BYTES);
|
|
4307
|
+
fromCut ||= smaller.raw === shorter && smaller.fromCut;
|
|
4308
|
+
}
|
|
4309
|
+
if (shorter.length >= input.length) {
|
|
4310
|
+
throw error;
|
|
4311
|
+
}
|
|
4312
|
+
reason ??= limit;
|
|
4313
|
+
input = shorter;
|
|
4314
|
+
}
|
|
4315
|
+
}
|
|
4316
|
+
}
|
|
4317
|
+
async function expandAttachments(attachments = [], errors = [], parse = parseMessage) {
|
|
4318
|
+
const all = [];
|
|
4319
|
+
const deadline = Date.now() + PARSE_TIMEOUT;
|
|
4320
|
+
let budget = MAX_NESTED_BYTES;
|
|
4321
|
+
let nested = 0;
|
|
4322
|
+
const visit = async (list, prefix, depth) => {
|
|
4323
|
+
for (const attachment of list) {
|
|
4324
|
+
if (prefix) {
|
|
4325
|
+
if (nested >= MAX_ATTACHMENTS) {
|
|
4326
|
+
return;
|
|
4327
|
+
}
|
|
4328
|
+
nested++;
|
|
4329
|
+
}
|
|
4330
|
+
const name = attachment.filename || (prefix ? "unnamed attachment" : "");
|
|
4331
|
+
all.push(prefix ? { ...attachment, filename: `${prefix}/${name}` } : attachment);
|
|
4332
|
+
const { content } = attachment;
|
|
4333
|
+
const attached = attachment.contentType === "message/rfc822" || /\.eml$/i.test(attachment.filename || "");
|
|
4334
|
+
if (attached && depth < MAX_NESTED_DEPTH && import_node_buffer2.Buffer.isBuffer(content) && content.length <= budget) {
|
|
4335
|
+
budget -= content.length;
|
|
4336
|
+
let inner;
|
|
4337
|
+
try {
|
|
4338
|
+
inner = await parseWithin(content, parse, deadline);
|
|
4339
|
+
} catch (error) {
|
|
4340
|
+
errors.push(`${name || "attached message"}: ${error.message}`);
|
|
4341
|
+
continue;
|
|
4342
|
+
}
|
|
4343
|
+
if (inner.error) {
|
|
4344
|
+
errors.push(inner.error);
|
|
4345
|
+
}
|
|
4346
|
+
await visit(inner.mail.attachments, prefix ? `${prefix}/${name}` : name || "attached message", depth + 1);
|
|
4347
|
+
}
|
|
4348
|
+
}
|
|
4349
|
+
};
|
|
4350
|
+
await visit(attachments, "", 0);
|
|
4351
|
+
return all;
|
|
4352
|
+
}
|
|
4353
|
+
var import_node_buffer2, import_node_crypto, import_node_stream, import_mailparser, ADDRESS_FIELDS, MAX_ADDRESS_FIELD, MAX_ADDRESS_SEPARATORS, MAX_ADDRESS_GROUPS, MAX_ADDRESS_TOTAL, AUTH_ADDRESS_FIELDS, ADDRESS_OPERATORS, SINGLE_FIELDS, PRIORITY_FIELDS, BOUNDED2, ROOM, PARSER_OPTIONS, processHeaders, TOO_MANY_PARTS, HEADER_TOO_LARGE, MAX_PART_LINES, MAX_PREFIX_BYTES, MAX_LINE, MAX_FIELD, MAX_HEADER, MAX_REPEATED, KEPT_FIELDS, MAX_AUTH_WORK, SIGNATURE_FIELDS, AUTH_FIELDS, MAX_SIGNATURES, MAX_DKIM_SIGNATURES, MAX_NESTED_DEPTH, MAX_ATTACHMENTS, MAX_NESTED_BYTES, PARSE_TIMEOUT, MAX_FOLDS;
|
|
4354
|
+
var init_parse = __esm({
|
|
4355
|
+
"src/parse.js"() {
|
|
4356
|
+
import_node_buffer2 = require("node:buffer");
|
|
4357
|
+
import_node_crypto = require("node:crypto");
|
|
4358
|
+
import_node_stream = require("node:stream");
|
|
4359
|
+
import_mailparser = require("mailparser");
|
|
4360
|
+
ADDRESS_FIELDS = /* @__PURE__ */ new Set(["from", "to", "cc", "bcc", "sender", "reply-to", "delivered-to", "return-path", "disposition-notification-to"]);
|
|
4361
|
+
MAX_ADDRESS_FIELD = 16 * 1024;
|
|
4362
|
+
MAX_ADDRESS_SEPARATORS = 2e3;
|
|
4363
|
+
MAX_ADDRESS_GROUPS = 16;
|
|
4364
|
+
MAX_ADDRESS_TOTAL = 64 * 1024;
|
|
4365
|
+
AUTH_ADDRESS_FIELDS = /* @__PURE__ */ new Set(["from", "return-path"]);
|
|
4366
|
+
ADDRESS_OPERATORS = /* @__PURE__ */ new Map([['"', '"'], ["(", ")"], ["<", ">"], [",", ""], [":", ";"], [";", ""]]);
|
|
4367
|
+
SINGLE_FIELDS = /* @__PURE__ */ new Set(["message-id", "content-id", "from", "sender", "in-reply-to", "reply-to", "subject", "date", "content-disposition", "content-type", "content-transfer-encoding", "priority", "mime-version", "content-description", "precedence", "errors-to", "disposition-notification-to"]);
|
|
4368
|
+
PRIORITY_FIELDS = /* @__PURE__ */ new Set(["priority", "x-priority", "x-msmail-priority", "importance"]);
|
|
4369
|
+
BOUNDED2 = /* @__PURE__ */ Symbol("bounded address fields");
|
|
4370
|
+
ROOM = /* @__PURE__ */ Symbol("room left for address fields");
|
|
4371
|
+
PARSER_OPTIONS = {
|
|
4372
|
+
skipHtmlToText: true,
|
|
4373
|
+
skipImageLinks: true,
|
|
4374
|
+
skipTextToHtml: true,
|
|
4375
|
+
skipTextLinks: true,
|
|
4376
|
+
[BOUNDED2]: true
|
|
4377
|
+
};
|
|
4378
|
+
({ processHeaders } = import_mailparser.MailParser.prototype);
|
|
4379
|
+
import_mailparser.MailParser.prototype.processHeaders = function(lines2) {
|
|
4380
|
+
if (!this.options[BOUNDED2]) {
|
|
4381
|
+
return processHeaders.call(this, lines2);
|
|
4382
|
+
}
|
|
4383
|
+
this[ROOM] ??= { room: MAX_ADDRESS_TOTAL };
|
|
4384
|
+
const bounded = boundAddressLines(lines2, this[ROOM]);
|
|
4385
|
+
const headers = processHeaders.call(this, bounded.lines);
|
|
4386
|
+
const keyOf = (entry) => PRIORITY_FIELDS.has(entry.key) ? "priority" : entry.key;
|
|
4387
|
+
const counts = /* @__PURE__ */ new Map();
|
|
4388
|
+
for (const key of bounded.lines.map((entry) => keyOf(entry)).filter((key2) => SINGLE_FIELDS.has(key2))) {
|
|
4389
|
+
counts.set(key, (counts.get(key) || 0) + 1);
|
|
4390
|
+
}
|
|
4391
|
+
for (const key of [...counts.keys()].filter((key2) => counts.get(key2) > 1)) {
|
|
4392
|
+
const last = bounded.lines.findLast((entry) => keyOf(entry) === key && processHeaders.call(this, [entry]).has(key));
|
|
4393
|
+
if (last) {
|
|
4394
|
+
headers.set(key, processHeaders.call(this, [last]).get(key));
|
|
4395
|
+
}
|
|
4396
|
+
}
|
|
4397
|
+
for (const value of [...bounded.cut].flatMap((key) => headers.get(key) ?? [])) {
|
|
4398
|
+
value.truncated = true;
|
|
4399
|
+
}
|
|
4400
|
+
return headers;
|
|
4401
|
+
};
|
|
4402
|
+
TOO_MANY_PARTS = "Max allowed child nodes exceeded";
|
|
4403
|
+
HEADER_TOO_LARGE = "Max header size for a MIME node exceeded";
|
|
4404
|
+
MAX_PART_LINES = 990;
|
|
4405
|
+
MAX_PREFIX_BYTES = 1e6;
|
|
4406
|
+
MAX_LINE = 998;
|
|
4407
|
+
MAX_FIELD = 8192;
|
|
4408
|
+
MAX_HEADER = 512 * 1024;
|
|
4409
|
+
MAX_REPEATED = 100;
|
|
4410
|
+
KEPT_FIELDS = /* @__PURE__ */ new Set([
|
|
4411
|
+
"from",
|
|
4412
|
+
"sender",
|
|
4413
|
+
"reply-to",
|
|
4414
|
+
"to",
|
|
4415
|
+
"cc",
|
|
4416
|
+
"subject",
|
|
4417
|
+
"date",
|
|
4418
|
+
"message-id",
|
|
4419
|
+
"return-path",
|
|
4420
|
+
"received",
|
|
4421
|
+
"authentication-results",
|
|
4422
|
+
"arc-authentication-results",
|
|
4423
|
+
"dkim-signature",
|
|
4424
|
+
"mime-version",
|
|
4425
|
+
"content-type",
|
|
4426
|
+
"content-transfer-encoding",
|
|
4427
|
+
"list-unsubscribe",
|
|
4428
|
+
"list-id"
|
|
4429
|
+
]);
|
|
4430
|
+
MAX_AUTH_WORK = 1e7;
|
|
4431
|
+
SIGNATURE_FIELDS = /* @__PURE__ */ new Set(["dkim-signature", "arc-message-signature"]);
|
|
4432
|
+
AUTH_FIELDS = /* @__PURE__ */ new Set([...KEPT_FIELDS, "arc-seal", "arc-message-signature"]);
|
|
4433
|
+
MAX_SIGNATURES = 10;
|
|
4434
|
+
MAX_DKIM_SIGNATURES = 8;
|
|
4435
|
+
MAX_NESTED_DEPTH = 3;
|
|
4436
|
+
MAX_ATTACHMENTS = 100;
|
|
4437
|
+
MAX_NESTED_BYTES = 25 * 1024 * 1024;
|
|
4438
|
+
PARSE_TIMEOUT = 6e4;
|
|
4439
|
+
MAX_FOLDS = 256;
|
|
4440
|
+
}
|
|
4441
|
+
});
|
|
4442
|
+
|
|
3943
4443
|
// src/dnsbl.js
|
|
3944
4444
|
function isLookupName(name) {
|
|
3945
4445
|
if (typeof name !== "string") {
|
|
@@ -4193,7 +4693,7 @@ function requestJson(method, url, body, options = {}) {
|
|
|
4193
4693
|
const { headers = {}, timeout = 3e4, maxResponseBytes = 1048576, ca } = options;
|
|
4194
4694
|
const target = new URL(url);
|
|
4195
4695
|
const transport = target.protocol === "https:" ? import_node_https.default : import_node_http.default;
|
|
4196
|
-
const payload = body === null || body === void 0 ? null :
|
|
4696
|
+
const payload = body === null || body === void 0 ? null : import_node_buffer3.Buffer.from(JSON.stringify(body));
|
|
4197
4697
|
return new Promise((resolve, reject) => {
|
|
4198
4698
|
let settled = false;
|
|
4199
4699
|
const finish = (error, value) => {
|
|
@@ -4231,7 +4731,7 @@ function requestJson(method, url, body, options = {}) {
|
|
|
4231
4731
|
});
|
|
4232
4732
|
response.on("error", (error) => finish(error));
|
|
4233
4733
|
response.on("end", () => {
|
|
4234
|
-
const text =
|
|
4734
|
+
const text = import_node_buffer3.Buffer.concat(chunks).toString("utf8");
|
|
4235
4735
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
4236
4736
|
const error = new Error(`HTTP ${response.statusCode} from ${target.origin}${target.pathname}: ${text.slice(0, 300)}`);
|
|
4237
4737
|
error.statusCode = response.statusCode;
|
|
@@ -4253,10 +4753,10 @@ function requestJson(method, url, body, options = {}) {
|
|
|
4253
4753
|
request.end(payload ?? void 0);
|
|
4254
4754
|
});
|
|
4255
4755
|
}
|
|
4256
|
-
var
|
|
4756
|
+
var import_node_buffer3, import_node_http, import_node_https;
|
|
4257
4757
|
var init_http = __esm({
|
|
4258
4758
|
"src/http.js"() {
|
|
4259
|
-
|
|
4759
|
+
import_node_buffer3 = require("node:buffer");
|
|
4260
4760
|
import_node_http = __toESM(require("node:http"), 1);
|
|
4261
4761
|
import_node_https = __toESM(require("node:https"), 1);
|
|
4262
4762
|
}
|
|
@@ -4396,7 +4896,7 @@ function trustedReputationValues(mail = {}, session = {}, authentication = null)
|
|
|
4396
4896
|
...[spfAligned].filter((signer) => signer !== null && (alignment?.spf?.strict !== true || signer === domain))
|
|
4397
4897
|
])];
|
|
4398
4898
|
const fromValues = [];
|
|
4399
|
-
for (const { address: address2 } of mail.from?.value || []) {
|
|
4899
|
+
for (const { address: address2 } of mail.from?.truncated ? [] : mail.from?.value || []) {
|
|
4400
4900
|
const domain = domainOf(address2);
|
|
4401
4901
|
const values = reputationValues({ from: { value: [{ address: address2 }] } });
|
|
4402
4902
|
if (domain !== null && dmarcDomain !== null && (domain === dmarcDomain || registrableDomain(domain) === dmarcDomain)) {
|
|
@@ -4552,7 +5052,7 @@ var init_reputation = __esm({
|
|
|
4552
5052
|
|
|
4553
5053
|
// src/auth.js
|
|
4554
5054
|
async function getMailauth() {
|
|
4555
|
-
globalThis.File ||=
|
|
5055
|
+
globalThis.File ||= import_node_buffer4.File;
|
|
4556
5056
|
mailauth ||= await import("mailauth");
|
|
4557
5057
|
return mailauth;
|
|
4558
5058
|
}
|
|
@@ -4585,6 +5085,35 @@ function createResolver(timeout = 1e4, servers) {
|
|
|
4585
5085
|
}
|
|
4586
5086
|
};
|
|
4587
5087
|
}
|
|
5088
|
+
function dnsTimeout(message) {
|
|
5089
|
+
const error = new Error(message);
|
|
5090
|
+
error.code = "ETIMEOUT";
|
|
5091
|
+
return error;
|
|
5092
|
+
}
|
|
5093
|
+
function limitResolver(resolver, { maxQueries = MAX_DNS_QUERIES, deadline = Number.POSITIVE_INFINITY } = {}) {
|
|
5094
|
+
let count = 0;
|
|
5095
|
+
return async (...args) => {
|
|
5096
|
+
count++;
|
|
5097
|
+
if (count > maxQueries) {
|
|
5098
|
+
throw dnsTimeout(`More than ${maxQueries} DNS lookups for one message`);
|
|
5099
|
+
}
|
|
5100
|
+
const left = deadline - Date.now();
|
|
5101
|
+
if (left <= 0) {
|
|
5102
|
+
throw dnsTimeout("Out of time for this message's DNS lookups");
|
|
5103
|
+
}
|
|
5104
|
+
let timer;
|
|
5105
|
+
try {
|
|
5106
|
+
return await Promise.race([
|
|
5107
|
+
resolver(...args),
|
|
5108
|
+
new Promise((_resolve, reject) => {
|
|
5109
|
+
timer = setTimeout(() => reject(dnsTimeout("Out of time for this message's DNS lookups")), Math.min(left, 2147483647));
|
|
5110
|
+
})
|
|
5111
|
+
]);
|
|
5112
|
+
} finally {
|
|
5113
|
+
clearTimeout(timer);
|
|
5114
|
+
}
|
|
5115
|
+
};
|
|
5116
|
+
}
|
|
4588
5117
|
function emptyResult() {
|
|
4589
5118
|
return {
|
|
4590
5119
|
dkim: { status: { result: "none", comment: "not checked" }, results: [], aligned: null },
|
|
@@ -4722,9 +5251,14 @@ async function authenticate(message, options = {}) {
|
|
|
4722
5251
|
}
|
|
4723
5252
|
try {
|
|
4724
5253
|
const { authenticate: run } = await getMailauth();
|
|
4725
|
-
const resolver = options.resolver || createResolver(timeout, options.dnsServers)
|
|
4726
|
-
|
|
4727
|
-
|
|
5254
|
+
const resolver = limitResolver(options.resolver || createResolver(timeout, options.dnsServers), {
|
|
5255
|
+
maxQueries: options.maxDnsQueries ?? MAX_DNS_QUERIES,
|
|
5256
|
+
// (a little after `timeout`, so a caller that waits that long for the
|
|
5257
|
+
// result, as scan() does, has its answer first)
|
|
5258
|
+
deadline: Date.now() + timeout + 100
|
|
5259
|
+
});
|
|
5260
|
+
const bounded = boundedForAuth(import_node_buffer4.Buffer.isBuffer(message) ? message : import_node_buffer4.Buffer.from(String(message)));
|
|
5261
|
+
const output = await run(bounded.raw, {
|
|
4728
5262
|
ip,
|
|
4729
5263
|
helo: helo || void 0,
|
|
4730
5264
|
sender: sender ?? void 0,
|
|
@@ -4732,6 +5266,10 @@ async function authenticate(message, options = {}) {
|
|
|
4732
5266
|
resolver
|
|
4733
5267
|
});
|
|
4734
5268
|
Object.assign(result, normalizeAuthOutput(output, { trustedArcSealers: options.trustedArcSealers }));
|
|
5269
|
+
if (bounded.fromCut) {
|
|
5270
|
+
result.dmarc = { ...result.dmarc, status: { result: "fail", comment: "From field too long to read whole" } };
|
|
5271
|
+
result.headers = "";
|
|
5272
|
+
}
|
|
4735
5273
|
} catch (error) {
|
|
4736
5274
|
debug("authentication failed: %s", error.message);
|
|
4737
5275
|
result.error = error.message;
|
|
@@ -4766,11 +5304,24 @@ function calculateAuthScore(auth, weights = {}) {
|
|
|
4766
5304
|
}
|
|
4767
5305
|
return { score: tests.reduce((sum, test) => sum + test.score, 0), tests };
|
|
4768
5306
|
}
|
|
5307
|
+
function limitResults(value) {
|
|
5308
|
+
if (value.length <= MAX_AUTH_RESULTS_LENGTH) {
|
|
5309
|
+
return value;
|
|
5310
|
+
}
|
|
5311
|
+
const [server, ...results] = value.split(/;(?=\s*(?:dkim|spf|dmarc|arc|bimi)=)/i);
|
|
5312
|
+
let kept = server;
|
|
5313
|
+
for (const result of results) {
|
|
5314
|
+
if (kept.length + 1 + result.length <= MAX_AUTH_RESULTS_LENGTH) {
|
|
5315
|
+
kept += `;${result}`;
|
|
5316
|
+
}
|
|
5317
|
+
}
|
|
5318
|
+
return kept;
|
|
5319
|
+
}
|
|
4769
5320
|
function formatAuthResultsHeader(auth, hostname) {
|
|
4770
5321
|
if (typeof auth?.headers === "string") {
|
|
4771
5322
|
const match = auth.headers.match(/^authentication-results:[ \t]*([\s\S]*?)(?=\r?\n\S|(?![\s\S]))/im);
|
|
4772
5323
|
if (match) {
|
|
4773
|
-
const value = match[1].replaceAll(/\r?\n/g, "\r\n").trim();
|
|
5324
|
+
const value = limitResults(match[1].replaceAll(/\r?\n/g, "\r\n").trim());
|
|
4774
5325
|
return hostname ? value.replace(/^[^;\s]+/, hostname) : value;
|
|
4775
5326
|
}
|
|
4776
5327
|
}
|
|
@@ -4783,16 +5334,20 @@ function formatAuthResultsHeader(auth, hostname) {
|
|
|
4783
5334
|
function summarizeAuth(auth) {
|
|
4784
5335
|
return ["spf", "dkim", "dmarc", "arc"].map((name) => `${name}=${auth?.[name]?.status?.result || "none"}`).join(" ");
|
|
4785
5336
|
}
|
|
4786
|
-
var
|
|
5337
|
+
var import_node_buffer4, import_node_dns2, import_node_util, debug, mailauth, RECORD_TYPES, MAX_DNS_QUERIES, MAX_AUTH_RESULTS_LENGTH, tagValue, tagText, DEFAULT_AUTH_WEIGHTS;
|
|
4787
5338
|
var init_auth = __esm({
|
|
4788
5339
|
"src/auth.js"() {
|
|
4789
|
-
|
|
5340
|
+
import_node_buffer4 = require("node:buffer");
|
|
4790
5341
|
import_node_dns2 = __toESM(require("node:dns"), 1);
|
|
4791
5342
|
import_node_util = require("node:util");
|
|
5343
|
+
init_parse();
|
|
4792
5344
|
init_reputation();
|
|
4793
5345
|
init_tokenizer();
|
|
5346
|
+
init_parse();
|
|
4794
5347
|
debug = (0, import_node_util.debuglog)("spamscanner:auth");
|
|
4795
5348
|
RECORD_TYPES = /* @__PURE__ */ new Set(["A", "AAAA", "MX", "TXT", "PTR", "CNAME", "NS", "SOA", "SRV"]);
|
|
5349
|
+
MAX_DNS_QUERIES = 300;
|
|
5350
|
+
MAX_AUTH_RESULTS_LENGTH = 8192;
|
|
4796
5351
|
tagValue = (value) => value && typeof value === "object" ? value.value : value;
|
|
4797
5352
|
tagText = (value) => typeof value === "string" && value.trim() !== "" ? value.trim().toLowerCase() : null;
|
|
4798
5353
|
DEFAULT_AUTH_WEIGHTS = {
|
|
@@ -4838,10 +5393,10 @@ function exchange(address2, write, timeout) {
|
|
|
4838
5393
|
resolve(value);
|
|
4839
5394
|
}
|
|
4840
5395
|
};
|
|
4841
|
-
const timer = setTimeout(() => finish(new Error(`clamd did not answer within ${timeout} ms`)), timeout);
|
|
5396
|
+
const timer = setTimeout(() => finish(Object.assign(new Error(`clamd did not answer within ${timeout} ms`), { code: "ETIMEDOUT", timedOut: true })), timeout);
|
|
4842
5397
|
socket.on("connect", () => write(socket));
|
|
4843
5398
|
socket.on("data", (chunk2) => chunks.push(chunk2));
|
|
4844
|
-
socket.on("end", () => finish(null,
|
|
5399
|
+
socket.on("end", () => finish(null, import_node_buffer5.Buffer.concat(chunks).toString("utf8").replaceAll("\0", "").trim()));
|
|
4845
5400
|
socket.on("error", (error) => finish(error));
|
|
4846
5401
|
});
|
|
4847
5402
|
}
|
|
@@ -4855,12 +5410,12 @@ async function scanBuffer(buffer, options = {}) {
|
|
|
4855
5410
|
socket.write("zINSTREAM\0");
|
|
4856
5411
|
for (let offset = 0; offset < buffer.length; offset += chunkSize) {
|
|
4857
5412
|
const chunk2 = buffer.subarray(offset, offset + chunkSize);
|
|
4858
|
-
const size =
|
|
5413
|
+
const size = import_node_buffer5.Buffer.alloc(4);
|
|
4859
5414
|
size.writeUInt32BE(chunk2.length);
|
|
4860
5415
|
socket.write(size);
|
|
4861
5416
|
socket.write(chunk2);
|
|
4862
5417
|
}
|
|
4863
|
-
socket.end(
|
|
5418
|
+
socket.end(import_node_buffer5.Buffer.alloc(4));
|
|
4864
5419
|
}, options.timeout ?? 3e4);
|
|
4865
5420
|
if (/\bOK$/.test(reply2)) {
|
|
4866
5421
|
return { infected: false, viruses: [], reply: reply2 };
|
|
@@ -4871,10 +5426,10 @@ async function scanBuffer(buffer, options = {}) {
|
|
|
4871
5426
|
}
|
|
4872
5427
|
throw new Error(`clamd: ${reply2 || "no reply"}`);
|
|
4873
5428
|
}
|
|
4874
|
-
var
|
|
5429
|
+
var import_node_buffer5, import_node_fs, import_node_net3, SOCKET_PATHS;
|
|
4875
5430
|
var init_clamav = __esm({
|
|
4876
5431
|
"src/clamav.js"() {
|
|
4877
|
-
|
|
5432
|
+
import_node_buffer5 = require("node:buffer");
|
|
4878
5433
|
import_node_fs = require("node:fs");
|
|
4879
5434
|
import_node_net3 = __toESM(require("node:net"), 1);
|
|
4880
5435
|
SOCKET_PATHS = ["/var/run/clamav/clamd.ctl", "/run/clamav/clamd.ctl", "/run/clamd.scan/clamd.sock", "/var/run/clamd.scan/clamd.sock", "/var/run/clamav/clamd.sock", "/tmp/clamd.socket", "/opt/homebrew/var/run/clamav/clamd.sock", "/usr/local/var/run/clamav/clamd.sock"];
|
|
@@ -4884,7 +5439,7 @@ var init_clamav = __esm({
|
|
|
4884
5439
|
// src/classifier.js
|
|
4885
5440
|
function hashFeature(value) {
|
|
4886
5441
|
let hash = 2166136261;
|
|
4887
|
-
const bytes =
|
|
5442
|
+
const bytes = import_node_buffer6.Buffer.from(value, "utf8");
|
|
4888
5443
|
for (const byte of bytes) {
|
|
4889
5444
|
hash ^= byte;
|
|
4890
5445
|
hash = Math.imul(hash, 16777619);
|
|
@@ -4920,10 +5475,10 @@ function modelOptions(options, corrupt) {
|
|
|
4920
5475
|
}
|
|
4921
5476
|
return checked;
|
|
4922
5477
|
}
|
|
4923
|
-
var
|
|
5478
|
+
var import_node_buffer6, MODEL_VERSION, DEFAULTS, OPTION_CHECKS, NULL_MEANS, isWord, IDENTITY, Classifier;
|
|
4924
5479
|
var init_classifier = __esm({
|
|
4925
5480
|
"src/classifier.js"() {
|
|
4926
|
-
|
|
5481
|
+
import_node_buffer6 = require("node:buffer");
|
|
4927
5482
|
MODEL_VERSION = 2;
|
|
4928
5483
|
DEFAULTS = {
|
|
4929
5484
|
// Robinson's s and x: how much an unseen or rare feature is pulled to 0.5.
|
|
@@ -5215,7 +5770,7 @@ var init_classifier = __esm({
|
|
|
5215
5770
|
spam[i] = s;
|
|
5216
5771
|
ham[i] = h;
|
|
5217
5772
|
}
|
|
5218
|
-
const encode = (array) =>
|
|
5773
|
+
const encode = (array) => import_node_buffer6.Buffer.from(array.buffer, array.byteOffset, array.byteLength).toString("base64");
|
|
5219
5774
|
return {
|
|
5220
5775
|
type: "spamscanner-classifier",
|
|
5221
5776
|
version: MODEL_VERSION,
|
|
@@ -5266,7 +5821,7 @@ var init_classifier = __esm({
|
|
|
5266
5821
|
throw new TypeError(`Unsupported classifier model version ${data2.version}; this release reads version ${MODEL_VERSION}`);
|
|
5267
5822
|
}
|
|
5268
5823
|
const decode = (value, name) => {
|
|
5269
|
-
const buffer = typeof value === "string" ?
|
|
5824
|
+
const buffer = typeof value === "string" ? import_node_buffer6.Buffer.from(value, "base64") : null;
|
|
5270
5825
|
if (!buffer || buffer.byteLength % 4 !== 0) {
|
|
5271
5826
|
throw corrupt(`${name} is not a list of 32-bit numbers`);
|
|
5272
5827
|
}
|
|
@@ -5757,7 +6312,7 @@ function authHeaders(config) {
|
|
|
5757
6312
|
case "basic": {
|
|
5758
6313
|
const user = config.username ?? "";
|
|
5759
6314
|
const pass = config.password ?? config.apiKey ?? "";
|
|
5760
|
-
headers.authorization = `Basic ${
|
|
6315
|
+
headers.authorization = `Basic ${import_node_buffer7.Buffer.from(`${user}:${pass}`).toString("base64")}`;
|
|
5761
6316
|
break;
|
|
5762
6317
|
}
|
|
5763
6318
|
case "x-api-key": {
|
|
@@ -6108,7 +6663,7 @@ function endpoint(config, suffix) {
|
|
|
6108
6663
|
return url.href;
|
|
6109
6664
|
}
|
|
6110
6665
|
function buildRequest(config, text) {
|
|
6111
|
-
const nonce = (0,
|
|
6666
|
+
const nonce = (0, import_node_crypto2.randomBytes)(6).toString("hex");
|
|
6112
6667
|
const policy = config.policy ? `
|
|
6113
6668
|
|
|
6114
6669
|
Additional policy from the mail server's operator:
|
|
@@ -6204,11 +6759,11 @@ function replyText(api, reply2) {
|
|
|
6204
6759
|
}
|
|
6205
6760
|
return reply2?.choices?.[0]?.message?.content || null;
|
|
6206
6761
|
}
|
|
6207
|
-
var
|
|
6762
|
+
var import_node_buffer7, import_node_crypto2, import_node_os, import_node_process, PROVIDERS, DECISION_APIS, TOP_LOGPROBS, VERDICTS, VERDICT_CRITERIA, SIGNS, GUIDE, DEFAULT_SYSTEM_PROMPT, DECISION_SYSTEM_PROMPT, DECISION_INSTRUCTIONS, LOCALHOST, PRIVATE_HOST, REDACTIONS, THINK_OPEN, THINK_CLOSE, ANSWER_FIRST, LOCAL_CHARACTER, BREAKS2, VERDICT_WORD2, NOT, DENIED, JOINED, SPAM_LABEL, HAM_WORDS, HAM_LABEL, NOT_HAM_LABEL, UNWANTED, MIN_VERDICT_MASS, percent, MODES, LLMClassifier;
|
|
6208
6763
|
var init_llm = __esm({
|
|
6209
6764
|
"src/llm.js"() {
|
|
6210
|
-
|
|
6211
|
-
|
|
6765
|
+
import_node_buffer7 = require("node:buffer");
|
|
6766
|
+
import_node_crypto2 = require("node:crypto");
|
|
6212
6767
|
import_node_os = __toESM(require("node:os"), 1);
|
|
6213
6768
|
import_node_process = __toESM(require("node:process"), 1);
|
|
6214
6769
|
init_http();
|
|
@@ -6576,7 +7131,7 @@ Answer with exactly one word: ham, spam, phishing, scam or malware.`;
|
|
|
6576
7131
|
* @returns {Promise<{verdict: string, confidence: number, language: string|null, reasons: string[], provider: string, model: string, cached: boolean, time: number}>}
|
|
6577
7132
|
*/
|
|
6578
7133
|
async classifyText(text) {
|
|
6579
|
-
const key = (0,
|
|
7134
|
+
const key = (0, import_node_crypto2.createHash)("sha256").update(text).digest("hex");
|
|
6580
7135
|
if (this.cache.has(key)) {
|
|
6581
7136
|
const hit = this.cache.get(key);
|
|
6582
7137
|
this.cache.delete(key);
|
|
@@ -6700,10 +7255,20 @@ function loadModel(file, options = {}) {
|
|
|
6700
7255
|
function saveModel(classifier, file, options = {}) {
|
|
6701
7256
|
const resolved = import_node_path.default.resolve(file);
|
|
6702
7257
|
(0, import_node_fs2.mkdirSync)(import_node_path.default.dirname(resolved), { recursive: true });
|
|
6703
|
-
const temporary = `${resolved}.${import_node_process2.default.pid}.${(0,
|
|
7258
|
+
const temporary = `${resolved}.${import_node_process2.default.pid}.${(0, import_node_crypto3.randomBytes)(6).toString("hex")}.tmp`;
|
|
7259
|
+
const mode = (0, import_node_fs2.statSync)(resolved, { throwIfNoEntry: false })?.mode;
|
|
7260
|
+
const fd = (0, import_node_fs2.openSync)(temporary, "wx");
|
|
6704
7261
|
try {
|
|
6705
|
-
|
|
7262
|
+
try {
|
|
7263
|
+
if (mode !== void 0) {
|
|
7264
|
+
(0, import_node_fs2.fchmodSync)(fd, mode % 512);
|
|
7265
|
+
}
|
|
7266
|
+
(0, import_node_fs2.writeFileSync)(fd, `${JSON.stringify(classifier.toJSON(options))}
|
|
6706
7267
|
`);
|
|
7268
|
+
(0, import_node_fs2.fsyncSync)(fd);
|
|
7269
|
+
} finally {
|
|
7270
|
+
(0, import_node_fs2.closeSync)(fd);
|
|
7271
|
+
}
|
|
6707
7272
|
(0, import_node_fs2.renameSync)(temporary, resolved);
|
|
6708
7273
|
} catch (error) {
|
|
6709
7274
|
(0, import_node_fs2.rmSync)(temporary, { force: true });
|
|
@@ -6711,11 +7276,11 @@ function saveModel(classifier, file, options = {}) {
|
|
|
6711
7276
|
}
|
|
6712
7277
|
loaded.delete(resolved);
|
|
6713
7278
|
}
|
|
6714
|
-
var import_node_fs2,
|
|
7279
|
+
var import_node_fs2, import_node_crypto3, import_node_module2, import_node_path, import_node_process2, import_node_url3, loaded;
|
|
6715
7280
|
var init_model = __esm({
|
|
6716
7281
|
"src/model.js"() {
|
|
6717
7282
|
import_node_fs2 = require("node:fs");
|
|
6718
|
-
|
|
7283
|
+
import_node_crypto3 = require("node:crypto");
|
|
6719
7284
|
import_node_module2 = require("node:module");
|
|
6720
7285
|
import_node_path = __toESM(require("node:path"), 1);
|
|
6721
7286
|
import_node_process2 = __toESM(require("node:process"), 1);
|
|
@@ -6725,180 +7290,6 @@ var init_model = __esm({
|
|
|
6725
7290
|
}
|
|
6726
7291
|
});
|
|
6727
7292
|
|
|
6728
|
-
// src/parse.js
|
|
6729
|
-
async function parseMessage(raw) {
|
|
6730
|
-
const buffer = import_node_buffer7.Buffer.isBuffer(raw) ? raw : import_node_buffer7.Buffer.from(raw);
|
|
6731
|
-
const mail = await (0, import_mailparser.simpleParser)(finishable(buffer), PARSER_OPTIONS);
|
|
6732
|
-
const added = buffer.at(-1) === 10 ? 2 : 3;
|
|
6733
|
-
for (const key of ["text", "html"]) {
|
|
6734
|
-
if (typeof mail[key] === "string") {
|
|
6735
|
-
mail[key] = mail[key].replace(new RegExp(`\\n{0,${added}}$`), "");
|
|
6736
|
-
}
|
|
6737
|
-
}
|
|
6738
|
-
const tail = import_node_buffer7.Buffer.from("\r\n".repeat(added));
|
|
6739
|
-
const last = mail.attachments?.at(-1);
|
|
6740
|
-
const encoding = String(last?.headers?.get?.("content-transfer-encoding") ?? "7bit").trim().toLowerCase();
|
|
6741
|
-
const own3 = import_node_buffer7.Buffer.isBuffer(last?.content) && last.content.length >= tail.length && last.content.subarray(-tail.length).equals(tail) ? last.content.subarray(0, -tail.length) : null;
|
|
6742
|
-
if (own3 && ["7bit", "8bit", "binary"].includes(encoding) && own3.length <= buffer.length && buffer.subarray(buffer.length - own3.length).equals(own3)) {
|
|
6743
|
-
last.content = own3;
|
|
6744
|
-
last.size = own3.length;
|
|
6745
|
-
last.checksum = (0, import_node_crypto3.createHash)("md5").update(own3).digest("hex");
|
|
6746
|
-
}
|
|
6747
|
-
return mail;
|
|
6748
|
-
}
|
|
6749
|
-
function finishable(raw) {
|
|
6750
|
-
const buffer = import_node_buffer7.Buffer.isBuffer(raw) ? raw : import_node_buffer7.Buffer.from(raw);
|
|
6751
|
-
return import_node_buffer7.Buffer.concat([buffer, import_node_buffer7.Buffer.from(buffer.at(-1) === 10 ? "\r\n\r\n" : "\r\n\r\n\r\n")]);
|
|
6752
|
-
}
|
|
6753
|
-
function upToLine(raw, offset) {
|
|
6754
|
-
return raw.subarray(0, Math.max(raw.lastIndexOf(10, offset - 1) + 1, 0));
|
|
6755
|
-
}
|
|
6756
|
-
function fewerParts(raw) {
|
|
6757
|
-
let offset = -1;
|
|
6758
|
-
for (let count = 0; count < MAX_PART_LINES; count++) {
|
|
6759
|
-
offset = raw.indexOf("\n--", offset + 1);
|
|
6760
|
-
if (offset === -1) {
|
|
6761
|
-
return upToLine(raw, Math.floor(raw.length / 2));
|
|
6762
|
-
}
|
|
6763
|
-
}
|
|
6764
|
-
return raw.subarray(0, offset + 1);
|
|
6765
|
-
}
|
|
6766
|
-
function smallerHeader(raw) {
|
|
6767
|
-
const text = raw.toString("latin1");
|
|
6768
|
-
const blank = /\r?\n\r?\n/.exec(text);
|
|
6769
|
-
const end = blank ? blank.index : text.length;
|
|
6770
|
-
const fields = [];
|
|
6771
|
-
let start = 0;
|
|
6772
|
-
while (start < end) {
|
|
6773
|
-
let stop = text.indexOf("\n", start);
|
|
6774
|
-
stop = stop === -1 || stop > end ? end : stop;
|
|
6775
|
-
const line = text.slice(start, Math.min(stop, start + MAX_LINE)).replace(/\r$/, "");
|
|
6776
|
-
const last = fields.at(-1);
|
|
6777
|
-
if (last && (line.startsWith(" ") || line.startsWith(" "))) {
|
|
6778
|
-
if (last.size + line.length <= MAX_FIELD) {
|
|
6779
|
-
last.lines.push(line);
|
|
6780
|
-
last.size += line.length;
|
|
6781
|
-
}
|
|
6782
|
-
} else {
|
|
6783
|
-
fields.push({ name: line.slice(0, line.indexOf(":")).trim().toLowerCase(), lines: [line], size: line.length });
|
|
6784
|
-
}
|
|
6785
|
-
start = stop + 1;
|
|
6786
|
-
}
|
|
6787
|
-
const total = fields.reduce((sum, field) => sum + field.size, 0);
|
|
6788
|
-
const counts = /* @__PURE__ */ new Map();
|
|
6789
|
-
const kept = total > MAX_HEADER ? fields.filter((field) => {
|
|
6790
|
-
counts.set(field.name, (counts.get(field.name) || 0) + 1);
|
|
6791
|
-
return KEPT_FIELDS.has(field.name) && counts.get(field.name) <= MAX_REPEATED;
|
|
6792
|
-
}) : fields;
|
|
6793
|
-
const header = kept.flatMap((field) => field.lines).join("\r\n");
|
|
6794
|
-
return import_node_buffer7.Buffer.concat([import_node_buffer7.Buffer.from(`${header}\r
|
|
6795
|
-
\r
|
|
6796
|
-
`, "latin1"), raw.subarray(blank ? blank.index + blank[0].length : raw.length)]);
|
|
6797
|
-
}
|
|
6798
|
-
async function parseWithin(raw, parse = parseMessage) {
|
|
6799
|
-
let input = raw;
|
|
6800
|
-
let reason = null;
|
|
6801
|
-
for (; ; ) {
|
|
6802
|
-
try {
|
|
6803
|
-
return { mail: await parse(input), error: reason };
|
|
6804
|
-
} catch (error) {
|
|
6805
|
-
const limit = error.code === "EMAXLEN" && [TOO_MANY_PARTS, HEADER_TOO_LARGE].includes(error.message) ? error.message : null;
|
|
6806
|
-
let shorter = input;
|
|
6807
|
-
if (limit === TOO_MANY_PARTS) {
|
|
6808
|
-
shorter = fewerParts(input);
|
|
6809
|
-
} else if (limit === HEADER_TOO_LARGE) {
|
|
6810
|
-
shorter = smallerHeader(input);
|
|
6811
|
-
shorter = shorter.length < input.length ? shorter : upToLine(input, MAX_PREFIX_BYTES);
|
|
6812
|
-
}
|
|
6813
|
-
if (shorter.length >= input.length) {
|
|
6814
|
-
throw error;
|
|
6815
|
-
}
|
|
6816
|
-
reason ??= limit;
|
|
6817
|
-
input = shorter;
|
|
6818
|
-
}
|
|
6819
|
-
}
|
|
6820
|
-
}
|
|
6821
|
-
async function expandAttachments(attachments = [], errors = [], parse = parseMessage) {
|
|
6822
|
-
const all = [];
|
|
6823
|
-
let budget = MAX_NESTED_BYTES;
|
|
6824
|
-
let nested = 0;
|
|
6825
|
-
const visit = async (list, prefix, depth) => {
|
|
6826
|
-
for (const attachment of list) {
|
|
6827
|
-
if (prefix) {
|
|
6828
|
-
if (nested >= MAX_ATTACHMENTS) {
|
|
6829
|
-
return;
|
|
6830
|
-
}
|
|
6831
|
-
nested++;
|
|
6832
|
-
}
|
|
6833
|
-
const name = attachment.filename || (prefix ? "unnamed attachment" : "");
|
|
6834
|
-
all.push(prefix ? { ...attachment, filename: `${prefix}/${name}` } : attachment);
|
|
6835
|
-
const { content } = attachment;
|
|
6836
|
-
const attached = attachment.contentType === "message/rfc822" || /\.eml$/i.test(attachment.filename || "");
|
|
6837
|
-
if (attached && depth < MAX_NESTED_DEPTH && import_node_buffer7.Buffer.isBuffer(content) && content.length <= budget) {
|
|
6838
|
-
budget -= content.length;
|
|
6839
|
-
let inner;
|
|
6840
|
-
try {
|
|
6841
|
-
inner = await parseWithin(content, parse);
|
|
6842
|
-
} catch (error) {
|
|
6843
|
-
errors.push(`${name || "attached message"}: ${error.message}`);
|
|
6844
|
-
continue;
|
|
6845
|
-
}
|
|
6846
|
-
if (inner.error) {
|
|
6847
|
-
errors.push(inner.error);
|
|
6848
|
-
}
|
|
6849
|
-
await visit(inner.mail.attachments, prefix ? `${prefix}/${name}` : name || "attached message", depth + 1);
|
|
6850
|
-
}
|
|
6851
|
-
}
|
|
6852
|
-
};
|
|
6853
|
-
await visit(attachments, "", 0);
|
|
6854
|
-
return all;
|
|
6855
|
-
}
|
|
6856
|
-
var import_node_buffer7, import_node_crypto3, import_mailparser, PARSER_OPTIONS, TOO_MANY_PARTS, HEADER_TOO_LARGE, MAX_PART_LINES, MAX_PREFIX_BYTES, MAX_LINE, MAX_FIELD, MAX_HEADER, MAX_REPEATED, KEPT_FIELDS, MAX_NESTED_DEPTH, MAX_ATTACHMENTS, MAX_NESTED_BYTES;
|
|
6857
|
-
var init_parse = __esm({
|
|
6858
|
-
"src/parse.js"() {
|
|
6859
|
-
import_node_buffer7 = require("node:buffer");
|
|
6860
|
-
import_node_crypto3 = require("node:crypto");
|
|
6861
|
-
import_mailparser = require("mailparser");
|
|
6862
|
-
PARSER_OPTIONS = {
|
|
6863
|
-
skipHtmlToText: true,
|
|
6864
|
-
skipImageLinks: true,
|
|
6865
|
-
skipTextToHtml: true,
|
|
6866
|
-
skipTextLinks: true
|
|
6867
|
-
};
|
|
6868
|
-
TOO_MANY_PARTS = "Max allowed child nodes exceeded";
|
|
6869
|
-
HEADER_TOO_LARGE = "Max header size for a MIME node exceeded";
|
|
6870
|
-
MAX_PART_LINES = 990;
|
|
6871
|
-
MAX_PREFIX_BYTES = 1e6;
|
|
6872
|
-
MAX_LINE = 998;
|
|
6873
|
-
MAX_FIELD = 8192;
|
|
6874
|
-
MAX_HEADER = 512 * 1024;
|
|
6875
|
-
MAX_REPEATED = 100;
|
|
6876
|
-
KEPT_FIELDS = /* @__PURE__ */ new Set([
|
|
6877
|
-
"from",
|
|
6878
|
-
"sender",
|
|
6879
|
-
"reply-to",
|
|
6880
|
-
"to",
|
|
6881
|
-
"cc",
|
|
6882
|
-
"subject",
|
|
6883
|
-
"date",
|
|
6884
|
-
"message-id",
|
|
6885
|
-
"return-path",
|
|
6886
|
-
"received",
|
|
6887
|
-
"authentication-results",
|
|
6888
|
-
"arc-authentication-results",
|
|
6889
|
-
"dkim-signature",
|
|
6890
|
-
"mime-version",
|
|
6891
|
-
"content-type",
|
|
6892
|
-
"content-transfer-encoding",
|
|
6893
|
-
"list-unsubscribe",
|
|
6894
|
-
"list-id"
|
|
6895
|
-
]);
|
|
6896
|
-
MAX_NESTED_DEPTH = 3;
|
|
6897
|
-
MAX_ATTACHMENTS = 100;
|
|
6898
|
-
MAX_NESTED_BYTES = 25 * 1024 * 1024;
|
|
6899
|
-
}
|
|
6900
|
-
});
|
|
6901
|
-
|
|
6902
7293
|
// src/score.js
|
|
6903
7294
|
function bayesTestName(probability) {
|
|
6904
7295
|
const steps = [[0.999, "999"], [0.99, "99"], [0.95, "95"], [0.8, "80"], [0.6, "60"], [0.4, "50"], [0.2, "40"], [0.05, "20"], [0.01, "05"]];
|
|
@@ -7123,7 +7514,7 @@ var init_package = __esm({
|
|
|
7123
7514
|
package_default = {
|
|
7124
7515
|
name: "spamscanner",
|
|
7125
7516
|
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.",
|
|
7126
|
-
version: "7.0.
|
|
7517
|
+
version: "7.0.5",
|
|
7127
7518
|
author: "Forward Email (https://forwardemail.net)",
|
|
7128
7519
|
bin: {
|
|
7129
7520
|
spamscanner: "./dist/esm/cli.js"
|
|
@@ -7751,13 +8142,207 @@ var init_train = __esm({
|
|
|
7751
8142
|
}
|
|
7752
8143
|
});
|
|
7753
8144
|
|
|
8145
|
+
// src/server.js
|
|
8146
|
+
function serializeResult(result, options = {}) {
|
|
8147
|
+
const { mail, tokens, ...rest } = result;
|
|
8148
|
+
const output = {
|
|
8149
|
+
...rest,
|
|
8150
|
+
results: { ...result.results }
|
|
8151
|
+
};
|
|
8152
|
+
if (output.results.authentication) {
|
|
8153
|
+
const { receivedChain, ...auth } = output.results.authentication;
|
|
8154
|
+
output.results.authentication = auth;
|
|
8155
|
+
}
|
|
8156
|
+
if (options.verbose) {
|
|
8157
|
+
output.tokens = tokens;
|
|
8158
|
+
output.subject = mail?.subject ?? null;
|
|
8159
|
+
}
|
|
8160
|
+
return output;
|
|
8161
|
+
}
|
|
8162
|
+
function sessionFromQuery(query) {
|
|
8163
|
+
const session = {};
|
|
8164
|
+
if (query.get("ip")) {
|
|
8165
|
+
session.remoteAddress = query.get("ip");
|
|
8166
|
+
}
|
|
8167
|
+
if (query.get("hostname")) {
|
|
8168
|
+
session.resolvedClientHostname = query.get("hostname");
|
|
8169
|
+
}
|
|
8170
|
+
if (query.get("helo")) {
|
|
8171
|
+
session.helo = query.get("helo");
|
|
8172
|
+
}
|
|
8173
|
+
const recipients = query.getAll("to").flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean);
|
|
8174
|
+
if (query.has("from") || recipients.length > 0) {
|
|
8175
|
+
session.envelope = { mailFrom: { address: query.get("from") || "" }, rcptTo: recipients.map((address2) => ({ address: address2 })) };
|
|
8176
|
+
}
|
|
8177
|
+
return session;
|
|
8178
|
+
}
|
|
8179
|
+
function sameToken(given, expected) {
|
|
8180
|
+
const a = import_node_buffer10.Buffer.from(String(given));
|
|
8181
|
+
const b = import_node_buffer10.Buffer.from(String(expected));
|
|
8182
|
+
return a.length === b.length && (0, import_node_crypto5.timingSafeEqual)(a, b);
|
|
8183
|
+
}
|
|
8184
|
+
function readBody(request, limit) {
|
|
8185
|
+
return new Promise((resolve, reject) => {
|
|
8186
|
+
const chunks = [];
|
|
8187
|
+
let size = 0;
|
|
8188
|
+
request.on("data", (chunk2) => {
|
|
8189
|
+
size += chunk2.length;
|
|
8190
|
+
if (size <= limit) {
|
|
8191
|
+
chunks.push(chunk2);
|
|
8192
|
+
}
|
|
8193
|
+
});
|
|
8194
|
+
request.on("end", () => {
|
|
8195
|
+
if (size > limit) {
|
|
8196
|
+
const error = new Error(`Message larger than ${limit} bytes`);
|
|
8197
|
+
error.statusCode = 413;
|
|
8198
|
+
reject(error);
|
|
8199
|
+
return;
|
|
8200
|
+
}
|
|
8201
|
+
resolve(import_node_buffer10.Buffer.concat(chunks));
|
|
8202
|
+
});
|
|
8203
|
+
request.on("error", reject);
|
|
8204
|
+
});
|
|
8205
|
+
}
|
|
8206
|
+
function createHttpServer(scanner, options = {}) {
|
|
8207
|
+
const {
|
|
8208
|
+
token = null,
|
|
8209
|
+
maxSize = MAX_MESSAGE_SIZE,
|
|
8210
|
+
modelPath = null,
|
|
8211
|
+
maxConnections = MAX_CONNECTIONS
|
|
8212
|
+
} = options;
|
|
8213
|
+
let active = 0;
|
|
8214
|
+
const server = import_node_http2.default.createServer(async (request, response) => {
|
|
8215
|
+
const send = (status, body, headers = {}) => {
|
|
8216
|
+
const isBuffer = import_node_buffer10.Buffer.isBuffer(body);
|
|
8217
|
+
response.writeHead(status, { "content-type": isBuffer ? "message/rfc822" : "application/json; charset=utf-8", "cache-control": "no-store", ...headers });
|
|
8218
|
+
response.end(isBuffer ? body : `${JSON.stringify(body)}
|
|
8219
|
+
`);
|
|
8220
|
+
};
|
|
8221
|
+
try {
|
|
8222
|
+
const url = new URL(request.url, "http://localhost");
|
|
8223
|
+
if (request.method === "GET" && url.pathname === "/health") {
|
|
8224
|
+
send(200, { ok: true, version: VERSION });
|
|
8225
|
+
return;
|
|
8226
|
+
}
|
|
8227
|
+
if (token && !sameToken((request.headers.authorization || "").replace(/^bearer\s+/i, ""), token)) {
|
|
8228
|
+
send(401, { error: "Unauthorized" }, { "www-authenticate": "Bearer" });
|
|
8229
|
+
return;
|
|
8230
|
+
}
|
|
8231
|
+
if (request.method !== "POST") {
|
|
8232
|
+
send(405, { error: "Method not allowed" }, { allow: "POST" });
|
|
8233
|
+
return;
|
|
8234
|
+
}
|
|
8235
|
+
const learn = /^\/learn\/(spam|ham)$/.exec(url.pathname);
|
|
8236
|
+
if (!["/scan", "/check"].includes(url.pathname) && !learn) {
|
|
8237
|
+
send(404, { error: "Not found" });
|
|
8238
|
+
return;
|
|
8239
|
+
}
|
|
8240
|
+
if (learn && !token) {
|
|
8241
|
+
send(403, { error: "Learning needs the server to be started with a token" });
|
|
8242
|
+
return;
|
|
8243
|
+
}
|
|
8244
|
+
const length = request.headers["content-length"];
|
|
8245
|
+
if (length !== void 0 && Number(length) > maxSize) {
|
|
8246
|
+
send(413, { error: `Message larger than ${maxSize} bytes` }, { connection: "close" });
|
|
8247
|
+
return;
|
|
8248
|
+
}
|
|
8249
|
+
if (active >= maxConnections) {
|
|
8250
|
+
send(503, { error: "Too many requests at once" }, { connection: "close", "retry-after": "1" });
|
|
8251
|
+
return;
|
|
8252
|
+
}
|
|
8253
|
+
active++;
|
|
8254
|
+
try {
|
|
8255
|
+
const raw = await readBody(request, maxSize);
|
|
8256
|
+
if (learn) {
|
|
8257
|
+
await scanner.learn(raw, learn[1]);
|
|
8258
|
+
if (modelPath) {
|
|
8259
|
+
scanner.saveModel(modelPath);
|
|
8260
|
+
}
|
|
8261
|
+
send(200, { ok: true, learned: learn[1] });
|
|
8262
|
+
return;
|
|
8263
|
+
}
|
|
8264
|
+
const result = await scanner.scan(raw, { session: sessionFromQuery(url.searchParams) });
|
|
8265
|
+
if (url.pathname === "/scan") {
|
|
8266
|
+
send(200, serializeResult(result, { verbose: url.searchParams.get("verbose") === "1" }));
|
|
8267
|
+
return;
|
|
8268
|
+
}
|
|
8269
|
+
const tag = url.searchParams.get("subjectTag");
|
|
8270
|
+
const rewritten = rewriteMessage(raw, spamHeaders(result, { version: VERSION }), { subjectTag: result.isSpam && tag ? tag : null });
|
|
8271
|
+
send(200, rewritten, { "x-spam-flag": result.isSpam ? "YES" : "NO", "x-spam-score": result.score.toFixed(1), "x-spam-action": result.action });
|
|
8272
|
+
} finally {
|
|
8273
|
+
active--;
|
|
8274
|
+
}
|
|
8275
|
+
} catch (error) {
|
|
8276
|
+
send(error.statusCode || 500, { error: error.message });
|
|
8277
|
+
}
|
|
8278
|
+
});
|
|
8279
|
+
server.maxConnections = maxConnections;
|
|
8280
|
+
return server;
|
|
8281
|
+
}
|
|
8282
|
+
function createTcpServer(scanner, options = {}) {
|
|
8283
|
+
const {
|
|
8284
|
+
json = true,
|
|
8285
|
+
maxSize = MAX_MESSAGE_SIZE,
|
|
8286
|
+
timeout = 6e4,
|
|
8287
|
+
maxConnections = MAX_CONNECTIONS
|
|
8288
|
+
} = options;
|
|
8289
|
+
const server = import_node_net4.default.createServer({ allowHalfOpen: true }, (socket) => {
|
|
8290
|
+
const chunks = [];
|
|
8291
|
+
let size = 0;
|
|
8292
|
+
socket.setTimeout(timeout, () => socket.destroy());
|
|
8293
|
+
const onEnd = async () => {
|
|
8294
|
+
socket.setTimeout(0);
|
|
8295
|
+
try {
|
|
8296
|
+
const result = await scanner.scan(import_node_buffer10.Buffer.concat(chunks));
|
|
8297
|
+
socket.end(json ? `${JSON.stringify(serializeResult(result))}
|
|
8298
|
+
` : `${result.isSpam ? "SPAM" : "HAM"} ${result.score.toFixed(1)}/${result.threshold.toFixed(1)} ${result.tests.map((test) => test.name).join(",")}
|
|
8299
|
+
`);
|
|
8300
|
+
} catch (error) {
|
|
8301
|
+
socket.end(json ? `${JSON.stringify({ error: error.message })}
|
|
8302
|
+
` : `ERROR ${error.message}
|
|
8303
|
+
`);
|
|
8304
|
+
}
|
|
8305
|
+
};
|
|
8306
|
+
const onData = (chunk2) => {
|
|
8307
|
+
size += chunk2.length;
|
|
8308
|
+
if (size > maxSize) {
|
|
8309
|
+
socket.off("data", onData);
|
|
8310
|
+
socket.off("end", onEnd);
|
|
8311
|
+
socket.end(`${JSON.stringify({ error: `Message larger than ${maxSize} bytes` })}
|
|
8312
|
+
`, () => socket.destroy());
|
|
8313
|
+
return;
|
|
8314
|
+
}
|
|
8315
|
+
chunks.push(chunk2);
|
|
8316
|
+
};
|
|
8317
|
+
socket.on("data", onData);
|
|
8318
|
+
socket.on("end", onEnd);
|
|
8319
|
+
socket.on("error", () => {
|
|
8320
|
+
});
|
|
8321
|
+
});
|
|
8322
|
+
server.maxConnections = maxConnections;
|
|
8323
|
+
return server;
|
|
8324
|
+
}
|
|
8325
|
+
var import_node_buffer10, import_node_crypto5, import_node_http2, import_node_net4, MAX_MESSAGE_SIZE, MAX_CONNECTIONS;
|
|
8326
|
+
var init_server = __esm({
|
|
8327
|
+
"src/server.js"() {
|
|
8328
|
+
import_node_buffer10 = require("node:buffer");
|
|
8329
|
+
import_node_crypto5 = require("node:crypto");
|
|
8330
|
+
import_node_http2 = __toESM(require("node:http"), 1);
|
|
8331
|
+
import_node_net4 = __toESM(require("node:net"), 1);
|
|
8332
|
+
init_headers();
|
|
8333
|
+
init_version();
|
|
8334
|
+
MAX_MESSAGE_SIZE = 25 * 1024 * 1024;
|
|
8335
|
+
MAX_CONNECTIONS = 200;
|
|
8336
|
+
}
|
|
8337
|
+
});
|
|
8338
|
+
|
|
7754
8339
|
// src/milter.js
|
|
7755
|
-
function packet(command, data2 =
|
|
7756
|
-
const payload =
|
|
7757
|
-
const header =
|
|
8340
|
+
function packet(command, data2 = import_node_buffer11.Buffer.alloc(0)) {
|
|
8341
|
+
const payload = import_node_buffer11.Buffer.isBuffer(data2) ? data2 : import_node_buffer11.Buffer.from(data2, "latin1");
|
|
8342
|
+
const header = import_node_buffer11.Buffer.alloc(5);
|
|
7758
8343
|
header.writeUInt32BE(payload.length + 1);
|
|
7759
8344
|
header.write(command, 4, "latin1");
|
|
7760
|
-
return
|
|
8345
|
+
return import_node_buffer11.Buffer.concat([header, payload]);
|
|
7761
8346
|
}
|
|
7762
8347
|
function cstrings(data2) {
|
|
7763
8348
|
const parts = data2.toString("latin1").split("\0");
|
|
@@ -7767,21 +8352,22 @@ function cstrings(data2) {
|
|
|
7767
8352
|
return parts;
|
|
7768
8353
|
}
|
|
7769
8354
|
function cstring(value) {
|
|
7770
|
-
return
|
|
8355
|
+
return import_node_buffer11.Buffer.from(`${value}\0`, "latin1");
|
|
7771
8356
|
}
|
|
7772
8357
|
function address(value) {
|
|
7773
8358
|
const text = String(value ?? "").trim();
|
|
7774
8359
|
const match = /^<?([^<>]*)>?$/.exec(text);
|
|
7775
8360
|
return match ? match[1] : text;
|
|
7776
8361
|
}
|
|
7777
|
-
var
|
|
8362
|
+
var import_node_buffer11, import_node_events, import_node_net5, COMMANDS, RESPONSES, ACTIONS, PROTOCOL, MAX_PACKET, MilterSession, MilterServer;
|
|
7778
8363
|
var init_milter = __esm({
|
|
7779
8364
|
"src/milter.js"() {
|
|
7780
|
-
|
|
8365
|
+
import_node_buffer11 = require("node:buffer");
|
|
7781
8366
|
import_node_events = require("node:events");
|
|
7782
|
-
|
|
8367
|
+
import_node_net5 = __toESM(require("node:net"), 1);
|
|
7783
8368
|
init_headers();
|
|
7784
8369
|
init_auth();
|
|
8370
|
+
init_server();
|
|
7785
8371
|
init_version();
|
|
7786
8372
|
COMMANDS = {
|
|
7787
8373
|
ABORT: "A",
|
|
@@ -7875,6 +8461,15 @@ var init_milter = __esm({
|
|
|
7875
8461
|
this.body = [];
|
|
7876
8462
|
this.size = 0;
|
|
7877
8463
|
this.truncated = false;
|
|
8464
|
+
this.headerSize = 0;
|
|
8465
|
+
}
|
|
8466
|
+
// The envelope and headers are kept whole for the scan, so beyond the
|
|
8467
|
+
// message size limit the message is not scanned (see onError).
|
|
8468
|
+
keep(data2) {
|
|
8469
|
+
this.headerSize += data2.length;
|
|
8470
|
+
if (this.headerSize > this.server.options.maxSize) {
|
|
8471
|
+
throw new Error(`Message headers larger than ${this.server.options.maxSize} bytes`);
|
|
8472
|
+
}
|
|
7878
8473
|
}
|
|
7879
8474
|
send(command, data2) {
|
|
7880
8475
|
if (!this.socket.destroyed) {
|
|
@@ -7887,7 +8482,7 @@ var init_milter = __esm({
|
|
|
7887
8482
|
if (this.buffered < this.needed) {
|
|
7888
8483
|
return;
|
|
7889
8484
|
}
|
|
7890
|
-
let buffer = this.chunks.length === 1 ? this.chunks[0] :
|
|
8485
|
+
let buffer = this.chunks.length === 1 ? this.chunks[0] : import_node_buffer11.Buffer.concat(this.chunks);
|
|
7891
8486
|
this.needed = 4;
|
|
7892
8487
|
while (buffer.length >= 4) {
|
|
7893
8488
|
const length = buffer.readUInt32BE(0);
|
|
@@ -7932,7 +8527,7 @@ var init_milter = __esm({
|
|
|
7932
8527
|
const protocol = data2.readUInt32BE(8);
|
|
7933
8528
|
const wanted = ACTIONS.ADDHDRS | ACTIONS.CHGHDRS | ACTIONS.QUARANTINE;
|
|
7934
8529
|
this.leadingSpace = Boolean(protocol & PROTOCOL.HDR_LEADSPC);
|
|
7935
|
-
const reply2 =
|
|
8530
|
+
const reply2 = import_node_buffer11.Buffer.alloc(12);
|
|
7936
8531
|
reply2.writeUInt32BE(Math.min(version, 6), 0);
|
|
7937
8532
|
reply2.writeUInt32BE(actions & wanted, 4);
|
|
7938
8533
|
reply2.writeUInt32BE(protocol & (PROTOCOL.HDR_LEADSPC | PROTOCOL.NOUNKNOWN), 8);
|
|
@@ -7975,16 +8570,19 @@ var init_milter = __esm({
|
|
|
7975
8570
|
}
|
|
7976
8571
|
case COMMANDS.MAIL: {
|
|
7977
8572
|
this.resetMessage();
|
|
8573
|
+
this.keep(data2);
|
|
7978
8574
|
this.envelope.mailFrom = { address: address(cstrings(data2)[0]) };
|
|
7979
8575
|
this.send(RESPONSES.CONTINUE);
|
|
7980
8576
|
break;
|
|
7981
8577
|
}
|
|
7982
8578
|
case COMMANDS.RCPT: {
|
|
8579
|
+
this.keep(data2);
|
|
7983
8580
|
this.envelope.rcptTo.push({ address: address(cstrings(data2)[0]) });
|
|
7984
8581
|
this.send(RESPONSES.CONTINUE);
|
|
7985
8582
|
break;
|
|
7986
8583
|
}
|
|
7987
8584
|
case COMMANDS.HEADER: {
|
|
8585
|
+
this.keep(data2);
|
|
7988
8586
|
const [name, value = ""] = cstrings(data2);
|
|
7989
8587
|
this.headers.push([name, value]);
|
|
7990
8588
|
this.send(RESPONSES.CONTINUE);
|
|
@@ -7994,14 +8592,19 @@ var init_milter = __esm({
|
|
|
7994
8592
|
if (this.truncated || this.size + data2.length > options.maxSize) {
|
|
7995
8593
|
this.truncated = true;
|
|
7996
8594
|
} else {
|
|
7997
|
-
this.body.push(
|
|
8595
|
+
this.body.push(import_node_buffer11.Buffer.from(data2));
|
|
7998
8596
|
this.size += data2.length;
|
|
7999
8597
|
}
|
|
8000
8598
|
this.send(RESPONSES.CONTINUE);
|
|
8001
8599
|
break;
|
|
8002
8600
|
}
|
|
8003
8601
|
case COMMANDS.BODYEOB: {
|
|
8004
|
-
|
|
8602
|
+
this.socket.pause();
|
|
8603
|
+
try {
|
|
8604
|
+
await this.endOfMessage();
|
|
8605
|
+
} finally {
|
|
8606
|
+
this.socket.resume();
|
|
8607
|
+
}
|
|
8005
8608
|
break;
|
|
8006
8609
|
}
|
|
8007
8610
|
case COMMANDS.ABORT: {
|
|
@@ -8024,7 +8627,7 @@ var init_milter = __esm({
|
|
|
8024
8627
|
/** @returns {Buffer} */
|
|
8025
8628
|
rawMessage() {
|
|
8026
8629
|
const head = this.headers.map(([name, value]) => `${name}:${this.leadingSpace || value.startsWith(" ") || value.startsWith(" ") ? "" : " "}${value}`).join("\r\n");
|
|
8027
|
-
return
|
|
8630
|
+
return import_node_buffer11.Buffer.concat([import_node_buffer11.Buffer.from(this.headers.length > 0 ? `${head}\r
|
|
8028
8631
|
\r
|
|
8029
8632
|
` : "\r\n", "latin1"), ...this.body]);
|
|
8030
8633
|
}
|
|
@@ -8035,9 +8638,9 @@ var init_milter = __esm({
|
|
|
8035
8638
|
const key = name.toLowerCase();
|
|
8036
8639
|
counts.set(key, (counts.get(key) || 0) + 1);
|
|
8037
8640
|
if (key.startsWith("x-spam-")) {
|
|
8038
|
-
const index =
|
|
8641
|
+
const index = import_node_buffer11.Buffer.alloc(4);
|
|
8039
8642
|
index.writeUInt32BE(counts.get(key));
|
|
8040
|
-
this.send(RESPONSES.CHGHEADER,
|
|
8643
|
+
this.send(RESPONSES.CHGHEADER, import_node_buffer11.Buffer.concat([index, cstring(name), cstring("")]));
|
|
8041
8644
|
}
|
|
8042
8645
|
}
|
|
8043
8646
|
}
|
|
@@ -8060,7 +8663,7 @@ var init_milter = __esm({
|
|
|
8060
8663
|
}
|
|
8061
8664
|
this.server.report(error);
|
|
8062
8665
|
this.removeForgedHeaders();
|
|
8063
|
-
this.send(RESPONSES.ADDHEADER,
|
|
8666
|
+
this.send(RESPONSES.ADDHEADER, import_node_buffer11.Buffer.concat([cstring("X-Spam-Virus-Scan"), cstring(`${this.leadingSpace ? " " : ""}failed`)]));
|
|
8064
8667
|
this.send(RESPONSES.CONTINUE);
|
|
8065
8668
|
this.resetMessage();
|
|
8066
8669
|
return;
|
|
@@ -8081,15 +8684,15 @@ var init_milter = __esm({
|
|
|
8081
8684
|
headers.unshift(["Authentication-Results", formatAuthResultsHeader(result.results.authentication, options.hostname)]);
|
|
8082
8685
|
}
|
|
8083
8686
|
for (const [name, value] of headers) {
|
|
8084
|
-
this.send(RESPONSES.ADDHEADER,
|
|
8687
|
+
this.send(RESPONSES.ADDHEADER, import_node_buffer11.Buffer.concat([cstring(name), cstring(`${this.leadingSpace ? " " : ""}${value}`.replaceAll("\r\n", "\n"))]));
|
|
8085
8688
|
}
|
|
8086
8689
|
if (result.isSpam && options.subjectTag) {
|
|
8087
8690
|
const subject = this.headers.find(([name]) => name.toLowerCase() === "subject");
|
|
8088
8691
|
const value = taggedSubject(options.subjectTag, subject ? subject[1].trim() : "", "\n");
|
|
8089
8692
|
if (value !== null) {
|
|
8090
|
-
const index =
|
|
8693
|
+
const index = import_node_buffer11.Buffer.alloc(4);
|
|
8091
8694
|
index.writeUInt32BE(1);
|
|
8092
|
-
this.send(RESPONSES.CHGHEADER,
|
|
8695
|
+
this.send(RESPONSES.CHGHEADER, import_node_buffer11.Buffer.concat([index, cstring("Subject"), cstring(`${this.leadingSpace ? " " : ""}${value}`.replaceAll("\r\n", "\n"))]));
|
|
8093
8696
|
}
|
|
8094
8697
|
}
|
|
8095
8698
|
if (result.action === "reject" && options.reject) {
|
|
@@ -8116,8 +8719,10 @@ var init_milter = __esm({
|
|
|
8116
8719
|
* @param {boolean} [options.quarantine] - hold spam in the MTA's quarantine
|
|
8117
8720
|
* @param {string|null} [options.subjectTag] - e.g. "[SPAM]"
|
|
8118
8721
|
* @param {string} [options.hostname] - this server's name, for Authentication-Results
|
|
8119
|
-
* @param {number} [options.maxSize] - body bytes kept for scanning
|
|
8722
|
+
* @param {number} [options.maxSize] - body bytes kept for scanning (and the
|
|
8723
|
+
* most bytes of envelope and headers: beyond them a message is not scanned)
|
|
8120
8724
|
* @param {'tempfail'|'accept'} [options.onError] - answer when a scan fails
|
|
8725
|
+
* @param {number} [options.maxConnections] - connections kept open at once; more are closed at once
|
|
8121
8726
|
*/
|
|
8122
8727
|
constructor(scanner, options = {}) {
|
|
8123
8728
|
super();
|
|
@@ -8129,19 +8734,21 @@ var init_milter = __esm({
|
|
|
8129
8734
|
quarantine: false,
|
|
8130
8735
|
subjectTag: null,
|
|
8131
8736
|
hostname: "spamscanner",
|
|
8132
|
-
maxSize:
|
|
8737
|
+
maxSize: MAX_MESSAGE_SIZE,
|
|
8133
8738
|
onError: "tempfail",
|
|
8739
|
+
maxConnections: MAX_CONNECTIONS,
|
|
8134
8740
|
...options
|
|
8135
8741
|
};
|
|
8136
8742
|
if (!/^[45]\d\d$/.test(String(this.options.rejectCode))) {
|
|
8137
8743
|
throw new RangeError(`rejectCode must be a 4xx or 5xx SMTP code, not ${this.options.rejectCode}`);
|
|
8138
8744
|
}
|
|
8139
8745
|
this.sockets = /* @__PURE__ */ new Set();
|
|
8140
|
-
this.server =
|
|
8746
|
+
this.server = import_node_net5.default.createServer((socket) => {
|
|
8141
8747
|
this.sockets.add(socket);
|
|
8142
8748
|
socket.on("close", () => this.sockets.delete(socket));
|
|
8143
8749
|
return new MilterSession(socket, this);
|
|
8144
8750
|
});
|
|
8751
|
+
this.server.maxConnections = this.options.maxConnections;
|
|
8145
8752
|
this.server.on("error", (error) => this.report(error));
|
|
8146
8753
|
}
|
|
8147
8754
|
// Errors go to "error" listeners; with none, a broken connection or failed
|
|
@@ -8189,169 +8796,6 @@ var init_milter = __esm({
|
|
|
8189
8796
|
}
|
|
8190
8797
|
});
|
|
8191
8798
|
|
|
8192
|
-
// src/server.js
|
|
8193
|
-
function serializeResult(result, options = {}) {
|
|
8194
|
-
const { mail, tokens, ...rest } = result;
|
|
8195
|
-
const output = {
|
|
8196
|
-
...rest,
|
|
8197
|
-
results: { ...result.results }
|
|
8198
|
-
};
|
|
8199
|
-
if (output.results.authentication) {
|
|
8200
|
-
const { receivedChain, ...auth } = output.results.authentication;
|
|
8201
|
-
output.results.authentication = auth;
|
|
8202
|
-
}
|
|
8203
|
-
if (options.verbose) {
|
|
8204
|
-
output.tokens = tokens;
|
|
8205
|
-
output.subject = mail?.subject ?? null;
|
|
8206
|
-
}
|
|
8207
|
-
return output;
|
|
8208
|
-
}
|
|
8209
|
-
function sessionFromQuery(query) {
|
|
8210
|
-
const session = {};
|
|
8211
|
-
if (query.get("ip")) {
|
|
8212
|
-
session.remoteAddress = query.get("ip");
|
|
8213
|
-
}
|
|
8214
|
-
if (query.get("hostname")) {
|
|
8215
|
-
session.resolvedClientHostname = query.get("hostname");
|
|
8216
|
-
}
|
|
8217
|
-
if (query.get("helo")) {
|
|
8218
|
-
session.helo = query.get("helo");
|
|
8219
|
-
}
|
|
8220
|
-
const recipients = query.getAll("to").flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean);
|
|
8221
|
-
if (query.has("from") || recipients.length > 0) {
|
|
8222
|
-
session.envelope = { mailFrom: { address: query.get("from") || "" }, rcptTo: recipients.map((address2) => ({ address: address2 })) };
|
|
8223
|
-
}
|
|
8224
|
-
return session;
|
|
8225
|
-
}
|
|
8226
|
-
function sameToken(given, expected) {
|
|
8227
|
-
const a = import_node_buffer11.Buffer.from(String(given));
|
|
8228
|
-
const b = import_node_buffer11.Buffer.from(String(expected));
|
|
8229
|
-
return a.length === b.length && (0, import_node_crypto5.timingSafeEqual)(a, b);
|
|
8230
|
-
}
|
|
8231
|
-
function readBody(request, limit) {
|
|
8232
|
-
return new Promise((resolve, reject) => {
|
|
8233
|
-
const chunks = [];
|
|
8234
|
-
let size = 0;
|
|
8235
|
-
request.on("data", (chunk2) => {
|
|
8236
|
-
size += chunk2.length;
|
|
8237
|
-
if (size <= limit) {
|
|
8238
|
-
chunks.push(chunk2);
|
|
8239
|
-
}
|
|
8240
|
-
});
|
|
8241
|
-
request.on("end", () => {
|
|
8242
|
-
if (size > limit) {
|
|
8243
|
-
const error = new Error(`Message larger than ${limit} bytes`);
|
|
8244
|
-
error.statusCode = 413;
|
|
8245
|
-
reject(error);
|
|
8246
|
-
return;
|
|
8247
|
-
}
|
|
8248
|
-
resolve(import_node_buffer11.Buffer.concat(chunks));
|
|
8249
|
-
});
|
|
8250
|
-
request.on("error", reject);
|
|
8251
|
-
});
|
|
8252
|
-
}
|
|
8253
|
-
function createHttpServer(scanner, options = {}) {
|
|
8254
|
-
const { token = null, maxSize = 25 * 1024 * 1024, modelPath = null } = options;
|
|
8255
|
-
return import_node_http2.default.createServer(async (request, response) => {
|
|
8256
|
-
const send = (status, body, headers = {}) => {
|
|
8257
|
-
const isBuffer = import_node_buffer11.Buffer.isBuffer(body);
|
|
8258
|
-
response.writeHead(status, { "content-type": isBuffer ? "message/rfc822" : "application/json; charset=utf-8", "cache-control": "no-store", ...headers });
|
|
8259
|
-
response.end(isBuffer ? body : `${JSON.stringify(body)}
|
|
8260
|
-
`);
|
|
8261
|
-
};
|
|
8262
|
-
try {
|
|
8263
|
-
const url = new URL(request.url, "http://localhost");
|
|
8264
|
-
if (request.method === "GET" && url.pathname === "/health") {
|
|
8265
|
-
send(200, { ok: true, version: VERSION });
|
|
8266
|
-
return;
|
|
8267
|
-
}
|
|
8268
|
-
if (token && !sameToken((request.headers.authorization || "").replace(/^bearer\s+/i, ""), token)) {
|
|
8269
|
-
send(401, { error: "Unauthorized" }, { "www-authenticate": "Bearer" });
|
|
8270
|
-
return;
|
|
8271
|
-
}
|
|
8272
|
-
if (request.method !== "POST") {
|
|
8273
|
-
send(405, { error: "Method not allowed" }, { allow: "POST" });
|
|
8274
|
-
return;
|
|
8275
|
-
}
|
|
8276
|
-
const learn = /^\/learn\/(spam|ham)$/.exec(url.pathname);
|
|
8277
|
-
if (!["/scan", "/check"].includes(url.pathname) && !learn) {
|
|
8278
|
-
send(404, { error: "Not found" });
|
|
8279
|
-
return;
|
|
8280
|
-
}
|
|
8281
|
-
if (learn && !token) {
|
|
8282
|
-
send(403, { error: "Learning needs the server to be started with a token" });
|
|
8283
|
-
return;
|
|
8284
|
-
}
|
|
8285
|
-
const raw = await readBody(request, maxSize);
|
|
8286
|
-
if (learn) {
|
|
8287
|
-
await scanner.learn(raw, learn[1]);
|
|
8288
|
-
if (modelPath) {
|
|
8289
|
-
scanner.saveModel(modelPath);
|
|
8290
|
-
}
|
|
8291
|
-
send(200, { ok: true, learned: learn[1] });
|
|
8292
|
-
return;
|
|
8293
|
-
}
|
|
8294
|
-
const result = await scanner.scan(raw, { session: sessionFromQuery(url.searchParams) });
|
|
8295
|
-
if (url.pathname === "/scan") {
|
|
8296
|
-
send(200, serializeResult(result, { verbose: url.searchParams.get("verbose") === "1" }));
|
|
8297
|
-
return;
|
|
8298
|
-
}
|
|
8299
|
-
const tag = url.searchParams.get("subjectTag");
|
|
8300
|
-
const rewritten = rewriteMessage(raw, spamHeaders(result, { version: VERSION }), { subjectTag: result.isSpam && tag ? tag : null });
|
|
8301
|
-
send(200, rewritten, { "x-spam-flag": result.isSpam ? "YES" : "NO", "x-spam-score": result.score.toFixed(1), "x-spam-action": result.action });
|
|
8302
|
-
} catch (error) {
|
|
8303
|
-
send(error.statusCode || 500, { error: error.message });
|
|
8304
|
-
}
|
|
8305
|
-
});
|
|
8306
|
-
}
|
|
8307
|
-
function createTcpServer(scanner, options = {}) {
|
|
8308
|
-
const { json = true, maxSize = 25 * 1024 * 1024, timeout = 6e4 } = options;
|
|
8309
|
-
return import_node_net5.default.createServer({ allowHalfOpen: true }, (socket) => {
|
|
8310
|
-
const chunks = [];
|
|
8311
|
-
let size = 0;
|
|
8312
|
-
socket.setTimeout(timeout, () => socket.destroy());
|
|
8313
|
-
const onEnd = async () => {
|
|
8314
|
-
socket.setTimeout(0);
|
|
8315
|
-
try {
|
|
8316
|
-
const result = await scanner.scan(import_node_buffer11.Buffer.concat(chunks));
|
|
8317
|
-
socket.end(json ? `${JSON.stringify(serializeResult(result))}
|
|
8318
|
-
` : `${result.isSpam ? "SPAM" : "HAM"} ${result.score.toFixed(1)}/${result.threshold.toFixed(1)} ${result.tests.map((test) => test.name).join(",")}
|
|
8319
|
-
`);
|
|
8320
|
-
} catch (error) {
|
|
8321
|
-
socket.end(json ? `${JSON.stringify({ error: error.message })}
|
|
8322
|
-
` : `ERROR ${error.message}
|
|
8323
|
-
`);
|
|
8324
|
-
}
|
|
8325
|
-
};
|
|
8326
|
-
const onData = (chunk2) => {
|
|
8327
|
-
size += chunk2.length;
|
|
8328
|
-
if (size > maxSize) {
|
|
8329
|
-
socket.off("data", onData);
|
|
8330
|
-
socket.off("end", onEnd);
|
|
8331
|
-
socket.end(`${JSON.stringify({ error: `Message larger than ${maxSize} bytes` })}
|
|
8332
|
-
`, () => socket.destroy());
|
|
8333
|
-
return;
|
|
8334
|
-
}
|
|
8335
|
-
chunks.push(chunk2);
|
|
8336
|
-
};
|
|
8337
|
-
socket.on("data", onData);
|
|
8338
|
-
socket.on("end", onEnd);
|
|
8339
|
-
socket.on("error", () => {
|
|
8340
|
-
});
|
|
8341
|
-
});
|
|
8342
|
-
}
|
|
8343
|
-
var import_node_buffer11, import_node_crypto5, import_node_http2, import_node_net5;
|
|
8344
|
-
var init_server = __esm({
|
|
8345
|
-
"src/server.js"() {
|
|
8346
|
-
import_node_buffer11 = require("node:buffer");
|
|
8347
|
-
import_node_crypto5 = require("node:crypto");
|
|
8348
|
-
import_node_http2 = __toESM(require("node:http"), 1);
|
|
8349
|
-
import_node_net5 = __toESM(require("node:net"), 1);
|
|
8350
|
-
init_headers();
|
|
8351
|
-
init_version();
|
|
8352
|
-
}
|
|
8353
|
-
});
|
|
8354
|
-
|
|
8355
8799
|
// src/spamd.js
|
|
8356
8800
|
function parseRequest(head) {
|
|
8357
8801
|
const [line, ...rest] = head.split("\r\n");
|
|
@@ -8438,14 +8882,15 @@ async function answer(scanner, request, message, options) {
|
|
|
8438
8882
|
}
|
|
8439
8883
|
function createSpamdServer(scanner, options = {}) {
|
|
8440
8884
|
const settings = {
|
|
8441
|
-
maxSize:
|
|
8885
|
+
maxSize: MAX_MESSAGE_SIZE,
|
|
8442
8886
|
allowTell: false,
|
|
8443
8887
|
modelPath: null,
|
|
8444
8888
|
subjectTag: null,
|
|
8445
8889
|
timeout: 6e4,
|
|
8890
|
+
maxConnections: MAX_CONNECTIONS,
|
|
8446
8891
|
...options
|
|
8447
8892
|
};
|
|
8448
|
-
|
|
8893
|
+
const server = import_node_net6.default.createServer({ allowHalfOpen: true }, (socket) => {
|
|
8449
8894
|
let head = import_node_buffer12.Buffer.alloc(0);
|
|
8450
8895
|
const chunks = [];
|
|
8451
8896
|
let size = 0;
|
|
@@ -8527,6 +8972,8 @@ function createSpamdServer(scanner, options = {}) {
|
|
|
8527
8972
|
socket.on("error", () => {
|
|
8528
8973
|
});
|
|
8529
8974
|
});
|
|
8975
|
+
server.maxConnections = settings.maxConnections;
|
|
8976
|
+
return server;
|
|
8530
8977
|
}
|
|
8531
8978
|
var import_node_buffer12, import_node_net6, EX_OK, EX_USAGE, EX_DATAERR, EX_SOFTWARE, EX_PROTOCOL, EX_NOPERM, COMMANDS2;
|
|
8532
8979
|
var init_spamd = __esm({
|
|
@@ -8534,6 +8981,7 @@ var init_spamd = __esm({
|
|
|
8534
8981
|
import_node_buffer12 = require("node:buffer");
|
|
8535
8982
|
import_node_net6 = __toESM(require("node:net"), 1);
|
|
8536
8983
|
init_headers();
|
|
8984
|
+
init_server();
|
|
8537
8985
|
init_version();
|
|
8538
8986
|
EX_OK = 0;
|
|
8539
8987
|
EX_USAGE = 64;
|
|
@@ -9190,6 +9638,7 @@ var init_src = __esm({
|
|
|
9190
9638
|
const settings = config.clamav === true ? {} : config.clamav;
|
|
9191
9639
|
const budget = settings.timeout ?? CLAMAV_TIMEOUT;
|
|
9192
9640
|
const deadline = Date.now() + budget;
|
|
9641
|
+
let expired = false;
|
|
9193
9642
|
const viruses = [];
|
|
9194
9643
|
const errors = [];
|
|
9195
9644
|
const skipped = [];
|
|
@@ -9209,7 +9658,7 @@ var init_src = __esm({
|
|
|
9209
9658
|
while (queue.length > 0) {
|
|
9210
9659
|
const attachment = queue.shift();
|
|
9211
9660
|
const filename = attachment.filename || "unnamed attachment";
|
|
9212
|
-
if (deadline <= Date.now()) {
|
|
9661
|
+
if (expired || deadline <= Date.now()) {
|
|
9213
9662
|
errors.push({ filename, error: `not scanned within ${budget} ms` });
|
|
9214
9663
|
continue;
|
|
9215
9664
|
}
|
|
@@ -9229,6 +9678,7 @@ var init_src = __esm({
|
|
|
9229
9678
|
}
|
|
9230
9679
|
} catch (error) {
|
|
9231
9680
|
debug2("virus scan failed: %s", error.message);
|
|
9681
|
+
expired ||= Boolean(error.timedOut);
|
|
9232
9682
|
if (CLAMAV_LIMITS.test(error.message)) {
|
|
9233
9683
|
skipped.push({ filename, reason: error.message.replace(/^clamd: (?:stream: )?/, "") });
|
|
9234
9684
|
} else {
|