@zone-eu/mailsplit
Advanced tools
+81
-10
@@ -133,3 +133,4 @@ 'use strict'; | ||
| value = value.toString('binary'); | ||
| this.addFormatted(key, this.libmime.foldLines(key + ': ' + value.replace(/\r?\n/g, ''), 76, false), index); | ||
| // a header value may not contain line breaks of its own, folding is added by foldLines | ||
| this.addFormatted(key, this.libmime.foldLines(key + ': ' + value.replace(/[\r\n]/g, ''), 76, false), index); | ||
| } | ||
@@ -159,2 +160,9 @@ | ||
| // every header insertion runs through here, so this is where a value or a key | ||
| // built from untrusted input is stopped from injecting an extra header line | ||
| line = this._normalizeInsertedLine(line); | ||
| if (!line) { | ||
| return; | ||
| } | ||
| let header = { | ||
@@ -232,3 +240,6 @@ key: this._normalizeHeader(key), | ||
| /** | ||
| * @param {string | false} [lineEnd] | ||
| * Serializes the headers. Unmodified headers are returned byte for byte as they | ||
| * were received, otherwise every line is rebuilt with `lineEnd` line endings. | ||
| * | ||
| * @param {string | false} [lineEnd] Line ending to use, defaults to CRLF. | ||
| * @returns {Buffer} | ||
@@ -246,9 +257,13 @@ */ | ||
| lineEnd = lineEnd || '\r\n'; | ||
| const ending = lineEnd || '\r\n'; | ||
| let headers = lines | ||
| .map(line => this._buildHeaderLine(line.line.replace(/\r?\n/g, lineEnd))) | ||
| .map(line => this._normalizeLineBreaks(line.line, ending)) | ||
| // an empty line would close the header block and demote every later header | ||
| // into the body, so a line left with nothing in it is dropped instead | ||
| .filter(line => line !== '') | ||
| .map(line => this._buildHeaderLine(line)) | ||
| .reduce((joined, line, idx) => { | ||
| if (idx) { | ||
| joined.push(Buffer.from(lineEnd, 'binary')); | ||
| joined.push(Buffer.from(ending, 'binary')); | ||
| } | ||
@@ -259,10 +274,10 @@ joined.push(line); | ||
| headers.push(Buffer.from(lineEnd + lineEnd, 'binary')); | ||
| headers.push(Buffer.from(ending + ending, 'binary')); | ||
| if (this.mbox) { | ||
| headers.unshift(Buffer.from(this.mbox + lineEnd, 'binary')); | ||
| headers.unshift(Buffer.from(this.mbox + ending, 'binary')); | ||
| } | ||
| if (this.http) { | ||
| headers.unshift(Buffer.from(this.http + lineEnd, 'binary')); | ||
| headers.unshift(Buffer.from(this.http + ending, 'binary')); | ||
| } | ||
@@ -282,2 +297,59 @@ | ||
| /** | ||
| * Rewrites the line breaks of a header line so that the line can only ever parse | ||
| * back as the single header it was reported as. A line break followed by whitespace | ||
| * is folding and becomes `lineEnd`, every other line break would start a new header | ||
| * line and is dropped. | ||
| * | ||
| * A bare <CR> is never a line break for _parseHeaders, so promoting one here would | ||
| * emit a header line that was never reported as parsed. | ||
| * | ||
| * @param {string} line Header line to normalize. | ||
| * @param {string} lineEnd Line ending to fold with. | ||
| * @returns {string} Line with only folding line breaks left. | ||
| */ | ||
| _normalizeLineBreaks(line, lineEnd) { | ||
| return ( | ||
| line | ||
| // lines are joined with lineEnd, so a line that opens with a break of its own | ||
| // would close the header block. Dropping only the break leaves any whitespace | ||
| // behind it folding into the line before, which adds no header of its own. | ||
| .replace(/^[\r\n]+/, '') | ||
| .replace(/\r\n|\r|\n/g, (match, offset, source) => (match !== '\r' && this._isFoldingChar(source.charAt(offset + match.length)) ? lineEnd : '')) | ||
| ); | ||
| } | ||
| /** | ||
| * Prepares a caller supplied line for insertion. On top of the line break rules an | ||
| * inserted line has to stand on its own: a leading fold or indent would attach it to | ||
| * whichever header happens to precede it, and a leading line break would close the | ||
| * header block outright. | ||
| * | ||
| * Lines that were parsed out of a message keep their leading whitespace instead, so | ||
| * that rebuilding can never turn an indented continuation into a header of its own. | ||
| * | ||
| * An inserted line is normalized twice, here with CRLF and again in build() with the | ||
| * line ending the caller asked for. That is only sound because _normalizeLineBreaks is | ||
| * idempotent over its own output: the folds this pass emits are still recognized as | ||
| * folds by the next one. Any change to how a fold is represented has to keep that true. | ||
| * | ||
| * @param {string} line Formatted header line supplied by the caller. | ||
| * @returns {string} Line that inserts as exactly one header, or an empty string. | ||
| */ | ||
| _normalizeInsertedLine(line) { | ||
| return this._normalizeLineBreaks(line.replace(/^[\r\n \t]+/, ''), '\r\n'); | ||
| } | ||
| /** | ||
| * Tells whether a character continues the previous header line rather than | ||
| * starting a new one. Used by both the parser and the builder so that the two | ||
| * can not disagree on what folding is. | ||
| * | ||
| * @param {string} chr Character that follows a line break. | ||
| * @returns {boolean} True if the line break is folding. | ||
| */ | ||
| _isFoldingChar(chr) { | ||
| return chr === ' ' || chr === '\t'; | ||
| } | ||
| /** | ||
| * @returns {HeaderLine[]} | ||
@@ -310,4 +382,3 @@ */ | ||
| let currentLine = /** @type {string} */ (lines[i]); | ||
| let chr = currentLine.charAt(0); | ||
| if (i && (chr === ' ' || chr === '\t')) { | ||
| if (i && this._isFoldingChar(currentLine.charAt(0))) { | ||
| lines[i - 1] = /** @type {string} */ (lines[i - 1]) + '\r\n' + currentLine; | ||
@@ -314,0 +385,0 @@ lines.splice(i, 1); |
+233
-73
@@ -16,2 +16,17 @@ 'use strict'; | ||
| // how much of a body line without a line break is buffered before it is flushed | ||
| // out as regular content instead of being kept in memory | ||
| const MAX_PENDING_LINE_SIZE = 64 * 1024; | ||
| // how many separate writes the pending line may be kept in before it is compacted | ||
| const MAX_PENDING_LINE_CHUNKS = 1024; | ||
| // what a delimiter line may carry after the boundary value: the "--" prefix, an optional | ||
| // "--" suffix and the line terminator. This is the bound compareBoundary() accepts. | ||
| const BOUNDARY_LINE_SUFFIX = 2 /* "--" prefix */ + 2 /* "--" suffix */ + 2; /* trailing <CR><LF> */ | ||
| // checkBoundary() additionally allows a line ending in front of the delimiter, so this is | ||
| // the longest a delimiter line can ever be once the boundary value is subtracted | ||
| const BOUNDARY_LINE_OVERHEAD = BOUNDARY_LINE_SUFFIX + 2; /* leading <CR><LF> */ | ||
| const HEAD = 0x01; | ||
@@ -21,2 +36,39 @@ const BODY = 0x02; | ||
| /** | ||
| * Creates the error used for all size limit violations. | ||
| * | ||
| * @param {string} message Human readable error message. | ||
| * @returns {Error & {code: string}} Error tagged with the EMAXLEN code. | ||
| */ | ||
| function maxLenError(message) { | ||
| let err = /** @type {Error & {code: string}} */ (new Error(message)); | ||
| err.code = 'EMAXLEN'; | ||
| return err; | ||
| } | ||
| /** | ||
| * Moves an end offset back over the line ending that closes a body line, because the line | ||
| * ending in front of a boundary belongs to the delimiter and not to the part content. Only | ||
| * a group holding the body of a child node carries such a line ending, anything else is | ||
| * returned untouched. | ||
| * | ||
| * @param {SplitterGroup} group Group the offsets describe. | ||
| * @param {Buffer} chunk Chunk the offsets point into. | ||
| * @param {number} start Start offset of the body slice. | ||
| * @param {number} end End offset of the body slice. | ||
| * @returns {number} End offset with a trailing <CR><LF>, <LF> or nothing removed. | ||
| */ | ||
| function trimBodyLineEnd(group, chunk, start, end) { | ||
| if (group.type !== 'body' || !group.node || !group.node.parentNode) { | ||
| return end; | ||
| } | ||
| if (end > start && chunk[end - 1] === 0x0a) { | ||
| end--; | ||
| if (end > start && chunk[end - 1] === 0x0d) { | ||
| end--; | ||
| } | ||
| } | ||
| return end; | ||
| } | ||
| /** | ||
| * Transform stream that splits raw email bytes into MIME node and content chunks. | ||
@@ -38,14 +90,54 @@ */ | ||
| this.maxChildNodes = this.config.maxChildNodes || MAX_CHILD_NODES; | ||
| /** @type {MimeNodeType[]} */ | ||
| this.tree = []; | ||
| this.nodeCounter = 0; | ||
| this.node = /** @type {MimeNodeType} */ (/** @type {unknown} */ (null)); | ||
| // set once the closing delimiter of the current node's multipart has been seen, so | ||
| // that any later boundary line of that node counts as epilogue. Reset per node. | ||
| this.inEpilogue = false; | ||
| this.newNode(); | ||
| this.tree.push(this.node); | ||
| /** @type {Buffer | false} */ | ||
| this.line = false; | ||
| // incomplete trailing line of the previous chunk, kept as a list of chunks so | ||
| // that a long line without a line break is not copied over for every write | ||
| /** @type {Buffer[]} */ | ||
| this.lineChunks = []; | ||
| this.lineLength = 0; | ||
| this.hasFailed = false; | ||
| // set when the pending line was flushed as overlong content, the remainder | ||
| // of that same line can not be a boundary either | ||
| this.pendingLineTruncated = false; | ||
| } | ||
| /** | ||
| * Appends unterminated trailing data to the pending line. | ||
| * | ||
| * @param {Buffer} chunk Data that follows the last line break of a write. | ||
| * @returns {void} | ||
| */ | ||
| appendPendingLine(chunk) { | ||
| if (!chunk.length) { | ||
| return; | ||
| } | ||
| this.lineChunks.push(chunk); | ||
| this.lineLength += chunk.length; | ||
| if (this.lineChunks.length >= MAX_PENDING_LINE_CHUNKS) { | ||
| // a line written one byte at a time would otherwise cost an array slot and a | ||
| // Buffer view per byte, which is far more memory than the data itself | ||
| this.lineChunks = [Buffer.concat(this.lineChunks, this.lineLength)]; | ||
| } | ||
| } | ||
| /** | ||
| * Returns the pending line as a single buffer and clears the pending state. | ||
| * | ||
| * @returns {Buffer | false} Pending line contents or false if there was none. | ||
| */ | ||
| takePendingLine() { | ||
| if (!this.lineLength) { | ||
| return false; | ||
| } | ||
| let line = this.lineChunks.length === 1 ? this.lineChunks[0] : Buffer.concat(this.lineChunks, this.lineLength); | ||
| this.lineChunks = []; | ||
| this.lineLength = 0; | ||
| return line; | ||
| } | ||
| /** | ||
| * @param {Buffer} chunk | ||
@@ -65,3 +157,3 @@ * @param {BufferEncoding} encoding | ||
| }; | ||
| let groupstart = this.line ? -this.line.length : 0; | ||
| let groupstart = this.lineLength ? -this.lineLength : 0; | ||
| let groupend = 0; | ||
@@ -85,6 +177,5 @@ | ||
| pos--; | ||
| if (groupstart < 0 && !this.line) { | ||
| if (groupstart < 0 && !this.lineLength) { | ||
| // store only <CR> as <LF> should be on the positive side | ||
| this.line = Buffer.allocUnsafe(1); | ||
| this.line[0] = 0x0d; | ||
| this.appendPendingLine(Buffer.from([0x0d])); | ||
| } | ||
@@ -130,17 +221,15 @@ data.value = data.value.slice(0, data.value.length - 2); | ||
| if (group && group.type !== 'none') { | ||
| if (group.type === 'body' && groupend >= groupstart && group.node && group.node.parentNode) { | ||
| // do not include the last line ending for body | ||
| if (chunk[groupend - 1] === 0x0a) { | ||
| groupend--; | ||
| if (groupend >= groupstart && chunk[groupend - 1] === 0x0d) { | ||
| groupend--; | ||
| } | ||
| } | ||
| } | ||
| if (groupstart !== groupend) { | ||
| // do not include the last line ending for body | ||
| groupend = trimBodyLineEnd(group, chunk, groupstart, groupend); | ||
| if (groupstart < groupend) { | ||
| // re-slice, the value the line was emitted with may | ||
| // still include the line ending we just trimmed | ||
| group.value = chunk.slice(groupstart, groupend); | ||
| if (groupend < i && 'value' in data) { | ||
| // the trimmed line ending belongs to the boundary line | ||
| data.value = chunk.slice(groupend, i); | ||
| } | ||
| } | ||
| // the group is pushed even when nothing is left of it, so that a | ||
| // part whose whole body is a line ending still reports a body | ||
| this.push(group); | ||
@@ -161,11 +250,4 @@ group = { | ||
| } else { | ||
| if (group.type === 'body' && groupend >= groupstart && group.node && group.node.parentNode) { | ||
| // do not include the last line ending for body | ||
| if (chunk[groupend - 1] === 0x0a) { | ||
| groupend--; | ||
| if (groupend >= groupstart && chunk[groupend - 1] === 0x0d) { | ||
| groupend--; | ||
| } | ||
| } | ||
| } | ||
| // do not include the last line ending for body | ||
| groupend = trimBodyLineEnd(group, chunk, groupstart, groupend); | ||
@@ -209,11 +291,3 @@ if (group.type !== 'none' && group.type !== 'node') { | ||
| // skip last linebreak for body | ||
| if (pos >= groupstart + 1 && group.type === 'body' && group.node && group.node.parentNode) { | ||
| // do not include the last line ending for body | ||
| if (chunk[pos - 1] === 0x0a) { | ||
| pos--; | ||
| if (pos >= groupstart && chunk[pos - 1] === 0x0d) { | ||
| pos--; | ||
| } | ||
| } | ||
| } | ||
| pos = trimBodyLineEnd(group, chunk, groupstart, pos); | ||
@@ -233,8 +307,13 @@ if (group.type !== 'none' && group.type !== 'node' && pos > groupstart) { | ||
| if (pos < chunk.length) { | ||
| if (this.line) { | ||
| this.line = Buffer.concat([this.line, chunk.slice(pos)]); | ||
| } else { | ||
| this.line = chunk.slice(pos); | ||
| } | ||
| // checkTrailingLinebreak can push pos before the start of this write when a | ||
| // line ending straddles it. A negative start would make slice() count from | ||
| // the END of the buffer and hand over the wrong bytes entirely. | ||
| this.appendPendingLine(chunk.slice(Math.max(pos, 0))); | ||
| } | ||
| let pendingLineError = this.enforcePendingLineLimit(); | ||
| if (pendingLineError) { | ||
| this.hasFailed = true; | ||
| return callback(pendingLineError); | ||
| } | ||
| callback(); | ||
@@ -273,3 +352,3 @@ }; | ||
| // --{boundary}\r\n or --{boundary}--\r\n | ||
| if (line.length < boundary.length + 3 + startpos || line.length > boundary.length + 6 + startpos) { | ||
| if (line.length < boundary.length + 3 + startpos || line.length > boundary.length + BOUNDARY_LINE_SUFFIX + startpos) { | ||
| return false; | ||
@@ -321,3 +400,4 @@ } | ||
| startpos++; | ||
| if (line.length >= 2 && (line[0] === 0x0d || line[1] === 0x0a)) { | ||
| if (line.length >= 2 && line[0] === 0x0d && line[1] === 0x0a) { | ||
| // only <CR><LF> is two bytes, a lone <CR> in front of a delimiter is one | ||
| startpos++; | ||
@@ -333,3 +413,3 @@ } | ||
| let boundary; | ||
| if (this.node._boundary && (boundary = this.compareBoundary(line, startpos, this.node._boundary))) { | ||
| if (!this.inEpilogue && this.node._boundary && (boundary = this.compareBoundary(line, startpos, this.node._boundary))) { | ||
| // 1: next child | ||
@@ -350,2 +430,71 @@ // 2: multipart end | ||
| /** | ||
| * Checks the header bytes collected for the current node against maxHeadSize. | ||
| * | ||
| * @param {number} [extra] Bytes that belong to the header block but are not stored yet. | ||
| * @returns {(Error & {code?: string}) | null} Error object if the limit was exceeded. | ||
| */ | ||
| checkHeadSize(extra) { | ||
| if (this.node._headerlen + (extra || 0) > this.maxHeadSize) { | ||
| return maxLenError('Max header size for a MIME node exceeded'); | ||
| } | ||
| return null; | ||
| } | ||
| /** | ||
| * Enforces the limits on the pending line so that it can not grow without bound. | ||
| * A line that is still short enough to become a boundary delimiter is always kept. | ||
| * Past that length it is a header line and counts against maxHeadSize, or it is | ||
| * body content, in which case it is pushed out rather than held in memory. Flushing | ||
| * marks the pending line truncated, so the tail of it is not tested as a delimiter. | ||
| * | ||
| * @returns {(Error & {code?: string}) | null} Error object if a limit was exceeded. | ||
| */ | ||
| enforcePendingLineLimit() { | ||
| if (!this.lineLength) { | ||
| return null; | ||
| } | ||
| let maxBoundaryLength = Math.max( | ||
| this.node._boundary ? this.node._boundary.length : 0, | ||
| this.node._parentBoundary ? this.node._parentBoundary.length : 0 | ||
| ); | ||
| if (this.lineLength <= maxBoundaryLength + BOUNDARY_LINE_OVERHEAD) { | ||
| // might still turn out to be a boundary delimiter line | ||
| return null; | ||
| } | ||
| if (this.state === HEAD) { | ||
| // not a boundary line, so it is a header line and counts against the | ||
| // header size limit even though it has not been stored on the node yet | ||
| return this.checkHeadSize(this.lineLength); | ||
| } | ||
| if (this.lineLength < MAX_PENDING_LINE_SIZE) { | ||
| return null; | ||
| } | ||
| let value = /** @type {Buffer} */ (this.takePendingLine()); | ||
| if (value[value.length - 1] === 0x0d) { | ||
| // a trailing <CR> may still turn out to be the first half of the line ending | ||
| // that closes this line, and a line ending in front of a boundary belongs to | ||
| // the delimiter. Keep it pending so the normal trimming can decide. Copy it | ||
| // rather than slicing, a view would pin the whole flushed buffer. | ||
| this.appendPendingLine(Buffer.from([0x0d])); | ||
| value = value.slice(0, value.length - 1); | ||
| } | ||
| this.push({ | ||
| node: this.node, | ||
| type: this.node.multipart ? 'data' : 'body', | ||
| value | ||
| }); | ||
| // whatever follows continues an overlong line, so the tail of it | ||
| // can not be a boundary line either | ||
| this.pendingLineTruncated = true; | ||
| return null; | ||
| } | ||
| /** | ||
| * @param {Buffer | false} line | ||
@@ -359,8 +508,9 @@ * @param {boolean} final | ||
| if (this.line && line) { | ||
| line = Buffer.concat([this.line, line]); | ||
| this.line = false; | ||
| } else if (this.line && !line) { | ||
| line = this.line; | ||
| this.line = false; | ||
| // consumed here so that no later branch can leak it into the next line | ||
| let truncatedLine = this.pendingLineTruncated; | ||
| this.pendingLineTruncated = false; | ||
| let pending = this.takePendingLine(); | ||
| if (pending) { | ||
| line = line ? Buffer.concat([pending, line]) : pending; | ||
| } | ||
@@ -373,9 +523,8 @@ | ||
| if (this.nodeCounter > this.maxChildNodes) { | ||
| let err = /** @type {Error & {code?: string}} */ (new Error('Max allowed child nodes exceeded')); | ||
| err.code = 'EMAXLEN'; | ||
| return next(err); | ||
| return next(maxLenError('Max allowed child nodes exceeded')); | ||
| } | ||
| // we check boundary outside the HEAD/BODY scope as it may appear anywhere | ||
| let boundary = this.checkBoundary(line); | ||
| // unless the line is the remainder of an already flushed overlong line | ||
| let boundary = truncatedLine ? false : this.checkBoundary(line); | ||
| if (boundary) { | ||
@@ -394,13 +543,7 @@ // reached boundary, switch context | ||
| // next sibling | ||
| let parentNode = this.node.parentNode; | ||
| if (parentNode && parentNode.contentType === 'message/rfc822') { | ||
| // special case where immediate parent is an inline message block | ||
| // move up another step | ||
| parentNode = parentNode.parentNode; | ||
| } | ||
| this.newNode(parentNode); | ||
| this.newNode(this.parentMultipartNode()); | ||
| flush = true; | ||
| break; | ||
| } | ||
| case 4: | ||
| case 4: { | ||
| // special case when boundary close a node with only header. | ||
@@ -411,8 +554,14 @@ if (this.node && this.node._headerlen && !this.node.headers) { | ||
| } | ||
| // move up | ||
| if (this.tree.length) { | ||
| this.node = /** @type {MimeNodeType} */ (this.tree.pop()); | ||
| // move up to the multipart node this closing delimiter belongs to | ||
| let parentNode = this.parentMultipartNode(); | ||
| if (parentNode) { | ||
| this.node = parentNode; | ||
| // the closing delimiter of this multipart was just processed, so any | ||
| // later boundary line of this node belongs to the epilogue. A closing | ||
| // delimiter seen in the preamble (case 2) deliberately does not count. | ||
| this.inEpilogue = true; | ||
| } | ||
| this.state = BODY; | ||
| break; | ||
| } | ||
| } | ||
@@ -434,6 +583,5 @@ | ||
| this.node.addHeaderChunk(line); | ||
| if (this.node._headerlen > this.maxHeadSize) { | ||
| let err = /** @type {Error & {code?: string}} */ (new Error('Max header size for a MIME node exceeded')); | ||
| err.code = 'EMAXLEN'; | ||
| return next(err); | ||
| let headSizeError = this.checkHeadSize(); | ||
| if (headSizeError) { | ||
| return next(headSizeError); | ||
| } | ||
@@ -455,3 +603,6 @@ if (final || (line.length === 1 && line[0] === 0x0a) || (line.length === 2 && line[0] === 0x0d && line[1] === 0x0a)) { | ||
| if (currentNode.parentNode) { | ||
| // the embedded message continues inside its container, so a | ||
| // delimiter of the container's own parent still applies here | ||
| this.node._parentBoundary = currentNode.parentNode._boundary; | ||
| this.node._parentBoundaryOwner = currentNode.parentNode; | ||
| } | ||
@@ -463,5 +614,2 @@ } else { | ||
| this.state = BODY; | ||
| if (currentNode.multipart && currentNode._boundary) { | ||
| this.tree.push(currentNode); | ||
| } | ||
| } | ||
@@ -491,2 +639,12 @@ | ||
| /** | ||
| * Resolves the multipart node that owns the boundary of the current node, ie. the | ||
| * node a sibling delimiter or a closing delimiter of _parentBoundary refers to. | ||
| * | ||
| * @returns {MimeNodeType | false} Owner of _parentBoundary or false for the root node. | ||
| */ | ||
| parentMultipartNode() { | ||
| return this.node._parentBoundaryOwner || false; | ||
| } | ||
| /** | ||
| * @param {MimeNodeType | false} [parent] | ||
@@ -499,2 +657,4 @@ * @returns {void} | ||
| this.nodeCounter++; | ||
| // a fresh node starts before its own content, never in an epilogue | ||
| this.inEpilogue = false; | ||
| } | ||
@@ -501,0 +661,0 @@ } |
@@ -21,2 +21,5 @@ import type { ContentStream, MimeNode as MimeNodeShape, PartNumber, PartNumberItem, SplitterOptions } from './types'; | ||
| /** Node whose boundary `_parentBoundary` was copied from. */ | ||
| _parentBoundaryOwner: MimeNode | false; | ||
| /** Length, in bytes, of the raw header block collected for this node. */ | ||
@@ -23,0 +26,0 @@ _headerlen: number; |
+4
-0
@@ -38,2 +38,6 @@ 'use strict'; | ||
| this._parentBoundary = this.parentNode && this.parentNode._boundary; | ||
| // the node whose boundary _parentBoundary was copied from, recorded here so that | ||
| // a delimiter for it never has to be matched back to an owner by walking the tree | ||
| /** @type {MimeNodeType | false} */ | ||
| this._parentBoundaryOwner = this.parentNode || false; | ||
| /** @type {Buffer[]} */ | ||
@@ -40,0 +44,0 @@ this._headersLines = []; |
+3
-0
@@ -84,2 +84,5 @@ import type { PassThrough, Transform } from 'node:stream'; | ||
| /** Node whose boundary `_parentBoundary` was copied from. */ | ||
| _parentBoundaryOwner: MimeNode | false; | ||
| /** Length, in bytes, of the raw header block collected for this node. */ | ||
@@ -86,0 +89,0 @@ _headerlen: number; |
+5
-5
| { | ||
| "name": "@zone-eu/mailsplit", | ||
| "version": "5.4.14", | ||
| "version": "5.4.15", | ||
| "description": "Split email messages into an object stream", | ||
@@ -19,3 +19,3 @@ "main": "index.js", | ||
| "libbase64": "1.3.0", | ||
| "libmime": "5.4.1", | ||
| "libmime": "5.4.2", | ||
| "libqp": "2.1.1" | ||
@@ -29,7 +29,7 @@ }, | ||
| "@types/libqp": "1.1.3", | ||
| "@types/node": "26.1.0", | ||
| "@types/node": "26.1.2", | ||
| "eslint": "8.29.0", | ||
| "eslint-config-nodemailer": "1.2.0", | ||
| "eslint-config-prettier": "9.1.0", | ||
| "grunt": "1.6.2", | ||
| "grunt": "1.6.3", | ||
| "grunt-cli": "1.5.0", | ||
@@ -39,3 +39,3 @@ "grunt-contrib-nodeunit": "5.0.0", | ||
| "random-message": "1.1.0", | ||
| "typescript": "6.0.3" | ||
| "typescript": "7.0.2" | ||
| }, | ||
@@ -42,0 +42,0 @@ "files": [ |
131763
9.21%2691
8.68%+ Added
- Removed
Updated