Compare commits

..
35 Commits
Author SHA1 Message Date
dawidd6 df3e9b2c0f node_modules: update 2026-10-19 06:16:48 +00:00
renovate[bot] 955f286ed4 chore(deps): update dependency nodemailer to v10.1.7 (#351)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-19 08:16:31 +02:00
Dawid Dziurlaanddawidd6 0a1f5db16c node_modules: update (#349)
Co-authored-by: dawidd6 <9713907+dawidd6@users.noreply.github.com>
2026-10-18 15:39:30 +02:00
renovate[bot] 9d0c26ec8b chore(deps): update dependency nodemailer to v10.1.6 (#350)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-18 15:37:26 +02:00
renovate[bot] b90fafaa5c chore(deps): update dependency nodemailer to v10.1.2 (#348)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-16 16:06:00 +02:00
Dawid Dziurlaanddawidd6 f6664f2182 node_modules: update (#347)
Co-authored-by: dawidd6 <9713907+dawidd6@users.noreply.github.com>
2026-10-14 12:50:45 +02:00
renovate[bot] 16c91eb93f chore(deps): update dependency nodemailer to v10.1.1 (#346)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-14 12:47:18 +02:00
Dawid Dziurlaanddawidd6 6e88c72525 node_modules: update (#344)
Co-authored-by: dawidd6 <9713907+dawidd6@users.noreply.github.com>
2026-10-10 17:27:57 +02:00
renovate[bot] b099186883 chore(deps): update dependency nodemailer to v10.1.0 (#345)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-10 17:26:09 +02:00
renovate[bot] ef74bf1f25 chore(deps): update dependency nodemailer to v10.0.16 (#343)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-07 22:11:37 +02:00
Dawid Dziurlaanddawidd6 ad4baca30f node_modules: update (#342)
Co-authored-by: dawidd6 <9713907+dawidd6@users.noreply.github.com>
2026-10-05 14:32:33 +02:00
renovate[bot] ab08312534 chore(deps): update dependency nodemailer to v10.0.15 (#341)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-05 14:31:30 +02:00
polyfloyd c2c6302b92 Fix secure setting and fix smtp+starttls defaulting to the wrong port (#329)
* Auto-derive the secure setting from serverPort, after setting serverPort

* Breaking: Add smtps scheme, fix smtp+starttls to default to port 587

* Use getBooleanInput for secure
2026-10-04 16:34:48 +02:00
Dawid Dziurlaanddawidd6 0635835dfc node_modules: update (#340)
Co-authored-by: dawidd6 <9713907+dawidd6@users.noreply.github.com>
2026-10-03 22:21:10 +02:00
renovate[bot] 791f27c1e4 chore(deps): update dependency nodemailer to v10.0.14 (#339)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-03 22:17:59 +02:00
Dawid Dziurlaanddawidd6 a9c5eb6c35 node_modules: update (#338)
Co-authored-by: dawidd6 <9713907+dawidd6@users.noreply.github.com>
2026-10-03 10:23:35 +02:00
renovate[bot] e1936242ed chore(deps): update dependency nodemailer to v10.0.13 (#335)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-03 10:22:09 +02:00
Dawid Dziurla 411739e2ff Delete .github/dependabot.yml 2026-10-03 10:18:28 +02:00
Dawid Dziurla dfde770ea1 Delete .devcontainer directory 2026-10-03 10:18:04 +02:00
renovate[bot] 0989a8cbe9 Add renovate.json (#334)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-03 10:17:02 +02:00
polyfloyd 6b2df9babe Respect convert_markdown setting (#330) 2026-10-02 10:23:50 +02:00
Dawid Dziurlaanddawidd6 6bf0a2f26a node_modules: update (#332)
Co-authored-by: dawidd6 <9713907+dawidd6@users.noreply.github.com>
2026-10-01 09:48:38 +02:00
dependabot[bot] c1dac70e3a build(deps): bump brace-expansion from 5.0.9 to 5.0.12 (#333)
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 5.0.9 to 5.0.12.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v5.0.9...v5.0.12)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 5.0.12
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-01 09:47:48 +02:00
dependabot[bot] b83e41d98d build(deps): bump nodemailer from 10.0.10 to 10.0.11 (#331)
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 10.0.10 to 10.0.11.
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v10.0.10...v10.0.11)

---
updated-dependencies:
- dependency-name: nodemailer
  dependency-version: 10.0.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-01 08:47:38 +02:00
Dawid Dziurlaanddawidd6 0e0ab085a3 node_modules: update (#328)
Co-authored-by: dawidd6 <9713907+dawidd6@users.noreply.github.com>
2026-09-29 16:54:26 +02:00
dependabot[bot] 23dc752083 build(deps): bump undici from 6.28.0 to 6.29.0 (#327)
Bumps [undici](https://github.com/nodejs/undici) from 6.28.0 to 6.29.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.28.0...v6.29.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 6.29.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-29 16:49:41 +02:00
Dawid Dziurlaanddawidd6 6d91308e46 node_modules: update (#325)
Co-authored-by: dawidd6 <9713907+dawidd6@users.noreply.github.com>
2026-09-25 18:42:41 +02:00
Dawid Dziurla bd35283f19 workflows: update 2026-09-25 18:40:52 +02:00
Dawid Dziurla cab633514f dependabot: weekly 2026-09-18 09:02:47 +02:00
dependabot[bot] b6796c9b12 build(deps): bump nodemailer from 10.0.8 to 10.0.10 (#324)
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 10.0.8 to 10.0.10.
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v10.0.8...v10.0.10)

---
updated-dependencies:
- dependency-name: nodemailer
  dependency-version: 10.0.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-18 09:01:43 +02:00
Dawid Dziurlaanddawidd6 67ce3558d6 node_modules: update (#322)
Co-authored-by: dawidd6 <9713907+dawidd6@users.noreply.github.com>
2026-09-15 13:00:32 +02:00
dependabot[bot] 4a3d0f9ca3 build(deps): bump nodemailer from 10.0.3 to 10.0.8 (#320)
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 10.0.3 to 10.0.8.
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v10.0.3...v10.0.8)

---
updated-dependencies:
- dependency-name: nodemailer
  dependency-version: 10.0.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-15 09:01:07 +02:00
dependabot[bot] 364e471101 build(deps): bump nodemailer from 10.0.1 to 10.0.3 (#319)
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 10.0.1 to 10.0.3.
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v10.0.1...v10.0.3)

---
updated-dependencies:
- dependency-name: nodemailer
  dependency-version: 10.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-14 08:21:54 +02:00
dependabot[bot] 420aeba895 build(deps): bump nodemailer from 10.0.0 to 10.0.1 (#318)
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 10.0.0 to 10.0.1.
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v10.0.0...v10.0.1)

---
updated-dependencies:
- dependency-name: nodemailer
  dependency-version: 10.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-11 11:50:05 +02:00
Bernhard 03a02a4940 Add require_tls input to prevent STARTTLS fallback to plain text (#317)
Expose nodemailer's requireTLS option so that when secure is false
(e.g. STARTTLS on port 587), the connection aborts if the server
does not support/negotiate STARTTLS, instead of silently sending
credentials in clear text.
2026-09-10 19:57:59 +02:00
210 changed files with 33000 additions and 7780 deletions

No files matched your search

-16
View File
@@ -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"
]
}
}
}
-28
View File
@@ -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
+7 -1
View File
@@ -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}}
+9 -2
View File
@@ -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:
@@ -37,6 +38,12 @@ Some features:
# Optional whether this connection use TLS (default is true if server_port is 465)
secure: true
# Optional: when secure is false (e.g. STARTTLS on port 587), abort the
# connection if the server does not support/negotiate STARTTLS, instead
# of silently falling back to a plain text connection. Recommended
# whenever secure is false and you authenticate with username/password.
require_tls: true
# Optional (recommended) mail server username:
username: ${{secrets.MAIL_USERNAME}}
+5 -1
View File
@@ -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:
@@ -14,6 +14,9 @@ inputs:
default: "25"
secure:
description: Whether this connection use TLS (default is true if server_port is 465)
require_tls:
description: When 'secure' is false, refuse to send if the server does not support STARTTLS (prevents credentials from ever being sent in clear text)
required: false
username:
description: Authenticate as this user to SMTP server
password:
@@ -51,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
+18 -10
View File
@@ -93,14 +93,13 @@ async function main() {
try {
let serverAddress = core.getInput("server_address");
let serverPort = core.getInput("server_port");
let secure = core.getInput("secure");
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);
@@ -111,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) {
@@ -132,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 });
@@ -142,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 });
@@ -195,7 +202,8 @@ async function main() {
}
: undefined,
port: serverPort,
secure: secure === "true",
secure: secure,
requireTLS: requireTLS === "true",
tls:
ignoreCert == "true"
? {
+10 -10
View File
@@ -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,12 +94,12 @@
}
},
"node_modules/nodemailer": {
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.1.1.tgz",
"integrity": "sha512-izw9mVKFix6YSnC9eLgV6g1opl9DUlRio9ZNcq+Wu9Ujn2UwF+8Nl0B8nz22kEC+CTZCvinkxwJ0DeFbb6NwcQ==",
"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": ">=6.0.0"
"node": ">=20.0.0"
}
},
"node_modules/showdown": {
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;AA2FA,wBAkBC;AA7GD,mDAAyC;AAEzC,MAAM,QAAQ,GAAG,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAA;AACjD,MAAM,OAAO,GAAG,QAAQ,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAA;AAC/C,MAAM,QAAQ,GAAG,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAA;AACjD,MAAM,QAAQ,GAAG,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAA;AACjD,MAAM,SAAS,GAAG,UAAU,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAA;AACnD,MAAM,eAAe,GAAG,IAAI,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAA;AACjD,MAAM,cAAc,GAAG,IAAI,MAAM,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;AAC/C,MAAM,eAAe,GAAG,IAAI,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAA;AACjD,MAAM,eAAe,GAAG,IAAI,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAA;AACjD,MAAM,gBAAgB,GAAG,IAAI,MAAM,CAAC,SAAS,EAAE,GAAG,CAAC,CAAA;AACnD,MAAM,YAAY,GAAG,OAAO,CAAA;AAC5B,MAAM,WAAW,GAAG,MAAM,CAAA;AAC1B,MAAM,YAAY,GAAG,MAAM,CAAA;AAC3B,MAAM,YAAY,GAAG,MAAM,CAAA;AAC3B,MAAM,aAAa,GAAG,OAAO,CAAA;AAEhB,QAAA,aAAa,GAAG,OAAO,CAAA;AAEpC,4EAA4E;AAC5E,2EAA2E;AAC3E,yEAAyE;AACzE,0EAA0E;AAC1E,6EAA6E;AAC7E,sEAAsE;AACtE,4EAA4E;AAC5E,0EAA0E;AAC1E,wEAAwE;AACxE,iDAAiD;AACpC,QAAA,oBAAoB,GAAG,SAAS,CAAA;AAE7C,SAAS,OAAO,CAAC,GAAW;IAC1B,OAAO,CAAC,KAAK,CAAC,GAAU,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;AACnE,CAAC;AAED,SAAS,YAAY,CAAC,GAAW;IAC/B,OAAO,GAAG;SACP,OAAO,CAAC,YAAY,EAAE,QAAQ,CAAC;SAC/B,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC;SAC7B,OAAO,CAAC,YAAY,EAAE,QAAQ,CAAC;SAC/B,OAAO,CAAC,YAAY,EAAE,QAAQ,CAAC;SAC/B,OAAO,CAAC,aAAa,EAAE,SAAS,CAAC,CAAA;AACtC,CAAC;AAED,SAAS,cAAc,CAAC,GAAW;IACjC,OAAO,GAAG;SACP,OAAO,CAAC,eAAe,EAAE,IAAI,CAAC;SAC9B,OAAO,CAAC,cAAc,EAAE,GAAG,CAAC;SAC5B,OAAO,CAAC,eAAe,EAAE,GAAG,CAAC;SAC7B,OAAO,CAAC,eAAe,EAAE,GAAG,CAAC;SAC7B,OAAO,CAAC,gBAAgB,EAAE,GAAG,CAAC,CAAA;AACnC,CAAC;AAED;;;;GAIG;AACH,SAAS,eAAe,CAAC,GAAW;IAClC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO,CAAC,EAAE,CAAC,CAAA;IACb,CAAC;IAED,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,MAAM,CAAC,GAAG,IAAA,yBAAQ,EAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAA;IAEjC,IAAI,CAAC,CAAC,EAAE,CAAC;QACP,OAAO,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IACvB,CAAC;IAED,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,CAAA;IAC7B,MAAM,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAExB,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,GAAG,GAAG,IAAI,GAAG,GAAG,CAAA;IACnC,MAAM,SAAS,GAAG,eAAe,CAAC,IAAI,CAAC,CAAA;IACvC,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QAChB,CAAC;QAAC,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAY,IAAI,SAAS,CAAC,KAAK,EAAE,CAAA;QACjD,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,CAAA;IAC5B,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAA;IAE1B,OAAO,KAAK,CAAA;AACd,CAAC;AAOD,SAAgB,MAAM,CAAC,GAAW,EAAE,UAAiC,EAAE;IACrE,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO,EAAE,CAAA;IACX,CAAC;IAED,MAAM,EAAE,GAAG,GAAG,qBAAa,EAAE,SAAS,GAAG,4BAAoB,EAAE,GAAG,OAAO,CAAA;IAEzE,oDAAoD;IACpD,oEAAoE;IACpE,sEAAsE;IACtE,6CAA6C;IAC7C,oEAAoE;IACpE,+DAA+D;IAC/D,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAC7B,GAAG,GAAG,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IAC/B,CAAC;IAED,OAAO,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,cAAc,CAAC,CAAA;AAC7E,CAAC;AAED,SAAS,OAAO,CAAC,GAAW;IAC1B,OAAO,GAAG,GAAG,GAAG,GAAG,GAAG,CAAA;AACxB,CAAC;AAED,SAAS,QAAQ,CAAC,EAAU;IAC1B,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;AAC1B,CAAC;AAED,SAAS,GAAG,CAAC,CAAS,EAAE,CAAS;IAC/B,OAAO,CAAC,IAAI,CAAC,CAAA;AACf,CAAC;AAED,SAAS,GAAG,CAAC,CAAS,EAAE,CAAS;IAC/B,OAAO,CAAC,IAAI,CAAC,CAAA;AACf,CAAC;AAED,0EAA0E;AAC1E,gFAAgF;AAChF,2EAA2E;AAC3E,gFAAgF;AAChF,iCAAiC;AACjC,SAAS,OAAO,CACd,GAAa,EACb,GAAW,EACX,MAAgB,EAChB,GAAW,EACX,SAAiB,EACjB,WAAoB;IAEpB,MAAM,GAAG,GAAa,EAAE,CAAA;IACxB,IAAI,MAAM,GAAG,CAAC,CAAA;IACd,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACpC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACvC,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG;gBAAE,OAAO,GAAG,CAAA;YACjC,MAAM,SAAS,GAAI,GAAG,CAAC,CAAC,CAAY,GAAG,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,CAAA;YACtD,yEAAyE;YACzE,+DAA+D;YAC/D,IAAI,WAAW,IAAI,CAAC,SAAS;gBAAE,SAAQ;YACvC,IAAI,MAAM,GAAG,SAAS,CAAC,MAAM,GAAG,SAAS;gBAAE,OAAO,GAAG,CAAA;YACrD,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;YACnB,MAAM,IAAI,SAAS,CAAC,MAAM,CAAA;QAC5B,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,8EAA8E;AAC9E,iBAAiB;AACjB,SAAS,cAAc,CACrB,IAAY,EACZ,eAAwB,EACxB,GAAW,EACX,SAAiB;IAEjB,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;IAC5B,MAAM,CAAC,GAAa,EAAE,CAAA;IACtB,0EAA0E;IAC1E,mBAAmB;IACnB,qBAAqB;IACrB,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,SAAS,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,SAAS,EAAE,CAAC;QAC7C,OAAO,CAAC,CAAA;IACV,CAAC;IACD,oBAAoB;IACpB,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACvB,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACvB,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;IAChD,IAAI,IAAI,GACN,CAAC,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC;QACpC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACtC,CAAC,CAAC,CAAC,CAAA;IACL,IAAI,IAAILine truncated
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;AAkIA,wBA+BC;AAjKD,mDAAyC;AAEzC,MAAM,QAAQ,GAAG,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAA;AACjD,MAAM,OAAO,GAAG,QAAQ,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAA;AAC/C,MAAM,QAAQ,GAAG,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAA;AACjD,MAAM,QAAQ,GAAG,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAA;AACjD,MAAM,SAAS,GAAG,UAAU,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAA;AACnD,MAAM,eAAe,GAAG,IAAI,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAA;AACjD,MAAM,cAAc,GAAG,IAAI,MAAM,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;AAC/C,MAAM,eAAe,GAAG,IAAI,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAA;AACjD,MAAM,eAAe,GAAG,IAAI,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAA;AACjD,MAAM,gBAAgB,GAAG,IAAI,MAAM,CAAC,SAAS,EAAE,GAAG,CAAC,CAAA;AACnD,MAAM,YAAY,GAAG,OAAO,CAAA;AAC5B,MAAM,WAAW,GAAG,MAAM,CAAA;AAC1B,MAAM,YAAY,GAAG,MAAM,CAAA;AAC3B,MAAM,YAAY,GAAG,MAAM,CAAA;AAC3B,MAAM,aAAa,GAAG,OAAO,CAAA;AAEhB,QAAA,aAAa,GAAG,OAAO,CAAA;AAEpC,4EAA4E;AAC5E,2EAA2E;AAC3E,yEAAyE;AACzE,0EAA0E;AAC1E,6EAA6E;AAC7E,sEAAsE;AACtE,4EAA4E;AAC5E,0EAA0E;AAC1E,wEAAwE;AACxE,iDAAiD;AACpC,QAAA,oBAAoB,GAAG,SAAS,CAAA;AAE7C,+EAA+E;AAC/E,8EAA8E;AAC9E,+EAA+E;AAC/E,4EAA4E;AAC5E,gFAAgF;AAChF,4EAA4E;AAC5E,8EAA8E;AAC9E,yCAAyC;AAC5B,QAAA,mBAAmB,GAAG,KAAK,CAAA;AAExC,+EAA+E;AAC/E,8EAA8E;AAC9E,+EAA+E;AAC/E,8EAA8E;AAC9E,4EAA4E;AAC5E,yEAAyE;AACzE,2EAA2E;AAC3E,mEAAmE;AACtD,QAAA,sBAAsB,GAAG,KAAK,CAAA;AAE3C,SAAS,OAAO,CAAC,GAAW;IAC1B,OAAO,CAAC,KAAK,CAAC,GAAU,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;AACnE,CAAC;AAED,SAAS,YAAY,CAAC,GAAW;IAC/B,OAAO,GAAG;SACP,OAAO,CAAC,YAAY,EAAE,QAAQ,CAAC;SAC/B,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC;SAC7B,OAAO,CAAC,YAAY,EAAE,QAAQ,CAAC;SAC/B,OAAO,CAAC,YAAY,EAAE,QAAQ,CAAC;SAC/B,OAAO,CAAC,aAAa,EAAE,SAAS,CAAC,CAAA;AACtC,CAAC;AAED,SAAS,cAAc,CAAC,GAAW;IACjC,OAAO,GAAG;SACP,OAAO,CAAC,eAAe,EAAE,IAAI,CAAC;SAC9B,OAAO,CAAC,cAAc,EAAE,GAAG,CAAC;SAC5B,OAAO,CAAC,eAAe,EAAE,GAAG,CAAC;SAC7B,OAAO,CAAC,eAAe,EAAE,GAAG,CAAC;SAC7B,OAAO,CAAC,gBAAgB,EAAE,GAAG,CAAC,CAAA;AACnC,CAAC;AAED,8DAA8D;AAC9D,SAAS,OAAO,CAAC,MAAgB,EAAE,KAAe;IAChD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAW,CAAC,CAAA;IACjC,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,SAAS,eAAe,CAAC,GAAW;IAClC,MAAM,KAAK,GAAa,EAAE,CAAA;IAE1B,8EAA8E;IAC9E,oEAAoE;IACpE,8EAA8E;IAC9E,4DAA4D;IAC5D,EAAE;IACF,oCAAoC;IACpC,IAAI,KAAK,GAAG,EAAE,CAAA;IAEd,SAAS,CAAC;QACR,MAAM,CAAC,GAAG,IAAA,yBAAQ,EAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAA;QAEjC,IAAI,CAAC,CAAC,EAAE,CAAC;YACP,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;YAC3B,IAAI,CAAC,CAAC,CAAC,GAAG,KAAK,GAAI,IAAI,CAAC,CAAC,CAAY,CAAA;YACrC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;YACpB,OAAO,KAAK,CAAA;QACd,CAAC;QAED,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,CAAA;QAC7B,MAAM,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QACxB,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,GAAI,CAAC,CAAC,CAAC,CAAY,CAAA;QAC/B,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,GAAG,GAAG,IAAI,GAAG,GAAG,CAAA;QAEnC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACjB,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAA;YACjB,OAAO,KAAK,CAAA;QACd,CAAC;QAED,KAAK,GAAG,CAAC,CAAC,GAAG,EAAY,CAAA;QACzB,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAA;QACjB,GAAG,GAAG,IAAI,CAAA;IACZ,CAAC;AACH,CAAC;AASD,SAAgB,MAAM,CAAC,GAAW,EAAE,UAAiC,EAAE;IACrE,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO,EAAE,CAAA;IACX,CAAC;IAED,MAAM,EACJ,GAAG,GAAG,qBAAa,EACnB,SAAS,GAAG,4BAAoB,EAChC,QAAQ,GAAG,2BAAmB,EAC9B,WAAW,GAAG,8BAAsB,GACrC,GAAG,OAAO,CAAA;IAEX,oDAAoD;IACpD,oEAAoE;IACpE,sEAAsE;IACtE,6CAA6C;IAC7C,oEAAoE;IACpE,+DAA+D;IAC/D,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAC7B,GAAG,GAAG,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IAC/B,CAAC;IAED,OAAO,OAAO,CACZ,YAAY,CAAC,GAAG,CAAC,EACjB,GAAG,EACH,SAAS,EACT,QAAQ,EACR,CAAC,EACD,WAAW,EACX,IAAI,CACL,CAAC,GAAG,CAAC,cAAc,CAAC,CAAA;AACvB,CAAC;AAED,SAAS,OAAO,CAAC,GAAW;IAC1B,OAAO,GAAG,GAAG,GAAG,GAAG,GAAG,CAAA;AACxB,CAAC;AAED,SAAS,QAAQ,CAAC,EAAU;IAC1B,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;AAC1B,CAAC;AAED,SAAS,GAAG,CAAC,CAAS,EAAE,CAAS;IAC/B,OAAO,CAAC,IAAI,CAAC,CAAA;AACf,CAAC;AAED,SAAS,GAAG,CAAC,CAAS,EAAE,CAAS;IAC/B,OAAO,CAAC,IAAI,CAAC,CAAA;AACf,CAAC;AAED,0EAA0E;AAC1E,gFAAgF;AAChF,2EAA2E;AAC3E,gFAAgF;AAChF,iCAAiC;AACjC,SAAS,OAAO,CACd,GAAa,EACb,GAAW,EACX,MAAgB,EAChB,GAAW,EACX,SAAiB,EACjB,WAAoB;IAEpB,MAAM,GAAG,GAAa,EAAE,CAAA;IACxB,IAAI,MAAM,GAAG,CAAC,CAAA;IACd,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACpC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACvC,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG;gBAAE,OAAO,GAAG,CAAA;YACjC,MAAM,SAAS,GAAI,GAAG,CAAC,CAAC,CAAY,GAAG,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,CAAA;YACtD,yEAAyE;YACzE,+DAA+D;YAC/D,IAAI,WAAW,IAAI,CAAC,SAAS;gBAAE,SAAQ;YACvC,IAAI,MAAM,GAAG,SAAS,CAAC,MAAM,GAAG,SAAS;gBAAE,OAAO,GAAG,CAAA;YACrD,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;YACnB,MAAM,IAAI,SAAS,CAAC,MAAM,CAAA;QAC5B,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,8EAA8E;AAC9E,iBAAiB;AACjB,SAAS,cAAc,CACrB,IAAY,EACZ,eAAwB,EACxB,GAAW,EACX,SAAiBLine truncated
+4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEzC,MAAM,QAAQ,GAAG,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAA;AACjD,MAAM,OAAO,GAAG,QAAQ,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAA;AAC/C,MAAM,QAAQ,GAAG,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAA;AACjD,MAAM,QAAQ,GAAG,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAA;AACjD,MAAM,SAAS,GAAG,UAAU,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAA;AACnD,MAAM,eAAe,GAAG,IAAI,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAA;AACjD,MAAM,cAAc,GAAG,IAAI,MAAM,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;AAC/C,MAAM,eAAe,GAAG,IAAI,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAA;AACjD,MAAM,eAAe,GAAG,IAAI,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAA;AACjD,MAAM,gBAAgB,GAAG,IAAI,MAAM,CAAC,SAAS,EAAE,GAAG,CAAC,CAAA;AACnD,MAAM,YAAY,GAAG,OAAO,CAAA;AAC5B,MAAM,WAAW,GAAG,MAAM,CAAA;AAC1B,MAAM,YAAY,GAAG,MAAM,CAAA;AAC3B,MAAM,YAAY,GAAG,MAAM,CAAA;AAC3B,MAAM,aAAa,GAAG,OAAO,CAAA;AAE7B,MAAM,CAAC,MAAM,aAAa,GAAG,OAAO,CAAA;AAEpC,4EAA4E;AAC5E,2EAA2E;AAC3E,yEAAyE;AACzE,0EAA0E;AAC1E,6EAA6E;AAC7E,sEAAsE;AACtE,4EAA4E;AAC5E,0EAA0E;AAC1E,wEAAwE;AACxE,iDAAiD;AACjD,MAAM,CAAC,MAAM,oBAAoB,GAAG,SAAS,CAAA;AAE7C,SAAS,OAAO,CAAC,GAAW;IAC1B,OAAO,CAAC,KAAK,CAAC,GAAU,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;AACnE,CAAC;AAED,SAAS,YAAY,CAAC,GAAW;IAC/B,OAAO,GAAG;SACP,OAAO,CAAC,YAAY,EAAE,QAAQ,CAAC;SAC/B,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC;SAC7B,OAAO,CAAC,YAAY,EAAE,QAAQ,CAAC;SAC/B,OAAO,CAAC,YAAY,EAAE,QAAQ,CAAC;SAC/B,OAAO,CAAC,aAAa,EAAE,SAAS,CAAC,CAAA;AACtC,CAAC;AAED,SAAS,cAAc,CAAC,GAAW;IACjC,OAAO,GAAG;SACP,OAAO,CAAC,eAAe,EAAE,IAAI,CAAC;SAC9B,OAAO,CAAC,cAAc,EAAE,GAAG,CAAC;SAC5B,OAAO,CAAC,eAAe,EAAE,GAAG,CAAC;SAC7B,OAAO,CAAC,eAAe,EAAE,GAAG,CAAC;SAC7B,OAAO,CAAC,gBAAgB,EAAE,GAAG,CAAC,CAAA;AACnC,CAAC;AAED;;;;GAIG;AACH,SAAS,eAAe,CAAC,GAAW;IAClC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO,CAAC,EAAE,CAAC,CAAA;IACb,CAAC;IAED,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAA;IAEjC,IAAI,CAAC,CAAC,EAAE,CAAC;QACP,OAAO,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IACvB,CAAC;IAED,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,CAAA;IAC7B,MAAM,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAExB,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,GAAG,GAAG,IAAI,GAAG,GAAG,CAAA;IACnC,MAAM,SAAS,GAAG,eAAe,CAAC,IAAI,CAAC,CAAA;IACvC,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QAChB,CAAC;QAAC,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAY,IAAI,SAAS,CAAC,KAAK,EAAE,CAAA;QACjD,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,CAAA;IAC5B,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAA;IAE1B,OAAO,KAAK,CAAA;AACd,CAAC;AAOD,MAAM,UAAU,MAAM,CAAC,GAAW,EAAE,UAAiC,EAAE;IACrE,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO,EAAE,CAAA;IACX,CAAC;IAED,MAAM,EAAE,GAAG,GAAG,aAAa,EAAE,SAAS,GAAG,oBAAoB,EAAE,GAAG,OAAO,CAAA;IAEzE,oDAAoD;IACpD,oEAAoE;IACpE,sEAAsE;IACtE,6CAA6C;IAC7C,oEAAoE;IACpE,+DAA+D;IAC/D,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAC7B,GAAG,GAAG,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IAC/B,CAAC;IAED,OAAO,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,cAAc,CAAC,CAAA;AAC7E,CAAC;AAED,SAAS,OAAO,CAAC,GAAW;IAC1B,OAAO,GAAG,GAAG,GAAG,GAAG,GAAG,CAAA;AACxB,CAAC;AAED,SAAS,QAAQ,CAAC,EAAU;IAC1B,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;AAC1B,CAAC;AAED,SAAS,GAAG,CAAC,CAAS,EAAE,CAAS;IAC/B,OAAO,CAAC,IAAI,CAAC,CAAA;AACf,CAAC;AAED,SAAS,GAAG,CAAC,CAAS,EAAE,CAAS;IAC/B,OAAO,CAAC,IAAI,CAAC,CAAA;AACf,CAAC;AAED,0EAA0E;AAC1E,gFAAgF;AAChF,2EAA2E;AAC3E,gFAAgF;AAChF,iCAAiC;AACjC,SAAS,OAAO,CACd,GAAa,EACb,GAAW,EACX,MAAgB,EAChB,GAAW,EACX,SAAiB,EACjB,WAAoB;IAEpB,MAAM,GAAG,GAAa,EAAE,CAAA;IACxB,IAAI,MAAM,GAAG,CAAC,CAAA;IACd,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACpC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACvC,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG;gBAAE,OAAO,GAAG,CAAA;YACjC,MAAM,SAAS,GAAI,GAAG,CAAC,CAAC,CAAY,GAAG,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,CAAA;YACtD,yEAAyE;YACzE,+DAA+D;YAC/D,IAAI,WAAW,IAAI,CAAC,SAAS;gBAAE,SAAQ;YACvC,IAAI,MAAM,GAAG,SAAS,CAAC,MAAM,GAAG,SAAS;gBAAE,OAAO,GAAG,CAAA;YACrD,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;YACnB,MAAM,IAAI,SAAS,CAAC,MAAM,CAAA;QAC5B,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,8EAA8E;AAC9E,iBAAiB;AACjB,SAAS,cAAc,CACrB,IAAY,EACZ,eAAwB,EACxB,GAAW,EACX,SAAiB;IAEjB,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;IAC5B,MAAM,CAAC,GAAa,EAAE,CAAA;IACtB,0EAA0E;IAC1E,mBAAmB;IACnB,qBAAqB;IACrB,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,SAAS,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,SAAS,EAAE,CAAC;QAC7C,OAAO,CAAC,CAAA;IACV,CAAC;IACD,oBAAoB;IACpB,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACvB,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACvB,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;IAChD,IAAI,IAAI,GACN,CAAC,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC;QACpC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACtC,CAACLine truncated
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEzC,MAAM,QAAQ,GAAG,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAA;AACjD,MAAM,OAAO,GAAG,QAAQ,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAA;AAC/C,MAAM,QAAQ,GAAG,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAA;AACjD,MAAM,QAAQ,GAAG,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAA;AACjD,MAAM,SAAS,GAAG,UAAU,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAA;AACnD,MAAM,eAAe,GAAG,IAAI,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAA;AACjD,MAAM,cAAc,GAAG,IAAI,MAAM,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;AAC/C,MAAM,eAAe,GAAG,IAAI,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAA;AACjD,MAAM,eAAe,GAAG,IAAI,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAA;AACjD,MAAM,gBAAgB,GAAG,IAAI,MAAM,CAAC,SAAS,EAAE,GAAG,CAAC,CAAA;AACnD,MAAM,YAAY,GAAG,OAAO,CAAA;AAC5B,MAAM,WAAW,GAAG,MAAM,CAAA;AAC1B,MAAM,YAAY,GAAG,MAAM,CAAA;AAC3B,MAAM,YAAY,GAAG,MAAM,CAAA;AAC3B,MAAM,aAAa,GAAG,OAAO,CAAA;AAE7B,MAAM,CAAC,MAAM,aAAa,GAAG,OAAO,CAAA;AAEpC,4EAA4E;AAC5E,2EAA2E;AAC3E,yEAAyE;AACzE,0EAA0E;AAC1E,6EAA6E;AAC7E,sEAAsE;AACtE,4EAA4E;AAC5E,0EAA0E;AAC1E,wEAAwE;AACxE,iDAAiD;AACjD,MAAM,CAAC,MAAM,oBAAoB,GAAG,SAAS,CAAA;AAE7C,+EAA+E;AAC/E,8EAA8E;AAC9E,+EAA+E;AAC/E,4EAA4E;AAC5E,gFAAgF;AAChF,4EAA4E;AAC5E,8EAA8E;AAC9E,yCAAyC;AACzC,MAAM,CAAC,MAAM,mBAAmB,GAAG,KAAK,CAAA;AAExC,+EAA+E;AAC/E,8EAA8E;AAC9E,+EAA+E;AAC/E,8EAA8E;AAC9E,4EAA4E;AAC5E,yEAAyE;AACzE,2EAA2E;AAC3E,mEAAmE;AACnE,MAAM,CAAC,MAAM,sBAAsB,GAAG,KAAK,CAAA;AAE3C,SAAS,OAAO,CAAC,GAAW;IAC1B,OAAO,CAAC,KAAK,CAAC,GAAU,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;AACnE,CAAC;AAED,SAAS,YAAY,CAAC,GAAW;IAC/B,OAAO,GAAG;SACP,OAAO,CAAC,YAAY,EAAE,QAAQ,CAAC;SAC/B,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC;SAC7B,OAAO,CAAC,YAAY,EAAE,QAAQ,CAAC;SAC/B,OAAO,CAAC,YAAY,EAAE,QAAQ,CAAC;SAC/B,OAAO,CAAC,aAAa,EAAE,SAAS,CAAC,CAAA;AACtC,CAAC;AAED,SAAS,cAAc,CAAC,GAAW;IACjC,OAAO,GAAG;SACP,OAAO,CAAC,eAAe,EAAE,IAAI,CAAC;SAC9B,OAAO,CAAC,cAAc,EAAE,GAAG,CAAC;SAC5B,OAAO,CAAC,eAAe,EAAE,GAAG,CAAC;SAC7B,OAAO,CAAC,eAAe,EAAE,GAAG,CAAC;SAC7B,OAAO,CAAC,gBAAgB,EAAE,GAAG,CAAC,CAAA;AACnC,CAAC;AAED,8DAA8D;AAC9D,SAAS,OAAO,CAAC,MAAgB,EAAE,KAAe;IAChD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAW,CAAC,CAAA;IACjC,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,SAAS,eAAe,CAAC,GAAW;IAClC,MAAM,KAAK,GAAa,EAAE,CAAA;IAE1B,8EAA8E;IAC9E,oEAAoE;IACpE,8EAA8E;IAC9E,4DAA4D;IAC5D,EAAE;IACF,oCAAoC;IACpC,IAAI,KAAK,GAAG,EAAE,CAAA;IAEd,SAAS,CAAC;QACR,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAA;QAEjC,IAAI,CAAC,CAAC,EAAE,CAAC;YACP,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;YAC3B,IAAI,CAAC,CAAC,CAAC,GAAG,KAAK,GAAI,IAAI,CAAC,CAAC,CAAY,CAAA;YACrC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;YACpB,OAAO,KAAK,CAAA;QACd,CAAC;QAED,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,CAAA;QAC7B,MAAM,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QACxB,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,GAAI,CAAC,CAAC,CAAC,CAAY,CAAA;QAC/B,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,GAAG,GAAG,IAAI,GAAG,GAAG,CAAA;QAEnC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACjB,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAA;YACjB,OAAO,KAAK,CAAA;QACd,CAAC;QAED,KAAK,GAAG,CAAC,CAAC,GAAG,EAAY,CAAA;QACzB,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAA;QACjB,GAAG,GAAG,IAAI,CAAA;IACZ,CAAC;AACH,CAAC;AASD,MAAM,UAAU,MAAM,CAAC,GAAW,EAAE,UAAiC,EAAE;IACrE,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO,EAAE,CAAA;IACX,CAAC;IAED,MAAM,EACJ,GAAG,GAAG,aAAa,EACnB,SAAS,GAAG,oBAAoB,EAChC,QAAQ,GAAG,mBAAmB,EAC9B,WAAW,GAAG,sBAAsB,GACrC,GAAG,OAAO,CAAA;IAEX,oDAAoD;IACpD,oEAAoE;IACpE,sEAAsE;IACtE,6CAA6C;IAC7C,oEAAoE;IACpE,+DAA+D;IAC/D,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAC7B,GAAG,GAAG,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IAC/B,CAAC;IAED,OAAO,OAAO,CACZ,YAAY,CAAC,GAAG,CAAC,EACjB,GAAG,EACH,SAAS,EACT,QAAQ,EACR,CAAC,EACD,WAAW,EACX,IAAI,CACL,CAAC,GAAG,CAAC,cAAc,CAAC,CAAA;AACvB,CAAC;AAED,SAAS,OAAO,CAAC,GAAW;IAC1B,OAAO,GAAG,GAAG,GAAG,GAAG,GAAG,CAAA;AACxB,CAAC;AAED,SAAS,QAAQ,CAAC,EAAU;IAC1B,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;AAC1B,CAAC;AAED,SAAS,GAAG,CAAC,CAAS,EAAE,CAAS;IAC/B,OAAO,CAAC,IAAI,CAAC,CAAA;AACf,CAAC;AAED,SAAS,GAAG,CAAC,CAAS,EAAE,CAAS;IAC/B,OAAO,CAAC,IAAI,CAAC,CAAA;AACf,CAAC;AAED,0EAA0E;AAC1E,gFAAgF;AAChF,2EAA2E;AAC3E,gFAAgF;AAChF,iCAAiC;AACjC,SAAS,OAAO,CACd,GAAa,EACb,GAAW,EACX,MAAgB,EAChB,GAAW,EACX,SAAiB,EACjB,WAAoB;IAEpB,MAAM,GAAG,GAAa,EAAE,CAAA;IACxB,IAAI,MAAM,GAAG,CAAC,CAAA;IACd,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACpC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACvC,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG;gBAAE,OAAO,GAAG,CAAA;YACjC,MAAM,SAAS,GAAI,GAAG,CAAC,CAAC,CAAY,GAAG,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,CAAA;YACtD,yEAAyE;YACzE,+DAA+D;YAC/D,IAAI,WAAW,IAAI,CAAC,SAAS;gBAAE,SAAQ;YACvC,IAAI,MAAM,GAAG,SAAS,CAAC,MAAM,GAAG,SAAS;gBAAE,OAAO,GAAG,CAAA;YACrD,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;YACnB,MAAM,IAAI,SAAS,CAAC,MAAM,CAAA;QAC5B,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,8EAA8E;AAC9E,iBAAiB;AACjB,SAALine truncated
+2 -1
View File
@@ -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"
},
-6
View File
@@ -1,6 +0,0 @@
*.js text eol=lf
*.txt text eol=lf
*.html text eol=lf
*.htm text eol=lf
*.ics -text
*.bin -text
-9
View File
@@ -1,9 +0,0 @@
'use strict';
module.exports = {
upgrade: true,
reject: [
// API changes break existing tests
'proxy'
]
};
-8
View File
@@ -1,8 +0,0 @@
node_modules
coverage
*.min.js
dist
build
.nyc_output
package-lock.json
CHANGELOG.md
-12
View File
@@ -1,12 +0,0 @@
{
"printWidth": 140,
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": true,
"quoteProps": "as-needed",
"trailingComma": "none",
"bracketSpacing": true,
"arrowParens": "avoid",
"endOfLine": "lf"
}
-10
View File
@@ -1,10 +0,0 @@
'use strict';
module.exports = {
printWidth: 160,
tabWidth: 4,
singleQuote: true,
endOfLine: 'lf',
trailingComma: 'none',
arrowParens: 'avoid'
};
-9
View File
@@ -1,9 +0,0 @@
{
"packages": {
".": {
"release-type": "node",
"package-name": "nodemailer",
"pull-request-title-pattern": "chore${scope}: release ${version} [skip-ci]"
}
}
}
+249
View File
@@ -1,5 +1,254 @@
# 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 "&lt;" 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)
### Bug Fixes
* **mime-node:** clean the boundary where it is written, not only where it is built ([e14278d](https://github.com/nodemailer/nodemailer/commit/e14278d2dae9427280c79450605a27b1c5d2c355))
* **mime-node:** drop every control character from multipart boundary material ([a82a355](https://github.com/nodemailer/nodemailer/commit/a82a35554848a2e07485ff81f80aefe2736e5a04))
## [10.0.7](https://github.com/nodemailer/nodemailer/compare/v10.0.6...v10.0.7) (2026-09-11)
### Bug Fixes
* **mime-funcs:** do not double encode Buffer input when chunking base64 mime words ([#1865](https://github.com/nodemailer/nodemailer/issues/1865)) ([4327a59](https://github.com/nodemailer/nodemailer/commit/4327a59939748a7f9394b0a029495fd476f68f30))
* **mime-node:** keep a boundary that is only line breaks from stripping to empty ([ec46800](https://github.com/nodemailer/nodemailer/commit/ec46800cdcab734d9aa79e1278e819962b3b8f09))
* **mime-node:** strip line breaks from multipart boundary material ([#1867](https://github.com/nodemailer/nodemailer/issues/1867)) ([03c1a5c](https://github.com/nodemailer/nodemailer/commit/03c1a5c48b6828d9392983d8718fdb1fd583a06c))
* **smtp-pool:** release rate-limited connections on close ([#1866](https://github.com/nodemailer/nodemailer/issues/1866)) ([7f5c7a4](https://github.com/nodemailer/nodemailer/commit/7f5c7a46b61da9f5c5feaffd921c55b9d5892ee5))
## [10.0.6](https://github.com/nodemailer/nodemailer/compare/v10.0.5...v10.0.6) (2026-09-11)
### Bug Fixes
* **addressparser:** scan free text for an address in linear time ([437d7fc](https://github.com/nodemailer/nodemailer/commit/437d7fc47403df176bc39271641541b7a9bce102))
## [10.0.5](https://github.com/nodemailer/nodemailer/compare/v10.0.4...v10.0.5) (2026-09-11)
### Bug Fixes
* **addressparser:** parse comment-joined addresses in linear time ([c07f175](https://github.com/nodemailer/nodemailer/commit/c07f17518d25aca8ab2ad66968dcbca538c24b89))
## [10.0.4](https://github.com/nodemailer/nodemailer/compare/v10.0.3...v10.0.4) (2026-09-11)
### Bug Fixes
* **fetch:** scope a cookie without a Path to the RFC 6265 default path ([2f907cb](https://github.com/nodemailer/nodemailer/commit/2f907cb71396860d0b2534dc72c3d252ef16339d))
* **fetch:** send cookies set with Path back to the exact path ([#1861](https://github.com/nodemailer/nodemailer/issues/1861)) ([d557113](https://github.com/nodemailer/nodemailer/commit/d5571139750de47abbac73a3b07ae90ff39a7bdd))
* **mail-composer:** keep httpHeaders and tls for href alternatives and icalEvent ([#1862](https://github.com/nodemailer/nodemailer/issues/1862)) ([7f502be](https://github.com/nodemailer/nodemailer/commit/7f502bee4a6b0386099c66c0a35d92850ec462f7))
* resolve well-known services by their primary domains ([#1859](https://github.com/nodemailer/nodemailer/issues/1859)) ([085f525](https://github.com/nodemailer/nodemailer/commit/085f525e6b292f0ca32ab1b20d4a4ee82fc40e2d))
* **ses-transport:** throw a configuration error when the SES client is missing ([#1863](https://github.com/nodemailer/nodemailer/issues/1863)) ([4d9c4c9](https://github.com/nodemailer/nodemailer/commit/4d9c4c966aaca61ff6d7649a1ac0aa0ceac6ef1e))
## [10.0.3](https://github.com/nodemailer/nodemailer/compare/v10.0.2...v10.0.3) (2026-09-10)
### Bug Fixes
* **fetch:** honor the cookie Domain attribute without accepting public suffixes ([1608391](https://github.com/nodemailer/nodemailer/commit/1608391ff4ccd5422a88e4cf760b26068aad6d39)), closes [#1856](https://github.com/nodemailer/nodemailer/issues/1856)
## [10.0.2](https://github.com/nodemailer/nodemailer/compare/v10.0.1...v10.0.2) (2026-09-09)
### Bug Fixes
* **mime-node:** flatten nested recipient arrays without recursion ([ebe0849](https://github.com/nodemailer/nodemailer/commit/ebe084940aef88278afc6016b78c6d1c3821bb66))
* **shared:** keep the TLS server name out of the DNS cache ([a6512db](https://github.com/nodemailer/nodemailer/commit/a6512dbcb3c6e7f2f70d3acccc5752defe3c61fe))
## [10.0.1](https://github.com/nodemailer/nodemailer/compare/v10.0.0...v10.0.1) (2026-09-07)
### Bug Fixes
* **types:** accept an explicit undefined for optional properties ([209719d](https://github.com/nodemailer/nodemailer/commit/209719dd6a192656a6b18de265a3b4ac387208ff)), closes [#1853](https://github.com/nodemailer/nodemailer/issues/1853)
* **types:** drop the internal members from the published declarations ([81e64ea](https://github.com/nodemailer/nodemailer/commit/81e64eaee70aabf867afef88d3733556c9fcc1cc))
## [10.0.0](https://github.com/nodemailer/nodemailer/compare/v9.1.1...v10.0.0) (2026-09-03)
### ⚠ BREAKING CHANGES
* Node.js 20 or newer is required. The Node.js 6 syntax compatibility check and the .npmignore file are gone.
### Features
* keep the @types/nodemailer type layout working ([1cc5356](https://github.com/nodemailer/nodemailer/commit/1cc535627e1bb672214b09fa4cc503567275fd7f))
* migrate to TypeScript with ES module and CommonJS builds ([f7cbf83](https://github.com/nodemailer/nodemailer/commit/f7cbf8384e0ea6055efcc514afb8bc070ac3a2b1))
### Bug Fixes
* apply the other keys of a configuration object next to its url ([29610f9](https://github.com/nodemailer/nodemailer/commit/29610f9fe04dbd8af04c4a3362f3b68d293592da))
* **dkim:** canonicalize raw messages the way verifiers do ([2c84b11](https://github.com/nodemailer/nodemailer/commit/2c84b117d4d138751e9ffa8607f3796771bb5e66))
* keep a transporter assignable to the plain Transporter type ([8bf55fb](https://github.com/nodemailer/nodemailer/commit/8bf55fb177cf155b24d6bb86a7cd86670146d551))
* **shared:** keep a colon in the user name of a connection or proxy url ([6acf4b6](https://github.com/nodemailer/nodemailer/commit/6acf4b67d919971f516f58940113c9b8598b2254))
* **shared:** refuse URL hosts the legacy parser would truncate ([17a5068](https://github.com/nodemailer/nodemailer/commit/17a5068f06e62fc04e1d5545ed70d4248bef258e))
* **shared:** resolve hostnames when the runtime has no interface table ([8b03240](https://github.com/nodemailer/nodemailer/commit/8b032407135ea05ae377e623a7021705a4a139d0))
* **smtp-connection:** clear the timers of a connection dropped before the greeting ([01dcaa0](https://github.com/nodemailer/nodemailer/commit/01dcaa05b212aafc33747041027fb8c66497e0eb))
* **smtp-connection:** keep an incomplete server reply out of lastServerResponse ([1a6e427](https://github.com/nodemailer/nodemailer/commit/1a6e4271adb808509adf11832ad1adfdfc05c31b))
* **smtp-pool:** free the pool slot when the proxy socket can not be opened ([204a344](https://github.com/nodemailer/nodemailer/commit/204a344f7e4068ff41385195ee394a223ebadd84))
* **well-known:** keep nodemailer/lib/well-known/services.json available ([367730c](https://github.com/nodemailer/nodemailer/commit/367730cce85bc78c7804405c2cc7d89d93aafdd1))
## [9.1.1](https://github.com/nodemailer/nodemailer/compare/v9.1.0...v9.1.1) (2026-09-01)
-56
View File
@@ -1,56 +0,0 @@
# Nodemailer
E-mail sending library for Node.js. Zero runtime dependencies. Entry point is `lib/nodemailer.js`, which exposes `createTransport(transporter, defaults)` and routes to one of the bundled transports based on the options object.
## Layout
- `lib/nodemailer.js` — public entry, transport dispatch (`createTransport`).
- `lib/mailer/` — `Mail` class: the user-facing transport wrapper that normalizes messages, runs the DKIM signer, and hands off to the underlying transport's `.send()`.
- `lib/mail-composer/` + `lib/mime-node/` — message → MIME tree → raw RFC822 stream.
- `lib/smtp-connection/` — low-level SMTP/LMTP/ESMTP client. Hot path; security-sensitive. Used by `smtp-transport` and `smtp-pool`.
- `lib/smtp-transport/` — single-shot SMTP transport.
- `lib/smtp-pool/` — pooled SMTP transport with rate limiting.
- `lib/sendmail-transport/`, `lib/ses-transport/`, `lib/stream-transport/`, `lib/json-transport/` — alternate transports.
- `lib/dkim/`, `lib/addressparser/`, `lib/mime-funcs/`, `lib/base64/`, `lib/qp/`, `lib/punycode/`, `lib/well-known/`, `lib/xoauth2/`, `lib/fetch/`, `lib/shared/` — supporting modules.
- `test/` — mirrors `lib/` structure. Most suites spin up real `smtp-server` instances on ephemeral ports; raw `net` servers are used when byte-exact reply control is needed (e.g. injecting non-ASCII or invalid UTF-8).
Each transport must implement `name`, `version`, and `send(mail, callback)`. `Mail` discovers them via duck typing.
## Engine target
`engines.node = ">=6.0.0"`. The library is shipped as ES2017 script-mode CommonJS — no `import`, no top-level `await`, no optional chaining, no nullish coalescing, no class fields. ESLint enforces `ecmaVersion: 2017` and `sourceType: 'script'`. There is a Node 6 syntax-compat check (`npm run test:syntax`, runs `test/syntax-compat.js` inside `node:6-alpine`) that must keep passing — do not introduce syntax that breaks it. `'use strict';` directive at the top of every file.
## Conventions
- CommonJS only: `const x = require('...')`, `module.exports = ...`.
- Callback-first style throughout the public API. Many internals are still callback-based — match the style of the file you are editing rather than introducing promises mid-module.
- Prettier handles formatting; ESLint handles correctness. Run `npm run format` and `npm run lint` before sending changes. The lint config disables Prettier-overlapping rules.
- Snake_case is not used; camelCase for variables and methods, PascalCase for classes.
- Prefer small, surgical diffs. The codebase is mature and load-bearing — avoid drive-by refactors, comment churn, or "improvements" outside the scope of the change.
- Every change to security-sensitive code (anything in `lib/smtp-connection/`, address parsing, header generation, DKIM) needs tests that exercise the failure mode, not just the happy path.
## Testing
- `npm test` — full suite via `node --test` (~150s, 480+ tests, runs serially).
- `npm run test:coverage` — same suite under `c8`.
- `npm run test:syntax` — Node 6 syntax compatibility check in Docker.
- `npm run lint` / `npm run lint:fix`.
- `npm run format` / `npm run format:check`.
Always run `npm test` and `npm run lint` before considering a change done. Tests are required to pass on every commit because release-please cuts releases directly from `master`.
## Releases
Releases, version numbers, the `version` field in `package.json`, git tags, `CHANGELOG.md` entries, and npm publication are all managed automatically by the release-please GitHub Action (`.github/workflows/release.yaml`, configured by `.release-please-config.json`). **Never edit any of these manually and never propose manual edits to them.**
Release-please derives the next version and changelog from Conventional Commit messages on `master`, opens a release PR (`chore: release X.Y.Z [skip-ci]`), and publishes to npm with provenance when that PR is merged. The only thing that should land on `master` between releases is normal commits with Conventional Commit prefixes — release-please takes care of the rest.
Conventional Commit prefixes used in this repo: `fix:`, `feat:`, `chore:`, `docs:`, `refactor:`, `test:`. Use `fix:` for anything users would benefit from seeing in the changelog, including security fixes (reference the GHSA in the body).
## Security
This is a widely-deployed library — security-sensitive changes get extra scrutiny:
- SMTP command injection: any user-controllable value that flows into a written SMTP command (envelope addresses, sizes, the `name`/EHLO option, headers) must be CRLF-stripped or rejected at the boundary. Sanitize at the assignment, not at every call site.
- Server reply parsing in `lib/smtp-connection/index.js` uses a `'binary'` byte-container intermediate to reassemble multi-byte UTF-8 across socket chunks; the actual decode happens at line boundaries via `decodeServerResponse`. Don't change the chunk-buffering encoding without understanding why.
- Reference the GHSA ID in commit messages for advisories.
-76
View File
@@ -1,76 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at info@nodemailer.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
+7 -3
View File
@@ -10,15 +10,19 @@ See [nodemailer.com](https://nodemailer.com/) for documentation and terms.
> Nodemailer is developed by the team behind **[EmailEngine](https://emailengine.app/?utm_source=nodemailer-readme&utm_medium=readme&utm_campaign=oss-docs)**, a self-hosted email API that turns any Gmail, Microsoft 365, or IMAP account into a REST endpoint, with managed OAuth2, webhooks for incoming mail, and built-in [sending](https://emailengine.app/sending-emails?utm_source=nodemailer-readme&utm_medium=readme&utm_campaign=oss-docs). If you would rather call an HTTP API than maintain IMAP and SMTP connections yourself, that is what it is for.
## Supported runtimes
Nodemailer targets Node.js 20 and later. The ES module build also runs on [Bun](https://bun.sh/) (tested against the latest release) and on [Cloudflare Workers](https://developers.cloudflare.com/workers/) with the `nodejs_compat` compatibility flag. On Workers only the SMTP based transports apply, `sendmail` needs a child process, and the runtime does not allow turning certificate validation off, so `tls.rejectUnauthorized: false` fails there with an error.
## Having an issue?
#### First review the docs
Documentation for Nodemailer can be found at [nodemailer.com](https://nodemailer.com/about/).
#### Nodemailer throws a SyntaxError for "..."
#### Nodemailer fails to load or throws a SyntaxError
You are using an older Node.js version than v6.0. Upgrade Node.js to get support for the spread operator. Nodemailer supports all Node.js versions starting from Node.js@v6.0.0.
Nodemailer 10 and later require Node.js 20 or newer. The package ships both ES module and CommonJS builds, so `import nodemailer from 'nodemailer'` and `const nodemailer = require('nodemailer')` both work. If you need to run on an older Node.js version, stay on the 9.x line.
#### I'm having issues with Gmail
@@ -68,7 +72,7 @@ let configOptions = {
#### I have an issue with TypeScript types
Nodemailer has official support for Node.js only. For anything related to TypeScript, you need to directly contact the authors of the [type definitions](https://www.npmjs.com/package/@types/nodemailer).
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
+108 -32
View File
@@ -1,61 +1,137 @@
# 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
Security fixes are released only against the latest major version. We do not
backport patches to older majors — upgrading to the current release line is the
supported way to receive security updates.
backport patches to older majors, so upgrading to the current release line is
the supported way to receive security updates.
| Version | Supported |
| ------- | ------------------ |
| 9.x | :white_check_mark: |
| < 9.0 | :x: |
| 10.x | :white_check_mark: |
| < 10.0 | :x: |
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
In scope: the `nodemailer` package source in this repository: message and MIME
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
+59
View File
@@ -0,0 +1,59 @@
/**
* Options for addressparser
*/
export interface AddressParserOptions {
/** Flatten groups into a single list of mailboxes */
flatten?: boolean | undefined;
}
/**
* A single mailbox. Either value may be an empty string when the input did not carry it
*/
export interface MailboxAddress {
name: string;
address: string;
group?: undefined;
}
/**
* An address group. RFC 5322 does not allow nested groups, so any nesting is flattened
* into `group`
*/
export interface GroupAddress {
name: string;
group: Address[];
address?: undefined;
}
/**
* A parsed address entry, either a mailbox or a group
*/
export type Address = MailboxAddress | GroupAddress;
/**
* Parses structured e-mail addresses from an address field
*
* Example:
*
* 'Name <address@domain>'
*
* will be converted to
*
* [{name: 'Name', address: 'address@domain'}]
*
* @param str Address field
* @param options Optional options object
* @return An array of address objects
*/
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;
File diff suppressed because it is too large. Load diff
+39
View File
@@ -0,0 +1,39 @@
import { Transform } from 'node:stream';
/**
* Encodes a Buffer into a base64 encoded string
*
* @param buffer Buffer to convert
* @returns base64 encoded string
*/
export declare function encode(buffer: Buffer | string): string;
/**
* Adds soft line breaks to a base64 string
*
* @param str base64 encoded string that might need line wrapping
* @param [lineLength=76] Maximum allowed length for a line
* @returns Soft-wrapped base64 encoded string
*/
export declare function wrap(str: string, lineLength?: number | false): string;
/**
* Options for the base64 encoder stream
*/
export interface EncoderOptions {
/** Maximum length for lines, set to false to disable wrapping */
lineLength?: number | false | undefined;
}
/**
* 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
*/
export declare class Encoder extends Transform {
options: EncoderOptions;
inputBytes: number;
outputBytes: number;
constructor(options?: EncoderOptions);
}
+158
View File
@@ -0,0 +1,158 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Encoder = void 0;
exports.encode = encode;
exports.wrap = wrap;
const node_stream_1 = require("node:stream");
/**
* Encodes a Buffer into a base64 encoded string
*
* @param buffer Buffer to convert
* @returns base64 encoded string
*/
function encode(buffer) {
if (typeof buffer === 'string') {
buffer = Buffer.from(buffer, 'utf-8');
}
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
*
* @param str base64 encoded string that might need line wrapping
* @param [lineLength=76] Maximum allowed length for a line
* @returns Soft-wrapped base64 encoded string
*/
function wrap(str, lineLength) {
str = (str || '').toString();
// 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;
}
const result = [];
let pos = 0;
const chunkLength = lineLength * 1024;
const wrapRegex = new RegExp('.{' + lineLength + '}', 'g');
while (pos < str.length) {
const wrappedLines = str.substr(pos, chunkLength).replace(wrapRegex, '$&\r\n').trim();
result.push(wrappedLines);
pos += chunkLength;
}
return result.join('\r\n').trim();
}
/**
* 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
*/
class Encoder extends node_stream_1.Transform {
constructor(options) {
super();
this.options = options || {};
if (this.options.lineLength !== false) {
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) {
return done();
}
this.inputBytes += buf.length;
if (this._remainingBytes) {
buf = Buffer.concat([this._remainingBytes, buf], this._remainingBytes.length + buf.length);
this._remainingBytes = false;
}
const extra = buf.length % 3;
if (extra) {
this._remainingBytes = buf.subarray(buf.length - extra);
buf = buf.subarray(0, buf.length - extra);
}
this._emit(encode(buf), false);
done();
}
/** @internal */
_flush(done) {
this._emit(this._remainingBytes ? encode(this._remainingBytes) : '', true);
this._remainingBytes = false;
done();
}
}
exports.Encoder = Encoder;
+52
View File
@@ -0,0 +1,52 @@
import { type DKIMKey, type DKIMPrivateKey, type DKIMSignOptions } from './sign.js';
import { PassThrough, type Readable } from 'node:stream';
/**
* A single DKIM signing key
*/
export type { DKIMKey, DKIMPrivateKey, DKIMSignOptions };
/**
* Options for the DKIM signer
*/
export interface DKIMOptions extends DKIMSignOptions {
/** One or more signing keys, used instead of the domainName, keySelector and privateKey options */
keys?: DKIMKey | DKIMKey[] | undefined;
/** Directory for buffering large message bodies to disk, no buffering when not set */
cacheDir?: string | false | undefined;
/** Body size in bytes from which the body is buffered to cacheDir, defaults to 10 MB */
cacheTreshold?: number | undefined;
/** Hash algorithm for the body hash and the signature, defaults to sha256 */
hashAlgo?: string | undefined;
}
/**
* The signed message as returned by DKIM#sign
*/
export interface DKIMSignedStream extends PassThrough {
/** true if the message body was buffered to cacheDir while signing */
usingCache: boolean;
}
declare class DKIM {
options: DKIMOptions;
keys: DKIMKey[];
constructor(options: DKIMOptions);
sign(input: Readable | Buffer | string, extraOptions?: DKIMOptions): DKIMSignedStream;
}
/** 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 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;
+243
View File
@@ -0,0 +1,243 @@
"use strict";
// FIXME:
// replace this Transform mess with a method that pipes input argument to output argument
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const message_parser_js_1 = __importDefault(require("./message-parser.js"));
const relaxed_body_js_1 = __importDefault(require("./relaxed-body.js"));
const sign_js_1 = __importDefault(require("./sign.js"));
const node_stream_1 = require("node:stream");
const node_fs_1 = __importDefault(require("node:fs"));
const node_path_1 = __importDefault(require("node:path"));
const node_crypto_1 = __importDefault(require("node:crypto"));
const objects_js_1 = require("../shared/objects.js");
const DKIM_ALGO = 'sha256';
const MAX_MESSAGE_SIZE = 10 * 1024 * 1024; // buffer messages larger than this to disk
class DKIMSigner {
constructor(options, keys, input, output) {
this.options = options || {};
this.keys = keys;
this.cacheTreshold = Number(this.options.cacheTreshold) || MAX_MESSAGE_SIZE;
this.hashAlgo = this.options.hashAlgo || DKIM_ALGO;
this.cacheDir = this.options.cacheDir || false;
this.chunks = [];
this.chunklen = 0;
this.readPos = 0;
this.cachePath = this.cacheDir
? node_path_1.default.join(this.cacheDir, 'message.' + Date.now() + '-' + node_crypto_1.default.randomBytes(14).toString('hex'))
: false;
this.cache = false;
this.headers = false;
this.bodyHash = false;
this.parser = false;
this.relaxedBody = false;
this.input = input;
this.output = output;
this.output.usingCache = false;
this.hasErrored = false;
this.input.on('error', err => this.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();
});
}
/**
* 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.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;
}
let keyPos = 0;
const signNextKey = () => {
if (keyPos >= this.keys.length) {
this.output.write(this.parser.rawHeaders);
setImmediate(() => this.sendNextChunk());
return;
}
const key = this.keys[keyPos++];
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'));
}
setImmediate(signNextKey);
};
if (this.bodyHash && this.headers) {
return signNextKey();
}
this.output.write(this.parser.rawHeaders);
this.sendNextChunk();
}
createWriteCache() {
this.output.usingCache = true;
// pipe remainings to cache file. 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 => {
// drain input
this.relaxedBody.unpipe(this.cache);
this.relaxedBody.on('readable', () => {
while (this.relaxedBody.read() !== null) {
// do nothing
}
});
this.fail(err);
});
this.cache.once('close', () => {
if (this.hasErrored) {
return;
}
this.sendSignedOutput();
});
this.relaxedBody.removeAllListeners('readable');
this.relaxedBody.pipe(this.cache);
}
signStream() {
this.parser = new message_parser_js_1.default();
this.relaxedBody = new relaxed_body_js_1.default({
hashAlgo: this.hashAlgo
});
this.parser.on('headers', value => {
this.headers = value;
});
this.relaxedBody.on('hash', value => {
this.bodyHash = value;
});
this.relaxedBody.on('readable', () => {
let chunk;
if (this.cache) {
return;
}
while ((chunk = this.relaxedBody.read()) !== null) {
this.chunks.push(chunk);
this.chunklen += chunk.length;
if (this.chunklen >= this.cacheTreshold && this.cachePath) {
return this.createWriteCache();
}
}
});
this.relaxedBody.on('end', () => {
if (this.cache) {
return;
}
this.sendSignedOutput();
});
this.parser.pipe(this.relaxedBody);
setImmediate(() => this.input.pipe(this.parser));
}
}
class DKIM {
constructor(options) {
this.options = options || {};
this.keys = [].concat(this.options.keys || {
domainName: options.domainName,
keySelector: options.keySelector,
privateKey: options.privateKey
});
}
sign(input, extraOptions) {
const output = new node_stream_1.PassThrough();
let inputStream = input;
let writeValue = false;
if (Buffer.isBuffer(input)) {
writeValue = input;
inputStream = new node_stream_1.PassThrough();
}
else if (typeof input === 'string') {
writeValue = Buffer.from(input);
inputStream = new node_stream_1.PassThrough();
}
let options = this.options;
if (extraOptions && Object.keys(extraOptions).length) {
// extraOptions is mail.data._dkim, caller supplied message data. An own
// "__proto__" key there would let every option this signer reads and the
// transport did not set, such as skipFields, answer from the caller
options = (0, objects_js_1.copyOwnKeys)({}, extraOptions);
(0, objects_js_1.copyOwnKeys)(options, this.options);
}
const signer = new DKIMSigner(options, this.keys, inputStream, output);
setImmediate(() => {
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);
});
}
});
return output;
}
}
exports.default = DKIM;
module.exports = exports.default;
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
+48
View File
@@ -0,0 +1,48 @@
import { Transform, type TransformOptions } from 'node:stream';
/**
* A header line as emitted with the 'headers' event
*/
export interface MessageParserHeaderLine {
/** Lowercase header field name */
key: string;
/** Full header line, folded continuation lines included, one character per byte ('binary' encoding) */
line: string;
}
/**
* MessageParser instance is a transform stream that separates message headers
* from the rest of the body. Headers are emitted with the 'headers' event. Message
* body is passed on as the resulting stream.
*/
declare class MessageParser extends Transform {
lastBytes: Buffer;
headersParsed: boolean;
headerBytes: number;
headerChunks: Buffer[] | null;
rawHeaders: Buffer | false;
bodySize: number;
constructor(options?: TransformOptions);
/**
* Keeps count of the last 4 bytes in order to detect line breaks on chunk boundaries
*
* @param data Next data chunk from the stream
*/
updateLastBytes(data: Buffer): void;
/**
* Finds and removes message headers from the remaining body. We want to keep
* headers separated until final delivery to be able to modify these
*
* @param data Next chunk of data
* @return Returns true if headers are already found or false otherwise
*/
checkHeaders(data: Buffer): boolean;
parseHeaders(): MessageParserHeaderLine[];
}
/**
* 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;
+180
View File
@@ -0,0 +1,180 @@
"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
* body is passed on as the resulting stream.
*/
class MessageParser extends node_stream_1.Transform {
constructor(options) {
super(options);
this.lastBytes = Buffer.alloc(4);
this.headersParsed = false;
this.headerBytes = 0;
this.headerChunks = [];
this.rawHeaders = false;
this.bodySize = 0;
}
/**
* Keeps count of the last 4 bytes in order to detect line breaks on chunk boundaries
*
* @param data Next data chunk from the stream
*/
updateLastBytes(data) {
const lblen = this.lastBytes.length;
const nblen = Math.min(data.length, lblen);
// shift existing bytes
for (let i = 0, len = lblen - nblen; i < len; i++) {
this.lastBytes[i] = this.lastBytes[i + nblen];
}
// add new bytes
for (let i = 1; i <= nblen; i++) {
this.lastBytes[lblen - i] = data[data.length - i];
}
}
/**
* Finds and removes message headers from the remaining body. We want to keep
* headers separated until final delivery to be able to modify these
*
* @param data Next chunk of data
* @return Returns true if headers are already found or false otherwise
*/
checkHeaders(data) {
if (this.headersParsed) {
return true;
}
const lblen = this.lastBytes.length;
let headerPos = 0;
for (let i = 0, len = this.lastBytes.length + data.length; i < len; i++) {
let chr;
if (i < lblen) {
chr = this.lastBytes[i];
}
else {
chr = data[i - lblen];
}
if (chr === 0x0a && i) {
const pr1 = i - 1 < lblen ? this.lastBytes[i - 1] : data[i - 1 - lblen];
const pr2 = i > 1 ? (i - 2 < lblen ? this.lastBytes[i - 2] : data[i - 2 - lblen]) : false;
if (pr1 === 0x0a) {
this.headersParsed = true;
headerPos = i - lblen + 1;
this.headerBytes += headerPos;
break;
}
else if (pr1 === 0x0d && pr2 === 0x0a) {
this.headersParsed = true;
headerPos = i - lblen + 1;
this.headerBytes += headerPos;
break;
}
}
}
if (this.headersParsed) {
this.headerChunks.push(data.slice(0, headerPos));
this.rawHeaders = Buffer.concat(this.headerChunks, this.headerBytes);
this.headerChunks = null;
this.emit('headers', this.parseHeaders());
if (data.length > headerPos) {
const chunk = data.slice(headerPos);
this.bodySize += chunk.length;
// this would be the first chunk of data sent downstream
this.push(chunk);
}
return false;
}
this.headerBytes += data.length;
this.headerChunks.push(data);
// store last 4 bytes to catch header break
this.updateLastBytes(data);
return false;
}
/** @internal */
_transform(chunk, encoding, callback) {
if (!chunk || !chunk.length) {
return callback();
}
if (typeof chunk === 'string') {
chunk = Buffer.from(chunk, encoding);
}
let headersFound;
try {
headersFound = this.checkHeaders(chunk);
}
catch (E) {
return callback(E);
}
if (headersFound) {
this.bodySize += chunk.length;
this.push(chunk);
}
callback();
}
/** @internal */
_flush(callback) {
if (this.headerChunks) {
// no empty line was seen, so the message consists of headers only
this.rawHeaders = Buffer.concat(this.headerChunks, this.headerBytes);
this.headerChunks = null;
this.emit('headers', this.parseHeaders());
}
callback();
}
parseHeaders() {
// the header bytes are kept as they are, one character per byte, so the
// signature covers exactly the bytes the receiving side canonicalizes
// Only SP and HTAB fold a line, and only they are trimmed from the field name, the
// same whitespace the relaxed canonicalization in sign.ts works with
const 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: _trimFieldName(line.substr(0, line.indexOf(':'))).toLowerCase(),
line
}));
}
}
exports.default = MessageParser;
module.exports = exports.default;
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
+36
View File
@@ -0,0 +1,36 @@
import { Transform } from 'node:stream';
import crypto from 'node:crypto';
/**
* Options for the relaxed body hash stream
*/
export interface RelaxedBodyOptions {
/** Hash algorithm for the body hash, defaults to sha256 */
hashAlgo?: string | undefined;
/** Collect the canonicalized body and emit it with the 'hash' event */
debug?: boolean | undefined;
}
/**
* Passes the message body through unchanged and hashes its relaxed
* canonicalization (RFC 6376 section 3.4.4) on the side: whitespace at the end
* of a line is dropped, runs of whitespace within a line become a single space,
* every line ends with CRLF, empty lines at the end of the body are ignored and
* a non-empty body always ends with CRLF. Bytes are canonicalized as they arrive,
* so a line of any length costs constant memory.
*/
declare class RelaxedBody extends Transform {
bodyHash: crypto.Hash;
/** Bytes of the original body seen so far */
byteLength: number;
debug: boolean | undefined;
constructor(options?: RelaxedBodyOptions);
updateHash(chunk: Buffer, final?: boolean): void;
}
/**
* 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;
+148
View File
@@ -0,0 +1,148 @@
"use strict";
// streams through a message body and calculates relaxed body hash
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const node_stream_1 = require("node:stream");
const node_crypto_1 = __importDefault(require("node:crypto"));
const CHAR_CR = 0x0d;
const CHAR_LF = 0x0a;
const CHAR_SPACE = 0x20;
const CHAR_TAB = 0x09;
const CRLF = Buffer.from('\r\n');
// a run of empty lines is hashed from this buffer in slices
const EMPTY_LINES = Buffer.alloc(4096, CRLF);
/**
* Passes the message body through unchanged and hashes its relaxed
* canonicalization (RFC 6376 section 3.4.4) on the side: whitespace at the end
* of a line is dropped, runs of whitespace within a line become a single space,
* every line ends with CRLF, empty lines at the end of the body are ignored and
* a non-empty body always ends with CRLF. Bytes are canonicalized as they arrive,
* so a line of any length costs constant memory.
*/
class RelaxedBody extends node_stream_1.Transform {
constructor(options) {
super();
options = options || {};
this.bodyHash = node_crypto_1.default.createHash(options.hashAlgo || 'sha256');
this.byteLength = 0;
this.debug = options.debug;
this._debugBody = options.debug ? [] : false;
this._lineHasContent = false;
this._pendingWsp = false;
this._pendingCr = false;
this._pendingEmptyLines = 0;
}
/** @internal */
_hashCanonical(data) {
if (!data.length) {
return;
}
this.bodyHash.update(data);
if (this._debugBody) {
this._debugBody.push(Buffer.from(data));
}
}
/** @internal */
_hashEmptyLines() {
while (this._pendingEmptyLines > 0) {
const count = Math.min(this._pendingEmptyLines, EMPTY_LINES.length / 2);
this._hashCanonical(EMPTY_LINES.subarray(0, count * 2));
this._pendingEmptyLines -= count;
}
}
/**
* Writes a content byte, with the space a pending run of whitespace collapses to,
* into the output buffer and returns the new write position. Kept a method rather
* than a closure so the write position stays a plain local in the byte loop
* @internal
*/
_emitContent(out, outPos, c) {
if (!this._lineHasContent) {
if (this._pendingEmptyLines) {
// the first content byte of a line is where the empty lines before it
// become part of the body, so hash what is in the buffer before them
this._hashCanonical(out.subarray(0, outPos));
outPos = 0;
this._hashEmptyLines();
}
this._lineHasContent = true;
}
if (this._pendingWsp) {
out[outPos++] = CHAR_SPACE;
this._pendingWsp = false;
}
out[outPos++] = c;
return outPos;
}
updateHash(chunk, final) {
// every byte contributes itself at most once, plus a CR for a bare LF
// and, once per chunk, a pending space and CR carried over from before
const out = Buffer.allocUnsafe(chunk.length * 2 + 2);
let outPos = 0;
for (let i = 0; i < chunk.length; i++) {
const c = chunk[i];
if (c === CHAR_LF) {
// end of line, a CR right before it and any trailing whitespace are dropped
if (this._lineHasContent) {
out[outPos++] = CHAR_CR;
out[outPos++] = CHAR_LF;
this._lineHasContent = false;
}
else {
this._pendingEmptyLines++;
}
this._pendingWsp = false;
this._pendingCr = false;
continue;
}
if (this._pendingCr) {
// not followed by LF, so the CR is content
outPos = this._emitContent(out, outPos, CHAR_CR);
this._pendingCr = false;
}
if (c === CHAR_CR) {
this._pendingCr = true;
}
else if (c === CHAR_SPACE || c === CHAR_TAB) {
this._pendingWsp = true;
}
else {
outPos = this._emitContent(out, outPos, c);
}
}
if (final && this._pendingCr) {
// a CR at the very end of the body is content
outPos = this._emitContent(out, outPos, CHAR_CR);
this._pendingCr = false;
}
this._hashCanonical(out.subarray(0, outPos));
}
/** @internal */
_transform(chunk, encoding, callback) {
if (!chunk || !chunk.length) {
return callback();
}
if (typeof chunk === 'string') {
chunk = Buffer.from(chunk, encoding);
}
this.updateHash(chunk);
this.byteLength += chunk.length;
this.push(chunk);
callback();
}
/** @internal */
_flush(callback) {
this.updateHash(Buffer.alloc(0), true);
if (this._lineHasContent) {
// the body does not end with a line break, add one
this._hashCanonical(CRLF);
}
this.emit('hash', this.bodyHash.digest('base64'), this.debug ? Buffer.concat(this._debugBody) : false);
callback();
}
}
exports.default = RelaxedBody;
module.exports = exports.default;
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
+52
View File
@@ -0,0 +1,52 @@
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
* object with the key and its passphrase
*/
export type DKIMPrivateKey = crypto.KeyLike | crypto.SignKeyObjectInput | crypto.SignPrivateKeyInput;
/**
* Options for the DKIM signature header generator
*/
export interface DKIMKey {
/** Domain name to be signed for */
domainName?: string | undefined;
/** DKIM key selector to use */
keySelector?: string | undefined;
/** DKIM private key to use */
privateKey?: DKIMPrivateKey | undefined;
}
export interface DKIMSignOptions extends DKIMKey {
/** Colon separated list of header field names to sign, defaults to the RFC4871 list */
headerFieldNames?: string | undefined;
/** Colon separated list of header field names to leave out of the signature */
skipFields?: string | undefined;
}
/**
* Canonicalized headers and the list of field names that went into them
*/
export interface DKIMRelaxedHeaders {
/** Relaxed header lines, each terminated with CRLF, one character per byte ('binary' encoding) */
headers: string;
/** Colon separated list of the field names that were included */
fieldNames: string;
}
/**
* Returns DKIM signature header line
*
* @param headers Parsed headers object from MessageParser
* @param 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
* @param options.keySelector DKIM key selector to use
* @param options.privateKey DKIM private key to use
* @return Complete header line
*/
declare function sign(headers: MessageParserHeaderLine[], hashAlgo: string, bodyHash: string, options?: DKIMSignOptions): string | false;
declare namespace sign {
var relaxedHeaders: (headers: MessageParserHeaderLine[], fieldNames?: string, skipFields?: string) => DKIMRelaxedHeaders;
var unsupportedHashAlgoError: (hashAlgo: string) => NodemailerError;
}
export default sign;
+165
View File
@@ -0,0 +1,165 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const punycode = __importStar(require("../punycode/index.js"));
const mimeFuncs = __importStar(require("../mime-funcs/index.js"));
const node_crypto_1 = __importDefault(require("node:crypto"));
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
* @param options.keySelector DKIM key selector to use
* @param options.privateKey DKIM private key to use
* @return Complete header line
*/
function sign(headers, hashAlgo, bodyHash, options) {
options = options || {};
// all listed fields from RFC4871 #5.5
const defaultFieldNames = 'From:Sender:Reply-To:Subject:Date:Message-ID:To:' +
'Cc:MIME-Version:Content-Type:Content-Transfer-Encoding:Content-ID:' +
'Content-Description:Resent-Date:Resent-From:Resent-Sender:' +
'Resent-To:Resent-Cc:Resent-Message-ID:In-Reply-To:References:' +
'List-Id:List-Help:List-Unsubscribe:List-Subscribe:List-Post:' +
'List-Owner:List-Archive';
const fieldNames = options.headerFieldNames || defaultFieldNames;
const canonicalizedHeaderData = relaxedHeaders(headers, fieldNames, options.skipFields);
const dkimHeader = generateDKIMHeader(options.domainName, options.keySelector, canonicalizedHeaderData.fieldNames, hashAlgo, bodyHash);
canonicalizedHeaderData.headers += 'dkim-signature:' + relaxedHeaderLine(dkimHeader);
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;
try {
signature = signer.sign(options.privateKey, 'base64');
}
catch (_E) {
return false;
}
return dkimHeader + signature.replace(/(^.{73}|.{75}(?!\r?\n|\r))/g, '$&\r\n ').trim();
}
sign.relaxedHeaders = relaxedHeaders;
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
// them has any way to carry a control char, DEL, or one of the delimiters that would close
// the value and open a tag of its own
const cleanTagValue = (value) => (value || '').toString().replace(/[\x00-\x1f\x7f;=]/g, '');
const dkim = [
'v=1',
'a=rsa-' + hashAlgo,
'c=relaxed/relaxed',
'd=' + punycode.toASCII(cleanTagValue(domainName)),
'q=dns/txt',
's=' + cleanTagValue(keySelector),
'bh=' + bodyHash,
'h=' + cleanTagValue(fieldNames)
].join('; ');
return mimeFuncs.foldLines('DKIM-Signature: ' + dkim, 76) + ';\r\n b=';
}
function relaxedHeaders(headers, fieldNames, skipFields) {
const includedFields = new Set();
const skip = new Set();
const headerFields = new Map();
(skipFields || '')
.toLowerCase()
.split(':')
.forEach(field => {
skip.add(field.trim());
});
(fieldNames || '')
.toLowerCase()
.split(':')
.filter(field => !skip.has(field.trim()))
.forEach(field => {
includedFields.add(field.trim());
});
for (let i = headers.length - 1; i >= 0; i--) {
const line = headers[i];
// only include the first value from bottom to top
if (includedFields.has(line.key) && !headerFields.has(line.key)) {
headerFields.set(line.key, relaxedHeaderLine(line.line));
}
}
const headersList = [];
const fields = [];
includedFields.forEach(field => {
if (headerFields.has(field)) {
fields.push(field);
headersList.push(field + ':' + headerFields.get(field));
}
});
return {
headers: headersList.join('\r\n') + '\r\n',
fieldNames: fields.join(':')
};
}
/**
* Relaxed canonicalization of a header field value (RFC 6376 section 3.4.2): unfold, turn
* every run of SP and HTAB into a single SP and drop the whitespace next to the colon and
* at the end. Only SP and HTAB count as whitespace, so bytes that decode to other space
* characters, such as a non-breaking space in a UTF-8 header, stay as they are
*/
function relaxedHeaderLine(line) {
return line
.substr(line.indexOf(':') + 1)
.replace(/\r?\n/g, '')
.replace(/[ \t]+/g, ' ')
.replace(/^ | $/g, '');
}
module.exports = exports.default;
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
+113
View File
@@ -0,0 +1,113 @@
/**
* Nodemailer Error Codes
*
* Centralized error code definitions for consistent error handling.
*
* Usage:
* import * as errors from './errors.js';
* const err: NodemailerError = new Error('Connection closed');
* err.code = errors.ECONNECTION;
*/
/**
* Error code descriptions for documentation and debugging
*/
export declare const ERROR_CODES: {
readonly ECONNECTION: "Connection closed unexpectedly";
readonly ETIMEDOUT: "Connection or operation timed out";
readonly ESOCKET: "Socket-level error";
readonly EDNS: "DNS resolution failed";
readonly ETLS: "TLS handshake or STARTTLS failed";
readonly EREQUIRETLS: "REQUIRETLS not supported by server (RFC 8689)";
readonly EPROTOCOL: "Invalid SMTP server response";
readonly EENVELOPE: "Invalid mail envelope (sender or recipients)";
readonly EMESSAGE: "Message delivery error";
readonly ESTREAM: "Stream processing error";
readonly EAUTH: "Authentication failed";
readonly ENOAUTH: "Authentication credentials not provided";
readonly EOAUTH2: "OAuth2 token generation or refresh error";
readonly EMAXLIMIT: "Pool resource limit reached (max messages per connection)";
readonly EMAXRECIPIENTS: "Recipient count exceeds maxRecipients";
readonly ESENDMAIL: "Sendmail command error";
readonly ESES: "AWS SES transport error";
readonly ECONFIG: "Invalid configuration";
readonly EPROXY: "Proxy connection error";
readonly EFILEACCESS: "File access rejected (disableFileAccess is set)";
readonly EURLACCESS: "URL access rejected (disableUrlAccess is set)";
readonly EFETCH: "HTTP fetch error";
};
/**
* Union of all known Nodemailer error codes
*/
export type ErrorCode = keyof typeof ERROR_CODES;
export declare const ECONNECTION = "ECONNECTION";
export declare const ETIMEDOUT = "ETIMEDOUT";
export declare const ESOCKET = "ESOCKET";
export declare const EDNS = "EDNS";
export declare const ETLS = "ETLS";
export declare const EREQUIRETLS = "EREQUIRETLS";
export declare const EPROTOCOL = "EPROTOCOL";
export declare const EENVELOPE = "EENVELOPE";
export declare const EMESSAGE = "EMESSAGE";
export declare const ESTREAM = "ESTREAM";
export declare const EAUTH = "EAUTH";
export declare const ENOAUTH = "ENOAUTH";
export declare const EOAUTH2 = "EOAUTH2";
export declare const EMAXLIMIT = "EMAXLIMIT";
export declare const EMAXRECIPIENTS = "EMAXRECIPIENTS";
export declare const ESENDMAIL = "ESENDMAIL";
export declare const ESES = "ESES";
export declare const ECONFIG = "ECONFIG";
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. 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 NodeJS.ErrnoException {
/** SMTP command that was in flight when the server replied with an error */
command?: string | undefined;
/** Raw SMTP server response */
response?: string | undefined;
/** Numeric SMTP response code */
responseCode?: number | undefined;
/** URL of the resource that could not be fetched */
sourceUrl?: string | undefined;
/** Recipient address the error applies to */
recipient?: string | undefined;
/** Recipient addresses rejected by the server */
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
*/
export type Callback<T> = (err: NodemailerError | null, result: T) => void;
/**
* Callback as the library calls it on its error paths: with an error alone, or with null and
* the result. The public signatures use Callback, internally the error paths cast to this
*/
export type ResultCallback<T> = (err: NodemailerError | null, result?: T) => void;
+81
View File
@@ -0,0 +1,81 @@
"use strict";
/**
* Nodemailer Error Codes
*
* Centralized error code definitions for consistent error handling.
*
* Usage:
* import * as errors from './errors.js';
* const err: NodemailerError = new Error('Connection closed');
* err.code = errors.ECONNECTION;
*/
Object.defineProperty(exports, "__esModule", { value: true });
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
*/
exports.ERROR_CODES = {
// Connection errors
ECONNECTION: 'Connection closed unexpectedly',
ETIMEDOUT: 'Connection or operation timed out',
ESOCKET: 'Socket-level error',
EDNS: 'DNS resolution failed',
// TLS/Security errors
ETLS: 'TLS handshake or STARTTLS failed',
EREQUIRETLS: 'REQUIRETLS not supported by server (RFC 8689)',
// Protocol errors
EPROTOCOL: 'Invalid SMTP server response',
EENVELOPE: 'Invalid mail envelope (sender or recipients)',
EMESSAGE: 'Message delivery error',
ESTREAM: 'Stream processing error',
// Authentication errors
EAUTH: 'Authentication failed',
ENOAUTH: 'Authentication credentials not provided',
EOAUTH2: 'OAuth2 token generation or refresh error',
// Resource errors
EMAXLIMIT: 'Pool resource limit reached (max messages per connection)',
EMAXRECIPIENTS: 'Recipient count exceeds maxRecipients',
// Transport-specific errors
ESENDMAIL: 'Sendmail command error',
ESES: 'AWS SES transport error',
// Configuration and access errors
ECONFIG: 'Invalid configuration',
EPROXY: 'Proxy connection error',
EFILEACCESS: 'File access rejected (disableFileAccess is set)',
EURLACCESS: 'URL access rejected (disableUrlAccess is set)',
EFETCH: 'HTTP fetch error'
};
// Error codes as string constants
exports.ECONNECTION = 'ECONNECTION';
exports.ETIMEDOUT = 'ETIMEDOUT';
exports.ESOCKET = 'ESOCKET';
exports.EDNS = 'EDNS';
exports.ETLS = 'ETLS';
exports.EREQUIRETLS = 'EREQUIRETLS';
exports.EPROTOCOL = 'EPROTOCOL';
exports.EENVELOPE = 'EENVELOPE';
exports.EMESSAGE = 'EMESSAGE';
exports.ESTREAM = 'ESTREAM';
exports.EAUTH = 'EAUTH';
exports.ENOAUTH = 'ENOAUTH';
exports.EOAUTH2 = 'EOAUTH2';
exports.EMAXLIMIT = 'EMAXLIMIT';
exports.EMAXRECIPIENTS = 'EMAXRECIPIENTS';
exports.ESENDMAIL = 'ESENDMAIL';
exports.ESES = 'ESES';
exports.ECONFIG = 'ECONFIG';
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';
+107
View File
@@ -0,0 +1,107 @@
/**
* Options for the Cookies jar
*/
export interface CookiesOptions {
/** Lifetime in seconds for cookies that do not set their own expiration (default 1800) */
sessionTimeout?: number | string | undefined;
}
/**
* A cookie as parsed from a 'Set-Cookie:' header and kept in the jar
*/
export interface Cookie {
name?: string | undefined;
value?: string | undefined;
domain?: string | undefined;
path?: string | undefined;
expires?: Date | undefined;
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
*/
declare class Cookies {
options: CookiesOptions;
cookies: Cookie[];
constructor(options?: CookiesOptions);
/**
* Stores a cookie string to the cookie storage
*
* @param cookieStr Value from the 'Set-Cookie:' header
* @param url Current URL
*/
set(cookieStr: string, url?: string): boolean;
/**
* Returns cookie string for the 'Cookie:' header.
*
* @param url URL to check for
* @returns Cookie header or empty string if no matches were found
*/
get(url?: string): string;
/**
* Lists all valied cookie objects for the specified URL
*
* @param url URL to check for
* @returns An array of cookie objects
*/
list(url?: string): Cookie[];
/**
* Parses cookie string from the 'Set-Cookie:' header
*
* @param cookieStr String from the 'Set-Cookie:' header
* @returns Cookie object
*/
parse(cookieStr?: string): Cookie;
/**
* Checks if a cookie object is valid for a specified URL
*
* @param cookie Cookie object
* @param url URL to check for
* @returns true if cookie is valid for specifiec URL
*/
match(cookie: Cookie, url?: string): boolean;
/**
* Adds (or updates/removes if needed) a cookie object to the cookie storage
*
* @param cookie Cookie value to be stored
*/
add(cookie: Cookie): boolean;
/**
* Checks if two cookie objects are the same
*
* @param a Cookie to check against
* @param b Cookie to check against
* @returns True, if the cookies are the same
*/
compare(a: Cookie, b: Cookie): boolean;
/**
* Checks if a cookie is expired
*
* @param cookie Cookie object to check against
* @returns True, if the cookie is expired
*/
isExpired(cookie: Cookie): boolean;
/**
* Returns the default path for an URL path argument, the default-path of
* RFC 6265 section 5.1.4. A cookie that carries no Path attribute is scoped
* to the directory of the URL it was set from
*
* @param pathname
* @returns Default path
*/
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;
+289
View File
@@ -0,0 +1,289 @@
"use strict";
// module to handle cookies
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const node_net_1 = __importDefault(require("node:net"));
const urllib = __importStar(require("../shared/url.js"));
const SESSION_TIMEOUT = 1800; // 30 min
/**
* Creates a biskviit cookie jar for managing cookie values in memory
*
* @constructor
* @param [options] Optional options object
*/
class Cookies {
constructor(options) {
this.options = options || {};
this.cookies = [];
}
/**
* Stores a cookie string to the cookie storage
*
* @param cookieStr Value from the 'Set-Cookie:' header
* @param url Current URL
*/
set(cookieStr, url) {
const urlparts = urllib.parse(url || '');
const cookie = this.parse(cookieStr);
let domain;
if (cookie.domain) {
domain = cookie.domain.replace(/^\./, '');
// do not allow cross origin cookies. There is no public suffix list here, so a
// multi-label suffix like 'co.uk' can not be told apart from a registrable domain
if (
// can't be valid if the requested domain is shorter than current hostname
urlparts.hostname.length < domain.length ||
// a top level domain is not a valid scope, 'Domain=com' would otherwise be
// sent to every .com host. A trailing dot does not make 'com.' any better
domain.indexOf('.') < 0 ||
domain.endsWith('.') ||
// an IP address has no subdomains, so cookies set on it stay host-only
node_net_1.default.isIP(urlparts.hostname) ||
// prefix domains with dot to be sure that partial matches are not used
!('.' + urlparts.hostname).endsWith('.' + domain)) {
cookie.domain = urlparts.hostname;
}
}
else {
cookie.domain = urlparts.hostname;
}
if (!cookie.path) {
cookie.path = this.getPath(urlparts.pathname);
}
// if no expire date, then use sessionTimeout value
if (!cookie.expires) {
cookie.expires = new Date(Date.now() + (Number(this.options.sessionTimeout || SESSION_TIMEOUT) || SESSION_TIMEOUT) * 1000);
}
return this.add(cookie);
}
/**
* Returns cookie string for the 'Cookie:' header.
*
* @param url URL to check for
* @returns Cookie header or empty string if no matches were found
*/
get(url) {
return this.list(url)
.map(cookie => cookie.name + '=' + cookie.value)
.join('; ');
}
/**
* Lists all valied cookie objects for the specified URL
*
* @param url URL to check for
* @returns An array of cookie objects
*/
list(url) {
const result = [];
for (let i = this.cookies.length - 1; i >= 0; i--) {
const cookie = this.cookies[i];
if (this.isExpired(cookie)) {
this.cookies.splice(i, 1);
continue;
}
if (this.match(cookie, url)) {
result.unshift(cookie);
}
}
return result;
}
/**
* Parses cookie string from the 'Set-Cookie:' header
*
* @param cookieStr String from the 'Set-Cookie:' header
* @returns Cookie object
*/
parse(cookieStr) {
const cookie = {};
let hasNameValue = false;
(cookieStr || '')
.toString()
.split(';')
.forEach(cookiePart => {
if (!cookiePart.trim()) {
// skip empty parts
return;
}
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
if (expires.toString() !== 'Invalid Date') {
cookie.expires = expires;
}
break;
}
case 'path':
cookie.path = value;
break;
case 'domain':
domain = value.toLowerCase();
if (domain.length && domain.charAt(0) !== '.') {
domain = '.' + domain; // ensure preceeding dot for user set domains
}
cookie.domain = domain;
break;
case 'max-age':
cookie.expires = new Date(Date.now() + (Number(value) || 0) * 1000);
break;
case 'secure':
cookie.secure = true;
break;
case 'httponly':
cookie.httponly = true;
break;
}
});
return cookie;
}
/**
* Checks if a cookie object is valid for a specified URL
*
* @param cookie Cookie object
* @param url URL to check for
* @returns true if cookie is valid for specifiec URL
*/
match(cookie, url) {
const urlparts = urllib.parse(url || '');
// check if hostname matches
// .foo.com also matches subdomains, foo.com does not
if (urlparts.hostname !== cookie.domain &&
(cookie.domain.charAt(0) !== '.' ||
('.' + urlparts.hostname).substr(-cookie.domain.length) !== cookie.domain)) {
return false;
}
// check if the request path path-matches the cookie path (RFC 6265 section 5.1.4):
// identical paths match, otherwise the cookie path must be a directory prefix
const pathname = urlparts.pathname || '/';
const cookiePath = cookie.path;
const pathMatches = pathname === cookiePath ||
(pathname.startsWith(cookiePath) && (cookiePath.endsWith('/') || pathname.charAt(cookiePath.length) === '/'));
if (!pathMatches) {
return false;
}
// check secure argument
if (cookie.secure && urlparts.protocol !== 'https:') {
return false;
}
return true;
}
/**
* Adds (or updates/removes if needed) a cookie object to the cookie storage
*
* @param cookie Cookie value to be stored
*/
add(cookie) {
// nothing to do here
if (!cookie || !cookie.name) {
return false;
}
// overwrite if has same params
for (let i = 0, len = this.cookies.length; i < len; i++) {
if (this.compare(this.cookies[i], cookie)) {
// check if the cookie needs to be removed instead
if (this.isExpired(cookie)) {
this.cookies.splice(i, 1); // remove expired/unset cookie
return false;
}
this.cookies[i] = cookie;
return true;
}
}
// add as new if not already expired
if (!this.isExpired(cookie)) {
this.cookies.push(cookie);
}
return true;
}
/**
* Checks if two cookie objects are the same
*
* @param a Cookie to check against
* @param b Cookie to check against
* @returns True, if the cookies are the same
*/
compare(a, b) {
return a.name === b.name && a.path === b.path && a.domain === b.domain && a.secure === b.secure && a.httponly === b.httponly;
}
/**
* Checks if a cookie is expired
*
* @param cookie Cookie object to check against
* @returns True, if the cookie is expired
*/
isExpired(cookie) {
return (cookie.expires && cookie.expires < new Date()) || !cookie.value;
}
/**
* Returns the default path for an URL path argument, the default-path of
* RFC 6265 section 5.1.4. A cookie that carries no Path attribute is scoped
* to the directory of the URL it was set from
*
* @param pathname
* @returns Default path
*/
getPath(pathname) {
const pathParts = (pathname || '/').split('/');
pathParts.pop(); // remove filename part
const path = pathParts.join('/').trim();
// a path that holds no more than one '/' is scoped to the root path, and so
// is one that does not start with '/' at all
if (path.charAt(0) !== '/') {
return '/';
}
return path;
}
}
exports.default = Cookies;
module.exports = exports.default;
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
+70
View File
@@ -0,0 +1,70 @@
import http from 'node:http';
import { PassThrough, type Readable } from 'node:stream';
import Cookies from './cookies.js';
/**
* Options for nmfetch
*/
export interface FetchOptions {
/** HTTP method, defaults to GET, or to POST when a body is given */
method?: string | undefined;
/** Request headers, keys are lowercased before use */
headers?: http.OutgoingHttpHeaders | undefined;
/** Overrides the default User-Agent header */
userAgent?: string | undefined;
/** Cookie string(s) to seed the cookie jar with for this URL */
cookie?: string | string[] | false | undefined;
/** Cookie jar shared across redirects, created when missing */
cookies?: Cookies | undefined;
/** Request body: a readable stream, a Buffer, a form object or a string */
body?: Readable | Buffer | {
[key: string]: any;
} | string | false | undefined;
/** Content-Type header for the body, false leaves it out for a stream body */
contentType?: string | false | undefined;
/** TLS settings, only the keys listed in TLS_OPTION_KEYS are used */
tls?: {
[key: string]: any;
} | undefined;
/** 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 */
allowErrorResponse?: boolean | undefined;
/** Redirects followed so far, set by nmfetch itself */
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
* headers of the final response are attached once they arrive
*/
export interface FetchResponse extends PassThrough {
statusCode?: number | undefined;
headers?: http.IncomingHttpHeaders | undefined;
}
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;
+440
View File
@@ -0,0 +1,440 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const node_http_1 = __importDefault(require("node:http"));
const node_https_1 = __importDefault(require("node:https"));
const urllib = __importStar(require("../shared/url.js"));
const node_zlib_1 = __importDefault(require("node:zlib"));
const node_stream_1 = require("node:stream");
const cookies_js_1 = __importDefault(require("./cookies.js"));
const packageData = __importStar(require("../package-info.js"));
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
// the URL checks below.
//
// The source of truth is the tls.connect() option list in the Node docs. A key missing
// here is dropped silently, so extend this list rather than working around it.
const TLS_OPTION_KEYS = [
'ALPNProtocols',
'ca',
'cert',
'checkServerIdentity',
'ciphers',
'crl',
'dhparam',
'ecdhCurve',
'honorCipherOrder',
'key',
'maxVersion',
'minVersion',
'passphrase',
'pfx',
'rejectUnauthorized',
'secureContext',
'secureOptions',
'secureProtocol',
'servername',
'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.
*
* urllib.parse throws for a host that contains forbidden bytes, and it is called for
* every URL that reaches nmfetch, including ones that arrive from a message attachment
* or from a redirect Location header. An uncaught throw here takes the process down,
* so a URL that does not parse is reported the same way as one with a scheme we refuse.
*
* @param url URL to parse
* @returns Parsed URL, or false if it is not a usable http(s) URL
*/
function parseFetchUrl(url) {
let parsed;
try {
parsed = urllib.parse(url);
}
catch (_err) {
return false;
}
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return false;
}
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) {
// Only http(s) URLs can be fetched. Any other scheme (file:, gopher:, a
// protocol-relative redirect target etc.) would otherwise be silently served over
// plain HTTP, possibly against an unintended host. Bail out before the cookie jar
// is touched so a refused URL can not seed it, and release a caller supplied body:
// this is the one exit that runs before the error handler below is attached to it,
// so an error on that stream would have nowhere to go and the fd or socket behind
// it would never be released.
if (options.body && typeof options.body.destroy === 'function') {
options.body.on('error', () => false);
options.body.destroy();
}
setImmediate(() => {
const err = new Error('Unsupported protocol for URL ' + url);
err.code = errors.EFETCH;
err.sourceUrl = url;
fetchRes.emit('error', err);
});
return fetchRes;
}
if (options.cookie) {
[].concat(options.cookie || []).forEach(cookie => {
options.cookies.set(cookie, url);
});
options.cookie = false;
}
let method = (options.method || '').toString().trim().toUpperCase() || 'GET';
let finished = false;
let cookies;
let body;
const handler = parsed.protocol === 'https:' ? node_https_1.default : node_http_1.default;
const headers = {
'accept-encoding': 'gzip,deflate',
'user-agent': 'nodemailer/' + packageData.version
};
Object.keys(options.headers || {}).forEach(key => {
// options.headers is the caller's httpHeaders, straight off an attachment
if ((0, objects_js_1.isProtoKey)(key.toLowerCase().trim())) {
return;
}
headers[key.toLowerCase().trim()] = options.headers[key];
});
if (options.userAgent) {
headers['user-agent'] = options.userAgent;
}
if (parsed.auth) {
headers.Authorization = 'Basic ' + Buffer.from(parsed.auth).toString('base64');
}
if ((cookies = options.cookies.get(url))) {
headers.cookie = cookies;
}
if (options.body) {
if (options.contentType !== false) {
headers['Content-Type'] = options.contentType || 'application/x-www-form-urlencoded';
}
if (typeof options.body.pipe === 'function') {
// it's a stream
headers['Transfer-Encoding'] = 'chunked';
body = options.body;
body.on('error', (err) => {
if (finished) {
return;
}
finished = true;
setFetchCode(err);
err.sourceUrl = url;
fetchRes.emit('error', err);
});
}
else {
if (options.body instanceof Buffer) {
body = options.body;
}
else if (typeof options.body === 'object') {
try {
// encodeURIComponent can fail on invalid input (partial emoji etc.)
body = Buffer.from(Object.keys(options.body)
.map(key => {
const value = options.body[key].toString().trim();
return encodeURIComponent(key) + '=' + encodeURIComponent(value);
})
.join('&'));
}
catch (E) {
// 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;
setImmediate(() => fetchRes.emit('error', E));
return fetchRes;
}
}
else {
body = Buffer.from(options.body.toString().trim());
}
headers['Content-Type'] = options.contentType || 'application/x-www-form-urlencoded';
headers['Content-Length'] = body.length;
}
// if method is not provided, use POST instead of GET
method = (options.method || '').toString().trim().toUpperCase() || 'POST';
}
let req;
const reqOptions = {
method,
host: parsed.hostname,
path: parsed.path,
port: fetchPort(parsed),
headers,
// Validate TLS certificates by default. Callers that genuinely need to
// reach a self-signed/internal host opt out explicitly with
// options.tls = { rejectUnauthorized: false }.
rejectUnauthorized: true,
agent: false
};
if (options.tls) {
// see TLS_OPTION_KEYS
Object.keys(options.tls).forEach(key => {
if (TLS_OPTION_KEYS.includes(key)) {
reqOptions[key] = options.tls[key];
}
});
}
if (parsed.protocol === 'https:' &&
parsed.hostname &&
parsed.hostname !== reqOptions.host &&
!node_net_1.default.isIP(parsed.hostname) &&
!reqOptions.servername) {
reqOptions.servername = parsed.hostname;
}
try {
req = handler.request(reqOptions);
}
catch (E) {
finished = true;
setImmediate(() => {
setFetchCode(E);
E.sourceUrl = url;
fetchRes.emit('error', E);
});
return fetchRes;
}
// 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;
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;
if (finished) {
return;
}
switch (res.headers['content-encoding']) {
case 'gzip':
case 'deflate':
inflate = node_zlib_1.default.createUnzip();
break;
}
if (res.headers['set-cookie']) {
[].concat(res.headers['set-cookie'] || []).forEach(cookie => {
options.cookies.set(cookie, url);
});
}
if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) {
// redirect
options.redirects++;
if (options.redirects > options.maxRedirects) {
return fail(new Error('Maximum redirect count exceeded'));
}
// redirect does not include POST body
options.method = 'GET';
options.body = false;
let redirectUrl;
try {
redirectUrl = urllib.resolve(url, res.headers.location);
}
catch (_err) {
// the legacy resolver throws on a Location the WHATWG parser also refused,
// so fall through to the check below with what the server actually sent
redirectUrl = res.headers.location;
}
const redirectParsed = parseFetchUrl(redirectUrl);
if (!redirectParsed) {
// Refuse the redirect target here rather than leaving it to the recursive
// 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().
return fail(new Error('Unsupported protocol for URL ' + redirectUrl), redirectUrl);
}
// 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 => {
// 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) {
return fail(new Error('Invalid status code ' + res.statusCode));
}
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;
}
source.unpipe(fetchRes);
fail(new Error('Response size exceeds the allowed ' + maxBytes + ' bytes'));
});
if (inflate) {
res.pipe(inflate).pipe(fetchRes);
inflate.on('error', (err) => fail(err));
}
else {
res.pipe(fetchRes);
}
});
setImmediate(() => {
if (body) {
try {
if (typeof body.pipe === 'function') {
return body.pipe(req);
}
req.write(body);
}
catch (err) {
return fail(err);
}
}
req.end();
});
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 });
+64
View File
@@ -0,0 +1,64 @@
import type { Logger } from '../shared/index.js';
import type { MimeNodeEnvelope } from '../mime-node/index.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
*/
export interface JSONTransportOptions extends TransportOptions {
/** Selects this transport in createTransport */
jsonTransport?: boolean | undefined;
/** If true, the message is returned as an object instead of a JSON string */
skipEncoding?: boolean | undefined;
}
/**
* 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<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: 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
*
* @constructor
* @param optional config parameter
*/
declare class JSONTransport {
mailer: Mail<JSONSentMessageInfo>;
options: JSONTransportOptions;
name: string;
version: string;
logger: Logger;
constructor(options?: JSONTransportOptions);
/**
* <p>Compiles a mailcomposer message and forwards it to handler that sends it.</p>
*
* @param mail MailComposer object
* @param done Callback function to run when the sending is completed
*/
send(mail: MailMessage<JSONSentMessageInfo>, done: (err: Error | null, info?: JSONSentMessageInfo) => void): void;
}
/**
* Type aliases in the layout of @types/nodemailer, so `JSONTransport.Options` style references keep working
*/
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;
+99
View File
@@ -0,0 +1,99 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const packageData = __importStar(require("../package-info.js"));
const shared = __importStar(require("../shared/index.js"));
/**
* Generates a Transport object to generate JSON output
*
* @constructor
* @param optional config parameter
*/
class JSONTransport {
constructor(options) {
options = options || {};
this.options = options;
this.name = 'JSONTransport';
this.version = packageData.version;
this.logger = shared.getLogger(this.options, {
component: this.options.component || 'json-transport'
});
}
/**
* <p>Compiles a mailcomposer message and forwards it to handler that sends it.</p>
*
* @param mail MailComposer object
* @param done Callback function to run when the sending is completed
*/
send(mail, done) {
// Sendmail strips this header line by itself. send() runs after the message was
// compiled, so mail.message is set
mail.message.keepBcc = true;
const envelope = mail.message.getEnvelope();
const messageId = mail.message.messageId();
const recipients = [].concat(envelope.to || []);
if (recipients.length > 3) {
recipients.push('...and ' + recipients.splice(2).length + ' more');
}
this.logger.info({
tnx: 'send',
messageId
}, 'Composing JSON structure of %s to <%s>', messageId, recipients.join(', '));
setImmediate(() => {
mail.normalize((err, data) => {
if (err) {
this.logger.error({
err,
tnx: 'send',
messageId
}, 'Failed building JSON structure for %s. %s', messageId, err.message);
return done(err);
}
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,
message: this.options.skipEncoding ? data : JSON.stringify(data)
});
});
});
}
}
exports.default = JSONTransport;
module.exports = exports.default;
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
+174
View File
@@ -0,0 +1,174 @@
import MimeNode, { type MimeNodeAddressInput, type MimeNodeContent, type MimeNodeEnvelopeInput, type MimeNodeHeaders, type MimeNodeOptions } from '../mime-node/index.js';
import type { Readable } from 'node:stream';
import type { OutgoingHttpHeaders } from 'node:http';
/**
* A content element, the shape of the text, html, watchHtml, amp and alternatives values.
* The content is either given in `content`, read from `path` or `href`, or `raw` supplies
* a pregenerated MIME part
*/
export interface MailComposerAlternative {
/** The content itself: a string, a Buffer, a readable stream or a content descriptor */
content?: MimeNodeContent | undefined;
/** File path, data URI or http(s) URL to read the content from */
path?: string | false | undefined;
/** URL or data URI to fetch the content from */
href?: string | false | undefined;
/** Request headers for a URL fetch */
httpHeaders?: OutgoingHttpHeaders | undefined;
/** TLS settings for a URL fetch, see nmfetch */
tls?: {
[key: string]: any;
} | undefined;
/** Encoding of a string `content`, decoded into a Buffer unless it is utf8 or ascii */
encoding?: string | undefined;
/** Pregenerated MIME part, used as is instead of building the node */
raw?: MimeNodeContent | undefined;
/** Content type, detected from the filename, path or URL when not set */
contentType?: string | undefined;
/** Content-Transfer-Encoding for the node, false leaves the choice to the node */
contentTransferEncoding?: string | false | undefined;
/** Filename for the node, false suppresses the generated one */
filename?: string | false | undefined;
/** Additional headers for the node */
headers?: MimeNodeHeaders | undefined;
}
/**
* An attachment, the shape of the attachments entries
*/
export interface MailComposerAttachment extends MailComposerAlternative {
/** Content-Disposition for the node, 'attachment' by default and 'inline' for a message node or an image with a cid */
contentDisposition?: string | undefined;
/** Content-Id for an embedded image, moves the attachment into the multipart/related node beside the html */
cid?: string | undefined;
}
/**
* The icalEvent value as an object
*/
export interface MailComposerIcalEvent extends MailComposerAlternative {
/** iCalendar method, PUBLISH by default */
method?: string | undefined;
}
/**
* The attachments as sorted by getAttachments
*/
export interface MailComposerAttachments {
/** Attachments for the multipart/mixed node, the icalEvent attachment included */
attached: MailComposerAttachment[];
/** Attachments with a cid, embedded in the multipart/related node beside the html */
related: MailComposerAttachment[];
}
/**
* A List-* header entry, a bare url or an url with a comment
*/
export type MailComposerListHeaderEntry = string | {
url: string;
comment?: string | undefined;
};
/**
* The list value: List-* headers keyed by the part after "List-" (help, unsubscribe,
* subscribe, post, owner, archive, id). An array emits one header per entry, an entry that
* is itself an array joins its values into one header
*/
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
* node. Some fields are read by the mailer that hands the data to MailComposer rather than
* by MailComposer itself, these are marked as such
*/
export interface MailComposerOptions {
from?: MimeNodeAddressInput | undefined;
sender?: MimeNodeAddressInput | undefined;
to?: MimeNodeAddressInput | undefined;
cc?: MimeNodeAddressInput | undefined;
bcc?: MimeNodeAddressInput | undefined;
replyTo?: MimeNodeAddressInput | undefined;
inReplyTo?: string | undefined;
references?: string | string[] | undefined;
subject?: string | undefined;
/** Message-ID header value, generated when missing */
messageId?: string | undefined;
/** Date header value, the current time when missing */
date?: Date | string | undefined;
/** Plaintext version of the message */
text?: string | Buffer | Readable | MailComposerAlternative | undefined;
/** HTML version of the message */
html?: string | Buffer | Readable | MailComposerAlternative | undefined;
/** Apple Watch specific HTML version of the message */
watchHtml?: string | Buffer | Readable | MailComposerAlternative | undefined;
/** AMP4EMAIL version of the message */
amp?: string | Buffer | Readable | MailComposerAlternative | undefined;
/** iCalendar event, included both as a text/calendar alternative and as an application/ics attachment */
icalEvent?: string | Buffer | Readable | MailComposerIcalEvent | undefined;
attachments?: MailComposerAttachment[] | undefined;
/** Further alternatives for the multipart/alternative node, after text, watchHtml, amp, html and the calendar event */
alternatives?: MailComposerAlternative[] | undefined;
/** Custom headers for the root node, the standard headers above override them */
headers?: MimeNodeHeaders | undefined;
/** List-* headers, read by the mailer */
list?: MailComposerListHeaders | undefined;
/** SMTP envelope to use instead of the one generated from the headers */
envelope?: MimeNodeEnvelopeInput | undefined;
/** 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?: MailComposerTextEncoding | undefined;
/** Pregenerated rfc822 message, used as is instead of building one */
raw?: MimeNodeContent | undefined;
/** Reject content that points to a URL */
disableUrlAccess?: boolean | undefined;
/** Reject content that points to a file path */
disableFileAccess?: boolean | undefined;
/** Convert data: images in the html into embedded attachments, read by the mailer */
attachDataUrls?: boolean | undefined;
/** Prefix for the generated multipart boundaries */
boundaryPrefix?: string | undefined;
/** Shared part of the unique multipart boundary */
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;
/** Method to normalize header keys for custom caseing */
normalizeHeaderKey?: MimeNodeOptions['normalizeHeaderKey'] | undefined;
/** 'high', 'normal' or 'low', sets the priority headers, read by the mailer */
priority?: string | undefined;
/** X-Mailer header value, false leaves the header out, read by the mailer */
xMailer?: string | false | undefined;
}
declare class MailComposer {
mail: MailComposerOptions;
message: MimeNode | false;
constructor(mail?: MailComposerOptions);
/**
* Builds MimeNode instance
*/
compile(): MimeNode;
/**
* List all attachments. Resulting attachment objects can be used as input for MimeNode nodes
*
* @param findRelated If true separate related attachments from attached ones
* @returns An object of arrays (`related` and `attached`)
*/
getAttachments(findRelated?: boolean): MailComposerAttachments;
/**
* List alternatives. Resulting objects can be used as input for MimeNode nodes
*
* @returns An array of alternative elements. Includes the `text` and `html` values as well
*/
getAlternatives(): MailComposerAlternative[];
}
/**
* Type alias in the layout of the other modules (@types/nodemailer had no namespace for 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;
+613
View File
@@ -0,0 +1,613 @@
"use strict";
/* eslint no-undefined: 0 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const index_js_1 = __importDefault(require("../mime-node/index.js"));
const mimeFuncs = __importStar(require("../mime-funcs/index.js"));
const index_js_2 = require("../shared/index.js");
/**
* Creates the object for composing a MimeNode instance out from the mail options
*
* @constructor
* @param mail Mail options
*/
/**
* Tells whether a content value is a content descriptor object (something to load or to
* use as is) rather than the content itself
*/
function isContentObject(value) {
const content = value;
return typeof value === 'object' && !!(content.content || content.path || content.href || content.raw);
}
class MailComposer {
constructor(mail) {
this.mail = mail || {};
this.message = false;
}
/**
* Builds MimeNode instance
*/
compile() {
this._alternatives = this.getAlternatives();
this._htmlNode = this._alternatives.filter(alternative => /^text\/html\b/i.test(alternative.contentType)).pop();
this._attachments = this.getAttachments(!!this._htmlNode);
this._useRelated = !!(this._htmlNode && this._attachments.related.length);
this._useAlternative = this._alternatives.length > 1;
this._useMixed = this._attachments.attached.length > 1 || (this._alternatives.length && this._attachments.attached.length === 1);
// Compose MIME tree
if (this.mail.raw) {
this.message = new index_js_1.default('message/rfc822', {
newline: this.mail.newline,
disableUrlAccess: this.mail.disableUrlAccess,
disableFileAccess: this.mail.disableFileAccess
}).setRaw(this.mail.raw);
}
else if (this._useMixed) {
this.message = this._createMixed();
}
else if (this._useAlternative) {
this.message = this._createAlternative();
}
else if (this._useRelated) {
this.message = this._createRelated();
}
else {
this.message = this._createContentNode(false, []
.concat(this._alternatives || [])
.concat(this._attachments.attached || [])
.shift() || {
contentType: 'text/plain',
content: ''
});
}
// Add custom headers
if (this.mail.headers) {
this.message.addHeader(this.mail.headers);
}
// Add headers to the root node, always overrides custom headers
['from', 'sender', 'to', 'cc', 'bcc', 'reply-to', 'in-reply-to', 'references', 'subject', 'message-id', 'date'].forEach(header => {
const key = header.replace(/-(\w)/g, (o, c) => c.toUpperCase());
if (this.mail[key]) {
this.message.setHeader(header, this.mail[key]);
}
});
// Sets custom envelope
if (this.mail.envelope) {
this.message.setEnvelope(this.mail.envelope);
}
// ensure Message-Id value
this.message.messageId();
return this.message;
}
/**
* List all attachments. Resulting attachment objects can be used as input for MimeNode nodes
*
* @param findRelated If true separate related attachments from attached ones
* @returns An object of arrays (`related` and `attached`)
*/
getAttachments(findRelated) {
let eventObject;
const attachments = [].concat(this.mail.attachments || []).map((attachment, i) => {
if (/^data:/i.test((attachment.path || attachment.href))) {
attachment = this._processDataUrl(attachment);
}
const contentType = attachment.contentType || mimeFuncs.detectMimeType(attachment.filename || attachment.path || attachment.href || 'bin');
const isImage = /^image\//i.test(contentType);
const isMessageNode = /^message\//i.test(contentType);
const contentDisposition = attachment.contentDisposition || (isMessageNode || (isImage && attachment.cid) ? 'inline' : 'attachment');
let contentTransferEncoding;
if ('contentTransferEncoding' in attachment) {
// also contains `false`, to set
contentTransferEncoding = attachment.contentTransferEncoding;
}
else if (isMessageNode) {
// the content might include non-ASCII bytes but at this point we do not know it yet
contentTransferEncoding = '8bit';
}
else {
contentTransferEncoding = 'base64'; // the default
}
const data = {
contentType,
contentDisposition,
contentTransferEncoding
};
if (attachment.filename) {
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);
if (data.filename.indexOf('.') < 0) {
data.filename += '.' + mimeFuncs.detectExtension(data.contentType);
}
}
if (/^https?:\/\//i.test(attachment.path)) {
attachment.href = attachment.path;
attachment.path = undefined;
}
if (attachment.cid) {
data.cid = attachment.cid;
}
if (attachment.raw) {
data.raw = attachment.raw;
}
else if (attachment.path) {
data.content = {
path: attachment.path
};
}
else if (attachment.href) {
data.content = {
href: attachment.href,
httpHeaders: attachment.httpHeaders,
tls: attachment.tls
};
}
else {
data.content = attachment.content || '';
}
if (attachment.encoding) {
data.encoding = attachment.encoding;
}
if (attachment.headers) {
data.headers = attachment.headers;
}
return data;
});
if (this.mail.icalEvent) {
eventObject = Object.assign({}, this._getIcalEvent());
eventObject.contentType = 'application/ics';
if (!eventObject.headers) {
eventObject.headers = {};
}
eventObject.filename = eventObject.filename || 'invite.ics';
eventObject.headers['Content-Disposition'] = 'attachment';
eventObject.headers['Content-Transfer-Encoding'] = 'base64';
}
if (!findRelated) {
return {
attached: attachments.concat(eventObject || []),
related: []
};
}
return {
attached: attachments.filter(attachment => !attachment.cid).concat(eventObject || []),
related: attachments.filter(attachment => !!attachment.cid)
};
}
/**
* Returns the icalEvent value with `path`/`href`/data uri input normalized into
* a `content` entry, the same way as for regular attachments. The same event is
* included twice (as a text/calendar alternative and as an application/ics
* attachment), so the shared content object is marked to be resolved just once
* and the buffered result is reused by the second node.
*
* @returns Normalized icalEvent data
* @internal
*/
_getIcalEvent() {
if (!this._icalEvent) {
let icalEvent;
if (isContentObject(this.mail.icalEvent)) {
// an own "__proto__" key would make the copy inherit path/href from caller
// data, and the mapping below then replaces the content the caller did set
icalEvent = (0, index_js_2.copyOwnKeys)({}, this.mail.icalEvent);
}
else {
icalEvent = {
content: this.mail.icalEvent
};
}
if (/^data:/i.test((icalEvent.path || icalEvent.href))) {
icalEvent = this._processDataUrl(icalEvent);
}
if (/^https?:\/\//i.test(icalEvent.path)) {
icalEvent.href = icalEvent.path;
icalEvent.path = undefined;
}
if (!icalEvent.raw) {
// map file path and URL values into `content`, otherwise the content
// nodes would render an empty body
if (icalEvent.path) {
icalEvent.content = {
path: icalEvent.path
};
icalEvent.path = undefined;
}
else if (icalEvent.href) {
icalEvent.content = {
href: icalEvent.href,
httpHeaders: icalEvent.httpHeaders,
tls: icalEvent.tls
};
icalEvent.href = undefined;
}
}
if (icalEvent.content && typeof icalEvent.content === 'object') {
// we are going to have the same attachment twice, so mark this to be
// resolved just once
icalEvent.content._resolve = true;
}
this._icalEvent = icalEvent;
}
return this._icalEvent;
}
/**
* List alternatives. Resulting objects can be used as input for MimeNode nodes
*
* @returns An array of alternative elements. Includes the `text` and `html` values as well
*/
getAlternatives() {
const alternatives = [];
let text, html, watchHtml, amp, eventObject;
if (this.mail.text) {
if (isContentObject(this.mail.text)) {
text = this.mail.text;
}
else {
text = {
content: this.mail.text
};
}
text.contentType = 'text/plain; charset=utf-8';
}
if (this.mail.watchHtml) {
if (isContentObject(this.mail.watchHtml)) {
watchHtml = this.mail.watchHtml;
}
else {
watchHtml = {
content: this.mail.watchHtml
};
}
watchHtml.contentType = 'text/watch-html; charset=utf-8';
}
if (this.mail.amp) {
if (isContentObject(this.mail.amp)) {
amp = this.mail.amp;
}
else {
amp = {
content: this.mail.amp
};
}
amp.contentType = 'text/x-amp-html; charset=utf-8';
}
// NB! when including attachments with a calendar alternative you might end up in a blank screen on some clients
if (this.mail.icalEvent) {
eventObject = Object.assign({}, this._getIcalEvent());
eventObject.filename = false;
eventObject.contentType =
'text/calendar; charset=utf-8; method=' + (eventObject.method || 'PUBLISH').toString().trim().toUpperCase();
if (!eventObject.headers) {
eventObject.headers = {};
}
}
if (this.mail.html) {
if (isContentObject(this.mail.html)) {
html = this.mail.html;
}
else {
html = {
content: this.mail.html
};
}
html.contentType = 'text/html; charset=utf-8';
}
[]
.concat(text || [])
.concat(watchHtml || [])
.concat(amp || [])
.concat(html || [])
.concat(eventObject || [])
.concat(this.mail.alternatives || [])
.forEach(alternative => {
if (/^data:/i.test((alternative.path || alternative.href))) {
alternative = this._processDataUrl(alternative);
}
const data = {
contentType: alternative.contentType ||
mimeFuncs.detectMimeType(alternative.filename || alternative.path || alternative.href || 'txt'),
contentTransferEncoding: alternative.contentTransferEncoding
};
if (alternative.filename) {
data.filename = alternative.filename;
}
if (/^https?:\/\//i.test(alternative.path)) {
alternative.href = alternative.path;
alternative.path = undefined;
}
if (alternative.raw) {
data.raw = alternative.raw;
}
else if (alternative.path) {
data.content = {
path: alternative.path
};
}
else if (alternative.href) {
data.content = {
href: alternative.href,
httpHeaders: alternative.httpHeaders,
tls: alternative.tls
};
}
else {
data.content = alternative.content || '';
}
if (alternative.encoding) {
data.encoding = alternative.encoding;
}
if (alternative.headers) {
data.headers = alternative.headers;
}
alternatives.push(data);
});
return alternatives;
}
/**
* Builds multipart/mixed node. It should always contain different type of elements on the same level
* eg. text + attachments
*
* @param parentNode Parent for this note. If it does not exist, a root node is created
* @returns MimeNode node element
* @internal
*/
_createMixed(parentNode) {
const node = parentNode
? parentNode.createChild('multipart/mixed', {
disableUrlAccess: this.mail.disableUrlAccess,
disableFileAccess: this.mail.disableFileAccess,
normalizeHeaderKey: this.mail.normalizeHeaderKey,
newline: this.mail.newline
})
: new index_js_1.default('multipart/mixed', {
baseBoundary: this.mail.baseBoundary,
textEncoding: this.mail.textEncoding,
boundaryPrefix: this.mail.boundaryPrefix,
disableUrlAccess: this.mail.disableUrlAccess,
disableFileAccess: this.mail.disableFileAccess,
normalizeHeaderKey: this.mail.normalizeHeaderKey,
newline: this.mail.newline
});
if (this._useAlternative) {
this._createAlternative(node);
}
else if (this._useRelated) {
this._createRelated(node);
}
[]
.concat((!this._useAlternative && this._alternatives) || [])
.concat(this._attachments.attached || [])
.forEach(element => {
// if the element is a html node from related subpart then ignore it
if (!this._useRelated || element !== this._htmlNode) {
this._createContentNode(node, element);
}
});
return node;
}
/**
* Builds multipart/alternative node. It should always contain same type of elements on the same level
* eg. text + html view of the same data
*
* @param parentNode Parent for this note. If it does not exist, a root node is created
* @returns MimeNode node element
* @internal
*/
_createAlternative(parentNode) {
const node = parentNode
? parentNode.createChild('multipart/alternative', {
disableUrlAccess: this.mail.disableUrlAccess,
disableFileAccess: this.mail.disableFileAccess,
normalizeHeaderKey: this.mail.normalizeHeaderKey,
newline: this.mail.newline
})
: new index_js_1.default('multipart/alternative', {
baseBoundary: this.mail.baseBoundary,
textEncoding: this.mail.textEncoding,
boundaryPrefix: this.mail.boundaryPrefix,
disableUrlAccess: this.mail.disableUrlAccess,
disableFileAccess: this.mail.disableFileAccess,
normalizeHeaderKey: this.mail.normalizeHeaderKey,
newline: this.mail.newline
});
this._alternatives.forEach(alternative => {
if (this._useRelated && this._htmlNode === alternative) {
this._createRelated(node);
}
else {
this._createContentNode(node, alternative);
}
});
return node;
}
/**
* Builds multipart/related node. It should always contain html node with related attachments
*
* @param parentNode Parent for this note. If it does not exist, a root node is created
* @returns MimeNode node element
* @internal
*/
_createRelated(parentNode) {
const node = parentNode
? parentNode.createChild('multipart/related; type="text/html"', {
disableUrlAccess: this.mail.disableUrlAccess,
disableFileAccess: this.mail.disableFileAccess,
normalizeHeaderKey: this.mail.normalizeHeaderKey,
newline: this.mail.newline
})
: new index_js_1.default('multipart/related; type="text/html"', {
baseBoundary: this.mail.baseBoundary,
textEncoding: this.mail.textEncoding,
boundaryPrefix: this.mail.boundaryPrefix,
disableUrlAccess: this.mail.disableUrlAccess,
disableFileAccess: this.mail.disableFileAccess,
normalizeHeaderKey: this.mail.normalizeHeaderKey,
newline: this.mail.newline
});
this._createContentNode(node, this._htmlNode);
this._attachments.related.forEach(alternative => this._createContentNode(node, alternative));
return node;
}
/**
* Creates a regular node with contents
*
* @param parentNode Parent for this note. If it does not exist, a root node is created
* @param element Node data
* @returns MimeNode node element
* @internal
*/
_createContentNode(parentNode, element) {
element = element || {};
element.content = element.content || '';
const encoding = (element.encoding || 'utf8')
.toString()
.toLowerCase()
.replace(/[-_\s]/g, '');
const node = parentNode
? parentNode.createChild(element.contentType, {
filename: element.filename,
textEncoding: this.mail.textEncoding,
disableUrlAccess: this.mail.disableUrlAccess,
disableFileAccess: this.mail.disableFileAccess,
normalizeHeaderKey: this.mail.normalizeHeaderKey,
newline: this.mail.newline
})
: new index_js_1.default(element.contentType, {
filename: element.filename,
baseBoundary: this.mail.baseBoundary,
textEncoding: this.mail.textEncoding,
boundaryPrefix: this.mail.boundaryPrefix,
disableUrlAccess: this.mail.disableUrlAccess,
disableFileAccess: this.mail.disableFileAccess,
normalizeHeaderKey: this.mail.normalizeHeaderKey,
newline: this.mail.newline
});
// add custom headers
if (element.headers) {
node.addHeader(element.headers);
}
if (element.cid) {
node.setHeader('Content-Id', '<' + element.cid.replace(/[<>]/g, '') + '>');
}
if (element.contentTransferEncoding) {
node.setHeader('Content-Transfer-Encoding', element.contentTransferEncoding);
}
else if (this.mail.encoding && /^text\//i.test(element.contentType)) {
node.setHeader('Content-Transfer-Encoding', this.mail.encoding);
}
if (!/^text\//i.test(element.contentType) || element.contentDisposition) {
node.setHeader('Content-Disposition', element.contentDisposition || (element.cid && /^image\//i.test(element.contentType) ? 'inline' : 'attachment'));
}
if (typeof element.content === 'string' && !['utf8', 'usascii', 'ascii'].includes(encoding)) {
element.content = Buffer.from(element.content, encoding);
}
// prefer pregenerated raw content
if (element.raw) {
node.setRaw(element.raw);
}
else {
node.setContent(element.content);
}
return node;
}
/**
* Parses data uri and converts it to a Buffer
*
* @param element Content element
* @return Parsed element
* @internal
*/
_processDataUrl(element) {
const dataUrl = element.path || element.href;
// Early validation to prevent ReDoS
if (!dataUrl || typeof dataUrl !== 'string') {
return element;
}
if (!dataUrl.startsWith('data:')) {
return element;
}
if (dataUrl.length > 52428800) {
// 52428800 chars = 50MB limit for data URL string (~37.5MB decoded image)
// Extract content type before rejecting to preserve MIME type
let detectedType = 'application/octet-stream';
const commaPos = dataUrl.indexOf(',');
if (commaPos > 0 && commaPos < 200) {
// Parse header safely with size limit
const header = dataUrl.substring(5, commaPos); // skip 'data:'
const parts = header.split(';');
if (parts[0] && parts[0].includes('/')) {
detectedType = parts[0].trim();
}
}
// Return empty content for excessively long data URLs
return Object.assign((0, index_js_2.copyOwnKeys)({}, element), {
path: false,
href: false,
content: Buffer.alloc(0),
contentType: element.contentType || detectedType
});
}
let parsedDataUri;
try {
parsedDataUri = (0, index_js_2.parseDataURI)(dataUrl);
}
catch (_err) {
return element;
}
if (!parsedDataUri) {
return element;
}
element.content = parsedDataUri.data;
element.contentType = element.contentType || parsedDataUri.contentType;
if ('path' in element) {
element.path = false;
}
if ('href' in element) {
element.href = false;
}
return element;
}
}
exports.default = MailComposer;
module.exports = exports.default;
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
+237
View File
@@ -0,0 +1,237 @@
import { EventEmitter } from 'node:events';
import * as shared from '../shared/index.js';
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, 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';
export type { SendMailOptions, MailDefaults, MailMessageData, MailMessageDataCallback, MailMessageContentCallback, MailMessageListHeader, MailMessageListHeaderValue } from './mail-message.js';
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, 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 */
envelope: MimeNodeEnvelope;
/** Message-ID of the sent message */
messageId: string;
/** Recipient addresses the transport accepted */
accepted?: string[] | undefined;
/** Recipient addresses the transport rejected */
rejected?: string[] | undefined;
/** Recipient addresses left pending, LMTP reports these */
pending?: string[] | undefined;
/** Last response from the server */
response?: string | undefined;
/** The generated message, for the transports that hand it back instead of sending it */
message?: any;
/** Transport specific fields */
[key: string]: any;
}
/**
* Callback for sendMail, receives the transport result once the transport has taken the
* message
*/
export type SendMailCallback<T = SentMessageInfo> = (err: NodemailerError | null, info: T) => void;
/**
* 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 = Callback<true>;
/**
* Callback a plugin calls once it is done, an error aborts the send
*/
export type PluginCallback = (err?: NodemailerError | null) => void;
/**
* A plugin registered with use(): receives the message and a callback to call once done
*/
export type PluginFunction<T = SentMessageInfo> = (mail: MailMessage<T>, callback: PluginCallback) => void;
/**
* Connection options a getSocket handler receives: the options of the transport asking for
* the socket, with the host and port to connect to
*/
export interface GetSocketOptions {
host?: string | undefined;
port?: number | undefined;
[key: string]: any;
}
/**
* 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, false when a new socket should be
* opened, or the error that prevented the connection
*/
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
*/
export type GetSocketHandler = (options: GetSocketOptions, callback: GetSocketCallback) => void;
/**
* A custom proxy handler, registered with set('proxy_handler_' + protocol, handler) for the
* protocol of the proxy url
*/
export type ProxyHandler = (proxy: ParsedUrl, options: GetSocketOptions, callback: GetSocketCallback) => void;
/**
* Well known keys of the meta store, see set() and get(): the OAuth2 token provisioning
* callback the SMTP transports use, the socks module for socks proxies, and a custom proxy
* handler per proxy protocol
*/
export interface MailMeta {
/** Called by the SMTP transports when a new OAuth2 access token is needed */
oauth2_provision_cb: XOAuth2ProvisionCallback;
/** The socks module, v1 or v2, used to connect through a socks proxy */
proxy_socks_module: any;
[key: `proxy_handler_${string}`]: ProxyHandler;
[key: string]: any;
}
/**
* 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.
*
* 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, D extends TransportOptions = TransportOptions> {
/** Transport name, used for logging */
name: string;
/** Transport version, used for logging */
version: string;
/** Hands a message to the transport, the callback receives the transport result */
send(mail: MailMessage<T>, callback: ResultCallback<T>): void;
/** Checks the configuration, the SMTP transports connect and authenticate for it */
verify?(...args: any[]): any;
/** Closes the transport */
close?(...args: any[]): any;
/** Tells whether the transport can take a message right away */
isIdle?(...args: any[]): any;
/** 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, D> | undefined;
/** Socket handler for a proxied connection, set by Mail when a proxy is configured */
getSocket?: GetSocketHandler | undefined;
}
/**
* Transport configuration as read by Mail itself. The transport reads its own options from
* the same object, see the transport for those
*/
export interface TransportOptions {
/** Bunyan compatible logger, true for the default console logger, false or unset for no logging */
logger?: shared.ExternalLogger | boolean | undefined;
/** Component name for the log lines, defaults to 'mail' */
component?: string | undefined;
/** DKIM signing options, every message is signed with these unless it carries its own */
dkim?: DKIMOptions | undefined;
/** Proxy url. http(s) proxies work as is, socks proxies need the socks module set with set('proxy_socks_module', socks) */
proxy?: string | undefined;
/** TLS options, rejectUnauthorized applies to an https proxy as well */
tls?: ConnectionOptions | undefined;
/** Reject content that points to a file path, forced onto every message */
disableFileAccess?: boolean | undefined;
/** Reject content that points to a URL, forced onto every message */
disableUrlAccess?: boolean | undefined;
/** Method to normalize header keys for custom caseing, forced onto every message */
normalizeHeaderKey?: MimeNodeOptions['normalizeHeaderKey'] | undefined;
/** Recipients allowed on one message, forced onto every message, 0 disables the limit, defaults to 100000 */
maxRecipients?: number | undefined;
/** Convert data: images in the html into embedded attachments */
attachDataUrls?: boolean | undefined;
}
/**
* 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, 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, 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, D>;
logger: shared.Logger;
/** Closes the transport, the pooled SMTP transport closes its connections */
close: () => void;
/** Tells whether the transport can take a message right away */
isIdle: () => boolean;
/** Checks the configuration, the SMTP transports connect and authenticate for it */
verify: {
(callback: VerifyCallback): void;
(): Promise<true>;
};
/** 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, 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, callback: SendMailCallback<T>): void;
getVersionString(): string;
/**
* Sets up proxy handler for a Nodemailer object
*
* @param proxyUrl Proxy configuration url
*/
setupProxy(proxyUrl: string): void;
set<K extends keyof MailMeta & string>(key: K, value: MailMeta[K]): Map<string, any>;
get<K extends keyof MailMeta & string>(key: K): MailMeta[K] | undefined;
}
/**
* Type aliases in the layout of @types/nodemailer, so `Mail.Options` style references keep working
*/
type MailPluginFunction<T> = PluginFunction<T>;
declare namespace Mail {
type Options = SendMailOptions;
type Address = MimeNodeAddress;
type Attachment = MailComposerAttachment;
type AttachmentLike = MailComposerAlternative;
type AmpAttachment = MailComposerAlternative;
type IcalAttachment = MailComposerIcalEvent;
type Headers = MimeNodeHeaders;
type ListHeader = MailComposerListHeaderEntry;
type ListHeaders = MailComposerListHeaders;
type Envelope = MimeNodeEnvelopeInput;
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;
+434
View File
@@ -0,0 +1,434 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const node_events_1 = require("node:events");
const shared = __importStar(require("../shared/index.js"));
const mimeTypes = __importStar(require("../mime-funcs/mime-types.js"));
const index_js_1 = __importDefault(require("../mail-composer/index.js"));
const index_js_2 = __importDefault(require("../dkim/index.js"));
const http_proxy_client_js_1 = __importDefault(require("../smtp-connection/http-proxy-client.js"));
const errors = __importStar(require("../errors.js"));
const node_util_1 = __importDefault(require("node:util"));
const urllib = __importStar(require("../shared/url.js"));
const packageData = __importStar(require("../package-info.js"));
const mail_message_js_1 = __importDefault(require("./mail-message.js"));
const node_net_1 = __importDefault(require("node:net"));
const node_dns_1 = __importDefault(require("node:dns"));
const node_crypto_1 = __importDefault(require("node:crypto"));
/**
* Recipients allowed on one message unless the caller sets its own maxRecipients. A backstop
* against a runaway or hostile recipient list rather than a delivery policy: RFC 5321 only
* asks a server to accept 100, so a real send is bounded far below this.
*/
const DEFAULT_MAX_RECIPIENTS = 100000;
/**
* Creates an object for exposing the Mail API
*
* @constructor
* @param transporter Transport object instance to pass the mails to
*/
class Mail extends node_events_1.EventEmitter {
constructor(transporter, options, defaults) {
super();
this.options = options || {};
this._defaults = defaults || {};
this._defaultPlugins = {
compile: [(...args) => this._convertDataImages(...args)],
stream: []
};
this._userPlugins = {
compile: [],
stream: []
};
this.meta = new Map();
this.dkim = this.options.dkim ? new index_js_2.default(this.options.dkim) : false;
this.transporter = transporter;
this.transporter.mailer = this;
this.logger = shared.getLogger(this.options, {
component: this.options.component || 'mail'
});
this.logger.debug({
tnx: 'create'
}, 'Creating transport: %s', this.getVersionString());
// setup emit handlers for the transporter
if (typeof this.transporter.on === 'function') {
// deprecated log interface
this.transporter.on('log', log => {
this.logger.debug({
tnx: 'transport'
}, '%s: %s', log.type, log.message);
});
// transporter errors
this.transporter.on('error', err => {
this.logger.error({
err,
tnx: 'transport'
}, 'Transport Error: %s', err.message);
this.emit('error', err);
});
// indicates if the sender has became idle
this.transporter.on('idle', (...args) => {
this.emit('idle', ...args);
});
// indicates if the sender has became idle and all connections are terminated
this.transporter.on('clear', (...args) => {
this.emit('clear', ...args);
});
}
/**
* Optional methods passed to the underlying transport object
*/
['close', 'isIdle', 'verify'].forEach(method => {
this[method] = (...args) => {
if (typeof this.transporter[method] === 'function') {
if (method === 'verify' && typeof this.getSocket === 'function') {
this.transporter.getSocket = this.getSocket;
this.getSocket = false;
}
return this.transporter[method](...args);
}
this.logger.warn({
tnx: 'transport',
methodName: method
}, 'Non existing method %s called for transport', method);
return false;
};
});
// setup proxy handling
if (this.options.proxy && typeof this.options.proxy === 'string') {
this.setupProxy(this.options.proxy);
}
}
use(step, plugin) {
step = (step || '').toString();
if (!this._userPlugins.hasOwnProperty(step)) {
this._userPlugins[step] = [plugin];
}
else {
this._userPlugins[step].push(plugin);
}
return this;
}
sendMail(data, callback = null) {
let promise;
if (!callback) {
promise = new Promise((resolve, reject) => {
callback = shared.callbackPromise(resolve, reject);
});
}
const done = callback;
if (typeof this.getSocket === 'function') {
this.transporter.getSocket = this.getSocket;
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,
version: this.transporter.version,
action: 'send'
}, 'Sending mail using %s/%s', this.transporter.name, this.transporter.version);
this._processPlugins('compile', mail, err => {
if (err) {
this.logger.error({
err,
tnx: 'plugin',
action: 'compile'
}, 'PluginCompile Error: %s', err.message);
return fail(err);
}
let recipientCount;
try {
mail.message = new index_js_1.default(mail.data).compile();
mail.setMailerHeader();
mail.setPriorityHeaders();
mail.setListHeaders();
recipientCount = mail.message.getEnvelope().to.length;
}
catch (err) {
// message data can throw while it is compiled, the error belongs to the callback
this.logger.error({
err,
tnx: 'transport',
action: 'send'
}, 'Compile Error: %s', err.message);
return fail(err);
}
const maxRecipients = mail.data.maxRecipients === undefined ? DEFAULT_MAX_RECIPIENTS : mail.data.maxRecipients;
if (maxRecipients && recipientCount > maxRecipients) {
const err = new Error(`Message has ${recipientCount} recipients, which is over the ${maxRecipients} allowed by maxRecipients`);
err.code = errors.EMAXRECIPIENTS;
this.logger.error({
err,
tnx: 'transport',
action: 'send'
}, 'Send Error: %s', err.message);
return fail(err);
}
this._processPlugins('stream', mail, err => {
if (err) {
this.logger.error({
err,
tnx: 'plugin',
action: 'stream'
}, 'PluginStream Error: %s', err.message);
return fail(err);
}
if (mail.data.dkim || this.dkim) {
mail.message.processFunc(input => {
const dkim = mail.data.dkim ? new index_js_2.default(mail.data.dkim) : this.dkim;
this.logger.debug({
tnx: 'DKIM',
messageId: mail.message.messageId(),
dkimDomains: dkim.keys.map(key => key.keySelector + '.' + key.domainName).join(', ')
}, 'Signing outgoing message with %s keys', dkim.keys.length);
return dkim.sign(input, mail.data._dkim);
});
}
this.transporter.send(mail, (...args) => {
if (args[0]) {
mail.releaseStreams();
this.logger.error({
err: args[0],
tnx: 'transport',
action: 'send'
}, 'Send Error: %s', args[0].message);
}
done(...args);
});
});
});
return promise;
}
getVersionString() {
return node_util_1.default.format('%s (%s; +%s; %s/%s)', packageData.name, packageData.version, packageData.homepage, this.transporter.name, this.transporter.version);
}
/** @internal */
_processPlugins(step, mail, callback) {
step = (step || '').toString();
if (!this._userPlugins.hasOwnProperty(step)) {
return callback();
}
const userPlugins = this._userPlugins[step] || [];
const defaultPlugins = this._defaultPlugins[step] || [];
if (userPlugins.length) {
this.logger.debug({
tnx: 'transaction',
pluginCount: userPlugins.length,
step
}, 'Using %s plugins for %s', userPlugins.length, step);
}
if (userPlugins.length + defaultPlugins.length === 0) {
return callback();
}
let pos = 0;
let block = 'default';
const processPlugins = () => {
let curplugins = block === 'default' ? defaultPlugins : userPlugins;
if (pos >= curplugins.length) {
if (block === 'default' && userPlugins.length) {
block = 'user';
pos = 0;
curplugins = userPlugins;
}
else {
return callback();
}
}
const plugin = curplugins[pos++];
plugin(mail, err => {
if (err) {
return callback(err);
}
processPlugins();
});
};
processPlugins();
}
/**
* Sets up proxy handler for a Nodemailer object
*
* @param proxyUrl Proxy configuration url
*/
setupProxy(proxyUrl) {
const proxy = urllib.parse(proxyUrl);
// 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);
}
switch (protocol) {
// Connect using a HTTP CONNECT method
case 'http':
case 'https':
(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);
}
return callback(null, {
connection: socket
});
});
return;
case 'socks':
case 'socks5':
case 'socks4':
case 'socks4a': {
if (!this.meta.has('proxy_socks_module')) {
let err = new Error('Socks module not loaded');
err.code = errors.EPROXY;
return callback(err);
}
const connect = (ipaddress) => {
const proxyV2 = !!this.meta.get('proxy_socks_module').SocksClient;
const socksClient = proxyV2 ? this.meta.get('proxy_socks_module').SocksClient : this.meta.get('proxy_socks_module');
const proxyType = Number(proxy.protocol.replace(/\D/g, '')) || 5;
const connectionOpts = {
proxy: {
ipaddress,
port: Number(proxy.port),
type: proxyType
},
[proxyV2 ? 'destination' : 'target']: {
host: options.host,
port: options.port
},
command: 'connect'
};
if (connectionTimeout) {
connectionOpts.timeout = connectionTimeout;
}
if (proxy.username || proxy.password) {
const username = proxy.username || '';
const password = proxy.password || '';
if (proxyV2) {
connectionOpts.proxy.userId = username;
connectionOpts.proxy.password = password;
}
else if (proxyType === 4) {
connectionOpts.userid = username;
}
else {
connectionOpts.authentication = {
username,
password
};
}
}
socksClient.createConnection(connectionOpts, (err, info) => {
if (err) {
return callback(err);
}
return callback(null, {
connection: info.socket || info
});
});
};
if (node_net_1.default.isIP(proxy.hostname)) {
return connect(proxy.hostname);
}
// 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(address);
});
}
}
let err = new Error('Unknown proxy configuration');
err.code = errors.EPROXY;
callback(err);
};
}
/** @internal */
_convertDataImages(mail, callback) {
if ((!this.options.attachDataUrls && !mail.data.attachDataUrls) || !mail.data.html) {
return callback();
}
mail.resolveContent(mail.data, 'html', { disableFileAccess: mail.data.disableFileAccess, disableUrlAccess: mail.data.disableUrlAccess }, (err, html) => {
if (err) {
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]{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 = [];
}
if (!Array.isArray(mail.data.attachments)) {
mail.data.attachments = [].concat(mail.data.attachments || []);
}
mail.data.attachments.push({
path: dataUri,
cid,
filename: 'image-' + ++cidCounter + '.' + mimeTypes.detectExtension(mimeType)
});
return prefix + 'cid:' + cid;
});
mail.data.html = html;
callback();
});
}
set(key, value) {
return this.meta.set(key, value);
}
get(key) {
return this.meta.get(key);
}
}
exports.default = Mail;
module.exports = exports.default;
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
+106
View File
@@ -0,0 +1,106 @@
import MimeNode, { type MimeNodePreparedHeaderValue } from '../mime-node/index.js';
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';
import type { SentMessageInfo } from './index.js';
/**
* The message data accepted by sendMail. MailComposerOptions describes the fields the MIME
* tree is built from, the fields below are the ones the mailer reads on top of those
*/
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.
* 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?: SESSendEmailRequest | undefined;
}
/**
* Default message fields, the third argument of createTransport. Applied to every message
* for the fields the message does not set itself, the headers are merged one by one
*/
export type MailDefaults = SendMailOptions;
/**
* The message data as held by a MailMessage: the options the caller passed to sendMail with
* the transporter defaults applied. resolveAll rewrites the content and address fields in
* place and normalize adds the envelope, the Message-ID and the normalized headers
*/
export interface MailMessageData extends SendMailOptions {
/** Header values flattened to strings and keyed by lowercase header name, set by normalize */
normalizedHeaders?: {
[key: string]: string;
} | undefined;
}
/**
* Callback for resolveAll and normalize, receives the message data with every content value
* resolved
*/
export type MailMessageDataCallback = (err: NodemailerError | null, data: MailMessageData) => void;
/**
* Callback for resolveContent, receives the resolved content value
*/
export type MailMessageContentCallback = (err: NodemailerError | null, value?: any) => void;
/**
* A single prepared List-* header value, emitted as is
*/
export interface MailMessageListHeaderValue extends MimeNodePreparedHeaderValue {
prepared: boolean;
foldLines: boolean;
value: string;
}
/**
* A List-* header as built from the list value, one prepared value per list entry
*/
export interface MailMessageListHeader {
/** Header key, 'list-' followed by the lowercase list key */
key: string;
value: MailMessageListHeaderValue[];
}
export default class MailMessage<T = SentMessageInfo> {
mailer: Mail<T>;
data: MailMessageData;
/**
* 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;
}, key: string | number, callback: MailMessageContentCallback): void;
resolveContent(data: {
[key: string]: any;
}, key: string | number, options: ResolveContentOptions | false | undefined, callback: MailMessageContentCallback): void;
resolveContent(data: {
[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;
setListHeaders(): void;
}
+365
View File
@@ -0,0 +1,365 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const shared = __importStar(require("../shared/index.js"));
const index_js_1 = __importDefault(require("../mime-node/index.js"));
const mimeFuncs = __importStar(require("../mime-funcs/index.js"));
// Only an own key counts as already set. `key in obj` also matches every member of
// Object.prototype, which silently drops a transporter default legitimately named
// toString or constructor.
const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
class MailMessage {
constructor(mailer, data) {
this.mailer = mailer;
this.data = {};
this.message = null;
data = data || {};
const options = mailer.options || {};
const defaults = mailer._defaults || {};
shared.copyOwnKeys(this.data, data);
this.data.headers = this.data.headers || {};
// Apply defaults. `_defaults` is caller supplied too, it is the second argument of
// createTransport, so it needs the same treatment as `data` above
shared.copyOwnKeys(this.data, defaults, key => hasOwn(this.data, key));
// headers is a special case. Allow setting individual default headers
shared.copyOwnKeys(this.data.headers, defaults.headers, key => hasOwn(this.data.headers, key));
// force specific keys from transporter options
['disableFileAccess', 'disableUrlAccess', 'normalizeHeaderKey', 'maxRecipients'].forEach(key => {
if (key in options) {
this.data[key] = options[key];
}
});
// The access flags are a sandbox rather than a message field, so `defaults` counts as
// transporter configuration for them. For a transporter plugin it is the only channel
// there is, createTransport leaves `options` undefined for one, and the defaults copy
// above yields to anything the message already set, which let message data switch the
// sandbox back off. Closing is one way here, same as in resolveContent below: either
// side may switch a flag on, neither can switch off what the other closed.
['disableFileAccess', 'disableUrlAccess'].forEach(key => {
if (!(key in options) && hasOwn(defaults, key)) {
this.data[key] = this.data[key] || defaults[key];
}
});
}
resolveContent(data, key, options, callback) {
// Most plugins call this with the legacy (data, key, callback) signature, which carries
// no access policy. The policy belongs to the message, so apply it here. Explicit
// options may only tighten it, never reopen what the transporter closed.
if (!callback && typeof options === 'function') {
callback = options;
options = false;
}
options = options || {};
const policy = {
disableFileAccess: this.data.disableFileAccess || options.disableFileAccess,
disableUrlAccess: this.data.disableUrlAccess || options.disableUrlAccess
};
return shared.resolveContent(data, key, policy, callback);
}
resolveAll(callback) {
const keys = [
[this.data, 'html'],
[this.data, 'text'],
[this.data, 'watchHtml'],
[this.data, 'amp'],
[this.data, 'icalEvent']
];
if (this.data.alternatives && this.data.alternatives.length) {
this.data.alternatives.forEach((alternative, i) => {
keys.push([this.data.alternatives, i]);
});
}
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-' + (i + 1);
if (attachment.filename.indexOf('.') < 0) {
attachment.filename += '.' + mimeFuncs.detectExtension(attachment.contentType);
}
}
if (!attachment.contentType) {
attachment.contentType = mimeFuncs.detectMimeType(attachment.filename || attachment.path || attachment.href || 'bin');
}
keys.push([this.data.attachments, i]);
});
}
const mimeNode = new index_js_1.default();
const addressKeys = ['from', 'to', 'cc', 'bcc', 'sender', 'replyTo'];
addressKeys.forEach(address => {
let value;
if (this.message) {
value = [].concat(mimeNode._parseAddresses(this.message.getHeader(address === 'replyTo' ? 'reply-to' : address)) || []);
}
else if (this.data[address]) {
value = [].concat(mimeNode._parseAddresses(this.data[address]) || []);
}
if (value && value.length) {
this.data[address] = value;
}
else if (address in this.data) {
this.data[address] = null;
}
});
const singleKeys = ['from', 'sender'];
singleKeys.forEach(address => {
if (this.data[address]) {
this.data[address] = this.data[address].shift();
}
});
let pos = 0;
const resolveNext = () => {
if (pos >= keys.length) {
return callback(null, this.data);
}
const args = keys[pos++];
if (!args[0] || !args[0][args[1]]) {
return resolveNext();
}
shared.resolveContent(...args, { disableFileAccess: this.data.disableFileAccess, disableUrlAccess: this.data.disableUrlAccess }, (err, value) => {
if (err) {
return callback(err);
}
const node = {
content: value
};
if (args[0][args[1]] && typeof args[0][args[1]] === 'object' && !Buffer.isBuffer(args[0][args[1]])) {
// The keys are the caller's, so copying them takes the same "__proto__"
// rule as the constructor. `key in node` stays as the already-set test
// here, unlike for the defaults: it also skips the Object.prototype
// member names, and letting message data land a `toString` string on a
// node only buys a TypeError the first time something stringifies it.
shared.copyOwnKeys(node, args[0][args[1]], key => key in node || ['content', 'path', 'href', 'raw'].includes(key));
}
args[0][args[1]] = node;
resolveNext();
});
};
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();
this.resolveAll((err, data) => {
if (err) {
return callback(err);
}
data.envelope = envelope;
data.messageId = messageId;
['html', 'text', 'watchHtml', 'amp'].forEach(key => {
if (data[key] && data[key].content) {
if (typeof data[key].content === 'string') {
data[key] = data[key].content;
}
else if (Buffer.isBuffer(data[key].content)) {
data[key] = data[key].content.toString();
}
}
});
if (data.icalEvent && Buffer.isBuffer(data.icalEvent.content)) {
data.icalEvent.content = data.icalEvent.content.toString('base64');
data.icalEvent.encoding = 'base64';
}
if (data.alternatives && data.alternatives.length) {
data.alternatives.forEach((alternative) => {
if (alternative && alternative.content && Buffer.isBuffer(alternative.content)) {
alternative.content = alternative.content.toString('base64');
alternative.encoding = 'base64';
}
});
}
if (data.attachments && data.attachments.length) {
data.attachments.forEach((attachment) => {
if (attachment && attachment.content && Buffer.isBuffer(attachment.content)) {
attachment.content = attachment.content.toString('base64');
attachment.encoding = 'base64';
}
});
}
data.normalizedHeaders = {};
Object.keys(data.headers || {}).forEach(key => {
if (shared.isProtoKey(key)) {
return;
}
let value = [].concat(data.headers[key] || []).shift();
value = (value && value.value) || value;
if (value) {
if (['references', 'in-reply-to', 'message-id', 'content-id'].includes(key)) {
value = this.message._encodeHeaderValue(key, value);
}
data.normalizedHeaders[key] = value;
}
});
if (data.list && typeof data.list === 'object') {
const listHeaders = this._getListHeaders(data.list);
listHeaders.forEach(entry => {
data.normalizedHeaders[entry.key] = entry.value.map(val => (val && val.value) || val).join(', ');
});
}
if (data.references) {
data.normalizedHeaders.references = this.message._encodeHeaderValue('references', data.references);
}
if (data.inReplyTo) {
data.normalizedHeaders['in-reply-to'] = this.message._encodeHeaderValue('in-reply-to', data.inReplyTo);
}
return callback(null, data);
});
}
setMailerHeader() {
if (!this.message || !this.data.xMailer) {
return;
}
this.message.setHeader('X-Mailer', this.data.xMailer);
}
setPriorityHeaders() {
if (!this.message || !this.data.priority) {
return;
}
switch ((this.data.priority || '').toString().toLowerCase()) {
case 'high':
this.message.setHeader('X-Priority', '1 (Highest)');
this.message.setHeader('X-MSMail-Priority', 'High');
this.message.setHeader('Importance', 'High');
break;
case 'low':
this.message.setHeader('X-Priority', '5 (Lowest)');
this.message.setHeader('X-MSMail-Priority', 'Low');
this.message.setHeader('Importance', 'Low');
break;
default:
// do not add anything, since all messages are 'Normal' by default
}
}
setListHeaders() {
if (!this.message || !this.data.list || typeof this.data.list !== 'object') {
return;
}
// add optional List-* headers
this._getListHeaders(this.data.list).forEach(listHeader => {
listHeader.value.forEach(value => {
this.message.addHeader(listHeader.key, value);
});
});
}
/** @internal */
_getListHeaders(listData) {
// make sure an url looks like <protocol:url>
return Object.keys(listData).map(key => ({
key: 'list-' + key.toLowerCase().trim(),
value: [].concat(listData[key] || []).map(value => ({
prepared: true,
foldLines: true,
value: []
.concat(value || [])
.map(value => {
if (typeof value === 'string') {
value = {
url: value
};
}
if (value && value.url) {
// strip CR/LF so a comment can't inject extra header lines. DEL is neither
// qtext nor ctext, so it can not be carried literally by either construct
// and has to become an encoded word like any other non-plaintext value
let comment = (value.comment || '').toString().replace(/\r?\n|\r/g, ' ');
const needsEncoding = !mimeFuncs.isPlainText(comment) || /\x7f/.test(comment);
if (key.toLowerCase().trim() === 'id') {
// List-ID: "comment" <domain>, where an unescaped quote or a trailing
// backslash in the comment would swallow the <domain> behind it
comment = needsEncoding ? mimeFuncs.encodeWord(comment) : mimeFuncs.quoteString(comment);
// List-ID expects a bare domain-like identifier, so strip the
// scheme prefix that _formatListUrl adds or passes through
return ((value.comment ? comment + ' ' : '') + this._formatListUrl(value.url).replace(/^<[^:]+:\/{0,2}/, '<'));
}
// List-*: <http://domain> (comment)
// the ctext specials go out as quoted-pairs, otherwise a ")" closes the
// comment early and leaves the rest as junk, an unpaired "(" opens a
// nested comment that never closes, and a trailing backslash escapes
// the closing ")" so the comment swallows whatever follows it
comment = needsEncoding ? mimeFuncs.encodeWord(comment) : comment.replace(/[()\\]/g, '\\$&');
return this._formatListUrl(value.url) + (value.comment ? ' (' + comment + ')' : '');
}
return '';
})
.filter(value => value)
.join(', ')
}))
}));
}
/** @internal */
_formatListUrl(url) {
// a url has no way to carry a control char or DEL, and the angle brackets around it
// are not a quoting construct, so anything left here lands in the header raw
url = url.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '').replace(/[\s<]+|[\s>]+/g, '');
if (/^(https?|mailto|ftp):/.test(url)) {
return '<' + url + '>';
}
if (/^[^@]+@[^@]+$/.test(url)) {
return '<mailto:' + url + '>';
}
return '<http://' + url + '>';
}
}
exports.default = MailMessage;
module.exports = exports.default;
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
+161
View File
@@ -0,0 +1,161 @@
/**
* 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, an empty string when there is none */
value: string;
/** Parameter values keyed by lowercase parameter name */
params: Record<string, string>;
}
/**
* Header value structure accepted by buildHeaderValue
*/
export interface StructuredHeaderValue {
/** The value ahead of the parameters, for example the content type */
value: string | false;
/** Parameter values keyed by parameter name */
params?: Record<string, string> | undefined;
}
/**
* A single key=value pair of an rfc2231 encoded header parameter
*/
export interface EncodedHeaderParam {
/** Parameter name with the continuation suffix, for example title*0* */
key: string;
/** 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)
*
* When isParam is set the value is destined for a header parameter, so HT, CR and LF
* are not plaintext either: a header parameter has no way to carry them. HT is a valid
* fold point, so folding and unfolding a header would rewrite it as a space, and CR/LF
* cannot appear in a header value at all. DEL is neither a token character nor qtext,
* so it can not be carried bare or quoted. Such values have to go through the rfc2231
* parameter continuation encoding instead, the same way a quote already does.
*
* @param value String to be tested
* @param [isParam] Set to true if the value is a header parameter value
* @returns true if it is a plaintext string
*/
export declare function isPlainText(value: unknown, isParam?: boolean): boolean;
/**
* 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.
*
* @param value String to be quoted
* @returns The value as a quoted-string, quotes included
*/
export declare function quoteString(value?: string): string;
/**
* Checks if a multi line string containes lines longer than the selected value.
*
* Useful when detecting if a mail message needs any processing at all:
* if only plaintext characters are used and lines are short, then there is
* 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
*/
export declare function hasLongerLines(str: string, lineLength: number): boolean;
/**
* Encodes a string or an Buffer to an UTF-8 MIME Word (rfc2047)
*
* @param data String to be encoded
* @param mimeWordEncoding='Q' Encoding for the mime word, either Q or B
* @param [maxLength=0] If set, split mime words into several chunks if needed
* @return Single or several mime words joined together
*/
export declare function encodeWord(data: string | Buffer, mimeWordEncoding?: string, maxLength?: number): string;
/**
* Finds word sequences with non ascii text and converts these to mime words
*
* @param value String to be encoded
* @param mimeWordEncoding='Q' Encoding for the mime word, either Q or B
* @param [maxLength=0] If set, split mime words into several chunks if needed
* @param [encodeAll=false] If true and the value needs encoding then encodes entire string, not just the smallest match
* @return String with possible mime words
*/
export declare function encodeWords(value: string, mimeWordEncoding?: string, maxLength?: number, encodeAll?: boolean): string;
/**
* Joins parsed header value together as 'value; param1=value1; param2=value2'
* PS: We are following RFC 822 for the list of special characters that we need to keep in quotes.
* Refer: https://www.w3.org/Protocols/rfc1341/4_Content-Type.html
* @param structured Parsed header value
* @return joined header value
*/
export declare function buildHeaderValue(structured: StructuredHeaderValue): string;
/**
* Encodes a string or an Buffer to an UTF-8 Parameter Value Continuation encoding (rfc2231)
* Useful for splitting long parameter values.
*
* For example
* title="unicode string"
* becomes
* 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
* @return A list of encoded keys and headers
*/
export declare function buildHeaderParam(key: string, data: string | Buffer, maxLength?: number): EncodedHeaderParam[];
/**
* Parses a header value with key=value arguments into a structured
* object.
*
* parseHeaderValue('content-type: text/plain; CHARSET='UTF-8'') ->
* {
* 'value': 'text/plain',
* 'params': {
* 'charset': 'UTF-8'
* }
* }
*
* @param str Header value
* @return Header value as a parsed structure
*/
export declare function parseHeaderValue(str: string): ParsedHeaderValue;
/**
* Returns file extension for a content type string. If no suitable extensions
* are found, 'bin' is used as the default extension
*
* @param mimeType Content type to be checked for
* @return File extension
*/
export declare function detectExtension(mimeType?: string | false): string;
/**
* Returns content type for a file extension. If no suitable content types
* are found, 'application/octet-stream' is used as the default content type
*
* @param extension Extension to be checked for
* @return File extension
*/
export declare function detectMimeType(extension?: string | false): string;
/**
* Folds long lines, useful for folding header lines (afterSpace=false) and
* flowed text (afterSpace=true)
*
* @param str String to be folded
* @param [lineLength=76] Maximum length of a line
* @param afterSpace If true, leave a space in th end of a line
* @return String with folded lines
*/
export declare function foldLines(str: string, lineLength?: number, afterSpace?: boolean): string;
/**
* Splits a mime encoded string. Needed for dividing mime words into smaller chunks
*
* @param str Mime encoded string to be split up
* @param maxlen Maximum length of characters for one part (minimum 12)
* @return Split string
*/
export declare function splitMimeEncodedString(str: string, maxlen?: number): string[];
export declare function encodeURICharComponent(chr: string): string;
export declare function safeEncodeURIComponent(str: string): string;
+805
View File
@@ -0,0 +1,805 @@
"use strict";
/* eslint no-control-regex:0 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.isPlainText = isPlainText;
exports.quoteString = quoteString;
exports.hasLongerLines = hasLongerLines;
exports.encodeWord = encodeWord;
exports.encodeWords = encodeWords;
exports.buildHeaderValue = buildHeaderValue;
exports.buildHeaderParam = buildHeaderParam;
exports.parseHeaderValue = parseHeaderValue;
exports.detectExtension = detectExtension;
exports.detectMimeType = detectMimeType;
exports.foldLines = foldLines;
exports.splitMimeEncodedString = splitMimeEncodedString;
exports.encodeURICharComponent = encodeURICharComponent;
exports.safeEncodeURIComponent = safeEncodeURIComponent;
const base64 = __importStar(require("../base64/index.js"));
const qp = __importStar(require("../qp/index.js"));
const mimeTypes = __importStar(require("./mime-types.js"));
const objects_js_1 = require("../shared/objects.js");
/**
* Checks if a value is plaintext string (uses only printable 7bit chars)
*
* When isParam is set the value is destined for a header parameter, so HT, CR and LF
* are not plaintext either: a header parameter has no way to carry them. HT is a valid
* fold point, so folding and unfolding a header would rewrite it as a space, and CR/LF
* cannot appear in a header value at all. DEL is neither a token character nor qtext,
* so it can not be carried bare or quoted. Such values have to go through the rfc2231
* parameter continuation encoding instead, the same way a quote already does.
*
* @param value String to be tested
* @param [isParam] Set to true if the value is a header parameter value
* @returns true if it is a plaintext string
*/
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.
*
* @param value String to be quoted
* @returns The value as a quoted-string, quotes included
*/
function quoteString(value) {
return '"' + (value || '').toString().replace(/["\\]/g, '\\$&') + '"';
}
/**
* Checks if a multi line string containes lines longer than the selected value.
*
* Useful when detecting if a mail message needs any processing at all:
* if only plaintext characters are used and lines are short, then there is
* 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
*/
function hasLongerLines(str, lineLength) {
if (str.length > 128 * 1024) {
// do not test strings longer than 128kB
return true;
}
return new RegExp('^.{' + (lineLength + 1) + ',}', 'm').test(str);
}
/**
* Encodes a string or an Buffer to an UTF-8 MIME Word (rfc2047)
*
* @param data String to be encoded
* @param mimeWordEncoding='Q' Encoding for the mime word, either Q or B
* @param [maxLength=0] If set, split mime words into several chunks if needed
* @return Single or several mime words joined together
*/
function encodeWord(data, mimeWordEncoding, maxLength) {
mimeWordEncoding = (mimeWordEncoding || 'Q').toString().toUpperCase().trim().charAt(0);
maxLength = maxLength || 0;
let encodedStr;
const toCharset = 'UTF-8';
if (maxLength && maxLength > 7 + toCharset.length) {
maxLength -= 7 + toCharset.length;
}
if (mimeWordEncoding === 'Q') {
// https://tools.ietf.org/html/rfc2047#section-5 rule (3)
encodedStr = qp.encode(data).replace(/[^a-z0-9!*+\-/=]/gi, chr => {
const ord = chr.charCodeAt(0).toString(16).toUpperCase();
if (chr === ' ') {
return '_';
}
return '=' + (ord.length === 1 ? '0' + ord : ord);
});
}
else if (mimeWordEncoding === 'B') {
// the chunking loop below splits raw text and base64 encodes each part, so a
// Buffer goes in as its UTF-8 string: handing it the base64 of the whole input
// would encode the encoding itself and decode back to base64 text
encodedStr = typeof data === 'string' ? data : data.toString('utf-8');
maxLength = maxLength ? Math.max(3, ((maxLength - (maxLength % 4)) / 4) * 3) : 0;
}
if (maxLength && (mimeWordEncoding !== 'B' ? encodedStr : base64.encode(data)).length > maxLength) {
if (mimeWordEncoding === 'Q') {
encodedStr = splitMimeEncodedString(encodedStr, maxLength).join('?= =?' + toCharset + '?' + mimeWordEncoding + '?');
}
else {
// RFC2047 6.3 (2) states that encoded-word must include an integral number of characters, so no chopping unicode sequences
const parts = [];
let lpart = '';
for (let i = 0, len = encodedStr.length; i < len; i++) {
let chr = encodedStr.charAt(i);
if (/[\ud800-\udbff]/.test(chr) && /[\udc00-\udfff]/.test(encodedStr.charAt(i + 1))) {
// leading surrogate, so add the trailing surrogate as well
// an unpaired one must not swallow the next unit, that would destroy
// a valid pair following it
chr += encodedStr.charAt(++i);
}
// check if we can add this character to the existing string
// without breaking byte length limit
if (Buffer.byteLength(lpart + chr) <= maxLength || i === 0) {
lpart += chr;
}
else {
// we hit the length limit, so push the existing string and start over
parts.push(base64.encode(lpart));
lpart = chr;
}
}
if (lpart) {
parts.push(base64.encode(lpart));
}
if (parts.length > 1) {
encodedStr = parts.join('?= =?' + toCharset + '?' + mimeWordEncoding + '?');
}
else {
encodedStr = parts.join('');
}
}
}
else if (mimeWordEncoding === 'B') {
encodedStr = base64.encode(data);
}
return '=?' + toCharset + '?' + mimeWordEncoding + '?' + encodedStr + (encodedStr.substr(-2) === '?=' ? '' : '?=');
}
/**
* Finds word sequences with non ascii text and converts these to mime words
*
* @param value String to be encoded
* @param mimeWordEncoding='Q' Encoding for the mime word, either Q or B
* @param [maxLength=0] If set, split mime words into several chunks if needed
* @param [encodeAll=false] If true and the value needs encoding then encodes entire string, not just the smallest match
* @return String with possible mime words
*/
function encodeWords(value, mimeWordEncoding, maxLength, encodeAll) {
maxLength = maxLength || 0;
// 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);
}
// 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++;
}
return ((startIndex ? value.substr(0, startIndex) : '') +
encodeWord(value.substring(startIndex, endIndex), mimeWordEncoding || 'Q', maxLength) +
(endIndex < value.length ? value.substr(endIndex) : ''));
}
/**
* Joins parsed header value together as 'value; param1=value1; param2=value2'
* PS: We are following RFC 822 for the list of special characters that we need to keep in quotes.
* Refer: https://www.w3.org/Protocols/rfc1341/4_Content-Type.html
* @param structured Parsed header value
* @return joined header value
*/
function buildHeaderValue(structured) {
const paramsArray = [];
Object.keys(structured.params || {}).forEach(key => {
// filename might include unicode characters so it is a special case
// other values probably do not
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 (!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);
}
else {
paramsArray.push(encodedParam.key + '=' + JSON.stringify(encodedParam.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 {
paramsArray.push(param + '=' + value);
}
});
// the value ahead of the parameters is a token, it has no way to carry a control
// char or DEL and there is no quoting construct around it to escape one into
const value = typeof structured.value === 'string' ? structured.value.replace(/[\x00-\x1f\x7f]/g, '') : structured.value;
return value + (paramsArray.length ? '; ' + paramsArray.join('; ') : '');
}
/**
* Encodes a string or an Buffer to an UTF-8 Parameter Value Continuation encoding (rfc2231)
* Useful for splitting long parameter values.
*
* For example
* title="unicode string"
* becomes
* 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
* @return A list of encoded keys and headers
*/
function buildHeaderParam(key, data, maxLength) {
const list = [];
let encodedStr = typeof data === 'string' ? data : (data || '').toString();
let chr;
let line;
let startPos = 0;
let i, len;
maxLength = maxLength || 50;
// process ascii only text
if (isPlainParamValue(data)) {
// check if conversion is even needed
if (encodedStr.length <= maxLength) {
return [
{
key,
value: encodedStr
}
];
}
encodedStr = encodedStr.replace(new RegExp('.{' + maxLength + '}', 'g'), str => {
list.push({
line: str
});
return '';
});
if (encodedStr) {
list.push({
line: encodedStr
});
}
}
else {
if (/[\uD800-\uDBFF]/.test(encodedStr)) {
// string containts surrogate pairs, so normalize it to an array of bytes
const encodedStrArr = [];
for (i = 0, len = encodedStr.length; i < len; i++) {
chr = encodedStr.charAt(i);
if (/[\ud800-\udbff]/.test(chr) && /[\udc00-\udfff]/.test(encodedStr.charAt(i + 1))) {
// an unpaired leading surrogate must not consume the next unit, that
// would tear apart a valid pair following it
chr += encodedStr.charAt(i + 1);
encodedStrArr.push(chr);
i++;
}
else {
encodedStrArr.push(chr);
}
}
encodedStr = encodedStrArr;
}
// first line includes the charset and language info and needs to be encoded
// even if it does not contain any unicode characters
line = "utf-8''";
let encoded = true;
startPos = 0;
// process text with unicode or special chars
for (i = 0, len = encodedStr.length; i < len; i++) {
chr = encodedStr[i];
if (encoded) {
chr = safeEncodeURIComponent(chr);
}
else {
// try to urlencode current char
chr = chr === ' ' ? chr : safeEncodeURIComponent(chr);
// By default it is not required to encode a line, the need
// only appears when the string contains unicode or special chars
// in this case we start processing the line over and encode all chars
if (chr !== encodedStr[i]) {
// Check if it is even possible to add the encoded char to the line
// If not, there is no reason to use this line, just push it to the list
// and start a new line with the char that needs encoding
if ((safeEncodeURIComponent(line) + chr).length >= maxLength) {
list.push({
line,
encoded
});
// the line we start here holds an encoded char, so it has to be
// flagged as one. otherwise it gets an unstarred continuation key
// and a receiver reads the percent escapes as literal text
line = '';
encoded = true;
}
else {
encoded = true;
i = startPos;
line = '';
continue;
}
}
}
// if the line is already too long, push it to the list and start a new one
if ((line + chr).length >= maxLength) {
list.push({
line,
encoded
});
line = chr = encodedStr[i] === ' ' ? ' ' : safeEncodeURIComponent(encodedStr[i]);
if (chr === encodedStr[i]) {
encoded = false;
startPos = i - 1;
}
else {
encoded = true;
}
}
else {
line += chr;
}
}
if (line) {
list.push({
line,
encoded
});
}
}
return list.map((item, i) => ({
// encoded lines: {name}*{part}*
// unencoded lines: {name}*{part}
// if any line needs to be encoded then the first line (part==0) is always encoded
key: key + '*' + i + (item.encoded ? '*' : ''),
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.
*
* parseHeaderValue('content-type: text/plain; CHARSET='UTF-8'') ->
* {
* 'value': 'text/plain',
* 'params': {
* 'charset': 'UTF-8'
* }
* }
*
* @param str Header value
* @return Header value as a parsed structure
*/
function parseHeaderValue(str) {
const response = {
value: '',
params: {}
};
// 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) => {
name = name.toLowerCase();
if (_isParamName(name) && !Object.prototype.hasOwnProperty.call(response.params, name)) {
response.params[name] = value;
}
};
let key = false;
let 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 (stage === 'key') {
if (chr === '=') {
key = takeValue().trim();
stage = 'value';
continue;
}
if (chr === ';') {
storeEmptyKey();
continue;
}
value += chr;
}
else {
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;
}
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 === ';') {
storeValue();
stage = 'key';
}
else if (!quote && (chr === ' ' || chr === '\t')) {
pendingSpace += chr;
}
else {
addChr(chr);
}
escaped = false;
}
}
if (stage === 'value') {
storeValue();
}
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
// 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 => {
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
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
*
* @param mimeType Content type to be checked for
* @return File extension
*/
function detectExtension(mimeType) {
return mimeTypes.detectExtension(mimeType);
}
/**
* Returns content type for a file extension. If no suitable content types
* are found, 'application/octet-stream' is used as the default content type
*
* @param extension Extension to be checked for
* @return File extension
*/
function detectMimeType(extension) {
return mimeTypes.detectMimeType(extension);
}
/**
* Folds long lines, useful for folding header lines (afterSpace=false) and
* flowed text (afterSpace=true)
*
* @param str String to be folded
* @param [lineLength=76] Maximum length of a line
* @param afterSpace If true, leave a space in th end of a line
* @return String with folded lines
*/
function foldLines(str, lineLength, afterSpace) {
str = (str || '').toString();
lineLength = lineLength || 76;
let pos = 0;
const len = str.length;
let result = '';
let line, match;
while (pos < len) {
line = str.substr(pos, lineLength);
if (line.length < lineLength) {
result += line;
break;
}
if ((match = line.match(/^[^\n\r]*(\r?\n|\r)/))) {
line = match[0];
result += line;
pos += line.length;
continue;
}
else if ((match = line.match(/(\s+)[^\s]*$/)) && match[0].length - (afterSpace ? (match[1] || '').length : 0) < line.length) {
line = line.substr(0, line.length - (match[0].length - (afterSpace ? (match[1] || '').length : 0)));
}
else if ((match = str.substr(pos + line.length).match(/^[^\s]+(\s*)/))) {
line = line + match[0].substr(0, match[0].length - (!afterSpace ? (match[1] || '').length : 0));
}
result += line;
pos += line.length;
if (pos < len) {
result += '\r\n';
}
}
return result;
}
/**
* Splits a mime encoded string. Needed for dividing mime words into smaller chunks
*
* @param str Mime encoded string to be split up
* @param maxlen Maximum length of characters for one part (minimum 12)
* @return Split string
*/
function splitMimeEncodedString(str, maxlen) {
const lines = [];
let curLine, fallbackLine, match, chr, done;
// require at least 12 symbols to fit possible 4 octet UTF-8 sequences
maxlen = Math.max(maxlen || 0, 12);
while (str.length) {
curLine = str.substr(0, maxlen);
// move incomplete escaped char back to main
if ((match = curLine.match(/[=][0-9A-F]?$/i))) {
curLine = curLine.substr(0, match.index);
}
// Malformed input (a run of stray UTF-8 continuation bytes) has no split point
// that keeps a character sequence whole, so the loop below walks back to an
// empty line looking for one. Keep the widest chunk that at least does not cut
// a "=XX" escape in half, so the part stays a decodable encoded word.
fallbackLine = curLine.length ? curLine : str.substr(0, maxlen);
done = false;
while (!done && curLine.length) {
done = true;
// check if not middle of a unicode char sequence
if ((match = str.substr(curLine.length).match(/^[=]([0-9A-F]{2})/i))) {
chr = parseInt(match[1], 16);
// invalid sequence, move one char back anc recheck
if (chr < 0xc2 && chr > 0x7f) {
curLine = curLine.substr(0, curLine.length - 3);
done = false;
}
}
}
if (!curLine.length) {
curLine = fallbackLine;
}
lines.push(curLine);
str = str.substr(curLine.length);
}
return lines;
}
function encodeURICharComponent(chr) {
let res = '';
let ord = chr.charCodeAt(0).toString(16).toUpperCase();
if (ord.length % 2) {
ord = '0' + ord;
}
if (ord.length > 2) {
for (let i = 0, len = ord.length / 2; i < len; i++) {
res += '%' + ord.substr(i, 2);
}
}
else {
res += '%' + ord;
}
return res;
}
function safeEncodeURIComponent(str) {
str = (str || '').toString();
try {
// might throw if we try to encode invalid sequences, eg. partial emoji
str = encodeURIComponent(str);
}
catch (_E) {
// an unpaired surrogate has no utf-8 representation, so run the value through a
// utf-8 roundtrip to get the same U+FFFD every other encoder here produces and
// retry. the value must never come back unencoded, it goes into a header parameter
// where a bare quote or semicolon would break it out into a parameter of its own
str = encodeURIComponent(Buffer.from(str, 'utf-8').toString('utf-8'));
}
// ensure chars that are not handled by encodeURICompent are converted as well
return str.replace(/[\x00-\x1F *'()<>@,;:\\"[\]?=\u007F-\uFFFF]/g, chr => encodeURICharComponent(chr));
}
+2
View File
@@ -0,0 +1,2 @@
export declare function detectMimeType(filename?: string | false): string;
export declare function detectExtension(mimeType?: string | false): string;
File diff suppressed because it is too large. Load diff
+333
View File
@@ -0,0 +1,333 @@
import { type Duplex, type Readable, type TransformOptions, type Writable } from 'node:stream';
import type { NodemailerError } from '../errors.js';
import type { OutgoingHttpHeaders } from 'node:http';
/**
* Options for a MimeNode
*/
export interface MimeNodeOptions {
/** root node for this tree */
rootNode?: MimeNode | undefined;
/** immediate parent for this node */
parentNode?: MimeNode | undefined;
/** filename for an attachment node */
filename?: string | undefined;
/** shared part of the unique multipart boundary */
baseBoundary?: string | undefined;
/** prefix for the generated multipart boundaries, defaults to '--_NmP' */
boundaryPrefix?: string | undefined;
/** If true, do not exclude Bcc from the generated headers */
keepBcc?: boolean | undefined;
/** method to normalize header keys for custom caseing */
normalizeHeaderKey?: ((key: string, value: string) => string) | undefined;
/** either 'Q' (the default) or 'B' */
textEncoding?: string | undefined;
/** Hostname for default message-id values */
hostname?: string | undefined;
/** If set to 'win' then uses \r\n, if 'linux' then \n. If not set (or `raw` is used) then newlines are kept as is */
newline?: string | undefined;
/** Reject content that points to a file path, for this node and every node below it */
disableFileAccess?: boolean | undefined;
/** Reject content that points to a URL, for this node and every node below it */
disableUrlAccess?: boolean | undefined;
}
/**
* An address object as accepted in address headers and produced by getAddresses. Either a
* mailbox with an address or a group holding a list of addresses
*/
export interface MimeNodeAddress {
name?: string | undefined;
address?: string | undefined;
group?: MimeNodeAddress[] | undefined;
}
/**
* Addresses as accepted by address headers and envelope fields: an address string, an
* address object, or an array of these, nested arrays included
*/
export type MimeNodeAddressInput = string | MimeNodeAddress | MimeNodeAddressInput[];
/**
* A header value object. When `prepared` is set the value is emitted as is, without
* encoding, and folded only if `foldLines` is set as well
*/
export interface MimeNodePreparedHeaderValue {
value?: unknown;
prepared?: boolean | undefined;
foldLines?: boolean | undefined;
}
/**
* The values a header can hold: a string, a number, a Date (for the Date header), address
* objects (for the address headers), a prepared value object, or a list of these
*/
export type MimeNodeHeaderValue = string | number | boolean | Date | MimeNodeAddress | MimeNodePreparedHeaderValue | MimeNodeHeaderValue[] | null | undefined;
/**
* A single header, the shape the headers are stored in and one of the list forms
* setHeader and addHeader accept
*/
export interface MimeNodeHeader {
key: string;
value: MimeNodeHeaderValue;
}
/**
* Several headers keyed by header name
*/
export interface MimeNodeHeaderMap {
[key: string]: MimeNodeHeaderValue;
}
/**
* Several headers at once, as accepted by setHeader and addHeader: a single {key, value}
* pair, a list of them, or an object keyed by header name
*/
export type MimeNodeHeaders = MimeNodeHeader | MimeNodeHeader[] | MimeNodeHeaderMap;
/**
* A content descriptor object, see setContent. Points to the content instead of carrying it
*/
export interface MimeNodeContentObject {
/** File path to read the content from */
path?: string | undefined;
/** URL to fetch the content from */
href?: string | undefined;
/** Request headers for a URL fetch */
httpHeaders?: OutgoingHttpHeaders | undefined;
/** TLS settings for a URL fetch, see nmfetch */
tls?: {
[key: string]: any;
} | undefined;
}
/**
* Body content as accepted by setContent and setRaw: a string, a Buffer, a readable stream
* or an object pointing to the content
*/
export type MimeNodeContent = string | Buffer | Readable | MimeNodeContentObject;
/**
* SMTP envelope as returned by getEnvelope. Custom fields of an envelope set with
* setEnvelope are carried along
*/
export interface MimeNodeEnvelope {
from: string | false;
to: string[];
[key: string]: unknown;
}
/**
* Envelope as accepted by setEnvelope. Recipients are collected from to, cc and bcc, any
* 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 | false | undefined;
to?: MimeNodeAddressInput | undefined;
cc?: MimeNodeAddressInput | undefined;
bcc?: MimeNodeAddressInput | undefined;
[key: string]: unknown;
}
/**
* Parsed address headers as returned by getAddresses, keyed by lowercase header name
*/
export interface MimeNodeAddresses {
from?: MimeNodeAddress[] | undefined;
sender?: MimeNodeAddress[] | undefined;
'reply-to'?: MimeNodeAddress[] | undefined;
to?: MimeNodeAddress[] | undefined;
cc?: MimeNodeAddress[] | undefined;
bcc?: MimeNodeAddress[] | undefined;
}
/**
* Options for createReadStream. Handed to the PassThrough the message is written to and to
* the base64 / quoted-printable encoders, which read `lineLength` from it
*/
export interface MimeNodeStreamOptions extends TransformOptions {
/** Maximum line length for base64 and quoted-printable bodies, false disables wrapping */
lineLength?: number | false | undefined;
}
/**
* A transform stream the message is piped through, or a function returning one
*/
export type MimeNodeTransform = Duplex | (() => Duplex);
/**
* A post process function, takes the message stream and returns the stream to expose instead
*/
export type MimeNodeProcessFunc = (input: Readable) => Readable;
/**
* Callback for build
*/
export type MimeNodeBuildCallback = (err: NodemailerError | null, message: Buffer) => void;
/**
* Creates a new mime tree node. Assumes 'multipart/*' as the content type
* if it is a branch, anything else counts as leaf. If rootNode is missing from
* the options, assumes this is the root.
*
* @param contentType Define the content type for the node. Can be left blank for attachments (derived from filename)
* @param [options] optional options
* @param [options.rootNode] root node for this tree
* @param [options.parentNode] immediate parent for this node
* @param [options.filename] filename for an attachment node
* @param [options.baseBoundary] shared part of the unique multipart boundary
* @param [options.keepBcc] If true, do not exclude Bcc from the generated headers
* @param [options.normalizeHeaderKey] method to normalize header keys for custom caseing
* @param [options.textEncoding] either 'Q' (the default) or 'B'
*/
declare class MimeNode {
nodeCounter: number;
baseBoundary: string;
boundaryPrefix: string;
disableFileAccess: boolean;
disableUrlAccess: boolean;
normalizeHeaderKey: MimeNodeOptions['normalizeHeaderKey'];
date: Date | null;
rootNode: MimeNode;
keepBcc: boolean;
textEncoding: string;
parentNode: MimeNode | undefined;
hostname: string | undefined;
newline: string | undefined;
childNodes: MimeNode[];
/** Filename for this node. Useful with attachments */
filename?: string | undefined;
/** Body content */
content?: MimeNodeContent | Error | undefined;
/** Lowercase content type, set when the headers are built */
contentType?: string | undefined;
/** Multipart subtype, false for a non-multipart node, set when the headers are built */
multipart?: string | false | undefined;
/** Multipart boundary, false for a non-multipart node, set when the headers are built */
boundary?: string | false | undefined;
constructor(contentType?: string | false, options?: MimeNodeOptions);
/**
* Creates and appends a child node.Arguments provided are passed to MimeNode constructor
*
* @param [contentType] Optional content type
* @param [options] Optional options object
* @return Created node object
*/
createChild(contentType?: string | false | MimeNodeOptions, options?: MimeNodeOptions): MimeNode;
/**
* Appends an existing node to the mime tree. Removes the node from an existing
* tree if needed
*
* @param childNode node to be appended
* @return Appended node object
*/
appendChild(childNode: MimeNode): MimeNode;
/**
* Replaces current node with another node
*
* @param node Replacement node
* @return Replacement node
*/
replace(node: MimeNode): MimeNode;
/**
* Removes current node from the mime tree
*
* @return removed node
*/
remove(): MimeNode | undefined;
/**
* Sets a header value. If the value for selected key exists, it is overwritten.
* You can set multiple values as well by using [{key:'', value:''}] or
* {key: 'value'} as the first argument.
*
* @param key Header key or a list of key value pairs
* @param value Header value
* @return current node
*/
setHeader(key: string | MimeNodeHeaders, value?: MimeNodeHeaderValue): this;
/**
* Adds a header value. If the value for selected key exists, the value is appended
* as a new field and old one is not touched.
* You can set multiple values as well by using [{key:'', value:''}] or
* {key: 'value'} as the first argument.
*
* @param key Header key or a list of key value pairs
* @param value Header value
* @return current node
*/
addHeader(key: string | MimeNodeHeaders, value?: MimeNodeHeaderValue): this;
/**
* Retrieves the first mathcing value of a selected key
*
* @param key Key to search for
* @retun Value for the key
*/
getHeader(key: string): MimeNodeHeaderValue;
/**
* Sets body content for current node. If the value is a string, charset is added automatically
* to Content-Type (if it is text/*). If the value is a Buffer, you need to specify
* the charset yourself
*
* @param content Body content
* @return current node
*/
setContent(content: MimeNodeContent): this;
build(): Promise<Buffer>;
build(callback: MimeNodeBuildCallback): void;
getTransferEncoding(): string | false;
/**
* Builds the header block for the mime node. Append \r\n\r\n before writing the content
*
* @returns Headers
*/
buildHeaders(): string;
/**
* Streams the rfc2822 message from the current node. If this is a root node,
* mandatory header fields are set if missing (Date, Message-Id, MIME-Version)
*
* @return Compiled message
*/
createReadStream(options?: MimeNodeStreamOptions): Readable;
/**
* Appends a transform stream object to the transforms list. Final output
* is passed through this stream before exposing
*
* @param transform Read-Write stream
*/
transform(transform: MimeNodeTransform): void;
/**
* Appends a post process function. The functon is run after transforms and
* uses the following syntax
*
* processFunc(input) -> outputStream
*
* @param processFunc Read-Write stream
*/
processFunc(processFunc: MimeNodeProcessFunc): void;
stream(outputStream: Writable, options: MimeNodeStreamOptions, done: (err?: Error | null) => void): void;
/**
* Sets envelope to be used instead of the generated one
*
* @return SMTP envelope in the form of {from: 'from@example.com', to: ['to@example.com']}
*/
setEnvelope(envelope: MimeNodeEnvelopeInput): this;
/**
* Generates and returns an object with parsed address fields
*
* @return Address object
*/
getAddresses(): MimeNodeAddresses;
/**
* Generates and returns SMTP envelope with the sender address and a list of recipients addresses
*
* @return SMTP envelope in the form of {from: 'from@example.com', to: ['to@example.com']}
*/
getEnvelope(): MimeNodeEnvelope;
/**
* Returns Message-Id value. If it does not exist, then creates one
*
* @return Message-Id value
*/
messageId(): string;
/**
* Sets pregenerated content that will be used as the output of this node
*
* @param raw Raw MIME contents
*/
setRaw(raw: MimeNodeContent): this;
}
/**
* Type aliases in the layout of @types/nodemailer, so `MimeNode.Options` style references keep working
*/
declare namespace MimeNode {
type Options = MimeNodeOptions;
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;
File diff suppressed because it is too large. Load diff
+5
View File
@@ -0,0 +1,5 @@
import { Transform } from 'node:stream';
export default class LastNewline extends Transform {
lastByte: number | false;
constructor();
}
+32
View File
@@ -0,0 +1,32 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const node_stream_1 = require("node:stream");
class LastNewline extends node_stream_1.Transform {
constructor() {
super();
this.lastByte = false;
}
/** @internal */
_transform(chunk, encoding, done) {
if (chunk.length) {
this.lastByte = chunk[chunk.length - 1];
}
this.push(chunk);
done();
}
/** @internal */
_flush(done) {
if (this.lastByte === 0x0a) {
return done();
}
if (this.lastByte === 0x0d) {
this.push(Buffer.from('\n'));
return done();
}
this.push(Buffer.from('\r\n'));
return done();
}
}
exports.default = LastNewline;
module.exports = exports.default;
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
+9
View File
@@ -0,0 +1,9 @@
import { Transform, type TransformOptions } from 'node:stream';
/**
* Ensures that only <LF> is used for linebreaks
*
* @param options Stream options
*/
export default class LeUnix extends Transform {
constructor(options?: TransformOptions);
}
+40
View File
@@ -0,0 +1,40 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const node_stream_1 = require("node:stream");
/**
* Ensures that only <LF> is used for linebreaks
*
* @param options Stream options
*/
class LeUnix extends node_stream_1.Transform {
constructor(options) {
super(options);
}
/**
* Escapes dots
* @internal
*/
_transform(chunk, encoding, done) {
let buf;
let lastPos = 0;
for (let i = 0, len = chunk.length; i < len; i++) {
if (chunk[i] === 0x0d) {
// \r
buf = chunk.slice(lastPos, i);
lastPos = i + 1;
this.push(buf);
}
}
if (lastPos && lastPos < chunk.length) {
buf = chunk.slice(lastPos);
this.push(buf);
}
else if (!lastPos) {
this.push(chunk);
}
done();
}
}
exports.default = LeUnix;
module.exports = exports.default;
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
+10
View File
@@ -0,0 +1,10 @@
import { Transform, type TransformOptions } from 'node:stream';
/**
* Ensures that only <CR><LF> sequences are used for linebreaks
*
* @param options Stream options
*/
export default class LeWindows extends Transform {
lastByte: number | false;
constructor(options?: TransformOptions);
}
+47
View File
@@ -0,0 +1,47 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const node_stream_1 = require("node:stream");
/**
* Ensures that only <CR><LF> sequences are used for linebreaks
*
* @param options Stream options
*/
class LeWindows extends node_stream_1.Transform {
constructor(options) {
super(options);
this.lastByte = false;
}
/**
* Escapes dots
* @internal
*/
_transform(chunk, encoding, done) {
let buf;
let lastPos = 0;
for (let i = 0, len = chunk.length; i < len; i++) {
if (chunk[i] === 0x0a) {
// \n
if ((i && chunk[i - 1] !== 0x0d) || (!i && this.lastByte !== 0x0d)) {
if (i > lastPos) {
buf = chunk.slice(lastPos, i);
this.push(buf);
}
this.push(Buffer.from('\r\n'));
lastPos = i + 1;
}
}
}
if (lastPos && lastPos < chunk.length) {
buf = chunk.slice(lastPos);
this.push(buf);
}
else if (!lastPos) {
this.push(chunk);
}
this.lastByte = chunk[chunk.length - 1];
done();
}
}
exports.default = LeWindows;
module.exports = exports.default;
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
+124
View File
@@ -0,0 +1,124 @@
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 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
*/
export interface TestAccountService {
host: string;
port: number;
secure: boolean;
}
/**
* Ethereal test account returned by createTestAccount()
*/
export interface TestAccount {
user: string;
pass: string;
smtp: TestAccountService;
imap: TestAccountService;
pop3: TestAccountService;
web: string;
/** true if the account can also receive external mail */
mxEnabled?: boolean | undefined;
[key: string]: unknown;
}
/**
* Callback for createTestAccount()
*/
export type TestAccountCallback = (err: Error | null, account: TestAccount) => void;
/**
* Configuration object of any of the bundled transports
*/
export type TransportConfig = SMTPTransportOptions | SMTPPoolOptions | SendmailTransportOptions | StreamTransportOptions | JSONTransportOptions | SESTransportOptions;
/**
* Creates a transporter object for sending e-mails
*
* @param transporter Transport configuration object, a connection URL or a transport plugin instance
* @param defaults Default message fields that are merged into every message
* @returns Mail instance wrapping the transport
*/
export declare function createTransport(transporter: SMTPPool | (SMTPPoolOptions & {
pool: true;
}), defaults?: MailDefaults): Mail<SMTPPoolSentMessageInfo, SMTPPoolOptions>;
export declare function createTransport(transporter: SendmailTransport | (SendmailTransportOptions & {
sendmail: true | string;
}), defaults?: MailDefaults): Mail<SendmailSentMessageInfo, SendmailTransportOptions>;
export declare function createTransport(transporter: StreamTransport | (StreamTransportOptions & {
streamTransport: true;
}), defaults?: MailDefaults): Mail<StreamSentMessageInfo, StreamTransportOptions>;
export declare function createTransport(transporter: JSONTransportOptions & {
jsonTransport: true;
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, 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 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
*
* @param info Result object of sendMail()
* @returns URL of the message in the Ethereal web interface, or false if the response does not carry one
*/
export declare function getTestMessageUrl(info?: {
response?: string | Buffer | null | undefined;
} | false | null): string | false;
declare const nodemailer: {
createTransport: typeof createTransport;
createTestAccount: typeof createTestAccount;
getTestMessageUrl: typeof getTestMessageUrl;
};
export default nodemailer;
export type { Mail, MailDefaults, SentMessageInfo, Transport, TransportOptions };
export type { SendMailOptions, Transporter } from './mailer/index.js';
export type { NodemailerError, ErrorCode } from './errors.js';
export type { MailMessage, MailMessageData, PluginFunction, VerifyCallback } from './mailer/index.js';
export type { MailComposerOptions, MailComposerAttachment as Attachment, MailComposerAlternative as AttachmentLike, MailComposerIcalEvent as IcalAttachment, MailComposerListHeaders as ListHeaders } from './mail-composer/index.js';
export type { MimeNodeAddress as Address, MimeNodeHeaders as Headers } from './mime-node/index.js';
export type { SMTPConnectionOptions, SMTPConnectionAuth, SMTPEnvelope, SMTPEnvelopeDsn } from './smtp-connection/index.js';
export type { DKIMOptions } from './dkim/index.js';
export type { XOAuth2Options, XOAuth2Token } from './xoauth2/index.js';
export type { SMTPTransportOptions, SMTPSentMessageInfo };
export type { SMTPPoolOptions, SMTPPoolSentMessageInfo };
export type { SendmailTransportOptions, SendmailSentMessageInfo };
export type { StreamTransportOptions, StreamSentMessageInfo };
export type { JSONTransportOptions, JSONSentMessageInfo, JSONSentMessageObjectInfo };
export type { SESTransportOptions, SESSentMessageInfo };
+211
View File
@@ -0,0 +1,211 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createTransport = createTransport;
exports.createTestAccount = createTestAccount;
exports.getTestMessageUrl = getTestMessageUrl;
const index_js_1 = __importDefault(require("./mailer/index.js"));
const shared = __importStar(require("./shared/index.js"));
const index_js_2 = __importDefault(require("./smtp-pool/index.js"));
const index_js_3 = __importDefault(require("./smtp-transport/index.js"));
const index_js_4 = __importDefault(require("./sendmail-transport/index.js"));
const index_js_5 = __importDefault(require("./stream-transport/index.js"));
const index_js_6 = __importDefault(require("./json-transport/index.js"));
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"));
// 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;
if (
// provided transporter is a configuration object, not transporter plugin
(typeof transporter === 'object' && typeof transporter.send !== 'function') ||
// provided transporter looks like a connection url
(typeof transporter === 'string' && /^(smtps?|direct):/i.test(transporter))) {
const urlConfig = typeof transporter === 'string' ? transporter : transporter.url;
if (urlConfig) {
// parse a configuration URL into configuration options. The other keys of a
// configuration object apply where the url does not set them, merged the same
// way the SMTP transports merge their own url option
const parsed = shared.parseConnectionUrl(urlConfig);
options = (typeof transporter === 'object'
? shared.assign(false, shared.copyOwnKeys({}, transporter, key => key === 'url'), parsed)
: parsed);
}
else {
options = transporter;
}
if (options.pool) {
transporter = new index_js_2.default(options);
}
else if (options.sendmail) {
transporter = new index_js_4.default(options);
}
else if (options.streamTransport) {
transporter = new index_js_5.default(options);
}
else if (options.jsonTransport) {
transporter = new index_js_6.default(options);
}
else if (options.SES) {
const ses = options.SES;
if (ses.ses && ses.aws) {
const error = new Error('Using legacy SES configuration, expecting @aws-sdk/client-sesv2, see https://nodemailer.com/transports/ses/');
error.code = errors.ECONFIG;
throw error;
}
transporter = new index_js_7.default(options);
}
else {
transporter = new index_js_3.default(options);
}
}
return new index_js_1.default(transporter, options, defaults);
}
function createTestAccount(apiUrl, callback) {
let promise;
if (!callback && typeof apiUrl === 'function') {
callback = apiUrl;
apiUrl = false;
}
if (!callback) {
promise = new Promise((resolve, reject) => {
callback = shared.callbackPromise(resolve, reject);
});
}
const done = callback;
if (etherealCache() && testAccount) {
setImmediate(() => done(null, testAccount));
return promise;
}
apiUrl = apiUrl || etherealApi();
const chunks = [];
let chunklen = 0;
const requestHeaders = {};
const requestBody = {
requestor: packageData.name,
version: packageData.version
};
const apiKey = etherealApiKey();
if (apiKey) {
requestHeaders.Authorization = 'Bearer ' + apiKey;
}
const fetchOptions = {
contentType: 'application/json',
method: 'POST',
headers: requestHeaders,
body: Buffer.from(JSON.stringify(requestBody))
};
// Credential-bearing request to the Ethereal API. src/fetch already
// validates certs by default; pin rejectUnauthorized:true here so this
// call stays strict regardless of any future default change and is never
// relaxed for a real-cert endpoint.
if (/^https:/i.test(apiUrl)) {
fetchOptions.tls = { rejectUnauthorized: true };
}
const req = (0, index_js_8.default)(apiUrl + '/user', fetchOptions);
req.on('readable', () => {
let chunk;
while ((chunk = req.read()) !== null) {
chunks.push(chunk);
chunklen += chunk.length;
}
});
req.once('error', err => done(err));
req.once('end', () => {
const res = Buffer.concat(chunks, chunklen);
let data;
try {
data = JSON.parse(res.toString());
}
catch (E) {
return done(E);
}
if (data.status !== 'success' || data.error) {
return done(new Error(data.error || 'Request failed'));
}
delete data.status;
testAccount = data;
done(null, testAccount);
});
return promise;
}
/**
* Resolves the Ethereal web URL for a message sent through an Ethereal test account
*
* @param info Result object of sendMail()
* @returns URL of the message in the Ethereal web interface, or false if the response does not carry one
*/
function getTestMessageUrl(info) {
if (!info || !info.response) {
return false;
}
const infoProps = new Map();
// Extract the trailing "[...]" part of the response (no "]" allowed inside)
// with linear string scanning; the equivalent regex /\[([^\]]+)\]$/ was
// flagged for polynomial backtracking on adversarial server responses
const response = info.response.toString();
if (response.length > 2 && response.charAt(response.length - 1) === ']') {
const open = response.indexOf('[', response.lastIndexOf(']', response.length - 2) + 1);
if (open >= 0 && open < response.length - 2) {
const props = response.substring(open + 1, response.length - 1);
props.replace(/\b([A-Z0-9]+)=([^\s]+)/g, (m, key, value) => {
infoProps.set(key, value);
return m;
});
}
}
if (infoProps.has('STATUS') && infoProps.has('MSGID')) {
return ((testAccount && testAccount.web) || etherealWeb()) + '/message/' + infoProps.get('MSGID');
}
return false;
}
const nodemailer = {
createTransport,
createTestAccount,
getTestMessageUrl
};
exports.default = nodemailer;
Object.defineProperty(exports, 'default', { value: exports, enumerable: false, writable: true, configurable: true });
+3
View File
@@ -0,0 +1,3 @@
export declare const name = "nodemailer";
export declare const version = "10.1.7";
export declare const homepage = "https://nodemailer.com/";
+7
View File
@@ -0,0 +1,7 @@
"use strict";
// Generated by scripts/build.js from package.json. Do not edit by hand.
Object.defineProperty(exports, "__esModule", { value: true });
exports.homepage = exports.version = exports.name = void 0;
exports.name = 'nodemailer';
exports.version = '10.1.7';
exports.homepage = 'https://nodemailer.com/';
+3
View File
@@ -0,0 +1,3 @@
{
"type": "commonjs"
}
+73
View File
@@ -0,0 +1,73 @@
/**
* Creates an array containing the numeric code points of each Unicode
* character in the string. While JavaScript uses UCS-2 internally,
* this function will convert a pair of surrogate halves (each of which
* UCS-2 exposes as separate characters) into a single code point,
* matching UTF-16.
* @see `punycode.ucs2.encode`
* @see <https://mathiasbynens.be/notes/javascript-encoding>
* @memberOf punycode.ucs2
* @name decode
* @param string The Unicode input string (UCS-2).
* @returns The new array of code points.
*/
declare function ucs2decode(string: string): number[];
/**
* Converts a Punycode string of ASCII-only symbols to a string of Unicode
* symbols.
* @memberOf punycode
* @param input The Punycode string of ASCII-only symbols.
* @returns The resulting string of Unicode symbols.
*/
declare const decode: (input: string) => string;
/**
* Converts a string of Unicode symbols (e.g. a domain name label) to a
* Punycode string of ASCII-only symbols.
* @memberOf punycode
* @param input The string of Unicode symbols.
* @returns The resulting Punycode string of ASCII-only symbols.
*/
declare const encode: (input: string) => string;
/**
* Converts a Punycode string representing a domain name or an email address
* to Unicode. Only the Punycoded parts of the input will be converted, i.e.
* it doesn't matter if you call it on a string that has already been
* converted to Unicode.
* @memberOf punycode
* @param input The Punycoded domain name or email address to
* convert to Unicode.
* @returns The Unicode representation of the given Punycode
* string.
*/
declare const toUnicode: (input: string) => string;
/**
* Converts a Unicode string representing a domain name or an email address to
* Punycode. Only the non-ASCII parts of the domain name will be converted,
* i.e. it doesn't matter if you call it with a domain that's already in
* ASCII.
* @memberOf punycode
* @param input The domain name or email address to convert, as a
* Unicode string.
* @returns The Punycode representation of the given domain name or
* email address.
*/
declare const toASCII: (input: string) => string;
/** Define the public API */
/**
* A string representing the current Punycode.js version number.
* @memberOf punycode
* @type String
*/
export declare const version = "2.3.1";
/**
* An object of methods to convert from JavaScript's internal character
* representation (UCS-2) to Unicode code points, and back.
* @see <https://mathiasbynens.be/notes/javascript-encoding>
* @memberOf punycode
* @type Object
*/
export declare const ucs2: {
decode: typeof ucs2decode;
encode: (codePoints: number[]) => string;
};
export { decode, encode, toASCII, toUnicode };
@@ -1,3 +1,4 @@
"use strict";
/*
Copied from https://github.com/mathiasbynens/punycode.js/blob/ef3505c8abb5143a00d53ce59077c9f7f4b2ac47/punycode.js
@@ -24,13 +25,11 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/* eslint callback-return: 0, no-bitwise: 0, eqeqeq: 0, prefer-arrow-callback: 0, object-shorthand: 0 */
'use strict';
/* eslint callback-return: 0, no-bitwise: 0, eqeqeq: 0, prefer-arrow-callback: 0 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.toUnicode = exports.toASCII = exports.encode = exports.decode = exports.ucs2 = exports.version = void 0;
/** Highest positive signed 32-bit float value */
const maxInt = 2147483647; // aka. 0x7FFFFFFF or 2^31-1
/** Bootstring parameters */
const base = 36;
const tMin = 1;
@@ -40,43 +39,37 @@ const damp = 700;
const initialBias = 72;
const initialN = 128; // 0x80
const delimiter = '-'; // '\x2D'
/** Regular expressions */
const regexPunycode = /^xn--/;
const regexNonASCII = /[^\0-\x7F]/; // Note: U+007F DEL is excluded too.
const regexSeparators = /[\x2E\u3002\uFF0E\uFF61]/g; // RFC 3490 separators
/** Error messages */
const errors = {
overflow: 'Overflow: input needs wider integers to process',
'not-basic': 'Illegal input >= 0x80 (not a basic code point)',
'invalid-input': 'Invalid input'
};
/** Convenience shortcuts */
const baseMinusTMin = base - tMin;
const floor = Math.floor;
const stringFromCharCode = String.fromCharCode;
/*--------------------------------------------------------------------------*/
/**
* A generic error utility function.
* @private
* @param {String} type The error type.
* @returns {Error} Throws a `RangeError` with the applicable error message.
* @param type The error type.
* @returns Throws a `RangeError` with the applicable error message.
*/
function error(type) {
throw new RangeError(errors[type]);
}
/**
* A generic `Array#map` utility function.
* @private
* @param {Array} array The array to iterate over.
* @param {Function} callback The function that gets called for every array
* @param array The array to iterate over.
* @param callback The function that gets called for every array
* item.
* @returns {Array} A new array of values returned by the callback function.
* @returns A new array of values returned by the callback function.
*/
function map(array, callback) {
const result = [];
@@ -86,15 +79,14 @@ function map(array, callback) {
}
return result;
}
/**
* A simple `Array#map`-like wrapper to work with domain name strings or email
* addresses.
* @private
* @param {String} domain The domain name or email address.
* @param {Function} callback The function that gets called for every
* @param domain The domain name or email address.
* @param callback The function that gets called for every
* character.
* @returns {String} A new string of characters returned by the callback
* @returns A new string of characters returned by the callback
* function.
*/
function mapDomain(domain, callback) {
@@ -112,7 +104,6 @@ function mapDomain(domain, callback) {
const encoded = map(labels, callback).join('.');
return result + encoded;
}
/**
* Creates an array containing the numeric code points of each Unicode
* character in the string. While JavaScript uses UCS-2 internally,
@@ -123,8 +114,8 @@ function mapDomain(domain, callback) {
* @see <https://mathiasbynens.be/notes/javascript-encoding>
* @memberOf punycode.ucs2
* @name decode
* @param {String} string The Unicode input string (UCS-2).
* @returns {Array} The new array of code points.
* @param string The Unicode input string (UCS-2).
* @returns The new array of code points.
*/
function ucs2decode(string) {
const output = [];
@@ -138,35 +129,35 @@ function ucs2decode(string) {
if ((extra & 0xfc00) == 0xdc00) {
// Low surrogate.
output.push(((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000);
} else {
}
else {
// It's an unmatched surrogate; only append this code unit, in case the
// next code unit is the high surrogate of a surrogate pair.
output.push(value);
counter--;
}
} else {
}
else {
output.push(value);
}
}
return output;
}
/**
* Creates a string based on an array of numeric code points.
* @see `punycode.ucs2.decode`
* @memberOf punycode.ucs2
* @name encode
* @param {Array} codePoints The array of numeric code points.
* @returns {String} The new Unicode string (UCS-2).
* @param codePoints The array of numeric code points.
* @returns The new Unicode string (UCS-2).
*/
const ucs2encode = codePoints => String.fromCodePoint(...codePoints);
const ucs2encode = (codePoints) => String.fromCodePoint(...codePoints);
/**
* Converts a basic code point into a digit/integer.
* @see `digitToBasic()`
* @private
* @param {Number} codePoint The basic numeric code point value.
* @returns {Number} The numeric value of a basic code point (for use in
* @param codePoint The basic numeric code point value.
* @returns The numeric value of a basic code point (for use in
* representing integers) in the range `0` to `base - 1`, or `base` if
* the code point does not represent a value.
*/
@@ -182,13 +173,13 @@ const basicToDigit = function (codePoint) {
}
return base;
};
/**
* Converts a digit/integer into a basic code point.
* @see `basicToDigit()`
* @private
* @param {Number} digit The numeric value of a basic code point.
* @returns {Number} The basic code point whose value (when used for
* @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
* used; else, the lowercase form is used. The behavior is undefined
@@ -197,9 +188,8 @@ const basicToDigit = function (codePoint) {
const digitToBasic = function (digit, flag) {
// 0..25 map to ASCII a..z or A..Z
// 26..35 map to ASCII 0..9
return digit + 22 + 75 * (digit < 26) - ((flag != 0) << 5);
return digit + 22 + 75 * Number(digit < 26) - (Number(flag != 0) << 5);
};
/**
* Bias adaptation function as per section 3.4 of RFC 3492.
* https://tools.ietf.org/html/rfc3492#section-3.4
@@ -214,13 +204,12 @@ const adapt = function (delta, numPoints, firstTime) {
}
return floor(k + ((baseMinusTMin + 1) * delta) / (delta + skew));
};
/**
* Converts a Punycode string of ASCII-only symbols to a string of Unicode
* symbols.
* @memberOf punycode
* @param {String} input The Punycode string of ASCII-only symbols.
* @returns {String} The resulting string of Unicode symbols.
* @param input The Punycode string of ASCII-only symbols.
* @returns The resulting string of Unicode symbols.
*/
const decode = function (input) {
// Don't use UCS-2.
@@ -229,16 +218,13 @@ const decode = function (input) {
let i = 0;
let n = initialN;
let bias = initialBias;
// Handle the basic code points: let `basic` be the number of input code
// points before the last delimiter, or `0` if there is none, then copy
// the first basic code points to the output.
let basic = input.lastIndexOf(delimiter);
if (basic < 0) {
basic = 0;
}
for (let j = 0; j < basic; ++j) {
// if it's not a basic code point
if (input.charCodeAt(j) >= 0x80) {
@@ -246,10 +232,8 @@ const decode = function (input) {
}
output.push(input.charCodeAt(j));
}
// Main decoding loop: start just after the last delimiter if any basic code
// points were copied; start at the beginning otherwise.
for (let index = basic > 0 ? basic + 1 : 0; index < inputLength /* no final expression */;) {
// `index` is the index of the next character to be consumed.
// Decode a generalized variable-length integer into `delta`,
@@ -257,122 +241,100 @@ const decode = function (input) {
// if we increase `i` as we go, then subtract off its starting
// value at the end to obtain `delta`.
const oldi = i;
for (let w = 1, k = base /* no condition */; ; k += base) {
for (let w = 1, k = base /* no condition */;; k += base) {
if (index >= inputLength) {
error('invalid-input');
}
const digit = basicToDigit(input.charCodeAt(index++));
if (digit >= base) {
error('invalid-input');
}
if (digit > floor((maxInt - i) / w)) {
error('overflow');
}
i += digit * w;
const t = k <= bias ? tMin : k >= bias + tMax ? tMax : k - bias;
if (digit < t) {
break;
}
const baseMinusT = base - t;
if (w > floor(maxInt / baseMinusT)) {
error('overflow');
}
w *= baseMinusT;
}
const out = output.length + 1;
bias = adapt(i - oldi, out, oldi == 0);
// `i` was supposed to wrap around from `out` to `0`,
// incrementing `n` each time, so we'll fix that now:
if (floor(i / out) > maxInt - n) {
error('overflow');
}
n += floor(i / out);
i %= out;
// Insert `n` at position `i` of the output.
output.splice(i++, 0, n);
}
return String.fromCodePoint(...output);
};
exports.decode = decode;
/**
* Converts a string of Unicode symbols (e.g. a domain name label) to a
* Punycode string of ASCII-only symbols.
* @memberOf punycode
* @param {String} input The string of Unicode symbols.
* @returns {String} The resulting Punycode string of ASCII-only symbols.
* @param input The string of Unicode symbols.
* @returns The resulting Punycode string of ASCII-only symbols.
*/
const encode = function (input) {
const output = [];
// Convert the input in UCS-2 to an array of Unicode code points.
input = ucs2decode(input);
const codePoints = ucs2decode(input);
// Cache the length.
const inputLength = input.length;
const inputLength = codePoints.length;
// Initialize the state.
let n = initialN;
let delta = 0;
let bias = initialBias;
// Handle the basic code points.
for (const currentValue of input) {
for (const currentValue of codePoints) {
if (currentValue < 0x80) {
output.push(stringFromCharCode(currentValue));
}
}
const basicLength = output.length;
let handledCPCount = basicLength;
// `handledCPCount` is the number of code points that have been handled;
// `basicLength` is the number of basic code points.
// Finish the basic string with a delimiter unless it's empty.
if (basicLength) {
output.push(delimiter);
}
// Main encoding loop:
while (handledCPCount < inputLength) {
// All non-basic code points < n have been handled already. Find the next
// larger one:
let m = maxInt;
for (const currentValue of input) {
for (const currentValue of codePoints) {
if (currentValue >= n && currentValue < m) {
m = currentValue;
}
}
// Increase `delta` enough to advance the decoder's <n,i> state to <m,0>,
// but guard against overflow.
const handledCPCountPlusOne = handledCPCount + 1;
if (m - n > floor((maxInt - delta) / handledCPCountPlusOne)) {
error('overflow');
}
delta += (m - n) * handledCPCountPlusOne;
n = m;
for (const currentValue of input) {
for (const currentValue of codePoints) {
if (currentValue < n && ++delta > maxInt) {
error('overflow');
}
if (currentValue === n) {
// Represent delta as a generalized variable-length integer.
let q = delta;
for (let k = base /* no condition */; ; k += base) {
for (let k = base /* no condition */;; k += base) {
const t = k <= bias ? tMin : k >= bias + tMax ? tMax : k - bias;
if (q < t) {
break;
@@ -382,29 +344,27 @@ const encode = function (input) {
output.push(stringFromCharCode(digitToBasic(t + (qMinusT % baseMinusT), 0)));
q = floor(qMinusT / baseMinusT);
}
output.push(stringFromCharCode(digitToBasic(q, 0)));
bias = adapt(delta, handledCPCountPlusOne, handledCPCount === basicLength);
delta = 0;
++handledCPCount;
}
}
++delta;
++n;
}
return output.join('');
};
exports.encode = encode;
/**
* Converts a Punycode string representing a domain name or an email address
* to Unicode. Only the Punycoded parts of the input will be converted, i.e.
* it doesn't matter if you call it on a string that has already been
* converted to Unicode.
* @memberOf punycode
* @param {String} input The Punycoded domain name or email address to
* @param input The Punycoded domain name or email address to
* convert to Unicode.
* @returns {String} The Unicode representation of the given Punycode
* @returns The Unicode representation of the given Punycode
* string.
*/
const toUnicode = function (input) {
@@ -412,16 +372,16 @@ const toUnicode = function (input) {
return regexPunycode.test(string) ? decode(string.slice(4).toLowerCase()) : string;
});
};
exports.toUnicode = toUnicode;
/**
* Converts a Unicode string representing a domain name or an email address to
* Punycode. Only the non-ASCII parts of the domain name will be converted,
* i.e. it doesn't matter if you call it with a domain that's already in
* ASCII.
* @memberOf punycode
* @param {String} input The domain name or email address to convert, as a
* @param input The domain name or email address to convert, as a
* Unicode string.
* @returns {String} The Punycode representation of the given domain name or
* @returns The Punycode representation of the given domain name or
* email address.
*/
const toASCII = function (input) {
@@ -429,32 +389,23 @@ const toASCII = function (input) {
return regexNonASCII.test(string) ? 'xn--' + encode(string) : string;
});
};
exports.toASCII = toASCII;
/*--------------------------------------------------------------------------*/
/** Define the public API */
const punycode = {
/**
* A string representing the current Punycode.js version number.
* @memberOf punycode
* @type String
*/
version: '2.3.1',
/**
* An object of methods to convert from JavaScript's internal character
* representation (UCS-2) to Unicode code points, and back.
* @see <https://mathiasbynens.be/notes/javascript-encoding>
* @memberOf punycode
* @type Object
*/
ucs2: {
decode: ucs2decode,
encode: ucs2encode
},
decode: decode,
encode: encode,
toASCII: toASCII,
toUnicode: toUnicode
/**
* A string representing the current Punycode.js version number.
* @memberOf punycode
* @type String
*/
exports.version = '2.3.1';
/**
* An object of methods to convert from JavaScript's internal character
* representation (UCS-2) to Unicode code points, and back.
* @see <https://mathiasbynens.be/notes/javascript-encoding>
* @memberOf punycode
* @type Object
*/
exports.ucs2 = {
decode: ucs2decode,
encode: ucs2encode
};
module.exports = punycode;
+34
View File
@@ -0,0 +1,34 @@
import { Transform } from 'node:stream';
export declare function encode(buffer: Buffer | string): string;
/**
* Adds soft line breaks to a Quoted-Printable string
*
* @param str Quoted-Printable encoded string that might need line wrapping
* @param [lineLength=76] Maximum allowed length for a line
* @returns Soft-wrapped Quoted-Printable encoded string
*/
export declare function wrap(str: string, lineLength?: number): string;
/**
* Options for the Quoted-Printable encoder stream
*/
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
*
* @constructor
* @param options Stream options
* @param [options.lineLength=76] Maximum length for lines, set to false to disable wrapping
*/
export declare class Encoder extends Transform {
options: QPEncoderOptions;
inputBytes: number;
outputBytes: number;
constructor(options?: QPEncoderOptions);
}
+277
View File
@@ -0,0 +1,277 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Encoder = void 0;
exports.encode = encode;
exports.wrap = wrap;
const node_stream_1 = require("node:stream");
/**
* Encodes a Buffer into a Quoted-Printable encoded string
*
* @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 = [
[0x09], // <TAB>
[0x0a], // <LF>
[0x0d], // <CR>
[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) {
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 (QP_LITERAL[ord] &&
lineBreakByte &&
!(isWhitespace(ord) && (following === undefined || following === 0x0a || following === 0x0d))) {
output[pos++] = ord;
continue;
}
output[pos++] = 0x3d; // =
output[pos++] = HEX_DIGITS[Math.floor(ord / 16)];
output[pos++] = HEX_DIGITS[ord % 16];
}
return output.toString('latin1', 0, pos);
}
/**
* Adds soft line breaks to a Quoted-Printable string
*
* @param str Quoted-Printable encoded string that might need line wrapping
* @param [lineLength=76] Maximum allowed length for a line
* @returns Soft-wrapped Quoted-Printable encoded string
*/
function wrap(str, lineLength) {
str = (str || '').toString();
// 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;
}
let pos = 0;
const len = str.length;
let match, code, line;
const lineMargin = Math.floor(lineLength / 3);
let result = '';
// insert soft linebreaks where needed
while (pos < len) {
line = str.substr(pos, lineLength);
if ((match = line.match(/\r\n/))) {
line = line.substr(0, match.index + match[0].length);
result += line;
pos += line.length;
continue;
}
if (line.substr(-1) === '\n') {
result += line;
pos += line.length;
continue;
}
if ((match = line.substr(-lineMargin).match(/\n.*?$/))) {
// truncate to nearest line break
line = line.substr(0, line.length - (match[0].length - 1));
result += line;
pos += line.length;
continue;
}
if (line.length > lineLength - lineMargin && (match = line.substr(-lineMargin).match(/[ \t.,!?][^ \t.,!?]*$/))) {
// truncate to nearest space
line = line.substr(0, line.length - (match[0].length - 1));
}
else if (line.match(/[=][\da-f]{0,2}$/i)) {
// push incomplete encoding sequences to the next line
if ((match = line.match(/[=][\da-f]{0,1}$/i))) {
line = line.substr(0, line.length - match[0].length);
}
// ensure that utf-8 sequences are not split
while (line.length > 3 &&
line.length < len - pos &&
!line.match(/^(?:=[\da-f]{2}){1,4}$/i) &&
(match = line.match(/[=][\da-f]{2}$/gi))) {
code = parseInt(match[0].substr(1, 2), 16);
if (code < 128) {
break;
}
line = line.substr(0, line.length - 3);
if (code >= 0xc0) {
break;
}
}
}
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);
}
else if (line.length === lineLength) {
line = line.substr(0, line.length - 1);
}
pos += line.length;
line += '=\r\n';
}
else {
pos += line.length;
}
result += line;
}
return result;
}
/**
* Helper function to check if a number is inside provided ranges
*
* @param nr Number to check for
* @param ranges An Array of allowed values
* @returns True if the value was found inside allowed ranges, false otherwise
*/
function checkRanges(nr, ranges) {
for (let i = ranges.length - 1; i >= 0; i--) {
const range = ranges[i];
if (!range.length) {
continue;
}
if (range.length === 1 && nr === range[0]) {
return true;
}
if (range.length === 2 && nr >= range[0] && nr <= range[1]) {
return true;
}
}
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
*
* @constructor
* @param options Stream options
* @param [options.lineLength=76] Maximum length for lines, set to false to disable wrapping
*/
class Encoder extends node_stream_1.Transform {
constructor(options) {
super();
this.options = options || {};
if (this.options.lineLength !== false) {
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 buf = encoding !== 'buffer' ? Buffer.from(chunk, encoding) : chunk;
if (!buf || !buf.length) {
return done();
}
this.inputBytes += buf.length;
if (this._remainingBytes) {
buf = Buffer.concat([this._remainingBytes, buf], this._remainingBytes.length + buf.length);
this._remainingBytes = false;
}
// 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;
+69
View File
@@ -0,0 +1,69 @@
import type { Logger } from '../shared/index.js';
import type { MimeNodeEnvelope } from '../mime-node/index.js';
import type MailMessage from '../mailer/mail-message.js';
import type { default as Mail, SentMessageInfo, SendMailOptions, TransportOptions } from '../mailer/index.js';
/**
* Options for the Sendmail transport
*/
export interface SendmailTransportOptions extends TransportOptions {
/** Selects this transport in createTransport, the binary itself is set with `path` */
sendmail?: boolean | string | undefined;
/** Path to the sendmail binary, defaults to 'sendmail' */
path?: string | undefined;
/** Either 'windows' or 'unix', the line ending of the message piped to sendmail */
newline?: string | undefined;
/** Arguments for the sendmail binary, replaces the default '-f <sender>' */
args?: string[] | undefined;
}
/**
* The value the Sendmail transport hands to the send callback
*/
export interface SendmailSentMessageInfo extends SentMessageInfo {
/** The envelope the message was sent with */
envelope: MimeNodeEnvelope;
/** Message-ID value of the message */
messageId: string;
/** Always 'Messages queued for delivery' */
response: string;
}
/**
* Generates a Transport object for Sendmail
*
* Possible options can be the following:
*
* * **path** optional path to sendmail binary
* * **newline** either 'windows' or 'unix'
* * **args** an array of arguments for the sendmail binary
*
* @constructor
* @param optional config parameter for Sendmail
*/
declare class SendmailTransport {
mailer: Mail<SendmailSentMessageInfo>;
options: SendmailTransportOptions;
name: string;
version: string;
path: string;
args: string[] | false;
logger: Logger;
winbreak: boolean;
constructor(options?: SendmailTransportOptions | string);
/**
* <p>Compiles a mailcomposer message and forwards it to handler that sends it.</p>
*
* @param mail MailComposer object
* @param done Callback function to run when the sending is completed
*/
send(mail: MailMessage<SendmailSentMessageInfo>, done: (err: Error | null, info?: SendmailSentMessageInfo) => void): void;
}
/**
* Type aliases in the layout of @types/nodemailer, so `SendmailTransport.Options` style references keep working
*/
declare namespace SendmailTransport {
type Options = SendmailTransportOptions;
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;
+250
View File
@@ -0,0 +1,250 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const 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
*
* Possible options can be the following:
*
* * **path** optional path to sendmail binary
* * **newline** either 'windows' or 'unix'
* * **args** an array of arguments for the sendmail binary
*
* @constructor
* @param optional config parameter for Sendmail
*/
class SendmailTransport {
constructor(options) {
options = options || {};
// use a reference to spawn for mocking purposes
this._spawn = node_child_process_1.spawn;
this.options = options;
this.name = 'Sendmail';
this.version = packageData.version;
this.path = 'sendmail';
this.args = false;
this.logger = shared.getLogger(this.options, {
component: this.options.component || 'sendmail'
});
if (typeof options === 'string') {
this.path = options;
}
else if (typeof options === 'object') {
if (options.path) {
this.path = options.path;
}
if (Array.isArray(options.args)) {
this.args = options.args;
}
}
this.winbreak = ['win', 'windows', 'dos', '\r\n'].includes((options.newline || '').toString().toLowerCase());
}
/**
* <p>Compiles a mailcomposer message and forwards it to handler that sends it.</p>
*
* @param mail MailComposer object
* @param done Callback function to run when the sending is completed
*/
send(mail, done) {
// Sendmail strips this header line by itself. send() runs after the message was
// compiled, so mail.message is set
mail.message.keepBcc = true;
const envelope = mail.message.getEnvelope();
const messageId = mail.message.messageId();
let returned;
const hasInvalidAddresses = []
.concat(envelope.from || [])
.concat(envelope.to || [])
// a local part is either a dot-atom or a quoted-string, so a leading dash sits at
// offset 0 or, behind the opening quote, at offset 1. Only the first shape is read
// as an option by sendmail, but both are the address this guard keeps out of argv
.some(addr => /^"?-/.test(addr));
if (hasInvalidAddresses) {
const err = new Error('Can not send mail. Invalid envelope addresses.');
err.code = errors.ESENDMAIL;
return done(err);
}
// force -i to keep single dots
const args = this.args
? ['-i'].concat(this.args).concat(envelope.to)
: ['-i'].concat(envelope.from ? ['-f', envelope.from] : []).concat(envelope.to);
const callback = (err) => {
if (returned) {
// ignore any additional responses, already done
return;
}
returned = true;
if (typeof done === 'function') {
if (err) {
return done(err);
}
return done(null, {
envelope,
messageId,
response: 'Messages queued for delivery'
});
}
};
let sendmail;
try {
sendmail = this._spawn(this.path, args);
}
catch (E) {
this.logger.error({
err: E,
tnx: 'spawn',
messageId
}, 'Error occurred while spawning sendmail. %s', E.message);
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);
fail(err);
});
// '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();
}
err.code = err.code || errors.ESENDMAIL;
this.logger.error({
err,
tnx: 'stdin',
messageId
}, 'Error sending message %s to sendmail. %s', messageId, err.message);
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);
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) {
recipients.push('...and ' + recipients.splice(2).length + ' more');
}
this.logger.info({
tnx: 'send',
messageId
}, 'Sending message %s to <%s>', messageId, recipients.join(', '));
const sourceStream = mail.message.createReadStream();
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 = (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({
err,
tnx: 'stdin',
messageId
}, 'Error occurred when generating message %s. %s', messageId, err.message);
sendmail.kill('SIGINT'); // do not deliver the message
callback(err);
});
stream.pipe(sendmail.stdin);
}
else {
const err = new Error('sendmail was not found');
err.code = errors.ESENDMAIL;
return callback(err);
}
}
}
exports.default = SendmailTransport;
module.exports = exports.default;
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
+112
View File
@@ -0,0 +1,112 @@
import EventEmitter from 'node:events';
import type { Logger } from '../shared/index.js';
import { type MimeNodeEnvelope } from '../mime-node/index.js';
import type MailMessage from '../mailer/mail-message.js';
import type { default as Mail, SentMessageInfo, SendMailOptions, TransportOptions, VerifyCallback } from '../mailer/index.js';
/**
* Options for the SES transport
*/
export interface SESTransportOptions extends TransportOptions {
/** The AWS SDK v3 objects to send with, `{ sesClient, SendEmailCommand }` from @aws-sdk/client-sesv2 */
SES: {
/** SESv2Client instance. Its config.region provider is resolved for the domain of the returned Message-ID */
sesClient: {
config?: {
region?: (() => Promise<string>) | undefined;
[key: string]: any;
} | undefined;
send(command: unknown): Promise<any>;
};
/** SendEmailCommand class, constructed with the SendEmailCommandInput of every message */
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
*/
export interface SESSentMessageInfo extends SentMessageInfo {
/** The envelope the message was sent with */
envelope: MimeNodeEnvelope;
/** Message-ID built from the MessageId SES returned */
messageId: string;
/** The MessageId SES returned */
response: string;
/** The raw RFC822 message that was sent */
raw: Buffer;
}
/**
* Generates a Transport object for AWS SES
*
* @constructor
* @param optional config parameter
*/
declare class SESTransport extends EventEmitter {
mailer: Mail<SESSentMessageInfo>;
options: SESTransportOptions;
ses: SESTransportOptions['SES'];
name: string;
version: string;
logger: Logger;
constructor(options: SESTransportOptions);
getRegion(cb: (err: Error | null, region?: string | false) => void): void;
/**
* Compiles a mailcomposer message and forwards it to SES
*
* @param mail MailComposer object
* @param callback Callback function to run when the sending is completed
*/
send(mail: MailMessage<SESSentMessageInfo>, callback: (err: Error | null, info?: SESSentMessageInfo) => void): void;
/**
* Verifies SES configuration
*
* @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;
}
/**
* Type aliases in the layout of @types/nodemailer, so `SESTransport.Options` style references keep working
*/
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;
+252
View File
@@ -0,0 +1,252 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const node_events_1 = __importDefault(require("node:events"));
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 index_js_1 = __importDefault(require("../mime-node/index.js"));
/**
* Tags AWS SDK rejections that carry no `code` property (SDK v3 errors only
* have a `name`) with the generic SES transport error code, keeping the
* original error object intact
*/
function tagSesError(err) {
if (err && typeof err === 'object' && !err.code) {
err.code = errors.ESES;
}
return err;
}
/**
* Generates a Transport object for AWS SES
*
* @constructor
* @param optional config parameter
*/
class SESTransport extends node_events_1.default {
constructor(options) {
super();
if (!options || !options.SES || !options.SES.sesClient) {
const error = new Error('Missing SES configuration, expecting { sesClient, SendEmailCommand } from @aws-sdk/client-sesv2, see https://nodemailer.com/transports/ses/');
error.code = errors.ECONFIG;
throw error;
}
this.options = options;
this.ses = this.options.SES;
this.name = 'SESTransport';
this.version = packageData.version;
this.logger = shared.getLogger(this.options, {
component: this.options.component || 'ses-transport'
});
}
getRegion(cb) {
if (this.ses.sesClient.config && typeof this.ses.sesClient.config.region === 'function') {
// Resolve the region provider. Use the two-argument form of then() so that a
// synchronous throw from cb is not recaught here and used to invoke cb a second time.
this.ses.sesClient.config.region().then(region => cb(null, region), err => cb(err));
return;
}
return cb(null, false);
}
/**
* Compiles a mailcomposer message and forwards it to SES
*
* @param mail MailComposer object
* @param callback Callback function to run when the sending is completed
*/
send(mail, callback) {
// send() runs after the message was compiled, so mail.message is set
let fromHeader = mail.message._headers.find(header => /^from$/i.test(header.key));
if (fromHeader) {
const mimeNode = new index_js_1.default('text/plain');
fromHeader = mimeNode._convertAddresses(mimeNode._parseAddresses(fromHeader.value));
}
const envelope = mail.message.getEnvelope();
const messageId = mail.message.messageId();
const recipients = [].concat(envelope.to || []);
if (recipients.length > 3) {
recipients.push('...and ' + recipients.splice(2).length + ' more');
}
this.logger.info({
tnx: 'send',
messageId
}, 'Sending message %s to <%s>', messageId, recipients.join(', '));
const getRawMessage = (next) => {
// do not use Message-ID and Date in DKIM signature
if (!mail.data._dkim) {
mail.data._dkim = {};
}
if (mail.data._dkim.skipFields && typeof mail.data._dkim.skipFields === 'string') {
mail.data._dkim.skipFields += ':date:message-id';
}
else {
mail.data._dkim.skipFields = 'date:message-id';
}
const sourceStream = mail.message.createReadStream();
const stream = sourceStream.pipe(new le_windows_js_1.default());
const chunks = [];
let chunklen = 0;
stream.on('readable', () => {
let chunk;
while ((chunk = stream.read()) !== null) {
chunks.push(chunk);
chunklen += chunk.length;
}
});
sourceStream.once('error', err => stream.emit('error', err));
stream.once('error', err => next(err));
stream.once('end', () => next(null, Buffer.concat(chunks, chunklen)));
};
setImmediate(() => getRawMessage((err, raw) => {
if (err) {
this.logger.error({
err,
tnx: 'send',
messageId
}, 'Failed creating message for %s. %s', messageId, err.message);
return callback(err);
}
// mail.data.ses is caller supplied message data, so copy its own keys only
const sesMessage = shared.copyOwnKeys({
Content: {
Raw: {
// required
Data: raw // required
}
},
FromEmailAddress: fromHeader || envelope.from,
Destination: {
ToAddresses: envelope.to
}
}, mail.data.ses);
this.getRegion((err, region) => {
if (err || !region) {
region = 'us-east-1';
}
let sendPromise;
try {
// command construction or dispatch can throw synchronously on a
// misconfigured SDK; surface it as a single error callback instead
// of letting it escape into getRegion's promise chain
const command = new this.ses.SendEmailCommand(sesMessage);
sendPromise = this.ses.sesClient.send(command);
}
catch (err) {
tagSesError(err);
this.logger.error({
err,
tnx: 'send'
}, 'Send error for %s: %s', messageId, err.message);
setImmediate(() => callback(err));
return;
}
sendPromise
.then(data => {
if (region === 'us-east-1') {
region = 'email';
}
const info = {
envelope: {
from: envelope.from,
to: envelope.to
},
messageId: '<' + data.MessageId + (!/@/.test(data.MessageId) ? '@' + region + '.amazonses.com' : '') + '>',
response: data.MessageId,
raw: raw
};
// invoke the callback outside the promise chain so a throw from it
// is not recaught by .catch() and used to call it a second time
setImmediate(() => callback(null, info));
})
.catch(err => {
tagSesError(err);
this.logger.error({
err,
tnx: 'send'
}, 'Send error for %s: %s', messageId, err.message);
setImmediate(() => callback(err));
});
});
}));
}
verify(callback) {
let promise;
if (!callback) {
promise = new Promise((resolve, reject) => {
callback = shared.callbackPromise(resolve, reject);
});
}
const done = callback;
const cb = (err) => {
if (err && !['InvalidParameterValue', 'MessageRejected'].includes(err.code || err.Code || err.name)) {
return done(tagSesError(err));
}
return done(null, true);
};
const sesMessage = {
Content: {
Raw: {
Data: Buffer.from('From: <invalid@invalid>\r\nTo: <invalid@invalid>\r\n Subject: Invalid\r\n\r\nInvalid')
}
},
FromEmailAddress: 'invalid@invalid',
Destination: {
ToAddresses: ['invalid@invalid']
}
};
// the region value is not used for anything when verifying, but the lookup
// exercises the client configuration the same way as send() does
this.getRegion(() => {
let sendPromise;
try {
const command = new this.ses.SendEmailCommand(sesMessage);
sendPromise = this.ses.sesClient.send(command);
}
catch (err) {
setImmediate(() => cb(err));
return;
}
sendPromise.then(() => setImmediate(() => cb(null))).catch(err => setImmediate(() => cb(err)));
});
return promise;
}
}
exports.default = SESTransport;
module.exports = exports.default;
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
+291
View File
@@ -0,0 +1,291 @@
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 { OutgoingHttpHeaders } from 'node:http';
export { isProtoKey, copyOwnKeys };
/**
* Options for resolveHostname. The object is also handed to dns.Resolver, so its
* `timeout` and `tries` settings apply to the lookups
*/
export interface ResolveHostnameOptions {
/** Hostname or IP address to resolve */
host?: string | undefined;
/** Server name for TLS, used as the host when no host is set */
servername?: string | undefined;
/** Count loopback interfaces when checking which address families are usable */
allowInternalNetworkInterfaces?: boolean | undefined;
/** How long a resolved value stays cached, in milliseconds (default 5 minutes) */
dnsTtl?: number | undefined;
/** Query timeout in milliseconds, passed to dns.Resolver */
timeout?: number | undefined;
/** Number of query attempts, passed to dns.Resolver */
tries?: number | undefined;
}
/**
* Resolved value handed to the resolveHostname callback
*/
export interface ResolvedHostname {
/** Server name to use for TLS, false when an IP literal was given without one */
servername?: string | false | undefined;
/** Address to connect to, picked at random from the resolved addresses */
host?: string | null | undefined;
/** Whether the value came from the DNS cache */
cached?: boolean | undefined;
/** 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
*/
export interface DnsCacheValue {
addresses: string[];
}
/**
* A DNS cache entry
*/
export interface DnsCacheEntry {
value: DnsCacheValue;
/** Expiration time as a timestamp, entries without one never expire */
expires?: number | undefined;
}
/**
* Configuration object parsed from a connection url. Query parameters become
* top level keys, `tls.*` parameters go into `tls`
*/
export interface ConnectionUrlOptions {
secure?: boolean | undefined;
direct?: boolean | undefined;
port?: number | undefined;
host?: string | undefined;
/** Well-known service name from the ?service= query parameter */
service?: string | undefined;
auth?: {
user: string;
pass: string;
} | undefined;
tls?: {
[key: string]: unknown;
} | undefined;
[key: string]: unknown;
}
/**
* Structured data attached to a log line. tnx, sid and cid drive the line prefix
* of the default console logger
*/
export interface LogEntry {
/** 'server' or 'client' for SMTP transaction lines */
tnx?: string | undefined;
/** Session id */
sid?: string | undefined;
/** Connection id */
cid?: string | number | undefined;
level?: string | undefined;
[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
*/
export interface ExternalLogger {
trace?(...args: any[]): any;
debug?(...args: any[]): any;
info?(...args: any[]): any;
warn?(...args: any[]): any;
error?(...args: any[]): any;
fatal?(...args: any[]): any;
log?(...args: any[]): any;
[level: string]: any;
}
/**
* Options for getLogger
*/
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: LogMethod;
debug: LogMethod;
info: LogMethod;
warn: LogMethod;
error: LogMethod;
fatal: LogMethod;
}
/**
* A parsed data URI
*/
export interface ParsedDataURI {
/** Decoded payload */
data: Buffer;
/** 'base64', 'utf8' or 'utf-8' when the URI declared one, null otherwise */
encoding: string | null;
contentType: string;
/** Further `key=value` parameters from the metadata section */
params: {
[key: string]: string;
};
}
/**
* Access policy for resolveContent
*/
export interface ResolveContentOptions {
/** Reject content that points to a file path */
disableFileAccess?: boolean | undefined;
/** Reject content that points to a URL */
disableUrlAccess?: boolean | undefined;
}
/**
* An object value resolveContent understands. A plain string or Buffer value is returned as
* is and a readable stream is read into a Buffer, an object carries the content in `content`
* or points to it with `path` or `href`
*/
export interface ContentDescriptor {
/** The content itself, a string, a Buffer or a readable stream */
content?: string | Buffer | Readable | undefined;
/** Encoding of a string `content`, it is decoded into a Buffer unless it is utf8 or ascii */
encoding?: string | undefined;
/** File path, http(s) URL or data URI to read the content from */
path?: string | undefined;
/** URL to fetch the content from */
href?: string | undefined;
/** Request headers for a URL fetch */
httpHeaders?: OutgoingHttpHeaders | undefined;
/** TLS settings for a URL fetch, see nmfetch */
tls?: {
[key: string]: any;
} | undefined;
}
export type ResolveContentCallback = (err: Error | null, value?: any) => void;
export declare let networkInterfaces: NodeJS.Dict<os.NetworkInterfaceInfo[]> | undefined;
export declare const dnsCache: Map<string, DnsCacheEntry>;
export declare const resolveHostname: (options: ResolveHostnameOptions | undefined, callback: (err: Error | null, result?: ResolvedHostname) => void) => void;
/**
* Parses connection url to a structured configuration object
*
* @param str Connection url
* @return Configuration object
*/
export declare const parseConnectionUrl: (str?: string | null) => ConnectionUrlOptions;
/**
* Returns a bunyan-compatible logger interface. Uses either provided logger or
* 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;
/**
* Wrapper for creating a callback that either resolves or rejects a promise
* based on input
*
* @param resolve Function to run if callback is called
* @param reject Function to run if callback ends with an error
*/
export declare const callbackPromise: (resolve: (...args: any[]) => void, reject: (reason?: any) => void) => (...args: any[]) => void;
export declare const parseDataURI: (uri: unknown) => ParsedDataURI | null;
/**
* Resolves a String or a Buffer value for content value. Useful if the value
* is a Stream or a file or an URL. If the value is a Stream, overwrites
* the stream object with the resolved value (you can't stream a value twice).
*
* This is useful when you want to create a plugin that needs a content value,
* for example the `html` or `text` value as a String or a Buffer but not as
* a file path or an URL.
*
* @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 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;
/**
* Copies properties from source objects to target objects
*/
export declare const assign: (...args: ({
[key: string]: any;
} | false | null | undefined)[]) => {
[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;
+777
View File
@@ -0,0 +1,777 @@
"use strict";
/* eslint no-console: 0 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
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"));
const index_js_1 = __importDefault(require("../fetch/index.js"));
const errors = __importStar(require("../errors.js"));
const objects_js_1 = require("./objects.js");
Object.defineProperty(exports, "isProtoKey", { enumerable: true, get: function () { return objects_js_1.isProtoKey; } });
Object.defineProperty(exports, "copyOwnKeys", { enumerable: true, get: function () { return objects_js_1.copyOwnKeys; } });
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
let lastCacheCleanup = 0;
/** @internal */
const _lastCacheCleanup = () => lastCacheCleanup;
exports._lastCacheCleanup = _lastCacheCleanup;
/** @internal */
const _resetCacheCleanup = () => {
lastCacheCleanup = 0;
};
exports._resetCacheCleanup = _resetCacheCleanup;
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((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
return true;
}
return addresses.filter(i => !i.internal || allowInternal).some(i => i.family === 'IPv' + family || i.family === family);
};
const resolve = (family, hostname, options, callback) => {
options = options || {};
if (!isFamilySupported(family, options.allowInternalNetworkInterfaces)) {
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) => {
if (err) {
switch (err.code) {
case node_dns_1.default.NODATA:
case node_dns_1.default.NOTFOUND:
case node_dns_1.default.NOTIMP:
case node_dns_1.default.SERVFAIL:
case node_dns_1.default.CONNREFUSED:
case node_dns_1.default.REFUSED:
case 'EAI_AGAIN':
return callback(null, []);
}
return callback(err);
}
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) => {
if (!value) {
return Object.assign({}, extra || {});
}
const addresses = value.addresses || [];
// Select a random address from available addresses, or null if none
const host = addresses.length > 0 ? addresses[Math.floor(Math.random() * addresses.length)] : null;
return Object.assign({
host,
// Include all addresses for connection fallback support
_addresses: addresses
}, extra || {});
};
const resolveHostname = (options, callback) => {
options = options || {};
if (!options.host && options.servername) {
options.host = options.servername;
}
if (!options.host || node_net_1.default.isIP(options.host)) {
// nothing to do here
const value = {
addresses: [options.host]
};
return callback(null, formatDNSValue(value, {
servername: options.servername || false,
cached: false
}));
}
const host = options.host;
// The TLS server name belongs to the connection asking, not to the host it resolves. The
// cache is shared by every transport of the process and keyed by host alone, so a server
// name stored in it would be the one of whichever transport resolved the host first, and a
// later transport with its own tls.servername would present and verify that name instead
const servername = options.servername || host;
let cached;
if (exports.dnsCache.has(options.host)) {
cached = exports.dnsCache.get(options.host);
// Lazy cleanup with time throttling
const now = Date.now();
if (now - lastCacheCleanup > CACHE_CLEANUP_INTERVAL) {
lastCacheCleanup = now;
// Clean up expired entries
for (const [host, entry] of exports.dnsCache.entries()) {
if (entry.expires && entry.expires < now) {
exports.dnsCache.delete(host);
}
}
// If cache is still too large, remove oldest entries
if (exports.dnsCache.size > MAX_CACHE_SIZE) {
const toDelete = Math.floor(MAX_CACHE_SIZE * 0.1); // Remove 10% of entries
const keys = Array.from(exports.dnsCache.keys()).slice(0, toDelete);
keys.forEach(key => exports.dnsCache.delete(key));
}
}
if (!cached.expires || cached.expires >= now) {
return callback(null, formatDNSValue(cached.value, {
servername,
cached: true
}));
}
}
// 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;
let pending = 2;
const onResolved = () => {
if (--pending || responded) {
return;
}
// 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
}));
}
// 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: supportedAddresses.length ? supportedAddresses : [host]
};
exports.dnsCache.set(host, {
value,
expires: Date.now() + (options.dnsTtl || DNS_TTL)
});
return respond(null, formatDNSValue(value, {
servername,
cached: false
}));
});
}
catch (lookupErr) {
if (respondCached(lookupErr)) {
return;
}
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 || [];
}
else {
ipv6Error = err;
ipv6Addresses = addresses || [];
}
onResolved();
});
if (resolver) {
resolvers.push(resolver);
}
}
};
exports.resolveHostname = resolveHostname;
/**
* Parses connection url to a structured configuration object
*
* @param str Connection url
* @return Configuration object
*/
const parseConnectionUrl = (str) => {
str = str || '';
const options = {};
const url = urllib.parse(str, true);
switch (url.protocol) {
case 'smtp:':
options.secure = false;
break;
case 'smtps:':
options.secure = true;
break;
case 'direct:':
options.direct = true;
break;
}
if (!isNaN(url.port) && Number(url.port)) {
options.port = Number(url.port);
}
if (url.hostname) {
options.host = url.hostname;
}
if (url.username || url.password) {
options.auth = {
user: url.username || '',
pass: url.password || ''
};
}
Object.keys(url.query || {}).forEach(key => {
let obj = options;
let lKey = key;
let value = url.query[key];
if (!isNaN(value)) {
value = Number(value);
}
switch (value) {
case 'true':
value = true;
break;
case 'false':
value = false;
break;
}
// tls is nested object
if (key.indexOf('tls.') === 0) {
lKey = key.substr(4);
if (!options.tls) {
options.tls = {};
}
obj = options.tls;
}
else if (key.indexOf('.') >= 0) {
// ignore nested properties besides tls
return;
}
// `in` already keeps "__proto__" out, but only as a side effect of it being an
// Object.prototype member. Say it, so the protection survives a change to the check
if (!(0, objects_js_1.isProtoKey)(lKey) && !(lKey in obj)) {
obj[lKey] = value;
}
});
return options;
};
exports.parseConnectionUrl = parseConnectionUrl;
/** @internal */
const _logFunc = (logger, level, defaults, data, message, ...args) => {
const entry = Object.assign({}, defaults || {}, data || {});
delete entry.level;
let logLevel = level;
if (typeof logger[logLevel] !== 'function') {
// Provided logger does not implement this level. Fall back to a
// lower-severity handler instead of throwing.
logLevel = ['info', 'debug', 'log', 'trace', 'warn', 'error'].find(name => typeof logger[name] === 'function');
}
if (logLevel) {
logger[logLevel](entry, message, ...args);
}
};
exports._logFunc = _logFunc;
/**
* Returns a bunyan-compatible logger interface. Uses either provided logger or
* 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) => {
options = options || {};
const response = {};
const levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
if (!options.logger) {
// use vanity logger
levels.forEach(level => {
response[level] = () => false;
});
return response;
}
const logger = options.logger === true ? createDefaultLogger(levels) : options.logger;
levels.forEach(level => {
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;
};
exports.getLogger = getLogger;
/**
* Wrapper for creating a callback that either resolves or rejects a promise
* based on input
*
* @param resolve Function to run if callback is called
* @param reject Function to run if callback ends with an error
*/
const callbackPromise = (resolve, reject) => function (...args) {
const err = args.shift();
if (err) {
reject(err);
}
else {
resolve(...args);
}
};
exports.callbackPromise = callbackPromise;
const parseDataURI = (uri) => {
if (typeof uri !== 'string') {
return null;
}
// Early return for non-data URIs to avoid unnecessary processing
if (!uri.startsWith('data:')) {
return null;
}
// Find the first comma safely - this prevents ReDoS
const commaPos = uri.indexOf(',');
if (commaPos === -1) {
return null;
}
const data = uri.substring(commaPos + 1);
const metaStr = uri.substring('data:'.length, commaPos);
let encoding;
const metaEntries = metaStr.split(';');
if (metaEntries.length > 0) {
const lastEntry = metaEntries[metaEntries.length - 1].toLowerCase().trim();
// Only recognize valid encoding types to prevent manipulation
if (['base64', 'utf8', 'utf-8'].includes(lastEntry) && lastEntry.indexOf('=') === -1) {
encoding = lastEntry;
metaEntries.pop();
}
}
const contentType = metaEntries.length > 0 ? metaEntries.shift() : 'application/octet-stream';
const params = {};
for (let i = 0; i < metaEntries.length; i++) {
const entry = metaEntries[i];
const sepPos = entry.indexOf('=');
if (sepPos > 0) {
// Ensure there's a key before the '='
const key = entry.substring(0, sepPos).trim();
const value = entry.substring(sepPos + 1).trim();
if (key && !(0, objects_js_1.isProtoKey)(key)) {
params[key] = value;
}
}
}
// Decode data based on encoding with proper error handling
let bufferData;
try {
if (encoding === 'base64') {
bufferData = Buffer.from(data, 'base64');
}
else {
try {
bufferData = Buffer.from(decodeURIComponent(data));
}
catch (_decodeError) {
bufferData = Buffer.from(data);
}
}
}
catch (_bufferError) {
bufferData = Buffer.alloc(0);
}
return {
data: bufferData,
encoding: encoding || null,
contentType: contentType || 'application/octet-stream',
params
};
};
exports.parseDataURI = parseDataURI;
function resolveContent(data, key, options, callback) {
// options is optional; support the legacy resolveContent(data, key, callback) signature
if (!callback && typeof options === 'function') {
callback = options;
options = false;
}
options = options || {};
let promise;
if (!callback) {
promise = new Promise((resolve, reject) => {
callback = (0, exports.callbackPromise)(resolve, reject);
});
}
resolveContentValue(data, key, options, callback);
return promise;
}
function resolveContentValue(data, key, options, callback) {
let content = (data && data[key] && data[key].content) || data[key];
const encoding = ((typeof data[key] === 'object' && data[key].encoding) || 'utf8')
.toString()
.toLowerCase()
.replace(/[-_\s]/g, '');
if (!content) {
return callback(null, content);
}
if (typeof content === 'object') {
if (typeof content.pipe === 'function') {
return resolveStream(content, (err, value) => {
if (err) {
return callback(err);
}
// we can't stream twice the same content, so we need
// to replace the stream object with the streaming result
if (data[key].content) {
data[key].content = value;
}
else {
data[key] = value;
}
callback(null, value);
});
}
else if (/^data:/i.test(content.path || content.href)) {
const parsedDataUri = (0, exports.parseDataURI)(content.path || content.href);
return callback(null, parsedDataUri && parsedDataUri.data ? parsedDataUri.data : Buffer.alloc(0));
}
else if (content.href || /^https?:\/\//i.test(content.path)) {
// An href is always a URL, and so is a path that looks like one. Let nmfetch
// decide whether it is fetchable, it validates the parsed URL. Testing the raw
// string here instead would let a file: href fall through to the "return as is"
// default below and travel on inside the resolved message.
const url = content.href || content.path;
if (options.disableUrlAccess) {
setImmediate(() => {
const err = new Error('Url access rejected for ' + url);
err.code = errors.EURLACCESS;
callback(err);
});
return;
}
return resolveStream((0, index_js_1.default)(url, { headers: content.httpHeaders, tls: content.tls }), callback);
}
else if (content.path) {
if (options.disableFileAccess) {
setImmediate(() => {
const err = new Error('File access rejected for ' + content.path);
err.code = errors.EFILEACCESS;
callback(err);
});
return;
}
return resolveStream(node_fs_1.default.createReadStream(content.path), callback);
}
}
if (typeof data[key].content === 'string' && !['utf8', 'usascii', 'ascii'].includes(encoding)) {
content = Buffer.from(data[key].content, encoding);
}
// default action, return as is
setImmediate(() => callback(null, content));
}
/**
* Copies properties from source objects to target objects
*/
const assign = function (...args) {
const target = args.shift() || {};
args.forEach(source => {
Object.keys(source || {}).forEach(key => {
if ((0, objects_js_1.isProtoKey)(key)) {
return;
}
if (['tls', 'auth'].includes(key) &&
source[key] &&
typeof source[key] === 'object') {
// tls and auth are special keys that need to be enumerated separately
// other objects are passed as is. Enumerating is a copy of user supplied
// keys just like the loop above, so it gets the same treatment
target[key] = (0, objects_js_1.copyOwnKeys)(target[key] || {}, source[key]);
}
else {
target[key] = source[key];
}
});
});
return target;
};
exports.assign = assign;
const encodeXText = (str) => {
// ! 0x21
// + 0x2B
// = 0x3D
// ~ 0x7E
if (!/[^\x21-\x2A\x2C-\x3C\x3E-\x7E]/.test(str)) {
return str;
}
const buf = Buffer.from(str);
let result = '';
for (let i = 0, len = buf.length; i < len; i++) {
const c = buf[i];
if (c < 0x21 || c > 0x7e || c === 0x2b || c === 0x3d) {
result += '+' + (c < 0x10 ? '0' : '') + c.toString(16).toUpperCase();
}
else {
result += String.fromCharCode(c);
}
}
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
*
* @param stream Readable stream
* @param callback Callback function with (err, value)
*/
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('data', (chunk) => {
if (typeof chunk === 'string') {
chunk = Buffer.from(chunk);
}
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);
}
let value;
try {
value = Buffer.concat(chunks, chunklen);
}
catch (E) {
return respond(E);
}
respond(null, value);
});
}
/**
* Generates a bunyan-like logger that prints to console
*
* @returns Bunyan logger instance
*/
function createDefaultLogger(levels) {
const levelMaxLen = levels.reduce((max, level) => Math.max(max, level.length), 0);
const levelNames = new Map();
levels.forEach(level => {
let levelName = level.toUpperCase();
if (levelName.length < levelMaxLen) {
levelName += ' '.repeat(levelMaxLen - levelName.length);
}
levelNames.set(level, levelName);
});
const print = (level, entry, message, ...args) => {
let prefix = '';
if (entry) {
if (entry.tnx === 'server') {
prefix = 'S: ';
}
else if (entry.tnx === 'client') {
prefix = 'C: ';
}
if (entry.sid) {
prefix = '[' + entry.sid + '] ' + prefix;
}
if (entry.cid) {
prefix = '[#' + entry.cid + '] ' + prefix;
}
}
message = node_util_1.default.format(message, ...args);
message.split(/\r?\n/).forEach((line) => {
console.log('[%s] %s %s', new Date().toISOString().substr(0, 19).replace(/T/, ' '), levelNames.get(level), prefix + line);
});
};
const logger = {};
levels.forEach(level => {
logger[level] = print.bind(null, level);
});
return logger;
}
+23
View File
@@ -0,0 +1,23 @@
/**
* Detects a key that can not be copied onto a plain object with `target[key] = value`.
*
* "__proto__" is the only one: assigning it runs the inherited setter and replaces the
* prototype of the target instead of adding a property to it, so a caller can smuggle
* values past validation that only inspects own keys. JSON.parse produces such a key
* where an object literal can not. "constructor" and "prototype" have no such setter and
* become ordinary own properties, so dropping them would only discard legitimate values.
*
* @param key Key to check
* @returns true if the key must not be copied
*/
export declare const isProtoKey: (key: string) => boolean;
/**
* Copies own enumerable keys from a source object to a target object. Every copy that
* walks the keys of user supplied data goes through here, see isProtoKey.
*
* @param target Object to copy the keys to
* @param source Object to copy the keys from
* @param [skip] Optional predicate, return true to leave a key out
* @returns The target object
*/
export declare const copyOwnKeys: <T extends object>(target: T, source: object | null | undefined, skip?: (key: string) => boolean) => T;
+43
View File
@@ -0,0 +1,43 @@
"use strict";
// Safe copying of objects whose keys come from the caller.
//
// This lives in its own leaf module, like ./url.ts, so that every layer can reach it.
// src/shared/index.ts imports src/fetch, so src/fetch can not import src/shared back,
// and src/mime-funcs is a leaf that would otherwise pull in dns/net/os/fs for a string
// comparison. src/shared/index.ts re-exports both functions for the callers that already
// depend on it.
Object.defineProperty(exports, "__esModule", { value: true });
exports.copyOwnKeys = exports.isProtoKey = void 0;
/**
* Detects a key that can not be copied onto a plain object with `target[key] = value`.
*
* "__proto__" is the only one: assigning it runs the inherited setter and replaces the
* prototype of the target instead of adding a property to it, so a caller can smuggle
* values past validation that only inspects own keys. JSON.parse produces such a key
* where an object literal can not. "constructor" and "prototype" have no such setter and
* become ordinary own properties, so dropping them would only discard legitimate values.
*
* @param key Key to check
* @returns true if the key must not be copied
*/
const isProtoKey = (key) => key === '__proto__';
exports.isProtoKey = isProtoKey;
/**
* Copies own enumerable keys from a source object to a target object. Every copy that
* walks the keys of user supplied data goes through here, see isProtoKey.
*
* @param target Object to copy the keys to
* @param source Object to copy the keys from
* @param [skip] Optional predicate, return true to leave a key out
* @returns The target object
*/
const copyOwnKeys = (target, source, skip) => {
Object.keys(source || {}).forEach(key => {
if ((0, exports.isProtoKey)(key) || (skip && skip(key))) {
return;
}
target[key] = source[key];
});
return target;
};
exports.copyOwnKeys = copyOwnKeys;
+21
View File
@@ -0,0 +1,21 @@
/**
* Parsed URL in the shape of the legacy `url.parse()` result
*/
export interface ParsedUrl {
protocol: string | null;
host: string | null;
hostname: string | null;
port: string | null;
pathname: string | null;
search: string | null;
path: string | null;
href: string;
auth: string | null;
/** Decoded user name, null when the URL carries no credentials */
username: string | null;
/** Decoded password, null when the URL carries none */
password: string | null;
query: string | null | Record<string, string | string[]>;
}
export declare const parse: (input?: string | null, parseQueryString?: boolean) => ParsedUrl;
export declare const resolve: (from: string, to: string) => string;
+272
View File
@@ -0,0 +1,272 @@
"use strict";
// URL parsing wrapper around the WHATWG `URL` class. It only falls back to the
// legacy, deprecation-warning-emitting `url.parse()` / `url.resolve()` for input
// the WHATWG parser rejects.
//
// The WHATWG `URL` exposes a different shape than the legacy parser, so results
// are normalized back into the legacy field names the rest of the codebase reads
// (`protocol`, `hostname`, `port`, `pathname`, `path`, `search`, `auth`, `query`,
// `href`). This keeps every existing call site unchanged.
//
// Known, accepted divergences from the legacy parser:
// - non-special schemes (smtp:/smtps:/direct:) are not host-lowercased by
// WHATWG; cosmetic only, SMTP/DNS hosts are case-insensitive. (IDNA mapping
// and IPv6 brackets are normalized back by normalizeHostname below.)
// - a literal unescaped ':' inside a password is percent-encoded by WHATWG;
// such passwords should be percent-encoded by the caller anyway.
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolve = exports.parse = void 0;
const node_net_1 = __importDefault(require("node:net"));
const node_url_1 = __importDefault(require("node:url"));
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]+)$/;
/**
* 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/..."
// string: the scheme, if any (anything the legacy parser takes for one, it is less strict
// than WHATWG about the first character), and what the legacy parser has to report as the
// host once the userinfo is removed, see legacyParse. The legacy parser treats a backslash
// as a slash.
const AUTHORITY = /^([a-zA-Z0-9+.-]+:)?[\\/]{2}([^\\/?#]*)/;
// The WHATWG forbidden domain code points, except '%' which an opaque host may carry: the
// C0 control characters, space, DEL and the URL delimiters. CONTROL_CHARS is the C0 and DEL
// subset, checked on its own in legacyParse where the host string still carries the port
// and IPv6 brackets.
const FORBIDDEN_HOST_CHARS = /[\x00-\x20#/:<>?@[\\\]^|\x7f]/;
const CONTROL_CHARS = /[\x00-\x1f\x7f]/;
// The error the WHATWG parser throws, for a host that only fails the checks in this module
function invalidUrl(input) {
const err = new TypeError('Invalid URL');
err.code = 'ERR_INVALID_URL';
err.input = input;
return err;
}
// Legacy url.parse() for input the WHATWG parser refused. The legacy parser does not
// reject a host it can not represent: a NUL byte, a percent-encoded byte, a space or a
// '<' inside the host ends the host early and the rest becomes the path, so
// 'localhost%00.example.com' silently turns into a request to 'localhost'. When the
// input has an authority that the legacy parser reads as the host (always for
// "scheme://", for "//host" only when slashesDenoteHost is set, as url.resolve() does
// for its target), the legacy result is accepted only if that host is the whole written
// authority, lowercased and IDNA mapped the way the legacy parser does it, and carries
// no control character. A legacy result with a host (an empty one included, 'http:///x'
// is a request to localhost) that no such authority accounts for is refused as well.
// Otherwise the WHATWG error is reported. Relative input has no authority to check and
// keeps the legacy behavior.
function legacyParse(input, parseQueryString, whatwgError, slashesDenoteHost) {
const parsed = node_url_1.default.parse(input, parseQueryString, slashesDenoteHost);
const authority = AUTHORITY.exec(input.replace(LEGACY_TRIM, ''));
if (authority && (authority[1] || parsed.hostname !== null)) {
const written = authority[2].slice(authority[2].lastIndexOf('@') + 1);
if (!written || CONTROL_CHARS.test(written) || (parsed.host || '').toLowerCase() !== punycode.toASCII(written.toLowerCase())) {
throw whatwgError;
}
// the legacy parser takes any bracketed value for an IPv6 literal
if (written.charAt(0) === '[' && !node_net_1.default.isIPv6(written.slice(1, written.indexOf(']')))) {
throw whatwgError;
}
}
else if (parsed.hostname !== null) {
throw whatwgError;
}
// the legacy parser only offers the joined form, split it on the first colon
const legacyAuth = parsed.auth === null || parsed.auth === undefined ? null : parsed.auth.split(':');
const result = parsed;
result.username = legacyAuth ? legacyAuth.shift() : null;
result.password = legacyAuth && legacyAuth.length ? legacyAuth.join(':') : null;
return result;
}
// decodeURIComponent that never throws. Legacy url.parse() decodes the auth
// component but tolerates malformed percent sequences, so mirror that.
function safeDecode(str) {
try {
return decodeURIComponent(str);
}
catch (_err) {
return str;
}
}
// Derives the legacy-shaped bare hostname from a WHATWG URL. WHATWG keeps IPv6
// literals bracketed ('[::1]') and, for non-special schemes (smtp:/smtps:/socks:),
// percent-encodes a non-ASCII host instead of IDNA-mapping it. Both forms are
// un-resolvable when handed to net/dns/http.request, which is what every call
// site does, so map them back to what legacy url.parse() returned: the bare
// address and the IDNA mapped (lowercased, punycode) form. Idempotent on plain
// ASCII and already-punycode hosts, so special-scheme hosts (already IDNA-mapped
// by WHATWG) pass through.
function normalizeHostname(raw, href) {
const hostname = raw || '';
if (!hostname) {
// Host-less URL (e.g. 'direct:'): legacy returned '' here, not null;
// consumers do `hostname.length` / `'.' + hostname`, so keep it a string.
return '';
}
if (hostname.charAt(0) === '[' && hostname.charAt(hostname.length - 1) === ']') {
return hostname.slice(1, -1);
}
const decoded = safeDecode(hostname);
// domainToASCII applies the WHATWG host rules (IDNA mapping included) and returns an
// empty string for a host it refuses, the forbidden characters among them
const mapped = FORBIDDEN_HOST_CHARS.test(decoded) ? '' : node_url_1.default.domainToASCII(decoded);
if (!mapped) {
throw invalidUrl(href);
}
return mapped;
}
const parse = (input, parseQueryString) => {
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
// parser instead treats a scheme not followed by "//" as an opaque path.
// Re-insert the "//" so slash-less connection/proxy URLs keep resolving to
// an authority, as they did before. This assumes a slash-authority scheme,
// which every consumer here uses (http/https/smtp/smtps/socks/direct); an
// opaque scheme like mailto:/data:/tel: would be mis-split, but none reach
// this module.
const slashless = SLASHLESS_AUTHORITY.exec(input);
const normalized = slashless ? slashless[1] + '//' + slashless[2] : input;
let u;
try {
u = new URL(normalized);
}
catch (err) {
// WHATWG rejects some input the legacy parser tolerated (empty/relative
// strings, scheme-relative '//host/path', out-of-range ports, ...). Fall
// back to the legacy parser so behavior, including the downstream errors
// callers rely on, is preserved. This is the only path that can still
// emit a deprecation warning; it fires for anything WHATWG cannot
// represent, including legitimate relative URLs, not just malformed input.
return legacyParse(normalized, parseQueryString, err);
}
const hostname = normalizeHostname(u.hostname, u.href);
const port = u.port || null;
const pathname = u.pathname || null;
const search = u.search || null;
// Legacy `.auth` is the decoded "user[:pass]" string; WHATWG keeps the
// username/password percent-encoded, so decode to stay byte-compatible with
// existing consumers (parseConnectionUrl, Basic/Proxy-Authorization headers).
let auth = null;
let username = null;
let password = null;
if (u.username || u.password) {
// Gate on password too: legacy url.parse('smtps://:pass@host').auth was
// ':pass'. Dropping it would silently connect unauthenticated.
username = safeDecode(u.username);
password = u.password ? safeDecode(u.password) : null;
// the joined form is ambiguous once the user name contains a colon, so
// consumers that need the parts read username and password instead
auth = username + (password !== null ? ':' + password : '');
}
let query;
if (parseQueryString) {
// Mirror querystring.parse(): null-prototype object, repeated keys become an array.
const parsed = Object.create(null);
u.searchParams.forEach((value, key) => {
if (Object.prototype.hasOwnProperty.call(parsed, key)) {
const existing = parsed[key];
if (Array.isArray(existing)) {
existing.push(value);
}
else {
parsed[key] = [existing, value];
}
}
else {
parsed[key] = value;
}
});
query = parsed;
}
else {
query = search ? search.slice(1) : null;
}
return {
protocol: u.protocol || null,
host: u.host || null,
hostname,
port,
pathname,
search,
path: (pathname || '') + (search || '') || null,
href: u.href,
auth,
username,
password,
query
};
};
exports.parse = parse;
const resolve = (from, to) => {
try {
return new URL(to, from).href;
}
catch (err) {
// Malformed target, fall back to the legacy resolver, but only when the legacy
// parser reads the same host out of both inputs that was written. The target
// decides the host when it is absolute or scheme-relative, the base otherwise
legacyParse(from, false, err, true);
legacyParse(to, false, err, true);
return node_url_1.default.resolve(from, to);
}
};
exports.resolve = resolve;
+15
View File
@@ -0,0 +1,15 @@
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, bare CR and bare LF
* are both turned into <CR><LF>
*
* @param options Stream options
*/
export default class DataStream extends Transform {
options: TransformOptions;
inByteCount: number;
outByteCount: number;
lastByte: number | false;
constructor(options?: TransformOptions);
}
+108
View File
@@ -0,0 +1,108 @@
"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, bare CR and bare LF
* are both turned into <CR><LF>
*
* @param options Stream options
*/
class DataStream extends node_stream_1.Transform {
constructor(options) {
super(options);
this.options = options || {};
this.inByteCount = 0;
this.outByteCount = 0;
this.lastByte = false;
}
/**
* Escapes dots
* @internal
*/
_transform(chunk, encoding, done) {
const chunks = [];
let chunklen = 0;
let i, len, lastPos = 0;
let buf;
if (!chunk || !chunk.length) {
return done();
}
if (typeof chunk === 'string') {
chunk = Buffer.from(chunk);
}
this.inByteCount += chunk.length;
for (i = 0, len = chunk.length; i < len; i++) {
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 (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 (chunks.length) {
// add last piece
if (lastPos < chunk.length) {
buf = chunk.slice(lastPos);
chunks.push(buf);
chunklen += buf.length;
}
this.outByteCount += chunklen;
this.push(Buffer.concat(chunks, chunklen));
}
else {
this.outByteCount += chunk.length;
this.push(chunk);
}
this.lastByte = chunk[chunk.length - 1];
done();
}
/**
* Finalizes the stream with a dot on a single line
* @internal
*/
_flush(done) {
let buf;
if (this.lastByte === 0x0a) {
buf = Buffer.from('.\r\n');
}
else if (this.lastByte === 0x0d) {
buf = Buffer.from('\n.\r\n');
}
else {
buf = Buffer.from('\r\n.\r\n');
}
this.outByteCount += buf.length;
this.push(buf);
done();
}
}
exports.default = DataStream;
module.exports = exports.default;
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
@@ -0,0 +1,49 @@
/**
* Minimal HTTP/S proxy client
*/
import net from 'node:net';
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 = Callback<net.Socket>;
/**
* Establishes proxied connection to destinationPort
*
* httpProxyClient("http://localhost:3128/", 80, "google.com", function(err, socket){
* socket.write("GET / HTTP/1.0\r\n\r\n");
* });
*
* @param proxyUrl proxy configuration, e.g. "http://proxy.host:3128/"
* @param destinationPort Port to open in destination host
* @param destinationHost Destination hostname
* @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;
/**
* 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 {
let timeout: number | undefined;
}
export default httpProxyClient;
+209
View File
@@ -0,0 +1,209 @@
"use strict";
/**
* Minimal HTTP/S proxy client
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const node_net_1 = __importDefault(require("node:net"));
const node_tls_1 = __importDefault(require("node:tls"));
const urllib = __importStar(require("../shared/url.js"));
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).
destinationPort = Number(destinationPort) || 0;
if (!destinationPort || /[\r\n]/.test(destinationHost)) {
const err = new Error('Invalid proxy destination');
err.code = errors.EPROXY;
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
};
let connect;
if (proxy.protocol === 'https:') {
// Validate the proxy's TLS certificate by default. A caller that uses a
// self-signed proxy (e.g. integration tests) opts out explicitly with
// tls.rejectUnauthorized === false.
connectOptions.rejectUnauthorized = tlsOptions.rejectUnauthorized !== false;
connect = node_tls_1.default.connect.bind(node_tls_1.default);
}
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;
// 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;
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
}
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: authority,
Connection: 'close'
};
if (proxy.auth) {
reqHeaders['Proxy-Authorization'] = 'Basic ' + Buffer.from(proxy.auth).toString('base64');
}
socket.write(
// HTTP method
'CONNECT ' +
authority +
' HTTP/1.1\r\n' +
// HTTP request headers
Object.keys(reqHeaders)
.map(key => key + ': ' + reqHeaders[key])
.join('\r\n') +
// End request
'\r\n\r\n');
socket.on('data', onSocketData);
});
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;
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
+422
View File
@@ -0,0 +1,422 @@
import { EventEmitter } from 'node:events';
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 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
*/
export type SMTPConnectionCustomAuthHandlers = {
[method: string]: SMTPConnectionCustomAuthHandler;
};
/**
* Options for the SMTP connection, see the SMTPConnection class description
*/
export interface SMTPConnectionOptions {
/** Port to connect to, defaults to 587, or to 465 when secure is set */
port?: number | string | undefined;
/** Hostname or IP address to connect to, defaults to 'localhost' */
host?: string | undefined;
/** Use TLS from the start */
secure?: boolean | undefined;
/** Marks the provided socket as already upgraded to TLS */
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, has no effect when requireTLS is set */
ignoreTLS?: boolean | undefined;
/** 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, 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;
/** Outbound address to bind to */
localAddress?: string | undefined;
/** Time to wait in ms for the connection to establish, defaults to 2 minutes */
connectionTimeout?: number | undefined;
/** Time to wait in ms until the greeting is received, defaults to 30 seconds */
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 */
lmtp?: boolean | undefined;
/** Bunyan compatible logger interface, true for the default console logger */
logger?: shared.ExternalLogger | boolean | undefined;
/** Pass SMTP traffic, including the message data, to the logger */
debug?: boolean | undefined;
/** Pass SMTP commands and responses to the logger */
transactionLog?: boolean | undefined;
/** Options for tls.connect */
tls?: tls.ConnectionOptions | undefined;
/** Existing socket to use instead of creating a new one, not connected yet */
socket?: net.Socket | undefined;
/** Already opened connection to use instead of creating a new one */
connection?: net.Socket | undefined;
/** Count loopback interfaces when checking which address families are usable */
allowInternalNetworkInterfaces?: boolean | undefined;
/** Logger component name, defaults to 'smtp-connection' */
component?: string | undefined;
/** Custom authentication handlers keyed by method name */
customAuth?: SMTPConnectionCustomAuthHandlers | undefined;
}
/**
* User and password credentials resolved for a SASL mechanism
*/
export interface SMTPConnectionCredentials {
user?: string | undefined;
pass?: string | undefined;
/** Extra options for the authentication method, copied from the auth object */
options?: {
[key: string]: any;
} | undefined;
}
/**
* Authentication data for login()
*/
export interface SMTPConnectionAuth {
/** Authentication type, informational */
type?: string | undefined;
/** SASL method to use, false or unset picks the first supported one (PLAIN if none is advertised) */
method?: string | false | undefined;
user?: string | undefined;
pass?: string | undefined;
/** Extra options for the authentication method */
options?: {
[key: string]: any;
} | undefined;
/** XOAuth2 token generator, selects XOAUTH2 when no method is set */
oauth2?: XOAuth2 | undefined;
/** Credentials for the SASL mechanism, filled in from user and pass when missing */
credentials?: SMTPConnectionCredentials | undefined;
/** Custom authentication handlers receive the auth object as is, so it may carry any other value */
[key: string]: any;
}
/**
* A parsed server reply, handed to the sendCommand callback of a custom authentication handler
*/
export interface SMTPConnectionCustomAuthResponse {
/** The command that was sent */
command: string;
/** Raw server response */
response: string;
/** Numeric status code, 0 if the response did not start with one */
status: number;
/** Enhanced status code, if any */
code?: string | undefined;
/** Response text without the status codes */
text: string;
}
/**
* 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(), with the credentials filled in */
auth: SMTPConnectionCustomAuthData;
/** Selected authentication method name */
method: string;
/** SMTP extensions the server advertised */
extensions: string[];
/** SASL methods the server advertised */
authMethods: string[];
/** Maximum message size the server accepts, false when not advertised */
maxAllowedSize: number | false;
/** 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 that settles the authentication. Any other return value is ignored
*/
export type SMTPConnectionCustomAuthHandler = (ctx: SMTPConnectionCustomAuthContext) => unknown;
/**
* An envelope address, either a plain string or an object with an address property
*/
export interface SMTPEnvelopeAddress {
address?: string | undefined;
name?: string | undefined;
}
/**
* DSN parameters for the envelope (RFC 3461)
*/
export interface SMTPEnvelopeDsn {
/** Return either 'HDRS' (headers) or 'FULL' (body) with the notification */
ret?: string | null | undefined;
/** Alias of ret */
return?: string | undefined;
/** Envelope identifier, sent as ENVID */
envid?: string | null | undefined;
/** Alias of envid */
id?: string | undefined;
/** When to notify: 'NEVER', or any combination of 'SUCCESS', 'FAILURE' and 'DELAY' */
notify?: string | string[] | null | undefined;
/** Original recipient, sent as ORCPT */
recipient?: string | undefined;
/** 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, 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 */
size?: number | string | undefined;
/** DSN parameters, sent when the server supports the DSN extension */
dsn?: SMTPEnvelopeDsn | undefined;
/** Declare BODY=8BITMIME when the server supports it */
use8BitMime?: boolean | undefined;
/** RFC 8689: send the REQUIRETLS parameter, requires a TLS connection and server support */
requireTLSExtensionEnabled?: boolean | undefined;
}
/**
* The envelope as tracked by the connection while a message is being sent. The from and to
* values are normalized to strings and the recipient bookkeeping is added by _setEnvelope
*/
export interface SMTPConnectionEnvelope extends SMTPEnvelope {
from?: string | undefined;
to?: string[] | undefined;
/** Recipients still waiting for RCPT TO */
rcptQueue: string[];
/** Recipients the server rejected */
rejected: string[];
/** Errors for the rejected recipients */
rejectedErrors: NodemailerError[];
/** Recipients the server accepted */
accepted: string[];
}
/**
* The recipient bookkeeping of a sent message, known once the envelope is accepted
*/
export interface SMTPConnectionEnvelopeInfo {
/** Recipients the server accepted */
accepted: string[];
/** Recipients the server rejected */
rejected: string[];
/** EHLO response lines, without the greeting line */
ehlo?: string[] | undefined;
/** Errors for the rejected recipients */
rejectedErrors?: NodemailerError[] | undefined;
}
/**
* Result of a sent message
*/
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
*/
export type SMTPConnectionCallback = (err: NodemailerError | null, result?: boolean) => void;
/**
* Callback for the message data response, yields the server response text
*/
export type SMTPConnectionResponseCallback = (err: NodemailerError | null, response?: string) => void;
/**
* Callback for connect(), run once the SMTP handshake is finished
*/
export type SMTPConnectionConnectCallback = (err?: NodemailerError) => void;
/**
* Options handed to net.connect or tls.connect, resolved hostname values are merged in
*/
export interface SMTPConnectionConnectOptions extends tls.ConnectionOptions {
port: number;
host: string;
/** Outbound address to bind to */
localAddress?: string | undefined;
/** Count loopback interfaces when resolving the hostname */
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
*/
export type SMTPConnectionResponseAction = (str: string) => void;
/**
* Generates a SMTP connection object
*
* Optional options object takes the following possible properties:
*
* * **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 (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
* * **debug** - if true pass SMTP traffic to the logger
* * **tls** - options for createCredentials
* * **socket** - existing socket to use instead of creating a new one (see: http://nodejs.org/api/net.html#net_class_net_socket)
* * **secured** - boolean indicates that the provided socket has already been upgraded to tls
*
* @constructor
* @namespace SMTP Client module
* @param [options] Option properties
*/
declare class SMTPConnection extends EventEmitter {
id: string;
stage: 'init' | 'connected';
options: SMTPConnectionOptions;
secureConnection: boolean;
alreadySecured: boolean;
port: number;
host: string;
servername: string | false;
allowInternalNetworkInterfaces: boolean;
name: string;
logger: shared.Logger;
customAuth: Map<string, SMTPConnectionCustomAuthHandler>;
/**
* Expose version nr, just for the reference
*/
version: string;
/**
* If true, then the user is authenticated
*/
authenticated: boolean;
/**
* If set to true, this instance is no longer active
* @private
*/
destroyed: boolean;
/**
* Defines if the current connection is secure or not. If not,
* STARTTLS can be used if available
* @private
*/
secure: boolean;
lastServerResponse: string | false;
/**
* The socket connecting to the server
* @public
*/
_socket: net.Socket | false | null;
/**
* Set to true, if EHLO response includes "AUTH".
* If false then authentication is not tried
*/
allowsAuth: boolean;
/**
* True while the STARTTLS upgrade is in progress
* @private
*/
upgrading?: boolean | undefined;
constructor(options?: SMTPConnectionOptions);
/**
* Creates a connection to a SMTP server and sets up connection
* listener
*/
connect(connectCallback?: SMTPConnectionConnectCallback): void;
/**
* Sends QUIT
*/
quit(): void;
/**
* Closes the connection to the server
*/
close(): void;
/**
* Authenticate user
*/
login(authData: SMTPConnectionAuth | undefined, callback: SMTPConnectionCallback): void;
/**
* Sends a message
*
* @param envelope Envelope object, {from: addr, to: [addr]}
* @param message String, Buffer or a Stream
* @param done Callback to return once sending is completed
*/
send(envelope: SMTPEnvelope, message: string | Buffer | Readable, done: SMTPConnectionSendCallback): void;
/**
* Resets connection state
*
* @param callback Callback to return once connection is reset
*/
reset(callback: SMTPConnectionCallback): void;
}
/**
* Type aliases in the layout of @types/nodemailer, so `SMTPConnection.Options` style references keep working
*/
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;
File diff suppressed because it is too large. Load diff
+136
View File
@@ -0,0 +1,136 @@
import { EventEmitter } from 'node:events';
import * as shared from '../shared/index.js';
import type { SMTPTransportOptions, SMTPTransportGetSocketCallback, SMTPTransportSendCallback, SMTPSentMessageInfo } from '../smtp-transport/index.js';
import type MailMessage from '../mailer/mail-message.js';
import type { default as Mail, SendMailOptions, VerifyCallback } from '../mailer/index.js';
/**
* Options for the pooled SMTP transport, the SMTP transport options plus the pool settings
*/
export interface SMTPPoolOptions extends SMTPTransportOptions {
/** Set to true to get this pooled transport from createTransport */
pool?: boolean | undefined;
/** Maximum number of open connections, defaults to 5 */
maxConnections?: number | undefined;
/** Number of messages a connection sends before it is closed and replaced, defaults to 100 */
maxMessages?: number | undefined;
/** Maximum number of messages to send in rateDelta milliseconds, unlimited when not set */
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, 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
*/
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
*/
export type SMTPPoolSentMessageInfo = SMTPSentMessageInfo;
/**
* Callback for send()
*/
export type SMTPPoolSendCallback = SMTPTransportSendCallback;
/**
* A message waiting in the pool queue
*/
export interface SMTPPoolQueueEntry {
/** The message to send */
mail: MailMessage;
/** How many times the entry was put back on the queue after its connection closed */
requeueAttempts: number;
/** Callback to run once the message is sent or failed */
callback: SMTPPoolSendCallback;
/** Message-ID value without the angle brackets, set when the entry is assigned to a connection */
messageId?: string | undefined;
}
/**
* Rate limiter state of the pool
*/
export interface SMTPPoolRateLimit {
/** Messages assigned within the current window */
counter: number;
/** Timer that clears the current window */
timeout: NodeJS.Timeout | null;
/** Availability callbacks waiting for the window to clear */
waiting: Array<() => void>;
/** Start of the current window as a timestamp, false when no window is open */
checkpoint: number | false;
/** Window length in milliseconds */
delta: number;
/** Maximum number of messages per window, 0 for no limit */
limit: number;
}
/**
* Creates a SMTP pool transport object for Nodemailer
*
* @constructor
* @param options SMTP Connection options
*/
declare class SMTPPool extends EventEmitter {
options: SMTPPoolResolvedOptions;
logger: shared.Logger;
name: string;
version: string;
idling: boolean;
/**
* The Mail instance using this transport, assigned by Mail
*/
mailer?: Mail<SMTPPoolSentMessageInfo, SMTPPoolOptions> | undefined;
constructor(options?: SMTPPoolOptions | string);
/**
* Placeholder function for creating proxy sockets. This method immediatelly returns
* without a socket
*
* @param options Connection options
* @param callback Callback function to run with the socket keys
*/
getSocket(options: SMTPPoolOptions, callback: SMTPTransportGetSocketCallback): void;
/**
* Queues an e-mail to be sent using the selected settings
*
* @param mail Mail object
* @param callback Callback function
*/
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
*/
close(): void;
/**
* Returns true if there are free slots in the queue
*/
isIdle(): boolean;
/**
* 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(callback: VerifyCallback): void;
}
/**
* Type aliases in the layout of @types/nodemailer, so `SMTPPool.Options` style references keep working
*/
declare namespace SMTPPool {
type Options = SMTPPoolOptions;
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;
+575
View File
@@ -0,0 +1,575 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const node_events_1 = require("node:events");
const pool_resource_js_1 = __importDefault(require("./pool-resource.js"));
const index_js_1 = __importDefault(require("../smtp-connection/index.js"));
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
*
* @constructor
* @param options SMTP Connection options
*/
class SMTPPool extends node_events_1.EventEmitter {
constructor(options) {
super();
options = options || {};
if (typeof options === 'string') {
options = {
url: options
};
}
let urlData;
let service = options.service;
if (typeof options.getSocket === 'function') {
this.getSocket = options.getSocket;
}
if (options.url) {
urlData = shared.parseConnectionUrl(options.url);
service = service || urlData.service;
}
this.options = shared.assign(false, // create new object
options, // regular options
urlData, // url options
(service && (0, index_js_2.default)(service)) // wellknown options
);
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'
});
this.name = 'SMTP (pool)';
this.version = packageData.version + '[client:' + packageData.version + ']';
this._rateLimit = {
counter: 0,
timeout: null,
waiting: [],
checkpoint: false,
delta: Number(this.options.rateDelta) || 1000,
limit: Number(this.options.rateLimit) || 0
};
this._closed = false;
this._queue = [];
this._connections = [];
this._connectionCounter = 0;
this.idling = true;
setImmediate(() => {
if (this.idling) {
this.emit('idle');
}
});
}
/**
* Placeholder function for creating proxy sockets. This method immediatelly returns
* without a socket
*
* @param options Connection options
* @param callback Callback function to run with the socket keys
*/
getSocket(options, callback) {
// return immediatelly
setImmediate(() => callback(null, false));
}
/**
* Queues an e-mail to be sent using the selected settings
*
* @param mail Mail object
* @param callback Callback function
*/
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({
mail,
requeueAttempts: 0,
callback
});
if (this.idling && this._queue.length >= this.options.maxConnections) {
this.idling = false;
}
setImmediate(() => this._processMessages());
return true;
}
/**
* Closes all connections in the pool. If there is a message being sent, the connection
* is closed later
*/
close() {
let connection;
const len = this._connections.length;
this._closed = true;
// release connections gated by the rate limiter so they become
// available and are torn down below instead of leaking with a
// cleared timer that never fires
this._clearRateLimit();
if (!len && !this._queue.length) {
return;
}
// remove all available connections
for (let i = len - 1; i >= 0; i--) {
if (this._connections[i] && this._connections[i].available) {
connection = this._connections[i];
connection.close();
this.logger.info({
tnx: 'connection',
cid: connection.id,
action: 'removed'
}, 'Connection #%s removed', connection.id);
}
}
if (len && !this._connections.length) {
this.logger.debug({
tnx: 'connection'
}, 'All connections removed');
}
if (!this._queue.length) {
return;
}
// make sure that entire queue would be cleaned
const invokeCallbacks = () => {
if (!this._queue.length) {
this.logger.debug({
tnx: 'connection'
}, 'Pending queue entries cleared');
return;
}
const entry = this._queue.shift();
if (entry && typeof entry.callback === 'function') {
try {
entry.callback(new Error('Connection pool was closed'));
}
catch (E) {
// the queue is drained without a connection, so there is no cid to log
this.logger.error({
err: E,
tnx: 'callback'
}, 'Callback error: %s', E.message);
}
}
setImmediate(invokeCallbacks);
};
setImmediate(invokeCallbacks);
}
/**
* Check the queue and available connections. If there is a message to be sent and there is
* an available connection, then use this connection to send the mail
* @internal
*/
_processMessages() {
// do nothing if already closed
if (this._closed) {
return;
}
// do nothing if queue is empty
if (!this._queue.length) {
if (!this.idling) {
// no pending jobs
this.idling = true;
this.emit('idle');
}
return;
}
// find first available connection
let connection = this._connections.find(c => c.available);
if (!connection && this._connections.length < this.options.maxConnections) {
connection = this._createConnection();
}
if (!connection) {
// no more free connection slots available
this.idling = false;
return;
}
// check if there is free space in the processing queue
if (!this.idling && this._queue.length < this.options.maxConnections) {
this.idling = true;
this.emit('idle');
}
const entry = (connection.queueEntry = this._queue.shift());
entry.messageId = (connection.queueEntry.mail.message.getHeader('message-id') || '').replace(/[<>\s]/g, '');
connection.available = false;
this.logger.debug({
tnx: 'pool',
cid: connection.id,
messageId: entry.messageId,
action: 'assign'
}, 'Assigned message <%s> to #%s (%s)', entry.messageId, connection.id, connection.messages + 1);
if (this._rateLimit.limit) {
this._rateLimit.counter++;
if (!this._rateLimit.checkpoint) {
this._rateLimit.checkpoint = Date.now();
}
}
connection.send(entry.mail, (err, info) => {
// only process callback if current handler is not changed
if (entry === connection.queueEntry) {
try {
entry.callback(err, info);
}
catch (E) {
this.logger.error({
err: E,
tnx: 'callback',
cid: connection.id
}, 'Callback error for #%s: %s', connection.id, E.message);
}
connection.queueEntry = false;
}
});
}
/**
* Creates a new pool resource
* @internal
*/
_createConnection() {
const connection = new pool_resource_js_1.default(this);
connection.id = ++this._connectionCounter;
this.logger.info({
tnx: 'pool',
cid: connection.id,
action: 'conection'
}, 'Created new pool resource #%s', connection.id);
// resource comes available
connection.on('available', () => {
this.logger.debug({
tnx: 'connection',
cid: connection.id,
action: 'available'
}, 'Connection #%s became available', connection.id);
if (this._closed) {
// if already closed run close() that will remove this connections from connections list
this.close();
}
else {
// check if there's anything else to send
this._processMessages();
}
});
// resource is terminated with an error
connection.once('error', (err) => {
if (err.code !== errors.EMAXLIMIT) {
this.logger.warn({
err,
tnx: 'pool',
cid: connection.id
}, 'Pool Error for #%s: %s', connection.id, err.message);
}
else {
this.logger.debug({
tnx: 'pool',
cid: connection.id,
action: 'maxlimit'
}, 'Max messages limit exchausted for #%s', connection.id);
}
if (connection.queueEntry) {
try {
connection.queueEntry.callback(err);
}
catch (E) {
this.logger.error({
err: E,
tnx: 'callback',
cid: connection.id
}, 'Callback error for #%s: %s', connection.id, E.message);
}
connection.queueEntry = false;
}
// remove the erroneus connection from connections list
this._removeConnection(connection);
this._continueProcessing();
});
connection.once('close', () => {
this.logger.info({
tnx: 'connection',
cid: connection.id,
action: 'closed'
}, 'Connection #%s was closed', connection.id);
this._removeConnection(connection);
if (connection.queueEntry) {
// If the connection closed when sending, add the message to the queue again
// if max number of requeues is not reached yet
// 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)) {
delay = this._requeueEntryOnConnectionClose(connection);
}
else {
this._failDeliveryOnConnectionClose(connection);
}
}
if (delay) {
// back off, a server that keeps dropping connections is not hammered
setTimeout(() => this._continueProcessing(), delay);
}
else {
this._continueProcessing();
}
}, 50);
}
else {
if (!this._closed && this.idling && !this._connections.length) {
this.emit('clear');
}
this._continueProcessing();
}
});
this._connections.push(connection);
return connection;
}
/** @internal */
_shouldRequeuOnConnectionClose(queueEntry) {
if (this.options.maxRequeues < 0) {
return true;
}
return queueEntry.requeueAttempts < this.options.maxRequeues;
}
/** @internal */
_failDeliveryOnConnectionClose(connection) {
if (connection.queueEntry && connection.queueEntry.callback) {
try {
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({
err: E,
tnx: 'callback',
messageId: connection.queueEntry.messageId,
cid: connection.id
}, 'Callback error for #%s: %s', connection.id, E.message);
}
connection.queueEntry = false;
}
}
/** @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',
cid: connection.id,
messageId: connection.queueEntry.messageId,
action: 'requeue'
}, '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
* @internal
*/
_continueProcessing() {
if (this._closed) {
this.close();
}
else {
setTimeout(() => this._processMessages(), 100);
}
}
/**
* Remove resource from pool
*
* @param connection The PoolResource to remove
* @internal
*/
_removeConnection(connection) {
const index = this._connections.indexOf(connection);
if (index !== -1) {
this._connections.splice(index, 1);
}
}
/**
* Checks if connections have hit current rate limit and if so, queues the availability callback
*
* @param callback Callback function to run once rate limiter has been cleared
* @internal
*/
_checkRateLimit(callback) {
if (!this._rateLimit.limit) {
return callback();
}
const now = Date.now();
if (this._rateLimit.counter < this._rateLimit.limit) {
return callback();
}
this._rateLimit.waiting.push(callback);
if (this._rateLimit.checkpoint <= now - this._rateLimit.delta) {
return this._clearRateLimit();
}
if (!this._rateLimit.timeout) {
this._rateLimit.timeout = setTimeout(() => this._clearRateLimit(), this._rateLimit.delta - (now - this._rateLimit.checkpoint));
this._rateLimit.checkpoint = now;
}
}
/**
* Clears current rate limit limitation and runs paused callback
* @internal
*/
_clearRateLimit() {
clearTimeout(this._rateLimit.timeout);
this._rateLimit.timeout = null;
this._rateLimit.counter = 0;
this._rateLimit.checkpoint = false;
// resume all paused connections
while (this._rateLimit.waiting.length) {
const cb = this._rateLimit.waiting.shift();
setImmediate(cb);
}
}
/**
* Returns true if there are free slots in the queue
*/
isIdle() {
return this.idling;
}
verify(callback) {
let promise;
if (!callback) {
promise = new Promise((resolve, reject) => {
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;
// 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 done(err);
}
let options = this.options;
if (socketOptions && socketOptions.connection) {
this.logger.info({
tnx: 'proxy',
remoteAddress: socketOptions.connection.remoteAddress,
remotePort: socketOptions.connection.remotePort,
destHost: options.host || '',
destPort: options.port || '',
action: 'connected'
}, 'Using proxied socket from %s:%s to %s:%s', socketOptions.connection.remoteAddress, socketOptions.connection.remotePort, options.host || '', options.port || '');
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) {
return;
}
returned = true;
connection.close();
return done(err);
});
connection.once('end', () => {
if (returned) {
return;
}
returned = true;
return done(new Error('Connection closed'));
});
const finalize = () => {
if (returned) {
return;
}
returned = true;
connection.quit();
return done(null, true);
};
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) {
return;
}
if (err) {
returned = true;
connection.close();
return done(err);
}
finalize();
});
}
else if (!auth && connection.allowsAuth && options.forceAuth) {
const err = new Error('Authentication info was not provided');
err.code = errors.ENOAUTH;
returned = true;
connection.close();
return done(err);
}
else {
finalize();
}
});
});
return promise;
}
}
exports.default = SMTPPool;
module.exports = exports.default;
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
+62
View File
@@ -0,0 +1,62 @@
import SMTPConnection from '../smtp-connection/index.js';
import { type Logger } from '../shared/index.js';
import { EventEmitter } from 'node:events';
import type { SMTPTransportAuth, SMTPTransportSendCallback } from '../smtp-transport/index.js';
import type MailMessage from '../mailer/mail-message.js';
import type SMTPPool from './index.js';
import type { SMTPPoolResolvedOptions, SMTPPoolQueueEntry } from './index.js';
/**
* Callback for connect(), the result is true once the connection is ready for messages
*/
export type PoolResourceConnectCallback = (err: Error | null, connected?: true) => void;
/**
* Callback for send()
*/
export type PoolResourceSendCallback = SMTPTransportSendCallback;
/**
* Creates an element for the pool
*
* @constructor
* @param pool SMTPPool instance
*/
export default class PoolResource extends EventEmitter {
pool: SMTPPool;
options: SMTPPoolResolvedOptions;
logger: Logger;
/**
* Authentication data for the connection, set when the pool options include auth
*/
auth?: SMTPTransportAuth | undefined;
messages: number;
available: boolean;
/**
* The SMTP connection, set by connect()
*/
connection: SMTPConnection;
/**
* Resource id, assigned by the pool
*/
id: number;
/**
* The queue entry being sent, assigned by the pool. False once it has been handled
*/
queueEntry?: SMTPPoolQueueEntry | false | undefined;
constructor(pool: SMTPPool);
/**
* Initiates a connection to the SMTP server
*
* @param callback Callback function to run once the connection is established or failed
*/
connect(callback: PoolResourceConnectCallback): void;
/**
* Sends an e-mail to be sent using the selected settings
*
* @param mail Mail object
* @param callback Callback function
*/
send(mail: MailMessage, callback: PoolResourceSendCallback): void;
/**
* Closes the connection
*/
close(): void;
}
+346
View File
@@ -0,0 +1,346 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const index_js_1 = __importDefault(require("../smtp-connection/index.js"));
const index_js_2 = require("../shared/index.js");
const index_js_3 = __importDefault(require("../xoauth2/index.js"));
const errors = __importStar(require("../errors.js"));
const node_events_1 = require("node:events");
/**
* Creates an element for the pool
*
* @constructor
* @param pool SMTPPool instance
*/
class PoolResource extends node_events_1.EventEmitter {
constructor(pool) {
super();
this.pool = pool;
this.options = pool.options;
this.logger = this.pool.logger;
if (this.options.auth) {
switch ((this.options.auth.type || '').toString().toUpperCase()) {
case 'OAUTH2': {
const oauth2 = new index_js_3.default(this.options.auth, this.logger);
oauth2.provisionCallback =
(this.pool.mailer && this.pool.mailer.get('oauth2_provision_cb')) || oauth2.provisionCallback;
this.auth = {
type: 'OAUTH2',
user: this.options.auth.user,
oauth2,
method: 'XOAUTH2'
};
oauth2.on('token', (token) => this.pool.mailer.emit('token', token));
oauth2.on('error', err => this._fail(err));
break;
}
default:
if (!this.options.auth.user && !this.options.auth.pass) {
break;
}
this.auth = {
type: (this.options.auth.type || '').toString().toUpperCase() || 'LOGIN',
user: this.options.auth.user,
credentials: {
user: this.options.auth.user || '',
pass: this.options.auth.pass,
options: this.options.auth.options
},
method: (this.options.auth.method || '').trim().toUpperCase() || this.options.authMethod || false
};
}
}
this._connection = false;
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
*
* @param callback Callback function to run once the connection is established or failed
*/
connect(callback) {
// 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._fail(err);
return callback(err);
}
let returned = false;
let options = this.options;
if (socketOptions && socketOptions.connection) {
this.logger.info({
tnx: 'proxy',
remoteAddress: socketOptions.connection.remoteAddress,
remotePort: socketOptions.connection.remotePort,
destHost: options.host || '',
destPort: options.port || '',
action: 'connected'
}, 'Using proxied socket from %s:%s to %s:%s', socketOptions.connection.remoteAddress, socketOptions.connection.remotePort, options.host || '', options.port || '');
options = Object.assign((0, index_js_2.assign)(false, options), socketOptions);
}
this.connection = new index_js_1.default(options);
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;
}
returned = true;
return callback(err);
});
this.connection.once('end', () => {
this.close();
returned = true;
});
this.connection.connect(err => {
if (returned) {
return;
}
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)) {
this.connection.login(this.auth, err => {
if (returned) {
return;
}
returned = true;
if (err) {
this.connection.close();
this._fail(err);
return callback(err);
}
this._connected = true;
callback(null, true);
});
}
else {
returned = true;
this._connected = true;
return callback(null, true);
}
});
});
}
/**
* Sends an e-mail to be sent using the selected settings
*
* @param mail Mail object
* @param callback Callback function
*/
send(mail, callback) {
if (!this._connected) {
return this.connect(err => {
if (err) {
return callback(err);
}
return this.send(mail, callback);
});
}
const envelope = mail.message.getEnvelope();
const messageId = mail.message.messageId();
const recipients = [].concat(envelope.to || []);
if (recipients.length > 3) {
recipients.push('...and ' + recipients.splice(2).length + ' more');
}
this.logger.info({
tnx: 'send',
messageId,
cid: this.id
}, 'Sending message %s using #%s to <%s>', messageId, this.id, recipients.join(', '));
if (mail.data.dsn) {
envelope.dsn = mail.data.dsn;
}
// RFC 8689: Pass requireTLSExtensionEnabled to envelope for MAIL FROM parameter
if (mail.data.requireTLSExtensionEnabled) {
envelope.requireTLSExtensionEnabled = mail.data.requireTLSExtensionEnabled;
}
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._fail(err);
return callback(err);
}
info.envelope = {
from: envelope.from,
to: envelope.to
};
info.messageId = messageId;
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();
}
if (this.connection) {
this.connection.close();
}
this.emit('close');
}
}
exports.default = PoolResource;
module.exports = exports.default;
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
+156
View File
@@ -0,0 +1,156 @@
import { EventEmitter } from 'node:events';
import { type SMTPConnectionOptions, type SMTPConnectionAuth, type SMTPConnectionSendInfo } from '../smtp-connection/index.js';
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, 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.
* OAuth2 settings are handed to XOAuth2 as is
*/
export interface SMTPTransportAuthOptions extends XOAuth2Options {
/** 'OAuth2' selects XOAUTH2, anything else is a password login, 'LOGIN' when not set */
type?: string | undefined;
/** Username */
user?: string | undefined;
/** Password */
pass?: string | undefined;
/** SASL method to use, e.g. 'PLAIN', 'LOGIN' or 'CRAM-MD5' */
method?: string | undefined;
/** Extra options for the authentication method, handed to a custom SASL handler */
options?: {
[key: string]: any;
} | undefined;
/** Service identifier, an OAuth2 login needs either this or a user */
service?: string | undefined;
}
/**
* Authentication data built by getAuth() and handed to SMTPConnection#login
*/
export interface SMTPTransportAuth extends SMTPConnectionAuth {
/** 'OAUTH2', or the upper cased type from the settings, 'LOGIN' when none was given */
type: string;
/** 'XOAUTH2' for OAuth2, otherwise the configured SASL method, false lets the connection pick one */
method: string | false;
}
/**
* 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`. The same callback type Mail hands to a transport, so that the SMTP
* transports stay assignable to the Transport interface
*/
export type SMTPTransportGetSocketCallback = GetSocketCallback;
/**
* Custom socket provider, replaces the getSocket method of the transport
*/
export type SMTPTransportGetSocket = (options: SMTPTransportOptions, callback: SMTPTransportGetSocketCallback) => void;
/**
* Options for the SMTP transport, the connection options plus the transport level settings
*/
export interface SMTPTransportOptions extends SMTPConnectionOptions, TransportOptions {
/** Well-known service name, e.g. 'Gmail', fills in host, port and secure */
service?: string | undefined;
/** Connection url, e.g. 'smtps://user:pass@smtp.example.com', parsed into options */
url?: string | undefined;
/** Authentication settings, no authentication when not set */
auth?: SMTPTransportAuthOptions | undefined;
/** Custom socket provider, e.g. for connecting through a proxy, replaces the getSocket method */
getSocket?: SMTPTransportGetSocket | undefined;
/** Authenticate even when the server does not advertise AUTH, and fail verify() when it does but no credentials were given */
forceAuth?: boolean | undefined;
/** Default SASL method for the password logins when the auth settings do not name one */
authMethod?: string | undefined;
/** Logger component name, defaults to 'smtp-transport' */
component?: string | undefined;
/** Set to true to get a pooled transport from createTransport, this transport does not read it */
pool?: boolean | undefined;
}
/**
* Result of a sent message, the connection result plus the envelope and the Message-ID
*/
export interface SMTPSentMessageInfo extends SMTPConnectionSendInfo, SentMessageInfo {
/** Envelope the message was sent with */
envelope: MimeNodeEnvelope;
/** Message-ID value of the sent message */
messageId: string;
/** Recipients the server accepted */
accepted: string[];
/** Recipients the server rejected */
rejected: string[];
/** Final server response for the message */
response: string;
}
/**
* Callback for send()
*/
export type SMTPTransportSendCallback = ResultCallback<SMTPSentMessageInfo>;
/**
* Creates a SMTP transport object for Nodemailer
*
* @constructor
* @param options Connection options
*/
declare class SMTPTransport extends EventEmitter {
options: SMTPTransportOptions;
logger: shared.Logger;
name: string;
version: string;
/**
* Transport level authentication data, set when the options include auth
*/
auth?: SMTPTransportAuth | false | undefined;
/**
* The Mail instance using this transport, assigned by Mail
*/
mailer?: Mail<SMTPSentMessageInfo, SMTPTransportOptions> | undefined;
constructor(options?: SMTPTransportOptions | string);
/**
* Placeholder function for creating proxy sockets. This method immediatelly returns
* without a socket
*
* @param options Connection options
* @param callback Callback function to run with the socket keys
*/
getSocket(options: SMTPTransportOptions, callback: SMTPTransportGetSocketCallback): void;
getAuth(authOpts?: SMTPTransportAuthOptions | false | null): SMTPTransportAuth | false | undefined;
/**
* Sends an e-mail using the selected settings
*
* @param mail Mail object
* @param callback Callback function
*/
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(callback: VerifyCallback): void;
/**
* Releases resources
*/
close(): void;
}
/**
* Type aliases in the layout of @types/nodemailer, so `SMTPTransport.Options` style references keep working
*/
declare namespace SMTPTransport {
type Options = SMTPTransportOptions;
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;
+388
View File
@@ -0,0 +1,388 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const node_events_1 = require("node:events");
const index_js_1 = __importDefault(require("../smtp-connection/index.js"));
const index_js_2 = __importDefault(require("../well-known/index.js"));
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
*
* @constructor
* @param options Connection options
*/
class SMTPTransport extends node_events_1.EventEmitter {
constructor(options) {
super();
options = options || {};
if (typeof options === 'string') {
options = {
url: options
};
}
let urlData;
let service = options.service;
if (typeof options.getSocket === 'function') {
this.getSocket = options.getSocket;
}
if (options.url) {
urlData = shared.parseConnectionUrl(options.url);
service = service || urlData.service;
}
this.options = shared.assign(false, // create new object
options, // regular options
urlData, // url options
(service && (0, index_js_2.default)(service)) // wellknown options
);
this.logger = shared.getLogger(this.options, {
component: this.options.component || 'smtp-transport'
});
this.name = 'SMTP';
this.version = packageData.version + '[client:' + packageData.version + ']';
if (this.options.auth) {
this.auth = this.getAuth({});
}
}
/**
* Placeholder function for creating proxy sockets. This method immediatelly returns
* without a socket
*
* @param options Connection options
* @param callback Callback function to run with the socket keys
*/
getSocket(options, callback) {
// return immediatelly
setImmediate(() => callback(null, false));
}
getAuth(authOpts) {
if (!authOpts) {
if (this.auth && this.auth.oauth2 && this.mailer) {
// Transport-level auth is resolved in the constructor, before the Mail wrapper
// assigns `this.mailer`, so a provision callback registered with
// `transporter.set('oauth2_provision_cb', ...)` has to be re-checked here
this.auth.oauth2.provisionCallback = this.mailer.get('oauth2_provision_cb') || this.auth.oauth2.provisionCallback;
}
return this.auth;
}
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;
}
switch ((authData.type || '').toString().toUpperCase()) {
case 'OAUTH2': {
if (!authData.service && !authData.user) {
return false;
}
const oauth2 = new index_js_3.default(authData, this.logger);
oauth2.provisionCallback = (this.mailer && this.mailer.get('oauth2_provision_cb')) || oauth2.provisionCallback;
oauth2.on('token', (token) => this.mailer.emit('token', token));
oauth2.on('error', err => this.emit('error', err));
return {
type: 'OAUTH2',
user: authData.user,
oauth2,
method: 'XOAUTH2'
};
}
default:
return {
type: (authData.type || '').toString().toUpperCase() || 'LOGIN',
user: authData.user,
credentials: {
user: authData.user || '',
pass: authData.pass,
options: authData.options
},
method: (authData.method || '').trim().toUpperCase() || this.options.authMethod || false
};
}
}
/**
* Sends an e-mail using the selected settings
*
* @param mail Mail object
* @param callback Callback function
*/
send(mail, 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);
}
let returned = false;
let options = this.options;
if (socketOptions && socketOptions.connection) {
this.logger.info({
tnx: 'proxy',
remoteAddress: socketOptions.connection.remoteAddress,
remotePort: socketOptions.connection.remotePort,
destHost: options.host || '',
destPort: options.port || '',
action: 'connected'
}, 'Using proxied socket from %s:%s to %s:%s', socketOptions.connection.remoteAddress, socketOptions.connection.remotePort, options.host || '', options.port || '');
// only copy options if we need to modify it
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) {
perCallAuth.oauth2.removeAllListeners();
}
perCallAuth = null;
};
connection.once('error', err => {
if (returned) {
return;
}
returned = true;
cleanupPerCallAuth();
connection.close();
return callback(err);
});
const sendMessage = () => {
const envelope = mail.message.getEnvelope();
const messageId = mail.message.messageId();
const recipients = [].concat(envelope.to || []);
if (recipients.length > 3) {
recipients.push('...and ' + recipients.splice(2).length + ' more');
}
if (mail.data.dsn) {
envelope.dsn = mail.data.dsn;
}
// RFC 8689: Pass requireTLSExtensionEnabled to envelope for MAIL FROM parameter
if (mail.data.requireTLSExtensionEnabled) {
envelope.requireTLSExtensionEnabled = mail.data.requireTLSExtensionEnabled;
}
this.logger.info({
tnx: 'send',
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();
if (err) {
this.logger.error({
err,
tnx: 'send'
}, 'Send error for %s: %s', messageId, err.message);
return callback(err);
}
info.envelope = {
from: envelope.from,
to: envelope.to
};
info.messageId = messageId;
try {
return callback(null, info);
}
catch (E) {
this.logger.error({
err: E,
tnx: 'callback'
}, 'Callback error for %s: %s', messageId, E.message);
}
});
};
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 => {
cleanupPerCallAuth();
if (returned) {
return;
}
if (err) {
returned = true;
connection.close();
return callback(err);
}
sendMessage();
});
}
else {
sendMessage();
}
});
});
}
verify(callback) {
let promise;
if (!callback) {
promise = new Promise((resolve, reject) => {
callback = shared.callbackPromise(resolve, reject);
});
}
// 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 done(err);
}
let options = this.options;
if (socketOptions && socketOptions.connection) {
this.logger.info({
tnx: 'proxy',
remoteAddress: socketOptions.connection.remoteAddress,
remotePort: socketOptions.connection.remotePort,
destHost: options.host || '',
destPort: options.port || '',
action: 'connected'
}, 'Using proxied socket from %s:%s to %s:%s', socketOptions.connection.remoteAddress, socketOptions.connection.remotePort, options.host || '', options.port || '');
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 = () => {
if (perCallAuth && perCallAuth !== this.auth && perCallAuth.oauth2) {
perCallAuth.oauth2.removeAllListeners();
}
perCallAuth = null;
};
connection.once('error', err => {
if (returned) {
return;
}
returned = true;
cleanupPerCallAuth();
connection.close();
return done(err);
});
connection.once('end', () => {
if (returned) {
return;
}
returned = true;
cleanupPerCallAuth();
return done(new Error('Connection closed'));
});
const finalize = () => {
if (returned) {
return;
}
returned = true;
cleanupPerCallAuth();
connection.quit();
return done(null, true);
};
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 => {
cleanupPerCallAuth();
if (returned) {
return;
}
if (err) {
returned = true;
connection.close();
return done(err);
}
finalize();
});
}
else if (!perCallAuth && connection.allowsAuth && options.forceAuth) {
const err = new Error('Authentication info was not provided');
err.code = errors.ENOAUTH;
returned = true;
cleanupPerCallAuth();
connection.close();
return done(err);
}
else {
finalize();
}
});
});
return promise;
}
/**
* Releases resources
*/
close() {
if (this.auth && this.auth.oauth2) {
this.auth.oauth2.removeAllListeners();
}
this.emit('close');
}
}
exports.default = SMTPTransport;
module.exports = exports.default;
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
+65
View File
@@ -0,0 +1,65 @@
import type { Readable } from 'node:stream';
import type { Logger } from '../shared/index.js';
import type { MimeNodeEnvelope } from '../mime-node/index.js';
import type MailMessage from '../mailer/mail-message.js';
import type { default as Mail, SentMessageInfo, SendMailOptions, TransportOptions } from '../mailer/index.js';
/**
* Options for the Stream transport
*/
export interface StreamTransportOptions extends TransportOptions {
/** Selects this transport in createTransport */
streamTransport?: boolean | undefined;
/** If true, the message is returned as a Buffer object instead of a stream */
buffer?: boolean | undefined;
/** Either 'windows' or 'unix', the line ending of the generated message */
newline?: string | undefined;
}
/**
* The value the Stream transport hands to the send callback
*/
export interface StreamSentMessageInfo extends SentMessageInfo {
/** The envelope the message was generated with */
envelope: MimeNodeEnvelope;
/** Message-ID value of the message */
messageId: string;
/** The generated message, a Buffer when the buffer option is set, a readable stream otherwise */
message: Readable | Buffer;
}
/**
* Generates a Transport object for streaming
*
* Possible options can be the following:
*
* * **buffer** if true, then returns the message as a Buffer object instead of a stream
* * **newline** either 'windows' or 'unix'
*
* @constructor
* @param optional config parameter
*/
declare class StreamTransport {
mailer: Mail<StreamSentMessageInfo>;
options: StreamTransportOptions;
name: string;
version: string;
logger: Logger;
winbreak: boolean;
constructor(options?: StreamTransportOptions);
/**
* Compiles a mailcomposer message and forwards it to handler that sends it
*
* @param mail MailComposer object
* @param done Callback function to run when the sending is completed
*/
send(mail: MailMessage<StreamSentMessageInfo>, done: (err: Error | null, info?: StreamSentMessageInfo) => void): void;
}
/**
* Type aliases in the layout of @types/nodemailer, so `StreamTransport.Options` style references keep working
*/
declare namespace StreamTransport {
type Options = StreamTransportOptions;
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;
+146
View File
@@ -0,0 +1,146 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const packageData = __importStar(require("../package-info.js"));
const shared = __importStar(require("../shared/index.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"));
/**
* Generates a Transport object for streaming
*
* Possible options can be the following:
*
* * **buffer** if true, then returns the message as a Buffer object instead of a stream
* * **newline** either 'windows' or 'unix'
*
* @constructor
* @param optional config parameter
*/
class StreamTransport {
constructor(options) {
options = options || {};
this.options = options;
this.name = 'StreamTransport';
this.version = packageData.version;
this.logger = shared.getLogger(this.options, {
component: this.options.component || 'stream-transport'
});
this.winbreak = ['win', 'windows', 'dos', '\r\n'].includes((options.newline || '').toString().toLowerCase());
}
/**
* Compiles a mailcomposer message and forwards it to handler that sends it
*
* @param mail MailComposer object
* @param done Callback function to run when the sending is completed
*/
send(mail, done) {
// We probably need this in the output. send() runs after the message was compiled,
// so mail.message is set
mail.message.keepBcc = true;
const envelope = mail.message.getEnvelope();
const messageId = mail.message.messageId();
const recipients = [].concat(envelope.to || []);
if (recipients.length > 3) {
recipients.push('...and ' + recipients.splice(2).length + ' more');
}
this.logger.info({
tnx: 'send',
messageId
}, 'Sending message %s to <%s> using %s line breaks', messageId, recipients.join(', '), this.winbreak ? '<CR><LF>' : '<LF>');
setImmediate(() => {
let stream;
try {
stream = mail.message.createReadStream();
if (this.options.newline) {
// apply the transport-level line ending transform; the message-level
// `newline` option is handled by MimeNode in createReadStream()
const sourceStream = stream;
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));
}
}
catch (E) {
this.logger.error({
err: E,
tnx: 'send',
messageId
}, 'Creating send stream failed for %s. %s', messageId, E.message);
return done(E);
}
if (!this.options.buffer) {
stream.once('error', err => {
this.logger.error({
err,
tnx: 'send',
messageId
}, 'Failed creating message for %s. %s', messageId, err.message);
});
return done(null, {
envelope,
messageId,
message: stream
});
}
const chunks = [];
let chunklen = 0;
stream.on('readable', () => {
let chunk;
while ((chunk = stream.read()) !== null) {
chunks.push(chunk);
chunklen += chunk.length;
}
});
stream.once('error', err => {
this.logger.error({
err,
tnx: 'send',
messageId
}, 'Failed creating message for %s. %s', messageId, err.message);
return done(err);
});
stream.on('end', () => done(null, {
envelope,
messageId,
message: Buffer.concat(chunks, chunklen)
}));
});
}
}
exports.default = StreamTransport;
module.exports = exports.default;
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
+35
View File
@@ -0,0 +1,35 @@
/**
* Connection settings of a well-known e-mail service
*/
export interface WellKnownServiceDefinition {
/** Human readable description of the service */
description?: string | undefined;
/** Domains of e-mail addresses hosted by the service */
domains?: string[] | undefined;
/** Alternative names the service can be looked up by */
aliases?: string[] | undefined;
/** SMTP hostname */
host?: string | undefined;
/** SMTP port (a numeric string in a few entries) */
port?: number | string | undefined;
/** true for implicit TLS (usually port 465) */
secure?: boolean | undefined;
/** Preferred authentication method */
authMethod?: string | undefined;
/** Set to true to require STARTTLS on a plaintext connection */
requireTLS?: boolean | undefined;
/** Set to true to skip STARTTLS */
ignoreTLS?: boolean | undefined;
}
/**
* SMTP settings of a well-known service, without the lookup keys
*/
export type WellKnownService = Omit<WellKnownServiceDefinition, 'domains' | 'aliases'>;
/**
* Resolves SMTP config for given key. Key can be a name (like 'Gmail'), alias (like 'Google Mail') or
* an email address (like 'test@googlemail.com').
*
* @param key Service name, alias or an email address
* @returns SMTP config or false if not found
*/
export default function wellKnown(key: string): WellKnownService | false;
+41
View File
@@ -0,0 +1,41 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = wellKnown;
const services_js_1 = require("./services.js");
const normalized = {};
Object.keys(services_js_1.services).forEach(key => {
const service = services_js_1.services[key];
const normalizedService = normalizeService(service);
normalized[normalizeKey(key)] = normalizedService;
[].concat(service.aliases || []).forEach(alias => {
normalized[normalizeKey(alias)] = normalizedService;
});
[].concat(service.domains || []).forEach(domain => {
normalized[normalizeKey(domain)] = normalizedService;
});
});
function normalizeKey(key) {
return key.replace(/[^a-zA-Z0-9.-]/g, '').toLowerCase();
}
function normalizeService(service) {
const response = {};
Object.keys(service).forEach(key => {
if (!['domains', 'aliases'].includes(key)) {
response[key] = service[key];
}
});
return response;
}
/**
* Resolves SMTP config for given key. Key can be a name (like 'Gmail'), alias (like 'Google Mail') or
* an email address (like 'test@googlemail.com').
*
* @param key Service name, alias or an email address
* @returns SMTP config or false if not found
*/
function wellKnown(key) {
key = normalizeKey(key.split('@').pop());
return normalized[key] || false;
}
module.exports = exports.default;
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
Loaded 100 of 210 files, more files were not shown because too many files have changed in this diff. Show more