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.
@@ -28,7 +28,7 @@ export function findPart(raw: string, boundary: string, types: string[]): {
28
28
  export function isArfMessage(parsed: object): boolean;
29
29
  /**
30
30
  * Parse an ARF (RFC 5965) abuse report.
31
- * @param {Buffer|string} source - the raw report message
31
+ * @param {Buffer|Uint8Array|string} source - the raw report message
32
32
  * @returns {Promise<object>} feedbackType, userAgent, version, arrivalDate,
33
33
  * sourceIp, originalMailFrom, originalRcptTo, reportingMta,
34
34
  * originalEnvelopeId, authenticationResults, reportedDomain, reportedUri,
@@ -37,13 +37,13 @@ export function isArfMessage(parsed: object): boolean;
37
37
  * message) and rawFeedbackReport
38
38
  * @throws {Error} when the message is not a valid ARF report
39
39
  */
40
- export function parse(source: Buffer | string): Promise<object>;
40
+ export function parse(message: any): Promise<object>;
41
41
  /**
42
42
  * Parse an ARF report, or return null when the message is not one.
43
- * @param {Buffer|string} source
43
+ * @param {Buffer|Uint8Array|string} source
44
44
  * @returns {Promise<object|null>}
45
45
  */
46
- export function tryParse(source: Buffer | string): Promise<object | null>;
46
+ export function tryParse(source: Buffer | Uint8Array | string): Promise<object | null>;
47
47
  /**
48
48
  * Write an ARF report. (A string message gives a string report, and a
49
49
  * Buffer a Buffer, its bytes kept as they are, whatever their character set.)
@@ -21,7 +21,9 @@ export function sniff(buffer: Buffer): {
21
21
  * is read, as well as the last one found: archive programs differ in which
22
22
  * they use, so a directory hidden behind another record is read too. ZIP64
23
23
  * archives, and archives after other data (self-extracting archives), are
24
- * read as well.
24
+ * read as well. Each entry is read once, however many directories overlap
25
+ * it; a name longer than 1 KiB keeps its first and last 512 bytes (with "…"
26
+ * between them), and reading stops after 1 MiB of names.
25
27
  * @param {Buffer} buffer
26
28
  * @param {number} [limit] - most entries to read
27
29
  * @returns {Array<{name: string, encrypted: boolean, size: number}>}
@@ -8,6 +8,20 @@
8
8
  * @returns {(name: string, type: string) => Promise<any[]>}
9
9
  */
10
10
  export function createResolver(timeout?: number, servers?: string[]): (name: string, type: string) => Promise<any[]>;
11
+ /**
12
+ * A resolver that answers through `resolver` at most `maxQueries` times and
13
+ * until `deadline` (a time in milliseconds, as Date.now() gives it): any
14
+ * lookup after either, or still unanswered at the deadline, fails at once
15
+ * with ETIMEOUT, so a message's checks stop sending queries when its time is
16
+ * up and never send more than `maxQueries`.
17
+ * @param {(name: string, type: string) => Promise<any[]>} resolver
18
+ * @param {{maxQueries?: number, deadline?: number}} [limits]
19
+ * @returns {(name: string, type: string) => Promise<any[]>}
20
+ */
21
+ export function limitResolver(resolver: (name: string, type: string) => Promise<any[]>, { maxQueries, deadline }?: {
22
+ maxQueries?: number;
23
+ deadline?: number;
24
+ }): (name: string, type: string) => Promise<any[]>;
11
25
  /**
12
26
  * Overall DKIM result from mailauth's per-signature results: "pass" when any
13
27
  * signature passes, "fail" when signatures exist and none passes, "none"
@@ -57,6 +71,7 @@ export function normalizeAuthOutput(output?: object, options?: {
57
71
  *
58
72
  * Needs the connecting client's IP address: without it nothing is checked and
59
73
  * every result is "none". DNS errors give "temperror" results, never a pass.
74
+ * Only the first MAX_DKIM_SIGNATURES DKIM signatures are checked.
60
75
  *
61
76
  * @param {Buffer|string} message - the raw message
62
77
  * @param {object} options
@@ -65,7 +80,11 @@ export function normalizeAuthOutput(output?: object, options?: {
65
80
  * @param {string} [options.sender] - envelope sender (MAIL FROM)
66
81
  * @param {string} [options.mta] - this server's hostname, for the headers
67
82
  * @param {Function} [options.resolver] - custom DNS resolver (name, type)
68
- * @param {number} [options.timeout] - DNS timeout in milliseconds
83
+ * @param {number} [options.timeout] - milliseconds for a DNS lookup, and for
84
+ * all of the message's: no lookup is sent, nor waited for, after this long
85
+ * (and 100 ms more)
86
+ * @param {number} [options.maxDnsQueries] - DNS lookups allowed for the
87
+ * message (MAX_DNS_QUERIES); any more fail with a temporary error
69
88
  * @param {string[]} [options.dnsServers] - name servers to use
70
89
  * @param {string[]} [options.trustedArcSealers] - forwarders (such as a
71
90
  * mailing list's domain) whose ARC seal is believed: a message they passed
@@ -81,6 +100,7 @@ export function authenticate(message: Buffer | string, options?: {
81
100
  mta?: string;
82
101
  resolver?: Function;
83
102
  timeout?: number;
103
+ maxDnsQueries?: number;
84
104
  dnsServers?: string[];
85
105
  trustedArcSealers?: string[];
86
106
  }): Promise<object>;
@@ -109,7 +129,8 @@ export function calculateAuthScore(auth: object, weights?: object): {
109
129
  };
110
130
  /**
111
131
  * The Authentication-Results header (RFC 8601) for a result, without the
112
- * header name. mailauth's own headers are used when present.
132
+ * header name. mailauth's own headers are used when present, with as many
133
+ * of their results as fit in 8 KiB.
113
134
  * @param {object} auth
114
135
  * @param {string} [hostname]
115
136
  * @returns {string}
@@ -121,6 +142,15 @@ export function formatAuthResultsHeader(auth: object, hostname?: string): string
121
142
  * @returns {string}
122
143
  */
123
144
  export function summarizeAuth(auth: object): string;
145
+ export { MAX_DKIM_SIGNATURES } from "./parse.js";
146
+ /**
147
+ * DNS lookups allowed for one message: room for SPF at the limits of RFC
148
+ * 7208 (10 lookups, each MX mechanism with up to 10 hosts, A and AAAA, about
149
+ * 220 in all, as mailauth counts them), MAX_DKIM_SIGNATURES keys (see
150
+ * boundedForAuth), the
151
+ * newest ARC seal, DMARC and BIMI.
152
+ */
153
+ export const MAX_DNS_QUERIES: 300;
124
154
  export namespace DEFAULT_AUTH_WEIGHTS {
125
155
  let dkimPass: number;
126
156
  let dkimFail: number;
@@ -116,6 +116,8 @@ export class MilterSession {
116
116
  body: any[];
117
117
  size: number;
118
118
  truncated: boolean;
119
+ headerSize: number;
120
+ keep(data: any): void;
119
121
  send(command: any, data: any): void;
120
122
  receive(chunk: any): void;
121
123
  process(): Promise<void>;
@@ -146,8 +148,10 @@ export class MilterServer extends EventEmitter<[never]> {
146
148
  * @param {boolean} [options.quarantine] - hold spam in the MTA's quarantine
147
149
  * @param {string|null} [options.subjectTag] - e.g. "[SPAM]"
148
150
  * @param {string} [options.hostname] - this server's name, for Authentication-Results
149
- * @param {number} [options.maxSize] - body bytes kept for scanning
151
+ * @param {number} [options.maxSize] - body bytes kept for scanning (and the
152
+ * most bytes of envelope and headers: beyond them a message is not scanned)
150
153
  * @param {'tempfail'|'accept'} [options.onError] - answer when a scan fails
154
+ * @param {number} [options.maxConnections] - connections kept open at once; more are closed at once
151
155
  */
152
156
  constructor(scanner: import("./index.js").SpamScanner, options?: {
153
157
  reject?: boolean;
@@ -158,6 +162,7 @@ export class MilterServer extends EventEmitter<[never]> {
158
162
  hostname?: string;
159
163
  maxSize?: number;
160
164
  onError?: "tempfail" | "accept";
165
+ maxConnections?: number;
161
166
  });
162
167
  scanner: import("./index.js").SpamScanner;
163
168
  options: {
@@ -169,6 +174,7 @@ export class MilterServer extends EventEmitter<[never]> {
169
174
  hostname: string;
170
175
  maxSize: number;
171
176
  onError: string;
177
+ maxConnections: number;
172
178
  };
173
179
  sockets: Set<any>;
174
180
  server: net.Server;
@@ -49,8 +49,9 @@ export function loadDefaultModel(options?: object, { file, sea }?: {
49
49
  export function loadModel(file: string, options?: object): Classifier;
50
50
  /**
51
51
  * Write a classifier to a file, creating its directory. The model is written
52
- * to a temporary file next to it first, then renamed, so a process loading
53
- * the file never reads half of it.
52
+ * to a new temporary file next to it first, flushed to disk, then renamed, so
53
+ * a process loading the file, or one reading it after a crash, never reads
54
+ * half of it. A file replaced keeps its permissions.
54
55
  * @param {Classifier} classifier
55
56
  * @param {string} file
56
57
  * @param {object} [options] - see Classifier#toJSON
@@ -1,11 +1,49 @@
1
+ /**
2
+ * The message as mailauth is to read it: its address fields bounded as
3
+ * parseMessage bounds them (mailauth reads From and Return-Path with the same
4
+ * parser), and, when the header would take mailauth longer than linear time
5
+ * (it joins folded lines, and finds the fields a DKIM signature names, in
6
+ * time that grows with the square of their number), the header cut down:
7
+ * its folded lines unfolded (which relaxed canonicalization of DKIM does
8
+ * too) and only the fields that say who sent the message and how, as many
9
+ * as smallerHeader keeps, each up to MAX_FIELD characters (address fields as
10
+ * bounded), and at most MAX_SIGNATURES signatures. Only the first
11
+ * MAX_DKIM_SIGNATURES DKIM-Signature fields are ever kept (with their folded
12
+ * lines; the body is untouched). `fromCut` is whether a
13
+ * From field was cut or left out: then mailauth did not read the From
14
+ * address the message gives.
15
+ * @param {Buffer} raw
16
+ * @returns {{raw: Buffer, fromCut: boolean}}
17
+ */
18
+ export function boundedForAuth(raw: Buffer): {
19
+ raw: Buffer;
20
+ fromCut: boolean;
21
+ };
22
+ /**
23
+ * The simpleParser of mailparser on a message it can finish (see finishable),
24
+ * settled within `timeout`: a parse that has not settled by then is stopped
25
+ * (its parser destroyed) and rejected with an ETIMEDOUT error.
26
+ * @param {Buffer|string} raw
27
+ * @param {object} [options]
28
+ * @param {number} [options.timeout] - milliseconds (PARSE_TIMEOUT)
29
+ * @param {typeof simpleParser} [options.parser] - for tests
30
+ * @returns {Promise<object>} the parsed message
31
+ */
32
+ export function simpleParse(raw: Buffer | string, { timeout, parser }?: {
33
+ timeout?: number;
34
+ parser?: typeof simpleParser;
35
+ }): Promise<object>;
1
36
  /**
2
37
  * Parse a message with mailparser, which does not make text of HTML here:
3
38
  * `text` is that of the plain text parts only (empty for a message of HTML
4
39
  * alone), and the text HTML shows is read by getFeatures.
5
40
  * @param {Buffer|string} raw
41
+ * @param {{timeout?: number}} [options] - milliseconds for the parse (PARSE_TIMEOUT)
6
42
  * @returns {Promise<object>} the parsed message
7
43
  */
8
- export function parseMessage(raw: Buffer | string): Promise<object>;
44
+ export function parseMessage(raw: Buffer | string, options?: {
45
+ timeout?: number;
46
+ }): Promise<object>;
9
47
  /**
10
48
  * A message mailparser can finish: it never finishes one that ends in the
11
49
  * header of an attached message (however that header names or folds its
@@ -15,6 +53,17 @@ export function parseMessage(raw: Buffer | string): Promise<object>;
15
53
  * @returns {Buffer}
16
54
  */
17
55
  export function finishable(raw: Buffer | string): Buffer;
56
+ /**
57
+ * The message with each header field folded on more than MAX_FOLDS lines
58
+ * unfolded: its line breaks and the white space after each made one space,
59
+ * as mailparser reads a field (libmime's decodeHeader), so what it reads is
60
+ * the same. Header fields are those of the message and, as mailsplit finds
61
+ * them, of each MIME part and each attached message inline; everything else
62
+ * is left as it is. The message itself when nothing is unfolded.
63
+ * @param {Buffer} raw
64
+ * @returns {Buffer}
65
+ */
66
+ export function unfoldHeaders(raw: Buffer): Buffer;
18
67
  /**
19
68
  * Parse a message, reading what can be read of one beyond mailparser's
20
69
  * limits (more than 1000 MIME parts, a part's header over 1 MB): its parts
@@ -23,12 +72,16 @@ export function finishable(raw: Buffer | string): Buffer;
23
72
  * is too large, the message with its header lines cut down, or its first
24
73
  * megabyte when the header is that of a later part. `error` is the limit,
25
74
  * or null. Other errors are thrown, as earlier versions did (servers defer
26
- * the message).
75
+ * the message), and so is a parse not finished by `deadline` (ETIMEDOUT).
27
76
  * @param {Buffer} raw
28
- * @param {(raw: Buffer) => Promise<object>} [parse] - for tests
77
+ * @param {(raw: Buffer, options: {timeout: number}) => Promise<object>} [parse] - for tests
78
+ * @param {number} [deadline] - Date.now() milliseconds by which every try
79
+ * ends (PARSE_TIMEOUT from now)
29
80
  * @returns {Promise<{mail: object, error: string|null}>}
30
81
  */
31
- export function parseWithin(raw: Buffer, parse?: (raw: Buffer) => Promise<object>): Promise<{
82
+ export function parseWithin(raw: Buffer, parse?: (raw: Buffer, options: {
83
+ timeout: number;
84
+ }) => Promise<object>, deadline?: number): Promise<{
32
85
  mail: object;
33
86
  error: string | null;
34
87
  }>;
@@ -43,4 +96,13 @@ export function parseWithin(raw: Buffer, parse?: (raw: Buffer) => Promise<object
43
96
  * @returns {Promise<Array<object>>}
44
97
  */
45
98
  export function expandAttachments(attachments?: Array<object>, errors?: string[], parse?: (raw: Buffer) => Promise<object>): Promise<Array<object>>;
99
+ /**
100
+ * DKIM-Signature fields mailauth reads of a message (see boundedForAuth): it
101
+ * checks every signature, hashing the body once for each body length (l=)
102
+ * and canonicalization and looking up each signer's key, so hundreds of them
103
+ * would hold up the process and send as many DNS queries. Real mail carries
104
+ * one to three.
105
+ */
106
+ export const MAX_DKIM_SIGNATURES: 8;
107
+ export const PARSE_TIMEOUT: 60000;
46
108
  import { Buffer } from 'node:buffer';
@@ -71,7 +71,8 @@ export function reputationValues(mail?: object, session?: object): string[];
71
71
  * domain itself vouches for it. A DKIM signature of part of the body (l=
72
72
  * shorter than the body, mailauth's underSized) vouches for nothing, nor
73
73
  * does a DMARC pass that rests on one alone, and no signature vouches for a
74
- * From address when DMARC fails.
74
+ * From address when DMARC fails, nor for one of a From field too long to read
75
+ * whole (`mail.from.truncated`).
75
76
  * @param {object} mail - parsed message
76
77
  * @param {object} [session] - remoteAddress, resolvedClientHostname, envelope
77
78
  * @param {object|null} [authentication] - SPF, DKIM and DMARC results
@@ -35,12 +35,14 @@ export function sessionFromQuery(query: URLSearchParams): object;
35
35
  * @param {string} [options.token] - required as "Authorization: Bearer <token>" when set
36
36
  * @param {number} [options.maxSize] - largest message accepted, in bytes
37
37
  * @param {string} [options.modelPath] - where /learn saves the classifier
38
+ * @param {number} [options.maxConnections] - connections kept open, and requests handled, at once
38
39
  * @returns {http.Server}
39
40
  */
40
41
  export function createHttpServer(scanner: import("./index.js").SpamScanner, options?: {
41
42
  token?: string;
42
43
  maxSize?: number;
43
44
  modelPath?: string;
45
+ maxConnections?: number;
44
46
  }): http.Server;
45
47
  /**
46
48
  * A plain TCP server: a client sends a raw message and closes its side; the
@@ -54,12 +56,27 @@ export function createHttpServer(scanner: import("./index.js").SpamScanner, opti
54
56
  * @param {boolean} [options.json] - answer in JSON (default true)
55
57
  * @param {number} [options.maxSize]
56
58
  * @param {number} [options.timeout] - close a connection that sends nothing for this many milliseconds (default 60000)
59
+ * @param {number} [options.maxConnections] - connections kept open at once; more are closed at once
57
60
  * @returns {net.Server}
58
61
  */
59
62
  export function createTcpServer(scanner: import("./index.js").SpamScanner, options?: {
60
63
  json?: boolean;
61
64
  maxSize?: number;
62
65
  timeout?: number;
66
+ maxConnections?: number;
63
67
  }): net.Server;
68
+ /**
69
+ * The largest message the servers and the command line read by default, in
70
+ * bytes (25 MiB): above Postfix's default message_size_limit (10 MB) and the
71
+ * limits of most mail services.
72
+ */
73
+ export const MAX_MESSAGE_SIZE: number;
74
+ /**
75
+ * The most connections each server keeps open at once by default (and, for
76
+ * the HTTP API, requests it handles at once): each may hold a message of up
77
+ * to MAX_MESSAGE_SIZE bytes while it is scanned. Postfix runs at most 100
78
+ * smtpd and 100 cleanup processes by default, each with one milter connection.
79
+ */
80
+ export const MAX_CONNECTIONS: 200;
64
81
  import http from 'node:http';
65
82
  import net from 'node:net';
@@ -43,6 +43,7 @@ export function answer(scanner: import("./index.js").SpamScanner, request: {
43
43
  * @param {string} [options.modelPath] - where TELL saves the classifier
44
44
  * @param {string|null} [options.subjectTag] - tag for spam in PROCESS and HEADERS answers
45
45
  * @param {number} [options.timeout] - close a connection that sends nothing for this many milliseconds (default 60000)
46
+ * @param {number} [options.maxConnections] - connections kept open at once; more are closed at once
46
47
  * @returns {net.Server}
47
48
  */
48
49
  export function createSpamdServer(scanner: import("./index.js").SpamScanner, options?: {
@@ -51,6 +52,7 @@ export function createSpamdServer(scanner: import("./index.js").SpamScanner, opt
51
52
  modelPath?: string;
52
53
  subjectTag?: string | null;
53
54
  timeout?: number;
55
+ maxConnections?: number;
54
56
  }): net.Server;
55
57
  export namespace SPAMD_EXIT_CODES {
56
58
  export { EX_OK };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "spamscanner",
3
3
  "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.",
4
- "version": "7.0.4",
4
+ "version": "7.0.5",
5
5
  "author": "Forward Email (https://forwardemail.net)",
6
6
  "bin": {
7
7
  "spamscanner": "./dist/esm/cli.js"