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 @@
1
+ {"version":3,"file":"index.js","names":["MS_PER_SECOND","ValueError","ValueError","isDirectory","netConnect","tlsConnect","MS_PER_SECOND","CLOSE_TIMEOUT_MS","MS_PER_SECOND","httpsRequest","httpRequest","DEFAULT_TIMEOUT","CLOSED_MESSAGE","INVALID_URL_MESSAGE","DEFAULT_TIMEOUT","CLOSED_MESSAGE","INVALID_URL_MESSAGE","Buffer","fsConstants","sleep"],"sources":["../src/mailbox/base.ts","../src/mailbox/reports.ts","../src/output.ts","../src/outputs/socket-io.ts","../src/outputs/gelf.ts","../src/outputs/http.ts","../src/outputs/splunk.ts","../src/outputs/loki.ts","../src/outputs/syslog.ts","../src/outputs/webhook.ts","../src/mailbox/maildir.ts","../src/utils/create-email.ts"],"sourcesContent":["// Provider-agnostic mailbox interface, ported from mailsuite.mailbox.base.\n//\n// Backends live in their own packages (`@tsdmarc/imap`, `@tsdmarc/gmail`,\n// `@tsdmarc/msgraph`) plus `MaildirConnection` in this package. Methods a\n// backend does not support throw `NotImplementedError`. All operations are\n// async. `kind` identifies the backend, replacing Python's `isinstance`\n// checks so the core never imports backend packages.\n\nimport { PyValueError } from '../internal/py';\n\n/** Raised by mailbox methods a backend does not implement. */\nexport class NotImplementedError extends Error {\n override name = 'NotImplementedError';\n}\n\n/** A folder/label operation targets a name that is already taken. */\nexport class FolderExistsError extends Error {\n override name = 'FolderExistsError';\n}\n\n/** A folder/label referenced by an operation does not exist. */\nexport class FolderNotFoundError extends Error {\n override name = 'FolderNotFoundError';\n}\n\n/** Backend identifier. */\nexport type MailboxKind = 'imap' | 'gmail' | 'msgraph' | 'maildir';\n\n/** Provider-specific message identifier (IMAP UID, Graph/Gmail ID, Maildir key). */\nexport type MessageId = string | number;\n\nexport interface FetchMessagesOptions {\n /** Only messages received since this date (backend-specific format, e.g. `\"2d\"` or an ISO date). */\n since?: string | null;\n /** Maximum number of messages to return (0 or unset: all). */\n batchSize?: number;\n}\n\nexport interface FetchMessageOptions {\n /** Mark the message as read after fetching it. */\n markRead?: boolean;\n /**\n * Largest message to download, in bytes. Backends that can learn the size\n * first throw `EmailTooLargeError` instead of downloading a larger one\n * (not marking it read). Not in mailsuite.\n */\n maxSize?: number | null;\n}\n\nexport interface MoveFolderOptions {\n /** The folder's complete new path. */\n newPath?: string | null;\n /** The folder to move the source under, keeping its leaf name (\"\" = root). */\n newParent?: string | null;\n /** Create the destination's parent path if missing. */\n create?: boolean;\n}\n\nexport interface MergeFoldersOptions {\n /** Create the destination if missing. */\n create?: boolean;\n /** Keep the emptied source folders. */\n keepSourceFolders?: boolean;\n}\n\nexport interface WatchOptions {\n /** Called with this connection whenever the watcher fires. */\n checkCallback: (connection: MailboxConnection) => Promise<void> | void;\n /** Polling interval (or IDLE timeout), in seconds. */\n checkTimeout: number;\n /** When it returns true, the watcher returns so the caller can reload config. */\n configReloading?: (() => boolean) | null;\n /** Aborts the watch loop. */\n signal?: AbortSignal;\n}\n\nexport interface SendMessageOptions {\n messageFrom: string;\n messageTo?: string[] | null;\n messageCc?: string[] | null;\n messageBcc?: string[] | null;\n subject?: string | null;\n messageHeaders?: Record<string, string> | null;\n /** `[filename, content]` pairs. */\n attachments?: [string, Uint8Array][] | null;\n plainMessage?: string | null;\n htmlMessage?: string | null;\n /** Save a copy to Sent Items (Microsoft Graph only). Default: true. */\n saveToSentItems?: boolean;\n}\n\nfunction notImplemented(method: string): Promise<never> {\n return Promise.reject(\n new NotImplementedError(\n `${method} is not implemented by this mailbox backend`,\n ),\n );\n}\n\nexport abstract class MailboxConnection {\n abstract readonly kind: MailboxKind;\n\n /**\n * Tells this mailbox from others whose message IDs may be the same, for\n * state kept between calls (several connection objects to one mailbox\n * share it). Backends whose IDs are unique on their own leave it empty.\n */\n get mailboxIdentity(): string {\n return '';\n }\n\n createFolder(_folderName: string): Promise<void> {\n return notImplemented('createFolder');\n }\n\n deleteFolder(_folderName: string): Promise<void> {\n return notImplemented('deleteFolder');\n }\n\n /**\n * Rename a folder. Implementations call `ensureNoFolderConflict()` first,\n * so renaming onto an existing name throws `FolderExistsError`.\n */\n renameFolder(_oldName: string, _newName: string): Promise<void> {\n return notImplemented('renameFolder');\n }\n\n folderExists(_folderName: string): Promise<boolean> {\n return notImplemented('folderExists');\n }\n\n /** Throw `FolderExistsError` if `folderName` already exists. */\n protected async ensureNoFolderConflict(folderName: string): Promise<void> {\n if (await this.folderExists(folderName)) {\n throw new FolderExistsError(\n `cannot rename to ${JSON.stringify(folderName)}: a folder or label with that name already exists`,\n );\n }\n }\n\n /**\n * Move a folder (and its contents). Give exactly one of `newPath` (the\n * complete new path) or `newParent` (keep the leaf name).\n */\n async moveFolder(source: string, options: MoveFolderOptions): Promise<void> {\n const newPath = options.newPath ?? null;\n const newParent = options.newParent ?? null;\n if ((newPath === null) === (newParent === null)) {\n throw new PyValueError('provide exactly one of newPath or newParent');\n }\n if (!(await this.folderExists(source))) {\n throw new FolderNotFoundError(\n `folder ${JSON.stringify(source)} not found`,\n );\n }\n\n let targetParent: string;\n let targetPath: string;\n if (newParent === null) {\n const path = newPath as string;\n const slash = path.lastIndexOf('/');\n targetParent = slash === -1 ? '' : path.slice(0, slash);\n targetPath = path;\n } else {\n const sourceLeaf = source.slice(source.lastIndexOf('/') + 1);\n targetParent = newParent;\n targetPath = newParent ? `${newParent}/${sourceLeaf}` : sourceLeaf;\n }\n\n if (targetParent && !(await this.folderExists(targetParent))) {\n if (options.create) {\n await this.createFolder(targetParent);\n } else {\n throw new FolderNotFoundError(\n `destination parent ${JSON.stringify(targetParent)} not found (pass create: true to create it)`,\n );\n }\n }\n\n await this.ensureNoFolderConflict(targetPath);\n await this.doMoveFolder(source, targetParent, targetPath);\n }\n\n /** Move every message of each source folder into `destination`. */\n async mergeFolders(\n sources: string | string[],\n destination: string,\n options: MergeFoldersOptions = {},\n ): Promise<void> {\n const sourceList = typeof sources === 'string' ? [sources] : sources;\n if (!(await this.folderExists(destination))) {\n if (options.create) {\n await this.createFolder(destination);\n } else {\n throw new FolderNotFoundError(\n `destination ${JSON.stringify(destination)} not found (pass create: true to create it)`,\n );\n }\n }\n for (const source of sourceList) {\n if (source === destination) {\n continue;\n }\n if (!(await this.folderExists(source))) {\n throw new FolderNotFoundError(\n `folder ${JSON.stringify(source)} not found`,\n );\n }\n for (const messageId of await this.fetchMessages(source)) {\n await this.moveMessageFrom(messageId, source, destination);\n }\n if (!options.keepSourceFolders) {\n await this.deleteFolder(source);\n }\n }\n }\n\n /** Backend-specific relocation, called by `moveFolder()` after validation. */\n protected doMoveFolder(\n _source: string,\n _targetParent: string,\n _targetPath: string,\n ): Promise<void> {\n return notImplemented('moveFolder');\n }\n\n /** Move one message during `mergeFolders()`. Defaults to `moveMessage()`. */\n protected moveMessageFrom(\n messageId: MessageId,\n _source: string,\n destination: string,\n ): Promise<void> {\n return this.moveMessage(messageId, destination);\n }\n\n /** Identifiers of the messages in a folder. */\n fetchMessages(\n _reportsFolder: string,\n _options?: FetchMessagesOptions,\n ): Promise<MessageId[]> {\n return notImplemented('fetchMessages');\n }\n\n /** Raw RFC 822 contents of a message. */\n fetchMessage(\n _messageId: MessageId,\n _options?: FetchMessageOptions,\n ): Promise<string> {\n return notImplemented('fetchMessage');\n }\n\n deleteMessage(_messageId: MessageId): Promise<void> {\n return notImplemented('deleteMessage');\n }\n\n moveMessage(_messageId: MessageId, _folderName: string): Promise<void> {\n return notImplemented('moveMessage');\n }\n\n /** Send a no-op to keep the connection alive (if applicable). */\n keepalive(): Promise<void> {\n return notImplemented('keepalive');\n }\n\n /** Watch for new messages, calling `checkCallback` on new mail or each interval. */\n watch(_options: WatchOptions): Promise<void> {\n return notImplemented('watch');\n }\n\n /** Send through the backend's native API. Returns a message ID when available. */\n sendMessage(_options: SendMessageOptions): Promise<string | null> {\n return notImplemented('sendMessage');\n }\n\n /** Release network resources. No-op for backends without a connection. */\n close(): Promise<void> {\n return Promise.resolve();\n }\n}\n","// Mailbox report processing, ported from parsedmarc/__init__.py:\n// get_dmarc_reports_from_mailbox(), watch_inbox() and their private helpers.\n//\n// Backends are told apart by `connection.kind` instead of parsedmarc's\n// `isinstance` checks, so this module never imports the IMAP, Gmail or\n// Microsoft Graph packages.\n\nimport {\n newSeenAggregateReportIds,\n type ParserConfig,\n type ParserConfigInput,\n replaceParserConfig,\n resolveParserConfig,\n type SeenAggregateReportIds,\n} from '../config';\nimport {\n EmailTooLargeError,\n emailTooLargeError,\n findEnrichmentError,\n ParserError,\n} from '../errors';\nimport { errorText, pyInt } from '../internal/py';\nimport { logger } from '../log';\nimport {\n JobTimeoutError,\n parallelMap,\n parseReportEmailTask,\n WorkerCrashedError,\n} from '../parallel';\nimport {\n classifyParsedEmail,\n emailByteLength,\n parseReportEmail,\n} from '../report-email';\nimport type {\n AggregateReport,\n FailureReport,\n ParsedReport,\n ParsingResults,\n SMTPTLSReport,\n} from '../types';\nimport { PyDateTime } from '../utils/datetime';\nimport { ValueError } from '../utils/email';\nimport { MessageError, RecursionError } from '../utils/py-email/errors';\nimport { sanitizeForLog } from '../utils/sanitize';\nimport type { MailboxConnection, MessageId } from './base';\n\n/**\n * Per-message count of consecutive failed saves, keyed on\n * `failedSaveKey(connection, reportsFolder, messageUid)` (parsedmarc's\n * `_FAILED_SAVE_ATTEMPTS`). Populated when a `saveCallback` reports a batch\n * unsaved, or a message runs past `jobTimeout` or loses its worker; a message\n * whose count exceeds `maxUnsavedRetries` is moved to `{archiveFolder}/Unsaved` and its\n * entry dropped. A successful save clears the entries of its batch.\n *\n * Process-local and deliberately not persisted: a restart re-attempts every\n * message still in the reports folder (retry rather than shelve). Unlike\n * parsedmarc's, the key carries `mailboxIdentity`: IMAP UIDs are small\n * integers, so two mailboxes would share counters. Reconnecting to the same\n * mailbox keeps them.\n */\nexport const FAILED_SAVE_ATTEMPTS: Map<string, number> = new Map();\n\n/** The `FAILED_SAVE_ATTEMPTS` key: Python's `(reports_folder, str(uid))` plus the mailbox. */\nexport function failedSaveKey(\n connection: MailboxConnection,\n reportsFolder: string,\n messageUid: MessageId,\n): string {\n return JSON.stringify([\n connection.mailboxIdentity,\n reportsFolder,\n String(messageUid),\n ]);\n}\n\n/** Each connection's seen aggregate report IDs, for calls without `config`. */\nconst CONNECTION_SEEN_REPORT_IDS = new WeakMap<\n MailboxConnection,\n SeenAggregateReportIds\n>();\n\n/**\n * `resolveParserConfig()` for a mailbox: without an explicit `config`, dedup\n * is per connection instead of per process (parsedmarc's module-wide\n * cache), so a report seen in one mailbox isn't dropped, and its message\n * archived unsaved, in another. Share an explicit `config` to dedup across\n * connections.\n */\nfunction resolveMailboxConfig(\n connection: MailboxConnection,\n input: ParserConfigInput,\n): ParserConfig {\n const config = resolveParserConfig(input);\n if (input.config) {\n return config;\n }\n let seenAggregateReportIds = CONNECTION_SEEN_REPORT_IDS.get(connection);\n if (seenAggregateReportIds === undefined) {\n seenAggregateReportIds = newSeenAggregateReportIds();\n CONNECTION_SEEN_REPORT_IDS.set(connection, seenAggregateReportIds);\n }\n return replaceParserConfig(config, { seenAggregateReportIds });\n}\n\n/** What a save callback returns: `false` means \"not saved\". */\nexport type SaveCallbackResult = boolean | null | undefined | void;\n\n/**\n * Called once per fetched batch with only that batch's reports, before any\n * of its messages is moved or deleted. Returning `false` (or throwing)\n * marks the batch unsaved; anything else commits it.\n */\nexport type SaveCallback = (\n results: ParsingResults,\n) => SaveCallbackResult | Promise<SaveCallbackResult>;\n\n/** Mailbox-handling options shared by both entry points (not part of `config`). */\ninterface MailboxProcessingOptions extends ParserConfigInput {\n /** The folder where reports can be found. Default: `\"INBOX\"`. */\n reportsFolder?: string;\n /** The folder to move processed mail to. Default: `\"Archive\"`. */\n archiveFolder?: string;\n /** Delete messages after processing them. Default: false. */\n delete?: boolean;\n /**\n * Delete aggregate report messages instead of moving them to the\n * `Aggregate` archive subfolder. `null`/unset inherits `delete`.\n */\n deleteAggregate?: boolean | null;\n /**\n * Delete failure report messages instead of moving them to the `Failure`\n * archive subfolder. `null`/unset inherits `delete`.\n */\n deleteFailure?: boolean | null;\n /**\n * Delete SMTP TLS report messages instead of moving them to the\n * `SMTP-TLS` archive subfolder. `null`/unset inherits `delete`.\n */\n deleteSmtpTls?: boolean | null;\n /**\n * Delete unparseable messages instead of moving them to the `Invalid`\n * archive subfolder. `null`/unset inherits `delete`.\n */\n deleteInvalid?: boolean | null;\n /** Do not move or delete messages after processing them. Default: false. */\n test?: boolean;\n /** Messages to read and process before saving (0: no limit). Default: 10. */\n batchSize?: number;\n /**\n * Only messages since this relative time (`5m`, `3h`, `2d`, `1w`). Any\n * other string falls back to one day, with a warning.\n */\n since?: string | null;\n /**\n * Worker threads used to parse messages. Fetching, archiving and dedup\n * stay sequential; with `nProcs > 1`, invalid messages are disposed of\n * after the parsing phase. Default: 1.\n */\n nProcs?: number;\n /**\n * Retries a message gets after its batch was first reported unsaved,\n * before it moves to the `Unsaved` archive subfolder (never deleted).\n * `0` moves it on the first failed save; negative values throw\n * `ValueError`. Default: 2.\n */\n maxUnsavedRetries?: number;\n}\n\n/** Options of {@link getDmarcReportsFromMailbox}. */\nexport interface GetDmarcReportsFromMailboxOptions extends MailboxProcessingOptions {\n /** Results from the previous run, to accumulate into. */\n results?: ParsingResults | null;\n /** Create the destination folders (not used in watch). Default: true. */\n createFolders?: boolean;\n /**\n * Called once per batch before its messages are archived or deleted.\n * Returning `false` leaves them in `reportsFolder` for retry (or moves\n * them to `Unsaved` once retries run out) and keeps their aggregate\n * report keys out of the dedup cache. Throwing counts as unsaved too, and\n * the error is rethrown after that bookkeeping. Still invoked in `test`\n * mode, where the mailbox and retry counters are never touched.\n */\n saveCallback?: SaveCallback | null;\n}\n\n/** Options of {@link watchInbox}. */\nexport interface WatchInboxOptions extends MailboxProcessingOptions {\n /** Seconds to wait for an IMAP IDLE response or between checks. Default: 30. */\n checkTimeout?: number;\n /** Returns true when a config reload or shutdown was requested. */\n configReloading?: (() => boolean) | null;\n /** Aborts the watch loop. */\n signal?: AbortSignal;\n}\n\n/**\n * Fetch one message by UID, casting the UID to what each backend expects\n * (parsedmarc's `_fetch_mailbox_message`). Returns `[messageId, content]`;\n * `messageId` is what later move/delete calls use.\n *\n * A content error raised while fetching (Python's `email.errors.MessageError`\n * or `RecursionError`, from a backend that re-serializes the message) is\n * returned, not thrown, as `content`: a `ParserError` the caller treats like\n * a parse failure, so the message is disposed of as invalid instead of\n * aborting the batch on every run (GHSA-qxwv-2vwj-v828). Transport and\n * service errors still throw: a transient network failure must not move a\n * legitimate report to the invalid folder.\n *\n * With `maxSize` (bytes, not in parsedmarc), a message over the limit is\n * returned as an `EmailTooLargeError` the same way: backends that know a\n * message's size refuse it before downloading it.\n */\nexport async function fetchMailboxMessage(\n connection: MailboxConnection,\n msgUid: MessageId,\n test: boolean,\n maxSize: number | null = null,\n): Promise<[MessageId, string | ParserError]> {\n const limit = maxSize !== null && maxSize > 0 ? { maxSize } : {};\n const messageId: MessageId =\n connection.kind === 'imap' ? pyInt(msgUid) : String(msgUid);\n let content: string;\n try {\n content =\n connection.kind === 'msgraph' || connection.kind === 'maildir'\n ? await connection.fetchMessage(messageId, {\n markRead: !test,\n ...limit,\n })\n : await connection.fetchMessage(messageId, limit);\n } catch (error) {\n if (error instanceof MessageError || error instanceof RecursionError) {\n return [\n messageId,\n new ParserError(\n `Unable to fetch message UID ${messageId}: ${error.name}: ${error.message}`,\n { cause: error },\n ),\n ];\n }\n if (error instanceof EmailTooLargeError) {\n return [\n messageId,\n new EmailTooLargeError(`Message UID ${msgUid}: ${error.message}`, {\n cause: error,\n }),\n ];\n }\n throw error;\n }\n if (limit.maxSize !== undefined) {\n const size = emailByteLength(content);\n if (size > limit.maxSize) {\n const tooLarge = emailTooLargeError(size, limit.maxSize);\n return [\n messageId,\n new EmailTooLargeError(`Message UID ${msgUid}: ${tooLarge.message}`, {\n cause: tooLarge,\n }),\n ];\n }\n }\n return [messageId, content];\n}\n\n/**\n * The warning for a message that couldn't be parsed: a {@link ParserError}'s\n * own message (as parsedmarc logs it), or the error type and message of any\n * other error. Untrusted text is escaped.\n */\nexport function invalidMessageWarning(\n error: unknown,\n messageId: MessageId,\n): string {\n if (error instanceof ParserError) {\n return sanitizeForLog(error.message);\n }\n const name = error instanceof Error ? error.name : 'Error';\n return sanitizeForLog(\n `Unexpected error parsing message UID ${messageId}, treating it as invalid: ${name}: ${errorText(error)}`,\n );\n}\n\n/** The warning for a message that ran past `jobTimeout`. */\nfunction timedOutMessageWarning(\n error: JobTimeoutError,\n messageId: MessageId,\n): string {\n return `Timed out parsing message UID ${messageId}: ${error.message}`;\n}\n\n/** The warning for a message whose worker died (usually out of memory). */\nfunction crashedMessageWarning(\n error: WorkerCrashedError,\n messageId: MessageId,\n): string {\n return `Worker failed parsing message UID ${messageId}: ${error.message}: ${errorText(error.cause)}`;\n}\n\n/**\n * Delete or move an unparseable message (parsedmarc's\n * `_dispose_invalid_message`). Callers pass their effective\n * `deleteInvalid` as `del`.\n */\nexport async function disposeInvalidMessage(\n connection: MailboxConnection,\n messageId: MessageId,\n del: boolean,\n invalidReportsFolder: string,\n): Promise<void> {\n try {\n const id =\n connection.kind === 'imap' ? pyInt(messageId) : String(messageId);\n if (del) {\n logger.debug(`Deleting message UID ${messageId}`);\n await connection.deleteMessage(id);\n } else {\n logger.debug(\n `Moving message UID ${messageId} to ${invalidReportsFolder}`,\n );\n await connection.moveMessage(id, invalidReportsFolder);\n }\n } catch (error) {\n // Logged and left in place, as the archival of parsed reports does,\n // rather than aborting the rest of the batch (GHSA-qxwv-2vwj-v828).\n const action = del ? 'deleting' : 'moving';\n const name = error instanceof Error ? error.name : 'Error';\n logger.error(\n `Mailbox error: Error ${action} invalid message UID ${messageId}: ${name}: ${errorText(error)}`,\n );\n }\n}\n\n/**\n * Consolidate a pre-rename `<archive>/Forensic` subfolder into\n * `<archive>/Failure` (parsedmarc's `_migrate_forensic_archive_folder`).\n * A no-op without a legacy folder; never throws (warn, don't crash).\n */\nexport async function migrateForensicArchiveFolder(\n connection: MailboxConnection,\n archiveFolder: string,\n): Promise<void> {\n const oldFolder = `${archiveFolder}/Forensic`;\n const newFolder = `${archiveFolder}/Failure`;\n try {\n if (!(await connection.folderExists(oldFolder))) {\n return;\n }\n if (await connection.folderExists(newFolder)) {\n // Both exist (a partial earlier migration, or a manually created\n // Failure folder): merge and drop the emptied legacy folder.\n await connection.mergeFolders(oldFolder, newFolder);\n logger.info(\n `Merged legacy archive folder ${oldFolder} into ${newFolder}`,\n );\n } else {\n await connection.renameFolder(oldFolder, newFolder);\n logger.info(`Renamed legacy archive folder ${oldFolder} to ${newFolder}`);\n }\n } catch (error) {\n logger.warning(\n `Could not migrate legacy archive folder ${oldFolder} to ${newFolder}: ${errorText(error)}`,\n );\n }\n}\n\n/**\n * Best-effort create `folder` if the backend says it is missing\n * (parsedmarc's `_ensure_folder`). Never throws.\n */\nexport async function ensureFolder(\n connection: MailboxConnection,\n folder: string,\n): Promise<void> {\n try {\n if (!(await connection.folderExists(folder))) {\n await connection.createFolder(folder);\n }\n } catch (error) {\n logger.warning(`Could not create folder ${folder}: ${errorText(error)}`);\n }\n}\n\nconst SINCE_PATTERN = /^(?<amount>\\p{Nd}+)(?<unit>[mhdw]\\n?)$/u;\nconst MINUTES_PER_UNIT: Readonly<Record<string, number>> = {\n m: 1,\n h: 60,\n d: 60 * 24,\n w: 60 * 24 * 7,\n};\nconst DEFAULT_SINCE_MINUTES = 1440;\nconst SECONDS_PER_MINUTE = 60;\nconst DECIMAL_BASE = 10;\nconst MS_PER_SECOND = 1000;\n\n/** Python's `int()` of a run of Unicode decimal digits (`\\d` matches them all). */\nfunction decimalDigitsValue(digits: string): number {\n let value = 0;\n for (const char of digits) {\n let codePoint = char.codePointAt(0) ?? 0;\n // Unicode decimal digits come in contiguous 0-9 runs: find the run's\n // start, then the digit is the offset into its block of ten.\n let start = codePoint;\n while (/^\\p{Nd}$/u.test(String.fromCodePoint(start - 1))) {\n start -= 1;\n }\n codePoint = (codePoint - start) % DECIMAL_BASE;\n value = value * DECIMAL_BASE + codePoint;\n }\n return value;\n}\n\n/** The minutes a relative `since` (`5m`, `3h`, `2d`, `1w`) stands for. */\nfunction sinceMinutes(since: string): number {\n const match = SINCE_PATTERN.exec(since);\n if (!match?.groups) {\n logger.warning(\n `Incorrect format for 'since' option. Provided value: ${since}, ` +\n 'expected values: (5m|3h|2d|1w). Ignoring option, fetching ' +\n 'messages for last 24hrs. SMTP does not support a time or ' +\n 'timezone in since. See ' +\n 'https://www.rfc-editor.org/rfc/rfc3501#page-52',\n );\n return DEFAULT_SINCE_MINUTES;\n }\n // Python compares re.split()'s unit token exactly, so \"2d\\n\" (accepted by\n // `$`) keeps the one-day default.\n const perUnit = MINUTES_PER_UNIT[match.groups.unit ?? ''];\n if (perUnit === undefined) {\n return DEFAULT_SINCE_MINUTES;\n }\n return decimalDigitsValue(match.groups.amount ?? '') * perUnit;\n}\n\n/**\n * `strftime(\"%s\")` of an aware UTC datetime, as CPython computes it on\n * glibc: the tm fields go through mktime(), which reads them as local\n * time, so the result is off by the local UTC offset (exact under TZ=UTC).\n */\nfunction pyStrftimeEpoch(datetime: PyDateTime): string {\n return String(Math.floor(datetime.withOffset(null).timestamp()));\n}\n\n/** Backend-specific `since` and `currentTime` strings. */\nfunction resolveSince(\n connection: MailboxConnection,\n since: string,\n): { since: string; currentTime: string | null } {\n const minutes = sinceMinutes(since);\n const sinceTime = (): PyDateTime =>\n PyDateTime.now(true).addSeconds(-minutes * SECONDS_PER_MINUTE);\n switch (connection.kind) {\n case 'imap':\n logger.debug(\n \"Only days and weeks values in 'since' option are considered \" +\n 'for IMAP connections. Examples: 2d or 1w',\n );\n return {\n since: sinceTime().strftime('%d-%b-%Y'),\n currentTime: PyDateTime.now(true).strftime('%d-%b-%Y'),\n };\n case 'msgraph':\n return {\n since: sinceTime().isoformat(),\n currentTime: PyDateTime.now(true).isoformat(),\n };\n case 'gmail':\n return {\n since: pyStrftimeEpoch(sinceTime()),\n currentTime: pyStrftimeEpoch(PyDateTime.now(true)),\n };\n default:\n return { since, currentTime: null };\n }\n}\n\ninterface ArchiveFolders {\n aggregate: string;\n failure: string;\n smtpTls: string;\n invalid: string;\n unsaved: string;\n}\n\n/** The per-type delete flags, each inheriting `delete` when unset. */\ninterface DeleteFlags {\n aggregate: boolean;\n failure: boolean;\n smtpTls: boolean;\n invalid: boolean;\n}\n\n/** One batch's parsed reports and the message IDs behind them. */\nclass Batch {\n readonly aggregateReports: AggregateReport[] = [];\n readonly failureReports: FailureReport[] = [];\n readonly smtpTlsReports: SMTPTLSReport[] = [];\n /** Aggregate dedup keys, staged until the batch is saved. */\n readonly pendingAggregateKeys = new Set<string>();\n readonly aggregateMsgUids: MessageId[] = [];\n readonly failureMsgUids: MessageId[] = [];\n readonly smtpTlsMsgUids: MessageId[] = [];\n\n constructor(private readonly config: ParserConfig) {}\n\n add(parsedEmail: ParsedReport, messageId: MessageId): void {\n const reportType = classifyParsedEmail(\n parsedEmail,\n this.aggregateReports,\n this.failureReports,\n this.smtpTlsReports,\n {\n seenAggregateReportIds: this.config.seenAggregateReportIds,\n pendingAggregateKeys: this.pendingAggregateKeys,\n },\n );\n // The UID is kept even for a dropped duplicate, so its message is\n // archived too.\n if (reportType === 'aggregate') {\n this.aggregateMsgUids.push(messageId);\n } else if (reportType === 'failure') {\n this.failureMsgUids.push(messageId);\n } else if (reportType === 'smtp_tls') {\n this.smtpTlsMsgUids.push(messageId);\n }\n }\n\n results(): ParsingResults {\n return {\n aggregateReports: this.aggregateReports,\n failureReports: this.failureReports,\n smtpTlsReports: this.smtpTlsReports,\n };\n }\n\n msgUids(): MessageId[] {\n return [\n ...this.aggregateMsgUids,\n ...this.failureMsgUids,\n ...this.smtpTlsMsgUids,\n ];\n }\n}\n\ninterface ParseContext {\n connection: MailboxConnection;\n /** `config.maxEmailSize`, or null when unlimited. */\n maxSize: number | null;\n messages: MessageId[];\n messageLimit: number;\n test: boolean;\n config: ParserConfig;\n batch: Batch;\n deleteInvalid: boolean;\n invalidReportsFolder: string;\n reportsFolder: string;\n unsavedReportsFolder: string;\n maxUnsavedRetries: number;\n}\n\n/**\n * Put back messages that ran past `jobTimeout` or lost their worker. Neither\n * says anything about the content (the same report can fit in on a healthy\n * resolver, or in a worker with free memory), so unlike an invalid message\n * one is never deleted: it is retried like an unsaved batch, up to\n * `maxUnsavedRetries` times so it can't hold up every pass, then moved to\n * `Unsaved`.\n */\nasync function holdBackUnfinishedMessages(\n context: ParseContext,\n messageIds: MessageId[],\n): Promise<void> {\n if (context.test || messageIds.length === 0) {\n return;\n }\n await holdBackUnsavedBatch(\n context.connection,\n messageIds,\n context.reportsFolder,\n context.unsavedReportsFolder,\n context.maxUnsavedRetries,\n );\n}\n\nasync function parseInParallel(\n context: ParseContext,\n nProcs: number,\n): Promise<void> {\n const { connection, messages, messageLimit, test, batch } = context;\n // Results come back in submission order, so the oldest queued ID always\n // belongs to the next result.\n const fetchedIds: MessageId[] = [];\n const invalidMsgIds: MessageId[] = [];\n const unfinishedMsgIds: MessageId[] = [];\n\n async function* jobs(): AsyncGenerator<string> {\n for (let index = 0; index < messageLimit; index += 1) {\n const msgUid = messages[index] as MessageId;\n logger.debug(\n `Processing message ${index + 1} of ${messageLimit}: UID ${msgUid}`,\n );\n const [messageId, msgContent] = await fetchMailboxMessage(\n connection,\n msgUid,\n test,\n context.maxSize,\n );\n if (msgContent instanceof ParserError) {\n // Never submitted, so never yielded and never queued in fetchedIds;\n // the post-loop disposal handles it.\n logger.warning(sanitizeForLog(msgContent.message));\n invalidMsgIds.push(messageId);\n continue;\n }\n fetchedIds.push(messageId);\n yield msgContent;\n }\n }\n\n // A failure of one message (a ParserError, an unexpected error) makes only\n // that message invalid. A failed heartbeat or an enrichment failure is no\n // such thing: it ends this loop before anything is disposed of, and the\n // batch stays in the mailbox for the next run.\n for await (const result of parallelMap<\n string,\n ParsedReport | ParserError | Error\n >(parseReportEmailTask(context.config), jobs(), nProcs, {\n heartbeat: () => connection.keepalive(),\n onJobError: error =>\n error instanceof Error ? error : new Error(String(error)),\n })) {\n const messageId = fetchedIds.shift() as MessageId;\n if (result instanceof JobTimeoutError) {\n logger.warning(timedOutMessageWarning(result, messageId));\n unfinishedMsgIds.push(messageId);\n } else if (result instanceof WorkerCrashedError) {\n logger.warning(crashedMessageWarning(result, messageId));\n unfinishedMsgIds.push(messageId);\n } else if (result instanceof Error) {\n const enrichmentError = findEnrichmentError(result);\n if (enrichmentError !== null) {\n throw enrichmentError;\n }\n logger.warning(invalidMessageWarning(result, messageId));\n invalidMsgIds.push(messageId);\n } else {\n batch.add(result, messageId);\n }\n }\n\n if (!test) {\n for (const invalidMessageId of invalidMsgIds) {\n await disposeInvalidMessage(\n connection,\n invalidMessageId,\n context.deleteInvalid,\n context.invalidReportsFolder,\n );\n }\n }\n await holdBackUnfinishedMessages(context, unfinishedMsgIds);\n}\n\n/**\n * `parseReportEmail()` on the calling thread, with the connection kept alive\n * and `config.jobTimeout` checked at each keepalive. A failure of either\n * reaches the caller as thrown (see `parseReportEmail()`): the message isn't\n * at fault, and a `ParserError` here would dispose of a legitimate report.\n */\nasync function parseWithKeepalive(\n context: ParseContext,\n content: string,\n): Promise<ParsedReport> {\n const { connection, config } = context;\n const deadline =\n config.jobTimeout > 0\n ? Date.now() + config.jobTimeout * MS_PER_SECOND\n : Number.POSITIVE_INFINITY;\n const keepAlive = async (): Promise<void> => {\n if (Date.now() >= deadline) {\n throw new JobTimeoutError(\n `Job did not finish within ${config.jobTimeout} seconds`,\n );\n }\n await connection.keepalive();\n };\n return parseReportEmail(content, { config, keepAlive });\n}\n\nasync function parseSequentially(context: ParseContext): Promise<void> {\n const { connection, messages, messageLimit, test, batch } = context;\n for (let index = 0; index < messageLimit; index += 1) {\n const msgUid = messages[index] as MessageId;\n logger.debug(\n `Processing message ${index + 1} of ${messageLimit}: UID ${msgUid}`,\n );\n const [messageId, msgContent] = await fetchMailboxMessage(\n connection,\n msgUid,\n test,\n context.maxSize,\n );\n try {\n if (msgContent instanceof ParserError) {\n throw msgContent;\n }\n batch.add(await parseWithKeepalive(context, msgContent), messageId);\n } catch (error) {\n if (error instanceof JobTimeoutError) {\n logger.warning(timedOutMessageWarning(error, messageId));\n await holdBackUnfinishedMessages(context, [messageId]);\n continue;\n }\n if (!(error instanceof ParserError)) {\n throw error;\n }\n // Every report would fail alike, whatever it holds: the pass stops\n // and the batch stays in the mailbox, as after a failed keepalive.\n const enrichmentError = findEnrichmentError(error);\n if (enrichmentError !== null) {\n throw enrichmentError;\n }\n logger.warning(invalidMessageWarning(error, messageId));\n if (!test) {\n await disposeInvalidMessage(\n connection,\n messageId,\n context.deleteInvalid,\n context.invalidReportsFolder,\n );\n }\n }\n }\n}\n\n/**\n * Failure bookkeeping for an unsaved batch: count the attempt for each\n * message, keep those under the cap in place and move the rest to\n * `Unsaved` (never deleted).\n */\nasync function holdBackUnsavedBatch(\n connection: MailboxConnection,\n batchMsgUids: MessageId[],\n reportsFolder: string,\n unsavedReportsFolder: string,\n maxUnsavedRetries: number,\n): Promise<void> {\n const retainedUids: MessageId[] = [];\n const overCapUids: MessageId[] = [];\n let highestAttempt = 0;\n for (const msgUid of batchMsgUids) {\n const attemptsKey = failedSaveKey(connection, reportsFolder, msgUid);\n const attempts = (FAILED_SAVE_ATTEMPTS.get(attemptsKey) ?? 0) + 1;\n FAILED_SAVE_ATTEMPTS.set(attemptsKey, attempts);\n if (attempts > maxUnsavedRetries) {\n overCapUids.push(msgUid);\n } else {\n retainedUids.push(msgUid);\n highestAttempt = Math.max(highestAttempt, attempts);\n }\n }\n if (retainedUids.length > 0) {\n logger.error(\n `Reports were not saved: leaving ${retainedUids.length} ` +\n `message(s) in ${reportsFolder} to retry on the next run ` +\n `or check (failed attempt ${highestAttempt} of ` +\n `${maxUnsavedRetries + 1})`,\n );\n }\n if (overCapUids.length > 0) {\n logger.error(\n `Reports were not saved after ${maxUnsavedRetries + 1} ` +\n `attempt(s): moving ${overCapUids.length} message(s) from ` +\n `${reportsFolder} to ${unsavedReportsFolder} instead of ` +\n 'retrying them further. They are never deleted -- fix the ' +\n `output destination, then move them back to ${reportsFolder}`,\n );\n await ensureFolder(connection, unsavedReportsFolder);\n for (const msgUid of overCapUids) {\n try {\n await connection.moveMessage(msgUid, unsavedReportsFolder);\n } catch (error) {\n logger.error(\n `Mailbox error: Error moving message UID ${msgUid}: ${errorText(error)}`,\n );\n continue;\n }\n // Drop the counter only once the message is out of the retry loop:\n // after a failed move it stays over the cap, so the move is retried.\n FAILED_SAVE_ATTEMPTS.delete(\n failedSaveKey(connection, reportsFolder, msgUid),\n );\n }\n }\n}\n\n/** Delete or archive each report type's messages per its delete flag. */\nasync function disposeSavedBatch(\n connection: MailboxConnection,\n batch: Batch,\n reportsFolder: string,\n folders: ArchiveFolders,\n deleteFlags: DeleteFlags,\n): Promise<void> {\n const groups: [MessageId[], boolean, string, string][] = [\n [\n batch.aggregateMsgUids,\n deleteFlags.aggregate,\n folders.aggregate,\n 'aggregate report',\n ],\n [\n batch.failureMsgUids,\n deleteFlags.failure,\n folders.failure,\n 'failure report',\n ],\n [\n batch.smtpTlsMsgUids,\n deleteFlags.smtpTls,\n folders.smtpTls,\n 'SMTP TLS report',\n ],\n ];\n for (const [msgUids, deleteType, destinationFolder, label] of groups) {\n const numberOfMsgs = msgUids.length;\n if (numberOfMsgs === 0) {\n continue;\n }\n if (!deleteType) {\n logger.debug(\n `Moving ${label} messages from ${reportsFolder} to ${destinationFolder}`,\n );\n }\n for (const [index, msgUid] of msgUids.entries()) {\n if (deleteType) {\n logger.debug(\n `Deleting message ${index + 1} of ${numberOfMsgs}: UID ${msgUid}`,\n );\n try {\n await connection.deleteMessage(msgUid);\n } catch (error) {\n logger.error(\n `Mailbox error: Error deleting message UID ${msgUid}: ${errorText(error)}`,\n );\n }\n } else {\n logger.debug(\n `Moving message ${index + 1} of ${numberOfMsgs}: UID ${msgUid}`,\n );\n try {\n await connection.moveMessage(msgUid, destinationFolder);\n } catch (error) {\n logger.error(\n `Mailbox error: Error moving message UID ${msgUid}: ${errorText(error)}`,\n );\n }\n }\n }\n }\n}\n\n/**\n * Fetches and parses DMARC and SMTP TLS reports from a mailbox, then\n * archives (or deletes) each message by report type: `<archive>/Aggregate`,\n * `Failure`, `SMTP-TLS`, `Invalid`, and `Unsaved` for batches the\n * `saveCallback` kept failing to save.\n *\n * Parsing options (or `config`) work as in every parsing function; the\n * mailbox options always apply. With `batchSize: 0` the reports folder is\n * re-checked after the run and new messages are processed recursively.\n *\n * Throws an `EnrichmentError` when enrichment fails for every report\n * (a revoked IPinfo token, no readable IP database): the batch stays in\n * `reportsFolder`, unsaved, for the next run. Unlike parsedmarc, which\n * files those reports as invalid or empty, and deletes them with `delete`.\n *\n * Throws `ValueError` when a delete option is combined with `test`, when\n * `maxUnsavedRetries` is negative, or without a connection.\n */\nexport async function getDmarcReportsFromMailbox(\n connection: MailboxConnection,\n options: GetDmarcReportsFromMailboxOptions = {},\n): Promise<ParsingResults> {\n const {\n reportsFolder = 'INBOX',\n archiveFolder = 'Archive',\n delete: del = false,\n deleteAggregate = null,\n deleteFailure = null,\n deleteSmtpTls = null,\n deleteInvalid = null,\n test = false,\n results = null,\n batchSize = 10,\n since: sinceOption = null,\n createFolders = true,\n nProcs = 1,\n saveCallback = null,\n maxUnsavedRetries = 2,\n ...configOptions\n } = options;\n\n // Each per-type flag inherits `delete` when unset; every decision below\n // reads only these resolved flags.\n const deleteFlags: DeleteFlags = {\n aggregate: deleteAggregate ?? del,\n failure: deleteFailure ?? del,\n smtpTls: deleteSmtpTls ?? del,\n invalid: deleteInvalid ?? del,\n };\n\n if (\n test &&\n (deleteFlags.aggregate ||\n deleteFlags.failure ||\n deleteFlags.smtpTls ||\n deleteFlags.invalid)\n ) {\n throw new ValueError('delete options and test are mutually exclusive');\n }\n if (maxUnsavedRetries < 0) {\n throw new ValueError('max_unsaved_retries must be >= 0');\n }\n // Runtime guard for JavaScript callers and casts.\n if ((connection as MailboxConnection | null | undefined) == null) {\n throw new ValueError('Must supply a connection');\n }\n\n const config = resolveMailboxConfig(connection, configOptions);\n\n // Used by the batchSize 0 re-check below.\n let currentTime: string | null = null;\n\n let aggregateReports: AggregateReport[] = [];\n let failureReports: FailureReport[] = [];\n let smtpTlsReports: SMTPTLSReport[] = [];\n // This call's own reports, kept apart from the accumulated lists so the\n // save callback gets only what this batch parsed.\n const batch = new Batch(config);\n const folders: ArchiveFolders = {\n aggregate: `${archiveFolder}/Aggregate`,\n failure: `${archiveFolder}/Failure`,\n smtpTls: `${archiveFolder}/SMTP-TLS`,\n invalid: `${archiveFolder}/Invalid`,\n unsaved: `${archiveFolder}/Unsaved`,\n };\n\n if (results) {\n aggregateReports = [...results.aggregateReports];\n failureReports = [...results.failureReports];\n smtpTlsReports = [...results.smtpTlsReports];\n }\n\n if (!test && createFolders) {\n await migrateForensicArchiveFolder(connection, archiveFolder);\n await connection.createFolder(archiveFolder);\n await connection.createFolder(folders.aggregate);\n await connection.createFolder(folders.failure);\n await connection.createFolder(folders.smtpTls);\n await connection.createFolder(folders.invalid);\n if (saveCallback !== null) {\n // Only a callback can hold a batch back; without one the folder\n // would sit empty in every mailbox.\n await connection.createFolder(folders.unsaved);\n }\n }\n\n let since = sinceOption;\n if (since) {\n ({ since, currentTime } = resolveSince(connection, since));\n }\n\n const messages = await connection.fetchMessages(reportsFolder, {\n batchSize,\n since,\n });\n let totalMessages = messages.length;\n logger.debug(`Found ${messages.length} messages in ${reportsFolder}`);\n\n const messageLimit =\n batchSize && !since ? Math.min(totalMessages, batchSize) : totalMessages;\n\n logger.debug(`Processing ${messageLimit} messages`);\n\n const context: ParseContext = {\n connection,\n maxSize: config.maxEmailSize > 0 ? config.maxEmailSize : null,\n messages,\n messageLimit,\n test,\n config,\n batch,\n deleteInvalid: deleteFlags.invalid,\n invalidReportsFolder: folders.invalid,\n reportsFolder,\n unsavedReportsFolder: folders.unsaved,\n maxUnsavedRetries,\n };\n if (nProcs > 1 && messageLimit > 1) {\n await parseInParallel(context, nProcs);\n } else {\n await parseSequentially(context);\n }\n\n // Ask the caller whether the batch reached its outputs before touching a\n // single message: an unsaved batch exists nowhere else yet.\n let persisted = true;\n let callbackError: { error: unknown } | null = null;\n if (saveCallback !== null) {\n try {\n persisted = (await saveCallback(batch.results())) !== false;\n } catch (error) {\n // A throwing callback is a failed save too: the bookkeeping below\n // still runs (so the retry cap applies even when a watch loop\n // swallows the error), then the error is rethrown.\n persisted = false;\n callbackError = { error };\n }\n }\n\n const batchMsgUids = batch.msgUids();\n\n if (persisted) {\n for (const reportKey of batch.pendingAggregateKeys) {\n config.seenAggregateReportIds.set(reportKey, true);\n }\n if (!test) {\n for (const msgUid of batchMsgUids) {\n FAILED_SAVE_ATTEMPTS.delete(\n failedSaveKey(connection, reportsFolder, msgUid),\n );\n }\n }\n } else if (!test) {\n await holdBackUnsavedBatch(\n connection,\n batchMsgUids,\n reportsFolder,\n folders.unsaved,\n maxUnsavedRetries,\n );\n }\n\n if (callbackError !== null) {\n throw callbackError.error;\n }\n\n // One by one: with batchSize 0, a spread can run out of call stack.\n for (const report of batch.aggregateReports) {\n aggregateReports.push(report);\n }\n for (const report of batch.failureReports) {\n failureReports.push(report);\n }\n for (const report of batch.smtpTlsReports) {\n smtpTlsReports.push(report);\n }\n\n if (persisted && !test) {\n await disposeSavedBatch(\n connection,\n batch,\n reportsFolder,\n folders,\n deleteFlags,\n );\n }\n let accumulated: ParsingResults = {\n aggregateReports,\n failureReports,\n smtpTlsReports,\n };\n\n // An unsaved batch left its messages in place; re-checking now would\n // reprocess them at once and burn the retry cap in one call.\n if (persisted && !test && !batchSize) {\n totalMessages = currentTime\n ? (await connection.fetchMessages(reportsFolder, { since: currentTime }))\n .length\n : (await connection.fetchMessages(reportsFolder)).length;\n } else {\n totalMessages = 0;\n }\n\n if (totalMessages > 0) {\n // Process emails that came in during the last run. Like parsedmarc,\n // batchSize and createFolders are left at their defaults here.\n accumulated = await getDmarcReportsFromMailbox(connection, {\n reportsFolder,\n archiveFolder,\n delete: del,\n deleteAggregate: deleteFlags.aggregate,\n deleteFailure: deleteFlags.failure,\n deleteSmtpTls: deleteFlags.smtpTls,\n deleteInvalid: deleteFlags.invalid,\n test,\n results: accumulated,\n since: currentTime,\n nProcs,\n saveCallback,\n maxUnsavedRetries,\n config,\n });\n }\n\n return accumulated;\n}\n\n/**\n * Watches the mailbox for new messages and processes them with\n * {@link getDmarcReportsFromMailbox} on every check, with `callback` as its\n * `saveCallback`: it runs once per batch, before the batch's messages are\n * archived or deleted, and returning `false` (or throwing) leaves them for\n * the next check. What happens to a thrown error is backend-specific: the\n * IMAP and Maildir watch loops log it and keep checking; Gmail and\n * Microsoft Graph end the watch.\n *\n * Throws `ValueError` for a negative `maxUnsavedRetries` before watching.\n */\nexport async function watchInbox(\n mailboxConnection: MailboxConnection,\n callback: SaveCallback,\n options: WatchInboxOptions = {},\n): Promise<void> {\n const {\n reportsFolder = 'INBOX',\n archiveFolder = 'Archive',\n delete: del = false,\n deleteAggregate = null,\n deleteFailure = null,\n deleteSmtpTls = null,\n deleteInvalid = null,\n test = false,\n checkTimeout = 30,\n batchSize = 10,\n since = null,\n configReloading = null,\n nProcs = 1,\n maxUnsavedRetries = 2,\n signal,\n ...configOptions\n } = options;\n\n // Validated up front: thrown inside a check, the IMAP and Maildir watch\n // loops would swallow it and retry forever.\n if (maxUnsavedRetries < 0) {\n throw new ValueError('max_unsaved_retries must be >= 0');\n }\n\n const config = resolveMailboxConfig(mailboxConnection, configOptions);\n\n const checkCallback = async (\n connection: MailboxConnection,\n ): Promise<void> => {\n await getDmarcReportsFromMailbox(connection, {\n reportsFolder,\n archiveFolder,\n delete: del,\n deleteAggregate,\n deleteFailure,\n deleteSmtpTls,\n deleteInvalid,\n test,\n batchSize,\n since,\n createFolders: false,\n nProcs,\n saveCallback: callback,\n maxUnsavedRetries,\n config,\n });\n };\n\n await mailboxConnection.watch({\n checkCallback,\n checkTimeout,\n ...(configReloading === null ? {} : { configReloading }),\n ...(signal === undefined ? {} : { signal }),\n });\n}\n","// Port of parsedmarc's file output: `append_json`, `append_csv`,\n// `save_output` and `get_report_zip` (parsedmarc/__init__.py).\n\nimport { randomBytes } from 'node:crypto';\nimport type { Stats } from 'node:fs';\nimport {\n type FileHandle,\n opendir,\n mkdir,\n mkdtemp,\n open,\n readFile,\n realpath,\n rename,\n rm,\n stat,\n writeFile,\n} from 'node:fs/promises';\nimport { homedir, tmpdir } from 'node:os';\nimport path from 'node:path';\n\nimport { zipSync, type Zippable, type ZipOptions } from 'fflate';\n\nimport { parsedAggregateReportsToCsv } from './aggregate';\nimport { parsedFailureReportsToCsv } from './failure';\nimport { errorText, PyValueError } from './internal/py';\nimport { pyJsonDumps } from './internal/py-json';\nimport { JSONDecodeError, pyJsonLoads } from './internal/py-json-loads';\nimport { logger } from './log';\nimport { toSnakeCaseKeys } from './serialize';\nimport { parsedSmtpTlsReportsToCsv } from './smtp-tls';\nimport type {\n AggregateReport,\n FailureReport,\n ParsingResults,\n SMTPTLSReport,\n} from './types';\nimport { getFilenameSafeString, ValueError } from './utils/email';\nimport { pyDecode } from './utils/py-codecs';\nimport { sanitizeForLog } from './utils/sanitize';\n\nexport interface SaveOutputOptions {\n /** The path to the directory to save in (default `output`; `~` expands). */\n outputDirectory?: string;\n /** Filename for the aggregate JSON file (default `aggregate.json`). */\n aggregateJsonFilename?: string;\n /** Filename for the failure JSON file (default `failure.json`). */\n failureJsonFilename?: string;\n /** Filename for the SMTP TLS JSON file (default `smtp_tls.json`). */\n smtpTlsJsonFilename?: string;\n /** Filename for the aggregate CSV file (default `aggregate.csv`). */\n aggregateCsvFilename?: string;\n /** Filename for the failure CSV file (default `failure.csv`). */\n failureCsvFilename?: string;\n /** Filename for the SMTP TLS CSV file (default `smtp_tls.csv`). */\n smtpTlsCsvFilename?: string;\n /**\n * Prefix CSV text cells that a spreadsheet would run as a formula with\n * `'` (see `CsvOptions.formulaEscape`). Default true, like parsedmarc.\n */\n csvFormulaEscape?: boolean;\n}\n\nexport interface GetReportZipOptions {\n /** See {@link SaveOutputOptions.csvFormulaEscape}. Default true. */\n csvFormulaEscape?: boolean;\n}\n\nfunction errorCode(error: unknown): string | undefined {\n if (error instanceof Error && 'code' in error) {\n return typeof error.code === 'string' ? error.code : undefined;\n }\n return undefined;\n}\n\n/** `os.stat()` that returns `null` where `os.path.exists()` is false. */\nasync function statOrNull(filePath: string): Promise<Stats | null> {\n try {\n return await stat(filePath);\n } catch (error) {\n // os.path.exists() is False for any OSError (missing, broken link, ...).\n if (errorCode(error) !== undefined) {\n return null;\n }\n throw error;\n }\n}\n\n/** Python's `os.path.expanduser()` for `~` and `~/...`. */\nfunction expandUser(filePath: string): string {\n if (filePath === '~' || filePath.startsWith('~/')) {\n return path.join(homedir(), filePath.slice(1));\n }\n return filePath;\n}\n\n/**\n * Files up to this size are parsed and rewritten, like parsedmarc. A larger\n * file that ends the way {@link appendJson} writes it only gets the new\n * entries spliced in: parsing it would take seconds per batch.\n */\nconst MAX_REWRITTEN_JSON_BYTES = 8 * 1024 * 1024;\n/** The largest file ever parsed; V8 strings stop at 512 MiB of characters. */\nconst MAX_PARSED_JSON_BYTES = 256 * 1024 * 1024;\n/** How `json.dump(reports, indent=2)` starts and ends a list of objects. */\nconst JSON_ARRAY_HEAD = '[\\n {';\nconst JSON_ARRAY_TAIL = '\\n }\\n]';\n/** The closing `\\n]`, replaced when entries are spliced in. */\nconst JSON_ARRAY_CLOSE_BYTES = 2;\nconst COPY_CHUNK_BYTES = 1024 * 1024;\nconst TEMP_SUFFIX_BYTES = 6;\nconst PERMISSION_BITS = 0o7777;\n\n/** One array element, laid out as `json.dump(list, indent=2)` writes it. */\nfunction encodeJsonEntry(entry: unknown): string {\n try {\n return pyJsonDumps([entry], { ensureAscii: false, indent: 2 }).slice(2, -2);\n } catch (error) {\n if (error instanceof RangeError) {\n throw new ValueError(\n `Report too large for JSON output: ${error.message}`,\n );\n }\n throw error;\n }\n}\n\n/** Whether a file looks like what `json.dump(reports, indent=2)` writes. */\nasync function isJsonArrayOfObjects(\n filename: string,\n size: number,\n): Promise<boolean> {\n const handle = await open(filename, 'r');\n try {\n const head = Buffer.alloc(JSON_ARRAY_HEAD.length);\n const tail = Buffer.alloc(JSON_ARRAY_TAIL.length);\n await handle.read(head, 0, head.length, 0);\n await handle.read(tail, 0, tail.length, size - tail.length);\n return (\n head.toString('latin1') === JSON_ARRAY_HEAD &&\n tail.toString('latin1') === JSON_ARRAY_TAIL\n );\n } finally {\n await handle.close();\n }\n}\n\n/** Copy the first `length` bytes of `filename`, without holding them all. */\nasync function copyFileStart(\n filename: string,\n length: number,\n output: FileHandle,\n): Promise<void> {\n const input = await open(filename, 'r');\n try {\n const buffer = Buffer.allocUnsafe(COPY_CHUNK_BYTES);\n let remaining = length;\n while (remaining > 0) {\n // oxlint-disable-next-line no-await-in-loop -- bytes stay in order\n const { bytesRead } = await input.read(\n buffer,\n 0,\n Math.min(remaining, buffer.length),\n );\n if (bytesRead === 0) {\n throw new ValueError(`${filename} shrank while being copied`);\n }\n // oxlint-disable-next-line no-await-in-loop -- bytes stay in order\n await output.writeFile(buffer.subarray(0, bytesRead));\n remaining -= bytesRead;\n }\n } finally {\n await input.close();\n }\n}\n\n/**\n * Replace `filename` with what `write` produces: a temporary file in the same\n * directory, flushed, then renamed over it, so a crash or a full disk leaves\n * the old content whole. Symbolic links, the mode and (when allowed) the\n * owner are kept. Anything but a regular file (a FIFO, `/dev/stdout`) can't\n * be replaced and is written directly, like Python.\n */\nasync function replaceFile(\n filename: string,\n info: Stats | null,\n write: (output: FileHandle) => Promise<void>,\n): Promise<void> {\n if (info !== null && !info.isFile()) {\n const output = await open(filename, 'w');\n try {\n await write(output);\n } finally {\n await output.close();\n }\n return;\n }\n const target = info === null ? filename : await realpath(filename);\n const suffix = randomBytes(TEMP_SUFFIX_BYTES).toString('hex');\n const tempPath = `${target}.${suffix}.tmp`;\n const output = await open(tempPath, 'wx');\n try {\n await write(output);\n if (info !== null) {\n await output.chmod(info.mode & PERMISSION_BITS);\n await output.chown(info.uid, info.gid).catch(() => undefined);\n }\n await output.sync();\n await output.close();\n await rename(tempPath, target);\n } catch (error) {\n await output.close().catch(() => undefined);\n await rm(tempPath, { force: true });\n throw error;\n }\n}\n\n/** Keep a file {@link appendJson} can't use next to it, and say so. */\nasync function setAsideJsonFile(\n filename: string,\n reason: string,\n): Promise<void> {\n const aside = `${filename}.corrupt-${Date.now()}`;\n await rename(filename, aside);\n logger.warning(\n `${sanitizeForLog(filename)} ${reason}: moved to ${sanitizeForLog(aside)}, starting a new file`,\n );\n}\n\n/**\n * The entries of an existing JSON array to carry over, or `null` when the\n * file was moved aside (it holds no array that can be read).\n */\nasync function readJsonArray(\n filename: string,\n size: number,\n): Promise<unknown[] | null> {\n if (size > MAX_PARSED_JSON_BYTES) {\n await setAsideJsonFile(filename, 'is too large to parse');\n return null;\n }\n let bytes: Uint8Array;\n try {\n bytes = await readFile(filename);\n } catch (error) {\n if (errorCode(error) === undefined) {\n throw error;\n }\n await setAsideJsonFile(filename, `is unreadable (${errorText(error)})`);\n return null;\n }\n // Strict UTF-8 like open(encoding=\"utf-8\"): UnicodeDecodeError is not\n // caught by parsedmarc and propagates.\n const text = pyDecode(bytes, 'utf-8');\n try {\n // Exact numbers: json.dump() writes back what json.loads() read (big\n // ints, `1.0`, `NaN`), which JS numbers would change.\n const loaded: unknown = pyJsonLoads(text, { exactNumbers: true });\n if (Array.isArray(loaded)) {\n return loaded;\n }\n await setAsideJsonFile(filename, 'does not hold a JSON array');\n return null;\n } catch (error) {\n if (!(error instanceof JSONDecodeError)) {\n throw error;\n }\n await setAsideJsonFile(filename, 'is not valid JSON');\n return null;\n }\n}\n\nasync function appendJsonEntries(\n filename: string,\n reports: readonly unknown[],\n): Promise<void> {\n const entries = reports.map(report => toSnakeCaseKeys(report));\n const info = await statOrNull(filename);\n let existing: unknown[] = [];\n let keptBytes = 0;\n if (info?.isFile() && info.size > 0) {\n if (\n info.size > MAX_REWRITTEN_JSON_BYTES &&\n (await isJsonArrayOfObjects(filename, info.size))\n ) {\n keptBytes = info.size - JSON_ARRAY_CLOSE_BYTES;\n } else {\n existing = (await readJsonArray(filename, info.size)) ?? [];\n }\n }\n\n // Stat again: a file that couldn't be used has been moved aside.\n await replaceFile(filename, await statOrNull(filename), async output => {\n // json.dump(merged, output, ensure_ascii=False, indent=2), one entry at\n // a time: the whole array may not fit in a string.\n let separator = '[\\n';\n if (keptBytes > 0) {\n await copyFileStart(filename, keptBytes, output);\n separator = ',\\n';\n }\n for (const entry of [...existing, ...entries]) {\n // oxlint-disable-next-line no-await-in-loop -- bytes stay in order\n await output.writeFile(`${separator}${encodeJsonEntry(entry)}`, 'utf-8');\n separator = ',\\n';\n }\n await output.writeFile('\\n]', 'utf-8');\n });\n}\n\n/** Tails of the {@link appendJson} calls running, by absolute path. */\nconst jsonAppends = new Map<string, Promise<void>>();\n\n/**\n * Append `reports` to a JSON array on disk, creating the file if needed.\n *\n * Reads the existing array (if the file exists and parses cleanly), merges\n * the new reports onto the end, and replaces the file with a single valid\n * JSON array (indented by 2, keys in snake_case like parsedmarc's output).\n * A file above {@link MAX_REWRITTEN_JSON_BYTES} that already has that layout\n * keeps its bytes and only gets the new entries, which gives the same result.\n * A file that can't be used (corrupt, unreadable, a root that is not an\n * array) is moved to `<filename>.corrupt-<timestamp>` with a warning, never\n * discarded. An empty `reports` leaves the file alone (and creates nothing).\n * Calls for one file run in turn; other processes are not coordinated with.\n *\n * Throws `ValueError` when a single report is too large to serialize.\n */\nexport async function appendJson(\n filename: string,\n reports:\n | readonly AggregateReport[]\n | readonly FailureReport[]\n | readonly SMTPTLSReport[],\n): Promise<void> {\n if (reports.length === 0) {\n // Don't create an empty-array file for an empty input; if a file\n // already exists, leave it alone.\n return;\n }\n\n const key = path.resolve(filename);\n const previous = jsonAppends.get(key) ?? Promise.resolve();\n const current = previous\n .catch(() => undefined)\n .then(() => appendJsonEntries(filename, reports));\n jsonAppends.set(key, current);\n try {\n await current;\n } finally {\n if (jsonAppends.get(key) === current) {\n jsonAppends.delete(key);\n }\n }\n}\n\n/**\n * Append CSV text to a file. When the file already has content, the header\n * line of `csv` is dropped; a header-only `csv` then writes nothing.\n */\nexport async function appendCsv(filename: string, csv: string): Promise<void> {\n // \"a+\" like Python: creates the file, and every write goes to the end.\n const handle = await open(filename, 'a+');\n try {\n let text = csv;\n const { size } = await handle.stat();\n if (size !== 0) {\n // strip the headers from the CSV\n const newline = text.indexOf('\\n');\n if (newline === -1) {\n // Python's `_headers, csv = csv.split(\"\\n\", 1)`\n throw new PyValueError(\n 'not enough values to unpack (expected 2, got 1)',\n );\n }\n text = text.slice(newline + 1);\n if (text.length === 0) {\n // not appending anything, don't do any dance to append it correctly\n return;\n }\n }\n await handle.writeFile(text, 'utf-8');\n } finally {\n await handle.close();\n }\n}\n\n/** Numbered names tried for a sample before a random one is used. */\nconst MAX_SAMPLE_NAME_PROBES = 1000;\n\n/**\n * Write `sample` to a file that doesn't exist yet, named `<subject>.eml`,\n * then `<subject> (1).eml`, … A sender repeats a subject at will, so an\n * existing sample is never reused, and a long run of one subject stops\n * probing. Returns the name used, without the extension.\n */\nasync function writeNewSample(\n directory: string,\n subject: string,\n sample: string,\n): Promise<string> {\n for (let count = 0; ; count += 1) {\n let filename = subject;\n if (count > MAX_SAMPLE_NAME_PROBES) {\n filename = `${subject} (${randomBytes(TEMP_SUFFIX_BYTES).toString('hex')})`;\n } else if (count > 0) {\n filename = `${subject} (${count})`;\n }\n try {\n // oxlint-disable-next-line no-await-in-loop -- each name depends on the last\n await writeFile(path.join(directory, `${filename}.eml`), sample, {\n encoding: 'utf-8',\n flag: 'wx',\n });\n return filename;\n } catch (error) {\n if (errorCode(error) !== 'EEXIST') {\n throw error;\n }\n }\n }\n}\n\n/**\n * Save report data in the given directory.\n *\n * Writes the JSON and CSV files of each report type, then the message\n * sample of each failure report to a `samples` subdirectory, named after the\n * sample's own `subject` header run through {@link getFilenameSafeString},\n * falling back to `sample` when sanitizing leaves nothing. A subject comes\n * from an untrusted sender, so it is sanitized here, at write time; a\n * caller-supplied `filenameSafeSubject` is not trusted and not used. Names\n * that collide, in this call or with a file already there, get a ` (1)`,\n * ` (2)`, … suffix.\n *\n * Throws `ValueError` if `outputDirectory` exists and is not a directory.\n */\nexport async function saveOutput(\n results: ParsingResults,\n options: SaveOutputOptions = {},\n): Promise<void> {\n const {\n aggregateJsonFilename = 'aggregate.json',\n failureJsonFilename = 'failure.json',\n smtpTlsJsonFilename = 'smtp_tls.json',\n aggregateCsvFilename = 'aggregate.csv',\n failureCsvFilename = 'failure.csv',\n smtpTlsCsvFilename = 'smtp_tls.csv',\n } = options;\n const { aggregateReports, failureReports, smtpTlsReports } = results;\n const outputDirectory = expandUser(options.outputDirectory ?? 'output');\n const csvOptions = { formulaEscape: options.csvFormulaEscape !== false };\n\n const outputInfo = await statOrNull(outputDirectory);\n if (outputInfo === null) {\n await mkdir(outputDirectory, { recursive: true });\n } else if (!outputInfo.isDirectory()) {\n throw new ValueError(`${outputDirectory} is not a directory`);\n }\n\n // Flattened before the first write: a report that can't be flattened\n // would otherwise leave its JSON appended, and a copy more on each retry.\n const aggregateCsv = parsedAggregateReportsToCsv(\n aggregateReports,\n csvOptions,\n );\n const failureCsv = parsedFailureReportsToCsv(failureReports, csvOptions);\n const smtpTlsCsv = parsedSmtpTlsReportsToCsv(smtpTlsReports, csvOptions);\n\n await appendJson(\n path.join(outputDirectory, aggregateJsonFilename),\n aggregateReports,\n );\n await appendCsv(\n path.join(outputDirectory, aggregateCsvFilename),\n aggregateCsv,\n );\n await appendJson(\n path.join(outputDirectory, failureJsonFilename),\n failureReports,\n );\n await appendCsv(path.join(outputDirectory, failureCsvFilename), failureCsv);\n await appendJson(\n path.join(outputDirectory, smtpTlsJsonFilename),\n smtpTlsReports,\n );\n await appendCsv(path.join(outputDirectory, smtpTlsCsvFilename), smtpTlsCsv);\n\n const samplesDirectory = path.join(outputDirectory, 'samples');\n if ((await statOrNull(samplesDirectory)) === null) {\n await mkdir(samplesDirectory, { recursive: true });\n }\n\n for (const failureReport of failureReports) {\n const { sample, parsedSample } = failureReport;\n const subject =\n getFilenameSafeString(parsedSample.subject ?? null) || 'sample';\n try {\n await writeNewSample(samplesDirectory, subject, sample);\n } catch (error) {\n if (subject === 'sample') {\n throw error;\n }\n // A name the filesystem refuses (too long, invalid encoding) must not\n // lose the batch's other output: fall back to the generic name.\n const fallback = await writeNewSample(samplesDirectory, 'sample', sample);\n logger.warning(\n `Could not write sample ${sanitizeForLog(JSON.stringify(subject))}.eml ` +\n `(${errorText(error)}); writing it as ${fallback}.eml instead`,\n );\n }\n }\n}\n\ninterface WalkEntry {\n root: string;\n dirs: string[];\n files: string[];\n}\n\n/** Entries of a directory in on-disk order, like `os.scandir()`. */\nasync function scanDirectory(directory: string): Promise<WalkEntry> {\n const dirs: string[] = [];\n const files: string[] = [];\n // opendir() keeps the directory's own order (readdir() sorts names).\n for await (const entry of await opendir(directory)) {\n if (entry.isDirectory()) {\n dirs.push(entry.name);\n } else {\n files.push(entry.name);\n }\n }\n return { root: directory, dirs, files };\n}\n\n/**\n * Python's `os.walk(top)`: top-down, a directory before its children.\n *\n * @yields {WalkEntry} Each directory with its subdirectory and file names.\n */\nasync function* walk(top: string): AsyncGenerator<WalkEntry> {\n let entry: WalkEntry;\n try {\n entry = await scanDirectory(top);\n } catch {\n // os.walk() ignores errors by default.\n return;\n }\n yield entry;\n for (const directory of entry.dirs) {\n yield* walk(path.join(top, directory));\n }\n}\n\n/** The arcname `zipfile.ZipInfo.from_file()` stores (normalized, relative). */\nfunction zipArcname(arcname: string, isDirectory: boolean): string {\n let name = path.normalize(arcname);\n while (name.startsWith('/')) {\n name = name.slice(1);\n }\n return isDirectory ? `${name}/` : name;\n}\n\nconst ZIP_UNIX = 3;\nconst MS_DOS_DIRECTORY = 0x10;\nconst MODE_MASK = 0xff_ff;\nconst ATTRS_SHIFT = 0x1_00_00;\n\n/** Python's `ZipFile.write(filename, arcname)` with `ZIP_DEFLATED`. */\nasync function addToZip(\n entries: [string, Zippable[string]][],\n filePath: string,\n arcname: string,\n): Promise<void> {\n const info = await stat(filePath);\n const isDirectory = info.isDirectory();\n // (st_mode & 0xFFFF) << 16, plus the MS-DOS directory flag for folders.\n let attrs = (info.mode & MODE_MASK) * ATTRS_SHIFT;\n if (isDirectory) {\n attrs += MS_DOS_DIRECTORY;\n }\n const options: ZipOptions = {\n os: ZIP_UNIX,\n attrs,\n mtime: info.mtime,\n // Directories are stored, files deflated at zlib's default level.\n level: isDirectory ? 0 : 6,\n };\n const data = isDirectory ? new Uint8Array(0) : await readFile(filePath);\n entries.push([zipArcname(arcname, isDirectory), [data, options]]);\n}\n\nasync function isFile(filePath: string): Promise<boolean> {\n return (await statOrNull(filePath))?.isFile() ?? false;\n}\n\nasync function isDirectory(filePath: string): Promise<boolean> {\n return (await statOrNull(filePath))?.isDirectory() ?? false;\n}\n\n/**\n * Creates a zip file of parsed report output: {@link saveOutput} into a\n * temporary directory, then every file in it.\n *\n * The layout is parsedmarc's, except that parsedmarc also stores each file\n * of `samples/` a second time as `../<name>` (its `add_subdir()` swaps the\n * `relpath` arguments). Those entries point outside the extraction\n * directory, a path-traversal risk for whoever unzips the attachment, so\n * they are left out; every file is still in the zip once.\n */\nexport async function getReportZip(\n results: ParsingResults,\n options: GetReportZipOptions = {},\n): Promise<Uint8Array> {\n const entries: [string, Zippable[string]][] = [];\n\n const tmpDir = await mkdtemp(path.join(tmpdir(), 'tmp'));\n try {\n await saveOutput(results, {\n outputDirectory: tmpDir,\n csvFormulaEscape: options.csvFormulaEscape !== false,\n });\n for await (const { root, dirs, files } of walk(tmpDir)) {\n for (const file of files) {\n const filePath = path.join(root, file);\n if (await isFile(filePath)) {\n const arcname = path.join(path.relative(tmpDir, root) || '.', file);\n await addToZip(entries, filePath, arcname);\n }\n }\n for (const directory of dirs) {\n const dirPath = path.join(root, directory);\n if (await isDirectory(dirPath)) {\n await addToZip(entries, dirPath, directory);\n }\n }\n }\n } finally {\n await rm(tmpDir, { recursive: true, force: true });\n }\n\n // zipSync keeps insertion order. parsedmarc's layout never repeats a name\n // (zipfile would store a duplicate twice; an object can hold it once).\n return zipSync(Object.fromEntries(entries));\n}\n","// Socket plumbing shared by the syslog and GELF handlers. Python's logging\n// handlers send with blocking calls; here `emit()` stays synchronous and\n// queues the send, keeping Python's one-at-a-time order.\n\nimport type { Socket as DatagramSocket } from 'node:dgram';\nimport { connect as netConnect, type Socket } from 'node:net';\nimport {\n connect as tlsConnect,\n type ConnectionOptions,\n type TLSSocket,\n} from 'node:tls';\n\nconst KEEP_ALIVE_INTERVAL_MS = 2 ** 30;\n\n/** Python's `SocketHandler` reconnect back-off, in seconds. */\nexport const RETRY_START = 1;\nexport const RETRY_MAX = 30;\nexport const RETRY_FACTOR = 2;\n\n/** Python's `TimeoutError: timed out` for socket operations. */\nexport class SocketTimeoutError extends Error {\n override name = 'TimeoutError';\n\n constructor() {\n super('timed out');\n }\n}\n\n/**\n * Runs send tasks one after another. Sockets are unref'd, like Python's,\n * which never hold the interpreter open; while sends are queued a timer\n * keeps the event loop alive so they are not cut short.\n */\nexport class SendQueue {\n #tail: Promise<void> = Promise.resolve();\n #queued = 0;\n #keepAlive: ReturnType<typeof setInterval> | null = null;\n\n /** Queue `task`; its error goes to `onError` (never rejects the queue). */\n push(task: () => Promise<void>, onError: (error: unknown) => void): void {\n this.#queued += 1;\n this.#keepAlive ??= setInterval(() => undefined, KEEP_ALIVE_INTERVAL_MS);\n this.#tail = this.#tail.then(() => this.#run(task, onError));\n }\n\n async #run(\n task: () => Promise<void>,\n onError: (error: unknown) => void,\n ): Promise<void> {\n try {\n await task();\n } catch (error) {\n onError(error);\n } finally {\n this.#queued -= 1;\n if (this.#queued === 0 && this.#keepAlive !== null) {\n clearInterval(this.#keepAlive);\n this.#keepAlive = null;\n }\n }\n }\n\n /** Resolves once every queued send has finished. */\n async drain(): Promise<void> {\n await this.#tail;\n }\n}\n\n/**\n * Messages a stream handler could not deliver. `emit()` can't report them\n * and `SendQueue` runs on, so the client's save method asks here after\n * draining and fails, which keeps the batch from counting as saved.\n */\nexport class SendFailures {\n #count = 0;\n #first: unknown = null;\n\n record(error: unknown): void {\n if (this.#count === 0) {\n this.#first = error;\n }\n this.#count += 1;\n }\n\n /** The failures since the last call, or `null` when there were none. */\n take(): { count: number; error: unknown } | null {\n if (this.#count === 0) {\n return null;\n }\n const failures = { count: this.#count, error: this.#first };\n this.#count = 0;\n this.#first = null;\n return failures;\n }\n}\n\n/** Run `start` and settle with it, or reject after `timeoutMs`. */\nasync function withTimeout<T>(\n timeoutMs: number | null,\n start: (\n resolve: (value: T) => void,\n reject: (error: unknown) => void,\n ) => void,\n onTimeout: () => void = () => undefined,\n): Promise<T> {\n return await new Promise<T>((resolve, reject) => {\n const timer =\n timeoutMs === null\n ? null\n : setTimeout(() => {\n onTimeout();\n reject(new SocketTimeoutError());\n }, timeoutMs);\n const done = (): void => {\n if (timer !== null) {\n clearTimeout(timer);\n }\n };\n start(\n value => {\n done();\n resolve(value);\n },\n error => {\n done();\n reject(error);\n },\n );\n });\n}\n\n/** Connect a TCP socket, failing after `timeoutMs`. */\nexport async function connectTcp(\n options: { host: string; port: number; family?: number },\n timeoutMs: number | null,\n): Promise<Socket> {\n const socket = netConnect(options);\n try {\n await withTimeout<void>(\n timeoutMs,\n (resolve, reject) => {\n socket.once('connect', () => resolve());\n socket.once('error', reject);\n },\n () => socket.destroy(),\n );\n } catch (error) {\n socket.destroy();\n throw error;\n }\n socket.removeAllListeners('error');\n socket.on('error', () => undefined);\n return socket;\n}\n\n/** Start TLS (over `options.socket` or a new connection), with a timeout. */\nexport async function connectTls(\n options: ConnectionOptions,\n timeoutMs: number | null,\n): Promise<TLSSocket> {\n const socket = tlsConnect(options);\n try {\n await withTimeout<void>(\n timeoutMs,\n (resolve, reject) => {\n socket.once('secureConnect', () => resolve());\n socket.once('error', reject);\n },\n () => socket.destroy(),\n );\n } catch (error) {\n socket.destroy();\n throw error;\n }\n socket.removeAllListeners('error');\n socket.on('error', () => undefined);\n return socket;\n}\n\n/** Python's `sock.sendall(data)`, with the socket's timeout. */\nexport async function sendAll(\n socket: Socket,\n data: Uint8Array,\n timeoutMs: number | null,\n): Promise<void> {\n await withTimeout<void>(timeoutMs, (resolve, reject) => {\n if (socket.destroyed || !socket.writable) {\n reject(Object.assign(new Error('Broken pipe'), { code: 'EPIPE' }));\n return;\n }\n socket.write(data, error => {\n if (error) {\n reject(error);\n } else {\n resolve();\n }\n });\n });\n}\n\n/** Python's `sock.sendto(data, (host, port))`. */\nexport async function sendTo(\n socket: DatagramSocket,\n data: Uint8Array,\n port: number,\n host: string,\n): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n socket.send(data, port, host, error => {\n if (error) {\n reject(error);\n } else {\n resolve();\n }\n });\n });\n}\n\n/** Close a stream socket after its pending writes, without waiting forever. */\nexport async function closeStream(\n socket: Socket,\n timeoutMs: number,\n): Promise<void> {\n if (socket.destroyed) {\n return;\n }\n await new Promise<void>(resolve => {\n const timer = setTimeout(() => {\n socket.destroy();\n }, timeoutMs);\n timer.unref();\n socket.once('close', () => {\n clearTimeout(timer);\n resolve();\n });\n // Like Python's close(): flush what was written, then drop the socket\n // without waiting for the peer to close its side.\n socket.end(() => socket.destroy());\n });\n}\n\n/** Close a datagram socket. */\nexport async function closeDatagram(socket: DatagramSocket): Promise<void> {\n await new Promise<void>(resolve => {\n try {\n socket.close(() => resolve());\n } catch {\n // Already closed.\n resolve();\n }\n });\n}\n","// Port of parsedmarc/gelf.py and the pygelf handlers it uses\n// (GelfUdpHandler, GelfTcpHandler, GelfTlsHandler, with\n// `include_extra_fields=True`).\n\nimport { randomBytes } from 'node:crypto';\nimport { createSocket, type Socket as DatagramSocket } from 'node:dgram';\nimport { readFileSync } from 'node:fs';\nimport { isIP, type Socket } from 'node:net';\nimport { hostname } from 'node:os';\nimport { type ConnectionOptions, getCACertificates } from 'node:tls';\nimport { deflateSync } from 'node:zlib';\n\nimport { parsedAggregateReportsToCsvRows } from '../aggregate';\nimport { parsedFailureReportsToCsvRows } from '../failure';\nimport {\n errorText,\n KeyError,\n PyAttributeError,\n PyValueError,\n} from '../internal/py';\nimport { pyJsonDumps } from '../internal/py-json';\nimport {\n CRITICAL,\n DEBUG,\n ERROR,\n Handler,\n INFO,\n Logger,\n type LogRecord,\n WARNING,\n} from '../log';\nimport { parsedSmtpTlsReportsToCsvRows } from '../smtp-tls';\nimport type { AggregateReport, FailureReport, SMTPTLSReport } from '../types';\nimport { PyDateTime } from '../utils/datetime';\nimport {\n closeDatagram,\n closeStream,\n connectTcp,\n connectTls,\n RETRY_FACTOR,\n RETRY_MAX,\n RETRY_START,\n SendFailures,\n SendQueue,\n sendAll,\n sendTo,\n} from './socket-io';\n\nconst MS_PER_SECOND = 1000;\n/** How long `close()` waits for queued bytes to flush. */\nconst CLOSE_TIMEOUT_MS = 5000;\n\n/** pygelf's `LEVELS`: logging level → syslog severity. */\nconst LEVELS: ReadonlyMap<number, number> = new Map([\n [DEBUG, 7],\n [INFO, 6],\n [WARNING, 4],\n [ERROR, 3],\n [CRITICAL, 2],\n]);\n\n/**\n * pygelf's `SKIP_LIST`: LogRecord attributes never sent as extra fields\n * (plus `id`, which GELF forbids).\n */\nconst SKIP_LIST: ReadonlySet<string> = new Set([\n 'args',\n 'asctime',\n 'created',\n 'exc_info',\n 'exc_text',\n 'filename',\n 'funcName',\n 'id',\n 'levelname',\n 'levelno',\n 'lineno',\n 'module',\n 'msecs',\n 'message',\n 'msg',\n 'name',\n 'pathname',\n 'process',\n 'processName',\n 'relativeCreated',\n 'thread',\n 'threadName',\n]);\n\n/**\n * LogRecord attributes of Python 3.12+ that pygelf's `SKIP_LIST` misses, so\n * they always reach the packet as `_stack_info` / `_taskName` (both `None`\n * for parsedmarc's records).\n */\nconst UNSKIPPED_RECORD_ATTRIBUTES = ['stack_info', 'taskName'] as const;\n\n/** pygelf's `object_to_json`: datetimes as ISO 8601, anything else `str()`. */\nfunction objectToJson(value: unknown): unknown {\n if (value instanceof PyDateTime) {\n return value.isoformat();\n }\n if (value instanceof Date) {\n return value.toISOString();\n }\n return String(value);\n}\n\nfunction tracebackText(error: unknown): string {\n return error instanceof Error\n ? (error.stack ?? errorText(error))\n : String(error);\n}\n\n/** pygelf's `gelf.make()` with `debug=False` and no static fields. */\nexport function makeGelf(\n record: LogRecord,\n domain: string,\n version: string,\n includeExtraFields: boolean,\n): Record<string, unknown> {\n const level = LEVELS.get(record.level);\n if (level === undefined) {\n throw new KeyError(record.level);\n }\n const gelf: Record<string, unknown> = {\n version,\n short_message: record.message,\n timestamp: record.created / MS_PER_SECOND,\n level,\n host: domain,\n };\n if (record.error !== undefined) {\n gelf.full_message = tracebackText(record.error);\n }\n if (includeExtraFields) {\n for (const key of UNSKIPPED_RECORD_ATTRIBUTES) {\n gelf[`_${key}`] = null;\n }\n for (const [key, value] of Object.entries(record.extra)) {\n if (!(SKIP_LIST.has(key) || key.startsWith('_'))) {\n gelf[`_${key}`] = value;\n }\n }\n }\n return gelf;\n}\n\n/** pygelf's `gelf.pack()`: compact JSON, UTF-8, zlib when compressing. */\nexport function packGelf(\n gelf: Record<string, unknown>,\n compress: boolean,\n): Uint8Array {\n const packed = Buffer.from(\n pyJsonDumps(gelf, { separators: [',', ':'], default: objectToJson }),\n 'utf-8',\n );\n return compress ? deflateSync(packed) : packed;\n}\n\nconst CHUNK_MAGIC = [0x1e, 0x0f] as const;\nconst MESSAGE_ID_LENGTH = 8;\nconst MAX_CHUNKS = 127;\n\n/**\n * pygelf's `gelf.split()`: GELF chunks of `chunkSize` bytes, each with the\n * magic bytes, a random 8-byte message id, the sequence number and count.\n */\nexport function splitGelf(gelf: Uint8Array, chunkSize: number): Uint8Array[] {\n const messageId = randomBytes(MESSAGE_ID_LENGTH);\n const count = Math.ceil(gelf.length / chunkSize);\n if (count > MAX_CHUNKS) {\n // struct.pack('b', number_of_chunks) fails before the first chunk.\n throw new Error(`'b' format requires -128 <= number <= ${MAX_CHUNKS}`);\n }\n const chunks: Uint8Array[] = [];\n for (let index = 0; index < count; index += 1) {\n const body = gelf.subarray(index * chunkSize, (index + 1) * chunkSize);\n chunks.push(\n Buffer.concat([\n Buffer.from(CHUNK_MAGIC),\n messageId,\n Buffer.from([index, count]),\n body,\n ]),\n );\n }\n return chunks;\n}\n\nexport interface GelfHandlerOptions {\n /** GELF spec version (default `1.1`). */\n version?: string;\n /** Send the record's extra fields as `_<name>` (default false). */\n includeExtraFields?: boolean;\n /** zlib-compress each message (default: true for UDP, false otherwise). */\n compress?: boolean;\n}\n\n/** pygelf's `BaseHandler`: turns records into GELF. */\nabstract class BaseGelfHandler extends Handler {\n readonly host: string;\n readonly port: number;\n readonly version: string;\n readonly includeExtraFields: boolean;\n readonly compress: boolean;\n /** The `host` field: this machine's hostname. */\n readonly domain: string = hostname();\n protected readonly queue: SendQueue = new SendQueue();\n protected readonly failures: SendFailures = new SendFailures();\n\n constructor(\n host: string,\n port: number,\n options: GelfHandlerOptions,\n compressDefault: boolean,\n ) {\n super();\n this.host = host;\n this.port = port;\n this.version = options.version ?? '1.1';\n this.includeExtraFields = options.includeExtraFields ?? false;\n this.compress = options.compress ?? compressDefault;\n }\n\n convertRecordToGelf(record: LogRecord): Uint8Array {\n return packGelf(\n makeGelf(record, this.domain, this.version, this.includeExtraFields),\n this.compress,\n );\n }\n\n /** The bytes sent for one record. */\n makePickle(record: LogRecord): Uint8Array {\n return this.convertRecordToGelf(record);\n }\n\n protected abstract send(data: Uint8Array): Promise<void>;\n\n emit(record: LogRecord): void {\n const data = this.makePickle(record);\n this.queue.push(\n () => this.send(data),\n error => this.handleError(record, error),\n );\n }\n\n /** Resolves once every record emitted so far has been sent (or dropped). */\n async drain(): Promise<void> {\n await this.queue.drain();\n }\n\n /** The messages dropped since the last call (always none over UDP). */\n takeFailures(): { count: number; error: unknown } | null {\n return this.failures.take();\n }\n\n override close(): void {\n void this.closeAsync();\n }\n\n abstract closeAsync(): Promise<void>;\n}\n\n/** `SocketHandler.makeSocket()`'s timeout, in seconds. */\nconst SOCKET_TIMEOUT = 1;\n\n/**\n * pygelf's `GelfTcpHandler` over Python's `SocketHandler`: connects on the\n * first record, sends each message NUL-terminated, and on a connection or\n * send error closes the socket and retries later with exponential back-off\n * (1 s doubling up to 30 s). Unlike Python, a message that fails on a stale\n * connection is sent again once over a new one, and one that is still lost\n * is recorded in `failures` (`SocketHandler` drops it without a trace).\n */\nexport class GelfTcpHandler extends BaseGelfHandler {\n sock: Socket | null = null;\n /** Epoch ms before which no reconnect is attempted. */\n retryTime: number | null = null;\n retryPeriod: number = RETRY_START;\n #connectError: unknown = null;\n\n constructor(host: string, port: number, options: GelfHandlerOptions = {}) {\n super(host, port, options, false);\n }\n\n /** NUL-terminated: \"if you send the message over tcp, it should always be null terminated\". */\n override makePickle(record: LogRecord): Uint8Array {\n return Buffer.concat([this.convertRecordToGelf(record), Buffer.from([0])]);\n }\n\n /** Open the connection (`SocketHandler.makeSocket`). */\n protected async makeSocket(\n timeout: number = SOCKET_TIMEOUT,\n ): Promise<Socket> {\n return await connectTcp(\n { host: this.host, port: this.port },\n timeout * MS_PER_SECOND,\n );\n }\n\n /** `SocketHandler.createSocket()`: connect unless backing off. */\n async createSocket(): Promise<void> {\n const now = Date.now();\n if (this.retryTime !== null && now < this.retryTime) {\n return;\n }\n try {\n this.sock = await this.makeSocket();\n this.sock.unref();\n this.retryTime = null;\n } catch (error) {\n this.#connectError = error;\n if (this.retryTime === null) {\n this.retryPeriod = RETRY_START;\n } else {\n this.retryPeriod = Math.min(this.retryPeriod * RETRY_FACTOR, RETRY_MAX);\n }\n this.retryTime = now + this.retryPeriod * MS_PER_SECOND;\n }\n }\n\n /** Send over the open connection, or a new one; throws if that fails. */\n async #sendOnce(data: Uint8Array): Promise<void> {\n if (this.sock === null) {\n await this.createSocket();\n }\n const { sock } = this;\n if (sock === null) {\n throw this.#connectError;\n }\n try {\n await sendAll(sock, data, SOCKET_TIMEOUT * MS_PER_SECOND);\n } catch (error) {\n // OSError: close, so the next attempt reconnects.\n sock.destroy();\n this.sock = null;\n throw error;\n }\n }\n\n protected async send(data: Uint8Array): Promise<void> {\n try {\n await this.#sendOnce(data);\n } catch {\n // A server restart only shows on the next send, over the dead socket.\n try {\n await this.#sendOnce(data);\n } catch (error) {\n this.failures.record(error);\n }\n }\n }\n\n async closeAsync(): Promise<void> {\n await this.drain();\n const { sock } = this;\n this.sock = null;\n if (sock !== null) {\n await closeStream(sock, CLOSE_TIMEOUT_MS);\n }\n }\n}\n\nexport interface GelfUdpHandlerOptions extends GelfHandlerOptions {\n /** Maximum datagram size before chunking (default 1300). */\n chunkSize?: number;\n}\n\n/**\n * pygelf's `GelfUdpHandler`: zlib-compressed datagrams (IPv4, like Python's\n * `DatagramHandler`), split into GELF chunks above `chunkSize` bytes.\n */\nexport class GelfUdpHandler extends BaseGelfHandler {\n readonly chunkSize: number;\n sock: DatagramSocket | null = null;\n\n constructor(host: string, port: number, options: GelfUdpHandlerOptions = {}) {\n super(host, port, options, true);\n this.chunkSize = options.chunkSize ?? 1300;\n }\n\n protected async send(data: Uint8Array): Promise<void> {\n if (this.sock === null) {\n const sock = createSocket('udp4');\n sock.on('error', () => undefined);\n sock.unref();\n this.sock = sock;\n }\n const chunks =\n data.length <= this.chunkSize ? [data] : splitGelf(data, this.chunkSize);\n for (const chunk of chunks) {\n await sendTo(this.sock, chunk, this.port, this.host);\n }\n }\n\n async closeAsync(): Promise<void> {\n await this.drain();\n const { sock } = this;\n this.sock = null;\n if (sock !== null) {\n await closeDatagram(sock);\n }\n }\n}\n\nexport interface GelfTlsOptions {\n /**\n * Verify the server certificate and hostname (default true). pygelf\n * defaults to false; see the deviations in the architecture notes.\n */\n validate?: boolean;\n /** CA bundle file, added to the system CAs. */\n caCerts?: string | null;\n /** Client certificate file (may also hold the key). */\n certfile?: string | null;\n /** Client private key file. */\n keyfile?: string | null;\n}\n\n/** pygelf's `GelfTlsHandler`: {@link GelfTcpHandler} over TLS (IPv4). */\nexport class GelfTlsHandler extends GelfTcpHandler {\n readonly tlsOptions: ConnectionOptions;\n\n constructor(\n host: string,\n port: number,\n options: GelfHandlerOptions & GelfTlsOptions = {},\n ) {\n const { validate = true, caCerts, certfile, keyfile } = options;\n if (keyfile && !certfile) {\n throw new PyValueError('certfile must be specified');\n }\n super(host, port, options);\n const tlsOptions: ConnectionOptions = { rejectUnauthorized: validate };\n if (caCerts) {\n tlsOptions.ca = [\n ...getCACertificates('default'),\n readFileSync(caCerts, 'utf-8'),\n ];\n }\n if (certfile) {\n tlsOptions.cert = readFileSync(certfile);\n tlsOptions.key = readFileSync(keyfile || certfile);\n }\n this.tlsOptions = tlsOptions;\n }\n\n protected override async makeSocket(\n timeout: number = SOCKET_TIMEOUT,\n ): Promise<Socket> {\n const timeoutMs = timeout * MS_PER_SECOND;\n // Like pygelf: an IPv4 socket, wrapped, then connected.\n const plain = await connectTcp(\n { host: this.host, port: this.port, family: 4 },\n timeoutMs,\n );\n // The handshake's timeout timer keeps the event loop alive meanwhile.\n plain.unref();\n try {\n return await connectTls(\n {\n ...this.tlsOptions,\n socket: plain,\n host: this.host,\n // Python sends no SNI for IP addresses; Node refuses one.\n ...(isIP(this.host) === 0 && { servername: this.host }),\n },\n timeoutMs,\n );\n } catch (error) {\n plain.destroy();\n throw error;\n }\n }\n}\n\n/** parsedmarc's thread-local `log_context_data`: the row being logged. */\nexport const logContextData: { parsedmarc?: unknown } = {};\n\n/** Copies `logContextData.parsedmarc` onto each record as `parsedmarc`. */\nexport class ContextFilter {\n readonly filter = (record: LogRecord): boolean => {\n if (!('parsedmarc' in logContextData)) {\n // Python reads an attribute the thread-local doesn't have yet.\n throw new PyAttributeError(\n \"'_thread._local' object has no attribute 'parsedmarc'\",\n );\n }\n record.extra.parsedmarc = logContextData.parsedmarc;\n return true;\n };\n}\n\n/** Raised when messages could not be delivered over TCP or TLS. */\nexport class GelfError extends Error {\n override name = 'GelfError';\n}\n\nexport type GelfMode = 'udp' | 'tcp' | 'tls';\n\nexport interface GelfClientOptions {\n /** TLS settings for `tls` mode. */\n tls?: GelfTlsOptions;\n}\n\ntype GelfHandler = GelfUdpHandler | GelfTcpHandler | GelfTlsHandler;\n\n/** A client for the Graylog Extended Log Format. */\nexport class GelfClient {\n readonly host: string;\n readonly port: number;\n readonly logger: Logger;\n readonly gelfMode: Readonly<\n Record<GelfMode, (host: string, port: number) => GelfHandler>\n >;\n /** The GELF handler attached to {@link logger}. */\n handler: Handler;\n\n /**\n * @param host - The GELF host\n * @param port - The GELF port\n * @param mode - The GELF transport mode: `udp`, `tcp` or `tls`\n */\n constructor(\n host: string,\n port: number,\n mode: string,\n options: GelfClientOptions = {},\n ) {\n this.host = host;\n this.port = port;\n // Its own logger, not parsedmarc's shared `getLogger()` one: a logger\n // sends to all its handlers, so clients sharing it would each send their\n // rows to every other client's server.\n this.logger = new Logger('tsdmarc_gelf');\n this.logger.setLevel(INFO);\n this.logger.addFilter(new ContextFilter().filter);\n this.gelfMode = {\n udp: (h, p) => new GelfUdpHandler(h, p, { includeExtraFields: true }),\n tcp: (h, p) => new GelfTcpHandler(h, p, { includeExtraFields: true }),\n tls: (h, p) =>\n new GelfTlsHandler(h, p, { ...options.tls, includeExtraFields: true }),\n };\n if (!Object.hasOwn(this.gelfMode, mode)) {\n throw new KeyError(mode);\n }\n this.handler = this.gelfMode[mode as GelfMode](this.host, this.port);\n this.logger.addHandler(this.handler);\n }\n\n async saveAggregateReportToGelf(\n aggregateReports: AggregateReport | AggregateReport[],\n ): Promise<void> {\n const rows = parsedAggregateReportsToCsvRows(aggregateReports);\n for (const row of rows) {\n logContextData.parsedmarc = row;\n this.logger.info('parsedmarc aggregate report');\n }\n logContextData.parsedmarc = null;\n await this.#drain();\n }\n\n async saveFailureReportToGelf(\n failureReports: FailureReport | FailureReport[],\n ): Promise<void> {\n const rows = parsedFailureReportsToCsvRows(failureReports);\n for (const row of rows) {\n logContextData.parsedmarc = row;\n this.logger.info('parsedmarc failure report');\n }\n await this.#drain();\n }\n\n /** Backward-compatible alias of {@link saveFailureReportToGelf}. */\n async saveForensicReportToGelf(\n failureReports: FailureReport | FailureReport[],\n ): Promise<void> {\n await this.saveFailureReportToGelf(failureReports);\n }\n\n async saveSmtpTlsReportToGelf(\n smtpTlsReports: SMTPTLSReport | SMTPTLSReport[],\n ): Promise<void> {\n const rows = parsedSmtpTlsReportsToCsvRows(smtpTlsReports);\n for (const row of rows) {\n logContextData.parsedmarc = row;\n this.logger.info('parsedmarc smtptls report');\n }\n await this.#drain();\n }\n\n /**\n * Wait until the messages logged so far have been sent.\n *\n * @throws {GelfError} When some could not be delivered over TCP or TLS.\n */\n async #drain(): Promise<void> {\n if (this.handler instanceof BaseGelfHandler) {\n await this.handler.drain();\n const failures = this.handler.takeFailures();\n if (failures !== null) {\n throw new GelfError(\n `${failures.count} message(s) not sent to ${this.host}:${this.port}: ${errorText(failures.error)}`,\n { cause: failures.error },\n );\n }\n }\n }\n\n /** Remove and close the GELF handler, releasing its connection. */\n async close(): Promise<void> {\n this.logger.removeHandler(this.handler);\n if (this.handler instanceof BaseGelfHandler) {\n await this.handler.closeAsync();\n } else {\n this.handler.close();\n }\n }\n}\n","// Minimal HTTP POST shared by the Splunk HEC and webhook outputs.\n//\n// parsedmarc uses an httpx.Client with `follow_redirects=True`. Here,\n// requests go through the global `fetch` (Node, Bun and Deno all have one),\n// which follows redirects too. `fetch` has no portable switch to skip TLS\n// certificate verification, so when the caller turns verification off the\n// request goes through `node:https` with `rejectUnauthorized: false`\n// instead, following redirects the same way httpx does.\n//\n// Response bodies are read only on demand and capped at\n// MAX_RESPONSE_BYTES on both paths (httpx reads any size).\n//\n// Like httpx, a URL's `user:password@` becomes Basic auth, which `fetch`\n// refuses to do. URLs often are the secret (webhook tokens in the path), and\n// runtimes quote them in their errors, so what `httpPost()` throws never\n// carries more of a URL than its scheme and host. Header values `fetch` would\n// refuse are refused first, with an error that doesn't quote them (they hold\n// tokens, such as Splunk's HEC token).\n\nimport { request as httpRequest, type IncomingMessage } from 'node:http';\nimport { request as httpsRequest } from 'node:https';\n\nimport { InvalidURL } from '../utils/http';\n\n/**\n * The largest response body read (Splunk's HEC answers with a few bytes of\n * JSON). httpx reads any size; a hostile or broken server could otherwise\n * make the client buffer without limit.\n */\nexport const MAX_RESPONSE_BYTES: number = 1024 * 1024;\n\n/** httpx's default `max_redirects`. */\nconst MAX_REDIRECTS = 20;\nconst MS_PER_SECOND = 1000;\nconst URL_SCHEME = /^[a-z][a-z0-9+.-]*:/iu;\nconst REDIRECT_STATUSES: ReadonlySet<number> = new Set([\n 301, 302, 303, 307, 308,\n]);\n/** `scheme://`, then `user:password@`, the host, and the rest of the URL. */\n/** What `fetch` refuses in a header value (it quotes the value in its error). */\nconst INVALID_HEADER_VALUE = /[\\0\\n\\r\\u0100-\\u{10ffff}]/u;\nconst URL_IN_TEXT =\n /\\b([a-z][a-z0-9+.-]*:\\/\\/)(?:[^\\s/@\"'<>)]*@)?([^\\s/?#\"'<>)]*)[^\\s\"'<>)]*/giu;\n\nexport interface HttpPostOptions {\n headers: Record<string, string>;\n body: string | Uint8Array | URLSearchParams;\n /** Seconds before the whole request is aborted; `null` waits forever. */\n timeout: number | null;\n /** Verify TLS certificates (default `true`). */\n verify?: boolean;\n /**\n * Follow redirects like httpx's `follow_redirects=True` (default `true`).\n * With `false`, a 3xx response is returned as it is.\n */\n followRedirects?: boolean;\n /** Aborts the request (e.g. when the client is closed). */\n signal?: AbortSignal;\n}\n\nexport interface HttpResponse {\n status: number;\n /**\n * The body as UTF-8 text. Throws when it is larger than\n * {@link MAX_RESPONSE_BYTES} (the rest is not downloaded).\n */\n text: () => Promise<string>;\n /** Drop the body unread, releasing the connection. Never throws. */\n discard: () => Promise<void>;\n}\n\nfunction bodyTooLarge(): Error {\n return new Error(`Response body exceeds ${MAX_RESPONSE_BYTES} bytes`);\n}\n\n/** Read a fetch body up to {@link MAX_RESPONSE_BYTES}. */\nasync function readFetchBody(response: Response): Promise<string> {\n const declared = Number(response.headers.get('content-length'));\n if (Number.isFinite(declared) && declared > MAX_RESPONSE_BYTES) {\n await response.body?.cancel();\n throw bodyTooLarge();\n }\n if (response.body === null) {\n return '';\n }\n // An explicit reader: cancelling the stream itself while an async\n // iterator holds its lock throws \"ReadableStream is locked\".\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let total = 0;\n for (;;) {\n // oxlint-disable-next-line no-await-in-loop -- chunks arrive in order\n const { done, value } = await reader.read();\n if (done) {\n break;\n }\n total += value.length;\n if (total > MAX_RESPONSE_BYTES) {\n // oxlint-disable-next-line no-await-in-loop -- leaves the loop\n await reader.cancel();\n throw bodyTooLarge();\n }\n chunks.push(value);\n }\n return Buffer.concat(chunks, total).toString('utf8');\n}\n\nfunction fetchResponse(response: Response): HttpResponse {\n return {\n status: response.status,\n text: () => readFetchBody(response),\n discard: async () => {\n try {\n await response.body?.cancel();\n } catch {\n // Already consumed or errored: nothing left to release.\n }\n },\n };\n}\n\nfunction requestSignal(options: HttpPostOptions): AbortSignal | undefined {\n const signals: AbortSignal[] = [];\n if (options.timeout !== null) {\n signals.push(AbortSignal.timeout(options.timeout * MS_PER_SECOND));\n }\n if (options.signal) {\n signals.push(options.signal);\n }\n if (signals.length === 0) {\n return undefined;\n }\n return signals.length === 1 ? signals[0] : AbortSignal.any(signals);\n}\n\n/**\n * The URL to request, like httpx refusing what it can't send (no URL\n * quoted). An unparsable URL with a scheme throws {@link InvalidURL}: a\n * password with an unencoded `/`, `?` or `#` ends the authority early, and\n * httpx's `InvalidURL` then quotes part of it as the port.\n */\nfunction parseRequestUrl(url: string): URL {\n if (!URL.canParse(url)) {\n if (URL_SCHEME.test(url)) {\n throw new InvalidURL('Invalid URL');\n }\n throw new Error(\n \"Request URL is missing an 'http://' or 'https://' protocol.\",\n );\n }\n const target = new URL(url);\n if (target.protocol !== 'http:' && target.protocol !== 'https:') {\n throw new Error(\n `Request URL has an unsupported protocol '${target.protocol}//'.`,\n );\n }\n return target;\n}\n\n/** httpx's `unquote()`: invalid percent-encoding stays as it is. */\nfunction unquote(text: string): string {\n try {\n return decodeURIComponent(text);\n } catch {\n return text;\n }\n}\n\n/** Basic auth from a URL's userinfo; it replaces any `Authorization`, like httpx. */\nfunction withUrlCredentials(\n target: URL,\n headers: Record<string, string>,\n): Record<string, string> {\n const credentials = `${unquote(target.username)}:${unquote(target.password)}`;\n return {\n ...withoutHeader(headers, ['Authorization']),\n Authorization: `Basic ${Buffer.from(credentials).toString('base64')}`,\n };\n}\n\n/** `text` with each URL cut down to its scheme and host. */\nfunction redactUrls(text: string): string {\n return text.replaceAll(URL_IN_TEXT, '$1$2');\n}\n\n/** `error`, or a copy of it (and its causes) without the URLs it quotes. */\nfunction withoutUrlSecrets(error: unknown): unknown {\n if (!(error instanceof Error)) {\n return error;\n }\n const { cause } = error;\n const safeCause = withoutUrlSecrets(cause);\n const message = redactUrls(error.message);\n if (message === error.message && safeCause === cause) {\n return error;\n }\n const safe = new Error(message, { cause: safeCause });\n safe.name = error.name;\n return safe;\n}\n\n/** POST `body` to `url`, following redirects unless `followRedirects` is `false`. */\nexport async function httpPost(\n url: string,\n options: HttpPostOptions,\n): Promise<HttpResponse> {\n const target = parseRequestUrl(url);\n let request = options;\n let requestUrl = url;\n if (target.username || target.password) {\n request = {\n ...options,\n headers: withUrlCredentials(target, options.headers),\n };\n target.username = '';\n target.password = '';\n requestUrl = target.href;\n }\n for (const [name, value] of Object.entries(request.headers)) {\n if (INVALID_HEADER_VALUE.test(value)) {\n throw new Error(`Invalid value for the ${name} header`);\n }\n }\n const signal = requestSignal(request);\n try {\n if (request.verify === false && target.protocol === 'https:') {\n return await insecurePost(requestUrl, request, signal);\n }\n const response = await fetch(requestUrl, {\n method: 'POST',\n headers: request.headers,\n body: request.body,\n redirect: request.followRedirects === false ? 'manual' : 'follow',\n signal,\n });\n return fetchResponse(response);\n } catch (error) {\n throw withoutUrlSecrets(error);\n }\n}\n\n/** Read a node:http body up to {@link MAX_RESPONSE_BYTES}. */\nfunction readNodeBody(response: IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const declared = Number(response.headers['content-length']);\n if (Number.isFinite(declared) && declared > MAX_RESPONSE_BYTES) {\n response.destroy();\n reject(bodyTooLarge());\n return;\n }\n const chunks: Buffer[] = [];\n let total = 0;\n response.on('data', (chunk: Buffer) => {\n total += chunk.length;\n if (total > MAX_RESPONSE_BYTES) {\n response.destroy();\n reject(bodyTooLarge());\n return;\n }\n chunks.push(chunk);\n });\n response.on('end', () =>\n resolve(Buffer.concat(chunks, total).toString('utf8')),\n );\n response.on('error', reject);\n });\n}\n\nfunction nodeRequest(\n url: URL,\n method: string,\n headers: Record<string, string>,\n body: Uint8Array | undefined,\n signal: AbortSignal | undefined,\n): Promise<IncomingMessage> {\n return new Promise((resolve, reject) => {\n const send = url.protocol === 'https:' ? httpsRequest : httpRequest;\n const outgoing = send(\n url,\n { method, headers, rejectUnauthorized: false, signal },\n resolve,\n );\n outgoing.on('error', reject);\n outgoing.end(body);\n });\n}\n\nfunction encodeBody(body: HttpPostOptions['body']): Uint8Array {\n if (body instanceof Uint8Array) {\n return body;\n }\n return new TextEncoder().encode(body.toString());\n}\n\nfunction withoutHeader(\n headers: Record<string, string>,\n names: readonly string[],\n): Record<string, string> {\n const lowered = new Set(names.map(name => name.toLowerCase()));\n return Object.fromEntries(\n Object.entries(headers).filter(([key]) => !lowered.has(key.toLowerCase())),\n );\n}\n\nasync function insecurePost(\n url: string,\n options: HttpPostOptions,\n signal: AbortSignal | undefined,\n): Promise<HttpResponse> {\n let target = new URL(url);\n let method = 'POST';\n let body: Uint8Array | undefined = encodeBody(options.body);\n let headers = { ...options.headers };\n if (options.body instanceof URLSearchParams && !hasHeader(headers)) {\n headers['Content-Type'] = 'application/x-www-form-urlencoded';\n }\n for (let redirects = 0; ; redirects += 1) {\n // oxlint-disable-next-line no-await-in-loop -- redirects are sequential\n const response = await nodeRequest(target, method, headers, body, signal);\n const status = response.statusCode ?? 0;\n const { location } = response.headers;\n if (\n options.followRedirects === false ||\n !REDIRECT_STATUSES.has(status) ||\n location === undefined\n ) {\n return {\n status,\n text: () => readNodeBody(response),\n discard: () => {\n response.destroy();\n return Promise.resolve();\n },\n };\n }\n // A redirect's body is never read.\n response.destroy();\n if (redirects >= MAX_REDIRECTS) {\n throw new Error('Exceeded maximum allowed redirects.');\n }\n const next = new URL(location, target);\n // Same rules as httpx: 303 (and 301/302 after a POST) switch to a\n // body-less GET; credentials never follow a redirect to another origin.\n if (status !== 307 && status !== 308) {\n method = 'GET';\n body = undefined;\n headers = withoutHeader(headers, ['Content-Type', 'Content-Length']);\n }\n if (next.origin !== target.origin) {\n headers = withoutHeader(headers, ['Authorization']);\n }\n target = next;\n }\n}\n\nfunction hasHeader(headers: Record<string, string>): boolean {\n return Object.keys(headers).some(key => key.toLowerCase() === 'content-type');\n}\n\n/** The message of an error, with the low-level cause `fetch` wraps. */\nexport function describeError(error: unknown): string {\n if (!(error instanceof Error)) {\n return String(error);\n }\n const { cause } = error;\n if (\n cause instanceof Error &&\n cause.message &&\n cause.message !== error.message\n ) {\n return `${error.message}: ${cause.message}`;\n }\n return error.message;\n}\n","// Port of parsedmarc/splunk.py: a client for a Splunk HTTP Event Collector.\n//\n// http://docs.splunk.com/Documentation/Splunk/latest/Data/AboutHEC\n// http://docs.splunk.com/Documentation/Splunk/latest/RESTREF/RESTinput#services.2Fcollector\n\nimport { hostname } from 'node:os';\n\nimport { USER_AGENT } from '../constants';\nimport { pyJsonDumps } from '../internal/py-json';\nimport { logger } from '../log';\nimport { toSnakeCaseKeys } from '../serialize';\nimport type { AggregateReport, FailureReport, SMTPTLSReport } from '../types';\nimport { InvalidURL } from '../utils/http';\nimport { sanitizeForLog } from '../utils/sanitize';\nimport { humanTimestampToUnixTimestamp } from '../utils/timestamps';\nimport { describeError, httpPost } from './http';\n\nconst DEFAULT_TIMEOUT = 60;\nconst CLOSED_MESSAGE = 'Cannot send a request, as the client has been closed.';\n// Python's `str.lstrip(\"Splunk \")` strips any of these characters, not the\n// prefix (see `lstripChars`).\nconst TOKEN_PREFIX_CHARS = 'Splunk ';\n// `urlparse()`: an optional scheme, then `//netloc` up to the path.\nconst URL_SCHEME_AND_NETLOC = /^([A-Za-z][A-Za-z0-9+.-]*):(?:\\/\\/([^/?#]*))?/u;\n// httpx's InvalidURL quotes part of the URL, which can be part of a\n// password (one containing \"/\", \"?\" or \"#\" ends up as the \"port\"), so its\n// text is replaced by this fixed message.\nconst INVALID_URL_MESSAGE =\n 'Invalid Splunk HEC URL (details withheld because they may include credentials)';\n/** Characters of a sender's date quoted in a warning. */\nconst MAX_LOGGED_DATE = 100;\n\n/** Raised when a Splunk API error occurs. */\nexport class SplunkError extends Error {\n override name = 'SplunkError';\n}\n\nexport interface HECClientOptions {\n /** The source name (default `\"tsdmarc\"`). */\n source?: string;\n /** Verify TLS certificates (default `true`). */\n verify?: boolean;\n /** Seconds to wait for the server before giving up (default 60). */\n timeout?: number;\n}\n\ntype JsonObject = Record<string, unknown>;\n\n// Deviation: `json.dumps()` writes a reporter's `NaN` or `1e999` bare, which\n// is not JSON, and HEC answers for the whole request: one report would fail\n// every other event of the POST.\nconst HEC_JSON = { nonFiniteAsNull: true } as const;\n\n/** Python's `str.lstrip(chars)`: drop leading characters found in `chars`. */\nfunction lstripChars(value: string, chars: string): string {\n let start = 0;\n while (start < value.length && chars.includes(value.charAt(start))) {\n start += 1;\n }\n return value.slice(start);\n}\n\n/** `{scheme}://{netloc}` as Python's `urlparse()` splits them. */\nfunction schemeAndNetloc(url: string): string {\n const match = URL_SCHEME_AND_NETLOC.exec(url);\n const scheme = match?.[1]?.toLowerCase() ?? '';\n const netloc = match?.[2] ?? '';\n return `${scheme}://${netloc}`;\n}\n\n/**\n * The event time of an SMTP TLS report, or `null` when its `start-datetime`\n * is no date: the parser keeps that value as the sender wrote it (any JSON\n * type), and one such report must not cost the batch its Splunk events.\n */\nfunction smtpTlsEventTime(report: SMTPTLSReport): number | null {\n try {\n return humanTimestampToUnixTimestamp(report.beginDate);\n } catch (error) {\n if (!(error instanceof Error)) {\n throw error;\n }\n const value = sanitizeForLog(\n String(report.beginDate).slice(0, MAX_LOGGED_DATE),\n );\n logger.warning(\n `Sending SMTP TLS report to Splunk without an event time: invalid begin_date ${value}`,\n );\n return null;\n }\n}\n\nfunction snake(value: unknown): JsonObject {\n return toSnakeCaseKeys(value) as JsonObject;\n}\n\n/** A client for a Splunk HTTP Events Collector (HEC). */\nexport class HECClient {\n readonly url: string;\n readonly accessToken: string;\n readonly index: string;\n readonly host: string;\n readonly source: string;\n readonly timeout: number;\n readonly verify: boolean;\n /** Headers sent with every request. */\n readonly headers: Readonly<Record<string, string>>;\n readonly #commonData: JsonObject;\n readonly #closed = new AbortController();\n\n /**\n * @param url - The URL of the HEC; only its scheme and host are used\n * @param accessToken - The HEC access token\n * @param index - The name of the index\n */\n constructor(\n url: string,\n accessToken: string,\n index: string,\n options: HECClientOptions = {},\n ) {\n this.url = `${schemeAndNetloc(url)}/services/collector/event/1.0`;\n // Same as parsedmarc's `access_token.lstrip(\"Splunk \")`: it strips a\n // pasted \"Splunk \" prefix, and also any leading S/p/l/u/n/k/space.\n this.accessToken = lstripChars(accessToken, TOKEN_PREFIX_CHARS);\n this.index = index;\n // parsedmarc uses socket.getfqdn(); there is no synchronous FQDN lookup\n // in Node, so this is the host name the OS reports.\n this.host = hostname();\n this.source = options.source ?? 'tsdmarc';\n this.timeout = options.timeout ?? DEFAULT_TIMEOUT;\n this.verify = options.verify ?? true;\n this.#commonData = {\n host: this.host,\n source: this.source,\n index: this.index,\n };\n this.headers = {\n 'User-Agent': USER_AGENT,\n Authorization: `Splunk ${this.accessToken}`,\n };\n }\n\n /** The `host`, `source` and `index` fields every event carries. */\n get commonData(): Readonly<JsonObject> {\n return { ...this.#commonData };\n }\n\n /** Saves aggregate DMARC reports to Splunk, one event per record. */\n async saveAggregateReportsToSplunk(\n aggregateReports: AggregateReport | AggregateReport[],\n ): Promise<void> {\n logger.debug('Saving aggregate reports to Splunk');\n const reports = Array.isArray(aggregateReports)\n ? aggregateReports\n : [aggregateReports];\n if (reports.length < 1) {\n return;\n }\n\n const data: JsonObject = { ...this.#commonData };\n const jsonLines: string[] = [];\n for (const report of reports) {\n const metadata = snake(report.reportMetadata);\n const policyPublished = snake(report.policyPublished);\n for (const record of report.records) {\n const snakeRecord = snake(record);\n const { source, alignment, identifiers } = record;\n const newReport: JsonObject = { ...metadata };\n newReport.interval_begin = record.intervalBegin;\n newReport.interval_end = record.intervalEnd;\n newReport.normalized_timespan = snakeRecord.normalized_timespan;\n newReport.published_policy = policyPublished;\n newReport.source_ip_address = source.ipAddress;\n newReport.source_country = source.country;\n newReport.source_reverse_dns = source.reverseDns;\n newReport.source_base_domain = source.baseDomain;\n newReport.source_type = source.type;\n newReport.source_name = source.name;\n newReport.source_asn = source.asn;\n newReport.source_as_name = source.asName;\n newReport.source_as_domain = source.asDomain;\n newReport.message_count = record.count;\n newReport.disposition = record.policyEvaluated.disposition;\n newReport.spf_aligned = alignment.spf;\n newReport.dkim_aligned = alignment.dkim;\n newReport.passed_dmarc = alignment.dmarc;\n newReport.header_from = identifiers.headerFrom;\n newReport.envelope_from = identifiers.envelopeFrom;\n const authResults = snakeRecord.auth_results as JsonObject;\n if ('dkim' in authResults) {\n newReport.dkim_results = authResults.dkim;\n }\n if ('spf' in authResults) {\n newReport.spf_results = authResults.spf;\n }\n\n data.sourcetype = 'dmarc:aggregate';\n // interval_begin is a UTC wall-clock string; assumeUtc keeps it\n // from being re-interpreted as local time on non-UTC hosts.\n data.time = humanTimestampToUnixTimestamp(record.intervalBegin, {\n assumeUtc: true,\n });\n data.event = newReport;\n jsonLines.push(`${pyJsonDumps(data, HEC_JSON)}\\n`);\n }\n }\n await this.#post(jsonLines);\n }\n\n /** Saves failure DMARC reports to Splunk, one event per report. */\n async saveFailureReportsToSplunk(\n failureReports: FailureReport | FailureReport[],\n ): Promise<void> {\n logger.debug('Saving failure reports to Splunk');\n const reports = Array.isArray(failureReports)\n ? failureReports\n : [failureReports];\n if (reports.length < 1) {\n return;\n }\n\n const jsonLines: string[] = [];\n for (const report of reports) {\n const data: JsonObject = { ...this.#commonData };\n data.sourcetype = 'dmarc:failure';\n // arrival_date_utc is a UTC wall-clock string; assumeUtc keeps it\n // from being re-interpreted as local time on non-UTC hosts.\n data.time = humanTimestampToUnixTimestamp(report.arrivalDateUtc, {\n assumeUtc: true,\n });\n data.event = snake(report);\n jsonLines.push(`${pyJsonDumps(data, HEC_JSON)}\\n`);\n }\n await this.#post(jsonLines);\n }\n\n /** Backward-compatible alias of {@link saveFailureReportsToSplunk}. */\n async saveForensicReportsToSplunk(\n failureReports: FailureReport | FailureReport[],\n ): Promise<void> {\n await this.saveFailureReportsToSplunk(failureReports);\n }\n\n /** Saves SMTP TLS reports to Splunk, one event per report. */\n async saveSmtpTlsReportsToSplunk(\n reports: SMTPTLSReport | SMTPTLSReport[],\n ): Promise<void> {\n logger.debug('Saving SMTP TLS reports to Splunk');\n const reportList = Array.isArray(reports) ? reports : [reports];\n if (reportList.length < 1) {\n return;\n }\n\n const jsonLines: string[] = [];\n for (const report of reportList) {\n const data: JsonObject = { ...this.#commonData, sourcetype: 'smtp:tls' };\n const time = smtpTlsEventTime(report);\n if (time !== null) {\n data.time = time;\n }\n data.event = snake(report);\n jsonLines.push(`${pyJsonDumps(data, HEC_JSON)}\\n`);\n }\n await this.#post(jsonLines);\n }\n\n async #post(jsonLines: string[]): Promise<void> {\n if (!this.verify) {\n logger.debug('Skipping certificate verification for Splunk HEC');\n }\n let response: unknown;\n try {\n if (this.#closed.signal.aborted) {\n throw new Error(CLOSED_MESSAGE);\n }\n const result = await httpPost(this.url, {\n headers: { ...this.headers },\n // Bytes, so no Content-Type is added (httpx sends none either).\n body: new TextEncoder().encode(jsonLines.join('')),\n timeout: this.timeout,\n verify: this.verify,\n signal: this.#closed.signal,\n });\n response = JSON.parse(await result.text());\n } catch (error) {\n if (error instanceof InvalidURL) {\n throw new SplunkError(INVALID_URL_MESSAGE);\n }\n throw new SplunkError(describeError(error), { cause: error });\n }\n const { code, text } = (response ?? {}) as {\n code?: unknown;\n text?: unknown;\n };\n if (code !== 0) {\n throw new SplunkError(String(text));\n }\n }\n\n /** Close the client: abort in-flight requests and refuse new ones. */\n close(): void {\n this.#closed.abort(new Error(CLOSED_MESSAGE));\n }\n}\n","// Port of parsedmarc/loki.py: a client for the Grafana Loki push API.\n//\n// https://grafana.com/docs/loki/latest/reference/loki-http-api/#ingest-logs\n\nimport { Buffer } from 'node:buffer';\nimport { hostname } from 'node:os';\n\nimport { parsedAggregateReportsToCsvRows } from '../aggregate';\nimport { USER_AGENT } from '../constants';\nimport type { CsvRow } from '../csv';\nimport { parsedFailureReportsToCsvRows } from '../failure';\nimport { PyValueError } from '../internal/py';\nimport { pyJsonDumps } from '../internal/py-json';\nimport { logger } from '../log';\nimport { parsedSmtpTlsReportsToCsvRows } from '../smtp-tls';\nimport type { AggregateReport, FailureReport, SMTPTLSReport } from '../types';\nimport { pyStr } from '../utils/python';\nimport { redactUrlCredentials } from '../utils/redact';\nimport { humanTimestampToUnixTimestamp } from '../utils/timestamps';\nimport { describeError, httpPost } from './http';\n\nexport const LOKI_PUSH_PATH = '/loki/api/v1/push';\nexport const LOKI_TIMESTAMP_MODES: readonly LokiTimestampMode[] = [\n 'ingest',\n 'report_begin',\n];\nexport const LOKI_DEFAULT_LABEL_FIELDS: readonly string[] = [\n 'report_type',\n 'domain',\n];\n\nexport type LokiTimestampMode = 'ingest' | 'report_begin';\nexport type LokiReportType = 'aggregate' | 'failure' | 'smtp_tls';\n\nconst LABEL_NAME = /^[a-zA-Z_][a-zA-Z0-9_]*$/u;\nconst DEFAULT_TIMEOUT = 60;\nconst DEFAULT_BATCH_SIZE = 1000;\nconst NS_PER_MS = 1_000_000n;\nconst NS_PER_SECOND = 1_000_000_000n;\n/** Characters of the response body quoted in a failed push's error. */\nconst MAX_ERROR_BODY = 500;\n/**\n * Loki answers 204 when it stored the push; 200 is also accepted. Other\n * 2xx codes are not success: with `block_ingestion_until` set it returns a\n * custom 260 and discards the logs (`block_ingestion_status_code`).\n */\nconst SUCCESS_STATUSES: ReadonlySet<number> = new Set([200, 204]);\nconst CLOSED_MESSAGE = 'Cannot send a request, as the client has been closed.';\nconst INVALID_URL_MESSAGE =\n 'Invalid Loki URL (details withheld because they may include credentials)';\n\n/**\n * Per report type: the row field holding the domain the report is about,\n * the row field with its begin time, and whether that value is UTC without\n * an offset (aggregate rows carry the record's interval_begin, which is\n * naive UTC wall-clock time).\n */\nconst REPORT_TYPES: Readonly<\n Record<LokiReportType, readonly [string, string, boolean]>\n> = {\n aggregate: ['domain', 'begin_date', true],\n failure: ['reported_domain', 'arrival_date_utc', true],\n smtp_tls: ['policy_domain', 'begin_date', false],\n};\n\n/** Raised when pushing to Loki fails. */\nexport class LokiError extends Error {\n override name = 'LokiError';\n}\n\nexport interface LokiClientOptions {\n /**\n * Static stream labels, merged over the defaults `job=parsedmarc` and\n * `host=<host name>`.\n */\n labels?: Readonly<Record<string, string>> | null;\n /**\n * Record fields used as stream labels (default `report_type`, `domain`);\n * keep them to fields with few distinct values.\n */\n labelFields?: readonly string[] | null;\n /**\n * `ingest` (time of the push, the default) or `report_begin` (begin of the\n * reported period; Loki rejects entries older than its\n * `reject_old_samples_max_age`).\n */\n timestamp?: LokiTimestampMode;\n /** Basic auth user. */\n username?: string | null;\n /** Basic auth password (used only with `username`). */\n password?: string | null;\n /** Bearer token, instead of basic auth. */\n token?: string | null;\n /** Sent as `X-Scope-OrgID`. */\n tenantId?: string | null;\n /** Verify TLS certificates (default `true`). */\n verify?: boolean;\n /** Request timeout in seconds (default 60). */\n timeout?: number;\n /** Maximum log lines per push request (default 1000). */\n batchSize?: number;\n}\n\ntype Labels = readonly (readonly [string, string])[];\n\ninterface Entry {\n labels: Labels;\n timestamp: bigint;\n line: CsvRow;\n}\n\n/**\n * The push URL: `url` with {@link LOKI_PUSH_PATH} as its path when it has none\n * (keeping its query string, userinfo and port), else `url` unchanged.\n */\nfunction pushUrl(url: string): string {\n if (!URL.canParse(url)) {\n // URL parsers quote part of the URL (part of a password containing\n // \"/\", \"?\" or \"#\" ends up as the \"port\"), so nothing of it is kept.\n throw new PyValueError(INVALID_URL_MESSAGE);\n }\n // Like httpx's `copy_with(path=...)`, which also normalizes the URL\n // (lower-case host, no default port).\n const target = new URL(url);\n if (target.pathname !== '' && target.pathname !== '/') {\n return url;\n }\n target.pathname = LOKI_PUSH_PATH;\n return target.href;\n}\n\n/** `url` without its userinfo. */\nfunction withoutUserinfo(url: string): string {\n const target = new URL(url);\n if (!target.username && !target.password) {\n return url;\n }\n target.username = '';\n target.password = '';\n return target.href;\n}\n\n/** The first `length` characters (code points) of `text`, like Python. */\nfunction headChars(text: string, length: number): string {\n return Array.from(text).slice(0, length).join('');\n}\n\n/** A client for the Grafana Loki push API. */\nexport class LokiClient {\n /** The push URL (with its userinfo, if any). */\n readonly url: string;\n readonly labels: Readonly<Record<string, string>>;\n readonly labelFields: readonly string[];\n readonly timestamp: LokiTimestampMode;\n readonly timeout: number;\n readonly batchSize: number;\n readonly verify: boolean;\n /** Headers sent with every request. */\n readonly headers: Readonly<Record<string, string>>;\n readonly #requestUrl: string;\n readonly #closed = new AbortController();\n\n /**\n * Every report record becomes one JSON log line.\n *\n * @param url - Loki base URL (`/loki/api/v1/push` is appended when the URL\n * has no path) or the full push URL\n */\n constructor(url: string, options: LokiClientOptions = {}) {\n const timestamp = options.timestamp ?? 'ingest';\n if (!LOKI_TIMESTAMP_MODES.includes(timestamp)) {\n throw new PyValueError(\n `Invalid Loki timestamp '${pyStr(timestamp)}'. Must be one of: ${LOKI_TIMESTAMP_MODES.join(', ')}`,\n );\n }\n const { username, password, token, tenantId } = options;\n if (token && (username || password)) {\n throw new PyValueError('Loki token and username/password are exclusive');\n }\n // parsedmarc uses socket.getfqdn(); there is no synchronous FQDN lookup\n // in Node, so this is the host name the OS reports.\n const labels: Record<string, string> = {\n job: 'parsedmarc',\n host: hostname(),\n };\n for (const [name, value] of Object.entries(options.labels ?? {})) {\n labels[name] = value;\n }\n this.labels = labels;\n this.labelFields = [...(options.labelFields ?? LOKI_DEFAULT_LABEL_FIELDS)];\n for (const name of [...Object.keys(labels), ...this.labelFields]) {\n if (!LABEL_NAME.test(name)) {\n throw new PyValueError(`Invalid Loki label name '${name}'`);\n }\n }\n this.url = pushUrl(url);\n this.timestamp = timestamp;\n this.timeout = options.timeout ?? DEFAULT_TIMEOUT;\n this.batchSize = Math.max(\n 1,\n Math.trunc(options.batchSize ?? DEFAULT_BATCH_SIZE),\n );\n if (Number.isNaN(this.batchSize)) {\n throw new PyValueError('Invalid Loki batch size');\n }\n this.verify = options.verify ?? true;\n\n const headers: Record<string, string> = {\n 'User-Agent': USER_AGENT,\n 'Content-Type': 'application/json',\n };\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n if (tenantId) {\n headers['X-Scope-OrgID'] = tenantId;\n }\n // Like httpx's `auth=BasicAuth(...)`, which wins over credentials in\n // the URL; without it, URL credentials become Basic auth (httpPost).\n this.#requestUrl = this.url;\n if (username) {\n const credentials = Buffer.from(\n `${username}:${password ?? ''}`,\n 'utf8',\n ).toString('base64');\n headers.Authorization = `Basic ${credentials}`;\n this.#requestUrl = withoutUserinfo(this.url);\n }\n this.headers = headers;\n }\n\n async saveAggregateReportToLoki(\n aggregateReports: AggregateReport | AggregateReport[],\n ): Promise<void> {\n await this.#save(\n 'aggregate',\n parsedAggregateReportsToCsvRows(aggregateReports),\n );\n }\n\n async saveFailureReportToLoki(\n failureReports: FailureReport | FailureReport[],\n ): Promise<void> {\n await this.#save('failure', parsedFailureReportsToCsvRows(failureReports));\n }\n\n /** Backward-compatible alias of {@link saveFailureReportToLoki}. */\n async saveForensicReportToLoki(\n failureReports: FailureReport | FailureReport[],\n ): Promise<void> {\n await this.saveFailureReportToLoki(failureReports);\n }\n\n async saveSmtpTlsReportToLoki(\n smtpTlsReports: SMTPTLSReport | SMTPTLSReport[],\n ): Promise<void> {\n await this.#save('smtp_tls', parsedSmtpTlsReportsToCsvRows(smtpTlsReports));\n }\n\n async #save(reportType: LokiReportType, rows: CsvRow[]): Promise<void> {\n if (rows.length === 0) {\n return;\n }\n logger.debug(`Saving ${rows.length} ${reportType} records to Loki`);\n const [domainField, beginField, beginIsUtc] = REPORT_TYPES[reportType];\n const now = BigInt(Date.now()) * NS_PER_MS;\n const entries: Entry[] = rows.map((row, index) => {\n const line: CsvRow = { ...row, report_type: reportType };\n if (!Object.hasOwn(line, 'domain')) {\n line.domain = row[domainField];\n }\n let timestamp = now;\n if (this.timestamp === 'report_begin') {\n try {\n if (!Object.hasOwn(row, beginField)) {\n throw new PyValueError(`'${beginField}'`);\n }\n const begin = humanTimestampToUnixTimestamp(pyStr(row[beginField]), {\n assumeUtc: beginIsUtc,\n });\n timestamp = BigInt(begin) * NS_PER_SECOND;\n } catch {\n logger.debug(`Unparsable ${beginField}, using ingest time`);\n }\n }\n return {\n labels: this.#streamLabels(line),\n timestamp: timestamp + BigInt(index),\n line,\n };\n });\n\n // Loki only accepts out-of-order entries within half of max_chunk_age\n // (so 1h by default) of the newest line in a stream, else it rejects\n // them with too_far_behind. Aggregate rows come source-major then per\n // day, so sort the whole list (not per push, which would still go\n // backwards across batches). The sort is stable, like Python's.\n const sorted = entries.toSorted((a, b) => {\n if (a.timestamp === b.timestamp) {\n return 0;\n }\n return a.timestamp < b.timestamp ? -1 : 1;\n });\n\n for (let start = 0; start < sorted.length; start += this.batchSize) {\n // oxlint-disable-next-line no-await-in-loop -- pushes are sequential\n await this.#push(sorted.slice(start, start + this.batchSize));\n }\n }\n\n #streamLabels(line: CsvRow): Labels {\n const labels: Record<string, string> = { ...this.labels };\n for (const field of this.labelFields) {\n const value = Object.hasOwn(line, field) ? line[field] : undefined;\n if (value !== null && value !== undefined && value !== '') {\n labels[field] = pyStr(value);\n }\n }\n return Object.entries(labels);\n }\n\n async #push(entries: readonly Entry[]): Promise<void> {\n const streams = new Map<string, { labels: Labels; values: string[][] }>();\n for (const { labels, timestamp, line } of entries) {\n const key = JSON.stringify(labels);\n let stream = streams.get(key);\n if (stream === undefined) {\n stream = { labels, values: [] };\n streams.set(key, stream);\n }\n stream.values.push([\n timestamp.toString(),\n pyJsonDumps(line, { ensureAscii: false, default: pyStr }),\n ]);\n }\n const body = {\n streams: [...streams.values()].map(({ labels, values }) => ({\n stream: Object.fromEntries(labels),\n values,\n })),\n };\n const redactedUrl = redactUrlCredentials(this.url);\n let status: number;\n let text: string;\n try {\n if (this.#closed.signal.aborted) {\n throw new Error(CLOSED_MESSAGE);\n }\n // Redirects are not followed: httpx re-sends a 301/302 POST as a GET,\n // which the target can answer 200 without storing anything.\n const response = await httpPost(this.#requestUrl, {\n headers: { ...this.headers },\n body: pyJsonDumps(body),\n timeout: this.timeout,\n verify: this.verify,\n followRedirects: false,\n signal: this.#closed.signal,\n });\n ({ status } = response);\n if (SUCCESS_STATUSES.has(status)) {\n await response.discard();\n return;\n }\n try {\n text = await response.text();\n } catch {\n text = '';\n }\n } catch (error) {\n if (this.#closed.signal.aborted) {\n throw new Error(CLOSED_MESSAGE, { cause: error });\n }\n throw new LokiError(\n `Loki push to ${redactedUrl} failed: ${describeError(error)}`,\n { cause: error },\n );\n }\n throw new LokiError(\n `Loki push to ${redactedUrl} failed with HTTP ${status}: ${headChars(text, MAX_ERROR_BODY)}`,\n );\n }\n\n /** Close the client: abort in-flight requests and refuse new ones. */\n close(): void {\n this.#closed.abort(new Error(CLOSED_MESSAGE));\n }\n}\n","// Port of parsedmarc/syslog.py, with the parts of Python's\n// `logging.handlers.SysLogHandler` it uses (UDP and TCP to host:port).\n\nimport { createSocket, type Socket as DatagramSocket } from 'node:dgram';\nimport { lookup } from 'node:dns/promises';\nimport { readFile } from 'node:fs/promises';\nimport { isIP, type Socket } from 'node:net';\nimport { type ConnectionOptions, getCACertificates } from 'node:tls';\n\nimport { parsedAggregateReportsToCsvRows } from '../aggregate';\nimport { parsedFailureReportsToCsvRows } from '../failure';\nimport { errorText, PyValueError } from '../internal/py';\nimport { pyJsonDumps } from '../internal/py-json';\nimport { getLogger, Handler, INFO, Logger, type LogRecord } from '../log';\nimport { parsedSmtpTlsReportsToCsvRows } from '../smtp-tls';\nimport type { AggregateReport, FailureReport, SMTPTLSReport } from '../types';\nimport {\n closeDatagram,\n closeStream,\n connectTcp,\n connectTls,\n RETRY_FACTOR,\n RETRY_MAX,\n RETRY_START,\n SendFailures,\n SendQueue,\n sendAll,\n sendTo,\n} from './socket-io';\n\n/** Connection problems, shared; report rows go through each client's logger. */\nconst connectionLogger = getLogger('tsdmarc_syslog');\n\nconst MS_PER_SECOND = 1000;\n/** How long `close()` waits for queued bytes to flush. */\nconst CLOSE_TIMEOUT_MS = 5000;\n\n/** `LOG_USER`, SysLogHandler's default facility. */\nexport const LOG_USER = 1;\n\n/** SysLogHandler's `priority_names` for the levels its `priority_map` uses. */\nconst PRIORITIES: Readonly<Record<string, number>> = {\n DEBUG: 7,\n INFO: 6,\n WARNING: 4,\n ERROR: 3,\n CRITICAL: 2,\n};\nconst DEFAULT_PRIORITY = PRIORITIES.WARNING ?? 4;\n\nexport type SyslogSocketType = 'dgram' | 'stream';\n\nexport interface SysLogHandlerOptions {\n /** `socket.SOCK_DGRAM` (UDP, the default) or `SOCK_STREAM` (TCP). */\n socktype?: SyslogSocketType;\n /** Seconds for connect and send; `null` (the default) waits forever. */\n timeout?: number | null;\n /** Turns each new TCP connection into the one to use (TLS). */\n wrapSocket?: (socket: Socket) => Promise<Socket>;\n}\n\n/**\n * Port of `logging.handlers.SysLogHandler` for a `(host, port)` address:\n * sends `<PRI>message\\0` per record, as one datagram (UDP) or on one\n * connection (TCP, no octet counting, like Python). Create it with\n * {@link SysLogHandler.create}, which resolves the host and, for TCP,\n * connects, like Python's constructor. Unlike Python, which keeps using a\n * dead TCP socket forever, a failed send reconnects (with `SocketHandler`'s\n * back-off) and tries once more; what is still lost is in {@link takeFailures}.\n */\nexport class SysLogHandler extends Handler {\n readonly address: readonly [string, number];\n readonly facility: number = LOG_USER;\n readonly socktype: SyslogSocketType;\n /** Seconds for each send on a stream socket; `null` waits forever. */\n timeout: number | null;\n /** The datagram socket, or the (possibly TLS-wrapped) stream socket. */\n socket: DatagramSocket | Socket | null = null;\n readonly #queue = new SendQueue();\n readonly #failures = new SendFailures();\n readonly #wrapSocket: SysLogHandlerOptions['wrapSocket'];\n /** Epoch ms before which no reconnect is attempted. */\n #retryTime: number | null = null;\n #retryPeriod: number = RETRY_START;\n #connectError: unknown = null;\n\n protected constructor(\n address: readonly [string, number],\n options: SysLogHandlerOptions,\n ) {\n super();\n this.address = address;\n this.socktype = options.socktype ?? 'dgram';\n this.timeout = options.timeout ?? null;\n this.#wrapSocket = options.wrapSocket;\n // Python's default Formatter: the message, then any traceback.\n this.formatter = (record: LogRecord): string =>\n record.error === undefined\n ? record.message\n : `${record.message}\\n${record.error instanceof Error ? (record.error.stack ?? errorText(record.error)) : String(record.error)}`;\n }\n\n /** Create the handler and its socket (throws if that fails). */\n static async create(\n address: readonly [string, number],\n options: SysLogHandlerOptions = {},\n ): Promise<SysLogHandler> {\n const handler = new SysLogHandler(address, options);\n await handler.createSocket();\n return handler;\n }\n\n /**\n * Resolve the address and create the socket; for TCP, connect to the\n * first address that accepts, like Python's `createSocket()`.\n */\n async createSocket(): Promise<void> {\n const [host, port] = this.address;\n const addresses = await lookup(host, { all: true, order: 'verbatim' });\n if (addresses.length === 0) {\n throw new Error('getaddrinfo returns an empty list');\n }\n const timeoutMs = this.timeout ? this.timeout * MS_PER_SECOND : null;\n let lastError: unknown = null;\n for (const { address, family } of addresses) {\n if (this.socktype === 'dgram') {\n const socket = createSocket(family === 6 ? 'udp6' : 'udp4');\n socket.on('error', () => undefined);\n socket.unref();\n this.socket = socket;\n return;\n }\n try {\n const plain = await connectTcp(\n { host: address, port, family },\n timeoutMs,\n );\n plain.unref();\n const socket = (await this.#wrapSocket?.(plain)) ?? plain;\n socket.unref();\n this.socket = socket;\n return;\n } catch (error) {\n lastError = error;\n }\n }\n throw lastError;\n }\n\n /** `SysLogHandler.encodePriority()` with `mapPriority()`. */\n encodePriority(levelName: string): number {\n // Python: (facility << 3) | priority\n return this.facility * 8 + (PRIORITIES[levelName] ?? DEFAULT_PRIORITY);\n }\n\n emit(record: LogRecord): void {\n // Python appends a NUL (\"some old syslog daemons expect\" it), on UDP\n // and TCP alike, and frames TCP messages by nothing else.\n const message = `<${this.encodePriority(record.levelName)}>${this.format(record)}\\0`;\n const data = Buffer.from(message, 'utf-8');\n this.#queue.push(\n () => this.#send(data),\n error => {\n this.handleError(record, error);\n if (this.socktype === 'stream') {\n this.#failures.record(error);\n }\n },\n );\n }\n\n async #send(data: Uint8Array): Promise<void> {\n if (this.socktype === 'stream') {\n try {\n await this.#sendOverStream(data);\n } catch {\n // A server restart only shows on the next send, over the dead socket.\n await this.#sendOverStream(data);\n }\n return;\n }\n if (this.socket === null) {\n await this.createSocket();\n }\n const [host, port] = this.address;\n await sendTo(this.socket as DatagramSocket, data, port, host);\n }\n\n /** Send over the open connection, or a new one; throws if that fails. */\n async #sendOverStream(data: Uint8Array): Promise<void> {\n if (this.socket === null) {\n await this.#reconnect();\n }\n const socket = this.socket as Socket;\n const timeoutMs = this.timeout ? this.timeout * MS_PER_SECOND : null;\n try {\n await sendAll(socket, data, timeoutMs);\n } catch (error) {\n socket.destroy();\n this.socket = null;\n throw error;\n }\n }\n\n /** `createSocket()` with the back-off of Python's `SocketHandler`. */\n async #reconnect(): Promise<void> {\n const now = Date.now();\n if (this.#retryTime !== null && now < this.#retryTime) {\n throw this.#connectError;\n }\n try {\n await this.createSocket();\n this.#retryTime = null;\n } catch (error) {\n this.#retryPeriod =\n this.#retryTime === null\n ? RETRY_START\n : Math.min(this.#retryPeriod * RETRY_FACTOR, RETRY_MAX);\n this.#retryTime = now + this.#retryPeriod * MS_PER_SECOND;\n this.#connectError = error;\n throw error;\n }\n }\n\n /** Resolves once every record emitted so far has been sent (or failed). */\n async drain(): Promise<void> {\n await this.#queue.drain();\n }\n\n /** The messages lost over TCP/TLS since the last call (UDP has no record). */\n takeFailures(): { count: number; error: unknown } | null {\n return this.#failures.take();\n }\n\n /** Close the socket (after the queued sends). Use {@link closeAsync} to wait. */\n override close(): void {\n void this.closeAsync();\n }\n\n /** Send what is queued, then close the socket. */\n async closeAsync(): Promise<void> {\n await this.drain();\n const { socket } = this;\n this.socket = null;\n if (socket === null) {\n return;\n }\n if (this.socktype === 'dgram') {\n await closeDatagram(socket as DatagramSocket);\n } else {\n await closeStream(socket as Socket, CLOSE_TIMEOUT_MS);\n }\n }\n}\n\nexport interface SyslogClientOptions {\n /** `udp` (default), `tcp` or `tls`; case-insensitive. */\n protocol?: string;\n /** CA certificate file for TLS server verification, added to the system CAs. */\n cafilePath?: string | null;\n /** Client certificate file for TLS authentication. */\n certfilePath?: string | null;\n /** Client private key file for TLS authentication. */\n keyfilePath?: string | null;\n /** Connection timeout in seconds for TCP/TLS (default 5). */\n timeout?: number;\n /** Connection attempts for TCP/TLS (default 3). */\n retryAttempts?: number;\n /** Seconds between connection attempts (default 5). */\n retryDelay?: number;\n}\n\n/** Hooks for tests: the delay between connection attempts. */\nexport const syslogTimers: { sleep: (seconds: number) => Promise<void> } = {\n async sleep(seconds: number): Promise<void> {\n await new Promise(resolve => setTimeout(resolve, seconds * MS_PER_SECOND));\n },\n};\n\nasync function readOptionalFile(\n filePath: string | null | undefined,\n): Promise<Buffer | null> {\n return filePath ? await readFile(filePath) : null;\n}\n\n/** Raised when messages could not be delivered over TCP or TLS. */\nexport class SyslogError extends Error {\n override name = 'SyslogError';\n}\n\n/** A client for Syslog. */\nexport class SyslogClient {\n readonly serverName: string;\n readonly serverPort: number;\n readonly protocol: string;\n readonly timeout: number;\n readonly retryAttempts: number;\n readonly retryDelay: number;\n readonly logger: Logger;\n /** The syslog handler attached to {@link logger}. */\n logHandler!: Handler;\n\n protected constructor(\n serverName: string,\n serverPort: number,\n options: SyslogClientOptions,\n ) {\n this.serverName = serverName;\n this.serverPort = serverPort;\n this.protocol = (options.protocol ?? 'udp').toLowerCase();\n this.timeout = options.timeout ?? 5;\n this.retryAttempts = options.retryAttempts ?? 3;\n this.retryDelay = options.retryDelay ?? 5;\n // Its own logger, not parsedmarc's shared `getLogger()` one: a logger\n // sends to all its handlers, so clients sharing it would each send their\n // rows to every other client's server.\n this.logger = new Logger('tsdmarc_syslog');\n this.logger.setLevel(INFO);\n }\n\n /**\n * Create a SyslogClient: resolves the server and, for TCP/TLS, connects\n * (retrying `retryAttempts` times, `retryDelay` seconds apart), so a bad\n * address fails here as Python's constructor does.\n *\n * @param serverName - The Syslog server\n * @param serverPort - The Syslog port\n */\n static async create(\n serverName: string,\n serverPort: number,\n options: SyslogClientOptions = {},\n ): Promise<SyslogClient> {\n const client = new SyslogClient(serverName, serverPort, options);\n client.logHandler = await client.createSyslogHandler(options);\n client.logger.addHandler(client.logHandler);\n return client;\n }\n\n /** Creates a SysLogHandler with the specified protocol and TLS settings. */\n protected async createSyslogHandler(\n options: SyslogClientOptions,\n ): Promise<SysLogHandler> {\n const { protocol, retryAttempts, retryDelay, timeout } = this;\n const address = [this.serverName, this.serverPort] as const;\n if (protocol === 'udp') {\n // UDP protocol (default, backward compatible)\n return await SysLogHandler.create(address, { socktype: 'dgram' });\n }\n if (protocol !== 'tcp' && protocol !== 'tls') {\n throw new PyValueError(\n `Invalid protocol '${protocol}'. Must be 'udp', 'tcp', or 'tls'.`,\n );\n }\n // TCP or TLS protocol with retry logic\n for (let attempt = 1; attempt <= retryAttempts; attempt += 1) {\n try {\n if (protocol === 'tcp') {\n return await SysLogHandler.create(address, {\n socktype: 'stream',\n timeout,\n });\n }\n return await this.createTlsHandler(options);\n } catch (error) {\n if (attempt < retryAttempts) {\n connectionLogger.warning(\n `Syslog connection attempt ${attempt}/${retryAttempts} failed: ${errorText(error)}. ` +\n `Retrying in ${retryDelay} seconds...`,\n );\n await syslogTimers.sleep(retryDelay);\n } else {\n connectionLogger.error(\n `Syslog connection failed after ${retryAttempts} attempts: ${errorText(error)}`,\n );\n throw error;\n }\n }\n }\n // Only reachable when retryAttempts < 1.\n throw new PyValueError('retry_attempts must be at least 1');\n }\n\n /** TCP connection wrapped in TLS ≥ 1.2, verifying the server. */\n protected async createTlsHandler(\n options: SyslogClientOptions,\n ): Promise<SysLogHandler> {\n const { cafilePath, certfilePath, keyfilePath } = options;\n // ssl.create_default_context(): system CAs plus the given CA file,\n // hostname check, TLS 1.2 minimum.\n const tlsOptions: ConnectionOptions = {\n minVersion: 'TLSv1.2',\n rejectUnauthorized: true,\n };\n const ca = await readOptionalFile(cafilePath);\n if (ca !== null) {\n tlsOptions.ca = [...getCACertificates('default'), ca.toString('utf-8')];\n }\n if (certfilePath && keyfilePath) {\n tlsOptions.cert = await readFile(certfilePath);\n tlsOptions.key = await readFile(keyfilePath);\n } else if (certfilePath || keyfilePath) {\n connectionLogger.warning(\n 'Both certfile_path and keyfile_path are required for ' +\n 'client certificate authentication. Client authentication ' +\n 'will not be used.',\n );\n }\n\n const timeoutMs = this.timeout * MS_PER_SECOND;\n // Every connection is wrapped, reconnections included: plain TCP after a\n // dropped connection would send the reports in the clear.\n const wrapSocket = async (plain: Socket): Promise<Socket> => {\n try {\n return await connectTls(\n {\n ...tlsOptions,\n socket: plain,\n host: this.serverName,\n // Python sends no SNI for IP addresses; Node refuses one.\n ...(isIP(this.serverName) === 0 && { servername: this.serverName }),\n },\n timeoutMs,\n );\n } catch (error) {\n plain.destroy();\n throw error;\n }\n };\n return await SysLogHandler.create([this.serverName, this.serverPort], {\n socktype: 'stream',\n timeout: this.timeout,\n wrapSocket,\n });\n }\n\n async saveAggregateReportToSyslog(\n aggregateReports: AggregateReport | AggregateReport[],\n ): Promise<void> {\n const rows = parsedAggregateReportsToCsvRows(aggregateReports);\n for (const row of rows) {\n this.logger.info(pyJsonDumps(row));\n }\n await this.#drain();\n }\n\n async saveFailureReportToSyslog(\n failureReports: FailureReport | FailureReport[],\n ): Promise<void> {\n const rows = parsedFailureReportsToCsvRows(failureReports);\n for (const row of rows) {\n this.logger.info(pyJsonDumps(row));\n }\n await this.#drain();\n }\n\n /** Backward-compatible alias of {@link saveFailureReportToSyslog}. */\n async saveForensicReportToSyslog(\n failureReports: FailureReport | FailureReport[],\n ): Promise<void> {\n await this.saveFailureReportToSyslog(failureReports);\n }\n\n async saveSmtpTlsReportToSyslog(\n smtpTlsReports: SMTPTLSReport | SMTPTLSReport[],\n ): Promise<void> {\n const rows = parsedSmtpTlsReportsToCsvRows(smtpTlsReports);\n for (const row of rows) {\n this.logger.info(pyJsonDumps(row));\n }\n await this.#drain();\n }\n\n /**\n * Wait until the messages logged so far have been sent.\n *\n * @throws {SyslogError} When some could not be delivered over TCP or TLS.\n */\n async #drain(): Promise<void> {\n if (this.logHandler instanceof SysLogHandler) {\n await this.logHandler.drain();\n const failures = this.logHandler.takeFailures();\n if (failures !== null) {\n throw new SyslogError(\n `${failures.count} message(s) not sent to ${this.serverName}:${this.serverPort}: ${errorText(failures.error)}`,\n { cause: failures.error },\n );\n }\n }\n }\n\n /** Remove and close the syslog handler, releasing its socket. */\n async close(): Promise<void> {\n this.logger.removeHandler(this.logHandler);\n if (this.logHandler instanceof SysLogHandler) {\n await this.logHandler.closeAsync();\n } else {\n this.logHandler.close();\n }\n }\n}\n","// Port of parsedmarc/webhook.py.\n\nimport { USER_AGENT } from '../constants';\nimport { logger } from '../log';\nimport { InvalidURL } from '../utils/http';\nimport { describeError, httpPost } from './http';\n\nconst DEFAULT_TIMEOUT = 60;\n\n/** Raised (and logged) when a request is made after {@link WebhookClient.close}. */\nconst CLOSED_MESSAGE = 'Cannot send a request, as the client has been closed.';\n\n/** Logged (or thrown) for a URL that can't be parsed; see {@link InvalidURL}. */\nconst INVALID_URL_MESSAGE =\n 'invalid webhook URL (details withheld because they may include credentials)';\n\nconst HTTP_SUCCESS_MIN = 200;\nconst HTTP_SUCCESS_MAX = 299;\n\nfunction isSuccess(status: number): boolean {\n return status >= HTTP_SUCCESS_MIN && status <= HTTP_SUCCESS_MAX;\n}\n\nexport type WebhookPayload = string | Uint8Array | Record<string, string>;\n\nexport interface WebhookClientOptions {\n /** Seconds to wait for each webhook call (default 60); `null` waits forever. */\n timeout?: number | null;\n /**\n * Throw network errors, timeouts and non-2xx responses instead of only\n * logging them (default `false`, as parsedmarc). Turn it on when the\n * caller has to know a report was not delivered, e.g. before deleting\n * its email; the CLI does.\n */\n throwOnError?: boolean;\n}\n\n/** A client for webhooks. */\nexport class WebhookClient {\n readonly aggregateUrl: string;\n readonly failureUrl: string;\n readonly smtpTlsUrl: string;\n readonly timeout: number | null;\n readonly throwOnError: boolean;\n /** Headers sent with every request. */\n readonly headers: Readonly<Record<string, string>>;\n readonly #closed = new AbortController();\n\n /**\n * @param aggregateUrl - The aggregate report webhook URL\n * @param failureUrl - The failure report webhook URL\n * @param smtpTlsUrl - The SMTP TLS report webhook URL\n */\n constructor(\n aggregateUrl: string,\n failureUrl: string,\n smtpTlsUrl: string,\n options: WebhookClientOptions = {},\n ) {\n this.aggregateUrl = aggregateUrl;\n this.failureUrl = failureUrl;\n this.smtpTlsUrl = smtpTlsUrl;\n this.timeout =\n options.timeout === undefined ? DEFAULT_TIMEOUT : options.timeout;\n this.throwOnError = options.throwOnError ?? false;\n this.headers = {\n 'User-Agent': USER_AGENT,\n 'Content-Type': 'application/json',\n };\n }\n\n async saveFailureReportToWebhook(report: string): Promise<void> {\n await this.sendToWebhook(this.failureUrl, report);\n }\n\n /** Backward-compatible alias of {@link saveFailureReportToWebhook}. */\n async saveForensicReportToWebhook(report: string): Promise<void> {\n await this.saveFailureReportToWebhook(report);\n }\n\n async saveSmtpTlsReportToWebhook(report: string): Promise<void> {\n await this.sendToWebhook(this.smtpTlsUrl, report);\n }\n\n async saveAggregateReportToWebhook(report: string): Promise<void> {\n await this.sendToWebhook(this.aggregateUrl, report);\n }\n\n /**\n * POST `payload` to `webhookUrl`. A string or bytes payload is sent as\n * is; an object is form-encoded. Network errors are logged and swallowed\n * and HTTP error statuses ignored, so a failing webhook never aborts the\n * surrounding batch; with `throwOnError`, both are thrown instead (and\n * not logged here).\n */\n async sendToWebhook(\n webhookUrl: string,\n payload: WebhookPayload,\n ): Promise<void> {\n let status: number;\n try {\n if (this.#closed.signal.aborted) {\n throw new Error(CLOSED_MESSAGE);\n }\n const body =\n typeof payload === 'string' || payload instanceof Uint8Array\n ? payload\n : new URLSearchParams(payload);\n const response = await httpPost(webhookUrl, {\n headers: { ...this.headers },\n body,\n timeout: this.timeout,\n signal: this.#closed.signal,\n });\n // The body is not used (parsedmarc doesn't look at it either); drop\n // it so the connection is released now rather than at GC.\n await response.discard();\n ({ status } = response);\n } catch (error) {\n if (error instanceof InvalidURL) {\n // httpx quotes part of the URL, which can be part of a password\n if (this.throwOnError) {\n throw new Error(INVALID_URL_MESSAGE);\n }\n logger.error(`Webhook Error: ${INVALID_URL_MESSAGE}`);\n return;\n }\n if (this.throwOnError) {\n throw new Error(describeError(error), { cause: error });\n }\n logger.error(`Webhook Error: ${describeError(error)}`);\n return;\n }\n if (this.throwOnError && !isSuccess(status)) {\n // No URL in the message: webhook URLs often carry a token.\n throw new Error(`Webhook responded with HTTP ${status}`);\n }\n }\n\n /** Close the client: abort in-flight requests and refuse new ones. */\n close(): void {\n this.#closed.abort(new Error(CLOSED_MESSAGE));\n }\n}\n","// Maildir mailbox backend, ported from mailsuite.mailbox.maildir.\n//\n// mailsuite builds on Python's `mailbox.Maildir`; this module reimplements\n// the parts of it the connection relies on with `node:fs`: the\n// `new`/`cur`/`tmp` layout, Maildir++ subfolders stored as sibling\n// `.<folder>` directories, and the `:2,<flags>` info suffix. Like mailsuite\n// 2.3.3, messages are read as stored and marked read or moved by renaming\n// their files, never re-serialized. Keys, file names and folder names are the\n// same as Python's, so a Maildir can be shared with mailsuite/parsedmarc.\n\nimport { constants as fsConstants, mkdirSync, statSync } from 'node:fs';\nimport {\n open,\n readdir,\n readFile,\n rename,\n rm,\n rmdir,\n stat,\n unlink,\n} from 'node:fs/promises';\nimport { EOL } from 'node:os';\nimport path from 'node:path';\nimport { setTimeout as sleep } from 'node:timers/promises';\n\nimport { emailTooLargeError } from '../errors';\nimport { logger } from '../log';\nimport {\n type FetchMessageOptions,\n type FetchMessagesOptions,\n MailboxConnection,\n type MessageId,\n NotImplementedError,\n type SendMessageOptions,\n type WatchOptions,\n} from './base';\n\n/** The Maildir directory does not exist and `maildirCreate` was not set. */\nexport class NoSuchMailboxError extends Error {\n override name = 'NoSuchMailboxError';\n}\n\n/** A folder can't be removed because it still holds messages or subfolders. */\nexport class NotEmptyError extends Error {\n override name = 'NotEmptyError';\n}\n\n/** A move would overwrite a message with the same key (`mailbox.ExternalClashError`). */\nexport class ExternalClashError extends Error {\n override name = 'ExternalClashError';\n}\n\n/**\n * Running as root, the process could not fully drop to the Maildir's owner\n * (uid, gid and supplementary groups). mailsuite only warns and stays root.\n */\nexport class PrivilegeDropError extends Error {\n override name = 'PrivilegeDropError';\n}\n\n/** No message has the given key (Python's `KeyError`). */\nexport class MaildirKeyError extends Error {\n override name = 'KeyError';\n}\n\nexport interface MaildirConnectionOptions {\n /** Create the Maildir (`cur`, `new`, `tmp`) if it doesn't exist. Default: false. */\n maildirCreate?: boolean;\n}\n\n/** Separator between a message's unique name and its info (`:2,S`). */\nconst COLON = ':';\nconst SUBDIRS = ['tmp', 'new', 'cur'] as const;\nconst TOC_SUBDIRS = ['new', 'cur'] as const;\nconst FOLDER_MODE = 0o700;\nconst FILE_MODE = 0o666;\nconst MILLISECONDS_PER_SECOND = 1000;\nconst MAILDIR_INFO_PREFIX = '2,';\nconst SEEN_FLAG = 'S';\n\n// `bytes.decode(\"utf-8\", \"replace\")`, which keeps a byte order mark.\nconst utf8Decoder = new TextDecoder('utf-8', { ignoreBOM: true });\n\n/** Python's `str(exception)`: the message without the class name. */\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/** Python's `os.path.isdir`: false for anything missing or unreadable. */\nasync function isDirectory(target: string): Promise<boolean> {\n try {\n return (await stat(target)).isDirectory();\n } catch {\n return false;\n }\n}\n\n/** The flags of an info suffix (`2,RS` → `RS`), like `MaildirMessage.get_flags()`. */\nfunction flagsOf(info: string): string {\n return info.startsWith(MAILDIR_INFO_PREFIX)\n ? info.slice(MAILDIR_INFO_PREFIX.length)\n : '';\n}\n\n/** `MaildirMessage.add_flag()`: merge, deduplicate and sort the flags. */\nfunction addFlag(info: string, flag: string): string {\n const flags = new Set([...flagsOf(info), ...flag]);\n return `${MAILDIR_INFO_PREFIX}${[...flags].sort().join('')}`;\n}\n\n/**\n * The parts of Python's `mailbox.Maildir` that `MaildirConnection` uses.\n *\n * Keys are the unique part of a file name (before the colon). The table of\n * contents is rebuilt on every lookup miss and every `keys()` call, which is\n * what Python's `_refresh()` amounts to when files change underneath it.\n */\nclass Maildir {\n readonly root: string;\n private toc = new Map<string, string>();\n\n private constructor(root: string) {\n this.root = root;\n }\n\n /** Open (and with `create`, make) a Maildir, like `mailbox.Maildir(path, create=...)`. */\n static openSync(dirname: string, create: boolean): Maildir {\n const root = path.resolve(dirname);\n let exists = true;\n try {\n statSync(root);\n } catch {\n exists = false;\n }\n if (!exists) {\n if (!create) {\n throw new NoSuchMailboxError(root);\n }\n mkdirSync(root, { mode: FOLDER_MODE });\n for (const subdir of SUBDIRS) {\n mkdirSync(path.join(root, subdir), { mode: FOLDER_MODE });\n }\n }\n return new Maildir(root);\n }\n\n /** `Maildir.add_folder()`: open or create `.<folder>` and its `maildirfolder` marker. */\n async addFolder(folder: string): Promise<Maildir> {\n const folderPath = path.join(this.root, `.${folder}`);\n const result = Maildir.openSync(folderPath, true);\n const markerPath = path.join(folderPath, 'maildirfolder');\n // O_CREAT without O_EXCL/O_TRUNC: create the marker, keep an existing one.\n const handle = await open(\n markerPath,\n fsConstants.O_CREAT | fsConstants.O_WRONLY,\n FILE_MODE,\n );\n await handle.close();\n return result;\n }\n\n /** `Maildir.remove_folder()`: delete an empty folder. */\n async removeFolder(folder: string): Promise<void> {\n const folderPath = path.join(this.root, `.${folder}`);\n const entries = [\n ...(await readdir(path.join(folderPath, 'new'))),\n ...(await readdir(path.join(folderPath, 'cur'))),\n ];\n for (const entry of entries) {\n if (entry.length < 1 || !entry.startsWith('.')) {\n throw new NotEmptyError(`Folder contains message(s): ${folder}`);\n }\n }\n for (const entry of await readdir(folderPath)) {\n if (\n entry !== 'new' &&\n entry !== 'cur' &&\n entry !== 'tmp' &&\n (await isDirectory(path.join(folderPath, entry)))\n ) {\n throw new NotEmptyError(\n `Folder contains subdirectory '${folder}': ${entry}`,\n );\n }\n }\n // What is left are new/cur/tmp (holding only dot files) and plain files.\n for (const entry of await readdir(folderPath)) {\n await rm(path.join(folderPath, entry), { recursive: true });\n }\n await rmdir(folderPath);\n }\n\n /** `Maildir._refresh()`: map each key to its `new/...` or `cur/...` subpath. */\n private async refresh(): Promise<void> {\n const toc = new Map<string, string>();\n for (const subdir of TOC_SUBDIRS) {\n const subdirPath = path.join(this.root, subdir);\n for (const entry of await readdir(subdirPath)) {\n if (entry.startsWith('.')) {\n continue;\n }\n if (await isDirectory(path.join(subdirPath, entry))) {\n continue;\n }\n const key = entry.split(COLON)[0] ?? entry;\n toc.set(key, path.join(subdir, entry));\n }\n }\n this.toc = toc;\n }\n\n /** `Maildir._lookup()`: the subpath of a key, or `MaildirKeyError`. */\n private async lookup(key: string): Promise<string> {\n const cached = this.toc.get(key);\n if (cached !== undefined) {\n try {\n await stat(path.join(this.root, cached));\n return cached;\n } catch {\n // Moved or deleted since the last refresh: rescan below.\n }\n }\n await this.refresh();\n const subpath = this.toc.get(key);\n if (subpath === undefined) {\n throw new MaildirKeyError(`No message with key: ${key}`);\n }\n return subpath;\n }\n\n async keys(): Promise<string[]> {\n await this.refresh();\n return [...this.toc.keys()];\n }\n\n /** The size of a message file in bytes, or `null` when the key is unknown. */\n async size(key: string): Promise<number | null> {\n let subpath: string;\n try {\n subpath = await this.lookup(key);\n } catch (error) {\n if (error instanceof MaildirKeyError) {\n return null;\n }\n throw error;\n }\n return (await stat(path.join(this.root, subpath))).size;\n }\n\n /**\n * `Maildir.get_bytes()`: the stored bytes, or `null` when the key is\n * unknown. Python converts `os.linesep` to `\\n` (a no-op on POSIX).\n */\n async getBytes(key: string): Promise<Buffer | null> {\n let subpath: string;\n try {\n subpath = await this.lookup(key);\n } catch (error) {\n if (error instanceof MaildirKeyError) {\n return null;\n }\n throw error;\n }\n const content = await readFile(path.join(this.root, subpath));\n return EOL === '\\n'\n ? content\n : Buffer.from(content.toString('latin1').replaceAll(EOL, '\\n'), 'latin1');\n }\n\n /** `key in maildir`. */\n async has(key: string): Promise<boolean> {\n await this.refresh();\n return this.toc.has(key);\n }\n\n /**\n * Move a message's file into the same subdirectory (`new`/`cur`) of\n * `destination`, keeping its name; `MaildirKeyError` for an unknown key.\n */\n async moveTo(key: string, destination: Maildir): Promise<void> {\n const subpath = await this.lookup(key);\n await rename(\n path.join(this.root, subpath),\n path.join(destination.root, subpath),\n );\n this.toc.delete(key);\n destination.toc.set(key, subpath);\n }\n\n /** `Maildir.remove()`: delete a message, or `MaildirKeyError`. */\n async remove(key: string): Promise<void> {\n await unlink(path.join(this.root, await this.lookup(key)));\n }\n\n /**\n * Mark a message read like mailsuite 2.3.3: rename its file into `cur/`\n * with the `S` (Seen) flag added to its info suffix, which is what the\n * Maildir spec defines a flag change to be; the content is untouched.\n */\n async markRead(key: string): Promise<void> {\n const subpath = await this.lookup(key);\n const name = path.basename(subpath);\n const info = name.includes(COLON) ? (name.split(COLON).at(-1) ?? '') : '';\n const newSubpath = path.join(\n 'cur',\n `${key}${COLON}${addFlag(info, SEEN_FLAG)}`,\n );\n await rename(\n path.join(this.root, subpath),\n path.join(this.root, newSubpath),\n );\n this.toc.set(key, newSubpath);\n }\n}\n\n/**\n * Drop root to the Maildir's owner: its group as the only group, then its uid.\n *\n * Unlike mailsuite, which keeps root's groups and, if `setuid` fails, only\n * warns and goes on as root, any failure throws `PrivilegeDropError`, so the\n * process never keeps working as root (or half-dropped) by accident.\n */\nfunction dropPrivileges(maildirPath: string, uid: number, gid: number): void {\n const { setuid, setgid, setgroups } = process;\n const fix =\n `Run tsdmarc as the Maildir's owner (uid ${uid}), or chown ${maildirPath} ` +\n 'to the user tsdmarc runs as.';\n // Deno has no process.setgroups(): root's groups (gid 0) would be kept.\n if (setgroups === undefined || setgid === undefined || setuid === undefined) {\n throw new PrivilegeDropError(\n `Running as root over ${maildirPath} (owner uid ${uid}), but this ` +\n `runtime can't drop root's groups. ${fix}`,\n );\n }\n try {\n // Groups first: without root they can no longer be changed.\n setgroups([gid]);\n setgid(gid);\n setuid(uid);\n } catch (error) {\n throw new PrivilegeDropError(\n `Failed to switch to uid ${uid} and gid ${gid} to access Maildir ` +\n `${maildirPath}: ${errorMessage(error)}. ${fix}`,\n { cause: error },\n );\n }\n}\n\n/**\n * A `MailboxConnection` backed by an on-disk Maildir.\n *\n * Useful for local processing of messages dropped into a Maildir by an MTA\n * (e.g. postfix delivering DMARC reports). Maildir has no concept of\n * sending: `sendMessage()` throws `NotImplementedError`.\n *\n * The constructor does its filesystem checks synchronously, like\n * mailsuite's: uid checks (warn, don't crash, but throw `PrivilegeDropError`\n * if root can't drop to the Maildir's owner), the optional `cur`/`new`/`tmp`\n * creation, and opening the Maildir.\n */\nexport class MaildirConnection extends MailboxConnection {\n readonly kind = 'maildir' as const;\n private readonly maildirPath: string;\n private readonly maildirCreate: boolean;\n private readonly client: Maildir;\n private activeFolder: Maildir;\n private readonly subfolderClient = new Map<string, Maildir>();\n\n constructor(maildirPath: string, options: MaildirConnectionOptions = {}) {\n super();\n this.maildirPath = maildirPath;\n this.maildirCreate = options.maildirCreate ?? false;\n\n // When run as root over a user-owned Maildir, drop privileges to the\n // Maildir's owner so file creation works correctly. Only on systems\n // with getuid()/setuid() (not Windows).\n const { getuid, setuid } = process;\n if (getuid !== undefined && setuid !== undefined) {\n let maildirOwner: number | null = null;\n let maildirGroup = 0;\n try {\n ({ uid: maildirOwner, gid: maildirGroup } = statSync(maildirPath));\n } catch {\n maildirOwner = null;\n }\n const currentUid = getuid();\n if (maildirOwner !== null && currentUid !== maildirOwner) {\n if (currentUid === 0) {\n logger.warning(`Switching uid to ${maildirOwner} to access Maildir`);\n dropPrivileges(maildirPath, maildirOwner, maildirGroup);\n } else {\n logger.warning(\n `Runtime uid ${currentUid} differs from maildir ${maildirPath} owner ${maildirOwner}. ` +\n 'Access may fail if permissions are insufficient.',\n );\n }\n }\n }\n\n if (this.maildirCreate) {\n for (const subdir of ['cur', 'new', 'tmp']) {\n mkdirSync(path.join(maildirPath, subdir), { recursive: true });\n }\n }\n this.client = Maildir.openSync(maildirPath, this.maildirCreate);\n this.activeFolder = this.client;\n }\n\n private async getFolder(folderName: string): Promise<Maildir> {\n let folder = this.subfolderClient.get(folderName);\n if (folder === undefined) {\n folder = await this.client.addFolder(folderName);\n this.subfolderClient.set(folderName, folder);\n }\n return folder;\n }\n\n private folderPath(folderName: string): string {\n return path.join(this.maildirPath, `.${folderName}`);\n }\n\n override async createFolder(folderName: string): Promise<void> {\n await this.getFolder(folderName);\n }\n\n override async renameFolder(oldName: string, newName: string): Promise<void> {\n // Maildir++ stores each folder as a sibling directory named \".<folder>\",\n // so a rename moves the directory. Guard first: rename(2) would\n // otherwise silently replace an empty target.\n await this.ensureNoFolderConflict(newName);\n await rename(this.folderPath(oldName), this.folderPath(newName));\n this.subfolderClient.delete(oldName);\n this.subfolderClient.delete(newName);\n }\n\n override folderExists(folderName: string): Promise<boolean> {\n return isDirectory(this.folderPath(folderName));\n }\n\n override async deleteFolder(folderName: string): Promise<void> {\n // The folder must be empty of messages and subfolders; mergeFolders()\n // empties it first.\n await this.client.removeFolder(folderName);\n this.subfolderClient.delete(folderName);\n }\n\n protected override async doMoveFolder(\n source: string,\n _targetParent: string,\n targetPath: string,\n ): Promise<void> {\n await rename(this.folderPath(source), this.folderPath(targetPath));\n this.subfolderClient.delete(source);\n this.subfolderClient.delete(targetPath);\n }\n\n override async fetchMessages(\n reportsFolder: string,\n _options?: FetchMessagesOptions,\n ): Promise<MessageId[]> {\n this.activeFolder =\n reportsFolder && reportsFolder !== 'INBOX'\n ? await this.getFolder(reportsFolder)\n : this.client;\n return this.activeFolder.keys();\n }\n\n /**\n * The message's stored bytes decoded as UTF-8 (undecodable bytes\n * replaced), like mailsuite 2.3.3 and its IMAP backend's raw fetch, or\n * `\"\"` for an unknown key. The file is not parsed or re-serialized, so a\n * message Python's `email` package can parse but not generate (a `\\x1c`\n * in a nested header, deep multipart nesting) is returned as is.\n *\n * With `markRead` the file is renamed into `cur/` with the `S` flag (see\n * `Maildir.markRead`). With `maxSize`, a larger file throws\n * `EmailTooLargeError` without being read (not in mailsuite).\n */\n override async fetchMessage(\n messageId: MessageId,\n options: FetchMessageOptions = {},\n ): Promise<string> {\n const key = String(messageId);\n const maxSize = options.maxSize ?? 0;\n if (maxSize > 0) {\n const size = await this.activeFolder.size(key);\n if (size !== null && size > maxSize) {\n throw emailTooLargeError(size, maxSize);\n }\n }\n const content = await this.activeFolder.getBytes(key);\n if (content === null) {\n return '';\n }\n const text = utf8Decoder.decode(content);\n if (options.markRead) {\n await this.activeFolder.markRead(key);\n }\n return text;\n }\n\n override async deleteMessage(messageId: MessageId): Promise<void> {\n await this.activeFolder.remove(String(messageId));\n }\n\n /**\n * Move the message's file into the same subdirectory (`new`/`cur`) of\n * `folderName`, keeping its file name, flags and bytes, like mailsuite\n * 2.3.3 (it no longer re-serializes the message). Maildir++ folders live\n * under the same root, so this is a same-filesystem rename; a folder on\n * another mount fails with `EXDEV`. Throws {@link ExternalClashError}\n * rather than overwriting when the destination already holds a message\n * with the same key (including a move into the active folder itself).\n */\n override async moveMessage(\n messageId: MessageId,\n folderName: string,\n ): Promise<void> {\n const key = String(messageId);\n if (!(await this.activeFolder.has(key))) {\n return;\n }\n const destination = await this.getFolder(folderName);\n if (await destination.has(key)) {\n throw new ExternalClashError(\n `Folder ${folderName} already contains message ${key}`,\n );\n }\n await this.activeFolder.moveTo(key, destination);\n }\n\n override keepalive(): Promise<void> {\n return Promise.resolve();\n }\n\n override async watch(options: WatchOptions): Promise<void> {\n const { checkCallback, checkTimeout, configReloading, signal } = options;\n while (true) {\n if (signal?.aborted || configReloading?.()) {\n return;\n }\n try {\n await checkCallback(this);\n } catch (error) {\n logger.warning(`Maildir init error. ${errorMessage(error)}`);\n }\n if (configReloading?.()) {\n return;\n }\n try {\n await sleep(checkTimeout * MILLISECONDS_PER_SECOND, undefined, {\n signal,\n });\n } catch {\n // Aborted while sleeping.\n return;\n }\n }\n }\n\n override sendMessage(_options: SendMessageOptions): Promise<string | null> {\n return Promise.reject(\n new NotImplementedError(\n 'Maildir cannot send mail; use sendEmail from @tsdmarc/smtp',\n ),\n );\n }\n}\n","// Port of mailsuite.utils.create_email: builds the RFC 822 message that SMTP\n// and the Gmail API send. The message is assembled like Python's\n// `MIMEMultipart` + `MIMEText` + `MIMEApplication` (compat32 policy) with the\n// port of the `email` package, and `Message.as_string()` writes it, so the\n// output is Python's: headers are never folded, non-ASCII header values\n// become one RFC 2047 `utf-8` encoded word (Q or B, whichever is shorter),\n// body line endings become `\\n`, and a value with an embedded header\n// (a line break followed by a non-blank) throws `HeaderParseError`.\n\nimport { base64BodyEncode, formatParam } from './py-email/charset';\nimport { Message } from './py-email/message';\n\nexport interface CreateEmailOptions {\n messageFrom: string;\n messageTo?: string[] | null;\n messageCc?: string[] | null;\n subject?: string | null;\n messageHeaders?: Record<string, string> | null;\n /** `[filename, content]` pairs. */\n attachments?: [string, Uint8Array][] | null;\n plainMessage?: string | null;\n htmlMessage?: string | null;\n}\n\nconst NON_ASCII = /\\P{ASCII}/u;\nconst DAY_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] as const;\nconst MONTH_NAMES = [\n 'Jan',\n 'Feb',\n 'Mar',\n 'Apr',\n 'May',\n 'Jun',\n 'Jul',\n 'Aug',\n 'Sep',\n 'Oct',\n 'Nov',\n 'Dec',\n] as const;\n\nfunction pad2(value: number): string {\n return String(value).padStart(2, '0');\n}\n\n/** `email.utils.formatdate(localtime=True)`: RFC 2822 date in local time. */\nexport function formatDate(date: Date = new Date()): string {\n const offsetMinutes = -date.getTimezoneOffset();\n const sign = offsetMinutes < 0 ? '-' : '+';\n const absolute = Math.abs(offsetMinutes);\n const offset = `${sign}${pad2(Math.floor(absolute / 60))}${pad2(absolute % 60)}`;\n const day = DAY_NAMES[date.getDay()] ?? '';\n const month = MONTH_NAMES[date.getMonth()] ?? '';\n return `${day}, ${pad2(date.getDate())} ${month} ${date.getFullYear()} ${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())} ${offset}`;\n}\n\n/** `MIMEBase.__init__`: Content-Type (with `add_header` params) and MIME-Version. */\nfunction mimeBase(\n maintype: string,\n subtype: string,\n params: [string, string][],\n): Message {\n const message = new Message();\n const parts = [`${maintype}/${subtype}`];\n for (const [name, value] of params) {\n parts.push(formatParam(name, value));\n }\n message.set('Content-Type', parts.join('; '));\n message.set('MIME-Version', '1.0');\n return message;\n}\n\n/** `MIMEText(text, subtype)`: us-ascii when it encodes, else utf-8 (base64). */\nfunction mimeText(text: string, subtype: string): Message {\n const charset = NON_ASCII.test(text) ? 'utf-8' : 'us-ascii';\n const message = mimeBase('text', subtype, [['charset', charset]]);\n message.setPayload(text, charset);\n return message;\n}\n\n/** `MIMEApplication(data, Name=filename)` with `encode_base64`. */\nfunction mimeApplication(data: Uint8Array, filename: string): Message {\n const message = mimeBase('application', 'octet-stream', [['Name', filename]]);\n message.setPayload(base64BodyEncode(data));\n message.set('Content-Transfer-Encoding', 'base64');\n return message;\n}\n\n/**\n * Create an RFC 822 email message and return it as a string.\n *\n * Port of `mailsuite.utils.create_email`: a `multipart/mixed` message with\n * the plain and/or HTML body followed by one `application/octet-stream`\n * part per attachment.\n */\nexport function createEmail(options: CreateEmailOptions): string {\n const message = mimeBase('multipart', 'mixed', []);\n message.payload = [];\n message.set('From', options.messageFrom);\n if (options.messageTo && options.messageTo.length > 0) {\n message.set('To', options.messageTo.join(', '));\n }\n if (options.messageCc && options.messageCc.length > 0) {\n message.set('Cc', options.messageCc.join(', '));\n }\n message.set('Date', formatDate());\n if (options.subject) {\n message.set('Subject', options.subject);\n }\n for (const [name, value] of Object.entries(options.messageHeaders ?? {})) {\n message.set(name, value);\n }\n if (options.plainMessage !== null && options.plainMessage !== undefined) {\n message.attach(mimeText(options.plainMessage, 'plain'));\n }\n if (options.htmlMessage !== null && options.htmlMessage !== undefined) {\n message.attach(mimeText(options.htmlMessage, 'html'));\n }\n for (const [filename, payload] of options.attachments ?? []) {\n const part = mimeApplication(payload, filename);\n part.set('Content-Disposition', `attachment; filename=\"${filename}\"`);\n message.attach(part);\n }\n return message.asString();\n}\n"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {}