From 9bb2b33c55a9f705d5032028a0c79dbbc1647e06 Mon Sep 17 00:00:00 2001 From: NickOvt Date: Tue, 19 May 2026 10:31:36 +0300 Subject: [PATCH] Add jsdoc to funcs --- index.d.ts | 56 +++++++++++ lib/chunked-passthrough.d.ts | 46 +++++++++ lib/chunked-passthrough.js | 3 + lib/flowed-decoder.d.ts | 46 +++++++++ lib/flowed-decoder.js | 9 +- lib/headers.d.ts | 93 ++++++++++++++++++ lib/headers.js | 8 +- lib/message-joiner.d.ts | 44 +++++++++ lib/message-joiner.js | 6 ++ lib/message-splitter.d.ts | 46 +++++++++ lib/message-splitter.js | 15 ++- lib/mime-node.d.ts | 106 +++++++++++++++++++++ lib/mime-node.js | 7 +- lib/node-rewriter.d.ts | 87 +++++++++++++++++ lib/node-rewriter.js | 32 +++++-- lib/node-streamer.d.ts | 87 +++++++++++++++++ lib/node-streamer.js | 39 ++++++-- lib/types.d.ts | 180 +++++++++++++++++++++++++++++++++++ 18 files changed, 879 insertions(+), 31 deletions(-) diff --git a/index.d.ts b/index.d.ts index c4ee097..39c9c54 100644 --- a/index.d.ts +++ b/index.d.ts @@ -9,36 +9,92 @@ import Headers = require('./lib/headers'); import MimeNode = require('./lib/mime-node'); export { + /** Splits raw message bytes into MIME node and content chunks. */ Splitter, + + /** Joins MIME node and content chunks back into raw message bytes. */ Joiner, + + /** Rewrites body content for MIME nodes selected by a filter function. */ Rewriter, + + /** Streams decoded body content for MIME nodes selected by a filter function. */ Streamer, + + /** Buffers byte input and emits larger Buffer chunks. */ ChunkedPassthrough, + + /** Parses, mutates, and rebuilds message header blocks. */ Headers, + + /** Represents one parsed MIME node and its header/body metadata. */ MimeNode }; export type { + /** Value that is either present as `T` or explicitly unavailable as `false`. */ Maybe, + + /** Single item in an IMAP-style MIME part number. */ PartNumberItem, + + /** IMAP-style path to a MIME part. */ PartNumber, + + /** Options passed through to libmime instances. */ LibmimeOptions, + + /** Configuration for `Splitter` and MIME node parsing. */ SplitterOptions, + + /** Configuration for `ChunkedPassthrough`. */ ChunkedPassthroughOptions, + + /** Configuration for format=flowed decoding. */ FlowedDecoderOptions, + + /** Parsed raw header line with a normalized lookup key. */ HeaderLine, + + /** Decoded structured header value. */ DecodedHeader, + + /** MIME node shape emitted by `Splitter`. */ MimeNode, + + /** Data or body bytes emitted by `Splitter`. */ MessageChunk, + + /** Sentinel input used internally by rewriter/streamer transforms. */ EmptyChunk, + + /** Object emitted by `Splitter`. */ SplitterChunk, + + /** Object accepted by rewriter and streamer transforms. */ RewriterInput, + + /** Predicate used to select MIME nodes. */ FilterFunc, + + /** Error object that may include a Node-style string code. */ ErrorWithCode, + + /** Callback that resumes processing after a selected node stream ends. */ ContinueCallback, + + /** Content transform stream used for decoded or encoded node bodies. */ ContentStream, + + /** Decoder stream with an internal readable-state guard. */ DecoderStream, + + /** Internal splitter grouping state. */ SplitterGroup, + + /** Payload emitted with `Rewriter`'s `node` event. */ RewriterNode, + + /** Payload emitted with `Streamer`'s `node` event. */ StreamerNode } from './lib/types'; diff --git a/lib/chunked-passthrough.d.ts b/lib/chunked-passthrough.d.ts index d0b8576..4cd0e1e 100644 --- a/lib/chunked-passthrough.d.ts +++ b/lib/chunked-passthrough.d.ts @@ -1,12 +1,58 @@ import { Transform } from 'node:stream'; import type { ChunkedPassthroughOptions } from './types'; +/** Transform stream that buffers byte input and emits larger Buffer chunks. */ declare class ChunkedPassthrough extends Transform { + /** + * Creates a chunking passthrough transform that accepts Buffer input and emits Buffer chunks. + * + * @param options Optional chunk size configuration. + */ constructor(options?: ChunkedPassthroughOptions); + + /** + * Registers a listener for buffered byte chunks. + * + * @param event Event name. + * @param listener Receives each buffered Buffer chunk. + * @returns This passthrough instance. + */ on(event: 'data', listener: (data: Buffer) => void): this; + + /** + * Registers a one-time listener for the next buffered byte chunk. + * + * @param event Event name. + * @param listener Receives the next buffered Buffer chunk. + * @returns This passthrough instance. + */ once(event: 'data', listener: (data: Buffer) => void): this; + + /** + * Adds a listener for buffered byte chunks. + * + * @param event Event name. + * @param listener Receives each buffered Buffer chunk. + * @returns This passthrough instance. + */ addListener(event: 'data', listener: (data: Buffer) => void): this; + + /** + * Prepends a listener for buffered byte chunks. + * + * @param event Event name. + * @param listener Receives each buffered Buffer chunk. + * @returns This passthrough instance. + */ prependListener(event: 'data', listener: (data: Buffer) => void): this; + + /** + * Emits a buffered byte chunk. + * + * @param event Event name. + * @param data Buffer chunk to emit. + * @returns `true` when the event had listeners. + */ emit(event: 'data', data: Buffer): boolean; } diff --git a/lib/chunked-passthrough.js b/lib/chunked-passthrough.js index ef19116..129a640 100644 --- a/lib/chunked-passthrough.js +++ b/lib/chunked-passthrough.js @@ -2,6 +2,9 @@ const { Transform } = require('stream'); +/** + * Transform stream that buffers byte input and emits larger Buffer chunks. + */ class ChunkedPassthrough extends Transform { /** * @param {import('..').ChunkedPassthroughOptions} [options] diff --git a/lib/flowed-decoder.d.ts b/lib/flowed-decoder.d.ts index 0b9866b..a214deb 100644 --- a/lib/flowed-decoder.d.ts +++ b/lib/flowed-decoder.d.ts @@ -1,12 +1,58 @@ import { Transform } from 'node:stream'; import type { FlowedDecoderOptions } from './types'; +/** Transform stream that decodes `text/plain; format=flowed` content. */ declare class FlowedDecoder extends Transform { + /** + * Creates a flowed text decoder that accepts encoded text bytes and emits decoded Buffer chunks. + * + * @param config Optional flowed text and charset decoding settings. + */ constructor(config?: FlowedDecoderOptions); + + /** + * Registers a listener for decoded flowed text bytes. + * + * @param event Event name. + * @param listener Receives decoded Buffer chunks. + * @returns This decoder instance. + */ on(event: 'data', listener: (data: Buffer) => void): this; + + /** + * Registers a one-time listener for the next decoded flowed text chunk. + * + * @param event Event name. + * @param listener Receives the next decoded Buffer chunk. + * @returns This decoder instance. + */ once(event: 'data', listener: (data: Buffer) => void): this; + + /** + * Adds a listener for decoded flowed text bytes. + * + * @param event Event name. + * @param listener Receives decoded Buffer chunks. + * @returns This decoder instance. + */ addListener(event: 'data', listener: (data: Buffer) => void): this; + + /** + * Prepends a listener for decoded flowed text bytes. + * + * @param event Event name. + * @param listener Receives decoded Buffer chunks. + * @returns This decoder instance. + */ prependListener(event: 'data', listener: (data: Buffer) => void): this; + + /** + * Emits a decoded flowed text chunk. + * + * @param event Event name. + * @param data Buffer chunk to emit. + * @returns `true` when the event had listeners. + */ emit(event: 'data', data: Buffer): boolean; } diff --git a/lib/flowed-decoder.js b/lib/flowed-decoder.js index 87ba603..96bdb86 100644 --- a/lib/flowed-decoder.js +++ b/lib/flowed-decoder.js @@ -1,6 +1,6 @@ 'use strict'; -// Helper class to rewrite nodes with specific mime type +// Helper class to decode format=flowed text nodes const Transform = require('stream').Transform; const libmime = require('libmime'); @@ -10,14 +10,13 @@ const libmime = require('libmime'); const Libmime = /** @type {any} */ (libmime.Libmime); /** - * Really bad "stream" transform to parse format=flowed content + * Transform stream that decodes text/plain format=flowed content. * - * @constructor - * @param {FlowedDecoderOptions} [config] + * @param {FlowedDecoderOptions} [config] Flowed text and charset decoding settings. */ class FlowedDecoder extends Transform { /** - * @param {FlowedDecoderOptions} [config] + * @param {FlowedDecoderOptions} [config] Flowed text and charset decoding settings. */ constructor(config) { super(); diff --git a/lib/headers.d.ts b/lib/headers.d.ts index 8cc63a4..dc63310 100644 --- a/lib/headers.d.ts +++ b/lib/headers.d.ts @@ -1,23 +1,116 @@ import type { DecodedHeader, HeaderLine, LibmimeOptions } from './types'; +/** Mutable parser and builder for RFC-style message header blocks. */ declare class Headers { + /** Whether header lines have been modified after construction. */ changed: boolean; + + /** Original unparsed header source, or `false` when constructed from parsed lines. */ headers: string | Buffer | false; + + /** Whether `headers` has been parsed into `lines`. */ parsed: boolean; + + /** Parsed header lines, or `false` until parsing occurs. */ lines: HeaderLine[] | false; + + /** MBOX `From ` prefix line, or `false` when absent. */ mbox: string | false; + + /** HTTP request prefix line, or `false` when absent. */ http: string | false; + /** + * Creates a mutable header collection. + * + * @param headers Raw header bytes/string, already parsed header lines, or `false` for an empty collection. + * @param config Optional libmime configuration. + */ constructor(headers?: string | Buffer | HeaderLine[] | false, config?: LibmimeOptions); + + /** + * Checks whether at least one header with the requested key exists. + * + * @param key Header field name to find, case-insensitively. + * @returns `true` when the header exists. + */ hasHeader(key: string): boolean; + + /** + * Gets all raw header lines for a key. + * + * @param key Header field name to find, case-insensitively. + * @returns Full decoded header lines, including field names. + */ get(key: string): string[]; + + /** + * Gets all decoded structured header values for a key. + * + * @param key Header field name to decode, case-insensitively. + * @returns Decoded header entries with key and value fields. + */ getDecoded(key: string): DecodedHeader[]; + + /** + * Gets the first decoded header value for a key. + * + * @param key Header field name to find, case-insensitively. + * @returns Trimmed decoded value, or an empty string when the header is absent. + */ getFirst(key: string): string; + + /** + * Gets the mutable parsed header list. + * + * @returns Parsed header lines in message order. + */ getList(): HeaderLine[]; + + /** + * Adds a folded header line. + * + * @param key Header field name to add. + * @param value Header value; `undefined` leaves the collection unchanged. + * @param index Insertion index, where omitted or less than 1 inserts at the top. + * @returns Nothing. + */ add(key: string, value?: string | number | Buffer, index?: number): void; + + /** + * Adds a preformatted header line. + * + * @param key Header field name used for normalized lookup. + * @param line Full header line to insert; falsy values leave the collection unchanged. + * @param index Insertion index, where omitted or less than 1 inserts at the top. + * @returns Nothing. + */ addFormatted(key: string, line?: string | Buffer | false, index?: number): void; + + /** + * Removes all headers matching a key. + * + * @param key Header field name to remove, case-insensitively. + * @returns Nothing. + */ remove(key: string): void; + + /** + * Replaces matching headers with a new folded header value. + * + * @param key Header field name to update. + * @param value Header value to write; `undefined` removes matching values without adding a replacement. + * @param relativeIndex Optional zero-based index among headers with the same key. + * @returns Nothing. + */ update(key: string, value?: string | number | Buffer, relativeIndex?: number): void; + + /** + * Builds a raw header block. + * + * @param lineEnd Line ending to use when rebuilding changed headers; defaults to CRLF. + * @returns Header bytes ending with an empty header/body separator line. + */ build(lineEnd?: string | false): Buffer; } diff --git a/lib/headers.js b/lib/headers.js index 6860c01..b9f3f4e 100644 --- a/lib/headers.js +++ b/lib/headers.js @@ -9,13 +9,13 @@ const libmime = require('libmime'); const Libmime = /** @type {any} */ (libmime.Libmime); /** - * Class Headers to parse and handle message headers. Headers instance allows to - * check existing, delete or add new headers + * Parses and builds message headers. A Headers instance allows callers to + * inspect, delete, update, and add header lines. */ class Headers { /** - * @param {string | Buffer | HeaderLine[] | false} [headers] - * @param {LibmimeOptions} [config] + * @param {string | Buffer | HeaderLine[] | false} [headers] Raw header source or already parsed lines. + * @param {LibmimeOptions} [config] Optional libmime configuration. */ constructor(headers, config) { config = config || {}; diff --git a/lib/message-joiner.d.ts b/lib/message-joiner.d.ts index 792d1c1..37cd839 100644 --- a/lib/message-joiner.d.ts +++ b/lib/message-joiner.d.ts @@ -1,11 +1,55 @@ import { Transform } from 'node:stream'; +/** Transform stream that joins splitter objects back into raw email bytes. */ declare class MessageJoiner extends Transform { + /** + * Creates a joiner that accepts splitter objects and emits Buffer chunks. + */ constructor(); + + /** + * Registers a listener for generated message bytes. + * + * @param event Event name. + * @param listener Receives each generated Buffer chunk. + * @returns This joiner instance. + */ on(event: 'data', listener: (data: Buffer) => void): this; + + /** + * Registers a one-time listener for generated message bytes. + * + * @param event Event name. + * @param listener Receives the next generated Buffer chunk. + * @returns This joiner instance. + */ once(event: 'data', listener: (data: Buffer) => void): this; + + /** + * Adds a listener for generated message bytes. + * + * @param event Event name. + * @param listener Receives each generated Buffer chunk. + * @returns This joiner instance. + */ addListener(event: 'data', listener: (data: Buffer) => void): this; + + /** + * Prepends a listener for generated message bytes. + * + * @param event Event name. + * @param listener Receives each generated Buffer chunk. + * @returns This joiner instance. + */ prependListener(event: 'data', listener: (data: Buffer) => void): this; + + /** + * Emits a generated message byte chunk. + * + * @param event Event name. + * @param data Buffer chunk to emit. + * @returns `true` when the event had listeners. + */ emit(event: 'data', data: Buffer): boolean; } diff --git a/lib/message-joiner.js b/lib/message-joiner.js index 6446c86..8d6dff6 100644 --- a/lib/message-joiner.js +++ b/lib/message-joiner.js @@ -4,7 +4,13 @@ const Transform = require('stream').Transform; /** @typedef {import('..').SplitterChunk} SplitterChunk */ +/** + * Transform stream that joins splitter objects back into raw message bytes. + */ class MessageJoiner extends Transform { + /** + * Creates a joiner that accepts splitter objects and emits Buffer chunks. + */ constructor() { let options = { readableObjectMode: false, diff --git a/lib/message-splitter.d.ts b/lib/message-splitter.d.ts index 39931b5..ea2d428 100644 --- a/lib/message-splitter.d.ts +++ b/lib/message-splitter.d.ts @@ -1,12 +1,58 @@ import { Transform } from 'node:stream'; import type { SplitterChunk, SplitterOptions } from './types'; +/** Transform stream that splits raw email bytes into MIME node and content chunks. */ declare class MessageSplitter extends Transform { + /** + * Creates a splitter that accepts Buffer input and emits `SplitterChunk` objects. + * + * @param config Optional parser limits and embedded-message behavior. + */ constructor(config?: SplitterOptions); + + /** + * Registers a listener for parsed splitter chunks. + * + * @param event Event name. + * @param listener Receives each parsed MIME node, data chunk, or body chunk. + * @returns This splitter instance. + */ on(event: 'data', listener: (data: SplitterChunk) => void): this; + + /** + * Registers a one-time listener for the next parsed splitter chunk. + * + * @param event Event name. + * @param listener Receives the next parsed MIME node, data chunk, or body chunk. + * @returns This splitter instance. + */ once(event: 'data', listener: (data: SplitterChunk) => void): this; + + /** + * Adds a listener for parsed splitter chunks. + * + * @param event Event name. + * @param listener Receives each parsed MIME node, data chunk, or body chunk. + * @returns This splitter instance. + */ addListener(event: 'data', listener: (data: SplitterChunk) => void): this; + + /** + * Prepends a listener for parsed splitter chunks. + * + * @param event Event name. + * @param listener Receives each parsed MIME node, data chunk, or body chunk. + * @returns This splitter instance. + */ prependListener(event: 'data', listener: (data: SplitterChunk) => void): this; + + /** + * Emits a parsed splitter chunk. + * + * @param event Event name. + * @param data MIME node, data chunk, or body chunk to emit. + * @returns `true` when the event had listeners. + */ emit(event: 'data', data: SplitterChunk): boolean; } diff --git a/lib/message-splitter.js b/lib/message-splitter.js index aee18f1..0cc2046 100644 --- a/lib/message-splitter.js +++ b/lib/message-splitter.js @@ -16,6 +16,9 @@ const MAX_CHILD_NODES = 1000; const HEAD = 0x01; const BODY = 0x02; +/** + * Transform stream that splits raw email bytes into MIME node and content chunks. + */ class MessageSplitter extends Transform { /** * @param {SplitterOptions} [config] @@ -59,7 +62,12 @@ class MessageSplitter extends Transform { let groupstart = this.line ? -this.line.length : 0; let groupend = 0; - /** @param {MessageChunk} data */ + /** + * Removes a pending line break from body data that belongs to a following boundary. + * + * @param {MessageChunk} data Body chunk to adjust in place. + * @returns {void} + */ let checkTrailingLinebreak = data => { if (data.type === 'body' && data.node.parentNode && data.value && data.value.length) { if (data.value[data.value.length - 1] === 0x0a) { @@ -88,6 +96,11 @@ class MessageSplitter extends Transform { } }; + /** + * Iterates the current input chunk line by line and emits parsed groups. + * + * @returns {void} + */ let iterateData = () => { for (let len = chunk.length; i < len; i++) { // find next diff --git a/lib/mime-node.d.ts b/lib/mime-node.d.ts index 9e71bd1..cdf5f9b 100644 --- a/lib/mime-node.d.ts +++ b/lib/mime-node.d.ts @@ -1,37 +1,143 @@ import type { ContentStream, MimeNode as MimeNodeShape, PartNumber, PartNumberItem, SplitterOptions } from './types'; import type Headers = require('./headers'); +/** Parsed MIME node with mutable headers and content encoding helpers. */ declare class MimeNode implements MimeNodeShape { + /** Discriminator identifying this chunk as a MIME node. */ type: 'node'; + + /** Whether this node is the root message node. */ root: boolean; + + /** Parent MIME node, or `false` for the root node. */ parentNode: MimeNodeShape | false; + + /** Boundary used by this multipart node, or `false` when not multipart. */ _boundary: Buffer | false; + + /** Boundary inherited from the parent multipart node, or `false` when absent. */ _parentBoundary: Buffer | false; + + /** Length, in bytes, of the raw header block collected for this node. */ _headerlen: number; + + /** Multipart subtype such as `mixed` or `alternative`, or `false` for leaf nodes. */ multipart: string | false; + + /** Content-Transfer-Encoding value, normalized to lower case, or `false` when absent. */ encoding: string | false; + + /** Parsed and mutable header collection, available after headers are parsed. */ headers: Headers | false; + + /** MIME content type such as `text/plain`, or `false` when unavailable. */ contentType: string | false; + + /** Charset parameter from Content-Type, or `false` when absent. */ charset: string | false; + + /** Content-Disposition value such as `inline` or `attachment`, or `false` when absent. */ disposition: string | false; + + /** Decoded filename from Content-Disposition or Content-Type parameters, or `false` when absent. */ filename: string | false; + + /** Whether this node is `text/*` with `format=flowed`. */ flowed: boolean; + + /** Whether flowed text uses `delsp=yes`. */ delSp: boolean; + + /** Splitter configuration used when parsing this node. */ config: SplitterOptions; + + /** Resolved IMAP-style part number for this node, or `false` before resolution. */ partNr: PartNumber | false; + + /** Number of child part numbers allocated by this node. */ childPartNumbers: number; + + /** Whether this node's content type is `message/rfc822`. */ rfc822: boolean; + + /** Whether an embedded `message/rfc822` node was parsed as a nested message. */ messageNode?: boolean; + /** + * Creates a MIME node with empty header state. + * + * @param parentNode Parent node, or `false`/omitted for the root node. + * @param config Optional splitter and libmime configuration. + */ constructor(parentNode?: MimeNodeShape | false, config?: SplitterOptions); + + /** + * Builds the next child part number for this node. + * + * @param provided Optional explicit part number item to append. + * @returns Resolved MIME part number. + */ getPartNr(provided?: PartNumberItem): PartNumber; + + /** + * Appends one raw header line to this node while parsing. + * + * @param line Raw header line bytes; falsy values are ignored. + * @returns Nothing. + */ addHeaderChunk(line?: Buffer | false): void; + + /** + * Parses collected header bytes and populates MIME metadata fields. + * + * @returns Nothing. + */ parseHeaders(): void; + + /** + * Builds this node's header block. + * + * @returns Header bytes ending with an empty header/body separator line. + */ getHeaders(): Buffer; + + /** + * Sets or updates the Content-Type header value. + * + * @param contentType MIME content type to set; falsy keeps the current type. + * @returns Nothing. + */ setContentType(contentType?: string | false): void; + + /** + * Sets, updates, or removes the Content-Type charset parameter. + * + * @param charset Charset to set; falsy removes it when possible. + * @returns Nothing. + */ setCharset(charset?: string | false): void; + + /** + * Sets, updates, or removes the filename parameter. + * + * @param filename Filename to set; falsy removes it when possible. + * @returns Nothing. + */ setFilename(filename?: string | false): void; + + /** + * Creates a decoder stream for this node's transfer encoding. + * + * @returns Transform stream that outputs decoded content bytes. + */ getDecoder(): ContentStream; + + /** + * Creates an encoder stream and updates the Content-Transfer-Encoding header when needed. + * + * @param encoding Target transfer encoding; defaults to the node's current encoding. + * @returns Transform stream that outputs encoded content bytes. + */ getEncoder(encoding?: string | false): ContentStream; } diff --git a/lib/mime-node.js b/lib/mime-node.js index 821b96b..630535b 100644 --- a/lib/mime-node.js +++ b/lib/mime-node.js @@ -18,10 +18,13 @@ const pathlib = require('path'); const Libmime = /** @type {any} */ (libmime.Libmime); +/** + * Parsed MIME node with mutable headers and transfer-encoding helpers. + */ class MimeNode { /** - * @param {MimeNodeType | false} parentNode - * @param {SplitterOptions} [config] + * @param {MimeNodeType | false} parentNode Parent node, or false for the root node. + * @param {SplitterOptions} [config] Splitter and libmime configuration. */ constructor(parentNode, config) { /** @type {'node'} */ diff --git a/lib/node-rewriter.d.ts b/lib/node-rewriter.d.ts index f10a3b0..be028f4 100644 --- a/lib/node-rewriter.d.ts +++ b/lib/node-rewriter.d.ts @@ -1,17 +1,104 @@ import { Transform } from 'node:stream'; import type { FilterFunc, RewriterNode, SplitterChunk } from './types'; +/** Transform stream that replaces the body content of selected MIME nodes. */ declare class NodeRewriter extends Transform { + /** + * Creates a node rewriter that accepts splitter chunks and emits rewritten splitter chunks. + * + * @param filterFunc Predicate that selects MIME nodes to rewrite. + * @param rewriteAction Optional compatibility hook stored on the instance; consumers usually handle the `node` event. + */ constructor(filterFunc: FilterFunc, rewriteAction?: Function); + + /** + * Registers a listener for rewritten splitter chunks. + * + * @param event Event name. + * @param listener Receives each outgoing MIME node, data chunk, or body chunk. + * @returns This rewriter instance. + */ on(event: 'data', listener: (data: SplitterChunk) => void): this; + + /** + * Registers a listener for selected nodes. + * + * @param event Event name. + * @param listener Receives decoder and encoder streams for a selected node. + * @returns This rewriter instance. + */ on(event: 'node', listener: (data: RewriterNode) => void): this; + + /** + * Registers a one-time listener for the next rewritten splitter chunk. + * + * @param event Event name. + * @param listener Receives the next outgoing MIME node, data chunk, or body chunk. + * @returns This rewriter instance. + */ once(event: 'data', listener: (data: SplitterChunk) => void): this; + + /** + * Registers a one-time listener for the next selected node. + * + * @param event Event name. + * @param listener Receives decoder and encoder streams for the next selected node. + * @returns This rewriter instance. + */ once(event: 'node', listener: (data: RewriterNode) => void): this; + + /** + * Adds a listener for rewritten splitter chunks. + * + * @param event Event name. + * @param listener Receives each outgoing MIME node, data chunk, or body chunk. + * @returns This rewriter instance. + */ addListener(event: 'data', listener: (data: SplitterChunk) => void): this; + + /** + * Adds a listener for selected nodes. + * + * @param event Event name. + * @param listener Receives decoder and encoder streams for a selected node. + * @returns This rewriter instance. + */ addListener(event: 'node', listener: (data: RewriterNode) => void): this; + + /** + * Prepends a listener for rewritten splitter chunks. + * + * @param event Event name. + * @param listener Receives each outgoing MIME node, data chunk, or body chunk. + * @returns This rewriter instance. + */ prependListener(event: 'data', listener: (data: SplitterChunk) => void): this; + + /** + * Prepends a listener for selected nodes. + * + * @param event Event name. + * @param listener Receives decoder and encoder streams for a selected node. + * @returns This rewriter instance. + */ prependListener(event: 'node', listener: (data: RewriterNode) => void): this; + + /** + * Emits a rewritten splitter chunk. + * + * @param event Event name. + * @param data MIME node, data chunk, or body chunk to emit. + * @returns `true` when the event had listeners. + */ emit(event: 'data', data: SplitterChunk): boolean; + + /** + * Emits a selected-node payload. + * + * @param event Event name. + * @param data Selected node payload containing decoder and encoder streams. + * @returns `true` when the event had listeners. + */ emit(event: 'node', data: RewriterNode): boolean; } diff --git a/lib/node-rewriter.js b/lib/node-rewriter.js index afeabc4..414986a 100644 --- a/lib/node-rewriter.js +++ b/lib/node-rewriter.js @@ -14,16 +14,16 @@ const FlowedDecoder = require('./flowed-decoder'); /** @typedef {import('..').ContinueCallback} ContinueCallback */ /** - * NodeRewriter Transform stream. Updates content for all nodes with specified mime type + * NodeRewriter Transform stream. Updates content for all nodes selected by the + * filter function. * - * @constructor - * @param {FilterFunc} filterFunc Function to select nodes to rewrite - * @param {Function} [rewriteAction] Function to run with the node content + * @param {FilterFunc} filterFunc Function that receives a MIME node and returns true to rewrite it. + * @param {Function} [rewriteAction] Optional compatibility hook stored on the instance. */ class NodeRewriter extends Transform { /** - * @param {FilterFunc} filterFunc - * @param {Function} [rewriteAction] + * @param {FilterFunc} filterFunc Function that receives a MIME node and returns true to rewrite it. + * @param {Function} [rewriteAction] Optional compatibility hook stored on the instance. */ constructor(filterFunc, rewriteAction) { let options = { @@ -128,6 +128,11 @@ class NodeRewriter extends Transform { let firstChunk = true; decoder.$reading = false; + /** + * Reads encoded replacement bytes and forwards them as body chunks. + * + * @returns {void | NodeJS.Immediate} + */ let readFromEncoder = () => { decoder.$reading = true; @@ -202,9 +207,18 @@ class NodeRewriter extends Transform { delSp: node.delSp, encoding: node.encoding || false }); - flowDecoder.on('error', /** @param {Error} err */ err => { - decoder.emit('error', err); - }); + flowDecoder.on( + 'error', + /** + * Forwards flowed decoder errors to the replacement decoder. + * + * @param {Error} err Decoder error to forward. + * @returns {void} + */ + err => { + decoder.emit('error', err); + } + ); flowDecoder.pipe(decoder); // we don't know what kind of data we are going to get, does it comply with the diff --git a/lib/node-streamer.d.ts b/lib/node-streamer.d.ts index fba2535..5ddadf6 100644 --- a/lib/node-streamer.d.ts +++ b/lib/node-streamer.d.ts @@ -1,17 +1,104 @@ import { Transform } from 'node:stream'; import type { FilterFunc, SplitterChunk, StreamerNode } from './types'; +/** Transform stream that exposes decoded body streams for selected MIME nodes without replacing them. */ declare class NodeStreamer extends Transform { + /** + * Creates a node streamer that accepts splitter chunks and passes them through unchanged. + * + * @param filterFunc Predicate that selects MIME nodes to stream. + * @param streamAction Optional compatibility hook stored on the instance; consumers usually handle the `node` event. + */ constructor(filterFunc: FilterFunc, streamAction?: Function); + + /** + * Registers a listener for passed-through splitter chunks. + * + * @param event Event name. + * @param listener Receives each outgoing MIME node, data chunk, or body chunk. + * @returns This streamer instance. + */ on(event: 'data', listener: (data: SplitterChunk) => void): this; + + /** + * Registers a listener for selected nodes. + * + * @param event Event name. + * @param listener Receives a decoder stream and completion callback for a selected node. + * @returns This streamer instance. + */ on(event: 'node', listener: (data: StreamerNode) => void): this; + + /** + * Registers a one-time listener for the next passed-through splitter chunk. + * + * @param event Event name. + * @param listener Receives the next outgoing MIME node, data chunk, or body chunk. + * @returns This streamer instance. + */ once(event: 'data', listener: (data: SplitterChunk) => void): this; + + /** + * Registers a one-time listener for the next selected node. + * + * @param event Event name. + * @param listener Receives a decoder stream and completion callback for the next selected node. + * @returns This streamer instance. + */ once(event: 'node', listener: (data: StreamerNode) => void): this; + + /** + * Adds a listener for passed-through splitter chunks. + * + * @param event Event name. + * @param listener Receives each outgoing MIME node, data chunk, or body chunk. + * @returns This streamer instance. + */ addListener(event: 'data', listener: (data: SplitterChunk) => void): this; + + /** + * Adds a listener for selected nodes. + * + * @param event Event name. + * @param listener Receives a decoder stream and completion callback for a selected node. + * @returns This streamer instance. + */ addListener(event: 'node', listener: (data: StreamerNode) => void): this; + + /** + * Prepends a listener for passed-through splitter chunks. + * + * @param event Event name. + * @param listener Receives each outgoing MIME node, data chunk, or body chunk. + * @returns This streamer instance. + */ prependListener(event: 'data', listener: (data: SplitterChunk) => void): this; + + /** + * Prepends a listener for selected nodes. + * + * @param event Event name. + * @param listener Receives a decoder stream and completion callback for a selected node. + * @returns This streamer instance. + */ prependListener(event: 'node', listener: (data: StreamerNode) => void): this; + + /** + * Emits a passed-through splitter chunk. + * + * @param event Event name. + * @param data MIME node, data chunk, or body chunk to emit. + * @returns `true` when the event had listeners. + */ emit(event: 'data', data: SplitterChunk): boolean; + + /** + * Emits a selected-node payload. + * + * @param event Event name. + * @param data Selected node payload containing decoder stream and completion callback. + * @returns `true` when the event had listeners. + */ emit(event: 'node', data: StreamerNode): boolean; } diff --git a/lib/node-streamer.js b/lib/node-streamer.js index fc22376..75b2353 100644 --- a/lib/node-streamer.js +++ b/lib/node-streamer.js @@ -1,6 +1,6 @@ 'use strict'; -// Helper class to rewrite nodes with specific mime type +// Helper class to stream selected nodes by MIME metadata const Transform = require('stream').Transform; const FlowedDecoder = require('./flowed-decoder'); @@ -13,16 +13,16 @@ const FlowedDecoder = require('./flowed-decoder'); /** @typedef {import('..').ContinueCallback} ContinueCallback */ /** - * NodeRewriter Transform stream. Updates content for all nodes with specified mime type + * NodeStreamer Transform stream. Exposes decoded content for nodes selected by + * the filter function while passing the original message through unchanged. * - * @constructor - * @param {FilterFunc} filterFunc Function to select nodes to stream - * @param {Function} [streamAction] Function to run with the node content + * @param {FilterFunc} filterFunc Function that receives a MIME node and returns true to stream it. + * @param {Function} [streamAction] Optional compatibility hook stored on the instance. */ class NodeStreamer extends Transform { /** - * @param {FilterFunc} filterFunc - * @param {Function} [streamAction] + * @param {FilterFunc} filterFunc Function that receives a MIME node and returns true to stream it. + * @param {Function} [streamAction] Optional compatibility hook stored on the instance. */ constructor(filterFunc, streamAction) { let options = { @@ -89,6 +89,11 @@ class NodeStreamer extends Transform { // the parsed data is completely processed, so we store a reference to the // continue callback + /** + * Resumes processing with the first chunk after the streamed node. + * + * @returns {void} + */ let doContinue = () => { this.continue = false; this.decoder = false; @@ -130,15 +135,29 @@ class NodeStreamer extends Transform { decoder = new FlowedDecoder({ delSp: node.delSp }); - flowDecoder.on('error', /** @param {Error} err */ err => { - decoder.emit('error', err); - }); + flowDecoder.on( + 'error', + /** + * Forwards flowed decoder errors to the output decoder. + * + * @param {Error} err Decoder error to forward. + * @returns {void} + */ + err => { + decoder.emit('error', err); + } + ); flowDecoder.pipe(decoder); } return { node, decoder, + /** + * Marks the selected node stream as consumed so the passthrough can continue. + * + * @returns {void} + */ done: () => { if (typeof this.continue === 'function') { // called once input stream is processed diff --git a/lib/types.d.ts b/lib/types.d.ts index f882cc4..02b0d42 100644 --- a/lib/types.d.ts +++ b/lib/types.d.ts @@ -1,105 +1,285 @@ import type { PassThrough, Transform } from 'node:stream'; import type Headers = require('./headers'); +/** Value that is either present as `T` or explicitly unavailable as `false`. */ export type Maybe = T | false; + +/** Single item in an IMAP-style MIME part number. */ export type PartNumberItem = number | 'TEXT'; + +/** IMAP-style path to a MIME part, for example `[1, 2, 'TEXT']`. */ export type PartNumber = PartNumberItem[]; +/** Options passed through to libmime instances created by this package. */ export interface LibmimeOptions { + /** Optional iconv-compatible implementation used by libmime for charset conversion. */ Iconv?: unknown; } +/** Configuration for `MessageSplitter` and MIME node parsing. */ export interface SplitterOptions extends LibmimeOptions { + /** Treat `message/rfc822` parts as leaf nodes instead of parsing embedded messages. */ ignoreEmbedded?: boolean; + + /** Parse embedded messages as inline unless their disposition is `attachment`. */ defaultInlineEmbedded?: boolean; + + /** Maximum header block size, in bytes, allowed for a single MIME node. */ maxHeadSize?: number; + + /** Maximum number of MIME child nodes accepted before parsing fails. */ maxChildNodes?: number; } +/** Configuration for `ChunkedPassthrough`. */ export interface ChunkedPassthroughOptions { + /** Buffered byte threshold for non-final chunks. Defaults to 64 KiB. */ chunkSize?: number; } +/** Configuration for `FlowedDecoder`. */ export interface FlowedDecoderOptions extends LibmimeOptions { + /** Whether format=flowed uses RFC 3676 `DelSp=yes` space deletion semantics. */ delSp?: boolean; + + /** Source Content-Transfer-Encoding hint used during format=flowed handling. */ encoding?: string | false; } +/** Parsed raw header line with a normalized lookup key. */ export interface HeaderLine { + /** Lower-case header key used for comparisons and lookups. */ key: string; + + /** Full header line, including the original field name, value, and any folded continuations. */ line: string; } +/** Decoded structured header value returned by libmime. */ export interface DecodedHeader { + /** Header key returned by libmime for the decoded value. */ key: string; + + /** Unicode decoded header value. */ value: string; } +/** MIME node emitted by `MessageSplitter` and accepted by `MessageJoiner`. */ export interface MimeNode { + /** Discriminator identifying this chunk as a MIME node. */ type: 'node'; + + /** Whether this node is the root message node. */ root: boolean; + + /** Parent MIME node, or `false` for the root node. */ parentNode: MimeNode | false; + + /** Boundary used by this multipart node, or `false` when not multipart. */ _boundary: Buffer | false; + + /** Boundary inherited from the parent multipart node, or `false` when absent. */ _parentBoundary: Buffer | false; + + /** Length, in bytes, of the raw header block collected for this node. */ _headerlen: number; + + /** Multipart subtype such as `mixed` or `alternative`, or `false` for leaf nodes. */ multipart: string | false; + + /** Content-Transfer-Encoding value, normalized to lower case, or `false` when absent. */ encoding: string | false; + + /** Parsed and mutable header collection, available after headers are parsed. */ headers: Headers | false; + + /** MIME content type such as `text/plain`, or `false` when unavailable. */ contentType: string | false; + + /** Charset parameter from Content-Type, or `false` when absent. */ charset: string | false; + + /** Content-Disposition value such as `inline` or `attachment`, or `false` when absent. */ disposition: string | false; + + /** Decoded filename from Content-Disposition or Content-Type parameters, or `false` when absent. */ filename: string | false; + + /** Whether this node is `text/*` with `format=flowed`. */ flowed: boolean; + + /** Whether flowed text uses `delsp=yes`. */ delSp: boolean; + + /** Splitter configuration used when parsing this node. */ config: SplitterOptions; + + /** Resolved IMAP-style part number for this node, or `false` before resolution. */ partNr: PartNumber | false; + + /** Number of child part numbers allocated by this node. */ childPartNumbers: number; + + /** Whether this node's content type is `message/rfc822`. */ rfc822: boolean; + + /** Whether an embedded `message/rfc822` node was parsed as a nested message. */ messageNode?: boolean; + /** + * Builds the next child part number for this node. + * + * @param provided Optional explicit part number item to append. + * @returns Resolved MIME part number. + */ getPartNr(provided?: PartNumberItem): PartNumber; + + /** + * Appends one raw header line to this node while parsing. + * + * @param line Raw header line bytes; falsy values are ignored. + * @returns Nothing. + */ addHeaderChunk(line?: Buffer | false): void; + + /** + * Parses collected header bytes and populates MIME metadata fields. + * + * @returns Nothing. + */ parseHeaders(): void; + + /** + * Builds this node's header block. + * + * @returns Header bytes ending with an empty header/body separator line. + */ getHeaders(): Buffer; + + /** + * Sets or updates the Content-Type header value. + * + * @param contentType MIME content type to set; falsy keeps the current type. + * @returns Nothing. + */ setContentType(contentType?: string | false): void; + + /** + * Sets, updates, or removes the Content-Type charset parameter. + * + * @param charset Charset to set; falsy removes it when possible. + * @returns Nothing. + */ setCharset(charset?: string | false): void; + + /** + * Sets, updates, or removes the filename parameter. + * + * @param filename Filename to set; falsy removes it when possible. + * @returns Nothing. + */ setFilename(filename?: string | false): void; + + /** + * Creates a decoder stream for this node's transfer encoding. + * + * @returns Transform stream that outputs decoded content bytes. + */ getDecoder(): Transform | PassThrough; + + /** + * Creates an encoder stream and updates the Content-Transfer-Encoding header when needed. + * + * @param encoding Target transfer encoding; defaults to the node's current encoding. + * @returns Transform stream that outputs encoded content bytes. + */ getEncoder(encoding?: string | false): Transform | PassThrough; } +/** Data or body bytes emitted by `MessageSplitter`. */ export interface MessageChunk { + /** MIME node that owns or precedes this chunk. */ node: MimeNode; + + /** Chunk kind: multipart structure bytes (`data`) or leaf content bytes (`body`). */ type: 'data' | 'body'; + + /** Raw chunk bytes. */ value: Buffer; } +/** Sentinel input used internally to finish a pending rewriter or streamer node. */ export interface EmptyChunk { + /** Discriminator for an empty control chunk. */ type: 'none'; } +/** Object emitted by `MessageSplitter`: either a MIME node or a data/body byte chunk. */ export type SplitterChunk = MimeNode | MessageChunk; + +/** Object accepted by rewriter and streamer transforms. */ export type RewriterInput = SplitterChunk | EmptyChunk; + +/** + * Predicate used to select MIME nodes. + * + * @param node MIME node being inspected. + * @returns `true` to process the node, otherwise `false`. + */ export type FilterFunc = (node: MimeNode) => boolean; + +/** Error object that may include a Node-style string error code. */ export type ErrorWithCode = Error & { code?: string }; + +/** + * Callback that resumes processing after a selected node stream has ended. + * + * @returns Nothing. + */ export type ContinueCallback = () => void; + +/** Content transform stream used for decoded or encoded node bodies. */ export type ContentStream = Transform | PassThrough; + +/** Decoder stream with an internal readable-state guard used by rewriter/streamer. */ export type DecoderStream = ContentStream & { $reading?: boolean }; +/** Internal grouping state used while splitter coalesces adjacent chunks. */ export interface SplitterGroup { + /** MIME node associated with the group, when one exists. */ node?: MimeNode; + + /** Group kind currently being accumulated. */ type: 'none' | 'node' | 'data' | 'body'; + + /** Buffered raw bytes for `data` or `body` groups. */ value?: Buffer; } +/** Payload emitted with `NodeRewriter`'s `node` event. */ export interface RewriterNode { + /** Selected MIME node whose body can be rewritten. */ node: MimeNode; + + /** Stream that yields decoded original body bytes. */ decoder: Transform; + + /** Stream that accepts replacement decoded bytes and emits properly encoded body bytes. */ encoder: Transform; } +/** Payload emitted with `NodeStreamer`'s `node` event. */ export interface StreamerNode { + /** Selected MIME node whose body is being streamed. */ node: MimeNode; + + /** Stream that yields decoded original body bytes. */ decoder: Transform; + + /** + * Signals that the consumer has finished reading the selected node. + * + * @returns Nothing. + */ done: () => void; }