tsdmarc 0.0.0-stage → 1.0.0
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/CHANGELOG.md +12 -0
- package/LICENSE +201 -0
- package/NOTICE +61 -0
- package/README.md +137 -2
- package/dist/index.d.ts +2700 -0
- package/dist/index.js +3146 -0
- package/dist/index.js.map +1 -0
- package/dist/parallel-worker.d.ts +1 -0
- package/dist/parallel-worker.js +42 -0
- package/dist/parallel-worker.js.map +1 -0
- package/dist/report-file-C6u0I5I1.js +27271 -0
- package/dist/report-file-C6u0I5I1.js.map +1 -0
- package/package.json +92 -3
- package/resources/base_reverse_dns_map.csv +38629 -0
- package/resources/known_unknown_base_reverse_dns.txt +31882 -0
- package/resources/psl_overrides.txt +298 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,2700 @@
|
|
|
1
|
+
import { Socket } from "node:dgram";
|
|
2
|
+
import { Socket as Socket$1 } from "node:net";
|
|
3
|
+
import { ConnectionOptions } from "node:tls";
|
|
4
|
+
type ReportType = "aggregate" | "failure" | "smtp_tls";
|
|
5
|
+
interface AggregateReportMetadata {
|
|
6
|
+
orgName: string;
|
|
7
|
+
orgEmail: string;
|
|
8
|
+
orgExtraContactInfo: string | null;
|
|
9
|
+
reportId: string;
|
|
10
|
+
beginDate: string;
|
|
11
|
+
endDate: string;
|
|
12
|
+
timespanRequiresNormalization: boolean;
|
|
13
|
+
/** A `bigint` only beyond ±2^53 (Python ints are unbounded). */
|
|
14
|
+
originalTimespanSeconds: number | bigint;
|
|
15
|
+
errors: string[];
|
|
16
|
+
generator: string | null;
|
|
17
|
+
}
|
|
18
|
+
interface AggregatePolicyPublished {
|
|
19
|
+
domain: string;
|
|
20
|
+
adkim: string;
|
|
21
|
+
aspf: string;
|
|
22
|
+
p: string;
|
|
23
|
+
sp: string;
|
|
24
|
+
pct: string | null;
|
|
25
|
+
fo: string | null;
|
|
26
|
+
np: string | null;
|
|
27
|
+
testing: string | null;
|
|
28
|
+
discoveryMethod: string | null;
|
|
29
|
+
}
|
|
30
|
+
interface IPSourceInfo {
|
|
31
|
+
ipAddress: string;
|
|
32
|
+
country: string | null;
|
|
33
|
+
reverseDns: string | null;
|
|
34
|
+
baseDomain: string | null;
|
|
35
|
+
name: string | null;
|
|
36
|
+
type: string | null;
|
|
37
|
+
asn: number | null;
|
|
38
|
+
asName: string | null;
|
|
39
|
+
asDomain: string | null;
|
|
40
|
+
}
|
|
41
|
+
/** Same shape as {@link IPSourceInfo}; returned by `getIpAddressInfo()`. */
|
|
42
|
+
type IPAddressInfo = IPSourceInfo;
|
|
43
|
+
interface ReverseDNSService {
|
|
44
|
+
name: string;
|
|
45
|
+
type: string | null;
|
|
46
|
+
}
|
|
47
|
+
/** Base domain → service, loaded from `base_reverse_dns_map.csv`. */
|
|
48
|
+
type ReverseDNSMap = Map<string, ReverseDNSService>;
|
|
49
|
+
interface AggregateAlignment {
|
|
50
|
+
spf: boolean;
|
|
51
|
+
dkim: boolean;
|
|
52
|
+
dmarc: boolean;
|
|
53
|
+
}
|
|
54
|
+
interface AggregateIdentifiers {
|
|
55
|
+
headerFrom: string;
|
|
56
|
+
envelopeFrom: string | null;
|
|
57
|
+
envelopeTo: string | null;
|
|
58
|
+
}
|
|
59
|
+
interface AggregatePolicyOverrideReason {
|
|
60
|
+
type: string | null;
|
|
61
|
+
comment: string | null;
|
|
62
|
+
}
|
|
63
|
+
interface AggregateAuthResultDKIM {
|
|
64
|
+
domain: string;
|
|
65
|
+
result: string;
|
|
66
|
+
selector: string;
|
|
67
|
+
humanResult: string | null;
|
|
68
|
+
}
|
|
69
|
+
interface AggregateAuthResultSPF {
|
|
70
|
+
domain: string;
|
|
71
|
+
result: string;
|
|
72
|
+
scope: string;
|
|
73
|
+
humanResult: string | null;
|
|
74
|
+
}
|
|
75
|
+
interface AggregateAuthResults {
|
|
76
|
+
dkim: AggregateAuthResultDKIM[];
|
|
77
|
+
spf: AggregateAuthResultSPF[];
|
|
78
|
+
}
|
|
79
|
+
interface AggregatePolicyEvaluated {
|
|
80
|
+
disposition: string;
|
|
81
|
+
dkim: string;
|
|
82
|
+
spf: string;
|
|
83
|
+
policyOverrideReasons: AggregatePolicyOverrideReason[];
|
|
84
|
+
}
|
|
85
|
+
interface AggregateRecord {
|
|
86
|
+
intervalBegin: string;
|
|
87
|
+
intervalEnd: string;
|
|
88
|
+
/** True when the record was split into daily buckets. */
|
|
89
|
+
normalizedTimespan: boolean;
|
|
90
|
+
source: IPSourceInfo;
|
|
91
|
+
/** A `bigint` only beyond 2^53 (Python ints are unbounded). */
|
|
92
|
+
count: number | bigint;
|
|
93
|
+
alignment: AggregateAlignment;
|
|
94
|
+
policyEvaluated: AggregatePolicyEvaluated;
|
|
95
|
+
disposition: string;
|
|
96
|
+
identifiers: AggregateIdentifiers;
|
|
97
|
+
authResults: AggregateAuthResults;
|
|
98
|
+
}
|
|
99
|
+
interface AggregateReport {
|
|
100
|
+
xmlSchema: string;
|
|
101
|
+
xmlNamespace: string | null;
|
|
102
|
+
reportMetadata: AggregateReportMetadata;
|
|
103
|
+
policyPublished: AggregatePolicyPublished;
|
|
104
|
+
records: AggregateRecord[];
|
|
105
|
+
}
|
|
106
|
+
interface EmailAddress {
|
|
107
|
+
displayName: string | null;
|
|
108
|
+
address: string;
|
|
109
|
+
local: string | null;
|
|
110
|
+
domain: string | null;
|
|
111
|
+
}
|
|
112
|
+
interface EmailAttachment {
|
|
113
|
+
filename?: string | null;
|
|
114
|
+
/** The filename reduced to a safe basename, or null (mail-parser). */
|
|
115
|
+
safeFilename?: string | null;
|
|
116
|
+
mailContentType?: string | null;
|
|
117
|
+
sha256?: string | null;
|
|
118
|
+
payload?: string;
|
|
119
|
+
contentTransferEncoding?: string;
|
|
120
|
+
binary?: boolean;
|
|
121
|
+
charset?: string | null;
|
|
122
|
+
[key: string]: unknown;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* One Received header of a parsed email, as mail-parser reports it (the
|
|
126
|
+
* first hop first). Clause keys (`from`, `by`, `with`, `id`, `for`, `via`)
|
|
127
|
+
* hold the clause text; `raw` replaces them when the header is unparseable.
|
|
128
|
+
*/
|
|
129
|
+
interface EmailReceivedHop {
|
|
130
|
+
hop: number;
|
|
131
|
+
delay: number;
|
|
132
|
+
date?: string;
|
|
133
|
+
dateUtc?: string;
|
|
134
|
+
envelopeFrom?: string;
|
|
135
|
+
envelopeSender?: string;
|
|
136
|
+
raw?: string;
|
|
137
|
+
[key: string]: unknown;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* A parsed email, in the shape parsedmarc builds from mail-parser's JSON
|
|
141
|
+
* (`parse_email()`). Only the fields parsedmarc relies on are typed; other
|
|
142
|
+
* header-derived keys may be present. `headers` maps raw header names to
|
|
143
|
+
* values and is copied verbatim to the output (its keys are not converted).
|
|
144
|
+
*/
|
|
145
|
+
interface ParsedEmail {
|
|
146
|
+
headers: Record<string, unknown>;
|
|
147
|
+
subject: string | null;
|
|
148
|
+
filenameSafeSubject: string | null;
|
|
149
|
+
date: string | null;
|
|
150
|
+
from: EmailAddress | null;
|
|
151
|
+
to: EmailAddress[];
|
|
152
|
+
cc: EmailAddress[];
|
|
153
|
+
bcc: EmailAddress[];
|
|
154
|
+
replyTo: EmailAddress[];
|
|
155
|
+
deliveredTo?: EmailAddress[];
|
|
156
|
+
attachments: EmailAttachment[];
|
|
157
|
+
body: string | null;
|
|
158
|
+
hasDefects?: boolean;
|
|
159
|
+
defects?: unknown;
|
|
160
|
+
defectsCategories?: unknown;
|
|
161
|
+
received?: EmailReceivedHop[];
|
|
162
|
+
/** Domains of the To and Reply-To addresses. */
|
|
163
|
+
toDomains?: string[];
|
|
164
|
+
/** UTC offset of the Date header in hours (`"+2.0"`). */
|
|
165
|
+
timezone?: string;
|
|
166
|
+
addressHeaderDefects?: unknown[];
|
|
167
|
+
receivedHeaderDefects?: unknown[];
|
|
168
|
+
dateHeaderDefects?: unknown[];
|
|
169
|
+
[key: string]: unknown;
|
|
170
|
+
}
|
|
171
|
+
interface FailureReport {
|
|
172
|
+
feedbackType: string | null;
|
|
173
|
+
userAgent: string | null;
|
|
174
|
+
version: string | null;
|
|
175
|
+
originalEnvelopeId: string | null;
|
|
176
|
+
originalMailFrom: string | null;
|
|
177
|
+
originalRcptTo: string | null;
|
|
178
|
+
arrivalDate: string;
|
|
179
|
+
arrivalDateUtc: string;
|
|
180
|
+
authenticationResults: string | null;
|
|
181
|
+
deliveryResult: string | null;
|
|
182
|
+
authFailure: string[];
|
|
183
|
+
authenticationMechanisms: string[];
|
|
184
|
+
dkimDomain: string | null;
|
|
185
|
+
reportedDomain: string;
|
|
186
|
+
sampleHeadersOnly: boolean;
|
|
187
|
+
source: IPSourceInfo;
|
|
188
|
+
sample: string;
|
|
189
|
+
parsedSample: ParsedEmail;
|
|
190
|
+
/** The `Message-ID` field of the feedback report, when present. */
|
|
191
|
+
messageId?: string;
|
|
192
|
+
/** The `Reported-URI` field of the feedback report, when present. */
|
|
193
|
+
reportedUri?: string;
|
|
194
|
+
/**
|
|
195
|
+
* Every other field of the feedback report is kept, keyed by its name
|
|
196
|
+
* lowercased with `-` → `_` and then camelCased (`Incidents` →
|
|
197
|
+
* `incidents`, `X-Custom-Field` → `xCustomField`).
|
|
198
|
+
*/
|
|
199
|
+
[field: string]: unknown;
|
|
200
|
+
}
|
|
201
|
+
/** Backward-compatible alias of {@link FailureReport}. */
|
|
202
|
+
type ForensicReport = FailureReport;
|
|
203
|
+
interface SMTPTLSFailureDetails {
|
|
204
|
+
resultType: string;
|
|
205
|
+
/** A `bigint` only beyond 2^53 (the report's JSON integer, exact). */
|
|
206
|
+
failedSessionCount: number | bigint;
|
|
207
|
+
sendingMtaIp?: string;
|
|
208
|
+
receivingIp?: string;
|
|
209
|
+
receivingMxHostname?: string;
|
|
210
|
+
receivingMxHelo?: string;
|
|
211
|
+
additionalInfoUri?: string;
|
|
212
|
+
failureReasonCode?: string;
|
|
213
|
+
ipAddress?: string;
|
|
214
|
+
}
|
|
215
|
+
interface SMTPTLSPolicy {
|
|
216
|
+
policyDomain: string;
|
|
217
|
+
policyType: string;
|
|
218
|
+
/** A `bigint` only beyond 2^53 (the report's JSON integer, exact). */
|
|
219
|
+
successfulSessionCount: number | bigint;
|
|
220
|
+
/** A `bigint` only beyond 2^53 (the report's JSON integer, exact). */
|
|
221
|
+
failedSessionCount: number | bigint;
|
|
222
|
+
policyStrings?: string[];
|
|
223
|
+
mxHostPatterns?: string[];
|
|
224
|
+
failureDetails?: SMTPTLSFailureDetails[];
|
|
225
|
+
}
|
|
226
|
+
interface SMTPTLSReport {
|
|
227
|
+
organizationName: string;
|
|
228
|
+
beginDate: string;
|
|
229
|
+
endDate: string;
|
|
230
|
+
contactInfo: string | string[];
|
|
231
|
+
reportId: string;
|
|
232
|
+
policies: SMTPTLSPolicy[];
|
|
233
|
+
}
|
|
234
|
+
interface AggregateParsedReport {
|
|
235
|
+
reportType: "aggregate";
|
|
236
|
+
report: AggregateReport;
|
|
237
|
+
}
|
|
238
|
+
interface FailureParsedReport {
|
|
239
|
+
reportType: "failure";
|
|
240
|
+
report: FailureReport;
|
|
241
|
+
}
|
|
242
|
+
/** Backward-compatible alias of {@link FailureParsedReport}. */
|
|
243
|
+
type ForensicParsedReport = FailureParsedReport;
|
|
244
|
+
interface SMTPTLSParsedReport {
|
|
245
|
+
reportType: "smtp_tls";
|
|
246
|
+
report: SMTPTLSReport;
|
|
247
|
+
}
|
|
248
|
+
type ParsedReport = AggregateParsedReport | FailureParsedReport | SMTPTLSParsedReport;
|
|
249
|
+
interface ParsingResults {
|
|
250
|
+
aggregateReports: AggregateReport[];
|
|
251
|
+
failureReports: FailureReport[];
|
|
252
|
+
smtpTlsReports: SMTPTLSReport[];
|
|
253
|
+
}
|
|
254
|
+
interface ExpiringDictOptions {
|
|
255
|
+
maxLen: number;
|
|
256
|
+
maxAgeSeconds: number;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* An insertion-ordered map whose entries expire after `maxAgeSeconds` and
|
|
260
|
+
* which drops its oldest entry when full. Port of Python's `ExpiringDict`.
|
|
261
|
+
*/
|
|
262
|
+
export declare class ExpiringDict<K, V> {
|
|
263
|
+
readonly maxLen: number;
|
|
264
|
+
readonly maxAgeSeconds: number;
|
|
265
|
+
private readonly entries;
|
|
266
|
+
constructor(options: ExpiringDictOptions);
|
|
267
|
+
private isExpired;
|
|
268
|
+
/**
|
|
269
|
+
* Drop expired entries. Python's `ExpiringDict` only does so for a key
|
|
270
|
+
* being read, so unread entries live as long as the process. Entries are
|
|
271
|
+
* kept in `setAt` order (see `set()`), so the expired ones are at the
|
|
272
|
+
* front and each is visited once.
|
|
273
|
+
*/
|
|
274
|
+
private prune;
|
|
275
|
+
get size(): number;
|
|
276
|
+
has(key: K): boolean;
|
|
277
|
+
get(key: K): V | undefined;
|
|
278
|
+
set(key: K, value: V): this;
|
|
279
|
+
delete(key: K): boolean;
|
|
280
|
+
clear(): void;
|
|
281
|
+
keys(): IterableIterator<K>;
|
|
282
|
+
}
|
|
283
|
+
type IPAddressCache = ExpiringDict<string, IPAddressInfo>;
|
|
284
|
+
type SeenAggregateReportIds = ExpiringDict<string, boolean>;
|
|
285
|
+
/** IP address info cache: 10,000 entries, 4 hour expiry. */
|
|
286
|
+
export declare function newIpAddressCache(): IPAddressCache;
|
|
287
|
+
/** Seen aggregate report ID cache: 100,000,000 entries, 1 hour expiry. */
|
|
288
|
+
export declare function newSeenAggregateReportIds(): SeenAggregateReportIds;
|
|
289
|
+
/** Module-default IP address info cache. */
|
|
290
|
+
export declare const IP_ADDRESS_CACHE: IPAddressCache;
|
|
291
|
+
/** Module-default seen aggregate report ID cache. */
|
|
292
|
+
export declare const SEEN_AGGREGATE_REPORT_IDS: SeenAggregateReportIds;
|
|
293
|
+
/** Module-default reverse DNS map. */
|
|
294
|
+
export declare const REVERSE_DNS_MAP: ReverseDNSMap;
|
|
295
|
+
/** Parsing and enrichment options (everything except the caches). */
|
|
296
|
+
interface ParserOptions {
|
|
297
|
+
/** Do not make online requests (DNS, IP database, reverse DNS map, PSL overrides). */
|
|
298
|
+
offline: boolean;
|
|
299
|
+
/** Path to a local MMDB file from IPinfo, MaxMind or DBIP. */
|
|
300
|
+
ipDbPath: string | null;
|
|
301
|
+
/** Always use local/bundled files instead of downloading them. */
|
|
302
|
+
alwaysUseLocalFiles: boolean;
|
|
303
|
+
/** Path to a local reverse DNS map file. */
|
|
304
|
+
reverseDnsMapPath: string | null;
|
|
305
|
+
/** URL of a reverse DNS map file. */
|
|
306
|
+
reverseDnsMapUrl: string | null;
|
|
307
|
+
/** Path to a local PSL overrides file. */
|
|
308
|
+
pslOverridesPath: string | null;
|
|
309
|
+
/** URL of a PSL overrides file. */
|
|
310
|
+
pslOverridesUrl: string | null;
|
|
311
|
+
/** Nameservers for DNS queries (`null`: Cloudflare's public resolvers). */
|
|
312
|
+
nameservers: string[] | null;
|
|
313
|
+
/** DNS query timeout, in seconds. */
|
|
314
|
+
dnsTimeout: number;
|
|
315
|
+
/** Times to retry a DNS query after a timeout or other transient error. */
|
|
316
|
+
dnsRetries: number;
|
|
317
|
+
/** Remove attachment payloads from parsed email results. */
|
|
318
|
+
stripAttachmentPayloads: boolean;
|
|
319
|
+
/** Aggregate reports spanning more hours than this are normalized/split. */
|
|
320
|
+
normalizeTimespanThresholdHours: number;
|
|
321
|
+
/**
|
|
322
|
+
* Largest email accepted, in bytes (UTF-8 for text input); `0` disables
|
|
323
|
+
* the check. Larger emails raise `EmailTooLargeError` and mailboxes move
|
|
324
|
+
* them to the invalid folder, without downloading them where the backend
|
|
325
|
+
* reports the size first. Not in parsedmarc. Default: 50 MiB.
|
|
326
|
+
*/
|
|
327
|
+
maxEmailSize: number;
|
|
328
|
+
/**
|
|
329
|
+
* Most MIME parts accepted in an email; `0` disables the check. Lines
|
|
330
|
+
* starting with `--` (an upper bound on boundary parts) are counted before
|
|
331
|
+
* parsing, and the parser counts every message it creates, so parts that
|
|
332
|
+
* need no boundary line count too. Not in parsedmarc. Default: 10,000.
|
|
333
|
+
*/
|
|
334
|
+
maxMimeParts: number;
|
|
335
|
+
/**
|
|
336
|
+
* Most header lines accepted in an email, over all its parts (folded
|
|
337
|
+
* lines included); `0` disables the check. Not in parsedmarc.
|
|
338
|
+
* Default: 100,000.
|
|
339
|
+
*/
|
|
340
|
+
maxHeaderLines: number;
|
|
341
|
+
/**
|
|
342
|
+
* Most text, in characters, that re-serializing the parts of one email
|
|
343
|
+
* may produce; text with a character outside Latin-1 counts each of its
|
|
344
|
+
* characters twice, as it takes twice the memory. Every part is counted
|
|
345
|
+
* once per level it is flattened at, so this bounds size × nesting
|
|
346
|
+
* depth, which `maxEmailSize` does not; `0` disables the check. Not in
|
|
347
|
+
* parsedmarc. Default: 512 MiB.
|
|
348
|
+
*/
|
|
349
|
+
maxExpandedEmailSize: number;
|
|
350
|
+
/**
|
|
351
|
+
* Most source IPs of one aggregate report looked up over the network
|
|
352
|
+
* (reverse DNS, IPinfo API); `0` disables the check. Later IPs get no
|
|
353
|
+
* reverse DNS and only the IP database. Cached IPs don't count. Not in
|
|
354
|
+
* parsedmarc. Default: 100,000.
|
|
355
|
+
*/
|
|
356
|
+
maxLookupsPerReport: number;
|
|
357
|
+
/**
|
|
358
|
+
* Most reverse DNS lookups of one aggregate report that may time out or
|
|
359
|
+
* fail (an address with no PTR is an answer, not a failure); `0`
|
|
360
|
+
* disables the check. Later IPs are handled as in
|
|
361
|
+
* {@link ParserOptions.maxLookupsPerReport}. A failed lookup can take
|
|
362
|
+
* seconds, and the report's sender can choose addresses that fail. Not
|
|
363
|
+
* in parsedmarc. Default: 256.
|
|
364
|
+
*/
|
|
365
|
+
maxFailedLookupsPerReport: number;
|
|
366
|
+
/**
|
|
367
|
+
* Seconds of network lookups (reverse DNS, IPinfo API) one aggregate
|
|
368
|
+
* report may use in total; `0` disables the check. Later IPs are handled
|
|
369
|
+
* as in {@link ParserOptions.maxLookupsPerReport}. An answer that is not a
|
|
370
|
+
* failure can still be slow, and a sender can make every record cost one.
|
|
371
|
+
* Not in parsedmarc. Default: 600.
|
|
372
|
+
*/
|
|
373
|
+
maxLookupTimePerReport: number;
|
|
374
|
+
/**
|
|
375
|
+
* Most records an aggregate report may have once its timespan is
|
|
376
|
+
* normalized into daily buckets; `0` disables the check. Sender-chosen
|
|
377
|
+
* dates and counts turn one `<record>` into one record per day, for up to
|
|
378
|
+
* 2.9 million days. A record with few messages only takes as many days as
|
|
379
|
+
* it has messages. Larger reports raise `InvalidAggregateReport`. Not in
|
|
380
|
+
* parsedmarc. Default: 250,000.
|
|
381
|
+
*/
|
|
382
|
+
maxNormalizedRecords: number;
|
|
383
|
+
/**
|
|
384
|
+
* Deepest element nesting accepted in an aggregate report's XML; `0`
|
|
385
|
+
* disables the check. Deeper reports raise `InvalidAggregateReport`. Not
|
|
386
|
+
* in parsedmarc. Default: 100,000.
|
|
387
|
+
*/
|
|
388
|
+
maxXmlDepth: number;
|
|
389
|
+
/**
|
|
390
|
+
* Most elements and attributes accepted in an aggregate report's XML; `0`
|
|
391
|
+
* disables the check. Larger reports raise `InvalidAggregateReport`. Not
|
|
392
|
+
* in parsedmarc. Default: 5,000,000.
|
|
393
|
+
*/
|
|
394
|
+
maxXmlNodes: number;
|
|
395
|
+
/**
|
|
396
|
+
* Largest SMTP TLS report accepted, in bytes of JSON (UTF-16 code units
|
|
397
|
+
* for text input); `0` disables the check. Larger reports raise
|
|
398
|
+
* `InvalidSMTPTLSReport`. Not in parsedmarc. Default: 10 MiB.
|
|
399
|
+
*/
|
|
400
|
+
maxSmtpTlsReportSize: number;
|
|
401
|
+
/**
|
|
402
|
+
* Seconds one email or file may take to parse; `0` disables the check. A
|
|
403
|
+
* worker past it is stopped. Without workers (`nProcs` 1) only a mailbox
|
|
404
|
+
* applies it, at the next keepalive (every 20 records), so only time
|
|
405
|
+
* spent awaiting counts. A mailbox leaves such a message for retry and
|
|
406
|
+
* then moves it to the `Unsaved` folder, without ever deleting it. Not
|
|
407
|
+
* in parsedmarc. Default: 1 hour.
|
|
408
|
+
*/
|
|
409
|
+
jobTimeout: number;
|
|
410
|
+
/**
|
|
411
|
+
* JavaScript heap limit of each parsing worker thread, in MiB; `0` keeps
|
|
412
|
+
* the runtime's limit (about 4 GiB per worker on 64-bit Node). A worker
|
|
413
|
+
* over it dies and only its job fails: a mailbox leaves the message for
|
|
414
|
+
* retry and then moves it to the `Unsaved` folder, without ever deleting
|
|
415
|
+
* it. Bun ignores it. Not in parsedmarc. Default: 2 GiB.
|
|
416
|
+
*/
|
|
417
|
+
maxWorkerHeapMb: number;
|
|
418
|
+
}
|
|
419
|
+
/** Default {@link ParserOptions.maxEmailSize}: 50 MiB. */
|
|
420
|
+
export declare const DEFAULT_MAX_EMAIL_SIZE: number;
|
|
421
|
+
/** Default {@link ParserOptions.maxMimeParts}. */
|
|
422
|
+
export declare const DEFAULT_MAX_MIME_PARTS = 1e4;
|
|
423
|
+
/** Default {@link ParserOptions.maxHeaderLines}. */
|
|
424
|
+
export declare const DEFAULT_MAX_HEADER_LINES = 1e5;
|
|
425
|
+
/** Default {@link ParserOptions.maxExpandedEmailSize}: 512 MiB. */
|
|
426
|
+
export declare const DEFAULT_MAX_EXPANDED_EMAIL_SIZE: number;
|
|
427
|
+
/** Default {@link ParserOptions.maxNormalizedRecords}. */
|
|
428
|
+
export declare const DEFAULT_MAX_NORMALIZED_RECORDS = 25e4;
|
|
429
|
+
/** Default {@link ParserOptions.maxXmlDepth}. */
|
|
430
|
+
export declare const DEFAULT_MAX_XML_DEPTH = 1e5;
|
|
431
|
+
/** Default {@link ParserOptions.maxXmlNodes}. */
|
|
432
|
+
export declare const DEFAULT_MAX_XML_NODES = 5e6;
|
|
433
|
+
/** Default {@link ParserOptions.maxSmtpTlsReportSize}: 10 MiB. */
|
|
434
|
+
export declare const DEFAULT_MAX_SMTP_TLS_REPORT_SIZE: number;
|
|
435
|
+
/** Default {@link ParserOptions.jobTimeout}: one hour. */
|
|
436
|
+
export declare const DEFAULT_JOB_TIMEOUT = 3600;
|
|
437
|
+
/** Default {@link ParserOptions.maxWorkerHeapMb}. */
|
|
438
|
+
export declare const DEFAULT_MAX_WORKER_HEAP_MB = 2048;
|
|
439
|
+
/** Default {@link ParserOptions.maxLookupsPerReport}. */
|
|
440
|
+
export declare const DEFAULT_MAX_LOOKUPS_PER_REPORT = 1e5;
|
|
441
|
+
/** Default {@link ParserOptions.maxFailedLookupsPerReport}. */
|
|
442
|
+
export declare const DEFAULT_MAX_FAILED_LOOKUPS_PER_REPORT = 256;
|
|
443
|
+
/** Default {@link ParserOptions.maxLookupTimePerReport}, in seconds. */
|
|
444
|
+
export declare const DEFAULT_MAX_LOOKUP_TIME_PER_REPORT = 600;
|
|
445
|
+
/** The limits on an email's structure, applied while parsing it. */
|
|
446
|
+
type EmailLimits = Pick<ParserOptions, "maxMimeParts" | "maxHeaderLines" | "maxExpandedEmailSize">;
|
|
447
|
+
interface ParserCaches {
|
|
448
|
+
ipAddressCache: IPAddressCache;
|
|
449
|
+
/** Seen aggregate reports, keyed on reporter, Report-ID and policy domain (`aggregateReportKey()`). */
|
|
450
|
+
seenAggregateReportIds: SeenAggregateReportIds;
|
|
451
|
+
reverseDnsMap: ReverseDNSMap;
|
|
452
|
+
}
|
|
453
|
+
/** Immutable parser configuration: options plus shared caches. */
|
|
454
|
+
type ParserConfig = Readonly<ParserOptions & ParserCaches>;
|
|
455
|
+
export declare const DEFAULT_PARSER_OPTIONS: Readonly<ParserOptions>;
|
|
456
|
+
/** Build a config. Caches not given are fresh and isolated. */
|
|
457
|
+
export declare function createParserConfig(options?: Partial<ParserOptions & ParserCaches>): ParserConfig;
|
|
458
|
+
/** Derive a variant of `config` that keeps sharing its caches. */
|
|
459
|
+
export declare function replaceParserConfig(config: ParserConfig, changes: Partial<ParserOptions & ParserCaches>): ParserConfig;
|
|
460
|
+
/** Options accepted by public parsing functions (individual options or `config`). */
|
|
461
|
+
interface ParserConfigInput extends Partial<Omit<ParserOptions, "pslOverridesPath" | "pslOverridesUrl">> {
|
|
462
|
+
/** When given, the individual options are ignored. */
|
|
463
|
+
config?: ParserConfig | null;
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Resolve the config for a public parsing call: `input.config` when given,
|
|
467
|
+
* otherwise a config built from the individual options and bound to the
|
|
468
|
+
* module-default caches. PSL override options are only settable through an
|
|
469
|
+
* explicit `config`. The module-default caches ignore the options, so calls
|
|
470
|
+
* with different `offline`, `nameservers` or `ipDbPath` values, or for
|
|
471
|
+
* different tenants, need a `createParserConfig()` each.
|
|
472
|
+
*/
|
|
473
|
+
export declare function resolveParserConfig(input?: ParserConfigInput): ParserConfig;
|
|
474
|
+
/** The options of a config, without caches, for sending to a worker. */
|
|
475
|
+
export declare function parserConfigToState(config: ParserConfig): ParserOptions;
|
|
476
|
+
/**
|
|
477
|
+
* Rebuild a config from worker state. Missing options take their defaults
|
|
478
|
+
* and the caches bind to this thread's module-default caches.
|
|
479
|
+
*/
|
|
480
|
+
export declare function parserConfigFromState(state: Partial<ParserOptions>): ParserConfig;
|
|
481
|
+
type CsvRow = Record<string, unknown>;
|
|
482
|
+
interface CsvOptions {
|
|
483
|
+
/**
|
|
484
|
+
* Neutralize spreadsheet formulas (CSV injection): a text cell starting
|
|
485
|
+
* with `=`, `+`, `-`, `@`, a tab or a carriage return gets a leading `'`,
|
|
486
|
+
* so Excel, LibreOffice or Google Sheets show it as text instead of
|
|
487
|
+
* evaluating it. Report data comes from untrusted senders. On by
|
|
488
|
+
* default, like parsedmarc 11.1.0 (`_csv_safe`); `false`
|
|
489
|
+
* writes cells unchanged. Numbers (a negative count) are never changed.
|
|
490
|
+
*/
|
|
491
|
+
formulaEscape?: boolean;
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* A text cell with a leading `'` when a spreadsheet would read it as a
|
|
495
|
+
* formula (see {@link CsvOptions.formulaEscape}); other values unchanged.
|
|
496
|
+
*/
|
|
497
|
+
export declare function csvFormulaEscape(value: unknown): unknown;
|
|
498
|
+
/** Incremental writer, like `csv.DictWriter` over a `StringIO`. */
|
|
499
|
+
export declare class CsvDictWriter {
|
|
500
|
+
readonly fieldnames: readonly string[];
|
|
501
|
+
readonly formulaEscape: boolean;
|
|
502
|
+
private text;
|
|
503
|
+
constructor(fieldnames: readonly string[], options?: CsvOptions);
|
|
504
|
+
writeHeader(): void;
|
|
505
|
+
/**
|
|
506
|
+
* Write one row. Missing fields are empty; a field not in `fieldnames`
|
|
507
|
+
* throws, like `DictWriter`'s default `extrasaction="raise"`.
|
|
508
|
+
*/
|
|
509
|
+
writeRow(row: CsvRow): void;
|
|
510
|
+
getValue(): string;
|
|
511
|
+
}
|
|
512
|
+
/** Header plus rows, as one CSV string. */
|
|
513
|
+
export declare function rowsToCsv(fieldnames: readonly string[], rows: Iterable<CsvRow>, options?: CsvOptions): string;
|
|
514
|
+
/**
|
|
515
|
+
* Strip NUL characters from a string cell (parsedmarc's `_csv_safe`).
|
|
516
|
+
* Non-string values are returned unchanged.
|
|
517
|
+
*/
|
|
518
|
+
export declare function csvSafe(value: unknown): unknown;
|
|
519
|
+
/** Largest decompressed report accepted, in bytes. */
|
|
520
|
+
export declare const MAX_DECOMPRESSED_REPORT_SIZE: number;
|
|
521
|
+
/**
|
|
522
|
+
* Largest decompressed zip member accepted for the bzip2, LZMA and
|
|
523
|
+
* Zstandard methods (12, 14, 93), in bytes. Their pure-TypeScript decoders
|
|
524
|
+
* are much slower than inflate, and a 1 KB bzip2 member can expand to the
|
|
525
|
+
* 100 MiB cap, costing seconds of CPU per attachment. Report senders use
|
|
526
|
+
* deflate (RFC 7489 asks for zip or gzip), so these methods only need to
|
|
527
|
+
* handle small reports. Not in parsedmarc.
|
|
528
|
+
*/
|
|
529
|
+
export declare const MAX_DECOMPRESSED_SLOW_CODEC_SIZE: number;
|
|
530
|
+
interface ExtractOptions {
|
|
531
|
+
/** Override the decompression cap (tests); defaults to 100 MiB. */
|
|
532
|
+
maxDecompressedSize?: number;
|
|
533
|
+
/**
|
|
534
|
+
* Override the cap for bzip2, LZMA and Zstandard zip members (tests);
|
|
535
|
+
* defaults to 10 MiB, and never exceeds `maxDecompressedSize`.
|
|
536
|
+
*/
|
|
537
|
+
maxSlowCodecSize?: number;
|
|
538
|
+
/**
|
|
539
|
+
* Largest stream or file read, in bytes; `0` disables the check. Defaults
|
|
540
|
+
* to 100 MiB, the largest report once decompressed. Not in parsedmarc.
|
|
541
|
+
*/
|
|
542
|
+
maxInputSize?: number;
|
|
543
|
+
/** Told how many bytes were decompressed (not called for plain content). */
|
|
544
|
+
onDecompressed?: (size: number) => void;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Decompress the first member of a gzip stream, refusing to produce more
|
|
548
|
+
* than `limit` bytes (parsedmarc's `_decompress_gzip_bounded`). Corrupt
|
|
549
|
+
* data raises zlib's errors (`Error -3 while decompressing data: ...`);
|
|
550
|
+
* trailing bytes after the first member are ignored.
|
|
551
|
+
*/
|
|
552
|
+
export declare function decompressGzipBounded(data: Uint8Array, limit?: number): Uint8Array;
|
|
553
|
+
/**
|
|
554
|
+
* Extract report text from zip- or gzip-compressed content, or return
|
|
555
|
+
* plain XML/JSON content decoded as-is. A string is first decoded as strict
|
|
556
|
+
* base64; a string that is not valid base64 is returned unchanged.
|
|
557
|
+
*/
|
|
558
|
+
export declare function extractReport(content: string | Uint8Array | ArrayBuffer, options?: ExtractOptions): string;
|
|
559
|
+
type ReportStream = AsyncIterable<Uint8Array | string> | ReadableStream<Uint8Array>;
|
|
560
|
+
/**
|
|
561
|
+
* Read a whole stream and extract the report from it. A stream that yields
|
|
562
|
+
* text instead of bytes is rejected, like a Python file opened in text mode.
|
|
563
|
+
*/
|
|
564
|
+
export declare function extractReportFromStream(stream: ReportStream, options?: ExtractOptions): Promise<string>;
|
|
565
|
+
/** Extract the report from a file. */
|
|
566
|
+
export declare function extractReportFromFilePath(filePath: string, options?: ExtractOptions): Promise<string>;
|
|
567
|
+
interface PyDateTimeFields {
|
|
568
|
+
year: number;
|
|
569
|
+
month: number;
|
|
570
|
+
day: number;
|
|
571
|
+
hour?: number;
|
|
572
|
+
minute?: number;
|
|
573
|
+
second?: number;
|
|
574
|
+
microsecond?: number;
|
|
575
|
+
/** Minutes east of UTC; `null` (default) for a naive datetime. */
|
|
576
|
+
utcOffsetMinutes?: number | null;
|
|
577
|
+
}
|
|
578
|
+
export declare class PyDateTime {
|
|
579
|
+
readonly year: number;
|
|
580
|
+
readonly month: number;
|
|
581
|
+
readonly day: number;
|
|
582
|
+
readonly hour: number;
|
|
583
|
+
readonly minute: number;
|
|
584
|
+
readonly second: number;
|
|
585
|
+
readonly microsecond: number;
|
|
586
|
+
readonly utcOffsetMinutes: number | null;
|
|
587
|
+
constructor(fields: PyDateTimeFields);
|
|
588
|
+
/** Naive datetime in local time from an epoch instant (ms precision). */
|
|
589
|
+
static fromDateLocal(date: Date, microsecond?: number): PyDateTime;
|
|
590
|
+
/** Aware datetime with the given offset, from an epoch instant. */
|
|
591
|
+
static fromDateWithOffset(date: Date, utcOffsetMinutes: number, microsecond?: number): PyDateTime;
|
|
592
|
+
/**
|
|
593
|
+
* Python's `datetime.fromtimestamp(seconds)` (naive, local time), or
|
|
594
|
+
* `datetime.fromtimestamp(seconds, timezone.utc)` with `utc: true`.
|
|
595
|
+
*/
|
|
596
|
+
static fromTimestamp(seconds: number, utc?: boolean): PyDateTime;
|
|
597
|
+
/** Python's `datetime.now()` (naive local) or `datetime.now(timezone.utc)`. */
|
|
598
|
+
static now(utc?: boolean): PyDateTime;
|
|
599
|
+
get isNaive(): boolean;
|
|
600
|
+
/** The instant this datetime denotes (naive values are read as local time). */
|
|
601
|
+
toDate(): Date;
|
|
602
|
+
/** POSIX timestamp in seconds (Python's `datetime.timestamp()`). */
|
|
603
|
+
timestamp(): number;
|
|
604
|
+
/** Python's `astimezone(timezone.utc)`. */
|
|
605
|
+
toUtc(): PyDateTime;
|
|
606
|
+
/** Python's `astimezone()` with no argument: aware, in local time. */
|
|
607
|
+
toLocal(): PyDateTime;
|
|
608
|
+
/** Python's `replace(tzinfo=...)`: same wall clock, new (or no) offset. */
|
|
609
|
+
withOffset(utcOffsetMinutes: number | null): PyDateTime;
|
|
610
|
+
/** Python's `replace(...)` for wall-clock fields. */
|
|
611
|
+
replace(changes: Partial<PyDateTimeFields>): PyDateTime;
|
|
612
|
+
/** Add a duration in seconds (Python's `+ timedelta(seconds=...)`). */
|
|
613
|
+
addSeconds(seconds: number): PyDateTime;
|
|
614
|
+
/** Midnight of the same day (Python's `datetime.combine(dt.date(), time())`). */
|
|
615
|
+
startOfDay(): PyDateTime;
|
|
616
|
+
/**
|
|
617
|
+
* Seconds from `other` to this datetime (Python's `(self - other)
|
|
618
|
+
* .total_seconds()`). Both must be naive or both aware, as in Python.
|
|
619
|
+
*/
|
|
620
|
+
secondsSince(other: PyDateTime): number;
|
|
621
|
+
/** Negative, zero or positive, like comparing Python datetimes. */
|
|
622
|
+
compare(other: PyDateTime): number;
|
|
623
|
+
equals(other: PyDateTime): boolean;
|
|
624
|
+
private wallClockMicros;
|
|
625
|
+
fields(): PyDateTimeFields;
|
|
626
|
+
/** Day of the week, Monday = 0 (Python's `weekday()`). */
|
|
627
|
+
weekday(): number;
|
|
628
|
+
/**
|
|
629
|
+
* Python's `strftime` for the directives parsedmarc uses: `%Y %y %m %d %H
|
|
630
|
+
* %I %M %S %f %p %z %Z %a %A %b %B %j %%`.
|
|
631
|
+
*/
|
|
632
|
+
strftime(format: string): string;
|
|
633
|
+
private directive;
|
|
634
|
+
/** Python's `isoformat(sep)`: `2024-01-01T00:00:00[.ffffff][+00:00]`. */
|
|
635
|
+
isoformat(separator?: string): string;
|
|
636
|
+
/** Python's `str(datetime)`: ISO format with a space separator. */
|
|
637
|
+
toString(): string;
|
|
638
|
+
/** JSON form, matching Python's `json.dumps(..., default=str)`. */
|
|
639
|
+
toJSON(): string;
|
|
640
|
+
}
|
|
641
|
+
/** Base class of every DNS error (dnspython's `dns.exception.DNSException`). */
|
|
642
|
+
export declare class DNSException extends Error {
|
|
643
|
+
override name: string;
|
|
644
|
+
}
|
|
645
|
+
/** The DNS operation timed out (`dns.exception.Timeout`). */
|
|
646
|
+
export declare class DNSTimeout extends DNSException {
|
|
647
|
+
override name: string;
|
|
648
|
+
/** Seconds waited, when known. */
|
|
649
|
+
readonly timeout: number | null;
|
|
650
|
+
constructor(timeout?: number | null);
|
|
651
|
+
}
|
|
652
|
+
/** One failed attempt against one nameserver during a resolution. */
|
|
653
|
+
interface ResolutionError {
|
|
654
|
+
/** The nameserver, as dnspython prints it (`Do53:1.1.1.1@53`, a URL, ...). */
|
|
655
|
+
nameserver: string;
|
|
656
|
+
/** Whether the attempt used TCP (or a stream transport). */
|
|
657
|
+
tcp: boolean;
|
|
658
|
+
port: number;
|
|
659
|
+
/** The error raised, or the response code text (`SERVFAIL`). */
|
|
660
|
+
error: unknown;
|
|
661
|
+
}
|
|
662
|
+
/** The resolution lifetime expired (`dns.resolver.LifetimeTimeout`). */
|
|
663
|
+
export declare class LifetimeTimeout extends DNSTimeout {
|
|
664
|
+
override name: string;
|
|
665
|
+
readonly errors: readonly ResolutionError[];
|
|
666
|
+
constructor(timeout: number, errors: readonly ResolutionError[]);
|
|
667
|
+
}
|
|
668
|
+
/** DNS message is malformed (`dns.exception.FormError`). */
|
|
669
|
+
export declare class DNSFormError extends DNSException {
|
|
670
|
+
override name: string;
|
|
671
|
+
constructor(message?: string);
|
|
672
|
+
}
|
|
673
|
+
/** A DNS response does not respond to the question asked (`dns.query.BadResponse`). */
|
|
674
|
+
export declare class BadResponse extends DNSFormError {
|
|
675
|
+
override name: string;
|
|
676
|
+
constructor(message?: string);
|
|
677
|
+
}
|
|
678
|
+
/** Text input is malformed (`dns.exception.SyntaxError`). */
|
|
679
|
+
export declare class DNSSyntaxError extends DNSException {
|
|
680
|
+
override name: string;
|
|
681
|
+
constructor(message?: string);
|
|
682
|
+
}
|
|
683
|
+
/** An escaped code in a name is invalid (`dns.name.BadEscape`). */
|
|
684
|
+
export declare class BadEscape extends DNSSyntaxError {
|
|
685
|
+
override name: string;
|
|
686
|
+
constructor();
|
|
687
|
+
}
|
|
688
|
+
/** IDNA encoding failed (`dns.name.IDNAException`). */
|
|
689
|
+
export declare class IDNAException extends DNSException {
|
|
690
|
+
override name: string;
|
|
691
|
+
constructor(detail: string);
|
|
692
|
+
}
|
|
693
|
+
/** A DNS label is empty (`dns.name.EmptyLabel`). */
|
|
694
|
+
export declare class EmptyLabel extends DNSSyntaxError {
|
|
695
|
+
override name: string;
|
|
696
|
+
constructor();
|
|
697
|
+
}
|
|
698
|
+
/** A DNS label is > 63 octets long (`dns.name.LabelTooLong`). */
|
|
699
|
+
export declare class LabelTooLong extends DNSSyntaxError {
|
|
700
|
+
override name: string;
|
|
701
|
+
constructor();
|
|
702
|
+
}
|
|
703
|
+
/** A DNS name is > 255 octets long (`dns.name.NameTooLong`). */
|
|
704
|
+
export declare class NameTooLong extends DNSFormError {
|
|
705
|
+
override name: string;
|
|
706
|
+
constructor();
|
|
707
|
+
}
|
|
708
|
+
/** The DNS query name does not exist (`dns.resolver.NXDOMAIN`). */
|
|
709
|
+
export declare class NXDOMAIN extends DNSException {
|
|
710
|
+
override name: string;
|
|
711
|
+
/** The names that were tried, in text form with the final dot. */
|
|
712
|
+
readonly qnames: readonly string[];
|
|
713
|
+
constructor(qnames: readonly string[]);
|
|
714
|
+
}
|
|
715
|
+
/** The DNS query name is too long after DNAME substitution (`dns.resolver.YXDOMAIN`). */
|
|
716
|
+
export declare class YXDOMAIN extends DNSException {
|
|
717
|
+
override name: string;
|
|
718
|
+
constructor();
|
|
719
|
+
}
|
|
720
|
+
/** The DNS response does not contain an answer to the question (`dns.resolver.NoAnswer`). */
|
|
721
|
+
export declare class NoAnswer extends DNSException {
|
|
722
|
+
override name: string;
|
|
723
|
+
/** @param question The question as dnspython prints it (`example.com. IN TXT`). */
|
|
724
|
+
constructor(question: string);
|
|
725
|
+
}
|
|
726
|
+
/** All nameservers failed to answer the query (`dns.resolver.NoNameservers`). */
|
|
727
|
+
export declare class NoNameservers extends DNSException {
|
|
728
|
+
override name: string;
|
|
729
|
+
readonly errors: readonly ResolutionError[];
|
|
730
|
+
/** @param question The question as dnspython prints it (`example.com. IN TXT`). */
|
|
731
|
+
constructor(question: string, errors: readonly ResolutionError[]);
|
|
732
|
+
}
|
|
733
|
+
/** DNS metaqueries are not allowed (`dns.resolver.NoMetaqueries`). */
|
|
734
|
+
export declare class NoMetaqueries extends DNSException {
|
|
735
|
+
override name: string;
|
|
736
|
+
constructor();
|
|
737
|
+
}
|
|
738
|
+
/** DNS resource record type is unknown (`dns.rdatatype.UnknownRdatatype`). */
|
|
739
|
+
export declare class UnknownRdatatype extends DNSException {
|
|
740
|
+
override name: string;
|
|
741
|
+
constructor(message?: string);
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Module-level state the loaders write (parsedmarc's `_IP_DB_PATH`,
|
|
745
|
+
* `_LAST_LOGGED_IP_DB_PATH` and `_IPINFO_API_TOKEN` globals). Exposed so a
|
|
746
|
+
* CLI can snapshot and restore it around a configuration reload.
|
|
747
|
+
*/
|
|
748
|
+
interface IpInfoState {
|
|
749
|
+
/** The database selected by `loadIpDb()` (downloaded, cached or bundled). */
|
|
750
|
+
ipDbPath: string | null;
|
|
751
|
+
/** The last database path logged, so the selection is logged when it changes. */
|
|
752
|
+
lastLoggedIpDbPath: string | null;
|
|
753
|
+
/** IPinfo Lite API token; `null` disables the API. */
|
|
754
|
+
ipinfoApiToken: string | null;
|
|
755
|
+
}
|
|
756
|
+
export declare const ipInfoState: IpInfoState;
|
|
757
|
+
/** Country and ASN fields of an IP address. */
|
|
758
|
+
interface IPDatabaseRecord {
|
|
759
|
+
country: string | null;
|
|
760
|
+
asn: number | null;
|
|
761
|
+
asName: string | null;
|
|
762
|
+
asDomain: string | null;
|
|
763
|
+
}
|
|
764
|
+
interface LoadIpDbOptions {
|
|
765
|
+
/** Always use a local/bundled database file. */
|
|
766
|
+
alwaysUseLocalFile?: boolean;
|
|
767
|
+
/** Path to a local MMDB file. */
|
|
768
|
+
localFilePath?: string | null;
|
|
769
|
+
/** URL to the MMDB database file. */
|
|
770
|
+
url?: string | null;
|
|
771
|
+
/** Do not make online requests. */
|
|
772
|
+
offline?: boolean;
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Downloads the IP-to-country MMDB database from a URL and caches it
|
|
776
|
+
* locally. An existing `localFilePath` is used as-is, with no download. On
|
|
777
|
+
* download failure (or when offline), a previously cached download is used
|
|
778
|
+
* if available, falling back to the bundled copy from `@tsdmarc/ipinfo-lite`.
|
|
779
|
+
* A download that isn't a MMDB database counts as a failure: parsedmarc
|
|
780
|
+
* saves it over the cached copy, and every lookup fails from then on.
|
|
781
|
+
*/
|
|
782
|
+
export declare function loadIpDb(options?: LoadIpDbOptions): Promise<void>;
|
|
783
|
+
/**
|
|
784
|
+
* Configure the IPinfo Lite REST API as the primary source for IP lookups.
|
|
785
|
+
*
|
|
786
|
+
* With a token, `getIpAddressDbRecord()` queries the API first and falls
|
|
787
|
+
* back to the MMDB on network errors or non-2xx responses. An invalid token
|
|
788
|
+
* raises `InvalidIPinfoAPIKey`.
|
|
789
|
+
*
|
|
790
|
+
* With `probe` (the default), the token is verified by looking up
|
|
791
|
+
* `1.1.1.1`: a 401/403 raises `InvalidIPinfoAPIKey`; other errors are
|
|
792
|
+
* logged and the token is still accepted so per-request fallback can take
|
|
793
|
+
* over.
|
|
794
|
+
*/
|
|
795
|
+
export declare function configureIpinfoApi(token: string | null, options?: {
|
|
796
|
+
probe?: boolean;
|
|
797
|
+
}): Promise<void>;
|
|
798
|
+
/**
|
|
799
|
+
* Look up an IP and return country and ASN fields.
|
|
800
|
+
*
|
|
801
|
+
* If the IPinfo Lite API is configured (`configureIpinfoApi()`), it is
|
|
802
|
+
* queried first; any non-fatal failure falls through to the MMDB. An
|
|
803
|
+
* invalid API token raises `InvalidIPinfoAPIKey`. MaxMind/DBIP
|
|
804
|
+
* country-only databases carry only the country, so the ASN fields come
|
|
805
|
+
* back `null` for them.
|
|
806
|
+
*/
|
|
807
|
+
export declare function getIpAddressDbRecord(ipAddress: string, options?: {
|
|
808
|
+
dbPath?: string | null;
|
|
809
|
+
skipApi?: boolean;
|
|
810
|
+
}): Promise<IPDatabaseRecord>;
|
|
811
|
+
/**
|
|
812
|
+
* The ISO code of the country of an IPv4 or IPv6 address, or `null` when
|
|
813
|
+
* unknown. `dbPath` is a MMDB file from IPinfo, MaxMind or DBIP.
|
|
814
|
+
*/
|
|
815
|
+
export declare function getIpAddressCountry(ipAddress: string, options?: {
|
|
816
|
+
dbPath?: string | null;
|
|
817
|
+
}): Promise<string | null>;
|
|
818
|
+
interface LoadReverseDnsMapOptions {
|
|
819
|
+
/** Always use a local map file. */
|
|
820
|
+
alwaysUseLocalFile?: boolean;
|
|
821
|
+
/** Path to a local map file. */
|
|
822
|
+
localFilePath?: string | null;
|
|
823
|
+
/** URL to a reverse DNS map. */
|
|
824
|
+
url?: string | null;
|
|
825
|
+
/** Do not make online requests. */
|
|
826
|
+
offline?: boolean;
|
|
827
|
+
/** Path to a local PSL overrides file. */
|
|
828
|
+
pslOverridesPath?: string | null;
|
|
829
|
+
/** URL to a PSL overrides file. */
|
|
830
|
+
pslOverridesUrl?: string | null;
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Loads the reverse DNS map from a URL or local file.
|
|
834
|
+
*
|
|
835
|
+
* Repopulates the given map in place. The URL is tried first; on
|
|
836
|
+
* failure (or when `offline` / `alwaysUseLocalFile` is set) the local path
|
|
837
|
+
* is used, defaulting to the bundled CSV. The PSL overrides are reloaded
|
|
838
|
+
* first with the same flags, so map entries that depend on a folded base
|
|
839
|
+
* domain resolve against the current overrides.
|
|
840
|
+
*
|
|
841
|
+
* parsedmarc clears the map before downloading; here the new entries are
|
|
842
|
+
* built aside and swapped in at once, so concurrent lookups never see a
|
|
843
|
+
* half-loaded map (and never cache results computed from it). Concurrent
|
|
844
|
+
* loads of the same map share one download.
|
|
845
|
+
*/
|
|
846
|
+
export declare function loadReverseDnsMap(reverseDnsMap: ReverseDNSMap, options?: LoadReverseDnsMapOptions): Promise<void>;
|
|
847
|
+
interface GetServiceOptions extends LoadReverseDnsMapOptions {
|
|
848
|
+
/** A reverse DNS map; loaded into when empty. */
|
|
849
|
+
reverseDnsMap?: ReverseDNSMap | null;
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* The service of a base domain from reverse DNS. Unknown services have the
|
|
853
|
+
* base domain as name and a `null` type.
|
|
854
|
+
*/
|
|
855
|
+
export declare function getServiceFromReverseDnsBaseDomain(baseDomain: string, options?: GetServiceOptions): Promise<ReverseDNSService>;
|
|
856
|
+
/**
|
|
857
|
+
* The network lookups one aggregate report may still make. parsedmarc has no
|
|
858
|
+
* such limit: records are resolved one at a time, a failed lookup takes
|
|
859
|
+
* seconds, and a sender choosing addresses that fail can hold a small
|
|
860
|
+
* report for days. Past a limit the report keeps all its records, without
|
|
861
|
+
* reverse DNS.
|
|
862
|
+
*/
|
|
863
|
+
declare class ReportLookupBudget {
|
|
864
|
+
private readonly maxLookups;
|
|
865
|
+
private readonly maxFailures;
|
|
866
|
+
private readonly maxTimeMs;
|
|
867
|
+
private lookups;
|
|
868
|
+
private failures;
|
|
869
|
+
private spentMs;
|
|
870
|
+
private warned;
|
|
871
|
+
/**
|
|
872
|
+
* What this report's lookups found for IPs the shared cache doesn't keep
|
|
873
|
+
* (no PTR record, ASN not in the map): a repeated address costs one
|
|
874
|
+
* lookup per report, not one per record.
|
|
875
|
+
*/
|
|
876
|
+
private readonly uncached;
|
|
877
|
+
constructor(config: ParserConfig);
|
|
878
|
+
/** Whether one more IP may use the network; counts it if so. */
|
|
879
|
+
take(): boolean;
|
|
880
|
+
/** Count the time of a lookup against the report. */
|
|
881
|
+
charge(elapsedMs: number): void;
|
|
882
|
+
/** What an earlier record of this report found for `ipAddress`. */
|
|
883
|
+
recall(ipAddress: string): IPAddressInfo | undefined;
|
|
884
|
+
/** Keep what was found for an IP the shared cache doesn't keep. */
|
|
885
|
+
remember(info: IPAddressInfo): void;
|
|
886
|
+
/** Count a DNS error, unless it is a real answer that there is no PTR. */
|
|
887
|
+
recordError(error: DNSException): void;
|
|
888
|
+
}
|
|
889
|
+
interface GetIpAddressInfoOptions {
|
|
890
|
+
/**
|
|
891
|
+
* Take every option below from this config (its IP address cache,
|
|
892
|
+
* reverse DNS map, DNS settings, ...), the way parsedmarc's parsers call
|
|
893
|
+
* `get_ip_address_info()`. Options given alongside it are ignored.
|
|
894
|
+
*/
|
|
895
|
+
config?: ParserConfig | null;
|
|
896
|
+
/** Limits on the network lookups of the report this IP comes from. */
|
|
897
|
+
lookupBudget?: ReportLookupBudget | null;
|
|
898
|
+
/** Path to a MMDB file from IPinfo, MaxMind, or DBIP. */
|
|
899
|
+
ipDbPath?: string | null;
|
|
900
|
+
/** Path to a reverse DNS map file. */
|
|
901
|
+
reverseDnsMapPath?: string | null;
|
|
902
|
+
/** Do not download files. */
|
|
903
|
+
alwaysUseLocalFiles?: boolean;
|
|
904
|
+
/** URL to the reverse DNS map file. */
|
|
905
|
+
reverseDnsMapUrl?: string | null;
|
|
906
|
+
/** Cache storage. */
|
|
907
|
+
cache?: IPAddressCache | null;
|
|
908
|
+
/** A reverse DNS map. */
|
|
909
|
+
reverseDnsMap?: ReverseDNSMap | null;
|
|
910
|
+
/** Do not make online queries for geolocation or DNS. */
|
|
911
|
+
offline?: boolean;
|
|
912
|
+
/** Nameservers to use (Cloudflare's public DNS resolvers by default). */
|
|
913
|
+
nameservers?: readonly string[] | null;
|
|
914
|
+
/** DNS timeout, in seconds. */
|
|
915
|
+
timeout?: number;
|
|
916
|
+
/** Times to retry on timeout or other transient DNS errors. */
|
|
917
|
+
retries?: number;
|
|
918
|
+
/** Path to a local PSL overrides file. */
|
|
919
|
+
pslOverridesPath?: string | null;
|
|
920
|
+
/** URL to a PSL overrides file. */
|
|
921
|
+
pslOverridesUrl?: string | null;
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Reverse DNS, country, ASN, and service information for an IP address.
|
|
925
|
+
*/
|
|
926
|
+
export declare function getIpAddressInfo(ip: string, input?: GetIpAddressInfoOptions): Promise<IPAddressInfo>;
|
|
927
|
+
/** The XML namespace RFC 9990 assigns to DMARC aggregate reports. */
|
|
928
|
+
export declare const RFC_9990_NAMESPACE = "urn:ietf:params:xml:ns:dmarc-2.0";
|
|
929
|
+
/**
|
|
930
|
+
* RFC 9990's PolicyOverrideType enumeration: `policy_test_mode` was added
|
|
931
|
+
* and `forwarded` / `sampled_out` were removed.
|
|
932
|
+
*/
|
|
933
|
+
export declare const RFC_9990_POLICY_OVERRIDE_TYPES: ReadonlySet<string>;
|
|
934
|
+
export declare const RFC_7489_REMOVED_POLICY_OVERRIDE_TYPES: ReadonlySet<string>;
|
|
935
|
+
/**
|
|
936
|
+
* Unwrap a possibly-langAttrString value (`{"#text": ..., "@lang": ...}`)
|
|
937
|
+
* to its text. `null` stays `null`; other shapes pass through unchanged.
|
|
938
|
+
*/
|
|
939
|
+
export declare function unwrapText(value: unknown): unknown;
|
|
940
|
+
interface DayBucket {
|
|
941
|
+
begin: PyDateTime;
|
|
942
|
+
end: PyDateTime;
|
|
943
|
+
count: number | bigint;
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Splits a report interval into calendar-day buckets
|
|
947
|
+
* (`_bucket_interval_by_day`), computing the day layout once for all of a
|
|
948
|
+
* report's records.
|
|
949
|
+
*
|
|
950
|
+
* Python walks every day of the interval; a reporter-supplied `date_range`
|
|
951
|
+
* can span thousands of years, so this works per group of days with the
|
|
952
|
+
* same overlap (first day, full days, last day). Every day in a group gets
|
|
953
|
+
* the same exact share, so floors, fractions and the largest-remainder
|
|
954
|
+
* order (Python's stable `sorted(..., reverse=True)`: among equal
|
|
955
|
+
* fractions, earlier days first) are computed per group, and only buckets
|
|
956
|
+
* with a non-zero count are built. The result equals Python's.
|
|
957
|
+
*/
|
|
958
|
+
declare class IntervalDays {
|
|
959
|
+
#private;
|
|
960
|
+
readonly begin: PyDateTime;
|
|
961
|
+
readonly end: PyDateTime;
|
|
962
|
+
constructor(begin: PyDateTime, end: PyDateTime);
|
|
963
|
+
/**
|
|
964
|
+
* Distribute `totalCount` over the days, like Python, raising like it.
|
|
965
|
+
* More than `maxBuckets` buckets raise `InvalidAggregateReport` before any
|
|
966
|
+
* is built (not in parsedmarc).
|
|
967
|
+
*/
|
|
968
|
+
buckets(totalCount: number | bigint, maxBuckets?: number): DayBucket[];
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Split [begin, end) into calendar-day buckets and distribute `totalCount`
|
|
972
|
+
* across them in proportion to each day's overlap, using largest-remainder
|
|
973
|
+
* rounding so the counts sum to `totalCount` (`_bucket_interval_by_day`).
|
|
974
|
+
*/
|
|
975
|
+
export declare function bucketIntervalByDay(begin: PyDateTime, end: PyDateTime, totalCount: number | bigint): DayBucket[];
|
|
976
|
+
/** A parsed record before its interval fields are added. */
|
|
977
|
+
type RecordWithoutInterval = Omit<AggregateRecord, "intervalBegin" | "intervalEnd" | "normalizedTimespan">;
|
|
978
|
+
/**
|
|
979
|
+
* What a report's normalized timespan may still expand to, in records of
|
|
980
|
+
* ordinary size (`maxNormalizedRecords`; not in parsedmarc). Every day
|
|
981
|
+
* bucket is a row of the CSV and a document of the search outputs, and
|
|
982
|
+
* repeats its record's results and the report's data (reporter, errors,
|
|
983
|
+
* published policy), so a record with thousands of `<dkim>` entries, or a
|
|
984
|
+
* long `extra_contact_info`, weighs as many records as its rows take in the
|
|
985
|
+
* output: the number of records alone doesn't bound the report.
|
|
986
|
+
*/
|
|
987
|
+
export declare class NormalizedRecordBudget {
|
|
988
|
+
#private;
|
|
989
|
+
/**
|
|
990
|
+
* @param reportSize Estimated characters of the report data each row
|
|
991
|
+
* repeats ({@link estimatedSize}).
|
|
992
|
+
*/
|
|
993
|
+
constructor(maxRecords: number, reportSize?: number);
|
|
994
|
+
/** The records of ordinary size a row of `record` weighs, at least one. */
|
|
995
|
+
weigh(record: RecordWithoutInterval): number;
|
|
996
|
+
/** The buckets of a record of `weight` that still fit. */
|
|
997
|
+
room(weight: number): number;
|
|
998
|
+
spend(weight: number): void;
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Append a parsed record unchanged, or split into daily buckets when the
|
|
1002
|
+
* report's timespan needs normalizing (`_append_parsed_record`).
|
|
1003
|
+
*/
|
|
1004
|
+
export declare function appendParsedRecord(parsedRecord: RecordWithoutInterval, records: AggregateRecord[], beginDt: PyDateTime, endDt: PyDateTime, normalize: boolean, interval?: IntervalDays, maxRecords?: number | NormalizedRecordBudget): void;
|
|
1005
|
+
/**
|
|
1006
|
+
* Convert one `<record>` into parsedmarc's consistent shape, enriching the
|
|
1007
|
+
* source IP (`_parse_report_record`).
|
|
1008
|
+
*/
|
|
1009
|
+
export declare function parseReportRecord(record: unknown, config: ParserConfig, isRfc9990?: boolean, lookupBudget?: ReportLookupBudget | null): Promise<RecordWithoutInterval>;
|
|
1010
|
+
interface ParseAggregateReportOptions extends ParserConfigInput {
|
|
1011
|
+
/** DNS timeout in seconds (parsedmarc's `timeout`); same as `dnsTimeout`. */
|
|
1012
|
+
timeout?: number;
|
|
1013
|
+
/** DNS retries (parsedmarc's `retries`); same as `dnsRetries`. */
|
|
1014
|
+
retries?: number;
|
|
1015
|
+
/** Called every 20 records of large reports (not part of `config`). */
|
|
1016
|
+
keepAlive?: (() => unknown) | null;
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Parse a DMARC aggregate report's XML. Malformed XML is recovered the way
|
|
1020
|
+
* parsedmarc does (with the parse error kept in `reportMetadata.errors`).
|
|
1021
|
+
* An error thrown by `keepAlive` is rethrown as is.
|
|
1022
|
+
*/
|
|
1023
|
+
export declare function parseAggregateReportXml(xml: string | Uint8Array, options?: ParseAggregateReportOptions): Promise<AggregateReport>;
|
|
1024
|
+
interface ParseAggregateReportFileOptions extends ParserConfigInput, ExtractOptions {
|
|
1025
|
+
keepAlive?: (() => unknown) | null;
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Parse an aggregate report from bytes (zip, gzip or XML) or base64 text
|
|
1029
|
+
* (`parse_aggregate_report_file`). Like Python, a string goes to
|
|
1030
|
+
* `extractReport()` as base64, or as the XML itself when it is not valid
|
|
1031
|
+
* base64; it is never opened as a path (see
|
|
1032
|
+
* {@link parseAggregateReportFromFilePath}).
|
|
1033
|
+
*/
|
|
1034
|
+
export declare function parseAggregateReportFile(input: string | Uint8Array, options?: ParseAggregateReportFileOptions): Promise<AggregateReport>;
|
|
1035
|
+
/**
|
|
1036
|
+
* Parse an aggregate report file from disk (`extract_report_from_file_path`
|
|
1037
|
+
* followed by `parse_aggregate_report_xml`). parsedmarc's
|
|
1038
|
+
* `parse_aggregate_report_file()` documents a path argument but treats a
|
|
1039
|
+
* string as report content; this is the path form.
|
|
1040
|
+
*/
|
|
1041
|
+
export declare function parseAggregateReportFromFilePath(filePath: string, options?: ParseAggregateReportFileOptions): Promise<AggregateReport>;
|
|
1042
|
+
/** Fields of `parsedAggregateReportsToCsv()`, in column order. */
|
|
1043
|
+
export declare const AGGREGATE_CSV_FIELDS: readonly string[];
|
|
1044
|
+
/**
|
|
1045
|
+
* Flatten aggregate reports into CSV rows (snake_case keys), one per
|
|
1046
|
+
* record (`parsed_aggregate_reports_to_csv_rows`).
|
|
1047
|
+
*/
|
|
1048
|
+
export declare function parsedAggregateReportsToCsvRows(reports: AggregateReport | AggregateReport[]): CsvRow[];
|
|
1049
|
+
/** Aggregate reports as flat CSV, with headers. */
|
|
1050
|
+
export declare function parsedAggregateReportsToCsv(reports: AggregateReport | AggregateReport[], options?: CsvOptions): string;
|
|
1051
|
+
/** Version of tsdmarc itself. */
|
|
1052
|
+
export declare const VERSION: string;
|
|
1053
|
+
/** parsedmarc version this port matches feature-for-feature. */
|
|
1054
|
+
export declare const PARSEDMARC_COMPAT_VERSION: string;
|
|
1055
|
+
export declare const USER_AGENT: string;
|
|
1056
|
+
/** DNS query timeout, in seconds. */
|
|
1057
|
+
export declare const DEFAULT_DNS_TIMEOUT: number;
|
|
1058
|
+
export declare const DEFAULT_DNS_MAX_RETRIES: number;
|
|
1059
|
+
/**
|
|
1060
|
+
* Recommended mix of public resolvers for cross-provider DNS failover. Not
|
|
1061
|
+
* applied automatically: callers opt in by passing
|
|
1062
|
+
* `nameservers: RECOMMENDED_DNS_NAMESERVERS`. Mixing providers means a single
|
|
1063
|
+
* operator's anycast outage falls through to a different provider.
|
|
1064
|
+
*/
|
|
1065
|
+
export declare const RECOMMENDED_DNS_NAMESERVERS: readonly string[];
|
|
1066
|
+
/** Raised when an error parsing a report occurs. */
|
|
1067
|
+
export declare class ParserError extends Error {
|
|
1068
|
+
override name: string;
|
|
1069
|
+
}
|
|
1070
|
+
/** Raised when an invalid DMARC report is encountered. */
|
|
1071
|
+
export declare class InvalidDMARCReport extends ParserError {
|
|
1072
|
+
override name: string;
|
|
1073
|
+
}
|
|
1074
|
+
/** Raised when an invalid SMTP TLS report is encountered. */
|
|
1075
|
+
export declare class InvalidSMTPTLSReport extends ParserError {
|
|
1076
|
+
override name: string;
|
|
1077
|
+
}
|
|
1078
|
+
/** Raised when an invalid DMARC aggregate report is encountered. */
|
|
1079
|
+
export declare class InvalidAggregateReport extends InvalidDMARCReport {
|
|
1080
|
+
override name: string;
|
|
1081
|
+
}
|
|
1082
|
+
/** Raised when an invalid DMARC failure report is encountered. */
|
|
1083
|
+
export declare class InvalidFailureReport extends InvalidDMARCReport {
|
|
1084
|
+
override name: string;
|
|
1085
|
+
}
|
|
1086
|
+
/** Backward-compatible alias of {@link InvalidFailureReport}. */
|
|
1087
|
+
export declare const InvalidForensicReport: typeof InvalidFailureReport;
|
|
1088
|
+
/**
|
|
1089
|
+
* Raised when an email is over the configured size or structure limits
|
|
1090
|
+
* (`maxEmailSize`, `maxMimeParts`, `maxHeaderLines`,
|
|
1091
|
+
* `maxExpandedEmailSize`); parsedmarc has no such limits. Mailbox processing
|
|
1092
|
+
* treats the message as invalid.
|
|
1093
|
+
*/
|
|
1094
|
+
export declare class EmailTooLargeError extends ParserError {
|
|
1095
|
+
override name: string;
|
|
1096
|
+
}
|
|
1097
|
+
/** The error for an email of `size` bytes over the `limit` (`maxEmailSize`). */
|
|
1098
|
+
export declare function emailTooLargeError(size: number, limit: number): EmailTooLargeError;
|
|
1099
|
+
/** Raised when an error parsing the email occurs. */
|
|
1100
|
+
export declare class EmailParserError extends Error {
|
|
1101
|
+
override name: string;
|
|
1102
|
+
}
|
|
1103
|
+
/** Raised when an error occurs when downloading a file. */
|
|
1104
|
+
export declare class DownloadError extends Error {
|
|
1105
|
+
override name: string;
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Enrichment can't work for any record: nothing is wrong with the report
|
|
1109
|
+
* being parsed. Unlike a bad record, it must not be skipped, or a report
|
|
1110
|
+
* loses all its records and still looks parsed. parsedmarc has no such
|
|
1111
|
+
* class.
|
|
1112
|
+
*/
|
|
1113
|
+
export declare class EnrichmentError extends Error {
|
|
1114
|
+
override name: string;
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* The {@link EnrichmentError} behind `error`, or `null`: report parsers wrap
|
|
1118
|
+
* whatever fails in a report-level error, keeping it in `cause`.
|
|
1119
|
+
*/
|
|
1120
|
+
export declare function findEnrichmentError(error: unknown): EnrichmentError | null;
|
|
1121
|
+
/** Raised when the IPinfo API rejects the configured token. */
|
|
1122
|
+
export declare class InvalidIPinfoAPIKey extends EnrichmentError {
|
|
1123
|
+
override name: string;
|
|
1124
|
+
}
|
|
1125
|
+
/** Raised when no IP database can be opened. */
|
|
1126
|
+
export declare class IPDatabaseError extends EnrichmentError {
|
|
1127
|
+
override name: string;
|
|
1128
|
+
/** The system error code (`ENOENT`) of a missing file, if any. */
|
|
1129
|
+
readonly code: string | undefined;
|
|
1130
|
+
constructor(message: string, options?: ErrorOptions & {
|
|
1131
|
+
code?: string;
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
export declare const NOTSET = 0;
|
|
1135
|
+
export declare const DEBUG = 10;
|
|
1136
|
+
export declare const INFO = 20;
|
|
1137
|
+
export declare const WARNING = 30;
|
|
1138
|
+
export declare const ERROR = 40;
|
|
1139
|
+
export declare const CRITICAL = 50;
|
|
1140
|
+
/** Name of the level, or `Level <n>` for custom levels (as in Python). */
|
|
1141
|
+
export declare function getLevelName(level: number): string;
|
|
1142
|
+
interface LogRecord {
|
|
1143
|
+
name: string;
|
|
1144
|
+
level: number;
|
|
1145
|
+
levelName: string;
|
|
1146
|
+
message: string;
|
|
1147
|
+
/** Milliseconds since the epoch. */
|
|
1148
|
+
created: number;
|
|
1149
|
+
/** Error passed to `exception()` / `log(..., { error })`. */
|
|
1150
|
+
error?: unknown;
|
|
1151
|
+
/** Extra fields, like Python's `extra=`; filters may add to it. */
|
|
1152
|
+
extra: Record<string, unknown>;
|
|
1153
|
+
}
|
|
1154
|
+
interface LogOptions {
|
|
1155
|
+
error?: unknown;
|
|
1156
|
+
extra?: Record<string, unknown>;
|
|
1157
|
+
}
|
|
1158
|
+
type LogFilter = (record: LogRecord) => boolean;
|
|
1159
|
+
type LogFormatter = (record: LogRecord) => string;
|
|
1160
|
+
/** Python's default `asctime`: local time, `2026-10-24 03:17:13,123`. */
|
|
1161
|
+
export declare function formatAsctime(created: number): string;
|
|
1162
|
+
/** `LEVEL:name:message`, the format of Python's `assertLogs` output. */
|
|
1163
|
+
export declare function basicFormat(record: LogRecord): string;
|
|
1164
|
+
/** Console format used by `configureLogging()`. */
|
|
1165
|
+
export declare function consoleFormat(record: LogRecord): string;
|
|
1166
|
+
/** Log file format used by `configureLogging()`. */
|
|
1167
|
+
export declare function fileFormat(record: LogRecord): string;
|
|
1168
|
+
export declare abstract class Handler {
|
|
1169
|
+
level: number;
|
|
1170
|
+
formatter: LogFormatter;
|
|
1171
|
+
readonly filters: LogFilter[];
|
|
1172
|
+
setLevel(level: number): void;
|
|
1173
|
+
setFormatter(formatter: LogFormatter): void;
|
|
1174
|
+
addFilter(filter: LogFilter): void;
|
|
1175
|
+
removeFilter(filter: LogFilter): void;
|
|
1176
|
+
format(record: LogRecord): string;
|
|
1177
|
+
/** Apply level and filters, then emit. Errors in `emit` never propagate. */
|
|
1178
|
+
handle(record: LogRecord): void;
|
|
1179
|
+
/** Like Python's `Handler.handleError`: report to stderr, keep going. */
|
|
1180
|
+
handleError(record: LogRecord, error: unknown): void;
|
|
1181
|
+
abstract emit(record: LogRecord): void;
|
|
1182
|
+
flush(): void;
|
|
1183
|
+
close(): void;
|
|
1184
|
+
}
|
|
1185
|
+
/** Discards every record. */
|
|
1186
|
+
export declare class NullHandler extends Handler {
|
|
1187
|
+
emit(_record: LogRecord): void;
|
|
1188
|
+
}
|
|
1189
|
+
interface WritableTextStream {
|
|
1190
|
+
write: (text: string) => unknown;
|
|
1191
|
+
}
|
|
1192
|
+
/** Writes formatted records to a stream (stderr by default). */
|
|
1193
|
+
export declare class StreamHandler extends Handler {
|
|
1194
|
+
readonly stream: WritableTextStream;
|
|
1195
|
+
constructor(stream?: WritableTextStream);
|
|
1196
|
+
emit(record: LogRecord): void;
|
|
1197
|
+
}
|
|
1198
|
+
/** Appends formatted records to a file, synchronously and in order. */
|
|
1199
|
+
export declare class FileHandler extends StreamHandler {
|
|
1200
|
+
/** Absolute path of the log file, like Python's `baseFilename`. */
|
|
1201
|
+
readonly baseFilename: string;
|
|
1202
|
+
private fd;
|
|
1203
|
+
constructor(filename: string, mode?: "a" | "w");
|
|
1204
|
+
override close(): void;
|
|
1205
|
+
}
|
|
1206
|
+
export declare class Logger {
|
|
1207
|
+
readonly name: string;
|
|
1208
|
+
level: number;
|
|
1209
|
+
readonly handlers: Handler[];
|
|
1210
|
+
readonly filters: LogFilter[];
|
|
1211
|
+
disabled: boolean;
|
|
1212
|
+
constructor(name: string);
|
|
1213
|
+
setLevel(level: number): void;
|
|
1214
|
+
getEffectiveLevel(): number;
|
|
1215
|
+
isEnabledFor(level: number): boolean;
|
|
1216
|
+
addHandler(handler: Handler): void;
|
|
1217
|
+
removeHandler(handler: Handler): void;
|
|
1218
|
+
hasHandlers(): boolean;
|
|
1219
|
+
addFilter(filter: LogFilter): void;
|
|
1220
|
+
removeFilter(filter: LogFilter): void;
|
|
1221
|
+
log(level: number, message: string, options?: LogOptions): void;
|
|
1222
|
+
debug(message: string, options?: LogOptions): void;
|
|
1223
|
+
info(message: string, options?: LogOptions): void;
|
|
1224
|
+
warning(message: string, options?: LogOptions): void;
|
|
1225
|
+
error(message: string, options?: LogOptions): void;
|
|
1226
|
+
critical(message: string, options?: LogOptions): void;
|
|
1227
|
+
/** Log at ERROR with the error attached (Python's `logger.exception`). */
|
|
1228
|
+
exception(message: string, error: unknown, options?: LogOptions): void;
|
|
1229
|
+
}
|
|
1230
|
+
/** Return the shared logger with this name, creating it on first use. */
|
|
1231
|
+
export declare function getLogger(name: string): Logger;
|
|
1232
|
+
/** The library logger (parsedmarc's `parsedmarc.log.logger`). */
|
|
1233
|
+
export declare const logger: Logger;
|
|
1234
|
+
/**
|
|
1235
|
+
* Configure the library logger's handlers: set the level, add a stderr
|
|
1236
|
+
* handler if there is none, and add a file handler for `logFile` unless one
|
|
1237
|
+
* for the same absolute path is already attached (so repeated calls, e.g. a
|
|
1238
|
+
* SIGHUP reload, never write each record twice).
|
|
1239
|
+
*/
|
|
1240
|
+
export declare function configureLogging(level: number, logFile?: string | null): void;
|
|
1241
|
+
/** Collects records; used by {@link captureLogs}. */
|
|
1242
|
+
export declare class CaptureHandler extends Handler {
|
|
1243
|
+
readonly records: LogRecord[];
|
|
1244
|
+
emit(record: LogRecord): void;
|
|
1245
|
+
}
|
|
1246
|
+
interface LogCapture {
|
|
1247
|
+
/** Captured records, in order. */
|
|
1248
|
+
readonly records: LogRecord[];
|
|
1249
|
+
/** Captured records as `LEVEL:name:message` strings (like `assertLogs`). */
|
|
1250
|
+
readonly output: string[];
|
|
1251
|
+
/** Detach the capture handler and restore the logger's level. */
|
|
1252
|
+
restore: () => void;
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Capture records of `level` and above from a logger, like Python's
|
|
1256
|
+
* `assertLogs`. Call `restore()` when done (e.g. in `afterEach`).
|
|
1257
|
+
*/
|
|
1258
|
+
export declare function captureLogs(target?: Logger | string, level?: number): LogCapture;
|
|
1259
|
+
/**
|
|
1260
|
+
* Content types of the MIME part that carries a failure report's sample
|
|
1261
|
+
* message (or only its headers).
|
|
1262
|
+
*/
|
|
1263
|
+
export declare const EMAIL_SAMPLE_CONTENT_TYPES: readonly string[];
|
|
1264
|
+
/** Fields of `parsedFailureReportsToCsv()`, in column order. */
|
|
1265
|
+
export declare const FAILURE_CSV_FIELDS: readonly string[];
|
|
1266
|
+
/** Options of {@link parseFailureReport}. */
|
|
1267
|
+
type ParseFailureReportOptions = Omit<ParserConfigInput, "normalizeTimespanThresholdHours">;
|
|
1268
|
+
/**
|
|
1269
|
+
* Parse a DMARC failure report (the `message/feedback-report` part) and its
|
|
1270
|
+
* sample (the RFC 822 message or headers).
|
|
1271
|
+
*
|
|
1272
|
+
* `msgDate` (the report email's date) is the arrival date when the report
|
|
1273
|
+
* has no `Arrival-Date` field. Feedback-report fields parsedmarc doesn't
|
|
1274
|
+
* know are kept, with camelCase keys (`Reported-URI` → `reportedUri`).
|
|
1275
|
+
*/
|
|
1276
|
+
export declare function parseFailureReport(feedbackReport: string, sample: string, msgDate: PyDateTime | null, options?: ParseFailureReportOptions): Promise<FailureReport>;
|
|
1277
|
+
/** Backward-compatible alias of {@link parseFailureReport}. */
|
|
1278
|
+
export declare const parseForensicReport: typeof parseFailureReport;
|
|
1279
|
+
/**
|
|
1280
|
+
* Flatten failure reports into CSV rows (snake_case keys): every report
|
|
1281
|
+
* field except the sample, with the source flattened into `source_*`, the
|
|
1282
|
+
* sample's subject, and the list fields joined with commas.
|
|
1283
|
+
*/
|
|
1284
|
+
export declare function parsedFailureReportsToCsvRows(reports: FailureReport | FailureReport[]): CsvRow[];
|
|
1285
|
+
/** Backward-compatible alias of {@link parsedFailureReportsToCsvRows}. */
|
|
1286
|
+
export declare const parsedForensicReportsToCsvRows: typeof parsedFailureReportsToCsvRows;
|
|
1287
|
+
/** Failure reports as flat CSV, with headers. */
|
|
1288
|
+
export declare function parsedFailureReportsToCsv(reports: FailureReport | FailureReport[], options?: CsvOptions): string;
|
|
1289
|
+
/** Backward-compatible alias of {@link parsedFailureReportsToCsv}. */
|
|
1290
|
+
export declare const parsedForensicReportsToCsv: typeof parsedFailureReportsToCsv;
|
|
1291
|
+
interface PyJsonDumpsOptions {
|
|
1292
|
+
/** `(item_separator, key_separator)`; Python's default is `(', ', ': ')`. */
|
|
1293
|
+
separators?: readonly [string, string];
|
|
1294
|
+
/**
|
|
1295
|
+
* Called for values JSON can't represent, like Python's `default=`; its
|
|
1296
|
+
* result is serialized instead. Without it they throw `TypeError`.
|
|
1297
|
+
*/
|
|
1298
|
+
default?: (value: unknown) => unknown;
|
|
1299
|
+
/** Pretty-print with this many spaces per level, like `indent=`. */
|
|
1300
|
+
indent?: number | null;
|
|
1301
|
+
/** Escape non-ASCII as `\uXXXX`, like `ensure_ascii=`. Default: true. */
|
|
1302
|
+
ensureAscii?: boolean;
|
|
1303
|
+
/**
|
|
1304
|
+
* Write `NaN`, `Infinity` and `-Infinity` as `null`, for a consumer that
|
|
1305
|
+
* expects standard JSON. Python writes them bare (`allow_nan=True`).
|
|
1306
|
+
*/
|
|
1307
|
+
nonFiniteAsNull?: boolean;
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* A number as Python's `json.dumps()` writes it back after `json.loads()`
|
|
1311
|
+
* (an exact int, or a float's `repr`), so values a JS number can't keep
|
|
1312
|
+
* (`12345678901234567890`, `1.0`) survive a load/dump round trip.
|
|
1313
|
+
*/
|
|
1314
|
+
export declare class PyJsonNumber {
|
|
1315
|
+
readonly text: string;
|
|
1316
|
+
constructor(text: string);
|
|
1317
|
+
}
|
|
1318
|
+
/** Python's `json.dumps(value, separators=, default=, indent=, ensure_ascii=)`. */
|
|
1319
|
+
export declare function pyJsonDumps(value: unknown, options?: PyJsonDumpsOptions): string;
|
|
1320
|
+
/**
|
|
1321
|
+
* A copy of `value` that `JSON.stringify()` serializes like Python's
|
|
1322
|
+
* `json.dumps()` writes ints: every `bigint` (and `PyJsonNumber`) becomes a
|
|
1323
|
+
* `JSON.rawJSON()` exact integer, where `JSON.stringify()` would throw on a
|
|
1324
|
+
* bigint. For payloads that SDKs serialize themselves (Elasticsearch,
|
|
1325
|
+
* OpenSearch, Azure Log Analytics). Arrays and plain objects are copied;
|
|
1326
|
+
* other values are kept as they are.
|
|
1327
|
+
*/
|
|
1328
|
+
export declare function rawJsonIntegers(value: unknown): unknown;
|
|
1329
|
+
/** Raised by mailbox methods a backend does not implement. */
|
|
1330
|
+
export declare class NotImplementedError extends Error {
|
|
1331
|
+
override name: string;
|
|
1332
|
+
}
|
|
1333
|
+
/** A folder/label operation targets a name that is already taken. */
|
|
1334
|
+
export declare class FolderExistsError extends Error {
|
|
1335
|
+
override name: string;
|
|
1336
|
+
}
|
|
1337
|
+
/** A folder/label referenced by an operation does not exist. */
|
|
1338
|
+
export declare class FolderNotFoundError extends Error {
|
|
1339
|
+
override name: string;
|
|
1340
|
+
}
|
|
1341
|
+
/** Backend identifier. */
|
|
1342
|
+
type MailboxKind = "imap" | "gmail" | "msgraph" | "maildir";
|
|
1343
|
+
/** Provider-specific message identifier (IMAP UID, Graph/Gmail ID, Maildir key). */
|
|
1344
|
+
type MessageId = string | number;
|
|
1345
|
+
interface FetchMessagesOptions {
|
|
1346
|
+
/** Only messages received since this date (backend-specific format, e.g. `"2d"` or an ISO date). */
|
|
1347
|
+
since?: string | null;
|
|
1348
|
+
/** Maximum number of messages to return (0 or unset: all). */
|
|
1349
|
+
batchSize?: number;
|
|
1350
|
+
}
|
|
1351
|
+
interface FetchMessageOptions {
|
|
1352
|
+
/** Mark the message as read after fetching it. */
|
|
1353
|
+
markRead?: boolean;
|
|
1354
|
+
/**
|
|
1355
|
+
* Largest message to download, in bytes. Backends that can learn the size
|
|
1356
|
+
* first throw `EmailTooLargeError` instead of downloading a larger one
|
|
1357
|
+
* (not marking it read). Not in mailsuite.
|
|
1358
|
+
*/
|
|
1359
|
+
maxSize?: number | null;
|
|
1360
|
+
}
|
|
1361
|
+
interface MoveFolderOptions {
|
|
1362
|
+
/** The folder's complete new path. */
|
|
1363
|
+
newPath?: string | null;
|
|
1364
|
+
/** The folder to move the source under, keeping its leaf name ("" = root). */
|
|
1365
|
+
newParent?: string | null;
|
|
1366
|
+
/** Create the destination's parent path if missing. */
|
|
1367
|
+
create?: boolean;
|
|
1368
|
+
}
|
|
1369
|
+
interface MergeFoldersOptions {
|
|
1370
|
+
/** Create the destination if missing. */
|
|
1371
|
+
create?: boolean;
|
|
1372
|
+
/** Keep the emptied source folders. */
|
|
1373
|
+
keepSourceFolders?: boolean;
|
|
1374
|
+
}
|
|
1375
|
+
interface WatchOptions {
|
|
1376
|
+
/** Called with this connection whenever the watcher fires. */
|
|
1377
|
+
checkCallback: (connection: MailboxConnection) => Promise<void> | void;
|
|
1378
|
+
/** Polling interval (or IDLE timeout), in seconds. */
|
|
1379
|
+
checkTimeout: number;
|
|
1380
|
+
/** When it returns true, the watcher returns so the caller can reload config. */
|
|
1381
|
+
configReloading?: (() => boolean) | null;
|
|
1382
|
+
/** Aborts the watch loop. */
|
|
1383
|
+
signal?: AbortSignal;
|
|
1384
|
+
}
|
|
1385
|
+
interface SendMessageOptions {
|
|
1386
|
+
messageFrom: string;
|
|
1387
|
+
messageTo?: string[] | null;
|
|
1388
|
+
messageCc?: string[] | null;
|
|
1389
|
+
messageBcc?: string[] | null;
|
|
1390
|
+
subject?: string | null;
|
|
1391
|
+
messageHeaders?: Record<string, string> | null;
|
|
1392
|
+
/** `[filename, content]` pairs. */
|
|
1393
|
+
attachments?: [string, Uint8Array][] | null;
|
|
1394
|
+
plainMessage?: string | null;
|
|
1395
|
+
htmlMessage?: string | null;
|
|
1396
|
+
/** Save a copy to Sent Items (Microsoft Graph only). Default: true. */
|
|
1397
|
+
saveToSentItems?: boolean;
|
|
1398
|
+
}
|
|
1399
|
+
export declare abstract class MailboxConnection {
|
|
1400
|
+
abstract readonly kind: MailboxKind;
|
|
1401
|
+
/**
|
|
1402
|
+
* Tells this mailbox from others whose message IDs may be the same, for
|
|
1403
|
+
* state kept between calls (several connection objects to one mailbox
|
|
1404
|
+
* share it). Backends whose IDs are unique on their own leave it empty.
|
|
1405
|
+
*/
|
|
1406
|
+
get mailboxIdentity(): string;
|
|
1407
|
+
createFolder(_folderName: string): Promise<void>;
|
|
1408
|
+
deleteFolder(_folderName: string): Promise<void>;
|
|
1409
|
+
/**
|
|
1410
|
+
* Rename a folder. Implementations call `ensureNoFolderConflict()` first,
|
|
1411
|
+
* so renaming onto an existing name throws `FolderExistsError`.
|
|
1412
|
+
*/
|
|
1413
|
+
renameFolder(_oldName: string, _newName: string): Promise<void>;
|
|
1414
|
+
folderExists(_folderName: string): Promise<boolean>;
|
|
1415
|
+
/** Throw `FolderExistsError` if `folderName` already exists. */
|
|
1416
|
+
protected ensureNoFolderConflict(folderName: string): Promise<void>;
|
|
1417
|
+
/**
|
|
1418
|
+
* Move a folder (and its contents). Give exactly one of `newPath` (the
|
|
1419
|
+
* complete new path) or `newParent` (keep the leaf name).
|
|
1420
|
+
*/
|
|
1421
|
+
moveFolder(source: string, options: MoveFolderOptions): Promise<void>;
|
|
1422
|
+
/** Move every message of each source folder into `destination`. */
|
|
1423
|
+
mergeFolders(sources: string | string[], destination: string, options?: MergeFoldersOptions): Promise<void>;
|
|
1424
|
+
/** Backend-specific relocation, called by `moveFolder()` after validation. */
|
|
1425
|
+
protected doMoveFolder(_source: string, _targetParent: string, _targetPath: string): Promise<void>;
|
|
1426
|
+
/** Move one message during `mergeFolders()`. Defaults to `moveMessage()`. */
|
|
1427
|
+
protected moveMessageFrom(messageId: MessageId, _source: string, destination: string): Promise<void>;
|
|
1428
|
+
/** Identifiers of the messages in a folder. */
|
|
1429
|
+
fetchMessages(_reportsFolder: string, _options?: FetchMessagesOptions): Promise<MessageId[]>;
|
|
1430
|
+
/** Raw RFC 822 contents of a message. */
|
|
1431
|
+
fetchMessage(_messageId: MessageId, _options?: FetchMessageOptions): Promise<string>;
|
|
1432
|
+
deleteMessage(_messageId: MessageId): Promise<void>;
|
|
1433
|
+
moveMessage(_messageId: MessageId, _folderName: string): Promise<void>;
|
|
1434
|
+
/** Send a no-op to keep the connection alive (if applicable). */
|
|
1435
|
+
keepalive(): Promise<void>;
|
|
1436
|
+
/** Watch for new messages, calling `checkCallback` on new mail or each interval. */
|
|
1437
|
+
watch(_options: WatchOptions): Promise<void>;
|
|
1438
|
+
/** Send through the backend's native API. Returns a message ID when available. */
|
|
1439
|
+
sendMessage(_options: SendMessageOptions): Promise<string | null>;
|
|
1440
|
+
/** Release network resources. No-op for backends without a connection. */
|
|
1441
|
+
close(): Promise<void>;
|
|
1442
|
+
}
|
|
1443
|
+
/** What a save callback returns: `false` means "not saved". */
|
|
1444
|
+
type SaveCallbackResult = boolean | null | undefined | void;
|
|
1445
|
+
/**
|
|
1446
|
+
* Called once per fetched batch with only that batch's reports, before any
|
|
1447
|
+
* of its messages is moved or deleted. Returning `false` (or throwing)
|
|
1448
|
+
* marks the batch unsaved; anything else commits it.
|
|
1449
|
+
*/
|
|
1450
|
+
type SaveCallback = (results: ParsingResults) => SaveCallbackResult | Promise<SaveCallbackResult>;
|
|
1451
|
+
/** Mailbox-handling options shared by both entry points (not part of `config`). */
|
|
1452
|
+
interface MailboxProcessingOptions extends ParserConfigInput {
|
|
1453
|
+
/** The folder where reports can be found. Default: `"INBOX"`. */
|
|
1454
|
+
reportsFolder?: string;
|
|
1455
|
+
/** The folder to move processed mail to. Default: `"Archive"`. */
|
|
1456
|
+
archiveFolder?: string;
|
|
1457
|
+
/** Delete messages after processing them. Default: false. */
|
|
1458
|
+
delete?: boolean;
|
|
1459
|
+
/**
|
|
1460
|
+
* Delete aggregate report messages instead of moving them to the
|
|
1461
|
+
* `Aggregate` archive subfolder. `null`/unset inherits `delete`.
|
|
1462
|
+
*/
|
|
1463
|
+
deleteAggregate?: boolean | null;
|
|
1464
|
+
/**
|
|
1465
|
+
* Delete failure report messages instead of moving them to the `Failure`
|
|
1466
|
+
* archive subfolder. `null`/unset inherits `delete`.
|
|
1467
|
+
*/
|
|
1468
|
+
deleteFailure?: boolean | null;
|
|
1469
|
+
/**
|
|
1470
|
+
* Delete SMTP TLS report messages instead of moving them to the
|
|
1471
|
+
* `SMTP-TLS` archive subfolder. `null`/unset inherits `delete`.
|
|
1472
|
+
*/
|
|
1473
|
+
deleteSmtpTls?: boolean | null;
|
|
1474
|
+
/**
|
|
1475
|
+
* Delete unparseable messages instead of moving them to the `Invalid`
|
|
1476
|
+
* archive subfolder. `null`/unset inherits `delete`.
|
|
1477
|
+
*/
|
|
1478
|
+
deleteInvalid?: boolean | null;
|
|
1479
|
+
/** Do not move or delete messages after processing them. Default: false. */
|
|
1480
|
+
test?: boolean;
|
|
1481
|
+
/** Messages to read and process before saving (0: no limit). Default: 10. */
|
|
1482
|
+
batchSize?: number;
|
|
1483
|
+
/**
|
|
1484
|
+
* Only messages since this relative time (`5m`, `3h`, `2d`, `1w`). Any
|
|
1485
|
+
* other string falls back to one day, with a warning.
|
|
1486
|
+
*/
|
|
1487
|
+
since?: string | null;
|
|
1488
|
+
/**
|
|
1489
|
+
* Worker threads used to parse messages. Fetching, archiving and dedup
|
|
1490
|
+
* stay sequential; with `nProcs > 1`, invalid messages are disposed of
|
|
1491
|
+
* after the parsing phase. Default: 1.
|
|
1492
|
+
*/
|
|
1493
|
+
nProcs?: number;
|
|
1494
|
+
/**
|
|
1495
|
+
* Retries a message gets after its batch was first reported unsaved,
|
|
1496
|
+
* before it moves to the `Unsaved` archive subfolder (never deleted).
|
|
1497
|
+
* `0` moves it on the first failed save; negative values throw
|
|
1498
|
+
* `ValueError`. Default: 2.
|
|
1499
|
+
*/
|
|
1500
|
+
maxUnsavedRetries?: number;
|
|
1501
|
+
}
|
|
1502
|
+
/** Options of {@link getDmarcReportsFromMailbox}. */
|
|
1503
|
+
interface GetDmarcReportsFromMailboxOptions extends MailboxProcessingOptions {
|
|
1504
|
+
/** Results from the previous run, to accumulate into. */
|
|
1505
|
+
results?: ParsingResults | null;
|
|
1506
|
+
/** Create the destination folders (not used in watch). Default: true. */
|
|
1507
|
+
createFolders?: boolean;
|
|
1508
|
+
/**
|
|
1509
|
+
* Called once per batch before its messages are archived or deleted.
|
|
1510
|
+
* Returning `false` leaves them in `reportsFolder` for retry (or moves
|
|
1511
|
+
* them to `Unsaved` once retries run out) and keeps their aggregate
|
|
1512
|
+
* report keys out of the dedup cache. Throwing counts as unsaved too, and
|
|
1513
|
+
* the error is rethrown after that bookkeeping. Still invoked in `test`
|
|
1514
|
+
* mode, where the mailbox and retry counters are never touched.
|
|
1515
|
+
*/
|
|
1516
|
+
saveCallback?: SaveCallback | null;
|
|
1517
|
+
}
|
|
1518
|
+
/** Options of {@link watchInbox}. */
|
|
1519
|
+
interface WatchInboxOptions extends MailboxProcessingOptions {
|
|
1520
|
+
/** Seconds to wait for an IMAP IDLE response or between checks. Default: 30. */
|
|
1521
|
+
checkTimeout?: number;
|
|
1522
|
+
/** Returns true when a config reload or shutdown was requested. */
|
|
1523
|
+
configReloading?: (() => boolean) | null;
|
|
1524
|
+
/** Aborts the watch loop. */
|
|
1525
|
+
signal?: AbortSignal;
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* Fetches and parses DMARC and SMTP TLS reports from a mailbox, then
|
|
1529
|
+
* archives (or deletes) each message by report type: `<archive>/Aggregate`,
|
|
1530
|
+
* `Failure`, `SMTP-TLS`, `Invalid`, and `Unsaved` for batches the
|
|
1531
|
+
* `saveCallback` kept failing to save.
|
|
1532
|
+
*
|
|
1533
|
+
* Parsing options (or `config`) work as in every parsing function; the
|
|
1534
|
+
* mailbox options always apply. With `batchSize: 0` the reports folder is
|
|
1535
|
+
* re-checked after the run and new messages are processed recursively.
|
|
1536
|
+
*
|
|
1537
|
+
* Throws an `EnrichmentError` when enrichment fails for every report
|
|
1538
|
+
* (a revoked IPinfo token, no readable IP database): the batch stays in
|
|
1539
|
+
* `reportsFolder`, unsaved, for the next run. Unlike parsedmarc, which
|
|
1540
|
+
* files those reports as invalid or empty, and deletes them with `delete`.
|
|
1541
|
+
*
|
|
1542
|
+
* Throws `ValueError` when a delete option is combined with `test`, when
|
|
1543
|
+
* `maxUnsavedRetries` is negative, or without a connection.
|
|
1544
|
+
*/
|
|
1545
|
+
export declare function getDmarcReportsFromMailbox(connection: MailboxConnection, options?: GetDmarcReportsFromMailboxOptions): Promise<ParsingResults>;
|
|
1546
|
+
/**
|
|
1547
|
+
* Watches the mailbox for new messages and processes them with
|
|
1548
|
+
* {@link getDmarcReportsFromMailbox} on every check, with `callback` as its
|
|
1549
|
+
* `saveCallback`: it runs once per batch, before the batch's messages are
|
|
1550
|
+
* archived or deleted, and returning `false` (or throwing) leaves them for
|
|
1551
|
+
* the next check. What happens to a thrown error is backend-specific: the
|
|
1552
|
+
* IMAP and Maildir watch loops log it and keep checking; Gmail and
|
|
1553
|
+
* Microsoft Graph end the watch.
|
|
1554
|
+
*
|
|
1555
|
+
* Throws `ValueError` for a negative `maxUnsavedRetries` before watching.
|
|
1556
|
+
*/
|
|
1557
|
+
export declare function watchInbox(mailboxConnection: MailboxConnection, callback: SaveCallback, options?: WatchInboxOptions): Promise<void>;
|
|
1558
|
+
interface SaveOutputOptions {
|
|
1559
|
+
/** The path to the directory to save in (default `output`; `~` expands). */
|
|
1560
|
+
outputDirectory?: string;
|
|
1561
|
+
/** Filename for the aggregate JSON file (default `aggregate.json`). */
|
|
1562
|
+
aggregateJsonFilename?: string;
|
|
1563
|
+
/** Filename for the failure JSON file (default `failure.json`). */
|
|
1564
|
+
failureJsonFilename?: string;
|
|
1565
|
+
/** Filename for the SMTP TLS JSON file (default `smtp_tls.json`). */
|
|
1566
|
+
smtpTlsJsonFilename?: string;
|
|
1567
|
+
/** Filename for the aggregate CSV file (default `aggregate.csv`). */
|
|
1568
|
+
aggregateCsvFilename?: string;
|
|
1569
|
+
/** Filename for the failure CSV file (default `failure.csv`). */
|
|
1570
|
+
failureCsvFilename?: string;
|
|
1571
|
+
/** Filename for the SMTP TLS CSV file (default `smtp_tls.csv`). */
|
|
1572
|
+
smtpTlsCsvFilename?: string;
|
|
1573
|
+
/**
|
|
1574
|
+
* Prefix CSV text cells that a spreadsheet would run as a formula with
|
|
1575
|
+
* `'` (see `CsvOptions.formulaEscape`). Default true, like parsedmarc.
|
|
1576
|
+
*/
|
|
1577
|
+
csvFormulaEscape?: boolean;
|
|
1578
|
+
}
|
|
1579
|
+
interface GetReportZipOptions {
|
|
1580
|
+
/** See {@link SaveOutputOptions.csvFormulaEscape}. Default true. */
|
|
1581
|
+
csvFormulaEscape?: boolean;
|
|
1582
|
+
}
|
|
1583
|
+
/**
|
|
1584
|
+
* Append `reports` to a JSON array on disk, creating the file if needed.
|
|
1585
|
+
*
|
|
1586
|
+
* Reads the existing array (if the file exists and parses cleanly), merges
|
|
1587
|
+
* the new reports onto the end, and replaces the file with a single valid
|
|
1588
|
+
* JSON array (indented by 2, keys in snake_case like parsedmarc's output).
|
|
1589
|
+
* A file above {@link MAX_REWRITTEN_JSON_BYTES} that already has that layout
|
|
1590
|
+
* keeps its bytes and only gets the new entries, which gives the same result.
|
|
1591
|
+
* A file that can't be used (corrupt, unreadable, a root that is not an
|
|
1592
|
+
* array) is moved to `<filename>.corrupt-<timestamp>` with a warning, never
|
|
1593
|
+
* discarded. An empty `reports` leaves the file alone (and creates nothing).
|
|
1594
|
+
* Calls for one file run in turn; other processes are not coordinated with.
|
|
1595
|
+
*
|
|
1596
|
+
* Throws `ValueError` when a single report is too large to serialize.
|
|
1597
|
+
*/
|
|
1598
|
+
export declare function appendJson(filename: string, reports: readonly AggregateReport[] | readonly FailureReport[] | readonly SMTPTLSReport[]): Promise<void>;
|
|
1599
|
+
/**
|
|
1600
|
+
* Append CSV text to a file. When the file already has content, the header
|
|
1601
|
+
* line of `csv` is dropped; a header-only `csv` then writes nothing.
|
|
1602
|
+
*/
|
|
1603
|
+
export declare function appendCsv(filename: string, csv: string): Promise<void>;
|
|
1604
|
+
/**
|
|
1605
|
+
* Save report data in the given directory.
|
|
1606
|
+
*
|
|
1607
|
+
* Writes the JSON and CSV files of each report type, then the message
|
|
1608
|
+
* sample of each failure report to a `samples` subdirectory, named after the
|
|
1609
|
+
* sample's own `subject` header run through {@link getFilenameSafeString},
|
|
1610
|
+
* falling back to `sample` when sanitizing leaves nothing. A subject comes
|
|
1611
|
+
* from an untrusted sender, so it is sanitized here, at write time; a
|
|
1612
|
+
* caller-supplied `filenameSafeSubject` is not trusted and not used. Names
|
|
1613
|
+
* that collide, in this call or with a file already there, get a ` (1)`,
|
|
1614
|
+
* ` (2)`, … suffix.
|
|
1615
|
+
*
|
|
1616
|
+
* Throws `ValueError` if `outputDirectory` exists and is not a directory.
|
|
1617
|
+
*/
|
|
1618
|
+
export declare function saveOutput(results: ParsingResults, options?: SaveOutputOptions): Promise<void>;
|
|
1619
|
+
/**
|
|
1620
|
+
* Creates a zip file of parsed report output: {@link saveOutput} into a
|
|
1621
|
+
* temporary directory, then every file in it.
|
|
1622
|
+
*
|
|
1623
|
+
* The layout is parsedmarc's, except that parsedmarc also stores each file
|
|
1624
|
+
* of `samples/` a second time as `../<name>` (its `add_subdir()` swaps the
|
|
1625
|
+
* `relpath` arguments). Those entries point outside the extraction
|
|
1626
|
+
* directory, a path-traversal risk for whoever unzips the attachment, so
|
|
1627
|
+
* they are left out; every file is still in the zip once.
|
|
1628
|
+
*/
|
|
1629
|
+
export declare function getReportZip(results: ParsingResults, options?: GetReportZipOptions): Promise<Uint8Array>;
|
|
1630
|
+
/**
|
|
1631
|
+
* Runs send tasks one after another. Sockets are unref'd, like Python's,
|
|
1632
|
+
* which never hold the interpreter open; while sends are queued a timer
|
|
1633
|
+
* keeps the event loop alive so they are not cut short.
|
|
1634
|
+
*/
|
|
1635
|
+
declare class SendQueue {
|
|
1636
|
+
#private;
|
|
1637
|
+
/** Queue `task`; its error goes to `onError` (never rejects the queue). */
|
|
1638
|
+
push(task: () => Promise<void>, onError: (error: unknown) => void): void;
|
|
1639
|
+
/** Resolves once every queued send has finished. */
|
|
1640
|
+
drain(): Promise<void>;
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1643
|
+
* Messages a stream handler could not deliver. `emit()` can't report them
|
|
1644
|
+
* and `SendQueue` runs on, so the client's save method asks here after
|
|
1645
|
+
* draining and fails, which keeps the batch from counting as saved.
|
|
1646
|
+
*/
|
|
1647
|
+
declare class SendFailures {
|
|
1648
|
+
#private;
|
|
1649
|
+
record(error: unknown): void;
|
|
1650
|
+
/** The failures since the last call, or `null` when there were none. */
|
|
1651
|
+
take(): {
|
|
1652
|
+
count: number;
|
|
1653
|
+
error: unknown;
|
|
1654
|
+
} | null;
|
|
1655
|
+
}
|
|
1656
|
+
interface GelfHandlerOptions {
|
|
1657
|
+
/** GELF spec version (default `1.1`). */
|
|
1658
|
+
version?: string;
|
|
1659
|
+
/** Send the record's extra fields as `_<name>` (default false). */
|
|
1660
|
+
includeExtraFields?: boolean;
|
|
1661
|
+
/** zlib-compress each message (default: true for UDP, false otherwise). */
|
|
1662
|
+
compress?: boolean;
|
|
1663
|
+
}
|
|
1664
|
+
/** pygelf's `BaseHandler`: turns records into GELF. */
|
|
1665
|
+
declare abstract class BaseGelfHandler extends Handler {
|
|
1666
|
+
readonly host: string;
|
|
1667
|
+
readonly port: number;
|
|
1668
|
+
readonly version: string;
|
|
1669
|
+
readonly includeExtraFields: boolean;
|
|
1670
|
+
readonly compress: boolean;
|
|
1671
|
+
/** The `host` field: this machine's hostname. */
|
|
1672
|
+
readonly domain: string;
|
|
1673
|
+
protected readonly queue: SendQueue;
|
|
1674
|
+
protected readonly failures: SendFailures;
|
|
1675
|
+
constructor(host: string, port: number, options: GelfHandlerOptions, compressDefault: boolean);
|
|
1676
|
+
convertRecordToGelf(record: LogRecord): Uint8Array;
|
|
1677
|
+
/** The bytes sent for one record. */
|
|
1678
|
+
makePickle(record: LogRecord): Uint8Array;
|
|
1679
|
+
protected abstract send(data: Uint8Array): Promise<void>;
|
|
1680
|
+
emit(record: LogRecord): void;
|
|
1681
|
+
/** Resolves once every record emitted so far has been sent (or dropped). */
|
|
1682
|
+
drain(): Promise<void>;
|
|
1683
|
+
/** The messages dropped since the last call (always none over UDP). */
|
|
1684
|
+
takeFailures(): {
|
|
1685
|
+
count: number;
|
|
1686
|
+
error: unknown;
|
|
1687
|
+
} | null;
|
|
1688
|
+
override close(): void;
|
|
1689
|
+
abstract closeAsync(): Promise<void>;
|
|
1690
|
+
}
|
|
1691
|
+
/**
|
|
1692
|
+
* pygelf's `GelfTcpHandler` over Python's `SocketHandler`: connects on the
|
|
1693
|
+
* first record, sends each message NUL-terminated, and on a connection or
|
|
1694
|
+
* send error closes the socket and retries later with exponential back-off
|
|
1695
|
+
* (1 s doubling up to 30 s). Unlike Python, a message that fails on a stale
|
|
1696
|
+
* connection is sent again once over a new one, and one that is still lost
|
|
1697
|
+
* is recorded in `failures` (`SocketHandler` drops it without a trace).
|
|
1698
|
+
*/
|
|
1699
|
+
export declare class GelfTcpHandler extends BaseGelfHandler {
|
|
1700
|
+
#private;
|
|
1701
|
+
sock: Socket$1 | null;
|
|
1702
|
+
/** Epoch ms before which no reconnect is attempted. */
|
|
1703
|
+
retryTime: number | null;
|
|
1704
|
+
retryPeriod: number;
|
|
1705
|
+
constructor(host: string, port: number, options?: GelfHandlerOptions);
|
|
1706
|
+
/** NUL-terminated: "if you send the message over tcp, it should always be null terminated". */
|
|
1707
|
+
override makePickle(record: LogRecord): Uint8Array;
|
|
1708
|
+
/** Open the connection (`SocketHandler.makeSocket`). */
|
|
1709
|
+
protected makeSocket(timeout?: number): Promise<Socket$1>;
|
|
1710
|
+
/** `SocketHandler.createSocket()`: connect unless backing off. */
|
|
1711
|
+
createSocket(): Promise<void>;
|
|
1712
|
+
protected send(data: Uint8Array): Promise<void>;
|
|
1713
|
+
closeAsync(): Promise<void>;
|
|
1714
|
+
}
|
|
1715
|
+
interface GelfUdpHandlerOptions extends GelfHandlerOptions {
|
|
1716
|
+
/** Maximum datagram size before chunking (default 1300). */
|
|
1717
|
+
chunkSize?: number;
|
|
1718
|
+
}
|
|
1719
|
+
/**
|
|
1720
|
+
* pygelf's `GelfUdpHandler`: zlib-compressed datagrams (IPv4, like Python's
|
|
1721
|
+
* `DatagramHandler`), split into GELF chunks above `chunkSize` bytes.
|
|
1722
|
+
*/
|
|
1723
|
+
export declare class GelfUdpHandler extends BaseGelfHandler {
|
|
1724
|
+
readonly chunkSize: number;
|
|
1725
|
+
sock: Socket | null;
|
|
1726
|
+
constructor(host: string, port: number, options?: GelfUdpHandlerOptions);
|
|
1727
|
+
protected send(data: Uint8Array): Promise<void>;
|
|
1728
|
+
closeAsync(): Promise<void>;
|
|
1729
|
+
}
|
|
1730
|
+
interface GelfTlsOptions {
|
|
1731
|
+
/**
|
|
1732
|
+
* Verify the server certificate and hostname (default true). pygelf
|
|
1733
|
+
* defaults to false; see the deviations in the architecture notes.
|
|
1734
|
+
*/
|
|
1735
|
+
validate?: boolean;
|
|
1736
|
+
/** CA bundle file, added to the system CAs. */
|
|
1737
|
+
caCerts?: string | null;
|
|
1738
|
+
/** Client certificate file (may also hold the key). */
|
|
1739
|
+
certfile?: string | null;
|
|
1740
|
+
/** Client private key file. */
|
|
1741
|
+
keyfile?: string | null;
|
|
1742
|
+
}
|
|
1743
|
+
/** pygelf's `GelfTlsHandler`: {@link GelfTcpHandler} over TLS (IPv4). */
|
|
1744
|
+
export declare class GelfTlsHandler extends GelfTcpHandler {
|
|
1745
|
+
readonly tlsOptions: ConnectionOptions;
|
|
1746
|
+
constructor(host: string, port: number, options?: GelfHandlerOptions & GelfTlsOptions);
|
|
1747
|
+
protected override makeSocket(timeout?: number): Promise<Socket$1>;
|
|
1748
|
+
}
|
|
1749
|
+
/** parsedmarc's thread-local `log_context_data`: the row being logged. */
|
|
1750
|
+
export declare const logContextData: {
|
|
1751
|
+
parsedmarc?: unknown;
|
|
1752
|
+
};
|
|
1753
|
+
/** Copies `logContextData.parsedmarc` onto each record as `parsedmarc`. */
|
|
1754
|
+
export declare class ContextFilter {
|
|
1755
|
+
readonly filter: (record: LogRecord) => boolean;
|
|
1756
|
+
}
|
|
1757
|
+
/** Raised when messages could not be delivered over TCP or TLS. */
|
|
1758
|
+
export declare class GelfError extends Error {
|
|
1759
|
+
override name: string;
|
|
1760
|
+
}
|
|
1761
|
+
type GelfMode = "udp" | "tcp" | "tls";
|
|
1762
|
+
interface GelfClientOptions {
|
|
1763
|
+
/** TLS settings for `tls` mode. */
|
|
1764
|
+
tls?: GelfTlsOptions;
|
|
1765
|
+
}
|
|
1766
|
+
type GelfHandler = GelfUdpHandler | GelfTcpHandler | GelfTlsHandler;
|
|
1767
|
+
/** A client for the Graylog Extended Log Format. */
|
|
1768
|
+
export declare class GelfClient {
|
|
1769
|
+
#private;
|
|
1770
|
+
readonly host: string;
|
|
1771
|
+
readonly port: number;
|
|
1772
|
+
readonly logger: Logger;
|
|
1773
|
+
readonly gelfMode: Readonly<Record<GelfMode, (host: string, port: number) => GelfHandler>>;
|
|
1774
|
+
/** The GELF handler attached to {@link logger}. */
|
|
1775
|
+
handler: Handler;
|
|
1776
|
+
/**
|
|
1777
|
+
* @param host - The GELF host
|
|
1778
|
+
* @param port - The GELF port
|
|
1779
|
+
* @param mode - The GELF transport mode: `udp`, `tcp` or `tls`
|
|
1780
|
+
*/
|
|
1781
|
+
constructor(host: string, port: number, mode: string, options?: GelfClientOptions);
|
|
1782
|
+
saveAggregateReportToGelf(aggregateReports: AggregateReport | AggregateReport[]): Promise<void>;
|
|
1783
|
+
saveFailureReportToGelf(failureReports: FailureReport | FailureReport[]): Promise<void>;
|
|
1784
|
+
/** Backward-compatible alias of {@link saveFailureReportToGelf}. */
|
|
1785
|
+
saveForensicReportToGelf(failureReports: FailureReport | FailureReport[]): Promise<void>;
|
|
1786
|
+
saveSmtpTlsReportToGelf(smtpTlsReports: SMTPTLSReport | SMTPTLSReport[]): Promise<void>;
|
|
1787
|
+
/** Remove and close the GELF handler, releasing its connection. */
|
|
1788
|
+
close(): Promise<void>;
|
|
1789
|
+
}
|
|
1790
|
+
/** Raised when a Splunk API error occurs. */
|
|
1791
|
+
export declare class SplunkError extends Error {
|
|
1792
|
+
override name: string;
|
|
1793
|
+
}
|
|
1794
|
+
interface HECClientOptions {
|
|
1795
|
+
/** The source name (default `"tsdmarc"`). */
|
|
1796
|
+
source?: string;
|
|
1797
|
+
/** Verify TLS certificates (default `true`). */
|
|
1798
|
+
verify?: boolean;
|
|
1799
|
+
/** Seconds to wait for the server before giving up (default 60). */
|
|
1800
|
+
timeout?: number;
|
|
1801
|
+
}
|
|
1802
|
+
type JsonObject = Record<string, unknown>;
|
|
1803
|
+
/** A client for a Splunk HTTP Events Collector (HEC). */
|
|
1804
|
+
export declare class HECClient {
|
|
1805
|
+
#private;
|
|
1806
|
+
readonly url: string;
|
|
1807
|
+
readonly accessToken: string;
|
|
1808
|
+
readonly index: string;
|
|
1809
|
+
readonly host: string;
|
|
1810
|
+
readonly source: string;
|
|
1811
|
+
readonly timeout: number;
|
|
1812
|
+
readonly verify: boolean;
|
|
1813
|
+
/** Headers sent with every request. */
|
|
1814
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
1815
|
+
/**
|
|
1816
|
+
* @param url - The URL of the HEC; only its scheme and host are used
|
|
1817
|
+
* @param accessToken - The HEC access token
|
|
1818
|
+
* @param index - The name of the index
|
|
1819
|
+
*/
|
|
1820
|
+
constructor(url: string, accessToken: string, index: string, options?: HECClientOptions);
|
|
1821
|
+
/** The `host`, `source` and `index` fields every event carries. */
|
|
1822
|
+
get commonData(): Readonly<JsonObject>;
|
|
1823
|
+
/** Saves aggregate DMARC reports to Splunk, one event per record. */
|
|
1824
|
+
saveAggregateReportsToSplunk(aggregateReports: AggregateReport | AggregateReport[]): Promise<void>;
|
|
1825
|
+
/** Saves failure DMARC reports to Splunk, one event per report. */
|
|
1826
|
+
saveFailureReportsToSplunk(failureReports: FailureReport | FailureReport[]): Promise<void>;
|
|
1827
|
+
/** Backward-compatible alias of {@link saveFailureReportsToSplunk}. */
|
|
1828
|
+
saveForensicReportsToSplunk(failureReports: FailureReport | FailureReport[]): Promise<void>;
|
|
1829
|
+
/** Saves SMTP TLS reports to Splunk, one event per report. */
|
|
1830
|
+
saveSmtpTlsReportsToSplunk(reports: SMTPTLSReport | SMTPTLSReport[]): Promise<void>;
|
|
1831
|
+
/** Close the client: abort in-flight requests and refuse new ones. */
|
|
1832
|
+
close(): void;
|
|
1833
|
+
}
|
|
1834
|
+
export declare const LOKI_PUSH_PATH = "/loki/api/v1/push";
|
|
1835
|
+
export declare const LOKI_TIMESTAMP_MODES: readonly LokiTimestampMode[];
|
|
1836
|
+
export declare const LOKI_DEFAULT_LABEL_FIELDS: readonly string[];
|
|
1837
|
+
type LokiTimestampMode = "ingest" | "report_begin";
|
|
1838
|
+
type LokiReportType = "aggregate" | "failure" | "smtp_tls";
|
|
1839
|
+
/** Raised when pushing to Loki fails. */
|
|
1840
|
+
export declare class LokiError extends Error {
|
|
1841
|
+
override name: string;
|
|
1842
|
+
}
|
|
1843
|
+
interface LokiClientOptions {
|
|
1844
|
+
/**
|
|
1845
|
+
* Static stream labels, merged over the defaults `job=parsedmarc` and
|
|
1846
|
+
* `host=<host name>`.
|
|
1847
|
+
*/
|
|
1848
|
+
labels?: Readonly<Record<string, string>> | null;
|
|
1849
|
+
/**
|
|
1850
|
+
* Record fields used as stream labels (default `report_type`, `domain`);
|
|
1851
|
+
* keep them to fields with few distinct values.
|
|
1852
|
+
*/
|
|
1853
|
+
labelFields?: readonly string[] | null;
|
|
1854
|
+
/**
|
|
1855
|
+
* `ingest` (time of the push, the default) or `report_begin` (begin of the
|
|
1856
|
+
* reported period; Loki rejects entries older than its
|
|
1857
|
+
* `reject_old_samples_max_age`).
|
|
1858
|
+
*/
|
|
1859
|
+
timestamp?: LokiTimestampMode;
|
|
1860
|
+
/** Basic auth user. */
|
|
1861
|
+
username?: string | null;
|
|
1862
|
+
/** Basic auth password (used only with `username`). */
|
|
1863
|
+
password?: string | null;
|
|
1864
|
+
/** Bearer token, instead of basic auth. */
|
|
1865
|
+
token?: string | null;
|
|
1866
|
+
/** Sent as `X-Scope-OrgID`. */
|
|
1867
|
+
tenantId?: string | null;
|
|
1868
|
+
/** Verify TLS certificates (default `true`). */
|
|
1869
|
+
verify?: boolean;
|
|
1870
|
+
/** Request timeout in seconds (default 60). */
|
|
1871
|
+
timeout?: number;
|
|
1872
|
+
/** Maximum log lines per push request (default 1000). */
|
|
1873
|
+
batchSize?: number;
|
|
1874
|
+
}
|
|
1875
|
+
/** A client for the Grafana Loki push API. */
|
|
1876
|
+
export declare class LokiClient {
|
|
1877
|
+
#private;
|
|
1878
|
+
/** The push URL (with its userinfo, if any). */
|
|
1879
|
+
readonly url: string;
|
|
1880
|
+
readonly labels: Readonly<Record<string, string>>;
|
|
1881
|
+
readonly labelFields: readonly string[];
|
|
1882
|
+
readonly timestamp: LokiTimestampMode;
|
|
1883
|
+
readonly timeout: number;
|
|
1884
|
+
readonly batchSize: number;
|
|
1885
|
+
readonly verify: boolean;
|
|
1886
|
+
/** Headers sent with every request. */
|
|
1887
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
1888
|
+
/**
|
|
1889
|
+
* Every report record becomes one JSON log line.
|
|
1890
|
+
*
|
|
1891
|
+
* @param url - Loki base URL (`/loki/api/v1/push` is appended when the URL
|
|
1892
|
+
* has no path) or the full push URL
|
|
1893
|
+
*/
|
|
1894
|
+
constructor(url: string, options?: LokiClientOptions);
|
|
1895
|
+
saveAggregateReportToLoki(aggregateReports: AggregateReport | AggregateReport[]): Promise<void>;
|
|
1896
|
+
saveFailureReportToLoki(failureReports: FailureReport | FailureReport[]): Promise<void>;
|
|
1897
|
+
/** Backward-compatible alias of {@link saveFailureReportToLoki}. */
|
|
1898
|
+
saveForensicReportToLoki(failureReports: FailureReport | FailureReport[]): Promise<void>;
|
|
1899
|
+
saveSmtpTlsReportToLoki(smtpTlsReports: SMTPTLSReport | SMTPTLSReport[]): Promise<void>;
|
|
1900
|
+
/** Close the client: abort in-flight requests and refuse new ones. */
|
|
1901
|
+
close(): void;
|
|
1902
|
+
}
|
|
1903
|
+
/** `LOG_USER`, SysLogHandler's default facility. */
|
|
1904
|
+
export declare const LOG_USER = 1;
|
|
1905
|
+
type SyslogSocketType = "dgram" | "stream";
|
|
1906
|
+
interface SysLogHandlerOptions {
|
|
1907
|
+
/** `socket.SOCK_DGRAM` (UDP, the default) or `SOCK_STREAM` (TCP). */
|
|
1908
|
+
socktype?: SyslogSocketType;
|
|
1909
|
+
/** Seconds for connect and send; `null` (the default) waits forever. */
|
|
1910
|
+
timeout?: number | null;
|
|
1911
|
+
/** Turns each new TCP connection into the one to use (TLS). */
|
|
1912
|
+
wrapSocket?: (socket: Socket$1) => Promise<Socket$1>;
|
|
1913
|
+
}
|
|
1914
|
+
/**
|
|
1915
|
+
* Port of `logging.handlers.SysLogHandler` for a `(host, port)` address:
|
|
1916
|
+
* sends `<PRI>message\0` per record, as one datagram (UDP) or on one
|
|
1917
|
+
* connection (TCP, no octet counting, like Python). Create it with
|
|
1918
|
+
* {@link SysLogHandler.create}, which resolves the host and, for TCP,
|
|
1919
|
+
* connects, like Python's constructor. Unlike Python, which keeps using a
|
|
1920
|
+
* dead TCP socket forever, a failed send reconnects (with `SocketHandler`'s
|
|
1921
|
+
* back-off) and tries once more; what is still lost is in {@link takeFailures}.
|
|
1922
|
+
*/
|
|
1923
|
+
export declare class SysLogHandler extends Handler {
|
|
1924
|
+
#private;
|
|
1925
|
+
readonly address: readonly [string, number];
|
|
1926
|
+
readonly facility: number;
|
|
1927
|
+
readonly socktype: SyslogSocketType;
|
|
1928
|
+
/** Seconds for each send on a stream socket; `null` waits forever. */
|
|
1929
|
+
timeout: number | null;
|
|
1930
|
+
/** The datagram socket, or the (possibly TLS-wrapped) stream socket. */
|
|
1931
|
+
socket: Socket | Socket$1 | null;
|
|
1932
|
+
protected constructor(address: readonly [string, number], options: SysLogHandlerOptions);
|
|
1933
|
+
/** Create the handler and its socket (throws if that fails). */
|
|
1934
|
+
static create(address: readonly [string, number], options?: SysLogHandlerOptions): Promise<SysLogHandler>;
|
|
1935
|
+
/**
|
|
1936
|
+
* Resolve the address and create the socket; for TCP, connect to the
|
|
1937
|
+
* first address that accepts, like Python's `createSocket()`.
|
|
1938
|
+
*/
|
|
1939
|
+
createSocket(): Promise<void>;
|
|
1940
|
+
/** `SysLogHandler.encodePriority()` with `mapPriority()`. */
|
|
1941
|
+
encodePriority(levelName: string): number;
|
|
1942
|
+
emit(record: LogRecord): void;
|
|
1943
|
+
/** Resolves once every record emitted so far has been sent (or failed). */
|
|
1944
|
+
drain(): Promise<void>;
|
|
1945
|
+
/** The messages lost over TCP/TLS since the last call (UDP has no record). */
|
|
1946
|
+
takeFailures(): {
|
|
1947
|
+
count: number;
|
|
1948
|
+
error: unknown;
|
|
1949
|
+
} | null;
|
|
1950
|
+
/** Close the socket (after the queued sends). Use {@link closeAsync} to wait. */
|
|
1951
|
+
override close(): void;
|
|
1952
|
+
/** Send what is queued, then close the socket. */
|
|
1953
|
+
closeAsync(): Promise<void>;
|
|
1954
|
+
}
|
|
1955
|
+
interface SyslogClientOptions {
|
|
1956
|
+
/** `udp` (default), `tcp` or `tls`; case-insensitive. */
|
|
1957
|
+
protocol?: string;
|
|
1958
|
+
/** CA certificate file for TLS server verification, added to the system CAs. */
|
|
1959
|
+
cafilePath?: string | null;
|
|
1960
|
+
/** Client certificate file for TLS authentication. */
|
|
1961
|
+
certfilePath?: string | null;
|
|
1962
|
+
/** Client private key file for TLS authentication. */
|
|
1963
|
+
keyfilePath?: string | null;
|
|
1964
|
+
/** Connection timeout in seconds for TCP/TLS (default 5). */
|
|
1965
|
+
timeout?: number;
|
|
1966
|
+
/** Connection attempts for TCP/TLS (default 3). */
|
|
1967
|
+
retryAttempts?: number;
|
|
1968
|
+
/** Seconds between connection attempts (default 5). */
|
|
1969
|
+
retryDelay?: number;
|
|
1970
|
+
}
|
|
1971
|
+
/** Raised when messages could not be delivered over TCP or TLS. */
|
|
1972
|
+
export declare class SyslogError extends Error {
|
|
1973
|
+
override name: string;
|
|
1974
|
+
}
|
|
1975
|
+
/** A client for Syslog. */
|
|
1976
|
+
export declare class SyslogClient {
|
|
1977
|
+
#private;
|
|
1978
|
+
readonly serverName: string;
|
|
1979
|
+
readonly serverPort: number;
|
|
1980
|
+
readonly protocol: string;
|
|
1981
|
+
readonly timeout: number;
|
|
1982
|
+
readonly retryAttempts: number;
|
|
1983
|
+
readonly retryDelay: number;
|
|
1984
|
+
readonly logger: Logger;
|
|
1985
|
+
/** The syslog handler attached to {@link logger}. */
|
|
1986
|
+
logHandler: Handler;
|
|
1987
|
+
protected constructor(serverName: string, serverPort: number, options: SyslogClientOptions);
|
|
1988
|
+
/**
|
|
1989
|
+
* Create a SyslogClient: resolves the server and, for TCP/TLS, connects
|
|
1990
|
+
* (retrying `retryAttempts` times, `retryDelay` seconds apart), so a bad
|
|
1991
|
+
* address fails here as Python's constructor does.
|
|
1992
|
+
*
|
|
1993
|
+
* @param serverName - The Syslog server
|
|
1994
|
+
* @param serverPort - The Syslog port
|
|
1995
|
+
*/
|
|
1996
|
+
static create(serverName: string, serverPort: number, options?: SyslogClientOptions): Promise<SyslogClient>;
|
|
1997
|
+
/** Creates a SysLogHandler with the specified protocol and TLS settings. */
|
|
1998
|
+
protected createSyslogHandler(options: SyslogClientOptions): Promise<SysLogHandler>;
|
|
1999
|
+
/** TCP connection wrapped in TLS ≥ 1.2, verifying the server. */
|
|
2000
|
+
protected createTlsHandler(options: SyslogClientOptions): Promise<SysLogHandler>;
|
|
2001
|
+
saveAggregateReportToSyslog(aggregateReports: AggregateReport | AggregateReport[]): Promise<void>;
|
|
2002
|
+
saveFailureReportToSyslog(failureReports: FailureReport | FailureReport[]): Promise<void>;
|
|
2003
|
+
/** Backward-compatible alias of {@link saveFailureReportToSyslog}. */
|
|
2004
|
+
saveForensicReportToSyslog(failureReports: FailureReport | FailureReport[]): Promise<void>;
|
|
2005
|
+
saveSmtpTlsReportToSyslog(smtpTlsReports: SMTPTLSReport | SMTPTLSReport[]): Promise<void>;
|
|
2006
|
+
/** Remove and close the syslog handler, releasing its socket. */
|
|
2007
|
+
close(): Promise<void>;
|
|
2008
|
+
}
|
|
2009
|
+
type WebhookPayload = string | Uint8Array | Record<string, string>;
|
|
2010
|
+
interface WebhookClientOptions {
|
|
2011
|
+
/** Seconds to wait for each webhook call (default 60); `null` waits forever. */
|
|
2012
|
+
timeout?: number | null;
|
|
2013
|
+
/**
|
|
2014
|
+
* Throw network errors, timeouts and non-2xx responses instead of only
|
|
2015
|
+
* logging them (default `false`, as parsedmarc). Turn it on when the
|
|
2016
|
+
* caller has to know a report was not delivered, e.g. before deleting
|
|
2017
|
+
* its email; the CLI does.
|
|
2018
|
+
*/
|
|
2019
|
+
throwOnError?: boolean;
|
|
2020
|
+
}
|
|
2021
|
+
/** A client for webhooks. */
|
|
2022
|
+
export declare class WebhookClient {
|
|
2023
|
+
#private;
|
|
2024
|
+
readonly aggregateUrl: string;
|
|
2025
|
+
readonly failureUrl: string;
|
|
2026
|
+
readonly smtpTlsUrl: string;
|
|
2027
|
+
readonly timeout: number | null;
|
|
2028
|
+
readonly throwOnError: boolean;
|
|
2029
|
+
/** Headers sent with every request. */
|
|
2030
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
2031
|
+
/**
|
|
2032
|
+
* @param aggregateUrl - The aggregate report webhook URL
|
|
2033
|
+
* @param failureUrl - The failure report webhook URL
|
|
2034
|
+
* @param smtpTlsUrl - The SMTP TLS report webhook URL
|
|
2035
|
+
*/
|
|
2036
|
+
constructor(aggregateUrl: string, failureUrl: string, smtpTlsUrl: string, options?: WebhookClientOptions);
|
|
2037
|
+
saveFailureReportToWebhook(report: string): Promise<void>;
|
|
2038
|
+
/** Backward-compatible alias of {@link saveFailureReportToWebhook}. */
|
|
2039
|
+
saveForensicReportToWebhook(report: string): Promise<void>;
|
|
2040
|
+
saveSmtpTlsReportToWebhook(report: string): Promise<void>;
|
|
2041
|
+
saveAggregateReportToWebhook(report: string): Promise<void>;
|
|
2042
|
+
/**
|
|
2043
|
+
* POST `payload` to `webhookUrl`. A string or bytes payload is sent as
|
|
2044
|
+
* is; an object is form-encoded. Network errors are logged and swallowed
|
|
2045
|
+
* and HTTP error statuses ignored, so a failing webhook never aborts the
|
|
2046
|
+
* surrounding batch; with `throwOnError`, both are thrown instead (and
|
|
2047
|
+
* not logged here).
|
|
2048
|
+
*/
|
|
2049
|
+
sendToWebhook(webhookUrl: string, payload: WebhookPayload): Promise<void>;
|
|
2050
|
+
/** Close the client: abort in-flight requests and refuse new ones. */
|
|
2051
|
+
close(): void;
|
|
2052
|
+
}
|
|
2053
|
+
/** The Maildir directory does not exist and `maildirCreate` was not set. */
|
|
2054
|
+
export declare class NoSuchMailboxError extends Error {
|
|
2055
|
+
override name: string;
|
|
2056
|
+
}
|
|
2057
|
+
/** A folder can't be removed because it still holds messages or subfolders. */
|
|
2058
|
+
export declare class NotEmptyError extends Error {
|
|
2059
|
+
override name: string;
|
|
2060
|
+
}
|
|
2061
|
+
/** A move would overwrite a message with the same key (`mailbox.ExternalClashError`). */
|
|
2062
|
+
export declare class ExternalClashError extends Error {
|
|
2063
|
+
override name: string;
|
|
2064
|
+
}
|
|
2065
|
+
/**
|
|
2066
|
+
* Running as root, the process could not fully drop to the Maildir's owner
|
|
2067
|
+
* (uid, gid and supplementary groups). mailsuite only warns and stays root.
|
|
2068
|
+
*/
|
|
2069
|
+
export declare class PrivilegeDropError extends Error {
|
|
2070
|
+
override name: string;
|
|
2071
|
+
}
|
|
2072
|
+
/** No message has the given key (Python's `KeyError`). */
|
|
2073
|
+
export declare class MaildirKeyError extends Error {
|
|
2074
|
+
override name: string;
|
|
2075
|
+
}
|
|
2076
|
+
interface MaildirConnectionOptions {
|
|
2077
|
+
/** Create the Maildir (`cur`, `new`, `tmp`) if it doesn't exist. Default: false. */
|
|
2078
|
+
maildirCreate?: boolean;
|
|
2079
|
+
}
|
|
2080
|
+
/**
|
|
2081
|
+
* A `MailboxConnection` backed by an on-disk Maildir.
|
|
2082
|
+
*
|
|
2083
|
+
* Useful for local processing of messages dropped into a Maildir by an MTA
|
|
2084
|
+
* (e.g. postfix delivering DMARC reports). Maildir has no concept of
|
|
2085
|
+
* sending: `sendMessage()` throws `NotImplementedError`.
|
|
2086
|
+
*
|
|
2087
|
+
* The constructor does its filesystem checks synchronously, like
|
|
2088
|
+
* mailsuite's: uid checks (warn, don't crash, but throw `PrivilegeDropError`
|
|
2089
|
+
* if root can't drop to the Maildir's owner), the optional `cur`/`new`/`tmp`
|
|
2090
|
+
* creation, and opening the Maildir.
|
|
2091
|
+
*/
|
|
2092
|
+
export declare class MaildirConnection extends MailboxConnection {
|
|
2093
|
+
readonly kind: "maildir";
|
|
2094
|
+
private readonly maildirPath;
|
|
2095
|
+
private readonly maildirCreate;
|
|
2096
|
+
private readonly client;
|
|
2097
|
+
private activeFolder;
|
|
2098
|
+
private readonly subfolderClient;
|
|
2099
|
+
constructor(maildirPath: string, options?: MaildirConnectionOptions);
|
|
2100
|
+
private getFolder;
|
|
2101
|
+
private folderPath;
|
|
2102
|
+
override createFolder(folderName: string): Promise<void>;
|
|
2103
|
+
override renameFolder(oldName: string, newName: string): Promise<void>;
|
|
2104
|
+
override folderExists(folderName: string): Promise<boolean>;
|
|
2105
|
+
override deleteFolder(folderName: string): Promise<void>;
|
|
2106
|
+
protected override doMoveFolder(source: string, _targetParent: string, targetPath: string): Promise<void>;
|
|
2107
|
+
override fetchMessages(reportsFolder: string, _options?: FetchMessagesOptions): Promise<MessageId[]>;
|
|
2108
|
+
/**
|
|
2109
|
+
* The message's stored bytes decoded as UTF-8 (undecodable bytes
|
|
2110
|
+
* replaced), like mailsuite 2.3.3 and its IMAP backend's raw fetch, or
|
|
2111
|
+
* `""` for an unknown key. The file is not parsed or re-serialized, so a
|
|
2112
|
+
* message Python's `email` package can parse but not generate (a `\x1c`
|
|
2113
|
+
* in a nested header, deep multipart nesting) is returned as is.
|
|
2114
|
+
*
|
|
2115
|
+
* With `markRead` the file is renamed into `cur/` with the `S` flag (see
|
|
2116
|
+
* `Maildir.markRead`). With `maxSize`, a larger file throws
|
|
2117
|
+
* `EmailTooLargeError` without being read (not in mailsuite).
|
|
2118
|
+
*/
|
|
2119
|
+
override fetchMessage(messageId: MessageId, options?: FetchMessageOptions): Promise<string>;
|
|
2120
|
+
override deleteMessage(messageId: MessageId): Promise<void>;
|
|
2121
|
+
/**
|
|
2122
|
+
* Move the message's file into the same subdirectory (`new`/`cur`) of
|
|
2123
|
+
* `folderName`, keeping its file name, flags and bytes, like mailsuite
|
|
2124
|
+
* 2.3.3 (it no longer re-serializes the message). Maildir++ folders live
|
|
2125
|
+
* under the same root, so this is a same-filesystem rename; a folder on
|
|
2126
|
+
* another mount fails with `EXDEV`. Throws {@link ExternalClashError}
|
|
2127
|
+
* rather than overwriting when the destination already holds a message
|
|
2128
|
+
* with the same key (including a move into the active folder itself).
|
|
2129
|
+
*/
|
|
2130
|
+
override moveMessage(messageId: MessageId, folderName: string): Promise<void>;
|
|
2131
|
+
override keepalive(): Promise<void>;
|
|
2132
|
+
override watch(options: WatchOptions): Promise<void>;
|
|
2133
|
+
override sendMessage(_options: SendMessageOptions): Promise<string | null>;
|
|
2134
|
+
}
|
|
2135
|
+
/**
|
|
2136
|
+
* Convert one camelCase key to snake_case (`ipAddress` → `ip_address`).
|
|
2137
|
+
* Keys that aren't plain camelCase (`x-mailer`, `@lang`, `Header_From`) are
|
|
2138
|
+
* data from the report and are returned unchanged.
|
|
2139
|
+
*/
|
|
2140
|
+
export declare function camelToSnake(key: string): string;
|
|
2141
|
+
/**
|
|
2142
|
+
* Convert one snake_case key to camelCase (`ip_address` → `ipAddress`).
|
|
2143
|
+
* Keys that aren't plain snake_case are returned unchanged, so the two
|
|
2144
|
+
* functions are inverses for every key either produces.
|
|
2145
|
+
*/
|
|
2146
|
+
export declare function snakeToCamel(key: string): string;
|
|
2147
|
+
/**
|
|
2148
|
+
* Deep-copy `value`, converting every object key from camelCase to
|
|
2149
|
+
* snake_case, except inside values whose key is in `opaqueKeys`.
|
|
2150
|
+
*/
|
|
2151
|
+
export declare function toSnakeCaseKeys(value: unknown, opaqueKeys?: ReadonlySet<string>): unknown;
|
|
2152
|
+
/**
|
|
2153
|
+
* Deep-copy `value`, converting every object key from snake_case to
|
|
2154
|
+
* camelCase, except inside values whose key is in `opaqueKeys`. The inverse of
|
|
2155
|
+
* {@link toSnakeCaseKeys}.
|
|
2156
|
+
*/
|
|
2157
|
+
export declare function toCamelCaseKeys(value: unknown, opaqueKeys?: ReadonlySet<string>): unknown;
|
|
2158
|
+
/**
|
|
2159
|
+
* Pool initializer (parsedmarc's `_init_worker`): restore the parent's
|
|
2160
|
+
* lookup context in a worker, which starts with fresh modules. The IP
|
|
2161
|
+
* database path, IPinfo API token and PSL overrides (and, unlike
|
|
2162
|
+
* parsedmarc, a loaded reverse DNS map) come from `shared`, so enrichment
|
|
2163
|
+
* matches what the main thread configured; then the parent's logging is
|
|
2164
|
+
* rebuilt, so records logged while parsing in a worker are not dropped.
|
|
2165
|
+
*/
|
|
2166
|
+
export declare function initWorker(logLevel: number, logFiles: readonly string[], shared: WorkerSharedState, reverseDnsMap: Map<string, ReverseDNSService>): void;
|
|
2167
|
+
/** Options passed to a job function when its task carries a config. */
|
|
2168
|
+
interface ParallelJobOptions {
|
|
2169
|
+
config: ParserConfig;
|
|
2170
|
+
}
|
|
2171
|
+
/** What a worker runs for each job: `fn(job)` or `fn(job, { config })`. */
|
|
2172
|
+
interface ParallelTask {
|
|
2173
|
+
/**
|
|
2174
|
+
* URL or absolute path of an ES module exporting the function. Omit to
|
|
2175
|
+
* use a built-in job: `parseReportEmailJob` or `parseReportFileJob`.
|
|
2176
|
+
*/
|
|
2177
|
+
module?: string | URL | null;
|
|
2178
|
+
/** Name of the exported function. */
|
|
2179
|
+
name: string;
|
|
2180
|
+
/** Parser config; rebuilt in the worker and passed as `{ config }`. */
|
|
2181
|
+
config?: ParserConfig | null;
|
|
2182
|
+
}
|
|
2183
|
+
/** Task running `parseReportEmailJob` (report-email.ts) with `config`. */
|
|
2184
|
+
export declare function parseReportEmailTask(config: ParserConfig): ParallelTask;
|
|
2185
|
+
/** Task running `parseReportFileJob` (report-file.ts) with `config`. */
|
|
2186
|
+
export declare function parseReportFileTask(config: ParserConfig): ParallelTask;
|
|
2187
|
+
/**
|
|
2188
|
+
* Module-level enrichment state the main thread may have set up before
|
|
2189
|
+
* parsing in parallel (the IP database `loadIpDb()` chose, the IPinfo API
|
|
2190
|
+
* token, PSL overrides loaded from a URL or file, a loaded reverse DNS
|
|
2191
|
+
* map). A worker starts with fresh modules, so it gets a copy: otherwise it
|
|
2192
|
+
* would use the bundled IP database, no IPinfo API, the bundled overrides
|
|
2193
|
+
* and download the map again. parsedmarc's forkserver workers start fresh
|
|
2194
|
+
* too and have the same gap.
|
|
2195
|
+
*/
|
|
2196
|
+
interface WorkerSharedState {
|
|
2197
|
+
ipDbPath: string | null;
|
|
2198
|
+
ipinfoApiToken: string | null;
|
|
2199
|
+
pslOverrides: string[];
|
|
2200
|
+
/** Entries of the task config's reverse DNS map, when it is loaded. */
|
|
2201
|
+
reverseDnsMap: [string, ReverseDNSService][] | null;
|
|
2202
|
+
}
|
|
2203
|
+
/**
|
|
2204
|
+
* Python's `concurrent.futures.process.BrokenProcessPool`: the pool can't
|
|
2205
|
+
* run jobs at all (a worker failed while starting).
|
|
2206
|
+
*/
|
|
2207
|
+
export declare class BrokenProcessPool extends Error {
|
|
2208
|
+
override name: string;
|
|
2209
|
+
}
|
|
2210
|
+
/**
|
|
2211
|
+
* A worker thread died while running a job (over its heap limit,
|
|
2212
|
+
* `process.exit()` or an uncaught error outside the job's promise). Only
|
|
2213
|
+
* that job fails; the worker is replaced. parsedmarc's process pool breaks
|
|
2214
|
+
* for good instead (`BrokenProcessPool`). It says nothing about the job's
|
|
2215
|
+
* content: the heap also holds what the worker kept from earlier jobs.
|
|
2216
|
+
*/
|
|
2217
|
+
export declare class WorkerCrashedError extends Error {
|
|
2218
|
+
override name: string;
|
|
2219
|
+
}
|
|
2220
|
+
/** A job ran longer than `jobTimeout`; its worker was stopped and replaced. */
|
|
2221
|
+
export declare class JobTimeoutError extends Error {
|
|
2222
|
+
override name: string;
|
|
2223
|
+
}
|
|
2224
|
+
/** Options of {@link parallelMap}. */
|
|
2225
|
+
interface ParallelMapOptions<J = unknown, R = unknown> {
|
|
2226
|
+
/** Jobs submitted ahead of the oldest unfinished one, per worker. */
|
|
2227
|
+
windowFactor?: number;
|
|
2228
|
+
/**
|
|
2229
|
+
* Called every `heartbeatInterval` seconds spent waiting for a result. If
|
|
2230
|
+
* it throws, the iteration ends with that error: the jobs did not fail,
|
|
2231
|
+
* so `onJobError` doesn't see it.
|
|
2232
|
+
*/
|
|
2233
|
+
heartbeat?: (() => unknown) | null;
|
|
2234
|
+
/** Seconds between heartbeats (default 30). */
|
|
2235
|
+
heartbeatInterval?: number;
|
|
2236
|
+
/**
|
|
2237
|
+
* Checked after each result; when it returns true, queued jobs are
|
|
2238
|
+
* cancelled, running ones are awaited and yielded, and iteration ends.
|
|
2239
|
+
*/
|
|
2240
|
+
shouldStop?: (() => boolean | Promise<boolean>) | null;
|
|
2241
|
+
/**
|
|
2242
|
+
* Turns a failed job into a result instead of ending the iteration: an
|
|
2243
|
+
* error the job threw, a {@link WorkerCrashedError} or a
|
|
2244
|
+
* {@link JobTimeoutError}. Not called for {@link BrokenProcessPool}
|
|
2245
|
+
* (workers that can't start), which still ends the iteration.
|
|
2246
|
+
*/
|
|
2247
|
+
onJobError?: ((error: unknown, job: J) => R) | null;
|
|
2248
|
+
/**
|
|
2249
|
+
* Seconds a job may run before its worker is stopped and the job fails
|
|
2250
|
+
* with {@link JobTimeoutError}, counted once the worker has started.
|
|
2251
|
+
* `null` or `0`: no limit. Default: `jobTimeout` of the task's config,
|
|
2252
|
+
* else no limit.
|
|
2253
|
+
*/
|
|
2254
|
+
jobTimeout?: number | null;
|
|
2255
|
+
/**
|
|
2256
|
+
* Heap limit of each worker, in MiB. A worker over it dies, which fails
|
|
2257
|
+
* its job with a {@link WorkerCrashedError}. `null` or `0`: the runtime's
|
|
2258
|
+
* limit. Default: `maxWorkerHeapMb` of the task's config, else no limit.
|
|
2259
|
+
*/
|
|
2260
|
+
maxWorkerHeapMb?: number | null;
|
|
2261
|
+
}
|
|
2262
|
+
/**
|
|
2263
|
+
* Map a task over `jobs` with a bounded pool of `nProcs` worker threads
|
|
2264
|
+
* (parsedmarc's `parallel_map`).
|
|
2265
|
+
*
|
|
2266
|
+
* `jobs` is consumed lazily: at most `windowFactor * nProcs` jobs are
|
|
2267
|
+
* submitted ahead of the oldest unfinished one. Results come back in
|
|
2268
|
+
* submission order. No worker is started when `jobs` is empty. A job that
|
|
2269
|
+
* throws, or whose worker dies ({@link WorkerCrashedError}) or runs past
|
|
2270
|
+
* `jobTimeout` ({@link JobTimeoutError}), makes the iteration throw at its
|
|
2271
|
+
* position, unless `onJobError` turns it into a result. A dead worker is
|
|
2272
|
+
* replaced; only a worker that fails while starting breaks the pool
|
|
2273
|
+
* ({@link BrokenProcessPool}). parsedmarc's process pool breaks on any
|
|
2274
|
+
* dead worker.
|
|
2275
|
+
*
|
|
2276
|
+
* Throws {@link ValueError} right away when `nProcs` is less than 1.
|
|
2277
|
+
*/
|
|
2278
|
+
export declare function parallelMap<J, R>(task: ParallelTask, jobs: Iterable<J> | AsyncIterable<J>, nProcs: number, options?: ParallelMapOptions<J, R>): AsyncGenerator<R>;
|
|
2279
|
+
/**
|
|
2280
|
+
* parsedmarc's `text_report_regex`: `Name: value` fields of a legacy
|
|
2281
|
+
* plain-text failure notice. Anchored at line starts (`(?<![^\n])` is
|
|
2282
|
+
* Python's `^` under `re.MULTILINE`, which, unlike JS's `m` flag, does not
|
|
2283
|
+
* treat `\r` as a line break); the classes are disjoint so it runs in linear
|
|
2284
|
+
* time (GHSA-7grx-mj6g-qxmc).
|
|
2285
|
+
*/
|
|
2286
|
+
export declare const TEXT_REPORT_REGEX: RegExp;
|
|
2287
|
+
/**
|
|
2288
|
+
* Throw {@link EmailTooLargeError} when an email is over the config's
|
|
2289
|
+
* `maxEmailSize` bytes or has more than `maxMimeParts` MIME boundary
|
|
2290
|
+
* lines. Both checks are linear and run before any parsing; the parser then
|
|
2291
|
+
* counts the parts that need no boundary line, see `messageFromString()`.
|
|
2292
|
+
*/
|
|
2293
|
+
export declare function checkEmailLimits(input: string | Uint8Array, config: Pick<ParserConfig, "maxEmailSize" | "maxMimeParts">): void;
|
|
2294
|
+
/** Options of {@link parseReportEmail}. */
|
|
2295
|
+
interface ParseReportEmailOptions extends ParserConfigInput {
|
|
2296
|
+
/** Keep-alive callback for large aggregate reports (not part of `config`). */
|
|
2297
|
+
keepAlive?: (() => unknown) | null;
|
|
2298
|
+
}
|
|
2299
|
+
/**
|
|
2300
|
+
* Parses a DMARC or SMTP TLS report from an email (RFC 822 bytes or text).
|
|
2301
|
+
*
|
|
2302
|
+
* Throws {@link ParserError} when the email can't be read,
|
|
2303
|
+
* {@link InvalidDMARCReport} when it holds no valid report, and
|
|
2304
|
+
* {@link InvalidFailureReport} when its failure report is invalid. Throws
|
|
2305
|
+
* {@link EmailTooLargeError} when the email is over the config's
|
|
2306
|
+
* `maxEmailSize`, `maxMimeParts`, `maxHeaderLines` or `maxExpandedEmailSize`
|
|
2307
|
+
* (not in parsedmarc). Whatever the input, only `ParserError` subclasses are
|
|
2308
|
+
* thrown, apart from what `keepAlive` throws: that is rethrown as is, since
|
|
2309
|
+
* it says nothing about the email (parsedmarc reports a `ParserError`).
|
|
2310
|
+
*/
|
|
2311
|
+
export declare function parseReportEmail(input: string | Uint8Array, options?: ParseReportEmailOptions): Promise<ParsedReport>;
|
|
2312
|
+
/**
|
|
2313
|
+
* Worker job that parses one report email (parsedmarc's
|
|
2314
|
+
* `parallel._parse_report_email_job`). A {@link ParserError} is returned as
|
|
2315
|
+
* a value, never thrown, so one invalid message needs no special handling.
|
|
2316
|
+
* `parseReportEmail` throws only `ParserError` subclasses, whatever the
|
|
2317
|
+
* input, so no other error is expected from here.
|
|
2318
|
+
*/
|
|
2319
|
+
export declare function parseReportEmailJob(messageContent: string | Uint8Array, options: {
|
|
2320
|
+
config: ParserConfig;
|
|
2321
|
+
}): Promise<ParsedReport | ParserError>;
|
|
2322
|
+
/**
|
|
2323
|
+
* The dedup key of an aggregate report (parsedmarc's
|
|
2324
|
+
* `_aggregate_report_dedup_key()`): RFC 9990 section 3.5.1 defines a
|
|
2325
|
+
* Report-ID as unique only "among reports to the same domain", so the key
|
|
2326
|
+
* is the reporter, the Report-ID and the policy domain, as a JSON array
|
|
2327
|
+
* (no delimiter a field could contain). ASCII letters of the domain are
|
|
2328
|
+
* compared without case (RFC 4343 section 3); the report is unchanged.
|
|
2329
|
+
* Every writer of the seen report ID cache (mbox, mailbox, the CLI's
|
|
2330
|
+
* direct files) must use it. The key outlives parsing in that cache and
|
|
2331
|
+
* every part comes from the sender, so a long one is replaced by its hash
|
|
2332
|
+
* (parsedmarc keeps it whole).
|
|
2333
|
+
*/
|
|
2334
|
+
export declare function aggregateReportKey(report: AggregateReport): string;
|
|
2335
|
+
/** Input of {@link parseReportFile}: a path, bytes, or a binary stream. */
|
|
2336
|
+
type ReportFileInput = string | URL | Uint8Array | ArrayBuffer | ReportStream;
|
|
2337
|
+
/** Options of {@link parseReportFile}. */
|
|
2338
|
+
interface ParseReportFileOptions extends ParserConfigInput {
|
|
2339
|
+
/** Keep-alive callback for large aggregate reports (not part of `config`). */
|
|
2340
|
+
keepAlive?: (() => unknown) | null;
|
|
2341
|
+
}
|
|
2342
|
+
/**
|
|
2343
|
+
* Parses a DMARC aggregate report, DMARC failure report or SMTP TLS report
|
|
2344
|
+
* from a file path, bytes or a binary stream (compressed or not, report
|
|
2345
|
+
* email or bare report).
|
|
2346
|
+
*
|
|
2347
|
+
* A path is opened and closed here; a caller-supplied stream is read but
|
|
2348
|
+
* left for the caller to close. When no format matches, the
|
|
2349
|
+
* {@link ParserError} explains why, based on what the content looks like.
|
|
2350
|
+
*/
|
|
2351
|
+
export declare function parseReportFile(input: ReportFileInput, options?: ParseReportFileOptions): Promise<ParsedReport>;
|
|
2352
|
+
/**
|
|
2353
|
+
* Worker job that parses one report file (parsedmarc's
|
|
2354
|
+
* `parallel._parse_report_file_job`). Any error is returned with the path
|
|
2355
|
+
* instead of being thrown, so the caller can log it and move on.
|
|
2356
|
+
*/
|
|
2357
|
+
export declare function parseReportFileJob(filePath: string, options: {
|
|
2358
|
+
config: ParserConfig;
|
|
2359
|
+
}): Promise<[string, ParsedReport | Error]>;
|
|
2360
|
+
/** Options of {@link getDmarcReportsFromMbox}. */
|
|
2361
|
+
interface GetDmarcReportsFromMboxOptions extends ParserConfigInput {
|
|
2362
|
+
/**
|
|
2363
|
+
* Worker threads used to parse messages in parallel (not part of
|
|
2364
|
+
* `config`). Reading, dedup and result assembly stay in the caller.
|
|
2365
|
+
*/
|
|
2366
|
+
nProcs?: number;
|
|
2367
|
+
}
|
|
2368
|
+
/**
|
|
2369
|
+
* Parses an mbox file containing emails with attached DMARC and SMTP TLS
|
|
2370
|
+
* reports. Each message's stored bytes are parsed. A message that can't be
|
|
2371
|
+
* parsed or is over `maxEmailSize` is logged as a warning and skipped.
|
|
2372
|
+
* Duplicate aggregate reports are dropped using the config's seen-report
|
|
2373
|
+
* cache.
|
|
2374
|
+
*/
|
|
2375
|
+
export declare function getDmarcReportsFromMbox(input: string, options?: GetDmarcReportsFromMboxOptions): Promise<ParsingResults>;
|
|
2376
|
+
/** Fields of `parsedSmtpTlsReportsToCsv()`, in column order. */
|
|
2377
|
+
export declare const SMTP_TLS_CSV_FIELDS: readonly string[];
|
|
2378
|
+
/** Parse one `failure-details` entry (parsedmarc's `_parse_smtp_tls_failure_details`). */
|
|
2379
|
+
export declare function parseSmtpTlsFailureDetails(failureDetails: unknown): SMTPTLSFailureDetails;
|
|
2380
|
+
/** Parse one policy entry (parsedmarc's `_parse_smtp_tls_report_policy`). */
|
|
2381
|
+
export declare function parseSmtpTlsReportPolicy(policy: unknown): SMTPTLSPolicy;
|
|
2382
|
+
/** Parse and validate an SMTP TLS report (RFC 8460 JSON). */
|
|
2383
|
+
export declare function parseSmtpTlsReportJson(report: string | Uint8Array, options?: Partial<Pick<ParserOptions, "maxSmtpTlsReportSize">>): SMTPTLSReport;
|
|
2384
|
+
/**
|
|
2385
|
+
* Flatten SMTP TLS reports into CSV rows (snake_case keys): one row per
|
|
2386
|
+
* policy, plus one per failure-details entry.
|
|
2387
|
+
*/
|
|
2388
|
+
export declare function parsedSmtpTlsReportsToCsvRows(reports: SMTPTLSReport | SMTPTLSReport[]): CsvRow[];
|
|
2389
|
+
/** SMTP TLS reports as flat CSV, with headers. */
|
|
2390
|
+
export declare function parsedSmtpTlsReportsToCsv(reports: SMTPTLSReport | SMTPTLSReport[], options?: CsvOptions): string;
|
|
2391
|
+
interface CreateEmailOptions {
|
|
2392
|
+
messageFrom: string;
|
|
2393
|
+
messageTo?: string[] | null;
|
|
2394
|
+
messageCc?: string[] | null;
|
|
2395
|
+
subject?: string | null;
|
|
2396
|
+
messageHeaders?: Record<string, string> | null;
|
|
2397
|
+
/** `[filename, content]` pairs. */
|
|
2398
|
+
attachments?: [string, Uint8Array][] | null;
|
|
2399
|
+
plainMessage?: string | null;
|
|
2400
|
+
htmlMessage?: string | null;
|
|
2401
|
+
}
|
|
2402
|
+
/** `email.utils.formatdate(localtime=True)`: RFC 2822 date in local time. */
|
|
2403
|
+
export declare function formatDate(date?: Date): string;
|
|
2404
|
+
/**
|
|
2405
|
+
* Create an RFC 822 email message and return it as a string.
|
|
2406
|
+
*
|
|
2407
|
+
* Port of `mailsuite.utils.create_email`: a `multipart/mixed` message with
|
|
2408
|
+
* the plain and/or HTML body followed by one `application/octet-stream`
|
|
2409
|
+
* part per attachment.
|
|
2410
|
+
*/
|
|
2411
|
+
export declare function createEmail(options: CreateEmailOptions): string;
|
|
2412
|
+
/** Python's `repr(float)`: shortest round-trip digits, `1.0`, `1e-05`, `inf`. */
|
|
2413
|
+
export declare function pyFloatRepr(value: number): string;
|
|
2414
|
+
/**
|
|
2415
|
+
* Python's `repr()` for JSON-like values (strings use single quotes).
|
|
2416
|
+
* Written in one pass with an explicit stack: linear in the output, and data
|
|
2417
|
+
* nested deeper than the call stack allows still prints.
|
|
2418
|
+
*/
|
|
2419
|
+
export declare function pyRepr(value: unknown): string;
|
|
2420
|
+
/** Python's `str()` for JSON-like values. */
|
|
2421
|
+
export declare function pyStr(value: unknown): string;
|
|
2422
|
+
/**
|
|
2423
|
+
* Masks the credentials in a URL or host string so it is safe to log
|
|
2424
|
+
* (parsedmarc's `redact_url_credentials()`): the password, after the first
|
|
2425
|
+
* `:` of the userinfo, becomes `***` (`https://user:***@host:9200`), and a
|
|
2426
|
+
* userinfo without `:` (a bare token) is replaced whole
|
|
2427
|
+
* (`https://***@host`). When the last `@` is past the end of the authority
|
|
2428
|
+
* (a `/`, `?` or `#` in the password, or an `@` in the path or query),
|
|
2429
|
+
* everything from the start of the authority through that `@` is masked
|
|
2430
|
+
* (`https://admin:p4ss/w0rd@host:3100` becomes `https://***@host:3100`;
|
|
2431
|
+
* `https://host/path@file` becomes `https://***@file`).
|
|
2432
|
+
*
|
|
2433
|
+
* One URL per string: split a comma-separated list first, or pass an
|
|
2434
|
+
* array, which is masked element by element into a new array (non-string
|
|
2435
|
+
* items are kept). Never throws.
|
|
2436
|
+
*/
|
|
2437
|
+
export declare function redactUrlCredentials(value: string): string;
|
|
2438
|
+
export declare function redactUrlCredentials<T>(value: readonly T[]): T[];
|
|
2439
|
+
/**
|
|
2440
|
+
* Masks the credentials of the configured nameserver URLs in free text
|
|
2441
|
+
* such as an error message (parsedmarc's `_mask_nameserver_secrets()`):
|
|
2442
|
+
* each entry is replaced by its masked form, and each of its
|
|
2443
|
+
* {@link urlSecrets} by `***` wherever it still appears. A short secret
|
|
2444
|
+
* masks every occurrence of that text. Never throws.
|
|
2445
|
+
*/
|
|
2446
|
+
export declare function maskNameserverSecrets(text: string, nameservers: readonly string[] | null | undefined): string;
|
|
2447
|
+
/**
|
|
2448
|
+
* Escapes the non-printable characters of a sender-controlled string so it
|
|
2449
|
+
* is safe to put in a log line or an error message.
|
|
2450
|
+
*
|
|
2451
|
+
* Every character for which Python's `str.isprintable()` is false (CR, LF,
|
|
2452
|
+
* tab, ESC, U+2028, NUL, and so on) is replaced by its `unicode_escape`
|
|
2453
|
+
* form, e.g. `\r\n`. A sender can plant such characters in a header through
|
|
2454
|
+
* an RFC 2047 encoded-word (`=?utf-8?q?a=0D=0Ab?=` decodes to a raw CR LF),
|
|
2455
|
+
* and an unescaped CR/LF would let it forge extra log lines. The ASCII
|
|
2456
|
+
* space, letters in any script, and most emoji pass through unchanged;
|
|
2457
|
+
* non-ASCII spaces (NBSP, U+3000) and format characters such as the
|
|
2458
|
+
* zero-width joiner or the bidi marks are escaped.
|
|
2459
|
+
*/
|
|
2460
|
+
export declare function sanitizeForLog(value: string): string;
|
|
2461
|
+
/** Cache of `queryDns()` answers, keyed `<domain>_<TYPE>`. */
|
|
2462
|
+
type DnsCache = ExpiringDict<string, string[]>;
|
|
2463
|
+
/** Cloudflare's public resolvers, used when no nameservers are configured. */
|
|
2464
|
+
export declare const DEFAULT_NAMESERVERS: readonly string[];
|
|
2465
|
+
/**
|
|
2466
|
+
* A malformed nameserver entry: Python's `ValueError`, raised by
|
|
2467
|
+
* parsedmarc's nameserver parsing and dnspython's `Resolver.nameservers`.
|
|
2468
|
+
* `name` is `ValueError` so messages that print the error type (the CLI's
|
|
2469
|
+
* DNS pre-flight check) match parsedmarc's; it is a `RangeError` for
|
|
2470
|
+
* callers that catch the built-in type.
|
|
2471
|
+
*/
|
|
2472
|
+
export declare class InvalidNameserverError extends RangeError {
|
|
2473
|
+
override name: string;
|
|
2474
|
+
}
|
|
2475
|
+
/**
|
|
2476
|
+
* A socket-level failure (Python's `OSError`): connection refused, network
|
|
2477
|
+
* unreachable, TLS handshake failure. dnspython takes the nameserver out of
|
|
2478
|
+
* the mix, and parsedmarc retries the whole query on it.
|
|
2479
|
+
*/
|
|
2480
|
+
export declare class DNSSocketError extends Error {
|
|
2481
|
+
override name: string;
|
|
2482
|
+
/** The Node error code (`ECONNREFUSED`, ...), when known. */
|
|
2483
|
+
readonly code: string | null;
|
|
2484
|
+
constructor(message: string, code?: string | null);
|
|
2485
|
+
}
|
|
2486
|
+
interface DnsQueryOptions {
|
|
2487
|
+
/** Cache storage. */
|
|
2488
|
+
cache?: DnsCache | null;
|
|
2489
|
+
/**
|
|
2490
|
+
* Nameservers to use (Cloudflare's public resolvers by default). Each
|
|
2491
|
+
* entry is an IP address (DNS over UDP/TCP port 53), an `https://` URL
|
|
2492
|
+
* (DNS over HTTPS) or `tls://ip[:port][#hostname]` (DNS over TLS, port
|
|
2493
|
+
* 853 by default, `#hostname` naming the server's TLS certificate
|
|
2494
|
+
* identity). Pass `RECOMMENDED_DNS_NAMESERVERS` for a cross-provider mix.
|
|
2495
|
+
*/
|
|
2496
|
+
nameservers?: readonly string[] | null;
|
|
2497
|
+
/**
|
|
2498
|
+
* Overall DNS lifetime budget in seconds per configured nameserver.
|
|
2499
|
+
* Per-query attempts are capped at `min(1, timeout)` so a lost UDP packet
|
|
2500
|
+
* is retried within the lifetime, and a slow nameserver falls through to
|
|
2501
|
+
* the next quickly.
|
|
2502
|
+
*/
|
|
2503
|
+
timeout?: number;
|
|
2504
|
+
/**
|
|
2505
|
+
* Times to retry the whole query after a timeout or other transient error
|
|
2506
|
+
* (`LifetimeTimeout`, `NoNameservers`, `DNSSocketError`).
|
|
2507
|
+
*/
|
|
2508
|
+
retries?: number;
|
|
2509
|
+
}
|
|
2510
|
+
/**
|
|
2511
|
+
* Queries DNS and returns the answers as text. Throws
|
|
2512
|
+
* `InvalidNameserverError` (a `RangeError`) when a
|
|
2513
|
+
* nameserver is malformed, with the nameservers' URL credentials masked.
|
|
2514
|
+
*/
|
|
2515
|
+
export declare function queryDns(domain: string, recordType: string, options?: DnsQueryOptions): Promise<string[]>;
|
|
2516
|
+
interface GetReverseDnsOptions extends DnsQueryOptions {
|
|
2517
|
+
/** Called with the DNS error when there is one, before returning `null`. */
|
|
2518
|
+
onError?: (error: DNSException) => void;
|
|
2519
|
+
}
|
|
2520
|
+
/**
|
|
2521
|
+
* Resolves an IP address to a hostname using a reverse DNS query. Returns
|
|
2522
|
+
* `null` on any DNS error.
|
|
2523
|
+
*/
|
|
2524
|
+
export declare function getReverseDns(ipAddress: string, options?: GetReverseDnsOptions): Promise<string | null>;
|
|
2525
|
+
/**
|
|
2526
|
+
* Reverse-map domain name of an IP address, with the final dot
|
|
2527
|
+
* (`str(dns.reversename.from_address(text))`). Raises `DNSSyntaxError` for
|
|
2528
|
+
* text that is not an IP address.
|
|
2529
|
+
*/
|
|
2530
|
+
export declare function reverseNameFromAddress(text: string): string;
|
|
2531
|
+
/**
|
|
2532
|
+
* The module-level PSL overrides list (parsedmarc's `psl_overrides`).
|
|
2533
|
+
* `loadPslOverrides()` refills this same array in place, and
|
|
2534
|
+
* `getBaseDomain()` reads it on every call.
|
|
2535
|
+
*/
|
|
2536
|
+
export declare const pslOverrides: string[];
|
|
2537
|
+
interface LoadPslOverridesOptions {
|
|
2538
|
+
/** Always use a local overrides file. */
|
|
2539
|
+
alwaysUseLocalFile?: boolean;
|
|
2540
|
+
/** Path to a local overrides file (default: the bundled `psl_overrides.txt`). */
|
|
2541
|
+
localFilePath?: string | null;
|
|
2542
|
+
/** URL to a PSL overrides file. */
|
|
2543
|
+
url?: string | null;
|
|
2544
|
+
/** Do not make online requests. */
|
|
2545
|
+
offline?: boolean;
|
|
2546
|
+
}
|
|
2547
|
+
/**
|
|
2548
|
+
* Loads the PSL overrides list from a URL or local file.
|
|
2549
|
+
*
|
|
2550
|
+
* Repopulates the module-level `pslOverrides` array in place, then returns
|
|
2551
|
+
* it. The URL is tried first; on failure (or when `offline` /
|
|
2552
|
+
* `alwaysUseLocalFile` is set) the local path is used, defaulting to the
|
|
2553
|
+
* bundled `psl_overrides.txt`.
|
|
2554
|
+
*
|
|
2555
|
+
* parsedmarc clears the list before downloading; here the new list is built
|
|
2556
|
+
* aside and swapped in at once, so lookups running during the download keep
|
|
2557
|
+
* seeing the previous overrides instead of an empty list.
|
|
2558
|
+
*/
|
|
2559
|
+
export declare function loadPslOverrides(options?: LoadPslOverridesOptions): Promise<string[]>;
|
|
2560
|
+
/**
|
|
2561
|
+
* Gets the base domain name for the given domain.
|
|
2562
|
+
*
|
|
2563
|
+
* Results are based on the Public Suffix List and the overrides in
|
|
2564
|
+
* `psl_overrides.txt`. Returns `null` if a base domain cannot be determined.
|
|
2565
|
+
*/
|
|
2566
|
+
export declare function getBaseDomain(domain: string): string | null;
|
|
2567
|
+
/** Python's `ValueError`. */
|
|
2568
|
+
declare class PyValueError extends Error {
|
|
2569
|
+
override name: string;
|
|
2570
|
+
}
|
|
2571
|
+
/**
|
|
2572
|
+
* Decodes a base64 string, with padding being optional.
|
|
2573
|
+
*
|
|
2574
|
+
* Throws `UnicodeEncodeError` for non-ASCII input and `binascii.Error`
|
|
2575
|
+
* (`BinasciiError`) for undecodable data, like Python.
|
|
2576
|
+
*/
|
|
2577
|
+
export declare function decodeBase64(data: string): Uint8Array;
|
|
2578
|
+
/**
|
|
2579
|
+
* Builds an address dict from a `(display_name, address)` pair, as
|
|
2580
|
+
* mail-parser returns them.
|
|
2581
|
+
*/
|
|
2582
|
+
export declare function parseEmailAddress(originalAddress: readonly [string, string] | readonly string[]): EmailAddress;
|
|
2583
|
+
/**
|
|
2584
|
+
* Converts a string to a string that is safe to use as a filename.
|
|
2585
|
+
*
|
|
2586
|
+
* The result is a single path component: no `/`, `\`, `:`, NUL, other
|
|
2587
|
+
* control characters or bidi marks, never ending in `.` or a space, never
|
|
2588
|
+
* `.` or `..`, and at most 100 characters and 100 UTF-8 bytes (cut on a
|
|
2589
|
+
* character boundary; lone surrogates are dropped), like parsedmarc. It can
|
|
2590
|
+
* be empty, so callers that need a name must supply a fallback. `null` is
|
|
2591
|
+
* treated as the literal string `"None"`.
|
|
2592
|
+
*
|
|
2593
|
+
* parsedmarc removes only `\ / : " * ? |`, CR, LF and NUL; the other control
|
|
2594
|
+
* characters and the bidi marks are a tsdmarc addition.
|
|
2595
|
+
*/
|
|
2596
|
+
export declare function getFilenameSafeString(value: string | null): string;
|
|
2597
|
+
/**
|
|
2598
|
+
* Checks if the file at the given path is an mbox mailbox file (it holds at
|
|
2599
|
+
* least one message). Errors are logged at debug level and give `false`.
|
|
2600
|
+
*/
|
|
2601
|
+
export declare function isMbox(path: string): Promise<boolean>;
|
|
2602
|
+
/** Checks if the given content is an Outlook msg OLE/MSG file. */
|
|
2603
|
+
export declare function isOutlookMsg(content: unknown): boolean;
|
|
2604
|
+
/** How long `msgconvert` may run before it is killed (milliseconds). */
|
|
2605
|
+
export declare const MSGCONVERT_TIMEOUT_MS = 6e4;
|
|
2606
|
+
/** Python's `subprocess.CalledProcessError`. */
|
|
2607
|
+
export declare class CalledProcessError extends Error {
|
|
2608
|
+
override name: string;
|
|
2609
|
+
}
|
|
2610
|
+
/**
|
|
2611
|
+
* Uses the `msgconvert` Perl utility to convert an Outlook MSG file to
|
|
2612
|
+
* standard RFC 822 format.
|
|
2613
|
+
*
|
|
2614
|
+
* Throws {@link ValueError} when the bytes are not an Outlook MSG file and
|
|
2615
|
+
* {@link EmailParserError} when `msgconvert` is not installed (or, unlike
|
|
2616
|
+
* parsedmarc, runs longer than {@link MSGCONVERT_TIMEOUT_MS}). Throws
|
|
2617
|
+
* `EmailTooLargeError` when `maxSize` is not 0 and the converted message is
|
|
2618
|
+
* larger, before reading it: a small MSG can expand a lot.
|
|
2619
|
+
*/
|
|
2620
|
+
export declare function convertOutlookMsg(msgBytes: Uint8Array, maxSize?: number): Promise<Uint8Array>;
|
|
2621
|
+
/** Options of {@link parseEmail}. */
|
|
2622
|
+
interface ParseEmailOptions {
|
|
2623
|
+
/** Remove attachment payloads. */
|
|
2624
|
+
stripAttachmentPayloads?: boolean;
|
|
2625
|
+
/** Limits on the message's structure (not in parsedmarc). */
|
|
2626
|
+
limits?: EmailLimits;
|
|
2627
|
+
}
|
|
2628
|
+
/**
|
|
2629
|
+
* A simplified email parser.
|
|
2630
|
+
*
|
|
2631
|
+
* `data` is the RFC 822 message text, or its bytes (Outlook MSG bytes are
|
|
2632
|
+
* converted with `msgconvert` first; other bytes are decoded as UTF-8 with
|
|
2633
|
+
* replacement). `toSnakeCaseKeys()` of the result is parsedmarc's
|
|
2634
|
+
* `parse_email()` output.
|
|
2635
|
+
*/
|
|
2636
|
+
export declare function parseEmail(data: Uint8Array | string, options?: ParseEmailOptions): Promise<ParsedEmail>;
|
|
2637
|
+
/**
|
|
2638
|
+
* The synchronous part of {@link parseEmail}, for already-decoded text (no
|
|
2639
|
+
* Outlook MSG conversion).
|
|
2640
|
+
*/
|
|
2641
|
+
export declare function parseEmailText(text: string, options?: ParseEmailOptions): ParsedEmail;
|
|
2642
|
+
/** Raised when a date string cannot be parsed (dateutil's `ParserError`). */
|
|
2643
|
+
export declare class DateParseError extends Error {
|
|
2644
|
+
override name: string;
|
|
2645
|
+
}
|
|
2646
|
+
interface ParseDateOptions {
|
|
2647
|
+
/**
|
|
2648
|
+
* Naive datetime supplying the fields the string leaves out (dateutil's
|
|
2649
|
+
* `default`). Defaults to today at midnight, local time.
|
|
2650
|
+
*/
|
|
2651
|
+
default?: PyDateTime;
|
|
2652
|
+
/**
|
|
2653
|
+
* Year that two-digit years are resolved around (dateutil uses the local
|
|
2654
|
+
* year when it is imported). Defaults to the current local year.
|
|
2655
|
+
*/
|
|
2656
|
+
currentYear?: number;
|
|
2657
|
+
}
|
|
2658
|
+
/**
|
|
2659
|
+
* Parse a date/time string like `dateutil.parser.parse(timestr)`. Missing
|
|
2660
|
+
* fields come from `options.default` (today at midnight); a numeric offset
|
|
2661
|
+
* or UTC zone name makes the result aware, an unknown zone name is ignored.
|
|
2662
|
+
*/
|
|
2663
|
+
export declare function parseDate(input: string, options?: ParseDateOptions): PyDateTime;
|
|
2664
|
+
/** Python's `datetime.fromtimestamp(int(timestamp))`: naive, local time. */
|
|
2665
|
+
export declare function timestampToDatetime(timestamp: number | string): PyDateTime;
|
|
2666
|
+
/** A UNIX timestamp as local `YYYY-MM-DD HH:MM:SS`. */
|
|
2667
|
+
export declare function timestampToHuman(timestamp: number | string): string;
|
|
2668
|
+
/**
|
|
2669
|
+
* Remove RFC 5322 comments from a header value. Comments nest and may hold
|
|
2670
|
+
* quoted-pairs such as `\)`, so this counts depth instead of using a regex;
|
|
2671
|
+
* an unclosed comment runs to the end of the value.
|
|
2672
|
+
*/
|
|
2673
|
+
export declare function stripComments(value: string): string;
|
|
2674
|
+
interface HumanTimestampOptions {
|
|
2675
|
+
/** Convert the result to UTC. */
|
|
2676
|
+
toUtc?: boolean;
|
|
2677
|
+
/** Treat a timestamp without a UTC offset as UTC instead of local time. */
|
|
2678
|
+
assumeUtc?: boolean;
|
|
2679
|
+
}
|
|
2680
|
+
/** Parse a human-readable timestamp (Python's `human_timestamp_to_datetime`). */
|
|
2681
|
+
export declare function humanTimestampToDatetime(humanTimestamp: string, options?: HumanTimestampOptions): PyDateTime;
|
|
2682
|
+
/** Parse a human-readable timestamp into a UNIX timestamp (whole seconds). */
|
|
2683
|
+
export declare function humanTimestampToUnixTimestamp(humanTimestamp: string, options?: Pick<HumanTimestampOptions, "assumeUtc">): number;
|
|
2684
|
+
/**
|
|
2685
|
+
* Python's `xml.parsers.expat.ExpatError`: the message is Expat's error
|
|
2686
|
+
* string followed by `: line N, column M` (line 1-based, column 0-based and
|
|
2687
|
+
* counted in characters), exactly as pyexpat formats it.
|
|
2688
|
+
*/
|
|
2689
|
+
export declare class ExpatError extends Error {
|
|
2690
|
+
override name: string;
|
|
2691
|
+
/** Expat's numeric error code (`enum XML_Error`). */
|
|
2692
|
+
readonly code: number;
|
|
2693
|
+
/** 1-based line of the error. */
|
|
2694
|
+
readonly lineno: number;
|
|
2695
|
+
/** 0-based column of the error, in characters. */
|
|
2696
|
+
readonly offset: number;
|
|
2697
|
+
constructor(code: number, lineno: number, offset: number);
|
|
2698
|
+
}
|
|
2699
|
+
export { type AggregateAlignment, type AggregateAuthResultDKIM, type AggregateAuthResultSPF, type AggregateAuthResults, type AggregateIdentifiers, type AggregateParsedReport, type AggregatePolicyEvaluated, type AggregatePolicyOverrideReason, type AggregatePolicyPublished, type AggregateRecord, type AggregateReport, type AggregateReportMetadata, type CreateEmailOptions, type CsvOptions, type CsvRow, type DayBucket, type DnsCache, type DnsQueryOptions, type EmailAddress, type EmailAttachment, type EmailReceivedHop, type ExpiringDictOptions, type ExtractOptions, type FailureParsedReport, type FailureReport, type FetchMessageOptions, type FetchMessagesOptions, type ForensicParsedReport, type ForensicReport, type GelfClientOptions, type GelfHandlerOptions, type GelfMode, type GelfTlsOptions, type GelfUdpHandlerOptions, type GetDmarcReportsFromMailboxOptions, type GetDmarcReportsFromMboxOptions, type GetIpAddressInfoOptions, type GetServiceOptions, type HECClientOptions, type HumanTimestampOptions, type IPAddressCache, type IPAddressInfo, type IPDatabaseRecord, type IPSourceInfo, type IpInfoState, type LoadIpDbOptions, type LoadPslOverridesOptions, type LoadReverseDnsMapOptions, type LogCapture, type LogFilter, type LogFormatter, type LogOptions, type LogRecord, type LokiClientOptions, type LokiReportType, type LokiTimestampMode, type MailboxKind, type MaildirConnectionOptions, type MergeFoldersOptions, type MessageId, type MoveFolderOptions, type ParallelJobOptions, type ParallelMapOptions, type ParallelTask, type ParseAggregateReportFileOptions, type ParseAggregateReportOptions, type ParseEmailOptions, type ParseFailureReportOptions, type ParseReportEmailOptions, type ParseReportFileOptions, type ParsedEmail, type ParsedReport, type ParserCaches, type ParserConfig, type ParserConfigInput, type ParserOptions, type ParsingResults, type PyDateTimeFields, type PyJsonDumpsOptions, type ReportFileInput, type ReportStream, type ReportType, type ResolutionError, type ReverseDNSMap, type ReverseDNSService, type SMTPTLSFailureDetails, type SMTPTLSParsedReport, type SMTPTLSPolicy, type SMTPTLSReport, type SaveCallback, type SaveCallbackResult, type SaveOutputOptions, type SeenAggregateReportIds, type SendMessageOptions, type SysLogHandlerOptions, type SyslogClientOptions, type SyslogSocketType, PyValueError as ValueError, type WatchInboxOptions, type WatchOptions, type WebhookClientOptions, type WebhookPayload, type WritableTextStream };
|
|
2700
|
+
//# sourceMappingURL=index.d.ts.map
|