mirror of
https://github.com/dawidd6/action-send-mail.git
synced 2026-09-17 09:06:48 +07:00
node_modules: update (#322)
Co-authored-by: dawidd6 <9713907+dawidd6@users.noreply.github.com>
This commit is contained in:
+40
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 });
|
||||
Reference in New Issue
Block a user