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.
@@ -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