spamscanner 7.0.2 → 7.0.4

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.
@@ -0,0 +1,11 @@
1
+ import type * as m from './arf.js' with {'resolution-mode': 'import'};
2
+ export declare const parseReportFields: typeof m.parseReportFields;
3
+ export declare const findPart: typeof m.findPart;
4
+ export declare const isArfMessage: typeof m.isArfMessage;
5
+ export declare const parse: typeof m.parse;
6
+ export declare const tryParse: typeof m.tryParse;
7
+ export declare const create: typeof m.create;
8
+ export declare const VALID_FEEDBACK_TYPES: typeof m.VALID_FEEDBACK_TYPES;
9
+ export declare const ArfParser: typeof m.ArfParser;
10
+ declare const _default: typeof m.default;
11
+ export {_default as default};
@@ -32,8 +32,9 @@ export function isArfMessage(parsed: object): boolean;
32
32
  * @returns {Promise<object>} feedbackType, userAgent, version, arrivalDate,
33
33
  * sourceIp, originalMailFrom, originalRcptTo, reportingMta,
34
34
  * originalEnvelopeId, authenticationResults, reportedDomain, reportedUri,
35
- * incidents, humanReadable, originalMessage, originalHeaders (always an
36
- * object of lowercase header name to value) and rawFeedbackReport
35
+ * incidents, humanReadable, originalMessage, originalHeaders (an object of
36
+ * lowercase header name to value, or null when the report has no original
37
+ * message) and rawFeedbackReport
37
38
  * @throws {Error} when the message is not a valid ARF report
38
39
  */
39
40
  export function parse(source: Buffer | string): Promise<object>;
@@ -44,36 +45,38 @@ export function parse(source: Buffer | string): Promise<object>;
44
45
  */
45
46
  export function tryParse(source: Buffer | string): Promise<object | null>;
46
47
  /**
47
- * Write an ARF report.
48
+ * Write an ARF report. (A string message gives a string report, and a
49
+ * Buffer a Buffer, its bytes kept as they are, whatever their character set.)
50
+ * @template {string|Buffer} M
48
51
  * @param {object} options
49
52
  * @param {string} options.feedbackType - abuse, fraud, virus, other, not-spam, ...
50
53
  * @param {string} options.userAgent
51
54
  * @param {string} options.from
52
55
  * @param {string} options.to
53
- * @param {string|Buffer} options.originalMessage
56
+ * @param {M} options.originalMessage
54
57
  * @param {string} [options.humanReadable]
55
58
  * @param {string} [options.subject]
56
59
  * @param {string} [options.sourceIp]
57
60
  * @param {string} [options.originalMailFrom]
58
- * @param {string[]} [options.originalRcptTo]
59
- * @param {Date} [options.arrivalDate]
61
+ * @param {string|string[]} [options.originalRcptTo]
62
+ * @param {Date|string|number} [options.arrivalDate]
60
63
  * @param {string} [options.reportingMta]
61
- * @returns {string}
64
+ * @returns {M extends Buffer ? Buffer : string} the report
62
65
  */
63
- export function create(options?: {
66
+ export function create<M extends string | Buffer>(options?: {
64
67
  feedbackType: string;
65
68
  userAgent: string;
66
69
  from: string;
67
70
  to: string;
68
- originalMessage: string | Buffer;
71
+ originalMessage: M;
69
72
  humanReadable?: string;
70
73
  subject?: string;
71
74
  sourceIp?: string;
72
75
  originalMailFrom?: string;
73
- originalRcptTo?: string[];
74
- arrivalDate?: Date;
76
+ originalRcptTo?: string | string[];
77
+ arrivalDate?: Date | string | number;
75
78
  reportingMta?: string;
76
- }): string;
79
+ }): M extends Buffer ? Buffer : string;
77
80
  /**
78
81
  * Feedback types from RFC 5965, RFC 6591 (auth-failure) and RFC 7489 (dmarc).
79
82
  */
@@ -39,11 +39,15 @@ export function zipEntries(buffer: Buffer, limit?: number): Array<{
39
39
  * - disguised_executable: content is a program but the name says otherwise
40
40
  * - double_extension: "invoice.pdf.exe"
41
41
  * - rtl_override: a right-to-left override hides the real extension (a file named "invoice", U+202E, "fdp.exe" shows as "invoiceexe.pdf")
42
+ * - risky_attachment: a type that is risky by name but not a program
43
+ * (a certificate, an Internet shortcut, a theme; see RISKY_EXTENSIONS)
42
44
  * - executable_in_archive: a ZIP holds a program, read without unpacking
43
45
  * (extension: the first program's, extensions: every program's)
46
+ * - script_in_archive: a ZIP holds scripts (JavaScript, Python, shell, Perl)
47
+ * but no other programs
44
48
  * - encrypted_archive: a password-protected ZIP, which virus scanners cannot open
45
49
  * - macro: an Office document with a VBA project
46
- * - pdf_active: a PDF with JavaScript, launch actions or embedded files
50
+ * - pdf_active: a PDF with JavaScript or launch actions
47
51
  * - rtf_object: an RTF document with an embedded OLE object
48
52
  * - html_attachment: an HTML or SVG file, the usual carrier of fake login pages
49
53
  *
@@ -69,4 +73,12 @@ export function inspectAttachments(attachments?: Array<{
69
73
  * campaigns.
70
74
  */
71
75
  export const EXECUTABLE_EXTENSIONS: Set<string>;
76
+ /**
77
+ * File extensions Outlook blocks that are risky by name rather than programs:
78
+ * certificates (installing one trusts whoever holds its key), Internet
79
+ * shortcuts, Windows themes, and data files people send every day (MATLAB
80
+ * data, Outlook data, temporary files). Reported as risky_attachment, with
81
+ * few points.
82
+ */
83
+ export const RISKY_EXTENSIONS: Set<string>;
72
84
  import { Buffer } from 'node:buffer';
@@ -11,8 +11,11 @@ export function createResolver(timeout?: number, servers?: string[]): (name: str
11
11
  /**
12
12
  * Overall DKIM result from mailauth's per-signature results: "pass" when any
13
13
  * signature passes, "fail" when signatures exist and none passes, "none"
14
- * without signatures. `aligned` is the From domain a passing signature is
15
- * aligned with, if any.
14
+ * without signatures. `aligned` is the signing domain of a passing signature
15
+ * aligned with the From domain (relaxed: they share their registrable domain),
16
+ * in ASCII, if any. A signature whose body length (l=) is shorter than the
17
+ * body (mailauth's `status.underSized`) is aligned with nothing: it signs only
18
+ * the start of the body, and anyone can append to it.
16
19
  * @param {object} dkim
17
20
  * @returns {{status: {result: string, comment: string}, results: object[], aligned: string|null}}
18
21
  */
@@ -26,11 +29,29 @@ export function summarizeDkim(dkim: object): {
26
29
  };
27
30
  /**
28
31
  * Turn mailauth's output into this module's result shape, filling in "none"
29
- * for anything mailauth left out.
32
+ * for anything mailauth left out, and checking what mailauth does not:
33
+ *
34
+ * - the policy (`p`, `sp`) and alignment modes (`adkim`, `aspf`) of the
35
+ * DMARC record are read without spaces and in any case, as RFC 7489 reads
36
+ * them ("p= None" is none, "adkim=S" strict), and strict alignment is
37
+ * enforced: under it only a signature or SPF pass of the From domain itself
38
+ * aligns, and DMARC fails without one;
39
+ * - a message without exactly one From address, whose DMARC mailauth does
40
+ * not check, fails DMARC, as RFC 7489 (6.6.1) allows;
41
+ * - `arc.sealer` is the domain of the newest ARC seal of a passing chain,
42
+ * when its ARC-Message-Signature is of the same domain, and `arc.trusted`
43
+ * whether it is one of `trustedArcSealers` (or under one) and the results
44
+ * it sealed show the message passed DMARC, or DKIM or SPF aligned with the
45
+ * From domain, when it got it.
30
46
  * @param {object} output
47
+ * @param {object} [options]
48
+ * @param {string[]} [options.trustedArcSealers] - forwarders whose ARC seal
49
+ * is believed
31
50
  * @returns {object}
32
51
  */
33
- export function normalizeAuthOutput(output?: object): object;
52
+ export function normalizeAuthOutput(output?: object, options?: {
53
+ trustedArcSealers?: string[];
54
+ }): object;
34
55
  /**
35
56
  * Check SPF, DKIM, DMARC, ARC and BIMI for a message, using mailauth.
36
57
  *
@@ -46,6 +67,10 @@ export function normalizeAuthOutput(output?: object): object;
46
67
  * @param {Function} [options.resolver] - custom DNS resolver (name, type)
47
68
  * @param {number} [options.timeout] - DNS timeout in milliseconds
48
69
  * @param {string[]} [options.dnsServers] - name servers to use
70
+ * @param {string[]} [options.trustedArcSealers] - forwarders (such as a
71
+ * mailing list's domain) whose ARC seal is believed: a message they passed
72
+ * on, which passed when they got it, gets no points for failing SPF, DKIM
73
+ * and DMARC (see normalizeAuthOutput)
49
74
  * @returns {Promise<object>} dkim, spf, dmarc, arc, bimi, receivedChain and
50
75
  * headers (Received-SPF and Authentication-Results, ready to prepend)
51
76
  */
@@ -57,9 +82,20 @@ export function authenticate(message: Buffer | string, options?: {
57
82
  resolver?: Function;
58
83
  timeout?: number;
59
84
  dnsServers?: string[];
85
+ trustedArcSealers?: string[];
60
86
  }): Promise<object>;
61
87
  /**
62
88
  * Score authentication results: failures add points, passes take some away.
89
+ *
90
+ * A DMARC failure scores `dmarcFail` when the domain's policy is quarantine
91
+ * or reject (or not known), and SPF failing as well adds nothing more: DMARC
92
+ * already counts it. When the policy is none it scores `dmarcFailNone`, on
93
+ * top of SPF's points, so a domain with policy none is never easier to
94
+ * spoof than one without DMARC. A DKIM signature of only part of the body
95
+ * (l=) earns no DKIM_PASS, nor a DMARC pass that rests on one alone
96
+ * DMARC_PASS. An ARC seal counts only from a trusted forwarder (`arc.trusted`,
97
+ * see authenticate's trustedArcSealers): its pass gets ARC_PASS, and no
98
+ * points for failing SPF, DKIM or DMARC, as forwarding is what broke them.
63
99
  * @param {object} auth - from authenticate
64
100
  * @param {object} [weights] - overrides for DEFAULT_AUTH_WEIGHTS
65
101
  * @returns {{score: number, tests: Array<{name: string, score: number}>}}
@@ -93,6 +129,7 @@ export namespace DEFAULT_AUTH_WEIGHTS {
93
129
  let spfSoftfail: number;
94
130
  let dmarcPass: number;
95
131
  let dmarcFail: number;
132
+ let dmarcFailNone: number;
96
133
  let arcPass: number;
97
134
  let arcFail: number;
98
135
  }
@@ -17,7 +17,7 @@ export function clamdAddress(options?: object): {
17
17
  * @param {string} [options.socket] - clamd's Unix socket
18
18
  * @param {string} [options.host] - or its TCP host
19
19
  * @param {number} [options.port] - and port (3310)
20
- * @param {number} [options.timeout] - milliseconds
20
+ * @param {number} [options.timeout] - milliseconds for the whole scan
21
21
  * @param {number} [options.chunkSize]
22
22
  * @returns {Promise<{infected: boolean, viruses: string[], reply: string}>}
23
23
  */
@@ -27,6 +27,9 @@ export const MODEL_VERSION: 2;
27
27
  export class Classifier {
28
28
  /**
29
29
  * Load a model written by toJSON. Options passed here override the model's.
30
+ * A model is data that may come from anywhere, so everything in it is
31
+ * checked: a damaged one throws a TypeError starting "Corrupt classifier
32
+ * model".
30
33
  * @param {object|string} json
31
34
  * @param {object} [options]
32
35
  * @returns {Classifier}
@@ -67,7 +70,7 @@ export class Classifier {
67
70
  /**
68
71
  * Score a message.
69
72
  * @param {string[]} features
70
- * @returns {{probability: number, category: 'spam'|'ham'|'unsure', spam: number, ham: number, clues: Array<{feature: string, probability: number}>}}
73
+ * @returns {{probability: number, category: 'spam'|'ham'|'unsure', spam: number, ham: number, clues: Array<{feature: string, probability: number}>, coverage: {feature: string|null, spam: number, ham: number, confidence: number}}}
71
74
  */
72
75
  classify(features: string[]): {
73
76
  probability: number;
@@ -78,6 +81,12 @@ export class Classifier {
78
81
  feature: string;
79
82
  probability: number;
80
83
  }>;
84
+ coverage: {
85
+ feature: string | null;
86
+ spam: number;
87
+ ham: number;
88
+ confidence: number;
89
+ };
81
90
  };
82
91
  /**
83
92
  * How well the training data covers the language of a message, from the
@@ -1,6 +1,20 @@
1
+ export function isLookupName(name: any): boolean;
2
+ /**
3
+ * An IP address as blocklists and allowlists know it: without an IPv6 zone
4
+ * ("fe80::1%eth0"), and an IPv4 address mapped into IPv6 ("::ffff:192.0.2.1",
5
+ * as a dual-stack server reports IPv4 clients) as the IPv4 address, and any
6
+ * other IPv6 address in its canonical form (RFC 5952: "2001:db8::1", and an
7
+ * IPv4 address translated into IPv6 or under the NAT64 prefix in dotted form:
8
+ * "::ffff:0:192.0.2.1", "64:ff9b::192.0.2.1"). Anything
9
+ * that is not an IP address is returned as it is.
10
+ * @param {string} ip
11
+ * @returns {string}
12
+ */
13
+ export function normalizeIp(ip: string): string;
1
14
  /**
2
15
  * The DNS name to query for an IP address in a blocklist zone: reversed
3
16
  * octets for IPv4 ("4.3.2.1.zen.spamhaus.org"), reversed nibbles for IPv6.
17
+ * An IPv4 address mapped into IPv6 is looked up as IPv4.
4
18
  * @param {string} ip
5
19
  * @param {string} zone
6
20
  * @returns {string|null}
@@ -8,8 +22,12 @@
8
22
  export function reverseName(ip: string, zone: string): string | null;
9
23
  /**
10
24
  * Whether an answer means "listed": an address in 127.0.0.0/8 that is not one
11
- * of the error codes lists return when queries are refused (127.255.255.x at
12
- * Spamhaus, 127.0.0.255 at SURBL, 127.0.0.1 at URIBL for blocked resolvers).
25
+ * of the error codes lists return when queries are refused or wrong: at
26
+ * Spamhaus, 127.255.255.x (127.255.255.252 for a misspelled zone,
27
+ * .254 for queries through a public resolver, .255 for too many queries) and
28
+ * 127.0.1.255 (an address looked up in the DBL), 127.0.0.255 anywhere, and
29
+ * 127.0.0.1 at SURBL and URIBL, which answer it to resolvers they block
30
+ * (SpamAssassin's SURBL_BLOCKED and URIBL_BLOCKED).
13
31
  * @param {string} address
14
32
  * @param {string} zone
15
33
  * @returns {boolean}
@@ -69,6 +87,16 @@ export class DnsChecker {
69
87
  * @returns {Promise<string[]>}
70
88
  */
71
89
  resolve4(name: string, servers?: string[] | null): Promise<string[]>;
90
+ /**
91
+ * A records for a name, an empty list when the name does not exist or has
92
+ * no A records, and null when no answer came: the lookup timed out or
93
+ * failed (SERVFAIL, refused, unreachable). Only answers are cached, so a
94
+ * failed lookup is tried again for the next message.
95
+ * @param {string} name
96
+ * @param {string[]|null} [servers]
97
+ * @returns {Promise<string[]|null>}
98
+ */
99
+ answer(name: string, servers?: string[] | null): Promise<string[] | null>;
72
100
  /**
73
101
  * Look an IP address up in IP blocklists.
74
102
  * @param {string} ip
@@ -94,13 +122,15 @@ export class DnsChecker {
94
122
  /**
95
123
  * Ask Cloudflare's filtering resolvers about host names. A name blocked by
96
124
  * the malware resolver is "malware"; one blocked only by the family
97
- * resolver is "adult".
125
+ * resolver is "adult". The hosts that got no answer (from the malware
126
+ * resolver, or from the family resolver when asked) are listed in the
127
+ * result's (non-enumerable) `unanswered` property.
98
128
  * @param {string[]} hosts
99
129
  * @param {object} [options]
100
130
  * @param {boolean} [options.adult] - also ask the family resolver
101
131
  * @param {string[]} [options.malwareServers]
102
132
  * @param {string[]} [options.familyServers]
103
- * @returns {Promise<Array<{host: string, category: 'malware'|'adult'}>>}
133
+ * @returns {Promise<Array<{host: string, category: 'malware'|'adult'}> & {unanswered: string[]}>}
104
134
  */
105
135
  checkCloudflare(hosts?: string[], options?: {
106
136
  adult?: boolean;
@@ -109,5 +139,7 @@ export class DnsChecker {
109
139
  }): Promise<Array<{
110
140
  host: string;
111
141
  category: "malware" | "adult";
112
- }>>;
142
+ }> & {
143
+ unanswered: string[];
144
+ }>;
113
145
  }
@@ -16,8 +16,9 @@ export function foldHeader(name: string, value: string, newline?: string): strin
16
16
  * - X-Spam-Level: one star per point (Sieve: header :contains "X-Spam-Level" "*****")
17
17
  * - X-Spam-Status: Yes or No, with score, threshold, tests and version
18
18
  * - X-Spam-Action: accept, tag or reject
19
+ * - X-Spam-Virus-Scan: failed, when ClamAV could not scan an attachment
19
20
  *
20
- * @param {{score: number, threshold: number, isSpam: boolean, action: string, tests: Array<{name: string}>}} result
21
+ * @param {{score: number, threshold: number, isSpam: boolean, action: string, tests: Array<{name: string}>, results?: {virusScanErrors?: Array<object>}}} result
21
22
  * @param {object} [options]
22
23
  * @param {string} [options.version]
23
24
  * @returns {Array<[string, string]>} header name and value pairs
@@ -30,6 +31,9 @@ export function spamHeaders(result: {
30
31
  tests: Array<{
31
32
  name: string;
32
33
  }>;
34
+ results?: {
35
+ virusScanErrors?: Array<object>;
36
+ };
33
37
  }, options?: {
34
38
  version?: string;
35
39
  }): Array<[string, string]>;
@@ -43,6 +47,31 @@ export function splitMessage(raw: Buffer): {
43
47
  body: Buffer;
44
48
  newline: string;
45
49
  };
50
+ /**
51
+ * A subject tag as it can go in a header: on one line, and, when it is not
52
+ * printable ASCII, holds "=?" (which would read as the start of an encoded
53
+ * word) or a word too long to fold, as encoded words (RFC 2047) of at most
54
+ * 64 characters, split only between whole characters and separated by
55
+ * spaces, so "[垃圾]" survives and a tag cannot add header lines. A tag empty
56
+ * after cleanup is "".
57
+ * @param {string} tag
58
+ * @param {boolean} [spaceAfter] - for a tag before an encoded word: the space
59
+ * after an encoded tag goes inside it, since the space between two encoded
60
+ * words is not shown
61
+ * @returns {string}
62
+ */
63
+ export function subjectTagText(tag: string, spaceAfter?: boolean): string;
64
+ /**
65
+ * A Subject header's value with a tag before it (see subjectTagText), folded
66
+ * between the tag's words so a line with an encoded word stays within 76
67
+ * characters, and before the subject when the line would be too long, or
68
+ * null when the tag is empty or the subject already starts with it.
69
+ * @param {string} tag
70
+ * @param {string} current - the subject's value, as written, perhaps folded
71
+ * @param {string} [newline]
72
+ * @returns {string|null} the value, after "Subject: "
73
+ */
74
+ export function taggedSubject(tag: string, current: string, newline?: string): string | null;
46
75
  /**
47
76
  * Add headers to a raw message and optionally tag its subject.
48
77
  *
@@ -1,3 +1,11 @@
1
+ /**
2
+ * Whether a name is a brand with one letter left out inside it, or with one
3
+ * of its letters doubled.
4
+ * @param {string} name
5
+ * @param {string} brand
6
+ * @returns {boolean}
7
+ */
8
+ export function isDroppedOrDoubled(name: string, brand: string): boolean;
1
9
  /**
2
10
  * Map every lookalike character of a domain label to the Latin letter it
3
11
  * imitates (Cyrillic "рауpаl" becomes "paypal").
@@ -24,6 +32,14 @@ export function editDistance(a: string, b: string, max?: number): number;
24
32
  * @returns {string|null}
25
33
  */
26
34
  export function brandWithPhishingWord(name: string, brands: Set<string>): string | null;
35
+ /**
36
+ * Whether two names differ only by two neighbouring letters swapped (not
37
+ * when they are the same).
38
+ * @param {string} a
39
+ * @param {string} b
40
+ * @returns {boolean}
41
+ */
42
+ export function isSwap(a: string, b: string): boolean;
27
43
  /**
28
44
  * Brands most often imitated in phishing: banks, payment services, mail and
29
45
  * cloud providers, shops, couriers and tax offices. A domain whose name is a
@@ -31,6 +47,7 @@ export function brandWithPhishingWord(name: string, brands: Set<string>): string
31
47
  * never flagged.
32
48
  */
33
49
  export const BRANDS: string[];
50
+ export const PHISHING_WORDS: string[];
34
51
  /**
35
52
  * Detects domains made to look like well-known brands: lookalike letters from
36
53
  * other scripts (Cyrillic "а" for Latin "a"), mixed scripts in one label,
@@ -79,7 +96,7 @@ export default class HomographDetector {
79
96
  recommendations: string[];
80
97
  confidence: number;
81
98
  };
82
- analyse(input: any): {
99
+ analyse(input: any, written?: any): {
83
100
  domain: any;
84
101
  unicode: any;
85
102
  isIDN: boolean;
@@ -12,6 +12,35 @@
12
12
  export function decodeEntities(value: string, { attribute }?: {
13
13
  attribute?: boolean;
14
14
  }): string;
15
+ /**
16
+ * The values of the attributes of an HTML document that may hold words (see
17
+ * NO_WORD_ATTRIBUTES), with their character references decoded, where the
18
+ * tag that holds each starts and ends, and whether it is a form's help text
19
+ * (see FIELD_HELP), in order: not
20
+ * those in comments or in the raw text of scripts and styles, and none after
21
+ * a tag the input ends inside, as browsers read them. In time linear in the
22
+ * input.
23
+ * @param {string} html
24
+ * @returns {Array<{text: string, start: number, end: number, help: boolean}>}
25
+ */
26
+ export function attributeText(html: string): Array<{
27
+ text: string;
28
+ start: number;
29
+ end: number;
30
+ help: boolean;
31
+ }>;
32
+ /**
33
+ * Where a form's help text is in an HTML document (see FIELD_HELP): the
34
+ * values of those attributes and the text of each <label> of at most
35
+ * MAX_HELP characters, as places in the input, in order of where they start
36
+ * (a label's text may hold another's). In time linear in the input.
37
+ * @param {string} html
38
+ * @returns {Array<{from: number, to: number}>}
39
+ */
40
+ export function formHelp(html: string): Array<{
41
+ from: number;
42
+ to: number;
43
+ }>;
15
44
  /**
16
45
  * The text of HTML as a mail client shows it, read in linear time: without
17
46
  * tags, comments, scripts, styles and the title, with character references
@@ -0,0 +1,22 @@
1
+ import type * as m from './index.js' with {'resolution-mode': 'import'};
2
+ type SpamScanner = m.SpamScanner;
3
+ declare const SpamScanner: typeof m.SpamScanner & Omit<typeof m, 'default'> & {default: typeof m.SpamScanner};
4
+ declare namespace SpamScanner {
5
+ export type SpamScanner = m.SpamScanner;
6
+ export type Classifier = m.Classifier;
7
+ export type MilterServer = m.MilterServer;
8
+ export type SmtpSession = m.SmtpSession;
9
+ export type ReputationOptions = m.ReputationOptions;
10
+ export type LegacyOptions = m.LegacyOptions;
11
+ export type SpamScannerOptions = m.SpamScannerOptions;
12
+ export type ScannerSettings = m.ScannerSettings;
13
+ export type ScanOptions = m.ScanOptions;
14
+ export type ScanTest = m.ScanTest;
15
+ export type ScanFinding = m.ScanFinding;
16
+ export type ReputationAnswer = m.ReputationAnswer;
17
+ export type ReputationResult = m.ReputationResult;
18
+ export type ScanDetails = m.ScanDetails;
19
+ export type ScanResult = m.ScanResult;
20
+ export type LLMClassifier = m.LLMClassifier;
21
+ }
22
+ export = SpamScanner;