mirror of
https://github.com/dawidd6/action-send-mail.git
synced 2026-10-21 06:22:01 +07:00
Compare commits
30
Commits
v22
...
node_modules
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df3e9b2c0f | ||
|
|
955f286ed4 | ||
|
|
0a1f5db16c | ||
|
|
9d0c26ec8b | ||
|
|
b90fafaa5c | ||
|
|
f6664f2182 | ||
|
|
16c91eb93f | ||
|
|
6e88c72525 | ||
|
|
b099186883 | ||
|
|
ef74bf1f25 | ||
|
|
ad4baca30f | ||
|
|
ab08312534 | ||
|
|
c2c6302b92 | ||
|
|
0635835dfc | ||
|
|
791f27c1e4 | ||
|
|
a9c5eb6c35 | ||
|
|
e1936242ed | ||
|
|
411739e2ff | ||
|
|
dfde770ea1 | ||
|
|
0989a8cbe9 | ||
|
|
6b2df9babe | ||
|
|
6bf0a2f26a | ||
|
|
c1dac70e3a | ||
|
|
b83e41d98d | ||
|
|
0e0ab085a3 | ||
|
|
23dc752083 | ||
|
|
6d91308e46 | ||
|
|
bd35283f19 | ||
|
|
cab633514f | ||
|
|
b6796c9b12 |
No files matched your search
@@ -1,16 +0,0 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
|
||||
{
|
||||
"name": "Node.js & TypeScript",
|
||||
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"GitHub.vscode-github-actions",
|
||||
"esbenp.prettier-vscode",
|
||||
"mads-hartmann.bash-ide-vscode",
|
||||
"ms-azuretools.vscode-containers"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
pull-request-branch-name:
|
||||
separator: "-"
|
||||
open-pull-requests-limit: 10
|
||||
ignore:
|
||||
- dependency-name: nodemailer
|
||||
versions:
|
||||
- 6.4.18
|
||||
- 6.5.0
|
||||
- 6.6.0
|
||||
- package-ecosystem: github-actions
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
pull-request-branch-name:
|
||||
separator: "-"
|
||||
open-pull-requests-limit: 10
|
||||
- package-ecosystem: "devcontainers"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
@@ -2,9 +2,15 @@ name: NPM Updates
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- package.json
|
||||
- package-lock.json
|
||||
|
||||
jobs:
|
||||
npm-updates:
|
||||
uses: dawidd6/reusable-workflows/.github/workflows/npm-updates.yml@master
|
||||
uses: dawidd6/reusable-workflows/.github/workflows/npm-updates.yml@master # zizmor: ignore[unpinned-uses]
|
||||
secrets:
|
||||
personal_access_token: ${{secrets.TOKEN}}
|
||||
@@ -19,13 +19,14 @@ Some features:
|
||||
# Optional proxy, NO_PROXY is honored too:
|
||||
SMTP_PROXY: http://proxy.example.test:3128
|
||||
with:
|
||||
# Specify connection via URL (replaces server_address, server_port, secure,
|
||||
# Specify connection via URL (replaces server_address, server_port,
|
||||
# username and password)
|
||||
#
|
||||
# Format:
|
||||
#
|
||||
# * smtp://user:password@server:port
|
||||
# * smtp+starttls://user:password@server:port
|
||||
# * smtps://user:password@server:port (TLS, default port 465)
|
||||
# * smtp+starttls://user:password@server:port (STARTTLS required, default port 587)
|
||||
connection_url: ${{secrets.MAIL_CONNECTION}}
|
||||
|
||||
# Required mail server address if not connection_url:
|
||||
|
||||
+2
-1
@@ -6,7 +6,7 @@ branding:
|
||||
color: blue
|
||||
inputs:
|
||||
connection_url:
|
||||
description: Connection URL protocol://user:password@server:port, protocol can be smtp or smtp+starttls, replaces server_address, server_port, secure, username and password
|
||||
description: Connection URL protocol://user:password@server:port, protocol can be smtp, smtps (TLS, default port 465) or smtp+starttls (STARTTLS required, default port 587), replaces server_address, server_port, username and password
|
||||
server_address:
|
||||
description: SMTP server address
|
||||
server_port:
|
||||
@@ -54,6 +54,7 @@ inputs:
|
||||
convert_markdown:
|
||||
description: Convert body from Markdown to HTML (set content_type input as text/html too)
|
||||
required: false
|
||||
default: "false"
|
||||
attachments:
|
||||
description: Files that will be added to mail message attachments (separated with comma)
|
||||
required: false
|
||||
|
||||
@@ -93,15 +93,13 @@ async function main() {
|
||||
try {
|
||||
let serverAddress = core.getInput("server_address");
|
||||
let serverPort = core.getInput("server_port");
|
||||
let secure = core.getInput("secure");
|
||||
const requireTLS = core.getInput("require_tls", { required: false });
|
||||
let secure = core.getInput("secure")
|
||||
? core.getBooleanInput("secure")
|
||||
: undefined;
|
||||
let requireTLS = core.getInput("require_tls", { required: false });
|
||||
let username = core.getInput("username");
|
||||
let password = core.getInput("password");
|
||||
|
||||
if (!secure) {
|
||||
secure = serverPort === "465" ? "true" : "false";
|
||||
}
|
||||
|
||||
const connectionUrl = core.getInput("connection_url");
|
||||
if (connectionUrl) {
|
||||
const url = new URL(connectionUrl);
|
||||
@@ -112,11 +110,15 @@ async function main() {
|
||||
);
|
||||
case "smtp:":
|
||||
serverPort = "25";
|
||||
secure = "false";
|
||||
break;
|
||||
case "smtps:":
|
||||
serverPort = "465";
|
||||
secure = true;
|
||||
break;
|
||||
case "smtp+starttls:":
|
||||
serverPort = "465";
|
||||
secure = "true";
|
||||
serverPort = "587";
|
||||
secure = false;
|
||||
requireTLS = "true";
|
||||
break;
|
||||
}
|
||||
if (url.hostname) {
|
||||
@@ -133,6 +135,10 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
if (secure === undefined) {
|
||||
secure = serverPort === "465";
|
||||
}
|
||||
|
||||
const subject = core.getInput("subject", { required: true });
|
||||
const from = core.getInput("from", { required: true });
|
||||
const to = core.getInput("to", { required: false });
|
||||
@@ -143,7 +149,7 @@ async function main() {
|
||||
const replyTo = core.getInput("reply_to", { required: false });
|
||||
const inReplyTo = core.getInput("in_reply_to", { required: false });
|
||||
const attachments = core.getInput("attachments", { required: false });
|
||||
const convertMarkdown = core.getInput("convert_markdown", {
|
||||
const convertMarkdown = core.getBooleanInput("convert_markdown", {
|
||||
required: false,
|
||||
});
|
||||
const ignoreCert = core.getInput("ignore_cert", { required: false });
|
||||
@@ -196,7 +202,7 @@ async function main() {
|
||||
}
|
||||
: undefined,
|
||||
port: serverPort,
|
||||
secure: secure === "true",
|
||||
secure: secure,
|
||||
requireTLS: requireTLS === "true",
|
||||
tls:
|
||||
ignoreCert == "true"
|
||||
|
||||
+9
-9
@@ -58,9 +58,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "5.0.9",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
|
||||
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
|
||||
"version": "5.0.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
|
||||
"integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
@@ -94,9 +94,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "10.0.8",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-10.0.8.tgz",
|
||||
"integrity": "sha512-uCZ0AtFDI46sHwJ6mhwNuIoA/KiTC0ZCzX7g01941+H9VMIXEiZjtgjQkzoBRTeQba0cIrKfwk31q99PfcIMRw==",
|
||||
"version": "10.1.7",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-10.1.7.tgz",
|
||||
"integrity": "sha512-OQa6FDln5fLttmqSbjDiRYvFDOFNrBqfiOwHPlEbURil3b+vPB1v8xpi+Sp46drzNDlsTnNsS502vthmz56r4w==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
@@ -128,9 +128,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.28.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz",
|
||||
"integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==",
|
||||
"version": "6.29.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.29.0.tgz",
|
||||
"integrity": "sha512-R+RODBqp6i2pPflGdq+xIOUkl+RNfGgHwoinecKu/JCuf2uO06cOKoDbI2P7Dn6KcswdKwrczbU6IYJ6K8X+wg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
|
||||
+4
@@ -1,8 +1,12 @@
|
||||
export declare const EXPANSION_MAX = 100000;
|
||||
export declare const EXPANSION_MAX_LENGTH = 4000000;
|
||||
export declare const EXPANSION_MAX_DEPTH = 1000;
|
||||
export declare const EXPANSION_MAX_REWRITES = 1000;
|
||||
export type BraceExpansionOptions = {
|
||||
max?: number;
|
||||
maxLength?: number;
|
||||
maxDepth?: number;
|
||||
maxRewrites?: number;
|
||||
};
|
||||
export declare function expand(str: string, options?: BraceExpansionOptions): string[];
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAkBA,eAAO,MAAM,aAAa,SAAU,CAAA;AAYpC,eAAO,MAAM,oBAAoB,UAAY,CAAA;AAwD7C,MAAM,MAAM,qBAAqB,GAAG;IAClC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,qBAA0B,YAkBtE"}
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAkBA,eAAO,MAAM,aAAa,SAAU,CAAA;AAYpC,eAAO,MAAM,oBAAoB,UAAY,CAAA;AAU7C,eAAO,MAAM,mBAAmB,OAAQ,CAAA;AAUxC,eAAO,MAAM,sBAAsB,OAAQ,CAAA;AAyE3C,MAAM,MAAM,qBAAqB,GAAG;IAClC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA;AAED,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,qBAA0B,YA+BtE"}
|
||||
+69
-25
@@ -1,6 +1,6 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.EXPANSION_MAX_LENGTH = exports.EXPANSION_MAX = void 0;
|
||||
exports.EXPANSION_MAX_REWRITES = exports.EXPANSION_MAX_DEPTH = exports.EXPANSION_MAX_LENGTH = exports.EXPANSION_MAX = void 0;
|
||||
exports.expand = expand;
|
||||
const balanced_match_1 = require("balanced-match");
|
||||
const escSlash = '\0SLASH' + Math.random() + '\0';
|
||||
@@ -30,6 +30,24 @@ exports.EXPANSION_MAX = 100_000;
|
||||
// realistic expansion (100k results hitting `EXPANSION_MAX` measure ~1M
|
||||
// characters) so legitimate input is unaffected.
|
||||
exports.EXPANSION_MAX_LENGTH = 4_000_000;
|
||||
// `expand_` recurses once per level of brace *nesting* - both when expanding a
|
||||
// set's comma members and when re-wrapping a set whose body is a single part.
|
||||
// The CVE-2026-14257 fix made the *tail* iterative (recursion on `m.post`, one
|
||||
// level per chained group), which left nesting depth unbounded: about 3,100
|
||||
// levels of `{{{...a,b...}}}` - only ~6KB of input - exhausted the native stack
|
||||
// and crashed the process. `EXPANSION_MAX_DEPTH` bounds how deep the parser
|
||||
// will follow nesting. It sits far above any realistic pattern and well below
|
||||
// the depth at which the stack runs out.
|
||||
exports.EXPANSION_MAX_DEPTH = 1_000;
|
||||
// Bash keeps a quirk where a brace group followed by a comma set still expands
|
||||
// (`{a},b}`). The parser implements it by rewriting the string and restarting
|
||||
// the scan, absorbing one `}` per pass. `n` trailing braces therefore cost `n`
|
||||
// full passes over a string that itself grows by one `escClose` sentinel each
|
||||
// time - quadratic in `n`, with a ~26x constant from the sentinel's length.
|
||||
// 128KB of `'{a}' + '}'.repeat(n) + ',z}'` blocked the event loop for 27
|
||||
// seconds to produce two results. `EXPANSION_MAX_REWRITES` bounds how many
|
||||
// times the scan may restart. Real `{a},b}` input needs a handful.
|
||||
exports.EXPANSION_MAX_REWRITES = 1_000;
|
||||
function numeric(str) {
|
||||
return !isNaN(str) ? parseInt(str, 10) : str.charCodeAt(0);
|
||||
}
|
||||
@@ -49,37 +67,52 @@ function unescapeBraces(str) {
|
||||
.replace(escCommaPattern, ',')
|
||||
.replace(escPeriodPattern, '.');
|
||||
}
|
||||
// Like `target.push(...items)` but doesn't overflow the stack
|
||||
function pushAll(target, items) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
target.push(items[i]);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Basically just str.split(","), but handling cases
|
||||
* where we have nested braced sections, which should be
|
||||
* treated as individual members, like {a,{b,c},d}
|
||||
*/
|
||||
function parseCommaParts(str) {
|
||||
if (!str) {
|
||||
return [''];
|
||||
}
|
||||
const parts = [];
|
||||
const m = (0, balanced_match_1.balanced)('{', '}', str);
|
||||
if (!m) {
|
||||
return str.split(',');
|
||||
// Walk the brace groups iteratively. Recursing on `post` once per group let a
|
||||
// chain of them exhaust the stack - the parsing-side counterpart to
|
||||
// the `expand_` overflow fixed for CVE-2026-14257, and not something `max` or
|
||||
// `maxLength` can bound, since it happens before expansion.
|
||||
//
|
||||
// The part the next chunk continues
|
||||
let carry = '';
|
||||
for (;;) {
|
||||
const m = (0, balanced_match_1.balanced)('{', '}', str);
|
||||
if (!m) {
|
||||
const tail = str.split(',');
|
||||
tail[0] = carry + tail[0];
|
||||
pushAll(parts, tail);
|
||||
return parts;
|
||||
}
|
||||
const { pre, body, post } = m;
|
||||
const p = pre.split(',');
|
||||
p[0] = carry + p[0];
|
||||
p[p.length - 1] += '{' + body + '}';
|
||||
if (!post.length) {
|
||||
pushAll(parts, p);
|
||||
return parts;
|
||||
}
|
||||
carry = p.pop();
|
||||
pushAll(parts, p);
|
||||
str = post;
|
||||
}
|
||||
const { pre, body, post } = m;
|
||||
const p = pre.split(',');
|
||||
p[p.length - 1] += '{' + body + '}';
|
||||
const postParts = parseCommaParts(post);
|
||||
if (post.length) {
|
||||
;
|
||||
p[p.length - 1] += postParts.shift();
|
||||
p.push.apply(p, postParts);
|
||||
}
|
||||
parts.push.apply(parts, p);
|
||||
return parts;
|
||||
}
|
||||
function expand(str, options = {}) {
|
||||
if (!str) {
|
||||
return [];
|
||||
}
|
||||
const { max = exports.EXPANSION_MAX, maxLength = exports.EXPANSION_MAX_LENGTH } = options;
|
||||
const { max = exports.EXPANSION_MAX, maxLength = exports.EXPANSION_MAX_LENGTH, maxDepth = exports.EXPANSION_MAX_DEPTH, maxRewrites = exports.EXPANSION_MAX_REWRITES, } = options;
|
||||
// I don't know why Bash 4.3 does this, but it does.
|
||||
// Anything starting with {} will have the first two bytes preserved
|
||||
// but *only* at the top level, so {},a}b will not expand to anything,
|
||||
@@ -89,7 +122,7 @@ function expand(str, options = {}) {
|
||||
if (str.slice(0, 2) === '{}') {
|
||||
str = '\\{\\}' + str.slice(2);
|
||||
}
|
||||
return expand_(escapeBraces(str), max, maxLength, true).map(unescapeBraces);
|
||||
return expand_(escapeBraces(str), max, maxLength, maxDepth, 0, maxRewrites, true).map(unescapeBraces);
|
||||
}
|
||||
function embrace(str) {
|
||||
return '{' + str + '}';
|
||||
@@ -184,7 +217,13 @@ function expandSequence(body, isAlphaSequence, max, maxLength) {
|
||||
}
|
||||
return N;
|
||||
}
|
||||
function expand_(str, max, maxLength, isTop) {
|
||||
function expand_(str, max, maxLength, maxDepth, depth, maxRewrites, isTop) {
|
||||
// Too deeply nested to keep following: treat the rest as literal, the same
|
||||
// way a group that cannot expand is already handled. Truncating rather than
|
||||
// throwing keeps `expand` total, matching `max` and `maxLength`.
|
||||
if (depth > maxDepth) {
|
||||
return [str];
|
||||
}
|
||||
// Consume the string's top-level brace groups left to right, threading a
|
||||
// running set of combined prefixes (`acc`). Expanding the tail iteratively -
|
||||
// rather than recursing on `m.post` once per group - keeps the native stack
|
||||
@@ -196,6 +235,9 @@ function expand_(str, max, maxLength, isTop) {
|
||||
// comma set - a sequence like `{a..\}` may legitimately yield ''. The drop
|
||||
// is on the final strings, so it is applied to whichever `combine` produces
|
||||
// them (the one with no brace set left in the tail).
|
||||
// How many times the `{a},b}` rewrite below has restarted the scan. Each pass
|
||||
// re-reads the whole string, so leaving this unbounded is quadratic.
|
||||
let rewrites = 0;
|
||||
let dropEmpties = false;
|
||||
let firstGroup = true;
|
||||
for (;;) {
|
||||
@@ -220,7 +262,8 @@ function expand_(str, max, maxLength, isTop) {
|
||||
const isOptions = m.body.indexOf(',') >= 0;
|
||||
if (!isSequence && !isOptions) {
|
||||
// {a},b}
|
||||
if (m.post.match(/,(?!,).*\}/)) {
|
||||
if (rewrites < maxRewrites && m.post.match(/,(?!,).*\}/)) {
|
||||
rewrites++;
|
||||
str = m.pre + '{' + m.body + escClose + m.post;
|
||||
isTop = true;
|
||||
continue;
|
||||
@@ -240,7 +283,7 @@ function expand_(str, max, maxLength, isTop) {
|
||||
let n = parseCommaParts(m.body);
|
||||
if (n.length === 1 && n[0] !== undefined) {
|
||||
// x{{a,b}}y ==> x{a}y x{b}y
|
||||
n = expand_(n[0], max, maxLength, false).map(embrace);
|
||||
n = expand_(n[0], max, maxLength, maxDepth, depth + 1, maxRewrites, false).map(embrace);
|
||||
//XXX is this necessary? Can't seem to hit it in tests.
|
||||
/* c8 ignore start */
|
||||
if (n.length === 1) {
|
||||
@@ -266,12 +309,13 @@ function expand_(str, max, maxLength, isTop) {
|
||||
values = [];
|
||||
let valuesLength = 0;
|
||||
outer: for (let j = 0; j < n.length; j++) {
|
||||
const expanded = expand_(n[j], max, maxLength, false);
|
||||
const expanded = expand_(n[j], max, maxLength, maxDepth, depth + 1, maxRewrites, false);
|
||||
for (let k = 0; k < expanded.length; k++) {
|
||||
const v = expanded[k];
|
||||
if (dropsEmpties && !v)
|
||||
continue;
|
||||
if (values.length >= max || valuesLength + v.length > maxLength) {
|
||||
if (values.length >= max ||
|
||||
valuesLength + v.length > maxLength) {
|
||||
break outer;
|
||||
}
|
||||
values.push(v);
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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 truncated
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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 truncated
|
||||
+4
@@ -1,8 +1,12 @@
|
||||
export declare const EXPANSION_MAX = 100000;
|
||||
export declare const EXPANSION_MAX_LENGTH = 4000000;
|
||||
export declare const EXPANSION_MAX_DEPTH = 1000;
|
||||
export declare const EXPANSION_MAX_REWRITES = 1000;
|
||||
export type BraceExpansionOptions = {
|
||||
max?: number;
|
||||
maxLength?: number;
|
||||
maxDepth?: number;
|
||||
maxRewrites?: number;
|
||||
};
|
||||
export declare function expand(str: string, options?: BraceExpansionOptions): string[];
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAkBA,eAAO,MAAM,aAAa,SAAU,CAAA;AAYpC,eAAO,MAAM,oBAAoB,UAAY,CAAA;AAwD7C,MAAM,MAAM,qBAAqB,GAAG;IAClC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,qBAA0B,YAkBtE"}
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAkBA,eAAO,MAAM,aAAa,SAAU,CAAA;AAYpC,eAAO,MAAM,oBAAoB,UAAY,CAAA;AAU7C,eAAO,MAAM,mBAAmB,OAAQ,CAAA;AAUxC,eAAO,MAAM,sBAAsB,OAAQ,CAAA;AAyE3C,MAAM,MAAM,qBAAqB,GAAG;IAClC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA;AAED,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,qBAA0B,YA+BtE"}
|
||||
+68
-24
@@ -26,6 +26,24 @@ export const EXPANSION_MAX = 100_000;
|
||||
// realistic expansion (100k results hitting `EXPANSION_MAX` measure ~1M
|
||||
// characters) so legitimate input is unaffected.
|
||||
export const EXPANSION_MAX_LENGTH = 4_000_000;
|
||||
// `expand_` recurses once per level of brace *nesting* - both when expanding a
|
||||
// set's comma members and when re-wrapping a set whose body is a single part.
|
||||
// The CVE-2026-14257 fix made the *tail* iterative (recursion on `m.post`, one
|
||||
// level per chained group), which left nesting depth unbounded: about 3,100
|
||||
// levels of `{{{...a,b...}}}` - only ~6KB of input - exhausted the native stack
|
||||
// and crashed the process. `EXPANSION_MAX_DEPTH` bounds how deep the parser
|
||||
// will follow nesting. It sits far above any realistic pattern and well below
|
||||
// the depth at which the stack runs out.
|
||||
export const EXPANSION_MAX_DEPTH = 1_000;
|
||||
// Bash keeps a quirk where a brace group followed by a comma set still expands
|
||||
// (`{a},b}`). The parser implements it by rewriting the string and restarting
|
||||
// the scan, absorbing one `}` per pass. `n` trailing braces therefore cost `n`
|
||||
// full passes over a string that itself grows by one `escClose` sentinel each
|
||||
// time - quadratic in `n`, with a ~26x constant from the sentinel's length.
|
||||
// 128KB of `'{a}' + '}'.repeat(n) + ',z}'` blocked the event loop for 27
|
||||
// seconds to produce two results. `EXPANSION_MAX_REWRITES` bounds how many
|
||||
// times the scan may restart. Real `{a},b}` input needs a handful.
|
||||
export const EXPANSION_MAX_REWRITES = 1_000;
|
||||
function numeric(str) {
|
||||
return !isNaN(str) ? parseInt(str, 10) : str.charCodeAt(0);
|
||||
}
|
||||
@@ -45,37 +63,52 @@ function unescapeBraces(str) {
|
||||
.replace(escCommaPattern, ',')
|
||||
.replace(escPeriodPattern, '.');
|
||||
}
|
||||
// Like `target.push(...items)` but doesn't overflow the stack
|
||||
function pushAll(target, items) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
target.push(items[i]);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Basically just str.split(","), but handling cases
|
||||
* where we have nested braced sections, which should be
|
||||
* treated as individual members, like {a,{b,c},d}
|
||||
*/
|
||||
function parseCommaParts(str) {
|
||||
if (!str) {
|
||||
return [''];
|
||||
}
|
||||
const parts = [];
|
||||
const m = balanced('{', '}', str);
|
||||
if (!m) {
|
||||
return str.split(',');
|
||||
// Walk the brace groups iteratively. Recursing on `post` once per group let a
|
||||
// chain of them exhaust the stack - the parsing-side counterpart to
|
||||
// the `expand_` overflow fixed for CVE-2026-14257, and not something `max` or
|
||||
// `maxLength` can bound, since it happens before expansion.
|
||||
//
|
||||
// The part the next chunk continues
|
||||
let carry = '';
|
||||
for (;;) {
|
||||
const m = balanced('{', '}', str);
|
||||
if (!m) {
|
||||
const tail = str.split(',');
|
||||
tail[0] = carry + tail[0];
|
||||
pushAll(parts, tail);
|
||||
return parts;
|
||||
}
|
||||
const { pre, body, post } = m;
|
||||
const p = pre.split(',');
|
||||
p[0] = carry + p[0];
|
||||
p[p.length - 1] += '{' + body + '}';
|
||||
if (!post.length) {
|
||||
pushAll(parts, p);
|
||||
return parts;
|
||||
}
|
||||
carry = p.pop();
|
||||
pushAll(parts, p);
|
||||
str = post;
|
||||
}
|
||||
const { pre, body, post } = m;
|
||||
const p = pre.split(',');
|
||||
p[p.length - 1] += '{' + body + '}';
|
||||
const postParts = parseCommaParts(post);
|
||||
if (post.length) {
|
||||
;
|
||||
p[p.length - 1] += postParts.shift();
|
||||
p.push.apply(p, postParts);
|
||||
}
|
||||
parts.push.apply(parts, p);
|
||||
return parts;
|
||||
}
|
||||
export function expand(str, options = {}) {
|
||||
if (!str) {
|
||||
return [];
|
||||
}
|
||||
const { max = EXPANSION_MAX, maxLength = EXPANSION_MAX_LENGTH } = options;
|
||||
const { max = EXPANSION_MAX, maxLength = EXPANSION_MAX_LENGTH, maxDepth = EXPANSION_MAX_DEPTH, maxRewrites = EXPANSION_MAX_REWRITES, } = options;
|
||||
// I don't know why Bash 4.3 does this, but it does.
|
||||
// Anything starting with {} will have the first two bytes preserved
|
||||
// but *only* at the top level, so {},a}b will not expand to anything,
|
||||
@@ -85,7 +118,7 @@ export function expand(str, options = {}) {
|
||||
if (str.slice(0, 2) === '{}') {
|
||||
str = '\\{\\}' + str.slice(2);
|
||||
}
|
||||
return expand_(escapeBraces(str), max, maxLength, true).map(unescapeBraces);
|
||||
return expand_(escapeBraces(str), max, maxLength, maxDepth, 0, maxRewrites, true).map(unescapeBraces);
|
||||
}
|
||||
function embrace(str) {
|
||||
return '{' + str + '}';
|
||||
@@ -180,7 +213,13 @@ function expandSequence(body, isAlphaSequence, max, maxLength) {
|
||||
}
|
||||
return N;
|
||||
}
|
||||
function expand_(str, max, maxLength, isTop) {
|
||||
function expand_(str, max, maxLength, maxDepth, depth, maxRewrites, isTop) {
|
||||
// Too deeply nested to keep following: treat the rest as literal, the same
|
||||
// way a group that cannot expand is already handled. Truncating rather than
|
||||
// throwing keeps `expand` total, matching `max` and `maxLength`.
|
||||
if (depth > maxDepth) {
|
||||
return [str];
|
||||
}
|
||||
// Consume the string's top-level brace groups left to right, threading a
|
||||
// running set of combined prefixes (`acc`). Expanding the tail iteratively -
|
||||
// rather than recursing on `m.post` once per group - keeps the native stack
|
||||
@@ -192,6 +231,9 @@ function expand_(str, max, maxLength, isTop) {
|
||||
// comma set - a sequence like `{a..\}` may legitimately yield ''. The drop
|
||||
// is on the final strings, so it is applied to whichever `combine` produces
|
||||
// them (the one with no brace set left in the tail).
|
||||
// How many times the `{a},b}` rewrite below has restarted the scan. Each pass
|
||||
// re-reads the whole string, so leaving this unbounded is quadratic.
|
||||
let rewrites = 0;
|
||||
let dropEmpties = false;
|
||||
let firstGroup = true;
|
||||
for (;;) {
|
||||
@@ -216,7 +258,8 @@ function expand_(str, max, maxLength, isTop) {
|
||||
const isOptions = m.body.indexOf(',') >= 0;
|
||||
if (!isSequence && !isOptions) {
|
||||
// {a},b}
|
||||
if (m.post.match(/,(?!,).*\}/)) {
|
||||
if (rewrites < maxRewrites && m.post.match(/,(?!,).*\}/)) {
|
||||
rewrites++;
|
||||
str = m.pre + '{' + m.body + escClose + m.post;
|
||||
isTop = true;
|
||||
continue;
|
||||
@@ -236,7 +279,7 @@ function expand_(str, max, maxLength, isTop) {
|
||||
let n = parseCommaParts(m.body);
|
||||
if (n.length === 1 && n[0] !== undefined) {
|
||||
// x{{a,b}}y ==> x{a}y x{b}y
|
||||
n = expand_(n[0], max, maxLength, false).map(embrace);
|
||||
n = expand_(n[0], max, maxLength, maxDepth, depth + 1, maxRewrites, false).map(embrace);
|
||||
//XXX is this necessary? Can't seem to hit it in tests.
|
||||
/* c8 ignore start */
|
||||
if (n.length === 1) {
|
||||
@@ -262,12 +305,13 @@ function expand_(str, max, maxLength, isTop) {
|
||||
values = [];
|
||||
let valuesLength = 0;
|
||||
outer: for (let j = 0; j < n.length; j++) {
|
||||
const expanded = expand_(n[j], max, maxLength, false);
|
||||
const expanded = expand_(n[j], max, maxLength, maxDepth, depth + 1, maxRewrites, false);
|
||||
for (let k = 0; k < expanded.length; k++) {
|
||||
const v = expanded[k];
|
||||
if (dropsEmpties && !v)
|
||||
continue;
|
||||
if (values.length >= max || valuesLength + v.length > maxLength) {
|
||||
if (values.length >= max ||
|
||||
valuesLength + v.length > maxLength) {
|
||||
break outer;
|
||||
}
|
||||
values.push(v);
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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 truncated
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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 truncated
|
||||
+2
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "brace-expansion",
|
||||
"description": "Brace expansion as known from sh/bash",
|
||||
"version": "5.0.9",
|
||||
"version": "5.0.12",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
@@ -29,6 +29,7 @@
|
||||
"test": "tap",
|
||||
"snap": "tap",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"benchmark": "node benchmark/index.js",
|
||||
"typedoc": "typedoc --tsconfig .tshy/esm.json ./src/*.ts"
|
||||
},
|
||||
|
||||
+157
@@ -1,5 +1,162 @@
|
||||
# CHANGELOG
|
||||
|
||||
## [10.1.7](https://github.com/nodemailer/nodemailer/compare/v10.1.6...v10.1.7) (2026-10-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **mime-funcs:** encode parameter values that look like encoded words ([651d79b](https://github.com/nodemailer/nodemailer/commit/651d79b0e1067b93b0ae8fc1afbdf383a46c214d))
|
||||
|
||||
## [10.1.6](https://github.com/nodemailer/nodemailer/compare/v10.1.5...v10.1.6) (2026-10-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **addressparser:** read groups in the same pass as the rest of the list ([ddf0de1](https://github.com/nodemailer/nodemailer/commit/ddf0de1e7615908101bf593c62edcadccc05c8d3))
|
||||
|
||||
## [10.1.5](https://github.com/nodemailer/nodemailer/compare/v10.1.4...v10.1.5) (2026-10-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **addressparser:** handle an address list without copying it per address ([6bd09b3](https://github.com/nodemailer/nodemailer/commit/6bd09b395cb7ffd1b0ae95f960f3985ba9c25afb))
|
||||
* **addressparser:** tokenize into offsets instead of an object per token ([3fd3f50](https://github.com/nodemailer/nodemailer/commit/3fd3f50ce86671884896ef1eaddafff5e137f820))
|
||||
|
||||
## [10.1.4](https://github.com/nodemailer/nodemailer/compare/v10.1.3...v10.1.4) (2026-10-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **addressparser:** apply the unexpected "<" rule outside quotes and comments ([ee35f57](https://github.com/nodemailer/nodemailer/commit/ee35f5745658bf46952651fa53207255e9dc3406))
|
||||
* **addressparser:** bound group nesting at ten levels ([ce63303](https://github.com/nodemailer/nodemailer/commit/ce633031d58f8d2e9c9b0f5b671944a1afe71f95))
|
||||
* **addressparser:** drop the obsolete route from an angle-addr ([3dbc730](https://github.com/nodemailer/nodemailer/commit/3dbc7304b0cfa06551543fa2dc01222447c2b554))
|
||||
* **addressparser:** end a comment only at the ")" that closes the outermost one ([0a87617](https://github.com/nodemailer/nodemailer/commit/0a8761762ac78f2db2ba37f712f2a52a404ce011))
|
||||
* **addressparser:** honour quoted strings and comments inside an angle-addr and a group ([1cd57d2](https://github.com/nodemailer/nodemailer/commit/1cd57d2b8c49dff048c21cdbafdb54439a7f07e2))
|
||||
* **addressparser:** never read a mailbox out of a comment ([a1538ea](https://github.com/nodemailer/nodemailer/commit/a1538ea39c76c48a4134806c41e31e59a258b1e5))
|
||||
* **addressparser:** read CFWS beside the "@" of an addr-spec as nothing ([ba3c095](https://github.com/nodemailer/nodemailer/commit/ba3c0952542581f95cb20d9322cc9e27baa470a2))
|
||||
* **addressparser:** read the first addr-spec of the display text ([038940a](https://github.com/nodemailer/nodemailer/commit/038940a069a2cecb64a70d0e81b17eb8ce200d3a))
|
||||
* split a domain literal off at the "@" in front of it ([d5983b0](https://github.com/nodemailer/nodemailer/commit/d5983b0ae234cd22ab45be817fefd434e9a53d58))
|
||||
|
||||
## [10.1.3](https://github.com/nodemailer/nodemailer/compare/v10.1.2...v10.1.3) (2026-10-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* format References and strip URLs in linear time, release any number of streams ([2f18641](https://github.com/nodemailer/nodemailer/commit/2f186418cf6c8fff18f235d4318892fd42f810fa))
|
||||
|
||||
## [10.1.2](https://github.com/nodemailer/nodemailer/compare/v10.1.1...v10.1.2) (2026-10-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bound the work a message can make the mailer do and keep per-message auth off the transport's token endpoint ([e347fa2](https://github.com/nodemailer/nodemailer/commit/e347fa2260a7e342e772a7e187937aa87ac96364))
|
||||
|
||||
## [10.1.1](https://github.com/nodemailer/nodemailer/compare/v10.1.0...v10.1.1) (2026-10-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **fetch:** strip credentials on any cross-origin redirect ([a15360c](https://github.com/nodemailer/nodemailer/commit/a15360cd2cd12e4670e96ab0b7290e8c862391cf))
|
||||
|
||||
## [10.1.0](https://github.com/nodemailer/nodemailer/compare/v10.0.16...v10.1.0) (2026-10-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* harden message streams, SMTP connections and the connection pool ([8009da6](https://github.com/nodemailer/nodemailer/commit/8009da6e10bb8e10d7f0948a25d65895fdcd64f1))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* do not read the environment or the interface table on import ([094179d](https://github.com/nodemailer/nodemailer/commit/094179d176d2ebe7a28742411ad427d3c87a3981))
|
||||
* end the SMTP session on any 421 reply and drive the envelope through one step ([5638ac1](https://github.com/nodemailer/nodemailer/commit/5638ac1914342460df270f736e7ceb0a844cf83a))
|
||||
* keep ERR_ACCESS_DENIED from the Node.js permission model ([8b33a3d](https://github.com/nodemailer/nodemailer/commit/8b33a3d4fedee3431b97da7da12af954c3c8dceb))
|
||||
* let the proxy handshake and the SMTP connection share one deadline ([91ed683](https://github.com/nodemailer/nodemailer/commit/91ed683f7a1e9ff8c56e24fb76facf16af08b162))
|
||||
* **mime-node:** decode an SMTPUTF8 domain from its A-label form ([e436f4d](https://github.com/nodemailer/nodemailer/commit/e436f4dccbc8b0fb57b4747ebaafa5bb035c15cb))
|
||||
* **mime-node:** finish an encoded part only after its output was read ([ec97d8f](https://github.com/nodemailer/nodemailer/commit/ec97d8f679a1d9d47bbebb03ea064f5fa6f58d01))
|
||||
* **qp:** encode a lone CR or LF in binary quoted-printable parts ([42b91d9](https://github.com/nodemailer/nodemailer/commit/42b91d9a0fe18eea412c9774c5c142adf5b3efc7))
|
||||
* **smtp-connection:** count the wait for the greeting against connectionTimeout ([bdcd046](https://github.com/nodemailer/nodemailer/commit/bdcd0462cab163eca3c34058d09b9981ab671c47))
|
||||
* **smtp-connection:** drop the connection when DATA is taken after a refused sender ([f90fb04](https://github.com/nodemailer/nodemailer/commit/f90fb045bc24f96a128710791b2fa443e64f55fd))
|
||||
* **smtp-connection:** refuse a second connect() call ([24ba2e0](https://github.com/nodemailer/nodemailer/commit/24ba2e0847ecb8202540f1c5e53483bd119ae4f2))
|
||||
* turn Nagle off for SMTP sockets and report an early sendmail exit as such ([fa9b8d3](https://github.com/nodemailer/nodemailer/commit/fa9b8d300bf642cd9e11f6732ebb312c09868677))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* encode base64 and quoted-printable without per-chunk delays ([fd377bb](https://github.com/nodemailer/nodemailer/commit/fd377bbf559363860aedbedba6f107348fc152d2))
|
||||
|
||||
## [10.0.16](https://github.com/nodemailer/nodemailer/compare/v10.0.15...v10.0.16) (2026-10-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **addressparser:** keep an escaped parenthesis inside a comment ([c4ae20d](https://github.com/nodemailer/nodemailer/commit/c4ae20d074ac0510cc2d0cbd6039ee7fab3a4ca8))
|
||||
* **fetch:** keep the case of cookie names ([#1888](https://github.com/nodemailer/nodemailer/issues/1888)) ([9f16eed](https://github.com/nodemailer/nodemailer/commit/9f16eedf8de0485e6d07969113db992ff70f89ce))
|
||||
* **fetch:** read the cookie name-value pair by position ([645e97c](https://github.com/nodemailer/nodemailer/commit/645e97c8f0586404d9fb861c9527be7572c2c832))
|
||||
|
||||
## [10.0.15](https://github.com/nodemailer/nodemailer/compare/v10.0.14...v10.0.15) (2026-10-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **errors:** inherit NodemailerError code from ErrnoException ([e99db61](https://github.com/nodemailer/nodemailer/commit/e99db61d51c244e184a3a79103d1d732ffb6376b)), closes [#1884](https://github.com/nodemailer/nodemailer/issues/1884)
|
||||
* **errors:** keep NodemailerError compatible with @types/node 26 ErrnoException ([#1885](https://github.com/nodemailer/nodemailer/issues/1885)) ([b660328](https://github.com/nodemailer/nodemailer/commit/b6603286cc45bff52c13cc74e5ac47f424452fdc))
|
||||
|
||||
## [10.0.14](https://github.com/nodemailer/nodemailer/compare/v10.0.13...v10.0.14) (2026-10-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **addressparser:** keep a quoted display name that holds no "@" out of the address ([3570d26](https://github.com/nodemailer/nodemailer/commit/3570d26c7b55f6e5df50d4076046600ad3fd040b))
|
||||
* **addressparser:** keep the group recursion depth out of the options object ([a680254](https://github.com/nodemailer/nodemailer/commit/a68025405a1378c82e78f64ffad27b92c1035cbe))
|
||||
* **addressparser:** stop a "[" from hiding the operators after it ([5619784](https://github.com/nodemailer/nodemailer/commit/561978491d5cc33d2052a6c89a499b838b960262))
|
||||
* **dkim:** trim a header field name in linear time ([c6f7a55](https://github.com/nodemailer/nodemailer/commit/c6f7a55a2978bf031b8519ebf59baddf94c032c8))
|
||||
* **mime-funcs:** read and write header parameters per rfc2045 and rfc2231 ([b8ccad7](https://github.com/nodemailer/nodemailer/commit/b8ccad723a7e49a674a4d70f32eb9fe77312008d))
|
||||
* search only the new bytes for the end of the proxy CONNECT response ([81efd7b](https://github.com/nodemailer/nodemailer/commit/81efd7bfc559305f900d6a22b5c56aa94556002b))
|
||||
|
||||
## [10.0.13](https://github.com/nodemailer/nodemailer/compare/v10.0.12...v10.0.13) (2026-09-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* read the advertised SASL methods without backtracking regexes ([b5a896f](https://github.com/nodemailer/nodemailer/commit/b5a896fcb02df1628329d7cd665ca08490b14b56))
|
||||
* strip comments inside an angle-addr before it becomes the address ([a502247](https://github.com/nodemailer/nodemailer/commit/a502247555fd81773eac337486781f3b6008e16a))
|
||||
|
||||
## [10.0.12](https://github.com/nodemailer/nodemailer/compare/v10.0.11...v10.0.12) (2026-09-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* settle every send on a connection error, back off pool requeues, turn a bare CR into CRLF, bound fetch, honour requireTLS ([63ccd66](https://github.com/nodemailer/nodemailer/commit/63ccd66c894bbaac18c82084ee385bf6488226cf))
|
||||
|
||||
## [10.0.11](https://github.com/nodemailer/nodemailer/compare/v10.0.10...v10.0.11) (2026-09-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **fetch:** report a form body that can not be encoded through the returned stream ([74d40bf](https://github.com/nodemailer/nodemailer/commit/74d40bf1e4553bc0568b471250a9251bcb569b00))
|
||||
* keep the CommonJS entry point and the services subpath compatible with the pre-TypeScript build ([52901ef](https://github.com/nodemailer/nodemailer/commit/52901ef3725ad179aa56276495a60f53b2c83516))
|
||||
* **qp:** keep wrap() terminating for short line lengths and a trailing incomplete escape ([8fa140b](https://github.com/nodemailer/nodemailer/commit/8fa140b11c62cb1202eac84d35bb3ba28bc3b1a1))
|
||||
* **smtp-connection:** fail a password login cleanly when the server offers only XOAUTH2 ([90abf7d](https://github.com/nodemailer/nodemailer/commit/90abf7d11db5ebfeb4292b62c1fff969741c7d33))
|
||||
* **types:** restore the layout of @types/nodemailer in the bundled declarations ([ac2e40f](https://github.com/nodemailer/nodemailer/commit/ac2e40ffc69427e0eb33ce3945fae76c4c828c8d))
|
||||
|
||||
## [10.0.10](https://github.com/nodemailer/nodemailer/compare/v10.0.9...v10.0.10) (2026-09-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* derive the attachment filename from the basename of a Windows path ([c7cc7ce](https://github.com/nodemailer/nodemailer/commit/c7cc7ce41a3602441747476a2a2c4a8ff466a83e))
|
||||
* **dkim:** unfold folded header lines in linear time ([28a5909](https://github.com/nodemailer/nodemailer/commit/28a5909cec27646cb001dc7e97a8f5d26c973078))
|
||||
* **smtp-connection:** reassemble multiline replies in linear time ([f2d82fa](https://github.com/nodemailer/nodemailer/commit/f2d82fa84d47015a7bbb4253da61eeb778c658b1))
|
||||
|
||||
## [10.0.9](https://github.com/nodemailer/nodemailer/compare/v10.0.8...v10.0.9) (2026-09-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **addressparser:** bound the '@' probe to the run being scanned ([1465c3f](https://github.com/nodemailer/nodemailer/commit/1465c3f5ff74a7c4fbbe9853bd01448bb92bf5b7))
|
||||
* **addressparser:** keep the text after a comment out of a quoted local part address ([2f36eb1](https://github.com/nodemailer/nodemailer/commit/2f36eb1aa1dd33e312411dc9b888548e14db54ee))
|
||||
|
||||
## [10.0.8](https://github.com/nodemailer/nodemailer/compare/v10.0.7...v10.0.8) (2026-09-11)
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@ let configOptions = {
|
||||
|
||||
#### I have an issue with TypeScript types
|
||||
|
||||
Nodemailer 10 and later are written in TypeScript and ship their own type definitions, so `@types/nodemailer` is no longer needed and should be removed from your project to avoid conflicting declarations. The type names follow the layout of the old definitions, so references such as `Mail.Options`, `SMTPTransport.Options` or `Transporter<SMTPTransport.SentMessageInfo>` keep compiling, and the most used types (`SendMailOptions`, `Transporter`, `SentMessageInfo`, `Attachment`, `Address`) are exported from the package root. For older Nodemailer versions, the community maintained [type definitions](https://www.npmjs.com/package/@types/nodemailer) still apply.
|
||||
Nodemailer 10 and later are written in TypeScript and ship their own type definitions, so `@types/nodemailer` is no longer needed and should be removed from your project to avoid conflicting declarations. The type names follow the layout of the old definitions, so references such as `Mail.Options`, `SMTPTransport.Options` or `Transporter<SMTPTransport.SentMessageInfo>` keep compiling, and the most used types (`SendMailOptions`, `Transporter`, `SentMessageInfo`, `Attachment`, `Address`) are exported from the package root. The modules under `nodemailer/lib/` are declared with a default export, so import a class as `import SMTPTransport from 'nodemailer/lib/smtp-transport'` (with `esModuleInterop` in a CommonJS project). The `import SMTPTransport = require(...)` and `import * as SMTPTransport from ...` forms still resolve the type aliases such as `SMTPTransport.Options`, but they refer to the module rather than to the class, so the class itself has to be imported with a default import. For older Nodemailer versions, the community maintained [type definitions](https://www.npmjs.com/package/@types/nodemailer) still apply.
|
||||
|
||||
#### I have a different problem
|
||||
|
||||
|
||||
+103
-27
@@ -1,7 +1,9 @@
|
||||
# Security Policy
|
||||
|
||||
Nodemailer is a widely deployed, zero-dependency e-mail library. We take security
|
||||
reports seriously and aim to respond quickly.
|
||||
Nodemailer is a widely deployed, zero-dependency email library. We welcome
|
||||
responsible reports of vulnerabilities that present a credible security risk to
|
||||
users. Please distinguish security vulnerabilities from ordinary bugs, unusual
|
||||
edge cases, and hardening suggestions.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
@@ -17,45 +19,119 @@ the supported way to receive security updates.
|
||||
If you are on an older major, please upgrade. See the migration notes at
|
||||
<https://nodemailer.com/> before updating.
|
||||
|
||||
## What Qualifies as a Security Vulnerability
|
||||
|
||||
A security report should identify a realistic way for an attacker to cross a
|
||||
security boundary in a supported version of Nodemailer. Unexpected behavior,
|
||||
a failing test, or a technically correct proof of concept is not sufficient on
|
||||
its own to establish security impact.
|
||||
|
||||
In particular, explain:
|
||||
|
||||
- **Attacker capabilities:** Who is the attacker, and what inputs, systems, or
|
||||
responses can they control _without_ already having the privileges or secrets
|
||||
they are trying to gain?
|
||||
- **Attack path:** How does attacker-controlled input reach the affected code
|
||||
through a supported Nodemailer API or a realistic application integration?
|
||||
- **Prerequisites:** What configuration and user actions are required? If the
|
||||
attack depends on unusual options or deliberately malformed input, explain
|
||||
why those conditions could arise in a real application.
|
||||
- **Impact:** What confidentiality, integrity, or availability boundary is
|
||||
crossed, and what can the attacker achieve as a result?
|
||||
|
||||
A vulnerability involving an optional or less common feature can still qualify
|
||||
if these conditions are met. Conversely, the existence of a defensive check in
|
||||
the code does not, by itself, establish that every bypass of that check has a
|
||||
realistic security impact.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
**Please do not report security vulnerabilities through public GitHub issues,
|
||||
pull requests, or discussions.**
|
||||
**Do not disclose suspected exploitable vulnerabilities through public GitHub
|
||||
issues, pull requests, or discussions before coordinating with the maintainer.**
|
||||
|
||||
Report privately through one of the following channels:
|
||||
|
||||
1. **GitHub Security Advisories (preferred).** Open a private report at
|
||||
<https://github.com/nodemailer/nodemailer/security/advisories/new>. This keeps
|
||||
the discussion private until a fix is published and lets us credit you.
|
||||
2. **Email.** Send details to **andris@reinman.eu** (the contact listed in
|
||||
[`SECURITY.txt`](SECURITY.txt)). Encrypt sensitive details if possible.
|
||||
1. **GitHub Security Advisories (preferred):**
|
||||
<https://github.com/nodemailer/nodemailer/security/advisories/new>.
|
||||
2. **Email:** **andris@reinman.eu**, as listed in
|
||||
[`SECURITY.txt`](SECURITY.txt). Encrypt sensitive details if possible.
|
||||
|
||||
When reporting, please include as much of the following as you can:
|
||||
### Required Information
|
||||
|
||||
- The affected version(s) and environment (Node.js version, OS).
|
||||
- The component involved (e.g. SMTP connection, address parsing, MIME/header
|
||||
generation, DKIM).
|
||||
- A clear description of the issue and its impact (e.g. header/SMTP command
|
||||
injection, information disclosure, DoS).
|
||||
- A minimal proof of concept or reproduction steps.
|
||||
- Any suggested remediation, if you have one.
|
||||
Please provide enough information to evaluate both the defect and its security
|
||||
impact. Reports should include:
|
||||
|
||||
Nodemailer is maintained by a single person, so there is no guaranteed response
|
||||
time: sometimes reports are handled within hours, sometimes they take longer.
|
||||
Accepted issues are fixed in a new release and coordinated through a GitHub
|
||||
Security Advisory, and reporters who wish to be named are credited.
|
||||
1. The affected Nodemailer version or commit, Node.js version, and relevant
|
||||
configuration.
|
||||
2. A concise description of the issue and the affected public API or code path.
|
||||
3. A minimal, tested reproduction, including steps to run it, expected behavior,
|
||||
and observed behavior.
|
||||
4. A concrete threat model: attacker control, prerequisites, trust boundary, and
|
||||
resulting impact. Clearly distinguish what your reproduction proves from what
|
||||
you believe might be possible.
|
||||
5. An explanation of why the issue affects applications beyond a deliberately
|
||||
constructed test setup, where this is not obvious.
|
||||
|
||||
## CVEs
|
||||
A suggested fix is welcome but does not replace an explanation of exploitability.
|
||||
A complete real-world exploit is not always necessary, but the report must make
|
||||
a credible security impact clear.
|
||||
|
||||
We track and disclose vulnerabilities through GitHub Security Advisories. We do
|
||||
not request or manage CVE identifiers ourselves. If you need a CVE assigned for a
|
||||
reported issue, please request one yourself, for example through GitHub's own
|
||||
CVE request flow on the published advisory, or another CNA.
|
||||
### Reports That May Be Treated as Ordinary Bugs
|
||||
|
||||
The following generally do **not** qualify for private security handling unless
|
||||
you can demonstrate an additional, meaningful security boundary crossing:
|
||||
|
||||
- Issues that require the attacker to control trusted application code,
|
||||
administrator-only configuration, or the secret they supposedly extract.
|
||||
- Problems caused solely by application-level misuse, exposed credentials, or
|
||||
insecure deployment choices outside Nodemailer's control.
|
||||
- Contrived proof-of-concept setups where the tester controls both sides of a
|
||||
request, without explaining how a separate attacker could trigger the issue.
|
||||
- Parser quirks, unexpected behavior, and bypasses of internal checks with no
|
||||
demonstrated security impact.
|
||||
- Speculative findings based only on static analysis or an automated tool's
|
||||
assessment, without a verified reproduction and credible attack scenario.
|
||||
|
||||
These may still be valid bugs and may be considered through the normal issue
|
||||
tracker. Please coordinate before making any potentially exploitable details
|
||||
public. Classification as a bug does not guarantee a fix or a particular
|
||||
priority.
|
||||
|
||||
### Automated and AI-Assisted Reports
|
||||
|
||||
Automated analysis and AI tools are welcome as research aids, but the person
|
||||
submitting a report is responsible for verifying its claims. **Do not submit
|
||||
bulk, unverified, or speculative AI-generated findings.** Test each finding
|
||||
against a supported release and establish the actual security impact. Reports
|
||||
that do not meet the requirements above may be closed without further
|
||||
investigation or individual feedback; they can be resubmitted with stronger
|
||||
evidence.
|
||||
|
||||
## Triage and Disclosure
|
||||
|
||||
Nodemailer is maintained by a single person, and there is **no guaranteed
|
||||
response or resolution time**. Reports are prioritized based on demonstrated
|
||||
impact, exploitability, affected usage, and available maintenance resources.
|
||||
|
||||
A private report does not automatically become a security advisory. Confirmed
|
||||
vulnerabilities may be fixed and disclosed through GitHub Security Advisories;
|
||||
other findings may be closed, deferred, or handled as ordinary bugs. Please do
|
||||
not disclose details of an unresolved, potentially exploitable issue publicly
|
||||
without coordinating with the maintainer.
|
||||
|
||||
## CVEs and Credit
|
||||
|
||||
We do not request or manage CVE identifiers. A report being acknowledged, a bug
|
||||
being fixed, or a pull request being merged does **not** mean that Nodemailer
|
||||
has confirmed a security vulnerability or endorsed a CVE assignment. We do not
|
||||
guarantee advisories, CVE identifiers, or public credit for reports. For
|
||||
confirmed vulnerabilities disclosed through an advisory, substantive reporters
|
||||
may be credited if they wish.
|
||||
|
||||
## Scope
|
||||
|
||||
In scope: the `nodemailer` package source in this repository: message and MIME
|
||||
generation, SMTP/LMTP client behaviour, address parsing, header handling, DKIM
|
||||
generation, SMTP/LMTP client behavior, address parsing, header handling, DKIM
|
||||
signing, and the bundled transports.
|
||||
|
||||
Out of scope: vulnerabilities in your own application code, misconfiguration of
|
||||
|
||||
+16
-2
@@ -39,7 +39,21 @@ export type Address = MailboxAddress | GroupAddress;
|
||||
*
|
||||
* @param str Address field
|
||||
* @param options Optional options object
|
||||
* @param options._depth Internal recursion depth counter (do not set manually)
|
||||
* @return An array of address objects
|
||||
*/
|
||||
export default function addressparser(str?: string | null, options?: AddressParserOptions): Address[];
|
||||
declare function addressparser(str: string | null | undefined, options: AddressParserOptions & {
|
||||
flatten: true;
|
||||
}): MailboxAddress[];
|
||||
declare function addressparser(str?: string | null, options?: AddressParserOptions): Address[];
|
||||
/**
|
||||
* Type aliases in the layout of @types/nodemailer, so `addressparser.Address` style references keep working
|
||||
*/
|
||||
type AddressEntry = Address;
|
||||
declare namespace addressparser {
|
||||
type Address = MailboxAddress;
|
||||
type Group = GroupAddress;
|
||||
type AddressOrGroup = AddressEntry;
|
||||
}
|
||||
/** The names @types/nodemailer used for the group entry and for the union of both entry types */
|
||||
export type { GroupAddress as Group, Address as AddressOrGroup };
|
||||
export default addressparser;
|
||||
+840
-244
File diff suppressed because it is too large.
Load diff
+3
@@ -24,6 +24,9 @@ export interface EncoderOptions {
|
||||
/**
|
||||
* Creates a transform stream for encoding data to base64 encoding
|
||||
*
|
||||
* The output is the same as `wrap(encode(input), lineLength)` no matter how the input is split
|
||||
* into chunks: every line but the last one ends with a line break, the last one does not
|
||||
*
|
||||
* @constructor
|
||||
* @param options Stream options
|
||||
* @param [options.lineLength=76] Maximum length for lines, set to false to disable wrapping
|
||||
|
||||
+82
-42
@@ -16,6 +16,47 @@ function encode(buffer) {
|
||||
}
|
||||
return buffer.toString('base64');
|
||||
}
|
||||
/**
|
||||
* Turns a line length option into a whole number of characters, the default for anything unusable
|
||||
*/
|
||||
function normalizeLineLength(lineLength) {
|
||||
const length = Math.floor(Number(lineLength));
|
||||
return Number.isFinite(length) && length >= 1 ? length : 76;
|
||||
}
|
||||
/**
|
||||
* Splits the bytes of `src` into lines of `lineLength` bytes, each followed by a line break. With
|
||||
* `final` set the last line, which may be shorter, gets no line break; otherwise only complete
|
||||
* lines are taken and the rest is left for the caller
|
||||
*
|
||||
* @param src Bytes to wrap
|
||||
* @param lineLength Line length
|
||||
* @param final Whether `src` ends the output
|
||||
* @returns The wrapped bytes and the number of trailing bytes not taken
|
||||
*/
|
||||
function wrapBuffer(src, lineLength, final) {
|
||||
const lines = Math.ceil(src.length / lineLength);
|
||||
// the last line waits for more data unless this is the end: whether it gets a line break
|
||||
// depends on whether anything follows it
|
||||
const complete = Math.max(lines - 1, 0);
|
||||
let rest = src.length - complete * lineLength;
|
||||
const output = Buffer.allocUnsafe(complete * (lineLength + 2) + (final ? rest : 0));
|
||||
let to = 0;
|
||||
for (let from = 0; from < complete * lineLength; from += lineLength) {
|
||||
src.copy(output, to, from, from + lineLength);
|
||||
to += lineLength;
|
||||
output[to++] = 0x0d;
|
||||
output[to++] = 0x0a;
|
||||
}
|
||||
if (final) {
|
||||
to += src.copy(output, to, complete * lineLength);
|
||||
rest = 0;
|
||||
}
|
||||
if (to !== output.length) {
|
||||
// never hand out bytes of the unfilled allocation
|
||||
throw new Error('Unexpected wrapped length');
|
||||
}
|
||||
return { output, rest };
|
||||
}
|
||||
/**
|
||||
* Adds soft line breaks to a base64 string
|
||||
*
|
||||
@@ -25,7 +66,8 @@ function encode(buffer) {
|
||||
*/
|
||||
function wrap(str, lineLength) {
|
||||
str = (str || '').toString();
|
||||
lineLength = lineLength || 76;
|
||||
// a negative length would step backwards through the input and never finish
|
||||
lineLength = Math.max(Number(lineLength) || 76, 1);
|
||||
if (str.length <= lineLength) {
|
||||
return str;
|
||||
}
|
||||
@@ -43,6 +85,9 @@ function wrap(str, lineLength) {
|
||||
/**
|
||||
* Creates a transform stream for encoding data to base64 encoding
|
||||
*
|
||||
* The output is the same as `wrap(encode(input), lineLength)` no matter how the input is split
|
||||
* into chunks: every line but the last one ends with a line break, the last one does not
|
||||
*
|
||||
* @constructor
|
||||
* @param options Stream options
|
||||
* @param [options.lineLength=76] Maximum length for lines, set to false to disable wrapping
|
||||
@@ -52,66 +97,61 @@ class Encoder extends node_stream_1.Transform {
|
||||
super();
|
||||
this.options = options || {};
|
||||
if (this.options.lineLength !== false) {
|
||||
this.options.lineLength = this.options.lineLength || 76;
|
||||
this.options.lineLength = normalizeLineLength(this.options.lineLength);
|
||||
}
|
||||
this._curLine = '';
|
||||
this._remainingBytes = false;
|
||||
this.inputBytes = 0;
|
||||
this.outputBytes = 0;
|
||||
}
|
||||
/**
|
||||
* Emits the encoded characters `b64` that follow the current line, keeping what can not be
|
||||
* emitted yet as the new current line
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
_emit(b64, final) {
|
||||
const src = Buffer.from(this._curLine + b64, 'latin1');
|
||||
if (!src.length) {
|
||||
return;
|
||||
}
|
||||
let output = src;
|
||||
if (this.options.lineLength) {
|
||||
const wrapped = wrapBuffer(src, this.options.lineLength, final);
|
||||
output = wrapped.output;
|
||||
this._curLine = wrapped.rest ? src.toString('latin1', src.length - wrapped.rest) : '';
|
||||
}
|
||||
else {
|
||||
this._curLine = '';
|
||||
}
|
||||
if (output.length) {
|
||||
this.outputBytes += output.length;
|
||||
this.push(output);
|
||||
}
|
||||
}
|
||||
/** @internal */
|
||||
_transform(chunk, encoding, done) {
|
||||
let buf = encoding !== 'buffer' ? Buffer.from(chunk, encoding) : chunk;
|
||||
if (!buf || !buf.length) {
|
||||
setImmediate(done);
|
||||
return;
|
||||
return done();
|
||||
}
|
||||
this.inputBytes += buf.length;
|
||||
if (this._remainingBytes && this._remainingBytes.length) {
|
||||
if (this._remainingBytes) {
|
||||
buf = Buffer.concat([this._remainingBytes, buf], this._remainingBytes.length + buf.length);
|
||||
this._remainingBytes = false;
|
||||
}
|
||||
if (buf.length % 3) {
|
||||
this._remainingBytes = buf.slice(buf.length - (buf.length % 3));
|
||||
buf = buf.slice(0, buf.length - (buf.length % 3));
|
||||
const extra = buf.length % 3;
|
||||
if (extra) {
|
||||
this._remainingBytes = buf.subarray(buf.length - extra);
|
||||
buf = buf.subarray(0, buf.length - extra);
|
||||
}
|
||||
else {
|
||||
this._remainingBytes = false;
|
||||
}
|
||||
let b64 = this._curLine + encode(buf);
|
||||
if (this.options.lineLength) {
|
||||
b64 = wrap(b64, this.options.lineLength);
|
||||
// remove last line as it is still most probably incomplete
|
||||
const lastLF = b64.lastIndexOf('\n');
|
||||
if (lastLF < 0) {
|
||||
this._curLine = b64;
|
||||
b64 = '';
|
||||
}
|
||||
else if (lastLF === b64.length - 1) {
|
||||
this._curLine = '';
|
||||
}
|
||||
else {
|
||||
this._curLine = b64.substring(lastLF + 1);
|
||||
b64 = b64.substring(0, lastLF + 1);
|
||||
}
|
||||
}
|
||||
if (b64) {
|
||||
this.outputBytes += b64.length;
|
||||
this.push(Buffer.from(b64, 'ascii'));
|
||||
}
|
||||
setImmediate(done);
|
||||
this._emit(encode(buf), false);
|
||||
done();
|
||||
}
|
||||
/** @internal */
|
||||
_flush(done) {
|
||||
if (this._remainingBytes && this._remainingBytes.length) {
|
||||
this._curLine += encode(this._remainingBytes);
|
||||
}
|
||||
if (this._curLine) {
|
||||
this._curLine = wrap(this._curLine, this.options.lineLength);
|
||||
this.outputBytes += this._curLine.length;
|
||||
this.push(Buffer.from(this._curLine, 'ascii'));
|
||||
this._curLine = '';
|
||||
}
|
||||
this._emit(this._remainingBytes ? encode(this._remainingBytes) : '', true);
|
||||
this._remainingBytes = false;
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
+13
-1
@@ -30,11 +30,23 @@ declare class DKIM {
|
||||
constructor(options: DKIMOptions);
|
||||
sign(input: Readable | Buffer | string, extraOptions?: DKIMOptions): DKIMSignedStream;
|
||||
}
|
||||
/** The signer options without any key material */
|
||||
export type DKIMOptionalOptions = Omit<DKIMOptions, 'keys' | 'domainName' | 'keySelector' | 'privateKey'>;
|
||||
/** The signer options for a single key given as domainName, keySelector and privateKey */
|
||||
export type DKIMSingleKeyOptions = Omit<DKIMOptions, 'keys'>;
|
||||
/** The signer options for one or more keys given through `keys` */
|
||||
export type DKIMMultipleKeysOptions = DKIMOptionalOptions & {
|
||||
keys: DKIMKey | DKIMKey[];
|
||||
};
|
||||
/**
|
||||
* 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'>;
|
||||
type OptionalOptions = DKIMOptionalOptions;
|
||||
type SingleKeyOptions = DKIMSingleKeyOptions;
|
||||
type MultipleKeysOptions = DKIMMultipleKeysOptions;
|
||||
}
|
||||
/** The same aliases as module level exports, for `import * as DKIM` and `import DKIM = require()` */
|
||||
export type { DKIMOptions as Options, DKIMOptionalOptions as OptionalOptions, DKIMSingleKeyOptions as SingleKeyOptions, DKIMMultipleKeysOptions as MultipleKeysOptions };
|
||||
export default DKIM;
|
||||
+69
-37
@@ -37,51 +37,67 @@ class DKIMSigner {
|
||||
this.output = output;
|
||||
this.output.usingCache = false;
|
||||
this.hasErrored = false;
|
||||
this.input.on('error', err => {
|
||||
this.input.on('error', err => this.fail(err));
|
||||
// A consumer that goes away before the signed message was read in full destroys the
|
||||
// output. Stop reading the input and drop the cache file instead of leaving them open
|
||||
this.output.once('close', () => {
|
||||
if (this.output.writableFinished) {
|
||||
return;
|
||||
}
|
||||
this.hasErrored = true;
|
||||
this.input.unpipe();
|
||||
this.input.destroy();
|
||||
this.cleanup();
|
||||
output.emit('error', err);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Ends the output with an error and releases the cache file
|
||||
*/
|
||||
fail(err) {
|
||||
this.hasErrored = true;
|
||||
this.cleanup();
|
||||
this.output.destroy(err);
|
||||
}
|
||||
cleanup() {
|
||||
if (!this.cache || !this.cachePath) {
|
||||
return;
|
||||
}
|
||||
const cache = this.cache;
|
||||
this.cache = false;
|
||||
cache.destroy();
|
||||
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('error', err => this.fail(err));
|
||||
this.cache.once('close', () => {
|
||||
this.cleanup();
|
||||
});
|
||||
this.cache.pipe(this.output);
|
||||
}
|
||||
sendNextChunk() {
|
||||
while (!this.hasErrored) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
sendSignedOutput() {
|
||||
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) {
|
||||
@@ -90,13 +106,20 @@ class DKIMSigner {
|
||||
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
|
||||
});
|
||||
let dkimField;
|
||||
try {
|
||||
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
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
this.fail(err);
|
||||
return;
|
||||
}
|
||||
if (dkimField) {
|
||||
this.output.write(Buffer.from(dkimField + '\r\n'));
|
||||
}
|
||||
@@ -110,10 +133,10 @@ class DKIMSigner {
|
||||
}
|
||||
createWriteCache() {
|
||||
this.output.usingCache = true;
|
||||
// pipe remainings to cache file
|
||||
this.cache = node_fs_1.default.createWriteStream(this.cachePath);
|
||||
// pipe remainings to cache file. It holds the message body and cacheDir is often a
|
||||
// shared directory such as /tmp, so nobody but the owner gets to read it
|
||||
this.cache = node_fs_1.default.createWriteStream(this.cachePath, { mode: 0o600 });
|
||||
this.cache.once('error', err => {
|
||||
this.cleanup();
|
||||
// drain input
|
||||
this.relaxedBody.unpipe(this.cache);
|
||||
this.relaxedBody.on('readable', () => {
|
||||
@@ -121,11 +144,12 @@ class DKIMSigner {
|
||||
// do nothing
|
||||
}
|
||||
});
|
||||
this.hasErrored = true;
|
||||
// emit error
|
||||
this.output.emit('error', err);
|
||||
this.fail(err);
|
||||
});
|
||||
this.cache.once('close', () => {
|
||||
if (this.hasErrored) {
|
||||
return;
|
||||
}
|
||||
this.sendSignedOutput();
|
||||
});
|
||||
this.relaxedBody.removeAllListeners('readable');
|
||||
@@ -196,7 +220,15 @@ class DKIM {
|
||||
}
|
||||
const signer = new DKIMSigner(options, this.keys, inputStream, output);
|
||||
setImmediate(() => {
|
||||
signer.signStream();
|
||||
try {
|
||||
signer.signStream();
|
||||
}
|
||||
catch (_E) {
|
||||
// the body hash is created here, an unknown hashAlgo throws inside this timer
|
||||
// where nothing else could catch it
|
||||
signer.fail(sign_js_1.default.unsupportedHashAlgoError(signer.hashAlgo));
|
||||
return;
|
||||
}
|
||||
if (writeValue) {
|
||||
setImmediate(() => {
|
||||
inputStream.end(writeValue);
|
||||
|
||||
+10
-1
@@ -13,7 +13,7 @@ export interface MessageParserHeaderLine {
|
||||
* 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 {
|
||||
declare class MessageParser extends Transform {
|
||||
lastBytes: Buffer;
|
||||
headersParsed: boolean;
|
||||
headerBytes: number;
|
||||
@@ -37,3 +37,12 @@ export default class MessageParser extends Transform {
|
||||
checkHeaders(data: Buffer): boolean;
|
||||
parseHeaders(): MessageParserHeaderLine[];
|
||||
}
|
||||
/**
|
||||
* Type aliases in the layout of @types/nodemailer, so `MessageParser.Header` style references keep working
|
||||
*/
|
||||
declare namespace MessageParser {
|
||||
type Header = MessageParserHeaderLine;
|
||||
}
|
||||
/** The same alias as a module level export, for `import * as MessageParser` and `import MessageParser = require()` */
|
||||
export type { MessageParserHeaderLine as Header };
|
||||
export default MessageParser;
|
||||
+42
-11
@@ -1,6 +1,33 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const node_stream_1 = require("node:stream");
|
||||
/**
|
||||
* Drops the SP and HTAB from both ends of a field name.
|
||||
*
|
||||
* An index scan rather than `/^[ \t]+|[ \t]+$/g`, which retries the trailing branch at
|
||||
* every offset of a blank run that is followed by other text: the run is consumed greedily,
|
||||
* `$` fails, and the engine gives the characters back one at a time before moving on to
|
||||
* start the same walk one offset further in. A field name carrying a 128 KiB run of spaces
|
||||
* between two atoms took about 6.7 seconds to trim, and the cost grows with its square.
|
||||
*
|
||||
* Not `.trim()`, which takes the other Unicode spaces along: an NBSP in front of a field
|
||||
* name would come off and select the name into the signed set, where the relaxed
|
||||
* canonicalization in sign.ts deliberately leaves everything but SP and HTAB alone.
|
||||
*
|
||||
* @param str Field name to trim
|
||||
* @return The field name without the surrounding SP and HTAB
|
||||
*/
|
||||
function _trimFieldName(str) {
|
||||
let start = 0;
|
||||
let end = str.length;
|
||||
while (start < end && (str.charCodeAt(start) === 0x20 || str.charCodeAt(start) === 0x09)) {
|
||||
start++;
|
||||
}
|
||||
while (end > start && (str.charCodeAt(end - 1) === 0x20 || str.charCodeAt(end - 1) === 0x09)) {
|
||||
end--;
|
||||
}
|
||||
return str.slice(start, end);
|
||||
}
|
||||
/**
|
||||
* MessageParser instance is a transform stream that separates message headers
|
||||
* from the rest of the body. Headers are emitted with the 'headers' event. Message
|
||||
@@ -80,7 +107,7 @@ class MessageParser extends node_stream_1.Transform {
|
||||
const chunk = data.slice(headerPos);
|
||||
this.bodySize += chunk.length;
|
||||
// this would be the first chunk of data sent downstream
|
||||
setImmediate(() => this.push(chunk));
|
||||
this.push(chunk);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -109,7 +136,7 @@ class MessageParser extends node_stream_1.Transform {
|
||||
this.bodySize += chunk.length;
|
||||
this.push(chunk);
|
||||
}
|
||||
setImmediate(callback);
|
||||
callback();
|
||||
}
|
||||
/** @internal */
|
||||
_flush(callback) {
|
||||
@@ -126,20 +153,24 @@ class MessageParser extends node_stream_1.Transform {
|
||||
// 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);
|
||||
const rawLines = (this.rawHeaders || Buffer.alloc(0)).toString('binary').split(/\r?\n/);
|
||||
// Unfold in a single forward pass and only ever test a freshly split line for the
|
||||
// continuation prefix. Testing an already merged line instead would rescan a string
|
||||
// that grows with every continuation line, so a header folded into many continuation
|
||||
// lines (a large recipient list, for example) would take quadratic time to unfold
|
||||
const lines = [];
|
||||
for (const rawLine of rawLines) {
|
||||
if (lines.length && /^[ \t]/.test(rawLine)) {
|
||||
lines[lines.length - 1] += '\n' + rawLine;
|
||||
}
|
||||
else {
|
||||
lines.push(rawLine);
|
||||
}
|
||||
}
|
||||
return lines
|
||||
.filter(line => /[^ \t\r]/.test(line))
|
||||
.map(line => ({
|
||||
key: line
|
||||
.substr(0, line.indexOf(':'))
|
||||
.replace(/^[ \t]+|[ \t]+$/g, '')
|
||||
.toLowerCase(),
|
||||
key: _trimFieldName(line.substr(0, line.indexOf(':'))).toLowerCase(),
|
||||
line
|
||||
}));
|
||||
}
|
||||
|
||||
+10
-1
@@ -17,7 +17,7 @@ export interface RelaxedBodyOptions {
|
||||
* 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 {
|
||||
declare class RelaxedBody extends Transform {
|
||||
bodyHash: crypto.Hash;
|
||||
/** Bytes of the original body seen so far */
|
||||
byteLength: number;
|
||||
@@ -25,3 +25,12 @@ export default class RelaxedBody extends Transform {
|
||||
constructor(options?: RelaxedBodyOptions);
|
||||
updateHash(chunk: Buffer, final?: boolean): void;
|
||||
}
|
||||
/**
|
||||
* Type aliases in the layout of @types/nodemailer, so `RelaxedBody.Options` style references keep working
|
||||
*/
|
||||
declare namespace RelaxedBody {
|
||||
type Options = RelaxedBodyOptions;
|
||||
}
|
||||
/** The same alias as a module level export, for `import * as RelaxedBody` and `import RelaxedBody = require()` */
|
||||
export type { RelaxedBodyOptions as Options };
|
||||
export default RelaxedBody;
|
||||
+3
@@ -1,4 +1,5 @@
|
||||
import crypto from 'node:crypto';
|
||||
import type { NodemailerError } from '../errors.js';
|
||||
import type { MessageParserHeaderLine } from './message-parser.js';
|
||||
/**
|
||||
* Private key accepted by crypto.Sign#sign: a PEM string, a Buffer, a KeyObject or an
|
||||
@@ -35,6 +36,7 @@ export interface DKIMRelaxedHeaders {
|
||||
* Returns DKIM signature header line
|
||||
*
|
||||
* @param headers Parsed headers object from MessageParser
|
||||
* @param hashAlgo Hash algorithm the body hash was calculated with, for example "sha256"
|
||||
* @param bodyHash Base64 encoded hash of the message
|
||||
* @param options DKIM options
|
||||
* @param options.domainName Domain name to be signed for
|
||||
@@ -45,5 +47,6 @@ export interface DKIMRelaxedHeaders {
|
||||
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;
|
||||
var unsupportedHashAlgoError: (hashAlgo: string) => NodemailerError;
|
||||
}
|
||||
export default sign;
|
||||
+18
-1
@@ -39,10 +39,20 @@ 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"));
|
||||
const errors = __importStar(require("../errors.js"));
|
||||
/**
|
||||
* Error for a hashAlgo value the crypto module does not know
|
||||
*/
|
||||
function unsupportedHashAlgoError(hashAlgo) {
|
||||
const err = new Error('Unsupported DKIM hash algorithm "' + hashAlgo + '"');
|
||||
err.code = errors.ECONFIG;
|
||||
return err;
|
||||
}
|
||||
/**
|
||||
* Returns DKIM signature header line
|
||||
*
|
||||
* @param headers Parsed headers object from MessageParser
|
||||
* @param hashAlgo Hash algorithm the body hash was calculated with, for example "sha256"
|
||||
* @param bodyHash Base64 encoded hash of the message
|
||||
* @param options DKIM options
|
||||
* @param options.domainName Domain name to be signed for
|
||||
@@ -63,7 +73,13 @@ function sign(headers, hashAlgo, bodyHash, options) {
|
||||
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());
|
||||
let signer;
|
||||
try {
|
||||
signer = node_crypto_1.default.createSign(('rsa-' + hashAlgo).toUpperCase());
|
||||
}
|
||||
catch (_E) {
|
||||
throw unsupportedHashAlgoError(hashAlgo);
|
||||
}
|
||||
// the header lines are 'binary' strings, so this reproduces the original header bytes
|
||||
signer.update(canonicalizedHeaderData.headers, 'latin1');
|
||||
let signature;
|
||||
@@ -76,6 +92,7 @@ function sign(headers, hashAlgo, bodyHash, options) {
|
||||
return dkimHeader + signature.replace(/(^.{73}|.{75}(?!\r?\n|\r))/g, '$&\r\n ').trim();
|
||||
}
|
||||
sign.relaxedHeaders = relaxedHeaders;
|
||||
sign.unsupportedHashAlgoError = unsupportedHashAlgoError;
|
||||
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
|
||||
|
||||
+22
-4
@@ -61,14 +61,28 @@ export declare const EPROXY = "EPROXY";
|
||||
export declare const EFILEACCESS = "EFILEACCESS";
|
||||
export declare const EURLACCESS = "EURLACCESS";
|
||||
export declare const EFETCH = "EFETCH";
|
||||
/**
|
||||
* Tells whether the error ended the connection rather than rejected what was sent over it
|
||||
*/
|
||||
export declare const isTransientError: (err: {
|
||||
code?: string | undefined;
|
||||
responseCode?: number | undefined;
|
||||
}) => boolean;
|
||||
/**
|
||||
* Code Node.js sets on an error when its permission model (`--permission`) denies an
|
||||
* operation. It is not replaced with the Nodemailer code of the failing step (`ESOCKET`,
|
||||
* `EDNS`, `EFETCH`, ...), so a missing grant such as `--allow-net` stays recognizable.
|
||||
*/
|
||||
export declare const ERR_ACCESS_DENIED = "ERR_ACCESS_DENIED";
|
||||
/**
|
||||
* An Error together with the properties Nodemailer attaches to the errors it
|
||||
* hands to callers. Every property is optional, the set that is present
|
||||
* depends on where the error originated.
|
||||
* depends on where the error originated. Socket level errors keep the errno
|
||||
* and syscall fields Node.js sets on them. `code` (see ERROR_CODES) is
|
||||
* inherited from ErrnoException, not redeclared, so it follows the
|
||||
* consumer's @types/node.
|
||||
*/
|
||||
export interface NodemailerError extends Error {
|
||||
/** Nodemailer error code, see ERROR_CODES */
|
||||
code?: string | undefined;
|
||||
export interface NodemailerError extends NodeJS.ErrnoException {
|
||||
/** SMTP command that was in flight when the server replied with an error */
|
||||
command?: string | undefined;
|
||||
/** Raw SMTP server response */
|
||||
@@ -83,6 +97,10 @@ export interface NodemailerError extends Error {
|
||||
rejected?: string[] | undefined;
|
||||
/** Per-recipient errors for the rejected addresses */
|
||||
rejectedErrors?: NodemailerError[] | undefined;
|
||||
/** The code the error had before Nodemailer set its own, such as ECONNREFUSED for an ESOCKET error */
|
||||
originalCode?: string | undefined;
|
||||
/** Which wait an ETIMEDOUT error ended: connecting, the greeting, a TLS upgrade or an idle socket */
|
||||
timeoutType?: 'CONNECT_TIMEOUT' | 'GREETING_TIMEOUT' | 'UPGRADE_TIMEOUT' | 'SOCKET_TIMEOUT' | undefined;
|
||||
}
|
||||
/**
|
||||
* Node style callback: called with an error, or with null and the result
|
||||
|
||||
+12
-1
@@ -10,7 +10,7 @@
|
||||
* err.code = errors.ECONNECTION;
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.EFETCH = exports.EURLACCESS = exports.EFILEACCESS = exports.EPROXY = exports.ECONFIG = exports.ESES = exports.ESENDMAIL = exports.EMAXRECIPIENTS = exports.EMAXLIMIT = exports.EOAUTH2 = exports.ENOAUTH = exports.EAUTH = exports.ESTREAM = exports.EMESSAGE = exports.EENVELOPE = exports.EPROTOCOL = exports.EREQUIRETLS = exports.ETLS = exports.EDNS = exports.ESOCKET = exports.ETIMEDOUT = exports.ECONNECTION = exports.ERROR_CODES = void 0;
|
||||
exports.ERR_ACCESS_DENIED = exports.isTransientError = exports.EFETCH = exports.EURLACCESS = exports.EFILEACCESS = exports.EPROXY = exports.ECONFIG = exports.ESES = exports.ESENDMAIL = exports.EMAXRECIPIENTS = exports.EMAXLIMIT = exports.EOAUTH2 = exports.ENOAUTH = exports.EAUTH = exports.ESTREAM = exports.EMESSAGE = exports.EENVELOPE = exports.EPROTOCOL = exports.EREQUIRETLS = exports.ETLS = exports.EDNS = exports.ESOCKET = exports.ETIMEDOUT = exports.ECONNECTION = exports.ERROR_CODES = void 0;
|
||||
/**
|
||||
* Error code descriptions for documentation and debugging
|
||||
*/
|
||||
@@ -68,3 +68,14 @@ exports.EPROXY = 'EPROXY';
|
||||
exports.EFILEACCESS = 'EFILEACCESS';
|
||||
exports.EURLACCESS = 'EURLACCESS';
|
||||
exports.EFETCH = 'EFETCH';
|
||||
/**
|
||||
* Tells whether the error ended the connection rather than rejected what was sent over it
|
||||
*/
|
||||
const isTransientError = (err) => err.responseCode === 421 || err.code === exports.ECONNECTION || err.code === exports.ESOCKET || err.code === exports.ETIMEDOUT;
|
||||
exports.isTransientError = isTransientError;
|
||||
/**
|
||||
* Code Node.js sets on an error when its permission model (`--permission`) denies an
|
||||
* operation. It is not replaced with the Nodemailer code of the failing step (`ESOCKET`,
|
||||
* `EDNS`, `EFETCH`, ...), so a missing grant such as `--allow-net` stays recognizable.
|
||||
*/
|
||||
exports.ERR_ACCESS_DENIED = 'ERR_ACCESS_DENIED';
|
||||
+12
-1
@@ -17,13 +17,14 @@ export interface Cookie {
|
||||
secure?: boolean | undefined;
|
||||
httponly?: boolean | undefined;
|
||||
}
|
||||
type CookieEntry = Cookie;
|
||||
/**
|
||||
* Creates a biskviit cookie jar for managing cookie values in memory
|
||||
*
|
||||
* @constructor
|
||||
* @param [options] Optional options object
|
||||
*/
|
||||
export default class Cookies {
|
||||
declare class Cookies {
|
||||
options: CookiesOptions;
|
||||
cookies: Cookie[];
|
||||
constructor(options?: CookiesOptions);
|
||||
@@ -94,3 +95,13 @@ export default class Cookies {
|
||||
*/
|
||||
getPath(pathname?: string | null): string;
|
||||
}
|
||||
/**
|
||||
* Type aliases in the layout of @types/nodemailer, so `Cookies.Cookie` style references keep working
|
||||
*/
|
||||
declare namespace Cookies {
|
||||
type Options = CookiesOptions;
|
||||
type Cookie = CookieEntry;
|
||||
}
|
||||
/** The same alias as a module level export, for `import * as Cookies` and `import Cookies = require()` */
|
||||
export type { CookiesOptions as Options };
|
||||
export default Cookies;
|
||||
+18
-11
@@ -130,19 +130,31 @@ class Cookies {
|
||||
*/
|
||||
parse(cookieStr) {
|
||||
const cookie = {};
|
||||
let hasNameValue = false;
|
||||
(cookieStr || '')
|
||||
.toString()
|
||||
.split(';')
|
||||
.forEach(cookiePart => {
|
||||
const valueParts = cookiePart.split('=');
|
||||
const key = valueParts.shift().trim().toLowerCase();
|
||||
let value = valueParts.join('=').trim();
|
||||
let domain;
|
||||
if (!key) {
|
||||
if (!cookiePart.trim()) {
|
||||
// skip empty parts
|
||||
return;
|
||||
}
|
||||
switch (key) {
|
||||
const valueParts = cookiePart.split('=');
|
||||
const name = valueParts.shift().trim();
|
||||
let value = valueParts.join('=').trim();
|
||||
let domain;
|
||||
// the first part is always the name-value pair, so a cookie named
|
||||
// like an attribute is not mistaken for one (RFC 6265 section 5.2)
|
||||
if (!hasNameValue) {
|
||||
hasNameValue = true;
|
||||
if (name) {
|
||||
// cookie names are case-sensitive, only attribute names are not
|
||||
cookie.name = name;
|
||||
cookie.value = value;
|
||||
}
|
||||
return;
|
||||
}
|
||||
switch (name.toLowerCase()) {
|
||||
case 'expires': {
|
||||
const expires = new Date(value);
|
||||
// ignore date if can not parse it
|
||||
@@ -170,11 +182,6 @@ class Cookies {
|
||||
case 'httponly':
|
||||
cookie.httponly = true;
|
||||
break;
|
||||
default:
|
||||
if (!cookie.name) {
|
||||
cookie.name = key;
|
||||
cookie.value = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
return cookie;
|
||||
|
||||
+20
-1
@@ -25,8 +25,12 @@ export interface FetchOptions {
|
||||
tls?: {
|
||||
[key: string]: any;
|
||||
} | undefined;
|
||||
/** Request timeout in milliseconds */
|
||||
/** Socket inactivity timeout in milliseconds, defaults to 60000, 0 disables it */
|
||||
timeout?: number | undefined;
|
||||
/** Time limit for the whole request in milliseconds, redirects included, defaults to 120000, 0 disables it */
|
||||
maxTime?: number | undefined;
|
||||
/** Maximum size of the (decoded) response body in bytes, defaults to 64 MB, Infinity disables the limit */
|
||||
maxBytes?: number | undefined;
|
||||
/** Maximum number of redirects to follow (default 5) */
|
||||
maxRedirects?: number | undefined;
|
||||
/** Resolve responses with a status code of 300 or above instead of emitting an error */
|
||||
@@ -35,6 +39,8 @@ export interface FetchOptions {
|
||||
redirects?: number | undefined;
|
||||
/** Response stream shared across redirects, set by nmfetch itself */
|
||||
fetchRes?: FetchResponse | undefined;
|
||||
/** Timestamp the whole request has to be done by, shared across redirects, set by nmfetch itself */
|
||||
deadline?: number | undefined;
|
||||
}
|
||||
/**
|
||||
* The stream nmfetch returns. The response body is piped into it, the status code and
|
||||
@@ -47,5 +53,18 @@ export interface FetchResponse extends PassThrough {
|
||||
declare function nmfetch(url: string, options?: FetchOptions): FetchResponse;
|
||||
declare namespace nmfetch {
|
||||
var Cookies: typeof import("./cookies.js").default;
|
||||
var DEFAULT_TIMEOUT: number;
|
||||
var DEFAULT_MAX_TIME: number;
|
||||
}
|
||||
type CookiesJar = Cookies;
|
||||
/**
|
||||
* Type aliases in the layout of @types/nodemailer, so `fetch.Options` style references keep working
|
||||
*/
|
||||
declare namespace nmfetch {
|
||||
type Options = FetchOptions;
|
||||
type WritableResponse = FetchResponse;
|
||||
type Cookies = CookiesJar;
|
||||
}
|
||||
/** The same aliases as module level exports, for `import * as fetch` and `import fetch = require()` */
|
||||
export type { FetchOptions as Options, FetchResponse as WritableResponse, Cookies };
|
||||
export default nmfetch;
|
||||
+107
-76
@@ -47,6 +47,14 @@ const node_net_1 = __importDefault(require("node:net"));
|
||||
const errors = __importStar(require("../errors.js"));
|
||||
const objects_js_1 = require("../shared/objects.js");
|
||||
const MAX_REDIRECTS = 5;
|
||||
// a stalled server would otherwise hold the request, and whatever waits on it, open forever
|
||||
const DEFAULT_TIMEOUT = 60 * 1000;
|
||||
// the inactivity timeout above is reset by every byte, so a server dripping one within each
|
||||
// window could still hold the request, and the message being built from it, open for as
|
||||
// long as it liked. The whole request, redirects included, has to be done within this
|
||||
const DEFAULT_MAX_TIME = 2 * 60 * 1000;
|
||||
// the body is usually buffered in memory by the caller, so an unbounded download is refused
|
||||
const DEFAULT_MAX_BYTES = 64 * 1024 * 1024;
|
||||
// Only genuine TLS settings are taken from options.tls. That object reaches us straight
|
||||
// from a user supplied attachment (content.tls), so keys like host, port, path, socketPath
|
||||
// or lookup would otherwise repoint the request at a destination that never went through
|
||||
@@ -77,6 +85,17 @@ const TLS_OPTION_KEYS = [
|
||||
'sessionIdContext',
|
||||
'sigalgs'
|
||||
];
|
||||
/**
|
||||
* Marks an error of the request as a fetch error, unless it is a permission model
|
||||
* denial, which keeps its own code (see ERR_ACCESS_DENIED)
|
||||
*
|
||||
* @param err Error to mark
|
||||
*/
|
||||
function setFetchCode(err) {
|
||||
if (err.code !== errors.ERR_ACCESS_DENIED) {
|
||||
err.code = errors.EFETCH;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Resolves a URL only if it is one this module is willing to request.
|
||||
*
|
||||
@@ -101,12 +120,22 @@ function parseFetchUrl(url) {
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
// the port a request to the URL goes to, explicit or the default of its scheme
|
||||
function fetchPort(parsed) {
|
||||
return parsed.port ? Number(parsed.port) : parsed.protocol === 'https:' ? 443 : 80;
|
||||
}
|
||||
function nmfetch(url, options) {
|
||||
options = options || {};
|
||||
options.fetchRes = options.fetchRes || new node_stream_1.PassThrough();
|
||||
options.cookies = options.cookies || new cookies_js_1.default();
|
||||
options.redirects = options.redirects || 0;
|
||||
options.maxRedirects = isNaN(options.maxRedirects) ? MAX_REDIRECTS : options.maxRedirects;
|
||||
if (!options.deadline) {
|
||||
options.maxTime = typeof options.maxTime === 'number' && options.maxTime >= 0 ? options.maxTime : DEFAULT_MAX_TIME;
|
||||
// the one deadline is shared with the requests the redirects lead to, every hop
|
||||
// gets the time that is left rather than starting over
|
||||
options.deadline = options.maxTime ? Date.now() + options.maxTime : 0;
|
||||
}
|
||||
const fetchRes = options.fetchRes;
|
||||
const parsed = parseFetchUrl(url);
|
||||
if (!parsed) {
|
||||
@@ -173,7 +202,7 @@ function nmfetch(url, options) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
err.code = errors.EFETCH;
|
||||
setFetchCode(err);
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
});
|
||||
@@ -193,14 +222,14 @@ function nmfetch(url, options) {
|
||||
.join('&'));
|
||||
}
|
||||
catch (E) {
|
||||
if (finished) {
|
||||
return undefined;
|
||||
}
|
||||
// the caller attaches its error listener once nmfetch has returned, so
|
||||
// the error is emitted on the next tick and the stream is handed back
|
||||
// the way every other failure is reported
|
||||
finished = true;
|
||||
E.code = errors.EFETCH;
|
||||
E.sourceUrl = url;
|
||||
fetchRes.emit('error', E);
|
||||
return undefined;
|
||||
setImmediate(() => fetchRes.emit('error', E));
|
||||
return fetchRes;
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -217,7 +246,7 @@ function nmfetch(url, options) {
|
||||
method,
|
||||
host: parsed.hostname,
|
||||
path: parsed.path,
|
||||
port: parsed.port ? parsed.port : parsed.protocol === 'https:' ? 443 : 80,
|
||||
port: fetchPort(parsed),
|
||||
headers,
|
||||
// Validate TLS certificates by default. Callers that genuinely need to
|
||||
// reach a self-signed/internal host opt out explicitly with
|
||||
@@ -246,33 +275,52 @@ function nmfetch(url, options) {
|
||||
catch (E) {
|
||||
finished = true;
|
||||
setImmediate(() => {
|
||||
E.code = errors.EFETCH;
|
||||
setFetchCode(E);
|
||||
E.sourceUrl = url;
|
||||
fetchRes.emit('error', E);
|
||||
});
|
||||
return fetchRes;
|
||||
}
|
||||
if (options.timeout) {
|
||||
req.setTimeout(options.timeout, () => {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
req.abort();
|
||||
const err = new Error('Request Timeout');
|
||||
err.code = errors.EFETCH;
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
});
|
||||
}
|
||||
req.on('error', (err) => {
|
||||
// the whole request has to be done by options.deadline, see maxTime. The timer is
|
||||
// released on every way out of the request, so it never outlives it
|
||||
let deadlineTimer;
|
||||
const clearDeadline = () => {
|
||||
if (deadlineTimer) {
|
||||
clearTimeout(deadlineTimer);
|
||||
deadlineTimer = undefined;
|
||||
}
|
||||
};
|
||||
// reports the first failure of this request on fetchRes and releases the request
|
||||
const fail = (err, sourceUrl = url) => {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
err.code = errors.EFETCH;
|
||||
err.sourceUrl = url;
|
||||
clearDeadline();
|
||||
setFetchCode(err);
|
||||
err.sourceUrl = sourceUrl;
|
||||
fetchRes.emit('error', err);
|
||||
req.abort();
|
||||
};
|
||||
const timeout = typeof options.timeout === 'number' && options.timeout >= 0 ? options.timeout : DEFAULT_TIMEOUT;
|
||||
if (timeout) {
|
||||
req.setTimeout(timeout, () => fail(new Error('Request Timeout')));
|
||||
}
|
||||
if (options.deadline) {
|
||||
deadlineTimer = setTimeout(() => fail(new Error('Request exceeded the allowed ' + options.maxTime + ' ms')), Math.max(options.deadline - Date.now(), 1));
|
||||
// a pending deadline is no reason to keep the process alive
|
||||
deadlineTimer.unref();
|
||||
}
|
||||
req.on('error', (err) => fail(err));
|
||||
// a consumer that destroys the response stream before it ended does not want the rest of
|
||||
// the body, release the request now instead of when the timeout fires
|
||||
fetchRes.once('close', () => {
|
||||
clearDeadline();
|
||||
if (finished || fetchRes.readableEnded) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
req.destroy();
|
||||
});
|
||||
req.on('response', res => {
|
||||
let inflate;
|
||||
@@ -294,13 +342,7 @@ function nmfetch(url, options) {
|
||||
// redirect
|
||||
options.redirects++;
|
||||
if (options.redirects > options.maxRedirects) {
|
||||
finished = true;
|
||||
const err = new Error('Maximum redirect count exceeded');
|
||||
err.code = errors.EFETCH;
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
req.abort();
|
||||
return;
|
||||
return fail(new Error('Maximum redirect count exceeded'));
|
||||
}
|
||||
// redirect does not include POST body
|
||||
options.method = 'GET';
|
||||
@@ -320,64 +362,55 @@ function nmfetch(url, options) {
|
||||
// call: that call gets its own `finished` flag and no handle on this
|
||||
// request, so this one would stay open and could emit a second error on
|
||||
// the shared fetchRes once it times out. Callers listen with req.once().
|
||||
finished = true;
|
||||
const err = new Error('Unsupported protocol for URL ' + redirectUrl);
|
||||
err.code = errors.EFETCH;
|
||||
err.sourceUrl = redirectUrl;
|
||||
fetchRes.emit('error', err);
|
||||
req.abort();
|
||||
return;
|
||||
return fail(new Error('Unsupported protocol for URL ' + redirectUrl), redirectUrl);
|
||||
}
|
||||
// Do not forward credentials when the redirect leaves the original
|
||||
// security context: a different host, or a downgrade from https to
|
||||
// http (which would otherwise put them on the wire in cleartext).
|
||||
// Strip sensitive request headers so an attacker who controls the
|
||||
// redirect target cannot harvest them.
|
||||
const crossHost = redirectParsed.hostname !== parsed.hostname;
|
||||
const downgrade = parsed.protocol === 'https:' && redirectParsed.protocol === 'http:';
|
||||
if (options.headers && (crossHost || downgrade)) {
|
||||
// Do not forward credentials when the redirect leaves the origin: a different
|
||||
// scheme, host or port is a different security context (an https to http
|
||||
// downgrade would also put them on the wire in cleartext). Strip sensitive
|
||||
// request headers so an attacker who controls the redirect target cannot
|
||||
// harvest them.
|
||||
const crossOrigin = redirectParsed.protocol !== parsed.protocol ||
|
||||
redirectParsed.hostname !== parsed.hostname ||
|
||||
fetchPort(redirectParsed) !== fetchPort(parsed);
|
||||
if (options.headers && crossOrigin) {
|
||||
const sensitive = ['authorization', 'cookie', 'proxy-authorization'];
|
||||
Object.keys(options.headers).forEach(key => {
|
||||
if (sensitive.includes(key.toLowerCase())) {
|
||||
// normalized the way the request headers are built above, a padded name is sent too
|
||||
if (sensitive.includes(key.toLowerCase().trim())) {
|
||||
delete options.headers[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
// this request is done with, release it so its socket and timeout do not
|
||||
// outlive it and report a late error on the shared fetchRes
|
||||
finished = true;
|
||||
clearDeadline();
|
||||
res.resume();
|
||||
req.abort();
|
||||
return nmfetch(redirectUrl, options);
|
||||
}
|
||||
fetchRes.statusCode = res.statusCode;
|
||||
fetchRes.headers = res.headers;
|
||||
if (res.statusCode >= 300 && !options.allowErrorResponse) {
|
||||
finished = true;
|
||||
const err = new Error('Invalid status code ' + res.statusCode);
|
||||
err.code = errors.EFETCH;
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
req.abort();
|
||||
return;
|
||||
return fail(new Error('Invalid status code ' + res.statusCode));
|
||||
}
|
||||
res.on('error', (err) => {
|
||||
if (finished) {
|
||||
res.on('error', (err) => fail(err));
|
||||
// the response is in, whatever is left of it is in memory and no longer on the network
|
||||
res.once('end', () => clearDeadline());
|
||||
const maxBytes = typeof options.maxBytes === 'number' && options.maxBytes > 0 ? options.maxBytes : DEFAULT_MAX_BYTES;
|
||||
const source = inflate || res;
|
||||
let received = 0;
|
||||
source.on('data', (chunk) => {
|
||||
received += chunk.length;
|
||||
if (received <= maxBytes || finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
err.code = errors.EFETCH;
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
req.abort();
|
||||
source.unpipe(fetchRes);
|
||||
fail(new Error('Response size exceeds the allowed ' + maxBytes + ' bytes'));
|
||||
});
|
||||
if (inflate) {
|
||||
res.pipe(inflate).pipe(fetchRes);
|
||||
inflate.on('error', (err) => {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
err.code = errors.EFETCH;
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
req.abort();
|
||||
});
|
||||
inflate.on('error', (err) => fail(err));
|
||||
}
|
||||
else {
|
||||
res.pipe(fetchRes);
|
||||
@@ -392,11 +425,7 @@ function nmfetch(url, options) {
|
||||
req.write(body);
|
||||
}
|
||||
catch (err) {
|
||||
finished = true;
|
||||
err.code = errors.EFETCH;
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
return;
|
||||
return fail(err);
|
||||
}
|
||||
}
|
||||
req.end();
|
||||
@@ -404,6 +433,8 @@ function nmfetch(url, options) {
|
||||
return fetchRes;
|
||||
}
|
||||
nmfetch.Cookies = cookies_js_1.default;
|
||||
nmfetch.DEFAULT_TIMEOUT = DEFAULT_TIMEOUT;
|
||||
nmfetch.DEFAULT_MAX_TIME = DEFAULT_MAX_TIME;
|
||||
exports.default = nmfetch;
|
||||
module.exports = exports.default;
|
||||
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
|
||||
+14
-4
@@ -1,6 +1,6 @@
|
||||
import type { Logger } from '../shared/index.js';
|
||||
import type { MimeNodeEnvelope } from '../mime-node/index.js';
|
||||
import type { default as MailMessage, MailMessageData } from '../mailer/mail-message.js';
|
||||
import type MailMessage from '../mailer/mail-message.js';
|
||||
import type { default as Mail, SentMessageInfo, SendMailOptions, TransportOptions } from '../mailer/index.js';
|
||||
/**
|
||||
* Options for the JSON transport
|
||||
@@ -12,16 +12,23 @@ export interface JSONTransportOptions extends TransportOptions {
|
||||
skipEncoding?: boolean | undefined;
|
||||
}
|
||||
/**
|
||||
* The value the JSON transport hands to the send callback
|
||||
* The value the JSON transport hands to the send callback. M is the type of the message
|
||||
* field: a JSON string by default, the message object itself when skipEncoding is set
|
||||
*/
|
||||
export interface JSONSentMessageInfo extends SentMessageInfo {
|
||||
export interface JSONSentMessageInfo<M = string> extends SentMessageInfo {
|
||||
/** The envelope the message was generated with */
|
||||
envelope: MimeNodeEnvelope;
|
||||
/** Message-ID value of the message */
|
||||
messageId: string;
|
||||
/** The normalized message as a JSON string, or as the object itself when skipEncoding is set */
|
||||
message: string | MailMessageData;
|
||||
message: M;
|
||||
}
|
||||
/**
|
||||
* The value the JSON transport hands to the send callback when skipEncoding is set: the
|
||||
* message field holds the message object. Typed loosely so that a transporter created with
|
||||
* skipEncoding still fits a variable declared with the plain result type
|
||||
*/
|
||||
export type JSONSentMessageObjectInfo = JSONSentMessageInfo<any>;
|
||||
/**
|
||||
* Generates a Transport object to generate JSON output
|
||||
*
|
||||
@@ -50,5 +57,8 @@ declare namespace JSONTransport {
|
||||
type Options = JSONTransportOptions;
|
||||
type MailOptions = SendMailOptions;
|
||||
type SentMessageInfo = JSONSentMessageInfo;
|
||||
type SentMessageObjectInfo = JSONSentMessageObjectInfo;
|
||||
}
|
||||
/** The same aliases as module level exports, for `import * as JSONTransport` and `import JSONTransport = require()` */
|
||||
export type { JSONTransportOptions as Options, SendMailOptions as MailOptions, JSONSentMessageInfo as SentMessageInfo, JSONSentMessageObjectInfo as SentMessageObjectInfo };
|
||||
export default JSONTransport;
|
||||
+2
@@ -83,6 +83,8 @@ class JSONTransport {
|
||||
}
|
||||
delete data.envelope;
|
||||
delete data.normalizedHeaders;
|
||||
// the message field is the object itself with skipEncoding, the
|
||||
// createTransport overload for that option types the result accordingly
|
||||
return done(null, {
|
||||
envelope,
|
||||
messageId,
|
||||
|
||||
+8
-3
@@ -72,6 +72,11 @@ export type MailComposerListHeaderEntry = string | {
|
||||
export interface MailComposerListHeaders {
|
||||
[key: string]: MailComposerListHeaderEntry | (MailComposerListHeaderEntry | MailComposerListHeaderEntry[])[];
|
||||
}
|
||||
/**
|
||||
* Encoding for the non-ascii header values: quoted-printable ('Q', the default) or base64
|
||||
* ('B'). Only the first letter is read, so the short forms work as well
|
||||
*/
|
||||
export type MailComposerTextEncoding = 'quoted-printable' | 'base64' | 'Q' | 'B';
|
||||
/**
|
||||
* Mail options, the message data MailComposer builds the MIME tree from. The address
|
||||
* fields, subject, messageId, date, inReplyTo and references become headers of the root
|
||||
@@ -114,7 +119,7 @@ export interface MailComposerOptions {
|
||||
/** Content-Transfer-Encoding to force for the text/* nodes that do not set their own */
|
||||
encoding?: string | undefined;
|
||||
/** Header string encoding, 'Q' (the default) or 'B', 'quoted-printable' and 'base64' are accepted as well */
|
||||
textEncoding?: string | undefined;
|
||||
textEncoding?: MailComposerTextEncoding | undefined;
|
||||
/** Pregenerated rfc822 message, used as is instead of building one */
|
||||
raw?: MimeNodeContent | undefined;
|
||||
/** Reject content that points to a URL */
|
||||
@@ -129,8 +134,6 @@ export interface MailComposerOptions {
|
||||
baseBoundary?: string | undefined;
|
||||
/** 'win' for CRLF and 'linux' for LF line breaks in the generated message, kept as is when not set */
|
||||
newline?: string | undefined;
|
||||
/** Keep the Bcc header in the generated message, listed for completeness, the transports set it on the message directly */
|
||||
keepBcc?: boolean | undefined;
|
||||
/** Method to normalize header keys for custom caseing */
|
||||
normalizeHeaderKey?: MimeNodeOptions['normalizeHeaderKey'] | undefined;
|
||||
/** 'high', 'normal' or 'low', sets the priority headers, read by the mailer */
|
||||
@@ -166,4 +169,6 @@ declare class MailComposer {
|
||||
declare namespace MailComposer {
|
||||
type Options = MailComposerOptions;
|
||||
}
|
||||
/** The same alias as a module level export, for `import * as MailComposer` and `import MailComposer = require()` */
|
||||
export type { MailComposerOptions as Options };
|
||||
export default MailComposer;
|
||||
+3
-1
@@ -151,8 +151,10 @@ class MailComposer {
|
||||
data.filename = attachment.filename;
|
||||
}
|
||||
else if (!isMessageNode && attachment.filename !== false) {
|
||||
// a backslash separates as well, so a Windows path does not put the sender's directories in the headers
|
||||
data.filename =
|
||||
(attachment.path || attachment.href || '').split('/').pop().split('?').shift() || 'attachment-' + (i + 1);
|
||||
(attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() ||
|
||||
'attachment-' + (i + 1);
|
||||
if (data.filename.indexOf('.') < 0) {
|
||||
data.filename += '.' + mimeFuncs.detectExtension(data.contentType);
|
||||
}
|
||||
|
||||
+43
-25
@@ -4,8 +4,8 @@ import DKIM, { type DKIMOptions } from '../dkim/index.js';
|
||||
import MailMessage, { type MailDefaults, type SendMailOptions } from './mail-message.js';
|
||||
import net from 'node:net';
|
||||
import type { ConnectionOptions } from 'node:tls';
|
||||
import type { MailComposerAlternative, MailComposerAttachment, MailComposerIcalEvent, MailComposerListHeaderEntry, MailComposerListHeaders } from '../mail-composer/index.js';
|
||||
import type { NodemailerError, ResultCallback } from '../errors.js';
|
||||
import type { MailComposerAlternative, MailComposerAttachment, MailComposerIcalEvent, MailComposerListHeaderEntry, MailComposerListHeaders, MailComposerTextEncoding } from '../mail-composer/index.js';
|
||||
import type { Callback, NodemailerError, ResultCallback } from '../errors.js';
|
||||
import type { ParsedUrl } from '../shared/url.js';
|
||||
import type { MimeNodeAddress, MimeNodeEnvelope, MimeNodeEnvelopeInput, MimeNodeHeaders, MimeNodeOptions } from '../mime-node/index.js';
|
||||
import type { XOAuth2ProvisionCallback } from '../xoauth2/index.js';
|
||||
@@ -15,9 +15,10 @@ export type { default as MailMessage } from './mail-message.js';
|
||||
* The base shape of the object a transport hands back for a sent message. Every bundled
|
||||
* transport sets the envelope and the Message-ID, the rest depends on the transport.
|
||||
*
|
||||
* The index signature keeps a transport specific field readable through this type, and it
|
||||
* is also what a result type has to inherit to stay assignable to it, so the result type
|
||||
* of a transport outside this package has to extend this interface rather than restate it
|
||||
* The index signature keeps a transport specific field readable through this type, the way
|
||||
* the `any` typed result of @types/nodemailer was, and it is also what a result type has
|
||||
* to inherit to stay assignable to it, so the result type of a transport outside this
|
||||
* package has to extend this interface rather than restate it
|
||||
*/
|
||||
export interface SentMessageInfo {
|
||||
/** The envelope the message was sent with */
|
||||
@@ -33,9 +34,9 @@ export interface SentMessageInfo {
|
||||
/** Last response from the server */
|
||||
response?: string | undefined;
|
||||
/** The generated message, for the transports that hand it back instead of sending it */
|
||||
message?: unknown;
|
||||
message?: any;
|
||||
/** Transport specific fields */
|
||||
[key: string]: unknown;
|
||||
[key: string]: any;
|
||||
}
|
||||
/**
|
||||
* Callback for sendMail, receives the transport result once the transport has taken the
|
||||
@@ -43,9 +44,11 @@ export interface SentMessageInfo {
|
||||
*/
|
||||
export type SendMailCallback<T = SentMessageInfo> = (err: NodemailerError | null, info: T) => void;
|
||||
/**
|
||||
* Callback for verify(), success is true once the transport accepted the configuration
|
||||
* Callback for verify(), success is true once the transport accepted the configuration.
|
||||
* Declared with a required success value, the way @types/nodemailer declared it, the
|
||||
* error path hands over the error alone
|
||||
*/
|
||||
export type VerifyCallback = (err: NodemailerError | null, success?: true) => void;
|
||||
export type VerifyCallback = Callback<true>;
|
||||
/**
|
||||
* Callback a plugin calls once it is done, an error aborts the send
|
||||
*/
|
||||
@@ -60,21 +63,23 @@ export type PluginFunction<T = SentMessageInfo> = (mail: MailMessage<T>, callbac
|
||||
*/
|
||||
export interface GetSocketOptions {
|
||||
host?: string | undefined;
|
||||
port?: number | string | undefined;
|
||||
port?: number | undefined;
|
||||
[key: string]: any;
|
||||
}
|
||||
/**
|
||||
* The result of a getSocket handler, the socket to use for the connection
|
||||
* The result of a getSocket handler, the socket to use for the connection. The object is
|
||||
* merged into the connection options, so it may carry any of those as well
|
||||
*/
|
||||
export interface SocketOptions {
|
||||
/** An established socket, the proxied connection */
|
||||
connection?: net.Socket | undefined;
|
||||
[key: string]: any;
|
||||
}
|
||||
/**
|
||||
* Receives the socket options from a getSocket handler, or the error that prevented the
|
||||
* connection
|
||||
* Receives the socket options from a getSocket handler, false when a new socket should be
|
||||
* opened, or the error that prevented the connection
|
||||
*/
|
||||
export type GetSocketCallback = (err: NodemailerError | null, socketOptions?: SocketOptions) => void;
|
||||
export type GetSocketCallback = (err: Error | null, socketOptions?: SocketOptions | false) => void;
|
||||
/**
|
||||
* A socket handler. Mail sets one on the transport as getSocket when a proxy is configured,
|
||||
* the SMTP transports call it to get a proxied socket instead of connecting directly
|
||||
@@ -101,9 +106,12 @@ export interface MailMeta {
|
||||
/**
|
||||
* A transport as consumed by Mail: any object with a name, a version and a send method
|
||||
* works, the rest is optional. Mail forwards its close, isIdle and verify calls to the
|
||||
* methods of the same name as they are, so their arguments are up to the transport
|
||||
* methods of the same name as they are, so their arguments are up to the transport.
|
||||
*
|
||||
* D is the options type of the transport, the second type parameter @types/nodemailer
|
||||
* declared on Transport, Transporter and Mail
|
||||
*/
|
||||
export interface Transport<T = SentMessageInfo> {
|
||||
export interface Transport<T = SentMessageInfo, D extends TransportOptions = TransportOptions> {
|
||||
/** Transport name, used for logging */
|
||||
name: string;
|
||||
/** Transport version, used for logging */
|
||||
@@ -119,7 +127,7 @@ export interface Transport<T = SentMessageInfo> {
|
||||
/** Registers an event listener, the transport may emit 'log', 'error', 'idle' and 'clear' */
|
||||
on?(event: string | symbol, listener: (...args: any[]) => void): this;
|
||||
/** The Mail object the transport belongs to, set by Mail */
|
||||
mailer?: Mail<T> | undefined;
|
||||
mailer?: Mail<T, D> | undefined;
|
||||
/** Socket handler for a proxied connection, set by Mail when a proxy is configured */
|
||||
getSocket?: GetSocketHandler | undefined;
|
||||
}
|
||||
@@ -150,22 +158,23 @@ export interface TransportOptions {
|
||||
attachDataUrls?: boolean | undefined;
|
||||
}
|
||||
/**
|
||||
* The transporter object createTransport returns, a Mail instance wrapping a transport
|
||||
* The transporter object createTransport returns, a Mail instance wrapping a transport. D
|
||||
* is the options type of the transport, it types the options field of the transporter
|
||||
*/
|
||||
export type Transporter<T = SentMessageInfo> = Mail<T>;
|
||||
export type Transporter<T = SentMessageInfo, D extends TransportOptions = TransportOptions> = Mail<T, D>;
|
||||
/**
|
||||
* Creates an object for exposing the Mail API
|
||||
*
|
||||
* @constructor
|
||||
* @param transporter Transport object instance to pass the mails to
|
||||
*/
|
||||
declare class Mail<out T = SentMessageInfo> extends EventEmitter {
|
||||
options: TransportOptions;
|
||||
declare class Mail<out T = SentMessageInfo, out D extends TransportOptions = TransportOptions> extends EventEmitter {
|
||||
options: D;
|
||||
/** Message defaults given to createTransport, kept public because the DefinitelyTyped typings declared it */
|
||||
_defaults: MailDefaults;
|
||||
meta: Map<string, any>;
|
||||
dkim: DKIM | false;
|
||||
transporter: Transport<T>;
|
||||
transporter: Transport<T, D>;
|
||||
logger: shared.Logger;
|
||||
/** Closes the transport, the pooled SMTP transport closes its connections */
|
||||
close: () => void;
|
||||
@@ -178,15 +187,21 @@ declare class Mail<out T = SentMessageInfo> extends EventEmitter {
|
||||
};
|
||||
/** Socket handler for a proxied connection, set by setupProxy and handed to the transport on the next send */
|
||||
getSocket?: GetSocketHandler | false | undefined;
|
||||
constructor(transporter: Transport<T>, options?: TransportOptions, defaults?: MailDefaults);
|
||||
constructor(transporter: Transport<T, D>, options?: D, defaults?: MailDefaults);
|
||||
use(step: string, plugin: PluginFunction<T>): this;
|
||||
/**
|
||||
* Sends an email using the preselected transport object
|
||||
*
|
||||
* @param data E-data description
|
||||
* @returns Promise that resolves with the result of the send
|
||||
*/
|
||||
sendMail(data: SendMailOptions): Promise<T>;
|
||||
/**
|
||||
* Sends an email using the preselected transport object
|
||||
*
|
||||
* @param data E-data description
|
||||
* @param callback Callback to run once the sending succeeded or failed
|
||||
*/
|
||||
sendMail(data: SendMailOptions): Promise<T>;
|
||||
sendMail(data: SendMailOptions, callback: SendMailCallback<T>): void;
|
||||
getVersionString(): string;
|
||||
/**
|
||||
@@ -213,7 +228,10 @@ declare namespace Mail {
|
||||
type ListHeader = MailComposerListHeaderEntry;
|
||||
type ListHeaders = MailComposerListHeaders;
|
||||
type Envelope = MimeNodeEnvelopeInput;
|
||||
type TextEncoding = NonNullable<SendMailOptions['textEncoding']>;
|
||||
type Connection = SocketOptions;
|
||||
type TextEncoding = MailComposerTextEncoding;
|
||||
type PluginFunction<T = SentMessageInfo> = MailPluginFunction<T>;
|
||||
}
|
||||
/** The same aliases as module level exports, for `import * as Mail` and `import Mail = require()` */
|
||||
export type { SendMailOptions as Options, MimeNodeAddress as Address, MailComposerAttachment as Attachment, MailComposerAlternative as AttachmentLike, MailComposerAlternative as AmpAttachment, MailComposerIcalEvent as IcalAttachment, MimeNodeHeaders as Headers, MailComposerListHeaderEntry as ListHeader, MailComposerListHeaders as ListHeaders, MimeNodeEnvelopeInput as Envelope, SocketOptions as Connection, MailComposerTextEncoding as TextEncoding };
|
||||
export default Mail;
|
||||
+27
-8
@@ -157,6 +157,11 @@ class Mail extends node_events_1.EventEmitter {
|
||||
this.getSocket = false;
|
||||
}
|
||||
const mail = new mail_message_js_1.default(this, data);
|
||||
// a failed message is not going to be read anymore, release its content streams
|
||||
const fail = (err) => {
|
||||
mail.releaseStreams();
|
||||
done(err);
|
||||
};
|
||||
this.logger.debug({
|
||||
tnx: 'transport',
|
||||
name: this.transporter.name,
|
||||
@@ -170,7 +175,7 @@ class Mail extends node_events_1.EventEmitter {
|
||||
tnx: 'plugin',
|
||||
action: 'compile'
|
||||
}, 'PluginCompile Error: %s', err.message);
|
||||
return done(err);
|
||||
return fail(err);
|
||||
}
|
||||
let recipientCount;
|
||||
try {
|
||||
@@ -187,7 +192,7 @@ class Mail extends node_events_1.EventEmitter {
|
||||
tnx: 'transport',
|
||||
action: 'send'
|
||||
}, 'Compile Error: %s', err.message);
|
||||
return done(err);
|
||||
return fail(err);
|
||||
}
|
||||
const maxRecipients = mail.data.maxRecipients === undefined ? DEFAULT_MAX_RECIPIENTS : mail.data.maxRecipients;
|
||||
if (maxRecipients && recipientCount > maxRecipients) {
|
||||
@@ -198,7 +203,7 @@ class Mail extends node_events_1.EventEmitter {
|
||||
tnx: 'transport',
|
||||
action: 'send'
|
||||
}, 'Send Error: %s', err.message);
|
||||
return done(err);
|
||||
return fail(err);
|
||||
}
|
||||
this._processPlugins('stream', mail, err => {
|
||||
if (err) {
|
||||
@@ -207,7 +212,7 @@ class Mail extends node_events_1.EventEmitter {
|
||||
tnx: 'plugin',
|
||||
action: 'stream'
|
||||
}, 'PluginStream Error: %s', err.message);
|
||||
return done(err);
|
||||
return fail(err);
|
||||
}
|
||||
if (mail.data.dkim || this.dkim) {
|
||||
mail.message.processFunc(input => {
|
||||
@@ -222,6 +227,7 @@ class Mail extends node_events_1.EventEmitter {
|
||||
}
|
||||
this.transporter.send(mail, (...args) => {
|
||||
if (args[0]) {
|
||||
mail.releaseStreams();
|
||||
this.logger.error({
|
||||
err: args[0],
|
||||
tnx: 'transport',
|
||||
@@ -289,6 +295,11 @@ class Mail extends node_events_1.EventEmitter {
|
||||
// setup socket handler for the mailer object
|
||||
this.getSocket = (options, callback) => {
|
||||
const protocol = proxy.protocol.replace(/:$/, '').toLowerCase();
|
||||
// The proxy handshake is a part of connecting and draws from the same deadline as the
|
||||
// SMTP connection that follows it: whatever the proxy takes, the connection does not get
|
||||
const configuredTimeout = Number(this.options.connectionTimeout) || 0;
|
||||
const connectStartedAt = Number(options.connectStartedAt) || Date.now();
|
||||
const connectionTimeout = configuredTimeout ? Math.max(connectStartedAt + configuredTimeout - Date.now(), 1) : undefined;
|
||||
if (this.meta.has('proxy_handler_' + protocol)) {
|
||||
return this.meta.get('proxy_handler_' + protocol)(proxy, options, callback);
|
||||
}
|
||||
@@ -296,7 +307,7 @@ class Mail extends node_events_1.EventEmitter {
|
||||
// Connect using a HTTP CONNECT method
|
||||
case 'http':
|
||||
case 'https':
|
||||
(0, http_proxy_client_js_1.default)(proxy.href, options.port, options.host, this.options.tls || {}, (err, socket) => {
|
||||
(0, http_proxy_client_js_1.default)(proxy.href, options.port, options.host, connectionTimeout ? Object.assign({}, this.options.tls, { timeout: connectionTimeout }) : this.options.tls || {}, (err, socket) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
@@ -330,6 +341,9 @@ class Mail extends node_events_1.EventEmitter {
|
||||
},
|
||||
command: 'connect'
|
||||
};
|
||||
if (connectionTimeout) {
|
||||
connectionOpts.timeout = connectionTimeout;
|
||||
}
|
||||
if (proxy.username || proxy.password) {
|
||||
const username = proxy.username || '';
|
||||
const password = proxy.password || '';
|
||||
@@ -359,11 +373,13 @@ class Mail extends node_events_1.EventEmitter {
|
||||
if (node_net_1.default.isIP(proxy.hostname)) {
|
||||
return connect(proxy.hostname);
|
||||
}
|
||||
return node_dns_1.default.resolve(proxy.hostname, (err, address) => {
|
||||
// lookup goes through the hosts file and returns an IPv6 address as well,
|
||||
// the same way a connection to the proxy host would be resolved otherwise
|
||||
return node_dns_1.default.lookup(proxy.hostname, (err, address) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
connect(Array.isArray(address) ? address[0] : address);
|
||||
connect(address);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -382,9 +398,12 @@ class Mail extends node_events_1.EventEmitter {
|
||||
return callback(err);
|
||||
}
|
||||
let cidCounter = 0;
|
||||
// the media type is a bounded token: a quote, whitespace, '>' or '<' ends the
|
||||
// attribute value and with it the type, so one img tag never scans ahead to a
|
||||
// ';' that belongs to a later tag, which was quadratic
|
||||
html = (html || '')
|
||||
.toString()
|
||||
.replace(/(<img\b[^<>]{0,1024} src\s{0,20}=[\s"']{0,20})(data:([^;]+);[^"'>\s]+)/gi, (match, prefix, dataUri, mimeType) => {
|
||||
.replace(/(<img\b[^<>]{0,1024} src\s{0,20}=[\s"']{0,20})(data:([^;"'<>\s]{1,255});[^"'>\s]+)/gi, (match, prefix, dataUri, mimeType) => {
|
||||
const cid = node_crypto_1.default.randomBytes(10).toString('hex') + '@localhost';
|
||||
if (!mail.data.attachments) {
|
||||
mail.data.attachments = [];
|
||||
|
||||
+21
-5
@@ -3,6 +3,7 @@ import type { MailComposerOptions } from '../mail-composer/index.js';
|
||||
import type { DKIMOptions } from '../dkim/index.js';
|
||||
import type { SMTPEnvelopeDsn } from '../smtp-connection/index.js';
|
||||
import type { SMTPTransportAuthOptions } from '../smtp-transport/index.js';
|
||||
import type { SESSendEmailRequest } from '../ses-transport/index.js';
|
||||
import type { NodemailerError } from '../errors.js';
|
||||
import type { ResolveContentOptions } from '../shared/index.js';
|
||||
import type Mail from './index.js';
|
||||
@@ -14,18 +15,22 @@ import type { SentMessageInfo } from './index.js';
|
||||
export interface SendMailOptions extends MailComposerOptions {
|
||||
/** DKIM signing options for this message, used instead of the ones of the transporter */
|
||||
dkim?: DKIMOptions | undefined;
|
||||
/** Extra DKIM options for this message, merged over the options of the signer. The SES transport sets skipFields here, the option is not meant for callers */
|
||||
_dkim?: DKIMOptions | undefined;
|
||||
/** Recipients allowed on this message, 0 disables the limit, defaults to 100000 */
|
||||
maxRecipients?: number | undefined;
|
||||
/** SMTP transports: DSN parameters for the envelope, sent when the server supports the DSN extension */
|
||||
dsn?: SMTPEnvelopeDsn | undefined;
|
||||
/** SMTP transports: RFC 8689, send the REQUIRETLS parameter with MAIL FROM */
|
||||
requireTLSExtensionEnabled?: boolean | undefined;
|
||||
/** SMTP transports: per-message authentication settings, used instead of the transport level auth */
|
||||
/**
|
||||
* SMTP transports: per-message authentication settings, used instead of the transport level auth.
|
||||
* Only the credential fields are taken from it (type, user, pass, accessToken, refreshToken, expires),
|
||||
* the token endpoint and client settings stay those of the transport
|
||||
*/
|
||||
auth?: SMTPTransportAuthOptions | undefined;
|
||||
/** SES transport: extra SendEmailCommand parameters merged into the API call */
|
||||
ses?: {
|
||||
[key: string]: unknown;
|
||||
} | undefined;
|
||||
ses?: SESSendEmailRequest | undefined;
|
||||
}
|
||||
/**
|
||||
* Default message fields, the third argument of createTransport. Applied to every message
|
||||
@@ -71,7 +76,13 @@ export interface MailMessageListHeader {
|
||||
export default class MailMessage<T = SentMessageInfo> {
|
||||
mailer: Mail<T>;
|
||||
data: MailMessageData;
|
||||
message: MimeNode | null;
|
||||
/**
|
||||
* The compiled MIME tree. Set once the compile step is done, so it is null while the
|
||||
* 'compile' plugins run and set by the time the 'stream' plugins and the transport see
|
||||
* the message. Declared as always set, the way @types/nodemailer declared it, since the
|
||||
* plugins that read it are the ones that run after it is set
|
||||
*/
|
||||
message: MimeNode;
|
||||
constructor(mailer: Mail<T>, data?: SendMailOptions);
|
||||
resolveContent(data: {
|
||||
[key: string]: any;
|
||||
@@ -83,6 +94,11 @@ export default class MailMessage<T = SentMessageInfo> {
|
||||
[key: string]: any;
|
||||
}, key: string | number, options?: ResolveContentOptions | false): Promise<any>;
|
||||
resolveAll(callback: MailMessageDataCallback): void;
|
||||
/**
|
||||
* Destroys the content streams of the message once sending failed. A stream that was not
|
||||
* read to the end would otherwise keep the file or the socket behind it open
|
||||
*/
|
||||
releaseStreams(): void;
|
||||
normalize(callback: MailMessageDataCallback): void;
|
||||
setMailerHeader(): void;
|
||||
setPriorityHeaders(): void;
|
||||
|
||||
+25
-1
@@ -107,8 +107,9 @@ class MailMessage {
|
||||
if (this.data.attachments && this.data.attachments.length) {
|
||||
this.data.attachments.forEach((attachment, i) => {
|
||||
if (!attachment.filename) {
|
||||
// a backslash separates as well, so a Windows path does not put the sender's directories in the headers
|
||||
attachment.filename =
|
||||
(attachment.path || attachment.href || '').split('/').pop().split('?').shift() ||
|
||||
(attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() ||
|
||||
'attachment-' + (i + 1);
|
||||
if (attachment.filename.indexOf('.') < 0) {
|
||||
attachment.filename += '.' + mimeFuncs.detectExtension(attachment.contentType);
|
||||
@@ -173,6 +174,29 @@ class MailMessage {
|
||||
};
|
||||
setImmediate(() => resolveNext());
|
||||
}
|
||||
/**
|
||||
* Destroys the content streams of the message once sending failed. A stream that was not
|
||||
* read to the end would otherwise keep the file or the socket behind it open
|
||||
*/
|
||||
releaseStreams() {
|
||||
const data = this.data;
|
||||
const values = [data.html, data.text, data.watchHtml, data.amp, data.raw, data.icalEvent];
|
||||
for (const key of ['attachments', 'alternatives']) {
|
||||
if (Array.isArray(data[key])) {
|
||||
// one at a time, spreading a long enough list into the arguments throws
|
||||
for (const entry of data[key]) {
|
||||
values.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const value of values) {
|
||||
if (value && typeof value === 'object') {
|
||||
shared.destroyStream(value);
|
||||
shared.destroyStream(value.content);
|
||||
shared.destroyStream(value.raw);
|
||||
}
|
||||
}
|
||||
}
|
||||
normalize(callback) {
|
||||
const envelope = this.message.getEnvelope();
|
||||
const messageId = this.message.messageId();
|
||||
|
||||
+7
-3
@@ -2,8 +2,8 @@
|
||||
* A header value split into the value token and its parameters, the result of parseHeaderValue
|
||||
*/
|
||||
export interface ParsedHeaderValue {
|
||||
/** The value ahead of the parameters, for example the content type */
|
||||
value: string | false;
|
||||
/** The value ahead of the parameters, for example the content type, an empty string when there is none */
|
||||
value: string;
|
||||
/** Parameter values keyed by lowercase parameter name */
|
||||
params: Record<string, string>;
|
||||
}
|
||||
@@ -25,6 +25,9 @@ export interface EncodedHeaderParam {
|
||||
/** Parameter value of this part */
|
||||
value: string;
|
||||
}
|
||||
/** The names @types/nodemailer used for the two types above */
|
||||
export type HeaderValue = StructuredHeaderValue;
|
||||
export type ParsedHeaderParam = EncodedHeaderParam;
|
||||
/**
|
||||
* Checks if a value is plaintext string (uses only printable 7bit chars)
|
||||
*
|
||||
@@ -56,6 +59,7 @@ export declare function quoteString(value?: string): string;
|
||||
* no need to encode the values in any way. If the value is plaintext but has
|
||||
* longer lines then allowed, then use format=flowed
|
||||
*
|
||||
* @param str Multi line string to check
|
||||
* @param lineLength Max line length to check for
|
||||
* @returns Returns true if there is at least one line longer than lineLength chars
|
||||
*/
|
||||
@@ -97,9 +101,9 @@ export declare function buildHeaderValue(structured: StructuredHeaderValue): str
|
||||
* title*0*=utf-8''unicode
|
||||
* title*1*=%20string
|
||||
*
|
||||
* @param key Parameter name the generated keys are built from, for example title
|
||||
* @param data String to be encoded
|
||||
* @param [maxLength=50] Max length for generated chunks
|
||||
* @param [fromCharset='UTF-8'] Source sharacter set
|
||||
* @return A list of encoded keys and headers
|
||||
*/
|
||||
export declare function buildHeaderParam(key: string, data: string | Buffer, maxLength?: number): EncodedHeaderParam[];
|
||||
|
||||
+232
-100
@@ -70,6 +70,12 @@ function isPlainText(value, isParam) {
|
||||
const re = isParam ? /[\x00-\x1f\x7f"\u0080-\uFFFF]/ : /[\x00-\x08\x0b\x0c\x0e-\x1f\u0080-\uFFFF]/;
|
||||
return typeof value === 'string' && !re.test(value);
|
||||
}
|
||||
// A parameter value goes out as is only when it is plain text that does not look like an encoded
|
||||
// word. Receivers decode encoded words found in parameter values (libmime does so for unencoded
|
||||
// continuations), so as plain text it would come back as something else
|
||||
function isPlainParamValue(value) {
|
||||
return isPlainText(value, true) && !value.includes('=?');
|
||||
}
|
||||
/**
|
||||
* Wraps a value into a quoted-string. Inside one a quote would end the string early
|
||||
* and a backslash would escape whatever follows it, so both go out as quoted-pairs.
|
||||
@@ -88,6 +94,7 @@ function quoteString(value) {
|
||||
* no need to encode the values in any way. If the value is plaintext but has
|
||||
* longer lines then allowed, then use format=flowed
|
||||
*
|
||||
* @param str Multi line string to check
|
||||
* @param lineLength Max line length to check for
|
||||
* @returns Returns true if there is at least one line longer than lineLength chars
|
||||
*/
|
||||
@@ -185,26 +192,38 @@ function encodeWord(data, mimeWordEncoding, maxLength) {
|
||||
*/
|
||||
function encodeWords(value, mimeWordEncoding, maxLength, encodeAll) {
|
||||
maxLength = maxLength || 0;
|
||||
// find first word with a non-printable ascii or special symbol in it
|
||||
const firstMatch = value.match(/(?:^|\s)([^\s]*["\u0080-\uFFFF])/);
|
||||
if (!firstMatch) {
|
||||
// find the first and the last character that needs encoding, a quote or anything outside
|
||||
// printable ascii. A scan rather than a regex: matching the last such word with a
|
||||
// backtracking pattern took quadratic time on a long word of non-ascii characters
|
||||
let firstSpecial = -1;
|
||||
let lastSpecial = -1;
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const code = value.charCodeAt(i);
|
||||
if (code === 0x22 || code >= 0x80) {
|
||||
if (firstSpecial < 0) {
|
||||
firstSpecial = i;
|
||||
}
|
||||
lastSpecial = i;
|
||||
}
|
||||
}
|
||||
if (firstSpecial < 0) {
|
||||
return value;
|
||||
}
|
||||
if (encodeAll) {
|
||||
// if it is requested to encode everything or the string contains something that resebles encoded word, then encode everything
|
||||
return encodeWord(value, mimeWordEncoding, maxLength);
|
||||
}
|
||||
// find the last word with a non-printable ascii in it
|
||||
const lastMatch = value.match(/(["\u0080-\uFFFF][^\s]*)[^"\u0080-\uFFFF]*$/);
|
||||
if (!lastMatch) {
|
||||
// should not happen
|
||||
return value;
|
||||
// encode from the start of the word holding the first such character to the end of the
|
||||
// word holding the last one, so the encoded words stay separated from the plain text by
|
||||
// the whitespace that was already there
|
||||
let startIndex = firstSpecial;
|
||||
while (startIndex > 0 && !/\s/.test(value.charAt(startIndex - 1))) {
|
||||
startIndex--;
|
||||
}
|
||||
let endIndex = lastSpecial + 1;
|
||||
while (endIndex < value.length && !/\s/.test(value.charAt(endIndex))) {
|
||||
endIndex++;
|
||||
}
|
||||
const startIndex = firstMatch.index +
|
||||
(firstMatch[0].match(/[^\s]/) || {
|
||||
index: 0
|
||||
}).index;
|
||||
const endIndex = lastMatch.index + (lastMatch[1] || '').length;
|
||||
return ((startIndex ? value.substr(0, startIndex) : '') +
|
||||
encodeWord(value.substring(startIndex, endIndex), mimeWordEncoding || 'Q', maxLength) +
|
||||
(endIndex < value.length ? value.substr(endIndex) : ''));
|
||||
@@ -224,7 +243,7 @@ function buildHeaderValue(structured) {
|
||||
const value = structured.params[key];
|
||||
// a parameter name is a token too and it is emitted without any quoting around it
|
||||
const param = key.replace(/[\x00-\x1f\x7f]/g, '');
|
||||
if (!isPlainText(value, true) || value.length >= 75) {
|
||||
if (!isPlainParamValue(value) || value.length >= 75) {
|
||||
buildHeaderParam(param, value, 50).forEach(encodedParam => {
|
||||
if (!/[\s"\\;:/=(),<>@[\]?]|^[-']|'$/.test(encodedParam.value) || encodedParam.key.substr(-1) === '*') {
|
||||
paramsArray.push(encodedParam.key + '=' + encodedParam.value);
|
||||
@@ -234,7 +253,10 @@ function buildHeaderValue(structured) {
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (/[\s'"\\;:/=(),<>@[\]?]|^-/.test(value)) {
|
||||
else if (!value.length || /[\s'"\\;:/=(),<>@[\]?]|^-/.test(value)) {
|
||||
// a parameter value is a token or a quoted-string and a token is never empty, so
|
||||
// a valueless parameter such as the 'flag' of 'multipart/mixed; flag; boundary=b'
|
||||
// goes out as 'flag=""' rather than as the 'flag=' that parses as neither
|
||||
paramsArray.push(param + '=' + JSON.stringify(value));
|
||||
}
|
||||
else {
|
||||
@@ -256,9 +278,9 @@ function buildHeaderValue(structured) {
|
||||
* title*0*=utf-8''unicode
|
||||
* title*1*=%20string
|
||||
*
|
||||
* @param key Parameter name the generated keys are built from, for example title
|
||||
* @param data String to be encoded
|
||||
* @param [maxLength=50] Max length for generated chunks
|
||||
* @param [fromCharset='UTF-8'] Source sharacter set
|
||||
* @return A list of encoded keys and headers
|
||||
*/
|
||||
function buildHeaderParam(key, data, maxLength) {
|
||||
@@ -270,7 +292,7 @@ function buildHeaderParam(key, data, maxLength) {
|
||||
let i, len;
|
||||
maxLength = maxLength || 50;
|
||||
// process ascii only text
|
||||
if (isPlainText(data, true)) {
|
||||
if (isPlainParamValue(data)) {
|
||||
// check if conversion is even needed
|
||||
if (encodedStr.length <= maxLength) {
|
||||
return [
|
||||
@@ -385,6 +407,26 @@ function buildHeaderParam(key, data, maxLength) {
|
||||
value: item.line
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* An RFC 2045 token: printable ASCII without SPACE, the control characters, DEL and the
|
||||
* tspecials. A charset name is one, and the name a continuation carries is written into
|
||||
* the encoded word the parameter value becomes, so it is checked against this before it
|
||||
* goes in. Whitespace used to come off it only because the value was trimmed first.
|
||||
*/
|
||||
const TOKEN = /^[^\x00-\x20\x7f()<>@,;:\\"/[\]?=]+$/;
|
||||
/**
|
||||
* Whether a string can be a header parameter name.
|
||||
*
|
||||
* A parameter name is a token, so it is never empty. A "__proto__" name would target the
|
||||
* prototype chain of the params object instead of an own property of it and read back as
|
||||
* Object.prototype, so it is no name either.
|
||||
*
|
||||
* @param name Candidate parameter name, already lowercased
|
||||
* @return true when the name can be used
|
||||
*/
|
||||
function _isParamName(name) {
|
||||
return !!name && !(0, objects_js_1.isProtoKey)(name);
|
||||
}
|
||||
/**
|
||||
* Parses a header value with key=value arguments into a structured
|
||||
* object.
|
||||
@@ -402,137 +444,227 @@ function buildHeaderParam(key, data, maxLength) {
|
||||
*/
|
||||
function parseHeaderValue(str) {
|
||||
const response = {
|
||||
value: false,
|
||||
value: '',
|
||||
params: {}
|
||||
};
|
||||
// Parameter names come from a caller supplied contentType/contentDisposition. A
|
||||
// "__proto__" name would target the prototype chain of the params object instead of
|
||||
// an own property of it, and read back as Object.prototype, so it is dropped.
|
||||
// A duplicated parameter resolves to its first occurrence, the way a duplicated header
|
||||
// does. Letting the last one win disagrees with the receivers that take the first, and
|
||||
// the two readings of 'boundary="b"; boundary="c"' name different delimiters. The
|
||||
// continuation join below is the only writer of the name it builds, so it tests the
|
||||
// name with _isParamName directly rather than taking that rule along from here.
|
||||
const setParam = (name, value) => {
|
||||
if (!(0, objects_js_1.isProtoKey)(name)) {
|
||||
name = name.toLowerCase();
|
||||
if (_isParamName(name) && !Object.prototype.hasOwnProperty.call(response.params, name)) {
|
||||
response.params[name] = value;
|
||||
}
|
||||
};
|
||||
let key = false;
|
||||
let value = '';
|
||||
let type = 'value';
|
||||
let stage = 'value';
|
||||
let quote = false;
|
||||
let escaped = false;
|
||||
let chr;
|
||||
// Whitespace seen outside a quoted string is held back until a significant character
|
||||
// follows it, so the whitespace around a value is dropped without trimming spaces the
|
||||
// sender quoted on purpose. Trimming the stored value instead loses the trailing space
|
||||
// of 'filename*0="Annual Report "', which the next continuation section is appended to.
|
||||
let pendingSpace = '';
|
||||
let quoteClosed = false;
|
||||
// Whitespace ahead of the first character of a value is padding and is dropped, the
|
||||
// whitespace between two characters of it is content
|
||||
const flushSpace = () => {
|
||||
if (value.length) {
|
||||
value += pendingSpace;
|
||||
}
|
||||
pendingSpace = '';
|
||||
};
|
||||
const addChr = (c) => {
|
||||
flushSpace();
|
||||
value += c;
|
||||
};
|
||||
const takeValue = () => {
|
||||
const taken = value;
|
||||
value = '';
|
||||
pendingSpace = '';
|
||||
quoteClosed = false;
|
||||
return taken;
|
||||
};
|
||||
const storeValue = () => {
|
||||
const taken = takeValue();
|
||||
if (key === false) {
|
||||
response.value = taken;
|
||||
}
|
||||
else {
|
||||
setParam(key, taken);
|
||||
}
|
||||
};
|
||||
// A parameter name with no '=' is a valueless parameter, not the start of the next one.
|
||||
// Without this the name keeps growing across the ';' and swallows whatever follows, which
|
||||
// is how 'multipart/mixed; flag; boundary="AAA"' lost its boundary to a parameter named
|
||||
// 'flag; boundary' and left the node declaring the generated boundary beside the asked
|
||||
// for one, so a receiver reading the first of the two found no delimiter it matched.
|
||||
const storeEmptyKey = () => {
|
||||
setParam(takeValue().trim(), '');
|
||||
};
|
||||
for (let i = 0, len = str.length; i < len; i++) {
|
||||
chr = str.charAt(i);
|
||||
if (type === 'key') {
|
||||
if (stage === 'key') {
|
||||
if (chr === '=') {
|
||||
key = value.trim().toLowerCase();
|
||||
type = 'value';
|
||||
value = '';
|
||||
key = takeValue().trim();
|
||||
stage = 'value';
|
||||
continue;
|
||||
}
|
||||
if (chr === ';') {
|
||||
storeEmptyKey();
|
||||
continue;
|
||||
}
|
||||
value += chr;
|
||||
}
|
||||
else {
|
||||
if (escaped) {
|
||||
value += chr;
|
||||
if (quoteClosed && chr !== ';') {
|
||||
// Nothing behind a closed quoted string reaches the value. RFC 2045 says a
|
||||
// parameter value is a token or a quoted string and not both, so what follows
|
||||
// one is junk and only the ';' that ends the parameter still counts. Tested
|
||||
// ahead of the branches rather than beside the append at the foot of them,
|
||||
// where each branch above was a way around it: a second '"' reopened quoting
|
||||
// and swallowed the rest of the header, so 'boundary="AAA" "; boundary=BBB"'
|
||||
// read as a single boundary of 'AAA ', and the junk of
|
||||
// 'boundary="AAA" (unterminated comment' still joined the declared boundary
|
||||
// through the escape branch. quoteClosed is only ever set while no quote is
|
||||
// open, and this is what keeps one from being opened afterwards.
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
else if (chr === '\\') {
|
||||
if (escaped) {
|
||||
addChr(chr);
|
||||
}
|
||||
else if (quote && chr === '\\') {
|
||||
// a backslash only escapes inside a quoted string, everywhere else it is an
|
||||
// ordinary character. Treating it as an escape turns the parameter value
|
||||
// 'C:\Users\me\report.txt' into 'C:Usersmereport.txt'
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
else if (quote && chr === quote) {
|
||||
quote = false;
|
||||
quoteClosed = true;
|
||||
}
|
||||
else if (!quote && chr === '"') {
|
||||
quote = chr;
|
||||
flushSpace();
|
||||
}
|
||||
else if (!quote && chr === ';') {
|
||||
if (key === false) {
|
||||
response.value = value.trim();
|
||||
}
|
||||
else {
|
||||
setParam(key, value.trim());
|
||||
}
|
||||
type = 'key';
|
||||
value = '';
|
||||
storeValue();
|
||||
stage = 'key';
|
||||
}
|
||||
else if (!quote && (chr === ' ' || chr === '\t')) {
|
||||
pendingSpace += chr;
|
||||
}
|
||||
else {
|
||||
value += chr;
|
||||
addChr(chr);
|
||||
}
|
||||
escaped = false;
|
||||
}
|
||||
}
|
||||
if (type === 'value') {
|
||||
if (key === false) {
|
||||
response.value = value.trim();
|
||||
}
|
||||
else {
|
||||
setParam(key, value.trim());
|
||||
}
|
||||
if (stage === 'value') {
|
||||
storeValue();
|
||||
}
|
||||
else if (value.trim()) {
|
||||
setParam(value.trim().toLowerCase(), '');
|
||||
else {
|
||||
// a key with no value, as in 'Header-Key: somevalue; key=value; emptykey'
|
||||
storeEmptyKey();
|
||||
}
|
||||
// handle parameter value continuations
|
||||
// https://tools.ietf.org/html/rfc2231#section-3
|
||||
// preprocess values
|
||||
// Sections are collected in a list and ordered below rather than written into an array
|
||||
// at their own section number. An index write makes the array as long as the number the
|
||||
// header asked for, and the join that follows walks all of it, so the 55 byte value
|
||||
// "attachment; filename*0*=utf-8''a; filename*4000000000=b" held a core for over two
|
||||
// minutes.
|
||||
const continuations = new Map();
|
||||
Object.keys(response.params).forEach(key => {
|
||||
let actualKey, nr, match, value;
|
||||
if ((match = key.match(/(\*(\d+)|\*(\d+)\*|\*)$/))) {
|
||||
actualKey = key.substr(0, match.index);
|
||||
nr = Number(match[2] || match[3]) || 0;
|
||||
if ((0, objects_js_1.isProtoKey)(actualKey)) {
|
||||
// see setParam. Reading it back would yield Object.prototype, which is
|
||||
// an object, so the initializer below would be skipped and the write
|
||||
// that follows would throw out of a header build the caller can not catch
|
||||
delete response.params[key];
|
||||
return;
|
||||
}
|
||||
if (!response.params[actualKey] || typeof response.params[actualKey] !== 'object') {
|
||||
response.params[actualKey] = {
|
||||
charset: false,
|
||||
values: []
|
||||
};
|
||||
}
|
||||
value = response.params[key];
|
||||
if (nr === 0 && match[0].substr(-1) === '*' && (match = value.match(/^([^']*)'[^']*'(.*)$/))) {
|
||||
response.params[actualKey].charset = match[1] || 'iso-8859-1';
|
||||
value = match[2];
|
||||
}
|
||||
response.params[actualKey].values[nr] = value;
|
||||
// remove the old reference
|
||||
delete response.params[key];
|
||||
const match = key.match(/(\*(\d+)|\*(\d+)\*|\*)$/);
|
||||
if (!match) {
|
||||
// not a continuation parameter, there is nothing to join
|
||||
return;
|
||||
}
|
||||
const actualKey = key.substr(0, match.index);
|
||||
const nr = Number(match[2] || match[3]) || 0;
|
||||
// RFC 2231 section 4.1: only a section whose name ends in '*' is percent encoded
|
||||
const encoded = match[0].substr(-1) === '*';
|
||||
// remove the old reference
|
||||
let value = response.params[key];
|
||||
delete response.params[key];
|
||||
if (!_isParamName(actualKey)) {
|
||||
// the joined value can not be written back under this name. It is empty when the
|
||||
// continuation suffix was all there was of it, as in the bare '*' of 'text/plain; a;*'
|
||||
return;
|
||||
}
|
||||
let continuation = continuations.get(actualKey);
|
||||
if (!continuation) {
|
||||
continuation = { charset: false, sections: [] };
|
||||
continuations.set(actualKey, continuation);
|
||||
}
|
||||
const charsetMatch = nr === 0 && encoded ? value.match(/^([^']*)'[^']*'(.*)$/) : null;
|
||||
if (charsetMatch) {
|
||||
// the charset is a token, and anything else named as one is no charset a consumer
|
||||
// could resolve, so it reads as the unnamed case rather than being carried into
|
||||
// the encoded word below. A "\r\n" of a prefix would otherwise reach a consumer
|
||||
// of the parsed value as the charset of a word that no decoder can act on
|
||||
continuation.charset = TOKEN.test(charsetMatch[1]) ? charsetMatch[1] : 'iso-8859-1';
|
||||
value = charsetMatch[2];
|
||||
}
|
||||
continuation.sections.push({ nr, value, encoded });
|
||||
});
|
||||
// concatenate split rfc2231 strings and convert encoded strings to mime encoded words
|
||||
Object.keys(response.params).forEach(key => {
|
||||
let value;
|
||||
if (response.params[key] && Array.isArray(response.params[key].values)) {
|
||||
value = response.params[key].values.map((val) => val || '').join('');
|
||||
if (response.params[key].charset) {
|
||||
// convert "%AB" to "=?charset?Q?=AB?="
|
||||
response.params[key] =
|
||||
'=?' +
|
||||
response.params[key].charset +
|
||||
'?Q?' +
|
||||
value
|
||||
// fix invalidly encoded chars
|
||||
.replace(/[=?_\s]/g, s => {
|
||||
const c = s.charCodeAt(0).toString(16);
|
||||
if (s === ' ') {
|
||||
return '_';
|
||||
}
|
||||
return '%' + (c.length < 2 ? '0' : '') + c;
|
||||
})
|
||||
// change from urlencoding to percent encoding
|
||||
.replace(/%/g, '=') +
|
||||
'?=';
|
||||
}
|
||||
else {
|
||||
response.params[key] = value;
|
||||
}
|
||||
continuations.forEach((continuation, key) => {
|
||||
if (Object.prototype.hasOwnProperty.call(response.params, key)) {
|
||||
// The same name was also given as a plain parameter, which the starred keys were
|
||||
// just deleted from around, so this write would be the only one in the function
|
||||
// to override a name already taken. 'filename=plain.txt; filename*0=evil.txt'
|
||||
// resolves to the plain parameter either way round, so the reading does not come
|
||||
// down to which of the two spellings the sender put first
|
||||
return;
|
||||
}
|
||||
continuation.sections.sort((a, b) => a.nr - b.nr);
|
||||
if (!continuation.charset) {
|
||||
// nothing said which charset the percent escapes of an encoded section are in,
|
||||
// so every section is passed on as the text it already is
|
||||
response.params[key] = continuation.sections.map(section => section.value).join('');
|
||||
return;
|
||||
}
|
||||
// convert "%AB" to "=?charset?Q?=AB?="
|
||||
response.params[key] = '=?' + continuation.charset + '?Q?' + continuation.sections.map(_encodeContinuationSection).join('') + '?=';
|
||||
});
|
||||
return response;
|
||||
}
|
||||
/**
|
||||
* Renders one parameter value continuation section as the payload of a Q encoded word.
|
||||
*
|
||||
* A section whose name ends in '*' is percent encoded and its escapes carry the bytes of
|
||||
* the value, so they only have to be rewritten into the "=AB" spelling a Q encoded word
|
||||
* uses. A section without the '*' is literal text (RFC 2231 section 4.1), so its '%' is a
|
||||
* '%' and is escaped along with the characters a Q encoded word can not carry bare.
|
||||
* Decoding a literal section invents bytes that never appeared on the wire: it is how the
|
||||
* value 'filename*0*=utf-8''safe; filename*1=%2F..%2F..%2Fetc%2Fpasswd' was emitted as a
|
||||
* filename every receiving client reads back as 'safe/../../etc/passwd'.
|
||||
*
|
||||
* @param section One collected continuation section
|
||||
* @return The section as Q encoded word payload
|
||||
*/
|
||||
function _encodeContinuationSection(section) {
|
||||
const specials = section.encoded ? /[=?_\s]/g : /[=?_\s%]/g;
|
||||
return (section.value
|
||||
// fix invalidly encoded chars
|
||||
.replace(specials, s => {
|
||||
const c = s.charCodeAt(0).toString(16);
|
||||
if (s === ' ') {
|
||||
return '_';
|
||||
}
|
||||
return '%' + (c.length < 2 ? '0' : '') + c;
|
||||
})
|
||||
// change from urlencoding to percent encoding
|
||||
.replace(/%/g, '='));
|
||||
}
|
||||
/**
|
||||
* Returns file extension for a content type string. If no suitable extensions
|
||||
* are found, 'bin' is used as the default extension
|
||||
|
||||
+6
-3
@@ -108,10 +108,11 @@ export interface MimeNodeEnvelope {
|
||||
}
|
||||
/**
|
||||
* Envelope as accepted by setEnvelope. Recipients are collected from to, cc and bcc, any
|
||||
* other field is copied to the envelope as is
|
||||
* other field is copied to the envelope as is. A `from` of false is the null sender of a
|
||||
* bounce message, it is sent as MAIL FROM:<>
|
||||
*/
|
||||
export interface MimeNodeEnvelopeInput {
|
||||
from?: MimeNodeAddressInput | undefined;
|
||||
from?: MimeNodeAddressInput | false | undefined;
|
||||
to?: MimeNodeAddressInput | undefined;
|
||||
cc?: MimeNodeAddressInput | undefined;
|
||||
bcc?: MimeNodeAddressInput | undefined;
|
||||
@@ -180,7 +181,7 @@ declare class MimeNode {
|
||||
childNodes: MimeNode[];
|
||||
/** Filename for this node. Useful with attachments */
|
||||
filename?: string | undefined;
|
||||
/** Body content, or the error a content stream emitted before it was read */
|
||||
/** Body content */
|
||||
content?: MimeNodeContent | Error | undefined;
|
||||
/** Lowercase content type, set when the headers are built */
|
||||
contentType?: string | undefined;
|
||||
@@ -327,4 +328,6 @@ declare namespace MimeNode {
|
||||
type Addresses = MimeNodeAddresses;
|
||||
type Envelope = MimeNodeEnvelope;
|
||||
}
|
||||
/** The same aliases as module level exports, for `import * as MimeNode` and `import MimeNode = require()` */
|
||||
export type { MimeNodeOptions as Options, MimeNodeAddresses as Addresses, MimeNodeEnvelope as Envelope };
|
||||
export default MimeNode;
|
||||
+269
-133
@@ -64,6 +64,35 @@ const QUOTED_STRING = /^"(?:[^"\\]|\\[\s\S])*"$/;
|
||||
// else goes into angle brackets so that the header can not be read as more addresses than
|
||||
// the envelope carries
|
||||
const PLAIN_ADDRESS = /^[^\s"(),:;<>@[\\\]]+@[^\s"(),:;<>@[\\\]]+$/;
|
||||
/**
|
||||
* A domain literal that ends an address, see _normalizeAddress
|
||||
*/
|
||||
const DOMAIN_LITERAL_TAIL = /@(\[[^[\]\\]*\])$/;
|
||||
/**
|
||||
* Removes the whitespace inside every <...> of a References value. A "<" with no ">" after
|
||||
* it is left as it is. An index scan, the regex /<[^>]*>/g rescans the rest of the value
|
||||
* from every such "<"
|
||||
*
|
||||
* @param value References header value
|
||||
* @returns The value with the msg-ids closed up
|
||||
*/
|
||||
function stripWhitespaceInAngleBrackets(value) {
|
||||
let result = '';
|
||||
let pos = 0;
|
||||
for (;;) {
|
||||
const open = value.indexOf('<', pos);
|
||||
if (open < 0) {
|
||||
break;
|
||||
}
|
||||
const close = value.indexOf('>', open + 1);
|
||||
if (close < 0) {
|
||||
break;
|
||||
}
|
||||
result += value.slice(pos, open) + value.slice(open, close + 1).replace(/\s/g, '');
|
||||
pos = close + 1;
|
||||
}
|
||||
return result + value.slice(pos);
|
||||
}
|
||||
// domainToASCII and domainToUnicode are WHATWG host parsers rather than plain IDNA
|
||||
// mappers, so they do more than map: they cut the host at '/', '\\', '?' and '#', drop C0
|
||||
// controls, and percent-decode. Handing them 'evil.example/mail.corp.example' returns the
|
||||
@@ -71,6 +100,42 @@ const PLAIN_ADDRESS = /^[^\s"(),:;<>@[\\\]]+@[^\s"(),:;<>@[\\\]]+$/;
|
||||
// unroutable garbage into mail for a domain the sender never named. None of these
|
||||
// characters are legal in a domain, so keep them away from the mapper.
|
||||
const URL_PARSER_UNSAFE = /[/\\?#%\x00-\x20\x7F]/;
|
||||
// RFC 1035 bounds a label at 63 octets and a domain at 253. An A-label is never shorter than
|
||||
// the code points it encodes, so anything past these bounds could not have become a valid
|
||||
// domain whichever way it is encoded. The bundled codec is super-linear in the length of a
|
||||
// label, a 60 KB label of distinct code points took seconds to encode, several times per
|
||||
// address, so it is only handed what could be a domain
|
||||
const MAX_DOMAIN_LENGTH = 253;
|
||||
const MAX_LABEL_LENGTH = 63;
|
||||
// pipeline() needs a callback to not throw, the errors it sees reach the last stream anyway
|
||||
const PIPELINE_NOOP = () => false;
|
||||
// The error a node streaming into an output that was destroyed stops with
|
||||
function abortedError() {
|
||||
const err = new Error('Message stream was closed before the message was generated');
|
||||
err.code = errors.ESTREAM;
|
||||
return err;
|
||||
}
|
||||
/**
|
||||
* Checks a domain against the DNS length limits, counted in code points per label and in
|
||||
* total. The label separators are the ones the bundled codec splits on.
|
||||
*
|
||||
* @param domain Domain to check
|
||||
* @return true if a label or the domain as a whole is too long to be a valid domain
|
||||
*/
|
||||
function exceedsDomainLimits(domain) {
|
||||
let total = 0;
|
||||
let label = 0;
|
||||
for (const char of domain) {
|
||||
total++;
|
||||
if (char === '.' || char === '。' || char === '.' || char === '。') {
|
||||
label = 0;
|
||||
}
|
||||
else if (++label > MAX_LABEL_LENGTH) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return total > MAX_DOMAIN_LENGTH;
|
||||
}
|
||||
/**
|
||||
* Encodes a domain the way browsers, the WHATWG URL Standard and DNS facing resolvers do,
|
||||
* which is with UTS-46 mapping applied before the Punycode step.
|
||||
@@ -90,13 +155,22 @@ const URL_PARSER_UNSAFE = /[/\\?#%\x00-\x20\x7F]/;
|
||||
*/
|
||||
function normalizeDomain(domain, toUnicode) {
|
||||
// domainToASCII and domainToUnicode landed in Node 7, the bundled codec covers Node 6
|
||||
const mapper = toUnicode ? node_url_1.default.domainToUnicode : node_url_1.default.domainToASCII;
|
||||
if (typeof mapper === 'function' && !URL_PARSER_UNSAFE.test(domain)) {
|
||||
const mapped = mapper(domain);
|
||||
if (typeof node_url_1.default.domainToASCII === 'function' &&
|
||||
typeof node_url_1.default.domainToUnicode === 'function' &&
|
||||
!URL_PARSER_UNSAFE.test(domain)) {
|
||||
// The U-label form is decoded from the A-label form rather than from the input:
|
||||
// Deno's domainToASCII returns an empty string for a label it cannot decode, while
|
||||
// its domainToUnicode returns the label with U+FFFD in place of the bad part
|
||||
const ascii = node_url_1.default.domainToASCII(domain);
|
||||
const mapped = ascii && toUnicode ? node_url_1.default.domainToUnicode(ascii) : ascii;
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
}
|
||||
if (exceedsDomainLimits(domain)) {
|
||||
// too long to be a domain whichever way it is encoded, keep it as supplied
|
||||
return domain;
|
||||
}
|
||||
return toUnicode ? punycode.toUnicode(domain) : punycode.toASCII(domain);
|
||||
}
|
||||
/**
|
||||
@@ -444,11 +518,7 @@ class MimeNode {
|
||||
if (typeof this.content.pipe === 'function') {
|
||||
// pre-stream handler. might be triggered if a stream is set as content
|
||||
// and 'error' fires before anything is done with this stream
|
||||
this._contentErrorHandler = err => {
|
||||
this.content.removeListener('error', this._contentErrorHandler);
|
||||
this.content = err;
|
||||
};
|
||||
this.content.once('error', this._contentErrorHandler);
|
||||
shared.recordStreamErrors(this.content);
|
||||
}
|
||||
else if (typeof this.content === 'string') {
|
||||
this._isPlainText = mimeFuncs.isPlainText(this.content);
|
||||
@@ -605,6 +675,17 @@ class MimeNode {
|
||||
/[\u0080-\uFFFF]/.test(this.content)) {
|
||||
structured.params.charset = 'utf-8';
|
||||
}
|
||||
if (this.filename) {
|
||||
// the name parameter is derived from the filename below, so a name that
|
||||
// arrived inside the content type is dropped first (a plain parameter or
|
||||
// rfc2231 continuation sections), otherwise the header carries two and a
|
||||
// receiver that reads the first sees a name the filename does not match
|
||||
for (const paramKey of Object.keys(structured.params)) {
|
||||
if (/^name(\*|$)/i.test(paramKey)) {
|
||||
delete structured.params[paramKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
value = mimeFuncs.buildHeaderValue(structured);
|
||||
if (this.filename) {
|
||||
// add support for non-compliant clients like QQ webmail
|
||||
@@ -660,40 +741,45 @@ class MimeNode {
|
||||
createReadStream(options) {
|
||||
options = options || {};
|
||||
const stream = new node_stream_1.PassThrough(options);
|
||||
let outputStream = stream;
|
||||
let transform;
|
||||
this.stream(stream, options, err => {
|
||||
if (err) {
|
||||
outputStream.emit('error', err);
|
||||
stream.destroy(err);
|
||||
return;
|
||||
}
|
||||
stream.end();
|
||||
});
|
||||
for (let i = 0, len = this._transforms.length; i < len; i++) {
|
||||
transform =
|
||||
typeof this._transforms[i] === 'function' ? this._transforms[i]() : this._transforms[i];
|
||||
outputStream.once('error', err => {
|
||||
transform.emit('error', err);
|
||||
});
|
||||
outputStream = outputStream.pipe(transform);
|
||||
// the content streams of the nodes the message did not get to are not going to be read
|
||||
stream.once('close', () => {
|
||||
if (!stream.writableFinished) {
|
||||
this._destroyContentStreams();
|
||||
}
|
||||
});
|
||||
// The stages are joined with pipeline, which destroys all of them when any one fails
|
||||
// or is destroyed. An error anywhere reaches the returned stream exactly once, and a
|
||||
// consumer that destroys the returned stream stops the tree from reading its sources
|
||||
const stages = [stream];
|
||||
for (const transform of this._transforms) {
|
||||
stages.push(typeof transform === 'function' ? transform() : transform);
|
||||
}
|
||||
// ensure terminating newline after possible user transforms
|
||||
transform = new last_newline_js_1.default();
|
||||
outputStream.once('error', err => {
|
||||
transform.emit('error', err);
|
||||
});
|
||||
outputStream = outputStream.pipe(transform);
|
||||
// dkim and stuff
|
||||
for (let i = 0, len = this._processFuncs.length; i < len; i++) {
|
||||
transform = this._processFuncs[i];
|
||||
outputStream = transform(outputStream);
|
||||
stages.push(new last_newline_js_1.default());
|
||||
let outputStream = (0, node_stream_1.pipeline)(stages, PIPELINE_NOOP);
|
||||
// dkim and stuff. A process function reads its input itself and reports the errors of
|
||||
// that input on its output, so only a consumer abort has to be carried back upstream
|
||||
for (const processFunc of this._processFuncs) {
|
||||
const input = outputStream;
|
||||
outputStream = processFunc(input);
|
||||
if (outputStream !== input) {
|
||||
(0, node_stream_1.finished)(outputStream, err => {
|
||||
if (err) {
|
||||
input.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if (this.newline) {
|
||||
const winbreak = ['win', 'windows', 'dos', '\r\n'].includes(this.newline.toString().toLowerCase());
|
||||
const newlineTransform = winbreak ? new le_windows_js_1.default() : new le_unix_js_1.default();
|
||||
const stream = outputStream.pipe(newlineTransform);
|
||||
outputStream.on('error', err => stream.emit('error', err));
|
||||
return stream;
|
||||
outputStream = (0, node_stream_1.pipeline)(outputStream, winbreak ? new le_windows_js_1.default() : new le_unix_js_1.default(), PIPELINE_NOOP);
|
||||
}
|
||||
return outputStream;
|
||||
}
|
||||
@@ -719,8 +805,10 @@ class MimeNode {
|
||||
}
|
||||
stream(outputStream, options, done) {
|
||||
const transferEncoding = this.getTransferEncoding();
|
||||
let contentStream;
|
||||
let localStream;
|
||||
// the streams this node is reading from. A consumer that goes away mid-message destroys
|
||||
// the output, and these are released with it instead of staying paused with a file or a
|
||||
// socket open behind them
|
||||
let activeStreams = [];
|
||||
// protect actual callback against multiple triggering
|
||||
let returned = false;
|
||||
const callback = (err) => {
|
||||
@@ -728,13 +816,39 @@ class MimeNode {
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
outputStream.removeListener('close', onOutputClose);
|
||||
done(err);
|
||||
};
|
||||
function onOutputClose() {
|
||||
for (const stream of activeStreams) {
|
||||
stream.destroy();
|
||||
}
|
||||
return callback(abortedError());
|
||||
}
|
||||
// reads the streams into the output. The listener is only attached while the node reads
|
||||
// something, a deeply nested tree would otherwise stack one per level on the output
|
||||
const readInto = (...streams) => {
|
||||
if (!activeStreams.length) {
|
||||
outputStream.once('close', onOutputClose);
|
||||
}
|
||||
activeStreams = streams;
|
||||
};
|
||||
// stops here if the output was destroyed in the meantime
|
||||
const aborted = () => {
|
||||
if (outputStream.destroyed) {
|
||||
callback(abortedError());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
// for multipart nodes, push child nodes
|
||||
// for content nodes end the stream
|
||||
const finalize = () => {
|
||||
let childId = 0;
|
||||
const processChildNode = () => {
|
||||
if (aborted()) {
|
||||
return;
|
||||
}
|
||||
if (childId >= this.childNodes.length) {
|
||||
outputStream.write('\r\n--' + this.boundary + '--\r\n');
|
||||
return callback();
|
||||
@@ -757,89 +871,80 @@ class MimeNode {
|
||||
};
|
||||
// pushes node content
|
||||
const sendContent = () => {
|
||||
if (this.content) {
|
||||
if (Object.prototype.toString.call(this.content) === '[object Error]') {
|
||||
// content is already errored
|
||||
return callback(this.content);
|
||||
}
|
||||
if (typeof this.content.pipe === 'function') {
|
||||
this.content.removeListener('error', this._contentErrorHandler);
|
||||
this._contentErrorHandler = err => callback(err);
|
||||
this.content.once('error', this._contentErrorHandler);
|
||||
}
|
||||
const createStream = () => {
|
||||
if (['quoted-printable', 'base64'].includes(transferEncoding)) {
|
||||
contentStream = new (transferEncoding === 'base64' ? base64 : qp).Encoder(options);
|
||||
contentStream.pipe(outputStream, {
|
||||
end: false
|
||||
});
|
||||
contentStream.once('end', finalize);
|
||||
contentStream.once('error', err => callback(err));
|
||||
localStream = this._getStream(this.content);
|
||||
localStream.pipe(contentStream);
|
||||
}
|
||||
else {
|
||||
// anything that is not QP or Base54 passes as-is
|
||||
localStream = this._getStream(this.content);
|
||||
localStream.pipe(outputStream, {
|
||||
end: false
|
||||
});
|
||||
localStream.once('end', finalize);
|
||||
}
|
||||
localStream.once('error', err => callback(err));
|
||||
};
|
||||
if (this.content._resolve) {
|
||||
const chunks = [];
|
||||
let chunklen = 0;
|
||||
let returned = false;
|
||||
const sourceStream = this._getStream(this.content);
|
||||
sourceStream.on('error', err => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
callback(err);
|
||||
});
|
||||
sourceStream.on('readable', () => {
|
||||
let chunk;
|
||||
while ((chunk = sourceStream.read()) !== null) {
|
||||
chunks.push(chunk);
|
||||
chunklen += chunk.length;
|
||||
}
|
||||
});
|
||||
sourceStream.on('end', () => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
this.content._resolve = false;
|
||||
this.content._resolvedValue = Buffer.concat(chunks, chunklen);
|
||||
setImmediate(createStream);
|
||||
});
|
||||
}
|
||||
else {
|
||||
setImmediate(createStream);
|
||||
}
|
||||
if (aborted()) {
|
||||
return;
|
||||
}
|
||||
return setImmediate(finalize);
|
||||
if (!this.content) {
|
||||
return setImmediate(finalize);
|
||||
}
|
||||
const createStream = () => {
|
||||
if (aborted()) {
|
||||
return;
|
||||
}
|
||||
const contentError = this._takeStreamContent(this.content);
|
||||
if (contentError) {
|
||||
return callback(contentError);
|
||||
}
|
||||
const localStream = this._getStream(this.content);
|
||||
if (['quoted-printable', 'base64'].includes(transferEncoding)) {
|
||||
const contentStream = transferEncoding === 'base64'
|
||||
? new base64.Encoder(options)
|
||||
: // outside of text a lone CR or LF is data, encoding it keeps it from being
|
||||
// turned into a line break by a newline transform or the receiving side
|
||||
new qp.Encoder(Object.assign({}, options, { binary: !/^text\//i.test(this.contentType || '') }));
|
||||
readInto(localStream, contentStream);
|
||||
contentStream.pipe(outputStream, {
|
||||
end: false
|
||||
});
|
||||
// pipeline reports errors and tears both streams down. It calls back once the
|
||||
// encoder took all of its input, the encoded output may still be waiting to
|
||||
// be read, so the node is only done once the encoder's readable side ended
|
||||
(0, node_stream_1.pipeline)(localStream, contentStream, err => err && callback(err));
|
||||
(0, node_stream_1.finished)(contentStream, { writable: false }, err => (err ? callback(err) : finalize()));
|
||||
}
|
||||
else {
|
||||
// anything that is not QP or Base54 passes as-is
|
||||
readInto(localStream);
|
||||
localStream.pipe(outputStream, {
|
||||
end: false
|
||||
});
|
||||
(0, node_stream_1.finished)(localStream, { writable: false }, err => (err ? callback(err) : finalize()));
|
||||
}
|
||||
};
|
||||
if (this.content._resolve) {
|
||||
const sourceStream = this._getStream(this.content);
|
||||
readInto(sourceStream);
|
||||
shared.resolveStream(sourceStream, (err, value) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
this.content._resolve = false;
|
||||
this.content._resolvedValue = value;
|
||||
setImmediate(createStream);
|
||||
});
|
||||
}
|
||||
else {
|
||||
setImmediate(createStream);
|
||||
}
|
||||
};
|
||||
if (this._raw) {
|
||||
setImmediate(() => {
|
||||
if (Object.prototype.toString.call(this._raw) === '[object Error]') {
|
||||
// content is already errored
|
||||
return callback(this._raw);
|
||||
if (aborted()) {
|
||||
return;
|
||||
}
|
||||
// remove default error handler (if set)
|
||||
if (typeof this._raw.pipe === 'function') {
|
||||
this._raw.removeListener('error', this._contentErrorHandler);
|
||||
const rawError = this._takeStreamContent(this._raw);
|
||||
if (rawError) {
|
||||
return callback(rawError);
|
||||
}
|
||||
const raw = this._getStream(this._raw);
|
||||
readInto(raw);
|
||||
raw.pipe(outputStream, {
|
||||
end: false
|
||||
});
|
||||
raw.on('error', err => outputStream.emit('error', err));
|
||||
raw.on('end', finalize);
|
||||
(0, node_stream_1.finished)(raw, { writable: false }, err => (err ? callback(err) : finalize()));
|
||||
});
|
||||
}
|
||||
else {
|
||||
@@ -955,11 +1060,7 @@ class MimeNode {
|
||||
if (this._raw && typeof this._raw.pipe === 'function') {
|
||||
// pre-stream handler. might be triggered if a stream is set as content
|
||||
// and 'error' fires before anything is done with this stream
|
||||
this._contentErrorHandler = err => {
|
||||
this._raw.removeListener('error', this._contentErrorHandler);
|
||||
this._raw = err;
|
||||
};
|
||||
this._raw.once('error', this._contentErrorHandler);
|
||||
shared.recordStreamErrors(this._raw);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
@@ -985,6 +1086,37 @@ class MimeNode {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Destroys the content streams of this node and of every node below it
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
_destroyContentStreams() {
|
||||
shared.destroyStream(this.content);
|
||||
shared.destroyStream(this._raw);
|
||||
for (const child of this.childNodes) {
|
||||
child._destroyContentStreams();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Checks that a content value can still be read before the node streams it. A stream is
|
||||
* refused once it errored, ended or was destroyed: piping it would either never finish or
|
||||
* produce an empty body without any error
|
||||
*
|
||||
* @param content Node content or raw value
|
||||
* @returns The error to stop with, or null when the content can be read
|
||||
* @internal
|
||||
*/
|
||||
_takeStreamContent(content) {
|
||||
if (Object.prototype.toString.call(content) === '[object Error]') {
|
||||
return content;
|
||||
}
|
||||
if (!content || typeof content.pipe !== 'function' || content._resolvedValue) {
|
||||
// the value of a resolved stream is read from the buffered copy
|
||||
return null;
|
||||
}
|
||||
return shared.unreadableStreamError(content);
|
||||
}
|
||||
/**
|
||||
* Detects and returns handle to a stream related with the content.
|
||||
*
|
||||
@@ -1163,12 +1295,12 @@ class MimeNode {
|
||||
_normalizeHeaderKey(key) {
|
||||
key = (key || '')
|
||||
.toString()
|
||||
// no newlines in keys
|
||||
.replace(/\r?\n|\r/g, ' ')
|
||||
// a field name is printable ascii without the colon, so a control char or DEL
|
||||
// can only be dropped, there is no quoting construct around a field name
|
||||
.replace(/[\x00-\x1f\x7f]/g, '')
|
||||
.trim()
|
||||
// a field name is printable ascii without the colon, so a control char, DEL,
|
||||
// whitespace or a colon can only be dropped, there is no quoting construct
|
||||
// around a field name. A colon or a space left in place ends the name early on
|
||||
// the wire: the key 'From: ceo@example.com (' with the value ')' was emitted as a
|
||||
// second From header that neither setHeader() nor the Bcc filter could match
|
||||
.replace(/[\s\x00-\x20\x7f:]/g, '')
|
||||
.toLowerCase()
|
||||
// use uppercase words, except MIME
|
||||
.replace(/^X-SMTPAPI$|^(MIME|DKIM|ARC|BIMI)\b|^[a-z]|-(SPF|FBL|ID|MD5)$|-[a-z]/gi, c => c.toUpperCase())
|
||||
@@ -1254,26 +1386,26 @@ class MimeNode {
|
||||
}
|
||||
return value;
|
||||
// space separated list of values enclosed in <>
|
||||
case 'References':
|
||||
value = [].concat
|
||||
.apply([], [].concat(value || '').map(elm => {
|
||||
elm = (elm || '')
|
||||
case 'References': {
|
||||
const ids = [];
|
||||
for (const entry of [].concat(value || '')) {
|
||||
const elm = (entry || '')
|
||||
.toString()
|
||||
.replace(/\r?\n|\r/g, ' ')
|
||||
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
|
||||
.trim();
|
||||
return elm.replace(/<[^>]*>/g, str => str.replace(/\s/g, '')).split(/\s+/);
|
||||
}))
|
||||
.map(elm => {
|
||||
if (elm.charAt(0) !== '<') {
|
||||
elm = '<' + elm;
|
||||
for (let id of stripWhitespaceInAngleBrackets(elm).split(/\s+/)) {
|
||||
if (id.charAt(0) !== '<') {
|
||||
id = '<' + id;
|
||||
}
|
||||
if (id.charAt(id.length - 1) !== '>') {
|
||||
id = id + '>';
|
||||
}
|
||||
ids.push(id);
|
||||
}
|
||||
if (elm.charAt(elm.length - 1) !== '>') {
|
||||
elm = elm + '>';
|
||||
}
|
||||
return elm;
|
||||
});
|
||||
return value.join(' ').trim();
|
||||
}
|
||||
return ids.join(' ').trim();
|
||||
}
|
||||
case 'Date':
|
||||
if (Object.prototype.toString.call(value) === '[object Date]') {
|
||||
return value.toUTCString().replace(/GMT/, '+0000');
|
||||
@@ -1294,6 +1426,7 @@ class MimeNode {
|
||||
*
|
||||
* @param addresses An array of address objects
|
||||
* @param [uniqueList] An array to be populated with addresses
|
||||
* @param [seenAddresses] Addresses already added to uniqueList, shared with recursive calls to keep deduplication linear
|
||||
* @return address string
|
||||
* @internal
|
||||
*/
|
||||
@@ -1351,7 +1484,10 @@ class MimeNode {
|
||||
// callers use an empty value to detect a missing address
|
||||
return address;
|
||||
}
|
||||
const lastAt = address.lastIndexOf('@');
|
||||
// dtext includes '@', so a domain literal is split off at the '@' in front of it and
|
||||
// not at the last one, which delivered 'a@[x@evil.com]' to evil.com
|
||||
const literal = address.charAt(address.length - 1) === ']' ? DOMAIN_LITERAL_TAIL.exec(address) : null;
|
||||
const lastAt = literal ? address.length - literal[1].length - 1 : address.lastIndexOf('@');
|
||||
if (lastAt < 0) {
|
||||
// Bare username, there is no domain to split off
|
||||
return this._normalizeLocalPart(address);
|
||||
@@ -1431,7 +1567,7 @@ class MimeNode {
|
||||
/**
|
||||
* If needed, mime encodes the name part
|
||||
*
|
||||
* @param name Name part of an address
|
||||
* @param value Name part of an address
|
||||
* @returns Mime word encoded string if needed
|
||||
* @internal
|
||||
*/
|
||||
|
||||
+38
-16
@@ -1,10 +1,16 @@
|
||||
import Mail from './mailer/index.js';
|
||||
import type { MailDefaults, SentMessageInfo, Transport, TransportOptions } from './mailer/index.js';
|
||||
import SMTPPool from './smtp-pool/index.js';
|
||||
import type { SMTPPoolOptions, SMTPPoolSentMessageInfo } from './smtp-pool/index.js';
|
||||
import SMTPTransport from './smtp-transport/index.js';
|
||||
import type { SMTPTransportOptions, SMTPSentMessageInfo } from './smtp-transport/index.js';
|
||||
import SendmailTransport from './sendmail-transport/index.js';
|
||||
import type { SendmailTransportOptions, SendmailSentMessageInfo } from './sendmail-transport/index.js';
|
||||
import StreamTransport from './stream-transport/index.js';
|
||||
import type { StreamTransportOptions, StreamSentMessageInfo } from './stream-transport/index.js';
|
||||
import type { JSONTransportOptions, JSONSentMessageInfo } from './json-transport/index.js';
|
||||
import JSONTransport from './json-transport/index.js';
|
||||
import type { JSONTransportOptions, JSONSentMessageInfo, JSONSentMessageObjectInfo } from './json-transport/index.js';
|
||||
import SESTransport from './ses-transport/index.js';
|
||||
import type { SESTransportOptions, SESSentMessageInfo } from './ses-transport/index.js';
|
||||
/**
|
||||
* Connection details of a service endpoint of an Ethereal test account
|
||||
@@ -43,32 +49,48 @@ export type TransportConfig = SMTPTransportOptions | SMTPPoolOptions | SendmailT
|
||||
* @param defaults Default message fields that are merged into every message
|
||||
* @returns Mail instance wrapping the transport
|
||||
*/
|
||||
export declare function createTransport(transporter: SMTPPoolOptions & {
|
||||
export declare function createTransport(transporter: SMTPPool | (SMTPPoolOptions & {
|
||||
pool: true;
|
||||
}, defaults?: MailDefaults): Mail<SMTPPoolSentMessageInfo>;
|
||||
export declare function createTransport(transporter: SendmailTransportOptions & {
|
||||
}), defaults?: MailDefaults): Mail<SMTPPoolSentMessageInfo, SMTPPoolOptions>;
|
||||
export declare function createTransport(transporter: SendmailTransport | (SendmailTransportOptions & {
|
||||
sendmail: true | string;
|
||||
}, defaults?: MailDefaults): Mail<SendmailSentMessageInfo>;
|
||||
export declare function createTransport(transporter: StreamTransportOptions & {
|
||||
}), defaults?: MailDefaults): Mail<SendmailSentMessageInfo, SendmailTransportOptions>;
|
||||
export declare function createTransport(transporter: StreamTransport | (StreamTransportOptions & {
|
||||
streamTransport: true;
|
||||
}, defaults?: MailDefaults): Mail<StreamSentMessageInfo>;
|
||||
}), defaults?: MailDefaults): Mail<StreamSentMessageInfo, StreamTransportOptions>;
|
||||
export declare function createTransport(transporter: JSONTransportOptions & {
|
||||
jsonTransport: true;
|
||||
}, defaults?: MailDefaults): Mail<JSONSentMessageInfo>;
|
||||
export declare function createTransport(transporter: SESTransportOptions & {
|
||||
skipEncoding: true;
|
||||
}, defaults?: MailDefaults): Mail<JSONSentMessageObjectInfo, JSONTransportOptions>;
|
||||
export declare function createTransport(transporter: JSONTransport | (JSONTransportOptions & {
|
||||
jsonTransport: true;
|
||||
}), defaults?: MailDefaults): Mail<JSONSentMessageInfo, JSONTransportOptions>;
|
||||
export declare function createTransport(transporter: SESTransport | (SESTransportOptions & {
|
||||
SES: object;
|
||||
}, defaults?: MailDefaults): Mail<SESSentMessageInfo>;
|
||||
export declare function createTransport<T = SentMessageInfo>(transporter: Transport<T>, defaults?: MailDefaults): Mail<T>;
|
||||
export declare function createTransport(transporter?: SMTPTransportOptions | string, defaults?: MailDefaults): Mail<SMTPSentMessageInfo>;
|
||||
export declare function createTransport(transporter?: TransportConfig | Transport<any> | string, defaults?: MailDefaults): Mail<any>;
|
||||
}), defaults?: MailDefaults): Mail<SESSentMessageInfo, SESTransportOptions>;
|
||||
export declare function createTransport(transporter: SMTPTransport, defaults?: MailDefaults): Mail<SMTPSentMessageInfo, SMTPTransportOptions>;
|
||||
export declare function createTransport<T = SentMessageInfo, D extends TransportOptions = TransportOptions>(transporter: Transport<T, D>, defaults?: MailDefaults): Mail<T, D>;
|
||||
export declare function createTransport(transporter?: SMTPTransportOptions | string, defaults?: MailDefaults): Mail<SMTPSentMessageInfo, SMTPTransportOptions>;
|
||||
export declare function createTransport(transporter?: TransportConfig | Transport<any, any> | string, defaults?: MailDefaults): Mail<any, any>;
|
||||
/**
|
||||
* Creates a test account from the Ethereal service (https://ethereal.email)
|
||||
*
|
||||
* @param apiUrl Optional API endpoint, defaults to https://api.nodemailer.com
|
||||
* @param callback Callback function to run with the account object. If not set, a Promise is returned
|
||||
* @param callback Callback function to run with the account object
|
||||
*/
|
||||
export declare function createTestAccount(callback: TestAccountCallback): void;
|
||||
/**
|
||||
* Creates a test account from the Ethereal service (https://ethereal.email)
|
||||
*
|
||||
* @param apiUrl API endpoint, defaults to https://api.nodemailer.com
|
||||
* @param callback Callback function to run with the account object
|
||||
*/
|
||||
export declare function createTestAccount(apiUrl: string | false | null | undefined, callback: TestAccountCallback): void;
|
||||
/**
|
||||
* Creates a test account from the Ethereal service (https://ethereal.email)
|
||||
*
|
||||
* @param [apiUrl] API endpoint, defaults to https://api.nodemailer.com
|
||||
* @returns Promise that resolves with the account object
|
||||
*/
|
||||
export declare function createTestAccount(apiUrl?: string | false | null): Promise<TestAccount>;
|
||||
/**
|
||||
* Resolves the Ethereal web URL for a message sent through an Ethereal test account
|
||||
@@ -98,5 +120,5 @@ export type { SMTPTransportOptions, SMTPSentMessageInfo };
|
||||
export type { SMTPPoolOptions, SMTPPoolSentMessageInfo };
|
||||
export type { SendmailTransportOptions, SendmailSentMessageInfo };
|
||||
export type { StreamTransportOptions, StreamSentMessageInfo };
|
||||
export type { JSONTransportOptions, JSONSentMessageInfo };
|
||||
export type { JSONTransportOptions, JSONSentMessageInfo, JSONSentMessageObjectInfo };
|
||||
export type { SESTransportOptions, SESSentMessageInfo };
|
||||
+13
-9
@@ -50,10 +50,12 @@ const index_js_7 = __importDefault(require("./ses-transport/index.js"));
|
||||
const errors = __importStar(require("./errors.js"));
|
||||
const index_js_8 = __importDefault(require("./fetch/index.js"));
|
||||
const packageData = __importStar(require("./package-info.js"));
|
||||
const ETHEREAL_API = (process.env.ETHEREAL_API || 'https://api.nodemailer.com').replace(/\/+$/, '');
|
||||
const ETHEREAL_WEB = (process.env.ETHEREAL_WEB || 'https://ethereal.email').replace(/\/+$/, '');
|
||||
const ETHEREAL_API_KEY = (process.env.ETHEREAL_API_KEY || '').replace(/\s*/g, '') || null;
|
||||
const ETHEREAL_CACHE = ['true', 'yes', 'y', '1'].includes((process.env.ETHEREAL_CACHE || 'yes').toString().trim().toLowerCase());
|
||||
// Read on use rather than at load, so that importing the module does not touch the
|
||||
// environment (Deno refuses that without --allow-env, even for apps that never use Ethereal)
|
||||
const etherealApi = () => (process.env.ETHEREAL_API || 'https://api.nodemailer.com').replace(/\/+$/, '');
|
||||
const etherealWeb = () => (process.env.ETHEREAL_WEB || 'https://ethereal.email').replace(/\/+$/, '');
|
||||
const etherealApiKey = () => (process.env.ETHEREAL_API_KEY || '').replace(/\s*/g, '') || null;
|
||||
const etherealCache = () => ['true', 'yes', 'y', '1'].includes((process.env.ETHEREAL_CACHE || 'yes').toString().trim().toLowerCase());
|
||||
let testAccount = false;
|
||||
function createTransport(transporter, defaults) {
|
||||
let options;
|
||||
@@ -114,11 +116,11 @@ function createTestAccount(apiUrl, callback) {
|
||||
});
|
||||
}
|
||||
const done = callback;
|
||||
if (ETHEREAL_CACHE && testAccount) {
|
||||
if (etherealCache() && testAccount) {
|
||||
setImmediate(() => done(null, testAccount));
|
||||
return promise;
|
||||
}
|
||||
apiUrl = apiUrl || ETHEREAL_API;
|
||||
apiUrl = apiUrl || etherealApi();
|
||||
const chunks = [];
|
||||
let chunklen = 0;
|
||||
const requestHeaders = {};
|
||||
@@ -126,8 +128,9 @@ function createTestAccount(apiUrl, callback) {
|
||||
requestor: packageData.name,
|
||||
version: packageData.version
|
||||
};
|
||||
if (ETHEREAL_API_KEY) {
|
||||
requestHeaders.Authorization = 'Bearer ' + ETHEREAL_API_KEY;
|
||||
const apiKey = etherealApiKey();
|
||||
if (apiKey) {
|
||||
requestHeaders.Authorization = 'Bearer ' + apiKey;
|
||||
}
|
||||
const fetchOptions = {
|
||||
contentType: 'application/json',
|
||||
@@ -195,7 +198,7 @@ function getTestMessageUrl(info) {
|
||||
}
|
||||
}
|
||||
if (infoProps.has('STATUS') && infoProps.has('MSGID')) {
|
||||
return ((testAccount && testAccount.web) || ETHEREAL_WEB) + '/message/' + infoProps.get('MSGID');
|
||||
return ((testAccount && testAccount.web) || etherealWeb()) + '/message/' + infoProps.get('MSGID');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -205,3 +208,4 @@ const nodemailer = {
|
||||
getTestMessageUrl
|
||||
};
|
||||
exports.default = nodemailer;
|
||||
Object.defineProperty(exports, 'default', { value: exports, enumerable: false, writable: true, configurable: true });
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
export declare const name = "nodemailer";
|
||||
export declare const version = "10.0.8";
|
||||
export declare const version = "10.1.7";
|
||||
export declare const homepage = "https://nodemailer.com/";
|
||||
+1
-1
@@ -3,5 +3,5 @@
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.homepage = exports.version = exports.name = void 0;
|
||||
exports.name = 'nodemailer';
|
||||
exports.version = '10.0.8';
|
||||
exports.version = '10.1.7';
|
||||
exports.homepage = 'https://nodemailer.com/';
|
||||
+1
@@ -178,6 +178,7 @@ const basicToDigit = function (codePoint) {
|
||||
* @see `basicToDigit()`
|
||||
* @private
|
||||
* @param digit The numeric value of a basic code point.
|
||||
* @param flag Non-zero to use the uppercase form of the code point.
|
||||
* @returns The basic code point whose value (when used for
|
||||
* representing integers) is `digit`, which needs to be in the range
|
||||
* `0` to `base - 1`. If `flag` is non-zero, the uppercase form is
|
||||
|
||||
+4
@@ -14,7 +14,11 @@ export declare function wrap(str: string, lineLength?: number): string;
|
||||
export interface QPEncoderOptions {
|
||||
/** Maximum length for lines, set to false to disable wrapping */
|
||||
lineLength?: number | false | undefined;
|
||||
/** The input is binary data: a CR or LF that is not part of a CRLF pair is encoded */
|
||||
binary?: boolean | undefined;
|
||||
}
|
||||
/** The name @types/nodemailer used for QPEncoderOptions */
|
||||
export type EncoderOptions = QPEncoderOptions;
|
||||
/**
|
||||
* Creates a transform stream for encoding data to Quoted-Printable encoding
|
||||
*
|
||||
|
||||
+113
-34
@@ -10,6 +10,8 @@ const node_stream_1 = require("node:stream");
|
||||
* @param buffer Buffer to convert
|
||||
* @returns Quoted-Printable encoded string
|
||||
*/
|
||||
// the shortest line wrap() can make progress with: a complete =XX sequence and the soft break
|
||||
const MIN_LINE_LENGTH = 4;
|
||||
// usable characters that do not need encoding
|
||||
// https://tools.ietf.org/html/rfc2045#section-6.7
|
||||
const QP_RANGES = [
|
||||
@@ -19,23 +21,52 @@ const QP_RANGES = [
|
||||
[0x20, 0x3c], // <SP>!"#$%&'()*+,-./0123456789:;
|
||||
[0x3e, 0x7e] // >?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}
|
||||
];
|
||||
// 1 for every byte value that is written as is, see QP_RANGES
|
||||
const QP_LITERAL = new Uint8Array(256);
|
||||
for (let i = 0; i < 256; i++) {
|
||||
QP_LITERAL[i] = checkRanges(i, QP_RANGES) ? 1 : 0;
|
||||
}
|
||||
const HEX_DIGITS = Buffer.from('0123456789ABCDEF', 'latin1');
|
||||
const isWhitespace = (c) => c === 0x20 || c === 0x09;
|
||||
function encode(buffer) {
|
||||
if (typeof buffer === 'string') {
|
||||
buffer = Buffer.from(buffer, 'utf-8');
|
||||
}
|
||||
let result = '';
|
||||
let ord;
|
||||
for (let i = 0, len = buffer.length; i < len; i++) {
|
||||
ord = buffer[i];
|
||||
return encodeBytes(typeof buffer === 'string' ? Buffer.from(buffer, 'utf-8') : buffer);
|
||||
}
|
||||
/**
|
||||
* Encodes bytes that may be followed by more input
|
||||
*
|
||||
* @param buffer Bytes to encode
|
||||
* @param [next] The byte that follows the buffer, whitespace before it is kept literal unless it
|
||||
* is a line break. Without it the buffer ends the input and its trailing whitespace is encoded
|
||||
* @param [binary] Keep only CRLF pairs literal. A lone CR or LF is data, not a line break, and
|
||||
* anything that rewrites line endings on the way would change it
|
||||
* @param [previous] The byte before the buffer, for a buffer that starts with LF
|
||||
* @returns Quoted-Printable encoded string
|
||||
*/
|
||||
function encodeBytes(buffer, next, binary, previous) {
|
||||
const len = buffer.length;
|
||||
// every byte takes three characters at most
|
||||
const output = Buffer.allocUnsafe(len * 3);
|
||||
let pos = 0;
|
||||
for (let i = 0; i < len; i++) {
|
||||
const ord = buffer[i];
|
||||
const following = i + 1 < len ? buffer[i + 1] : next;
|
||||
const lineBreakByte = binary && (ord === 0x0d || ord === 0x0a)
|
||||
? ord === 0x0d
|
||||
? following === 0x0a
|
||||
: (i > 0 ? buffer[i - 1] : previous) === 0x0d
|
||||
: true;
|
||||
// if the char is in allowed range, then keep as is, unless it is a WS in the end of a line
|
||||
if (checkRanges(ord, QP_RANGES) &&
|
||||
!((ord === 0x20 || ord === 0x09) && (i === len - 1 || buffer[i + 1] === 0x0a || buffer[i + 1] === 0x0d))) {
|
||||
result += String.fromCharCode(ord);
|
||||
if (QP_LITERAL[ord] &&
|
||||
lineBreakByte &&
|
||||
!(isWhitespace(ord) && (following === undefined || following === 0x0a || following === 0x0d))) {
|
||||
output[pos++] = ord;
|
||||
continue;
|
||||
}
|
||||
result += '=' + (ord < 0x10 ? '0' : '') + ord.toString(16).toUpperCase();
|
||||
output[pos++] = 0x3d; // =
|
||||
output[pos++] = HEX_DIGITS[Math.floor(ord / 16)];
|
||||
output[pos++] = HEX_DIGITS[ord % 16];
|
||||
}
|
||||
return result;
|
||||
return output.toString('latin1', 0, pos);
|
||||
}
|
||||
/**
|
||||
* Adds soft line breaks to a Quoted-Printable string
|
||||
@@ -46,7 +77,9 @@ function encode(buffer) {
|
||||
*/
|
||||
function wrap(str, lineLength) {
|
||||
str = (str || '').toString();
|
||||
lineLength = lineLength || 76;
|
||||
// a line has to hold a complete =XX sequence plus the soft break, shorter lengths
|
||||
// (or a negative one) would loop without consuming input
|
||||
lineLength = Math.max(Number(lineLength) || 76, MIN_LINE_LENGTH);
|
||||
if (str.length <= lineLength) {
|
||||
return str;
|
||||
}
|
||||
@@ -100,6 +133,11 @@ function wrap(str, lineLength) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!line.length) {
|
||||
// the trimming above emptied the line, which only happens for an incomplete
|
||||
// escape at the very end of the input. Take it as is rather than loop on it
|
||||
line = str.substr(pos, lineLength);
|
||||
}
|
||||
if (pos + line.length < len && line.substr(-1) !== '\n') {
|
||||
if (line.length === lineLength && line.match(/[=][\da-f]{2}$/i)) {
|
||||
line = line.substr(0, line.length - 3);
|
||||
@@ -139,6 +177,10 @@ function checkRanges(nr, ranges) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// Input is encoded and wrapped this many bytes at a time. wrap() works on strings, and a large
|
||||
// chunk handed over at once, such as a whole Buffer attachment, would be held several times over
|
||||
// as intermediate strings
|
||||
const ENCODE_SLICE_SIZE = 64 * 1024;
|
||||
/**
|
||||
* Creates a transform stream for encoding data to Quoted-Printable encoding
|
||||
*
|
||||
@@ -154,45 +196,82 @@ class Encoder extends node_stream_1.Transform {
|
||||
this.options.lineLength = this.options.lineLength || 76;
|
||||
}
|
||||
this._curLine = '';
|
||||
this._remainingBytes = false;
|
||||
this._lastByte = undefined;
|
||||
this.inputBytes = 0;
|
||||
this.outputBytes = 0;
|
||||
}
|
||||
/** @internal */
|
||||
_transform(chunk, encoding, done) {
|
||||
let qp;
|
||||
if (encoding !== 'buffer') {
|
||||
chunk = Buffer.from(chunk, encoding);
|
||||
}
|
||||
if (!chunk || !chunk.length) {
|
||||
let buf = encoding !== 'buffer' ? Buffer.from(chunk, encoding) : chunk;
|
||||
if (!buf || !buf.length) {
|
||||
return done();
|
||||
}
|
||||
this.inputBytes += chunk.length;
|
||||
if (this.options.lineLength) {
|
||||
qp = this._curLine + encode(chunk);
|
||||
qp = wrap(qp, this.options.lineLength);
|
||||
qp = qp.replace(/(^|\n)([^\n]*)$/, (match, lineBreak, lastLine) => {
|
||||
this._curLine = lastLine;
|
||||
return lineBreak;
|
||||
});
|
||||
if (qp) {
|
||||
this.outputBytes += qp.length;
|
||||
this.push(qp);
|
||||
}
|
||||
this.inputBytes += buf.length;
|
||||
if (this._remainingBytes) {
|
||||
buf = Buffer.concat([this._remainingBytes, buf], this._remainingBytes.length + buf.length);
|
||||
this._remainingBytes = false;
|
||||
}
|
||||
else {
|
||||
qp = encode(chunk);
|
||||
this.outputBytes += qp.length;
|
||||
this.push(qp, 'ascii');
|
||||
// Whitespace is encoded when it ends a line, and the end of the input counts as one. Hold
|
||||
// back the whitespace a chunk ends with until it is known what follows it, so the output
|
||||
// does not depend on where the input was split
|
||||
let end = buf.length;
|
||||
// a binary CR can only be written once it is known whether LF follows
|
||||
while (end > 0 && (isWhitespace(buf[end - 1]) || (this.options.binary && buf[end - 1] === 0x0d))) {
|
||||
end--;
|
||||
}
|
||||
if (buf.length - end <= ENCODE_SLICE_SIZE) {
|
||||
this._remainingBytes = end < buf.length ? Buffer.from(buf.subarray(end)) : false;
|
||||
buf = buf.subarray(0, end);
|
||||
}
|
||||
this._encodeSlices(buf);
|
||||
done();
|
||||
}
|
||||
/** @internal */
|
||||
_flush(done) {
|
||||
if (this._remainingBytes) {
|
||||
this._encodeSlices(this._remainingBytes);
|
||||
this._remainingBytes = false;
|
||||
}
|
||||
if (this._curLine) {
|
||||
this.outputBytes += this._curLine.length;
|
||||
this.push(this._curLine, 'ascii');
|
||||
this._curLine = '';
|
||||
}
|
||||
done();
|
||||
}
|
||||
/**
|
||||
* Encodes the input in slices of ENCODE_SLICE_SIZE bytes. Each slice is told the byte that
|
||||
* follows it, so a slice that ends in whitespace is encoded as if it was not split
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
_encodeSlices(buf) {
|
||||
for (let start = 0; start < buf.length; start += ENCODE_SLICE_SIZE) {
|
||||
const end = Math.min(start + ENCODE_SLICE_SIZE, buf.length);
|
||||
this._encodeSlice(buf.subarray(start, end), end < buf.length ? buf[end] : undefined);
|
||||
this._lastByte = buf[end - 1];
|
||||
}
|
||||
}
|
||||
/** @internal */
|
||||
_encodeSlice(buf, next) {
|
||||
let qp;
|
||||
if (this.options.lineLength) {
|
||||
qp = wrap(this._curLine + encodeBytes(buf, next, this.options.binary, this._lastByte), this.options.lineLength);
|
||||
// the last line is kept until it is known whether it needs a soft break
|
||||
const lastLF = qp.lastIndexOf('\n');
|
||||
this._curLine = qp.substring(lastLF + 1);
|
||||
qp = qp.substring(0, lastLF + 1);
|
||||
if (qp) {
|
||||
this.outputBytes += qp.length;
|
||||
this.push(qp, 'ascii');
|
||||
}
|
||||
}
|
||||
else {
|
||||
qp = encodeBytes(buf, next, this.options.binary, this._lastByte);
|
||||
this.outputBytes += qp.length;
|
||||
this.push(qp, 'ascii');
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.Encoder = Encoder;
|
||||
+2
@@ -64,4 +64,6 @@ declare namespace SendmailTransport {
|
||||
type MailOptions = SendMailOptions;
|
||||
type SentMessageInfo = SendmailSentMessageInfo;
|
||||
}
|
||||
/** The same aliases as module level exports, for `import * as SendmailTransport` and `import SendmailTransport = require()` */
|
||||
export type { SendmailTransportOptions as Options, SendMailOptions as MailOptions, SendmailSentMessageInfo as SentMessageInfo };
|
||||
export default SendmailTransport;
|
||||
+54
-14
@@ -37,11 +37,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const node_child_process_1 = require("node:child_process");
|
||||
const node_stream_1 = require("node:stream");
|
||||
const packageData = __importStar(require("../package-info.js"));
|
||||
const shared = __importStar(require("../shared/index.js"));
|
||||
const errors = __importStar(require("../errors.js"));
|
||||
const le_windows_js_1 = __importDefault(require("../mime-node/le-windows.js"));
|
||||
const le_unix_js_1 = __importDefault(require("../mime-node/le-unix.js"));
|
||||
// how long an stdin error waits for the exit code of the process before it is reported
|
||||
const STDIN_ERROR_EXIT_WAIT = 1000;
|
||||
/**
|
||||
* Generates a Transport object for Sendmail
|
||||
*
|
||||
@@ -139,37 +142,75 @@ class SendmailTransport {
|
||||
return callback(E);
|
||||
}
|
||||
if (sendmail) {
|
||||
let stream;
|
||||
// ended once the whole message was handed to stdin
|
||||
const messageWritten = () => !!stream && stream.readableEnded;
|
||||
// an EPIPE says less than the exit code that usually follows it, so it is only
|
||||
// reported when the process exits without one
|
||||
let stdinError = null;
|
||||
// releases whatever the message is still being read from
|
||||
const release = () => {
|
||||
if (stream && !messageWritten()) {
|
||||
stream.destroy();
|
||||
}
|
||||
};
|
||||
const fail = (err) => {
|
||||
release();
|
||||
callback(err);
|
||||
};
|
||||
sendmail.on('error', err => {
|
||||
this.logger.error({
|
||||
err,
|
||||
tnx: 'spawn',
|
||||
messageId
|
||||
}, 'Error occurred when sending message %s. %s', messageId, err.message);
|
||||
callback(err);
|
||||
fail(err);
|
||||
});
|
||||
sendmail.once('exit', code => {
|
||||
if (!code) {
|
||||
// 'close' follows 'exit' with the same arguments, it is only listened to in case the
|
||||
// process ends without an 'exit' event
|
||||
const onExit = (code, signal) => {
|
||||
let err = null;
|
||||
if (code) {
|
||||
err = new Error(code === 127 ? 'Sendmail command not found, process exited with code ' + code : 'Sendmail exited with code ' + code);
|
||||
}
|
||||
else if (signal) {
|
||||
err = new Error('Sendmail was terminated by ' + signal);
|
||||
}
|
||||
else if (!messageWritten()) {
|
||||
// exiting with 0 before the message was written does not mean it was queued
|
||||
err = new Error('Sendmail exited before the message was written');
|
||||
}
|
||||
else if (stdinError) {
|
||||
err = stdinError;
|
||||
}
|
||||
if (!err) {
|
||||
return callback();
|
||||
}
|
||||
const err = new Error(code === 127 ? 'Sendmail command not found, process exited with code ' + code : 'Sendmail exited with code ' + code);
|
||||
err.code = errors.ESENDMAIL;
|
||||
err.code = err.code || errors.ESENDMAIL;
|
||||
this.logger.error({
|
||||
err,
|
||||
tnx: 'stdin',
|
||||
messageId
|
||||
}, 'Error sending message %s to sendmail. %s', messageId, err.message);
|
||||
callback(err);
|
||||
});
|
||||
// the close listener is handed the exit code as its first argument, so a non-zero
|
||||
// code reaching it before the exit listener did counts as the error value
|
||||
sendmail.once('close', callback);
|
||||
fail(err);
|
||||
};
|
||||
sendmail.once('exit', onExit);
|
||||
sendmail.once('close', onExit);
|
||||
sendmail.stdin.on('error', err => {
|
||||
this.logger.error({
|
||||
err,
|
||||
tnx: 'stdin',
|
||||
messageId
|
||||
}, 'Error occurred when piping message %s to sendmail. %s', messageId, err.message);
|
||||
callback(err);
|
||||
if (stdinError) {
|
||||
return;
|
||||
}
|
||||
stdinError = err;
|
||||
release();
|
||||
// a process that closed its stdin normally exits right after, with a code that
|
||||
// tells more than the EPIPE. Report the EPIPE if it does not
|
||||
const exitTimer = setTimeout(() => fail(err), STDIN_ERROR_EXIT_WAIT);
|
||||
sendmail.once('exit', () => clearTimeout(exitTimer));
|
||||
});
|
||||
const recipients = [].concat(envelope.to || []);
|
||||
if (recipients.length > 3) {
|
||||
@@ -180,12 +221,11 @@ class SendmailTransport {
|
||||
messageId
|
||||
}, 'Sending message %s to <%s>', messageId, recipients.join(', '));
|
||||
const sourceStream = mail.message.createReadStream();
|
||||
let stream = sourceStream;
|
||||
stream = sourceStream;
|
||||
if (this.options.newline) {
|
||||
// apply the transport-level line ending transform; the message-level
|
||||
// `newline` option is handled by MimeNode in createReadStream()
|
||||
stream = sourceStream.pipe(this.winbreak ? new le_windows_js_1.default() : new le_unix_js_1.default());
|
||||
sourceStream.once('error', err => stream.emit('error', err));
|
||||
stream = (0, node_stream_1.pipeline)(sourceStream, this.winbreak ? new le_windows_js_1.default() : new le_unix_js_1.default(), () => false);
|
||||
}
|
||||
stream.once('error', err => {
|
||||
this.logger.error({
|
||||
|
||||
+39
-2
@@ -21,6 +21,32 @@ export interface SESTransportOptions extends TransportOptions {
|
||||
SendEmailCommand: new (input: any) => unknown;
|
||||
};
|
||||
}
|
||||
/** The SESv2Client shape the transport needs */
|
||||
export type SESTransportClient = SESTransportOptions['SES']['sesClient'];
|
||||
/** The SendEmailCommand constructor shape the transport needs */
|
||||
export type SESTransportSendEmailCommand = SESTransportOptions['SES']['SendEmailCommand'];
|
||||
/**
|
||||
* SendEmailCommand parameters, the shape of the `ses` message option that is merged into
|
||||
* the API call. Any further SendEmailCommandInput field is accepted as well
|
||||
*/
|
||||
export interface SESSendEmailRequest {
|
||||
FromEmailAddress?: string | undefined;
|
||||
Destination?: {
|
||||
ToAddresses?: string[] | undefined;
|
||||
CcAddresses?: string[] | undefined;
|
||||
BccAddresses?: string[] | undefined;
|
||||
} | undefined;
|
||||
ReplyToAddresses?: string[] | undefined;
|
||||
Content?: unknown;
|
||||
EmailTags?: Array<{
|
||||
Name?: string | undefined;
|
||||
Value?: string | undefined;
|
||||
}> | undefined;
|
||||
ConfigurationSetName?: string | undefined;
|
||||
ListManagementOptions?: unknown;
|
||||
FeedbackForwardingEmailAddress?: string | undefined;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
/**
|
||||
* The value the SES transport hands to the send callback
|
||||
*/
|
||||
@@ -47,7 +73,7 @@ declare class SESTransport extends EventEmitter {
|
||||
name: string;
|
||||
version: string;
|
||||
logger: Logger;
|
||||
constructor(options?: SESTransportOptions);
|
||||
constructor(options: SESTransportOptions);
|
||||
getRegion(cb: (err: Error | null, region?: string | false) => void): void;
|
||||
/**
|
||||
* Compiles a mailcomposer message and forwards it to SES
|
||||
@@ -59,9 +85,14 @@ declare class SESTransport extends EventEmitter {
|
||||
/**
|
||||
* Verifies SES configuration
|
||||
*
|
||||
* @param callback Callback function
|
||||
* @returns Promise that resolves to true if the configuration is usable
|
||||
*/
|
||||
verify(): Promise<true>;
|
||||
/**
|
||||
* Verifies SES configuration
|
||||
*
|
||||
* @param callback Callback function
|
||||
*/
|
||||
verify(callback: VerifyCallback): void;
|
||||
}
|
||||
/**
|
||||
@@ -71,5 +102,11 @@ declare namespace SESTransport {
|
||||
type Options = SESTransportOptions;
|
||||
type MailOptions = SendMailOptions;
|
||||
type SentMessageInfo = SESSentMessageInfo;
|
||||
type SESv2ClientLike = SESTransportClient;
|
||||
type SendEmailCommandConstructorLike = SESTransportSendEmailCommand;
|
||||
type SendEmailRequestLike = SESSendEmailRequest;
|
||||
type MailSesOptions = SESSendEmailRequest;
|
||||
}
|
||||
/** The same aliases as module level exports, for `import * as SESTransport` and `import SESTransport = require()` */
|
||||
export type { SESTransportOptions as Options, SendMailOptions as MailOptions, SESSentMessageInfo as SentMessageInfo, SESTransportClient as SESv2ClientLike, SESTransportSendEmailCommand as SendEmailCommandConstructorLike, SESSendEmailRequest as SendEmailRequestLike, SESSendEmailRequest as MailSesOptions };
|
||||
export default SESTransport;
|
||||
+75
-8
@@ -1,6 +1,7 @@
|
||||
import type { NodemailerError } from '../errors.js';
|
||||
import { isProtoKey, copyOwnKeys } from './objects.js';
|
||||
import os from 'node:os';
|
||||
import type { Readable } from 'node:stream';
|
||||
import { type Readable } from 'node:stream';
|
||||
import type { OutgoingHttpHeaders } from 'node:http';
|
||||
export { isProtoKey, copyOwnKeys };
|
||||
/**
|
||||
@@ -34,6 +35,8 @@ export interface ResolvedHostname {
|
||||
/** The resolver error when a cached value was used because of it */
|
||||
error?: Error | undefined;
|
||||
}
|
||||
/** The name @types/nodemailer used for ResolvedHostname */
|
||||
export type ResolveHostnameValue = ResolvedHostname;
|
||||
/**
|
||||
* Resolved addresses as stored in the DNS cache
|
||||
*/
|
||||
@@ -83,6 +86,8 @@ export interface LogEntry {
|
||||
[key: string]: any;
|
||||
}
|
||||
export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
||||
/** The name @types/nodemailer used for LogLevel */
|
||||
export type LoggerLevel = LogLevel;
|
||||
/**
|
||||
* A logger supplied by the caller, bunyan style. Any object works, a level it does not
|
||||
* implement is routed to one it does, see _logFunc
|
||||
@@ -104,16 +109,24 @@ export interface GetLoggerOptions {
|
||||
/** A bunyan compatible logger, true for the default console logger, false or unset for no logging */
|
||||
logger?: ExternalLogger | boolean | undefined;
|
||||
}
|
||||
/**
|
||||
* A bunyan style log method: a data object followed by a printf style message, or the
|
||||
* message alone
|
||||
*/
|
||||
export interface LogMethod {
|
||||
(data: LogEntry | undefined, message?: string, ...args: any[]): void;
|
||||
(message?: string, ...args: any[]): void;
|
||||
}
|
||||
/**
|
||||
* The bunyan compatible logger interface returned by getLogger
|
||||
*/
|
||||
export interface Logger {
|
||||
trace(data?: LogEntry, message?: string, ...args: any[]): void;
|
||||
debug(data?: LogEntry, message?: string, ...args: any[]): void;
|
||||
info(data?: LogEntry, message?: string, ...args: any[]): void;
|
||||
warn(data?: LogEntry, message?: string, ...args: any[]): void;
|
||||
error(data?: LogEntry, message?: string, ...args: any[]): void;
|
||||
fatal(data?: LogEntry, message?: string, ...args: any[]): void;
|
||||
trace: LogMethod;
|
||||
debug: LogMethod;
|
||||
info: LogMethod;
|
||||
warn: LogMethod;
|
||||
error: LogMethod;
|
||||
fatal: LogMethod;
|
||||
}
|
||||
/**
|
||||
* A parsed data URI
|
||||
@@ -175,6 +188,7 @@ export declare const parseConnectionUrl: (str?: string | null) => ConnectionUrlO
|
||||
* creates a default console logger
|
||||
*
|
||||
* @param [options] Options object that might include 'logger' value
|
||||
* @param [defaults] Fields merged into every log entry, overridden by the fields of the entry itself
|
||||
* @return bunyan compatible logger
|
||||
*/
|
||||
export declare const getLogger: (options?: GetLoggerOptions, defaults?: LogEntry) => Logger;
|
||||
@@ -198,18 +212,41 @@ export declare const parseDataURI: (uri: unknown) => ParsedDataURI | null;
|
||||
*
|
||||
* @param data An object or an Array you want to resolve an element for, see ContentDescriptor for the values it understands
|
||||
* @param key Property name or an Array index
|
||||
* @param [options] Optional access policy: { disableFileAccess, disableUrlAccess }
|
||||
* @param callback Callback function with (err, value)
|
||||
*/
|
||||
export declare function resolveContent(data: {
|
||||
[key: string]: any;
|
||||
}, key: string | number, callback: ResolveContentCallback): void;
|
||||
/**
|
||||
* Resolves a String or a Buffer value for content value
|
||||
*
|
||||
* @param data An object or an Array you want to resolve an element for, see ContentDescriptor for the values it understands
|
||||
* @param key Property name or an Array index
|
||||
* @param options Access policy: { disableFileAccess, disableUrlAccess }
|
||||
* @param callback Callback function with (err, value)
|
||||
*/
|
||||
export declare function resolveContent(data: {
|
||||
[key: string]: any;
|
||||
}, key: string | number, options: ResolveContentOptions | false | undefined, callback: ResolveContentCallback): void;
|
||||
/**
|
||||
* Resolves a String or a Buffer value for content value
|
||||
*
|
||||
* @param data An object or an Array you want to resolve an element for, see ContentDescriptor for the values it understands
|
||||
* @param key Property name or an Array index
|
||||
* @param [options] Optional access policy: { disableFileAccess, disableUrlAccess }
|
||||
* @returns Promise that resolves with the value
|
||||
*/
|
||||
export declare function resolveContent(data: {
|
||||
[key: string]: any;
|
||||
}, key: string | number, options?: ResolveContentOptions | false): Promise<any>;
|
||||
/**
|
||||
* Resolves a String or a Buffer value for content value
|
||||
*
|
||||
* @param data An object or an Array you want to resolve an element for, see ContentDescriptor for the values it understands
|
||||
* @param key Property name or an Array index
|
||||
* @param options Access policy: { disableFileAccess, disableUrlAccess }
|
||||
* @param callback Callback function with (err, value), a Promise is returned if not set
|
||||
*/
|
||||
export declare function resolveContent(data: {
|
||||
[key: string]: any;
|
||||
}, key: string | number, options: ResolveContentOptions | false | undefined, callback: ResolveContentCallback | undefined): Promise<any> | void;
|
||||
@@ -222,3 +259,33 @@ export declare const assign: (...args: ({
|
||||
[key: string]: any;
|
||||
};
|
||||
export declare const encodeXText: (str: string) => string;
|
||||
/**
|
||||
* Keeps the first error a content stream emits before it is read, so the error is reported
|
||||
* when the stream is read instead of being thrown as unhandled. The listener stays attached
|
||||
* for good, a stream that emits 'error' more than once never throws the later ones either
|
||||
*
|
||||
* @param stream Readable stream
|
||||
*/
|
||||
export declare function recordStreamErrors(stream: Readable): void;
|
||||
/**
|
||||
* Destroys a value if it is a readable stream that was not destroyed yet
|
||||
*
|
||||
* @param value Any content value
|
||||
*/
|
||||
export declare function destroyStream(value: unknown): void;
|
||||
/**
|
||||
* Tells why a stream can not be read from start to end anymore. A stream that ended or was
|
||||
* destroyed before anyone read it would never emit 'end' to a new reader, or, when piped, would
|
||||
* end the destination right away and turn into an empty value without any error
|
||||
*
|
||||
* @param stream Readable stream
|
||||
* @returns The error the stream failed with, an ESTREAM error, or null when it can be read
|
||||
*/
|
||||
export declare function unreadableStreamError(stream: Readable): NodemailerError | null;
|
||||
/**
|
||||
* Streams a stream value into a Buffer
|
||||
*
|
||||
* @param stream Readable stream
|
||||
* @param callback Callback function with (err, value)
|
||||
*/
|
||||
export declare function resolveStream(stream: Readable, callback: (err: Error | null, value?: Buffer) => void): void;
|
||||
+219
-119
@@ -37,8 +37,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.encodeXText = exports.assign = exports.parseDataURI = exports.callbackPromise = exports.getLogger = exports._logFunc = exports.parseConnectionUrl = exports.resolveHostname = exports.dnsCache = exports.networkInterfaces = exports._resetCacheCleanup = exports._lastCacheCleanup = exports.copyOwnKeys = exports.isProtoKey = void 0;
|
||||
exports.encodeXText = exports.assign = exports.parseDataURI = exports.callbackPromise = exports.getLogger = exports._logFunc = exports.parseConnectionUrl = exports.resolveHostname = exports.dnsCache = exports._readNetworkInterfaces = exports.networkInterfaces = exports._resetCacheCleanup = exports._lastCacheCleanup = exports.copyOwnKeys = exports.isProtoKey = void 0;
|
||||
exports.resolveContent = resolveContent;
|
||||
exports.recordStreamErrors = recordStreamErrors;
|
||||
exports.destroyStream = destroyStream;
|
||||
exports.unreadableStreamError = unreadableStreamError;
|
||||
exports.resolveStream = resolveStream;
|
||||
const urllib = __importStar(require("./url.js"));
|
||||
const node_util_1 = __importDefault(require("node:util"));
|
||||
const node_fs_1 = __importDefault(require("node:fs"));
|
||||
@@ -50,6 +54,7 @@ Object.defineProperty(exports, "copyOwnKeys", { enumerable: true, get: function
|
||||
const node_dns_1 = __importDefault(require("node:dns"));
|
||||
const node_net_1 = __importDefault(require("node:net"));
|
||||
const node_os_1 = __importDefault(require("node:os"));
|
||||
const node_stream_1 = require("node:stream");
|
||||
const DNS_TTL = 5 * 60 * 1000;
|
||||
const CACHE_CLEANUP_INTERVAL = 30 * 1000; // Minimum 30 seconds between cleanups
|
||||
const MAX_CACHE_SIZE = 1000; // Maximum number of entries in cache
|
||||
@@ -62,14 +67,25 @@ const _resetCacheCleanup = () => {
|
||||
lastCacheCleanup = 0;
|
||||
};
|
||||
exports._resetCacheCleanup = _resetCacheCleanup;
|
||||
try {
|
||||
exports.networkInterfaces = node_os_1.default.networkInterfaces();
|
||||
}
|
||||
catch (_err) {
|
||||
// fails on some systems
|
||||
}
|
||||
let networkInterfacesRead = false;
|
||||
// Read on first use rather than at load, so that importing the module does not ask for
|
||||
// the interface table (Deno prompts for --allow-sys on it)
|
||||
/** @internal */
|
||||
const _readNetworkInterfaces = () => {
|
||||
if (!networkInterfacesRead) {
|
||||
networkInterfacesRead = true;
|
||||
try {
|
||||
exports.networkInterfaces = node_os_1.default.networkInterfaces();
|
||||
}
|
||||
catch (_err) {
|
||||
// fails on some systems
|
||||
}
|
||||
}
|
||||
return exports.networkInterfaces;
|
||||
};
|
||||
exports._readNetworkInterfaces = _readNetworkInterfaces;
|
||||
const isFamilySupported = (family, allowInternal) => {
|
||||
const addresses = Object.values(exports.networkInterfaces || {}).flat();
|
||||
const addresses = Object.values((0, exports._readNetworkInterfaces)() || {}).flat();
|
||||
if (!addresses.length) {
|
||||
// hope for the best. Runtimes without an interface table (Cloudflare
|
||||
// Workers) report an empty object rather than throwing
|
||||
@@ -80,7 +96,8 @@ const isFamilySupported = (family, allowInternal) => {
|
||||
const resolve = (family, hostname, options, callback) => {
|
||||
options = options || {};
|
||||
if (!isFamilySupported(family, options.allowInternalNetworkInterfaces)) {
|
||||
return callback(null, []);
|
||||
callback(null, []);
|
||||
return null;
|
||||
}
|
||||
const dnsResolver = node_dns_1.default.Resolver ? new node_dns_1.default.Resolver(options) : node_dns_1.default;
|
||||
dnsResolver['resolve' + family](hostname, (err, addresses) => {
|
||||
@@ -99,6 +116,7 @@ const resolve = (family, hostname, options, callback) => {
|
||||
}
|
||||
return callback(null, Array.isArray(addresses) ? addresses : [].concat(addresses || []));
|
||||
});
|
||||
return node_dns_1.default.Resolver ? dnsResolver : null;
|
||||
};
|
||||
exports.dnsCache = new Map();
|
||||
const formatDNSValue = (value, extra) => {
|
||||
@@ -162,115 +180,139 @@ const resolveHostname = (options, callback) => {
|
||||
}));
|
||||
}
|
||||
}
|
||||
// Resolve both IPv4 and IPv6 addresses for fallback support
|
||||
// The timeout limits the lookup as a whole. It is also handed to the resolver, but there
|
||||
// it applies to every query attempt on its own, and the resolver retries a few times
|
||||
let responded = false;
|
||||
let deadline;
|
||||
const respond = (err, result) => {
|
||||
if (responded) {
|
||||
return;
|
||||
}
|
||||
responded = true;
|
||||
clearTimeout(deadline);
|
||||
callback(err, result);
|
||||
};
|
||||
// a stale cached value is still better than no value at all
|
||||
const respondCached = (error) => {
|
||||
if (!cached) {
|
||||
return false;
|
||||
}
|
||||
exports.dnsCache.set(host, {
|
||||
value: cached.value,
|
||||
expires: Date.now() + (options.dnsTtl || DNS_TTL)
|
||||
});
|
||||
respond(null, formatDNSValue(cached.value, {
|
||||
servername,
|
||||
cached: true,
|
||||
error
|
||||
}));
|
||||
return true;
|
||||
};
|
||||
const resolvers = [];
|
||||
const timeout = Number(options.timeout) || 0;
|
||||
if (timeout > 0) {
|
||||
deadline = setTimeout(() => {
|
||||
for (const resolver of resolvers) {
|
||||
if (typeof resolver.cancel === 'function') {
|
||||
resolver.cancel();
|
||||
}
|
||||
}
|
||||
const err = new Error('DNS lookup for ' + host + ' timed out');
|
||||
err.code = node_dns_1.default.TIMEOUT;
|
||||
if (!respondCached(err)) {
|
||||
respond(err);
|
||||
}
|
||||
}, timeout);
|
||||
}
|
||||
// Resolve both IPv4 and IPv6 addresses for fallback support, at the same time
|
||||
let ipv4Addresses = [];
|
||||
let ipv6Addresses = [];
|
||||
let ipv4Error = null;
|
||||
let ipv6Error = null;
|
||||
resolve(4, options.host, options, (err, addresses) => {
|
||||
if (err) {
|
||||
ipv4Error = err;
|
||||
let pending = 2;
|
||||
const onResolved = () => {
|
||||
if (--pending || responded) {
|
||||
return;
|
||||
}
|
||||
else {
|
||||
ipv4Addresses = addresses || [];
|
||||
// Combine addresses: IPv4 first, then IPv6
|
||||
const allAddresses = ipv4Addresses.concat(ipv6Addresses);
|
||||
if (allAddresses.length) {
|
||||
const value = {
|
||||
addresses: allAddresses
|
||||
};
|
||||
exports.dnsCache.set(host, {
|
||||
value,
|
||||
expires: Date.now() + (options.dnsTtl || DNS_TTL)
|
||||
});
|
||||
return respond(null, formatDNSValue(value, {
|
||||
servername,
|
||||
cached: false
|
||||
}));
|
||||
}
|
||||
resolve(6, host, options, (err, addresses) => {
|
||||
if (err) {
|
||||
ipv6Error = err;
|
||||
}
|
||||
else {
|
||||
ipv6Addresses = addresses || [];
|
||||
}
|
||||
// Combine addresses: IPv4 first, then IPv6
|
||||
const allAddresses = ipv4Addresses.concat(ipv6Addresses);
|
||||
if (allAddresses.length) {
|
||||
// No addresses from resolve4/resolve6, try dns.lookup as fallback
|
||||
if (ipv4Error && ipv6Error && respondCached(ipv4Error)) {
|
||||
// Both resolvers had errors
|
||||
return;
|
||||
}
|
||||
try {
|
||||
node_dns_1.default.lookup(host, { all: true }, (err, addresses) => {
|
||||
if (err) {
|
||||
if (respondCached(err)) {
|
||||
return;
|
||||
}
|
||||
return respond(err);
|
||||
}
|
||||
// Get all supported addresses from dns.lookup
|
||||
const supportedAddresses = addresses
|
||||
? addresses.filter(addr => isFamilySupported(addr.family)).map(addr => addr.address)
|
||||
: [];
|
||||
if (addresses && addresses.length && !supportedAddresses.length) {
|
||||
// there are addresses but none can be used
|
||||
console.warn(`Failed to resolve IPv${addresses[0].family} addresses with current network`);
|
||||
}
|
||||
if (!supportedAddresses.length && cached) {
|
||||
// nothing was found, fallback to cached value
|
||||
return respond(null, formatDNSValue(cached.value, {
|
||||
servername,
|
||||
cached: true
|
||||
}));
|
||||
}
|
||||
const value = {
|
||||
addresses: allAddresses
|
||||
addresses: supportedAddresses.length ? supportedAddresses : [host]
|
||||
};
|
||||
exports.dnsCache.set(host, {
|
||||
value,
|
||||
expires: Date.now() + (options.dnsTtl || DNS_TTL)
|
||||
});
|
||||
return callback(null, formatDNSValue(value, {
|
||||
return respond(null, formatDNSValue(value, {
|
||||
servername,
|
||||
cached: false
|
||||
}));
|
||||
});
|
||||
}
|
||||
catch (lookupErr) {
|
||||
if (respondCached(lookupErr)) {
|
||||
return;
|
||||
}
|
||||
// No addresses from resolve4/resolve6, try dns.lookup as fallback
|
||||
if (ipv4Error && ipv6Error) {
|
||||
// Both resolvers had errors
|
||||
if (cached) {
|
||||
exports.dnsCache.set(host, {
|
||||
value: cached.value,
|
||||
expires: Date.now() + (options.dnsTtl || DNS_TTL)
|
||||
});
|
||||
return callback(null, formatDNSValue(cached.value, {
|
||||
servername,
|
||||
cached: true,
|
||||
error: ipv4Error
|
||||
}));
|
||||
}
|
||||
return respond(ipv4Error || ipv6Error || lookupErr);
|
||||
}
|
||||
};
|
||||
for (const family of [4, 6]) {
|
||||
const resolver = resolve(family, host, options, (err, addresses) => {
|
||||
if (family === 4) {
|
||||
ipv4Error = err;
|
||||
ipv4Addresses = addresses || [];
|
||||
}
|
||||
try {
|
||||
node_dns_1.default.lookup(host, { all: true }, (err, addresses) => {
|
||||
if (err) {
|
||||
if (cached) {
|
||||
exports.dnsCache.set(host, {
|
||||
value: cached.value,
|
||||
expires: Date.now() + (options.dnsTtl || DNS_TTL)
|
||||
});
|
||||
return callback(null, formatDNSValue(cached.value, {
|
||||
servername,
|
||||
cached: true,
|
||||
error: err
|
||||
}));
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
// Get all supported addresses from dns.lookup
|
||||
const supportedAddresses = addresses
|
||||
? addresses.filter(addr => isFamilySupported(addr.family)).map(addr => addr.address)
|
||||
: [];
|
||||
if (addresses && addresses.length && !supportedAddresses.length) {
|
||||
// there are addresses but none can be used
|
||||
console.warn(`Failed to resolve IPv${addresses[0].family} addresses with current network`);
|
||||
}
|
||||
if (!supportedAddresses.length && cached) {
|
||||
// nothing was found, fallback to cached value
|
||||
return callback(null, formatDNSValue(cached.value, {
|
||||
servername,
|
||||
cached: true
|
||||
}));
|
||||
}
|
||||
const value = {
|
||||
addresses: supportedAddresses.length ? supportedAddresses : [host]
|
||||
};
|
||||
exports.dnsCache.set(host, {
|
||||
value,
|
||||
expires: Date.now() + (options.dnsTtl || DNS_TTL)
|
||||
});
|
||||
return callback(null, formatDNSValue(value, {
|
||||
servername,
|
||||
cached: false
|
||||
}));
|
||||
});
|
||||
}
|
||||
catch (lookupErr) {
|
||||
if (cached) {
|
||||
exports.dnsCache.set(host, {
|
||||
value: cached.value,
|
||||
expires: Date.now() + (options.dnsTtl || DNS_TTL)
|
||||
});
|
||||
return callback(null, formatDNSValue(cached.value, {
|
||||
servername,
|
||||
cached: true,
|
||||
error: lookupErr
|
||||
}));
|
||||
}
|
||||
return callback(ipv4Error || ipv6Error || lookupErr);
|
||||
else {
|
||||
ipv6Error = err;
|
||||
ipv6Addresses = addresses || [];
|
||||
}
|
||||
onResolved();
|
||||
});
|
||||
});
|
||||
if (resolver) {
|
||||
resolvers.push(resolver);
|
||||
}
|
||||
}
|
||||
};
|
||||
exports.resolveHostname = resolveHostname;
|
||||
/**
|
||||
@@ -362,6 +404,7 @@ exports._logFunc = _logFunc;
|
||||
* creates a default console logger
|
||||
*
|
||||
* @param [options] Options object that might include 'logger' value
|
||||
* @param [defaults] Fields merged into every log entry, overridden by the fields of the entry itself
|
||||
* @return bunyan compatible logger
|
||||
*/
|
||||
const getLogger = (options, defaults) => {
|
||||
@@ -377,8 +420,10 @@ const getLogger = (options, defaults) => {
|
||||
}
|
||||
const logger = options.logger === true ? createDefaultLogger(levels) : options.logger;
|
||||
levels.forEach(level => {
|
||||
response[level] = (data, message, ...args) => {
|
||||
(0, exports._logFunc)(logger, level, defaults, data, message, ...args);
|
||||
response[level] = (...args) => {
|
||||
// the bunyan forms: a data object first, or the message alone
|
||||
const data = typeof args[0] === 'string' ? undefined : args.shift();
|
||||
(0, exports._logFunc)(logger, level, defaults, data, ...args);
|
||||
};
|
||||
});
|
||||
return response;
|
||||
@@ -594,6 +639,53 @@ const encodeXText = (str) => {
|
||||
return result;
|
||||
};
|
||||
exports.encodeXText = encodeXText;
|
||||
// The first error a stream emitted, see recordStreamErrors
|
||||
const streamErrors = new WeakMap();
|
||||
/**
|
||||
* Keeps the first error a content stream emits before it is read, so the error is reported
|
||||
* when the stream is read instead of being thrown as unhandled. The listener stays attached
|
||||
* for good, a stream that emits 'error' more than once never throws the later ones either
|
||||
*
|
||||
* @param stream Readable stream
|
||||
*/
|
||||
function recordStreamErrors(stream) {
|
||||
stream.on('error', err => {
|
||||
if (!streamErrors.has(stream)) {
|
||||
streamErrors.set(stream, err);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Destroys a value if it is a readable stream that was not destroyed yet
|
||||
*
|
||||
* @param value Any content value
|
||||
*/
|
||||
function destroyStream(value) {
|
||||
const stream = value;
|
||||
if (stream && typeof stream.pipe === 'function' && typeof stream.destroy === 'function' && !stream.destroyed) {
|
||||
stream.destroy();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Tells why a stream can not be read from start to end anymore. A stream that ended or was
|
||||
* destroyed before anyone read it would never emit 'end' to a new reader, or, when piped, would
|
||||
* end the destination right away and turn into an empty value without any error
|
||||
*
|
||||
* @param stream Readable stream
|
||||
* @returns The error the stream failed with, an ESTREAM error, or null when it can be read
|
||||
*/
|
||||
function unreadableStreamError(stream) {
|
||||
const err = streamErrors.get(stream) || stream.errored;
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
if (stream.readableEnded || stream.destroyed) {
|
||||
const unreadable = new Error('Content stream was already read or destroyed');
|
||||
unreadable.code = errors.ESTREAM;
|
||||
return unreadable;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Streams a stream value into a Buffer
|
||||
*
|
||||
@@ -602,35 +694,43 @@ exports.encodeXText = encodeXText;
|
||||
*/
|
||||
function resolveStream(stream, callback) {
|
||||
let responded = false;
|
||||
const respond = (err, value) => {
|
||||
if (responded) {
|
||||
return;
|
||||
}
|
||||
responded = true;
|
||||
callback(err, value);
|
||||
};
|
||||
const unreadable = unreadableStreamError(stream);
|
||||
if (unreadable) {
|
||||
// absorbs a later 'error' from the stream, there is nobody left to report it to
|
||||
stream.on('error', () => false);
|
||||
setImmediate(() => respond(unreadable));
|
||||
return;
|
||||
}
|
||||
const chunks = [];
|
||||
let chunklen = 0;
|
||||
stream.on('error', err => {
|
||||
if (responded) {
|
||||
return;
|
||||
stream.on('data', (chunk) => {
|
||||
if (typeof chunk === 'string') {
|
||||
chunk = Buffer.from(chunk);
|
||||
}
|
||||
responded = true;
|
||||
callback(err);
|
||||
chunks.push(chunk);
|
||||
chunklen += chunk.length;
|
||||
});
|
||||
stream.on('readable', () => {
|
||||
let chunk;
|
||||
while ((chunk = stream.read()) !== null) {
|
||||
chunks.push(chunk);
|
||||
chunklen += chunk.length;
|
||||
// finished() also reports a stream that is destroyed before 'end', which would otherwise
|
||||
// leave the callback waiting forever
|
||||
(0, node_stream_1.finished)(stream, { writable: false }, err => {
|
||||
if (err) {
|
||||
return respond(err);
|
||||
}
|
||||
});
|
||||
stream.on('end', () => {
|
||||
if (responded) {
|
||||
return;
|
||||
}
|
||||
responded = true;
|
||||
let value;
|
||||
try {
|
||||
value = Buffer.concat(chunks, chunklen);
|
||||
}
|
||||
catch (E) {
|
||||
return callback(E);
|
||||
return respond(E);
|
||||
}
|
||||
callback(null, value);
|
||||
respond(null, value);
|
||||
});
|
||||
}
|
||||
/**
|
||||
|
||||
+22
-5
@@ -58,10 +58,27 @@ const punycode = __importStar(require("../punycode/index.js"));
|
||||
// Matches a "scheme:" not followed by "//" (and with something after it), used
|
||||
// to re-insert the authority separator the legacy parser did not require.
|
||||
const SLASHLESS_AUTHORITY = /^([a-zA-Z][a-zA-Z0-9+.-]*:)(?!\/\/)([\s\S]+)$/;
|
||||
// Leading and trailing C0 controls and spaces, which the WHATWG parser strips before it
|
||||
// looks at the input. Stripped up front so that the slash-less form is recognized in a
|
||||
// value read from a file with a trailing newline as well.
|
||||
const SURROUNDING_WHITESPACE = /^[\x00-\x20]+|[\x00-\x20]+$/g;
|
||||
/**
|
||||
* Drops the leading and trailing C0 controls and spaces, which the WHATWG parser strips
|
||||
* before it looks at the input. Stripped up front so that the slash-less form is recognized
|
||||
* in a value read from a file with a trailing newline as well. An index scan, the regex
|
||||
* /^[\x00-\x20]+|[\x00-\x20]+$/g retried a run of spaces inside the value from every
|
||||
* position in it
|
||||
*
|
||||
* @param input URL string
|
||||
* @returns The string without the surrounding controls and spaces
|
||||
*/
|
||||
function stripSurroundingWhitespace(input) {
|
||||
let start = 0;
|
||||
let end = input.length;
|
||||
while (start < end && input.charCodeAt(start) <= 0x20) {
|
||||
start++;
|
||||
}
|
||||
while (end > start && input.charCodeAt(end - 1) <= 0x20) {
|
||||
end--;
|
||||
}
|
||||
return input.slice(start, end);
|
||||
}
|
||||
// Leading characters legacy url.parse() skips before it reads the scheme
|
||||
const LEGACY_TRIM = /^[\x00-\x20\u00a0\ufeff]+/;
|
||||
// The authority of a "scheme://authority/..." or a scheme-relative "//authority/..."
|
||||
@@ -156,7 +173,7 @@ function normalizeHostname(raw, href) {
|
||||
return mapped;
|
||||
}
|
||||
const parse = (input, parseQueryString) => {
|
||||
input = (input || '').replace(SURROUNDING_WHITESPACE, '');
|
||||
input = stripSurroundingWhitespace(input || '');
|
||||
// Legacy url.parse() parses a "user:pass@host:port" authority that follows
|
||||
// the scheme even without the "//" separator, for schemes outside its
|
||||
// built-in slashed-protocol list (smtp:/smtps:/socks:/...). The WHATWG
|
||||
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
import { Transform, type TransformOptions } from 'node:stream';
|
||||
/**
|
||||
* Escapes dots in the beginning of lines. Ends the stream with <CR><LF>.<CR><LF>
|
||||
* Also makes sure that only <CR><LF> sequences are used for linebreaks
|
||||
* Also makes sure that only <CR><LF> sequences are used for linebreaks, bare CR and bare LF
|
||||
* are both turned into <CR><LF>
|
||||
*
|
||||
* @param options Stream options
|
||||
*/
|
||||
|
||||
+32
-24
@@ -1,9 +1,15 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const node_stream_1 = require("node:stream");
|
||||
// bytes inserted into the output, shared as they are only ever copied by Buffer.concat
|
||||
const INSERT_LF = Buffer.from('\n');
|
||||
const INSERT_LF_DOT = Buffer.from('\n.');
|
||||
const INSERT_CR = Buffer.from('\r');
|
||||
const INSERT_DOT = Buffer.from('.');
|
||||
/**
|
||||
* Escapes dots in the beginning of lines. Ends the stream with <CR><LF>.<CR><LF>
|
||||
* Also makes sure that only <CR><LF> sequences are used for linebreaks
|
||||
* Also makes sure that only <CR><LF> sequences are used for linebreaks, bare CR and bare LF
|
||||
* are both turned into <CR><LF>
|
||||
*
|
||||
* @param options Stream options
|
||||
*/
|
||||
@@ -32,33 +38,35 @@ class DataStream extends node_stream_1.Transform {
|
||||
}
|
||||
this.inByteCount += chunk.length;
|
||||
for (i = 0, len = chunk.length; i < len; i++) {
|
||||
if (chunk[i] === 0x2e) {
|
||||
// .
|
||||
if ((i && chunk[i - 1] === 0x0a) || (!i && (!this.lastByte || this.lastByte === 0x0a))) {
|
||||
buf = chunk.slice(lastPos, i + 1);
|
||||
chunks.push(buf);
|
||||
chunks.push(Buffer.from('.'));
|
||||
chunklen += buf.length + 1;
|
||||
lastPos = i + 1;
|
||||
}
|
||||
const byte = chunk[i];
|
||||
const prev = i ? chunk[i - 1] : this.lastByte;
|
||||
let insert = false;
|
||||
if (prev === 0x0d && byte !== 0x0a) {
|
||||
// a bare CR becomes CRLF. A receiver that treats a lone CR as a line end would
|
||||
// otherwise see "\r.\r" as the end of the data (SMTP smuggling), so a dot
|
||||
// following it is stuffed like at the start of any other line
|
||||
insert = byte === 0x2e ? INSERT_LF_DOT : INSERT_LF;
|
||||
}
|
||||
else if (chunk[i] === 0x0a) {
|
||||
// \n
|
||||
if ((i && chunk[i - 1] !== 0x0d) || (!i && this.lastByte !== 0x0d)) {
|
||||
if (i > lastPos) {
|
||||
buf = chunk.slice(lastPos, i);
|
||||
chunks.push(buf);
|
||||
chunklen += buf.length + 2;
|
||||
}
|
||||
else {
|
||||
chunklen += 2;
|
||||
}
|
||||
chunks.push(Buffer.from('\r\n'));
|
||||
lastPos = i + 1;
|
||||
else if (byte === 0x0a && prev !== 0x0d) {
|
||||
// a bare LF becomes CRLF
|
||||
insert = INSERT_CR;
|
||||
}
|
||||
else if (byte === 0x2e && (prev === 0x0a || prev === false)) {
|
||||
// a dot at the start of a line
|
||||
insert = INSERT_DOT;
|
||||
}
|
||||
if (insert) {
|
||||
if (i > lastPos) {
|
||||
buf = chunk.slice(lastPos, i);
|
||||
chunks.push(buf);
|
||||
chunklen += buf.length;
|
||||
}
|
||||
chunks.push(insert);
|
||||
chunklen += insert.length;
|
||||
lastPos = i;
|
||||
}
|
||||
}
|
||||
if (chunklen) {
|
||||
if (chunks.length) {
|
||||
// add last piece
|
||||
if (lastPos < chunk.length) {
|
||||
buf = chunk.slice(lastPos);
|
||||
|
||||
+16
-6
@@ -2,18 +2,20 @@
|
||||
* Minimal HTTP/S proxy client
|
||||
*/
|
||||
import net from 'node:net';
|
||||
import type { NodemailerError } from '../errors.js';
|
||||
import type { Callback } from '../errors.js';
|
||||
/**
|
||||
* TLS options for connecting to an HTTPS proxy
|
||||
*/
|
||||
export interface HttpProxyClientOptions {
|
||||
/** Set to false to accept a proxy certificate that fails validation (e.g. self-signed) */
|
||||
rejectUnauthorized?: boolean | undefined;
|
||||
/** Time in milliseconds the CONNECT handshake may take, defaults to httpProxyClient.timeout or 30 seconds */
|
||||
timeout?: number | undefined;
|
||||
}
|
||||
/**
|
||||
* Receives the proxied socket once the CONNECT handshake has succeeded, or the error that prevented it
|
||||
*/
|
||||
export type HttpProxyClientCallback = (err: NodemailerError | null, socket?: net.Socket) => void;
|
||||
export type HttpProxyClientCallback = Callback<net.Socket>;
|
||||
/**
|
||||
* Establishes proxied connection to destinationPort
|
||||
*
|
||||
@@ -21,16 +23,24 @@ export type HttpProxyClientCallback = (err: NodemailerError | null, socket?: net
|
||||
* socket.write("GET / HTTP/1.0\r\n\r\n");
|
||||
* });
|
||||
*
|
||||
* @param proxyUrl proxy configuration, etg "http://proxy.host:3128/"
|
||||
* @param proxyUrl proxy configuration, e.g. "http://proxy.host:3128/"
|
||||
* @param destinationPort Port to open in destination host
|
||||
* @param destinationHost Destination hostname
|
||||
* @param [tlsOptions] Optional TLS options for an HTTPS proxy (e.g. { rejectUnauthorized: false })
|
||||
* @param callback Callback to run with the rocket object once connection is established
|
||||
* @param callback Callback to run with the socket object once connection is established
|
||||
*/
|
||||
declare function httpProxyClient(proxyUrl: string, destinationPort: number | string, destinationHost: string, callback: HttpProxyClientCallback): void;
|
||||
/**
|
||||
* Establishes proxied connection to destinationPort through an HTTPS proxy
|
||||
*
|
||||
* @param proxyUrl proxy configuration, e.g. "https://proxy.host:3128/"
|
||||
* @param destinationPort Port to open in destination host
|
||||
* @param destinationHost Destination hostname
|
||||
* @param tlsOptions TLS options for the proxy connection (e.g. { rejectUnauthorized: false })
|
||||
* @param callback Callback to run with the socket object once connection is established
|
||||
*/
|
||||
declare function httpProxyClient(proxyUrl: string, destinationPort: number | string, destinationHost: string, tlsOptions: HttpProxyClientOptions | undefined, callback: HttpProxyClientCallback): void;
|
||||
/**
|
||||
* Socket timeout in milliseconds while the CONNECT handshake is in progress, defaults to 30 seconds.
|
||||
* Time in milliseconds the CONNECT handshake may take when the call does not set one, defaults to 30 seconds.
|
||||
* Settable on the function itself, the same way the CommonJS module exposed it.
|
||||
*/
|
||||
declare namespace httpProxyClient {
|
||||
|
||||
+92
-60
@@ -46,12 +46,16 @@ const errors = __importStar(require("../errors.js"));
|
||||
// Cap the CONNECT response we buffer before the header terminator, so a proxy that
|
||||
// never sends \r\n\r\n cannot grow memory unboundedly before the socket times out.
|
||||
const MAX_RESPONSE_HEADER_BYTES = 64 * 1024;
|
||||
// URL hostnames keep the brackets around an IPv6 literal, socket options and net.isIPv6 take it without
|
||||
const unbracket = (host) => typeof host === 'string' && host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host;
|
||||
function httpProxyClient(proxyUrl, destinationPort, destinationHost, tlsOptions, callback) {
|
||||
if (typeof tlsOptions === 'function') {
|
||||
callback = tlsOptions;
|
||||
tlsOptions = {};
|
||||
}
|
||||
tlsOptions = tlsOptions || {};
|
||||
// the error paths hand over the error alone
|
||||
const done = callback;
|
||||
// Reject CRLF in the destination before it reaches the CONNECT request line
|
||||
// and Host header. A tainted host/port could otherwise inject additional
|
||||
// request headers into the proxy connection (HTTP request splitting).
|
||||
@@ -59,10 +63,12 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, tlsOptions,
|
||||
if (!destinationPort || /[\r\n]/.test(destinationHost)) {
|
||||
const err = new Error('Invalid proxy destination');
|
||||
err.code = errors.EPROXY;
|
||||
setImmediate(() => callback(err));
|
||||
setImmediate(() => done(err));
|
||||
return;
|
||||
}
|
||||
const proxy = urllib.parse(proxyUrl);
|
||||
// the CONNECT request line and the Host header take an IPv6 destination in brackets
|
||||
const authority = (node_net_1.default.isIPv6(unbracket(destinationHost)) ? '[' + unbracket(destinationHost) + ']' : destinationHost) + ':' + destinationPort;
|
||||
const connectOptions = {
|
||||
host: proxy.hostname,
|
||||
port: Number(proxy.port) ? Number(proxy.port) : proxy.protocol === 'https:' ? 443 : 80
|
||||
@@ -78,34 +84,100 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, tlsOptions,
|
||||
else {
|
||||
connect = node_net_1.default.connect.bind(node_net_1.default);
|
||||
}
|
||||
// The handshake is bounded as a whole, a proxy that keeps sending a byte now and then can
|
||||
// not hold the connection open past it
|
||||
const timeout = Number(tlsOptions.timeout) || httpProxyClient.timeout || 30 * 1000;
|
||||
let socket;
|
||||
// Error harness for initial connection. Once connection is established, the responsibility
|
||||
// to handle errors is passed to whoever uses this socket
|
||||
// Single settlement path for the handshake: every temporary listener and the timer are
|
||||
// dropped exactly once. Once the tunnel is up, the responsibility to handle errors is passed
|
||||
// to whoever uses this socket
|
||||
let finished = false;
|
||||
const tempSocketErr = (err) => {
|
||||
let timer;
|
||||
const cleanup = () => {
|
||||
clearTimeout(timer);
|
||||
socket.removeListener('data', onSocketData);
|
||||
socket.removeListener('error', fail);
|
||||
socket.removeListener('close', onEarlyClose);
|
||||
};
|
||||
function fail(err) {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
cleanup();
|
||||
try {
|
||||
socket.destroy();
|
||||
}
|
||||
catch (_E) {
|
||||
// ignore
|
||||
}
|
||||
callback(err);
|
||||
};
|
||||
const timeoutErr = () => {
|
||||
const err = new Error('Proxy socket timed out');
|
||||
err.code = 'ETIMEDOUT';
|
||||
tempSocketErr(err);
|
||||
};
|
||||
done(err);
|
||||
}
|
||||
function onEarlyClose() {
|
||||
const err = new Error('Proxy closed the connection before the tunnel was established');
|
||||
err.code = errors.EPROXY;
|
||||
fail(err);
|
||||
}
|
||||
// The response is collected as chunks and only the bytes that just arrived, together
|
||||
// with the three before them, are searched for the end of the headers. Appending to a
|
||||
// string and searching all of it again re-read the whole response on every chunk.
|
||||
const chunks = [];
|
||||
let received = 0;
|
||||
let tail = '';
|
||||
function onSocketData(chunk) {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
const window = tail + chunk.toString('binary');
|
||||
const windowEnd = window.indexOf('\r\n\r\n');
|
||||
chunks.push(chunk);
|
||||
received += chunk.length;
|
||||
tail = window.slice(-3);
|
||||
if (windowEnd < 0) {
|
||||
if (received > MAX_RESPONSE_HEADER_BYTES) {
|
||||
const err = new Error('Proxy response headers too large');
|
||||
err.code = errors.EPROXY;
|
||||
fail(err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Stop reading before anything is put back. A socket that keeps flowing would emit the
|
||||
// bytes after the headers, a greeting the proxy sent together with its own response,
|
||||
// before the next owner of the socket has a listener for them
|
||||
socket.removeListener('data', onSocketData);
|
||||
socket.pause();
|
||||
const headerEnd = received - window.length + windowEnd;
|
||||
const response = Buffer.concat(chunks, received);
|
||||
if (response.length > headerEnd + 4) {
|
||||
socket.unshift(response.subarray(headerEnd + 4));
|
||||
}
|
||||
// check response code
|
||||
const match = response.toString('binary', 0, headerEnd).match(/^HTTP\/\d+\.\d+ (\d+)/i);
|
||||
if (!match || (match[1] || '').charAt(0) !== '2') {
|
||||
const err = new Error('Invalid response from proxy' + ((match && ': ' + match[1]) || ''));
|
||||
err.code = errors.EPROXY;
|
||||
return fail(err);
|
||||
}
|
||||
// proxy connection is now established
|
||||
finished = true;
|
||||
cleanup();
|
||||
// A fresh socket starts flowing once something listens for 'data', a paused one would
|
||||
// not. Keep that behaviour for the next owner of the socket
|
||||
const resumeOnData = (event) => {
|
||||
if (event === 'data') {
|
||||
socket.removeListener('newListener', resumeOnData);
|
||||
socket.resume();
|
||||
}
|
||||
};
|
||||
socket.on('newListener', resumeOnData);
|
||||
return done(null, socket);
|
||||
}
|
||||
socket = connect(connectOptions, () => {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
const reqHeaders = {
|
||||
Host: destinationHost + ':' + destinationPort,
|
||||
Host: authority,
|
||||
Connection: 'close'
|
||||
};
|
||||
if (proxy.auth) {
|
||||
@@ -114,9 +186,7 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, tlsOptions,
|
||||
socket.write(
|
||||
// HTTP method
|
||||
'CONNECT ' +
|
||||
destinationHost +
|
||||
':' +
|
||||
destinationPort +
|
||||
authority +
|
||||
' HTTP/1.1\r\n' +
|
||||
// HTTP request headers
|
||||
Object.keys(reqHeaders)
|
||||
@@ -124,53 +194,15 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, tlsOptions,
|
||||
.join('\r\n') +
|
||||
// End request
|
||||
'\r\n\r\n');
|
||||
let headers = '';
|
||||
const onSocketData = (chunk) => {
|
||||
let match;
|
||||
let remainder;
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
headers += chunk.toString('binary');
|
||||
if ((match = headers.match(/\r\n\r\n/))) {
|
||||
socket.removeListener('data', onSocketData);
|
||||
remainder = headers.substr(match.index + match[0].length);
|
||||
headers = headers.substr(0, match.index);
|
||||
if (remainder) {
|
||||
socket.unshift(Buffer.from(remainder, 'binary'));
|
||||
}
|
||||
// proxy connection is now established
|
||||
finished = true;
|
||||
// check response code
|
||||
match = headers.match(/^HTTP\/\d+\.\d+ (\d+)/i);
|
||||
if (!match || (match[1] || '').charAt(0) !== '2') {
|
||||
try {
|
||||
socket.destroy();
|
||||
}
|
||||
catch (_E) {
|
||||
// ignore
|
||||
}
|
||||
const err = new Error('Invalid response from proxy' + ((match && ': ' + match[1]) || ''));
|
||||
err.code = errors.EPROXY;
|
||||
return callback(err);
|
||||
}
|
||||
socket.removeListener('error', tempSocketErr);
|
||||
socket.removeListener('timeout', timeoutErr);
|
||||
socket.setTimeout(0);
|
||||
return callback(null, socket);
|
||||
}
|
||||
if (headers.length > MAX_RESPONSE_HEADER_BYTES) {
|
||||
socket.removeListener('data', onSocketData);
|
||||
const err = new Error('Proxy response headers too large');
|
||||
err.code = errors.EPROXY;
|
||||
return tempSocketErr(err);
|
||||
}
|
||||
};
|
||||
socket.on('data', onSocketData);
|
||||
});
|
||||
socket.setTimeout(httpProxyClient.timeout || 30 * 1000);
|
||||
socket.on('timeout', timeoutErr);
|
||||
socket.once('error', tempSocketErr);
|
||||
timer = setTimeout(() => {
|
||||
const err = new Error('Proxy socket timed out');
|
||||
err.code = errors.ETIMEDOUT;
|
||||
fail(err);
|
||||
}, timeout);
|
||||
socket.once('error', fail);
|
||||
socket.once('close', onEarlyClose);
|
||||
}
|
||||
exports.default = httpProxyClient;
|
||||
module.exports = exports.default;
|
||||
|
||||
+72
-28
@@ -3,8 +3,9 @@ import net from 'node:net';
|
||||
import tls from 'node:tls';
|
||||
import { type Readable } from 'node:stream';
|
||||
import * as shared from '../shared/index.js';
|
||||
import type { NodemailerError } from '../errors.js';
|
||||
import { type Callback, type NodemailerError } from '../errors.js';
|
||||
import type XOAuth2 from '../xoauth2/index.js';
|
||||
import type { XOAuth2Options } from '../xoauth2/index.js';
|
||||
/**
|
||||
* Custom authentication handlers keyed by (case insensitive) SASL method name
|
||||
*/
|
||||
@@ -25,11 +26,11 @@ export interface SMTPConnectionOptions {
|
||||
secured?: boolean | undefined;
|
||||
/** Server name for SNI, defaults to host when that is not an IP address */
|
||||
servername?: string | undefined;
|
||||
/** Ignore STARTTLS even when the server advertises it */
|
||||
/** Ignore STARTTLS even when the server advertises it, has no effect when requireTLS is set */
|
||||
ignoreTLS?: boolean | undefined;
|
||||
/** Force STARTTLS, fail when the server does not support it */
|
||||
/** Force STARTTLS, fail when the server does not support it. Takes precedence over ignoreTLS and opportunisticTLS */
|
||||
requireTLS?: boolean | undefined;
|
||||
/** Continue unencrypted when the STARTTLS upgrade fails */
|
||||
/** Continue unencrypted when the STARTTLS upgrade fails, has no effect when requireTLS is set */
|
||||
opportunisticTLS?: boolean | undefined;
|
||||
/** Name of the client server, sent with EHLO/HELO, CRLF is stripped */
|
||||
name?: string | undefined;
|
||||
@@ -41,6 +42,8 @@ export interface SMTPConnectionOptions {
|
||||
greetingTimeout?: number | undefined;
|
||||
/** Time of inactivity in ms until the connection is closed, defaults to 10 minutes */
|
||||
socketTimeout?: number | undefined;
|
||||
/** Largest single server response to accept in bytes, defaults to 1 MB */
|
||||
maxResponseSize?: number | undefined;
|
||||
/** Time to wait in ms for the DNS requests to be resolved, defaults to 30 seconds */
|
||||
dnsTimeout?: number | undefined;
|
||||
/** Use LMTP instead of SMTP */
|
||||
@@ -115,12 +118,23 @@ export interface SMTPConnectionCustomAuthResponse {
|
||||
* Callback for a command sent by a custom authentication handler
|
||||
*/
|
||||
export type SMTPConnectionCustomAuthCommandCallback = (err: Error | null, data: SMTPConnectionCustomAuthResponse) => void;
|
||||
/**
|
||||
* The auth object as a custom authentication handler sees it: the object handed to login()
|
||||
* with the credentials filled in from its user and pass values
|
||||
*/
|
||||
export interface SMTPConnectionCustomAuthData extends SMTPConnectionAuth {
|
||||
/** The user and pass values of the auth object, the way @types/nodemailer declared them */
|
||||
credentials: SMTPConnectionCredentials & {
|
||||
user: string;
|
||||
pass: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* The object a custom authentication handler is run with
|
||||
*/
|
||||
export interface SMTPConnectionCustomAuthContext {
|
||||
/** The auth object handed to login() */
|
||||
auth: SMTPConnectionAuth;
|
||||
/** The auth object handed to login(), with the credentials filled in */
|
||||
auth: SMTPConnectionCustomAuthData;
|
||||
/** Selected authentication method name */
|
||||
method: string;
|
||||
/** SMTP extensions the server advertised */
|
||||
@@ -129,17 +143,20 @@ export interface SMTPConnectionCustomAuthContext {
|
||||
authMethods: string[];
|
||||
/** Maximum message size the server accepts, false when not advertised */
|
||||
maxAllowedSize: number | false;
|
||||
/** Sends a command to the server. Returns a promise when no callback is given */
|
||||
sendCommand(cmd: string, done?: SMTPConnectionCustomAuthCommandCallback): Promise<SMTPConnectionCustomAuthResponse> | undefined;
|
||||
/** Sends a command to the server and resolves with the parsed reply */
|
||||
sendCommand(cmd: string): Promise<SMTPConnectionCustomAuthResponse>;
|
||||
/** Sends a command to the server and hands the parsed reply to the callback */
|
||||
sendCommand(cmd: string, done: SMTPConnectionCustomAuthCommandCallback): void;
|
||||
/** Marks the user as authenticated */
|
||||
resolve(): void;
|
||||
/** Fails the authentication with an error */
|
||||
reject(err: Error | string): void;
|
||||
}
|
||||
/**
|
||||
* A custom authentication handler. Calls resolve() or reject() on the context, or returns a promise
|
||||
* A custom authentication handler. Calls resolve() or reject() on the context, or returns a
|
||||
* promise that settles the authentication. Any other return value is ignored
|
||||
*/
|
||||
export type SMTPConnectionCustomAuthHandler = (ctx: SMTPConnectionCustomAuthContext) => void | Promise<unknown>;
|
||||
export type SMTPConnectionCustomAuthHandler = (ctx: SMTPConnectionCustomAuthContext) => unknown;
|
||||
/**
|
||||
* An envelope address, either a plain string or an object with an address property
|
||||
*/
|
||||
@@ -166,12 +183,16 @@ export interface SMTPEnvelopeDsn {
|
||||
/** Alias of recipient, in the 'rfc822;address' form */
|
||||
orcpt?: string | null | undefined;
|
||||
}
|
||||
/**
|
||||
* A single DSN notify value
|
||||
*/
|
||||
export type SMTPEnvelopeDsnNotify = 'NEVER' | 'SUCCESS' | 'FAILURE' | 'DELAY';
|
||||
/**
|
||||
* Envelope object accepted by send()
|
||||
*/
|
||||
export interface SMTPEnvelope {
|
||||
/** Sender address */
|
||||
from?: string | SMTPEnvelopeAddress | undefined;
|
||||
/** Sender address, false for the null sender of a bounce message (MAIL FROM:<>) */
|
||||
from?: string | SMTPEnvelopeAddress | false | undefined;
|
||||
/** Recipient address or addresses */
|
||||
to?: string | SMTPEnvelopeAddress | Array<string | SMTPEnvelopeAddress> | undefined;
|
||||
/** Message size in bytes, sent as the SIZE parameter when the server supports it */
|
||||
@@ -200,9 +221,9 @@ export interface SMTPConnectionEnvelope extends SMTPEnvelope {
|
||||
accepted: string[];
|
||||
}
|
||||
/**
|
||||
* Result of a sent message
|
||||
* The recipient bookkeeping of a sent message, known once the envelope is accepted
|
||||
*/
|
||||
export interface SMTPConnectionSendInfo {
|
||||
export interface SMTPConnectionEnvelopeInfo {
|
||||
/** Recipients the server accepted */
|
||||
accepted: string[];
|
||||
/** Recipients the server rejected */
|
||||
@@ -211,19 +232,25 @@ export interface SMTPConnectionSendInfo {
|
||||
ehlo?: string[] | undefined;
|
||||
/** Errors for the rejected recipients */
|
||||
rejectedErrors?: NodemailerError[] | undefined;
|
||||
/** Time in ms spent on the envelope commands */
|
||||
envelopeTime?: number | undefined;
|
||||
/** Time in ms spent on streaming the message */
|
||||
messageTime?: number | undefined;
|
||||
/** Size of the encoded message in bytes */
|
||||
messageSize?: number | undefined;
|
||||
/** Final server response for the message */
|
||||
response?: string | undefined;
|
||||
}
|
||||
/**
|
||||
* Callback for send()
|
||||
* Result of a sent message
|
||||
*/
|
||||
export type SMTPConnectionSendCallback = (err: NodemailerError | null, info?: SMTPConnectionSendInfo) => void;
|
||||
export interface SMTPConnectionSendInfo extends SMTPConnectionEnvelopeInfo {
|
||||
/** Time in ms spent on the envelope commands */
|
||||
envelopeTime: number;
|
||||
/** Time in ms spent on streaming the message */
|
||||
messageTime: number;
|
||||
/** Size of the encoded message in bytes */
|
||||
messageSize: number;
|
||||
/** Final server response for the message */
|
||||
response: string;
|
||||
}
|
||||
/**
|
||||
* Callback for send(), receives the result once the server accepted the message. The error
|
||||
* path hands over the error alone
|
||||
*/
|
||||
export type SMTPConnectionSendCallback = Callback<SMTPConnectionSendInfo>;
|
||||
/**
|
||||
* Callback for login() and reset(), the result is true on success
|
||||
*/
|
||||
@@ -248,6 +275,12 @@ export interface SMTPConnectionConnectOptions extends tls.ConnectionOptions {
|
||||
allowInternalNetworkInterfaces?: boolean | undefined;
|
||||
/** DNS lookup timeout in ms */
|
||||
timeout?: number | undefined;
|
||||
/** Try the resolved addresses in turn, see net.connect */
|
||||
autoSelectFamily?: boolean | undefined;
|
||||
/** Time in ms an address gets before the next one is tried, see net.connect */
|
||||
autoSelectFamilyAttemptTimeout?: number | undefined;
|
||||
/** Hands the resolved addresses to net.connect */
|
||||
lookup?: net.LookupFunction | undefined;
|
||||
}
|
||||
/**
|
||||
* A queued handler for the next server response
|
||||
@@ -261,13 +294,14 @@ export type SMTPConnectionResponseAction = (str: string) => void;
|
||||
* * **port** - is the port to connect to (defaults to 587 or 465)
|
||||
* * **host** - is the hostname or IP address to connect to (defaults to 'localhost')
|
||||
* * **secure** - use SSL
|
||||
* * **ignoreTLS** - ignore server support for STARTTLS
|
||||
* * **requireTLS** - forces the client to use STARTTLS
|
||||
* * **ignoreTLS** - ignore server support for STARTTLS (has no effect when requireTLS is set)
|
||||
* * **requireTLS** - forces the client to use STARTTLS, takes precedence over ignoreTLS and opportunisticTLS
|
||||
* * **name** - the name of the client server
|
||||
* * **localAddress** - outbound address to bind to (see: http://nodejs.org/api/net.html#net_net_connect_options_connectionlistener)
|
||||
* * **greetingTimeout** - Time to wait in ms until greeting message is received from the server (defaults to 30 seconds)
|
||||
* * **connectionTimeout** - how many milliseconds to wait for the connection to establish (defaults to 2 minutes)
|
||||
* * **socketTimeout** - Time of inactivity until the connection is closed (defaults to 10 minutes)
|
||||
* * **maxResponseSize** - Largest single server response to accept in bytes (defaults to 1 MB)
|
||||
* * **dnsTimeout** - Time to wait in ms for the DNS requests to be resolved (defaults to 30 seconds)
|
||||
* * **lmtp** - if true, uses LMTP instead of SMTP protocol
|
||||
* * **logger** - bunyan compatible logger interface
|
||||
@@ -282,7 +316,7 @@ export type SMTPConnectionResponseAction = (str: string) => void;
|
||||
*/
|
||||
declare class SMTPConnection extends EventEmitter {
|
||||
id: string;
|
||||
stage: string;
|
||||
stage: 'init' | 'connected';
|
||||
options: SMTPConnectionOptions;
|
||||
secureConnection: boolean;
|
||||
alreadySecured: boolean;
|
||||
@@ -351,7 +385,7 @@ declare class SMTPConnection extends EventEmitter {
|
||||
*
|
||||
* @param envelope Envelope object, {from: addr, to: [addr]}
|
||||
* @param message String, Buffer or a Stream
|
||||
* @param callback Callback to return once sending is completed
|
||||
* @param done Callback to return once sending is completed
|
||||
*/
|
||||
send(envelope: SMTPEnvelope, message: string | Buffer | Readable, done: SMTPConnectionSendCallback): void;
|
||||
/**
|
||||
@@ -367,12 +401,22 @@ declare class SMTPConnection extends EventEmitter {
|
||||
declare namespace SMTPConnection {
|
||||
type Options = SMTPConnectionOptions;
|
||||
type AuthenticationType = SMTPConnectionAuth;
|
||||
type AuthenticationTypeLogin = SMTPConnectionAuth;
|
||||
type AuthenticationTypeOAuth2 = SMTPConnectionAuth;
|
||||
type AuthenticationTypeCustom = SMTPConnectionAuth;
|
||||
type AuthenticationCredentials = SMTPConnectionAuth;
|
||||
type AuthenticationOAuth2 = SMTPConnectionAuth;
|
||||
type Credentials = SMTPConnectionCredentials;
|
||||
type OAuth2 = XOAuth2Options;
|
||||
type Envelope = SMTPEnvelope;
|
||||
type DSNOptions = SMTPEnvelopeDsn;
|
||||
type DSNOption = SMTPEnvelopeDsnNotify;
|
||||
type SentMessageInfo = SMTPConnectionSendInfo;
|
||||
type SMTPError = NodemailerError;
|
||||
type CustomAuthenticationContext = SMTPConnectionCustomAuthContext;
|
||||
type CustomAuthenticationResponse = SMTPConnectionCustomAuthResponse;
|
||||
type CustomAuthenticationHandlers = SMTPConnectionCustomAuthHandlers;
|
||||
}
|
||||
/** The same aliases as module level exports, for `import * as SMTPConnection` and `import SMTPConnection = require()` */
|
||||
export type { SMTPConnectionOptions as Options, SMTPConnectionAuth as AuthenticationType, SMTPConnectionAuth as AuthenticationTypeLogin, SMTPConnectionAuth as AuthenticationTypeOAuth2, SMTPConnectionAuth as AuthenticationTypeCustom, SMTPConnectionAuth as AuthenticationCredentials, SMTPConnectionAuth as AuthenticationOAuth2, SMTPConnectionCredentials as Credentials, XOAuth2Options as OAuth2, SMTPEnvelope as Envelope, SMTPEnvelopeDsn as DSNOptions, SMTPEnvelopeDsnNotify as DSNOption, SMTPConnectionSendInfo as SentMessageInfo, NodemailerError as SMTPError, SMTPConnectionCustomAuthContext as CustomAuthenticationContext, SMTPConnectionCustomAuthResponse as CustomAuthenticationResponse, SMTPConnectionCustomAuthHandlers as CustomAuthenticationHandlers };
|
||||
export default SMTPConnection;
|
||||
+579
-227
File diff suppressed because it is too large.
Load diff
+15
-4
@@ -17,8 +17,10 @@ export interface SMTPPoolOptions extends SMTPTransportOptions {
|
||||
rateLimit?: number | undefined;
|
||||
/** Time window for rateLimit in milliseconds, defaults to 1000 */
|
||||
rateDelta?: number | undefined;
|
||||
/** How many times a message is requeued when its connection closes while sending, unlimited when not set or negative */
|
||||
/** How many times a message is requeued when its connection closes while sending, defaults to 5, a negative value means unlimited */
|
||||
maxRequeues?: number | undefined;
|
||||
/** Time in milliseconds a connection may stay idle before it is closed, defaults to 4 minutes, 0 keeps it open until the server closes it */
|
||||
idleTimeout?: number | undefined;
|
||||
}
|
||||
/**
|
||||
* The pool options once the constructor has applied the defaults
|
||||
@@ -26,6 +28,8 @@ export interface SMTPPoolOptions extends SMTPTransportOptions {
|
||||
export type SMTPPoolResolvedOptions = SMTPPoolOptions & {
|
||||
maxConnections: number;
|
||||
maxMessages: number;
|
||||
maxRequeues: number;
|
||||
idleTimeout: number;
|
||||
};
|
||||
/**
|
||||
* Result of a message sent through the pool, same as for the SMTP transport
|
||||
@@ -80,7 +84,7 @@ declare class SMTPPool extends EventEmitter {
|
||||
/**
|
||||
* The Mail instance using this transport, assigned by Mail
|
||||
*/
|
||||
mailer?: Mail<SMTPPoolSentMessageInfo> | undefined;
|
||||
mailer?: Mail<SMTPPoolSentMessageInfo, SMTPPoolOptions> | undefined;
|
||||
constructor(options?: SMTPPoolOptions | string);
|
||||
/**
|
||||
* Placeholder function for creating proxy sockets. This method immediatelly returns
|
||||
@@ -96,7 +100,7 @@ declare class SMTPPool extends EventEmitter {
|
||||
* @param mail Mail object
|
||||
* @param callback Callback function
|
||||
*/
|
||||
send(mail: MailMessage, callback: SMTPPoolSendCallback): boolean;
|
||||
send(mail: MailMessage<SMTPPoolSentMessageInfo>, callback: SMTPPoolSendCallback): boolean;
|
||||
/**
|
||||
* Closes all connections in the pool. If there is a message being sent, the connection
|
||||
* is closed later
|
||||
@@ -109,9 +113,14 @@ declare class SMTPPool extends EventEmitter {
|
||||
/**
|
||||
* Verifies SMTP configuration
|
||||
*
|
||||
* @param callback Callback function
|
||||
* @returns Promise that resolves to true if the configuration is usable
|
||||
*/
|
||||
verify(): Promise<true>;
|
||||
/**
|
||||
* Verifies SMTP configuration
|
||||
*
|
||||
* @param callback Callback function
|
||||
*/
|
||||
verify(callback: VerifyCallback): void;
|
||||
}
|
||||
/**
|
||||
@@ -122,4 +131,6 @@ declare namespace SMTPPool {
|
||||
type MailOptions = SendMailOptions;
|
||||
type SentMessageInfo = SMTPPoolSentMessageInfo;
|
||||
}
|
||||
/** The same aliases as module level exports, for `import * as SMTPPool` and `import SMTPPool = require()` */
|
||||
export type { SMTPPoolOptions as Options, SendMailOptions as MailOptions, SMTPPoolSentMessageInfo as SentMessageInfo };
|
||||
export default SMTPPool;
|
||||
+46
-12
@@ -43,6 +43,10 @@ const index_js_2 = __importDefault(require("../well-known/index.js"));
|
||||
const shared = __importStar(require("../shared/index.js"));
|
||||
const errors = __importStar(require("../errors.js"));
|
||||
const packageData = __importStar(require("../package-info.js"));
|
||||
/** First delay before a requeued message is retried, doubled on every further requeue */
|
||||
const REQUEUE_BASE_DELAY = 50;
|
||||
/** Upper bound for the requeue delay */
|
||||
const REQUEUE_MAX_DELAY = 2000;
|
||||
/**
|
||||
* Creates a SMTP pool transport object for Nodemailer
|
||||
*
|
||||
@@ -74,6 +78,11 @@ class SMTPPool extends node_events_1.EventEmitter {
|
||||
);
|
||||
this.options.maxConnections = this.options.maxConnections || 5;
|
||||
this.options.maxMessages = this.options.maxMessages || 100;
|
||||
// a default bound, a server that closes every connection before the greeting would otherwise be retried forever
|
||||
this.options.maxRequeues = typeof this.options.maxRequeues === 'number' ? this.options.maxRequeues : 5;
|
||||
// below the 5 minutes RFC 5321 asks servers to wait at least, the connection is closed by us
|
||||
// and not by the server in the middle of handing it a message
|
||||
this.options.idleTimeout = typeof this.options.idleTimeout === 'number' ? this.options.idleTimeout : 4 * 60 * 1000;
|
||||
this.logger = shared.getLogger(this.options, {
|
||||
component: this.options.component || 'smtp-pool'
|
||||
});
|
||||
@@ -117,6 +126,10 @@ class SMTPPool extends node_events_1.EventEmitter {
|
||||
*/
|
||||
send(mail, callback) {
|
||||
if (this._closed) {
|
||||
// Mail.sendMail ignores the return value, so without a callback its promise would never settle
|
||||
const err = new Error('Connection pool was closed');
|
||||
err.code = errors.ECONNECTION;
|
||||
setImmediate(() => callback(err));
|
||||
return false;
|
||||
}
|
||||
this._queue.push({
|
||||
@@ -330,15 +343,22 @@ class SMTPPool extends node_events_1.EventEmitter {
|
||||
// Note that we must wait a bit.. because the callback of the 'error' handler might be called
|
||||
// in the next event loop
|
||||
setTimeout(() => {
|
||||
let delay = 0;
|
||||
if (connection.queueEntry) {
|
||||
if (this._shouldRequeuOnConnectionClose(connection.queueEntry)) {
|
||||
this._requeueEntryOnConnectionClose(connection);
|
||||
delay = this._requeueEntryOnConnectionClose(connection);
|
||||
}
|
||||
else {
|
||||
this._failDeliveryOnConnectionClose(connection);
|
||||
}
|
||||
}
|
||||
this._continueProcessing();
|
||||
if (delay) {
|
||||
// back off, a server that keeps dropping connections is not hammered
|
||||
setTimeout(() => this._continueProcessing(), delay);
|
||||
}
|
||||
else {
|
||||
this._continueProcessing();
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
else {
|
||||
@@ -353,7 +373,7 @@ class SMTPPool extends node_events_1.EventEmitter {
|
||||
}
|
||||
/** @internal */
|
||||
_shouldRequeuOnConnectionClose(queueEntry) {
|
||||
if (this.options.maxRequeues === undefined || this.options.maxRequeues < 0) {
|
||||
if (this.options.maxRequeues < 0) {
|
||||
return true;
|
||||
}
|
||||
return queueEntry.requeueAttempts < this.options.maxRequeues;
|
||||
@@ -362,7 +382,9 @@ class SMTPPool extends node_events_1.EventEmitter {
|
||||
_failDeliveryOnConnectionClose(connection) {
|
||||
if (connection.queueEntry && connection.queueEntry.callback) {
|
||||
try {
|
||||
connection.queueEntry.callback(new Error('Reached maximum number of retries after connection was closed'));
|
||||
const err = new Error('Reached maximum number of retries after connection was closed');
|
||||
err.code = errors.ECONNECTION;
|
||||
connection.queueEntry.callback(err);
|
||||
}
|
||||
catch (E) {
|
||||
this.logger.error({
|
||||
@@ -377,6 +399,7 @@ class SMTPPool extends node_events_1.EventEmitter {
|
||||
}
|
||||
/** @internal */
|
||||
_requeueEntryOnConnectionClose(connection) {
|
||||
const delay = Math.min(REQUEUE_BASE_DELAY * 2 ** connection.queueEntry.requeueAttempts, REQUEUE_MAX_DELAY);
|
||||
connection.queueEntry.requeueAttempts += 1;
|
||||
this.logger.debug({
|
||||
tnx: 'pool',
|
||||
@@ -386,6 +409,7 @@ class SMTPPool extends node_events_1.EventEmitter {
|
||||
}, 'Re-queued message <%s> for #%s. Attempt: #%s', connection.queueEntry.messageId, connection.id, connection.queueEntry.requeueAttempts);
|
||||
this._queue.unshift(connection.queueEntry);
|
||||
connection.queueEntry = false;
|
||||
return delay;
|
||||
}
|
||||
/**
|
||||
* Continue to process message if the pool hasn't closed
|
||||
@@ -462,10 +486,14 @@ class SMTPPool extends node_events_1.EventEmitter {
|
||||
callback = shared.callbackPromise(resolve, reject);
|
||||
});
|
||||
}
|
||||
// the error paths hand over the error alone
|
||||
const done = callback;
|
||||
const auth = new pool_resource_js_1.default(this).auth;
|
||||
this.getSocket(this.options, (err, socketOptions) => {
|
||||
// the proxy handshake, if any, counts against connectionTimeout as well
|
||||
const connectStartedAt = Date.now();
|
||||
this.getSocket(Object.assign({}, this.options, { connectStartedAt }), (err, socketOptions) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
return done(err);
|
||||
}
|
||||
let options = this.options;
|
||||
if (socketOptions && socketOptions.connection) {
|
||||
@@ -480,6 +508,7 @@ class SMTPPool extends node_events_1.EventEmitter {
|
||||
options = Object.assign(shared.assign(false, options), socketOptions);
|
||||
}
|
||||
const connection = new index_js_1.default(options);
|
||||
connection._connectStartedAt = connectStartedAt;
|
||||
let returned = false;
|
||||
connection.once('error', err => {
|
||||
if (returned) {
|
||||
@@ -487,14 +516,14 @@ class SMTPPool extends node_events_1.EventEmitter {
|
||||
}
|
||||
returned = true;
|
||||
connection.close();
|
||||
return callback(err);
|
||||
return done(err);
|
||||
});
|
||||
connection.once('end', () => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
return callback(new Error('Connection closed'));
|
||||
return done(new Error('Connection closed'));
|
||||
});
|
||||
const finalize = () => {
|
||||
if (returned) {
|
||||
@@ -502,12 +531,17 @@ class SMTPPool extends node_events_1.EventEmitter {
|
||||
}
|
||||
returned = true;
|
||||
connection.quit();
|
||||
return callback(null, true);
|
||||
return done(null, true);
|
||||
};
|
||||
connection.connect(() => {
|
||||
connection.connect(err => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
if (err) {
|
||||
returned = true;
|
||||
connection.close();
|
||||
return done(err);
|
||||
}
|
||||
if (auth && (connection.allowsAuth || options.forceAuth)) {
|
||||
connection.login(auth, err => {
|
||||
if (returned) {
|
||||
@@ -516,7 +550,7 @@ class SMTPPool extends node_events_1.EventEmitter {
|
||||
if (err) {
|
||||
returned = true;
|
||||
connection.close();
|
||||
return callback(err);
|
||||
return done(err);
|
||||
}
|
||||
finalize();
|
||||
});
|
||||
@@ -526,7 +560,7 @@ class SMTPPool extends node_events_1.EventEmitter {
|
||||
err.code = errors.ENOAUTH;
|
||||
returned = true;
|
||||
connection.close();
|
||||
return callback(err);
|
||||
return done(err);
|
||||
}
|
||||
else {
|
||||
finalize();
|
||||
|
||||
+131
-46
@@ -66,7 +66,7 @@ class PoolResource extends node_events_1.EventEmitter {
|
||||
method: 'XOAUTH2'
|
||||
};
|
||||
oauth2.on('token', (token) => this.pool.mailer.emit('token', token));
|
||||
oauth2.on('error', err => this.emit('error', err));
|
||||
oauth2.on('error', err => this._fail(err));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -89,6 +89,21 @@ class PoolResource extends node_events_1.EventEmitter {
|
||||
this._connected = false;
|
||||
this.messages = 0;
|
||||
this.available = true;
|
||||
this._failed = false;
|
||||
this._sending = false;
|
||||
this._idleTimer = false;
|
||||
}
|
||||
/**
|
||||
* Emits 'error' for the first failure only. A dead resource can report the same failure more
|
||||
* than once (the connection error, then the send callback), the pool handles it once
|
||||
* @internal
|
||||
*/
|
||||
_fail(err) {
|
||||
if (this._failed) {
|
||||
return;
|
||||
}
|
||||
this._failed = true;
|
||||
this.emit('error', err);
|
||||
}
|
||||
/**
|
||||
* Initiates a connection to the SMTP server
|
||||
@@ -96,12 +111,14 @@ class PoolResource extends node_events_1.EventEmitter {
|
||||
* @param callback Callback function to run once the connection is established or failed
|
||||
*/
|
||||
connect(callback) {
|
||||
this.pool.getSocket(this.options, (err, socketOptions) => {
|
||||
// the proxy handshake, if any, counts against connectionTimeout as well
|
||||
const connectStartedAt = Date.now();
|
||||
this.pool.getSocket(Object.assign({}, this.options, { connectStartedAt }), (err, socketOptions) => {
|
||||
if (err) {
|
||||
// nothing was connected, so no 'close' event is coming that would free the
|
||||
// slot this resource holds in the pool, report the failure the way a failed
|
||||
// login does
|
||||
this.emit('error', err);
|
||||
this._fail(err);
|
||||
return callback(err);
|
||||
}
|
||||
let returned = false;
|
||||
@@ -118,8 +135,22 @@ class PoolResource extends node_events_1.EventEmitter {
|
||||
options = Object.assign((0, index_js_2.assign)(false, options), socketOptions);
|
||||
}
|
||||
this.connection = new index_js_1.default(options);
|
||||
this.connection.once('error', err => {
|
||||
this.emit('error', err);
|
||||
this.connection._connectStartedAt = connectStartedAt;
|
||||
this.connection.on('error', (err) => {
|
||||
if (this._sending) {
|
||||
// the send callback gets the same error and decides what it means for the message
|
||||
return;
|
||||
}
|
||||
if (this._connected && errors.isTransientError(err)) {
|
||||
// the server ended a connection that had nothing in flight, usually after it
|
||||
// was idle for a while. The 'end' that follows closes this resource
|
||||
this.logger.info({
|
||||
tnx: 'pool',
|
||||
cid: this.id
|
||||
}, 'Connection #%s was closed by the server: %s', this.id, err.message);
|
||||
return;
|
||||
}
|
||||
this._fail(err);
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
@@ -128,33 +159,16 @@ class PoolResource extends node_events_1.EventEmitter {
|
||||
});
|
||||
this.connection.once('end', () => {
|
||||
this.close();
|
||||
returned = true;
|
||||
});
|
||||
this.connection.connect(err => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
const timer = setTimeout(() => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
// still have not returned, this means we have an unexpected connection close
|
||||
const err = new Error('Unexpected socket close');
|
||||
if (this.connection &&
|
||||
this.connection._socket &&
|
||||
this.connection._socket.upgrading) {
|
||||
// starttls connection errors
|
||||
err.code = errors.ETLS;
|
||||
}
|
||||
callback(err);
|
||||
}, 1000);
|
||||
try {
|
||||
timer.unref();
|
||||
}
|
||||
catch (_E) {
|
||||
// Ignore. Happens on envs with non-node timer implementation
|
||||
}
|
||||
});
|
||||
this.connection.connect(() => {
|
||||
if (returned) {
|
||||
if (err) {
|
||||
// a close before the greeting, the 'end' that follows closes this resource
|
||||
// and the pool requeues or fails the entry, bounded by maxRequeues
|
||||
returned = true;
|
||||
return;
|
||||
}
|
||||
if (this.auth && (this.connection.allowsAuth || options.forceAuth)) {
|
||||
@@ -165,7 +179,7 @@ class PoolResource extends node_events_1.EventEmitter {
|
||||
returned = true;
|
||||
if (err) {
|
||||
this.connection.close();
|
||||
this.emit('error', err);
|
||||
this._fail(err);
|
||||
return callback(err);
|
||||
}
|
||||
this._connected = true;
|
||||
@@ -213,11 +227,41 @@ class PoolResource extends node_events_1.EventEmitter {
|
||||
if (mail.data.requireTLSExtensionEnabled) {
|
||||
envelope.requireTLSExtensionEnabled = mail.data.requireTLSExtensionEnabled;
|
||||
}
|
||||
this.connection.send(envelope, mail.message.createReadStream(), (err, info) => {
|
||||
this._sending = true;
|
||||
// a connection that sent messages before may have been dropped by the server in between
|
||||
const reused = this.messages > 0;
|
||||
const messageStream = mail.message.createReadStream();
|
||||
this.connection.send(envelope, messageStream, (err, info) => {
|
||||
this._sending = false;
|
||||
this.messages++;
|
||||
if (err) {
|
||||
if (reused && messageStream.readableDidRead === false && errors.isTransientError(err)) {
|
||||
// Not a byte of the message was sent, so it can go out over another
|
||||
// connection. The pool requeues the message when this resource closes
|
||||
this.logger.info({
|
||||
tnx: 'pool',
|
||||
cid: this.id,
|
||||
messageId
|
||||
}, 'Connection #%s was closed by the server before message %s was sent: %s', this.id, messageId, err.message);
|
||||
this.connection.close();
|
||||
return;
|
||||
}
|
||||
if ((err.code === errors.EENVELOPE || err.code === errors.EMESSAGE) &&
|
||||
err.responseCode !== 421 &&
|
||||
!this.connection._destroyed) {
|
||||
// The server refused this message, the connection itself is fine. Reset the
|
||||
// session and keep using it instead of opening a new one
|
||||
this.connection.reset(resetErr => {
|
||||
if (resetErr) {
|
||||
this.connection.close();
|
||||
return;
|
||||
}
|
||||
this._release();
|
||||
});
|
||||
return callback(err);
|
||||
}
|
||||
this.connection.close();
|
||||
this.emit('error', err);
|
||||
this._fail(err);
|
||||
return callback(err);
|
||||
}
|
||||
info.envelope = {
|
||||
@@ -225,28 +269,69 @@ class PoolResource extends node_events_1.EventEmitter {
|
||||
to: envelope.to
|
||||
};
|
||||
info.messageId = messageId;
|
||||
setImmediate(() => {
|
||||
if (this.messages >= this.options.maxMessages) {
|
||||
const err = new Error('Resource exhausted');
|
||||
err.code = errors.EMAXLIMIT;
|
||||
this.connection.close();
|
||||
this.emit('error', err);
|
||||
}
|
||||
else {
|
||||
this.pool._checkRateLimit(() => {
|
||||
this.available = true;
|
||||
this.emit('available');
|
||||
});
|
||||
}
|
||||
});
|
||||
setImmediate(() => this._release());
|
||||
callback(null, info);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Makes the connection available for the next message, or closes it once it has sent
|
||||
* maxMessages messages
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
_release() {
|
||||
if (this.messages >= this.options.maxMessages) {
|
||||
const err = new Error('Resource exhausted');
|
||||
err.code = errors.EMAXLIMIT;
|
||||
this.connection.close();
|
||||
this._fail(err);
|
||||
return;
|
||||
}
|
||||
this.pool._checkRateLimit(() => {
|
||||
this.available = true;
|
||||
this._startIdleTimer();
|
||||
this.emit('available');
|
||||
});
|
||||
}
|
||||
/** @internal */
|
||||
_startIdleTimer() {
|
||||
if (!this.options.idleTimeout || this.options.idleTimeout < 0) {
|
||||
return;
|
||||
}
|
||||
// one timer per connection, restarted every time the connection becomes available. A
|
||||
// connection that is busy when it fires is simply not closed
|
||||
if (this._idleTimer && typeof this._idleTimer.refresh === 'function') {
|
||||
this._idleTimer.refresh();
|
||||
return;
|
||||
}
|
||||
clearTimeout(this._idleTimer);
|
||||
this._idleTimer = setTimeout(() => {
|
||||
if (!this.available) {
|
||||
return;
|
||||
}
|
||||
this.logger.debug({
|
||||
tnx: 'pool',
|
||||
cid: this.id
|
||||
}, 'Closing connection #%s after it was idle for %sms', this.id, this.options.idleTimeout);
|
||||
// not handed another message while it says goodbye
|
||||
this.available = false;
|
||||
this.connection.quit();
|
||||
}, this.options.idleTimeout);
|
||||
if (typeof this._idleTimer.unref === 'function') {
|
||||
this._idleTimer.unref();
|
||||
}
|
||||
}
|
||||
/** @internal */
|
||||
_stopIdleTimer() {
|
||||
clearTimeout(this._idleTimer);
|
||||
this._idleTimer = false;
|
||||
}
|
||||
/**
|
||||
* Closes the connection
|
||||
*/
|
||||
close() {
|
||||
this._connected = false;
|
||||
this._stopIdleTimer();
|
||||
if (this.auth && this.auth.oauth2) {
|
||||
this.auth.oauth2.removeAllListeners();
|
||||
}
|
||||
|
||||
+19
-6
@@ -4,7 +4,7 @@ import * as shared from '../shared/index.js';
|
||||
import { type XOAuth2Options } from '../xoauth2/index.js';
|
||||
import type { ResultCallback } from '../errors.js';
|
||||
import type MailMessage from '../mailer/mail-message.js';
|
||||
import type { default as Mail, SentMessageInfo, SendMailOptions, TransportOptions, VerifyCallback } from '../mailer/index.js';
|
||||
import type { default as Mail, GetSocketCallback, SentMessageInfo, SendMailOptions, TransportOptions, VerifyCallback } from '../mailer/index.js';
|
||||
import type { MimeNodeEnvelope } from '../mime-node/index.js';
|
||||
/**
|
||||
* Authentication settings, either from the transport options or from the message data.
|
||||
@@ -38,9 +38,10 @@ export interface SMTPTransportAuth extends SMTPConnectionAuth {
|
||||
/**
|
||||
* Receives the socket details from getSocket, false when a new socket should be opened. The
|
||||
* object is merged into the connection options, a proxy handler provides the connected socket
|
||||
* as `connection`
|
||||
* as `connection`. The same callback type Mail hands to a transport, so that the SMTP
|
||||
* transports stay assignable to the Transport interface
|
||||
*/
|
||||
export type SMTPTransportGetSocketCallback = (err: Error | null, socketOptions?: SMTPConnectionOptions | false) => void;
|
||||
export type SMTPTransportGetSocketCallback = GetSocketCallback;
|
||||
/**
|
||||
* Custom socket provider, replaces the getSocket method of the transport
|
||||
*/
|
||||
@@ -78,6 +79,8 @@ export interface SMTPSentMessageInfo extends SMTPConnectionSendInfo, SentMessage
|
||||
accepted: string[];
|
||||
/** Recipients the server rejected */
|
||||
rejected: string[];
|
||||
/** Final server response for the message */
|
||||
response: string;
|
||||
}
|
||||
/**
|
||||
* Callback for send()
|
||||
@@ -101,7 +104,7 @@ declare class SMTPTransport extends EventEmitter {
|
||||
/**
|
||||
* The Mail instance using this transport, assigned by Mail
|
||||
*/
|
||||
mailer?: Mail<SMTPSentMessageInfo> | undefined;
|
||||
mailer?: Mail<SMTPSentMessageInfo, SMTPTransportOptions> | undefined;
|
||||
constructor(options?: SMTPTransportOptions | string);
|
||||
/**
|
||||
* Placeholder function for creating proxy sockets. This method immediatelly returns
|
||||
@@ -118,13 +121,18 @@ declare class SMTPTransport extends EventEmitter {
|
||||
* @param mail Mail object
|
||||
* @param callback Callback function
|
||||
*/
|
||||
send(mail: MailMessage, callback: SMTPTransportSendCallback): void;
|
||||
send(mail: MailMessage<SMTPSentMessageInfo>, callback: SMTPTransportSendCallback): void;
|
||||
/**
|
||||
* Verifies SMTP configuration
|
||||
*
|
||||
* @returns Promise that resolves to true if the configuration is usable
|
||||
*/
|
||||
verify(): Promise<true>;
|
||||
/**
|
||||
* Verifies SMTP configuration
|
||||
*
|
||||
* @param callback Callback function
|
||||
*/
|
||||
verify(): Promise<true>;
|
||||
verify(callback: VerifyCallback): void;
|
||||
/**
|
||||
* Releases resources
|
||||
@@ -139,5 +147,10 @@ declare namespace SMTPTransport {
|
||||
type MailOptions = SendMailOptions;
|
||||
type SentMessageInfo = SMTPSentMessageInfo;
|
||||
type AuthenticationType = SMTPTransportAuth;
|
||||
type AuthenticationTypeLogin = SMTPTransportAuth;
|
||||
type AuthenticationTypeOAuth2 = SMTPTransportAuth;
|
||||
type AuthenticationTypeCustom = SMTPTransportAuth;
|
||||
}
|
||||
/** The same aliases as module level exports, for `import * as SMTPTransport` and `import SMTPTransport = require()` */
|
||||
export type { SMTPTransportOptions as Options, SendMailOptions as MailOptions, SMTPSentMessageInfo as SentMessageInfo, SMTPTransportAuth as AuthenticationType, SMTPTransportAuth as AuthenticationTypeLogin, SMTPTransportAuth as AuthenticationTypeOAuth2, SMTPTransportAuth as AuthenticationTypeCustom };
|
||||
export default SMTPTransport;
|
||||
+45
-36
@@ -43,6 +43,13 @@ const shared = __importStar(require("../shared/index.js"));
|
||||
const index_js_3 = __importDefault(require("../xoauth2/index.js"));
|
||||
const errors = __importStar(require("../errors.js"));
|
||||
const packageData = __importStar(require("../package-info.js"));
|
||||
// The keys a message's `auth` may set on top of the transport's auth settings: the ones that
|
||||
// say who logs in and with what. Everything else in the auth block (accessUrl, clientId,
|
||||
// clientSecret, customHeaders, customParams, tls, privateKey, serviceClient and the rest)
|
||||
// names the endpoint the credentials go to or is a secret of the application itself, and
|
||||
// a message that could set `auth.accessUrl` and force a refresh would have the transport's
|
||||
// client secret and refresh token posted to a URL of its choosing
|
||||
const MESSAGE_AUTH_KEYS = ['type', 'user', 'pass', 'accessToken', 'refreshToken', 'expires'];
|
||||
/**
|
||||
* Creates a SMTP transport object for Nodemailer
|
||||
*
|
||||
@@ -102,7 +109,11 @@ class SMTPTransport extends node_events_1.EventEmitter {
|
||||
}
|
||||
return this.auth;
|
||||
}
|
||||
const authData = Object.assign({}, this.options.auth && typeof this.options.auth === 'object' ? this.options.auth : {}, typeof authOpts === 'object' ? authOpts : {});
|
||||
const authData = Object.assign({}, this.options.auth && typeof this.options.auth === 'object' ? this.options.auth : {});
|
||||
if (typeof authOpts === 'object') {
|
||||
// see MESSAGE_AUTH_KEYS
|
||||
shared.copyOwnKeys(authData, authOpts, key => !MESSAGE_AUTH_KEYS.includes(key));
|
||||
}
|
||||
if (Object.keys(authData).length === 0) {
|
||||
return false;
|
||||
}
|
||||
@@ -142,7 +153,9 @@ class SMTPTransport extends node_events_1.EventEmitter {
|
||||
* @param callback Callback function
|
||||
*/
|
||||
send(mail, callback) {
|
||||
this.getSocket(this.options, (err, socketOptions) => {
|
||||
// the proxy handshake, if any, counts against connectionTimeout as well
|
||||
const connectStartedAt = Date.now();
|
||||
this.getSocket(Object.assign({}, this.options, { connectStartedAt }), (err, socketOptions) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
@@ -161,6 +174,7 @@ class SMTPTransport extends node_events_1.EventEmitter {
|
||||
options = Object.assign(shared.assign(false, options), socketOptions);
|
||||
}
|
||||
const connection = new index_js_1.default(options);
|
||||
connection._connectStartedAt = connectStartedAt;
|
||||
let perCallAuth;
|
||||
const cleanupPerCallAuth = () => {
|
||||
if (perCallAuth && perCallAuth !== this.auth && perCallAuth.oauth2) {
|
||||
@@ -177,31 +191,6 @@ class SMTPTransport extends node_events_1.EventEmitter {
|
||||
connection.close();
|
||||
return callback(err);
|
||||
});
|
||||
connection.once('end', () => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
const timer = setTimeout(() => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
cleanupPerCallAuth();
|
||||
// still have not returned, this means we have an unexpected connection close
|
||||
const err = new Error('Unexpected socket close');
|
||||
if (connection && connection._socket && connection._socket.upgrading) {
|
||||
// starttls connection errors
|
||||
err.code = errors.ETLS;
|
||||
}
|
||||
callback(err);
|
||||
}, 1000);
|
||||
try {
|
||||
timer.unref();
|
||||
}
|
||||
catch (_E) {
|
||||
// Ignore. Happens on envs with non-node timer implementation
|
||||
}
|
||||
});
|
||||
const sendMessage = () => {
|
||||
const envelope = mail.message.getEnvelope();
|
||||
const messageId = mail.message.messageId();
|
||||
@@ -221,6 +210,10 @@ class SMTPTransport extends node_events_1.EventEmitter {
|
||||
messageId
|
||||
}, 'Sending message %s to <%s>', messageId, recipients.join(', '));
|
||||
connection.send(envelope, mail.message.createReadStream(), (err, info) => {
|
||||
if (returned) {
|
||||
// the connection error handler has already reported this send
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
cleanupPerCallAuth();
|
||||
connection.close();
|
||||
@@ -247,10 +240,16 @@ class SMTPTransport extends node_events_1.EventEmitter {
|
||||
}
|
||||
});
|
||||
};
|
||||
connection.connect(() => {
|
||||
connection.connect(err => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
if (err) {
|
||||
// the server closed the connection before the greeting
|
||||
returned = true;
|
||||
connection.close();
|
||||
return callback(err);
|
||||
}
|
||||
perCallAuth = this.getAuth(mail.data.auth);
|
||||
if (perCallAuth && (connection.allowsAuth || options.forceAuth)) {
|
||||
connection.login(perCallAuth, err => {
|
||||
@@ -279,9 +278,13 @@ class SMTPTransport extends node_events_1.EventEmitter {
|
||||
callback = shared.callbackPromise(resolve, reject);
|
||||
});
|
||||
}
|
||||
this.getSocket(this.options, (err, socketOptions) => {
|
||||
// the error paths hand over the error alone
|
||||
const done = callback;
|
||||
// the proxy handshake, if any, counts against connectionTimeout as well
|
||||
const connectStartedAt = Date.now();
|
||||
this.getSocket(Object.assign({}, this.options, { connectStartedAt }), (err, socketOptions) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
return done(err);
|
||||
}
|
||||
let options = this.options;
|
||||
if (socketOptions && socketOptions.connection) {
|
||||
@@ -296,6 +299,7 @@ class SMTPTransport extends node_events_1.EventEmitter {
|
||||
options = Object.assign(shared.assign(false, options), socketOptions);
|
||||
}
|
||||
const connection = new index_js_1.default(options);
|
||||
connection._connectStartedAt = connectStartedAt;
|
||||
let returned = false;
|
||||
let perCallAuth;
|
||||
const cleanupPerCallAuth = () => {
|
||||
@@ -311,7 +315,7 @@ class SMTPTransport extends node_events_1.EventEmitter {
|
||||
returned = true;
|
||||
cleanupPerCallAuth();
|
||||
connection.close();
|
||||
return callback(err);
|
||||
return done(err);
|
||||
});
|
||||
connection.once('end', () => {
|
||||
if (returned) {
|
||||
@@ -319,7 +323,7 @@ class SMTPTransport extends node_events_1.EventEmitter {
|
||||
}
|
||||
returned = true;
|
||||
cleanupPerCallAuth();
|
||||
return callback(new Error('Connection closed'));
|
||||
return done(new Error('Connection closed'));
|
||||
});
|
||||
const finalize = () => {
|
||||
if (returned) {
|
||||
@@ -328,12 +332,17 @@ class SMTPTransport extends node_events_1.EventEmitter {
|
||||
returned = true;
|
||||
cleanupPerCallAuth();
|
||||
connection.quit();
|
||||
return callback(null, true);
|
||||
return done(null, true);
|
||||
};
|
||||
connection.connect(() => {
|
||||
connection.connect(err => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
if (err) {
|
||||
returned = true;
|
||||
connection.close();
|
||||
return done(err);
|
||||
}
|
||||
perCallAuth = this.getAuth({});
|
||||
if (perCallAuth && (connection.allowsAuth || options.forceAuth)) {
|
||||
connection.login(perCallAuth, err => {
|
||||
@@ -344,7 +353,7 @@ class SMTPTransport extends node_events_1.EventEmitter {
|
||||
if (err) {
|
||||
returned = true;
|
||||
connection.close();
|
||||
return callback(err);
|
||||
return done(err);
|
||||
}
|
||||
finalize();
|
||||
});
|
||||
@@ -355,7 +364,7 @@ class SMTPTransport extends node_events_1.EventEmitter {
|
||||
returned = true;
|
||||
cleanupPerCallAuth();
|
||||
connection.close();
|
||||
return callback(err);
|
||||
return done(err);
|
||||
}
|
||||
else {
|
||||
finalize();
|
||||
|
||||
+2
@@ -60,4 +60,6 @@ declare namespace StreamTransport {
|
||||
type MailOptions = SendMailOptions;
|
||||
type SentMessageInfo = StreamSentMessageInfo;
|
||||
}
|
||||
/** The same aliases as module level exports, for `import * as StreamTransport` and `import StreamTransport = require()` */
|
||||
export type { StreamTransportOptions as Options, SendMailOptions as MailOptions, StreamSentMessageInfo as SentMessageInfo };
|
||||
export default StreamTransport;
|
||||
+13
@@ -314,6 +314,12 @@ exports.services = {
|
||||
"host": "mailosaur.io",
|
||||
"port": 25
|
||||
},
|
||||
"MailSenpai": {
|
||||
"description": "MailSenpai (SMTP Senpai, EU)",
|
||||
"host": "relay.mailsenpai.com",
|
||||
"port": 2525,
|
||||
"secure": false
|
||||
},
|
||||
"Mailtrap": {
|
||||
"description": "Mailtrap",
|
||||
"host": "live.smtp.mailtrap.io",
|
||||
@@ -357,6 +363,13 @@ exports.services = {
|
||||
"port": 587,
|
||||
"secure": false
|
||||
},
|
||||
"Oveyon": {
|
||||
"description": "Oveyon",
|
||||
"host": "smtp.oveyon.com",
|
||||
"port": 587,
|
||||
"secure": false,
|
||||
"requireTLS": true
|
||||
},
|
||||
"Postmark": {
|
||||
"description": "Postmark",
|
||||
"aliases": [
|
||||
|
||||
+13
-3
@@ -1,6 +1,7 @@
|
||||
import { Stream } from 'node:stream';
|
||||
import crypto from 'node:crypto';
|
||||
import * as shared from '../shared/index.js';
|
||||
import type { ResultCallback } from '../errors.js';
|
||||
import type { OutgoingHttpHeaders } from 'node:http';
|
||||
/**
|
||||
* Receives the result of a provisionCallback run: an error, or the new access token and
|
||||
@@ -13,9 +14,11 @@ export type XOAuth2ProvisionResultCallback = (err: Error | null, accessToken?: s
|
||||
*/
|
||||
export type XOAuth2ProvisionCallback = (user: string, renew: boolean, callback: XOAuth2ProvisionResultCallback) => void;
|
||||
/**
|
||||
* Receives an access token, or the error that prevented generating one
|
||||
* Receives an access token, or the error that prevented generating one. Declared with a
|
||||
* required token, the way @types/nodemailer declared it, the error path hands over the
|
||||
* error alone
|
||||
*/
|
||||
export type XOAuth2TokenCallback = (err: Error | null, accessToken?: string) => void;
|
||||
export type XOAuth2TokenCallback = (err: Error | null, accessToken: string) => void;
|
||||
/**
|
||||
* A private key accepted by crypto.createSign().sign()
|
||||
*/
|
||||
@@ -58,6 +61,8 @@ export interface XOAuth2Options {
|
||||
component?: string | undefined;
|
||||
/** Extra headers for the token request */
|
||||
customHeaders?: OutgoingHttpHeaders | undefined;
|
||||
/** Timeout for the token request in milliseconds, defaults to 60000, 0 disables it */
|
||||
requestTimeout?: number | undefined;
|
||||
/** Extra form fields for the token request */
|
||||
customParams?: {
|
||||
[key: string]: any;
|
||||
@@ -79,7 +84,7 @@ export interface XOAuth2Token {
|
||||
*/
|
||||
export interface XOAuth2QueuedRequest {
|
||||
renew: boolean;
|
||||
callback: XOAuth2TokenCallback;
|
||||
callback: ResultCallback<string>;
|
||||
}
|
||||
/**
|
||||
* XOAUTH2 access_token generator for Gmail.
|
||||
@@ -181,11 +186,16 @@ declare class XOAuth2 extends Stream {
|
||||
[key: string]: any;
|
||||
}): string;
|
||||
}
|
||||
/** The extra request settings of the token request, the customHeaders and customParams options */
|
||||
export type XOAuth2RequestParams = Pick<XOAuth2Options, 'customHeaders' | 'customParams'>;
|
||||
/**
|
||||
* Type aliases in the layout of @types/nodemailer, so `XOAuth2.Options` style references keep working
|
||||
*/
|
||||
declare namespace XOAuth2 {
|
||||
type Options = XOAuth2Options;
|
||||
type Token = XOAuth2Token;
|
||||
type RequestParams = XOAuth2RequestParams;
|
||||
}
|
||||
/** The same aliases as module level exports, for `import * as XOAuth2` and `import XOAuth2 = require()` */
|
||||
export type { XOAuth2Options as Options, XOAuth2Token as Token, XOAuth2RequestParams as RequestParams };
|
||||
export default XOAuth2;
|
||||
+33
-15
@@ -74,12 +74,14 @@ class XOAuth2 extends node_stream_1.Stream {
|
||||
constructor(options, logger) {
|
||||
super();
|
||||
this.options = options || {};
|
||||
this.configError = false;
|
||||
if (options && options.serviceClient) {
|
||||
if (!options.privateKey || !options.user) {
|
||||
// reported through getToken() rather than an 'error' event: the transports forward
|
||||
// that event up to the Mail object, where nothing may be listening
|
||||
const err = new Error('Options "privateKey" and "user" are required for service account!');
|
||||
err.code = errors.EOAUTH2;
|
||||
setImmediate(() => this.emit('error', err));
|
||||
return;
|
||||
this.configError = err;
|
||||
}
|
||||
const serviceRequestTimeout = Math.min(Math.max(Number(this.options.serviceRequestTimeout) || 0, 0), 3600);
|
||||
this.options.serviceRequestTimeout = serviceRequestTimeout || 5 * 60;
|
||||
@@ -111,6 +113,11 @@ class XOAuth2 extends node_stream_1.Stream {
|
||||
* @param callback Callback function with error object and token string
|
||||
*/
|
||||
getToken(renew, callback) {
|
||||
// the error paths hand over the error alone
|
||||
const done = callback;
|
||||
if (this.configError) {
|
||||
return done(this.configError);
|
||||
}
|
||||
if (!renew && this.accessToken && (!this.expires || this.expires > Date.now())) {
|
||||
this.logger.debug({
|
||||
tnx: 'OAUTH2',
|
||||
@@ -136,11 +143,11 @@ class XOAuth2 extends node_stream_1.Stream {
|
||||
}, 'Cannot renew access token for %s: No refresh mechanism available', this.options.user);
|
||||
const err = new Error("Can't create new access token for user");
|
||||
err.code = errors.EOAUTH2;
|
||||
return callback(err);
|
||||
return done(err);
|
||||
}
|
||||
// If renewal already in progress, queue this request instead of starting another
|
||||
if (this.renewing) {
|
||||
this.renewalQueue.push({ renew, callback });
|
||||
this.renewalQueue.push({ renew, callback: done });
|
||||
return;
|
||||
}
|
||||
this.renewing = true;
|
||||
@@ -165,7 +172,7 @@ class XOAuth2 extends node_stream_1.Stream {
|
||||
}, 'Generated new Access Token for %s', this.options.user);
|
||||
}
|
||||
// Complete original request
|
||||
callback(err, accessToken);
|
||||
done(err, accessToken);
|
||||
};
|
||||
if (this.provisionCallback) {
|
||||
this.provisionCallback(this.options.user, !!renew, (err, accessToken, expires) => {
|
||||
@@ -204,6 +211,8 @@ class XOAuth2 extends node_stream_1.Stream {
|
||||
* @param callback Callback function with error object and token string
|
||||
*/
|
||||
generateToken(callback) {
|
||||
// the error paths hand over the error alone
|
||||
const done = callback;
|
||||
let urlOptions;
|
||||
let loggedUrlOptions;
|
||||
if (this.options.serviceClient) {
|
||||
@@ -224,7 +233,7 @@ class XOAuth2 extends node_stream_1.Stream {
|
||||
catch (_err) {
|
||||
const err = new Error("Can't generate token. Check your auth options");
|
||||
err.code = errors.EOAUTH2;
|
||||
return callback(err);
|
||||
return done(err);
|
||||
}
|
||||
urlOptions = {
|
||||
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
||||
@@ -239,7 +248,7 @@ class XOAuth2 extends node_stream_1.Stream {
|
||||
if (!this.options.refreshToken) {
|
||||
const err = new Error("Can't create new access token for user");
|
||||
err.code = errors.EOAUTH2;
|
||||
return callback(err);
|
||||
return done(err);
|
||||
}
|
||||
// web app - https://developers.google.com/identity/protocols/OAuth2WebServer
|
||||
urlOptions = {
|
||||
@@ -255,8 +264,14 @@ class XOAuth2 extends node_stream_1.Stream {
|
||||
grant_type: 'refresh_token'
|
||||
};
|
||||
}
|
||||
Object.assign(urlOptions, this.options.customParams);
|
||||
Object.assign(loggedUrlOptions, this.options.customParams);
|
||||
const customParams = this.options.customParams || {};
|
||||
Object.assign(urlOptions, customParams);
|
||||
// a custom param is as likely to be a client assertion or an API key as a tenant
|
||||
// name, so the log gets the same glimpse of it that it gets of the client secret
|
||||
for (const key of Object.keys(customParams)) {
|
||||
const param = customParams[key];
|
||||
loggedUrlOptions[key] = (param === undefined || param === null ? '' : String(param)).substr(0, 6) + '...';
|
||||
}
|
||||
this.logger.debug({
|
||||
tnx: 'OAUTH2',
|
||||
user: this.options.user,
|
||||
@@ -265,13 +280,13 @@ class XOAuth2 extends node_stream_1.Stream {
|
||||
this.postRequest(this.options.accessUrl, urlOptions, this.options, (error, body) => {
|
||||
let data;
|
||||
if (error) {
|
||||
return callback(error);
|
||||
return done(error);
|
||||
}
|
||||
try {
|
||||
data = JSON.parse(body.toString());
|
||||
}
|
||||
catch (E) {
|
||||
return callback(E);
|
||||
return done(E);
|
||||
}
|
||||
if (!data || typeof data !== 'object') {
|
||||
this.logger.debug({
|
||||
@@ -281,7 +296,7 @@ class XOAuth2 extends node_stream_1.Stream {
|
||||
}, 'Response: %s', (body || '').toString());
|
||||
const err = new Error('Invalid authentication response');
|
||||
err.code = errors.EOAUTH2;
|
||||
return callback(err);
|
||||
return done(err);
|
||||
}
|
||||
const logData = Object.assign({}, data);
|
||||
if (logData.access_token) {
|
||||
@@ -303,7 +318,7 @@ class XOAuth2 extends node_stream_1.Stream {
|
||||
}
|
||||
const err = new Error(errorMessage);
|
||||
err.code = errors.EOAUTH2;
|
||||
return callback(err);
|
||||
return done(err);
|
||||
}
|
||||
if (data.access_token) {
|
||||
this.updateToken(data.access_token, data.expires_in);
|
||||
@@ -311,7 +326,7 @@ class XOAuth2 extends node_stream_1.Stream {
|
||||
}
|
||||
const err = new Error('No access token');
|
||||
err.code = errors.EOAUTH2;
|
||||
return callback(err);
|
||||
return done(err);
|
||||
});
|
||||
}
|
||||
/**
|
||||
@@ -345,7 +360,10 @@ class XOAuth2 extends node_stream_1.Stream {
|
||||
method: 'post',
|
||||
headers: params.customHeaders,
|
||||
body: payload,
|
||||
allowErrorResponse: true
|
||||
allowErrorResponse: true,
|
||||
// unset falls back to the fetch default, a stalled token endpoint would otherwise keep
|
||||
// `renewing` set and queue every later request
|
||||
timeout: params.requestTimeout
|
||||
};
|
||||
// OAuth2 token endpoints are credential-bearing. src/fetch already
|
||||
// validates certs by default; pin rejectUnauthorized:true here so the
|
||||
|
||||
+16
-2
@@ -39,7 +39,21 @@ export type Address = MailboxAddress | GroupAddress;
|
||||
*
|
||||
* @param str Address field
|
||||
* @param options Optional options object
|
||||
* @param options._depth Internal recursion depth counter (do not set manually)
|
||||
* @return An array of address objects
|
||||
*/
|
||||
export default function addressparser(str?: string | null, options?: AddressParserOptions): Address[];
|
||||
declare function addressparser(str: string | null | undefined, options: AddressParserOptions & {
|
||||
flatten: true;
|
||||
}): MailboxAddress[];
|
||||
declare function addressparser(str?: string | null, options?: AddressParserOptions): Address[];
|
||||
/**
|
||||
* Type aliases in the layout of @types/nodemailer, so `addressparser.Address` style references keep working
|
||||
*/
|
||||
type AddressEntry = Address;
|
||||
declare namespace addressparser {
|
||||
type Address = MailboxAddress;
|
||||
type Group = GroupAddress;
|
||||
type AddressOrGroup = AddressEntry;
|
||||
}
|
||||
/** The names @types/nodemailer used for the group entry and for the union of both entry types */
|
||||
export type { GroupAddress as Group, Address as AddressOrGroup };
|
||||
export default addressparser;
|
||||
+840
-243
File diff suppressed because it is too large.
Load diff
+3
@@ -24,6 +24,9 @@ export interface EncoderOptions {
|
||||
/**
|
||||
* Creates a transform stream for encoding data to base64 encoding
|
||||
*
|
||||
* The output is the same as `wrap(encode(input), lineLength)` no matter how the input is split
|
||||
* into chunks: every line but the last one ends with a line break, the last one does not
|
||||
*
|
||||
* @constructor
|
||||
* @param options Stream options
|
||||
* @param [options.lineLength=76] Maximum length for lines, set to false to disable wrapping
|
||||
|
||||
+82
-42
@@ -11,6 +11,47 @@ export function encode(buffer) {
|
||||
}
|
||||
return buffer.toString('base64');
|
||||
}
|
||||
/**
|
||||
* Turns a line length option into a whole number of characters, the default for anything unusable
|
||||
*/
|
||||
function normalizeLineLength(lineLength) {
|
||||
const length = Math.floor(Number(lineLength));
|
||||
return Number.isFinite(length) && length >= 1 ? length : 76;
|
||||
}
|
||||
/**
|
||||
* Splits the bytes of `src` into lines of `lineLength` bytes, each followed by a line break. With
|
||||
* `final` set the last line, which may be shorter, gets no line break; otherwise only complete
|
||||
* lines are taken and the rest is left for the caller
|
||||
*
|
||||
* @param src Bytes to wrap
|
||||
* @param lineLength Line length
|
||||
* @param final Whether `src` ends the output
|
||||
* @returns The wrapped bytes and the number of trailing bytes not taken
|
||||
*/
|
||||
function wrapBuffer(src, lineLength, final) {
|
||||
const lines = Math.ceil(src.length / lineLength);
|
||||
// the last line waits for more data unless this is the end: whether it gets a line break
|
||||
// depends on whether anything follows it
|
||||
const complete = Math.max(lines - 1, 0);
|
||||
let rest = src.length - complete * lineLength;
|
||||
const output = Buffer.allocUnsafe(complete * (lineLength + 2) + (final ? rest : 0));
|
||||
let to = 0;
|
||||
for (let from = 0; from < complete * lineLength; from += lineLength) {
|
||||
src.copy(output, to, from, from + lineLength);
|
||||
to += lineLength;
|
||||
output[to++] = 0x0d;
|
||||
output[to++] = 0x0a;
|
||||
}
|
||||
if (final) {
|
||||
to += src.copy(output, to, complete * lineLength);
|
||||
rest = 0;
|
||||
}
|
||||
if (to !== output.length) {
|
||||
// never hand out bytes of the unfilled allocation
|
||||
throw new Error('Unexpected wrapped length');
|
||||
}
|
||||
return { output, rest };
|
||||
}
|
||||
/**
|
||||
* Adds soft line breaks to a base64 string
|
||||
*
|
||||
@@ -20,7 +61,8 @@ export function encode(buffer) {
|
||||
*/
|
||||
export function wrap(str, lineLength) {
|
||||
str = (str || '').toString();
|
||||
lineLength = lineLength || 76;
|
||||
// a negative length would step backwards through the input and never finish
|
||||
lineLength = Math.max(Number(lineLength) || 76, 1);
|
||||
if (str.length <= lineLength) {
|
||||
return str;
|
||||
}
|
||||
@@ -38,6 +80,9 @@ export function wrap(str, lineLength) {
|
||||
/**
|
||||
* Creates a transform stream for encoding data to base64 encoding
|
||||
*
|
||||
* The output is the same as `wrap(encode(input), lineLength)` no matter how the input is split
|
||||
* into chunks: every line but the last one ends with a line break, the last one does not
|
||||
*
|
||||
* @constructor
|
||||
* @param options Stream options
|
||||
* @param [options.lineLength=76] Maximum length for lines, set to false to disable wrapping
|
||||
@@ -47,66 +92,61 @@ export class Encoder extends Transform {
|
||||
super();
|
||||
this.options = options || {};
|
||||
if (this.options.lineLength !== false) {
|
||||
this.options.lineLength = this.options.lineLength || 76;
|
||||
this.options.lineLength = normalizeLineLength(this.options.lineLength);
|
||||
}
|
||||
this._curLine = '';
|
||||
this._remainingBytes = false;
|
||||
this.inputBytes = 0;
|
||||
this.outputBytes = 0;
|
||||
}
|
||||
/**
|
||||
* Emits the encoded characters `b64` that follow the current line, keeping what can not be
|
||||
* emitted yet as the new current line
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
_emit(b64, final) {
|
||||
const src = Buffer.from(this._curLine + b64, 'latin1');
|
||||
if (!src.length) {
|
||||
return;
|
||||
}
|
||||
let output = src;
|
||||
if (this.options.lineLength) {
|
||||
const wrapped = wrapBuffer(src, this.options.lineLength, final);
|
||||
output = wrapped.output;
|
||||
this._curLine = wrapped.rest ? src.toString('latin1', src.length - wrapped.rest) : '';
|
||||
}
|
||||
else {
|
||||
this._curLine = '';
|
||||
}
|
||||
if (output.length) {
|
||||
this.outputBytes += output.length;
|
||||
this.push(output);
|
||||
}
|
||||
}
|
||||
/** @internal */
|
||||
_transform(chunk, encoding, done) {
|
||||
let buf = encoding !== 'buffer' ? Buffer.from(chunk, encoding) : chunk;
|
||||
if (!buf || !buf.length) {
|
||||
setImmediate(done);
|
||||
return;
|
||||
return done();
|
||||
}
|
||||
this.inputBytes += buf.length;
|
||||
if (this._remainingBytes && this._remainingBytes.length) {
|
||||
if (this._remainingBytes) {
|
||||
buf = Buffer.concat([this._remainingBytes, buf], this._remainingBytes.length + buf.length);
|
||||
this._remainingBytes = false;
|
||||
}
|
||||
if (buf.length % 3) {
|
||||
this._remainingBytes = buf.slice(buf.length - (buf.length % 3));
|
||||
buf = buf.slice(0, buf.length - (buf.length % 3));
|
||||
const extra = buf.length % 3;
|
||||
if (extra) {
|
||||
this._remainingBytes = buf.subarray(buf.length - extra);
|
||||
buf = buf.subarray(0, buf.length - extra);
|
||||
}
|
||||
else {
|
||||
this._remainingBytes = false;
|
||||
}
|
||||
let b64 = this._curLine + encode(buf);
|
||||
if (this.options.lineLength) {
|
||||
b64 = wrap(b64, this.options.lineLength);
|
||||
// remove last line as it is still most probably incomplete
|
||||
const lastLF = b64.lastIndexOf('\n');
|
||||
if (lastLF < 0) {
|
||||
this._curLine = b64;
|
||||
b64 = '';
|
||||
}
|
||||
else if (lastLF === b64.length - 1) {
|
||||
this._curLine = '';
|
||||
}
|
||||
else {
|
||||
this._curLine = b64.substring(lastLF + 1);
|
||||
b64 = b64.substring(0, lastLF + 1);
|
||||
}
|
||||
}
|
||||
if (b64) {
|
||||
this.outputBytes += b64.length;
|
||||
this.push(Buffer.from(b64, 'ascii'));
|
||||
}
|
||||
setImmediate(done);
|
||||
this._emit(encode(buf), false);
|
||||
done();
|
||||
}
|
||||
/** @internal */
|
||||
_flush(done) {
|
||||
if (this._remainingBytes && this._remainingBytes.length) {
|
||||
this._curLine += encode(this._remainingBytes);
|
||||
}
|
||||
if (this._curLine) {
|
||||
this._curLine = wrap(this._curLine, this.options.lineLength);
|
||||
this.outputBytes += this._curLine.length;
|
||||
this.push(Buffer.from(this._curLine, 'ascii'));
|
||||
this._curLine = '';
|
||||
}
|
||||
this._emit(this._remainingBytes ? encode(this._remainingBytes) : '', true);
|
||||
this._remainingBytes = false;
|
||||
done();
|
||||
}
|
||||
}
|
||||
+13
-1
@@ -30,11 +30,23 @@ declare class DKIM {
|
||||
constructor(options: DKIMOptions);
|
||||
sign(input: Readable | Buffer | string, extraOptions?: DKIMOptions): DKIMSignedStream;
|
||||
}
|
||||
/** The signer options without any key material */
|
||||
export type DKIMOptionalOptions = Omit<DKIMOptions, 'keys' | 'domainName' | 'keySelector' | 'privateKey'>;
|
||||
/** The signer options for a single key given as domainName, keySelector and privateKey */
|
||||
export type DKIMSingleKeyOptions = Omit<DKIMOptions, 'keys'>;
|
||||
/** The signer options for one or more keys given through `keys` */
|
||||
export type DKIMMultipleKeysOptions = DKIMOptionalOptions & {
|
||||
keys: DKIMKey | DKIMKey[];
|
||||
};
|
||||
/**
|
||||
* 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'>;
|
||||
type OptionalOptions = DKIMOptionalOptions;
|
||||
type SingleKeyOptions = DKIMSingleKeyOptions;
|
||||
type MultipleKeysOptions = DKIMMultipleKeysOptions;
|
||||
}
|
||||
/** The same aliases as module level exports, for `import * as DKIM` and `import DKIM = require()` */
|
||||
export type { DKIMOptions as Options, DKIMOptionalOptions as OptionalOptions, DKIMSingleKeyOptions as SingleKeyOptions, DKIMMultipleKeysOptions as MultipleKeysOptions };
|
||||
export default DKIM;
|
||||
+69
-37
@@ -32,51 +32,67 @@ class DKIMSigner {
|
||||
this.output = output;
|
||||
this.output.usingCache = false;
|
||||
this.hasErrored = false;
|
||||
this.input.on('error', err => {
|
||||
this.input.on('error', err => this.fail(err));
|
||||
// A consumer that goes away before the signed message was read in full destroys the
|
||||
// output. Stop reading the input and drop the cache file instead of leaving them open
|
||||
this.output.once('close', () => {
|
||||
if (this.output.writableFinished) {
|
||||
return;
|
||||
}
|
||||
this.hasErrored = true;
|
||||
this.input.unpipe();
|
||||
this.input.destroy();
|
||||
this.cleanup();
|
||||
output.emit('error', err);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Ends the output with an error and releases the cache file
|
||||
*/
|
||||
fail(err) {
|
||||
this.hasErrored = true;
|
||||
this.cleanup();
|
||||
this.output.destroy(err);
|
||||
}
|
||||
cleanup() {
|
||||
if (!this.cache || !this.cachePath) {
|
||||
return;
|
||||
}
|
||||
const cache = this.cache;
|
||||
this.cache = false;
|
||||
cache.destroy();
|
||||
fs.unlink(this.cachePath, () => false);
|
||||
}
|
||||
createReadCache() {
|
||||
// pipe remainings to cache file
|
||||
this.cache = fs.createReadStream(this.cachePath);
|
||||
this.cache.once('error', err => {
|
||||
this.cleanup();
|
||||
this.output.emit('error', err);
|
||||
});
|
||||
this.cache.once('error', err => this.fail(err));
|
||||
this.cache.once('close', () => {
|
||||
this.cleanup();
|
||||
});
|
||||
this.cache.pipe(this.output);
|
||||
}
|
||||
sendNextChunk() {
|
||||
while (!this.hasErrored) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
sendSignedOutput() {
|
||||
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) {
|
||||
@@ -85,13 +101,20 @@ class DKIMSigner {
|
||||
return;
|
||||
}
|
||||
const key = this.keys[keyPos++];
|
||||
const dkimField = sign(this.headers, this.hashAlgo, this.bodyHash, {
|
||||
domainName: key.domainName,
|
||||
keySelector: key.keySelector,
|
||||
privateKey: key.privateKey,
|
||||
headerFieldNames: this.options.headerFieldNames,
|
||||
skipFields: this.options.skipFields
|
||||
});
|
||||
let dkimField;
|
||||
try {
|
||||
dkimField = sign(this.headers, this.hashAlgo, this.bodyHash, {
|
||||
domainName: key.domainName,
|
||||
keySelector: key.keySelector,
|
||||
privateKey: key.privateKey,
|
||||
headerFieldNames: this.options.headerFieldNames,
|
||||
skipFields: this.options.skipFields
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
this.fail(err);
|
||||
return;
|
||||
}
|
||||
if (dkimField) {
|
||||
this.output.write(Buffer.from(dkimField + '\r\n'));
|
||||
}
|
||||
@@ -105,10 +128,10 @@ class DKIMSigner {
|
||||
}
|
||||
createWriteCache() {
|
||||
this.output.usingCache = true;
|
||||
// pipe remainings to cache file
|
||||
this.cache = fs.createWriteStream(this.cachePath);
|
||||
// pipe remainings to cache file. It holds the message body and cacheDir is often a
|
||||
// shared directory such as /tmp, so nobody but the owner gets to read it
|
||||
this.cache = fs.createWriteStream(this.cachePath, { mode: 0o600 });
|
||||
this.cache.once('error', err => {
|
||||
this.cleanup();
|
||||
// drain input
|
||||
this.relaxedBody.unpipe(this.cache);
|
||||
this.relaxedBody.on('readable', () => {
|
||||
@@ -116,11 +139,12 @@ class DKIMSigner {
|
||||
// do nothing
|
||||
}
|
||||
});
|
||||
this.hasErrored = true;
|
||||
// emit error
|
||||
this.output.emit('error', err);
|
||||
this.fail(err);
|
||||
});
|
||||
this.cache.once('close', () => {
|
||||
if (this.hasErrored) {
|
||||
return;
|
||||
}
|
||||
this.sendSignedOutput();
|
||||
});
|
||||
this.relaxedBody.removeAllListeners('readable');
|
||||
@@ -191,7 +215,15 @@ class DKIM {
|
||||
}
|
||||
const signer = new DKIMSigner(options, this.keys, inputStream, output);
|
||||
setImmediate(() => {
|
||||
signer.signStream();
|
||||
try {
|
||||
signer.signStream();
|
||||
}
|
||||
catch (_E) {
|
||||
// the body hash is created here, an unknown hashAlgo throws inside this timer
|
||||
// where nothing else could catch it
|
||||
signer.fail(sign.unsupportedHashAlgoError(signer.hashAlgo));
|
||||
return;
|
||||
}
|
||||
if (writeValue) {
|
||||
setImmediate(() => {
|
||||
inputStream.end(writeValue);
|
||||
|
||||
+10
-1
@@ -13,7 +13,7 @@ export interface MessageParserHeaderLine {
|
||||
* 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 {
|
||||
declare class MessageParser extends Transform {
|
||||
lastBytes: Buffer;
|
||||
headersParsed: boolean;
|
||||
headerBytes: number;
|
||||
@@ -37,3 +37,12 @@ export default class MessageParser extends Transform {
|
||||
checkHeaders(data: Buffer): boolean;
|
||||
parseHeaders(): MessageParserHeaderLine[];
|
||||
}
|
||||
/**
|
||||
* Type aliases in the layout of @types/nodemailer, so `MessageParser.Header` style references keep working
|
||||
*/
|
||||
declare namespace MessageParser {
|
||||
type Header = MessageParserHeaderLine;
|
||||
}
|
||||
/** The same alias as a module level export, for `import * as MessageParser` and `import MessageParser = require()` */
|
||||
export type { MessageParserHeaderLine as Header };
|
||||
export default MessageParser;
|
||||
+44
-12
@@ -1,10 +1,37 @@
|
||||
import { Transform } from 'node:stream';
|
||||
/**
|
||||
* Drops the SP and HTAB from both ends of a field name.
|
||||
*
|
||||
* An index scan rather than `/^[ \t]+|[ \t]+$/g`, which retries the trailing branch at
|
||||
* every offset of a blank run that is followed by other text: the run is consumed greedily,
|
||||
* `$` fails, and the engine gives the characters back one at a time before moving on to
|
||||
* start the same walk one offset further in. A field name carrying a 128 KiB run of spaces
|
||||
* between two atoms took about 6.7 seconds to trim, and the cost grows with its square.
|
||||
*
|
||||
* Not `.trim()`, which takes the other Unicode spaces along: an NBSP in front of a field
|
||||
* name would come off and select the name into the signed set, where the relaxed
|
||||
* canonicalization in sign.ts deliberately leaves everything but SP and HTAB alone.
|
||||
*
|
||||
* @param str Field name to trim
|
||||
* @return The field name without the surrounding SP and HTAB
|
||||
*/
|
||||
function _trimFieldName(str) {
|
||||
let start = 0;
|
||||
let end = str.length;
|
||||
while (start < end && (str.charCodeAt(start) === 0x20 || str.charCodeAt(start) === 0x09)) {
|
||||
start++;
|
||||
}
|
||||
while (end > start && (str.charCodeAt(end - 1) === 0x20 || str.charCodeAt(end - 1) === 0x09)) {
|
||||
end--;
|
||||
}
|
||||
return str.slice(start, end);
|
||||
}
|
||||
/**
|
||||
* 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 {
|
||||
class MessageParser extends Transform {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this.lastBytes = Buffer.alloc(4);
|
||||
@@ -78,7 +105,7 @@ export default class MessageParser extends Transform {
|
||||
const chunk = data.slice(headerPos);
|
||||
this.bodySize += chunk.length;
|
||||
// this would be the first chunk of data sent downstream
|
||||
setImmediate(() => this.push(chunk));
|
||||
this.push(chunk);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -107,7 +134,7 @@ export default class MessageParser extends Transform {
|
||||
this.bodySize += chunk.length;
|
||||
this.push(chunk);
|
||||
}
|
||||
setImmediate(callback);
|
||||
callback();
|
||||
}
|
||||
/** @internal */
|
||||
_flush(callback) {
|
||||
@@ -124,21 +151,26 @@ export default class MessageParser extends Transform {
|
||||
// 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);
|
||||
const rawLines = (this.rawHeaders || Buffer.alloc(0)).toString('binary').split(/\r?\n/);
|
||||
// Unfold in a single forward pass and only ever test a freshly split line for the
|
||||
// continuation prefix. Testing an already merged line instead would rescan a string
|
||||
// that grows with every continuation line, so a header folded into many continuation
|
||||
// lines (a large recipient list, for example) would take quadratic time to unfold
|
||||
const lines = [];
|
||||
for (const rawLine of rawLines) {
|
||||
if (lines.length && /^[ \t]/.test(rawLine)) {
|
||||
lines[lines.length - 1] += '\n' + rawLine;
|
||||
}
|
||||
else {
|
||||
lines.push(rawLine);
|
||||
}
|
||||
}
|
||||
return lines
|
||||
.filter(line => /[^ \t\r]/.test(line))
|
||||
.map(line => ({
|
||||
key: line
|
||||
.substr(0, line.indexOf(':'))
|
||||
.replace(/^[ \t]+|[ \t]+$/g, '')
|
||||
.toLowerCase(),
|
||||
key: _trimFieldName(line.substr(0, line.indexOf(':'))).toLowerCase(),
|
||||
line
|
||||
}));
|
||||
}
|
||||
}
|
||||
export default MessageParser;
|
||||
+10
-1
@@ -17,7 +17,7 @@ export interface RelaxedBodyOptions {
|
||||
* 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 {
|
||||
declare class RelaxedBody extends Transform {
|
||||
bodyHash: crypto.Hash;
|
||||
/** Bytes of the original body seen so far */
|
||||
byteLength: number;
|
||||
@@ -25,3 +25,12 @@ export default class RelaxedBody extends Transform {
|
||||
constructor(options?: RelaxedBodyOptions);
|
||||
updateHash(chunk: Buffer, final?: boolean): void;
|
||||
}
|
||||
/**
|
||||
* Type aliases in the layout of @types/nodemailer, so `RelaxedBody.Options` style references keep working
|
||||
*/
|
||||
declare namespace RelaxedBody {
|
||||
type Options = RelaxedBodyOptions;
|
||||
}
|
||||
/** The same alias as a module level export, for `import * as RelaxedBody` and `import RelaxedBody = require()` */
|
||||
export type { RelaxedBodyOptions as Options };
|
||||
export default RelaxedBody;
|
||||
+2
-1
@@ -16,7 +16,7 @@ const EMPTY_LINES = Buffer.alloc(4096, CRLF);
|
||||
* 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 {
|
||||
class RelaxedBody extends Transform {
|
||||
constructor(options) {
|
||||
super();
|
||||
options = options || {};
|
||||
@@ -138,3 +138,4 @@ export default class RelaxedBody extends Transform {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
export default RelaxedBody;
|
||||
+3
@@ -1,4 +1,5 @@
|
||||
import crypto from 'node:crypto';
|
||||
import type { NodemailerError } from '../errors.js';
|
||||
import type { MessageParserHeaderLine } from './message-parser.js';
|
||||
/**
|
||||
* Private key accepted by crypto.Sign#sign: a PEM string, a Buffer, a KeyObject or an
|
||||
@@ -35,6 +36,7 @@ export interface DKIMRelaxedHeaders {
|
||||
* Returns DKIM signature header line
|
||||
*
|
||||
* @param headers Parsed headers object from MessageParser
|
||||
* @param hashAlgo Hash algorithm the body hash was calculated with, for example "sha256"
|
||||
* @param bodyHash Base64 encoded hash of the message
|
||||
* @param options DKIM options
|
||||
* @param options.domainName Domain name to be signed for
|
||||
@@ -45,5 +47,6 @@ export interface DKIMRelaxedHeaders {
|
||||
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;
|
||||
var unsupportedHashAlgoError: (hashAlgo: string) => NodemailerError;
|
||||
}
|
||||
export default sign;
|
||||
+18
-1
@@ -1,10 +1,20 @@
|
||||
import * as punycode from '../punycode/index.js';
|
||||
import * as mimeFuncs from '../mime-funcs/index.js';
|
||||
import crypto from 'node:crypto';
|
||||
import * as errors from '../errors.js';
|
||||
/**
|
||||
* Error for a hashAlgo value the crypto module does not know
|
||||
*/
|
||||
function unsupportedHashAlgoError(hashAlgo) {
|
||||
const err = new Error('Unsupported DKIM hash algorithm "' + hashAlgo + '"');
|
||||
err.code = errors.ECONFIG;
|
||||
return err;
|
||||
}
|
||||
/**
|
||||
* Returns DKIM signature header line
|
||||
*
|
||||
* @param headers Parsed headers object from MessageParser
|
||||
* @param hashAlgo Hash algorithm the body hash was calculated with, for example "sha256"
|
||||
* @param bodyHash Base64 encoded hash of the message
|
||||
* @param options DKIM options
|
||||
* @param options.domainName Domain name to be signed for
|
||||
@@ -25,7 +35,13 @@ function sign(headers, hashAlgo, bodyHash, options) {
|
||||
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 = crypto.createSign(('rsa-' + hashAlgo).toUpperCase());
|
||||
let signer;
|
||||
try {
|
||||
signer = crypto.createSign(('rsa-' + hashAlgo).toUpperCase());
|
||||
}
|
||||
catch (_E) {
|
||||
throw unsupportedHashAlgoError(hashAlgo);
|
||||
}
|
||||
// the header lines are 'binary' strings, so this reproduces the original header bytes
|
||||
signer.update(canonicalizedHeaderData.headers, 'latin1');
|
||||
let signature;
|
||||
@@ -38,6 +54,7 @@ function sign(headers, hashAlgo, bodyHash, options) {
|
||||
return dkimHeader + signature.replace(/(^.{73}|.{75}(?!\r?\n|\r))/g, '$&\r\n ').trim();
|
||||
}
|
||||
sign.relaxedHeaders = relaxedHeaders;
|
||||
sign.unsupportedHashAlgoError = unsupportedHashAlgoError;
|
||||
export default sign;
|
||||
function generateDKIMHeader(domainName, keySelector, fieldNames, hashAlgo, bodyHash) {
|
||||
// the caller supplied tag values are interpolated straight into the tag list, and none of
|
||||
|
||||
+22
-4
@@ -61,14 +61,28 @@ export declare const EPROXY = "EPROXY";
|
||||
export declare const EFILEACCESS = "EFILEACCESS";
|
||||
export declare const EURLACCESS = "EURLACCESS";
|
||||
export declare const EFETCH = "EFETCH";
|
||||
/**
|
||||
* Tells whether the error ended the connection rather than rejected what was sent over it
|
||||
*/
|
||||
export declare const isTransientError: (err: {
|
||||
code?: string | undefined;
|
||||
responseCode?: number | undefined;
|
||||
}) => boolean;
|
||||
/**
|
||||
* Code Node.js sets on an error when its permission model (`--permission`) denies an
|
||||
* operation. It is not replaced with the Nodemailer code of the failing step (`ESOCKET`,
|
||||
* `EDNS`, `EFETCH`, ...), so a missing grant such as `--allow-net` stays recognizable.
|
||||
*/
|
||||
export declare const ERR_ACCESS_DENIED = "ERR_ACCESS_DENIED";
|
||||
/**
|
||||
* An Error together with the properties Nodemailer attaches to the errors it
|
||||
* hands to callers. Every property is optional, the set that is present
|
||||
* depends on where the error originated.
|
||||
* depends on where the error originated. Socket level errors keep the errno
|
||||
* and syscall fields Node.js sets on them. `code` (see ERROR_CODES) is
|
||||
* inherited from ErrnoException, not redeclared, so it follows the
|
||||
* consumer's @types/node.
|
||||
*/
|
||||
export interface NodemailerError extends Error {
|
||||
/** Nodemailer error code, see ERROR_CODES */
|
||||
code?: string | undefined;
|
||||
export interface NodemailerError extends NodeJS.ErrnoException {
|
||||
/** SMTP command that was in flight when the server replied with an error */
|
||||
command?: string | undefined;
|
||||
/** Raw SMTP server response */
|
||||
@@ -83,6 +97,10 @@ export interface NodemailerError extends Error {
|
||||
rejected?: string[] | undefined;
|
||||
/** Per-recipient errors for the rejected addresses */
|
||||
rejectedErrors?: NodemailerError[] | undefined;
|
||||
/** The code the error had before Nodemailer set its own, such as ECONNREFUSED for an ESOCKET error */
|
||||
originalCode?: string | undefined;
|
||||
/** Which wait an ETIMEDOUT error ended: connecting, the greeting, a TLS upgrade or an idle socket */
|
||||
timeoutType?: 'CONNECT_TIMEOUT' | 'GREETING_TIMEOUT' | 'UPGRADE_TIMEOUT' | 'SOCKET_TIMEOUT' | undefined;
|
||||
}
|
||||
/**
|
||||
* Node style callback: called with an error, or with null and the result
|
||||
|
||||
+10
@@ -65,3 +65,13 @@ export const EPROXY = 'EPROXY';
|
||||
export const EFILEACCESS = 'EFILEACCESS';
|
||||
export const EURLACCESS = 'EURLACCESS';
|
||||
export const EFETCH = 'EFETCH';
|
||||
/**
|
||||
* Tells whether the error ended the connection rather than rejected what was sent over it
|
||||
*/
|
||||
export const isTransientError = (err) => err.responseCode === 421 || err.code === ECONNECTION || err.code === ESOCKET || err.code === ETIMEDOUT;
|
||||
/**
|
||||
* Code Node.js sets on an error when its permission model (`--permission`) denies an
|
||||
* operation. It is not replaced with the Nodemailer code of the failing step (`ESOCKET`,
|
||||
* `EDNS`, `EFETCH`, ...), so a missing grant such as `--allow-net` stays recognizable.
|
||||
*/
|
||||
export const ERR_ACCESS_DENIED = 'ERR_ACCESS_DENIED';
|
||||
+12
-1
@@ -17,13 +17,14 @@ export interface Cookie {
|
||||
secure?: boolean | undefined;
|
||||
httponly?: boolean | undefined;
|
||||
}
|
||||
type CookieEntry = Cookie;
|
||||
/**
|
||||
* Creates a biskviit cookie jar for managing cookie values in memory
|
||||
*
|
||||
* @constructor
|
||||
* @param [options] Optional options object
|
||||
*/
|
||||
export default class Cookies {
|
||||
declare class Cookies {
|
||||
options: CookiesOptions;
|
||||
cookies: Cookie[];
|
||||
constructor(options?: CookiesOptions);
|
||||
@@ -94,3 +95,13 @@ export default class Cookies {
|
||||
*/
|
||||
getPath(pathname?: string | null): string;
|
||||
}
|
||||
/**
|
||||
* Type aliases in the layout of @types/nodemailer, so `Cookies.Cookie` style references keep working
|
||||
*/
|
||||
declare namespace Cookies {
|
||||
type Options = CookiesOptions;
|
||||
type Cookie = CookieEntry;
|
||||
}
|
||||
/** The same alias as a module level export, for `import * as Cookies` and `import Cookies = require()` */
|
||||
export type { CookiesOptions as Options };
|
||||
export default Cookies;
|
||||
+20
-12
@@ -8,7 +8,7 @@ const SESSION_TIMEOUT = 1800; // 30 min
|
||||
* @constructor
|
||||
* @param [options] Optional options object
|
||||
*/
|
||||
export default class Cookies {
|
||||
class Cookies {
|
||||
constructor(options) {
|
||||
this.options = options || {};
|
||||
this.cookies = [];
|
||||
@@ -92,19 +92,31 @@ export default class Cookies {
|
||||
*/
|
||||
parse(cookieStr) {
|
||||
const cookie = {};
|
||||
let hasNameValue = false;
|
||||
(cookieStr || '')
|
||||
.toString()
|
||||
.split(';')
|
||||
.forEach(cookiePart => {
|
||||
const valueParts = cookiePart.split('=');
|
||||
const key = valueParts.shift().trim().toLowerCase();
|
||||
let value = valueParts.join('=').trim();
|
||||
let domain;
|
||||
if (!key) {
|
||||
if (!cookiePart.trim()) {
|
||||
// skip empty parts
|
||||
return;
|
||||
}
|
||||
switch (key) {
|
||||
const valueParts = cookiePart.split('=');
|
||||
const name = valueParts.shift().trim();
|
||||
let value = valueParts.join('=').trim();
|
||||
let domain;
|
||||
// the first part is always the name-value pair, so a cookie named
|
||||
// like an attribute is not mistaken for one (RFC 6265 section 5.2)
|
||||
if (!hasNameValue) {
|
||||
hasNameValue = true;
|
||||
if (name) {
|
||||
// cookie names are case-sensitive, only attribute names are not
|
||||
cookie.name = name;
|
||||
cookie.value = value;
|
||||
}
|
||||
return;
|
||||
}
|
||||
switch (name.toLowerCase()) {
|
||||
case 'expires': {
|
||||
const expires = new Date(value);
|
||||
// ignore date if can not parse it
|
||||
@@ -132,11 +144,6 @@ export default class Cookies {
|
||||
case 'httponly':
|
||||
cookie.httponly = true;
|
||||
break;
|
||||
default:
|
||||
if (!cookie.name) {
|
||||
cookie.name = key;
|
||||
cookie.value = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
return cookie;
|
||||
@@ -239,3 +246,4 @@ export default class Cookies {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
export default Cookies;
|
||||
+20
-1
@@ -25,8 +25,12 @@ export interface FetchOptions {
|
||||
tls?: {
|
||||
[key: string]: any;
|
||||
} | undefined;
|
||||
/** Request timeout in milliseconds */
|
||||
/** Socket inactivity timeout in milliseconds, defaults to 60000, 0 disables it */
|
||||
timeout?: number | undefined;
|
||||
/** Time limit for the whole request in milliseconds, redirects included, defaults to 120000, 0 disables it */
|
||||
maxTime?: number | undefined;
|
||||
/** Maximum size of the (decoded) response body in bytes, defaults to 64 MB, Infinity disables the limit */
|
||||
maxBytes?: number | undefined;
|
||||
/** Maximum number of redirects to follow (default 5) */
|
||||
maxRedirects?: number | undefined;
|
||||
/** Resolve responses with a status code of 300 or above instead of emitting an error */
|
||||
@@ -35,6 +39,8 @@ export interface FetchOptions {
|
||||
redirects?: number | undefined;
|
||||
/** Response stream shared across redirects, set by nmfetch itself */
|
||||
fetchRes?: FetchResponse | undefined;
|
||||
/** Timestamp the whole request has to be done by, shared across redirects, set by nmfetch itself */
|
||||
deadline?: number | undefined;
|
||||
}
|
||||
/**
|
||||
* The stream nmfetch returns. The response body is piped into it, the status code and
|
||||
@@ -47,5 +53,18 @@ export interface FetchResponse extends PassThrough {
|
||||
declare function nmfetch(url: string, options?: FetchOptions): FetchResponse;
|
||||
declare namespace nmfetch {
|
||||
var Cookies: typeof import("./cookies.js").default;
|
||||
var DEFAULT_TIMEOUT: number;
|
||||
var DEFAULT_MAX_TIME: number;
|
||||
}
|
||||
type CookiesJar = Cookies;
|
||||
/**
|
||||
* Type aliases in the layout of @types/nodemailer, so `fetch.Options` style references keep working
|
||||
*/
|
||||
declare namespace nmfetch {
|
||||
type Options = FetchOptions;
|
||||
type WritableResponse = FetchResponse;
|
||||
type Cookies = CookiesJar;
|
||||
}
|
||||
/** The same aliases as module level exports, for `import * as fetch` and `import fetch = require()` */
|
||||
export type { FetchOptions as Options, FetchResponse as WritableResponse, Cookies };
|
||||
export default nmfetch;
|
||||
+107
-76
@@ -9,6 +9,14 @@ import net from 'node:net';
|
||||
import * as errors from '../errors.js';
|
||||
import { isProtoKey } from '../shared/objects.js';
|
||||
const MAX_REDIRECTS = 5;
|
||||
// a stalled server would otherwise hold the request, and whatever waits on it, open forever
|
||||
const DEFAULT_TIMEOUT = 60 * 1000;
|
||||
// the inactivity timeout above is reset by every byte, so a server dripping one within each
|
||||
// window could still hold the request, and the message being built from it, open for as
|
||||
// long as it liked. The whole request, redirects included, has to be done within this
|
||||
const DEFAULT_MAX_TIME = 2 * 60 * 1000;
|
||||
// the body is usually buffered in memory by the caller, so an unbounded download is refused
|
||||
const DEFAULT_MAX_BYTES = 64 * 1024 * 1024;
|
||||
// Only genuine TLS settings are taken from options.tls. That object reaches us straight
|
||||
// from a user supplied attachment (content.tls), so keys like host, port, path, socketPath
|
||||
// or lookup would otherwise repoint the request at a destination that never went through
|
||||
@@ -39,6 +47,17 @@ const TLS_OPTION_KEYS = [
|
||||
'sessionIdContext',
|
||||
'sigalgs'
|
||||
];
|
||||
/**
|
||||
* Marks an error of the request as a fetch error, unless it is a permission model
|
||||
* denial, which keeps its own code (see ERR_ACCESS_DENIED)
|
||||
*
|
||||
* @param err Error to mark
|
||||
*/
|
||||
function setFetchCode(err) {
|
||||
if (err.code !== errors.ERR_ACCESS_DENIED) {
|
||||
err.code = errors.EFETCH;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Resolves a URL only if it is one this module is willing to request.
|
||||
*
|
||||
@@ -63,12 +82,22 @@ function parseFetchUrl(url) {
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
// the port a request to the URL goes to, explicit or the default of its scheme
|
||||
function fetchPort(parsed) {
|
||||
return parsed.port ? Number(parsed.port) : parsed.protocol === 'https:' ? 443 : 80;
|
||||
}
|
||||
function nmfetch(url, options) {
|
||||
options = options || {};
|
||||
options.fetchRes = options.fetchRes || new PassThrough();
|
||||
options.cookies = options.cookies || new Cookies();
|
||||
options.redirects = options.redirects || 0;
|
||||
options.maxRedirects = isNaN(options.maxRedirects) ? MAX_REDIRECTS : options.maxRedirects;
|
||||
if (!options.deadline) {
|
||||
options.maxTime = typeof options.maxTime === 'number' && options.maxTime >= 0 ? options.maxTime : DEFAULT_MAX_TIME;
|
||||
// the one deadline is shared with the requests the redirects lead to, every hop
|
||||
// gets the time that is left rather than starting over
|
||||
options.deadline = options.maxTime ? Date.now() + options.maxTime : 0;
|
||||
}
|
||||
const fetchRes = options.fetchRes;
|
||||
const parsed = parseFetchUrl(url);
|
||||
if (!parsed) {
|
||||
@@ -135,7 +164,7 @@ function nmfetch(url, options) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
err.code = errors.EFETCH;
|
||||
setFetchCode(err);
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
});
|
||||
@@ -155,14 +184,14 @@ function nmfetch(url, options) {
|
||||
.join('&'));
|
||||
}
|
||||
catch (E) {
|
||||
if (finished) {
|
||||
return undefined;
|
||||
}
|
||||
// the caller attaches its error listener once nmfetch has returned, so
|
||||
// the error is emitted on the next tick and the stream is handed back
|
||||
// the way every other failure is reported
|
||||
finished = true;
|
||||
E.code = errors.EFETCH;
|
||||
E.sourceUrl = url;
|
||||
fetchRes.emit('error', E);
|
||||
return undefined;
|
||||
setImmediate(() => fetchRes.emit('error', E));
|
||||
return fetchRes;
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -179,7 +208,7 @@ function nmfetch(url, options) {
|
||||
method,
|
||||
host: parsed.hostname,
|
||||
path: parsed.path,
|
||||
port: parsed.port ? parsed.port : parsed.protocol === 'https:' ? 443 : 80,
|
||||
port: fetchPort(parsed),
|
||||
headers,
|
||||
// Validate TLS certificates by default. Callers that genuinely need to
|
||||
// reach a self-signed/internal host opt out explicitly with
|
||||
@@ -208,33 +237,52 @@ function nmfetch(url, options) {
|
||||
catch (E) {
|
||||
finished = true;
|
||||
setImmediate(() => {
|
||||
E.code = errors.EFETCH;
|
||||
setFetchCode(E);
|
||||
E.sourceUrl = url;
|
||||
fetchRes.emit('error', E);
|
||||
});
|
||||
return fetchRes;
|
||||
}
|
||||
if (options.timeout) {
|
||||
req.setTimeout(options.timeout, () => {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
req.abort();
|
||||
const err = new Error('Request Timeout');
|
||||
err.code = errors.EFETCH;
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
});
|
||||
}
|
||||
req.on('error', (err) => {
|
||||
// the whole request has to be done by options.deadline, see maxTime. The timer is
|
||||
// released on every way out of the request, so it never outlives it
|
||||
let deadlineTimer;
|
||||
const clearDeadline = () => {
|
||||
if (deadlineTimer) {
|
||||
clearTimeout(deadlineTimer);
|
||||
deadlineTimer = undefined;
|
||||
}
|
||||
};
|
||||
// reports the first failure of this request on fetchRes and releases the request
|
||||
const fail = (err, sourceUrl = url) => {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
err.code = errors.EFETCH;
|
||||
err.sourceUrl = url;
|
||||
clearDeadline();
|
||||
setFetchCode(err);
|
||||
err.sourceUrl = sourceUrl;
|
||||
fetchRes.emit('error', err);
|
||||
req.abort();
|
||||
};
|
||||
const timeout = typeof options.timeout === 'number' && options.timeout >= 0 ? options.timeout : DEFAULT_TIMEOUT;
|
||||
if (timeout) {
|
||||
req.setTimeout(timeout, () => fail(new Error('Request Timeout')));
|
||||
}
|
||||
if (options.deadline) {
|
||||
deadlineTimer = setTimeout(() => fail(new Error('Request exceeded the allowed ' + options.maxTime + ' ms')), Math.max(options.deadline - Date.now(), 1));
|
||||
// a pending deadline is no reason to keep the process alive
|
||||
deadlineTimer.unref();
|
||||
}
|
||||
req.on('error', (err) => fail(err));
|
||||
// a consumer that destroys the response stream before it ended does not want the rest of
|
||||
// the body, release the request now instead of when the timeout fires
|
||||
fetchRes.once('close', () => {
|
||||
clearDeadline();
|
||||
if (finished || fetchRes.readableEnded) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
req.destroy();
|
||||
});
|
||||
req.on('response', res => {
|
||||
let inflate;
|
||||
@@ -256,13 +304,7 @@ function nmfetch(url, options) {
|
||||
// redirect
|
||||
options.redirects++;
|
||||
if (options.redirects > options.maxRedirects) {
|
||||
finished = true;
|
||||
const err = new Error('Maximum redirect count exceeded');
|
||||
err.code = errors.EFETCH;
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
req.abort();
|
||||
return;
|
||||
return fail(new Error('Maximum redirect count exceeded'));
|
||||
}
|
||||
// redirect does not include POST body
|
||||
options.method = 'GET';
|
||||
@@ -282,64 +324,55 @@ function nmfetch(url, options) {
|
||||
// call: that call gets its own `finished` flag and no handle on this
|
||||
// request, so this one would stay open and could emit a second error on
|
||||
// the shared fetchRes once it times out. Callers listen with req.once().
|
||||
finished = true;
|
||||
const err = new Error('Unsupported protocol for URL ' + redirectUrl);
|
||||
err.code = errors.EFETCH;
|
||||
err.sourceUrl = redirectUrl;
|
||||
fetchRes.emit('error', err);
|
||||
req.abort();
|
||||
return;
|
||||
return fail(new Error('Unsupported protocol for URL ' + redirectUrl), redirectUrl);
|
||||
}
|
||||
// Do not forward credentials when the redirect leaves the original
|
||||
// security context: a different host, or a downgrade from https to
|
||||
// http (which would otherwise put them on the wire in cleartext).
|
||||
// Strip sensitive request headers so an attacker who controls the
|
||||
// redirect target cannot harvest them.
|
||||
const crossHost = redirectParsed.hostname !== parsed.hostname;
|
||||
const downgrade = parsed.protocol === 'https:' && redirectParsed.protocol === 'http:';
|
||||
if (options.headers && (crossHost || downgrade)) {
|
||||
// Do not forward credentials when the redirect leaves the origin: a different
|
||||
// scheme, host or port is a different security context (an https to http
|
||||
// downgrade would also put them on the wire in cleartext). Strip sensitive
|
||||
// request headers so an attacker who controls the redirect target cannot
|
||||
// harvest them.
|
||||
const crossOrigin = redirectParsed.protocol !== parsed.protocol ||
|
||||
redirectParsed.hostname !== parsed.hostname ||
|
||||
fetchPort(redirectParsed) !== fetchPort(parsed);
|
||||
if (options.headers && crossOrigin) {
|
||||
const sensitive = ['authorization', 'cookie', 'proxy-authorization'];
|
||||
Object.keys(options.headers).forEach(key => {
|
||||
if (sensitive.includes(key.toLowerCase())) {
|
||||
// normalized the way the request headers are built above, a padded name is sent too
|
||||
if (sensitive.includes(key.toLowerCase().trim())) {
|
||||
delete options.headers[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
// this request is done with, release it so its socket and timeout do not
|
||||
// outlive it and report a late error on the shared fetchRes
|
||||
finished = true;
|
||||
clearDeadline();
|
||||
res.resume();
|
||||
req.abort();
|
||||
return nmfetch(redirectUrl, options);
|
||||
}
|
||||
fetchRes.statusCode = res.statusCode;
|
||||
fetchRes.headers = res.headers;
|
||||
if (res.statusCode >= 300 && !options.allowErrorResponse) {
|
||||
finished = true;
|
||||
const err = new Error('Invalid status code ' + res.statusCode);
|
||||
err.code = errors.EFETCH;
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
req.abort();
|
||||
return;
|
||||
return fail(new Error('Invalid status code ' + res.statusCode));
|
||||
}
|
||||
res.on('error', (err) => {
|
||||
if (finished) {
|
||||
res.on('error', (err) => fail(err));
|
||||
// the response is in, whatever is left of it is in memory and no longer on the network
|
||||
res.once('end', () => clearDeadline());
|
||||
const maxBytes = typeof options.maxBytes === 'number' && options.maxBytes > 0 ? options.maxBytes : DEFAULT_MAX_BYTES;
|
||||
const source = inflate || res;
|
||||
let received = 0;
|
||||
source.on('data', (chunk) => {
|
||||
received += chunk.length;
|
||||
if (received <= maxBytes || finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
err.code = errors.EFETCH;
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
req.abort();
|
||||
source.unpipe(fetchRes);
|
||||
fail(new Error('Response size exceeds the allowed ' + maxBytes + ' bytes'));
|
||||
});
|
||||
if (inflate) {
|
||||
res.pipe(inflate).pipe(fetchRes);
|
||||
inflate.on('error', (err) => {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
err.code = errors.EFETCH;
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
req.abort();
|
||||
});
|
||||
inflate.on('error', (err) => fail(err));
|
||||
}
|
||||
else {
|
||||
res.pipe(fetchRes);
|
||||
@@ -354,11 +387,7 @@ function nmfetch(url, options) {
|
||||
req.write(body);
|
||||
}
|
||||
catch (err) {
|
||||
finished = true;
|
||||
err.code = errors.EFETCH;
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
return;
|
||||
return fail(err);
|
||||
}
|
||||
}
|
||||
req.end();
|
||||
@@ -366,4 +395,6 @@ function nmfetch(url, options) {
|
||||
return fetchRes;
|
||||
}
|
||||
nmfetch.Cookies = Cookies;
|
||||
nmfetch.DEFAULT_TIMEOUT = DEFAULT_TIMEOUT;
|
||||
nmfetch.DEFAULT_MAX_TIME = DEFAULT_MAX_TIME;
|
||||
export default nmfetch;
|
||||
+14
-4
@@ -1,6 +1,6 @@
|
||||
import type { Logger } from '../shared/index.js';
|
||||
import type { MimeNodeEnvelope } from '../mime-node/index.js';
|
||||
import type { default as MailMessage, MailMessageData } from '../mailer/mail-message.js';
|
||||
import type MailMessage from '../mailer/mail-message.js';
|
||||
import type { default as Mail, SentMessageInfo, SendMailOptions, TransportOptions } from '../mailer/index.js';
|
||||
/**
|
||||
* Options for the JSON transport
|
||||
@@ -12,16 +12,23 @@ export interface JSONTransportOptions extends TransportOptions {
|
||||
skipEncoding?: boolean | undefined;
|
||||
}
|
||||
/**
|
||||
* The value the JSON transport hands to the send callback
|
||||
* The value the JSON transport hands to the send callback. M is the type of the message
|
||||
* field: a JSON string by default, the message object itself when skipEncoding is set
|
||||
*/
|
||||
export interface JSONSentMessageInfo extends SentMessageInfo {
|
||||
export interface JSONSentMessageInfo<M = string> extends SentMessageInfo {
|
||||
/** The envelope the message was generated with */
|
||||
envelope: MimeNodeEnvelope;
|
||||
/** Message-ID value of the message */
|
||||
messageId: string;
|
||||
/** The normalized message as a JSON string, or as the object itself when skipEncoding is set */
|
||||
message: string | MailMessageData;
|
||||
message: M;
|
||||
}
|
||||
/**
|
||||
* The value the JSON transport hands to the send callback when skipEncoding is set: the
|
||||
* message field holds the message object. Typed loosely so that a transporter created with
|
||||
* skipEncoding still fits a variable declared with the plain result type
|
||||
*/
|
||||
export type JSONSentMessageObjectInfo = JSONSentMessageInfo<any>;
|
||||
/**
|
||||
* Generates a Transport object to generate JSON output
|
||||
*
|
||||
@@ -50,5 +57,8 @@ declare namespace JSONTransport {
|
||||
type Options = JSONTransportOptions;
|
||||
type MailOptions = SendMailOptions;
|
||||
type SentMessageInfo = JSONSentMessageInfo;
|
||||
type SentMessageObjectInfo = JSONSentMessageObjectInfo;
|
||||
}
|
||||
/** The same aliases as module level exports, for `import * as JSONTransport` and `import JSONTransport = require()` */
|
||||
export type { JSONTransportOptions as Options, SendMailOptions as MailOptions, JSONSentMessageInfo as SentMessageInfo, JSONSentMessageObjectInfo as SentMessageObjectInfo };
|
||||
export default JSONTransport;
|
||||
+2
@@ -48,6 +48,8 @@ class JSONTransport {
|
||||
}
|
||||
delete data.envelope;
|
||||
delete data.normalizedHeaders;
|
||||
// the message field is the object itself with skipEncoding, the
|
||||
// createTransport overload for that option types the result accordingly
|
||||
return done(null, {
|
||||
envelope,
|
||||
messageId,
|
||||
|
||||
+8
-3
@@ -72,6 +72,11 @@ export type MailComposerListHeaderEntry = string | {
|
||||
export interface MailComposerListHeaders {
|
||||
[key: string]: MailComposerListHeaderEntry | (MailComposerListHeaderEntry | MailComposerListHeaderEntry[])[];
|
||||
}
|
||||
/**
|
||||
* Encoding for the non-ascii header values: quoted-printable ('Q', the default) or base64
|
||||
* ('B'). Only the first letter is read, so the short forms work as well
|
||||
*/
|
||||
export type MailComposerTextEncoding = 'quoted-printable' | 'base64' | 'Q' | 'B';
|
||||
/**
|
||||
* Mail options, the message data MailComposer builds the MIME tree from. The address
|
||||
* fields, subject, messageId, date, inReplyTo and references become headers of the root
|
||||
@@ -114,7 +119,7 @@ export interface MailComposerOptions {
|
||||
/** Content-Transfer-Encoding to force for the text/* nodes that do not set their own */
|
||||
encoding?: string | undefined;
|
||||
/** Header string encoding, 'Q' (the default) or 'B', 'quoted-printable' and 'base64' are accepted as well */
|
||||
textEncoding?: string | undefined;
|
||||
textEncoding?: MailComposerTextEncoding | undefined;
|
||||
/** Pregenerated rfc822 message, used as is instead of building one */
|
||||
raw?: MimeNodeContent | undefined;
|
||||
/** Reject content that points to a URL */
|
||||
@@ -129,8 +134,6 @@ export interface MailComposerOptions {
|
||||
baseBoundary?: string | undefined;
|
||||
/** 'win' for CRLF and 'linux' for LF line breaks in the generated message, kept as is when not set */
|
||||
newline?: string | undefined;
|
||||
/** Keep the Bcc header in the generated message, listed for completeness, the transports set it on the message directly */
|
||||
keepBcc?: boolean | undefined;
|
||||
/** Method to normalize header keys for custom caseing */
|
||||
normalizeHeaderKey?: MimeNodeOptions['normalizeHeaderKey'] | undefined;
|
||||
/** 'high', 'normal' or 'low', sets the priority headers, read by the mailer */
|
||||
@@ -166,4 +169,6 @@ declare class MailComposer {
|
||||
declare namespace MailComposer {
|
||||
type Options = MailComposerOptions;
|
||||
}
|
||||
/** The same alias as a module level export, for `import * as MailComposer` and `import MailComposer = require()` */
|
||||
export type { MailComposerOptions as Options };
|
||||
export default MailComposer;
|
||||
+3
-1
@@ -113,8 +113,10 @@ class MailComposer {
|
||||
data.filename = attachment.filename;
|
||||
}
|
||||
else if (!isMessageNode && attachment.filename !== false) {
|
||||
// a backslash separates as well, so a Windows path does not put the sender's directories in the headers
|
||||
data.filename =
|
||||
(attachment.path || attachment.href || '').split('/').pop().split('?').shift() || 'attachment-' + (i + 1);
|
||||
(attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() ||
|
||||
'attachment-' + (i + 1);
|
||||
if (data.filename.indexOf('.') < 0) {
|
||||
data.filename += '.' + mimeFuncs.detectExtension(data.contentType);
|
||||
}
|
||||
|
||||
+43
-25
@@ -4,8 +4,8 @@ import DKIM, { type DKIMOptions } from '../dkim/index.js';
|
||||
import MailMessage, { type MailDefaults, type SendMailOptions } from './mail-message.js';
|
||||
import net from 'node:net';
|
||||
import type { ConnectionOptions } from 'node:tls';
|
||||
import type { MailComposerAlternative, MailComposerAttachment, MailComposerIcalEvent, MailComposerListHeaderEntry, MailComposerListHeaders } from '../mail-composer/index.js';
|
||||
import type { NodemailerError, ResultCallback } from '../errors.js';
|
||||
import type { MailComposerAlternative, MailComposerAttachment, MailComposerIcalEvent, MailComposerListHeaderEntry, MailComposerListHeaders, MailComposerTextEncoding } from '../mail-composer/index.js';
|
||||
import type { Callback, NodemailerError, ResultCallback } from '../errors.js';
|
||||
import type { ParsedUrl } from '../shared/url.js';
|
||||
import type { MimeNodeAddress, MimeNodeEnvelope, MimeNodeEnvelopeInput, MimeNodeHeaders, MimeNodeOptions } from '../mime-node/index.js';
|
||||
import type { XOAuth2ProvisionCallback } from '../xoauth2/index.js';
|
||||
@@ -15,9 +15,10 @@ export type { default as MailMessage } from './mail-message.js';
|
||||
* The base shape of the object a transport hands back for a sent message. Every bundled
|
||||
* transport sets the envelope and the Message-ID, the rest depends on the transport.
|
||||
*
|
||||
* The index signature keeps a transport specific field readable through this type, and it
|
||||
* is also what a result type has to inherit to stay assignable to it, so the result type
|
||||
* of a transport outside this package has to extend this interface rather than restate it
|
||||
* The index signature keeps a transport specific field readable through this type, the way
|
||||
* the `any` typed result of @types/nodemailer was, and it is also what a result type has
|
||||
* to inherit to stay assignable to it, so the result type of a transport outside this
|
||||
* package has to extend this interface rather than restate it
|
||||
*/
|
||||
export interface SentMessageInfo {
|
||||
/** The envelope the message was sent with */
|
||||
@@ -33,9 +34,9 @@ export interface SentMessageInfo {
|
||||
/** Last response from the server */
|
||||
response?: string | undefined;
|
||||
/** The generated message, for the transports that hand it back instead of sending it */
|
||||
message?: unknown;
|
||||
message?: any;
|
||||
/** Transport specific fields */
|
||||
[key: string]: unknown;
|
||||
[key: string]: any;
|
||||
}
|
||||
/**
|
||||
* Callback for sendMail, receives the transport result once the transport has taken the
|
||||
@@ -43,9 +44,11 @@ export interface SentMessageInfo {
|
||||
*/
|
||||
export type SendMailCallback<T = SentMessageInfo> = (err: NodemailerError | null, info: T) => void;
|
||||
/**
|
||||
* Callback for verify(), success is true once the transport accepted the configuration
|
||||
* Callback for verify(), success is true once the transport accepted the configuration.
|
||||
* Declared with a required success value, the way @types/nodemailer declared it, the
|
||||
* error path hands over the error alone
|
||||
*/
|
||||
export type VerifyCallback = (err: NodemailerError | null, success?: true) => void;
|
||||
export type VerifyCallback = Callback<true>;
|
||||
/**
|
||||
* Callback a plugin calls once it is done, an error aborts the send
|
||||
*/
|
||||
@@ -60,21 +63,23 @@ export type PluginFunction<T = SentMessageInfo> = (mail: MailMessage<T>, callbac
|
||||
*/
|
||||
export interface GetSocketOptions {
|
||||
host?: string | undefined;
|
||||
port?: number | string | undefined;
|
||||
port?: number | undefined;
|
||||
[key: string]: any;
|
||||
}
|
||||
/**
|
||||
* The result of a getSocket handler, the socket to use for the connection
|
||||
* The result of a getSocket handler, the socket to use for the connection. The object is
|
||||
* merged into the connection options, so it may carry any of those as well
|
||||
*/
|
||||
export interface SocketOptions {
|
||||
/** An established socket, the proxied connection */
|
||||
connection?: net.Socket | undefined;
|
||||
[key: string]: any;
|
||||
}
|
||||
/**
|
||||
* Receives the socket options from a getSocket handler, or the error that prevented the
|
||||
* connection
|
||||
* Receives the socket options from a getSocket handler, false when a new socket should be
|
||||
* opened, or the error that prevented the connection
|
||||
*/
|
||||
export type GetSocketCallback = (err: NodemailerError | null, socketOptions?: SocketOptions) => void;
|
||||
export type GetSocketCallback = (err: Error | null, socketOptions?: SocketOptions | false) => void;
|
||||
/**
|
||||
* A socket handler. Mail sets one on the transport as getSocket when a proxy is configured,
|
||||
* the SMTP transports call it to get a proxied socket instead of connecting directly
|
||||
@@ -101,9 +106,12 @@ export interface MailMeta {
|
||||
/**
|
||||
* A transport as consumed by Mail: any object with a name, a version and a send method
|
||||
* works, the rest is optional. Mail forwards its close, isIdle and verify calls to the
|
||||
* methods of the same name as they are, so their arguments are up to the transport
|
||||
* methods of the same name as they are, so their arguments are up to the transport.
|
||||
*
|
||||
* D is the options type of the transport, the second type parameter @types/nodemailer
|
||||
* declared on Transport, Transporter and Mail
|
||||
*/
|
||||
export interface Transport<T = SentMessageInfo> {
|
||||
export interface Transport<T = SentMessageInfo, D extends TransportOptions = TransportOptions> {
|
||||
/** Transport name, used for logging */
|
||||
name: string;
|
||||
/** Transport version, used for logging */
|
||||
@@ -119,7 +127,7 @@ export interface Transport<T = SentMessageInfo> {
|
||||
/** Registers an event listener, the transport may emit 'log', 'error', 'idle' and 'clear' */
|
||||
on?(event: string | symbol, listener: (...args: any[]) => void): this;
|
||||
/** The Mail object the transport belongs to, set by Mail */
|
||||
mailer?: Mail<T> | undefined;
|
||||
mailer?: Mail<T, D> | undefined;
|
||||
/** Socket handler for a proxied connection, set by Mail when a proxy is configured */
|
||||
getSocket?: GetSocketHandler | undefined;
|
||||
}
|
||||
@@ -150,22 +158,23 @@ export interface TransportOptions {
|
||||
attachDataUrls?: boolean | undefined;
|
||||
}
|
||||
/**
|
||||
* The transporter object createTransport returns, a Mail instance wrapping a transport
|
||||
* The transporter object createTransport returns, a Mail instance wrapping a transport. D
|
||||
* is the options type of the transport, it types the options field of the transporter
|
||||
*/
|
||||
export type Transporter<T = SentMessageInfo> = Mail<T>;
|
||||
export type Transporter<T = SentMessageInfo, D extends TransportOptions = TransportOptions> = Mail<T, D>;
|
||||
/**
|
||||
* Creates an object for exposing the Mail API
|
||||
*
|
||||
* @constructor
|
||||
* @param transporter Transport object instance to pass the mails to
|
||||
*/
|
||||
declare class Mail<out T = SentMessageInfo> extends EventEmitter {
|
||||
options: TransportOptions;
|
||||
declare class Mail<out T = SentMessageInfo, out D extends TransportOptions = TransportOptions> extends EventEmitter {
|
||||
options: D;
|
||||
/** Message defaults given to createTransport, kept public because the DefinitelyTyped typings declared it */
|
||||
_defaults: MailDefaults;
|
||||
meta: Map<string, any>;
|
||||
dkim: DKIM | false;
|
||||
transporter: Transport<T>;
|
||||
transporter: Transport<T, D>;
|
||||
logger: shared.Logger;
|
||||
/** Closes the transport, the pooled SMTP transport closes its connections */
|
||||
close: () => void;
|
||||
@@ -178,15 +187,21 @@ declare class Mail<out T = SentMessageInfo> extends EventEmitter {
|
||||
};
|
||||
/** Socket handler for a proxied connection, set by setupProxy and handed to the transport on the next send */
|
||||
getSocket?: GetSocketHandler | false | undefined;
|
||||
constructor(transporter: Transport<T>, options?: TransportOptions, defaults?: MailDefaults);
|
||||
constructor(transporter: Transport<T, D>, options?: D, defaults?: MailDefaults);
|
||||
use(step: string, plugin: PluginFunction<T>): this;
|
||||
/**
|
||||
* Sends an email using the preselected transport object
|
||||
*
|
||||
* @param data E-data description
|
||||
* @returns Promise that resolves with the result of the send
|
||||
*/
|
||||
sendMail(data: SendMailOptions): Promise<T>;
|
||||
/**
|
||||
* Sends an email using the preselected transport object
|
||||
*
|
||||
* @param data E-data description
|
||||
* @param callback Callback to run once the sending succeeded or failed
|
||||
*/
|
||||
sendMail(data: SendMailOptions): Promise<T>;
|
||||
sendMail(data: SendMailOptions, callback: SendMailCallback<T>): void;
|
||||
getVersionString(): string;
|
||||
/**
|
||||
@@ -213,7 +228,10 @@ declare namespace Mail {
|
||||
type ListHeader = MailComposerListHeaderEntry;
|
||||
type ListHeaders = MailComposerListHeaders;
|
||||
type Envelope = MimeNodeEnvelopeInput;
|
||||
type TextEncoding = NonNullable<SendMailOptions['textEncoding']>;
|
||||
type Connection = SocketOptions;
|
||||
type TextEncoding = MailComposerTextEncoding;
|
||||
type PluginFunction<T = SentMessageInfo> = MailPluginFunction<T>;
|
||||
}
|
||||
/** The same aliases as module level exports, for `import * as Mail` and `import Mail = require()` */
|
||||
export type { SendMailOptions as Options, MimeNodeAddress as Address, MailComposerAttachment as Attachment, MailComposerAlternative as AttachmentLike, MailComposerAlternative as AmpAttachment, MailComposerIcalEvent as IcalAttachment, MimeNodeHeaders as Headers, MailComposerListHeaderEntry as ListHeader, MailComposerListHeaders as ListHeaders, MimeNodeEnvelopeInput as Envelope, SocketOptions as Connection, MailComposerTextEncoding as TextEncoding };
|
||||
export default Mail;
|
||||
+27
-8
@@ -119,6 +119,11 @@ class Mail extends EventEmitter {
|
||||
this.getSocket = false;
|
||||
}
|
||||
const mail = new MailMessage(this, data);
|
||||
// a failed message is not going to be read anymore, release its content streams
|
||||
const fail = (err) => {
|
||||
mail.releaseStreams();
|
||||
done(err);
|
||||
};
|
||||
this.logger.debug({
|
||||
tnx: 'transport',
|
||||
name: this.transporter.name,
|
||||
@@ -132,7 +137,7 @@ class Mail extends EventEmitter {
|
||||
tnx: 'plugin',
|
||||
action: 'compile'
|
||||
}, 'PluginCompile Error: %s', err.message);
|
||||
return done(err);
|
||||
return fail(err);
|
||||
}
|
||||
let recipientCount;
|
||||
try {
|
||||
@@ -149,7 +154,7 @@ class Mail extends EventEmitter {
|
||||
tnx: 'transport',
|
||||
action: 'send'
|
||||
}, 'Compile Error: %s', err.message);
|
||||
return done(err);
|
||||
return fail(err);
|
||||
}
|
||||
const maxRecipients = mail.data.maxRecipients === undefined ? DEFAULT_MAX_RECIPIENTS : mail.data.maxRecipients;
|
||||
if (maxRecipients && recipientCount > maxRecipients) {
|
||||
@@ -160,7 +165,7 @@ class Mail extends EventEmitter {
|
||||
tnx: 'transport',
|
||||
action: 'send'
|
||||
}, 'Send Error: %s', err.message);
|
||||
return done(err);
|
||||
return fail(err);
|
||||
}
|
||||
this._processPlugins('stream', mail, err => {
|
||||
if (err) {
|
||||
@@ -169,7 +174,7 @@ class Mail extends EventEmitter {
|
||||
tnx: 'plugin',
|
||||
action: 'stream'
|
||||
}, 'PluginStream Error: %s', err.message);
|
||||
return done(err);
|
||||
return fail(err);
|
||||
}
|
||||
if (mail.data.dkim || this.dkim) {
|
||||
mail.message.processFunc(input => {
|
||||
@@ -184,6 +189,7 @@ class Mail extends EventEmitter {
|
||||
}
|
||||
this.transporter.send(mail, (...args) => {
|
||||
if (args[0]) {
|
||||
mail.releaseStreams();
|
||||
this.logger.error({
|
||||
err: args[0],
|
||||
tnx: 'transport',
|
||||
@@ -251,6 +257,11 @@ class Mail extends EventEmitter {
|
||||
// setup socket handler for the mailer object
|
||||
this.getSocket = (options, callback) => {
|
||||
const protocol = proxy.protocol.replace(/:$/, '').toLowerCase();
|
||||
// The proxy handshake is a part of connecting and draws from the same deadline as the
|
||||
// SMTP connection that follows it: whatever the proxy takes, the connection does not get
|
||||
const configuredTimeout = Number(this.options.connectionTimeout) || 0;
|
||||
const connectStartedAt = Number(options.connectStartedAt) || Date.now();
|
||||
const connectionTimeout = configuredTimeout ? Math.max(connectStartedAt + configuredTimeout - Date.now(), 1) : undefined;
|
||||
if (this.meta.has('proxy_handler_' + protocol)) {
|
||||
return this.meta.get('proxy_handler_' + protocol)(proxy, options, callback);
|
||||
}
|
||||
@@ -258,7 +269,7 @@ class Mail extends EventEmitter {
|
||||
// Connect using a HTTP CONNECT method
|
||||
case 'http':
|
||||
case 'https':
|
||||
httpProxyClient(proxy.href, options.port, options.host, this.options.tls || {}, (err, socket) => {
|
||||
httpProxyClient(proxy.href, options.port, options.host, connectionTimeout ? Object.assign({}, this.options.tls, { timeout: connectionTimeout }) : this.options.tls || {}, (err, socket) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
@@ -292,6 +303,9 @@ class Mail extends EventEmitter {
|
||||
},
|
||||
command: 'connect'
|
||||
};
|
||||
if (connectionTimeout) {
|
||||
connectionOpts.timeout = connectionTimeout;
|
||||
}
|
||||
if (proxy.username || proxy.password) {
|
||||
const username = proxy.username || '';
|
||||
const password = proxy.password || '';
|
||||
@@ -321,11 +335,13 @@ class Mail extends EventEmitter {
|
||||
if (net.isIP(proxy.hostname)) {
|
||||
return connect(proxy.hostname);
|
||||
}
|
||||
return dns.resolve(proxy.hostname, (err, address) => {
|
||||
// lookup goes through the hosts file and returns an IPv6 address as well,
|
||||
// the same way a connection to the proxy host would be resolved otherwise
|
||||
return dns.lookup(proxy.hostname, (err, address) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
connect(Array.isArray(address) ? address[0] : address);
|
||||
connect(address);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -344,9 +360,12 @@ class Mail extends EventEmitter {
|
||||
return callback(err);
|
||||
}
|
||||
let cidCounter = 0;
|
||||
// the media type is a bounded token: a quote, whitespace, '>' or '<' ends the
|
||||
// attribute value and with it the type, so one img tag never scans ahead to a
|
||||
// ';' that belongs to a later tag, which was quadratic
|
||||
html = (html || '')
|
||||
.toString()
|
||||
.replace(/(<img\b[^<>]{0,1024} src\s{0,20}=[\s"']{0,20})(data:([^;]+);[^"'>\s]+)/gi, (match, prefix, dataUri, mimeType) => {
|
||||
.replace(/(<img\b[^<>]{0,1024} src\s{0,20}=[\s"']{0,20})(data:([^;"'<>\s]{1,255});[^"'>\s]+)/gi, (match, prefix, dataUri, mimeType) => {
|
||||
const cid = crypto.randomBytes(10).toString('hex') + '@localhost';
|
||||
if (!mail.data.attachments) {
|
||||
mail.data.attachments = [];
|
||||
|
||||
Loaded 100 of 148 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user