node_modules: update (#322)

Co-authored-by: dawidd6 <9713907+dawidd6@users.noreply.github.com>
This commit is contained in:
Dawid Dziurla
2026-09-15 13:00:32 +02:00
committed by GitHub
parent 4a3d0f9ca3
commit 67ce3558d6
174 changed files with 25661 additions and 5540 deletions
+40
View File
@@ -0,0 +1,40 @@
import { type DKIMKey, type DKIMPrivateKey, type DKIMSignOptions } from './sign.js';
import { PassThrough, type Readable } from 'node:stream';
/**
* A single DKIM signing key
*/
export type { DKIMKey, DKIMPrivateKey, DKIMSignOptions };
/**
* Options for the DKIM signer
*/
export interface DKIMOptions extends DKIMSignOptions {
/** One or more signing keys, used instead of the domainName, keySelector and privateKey options */
keys?: DKIMKey | DKIMKey[] | undefined;
/** Directory for buffering large message bodies to disk, no buffering when not set */
cacheDir?: string | false | undefined;
/** Body size in bytes from which the body is buffered to cacheDir, defaults to 10 MB */
cacheTreshold?: number | undefined;
/** Hash algorithm for the body hash and the signature, defaults to sha256 */
hashAlgo?: string | undefined;
}
/**
* The signed message as returned by DKIM#sign
*/
export interface DKIMSignedStream extends PassThrough {
/** true if the message body was buffered to cacheDir while signing */
usingCache: boolean;
}
declare class DKIM {
options: DKIMOptions;
keys: DKIMKey[];
constructor(options: DKIMOptions);
sign(input: Readable | Buffer | string, extraOptions?: DKIMOptions): DKIMSignedStream;
}
/**
* Type aliases in the layout of @types/nodemailer, so `DKIM.Options` style references keep working
*/
declare namespace DKIM {
type Options = DKIMOptions;
type SingleKeyOptions = Omit<DKIMOptions, 'keys'>;
}
export default DKIM;
+211
View File
@@ -0,0 +1,211 @@
"use strict";
// FIXME:
// replace this Transform mess with a method that pipes input argument to output argument
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const message_parser_js_1 = __importDefault(require("./message-parser.js"));
const relaxed_body_js_1 = __importDefault(require("./relaxed-body.js"));
const sign_js_1 = __importDefault(require("./sign.js"));
const node_stream_1 = require("node:stream");
const node_fs_1 = __importDefault(require("node:fs"));
const node_path_1 = __importDefault(require("node:path"));
const node_crypto_1 = __importDefault(require("node:crypto"));
const objects_js_1 = require("../shared/objects.js");
const DKIM_ALGO = 'sha256';
const MAX_MESSAGE_SIZE = 10 * 1024 * 1024; // buffer messages larger than this to disk
class DKIMSigner {
constructor(options, keys, input, output) {
this.options = options || {};
this.keys = keys;
this.cacheTreshold = Number(this.options.cacheTreshold) || MAX_MESSAGE_SIZE;
this.hashAlgo = this.options.hashAlgo || DKIM_ALGO;
this.cacheDir = this.options.cacheDir || false;
this.chunks = [];
this.chunklen = 0;
this.readPos = 0;
this.cachePath = this.cacheDir
? node_path_1.default.join(this.cacheDir, 'message.' + Date.now() + '-' + node_crypto_1.default.randomBytes(14).toString('hex'))
: false;
this.cache = false;
this.headers = false;
this.bodyHash = false;
this.parser = false;
this.relaxedBody = false;
this.input = input;
this.output = output;
this.output.usingCache = false;
this.hasErrored = false;
this.input.on('error', err => {
this.hasErrored = true;
this.cleanup();
output.emit('error', err);
});
}
cleanup() {
if (!this.cache || !this.cachePath) {
return;
}
node_fs_1.default.unlink(this.cachePath, () => false);
}
createReadCache() {
// pipe remainings to cache file
this.cache = node_fs_1.default.createReadStream(this.cachePath);
this.cache.once('error', err => {
this.cleanup();
this.output.emit('error', err);
});
this.cache.once('close', () => {
this.cleanup();
});
this.cache.pipe(this.output);
}
sendNextChunk() {
if (this.hasErrored) {
return;
}
if (this.readPos >= this.chunks.length) {
if (!this.cache) {
this.output.end();
return;
}
return this.createReadCache();
}
const chunk = this.chunks[this.readPos++];
if (this.output.write(chunk) === false) {
this.output.once('drain', () => {
this.sendNextChunk();
});
return;
}
setImmediate(() => this.sendNextChunk());
}
sendSignedOutput() {
let keyPos = 0;
const signNextKey = () => {
if (keyPos >= this.keys.length) {
this.output.write(this.parser.rawHeaders);
setImmediate(() => this.sendNextChunk());
return;
}
const key = this.keys[keyPos++];
const dkimField = (0, sign_js_1.default)(this.headers, this.hashAlgo, this.bodyHash, {
domainName: key.domainName,
keySelector: key.keySelector,
privateKey: key.privateKey,
headerFieldNames: this.options.headerFieldNames,
skipFields: this.options.skipFields
});
if (dkimField) {
this.output.write(Buffer.from(dkimField + '\r\n'));
}
setImmediate(signNextKey);
};
if (this.bodyHash && this.headers) {
return signNextKey();
}
this.output.write(this.parser.rawHeaders);
this.sendNextChunk();
}
createWriteCache() {
this.output.usingCache = true;
// pipe remainings to cache file
this.cache = node_fs_1.default.createWriteStream(this.cachePath);
this.cache.once('error', err => {
this.cleanup();
// drain input
this.relaxedBody.unpipe(this.cache);
this.relaxedBody.on('readable', () => {
while (this.relaxedBody.read() !== null) {
// do nothing
}
});
this.hasErrored = true;
// emit error
this.output.emit('error', err);
});
this.cache.once('close', () => {
this.sendSignedOutput();
});
this.relaxedBody.removeAllListeners('readable');
this.relaxedBody.pipe(this.cache);
}
signStream() {
this.parser = new message_parser_js_1.default();
this.relaxedBody = new relaxed_body_js_1.default({
hashAlgo: this.hashAlgo
});
this.parser.on('headers', value => {
this.headers = value;
});
this.relaxedBody.on('hash', value => {
this.bodyHash = value;
});
this.relaxedBody.on('readable', () => {
let chunk;
if (this.cache) {
return;
}
while ((chunk = this.relaxedBody.read()) !== null) {
this.chunks.push(chunk);
this.chunklen += chunk.length;
if (this.chunklen >= this.cacheTreshold && this.cachePath) {
return this.createWriteCache();
}
}
});
this.relaxedBody.on('end', () => {
if (this.cache) {
return;
}
this.sendSignedOutput();
});
this.parser.pipe(this.relaxedBody);
setImmediate(() => this.input.pipe(this.parser));
}
}
class DKIM {
constructor(options) {
this.options = options || {};
this.keys = [].concat(this.options.keys || {
domainName: options.domainName,
keySelector: options.keySelector,
privateKey: options.privateKey
});
}
sign(input, extraOptions) {
const output = new node_stream_1.PassThrough();
let inputStream = input;
let writeValue = false;
if (Buffer.isBuffer(input)) {
writeValue = input;
inputStream = new node_stream_1.PassThrough();
}
else if (typeof input === 'string') {
writeValue = Buffer.from(input);
inputStream = new node_stream_1.PassThrough();
}
let options = this.options;
if (extraOptions && Object.keys(extraOptions).length) {
// extraOptions is mail.data._dkim, caller supplied message data. An own
// "__proto__" key there would let every option this signer reads and the
// transport did not set, such as skipFields, answer from the caller
options = (0, objects_js_1.copyOwnKeys)({}, extraOptions);
(0, objects_js_1.copyOwnKeys)(options, this.options);
}
const signer = new DKIMSigner(options, this.keys, inputStream, output);
setImmediate(() => {
signer.signStream();
if (writeValue) {
setImmediate(() => {
inputStream.end(writeValue);
});
}
});
return output;
}
}
exports.default = DKIM;
module.exports = exports.default;
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
+39
View File
@@ -0,0 +1,39 @@
import { Transform, type TransformOptions } from 'node:stream';
/**
* A header line as emitted with the 'headers' event
*/
export interface MessageParserHeaderLine {
/** Lowercase header field name */
key: string;
/** Full header line, folded continuation lines included, one character per byte ('binary' encoding) */
line: string;
}
/**
* MessageParser instance is a transform stream that separates message headers
* from the rest of the body. Headers are emitted with the 'headers' event. Message
* body is passed on as the resulting stream.
*/
export default class MessageParser extends Transform {
lastBytes: Buffer;
headersParsed: boolean;
headerBytes: number;
headerChunks: Buffer[] | null;
rawHeaders: Buffer | false;
bodySize: number;
constructor(options?: TransformOptions);
/**
* Keeps count of the last 4 bytes in order to detect line breaks on chunk boundaries
*
* @param data Next data chunk from the stream
*/
updateLastBytes(data: Buffer): void;
/**
* Finds and removes message headers from the remaining body. We want to keep
* headers separated until final delivery to be able to modify these
*
* @param data Next chunk of data
* @return Returns true if headers are already found or false otherwise
*/
checkHeaders(data: Buffer): boolean;
parseHeaders(): MessageParserHeaderLine[];
}
+149
View File
@@ -0,0 +1,149 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const node_stream_1 = require("node:stream");
/**
* MessageParser instance is a transform stream that separates message headers
* from the rest of the body. Headers are emitted with the 'headers' event. Message
* body is passed on as the resulting stream.
*/
class MessageParser extends node_stream_1.Transform {
constructor(options) {
super(options);
this.lastBytes = Buffer.alloc(4);
this.headersParsed = false;
this.headerBytes = 0;
this.headerChunks = [];
this.rawHeaders = false;
this.bodySize = 0;
}
/**
* Keeps count of the last 4 bytes in order to detect line breaks on chunk boundaries
*
* @param data Next data chunk from the stream
*/
updateLastBytes(data) {
const lblen = this.lastBytes.length;
const nblen = Math.min(data.length, lblen);
// shift existing bytes
for (let i = 0, len = lblen - nblen; i < len; i++) {
this.lastBytes[i] = this.lastBytes[i + nblen];
}
// add new bytes
for (let i = 1; i <= nblen; i++) {
this.lastBytes[lblen - i] = data[data.length - i];
}
}
/**
* Finds and removes message headers from the remaining body. We want to keep
* headers separated until final delivery to be able to modify these
*
* @param data Next chunk of data
* @return Returns true if headers are already found or false otherwise
*/
checkHeaders(data) {
if (this.headersParsed) {
return true;
}
const lblen = this.lastBytes.length;
let headerPos = 0;
for (let i = 0, len = this.lastBytes.length + data.length; i < len; i++) {
let chr;
if (i < lblen) {
chr = this.lastBytes[i];
}
else {
chr = data[i - lblen];
}
if (chr === 0x0a && i) {
const pr1 = i - 1 < lblen ? this.lastBytes[i - 1] : data[i - 1 - lblen];
const pr2 = i > 1 ? (i - 2 < lblen ? this.lastBytes[i - 2] : data[i - 2 - lblen]) : false;
if (pr1 === 0x0a) {
this.headersParsed = true;
headerPos = i - lblen + 1;
this.headerBytes += headerPos;
break;
}
else if (pr1 === 0x0d && pr2 === 0x0a) {
this.headersParsed = true;
headerPos = i - lblen + 1;
this.headerBytes += headerPos;
break;
}
}
}
if (this.headersParsed) {
this.headerChunks.push(data.slice(0, headerPos));
this.rawHeaders = Buffer.concat(this.headerChunks, this.headerBytes);
this.headerChunks = null;
this.emit('headers', this.parseHeaders());
if (data.length > headerPos) {
const chunk = data.slice(headerPos);
this.bodySize += chunk.length;
// this would be the first chunk of data sent downstream
setImmediate(() => this.push(chunk));
}
return false;
}
this.headerBytes += data.length;
this.headerChunks.push(data);
// store last 4 bytes to catch header break
this.updateLastBytes(data);
return false;
}
/** @internal */
_transform(chunk, encoding, callback) {
if (!chunk || !chunk.length) {
return callback();
}
if (typeof chunk === 'string') {
chunk = Buffer.from(chunk, encoding);
}
let headersFound;
try {
headersFound = this.checkHeaders(chunk);
}
catch (E) {
return callback(E);
}
if (headersFound) {
this.bodySize += chunk.length;
this.push(chunk);
}
setImmediate(callback);
}
/** @internal */
_flush(callback) {
if (this.headerChunks) {
// no empty line was seen, so the message consists of headers only
this.rawHeaders = Buffer.concat(this.headerChunks, this.headerBytes);
this.headerChunks = null;
this.emit('headers', this.parseHeaders());
}
callback();
}
parseHeaders() {
// the header bytes are kept as they are, one character per byte, so the
// signature covers exactly the bytes the receiving side canonicalizes
// Only SP and HTAB fold a line, and only they are trimmed from the field name, the
// same whitespace the relaxed canonicalization in sign.ts works with
const lines = (this.rawHeaders || Buffer.alloc(0)).toString('binary').split(/\r?\n/);
for (let i = lines.length - 1; i > 0; i--) {
if (/^[ \t]/.test(lines[i])) {
lines[i - 1] += '\n' + lines[i];
lines.splice(i, 1);
}
}
return lines
.filter(line => /[^ \t\r]/.test(line))
.map(line => ({
key: line
.substr(0, line.indexOf(':'))
.replace(/^[ \t]+|[ \t]+$/g, '')
.toLowerCase(),
line
}));
}
}
exports.default = MessageParser;
module.exports = exports.default;
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
+27
View File
@@ -0,0 +1,27 @@
import { Transform } from 'node:stream';
import crypto from 'node:crypto';
/**
* Options for the relaxed body hash stream
*/
export interface RelaxedBodyOptions {
/** Hash algorithm for the body hash, defaults to sha256 */
hashAlgo?: string | undefined;
/** Collect the canonicalized body and emit it with the 'hash' event */
debug?: boolean | undefined;
}
/**
* Passes the message body through unchanged and hashes its relaxed
* canonicalization (RFC 6376 section 3.4.4) on the side: whitespace at the end
* of a line is dropped, runs of whitespace within a line become a single space,
* every line ends with CRLF, empty lines at the end of the body are ignored and
* a non-empty body always ends with CRLF. Bytes are canonicalized as they arrive,
* so a line of any length costs constant memory.
*/
export default class RelaxedBody extends Transform {
bodyHash: crypto.Hash;
/** Bytes of the original body seen so far */
byteLength: number;
debug: boolean | undefined;
constructor(options?: RelaxedBodyOptions);
updateHash(chunk: Buffer, final?: boolean): void;
}
+148
View File
@@ -0,0 +1,148 @@
"use strict";
// streams through a message body and calculates relaxed body hash
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const node_stream_1 = require("node:stream");
const node_crypto_1 = __importDefault(require("node:crypto"));
const CHAR_CR = 0x0d;
const CHAR_LF = 0x0a;
const CHAR_SPACE = 0x20;
const CHAR_TAB = 0x09;
const CRLF = Buffer.from('\r\n');
// a run of empty lines is hashed from this buffer in slices
const EMPTY_LINES = Buffer.alloc(4096, CRLF);
/**
* Passes the message body through unchanged and hashes its relaxed
* canonicalization (RFC 6376 section 3.4.4) on the side: whitespace at the end
* of a line is dropped, runs of whitespace within a line become a single space,
* every line ends with CRLF, empty lines at the end of the body are ignored and
* a non-empty body always ends with CRLF. Bytes are canonicalized as they arrive,
* so a line of any length costs constant memory.
*/
class RelaxedBody extends node_stream_1.Transform {
constructor(options) {
super();
options = options || {};
this.bodyHash = node_crypto_1.default.createHash(options.hashAlgo || 'sha256');
this.byteLength = 0;
this.debug = options.debug;
this._debugBody = options.debug ? [] : false;
this._lineHasContent = false;
this._pendingWsp = false;
this._pendingCr = false;
this._pendingEmptyLines = 0;
}
/** @internal */
_hashCanonical(data) {
if (!data.length) {
return;
}
this.bodyHash.update(data);
if (this._debugBody) {
this._debugBody.push(Buffer.from(data));
}
}
/** @internal */
_hashEmptyLines() {
while (this._pendingEmptyLines > 0) {
const count = Math.min(this._pendingEmptyLines, EMPTY_LINES.length / 2);
this._hashCanonical(EMPTY_LINES.subarray(0, count * 2));
this._pendingEmptyLines -= count;
}
}
/**
* Writes a content byte, with the space a pending run of whitespace collapses to,
* into the output buffer and returns the new write position. Kept a method rather
* than a closure so the write position stays a plain local in the byte loop
* @internal
*/
_emitContent(out, outPos, c) {
if (!this._lineHasContent) {
if (this._pendingEmptyLines) {
// the first content byte of a line is where the empty lines before it
// become part of the body, so hash what is in the buffer before them
this._hashCanonical(out.subarray(0, outPos));
outPos = 0;
this._hashEmptyLines();
}
this._lineHasContent = true;
}
if (this._pendingWsp) {
out[outPos++] = CHAR_SPACE;
this._pendingWsp = false;
}
out[outPos++] = c;
return outPos;
}
updateHash(chunk, final) {
// every byte contributes itself at most once, plus a CR for a bare LF
// and, once per chunk, a pending space and CR carried over from before
const out = Buffer.allocUnsafe(chunk.length * 2 + 2);
let outPos = 0;
for (let i = 0; i < chunk.length; i++) {
const c = chunk[i];
if (c === CHAR_LF) {
// end of line, a CR right before it and any trailing whitespace are dropped
if (this._lineHasContent) {
out[outPos++] = CHAR_CR;
out[outPos++] = CHAR_LF;
this._lineHasContent = false;
}
else {
this._pendingEmptyLines++;
}
this._pendingWsp = false;
this._pendingCr = false;
continue;
}
if (this._pendingCr) {
// not followed by LF, so the CR is content
outPos = this._emitContent(out, outPos, CHAR_CR);
this._pendingCr = false;
}
if (c === CHAR_CR) {
this._pendingCr = true;
}
else if (c === CHAR_SPACE || c === CHAR_TAB) {
this._pendingWsp = true;
}
else {
outPos = this._emitContent(out, outPos, c);
}
}
if (final && this._pendingCr) {
// a CR at the very end of the body is content
outPos = this._emitContent(out, outPos, CHAR_CR);
this._pendingCr = false;
}
this._hashCanonical(out.subarray(0, outPos));
}
/** @internal */
_transform(chunk, encoding, callback) {
if (!chunk || !chunk.length) {
return callback();
}
if (typeof chunk === 'string') {
chunk = Buffer.from(chunk, encoding);
}
this.updateHash(chunk);
this.byteLength += chunk.length;
this.push(chunk);
callback();
}
/** @internal */
_flush(callback) {
this.updateHash(Buffer.alloc(0), true);
if (this._lineHasContent) {
// the body does not end with a line break, add one
this._hashCanonical(CRLF);
}
this.emit('hash', this.bodyHash.digest('base64'), this.debug ? Buffer.concat(this._debugBody) : false);
callback();
}
}
exports.default = RelaxedBody;
module.exports = exports.default;
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
+49
View File
@@ -0,0 +1,49 @@
import crypto from 'node:crypto';
import type { MessageParserHeaderLine } from './message-parser.js';
/**
* Private key accepted by crypto.Sign#sign: a PEM string, a Buffer, a KeyObject or an
* object with the key and its passphrase
*/
export type DKIMPrivateKey = crypto.KeyLike | crypto.SignKeyObjectInput | crypto.SignPrivateKeyInput;
/**
* Options for the DKIM signature header generator
*/
export interface DKIMKey {
/** Domain name to be signed for */
domainName?: string | undefined;
/** DKIM key selector to use */
keySelector?: string | undefined;
/** DKIM private key to use */
privateKey?: DKIMPrivateKey | undefined;
}
export interface DKIMSignOptions extends DKIMKey {
/** Colon separated list of header field names to sign, defaults to the RFC4871 list */
headerFieldNames?: string | undefined;
/** Colon separated list of header field names to leave out of the signature */
skipFields?: string | undefined;
}
/**
* Canonicalized headers and the list of field names that went into them
*/
export interface DKIMRelaxedHeaders {
/** Relaxed header lines, each terminated with CRLF, one character per byte ('binary' encoding) */
headers: string;
/** Colon separated list of the field names that were included */
fieldNames: string;
}
/**
* Returns DKIM signature header line
*
* @param headers Parsed headers object from MessageParser
* @param bodyHash Base64 encoded hash of the message
* @param options DKIM options
* @param options.domainName Domain name to be signed for
* @param options.keySelector DKIM key selector to use
* @param options.privateKey DKIM private key to use
* @return Complete header line
*/
declare function sign(headers: MessageParserHeaderLine[], hashAlgo: string, bodyHash: string, options?: DKIMSignOptions): string | false;
declare namespace sign {
var relaxedHeaders: (headers: MessageParserHeaderLine[], fieldNames?: string, skipFields?: string) => DKIMRelaxedHeaders;
}
export default sign;
+148
View File
@@ -0,0 +1,148 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const punycode = __importStar(require("../punycode/index.js"));
const mimeFuncs = __importStar(require("../mime-funcs/index.js"));
const node_crypto_1 = __importDefault(require("node:crypto"));
/**
* Returns DKIM signature header line
*
* @param headers Parsed headers object from MessageParser
* @param bodyHash Base64 encoded hash of the message
* @param options DKIM options
* @param options.domainName Domain name to be signed for
* @param options.keySelector DKIM key selector to use
* @param options.privateKey DKIM private key to use
* @return Complete header line
*/
function sign(headers, hashAlgo, bodyHash, options) {
options = options || {};
// all listed fields from RFC4871 #5.5
const defaultFieldNames = 'From:Sender:Reply-To:Subject:Date:Message-ID:To:' +
'Cc:MIME-Version:Content-Type:Content-Transfer-Encoding:Content-ID:' +
'Content-Description:Resent-Date:Resent-From:Resent-Sender:' +
'Resent-To:Resent-Cc:Resent-Message-ID:In-Reply-To:References:' +
'List-Id:List-Help:List-Unsubscribe:List-Subscribe:List-Post:' +
'List-Owner:List-Archive';
const fieldNames = options.headerFieldNames || defaultFieldNames;
const canonicalizedHeaderData = relaxedHeaders(headers, fieldNames, options.skipFields);
const dkimHeader = generateDKIMHeader(options.domainName, options.keySelector, canonicalizedHeaderData.fieldNames, hashAlgo, bodyHash);
canonicalizedHeaderData.headers += 'dkim-signature:' + relaxedHeaderLine(dkimHeader);
const signer = node_crypto_1.default.createSign(('rsa-' + hashAlgo).toUpperCase());
// the header lines are 'binary' strings, so this reproduces the original header bytes
signer.update(canonicalizedHeaderData.headers, 'latin1');
let signature;
try {
signature = signer.sign(options.privateKey, 'base64');
}
catch (_E) {
return false;
}
return dkimHeader + signature.replace(/(^.{73}|.{75}(?!\r?\n|\r))/g, '$&\r\n ').trim();
}
sign.relaxedHeaders = relaxedHeaders;
exports.default = sign;
function generateDKIMHeader(domainName, keySelector, fieldNames, hashAlgo, bodyHash) {
// the caller supplied tag values are interpolated straight into the tag list, and none of
// them has any way to carry a control char, DEL, or one of the delimiters that would close
// the value and open a tag of its own
const cleanTagValue = (value) => (value || '').toString().replace(/[\x00-\x1f\x7f;=]/g, '');
const dkim = [
'v=1',
'a=rsa-' + hashAlgo,
'c=relaxed/relaxed',
'd=' + punycode.toASCII(cleanTagValue(domainName)),
'q=dns/txt',
's=' + cleanTagValue(keySelector),
'bh=' + bodyHash,
'h=' + cleanTagValue(fieldNames)
].join('; ');
return mimeFuncs.foldLines('DKIM-Signature: ' + dkim, 76) + ';\r\n b=';
}
function relaxedHeaders(headers, fieldNames, skipFields) {
const includedFields = new Set();
const skip = new Set();
const headerFields = new Map();
(skipFields || '')
.toLowerCase()
.split(':')
.forEach(field => {
skip.add(field.trim());
});
(fieldNames || '')
.toLowerCase()
.split(':')
.filter(field => !skip.has(field.trim()))
.forEach(field => {
includedFields.add(field.trim());
});
for (let i = headers.length - 1; i >= 0; i--) {
const line = headers[i];
// only include the first value from bottom to top
if (includedFields.has(line.key) && !headerFields.has(line.key)) {
headerFields.set(line.key, relaxedHeaderLine(line.line));
}
}
const headersList = [];
const fields = [];
includedFields.forEach(field => {
if (headerFields.has(field)) {
fields.push(field);
headersList.push(field + ':' + headerFields.get(field));
}
});
return {
headers: headersList.join('\r\n') + '\r\n',
fieldNames: fields.join(':')
};
}
/**
* Relaxed canonicalization of a header field value (RFC 6376 section 3.4.2): unfold, turn
* every run of SP and HTAB into a single SP and drop the whitespace next to the colon and
* at the end. Only SP and HTAB count as whitespace, so bytes that decode to other space
* characters, such as a non-breaking space in a UTF-8 header, stay as they are
*/
function relaxedHeaderLine(line) {
return line
.substr(line.indexOf(':') + 1)
.replace(/\r?\n/g, '')
.replace(/[ \t]+/g, ' ')
.replace(/^ | $/g, '');
}
module.exports = exports.default;
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });