diff --git a/.env b/.env index 9c94a29..2a09a9c 100644 --- a/.env +++ b/.env @@ -7,4 +7,6 @@ DB_PASSWORD=RLR+2DHmZ6XJGotT2F7Ylrpdptc DB_NAME=dok OFFLINE_SERVER_1=Test Server Alpha -OFFLINE_SERVER_2=Test Server Beta \ No newline at end of file +OFFLINE_SERVER_2=Test Server Beta + +APP_URL=https://spiel.dynastyofknights.com \ No newline at end of file diff --git a/app.js b/app.js index de2a14f..f29490f 100644 --- a/app.js +++ b/app.js @@ -6,6 +6,8 @@ const helmet = require("helmet"); const rateLimit = require("express-rate-limit"); const serverRoutes = require("./routes/servers"); +const registerRoutes = require("./routes/register"); +const verifyRoutes = require("./routes/verify"); const app = express(); const PORT = process.env.PORT || 3000; @@ -48,6 +50,8 @@ app.use(express.static(path.join(__dirname, "public"))); ======================== */ app.use("/", serverRoutes); +app.use("/register", registerRoutes); +app.use("/verify", verifyRoutes); /* ======================== 404 Handler diff --git a/node_modules/.bin/node-gyp-build b/node_modules/.bin/node-gyp-build new file mode 100644 index 0000000..b804ba9 --- /dev/null +++ b/node_modules/.bin/node-gyp-build @@ -0,0 +1,16 @@ +#!/bin/sh +basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir=`cygpath -w "$basedir"` + fi + ;; +esac + +if [ -x "$basedir/node" ]; then + exec "$basedir/node" "$basedir/../node-gyp-build/bin.js" "$@" +else + exec node "$basedir/../node-gyp-build/bin.js" "$@" +fi diff --git a/node_modules/.bin/node-gyp-build-optional b/node_modules/.bin/node-gyp-build-optional new file mode 100644 index 0000000..cb670aa --- /dev/null +++ b/node_modules/.bin/node-gyp-build-optional @@ -0,0 +1,16 @@ +#!/bin/sh +basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir=`cygpath -w "$basedir"` + fi + ;; +esac + +if [ -x "$basedir/node" ]; then + exec "$basedir/node" "$basedir/../node-gyp-build/optional.js" "$@" +else + exec node "$basedir/../node-gyp-build/optional.js" "$@" +fi diff --git a/node_modules/.bin/node-gyp-build-optional.cmd b/node_modules/.bin/node-gyp-build-optional.cmd new file mode 100644 index 0000000..74d85f2 --- /dev/null +++ b/node_modules/.bin/node-gyp-build-optional.cmd @@ -0,0 +1,17 @@ +@ECHO off +GOTO start +:find_dp0 +SET dp0=%~dp0 +EXIT /b +:start +SETLOCAL +CALL :find_dp0 + +IF EXIST "%dp0%\node.exe" ( + SET "_prog=%dp0%\node.exe" +) ELSE ( + SET "_prog=node" + SET PATHEXT=%PATHEXT:;.JS;=;% +) + +endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\node-gyp-build\optional.js" %* diff --git a/node_modules/.bin/node-gyp-build-optional.ps1 b/node_modules/.bin/node-gyp-build-optional.ps1 new file mode 100644 index 0000000..45995c3 --- /dev/null +++ b/node_modules/.bin/node-gyp-build-optional.ps1 @@ -0,0 +1,28 @@ +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir/node$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/node$exe" "$basedir/../node-gyp-build/optional.js" $args + } else { + & "$basedir/node$exe" "$basedir/../node-gyp-build/optional.js" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "node$exe" "$basedir/../node-gyp-build/optional.js" $args + } else { + & "node$exe" "$basedir/../node-gyp-build/optional.js" $args + } + $ret=$LASTEXITCODE +} +exit $ret diff --git a/node_modules/.bin/node-gyp-build-test b/node_modules/.bin/node-gyp-build-test new file mode 100644 index 0000000..bdf6dca --- /dev/null +++ b/node_modules/.bin/node-gyp-build-test @@ -0,0 +1,16 @@ +#!/bin/sh +basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir=`cygpath -w "$basedir"` + fi + ;; +esac + +if [ -x "$basedir/node" ]; then + exec "$basedir/node" "$basedir/../node-gyp-build/build-test.js" "$@" +else + exec node "$basedir/../node-gyp-build/build-test.js" "$@" +fi diff --git a/node_modules/.bin/node-gyp-build-test.cmd b/node_modules/.bin/node-gyp-build-test.cmd new file mode 100644 index 0000000..182a757 --- /dev/null +++ b/node_modules/.bin/node-gyp-build-test.cmd @@ -0,0 +1,17 @@ +@ECHO off +GOTO start +:find_dp0 +SET dp0=%~dp0 +EXIT /b +:start +SETLOCAL +CALL :find_dp0 + +IF EXIST "%dp0%\node.exe" ( + SET "_prog=%dp0%\node.exe" +) ELSE ( + SET "_prog=node" + SET PATHEXT=%PATHEXT:;.JS;=;% +) + +endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\node-gyp-build\build-test.js" %* diff --git a/node_modules/.bin/node-gyp-build-test.ps1 b/node_modules/.bin/node-gyp-build-test.ps1 new file mode 100644 index 0000000..6cb0b9b --- /dev/null +++ b/node_modules/.bin/node-gyp-build-test.ps1 @@ -0,0 +1,28 @@ +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir/node$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/node$exe" "$basedir/../node-gyp-build/build-test.js" $args + } else { + & "$basedir/node$exe" "$basedir/../node-gyp-build/build-test.js" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "node$exe" "$basedir/../node-gyp-build/build-test.js" $args + } else { + & "node$exe" "$basedir/../node-gyp-build/build-test.js" $args + } + $ret=$LASTEXITCODE +} +exit $ret diff --git a/node_modules/.bin/node-gyp-build.cmd b/node_modules/.bin/node-gyp-build.cmd new file mode 100644 index 0000000..ac854a6 --- /dev/null +++ b/node_modules/.bin/node-gyp-build.cmd @@ -0,0 +1,17 @@ +@ECHO off +GOTO start +:find_dp0 +SET dp0=%~dp0 +EXIT /b +:start +SETLOCAL +CALL :find_dp0 + +IF EXIST "%dp0%\node.exe" ( + SET "_prog=%dp0%\node.exe" +) ELSE ( + SET "_prog=node" + SET PATHEXT=%PATHEXT:;.JS;=;% +) + +endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\node-gyp-build\bin.js" %* diff --git a/node_modules/.bin/node-gyp-build.ps1 b/node_modules/.bin/node-gyp-build.ps1 new file mode 100644 index 0000000..c1f9a9a --- /dev/null +++ b/node_modules/.bin/node-gyp-build.ps1 @@ -0,0 +1,28 @@ +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir/node$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/node$exe" "$basedir/../node-gyp-build/bin.js" $args + } else { + & "$basedir/node$exe" "$basedir/../node-gyp-build/bin.js" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "node$exe" "$basedir/../node-gyp-build/bin.js" $args + } else { + & "node$exe" "$basedir/../node-gyp-build/bin.js" $args + } + $ret=$LASTEXITCODE +} +exit $ret diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json index 6b1d52f..60baeac 100644 --- a/node_modules/.package-lock.json +++ b/node_modules/.package-lock.json @@ -185,6 +185,29 @@ ], "license": "MIT" }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/bcrypt/node_modules/node-addon-api": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.6.0.tgz", + "integrity": "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/better-sqlite3": { "version": "12.6.2", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.6.2.tgz", @@ -483,6 +506,13 @@ "node": ">=6.6.0" } }, + "node_modules/crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", + "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.", + "license": "ISC" + }, "node_modules/csrf": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz", @@ -1732,6 +1762,26 @@ "node": ">= 10.12.0" } }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/nodemailer": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.2.tgz", + "integrity": "sha512-zbj002pZAIkWQFxyAaqoxvn+zoIwRnS40hgjqTXudKOOJkiFFgBeNqjgD3/YCR12sZnrghWYBY+yP1ZucdDRpw==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", diff --git a/node_modules/bcrypt/.dockerignore b/node_modules/bcrypt/.dockerignore new file mode 100644 index 0000000..01f4eb7 --- /dev/null +++ b/node_modules/bcrypt/.dockerignore @@ -0,0 +1,6 @@ +.git/ +.vscode/ +Dockerfile* +prebuilds/ +node_modules/ +build*/ diff --git a/node_modules/bcrypt/.editorconfig b/node_modules/bcrypt/.editorconfig new file mode 100644 index 0000000..4e12f93 --- /dev/null +++ b/node_modules/bcrypt/.editorconfig @@ -0,0 +1,19 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[{package.json,*.yml}] +indent_style = space +indent_size = 2 + +[appveyor.yml] +end_of_line = crlf + +[*.md] +trim_trailing_whitespace = false diff --git a/node_modules/bcrypt/.github/workflows/build-pack-publish.yml b/node_modules/bcrypt/.github/workflows/build-pack-publish.yml new file mode 100644 index 0000000..9b14ee1 --- /dev/null +++ b/node_modules/bcrypt/.github/workflows/build-pack-publish.yml @@ -0,0 +1,110 @@ +name: Prebuildify, package, publish + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + release: + types: [ prereleased, released ] + +jobs: + build-linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + # This is unsafe, but we really don't use any other native dependencies + - run: npm ci + - run: docker run -u $(id -u):$(id -g) -v `pwd`:/input -w /input ghcr.io/prebuild/almalinux-devtoolset11 npx prebuildify --napi --tag-libc --strip --target=node@18.0.0 + - run: docker run -u $(id -u):$(id -g) -v `pwd`:/input -w /input ghcr.io/prebuild/alpine npx prebuildify --napi --tag-libc --strip --target=node@18.0.0 + - run: docker run -u $(id -u):$(id -g) -v `pwd`:/input -w /input ghcr.io/prebuild/linux-armv7 npx prebuildify --napi --tag-libc --strip --target=node@18.0.0 + - run: docker run -u $(id -u):$(id -g) -v `pwd`:/input -w /input ghcr.io/prebuild/linux-armv7l-musl npx prebuildify --napi --tag-libc --strip --target=node@18.0.0 + - run: docker run -u $(id -u):$(id -g) -v `pwd`:/input -w /input ghcr.io/prebuild/linux-arm64 npx prebuildify --napi --tag-libc --strip --target=node@18.0.0 + - run: docker run -u $(id -u):$(id -g) -v `pwd`:/input -w /input ghcr.io/prebuild/linux-arm64-musl npx prebuildify --napi --tag-libc --strip --target=node@18.0.0 + - run: find prebuilds + - uses: actions/upload-artifact@v4 + with: + name: prebuild-linux + path: ./prebuilds/ + + build-windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: 'https://registry.npmjs.org' + - run: npm ci + - run: npx prebuildify --napi --strip --arch=x64 --target=node@18.0.0 + - run: npx prebuildify --napi --strip --arch=arm64 --target=node@20.0.0 + - run: dir prebuilds + - uses: actions/upload-artifact@v4 + with: + name: prebuild-windows + path: ./prebuilds/ + + build-macos: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18 + registry-url: 'https://registry.npmjs.org' + - run: npm ci + - run: npx prebuildify --napi --strip --arch=arm64 --target=node@18.0.0 + - run: npx prebuildify --napi --strip --arch=x64 --target=node@18.0.0 + - run: find prebuilds + - uses: actions/upload-artifact@v4 + with: + name: prebuild-macos + path: ./prebuilds/ + + pack: + needs: + - build-linux + - build-windows + - build-macos + runs-on: ubuntu-latest + outputs: + PACK_FILE: ${{ steps.pack.outputs.PACK_FILE }} + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + path: /tmp/prebuilds/ + - name: Coalesce prebuilds from build matrix + run: | + mkdir prebuilds + for d in /tmp/prebuilds/*; do + cp -Rav $d/* prebuilds/ + done + - run: chmod a+x prebuilds/*/*.node && find prebuilds -executable -type f + - id: pack + name: Prepare NPM package + run: | + echo "PACK_FILE=$(npm pack)" >> "$GITHUB_OUTPUT" + - uses: actions/upload-artifact@v4 + with: + name: package-tgz + path: ${{ steps.pack.outputs.PACK_FILE }} + if-no-files-found: 'error' + + test-package: + needs: pack + strategy: + matrix: + node-version: [ 18, 20, 22, 23 ] + os: [ ubuntu-latest, windows-latest, macos-latest ] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + registry-url: 'https://registry.npmjs.org' + - uses: actions/download-artifact@v4 + with: + name: package-tgz + - run: npm install ${{ needs.pack.outputs.PACK_FILE }} + - run: node -e "const b = require('bcrypt'); const h = b.hashSync('hello', 10); console.log(h, b.compareSync('hello', h))" diff --git a/node_modules/bcrypt/.github/workflows/ci.yaml b/node_modules/bcrypt/.github/workflows/ci.yaml new file mode 100644 index 0000000..77c4e5a --- /dev/null +++ b/node_modules/bcrypt/.github/workflows/ci.yaml @@ -0,0 +1,42 @@ +name: ci + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18, 20, 22] + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - run: npm ci + - name: Test + run: npm test + + build-alpine: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18, 20, 22] + container: + image: node:${{ matrix.node-version }}-alpine + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + run: | + apk add make g++ python3 + - run: npm ci + - name: Test + run: | + npm test --unsafe-perm diff --git a/node_modules/bcrypt/CHANGELOG.md b/node_modules/bcrypt/CHANGELOG.md new file mode 100644 index 0000000..eab713b --- /dev/null +++ b/node_modules/bcrypt/CHANGELOG.md @@ -0,0 +1,184 @@ +# 6.0.0 (2025-02-28) + * Drop support for NodeJS <= 16 + * Remove `node-pre-gyp` in favor of `prebuildify`, prebuilt binaries are now shipped with the package + * Update `node-addon-api` to 8.3.0 + * Update JS code to newer ES syntax + +# 5.1.0 (2022-10-06) + * Update `node-pre-gyp` to 1.0.11 + +# 5.1.0 (2022-10-06) + * Update `node-pre-gyp` to 1.0.10 + * Replace `nodeunit` with `jest` as the testing library + +# 5.0.1 (2021-02-22) + + * Update `node-pre-gyp` to 1.0.0 + +# 5.0.0 (2020-06-02) + + * Fix the bcrypt "wrap-around" bug. It affects passwords with lengths >= 255. + It is uncommon but it's a bug nevertheless. Previous attempts to fix the bug + was unsuccessful. + * Experimental support for z/OS + * Fix a bug related to NUL in password input + * Update `node-pre-gyp` to 0.15.0 + +# 4.0.1 (2020-02-27) + + * Fix compilation errors in Alpine linux + +# 4.0.0 (2020-02-17) + + * Switch to NAPI bcrypt + * Drop support for NodeJS 8 + +# 3.0.8 (2019-12-31) + + * Update `node-pre-gyp` to 0.14 + * Pre-built binaries for NodeJS 13 + +# 3.0.7 (2019-10-18) + + * Update `nan` to 2.14.0 + * Update `node-pre-gyp` to 0.13 + +# 3.0.6 (2019-04-11) + + * Update `nan` to 2.13.2 + +# 3.0.5 (2019-03-19) + + * Update `nan` to 2.13.1 + * NodeJS 12 compatibility + * Remove `node-pre-gyp` from bundled dependencies + +# 3.0.4-napi (2019-03-08) + + * Sync N-API bcrypt with NAN bcrypt + +# 3.0.4 (2019-02-07) + + * Fix GCC, NAN and V8 deprecation warnings + +# 3.0.3 (2018-12-19) + + * Update `nan` to 2.12.1 + +# 3.0.2 (2018-10-18) + + * Update `nan` to 2.11.1 + +# 3.0.1 (2018-09-20) + + * Update `nan` to 2.11.0 + +# 3.0.0 (2018-07-06) + + * Drop support for NodeJS <= 4 + +# 2.0.1 (2018-04-20) + + * Update `node-pre-gyp` to allow downloading prebuilt modules + +# 2.0.0 (2018-04-07) + + * Make `2b` the default bcrypt version + +# 1.1.0-napi (2018-01-21) + + * Initial support for [N-API](https://nodejs.org/api/n-api.html) + +# 1.0.3 (2016-08-23) + + * update to nan v2.6.2 for NodeJS 8 support + * Fix: use npm scripts instead of node-gyp directly. + +# 1.0.2 (2016-12-31) + + * Fix `compare` promise rejection with invalid arguments + +# 1.0.1 (2016-12-07) + + * Fix destructuring imports with promises + +# 1.0.0 (2016-12-04) + + * add Promise support (commit 2488473) + +# 0.8.7 (2016-06-09) + + * update nan to 2.3.5 for improved node v6 support + +# 0.8.6 (2016-04-20) + + * update nan for node v6 support + +# 0.8.5 (2015-08-12) + + * update to nan v2 (adds support for iojs 3) + +# 0.8.4 (2015-07-24) + + * fix deprecation warning for the Encode API + +# 0.8.3 (2015-05-06) + + * update nan to 1.8.4 for iojs 2.x support + +# 0.8.2 (2015-03-28) + + * always use callback for generating random bytes to avoid blocking + +# 0.8.1 (2015-01-18) + * update NaN to 1.5.0 for iojs support + +# 0.8.0 (2014-08-03) + * migrate to NAN for bindings + +# v0.5.0 + * Fix for issue around empty string params throwing Errors. + * Method deprecation. + * Upgrade from libeio/ev to libuv. (shtylman) + ** --- NOTE --- Breaks 0.4.x compatability + * EV_MULTIPLICITY compile flag. + +# v0.4.1 + * Thread safety fix around OpenSSL (GH-32). (bnoordhuis - through node) + * C++ code changes using delete and new instead of malloc and free. (shtylman) + * Compile options for speed, zoom. (shtylman) + * Move much of the type and variable checking to the JS. (shtylman) + +# v0.4.0 + * Added getRounds function that will tell you the number of rounds within a hash/salt + +# v0.3.2 + * Fix api issue with async salt gen first param + +# v0.3.1 + * Compile under node 0.5.x + +# v0.3.0 + * Internal Refactoring + * Remove pthread dependencies and locking + * Fix compiler warnings and a memory bug + +# v0.2.4 + * Use threadsafe functions instead of pthread mutexes + * salt validation to make sure the salt is of the correct size and format + +# v0.2.3 + * cygwin support + +# v0.2.2 + * Remove dependency on libbsd, use libssl instead + +# v0.2.0 + * Added async functionality + * API changes + * hashpw -> encrypt + * all old sync methods now end with _sync + * Removed libbsd(arc4random) dependency...now uses openssl which is more widely spread + +# v0.1.2 + * Security fix. Wasn't reading rounds in properly and was always only using 4 rounds diff --git a/node_modules/bcrypt/Dockerfile b/node_modules/bcrypt/Dockerfile new file mode 100644 index 0000000..2802baf --- /dev/null +++ b/node_modules/bcrypt/Dockerfile @@ -0,0 +1,57 @@ +# Usage: +# +# docker build -t bcryptjs-builder . +# CONTAINER=$(docker create bcryptjs-builder) +# # Then copy the artifact to your host: +# docker cp "$CONTAINER:/usr/local/opt/bcrypt-js/prebuilds" . +# docker rm "$CONTAINER" +# +# Use --platform to build cross-platform i.e. for ARM: +# +# docker build -t bcryptjs-builder --platform "linux/arm64/v8" . +# CONTAINER=$docker create --platform "linux/arm64/v8" bcryptjs-builder) +# # this copies the prebuilds/linux-arm artifacts +# docker cp "$CONTAINER:/usr/local/opt/bcrypt-js/prebuilds" . +# docker rm "$CONTAINER" + + +ARG FROM_IMAGE=node:18-bullseye +#ARG FROM_IMAGE=arm32v7/node:16-bullseye +#ARG FROM_IMAGE=arm64v8/node:16-bullseye +FROM ${FROM_IMAGE} + +ENV project bcrypt-js +ENV DEBIAN_FRONTEND noninteractive +ENV LC_ALL en_US.UTF-8 +ENV LANG ${LC_ALL} + +RUN echo "#log: ${project}: Setup system" \ + && set -x \ + && apt-get update -y \ + && apt-get install -y \ + build-essential \ + python3 \ + && apt-get clean \ + && update-alternatives --install /usr/local/bin/python python /usr/bin/python3 20 \ + && npm i -g prebuildify@5 node-gyp@9 \ + && sync + +ADD . /usr/local/opt/${project} +WORKDIR /usr/local/opt/${project} + +RUN echo "#log: ${project}: Running build" \ + && set -x \ + && npm ci \ + && npm run build + +ARG RUN_TESTS=true +ARG TEST_TIMEOUT_SECONDS= + +RUN if "${RUN_TESTS}"; then \ + echo "#log ${project}: Running tests" \ + && npm test; \ + else \ + echo "#log ${project}: Tests were skipped!"; \ + fi + +CMD /bin/bash -l diff --git a/node_modules/bcrypt/Dockerfile-alpine b/node_modules/bcrypt/Dockerfile-alpine new file mode 100644 index 0000000..7570cfe --- /dev/null +++ b/node_modules/bcrypt/Dockerfile-alpine @@ -0,0 +1,41 @@ +# Usage: +# +# docker build -t bcryptjs-linux-alpine-builder -f Dockerfile-alpine . +# CONTAINER=$(docker create bcryptjs-linux-alpine-builder) +# # Then copy the artifact to your host: +# docker cp "$CONTAINER:/usr/local/opt/bcrypt-js/prebuilds" . +# docker rm "$CONTAINER" + +ARG FROM_IMAGE=node:18-alpine +FROM ${FROM_IMAGE} + +ENV project bcrypt-js +ENV DEBIAN_FRONTEND noninteractive +ENV LC_ALL en_US.UTF-8 +ENV LANG ${LC_ALL} + +RUN echo "#log: ${project}: Setup system" \ + && set -x \ + && apk add --update build-base python3 \ + && npm i -g prebuildify@5 node-gyp@9 \ + && sync + +ADD . /usr/local/opt/${project} +WORKDIR /usr/local/opt/${project} + +RUN echo "#log: ${project}: Running build" \ + && set -x \ + && npm ci \ + && npm run build + +ARG RUN_TESTS=true +ARG TEST_TIMEOUT_SECONDS= + +RUN if "${RUN_TESTS}"; then \ + echo "#log ${project}: Running tests" \ + && npm test; \ + else \ + echo "#log ${project}: Tests were skipped!"; \ + fi + +CMD /bin/bash -l diff --git a/node_modules/bcrypt/ISSUE_TEMPLATE.md b/node_modules/bcrypt/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..b4baa00 --- /dev/null +++ b/node_modules/bcrypt/ISSUE_TEMPLATE.md @@ -0,0 +1,18 @@ +Thanks for reporting a new issue with the node bcrypt module! + +To help you resolve your issue faster please make sure you have done the following: + +* Searched existing issues (even closed ones) for your same problem +* Make sure you have installed the required dependencies listed on the readme +* Read your npm error log for lines telling you what failed, usually it is a problem with not having the correct dependencies installed to build the native module + +Once you have done the above and are still confident that the issue is with the module, please describe it below. Some things that really help get your issue resolved faster are: + +* What went wrong? +* What did you expect to happen? +* Which version of nodejs and OS? +* If you find a bug, please write a failing test. + +Thanks! + +P.S. If it doesn't look like you read the above then your issue will likely be closed without further explanation. Sorry, but there are just too many issues opened with no useful information or questions which have been previously addressed. diff --git a/node_modules/bcrypt/LICENSE b/node_modules/bcrypt/LICENSE new file mode 100644 index 0000000..94e2ba5 --- /dev/null +++ b/node_modules/bcrypt/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2010 Nicholas Campbell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/node_modules/bcrypt/Makefile b/node_modules/bcrypt/Makefile new file mode 100644 index 0000000..cb22252 --- /dev/null +++ b/node_modules/bcrypt/Makefile @@ -0,0 +1,19 @@ +TESTS = test/*.js + +all: test + +build: clean compile + +compile: + npm install . + npm run install + +test: build + @./node_modules/.bin/jest \ + $(TESTS) + +clean: + rm -Rf lib/bindings/ + + +.PHONY: clean test build diff --git a/node_modules/bcrypt/README.md b/node_modules/bcrypt/README.md new file mode 100644 index 0000000..e923108 --- /dev/null +++ b/node_modules/bcrypt/README.md @@ -0,0 +1,388 @@ +# node.bcrypt.js + +[![ci](https://github.com/kelektiv/node.bcrypt.js/actions/workflows/ci.yaml/badge.svg)](https://github.com/kelektiv/node.bcrypt.js/actions/workflows/ci.yaml) + +[![Build Status](https://ci.appveyor.com/api/projects/status/github/kelektiv/node.bcrypt.js)](https://ci.appveyor.com/project/defunctzombie/node-bcrypt-js-pgo26/branch/master) + +A library to help you hash passwords. + +You can read about [bcrypt in Wikipedia][bcryptwiki] as well as in the following article: +[How To Safely Store A Password][codahale] + +## If You Are Submitting Bugs or Issues + +Please verify that the NodeJS version you are using is a _stable_ version; Unstable versions are currently not supported and issues created while using an unstable version will be closed. + +If you are on a stable version of NodeJS, please provide a sufficient code snippet or log files for installation issues. The code snippet does not require you to include confidential information. However, it must provide enough information so the problem can be replicable, or it may be closed without an explanation. + + +## Version Compatibility + +_Please upgrade to atleast v5.0.0 to avoid security issues mentioned below._ + +| Node Version | Bcrypt Version | +| -------------- | ------------------| +| 0.4 | <= 0.4 | +| 0.6, 0.8, 0.10 | >= 0.5 | +| 0.11 | >= 0.8 | +| 4 | <= 2.1.0 | +| 8 | >= 1.0.3 < 4.0.0 | +| 10, 11 | >= 3 | +| 12 onwards | >= 3.0.6 | + +`node-gyp` only works with stable/released versions of node. Since the `bcrypt` module uses `node-gyp` to build and install, you'll need a stable version of node to use bcrypt. If you do not, you'll likely see an error that starts with: + +``` +gyp ERR! stack Error: "pre" versions of node cannot be installed, use the --nodedir flag instead +``` + +## Security Issues And Concerns + +> Per bcrypt implementation, only the first 72 bytes of a string are used. Any extra bytes are ignored when matching passwords. Note that this is not the first 72 *characters*. It is possible for a string to contain less than 72 characters, while taking up more than 72 bytes (e.g. a UTF-8 encoded string containing emojis). If a string is provided, it will be encoded using UTF-8. + +As should be the case with any security tool, anyone using this library should scrutinise it. If you find or suspect an issue with the code, please bring it to the maintainers' attention. We will spend some time ensuring that this library is as secure as possible. + +Here is a list of BCrypt-related security issues/concerns that have come up over the years. + +* An [issue with passwords][jtr] was found with a version of the Blowfish algorithm developed for John the Ripper. This is not present in the OpenBSD version and is thus not a problem for this module. HT [zooko][zooko]. +* Versions `< 5.0.0` suffer from bcrypt wrap-around bug and _will truncate passwords >= 255 characters leading to severely weakened passwords_. Please upgrade at earliest. See [this wiki page][wrap-around-bug] for more details. +* Versions `< 5.0.0` _do not handle NUL characters inside passwords properly leading to all subsequent characters being dropped and thus resulting in severely weakened passwords_. Please upgrade at earliest. See [this wiki page][improper-nuls] for more details. + +## Compatibility Note + +This library supports `$2a$` and `$2b$` prefix bcrypt hashes. `$2x$` and `$2y$` hashes are specific to bcrypt implementation developed for John the Ripper. In theory, they should be compatible with `$2b$` prefix. + +Compatibility with hashes generated by other languages is not 100% guaranteed due to difference in character encodings. However, it should not be an issue for most cases. + +### Migrating from v1.0.x + +Hashes generated in earlier version of `bcrypt` remain 100% supported in `v2.x.x` and later versions. In most cases, the migration should be a bump in the `package.json`. + +Hashes generated in `v2.x.x` using the defaults parameters will not work in earlier versions. + +## Dependencies + +* NodeJS +* `node-gyp` + * Please check the dependencies for this tool at: https://github.com/nodejs/node-gyp + * Windows users will need the options for c# and c++ installed with their visual studio instance. + * Python 2.x/3.x +* `OpenSSL` - This is only required to build the `bcrypt` project if you are using versions <= 0.7.7. Otherwise, we're using the builtin node crypto bindings for seed data (which use the same OpenSSL code paths we were, but don't have the external dependency). + +## Install via NPM + +``` +npm install bcrypt +``` +***Note:*** OS X users using Xcode 4.3.1 or above may need to run the following command in their terminal prior to installing if errors occur regarding xcodebuild: ```sudo xcode-select -switch /Applications/Xcode.app/Contents/Developer``` + +_Pre-built binaries for various NodeJS versions are made available on a best-effort basis._ + +Only the current stable and supported LTS releases are actively tested against. + +_There may be an interval between the release of the module and the availabilty of the compiled modules._ + +Currently, we have pre-built binaries that support the following platforms: + +1. Windows x32 and x64 +2. Linux x64 (GlibC and musl) +3. macOS + +If you face an error like this: + +``` +node-pre-gyp ERR! Tried to download(404): https://github.com/kelektiv/node.bcrypt.js/releases/download/v1.0.2/bcrypt_lib-v1.0.2-node-v48-linux-x64.tar.gz +``` + +make sure you have the appropriate dependencies installed and configured for your platform. You can find installation instructions for the dependencies for some common platforms [in this page][depsinstall]. + +## Usage + +### async (recommended) + +```javascript +const bcrypt = require('bcrypt'); +const saltRounds = 10; +const myPlaintextPassword = 's0/\/\P4$$w0rD'; +const someOtherPlaintextPassword = 'not_bacon'; +``` + +#### To hash a password: + +Technique 1 (generate a salt and hash on separate function calls): + +```javascript +bcrypt.genSalt(saltRounds, function(err, salt) { + bcrypt.hash(myPlaintextPassword, salt, function(err, hash) { + // Store hash in your password DB. + }); +}); +``` + +Technique 2 (auto-gen a salt and hash): + +```javascript +bcrypt.hash(myPlaintextPassword, saltRounds, function(err, hash) { + // Store hash in your password DB. +}); +``` + +Note that both techniques achieve the same end-result. + +#### To check a password: + +```javascript +// Load hash from your password DB. +bcrypt.compare(myPlaintextPassword, hash, function(err, result) { + // result == true +}); +bcrypt.compare(someOtherPlaintextPassword, hash, function(err, result) { + // result == false +}); +``` + +[A Note on Timing Attacks](#a-note-on-timing-attacks) + +### with promises + +bcrypt uses whatever `Promise` implementation is available in `global.Promise`. NodeJS >= 0.12 has a native `Promise` implementation built in. However, this should work in any Promises/A+ compliant implementation. + +Async methods that accept a callback, return a `Promise` when callback is not specified if Promise support is available. + +```javascript +bcrypt.hash(myPlaintextPassword, saltRounds).then(function(hash) { + // Store hash in your password DB. +}); +``` +```javascript +// Load hash from your password DB. +bcrypt.compare(myPlaintextPassword, hash).then(function(result) { + // result == true +}); +bcrypt.compare(someOtherPlaintextPassword, hash).then(function(result) { + // result == false +}); +``` + +This is also compatible with `async/await` + +```javascript +async function checkUser(username, password) { + //... fetch user from a db etc. + + const match = await bcrypt.compare(password, user.passwordHash); + + if(match) { + //login + } + + //... +} +``` + +### ESM import +```javascript +import bcrypt from "bcrypt"; + +// later +await bcrypt.compare(password, hash); +``` + +### sync + +```javascript +const bcrypt = require('bcrypt'); +const saltRounds = 10; +const myPlaintextPassword = 's0/\/\P4$$w0rD'; +const someOtherPlaintextPassword = 'not_bacon'; +``` + +#### To hash a password: + +Technique 1 (generate a salt and hash on separate function calls): + +```javascript +const salt = bcrypt.genSaltSync(saltRounds); +const hash = bcrypt.hashSync(myPlaintextPassword, salt); +// Store hash in your password DB. +``` + +Technique 2 (auto-gen a salt and hash): + +```javascript +const hash = bcrypt.hashSync(myPlaintextPassword, saltRounds); +// Store hash in your password DB. +``` + +As with async, both techniques achieve the same end-result. + +#### To check a password: + +```javascript +// Load hash from your password DB. +bcrypt.compareSync(myPlaintextPassword, hash); // true +bcrypt.compareSync(someOtherPlaintextPassword, hash); // false +``` + +[A Note on Timing Attacks](#a-note-on-timing-attacks) + +### Why is async mode recommended over sync mode? +We recommend using async API if you use `bcrypt` on a server. Bcrypt hashing is CPU intensive which will cause the sync APIs to block the event loop and prevent your application from servicing any inbound requests or events. The async version uses a thread pool which does not block the main event loop. + +## API + +`BCrypt.` + + * `genSaltSync(rounds, minor)` + * `rounds` - [OPTIONAL] - the cost of processing the data. (default - 10) + * `minor` - [OPTIONAL] - minor version of bcrypt to use. (default - b) + * `genSalt(rounds, minor, cb)` + * `rounds` - [OPTIONAL] - the cost of processing the data. (default - 10) + * `minor` - [OPTIONAL] - minor version of bcrypt to use. (default - b) + * `cb` - [OPTIONAL] - a callback to be fired once the salt has been generated. uses eio making it asynchronous. If `cb` is not specified, a `Promise` is returned if Promise support is available. + * `err` - First parameter to the callback detailing any errors. + * `salt` - Second parameter to the callback providing the generated salt. + * `hashSync(data, salt)` + * `data` - [REQUIRED] - the data to be encrypted. + * `salt` - [REQUIRED] - the salt to be used to hash the password. if specified as a number then a salt will be generated with the specified number of rounds and used (see example under **Usage**). + * `hash(data, salt, cb)` + * `data` - [REQUIRED] - the data to be encrypted. + * `salt` - [REQUIRED] - the salt to be used to hash the password. if specified as a number then a salt will be generated with the specified number of rounds and used (see example under **Usage**). + * `cb` - [OPTIONAL] - a callback to be fired once the data has been encrypted. uses eio making it asynchronous. If `cb` is not specified, a `Promise` is returned if Promise support is available. + * `err` - First parameter to the callback detailing any errors. + * `encrypted` - Second parameter to the callback providing the encrypted form. + * `compareSync(data, encrypted)` + * `data` - [REQUIRED] - data to compare. + * `encrypted` - [REQUIRED] - data to be compared to. + * `compare(data, encrypted, cb)` + * `data` - [REQUIRED] - data to compare. + * `encrypted` - [REQUIRED] - data to be compared to. + * `cb` - [OPTIONAL] - a callback to be fired once the data has been compared. uses eio making it asynchronous. If `cb` is not specified, a `Promise` is returned if Promise support is available. + * `err` - First parameter to the callback detailing any errors. + * `same` - Second parameter to the callback providing whether the data and encrypted forms match [true | false]. + * `getRounds(encrypted)` - return the number of rounds used to encrypt a given hash + * `encrypted` - [REQUIRED] - hash from which the number of rounds used should be extracted. + +## A Note on Rounds + +A note about the cost: when you are hashing your data, the module will go through a series of rounds to give you a secure hash. The value you submit is not just the number of rounds the module will go through to hash your data. The module will use the value you enter and go through `2^rounds` hashing iterations. + +From @garthk, on a 2GHz core you can roughly expect: + + rounds=8 : ~40 hashes/sec + rounds=9 : ~20 hashes/sec + rounds=10: ~10 hashes/sec + rounds=11: ~5 hashes/sec + rounds=12: 2-3 hashes/sec + rounds=13: ~1 sec/hash + rounds=14: ~1.5 sec/hash + rounds=15: ~3 sec/hash + rounds=25: ~1 hour/hash + rounds=31: 2-3 days/hash + + +## A Note on Timing Attacks + +Because it's come up multiple times in this project and other bcrypt projects, it needs to be said. The `bcrypt` library is not susceptible to timing attacks. From codahale/bcrypt-ruby#42: + +> One of the desired properties of a cryptographic hash function is preimage attack resistance, which means there is no shortcut for generating a message which, when hashed, produces a specific digest. + +A great thread on this, in much more detail can be found @ codahale/bcrypt-ruby#43 + +If you're unfamiliar with timing attacks and want to learn more you can find a great writeup @ [A Lesson In Timing Attacks][timingatk] + +However, timing attacks are real. And the comparison function is _not_ time safe. That means that it may exit the function early in the comparison process. Timing attacks happen because of the above. We don't need to be careful that an attacker will learn anything, and our comparison function provides a comparison of hashes. It is a utility to the overall purpose of the library. If you end up using it for something else, we cannot guarantee the security of the comparator. Keep that in mind as you use the library. + +## Hash Info + +The characters that comprise the resultant hash are `./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789$`. + +Resultant hashes will be 60 characters long and they will include the salt among other parameters, as follows: + +`$[algorithm]$[cost]$[salt][hash]` + +- 2 chars hash algorithm identifier prefix. `"$2a$" or "$2b$"` indicates BCrypt +- Cost-factor (n). Represents the exponent used to determine how many iterations 2^n +- 16-byte (128-bit) salt, base64 encoded to 22 characters +- 24-byte (192-bit) hash, base64 encoded to 31 characters + +Example: +``` +$2b$10$nOUIs5kJ7naTuTFkBy1veuK0kSxUFXfuaOKdOKf9xYT0KKIGSJwFa + | | | | + | | | hash-value = K0kSxUFXfuaOKdOKf9xYT0KKIGSJwFa + | | | + | | salt = nOUIs5kJ7naTuTFkBy1veu + | | + | cost-factor => 10 = 2^10 rounds + | + hash-algorithm identifier => 2b = BCrypt +``` + +## Testing + +If you create a pull request, tests better pass :) + +``` +npm install +npm test +``` + +## Credits + +The code for this comes from a few sources: + +* blowfish.cc - OpenBSD +* bcrypt.cc - OpenBSD +* bcrypt::gen_salt - [gen_salt inclusion to bcrypt][bcryptgs] +* bcrypt_node.cc - me + +## Contributors + +* [Antonio Salazar Cardozo][shadowfiend] - Early MacOS X support (when we used libbsd) +* [Ben Glow][pixelglow] - Fixes for thread safety with async calls +* [Van Nguyen][thegoleffect] - Found a timing attack in the comparator +* [NewITFarmer][newitfarmer] - Initial Cygwin support +* [David Trejo][dtrejo] - packaging fixes +* [Alfred Westerveld][alfredwesterveld] - packaging fixes +* [Vincent Côté-Roy][vincentr] - Testing around concurrency issues +* [Lloyd Hilaiel][lloyd] - Documentation fixes +* [Roman Shtylman][shtylman] - Code refactoring, general rot reduction, compile options, better memory management with delete and new, and an upgrade to libuv over eio/ev. +* [Vadim Graboys][vadimg] - Code changes to support 0.5.5+ +* [Ben Noordhuis][bnoordhuis] - Fixed a thread safety issue in nodejs that was perfectly mappable to this module. +* [Nate Rajlich][tootallnate] - Bindings and build process. +* [Sean McArthur][seanmonstar] - Windows Support +* [Fanie Oosthuysen][weareu] - Windows Support +* [Amitosh Swain Mahapatra][recrsn] - $2b$ hash support, ES6 Promise support +* [Nicola Del Gobbo][NickNaso] - Initial implementation with N-API + +## License +Unless stated elsewhere, file headers or otherwise, the license as stated in the LICENSE file. + +[bcryptwiki]: https://en.wikipedia.org/wiki/Bcrypt +[bcryptgs]: http://mail-index.netbsd.org/tech-crypto/2002/05/24/msg000204.html +[codahale]: http://codahale.com/how-to-safely-store-a-password/ +[gh13]: https://github.com/ncb000gt/node.bcrypt.js/issues/13 +[jtr]: http://www.openwall.com/lists/oss-security/2011/06/20/2 +[depsinstall]: https://github.com/kelektiv/node.bcrypt.js/wiki/Installation-Instructions +[timingatk]: https://codahale.com/a-lesson-in-timing-attacks/ +[wrap-around-bug]: https://github.com/kelektiv/node.bcrypt.js/wiki/Security-Issues-and-Concerns#bcrypt-wrap-around-bug-medium-severity +[improper-nuls]: https://github.com/kelektiv/node.bcrypt.js/wiki/Security-Issues-and-Concerns#improper-nul-handling-medium-severity + +[shadowfiend]:https://github.com/Shadowfiend +[thegoleffect]:https://github.com/thegoleffect +[pixelglow]:https://github.com/pixelglow +[dtrejo]:https://github.com/dtrejo +[alfredwesterveld]:https://github.com/alfredwesterveld +[newitfarmer]:https://github.com/newitfarmer +[zooko]:https://twitter.com/zooko +[vincentr]:https://twitter.com/vincentcr +[lloyd]:https://github.com/lloyd +[shtylman]:https://github.com/shtylman +[vadimg]:https://github.com/vadimg +[bnoordhuis]:https://github.com/bnoordhuis +[tootallnate]:https://github.com/tootallnate +[seanmonstar]:https://github.com/seanmonstar +[weareu]:https://github.com/weareu +[recrsn]:https://github.com/recrsn +[NickNaso]: https://github.com/NickNaso diff --git a/node_modules/bcrypt/SECURITY.md b/node_modules/bcrypt/SECURITY.md new file mode 100644 index 0000000..c132dc8 --- /dev/null +++ b/node_modules/bcrypt/SECURITY.md @@ -0,0 +1,15 @@ +# Security Policy + +As with any software, `bcrypt` is likely to have bugs. Please report any security vulnerabilities responsibly + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| 5.0.x | :white_check_mark: | +| < 5.0 | :x: | + +## Reporting a Vulnerability + +If you are reporting a security vulnerability, please refrain from opening a GitHub issue and instead mail it to +one of the maintainers listed in the README. diff --git a/node_modules/bcrypt/bcrypt.js b/node_modules/bcrypt/bcrypt.js new file mode 100644 index 0000000..62da525 --- /dev/null +++ b/node_modules/bcrypt/bcrypt.js @@ -0,0 +1,242 @@ +const path = require('path'); +const bindings = require('node-gyp-build')(path.resolve(__dirname)); + +const crypto = require('crypto'); + +const promises = require('./promises'); + +/// generate a salt (sync) +/// @param {Number} [rounds] number of rounds (default 10) +/// @return {String} salt +function genSaltSync(rounds, minor) { + // default 10 rounds + if (!rounds) { + rounds = 10; + } else if (typeof rounds !== 'number') { + throw new Error('rounds must be a number'); + } + + if (!minor) { + minor = 'b'; + } else if (minor !== 'b' && minor !== 'a') { + throw new Error('minor must be either "a" or "b"'); + } + + return bindings.gen_salt_sync(minor, rounds, crypto.randomBytes(16)); +} + +/// generate a salt +/// @param {Number} [rounds] number of rounds (default 10) +/// @param {Function} cb callback(err, salt) +function genSalt(rounds, minor, cb) { + let error; + + // if callback is first argument, then use defaults for others + if (typeof arguments[0] === 'function') { + // have to set callback first otherwise arguments are overridden + cb = arguments[0]; + rounds = 10; + minor = 'b'; + // callback is second argument + } else if (typeof arguments[1] === 'function') { + // have to set callback first otherwise arguments are overridden + cb = arguments[1]; + minor = 'b'; + } + + if (!cb) { + return promises.promise(genSalt, this, [rounds, minor]); + } + + // default 10 rounds + if (!rounds) { + rounds = 10; + } else if (typeof rounds !== 'number') { + // callback error asynchronously + error = new Error('rounds must be a number'); + return process.nextTick(function () { + cb(error); + }); + } + + if (!minor) { + minor = 'b' + } else if (minor !== 'b' && minor !== 'a') { + error = new Error('minor must be either "a" or "b"'); + return process.nextTick(function () { + cb(error); + }); + } + + crypto.randomBytes(16, function (error, randomBytes) { + if (error) { + cb(error); + return; + } + + bindings.gen_salt(minor, rounds, randomBytes, cb); + }); +} + +/// hash data using a salt +/// @param {String|Buffer} data the data to encrypt +/// @param {String} salt the salt to use when hashing +/// @return {String} hash +function hashSync(data, salt) { + if (data == null || salt == null) { + throw new Error('data and salt arguments required'); + } + + if (!(typeof data === 'string' || data instanceof Buffer) || (typeof salt !== 'string' && typeof salt !== 'number')) { + throw new Error('data must be a string or Buffer and salt must either be a salt string or a number of rounds'); + } + + if (typeof salt === 'number') { + salt = module.exports.genSaltSync(salt); + } + + return bindings.encrypt_sync(data, salt); +} + +/// hash data using a salt +/// @param {String|Buffer} data the data to encrypt +/// @param {String} salt the salt to use when hashing +/// @param {Function} cb callback(err, hash) +function hash(data, salt, cb) { + let error; + + if (typeof data === 'function') { + error = new Error('data must be a string or Buffer and salt must either be a salt string or a number of rounds'); + return process.nextTick(function () { + data(error); + }); + } + + if (typeof salt === 'function') { + error = new Error('data must be a string or Buffer and salt must either be a salt string or a number of rounds'); + return process.nextTick(function () { + salt(error); + }); + } + + // cb exists but is not a function + // return a rejecting promise + if (cb && typeof cb !== 'function') { + return promises.reject(new Error('cb must be a function or null to return a Promise')); + } + + if (!cb) { + return promises.promise(hash, this, [data, salt]); + } + + if (data == null || salt == null) { + error = new Error('data and salt arguments required'); + return process.nextTick(function () { + cb(error); + }); + } + + if (!(typeof data === 'string' || data instanceof Buffer) || (typeof salt !== 'string' && typeof salt !== 'number')) { + error = new Error('data must be a string or Buffer and salt must either be a salt string or a number of rounds'); + return process.nextTick(function () { + cb(error); + }); + } + + + if (typeof salt === 'number') { + return module.exports.genSalt(salt, function (err, salt) { + return bindings.encrypt(data, salt, cb); + }); + } + + return bindings.encrypt(data, salt, cb); +} + +/// compare raw data to hash +/// @param {String|Buffer} data the data to hash and compare +/// @param {String} hash expected hash +/// @return {bool} true if hashed data matches hash +function compareSync(data, hash) { + if (data == null || hash == null) { + throw new Error('data and hash arguments required'); + } + + if (!(typeof data === 'string' || data instanceof Buffer) || typeof hash !== 'string') { + throw new Error('data must be a string or Buffer and hash must be a string'); + } + + return bindings.compare_sync(data, hash); +} + +/// compare raw data to hash +/// @param {String|Buffer} data the data to hash and compare +/// @param {String} hash expected hash +/// @param {Function} cb callback(err, matched) - matched is true if hashed data matches hash +function compare(data, hash, cb) { + let error; + + if (typeof data === 'function') { + error = new Error('data and hash arguments required'); + return process.nextTick(function () { + data(error); + }); + } + + if (typeof hash === 'function') { + error = new Error('data and hash arguments required'); + return process.nextTick(function () { + hash(error); + }); + } + + // cb exists but is not a function + // return a rejecting promise + if (cb && typeof cb !== 'function') { + return promises.reject(new Error('cb must be a function or null to return a Promise')); + } + + if (!cb) { + return promises.promise(compare, this, [data, hash]); + } + + if (data == null || hash == null) { + error = new Error('data and hash arguments required'); + return process.nextTick(function () { + cb(error); + }); + } + + if (!(typeof data === 'string' || data instanceof Buffer) || typeof hash !== 'string') { + error = new Error('data and hash must be strings'); + return process.nextTick(function () { + cb(error); + }); + } + + return bindings.compare(data, hash, cb); +} + +/// @param {String} hash extract rounds from this hash +/// @return {Number} the number of rounds used to encrypt a given hash +function getRounds(hash) { + if (hash == null) { + throw new Error('hash argument required'); + } + + if (typeof hash !== 'string') { + throw new Error('hash must be a string'); + } + + return bindings.get_rounds(hash); +} + +module.exports = { + genSaltSync, + genSalt, + hashSync, + hash, + compareSync, + compare, + getRounds, +} diff --git a/node_modules/bcrypt/binding.gyp b/node_modules/bcrypt/binding.gyp new file mode 100644 index 0000000..46428be --- /dev/null +++ b/node_modules/bcrypt/binding.gyp @@ -0,0 +1,49 @@ +{ + "variables": { + "NODE_VERSION%":" { + const start = Date.now(); + + // genSalt + const salt = await bcrypt.genSalt(10) + console.log('salt: ' + salt); + console.log('salt cb end: ' + (Date.now() - start) + 'ms'); + + // hash + const crypted = await bcrypt.hash('test', salt) + console.log('crypted: ' + crypted); + console.log('crypted cb end: ' + (Date.now() - start) + 'ms'); + console.log('rounds used from hash:', bcrypt.getRounds(crypted)); + + // compare + const res = await bcrypt.compare('test', crypted) + console.log('compared true: ' + res); + console.log('compared true cb end: ' + (Date.now() - start) + 'ms'); + + // compare + const res2 = await bcrypt.compare('bacon', crypted) + console.log('compared false: ' + res2); + console.log('compared false cb end: ' + (Date.now() - start) + 'ms'); + + console.log('end: ' + (Date.now() - start) + 'ms'); +})(); diff --git a/node_modules/bcrypt/examples/forever_gen_salt.js b/node_modules/bcrypt/examples/forever_gen_salt.js new file mode 100644 index 0000000..3f2ff2f --- /dev/null +++ b/node_modules/bcrypt/examples/forever_gen_salt.js @@ -0,0 +1,8 @@ +const bcrypt = require('../bcrypt'); + +(function printSalt() { + bcrypt.genSalt(10, (err, salt) => { + console.log('salt: ' + salt); + printSalt(); + }); +})() diff --git a/node_modules/bcrypt/node_modules/node-addon-api/LICENSE.md b/node_modules/bcrypt/node_modules/node-addon-api/LICENSE.md new file mode 100644 index 0000000..819d91a --- /dev/null +++ b/node_modules/bcrypt/node_modules/node-addon-api/LICENSE.md @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright (c) 2017 [Node.js API collaborators](https://github.com/nodejs/node-addon-api#collaborators) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/node_modules/bcrypt/node_modules/node-addon-api/README.md b/node_modules/bcrypt/node_modules/node-addon-api/README.md new file mode 100644 index 0000000..39df5a9 --- /dev/null +++ b/node_modules/bcrypt/node_modules/node-addon-api/README.md @@ -0,0 +1,95 @@ +# **node-addon-api module** + +[![codecov](https://codecov.io/gh/nodejs/node-addon-api/branch/main/graph/badge.svg)](https://app.codecov.io/gh/nodejs/node-addon-api/tree/main) + +[![NPM](https://nodei.co/npm/node-addon-api.png?downloads=true&downloadRank=true)](https://nodei.co/npm/node-addon-api/) [![NPM](https://nodei.co/npm-dl/node-addon-api.png?months=6&height=1)](https://nodei.co/npm/node-addon-api/) + +This module contains **header-only C++ wrapper classes** which simplify +the use of the C based [Node-API](https://nodejs.org/dist/latest/docs/api/n-api.html) +provided by Node.js when using C++. It provides a C++ object model +and exception handling semantics with low overhead. + +- [API References](doc/README.md) +- [Badges](#badges) +- [Contributing](#contributing) +- [License](#license) + +## API References + +API references are available in the [doc](doc/README.md) directory. + + +## Current version: 8.6.0 + + +(See [CHANGELOG.md](CHANGELOG.md) for complete Changelog) + +node-addon-api is based on [Node-API](https://nodejs.org/api/n-api.html) and supports using different Node-API versions. +This allows addons built with it to run with Node.js versions which support the targeted Node-API version. +**However** the node-addon-api support model is to support only the active LTS Node.js versions. This means that +every year there will be a new major which drops support for the Node.js LTS version which has gone out of service. + +The oldest Node.js version supported by the current version of node-addon-api is Node.js 18.x. + +## Badges + +The use of badges is recommended to indicate the minimum version of Node-API +required for the module. This helps to determine which Node.js major versions are +supported. Addon maintainers can consult the [Node-API support matrix][] to determine +which Node.js versions provide a given Node-API version. The following badges are +available: + +![Node-API v1 Badge](https://github.com/nodejs/abi-stable-node/blob/doc/assets/Node-API%20v1%20Badge.svg) +![Node-API v2 Badge](https://github.com/nodejs/abi-stable-node/blob/doc/assets/Node-API%20v2%20Badge.svg) +![Node-API v3 Badge](https://github.com/nodejs/abi-stable-node/blob/doc/assets/Node-API%20v3%20Badge.svg) +![Node-API v4 Badge](https://github.com/nodejs/abi-stable-node/blob/doc/assets/Node-API%20v4%20Badge.svg) +![Node-API v5 Badge](https://github.com/nodejs/abi-stable-node/blob/doc/assets/Node-API%20v5%20Badge.svg) +![Node-API v6 Badge](https://github.com/nodejs/abi-stable-node/blob/doc/assets/Node-API%20v6%20Badge.svg) +![Node-API v7 Badge](https://github.com/nodejs/abi-stable-node/blob/doc/assets/Node-API%20v7%20Badge.svg) +![Node-API v8 Badge](https://github.com/nodejs/abi-stable-node/blob/doc/assets/Node-API%20v8%20Badge.svg) +![Node-API v9 Badge](https://github.com/nodejs/abi-stable-node/blob/doc/assets/Node-API%20v9%20Badge.svg) +![Node-API Experimental Version Badge](https://github.com/nodejs/abi-stable-node/blob/doc/assets/Node-API%20Experimental%20Version%20Badge.svg) + +## Contributing + +We love contributions from the community to **node-addon-api**! +See [CONTRIBUTING.md](CONTRIBUTING.md) for more details on our philosophy around extending this module. + +## Team members + +### Active + +| Name | GitHub Link | +| ------------------- | ----------------------------------------------------- | +| Anna Henningsen | [addaleax](https://github.com/addaleax) | +| Chengzhong Wu | [legendecas](https://github.com/legendecas) | +| Jack Xia | [JckXia](https://github.com/JckXia) | +| Kevin Eady | [KevinEady](https://github.com/KevinEady) | +| Michael Dawson | [mhdawson](https://github.com/mhdawson) | +| Nicola Del Gobbo | [NickNaso](https://github.com/NickNaso) | +| Vladimir Morozov | [vmoroz](https://github.com/vmoroz) | + +
+ +Emeritus + +### Emeritus + +| Name | GitHub Link | +| ------------------- | ----------------------------------------------------- | +| Arunesh Chandra | [aruneshchandra](https://github.com/aruneshchandra) | +| Benjamin Byholm | [kkoopa](https://github.com/kkoopa) | +| Gabriel Schulhof | [gabrielschulhof](https://github.com/gabrielschulhof) | +| Hitesh Kanwathirtha | [digitalinfinity](https://github.com/digitalinfinity) | +| Jason Ginchereau | [jasongin](https://github.com/jasongin) | +| Jim Schlight | [jschlight](https://github.com/jschlight) | +| Sampson Gao | [sampsongao](https://github.com/sampsongao) | +| Taylor Woll | [boingoing](https://github.com/boingoing) | + +
+ +## License + +Licensed under [MIT](./LICENSE.md) + +[Node-API support matrix]: https://nodejs.org/dist/latest/docs/api/n-api.html#node-api-version-matrix diff --git a/node_modules/bcrypt/node_modules/node-addon-api/common.gypi b/node_modules/bcrypt/node_modules/node-addon-api/common.gypi new file mode 100644 index 0000000..5fda7e7 --- /dev/null +++ b/node_modules/bcrypt/node_modules/node-addon-api/common.gypi @@ -0,0 +1,21 @@ +{ + 'variables': { + 'NAPI_VERSION%': " +inline PropertyDescriptor PropertyDescriptor::Accessor( + const char* utf8name, + Getter getter, + napi_property_attributes attributes, + void* /*data*/) { + using CbData = details::CallbackData; + // TODO: Delete when the function is destroyed + auto callbackData = new CbData({getter, nullptr}); + + return PropertyDescriptor({utf8name, + nullptr, + nullptr, + CbData::Wrapper, + nullptr, + nullptr, + attributes, + callbackData}); +} + +template +inline PropertyDescriptor PropertyDescriptor::Accessor( + const std::string& utf8name, + Getter getter, + napi_property_attributes attributes, + void* data) { + return Accessor(utf8name.c_str(), getter, attributes, data); +} + +template +inline PropertyDescriptor PropertyDescriptor::Accessor( + napi_value name, + Getter getter, + napi_property_attributes attributes, + void* /*data*/) { + using CbData = details::CallbackData; + // TODO: Delete when the function is destroyed + auto callbackData = new CbData({getter, nullptr}); + + return PropertyDescriptor({nullptr, + name, + nullptr, + CbData::Wrapper, + nullptr, + nullptr, + attributes, + callbackData}); +} + +template +inline PropertyDescriptor PropertyDescriptor::Accessor( + Name name, Getter getter, napi_property_attributes attributes, void* data) { + napi_value nameValue = name; + return PropertyDescriptor::Accessor(nameValue, getter, attributes, data); +} + +template +inline PropertyDescriptor PropertyDescriptor::Accessor( + const char* utf8name, + Getter getter, + Setter setter, + napi_property_attributes attributes, + void* /*data*/) { + using CbData = details::AccessorCallbackData; + // TODO: Delete when the function is destroyed + auto callbackData = new CbData({getter, setter, nullptr}); + + return PropertyDescriptor({utf8name, + nullptr, + nullptr, + CbData::GetterWrapper, + CbData::SetterWrapper, + nullptr, + attributes, + callbackData}); +} + +template +inline PropertyDescriptor PropertyDescriptor::Accessor( + const std::string& utf8name, + Getter getter, + Setter setter, + napi_property_attributes attributes, + void* data) { + return Accessor(utf8name.c_str(), getter, setter, attributes, data); +} + +template +inline PropertyDescriptor PropertyDescriptor::Accessor( + napi_value name, + Getter getter, + Setter setter, + napi_property_attributes attributes, + void* /*data*/) { + using CbData = details::AccessorCallbackData; + // TODO: Delete when the function is destroyed + auto callbackData = new CbData({getter, setter, nullptr}); + + return PropertyDescriptor({nullptr, + name, + nullptr, + CbData::GetterWrapper, + CbData::SetterWrapper, + nullptr, + attributes, + callbackData}); +} + +template +inline PropertyDescriptor PropertyDescriptor::Accessor( + Name name, + Getter getter, + Setter setter, + napi_property_attributes attributes, + void* data) { + napi_value nameValue = name; + return PropertyDescriptor::Accessor( + nameValue, getter, setter, attributes, data); +} + +template +inline PropertyDescriptor PropertyDescriptor::Function( + const char* utf8name, + Callable cb, + napi_property_attributes attributes, + void* /*data*/) { + using ReturnType = decltype(cb(CallbackInfo(nullptr, nullptr))); + using CbData = details::CallbackData; + // TODO: Delete when the function is destroyed + auto callbackData = new CbData({cb, nullptr}); + + return PropertyDescriptor({utf8name, + nullptr, + CbData::Wrapper, + nullptr, + nullptr, + nullptr, + attributes, + callbackData}); +} + +template +inline PropertyDescriptor PropertyDescriptor::Function( + const std::string& utf8name, + Callable cb, + napi_property_attributes attributes, + void* data) { + return Function(utf8name.c_str(), cb, attributes, data); +} + +template +inline PropertyDescriptor PropertyDescriptor::Function( + napi_value name, + Callable cb, + napi_property_attributes attributes, + void* /*data*/) { + using ReturnType = decltype(cb(CallbackInfo(nullptr, nullptr))); + using CbData = details::CallbackData; + // TODO: Delete when the function is destroyed + auto callbackData = new CbData({cb, nullptr}); + + return PropertyDescriptor({nullptr, + name, + CbData::Wrapper, + nullptr, + nullptr, + nullptr, + attributes, + callbackData}); +} + +template +inline PropertyDescriptor PropertyDescriptor::Function( + Name name, Callable cb, napi_property_attributes attributes, void* data) { + napi_value nameValue = name; + return PropertyDescriptor::Function(nameValue, cb, attributes, data); +} + +#endif // !SRC_NAPI_INL_DEPRECATED_H_ diff --git a/node_modules/bcrypt/node_modules/node-addon-api/napi-inl.h b/node_modules/bcrypt/node_modules/node-addon-api/napi-inl.h new file mode 100644 index 0000000..0f1717e --- /dev/null +++ b/node_modules/bcrypt/node_modules/node-addon-api/napi-inl.h @@ -0,0 +1,7108 @@ +#ifndef SRC_NAPI_INL_H_ +#define SRC_NAPI_INL_H_ + +//////////////////////////////////////////////////////////////////////////////// +// Node-API C++ Wrapper Classes +// +// Inline header-only implementations for "Node-API" ABI-stable C APIs for +// Node.js. +//////////////////////////////////////////////////////////////////////////////// + +// Note: Do not include this file directly! Include "napi.h" instead. +// This should be a no-op and is intended for better IDE integration. +#include "napi.h" + +#include +#include +#include +#if NAPI_HAS_THREADS +#include +#endif // NAPI_HAS_THREADS +#include +#include + +#if defined(__clang__) || defined(__GNUC__) +#define NAPI_NO_SANITIZE_VPTR __attribute__((no_sanitize("vptr"))) +#else +#define NAPI_NO_SANITIZE_VPTR +#endif + +namespace Napi { + +#ifdef NAPI_CPP_CUSTOM_NAMESPACE +namespace NAPI_CPP_CUSTOM_NAMESPACE { +#endif + +// Helpers to handle functions exposed from C++ and internal constants. +namespace details { + +// New napi_status constants not yet available in all supported versions of +// Node.js releases. Only necessary when they are used in napi.h and napi-inl.h. +constexpr int napi_no_external_buffers_allowed = 22; + +template +inline void default_basic_finalizer(node_addon_api_basic_env /*env*/, + void* data, + void* /*hint*/) { + delete static_cast(data); +} + +// Attach a data item to an object and delete it when the object gets +// garbage-collected. +// TODO: Replace this code with `napi_add_finalizer()` whenever it becomes +// available on all supported versions of Node.js. +template < + typename FreeType, + node_addon_api_basic_finalize finalizer = default_basic_finalizer> +inline napi_status AttachData(napi_env env, + napi_value obj, + FreeType* data, + void* hint = nullptr) { + napi_status status; +#if (NAPI_VERSION < 5) + napi_value symbol, external; + status = napi_create_symbol(env, nullptr, &symbol); + if (status == napi_ok) { + status = napi_create_external(env, data, finalizer, hint, &external); + if (status == napi_ok) { + napi_property_descriptor desc = {nullptr, + symbol, + nullptr, + nullptr, + nullptr, + external, + napi_default, + nullptr}; + status = napi_define_properties(env, obj, 1, &desc); + } + } +#else // NAPI_VERSION >= 5 + status = napi_add_finalizer(env, obj, data, finalizer, hint, nullptr); +#endif + return status; +} + +// For use in JS to C++ callback wrappers to catch any Napi::Error exceptions +// and rethrow them as JavaScript exceptions before returning from the callback. +template +#ifdef NODE_ADDON_API_CPP_EXCEPTIONS_ALL +inline napi_value WrapCallback(napi_env env, Callable callback) { +#else +inline napi_value WrapCallback(napi_env, Callable callback) { +#endif +#ifdef NODE_ADDON_API_CPP_EXCEPTIONS + try { + return callback(); + } catch (const Error& e) { + e.ThrowAsJavaScriptException(); + return nullptr; + } +#ifdef NODE_ADDON_API_CPP_EXCEPTIONS_ALL + catch (const std::exception& e) { + Napi::Error::New(env, e.what()).ThrowAsJavaScriptException(); + return nullptr; + } catch (...) { + Napi::Error::New(env, "A native exception was thrown") + .ThrowAsJavaScriptException(); + return nullptr; + } +#endif // NODE_ADDON_API_CPP_EXCEPTIONS_ALL +#else // NODE_ADDON_API_CPP_EXCEPTIONS + // When C++ exceptions are disabled, errors are immediately thrown as JS + // exceptions, so there is no need to catch and rethrow them here. + return callback(); +#endif // NODE_ADDON_API_CPP_EXCEPTIONS +} + +// For use in JS to C++ void callback wrappers to catch any Napi::Error +// exceptions and rethrow them as JavaScript exceptions before returning from +// the callback. +template +inline void WrapVoidCallback(Callable callback) { +#ifdef NODE_ADDON_API_CPP_EXCEPTIONS + try { + callback(); + } catch (const Error& e) { + e.ThrowAsJavaScriptException(); + } +#else // NAPI_CPP_EXCEPTIONS + // When C++ exceptions are disabled, errors are immediately thrown as JS + // exceptions, so there is no need to catch and rethrow them here. + callback(); +#endif // NAPI_CPP_EXCEPTIONS +} + +// For use in JS to C++ void callback wrappers to catch _any_ thrown exception +// and rethrow them as JavaScript exceptions before returning from the callback, +// wrapping in an Napi::Error as needed. +template +#ifdef NODE_ADDON_API_CPP_EXCEPTIONS_ALL +inline void WrapVoidCallback(napi_env env, Callable callback) { +#else +inline void WrapVoidCallback(napi_env, Callable callback) { +#endif +#ifdef NODE_ADDON_API_CPP_EXCEPTIONS + try { + callback(); + } catch (const Error& e) { + e.ThrowAsJavaScriptException(); + } +#ifdef NODE_ADDON_API_CPP_EXCEPTIONS_ALL + catch (const std::exception& e) { + Napi::Error::New(env, e.what()).ThrowAsJavaScriptException(); + } catch (...) { + Napi::Error::New(env, "A native exception was thrown") + .ThrowAsJavaScriptException(); + } +#endif // NODE_ADDON_API_CPP_EXCEPTIONS_ALL +#else + // When C++ exceptions are disabled, there is no need to catch and rethrow C++ + // exceptions. JS errors should be thrown with + // `Error::ThrowAsJavaScriptException`. + callback(); +#endif // NODE_ADDON_API_CPP_EXCEPTIONS +} + +template +struct CallbackData { + static inline napi_value Wrapper(napi_env env, napi_callback_info info) { + return details::WrapCallback(env, [&] { + CallbackInfo callbackInfo(env, info); + CallbackData* callbackData = + static_cast(callbackInfo.Data()); + callbackInfo.SetData(callbackData->data); + return callbackData->callback(callbackInfo); + }); + } + + Callable callback; + void* data; +}; + +template +struct CallbackData { + static inline napi_value Wrapper(napi_env env, napi_callback_info info) { + return details::WrapCallback(env, [&] { + CallbackInfo callbackInfo(env, info); + CallbackData* callbackData = + static_cast(callbackInfo.Data()); + callbackInfo.SetData(callbackData->data); + callbackData->callback(callbackInfo); + return nullptr; + }); + } + + Callable callback; + void* data; +}; + +template +napi_value TemplatedVoidCallback(napi_env env, + napi_callback_info info) NAPI_NOEXCEPT { + return details::WrapCallback(env, [&] { + CallbackInfo cbInfo(env, info); + Callback(cbInfo); + return nullptr; + }); +} + +template +napi_value TemplatedCallback(napi_env env, + napi_callback_info info) NAPI_NOEXCEPT { + return details::WrapCallback(env, [&] { + CallbackInfo cbInfo(env, info); + // MSVC requires to copy 'Callback' function pointer to a local variable + // before invoking it. + auto callback = Callback; + return callback(cbInfo); + }); +} + +template +napi_value TemplatedInstanceCallback(napi_env env, + napi_callback_info info) NAPI_NOEXCEPT { + return details::WrapCallback(env, [&] { + CallbackInfo cbInfo(env, info); + T* instance = T::Unwrap(cbInfo.This().As()); + return instance ? (instance->*UnwrapCallback)(cbInfo) : Napi::Value(); + }); +} + +template +napi_value TemplatedInstanceVoidCallback(napi_env env, napi_callback_info info) + NAPI_NOEXCEPT { + return details::WrapCallback(env, [&] { + CallbackInfo cbInfo(env, info); + T* instance = T::Unwrap(cbInfo.This().As()); + if (instance) (instance->*UnwrapCallback)(cbInfo); + return nullptr; + }); +} + +template +struct FinalizeData { +#ifdef NODE_API_EXPERIMENTAL_HAS_POST_FINALIZER + template >> +#endif + static inline void Wrapper(node_addon_api_basic_env env, + void* data, + void* finalizeHint) NAPI_NOEXCEPT { + WrapVoidCallback([&] { + FinalizeData* finalizeData = static_cast(finalizeHint); + finalizeData->callback(env, static_cast(data)); + delete finalizeData; + }); + } + +#ifdef NODE_API_EXPERIMENTAL_HAS_POST_FINALIZER + template >, + typename = void> + static inline void Wrapper(node_addon_api_basic_env env, + void* data, + void* finalizeHint) NAPI_NOEXCEPT { +#ifdef NODE_ADDON_API_REQUIRE_BASIC_FINALIZERS + static_assert(false, + "NODE_ADDON_API_REQUIRE_BASIC_FINALIZERS defined: Finalizer " + "must be basic."); +#endif + napi_status status = + node_api_post_finalizer(env, WrapperGC, data, finalizeHint); + NAPI_FATAL_IF_FAILED( + status, "FinalizeData::Wrapper", "node_api_post_finalizer failed"); + } +#endif + +#ifdef NODE_API_EXPERIMENTAL_HAS_POST_FINALIZER + template >> +#endif + static inline void WrapperWithHint(node_addon_api_basic_env env, + void* data, + void* finalizeHint) NAPI_NOEXCEPT { + WrapVoidCallback([&] { + FinalizeData* finalizeData = static_cast(finalizeHint); + finalizeData->callback(env, static_cast(data), finalizeData->hint); + delete finalizeData; + }); + } + +#ifdef NODE_API_EXPERIMENTAL_HAS_POST_FINALIZER + template >, + typename = void> + static inline void WrapperWithHint(node_addon_api_basic_env env, + void* data, + void* finalizeHint) NAPI_NOEXCEPT { +#ifdef NODE_ADDON_API_REQUIRE_BASIC_FINALIZERS + static_assert(false, + "NODE_ADDON_API_REQUIRE_BASIC_FINALIZERS defined: Finalizer " + "must be basic."); +#endif + napi_status status = + node_api_post_finalizer(env, WrapperGCWithHint, data, finalizeHint); + NAPI_FATAL_IF_FAILED( + status, "FinalizeData::Wrapper", "node_api_post_finalizer failed"); + } +#endif + + static inline void WrapperGCWithoutData(napi_env env, + void* /*data*/, + void* finalizeHint) NAPI_NOEXCEPT { + WrapVoidCallback(env, [&] { + FinalizeData* finalizeData = static_cast(finalizeHint); + finalizeData->callback(env); + delete finalizeData; + }); + } + + static inline void WrapperGC(napi_env env, + void* data, + void* finalizeHint) NAPI_NOEXCEPT { + WrapVoidCallback(env, [&] { + FinalizeData* finalizeData = static_cast(finalizeHint); + finalizeData->callback(env, static_cast(data)); + delete finalizeData; + }); + } + + static inline void WrapperGCWithHint(napi_env env, + void* data, + void* finalizeHint) NAPI_NOEXCEPT { + WrapVoidCallback(env, [&] { + FinalizeData* finalizeData = static_cast(finalizeHint); + finalizeData->callback(env, static_cast(data), finalizeData->hint); + delete finalizeData; + }); + } + + Finalizer callback; + Hint* hint; +}; + +#if (NAPI_VERSION > 3 && NAPI_HAS_THREADS) +template , + typename FinalizerDataType = void> +struct ThreadSafeFinalize { + static inline void Wrapper(napi_env env, + void* rawFinalizeData, + void* /* rawContext */) { + if (rawFinalizeData == nullptr) return; + + ThreadSafeFinalize* finalizeData = + static_cast(rawFinalizeData); + finalizeData->callback(Env(env)); + delete finalizeData; + } + + static inline void FinalizeWrapperWithData(napi_env env, + void* rawFinalizeData, + void* /* rawContext */) { + if (rawFinalizeData == nullptr) return; + + ThreadSafeFinalize* finalizeData = + static_cast(rawFinalizeData); + finalizeData->callback(Env(env), finalizeData->data); + delete finalizeData; + } + + static inline void FinalizeWrapperWithContext(napi_env env, + void* rawFinalizeData, + void* rawContext) { + if (rawFinalizeData == nullptr) return; + + ThreadSafeFinalize* finalizeData = + static_cast(rawFinalizeData); + finalizeData->callback(Env(env), static_cast(rawContext)); + delete finalizeData; + } + + static inline void FinalizeFinalizeWrapperWithDataAndContext( + napi_env env, void* rawFinalizeData, void* rawContext) { + if (rawFinalizeData == nullptr) return; + + ThreadSafeFinalize* finalizeData = + static_cast(rawFinalizeData); + finalizeData->callback( + Env(env), finalizeData->data, static_cast(rawContext)); + delete finalizeData; + } + + FinalizerDataType* data; + Finalizer callback; +}; + +template +inline typename std::enable_if(nullptr)>::type +CallJsWrapper(napi_env env, napi_value jsCallback, void* context, void* data) { + details::WrapVoidCallback(env, [&]() { + call(env, + Function(env, jsCallback), + static_cast(context), + static_cast(data)); + }); +} + +template +inline typename std::enable_if(nullptr)>::type +CallJsWrapper(napi_env env, + napi_value jsCallback, + void* /*context*/, + void* /*data*/) { + details::WrapVoidCallback(env, [&]() { + if (jsCallback != nullptr) { + Function(env, jsCallback).Call(0, nullptr); + } + }); +} + +#if NAPI_VERSION > 4 + +template +napi_value DefaultCallbackWrapper(napi_env /*env*/, std::nullptr_t /*cb*/) { + return nullptr; +} + +template +napi_value DefaultCallbackWrapper(napi_env /*env*/, Napi::Function cb) { + return cb; +} + +#else +template +napi_value DefaultCallbackWrapper(napi_env env, Napi::Function cb) { + if (cb.IsEmpty()) { + return TSFN::EmptyFunctionFactory(env); + } + return cb; +} +#endif // NAPI_VERSION > 4 +#endif // NAPI_VERSION > 3 && NAPI_HAS_THREADS + +template +struct AccessorCallbackData { + static inline napi_value GetterWrapper(napi_env env, + napi_callback_info info) { + return details::WrapCallback(env, [&] { + CallbackInfo callbackInfo(env, info); + AccessorCallbackData* callbackData = + static_cast(callbackInfo.Data()); + callbackInfo.SetData(callbackData->data); + return callbackData->getterCallback(callbackInfo); + }); + } + + static inline napi_value SetterWrapper(napi_env env, + napi_callback_info info) { + return details::WrapCallback(env, [&] { + CallbackInfo callbackInfo(env, info); + AccessorCallbackData* callbackData = + static_cast(callbackInfo.Data()); + callbackInfo.SetData(callbackData->data); + callbackData->setterCallback(callbackInfo); + return nullptr; + }); + } + + Getter getterCallback; + Setter setterCallback; + void* data; +}; + +// Debugging-purpose C++-style variant of sprintf(). +inline std::string StringFormat(const char* format, ...) { + std::string result; + va_list args; + va_start(args, format); + int len = vsnprintf(nullptr, 0, format, args); + result.resize(len); + vsnprintf(&result[0], len + 1, format, args); + va_end(args); + return result; +} + +template +class HasExtendedFinalizer { + private: + template + struct SFINAE {}; + template + static char test(SFINAE*); + template + static int test(...); + + public: + static constexpr bool value = sizeof(test(0)) == sizeof(char); +}; + +template +class HasBasicFinalizer { + private: + template + struct SFINAE {}; + template + static char test(SFINAE*); + template + static int test(...); + + public: + static constexpr bool value = sizeof(test(0)) == sizeof(char); +}; + +} // namespace details + +#ifndef NODE_ADDON_API_DISABLE_DEPRECATED +#include "napi-inl.deprecated.h" +#endif // !NODE_ADDON_API_DISABLE_DEPRECATED + +//////////////////////////////////////////////////////////////////////////////// +// Module registration +//////////////////////////////////////////////////////////////////////////////// + +// Register an add-on based on an initializer function. +#define NODE_API_MODULE(modname, regfunc) \ + static napi_value __napi_##regfunc(napi_env env, napi_value exports) { \ + return Napi::RegisterModule(env, exports, regfunc); \ + } \ + NAPI_MODULE(modname, __napi_##regfunc) + +// Register an add-on based on a subclass of `Addon` with a custom Node.js +// module name. +#define NODE_API_NAMED_ADDON(modname, classname) \ + static napi_value __napi_##classname(napi_env env, napi_value exports) { \ + return Napi::RegisterModule(env, exports, &classname::Init); \ + } \ + NAPI_MODULE(modname, __napi_##classname) + +// Register an add-on based on a subclass of `Addon` with the Node.js module +// name given by node-gyp from the `target_name` in binding.gyp. +#define NODE_API_ADDON(classname) \ + NODE_API_NAMED_ADDON(NODE_GYP_MODULE_NAME, classname) + +// Adapt the NAPI_MODULE registration function: +// - Wrap the arguments in NAPI wrappers. +// - Catch any NAPI errors and rethrow as JS exceptions. +inline napi_value RegisterModule(napi_env env, + napi_value exports, + ModuleRegisterCallback registerCallback) { + return details::WrapCallback(env, [&] { + return napi_value( + registerCallback(Napi::Env(env), Napi::Object(env, exports))); + }); +} + +//////////////////////////////////////////////////////////////////////////////// +// Maybe class +//////////////////////////////////////////////////////////////////////////////// + +template +bool Maybe::IsNothing() const { + return !_has_value; +} + +template +bool Maybe::IsJust() const { + return _has_value; +} + +template +void Maybe::Check() const { + NAPI_CHECK(IsJust(), "Napi::Maybe::Check", "Maybe value is Nothing."); +} + +template +T Maybe::Unwrap() const { + NAPI_CHECK(IsJust(), "Napi::Maybe::Unwrap", "Maybe value is Nothing."); + return _value; +} + +template +T Maybe::UnwrapOr(const T& default_value) const { + return _has_value ? _value : default_value; +} + +template +bool Maybe::UnwrapTo(T* out) const { + if (IsJust()) { + *out = _value; + return true; + }; + return false; +} + +template +bool Maybe::operator==(const Maybe& other) const { + return (IsJust() == other.IsJust()) && + (!IsJust() || Unwrap() == other.Unwrap()); +} + +template +bool Maybe::operator!=(const Maybe& other) const { + return !operator==(other); +} + +template +Maybe::Maybe() : _has_value(false) {} + +template +Maybe::Maybe(const T& t) : _has_value(true), _value(t) {} + +template +inline Maybe Nothing() { + return Maybe(); +} + +template +inline Maybe Just(const T& t) { + return Maybe(t); +} + +//////////////////////////////////////////////////////////////////////////////// +// BasicEnv / Env class +//////////////////////////////////////////////////////////////////////////////// + +inline BasicEnv::BasicEnv(node_addon_api_basic_env env) : _env(env) {} + +inline BasicEnv::operator node_addon_api_basic_env() const { + return _env; +} + +inline Env::Env(napi_env env) : BasicEnv(env) {} + +inline Env::operator napi_env() const { + return const_cast(_env); +} + +inline Object Env::Global() const { + napi_value value; + napi_status status = napi_get_global(*this, &value); + NAPI_THROW_IF_FAILED(*this, status, Object()); + return Object(*this, value); +} + +inline Value Env::Undefined() const { + napi_value value; + napi_status status = napi_get_undefined(*this, &value); + NAPI_THROW_IF_FAILED(*this, status, Value()); + return Value(*this, value); +} + +inline Value Env::Null() const { + napi_value value; + napi_status status = napi_get_null(*this, &value); + NAPI_THROW_IF_FAILED(*this, status, Value()); + return Value(*this, value); +} + +inline bool Env::IsExceptionPending() const { + bool result; + napi_status status = napi_is_exception_pending(*this, &result); + if (status != napi_ok) + result = false; // Checking for a pending exception shouldn't throw. + return result; +} + +inline Error Env::GetAndClearPendingException() const { + napi_value value; + napi_status status = napi_get_and_clear_last_exception(*this, &value); + if (status != napi_ok) { + // Don't throw another exception when failing to get the exception! + return Error(); + } + return Error(*this, value); +} + +inline MaybeOrValue Env::RunScript(const char* utf8script) const { + String script = String::New(*this, utf8script); + return RunScript(script); +} + +inline MaybeOrValue Env::RunScript(const std::string& utf8script) const { + return RunScript(utf8script.c_str()); +} + +inline MaybeOrValue Env::RunScript(String script) const { + napi_value result; + napi_status status = napi_run_script(*this, script, &result); + NAPI_RETURN_OR_THROW_IF_FAILED( + *this, status, Napi::Value(*this, result), Napi::Value); +} + +#if NAPI_VERSION > 2 +template +void BasicEnv::CleanupHook::Wrapper(void* data) NAPI_NOEXCEPT { + auto* cleanupData = static_cast< + typename Napi::BasicEnv::CleanupHook::CleanupData*>(data); + cleanupData->hook(); + delete cleanupData; +} + +template +void BasicEnv::CleanupHook::WrapperWithArg(void* data) + NAPI_NOEXCEPT { + auto* cleanupData = static_cast< + typename Napi::BasicEnv::CleanupHook::CleanupData*>(data); + cleanupData->hook(static_cast(cleanupData->arg)); + delete cleanupData; +} +#endif // NAPI_VERSION > 2 + +#if NAPI_VERSION > 5 +template fini> +inline void BasicEnv::SetInstanceData(T* data) const { + napi_status status = napi_set_instance_data( + _env, + data, + [](napi_env env, void* data, void*) { fini(env, static_cast(data)); }, + nullptr); + NAPI_FATAL_IF_FAILED( + status, "BasicEnv::SetInstanceData", "invalid arguments"); +} + +template fini> +inline void BasicEnv::SetInstanceData(DataType* data, HintType* hint) const { + napi_status status = napi_set_instance_data( + _env, + data, + [](napi_env env, void* data, void* hint) { + fini(env, static_cast(data), static_cast(hint)); + }, + hint); + NAPI_FATAL_IF_FAILED( + status, "BasicEnv::SetInstanceData", "invalid arguments"); +} + +template +inline T* BasicEnv::GetInstanceData() const { + void* data = nullptr; + + napi_status status = napi_get_instance_data(_env, &data); + NAPI_FATAL_IF_FAILED( + status, "BasicEnv::GetInstanceData", "invalid arguments"); + + return static_cast(data); +} + +template +void BasicEnv::DefaultFini(Env, T* data) { + delete data; +} + +template +void BasicEnv::DefaultFiniWithHint(Env, DataType* data, HintType*) { + delete data; +} +#endif // NAPI_VERSION > 5 + +#if NAPI_VERSION > 8 +inline const char* BasicEnv::GetModuleFileName() const { + const char* result; + napi_status status = node_api_get_module_file_name(_env, &result); + NAPI_FATAL_IF_FAILED( + status, "BasicEnv::GetModuleFileName", "invalid arguments"); + return result; +} +#endif // NAPI_VERSION > 8 +//////////////////////////////////////////////////////////////////////////////// +// Value class +//////////////////////////////////////////////////////////////////////////////// + +inline Value::Value() : _env(nullptr), _value(nullptr) {} + +inline Value::Value(napi_env env, napi_value value) + : _env(env), _value(value) {} + +inline Value::operator napi_value() const { + return _value; +} + +inline bool Value::operator==(const Value& other) const { + return StrictEquals(other); +} + +inline bool Value::operator!=(const Value& other) const { + return !this->operator==(other); +} + +inline bool Value::StrictEquals(const Value& other) const { + bool result; + napi_status status = napi_strict_equals(_env, *this, other, &result); + NAPI_THROW_IF_FAILED(_env, status, false); + return result; +} + +inline Napi::Env Value::Env() const { + return Napi::Env(_env); +} + +inline bool Value::IsEmpty() const { + return _value == nullptr; +} + +inline napi_valuetype Value::Type() const { + if (IsEmpty()) { + return napi_undefined; + } + + napi_valuetype type; + napi_status status = napi_typeof(_env, _value, &type); + NAPI_THROW_IF_FAILED(_env, status, napi_undefined); + return type; +} + +inline bool Value::IsUndefined() const { + return Type() == napi_undefined; +} + +inline bool Value::IsNull() const { + return Type() == napi_null; +} + +inline bool Value::IsBoolean() const { + return Type() == napi_boolean; +} + +inline bool Value::IsNumber() const { + return Type() == napi_number; +} + +#if NAPI_VERSION > 5 +inline bool Value::IsBigInt() const { + return Type() == napi_bigint; +} +#endif // NAPI_VERSION > 5 + +#if (NAPI_VERSION > 4) +inline bool Value::IsDate() const { + if (IsEmpty()) { + return false; + } + + bool result; + napi_status status = napi_is_date(_env, _value, &result); + NAPI_THROW_IF_FAILED(_env, status, false); + return result; +} +#endif + +inline bool Value::IsString() const { + return Type() == napi_string; +} + +inline bool Value::IsSymbol() const { + return Type() == napi_symbol; +} + +inline bool Value::IsArray() const { + if (IsEmpty()) { + return false; + } + + bool result; + napi_status status = napi_is_array(_env, _value, &result); + NAPI_THROW_IF_FAILED(_env, status, false); + return result; +} + +inline bool Value::IsArrayBuffer() const { + if (IsEmpty()) { + return false; + } + + bool result; + napi_status status = napi_is_arraybuffer(_env, _value, &result); + NAPI_THROW_IF_FAILED(_env, status, false); + return result; +} + +inline bool Value::IsTypedArray() const { + if (IsEmpty()) { + return false; + } + + bool result; + napi_status status = napi_is_typedarray(_env, _value, &result); + NAPI_THROW_IF_FAILED(_env, status, false); + return result; +} + +inline bool Value::IsObject() const { + return Type() == napi_object || IsFunction(); +} + +inline bool Value::IsFunction() const { + return Type() == napi_function; +} + +inline bool Value::IsPromise() const { + if (IsEmpty()) { + return false; + } + + bool result; + napi_status status = napi_is_promise(_env, _value, &result); + NAPI_THROW_IF_FAILED(_env, status, false); + return result; +} + +inline bool Value::IsDataView() const { + if (IsEmpty()) { + return false; + } + + bool result; + napi_status status = napi_is_dataview(_env, _value, &result); + NAPI_THROW_IF_FAILED(_env, status, false); + return result; +} + +inline bool Value::IsBuffer() const { + if (IsEmpty()) { + return false; + } + + bool result; + napi_status status = napi_is_buffer(_env, _value, &result); + NAPI_THROW_IF_FAILED(_env, status, false); + return result; +} + +inline bool Value::IsExternal() const { + return Type() == napi_external; +} + +#ifdef NODE_API_EXPERIMENTAL_HAS_SHAREDARRAYBUFFER +inline bool Value::IsSharedArrayBuffer() const { + if (IsEmpty()) { + return false; + } + + bool result; + napi_status status = node_api_is_sharedarraybuffer(_env, _value, &result); + NAPI_THROW_IF_FAILED(_env, status, false); + return result; +} +#endif + +template +inline T Value::As() const { +#ifdef NODE_ADDON_API_ENABLE_TYPE_CHECK_ON_AS + T::CheckCast(_env, _value); +#endif + return T(_env, _value); +} + +template +inline T Value::UnsafeAs() const { + return T(_env, _value); +} + +// static +inline void Value::CheckCast(napi_env /* env */, napi_value value) { + NAPI_CHECK(value != nullptr, "Value::CheckCast", "empty value"); +} + +inline MaybeOrValue Value::ToBoolean() const { + napi_value result; + napi_status status = napi_coerce_to_bool(_env, _value, &result); + NAPI_RETURN_OR_THROW_IF_FAILED( + _env, status, Napi::Boolean(_env, result), Napi::Boolean); +} + +inline MaybeOrValue Value::ToNumber() const { + napi_value result; + napi_status status = napi_coerce_to_number(_env, _value, &result); + NAPI_RETURN_OR_THROW_IF_FAILED( + _env, status, Napi::Number(_env, result), Napi::Number); +} + +inline MaybeOrValue Value::ToString() const { + napi_value result; + napi_status status = napi_coerce_to_string(_env, _value, &result); + NAPI_RETURN_OR_THROW_IF_FAILED( + _env, status, Napi::String(_env, result), Napi::String); +} + +inline MaybeOrValue Value::ToObject() const { + napi_value result; + napi_status status = napi_coerce_to_object(_env, _value, &result); + NAPI_RETURN_OR_THROW_IF_FAILED( + _env, status, Napi::Object(_env, result), Napi::Object); +} + +//////////////////////////////////////////////////////////////////////////////// +// Boolean class +//////////////////////////////////////////////////////////////////////////////// + +inline Boolean Boolean::New(napi_env env, bool val) { + napi_value value; + napi_status status = napi_get_boolean(env, val, &value); + NAPI_THROW_IF_FAILED(env, status, Boolean()); + return Boolean(env, value); +} + +inline void Boolean::CheckCast(napi_env env, napi_value value) { + NAPI_CHECK(value != nullptr, "Boolean::CheckCast", "empty value"); + + napi_valuetype type; + napi_status status = napi_typeof(env, value, &type); + NAPI_CHECK(status == napi_ok, "Boolean::CheckCast", "napi_typeof failed"); + NAPI_INTERNAL_CHECK_EQ(type, napi_boolean, "%d", "Boolean::CheckCast"); +} + +inline Boolean::Boolean() : Napi::Value() {} + +inline Boolean::Boolean(napi_env env, napi_value value) + : Napi::Value(env, value) {} + +inline Boolean::operator bool() const { + return Value(); +} + +inline bool Boolean::Value() const { + bool result; + napi_status status = napi_get_value_bool(_env, _value, &result); + NAPI_THROW_IF_FAILED(_env, status, false); + return result; +} + +//////////////////////////////////////////////////////////////////////////////// +// Number class +//////////////////////////////////////////////////////////////////////////////// + +inline Number Number::New(napi_env env, double val) { + napi_value value; + napi_status status = napi_create_double(env, val, &value); + NAPI_THROW_IF_FAILED(env, status, Number()); + return Number(env, value); +} + +inline void Number::CheckCast(napi_env env, napi_value value) { + NAPI_CHECK(value != nullptr, "Number::CheckCast", "empty value"); + + napi_valuetype type; + napi_status status = napi_typeof(env, value, &type); + NAPI_CHECK(status == napi_ok, "Number::CheckCast", "napi_typeof failed"); + NAPI_INTERNAL_CHECK_EQ(type, napi_number, "%d", "Number::CheckCast"); +} + +inline Number::Number() : Value() {} + +inline Number::Number(napi_env env, napi_value value) : Value(env, value) {} + +inline Number::operator int32_t() const { + return Int32Value(); +} + +inline Number::operator uint32_t() const { + return Uint32Value(); +} + +inline Number::operator int64_t() const { + return Int64Value(); +} + +inline Number::operator float() const { + return FloatValue(); +} + +inline Number::operator double() const { + return DoubleValue(); +} + +inline int32_t Number::Int32Value() const { + int32_t result; + napi_status status = napi_get_value_int32(_env, _value, &result); + NAPI_THROW_IF_FAILED(_env, status, 0); + return result; +} + +inline uint32_t Number::Uint32Value() const { + uint32_t result; + napi_status status = napi_get_value_uint32(_env, _value, &result); + NAPI_THROW_IF_FAILED(_env, status, 0); + return result; +} + +inline int64_t Number::Int64Value() const { + int64_t result; + napi_status status = napi_get_value_int64(_env, _value, &result); + NAPI_THROW_IF_FAILED(_env, status, 0); + return result; +} + +inline float Number::FloatValue() const { + return static_cast(DoubleValue()); +} + +inline double Number::DoubleValue() const { + double result; + napi_status status = napi_get_value_double(_env, _value, &result); + NAPI_THROW_IF_FAILED(_env, status, 0); + return result; +} + +#if NAPI_VERSION > 5 +//////////////////////////////////////////////////////////////////////////////// +// BigInt Class +//////////////////////////////////////////////////////////////////////////////// + +inline BigInt BigInt::New(napi_env env, int64_t val) { + napi_value value; + napi_status status = napi_create_bigint_int64(env, val, &value); + NAPI_THROW_IF_FAILED(env, status, BigInt()); + return BigInt(env, value); +} + +inline BigInt BigInt::New(napi_env env, uint64_t val) { + napi_value value; + napi_status status = napi_create_bigint_uint64(env, val, &value); + NAPI_THROW_IF_FAILED(env, status, BigInt()); + return BigInt(env, value); +} + +inline BigInt BigInt::New(napi_env env, + int sign_bit, + size_t word_count, + const uint64_t* words) { + napi_value value; + napi_status status = + napi_create_bigint_words(env, sign_bit, word_count, words, &value); + NAPI_THROW_IF_FAILED(env, status, BigInt()); + return BigInt(env, value); +} + +inline void BigInt::CheckCast(napi_env env, napi_value value) { + NAPI_CHECK(value != nullptr, "BigInt::CheckCast", "empty value"); + + napi_valuetype type; + napi_status status = napi_typeof(env, value, &type); + NAPI_CHECK(status == napi_ok, "BigInt::CheckCast", "napi_typeof failed"); + NAPI_INTERNAL_CHECK_EQ(type, napi_bigint, "%d", "BigInt::CheckCast"); +} + +inline BigInt::BigInt() : Value() {} + +inline BigInt::BigInt(napi_env env, napi_value value) : Value(env, value) {} + +inline int64_t BigInt::Int64Value(bool* lossless) const { + int64_t result; + napi_status status = + napi_get_value_bigint_int64(_env, _value, &result, lossless); + NAPI_THROW_IF_FAILED(_env, status, 0); + return result; +} + +inline uint64_t BigInt::Uint64Value(bool* lossless) const { + uint64_t result; + napi_status status = + napi_get_value_bigint_uint64(_env, _value, &result, lossless); + NAPI_THROW_IF_FAILED(_env, status, 0); + return result; +} + +inline size_t BigInt::WordCount() const { + size_t word_count; + napi_status status = + napi_get_value_bigint_words(_env, _value, nullptr, &word_count, nullptr); + NAPI_THROW_IF_FAILED(_env, status, 0); + return word_count; +} + +inline void BigInt::ToWords(int* sign_bit, + size_t* word_count, + uint64_t* words) { + napi_status status = + napi_get_value_bigint_words(_env, _value, sign_bit, word_count, words); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} +#endif // NAPI_VERSION > 5 + +#if (NAPI_VERSION > 4) +//////////////////////////////////////////////////////////////////////////////// +// Date Class +//////////////////////////////////////////////////////////////////////////////// + +inline Date Date::New(napi_env env, double val) { + napi_value value; + napi_status status = napi_create_date(env, val, &value); + NAPI_THROW_IF_FAILED(env, status, Date()); + return Date(env, value); +} + +inline void Date::CheckCast(napi_env env, napi_value value) { + NAPI_CHECK(value != nullptr, "Date::CheckCast", "empty value"); + + bool result; + napi_status status = napi_is_date(env, value, &result); + NAPI_CHECK(status == napi_ok, "Date::CheckCast", "napi_is_date failed"); + NAPI_CHECK(result, "Date::CheckCast", "value is not date"); +} + +inline Date::Date() : Value() {} + +inline Date::Date(napi_env env, napi_value value) : Value(env, value) {} + +inline Date::operator double() const { + return ValueOf(); +} + +inline double Date::ValueOf() const { + double result; + napi_status status = napi_get_date_value(_env, _value, &result); + NAPI_THROW_IF_FAILED(_env, status, 0); + return result; +} +#endif + +//////////////////////////////////////////////////////////////////////////////// +// Name class +//////////////////////////////////////////////////////////////////////////////// +inline void Name::CheckCast(napi_env env, napi_value value) { + NAPI_CHECK(value != nullptr, "Name::CheckCast", "empty value"); + + napi_valuetype type; + napi_status status = napi_typeof(env, value, &type); + NAPI_CHECK(status == napi_ok, "Name::CheckCast", "napi_typeof failed"); + NAPI_INTERNAL_CHECK(type == napi_string || type == napi_symbol, + "Name::CheckCast", + "value is not napi_string or napi_symbol, got %d.", + type); +} + +inline Name::Name() : Value() {} + +inline Name::Name(napi_env env, napi_value value) : Value(env, value) {} + +//////////////////////////////////////////////////////////////////////////////// +// String class +//////////////////////////////////////////////////////////////////////////////// + +inline String String::New(napi_env env, const std::string& val) { + return String::New(env, val.c_str(), val.size()); +} + +inline String String::New(napi_env env, const std::u16string& val) { + return String::New(env, val.c_str(), val.size()); +} + +inline String String::New(napi_env env, const char* val) { + // TODO(@gabrielschulhof) Remove if-statement when core's error handling is + // available in all supported versions. + if (val == nullptr) { + // Throw an error that looks like it came from core. + NAPI_THROW_IF_FAILED(env, napi_invalid_arg, String()); + } + napi_value value; + napi_status status = + napi_create_string_utf8(env, val, std::strlen(val), &value); + NAPI_THROW_IF_FAILED(env, status, String()); + return String(env, value); +} + +inline String String::New(napi_env env, const char16_t* val) { + napi_value value; + // TODO(@gabrielschulhof) Remove if-statement when core's error handling is + // available in all supported versions. + if (val == nullptr) { + // Throw an error that looks like it came from core. + NAPI_THROW_IF_FAILED(env, napi_invalid_arg, String()); + } + napi_status status = + napi_create_string_utf16(env, val, std::u16string(val).size(), &value); + NAPI_THROW_IF_FAILED(env, status, String()); + return String(env, value); +} + +inline String String::New(napi_env env, const char* val, size_t length) { + napi_value value; + napi_status status = napi_create_string_utf8(env, val, length, &value); + NAPI_THROW_IF_FAILED(env, status, String()); + return String(env, value); +} + +inline String String::New(napi_env env, const char16_t* val, size_t length) { + napi_value value; + napi_status status = napi_create_string_utf16(env, val, length, &value); + NAPI_THROW_IF_FAILED(env, status, String()); + return String(env, value); +} + +inline void String::CheckCast(napi_env env, napi_value value) { + NAPI_CHECK(value != nullptr, "String::CheckCast", "empty value"); + + napi_valuetype type; + napi_status status = napi_typeof(env, value, &type); + NAPI_CHECK(status == napi_ok, "String::CheckCast", "napi_typeof failed"); + NAPI_INTERNAL_CHECK_EQ(type, napi_string, "%d", "String::CheckCast"); +} + +inline String::String() : Name() {} + +inline String::String(napi_env env, napi_value value) : Name(env, value) {} + +inline String::operator std::string() const { + return Utf8Value(); +} + +inline String::operator std::u16string() const { + return Utf16Value(); +} + +inline std::string String::Utf8Value() const { + size_t length; + napi_status status = + napi_get_value_string_utf8(_env, _value, nullptr, 0, &length); + NAPI_THROW_IF_FAILED(_env, status, ""); + + std::string value; + value.reserve(length + 1); + value.resize(length); + status = napi_get_value_string_utf8( + _env, _value, &value[0], value.capacity(), nullptr); + NAPI_THROW_IF_FAILED(_env, status, ""); + return value; +} + +inline std::u16string String::Utf16Value() const { + size_t length; + napi_status status = + napi_get_value_string_utf16(_env, _value, nullptr, 0, &length); + NAPI_THROW_IF_FAILED(_env, status, NAPI_WIDE_TEXT("")); + + std::u16string value; + value.reserve(length + 1); + value.resize(length); + status = napi_get_value_string_utf16( + _env, _value, &value[0], value.capacity(), nullptr); + NAPI_THROW_IF_FAILED(_env, status, NAPI_WIDE_TEXT("")); + return value; +} + +//////////////////////////////////////////////////////////////////////////////// +// Symbol class +//////////////////////////////////////////////////////////////////////////////// + +inline Symbol Symbol::New(napi_env env, const char* description) { + napi_value descriptionValue = description != nullptr + ? String::New(env, description) + : static_cast(nullptr); + return Symbol::New(env, descriptionValue); +} + +inline Symbol Symbol::New(napi_env env, const std::string& description) { + napi_value descriptionValue = String::New(env, description); + return Symbol::New(env, descriptionValue); +} + +inline Symbol Symbol::New(napi_env env, String description) { + napi_value descriptionValue = description; + return Symbol::New(env, descriptionValue); +} + +inline Symbol Symbol::New(napi_env env, napi_value description) { + napi_value value; + napi_status status = napi_create_symbol(env, description, &value); + NAPI_THROW_IF_FAILED(env, status, Symbol()); + return Symbol(env, value); +} + +inline MaybeOrValue Symbol::WellKnown(napi_env env, + const std::string& name) { + // No need to check if the return value is a symbol or undefined. + // Well known symbols are definite and it is an develop time error + // if the symbol does not exist. +#if defined(NODE_ADDON_API_ENABLE_MAYBE) + Value symbol_obj; + Value symbol_value; + if (Napi::Env(env).Global().Get("Symbol").UnwrapTo(&symbol_obj) && + symbol_obj.As().Get(name).UnwrapTo(&symbol_value)) { + return Just(symbol_value.UnsafeAs()); + } + return Nothing(); +#else + return Napi::Env(env) + .Global() + .Get("Symbol") + .As() + .Get(name) + .UnsafeAs(); +#endif +} + +inline MaybeOrValue Symbol::For(napi_env env, + const std::string& description) { + napi_value descriptionValue = String::New(env, description); + return Symbol::For(env, descriptionValue); +} + +inline MaybeOrValue Symbol::For(napi_env env, const char* description) { + napi_value descriptionValue = String::New(env, description); + return Symbol::For(env, descriptionValue); +} + +inline MaybeOrValue Symbol::For(napi_env env, String description) { + return Symbol::For(env, static_cast(description)); +} + +inline MaybeOrValue Symbol::For(napi_env env, napi_value description) { +#if defined(NODE_ADDON_API_ENABLE_MAYBE) + Value symbol_obj; + Value symbol_for_value; + Value symbol_value; + if (Napi::Env(env).Global().Get("Symbol").UnwrapTo(&symbol_obj) && + symbol_obj.As().Get("for").UnwrapTo(&symbol_for_value) && + symbol_for_value.As() + .Call(symbol_obj, {description}) + .UnwrapTo(&symbol_value)) { + return Just(symbol_value.As()); + } + return Nothing(); +#else + Object symbol_obj = Napi::Env(env).Global().Get("Symbol").As(); + return symbol_obj.Get("for") + .As() + .Call(symbol_obj, {description}) + .As(); +#endif +} + +inline void Symbol::CheckCast(napi_env env, napi_value value) { + NAPI_CHECK(value != nullptr, "Symbol::CheckCast", "empty value"); + + napi_valuetype type; + napi_status status = napi_typeof(env, value, &type); + NAPI_CHECK(status == napi_ok, "Symbol::CheckCast", "napi_typeof failed"); + NAPI_INTERNAL_CHECK_EQ(type, napi_symbol, "%d", "Symbol::CheckCast"); +} + +inline Symbol::Symbol() : Name() {} + +inline Symbol::Symbol(napi_env env, napi_value value) : Name(env, value) {} + +//////////////////////////////////////////////////////////////////////////////// +// Automagic value creation +//////////////////////////////////////////////////////////////////////////////// + +namespace details { +template +struct vf_number { + static Number From(napi_env env, T value) { + return Number::New(env, static_cast(value)); + } +}; + +template <> +struct vf_number { + static Boolean From(napi_env env, bool value) { + return Boolean::New(env, value); + } +}; + +struct vf_utf8_charp { + static String From(napi_env env, const char* value) { + return String::New(env, value); + } +}; + +struct vf_utf16_charp { + static String From(napi_env env, const char16_t* value) { + return String::New(env, value); + } +}; +struct vf_utf8_string { + static String From(napi_env env, const std::string& value) { + return String::New(env, value); + } +}; + +struct vf_utf16_string { + static String From(napi_env env, const std::u16string& value) { + return String::New(env, value); + } +}; + +template +struct vf_fallback { + static Value From(napi_env env, const T& value) { return Value(env, value); } +}; + +template +struct disjunction : std::false_type {}; +template +struct disjunction : B {}; +template +struct disjunction + : std::conditional>::type {}; + +template +struct can_make_string + : disjunction::type, + typename std::is_convertible::type, + typename std::is_convertible::type, + typename std::is_convertible::type> {}; +} // namespace details + +template +Value Value::From(napi_env env, const T& value) { + using Helper = typename std::conditional< + std::is_integral::value || std::is_floating_point::value, + details::vf_number, + typename std::conditional::value, + String, + details::vf_fallback>::type>::type; + return Helper::From(env, value); +} + +template +String String::From(napi_env env, const T& value) { + struct Dummy {}; + using Helper = typename std::conditional< + std::is_convertible::value, + details::vf_utf8_charp, + typename std::conditional< + std::is_convertible::value, + details::vf_utf16_charp, + typename std::conditional< + std::is_convertible::value, + details::vf_utf8_string, + typename std::conditional< + std::is_convertible::value, + details::vf_utf16_string, + Dummy>::type>::type>::type>::type; + return Helper::From(env, value); +} + +//////////////////////////////////////////////////////////////////////////////// +// TypeTaggable class +//////////////////////////////////////////////////////////////////////////////// + +inline TypeTaggable::TypeTaggable() : Value() {} + +inline TypeTaggable::TypeTaggable(napi_env _env, napi_value _value) + : Value(_env, _value) {} + +#if NAPI_VERSION >= 8 + +inline void TypeTaggable::TypeTag(const napi_type_tag* type_tag) const { + napi_status status = napi_type_tag_object(_env, _value, type_tag); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} + +inline bool TypeTaggable::CheckTypeTag(const napi_type_tag* type_tag) const { + bool result; + napi_status status = + napi_check_object_type_tag(_env, _value, type_tag, &result); + NAPI_THROW_IF_FAILED(_env, status, false); + return result; +} + +#endif // NAPI_VERSION >= 8 + +//////////////////////////////////////////////////////////////////////////////// +// Object class +//////////////////////////////////////////////////////////////////////////////// + +template +inline Object::PropertyLValue::operator Value() const { + MaybeOrValue val = Object(_env, _object).Get(_key); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + return val.Unwrap(); +#else + return val; +#endif +} + +template +template +inline Object::PropertyLValue& Object::PropertyLValue::operator=( + ValueType value) { +#ifdef NODE_ADDON_API_ENABLE_MAYBE + MaybeOrValue result = +#endif + Object(_env, _object).Set(_key, value); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + result.Unwrap(); +#endif + return *this; +} + +template +inline Value Object::PropertyLValue::AsValue() const { + return Value(*this); +} + +template +inline Object::PropertyLValue::PropertyLValue(Object object, Key key) + : _env(object.Env()), _object(object), _key(key) {} + +inline Object Object::New(napi_env env) { + napi_value value; + napi_status status = napi_create_object(env, &value); + NAPI_THROW_IF_FAILED(env, status, Object()); + return Object(env, value); +} + +inline void Object::CheckCast(napi_env env, napi_value value) { + NAPI_CHECK(value != nullptr, "Object::CheckCast", "empty value"); + + napi_valuetype type; + napi_status status = napi_typeof(env, value, &type); + NAPI_CHECK(status == napi_ok, "Object::CheckCast", "napi_typeof failed"); + NAPI_INTERNAL_CHECK(type == napi_object || type == napi_function, + "Object::CheckCast", + "Expect napi_object or napi_function, but got %d.", + type); +} + +inline Object::Object() : TypeTaggable() {} + +inline Object::Object(napi_env env, napi_value value) + : TypeTaggable(env, value) {} + +inline Object::PropertyLValue Object::operator[]( + const char* utf8name) { + return PropertyLValue(*this, utf8name); +} + +inline Object::PropertyLValue Object::operator[]( + const std::string& utf8name) { + return PropertyLValue(*this, utf8name); +} + +inline Object::PropertyLValue Object::operator[](uint32_t index) { + return PropertyLValue(*this, index); +} + +inline Object::PropertyLValue Object::operator[](Value index) const { + return PropertyLValue(*this, index); +} + +inline MaybeOrValue Object::operator[](const char* utf8name) const { + return Get(utf8name); +} + +inline MaybeOrValue Object::operator[]( + const std::string& utf8name) const { + return Get(utf8name); +} + +inline MaybeOrValue Object::operator[](uint32_t index) const { + return Get(index); +} + +inline MaybeOrValue Object::Has(napi_value key) const { + bool result; + napi_status status = napi_has_property(_env, _value, key, &result); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, result, bool); +} + +inline MaybeOrValue Object::Has(Value key) const { + bool result; + napi_status status = napi_has_property(_env, _value, key, &result); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, result, bool); +} + +inline MaybeOrValue Object::Has(const char* utf8name) const { + bool result; + napi_status status = napi_has_named_property(_env, _value, utf8name, &result); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, result, bool); +} + +inline MaybeOrValue Object::Has(const std::string& utf8name) const { + return Has(utf8name.c_str()); +} + +inline MaybeOrValue Object::HasOwnProperty(napi_value key) const { + bool result; + napi_status status = napi_has_own_property(_env, _value, key, &result); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, result, bool); +} + +inline MaybeOrValue Object::HasOwnProperty(Value key) const { + bool result; + napi_status status = napi_has_own_property(_env, _value, key, &result); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, result, bool); +} + +inline MaybeOrValue Object::HasOwnProperty(const char* utf8name) const { + napi_value key; + napi_status status = + napi_create_string_utf8(_env, utf8name, std::strlen(utf8name), &key); + NAPI_MAYBE_THROW_IF_FAILED(_env, status, bool); + return HasOwnProperty(key); +} + +inline MaybeOrValue Object::HasOwnProperty( + const std::string& utf8name) const { + return HasOwnProperty(utf8name.c_str()); +} + +inline MaybeOrValue Object::Get(napi_value key) const { + napi_value result; + napi_status status = napi_get_property(_env, _value, key, &result); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, Value(_env, result), Value); +} + +inline MaybeOrValue Object::Get(Value key) const { + napi_value result; + napi_status status = napi_get_property(_env, _value, key, &result); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, Value(_env, result), Value); +} + +inline MaybeOrValue Object::Get(const char* utf8name) const { + napi_value result; + napi_status status = napi_get_named_property(_env, _value, utf8name, &result); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, Value(_env, result), Value); +} + +inline MaybeOrValue Object::Get(const std::string& utf8name) const { + return Get(utf8name.c_str()); +} + +template +inline MaybeOrValue Object::Set(napi_value key, + const ValueType& value) const { + napi_status status = + napi_set_property(_env, _value, key, Value::From(_env, value)); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, status == napi_ok, bool); +} + +template +inline MaybeOrValue Object::Set(Value key, const ValueType& value) const { + napi_status status = + napi_set_property(_env, _value, key, Value::From(_env, value)); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, status == napi_ok, bool); +} + +template +inline MaybeOrValue Object::Set(const char* utf8name, + const ValueType& value) const { + napi_status status = + napi_set_named_property(_env, _value, utf8name, Value::From(_env, value)); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, status == napi_ok, bool); +} + +template +inline MaybeOrValue Object::Set(const std::string& utf8name, + const ValueType& value) const { + return Set(utf8name.c_str(), value); +} + +inline MaybeOrValue Object::Delete(napi_value key) const { + bool result; + napi_status status = napi_delete_property(_env, _value, key, &result); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, result, bool); +} + +inline MaybeOrValue Object::Delete(Value key) const { + bool result; + napi_status status = napi_delete_property(_env, _value, key, &result); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, result, bool); +} + +inline MaybeOrValue Object::Delete(const char* utf8name) const { + return Delete(String::New(_env, utf8name)); +} + +inline MaybeOrValue Object::Delete(const std::string& utf8name) const { + return Delete(String::New(_env, utf8name)); +} + +inline MaybeOrValue Object::Has(uint32_t index) const { + bool result; + napi_status status = napi_has_element(_env, _value, index, &result); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, result, bool); +} + +inline MaybeOrValue Object::Get(uint32_t index) const { + napi_value value; + napi_status status = napi_get_element(_env, _value, index, &value); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, Value(_env, value), Value); +} + +template +inline MaybeOrValue Object::Set(uint32_t index, + const ValueType& value) const { + napi_status status = + napi_set_element(_env, _value, index, Value::From(_env, value)); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, status == napi_ok, bool); +} + +inline MaybeOrValue Object::Delete(uint32_t index) const { + bool result; + napi_status status = napi_delete_element(_env, _value, index, &result); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, result, bool); +} + +inline MaybeOrValue Object::GetPropertyNames() const { + napi_value result; + napi_status status = napi_get_property_names(_env, _value, &result); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, Array(_env, result), Array); +} + +inline MaybeOrValue Object::DefineProperty( + const PropertyDescriptor& property) const { + napi_status status = napi_define_properties( + _env, + _value, + 1, + reinterpret_cast(&property)); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, status == napi_ok, bool); +} + +inline MaybeOrValue Object::DefineProperties( + const std::initializer_list& properties) const { + napi_status status = napi_define_properties( + _env, + _value, + properties.size(), + reinterpret_cast(properties.begin())); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, status == napi_ok, bool); +} + +inline MaybeOrValue Object::DefineProperties( + const std::vector& properties) const { + napi_status status = napi_define_properties( + _env, + _value, + properties.size(), + reinterpret_cast(properties.data())); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, status == napi_ok, bool); +} + +inline MaybeOrValue Object::InstanceOf( + const Function& constructor) const { + bool result; + napi_status status = napi_instanceof(_env, _value, constructor, &result); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, result, bool); +} + +template +inline void Object::AddFinalizer(Finalizer finalizeCallback, T* data) const { + details::FinalizeData* finalizeData = + new details::FinalizeData( + {std::move(finalizeCallback), nullptr}); + napi_status status = + details::AttachData::Wrapper>( + _env, *this, data, finalizeData); + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED_VOID(_env, status); + } +} + +template +inline void Object::AddFinalizer(Finalizer finalizeCallback, + T* data, + Hint* finalizeHint) const { + details::FinalizeData* finalizeData = + new details::FinalizeData( + {std::move(finalizeCallback), finalizeHint}); + napi_status status = details:: + AttachData::WrapperWithHint>( + _env, *this, data, finalizeData); + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED_VOID(_env, status); + } +} + +#ifdef NODE_ADDON_API_CPP_EXCEPTIONS +inline Object::const_iterator::const_iterator(const Object* object, + const Type type) { + _object = object; + _keys = object->GetPropertyNames(); + _index = type == Type::BEGIN ? 0 : _keys.Length(); +} + +inline Object::const_iterator Napi::Object::begin() const { + const_iterator it(this, Object::const_iterator::Type::BEGIN); + return it; +} + +inline Object::const_iterator Napi::Object::end() const { + const_iterator it(this, Object::const_iterator::Type::END); + return it; +} + +inline Object::const_iterator& Object::const_iterator::operator++() { + ++_index; + return *this; +} + +inline bool Object::const_iterator::operator==( + const const_iterator& other) const { + return _index == other._index; +} + +inline bool Object::const_iterator::operator!=( + const const_iterator& other) const { + return _index != other._index; +} + +inline const std::pair> +Object::const_iterator::operator*() const { + const Value key = _keys[_index]; + const PropertyLValue value = (*_object)[key]; + return {key, value}; +} + +inline Object::iterator::iterator(Object* object, const Type type) { + _object = object; + _keys = object->GetPropertyNames(); + _index = type == Type::BEGIN ? 0 : _keys.Length(); +} + +inline Object::iterator Napi::Object::begin() { + iterator it(this, Object::iterator::Type::BEGIN); + return it; +} + +inline Object::iterator Napi::Object::end() { + iterator it(this, Object::iterator::Type::END); + return it; +} + +inline Object::iterator& Object::iterator::operator++() { + ++_index; + return *this; +} + +inline bool Object::iterator::operator==(const iterator& other) const { + return _index == other._index; +} + +inline bool Object::iterator::operator!=(const iterator& other) const { + return _index != other._index; +} + +inline std::pair> +Object::iterator::operator*() { + Value key = _keys[_index]; + PropertyLValue value = (*_object)[key]; + return {key, value}; +} +#endif // NODE_ADDON_API_CPP_EXCEPTIONS + +#if NAPI_VERSION >= 8 +inline MaybeOrValue Object::Freeze() const { + napi_status status = napi_object_freeze(_env, _value); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, status == napi_ok, bool); +} + +inline MaybeOrValue Object::Seal() const { + napi_status status = napi_object_seal(_env, _value); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, status == napi_ok, bool); +} +#endif // NAPI_VERSION >= 8 + +//////////////////////////////////////////////////////////////////////////////// +// External class +//////////////////////////////////////////////////////////////////////////////// + +template +inline External External::New(napi_env env, T* data) { + napi_value value; + napi_status status = + napi_create_external(env, data, nullptr, nullptr, &value); + NAPI_THROW_IF_FAILED(env, status, External()); + return External(env, value); +} + +template +template +inline External External::New(napi_env env, + T* data, + Finalizer finalizeCallback) { + napi_value value; + details::FinalizeData* finalizeData = + new details::FinalizeData( + {std::move(finalizeCallback), nullptr}); + napi_status status = + napi_create_external(env, + data, + details::FinalizeData::Wrapper, + finalizeData, + &value); + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED(env, status, External()); + } + return External(env, value); +} + +template +template +inline External External::New(napi_env env, + T* data, + Finalizer finalizeCallback, + Hint* finalizeHint) { + napi_value value; + details::FinalizeData* finalizeData = + new details::FinalizeData( + {std::move(finalizeCallback), finalizeHint}); + napi_status status = napi_create_external( + env, + data, + details::FinalizeData::WrapperWithHint, + finalizeData, + &value); + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED(env, status, External()); + } + return External(env, value); +} + +template +inline void External::CheckCast(napi_env env, napi_value value) { + NAPI_CHECK(value != nullptr, "External::CheckCast", "empty value"); + + napi_valuetype type; + napi_status status = napi_typeof(env, value, &type); + NAPI_CHECK(status == napi_ok, "External::CheckCast", "napi_typeof failed"); + NAPI_INTERNAL_CHECK_EQ(type, napi_external, "%d", "External::CheckCast"); +} + +template +inline External::External() : TypeTaggable() {} + +template +inline External::External(napi_env env, napi_value value) + : TypeTaggable(env, value) {} + +template +inline T* External::Data() const { + void* data; + napi_status status = napi_get_value_external(_env, _value, &data); + NAPI_THROW_IF_FAILED(_env, status, nullptr); + return reinterpret_cast(data); +} + +//////////////////////////////////////////////////////////////////////////////// +// Array class +//////////////////////////////////////////////////////////////////////////////// + +inline Array Array::New(napi_env env) { + napi_value value; + napi_status status = napi_create_array(env, &value); + NAPI_THROW_IF_FAILED(env, status, Array()); + return Array(env, value); +} + +inline Array Array::New(napi_env env, size_t length) { + napi_value value; + napi_status status = napi_create_array_with_length(env, length, &value); + NAPI_THROW_IF_FAILED(env, status, Array()); + return Array(env, value); +} + +inline void Array::CheckCast(napi_env env, napi_value value) { + NAPI_CHECK(value != nullptr, "Array::CheckCast", "empty value"); + + bool result; + napi_status status = napi_is_array(env, value, &result); + NAPI_CHECK(status == napi_ok, "Array::CheckCast", "napi_is_array failed"); + NAPI_CHECK(result, "Array::CheckCast", "value is not array"); +} + +inline Array::Array() : Object() {} + +inline Array::Array(napi_env env, napi_value value) : Object(env, value) {} + +inline uint32_t Array::Length() const { + uint32_t result; + napi_status status = napi_get_array_length(_env, _value, &result); + NAPI_THROW_IF_FAILED(_env, status, 0); + return result; +} + +#ifdef NODE_API_EXPERIMENTAL_HAS_SHAREDARRAYBUFFER +//////////////////////////////////////////////////////////////////////////////// +// SharedArrayBuffer class +//////////////////////////////////////////////////////////////////////////////// + +inline SharedArrayBuffer::SharedArrayBuffer() : Object() {} + +inline SharedArrayBuffer::SharedArrayBuffer(napi_env env, napi_value value) + : Object(env, value) {} + +inline void SharedArrayBuffer::CheckCast(napi_env env, napi_value value) { + NAPI_CHECK(value != nullptr, "SharedArrayBuffer::CheckCast", "empty value"); + + bool result; + napi_status status = node_api_is_sharedarraybuffer(env, value, &result); + NAPI_CHECK(status == napi_ok, + "SharedArrayBuffer::CheckCast", + "node_api_is_sharedarraybuffer failed"); + NAPI_CHECK( + result, "SharedArrayBuffer::CheckCast", "value is not sharedarraybuffer"); +} + +inline SharedArrayBuffer SharedArrayBuffer::New(napi_env env, + size_t byteLength) { + napi_value value; + void* data; + napi_status status = + node_api_create_sharedarraybuffer(env, byteLength, &data, &value); + NAPI_THROW_IF_FAILED(env, status, SharedArrayBuffer()); + + return SharedArrayBuffer(env, value); +} + +inline void* SharedArrayBuffer::Data() { + void* data; + napi_status status = napi_get_arraybuffer_info(_env, _value, &data, nullptr); + NAPI_THROW_IF_FAILED(_env, status, nullptr); + return data; +} + +inline size_t SharedArrayBuffer::ByteLength() { + size_t length; + napi_status status = + napi_get_arraybuffer_info(_env, _value, nullptr, &length); + NAPI_THROW_IF_FAILED(_env, status, 0); + return length; +} +#endif // NODE_API_EXPERIMENTAL_HAS_SHAREDARRAYBUFFER + +//////////////////////////////////////////////////////////////////////////////// +// ArrayBuffer class +//////////////////////////////////////////////////////////////////////////////// + +inline ArrayBuffer ArrayBuffer::New(napi_env env, size_t byteLength) { + napi_value value; + void* data; + napi_status status = napi_create_arraybuffer(env, byteLength, &data, &value); + NAPI_THROW_IF_FAILED(env, status, ArrayBuffer()); + + return ArrayBuffer(env, value); +} + +#ifndef NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED +inline ArrayBuffer ArrayBuffer::New(napi_env env, + void* externalData, + size_t byteLength) { + napi_value value; + napi_status status = napi_create_external_arraybuffer( + env, externalData, byteLength, nullptr, nullptr, &value); + NAPI_THROW_IF_FAILED(env, status, ArrayBuffer()); + + return ArrayBuffer(env, value); +} + +template +inline ArrayBuffer ArrayBuffer::New(napi_env env, + void* externalData, + size_t byteLength, + Finalizer finalizeCallback) { + napi_value value; + details::FinalizeData* finalizeData = + new details::FinalizeData( + {std::move(finalizeCallback), nullptr}); + napi_status status = napi_create_external_arraybuffer( + env, + externalData, + byteLength, + details::FinalizeData::Wrapper, + finalizeData, + &value); + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED(env, status, ArrayBuffer()); + } + + return ArrayBuffer(env, value); +} + +template +inline ArrayBuffer ArrayBuffer::New(napi_env env, + void* externalData, + size_t byteLength, + Finalizer finalizeCallback, + Hint* finalizeHint) { + napi_value value; + details::FinalizeData* finalizeData = + new details::FinalizeData( + {std::move(finalizeCallback), finalizeHint}); + napi_status status = napi_create_external_arraybuffer( + env, + externalData, + byteLength, + details::FinalizeData::WrapperWithHint, + finalizeData, + &value); + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED(env, status, ArrayBuffer()); + } + + return ArrayBuffer(env, value); +} +#endif // NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED + +inline void ArrayBuffer::CheckCast(napi_env env, napi_value value) { + NAPI_CHECK(value != nullptr, "ArrayBuffer::CheckCast", "empty value"); + + bool result; + napi_status status = napi_is_arraybuffer(env, value, &result); + NAPI_CHECK(status == napi_ok, + "ArrayBuffer::CheckCast", + "napi_is_arraybuffer failed"); + NAPI_CHECK(result, "ArrayBuffer::CheckCast", "value is not arraybuffer"); +} + +inline ArrayBuffer::ArrayBuffer() : Object() {} + +inline ArrayBuffer::ArrayBuffer(napi_env env, napi_value value) + : Object(env, value) {} + +inline void* ArrayBuffer::Data() { + void* data; + napi_status status = napi_get_arraybuffer_info(_env, _value, &data, nullptr); + NAPI_THROW_IF_FAILED(_env, status, nullptr); + return data; +} + +inline size_t ArrayBuffer::ByteLength() { + size_t length; + napi_status status = + napi_get_arraybuffer_info(_env, _value, nullptr, &length); + NAPI_THROW_IF_FAILED(_env, status, 0); + return length; +} + +#if NAPI_VERSION >= 7 +inline bool ArrayBuffer::IsDetached() const { + bool detached; + napi_status status = napi_is_detached_arraybuffer(_env, _value, &detached); + NAPI_THROW_IF_FAILED(_env, status, false); + return detached; +} + +inline void ArrayBuffer::Detach() { + napi_status status = napi_detach_arraybuffer(_env, _value); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} +#endif // NAPI_VERSION >= 7 + +//////////////////////////////////////////////////////////////////////////////// +// DataView class +//////////////////////////////////////////////////////////////////////////////// +inline DataView DataView::New(napi_env env, Napi::ArrayBuffer arrayBuffer) { + return New(env, arrayBuffer, 0, arrayBuffer.ByteLength()); +} + +inline DataView DataView::New(napi_env env, + Napi::ArrayBuffer arrayBuffer, + size_t byteOffset) { + if (byteOffset > arrayBuffer.ByteLength()) { + NAPI_THROW(RangeError::New( + env, "Start offset is outside the bounds of the buffer"), + DataView()); + } + return New( + env, arrayBuffer, byteOffset, arrayBuffer.ByteLength() - byteOffset); +} + +inline DataView DataView::New(napi_env env, + Napi::ArrayBuffer arrayBuffer, + size_t byteOffset, + size_t byteLength) { + if (byteOffset + byteLength > arrayBuffer.ByteLength()) { + NAPI_THROW(RangeError::New(env, "Invalid DataView length"), DataView()); + } + napi_value value; + napi_status status = + napi_create_dataview(env, byteLength, arrayBuffer, byteOffset, &value); + NAPI_THROW_IF_FAILED(env, status, DataView()); + return DataView(env, value); +} + +inline void DataView::CheckCast(napi_env env, napi_value value) { + NAPI_CHECK(value != nullptr, "DataView::CheckCast", "empty value"); + + bool result; + napi_status status = napi_is_dataview(env, value, &result); + NAPI_CHECK( + status == napi_ok, "DataView::CheckCast", "napi_is_dataview failed"); + NAPI_CHECK(result, "DataView::CheckCast", "value is not dataview"); +} + +inline DataView::DataView() : Object() {} + +inline DataView::DataView(napi_env env, napi_value value) : Object(env, value) { + napi_status status = napi_get_dataview_info(_env, + _value /* dataView */, + &_length /* byteLength */, + &_data /* data */, + nullptr /* arrayBuffer */, + nullptr /* byteOffset */); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} + +inline Napi::ArrayBuffer DataView::ArrayBuffer() const { + napi_value arrayBuffer; + napi_status status = napi_get_dataview_info(_env, + _value /* dataView */, + nullptr /* byteLength */, + nullptr /* data */, + &arrayBuffer /* arrayBuffer */, + nullptr /* byteOffset */); + NAPI_THROW_IF_FAILED(_env, status, Napi::ArrayBuffer()); + return Napi::ArrayBuffer(_env, arrayBuffer); +} + +inline size_t DataView::ByteOffset() const { + size_t byteOffset; + napi_status status = napi_get_dataview_info(_env, + _value /* dataView */, + nullptr /* byteLength */, + nullptr /* data */, + nullptr /* arrayBuffer */, + &byteOffset /* byteOffset */); + NAPI_THROW_IF_FAILED(_env, status, 0); + return byteOffset; +} + +inline size_t DataView::ByteLength() const { + return _length; +} + +inline void* DataView::Data() const { + return _data; +} + +inline float DataView::GetFloat32(size_t byteOffset) const { + return ReadData(byteOffset); +} + +inline double DataView::GetFloat64(size_t byteOffset) const { + return ReadData(byteOffset); +} + +inline int8_t DataView::GetInt8(size_t byteOffset) const { + return ReadData(byteOffset); +} + +inline int16_t DataView::GetInt16(size_t byteOffset) const { + return ReadData(byteOffset); +} + +inline int32_t DataView::GetInt32(size_t byteOffset) const { + return ReadData(byteOffset); +} + +inline uint8_t DataView::GetUint8(size_t byteOffset) const { + return ReadData(byteOffset); +} + +inline uint16_t DataView::GetUint16(size_t byteOffset) const { + return ReadData(byteOffset); +} + +inline uint32_t DataView::GetUint32(size_t byteOffset) const { + return ReadData(byteOffset); +} + +inline void DataView::SetFloat32(size_t byteOffset, float value) const { + WriteData(byteOffset, value); +} + +inline void DataView::SetFloat64(size_t byteOffset, double value) const { + WriteData(byteOffset, value); +} + +inline void DataView::SetInt8(size_t byteOffset, int8_t value) const { + WriteData(byteOffset, value); +} + +inline void DataView::SetInt16(size_t byteOffset, int16_t value) const { + WriteData(byteOffset, value); +} + +inline void DataView::SetInt32(size_t byteOffset, int32_t value) const { + WriteData(byteOffset, value); +} + +inline void DataView::SetUint8(size_t byteOffset, uint8_t value) const { + WriteData(byteOffset, value); +} + +inline void DataView::SetUint16(size_t byteOffset, uint16_t value) const { + WriteData(byteOffset, value); +} + +inline void DataView::SetUint32(size_t byteOffset, uint32_t value) const { + WriteData(byteOffset, value); +} + +template +inline T DataView::ReadData(size_t byteOffset) const { + if (byteOffset + sizeof(T) > _length || + byteOffset + sizeof(T) < byteOffset) { // overflow + NAPI_THROW( + RangeError::New(_env, "Offset is outside the bounds of the DataView"), + 0); + } + + return *reinterpret_cast(static_cast(_data) + byteOffset); +} + +template +inline void DataView::WriteData(size_t byteOffset, T value) const { + if (byteOffset + sizeof(T) > _length || + byteOffset + sizeof(T) < byteOffset) { // overflow + NAPI_THROW_VOID( + RangeError::New(_env, "Offset is outside the bounds of the DataView")); + } + + *reinterpret_cast(static_cast(_data) + byteOffset) = value; +} + +//////////////////////////////////////////////////////////////////////////////// +// TypedArray class +//////////////////////////////////////////////////////////////////////////////// +inline void TypedArray::CheckCast(napi_env env, napi_value value) { + NAPI_CHECK(value != nullptr, "TypedArray::CheckCast", "empty value"); + + bool result; + napi_status status = napi_is_typedarray(env, value, &result); + NAPI_CHECK( + status == napi_ok, "TypedArray::CheckCast", "napi_is_typedarray failed"); + NAPI_CHECK(result, "TypedArray::CheckCast", "value is not typedarray"); +} + +inline TypedArray::TypedArray() + : Object(), _type(napi_typedarray_type::napi_int8_array), _length(0) {} + +inline TypedArray::TypedArray(napi_env env, napi_value value) + : Object(env, value), + _type(napi_typedarray_type::napi_int8_array), + _length(0) { + if (value != nullptr) { + napi_status status = + napi_get_typedarray_info(_env, + _value, + &const_cast(this)->_type, + &const_cast(this)->_length, + nullptr, + nullptr, + nullptr); + NAPI_THROW_IF_FAILED_VOID(_env, status); + } +} + +inline TypedArray::TypedArray(napi_env env, + napi_value value, + napi_typedarray_type type, + size_t length) + : Object(env, value), _type(type), _length(length) {} + +inline napi_typedarray_type TypedArray::TypedArrayType() const { + return _type; +} + +inline uint8_t TypedArray::ElementSize() const { + switch (_type) { + case napi_int8_array: + case napi_uint8_array: + case napi_uint8_clamped_array: + return 1; + case napi_int16_array: + case napi_uint16_array: + return 2; + case napi_int32_array: + case napi_uint32_array: + case napi_float32_array: + return 4; + case napi_float64_array: +#if (NAPI_VERSION > 5) + case napi_bigint64_array: + case napi_biguint64_array: +#endif // (NAPI_VERSION > 5) + return 8; + default: + return 0; + } +} + +inline size_t TypedArray::ElementLength() const { + return _length; +} + +inline size_t TypedArray::ByteOffset() const { + size_t byteOffset; + napi_status status = napi_get_typedarray_info( + _env, _value, nullptr, nullptr, nullptr, nullptr, &byteOffset); + NAPI_THROW_IF_FAILED(_env, status, 0); + return byteOffset; +} + +inline size_t TypedArray::ByteLength() const { + return ElementSize() * ElementLength(); +} + +inline Napi::ArrayBuffer TypedArray::ArrayBuffer() const { + napi_value arrayBuffer; + napi_status status = napi_get_typedarray_info( + _env, _value, nullptr, nullptr, nullptr, &arrayBuffer, nullptr); + NAPI_THROW_IF_FAILED(_env, status, Napi::ArrayBuffer()); + return Napi::ArrayBuffer(_env, arrayBuffer); +} + +//////////////////////////////////////////////////////////////////////////////// +// TypedArrayOf class +//////////////////////////////////////////////////////////////////////////////// +template +inline void TypedArrayOf::CheckCast(napi_env env, napi_value value) { + TypedArray::CheckCast(env, value); + napi_typedarray_type type; + napi_status status = napi_get_typedarray_info( + env, value, &type, nullptr, nullptr, nullptr, nullptr); + NAPI_CHECK(status == napi_ok, + "TypedArrayOf::CheckCast", + "napi_is_typedarray failed"); + + NAPI_INTERNAL_CHECK( + (type == TypedArrayTypeForPrimitiveType() || + (type == napi_uint8_clamped_array && std::is_same::value)), + "TypedArrayOf::CheckCast", + "Array type must match the template parameter, (Uint8 arrays may " + "optionally have the \"clamped\" array type.), got %d.", + type); +} + +template +inline TypedArrayOf TypedArrayOf::New(napi_env env, + size_t elementLength, + napi_typedarray_type type) { + Napi::ArrayBuffer arrayBuffer = + Napi::ArrayBuffer::New(env, elementLength * sizeof(T)); + return New(env, elementLength, arrayBuffer, 0, type); +} + +template +inline TypedArrayOf TypedArrayOf::New(napi_env env, + size_t elementLength, + Napi::ArrayBuffer arrayBuffer, + size_t bufferOffset, + napi_typedarray_type type) { + napi_value value; + napi_status status = napi_create_typedarray( + env, type, elementLength, arrayBuffer, bufferOffset, &value); + NAPI_THROW_IF_FAILED(env, status, TypedArrayOf()); + + return TypedArrayOf( + env, + value, + type, + elementLength, + reinterpret_cast(reinterpret_cast(arrayBuffer.Data()) + + bufferOffset)); +} + +template +inline TypedArrayOf::TypedArrayOf() : TypedArray(), _data(nullptr) {} + +template +inline TypedArrayOf::TypedArrayOf(napi_env env, napi_value value) + : TypedArray(env, value), _data(nullptr) { + napi_status status = napi_ok; + if (value != nullptr) { + void* data = nullptr; + status = napi_get_typedarray_info( + _env, _value, &_type, &_length, &data, nullptr, nullptr); + _data = static_cast(data); + } else { + _type = TypedArrayTypeForPrimitiveType(); + _length = 0; + } + NAPI_THROW_IF_FAILED_VOID(_env, status); +} + +template +inline TypedArrayOf::TypedArrayOf(napi_env env, + napi_value value, + napi_typedarray_type type, + size_t length, + T* data) + : TypedArray(env, value, type, length), _data(data) { + if (!(type == TypedArrayTypeForPrimitiveType() || + (type == napi_uint8_clamped_array && + std::is_same::value))) { + NAPI_THROW_VOID(TypeError::New( + env, + "Array type must match the template parameter. " + "(Uint8 arrays may optionally have the \"clamped\" array type.)")); + } +} + +template +inline T& TypedArrayOf::operator[](size_t index) { + return _data[index]; +} + +template +inline const T& TypedArrayOf::operator[](size_t index) const { + return _data[index]; +} + +template +inline T* TypedArrayOf::Data() { + return _data; +} + +template +inline const T* TypedArrayOf::Data() const { + return _data; +} + +//////////////////////////////////////////////////////////////////////////////// +// Function class +//////////////////////////////////////////////////////////////////////////////// + +template +inline napi_status CreateFunction(napi_env env, + const char* utf8name, + napi_callback cb, + CbData* data, + napi_value* result) { + napi_status status = + napi_create_function(env, utf8name, NAPI_AUTO_LENGTH, cb, data, result); + if (status == napi_ok) { + status = Napi::details::AttachData(env, *result, data); + } + + return status; +} + +template +inline Function Function::New(napi_env env, const char* utf8name, void* data) { + napi_value result = nullptr; + napi_status status = napi_create_function(env, + utf8name, + NAPI_AUTO_LENGTH, + details::TemplatedVoidCallback, + data, + &result); + NAPI_THROW_IF_FAILED(env, status, Function()); + return Function(env, result); +} + +template +inline Function Function::New(napi_env env, const char* utf8name, void* data) { + napi_value result = nullptr; + napi_status status = napi_create_function(env, + utf8name, + NAPI_AUTO_LENGTH, + details::TemplatedCallback, + data, + &result); + NAPI_THROW_IF_FAILED(env, status, Function()); + return Function(env, result); +} + +template +inline Function Function::New(napi_env env, + const std::string& utf8name, + void* data) { + return Function::New(env, utf8name.c_str(), data); +} + +template +inline Function Function::New(napi_env env, + const std::string& utf8name, + void* data) { + return Function::New(env, utf8name.c_str(), data); +} + +template +inline Function Function::New(napi_env env, + Callable cb, + const char* utf8name, + void* data) { + using ReturnType = decltype(cb(CallbackInfo(nullptr, nullptr))); + using CbData = details::CallbackData; + auto callbackData = new CbData{std::move(cb), data}; + + napi_value value; + napi_status status = + CreateFunction(env, utf8name, CbData::Wrapper, callbackData, &value); + if (status != napi_ok) { + delete callbackData; + NAPI_THROW_IF_FAILED(env, status, Function()); + } + + return Function(env, value); +} + +template +inline Function Function::New(napi_env env, + Callable cb, + const std::string& utf8name, + void* data) { + return New(env, cb, utf8name.c_str(), data); +} + +inline void Function::CheckCast(napi_env env, napi_value value) { + NAPI_CHECK(value != nullptr, "Function::CheckCast", "empty value"); + + napi_valuetype type; + napi_status status = napi_typeof(env, value, &type); + NAPI_CHECK(status == napi_ok, "Function::CheckCast", "napi_typeof failed"); + NAPI_INTERNAL_CHECK_EQ(type, napi_function, "%d", "Function::CheckCast"); +} + +inline Function::Function() : Object() {} + +inline Function::Function(napi_env env, napi_value value) + : Object(env, value) {} + +inline MaybeOrValue Function::operator()( + const std::initializer_list& args) const { + return Call(Env().Undefined(), args); +} + +inline MaybeOrValue Function::Call( + const std::initializer_list& args) const { + return Call(Env().Undefined(), args); +} + +inline MaybeOrValue Function::Call( + const std::vector& args) const { + return Call(Env().Undefined(), args); +} + +inline MaybeOrValue Function::Call( + const std::vector& args) const { + return Call(Env().Undefined(), args); +} + +inline MaybeOrValue Function::Call(size_t argc, + const napi_value* args) const { + return Call(Env().Undefined(), argc, args); +} + +inline MaybeOrValue Function::Call( + napi_value recv, const std::initializer_list& args) const { + return Call(recv, args.size(), args.begin()); +} + +inline MaybeOrValue Function::Call( + napi_value recv, const std::vector& args) const { + return Call(recv, args.size(), args.data()); +} + +inline MaybeOrValue Function::Call( + napi_value recv, const std::vector& args) const { + const size_t argc = args.size(); + const size_t stackArgsCount = 6; + napi_value stackArgs[stackArgsCount]; + std::vector heapArgs; + napi_value* argv; + if (argc <= stackArgsCount) { + argv = stackArgs; + } else { + heapArgs.resize(argc); + argv = heapArgs.data(); + } + + for (size_t index = 0; index < argc; index++) { + argv[index] = static_cast(args[index]); + } + + return Call(recv, argc, argv); +} + +inline MaybeOrValue Function::Call(napi_value recv, + size_t argc, + const napi_value* args) const { + napi_value result; + napi_status status = + napi_call_function(_env, recv, _value, argc, args, &result); + NAPI_RETURN_OR_THROW_IF_FAILED( + _env, status, Napi::Value(_env, result), Napi::Value); +} + +inline MaybeOrValue Function::MakeCallback( + napi_value recv, + const std::initializer_list& args, + napi_async_context context) const { + return MakeCallback(recv, args.size(), args.begin(), context); +} + +inline MaybeOrValue Function::MakeCallback( + napi_value recv, + const std::vector& args, + napi_async_context context) const { + return MakeCallback(recv, args.size(), args.data(), context); +} + +inline MaybeOrValue Function::MakeCallback( + napi_value recv, + size_t argc, + const napi_value* args, + napi_async_context context) const { + napi_value result; + napi_status status = + napi_make_callback(_env, context, recv, _value, argc, args, &result); + NAPI_RETURN_OR_THROW_IF_FAILED( + _env, status, Napi::Value(_env, result), Napi::Value); +} + +inline MaybeOrValue Function::New( + const std::initializer_list& args) const { + return New(args.size(), args.begin()); +} + +inline MaybeOrValue Function::New( + const std::vector& args) const { + return New(args.size(), args.data()); +} + +inline MaybeOrValue Function::New(size_t argc, + const napi_value* args) const { + napi_value result; + napi_status status = napi_new_instance(_env, _value, argc, args, &result); + NAPI_RETURN_OR_THROW_IF_FAILED( + _env, status, Napi::Object(_env, result), Napi::Object); +} + +//////////////////////////////////////////////////////////////////////////////// +// Promise class +//////////////////////////////////////////////////////////////////////////////// + +inline Promise::Deferred Promise::Deferred::New(napi_env env) { + return Promise::Deferred(env); +} + +inline Promise::Deferred::Deferred(napi_env env) : _env(env) { + napi_status status = napi_create_promise(_env, &_deferred, &_promise); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} + +inline Promise Promise::Deferred::Promise() const { + return Napi::Promise(_env, _promise); +} + +inline Napi::Env Promise::Deferred::Env() const { + return Napi::Env(_env); +} + +inline void Promise::Deferred::Resolve(napi_value value) const { + napi_status status = napi_resolve_deferred(_env, _deferred, value); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} + +inline void Promise::Deferred::Reject(napi_value value) const { + napi_status status = napi_reject_deferred(_env, _deferred, value); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} + +inline void Promise::CheckCast(napi_env env, napi_value value) { + NAPI_CHECK(value != nullptr, "Promise::CheckCast", "empty value"); + + bool result; + napi_status status = napi_is_promise(env, value, &result); + NAPI_CHECK(status == napi_ok, "Promise::CheckCast", "napi_is_promise failed"); + NAPI_CHECK(result, "Promise::CheckCast", "value is not promise"); +} + +inline Promise::Promise() : Object() {} + +inline Promise::Promise(napi_env env, napi_value value) : Object(env, value) {} + +inline MaybeOrValue Promise::Then(napi_value onFulfilled) const { + EscapableHandleScope scope(_env); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + Value thenMethod; + if (!Get("then").UnwrapTo(&thenMethod)) { + return Nothing(); + } + MaybeOrValue result = + thenMethod.As().Call(*this, {onFulfilled}); + if (result.IsJust()) { + return Just(scope.Escape(result.Unwrap()).As()); + } + return Nothing(); +#else + Function thenMethod = Get("then").As(); + MaybeOrValue result = thenMethod.Call(*this, {onFulfilled}); + if (scope.Env().IsExceptionPending()) { + return Promise(); + } + return scope.Escape(result).As(); +#endif +} + +inline MaybeOrValue Promise::Then(napi_value onFulfilled, + napi_value onRejected) const { + EscapableHandleScope scope(_env); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + Value thenMethod; + if (!Get("then").UnwrapTo(&thenMethod)) { + return Nothing(); + } + MaybeOrValue result = + thenMethod.As().Call(*this, {onFulfilled, onRejected}); + if (result.IsJust()) { + return Just(scope.Escape(result.Unwrap()).As()); + } + return Nothing(); +#else + Function thenMethod = Get("then").As(); + MaybeOrValue result = + thenMethod.Call(*this, {onFulfilled, onRejected}); + if (scope.Env().IsExceptionPending()) { + return Promise(); + } + return scope.Escape(result).As(); +#endif +} + +inline MaybeOrValue Promise::Catch(napi_value onRejected) const { + EscapableHandleScope scope(_env); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + Value catchMethod; + if (!Get("catch").UnwrapTo(&catchMethod)) { + return Nothing(); + } + MaybeOrValue result = + catchMethod.As().Call(*this, {onRejected}); + if (result.IsJust()) { + return Just(scope.Escape(result.Unwrap()).As()); + } + return Nothing(); +#else + Function catchMethod = Get("catch").As(); + MaybeOrValue result = catchMethod.Call(*this, {onRejected}); + if (scope.Env().IsExceptionPending()) { + return Promise(); + } + return scope.Escape(result).As(); +#endif +} + +inline MaybeOrValue Promise::Then(const Function& onFulfilled) const { + return Then(static_cast(onFulfilled)); +} + +inline MaybeOrValue Promise::Then(const Function& onFulfilled, + const Function& onRejected) const { + return Then(static_cast(onFulfilled), + static_cast(onRejected)); +} + +inline MaybeOrValue Promise::Catch(const Function& onRejected) const { + return Catch(static_cast(onRejected)); +} + +//////////////////////////////////////////////////////////////////////////////// +// Buffer class +//////////////////////////////////////////////////////////////////////////////// + +template +inline Buffer Buffer::New(napi_env env, size_t length) { + napi_value value; + void* data; + napi_status status = + napi_create_buffer(env, length * sizeof(T), &data, &value); + NAPI_THROW_IF_FAILED(env, status, Buffer()); + return Buffer(env, value); +} + +#ifndef NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED +template +inline Buffer Buffer::New(napi_env env, T* data, size_t length) { + napi_value value; + napi_status status = napi_create_external_buffer( + env, length * sizeof(T), data, nullptr, nullptr, &value); + NAPI_THROW_IF_FAILED(env, status, Buffer()); + return Buffer(env, value); +} + +template +template +inline Buffer Buffer::New(napi_env env, + T* data, + size_t length, + Finalizer finalizeCallback) { + napi_value value; + details::FinalizeData* finalizeData = + new details::FinalizeData( + {std::move(finalizeCallback), nullptr}); + napi_status status = + napi_create_external_buffer(env, + length * sizeof(T), + data, + details::FinalizeData::Wrapper, + finalizeData, + &value); + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED(env, status, Buffer()); + } + return Buffer(env, value); +} + +template +template +inline Buffer Buffer::New(napi_env env, + T* data, + size_t length, + Finalizer finalizeCallback, + Hint* finalizeHint) { + napi_value value; + details::FinalizeData* finalizeData = + new details::FinalizeData( + {std::move(finalizeCallback), finalizeHint}); + napi_status status = napi_create_external_buffer( + env, + length * sizeof(T), + data, + details::FinalizeData::WrapperWithHint, + finalizeData, + &value); + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED(env, status, Buffer()); + } + return Buffer(env, value); +} +#endif // NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED + +template +inline Buffer Buffer::NewOrCopy(napi_env env, T* data, size_t length) { +#ifndef NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED + napi_value value; + napi_status status = napi_create_external_buffer( + env, length * sizeof(T), data, nullptr, nullptr, &value); + if (status == details::napi_no_external_buffers_allowed) { +#endif // NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED + // If we can't create an external buffer, we'll just copy the data. + return Buffer::Copy(env, data, length); +#ifndef NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED + } + NAPI_THROW_IF_FAILED(env, status, Buffer()); + return Buffer(env, value); +#endif // NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED +} + +template +template +inline Buffer Buffer::NewOrCopy(napi_env env, + T* data, + size_t length, + Finalizer finalizeCallback) { + details::FinalizeData* finalizeData = + new details::FinalizeData( + {std::move(finalizeCallback), nullptr}); +#ifndef NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED + napi_value value; + napi_status status = + napi_create_external_buffer(env, + length * sizeof(T), + data, + details::FinalizeData::Wrapper, + finalizeData, + &value); + if (status == details::napi_no_external_buffers_allowed) { +#endif // NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED + // If we can't create an external buffer, we'll just copy the data. + Buffer ret = Buffer::Copy(env, data, length); + details::FinalizeData::WrapperGC(env, data, finalizeData); + return ret; +#ifndef NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED + } + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED(env, status, Buffer()); + } + return Buffer(env, value); +#endif // NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED +} + +template +template +inline Buffer Buffer::NewOrCopy(napi_env env, + T* data, + size_t length, + Finalizer finalizeCallback, + Hint* finalizeHint) { + details::FinalizeData* finalizeData = + new details::FinalizeData( + {std::move(finalizeCallback), finalizeHint}); +#ifndef NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED + napi_value value; + napi_status status = napi_create_external_buffer( + env, + length * sizeof(T), + data, + details::FinalizeData::WrapperWithHint, + finalizeData, + &value); + if (status == details::napi_no_external_buffers_allowed) { +#endif + // If we can't create an external buffer, we'll just copy the data. + Buffer ret = Buffer::Copy(env, data, length); + details::FinalizeData::WrapperGCWithHint( + env, data, finalizeData); + return ret; +#ifndef NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED + } + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED(env, status, Buffer()); + } + return Buffer(env, value); +#endif +} + +template +inline Buffer Buffer::Copy(napi_env env, const T* data, size_t length) { + napi_value value; + napi_status status = + napi_create_buffer_copy(env, length * sizeof(T), data, nullptr, &value); + NAPI_THROW_IF_FAILED(env, status, Buffer()); + return Buffer(env, value); +} + +template +inline void Buffer::CheckCast(napi_env env, napi_value value) { + NAPI_CHECK(value != nullptr, "Buffer::CheckCast", "empty value"); + + bool result; + napi_status status = napi_is_buffer(env, value, &result); + NAPI_CHECK(status == napi_ok, "Buffer::CheckCast", "napi_is_buffer failed"); + NAPI_CHECK(result, "Buffer::CheckCast", "value is not buffer"); +} + +template +inline Buffer::Buffer() : Uint8Array() {} + +template +inline Buffer::Buffer(napi_env env, napi_value value) + : Uint8Array(env, value) {} + +template +inline size_t Buffer::Length() const { + return ByteLength() / sizeof(T); +} + +template +inline T* Buffer::Data() const { + return reinterpret_cast(const_cast(Uint8Array::Data())); +} + +//////////////////////////////////////////////////////////////////////////////// +// Error class +//////////////////////////////////////////////////////////////////////////////// + +inline Error Error::New(napi_env env) { + napi_status status; + napi_value error = nullptr; + bool is_exception_pending; + napi_extended_error_info last_error_info_copy; + + { + // We must retrieve the last error info before doing anything else because + // doing anything else will replace the last error info. + const napi_extended_error_info* last_error_info; + status = napi_get_last_error_info(env, &last_error_info); + NAPI_FATAL_IF_FAILED(status, "Error::New", "napi_get_last_error_info"); + + // All fields of the `napi_extended_error_info` structure gets reset in + // subsequent Node-API function calls on the same `env`. This includes a + // call to `napi_is_exception_pending()`. So here it is necessary to make a + // copy of the information as the `error_code` field is used later on. + memcpy(&last_error_info_copy, + last_error_info, + sizeof(napi_extended_error_info)); + } + + status = napi_is_exception_pending(env, &is_exception_pending); + NAPI_FATAL_IF_FAILED(status, "Error::New", "napi_is_exception_pending"); + + // A pending exception takes precedence over any internal error status. + if (is_exception_pending) { + status = napi_get_and_clear_last_exception(env, &error); + NAPI_FATAL_IF_FAILED( + status, "Error::New", "napi_get_and_clear_last_exception"); + } else { + const char* error_message = last_error_info_copy.error_message != nullptr + ? last_error_info_copy.error_message + : "Error in native callback"; + + napi_value message; + status = napi_create_string_utf8( + env, error_message, std::strlen(error_message), &message); + NAPI_FATAL_IF_FAILED(status, "Error::New", "napi_create_string_utf8"); + + switch (last_error_info_copy.error_code) { + case napi_object_expected: + case napi_string_expected: + case napi_boolean_expected: + case napi_number_expected: + status = napi_create_type_error(env, nullptr, message, &error); + break; + default: + status = napi_create_error(env, nullptr, message, &error); + break; + } + NAPI_FATAL_IF_FAILED(status, "Error::New", "napi_create_error"); + } + + return Error(env, error); +} + +inline Error Error::New(napi_env env, const char* message) { + return Error::New( + env, message, std::strlen(message), napi_create_error); +} + +inline Error Error::New(napi_env env, const std::string& message) { + return Error::New( + env, message.c_str(), message.size(), napi_create_error); +} + +inline NAPI_NO_RETURN void Error::Fatal(const char* location, + const char* message) { + napi_fatal_error(location, NAPI_AUTO_LENGTH, message, NAPI_AUTO_LENGTH); +} + +inline Error::Error() : ObjectReference() {} + +inline Error::Error(napi_env env, napi_value value) + : ObjectReference(env, nullptr) { + if (value != nullptr) { + // Attempting to create a reference on the error object. + // If it's not a Object/Function/Symbol, this call will return an error + // status. + napi_status status = napi_create_reference(env, value, 1, &_ref); + + if (status != napi_ok) { + napi_value wrappedErrorObj; + + // Create an error object + status = napi_create_object(env, &wrappedErrorObj); + NAPI_FATAL_IF_FAILED(status, "Error::Error", "napi_create_object"); + + // property flag that we attach to show the error object is wrapped + napi_property_descriptor wrapObjFlag = { + ERROR_WRAP_VALUE(), // Unique GUID identifier since Symbol isn't a + // viable option + nullptr, + nullptr, + nullptr, + nullptr, + Value::From(env, value), + napi_enumerable, + nullptr}; + + status = napi_define_properties(env, wrappedErrorObj, 1, &wrapObjFlag); +#ifdef NODE_API_SWALLOW_UNTHROWABLE_EXCEPTIONS + if (status == napi_pending_exception) { + // Test if the pending exception was reported because the environment is + // shutting down. We assume that a status of napi_pending_exception + // coupled with the absence of an actual pending exception means that + // the environment is shutting down. If so, we replace the + // napi_pending_exception status with napi_ok. + bool is_exception_pending = false; + status = napi_is_exception_pending(env, &is_exception_pending); + if (status == napi_ok && !is_exception_pending) { + status = napi_ok; + } else { + status = napi_pending_exception; + } + } +#endif // NODE_API_SWALLOW_UNTHROWABLE_EXCEPTIONS + NAPI_FATAL_IF_FAILED(status, "Error::Error", "napi_define_properties"); + + // Create a reference on the newly wrapped object + status = napi_create_reference(env, wrappedErrorObj, 1, &_ref); + } + + // Avoid infinite recursion in the failure case. + NAPI_FATAL_IF_FAILED(status, "Error::Error", "napi_create_reference"); + } +} + +inline Object Error::Value() const { + if (_ref == nullptr) { + return Object(_env, nullptr); + } + + napi_value refValue; + napi_status status = napi_get_reference_value(_env, _ref, &refValue); + NAPI_THROW_IF_FAILED(_env, status, Object()); + + napi_valuetype type; + status = napi_typeof(_env, refValue, &type); + NAPI_THROW_IF_FAILED(_env, status, Object()); + + // If refValue isn't a symbol, then we proceed to whether the refValue has the + // wrapped error flag + if (type != napi_symbol) { + // We are checking if the object is wrapped + bool isWrappedObject = false; + + status = napi_has_property(_env, + refValue, + String::From(_env, ERROR_WRAP_VALUE()), + &isWrappedObject); + + // Don't care about status + if (isWrappedObject) { + napi_value unwrappedValue; + status = napi_get_property(_env, + refValue, + String::From(_env, ERROR_WRAP_VALUE()), + &unwrappedValue); + NAPI_THROW_IF_FAILED(_env, status, Object()); + + return Object(_env, unwrappedValue); + } + } + + return Object(_env, refValue); +} + +inline Error::Error(Error&& other) : ObjectReference(std::move(other)) {} + +inline Error& Error::operator=(Error&& other) { + static_cast*>(this)->operator=(std::move(other)); + return *this; +} + +inline Error::Error(const Error& other) : ObjectReference(other) {} + +inline Error& Error::operator=(const Error& other) { + Reset(); + + _env = other.Env(); + HandleScope scope(_env); + + napi_value value = other.Value(); + if (value != nullptr) { + napi_status status = napi_create_reference(_env, value, 1, &_ref); + NAPI_THROW_IF_FAILED(_env, status, *this); + } + + return *this; +} + +inline const std::string& Error::Message() const NAPI_NOEXCEPT { + if (_message.size() == 0 && _env != nullptr) { +#ifdef NODE_ADDON_API_CPP_EXCEPTIONS + try { + _message = Get("message").As(); + } catch (...) { + // Catch all errors here, to include e.g. a std::bad_alloc from + // the std::string::operator=, because this method may not throw. + } +#else // NODE_ADDON_API_CPP_EXCEPTIONS +#if defined(NODE_ADDON_API_ENABLE_MAYBE) + Napi::Value message_val; + if (Get("message").UnwrapTo(&message_val)) { + _message = message_val.As(); + } +#else + _message = Get("message").As(); +#endif +#endif // NODE_ADDON_API_CPP_EXCEPTIONS + } + return _message; +} + +// we created an object on the &_ref +inline void Error::ThrowAsJavaScriptException() const { + HandleScope scope(_env); + if (!IsEmpty()) { +#ifdef NODE_API_SWALLOW_UNTHROWABLE_EXCEPTIONS + bool pendingException = false; + + // check if there is already a pending exception. If so don't try to throw a + // new one as that is not allowed/possible + napi_status status = napi_is_exception_pending(_env, &pendingException); + + if ((status != napi_ok) || + ((status == napi_ok) && (pendingException == false))) { + // We intentionally don't use `NAPI_THROW_*` macros here to ensure + // that there is no possible recursion as `ThrowAsJavaScriptException` + // is part of `NAPI_THROW_*` macro definition for noexcept. + + status = napi_throw(_env, Value()); + +#if (NAPI_VERSION >= 10) + napi_status expected_failure_mode = napi_cannot_run_js; +#else + napi_status expected_failure_mode = napi_pending_exception; +#endif + if (status == expected_failure_mode) { + // The environment must be terminating as we checked earlier and there + // was no pending exception. In this case continuing will result + // in a fatal error and there is nothing the author has done incorrectly + // in their code that is worth flagging through a fatal error + return; + } + } else { + status = napi_pending_exception; + } +#else + // We intentionally don't use `NAPI_THROW_*` macros here to ensure + // that there is no possible recursion as `ThrowAsJavaScriptException` + // is part of `NAPI_THROW_*` macro definition for noexcept. + + napi_status status = napi_throw(_env, Value()); +#endif + +#ifdef NODE_ADDON_API_CPP_EXCEPTIONS + if (status != napi_ok) { + throw Error::New(_env); + } +#else // NODE_ADDON_API_CPP_EXCEPTIONS + NAPI_FATAL_IF_FAILED( + status, "Error::ThrowAsJavaScriptException", "napi_throw"); +#endif // NODE_ADDON_API_CPP_EXCEPTIONS + } +} + +#ifdef NODE_ADDON_API_CPP_EXCEPTIONS + +inline const char* Error::what() const NAPI_NOEXCEPT { + return Message().c_str(); +} + +#endif // NODE_ADDON_API_CPP_EXCEPTIONS + +inline const char* Error::ERROR_WRAP_VALUE() NAPI_NOEXCEPT { + return "4bda9e7e-4913-4dbc-95de-891cbf66598e-errorVal"; +} + +template +inline TError Error::New(napi_env env, + const char* message, + size_t length, + create_error_fn create_error) { + napi_value str; + napi_status status = napi_create_string_utf8(env, message, length, &str); + NAPI_THROW_IF_FAILED(env, status, TError()); + + napi_value error; + status = create_error(env, nullptr, str, &error); + NAPI_THROW_IF_FAILED(env, status, TError()); + + return TError(env, error); +} + +inline TypeError TypeError::New(napi_env env, const char* message) { + return Error::New( + env, message, std::strlen(message), napi_create_type_error); +} + +inline TypeError TypeError::New(napi_env env, const std::string& message) { + return Error::New( + env, message.c_str(), message.size(), napi_create_type_error); +} + +inline TypeError::TypeError() : Error() {} + +inline TypeError::TypeError(napi_env env, napi_value value) + : Error(env, value) {} + +inline RangeError RangeError::New(napi_env env, const char* message) { + return Error::New( + env, message, std::strlen(message), napi_create_range_error); +} + +inline RangeError RangeError::New(napi_env env, const std::string& message) { + return Error::New( + env, message.c_str(), message.size(), napi_create_range_error); +} + +inline RangeError::RangeError() : Error() {} + +inline RangeError::RangeError(napi_env env, napi_value value) + : Error(env, value) {} + +#if NAPI_VERSION > 8 +inline SyntaxError SyntaxError::New(napi_env env, const char* message) { + return Error::New( + env, message, std::strlen(message), node_api_create_syntax_error); +} + +inline SyntaxError SyntaxError::New(napi_env env, const std::string& message) { + return Error::New( + env, message.c_str(), message.size(), node_api_create_syntax_error); +} + +inline SyntaxError::SyntaxError() : Error() {} + +inline SyntaxError::SyntaxError(napi_env env, napi_value value) + : Error(env, value) {} +#endif // NAPI_VERSION > 8 + +//////////////////////////////////////////////////////////////////////////////// +// Reference class +//////////////////////////////////////////////////////////////////////////////// + +template +inline Reference Reference::New(const T& value, + uint32_t initialRefcount) { + napi_env env = value.Env(); + napi_value val = value; + + if (val == nullptr) { + return Reference(env, nullptr); + } + + napi_ref ref; + napi_status status = napi_create_reference(env, value, initialRefcount, &ref); + NAPI_THROW_IF_FAILED(env, status, Reference()); + + return Reference(env, ref); +} + +template +inline Reference::Reference() + : _env(nullptr), _ref(nullptr), _suppressDestruct(false) {} + +template +inline Reference::Reference(napi_env env, napi_ref ref) + : _env(env), _ref(ref), _suppressDestruct(false) {} + +template +inline Reference::~Reference() { + if (_ref != nullptr) { + if (!_suppressDestruct) { + // TODO(legendecas): napi_delete_reference should be invoked immediately. + // Fix this when https://github.com/nodejs/node/pull/55620 lands. +#ifdef NODE_API_EXPERIMENTAL_HAS_POST_FINALIZER + Env().PostFinalizer( + [](Napi::Env env, napi_ref ref) { napi_delete_reference(env, ref); }, + _ref); +#else + napi_delete_reference(_env, _ref); +#endif + } + + _ref = nullptr; + } +} + +template +inline Reference::Reference(Reference&& other) + : _env(other._env), + _ref(other._ref), + _suppressDestruct(other._suppressDestruct) { + other._env = nullptr; + other._ref = nullptr; + other._suppressDestruct = false; +} + +template +inline Reference& Reference::operator=(Reference&& other) { + Reset(); + _env = other._env; + _ref = other._ref; + _suppressDestruct = other._suppressDestruct; + other._env = nullptr; + other._ref = nullptr; + other._suppressDestruct = false; + return *this; +} + +template +inline Reference::Reference(const Reference& other) + : _env(other._env), _ref(nullptr), _suppressDestruct(false) { + HandleScope scope(_env); + + napi_value value = other.Value(); + if (value != nullptr) { + // Copying is a limited scenario (currently only used for Error object) and + // always creates a strong reference to the given value even if the incoming + // reference is weak. + napi_status status = napi_create_reference(_env, value, 1, &_ref); + NAPI_FATAL_IF_FAILED( + status, "Reference::Reference", "napi_create_reference"); + } +} + +template +inline Reference::operator napi_ref() const { + return _ref; +} + +template +inline bool Reference::operator==(const Reference& other) const { + HandleScope scope(_env); + return this->Value().StrictEquals(other.Value()); +} + +template +inline bool Reference::operator!=(const Reference& other) const { + return !this->operator==(other); +} + +template +inline Napi::Env Reference::Env() const { + return Napi::Env(_env); +} + +template +inline bool Reference::IsEmpty() const { + return _ref == nullptr; +} + +template +inline T Reference::Value() const { + if (_ref == nullptr) { + return T(_env, nullptr); + } + + napi_value value; + napi_status status = napi_get_reference_value(_env, _ref, &value); + NAPI_THROW_IF_FAILED(_env, status, T()); + return T(_env, value); +} + +template +inline uint32_t Reference::Ref() const { + uint32_t result; + napi_status status = napi_reference_ref(_env, _ref, &result); + NAPI_THROW_IF_FAILED(_env, status, 0); + return result; +} + +template +inline uint32_t Reference::Unref() const { + uint32_t result; + napi_status status = napi_reference_unref(_env, _ref, &result); + NAPI_THROW_IF_FAILED(_env, status, 0); + return result; +} + +template +inline void Reference::Reset() { + if (_ref != nullptr) { + napi_status status = napi_delete_reference(_env, _ref); + NAPI_THROW_IF_FAILED_VOID(_env, status); + _ref = nullptr; + } +} + +template +inline void Reference::Reset(const T& value, uint32_t refcount) { + Reset(); + _env = value.Env(); + + napi_value val = value; + if (val != nullptr) { + napi_status status = napi_create_reference(_env, value, refcount, &_ref); + NAPI_THROW_IF_FAILED_VOID(_env, status); + } +} + +template +inline void Reference::SuppressDestruct() { + _suppressDestruct = true; +} + +template +inline Reference Weak(T value) { + return Reference::New(value, 0); +} + +inline ObjectReference Weak(Object value) { + return Reference::New(value, 0); +} + +inline FunctionReference Weak(Function value) { + return Reference::New(value, 0); +} + +template +inline Reference Persistent(T value) { + return Reference::New(value, 1); +} + +inline ObjectReference Persistent(Object value) { + return Reference::New(value, 1); +} + +inline FunctionReference Persistent(Function value) { + return Reference::New(value, 1); +} + +//////////////////////////////////////////////////////////////////////////////// +// ObjectReference class +//////////////////////////////////////////////////////////////////////////////// + +inline ObjectReference::ObjectReference() : Reference() {} + +inline ObjectReference::ObjectReference(napi_env env, napi_ref ref) + : Reference(env, ref) {} + +inline ObjectReference::ObjectReference(Reference&& other) + : Reference(std::move(other)) {} + +inline ObjectReference& ObjectReference::operator=(Reference&& other) { + static_cast*>(this)->operator=(std::move(other)); + return *this; +} + +inline ObjectReference::ObjectReference(ObjectReference&& other) + : Reference(std::move(other)) {} + +inline ObjectReference& ObjectReference::operator=(ObjectReference&& other) { + static_cast*>(this)->operator=(std::move(other)); + return *this; +} + +inline ObjectReference::ObjectReference(const ObjectReference& other) + : Reference(other) {} + +inline MaybeOrValue ObjectReference::Get( + const char* utf8name) const { + EscapableHandleScope scope(_env); + MaybeOrValue result = Value().Get(utf8name); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + if (result.IsJust()) { + return Just(scope.Escape(result.Unwrap())); + } + return result; +#else + if (scope.Env().IsExceptionPending()) { + return Value(); + } + return scope.Escape(result); +#endif +} + +inline MaybeOrValue ObjectReference::Get( + const std::string& utf8name) const { + EscapableHandleScope scope(_env); + MaybeOrValue result = Value().Get(utf8name); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + if (result.IsJust()) { + return Just(scope.Escape(result.Unwrap())); + } + return result; +#else + if (scope.Env().IsExceptionPending()) { + return Value(); + } + return scope.Escape(result); +#endif +} + +inline MaybeOrValue ObjectReference::Set(const char* utf8name, + napi_value value) const { + HandleScope scope(_env); + return Value().Set(utf8name, value); +} + +inline MaybeOrValue ObjectReference::Set(const char* utf8name, + Napi::Value value) const { + HandleScope scope(_env); + return Value().Set(utf8name, value); +} + +inline MaybeOrValue ObjectReference::Set(const char* utf8name, + const char* utf8value) const { + HandleScope scope(_env); + return Value().Set(utf8name, utf8value); +} + +inline MaybeOrValue ObjectReference::Set(const char* utf8name, + bool boolValue) const { + HandleScope scope(_env); + return Value().Set(utf8name, boolValue); +} + +inline MaybeOrValue ObjectReference::Set(const char* utf8name, + double numberValue) const { + HandleScope scope(_env); + return Value().Set(utf8name, numberValue); +} + +inline MaybeOrValue ObjectReference::Set(const std::string& utf8name, + napi_value value) const { + HandleScope scope(_env); + return Value().Set(utf8name, value); +} + +inline MaybeOrValue ObjectReference::Set(const std::string& utf8name, + Napi::Value value) const { + HandleScope scope(_env); + return Value().Set(utf8name, value); +} + +inline MaybeOrValue ObjectReference::Set(const std::string& utf8name, + std::string& utf8value) const { + HandleScope scope(_env); + return Value().Set(utf8name, utf8value); +} + +inline MaybeOrValue ObjectReference::Set(const std::string& utf8name, + bool boolValue) const { + HandleScope scope(_env); + return Value().Set(utf8name, boolValue); +} + +inline MaybeOrValue ObjectReference::Set(const std::string& utf8name, + double numberValue) const { + HandleScope scope(_env); + return Value().Set(utf8name, numberValue); +} + +inline MaybeOrValue ObjectReference::Get(uint32_t index) const { + EscapableHandleScope scope(_env); + MaybeOrValue result = Value().Get(index); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + if (result.IsJust()) { + return Just(scope.Escape(result.Unwrap())); + } + return result; +#else + if (scope.Env().IsExceptionPending()) { + return Value(); + } + return scope.Escape(result); +#endif +} + +inline MaybeOrValue ObjectReference::Set(uint32_t index, + napi_value value) const { + HandleScope scope(_env); + return Value().Set(index, value); +} + +inline MaybeOrValue ObjectReference::Set(uint32_t index, + Napi::Value value) const { + HandleScope scope(_env); + return Value().Set(index, value); +} + +inline MaybeOrValue ObjectReference::Set(uint32_t index, + const char* utf8value) const { + HandleScope scope(_env); + return Value().Set(index, utf8value); +} + +inline MaybeOrValue ObjectReference::Set( + uint32_t index, const std::string& utf8value) const { + HandleScope scope(_env); + return Value().Set(index, utf8value); +} + +inline MaybeOrValue ObjectReference::Set(uint32_t index, + bool boolValue) const { + HandleScope scope(_env); + return Value().Set(index, boolValue); +} + +inline MaybeOrValue ObjectReference::Set(uint32_t index, + double numberValue) const { + HandleScope scope(_env); + return Value().Set(index, numberValue); +} + +//////////////////////////////////////////////////////////////////////////////// +// FunctionReference class +//////////////////////////////////////////////////////////////////////////////// + +inline FunctionReference::FunctionReference() : Reference() {} + +inline FunctionReference::FunctionReference(napi_env env, napi_ref ref) + : Reference(env, ref) {} + +inline FunctionReference::FunctionReference(Reference&& other) + : Reference(std::move(other)) {} + +inline FunctionReference& FunctionReference::operator=( + Reference&& other) { + static_cast*>(this)->operator=(std::move(other)); + return *this; +} + +inline FunctionReference::FunctionReference(FunctionReference&& other) + : Reference(std::move(other)) {} + +inline FunctionReference& FunctionReference::operator=( + FunctionReference&& other) { + static_cast*>(this)->operator=(std::move(other)); + return *this; +} + +inline MaybeOrValue FunctionReference::operator()( + const std::initializer_list& args) const { + EscapableHandleScope scope(_env); + MaybeOrValue result = Value()(args); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + if (result.IsJust()) { + return Just(scope.Escape(result.Unwrap())); + } + return result; +#else + if (scope.Env().IsExceptionPending()) { + return Value(); + } + return scope.Escape(result); +#endif +} + +inline MaybeOrValue FunctionReference::Call( + const std::initializer_list& args) const { + EscapableHandleScope scope(_env); + MaybeOrValue result = Value().Call(args); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + if (result.IsJust()) { + return Just(scope.Escape(result.Unwrap())); + } + return result; +#else + if (scope.Env().IsExceptionPending()) { + return Value(); + } + return scope.Escape(result); +#endif +} + +inline MaybeOrValue FunctionReference::Call( + const std::vector& args) const { + EscapableHandleScope scope(_env); + MaybeOrValue result = Value().Call(args); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + if (result.IsJust()) { + return Just(scope.Escape(result.Unwrap())); + } + return result; +#else + if (scope.Env().IsExceptionPending()) { + return Value(); + } + return scope.Escape(result); +#endif +} + +inline MaybeOrValue FunctionReference::Call( + napi_value recv, const std::initializer_list& args) const { + EscapableHandleScope scope(_env); + MaybeOrValue result = Value().Call(recv, args); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + if (result.IsJust()) { + return Just(scope.Escape(result.Unwrap())); + } + return result; +#else + if (scope.Env().IsExceptionPending()) { + return Value(); + } + return scope.Escape(result); +#endif +} + +inline MaybeOrValue FunctionReference::Call( + napi_value recv, const std::vector& args) const { + EscapableHandleScope scope(_env); + MaybeOrValue result = Value().Call(recv, args); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + if (result.IsJust()) { + return Just(scope.Escape(result.Unwrap())); + } + return result; +#else + if (scope.Env().IsExceptionPending()) { + return Value(); + } + return scope.Escape(result); +#endif +} + +inline MaybeOrValue FunctionReference::Call( + napi_value recv, size_t argc, const napi_value* args) const { + EscapableHandleScope scope(_env); + MaybeOrValue result = Value().Call(recv, argc, args); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + if (result.IsJust()) { + return Just(scope.Escape(result.Unwrap())); + } + return result; +#else + if (scope.Env().IsExceptionPending()) { + return Value(); + } + return scope.Escape(result); +#endif +} + +inline MaybeOrValue FunctionReference::MakeCallback( + napi_value recv, + const std::initializer_list& args, + napi_async_context context) const { + EscapableHandleScope scope(_env); + MaybeOrValue result = Value().MakeCallback(recv, args, context); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + if (result.IsJust()) { + return Just(scope.Escape(result.Unwrap())); + } + + return result; +#else + if (scope.Env().IsExceptionPending()) { + return Value(); + } + return scope.Escape(result); +#endif +} + +inline MaybeOrValue FunctionReference::MakeCallback( + napi_value recv, + const std::vector& args, + napi_async_context context) const { + EscapableHandleScope scope(_env); + MaybeOrValue result = Value().MakeCallback(recv, args, context); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + if (result.IsJust()) { + return Just(scope.Escape(result.Unwrap())); + } + return result; +#else + if (scope.Env().IsExceptionPending()) { + return Value(); + } + return scope.Escape(result); +#endif +} + +inline MaybeOrValue FunctionReference::MakeCallback( + napi_value recv, + size_t argc, + const napi_value* args, + napi_async_context context) const { + EscapableHandleScope scope(_env); + MaybeOrValue result = + Value().MakeCallback(recv, argc, args, context); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + if (result.IsJust()) { + return Just(scope.Escape(result.Unwrap())); + } + return result; +#else + if (scope.Env().IsExceptionPending()) { + return Value(); + } + return scope.Escape(result); +#endif +} + +inline MaybeOrValue FunctionReference::New( + const std::initializer_list& args) const { + EscapableHandleScope scope(_env); + MaybeOrValue result = Value().New(args); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + if (result.IsJust()) { + return Just(scope.Escape(result.Unwrap()).As()); + } + return result; +#else + if (scope.Env().IsExceptionPending()) { + return Object(); + } + return scope.Escape(result).As(); +#endif +} + +inline MaybeOrValue FunctionReference::New( + const std::vector& args) const { + EscapableHandleScope scope(_env); + MaybeOrValue result = Value().New(args); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + if (result.IsJust()) { + return Just(scope.Escape(result.Unwrap()).As()); + } + return result; +#else + if (scope.Env().IsExceptionPending()) { + return Object(); + } + return scope.Escape(result).As(); +#endif +} + +//////////////////////////////////////////////////////////////////////////////// +// CallbackInfo class +//////////////////////////////////////////////////////////////////////////////// + +inline CallbackInfo::CallbackInfo(napi_env env, napi_callback_info info) + : _env(env), + _info(info), + _this(nullptr), + _dynamicArgs(nullptr), + _data(nullptr) { + _argc = _staticArgCount; + _argv = _staticArgs; + napi_status status = + napi_get_cb_info(env, info, &_argc, _argv, &_this, &_data); + NAPI_THROW_IF_FAILED_VOID(_env, status); + + if (_argc > _staticArgCount) { + // Use either a fixed-size array (on the stack) or a dynamically-allocated + // array (on the heap) depending on the number of args. + _dynamicArgs = new napi_value[_argc]; + _argv = _dynamicArgs; + + status = napi_get_cb_info(env, info, &_argc, _argv, nullptr, nullptr); + NAPI_THROW_IF_FAILED_VOID(_env, status); + } +} + +inline CallbackInfo::~CallbackInfo() { + if (_dynamicArgs != nullptr) { + delete[] _dynamicArgs; + } +} + +inline CallbackInfo::operator napi_callback_info() const { + return _info; +} + +inline Value CallbackInfo::NewTarget() const { + napi_value newTarget; + napi_status status = napi_get_new_target(_env, _info, &newTarget); + NAPI_THROW_IF_FAILED(_env, status, Value()); + return Value(_env, newTarget); +} + +inline bool CallbackInfo::IsConstructCall() const { + return !NewTarget().IsEmpty(); +} + +inline Napi::Env CallbackInfo::Env() const { + return Napi::Env(_env); +} + +inline size_t CallbackInfo::Length() const { + return _argc; +} + +inline const Value CallbackInfo::operator[](size_t index) const { + return index < _argc ? Value(_env, _argv[index]) : Env().Undefined(); +} + +inline Value CallbackInfo::This() const { + if (_this == nullptr) { + return Env().Undefined(); + } + return Object(_env, _this); +} + +inline void* CallbackInfo::Data() const { + return _data; +} + +inline void CallbackInfo::SetData(void* data) { + _data = data; +} + +//////////////////////////////////////////////////////////////////////////////// +// PropertyDescriptor class +//////////////////////////////////////////////////////////////////////////////// + +template +PropertyDescriptor PropertyDescriptor::Accessor( + const char* utf8name, napi_property_attributes attributes, void* data) { + napi_property_descriptor desc = napi_property_descriptor(); + + desc.utf8name = utf8name; + desc.getter = details::TemplatedCallback; + desc.attributes = attributes; + desc.data = data; + + return desc; +} + +template +PropertyDescriptor PropertyDescriptor::Accessor( + const std::string& utf8name, + napi_property_attributes attributes, + void* data) { + return Accessor(utf8name.c_str(), attributes, data); +} + +template +PropertyDescriptor PropertyDescriptor::Accessor( + Name name, napi_property_attributes attributes, void* data) { + napi_property_descriptor desc = napi_property_descriptor(); + + desc.name = name; + desc.getter = details::TemplatedCallback; + desc.attributes = attributes; + desc.data = data; + + return desc; +} + +template +PropertyDescriptor PropertyDescriptor::Accessor( + const char* utf8name, napi_property_attributes attributes, void* data) { + napi_property_descriptor desc = napi_property_descriptor(); + + desc.utf8name = utf8name; + desc.getter = details::TemplatedCallback; + desc.setter = details::TemplatedVoidCallback; + desc.attributes = attributes; + desc.data = data; + + return desc; +} + +template +PropertyDescriptor PropertyDescriptor::Accessor( + const std::string& utf8name, + napi_property_attributes attributes, + void* data) { + return Accessor(utf8name.c_str(), attributes, data); +} + +template +PropertyDescriptor PropertyDescriptor::Accessor( + Name name, napi_property_attributes attributes, void* data) { + napi_property_descriptor desc = napi_property_descriptor(); + + desc.name = name; + desc.getter = details::TemplatedCallback; + desc.setter = details::TemplatedVoidCallback; + desc.attributes = attributes; + desc.data = data; + + return desc; +} + +template +inline PropertyDescriptor PropertyDescriptor::Accessor( + Napi::Env env, + Napi::Object object, + const char* utf8name, + Getter getter, + napi_property_attributes attributes, + void* data) { + using CbData = details::CallbackData; + auto callbackData = new CbData({getter, data}); + + napi_status status = AttachData(env, object, callbackData); + if (status != napi_ok) { + delete callbackData; + NAPI_THROW_IF_FAILED(env, status, napi_property_descriptor()); + } + + return PropertyDescriptor({utf8name, + nullptr, + nullptr, + CbData::Wrapper, + nullptr, + nullptr, + attributes, + callbackData}); +} + +template +inline PropertyDescriptor PropertyDescriptor::Accessor( + Napi::Env env, + Napi::Object object, + const std::string& utf8name, + Getter getter, + napi_property_attributes attributes, + void* data) { + return Accessor(env, object, utf8name.c_str(), getter, attributes, data); +} + +template +inline PropertyDescriptor PropertyDescriptor::Accessor( + Napi::Env env, + Napi::Object object, + Name name, + Getter getter, + napi_property_attributes attributes, + void* data) { + using CbData = details::CallbackData; + auto callbackData = new CbData({getter, data}); + + napi_status status = AttachData(env, object, callbackData); + if (status != napi_ok) { + delete callbackData; + NAPI_THROW_IF_FAILED(env, status, napi_property_descriptor()); + } + + return PropertyDescriptor({nullptr, + name, + nullptr, + CbData::Wrapper, + nullptr, + nullptr, + attributes, + callbackData}); +} + +template +inline PropertyDescriptor PropertyDescriptor::Accessor( + Napi::Env env, + Napi::Object object, + const char* utf8name, + Getter getter, + Setter setter, + napi_property_attributes attributes, + void* data) { + using CbData = details::AccessorCallbackData; + auto callbackData = new CbData({getter, setter, data}); + + napi_status status = AttachData(env, object, callbackData); + if (status != napi_ok) { + delete callbackData; + NAPI_THROW_IF_FAILED(env, status, napi_property_descriptor()); + } + + return PropertyDescriptor({utf8name, + nullptr, + nullptr, + CbData::GetterWrapper, + CbData::SetterWrapper, + nullptr, + attributes, + callbackData}); +} + +template +inline PropertyDescriptor PropertyDescriptor::Accessor( + Napi::Env env, + Napi::Object object, + const std::string& utf8name, + Getter getter, + Setter setter, + napi_property_attributes attributes, + void* data) { + return Accessor( + env, object, utf8name.c_str(), getter, setter, attributes, data); +} + +template +inline PropertyDescriptor PropertyDescriptor::Accessor( + Napi::Env env, + Napi::Object object, + Name name, + Getter getter, + Setter setter, + napi_property_attributes attributes, + void* data) { + using CbData = details::AccessorCallbackData; + auto callbackData = new CbData({getter, setter, data}); + + napi_status status = AttachData(env, object, callbackData); + if (status != napi_ok) { + delete callbackData; + NAPI_THROW_IF_FAILED(env, status, napi_property_descriptor()); + } + + return PropertyDescriptor({nullptr, + name, + nullptr, + CbData::GetterWrapper, + CbData::SetterWrapper, + nullptr, + attributes, + callbackData}); +} + +template +inline PropertyDescriptor PropertyDescriptor::Function( + Napi::Env env, + Napi::Object /*object*/, + const char* utf8name, + Callable cb, + napi_property_attributes attributes, + void* data) { + return PropertyDescriptor({utf8name, + nullptr, + nullptr, + nullptr, + nullptr, + Napi::Function::New(env, cb, utf8name, data), + attributes, + nullptr}); +} + +template +inline PropertyDescriptor PropertyDescriptor::Function( + Napi::Env env, + Napi::Object object, + const std::string& utf8name, + Callable cb, + napi_property_attributes attributes, + void* data) { + return Function(env, object, utf8name.c_str(), cb, attributes, data); +} + +template +inline PropertyDescriptor PropertyDescriptor::Function( + Napi::Env env, + Napi::Object /*object*/, + Name name, + Callable cb, + napi_property_attributes attributes, + void* data) { + return PropertyDescriptor({nullptr, + name, + nullptr, + nullptr, + nullptr, + Napi::Function::New(env, cb, nullptr, data), + attributes, + nullptr}); +} + +inline PropertyDescriptor PropertyDescriptor::Value( + const char* utf8name, + napi_value value, + napi_property_attributes attributes) { + return PropertyDescriptor({utf8name, + nullptr, + nullptr, + nullptr, + nullptr, + value, + attributes, + nullptr}); +} + +inline PropertyDescriptor PropertyDescriptor::Value( + const std::string& utf8name, + napi_value value, + napi_property_attributes attributes) { + return Value(utf8name.c_str(), value, attributes); +} + +inline PropertyDescriptor PropertyDescriptor::Value( + napi_value name, napi_value value, napi_property_attributes attributes) { + return PropertyDescriptor( + {nullptr, name, nullptr, nullptr, nullptr, value, attributes, nullptr}); +} + +inline PropertyDescriptor PropertyDescriptor::Value( + Name name, Napi::Value value, napi_property_attributes attributes) { + napi_value nameValue = name; + napi_value valueValue = value; + return PropertyDescriptor::Value(nameValue, valueValue, attributes); +} + +inline PropertyDescriptor::PropertyDescriptor(napi_property_descriptor desc) + : _desc(desc) {} + +inline PropertyDescriptor::operator napi_property_descriptor&() { + return _desc; +} + +inline PropertyDescriptor::operator const napi_property_descriptor&() const { + return _desc; +} + +//////////////////////////////////////////////////////////////////////////////// +// InstanceWrap class +//////////////////////////////////////////////////////////////////////////////// + +template +inline void InstanceWrap::AttachPropData( + napi_env env, napi_value value, const napi_property_descriptor* prop) { + napi_status status; + if (!(prop->attributes & napi_static)) { + if (prop->method == T::InstanceVoidMethodCallbackWrapper) { + status = Napi::details::AttachData( + env, value, static_cast(prop->data)); + NAPI_THROW_IF_FAILED_VOID(env, status); + } else if (prop->method == T::InstanceMethodCallbackWrapper) { + status = Napi::details::AttachData( + env, value, static_cast(prop->data)); + NAPI_THROW_IF_FAILED_VOID(env, status); + } else if (prop->getter == T::InstanceGetterCallbackWrapper || + prop->setter == T::InstanceSetterCallbackWrapper) { + status = Napi::details::AttachData( + env, value, static_cast(prop->data)); + NAPI_THROW_IF_FAILED_VOID(env, status); + } + } +} + +template +inline ClassPropertyDescriptor InstanceWrap::InstanceMethod( + const char* utf8name, + InstanceVoidMethodCallback method, + napi_property_attributes attributes, + void* data) { + InstanceVoidMethodCallbackData* callbackData = + new InstanceVoidMethodCallbackData({method, data}); + + napi_property_descriptor desc = napi_property_descriptor(); + desc.utf8name = utf8name; + desc.method = T::InstanceVoidMethodCallbackWrapper; + desc.data = callbackData; + desc.attributes = attributes; + return desc; +} + +template +inline ClassPropertyDescriptor InstanceWrap::InstanceMethod( + const char* utf8name, + InstanceMethodCallback method, + napi_property_attributes attributes, + void* data) { + InstanceMethodCallbackData* callbackData = + new InstanceMethodCallbackData({method, data}); + + napi_property_descriptor desc = napi_property_descriptor(); + desc.utf8name = utf8name; + desc.method = T::InstanceMethodCallbackWrapper; + desc.data = callbackData; + desc.attributes = attributes; + return desc; +} + +template +inline ClassPropertyDescriptor InstanceWrap::InstanceMethod( + Symbol name, + InstanceVoidMethodCallback method, + napi_property_attributes attributes, + void* data) { + InstanceVoidMethodCallbackData* callbackData = + new InstanceVoidMethodCallbackData({method, data}); + + napi_property_descriptor desc = napi_property_descriptor(); + desc.name = name; + desc.method = T::InstanceVoidMethodCallbackWrapper; + desc.data = callbackData; + desc.attributes = attributes; + return desc; +} + +template +inline ClassPropertyDescriptor InstanceWrap::InstanceMethod( + Symbol name, + InstanceMethodCallback method, + napi_property_attributes attributes, + void* data) { + InstanceMethodCallbackData* callbackData = + new InstanceMethodCallbackData({method, data}); + + napi_property_descriptor desc = napi_property_descriptor(); + desc.name = name; + desc.method = T::InstanceMethodCallbackWrapper; + desc.data = callbackData; + desc.attributes = attributes; + return desc; +} + +template +template ::InstanceVoidMethodCallback method> +inline ClassPropertyDescriptor InstanceWrap::InstanceMethod( + const char* utf8name, napi_property_attributes attributes, void* data) { + napi_property_descriptor desc = napi_property_descriptor(); + desc.utf8name = utf8name; + desc.method = details::TemplatedInstanceVoidCallback; + desc.data = data; + desc.attributes = attributes; + return desc; +} + +template +template ::InstanceMethodCallback method> +inline ClassPropertyDescriptor InstanceWrap::InstanceMethod( + const char* utf8name, napi_property_attributes attributes, void* data) { + napi_property_descriptor desc = napi_property_descriptor(); + desc.utf8name = utf8name; + desc.method = details::TemplatedInstanceCallback; + desc.data = data; + desc.attributes = attributes; + return desc; +} + +template +template ::InstanceVoidMethodCallback method> +inline ClassPropertyDescriptor InstanceWrap::InstanceMethod( + Symbol name, napi_property_attributes attributes, void* data) { + napi_property_descriptor desc = napi_property_descriptor(); + desc.name = name; + desc.method = details::TemplatedInstanceVoidCallback; + desc.data = data; + desc.attributes = attributes; + return desc; +} + +template +template ::InstanceMethodCallback method> +inline ClassPropertyDescriptor InstanceWrap::InstanceMethod( + Symbol name, napi_property_attributes attributes, void* data) { + napi_property_descriptor desc = napi_property_descriptor(); + desc.name = name; + desc.method = details::TemplatedInstanceCallback; + desc.data = data; + desc.attributes = attributes; + return desc; +} + +template +inline ClassPropertyDescriptor InstanceWrap::InstanceAccessor( + const char* utf8name, + InstanceGetterCallback getter, + InstanceSetterCallback setter, + napi_property_attributes attributes, + void* data) { + InstanceAccessorCallbackData* callbackData = + new InstanceAccessorCallbackData({getter, setter, data}); + + napi_property_descriptor desc = napi_property_descriptor(); + desc.utf8name = utf8name; + desc.getter = getter != nullptr ? T::InstanceGetterCallbackWrapper : nullptr; + desc.setter = setter != nullptr ? T::InstanceSetterCallbackWrapper : nullptr; + desc.data = callbackData; + desc.attributes = attributes; + return desc; +} + +template +inline ClassPropertyDescriptor InstanceWrap::InstanceAccessor( + Symbol name, + InstanceGetterCallback getter, + InstanceSetterCallback setter, + napi_property_attributes attributes, + void* data) { + InstanceAccessorCallbackData* callbackData = + new InstanceAccessorCallbackData({getter, setter, data}); + + napi_property_descriptor desc = napi_property_descriptor(); + desc.name = name; + desc.getter = getter != nullptr ? T::InstanceGetterCallbackWrapper : nullptr; + desc.setter = setter != nullptr ? T::InstanceSetterCallbackWrapper : nullptr; + desc.data = callbackData; + desc.attributes = attributes; + return desc; +} + +template +template ::InstanceGetterCallback getter, + typename InstanceWrap::InstanceSetterCallback setter> +inline ClassPropertyDescriptor InstanceWrap::InstanceAccessor( + const char* utf8name, napi_property_attributes attributes, void* data) { + napi_property_descriptor desc = napi_property_descriptor(); + desc.utf8name = utf8name; + desc.getter = details::TemplatedInstanceCallback; + desc.setter = This::WrapSetter(This::SetterTag()); + desc.data = data; + desc.attributes = attributes; + return desc; +} + +template +template ::InstanceGetterCallback getter, + typename InstanceWrap::InstanceSetterCallback setter> +inline ClassPropertyDescriptor InstanceWrap::InstanceAccessor( + Symbol name, napi_property_attributes attributes, void* data) { + napi_property_descriptor desc = napi_property_descriptor(); + desc.name = name; + desc.getter = details::TemplatedInstanceCallback; + desc.setter = This::WrapSetter(This::SetterTag()); + desc.data = data; + desc.attributes = attributes; + return desc; +} + +template +inline ClassPropertyDescriptor InstanceWrap::InstanceValue( + const char* utf8name, + Napi::Value value, + napi_property_attributes attributes) { + napi_property_descriptor desc = napi_property_descriptor(); + desc.utf8name = utf8name; + desc.value = value; + desc.attributes = attributes; + return desc; +} + +template +inline ClassPropertyDescriptor InstanceWrap::InstanceValue( + Symbol name, Napi::Value value, napi_property_attributes attributes) { + napi_property_descriptor desc = napi_property_descriptor(); + desc.name = name; + desc.value = value; + desc.attributes = attributes; + return desc; +} + +template +inline napi_value InstanceWrap::InstanceVoidMethodCallbackWrapper( + napi_env env, napi_callback_info info) { + return details::WrapCallback(env, [&] { + CallbackInfo callbackInfo(env, info); + InstanceVoidMethodCallbackData* callbackData = + reinterpret_cast(callbackInfo.Data()); + callbackInfo.SetData(callbackData->data); + T* instance = T::Unwrap(callbackInfo.This().As()); + auto cb = callbackData->callback; + if (instance) (instance->*cb)(callbackInfo); + return nullptr; + }); +} + +template +inline napi_value InstanceWrap::InstanceMethodCallbackWrapper( + napi_env env, napi_callback_info info) { + return details::WrapCallback(env, [&] { + CallbackInfo callbackInfo(env, info); + InstanceMethodCallbackData* callbackData = + reinterpret_cast(callbackInfo.Data()); + callbackInfo.SetData(callbackData->data); + T* instance = T::Unwrap(callbackInfo.This().As()); + auto cb = callbackData->callback; + return instance ? (instance->*cb)(callbackInfo) : Napi::Value(); + }); +} + +template +inline napi_value InstanceWrap::InstanceGetterCallbackWrapper( + napi_env env, napi_callback_info info) { + return details::WrapCallback(env, [&] { + CallbackInfo callbackInfo(env, info); + InstanceAccessorCallbackData* callbackData = + reinterpret_cast(callbackInfo.Data()); + callbackInfo.SetData(callbackData->data); + T* instance = T::Unwrap(callbackInfo.This().As()); + auto cb = callbackData->getterCallback; + return instance ? (instance->*cb)(callbackInfo) : Napi::Value(); + }); +} + +template +inline napi_value InstanceWrap::InstanceSetterCallbackWrapper( + napi_env env, napi_callback_info info) { + return details::WrapCallback(env, [&] { + CallbackInfo callbackInfo(env, info); + InstanceAccessorCallbackData* callbackData = + reinterpret_cast(callbackInfo.Data()); + callbackInfo.SetData(callbackData->data); + T* instance = T::Unwrap(callbackInfo.This().As()); + auto cb = callbackData->setterCallback; + if (instance) (instance->*cb)(callbackInfo, callbackInfo[0]); + return nullptr; + }); +} + +template +template ::InstanceSetterCallback method> +inline napi_value InstanceWrap::WrappedMethod( + napi_env env, napi_callback_info info) NAPI_NOEXCEPT { + return details::WrapCallback(env, [&] { + const CallbackInfo cbInfo(env, info); + T* instance = T::Unwrap(cbInfo.This().As()); + if (instance) (instance->*method)(cbInfo, cbInfo[0]); + return nullptr; + }); +} + +//////////////////////////////////////////////////////////////////////////////// +// ObjectWrap class +//////////////////////////////////////////////////////////////////////////////// + +template +inline NAPI_NO_SANITIZE_VPTR ObjectWrap::ObjectWrap( + const Napi::CallbackInfo& callbackInfo) { + napi_env env = callbackInfo.Env(); + napi_value wrapper = callbackInfo.This(); + napi_status status; + napi_ref ref; + T* instance = static_cast(this); + status = napi_wrap(env, wrapper, instance, FinalizeCallback, nullptr, &ref); + NAPI_THROW_IF_FAILED_VOID(env, status); + + Reference* instanceRef = instance; + *instanceRef = Reference(env, ref); +} + +template +inline NAPI_NO_SANITIZE_VPTR ObjectWrap::~ObjectWrap() { + // If the JS object still exists at this point, remove the finalizer added + // through `napi_wrap()`. + if (!IsEmpty() && !_finalized) { + Object object = Value(); + // It is not valid to call `napi_remove_wrap()` with an empty `object`. + // This happens e.g. during garbage collection. + if (!object.IsEmpty() && _construction_failed) { + napi_remove_wrap(Env(), object, nullptr); + } + } +} + +// with RTTI turned on, modern compilers check to see if virtual function +// pointers are stripped of RTTI by void casts. this is intrinsic to how Unwrap +// works, so we inject a compiler pragma to turn off that check just for the +// affected methods. this compiler check is on by default in Android NDK 29. +template +inline NAPI_NO_SANITIZE_VPTR T* ObjectWrap::Unwrap(Object wrapper) { + void* unwrapped; + napi_status status = napi_unwrap(wrapper.Env(), wrapper, &unwrapped); + NAPI_THROW_IF_FAILED(wrapper.Env(), status, nullptr); + return static_cast(unwrapped); +} + +template +inline Function ObjectWrap::DefineClass( + Napi::Env env, + const char* utf8name, + const size_t props_count, + const napi_property_descriptor* descriptors, + void* data) { + napi_status status; + std::vector props(props_count); + + // We copy the descriptors to a local array because before defining the class + // we must replace static method property descriptors with value property + // descriptors such that the value is a function-valued `napi_value` created + // with `CreateFunction()`. + // + // This replacement could be made for instance methods as well, but V8 aborts + // if we do that, because it expects methods defined on the prototype template + // to have `FunctionTemplate`s. + for (size_t index = 0; index < props_count; index++) { + props[index] = descriptors[index]; + napi_property_descriptor* prop = &props[index]; + if (prop->method == T::StaticMethodCallbackWrapper) { + status = + CreateFunction(env, + utf8name, + prop->method, + static_cast(prop->data), + &(prop->value)); + NAPI_THROW_IF_FAILED(env, status, Function()); + prop->method = nullptr; + prop->data = nullptr; + } else if (prop->method == T::StaticVoidMethodCallbackWrapper) { + status = + CreateFunction(env, + utf8name, + prop->method, + static_cast(prop->data), + &(prop->value)); + NAPI_THROW_IF_FAILED(env, status, Function()); + prop->method = nullptr; + prop->data = nullptr; + } + } + + napi_value value; + status = napi_define_class(env, + utf8name, + NAPI_AUTO_LENGTH, + T::ConstructorCallbackWrapper, + data, + props_count, + props.data(), + &value); + NAPI_THROW_IF_FAILED(env, status, Function()); + + // After defining the class we iterate once more over the property descriptors + // and attach the data associated with accessors and instance methods to the + // newly created JavaScript class. + for (size_t idx = 0; idx < props_count; idx++) { + const napi_property_descriptor* prop = &props[idx]; + + if (prop->getter == T::StaticGetterCallbackWrapper || + prop->setter == T::StaticSetterCallbackWrapper) { + status = Napi::details::AttachData( + env, value, static_cast(prop->data)); + NAPI_THROW_IF_FAILED(env, status, Function()); + } else { + // InstanceWrap::AttachPropData is responsible for attaching the data + // of instance methods and accessors. + T::AttachPropData(env, value, prop); + } + } + + return Function(env, value); +} + +template +inline Function ObjectWrap::DefineClass( + Napi::Env env, + const char* utf8name, + const std::initializer_list>& properties, + void* data) { + return DefineClass( + env, + utf8name, + properties.size(), + reinterpret_cast(properties.begin()), + data); +} + +template +inline Function ObjectWrap::DefineClass( + Napi::Env env, + const char* utf8name, + const std::vector>& properties, + void* data) { + return DefineClass( + env, + utf8name, + properties.size(), + reinterpret_cast(properties.data()), + data); +} + +template +inline ClassPropertyDescriptor ObjectWrap::StaticMethod( + const char* utf8name, + StaticVoidMethodCallback method, + napi_property_attributes attributes, + void* data) { + StaticVoidMethodCallbackData* callbackData = + new StaticVoidMethodCallbackData({method, data}); + + napi_property_descriptor desc = napi_property_descriptor(); + desc.utf8name = utf8name; + desc.method = T::StaticVoidMethodCallbackWrapper; + desc.data = callbackData; + desc.attributes = + static_cast(attributes | napi_static); + return desc; +} + +template +inline ClassPropertyDescriptor ObjectWrap::StaticMethod( + const char* utf8name, + StaticMethodCallback method, + napi_property_attributes attributes, + void* data) { + StaticMethodCallbackData* callbackData = + new StaticMethodCallbackData({method, data}); + + napi_property_descriptor desc = napi_property_descriptor(); + desc.utf8name = utf8name; + desc.method = T::StaticMethodCallbackWrapper; + desc.data = callbackData; + desc.attributes = + static_cast(attributes | napi_static); + return desc; +} + +template +inline ClassPropertyDescriptor ObjectWrap::StaticMethod( + Symbol name, + StaticVoidMethodCallback method, + napi_property_attributes attributes, + void* data) { + StaticVoidMethodCallbackData* callbackData = + new StaticVoidMethodCallbackData({method, data}); + + napi_property_descriptor desc = napi_property_descriptor(); + desc.name = name; + desc.method = T::StaticVoidMethodCallbackWrapper; + desc.data = callbackData; + desc.attributes = + static_cast(attributes | napi_static); + return desc; +} + +template +inline ClassPropertyDescriptor ObjectWrap::StaticMethod( + Symbol name, + StaticMethodCallback method, + napi_property_attributes attributes, + void* data) { + StaticMethodCallbackData* callbackData = + new StaticMethodCallbackData({method, data}); + + napi_property_descriptor desc = napi_property_descriptor(); + desc.name = name; + desc.method = T::StaticMethodCallbackWrapper; + desc.data = callbackData; + desc.attributes = + static_cast(attributes | napi_static); + return desc; +} + +template +template ::StaticVoidMethodCallback method> +inline ClassPropertyDescriptor ObjectWrap::StaticMethod( + const char* utf8name, napi_property_attributes attributes, void* data) { + napi_property_descriptor desc = napi_property_descriptor(); + desc.utf8name = utf8name; + desc.method = details::TemplatedVoidCallback; + desc.data = data; + desc.attributes = + static_cast(attributes | napi_static); + return desc; +} + +template +template ::StaticVoidMethodCallback method> +inline ClassPropertyDescriptor ObjectWrap::StaticMethod( + Symbol name, napi_property_attributes attributes, void* data) { + napi_property_descriptor desc = napi_property_descriptor(); + desc.name = name; + desc.method = details::TemplatedVoidCallback; + desc.data = data; + desc.attributes = + static_cast(attributes | napi_static); + return desc; +} + +template +template ::StaticMethodCallback method> +inline ClassPropertyDescriptor ObjectWrap::StaticMethod( + const char* utf8name, napi_property_attributes attributes, void* data) { + napi_property_descriptor desc = napi_property_descriptor(); + desc.utf8name = utf8name; + desc.method = details::TemplatedCallback; + desc.data = data; + desc.attributes = + static_cast(attributes | napi_static); + return desc; +} + +template +template ::StaticMethodCallback method> +inline ClassPropertyDescriptor ObjectWrap::StaticMethod( + Symbol name, napi_property_attributes attributes, void* data) { + napi_property_descriptor desc = napi_property_descriptor(); + desc.name = name; + desc.method = details::TemplatedCallback; + desc.data = data; + desc.attributes = + static_cast(attributes | napi_static); + return desc; +} + +template +inline ClassPropertyDescriptor ObjectWrap::StaticAccessor( + const char* utf8name, + StaticGetterCallback getter, + StaticSetterCallback setter, + napi_property_attributes attributes, + void* data) { + StaticAccessorCallbackData* callbackData = + new StaticAccessorCallbackData({getter, setter, data}); + + napi_property_descriptor desc = napi_property_descriptor(); + desc.utf8name = utf8name; + desc.getter = getter != nullptr ? T::StaticGetterCallbackWrapper : nullptr; + desc.setter = setter != nullptr ? T::StaticSetterCallbackWrapper : nullptr; + desc.data = callbackData; + desc.attributes = + static_cast(attributes | napi_static); + return desc; +} + +template +inline ClassPropertyDescriptor ObjectWrap::StaticAccessor( + Symbol name, + StaticGetterCallback getter, + StaticSetterCallback setter, + napi_property_attributes attributes, + void* data) { + StaticAccessorCallbackData* callbackData = + new StaticAccessorCallbackData({getter, setter, data}); + + napi_property_descriptor desc = napi_property_descriptor(); + desc.name = name; + desc.getter = getter != nullptr ? T::StaticGetterCallbackWrapper : nullptr; + desc.setter = setter != nullptr ? T::StaticSetterCallbackWrapper : nullptr; + desc.data = callbackData; + desc.attributes = + static_cast(attributes | napi_static); + return desc; +} + +template +template ::StaticGetterCallback getter, + typename ObjectWrap::StaticSetterCallback setter> +inline ClassPropertyDescriptor ObjectWrap::StaticAccessor( + const char* utf8name, napi_property_attributes attributes, void* data) { + napi_property_descriptor desc = napi_property_descriptor(); + desc.utf8name = utf8name; + desc.getter = details::TemplatedCallback; + desc.setter = This::WrapStaticSetter(This::StaticSetterTag()); + desc.data = data; + desc.attributes = + static_cast(attributes | napi_static); + return desc; +} + +template +template ::StaticGetterCallback getter, + typename ObjectWrap::StaticSetterCallback setter> +inline ClassPropertyDescriptor ObjectWrap::StaticAccessor( + Symbol name, napi_property_attributes attributes, void* data) { + napi_property_descriptor desc = napi_property_descriptor(); + desc.name = name; + desc.getter = details::TemplatedCallback; + desc.setter = This::WrapStaticSetter(This::StaticSetterTag()); + desc.data = data; + desc.attributes = + static_cast(attributes | napi_static); + return desc; +} + +template +inline ClassPropertyDescriptor ObjectWrap::StaticValue( + const char* utf8name, + Napi::Value value, + napi_property_attributes attributes) { + napi_property_descriptor desc = napi_property_descriptor(); + desc.utf8name = utf8name; + desc.value = value; + desc.attributes = + static_cast(attributes | napi_static); + return desc; +} + +template +inline ClassPropertyDescriptor ObjectWrap::StaticValue( + Symbol name, Napi::Value value, napi_property_attributes attributes) { + napi_property_descriptor desc = napi_property_descriptor(); + desc.name = name; + desc.value = value; + desc.attributes = + static_cast(attributes | napi_static); + return desc; +} + +template +inline Value ObjectWrap::OnCalledAsFunction( + const Napi::CallbackInfo& callbackInfo) { + NAPI_THROW( + TypeError::New(callbackInfo.Env(), + "Class constructors cannot be invoked without 'new'"), + Napi::Value()); +} + +template +inline void ObjectWrap::Finalize(Napi::Env /*env*/) {} + +template +inline void ObjectWrap::Finalize(BasicEnv /*env*/) {} + +template +inline napi_value ObjectWrap::ConstructorCallbackWrapper( + napi_env env, napi_callback_info info) { + napi_value new_target; + napi_status status = napi_get_new_target(env, info, &new_target); + if (status != napi_ok) return nullptr; + + bool isConstructCall = (new_target != nullptr); + if (!isConstructCall) { + return details::WrapCallback( + env, [&] { return T::OnCalledAsFunction(CallbackInfo(env, info)); }); + } + + napi_value wrapper = details::WrapCallback(env, [&] { + CallbackInfo callbackInfo(env, info); + T* instance = new T(callbackInfo); +#ifdef NODE_ADDON_API_CPP_EXCEPTIONS + instance->_construction_failed = false; +#else + if (callbackInfo.Env().IsExceptionPending()) { + // We need to clear the exception so that removing the wrap might work. + Error e = callbackInfo.Env().GetAndClearPendingException(); + delete instance; + e.ThrowAsJavaScriptException(); + } else { + instance->_construction_failed = false; + } +#endif // NODE_ADDON_API_CPP_EXCEPTIONS + return callbackInfo.This(); + }); + + return wrapper; +} + +template +inline napi_value ObjectWrap::StaticVoidMethodCallbackWrapper( + napi_env env, napi_callback_info info) { + return details::WrapCallback(env, [&] { + CallbackInfo callbackInfo(env, info); + StaticVoidMethodCallbackData* callbackData = + reinterpret_cast(callbackInfo.Data()); + callbackInfo.SetData(callbackData->data); + callbackData->callback(callbackInfo); + return nullptr; + }); +} + +template +inline napi_value ObjectWrap::StaticMethodCallbackWrapper( + napi_env env, napi_callback_info info) { + return details::WrapCallback(env, [&] { + CallbackInfo callbackInfo(env, info); + StaticMethodCallbackData* callbackData = + reinterpret_cast(callbackInfo.Data()); + callbackInfo.SetData(callbackData->data); + return callbackData->callback(callbackInfo); + }); +} + +template +inline napi_value ObjectWrap::StaticGetterCallbackWrapper( + napi_env env, napi_callback_info info) { + return details::WrapCallback(env, [&] { + CallbackInfo callbackInfo(env, info); + StaticAccessorCallbackData* callbackData = + reinterpret_cast(callbackInfo.Data()); + callbackInfo.SetData(callbackData->data); + return callbackData->getterCallback(callbackInfo); + }); +} + +template +inline napi_value ObjectWrap::StaticSetterCallbackWrapper( + napi_env env, napi_callback_info info) { + return details::WrapCallback(env, [&] { + CallbackInfo callbackInfo(env, info); + StaticAccessorCallbackData* callbackData = + reinterpret_cast(callbackInfo.Data()); + callbackInfo.SetData(callbackData->data); + callbackData->setterCallback(callbackInfo, callbackInfo[0]); + return nullptr; + }); +} + +template +inline void ObjectWrap::FinalizeCallback(node_addon_api_basic_env env, + void* data, + void* /*hint*/) { + // If the child class does not override _any_ Finalize() method, `env` will be + // unused because of the constexpr guards. Explicitly reference it here to + // bypass compiler warnings. + (void)env; + T* instance = static_cast(data); + + // Prevent ~ObjectWrap from calling napi_remove_wrap. + // The instance->_ref should be deleted with napi_delete_reference in + // ~Reference. + instance->_finalized = true; + + // If class overrides the basic finalizer, execute it. + if constexpr (details::HasBasicFinalizer::value) { +#ifndef NODE_API_EXPERIMENTAL_HAS_POST_FINALIZER + HandleScope scope(env); +#endif + + instance->Finalize(Napi::BasicEnv(env)); + } + + // If class overrides the (extended) finalizer, either schedule it or + // execute it immediately (depending on experimental features enabled). + if constexpr (details::HasExtendedFinalizer::value) { +#ifdef NODE_API_EXPERIMENTAL_HAS_POST_FINALIZER + // In experimental, attach via node_api_post_finalizer. + // `PostFinalizeCallback` is responsible for deleting the `T* instance`, + // after calling the user-provided finalizer. + napi_status status = + node_api_post_finalizer(env, PostFinalizeCallback, data, nullptr); + NAPI_FATAL_IF_FAILED(status, + "ObjectWrap::FinalizeCallback", + "node_api_post_finalizer failed"); +#else + // In non-experimental, this `FinalizeCallback` already executes from a + // non-basic environment. Execute the override directly. + // `PostFinalizeCallback` is responsible for deleting the `T* instance`, + // after calling the user-provided finalizer. + HandleScope scope(env); + PostFinalizeCallback(env, data, static_cast(nullptr)); +#endif + } + // If the instance does _not_ override the (extended) finalizer, delete the + // `T* instance` immediately. + else { + delete instance; + } +} + +template +inline void ObjectWrap::PostFinalizeCallback(napi_env env, + void* data, + void* /*hint*/) { + T* instance = static_cast(data); + instance->Finalize(Napi::Env(env)); + delete instance; +} + +template +template ::StaticSetterCallback method> +inline napi_value ObjectWrap::WrappedMethod( + napi_env env, napi_callback_info info) NAPI_NOEXCEPT { + return details::WrapCallback(env, [&] { + const CallbackInfo cbInfo(env, info); + // MSVC requires to copy 'method' function pointer to a local variable + // before invoking it. + auto m = method; + m(cbInfo, cbInfo[0]); + return nullptr; + }); +} + +//////////////////////////////////////////////////////////////////////////////// +// HandleScope class +//////////////////////////////////////////////////////////////////////////////// + +inline HandleScope::HandleScope(napi_env env, napi_handle_scope scope) + : _env(env), _scope(scope) {} + +inline HandleScope::HandleScope(Napi::Env env) : _env(env) { + napi_status status = napi_open_handle_scope(_env, &_scope); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} + +inline HandleScope::~HandleScope() { + napi_status status = napi_close_handle_scope(_env, _scope); + NAPI_FATAL_IF_FAILED( + status, "HandleScope::~HandleScope", "napi_close_handle_scope"); +} + +inline HandleScope::operator napi_handle_scope() const { + return _scope; +} + +inline Napi::Env HandleScope::Env() const { + return Napi::Env(_env); +} + +//////////////////////////////////////////////////////////////////////////////// +// EscapableHandleScope class +//////////////////////////////////////////////////////////////////////////////// + +inline EscapableHandleScope::EscapableHandleScope( + napi_env env, napi_escapable_handle_scope scope) + : _env(env), _scope(scope) {} + +inline EscapableHandleScope::EscapableHandleScope(Napi::Env env) : _env(env) { + napi_status status = napi_open_escapable_handle_scope(_env, &_scope); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} + +inline EscapableHandleScope::~EscapableHandleScope() { + napi_status status = napi_close_escapable_handle_scope(_env, _scope); + NAPI_FATAL_IF_FAILED(status, + "EscapableHandleScope::~EscapableHandleScope", + "napi_close_escapable_handle_scope"); +} + +inline EscapableHandleScope::operator napi_escapable_handle_scope() const { + return _scope; +} + +inline Napi::Env EscapableHandleScope::Env() const { + return Napi::Env(_env); +} + +inline Value EscapableHandleScope::Escape(napi_value escapee) { + napi_value result; + napi_status status = napi_escape_handle(_env, _scope, escapee, &result); + NAPI_THROW_IF_FAILED(_env, status, Value()); + return Value(_env, result); +} + +#if (NAPI_VERSION > 2) +//////////////////////////////////////////////////////////////////////////////// +// CallbackScope class +//////////////////////////////////////////////////////////////////////////////// + +inline CallbackScope::CallbackScope(napi_env env, napi_callback_scope scope) + : _env(env), _scope(scope) {} + +inline CallbackScope::CallbackScope(napi_env env, napi_async_context context) + : _env(env) { + napi_status status = + napi_open_callback_scope(_env, Object::New(env), context, &_scope); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} + +inline CallbackScope::~CallbackScope() { + napi_status status = napi_close_callback_scope(_env, _scope); + NAPI_FATAL_IF_FAILED( + status, "CallbackScope::~CallbackScope", "napi_close_callback_scope"); +} + +inline CallbackScope::operator napi_callback_scope() const { + return _scope; +} + +inline Napi::Env CallbackScope::Env() const { + return Napi::Env(_env); +} +#endif + +//////////////////////////////////////////////////////////////////////////////// +// AsyncContext class +//////////////////////////////////////////////////////////////////////////////// + +inline AsyncContext::AsyncContext(napi_env env, const char* resource_name) + : AsyncContext(env, resource_name, Object::New(env)) {} + +inline AsyncContext::AsyncContext(napi_env env, + const char* resource_name, + const Object& resource) + : _env(env), _context(nullptr) { + napi_value resource_id; + napi_status status = napi_create_string_utf8( + _env, resource_name, NAPI_AUTO_LENGTH, &resource_id); + NAPI_THROW_IF_FAILED_VOID(_env, status); + + status = napi_async_init(_env, resource, resource_id, &_context); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} + +inline AsyncContext::~AsyncContext() { + if (_context != nullptr) { + napi_async_destroy(_env, _context); + _context = nullptr; + } +} + +inline AsyncContext::AsyncContext(AsyncContext&& other) { + _env = other._env; + other._env = nullptr; + _context = other._context; + other._context = nullptr; +} + +inline AsyncContext& AsyncContext::operator=(AsyncContext&& other) { + _env = other._env; + other._env = nullptr; + _context = other._context; + other._context = nullptr; + return *this; +} + +inline AsyncContext::operator napi_async_context() const { + return _context; +} + +inline Napi::Env AsyncContext::Env() const { + return Napi::Env(_env); +} + +//////////////////////////////////////////////////////////////////////////////// +// AsyncWorker class +//////////////////////////////////////////////////////////////////////////////// + +#if NAPI_HAS_THREADS + +inline AsyncWorker::AsyncWorker(const Function& callback) + : AsyncWorker(callback, "generic") {} + +inline AsyncWorker::AsyncWorker(const Function& callback, + const char* resource_name) + : AsyncWorker(callback, resource_name, Object::New(callback.Env())) {} + +inline AsyncWorker::AsyncWorker(const Function& callback, + const char* resource_name, + const Object& resource) + : AsyncWorker( + Object::New(callback.Env()), callback, resource_name, resource) {} + +inline AsyncWorker::AsyncWorker(const Object& receiver, + const Function& callback) + : AsyncWorker(receiver, callback, "generic") {} + +inline AsyncWorker::AsyncWorker(const Object& receiver, + const Function& callback, + const char* resource_name) + : AsyncWorker( + receiver, callback, resource_name, Object::New(callback.Env())) {} + +inline AsyncWorker::AsyncWorker(const Object& receiver, + const Function& callback, + const char* resource_name, + const Object& resource) + : _env(callback.Env()), + _receiver(Napi::Persistent(receiver)), + _callback(Napi::Persistent(callback)), + _suppress_destruct(false) { + napi_value resource_id; + napi_status status = napi_create_string_latin1( + _env, resource_name, NAPI_AUTO_LENGTH, &resource_id); + NAPI_THROW_IF_FAILED_VOID(_env, status); + + status = napi_create_async_work(_env, + resource, + resource_id, + OnAsyncWorkExecute, + OnAsyncWorkComplete, + this, + &_work); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} + +inline AsyncWorker::AsyncWorker(Napi::Env env) : AsyncWorker(env, "generic") {} + +inline AsyncWorker::AsyncWorker(Napi::Env env, const char* resource_name) + : AsyncWorker(env, resource_name, Object::New(env)) {} + +inline AsyncWorker::AsyncWorker(Napi::Env env, + const char* resource_name, + const Object& resource) + : _env(env), _receiver(), _callback(), _suppress_destruct(false) { + napi_value resource_id; + napi_status status = napi_create_string_latin1( + _env, resource_name, NAPI_AUTO_LENGTH, &resource_id); + NAPI_THROW_IF_FAILED_VOID(_env, status); + + status = napi_create_async_work(_env, + resource, + resource_id, + OnAsyncWorkExecute, + OnAsyncWorkComplete, + this, + &_work); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} + +inline AsyncWorker::~AsyncWorker() { + if (_work != nullptr) { + napi_delete_async_work(_env, _work); + _work = nullptr; + } +} + +inline void AsyncWorker::Destroy() { + delete this; +} + +inline AsyncWorker::operator napi_async_work() const { + return _work; +} + +inline Napi::Env AsyncWorker::Env() const { + return Napi::Env(_env); +} + +inline void AsyncWorker::Queue() { + napi_status status = napi_queue_async_work(_env, _work); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} + +inline void AsyncWorker::Cancel() { + napi_status status = napi_cancel_async_work(_env, _work); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} + +inline ObjectReference& AsyncWorker::Receiver() { + return _receiver; +} + +inline FunctionReference& AsyncWorker::Callback() { + return _callback; +} + +inline void AsyncWorker::SuppressDestruct() { + _suppress_destruct = true; +} + +inline void AsyncWorker::OnOK() { + if (!_callback.IsEmpty()) { + _callback.Call(_receiver.Value(), GetResult(_callback.Env())); + } +} + +inline void AsyncWorker::OnError(const Error& e) { + if (!_callback.IsEmpty()) { + _callback.Call(_receiver.Value(), + std::initializer_list{e.Value()}); + } +} + +inline void AsyncWorker::SetError(const std::string& error) { + _error = error; +} + +inline std::vector AsyncWorker::GetResult(Napi::Env /*env*/) { + return {}; +} +// The OnAsyncWorkExecute method receives an napi_env argument. However, do NOT +// use it within this method, as it does not run on the JavaScript thread and +// must not run any method that would cause JavaScript to run. In practice, +// this means that almost any use of napi_env will be incorrect. +inline void AsyncWorker::OnAsyncWorkExecute(napi_env env, void* asyncworker) { + AsyncWorker* self = static_cast(asyncworker); + self->OnExecute(env); +} +// The OnExecute method receives an napi_env argument. However, do NOT +// use it within this method, as it does not run on the JavaScript thread and +// must not run any method that would cause JavaScript to run. In practice, +// this means that almost any use of napi_env will be incorrect. +inline void AsyncWorker::OnExecute(Napi::Env /*DO_NOT_USE*/) { +#ifdef NODE_ADDON_API_CPP_EXCEPTIONS + try { + Execute(); + } catch (const std::exception& e) { + SetError(e.what()); + } +#else // NODE_ADDON_API_CPP_EXCEPTIONS + Execute(); +#endif // NODE_ADDON_API_CPP_EXCEPTIONS +} + +inline void AsyncWorker::OnAsyncWorkComplete(napi_env env, + napi_status status, + void* asyncworker) { + AsyncWorker* self = static_cast(asyncworker); + self->OnWorkComplete(env, status); +} +inline void AsyncWorker::OnWorkComplete(Napi::Env env, napi_status status) { + if (status != napi_cancelled) { + HandleScope scope(_env); + details::WrapCallback(env, [&] { + if (_error.size() == 0) { + OnOK(); + } else { + OnError(Error::New(_env, _error)); + } + return nullptr; + }); + } + if (!_suppress_destruct) { + Destroy(); + } +} + +#endif // NAPI_HAS_THREADS + +#if (NAPI_VERSION > 3 && NAPI_HAS_THREADS) +//////////////////////////////////////////////////////////////////////////////// +// TypedThreadSafeFunction class +//////////////////////////////////////////////////////////////////////////////// + +// Starting with NAPI 5, the JavaScript function `func` parameter of +// `napi_create_threadsafe_function` is optional. +#if NAPI_VERSION > 4 +// static, with Callback [missing] Resource [missing] Finalizer [missing] +template +template +inline TypedThreadSafeFunction +TypedThreadSafeFunction::New( + napi_env env, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context) { + TypedThreadSafeFunction tsfn; + + napi_status status = + napi_create_threadsafe_function(env, + nullptr, + nullptr, + String::From(env, resourceName), + maxQueueSize, + initialThreadCount, + nullptr, + nullptr, + context, + CallJsInternal, + &tsfn._tsfn); + if (status != napi_ok) { + NAPI_THROW_IF_FAILED( + env, status, TypedThreadSafeFunction()); + } + + return tsfn; +} + +// static, with Callback [missing] Resource [passed] Finalizer [missing] +template +template +inline TypedThreadSafeFunction +TypedThreadSafeFunction::New( + napi_env env, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context) { + TypedThreadSafeFunction tsfn; + + napi_status status = + napi_create_threadsafe_function(env, + nullptr, + resource, + String::From(env, resourceName), + maxQueueSize, + initialThreadCount, + nullptr, + nullptr, + context, + CallJsInternal, + &tsfn._tsfn); + if (status != napi_ok) { + NAPI_THROW_IF_FAILED( + env, status, TypedThreadSafeFunction()); + } + + return tsfn; +} + +// static, with Callback [missing] Resource [missing] Finalizer [passed] +template +template +inline TypedThreadSafeFunction +TypedThreadSafeFunction::New( + napi_env env, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data) { + TypedThreadSafeFunction tsfn; + + auto* finalizeData = new details:: + ThreadSafeFinalize( + {data, finalizeCallback}); + auto fini = + details::ThreadSafeFinalize:: + FinalizeFinalizeWrapperWithDataAndContext; + napi_status status = + napi_create_threadsafe_function(env, + nullptr, + nullptr, + String::From(env, resourceName), + maxQueueSize, + initialThreadCount, + finalizeData, + fini, + context, + CallJsInternal, + &tsfn._tsfn); + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED( + env, status, TypedThreadSafeFunction()); + } + + return tsfn; +} + +// static, with Callback [missing] Resource [passed] Finalizer [passed] +template +template +inline TypedThreadSafeFunction +TypedThreadSafeFunction::New( + napi_env env, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data) { + TypedThreadSafeFunction tsfn; + + auto* finalizeData = new details:: + ThreadSafeFinalize( + {data, finalizeCallback}); + auto fini = + details::ThreadSafeFinalize:: + FinalizeFinalizeWrapperWithDataAndContext; + napi_status status = + napi_create_threadsafe_function(env, + nullptr, + resource, + String::From(env, resourceName), + maxQueueSize, + initialThreadCount, + finalizeData, + fini, + context, + CallJsInternal, + &tsfn._tsfn); + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED( + env, status, TypedThreadSafeFunction()); + } + + return tsfn; +} +#endif + +// static, with Callback [passed] Resource [missing] Finalizer [missing] +template +template +inline TypedThreadSafeFunction +TypedThreadSafeFunction::New( + napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context) { + TypedThreadSafeFunction tsfn; + + napi_status status = + napi_create_threadsafe_function(env, + callback, + nullptr, + String::From(env, resourceName), + maxQueueSize, + initialThreadCount, + nullptr, + nullptr, + context, + CallJsInternal, + &tsfn._tsfn); + if (status != napi_ok) { + NAPI_THROW_IF_FAILED( + env, status, TypedThreadSafeFunction()); + } + + return tsfn; +} + +// static, with Callback [passed] Resource [passed] Finalizer [missing] +template +template +inline TypedThreadSafeFunction +TypedThreadSafeFunction::New( + napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context) { + TypedThreadSafeFunction tsfn; + + napi_status status = + napi_create_threadsafe_function(env, + callback, + resource, + String::From(env, resourceName), + maxQueueSize, + initialThreadCount, + nullptr, + nullptr, + context, + CallJsInternal, + &tsfn._tsfn); + if (status != napi_ok) { + NAPI_THROW_IF_FAILED( + env, status, TypedThreadSafeFunction()); + } + + return tsfn; +} + +// static, with Callback [passed] Resource [missing] Finalizer [passed] +template +template +inline TypedThreadSafeFunction +TypedThreadSafeFunction::New( + napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data) { + TypedThreadSafeFunction tsfn; + + auto* finalizeData = new details:: + ThreadSafeFinalize( + {data, finalizeCallback}); + auto fini = + details::ThreadSafeFinalize:: + FinalizeFinalizeWrapperWithDataAndContext; + napi_status status = + napi_create_threadsafe_function(env, + callback, + nullptr, + String::From(env, resourceName), + maxQueueSize, + initialThreadCount, + finalizeData, + fini, + context, + CallJsInternal, + &tsfn._tsfn); + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED( + env, status, TypedThreadSafeFunction()); + } + + return tsfn; +} + +// static, with: Callback [passed] Resource [passed] Finalizer [passed] +template +template +inline TypedThreadSafeFunction +TypedThreadSafeFunction::New( + napi_env env, + CallbackType callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data) { + TypedThreadSafeFunction tsfn; + + auto* finalizeData = new details:: + ThreadSafeFinalize( + {data, finalizeCallback}); + auto fini = + details::ThreadSafeFinalize:: + FinalizeFinalizeWrapperWithDataAndContext; + napi_status status = napi_create_threadsafe_function( + env, + details::DefaultCallbackWrapper< + CallbackType, + TypedThreadSafeFunction>(env, + callback), + resource, + String::From(env, resourceName), + maxQueueSize, + initialThreadCount, + finalizeData, + fini, + context, + CallJsInternal, + &tsfn._tsfn); + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED( + env, status, TypedThreadSafeFunction()); + } + + return tsfn; +} + +template +inline TypedThreadSafeFunction:: + TypedThreadSafeFunction() + : _tsfn() {} + +template +inline TypedThreadSafeFunction:: + TypedThreadSafeFunction(napi_threadsafe_function tsfn) + : _tsfn(tsfn) {} + +template +inline TypedThreadSafeFunction:: +operator napi_threadsafe_function() const { + return _tsfn; +} + +template +inline napi_status +TypedThreadSafeFunction::BlockingCall( + DataType* data) const { + return napi_call_threadsafe_function(_tsfn, data, napi_tsfn_blocking); +} + +template +inline napi_status +TypedThreadSafeFunction::NonBlockingCall( + DataType* data) const { + return napi_call_threadsafe_function(_tsfn, data, napi_tsfn_nonblocking); +} + +template +inline void TypedThreadSafeFunction::Ref( + napi_env env) const { + if (_tsfn != nullptr) { + napi_status status = napi_ref_threadsafe_function(env, _tsfn); + NAPI_THROW_IF_FAILED_VOID(env, status); + } +} + +template +inline void TypedThreadSafeFunction::Unref( + napi_env env) const { + if (_tsfn != nullptr) { + napi_status status = napi_unref_threadsafe_function(env, _tsfn); + NAPI_THROW_IF_FAILED_VOID(env, status); + } +} + +template +inline napi_status +TypedThreadSafeFunction::Acquire() const { + return napi_acquire_threadsafe_function(_tsfn); +} + +template +inline napi_status +TypedThreadSafeFunction::Release() const { + return napi_release_threadsafe_function(_tsfn, napi_tsfn_release); +} + +template +inline napi_status +TypedThreadSafeFunction::Abort() const { + return napi_release_threadsafe_function(_tsfn, napi_tsfn_abort); +} + +template +inline ContextType* +TypedThreadSafeFunction::GetContext() const { + void* context; + napi_status status = napi_get_threadsafe_function_context(_tsfn, &context); + NAPI_FATAL_IF_FAILED(status, + "TypedThreadSafeFunction::GetContext", + "napi_get_threadsafe_function_context"); + return static_cast(context); +} + +// static +template +void TypedThreadSafeFunction::CallJsInternal( + napi_env env, napi_value jsCallback, void* context, void* data) { + details::CallJsWrapper( + env, jsCallback, context, data); +} + +#if NAPI_VERSION == 4 +// static +template +Napi::Function +TypedThreadSafeFunction::EmptyFunctionFactory( + Napi::Env env) { + return Napi::Function::New(env, [](const CallbackInfo& cb) {}); +} + +// static +template +Napi::Function +TypedThreadSafeFunction::FunctionOrEmpty( + Napi::Env env, Napi::Function& callback) { + if (callback.IsEmpty()) { + return EmptyFunctionFactory(env); + } + return callback; +} + +#else +// static +template +std::nullptr_t +TypedThreadSafeFunction::EmptyFunctionFactory( + Napi::Env /*env*/) { + return nullptr; +} + +// static +template +Napi::Function +TypedThreadSafeFunction::FunctionOrEmpty( + Napi::Env /*env*/, Napi::Function& callback) { + return callback; +} + +#endif + +//////////////////////////////////////////////////////////////////////////////// +// ThreadSafeFunction class +//////////////////////////////////////////////////////////////////////////////// + +// static +template +inline ThreadSafeFunction ThreadSafeFunction::New(napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount) { + return New( + env, callback, Object(), resourceName, maxQueueSize, initialThreadCount); +} + +// static +template +inline ThreadSafeFunction ThreadSafeFunction::New(napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context) { + return New(env, + callback, + Object(), + resourceName, + maxQueueSize, + initialThreadCount, + context); +} + +// static +template +inline ThreadSafeFunction ThreadSafeFunction::New(napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + Finalizer finalizeCallback) { + return New(env, + callback, + Object(), + resourceName, + maxQueueSize, + initialThreadCount, + finalizeCallback); +} + +// static +template +inline ThreadSafeFunction ThreadSafeFunction::New(napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + Finalizer finalizeCallback, + FinalizerDataType* data) { + return New(env, + callback, + Object(), + resourceName, + maxQueueSize, + initialThreadCount, + finalizeCallback, + data); +} + +// static +template +inline ThreadSafeFunction ThreadSafeFunction::New(napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback) { + return New(env, + callback, + Object(), + resourceName, + maxQueueSize, + initialThreadCount, + context, + finalizeCallback); +} + +// static +template +inline ThreadSafeFunction ThreadSafeFunction::New(napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data) { + return New(env, + callback, + Object(), + resourceName, + maxQueueSize, + initialThreadCount, + context, + finalizeCallback, + data); +} + +// static +template +inline ThreadSafeFunction ThreadSafeFunction::New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount) { + return New(env, + callback, + resource, + resourceName, + maxQueueSize, + initialThreadCount, + static_cast(nullptr) /* context */); +} + +// static +template +inline ThreadSafeFunction ThreadSafeFunction::New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context) { + return New(env, + callback, + resource, + resourceName, + maxQueueSize, + initialThreadCount, + context, + [](Env, ContextType*) {} /* empty finalizer */); +} + +// static +template +inline ThreadSafeFunction ThreadSafeFunction::New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + Finalizer finalizeCallback) { + return New(env, + callback, + resource, + resourceName, + maxQueueSize, + initialThreadCount, + static_cast(nullptr) /* context */, + finalizeCallback, + static_cast(nullptr) /* data */, + details::ThreadSafeFinalize::Wrapper); +} + +// static +template +inline ThreadSafeFunction ThreadSafeFunction::New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + Finalizer finalizeCallback, + FinalizerDataType* data) { + return New(env, + callback, + resource, + resourceName, + maxQueueSize, + initialThreadCount, + static_cast(nullptr) /* context */, + finalizeCallback, + data, + details::ThreadSafeFinalize:: + FinalizeWrapperWithData); +} + +// static +template +inline ThreadSafeFunction ThreadSafeFunction::New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback) { + return New( + env, + callback, + resource, + resourceName, + maxQueueSize, + initialThreadCount, + context, + finalizeCallback, + static_cast(nullptr) /* data */, + details::ThreadSafeFinalize::FinalizeWrapperWithContext); +} + +// static +template +inline ThreadSafeFunction ThreadSafeFunction::New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data) { + return New( + env, + callback, + resource, + resourceName, + maxQueueSize, + initialThreadCount, + context, + finalizeCallback, + data, + details::ThreadSafeFinalize:: + FinalizeFinalizeWrapperWithDataAndContext); +} + +inline ThreadSafeFunction::ThreadSafeFunction() : _tsfn() {} + +inline ThreadSafeFunction::ThreadSafeFunction(napi_threadsafe_function tsfn) + : _tsfn(tsfn) {} + +inline ThreadSafeFunction::operator napi_threadsafe_function() const { + return _tsfn; +} + +inline napi_status ThreadSafeFunction::BlockingCall() const { + return CallInternal(nullptr, napi_tsfn_blocking); +} + +template <> +inline napi_status ThreadSafeFunction::BlockingCall(void* data) const { + return napi_call_threadsafe_function(_tsfn, data, napi_tsfn_blocking); +} + +template +inline napi_status ThreadSafeFunction::BlockingCall(Callback callback) const { + return CallInternal(new CallbackWrapper(callback), napi_tsfn_blocking); +} + +template +inline napi_status ThreadSafeFunction::BlockingCall(DataType* data, + Callback callback) const { + auto wrapper = [data, callback](Env env, Function jsCallback) { + callback(env, jsCallback, data); + }; + return CallInternal(new CallbackWrapper(wrapper), napi_tsfn_blocking); +} + +inline napi_status ThreadSafeFunction::NonBlockingCall() const { + return CallInternal(nullptr, napi_tsfn_nonblocking); +} + +template <> +inline napi_status ThreadSafeFunction::NonBlockingCall(void* data) const { + return napi_call_threadsafe_function(_tsfn, data, napi_tsfn_nonblocking); +} + +template +inline napi_status ThreadSafeFunction::NonBlockingCall( + Callback callback) const { + return CallInternal(new CallbackWrapper(callback), napi_tsfn_nonblocking); +} + +template +inline napi_status ThreadSafeFunction::NonBlockingCall( + DataType* data, Callback callback) const { + auto wrapper = [data, callback](Env env, Function jsCallback) { + callback(env, jsCallback, data); + }; + return CallInternal(new CallbackWrapper(wrapper), napi_tsfn_nonblocking); +} + +inline void ThreadSafeFunction::Ref(napi_env env) const { + if (_tsfn != nullptr) { + napi_status status = napi_ref_threadsafe_function(env, _tsfn); + NAPI_THROW_IF_FAILED_VOID(env, status); + } +} + +inline void ThreadSafeFunction::Unref(napi_env env) const { + if (_tsfn != nullptr) { + napi_status status = napi_unref_threadsafe_function(env, _tsfn); + NAPI_THROW_IF_FAILED_VOID(env, status); + } +} + +inline napi_status ThreadSafeFunction::Acquire() const { + return napi_acquire_threadsafe_function(_tsfn); +} + +inline napi_status ThreadSafeFunction::Release() const { + return napi_release_threadsafe_function(_tsfn, napi_tsfn_release); +} + +inline napi_status ThreadSafeFunction::Abort() const { + return napi_release_threadsafe_function(_tsfn, napi_tsfn_abort); +} + +inline ThreadSafeFunction::ConvertibleContext ThreadSafeFunction::GetContext() + const { + void* context; + napi_status status = napi_get_threadsafe_function_context(_tsfn, &context); + NAPI_FATAL_IF_FAILED(status, + "ThreadSafeFunction::GetContext", + "napi_get_threadsafe_function_context"); + return ConvertibleContext({context}); +} + +// static +template +inline ThreadSafeFunction ThreadSafeFunction::New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data, + napi_finalize wrapper) { + static_assert(details::can_make_string::value || + std::is_convertible::value, + "Resource name should be convertible to the string type"); + + ThreadSafeFunction tsfn; + auto* finalizeData = new details:: + ThreadSafeFinalize( + {data, finalizeCallback}); + napi_status status = + napi_create_threadsafe_function(env, + callback, + resource, + Value::From(env, resourceName), + maxQueueSize, + initialThreadCount, + finalizeData, + wrapper, + context, + CallJS, + &tsfn._tsfn); + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED(env, status, ThreadSafeFunction()); + } + + return tsfn; +} + +inline napi_status ThreadSafeFunction::CallInternal( + CallbackWrapper* callbackWrapper, + napi_threadsafe_function_call_mode mode) const { + napi_status status = + napi_call_threadsafe_function(_tsfn, callbackWrapper, mode); + if (status != napi_ok && callbackWrapper != nullptr) { + delete callbackWrapper; + } + + return status; +} + +// static +inline void ThreadSafeFunction::CallJS(napi_env env, + napi_value jsCallback, + void* /* context */, + void* data) { + if (env == nullptr && jsCallback == nullptr) { + return; + } + + details::WrapVoidCallback(env, [&]() { + if (data != nullptr) { + auto* callbackWrapper = static_cast(data); + (*callbackWrapper)(env, Function(env, jsCallback)); + delete callbackWrapper; + } else if (jsCallback != nullptr) { + Function(env, jsCallback).Call({}); + } + }); +} + +//////////////////////////////////////////////////////////////////////////////// +// Async Progress Worker Base class +//////////////////////////////////////////////////////////////////////////////// +template +inline AsyncProgressWorkerBase::AsyncProgressWorkerBase( + const Object& receiver, + const Function& callback, + const char* resource_name, + const Object& resource, + size_t queue_size) + : AsyncWorker(receiver, callback, resource_name, resource) { + // Fill all possible arguments to work around ambiguous + // ThreadSafeFunction::New signatures. + _tsfn = ThreadSafeFunction::New(callback.Env(), + callback, + resource, + resource_name, + queue_size, + /** initialThreadCount */ 1, + /** context */ this, + OnThreadSafeFunctionFinalize, + /** finalizeData */ this); +} + +#if NAPI_VERSION > 4 +template +inline AsyncProgressWorkerBase::AsyncProgressWorkerBase( + Napi::Env env, + const char* resource_name, + const Object& resource, + size_t queue_size) + : AsyncWorker(env, resource_name, resource) { + // TODO: Once the changes to make the callback optional for threadsafe + // functions are available on all versions we can remove the dummy Function + // here. + Function callback; + // Fill all possible arguments to work around ambiguous + // ThreadSafeFunction::New signatures. + _tsfn = ThreadSafeFunction::New(env, + callback, + resource, + resource_name, + queue_size, + /** initialThreadCount */ 1, + /** context */ this, + OnThreadSafeFunctionFinalize, + /** finalizeData */ this); +} +#endif + +template +inline AsyncProgressWorkerBase::~AsyncProgressWorkerBase() { + // Abort pending tsfn call. + // Don't send progress events after we've already completed. + // It's ok to call ThreadSafeFunction::Abort and ThreadSafeFunction::Release + // duplicated. + _tsfn.Abort(); +} + +template +inline void AsyncProgressWorkerBase::OnAsyncWorkProgress( + Napi::Env /* env */, Napi::Function /* jsCallback */, void* data) { + ThreadSafeData* tsd = static_cast(data); + tsd->asyncprogressworker()->OnWorkProgress(tsd->data()); + delete tsd; +} + +template +inline napi_status AsyncProgressWorkerBase::NonBlockingCall( + DataType* data) { + auto tsd = new AsyncProgressWorkerBase::ThreadSafeData(this, data); + auto ret = _tsfn.NonBlockingCall(tsd, OnAsyncWorkProgress); + if (ret != napi_ok) { + delete tsd; + } + return ret; +} + +template +inline void AsyncProgressWorkerBase::OnWorkComplete( + Napi::Env /* env */, napi_status status) { + _work_completed = true; + _complete_status = status; + _tsfn.Release(); +} + +template +inline void AsyncProgressWorkerBase::OnThreadSafeFunctionFinalize( + Napi::Env env, void* /* data */, AsyncProgressWorkerBase* context) { + if (context->_work_completed) { + context->AsyncWorker::OnWorkComplete(env, context->_complete_status); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Async Progress Worker class +//////////////////////////////////////////////////////////////////////////////// +template +inline AsyncProgressWorker::AsyncProgressWorker(const Function& callback) + : AsyncProgressWorker(callback, "generic") {} + +template +inline AsyncProgressWorker::AsyncProgressWorker(const Function& callback, + const char* resource_name) + : AsyncProgressWorker( + callback, resource_name, Object::New(callback.Env())) {} + +template +inline AsyncProgressWorker::AsyncProgressWorker(const Function& callback, + const char* resource_name, + const Object& resource) + : AsyncProgressWorker( + Object::New(callback.Env()), callback, resource_name, resource) {} + +template +inline AsyncProgressWorker::AsyncProgressWorker(const Object& receiver, + const Function& callback) + : AsyncProgressWorker(receiver, callback, "generic") {} + +template +inline AsyncProgressWorker::AsyncProgressWorker(const Object& receiver, + const Function& callback, + const char* resource_name) + : AsyncProgressWorker( + receiver, callback, resource_name, Object::New(callback.Env())) {} + +template +inline AsyncProgressWorker::AsyncProgressWorker(const Object& receiver, + const Function& callback, + const char* resource_name, + const Object& resource) + : AsyncProgressWorkerBase(receiver, callback, resource_name, resource), + _asyncdata(nullptr), + _asyncsize(0), + _signaled(false) {} + +#if NAPI_VERSION > 4 +template +inline AsyncProgressWorker::AsyncProgressWorker(Napi::Env env) + : AsyncProgressWorker(env, "generic") {} + +template +inline AsyncProgressWorker::AsyncProgressWorker(Napi::Env env, + const char* resource_name) + : AsyncProgressWorker(env, resource_name, Object::New(env)) {} + +template +inline AsyncProgressWorker::AsyncProgressWorker(Napi::Env env, + const char* resource_name, + const Object& resource) + : AsyncProgressWorkerBase(env, resource_name, resource), + _asyncdata(nullptr), + _asyncsize(0) {} +#endif + +template +inline AsyncProgressWorker::~AsyncProgressWorker() { + { + std::lock_guard lock(this->_mutex); + _asyncdata = nullptr; + _asyncsize = 0; + } +} + +template +inline void AsyncProgressWorker::Execute() { + ExecutionProgress progress(this); + Execute(progress); +} + +template +inline void AsyncProgressWorker::OnWorkProgress(void*) { + T* data; + size_t size; + bool signaled; + { + std::lock_guard lock(this->_mutex); + data = this->_asyncdata; + size = this->_asyncsize; + signaled = this->_signaled; + this->_asyncdata = nullptr; + this->_asyncsize = 0; + this->_signaled = false; + } + + /** + * The callback of ThreadSafeFunction is not been invoked immediately on the + * callback of uv_async_t (uv io poll), rather the callback of TSFN is + * invoked on the right next uv idle callback. There are chances that during + * the deferring the signal of uv_async_t is been sent again, i.e. potential + * not coalesced two calls of the TSFN callback. + */ + if (data == nullptr && !signaled) { + return; + } + + this->OnProgress(data, size); + delete[] data; +} + +template +inline void AsyncProgressWorker::SendProgress_(const T* data, size_t count) { + T* new_data = new T[count]; + std::copy(data, data + count, new_data); + + T* old_data; + { + std::lock_guard lock(this->_mutex); + old_data = _asyncdata; + _asyncdata = new_data; + _asyncsize = count; + _signaled = false; + } + this->NonBlockingCall(nullptr); + + delete[] old_data; +} + +template +inline void AsyncProgressWorker::Signal() { + { + std::lock_guard lock(this->_mutex); + _signaled = true; + } + this->NonBlockingCall(static_cast(nullptr)); +} + +template +inline void AsyncProgressWorker::ExecutionProgress::Signal() const { + this->_worker->Signal(); +} + +template +inline void AsyncProgressWorker::ExecutionProgress::Send( + const T* data, size_t count) const { + _worker->SendProgress_(data, count); +} + +//////////////////////////////////////////////////////////////////////////////// +// Async Progress Queue Worker class +//////////////////////////////////////////////////////////////////////////////// +template +inline AsyncProgressQueueWorker::AsyncProgressQueueWorker( + const Function& callback) + : AsyncProgressQueueWorker(callback, "generic") {} + +template +inline AsyncProgressQueueWorker::AsyncProgressQueueWorker( + const Function& callback, const char* resource_name) + : AsyncProgressQueueWorker( + callback, resource_name, Object::New(callback.Env())) {} + +template +inline AsyncProgressQueueWorker::AsyncProgressQueueWorker( + const Function& callback, const char* resource_name, const Object& resource) + : AsyncProgressQueueWorker( + Object::New(callback.Env()), callback, resource_name, resource) {} + +template +inline AsyncProgressQueueWorker::AsyncProgressQueueWorker( + const Object& receiver, const Function& callback) + : AsyncProgressQueueWorker(receiver, callback, "generic") {} + +template +inline AsyncProgressQueueWorker::AsyncProgressQueueWorker( + const Object& receiver, const Function& callback, const char* resource_name) + : AsyncProgressQueueWorker( + receiver, callback, resource_name, Object::New(callback.Env())) {} + +template +inline AsyncProgressQueueWorker::AsyncProgressQueueWorker( + const Object& receiver, + const Function& callback, + const char* resource_name, + const Object& resource) + : AsyncProgressWorkerBase>( + receiver, + callback, + resource_name, + resource, + /** unlimited queue size */ 0) {} + +#if NAPI_VERSION > 4 +template +inline AsyncProgressQueueWorker::AsyncProgressQueueWorker(Napi::Env env) + : AsyncProgressQueueWorker(env, "generic") {} + +template +inline AsyncProgressQueueWorker::AsyncProgressQueueWorker( + Napi::Env env, const char* resource_name) + : AsyncProgressQueueWorker(env, resource_name, Object::New(env)) {} + +template +inline AsyncProgressQueueWorker::AsyncProgressQueueWorker( + Napi::Env env, const char* resource_name, const Object& resource) + : AsyncProgressWorkerBase>( + env, resource_name, resource, /** unlimited queue size */ 0) {} +#endif + +template +inline void AsyncProgressQueueWorker::Execute() { + ExecutionProgress progress(this); + Execute(progress); +} + +template +inline void AsyncProgressQueueWorker::OnWorkProgress( + std::pair* datapair) { + if (datapair == nullptr) { + return; + } + + T* data = datapair->first; + size_t size = datapair->second; + + this->OnProgress(data, size); + delete datapair; + delete[] data; +} + +template +inline void AsyncProgressQueueWorker::SendProgress_(const T* data, + size_t count) { + T* new_data = new T[count]; + std::copy(data, data + count, new_data); + + auto pair = new std::pair(new_data, count); + this->NonBlockingCall(pair); +} + +template +inline void AsyncProgressQueueWorker::Signal() const { + this->SendProgress_(static_cast(nullptr), 0); +} + +template +inline void AsyncProgressQueueWorker::OnWorkComplete(Napi::Env env, + napi_status status) { + // Draining queued items in TSFN. + AsyncProgressWorkerBase>::OnWorkComplete(env, status); +} + +template +inline void AsyncProgressQueueWorker::ExecutionProgress::Signal() const { + _worker->SendProgress_(static_cast(nullptr), 0); +} + +template +inline void AsyncProgressQueueWorker::ExecutionProgress::Send( + const T* data, size_t count) const { + _worker->SendProgress_(data, count); +} +#endif // NAPI_VERSION > 3 && NAPI_HAS_THREADS + +//////////////////////////////////////////////////////////////////////////////// +// Memory Management class +//////////////////////////////////////////////////////////////////////////////// + +inline int64_t MemoryManagement::AdjustExternalMemory(BasicEnv env, + int64_t change_in_bytes) { + int64_t result; + napi_status status = + napi_adjust_external_memory(env, change_in_bytes, &result); + NAPI_FATAL_IF_FAILED(status, + "MemoryManagement::AdjustExternalMemory", + "napi_adjust_external_memory"); + return result; +} + +//////////////////////////////////////////////////////////////////////////////// +// Version Management class +//////////////////////////////////////////////////////////////////////////////// + +inline uint32_t VersionManagement::GetNapiVersion(BasicEnv env) { + uint32_t result; + napi_status status = napi_get_version(env, &result); + NAPI_FATAL_IF_FAILED( + status, "VersionManagement::GetNapiVersion", "napi_get_version"); + return result; +} + +inline const napi_node_version* VersionManagement::GetNodeVersion( + BasicEnv env) { + const napi_node_version* result; + napi_status status = napi_get_node_version(env, &result); + NAPI_FATAL_IF_FAILED( + status, "VersionManagement::GetNodeVersion", "napi_get_node_version"); + return result; +} + +#if NAPI_VERSION > 5 +//////////////////////////////////////////////////////////////////////////////// +// Addon class +//////////////////////////////////////////////////////////////////////////////// + +template +inline Object Addon::Init(Env env, Object exports) { + T* addon = new T(env, exports); + env.SetInstanceData(addon); + return addon->entry_point_; +} + +template +inline T* Addon::Unwrap(Object wrapper) { + return wrapper.Env().GetInstanceData(); +} + +template +inline void Addon::DefineAddon( + Object exports, const std::initializer_list& props) { + DefineProperties(exports, props); + entry_point_ = exports; +} + +template +inline Napi::Object Addon::DefineProperties( + Object object, const std::initializer_list& props) { + const napi_property_descriptor* properties = + reinterpret_cast(props.begin()); + size_t size = props.size(); + napi_status status = + napi_define_properties(object.Env(), object, size, properties); + NAPI_THROW_IF_FAILED(object.Env(), status, object); + for (size_t idx = 0; idx < size; idx++) + T::AttachPropData(object.Env(), object, &properties[idx]); + return object; +} +#endif // NAPI_VERSION > 5 + +#if NAPI_VERSION > 2 +template +Env::CleanupHook BasicEnv::AddCleanupHook(Hook hook, Arg* arg) { + return CleanupHook(*this, hook, arg); +} + +template +Env::CleanupHook BasicEnv::AddCleanupHook(Hook hook) { + return CleanupHook(*this, hook); +} + +template +Env::CleanupHook::CleanupHook() { + data = nullptr; +} + +template +Env::CleanupHook::CleanupHook(Napi::BasicEnv env, Hook hook) + : wrapper(Env::CleanupHook::Wrapper) { + data = new CleanupData{std::move(hook), nullptr}; + napi_status status = napi_add_env_cleanup_hook(env, wrapper, data); + if (status != napi_ok) { + delete data; + data = nullptr; + } +} + +template +Env::CleanupHook::CleanupHook(Napi::BasicEnv env, + Hook hook, + Arg* arg) + : wrapper(Env::CleanupHook::WrapperWithArg) { + data = new CleanupData{std::move(hook), arg}; + napi_status status = napi_add_env_cleanup_hook(env, wrapper, data); + if (status != napi_ok) { + delete data; + data = nullptr; + } +} + +template +bool Env::CleanupHook::Remove(BasicEnv env) { + napi_status status = napi_remove_env_cleanup_hook(env, wrapper, data); + delete data; + data = nullptr; + return status == napi_ok; +} + +template +bool Env::CleanupHook::IsEmpty() const { + return data == nullptr; +} +#endif // NAPI_VERSION > 2 + +#ifdef NODE_API_EXPERIMENTAL_HAS_POST_FINALIZER +template +inline void BasicEnv::PostFinalizer(FinalizerType finalizeCallback) const { + using T = void*; + details::FinalizeData* finalizeData = + new details::FinalizeData( + {std::move(finalizeCallback), nullptr}); + + napi_status status = node_api_post_finalizer( + _env, + details::FinalizeData::WrapperGCWithoutData, + static_cast(nullptr), + finalizeData); + if (status != napi_ok) { + delete finalizeData; + NAPI_FATAL_IF_FAILED( + status, "BasicEnv::PostFinalizer", "invalid arguments"); + } +} + +template +inline void BasicEnv::PostFinalizer(FinalizerType finalizeCallback, + T* data) const { + details::FinalizeData* finalizeData = + new details::FinalizeData( + {std::move(finalizeCallback), nullptr}); + + napi_status status = node_api_post_finalizer( + _env, + details::FinalizeData::WrapperGC, + data, + finalizeData); + if (status != napi_ok) { + delete finalizeData; + NAPI_FATAL_IF_FAILED( + status, "BasicEnv::PostFinalizer", "invalid arguments"); + } +} + +template +inline void BasicEnv::PostFinalizer(FinalizerType finalizeCallback, + T* data, + Hint* finalizeHint) const { + details::FinalizeData* finalizeData = + new details::FinalizeData( + {std::move(finalizeCallback), finalizeHint}); + napi_status status = node_api_post_finalizer( + _env, + details::FinalizeData::WrapperGCWithHint, + data, + finalizeData); + if (status != napi_ok) { + delete finalizeData; + NAPI_FATAL_IF_FAILED( + status, "BasicEnv::PostFinalizer", "invalid arguments"); + } +} +#endif // NODE_API_EXPERIMENTAL_HAS_POST_FINALIZER + +#ifdef NAPI_CPP_CUSTOM_NAMESPACE +} // namespace NAPI_CPP_CUSTOM_NAMESPACE +#endif + +} // namespace Napi + +#undef NAPI_NO_SANITIZE_VPTR + +#endif // SRC_NAPI_INL_H_ diff --git a/node_modules/bcrypt/node_modules/node-addon-api/napi.h b/node_modules/bcrypt/node_modules/node-addon-api/napi.h new file mode 100644 index 0000000..013a911 --- /dev/null +++ b/node_modules/bcrypt/node_modules/node-addon-api/napi.h @@ -0,0 +1,3327 @@ +#ifndef SRC_NAPI_H_ +#define SRC_NAPI_H_ + +#ifndef NAPI_HAS_THREADS +#if !defined(__wasm__) || (defined(__EMSCRIPTEN_PTHREADS__) || \ + (defined(__wasi__) && defined(_REENTRANT))) +#define NAPI_HAS_THREADS 1 +#else +#define NAPI_HAS_THREADS 0 +#endif +#endif + +#include +#include +#include +#include +#if NAPI_HAS_THREADS +#include +#endif // NAPI_HAS_THREADS +#include +#include + +// VS2015 RTM has bugs with constexpr, so require min of VS2015 Update 3 (known +// good version) +#if !defined(_MSC_VER) || _MSC_FULL_VER >= 190024210 +#define NAPI_HAS_CONSTEXPR 1 +#endif + +// VS2013 does not support char16_t literal strings, so we'll work around it +// using wchar_t strings and casting them. This is safe as long as the character +// sizes are the same. +#if defined(_MSC_VER) && _MSC_VER <= 1800 +static_assert(sizeof(char16_t) == sizeof(wchar_t), + "Size mismatch between char16_t and wchar_t"); +#define NAPI_WIDE_TEXT(x) reinterpret_cast(L##x) +#else +#define NAPI_WIDE_TEXT(x) u##x +#endif + +// Backwards-compatibility to handle the rename of this macro definition, in +// case they are used within userland code. +#ifdef NAPI_CPP_EXCEPTIONS +#define NODE_ADDON_API_CPP_EXCEPTIONS +#endif +#if defined(NODE_ADDON_API_CPP_EXCEPTIONS) && !defined(NAPI_CPP_EXCEPTIONS) +#define NAPI_CPP_EXCEPTIONS +#endif +#ifdef NAPI_DISABLE_CPP_EXCEPTIONS +#define NODE_ADDON_API_DISABLE_CPP_EXCEPTIONS +#endif +#if defined(NODE_ADDON_API_DISABLE_CPP_EXCEPTIONS) && \ + !defined(NAPI_DISABLE_CPP_EXCEPTIONS) +#define NAPI_DISABLE_CPP_EXCEPTIONS +#endif + +// If C++ exceptions are not explicitly enabled or disabled, enable them +// if exceptions were enabled in the compiler settings. +#if !defined(NODE_ADDON_API_CPP_EXCEPTIONS) && \ + !defined(NODE_ADDON_API_DISABLE_CPP_EXCEPTIONS) +#if defined(_CPPUNWIND) || defined(__EXCEPTIONS) +#define NODE_ADDON_API_CPP_EXCEPTIONS +#else +#error Exception support not detected. \ + Define either NODE_ADDON_API_CPP_EXCEPTIONS or NODE_ADDON_API_DISABLE_CPP_EXCEPTIONS. +#endif +#endif + +// If C++ NODE_ADDON_API_CPP_EXCEPTIONS are enabled, NODE_ADDON_API_ENABLE_MAYBE +// should not be set +#if defined(NODE_ADDON_API_CPP_EXCEPTIONS) && \ + defined(NODE_ADDON_API_ENABLE_MAYBE) +#error NODE_ADDON_API_ENABLE_MAYBE should not be set when \ + NODE_ADDON_API_CPP_EXCEPTIONS is defined. +#endif + +#ifdef _NOEXCEPT +#define NAPI_NOEXCEPT _NOEXCEPT +#else +#define NAPI_NOEXCEPT noexcept +#endif + +#ifdef NODE_ADDON_API_CPP_EXCEPTIONS + +// When C++ exceptions are enabled, Errors are thrown directly. There is no need +// to return anything after the throw statements. The variadic parameter is an +// optional return value that is ignored. +// We need _VOID versions of the macros to avoid warnings resulting from +// leaving the NAPI_THROW_* `...` argument empty. + +#define NAPI_THROW(e, ...) throw e +#define NAPI_THROW_VOID(e) throw e + +#define NAPI_THROW_IF_FAILED(env, status, ...) \ + if ((status) != napi_ok) throw Napi::Error::New(env); + +#define NAPI_THROW_IF_FAILED_VOID(env, status) \ + if ((status) != napi_ok) throw Napi::Error::New(env); + +#else // NODE_ADDON_API_CPP_EXCEPTIONS + +// When C++ exceptions are disabled, Errors are thrown as JavaScript exceptions, +// which are pending until the callback returns to JS. The variadic parameter +// is an optional return value; usually it is an empty result. +// We need _VOID versions of the macros to avoid warnings resulting from +// leaving the NAPI_THROW_* `...` argument empty. + +#define NAPI_THROW(e, ...) \ + do { \ + (e).ThrowAsJavaScriptException(); \ + return __VA_ARGS__; \ + } while (0) + +#define NAPI_THROW_VOID(e) \ + do { \ + (e).ThrowAsJavaScriptException(); \ + return; \ + } while (0) + +#define NAPI_THROW_IF_FAILED(env, status, ...) \ + if ((status) != napi_ok) { \ + Napi::Error::New(env).ThrowAsJavaScriptException(); \ + return __VA_ARGS__; \ + } + +#define NAPI_THROW_IF_FAILED_VOID(env, status) \ + if ((status) != napi_ok) { \ + Napi::Error::New(env).ThrowAsJavaScriptException(); \ + return; \ + } + +#endif // NODE_ADDON_API_CPP_EXCEPTIONS + +#ifdef NODE_ADDON_API_ENABLE_MAYBE +#define NAPI_MAYBE_THROW_IF_FAILED(env, status, type) \ + NAPI_THROW_IF_FAILED(env, status, Napi::Nothing()) + +#define NAPI_RETURN_OR_THROW_IF_FAILED(env, status, result, type) \ + NAPI_MAYBE_THROW_IF_FAILED(env, status, type); \ + return Napi::Just(result); +#else +#define NAPI_MAYBE_THROW_IF_FAILED(env, status, type) \ + NAPI_THROW_IF_FAILED(env, status, type()) + +#define NAPI_RETURN_OR_THROW_IF_FAILED(env, status, result, type) \ + NAPI_MAYBE_THROW_IF_FAILED(env, status, type); \ + return result; +#endif + +#define NAPI_DISALLOW_ASSIGN(CLASS) void operator=(const CLASS&) = delete; +#define NAPI_DISALLOW_COPY(CLASS) CLASS(const CLASS&) = delete; + +#define NAPI_DISALLOW_ASSIGN_COPY(CLASS) \ + NAPI_DISALLOW_ASSIGN(CLASS) \ + NAPI_DISALLOW_COPY(CLASS) + +#define NAPI_CHECK(condition, location, message) \ + do { \ + if (!(condition)) { \ + Napi::Error::Fatal((location), (message)); \ + } \ + } while (0) + +// Internal check helper. Be careful that the formatted message length should be +// max 255 size and null terminated. +#define NAPI_INTERNAL_CHECK(expr, location, ...) \ + do { \ + if (!(expr)) { \ + std::string msg = Napi::details::StringFormat(__VA_ARGS__); \ + Napi::Error::Fatal(location, msg.c_str()); \ + } \ + } while (0) + +#define NAPI_INTERNAL_CHECK_EQ(actual, expected, value_format, location) \ + do { \ + auto actual_value = (actual); \ + NAPI_INTERNAL_CHECK(actual_value == (expected), \ + location, \ + "Expected " #actual " to be equal to " #expected \ + ", but got " value_format ".", \ + actual_value); \ + } while (0) + +#define NAPI_FATAL_IF_FAILED(status, location, message) \ + NAPI_CHECK((status) == napi_ok, location, message) + +//////////////////////////////////////////////////////////////////////////////// +/// Node-API C++ Wrapper Classes +/// +/// These classes wrap the "Node-API" ABI-stable C APIs for Node.js, providing a +/// C++ object model and C++ exception-handling semantics with low overhead. +/// The wrappers are all header-only so that they do not affect the ABI. +//////////////////////////////////////////////////////////////////////////////// +namespace Napi { + +#ifdef NAPI_CPP_CUSTOM_NAMESPACE +// NAPI_CPP_CUSTOM_NAMESPACE can be #define'd per-addon to avoid symbol +// conflicts between different instances of node-addon-api + +// First dummy definition of the namespace to make sure that Napi::(name) still +// refers to the right things inside this file. +namespace NAPI_CPP_CUSTOM_NAMESPACE {} +using namespace NAPI_CPP_CUSTOM_NAMESPACE; + +namespace NAPI_CPP_CUSTOM_NAMESPACE { +#endif + +// Forward declarations +class Env; +class Value; +class Boolean; +class Number; +#if NAPI_VERSION > 5 +class BigInt; +#endif // NAPI_VERSION > 5 +#if (NAPI_VERSION > 4) +class Date; +#endif +class String; +class Object; +class Array; +class ArrayBuffer; +class Function; +class Error; +class PropertyDescriptor; +class CallbackInfo; +class TypedArray; +template +class TypedArrayOf; + +using Int8Array = + TypedArrayOf; ///< Typed-array of signed 8-bit integers +using Uint8Array = + TypedArrayOf; ///< Typed-array of unsigned 8-bit integers +using Int16Array = + TypedArrayOf; ///< Typed-array of signed 16-bit integers +using Uint16Array = + TypedArrayOf; ///< Typed-array of unsigned 16-bit integers +using Int32Array = + TypedArrayOf; ///< Typed-array of signed 32-bit integers +using Uint32Array = + TypedArrayOf; ///< Typed-array of unsigned 32-bit integers +using Float32Array = + TypedArrayOf; ///< Typed-array of 32-bit floating-point values +using Float64Array = + TypedArrayOf; ///< Typed-array of 64-bit floating-point values +#if NAPI_VERSION > 5 +using BigInt64Array = + TypedArrayOf; ///< Typed array of signed 64-bit integers +using BigUint64Array = + TypedArrayOf; ///< Typed array of unsigned 64-bit integers +#endif // NAPI_VERSION > 5 + +/// Defines the signature of a Node-API C++ module's registration callback +/// (init) function. +using ModuleRegisterCallback = Object (*)(Env env, Object exports); + +class MemoryManagement; + +/// A simple Maybe type, representing an object which may or may not have a +/// value. +/// +/// If an API method returns a Maybe<>, the API method can potentially fail +/// either because an exception is thrown, or because an exception is pending, +/// e.g. because a previous API call threw an exception that hasn't been +/// caught yet. In that case, a "Nothing" value is returned. +template +class Maybe { + public: + bool IsNothing() const; + bool IsJust() const; + + /// Short-hand for Unwrap(), which doesn't return a value. Could be used + /// where the actual value of the Maybe is not needed like Object::Set. + /// If this Maybe is nothing (empty), node-addon-api will crash the + /// process. + void Check() const; + + /// Return the value of type T contained in the Maybe. If this Maybe is + /// nothing (empty), node-addon-api will crash the process. + T Unwrap() const; + + /// Return the value of type T contained in the Maybe, or using a default + /// value if this Maybe is nothing (empty). + T UnwrapOr(const T& default_value) const; + + /// Converts this Maybe to a value of type T in the out. If this Maybe is + /// nothing (empty), `false` is returned and `out` is left untouched. + bool UnwrapTo(T* out) const; + + bool operator==(const Maybe& other) const; + bool operator!=(const Maybe& other) const; + + private: + Maybe(); + explicit Maybe(const T& t); + + bool _has_value; + T _value; + + template + friend Maybe Nothing(); + template + friend Maybe Just(const U& u); +}; + +template +inline Maybe Nothing(); + +template +inline Maybe Just(const T& t); + +#if defined(NODE_ADDON_API_ENABLE_MAYBE) +template +using MaybeOrValue = Maybe; +#else +template +using MaybeOrValue = T; +#endif + +#ifdef NODE_API_EXPERIMENTAL_HAS_POST_FINALIZER +using node_addon_api_basic_env = node_api_nogc_env; +using node_addon_api_basic_finalize = node_api_nogc_finalize; +#else +using node_addon_api_basic_env = napi_env; +using node_addon_api_basic_finalize = napi_finalize; +#endif + +/// Environment for Node-API values and operations. +/// +/// All Node-API values and operations must be associated with an environment. +/// An environment instance is always provided to callback functions; that +/// environment must then be used for any creation of Node-API values or other +/// Node-API operations within the callback. (Many methods infer the +/// environment from the `this` instance that the method is called on.) +/// +/// Multiple environments may co-exist in a single process or a thread. +/// +/// In the V8 JavaScript engine, a Node-API environment approximately +/// corresponds to an Isolate. +class BasicEnv { + private: + node_addon_api_basic_env _env; +#if NAPI_VERSION > 5 + template + static void DefaultFini(Env, T* data); + template + static void DefaultFiniWithHint(Env, DataType* data, HintType* hint); +#endif // NAPI_VERSION > 5 + public: + BasicEnv(node_addon_api_basic_env env); + + operator node_addon_api_basic_env() const; + + // Without these operator overloads, the error: + // + // Use of overloaded operator '==' is ambiguous (with operand types + // 'Napi::Env' and 'Napi::Env') + // + // ... occurs when comparing foo.Env() == bar.Env() or foo.Env() == nullptr + bool operator==(const BasicEnv& other) const { + return _env == other._env; + }; + bool operator==(std::nullptr_t /*other*/) const { + return _env == nullptr; + }; + +#if NAPI_VERSION > 2 + template + class CleanupHook; + + template + CleanupHook AddCleanupHook(Hook hook); + + template + CleanupHook AddCleanupHook(Hook hook, Arg* arg); +#endif // NAPI_VERSION > 2 + +#if NAPI_VERSION > 5 + template + T* GetInstanceData() const; + + template + using Finalizer = void (*)(Env, T*); + template fini = BasicEnv::DefaultFini> + void SetInstanceData(T* data) const; + + template + using FinalizerWithHint = void (*)(Env, DataType*, HintType*); + template fini = + BasicEnv::DefaultFiniWithHint> + void SetInstanceData(DataType* data, HintType* hint) const; +#endif // NAPI_VERSION > 5 + +#if NAPI_VERSION > 2 + template + class CleanupHook { + public: + CleanupHook(); + CleanupHook(BasicEnv env, Hook hook, Arg* arg); + CleanupHook(BasicEnv env, Hook hook); + bool Remove(BasicEnv env); + bool IsEmpty() const; + + private: + static inline void Wrapper(void* data) NAPI_NOEXCEPT; + static inline void WrapperWithArg(void* data) NAPI_NOEXCEPT; + + void (*wrapper)(void* arg); + struct CleanupData { + Hook hook; + Arg* arg; + } * data; + }; +#endif // NAPI_VERSION > 2 + +#if NAPI_VERSION > 8 + const char* GetModuleFileName() const; +#endif // NAPI_VERSION > 8 + +#ifdef NODE_API_EXPERIMENTAL_HAS_POST_FINALIZER + template + inline void PostFinalizer(FinalizerType finalizeCallback) const; + + template + inline void PostFinalizer(FinalizerType finalizeCallback, T* data) const; + + template + inline void PostFinalizer(FinalizerType finalizeCallback, + T* data, + Hint* finalizeHint) const; +#endif // NODE_API_EXPERIMENTAL_HAS_POST_FINALIZER + + friend class Env; +}; + +class Env : public BasicEnv { + public: + Env(napi_env env); + + operator napi_env() const; + + Object Global() const; + Value Undefined() const; + Value Null() const; + + bool IsExceptionPending() const; + Error GetAndClearPendingException() const; + + MaybeOrValue RunScript(const char* utf8script) const; + MaybeOrValue RunScript(const std::string& utf8script) const; + MaybeOrValue RunScript(String script) const; +}; + +/// A JavaScript value of unknown type. +/// +/// For type-specific operations, convert to one of the Value subclasses using a +/// `To*` or `As()` method. The `To*` methods do type coercion; the `As()` +/// method does not. +/// +/// Napi::Value value = ... +/// if (!value.IsString()) throw Napi::TypeError::New(env, "Invalid +/// arg..."); Napi::String str = value.As(); // Cast to a +/// string value +/// +/// Napi::Value anotherValue = ... +/// bool isTruthy = anotherValue.ToBoolean(); // Coerce to a boolean value +class Value { + public: + Value(); ///< Creates a new _empty_ Value instance. + Value(napi_env env, + napi_value value); ///< Wraps a Node-API value primitive. + + /// Creates a JS value from a C++ primitive. + /// + /// `value` may be any of: + /// - bool + /// - Any integer type + /// - Any floating point type + /// - const char* (encoded using UTF-8, null-terminated) + /// - const char16_t* (encoded using UTF-16-LE, null-terminated) + /// - std::string (encoded using UTF-8) + /// - std::u16string + /// - napi::Value + /// - napi_value + template + static Value From(napi_env env, const T& value); + + static void CheckCast(napi_env env, napi_value value); + + /// Converts to a Node-API value primitive. + /// + /// If the instance is _empty_, this returns `nullptr`. + operator napi_value() const; + + /// Tests if this value strictly equals another value. + bool operator==(const Value& other) const; + + /// Tests if this value does not strictly equal another value. + bool operator!=(const Value& other) const; + + /// Tests if this value strictly equals another value. + bool StrictEquals(const Value& other) const; + + /// Gets the environment the value is associated with. + Napi::Env Env() const; + + /// Checks if the value is empty (uninitialized). + /// + /// An empty value is invalid, and most attempts to perform an operation on an + /// empty value will result in an exception. Note an empty value is distinct + /// from JavaScript `null` or `undefined`, which are valid values. + /// + /// When C++ exceptions are disabled at compile time, a method with a `Value` + /// return type may return an empty value to indicate a pending exception. So + /// when not using C++ exceptions, callers should check whether the value is + /// empty before attempting to use it. + bool IsEmpty() const; + + napi_valuetype Type() const; ///< Gets the type of the value. + + bool IsUndefined() + const; ///< Tests if a value is an undefined JavaScript value. + bool IsNull() const; ///< Tests if a value is a null JavaScript value. + bool IsBoolean() const; ///< Tests if a value is a JavaScript boolean. + bool IsNumber() const; ///< Tests if a value is a JavaScript number. +#if NAPI_VERSION > 5 + bool IsBigInt() const; ///< Tests if a value is a JavaScript bigint. +#endif // NAPI_VERSION > 5 +#if (NAPI_VERSION > 4) + bool IsDate() const; ///< Tests if a value is a JavaScript date. +#endif + bool IsString() const; ///< Tests if a value is a JavaScript string. + bool IsSymbol() const; ///< Tests if a value is a JavaScript symbol. + bool IsArray() const; ///< Tests if a value is a JavaScript array. + bool IsArrayBuffer() + const; ///< Tests if a value is a JavaScript array buffer. + bool IsTypedArray() const; ///< Tests if a value is a JavaScript typed array. + bool IsObject() const; ///< Tests if a value is a JavaScript object. + bool IsFunction() const; ///< Tests if a value is a JavaScript function. + bool IsPromise() const; ///< Tests if a value is a JavaScript promise. + bool IsDataView() const; ///< Tests if a value is a JavaScript data view. + bool IsBuffer() const; ///< Tests if a value is a Node buffer. + bool IsExternal() const; ///< Tests if a value is a pointer to external data. +#ifdef NODE_API_EXPERIMENTAL_HAS_SHAREDARRAYBUFFER + bool IsSharedArrayBuffer() const; +#endif + + /// Casts to another type of `Napi::Value`, when the actual type is known or + /// assumed. + /// + /// This conversion does NOT coerce the type. Calling any methods + /// inappropriate for the actual value type will throw `Napi::Error`. + /// + /// If `NODE_ADDON_API_ENABLE_TYPE_CHECK_ON_AS` is defined, this method + /// asserts that the actual type is the expected type. + template + T As() const; + + // Unsafe Value::As(), should be avoided. + template + T UnsafeAs() const; + + MaybeOrValue ToBoolean() + const; ///< Coerces a value to a JavaScript boolean. + MaybeOrValue ToNumber() + const; ///< Coerces a value to a JavaScript number. + MaybeOrValue ToString() + const; ///< Coerces a value to a JavaScript string. + MaybeOrValue ToObject() + const; ///< Coerces a value to a JavaScript object. + + protected: + /// !cond INTERNAL + napi_env _env; + napi_value _value; + /// !endcond +}; + +/// A JavaScript boolean value. +class Boolean : public Value { + public: + static Boolean New(napi_env env, ///< Node-API environment + bool value ///< Boolean value + ); + + static void CheckCast(napi_env env, napi_value value); + + Boolean(); ///< Creates a new _empty_ Boolean instance. + Boolean(napi_env env, + napi_value value); ///< Wraps a Node-API value primitive. + + operator bool() const; ///< Converts a Boolean value to a boolean primitive. + bool Value() const; ///< Converts a Boolean value to a boolean primitive. +}; + +/// A JavaScript number value. +class Number : public Value { + public: + static Number New(napi_env env, ///< Node-API environment + double value ///< Number value + ); + + static void CheckCast(napi_env env, napi_value value); + + Number(); ///< Creates a new _empty_ Number instance. + Number(napi_env env, + napi_value value); ///< Wraps a Node-API value primitive. + + operator int32_t() + const; ///< Converts a Number value to a 32-bit signed integer value. + operator uint32_t() + const; ///< Converts a Number value to a 32-bit unsigned integer value. + operator int64_t() + const; ///< Converts a Number value to a 64-bit signed integer value. + operator float() + const; ///< Converts a Number value to a 32-bit floating-point value. + operator double() + const; ///< Converts a Number value to a 64-bit floating-point value. + + int32_t Int32Value() + const; ///< Converts a Number value to a 32-bit signed integer value. + uint32_t Uint32Value() + const; ///< Converts a Number value to a 32-bit unsigned integer value. + int64_t Int64Value() + const; ///< Converts a Number value to a 64-bit signed integer value. + float FloatValue() + const; ///< Converts a Number value to a 32-bit floating-point value. + double DoubleValue() + const; ///< Converts a Number value to a 64-bit floating-point value. +}; + +#if NAPI_VERSION > 5 +/// A JavaScript bigint value. +class BigInt : public Value { + public: + static BigInt New(napi_env env, ///< Node-API environment + int64_t value ///< Number value + ); + static BigInt New(napi_env env, ///< Node-API environment + uint64_t value ///< Number value + ); + + /// Creates a new BigInt object using a specified sign bit and a + /// specified list of digits/words. + /// The resulting number is calculated as: + /// (-1)^sign_bit * (words[0] * (2^64)^0 + words[1] * (2^64)^1 + ...) + static BigInt New(napi_env env, ///< Node-API environment + int sign_bit, ///< Sign bit. 1 if negative. + size_t word_count, ///< Number of words in array + const uint64_t* words ///< Array of words + ); + + static void CheckCast(napi_env env, napi_value value); + + BigInt(); ///< Creates a new _empty_ BigInt instance. + BigInt(napi_env env, + napi_value value); ///< Wraps a Node-API value primitive. + + int64_t Int64Value(bool* lossless) + const; ///< Converts a BigInt value to a 64-bit signed integer value. + uint64_t Uint64Value(bool* lossless) + const; ///< Converts a BigInt value to a 64-bit unsigned integer value. + + size_t WordCount() const; ///< The number of 64-bit words needed to store + ///< the result of ToWords(). + + /// Writes the contents of this BigInt to a specified memory location. + /// `sign_bit` must be provided and will be set to 1 if this BigInt is + /// negative. + /// `*word_count` has to be initialized to the length of the `words` array. + /// Upon return, it will be set to the actual number of words that would + /// be needed to store this BigInt (i.e. the return value of `WordCount()`). + void ToWords(int* sign_bit, size_t* word_count, uint64_t* words); +}; +#endif // NAPI_VERSION > 5 + +#if (NAPI_VERSION > 4) +/// A JavaScript date value. +class Date : public Value { + public: + /// Creates a new Date value from a double primitive. + static Date New(napi_env env, ///< Node-API environment + double value ///< Number value + ); + + static void CheckCast(napi_env env, napi_value value); + + Date(); ///< Creates a new _empty_ Date instance. + Date(napi_env env, napi_value value); ///< Wraps a Node-API value primitive. + operator double() const; ///< Converts a Date value to double primitive + + double ValueOf() const; ///< Converts a Date value to a double primitive. +}; +#endif + +/// A JavaScript string or symbol value (that can be used as a property name). +class Name : public Value { + public: + static void CheckCast(napi_env env, napi_value value); + + Name(); ///< Creates a new _empty_ Name instance. + Name(napi_env env, + napi_value value); ///< Wraps a Node-API value primitive. +}; + +/// A JavaScript string value. +class String : public Name { + public: + /// Creates a new String value from a UTF-8 encoded C++ string. + static String New(napi_env env, ///< Node-API environment + const std::string& value ///< UTF-8 encoded C++ string + ); + + /// Creates a new String value from a UTF-16 encoded C++ string. + static String New(napi_env env, ///< Node-API environment + const std::u16string& value ///< UTF-16 encoded C++ string + ); + + /// Creates a new String value from a UTF-8 encoded C string. + static String New( + napi_env env, ///< Node-API environment + const char* value ///< UTF-8 encoded null-terminated C string + ); + + /// Creates a new String value from a UTF-16 encoded C string. + static String New( + napi_env env, ///< Node-API environment + const char16_t* value ///< UTF-16 encoded null-terminated C string + ); + + /// Creates a new String value from a UTF-8 encoded C string with specified + /// length. + static String New(napi_env env, ///< Node-API environment + const char* value, ///< UTF-8 encoded C string (not + ///< necessarily null-terminated) + size_t length ///< length of the string in bytes + ); + + /// Creates a new String value from a UTF-16 encoded C string with specified + /// length. + static String New( + napi_env env, ///< Node-API environment + const char16_t* value, ///< UTF-16 encoded C string (not necessarily + ///< null-terminated) + size_t length ///< Length of the string in 2-byte code units + ); + + /// Creates a new String based on the original object's type. + /// + /// `value` may be any of: + /// - const char* (encoded using UTF-8, null-terminated) + /// - const char16_t* (encoded using UTF-16-LE, null-terminated) + /// - std::string (encoded using UTF-8) + /// - std::u16string + template + static String From(napi_env env, const T& value); + + static void CheckCast(napi_env env, napi_value value); + + String(); ///< Creates a new _empty_ String instance. + String(napi_env env, + napi_value value); ///< Wraps a Node-API value primitive. + + operator std::string() + const; ///< Converts a String value to a UTF-8 encoded C++ string. + operator std::u16string() + const; ///< Converts a String value to a UTF-16 encoded C++ string. + std::string Utf8Value() + const; ///< Converts a String value to a UTF-8 encoded C++ string. + std::u16string Utf16Value() + const; ///< Converts a String value to a UTF-16 encoded C++ string. +}; + +/// A JavaScript symbol value. +class Symbol : public Name { + public: + /// Creates a new Symbol value with an optional description. + static Symbol New( + napi_env env, ///< Node-API environment + const char* description = + nullptr ///< Optional UTF-8 encoded null-terminated C string + /// describing the symbol + ); + + /// Creates a new Symbol value with a description. + static Symbol New( + napi_env env, ///< Node-API environment + const std::string& + description ///< UTF-8 encoded C++ string describing the symbol + ); + + /// Creates a new Symbol value with a description. + static Symbol New(napi_env env, ///< Node-API environment + String description ///< String value describing the symbol + ); + + /// Creates a new Symbol value with a description. + static Symbol New( + napi_env env, ///< Node-API environment + napi_value description ///< String value describing the symbol + ); + + /// Get a public Symbol (e.g. Symbol.iterator). + static MaybeOrValue WellKnown(napi_env, const std::string& name); + + // Create a symbol in the global registry, UTF-8 Encoded cpp string + static MaybeOrValue For(napi_env env, const std::string& description); + + // Create a symbol in the global registry, C style string (null terminated) + static MaybeOrValue For(napi_env env, const char* description); + + // Create a symbol in the global registry, String value describing the symbol + static MaybeOrValue For(napi_env env, String description); + + // Create a symbol in the global registry, napi_value describing the symbol + static MaybeOrValue For(napi_env env, napi_value description); + + static void CheckCast(napi_env env, napi_value value); + + Symbol(); ///< Creates a new _empty_ Symbol instance. + Symbol(napi_env env, + napi_value value); ///< Wraps a Node-API value primitive. +}; + +class TypeTaggable : public Value { + public: +#if NAPI_VERSION >= 8 + void TypeTag(const napi_type_tag* type_tag) const; + bool CheckTypeTag(const napi_type_tag* type_tag) const; +#endif // NAPI_VERSION >= 8 + protected: + TypeTaggable(); + TypeTaggable(napi_env env, napi_value value); +}; + +/// A JavaScript object value. +class Object : public TypeTaggable { + public: + /// Enables property and element assignments using indexing syntax. + /// + /// This is a convenient helper to get and set object properties. As + /// getting and setting object properties may throw with JavaScript + /// exceptions, it is notable that these operations may fail. + /// When NODE_ADDON_API_ENABLE_MAYBE is defined, the process will abort + /// on JavaScript exceptions. + /// + /// Example: + /// + /// Napi::Value propertyValue = object1['A']; + /// object2['A'] = propertyValue; + /// Napi::Value elementValue = array[0]; + /// array[1] = elementValue; + template + class PropertyLValue { + public: + /// Converts an L-value to a value. + operator Value() const; + + /// Assigns a value to the property. The type of value can be + /// anything supported by `Object::Set`. + template + PropertyLValue& operator=(ValueType value); + + /// Converts an L-value to a value. For convenience. + Value AsValue() const; + + private: + PropertyLValue() = delete; + PropertyLValue(Object object, Key key); + napi_env _env; + napi_value _object; + Key _key; + + friend class Napi::Object; + }; + + /// Creates a new Object value. + static Object New(napi_env env ///< Node-API environment + ); + + static void CheckCast(napi_env env, napi_value value); + + Object(); ///< Creates a new _empty_ Object instance. + Object(napi_env env, + napi_value value); ///< Wraps a Node-API value primitive. + + /// Gets or sets a named property. + PropertyLValue operator[]( + const char* utf8name ///< UTF-8 encoded null-terminated property name + ); + + /// Gets or sets a named property. + PropertyLValue operator[]( + const std::string& utf8name ///< UTF-8 encoded property name + ); + + /// Gets or sets an indexed property or array element. + PropertyLValue operator[]( + uint32_t index /// Property / element index + ); + + /// Gets or sets an indexed property or array element. + PropertyLValue operator[](Value index /// Property / element index + ) const; + + /// Gets a named property. + MaybeOrValue operator[]( + const char* utf8name ///< UTF-8 encoded null-terminated property name + ) const; + + /// Gets a named property. + MaybeOrValue operator[]( + const std::string& utf8name ///< UTF-8 encoded property name + ) const; + + /// Gets an indexed property or array element. + MaybeOrValue operator[](uint32_t index ///< Property / element index + ) const; + + /// Checks whether a property is present. + MaybeOrValue Has(napi_value key ///< Property key primitive + ) const; + + /// Checks whether a property is present. + MaybeOrValue Has(Value key ///< Property key + ) const; + + /// Checks whether a named property is present. + MaybeOrValue Has( + const char* utf8name ///< UTF-8 encoded null-terminated property name + ) const; + + /// Checks whether a named property is present. + MaybeOrValue Has( + const std::string& utf8name ///< UTF-8 encoded property name + ) const; + + /// Checks whether a own property is present. + MaybeOrValue HasOwnProperty(napi_value key ///< Property key primitive + ) const; + + /// Checks whether a own property is present. + MaybeOrValue HasOwnProperty(Value key ///< Property key + ) const; + + /// Checks whether a own property is present. + MaybeOrValue HasOwnProperty( + const char* utf8name ///< UTF-8 encoded null-terminated property name + ) const; + + /// Checks whether a own property is present. + MaybeOrValue HasOwnProperty( + const std::string& utf8name ///< UTF-8 encoded property name + ) const; + + /// Gets a property. + MaybeOrValue Get(napi_value key ///< Property key primitive + ) const; + + /// Gets a property. + MaybeOrValue Get(Value key ///< Property key + ) const; + + /// Gets a named property. + MaybeOrValue Get( + const char* utf8name ///< UTF-8 encoded null-terminated property name + ) const; + + /// Gets a named property. + MaybeOrValue Get( + const std::string& utf8name ///< UTF-8 encoded property name + ) const; + + /// Sets a property. + template + MaybeOrValue Set(napi_value key, ///< Property key primitive + const ValueType& value ///< Property value primitive + ) const; + + /// Sets a property. + template + MaybeOrValue Set(Value key, ///< Property key + const ValueType& value ///< Property value + ) const; + + /// Sets a named property. + template + MaybeOrValue Set( + const char* utf8name, ///< UTF-8 encoded null-terminated property name + const ValueType& value) const; + + /// Sets a named property. + template + MaybeOrValue Set( + const std::string& utf8name, ///< UTF-8 encoded property name + const ValueType& value ///< Property value primitive + ) const; + + /// Delete property. + MaybeOrValue Delete(napi_value key ///< Property key primitive + ) const; + + /// Delete property. + MaybeOrValue Delete(Value key ///< Property key + ) const; + + /// Delete property. + MaybeOrValue Delete( + const char* utf8name ///< UTF-8 encoded null-terminated property name + ) const; + + /// Delete property. + MaybeOrValue Delete( + const std::string& utf8name ///< UTF-8 encoded property name + ) const; + + /// Checks whether an indexed property is present. + MaybeOrValue Has(uint32_t index ///< Property / element index + ) const; + + /// Gets an indexed property or array element. + MaybeOrValue Get(uint32_t index ///< Property / element index + ) const; + + /// Sets an indexed property or array element. + template + MaybeOrValue Set(uint32_t index, ///< Property / element index + const ValueType& value ///< Property value primitive + ) const; + + /// Deletes an indexed property or array element. + MaybeOrValue Delete(uint32_t index ///< Property / element index + ) const; + + /// This operation can fail in case of Proxy.[[OwnPropertyKeys]] and + /// Proxy.[[GetOwnProperty]] calling into JavaScript. See: + /// - + /// https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-ownpropertykeys + /// - + /// https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-getownproperty-p + MaybeOrValue GetPropertyNames() const; ///< Get all property names + + /// Defines a property on the object. + /// + /// This operation can fail in case of Proxy.[[DefineOwnProperty]] calling + /// into JavaScript. See + /// https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-defineownproperty-p-desc + MaybeOrValue DefineProperty( + const PropertyDescriptor& + property ///< Descriptor for the property to be defined + ) const; + + /// Defines properties on the object. + /// + /// This operation can fail in case of Proxy.[[DefineOwnProperty]] calling + /// into JavaScript. See + /// https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-defineownproperty-p-desc + MaybeOrValue DefineProperties( + const std::initializer_list& properties + ///< List of descriptors for the properties to be defined + ) const; + + /// Defines properties on the object. + /// + /// This operation can fail in case of Proxy.[[DefineOwnProperty]] calling + /// into JavaScript. See + /// https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-defineownproperty-p-desc + MaybeOrValue DefineProperties( + const std::vector& properties + ///< Vector of descriptors for the properties to be defined + ) const; + + /// Checks if an object is an instance created by a constructor function. + /// + /// This is equivalent to the JavaScript `instanceof` operator. + /// + /// This operation can fail in case of Proxy.[[GetPrototypeOf]] calling into + /// JavaScript. + /// See + /// https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-getprototypeof + MaybeOrValue InstanceOf( + const Function& constructor ///< Constructor function + ) const; + + template + inline void AddFinalizer(Finalizer finalizeCallback, T* data) const; + + template + inline void AddFinalizer(Finalizer finalizeCallback, + T* data, + Hint* finalizeHint) const; + +#ifdef NODE_ADDON_API_CPP_EXCEPTIONS + class const_iterator; + + inline const_iterator begin() const; + + inline const_iterator end() const; + + class iterator; + + inline iterator begin(); + + inline iterator end(); +#endif // NODE_ADDON_API_CPP_EXCEPTIONS + +#if NAPI_VERSION >= 8 + /// This operation can fail in case of Proxy.[[GetPrototypeOf]] calling into + /// JavaScript. + /// See + /// https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-getprototypeof + MaybeOrValue Freeze() const; + /// This operation can fail in case of Proxy.[[GetPrototypeOf]] calling into + /// JavaScript. + /// See + /// https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-getprototypeof + MaybeOrValue Seal() const; +#endif // NAPI_VERSION >= 8 +}; + +template +class External : public TypeTaggable { + public: + static External New(napi_env env, T* data); + + // Finalizer must implement `void operator()(Env env, T* data)`. + template + static External New(napi_env env, T* data, Finalizer finalizeCallback); + // Finalizer must implement `void operator()(Env env, T* data, Hint* hint)`. + template + static External New(napi_env env, + T* data, + Finalizer finalizeCallback, + Hint* finalizeHint); + + static void CheckCast(napi_env env, napi_value value); + + External(); + External(napi_env env, napi_value value); + + T* Data() const; +}; + +class Array : public Object { + public: + static Array New(napi_env env); + static Array New(napi_env env, size_t length); + + static void CheckCast(napi_env env, napi_value value); + + Array(); + Array(napi_env env, napi_value value); + + uint32_t Length() const; +}; + +#ifdef NODE_ADDON_API_CPP_EXCEPTIONS +class Object::const_iterator { + private: + enum class Type { BEGIN, END }; + + inline const_iterator(const Object* object, const Type type); + + public: + inline const_iterator& operator++(); + + inline bool operator==(const const_iterator& other) const; + + inline bool operator!=(const const_iterator& other) const; + + inline const std::pair> operator*() + const; + + private: + const Napi::Object* _object; + Array _keys; + uint32_t _index; + + friend class Object; +}; + +class Object::iterator { + private: + enum class Type { BEGIN, END }; + + inline iterator(Object* object, const Type type); + + public: + inline iterator& operator++(); + + inline bool operator==(const iterator& other) const; + + inline bool operator!=(const iterator& other) const; + + inline std::pair> operator*(); + + private: + Napi::Object* _object; + Array _keys; + uint32_t _index; + + friend class Object; +}; +#endif // NODE_ADDON_API_CPP_EXCEPTIONS + +#ifdef NODE_API_EXPERIMENTAL_HAS_SHAREDARRAYBUFFER +class SharedArrayBuffer : public Object { + public: + SharedArrayBuffer(); + SharedArrayBuffer(napi_env env, napi_value value); + + static SharedArrayBuffer New(napi_env env, size_t byteLength); + + static void CheckCast(napi_env env, napi_value value); + + void* Data(); + size_t ByteLength(); +}; +#endif + +/// A JavaScript array buffer value. +class ArrayBuffer : public Object { + public: + /// Creates a new ArrayBuffer instance over a new automatically-allocated + /// buffer. + static ArrayBuffer New( + napi_env env, ///< Node-API environment + size_t byteLength ///< Length of the buffer to be allocated, in bytes + ); + +#ifndef NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED + /// Creates a new ArrayBuffer instance, using an external buffer with + /// specified byte length. + static ArrayBuffer New( + napi_env env, ///< Node-API environment + void* externalData, ///< Pointer to the external buffer to be used by + ///< the array + size_t byteLength ///< Length of the external buffer to be used by the + ///< array, in bytes + ); + + /// Creates a new ArrayBuffer instance, using an external buffer with + /// specified byte length. + template + static ArrayBuffer New( + napi_env env, ///< Node-API environment + void* externalData, ///< Pointer to the external buffer to be used by + ///< the array + size_t byteLength, ///< Length of the external buffer to be used by the + ///< array, + /// in bytes + Finalizer finalizeCallback ///< Function to be called when the array + ///< buffer is destroyed; + /// must implement `void operator()(Env env, + /// void* externalData)` + ); + + /// Creates a new ArrayBuffer instance, using an external buffer with + /// specified byte length. + template + static ArrayBuffer New( + napi_env env, ///< Node-API environment + void* externalData, ///< Pointer to the external buffer to be used by + ///< the array + size_t byteLength, ///< Length of the external buffer to be used by the + ///< array, + /// in bytes + Finalizer finalizeCallback, ///< Function to be called when the array + ///< buffer is destroyed; + /// must implement `void operator()(Env + /// env, void* externalData, Hint* hint)` + Hint* finalizeHint ///< Hint (second parameter) to be passed to the + ///< finalize callback + ); +#endif // NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED + + static void CheckCast(napi_env env, napi_value value); + + ArrayBuffer(); ///< Creates a new _empty_ ArrayBuffer instance. + ArrayBuffer(napi_env env, + napi_value value); ///< Wraps a Node-API value primitive. + + void* Data(); ///< Gets a pointer to the data buffer. + size_t ByteLength(); ///< Gets the length of the array buffer in bytes. + +#if NAPI_VERSION >= 7 + bool IsDetached() const; + void Detach(); +#endif // NAPI_VERSION >= 7 +}; + +/// A JavaScript typed-array value with unknown array type. +/// +/// For type-specific operations, cast to a `TypedArrayOf` instance using the +/// `As()` method: +/// +/// Napi::TypedArray array = ... +/// if (t.TypedArrayType() == napi_int32_array) { +/// Napi::Int32Array int32Array = t.As(); +/// } +class TypedArray : public Object { + public: + static void CheckCast(napi_env env, napi_value value); + + TypedArray(); ///< Creates a new _empty_ TypedArray instance. + TypedArray(napi_env env, + napi_value value); ///< Wraps a Node-API value primitive. + + napi_typedarray_type TypedArrayType() + const; ///< Gets the type of this typed-array. + Napi::ArrayBuffer ArrayBuffer() const; ///< Gets the backing array buffer. + + uint8_t ElementSize() + const; ///< Gets the size in bytes of one element in the array. + size_t ElementLength() const; ///< Gets the number of elements in the array. + size_t ByteOffset() + const; ///< Gets the offset into the buffer where the array starts. + size_t ByteLength() const; ///< Gets the length of the array in bytes. + + protected: + /// !cond INTERNAL + napi_typedarray_type _type; + size_t _length; + + TypedArray(napi_env env, + napi_value value, + napi_typedarray_type type, + size_t length); + + template + static +#if defined(NAPI_HAS_CONSTEXPR) + constexpr +#endif + napi_typedarray_type + TypedArrayTypeForPrimitiveType() { + return std::is_same::value ? napi_int8_array + : std::is_same::value ? napi_uint8_array + : std::is_same::value ? napi_int16_array + : std::is_same::value ? napi_uint16_array + : std::is_same::value ? napi_int32_array + : std::is_same::value ? napi_uint32_array + : std::is_same::value ? napi_float32_array + : std::is_same::value ? napi_float64_array +#if NAPI_VERSION > 5 + : std::is_same::value ? napi_bigint64_array + : std::is_same::value ? napi_biguint64_array +#endif // NAPI_VERSION > 5 + : napi_int8_array; + } + /// !endcond +}; + +/// A JavaScript typed-array value with known array type. +/// +/// Note while it is possible to create and access Uint8 "clamped" arrays using +/// this class, the _clamping_ behavior is only applied in JavaScript. +template +class TypedArrayOf : public TypedArray { + public: + /// Creates a new TypedArray instance over a new automatically-allocated array + /// buffer. + /// + /// The array type parameter can normally be omitted (because it is inferred + /// from the template parameter T), except when creating a "clamped" array: + /// + /// Uint8Array::New(env, length, napi_uint8_clamped_array) + static TypedArrayOf New( + napi_env env, ///< Node-API environment + size_t elementLength, ///< Length of the created array, as a number of + ///< elements +#if defined(NAPI_HAS_CONSTEXPR) + napi_typedarray_type type = + TypedArray::TypedArrayTypeForPrimitiveType() +#else + napi_typedarray_type type +#endif + ///< Type of array, if different from the default array type for the + ///< template parameter T. + ); + + /// Creates a new TypedArray instance over a provided array buffer. + /// + /// The array type parameter can normally be omitted (because it is inferred + /// from the template parameter T), except when creating a "clamped" array: + /// + /// Uint8Array::New(env, length, buffer, 0, napi_uint8_clamped_array) + static TypedArrayOf New( + napi_env env, ///< Node-API environment + size_t elementLength, ///< Length of the created array, as a number of + ///< elements + Napi::ArrayBuffer arrayBuffer, ///< Backing array buffer instance to use + size_t bufferOffset, ///< Offset into the array buffer where the + ///< typed-array starts +#if defined(NAPI_HAS_CONSTEXPR) + napi_typedarray_type type = + TypedArray::TypedArrayTypeForPrimitiveType() +#else + napi_typedarray_type type +#endif + ///< Type of array, if different from the default array type for the + ///< template parameter T. + ); + + static void CheckCast(napi_env env, napi_value value); + + TypedArrayOf(); ///< Creates a new _empty_ TypedArrayOf instance. + TypedArrayOf(napi_env env, + napi_value value); ///< Wraps a Node-API value primitive. + + T& operator[](size_t index); ///< Gets or sets an element in the array. + const T& operator[](size_t index) const; ///< Gets an element in the array. + + /// Gets a pointer to the array's backing buffer. + /// + /// This is not necessarily the same as the `ArrayBuffer::Data()` pointer, + /// because the typed-array may have a non-zero `ByteOffset()` into the + /// `ArrayBuffer`. + T* Data(); + + /// Gets a pointer to the array's backing buffer. + /// + /// This is not necessarily the same as the `ArrayBuffer::Data()` pointer, + /// because the typed-array may have a non-zero `ByteOffset()` into the + /// `ArrayBuffer`. + const T* Data() const; + + private: + T* _data; + + TypedArrayOf(napi_env env, + napi_value value, + napi_typedarray_type type, + size_t length, + T* data); +}; + +/// The DataView provides a low-level interface for reading/writing multiple +/// number types in an ArrayBuffer irrespective of the platform's endianness. +class DataView : public Object { + public: + static DataView New(napi_env env, Napi::ArrayBuffer arrayBuffer); + static DataView New(napi_env env, + Napi::ArrayBuffer arrayBuffer, + size_t byteOffset); + static DataView New(napi_env env, + Napi::ArrayBuffer arrayBuffer, + size_t byteOffset, + size_t byteLength); + + static void CheckCast(napi_env env, napi_value value); + + DataView(); ///< Creates a new _empty_ DataView instance. + DataView(napi_env env, + napi_value value); ///< Wraps a Node-API value primitive. + + Napi::ArrayBuffer ArrayBuffer() const; ///< Gets the backing array buffer. + size_t ByteOffset() + const; ///< Gets the offset into the buffer where the array starts. + size_t ByteLength() const; ///< Gets the length of the array in bytes. + + void* Data() const; + + float GetFloat32(size_t byteOffset) const; + double GetFloat64(size_t byteOffset) const; + int8_t GetInt8(size_t byteOffset) const; + int16_t GetInt16(size_t byteOffset) const; + int32_t GetInt32(size_t byteOffset) const; + uint8_t GetUint8(size_t byteOffset) const; + uint16_t GetUint16(size_t byteOffset) const; + uint32_t GetUint32(size_t byteOffset) const; + + void SetFloat32(size_t byteOffset, float value) const; + void SetFloat64(size_t byteOffset, double value) const; + void SetInt8(size_t byteOffset, int8_t value) const; + void SetInt16(size_t byteOffset, int16_t value) const; + void SetInt32(size_t byteOffset, int32_t value) const; + void SetUint8(size_t byteOffset, uint8_t value) const; + void SetUint16(size_t byteOffset, uint16_t value) const; + void SetUint32(size_t byteOffset, uint32_t value) const; + + private: + template + T ReadData(size_t byteOffset) const; + + template + void WriteData(size_t byteOffset, T value) const; + + void* _data{}; + size_t _length{}; +}; + +class Function : public Object { + public: + using VoidCallback = void (*)(const CallbackInfo& info); + using Callback = Value (*)(const CallbackInfo& info); + + template + static Function New(napi_env env, + const char* utf8name = nullptr, + void* data = nullptr); + + template + static Function New(napi_env env, + const char* utf8name = nullptr, + void* data = nullptr); + + template + static Function New(napi_env env, + const std::string& utf8name, + void* data = nullptr); + + template + static Function New(napi_env env, + const std::string& utf8name, + void* data = nullptr); + + /// Callable must implement operator() accepting a const CallbackInfo& + /// and return either void or Value. + template + static Function New(napi_env env, + Callable cb, + const char* utf8name = nullptr, + void* data = nullptr); + /// Callable must implement operator() accepting a const CallbackInfo& + /// and return either void or Value. + template + static Function New(napi_env env, + Callable cb, + const std::string& utf8name, + void* data = nullptr); + + static void CheckCast(napi_env env, napi_value value); + + Function(); + Function(napi_env env, napi_value value); + + MaybeOrValue operator()( + const std::initializer_list& args) const; + + MaybeOrValue Call(const std::initializer_list& args) const; + MaybeOrValue Call(const std::vector& args) const; + MaybeOrValue Call(const std::vector& args) const; + MaybeOrValue Call(size_t argc, const napi_value* args) const; + MaybeOrValue Call(napi_value recv, + const std::initializer_list& args) const; + MaybeOrValue Call(napi_value recv, + const std::vector& args) const; + MaybeOrValue Call(napi_value recv, + const std::vector& args) const; + MaybeOrValue Call(napi_value recv, + size_t argc, + const napi_value* args) const; + + MaybeOrValue MakeCallback( + napi_value recv, + const std::initializer_list& args, + napi_async_context context = nullptr) const; + MaybeOrValue MakeCallback(napi_value recv, + const std::vector& args, + napi_async_context context = nullptr) const; + MaybeOrValue MakeCallback(napi_value recv, + size_t argc, + const napi_value* args, + napi_async_context context = nullptr) const; + + MaybeOrValue New(const std::initializer_list& args) const; + MaybeOrValue New(const std::vector& args) const; + MaybeOrValue New(size_t argc, const napi_value* args) const; +}; + +class Promise : public Object { + public: + class Deferred { + public: + static Deferred New(napi_env env); + Deferred(napi_env env); + + Napi::Promise Promise() const; + Napi::Env Env() const; + + void Resolve(napi_value value) const; + void Reject(napi_value value) const; + + private: + napi_env _env; + napi_deferred _deferred; + napi_value _promise; + }; + + static void CheckCast(napi_env env, napi_value value); + + Promise(); + Promise(napi_env env, napi_value value); + + MaybeOrValue Then(napi_value onFulfilled) const; + MaybeOrValue Then(napi_value onFulfilled, + napi_value onRejected) const; + MaybeOrValue Catch(napi_value onRejected) const; + + MaybeOrValue Then(const Function& onFulfilled) const; + MaybeOrValue Then(const Function& onFulfilled, + const Function& onRejected) const; + MaybeOrValue Catch(const Function& onRejected) const; +}; + +template +class Buffer : public Uint8Array { + public: + static Buffer New(napi_env env, size_t length); +#ifndef NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED + static Buffer New(napi_env env, T* data, size_t length); + + // Finalizer must implement `void operator()(Env env, T* data)`. + template + static Buffer New(napi_env env, + T* data, + size_t length, + Finalizer finalizeCallback); + // Finalizer must implement `void operator()(Env env, T* data, Hint* hint)`. + template + static Buffer New(napi_env env, + T* data, + size_t length, + Finalizer finalizeCallback, + Hint* finalizeHint); +#endif // NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED + + static Buffer NewOrCopy(napi_env env, T* data, size_t length); + // Finalizer must implement `void operator()(Env env, T* data)`. + template + static Buffer NewOrCopy(napi_env env, + T* data, + size_t length, + Finalizer finalizeCallback); + // Finalizer must implement `void operator()(Env env, T* data, Hint* hint)`. + template + static Buffer NewOrCopy(napi_env env, + T* data, + size_t length, + Finalizer finalizeCallback, + Hint* finalizeHint); + + static Buffer Copy(napi_env env, const T* data, size_t length); + + static void CheckCast(napi_env env, napi_value value); + + Buffer(); + Buffer(napi_env env, napi_value value); + size_t Length() const; + T* Data() const; + + private: +}; + +/// Holds a counted reference to a value; initially a weak reference unless +/// otherwise specified, may be changed to/from a strong reference by adjusting +/// the refcount. +/// +/// The referenced value is not immediately destroyed when the reference count +/// is zero; it is merely then eligible for garbage-collection if there are no +/// other references to the value. +template +class Reference { + public: + static Reference New(const T& value, uint32_t initialRefcount = 0); + + Reference(); + Reference(napi_env env, napi_ref ref); + ~Reference(); + + // A reference can be moved but cannot be copied. + Reference(Reference&& other); + Reference& operator=(Reference&& other); + NAPI_DISALLOW_ASSIGN(Reference) + + operator napi_ref() const; + bool operator==(const Reference& other) const; + bool operator!=(const Reference& other) const; + + Napi::Env Env() const; + bool IsEmpty() const; + + // Note when getting the value of a Reference it is usually correct to do so + // within a HandleScope so that the value handle gets cleaned up efficiently. + T Value() const; + + uint32_t Ref() const; + uint32_t Unref() const; + void Reset(); + void Reset(const T& value, uint32_t refcount = 0); + + // Call this on a reference that is declared as static data, to prevent its + // destructor from running at program shutdown time, which would attempt to + // reset the reference when the environment is no longer valid. Avoid using + // this if at all possible. If you do need to use static data, MAKE SURE to + // warn your users that your addon is NOT threadsafe. + void SuppressDestruct(); + + protected: + Reference(const Reference&); + + /// !cond INTERNAL + napi_env _env; + napi_ref _ref; + /// !endcond + + private: + bool _suppressDestruct; +}; + +class ObjectReference : public Reference { + public: + ObjectReference(); + ObjectReference(napi_env env, napi_ref ref); + + // A reference can be moved but cannot be copied. + ObjectReference(Reference&& other); + ObjectReference& operator=(Reference&& other); + ObjectReference(ObjectReference&& other); + ObjectReference& operator=(ObjectReference&& other); + NAPI_DISALLOW_ASSIGN(ObjectReference) + + MaybeOrValue Get(const char* utf8name) const; + MaybeOrValue Get(const std::string& utf8name) const; + MaybeOrValue Set(const char* utf8name, napi_value value) const; + MaybeOrValue Set(const char* utf8name, Napi::Value value) const; + MaybeOrValue Set(const char* utf8name, const char* utf8value) const; + MaybeOrValue Set(const char* utf8name, bool boolValue) const; + MaybeOrValue Set(const char* utf8name, double numberValue) const; + MaybeOrValue Set(const std::string& utf8name, napi_value value) const; + MaybeOrValue Set(const std::string& utf8name, Napi::Value value) const; + MaybeOrValue Set(const std::string& utf8name, + std::string& utf8value) const; + MaybeOrValue Set(const std::string& utf8name, bool boolValue) const; + MaybeOrValue Set(const std::string& utf8name, double numberValue) const; + + MaybeOrValue Get(uint32_t index) const; + MaybeOrValue Set(uint32_t index, const napi_value value) const; + MaybeOrValue Set(uint32_t index, const Napi::Value value) const; + MaybeOrValue Set(uint32_t index, const char* utf8value) const; + MaybeOrValue Set(uint32_t index, const std::string& utf8value) const; + MaybeOrValue Set(uint32_t index, bool boolValue) const; + MaybeOrValue Set(uint32_t index, double numberValue) const; + + protected: + ObjectReference(const ObjectReference&); +}; + +class FunctionReference : public Reference { + public: + FunctionReference(); + FunctionReference(napi_env env, napi_ref ref); + + // A reference can be moved but cannot be copied. + FunctionReference(Reference&& other); + FunctionReference& operator=(Reference&& other); + FunctionReference(FunctionReference&& other); + FunctionReference& operator=(FunctionReference&& other); + NAPI_DISALLOW_ASSIGN_COPY(FunctionReference) + + MaybeOrValue operator()( + const std::initializer_list& args) const; + + MaybeOrValue Call( + const std::initializer_list& args) const; + MaybeOrValue Call(const std::vector& args) const; + MaybeOrValue Call( + napi_value recv, const std::initializer_list& args) const; + MaybeOrValue Call(napi_value recv, + const std::vector& args) const; + MaybeOrValue Call(napi_value recv, + size_t argc, + const napi_value* args) const; + + MaybeOrValue MakeCallback( + napi_value recv, + const std::initializer_list& args, + napi_async_context context = nullptr) const; + MaybeOrValue MakeCallback( + napi_value recv, + const std::vector& args, + napi_async_context context = nullptr) const; + MaybeOrValue MakeCallback( + napi_value recv, + size_t argc, + const napi_value* args, + napi_async_context context = nullptr) const; + + MaybeOrValue New(const std::initializer_list& args) const; + MaybeOrValue New(const std::vector& args) const; +}; + +// Shortcuts to creating a new reference with inferred type and refcount = 0. +template +Reference Weak(T value); +ObjectReference Weak(Object value); +FunctionReference Weak(Function value); + +// Shortcuts to creating a new reference with inferred type and refcount = 1. +template +Reference Persistent(T value); +ObjectReference Persistent(Object value); +FunctionReference Persistent(Function value); + +/// A persistent reference to a JavaScript error object. Use of this class +/// depends somewhat on whether C++ exceptions are enabled at compile time. +/// +/// ### Handling Errors With C++ Exceptions +/// +/// If C++ exceptions are enabled, then the `Error` class extends +/// `std::exception` and enables integrated error-handling for C++ exceptions +/// and JavaScript exceptions. +/// +/// If a Node-API call fails without executing any JavaScript code (for +/// example due to an invalid argument), then the Node-API wrapper +/// automatically converts and throws the error as a C++ exception of type +/// `Napi::Error`. Or if a JavaScript function called by C++ code via Node-API +/// throws a JavaScript exception, then the Node-API wrapper automatically +/// converts and throws it as a C++ exception of type `Napi::Error`. +/// +/// If a C++ exception of type `Napi::Error` escapes from a Node-API C++ +/// callback, then the Node-API wrapper automatically converts and throws it +/// as a JavaScript exception. Therefore, catching a C++ exception of type +/// `Napi::Error` prevents a JavaScript exception from being thrown. +/// +/// #### Example 1A - Throwing a C++ exception: +/// +/// Napi::Env env = ... +/// throw Napi::Error::New(env, "Example exception"); +/// +/// Following C++ statements will not be executed. The exception will bubble +/// up as a C++ exception of type `Napi::Error`, until it is either caught +/// while still in C++, or else automatically propagated as a JavaScript +/// exception when the callback returns to JavaScript. +/// +/// #### Example 2A - Propagating a Node-API C++ exception: +/// +/// Napi::Function jsFunctionThatThrows = someObj.As(); +/// Napi::Value result = jsFunctionThatThrows({ arg1, arg2 }); +/// +/// Following C++ statements will not be executed. The exception will bubble +/// up as a C++ exception of type `Napi::Error`, until it is either caught +/// while still in C++, or else automatically propagated as a JavaScript +/// exception when the callback returns to JavaScript. +/// +/// #### Example 3A - Handling a Node-API C++ exception: +/// +/// Napi::Function jsFunctionThatThrows = someObj.As(); +/// Napi::Value result; +/// try { +/// result = jsFunctionThatThrows({ arg1, arg2 }); +/// } catch (const Napi::Error& e) { +/// cerr << "Caught JavaScript exception: " + e.what(); +/// } +/// +/// Since the exception was caught here, it will not be propagated as a +/// JavaScript exception. +/// +/// ### Handling Errors Without C++ Exceptions +/// +/// If C++ exceptions are disabled (by defining +/// `NODE_ADDON_API_DISABLE_CPP_EXCEPTIONS`) then this class does not extend +/// `std::exception`, and APIs in the `Napi` namespace do not throw C++ +/// exceptions when they fail. Instead, they raise _pending_ JavaScript +/// exceptions and return _empty_ `Value`s. Calling code should check +/// `Value::IsEmpty()` before attempting to use a returned value, and may use +/// methods on the `Env` class to check for, get, and clear a pending JavaScript +/// exception. If the pending exception is not cleared, it will be thrown when +/// the native callback returns to JavaScript. +/// +/// #### Example 1B - Throwing a JS exception +/// +/// Napi::Env env = ... +/// Napi::Error::New(env, "Example +/// exception").ThrowAsJavaScriptException(); return; +/// +/// After throwing a JS exception, the code should generally return +/// immediately from the native callback, after performing any necessary +/// cleanup. +/// +/// #### Example 2B - Propagating a Node-API JS exception: +/// +/// Napi::Function jsFunctionThatThrows = someObj.As(); +/// Napi::Value result = jsFunctionThatThrows({ arg1, arg2 }); +/// if (result.IsEmpty()) return; +/// +/// An empty value result from a Node-API call indicates an error occurred, +/// and a JavaScript exception is pending. To let the exception propagate, the +/// code should generally return immediately from the native callback, after +/// performing any necessary cleanup. +/// +/// #### Example 3B - Handling a Node-API JS exception: +/// +/// Napi::Function jsFunctionThatThrows = someObj.As(); +/// Napi::Value result = jsFunctionThatThrows({ arg1, arg2 }); +/// if (result.IsEmpty()) { +/// Napi::Error e = env.GetAndClearPendingException(); +/// cerr << "Caught JavaScript exception: " + e.Message(); +/// } +/// +/// Since the exception was cleared here, it will not be propagated as a +/// JavaScript exception after the native callback returns. +class Error : public ObjectReference +#ifdef NODE_ADDON_API_CPP_EXCEPTIONS + , + public std::exception +#endif // NODE_ADDON_API_CPP_EXCEPTIONS +{ + public: + static Error New(napi_env env); + static Error New(napi_env env, const char* message); + static Error New(napi_env env, const std::string& message); + + static NAPI_NO_RETURN void Fatal(const char* location, const char* message); + + Error(); + Error(napi_env env, napi_value value); + + // An error can be moved or copied. + Error(Error&& other); + Error& operator=(Error&& other); + Error(const Error&); + Error& operator=(const Error&); + + const std::string& Message() const NAPI_NOEXCEPT; + void ThrowAsJavaScriptException() const; + + Object Value() const; + +#ifdef NODE_ADDON_API_CPP_EXCEPTIONS + const char* what() const NAPI_NOEXCEPT override; +#endif // NODE_ADDON_API_CPP_EXCEPTIONS + + protected: + /// !cond INTERNAL + using create_error_fn = napi_status (*)(napi_env envb, + napi_value code, + napi_value msg, + napi_value* result); + + template + static TError New(napi_env env, + const char* message, + size_t length, + create_error_fn create_error); + /// !endcond + + private: + static inline const char* ERROR_WRAP_VALUE() NAPI_NOEXCEPT; + mutable std::string _message; +}; + +class TypeError : public Error { + public: + static TypeError New(napi_env env, const char* message); + static TypeError New(napi_env env, const std::string& message); + + TypeError(); + TypeError(napi_env env, napi_value value); +}; + +class RangeError : public Error { + public: + static RangeError New(napi_env env, const char* message); + static RangeError New(napi_env env, const std::string& message); + + RangeError(); + RangeError(napi_env env, napi_value value); +}; + +#if NAPI_VERSION > 8 +class SyntaxError : public Error { + public: + static SyntaxError New(napi_env env, const char* message); + static SyntaxError New(napi_env env, const std::string& message); + + SyntaxError(); + SyntaxError(napi_env env, napi_value value); +}; +#endif // NAPI_VERSION > 8 + +class CallbackInfo { + public: + CallbackInfo(napi_env env, napi_callback_info info); + ~CallbackInfo(); + + // Disallow copying to prevent multiple free of _dynamicArgs + NAPI_DISALLOW_ASSIGN_COPY(CallbackInfo) + + Napi::Env Env() const; + Value NewTarget() const; + bool IsConstructCall() const; + size_t Length() const; + const Value operator[](size_t index) const; + Value This() const; + void* Data() const; + void SetData(void* data); + explicit operator napi_callback_info() const; + + private: + const size_t _staticArgCount = 6; + napi_env _env; + napi_callback_info _info; + napi_value _this; + size_t _argc; + napi_value* _argv; + napi_value _staticArgs[6]{}; + napi_value* _dynamicArgs; + void* _data; +}; + +class PropertyDescriptor { + public: + using GetterCallback = Napi::Value (*)(const Napi::CallbackInfo& info); + using SetterCallback = void (*)(const Napi::CallbackInfo& info); + +#ifndef NODE_ADDON_API_DISABLE_DEPRECATED + template + static PropertyDescriptor Accessor( + const char* utf8name, + Getter getter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Accessor( + const std::string& utf8name, + Getter getter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Accessor( + napi_value name, + Getter getter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Accessor( + Name name, + Getter getter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Accessor( + const char* utf8name, + Getter getter, + Setter setter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Accessor( + const std::string& utf8name, + Getter getter, + Setter setter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Accessor( + napi_value name, + Getter getter, + Setter setter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Accessor( + Name name, + Getter getter, + Setter setter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Function( + const char* utf8name, + Callable cb, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Function( + const std::string& utf8name, + Callable cb, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Function( + napi_value name, + Callable cb, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Function( + Name name, + Callable cb, + napi_property_attributes attributes = napi_default, + void* data = nullptr); +#endif // !NODE_ADDON_API_DISABLE_DEPRECATED + + template + static PropertyDescriptor Accessor( + const char* utf8name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + + template + static PropertyDescriptor Accessor( + const std::string& utf8name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + + template + static PropertyDescriptor Accessor( + Name name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + + template + static PropertyDescriptor Accessor( + const char* utf8name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + + template + static PropertyDescriptor Accessor( + const std::string& utf8name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + + template + static PropertyDescriptor Accessor( + Name name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + + template + static PropertyDescriptor Accessor( + Napi::Env env, + Napi::Object object, + const char* utf8name, + Getter getter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Accessor( + Napi::Env env, + Napi::Object object, + const std::string& utf8name, + Getter getter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Accessor( + Napi::Env env, + Napi::Object object, + Name name, + Getter getter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Accessor( + Napi::Env env, + Napi::Object object, + const char* utf8name, + Getter getter, + Setter setter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Accessor( + Napi::Env env, + Napi::Object object, + const std::string& utf8name, + Getter getter, + Setter setter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Accessor( + Napi::Env env, + Napi::Object object, + Name name, + Getter getter, + Setter setter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Function( + Napi::Env env, + Napi::Object object, + const char* utf8name, + Callable cb, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Function( + Napi::Env env, + Napi::Object object, + const std::string& utf8name, + Callable cb, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Function( + Napi::Env env, + Napi::Object object, + Name name, + Callable cb, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + static PropertyDescriptor Value( + const char* utf8name, + napi_value value, + napi_property_attributes attributes = napi_default); + static PropertyDescriptor Value( + const std::string& utf8name, + napi_value value, + napi_property_attributes attributes = napi_default); + static PropertyDescriptor Value( + napi_value name, + napi_value value, + napi_property_attributes attributes = napi_default); + static PropertyDescriptor Value( + Name name, + Napi::Value value, + napi_property_attributes attributes = napi_default); + + PropertyDescriptor(napi_property_descriptor desc); + + operator napi_property_descriptor&(); + operator const napi_property_descriptor&() const; + + private: + napi_property_descriptor _desc; +}; + +/// Property descriptor for use with `ObjectWrap::DefineClass()`. +/// +/// This is different from the standalone `PropertyDescriptor` because it is +/// specific to each `ObjectWrap` subclass. This prevents using descriptors +/// from a different class when defining a new class (preventing the callbacks +/// from having incorrect `this` pointers). +template +class ClassPropertyDescriptor { + public: + ClassPropertyDescriptor(napi_property_descriptor desc) : _desc(desc) {} + + operator napi_property_descriptor&() { return _desc; } + operator const napi_property_descriptor&() const { return _desc; } + + private: + napi_property_descriptor _desc; +}; + +template +struct MethodCallbackData { + TCallback callback; + void* data; +}; + +template +struct AccessorCallbackData { + TGetterCallback getterCallback; + TSetterCallback setterCallback; + void* data; +}; + +template +class InstanceWrap { + public: + using InstanceVoidMethodCallback = void (T::*)(const CallbackInfo& info); + using InstanceMethodCallback = Napi::Value (T::*)(const CallbackInfo& info); + using InstanceGetterCallback = Napi::Value (T::*)(const CallbackInfo& info); + using InstanceSetterCallback = void (T::*)(const CallbackInfo& info, + const Napi::Value& value); + + using PropertyDescriptor = ClassPropertyDescriptor; + + static PropertyDescriptor InstanceMethod( + const char* utf8name, + InstanceVoidMethodCallback method, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + static PropertyDescriptor InstanceMethod( + const char* utf8name, + InstanceMethodCallback method, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + static PropertyDescriptor InstanceMethod( + Symbol name, + InstanceVoidMethodCallback method, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + static PropertyDescriptor InstanceMethod( + Symbol name, + InstanceMethodCallback method, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor InstanceMethod( + const char* utf8name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor InstanceMethod( + const char* utf8name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor InstanceMethod( + Symbol name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor InstanceMethod( + Symbol name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + static PropertyDescriptor InstanceAccessor( + const char* utf8name, + InstanceGetterCallback getter, + InstanceSetterCallback setter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + static PropertyDescriptor InstanceAccessor( + Symbol name, + InstanceGetterCallback getter, + InstanceSetterCallback setter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor InstanceAccessor( + const char* utf8name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor InstanceAccessor( + Symbol name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + static PropertyDescriptor InstanceValue( + const char* utf8name, + Napi::Value value, + napi_property_attributes attributes = napi_default); + static PropertyDescriptor InstanceValue( + Symbol name, + Napi::Value value, + napi_property_attributes attributes = napi_default); + + protected: + static void AttachPropData(napi_env env, + napi_value value, + const napi_property_descriptor* prop); + + private: + using This = InstanceWrap; + + using InstanceVoidMethodCallbackData = + MethodCallbackData; + using InstanceMethodCallbackData = + MethodCallbackData; + using InstanceAccessorCallbackData = + AccessorCallbackData; + + static napi_value InstanceVoidMethodCallbackWrapper(napi_env env, + napi_callback_info info); + static napi_value InstanceMethodCallbackWrapper(napi_env env, + napi_callback_info info); + static napi_value InstanceGetterCallbackWrapper(napi_env env, + napi_callback_info info); + static napi_value InstanceSetterCallbackWrapper(napi_env env, + napi_callback_info info); + + template + static napi_value WrappedMethod(napi_env env, + napi_callback_info info) NAPI_NOEXCEPT; + + template + struct SetterTag {}; + + template + static napi_callback WrapSetter(SetterTag) NAPI_NOEXCEPT { + return &This::WrappedMethod; + } + static napi_callback WrapSetter(SetterTag) NAPI_NOEXCEPT { + return nullptr; + } +}; + +/// Base class to be extended by C++ classes exposed to JavaScript; each C++ +/// class instance gets "wrapped" by a JavaScript object that is managed by this +/// class. +/// +/// At initialization time, the `DefineClass()` method must be used to +/// hook up the accessor and method callbacks. It takes a list of +/// property descriptors, which can be constructed via the various +/// static methods on the base class. +/// +/// #### Example: +/// +/// class Example: public Napi::ObjectWrap { +/// public: +/// static void Initialize(Napi::Env& env, Napi::Object& target) { +/// Napi::Function constructor = DefineClass(env, "Example", { +/// InstanceAccessor<&Example::GetSomething, +/// &Example::SetSomething>("value"), +/// InstanceMethod<&Example::DoSomething>("doSomething"), +/// }); +/// target.Set("Example", constructor); +/// } +/// +/// Example(const Napi::CallbackInfo& info); // Constructor +/// Napi::Value GetSomething(const Napi::CallbackInfo& info); +/// void SetSomething(const Napi::CallbackInfo& info, const Napi::Value& +/// value); Napi::Value DoSomething(const Napi::CallbackInfo& info); +/// } +template +class ObjectWrap : public InstanceWrap, public Reference { + public: + ObjectWrap(const CallbackInfo& callbackInfo); + virtual ~ObjectWrap(); + + static T* Unwrap(Object wrapper); + + // Methods exposed to JavaScript must conform to one of these callback + // signatures. + using StaticVoidMethodCallback = void (*)(const CallbackInfo& info); + using StaticMethodCallback = Napi::Value (*)(const CallbackInfo& info); + using StaticGetterCallback = Napi::Value (*)(const CallbackInfo& info); + using StaticSetterCallback = void (*)(const CallbackInfo& info, + const Napi::Value& value); + + using PropertyDescriptor = ClassPropertyDescriptor; + + static Function DefineClass( + Napi::Env env, + const char* utf8name, + const std::initializer_list& properties, + void* data = nullptr); + static Function DefineClass(Napi::Env env, + const char* utf8name, + const std::vector& properties, + void* data = nullptr); + static PropertyDescriptor StaticMethod( + const char* utf8name, + StaticVoidMethodCallback method, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + static PropertyDescriptor StaticMethod( + const char* utf8name, + StaticMethodCallback method, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + static PropertyDescriptor StaticMethod( + Symbol name, + StaticVoidMethodCallback method, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + static PropertyDescriptor StaticMethod( + Symbol name, + StaticMethodCallback method, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor StaticMethod( + const char* utf8name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor StaticMethod( + Symbol name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor StaticMethod( + const char* utf8name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor StaticMethod( + Symbol name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + static PropertyDescriptor StaticAccessor( + const char* utf8name, + StaticGetterCallback getter, + StaticSetterCallback setter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + static PropertyDescriptor StaticAccessor( + Symbol name, + StaticGetterCallback getter, + StaticSetterCallback setter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor StaticAccessor( + const char* utf8name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor StaticAccessor( + Symbol name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + static PropertyDescriptor StaticValue( + const char* utf8name, + Napi::Value value, + napi_property_attributes attributes = napi_default); + static PropertyDescriptor StaticValue( + Symbol name, + Napi::Value value, + napi_property_attributes attributes = napi_default); + static Napi::Value OnCalledAsFunction(const Napi::CallbackInfo& callbackInfo); + virtual void Finalize(Napi::Env env); + virtual void Finalize(BasicEnv env); + + private: + using This = ObjectWrap; + + static napi_value ConstructorCallbackWrapper(napi_env env, + napi_callback_info info); + static napi_value StaticVoidMethodCallbackWrapper(napi_env env, + napi_callback_info info); + static napi_value StaticMethodCallbackWrapper(napi_env env, + napi_callback_info info); + static napi_value StaticGetterCallbackWrapper(napi_env env, + napi_callback_info info); + static napi_value StaticSetterCallbackWrapper(napi_env env, + napi_callback_info info); + static void FinalizeCallback(node_addon_api_basic_env env, + void* data, + void* hint); + + static void PostFinalizeCallback(napi_env env, void* data, void* hint); + + static Function DefineClass(Napi::Env env, + const char* utf8name, + const size_t props_count, + const napi_property_descriptor* props, + void* data = nullptr); + + using StaticVoidMethodCallbackData = + MethodCallbackData; + using StaticMethodCallbackData = MethodCallbackData; + + using StaticAccessorCallbackData = + AccessorCallbackData; + + template + static napi_value WrappedMethod(napi_env env, + napi_callback_info info) NAPI_NOEXCEPT; + + template + struct StaticSetterTag {}; + + template + static napi_callback WrapStaticSetter(StaticSetterTag) NAPI_NOEXCEPT { + return &This::WrappedMethod; + } + static napi_callback WrapStaticSetter(StaticSetterTag) + NAPI_NOEXCEPT { + return nullptr; + } + + bool _construction_failed = true; + bool _finalized = false; +}; + +class HandleScope { + public: + HandleScope(napi_env env, napi_handle_scope scope); + explicit HandleScope(Napi::Env env); + ~HandleScope(); + + // Disallow copying to prevent double close of napi_handle_scope + NAPI_DISALLOW_ASSIGN_COPY(HandleScope) + + operator napi_handle_scope() const; + + Napi::Env Env() const; + + private: + napi_env _env; + napi_handle_scope _scope; +}; + +class EscapableHandleScope { + public: + EscapableHandleScope(napi_env env, napi_escapable_handle_scope scope); + explicit EscapableHandleScope(Napi::Env env); + ~EscapableHandleScope(); + + // Disallow copying to prevent double close of napi_escapable_handle_scope + NAPI_DISALLOW_ASSIGN_COPY(EscapableHandleScope) + + operator napi_escapable_handle_scope() const; + + Napi::Env Env() const; + Value Escape(napi_value escapee); + + private: + napi_env _env; + napi_escapable_handle_scope _scope; +}; + +#if (NAPI_VERSION > 2) +class CallbackScope { + public: + CallbackScope(napi_env env, napi_callback_scope scope); + CallbackScope(napi_env env, napi_async_context context); + virtual ~CallbackScope(); + + // Disallow copying to prevent double close of napi_callback_scope + NAPI_DISALLOW_ASSIGN_COPY(CallbackScope) + + operator napi_callback_scope() const; + + Napi::Env Env() const; + + private: + napi_env _env; + napi_callback_scope _scope; +}; +#endif + +class AsyncContext { + public: + explicit AsyncContext(napi_env env, const char* resource_name); + explicit AsyncContext(napi_env env, + const char* resource_name, + const Object& resource); + virtual ~AsyncContext(); + + AsyncContext(AsyncContext&& other); + AsyncContext& operator=(AsyncContext&& other); + NAPI_DISALLOW_ASSIGN_COPY(AsyncContext) + + operator napi_async_context() const; + + Napi::Env Env() const; + + private: + napi_env _env; + napi_async_context _context; +}; + +#if NAPI_HAS_THREADS +class AsyncWorker { + public: + virtual ~AsyncWorker(); + + NAPI_DISALLOW_ASSIGN_COPY(AsyncWorker) + + operator napi_async_work() const; + + Napi::Env Env() const; + + void Queue(); + void Cancel(); + void SuppressDestruct(); + + ObjectReference& Receiver(); + FunctionReference& Callback(); + + virtual void OnExecute(Napi::Env env); + virtual void OnWorkComplete(Napi::Env env, napi_status status); + + protected: + explicit AsyncWorker(const Function& callback); + explicit AsyncWorker(const Function& callback, const char* resource_name); + explicit AsyncWorker(const Function& callback, + const char* resource_name, + const Object& resource); + explicit AsyncWorker(const Object& receiver, const Function& callback); + explicit AsyncWorker(const Object& receiver, + const Function& callback, + const char* resource_name); + explicit AsyncWorker(const Object& receiver, + const Function& callback, + const char* resource_name, + const Object& resource); + + explicit AsyncWorker(Napi::Env env); + explicit AsyncWorker(Napi::Env env, const char* resource_name); + explicit AsyncWorker(Napi::Env env, + const char* resource_name, + const Object& resource); + + virtual void Execute() = 0; + virtual void OnOK(); + virtual void OnError(const Error& e); + virtual void Destroy(); + virtual std::vector GetResult(Napi::Env env); + + void SetError(const std::string& error); + + private: + static inline void OnAsyncWorkExecute(napi_env env, void* asyncworker); + static inline void OnAsyncWorkComplete(napi_env env, + napi_status status, + void* asyncworker); + + napi_env _env; + napi_async_work _work; + ObjectReference _receiver; + FunctionReference _callback; + std::string _error; + bool _suppress_destruct; +}; +#endif // NAPI_HAS_THREADS + +#if (NAPI_VERSION > 3 && NAPI_HAS_THREADS) +class ThreadSafeFunction { + public: + // This API may only be called from the main thread. + template + static ThreadSafeFunction New(napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount); + + // This API may only be called from the main thread. + template + static ThreadSafeFunction New(napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context); + + // This API may only be called from the main thread. + template + static ThreadSafeFunction New(napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + Finalizer finalizeCallback); + + // This API may only be called from the main thread. + template + static ThreadSafeFunction New(napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + Finalizer finalizeCallback, + FinalizerDataType* data); + + // This API may only be called from the main thread. + template + static ThreadSafeFunction New(napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback); + + // This API may only be called from the main thread. + template + static ThreadSafeFunction New(napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data); + + // This API may only be called from the main thread. + template + static ThreadSafeFunction New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount); + + // This API may only be called from the main thread. + template + static ThreadSafeFunction New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context); + + // This API may only be called from the main thread. + template + static ThreadSafeFunction New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + Finalizer finalizeCallback); + + // This API may only be called from the main thread. + template + static ThreadSafeFunction New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + Finalizer finalizeCallback, + FinalizerDataType* data); + + // This API may only be called from the main thread. + template + static ThreadSafeFunction New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback); + + // This API may only be called from the main thread. + template + static ThreadSafeFunction New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data); + + ThreadSafeFunction(); + ThreadSafeFunction(napi_threadsafe_function tsFunctionValue); + + operator napi_threadsafe_function() const; + + // This API may be called from any thread. + napi_status BlockingCall() const; + + // This API may be called from any thread. + template + napi_status BlockingCall(Callback callback) const; + + // This API may be called from any thread. + template + napi_status BlockingCall(DataType* data, Callback callback) const; + + // This API may be called from any thread. + napi_status NonBlockingCall() const; + + // This API may be called from any thread. + template + napi_status NonBlockingCall(Callback callback) const; + + // This API may be called from any thread. + template + napi_status NonBlockingCall(DataType* data, Callback callback) const; + + // This API may only be called from the main thread. + void Ref(napi_env env) const; + + // This API may only be called from the main thread. + void Unref(napi_env env) const; + + // This API may be called from any thread. + napi_status Acquire() const; + + // This API may be called from any thread. + napi_status Release() const; + + // This API may be called from any thread. + napi_status Abort() const; + + struct ConvertibleContext { + template + operator T*() { + return static_cast(context); + } + void* context; + }; + + // This API may be called from any thread. + ConvertibleContext GetContext() const; + + private: + using CallbackWrapper = std::function; + + template + static ThreadSafeFunction New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data, + napi_finalize wrapper); + + napi_status CallInternal(CallbackWrapper* callbackWrapper, + napi_threadsafe_function_call_mode mode) const; + + static void CallJS(napi_env env, + napi_value jsCallback, + void* context, + void* data); + + napi_threadsafe_function _tsfn; +}; + +// A TypedThreadSafeFunction by default has no context (nullptr) and can +// accept any type (void) to its CallJs. +template +class TypedThreadSafeFunction { + public: + // This API may only be called from the main thread. + // Helper function that returns nullptr if running Node-API 5+, otherwise a + // non-empty, no-op Function. This provides the ability to specify at + // compile-time a callback parameter to `New` that safely does no action + // when targeting _any_ Node-API version. +#if NAPI_VERSION > 4 + static std::nullptr_t EmptyFunctionFactory(Napi::Env env); +#else + static Napi::Function EmptyFunctionFactory(Napi::Env env); +#endif + static Napi::Function FunctionOrEmpty(Napi::Env env, + Napi::Function& callback); + +#if NAPI_VERSION > 4 + // This API may only be called from the main thread. + // Creates a new threadsafe function with: + // Callback [missing] Resource [missing] Finalizer [missing] + template + static TypedThreadSafeFunction New( + napi_env env, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context = nullptr); + + // This API may only be called from the main thread. + // Creates a new threadsafe function with: + // Callback [missing] Resource [passed] Finalizer [missing] + template + static TypedThreadSafeFunction New( + napi_env env, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context = nullptr); + + // This API may only be called from the main thread. + // Creates a new threadsafe function with: + // Callback [missing] Resource [missing] Finalizer [passed] + template + static TypedThreadSafeFunction New( + napi_env env, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data = nullptr); + + // This API may only be called from the main thread. + // Creates a new threadsafe function with: + // Callback [missing] Resource [passed] Finalizer [passed] + template + static TypedThreadSafeFunction New( + napi_env env, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data = nullptr); +#endif + + // This API may only be called from the main thread. + // Creates a new threadsafe function with: + // Callback [passed] Resource [missing] Finalizer [missing] + template + static TypedThreadSafeFunction New( + napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context = nullptr); + + // This API may only be called from the main thread. + // Creates a new threadsafe function with: + // Callback [passed] Resource [passed] Finalizer [missing] + template + static TypedThreadSafeFunction New( + napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context = nullptr); + + // This API may only be called from the main thread. + // Creates a new threadsafe function with: + // Callback [passed] Resource [missing] Finalizer [passed] + template + static TypedThreadSafeFunction New( + napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data = nullptr); + + // This API may only be called from the main thread. + // Creates a new threadsafe function with: + // Callback [passed] Resource [passed] Finalizer [passed] + template + static TypedThreadSafeFunction New( + napi_env env, + CallbackType callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data = nullptr); + + TypedThreadSafeFunction(); + TypedThreadSafeFunction(napi_threadsafe_function tsFunctionValue); + + operator napi_threadsafe_function() const; + + // This API may be called from any thread. + napi_status BlockingCall(DataType* data = nullptr) const; + + // This API may be called from any thread. + napi_status NonBlockingCall(DataType* data = nullptr) const; + + // This API may only be called from the main thread. + void Ref(napi_env env) const; + + // This API may only be called from the main thread. + void Unref(napi_env env) const; + + // This API may be called from any thread. + napi_status Acquire() const; + + // This API may be called from any thread. + napi_status Release() const; + + // This API may be called from any thread. + napi_status Abort() const; + + // This API may be called from any thread. + ContextType* GetContext() const; + + private: + template + static TypedThreadSafeFunction New( + napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data, + napi_finalize wrapper); + + static void CallJsInternal(napi_env env, + napi_value jsCallback, + void* context, + void* data); + + protected: + napi_threadsafe_function _tsfn; +}; +template +class AsyncProgressWorkerBase : public AsyncWorker { + public: + virtual void OnWorkProgress(DataType* data) = 0; + class ThreadSafeData { + public: + ThreadSafeData(AsyncProgressWorkerBase* asyncprogressworker, DataType* data) + : _asyncprogressworker(asyncprogressworker), _data(data) {} + + AsyncProgressWorkerBase* asyncprogressworker() { + return _asyncprogressworker; + }; + DataType* data() { return _data; }; + + private: + AsyncProgressWorkerBase* _asyncprogressworker; + DataType* _data; + }; + void OnWorkComplete(Napi::Env env, napi_status status) override; + + protected: + explicit AsyncProgressWorkerBase(const Object& receiver, + const Function& callback, + const char* resource_name, + const Object& resource, + size_t queue_size = 1); + virtual ~AsyncProgressWorkerBase(); + +// Optional callback of Napi::ThreadSafeFunction only available after +// NAPI_VERSION 4. Refs: https://github.com/nodejs/node/pull/27791 +#if NAPI_VERSION > 4 + explicit AsyncProgressWorkerBase(Napi::Env env, + const char* resource_name, + const Object& resource, + size_t queue_size = 1); +#endif + + static inline void OnAsyncWorkProgress(Napi::Env env, + Napi::Function jsCallback, + void* data); + + napi_status NonBlockingCall(DataType* data); + + private: + ThreadSafeFunction _tsfn; + bool _work_completed = false; + napi_status _complete_status; + static inline void OnThreadSafeFunctionFinalize( + Napi::Env env, void* data, AsyncProgressWorkerBase* context); +}; + +template +class AsyncProgressWorker : public AsyncProgressWorkerBase { + public: + virtual ~AsyncProgressWorker(); + + class ExecutionProgress { + friend class AsyncProgressWorker; + + public: + void Signal() const; + void Send(const T* data, size_t count) const; + + private: + explicit ExecutionProgress(AsyncProgressWorker* worker) : _worker(worker) {} + AsyncProgressWorker* const _worker; + }; + + void OnWorkProgress(void*) override; + + protected: + explicit AsyncProgressWorker(const Function& callback); + explicit AsyncProgressWorker(const Function& callback, + const char* resource_name); + explicit AsyncProgressWorker(const Function& callback, + const char* resource_name, + const Object& resource); + explicit AsyncProgressWorker(const Object& receiver, + const Function& callback); + explicit AsyncProgressWorker(const Object& receiver, + const Function& callback, + const char* resource_name); + explicit AsyncProgressWorker(const Object& receiver, + const Function& callback, + const char* resource_name, + const Object& resource); + +// Optional callback of Napi::ThreadSafeFunction only available after +// NAPI_VERSION 4. Refs: https://github.com/nodejs/node/pull/27791 +#if NAPI_VERSION > 4 + explicit AsyncProgressWorker(Napi::Env env); + explicit AsyncProgressWorker(Napi::Env env, const char* resource_name); + explicit AsyncProgressWorker(Napi::Env env, + const char* resource_name, + const Object& resource); +#endif + virtual void Execute(const ExecutionProgress& progress) = 0; + virtual void OnProgress(const T* data, size_t count) = 0; + + private: + void Execute() override; + void Signal(); + void SendProgress_(const T* data, size_t count); + + std::mutex _mutex; + T* _asyncdata; + size_t _asyncsize; + bool _signaled; +}; + +template +class AsyncProgressQueueWorker + : public AsyncProgressWorkerBase> { + public: + virtual ~AsyncProgressQueueWorker(){}; + + class ExecutionProgress { + friend class AsyncProgressQueueWorker; + + public: + void Signal() const; + void Send(const T* data, size_t count) const; + + private: + explicit ExecutionProgress(AsyncProgressQueueWorker* worker) + : _worker(worker) {} + AsyncProgressQueueWorker* const _worker; + }; + + void OnWorkComplete(Napi::Env env, napi_status status) override; + void OnWorkProgress(std::pair*) override; + + protected: + explicit AsyncProgressQueueWorker(const Function& callback); + explicit AsyncProgressQueueWorker(const Function& callback, + const char* resource_name); + explicit AsyncProgressQueueWorker(const Function& callback, + const char* resource_name, + const Object& resource); + explicit AsyncProgressQueueWorker(const Object& receiver, + const Function& callback); + explicit AsyncProgressQueueWorker(const Object& receiver, + const Function& callback, + const char* resource_name); + explicit AsyncProgressQueueWorker(const Object& receiver, + const Function& callback, + const char* resource_name, + const Object& resource); + +// Optional callback of Napi::ThreadSafeFunction only available after +// NAPI_VERSION 4. Refs: https://github.com/nodejs/node/pull/27791 +#if NAPI_VERSION > 4 + explicit AsyncProgressQueueWorker(Napi::Env env); + explicit AsyncProgressQueueWorker(Napi::Env env, const char* resource_name); + explicit AsyncProgressQueueWorker(Napi::Env env, + const char* resource_name, + const Object& resource); +#endif + virtual void Execute(const ExecutionProgress& progress) = 0; + virtual void OnProgress(const T* data, size_t count) = 0; + + private: + void Execute() override; + void Signal() const; + void SendProgress_(const T* data, size_t count); +}; +#endif // NAPI_VERSION > 3 && NAPI_HAS_THREADS + +// Memory management. +class MemoryManagement { + public: + static int64_t AdjustExternalMemory(BasicEnv env, int64_t change_in_bytes); +}; + +// Version management +class VersionManagement { + public: + static uint32_t GetNapiVersion(BasicEnv env); + static const napi_node_version* GetNodeVersion(BasicEnv env); +}; + +#if NAPI_VERSION > 5 +template +class Addon : public InstanceWrap { + public: + static inline Object Init(Env env, Object exports); + static T* Unwrap(Object wrapper); + + protected: + using AddonProp = ClassPropertyDescriptor; + void DefineAddon(Object exports, + const std::initializer_list& props); + Napi::Object DefineProperties(Object object, + const std::initializer_list& props); + + private: + Object entry_point_; +}; +#endif // NAPI_VERSION > 5 + +#ifdef NAPI_CPP_CUSTOM_NAMESPACE +} // namespace NAPI_CPP_CUSTOM_NAMESPACE +#endif + +} // namespace Napi + +// Inline implementations of all the above class methods are included here. +#include "napi-inl.h" + +#endif // SRC_NAPI_H_ diff --git a/node_modules/bcrypt/node_modules/node-addon-api/node_addon_api.gyp b/node_modules/bcrypt/node_modules/node-addon-api/node_addon_api.gyp new file mode 100644 index 0000000..8c09926 --- /dev/null +++ b/node_modules/bcrypt/node_modules/node-addon-api/node_addon_api.gyp @@ -0,0 +1,42 @@ +{ + 'targets': [ + { + 'target_name': 'node_addon_api', + 'type': 'none', + 'sources': [ 'napi.h', 'napi-inl.h' ], + 'direct_dependent_settings': { + 'include_dirs': [ '.' ], + 'includes': ['noexcept.gypi'], + } + }, + { + 'target_name': 'node_addon_api_except', + 'type': 'none', + 'sources': [ 'napi.h', 'napi-inl.h' ], + 'direct_dependent_settings': { + 'include_dirs': [ '.' ], + 'includes': ['except.gypi'], + } + }, + { + 'target_name': 'node_addon_api_except_all', + 'type': 'none', + 'sources': [ 'napi.h', 'napi-inl.h' ], + 'direct_dependent_settings': { + 'include_dirs': [ '.' ], + 'includes': ['except.gypi'], + 'defines': [ 'NODE_ADDON_API_CPP_EXCEPTIONS_ALL' ] + } + }, + { + 'target_name': 'node_addon_api_maybe', + 'type': 'none', + 'sources': [ 'napi.h', 'napi-inl.h' ], + 'direct_dependent_settings': { + 'include_dirs': [ '.' ], + 'includes': ['noexcept.gypi'], + 'defines': ['NODE_ADDON_API_ENABLE_MAYBE'] + } + }, + ] +} diff --git a/node_modules/bcrypt/node_modules/node-addon-api/node_api.gyp b/node_modules/bcrypt/node_modules/node-addon-api/node_api.gyp new file mode 100644 index 0000000..4ff0ae7 --- /dev/null +++ b/node_modules/bcrypt/node_modules/node-addon-api/node_api.gyp @@ -0,0 +1,9 @@ +{ + 'targets': [ + { + 'target_name': 'nothing', + 'type': 'static_library', + 'sources': [ 'nothing.c' ] + } + ] +} diff --git a/node_modules/bcrypt/node_modules/node-addon-api/noexcept.gypi b/node_modules/bcrypt/node_modules/node-addon-api/noexcept.gypi new file mode 100644 index 0000000..83df4dd --- /dev/null +++ b/node_modules/bcrypt/node_modules/node-addon-api/noexcept.gypi @@ -0,0 +1,26 @@ +{ + 'defines': [ 'NODE_ADDON_API_DISABLE_CPP_EXCEPTIONS' ], + 'cflags': [ '-fno-exceptions' ], + 'cflags_cc': [ '-fno-exceptions' ], + 'conditions': [ + ["OS=='win'", { + # _HAS_EXCEPTIONS is already defined and set to 0 in common.gypi + #"defines": [ + # "_HAS_EXCEPTIONS=0" + #], + "msvs_settings": { + "VCCLCompilerTool": { + 'ExceptionHandling': 0, + 'EnablePREfast': 'true', + }, + }, + }], + ["OS=='mac'", { + 'xcode_settings': { + 'CLANG_CXX_LIBRARY': 'libc++', + 'MACOSX_DEPLOYMENT_TARGET': '10.7', + 'GCC_ENABLE_CPP_EXCEPTIONS': 'NO', + }, + }], + ], +} diff --git a/node_modules/bcrypt/node_modules/node-addon-api/nothing.c b/node_modules/bcrypt/node_modules/node-addon-api/nothing.c new file mode 100644 index 0000000..e69de29 diff --git a/node_modules/bcrypt/node_modules/node-addon-api/package-support.json b/node_modules/bcrypt/node_modules/node-addon-api/package-support.json new file mode 100644 index 0000000..10d3607 --- /dev/null +++ b/node_modules/bcrypt/node_modules/node-addon-api/package-support.json @@ -0,0 +1,21 @@ +{ + "versions": [ + { + "version": "*", + "target": { + "node": "active" + }, + "response": { + "type": "time-permitting", + "paid": false, + "contact": { + "name": "node-addon-api team", + "url": "https://github.com/nodejs/node-addon-api/issues" + } + }, + "backing": [ { "project": "https://github.com/nodejs" }, + { "foundation": "https://openjsf.org/" } + ] + } + ] +} diff --git a/node_modules/bcrypt/node_modules/node-addon-api/package.json b/node_modules/bcrypt/node_modules/node-addon-api/package.json new file mode 100644 index 0000000..b9393c6 --- /dev/null +++ b/node_modules/bcrypt/node_modules/node-addon-api/package.json @@ -0,0 +1,480 @@ +{ + "bugs": { + "url": "https://github.com/nodejs/node-addon-api/issues" + }, + "contributors": [ + { + "name": "Abhishek Kumar Singh", + "url": "https://github.com/abhi11210646" + }, + { + "name": "Alba Mendez", + "url": "https://github.com/jmendeth" + }, + { + "name": "Alexander Floh", + "url": "https://github.com/alexanderfloh" + }, + { + "name": "Ammar Faizi", + "url": "https://github.com/ammarfaizi2" + }, + { + "name": "András Timár, Dr", + "url": "https://github.com/timarandras" + }, + { + "name": "Andrew Petersen", + "url": "https://github.com/kirbysayshi" + }, + { + "name": "Anisha Rohra", + "url": "https://github.com/anisha-rohra" + }, + { + "name": "Anna Henningsen", + "url": "https://github.com/addaleax" + }, + { + "name": "Arnaud Botella", + "url": "https://github.com/BotellaA" + }, + { + "name": "Arunesh Chandra", + "url": "https://github.com/aruneshchandra" + }, + { + "name": "Azlan Mukhtar", + "url": "https://github.com/azlan" + }, + { + "name": "Ben Berman", + "url": "https://github.com/rivertam" + }, + { + "name": "Benjamin Byholm", + "url": "https://github.com/kkoopa" + }, + { + "name": "Bill Gallafent", + "url": "https://github.com/gallafent" + }, + { + "name": "blagoev", + "url": "https://github.com/blagoev" + }, + { + "name": "Bruce A. MacNaughton", + "url": "https://github.com/bmacnaughton" + }, + { + "name": "Cory Mickelson", + "url": "https://github.com/corymickelson" + }, + { + "name": "Daniel Bevenius", + "url": "https://github.com/danbev" + }, + { + "name": "Dante Calderón", + "url": "https://github.com/dantehemerson" + }, + { + "name": "Darshan Sen", + "url": "https://github.com/RaisinTen" + }, + { + "name": "David Halls", + "url": "https://github.com/davedoesdev" + }, + { + "name": "Deepak Rajamohan", + "url": "https://github.com/deepakrkris" + }, + { + "name": "Dmitry Ashkadov", + "url": "https://github.com/dmitryash" + }, + { + "name": "Dongjin Na", + "url": "https://github.com/nadongguri" + }, + { + "name": "Doni Rubiagatra", + "url": "https://github.com/rubiagatra" + }, + { + "name": "Eric Bickle", + "url": "https://github.com/ebickle" + }, + { + "name": "extremeheat", + "url": "https://github.com/extremeheat" + }, + { + "name": "Feng Yu", + "url": "https://github.com/F3n67u" + }, + { + "name": "Ferdinand Holzer", + "url": "https://github.com/fholzer" + }, + { + "name": "Gabriel Schulhof", + "url": "https://github.com/gabrielschulhof" + }, + { + "name": "Guenter Sandner", + "url": "https://github.com/gms1" + }, + { + "name": "Gus Caplan", + "url": "https://github.com/devsnek" + }, + { + "name": "Helio Frota", + "url": "https://github.com/helio-frota" + }, + { + "name": "Hitesh Kanwathirtha", + "url": "https://github.com/digitalinfinity" + }, + { + "name": "ikokostya", + "url": "https://github.com/ikokostya" + }, + { + "name": "Jack Xia", + "url": "https://github.com/JckXia" + }, + { + "name": "Jake Barnes", + "url": "https://github.com/DuBistKomisch" + }, + { + "name": "Jake Yoon", + "url": "https://github.com/yjaeseok" + }, + { + "name": "Jason Ginchereau", + "url": "https://github.com/jasongin" + }, + { + "name": "Jenny", + "url": "https://github.com/egg-bread" + }, + { + "name": "Jeroen Janssen", + "url": "https://github.com/japj" + }, + { + "name": "Jim Schlight", + "url": "https://github.com/jschlight" + }, + { + "name": "Jinho Bang", + "url": "https://github.com/romandev" + }, + { + "name": "José Expósito", + "url": "https://github.com/JoseExposito" + }, + { + "name": "joshgarde", + "url": "https://github.com/joshgarde" + }, + { + "name": "Julian Mesa", + "url": "https://github.com/julianmesa-gitkraken" + }, + { + "name": "Kasumi Hanazuki", + "url": "https://github.com/hanazuki" + }, + { + "name": "Kelvin", + "url": "https://github.com/kelvinhammond" + }, + { + "name": "Kevin Eady", + "url": "https://github.com/KevinEady" + }, + { + "name": "Kévin VOYER", + "url": "https://github.com/kecsou" + }, + { + "name": "kidneysolo", + "url": "https://github.com/kidneysolo" + }, + { + "name": "Koki Nishihara", + "url": "https://github.com/Nishikoh" + }, + { + "name": "Konstantin Tarkus", + "url": "https://github.com/koistya" + }, + { + "name": "Kyle Farnung", + "url": "https://github.com/kfarnung" + }, + { + "name": "Kyle Kovacs", + "url": "https://github.com/nullromo" + }, + { + "name": "legendecas", + "url": "https://github.com/legendecas" + }, + { + "name": "LongYinan", + "url": "https://github.com/Brooooooklyn" + }, + { + "name": "Lovell Fuller", + "url": "https://github.com/lovell" + }, + { + "name": "Luciano Martorella", + "url": "https://github.com/lmartorella" + }, + { + "name": "mastergberry", + "url": "https://github.com/mastergberry" + }, + { + "name": "Mathias Küsel", + "url": "https://github.com/mathiask88" + }, + { + "name": "Mathias Stearn", + "url": "https://github.com/RedBeard0531" + }, + { + "name": "Matteo Collina", + "url": "https://github.com/mcollina" + }, + { + "name": "Michael Dawson", + "url": "https://github.com/mhdawson" + }, + { + "name": "Michael Price", + "url": "https://github.com/mikepricedev" + }, + { + "name": "Michele Campus", + "url": "https://github.com/kYroL01" + }, + { + "name": "Mikhail Cheshkov", + "url": "https://github.com/mcheshkov" + }, + { + "name": "nempoBu4", + "url": "https://github.com/nempoBu4" + }, + { + "name": "Nicola Del Gobbo", + "url": "https://github.com/NickNaso" + }, + { + "name": "Nick Soggin", + "url": "https://github.com/iSkore" + }, + { + "name": "Nikolai Vavilov", + "url": "https://github.com/seishun" + }, + { + "name": "Nurbol Alpysbayev", + "url": "https://github.com/anurbol" + }, + { + "name": "pacop", + "url": "https://github.com/pacop" + }, + { + "name": "Peter Šándor", + "url": "https://github.com/petersandor" + }, + { + "name": "Philipp Renoth", + "url": "https://github.com/DaAitch" + }, + { + "name": "rgerd", + "url": "https://github.com/rgerd" + }, + { + "name": "Richard Lau", + "url": "https://github.com/richardlau" + }, + { + "name": "Rolf Timmermans", + "url": "https://github.com/rolftimmermans" + }, + { + "name": "Ross Weir", + "url": "https://github.com/ross-weir" + }, + { + "name": "Ryuichi Okumura", + "url": "https://github.com/okuryu" + }, + { + "name": "Saint Gabriel", + "url": "https://github.com/chineduG" + }, + { + "name": "Sampson Gao", + "url": "https://github.com/sampsongao" + }, + { + "name": "Sam Roberts", + "url": "https://github.com/sam-github" + }, + { + "name": "strager", + "url": "https://github.com/strager" + }, + { + "name": "Taylor Woll", + "url": "https://github.com/boingoing" + }, + { + "name": "Thomas Gentilhomme", + "url": "https://github.com/fraxken" + }, + { + "name": "Tim Rach", + "url": "https://github.com/timrach" + }, + { + "name": "Tobias Nießen", + "url": "https://github.com/tniessen" + }, + { + "name": "todoroff", + "url": "https://github.com/todoroff" + }, + { + "name": "Toyo Li", + "url": "https://github.com/toyobayashi" + }, + { + "name": "Tux3", + "url": "https://github.com/tux3" + }, + { + "name": "Vlad Velmisov", + "url": "https://github.com/Velmisov" + }, + { + "name": "Vladimir Morozov", + "url": "https://github.com/vmoroz" + }, + { + "name": "WenheLI", + "url": "https://github.com/WenheLI" + }, + { + "name": "Xuguang Mei", + "url": "https://github.com/meixg" + }, + { + "name": "Yohei Kishimoto", + "url": "https://github.com/morokosi" + }, + { + "name": "Yulong Wang", + "url": "https://github.com/fs-eire" + }, + { + "name": "Ziqiu Zhao", + "url": "https://github.com/ZzqiZQute" + }, + { + "name": "Feng Yu", + "url": "https://github.com/F3n67u" + }, + { + "name": "wanlu wang", + "url": "https://github.com/wanlu" + }, + { + "name": "Caleb Hearon", + "url": "https://github.com/chearon" + }, + { + "name": "Marx", + "url": "https://github.com/MarxJiao" + }, + { + "name": "Ömer AKGÜL", + "url": "https://github.com/tuhalf" + } + ], + "description": "Node.js API (Node-API)", + "devDependencies": { + "benchmark": "^2.1.4", + "bindings": "^1.5.0", + "clang-format": "^1.4.0", + "eslint": "^9.13.0", + "fs-extra": "^11.1.1", + "neostandard": "^0.12.0", + "pre-commit": "^1.2.2", + "semver": "^7.6.0" + }, + "directories": {}, + "gypfile": false, + "homepage": "https://github.com/nodejs/node-addon-api", + "keywords": [ + "n-api", + "napi", + "addon", + "native", + "bindings", + "c", + "c++", + "nan", + "node-addon-api" + ], + "license": "MIT", + "main": "index.js", + "name": "node-addon-api", + "readme": "README.md", + "repository": { + "type": "git", + "url": "git://github.com/nodejs/node-addon-api.git" + }, + "files": [ + "*.{c,h,gyp,gypi}", + "package-support.json", + "tools/" + ], + "scripts": { + "prebenchmark": "node-gyp rebuild -C benchmark", + "benchmark": "node benchmark", + "create-coverage": "npm test --coverage", + "report-coverage-html": "rm -rf coverage-html && mkdir coverage-html && gcovr -e test --merge-mode-functions merge-use-line-max --html-nested ./coverage-html/index.html test", + "report-coverage-xml": "rm -rf coverage-xml && mkdir coverage-xml && gcovr -e test --merge-mode-functions merge-use-line-max --xml -o ./coverage-xml/coverage-cxx.xml test", + "pretest": "node-gyp rebuild -C test", + "test": "node test", + "test:debug": "node-gyp rebuild -C test --debug && NODE_API_BUILD_CONFIG=Debug node ./test/index.js", + "predev": "node-gyp rebuild -C test --debug", + "dev": "node test", + "predev:incremental": "node-gyp configure build -C test --debug", + "dev:incremental": "node test", + "doc": "doxygen doc/Doxyfile", + "lint": "eslint && node tools/clang-format", + "lint:fix": "eslint --fix && node tools/clang-format --fix" + }, + "pre-commit": "lint", + "version": "8.6.0", + "support": true, + "engines": { + "node": "^18 || ^20 || >= 21" + } +} diff --git a/node_modules/bcrypt/node_modules/node-addon-api/tools/README.md b/node_modules/bcrypt/node_modules/node-addon-api/tools/README.md new file mode 100644 index 0000000..6b80e94 --- /dev/null +++ b/node_modules/bcrypt/node_modules/node-addon-api/tools/README.md @@ -0,0 +1,73 @@ +# Tools + +## clang-format + +The clang-format checking tools is designed to check changed lines of code compared to given git-refs. + +## Migration Script + +The migration tool is designed to reduce repetitive work in the migration process. However, the script is not aiming to convert every thing for you. There are usually some small fixes and major reconstruction required. + +### How To Use + +To run the conversion script, first make sure you have the latest `node-addon-api` in your `node_modules` directory. +``` +npm install node-addon-api +``` + +Then run the script passing your project directory +``` +node ./node_modules/node-addon-api/tools/conversion.js ./ +``` + +After finish, recompile and debug things that are missed by the script. + + +### Quick Fixes +Here is the list of things that can be fixed easily. + 1. Change your methods' return value to void if it doesn't return value to JavaScript. + 2. Use `.` to access attribute or to invoke member function in Napi::Object instead of `->`. + 3. `Napi::New(env, value);` to `Napi::[Type]::New(env, value); + + +### Major Reconstructions +The implementation of `Napi::ObjectWrap` is significantly different from NAN's. `Napi::ObjectWrap` takes a pointer to the wrapped object and creates a reference to the wrapped object inside ObjectWrap constructor. `Napi::ObjectWrap` also associates wrapped object's instance methods to Javascript module instead of static methods like NAN. + +So if you use Nan::ObjectWrap in your module, you will need to execute the following steps. + + 1. Convert your [ClassName]::New function to a constructor function that takes a `Napi::CallbackInfo`. Declare it as +``` +[ClassName](const Napi::CallbackInfo& info); +``` +and define it as +``` +[ClassName]::[ClassName](const Napi::CallbackInfo& info) : Napi::ObjectWrap<[ClassName]>(info){ + ... +} +``` +This way, the `Napi::ObjectWrap` constructor will be invoked after the object has been instantiated and `Napi::ObjectWrap` can use the `this` pointer to create a reference to the wrapped object. + + 2. Move your original constructor code into the new constructor. Delete your original constructor. + 3. In your class initialization function, associate native methods in the following way. +``` +Napi::FunctionReference constructor; + +void [ClassName]::Init(Napi::Env env, Napi::Object exports, Napi::Object module) { + Napi::HandleScope scope(env); + Napi::Function ctor = DefineClass(env, "Canvas", { + InstanceMethod<&[ClassName]::Func1>("Func1"), + InstanceMethod<&[ClassName]::Func2>("Func2"), + InstanceAccessor<&[ClassName]::ValueGetter>("Value"), + StaticMethod<&[ClassName]::StaticMethod>("MethodName"), + InstanceValue("Value", Napi::[Type]::New(env, value)), + }); + + constructor = Napi::Persistent(ctor); + constructor .SuppressDestruct(); + exports.Set("[ClassName]", ctor); +} +``` + 4. In function where you need to Unwrap the ObjectWrap in NAN like `[ClassName]* native = Nan::ObjectWrap::Unwrap<[ClassName]>(info.This());`, use `this` pointer directly as the unwrapped object as each ObjectWrap instance is associated with a unique object instance. + + +If you still find issues after following this guide, please leave us an issue describing your problem and we will try to resolve it. diff --git a/node_modules/bcrypt/node_modules/node-addon-api/tools/check-napi.js b/node_modules/bcrypt/node_modules/node-addon-api/tools/check-napi.js new file mode 100644 index 0000000..9199af3 --- /dev/null +++ b/node_modules/bcrypt/node_modules/node-addon-api/tools/check-napi.js @@ -0,0 +1,99 @@ +'use strict'; +// Descend into a directory structure and, for each file matching *.node, output +// based on the imports found in the file whether it's an N-API module or not. + +const fs = require('fs'); +const path = require('path'); + +// Read the output of the command, break it into lines, and use the reducer to +// decide whether the file is an N-API module or not. +function checkFile (file, command, argv, reducer) { + const child = require('child_process').spawn(command, argv, { + stdio: ['inherit', 'pipe', 'inherit'] + }); + let leftover = ''; + let isNapi; + child.stdout.on('data', (chunk) => { + if (isNapi === undefined) { + chunk = (leftover + chunk.toString()).split(/[\r\n]+/); + leftover = chunk.pop(); + isNapi = chunk.reduce(reducer, isNapi); + if (isNapi !== undefined) { + child.kill(); + } + } + }); + child.on('close', (code, signal) => { + if ((code === null && signal !== null) || (code !== 0)) { + console.log( + command + ' exited with code: ' + code + ' and signal: ' + signal); + } else { + // Green if it's a N-API module, red otherwise. + console.log( + '\x1b[' + (isNapi ? '42' : '41') + 'm' + + (isNapi ? ' N-API' : 'Not N-API') + + '\x1b[0m: ' + file); + } + }); +} + +// Use nm -a to list symbols. +function checkFileUNIX (file) { + checkFile(file, 'nm', ['-a', file], (soFar, line) => { + if (soFar === undefined) { + line = line.match(/([0-9a-f]*)? ([a-zA-Z]) (.*$)/); + if (line[2] === 'U') { + if (/^napi/.test(line[3])) { + soFar = true; + } + } + } + return soFar; + }); +} + +// Use dumpbin /imports to list symbols. +function checkFileWin32 (file) { + checkFile(file, 'dumpbin', ['/imports', file], (soFar, line) => { + if (soFar === undefined) { + line = line.match(/([0-9a-f]*)? +([a-zA-Z0-9]) (.*$)/); + if (line && /^napi/.test(line[line.length - 1])) { + soFar = true; + } + } + return soFar; + }); +} + +// Descend into a directory structure and pass each file ending in '.node' to +// one of the above checks, depending on the OS. +function recurse (top) { + fs.readdir(top, (error, items) => { + if (error) { + throw new Error('error reading directory ' + top + ': ' + error); + } + items.forEach((item) => { + item = path.join(top, item); + fs.stat(item, ((item) => (error, stats) => { + if (error) { + throw new Error('error about ' + item + ': ' + error); + } + if (stats.isDirectory()) { + recurse(item); + } else if (/[.]node$/.test(item) && + // Explicitly ignore files called 'nothing.node' because they are + // artefacts of node-addon-api having identified a version of + // Node.js that ships with a correct implementation of N-API. + path.basename(item) !== 'nothing.node') { + process.platform === 'win32' + ? checkFileWin32(item) + : checkFileUNIX(item); + } + })(item)); + }); + }); +} + +// Start with the directory given on the command line or the current directory +// if nothing was given. +recurse(process.argv.length > 3 ? process.argv[2] : '.'); diff --git a/node_modules/bcrypt/node_modules/node-addon-api/tools/clang-format.js b/node_modules/bcrypt/node_modules/node-addon-api/tools/clang-format.js new file mode 100644 index 0000000..e4bb4f5 --- /dev/null +++ b/node_modules/bcrypt/node_modules/node-addon-api/tools/clang-format.js @@ -0,0 +1,71 @@ +#!/usr/bin/env node + +const spawn = require('child_process').spawnSync; +const path = require('path'); + +const filesToCheck = ['*.h', '*.cc']; +const FORMAT_START = process.env.FORMAT_START || 'main'; + +function main (args) { + let fix = false; + while (args.length > 0) { + switch (args[0]) { + case '-f': + case '--fix': + fix = true; + break; + default: + } + args.shift(); + } + + const clangFormatPath = path.dirname(require.resolve('clang-format')); + const binary = process.platform === 'win32' + ? 'node_modules\\.bin\\clang-format.cmd' + : 'node_modules/.bin/clang-format'; + const options = ['--binary=' + binary, '--style=file']; + if (fix) { + options.push(FORMAT_START); + } else { + options.push('--diff', FORMAT_START); + } + + const gitClangFormatPath = path.join(clangFormatPath, 'bin/git-clang-format'); + const result = spawn( + 'python', + [gitClangFormatPath, ...options, '--', ...filesToCheck], + { encoding: 'utf-8' } + ); + + if (result.stderr) { + console.error('Error running git-clang-format:', result.stderr); + return 2; + } + + const clangFormatOutput = result.stdout.trim(); + // Bail fast if in fix mode. + if (fix) { + console.log(clangFormatOutput); + return 0; + } + // Detect if there is any complains from clang-format + if ( + clangFormatOutput !== '' && + clangFormatOutput !== 'no modified files to format' && + clangFormatOutput !== 'clang-format did not modify any files' + ) { + console.error(clangFormatOutput); + const fixCmd = 'npm run lint:fix'; + console.error(` + ERROR: please run "${fixCmd}" to format changes in your commit + Note that when running the command locally, please keep your local + main branch and working branch up to date with nodejs/node-addon-api + to exclude un-related complains. + Or you can run "env FORMAT_START=upstream/main ${fixCmd}".`); + return 1; + } +} + +if (require.main === module) { + process.exitCode = main(process.argv.slice(2)); +} diff --git a/node_modules/bcrypt/node_modules/node-addon-api/tools/conversion.js b/node_modules/bcrypt/node_modules/node-addon-api/tools/conversion.js new file mode 100644 index 0000000..e92a03a --- /dev/null +++ b/node_modules/bcrypt/node_modules/node-addon-api/tools/conversion.js @@ -0,0 +1,301 @@ +#! /usr/bin/env node + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const args = process.argv.slice(2); +const dir = args[0]; +if (!dir) { + console.log('Usage: node ' + path.basename(__filename) + ' '); + process.exit(1); +} + +const NodeApiVersion = require('../').version; + +const disable = args[1]; +let ConfigFileOperations; +if (disable !== '--disable' && dir !== '--disable') { + ConfigFileOperations = { + 'package.json': [ + [/([ ]*)"dependencies": {/g, '$1"dependencies": {\n$1 "node-addon-api": "' + NodeApiVersion + '",'], + [/[ ]*"nan": *"[^"]+"(,|)[\n\r]/g, ''] + ], + 'binding.gyp': [ + [/([ ]*)'include_dirs': \[/g, '$1\'include_dirs\': [\n$1 \'\s+(\w+)\s*=\s*Nan::New\([\w\d:]+\);(?:\w+->Reset\(\1\))?\s+\1->SetClassName\(Nan::String::New\("(\w+)"\)\);/g, 'Napi::Function $1 = DefineClass(env, "$2", {'], + [/Local\s+(\w+)\s*=\s*Nan::New\([\w\d:]+\);\s+(\w+)\.Reset\((\1)\);\s+\1->SetClassName\((Nan::String::New|Nan::New<(v8::)*String>)\("(.+?)"\)\);/g, 'Napi::Function $1 = DefineClass(env, "$6", {'], + [/Local\s+(\w+)\s*=\s*Nan::New\([\w\d:]+\);(?:\w+->Reset\(\1\))?\s+\1->SetClassName\(Nan::String::New\("(\w+)"\)\);/g, 'Napi::Function $1 = DefineClass(env, "$2", {'], + [/Nan::New\(([\w\d:]+)\)->GetFunction\(\)/g, 'Napi::Function::New(env, $1)'], + [/Nan::New\(([\w\d:]+)\)->GetFunction()/g, 'Napi::Function::New(env, $1);'], + [/Nan::New\(([\w\d:]+)\)/g, 'Napi::Function::New(env, $1)'], + [/Nan::New\(([\w\d:]+)\)/g, 'Napi::Function::New(env, $1)'], + + // FunctionTemplate to FunctionReference + [/Nan::Persistent<(v8::)*FunctionTemplate>/g, 'Napi::FunctionReference'], + [/Nan::Persistent<(v8::)*Function>/g, 'Napi::FunctionReference'], + [/v8::Local/g, 'Napi::FunctionReference'], + [/Local/g, 'Napi::FunctionReference'], + [/v8::FunctionTemplate/g, 'Napi::FunctionReference'], + [/FunctionTemplate/g, 'Napi::FunctionReference'], + + [/([ ]*)Nan::SetPrototypeMethod\(\w+, "(\w+)", (\w+)\);/g, '$1InstanceMethod("$2", &$3),'], + [/([ ]*)(?:\w+\.Reset\(\w+\);\s+)?\(target\)\.Set\("(\w+)",\s*Nan::GetFunction\((\w+)\)\);/gm, + '});\n\n' + + '$1constructor = Napi::Persistent($3);\n' + + '$1constructor.SuppressDestruct();\n' + + '$1target.Set("$2", $3);'], + + // TODO: Other attribute combinations + [/static_cast\(ReadOnly\s*\|\s*DontDelete\)/gm, + 'static_cast(napi_enumerable | napi_configurable)'], + + [/([\w\d:<>]+?)::Cast\((.+?)\)/g, '$2.As<$1>()'], + + [/\*Nan::Utf8String\(([^)]+)\)/g, '$1->As().Utf8Value().c_str()'], + [/Nan::Utf8String +(\w+)\(([^)]+)\)/g, 'std::string $1 = $2.As()'], + [/Nan::Utf8String/g, 'std::string'], + + [/v8::String::Utf8Value (.+?)\((.+?)\)/g, 'Napi::String $1(env, $2)'], + [/String::Utf8Value (.+?)\((.+?)\)/g, 'Napi::String $1(env, $2)'], + [/\.length\(\)/g, '.Length()'], + + [/Nan::MakeCallback\(([^,]+),[\s\\]+([^,]+),/gm, '$2.MakeCallback($1,'], + + [/class\s+(\w+)\s*:\s*public\s+Nan::ObjectWrap/g, 'class $1 : public Napi::ObjectWrap<$1>'], + [/(\w+)\(([^)]*)\)\s*:\s*Nan::ObjectWrap\(\)\s*(,)?/gm, '$1($2) : Napi::ObjectWrap<$1>()$3'], + + // HandleOKCallback to OnOK + [/HandleOKCallback/g, 'OnOK'], + // HandleErrorCallback to OnError + [/HandleErrorCallback/g, 'OnError'], + + // ex. .As() to .As() + [/\.As\(\)/g, '.As()'], + [/\.As<(Value|Boolean|String|Number|Object|Array|Symbol|External|Function)>\(\)/g, '.As()'], + + // ex. Nan::New(info[0]) to Napi::Number::New(info[0]) + [/Nan::New<(v8::)*Integer>\((.+?)\)/g, 'Napi::Number::New(env, $2)'], + [/Nan::New\(([0-9.]+)\)/g, 'Napi::Number::New(env, $1)'], + [/Nan::New<(v8::)*String>\("(.+?)"\)/g, 'Napi::String::New(env, "$2")'], + [/Nan::New\("(.+?)"\)/g, 'Napi::String::New(env, "$1")'], + [/Nan::New<(v8::)*(.+?)>\(\)/g, 'Napi::$2::New(env)'], + [/Nan::New<(.+?)>\(\)/g, 'Napi::$1::New(env)'], + [/Nan::New<(v8::)*(.+?)>\(/g, 'Napi::$2::New(env, '], + [/Nan::New<(.+?)>\(/g, 'Napi::$1::New(env, '], + [/Nan::NewBuffer\(/g, 'Napi::Buffer::New(env, '], + // TODO: Properly handle this + [/Nan::New\(/g, 'Napi::New(env, '], + + [/\.IsInt32\(\)/g, '.IsNumber()'], + [/->IsInt32\(\)/g, '.IsNumber()'], + + [/(.+?)->BooleanValue\(\)/g, '$1.As().Value()'], + [/(.+?)->Int32Value\(\)/g, '$1.As().Int32Value()'], + [/(.+?)->Uint32Value\(\)/g, '$1.As().Uint32Value()'], + [/(.+?)->IntegerValue\(\)/g, '$1.As().Int64Value()'], + [/(.+?)->NumberValue\(\)/g, '$1.As().DoubleValue()'], + + // ex. Nan::To(info[0]) to info[0].Value() + [/Nan::To\((.+?)\)/g, '$2.To()'], + [/Nan::To<(Boolean|String|Number|Object|Array|Symbol|Function)>\((.+?)\)/g, '$2.To()'], + // ex. Nan::To(info[0]) to info[0].As().Value() + [/Nan::To\((.+?)\)/g, '$1.As().Value()'], + // ex. Nan::To(info[0]) to info[0].As().Int32Value() + [/Nan::To\((.+?)\)/g, '$1.As().Int32Value()'], + // ex. Nan::To(info[0]) to info[0].As().Int32Value() + [/Nan::To\((.+?)\)/g, '$1.As().Int32Value()'], + // ex. Nan::To(info[0]) to info[0].As().Uint32Value() + [/Nan::To\((.+?)\)/g, '$1.As().Uint32Value()'], + // ex. Nan::To(info[0]) to info[0].As().Int64Value() + [/Nan::To\((.+?)\)/g, '$1.As().Int64Value()'], + // ex. Nan::To(info[0]) to info[0].As().FloatValue() + [/Nan::To\((.+?)\)/g, '$1.As().FloatValue()'], + // ex. Nan::To(info[0]) to info[0].As().DoubleValue() + [/Nan::To\((.+?)\)/g, '$1.As().DoubleValue()'], + + [/Nan::New\((\w+)\)->HasInstance\((\w+)\)/g, '$2.InstanceOf($1.Value())'], + + [/Nan::Has\(([^,]+),\s*/gm, '($1).Has('], + [/\.Has\([\s|\\]*Nan::New<(v8::)*String>\(([^)]+)\)\)/gm, '.Has($1)'], + [/\.Has\([\s|\\]*Nan::New\(([^)]+)\)\)/gm, '.Has($1)'], + + [/Nan::Get\(([^,]+),\s*/gm, '($1).Get('], + [/\.Get\([\s|\\]*Nan::New<(v8::)*String>\(([^)]+)\)\)/gm, '.Get($1)'], + [/\.Get\([\s|\\]*Nan::New\(([^)]+)\)\)/gm, '.Get($1)'], + + [/Nan::Set\(([^,]+),\s*/gm, '($1).Set('], + [/\.Set\([\s|\\]*Nan::New<(v8::)*String>\(([^)]+)\)\s*,/gm, '.Set($1,'], + [/\.Set\([\s|\\]*Nan::New\(([^)]+)\)\s*,/gm, '.Set($1,'], + + // ex. node::Buffer::HasInstance(info[0]) to info[0].IsBuffer() + [/node::Buffer::HasInstance\((.+?)\)/g, '$1.IsBuffer()'], + // ex. node::Buffer::Length(info[0]) to info[0].Length() + [/node::Buffer::Length\((.+?)\)/g, '$1.As>().Length()'], + // ex. node::Buffer::Data(info[0]) to info[0].Data() + [/node::Buffer::Data\((.+?)\)/g, '$1.As>().Data()'], + [/Nan::CopyBuffer\(/g, 'Napi::Buffer::Copy(env, '], + + // Nan::AsyncQueueWorker(worker) + [/Nan::AsyncQueueWorker\((.+)\);/g, '$1.Queue();'], + [/Nan::(Undefined|Null|True|False)\(\)/g, 'env.$1()'], + + // Nan::ThrowError(error) to Napi::Error::New(env, error).ThrowAsJavaScriptException() + [/([ ]*)return Nan::Throw(\w*?)Error\((.+?)\);/g, '$1Napi::$2Error::New(env, $3).ThrowAsJavaScriptException();\n$1return env.Null();'], + [/Nan::Throw(\w*?)Error\((.+?)\);\n(\s*)return;/g, 'Napi::$1Error::New(env, $2).ThrowAsJavaScriptException();\n$3return env.Null();'], + [/Nan::Throw(\w*?)Error\((.+?)\);/g, 'Napi::$1Error::New(env, $2).ThrowAsJavaScriptException();\n'], + // Nan::RangeError(error) to Napi::RangeError::New(env, error) + [/Nan::(\w*?)Error\((.+)\)/g, 'Napi::$1Error::New(env, $2)'], + + [/Nan::Set\((.+?),\n* *(.+?),\n* *(.+?),\n* *(.+?)\)/g, '$1.Set($2, $3, $4)'], + + [/Nan::(Escapable)?HandleScope\s+(\w+)\s*;/g, 'Napi::$1HandleScope $2(env);'], + [/Nan::(Escapable)?HandleScope/g, 'Napi::$1HandleScope'], + [/Nan::ForceSet\(([^,]+), ?/g, '$1->DefineProperty('], + [/\.ForceSet\(Napi::String::New\(env, "(\w+)"\),\s*?/g, '.DefineProperty("$1", '], + // [ /Nan::GetPropertyNames\(([^,]+)\)/, '$1->GetPropertyNames()' ], + [/Nan::Equals\(([^,]+),/g, '$1.StrictEquals('], + + [/(.+)->Set\(/g, '$1.Set('], + + [/Nan::Callback/g, 'Napi::FunctionReference'], + + [/Nan::Persistent/g, 'Napi::ObjectReference'], + [/Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target/g, 'Napi::Env& env, Napi::Object& target'], + + [/(\w+)\*\s+(\w+)\s*=\s*Nan::ObjectWrap::Unwrap<\w+>\(info\.This\(\)\);/g, '$1* $2 = this;'], + [/Nan::ObjectWrap::Unwrap<(\w+)>\((.*)\);/g, '$2.Unwrap<$1>();'], + + [/Nan::NAN_METHOD_RETURN_TYPE/g, 'void'], + [/NAN_INLINE/g, 'inline'], + + [/Nan::NAN_METHOD_ARGS_TYPE/g, 'const Napi::CallbackInfo&'], + [/NAN_METHOD\(([\w\d:]+?)\)/g, 'Napi::Value $1(const Napi::CallbackInfo& info)'], + [/static\s*NAN_GETTER\(([\w\d:]+?)\)/g, 'Napi::Value $1(const Napi::CallbackInfo& info)'], + [/NAN_GETTER\(([\w\d:]+?)\)/g, 'Napi::Value $1(const Napi::CallbackInfo& info)'], + [/static\s*NAN_SETTER\(([\w\d:]+?)\)/g, 'void $1(const Napi::CallbackInfo& info, const Napi::Value& value)'], + [/NAN_SETTER\(([\w\d:]+?)\)/g, 'void $1(const Napi::CallbackInfo& info, const Napi::Value& value)'], + [/void Init\((v8::)*Local<(v8::)*Object> exports\)/g, 'Napi::Object Init(Napi::Env env, Napi::Object exports)'], + [/NAN_MODULE_INIT\(([\w\d:]+?)\);/g, 'Napi::Object $1(Napi::Env env, Napi::Object exports);'], + [/NAN_MODULE_INIT\(([\w\d:]+?)\)/g, 'Napi::Object $1(Napi::Env env, Napi::Object exports)'], + + [/::(Init(?:ialize)?)\(target\)/g, '::$1(env, target, module)'], + [/constructor_template/g, 'constructor'], + + [/Nan::FunctionCallbackInfo<(v8::)?Value>[ ]*& [ ]*info\)[ ]*{\n*([ ]*)/gm, 'Napi::CallbackInfo& info) {\n$2Napi::Env env = info.Env();\n$2'], + [/Nan::FunctionCallbackInfo<(v8::)*Value>\s*&\s*info\);/g, 'Napi::CallbackInfo& info);'], + [/Nan::FunctionCallbackInfo<(v8::)*Value>\s*&/g, 'Napi::CallbackInfo&'], + + [/Buffer::HasInstance\(([^)]+)\)/g, '$1.IsBuffer()'], + + [/info\[(\d+)\]->/g, 'info[$1].'], + [/info\[([\w\d]+)\]->/g, 'info[$1].'], + [/info\.This\(\)->/g, 'info.This().'], + [/->Is(Object|String|Int32|Number)\(\)/g, '.Is$1()'], + [/info.GetReturnValue\(\).SetUndefined\(\)/g, 'return env.Undefined()'], + [/info\.GetReturnValue\(\)\.Set\(((\n|.)+?)\);/g, 'return $1;'], + + // ex. Local to Napi::Value + [/v8::Local/g, 'Napi::$1'], + [/Local<(Value|Boolean|String|Number|Object|Array|Symbol|External|Function)>/g, 'Napi::$1'], + + // Declare an env in helper functions that take a Napi::Value + [/(\w+)\(Napi::Value (\w+)(,\s*[^()]+)?\)\s*{\n*([ ]*)/gm, '$1(Napi::Value $2$3) {\n$4Napi::Env env = $2.Env();\n$4'], + + // delete #include and/or + [/#include +(<|")(?:node|nan).h("|>)/g, '#include $1napi.h$2\n#include $1uv.h$2'], + // NODE_MODULE to NODE_API_MODULE + [/NODE_MODULE/g, 'NODE_API_MODULE'], + [/Nan::/g, 'Napi::'], + [/nan.h/g, 'napi.h'], + + // delete .FromJust() + [/\.FromJust\(\)/g, ''], + // delete .ToLocalCheck() + [/\.ToLocalChecked\(\)/g, ''], + [/^.*->SetInternalFieldCount\(.*$/gm, ''], + + // replace using node; and/or using v8; to using Napi; + [/using (node|v8);/g, 'using Napi;'], + [/using namespace (node|Nan|v8);/g, 'using namespace Napi;'], + // delete using v8::Local; + [/using v8::Local;\n/g, ''], + // replace using v8::XXX; with using Napi::XXX + [/using v8::([A-Za-z]+);/g, 'using Napi::$1;'] + +]; + +const paths = listFiles(dir); +paths.forEach(function (dirEntry) { + const filename = dirEntry.split('\\').pop().split('/').pop(); + + // Check whether the file is a source file or a config file + // then execute function accordingly + const sourcePattern = /.+\.h|.+\.cc|.+\.cpp/; + if (sourcePattern.test(filename)) { + convertFile(dirEntry, SourceFileOperations); + } else if (ConfigFileOperations[filename] != null) { + convertFile(dirEntry, ConfigFileOperations[filename]); + } +}); + +function listFiles (dir, filelist) { + const files = fs.readdirSync(dir); + filelist = filelist || []; + files.forEach(function (file) { + if (file === 'node_modules') { + return; + } + + if (fs.statSync(path.join(dir, file)).isDirectory()) { + filelist = listFiles(path.join(dir, file), filelist); + } else { + filelist.push(path.join(dir, file)); + } + }); + return filelist; +} + +function convert (content, operations) { + for (let i = 0; i < operations.length; i++) { + const operation = operations[i]; + content = content.replace(operation[0], operation[1]); + } + return content; +} + +function convertFile (fileName, operations) { + fs.readFile(fileName, 'utf-8', function (err, file) { + if (err) throw err; + + file = convert(file, operations); + + fs.writeFile(fileName, file, function (err) { + if (err) throw err; + }); + }); +} diff --git a/node_modules/bcrypt/package.json b/node_modules/bcrypt/package.json new file mode 100644 index 0000000..c849897 --- /dev/null +++ b/node_modules/bcrypt/package.json @@ -0,0 +1,62 @@ +{ + "name": "bcrypt", + "description": "A bcrypt library for NodeJS.", + "keywords": [ + "bcrypt", + "password", + "auth", + "authentication", + "encryption", + "crypt", + "crypto" + ], + "main": "./bcrypt", + "version": "6.0.0", + "author": "Nick Campbell (https://github.com/ncb000gt)", + "engines": { + "node": ">= 18" + }, + "repository": { + "type": "git", + "url": "https://github.com/kelektiv/node.bcrypt.js.git" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/kelektiv/node.bcrypt.js/issues" + }, + "scripts": { + "test": "jest", + "install": "node-gyp-build", + "build": "prebuildify --napi --tag-libc --strip" + }, + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "devDependencies": { + "jest": "^29.7.0", + "prebuildify": "^6.0.1" + }, + "contributors": [ + "Antonio Salazar Cardozo (https://github.com/Shadowfiend)", + "Van Nguyen (https://github.com/thegoleffect)", + "David Trejo (https://github.com/dtrejo)", + "Ben Glow (https://github.com/pixelglow)", + "NewITFarmer.com <> (https://github.com/newitfarmer)", + "Alfred Westerveld (https://github.com/alfredwesterveld)", + "Vincent Côté-Roy (https://github.com/vincentcr)", + "Lloyd Hilaiel (https://github.com/lloyd)", + "Roman Shtylman (https://github.com/shtylman)", + "Vadim Graboys (https://github.com/vadimg)", + "Ben Noorduis <> (https://github.com/bnoordhuis)", + "Nate Rajlich (https://github.com/tootallnate)", + "Sean McArthur (https://github.com/seanmonstar)", + "Fanie Oosthuysen (https://github.com/weareu)", + "Amitosh Swain Mahapatra (https://github.com/Agathver)", + "Corbin Crutchley (https://github.com/crutchcorn)", + "Nicola Del Gobbo (https://github.com/NickNaso)" + ], + "binary": { + "module_name": "bcrypt_lib" + } +} diff --git a/node_modules/bcrypt/prebuilds/darwin-arm64/bcrypt.node b/node_modules/bcrypt/prebuilds/darwin-arm64/bcrypt.node new file mode 100644 index 0000000..5160c6c Binary files /dev/null and b/node_modules/bcrypt/prebuilds/darwin-arm64/bcrypt.node differ diff --git a/node_modules/bcrypt/prebuilds/darwin-x64/bcrypt.node b/node_modules/bcrypt/prebuilds/darwin-x64/bcrypt.node new file mode 100644 index 0000000..8de9c5f Binary files /dev/null and b/node_modules/bcrypt/prebuilds/darwin-x64/bcrypt.node differ diff --git a/node_modules/bcrypt/prebuilds/linux-arm/bcrypt.glibc.node b/node_modules/bcrypt/prebuilds/linux-arm/bcrypt.glibc.node new file mode 100644 index 0000000..3deef80 Binary files /dev/null and b/node_modules/bcrypt/prebuilds/linux-arm/bcrypt.glibc.node differ diff --git a/node_modules/bcrypt/prebuilds/linux-arm/bcrypt.musl.node b/node_modules/bcrypt/prebuilds/linux-arm/bcrypt.musl.node new file mode 100644 index 0000000..be7ca5f Binary files /dev/null and b/node_modules/bcrypt/prebuilds/linux-arm/bcrypt.musl.node differ diff --git a/node_modules/bcrypt/prebuilds/linux-arm64/bcrypt.glibc.node b/node_modules/bcrypt/prebuilds/linux-arm64/bcrypt.glibc.node new file mode 100644 index 0000000..6b2c7a9 Binary files /dev/null and b/node_modules/bcrypt/prebuilds/linux-arm64/bcrypt.glibc.node differ diff --git a/node_modules/bcrypt/prebuilds/linux-arm64/bcrypt.musl.node b/node_modules/bcrypt/prebuilds/linux-arm64/bcrypt.musl.node new file mode 100644 index 0000000..717cd14 Binary files /dev/null and b/node_modules/bcrypt/prebuilds/linux-arm64/bcrypt.musl.node differ diff --git a/node_modules/bcrypt/prebuilds/linux-x64/bcrypt.glibc.node b/node_modules/bcrypt/prebuilds/linux-x64/bcrypt.glibc.node new file mode 100644 index 0000000..b58e7dc Binary files /dev/null and b/node_modules/bcrypt/prebuilds/linux-x64/bcrypt.glibc.node differ diff --git a/node_modules/bcrypt/prebuilds/linux-x64/bcrypt.musl.node b/node_modules/bcrypt/prebuilds/linux-x64/bcrypt.musl.node new file mode 100644 index 0000000..e62ce92 Binary files /dev/null and b/node_modules/bcrypt/prebuilds/linux-x64/bcrypt.musl.node differ diff --git a/node_modules/bcrypt/prebuilds/win32-arm64/bcrypt.node b/node_modules/bcrypt/prebuilds/win32-arm64/bcrypt.node new file mode 100644 index 0000000..3b30839 Binary files /dev/null and b/node_modules/bcrypt/prebuilds/win32-arm64/bcrypt.node differ diff --git a/node_modules/bcrypt/prebuilds/win32-x64/bcrypt.node b/node_modules/bcrypt/prebuilds/win32-x64/bcrypt.node new file mode 100644 index 0000000..f7ef025 Binary files /dev/null and b/node_modules/bcrypt/prebuilds/win32-x64/bcrypt.node differ diff --git a/node_modules/bcrypt/promises.js b/node_modules/bcrypt/promises.js new file mode 100644 index 0000000..6685cc2 --- /dev/null +++ b/node_modules/bcrypt/promises.js @@ -0,0 +1,45 @@ +let Promise = global.Promise; + +/// encapsulate a method with a node-style callback in a Promise +/// @param {object} 'this' of the encapsulated function +/// @param {function} function to be encapsulated +/// @param {Array-like} args to be passed to the called function +/// @return {Promise} a Promise encapsulating the function +function promise(fn, context, args) { + if (!Array.isArray(args)) { + args = Array.prototype.slice.call(args); + } + + if (typeof fn !== 'function') { + return Promise.reject(new Error('fn must be a function')); + } + + return new Promise((resolve, reject) => { + args.push((err, data) => { + if (err) { + reject(err); + } else { + resolve(data); + } + }); + + fn.apply(context, args); + }); +} + +/// @param {err} the error to be thrown +function reject(err) { + return Promise.reject(err); +} + +/// changes the promise implementation that bcrypt uses +/// @param {Promise} the implementation to use +function use(promise) { + Promise = promise; +} + +module.exports = { + promise, + reject, + use +} diff --git a/node_modules/bcrypt/src/bcrypt.cc b/node_modules/bcrypt/src/bcrypt.cc new file mode 100644 index 0000000..bd8c573 --- /dev/null +++ b/node_modules/bcrypt/src/bcrypt.cc @@ -0,0 +1,315 @@ +/* $OpenBSD: bcrypt.c,v 1.31 2014/03/22 23:02:03 tedu Exp $ */ + +/* + * Copyright (c) 1997 Niels Provos + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* This password hashing algorithm was designed by David Mazieres + * and works as follows: + * + * 1. state := InitState () + * 2. state := ExpandKey (state, salt, password) + * 3. REPEAT rounds: + * state := ExpandKey (state, 0, password) + * state := ExpandKey (state, 0, salt) + * 4. ctext := "OrpheanBeholderScryDoubt" + * 5. REPEAT 64: + * ctext := Encrypt_ECB (state, ctext); + * 6. RETURN Concatenate (salt, ctext); + * + */ + +#include +#include +#include +#include + +#include "node_blf.h" + +#ifdef _WIN32 +#define snprintf _snprintf +#endif + +//#if !defined(__APPLE__) && !defined(__MACH__) +//#include "bsd/stdlib.h" +//#endif + +/* This implementation is adaptable to current computing power. + * You can have up to 2^31 rounds which should be enough for some + * time to come. + */ + +static void encode_base64(u_int8_t *, u_int8_t *, u_int16_t); +static void decode_base64(u_int8_t *, u_int16_t, u_int8_t *); + +const static char* error = ":"; + +const static u_int8_t Base64Code[] = +"./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + +const static u_int8_t index_64[128] = { + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 0, 1, 54, 55, + 56, 57, 58, 59, 60, 61, 62, 63, 255, 255, + 255, 255, 255, 255, 255, 2, 3, 4, 5, 6, + 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, + 255, 255, 255, 255, 255, 255, 28, 29, 30, + 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, + 51, 52, 53, 255, 255, 255, 255, 255 +}; +#define CHAR64(c) ( (c) > 127 ? 255 : index_64[(c)]) + +static void +decode_base64(u_int8_t *buffer, u_int16_t len, u_int8_t *data) +{ + u_int8_t *bp = buffer; + u_int8_t *p = data; + u_int8_t c1, c2, c3, c4; + while (bp < buffer + len) { + c1 = CHAR64(*p); + c2 = CHAR64(*(p + 1)); + + /* Invalid data */ + if (c1 == 255 || c2 == 255) + break; + + *bp++ = (c1 << 2) | ((c2 & 0x30) >> 4); + if (bp >= buffer + len) + break; + + c3 = CHAR64(*(p + 2)); + if (c3 == 255) + break; + + *bp++ = ((c2 & 0x0f) << 4) | ((c3 & 0x3c) >> 2); + if (bp >= buffer + len) + break; + + c4 = CHAR64(*(p + 3)); + if (c4 == 255) + break; + *bp++ = ((c3 & 0x03) << 6) | c4; + + p += 4; + } +} + +void +encode_salt(char *salt, u_int8_t *csalt, char minor, u_int16_t clen, u_int8_t logr) +{ + salt[0] = '$'; + salt[1] = BCRYPT_VERSION; + salt[2] = minor; + salt[3] = '$'; + + // Max rounds are 31 + snprintf(salt + 4, 4, "%2.2u$", logr & 0x001F); + + encode_base64((u_int8_t *) salt + 7, csalt, clen); +} + + +/* Generates a salt for this version of crypt. + Since versions may change. Keeping this here + seems sensible. + from: http://mail-index.netbsd.org/tech-crypto/2002/05/24/msg000204.html +*/ +void +bcrypt_gensalt(char minor, u_int8_t log_rounds, u_int8_t *seed, char *gsalt) +{ + if (log_rounds < 4) + log_rounds = 4; + else if (log_rounds > 31) + log_rounds = 31; + + encode_salt(gsalt, seed, minor, BCRYPT_MAXSALT, log_rounds); +} + +/* We handle $Vers$log2(NumRounds)$salt+passwd$ + i.e. $2$04$iwouldntknowwhattosayetKdJ6iFtacBqJdKe6aW7ou */ + +void +bcrypt(const char *key, size_t key_len, const char *salt, char *encrypted) +{ + blf_ctx state; + u_int32_t rounds, i, k; + u_int16_t j; + u_int8_t salt_len, logr, minor; + u_int8_t ciphertext[4 * BCRYPT_BLOCKS+1] = "OrpheanBeholderScryDoubt"; + u_int8_t csalt[BCRYPT_MAXSALT]; + u_int32_t cdata[BCRYPT_BLOCKS]; + int n; + + /* Discard "$" identifier */ + salt++; + + if (*salt > BCRYPT_VERSION) { + /* How do I handle errors ? Return ':' */ + strcpy(encrypted, error); + return; + } + + /* Check for minor versions */ + if (salt[1] != '$') { + switch (salt[1]) { + case 'a': /* 'ab' should not yield the same as 'abab' */ + case 'b': /* cap input length at 72 bytes */ + minor = salt[1]; + salt++; + break; + default: + strcpy(encrypted, error); + return; + } + } else + minor = 0; + + /* Discard version + "$" identifier */ + salt += 2; + + if (salt[2] != '$') { + /* Out of sync with passwd entry */ + strcpy(encrypted, error); + return; + } + + /* Computer power doesn't increase linear, 2^x should be fine */ + n = atoi(salt); + if (n > 31 || n < 0) { + strcpy(encrypted, error); + return; + } + logr = (u_int8_t)n; + if ((rounds = (u_int32_t) 1 << logr) < BCRYPT_MINROUNDS) { + strcpy(encrypted, error); + return; + } + + /* Discard num rounds + "$" identifier */ + salt += 3; + + if (strlen(salt) * 3 / 4 < BCRYPT_MAXSALT) { + strcpy(encrypted, error); + return; + } + + /* We dont want the base64 salt but the raw data */ + decode_base64(csalt, BCRYPT_MAXSALT, (u_int8_t *) salt); + salt_len = BCRYPT_MAXSALT; + if (minor <= 'a') + key_len = (u_int8_t)(key_len + (minor >= 'a' ? 1 : 0)); + else + { + /* cap key_len at the actual maximum supported + * length here to avoid integer wraparound */ + if (key_len > 72) + key_len = 72; + key_len++; /* include the NUL */ + } + + + /* Setting up S-Boxes and Subkeys */ + Blowfish_initstate(&state); + Blowfish_expandstate(&state, csalt, salt_len, + (u_int8_t *) key, key_len); + for (k = 0; k < rounds; k++) { + Blowfish_expand0state(&state, (u_int8_t *) key, key_len); + Blowfish_expand0state(&state, csalt, salt_len); + } + + /* This can be precomputed later */ + j = 0; + for (i = 0; i < BCRYPT_BLOCKS; i++) + cdata[i] = Blowfish_stream2word(ciphertext, 4 * BCRYPT_BLOCKS, &j); + + /* Now do the encryption */ + for (k = 0; k < 64; k++) + blf_enc(&state, cdata, BCRYPT_BLOCKS / 2); + + for (i = 0; i < BCRYPT_BLOCKS; i++) { + ciphertext[4 * i + 3] = cdata[i] & 0xff; + cdata[i] = cdata[i] >> 8; + ciphertext[4 * i + 2] = cdata[i] & 0xff; + cdata[i] = cdata[i] >> 8; + ciphertext[4 * i + 1] = cdata[i] & 0xff; + cdata[i] = cdata[i] >> 8; + ciphertext[4 * i + 0] = cdata[i] & 0xff; + } + + i = 0; + encrypted[i++] = '$'; + encrypted[i++] = BCRYPT_VERSION; + if (minor) + encrypted[i++] = minor; + encrypted[i++] = '$'; + + snprintf(encrypted + i, 4, "%2.2u$", logr & 0x001F); + + encode_base64((u_int8_t *) encrypted + i + 3, csalt, BCRYPT_MAXSALT); + encode_base64((u_int8_t *) encrypted + strlen(encrypted), ciphertext, + 4 * BCRYPT_BLOCKS - 1); + memset(&state, 0, sizeof(state)); + memset(ciphertext, 0, sizeof(ciphertext)); + memset(csalt, 0, sizeof(csalt)); + memset(cdata, 0, sizeof(cdata)); +} + +u_int32_t bcrypt_get_rounds(const char * hash) +{ + /* skip past the leading "$" */ + if (!hash || *(hash++) != '$') return 0; + + /* skip past version */ + if (0 == (*hash++)) return 0; + if (*hash && *hash != '$') hash++; + if (*hash++ != '$') return 0; + + return atoi(hash); +} + +static void +encode_base64(u_int8_t *buffer, u_int8_t *data, u_int16_t len) +{ + u_int8_t *bp = buffer; + u_int8_t *p = data; + u_int8_t c1, c2; + while (p < data + len) { + c1 = *p++; + *bp++ = Base64Code[(c1 >> 2)]; + c1 = (c1 & 0x03) << 4; + if (p >= data + len) { + *bp++ = Base64Code[c1]; + break; + } + c2 = *p++; + c1 |= (c2 >> 4) & 0x0f; + *bp++ = Base64Code[c1]; + c1 = (c2 & 0x0f) << 2; + if (p >= data + len) { + *bp++ = Base64Code[c1]; + break; + } + c2 = *p++; + c1 |= (c2 >> 6) & 0x03; + *bp++ = Base64Code[c1]; + *bp++ = Base64Code[c2 & 0x3f]; + } + *bp = '\0'; +} diff --git a/node_modules/bcrypt/src/bcrypt_node.cc b/node_modules/bcrypt/src/bcrypt_node.cc new file mode 100644 index 0000000..2f072a4 --- /dev/null +++ b/node_modules/bcrypt/src/bcrypt_node.cc @@ -0,0 +1,288 @@ +#define NAPI_VERSION 3 + +#include + +#include +#include +#include +#include // atoi + +#include "node_blf.h" + +#define NODE_LESS_THAN (!(NODE_VERSION_AT_LEAST(0, 5, 4))) + +namespace { + + bool ValidateSalt(const char* salt) { + + if (!salt || *salt != '$') { + return false; + } + + // discard $ + salt++; + + if (*salt > BCRYPT_VERSION) { + return false; + } + + if (salt[1] != '$') { + switch (salt[1]) { + case 'a': + case 'b': + salt++; + break; + default: + return false; + } + } + + // discard version + $ + salt += 2; + + if (salt[2] != '$') { + return false; + } + + int n = atoi(salt); + if (n > 31 || n < 0) { + return false; + } + + if (((uint8_t)1 << (uint8_t)n) < BCRYPT_MINROUNDS) { + return false; + } + + salt += 3; + if (strlen(salt) * 3 / 4 < BCRYPT_MAXSALT) { + return false; + } + + return true; + } + + inline char ToCharVersion(const std::string& str) { + return str[0]; + } + + /* SALT GENERATION */ + + class SaltAsyncWorker : public Napi::AsyncWorker { + public: + SaltAsyncWorker(const Napi::Function& callback, const std::string& seed, ssize_t rounds, char minor_ver) + : Napi::AsyncWorker(callback, "bcrypt:SaltAsyncWorker"), seed(seed), rounds(rounds), minor_ver(minor_ver) { + } + + ~SaltAsyncWorker() {} + + void Execute() { + bcrypt_gensalt(minor_ver, rounds, (u_int8_t *)&seed[0], salt); + } + + void OnOK() { + Napi::HandleScope scope(Env()); + Callback().Call({Env().Undefined(), Napi::String::New(Env(), salt)}); + } + + private: + std::string seed; + ssize_t rounds; + char minor_ver; + char salt[_SALT_LEN]; + }; + + Napi::Value GenerateSalt(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + if (info.Length() < 4) { + throw Napi::TypeError::New(env, "4 arguments expected"); + } + if (!info[0].IsString()) { + throw Napi::TypeError::New(env, "First argument must be a string"); + } + if (!info[2].IsBuffer() || (info[2].As>()).Length() != 16) { + throw Napi::TypeError::New(env, "Second argument must be a 16 byte Buffer"); + } + + const char minor_ver = ToCharVersion(info[0].As()); + const int32_t rounds = info[1].As(); + Napi::Buffer seed = info[2].As>(); + Napi::Function callback = info[3].As(); + SaltAsyncWorker* saltWorker = new SaltAsyncWorker(callback, std::string(seed.Data(), 16), rounds, minor_ver); + saltWorker->Queue(); + return env.Undefined(); + } + + Napi::Value GenerateSaltSync(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + if (info.Length() < 3) { + throw Napi::TypeError::New(env, "3 arguments expected"); + } + if (!info[0].IsString()) { + throw Napi::TypeError::New(env, "First argument must be a string"); + } + if (!info[2].IsBuffer() || (info[2].As>()).Length() != 16) { + throw Napi::TypeError::New(env, "Third argument must be a 16 byte Buffer"); + } + const char minor_ver = ToCharVersion(info[0].As()); + const int32_t rounds = info[1].As(); + Napi::Buffer buffer = info[2].As>(); + u_int8_t* seed = (u_int8_t*) buffer.Data(); + char salt[_SALT_LEN]; + bcrypt_gensalt(minor_ver, rounds, seed, salt); + return Napi::String::New(env, salt, strlen(salt)); + } + + inline std::string BufferToString(const Napi::Buffer &buf) { + return std::string(buf.Data(), buf.Length()); + } + + /* ENCRYPT DATA - USED TO BE HASHPW */ + + class EncryptAsyncWorker : public Napi::AsyncWorker { + public: + EncryptAsyncWorker(const Napi::Function& callback, const std::string& input, const std::string& salt) + : Napi::AsyncWorker(callback, "bcrypt:EncryptAsyncWorker"), input(input), salt(salt) { + } + + ~EncryptAsyncWorker() {} + + void Execute() { + if (!(ValidateSalt(salt.c_str()))) { + SetError("Invalid salt. Salt must be in the form of: $Vers$log2(NumRounds)$saltvalue"); + } + bcrypt(input.c_str(), input.length(), salt.c_str(), bcrypted); + } + + void OnOK() { + Napi::HandleScope scope(Env()); + Callback().Call({Env().Undefined(),Napi::String::New(Env(), bcrypted)}); + } + private: + std::string input; + std::string salt; + char bcrypted[_PASSWORD_LEN]; + }; + + Napi::Value Encrypt(const Napi::CallbackInfo& info) { + if (info.Length() < 3) { + throw Napi::TypeError::New(info.Env(), "3 arguments expected"); + } + std::string data = info[0].IsBuffer() + ? BufferToString(info[0].As>()) + : info[0].As(); + std::string salt = info[1].As(); + Napi::Function callback = info[2].As(); + EncryptAsyncWorker* encryptWorker = new EncryptAsyncWorker(callback, data, salt); + encryptWorker->Queue(); + return info.Env().Undefined(); + } + + Napi::Value EncryptSync(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + if (info.Length() < 2) { + throw Napi::TypeError::New(info.Env(), "2 arguments expected"); + } + std::string data = info[0].IsBuffer() + ? BufferToString(info[0].As>()) + : info[0].As(); + std::string salt = info[1].As(); + if (!(ValidateSalt(salt.c_str()))) { + throw Napi::Error::New(env, "Invalid salt. Salt must be in the form of: $Vers$log2(NumRounds)$saltvalue"); + } + char bcrypted[_PASSWORD_LEN]; + bcrypt(data.c_str(), data.length(), salt.c_str(), bcrypted); + return Napi::String::New(env, bcrypted, strlen(bcrypted)); + } + + /* COMPARATOR */ + inline bool CompareStrings(const char* s1, const char* s2) { + return strcmp(s1, s2) == 0; + } + + class CompareAsyncWorker : public Napi::AsyncWorker { + public: + CompareAsyncWorker(const Napi::Function& callback, const std::string& input, const std::string& encrypted) + : Napi::AsyncWorker(callback, "bcrypt:CompareAsyncWorker"), input(input), encrypted(encrypted) { + result = false; + } + + ~CompareAsyncWorker() {} + + void Execute() { + char bcrypted[_PASSWORD_LEN]; + if (ValidateSalt(encrypted.c_str())) { + bcrypt(input.c_str(), input.length(), encrypted.c_str(), bcrypted); + result = CompareStrings(bcrypted, encrypted.c_str()); + } + } + + void OnOK() { + Napi::HandleScope scope(Env()); + Callback().Call({Env().Undefined(), Napi::Boolean::New(Env(), result)}); + } + + private: + std::string input; + std::string encrypted; + bool result; + }; + + Napi::Value Compare(const Napi::CallbackInfo& info) { + if (info.Length() < 3) { + throw Napi::TypeError::New(info.Env(), "3 arguments expected"); + } + std::string input = info[0].IsBuffer() + ? BufferToString(info[0].As>()) + : info[0].As(); + std::string encrypted = info[1].As(); + Napi::Function callback = info[2].As(); + CompareAsyncWorker* compareWorker = new CompareAsyncWorker(callback, input, encrypted); + compareWorker->Queue(); + return info.Env().Undefined(); + } + + Napi::Value CompareSync(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + if (info.Length() < 2) { + throw Napi::TypeError::New(info.Env(), "2 arguments expected"); + } + std::string pw = info[0].IsBuffer() + ? BufferToString(info[0].As>()) + : info[0].As(); + std::string hash = info[1].As(); + char bcrypted[_PASSWORD_LEN]; + if (ValidateSalt(hash.c_str())) { + bcrypt(pw.c_str(), pw.length(), hash.c_str(), bcrypted); + return Napi::Boolean::New(env, CompareStrings(bcrypted, hash.c_str())); + } else { + return Napi::Boolean::New(env, false); + } + } + + Napi::Value GetRounds(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + if (info.Length() < 1) { + throw Napi::TypeError::New(env, "1 argument expected"); + } + std::string hash = info[0].As(); + u_int32_t rounds; + if (!(rounds = bcrypt_get_rounds(hash.c_str()))) { + throw Napi::Error::New(env, "invalid hash provided"); + } + return Napi::Number::New(env, rounds); + } + +} // anonymous namespace + +Napi::Object init(Napi::Env env, Napi::Object exports) { + exports.Set(Napi::String::New(env, "gen_salt_sync"), Napi::Function::New(env, GenerateSaltSync)); + exports.Set(Napi::String::New(env, "encrypt_sync"), Napi::Function::New(env, EncryptSync)); + exports.Set(Napi::String::New(env, "compare_sync"), Napi::Function::New(env, CompareSync)); + exports.Set(Napi::String::New(env, "get_rounds"), Napi::Function::New(env, GetRounds)); + exports.Set(Napi::String::New(env, "gen_salt"), Napi::Function::New(env, GenerateSalt)); + exports.Set(Napi::String::New(env, "encrypt"), Napi::Function::New(env, Encrypt)); + exports.Set(Napi::String::New(env, "compare"), Napi::Function::New(env, Compare)); + return exports; +} + +NODE_API_MODULE(NODE_GYP_MODULE_NAME, init) diff --git a/node_modules/bcrypt/src/blowfish.cc b/node_modules/bcrypt/src/blowfish.cc new file mode 100644 index 0000000..1fc6cf1 --- /dev/null +++ b/node_modules/bcrypt/src/blowfish.cc @@ -0,0 +1,679 @@ +/* $OpenBSD: blowfish.c,v 1.18 2004/11/02 17:23:26 hshoexer Exp $ */ +/* + * Blowfish block cipher for OpenBSD + * Copyright 1997 Niels Provos + * All rights reserved. + * + * Implementation advice by David Mazieres . + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * This product includes software developed by Niels Provos. + * 4. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * This code is derived from section 14.3 and the given source + * in section V of Applied Cryptography, second edition. + * Blowfish is an unpatented fast block cipher designed by + * Bruce Schneier. + */ + +#include "node_blf.h" + +#undef inline +#ifdef __GNUC__ +#define inline __inline +#else /* !__GNUC__ */ +#define inline +#endif /* !__GNUC__ */ + +/* Function for Feistel Networks */ + +#define F(s, x) ((((s)[ (((x)>>24)&0xFF)] \ + + (s)[0x100 + (((x)>>16)&0xFF)]) \ + ^ (s)[0x200 + (((x)>> 8)&0xFF)]) \ + + (s)[0x300 + ( (x) &0xFF)]) + +#define BLFRND(s,p,i,j,n) (i ^= F(s,j) ^ (p)[n]) + +void +Blowfish_encipher(blf_ctx *c, u_int32_t *xl, u_int32_t *xr) +{ + u_int32_t Xl; + u_int32_t Xr; + u_int32_t *s = c->S[0]; + u_int32_t *p = c->P; + + Xl = *xl; + Xr = *xr; + + Xl ^= p[0]; + BLFRND(s, p, Xr, Xl, 1); BLFRND(s, p, Xl, Xr, 2); + BLFRND(s, p, Xr, Xl, 3); BLFRND(s, p, Xl, Xr, 4); + BLFRND(s, p, Xr, Xl, 5); BLFRND(s, p, Xl, Xr, 6); + BLFRND(s, p, Xr, Xl, 7); BLFRND(s, p, Xl, Xr, 8); + BLFRND(s, p, Xr, Xl, 9); BLFRND(s, p, Xl, Xr, 10); + BLFRND(s, p, Xr, Xl, 11); BLFRND(s, p, Xl, Xr, 12); + BLFRND(s, p, Xr, Xl, 13); BLFRND(s, p, Xl, Xr, 14); + BLFRND(s, p, Xr, Xl, 15); BLFRND(s, p, Xl, Xr, 16); + + *xl = Xr ^ p[17]; + *xr = Xl; +} + +void +Blowfish_decipher(blf_ctx *c, u_int32_t *xl, u_int32_t *xr) +{ + u_int32_t Xl; + u_int32_t Xr; + u_int32_t *s = c->S[0]; + u_int32_t *p = c->P; + + Xl = *xl; + Xr = *xr; + + Xl ^= p[17]; + BLFRND(s, p, Xr, Xl, 16); BLFRND(s, p, Xl, Xr, 15); + BLFRND(s, p, Xr, Xl, 14); BLFRND(s, p, Xl, Xr, 13); + BLFRND(s, p, Xr, Xl, 12); BLFRND(s, p, Xl, Xr, 11); + BLFRND(s, p, Xr, Xl, 10); BLFRND(s, p, Xl, Xr, 9); + BLFRND(s, p, Xr, Xl, 8); BLFRND(s, p, Xl, Xr, 7); + BLFRND(s, p, Xr, Xl, 6); BLFRND(s, p, Xl, Xr, 5); + BLFRND(s, p, Xr, Xl, 4); BLFRND(s, p, Xl, Xr, 3); + BLFRND(s, p, Xr, Xl, 2); BLFRND(s, p, Xl, Xr, 1); + + *xl = Xr ^ p[0]; + *xr = Xl; +} + +void +Blowfish_initstate(blf_ctx *c) +{ + /* P-box and S-box tables initialized with digits of Pi */ + + static const blf_ctx initstate = + { { + { + 0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, + 0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99, + 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, + 0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e, + 0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee, + 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, + 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, + 0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e, + 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, + 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, + 0x55ca396a, 0x2aab10b6, 0xb4cc5c34, 0x1141e8ce, + 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a, + 0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, + 0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677, + 0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193, + 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, + 0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88, + 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239, + 0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, + 0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0, + 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, + 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98, + 0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88, + 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe, + 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, + 0x4ed3aa62, 0x363f7706, 0x1bfedf72, 0x429b023d, + 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, + 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, + 0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba, + 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463, + 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, + 0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09, + 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, + 0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, + 0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279, + 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8, + 0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, + 0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82, + 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, + 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, + 0x695b27b0, 0xbbca58c8, 0xe1ffa35d, 0xb8f011a0, + 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, + 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, + 0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8, + 0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4, + 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, + 0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7, + 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c, + 0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, + 0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1, + 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, + 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9, + 0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477, + 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf, + 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, + 0x00250e2d, 0x2071b35e, 0x226800bb, 0x57b8e0af, + 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, + 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, + 0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41, + 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915, + 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, + 0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915, + 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, + 0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a}, + { + 0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, + 0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266, + 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1, + 0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e, + 0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6, + 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, + 0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e, + 0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1, + 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, + 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, + 0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff, + 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd, + 0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, + 0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7, + 0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41, + 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, + 0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf, + 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af, + 0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, + 0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87, + 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, + 0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2, + 0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16, + 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd, + 0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, + 0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509, + 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, + 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, + 0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f, + 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a, + 0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, + 0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960, + 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66, + 0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, + 0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802, + 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84, + 0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510, + 0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf, + 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, + 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e, + 0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50, + 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, + 0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, + 0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281, + 0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99, + 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, + 0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128, + 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73, + 0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, + 0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0, + 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, + 0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250, + 0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3, + 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285, + 0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, + 0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061, + 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, + 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e, + 0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735, + 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc, + 0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, + 0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340, + 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20, + 0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7}, + { + 0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, + 0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068, + 0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af, + 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, + 0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45, + 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504, + 0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, + 0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb, + 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, + 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, + 0xaace1e7c, 0xd3375fec, 0xce78a399, 0x406b2a42, + 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b, + 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, + 0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb, + 0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, + 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, + 0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33, + 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c, + 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, + 0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc, + 0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17, + 0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564, + 0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b, + 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115, + 0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, + 0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 0x2da2f728, + 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, + 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, + 0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37, + 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d, + 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, + 0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b, + 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3, + 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, + 0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d, + 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c, + 0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, + 0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9, + 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a, + 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, + 0x9dbc8057, 0xf0f7c086, 0x60787bf8, 0x6003604d, + 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc, + 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, + 0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61, + 0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, + 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, + 0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2, + 0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c, + 0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, + 0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633, + 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, + 0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169, + 0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52, + 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027, + 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, + 0xf0177a28, 0xc0f586e0, 0x006058aa, 0x30dc7d62, + 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, + 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, + 0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24, + 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc, + 0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, + 0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c, + 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837, + 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0}, + { + 0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, + 0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe, + 0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b, + 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, + 0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8, + 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6, + 0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304, + 0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22, + 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, + 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6, + 0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9, + 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59, + 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, + 0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, 0x022b8b51, + 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, + 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, + 0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b, + 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28, + 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, + 0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd, + 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, + 0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319, + 0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb, + 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f, + 0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, + 0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32, + 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, + 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, + 0xb39a460a, 0x6445c0dd, 0x586cdecf, 0x1c20c8ae, + 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, + 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, + 0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47, + 0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370, + 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, + 0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84, + 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048, + 0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8, + 0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd, + 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, + 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7, + 0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38, + 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f, + 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, + 0xbf97222c, 0x15e6fc2a, 0x0f91fc71, 0x9b941525, + 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, + 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, + 0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964, + 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e, + 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, + 0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d, + 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, + 0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, + 0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02, + 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, + 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, + 0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a, + 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, + 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, + 0x53113ec0, 0x1640e3d3, 0x38abbd60, 0x2547adf0, + 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060, + 0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, + 0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9, + 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f, + 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6} + }, + { + 0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, + 0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89, + 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, + 0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917, + 0x9216d5d9, 0x8979fb1b + } }; + + *c = initstate; +} + +u_int32_t +Blowfish_stream2word(const u_int8_t *data, u_int16_t databytes, + u_int16_t *current) +{ + u_int8_t i; + u_int16_t j; + u_int32_t temp; + + temp = 0x00000000; + j = *current; + + for (i = 0; i < 4; i++, j++) { + if (j >= databytes) + j = 0; + temp = (temp << 8) | data[j]; + } + + *current = j; + return temp; +} + +void +Blowfish_expand0state(blf_ctx *c, const u_int8_t *key, u_int16_t keybytes) +{ + u_int16_t i; + u_int16_t j; + u_int16_t k; + u_int32_t temp; + u_int32_t datal; + u_int32_t datar; + + j = 0; + for (i = 0; i < BLF_N + 2; i++) { + /* Extract 4 int8 to 1 int32 from keystream */ + temp = Blowfish_stream2word(key, keybytes, &j); + c->P[i] = c->P[i] ^ temp; + } + + j = 0; + datal = 0x00000000; + datar = 0x00000000; + for (i = 0; i < BLF_N + 2; i += 2) { + Blowfish_encipher(c, &datal, &datar); + + c->P[i] = datal; + c->P[i + 1] = datar; + } + + for (i = 0; i < 4; i++) { + for (k = 0; k < 256; k += 2) { + Blowfish_encipher(c, &datal, &datar); + + c->S[i][k] = datal; + c->S[i][k + 1] = datar; + } + } +} + + +void +Blowfish_expandstate(blf_ctx *c, const u_int8_t *data, u_int16_t databytes, + const u_int8_t *key, u_int16_t keybytes) +{ + u_int16_t i; + u_int16_t j; + u_int16_t k; + u_int32_t temp; + u_int32_t datal; + u_int32_t datar; + + j = 0; + for (i = 0; i < BLF_N + 2; i++) { + /* Extract 4 int8 to 1 int32 from keystream */ + temp = Blowfish_stream2word(key, keybytes, &j); + c->P[i] = c->P[i] ^ temp; + } + + j = 0; + datal = 0x00000000; + datar = 0x00000000; + for (i = 0; i < BLF_N + 2; i += 2) { + datal ^= Blowfish_stream2word(data, databytes, &j); + datar ^= Blowfish_stream2word(data, databytes, &j); + Blowfish_encipher(c, &datal, &datar); + + c->P[i] = datal; + c->P[i + 1] = datar; + } + + for (i = 0; i < 4; i++) { + for (k = 0; k < 256; k += 2) { + datal ^= Blowfish_stream2word(data, databytes, &j); + datar ^= Blowfish_stream2word(data, databytes, &j); + Blowfish_encipher(c, &datal, &datar); + + c->S[i][k] = datal; + c->S[i][k + 1] = datar; + } + } + +} + +void +blf_key(blf_ctx *c, const u_int8_t *k, u_int16_t len) +{ + /* Initialize S-boxes and subkeys with Pi */ + Blowfish_initstate(c); + + /* Transform S-boxes and subkeys with key */ + Blowfish_expand0state(c, k, len); +} + +void +blf_enc(blf_ctx *c, u_int32_t *data, u_int16_t blocks) +{ + u_int32_t *d; + u_int16_t i; + + d = data; + for (i = 0; i < blocks; i++) { + Blowfish_encipher(c, d, d + 1); + d += 2; + } +} + +void +blf_dec(blf_ctx *c, u_int32_t *data, u_int16_t blocks) +{ + u_int32_t *d; + u_int16_t i; + + d = data; + for (i = 0; i < blocks; i++) { + Blowfish_decipher(c, d, d + 1); + d += 2; + } +} + +void +blf_ecb_encrypt(blf_ctx *c, u_int8_t *data, u_int32_t len) +{ + u_int32_t l, r; + u_int32_t i; + + for (i = 0; i < len; i += 8) { + l = data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3]; + r = data[4] << 24 | data[5] << 16 | data[6] << 8 | data[7]; + Blowfish_encipher(c, &l, &r); + data[0] = l >> 24 & 0xff; + data[1] = l >> 16 & 0xff; + data[2] = l >> 8 & 0xff; + data[3] = l & 0xff; + data[4] = r >> 24 & 0xff; + data[5] = r >> 16 & 0xff; + data[6] = r >> 8 & 0xff; + data[7] = r & 0xff; + data += 8; + } +} + +void +blf_ecb_decrypt(blf_ctx *c, u_int8_t *data, u_int32_t len) +{ + u_int32_t l, r; + u_int32_t i; + + for (i = 0; i < len; i += 8) { + l = data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3]; + r = data[4] << 24 | data[5] << 16 | data[6] << 8 | data[7]; + Blowfish_decipher(c, &l, &r); + data[0] = l >> 24 & 0xff; + data[1] = l >> 16 & 0xff; + data[2] = l >> 8 & 0xff; + data[3] = l & 0xff; + data[4] = r >> 24 & 0xff; + data[5] = r >> 16 & 0xff; + data[6] = r >> 8 & 0xff; + data[7] = r & 0xff; + data += 8; + } +} + +void +blf_cbc_encrypt(blf_ctx *c, u_int8_t *iv, u_int8_t *data, u_int32_t len) +{ + u_int32_t l, r; + u_int32_t i, j; + + for (i = 0; i < len; i += 8) { + for (j = 0; j < 8; j++) + data[j] ^= iv[j]; + l = data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3]; + r = data[4] << 24 | data[5] << 16 | data[6] << 8 | data[7]; + Blowfish_encipher(c, &l, &r); + data[0] = l >> 24 & 0xff; + data[1] = l >> 16 & 0xff; + data[2] = l >> 8 & 0xff; + data[3] = l & 0xff; + data[4] = r >> 24 & 0xff; + data[5] = r >> 16 & 0xff; + data[6] = r >> 8 & 0xff; + data[7] = r & 0xff; + iv = data; + data += 8; + } +} + +void +blf_cbc_decrypt(blf_ctx *c, u_int8_t *iva, u_int8_t *data, u_int32_t len) +{ + u_int32_t l, r; + u_int8_t *iv; + u_int32_t i, j; + + iv = data + len - 16; + data = data + len - 8; + for (i = len - 8; i >= 8; i -= 8) { + l = data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3]; + r = data[4] << 24 | data[5] << 16 | data[6] << 8 | data[7]; + Blowfish_decipher(c, &l, &r); + data[0] = l >> 24 & 0xff; + data[1] = l >> 16 & 0xff; + data[2] = l >> 8 & 0xff; + data[3] = l & 0xff; + data[4] = r >> 24 & 0xff; + data[5] = r >> 16 & 0xff; + data[6] = r >> 8 & 0xff; + data[7] = r & 0xff; + for (j = 0; j < 8; j++) + data[j] ^= iv[j]; + iv -= 8; + data -= 8; + } + l = data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3]; + r = data[4] << 24 | data[5] << 16 | data[6] << 8 | data[7]; + Blowfish_decipher(c, &l, &r); + data[0] = l >> 24 & 0xff; + data[1] = l >> 16 & 0xff; + data[2] = l >> 8 & 0xff; + data[3] = l & 0xff; + data[4] = r >> 24 & 0xff; + data[5] = r >> 16 & 0xff; + data[6] = r >> 8 & 0xff; + data[7] = r & 0xff; + for (j = 0; j < 8; j++) + data[j] ^= iva[j]; +} + +#if 0 +void +report(u_int32_t data[], u_int16_t len) +{ + u_int16_t i; + for (i = 0; i < len; i += 2) + printf("Block %0hd: %08lx %08lx.\n", + i / 2, data[i], data[i + 1]); +} +void +main(void) +{ + + blf_ctx c; + char key[] = "AAAAA"; + char key2[] = "abcdefghijklmnopqrstuvwxyz"; + + u_int32_t data[10]; + u_int32_t data2[] = + {0x424c4f57l, 0x46495348l}; + + u_int16_t i; + + /* First test */ + for (i = 0; i < 10; i++) + data[i] = i; + + blf_key(&c, (u_int8_t *) key, 5); + blf_enc(&c, data, 5); + blf_dec(&c, data, 1); + blf_dec(&c, data + 2, 4); + printf("Should read as 0 - 9.\n"); + report(data, 10); + + /* Second test */ + blf_key(&c, (u_int8_t *) key2, strlen(key2)); + blf_enc(&c, data2, 1); + printf("\nShould read as: 0x324ed0fe 0xf413a203.\n"); + report(data2, 2); + blf_dec(&c, data2, 1); + report(data2, 2); +} +#endif diff --git a/node_modules/bcrypt/src/node_blf.h b/node_modules/bcrypt/src/node_blf.h new file mode 100644 index 0000000..2d50a39 --- /dev/null +++ b/node_modules/bcrypt/src/node_blf.h @@ -0,0 +1,132 @@ +/* $OpenBSD: blf.h,v 1.7 2007/03/14 17:59:41 grunk Exp $ */ +/* + * Blowfish - a fast block cipher designed by Bruce Schneier + * + * Copyright 1997 Niels Provos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * This product includes software developed by Niels Provos. + * 4. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef _NODE_BLF_H_ +#define _NODE_BLF_H_ + +#include + +/* Solaris compatibility */ +#ifdef __sun +#define u_int8_t uint8_t +#define u_int16_t uint16_t +#define u_int32_t uint32_t +#define u_int64_t uint64_t +#endif + +#ifdef _WIN32 +#define u_int8_t unsigned __int8 +#define u_int16_t unsigned __int16 +#define u_int32_t unsigned __int32 +#define u_int64_t unsigned __int64 +#endif + +/* Windows ssize_t compatibility */ +#if defined(_WIN32) || defined(_WIN64) +# if defined(_WIN64) + typedef __int64 LONG_PTR; +# else + typedef long LONG_PTR; +# endif + typedef LONG_PTR SSIZE_T; + typedef SSIZE_T ssize_t; +#endif + +/* z/OS compatibility */ +#ifdef __MVS__ +typedef unsigned char u_int8_t; +typedef unsigned short u_int16_t; +typedef unsigned int u_int32_t; +typedef unsigned long long u_int64_t; +#endif + +#define BCRYPT_VERSION '2' +#define BCRYPT_MAXSALT 16 /* Precomputation is just so nice */ +#define BCRYPT_BLOCKS 6 /* Ciphertext blocks */ +#define BCRYPT_MINROUNDS 16 /* we have log2(rounds) in salt */ + +/* Schneier specifies a maximum key length of 56 bytes. + * This ensures that every key bit affects every cipher + * bit. However, the subkeys can hold up to 72 bytes. + * Warning: For normal blowfish encryption only 56 bytes + * of the key affect all cipherbits. + */ + +#define BLF_N 16 /* Number of Subkeys */ +#define BLF_MAXKEYLEN ((BLF_N-2)*4) /* 448 bits */ +#define BLF_MAXUTILIZED ((BLF_N+2)*4) /* 576 bits */ + +#define _PASSWORD_LEN 128 /* max length, not counting NUL */ +#define _SALT_LEN 32 /* max length */ + +/* Blowfish context */ +typedef struct BlowfishContext { + u_int32_t S[4][256]; /* S-Boxes */ + u_int32_t P[BLF_N + 2]; /* Subkeys */ +} blf_ctx; + +/* Raw access to customized Blowfish + * blf_key is just: + * Blowfish_initstate( state ) + * Blowfish_expand0state( state, key, keylen ) + */ + +void Blowfish_encipher(blf_ctx *, u_int32_t *, u_int32_t *); +void Blowfish_decipher(blf_ctx *, u_int32_t *, u_int32_t *); +void Blowfish_initstate(blf_ctx *); +void Blowfish_expand0state(blf_ctx *, const u_int8_t *, u_int16_t); +void Blowfish_expandstate +(blf_ctx *, const u_int8_t *, u_int16_t, const u_int8_t *, u_int16_t); + +/* Standard Blowfish */ + +void blf_key(blf_ctx *, const u_int8_t *, u_int16_t); +void blf_enc(blf_ctx *, u_int32_t *, u_int16_t); +void blf_dec(blf_ctx *, u_int32_t *, u_int16_t); + +void blf_ecb_encrypt(blf_ctx *, u_int8_t *, u_int32_t); +void blf_ecb_decrypt(blf_ctx *, u_int8_t *, u_int32_t); + +void blf_cbc_encrypt(blf_ctx *, u_int8_t *, u_int8_t *, u_int32_t); +void blf_cbc_decrypt(blf_ctx *, u_int8_t *, u_int8_t *, u_int32_t); + +/* Converts u_int8_t to u_int32_t */ +u_int32_t Blowfish_stream2word(const u_int8_t *, u_int16_t , u_int16_t *); + +/* bcrypt functions*/ +void bcrypt_gensalt(char, u_int8_t, u_int8_t*, char *); +void bcrypt(const char *, size_t key_len, const char *, char *); +void encode_salt(char *, u_int8_t *, char, u_int16_t, u_int8_t); +u_int32_t bcrypt_get_rounds(const char *); + +#endif diff --git a/node_modules/bcrypt/test/async.test.js b/node_modules/bcrypt/test/async.test.js new file mode 100644 index 0000000..fb59367 --- /dev/null +++ b/node_modules/bcrypt/test/async.test.js @@ -0,0 +1,209 @@ +const bcrypt = require('../bcrypt'); + +test('salt_length', done => { + expect.assertions(1); + bcrypt.genSalt(10, function (err, salt) { + expect(salt).toHaveLength(29); + done(); + }); +}) + +test('salt_only_cb', () => { + expect.assertions(1); + expect(() => { + bcrypt.genSalt((err, salt) => { + }); + }).not.toThrow(); +}) + +test('salt_rounds_is_string_number', done => { + expect.assertions(2); + bcrypt.genSalt('10', void 0, function (err, salt) { + expect(err instanceof Error).toBe(true) + expect(err.message).toBe('rounds must be a number') + done(); + }); +}) + +test('salt_rounds_is_string_non_number', done => { + expect.assertions(2); + bcrypt.genSalt('z', function (err, salt) { + expect(err instanceof Error).toBe(true) + expect(err.message).toBe('rounds must be a number') + done(); + }); +}) + +test('salt_minor', done => { + expect.assertions(3); + bcrypt.genSalt(10, 'a', function (err, value) { + expect(value).toHaveLength(29); + const [_, minor, salt] = value.split('$'); + expect(minor).toEqual('2a'); + expect(salt).toEqual('10'); + done(); + }); +}) + +test('salt_minor_b', done => { + expect.assertions(3); + bcrypt.genSalt(10, 'b', function (err, value) { + expect(value).toHaveLength(29); + const [_, minor, salt] = value.split('$'); + expect(minor).toEqual('2b'); + expect(salt).toEqual('10'); + done(); + }); +}) + +test('hash', done => { + expect.assertions(2); + bcrypt.genSalt(10, function (err, salt) { + bcrypt.hash('password', salt, function (err, res) { + expect(res).toBeDefined(); + expect(err).toBeUndefined(); + done(); + }); + }); +}) + +test('hash_rounds', done => { + expect.assertions(1); + bcrypt.hash('bacon', 8, function (err, hash) { + expect(bcrypt.getRounds(hash)).toEqual(8); + done(); + }); +}) + +test('hash_empty_strings', done => { + expect.assertions(1); + bcrypt.genSalt(10, function (err, salt) { + bcrypt.hash('', salt, function (err, res) { + expect(res).toBeDefined(); + done(); + }); + }); +}) + +test('hash_fails_with_empty_salt', done => { + expect.assertions(1); + bcrypt.hash('', '', function (err, res) { + expect(err.message).toBe('Invalid salt. Salt must be in the form of: $Vers$log2(NumRounds)$saltvalue') + done(); + }); +}) + +test('hash_no_params', done => { + expect.assertions(1); + bcrypt.hash(function (err, hash) { + expect(err.message).toBe('data must be a string or Buffer and salt must either be a salt string or a number of rounds') + done(); + }); +}) + +test('hash_one_param', done => { + expect.assertions(1); + bcrypt.hash('password', function (err, hash) { + expect(err.message).toBe('data must be a string or Buffer and salt must either be a salt string or a number of rounds'); + done(); + }); +}) + +test('hash_salt_validity', done => { + expect.assertions(2); + bcrypt.hash('password', '$2a$10$somesaltyvaluertsetrse', function (err, enc) { + expect(err).toBeUndefined(); + bcrypt.hash('password', 'some$value', function (err, enc) { + expect(err.message).toBe("Invalid salt. Salt must be in the form of: $Vers$log2(NumRounds)$saltvalue"); + done(); + }); + }); +}) + +test('verify_salt', done => { + expect.assertions(2); + bcrypt.genSalt(10, function (err, value) { + const [_, version, rounds] = value.split('$'); + expect(version).toEqual('2b'); + expect(rounds).toEqual('10'); + done(); + }); +}) + +test('verify_salt_min_rounds', done => { + expect.assertions(2); + bcrypt.genSalt(1, function (err, value) { + const [_, version, rounds] = value.split('$'); + expect(version).toEqual('2b'); + expect(rounds).toEqual('04'); + done(); + }); +}) + +test('verify_salt_max_rounds', done => { + expect.assertions(2); + bcrypt.genSalt(100, function (err, value) { + const [_, version, rounds] = value.split('$'); + expect(version).toEqual('2b'); + expect(rounds).toEqual('31'); + done(); + }); +}) + +test('hash_compare', done => { + expect.assertions(2); + bcrypt.genSalt(10, function (err, salt) { + bcrypt.hash("test", salt, function (err, hash) { + bcrypt.compare("test", hash, function (err, res) { + expect(hash).toBeDefined(); + bcrypt.compare("blah", hash, function (err, res) { + expect(res).toBe(false); + done(); + }); + }); + }); + }); +}) + +test('hash_compare_empty_strings', done => { + expect.assertions(2); + const hash = bcrypt.hashSync("test", bcrypt.genSaltSync(10)); + + bcrypt.compare("", hash, function (err, res) { + expect(res).toEqual(false) + bcrypt.compare("", "", function (err, res) { + expect(res).toEqual(false); + done(); + }); + }); +}) + +test('hash_compare_invalid_strings', done => { + expect.assertions(2); + const fullString = 'envy1362987212538'; + const hash = '$2a$10$XOPbrlUPQdwdJUpSrIF6X.LbE14qsMmKGhM1A8W9iqaG3vv1BD7WC'; + const wut = ':'; + bcrypt.compare(fullString, hash, function (err, res) { + expect(res).toBe(true); + bcrypt.compare(fullString, wut, function (err, res) { + expect(res).toBe(false); + done(); + }); + }); +}) + +test('compare_no_params', done => { + expect.assertions(1); + bcrypt.compare(function (err, hash) { + expect(err.message).toBe('data and hash arguments required'); + done(); + }); +}) + +test('hash_compare_one_param', done => { + expect.assertions(1); + bcrypt.compare('password', function (err, hash) { + expect(err.message).toBe('data and hash arguments required'); + done(); + }); +}) diff --git a/node_modules/bcrypt/test/implementation.test.js b/node_modules/bcrypt/test/implementation.test.js new file mode 100644 index 0000000..647f32a --- /dev/null +++ b/node_modules/bcrypt/test/implementation.test.js @@ -0,0 +1,48 @@ +const bcrypt = require('../bcrypt'); + +// some tests were adapted from https://github.com/riverrun/bcrypt_elixir/blob/master/test/base_test.exs +// which are under the BSD LICENSE + +test('openwall', () => { + expect(bcrypt.hashSync("U*U", "$2a$05$CCCCCCCCCCCCCCCCCCCCC.")).toStrictEqual("$2a$05$CCCCCCCCCCCCCCCCCCCCC.E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW"); + expect(bcrypt.hashSync("U*U*", "$2a$05$CCCCCCCCCCCCCCCCCCCCC.")).toStrictEqual("$2a$05$CCCCCCCCCCCCCCCCCCCCC.VGOzA784oUp/Z0DY336zx7pLYAy0lwK"); + expect(bcrypt.hashSync("U*U*U", "$2a$05$XXXXXXXXXXXXXXXXXXXXXO")).toStrictEqual("$2a$05$XXXXXXXXXXXXXXXXXXXXXOAcXxm9kjPGEMsLznoKqmqw7tc8WCx4a"); + expect(bcrypt.hashSync("", "$2a$05$CCCCCCCCCCCCCCCCCCCCC.")).toStrictEqual("$2a$05$CCCCCCCCCCCCCCCCCCCCC.7uG0VCzI2bS7j6ymqJi9CdcdxiRTWNy"); + expect(bcrypt.hashSync("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", "$2a$05$abcdefghijklmnopqrstuu")).toStrictEqual("$2a$05$abcdefghijklmnopqrstuu5s2v8.iXieOjg/.AySBTTZIIVFJeBui"); +}) + +test('openbsd', () => { + expect(bcrypt.hashSync("000000000000000000000000000000000000000000000000000000000000000000000000", "$2a$05$CCCCCCCCCCCCCCCCCCCCC.")).toStrictEqual("$2a$05$CCCCCCCCCCCCCCCCCCCCC.6.O1dLNbjod2uo0DVcW.jHucKbPDdHS") + expect(bcrypt.hashSync("000000000000000000000000000000000000000000000000000000000000000000000000", "$2b$05$CCCCCCCCCCCCCCCCCCCCC.")).toStrictEqual("$2b$05$CCCCCCCCCCCCCCCCCCCCC.6.O1dLNbjod2uo0DVcW.jHucKbPDdHS") +}) + +test('long_passwords', () => { + // bcrypt wrap-around bug in $2a$ + expect(bcrypt.hashSync("012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234", "$2a$05$CCCCCCCCCCCCCCCCCCCCC.")).toStrictEqual("$2a$05$CCCCCCCCCCCCCCCCCCCCC.6.O1dLNbjod2uo0DVcW.jHucKbPDdHS") + expect(bcrypt.hashSync("01XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "$2a$05$CCCCCCCCCCCCCCCCCCCCC.")).toStrictEqual("$2a$05$CCCCCCCCCCCCCCCCCCCCC.6.O1dLNbjod2uo0DVcW.jHucKbPDdHS") + + // tests for $2b$ which fixes wrap-around bugs + expect(bcrypt.hashSync("012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234", "$2b$05$CCCCCCCCCCCCCCCCCCCCC.")).toStrictEqual("$2b$05$CCCCCCCCCCCCCCCCCCCCC.XxrQqgBi/5Sxuq9soXzDtjIZ7w5pMfK") + expect(bcrypt.hashSync("0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345", "$2b$05$CCCCCCCCCCCCCCCCCCCCC.")).toStrictEqual("$2b$05$CCCCCCCCCCCCCCCCCCCCC.XxrQqgBi/5Sxuq9soXzDtjIZ7w5pMfK") +}) + +test('embedded_nulls', () => { + expect(bcrypt.hashSync("Passw\0rd123", "$2b$05$CCCCCCCCCCCCCCCCCCCCC.")).toStrictEqual("$2b$05$CCCCCCCCCCCCCCCCCCCCC.VHy/kzL4sCcX3Ib3wN5rNGiRt.TpfxS") + expect(bcrypt.hashSync("Passw\0 you can literally write anything after the NUL character", "$2b$05$CCCCCCCCCCCCCCCCCCCCC.")).toStrictEqual("$2b$05$CCCCCCCCCCCCCCCCCCCCC.4vJLJQ6nZ/70INTjjSZWQ0iyUek92tu") + expect(bcrypt.hashSync(Buffer.from("Passw\0 you can literally write anything after the NUL character"), "$2b$05$CCCCCCCCCCCCCCCCCCCCC.")).toStrictEqual("$2b$05$CCCCCCCCCCCCCCCCCCCCC.4vJLJQ6nZ/70INTjjSZWQ0iyUek92tu") +}) + +test('shorten_salt_to_128_bits', () => { + expect(bcrypt.hashSync("test", "$2a$10$1234567899123456789012")).toStrictEqual("$2a$10$123456789912345678901u.OtL1A1eGK5wmvBKUDYKvuVKI7h2XBu") + expect(bcrypt.hashSync("U*U*", "$2a$05$CCCCCCCCCCCCCCCCCCCCCh")).toStrictEqual("$2a$05$CCCCCCCCCCCCCCCCCCCCCeUQ7VjYZ2hd4bLYZdhuPpZMUpEUJDw1S") + expect(bcrypt.hashSync("U*U*", "$2a$05$CCCCCCCCCCCCCCCCCCCCCM")).toStrictEqual("$2a$05$CCCCCCCCCCCCCCCCCCCCC.VGOzA784oUp/Z0DY336zx7pLYAy0lwK") + expect(bcrypt.hashSync("U*U*", "$2a$05$CCCCCCCCCCCCCCCCCCCCCA")).toStrictEqual("$2a$05$CCCCCCCCCCCCCCCCCCCCC.VGOzA784oUp/Z0DY336zx7pLYAy0lwK") +}) + +test('consistency', () => { + expect(bcrypt.hashSync("ππππππππ", "$2a$10$.TtQJ4Jr6isd4Hp.mVfZeu")).toStrictEqual("$2a$10$.TtQJ4Jr6isd4Hp.mVfZeuh6Gws4rOQ/vdBczhDx.19NFK0Y84Dle") + expect(bcrypt.hashSync("p@5sw0rd", "$2b$12$zQ4CooEXdGqcwi0PHsgc8e")).toStrictEqual("$2b$12$zQ4CooEXdGqcwi0PHsgc8eAf0DLXE/XHoBE8kCSGQ97rXwuClaPam") + expect(bcrypt.hashSync("C'est bon, la vie!", "$2b$12$cbo7LZ.wxgW4yxAA5Vqlv.")).toStrictEqual("$2b$12$cbo7LZ.wxgW4yxAA5Vqlv.KR6QFPt4qCdc9RYJNXxa/rbUOp.1sw.") + expect(bcrypt.hashSync("ἓν οἶδα ὅτι οὐδὲν οἶδα", "$2b$12$LeHKWR2bmrazi/6P22Jpau")).toStrictEqual("$2b$12$LeHKWR2bmrazi/6P22JpauX5my/eKwwKpWqL7L5iEByBnxNc76FRW") + expect(bcrypt.hashSync(Buffer.from("ἓν οἶδα ὅτι οὐδὲν οἶδα"), "$2b$12$LeHKWR2bmrazi/6P22Jpau")).toStrictEqual("$2b$12$LeHKWR2bmrazi/6P22JpauX5my/eKwwKpWqL7L5iEByBnxNc76FRW") +}) diff --git a/node_modules/bcrypt/test/promise.test.js b/node_modules/bcrypt/test/promise.test.js new file mode 100644 index 0000000..0103418 --- /dev/null +++ b/node_modules/bcrypt/test/promise.test.js @@ -0,0 +1,168 @@ +const bcrypt = require('../bcrypt'); +const promises = require('../promises'); + +test('salt_returns_promise_on_no_args', () => { + // make sure test passes with non-native implementations such as bluebird + // http://stackoverflow.com/questions/27746304/how-do-i-tell-if-an-object-is-a-promise + expect(typeof bcrypt.genSalt().then).toEqual('function') +}) + +test('salt_returns_promise_on_null_callback', () => { + expect(typeof bcrypt.genSalt(13, null, null).then).toEqual('function') +}) + +test('salt_length', () => { + return expect(bcrypt.genSalt(10)).resolves.toHaveLength(29); +}) + +test('salt_rounds_is_string_number', () => { + return expect(bcrypt.genSalt('10')).rejects.toThrow('rounds must be a number'); +}) + +test('salt_rounds_is_string_non_number', () => { + return expect(bcrypt.genSalt('b')).rejects.toThrow('rounds must be a number'); +}) + +test('hash_returns_promise_on_null_callback', () => { + expect(typeof bcrypt.hash('password', 10, null).then).toStrictEqual('function') +}) + +test('hash', () => { + return expect(bcrypt.genSalt(10) + .then(salt => bcrypt.hash('password', salt))).resolves.toBeDefined() +}) + +test('hash_rounds', () => { + return bcrypt.hash('bacon', 8).then(hash => { + expect(bcrypt.getRounds(hash)).toStrictEqual(8) + }); +}) + +test('hash_empty_strings', () => { + expect.assertions(2); + return Promise.all([ + expect(bcrypt.genSalt(10) + .then(salt => bcrypt.hash('', salt))) + .resolves.toBeDefined(), + expect(bcrypt.hash('', '')).rejects.toThrow(''), + ]); +}) + +test('hash_no_params', () => { + expect.assertions(1); + return expect(bcrypt.hash()).rejects.toThrow('data and salt arguments required'); +}) + +test('hash_one_param', () => { + return expect(bcrypt.hash('password')).rejects.toThrow('data and salt arguments required'); +}) + +test('hash_salt_validity', () => { + expect.assertions(2); + return Promise.all( + [ + expect(bcrypt.hash('password', '$2a$10$somesaltyvaluertsetrse')).resolves.toBeDefined(), + expect(bcrypt.hash('password', 'some$value')).rejects.toThrow("Invalid salt. Salt must be in the form of: $Vers$log2(NumRounds)$saltvalue") + ]); +}) + +test('verify_salt', () => { + expect.assertions(2); + return bcrypt.genSalt(10).then(result => { + const [_, version, salt] = result.split('$'); + expect(version).toEqual('2b') + expect(salt).toEqual('10') + }); +}) + +test('verify_salt_min_rounds', () => { + expect.assertions(2); + return bcrypt.genSalt(1).then(value => { + const [_, version, rounds] = value.split('$'); + expect(version).toEqual('2b'); + expect(rounds).toEqual('04'); + }); +}) + +test('verify_salt_max_rounds', () => { + expect.assertions(2); + return bcrypt.genSalt(100).then(value => { + const [_, version, rounds] = value.split('$'); + expect(version).toEqual('2b'); + expect(rounds).toEqual('31'); + }); +}) + +test('hash_compare_returns_promise_on_null_callback', () => { + expect(typeof bcrypt.compare('password', 'something', null).then).toStrictEqual('function') +}) + +test('hash_compare', () => { + expect.assertions(3); + return bcrypt.genSalt(10).then(function (salt) { + expect(salt).toHaveLength(29); + return bcrypt.hash("test", salt); + }).then(hash => Promise.all( + [ + expect(bcrypt.compare("test", hash)).resolves.toEqual(true), + expect(bcrypt.compare("blah", hash)).resolves.toEqual(false) + ])); +}) + +test('hash_compare_empty_strings', () => { + expect.assertions(2); + const hash = bcrypt.hashSync("test", bcrypt.genSaltSync(10)); + return Promise.all([ + expect(bcrypt.compare("", hash)).resolves.toEqual(false), + expect(bcrypt.compare("", "")).resolves.toEqual(false) + ]); +}) + +test('hash_compare_invalid_strings', () => { + const fullString = 'envy1362987212538'; + const hash = '$2a$10$XOPbrlUPQdwdJUpSrIF6X.LbE14qsMmKGhM1A8W9iqaG3vv1BD7WC'; + const wut = ':'; + return Promise.all([ + expect(bcrypt.compare(fullString, hash)).resolves.toEqual(true), + expect(bcrypt.compare(fullString, wut)).resolves.toEqual(false), + ]); +}) + +test('hash_compare_no_params', () => { + expect.assertions(1); + return expect(bcrypt.compare()).rejects.toThrow('data and hash arguments required') +}) + +test('hash_compare_one_param', () => { + expect.assertions(1); + return expect(bcrypt.compare('password')).rejects.toThrow('data and hash arguments required') +}) + +test('change_promise_impl_reject', () => { + + promises.use({ + reject: function () { + return 'mock'; + } + }); + + expect(promises.reject()).toEqual('mock'); + + // need to reset the promise implementation because of require cache + promises.use(global.Promise); +}) + +test('change_promise_impl_promise', () => { + + promises.use({ + reject: function (err) { + expect(err.message).toEqual('fn must be a function'); + return 'mock'; + } + }); + + expect(promises.promise('', '', '')).toEqual('mock'); + + // need to reset the promise implementation because of require cache + promises.use(global.Promise); +}) diff --git a/node_modules/bcrypt/test/repetitions.test.js b/node_modules/bcrypt/test/repetitions.test.js new file mode 100644 index 0000000..63ff407 --- /dev/null +++ b/node_modules/bcrypt/test/repetitions.test.js @@ -0,0 +1,55 @@ +const bcrypt = require('../bcrypt'); + +const EXPECTED = 2500; //number of times to iterate these tests.) +const { TEST_TIMEOUT_SECONDS } = process.env; +let timeout = 5e3; // default test timeout + +// it is necessary to increase the test timeout when emulating cross-architecture +// environments (i.e. arm64 from x86-64 host) which have significantly reduced performance: +if ( TEST_TIMEOUT_SECONDS ) + timeout = Number.parseInt(TEST_TIMEOUT_SECONDS, 10) * 1e3; + +jest.setTimeout(timeout); + +test('salt_length', () => { + expect.assertions(EXPECTED); + + return Promise.all(Array.from({length: EXPECTED}, + () => bcrypt.genSalt(10) + .then(salt => expect(salt).toHaveLength(29)))); +}) + +test('test_hash_length', () => { + expect.assertions(EXPECTED); + const SALT = '$2a$04$TnjywYklQbbZjdjBgBoA4e'; + return Promise.all(Array.from({length: EXPECTED}, + () => bcrypt.hash('test', SALT) + .then(hash => expect(hash).toHaveLength(60)))); +}) + +test('test_compare', () => { + expect.assertions(EXPECTED); + const HASH = '$2a$04$TnjywYklQbbZjdjBgBoA4e9G7RJt9blgMgsCvUvus4Iv4TENB5nHy'; + return Promise.all(Array.from({length: EXPECTED}, + () => bcrypt.compare('test', HASH) + .then(match => expect(match).toEqual(true)))); +}) + +test('test_hash_and_compare', () => { + expect.assertions(EXPECTED * 3); + const salt = bcrypt.genSaltSync(4) + + return Promise.all(Array.from({length: EXPECTED}, + () => { + const password = 'secret' + Math.random(); + return bcrypt.hash(password, salt) + .then(hash => { + expect(hash).toHaveLength(60); + const goodCompare = bcrypt.compare(password, hash).then(res => expect(res).toEqual(true)); + const badCompare = bcrypt.compare('bad' + password, hash).then(res => expect(res).toEqual(false)); + + return Promise.all([goodCompare, badCompare]); + }); + })); +}, timeout * 3); + diff --git a/node_modules/bcrypt/test/sync.test.js b/node_modules/bcrypt/test/sync.test.js new file mode 100644 index 0000000..2e6809a --- /dev/null +++ b/node_modules/bcrypt/test/sync.test.js @@ -0,0 +1,125 @@ +const bcrypt = require('../bcrypt') + +test('salt_length', () => { + const salt = bcrypt.genSaltSync(13); + expect(salt).toHaveLength(29); + const [_, version, rounds] = salt.split('$'); + expect(version).toStrictEqual('2b') + expect(rounds).toStrictEqual('13') +}) + +test('salt_no_params', () => { + const salt = bcrypt.genSaltSync(); + const [_, version, rounds] = salt.split('$'); + expect(version).toStrictEqual('2b') + expect(rounds).toStrictEqual('10') +}) + +test('salt_rounds_is_string_number', () => { + expect(() => bcrypt.genSaltSync('10')).toThrowError('rounds must be a number'); +}) + +test('salt_rounds_is_NaN', () => { + expect(() => bcrypt.genSaltSync('b')).toThrowError("rounds must be a number"); +}) + +test('salt_minor_a', () => { + const salt = bcrypt.genSaltSync(10, 'a'); + const [_, version, rounds] = salt.split('$'); + expect(version).toStrictEqual('2a') + expect(rounds).toStrictEqual('10') +}) + +test('salt_minor_b', () => { + const salt = bcrypt.genSaltSync(10, 'b'); + const [_, version, rounds] = salt.split('$'); + expect(version).toStrictEqual('2b') + expect(rounds).toStrictEqual('10') +}) + +test('hash', () => { + expect(() => bcrypt.hashSync('password', bcrypt.genSaltSync(10))).not.toThrow() +}) + +test('hash_rounds', () => { + const hash = bcrypt.hashSync('password', 8); + expect(bcrypt.getRounds(hash)).toStrictEqual(8) +}) + +test('hash_empty_string', () => { + expect(() => bcrypt.hashSync('', bcrypt.genSaltSync(10))).not.toThrow(); + expect(() => bcrypt.hashSync('password', '')).toThrowError('Invalid salt. Salt must be in the form of: $Vers$log2(NumRounds)$saltvalue'); + expect(() => bcrypt.hashSync('', '')).toThrowError('Invalid salt. Salt must be in the form of: $Vers$log2(NumRounds)$saltvalue'); +}) + +test('hash_pw_no_params', () => { + expect(() => bcrypt.hashSync()).toThrow('data and salt arguments required'); +}) + +test('hash_pw_one_param', () => { + expect(() => bcrypt.hashSync('password')).toThrow('data and salt arguments required'); +}) + +test('hash_pw_not_hash_str', () => { + expect(() => bcrypt.hashSync('password', {})).toThrow("data must be a string or Buffer and salt must either be a salt string or a number of rounds") +}) + +test('hash_salt_validity', () => { + expect(2); + expect(bcrypt.hashSync('password', '$2a$10$somesaltyvaluertsetrse')).toBeDefined() + expect(() => bcrypt.hashSync('password', 'some$value')).toThrow('Invalid salt. Salt must be in the form of: $Vers$log2(NumRounds)$saltvalue') +}) + +test('verify_salt', () => { + const salt = bcrypt.genSaltSync(10); + const split_salt = salt.split('$'); + expect(split_salt[1]).toStrictEqual('2b') + expect(split_salt[2]).toStrictEqual('10') +}) + +test('verify_salt_min_rounds', () => { + const salt = bcrypt.genSaltSync(1); + const split_salt = salt.split('$'); + expect(split_salt[1]).toStrictEqual('2b') + expect(split_salt[2]).toStrictEqual('04') +}) + +test('verify_salt_max_rounds', () => { + const salt = bcrypt.genSaltSync(100); + const split_salt = salt.split('$'); + expect(split_salt[1]).toStrictEqual('2b') + expect(split_salt[2]).toStrictEqual('31') +}) + +test('hash_compare', () => { + const salt = bcrypt.genSaltSync(10); + expect(29).toStrictEqual(salt.length) + const hash = bcrypt.hashSync("test", salt); + expect(bcrypt.compareSync("test", hash)).toBeDefined() + expect(!(bcrypt.compareSync("blah", hash))).toBeDefined() +}) + +test('hash_compare_empty_strings', () => { + expect(!(bcrypt.compareSync("", "password"))).toBeDefined() + expect(!(bcrypt.compareSync("", ""))).toBeDefined() + expect(!(bcrypt.compareSync("password", ""))).toBeDefined() +}) + +test('hash_compare_invalid_strings', () => { + const fullString = 'envy1362987212538'; + const hash = '$2a$10$XOPbrlUPQdwdJUpSrIF6X.LbE14qsMmKGhM1A8W9iqaG3vv1BD7WC'; + const wut = ':'; + expect(bcrypt.compareSync(fullString, hash)).toBe(true); + expect(bcrypt.compareSync(fullString, wut)).toBe(false); +}) + +test('getRounds', () => { + const hash = bcrypt.hashSync("test", bcrypt.genSaltSync(9)); + expect(9).toStrictEqual(bcrypt.getRounds(hash)) +}) + +test('getRounds', () => { + const hash = bcrypt.hashSync("test", bcrypt.genSaltSync(9)); + expect(9).toStrictEqual(bcrypt.getRounds(hash)) + expect(() => bcrypt.getRounds('')).toThrow("invalid hash provided"); +}); diff --git a/node_modules/crypto/README.md b/node_modules/crypto/README.md new file mode 100644 index 0000000..5437f14 --- /dev/null +++ b/node_modules/crypto/README.md @@ -0,0 +1,7 @@ +# Deprecated Package + +This package is no longer supported and has been deprecated. To avoid malicious use, npm is hanging on to the package name. + +It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in. + +Please contact support@npmjs.com if you have questions about this package. diff --git a/node_modules/crypto/package.json b/node_modules/crypto/package.json new file mode 100644 index 0000000..01aa4d3 --- /dev/null +++ b/node_modules/crypto/package.json @@ -0,0 +1,19 @@ +{ + "name": "crypto", + "version": "1.0.1", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/npm/deprecate-holder.git" + }, + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/npm/deprecate-holder/issues" + }, + "homepage": "https://github.com/npm/deprecate-holder#readme" +} diff --git a/node_modules/node-gyp-build/LICENSE b/node_modules/node-gyp-build/LICENSE new file mode 100644 index 0000000..56fce08 --- /dev/null +++ b/node_modules/node-gyp-build/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/node_modules/node-gyp-build/README.md b/node_modules/node-gyp-build/README.md new file mode 100644 index 0000000..f712ca6 --- /dev/null +++ b/node_modules/node-gyp-build/README.md @@ -0,0 +1,58 @@ +# node-gyp-build + +> Build tool and bindings loader for [`node-gyp`][node-gyp] that supports prebuilds. + +``` +npm install node-gyp-build +``` + +[![Test](https://github.com/prebuild/node-gyp-build/actions/workflows/test.yml/badge.svg)](https://github.com/prebuild/node-gyp-build/actions/workflows/test.yml) + +Use together with [`prebuildify`][prebuildify] to easily support prebuilds for your native modules. + +## Usage + +> **Note.** Prebuild names have changed in [`prebuildify@3`][prebuildify] and `node-gyp-build@4`. Please see the documentation below. + +`node-gyp-build` works similar to [`node-gyp build`][node-gyp] except that it will check if a build or prebuild is present before rebuilding your project. + +It's main intended use is as an npm install script and bindings loader for native modules that bundle prebuilds using [`prebuildify`][prebuildify]. + +First add `node-gyp-build` as an install script to your native project + +``` js +{ + ... + "scripts": { + "install": "node-gyp-build" + } +} +``` + +Then in your `index.js`, instead of using the [`bindings`](https://www.npmjs.com/package/bindings) module use `node-gyp-build` to load your binding. + +``` js +var binding = require('node-gyp-build')(__dirname) +``` + +If you do these two things and bundle prebuilds with [`prebuildify`][prebuildify] your native module will work for most platforms +without having to compile on install time AND will work in both node and electron without the need to recompile between usage. + +Users can override `node-gyp-build` and force compiling by doing `npm install --build-from-source`. + +Prebuilds will be attempted loaded from `MODULE_PATH/prebuilds/...` and then next `EXEC_PATH/prebuilds/...` (the latter allowing use with `zeit/pkg`) + +## Supported prebuild names + +If so desired you can bundle more specific flavors, for example `musl` builds to support Alpine, or targeting a numbered ARM architecture version. + +These prebuilds can be bundled in addition to generic prebuilds; `node-gyp-build` will try to find the most specific flavor first. Prebuild filenames are composed of _tags_. The runtime tag takes precedence, as does an `abi` tag over `napi`. For more details on tags, please see [`prebuildify`][prebuildify]. + +Values for the `libc` and `armv` tags are auto-detected but can be overridden through the `LIBC` and `ARM_VERSION` environment variables, respectively. + +## License + +MIT + +[prebuildify]: https://github.com/prebuild/prebuildify +[node-gyp]: https://www.npmjs.com/package/node-gyp diff --git a/node_modules/node-gyp-build/SECURITY.md b/node_modules/node-gyp-build/SECURITY.md new file mode 100644 index 0000000..da9c516 --- /dev/null +++ b/node_modules/node-gyp-build/SECURITY.md @@ -0,0 +1,5 @@ +## Security contact information + +To report a security vulnerability, please use the +[Tidelift security contact](https://tidelift.com/security). +Tidelift will coordinate the fix and disclosure. diff --git a/node_modules/node-gyp-build/bin.js b/node_modules/node-gyp-build/bin.js new file mode 100644 index 0000000..c778e0a --- /dev/null +++ b/node_modules/node-gyp-build/bin.js @@ -0,0 +1,84 @@ +#!/usr/bin/env node + +var proc = require('child_process') +var os = require('os') +var path = require('path') + +if (!buildFromSource()) { + proc.exec('node-gyp-build-test', function (err, stdout, stderr) { + if (err) { + if (verbose()) console.error(stderr) + preinstall() + } + }) +} else { + preinstall() +} + +function build () { + var win32 = os.platform() === 'win32' + var shell = win32 + var args = [win32 ? 'node-gyp.cmd' : 'node-gyp', 'rebuild'] + + try { + var pkg = require('node-gyp/package.json') + args = [ + process.execPath, + path.join(require.resolve('node-gyp/package.json'), '..', typeof pkg.bin === 'string' ? pkg.bin : pkg.bin['node-gyp']), + 'rebuild' + ] + shell = false + } catch (_) {} + + proc.spawn(args[0], args.slice(1), { stdio: 'inherit', shell, windowsHide: true }).on('exit', function (code) { + if (code || !process.argv[3]) process.exit(code) + exec(process.argv[3]).on('exit', function (code) { + process.exit(code) + }) + }) +} + +function preinstall () { + if (!process.argv[2]) return build() + exec(process.argv[2]).on('exit', function (code) { + if (code) process.exit(code) + build() + }) +} + +function exec (cmd) { + if (process.platform !== 'win32') { + var shell = os.platform() === 'android' ? 'sh' : true + return proc.spawn(cmd, [], { + shell, + stdio: 'inherit' + }) + } + + return proc.spawn(cmd, [], { + windowsVerbatimArguments: true, + stdio: 'inherit', + shell: true, + windowsHide: true + }) +} + +function buildFromSource () { + return hasFlag('--build-from-source') || process.env.npm_config_build_from_source === 'true' +} + +function verbose () { + return hasFlag('--verbose') || process.env.npm_config_loglevel === 'verbose' +} + +// TODO (next major): remove in favor of env.npm_config_* which works since npm +// 0.1.8 while npm_config_argv will stop working in npm 7. See npm/rfcs#90 +function hasFlag (flag) { + if (!process.env.npm_config_argv) return false + + try { + return JSON.parse(process.env.npm_config_argv).original.indexOf(flag) !== -1 + } catch (_) { + return false + } +} diff --git a/node_modules/node-gyp-build/build-test.js b/node_modules/node-gyp-build/build-test.js new file mode 100644 index 0000000..b6622a5 --- /dev/null +++ b/node_modules/node-gyp-build/build-test.js @@ -0,0 +1,19 @@ +#!/usr/bin/env node + +process.env.NODE_ENV = 'test' + +var path = require('path') +var test = null + +try { + var pkg = require(path.join(process.cwd(), 'package.json')) + if (pkg.name && process.env[pkg.name.toUpperCase().replace(/-/g, '_')]) { + process.exit(0) + } + test = pkg.prebuild.test +} catch (err) { + // do nothing +} + +if (test) require(path.join(process.cwd(), test)) +else require('./')() diff --git a/node_modules/node-gyp-build/index.js b/node_modules/node-gyp-build/index.js new file mode 100644 index 0000000..07eb14f --- /dev/null +++ b/node_modules/node-gyp-build/index.js @@ -0,0 +1,6 @@ +const runtimeRequire = typeof __webpack_require__ === 'function' ? __non_webpack_require__ : require // eslint-disable-line +if (typeof runtimeRequire.addon === 'function') { // if the platform supports native resolving prefer that + module.exports = runtimeRequire.addon.bind(runtimeRequire) +} else { // else use the runtime version here + module.exports = require('./node-gyp-build.js') +} diff --git a/node_modules/node-gyp-build/node-gyp-build.js b/node_modules/node-gyp-build/node-gyp-build.js new file mode 100644 index 0000000..76b96e1 --- /dev/null +++ b/node_modules/node-gyp-build/node-gyp-build.js @@ -0,0 +1,207 @@ +var fs = require('fs') +var path = require('path') +var os = require('os') + +// Workaround to fix webpack's build warnings: 'the request of a dependency is an expression' +var runtimeRequire = typeof __webpack_require__ === 'function' ? __non_webpack_require__ : require // eslint-disable-line + +var vars = (process.config && process.config.variables) || {} +var prebuildsOnly = !!process.env.PREBUILDS_ONLY +var abi = process.versions.modules // TODO: support old node where this is undef +var runtime = isElectron() ? 'electron' : (isNwjs() ? 'node-webkit' : 'node') + +var arch = process.env.npm_config_arch || os.arch() +var platform = process.env.npm_config_platform || os.platform() +var libc = process.env.LIBC || (isAlpine(platform) ? 'musl' : 'glibc') +var armv = process.env.ARM_VERSION || (arch === 'arm64' ? '8' : vars.arm_version) || '' +var uv = (process.versions.uv || '').split('.')[0] + +module.exports = load + +function load (dir) { + return runtimeRequire(load.resolve(dir)) +} + +load.resolve = load.path = function (dir) { + dir = path.resolve(dir || '.') + + try { + var name = runtimeRequire(path.join(dir, 'package.json')).name.toUpperCase().replace(/-/g, '_') + if (process.env[name + '_PREBUILD']) dir = process.env[name + '_PREBUILD'] + } catch (err) {} + + if (!prebuildsOnly) { + var release = getFirst(path.join(dir, 'build/Release'), matchBuild) + if (release) return release + + var debug = getFirst(path.join(dir, 'build/Debug'), matchBuild) + if (debug) return debug + } + + var prebuild = resolve(dir) + if (prebuild) return prebuild + + var nearby = resolve(path.dirname(process.execPath)) + if (nearby) return nearby + + var target = [ + 'platform=' + platform, + 'arch=' + arch, + 'runtime=' + runtime, + 'abi=' + abi, + 'uv=' + uv, + armv ? 'armv=' + armv : '', + 'libc=' + libc, + 'node=' + process.versions.node, + process.versions.electron ? 'electron=' + process.versions.electron : '', + typeof __webpack_require__ === 'function' ? 'webpack=true' : '' // eslint-disable-line + ].filter(Boolean).join(' ') + + throw new Error('No native build was found for ' + target + '\n loaded from: ' + dir + '\n') + + function resolve (dir) { + // Find matching "prebuilds/-" directory + var tuples = readdirSync(path.join(dir, 'prebuilds')).map(parseTuple) + var tuple = tuples.filter(matchTuple(platform, arch)).sort(compareTuples)[0] + if (!tuple) return + + // Find most specific flavor first + var prebuilds = path.join(dir, 'prebuilds', tuple.name) + var parsed = readdirSync(prebuilds).map(parseTags) + var candidates = parsed.filter(matchTags(runtime, abi)) + var winner = candidates.sort(compareTags(runtime))[0] + if (winner) return path.join(prebuilds, winner.file) + } +} + +function readdirSync (dir) { + try { + return fs.readdirSync(dir) + } catch (err) { + return [] + } +} + +function getFirst (dir, filter) { + var files = readdirSync(dir).filter(filter) + return files[0] && path.join(dir, files[0]) +} + +function matchBuild (name) { + return /\.node$/.test(name) +} + +function parseTuple (name) { + // Example: darwin-x64+arm64 + var arr = name.split('-') + if (arr.length !== 2) return + + var platform = arr[0] + var architectures = arr[1].split('+') + + if (!platform) return + if (!architectures.length) return + if (!architectures.every(Boolean)) return + + return { name, platform, architectures } +} + +function matchTuple (platform, arch) { + return function (tuple) { + if (tuple == null) return false + if (tuple.platform !== platform) return false + return tuple.architectures.includes(arch) + } +} + +function compareTuples (a, b) { + // Prefer single-arch prebuilds over multi-arch + return a.architectures.length - b.architectures.length +} + +function parseTags (file) { + var arr = file.split('.') + var extension = arr.pop() + var tags = { file: file, specificity: 0 } + + if (extension !== 'node') return + + for (var i = 0; i < arr.length; i++) { + var tag = arr[i] + + if (tag === 'node' || tag === 'electron' || tag === 'node-webkit') { + tags.runtime = tag + } else if (tag === 'napi') { + tags.napi = true + } else if (tag.slice(0, 3) === 'abi') { + tags.abi = tag.slice(3) + } else if (tag.slice(0, 2) === 'uv') { + tags.uv = tag.slice(2) + } else if (tag.slice(0, 4) === 'armv') { + tags.armv = tag.slice(4) + } else if (tag === 'glibc' || tag === 'musl') { + tags.libc = tag + } else { + continue + } + + tags.specificity++ + } + + return tags +} + +function matchTags (runtime, abi) { + return function (tags) { + if (tags == null) return false + if (tags.runtime && tags.runtime !== runtime && !runtimeAgnostic(tags)) return false + if (tags.abi && tags.abi !== abi && !tags.napi) return false + if (tags.uv && tags.uv !== uv) return false + if (tags.armv && tags.armv !== armv) return false + if (tags.libc && tags.libc !== libc) return false + + return true + } +} + +function runtimeAgnostic (tags) { + return tags.runtime === 'node' && tags.napi +} + +function compareTags (runtime) { + // Precedence: non-agnostic runtime, abi over napi, then by specificity. + return function (a, b) { + if (a.runtime !== b.runtime) { + return a.runtime === runtime ? -1 : 1 + } else if (a.abi !== b.abi) { + return a.abi ? -1 : 1 + } else if (a.specificity !== b.specificity) { + return a.specificity > b.specificity ? -1 : 1 + } else { + return 0 + } + } +} + +function isNwjs () { + return !!(process.versions && process.versions.nw) +} + +function isElectron () { + if (process.versions && process.versions.electron) return true + if (process.env.ELECTRON_RUN_AS_NODE) return true + return typeof window !== 'undefined' && window.process && window.process.type === 'renderer' +} + +function isAlpine (platform) { + return platform === 'linux' && fs.existsSync('/etc/alpine-release') +} + +// Exposed for unit tests +// TODO: move to lib +load.parseTags = parseTags +load.matchTags = matchTags +load.compareTags = compareTags +load.parseTuple = parseTuple +load.matchTuple = matchTuple +load.compareTuples = compareTuples diff --git a/node_modules/node-gyp-build/optional.js b/node_modules/node-gyp-build/optional.js new file mode 100644 index 0000000..8daa04a --- /dev/null +++ b/node_modules/node-gyp-build/optional.js @@ -0,0 +1,7 @@ +#!/usr/bin/env node + +/* +I am only useful as an install script to make node-gyp not compile for purely optional native deps +*/ + +process.exit(0) diff --git a/node_modules/node-gyp-build/package.json b/node_modules/node-gyp-build/package.json new file mode 100644 index 0000000..6f6f28b --- /dev/null +++ b/node_modules/node-gyp-build/package.json @@ -0,0 +1,43 @@ +{ + "name": "node-gyp-build", + "version": "4.8.4", + "description": "Build tool and bindings loader for node-gyp that supports prebuilds", + "main": "index.js", + "imports": { + "fs": { + "bare": "builtin:fs", + "default": "fs" + }, + "path": { + "bare": "builtin:path", + "default": "path" + }, + "os": { + "bare": "builtin:os", + "default": "os" + } + }, + "devDependencies": { + "array-shuffle": "^1.0.1", + "standard": "^14.0.0", + "tape": "^5.0.0" + }, + "scripts": { + "test": "standard && node test" + }, + "bin": { + "node-gyp-build": "./bin.js", + "node-gyp-build-optional": "./optional.js", + "node-gyp-build-test": "./build-test.js" + }, + "repository": { + "type": "git", + "url": "https://github.com/prebuild/node-gyp-build.git" + }, + "author": "Mathias Buus (@mafintosh)", + "license": "MIT", + "bugs": { + "url": "https://github.com/prebuild/node-gyp-build/issues" + }, + "homepage": "https://github.com/prebuild/node-gyp-build" +} diff --git a/node_modules/nodemailer/.gitattributes b/node_modules/nodemailer/.gitattributes new file mode 100644 index 0000000..0318d52 --- /dev/null +++ b/node_modules/nodemailer/.gitattributes @@ -0,0 +1,6 @@ +*.js text eol=lf +*.txt text eol=lf +*.html text eol=lf +*.htm text eol=lf +*.ics -text +*.bin -text \ No newline at end of file diff --git a/node_modules/nodemailer/.ncurc.js b/node_modules/nodemailer/.ncurc.js new file mode 100644 index 0000000..391ac07 --- /dev/null +++ b/node_modules/nodemailer/.ncurc.js @@ -0,0 +1,9 @@ +'use strict'; + +module.exports = { + upgrade: true, + reject: [ + // API changes break existing tests + 'proxy' + ] +}; diff --git a/node_modules/nodemailer/.prettierignore b/node_modules/nodemailer/.prettierignore new file mode 100644 index 0000000..ad770c2 --- /dev/null +++ b/node_modules/nodemailer/.prettierignore @@ -0,0 +1,8 @@ +node_modules +coverage +*.min.js +dist +build +.nyc_output +package-lock.json +CHANGELOG.md diff --git a/node_modules/nodemailer/.prettierrc b/node_modules/nodemailer/.prettierrc new file mode 100644 index 0000000..5e2bbe3 --- /dev/null +++ b/node_modules/nodemailer/.prettierrc @@ -0,0 +1,12 @@ +{ + "printWidth": 140, + "tabWidth": 4, + "useTabs": false, + "semi": true, + "singleQuote": true, + "quoteProps": "as-needed", + "trailingComma": "none", + "bracketSpacing": true, + "arrowParens": "avoid", + "endOfLine": "lf" +} diff --git a/node_modules/nodemailer/.prettierrc.js b/node_modules/nodemailer/.prettierrc.js new file mode 100644 index 0000000..1a6faac --- /dev/null +++ b/node_modules/nodemailer/.prettierrc.js @@ -0,0 +1,10 @@ +'use strict'; + +module.exports = { + printWidth: 160, + tabWidth: 4, + singleQuote: true, + endOfLine: 'lf', + trailingComma: 'none', + arrowParens: 'avoid' +}; diff --git a/node_modules/nodemailer/.release-please-config.json b/node_modules/nodemailer/.release-please-config.json new file mode 100644 index 0000000..6241d57 --- /dev/null +++ b/node_modules/nodemailer/.release-please-config.json @@ -0,0 +1,9 @@ +{ + "packages": { + ".": { + "release-type": "node", + "package-name": "nodemailer", + "pull-request-title-pattern": "chore${scope}: release ${version} [skip-ci]" + } + } +} diff --git a/node_modules/nodemailer/CHANGELOG.md b/node_modules/nodemailer/CHANGELOG.md new file mode 100644 index 0000000..410ff41 --- /dev/null +++ b/node_modules/nodemailer/CHANGELOG.md @@ -0,0 +1,966 @@ +# CHANGELOG + +## [8.0.2](https://github.com/nodemailer/nodemailer/compare/v8.0.1...v8.0.2) (2026-03-09) + + +### Bug Fixes + +* merge fragmented display names with unquoted commas in addressparser ([fe27f7f](https://github.com/nodemailer/nodemailer/commit/fe27f7fd57f7587d897274438da2f628ad0ad7d9)) + +## [8.0.1](https://github.com/nodemailer/nodemailer/compare/v8.0.0...v8.0.1) (2026-02-07) + + +### Bug Fixes + +* absorb TLS errors during socket teardown ([7f8dde4](https://github.com/nodemailer/nodemailer/commit/7f8dde41438c66b8311e888fa5f8c518fcaba6f1)) +* absorb TLS errors during socket teardown ([381f628](https://github.com/nodemailer/nodemailer/commit/381f628d55e62bb3131bd2a452fa1ce00bc48aea)) +* Add Gmail Workspace service configuration ([#1787](https://github.com/nodemailer/nodemailer/issues/1787)) ([dc97ede](https://github.com/nodemailer/nodemailer/commit/dc97ede417b3030b311771541b1f17f5ca76bcbf)) + +## [8.0.0](https://github.com/nodemailer/nodemailer/compare/v7.0.13...v8.0.0) (2026-02-04) + + +### ⚠ BREAKING CHANGES + +* Error code 'NoAuth' renamed to 'ENOAUTH' + +### Bug Fixes + +* add connection fallback to alternative DNS addresses ([e726d6f](https://github.com/nodemailer/nodemailer/commit/e726d6f44aa7ca14e943d4303243cb5494b09c75)) +* centralize and standardize error codes ([45062ce](https://github.com/nodemailer/nodemailer/commit/45062ce7a4705f3e63c5d9e606547f4d99fd29b5)) +* harden DNS fallback against race conditions and cleanup issues ([4fa3c63](https://github.com/nodemailer/nodemailer/commit/4fa3c63a1f36aefdbaea7f57a133adc458413a47)) +* improve socket cleanup to prevent potential memory leaks ([6069fdc](https://github.com/nodemailer/nodemailer/commit/6069fdcff68a3eef9a9bb16b2bf5ddb924c02091)) + +## [7.0.13](https://github.com/nodemailer/nodemailer/compare/v7.0.12...v7.0.13) (2026-01-27) + + +### Bug Fixes + +* downgrade transient connection error logs to warn level ([4c041db](https://github.com/nodemailer/nodemailer/commit/4c041db85d560e98bc5e1fd5d5a191835c5b7d2f)) + +## [7.0.12](https://github.com/nodemailer/nodemailer/compare/v7.0.11...v7.0.12) (2025-12-22) + + +### Bug Fixes + +* added support for REQUIRETLS ([#1793](https://github.com/nodemailer/nodemailer/issues/1793)) ([053ce6a](https://github.com/nodemailer/nodemailer/commit/053ce6a772a7c608e6bee7f58ebe9900afbd9b84)) +* use 8bit encoding for message/rfc822 attachments ([adf8611](https://github.com/nodemailer/nodemailer/commit/adf86113217b23ff3cd1191af5cd1d360fcc313b)) + +## [7.0.11](https://github.com/nodemailer/nodemailer/compare/v7.0.10...v7.0.11) (2025-11-26) + + +### Bug Fixes + +* prevent stack overflow DoS in addressparser with deeply nested groups ([b61b9c0](https://github.com/nodemailer/nodemailer/commit/b61b9c0cfd682b6f647754ca338373b68336a150)) + +## [7.0.10](https://github.com/nodemailer/nodemailer/compare/v7.0.9...v7.0.10) (2025-10-23) + + +### Bug Fixes + +* Increase data URI size limit from 100KB to 50MB and preserve content type ([28dbf3f](https://github.com/nodemailer/nodemailer/commit/28dbf3fe129653f5756c150a98dc40593bfb2cfe)) + +## [7.0.9](https://github.com/nodemailer/nodemailer/compare/v7.0.8...v7.0.9) (2025-10-07) + + +### Bug Fixes + +* **release:** Trying to fix release proecess by upgrading Node version in runner ([579fce4](https://github.com/nodemailer/nodemailer/commit/579fce4683eb588891613a6c9a00d8092e8c62d1)) + +## [7.0.8](https://github.com/nodemailer/nodemailer/compare/v7.0.7...v7.0.8) (2025-10-07) + + +### Bug Fixes + +* **addressparser:** flatten nested groups per RFC 5322 ([8f8a77c](https://github.com/nodemailer/nodemailer/commit/8f8a77c67f0ba94ddf4e16c68f604a5920fb5d26)) + +## [7.0.7](https://github.com/nodemailer/nodemailer/compare/v7.0.6...v7.0.7) (2025-10-05) + +### Bug Fixes + +- **addressparser:** Fixed addressparser handling of quoted nested email addresses ([1150d99](https://github.com/nodemailer/nodemailer/commit/1150d99fba77280df2cfb1885c43df23109a8626)) +- **dns:** add memory leak prevention for DNS cache ([0240d67](https://github.com/nodemailer/nodemailer/commit/0240d6795ded6d8008d102161a729f120b6d786a)) +- **linter:** Updated eslint and created prettier formatting task ([df13b74](https://github.com/nodemailer/nodemailer/commit/df13b7487e368acded35e45d0887d23c89c9177a)) +- refresh expired DNS cache on error ([#1759](https://github.com/nodemailer/nodemailer/issues/1759)) ([ea0fc5a](https://github.com/nodemailer/nodemailer/commit/ea0fc5a6633a3546f4b00fcf2f428e9ca732cdb6)) +- resolve linter errors in DNS cache tests ([3b8982c](https://github.com/nodemailer/nodemailer/commit/3b8982c1f24508089a8757b74039000a4498b158)) + +## [7.0.6](https://github.com/nodemailer/nodemailer/compare/v7.0.5...v7.0.6) (2025-08-27) + +### Bug Fixes + +- **encoder:** avoid silent data loss by properly flushing trailing base64 ([#1747](https://github.com/nodemailer/nodemailer/issues/1747)) ([01ae76f](https://github.com/nodemailer/nodemailer/commit/01ae76f2cfe991c0c3fe80170f236da60531496b)) +- handle multiple XOAUTH2 token requests correctly ([#1754](https://github.com/nodemailer/nodemailer/issues/1754)) ([dbe0028](https://github.com/nodemailer/nodemailer/commit/dbe00286351cddf012726a41a96ae613d30a34ee)) +- ReDoS vulnerability in parseDataURI and \_processDataUrl ([#1755](https://github.com/nodemailer/nodemailer/issues/1755)) ([90b3e24](https://github.com/nodemailer/nodemailer/commit/90b3e24d23929ebf9f4e16261049b40ee4055a39)) + +## [7.0.5](https://github.com/nodemailer/nodemailer/compare/v7.0.4...v7.0.5) (2025-07-07) + +### Bug Fixes + +- updated well known delivery service list ([fa2724b](https://github.com/nodemailer/nodemailer/commit/fa2724b337eb8d8fdcdd788fe903980b061316b8)) + +## [7.0.4](https://github.com/nodemailer/nodemailer/compare/v7.0.3...v7.0.4) (2025-06-29) + +### Bug Fixes + +- **pools:** Emit 'clear' once transporter is idle and all connections are closed ([839e286](https://github.com/nodemailer/nodemailer/commit/839e28634c9a93ae4321f399a8c893bf487a09fa)) +- **smtp-connection:** jsdoc public annotation for socket ([#1741](https://github.com/nodemailer/nodemailer/issues/1741)) ([c45c84f](https://github.com/nodemailer/nodemailer/commit/c45c84fe9b8e2ec5e0615ab02d4197473911ab3e)) +- **well-known-services:** Added AliyunQiye ([bb9e6da](https://github.com/nodemailer/nodemailer/commit/bb9e6daffb632d7d8f969359859f88a138de3a48)) + +## [7.0.3](https://github.com/nodemailer/nodemailer/compare/v7.0.2...v7.0.3) (2025-05-08) + +### Bug Fixes + +- **attachments:** Set the default transfer encoding for message/rfc822 attachments as '7bit' ([007d5f3](https://github.com/nodemailer/nodemailer/commit/007d5f3f40908c588f1db46c76de8b64ff429327)) + +## [7.0.2](https://github.com/nodemailer/nodemailer/compare/v7.0.1...v7.0.2) (2025-05-04) + +### Bug Fixes + +- **ses:** Fixed structured from header ([faa9a5e](https://github.com/nodemailer/nodemailer/commit/faa9a5eafaacbaf85de3540466a04636e12729b3)) + +## [7.0.1](https://github.com/nodemailer/nodemailer/compare/v7.0.0...v7.0.1) (2025-05-04) + +### Bug Fixes + +- **ses:** Use formatted FromEmailAddress for SES emails ([821cd09](https://github.com/nodemailer/nodemailer/commit/821cd09002f16c20369cc728b9414c7eb99e4113)) + +## [7.0.0](https://github.com/nodemailer/nodemailer/compare/v6.10.1...v7.0.0) (2025-05-03) + +### ⚠ BREAKING CHANGES + +- SESv2 SDK support, removed older SES SDK v2 and v3 , removed SES rate limiting and idling features + +### Features + +- SESv2 SDK support, removed older SES SDK v2 and v3 , removed SES rate limiting and idling features ([15db667](https://github.com/nodemailer/nodemailer/commit/15db667af2d0a5ed835281cfdbab16ee73b5edce)) + +## [6.10.1](https://github.com/nodemailer/nodemailer/compare/v6.10.0...v6.10.1) (2025-02-06) + +### Bug Fixes + +- close correct socket ([a18062c](https://github.com/nodemailer/nodemailer/commit/a18062c04d0e05ca4357fbe8f0a59b690fa5391e)) + +## [6.10.0](https://github.com/nodemailer/nodemailer/compare/v6.9.16...v6.10.0) (2025-01-23) + +### Features + +- **services:** add Seznam email service configuration ([#1695](https://github.com/nodemailer/nodemailer/issues/1695)) ([d1ae0a8](https://github.com/nodemailer/nodemailer/commit/d1ae0a86883ba6011a49a5bbdf076098e2e3637a)) + +### Bug Fixes + +- **proxy:** Set error and timeout errors for proxied sockets ([aa0c99c](https://github.com/nodemailer/nodemailer/commit/aa0c99c8f25440bb3dc91f4f3448777c800604d7)) + +## [6.9.16](https://github.com/nodemailer/nodemailer/compare/v6.9.15...v6.9.16) (2024-10-28) + +### Bug Fixes + +- **addressparser:** Correctly detect if user local part is attached to domain part ([f2096c5](https://github.com/nodemailer/nodemailer/commit/f2096c51b92a69ecfbcc15884c28cb2c2f00b826)) + +## [6.9.15](https://github.com/nodemailer/nodemailer/compare/v6.9.14...v6.9.15) (2024-08-08) + +### Bug Fixes + +- Fix memory leak ([#1667](https://github.com/nodemailer/nodemailer/issues/1667)) ([baa28f6](https://github.com/nodemailer/nodemailer/commit/baa28f659641a4bc30360633673d851618f8e8bd)) +- **mime:** Added GeoJSON closes [#1637](https://github.com/nodemailer/nodemailer/issues/1637) ([#1665](https://github.com/nodemailer/nodemailer/issues/1665)) ([79b8293](https://github.com/nodemailer/nodemailer/commit/79b8293ad557d36f066b4675e649dd80362fd45b)) + +## [6.9.14](https://github.com/nodemailer/nodemailer/compare/v6.9.13...v6.9.14) (2024-06-19) + +### Bug Fixes + +- **api:** Added support for Ethereal authentication ([56b2205](https://github.com/nodemailer/nodemailer/commit/56b22052a98de9e363f6c4d26d1512925349c3f3)) +- **services.json:** Add Email Services Provider Feishu Mail (CN) ([#1648](https://github.com/nodemailer/nodemailer/issues/1648)) ([e9e9ecc](https://github.com/nodemailer/nodemailer/commit/e9e9ecc99b352948a912868c7912b280a05178c6)) +- **services.json:** update Mailtrap host and port in well known ([#1652](https://github.com/nodemailer/nodemailer/issues/1652)) ([fc2c9ea](https://github.com/nodemailer/nodemailer/commit/fc2c9ea0b4c4f4e514143d2a138c9a23095fc827)) +- **well-known-services:** Add Loopia in well known services ([#1655](https://github.com/nodemailer/nodemailer/issues/1655)) ([21a28a1](https://github.com/nodemailer/nodemailer/commit/21a28a18fc9fdf8e0e86ddd846e54641395b2cb6)) + +## [6.9.13](https://github.com/nodemailer/nodemailer/compare/v6.9.12...v6.9.13) (2024-03-20) + +### Bug Fixes + +- **tls:** Ensure servername for SMTP ([d66fdd3](https://github.com/nodemailer/nodemailer/commit/d66fdd3dccacc4bc79d697fe9009204cc8d4bde0)) + +## [6.9.12](https://github.com/nodemailer/nodemailer/compare/v6.9.11...v6.9.12) (2024-03-08) + +### Bug Fixes + +- **message-generation:** Escape single quote in address names ([4ae5fad](https://github.com/nodemailer/nodemailer/commit/4ae5fadeaac70ba91abf529fcaae65f829a39101)) + +## [6.9.11](https://github.com/nodemailer/nodemailer/compare/v6.9.10...v6.9.11) (2024-02-29) + +### Bug Fixes + +- **headers:** Ensure that Content-type is the bottom header ([c7cf97e](https://github.com/nodemailer/nodemailer/commit/c7cf97e5ecc83f8eee773359951df995c9945446)) + +## [6.9.10](https://github.com/nodemailer/nodemailer/compare/v6.9.9...v6.9.10) (2024-02-22) + +### Bug Fixes + +- **data-uri:** Do not use regular expressions for parsing data URI schemes ([12e65e9](https://github.com/nodemailer/nodemailer/commit/12e65e975d80efe6bafe6de4590829b3b5ebb492)) +- **data-uri:** Moved all data-uri regexes to use the non-regex parseDataUri method ([edd5dfe](https://github.com/nodemailer/nodemailer/commit/edd5dfe5ce9b725f8b8ae2830797f65b2a2b0a33)) + +## [6.9.9](https://github.com/nodemailer/nodemailer/compare/v6.9.8...v6.9.9) (2024-02-01) + +### Bug Fixes + +- **security:** Fix issues described in GHSA-9h6g-pr28-7cqp. Do not use eternal matching pattern if only a few occurences are expected ([dd8f5e8](https://github.com/nodemailer/nodemailer/commit/dd8f5e8a4ddc99992e31df76bcff9c590035cd4a)) +- **tests:** Use native node test runner, added code coverage support, removed grunt ([#1604](https://github.com/nodemailer/nodemailer/issues/1604)) ([be45c1b](https://github.com/nodemailer/nodemailer/commit/be45c1b299d012358d69247019391a02734d70af)) + +## [6.9.8](https://github.com/nodemailer/nodemailer/compare/v6.9.7...v6.9.8) (2023-12-30) + +### Bug Fixes + +- **punycode:** do not use native punycode module ([b4d0e0c](https://github.com/nodemailer/nodemailer/commit/b4d0e0c7cc4b15bc4d9e287f91d1bcaca87508b0)) + +## [6.9.7](https://github.com/nodemailer/nodemailer/compare/v6.9.6...v6.9.7) (2023-10-22) + +### Bug Fixes + +- **customAuth:** Do not require user and pass to be set for custom authentication schemes (fixes [#1584](https://github.com/nodemailer/nodemailer/issues/1584)) ([41d482c](https://github.com/nodemailer/nodemailer/commit/41d482c3f01e26111b06f3e46351b193db3fb5cb)) + +## [6.9.6](https://github.com/nodemailer/nodemailer/compare/v6.9.5...v6.9.6) (2023-10-09) + +### Bug Fixes + +- **inline:** Use 'inline' as the default Content Dispostion value for embedded images ([db32c93](https://github.com/nodemailer/nodemailer/commit/db32c93fefee527bcc239f13056e5d9181a4d8af)) +- **tests:** Removed Node v12 from test matrix as it is not compatible with the test framework anymore ([7fe0a60](https://github.com/nodemailer/nodemailer/commit/7fe0a608ed6bcb70dc6b2de543ebfc3a30abf984)) + +## [6.9.5](https://github.com/nodemailer/nodemailer/compare/v6.9.4...v6.9.5) (2023-09-06) + +### Bug Fixes + +- **license:** Updated license year ([da4744e](https://github.com/nodemailer/nodemailer/commit/da4744e491f3a68f4f68e4073684370592630e01)) + +## 6.9.4 2023-07-19 + +- Renamed SendinBlue to Brevo + +## 6.9.3 2023-05-29 + +- Specified license identifier (was defined as MIT, actual value MIT-0) +- If SMTP server disconnects with a message, process it and include as part of the response error + +## 6.9.2 2023-05-11 + +- Fix uncaught exception on invalid attachment content payload + +## 6.9.1 2023-01-27 + +- Fix base64 encoding for emoji bytes in encoded words + +## 6.9.0 2023-01-12 + +- Do not throw if failed to resolve IPv4 addresses +- Include EHLO extensions in the send response +- fix sendMail function: callback should be optional + +## 6.8.0 2022-09-28 + +- Add DNS timeout (huksley) +- add dns.REFUSED (lucagianfelici) + +## 6.7.8 2022-08-11 + +- Allow to use multiple Reply-To addresses + +## 6.7.7 2022-07-06 + +- Resolver fixes + +## 6.7.5 2022-05-04 + +- No changes, pushing a new README to npmjs.org + +## 6.7.4 2022-04-29 + +- Ensure compatibility with Node 18 +- Replaced Travis with Github Actions + +## 6.7.3 2022-03-21 + +- Typo fixes +- Added stale issue automation fir Github +- Add Infomaniak config to well known service (popod) +- Update Outlook/Hotmail host in well known services (popod) +- fix: DSN recipient gets ignored (KornKalle) + +## 6.7.2 2021-11-26 + +- Fix proxies for account verification + +## 6.7.1 2021-11-15 + +- fix verify on ses-transport (stanofsky) + +## 6.7.0 2021-10-11 + +- Updated DNS resolving logic. If there are multiple responses for a A/AAAA record, then loop these randomly instead of only caching the first one + +## 6.6.5 2021-09-23 + +- Replaced Object.values() and Array.flat() with polyfills to allow using Nodemailer in Node v6+ + +## 6.6.4 2021-09-22 + +- Better compatibility with IPv6-only SMTP hosts (oxzi) +- Fix ses verify for sdk v3 (hannesvdvreken) +- Added SECURITY.txt for contact info + +## 6.6.3 2021-07-14 + +- Do not show passwords in SMTP transaction logs. All passwords used in logging are replaced by `"/* secret */"` + +## 6.6.1 2021-05-23 + +- Fixed address formatting issue where newlines in an email address, if provided via address object, were not properly removed. Reported by tmazeika (#1289) + +## 6.6.0 2021-04-28 + +- Added new option `newline` for MailComposer +- aws ses connection verification (Ognjen Jevremovic) + +## 6.5.0 2021-02-26 + +- Pass through textEncoding to subnodes +- Added support for AWS SES v3 SDK +- Fixed tests + +## 6.4.18 2021-02-11 + +- Updated README + +## 6.4.17 2020-12-11 + +- Allow mixing attachments with caendar alternatives + +## 6.4.16 2020-11-12 + +- Applied updated prettier formating rules + +## 6.4.15 2020-11-06 + +- Minor changes in header key casing + +## 6.4.14 2020-10-14 + +- Disabled postinstall script + +## 6.4.13 2020-10-02 + +- Fix normalizeHeaderKey method for single node messages + +## 6.4.12 2020-09-30 + +- Better handling of attachment filenames that include quote symbols +- Includes all information from the oath2 error response in the error message (Normal Gaussian) [1787f227] + +## 6.4.11 2020-07-29 + +- Fixed escape sequence handling in address parsing + +## 6.4.10 2020-06-17 + +- Fixed RFC822 output for MailComposer when using invalid content-type value. Mostly relevant if message attachments have stragne content-type values set. + +## 6.4.7 2020-05-28 + +- Always set charset=utf-8 for Content-Type headers +- Catch error when using invalid crypto.sign input + +## 6.4.6 2020-03-20 + +- fix: `requeueAttempts=n` should requeue `n` times (Patrick Malouin) [a27ed2f7] + +## 6.4.4 2020-03-01 + +- Add `options.forceAuth` for SMTP (Patrick Malouin) [a27ed2f7] + +## 6.4.3 2020-02-22 + +- Added an option to specify max number of requeues when connection closes unexpectedly (Igor Sechyn) [8a927f5a] + +## 6.4.2 2019-12-11 + +- Fixed bug where array item was used with a potentially empty array + +## 6.4.1 2019-12-07 + +- Fix processing server output with unterminated responses + +## 6.4.0 2019-12-04 + +- Do not use auth if server does not advertise AUTH support [f419b09d] +- add dns.CONNREFUSED (Hiroyuki Okada) [5c4c8ca8] + +## 6.3.1 2019-10-09 + +- Ignore "end" events because it might be "error" after it (dex4er) [72bade9] +- Set username and password on the connection proxy object correctly (UsamaAshraf) [250b1a8] +- Support more DNS errors (madarche) [2391aa4] + +## 6.3.0 2019-07-14 + +- Added new option to pass a set of httpHeaders to be sent when fetching attachments. See [PR #1034](https://github.com/nodemailer/nodemailer/pull/1034) + +## 6.2.1 2019-05-24 + +- No changes. It is the same as 6.2.0 that was accidentally published as 6.2.1 to npm + +## 6.2.0 2019-05-24 + +- Added new option for addressparser: `flatten`. If true then ignores group names and returns a single list of all addresses + +## 6.1.1 2019-04-20 + +- Fixed regression bug with missing smtp `authMethod` property + +## 6.1.0 2019-04-06 + +- Added new message property `amp` for providing AMP4EMAIL content + +## 6.0.0 2019-03-25 + +- SMTPConnection: use removeListener instead of removeAllListeners (xr0master) [ddc4af15] + Using removeListener should fix memory leak with Node.js streams + +## 5.1.1 2019-01-09 + +- Added missing option argument for custom auth + +## 5.1.0 2019-01-09 + +- Official support for custom authentication methods and examples (examples/custom-auth-async.js and examples/custom-auth-cb.js) + +## 5.0.1 2019-01-09 + +- Fixed regression error to support Node versions lower than 6.11 +- Added expiremental custom authentication support + +## 5.0.0 2018-12-28 + +- Start using dns.resolve() instead of dns.lookup() for resolving SMTP hostnames. Might be breaking change on some environments so upgrade with care +- Show more logs for renewing OAuth2 tokens, previously it was not possible to see what actually failed + +## 4.7.0 2018-11-19 + +- Cleaned up List-\* header generation +- Fixed 'full' return option for DSN (klaronix) [23b93a3b] +- Support promises `for mailcomposer.build()` + +## 4.6.8 2018-08-15 + +- Use first IP address from DNS resolution when using a proxy (Limbozz) [d4ca847c] +- Return raw email from SES transport (gabegorelick) [3aa08967] + +## 4.6.7 2018-06-15 + +- Added option `skipEncoding` to JSONTransport + +## 4.6.6 2018-06-10 + +- Fixes mime encoded-word compatibility issue with invalid clients like Zimbra + +## 4.6.5 2018-05-23 + +- Fixed broken DKIM stream in Node.js v10 +- Updated error messages for SMTP responses to not include a newline + +## 4.6.4 2018-03-31 + +- Readded logo author link to README that was accidentally removed a while ago + +## 4.6.3 2018-03-13 + +- Removed unneeded dependency + +## 4.6.2 2018-03-06 + +- When redirecting URL calls then do not include original POST content + +## 4.6.1 2018-03-06 + +- Fixed Smtp connection freezing, when trying to send after close / quit (twawszczak) [73d3911c] + +## 4.6.0 2018-02-22 + +- Support socks module v2 in addition to v1 [e228bcb2] +- Fixed invalid promise return value when using createTestAccount [5524e627] +- Allow using local addresses [8f6fa35f] + +## 4.5.0 2018-02-21 + +- Added new message transport option `normalizeHeaderKey(key)=>normalizedKey` for custom header formatting + +## 4.4.2 2018-01-20 + +- Added sponsors section to README +- enclose encodeURIComponent in try..catch to handle invalid urls + +## 4.4.1 2017-12-08 + +- Better handling of unexpectedly dropping connections + +## 4.4.0 2017-11-10 + +- Changed default behavior for attachment option contentTransferEncoding. If it is unset then base64 encoding is used for the attachment. If it is set to false then previous default applies (base64 for most, 7bit for text) + +## 4.3.1 2017-10-25 + +- Fixed a confict with Electron.js where timers do not have unref method + +## 4.3.0 2017-10-23 + +- Added new mail object method `mail.normalize(cb)` that should make creating HTTP API based transports much easier + +## 4.2.0 2017-10-13 + +- Expose streamed messages size and timers in info response + +## v4.1.3 2017-10-06 + +- Allow generating preview links without calling createTestAccount first + +## v4.1.2 2017-10-03 + +- No actual changes. Needed to push updated README to npmjs + +## v4.1.1 2017-09-25 + +- Fixed JSONTransport attachment handling + +## v4.1.0 2017-08-28 + +- Added new methods `createTestAccount` and `getTestMessageUrl` to use autogenerated email accounts from https://Ethereal.email + +## v4.0.1 2017-04-13 + +- Fixed issue with LMTP and STARTTLS + +## v4.0.0 2017-04-06 + +- License changed from EUPLv1.1 to MIT + +## v3.1.8 2017-03-21 + +- Fixed invalid List-\* header generation + +## v3.1.7 2017-03-14 + +- Emit an error if STARTTLS ends with connection being closed + +## v3.1.6 2017-03-14 + +- Expose last server response for smtpConnection + +## v3.1.5 2017-03-08 + +- Fixed SES transport, added missing `response` value + +## v3.1.4 2017-02-26 + +- Fixed DKIM calculation for empty body +- Ensure linebreak after message content. This fixes DKIM signatures for non-multipart messages where input did not end with a newline + +## v3.1.3 2017-02-17 + +- Fixed missing `transport.verify()` methods for SES transport + +## v3.1.2 2017-02-17 + +- Added missing error handlers for Sendmail, SES and Stream transports. If a messages contained an invalid URL as attachment then these transports threw an uncatched error + +## v3.1.1 2017-02-13 + +- Fixed missing `transport.on('idle')` and `transport.isIdle()` methods for SES transports + +## v3.1.0 2017-02-13 + +- Added built-in transport for AWS SES. [Docs](http://localhost:1313/transports/ses/) +- Updated stream transport to allow building JSON strings. [Docs](http://localhost:1313/transports/stream/#json-transport) +- Added new method _mail.resolveAll_ that fetches all attachments and such to be able to more easily build API-based transports + +## v3.0.2 2017-02-04 + +- Fixed a bug with OAuth2 login where error callback was fired twice if getToken was not available. + +## v3.0.1 2017-02-03 + +- Fixed a bug where Nodemailer threw an exception if `disableFileAccess` option was used +- Added FLOSS [exception declaration](FLOSS_EXCEPTIONS.md) + +## v3.0.0 2017-01-31 + +- Initial version of Nodemailer 3 + +This update brings a lot of breaking changes: + +- License changed from MIT to **EUPL-1.1**. This was possible as the new version of Nodemailer is a major rewrite. The features I don't have ownership for, were removed or reimplemented. If there's still some snippets in the code that have vague ownership then notify about the conflicting code and I'll fix it. +- Requires **Node.js v6+** +- All **templating is gone**. It was too confusing to use and to be really universal a huge list of different renderers would be required. Nodemailer is about email, not about parsing different template syntaxes +- **No NTLM authentication**. It was too difficult to re-implement. If you still need it then it would be possible to introduce a pluggable SASL interface where you could load the NTLM module in your own code and pass it to Nodemailer. Currently this is not possible. +- **OAuth2 authentication** is built in and has a different [configuration](https://nodemailer.com/smtp/oauth2/). You can use both user (3LO) and service (2LO) accounts to generate access tokens from Nodemailer. Additionally there's a new feature to authenticate differently for every message – useful if your application sends on behalf of different users instead of a single sender. +- **Improved Calendaring**. Provide an ical file to Nodemailer to send out [calendar events](https://nodemailer.com/message/calendar-events/). + +And also some non-breaking changes: + +- All **dependencies were dropped**. There is exactly 0 dependencies needed to use Nodemailer. This brings the installation time of Nodemailer from NPM down to less than 2 seconds +- **Delivery status notifications** added to Nodemailer +- Improved and built-in **DKIM** signing of messages. Previously you needed an external module for this and it did quite a lousy job with larger messages +- **Stream transport** to return a RFC822 formatted message as a stream. Useful if you want to use Nodemailer as a preprocessor and not for actual delivery. +- **Sendmail** transport built-in, no need for external transport plugin + +See [Nodemailer.com](https://nodemailer.com/) for full documentation + +## 2.7.0 2016-12-08 + +- Bumped mailcomposer that generates encoded-words differently which might break some tests + +## 2.6.0 2016-09-05 + +- Added new options disableFileAccess and disableUrlAccess +- Fixed envelope handling where cc/bcc fields were ignored in the envelope object + +## 2.4.2 2016-05-25 + +- Removed shrinkwrap file. Seemed to cause more trouble than help + +## 2.4.1 2016-05-12 + +- Fixed outdated shrinkwrap file + +## 2.4.0 2016-05-11 + +- Bumped mailcomposer module to allow using `false` as attachment filename (suppresses filename usage) +- Added NTLM authentication support + +## 2.3.2 2016-04-11 + +- Bumped smtp transport modules to get newest smtp-connection that fixes SMTPUTF8 support for internationalized email addresses + +## 2.3.1 2016-04-08 + +- Bumped mailcomposer to have better support for message/822 attachments + +## 2.3.0 2016-03-03 + +- Fixed a bug with attachment filename that contains mixed unicode and dashes +- Added built-in support for proxies by providing a new SMTP option `proxy` that takes a proxy configuration url as its value +- Added option `transport` to dynamically load transport plugins +- Do not require globally installed grunt-cli + +## 2.2.1 2016-02-20 + +- Fixed a bug in SMTP requireTLS option that was broken + +## 2.2.0 2016-02-18 + +- Removed the need to use `clone` dependency +- Added new method `verify` to check SMTP configuration +- Direct transport uses STARTTLS by default, fallbacks to plaintext if STARTTLS fails +- Added new message option `list` for setting List-\* headers +- Add simple proxy support with `getSocket` method +- Added new message option `textEncoding`. If `textEncoding` is not set then detect best encoding automatically +- Added new message option `icalEvent` to embed iCalendar events. Example [here](examples/ical-event.js) +- Added new attachment option `raw` to use prepared MIME contents instead of generating a new one. This might be useful when you want to handcraft some parts of the message yourself, for example if you want to inject a PGP encrypted message as the contents of a MIME node +- Added new message option `raw` to use an existing MIME message instead of generating a new one + +## 2.1.0 2016-02-01 + +Republishing 2.1.0-rc.1 as stable. To recap, here's the notable changes between v2.0 and v2.1: + +- Implemented templating support. You can either use a simple built-in renderer or some external advanced renderer, eg. [node-email-templates](https://github.com/niftylettuce/node-email-templates). Templating [docs](http://nodemailer.com/2-0-0-beta/templating/). +- Updated smtp-pool to emit 'idle' events in order to handle message queue more effectively +- Updated custom header handling, works everywhere the same now, no differences between adding custom headers to the message or to an attachment + +## 2.1.0-rc.1 2016-01-25 + +Sneaked in some new features even though it is already rc + +- If a SMTP pool is closed while there are still messages in a queue, the message callbacks are invoked with an error +- In case of SMTP pool the transporter emits 'idle' when there is a free connection slot available +- Added method `isIdle()` that checks if a pool has still some free connection slots available + +## 2.1.0-rc.0 2016-01-20 + +- Bumped dependency versions + +## 2.1.0-beta.3 2016-01-20 + +- Added support for node-email-templates templating in addition to the built-in renderer + +## 2.1.0-beta.2 2016-01-20 + +- Implemented simple templating feature + +## 2.1.0-beta.1 2016-01-20 + +- Allow using prepared header values that are not folded or encoded by Nodemailer + +## 2.1.0-beta.0 2016-01-20 + +- Use the same header custom structure for message root, attachments and alternatives +- Ensure that Message-Id exists when accessing message +- Allow using array values for custom headers (inserts every value in its own row) + +## 2.0.0 2016-01-11 + +- Released rc.2 as stable + +## 2.0.0-rc.2 2016-01-04 + +- Locked dependencies + +## 2.0.0-beta.2 2016-01-04 + +- Updated documentation to reflect changes with SMTP handling +- Use beta versions for smtp/pool/direct transports +- Updated logging + +## 2.0.0-beta.1 2016-01-03 + +- Use bunyan compatible logger instead of the emit('log') style +- Outsourced some reusable methods to nodemailer-shared +- Support setting direct/smtp/pool with the default configuration + +## 2.0.0-beta.0 2015-12-31 + +- Stream errors are not silently swallowed +- Do not use format=flowed +- Use nodemailer-fetch to fetch URL streams +- jshint replaced by eslint + +## v1.11.0 2015-12-28 + +Allow connection url based SMTP configurations + +## v1.10.0 2015-11-13 + +Added `defaults` argument for `createTransport` to predefine commonn values (eg. `from` address) + +## v1.9.0 2015-11-09 + +Returns a Promise for `sendMail` if callback is not defined + +## v1.8.0 2015-10-08 + +Added priority option (high, normal, low) for setting Importance header + +## v1.7.0 2015-10-06 + +Replaced hyperquest with needle. Fixes issues with compressed data and redirects + +## v1.6.0 2015-10-05 + +Maintenance release. Bumped dependencies to get support for unicode filenames for QQ webmail and to support emoji in filenames + +## v1.5.0 2015-09-24 + +Use mailcomposer instead of built in solution to generate message sources. Bumped libmime gives better quoted-printable handling. + +## v1.4.0 2015-06-27 + +Added new message option `watchHtml` to specify Apple Watch specific HTML part of the message. See [this post](https://litmus.com/blog/how-to-send-hidden-version-email-apple-watch) for details + +## v1.3.4 2015-04-25 + +Maintenance release, bumped buildmail version to get fixed format=flowed handling + +## v1.3.3 2015-04-25 + +Maintenance release, bumped dependencies + +## v1.3.2 2015-03-09 + +Maintenance release, upgraded dependencies. Replaced simplesmtp based tests with smtp-server based ones. + +## v1.3.0 2014-09-12 + +Maintenance release, upgrades buildmail and libmime. Allows using functions as transform plugins and fixes issue with unicode filenames in Gmail. + +## v1.2.2 2014-09-05 + +Proper handling of data uris as attachments. Attachment `path` property can also be defined as a data uri, not just regular url or file path. + +## v1.2.1 2014-08-21 + +Bumped libmime and mailbuild versions to properly handle filenames with spaces (short ascii only filenames with spaces were left unquoted). + +## v1.2.0 2014-08-18 + +Allow using encoded strings as attachments. Added new property `encoding` which defines the encoding used for a `content` string. If encoding is set, the content value is converted to a Buffer value using the defined encoding before usage. Useful for including binary attachemnts in JSON formatted email objects. + +## v1.1.2 2014-08-18 + +Return deprecatin error for v0.x style configuration + +## v1.1.1 2014-07-30 + +Bumped nodemailer-direct-transport dependency. Updated version includes a bugfix for Stream nodes handling. Important only if use direct-transport with Streams (not file paths or urls) as attachment content. + +## v1.1.0 2014-07-29 + +Added new method `resolveContent()` to get the html/text/attachment content as a String or Buffer. + +## v1.0.4 2014-07-23 + +Bugfix release. HTML node was instered twice if the message consisted of a HTML content (but no text content) + at least one attachment with CID + at least one attachment without CID. In this case the HTML node was inserted both to the root level multipart/mixed section and to the multipart/related sub section + +## v1.0.3 2014-07-16 + +Fixed a bug where Nodemailer crashed if the message content type was multipart/related + +## v1.0.2 2014-07-16 + +Upgraded nodemailer-smtp-transport to 0.1.11\. The docs state that for SSL you should use 'secure' option but the underlying smtp-connection module used 'secureConnection' for this purpose. Fixed smpt-connection to match the docs. + +## v1.0.1 2014-07-15 + +Implemented missing #close method that is passed to the underlying transport object. Required by the smtp pool. + +## v1.0.0 2014-07-15 + +Total rewrite. See migration guide here: + +## v0.7.1 2014-07-09 + +- Upgraded aws-sdk to 2.0.5 + +## v0.7.0 2014-06-17 + +- Bumped version to v0.7.0 +- Fix AWS-SES usage [5b6bc144] +- Replace current SES with new SES using AWS-SDK (Elanorr) [c79d797a] +- Updated README.md about Node Email Templates (niftylettuce) [e52bef81] + +## v0.6.5 2014-05-15 + +- Bumped version to v0.6.5 +- Use tildes instead of carets for dependency listing [5296ce41] +- Allow clients to set a custom identityString (venables) [5373287d] +- bugfix (adding "-i" to sendmail command line for each new mail) by copying this.args (vrodic) [05a8a9a3] +- update copyright (gdi2290) [3a6cba3a] + +## v0.6.4 2014-05-13 + +- Bumped version to v0.6.4 +- added npmignore, bumped dependencies [21bddcd9] +- Add AOL to well-known services (msouce) [da7dd3b7] + +## v0.6.3 2014-04-16 + +- Bumped version to v0.6.3 +- Upgraded simplesmtp dependency [dd367f59] + +## v0.6.2 2014-04-09 + +- Bumped version to v0.6.2 +- Added error option to Stub transport [c423acad] +- Use SVG npm badge (t3chnoboy) [677117b7] +- add SendCloud to well known services (haio) [43c358e0] +- High-res build-passing and NPM module badges (sahat) [9fdc37cd] + +## v0.6.1 2014-01-26 + +- Bumped version to v0.6.1 +- Do not throw on multiple errors from sendmail command [c6e2cd12] +- Do not require callback for pickup, fixes #238 [93eb3214] +- Added AWSSecurityToken information to README, fixes #235 [58e921d1] +- Added Nodemailer logo [06b7d1a8] + +## v0.6.0 2013-12-30 + +- Bumped version to v0.6.0 +- Allow defining custom transport methods [ec5b48ce] +- Return messageId with responseObject for all built in transport methods [74445cec] +- Bumped dependency versions for mailcomposer and readable-stream [9a034c34] +- Changed pickup argument name to 'directory' [01c3ea53] +- Added support for IIS pickup directory with PICKUP transport (philipproplesch) [36940b59..360a2878] +- Applied common styles [9e93a409] +- Updated readme [c78075e7] + +## v0.5.15 2013-12-13 + +- bumped version to v0.5.15 +- Updated README, added global options info for setting uo transports [554bb0e5] +- Resolve public hostname, if resolveHostname property for a transport object is set to `true` [9023a6e1..4c66b819] + +## v0.5.14 2013-12-05 + +- bumped version to v0.5.14 +- Expose status for direct messages [f0312df6] +- Allow to skip the X-Mailer header if xMailer value is set to 'false' [f2c20a68] + +## v0.5.13 2013-12-03 + +- bumped version to v0.5.13 +- Use the name property from the transport object to use for the domain part of message-id values (1598eee9) + +## v0.5.12 2013-12-02 + +- bumped version to v0.5.12 +- Expose transport method and transport module version if available [a495106e] +- Added 'he' module instead of using custom html entity decoding [c197d102] +- Added xMailer property for transport configuration object to override X-Mailer value [e8733a61] +- Updated README, added description for 'mail' method [e1f5f3a6] + +## v0.5.11 2013-11-28 + +- bumped version to v0.5.11 +- Updated mailcomposer version. Replaces ent with he [6a45b790e] + +## v0.5.10 2013-11-26 + +- bumped version to v0.5.10 +- added shorthand function mail() for direct transport type [88129bd7] +- minor tweaks and typo fixes [f797409e..ceac0ca4] + +## v0.5.9 2013-11-25 + +- bumped version to v0.5.9 +- Update for 'direct' handling [77b84e2f] +- do not require callback to be provided for 'direct' type [ec51c79f] + +## v0.5.8 2013-11-22 + +- bumped version to v0.5.8 +- Added support for 'direct' transport [826f226d..0dbbcbbc] + +## v0.5.7 2013-11-18 + +- bumped version to v0.5.7 +- Replace \r\n by \n in Sendmail transport (rolftimmermans) [fed2089e..616ec90c] A lot of sendmail implementations choke on \r\n newlines and require \n This commit addresses this by transforming all \r\n sequences passed to the sendmail command with \n + +## v0.5.6 2013-11-15 + +- bumped version to v0.5.6 +- Upgraded mailcomposer dependency to 0.2.4 [e5ff9c40] +- Removed noCR option [e810d1b8] +- Update wellknown.js, added FastMail (k-j-kleist) [cf930f6d] + +## v0.5.5 2013-10-30 + +- bumped version to v0.5.5 +- Updated mailcomposer dependnecy version to 0.2.3 +- Remove legacy code - node v0.4 is not supported anymore anyway +- Use hostname (autodetected or from the options.name property) for Message-Id instead of "Nodemailer" (helps a bit when messages are identified as spam) +- Added maxMessages info to README + +## v0.5.4 2013-10-29 + +- bumped version to v0.5.4 +- added "use strict" statements +- Added DSN info to README +- add support for QQ enterprise email (coderhaoxin) +- Add a Bitdeli Badge to README +- DSN options Passthrought into simplesmtp. (irvinzz) + +## v0.5.3 2013-10-03 + +- bumped version v0.5.3 +- Using a stub transport to prevent sendmail from being called during a test. (jsdevel) +- closes #78: sendmail transport does not work correctly on Unix machines. (jsdevel) +- Updated PaaS Support list to include Modulus. (fiveisprime) +- Translate self closing break tags to newline (kosmasgiannis) +- fix typos (aeosynth) + +## v0.5.2 2013-07-25 + +- bumped version v0.5.2 +- Merge pull request #177 from MrSwitch/master Fixing Amazon SES, fatal error caused by bad connection diff --git a/node_modules/nodemailer/CODE_OF_CONDUCT.md b/node_modules/nodemailer/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..1767057 --- /dev/null +++ b/node_modules/nodemailer/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# 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 diff --git a/node_modules/nodemailer/LICENSE b/node_modules/nodemailer/LICENSE new file mode 100644 index 0000000..fdfc967 --- /dev/null +++ b/node_modules/nodemailer/LICENSE @@ -0,0 +1,16 @@ +Copyright (c) 2011-2023 Andris Reinman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/node_modules/nodemailer/README.md b/node_modules/nodemailer/README.md new file mode 100644 index 0000000..519ec15 --- /dev/null +++ b/node_modules/nodemailer/README.md @@ -0,0 +1,86 @@ +# Nodemailer + +[![Nodemailer](https://raw.githubusercontent.com/nodemailer/nodemailer/master/assets/nm_logo_200x136.png)](https://nodemailer.com/about/) + +Send emails from Node.js – easy as cake! 🍰✉️ + +[![NPM](https://nodei.co/npm/nodemailer.png?downloads=true&downloadRank=true&stars=true)](https://nodemailer.com/about/) + +See [nodemailer.com](https://nodemailer.com/) for documentation and terms. + +> [!TIP] +> Check out **[EmailEngine](https://emailengine.app/?utm_source=github-nodemailer&utm_campaign=nodemailer&utm_medium=readme-link)** – a self-hosted email gateway that allows making **REST requests against IMAP and SMTP servers**. EmailEngine also sends webhooks whenever something changes on the registered accounts.\ +> \ +> Using the email accounts registered with EmailEngine, you can receive and [send emails](https://emailengine.app/sending-emails?utm_source=github-nodemailer&utm_campaign=nodemailer&utm_medium=readme-link). EmailEngine supports OAuth2, delayed sends, opens and clicks tracking, bounce detection, etc. All on top of regular email accounts without an external MTA service. + +## 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 "..." + +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. + +#### I'm having issues with Gmail + +Gmail either works well, or it does not work at all. It is probably easier to switch to an alternative service instead of fixing issues with Gmail. If Gmail does not work for you, then don't use it. Read more about it [here](https://nodemailer.com/usage/using-gmail/). + +#### I get ETIMEDOUT errors + +Check your firewall settings. Timeout usually occurs when you try to open a connection to a firewalled port either on the server or on your machine. Some ISPs also block email ports to prevent spamming. + +#### Nodemailer works on one machine but not in another + +It's either a firewall issue, or your SMTP server blocks authentication attempts from some servers. + +#### I get TLS errors + +- If you are running the code on your machine, check your antivirus settings. Antiviruses often mess around with email ports usage. Node.js might not recognize the MITM cert your antivirus is using. +- Latest Node versions allow only TLS versions 1.2 and higher. Some servers might still use TLS 1.1 or lower. Check Node.js docs on how to get correct TLS support for your app. You can change this with [tls.minVersion](https://nodejs.org/dist/latest-v16.x/docs/api/tls.html#tls_tls_createsecurecontext_options) option +- You might have the wrong value for the `secure` option. This should be set to `true` only for port 465. For every other port, it should be `false`. Setting it to `false` does not mean that Nodemailer would not use TLS. Nodemailer would still try to upgrade the connection to use TLS if the server supports it. +- Older Node versions do not fully support the certificate chain of the newest Let's Encrypt certificates. Either set [tls.rejectUnauthorized](https://nodejs.org/dist/latest-v16.x/docs/api/tls.html#tlsconnectoptions-callback) to `false` to skip chain verification or upgrade your Node version + +```js +let configOptions = { + host: 'smtp.example.com', + port: 587, + tls: { + rejectUnauthorized: true, + minVersion: 'TLSv1.2' + } +}; +``` + +#### I have issues with DNS / hosts file + +Node.js uses [c-ares](https://nodejs.org/en/docs/meta/topics/dependencies/#c-ares) to resolve domain names, not the DNS library provided by the system, so if you have some custom DNS routing set up, it might be ignored. Nodemailer runs [dns.resolve4()](https://nodejs.org/dist/latest-v16.x/docs/api/dns.html#dnsresolve4hostname-options-callback) and [dns.resolve6()](https://nodejs.org/dist/latest-v16.x/docs/api/dns.html#dnsresolve6hostname-options-callback) to resolve hostname into an IP address. If both calls fail, then Nodemailer will fall back to [dns.lookup()](https://nodejs.org/dist/latest-v16.x/docs/api/dns.html#dnslookuphostname-options-callback). If this does not work for you, you can hard code the IP address into the configuration like shown below. In that case, Nodemailer would not perform any DNS lookups. + +```js +let configOptions = { + host: '1.2.3.4', + port: 465, + secure: true, + tls: { + // must provide server name, otherwise TLS certificate check will fail + servername: 'example.com' + } +}; +``` + +#### 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). + +#### I have a different problem + +If you are having issues with Nodemailer, then the best way to find help would be [Stack Overflow](https://stackoverflow.com/search?q=nodemailer) or revisit the [docs](https://nodemailer.com/about/). + +### License + +Nodemailer is licensed under the **MIT No Attribution license** + +--- + +The Nodemailer logo was designed by [Sven Kristjansen](https://www.behance.net/kristjansen). diff --git a/node_modules/nodemailer/SECURITY.txt b/node_modules/nodemailer/SECURITY.txt new file mode 100644 index 0000000..27a54d3 --- /dev/null +++ b/node_modules/nodemailer/SECURITY.txt @@ -0,0 +1,22 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA256 + +Contact: mailto:andris@reinman.eu +Encryption: https://keys.openpgp.org/vks/v1/by-fingerprint/5D952A46E1D8C931F6364E01DC6C83F4D584D364 +Preferred-Languages: en, et +-----BEGIN PGP SIGNATURE----- + +iQIzBAEBCAAdFiEEXZUqRuHYyTH2Nk4B3GyD9NWE02QFAmFDnUgACgkQ3GyD9NWE +02RqUA/+MM3afmRYq874C7wp+uN6dTMCvUX5g5zqBZ2yKpFr46L+PYvM7o8TMm5h +hmLT2I1zZmi+xezOL3zHFizaw0tKkZIz9cWl3Jrgs0FLp0zOsSz1xucp9Q2tYM/Q +vbiP6ys0gbim4tkDGRmZOEiO23s0BuRnmHt7vZg210O+D105Yd8/Ohzbj6PSLBO5 +W1tA7Xw5t0FQ14NNH5+MKyDIKoCX12n0FmrC6qLTXeojf291UgKhCUPda3LIGTmx +mTXz0y68149Mw+JikRCYP8HfGRY9eA4XZrYXF7Bl2T9OJpKD3JAH+69P3xBw19Gn +Csaw3twu8P1bxoVGjY4KRrBOp68W8TwZYjWVWbqY6oV8hb/JfrMxa+kaSxRuloFs +oL6+phrDSPTWdOj2LlEDBJbPOMeDFzIlsBBcJ/JHCEHTvlHl7LoWr3YuWce9PUwl +4r3JUovvaeuJxLgC0vu3WCB3Jeocsl3SreqNkrVc1IjvkSomn3YGm5nCNAd/2F0V +exCGRk/8wbkSjAY38GwQ8K/VuFsefWN3L9sVwIMAMu88KFCAN+GzVFiwvyIXehF5 +eogP9mIXzdQ5YReQjUjApOzGz54XnDyv9RJ3sdvMHosLP+IOg+0q5t9agWv6aqSR +2HzCpiQnH/gmM5NS0AU4Koq/L7IBeLu1B8+61/+BiHgZJJmPdgU= +=BUZr +-----END PGP SIGNATURE----- diff --git a/node_modules/nodemailer/eslint.config.js b/node_modules/nodemailer/eslint.config.js new file mode 100644 index 0000000..fde0594 --- /dev/null +++ b/node_modules/nodemailer/eslint.config.js @@ -0,0 +1,88 @@ +'use strict'; + +const globals = require('globals'); + +module.exports = [ + { + ignores: ['node_modules/**', 'coverage/**', 'dist/**', 'build/**', '.nyc_output/**'] + }, + { + files: ['**/*.js'], + languageOptions: { + ecmaVersion: 2017, + sourceType: 'script', + globals: Object.assign({}, globals.node, globals.es2017, { + it: true, + describe: true, + beforeEach: true, + afterEach: true + }) + }, + rules: { + // Error detection + 'for-direction': 'error', + 'no-await-in-loop': 'error', + 'no-div-regex': 'error', + eqeqeq: 'error', + 'dot-notation': 'error', + curly: 'error', + 'no-fallthrough': 'error', + 'no-unused-expressions': [ + 'error', + { + allowShortCircuit: true + } + ], + 'no-unused-vars': [ + 'error', + { + varsIgnorePattern: '^_', + argsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_' + } + ], + 'handle-callback-err': 'error', + 'no-new': 'error', + 'new-cap': 'error', + 'no-eval': 'error', + 'no-invalid-this': 'error', + radix: ['error', 'always'], + 'no-use-before-define': ['error', 'nofunc'], + 'callback-return': ['error', ['callback', 'cb', 'done']], + 'no-regex-spaces': 'error', + 'no-empty': 'error', + 'no-duplicate-case': 'error', + 'no-empty-character-class': 'error', + 'no-redeclare': 'off', // Disabled per project preference + 'block-scoped-var': 'error', + 'no-sequences': 'error', + 'no-throw-literal': 'error', + 'no-useless-call': 'error', + 'no-useless-concat': 'error', + 'no-void': 'error', + yoda: 'error', + 'no-undef': 'error', + 'global-require': 'error', + 'no-var': 'error', + 'no-bitwise': 'error', + 'no-lonely-if': 'error', + 'no-mixed-spaces-and-tabs': 'error', + 'arrow-body-style': ['error', 'as-needed'], + 'arrow-parens': ['error', 'as-needed'], + 'prefer-arrow-callback': 'error', + 'object-shorthand': 'error', + 'prefer-spread': 'error', + 'no-prototype-builtins': 'off', // Disabled per project preference + strict: ['error', 'global'], + + // Disable all formatting rules (handled by Prettier) + indent: 'off', + quotes: 'off', + 'linebreak-style': 'off', + semi: 'off', + 'quote-props': 'off', + 'comma-dangle': 'off', + 'comma-style': 'off' + } + } +]; diff --git a/node_modules/nodemailer/lib/addressparser/index.js b/node_modules/nodemailer/lib/addressparser/index.js new file mode 100644 index 0000000..25ea950 --- /dev/null +++ b/node_modules/nodemailer/lib/addressparser/index.js @@ -0,0 +1,397 @@ +'use strict'; + +/** + * Converts tokens for a single address into an address object + * + * @param {Array} tokens Tokens object + * @param {Number} depth Current recursion depth for nested group protection + * @return {Object} Address object + */ +function _handleAddress(tokens, depth) { + let isGroup = false; + let state = 'text'; + let address; + let addresses = []; + let data = { + address: [], + comment: [], + group: [], + text: [], + textWasQuoted: [] // Track which text tokens came from inside quotes + }; + let i; + let len; + let insideQuotes = false; // Track if we're currently inside a quoted string + + // Filter out , (comments) and regular text + for (i = 0, len = tokens.length; i < len; i++) { + let token = tokens[i]; + let prevToken = i ? tokens[i - 1] : null; + if (token.type === 'operator') { + switch (token.value) { + case '<': + state = 'address'; + insideQuotes = false; + break; + case '(': + state = 'comment'; + insideQuotes = false; + break; + case ':': + state = 'group'; + isGroup = true; + insideQuotes = false; + break; + case '"': + // Track quote state for text tokens + insideQuotes = !insideQuotes; + state = 'text'; + break; + default: + state = 'text'; + insideQuotes = false; + break; + } + } else if (token.value) { + if (state === 'address') { + // handle use case where unquoted name includes a "<" + // Apple Mail truncates everything between an unexpected < and an address + // and so will we + token.value = token.value.replace(/^[^<]*<\s*/, ''); + } + + if (prevToken && prevToken.noBreak && data[state].length) { + // join values + data[state][data[state].length - 1] += token.value; + if (state === 'text' && insideQuotes) { + data.textWasQuoted[data.textWasQuoted.length - 1] = true; + } + } else { + data[state].push(token.value); + if (state === 'text') { + data.textWasQuoted.push(insideQuotes); + } + } + } + } + + // If there is no text but a comment, replace the two + if (!data.text.length && data.comment.length) { + data.text = data.comment; + data.comment = []; + } + + if (isGroup) { + // http://tools.ietf.org/html/rfc2822#appendix-A.1.3 + data.text = data.text.join(' '); + + // Parse group members, but flatten any nested groups (RFC 5322 doesn't allow nesting) + let groupMembers = []; + if (data.group.length) { + let parsedGroup = addressparser(data.group.join(','), { _depth: depth + 1 }); + // Flatten: if any member is itself a group, extract its members into the sequence + parsedGroup.forEach(member => { + if (member.group) { + // Nested group detected - flatten it by adding its members directly + groupMembers = groupMembers.concat(member.group); + } else { + groupMembers.push(member); + } + }); + } + + addresses.push({ + name: data.text || (address && address.name), + group: groupMembers + }); + } else { + // If no address was found, try to detect one from regular text + if (!data.address.length && data.text.length) { + for (i = data.text.length - 1; i >= 0; i--) { + // Security fix: Do not extract email addresses from quoted strings + // RFC 5321 allows @ inside quoted local-parts like "user@domain"@example.com + // Extracting emails from quoted text leads to misrouting vulnerabilities + if (!data.textWasQuoted[i] && data.text[i].match(/^[^@\s]+@[^@\s]+$/)) { + data.address = data.text.splice(i, 1); + data.textWasQuoted.splice(i, 1); + break; + } + } + + let _regexHandler = function (address) { + if (!data.address.length) { + data.address = [address.trim()]; + return ' '; + } else { + return address; + } + }; + + // still no address + if (!data.address.length) { + for (i = data.text.length - 1; i >= 0; i--) { + // Security fix: Do not extract email addresses from quoted strings + if (!data.textWasQuoted[i]) { + // fixed the regex to parse email address correctly when email address has more than one @ + data.text[i] = data.text[i].replace(/\s*\b[^@\s]+@[^\s]+\b\s*/, _regexHandler).trim(); + if (data.address.length) { + break; + } + } + } + } + } + + // If there's still is no text but a comment exixts, replace the two + if (!data.text.length && data.comment.length) { + data.text = data.comment; + data.comment = []; + } + + // Keep only the first address occurence, push others to regular text + if (data.address.length > 1) { + data.text = data.text.concat(data.address.splice(1)); + } + + // Join values with spaces + data.text = data.text.join(' '); + data.address = data.address.join(' '); + + if (!data.address && isGroup) { + return []; + } else { + address = { + address: data.address || data.text || '', + name: data.text || data.address || '' + }; + + if (address.address === address.name) { + if ((address.address || '').match(/@/)) { + address.name = ''; + } else { + address.address = ''; + } + } + + addresses.push(address); + } + } + + return addresses; +} + +/** + * Creates a Tokenizer object for tokenizing address field strings + * + * @constructor + * @param {String} str Address field string + */ +class Tokenizer { + constructor(str) { + this.str = (str || '').toString(); + this.operatorCurrent = ''; + this.operatorExpecting = ''; + this.node = null; + this.escaped = false; + + this.list = []; + /** + * Operator tokens and which tokens are expected to end the sequence + */ + this.operators = { + '"': '"', + '(': ')', + '<': '>', + ',': '', + ':': ';', + // Semicolons are not a legal delimiter per the RFC2822 grammar other + // than for terminating a group, but they are also not valid for any + // other use in this context. Given that some mail clients have + // historically allowed the semicolon as a delimiter equivalent to the + // comma in their UI, it makes sense to treat them the same as a comma + // when used outside of a group. + ';': '' + }; + } + + /** + * Tokenizes the original input string + * + * @return {Array} An array of operator|text tokens + */ + tokenize() { + let list = []; + + for (let i = 0, len = this.str.length; i < len; i++) { + let chr = this.str.charAt(i); + let nextChr = i < len - 1 ? this.str.charAt(i + 1) : null; + this.checkChar(chr, nextChr); + } + + this.list.forEach(node => { + node.value = (node.value || '').toString().trim(); + if (node.value) { + list.push(node); + } + }); + + return list; + } + + /** + * Checks if a character is an operator or text and acts accordingly + * + * @param {String} chr Character from the address field + */ + checkChar(chr, nextChr) { + if (this.escaped) { + // ignore next condition blocks + } else if (chr === this.operatorExpecting) { + this.node = { + type: 'operator', + value: chr + }; + + if (nextChr && ![' ', '\t', '\r', '\n', ',', ';'].includes(nextChr)) { + this.node.noBreak = true; + } + + this.list.push(this.node); + this.node = null; + this.operatorExpecting = ''; + this.escaped = false; + + return; + } else if (!this.operatorExpecting && chr in this.operators) { + this.node = { + type: 'operator', + value: chr + }; + this.list.push(this.node); + this.node = null; + this.operatorExpecting = this.operators[chr]; + this.escaped = false; + return; + } else if (['"', "'"].includes(this.operatorExpecting) && chr === '\\') { + this.escaped = true; + return; + } + + if (!this.node) { + this.node = { + type: 'text', + value: '' + }; + this.list.push(this.node); + } + + if (chr === '\n') { + // Convert newlines to spaces. Carriage return is ignored as \r and \n usually + // go together anyway and there already is a WS for \n. Lone \r means something is fishy. + chr = ' '; + } + + if (chr.charCodeAt(0) >= 0x21 || [' ', '\t'].includes(chr)) { + // skip command bytes + this.node.value += chr; + } + + this.escaped = false; + } +} + +/** + * Maximum recursion depth for parsing nested groups. + * RFC 5322 doesn't allow nested groups, so this is a safeguard against + * malicious input that could cause stack overflow. + */ +const MAX_NESTED_GROUP_DEPTH = 50; + +/** + * Parses structured e-mail addresses from an address field + * + * Example: + * + * 'Name ' + * + * will be converted to + * + * [{name: 'Name', address: 'address@domain'}] + * + * @param {String} str Address field + * @param {Object} options Optional options object + * @param {Number} options._depth Internal recursion depth counter (do not set manually) + * @return {Array} An array of address objects + */ +function addressparser(str, options) { + options = options || {}; + let depth = options._depth || 0; + + // Prevent stack overflow from deeply nested groups (DoS protection) + if (depth > MAX_NESTED_GROUP_DEPTH) { + return []; + } + + let tokenizer = new Tokenizer(str); + let tokens = tokenizer.tokenize(); + + let addresses = []; + let address = []; + let parsedAddresses = []; + + tokens.forEach(token => { + if (token.type === 'operator' && (token.value === ',' || token.value === ';')) { + if (address.length) { + addresses.push(address); + } + address = []; + } else { + address.push(token); + } + }); + + if (address.length) { + addresses.push(address); + } + + addresses.forEach(address => { + address = _handleAddress(address, depth); + if (address.length) { + parsedAddresses = parsedAddresses.concat(address); + } + }); + + // Merge fragments from unquoted display names containing commas/semicolons. + // When "Joe Foo, PhD " is split on the comma, it produces + // [{name:"Joe Foo", address:""}, {name:"PhD", address:"joe@example.com"}]. + // Detect this pattern and recombine: a name-only entry followed by an entry + // that has both a name and an address (from angle-bracket notation). + for (let i = parsedAddresses.length - 2; i >= 0; i--) { + let current = parsedAddresses[i]; + let next = parsedAddresses[i + 1]; + if (current.address === '' && current.name && !current.group && next.address && next.name && !next.group) { + next.name = current.name + ', ' + next.name; + parsedAddresses.splice(i, 1); + } + } + + if (options.flatten) { + let addresses = []; + let walkAddressList = list => { + list.forEach(address => { + if (address.group) { + return walkAddressList(address.group); + } else { + addresses.push(address); + } + }); + }; + walkAddressList(parsedAddresses); + return addresses; + } + + return parsedAddresses; +} + +// expose to the world +module.exports = addressparser; diff --git a/node_modules/nodemailer/lib/base64/index.js b/node_modules/nodemailer/lib/base64/index.js new file mode 100644 index 0000000..c23becc --- /dev/null +++ b/node_modules/nodemailer/lib/base64/index.js @@ -0,0 +1,139 @@ +'use strict'; + +const Transform = require('stream').Transform; + +/** + * Encodes a Buffer into a base64 encoded string + * + * @param {Buffer} buffer Buffer to convert + * @returns {String} base64 encoded string + */ +function encode(buffer) { + if (typeof buffer === 'string') { + buffer = Buffer.from(buffer, 'utf-8'); + } + + return buffer.toString('base64'); +} + +/** + * Adds soft line breaks to a base64 string + * + * @param {String} str base64 encoded string that might need line wrapping + * @param {Number} [lineLength=76] Maximum allowed length for a line + * @returns {String} Soft-wrapped base64 encoded string + */ +function wrap(str, lineLength) { + str = (str || '').toString(); + lineLength = lineLength || 76; + + if (str.length <= lineLength) { + return str; + } + + let result = []; + let pos = 0; + let chunkLength = lineLength * 1024; + while (pos < str.length) { + let wrappedLines = str.substr(pos, chunkLength).replace(new RegExp('.{' + lineLength + '}', 'g'), '$&\r\n'); + result.push(wrappedLines); + pos += chunkLength; + } + + return result.join(''); +} + +/** + * Creates a transform stream for encoding data to base64 encoding + * + * @constructor + * @param {Object} options Stream options + * @param {Number} [options.lineLength=76] Maximum length for lines, set to false to disable wrapping + */ +class Encoder extends 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.inputBytes = 0; + this.outputBytes = 0; + } + + _transform(chunk, encoding, done) { + if (encoding !== 'buffer') { + chunk = Buffer.from(chunk, encoding); + } + + if (!chunk || !chunk.length) { + return setImmediate(done); + } + + this.inputBytes += chunk.length; + + if (this._remainingBytes && this._remainingBytes.length) { + chunk = Buffer.concat([this._remainingBytes, chunk], this._remainingBytes.length + chunk.length); + this._remainingBytes = false; + } + + if (chunk.length % 3) { + this._remainingBytes = chunk.slice(chunk.length - (chunk.length % 3)); + chunk = chunk.slice(0, chunk.length - (chunk.length % 3)); + } else { + this._remainingBytes = false; + } + + let b64 = this._curLine + encode(chunk); + + if (this.options.lineLength) { + b64 = wrap(b64, this.options.lineLength); + + let lastLF = b64.lastIndexOf('\n'); + if (lastLF < 0) { + this._curLine = b64; + b64 = ''; + } else { + this._curLine = b64.substring(lastLF + 1); + b64 = b64.substring(0, lastLF + 1); + + if (b64 && !b64.endsWith('\r\n')) { + b64 += '\r\n'; + } + } + } else { + this._curLine = ''; + } + + if (b64) { + this.outputBytes += b64.length; + this.push(Buffer.from(b64, 'ascii')); + } + + setImmediate(done); + } + + _flush(done) { + if (this._remainingBytes && this._remainingBytes.length) { + this._curLine += encode(this._remainingBytes); + } + + if (this._curLine) { + this.outputBytes += this._curLine.length; + this.push(Buffer.from(this._curLine, 'ascii')); + this._curLine = ''; + } + done(); + } +} + +module.exports = { + encode, + wrap, + Encoder +}; diff --git a/node_modules/nodemailer/lib/dkim/index.js b/node_modules/nodemailer/lib/dkim/index.js new file mode 100644 index 0000000..e468652 --- /dev/null +++ b/node_modules/nodemailer/lib/dkim/index.js @@ -0,0 +1,253 @@ +'use strict'; + +// FIXME: +// replace this Transform mess with a method that pipes input argument to output argument + +const MessageParser = require('./message-parser'); +const RelaxedBody = require('./relaxed-body'); +const sign = require('./sign'); +const PassThrough = require('stream').PassThrough; +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); + +const DKIM_ALGO = 'sha256'; +const MAX_MESSAGE_SIZE = 2 * 1024 * 1024; // buffer messages larger than this to disk + +/* +// Usage: + +let dkim = new DKIM({ + domainName: 'example.com', + keySelector: 'key-selector', + privateKey, + cacheDir: '/tmp' +}); +dkim.sign(input).pipe(process.stdout); + +// Where inputStream is a rfc822 message (either a stream, string or Buffer) +// and outputStream is a DKIM signed rfc822 message +*/ + +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 + ? path.join(this.cacheDir, 'message.' + Date.now() + '-' + crypto.randomBytes(14).toString('hex')) + : false; + this.cache = false; + + this.headers = false; + this.bodyHash = false; + this.parser = false; + this.relaxedBody = false; + + this.input = input; + this.output = output; + this.output.usingCache = false; + + this.hasErrored = false; + + this.input.on('error', err => { + this.hasErrored = true; + this.cleanup(); + output.emit('error', err); + }); + } + + cleanup() { + if (!this.cache || !this.cachePath) { + return; + } + fs.unlink(this.cachePath, () => false); + } + + createReadCache() { + // pipe remainings to cache file + this.cache = fs.createReadStream(this.cachePath); + this.cache.once('error', err => { + this.cleanup(); + this.output.emit('error', err); + }); + this.cache.once('close', () => { + this.cleanup(); + }); + this.cache.pipe(this.output); + } + + sendNextChunk() { + if (this.hasErrored) { + return; + } + + if (this.readPos >= this.chunks.length) { + if (!this.cache) { + return this.output.end(); + } + return this.createReadCache(); + } + let chunk = this.chunks[this.readPos++]; + if (this.output.write(chunk) === false) { + return this.output.once('drain', () => { + this.sendNextChunk(); + }); + } + setImmediate(() => this.sendNextChunk()); + } + + sendSignedOutput() { + let keyPos = 0; + let signNextKey = () => { + if (keyPos >= this.keys.length) { + this.output.write(this.parser.rawHeaders); + return setImmediate(() => this.sendNextChunk()); + } + let key = this.keys[keyPos++]; + let dkimField = sign(this.headers, this.hashAlgo, this.bodyHash, { + domainName: key.domainName, + keySelector: key.keySelector, + privateKey: key.privateKey, + headerFieldNames: this.options.headerFieldNames, + skipFields: this.options.skipFields + }); + if (dkimField) { + this.output.write(Buffer.from(dkimField + '\r\n')); + } + return setImmediate(signNextKey); + }; + + if (this.bodyHash && this.headers) { + return signNextKey(); + } + + this.output.write(this.parser.rawHeaders); + this.sendNextChunk(); + } + + createWriteCache() { + this.output.usingCache = true; + // pipe remainings to cache file + this.cache = fs.createWriteStream(this.cachePath); + this.cache.once('error', err => { + this.cleanup(); + // drain input + this.relaxedBody.unpipe(this.cache); + this.relaxedBody.on('readable', () => { + while (this.relaxedBody.read() !== null) { + // do nothing + } + }); + this.hasErrored = true; + // emit error + this.output.emit('error', err); + }); + this.cache.once('close', () => { + this.sendSignedOutput(); + }); + this.relaxedBody.removeAllListeners('readable'); + this.relaxedBody.pipe(this.cache); + } + + signStream() { + this.parser = new MessageParser(); + this.relaxedBody = new RelaxedBody({ + 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) { + let output = new PassThrough(); + let inputStream = input; + let writeValue = false; + + if (Buffer.isBuffer(input)) { + writeValue = input; + inputStream = new PassThrough(); + } else if (typeof input === 'string') { + writeValue = Buffer.from(input); + inputStream = new PassThrough(); + } + + let options = this.options; + if (extraOptions && Object.keys(extraOptions).length) { + options = {}; + Object.keys(this.options || {}).forEach(key => { + options[key] = this.options[key]; + }); + Object.keys(extraOptions || {}).forEach(key => { + if (!(key in options)) { + options[key] = extraOptions[key]; + } + }); + } + + let signer = new DKIMSigner(options, this.keys, inputStream, output); + setImmediate(() => { + signer.signStream(); + if (writeValue) { + setImmediate(() => { + inputStream.end(writeValue); + }); + } + }); + + return output; + } +} + +module.exports = DKIM; diff --git a/node_modules/nodemailer/lib/dkim/message-parser.js b/node_modules/nodemailer/lib/dkim/message-parser.js new file mode 100644 index 0000000..8ee93d2 --- /dev/null +++ b/node_modules/nodemailer/lib/dkim/message-parser.js @@ -0,0 +1,155 @@ +'use strict'; + +const Transform = require('stream').Transform; + +/** + * 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 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 {Buffer} data Next data chunk from the stream + */ + updateLastBytes(data) { + let lblen = this.lastBytes.length; + let 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 {Buffer} data Next chunk of data + * @return {Boolean} Returns true if headers are already found or false otherwise + */ + checkHeaders(data) { + if (this.headersParsed) { + return true; + } + + let lblen = this.lastBytes.length; + let headerPos = 0; + this.curLinePos = 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) { + let pr1 = i - 1 < lblen ? this.lastBytes[i - 1] : data[i - 1 - lblen]; + let 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 - 1 > headerPos) { + let chunk = data.slice(headerPos); + this.bodySize += chunk.length; + // this would be the first chunk of data sent downstream + setImmediate(() => this.push(chunk)); + } + return false; + } else { + this.headerBytes += data.length; + this.headerChunks.push(data); + } + + // store last 4 bytes to catch header break + this.updateLastBytes(data); + + return false; + } + + _transform(chunk, encoding, callback) { + if (!chunk || !chunk.length) { + return callback(); + } + + if (typeof chunk === 'string') { + chunk = Buffer.from(chunk, encoding); + } + + let headersFound; + + try { + headersFound = this.checkHeaders(chunk); + } catch (E) { + return callback(E); + } + + if (headersFound) { + this.bodySize += chunk.length; + this.push(chunk); + } + + setImmediate(callback); + } + + _flush(callback) { + if (this.headerChunks) { + let chunk = Buffer.concat(this.headerChunks, this.headerBytes); + this.bodySize += chunk.length; + this.push(chunk); + this.headerChunks = null; + } + callback(); + } + + parseHeaders() { + let lines = (this.rawHeaders || '').toString().split(/\r?\n/); + for (let i = lines.length - 1; i > 0; i--) { + if (/^\s/.test(lines[i])) { + lines[i - 1] += '\n' + lines[i]; + lines.splice(i, 1); + } + } + return lines + .filter(line => line.trim()) + .map(line => ({ + key: line.substr(0, line.indexOf(':')).trim().toLowerCase(), + line + })); + } +} + +module.exports = MessageParser; diff --git a/node_modules/nodemailer/lib/dkim/relaxed-body.js b/node_modules/nodemailer/lib/dkim/relaxed-body.js new file mode 100644 index 0000000..03558e8 --- /dev/null +++ b/node_modules/nodemailer/lib/dkim/relaxed-body.js @@ -0,0 +1,154 @@ +'use strict'; + +// streams through a message body and calculates relaxed body hash + +const Transform = require('stream').Transform; +const crypto = require('crypto'); + +class RelaxedBody extends Transform { + constructor(options) { + super(); + options = options || {}; + this.chunkBuffer = []; + this.chunkBufferLen = 0; + this.bodyHash = crypto.createHash(options.hashAlgo || 'sha1'); + this.remainder = ''; + this.byteLength = 0; + + this.debug = options.debug; + this._debugBody = options.debug ? [] : false; + } + + updateHash(chunk) { + let bodyStr; + + // find next remainder + let nextRemainder = ''; + + // This crux finds and removes the spaces from the last line and the newline characters after the last non-empty line + // If we get another chunk that does not match this description then we can restore the previously processed data + let state = 'file'; + for (let i = chunk.length - 1; i >= 0; i--) { + let c = chunk[i]; + + if (state === 'file' && (c === 0x0a || c === 0x0d)) { + // do nothing, found \n or \r at the end of chunk, stil end of file + } else if (state === 'file' && (c === 0x09 || c === 0x20)) { + // switch to line ending mode, this is the last non-empty line + state = 'line'; + } else if (state === 'line' && (c === 0x09 || c === 0x20)) { + // do nothing, found ' ' or \t at the end of line, keep processing the last non-empty line + } else if (state === 'file' || state === 'line') { + // non line/file ending character found, switch to body mode + state = 'body'; + if (i === chunk.length - 1) { + // final char is not part of line end or file end, so do nothing + break; + } + } + + if (i === 0) { + // reached to the beginning of the chunk, check if it is still about the ending + // and if the remainder also matches + if ( + (state === 'file' && (!this.remainder || /[\r\n]$/.test(this.remainder))) || + (state === 'line' && (!this.remainder || /[ \t]$/.test(this.remainder))) + ) { + // keep everything + this.remainder += chunk.toString('binary'); + return; + } else if (state === 'line' || state === 'file') { + // process existing remainder as normal line but store the current chunk + nextRemainder = chunk.toString('binary'); + chunk = false; + break; + } + } + + if (state !== 'body') { + continue; + } + + // reached first non ending byte + nextRemainder = chunk.slice(i + 1).toString('binary'); + chunk = chunk.slice(0, i + 1); + break; + } + + let needsFixing = !!this.remainder; + if (chunk && !needsFixing) { + // check if we even need to change anything + for (let i = 0, len = chunk.length; i < len; i++) { + if (i && chunk[i] === 0x0a && chunk[i - 1] !== 0x0d) { + // missing \r before \n + needsFixing = true; + break; + } else if (i && chunk[i] === 0x0d && chunk[i - 1] === 0x20) { + // trailing WSP found + needsFixing = true; + break; + } else if (i && chunk[i] === 0x20 && chunk[i - 1] === 0x20) { + // multiple spaces found, needs to be replaced with just one + needsFixing = true; + break; + } else if (chunk[i] === 0x09) { + // TAB found, needs to be replaced with a space + needsFixing = true; + break; + } + } + } + + if (needsFixing) { + bodyStr = this.remainder + (chunk ? chunk.toString('binary') : ''); + this.remainder = nextRemainder; + bodyStr = bodyStr + .replace(/\r?\n/g, '\n') // use js line endings + .replace(/[ \t]*$/gm, '') // remove line endings, rtrim + .replace(/[ \t]+/gm, ' ') // single spaces + .replace(/\n/g, '\r\n'); // restore rfc822 line endings + chunk = Buffer.from(bodyStr, 'binary'); + } else if (nextRemainder) { + this.remainder = nextRemainder; + } + + if (this.debug) { + this._debugBody.push(chunk); + } + this.bodyHash.update(chunk); + } + + _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(); + } + + _flush(callback) { + // generate final hash and emit it + if (/[\r\n]$/.test(this.remainder) && this.byteLength > 2) { + // add terminating line end + this.bodyHash.update(Buffer.from('\r\n')); + } + if (!this.byteLength) { + // emit empty line buffer to keep the stream flowing + this.push(Buffer.from('\r\n')); + // this.bodyHash.update(Buffer.from('\r\n')); + } + + this.emit('hash', this.bodyHash.digest('base64'), this.debug ? Buffer.concat(this._debugBody) : false); + callback(); + } +} + +module.exports = RelaxedBody; diff --git a/node_modules/nodemailer/lib/dkim/sign.js b/node_modules/nodemailer/lib/dkim/sign.js new file mode 100644 index 0000000..4a103a4 --- /dev/null +++ b/node_modules/nodemailer/lib/dkim/sign.js @@ -0,0 +1,117 @@ +'use strict'; + +const punycode = require('../punycode'); +const mimeFuncs = require('../mime-funcs'); +const crypto = require('crypto'); + +/** + * Returns DKIM signature header line + * + * @param {Object} headers Parsed headers object from MessageParser + * @param {String} bodyHash Base64 encoded hash of the message + * @param {Object} options DKIM options + * @param {String} options.domainName Domain name to be signed for + * @param {String} options.keySelector DKIM key selector to use + * @param {String} options.privateKey DKIM private key to use + * @return {String} Complete header line + */ + +module.exports = (headers, hashAlgo, bodyHash, options) => { + options = options || {}; + + // all listed fields from RFC4871 #5.5 + let 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'; + + let fieldNames = options.headerFieldNames || defaultFieldNames; + + let canonicalizedHeaderData = relaxedHeaders(headers, fieldNames, options.skipFields); + let dkimHeader = generateDKIMHeader(options.domainName, options.keySelector, canonicalizedHeaderData.fieldNames, hashAlgo, bodyHash); + + let signer, signature; + + canonicalizedHeaderData.headers += 'dkim-signature:' + relaxedHeaderLine(dkimHeader); + + signer = crypto.createSign(('rsa-' + hashAlgo).toUpperCase()); + signer.update(canonicalizedHeaderData.headers); + try { + signature = signer.sign(options.privateKey, 'base64'); + } catch (_E) { + return false; + } + + return dkimHeader + signature.replace(/(^.{73}|.{75}(?!\r?\n|\r))/g, '$&\r\n ').trim(); +}; + +module.exports.relaxedHeaders = relaxedHeaders; + +function generateDKIMHeader(domainName, keySelector, fieldNames, hashAlgo, bodyHash) { + let dkim = [ + 'v=1', + 'a=rsa-' + hashAlgo, + 'c=relaxed/relaxed', + 'd=' + punycode.toASCII(domainName), + 'q=dns/txt', + 's=' + keySelector, + 'bh=' + bodyHash, + 'h=' + fieldNames + ].join('; '); + + return mimeFuncs.foldLines('DKIM-Signature: ' + dkim, 76) + ';\r\n b='; +} + +function relaxedHeaders(headers, fieldNames, skipFields) { + let includedFields = new Set(); + let skip = new Set(); + let 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--) { + let 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)); + } + } + + let headersList = []; + let 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(':') + }; +} + +function relaxedHeaderLine(line) { + return line + .substr(line.indexOf(':') + 1) + .replace(/\r?\n/g, '') + .replace(/\s+/g, ' ') + .trim(); +} diff --git a/node_modules/nodemailer/lib/errors.js b/node_modules/nodemailer/lib/errors.js new file mode 100644 index 0000000..86177f0 --- /dev/null +++ b/node_modules/nodemailer/lib/errors.js @@ -0,0 +1,61 @@ +'use strict'; + +/** + * Nodemailer Error Codes + * + * Centralized error code definitions for consistent error handling. + * + * Usage: + * const errors = require('./errors'); + * let err = new Error('Connection closed'); + * err.code = errors.ECONNECTION; + */ + +/** + * Error code descriptions for documentation and debugging + */ +const 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)', + + // 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' +}; + +// Export error codes as string constants and the full definitions object +module.exports = Object.keys(ERROR_CODES).reduce( + (exports, code) => { + exports[code] = code; + return exports; + }, + { ERROR_CODES } +); diff --git a/node_modules/nodemailer/lib/fetch/cookies.js b/node_modules/nodemailer/lib/fetch/cookies.js new file mode 100644 index 0000000..917b840 --- /dev/null +++ b/node_modules/nodemailer/lib/fetch/cookies.js @@ -0,0 +1,281 @@ +'use strict'; + +// module to handle cookies + +const urllib = require('url'); + +const SESSION_TIMEOUT = 1800; // 30 min + +/** + * Creates a biskviit cookie jar for managing cookie values in memory + * + * @constructor + * @param {Object} [options] Optional options object + */ +class Cookies { + constructor(options) { + this.options = options || {}; + this.cookies = []; + } + + /** + * Stores a cookie string to the cookie storage + * + * @param {String} cookieStr Value from the 'Set-Cookie:' header + * @param {String} url Current URL + */ + set(cookieStr, url) { + let urlparts = urllib.parse(url || ''); + let cookie = this.parse(cookieStr); + let domain; + + if (cookie.domain) { + domain = cookie.domain.replace(/^\./, ''); + + // do not allow cross origin cookies + if ( + // can't be valid if the requested domain is shorter than current hostname + urlparts.hostname.length < domain.length || + // prefix domains with dot to be sure that partial matches are not used + ('.' + urlparts.hostname).substr(-domain.length + 1) !== '.' + 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 {String} url URL to check for + * @returns {String} 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 {String} url URL to check for + * @returns {Array} An array of cookie objects + */ + list(url) { + let result = []; + let i; + let cookie; + + for (i = this.cookies.length - 1; i >= 0; i--) { + cookie = this.cookies[i]; + + if (this.isExpired(cookie)) { + this.cookies.splice(i, i); + continue; + } + + if (this.match(cookie, url)) { + result.unshift(cookie); + } + } + + return result; + } + + /** + * Parses cookie string from the 'Set-Cookie:' header + * + * @param {String} cookieStr String from the 'Set-Cookie:' header + * @returns {Object} Cookie object + */ + parse(cookieStr) { + let cookie = {}; + + (cookieStr || '') + .toString() + .split(';') + .forEach(cookiePart => { + let valueParts = cookiePart.split('='); + let key = valueParts.shift().trim().toLowerCase(); + let value = valueParts.join('=').trim(); + let domain; + + if (!key) { + // skip empty parts + return; + } + + switch (key) { + case 'expires': + value = new Date(value); + // ignore date if can not parse it + if (value.toString() !== 'Invalid Date') { + cookie.expires = value; + } + 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; + + default: + if (!cookie.name) { + cookie.name = key; + cookie.value = value; + } + } + }); + + return cookie; + } + + /** + * Checks if a cookie object is valid for a specified URL + * + * @param {Object} cookie Cookie object + * @param {String} url URL to check for + * @returns {Boolean} true if cookie is valid for specifiec URL + */ + match(cookie, url) { + let 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 path matches + let path = this.getPath(urlparts.pathname); + if (path.substr(0, cookie.path.length) !== cookie.path) { + 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 {Object} cookie Cookie value to be stored + */ + add(cookie) { + let i; + let len; + + // nothing to do here + if (!cookie || !cookie.name) { + return false; + } + + // overwrite if has same params + for (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 {Object} a Cookie to check against + * @param {Object} b Cookie to check against + * @returns {Boolean} 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 === a.httponly; + } + + /** + * Checks if a cookie is expired + * + * @param {Object} cookie Cookie object to check against + * @returns {Boolean} True, if the cookie is expired + */ + isExpired(cookie) { + return (cookie.expires && cookie.expires < new Date()) || !cookie.value; + } + + /** + * Returns normalized cookie path for an URL path argument + * + * @param {String} pathname + * @returns {String} Normalized path + */ + getPath(pathname) { + let path = (pathname || '/').split('/'); + path.pop(); // remove filename part + path = path.join('/').trim(); + + // ensure path prefix / + if (path.charAt(0) !== '/') { + path = '/' + path; + } + + // ensure path suffix / + if (path.substr(-1) !== '/') { + path += '/'; + } + + return path; + } +} + +module.exports = Cookies; diff --git a/node_modules/nodemailer/lib/fetch/index.js b/node_modules/nodemailer/lib/fetch/index.js new file mode 100644 index 0000000..3593a3d --- /dev/null +++ b/node_modules/nodemailer/lib/fetch/index.js @@ -0,0 +1,281 @@ +'use strict'; + +const http = require('http'); +const https = require('https'); +const urllib = require('url'); +const zlib = require('zlib'); +const PassThrough = require('stream').PassThrough; +const Cookies = require('./cookies'); +const packageData = require('../../package.json'); +const net = require('net'); +const errors = require('../errors'); + +const MAX_REDIRECTS = 5; + +module.exports = function (url, options) { + return nmfetch(url, options); +}; + +module.exports.Cookies = Cookies; + +function nmfetch(url, options) { + options = options || {}; + + options.fetchRes = options.fetchRes || new PassThrough(); + options.cookies = options.cookies || new Cookies(); + options.redirects = options.redirects || 0; + options.maxRedirects = isNaN(options.maxRedirects) ? MAX_REDIRECTS : options.maxRedirects; + + if (options.cookie) { + [].concat(options.cookie || []).forEach(cookie => { + options.cookies.set(cookie, url); + }); + options.cookie = false; + } + + let fetchRes = options.fetchRes; + let parsed = urllib.parse(url); + let method = (options.method || '').toString().trim().toUpperCase() || 'GET'; + let finished = false; + let cookies; + let body; + + let handler = parsed.protocol === 'https:' ? https : http; + + let headers = { + 'accept-encoding': 'gzip,deflate', + 'user-agent': 'nodemailer/' + packageData.version + }; + + Object.keys(options.headers || {}).forEach(key => { + 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; + err.code = errors.EFETCH; + 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 => { + let value = options.body[key].toString().trim(); + return encodeURIComponent(key) + '=' + encodeURIComponent(value); + }) + .join('&') + ); + } catch (E) { + if (finished) { + return; + } + finished = true; + E.code = errors.EFETCH; + E.sourceUrl = url; + fetchRes.emit('error', E); + return; + } + } 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; + let reqOptions = { + method, + host: parsed.hostname, + path: parsed.path, + port: parsed.port ? parsed.port : parsed.protocol === 'https:' ? 443 : 80, + headers, + rejectUnauthorized: false, + agent: false + }; + + if (options.tls) { + Object.keys(options.tls).forEach(key => { + reqOptions[key] = options.tls[key]; + }); + } + + if ( + parsed.protocol === 'https:' && + parsed.hostname && + parsed.hostname !== reqOptions.host && + !net.isIP(parsed.hostname) && + !reqOptions.servername + ) { + reqOptions.servername = parsed.hostname; + } + + try { + req = handler.request(reqOptions); + } catch (E) { + finished = true; + setImmediate(() => { + E.code = errors.EFETCH; + E.sourceUrl = url; + fetchRes.emit('error', E); + }); + return fetchRes; + } + + if (options.timeout) { + req.setTimeout(options.timeout, () => { + if (finished) { + return; + } + finished = true; + req.abort(); + let err = new Error('Request Timeout'); + err.code = errors.EFETCH; + err.sourceUrl = url; + fetchRes.emit('error', err); + }); + } + + req.on('error', err => { + if (finished) { + return; + } + finished = true; + err.code = errors.EFETCH; + err.sourceUrl = url; + fetchRes.emit('error', err); + }); + + req.on('response', res => { + let inflate; + + if (finished) { + return; + } + + switch (res.headers['content-encoding']) { + case 'gzip': + case 'deflate': + inflate = zlib.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) { + finished = true; + let err = new Error('Maximum redirect count exceeded'); + err.code = errors.EFETCH; + err.sourceUrl = url; + fetchRes.emit('error', err); + req.abort(); + return; + } + // redirect does not include POST body + options.method = 'GET'; + options.body = false; + return nmfetch(urllib.resolve(url, res.headers.location), options); + } + + fetchRes.statusCode = res.statusCode; + fetchRes.headers = res.headers; + + if (res.statusCode >= 300 && !options.allowErrorResponse) { + finished = true; + let err = new Error('Invalid status code ' + res.statusCode); + err.code = errors.EFETCH; + err.sourceUrl = url; + fetchRes.emit('error', err); + req.abort(); + return; + } + + res.on('error', err => { + if (finished) { + return; + } + finished = true; + err.code = errors.EFETCH; + err.sourceUrl = url; + fetchRes.emit('error', err); + req.abort(); + }); + + if (inflate) { + res.pipe(inflate).pipe(fetchRes); + inflate.on('error', err => { + if (finished) { + return; + } + finished = true; + err.code = errors.EFETCH; + err.sourceUrl = url; + fetchRes.emit('error', err); + req.abort(); + }); + } else { + res.pipe(fetchRes); + } + }); + + setImmediate(() => { + if (body) { + try { + if (typeof body.pipe === 'function') { + return body.pipe(req); + } else { + req.write(body); + } + } catch (err) { + finished = true; + err.code = errors.EFETCH; + err.sourceUrl = url; + fetchRes.emit('error', err); + return; + } + } + req.end(); + }); + + return fetchRes; +} diff --git a/node_modules/nodemailer/lib/json-transport/index.js b/node_modules/nodemailer/lib/json-transport/index.js new file mode 100644 index 0000000..769bde6 --- /dev/null +++ b/node_modules/nodemailer/lib/json-transport/index.js @@ -0,0 +1,82 @@ +'use strict'; + +const packageData = require('../../package.json'); +const shared = require('../shared'); + +/** + * Generates a Transport object to generate JSON output + * + * @constructor + * @param {Object} 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' + }); + } + + /** + *

Compiles a mailcomposer message and forwards it to handler that sends it.

+ * + * @param {Object} emailMessage MailComposer object + * @param {Function} callback Callback function to run when the sending is completed + */ + send(mail, done) { + // Sendmail strips this header line by itself + mail.message.keepBcc = true; + + let envelope = mail.data.envelope || mail.message.getEnvelope(); + let messageId = mail.message.messageId(); + + let 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; + + return done(null, { + envelope, + messageId, + message: this.options.skipEncoding ? data : JSON.stringify(data) + }); + }); + }); + } +} + +module.exports = JSONTransport; diff --git a/node_modules/nodemailer/lib/mail-composer/index.js b/node_modules/nodemailer/lib/mail-composer/index.js new file mode 100644 index 0000000..94611a0 --- /dev/null +++ b/node_modules/nodemailer/lib/mail-composer/index.js @@ -0,0 +1,629 @@ +/* eslint no-undefined: 0 */ + +'use strict'; + +const MimeNode = require('../mime-node'); +const mimeFuncs = require('../mime-funcs'); +const parseDataURI = require('../shared').parseDataURI; + +/** + * Creates the object for composing a MimeNode instance out from the mail options + * + * @constructor + * @param {Object} mail Mail options + */ +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 MimeNode('message/rfc822', { newline: this.mail.newline }).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 => { + let 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 {Boolean} findRelated If true separate related attachments from attached ones + * @returns {Object} An object of arrays (`related` and `attached`) + */ + getAttachments(findRelated) { + let icalEvent, eventObject; + let attachments = [].concat(this.mail.attachments || []).map((attachment, i) => { + let data; + + if (/^data:/i.test(attachment.path || attachment.href)) { + attachment = this._processDataUrl(attachment); + } + + let contentType = + attachment.contentType || mimeFuncs.detectMimeType(attachment.filename || attachment.path || attachment.href || 'bin'); + + let isImage = /^image\//i.test(contentType); + let isMessageNode = /^message\//i.test(contentType); + + let 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 + } + + data = { + contentType, + contentDisposition, + contentTransferEncoding + }; + + if (attachment.filename) { + data.filename = attachment.filename; + } else if (!isMessageNode && attachment.filename !== false) { + 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 + }; + } 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) { + if ( + typeof this.mail.icalEvent === 'object' && + (this.mail.icalEvent.content || this.mail.icalEvent.path || this.mail.icalEvent.href || this.mail.icalEvent.raw) + ) { + icalEvent = this.mail.icalEvent; + } else { + icalEvent = { + content: this.mail.icalEvent + }; + } + + eventObject = {}; + Object.keys(icalEvent).forEach(key => { + eventObject[key] = icalEvent[key]; + }); + + 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: [] + }; + } else { + return { + attached: attachments.filter(attachment => !attachment.cid).concat(eventObject || []), + related: attachments.filter(attachment => !!attachment.cid) + }; + } + } + + /** + * List alternatives. Resulting objects can be used as input for MimeNode nodes + * + * @returns {Array} An array of alternative elements. Includes the `text` and `html` values as well + */ + getAlternatives() { + let alternatives = [], + text, + html, + watchHtml, + amp, + icalEvent, + eventObject; + + if (this.mail.text) { + if ( + typeof this.mail.text === 'object' && + (this.mail.text.content || this.mail.text.path || this.mail.text.href || this.mail.text.raw) + ) { + text = this.mail.text; + } else { + text = { + content: this.mail.text + }; + } + text.contentType = 'text/plain; charset=utf-8'; + } + + if (this.mail.watchHtml) { + if ( + typeof this.mail.watchHtml === 'object' && + (this.mail.watchHtml.content || this.mail.watchHtml.path || this.mail.watchHtml.href || this.mail.watchHtml.raw) + ) { + watchHtml = this.mail.watchHtml; + } else { + watchHtml = { + content: this.mail.watchHtml + }; + } + watchHtml.contentType = 'text/watch-html; charset=utf-8'; + } + + if (this.mail.amp) { + if ( + typeof this.mail.amp === 'object' && + (this.mail.amp.content || this.mail.amp.path || this.mail.amp.href || this.mail.amp.raw) + ) { + 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) { + if ( + typeof this.mail.icalEvent === 'object' && + (this.mail.icalEvent.content || this.mail.icalEvent.path || this.mail.icalEvent.href || this.mail.icalEvent.raw) + ) { + icalEvent = this.mail.icalEvent; + } else { + icalEvent = { + content: this.mail.icalEvent + }; + } + + eventObject = {}; + Object.keys(icalEvent).forEach(key => { + eventObject[key] = icalEvent[key]; + }); + + if (eventObject.content && typeof eventObject.content === 'object') { + // we are going to have the same attachment twice, so mark this to be + // resolved just once + eventObject.content._resolve = true; + } + + 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 ( + typeof this.mail.html === 'object' && + (this.mail.html.content || this.mail.html.path || this.mail.html.href || this.mail.html.raw) + ) { + 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 => { + let data; + + if (/^data:/i.test(alternative.path || alternative.href)) { + alternative = this._processDataUrl(alternative); + } + + 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 + }; + } 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 {Object} parentNode Parent for this note. If it does not exist, a root node is created + * @returns {Object} MimeNode node element + */ + _createMixed(parentNode) { + let node; + + if (!parentNode) { + node = new MimeNode('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 + }); + } else { + node = parentNode.createChild('multipart/mixed', { + 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 {Object} parentNode Parent for this note. If it does not exist, a root node is created + * @returns {Object} MimeNode node element + */ + _createAlternative(parentNode) { + let node; + + if (!parentNode) { + node = new MimeNode('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 + }); + } else { + node = parentNode.createChild('multipart/alternative', { + 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 {Object} parentNode Parent for this note. If it does not exist, a root node is created + * @returns {Object} MimeNode node element + */ + _createRelated(parentNode) { + let node; + + if (!parentNode) { + node = new MimeNode('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 + }); + } else { + node = parentNode.createChild('multipart/related; type="text/html"', { + 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 {Object} parentNode Parent for this note. If it does not exist, a root node is created + * @param {Object} element Node data + * @returns {Object} MimeNode node element + */ + _createContentNode(parentNode, element) { + element = element || {}; + element.content = element.content || ''; + + let node; + let encoding = (element.encoding || 'utf8') + .toString() + .toLowerCase() + .replace(/[-_\s]/g, ''); + + if (!parentNode) { + node = new MimeNode(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 + }); + } else { + node = 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 + }); + } + + // 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 {Object} element Content element + * @return {Object} Parsed element + */ + _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({}, element, { + path: false, + href: false, + content: Buffer.alloc(0), + contentType: element.contentType || detectedType + }); + } + + let parsedDataUri; + try { + parsedDataUri = 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; + } +} + +module.exports = MailComposer; diff --git a/node_modules/nodemailer/lib/mailer/index.js b/node_modules/nodemailer/lib/mailer/index.js new file mode 100644 index 0000000..983c71c --- /dev/null +++ b/node_modules/nodemailer/lib/mailer/index.js @@ -0,0 +1,446 @@ +'use strict'; + +const EventEmitter = require('events'); +const shared = require('../shared'); +const mimeTypes = require('../mime-funcs/mime-types'); +const MailComposer = require('../mail-composer'); +const DKIM = require('../dkim'); +const httpProxyClient = require('../smtp-connection/http-proxy-client'); +const errors = require('../errors'); +const util = require('util'); +const urllib = require('url'); +const packageData = require('../../package.json'); +const MailMessage = require('./mail-message'); +const net = require('net'); +const dns = require('dns'); +const crypto = require('crypto'); + +/** + * Creates an object for exposing the Mail API + * + * @constructor + * @param {Object} transporter Transport object instance to pass the mails to + */ +class Mail extends 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 DKIM(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); + } else { + 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; + } + + /** + * Sends an email using the preselected transport object + * + * @param {Object} data E-data description + * @param {Function?} callback Callback to run once the sending succeeded or failed + */ + sendMail(data, callback = null) { + let promise; + + if (!callback) { + promise = new Promise((resolve, reject) => { + callback = shared.callbackPromise(resolve, reject); + }); + } + + if (typeof this.getSocket === 'function') { + this.transporter.getSocket = this.getSocket; + this.getSocket = false; + } + + let mail = new MailMessage(this, data); + + 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 callback(err); + } + + mail.message = new MailComposer(mail.data).compile(); + + mail.setMailerHeader(); + mail.setPriorityHeaders(); + mail.setListHeaders(); + + this._processPlugins('stream', mail, err => { + if (err) { + this.logger.error( + { + err, + tnx: 'plugin', + action: 'stream' + }, + 'PluginStream Error: %s', + err.message + ); + return callback(err); + } + + if (mail.data.dkim || this.dkim) { + mail.message.processFunc(input => { + let dkim = mail.data.dkim ? new DKIM(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]) { + this.logger.error( + { + err: args[0], + tnx: 'transport', + action: 'send' + }, + 'Send Error: %s', + args[0].message + ); + } + callback(...args); + }); + }); + }); + + return promise; + } + + getVersionString() { + return util.format( + '%s (%s; +%s; %s/%s)', + packageData.name, + packageData.version, + packageData.homepage, + this.transporter.name, + this.transporter.version + ); + } + + _processPlugins(step, mail, callback) { + step = (step || '').toString(); + + if (!this._userPlugins.hasOwnProperty(step)) { + return callback(); + } + + let userPlugins = this._userPlugins[step] || []; + let 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'; + let 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(); + } + } + let plugin = curplugins[pos++]; + plugin(mail, err => { + if (err) { + return callback(err); + } + processPlugins(); + }); + }; + + processPlugins(); + } + + /** + * Sets up proxy handler for a Nodemailer object + * + * @param {String} proxyUrl Proxy configuration url + */ + setupProxy(proxyUrl) { + let proxy = urllib.parse(proxyUrl); + + // setup socket handler for the mailer object + this.getSocket = (options, callback) => { + let protocol = proxy.protocol.replace(/:$/, '').toLowerCase(); + + 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': + httpProxyClient(proxy.href, options.port, options.host, (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); + } + let connect = ipaddress => { + let proxyV2 = !!this.meta.get('proxy_socks_module').SocksClient; + let socksClient = proxyV2 ? this.meta.get('proxy_socks_module').SocksClient : this.meta.get('proxy_socks_module'); + let proxyType = Number(proxy.protocol.replace(/\D/g, '')) || 5; + let connectionOpts = { + proxy: { + ipaddress, + port: Number(proxy.port), + type: proxyType + }, + [proxyV2 ? 'destination' : 'target']: { + host: options.host, + port: options.port + }, + command: 'connect' + }; + + if (proxy.auth) { + let username = decodeURIComponent(proxy.auth.split(':').shift()); + let password = decodeURIComponent(proxy.auth.split(':').pop()); + 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 (net.isIP(proxy.hostname)) { + return connect(proxy.hostname); + } + + return dns.resolve(proxy.hostname, (err, address) => { + if (err) { + return callback(err); + } + connect(Array.isArray(address) ? address[0] : address); + }); + } + } + let err = new Error('Unknown proxy configuration'); + err.code = errors.EPROXY; + callback(err); + }; + } + + _convertDataImages(mail, callback) { + if ((!this.options.attachDataUrls && !mail.data.attachDataUrls) || !mail.data.html) { + return callback(); + } + mail.resolveContent(mail.data, 'html', (err, html) => { + if (err) { + return callback(err); + } + let cidCounter = 0; + html = (html || '') + .toString() + .replace(/(]{0,1024} src\s{0,20}=[\s"']{0,20})(data:([^;]+);[^"'>\s]+)/gi, (match, prefix, dataUri, mimeType) => { + let cid = crypto.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); + } +} + +module.exports = Mail; diff --git a/node_modules/nodemailer/lib/mailer/mail-message.js b/node_modules/nodemailer/lib/mailer/mail-message.js new file mode 100644 index 0000000..e089c10 --- /dev/null +++ b/node_modules/nodemailer/lib/mailer/mail-message.js @@ -0,0 +1,316 @@ +'use strict'; + +const shared = require('../shared'); +const MimeNode = require('../mime-node'); +const mimeFuncs = require('../mime-funcs'); + +class MailMessage { + constructor(mailer, data) { + this.mailer = mailer; + this.data = {}; + this.message = null; + + data = data || {}; + let options = mailer.options || {}; + let defaults = mailer._defaults || {}; + + Object.keys(data).forEach(key => { + this.data[key] = data[key]; + }); + + this.data.headers = this.data.headers || {}; + + // apply defaults + Object.keys(defaults).forEach(key => { + if (!(key in this.data)) { + this.data[key] = defaults[key]; + } else if (key === 'headers') { + // headers is a special case. Allow setting individual default headers + Object.keys(defaults.headers).forEach(key => { + if (!(key in this.data.headers)) { + this.data.headers[key] = defaults.headers[key]; + } + }); + } + }); + + // force specific keys from transporter options + ['disableFileAccess', 'disableUrlAccess', 'normalizeHeaderKey'].forEach(key => { + if (key in options) { + this.data[key] = options[key]; + } + }); + } + + resolveContent(...args) { + return shared.resolveContent(...args); + } + + resolveAll(callback) { + let 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) { + 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]); + }); + } + + let mimeNode = new MimeNode(); + + let 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; + } + }); + + let singleKeys = ['from', 'sender']; + singleKeys.forEach(address => { + if (this.data[address]) { + this.data[address] = this.data[address].shift(); + } + }); + + let pos = 0; + let resolveNext = () => { + if (pos >= keys.length) { + return callback(null, this.data); + } + let args = keys[pos++]; + if (!args[0] || !args[0][args[1]]) { + return resolveNext(); + } + shared.resolveContent(...args, (err, value) => { + if (err) { + return callback(err); + } + + let node = { + content: value + }; + if (args[0][args[1]] && typeof args[0][args[1]] === 'object' && !Buffer.isBuffer(args[0][args[1]])) { + Object.keys(args[0][args[1]]).forEach(key => { + if (!(key in node) && !['content', 'path', 'href', 'raw'].includes(key)) { + node[key] = args[0][args[1]][key]; + } + }); + } + + args[0][args[1]] = node; + resolveNext(); + }); + }; + + setImmediate(() => resolveNext()); + } + + normalize(callback) { + let envelope = this.data.envelope || this.message.getEnvelope(); + let 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 => { + 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') { + let 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 + if (this.data.list && typeof this.data.list === 'object') { + this._getListHeaders(this.data.list).forEach(listHeader => { + listHeader.value.forEach(value => { + this.message.addHeader(listHeader.key, value); + }); + }); + } + } + + _getListHeaders(listData) { + // make sure an url looks like + 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) { + if (key.toLowerCase().trim() === 'id') { + // List-ID: "comment" + let comment = value.comment || ''; + if (mimeFuncs.isPlainText(comment)) { + comment = '"' + comment + '"'; + } else { + comment = mimeFuncs.encodeWord(comment); + } + + return (value.comment ? comment + ' ' : '') + this._formatListUrl(value.url).replace(/^<[^:]+\/{,2}/, ''); + } + + // List-*: (comment) + let comment = value.comment || ''; + if (!mimeFuncs.isPlainText(comment)) { + comment = mimeFuncs.encodeWord(comment); + } + + return this._formatListUrl(value.url) + (value.comment ? ' (' + comment + ')' : ''); + } + + return ''; + }) + .filter(value => value) + .join(', ') + })) + })); + } + + _formatListUrl(url) { + url = url.replace(/[\s<]+|[\s>]+/g, ''); + if (/^(https?|mailto|ftp):/.test(url)) { + return '<' + url + '>'; + } + if (/^[^@]+@[^@]+$/.test(url)) { + return ''; + } + + return ''; + } +} + +module.exports = MailMessage; diff --git a/node_modules/nodemailer/lib/mime-funcs/index.js b/node_modules/nodemailer/lib/mime-funcs/index.js new file mode 100644 index 0000000..566bcaa --- /dev/null +++ b/node_modules/nodemailer/lib/mime-funcs/index.js @@ -0,0 +1,625 @@ +/* eslint no-control-regex:0 */ + +'use strict'; + +const base64 = require('../base64'); +const qp = require('../qp'); +const mimeTypes = require('./mime-types'); + +module.exports = { + /** + * Checks if a value is plaintext string (uses only printable 7bit chars) + * + * @param {String} value String to be tested + * @returns {Boolean} true if it is a plaintext string + */ + isPlainText(value, isParam) { + const re = isParam ? /[\x00-\x08\x0b\x0c\x0e-\x1f"\u0080-\uFFFF]/ : /[\x00-\x08\x0b\x0c\x0e-\x1f\u0080-\uFFFF]/; + if (typeof value !== 'string' || re.test(value)) { + return false; + } else { + return true; + } + }, + + /** + * 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 {Number} lineLength Max line length to check for + * @returns {Boolean} Returns true if there is at least one line longer than lineLength chars + */ + 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 {String|Buffer} data String to be encoded + * @param {String} mimeWordEncoding='Q' Encoding for the mime word, either Q or B + * @param {Number} [maxLength=0] If set, split mime words into several chunks if needed + * @return {String} Single or several mime words joined together + */ + encodeWord(data, mimeWordEncoding, maxLength) { + mimeWordEncoding = (mimeWordEncoding || 'Q').toString().toUpperCase().trim().charAt(0); + maxLength = maxLength || 0; + + let encodedStr; + let 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 => { + let ord = chr.charCodeAt(0).toString(16).toUpperCase(); + if (chr === ' ') { + return '_'; + } else { + return '=' + (ord.length === 1 ? '0' + ord : ord); + } + }); + } else if (mimeWordEncoding === 'B') { + encodedStr = typeof data === 'string' ? data : base64.encode(data); + 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 = this.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 + let parts = []; + let lpart = ''; + for (let i = 0, len = encodedStr.length; i < len; i++) { + let chr = encodedStr.charAt(i); + + if (/[\ud83c\ud83d\ud83e]/.test(chr) && i < len - 1) { + // composite emoji byte, so add the next byte as well + 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 {String} value String to be encoded + * @param {String} mimeWordEncoding='Q' Encoding for the mime word, either Q or B + * @param {Number} [maxLength=0] If set, split mime words into several chunks if needed + * @param {Boolean} [encodeAll=false] If true and the value needs encoding then encodes entire string, not just the smallest match + * @return {String} String with possible mime words + */ + encodeWords(value, mimeWordEncoding, maxLength, encodeAll) { + maxLength = maxLength || 0; + + let encodedValue; + + // find first word with a non-printable ascii or special symbol in it + let firstMatch = value.match(/(?:^|\s)([^\s]*["\u0080-\uFFFF])/); + if (!firstMatch) { + return value; + } + + if (encodeAll) { + // if it is requested to encode everything or the string contains something that resebles encoded word, then encode everything + + return this.encodeWord(value, mimeWordEncoding, maxLength); + } + + // find the last word with a non-printable ascii in it + let lastMatch = value.match(/(["\u0080-\uFFFF][^\s]*)[^"\u0080-\uFFFF]*$/); + if (!lastMatch) { + // should not happen + return value; + } + + let startIndex = + firstMatch.index + + ( + firstMatch[0].match(/[^\s]/) || { + index: 0 + } + ).index; + let endIndex = lastMatch.index + (lastMatch[1] || '').length; + + encodedValue = + (startIndex ? value.substr(0, startIndex) : '') + + this.encodeWord(value.substring(startIndex, endIndex), mimeWordEncoding || 'Q', maxLength) + + (endIndex < value.length ? value.substr(endIndex) : ''); + + return encodedValue; + }, + + /** + * 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 {Object} structured Parsed header value + * @return {String} joined header value + */ + buildHeaderValue(structured) { + let paramsArray = []; + + Object.keys(structured.params || {}).forEach(param => { + // filename might include unicode characters so it is a special case + // other values probably do not + let value = structured.params[param]; + if (!this.isPlainText(value, true) || value.length >= 75) { + this.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 (/[\s'"\\;:/=(),<>@[\]?]|^-/.test(value)) { + paramsArray.push(param + '=' + JSON.stringify(value)); + } else { + paramsArray.push(param + '=' + value); + } + }); + + return structured.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 {String|Buffer} data String to be encoded + * @param {Number} [maxLength=50] Max length for generated chunks + * @param {String} [fromCharset='UTF-8'] Source sharacter set + * @return {Array} A list of encoded keys and headers + */ + buildHeaderParam(key, data, maxLength) { + let list = []; + let encodedStr = typeof data === 'string' ? data : (data || '').toString(); + let encodedStrArr; + let chr, ord; + let line; + let startPos = 0; + let i, len; + + maxLength = maxLength || 50; + + // process ascii only text + if (this.isPlainText(data, true)) { + // 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 + encodedStrArr = []; + for (i = 0, len = encodedStr.length; i < len; i++) { + chr = encodedStr.charAt(i); + ord = chr.charCodeAt(0); + if (ord >= 0xd800 && ord <= 0xdbff && i < len - 1) { + 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 = this.safeEncodeURIComponent(chr); + } else { + // try to urlencode current char + chr = chr === ' ' ? chr : this.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 ((this.safeEncodeURIComponent(line) + chr).length >= maxLength) { + list.push({ + line, + encoded + }); + line = ''; + startPos = i - 1; + } 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] === ' ' ? ' ' : this.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 + })); + }, + + /** + * 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 {String} str Header value + * @return {Object} Header value as a parsed structure + */ + parseHeaderValue(str) { + let response = { + value: false, + params: {} + }; + let key = false; + let value = ''; + let type = 'value'; + let quote = false; + let escaped = false; + let chr; + + for (let i = 0, len = str.length; i < len; i++) { + chr = str.charAt(i); + if (type === 'key') { + if (chr === '=') { + key = value.trim().toLowerCase(); + type = 'value'; + value = ''; + continue; + } + value += chr; + } else { + if (escaped) { + value += chr; + } else if (chr === '\\') { + escaped = true; + continue; + } else if (quote && chr === quote) { + quote = false; + } else if (!quote && chr === '"') { + quote = chr; + } else if (!quote && chr === ';') { + if (key === false) { + response.value = value.trim(); + } else { + response.params[key] = value.trim(); + } + type = 'key'; + value = ''; + } else { + value += chr; + } + escaped = false; + } + } + + if (type === 'value') { + if (key === false) { + response.value = value.trim(); + } else { + response.params[key] = value.trim(); + } + } else if (value.trim()) { + response.params[value.trim().toLowerCase()] = ''; + } + + // handle parameter value continuations + // https://tools.ietf.org/html/rfc2231#section-3 + + // preprocess values + Object.keys(response.params).forEach(key => { + let actualKey, nr, match, value; + if ((match = key.match(/(\*(\d+)|\*(\d+)\*|\*)$/))) { + actualKey = key.substr(0, match.index); + nr = Number(match[2] || match[3]) || 0; + + if (!response.params[actualKey] || typeof response.params[actualKey] !== 'object') { + response.params[actualKey] = { + charset: false, + values: [] + }; + } + + value = response.params[key]; + + if (nr === 0 && match[0].substr(-1) === '*' && (match = value.match(/^([^']*)'[^']*'(.*)$/))) { + response.params[actualKey].charset = match[1] || 'iso-8859-1'; + value = match[2]; + } + + response.params[actualKey].values[nr] = value; + + // remove the old reference + delete response.params[key]; + } + }); + + // concatenate split rfc2231 strings and convert encoded strings to mime encoded words + Object.keys(response.params).forEach(key => { + let value; + if (response.params[key] && Array.isArray(response.params[key].values)) { + value = response.params[key].values.map(val => val || '').join(''); + + if (response.params[key].charset) { + // convert "%AB" to "=?charset?Q?=AB?=" + response.params[key] = + '=?' + + response.params[key].charset + + '?Q?' + + value + // fix invalidly encoded chars + .replace(/[=?_\s]/g, s => { + let c = s.charCodeAt(0).toString(16); + if (s === ' ') { + return '_'; + } else { + return '%' + (c.length < 2 ? '0' : '') + c; + } + }) + // change from urlencoding to percent encoding + .replace(/%/g, '=') + + '?='; + } else { + response.params[key] = value; + } + } + }); + + return response; + }, + + /** + * Returns file extension for a content type string. If no suitable extensions + * are found, 'bin' is used as the default extension + * + * @param {String} mimeType Content type to be checked for + * @return {String} File extension + */ + detectExtension: mimeType => 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 {String} extension Extension to be checked for + * @return {String} File extension + */ + detectMimeType: extension => mimeTypes.detectMimeType(extension), + + /** + * Folds long lines, useful for folding header lines (afterSpace=false) and + * flowed text (afterSpace=true) + * + * @param {String} str String to be folded + * @param {Number} [lineLength=76] Maximum length of a line + * @param {Boolean} afterSpace If true, leave a space in th end of a line + * @return {String} String with folded lines + */ + foldLines(str, lineLength, afterSpace) { + str = (str || '').toString(); + lineLength = lineLength || 76; + + let pos = 0, + len = str.length, + result = '', + 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 {String} str Mime encoded string to be split up + * @param {Number} maxlen Maximum length of characters for one part (minimum 12) + * @return {Array} Split string + */ + splitMimeEncodedString: (str, maxlen) => { + let curLine, + match, + chr, + done, + lines = []; + + // 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); + } + + done = false; + while (!done) { + 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) { + lines.push(curLine); + } + str = str.substr(curLine.length); + } + + return lines; + }, + + 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; + }, + + safeEncodeURIComponent(str) { + str = (str || '').toString(); + + try { + // might throw if we try to encode invalid sequences, eg. partial emoji + str = encodeURIComponent(str); + } catch (_E) { + // should never run + return str.replace(/[^\x00-\x1F *'()<>@,;:\\"[\]?=\u007F-\uFFFF]+/g, ''); + } + + // ensure chars that are not handled by encodeURICompent are converted as well + return str.replace(/[\x00-\x1F *'()<>@,;:\\"[\]?=\u007F-\uFFFF]/g, chr => this.encodeURICharComponent(chr)); + } +}; diff --git a/node_modules/nodemailer/lib/mime-funcs/mime-types.js b/node_modules/nodemailer/lib/mime-funcs/mime-types.js new file mode 100644 index 0000000..eed3cd6 --- /dev/null +++ b/node_modules/nodemailer/lib/mime-funcs/mime-types.js @@ -0,0 +1,2113 @@ +/* eslint quote-props: 0 */ + +'use strict'; + +const path = require('path'); + +const defaultMimeType = 'application/octet-stream'; +const defaultExtension = 'bin'; + +const mimeTypes = new Map([ + ['application/acad', 'dwg'], + ['application/applixware', 'aw'], + ['application/arj', 'arj'], + ['application/atom+xml', 'xml'], + ['application/atomcat+xml', 'atomcat'], + ['application/atomsvc+xml', 'atomsvc'], + ['application/base64', ['mm', 'mme']], + ['application/binhex', 'hqx'], + ['application/binhex4', 'hqx'], + ['application/book', ['book', 'boo']], + ['application/ccxml+xml,', 'ccxml'], + ['application/cdf', 'cdf'], + ['application/cdmi-capability', 'cdmia'], + ['application/cdmi-container', 'cdmic'], + ['application/cdmi-domain', 'cdmid'], + ['application/cdmi-object', 'cdmio'], + ['application/cdmi-queue', 'cdmiq'], + ['application/clariscad', 'ccad'], + ['application/commonground', 'dp'], + ['application/cu-seeme', 'cu'], + ['application/davmount+xml', 'davmount'], + ['application/drafting', 'drw'], + ['application/dsptype', 'tsp'], + ['application/dssc+der', 'dssc'], + ['application/dssc+xml', 'xdssc'], + ['application/dxf', 'dxf'], + ['application/ecmascript', ['js', 'es']], + ['application/emma+xml', 'emma'], + ['application/envoy', 'evy'], + ['application/epub+zip', 'epub'], + ['application/excel', ['xls', 'xl', 'xla', 'xlb', 'xlc', 'xld', 'xlk', 'xll', 'xlm', 'xlt', 'xlv', 'xlw']], + ['application/exi', 'exi'], + ['application/font-tdpfr', 'pfr'], + ['application/fractals', 'fif'], + ['application/freeloader', 'frl'], + ['application/futuresplash', 'spl'], + ['application/geo+json', 'geojson'], + ['application/gnutar', 'tgz'], + ['application/groupwise', 'vew'], + ['application/hlp', 'hlp'], + ['application/hta', 'hta'], + ['application/hyperstudio', 'stk'], + ['application/i-deas', 'unv'], + ['application/iges', ['iges', 'igs']], + ['application/inf', 'inf'], + ['application/internet-property-stream', 'acx'], + ['application/ipfix', 'ipfix'], + ['application/java', 'class'], + ['application/java-archive', 'jar'], + ['application/java-byte-code', 'class'], + ['application/java-serialized-object', 'ser'], + ['application/java-vm', 'class'], + ['application/javascript', 'js'], + ['application/json', 'json'], + ['application/lha', 'lha'], + ['application/lzx', 'lzx'], + ['application/mac-binary', 'bin'], + ['application/mac-binhex', 'hqx'], + ['application/mac-binhex40', 'hqx'], + ['application/mac-compactpro', 'cpt'], + ['application/macbinary', 'bin'], + ['application/mads+xml', 'mads'], + ['application/marc', 'mrc'], + ['application/marcxml+xml', 'mrcx'], + ['application/mathematica', 'ma'], + ['application/mathml+xml', 'mathml'], + ['application/mbedlet', 'mbd'], + ['application/mbox', 'mbox'], + ['application/mcad', 'mcd'], + ['application/mediaservercontrol+xml', 'mscml'], + ['application/metalink4+xml', 'meta4'], + ['application/mets+xml', 'mets'], + ['application/mime', 'aps'], + ['application/mods+xml', 'mods'], + ['application/mp21', 'm21'], + ['application/mp4', 'mp4'], + ['application/mspowerpoint', ['ppt', 'pot', 'pps', 'ppz']], + ['application/msword', ['doc', 'dot', 'w6w', 'wiz', 'word']], + ['application/mswrite', 'wri'], + ['application/mxf', 'mxf'], + ['application/netmc', 'mcp'], + ['application/octet-stream', ['*']], + ['application/oda', 'oda'], + ['application/oebps-package+xml', 'opf'], + ['application/ogg', 'ogx'], + ['application/olescript', 'axs'], + ['application/onenote', 'onetoc'], + ['application/patch-ops-error+xml', 'xer'], + ['application/pdf', 'pdf'], + ['application/pgp-encrypted', 'asc'], + ['application/pgp-signature', 'pgp'], + ['application/pics-rules', 'prf'], + ['application/pkcs-12', 'p12'], + ['application/pkcs-crl', 'crl'], + ['application/pkcs10', 'p10'], + ['application/pkcs7-mime', ['p7c', 'p7m']], + ['application/pkcs7-signature', 'p7s'], + ['application/pkcs8', 'p8'], + ['application/pkix-attr-cert', 'ac'], + ['application/pkix-cert', ['cer', 'crt']], + ['application/pkix-crl', 'crl'], + ['application/pkix-pkipath', 'pkipath'], + ['application/pkixcmp', 'pki'], + ['application/plain', 'text'], + ['application/pls+xml', 'pls'], + ['application/postscript', ['ps', 'ai', 'eps']], + ['application/powerpoint', 'ppt'], + ['application/pro_eng', ['part', 'prt']], + ['application/prs.cww', 'cww'], + ['application/pskc+xml', 'pskcxml'], + ['application/rdf+xml', 'rdf'], + ['application/reginfo+xml', 'rif'], + ['application/relax-ng-compact-syntax', 'rnc'], + ['application/resource-lists+xml', 'rl'], + ['application/resource-lists-diff+xml', 'rld'], + ['application/ringing-tones', 'rng'], + ['application/rls-services+xml', 'rs'], + ['application/rsd+xml', 'rsd'], + ['application/rss+xml', 'xml'], + ['application/rtf', ['rtf', 'rtx']], + ['application/sbml+xml', 'sbml'], + ['application/scvp-cv-request', 'scq'], + ['application/scvp-cv-response', 'scs'], + ['application/scvp-vp-request', 'spq'], + ['application/scvp-vp-response', 'spp'], + ['application/sdp', 'sdp'], + ['application/sea', 'sea'], + ['application/set', 'set'], + ['application/set-payment-initiation', 'setpay'], + ['application/set-registration-initiation', 'setreg'], + ['application/shf+xml', 'shf'], + ['application/sla', 'stl'], + ['application/smil', ['smi', 'smil']], + ['application/smil+xml', 'smi'], + ['application/solids', 'sol'], + ['application/sounder', 'sdr'], + ['application/sparql-query', 'rq'], + ['application/sparql-results+xml', 'srx'], + ['application/srgs', 'gram'], + ['application/srgs+xml', 'grxml'], + ['application/sru+xml', 'sru'], + ['application/ssml+xml', 'ssml'], + ['application/step', ['step', 'stp']], + ['application/streamingmedia', 'ssm'], + ['application/tei+xml', 'tei'], + ['application/thraud+xml', 'tfi'], + ['application/timestamped-data', 'tsd'], + ['application/toolbook', 'tbk'], + ['application/vda', 'vda'], + ['application/vnd.3gpp.pic-bw-large', 'plb'], + ['application/vnd.3gpp.pic-bw-small', 'psb'], + ['application/vnd.3gpp.pic-bw-var', 'pvb'], + ['application/vnd.3gpp2.tcap', 'tcap'], + ['application/vnd.3m.post-it-notes', 'pwn'], + ['application/vnd.accpac.simply.aso', 'aso'], + ['application/vnd.accpac.simply.imp', 'imp'], + ['application/vnd.acucobol', 'acu'], + ['application/vnd.acucorp', 'atc'], + ['application/vnd.adobe.air-application-installer-package+zip', 'air'], + ['application/vnd.adobe.fxp', 'fxp'], + ['application/vnd.adobe.xdp+xml', 'xdp'], + ['application/vnd.adobe.xfdf', 'xfdf'], + ['application/vnd.ahead.space', 'ahead'], + ['application/vnd.airzip.filesecure.azf', 'azf'], + ['application/vnd.airzip.filesecure.azs', 'azs'], + ['application/vnd.amazon.ebook', 'azw'], + ['application/vnd.americandynamics.acc', 'acc'], + ['application/vnd.amiga.ami', 'ami'], + ['application/vnd.android.package-archive', 'apk'], + ['application/vnd.anser-web-certificate-issue-initiation', 'cii'], + ['application/vnd.anser-web-funds-transfer-initiation', 'fti'], + ['application/vnd.antix.game-component', 'atx'], + ['application/vnd.apple.installer+xml', 'mpkg'], + ['application/vnd.apple.mpegurl', 'm3u8'], + ['application/vnd.aristanetworks.swi', 'swi'], + ['application/vnd.audiograph', 'aep'], + ['application/vnd.blueice.multipass', 'mpm'], + ['application/vnd.bmi', 'bmi'], + ['application/vnd.businessobjects', 'rep'], + ['application/vnd.chemdraw+xml', 'cdxml'], + ['application/vnd.chipnuts.karaoke-mmd', 'mmd'], + ['application/vnd.cinderella', 'cdy'], + ['application/vnd.claymore', 'cla'], + ['application/vnd.cloanto.rp9', 'rp9'], + ['application/vnd.clonk.c4group', 'c4g'], + ['application/vnd.cluetrust.cartomobile-config', 'c11amc'], + ['application/vnd.cluetrust.cartomobile-config-pkg', 'c11amz'], + ['application/vnd.commonspace', 'csp'], + ['application/vnd.contact.cmsg', 'cdbcmsg'], + ['application/vnd.cosmocaller', 'cmc'], + ['application/vnd.crick.clicker', 'clkx'], + ['application/vnd.crick.clicker.keyboard', 'clkk'], + ['application/vnd.crick.clicker.palette', 'clkp'], + ['application/vnd.crick.clicker.template', 'clkt'], + ['application/vnd.crick.clicker.wordbank', 'clkw'], + ['application/vnd.criticaltools.wbs+xml', 'wbs'], + ['application/vnd.ctc-posml', 'pml'], + ['application/vnd.cups-ppd', 'ppd'], + ['application/vnd.curl.car', 'car'], + ['application/vnd.curl.pcurl', 'pcurl'], + ['application/vnd.data-vision.rdz', 'rdz'], + ['application/vnd.denovo.fcselayout-link', 'fe_launch'], + ['application/vnd.dna', 'dna'], + ['application/vnd.dolby.mlp', 'mlp'], + ['application/vnd.dpgraph', 'dpg'], + ['application/vnd.dreamfactory', 'dfac'], + ['application/vnd.dvb.ait', 'ait'], + ['application/vnd.dvb.service', 'svc'], + ['application/vnd.dynageo', 'geo'], + ['application/vnd.ecowin.chart', 'mag'], + ['application/vnd.enliven', 'nml'], + ['application/vnd.epson.esf', 'esf'], + ['application/vnd.epson.msf', 'msf'], + ['application/vnd.epson.quickanime', 'qam'], + ['application/vnd.epson.salt', 'slt'], + ['application/vnd.epson.ssf', 'ssf'], + ['application/vnd.eszigno3+xml', 'es3'], + ['application/vnd.ezpix-album', 'ez2'], + ['application/vnd.ezpix-package', 'ez3'], + ['application/vnd.fdf', 'fdf'], + ['application/vnd.fdsn.seed', 'seed'], + ['application/vnd.flographit', 'gph'], + ['application/vnd.fluxtime.clip', 'ftc'], + ['application/vnd.framemaker', 'fm'], + ['application/vnd.frogans.fnc', 'fnc'], + ['application/vnd.frogans.ltf', 'ltf'], + ['application/vnd.fsc.weblaunch', 'fsc'], + ['application/vnd.fujitsu.oasys', 'oas'], + ['application/vnd.fujitsu.oasys2', 'oa2'], + ['application/vnd.fujitsu.oasys3', 'oa3'], + ['application/vnd.fujitsu.oasysgp', 'fg5'], + ['application/vnd.fujitsu.oasysprs', 'bh2'], + ['application/vnd.fujixerox.ddd', 'ddd'], + ['application/vnd.fujixerox.docuworks', 'xdw'], + ['application/vnd.fujixerox.docuworks.binder', 'xbd'], + ['application/vnd.fuzzysheet', 'fzs'], + ['application/vnd.genomatix.tuxedo', 'txd'], + ['application/vnd.geogebra.file', 'ggb'], + ['application/vnd.geogebra.tool', 'ggt'], + ['application/vnd.geometry-explorer', 'gex'], + ['application/vnd.geonext', 'gxt'], + ['application/vnd.geoplan', 'g2w'], + ['application/vnd.geospace', 'g3w'], + ['application/vnd.gmx', 'gmx'], + ['application/vnd.google-earth.kml+xml', 'kml'], + ['application/vnd.google-earth.kmz', 'kmz'], + ['application/vnd.grafeq', 'gqf'], + ['application/vnd.groove-account', 'gac'], + ['application/vnd.groove-help', 'ghf'], + ['application/vnd.groove-identity-message', 'gim'], + ['application/vnd.groove-injector', 'grv'], + ['application/vnd.groove-tool-message', 'gtm'], + ['application/vnd.groove-tool-template', 'tpl'], + ['application/vnd.groove-vcard', 'vcg'], + ['application/vnd.hal+xml', 'hal'], + ['application/vnd.handheld-entertainment+xml', 'zmm'], + ['application/vnd.hbci', 'hbci'], + ['application/vnd.hhe.lesson-player', 'les'], + ['application/vnd.hp-hpgl', ['hgl', 'hpg', 'hpgl']], + ['application/vnd.hp-hpid', 'hpid'], + ['application/vnd.hp-hps', 'hps'], + ['application/vnd.hp-jlyt', 'jlt'], + ['application/vnd.hp-pcl', 'pcl'], + ['application/vnd.hp-pclxl', 'pclxl'], + ['application/vnd.hydrostatix.sof-data', 'sfd-hdstx'], + ['application/vnd.hzn-3d-crossword', 'x3d'], + ['application/vnd.ibm.minipay', 'mpy'], + ['application/vnd.ibm.modcap', 'afp'], + ['application/vnd.ibm.rights-management', 'irm'], + ['application/vnd.ibm.secure-container', 'sc'], + ['application/vnd.iccprofile', 'icc'], + ['application/vnd.igloader', 'igl'], + ['application/vnd.immervision-ivp', 'ivp'], + ['application/vnd.immervision-ivu', 'ivu'], + ['application/vnd.insors.igm', 'igm'], + ['application/vnd.intercon.formnet', 'xpw'], + ['application/vnd.intergeo', 'i2g'], + ['application/vnd.intu.qbo', 'qbo'], + ['application/vnd.intu.qfx', 'qfx'], + ['application/vnd.ipunplugged.rcprofile', 'rcprofile'], + ['application/vnd.irepository.package+xml', 'irp'], + ['application/vnd.is-xpr', 'xpr'], + ['application/vnd.isac.fcs', 'fcs'], + ['application/vnd.jam', 'jam'], + ['application/vnd.jcp.javame.midlet-rms', 'rms'], + ['application/vnd.jisp', 'jisp'], + ['application/vnd.joost.joda-archive', 'joda'], + ['application/vnd.kahootz', 'ktz'], + ['application/vnd.kde.karbon', 'karbon'], + ['application/vnd.kde.kchart', 'chrt'], + ['application/vnd.kde.kformula', 'kfo'], + ['application/vnd.kde.kivio', 'flw'], + ['application/vnd.kde.kontour', 'kon'], + ['application/vnd.kde.kpresenter', 'kpr'], + ['application/vnd.kde.kspread', 'ksp'], + ['application/vnd.kde.kword', 'kwd'], + ['application/vnd.kenameaapp', 'htke'], + ['application/vnd.kidspiration', 'kia'], + ['application/vnd.kinar', 'kne'], + ['application/vnd.koan', 'skp'], + ['application/vnd.kodak-descriptor', 'sse'], + ['application/vnd.las.las+xml', 'lasxml'], + ['application/vnd.llamagraphics.life-balance.desktop', 'lbd'], + ['application/vnd.llamagraphics.life-balance.exchange+xml', 'lbe'], + ['application/vnd.lotus-1-2-3', '123'], + ['application/vnd.lotus-approach', 'apr'], + ['application/vnd.lotus-freelance', 'pre'], + ['application/vnd.lotus-notes', 'nsf'], + ['application/vnd.lotus-organizer', 'org'], + ['application/vnd.lotus-screencam', 'scm'], + ['application/vnd.lotus-wordpro', 'lwp'], + ['application/vnd.macports.portpkg', 'portpkg'], + ['application/vnd.mcd', 'mcd'], + ['application/vnd.medcalcdata', 'mc1'], + ['application/vnd.mediastation.cdkey', 'cdkey'], + ['application/vnd.mfer', 'mwf'], + ['application/vnd.mfmp', 'mfm'], + ['application/vnd.micrografx.flo', 'flo'], + ['application/vnd.micrografx.igx', 'igx'], + ['application/vnd.mif', 'mif'], + ['application/vnd.mobius.daf', 'daf'], + ['application/vnd.mobius.dis', 'dis'], + ['application/vnd.mobius.mbk', 'mbk'], + ['application/vnd.mobius.mqy', 'mqy'], + ['application/vnd.mobius.msl', 'msl'], + ['application/vnd.mobius.plc', 'plc'], + ['application/vnd.mobius.txf', 'txf'], + ['application/vnd.mophun.application', 'mpn'], + ['application/vnd.mophun.certificate', 'mpc'], + ['application/vnd.mozilla.xul+xml', 'xul'], + ['application/vnd.ms-artgalry', 'cil'], + ['application/vnd.ms-cab-compressed', 'cab'], + ['application/vnd.ms-excel', ['xls', 'xla', 'xlc', 'xlm', 'xlt', 'xlw', 'xlb', 'xll']], + ['application/vnd.ms-excel.addin.macroenabled.12', 'xlam'], + ['application/vnd.ms-excel.sheet.binary.macroenabled.12', 'xlsb'], + ['application/vnd.ms-excel.sheet.macroenabled.12', 'xlsm'], + ['application/vnd.ms-excel.template.macroenabled.12', 'xltm'], + ['application/vnd.ms-fontobject', 'eot'], + ['application/vnd.ms-htmlhelp', 'chm'], + ['application/vnd.ms-ims', 'ims'], + ['application/vnd.ms-lrm', 'lrm'], + ['application/vnd.ms-officetheme', 'thmx'], + ['application/vnd.ms-outlook', 'msg'], + ['application/vnd.ms-pki.certstore', 'sst'], + ['application/vnd.ms-pki.pko', 'pko'], + ['application/vnd.ms-pki.seccat', 'cat'], + ['application/vnd.ms-pki.stl', 'stl'], + ['application/vnd.ms-pkicertstore', 'sst'], + ['application/vnd.ms-pkiseccat', 'cat'], + ['application/vnd.ms-pkistl', 'stl'], + ['application/vnd.ms-powerpoint', ['ppt', 'pot', 'pps', 'ppa', 'pwz']], + ['application/vnd.ms-powerpoint.addin.macroenabled.12', 'ppam'], + ['application/vnd.ms-powerpoint.presentation.macroenabled.12', 'pptm'], + ['application/vnd.ms-powerpoint.slide.macroenabled.12', 'sldm'], + ['application/vnd.ms-powerpoint.slideshow.macroenabled.12', 'ppsm'], + ['application/vnd.ms-powerpoint.template.macroenabled.12', 'potm'], + ['application/vnd.ms-project', 'mpp'], + ['application/vnd.ms-word.document.macroenabled.12', 'docm'], + ['application/vnd.ms-word.template.macroenabled.12', 'dotm'], + ['application/vnd.ms-works', ['wks', 'wcm', 'wdb', 'wps']], + ['application/vnd.ms-wpl', 'wpl'], + ['application/vnd.ms-xpsdocument', 'xps'], + ['application/vnd.mseq', 'mseq'], + ['application/vnd.musician', 'mus'], + ['application/vnd.muvee.style', 'msty'], + ['application/vnd.neurolanguage.nlu', 'nlu'], + ['application/vnd.noblenet-directory', 'nnd'], + ['application/vnd.noblenet-sealer', 'nns'], + ['application/vnd.noblenet-web', 'nnw'], + ['application/vnd.nokia.configuration-message', 'ncm'], + ['application/vnd.nokia.n-gage.data', 'ngdat'], + ['application/vnd.nokia.n-gage.symbian.install', 'n-gage'], + ['application/vnd.nokia.radio-preset', 'rpst'], + ['application/vnd.nokia.radio-presets', 'rpss'], + ['application/vnd.nokia.ringing-tone', 'rng'], + ['application/vnd.novadigm.edm', 'edm'], + ['application/vnd.novadigm.edx', 'edx'], + ['application/vnd.novadigm.ext', 'ext'], + ['application/vnd.oasis.opendocument.chart', 'odc'], + ['application/vnd.oasis.opendocument.chart-template', 'otc'], + ['application/vnd.oasis.opendocument.database', 'odb'], + ['application/vnd.oasis.opendocument.formula', 'odf'], + ['application/vnd.oasis.opendocument.formula-template', 'odft'], + ['application/vnd.oasis.opendocument.graphics', 'odg'], + ['application/vnd.oasis.opendocument.graphics-template', 'otg'], + ['application/vnd.oasis.opendocument.image', 'odi'], + ['application/vnd.oasis.opendocument.image-template', 'oti'], + ['application/vnd.oasis.opendocument.presentation', 'odp'], + ['application/vnd.oasis.opendocument.presentation-template', 'otp'], + ['application/vnd.oasis.opendocument.spreadsheet', 'ods'], + ['application/vnd.oasis.opendocument.spreadsheet-template', 'ots'], + ['application/vnd.oasis.opendocument.text', 'odt'], + ['application/vnd.oasis.opendocument.text-master', 'odm'], + ['application/vnd.oasis.opendocument.text-template', 'ott'], + ['application/vnd.oasis.opendocument.text-web', 'oth'], + ['application/vnd.olpc-sugar', 'xo'], + ['application/vnd.oma.dd2+xml', 'dd2'], + ['application/vnd.openofficeorg.extension', 'oxt'], + ['application/vnd.openxmlformats-officedocument.presentationml.presentation', 'pptx'], + ['application/vnd.openxmlformats-officedocument.presentationml.slide', 'sldx'], + ['application/vnd.openxmlformats-officedocument.presentationml.slideshow', 'ppsx'], + ['application/vnd.openxmlformats-officedocument.presentationml.template', 'potx'], + ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'xlsx'], + ['application/vnd.openxmlformats-officedocument.spreadsheetml.template', 'xltx'], + ['application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'docx'], + ['application/vnd.openxmlformats-officedocument.wordprocessingml.template', 'dotx'], + ['application/vnd.osgeo.mapguide.package', 'mgp'], + ['application/vnd.osgi.dp', 'dp'], + ['application/vnd.palm', 'pdb'], + ['application/vnd.pawaafile', 'paw'], + ['application/vnd.pg.format', 'str'], + ['application/vnd.pg.osasli', 'ei6'], + ['application/vnd.picsel', 'efif'], + ['application/vnd.pmi.widget', 'wg'], + ['application/vnd.pocketlearn', 'plf'], + ['application/vnd.powerbuilder6', 'pbd'], + ['application/vnd.previewsystems.box', 'box'], + ['application/vnd.proteus.magazine', 'mgz'], + ['application/vnd.publishare-delta-tree', 'qps'], + ['application/vnd.pvi.ptid1', 'ptid'], + ['application/vnd.quark.quarkxpress', 'qxd'], + ['application/vnd.realvnc.bed', 'bed'], + ['application/vnd.recordare.musicxml', 'mxl'], + ['application/vnd.recordare.musicxml+xml', 'musicxml'], + ['application/vnd.rig.cryptonote', 'cryptonote'], + ['application/vnd.rim.cod', 'cod'], + ['application/vnd.rn-realmedia', 'rm'], + ['application/vnd.rn-realplayer', 'rnx'], + ['application/vnd.route66.link66+xml', 'link66'], + ['application/vnd.sailingtracker.track', 'st'], + ['application/vnd.seemail', 'see'], + ['application/vnd.sema', 'sema'], + ['application/vnd.semd', 'semd'], + ['application/vnd.semf', 'semf'], + ['application/vnd.shana.informed.formdata', 'ifm'], + ['application/vnd.shana.informed.formtemplate', 'itp'], + ['application/vnd.shana.informed.interchange', 'iif'], + ['application/vnd.shana.informed.package', 'ipk'], + ['application/vnd.simtech-mindmapper', 'twd'], + ['application/vnd.smaf', 'mmf'], + ['application/vnd.smart.teacher', 'teacher'], + ['application/vnd.solent.sdkm+xml', 'sdkm'], + ['application/vnd.spotfire.dxp', 'dxp'], + ['application/vnd.spotfire.sfs', 'sfs'], + ['application/vnd.stardivision.calc', 'sdc'], + ['application/vnd.stardivision.draw', 'sda'], + ['application/vnd.stardivision.impress', 'sdd'], + ['application/vnd.stardivision.math', 'smf'], + ['application/vnd.stardivision.writer', 'sdw'], + ['application/vnd.stardivision.writer-global', 'sgl'], + ['application/vnd.stepmania.stepchart', 'sm'], + ['application/vnd.sun.xml.calc', 'sxc'], + ['application/vnd.sun.xml.calc.template', 'stc'], + ['application/vnd.sun.xml.draw', 'sxd'], + ['application/vnd.sun.xml.draw.template', 'std'], + ['application/vnd.sun.xml.impress', 'sxi'], + ['application/vnd.sun.xml.impress.template', 'sti'], + ['application/vnd.sun.xml.math', 'sxm'], + ['application/vnd.sun.xml.writer', 'sxw'], + ['application/vnd.sun.xml.writer.global', 'sxg'], + ['application/vnd.sun.xml.writer.template', 'stw'], + ['application/vnd.sus-calendar', 'sus'], + ['application/vnd.svd', 'svd'], + ['application/vnd.symbian.install', 'sis'], + ['application/vnd.syncml+xml', 'xsm'], + ['application/vnd.syncml.dm+wbxml', 'bdm'], + ['application/vnd.syncml.dm+xml', 'xdm'], + ['application/vnd.tao.intent-module-archive', 'tao'], + ['application/vnd.tmobile-livetv', 'tmo'], + ['application/vnd.trid.tpt', 'tpt'], + ['application/vnd.triscape.mxs', 'mxs'], + ['application/vnd.trueapp', 'tra'], + ['application/vnd.ufdl', 'ufd'], + ['application/vnd.uiq.theme', 'utz'], + ['application/vnd.umajin', 'umj'], + ['application/vnd.unity', 'unityweb'], + ['application/vnd.uoml+xml', 'uoml'], + ['application/vnd.vcx', 'vcx'], + ['application/vnd.visio', 'vsd'], + ['application/vnd.visionary', 'vis'], + ['application/vnd.vsf', 'vsf'], + ['application/vnd.wap.wbxml', 'wbxml'], + ['application/vnd.wap.wmlc', 'wmlc'], + ['application/vnd.wap.wmlscriptc', 'wmlsc'], + ['application/vnd.webturbo', 'wtb'], + ['application/vnd.wolfram.player', 'nbp'], + ['application/vnd.wordperfect', 'wpd'], + ['application/vnd.wqd', 'wqd'], + ['application/vnd.wt.stf', 'stf'], + ['application/vnd.xara', ['web', 'xar']], + ['application/vnd.xfdl', 'xfdl'], + ['application/vnd.yamaha.hv-dic', 'hvd'], + ['application/vnd.yamaha.hv-script', 'hvs'], + ['application/vnd.yamaha.hv-voice', 'hvp'], + ['application/vnd.yamaha.openscoreformat', 'osf'], + ['application/vnd.yamaha.openscoreformat.osfpvg+xml', 'osfpvg'], + ['application/vnd.yamaha.smaf-audio', 'saf'], + ['application/vnd.yamaha.smaf-phrase', 'spf'], + ['application/vnd.yellowriver-custom-menu', 'cmp'], + ['application/vnd.zul', 'zir'], + ['application/vnd.zzazz.deck+xml', 'zaz'], + ['application/vocaltec-media-desc', 'vmd'], + ['application/vocaltec-media-file', 'vmf'], + ['application/voicexml+xml', 'vxml'], + ['application/widget', 'wgt'], + ['application/winhlp', 'hlp'], + ['application/wordperfect', ['wp', 'wp5', 'wp6', 'wpd']], + ['application/wordperfect6.0', ['w60', 'wp5']], + ['application/wordperfect6.1', 'w61'], + ['application/wsdl+xml', 'wsdl'], + ['application/wspolicy+xml', 'wspolicy'], + ['application/x-123', 'wk1'], + ['application/x-7z-compressed', '7z'], + ['application/x-abiword', 'abw'], + ['application/x-ace-compressed', 'ace'], + ['application/x-aim', 'aim'], + ['application/x-authorware-bin', 'aab'], + ['application/x-authorware-map', 'aam'], + ['application/x-authorware-seg', 'aas'], + ['application/x-bcpio', 'bcpio'], + ['application/x-binary', 'bin'], + ['application/x-binhex40', 'hqx'], + ['application/x-bittorrent', 'torrent'], + ['application/x-bsh', ['bsh', 'sh', 'shar']], + ['application/x-bytecode.elisp', 'elc'], + ['application/x-bytecode.python', 'pyc'], + ['application/x-bzip', 'bz'], + ['application/x-bzip2', ['boz', 'bz2']], + ['application/x-cdf', 'cdf'], + ['application/x-cdlink', 'vcd'], + ['application/x-chat', ['cha', 'chat']], + ['application/x-chess-pgn', 'pgn'], + ['application/x-cmu-raster', 'ras'], + ['application/x-cocoa', 'cco'], + ['application/x-compactpro', 'cpt'], + ['application/x-compress', 'z'], + ['application/x-compressed', ['tgz', 'gz', 'z', 'zip']], + ['application/x-conference', 'nsc'], + ['application/x-cpio', 'cpio'], + ['application/x-cpt', 'cpt'], + ['application/x-csh', 'csh'], + ['application/x-debian-package', 'deb'], + ['application/x-deepv', 'deepv'], + ['application/x-director', ['dir', 'dcr', 'dxr']], + ['application/x-doom', 'wad'], + ['application/x-dtbncx+xml', 'ncx'], + ['application/x-dtbook+xml', 'dtb'], + ['application/x-dtbresource+xml', 'res'], + ['application/x-dvi', 'dvi'], + ['application/x-elc', 'elc'], + ['application/x-envoy', ['env', 'evy']], + ['application/x-esrehber', 'es'], + ['application/x-excel', ['xls', 'xla', 'xlb', 'xlc', 'xld', 'xlk', 'xll', 'xlm', 'xlt', 'xlv', 'xlw']], + ['application/x-font-bdf', 'bdf'], + ['application/x-font-ghostscript', 'gsf'], + ['application/x-font-linux-psf', 'psf'], + ['application/x-font-otf', 'otf'], + ['application/x-font-pcf', 'pcf'], + ['application/x-font-snf', 'snf'], + ['application/x-font-ttf', 'ttf'], + ['application/x-font-type1', 'pfa'], + ['application/x-font-woff', 'woff'], + ['application/x-frame', 'mif'], + ['application/x-freelance', 'pre'], + ['application/x-futuresplash', 'spl'], + ['application/x-gnumeric', 'gnumeric'], + ['application/x-gsp', 'gsp'], + ['application/x-gss', 'gss'], + ['application/x-gtar', 'gtar'], + ['application/x-gzip', ['gz', 'gzip']], + ['application/x-hdf', 'hdf'], + ['application/x-helpfile', ['help', 'hlp']], + ['application/x-httpd-imap', 'imap'], + ['application/x-ima', 'ima'], + ['application/x-internet-signup', ['ins', 'isp']], + ['application/x-internett-signup', 'ins'], + ['application/x-inventor', 'iv'], + ['application/x-ip2', 'ip'], + ['application/x-iphone', 'iii'], + ['application/x-java-class', 'class'], + ['application/x-java-commerce', 'jcm'], + ['application/x-java-jnlp-file', 'jnlp'], + ['application/x-javascript', 'js'], + ['application/x-koan', ['skd', 'skm', 'skp', 'skt']], + ['application/x-ksh', 'ksh'], + ['application/x-latex', ['latex', 'ltx']], + ['application/x-lha', 'lha'], + ['application/x-lisp', 'lsp'], + ['application/x-livescreen', 'ivy'], + ['application/x-lotus', 'wq1'], + ['application/x-lotusscreencam', 'scm'], + ['application/x-lzh', 'lzh'], + ['application/x-lzx', 'lzx'], + ['application/x-mac-binhex40', 'hqx'], + ['application/x-macbinary', 'bin'], + ['application/x-magic-cap-package-1.0', 'mc$'], + ['application/x-mathcad', 'mcd'], + ['application/x-meme', 'mm'], + ['application/x-midi', ['mid', 'midi']], + ['application/x-mif', 'mif'], + ['application/x-mix-transfer', 'nix'], + ['application/x-mobipocket-ebook', 'prc'], + ['application/x-mplayer2', 'asx'], + ['application/x-ms-application', 'application'], + ['application/x-ms-wmd', 'wmd'], + ['application/x-ms-wmz', 'wmz'], + ['application/x-ms-xbap', 'xbap'], + ['application/x-msaccess', 'mdb'], + ['application/x-msbinder', 'obd'], + ['application/x-mscardfile', 'crd'], + ['application/x-msclip', 'clp'], + ['application/x-msdownload', ['exe', 'dll']], + ['application/x-msexcel', ['xls', 'xla', 'xlw']], + ['application/x-msmediaview', ['mvb', 'm13', 'm14']], + ['application/x-msmetafile', 'wmf'], + ['application/x-msmoney', 'mny'], + ['application/x-mspowerpoint', 'ppt'], + ['application/x-mspublisher', 'pub'], + ['application/x-msschedule', 'scd'], + ['application/x-msterminal', 'trm'], + ['application/x-mswrite', 'wri'], + ['application/x-navi-animation', 'ani'], + ['application/x-navidoc', 'nvd'], + ['application/x-navimap', 'map'], + ['application/x-navistyle', 'stl'], + ['application/x-netcdf', ['cdf', 'nc']], + ['application/x-newton-compatible-pkg', 'pkg'], + ['application/x-nokia-9000-communicator-add-on-software', 'aos'], + ['application/x-omc', 'omc'], + ['application/x-omcdatamaker', 'omcd'], + ['application/x-omcregerator', 'omcr'], + ['application/x-pagemaker', ['pm4', 'pm5']], + ['application/x-pcl', 'pcl'], + ['application/x-perfmon', ['pma', 'pmc', 'pml', 'pmr', 'pmw']], + ['application/x-pixclscript', 'plx'], + ['application/x-pkcs10', 'p10'], + ['application/x-pkcs12', ['p12', 'pfx']], + ['application/x-pkcs7-certificates', ['p7b', 'spc']], + ['application/x-pkcs7-certreqresp', 'p7r'], + ['application/x-pkcs7-mime', ['p7m', 'p7c']], + ['application/x-pkcs7-signature', ['p7s', 'p7a']], + ['application/x-pointplus', 'css'], + ['application/x-portable-anymap', 'pnm'], + ['application/x-project', ['mpc', 'mpt', 'mpv', 'mpx']], + ['application/x-qpro', 'wb1'], + ['application/x-rar-compressed', 'rar'], + ['application/x-rtf', 'rtf'], + ['application/x-sdp', 'sdp'], + ['application/x-sea', 'sea'], + ['application/x-seelogo', 'sl'], + ['application/x-sh', 'sh'], + ['application/x-shar', ['shar', 'sh']], + ['application/x-shockwave-flash', 'swf'], + ['application/x-silverlight-app', 'xap'], + ['application/x-sit', 'sit'], + ['application/x-sprite', ['spr', 'sprite']], + ['application/x-stuffit', 'sit'], + ['application/x-stuffitx', 'sitx'], + ['application/x-sv4cpio', 'sv4cpio'], + ['application/x-sv4crc', 'sv4crc'], + ['application/x-tar', 'tar'], + ['application/x-tbook', ['sbk', 'tbk']], + ['application/x-tcl', 'tcl'], + ['application/x-tex', 'tex'], + ['application/x-tex-tfm', 'tfm'], + ['application/x-texinfo', ['texi', 'texinfo']], + ['application/x-troff', ['roff', 't', 'tr']], + ['application/x-troff-man', 'man'], + ['application/x-troff-me', 'me'], + ['application/x-troff-ms', 'ms'], + ['application/x-troff-msvideo', 'avi'], + ['application/x-ustar', 'ustar'], + ['application/x-visio', ['vsd', 'vst', 'vsw']], + ['application/x-vnd.audioexplosion.mzz', 'mzz'], + ['application/x-vnd.ls-xpix', 'xpix'], + ['application/x-vrml', 'vrml'], + ['application/x-wais-source', ['src', 'wsrc']], + ['application/x-winhelp', 'hlp'], + ['application/x-wintalk', 'wtk'], + ['application/x-world', ['wrl', 'svr']], + ['application/x-wpwin', 'wpd'], + ['application/x-wri', 'wri'], + ['application/x-x509-ca-cert', ['cer', 'crt', 'der']], + ['application/x-x509-user-cert', 'crt'], + ['application/x-xfig', 'fig'], + ['application/x-xpinstall', 'xpi'], + ['application/x-zip-compressed', 'zip'], + ['application/xcap-diff+xml', 'xdf'], + ['application/xenc+xml', 'xenc'], + ['application/xhtml+xml', 'xhtml'], + ['application/xml', 'xml'], + ['application/xml-dtd', 'dtd'], + ['application/xop+xml', 'xop'], + ['application/xslt+xml', 'xslt'], + ['application/xspf+xml', 'xspf'], + ['application/xv+xml', 'mxml'], + ['application/yang', 'yang'], + ['application/yin+xml', 'yin'], + ['application/ynd.ms-pkipko', 'pko'], + ['application/zip', 'zip'], + ['audio/adpcm', 'adp'], + ['audio/aiff', ['aiff', 'aif', 'aifc']], + ['audio/basic', ['snd', 'au']], + ['audio/it', 'it'], + ['audio/make', ['funk', 'my', 'pfunk']], + ['audio/make.my.funk', 'pfunk'], + ['audio/mid', ['mid', 'rmi']], + ['audio/midi', ['midi', 'kar', 'mid']], + ['audio/mod', 'mod'], + ['audio/mp4', 'mp4a'], + ['audio/mpeg', ['mpga', 'mp3', 'm2a', 'mp2', 'mpa', 'mpg']], + ['audio/mpeg3', 'mp3'], + ['audio/nspaudio', ['la', 'lma']], + ['audio/ogg', 'oga'], + ['audio/s3m', 's3m'], + ['audio/tsp-audio', 'tsi'], + ['audio/tsplayer', 'tsp'], + ['audio/vnd.dece.audio', 'uva'], + ['audio/vnd.digital-winds', 'eol'], + ['audio/vnd.dra', 'dra'], + ['audio/vnd.dts', 'dts'], + ['audio/vnd.dts.hd', 'dtshd'], + ['audio/vnd.lucent.voice', 'lvp'], + ['audio/vnd.ms-playready.media.pya', 'pya'], + ['audio/vnd.nuera.ecelp4800', 'ecelp4800'], + ['audio/vnd.nuera.ecelp7470', 'ecelp7470'], + ['audio/vnd.nuera.ecelp9600', 'ecelp9600'], + ['audio/vnd.qcelp', 'qcp'], + ['audio/vnd.rip', 'rip'], + ['audio/voc', 'voc'], + ['audio/voxware', 'vox'], + ['audio/wav', 'wav'], + ['audio/webm', 'weba'], + ['audio/x-aac', 'aac'], + ['audio/x-adpcm', 'snd'], + ['audio/x-aiff', ['aiff', 'aif', 'aifc']], + ['audio/x-au', 'au'], + ['audio/x-gsm', ['gsd', 'gsm']], + ['audio/x-jam', 'jam'], + ['audio/x-liveaudio', 'lam'], + ['audio/x-mid', ['mid', 'midi']], + ['audio/x-midi', ['midi', 'mid']], + ['audio/x-mod', 'mod'], + ['audio/x-mpeg', 'mp2'], + ['audio/x-mpeg-3', 'mp3'], + ['audio/x-mpegurl', 'm3u'], + ['audio/x-mpequrl', 'm3u'], + ['audio/x-ms-wax', 'wax'], + ['audio/x-ms-wma', 'wma'], + ['audio/x-nspaudio', ['la', 'lma']], + ['audio/x-pn-realaudio', ['ra', 'ram', 'rm', 'rmm', 'rmp']], + ['audio/x-pn-realaudio-plugin', ['ra', 'rmp', 'rpm']], + ['audio/x-psid', 'sid'], + ['audio/x-realaudio', 'ra'], + ['audio/x-twinvq', 'vqf'], + ['audio/x-twinvq-plugin', ['vqe', 'vql']], + ['audio/x-vnd.audioexplosion.mjuicemediafile', 'mjf'], + ['audio/x-voc', 'voc'], + ['audio/x-wav', 'wav'], + ['audio/xm', 'xm'], + ['chemical/x-cdx', 'cdx'], + ['chemical/x-cif', 'cif'], + ['chemical/x-cmdf', 'cmdf'], + ['chemical/x-cml', 'cml'], + ['chemical/x-csml', 'csml'], + ['chemical/x-pdb', ['pdb', 'xyz']], + ['chemical/x-xyz', 'xyz'], + ['drawing/x-dwf', 'dwf'], + ['i-world/i-vrml', 'ivr'], + ['image/bmp', ['bmp', 'bm']], + ['image/cgm', 'cgm'], + ['image/cis-cod', 'cod'], + ['image/cmu-raster', ['ras', 'rast']], + ['image/fif', 'fif'], + ['image/florian', ['flo', 'turbot']], + ['image/g3fax', 'g3'], + ['image/gif', 'gif'], + ['image/ief', ['ief', 'iefs']], + ['image/jpeg', ['jpeg', 'jpe', 'jpg', 'jfif', 'jfif-tbnl']], + ['image/jutvision', 'jut'], + ['image/ktx', 'ktx'], + ['image/naplps', ['nap', 'naplps']], + ['image/pict', ['pic', 'pict']], + ['image/pipeg', 'jfif'], + ['image/pjpeg', ['jfif', 'jpe', 'jpeg', 'jpg']], + ['image/png', ['png', 'x-png']], + ['image/prs.btif', 'btif'], + ['image/svg+xml', 'svg'], + ['image/tiff', ['tif', 'tiff']], + ['image/vasa', 'mcf'], + ['image/vnd.adobe.photoshop', 'psd'], + ['image/vnd.dece.graphic', 'uvi'], + ['image/vnd.djvu', 'djvu'], + ['image/vnd.dvb.subtitle', 'sub'], + ['image/vnd.dwg', ['dwg', 'dxf', 'svf']], + ['image/vnd.dxf', 'dxf'], + ['image/vnd.fastbidsheet', 'fbs'], + ['image/vnd.fpx', 'fpx'], + ['image/vnd.fst', 'fst'], + ['image/vnd.fujixerox.edmics-mmr', 'mmr'], + ['image/vnd.fujixerox.edmics-rlc', 'rlc'], + ['image/vnd.ms-modi', 'mdi'], + ['image/vnd.net-fpx', ['fpx', 'npx']], + ['image/vnd.rn-realflash', 'rf'], + ['image/vnd.rn-realpix', 'rp'], + ['image/vnd.wap.wbmp', 'wbmp'], + ['image/vnd.xiff', 'xif'], + ['image/webp', 'webp'], + ['image/x-cmu-raster', 'ras'], + ['image/x-cmx', 'cmx'], + ['image/x-dwg', ['dwg', 'dxf', 'svf']], + ['image/x-freehand', 'fh'], + ['image/x-icon', 'ico'], + ['image/x-jg', 'art'], + ['image/x-jps', 'jps'], + ['image/x-niff', ['niff', 'nif']], + ['image/x-pcx', 'pcx'], + ['image/x-pict', ['pct', 'pic']], + ['image/x-portable-anymap', 'pnm'], + ['image/x-portable-bitmap', 'pbm'], + ['image/x-portable-graymap', 'pgm'], + ['image/x-portable-greymap', 'pgm'], + ['image/x-portable-pixmap', 'ppm'], + ['image/x-quicktime', ['qif', 'qti', 'qtif']], + ['image/x-rgb', 'rgb'], + ['image/x-tiff', ['tif', 'tiff']], + ['image/x-windows-bmp', 'bmp'], + ['image/x-xbitmap', 'xbm'], + ['image/x-xbm', 'xbm'], + ['image/x-xpixmap', ['xpm', 'pm']], + ['image/x-xwd', 'xwd'], + ['image/x-xwindowdump', 'xwd'], + ['image/xbm', 'xbm'], + ['image/xpm', 'xpm'], + ['message/rfc822', ['eml', 'mht', 'mhtml', 'nws', 'mime']], + ['model/iges', ['iges', 'igs']], + ['model/mesh', 'msh'], + ['model/vnd.collada+xml', 'dae'], + ['model/vnd.dwf', 'dwf'], + ['model/vnd.gdl', 'gdl'], + ['model/vnd.gtw', 'gtw'], + ['model/vnd.mts', 'mts'], + ['model/vnd.vtu', 'vtu'], + ['model/vrml', ['vrml', 'wrl', 'wrz']], + ['model/x-pov', 'pov'], + ['multipart/x-gzip', 'gzip'], + ['multipart/x-ustar', 'ustar'], + ['multipart/x-zip', 'zip'], + ['music/crescendo', ['mid', 'midi']], + ['music/x-karaoke', 'kar'], + ['paleovu/x-pv', 'pvu'], + ['text/asp', 'asp'], + ['text/calendar', 'ics'], + ['text/css', 'css'], + ['text/csv', 'csv'], + ['text/ecmascript', 'js'], + ['text/h323', '323'], + ['text/html', ['html', 'htm', 'stm', 'acgi', 'htmls', 'htx', 'shtml']], + ['text/iuls', 'uls'], + ['text/javascript', 'js'], + ['text/mcf', 'mcf'], + ['text/n3', 'n3'], + ['text/pascal', 'pas'], + [ + 'text/plain', + [ + 'txt', + 'bas', + 'c', + 'h', + 'c++', + 'cc', + 'com', + 'conf', + 'cxx', + 'def', + 'f', + 'f90', + 'for', + 'g', + 'hh', + 'idc', + 'jav', + 'java', + 'list', + 'log', + 'lst', + 'm', + 'mar', + 'pl', + 'sdml', + 'text' + ] + ], + ['text/plain-bas', 'par'], + ['text/prs.lines.tag', 'dsc'], + ['text/richtext', ['rtx', 'rt', 'rtf']], + ['text/scriplet', 'wsc'], + ['text/scriptlet', 'sct'], + ['text/sgml', ['sgm', 'sgml']], + ['text/tab-separated-values', 'tsv'], + ['text/troff', 't'], + ['text/turtle', 'ttl'], + ['text/uri-list', ['uni', 'unis', 'uri', 'uris']], + ['text/vnd.abc', 'abc'], + ['text/vnd.curl', 'curl'], + ['text/vnd.curl.dcurl', 'dcurl'], + ['text/vnd.curl.mcurl', 'mcurl'], + ['text/vnd.curl.scurl', 'scurl'], + ['text/vnd.fly', 'fly'], + ['text/vnd.fmi.flexstor', 'flx'], + ['text/vnd.graphviz', 'gv'], + ['text/vnd.in3d.3dml', '3dml'], + ['text/vnd.in3d.spot', 'spot'], + ['text/vnd.rn-realtext', 'rt'], + ['text/vnd.sun.j2me.app-descriptor', 'jad'], + ['text/vnd.wap.wml', 'wml'], + ['text/vnd.wap.wmlscript', 'wmls'], + ['text/webviewhtml', 'htt'], + ['text/x-asm', ['asm', 's']], + ['text/x-audiosoft-intra', 'aip'], + ['text/x-c', ['c', 'cc', 'cpp']], + ['text/x-component', 'htc'], + ['text/x-fortran', ['for', 'f', 'f77', 'f90']], + ['text/x-h', ['h', 'hh']], + ['text/x-java-source', ['java', 'jav']], + ['text/x-java-source,java', 'java'], + ['text/x-la-asf', 'lsx'], + ['text/x-m', 'm'], + ['text/x-pascal', 'p'], + ['text/x-script', 'hlb'], + ['text/x-script.csh', 'csh'], + ['text/x-script.elisp', 'el'], + ['text/x-script.guile', 'scm'], + ['text/x-script.ksh', 'ksh'], + ['text/x-script.lisp', 'lsp'], + ['text/x-script.perl', 'pl'], + ['text/x-script.perl-module', 'pm'], + ['text/x-script.phyton', 'py'], + ['text/x-script.rexx', 'rexx'], + ['text/x-script.scheme', 'scm'], + ['text/x-script.sh', 'sh'], + ['text/x-script.tcl', 'tcl'], + ['text/x-script.tcsh', 'tcsh'], + ['text/x-script.zsh', 'zsh'], + ['text/x-server-parsed-html', ['shtml', 'ssi']], + ['text/x-setext', 'etx'], + ['text/x-sgml', ['sgm', 'sgml']], + ['text/x-speech', ['spc', 'talk']], + ['text/x-uil', 'uil'], + ['text/x-uuencode', ['uu', 'uue']], + ['text/x-vcalendar', 'vcs'], + ['text/x-vcard', 'vcf'], + ['text/xml', 'xml'], + ['video/3gpp', '3gp'], + ['video/3gpp2', '3g2'], + ['video/animaflex', 'afl'], + ['video/avi', 'avi'], + ['video/avs-video', 'avs'], + ['video/dl', 'dl'], + ['video/fli', 'fli'], + ['video/gl', 'gl'], + ['video/h261', 'h261'], + ['video/h263', 'h263'], + ['video/h264', 'h264'], + ['video/jpeg', 'jpgv'], + ['video/jpm', 'jpm'], + ['video/mj2', 'mj2'], + ['video/mp4', 'mp4'], + ['video/mpeg', ['mpeg', 'mp2', 'mpa', 'mpe', 'mpg', 'mpv2', 'm1v', 'm2v', 'mp3']], + ['video/msvideo', 'avi'], + ['video/ogg', 'ogv'], + ['video/quicktime', ['mov', 'qt', 'moov']], + ['video/vdo', 'vdo'], + ['video/vivo', ['viv', 'vivo']], + ['video/vnd.dece.hd', 'uvh'], + ['video/vnd.dece.mobile', 'uvm'], + ['video/vnd.dece.pd', 'uvp'], + ['video/vnd.dece.sd', 'uvs'], + ['video/vnd.dece.video', 'uvv'], + ['video/vnd.fvt', 'fvt'], + ['video/vnd.mpegurl', 'mxu'], + ['video/vnd.ms-playready.media.pyv', 'pyv'], + ['video/vnd.rn-realvideo', 'rv'], + ['video/vnd.uvvu.mp4', 'uvu'], + ['video/vnd.vivo', ['viv', 'vivo']], + ['video/vosaic', 'vos'], + ['video/webm', 'webm'], + ['video/x-amt-demorun', 'xdr'], + ['video/x-amt-showrun', 'xsr'], + ['video/x-atomic3d-feature', 'fmf'], + ['video/x-dl', 'dl'], + ['video/x-dv', ['dif', 'dv']], + ['video/x-f4v', 'f4v'], + ['video/x-fli', 'fli'], + ['video/x-flv', 'flv'], + ['video/x-gl', 'gl'], + ['video/x-isvideo', 'isu'], + ['video/x-la-asf', ['lsf', 'lsx']], + ['video/x-m4v', 'm4v'], + ['video/x-motion-jpeg', 'mjpg'], + ['video/x-mpeg', ['mp3', 'mp2']], + ['video/x-mpeq2a', 'mp2'], + ['video/x-ms-asf', ['asf', 'asr', 'asx']], + ['video/x-ms-asf-plugin', 'asx'], + ['video/x-ms-wm', 'wm'], + ['video/x-ms-wmv', 'wmv'], + ['video/x-ms-wmx', 'wmx'], + ['video/x-ms-wvx', 'wvx'], + ['video/x-msvideo', 'avi'], + ['video/x-qtc', 'qtc'], + ['video/x-scm', 'scm'], + ['video/x-sgi-movie', ['movie', 'mv']], + ['windows/metafile', 'wmf'], + ['www/mime', 'mime'], + ['x-conference/x-cooltalk', 'ice'], + ['x-music/x-midi', ['mid', 'midi']], + ['x-world/x-3dmf', ['3dm', '3dmf', 'qd3', 'qd3d']], + ['x-world/x-svr', 'svr'], + ['x-world/x-vrml', ['flr', 'vrml', 'wrl', 'wrz', 'xaf', 'xof']], + ['x-world/x-vrt', 'vrt'], + ['xgl/drawing', 'xgz'], + ['xgl/movie', 'xmz'] +]); +const extensions = new Map([ + ['123', 'application/vnd.lotus-1-2-3'], + ['323', 'text/h323'], + ['*', 'application/octet-stream'], + ['3dm', 'x-world/x-3dmf'], + ['3dmf', 'x-world/x-3dmf'], + ['3dml', 'text/vnd.in3d.3dml'], + ['3g2', 'video/3gpp2'], + ['3gp', 'video/3gpp'], + ['7z', 'application/x-7z-compressed'], + ['a', 'application/octet-stream'], + ['aab', 'application/x-authorware-bin'], + ['aac', 'audio/x-aac'], + ['aam', 'application/x-authorware-map'], + ['aas', 'application/x-authorware-seg'], + ['abc', 'text/vnd.abc'], + ['abw', 'application/x-abiword'], + ['ac', 'application/pkix-attr-cert'], + ['acc', 'application/vnd.americandynamics.acc'], + ['ace', 'application/x-ace-compressed'], + ['acgi', 'text/html'], + ['acu', 'application/vnd.acucobol'], + ['acx', 'application/internet-property-stream'], + ['adp', 'audio/adpcm'], + ['aep', 'application/vnd.audiograph'], + ['afl', 'video/animaflex'], + ['afp', 'application/vnd.ibm.modcap'], + ['ahead', 'application/vnd.ahead.space'], + ['ai', 'application/postscript'], + ['aif', ['audio/aiff', 'audio/x-aiff']], + ['aifc', ['audio/aiff', 'audio/x-aiff']], + ['aiff', ['audio/aiff', 'audio/x-aiff']], + ['aim', 'application/x-aim'], + ['aip', 'text/x-audiosoft-intra'], + ['air', 'application/vnd.adobe.air-application-installer-package+zip'], + ['ait', 'application/vnd.dvb.ait'], + ['ami', 'application/vnd.amiga.ami'], + ['ani', 'application/x-navi-animation'], + ['aos', 'application/x-nokia-9000-communicator-add-on-software'], + ['apk', 'application/vnd.android.package-archive'], + ['application', 'application/x-ms-application'], + ['apr', 'application/vnd.lotus-approach'], + ['aps', 'application/mime'], + ['arc', 'application/octet-stream'], + ['arj', ['application/arj', 'application/octet-stream']], + ['art', 'image/x-jg'], + ['asf', 'video/x-ms-asf'], + ['asm', 'text/x-asm'], + ['aso', 'application/vnd.accpac.simply.aso'], + ['asp', 'text/asp'], + ['asr', 'video/x-ms-asf'], + ['asx', ['video/x-ms-asf', 'application/x-mplayer2', 'video/x-ms-asf-plugin']], + ['atc', 'application/vnd.acucorp'], + ['atomcat', 'application/atomcat+xml'], + ['atomsvc', 'application/atomsvc+xml'], + ['atx', 'application/vnd.antix.game-component'], + ['au', ['audio/basic', 'audio/x-au']], + ['avi', ['video/avi', 'video/msvideo', 'application/x-troff-msvideo', 'video/x-msvideo']], + ['avs', 'video/avs-video'], + ['aw', 'application/applixware'], + ['axs', 'application/olescript'], + ['azf', 'application/vnd.airzip.filesecure.azf'], + ['azs', 'application/vnd.airzip.filesecure.azs'], + ['azw', 'application/vnd.amazon.ebook'], + ['bas', 'text/plain'], + ['bcpio', 'application/x-bcpio'], + ['bdf', 'application/x-font-bdf'], + ['bdm', 'application/vnd.syncml.dm+wbxml'], + ['bed', 'application/vnd.realvnc.bed'], + ['bh2', 'application/vnd.fujitsu.oasysprs'], + [ + 'bin', + ['application/octet-stream', 'application/mac-binary', 'application/macbinary', 'application/x-macbinary', 'application/x-binary'] + ], + ['bm', 'image/bmp'], + ['bmi', 'application/vnd.bmi'], + ['bmp', ['image/bmp', 'image/x-windows-bmp']], + ['boo', 'application/book'], + ['book', 'application/book'], + ['box', 'application/vnd.previewsystems.box'], + ['boz', 'application/x-bzip2'], + ['bsh', 'application/x-bsh'], + ['btif', 'image/prs.btif'], + ['bz', 'application/x-bzip'], + ['bz2', 'application/x-bzip2'], + ['c', ['text/plain', 'text/x-c']], + ['c++', 'text/plain'], + ['c11amc', 'application/vnd.cluetrust.cartomobile-config'], + ['c11amz', 'application/vnd.cluetrust.cartomobile-config-pkg'], + ['c4g', 'application/vnd.clonk.c4group'], + ['cab', 'application/vnd.ms-cab-compressed'], + ['car', 'application/vnd.curl.car'], + ['cat', ['application/vnd.ms-pkiseccat', 'application/vnd.ms-pki.seccat']], + ['cc', ['text/plain', 'text/x-c']], + ['ccad', 'application/clariscad'], + ['cco', 'application/x-cocoa'], + ['ccxml', 'application/ccxml+xml,'], + ['cdbcmsg', 'application/vnd.contact.cmsg'], + ['cdf', ['application/cdf', 'application/x-cdf', 'application/x-netcdf']], + ['cdkey', 'application/vnd.mediastation.cdkey'], + ['cdmia', 'application/cdmi-capability'], + ['cdmic', 'application/cdmi-container'], + ['cdmid', 'application/cdmi-domain'], + ['cdmio', 'application/cdmi-object'], + ['cdmiq', 'application/cdmi-queue'], + ['cdx', 'chemical/x-cdx'], + ['cdxml', 'application/vnd.chemdraw+xml'], + ['cdy', 'application/vnd.cinderella'], + ['cer', ['application/pkix-cert', 'application/x-x509-ca-cert']], + ['cgm', 'image/cgm'], + ['cha', 'application/x-chat'], + ['chat', 'application/x-chat'], + ['chm', 'application/vnd.ms-htmlhelp'], + ['chrt', 'application/vnd.kde.kchart'], + ['cif', 'chemical/x-cif'], + ['cii', 'application/vnd.anser-web-certificate-issue-initiation'], + ['cil', 'application/vnd.ms-artgalry'], + ['cla', 'application/vnd.claymore'], + [ + 'class', + ['application/octet-stream', 'application/java', 'application/java-byte-code', 'application/java-vm', 'application/x-java-class'] + ], + ['clkk', 'application/vnd.crick.clicker.keyboard'], + ['clkp', 'application/vnd.crick.clicker.palette'], + ['clkt', 'application/vnd.crick.clicker.template'], + ['clkw', 'application/vnd.crick.clicker.wordbank'], + ['clkx', 'application/vnd.crick.clicker'], + ['clp', 'application/x-msclip'], + ['cmc', 'application/vnd.cosmocaller'], + ['cmdf', 'chemical/x-cmdf'], + ['cml', 'chemical/x-cml'], + ['cmp', 'application/vnd.yellowriver-custom-menu'], + ['cmx', 'image/x-cmx'], + ['cod', ['image/cis-cod', 'application/vnd.rim.cod']], + ['com', ['application/octet-stream', 'text/plain']], + ['conf', 'text/plain'], + ['cpio', 'application/x-cpio'], + ['cpp', 'text/x-c'], + ['cpt', ['application/mac-compactpro', 'application/x-compactpro', 'application/x-cpt']], + ['crd', 'application/x-mscardfile'], + ['crl', ['application/pkix-crl', 'application/pkcs-crl']], + ['crt', ['application/pkix-cert', 'application/x-x509-user-cert', 'application/x-x509-ca-cert']], + ['cryptonote', 'application/vnd.rig.cryptonote'], + ['csh', ['text/x-script.csh', 'application/x-csh']], + ['csml', 'chemical/x-csml'], + ['csp', 'application/vnd.commonspace'], + ['css', ['text/css', 'application/x-pointplus']], + ['csv', 'text/csv'], + ['cu', 'application/cu-seeme'], + ['curl', 'text/vnd.curl'], + ['cww', 'application/prs.cww'], + ['cxx', 'text/plain'], + ['dae', 'model/vnd.collada+xml'], + ['daf', 'application/vnd.mobius.daf'], + ['davmount', 'application/davmount+xml'], + ['dcr', 'application/x-director'], + ['dcurl', 'text/vnd.curl.dcurl'], + ['dd2', 'application/vnd.oma.dd2+xml'], + ['ddd', 'application/vnd.fujixerox.ddd'], + ['deb', 'application/x-debian-package'], + ['deepv', 'application/x-deepv'], + ['def', 'text/plain'], + ['der', 'application/x-x509-ca-cert'], + ['dfac', 'application/vnd.dreamfactory'], + ['dif', 'video/x-dv'], + ['dir', 'application/x-director'], + ['dis', 'application/vnd.mobius.dis'], + ['djvu', 'image/vnd.djvu'], + ['dl', ['video/dl', 'video/x-dl']], + ['dll', 'application/x-msdownload'], + ['dms', 'application/octet-stream'], + ['dna', 'application/vnd.dna'], + ['doc', 'application/msword'], + ['docm', 'application/vnd.ms-word.document.macroenabled.12'], + ['docx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'], + ['dot', 'application/msword'], + ['dotm', 'application/vnd.ms-word.template.macroenabled.12'], + ['dotx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.template'], + ['dp', ['application/commonground', 'application/vnd.osgi.dp']], + ['dpg', 'application/vnd.dpgraph'], + ['dra', 'audio/vnd.dra'], + ['drw', 'application/drafting'], + ['dsc', 'text/prs.lines.tag'], + ['dssc', 'application/dssc+der'], + ['dtb', 'application/x-dtbook+xml'], + ['dtd', 'application/xml-dtd'], + ['dts', 'audio/vnd.dts'], + ['dtshd', 'audio/vnd.dts.hd'], + ['dump', 'application/octet-stream'], + ['dv', 'video/x-dv'], + ['dvi', 'application/x-dvi'], + ['dwf', ['model/vnd.dwf', 'drawing/x-dwf']], + ['dwg', ['application/acad', 'image/vnd.dwg', 'image/x-dwg']], + ['dxf', ['application/dxf', 'image/vnd.dwg', 'image/vnd.dxf', 'image/x-dwg']], + ['dxp', 'application/vnd.spotfire.dxp'], + ['dxr', 'application/x-director'], + ['ecelp4800', 'audio/vnd.nuera.ecelp4800'], + ['ecelp7470', 'audio/vnd.nuera.ecelp7470'], + ['ecelp9600', 'audio/vnd.nuera.ecelp9600'], + ['edm', 'application/vnd.novadigm.edm'], + ['edx', 'application/vnd.novadigm.edx'], + ['efif', 'application/vnd.picsel'], + ['ei6', 'application/vnd.pg.osasli'], + ['el', 'text/x-script.elisp'], + ['elc', ['application/x-elc', 'application/x-bytecode.elisp']], + ['eml', 'message/rfc822'], + ['emma', 'application/emma+xml'], + ['env', 'application/x-envoy'], + ['eol', 'audio/vnd.digital-winds'], + ['eot', 'application/vnd.ms-fontobject'], + ['eps', 'application/postscript'], + ['epub', 'application/epub+zip'], + ['es', ['application/ecmascript', 'application/x-esrehber']], + ['es3', 'application/vnd.eszigno3+xml'], + ['esf', 'application/vnd.epson.esf'], + ['etx', 'text/x-setext'], + ['evy', ['application/envoy', 'application/x-envoy']], + ['exe', ['application/octet-stream', 'application/x-msdownload']], + ['exi', 'application/exi'], + ['ext', 'application/vnd.novadigm.ext'], + ['ez2', 'application/vnd.ezpix-album'], + ['ez3', 'application/vnd.ezpix-package'], + ['f', ['text/plain', 'text/x-fortran']], + ['f4v', 'video/x-f4v'], + ['f77', 'text/x-fortran'], + ['f90', ['text/plain', 'text/x-fortran']], + ['fbs', 'image/vnd.fastbidsheet'], + ['fcs', 'application/vnd.isac.fcs'], + ['fdf', 'application/vnd.fdf'], + ['fe_launch', 'application/vnd.denovo.fcselayout-link'], + ['fg5', 'application/vnd.fujitsu.oasysgp'], + ['fh', 'image/x-freehand'], + ['fif', ['application/fractals', 'image/fif']], + ['fig', 'application/x-xfig'], + ['fli', ['video/fli', 'video/x-fli']], + ['flo', ['image/florian', 'application/vnd.micrografx.flo']], + ['flr', 'x-world/x-vrml'], + ['flv', 'video/x-flv'], + ['flw', 'application/vnd.kde.kivio'], + ['flx', 'text/vnd.fmi.flexstor'], + ['fly', 'text/vnd.fly'], + ['fm', 'application/vnd.framemaker'], + ['fmf', 'video/x-atomic3d-feature'], + ['fnc', 'application/vnd.frogans.fnc'], + ['for', ['text/plain', 'text/x-fortran']], + ['fpx', ['image/vnd.fpx', 'image/vnd.net-fpx']], + ['frl', 'application/freeloader'], + ['fsc', 'application/vnd.fsc.weblaunch'], + ['fst', 'image/vnd.fst'], + ['ftc', 'application/vnd.fluxtime.clip'], + ['fti', 'application/vnd.anser-web-funds-transfer-initiation'], + ['funk', 'audio/make'], + ['fvt', 'video/vnd.fvt'], + ['fxp', 'application/vnd.adobe.fxp'], + ['fzs', 'application/vnd.fuzzysheet'], + ['g', 'text/plain'], + ['g2w', 'application/vnd.geoplan'], + ['g3', 'image/g3fax'], + ['g3w', 'application/vnd.geospace'], + ['gac', 'application/vnd.groove-account'], + ['gdl', 'model/vnd.gdl'], + ['geo', 'application/vnd.dynageo'], + ['geojson', 'application/geo+json'], + ['gex', 'application/vnd.geometry-explorer'], + ['ggb', 'application/vnd.geogebra.file'], + ['ggt', 'application/vnd.geogebra.tool'], + ['ghf', 'application/vnd.groove-help'], + ['gif', 'image/gif'], + ['gim', 'application/vnd.groove-identity-message'], + ['gl', ['video/gl', 'video/x-gl']], + ['gmx', 'application/vnd.gmx'], + ['gnumeric', 'application/x-gnumeric'], + ['gph', 'application/vnd.flographit'], + ['gqf', 'application/vnd.grafeq'], + ['gram', 'application/srgs'], + ['grv', 'application/vnd.groove-injector'], + ['grxml', 'application/srgs+xml'], + ['gsd', 'audio/x-gsm'], + ['gsf', 'application/x-font-ghostscript'], + ['gsm', 'audio/x-gsm'], + ['gsp', 'application/x-gsp'], + ['gss', 'application/x-gss'], + ['gtar', 'application/x-gtar'], + ['gtm', 'application/vnd.groove-tool-message'], + ['gtw', 'model/vnd.gtw'], + ['gv', 'text/vnd.graphviz'], + ['gxt', 'application/vnd.geonext'], + ['gz', ['application/x-gzip', 'application/x-compressed']], + ['gzip', ['multipart/x-gzip', 'application/x-gzip']], + ['h', ['text/plain', 'text/x-h']], + ['h261', 'video/h261'], + ['h263', 'video/h263'], + ['h264', 'video/h264'], + ['hal', 'application/vnd.hal+xml'], + ['hbci', 'application/vnd.hbci'], + ['hdf', 'application/x-hdf'], + ['help', 'application/x-helpfile'], + ['hgl', 'application/vnd.hp-hpgl'], + ['hh', ['text/plain', 'text/x-h']], + ['hlb', 'text/x-script'], + ['hlp', ['application/winhlp', 'application/hlp', 'application/x-helpfile', 'application/x-winhelp']], + ['hpg', 'application/vnd.hp-hpgl'], + ['hpgl', 'application/vnd.hp-hpgl'], + ['hpid', 'application/vnd.hp-hpid'], + ['hps', 'application/vnd.hp-hps'], + [ + 'hqx', + [ + 'application/mac-binhex40', + 'application/binhex', + 'application/binhex4', + 'application/mac-binhex', + 'application/x-binhex40', + 'application/x-mac-binhex40' + ] + ], + ['hta', 'application/hta'], + ['htc', 'text/x-component'], + ['htke', 'application/vnd.kenameaapp'], + ['htm', 'text/html'], + ['html', 'text/html'], + ['htmls', 'text/html'], + ['htt', 'text/webviewhtml'], + ['htx', 'text/html'], + ['hvd', 'application/vnd.yamaha.hv-dic'], + ['hvp', 'application/vnd.yamaha.hv-voice'], + ['hvs', 'application/vnd.yamaha.hv-script'], + ['i2g', 'application/vnd.intergeo'], + ['icc', 'application/vnd.iccprofile'], + ['ice', 'x-conference/x-cooltalk'], + ['ico', 'image/x-icon'], + ['ics', 'text/calendar'], + ['idc', 'text/plain'], + ['ief', 'image/ief'], + ['iefs', 'image/ief'], + ['ifm', 'application/vnd.shana.informed.formdata'], + ['iges', ['application/iges', 'model/iges']], + ['igl', 'application/vnd.igloader'], + ['igm', 'application/vnd.insors.igm'], + ['igs', ['application/iges', 'model/iges']], + ['igx', 'application/vnd.micrografx.igx'], + ['iif', 'application/vnd.shana.informed.interchange'], + ['iii', 'application/x-iphone'], + ['ima', 'application/x-ima'], + ['imap', 'application/x-httpd-imap'], + ['imp', 'application/vnd.accpac.simply.imp'], + ['ims', 'application/vnd.ms-ims'], + ['inf', 'application/inf'], + ['ins', ['application/x-internet-signup', 'application/x-internett-signup']], + ['ip', 'application/x-ip2'], + ['ipfix', 'application/ipfix'], + ['ipk', 'application/vnd.shana.informed.package'], + ['irm', 'application/vnd.ibm.rights-management'], + ['irp', 'application/vnd.irepository.package+xml'], + ['isp', 'application/x-internet-signup'], + ['isu', 'video/x-isvideo'], + ['it', 'audio/it'], + ['itp', 'application/vnd.shana.informed.formtemplate'], + ['iv', 'application/x-inventor'], + ['ivp', 'application/vnd.immervision-ivp'], + ['ivr', 'i-world/i-vrml'], + ['ivu', 'application/vnd.immervision-ivu'], + ['ivy', 'application/x-livescreen'], + ['jad', 'text/vnd.sun.j2me.app-descriptor'], + ['jam', ['application/vnd.jam', 'audio/x-jam']], + ['jar', 'application/java-archive'], + ['jav', ['text/plain', 'text/x-java-source']], + ['java', ['text/plain', 'text/x-java-source,java', 'text/x-java-source']], + ['jcm', 'application/x-java-commerce'], + ['jfif', ['image/pipeg', 'image/jpeg', 'image/pjpeg']], + ['jfif-tbnl', 'image/jpeg'], + ['jisp', 'application/vnd.jisp'], + ['jlt', 'application/vnd.hp-jlyt'], + ['jnlp', 'application/x-java-jnlp-file'], + ['joda', 'application/vnd.joost.joda-archive'], + ['jpe', ['image/jpeg', 'image/pjpeg']], + ['jpeg', ['image/jpeg', 'image/pjpeg']], + ['jpg', ['image/jpeg', 'image/pjpeg']], + ['jpgv', 'video/jpeg'], + ['jpm', 'video/jpm'], + ['jps', 'image/x-jps'], + ['js', ['application/javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'application/x-javascript']], + ['json', 'application/json'], + ['jut', 'image/jutvision'], + ['kar', ['audio/midi', 'music/x-karaoke']], + ['karbon', 'application/vnd.kde.karbon'], + ['kfo', 'application/vnd.kde.kformula'], + ['kia', 'application/vnd.kidspiration'], + ['kml', 'application/vnd.google-earth.kml+xml'], + ['kmz', 'application/vnd.google-earth.kmz'], + ['kne', 'application/vnd.kinar'], + ['kon', 'application/vnd.kde.kontour'], + ['kpr', 'application/vnd.kde.kpresenter'], + ['ksh', ['application/x-ksh', 'text/x-script.ksh']], + ['ksp', 'application/vnd.kde.kspread'], + ['ktx', 'image/ktx'], + ['ktz', 'application/vnd.kahootz'], + ['kwd', 'application/vnd.kde.kword'], + ['la', ['audio/nspaudio', 'audio/x-nspaudio']], + ['lam', 'audio/x-liveaudio'], + ['lasxml', 'application/vnd.las.las+xml'], + ['latex', 'application/x-latex'], + ['lbd', 'application/vnd.llamagraphics.life-balance.desktop'], + ['lbe', 'application/vnd.llamagraphics.life-balance.exchange+xml'], + ['les', 'application/vnd.hhe.lesson-player'], + ['lha', ['application/octet-stream', 'application/lha', 'application/x-lha']], + ['lhx', 'application/octet-stream'], + ['link66', 'application/vnd.route66.link66+xml'], + ['list', 'text/plain'], + ['lma', ['audio/nspaudio', 'audio/x-nspaudio']], + ['log', 'text/plain'], + ['lrm', 'application/vnd.ms-lrm'], + ['lsf', 'video/x-la-asf'], + ['lsp', ['application/x-lisp', 'text/x-script.lisp']], + ['lst', 'text/plain'], + ['lsx', ['video/x-la-asf', 'text/x-la-asf']], + ['ltf', 'application/vnd.frogans.ltf'], + ['ltx', 'application/x-latex'], + ['lvp', 'audio/vnd.lucent.voice'], + ['lwp', 'application/vnd.lotus-wordpro'], + ['lzh', ['application/octet-stream', 'application/x-lzh']], + ['lzx', ['application/lzx', 'application/octet-stream', 'application/x-lzx']], + ['m', ['text/plain', 'text/x-m']], + ['m13', 'application/x-msmediaview'], + ['m14', 'application/x-msmediaview'], + ['m1v', 'video/mpeg'], + ['m21', 'application/mp21'], + ['m2a', 'audio/mpeg'], + ['m2v', 'video/mpeg'], + ['m3u', ['audio/x-mpegurl', 'audio/x-mpequrl']], + ['m3u8', 'application/vnd.apple.mpegurl'], + ['m4v', 'video/x-m4v'], + ['ma', 'application/mathematica'], + ['mads', 'application/mads+xml'], + ['mag', 'application/vnd.ecowin.chart'], + ['man', 'application/x-troff-man'], + ['map', 'application/x-navimap'], + ['mar', 'text/plain'], + ['mathml', 'application/mathml+xml'], + ['mbd', 'application/mbedlet'], + ['mbk', 'application/vnd.mobius.mbk'], + ['mbox', 'application/mbox'], + ['mc$', 'application/x-magic-cap-package-1.0'], + ['mc1', 'application/vnd.medcalcdata'], + ['mcd', ['application/mcad', 'application/vnd.mcd', 'application/x-mathcad']], + ['mcf', ['image/vasa', 'text/mcf']], + ['mcp', 'application/netmc'], + ['mcurl', 'text/vnd.curl.mcurl'], + ['mdb', 'application/x-msaccess'], + ['mdi', 'image/vnd.ms-modi'], + ['me', 'application/x-troff-me'], + ['meta4', 'application/metalink4+xml'], + ['mets', 'application/mets+xml'], + ['mfm', 'application/vnd.mfmp'], + ['mgp', 'application/vnd.osgeo.mapguide.package'], + ['mgz', 'application/vnd.proteus.magazine'], + ['mht', 'message/rfc822'], + ['mhtml', 'message/rfc822'], + ['mid', ['audio/mid', 'audio/midi', 'music/crescendo', 'x-music/x-midi', 'audio/x-midi', 'application/x-midi', 'audio/x-mid']], + ['midi', ['audio/midi', 'music/crescendo', 'x-music/x-midi', 'audio/x-midi', 'application/x-midi', 'audio/x-mid']], + ['mif', ['application/vnd.mif', 'application/x-mif', 'application/x-frame']], + ['mime', ['message/rfc822', 'www/mime']], + ['mj2', 'video/mj2'], + ['mjf', 'audio/x-vnd.audioexplosion.mjuicemediafile'], + ['mjpg', 'video/x-motion-jpeg'], + ['mlp', 'application/vnd.dolby.mlp'], + ['mm', ['application/base64', 'application/x-meme']], + ['mmd', 'application/vnd.chipnuts.karaoke-mmd'], + ['mme', 'application/base64'], + ['mmf', 'application/vnd.smaf'], + ['mmr', 'image/vnd.fujixerox.edmics-mmr'], + ['mny', 'application/x-msmoney'], + ['mod', ['audio/mod', 'audio/x-mod']], + ['mods', 'application/mods+xml'], + ['moov', 'video/quicktime'], + ['mov', 'video/quicktime'], + ['movie', 'video/x-sgi-movie'], + ['mp2', ['video/mpeg', 'audio/mpeg', 'video/x-mpeg', 'audio/x-mpeg', 'video/x-mpeq2a']], + ['mp3', ['audio/mpeg', 'audio/mpeg3', 'video/mpeg', 'audio/x-mpeg-3', 'video/x-mpeg']], + ['mp4', ['video/mp4', 'application/mp4']], + ['mp4a', 'audio/mp4'], + ['mpa', ['video/mpeg', 'audio/mpeg']], + ['mpc', ['application/vnd.mophun.certificate', 'application/x-project']], + ['mpe', 'video/mpeg'], + ['mpeg', 'video/mpeg'], + ['mpg', ['video/mpeg', 'audio/mpeg']], + ['mpga', 'audio/mpeg'], + ['mpkg', 'application/vnd.apple.installer+xml'], + ['mpm', 'application/vnd.blueice.multipass'], + ['mpn', 'application/vnd.mophun.application'], + ['mpp', 'application/vnd.ms-project'], + ['mpt', 'application/x-project'], + ['mpv', 'application/x-project'], + ['mpv2', 'video/mpeg'], + ['mpx', 'application/x-project'], + ['mpy', 'application/vnd.ibm.minipay'], + ['mqy', 'application/vnd.mobius.mqy'], + ['mrc', 'application/marc'], + ['mrcx', 'application/marcxml+xml'], + ['ms', 'application/x-troff-ms'], + ['mscml', 'application/mediaservercontrol+xml'], + ['mseq', 'application/vnd.mseq'], + ['msf', 'application/vnd.epson.msf'], + ['msg', 'application/vnd.ms-outlook'], + ['msh', 'model/mesh'], + ['msl', 'application/vnd.mobius.msl'], + ['msty', 'application/vnd.muvee.style'], + ['mts', 'model/vnd.mts'], + ['mus', 'application/vnd.musician'], + ['musicxml', 'application/vnd.recordare.musicxml+xml'], + ['mv', 'video/x-sgi-movie'], + ['mvb', 'application/x-msmediaview'], + ['mwf', 'application/vnd.mfer'], + ['mxf', 'application/mxf'], + ['mxl', 'application/vnd.recordare.musicxml'], + ['mxml', 'application/xv+xml'], + ['mxs', 'application/vnd.triscape.mxs'], + ['mxu', 'video/vnd.mpegurl'], + ['my', 'audio/make'], + ['mzz', 'application/x-vnd.audioexplosion.mzz'], + ['n-gage', 'application/vnd.nokia.n-gage.symbian.install'], + ['n3', 'text/n3'], + ['nap', 'image/naplps'], + ['naplps', 'image/naplps'], + ['nbp', 'application/vnd.wolfram.player'], + ['nc', 'application/x-netcdf'], + ['ncm', 'application/vnd.nokia.configuration-message'], + ['ncx', 'application/x-dtbncx+xml'], + ['ngdat', 'application/vnd.nokia.n-gage.data'], + ['nif', 'image/x-niff'], + ['niff', 'image/x-niff'], + ['nix', 'application/x-mix-transfer'], + ['nlu', 'application/vnd.neurolanguage.nlu'], + ['nml', 'application/vnd.enliven'], + ['nnd', 'application/vnd.noblenet-directory'], + ['nns', 'application/vnd.noblenet-sealer'], + ['nnw', 'application/vnd.noblenet-web'], + ['npx', 'image/vnd.net-fpx'], + ['nsc', 'application/x-conference'], + ['nsf', 'application/vnd.lotus-notes'], + ['nvd', 'application/x-navidoc'], + ['nws', 'message/rfc822'], + ['o', 'application/octet-stream'], + ['oa2', 'application/vnd.fujitsu.oasys2'], + ['oa3', 'application/vnd.fujitsu.oasys3'], + ['oas', 'application/vnd.fujitsu.oasys'], + ['obd', 'application/x-msbinder'], + ['oda', 'application/oda'], + ['odb', 'application/vnd.oasis.opendocument.database'], + ['odc', 'application/vnd.oasis.opendocument.chart'], + ['odf', 'application/vnd.oasis.opendocument.formula'], + ['odft', 'application/vnd.oasis.opendocument.formula-template'], + ['odg', 'application/vnd.oasis.opendocument.graphics'], + ['odi', 'application/vnd.oasis.opendocument.image'], + ['odm', 'application/vnd.oasis.opendocument.text-master'], + ['odp', 'application/vnd.oasis.opendocument.presentation'], + ['ods', 'application/vnd.oasis.opendocument.spreadsheet'], + ['odt', 'application/vnd.oasis.opendocument.text'], + ['oga', 'audio/ogg'], + ['ogv', 'video/ogg'], + ['ogx', 'application/ogg'], + ['omc', 'application/x-omc'], + ['omcd', 'application/x-omcdatamaker'], + ['omcr', 'application/x-omcregerator'], + ['onetoc', 'application/onenote'], + ['opf', 'application/oebps-package+xml'], + ['org', 'application/vnd.lotus-organizer'], + ['osf', 'application/vnd.yamaha.openscoreformat'], + ['osfpvg', 'application/vnd.yamaha.openscoreformat.osfpvg+xml'], + ['otc', 'application/vnd.oasis.opendocument.chart-template'], + ['otf', 'application/x-font-otf'], + ['otg', 'application/vnd.oasis.opendocument.graphics-template'], + ['oth', 'application/vnd.oasis.opendocument.text-web'], + ['oti', 'application/vnd.oasis.opendocument.image-template'], + ['otp', 'application/vnd.oasis.opendocument.presentation-template'], + ['ots', 'application/vnd.oasis.opendocument.spreadsheet-template'], + ['ott', 'application/vnd.oasis.opendocument.text-template'], + ['oxt', 'application/vnd.openofficeorg.extension'], + ['p', 'text/x-pascal'], + ['p10', ['application/pkcs10', 'application/x-pkcs10']], + ['p12', ['application/pkcs-12', 'application/x-pkcs12']], + ['p7a', 'application/x-pkcs7-signature'], + ['p7b', 'application/x-pkcs7-certificates'], + ['p7c', ['application/pkcs7-mime', 'application/x-pkcs7-mime']], + ['p7m', ['application/pkcs7-mime', 'application/x-pkcs7-mime']], + ['p7r', 'application/x-pkcs7-certreqresp'], + ['p7s', ['application/pkcs7-signature', 'application/x-pkcs7-signature']], + ['p8', 'application/pkcs8'], + ['par', 'text/plain-bas'], + ['part', 'application/pro_eng'], + ['pas', 'text/pascal'], + ['paw', 'application/vnd.pawaafile'], + ['pbd', 'application/vnd.powerbuilder6'], + ['pbm', 'image/x-portable-bitmap'], + ['pcf', 'application/x-font-pcf'], + ['pcl', ['application/vnd.hp-pcl', 'application/x-pcl']], + ['pclxl', 'application/vnd.hp-pclxl'], + ['pct', 'image/x-pict'], + ['pcurl', 'application/vnd.curl.pcurl'], + ['pcx', 'image/x-pcx'], + ['pdb', ['application/vnd.palm', 'chemical/x-pdb']], + ['pdf', 'application/pdf'], + ['pfa', 'application/x-font-type1'], + ['pfr', 'application/font-tdpfr'], + ['pfunk', ['audio/make', 'audio/make.my.funk']], + ['pfx', 'application/x-pkcs12'], + ['pgm', ['image/x-portable-graymap', 'image/x-portable-greymap']], + ['pgn', 'application/x-chess-pgn'], + ['pgp', 'application/pgp-signature'], + ['pic', ['image/pict', 'image/x-pict']], + ['pict', 'image/pict'], + ['pkg', 'application/x-newton-compatible-pkg'], + ['pki', 'application/pkixcmp'], + ['pkipath', 'application/pkix-pkipath'], + ['pko', ['application/ynd.ms-pkipko', 'application/vnd.ms-pki.pko']], + ['pl', ['text/plain', 'text/x-script.perl']], + ['plb', 'application/vnd.3gpp.pic-bw-large'], + ['plc', 'application/vnd.mobius.plc'], + ['plf', 'application/vnd.pocketlearn'], + ['pls', 'application/pls+xml'], + ['plx', 'application/x-pixclscript'], + ['pm', ['text/x-script.perl-module', 'image/x-xpixmap']], + ['pm4', 'application/x-pagemaker'], + ['pm5', 'application/x-pagemaker'], + ['pma', 'application/x-perfmon'], + ['pmc', 'application/x-perfmon'], + ['pml', ['application/vnd.ctc-posml', 'application/x-perfmon']], + ['pmr', 'application/x-perfmon'], + ['pmw', 'application/x-perfmon'], + ['png', 'image/png'], + ['pnm', ['application/x-portable-anymap', 'image/x-portable-anymap']], + ['portpkg', 'application/vnd.macports.portpkg'], + ['pot', ['application/vnd.ms-powerpoint', 'application/mspowerpoint']], + ['potm', 'application/vnd.ms-powerpoint.template.macroenabled.12'], + ['potx', 'application/vnd.openxmlformats-officedocument.presentationml.template'], + ['pov', 'model/x-pov'], + ['ppa', 'application/vnd.ms-powerpoint'], + ['ppam', 'application/vnd.ms-powerpoint.addin.macroenabled.12'], + ['ppd', 'application/vnd.cups-ppd'], + ['ppm', 'image/x-portable-pixmap'], + ['pps', ['application/vnd.ms-powerpoint', 'application/mspowerpoint']], + ['ppsm', 'application/vnd.ms-powerpoint.slideshow.macroenabled.12'], + ['ppsx', 'application/vnd.openxmlformats-officedocument.presentationml.slideshow'], + ['ppt', ['application/vnd.ms-powerpoint', 'application/mspowerpoint', 'application/powerpoint', 'application/x-mspowerpoint']], + ['pptm', 'application/vnd.ms-powerpoint.presentation.macroenabled.12'], + ['pptx', 'application/vnd.openxmlformats-officedocument.presentationml.presentation'], + ['ppz', 'application/mspowerpoint'], + ['prc', 'application/x-mobipocket-ebook'], + ['pre', ['application/vnd.lotus-freelance', 'application/x-freelance']], + ['prf', 'application/pics-rules'], + ['prt', 'application/pro_eng'], + ['ps', 'application/postscript'], + ['psb', 'application/vnd.3gpp.pic-bw-small'], + ['psd', ['application/octet-stream', 'image/vnd.adobe.photoshop']], + ['psf', 'application/x-font-linux-psf'], + ['pskcxml', 'application/pskc+xml'], + ['ptid', 'application/vnd.pvi.ptid1'], + ['pub', 'application/x-mspublisher'], + ['pvb', 'application/vnd.3gpp.pic-bw-var'], + ['pvu', 'paleovu/x-pv'], + ['pwn', 'application/vnd.3m.post-it-notes'], + ['pwz', 'application/vnd.ms-powerpoint'], + ['py', 'text/x-script.phyton'], + ['pya', 'audio/vnd.ms-playready.media.pya'], + ['pyc', 'application/x-bytecode.python'], + ['pyv', 'video/vnd.ms-playready.media.pyv'], + ['qam', 'application/vnd.epson.quickanime'], + ['qbo', 'application/vnd.intu.qbo'], + ['qcp', 'audio/vnd.qcelp'], + ['qd3', 'x-world/x-3dmf'], + ['qd3d', 'x-world/x-3dmf'], + ['qfx', 'application/vnd.intu.qfx'], + ['qif', 'image/x-quicktime'], + ['qps', 'application/vnd.publishare-delta-tree'], + ['qt', 'video/quicktime'], + ['qtc', 'video/x-qtc'], + ['qti', 'image/x-quicktime'], + ['qtif', 'image/x-quicktime'], + ['qxd', 'application/vnd.quark.quarkxpress'], + ['ra', ['audio/x-realaudio', 'audio/x-pn-realaudio', 'audio/x-pn-realaudio-plugin']], + ['ram', 'audio/x-pn-realaudio'], + ['rar', 'application/x-rar-compressed'], + ['ras', ['image/cmu-raster', 'application/x-cmu-raster', 'image/x-cmu-raster']], + ['rast', 'image/cmu-raster'], + ['rcprofile', 'application/vnd.ipunplugged.rcprofile'], + ['rdf', 'application/rdf+xml'], + ['rdz', 'application/vnd.data-vision.rdz'], + ['rep', 'application/vnd.businessobjects'], + ['res', 'application/x-dtbresource+xml'], + ['rexx', 'text/x-script.rexx'], + ['rf', 'image/vnd.rn-realflash'], + ['rgb', 'image/x-rgb'], + ['rif', 'application/reginfo+xml'], + ['rip', 'audio/vnd.rip'], + ['rl', 'application/resource-lists+xml'], + ['rlc', 'image/vnd.fujixerox.edmics-rlc'], + ['rld', 'application/resource-lists-diff+xml'], + ['rm', ['application/vnd.rn-realmedia', 'audio/x-pn-realaudio']], + ['rmi', 'audio/mid'], + ['rmm', 'audio/x-pn-realaudio'], + ['rmp', ['audio/x-pn-realaudio-plugin', 'audio/x-pn-realaudio']], + ['rms', 'application/vnd.jcp.javame.midlet-rms'], + ['rnc', 'application/relax-ng-compact-syntax'], + ['rng', ['application/ringing-tones', 'application/vnd.nokia.ringing-tone']], + ['rnx', 'application/vnd.rn-realplayer'], + ['roff', 'application/x-troff'], + ['rp', 'image/vnd.rn-realpix'], + ['rp9', 'application/vnd.cloanto.rp9'], + ['rpm', 'audio/x-pn-realaudio-plugin'], + ['rpss', 'application/vnd.nokia.radio-presets'], + ['rpst', 'application/vnd.nokia.radio-preset'], + ['rq', 'application/sparql-query'], + ['rs', 'application/rls-services+xml'], + ['rsd', 'application/rsd+xml'], + ['rt', ['text/richtext', 'text/vnd.rn-realtext']], + ['rtf', ['application/rtf', 'text/richtext', 'application/x-rtf']], + ['rtx', ['text/richtext', 'application/rtf']], + ['rv', 'video/vnd.rn-realvideo'], + ['s', 'text/x-asm'], + ['s3m', 'audio/s3m'], + ['saf', 'application/vnd.yamaha.smaf-audio'], + ['saveme', 'application/octet-stream'], + ['sbk', 'application/x-tbook'], + ['sbml', 'application/sbml+xml'], + ['sc', 'application/vnd.ibm.secure-container'], + ['scd', 'application/x-msschedule'], + [ + 'scm', + ['application/vnd.lotus-screencam', 'video/x-scm', 'text/x-script.guile', 'application/x-lotusscreencam', 'text/x-script.scheme'] + ], + ['scq', 'application/scvp-cv-request'], + ['scs', 'application/scvp-cv-response'], + ['sct', 'text/scriptlet'], + ['scurl', 'text/vnd.curl.scurl'], + ['sda', 'application/vnd.stardivision.draw'], + ['sdc', 'application/vnd.stardivision.calc'], + ['sdd', 'application/vnd.stardivision.impress'], + ['sdkm', 'application/vnd.solent.sdkm+xml'], + ['sdml', 'text/plain'], + ['sdp', ['application/sdp', 'application/x-sdp']], + ['sdr', 'application/sounder'], + ['sdw', 'application/vnd.stardivision.writer'], + ['sea', ['application/sea', 'application/x-sea']], + ['see', 'application/vnd.seemail'], + ['seed', 'application/vnd.fdsn.seed'], + ['sema', 'application/vnd.sema'], + ['semd', 'application/vnd.semd'], + ['semf', 'application/vnd.semf'], + ['ser', 'application/java-serialized-object'], + ['set', 'application/set'], + ['setpay', 'application/set-payment-initiation'], + ['setreg', 'application/set-registration-initiation'], + ['sfd-hdstx', 'application/vnd.hydrostatix.sof-data'], + ['sfs', 'application/vnd.spotfire.sfs'], + ['sgl', 'application/vnd.stardivision.writer-global'], + ['sgm', ['text/sgml', 'text/x-sgml']], + ['sgml', ['text/sgml', 'text/x-sgml']], + ['sh', ['application/x-shar', 'application/x-bsh', 'application/x-sh', 'text/x-script.sh']], + ['shar', ['application/x-bsh', 'application/x-shar']], + ['shf', 'application/shf+xml'], + ['shtml', ['text/html', 'text/x-server-parsed-html']], + ['sid', 'audio/x-psid'], + ['sis', 'application/vnd.symbian.install'], + ['sit', ['application/x-stuffit', 'application/x-sit']], + ['sitx', 'application/x-stuffitx'], + ['skd', 'application/x-koan'], + ['skm', 'application/x-koan'], + ['skp', ['application/vnd.koan', 'application/x-koan']], + ['skt', 'application/x-koan'], + ['sl', 'application/x-seelogo'], + ['sldm', 'application/vnd.ms-powerpoint.slide.macroenabled.12'], + ['sldx', 'application/vnd.openxmlformats-officedocument.presentationml.slide'], + ['slt', 'application/vnd.epson.salt'], + ['sm', 'application/vnd.stepmania.stepchart'], + ['smf', 'application/vnd.stardivision.math'], + ['smi', ['application/smil', 'application/smil+xml']], + ['smil', 'application/smil'], + ['snd', ['audio/basic', 'audio/x-adpcm']], + ['snf', 'application/x-font-snf'], + ['sol', 'application/solids'], + ['spc', ['text/x-speech', 'application/x-pkcs7-certificates']], + ['spf', 'application/vnd.yamaha.smaf-phrase'], + ['spl', ['application/futuresplash', 'application/x-futuresplash']], + ['spot', 'text/vnd.in3d.spot'], + ['spp', 'application/scvp-vp-response'], + ['spq', 'application/scvp-vp-request'], + ['spr', 'application/x-sprite'], + ['sprite', 'application/x-sprite'], + ['src', 'application/x-wais-source'], + ['sru', 'application/sru+xml'], + ['srx', 'application/sparql-results+xml'], + ['sse', 'application/vnd.kodak-descriptor'], + ['ssf', 'application/vnd.epson.ssf'], + ['ssi', 'text/x-server-parsed-html'], + ['ssm', 'application/streamingmedia'], + ['ssml', 'application/ssml+xml'], + ['sst', ['application/vnd.ms-pkicertstore', 'application/vnd.ms-pki.certstore']], + ['st', 'application/vnd.sailingtracker.track'], + ['stc', 'application/vnd.sun.xml.calc.template'], + ['std', 'application/vnd.sun.xml.draw.template'], + ['step', 'application/step'], + ['stf', 'application/vnd.wt.stf'], + ['sti', 'application/vnd.sun.xml.impress.template'], + ['stk', 'application/hyperstudio'], + ['stl', ['application/vnd.ms-pkistl', 'application/sla', 'application/vnd.ms-pki.stl', 'application/x-navistyle']], + ['stm', 'text/html'], + ['stp', 'application/step'], + ['str', 'application/vnd.pg.format'], + ['stw', 'application/vnd.sun.xml.writer.template'], + ['sub', 'image/vnd.dvb.subtitle'], + ['sus', 'application/vnd.sus-calendar'], + ['sv4cpio', 'application/x-sv4cpio'], + ['sv4crc', 'application/x-sv4crc'], + ['svc', 'application/vnd.dvb.service'], + ['svd', 'application/vnd.svd'], + ['svf', ['image/vnd.dwg', 'image/x-dwg']], + ['svg', 'image/svg+xml'], + ['svr', ['x-world/x-svr', 'application/x-world']], + ['swf', 'application/x-shockwave-flash'], + ['swi', 'application/vnd.aristanetworks.swi'], + ['sxc', 'application/vnd.sun.xml.calc'], + ['sxd', 'application/vnd.sun.xml.draw'], + ['sxg', 'application/vnd.sun.xml.writer.global'], + ['sxi', 'application/vnd.sun.xml.impress'], + ['sxm', 'application/vnd.sun.xml.math'], + ['sxw', 'application/vnd.sun.xml.writer'], + ['t', ['text/troff', 'application/x-troff']], + ['talk', 'text/x-speech'], + ['tao', 'application/vnd.tao.intent-module-archive'], + ['tar', 'application/x-tar'], + ['tbk', ['application/toolbook', 'application/x-tbook']], + ['tcap', 'application/vnd.3gpp2.tcap'], + ['tcl', ['text/x-script.tcl', 'application/x-tcl']], + ['tcsh', 'text/x-script.tcsh'], + ['teacher', 'application/vnd.smart.teacher'], + ['tei', 'application/tei+xml'], + ['tex', 'application/x-tex'], + ['texi', 'application/x-texinfo'], + ['texinfo', 'application/x-texinfo'], + ['text', ['application/plain', 'text/plain']], + ['tfi', 'application/thraud+xml'], + ['tfm', 'application/x-tex-tfm'], + ['tgz', ['application/gnutar', 'application/x-compressed']], + ['thmx', 'application/vnd.ms-officetheme'], + ['tif', ['image/tiff', 'image/x-tiff']], + ['tiff', ['image/tiff', 'image/x-tiff']], + ['tmo', 'application/vnd.tmobile-livetv'], + ['torrent', 'application/x-bittorrent'], + ['tpl', 'application/vnd.groove-tool-template'], + ['tpt', 'application/vnd.trid.tpt'], + ['tr', 'application/x-troff'], + ['tra', 'application/vnd.trueapp'], + ['trm', 'application/x-msterminal'], + ['tsd', 'application/timestamped-data'], + ['tsi', 'audio/tsp-audio'], + ['tsp', ['application/dsptype', 'audio/tsplayer']], + ['tsv', 'text/tab-separated-values'], + ['ttf', 'application/x-font-ttf'], + ['ttl', 'text/turtle'], + ['turbot', 'image/florian'], + ['twd', 'application/vnd.simtech-mindmapper'], + ['txd', 'application/vnd.genomatix.tuxedo'], + ['txf', 'application/vnd.mobius.txf'], + ['txt', 'text/plain'], + ['ufd', 'application/vnd.ufdl'], + ['uil', 'text/x-uil'], + ['uls', 'text/iuls'], + ['umj', 'application/vnd.umajin'], + ['uni', 'text/uri-list'], + ['unis', 'text/uri-list'], + ['unityweb', 'application/vnd.unity'], + ['unv', 'application/i-deas'], + ['uoml', 'application/vnd.uoml+xml'], + ['uri', 'text/uri-list'], + ['uris', 'text/uri-list'], + ['ustar', ['application/x-ustar', 'multipart/x-ustar']], + ['utz', 'application/vnd.uiq.theme'], + ['uu', ['application/octet-stream', 'text/x-uuencode']], + ['uue', 'text/x-uuencode'], + ['uva', 'audio/vnd.dece.audio'], + ['uvh', 'video/vnd.dece.hd'], + ['uvi', 'image/vnd.dece.graphic'], + ['uvm', 'video/vnd.dece.mobile'], + ['uvp', 'video/vnd.dece.pd'], + ['uvs', 'video/vnd.dece.sd'], + ['uvu', 'video/vnd.uvvu.mp4'], + ['uvv', 'video/vnd.dece.video'], + ['vcd', 'application/x-cdlink'], + ['vcf', 'text/x-vcard'], + ['vcg', 'application/vnd.groove-vcard'], + ['vcs', 'text/x-vcalendar'], + ['vcx', 'application/vnd.vcx'], + ['vda', 'application/vda'], + ['vdo', 'video/vdo'], + ['vew', 'application/groupwise'], + ['vis', 'application/vnd.visionary'], + ['viv', ['video/vivo', 'video/vnd.vivo']], + ['vivo', ['video/vivo', 'video/vnd.vivo']], + ['vmd', 'application/vocaltec-media-desc'], + ['vmf', 'application/vocaltec-media-file'], + ['voc', ['audio/voc', 'audio/x-voc']], + ['vos', 'video/vosaic'], + ['vox', 'audio/voxware'], + ['vqe', 'audio/x-twinvq-plugin'], + ['vqf', 'audio/x-twinvq'], + ['vql', 'audio/x-twinvq-plugin'], + ['vrml', ['model/vrml', 'x-world/x-vrml', 'application/x-vrml']], + ['vrt', 'x-world/x-vrt'], + ['vsd', ['application/vnd.visio', 'application/x-visio']], + ['vsf', 'application/vnd.vsf'], + ['vst', 'application/x-visio'], + ['vsw', 'application/x-visio'], + ['vtu', 'model/vnd.vtu'], + ['vxml', 'application/voicexml+xml'], + ['w60', 'application/wordperfect6.0'], + ['w61', 'application/wordperfect6.1'], + ['w6w', 'application/msword'], + ['wad', 'application/x-doom'], + ['wav', ['audio/wav', 'audio/x-wav']], + ['wax', 'audio/x-ms-wax'], + ['wb1', 'application/x-qpro'], + ['wbmp', 'image/vnd.wap.wbmp'], + ['wbs', 'application/vnd.criticaltools.wbs+xml'], + ['wbxml', 'application/vnd.wap.wbxml'], + ['wcm', 'application/vnd.ms-works'], + ['wdb', 'application/vnd.ms-works'], + ['web', 'application/vnd.xara'], + ['weba', 'audio/webm'], + ['webm', 'video/webm'], + ['webp', 'image/webp'], + ['wg', 'application/vnd.pmi.widget'], + ['wgt', 'application/widget'], + ['wiz', 'application/msword'], + ['wk1', 'application/x-123'], + ['wks', 'application/vnd.ms-works'], + ['wm', 'video/x-ms-wm'], + ['wma', 'audio/x-ms-wma'], + ['wmd', 'application/x-ms-wmd'], + ['wmf', ['windows/metafile', 'application/x-msmetafile']], + ['wml', 'text/vnd.wap.wml'], + ['wmlc', 'application/vnd.wap.wmlc'], + ['wmls', 'text/vnd.wap.wmlscript'], + ['wmlsc', 'application/vnd.wap.wmlscriptc'], + ['wmv', 'video/x-ms-wmv'], + ['wmx', 'video/x-ms-wmx'], + ['wmz', 'application/x-ms-wmz'], + ['woff', 'application/x-font-woff'], + ['word', 'application/msword'], + ['wp', 'application/wordperfect'], + ['wp5', ['application/wordperfect', 'application/wordperfect6.0']], + ['wp6', 'application/wordperfect'], + ['wpd', ['application/wordperfect', 'application/vnd.wordperfect', 'application/x-wpwin']], + ['wpl', 'application/vnd.ms-wpl'], + ['wps', 'application/vnd.ms-works'], + ['wq1', 'application/x-lotus'], + ['wqd', 'application/vnd.wqd'], + ['wri', ['application/mswrite', 'application/x-wri', 'application/x-mswrite']], + ['wrl', ['model/vrml', 'x-world/x-vrml', 'application/x-world']], + ['wrz', ['model/vrml', 'x-world/x-vrml']], + ['wsc', 'text/scriplet'], + ['wsdl', 'application/wsdl+xml'], + ['wspolicy', 'application/wspolicy+xml'], + ['wsrc', 'application/x-wais-source'], + ['wtb', 'application/vnd.webturbo'], + ['wtk', 'application/x-wintalk'], + ['wvx', 'video/x-ms-wvx'], + ['x-png', 'image/png'], + ['x3d', 'application/vnd.hzn-3d-crossword'], + ['xaf', 'x-world/x-vrml'], + ['xap', 'application/x-silverlight-app'], + ['xar', 'application/vnd.xara'], + ['xbap', 'application/x-ms-xbap'], + ['xbd', 'application/vnd.fujixerox.docuworks.binder'], + ['xbm', ['image/xbm', 'image/x-xbm', 'image/x-xbitmap']], + ['xdf', 'application/xcap-diff+xml'], + ['xdm', 'application/vnd.syncml.dm+xml'], + ['xdp', 'application/vnd.adobe.xdp+xml'], + ['xdr', 'video/x-amt-demorun'], + ['xdssc', 'application/dssc+xml'], + ['xdw', 'application/vnd.fujixerox.docuworks'], + ['xenc', 'application/xenc+xml'], + ['xer', 'application/patch-ops-error+xml'], + ['xfdf', 'application/vnd.adobe.xfdf'], + ['xfdl', 'application/vnd.xfdl'], + ['xgz', 'xgl/drawing'], + ['xhtml', 'application/xhtml+xml'], + ['xif', 'image/vnd.xiff'], + ['xl', 'application/excel'], + ['xla', ['application/vnd.ms-excel', 'application/excel', 'application/x-msexcel', 'application/x-excel']], + ['xlam', 'application/vnd.ms-excel.addin.macroenabled.12'], + ['xlb', ['application/excel', 'application/vnd.ms-excel', 'application/x-excel']], + ['xlc', ['application/vnd.ms-excel', 'application/excel', 'application/x-excel']], + ['xld', ['application/excel', 'application/x-excel']], + ['xlk', ['application/excel', 'application/x-excel']], + ['xll', ['application/excel', 'application/vnd.ms-excel', 'application/x-excel']], + ['xlm', ['application/vnd.ms-excel', 'application/excel', 'application/x-excel']], + ['xls', ['application/vnd.ms-excel', 'application/excel', 'application/x-msexcel', 'application/x-excel']], + ['xlsb', 'application/vnd.ms-excel.sheet.binary.macroenabled.12'], + ['xlsm', 'application/vnd.ms-excel.sheet.macroenabled.12'], + ['xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'], + ['xlt', ['application/vnd.ms-excel', 'application/excel', 'application/x-excel']], + ['xltm', 'application/vnd.ms-excel.template.macroenabled.12'], + ['xltx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.template'], + ['xlv', ['application/excel', 'application/x-excel']], + ['xlw', ['application/vnd.ms-excel', 'application/excel', 'application/x-msexcel', 'application/x-excel']], + ['xm', 'audio/xm'], + ['xml', ['application/xml', 'text/xml', 'application/atom+xml', 'application/rss+xml']], + ['xmz', 'xgl/movie'], + ['xo', 'application/vnd.olpc-sugar'], + ['xof', 'x-world/x-vrml'], + ['xop', 'application/xop+xml'], + ['xpi', 'application/x-xpinstall'], + ['xpix', 'application/x-vnd.ls-xpix'], + ['xpm', ['image/xpm', 'image/x-xpixmap']], + ['xpr', 'application/vnd.is-xpr'], + ['xps', 'application/vnd.ms-xpsdocument'], + ['xpw', 'application/vnd.intercon.formnet'], + ['xslt', 'application/xslt+xml'], + ['xsm', 'application/vnd.syncml+xml'], + ['xspf', 'application/xspf+xml'], + ['xsr', 'video/x-amt-showrun'], + ['xul', 'application/vnd.mozilla.xul+xml'], + ['xwd', ['image/x-xwd', 'image/x-xwindowdump']], + ['xyz', ['chemical/x-xyz', 'chemical/x-pdb']], + ['yang', 'application/yang'], + ['yin', 'application/yin+xml'], + ['z', ['application/x-compressed', 'application/x-compress']], + ['zaz', 'application/vnd.zzazz.deck+xml'], + ['zip', ['application/zip', 'multipart/x-zip', 'application/x-zip-compressed', 'application/x-compressed']], + ['zir', 'application/vnd.zul'], + ['zmm', 'application/vnd.handheld-entertainment+xml'], + ['zoo', 'application/octet-stream'], + ['zsh', 'text/x-script.zsh'] +]); + +module.exports = { + detectMimeType(filename) { + if (!filename) { + return defaultMimeType; + } + + let parsed = path.parse(filename); + let extension = (parsed.ext.substr(1) || parsed.name || '').split('?').shift().trim().toLowerCase(); + let value = defaultMimeType; + + if (extensions.has(extension)) { + value = extensions.get(extension); + } + + if (Array.isArray(value)) { + return value[0]; + } + return value; + }, + + detectExtension(mimeType) { + if (!mimeType) { + return defaultExtension; + } + let parts = (mimeType || '').toLowerCase().trim().split('/'); + let rootType = parts.shift().trim(); + let subType = parts.join('/').trim(); + + if (mimeTypes.has(rootType + '/' + subType)) { + let value = mimeTypes.get(rootType + '/' + subType); + if (Array.isArray(value)) { + return value[0]; + } + return value; + } + + switch (rootType) { + case 'text': + return 'txt'; + default: + return 'bin'; + } + } +}; diff --git a/node_modules/nodemailer/lib/mime-node/index.js b/node_modules/nodemailer/lib/mime-node/index.js new file mode 100644 index 0000000..30c9d5c --- /dev/null +++ b/node_modules/nodemailer/lib/mime-node/index.js @@ -0,0 +1,1325 @@ +/* eslint no-undefined: 0, prefer-spread: 0, no-control-regex: 0 */ + +'use strict'; + +const crypto = require('crypto'); +const fs = require('fs'); +const punycode = require('../punycode'); +const PassThrough = require('stream').PassThrough; +const shared = require('../shared'); + +const mimeFuncs = require('../mime-funcs'); +const qp = require('../qp'); +const base64 = require('../base64'); +const addressparser = require('../addressparser'); +const nmfetch = require('../fetch'); +const errors = require('../errors'); +const LastNewline = require('./last-newline'); + +const LeWindows = require('./le-windows'); +const LeUnix = require('./le-unix'); + +/** + * 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 {String} contentType Define the content type for the node. Can be left blank for attachments (derived from filename) + * @param {Object} [options] optional options + * @param {Object} [options.rootNode] root node for this tree + * @param {Object} [options.parentNode] immediate parent for this node + * @param {Object} [options.filename] filename for an attachment node + * @param {String} [options.baseBoundary] shared part of the unique multipart boundary + * @param {Boolean} [options.keepBcc] If true, do not exclude Bcc from the generated headers + * @param {Function} [options.normalizeHeaderKey] method to normalize header keys for custom caseing + * @param {String} [options.textEncoding] either 'Q' (the default) or 'B' + */ +class MimeNode { + constructor(contentType, options) { + this.nodeCounter = 0; + + options = options || {}; + + /** + * shared part of the unique multipart boundary + */ + this.baseBoundary = options.baseBoundary || crypto.randomBytes(8).toString('hex'); + this.boundaryPrefix = options.boundaryPrefix || '--_NmP'; + + this.disableFileAccess = !!options.disableFileAccess; + this.disableUrlAccess = !!options.disableUrlAccess; + + this.normalizeHeaderKey = options.normalizeHeaderKey; + + /** + * If date headers is missing and current node is the root, this value is used instead + */ + this.date = new Date(); + + /** + * Root node for current mime tree + */ + this.rootNode = options.rootNode || this; + + /** + * If true include Bcc in generated headers (if available) + */ + this.keepBcc = !!options.keepBcc; + + /** + * If filename is specified but contentType is not (probably an attachment) + * detect the content type from filename extension + */ + if (options.filename) { + /** + * Filename for this node. Useful with attachments + */ + this.filename = options.filename; + if (!contentType) { + contentType = mimeFuncs.detectMimeType(this.filename.split('.').pop()); + } + } + + /** + * Indicates which encoding should be used for header strings: "Q" or "B" + */ + this.textEncoding = (options.textEncoding || '').toString().trim().charAt(0).toUpperCase(); + + /** + * Immediate parent for this node (or undefined if not set) + */ + this.parentNode = options.parentNode; + + /** + * Hostname for default message-id values + */ + this.hostname = options.hostname; + + /** + * 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. + */ + this.newline = options.newline; + + /** + * An array for possible child nodes + */ + this.childNodes = []; + + /** + * Used for generating unique boundaries (prepended to the shared base) + */ + this._nodeId = ++this.rootNode.nodeCounter; + + /** + * A list of header values for this node in the form of [{key:'', value:''}] + */ + this._headers = []; + + /** + * True if the content only uses ASCII printable characters + * @type {Boolean} + */ + this._isPlainText = false; + + /** + * True if the content is plain text but has longer lines than allowed + * @type {Boolean} + */ + this._hasLongLines = false; + + /** + * If set, use instead this value for envelopes instead of generating one + * @type {Boolean} + */ + this._envelope = false; + + /** + * If set then use this value as the stream content instead of building it + * @type {String|Buffer|Stream} + */ + this._raw = false; + + /** + * Additional transform streams that the message will be piped before + * exposing by createReadStream + * @type {Array} + */ + this._transforms = []; + + /** + * Additional process functions that the message will be piped through before + * exposing by createReadStream. These functions are run after transforms + * @type {Array} + */ + this._processFuncs = []; + + /** + * If content type is set (or derived from the filename) add it to headers + */ + if (contentType) { + this.setHeader('Content-Type', contentType); + } + } + + /////// PUBLIC METHODS + + /** + * Creates and appends a child node.Arguments provided are passed to MimeNode constructor + * + * @param {String} [contentType] Optional content type + * @param {Object} [options] Optional options object + * @return {Object} Created node object + */ + createChild(contentType, options) { + if (!options && typeof contentType === 'object') { + options = contentType; + contentType = undefined; + } + let node = new MimeNode(contentType, options); + this.appendChild(node); + return node; + } + + /** + * Appends an existing node to the mime tree. Removes the node from an existing + * tree if needed + * + * @param {Object} childNode node to be appended + * @return {Object} Appended node object + */ + appendChild(childNode) { + if (childNode.rootNode !== this.rootNode) { + childNode.rootNode = this.rootNode; + childNode._nodeId = ++this.rootNode.nodeCounter; + } + + childNode.parentNode = this; + + this.childNodes.push(childNode); + return childNode; + } + + /** + * Replaces current node with another node + * + * @param {Object} node Replacement node + * @return {Object} Replacement node + */ + replace(node) { + if (node === this) { + return this; + } + + this.parentNode.childNodes.forEach((childNode, i) => { + if (childNode === this) { + node.rootNode = this.rootNode; + node.parentNode = this.parentNode; + node._nodeId = this._nodeId; + + this.rootNode = this; + this.parentNode = undefined; + + node.parentNode.childNodes[i] = node; + } + }); + + return node; + } + + /** + * Removes current node from the mime tree + * + * @return {Object} removed node + */ + remove() { + if (!this.parentNode) { + return this; + } + + for (let i = this.parentNode.childNodes.length - 1; i >= 0; i--) { + if (this.parentNode.childNodes[i] === this) { + this.parentNode.childNodes.splice(i, 1); + this.parentNode = undefined; + this.rootNode = this; + return this; + } + } + } + + /** + * 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 {String|Array|Object} key Header key or a list of key value pairs + * @param {String} value Header value + * @return {Object} current node + */ + setHeader(key, value) { + let added = false, + headerValue; + + // Allow setting multiple headers at once + if (!value && key && typeof key === 'object') { + // allow {key:'content-type', value: 'text/plain'} + if (key.key && 'value' in key) { + this.setHeader(key.key, key.value); + } else if (Array.isArray(key)) { + // allow [{key:'content-type', value: 'text/plain'}] + key.forEach(i => { + this.setHeader(i.key, i.value); + }); + } else { + // allow {'content-type': 'text/plain'} + Object.keys(key).forEach(i => { + this.setHeader(i, key[i]); + }); + } + return this; + } + + key = this._normalizeHeaderKey(key); + + headerValue = { + key, + value + }; + + // Check if the value exists and overwrite + for (let i = 0, len = this._headers.length; i < len; i++) { + if (this._headers[i].key === key) { + if (!added) { + // replace the first match + this._headers[i] = headerValue; + added = true; + } else { + // remove following matches + this._headers.splice(i, 1); + i--; + len--; + } + } + } + + // match not found, append the value + if (!added) { + this._headers.push(headerValue); + } + + return 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 {String|Array|Object} key Header key or a list of key value pairs + * @param {String} value Header value + * @return {Object} current node + */ + addHeader(key, value) { + // Allow setting multiple headers at once + if (!value && key && typeof key === 'object') { + // allow {key:'content-type', value: 'text/plain'} + if (key.key && key.value) { + this.addHeader(key.key, key.value); + } else if (Array.isArray(key)) { + // allow [{key:'content-type', value: 'text/plain'}] + key.forEach(i => { + this.addHeader(i.key, i.value); + }); + } else { + // allow {'content-type': 'text/plain'} + Object.keys(key).forEach(i => { + this.addHeader(i, key[i]); + }); + } + return this; + } else if (Array.isArray(value)) { + value.forEach(val => { + this.addHeader(key, val); + }); + return this; + } + + this._headers.push({ + key: this._normalizeHeaderKey(key), + value + }); + + return this; + } + + /** + * Retrieves the first mathcing value of a selected key + * + * @param {String} key Key to search for + * @retun {String} Value for the key + */ + getHeader(key) { + key = this._normalizeHeaderKey(key); + for (let i = 0, len = this._headers.length; i < len; i++) { + if (this._headers[i].key === key) { + return this._headers[i].value; + } + } + } + + /** + * 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 (String|Buffer) content Body content + * @return {Object} current node + */ + setContent(content) { + this.content = content; + if (typeof this.content.pipe === 'function') { + // pre-stream handler. might be triggered if a stream is set as content + // and 'error' fires before anything is done with this stream + this._contentErrorHandler = err => { + this.content.removeListener('error', this._contentErrorHandler); + this.content = err; + }; + this.content.once('error', this._contentErrorHandler); + } else if (typeof this.content === 'string') { + this._isPlainText = mimeFuncs.isPlainText(this.content); + if (this._isPlainText && mimeFuncs.hasLongerLines(this.content, 76)) { + // If there are lines longer than 76 symbols/bytes do not use 7bit + this._hasLongLines = true; + } + } + return this; + } + + build(callback) { + let promise; + + if (!callback) { + promise = new Promise((resolve, reject) => { + callback = shared.callbackPromise(resolve, reject); + }); + } + + let stream = this.createReadStream(); + let buf = []; + let buflen = 0; + let returned = false; + + stream.on('readable', () => { + let chunk; + + while ((chunk = stream.read()) !== null) { + buf.push(chunk); + buflen += chunk.length; + } + }); + + stream.once('error', err => { + if (returned) { + return; + } + returned = true; + + return callback(err); + }); + + stream.once('end', chunk => { + if (returned) { + return; + } + returned = true; + + if (chunk && chunk.length) { + buf.push(chunk); + buflen += chunk.length; + } + return callback(null, Buffer.concat(buf, buflen)); + }); + + return promise; + } + + getTransferEncoding() { + let transferEncoding = false; + let contentType = (this.getHeader('Content-Type') || '').toString().toLowerCase().trim(); + + if (this.content) { + transferEncoding = (this.getHeader('Content-Transfer-Encoding') || '').toString().toLowerCase().trim(); + if (!transferEncoding || !['base64', 'quoted-printable'].includes(transferEncoding)) { + if (/^text\//i.test(contentType)) { + // If there are no special symbols, no need to modify the text + if (this._isPlainText && !this._hasLongLines) { + transferEncoding = '7bit'; + } else if (typeof this.content === 'string' || this.content instanceof Buffer) { + // detect preferred encoding for string value + transferEncoding = this._getTextEncoding(this.content) === 'Q' ? 'quoted-printable' : 'base64'; + } else { + // we can not check content for a stream, so either use preferred encoding or fallback to QP + transferEncoding = this.textEncoding === 'B' ? 'base64' : 'quoted-printable'; + } + } else if (!/^(multipart|message)\//i.test(contentType)) { + transferEncoding = transferEncoding || 'base64'; + } + } + } + return transferEncoding; + } + + /** + * Builds the header block for the mime node. Append \r\n\r\n before writing the content + * + * @returns {String} Headers + */ + buildHeaders() { + let transferEncoding = this.getTransferEncoding(); + let headers = []; + + if (transferEncoding) { + this.setHeader('Content-Transfer-Encoding', transferEncoding); + } + + if (this.filename && !this.getHeader('Content-Disposition')) { + this.setHeader('Content-Disposition', 'attachment'); + } + + // Ensure mandatory header fields + if (this.rootNode === this) { + if (!this.getHeader('Date')) { + this.setHeader('Date', this.date.toUTCString().replace(/GMT/, '+0000')); + } + + // ensure that Message-Id is present + this.messageId(); + + if (!this.getHeader('MIME-Version')) { + this.setHeader('MIME-Version', '1.0'); + } + + // Ensure that Content-Type is the last header for the root node + for (let i = this._headers.length - 2; i >= 0; i--) { + let header = this._headers[i]; + if (header.key === 'Content-Type') { + this._headers.splice(i, 1); + this._headers.push(header); + } + } + } + + this._headers.forEach(header => { + let key = header.key; + let value = header.value; + let structured; + let param; + let options = {}; + let formattedHeaders = ['From', 'Sender', 'To', 'Cc', 'Bcc', 'Reply-To', 'Date', 'References']; + + if (value && typeof value === 'object' && !formattedHeaders.includes(key)) { + Object.keys(value).forEach(key => { + if (key !== 'value') { + options[key] = value[key]; + } + }); + value = (value.value || '').toString(); + if (!value.trim()) { + return; + } + } + + if (options.prepared) { + // header value is + if (options.foldLines) { + headers.push(mimeFuncs.foldLines(key + ': ' + value)); + } else { + headers.push(key + ': ' + value); + } + return; + } + + switch (header.key) { + case 'Content-Disposition': + structured = mimeFuncs.parseHeaderValue(value); + if (this.filename) { + structured.params.filename = this.filename; + } + value = mimeFuncs.buildHeaderValue(structured); + break; + + case 'Content-Type': + structured = mimeFuncs.parseHeaderValue(value); + + this._handleContentType(structured); + + if ( + structured.value.match(/^text\/plain\b/) && + typeof this.content === 'string' && + /[\u0080-\uFFFF]/.test(this.content) + ) { + structured.params.charset = 'utf-8'; + } + + value = mimeFuncs.buildHeaderValue(structured); + + if (this.filename) { + // add support for non-compliant clients like QQ webmail + // we can't build the value with buildHeaderValue as the value is non standard and + // would be converted to parameter continuation encoding that we do not want + param = this._encodeWords(this.filename); + + if (param !== this.filename || /[\s'"\\;:/=(),<>@[\]?]|^-/.test(param)) { + // include value in quotes if needed + param = '"' + param + '"'; + } + value += '; name=' + param; + } + break; + + case 'Bcc': + if (!this.keepBcc) { + // skip BCC values + return; + } + break; + } + + value = this._encodeHeaderValue(key, value); + + // skip empty lines + if (!(value || '').toString().trim()) { + return; + } + + if (typeof this.normalizeHeaderKey === 'function') { + let normalized = this.normalizeHeaderKey(key, value); + if (normalized && typeof normalized === 'string' && normalized.length) { + key = normalized; + } + } + + headers.push(mimeFuncs.foldLines(key + ': ' + value, 76)); + }); + + return headers.join('\r\n'); + } + + /** + * 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 {String} Compiled message + */ + createReadStream(options) { + options = options || {}; + + let stream = new PassThrough(options); + let outputStream = stream; + let transform; + + this.stream(stream, options, err => { + if (err) { + outputStream.emit('error', err); + return; + } + stream.end(); + }); + + for (let i = 0, len = this._transforms.length; i < len; i++) { + transform = typeof this._transforms[i] === 'function' ? this._transforms[i]() : this._transforms[i]; + outputStream.once('error', err => { + transform.emit('error', err); + }); + outputStream = outputStream.pipe(transform); + } + + // ensure terminating newline after possible user transforms + transform = new LastNewline(); + outputStream.once('error', err => { + transform.emit('error', err); + }); + outputStream = outputStream.pipe(transform); + + // dkim and stuff + for (let i = 0, len = this._processFuncs.length; i < len; i++) { + transform = this._processFuncs[i]; + outputStream = transform(outputStream); + } + + if (this.newline) { + const winbreak = ['win', 'windows', 'dos', '\r\n'].includes(this.newline.toString().toLowerCase()); + const newlineTransform = winbreak ? new LeWindows() : new LeUnix(); + + const stream = outputStream.pipe(newlineTransform); + outputStream.on('error', err => stream.emit('error', err)); + return stream; + } + + return outputStream; + } + + /** + * Appends a transform stream object to the transforms list. Final output + * is passed through this stream before exposing + * + * @param {Object} transform Read-Write stream + */ + transform(transform) { + this._transforms.push(transform); + } + + /** + * Appends a post process function. The functon is run after transforms and + * uses the following syntax + * + * processFunc(input) -> outputStream + * + * @param {Object} processFunc Read-Write stream + */ + processFunc(processFunc) { + this._processFuncs.push(processFunc); + } + + stream(outputStream, options, done) { + let transferEncoding = this.getTransferEncoding(); + let contentStream; + let localStream; + + // protect actual callback against multiple triggering + let returned = false; + let callback = err => { + if (returned) { + return; + } + returned = true; + done(err); + }; + + // for multipart nodes, push child nodes + // for content nodes end the stream + let finalize = () => { + let childId = 0; + let processChildNode = () => { + if (childId >= this.childNodes.length) { + outputStream.write('\r\n--' + this.boundary + '--\r\n'); + return callback(); + } + let child = this.childNodes[childId++]; + outputStream.write((childId > 1 ? '\r\n' : '') + '--' + this.boundary + '\r\n'); + child.stream(outputStream, options, err => { + if (err) { + return callback(err); + } + setImmediate(processChildNode); + }); + }; + + if (this.multipart) { + setImmediate(processChildNode); + } else { + return callback(); + } + }; + + // pushes node content + let sendContent = () => { + if (this.content) { + if (Object.prototype.toString.call(this.content) === '[object Error]') { + // content is already errored + return callback(this.content); + } + + if (typeof this.content.pipe === 'function') { + this.content.removeListener('error', this._contentErrorHandler); + this._contentErrorHandler = err => callback(err); + this.content.once('error', this._contentErrorHandler); + } + + let createStream = () => { + if (['quoted-printable', 'base64'].includes(transferEncoding)) { + contentStream = new (transferEncoding === 'base64' ? base64 : qp).Encoder(options); + + contentStream.pipe(outputStream, { + end: false + }); + contentStream.once('end', finalize); + contentStream.once('error', err => callback(err)); + + localStream = this._getStream(this.content); + localStream.pipe(contentStream); + } else { + // anything that is not QP or Base54 passes as-is + localStream = this._getStream(this.content); + localStream.pipe(outputStream, { + end: false + }); + localStream.once('end', finalize); + } + + localStream.once('error', err => callback(err)); + }; + + if (this.content._resolve) { + let chunks = []; + let chunklen = 0; + let returned = false; + let sourceStream = this._getStream(this.content); + sourceStream.on('error', err => { + if (returned) { + return; + } + returned = true; + callback(err); + }); + sourceStream.on('readable', () => { + let chunk; + while ((chunk = sourceStream.read()) !== null) { + chunks.push(chunk); + chunklen += chunk.length; + } + }); + sourceStream.on('end', () => { + if (returned) { + return; + } + returned = true; + this.content._resolve = false; + this.content._resolvedValue = Buffer.concat(chunks, chunklen); + setImmediate(createStream); + }); + } else { + setImmediate(createStream); + } + return; + } else { + return setImmediate(finalize); + } + }; + + if (this._raw) { + setImmediate(() => { + if (Object.prototype.toString.call(this._raw) === '[object Error]') { + // content is already errored + return callback(this._raw); + } + + // remove default error handler (if set) + if (typeof this._raw.pipe === 'function') { + this._raw.removeListener('error', this._contentErrorHandler); + } + + let raw = this._getStream(this._raw); + raw.pipe(outputStream, { + end: false + }); + raw.on('error', err => outputStream.emit('error', err)); + raw.on('end', finalize); + }); + } else { + outputStream.write(this.buildHeaders() + '\r\n\r\n'); + setImmediate(sendContent); + } + } + + /** + * Sets envelope to be used instead of the generated one + * + * @return {Object} SMTP envelope in the form of {from: 'from@example.com', to: ['to@example.com']} + */ + setEnvelope(envelope) { + let list; + + this._envelope = { + from: false, + to: [] + }; + + if (envelope.from) { + list = []; + this._convertAddresses(this._parseAddresses(envelope.from), list); + list = list.filter(address => address && address.address); + if (list.length && list[0]) { + this._envelope.from = list[0].address; + } + } + ['to', 'cc', 'bcc'].forEach(key => { + if (envelope[key]) { + this._convertAddresses(this._parseAddresses(envelope[key]), this._envelope.to); + } + }); + + this._envelope.to = this._envelope.to.map(to => to.address).filter(address => address); + + let standardFields = ['to', 'cc', 'bcc', 'from']; + Object.keys(envelope).forEach(key => { + if (!standardFields.includes(key)) { + this._envelope[key] = envelope[key]; + } + }); + + return this; + } + + /** + * Generates and returns an object with parsed address fields + * + * @return {Object} Address object + */ + getAddresses() { + let addresses = {}; + + this._headers.forEach(header => { + let key = header.key.toLowerCase(); + if (['from', 'sender', 'reply-to', 'to', 'cc', 'bcc'].includes(key)) { + if (!Array.isArray(addresses[key])) { + addresses[key] = []; + } + + this._convertAddresses(this._parseAddresses(header.value), addresses[key]); + } + }); + + return addresses; + } + + /** + * Generates and returns SMTP envelope with the sender address and a list of recipients addresses + * + * @return {Object} SMTP envelope in the form of {from: 'from@example.com', to: ['to@example.com']} + */ + getEnvelope() { + if (this._envelope) { + return this._envelope; + } + + let envelope = { + from: false, + to: [] + }; + this._headers.forEach(header => { + let list = []; + if (header.key === 'From' || (!envelope.from && ['Reply-To', 'Sender'].includes(header.key))) { + this._convertAddresses(this._parseAddresses(header.value), list); + if (list.length && list[0]) { + envelope.from = list[0].address; + } + } else if (['To', 'Cc', 'Bcc'].includes(header.key)) { + this._convertAddresses(this._parseAddresses(header.value), envelope.to); + } + }); + + envelope.to = envelope.to.map(to => to.address); + + return envelope; + } + + /** + * Returns Message-Id value. If it does not exist, then creates one + * + * @return {String} Message-Id value + */ + messageId() { + let messageId = this.getHeader('Message-ID'); + // You really should define your own Message-Id field! + if (!messageId) { + messageId = this._generateMessageId(); + this.setHeader('Message-ID', messageId); + } + return messageId; + } + + /** + * Sets pregenerated content that will be used as the output of this node + * + * @param {String|Buffer|Stream} Raw MIME contents + */ + setRaw(raw) { + this._raw = raw; + + if (this._raw && typeof this._raw.pipe === 'function') { + // pre-stream handler. might be triggered if a stream is set as content + // and 'error' fires before anything is done with this stream + this._contentErrorHandler = err => { + this._raw.removeListener('error', this._contentErrorHandler); + this._raw = err; + }; + this._raw.once('error', this._contentErrorHandler); + } + + return this; + } + + /////// PRIVATE METHODS + + /** + * Detects and returns handle to a stream related with the content. + * + * @param {Mixed} content Node content + * @returns {Object} Stream object + */ + _getStream(content) { + let contentStream; + + if (content._resolvedValue) { + // pass string or buffer content as a stream + contentStream = new PassThrough(); + + setImmediate(() => { + try { + contentStream.end(content._resolvedValue); + } catch (_err) { + contentStream.emit('error', _err); + } + }); + + return contentStream; + } else if (typeof content.pipe === 'function') { + // assume as stream + return content; + } else if (content && typeof content.path === 'string' && !content.href) { + if (this.disableFileAccess) { + contentStream = new PassThrough(); + setImmediate(() => { + let err = new Error('File access rejected for ' + content.path); + err.code = errors.EFILEACCESS; + contentStream.emit('error', err); + }); + return contentStream; + } + // read file + return fs.createReadStream(content.path); + } else if (content && typeof content.href === 'string') { + if (this.disableUrlAccess) { + contentStream = new PassThrough(); + setImmediate(() => { + let err = new Error('Url access rejected for ' + content.href); + err.code = errors.EURLACCESS; + contentStream.emit('error', err); + }); + return contentStream; + } + // fetch URL + return nmfetch(content.href, { headers: content.httpHeaders }); + } else { + // pass string or buffer content as a stream + contentStream = new PassThrough(); + + setImmediate(() => { + try { + contentStream.end(content || ''); + } catch (_err) { + contentStream.emit('error', _err); + } + }); + return contentStream; + } + } + + /** + * Parses addresses. Takes in a single address or an array or an + * array of address arrays (eg. To: [[first group], [second group],...]) + * + * @param {Mixed} addresses Addresses to be parsed + * @return {Array} An array of address objects + */ + _parseAddresses(addresses) { + return [].concat.apply( + [], + [].concat(addresses).map(address => { + if (address && address.address) { + address.address = this._normalizeAddress(address.address); + address.name = address.name || ''; + return [address]; + } + return addressparser(address); + }) + ); + } + + /** + * Normalizes a header key, uses Camel-Case form, except for uppercase MIME- + * + * @param {String} key Key to be normalized + * @return {String} key in Camel-Case form + */ + _normalizeHeaderKey(key) { + key = (key || '') + .toString() + // no newlines in keys + .replace(/\r?\n|\r/g, ' ') + .trim() + .toLowerCase() + // use uppercase words, except MIME + .replace(/^X-SMTPAPI$|^(MIME|DKIM|ARC|BIMI)\b|^[a-z]|-(SPF|FBL|ID|MD5)$|-[a-z]/gi, c => c.toUpperCase()) + // special case + .replace(/^Content-Features$/i, 'Content-features'); + + return key; + } + + /** + * Checks if the content type is multipart and defines boundary if needed. + * Doesn't return anything, modifies object argument instead. + * + * @param {Object} structured Parsed header value for 'Content-Type' key + */ + _handleContentType(structured) { + this.contentType = structured.value.trim().toLowerCase(); + + this.multipart = /^multipart\//i.test(this.contentType) ? this.contentType.substr(this.contentType.indexOf('/') + 1) : false; + + if (this.multipart) { + this.boundary = structured.params.boundary = structured.params.boundary || this.boundary || this._generateBoundary(); + } else { + this.boundary = false; + } + } + + /** + * Generates a multipart boundary value + * + * @return {String} boundary value + */ + _generateBoundary() { + return this.rootNode.boundaryPrefix + '-' + this.rootNode.baseBoundary + '-Part_' + this._nodeId; + } + + /** + * Encodes a header value for use in the generated rfc2822 email. + * + * @param {String} key Header key + * @param {String} value Header value + */ + _encodeHeaderValue(key, value) { + key = this._normalizeHeaderKey(key); + + switch (key) { + // Structured headers + case 'From': + case 'Sender': + case 'To': + case 'Cc': + case 'Bcc': + case 'Reply-To': + return this._convertAddresses(this._parseAddresses(value)); + + // values enclosed in <> + case 'Message-ID': + case 'In-Reply-To': + case 'Content-Id': + value = (value || '').toString().replace(/\r?\n|\r/g, ' '); + + if (value.charAt(0) !== '<') { + value = '<' + value; + } + + if (value.charAt(value.length - 1) !== '>') { + value = value + '>'; + } + return value; + + // space separated list of values enclosed in <> + case 'References': + value = [].concat + .apply( + [], + [].concat(value || '').map(elm => { + elm = (elm || '') + .toString() + .replace(/\r?\n|\r/g, ' ') + .trim(); + return elm.replace(/<[^>]*>/g, str => str.replace(/\s/g, '')).split(/\s+/); + }) + ) + .map(elm => { + if (elm.charAt(0) !== '<') { + elm = '<' + elm; + } + if (elm.charAt(elm.length - 1) !== '>') { + elm = elm + '>'; + } + return elm; + }); + + return value.join(' ').trim(); + + case 'Date': + if (Object.prototype.toString.call(value) === '[object Date]') { + return value.toUTCString().replace(/GMT/, '+0000'); + } + + value = (value || '').toString().replace(/\r?\n|\r/g, ' '); + return this._encodeWords(value); + + case 'Content-Type': + case 'Content-Disposition': + // if it includes a filename then it is already encoded + return (value || '').toString().replace(/\r?\n|\r/g, ' '); + + default: + value = (value || '').toString().replace(/\r?\n|\r/g, ' '); + // encodeWords only encodes if needed, otherwise the original string is returned + return this._encodeWords(value); + } + } + + /** + * Rebuilds address object using punycode and other adjustments + * + * @param {Array} addresses An array of address objects + * @param {Array} [uniqueList] An array to be populated with addresses + * @return {String} address string + */ + _convertAddresses(addresses, uniqueList) { + let values = []; + + uniqueList = uniqueList || []; + + [].concat(addresses || []).forEach(address => { + if (address.address) { + address.address = this._normalizeAddress(address.address); + + if (!address.name) { + values.push(address.address.indexOf(' ') >= 0 ? `<${address.address}>` : `${address.address}`); + } else if (address.name) { + values.push(`${this._encodeAddressName(address.name)} <${address.address}>`); + } + + if (address.address) { + if (!uniqueList.filter(a => a.address === address.address).length) { + uniqueList.push(address); + } + } + } else if (address.group) { + let groupListAddresses = (address.group.length ? this._convertAddresses(address.group, uniqueList) : '').trim(); + values.push(`${this._encodeAddressName(address.name)}:${groupListAddresses};`); + } + }); + + return values.join(', '); + } + + /** + * Normalizes an email address + * + * @param {Array} address An array of address objects + * @return {String} address string + */ + _normalizeAddress(address) { + address = (address || '') + .toString() + .replace(/[\x00-\x1F<>]+/g, ' ') // remove unallowed characters + .trim(); + + let lastAt = address.lastIndexOf('@'); + if (lastAt < 0) { + // Bare username + return address; + } + + let user = address.substr(0, lastAt); + let domain = address.substr(lastAt + 1); + + // Usernames are not touched and are kept as is even if these include unicode + // Domains are punycoded by default + // 'jõgeva.ee' will be converted to 'xn--jgeva-dua.ee' + // non-unicode domains are left as is + + let encodedDomain; + + try { + encodedDomain = punycode.toASCII(domain.toLowerCase()); + } catch (_err) { + // keep as is? + } + + if (user.indexOf(' ') >= 0) { + if (user.charAt(0) !== '"') { + user = '"' + user; + } + if (user.substr(-1) !== '"') { + user = user + '"'; + } + } + + return `${user}@${encodedDomain}`; + } + + /** + * If needed, mime encodes the name part + * + * @param {String} name Name part of an address + * @returns {String} Mime word encoded string if needed + */ + _encodeAddressName(name) { + if (!/^[\w ]*$/.test(name)) { + if (/^[\x20-\x7e]*$/.test(name)) { + return '"' + name.replace(/([\\"])/g, '\\$1') + '"'; + } else { + return mimeFuncs.encodeWord(name, this._getTextEncoding(name), 52); + } + } + return name; + } + + /** + * If needed, mime encodes the name part + * + * @param {String} name Name part of an address + * @returns {String} Mime word encoded string if needed + */ + _encodeWords(value) { + // set encodeAll parameter to true even though it is against the recommendation of RFC2047, + // by default only words that include non-ascii should be converted into encoded words + // but some clients (eg. Zimbra) do not handle it properly and remove surrounding whitespace + return mimeFuncs.encodeWords(value, this._getTextEncoding(value), 52, true); + } + + /** + * Detects best mime encoding for a text value + * + * @param {String} value Value to check for + * @return {String} either 'Q' or 'B' + */ + _getTextEncoding(value) { + value = (value || '').toString(); + + let encoding = this.textEncoding; + let latinLen; + let nonLatinLen; + + if (!encoding) { + // count latin alphabet symbols and 8-bit range symbols + control symbols + // if there are more latin characters, then use quoted-printable + // encoding, otherwise use base64 + nonLatinLen = (value.match(/[\x00-\x08\x0B\x0C\x0E-\x1F\u0080-\uFFFF]/g) || []).length; + latinLen = (value.match(/[a-z]/gi) || []).length; + // if there are more latin symbols than binary/unicode, then prefer Q, otherwise B + encoding = nonLatinLen < latinLen ? 'Q' : 'B'; + } + return encoding; + } + + /** + * Generates a message id + * + * @return {String} Random Message-ID value + */ + _generateMessageId() { + return ( + '<' + + [2, 2, 2, 6].reduce( + // crux to generate UUID-like random strings + (prev, len) => prev + '-' + crypto.randomBytes(len).toString('hex'), + crypto.randomBytes(4).toString('hex') + ) + + '@' + + // try to use the domain of the FROM address or fallback to server hostname + (this.getEnvelope().from || this.hostname || 'localhost').split('@').pop() + + '>' + ); + } +} + +module.exports = MimeNode; diff --git a/node_modules/nodemailer/lib/mime-node/last-newline.js b/node_modules/nodemailer/lib/mime-node/last-newline.js new file mode 100644 index 0000000..5fcd057 --- /dev/null +++ b/node_modules/nodemailer/lib/mime-node/last-newline.js @@ -0,0 +1,33 @@ +'use strict'; + +const Transform = require('stream').Transform; + +class LastNewline extends Transform { + constructor() { + super(); + this.lastByte = false; + } + + _transform(chunk, encoding, done) { + if (chunk.length) { + this.lastByte = chunk[chunk.length - 1]; + } + + this.push(chunk); + done(); + } + + _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(); + } +} + +module.exports = LastNewline; diff --git a/node_modules/nodemailer/lib/mime-node/le-unix.js b/node_modules/nodemailer/lib/mime-node/le-unix.js new file mode 100644 index 0000000..5feacd3 --- /dev/null +++ b/node_modules/nodemailer/lib/mime-node/le-unix.js @@ -0,0 +1,43 @@ +'use strict'; + +const stream = require('stream'); +const Transform = stream.Transform; + +/** + * Ensures that only is used for linebreaks + * + * @param {Object} options Stream options + */ +class LeWindows extends Transform { + constructor(options) { + super(options); + // init Transform + this.options = options || {}; + } + + /** + * Escapes dots + */ + _transform(chunk, encoding, done) { + let buf; + let lastPos = 0; + + for (let i = 0, len = chunk.length; i < len; i++) { + if (chunk[i] === 0x0d) { + // \n + 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(); + } +} + +module.exports = LeWindows; diff --git a/node_modules/nodemailer/lib/mime-node/le-windows.js b/node_modules/nodemailer/lib/mime-node/le-windows.js new file mode 100644 index 0000000..b156a7c --- /dev/null +++ b/node_modules/nodemailer/lib/mime-node/le-windows.js @@ -0,0 +1,52 @@ +'use strict'; + +const stream = require('stream'); +const Transform = stream.Transform; + +/** + * Ensures that only sequences are used for linebreaks + * + * @param {Object} options Stream options + */ +class LeWindows extends Transform { + constructor(options) { + super(options); + // init Transform + this.options = options || {}; + this.lastByte = false; + } + + /** + * Escapes dots + */ + _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(); + } +} + +module.exports = LeWindows; diff --git a/node_modules/nodemailer/lib/nodemailer.js b/node_modules/nodemailer/lib/nodemailer.js new file mode 100644 index 0000000..c763ed7 --- /dev/null +++ b/node_modules/nodemailer/lib/nodemailer.js @@ -0,0 +1,158 @@ +'use strict'; + +const Mailer = require('./mailer'); +const shared = require('./shared'); +const SMTPPool = require('./smtp-pool'); +const SMTPTransport = require('./smtp-transport'); +const SendmailTransport = require('./sendmail-transport'); +const StreamTransport = require('./stream-transport'); +const JSONTransport = require('./json-transport'); +const SESTransport = require('./ses-transport'); +const errors = require('./errors'); +const nmfetch = require('./fetch'); +const packageData = require('../package.json'); + +const ETHEREAL_API = (process.env.ETHEREAL_API || 'https://api.nodemailer.com').replace(/\/+$/, ''); +const ETHEREAL_WEB = (process.env.ETHEREAL_WEB || 'https://ethereal.email').replace(/\/+$/, ''); +const ETHEREAL_API_KEY = (process.env.ETHEREAL_API_KEY || '').replace(/\s*/g, '') || null; +const ETHEREAL_CACHE = ['true', 'yes', 'y', '1'].includes((process.env.ETHEREAL_CACHE || 'yes').toString().trim().toLowerCase()); + +let testAccount = false; + +module.exports.createTransport = function (transporter, defaults) { + let urlConfig; + let options; + let mailer; + + 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)) + ) { + if ((urlConfig = typeof transporter === 'string' ? transporter : transporter.url)) { + // parse a configuration URL into configuration options + options = shared.parseConnectionUrl(urlConfig); + } else { + options = transporter; + } + + if (options.pool) { + transporter = new SMTPPool(options); + } else if (options.sendmail) { + transporter = new SendmailTransport(options); + } else if (options.streamTransport) { + transporter = new StreamTransport(options); + } else if (options.jsonTransport) { + transporter = new JSONTransport(options); + } else if (options.SES) { + if (options.SES.ses && options.SES.aws) { + let 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 SESTransport(options); + } else { + transporter = new SMTPTransport(options); + } + } + + mailer = new Mailer(transporter, options, defaults); + + return mailer; +}; + +module.exports.createTestAccount = function (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); + }); + } + + if (ETHEREAL_CACHE && testAccount) { + setImmediate(() => callback(null, testAccount)); + return promise; + } + + apiUrl = apiUrl || ETHEREAL_API; + + let chunks = []; + let chunklen = 0; + + let requestHeaders = {}; + let requestBody = { + requestor: packageData.name, + version: packageData.version + }; + + if (ETHEREAL_API_KEY) { + requestHeaders.Authorization = 'Bearer ' + ETHEREAL_API_KEY; + } + + let req = nmfetch(apiUrl + '/user', { + contentType: 'application/json', + method: 'POST', + headers: requestHeaders, + body: Buffer.from(JSON.stringify(requestBody)) + }); + + req.on('readable', () => { + let chunk; + while ((chunk = req.read()) !== null) { + chunks.push(chunk); + chunklen += chunk.length; + } + }); + + req.once('error', err => callback(err)); + + req.once('end', () => { + let res = Buffer.concat(chunks, chunklen); + let data; + let err; + try { + data = JSON.parse(res.toString()); + } catch (E) { + err = E; + } + if (err) { + return callback(err); + } + if (data.status !== 'success' || data.error) { + return callback(new Error(data.error || 'Request failed')); + } + delete data.status; + testAccount = data; + callback(null, testAccount); + }); + + return promise; +}; + +module.exports.getTestMessageUrl = function (info) { + if (!info || !info.response) { + return false; + } + + let infoProps = new Map(); + info.response.replace(/\[([^\]]+)\]$/, (m, props) => { + props.replace(/\b([A-Z0-9]+)=([^\s]+)/g, (m, key, value) => { + infoProps.set(key, value); + }); + }); + + if (infoProps.has('STATUS') && infoProps.has('MSGID')) { + return (testAccount.web || ETHEREAL_WEB) + '/message/' + infoProps.get('MSGID'); + } + + return false; +}; diff --git a/node_modules/nodemailer/lib/punycode/index.js b/node_modules/nodemailer/lib/punycode/index.js new file mode 100644 index 0000000..cd3e494 --- /dev/null +++ b/node_modules/nodemailer/lib/punycode/index.js @@ -0,0 +1,460 @@ +/* + +Copied from https://github.com/mathiasbynens/punycode.js/blob/ef3505c8abb5143a00d53ce59077c9f7f4b2ac47/punycode.js + +Copyright Mathias Bynens + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +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'; + +/** Highest positive signed 32-bit float value */ +const maxInt = 2147483647; // aka. 0x7FFFFFFF or 2^31-1 + +/** Bootstring parameters */ +const base = 36; +const tMin = 1; +const tMax = 26; +const skew = 38; +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. + */ +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 + * item. + * @returns {Array} A new array of values returned by the callback function. + */ +function map(array, callback) { + const result = []; + let length = array.length; + while (length--) { + result[length] = callback(array[length]); + } + 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 + * character. + * @returns {String} A new string of characters returned by the callback + * function. + */ +function mapDomain(domain, callback) { + const parts = domain.split('@'); + let result = ''; + if (parts.length > 1) { + // In email addresses, only the domain name should be punycoded. Leave + // the local part (i.e. everything up to `@`) intact. + result = parts[0] + '@'; + domain = parts[1]; + } + // Avoid `split(regex)` for IE8 compatibility. See #17. + domain = domain.replace(regexSeparators, '\x2E'); + const labels = domain.split('.'); + 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, + * 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 + * @memberOf punycode.ucs2 + * @name decode + * @param {String} string The Unicode input string (UCS-2). + * @returns {Array} The new array of code points. + */ +function ucs2decode(string) { + const output = []; + let counter = 0; + const length = string.length; + while (counter < length) { + const value = string.charCodeAt(counter++); + if (value >= 0xd800 && value <= 0xdbff && counter < length) { + // It's a high surrogate, and there is a next character. + const extra = string.charCodeAt(counter++); + if ((extra & 0xfc00) == 0xdc00) { + // Low surrogate. + output.push(((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000); + } 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 { + 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). + */ +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 + * representing integers) in the range `0` to `base - 1`, or `base` if + * the code point does not represent a value. + */ +const basicToDigit = function (codePoint) { + if (codePoint >= 0x30 && codePoint < 0x3a) { + return 26 + (codePoint - 0x30); + } + if (codePoint >= 0x41 && codePoint < 0x5b) { + return codePoint - 0x41; + } + if (codePoint >= 0x61 && codePoint < 0x7b) { + return codePoint - 0x61; + } + 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 + * 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 + * if `flag` is non-zero and `digit` has no uppercase form. + */ +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); +}; + +/** + * Bias adaptation function as per section 3.4 of RFC 3492. + * https://tools.ietf.org/html/rfc3492#section-3.4 + * @private + */ +const adapt = function (delta, numPoints, firstTime) { + let k = 0; + delta = firstTime ? floor(delta / damp) : delta >> 1; + delta += floor(delta / numPoints); + for (; /* no initialization */ delta > (baseMinusTMin * tMax) >> 1; k += base) { + delta = floor(delta / baseMinusTMin); + } + 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. + */ +const decode = function (input) { + // Don't use UCS-2. + const output = []; + const inputLength = input.length; + 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) { + error('not-basic'); + } + 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`, + // which gets added to `i`. The overflow checking is easier + // 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) { + 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); +}; + +/** + * 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. + */ +const encode = function (input) { + const output = []; + + // Convert the input in UCS-2 to an array of Unicode code points. + input = ucs2decode(input); + + // Cache the length. + const inputLength = input.length; + + // Initialize the state. + let n = initialN; + let delta = 0; + let bias = initialBias; + + // Handle the basic code points. + for (const currentValue of input) { + 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) { + if (currentValue >= n && currentValue < m) { + m = currentValue; + } + } + + // Increase `delta` enough to advance the decoder's state to , + // 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) { + 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) { + const t = k <= bias ? tMin : k >= bias + tMax ? tMax : k - bias; + if (q < t) { + break; + } + const qMinusT = q - t; + const baseMinusT = base - t; + 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(''); +}; + +/** + * 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 + * convert to Unicode. + * @returns {String} The Unicode representation of the given Punycode + * string. + */ +const toUnicode = function (input) { + return mapDomain(input, function (string) { + return regexPunycode.test(string) ? decode(string.slice(4).toLowerCase()) : 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 {String} input The domain name or email address to convert, as a + * Unicode string. + * @returns {String} The Punycode representation of the given domain name or + * email address. + */ +const toASCII = function (input) { + return mapDomain(input, function (string) { + return regexNonASCII.test(string) ? 'xn--' + encode(string) : string; + }); +}; + +/*--------------------------------------------------------------------------*/ + +/** 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 + * @memberOf punycode + * @type Object + */ + ucs2: { + decode: ucs2decode, + encode: ucs2encode + }, + decode: decode, + encode: encode, + toASCII: toASCII, + toUnicode: toUnicode +}; + +module.exports = punycode; diff --git a/node_modules/nodemailer/lib/qp/index.js b/node_modules/nodemailer/lib/qp/index.js new file mode 100644 index 0000000..c6a7add --- /dev/null +++ b/node_modules/nodemailer/lib/qp/index.js @@ -0,0 +1,227 @@ +'use strict'; + +const Transform = require('stream').Transform; + +/** + * Encodes a Buffer into a Quoted-Printable encoded string + * + * @param {Buffer} buffer Buffer to convert + * @returns {String} Quoted-Printable encoded string + */ +function encode(buffer) { + if (typeof buffer === 'string') { + buffer = Buffer.from(buffer, 'utf-8'); + } + + // usable characters that do not need encoding + let ranges = [ + // https://tools.ietf.org/html/rfc2045#section-6.7 + [0x09], // + [0x0a], // + [0x0d], // + [0x20, 0x3c], // !"#$%&'()*+,-./0123456789:; + [0x3e, 0x7e] // >?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|} + ]; + let result = ''; + let ord; + + for (let i = 0, len = buffer.length; i < len; i++) { + ord = buffer[i]; + // if the char is in allowed range, then keep as is, unless it is a WS in the end of a line + if ( + checkRanges(ord, ranges) && + !((ord === 0x20 || ord === 0x09) && (i === len - 1 || buffer[i + 1] === 0x0a || buffer[i + 1] === 0x0d)) + ) { + result += String.fromCharCode(ord); + continue; + } + result += '=' + (ord < 0x10 ? '0' : '') + ord.toString(16).toUpperCase(); + } + + return result; +} + +/** + * Adds soft line breaks to a Quoted-Printable string + * + * @param {String} str Quoted-Printable encoded string that might need line wrapping + * @param {Number} [lineLength=76] Maximum allowed length for a line + * @returns {String} Soft-wrapped Quoted-Printable encoded string + */ +function wrap(str, lineLength) { + str = (str || '').toString(); + lineLength = lineLength || 76; + + if (str.length <= lineLength) { + return str; + } + + let pos = 0; + let len = str.length; + let match, code, line; + let 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') { + // nothing to change here + result += line; + pos += line.length; + continue; + } else 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; + } else 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 (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 {Number} nr Number to check for + * @param {Array} ranges An Array of allowed values + * @returns {Boolean} True if the value was found inside allowed ranges, false otherwise + */ +function checkRanges(nr, ranges) { + for (let i = ranges.length - 1; i >= 0; i--) { + if (!ranges[i].length) { + continue; + } + if (ranges[i].length === 1 && nr === ranges[i][0]) { + return true; + } + if (ranges[i].length === 2 && nr >= ranges[i][0] && nr <= ranges[i][1]) { + return true; + } + } + return false; +} + +/** + * Creates a transform stream for encoding data to Quoted-Printable encoding + * + * @constructor + * @param {Object} options Stream options + * @param {Number} [options.lineLength=76] Maximum length for lines, set to false to disable wrapping + */ +class Encoder extends Transform { + constructor(options) { + super(); + + // init Transform + this.options = options || {}; + + if (this.options.lineLength !== false) { + this.options.lineLength = this.options.lineLength || 76; + } + + this._curLine = ''; + + this.inputBytes = 0; + this.outputBytes = 0; + } + + _transform(chunk, encoding, done) { + let qp; + + if (encoding !== 'buffer') { + chunk = Buffer.from(chunk, encoding); + } + + if (!chunk || !chunk.length) { + return done(); + } + + this.inputBytes += chunk.length; + + if (this.options.lineLength) { + qp = this._curLine + encode(chunk); + qp = wrap(qp, this.options.lineLength); + qp = qp.replace(/(^|\n)([^\n]*)$/, (match, lineBreak, lastLine) => { + this._curLine = lastLine; + return lineBreak; + }); + + if (qp) { + this.outputBytes += qp.length; + this.push(qp); + } + } else { + qp = encode(chunk); + this.outputBytes += qp.length; + this.push(qp, 'ascii'); + } + + done(); + } + + _flush(done) { + if (this._curLine) { + this.outputBytes += this._curLine.length; + this.push(this._curLine, 'ascii'); + } + done(); + } +} + +// expose to the world +module.exports = { + encode, + wrap, + Encoder +}; diff --git a/node_modules/nodemailer/lib/sendmail-transport/index.js b/node_modules/nodemailer/lib/sendmail-transport/index.js new file mode 100644 index 0000000..7a2a9bc --- /dev/null +++ b/node_modules/nodemailer/lib/sendmail-transport/index.js @@ -0,0 +1,216 @@ +'use strict'; + +const spawn = require('child_process').spawn; +const packageData = require('../../package.json'); +const shared = require('../shared'); +const errors = require('../errors'); + +/** + * 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 {Object} optional config parameter for Sendmail + */ +class SendmailTransport { + constructor(options) { + options = options || {}; + + // use a reference to spawn for mocking purposes + this._spawn = spawn; + + this.options = options || {}; + + this.name = 'Sendmail'; + this.version = packageData.version; + + this.path = 'sendmail'; + this.args = false; + this.winbreak = false; + + this.logger = shared.getLogger(this.options, { + component: this.options.component || 'sendmail' + }); + + if (options) { + 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()); + } + } + } + + /** + *

Compiles a mailcomposer message and forwards it to handler that sends it.

+ * + * @param {Object} emailMessage MailComposer object + * @param {Function} callback Callback function to run when the sending is completed + */ + send(mail, done) { + // Sendmail strips this header line by itself + mail.message.keepBcc = true; + + let envelope = mail.data.envelope || mail.message.getEnvelope(); + let messageId = mail.message.messageId(); + let args; + let sendmail; + let returned; + + const hasInvalidAddresses = [] + .concat(envelope.from || []) + .concat(envelope.to || []) + .some(addr => /^-/.test(addr)); + if (hasInvalidAddresses) { + let err = new Error('Can not send mail. Invalid envelope addresses.'); + err.code = errors.ESENDMAIL; + return done(err); + } + + if (this.args) { + // force -i to keep single dots + args = ['-i'].concat(this.args).concat(envelope.to); + } else { + args = ['-i'].concat(envelope.from ? ['-f', envelope.from] : []).concat(envelope.to); + } + + let callback = err => { + if (returned) { + // ignore any additional responses, already done + return; + } + returned = true; + if (typeof done === 'function') { + if (err) { + return done(err); + } else { + return done(null, { + envelope: mail.data.envelope || mail.message.getEnvelope(), + messageId, + response: 'Messages queued for delivery' + }); + } + } + }; + + 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) { + sendmail.on('error', err => { + this.logger.error( + { + err, + tnx: 'spawn', + messageId + }, + 'Error occurred when sending message %s. %s', + messageId, + err.message + ); + callback(err); + }); + + sendmail.once('exit', code => { + if (!code) { + return callback(); + } + let err; + if (code === 127) { + err = new Error('Sendmail command not found, process exited with code ' + code); + } else { + err = new Error('Sendmail exited with code ' + code); + } + err.code = errors.ESENDMAIL; + + this.logger.error( + { + err, + tnx: 'stdin', + messageId + }, + 'Error sending message %s to sendmail. %s', + messageId, + err.message + ); + callback(err); + }); + sendmail.once('close', callback); + + sendmail.stdin.on('error', err => { + this.logger.error( + { + err, + tnx: 'stdin', + messageId + }, + 'Error occurred when piping message %s to sendmail. %s', + messageId, + err.message + ); + callback(err); + }); + + let 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(', ') + ); + + let sourceStream = mail.message.createReadStream(); + sourceStream.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); + }); + + sourceStream.pipe(sendmail.stdin); + } else { + let err = new Error('sendmail was not found'); + err.code = errors.ESENDMAIL; + return callback(err); + } + } +} + +module.exports = SendmailTransport; diff --git a/node_modules/nodemailer/lib/ses-transport/index.js b/node_modules/nodemailer/lib/ses-transport/index.js new file mode 100644 index 0000000..d2e2ccf --- /dev/null +++ b/node_modules/nodemailer/lib/ses-transport/index.js @@ -0,0 +1,234 @@ +'use strict'; + +const EventEmitter = require('events'); +const packageData = require('../../package.json'); +const shared = require('../shared'); +const LeWindows = require('../mime-node/le-windows'); +const MimeNode = require('../mime-node'); + +/** + * Generates a Transport object for AWS SES + * + * @constructor + * @param {Object} optional config parameter + */ +class SESTransport extends EventEmitter { + constructor(options) { + super(); + options = options || {}; + + 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') { + // promise + return this.ses.sesClient.config + .region() + .then(region => cb(null, region)) + .catch(err => cb(err)); + } + return cb(null, false); + } + + /** + * Compiles a mailcomposer message and forwards it to SES + * + * @param {Object} emailMessage MailComposer object + * @param {Function} callback Callback function to run when the sending is completed + */ + send(mail, callback) { + let statObject = { + ts: Date.now(), + pending: true + }; + + let fromHeader = mail.message._headers.find(header => /^from$/i.test(header.key)); + if (fromHeader) { + let mimeNode = new MimeNode('text/plain'); + fromHeader = mimeNode._convertAddresses(mimeNode._parseAddresses(fromHeader.value)); + } + + let envelope = mail.data.envelope || mail.message.getEnvelope(); + let messageId = mail.message.messageId(); + + let 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(', ') + ); + + let 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'; + } + + let sourceStream = mail.message.createReadStream(); + let stream = sourceStream.pipe(new LeWindows()); + let 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 + ); + statObject.pending = false; + return callback(err); + } + + let sesMessage = { + Content: { + Raw: { + // required + Data: raw // required + } + }, + FromEmailAddress: fromHeader ? fromHeader : envelope.from, + Destination: { + ToAddresses: envelope.to + } + }; + + Object.keys(mail.data.ses || {}).forEach(key => { + sesMessage[key] = mail.data.ses[key]; + }); + + this.getRegion((err, region) => { + if (err || !region) { + region = 'us-east-1'; + } + + const command = new this.ses.SendEmailCommand(sesMessage); + const sendPromise = this.ses.sesClient.send(command); + + sendPromise + .then(data => { + if (region === 'us-east-1') { + region = 'email'; + } + + statObject.pending = true; + callback(null, { + envelope: { + from: envelope.from, + to: envelope.to + }, + messageId: '<' + data.MessageId + (!/@/.test(data.MessageId) ? '@' + region + '.amazonses.com' : '') + '>', + response: data.MessageId, + raw + }); + }) + .catch(err => { + this.logger.error( + { + err, + tnx: 'send' + }, + 'Send error for %s: %s', + messageId, + err.message + ); + statObject.pending = false; + callback(err); + }); + }); + }) + ); + } + + /** + * Verifies SES configuration + * + * @param {Function} callback Callback function + */ + verify(callback) { + let promise; + if (!callback) { + promise = new Promise((resolve, reject) => { + callback = shared.callbackPromise(resolve, reject); + }); + } + + const cb = err => { + if (err && !['InvalidParameterValue', 'MessageRejected'].includes(err.code || err.Code || err.name)) { + return callback(err); + } + return callback(null, true); + }; + + const sesMessage = { + Content: { + Raw: { + Data: Buffer.from('From: \r\nTo: \r\n Subject: Invalid\r\n\r\nInvalid') + } + }, + FromEmailAddress: 'invalid@invalid', + Destination: { + ToAddresses: ['invalid@invalid'] + } + }; + + this.getRegion((err, region) => { + if (err || !region) { + region = 'us-east-1'; + } + + const command = new this.ses.SendEmailCommand(sesMessage); + const sendPromise = this.ses.sesClient.send(command); + + sendPromise.then(data => cb(null, data)).catch(err => cb(err)); + }); + + return promise; + } +} + +module.exports = SESTransport; diff --git a/node_modules/nodemailer/lib/shared/index.js b/node_modules/nodemailer/lib/shared/index.js new file mode 100644 index 0000000..c86dd8f --- /dev/null +++ b/node_modules/nodemailer/lib/shared/index.js @@ -0,0 +1,744 @@ +/* eslint no-console: 0 */ + +'use strict'; + +const urllib = require('url'); +const util = require('util'); +const fs = require('fs'); +const nmfetch = require('../fetch'); +const dns = require('dns'); +const net = require('net'); +const os = require('os'); + +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; +module.exports._lastCacheCleanup = () => lastCacheCleanup; +module.exports._resetCacheCleanup = () => { + lastCacheCleanup = 0; +}; + +let networkInterfaces; +try { + networkInterfaces = os.networkInterfaces(); +} catch (_err) { + // fails on some systems +} + +module.exports.networkInterfaces = networkInterfaces; + +const isFamilySupported = (family, allowInternal) => { + let networkInterfaces = module.exports.networkInterfaces; + if (!networkInterfaces) { + // hope for the best + return true; + } + + const familySupported = + // crux that replaces Object.values(networkInterfaces) as Object.values is not supported in nodejs v6 + Object.keys(networkInterfaces) + .map(key => networkInterfaces[key]) + // crux that replaces .flat() as it is not supported in older Node versions (v10 and older) + .reduce((acc, val) => acc.concat(val), []) + .filter(i => !i.internal || allowInternal) + .filter(i => i.family === 'IPv' + family || i.family === family).length > 0; + + return familySupported; +}; + +const resolver = (family, hostname, options, callback) => { + options = options || {}; + const familySupported = isFamilySupported(family, options.allowInternalNetworkInterfaces); + + if (!familySupported) { + return callback(null, []); + } + + const resolver = dns.Resolver ? new dns.Resolver(options) : dns; + resolver['resolve' + family](hostname, (err, addresses) => { + if (err) { + switch (err.code) { + case dns.NODATA: + case dns.NOTFOUND: + case dns.NOTIMP: + case dns.SERVFAIL: + case dns.CONNREFUSED: + case dns.REFUSED: + case 'EAI_AGAIN': + return callback(null, []); + } + return callback(err); + } + return callback(null, Array.isArray(addresses) ? addresses : [].concat(addresses || [])); + }); +}; + +const dnsCache = (module.exports.dnsCache = new Map()); + +const formatDNSValue = (value, extra) => { + if (!value) { + return Object.assign({}, extra || {}); + } + + let addresses = value.addresses || []; + + // Select a random address from available addresses, or null if none + let host = null; + if (addresses.length === 1) { + host = addresses[0]; + } else if (addresses.length > 1) { + host = addresses[Math.floor(Math.random() * addresses.length)]; + } + + return Object.assign( + { + servername: value.servername, + host, + // Include all addresses for connection fallback support + _addresses: addresses + }, + extra || {} + ); +}; + +module.exports.resolveHostname = (options, callback) => { + options = options || {}; + + if (!options.host && options.servername) { + options.host = options.servername; + } + + if (!options.host || net.isIP(options.host)) { + // nothing to do here + let value = { + addresses: [options.host], + servername: options.servername || false + }; + return callback( + null, + formatDNSValue(value, { + cached: false + }) + ); + } + + let cached; + if (dnsCache.has(options.host)) { + cached = 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 dnsCache.entries()) { + if (entry.expires && entry.expires < now) { + dnsCache.delete(host); + } + } + + // If cache is still too large, remove oldest entries + if (dnsCache.size > MAX_CACHE_SIZE) { + const toDelete = Math.floor(MAX_CACHE_SIZE * 0.1); // Remove 10% of entries + const keys = Array.from(dnsCache.keys()).slice(0, toDelete); + keys.forEach(key => dnsCache.delete(key)); + } + } + + if (!cached.expires || cached.expires >= now) { + return callback( + null, + formatDNSValue(cached.value, { + cached: true + }) + ); + } + } + + // Resolve both IPv4 and IPv6 addresses for fallback support + let ipv4Addresses = []; + let ipv6Addresses = []; + let ipv4Error = null; + let ipv6Error = null; + + resolver(4, options.host, options, (err, addresses) => { + if (err) { + ipv4Error = err; + } else { + ipv4Addresses = addresses || []; + } + + resolver(6, options.host, options, (err, addresses) => { + if (err) { + ipv6Error = err; + } else { + ipv6Addresses = addresses || []; + } + + // Combine addresses: IPv4 first, then IPv6 + let allAddresses = ipv4Addresses.concat(ipv6Addresses); + + if (allAddresses.length) { + let value = { + addresses: allAddresses, + servername: options.servername || options.host + }; + + dnsCache.set(options.host, { + value, + expires: Date.now() + (options.dnsTtl || DNS_TTL) + }); + + return callback( + null, + formatDNSValue(value, { + cached: false + }) + ); + } + + // No addresses from resolve4/resolve6, try dns.lookup as fallback + if (ipv4Error && ipv6Error) { + // Both resolvers had errors + if (cached) { + dnsCache.set(options.host, { + value: cached.value, + expires: Date.now() + (options.dnsTtl || DNS_TTL) + }); + + return callback( + null, + formatDNSValue(cached.value, { + cached: true, + error: ipv4Error + }) + ); + } + } + + try { + dns.lookup(options.host, { all: true }, (err, addresses) => { + if (err) { + if (cached) { + dnsCache.set(options.host, { + value: cached.value, + expires: Date.now() + (options.dnsTtl || DNS_TTL) + }); + + return callback( + null, + formatDNSValue(cached.value, { + cached: true, + error: err + }) + ); + } + return callback(err); + } + + // Get all supported addresses from dns.lookup + let supportedAddresses = addresses + ? addresses.filter(addr => isFamilySupported(addr.family)).map(addr => addr.address) + : []; + + if (addresses && addresses.length && !supportedAddresses.length) { + // there are addresses but none can be used + console.warn(`Failed to resolve IPv${addresses[0].family} addresses with current network`); + } + + if (!supportedAddresses.length && cached) { + // nothing was found, fallback to cached value + return callback( + null, + formatDNSValue(cached.value, { + cached: true + }) + ); + } + + let value = { + addresses: supportedAddresses.length ? supportedAddresses : [options.host], + servername: options.servername || options.host + }; + + dnsCache.set(options.host, { + value, + expires: Date.now() + (options.dnsTtl || DNS_TTL) + }); + + return callback( + null, + formatDNSValue(value, { + cached: false + }) + ); + }); + } catch (lookupErr) { + if (cached) { + dnsCache.set(options.host, { + value: cached.value, + expires: Date.now() + (options.dnsTtl || DNS_TTL) + }); + + return callback( + null, + formatDNSValue(cached.value, { + cached: true, + error: lookupErr + }) + ); + } + return callback(ipv4Error || ipv6Error || lookupErr); + } + }); + }); +}; +/** + * Parses connection url to a structured configuration object + * + * @param {String} str Connection url + * @return {Object} Configuration object + */ +module.exports.parseConnectionUrl = str => { + str = str || ''; + let options = {}; + + [urllib.parse(str, true)].forEach(url => { + let auth; + + 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.auth) { + auth = url.auth.split(':'); + + if (!options.auth) { + options.auth = {}; + } + + options.auth.user = auth.shift(); + options.auth.pass = auth.join(':'); + } + + 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; + } + + if (!(lKey in obj)) { + obj[lKey] = value; + } + }); + }); + + return options; +}; + +module.exports._logFunc = (logger, level, defaults, data, message, ...args) => { + let entry = {}; + + Object.keys(defaults || {}).forEach(key => { + if (key !== 'level') { + entry[key] = defaults[key]; + } + }); + + Object.keys(data || {}).forEach(key => { + if (key !== 'level') { + entry[key] = data[key]; + } + }); + + logger[level](entry, message, ...args); +}; + +/** + * Returns a bunyan-compatible logger interface. Uses either provided logger or + * creates a default console logger + * + * @param {Object} [options] Options object that might include 'logger' value + * @return {Object} bunyan compatible logger + */ +module.exports.getLogger = (options, defaults) => { + options = options || {}; + + let response = {}; + let levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal']; + + if (!options.logger) { + // use vanity logger + levels.forEach(level => { + response[level] = () => false; + }); + return response; + } + + let logger = options.logger; + + if (options.logger === true) { + // create console logger + logger = createDefaultLogger(levels); + } + + levels.forEach(level => { + response[level] = (data, message, ...args) => { + module.exports._logFunc(logger, level, defaults, data, message, ...args); + }; + }); + + return response; +}; + +/** + * Wrapper for creating a callback that either resolves or rejects a promise + * based on input + * + * @param {Function} resolve Function to run if callback is called + * @param {Function} reject Function to run if callback ends with an error + */ +module.exports.callbackPromise = (resolve, reject) => + function () { + let args = Array.from(arguments); + let err = args.shift(); + if (err) { + reject(err); + } else { + resolve(...args); + } + }; + +module.exports.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) { + 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 + }; +}; + +/** + * 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 {Object} data An object or an Array you want to resolve an element for + * @param {String|Number} key Property name or an Array index + * @param {Function} callback Callback function with (err, value) + */ +module.exports.resolveContent = (data, key, callback) => { + let promise; + + if (!callback) { + promise = new Promise((resolve, reject) => { + callback = module.exports.callbackPromise(resolve, reject); + }); + } + + let content = (data && data[key] && data[key].content) || data[key]; + let contentStream; + let 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 (/^https?:\/\//i.test(content.path || content.href)) { + contentStream = nmfetch(content.path || content.href); + return resolveStream(contentStream, callback); + } else if (/^data:/i.test(content.path || content.href)) { + let parsedDataUri = module.exports.parseDataURI(content.path || content.href); + + if (!parsedDataUri || !parsedDataUri.data) { + return callback(null, Buffer.from(0)); + } + return callback(null, parsedDataUri.data); + } else if (content.path) { + return resolveStream(fs.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)); + + return promise; +}; + +/** + * Copies properties from source objects to target objects + */ +module.exports.assign = function (/* target, ... sources */) { + let args = Array.from(arguments); + let target = args.shift() || {}; + + args.forEach(source => { + Object.keys(source || {}).forEach(key => { + 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 + if (!target[key]) { + // ensure that target has this key + target[key] = {}; + } + Object.keys(source[key]).forEach(subKey => { + target[key][subKey] = source[key][subKey]; + }); + } else { + target[key] = source[key]; + } + }); + }); + return target; +}; + +module.exports.encodeXText = str => { + // ! 0x21 + // + 0x2B + // = 0x3D + // ~ 0x7E + if (!/[^\x21-\x2A\x2C-\x3C\x3E-\x7E]/.test(str)) { + return str; + } + let buf = Buffer.from(str); + let result = ''; + for (let i = 0, len = buf.length; i < len; i++) { + let 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; +}; + +/** + * Streams a stream value into a Buffer + * + * @param {Object} stream Readable stream + * @param {Function} callback Callback function with (err, value) + */ +function resolveStream(stream, callback) { + let responded = false; + let chunks = []; + let chunklen = 0; + + stream.on('error', err => { + if (responded) { + return; + } + + responded = true; + callback(err); + }); + + stream.on('readable', () => { + let chunk; + while ((chunk = stream.read()) !== null) { + chunks.push(chunk); + chunklen += chunk.length; + } + }); + + stream.on('end', () => { + if (responded) { + return; + } + responded = true; + + let value; + + try { + value = Buffer.concat(chunks, chunklen); + } catch (E) { + return callback(E); + } + callback(null, value); + }); +} + +/** + * Generates a bunyan-like logger that prints to console + * + * @returns {Object} Bunyan logger instance + */ +function createDefaultLogger(levels) { + let levelMaxLen = 0; + let levelNames = new Map(); + levels.forEach(level => { + if (level.length > levelMaxLen) { + levelMaxLen = level.length; + } + }); + + levels.forEach(level => { + let levelName = level.toUpperCase(); + if (levelName.length < levelMaxLen) { + levelName += ' '.repeat(levelMaxLen - levelName.length); + } + levelNames.set(level, levelName); + }); + + let 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 = util.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); + }); + }; + + let logger = {}; + levels.forEach(level => { + logger[level] = print.bind(null, level); + }); + + return logger; +} diff --git a/node_modules/nodemailer/lib/smtp-connection/data-stream.js b/node_modules/nodemailer/lib/smtp-connection/data-stream.js new file mode 100644 index 0000000..5efa087 --- /dev/null +++ b/node_modules/nodemailer/lib/smtp-connection/data-stream.js @@ -0,0 +1,108 @@ +'use strict'; + +const stream = require('stream'); +const Transform = stream.Transform; + +/** + * Escapes dots in the beginning of lines. Ends the stream with . + * Also makes sure that only sequences are used for linebreaks + * + * @param {Object} options Stream options + */ +class DataStream extends Transform { + constructor(options) { + super(options); + // init Transform + this.options = options || {}; + this._curLine = ''; + + this.inByteCount = 0; + this.outByteCount = 0; + this.lastByte = false; + } + + /** + * Escapes dots + */ + _transform(chunk, encoding, done) { + let 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++) { + if (chunk[i] === 0x2e) { + // . + if ((i && chunk[i - 1] === 0x0a) || (!i && (!this.lastByte || this.lastByte === 0x0a))) { + buf = chunk.slice(lastPos, i + 1); + chunks.push(buf); + chunks.push(Buffer.from('.')); + chunklen += buf.length + 1; + lastPos = i + 1; + } + } else if (chunk[i] === 0x0a) { + // . + if ((i && chunk[i - 1] !== 0x0d) || (!i && this.lastByte !== 0x0d)) { + if (i > lastPos) { + buf = chunk.slice(lastPos, i); + chunks.push(buf); + chunklen += buf.length + 2; + } else { + chunklen += 2; + } + chunks.push(Buffer.from('\r\n')); + lastPos = i + 1; + } + } + } + + if (chunklen) { + // 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 + */ + _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(); + } +} + +module.exports = DataStream; diff --git a/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js b/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js new file mode 100644 index 0000000..64b0c08 --- /dev/null +++ b/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js @@ -0,0 +1,146 @@ +'use strict'; + +/** + * Minimal HTTP/S proxy client + */ + +const net = require('net'); +const tls = require('tls'); +const urllib = require('url'); +const errors = require('../errors'); + +/** + * 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 {String} proxyUrl proxy configuration, etg "http://proxy.host:3128/" + * @param {Number} destinationPort Port to open in destination host + * @param {String} destinationHost Destination hostname + * @param {Function} callback Callback to run with the rocket object once connection is established + */ +function httpProxyClient(proxyUrl, destinationPort, destinationHost, callback) { + let proxy = urllib.parse(proxyUrl); + + // create a socket connection to the proxy server + let options; + let connect; + let socket; + + options = { + host: proxy.hostname, + port: Number(proxy.port) ? Number(proxy.port) : proxy.protocol === 'https:' ? 443 : 80 + }; + + if (proxy.protocol === 'https:') { + // we can use untrusted proxies as long as we verify actual SMTP certificates + options.rejectUnauthorized = false; + connect = tls.connect.bind(tls); + } else { + connect = net.connect.bind(net); + } + + // Error harness for initial connection. Once connection is established, the responsibility + // to handle errors is passed to whoever uses this socket + let finished = false; + let tempSocketErr = err => { + if (finished) { + return; + } + finished = true; + try { + socket.destroy(); + } catch (_E) { + // ignore + } + callback(err); + }; + + let timeoutErr = () => { + let err = new Error('Proxy socket timed out'); + err.code = 'ETIMEDOUT'; + tempSocketErr(err); + }; + + socket = connect(options, () => { + if (finished) { + return; + } + + let reqHeaders = { + Host: destinationHost + ':' + destinationPort, + Connection: 'close' + }; + if (proxy.auth) { + reqHeaders['Proxy-Authorization'] = 'Basic ' + Buffer.from(proxy.auth).toString('base64'); + } + + socket.write( + // HTTP method + 'CONNECT ' + + destinationHost + + ':' + + destinationPort + + ' 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' + ); + + let headers = ''; + let onSocketData = chunk => { + let match; + let remainder; + + if (finished) { + return; + } + + headers += chunk.toString('binary'); + if ((match = headers.match(/\r\n\r\n/))) { + socket.removeListener('data', onSocketData); + + remainder = headers.substr(match.index + match[0].length); + headers = headers.substr(0, match.index); + if (remainder) { + socket.unshift(Buffer.from(remainder, 'binary')); + } + + // proxy connection is now established + finished = true; + + // check response code + match = headers.match(/^HTTP\/\d+\.\d+ (\d+)/i); + if (!match || (match[1] || '').charAt(0) !== '2') { + try { + socket.destroy(); + } catch (_E) { + // ignore + } + let err = new Error('Invalid response from proxy' + ((match && ': ' + match[1]) || '')); + err.code = errors.EPROXY; + return callback(err); + } + + socket.removeListener('error', tempSocketErr); + socket.removeListener('timeout', timeoutErr); + socket.setTimeout(0); + + return callback(null, socket); + } + }; + socket.on('data', onSocketData); + }); + + socket.setTimeout(httpProxyClient.timeout || 30 * 1000); + socket.on('timeout', timeoutErr); + + socket.once('error', tempSocketErr); +} + +module.exports = httpProxyClient; diff --git a/node_modules/nodemailer/lib/smtp-connection/index.js b/node_modules/nodemailer/lib/smtp-connection/index.js new file mode 100644 index 0000000..20cad07 --- /dev/null +++ b/node_modules/nodemailer/lib/smtp-connection/index.js @@ -0,0 +1,1970 @@ +'use strict'; + +const packageInfo = require('../../package.json'); +const EventEmitter = require('events').EventEmitter; +const net = require('net'); +const tls = require('tls'); +const os = require('os'); +const crypto = require('crypto'); +const DataStream = require('./data-stream'); +const PassThrough = require('stream').PassThrough; +const shared = require('../shared'); + +// default timeout values in ms +const CONNECTION_TIMEOUT = 2 * 60 * 1000; // how much to wait for the connection to be established +const SOCKET_TIMEOUT = 10 * 60 * 1000; // how much to wait for socket inactivity before disconnecting the client +const GREETING_TIMEOUT = 30 * 1000; // how much to wait after connection is established but SMTP greeting is not receieved +const DNS_TIMEOUT = 30 * 1000; // how much to wait for resolveHostname +const TEARDOWN_NOOP = () => {}; // reusable no-op handler for absorbing errors during socket teardown + +/** + * 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 + * * **requireTLS** - forces the client to use STARTTLS + * * **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 10000) + * * **connectionTimeout** - how many milliseconds to wait for the connection to establish + * * **socketTimeout** - Time of inactivity until the connection is closed (defaults to 1 hour) + * * **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 {Object} [options] Option properties + */ +class SMTPConnection extends EventEmitter { + constructor(options) { + super(options); + + this.id = crypto.randomBytes(8).toString('base64').replace(/\W/g, ''); + this.stage = 'init'; + + this.options = options || {}; + + this.secureConnection = !!this.options.secure; + this.alreadySecured = !!this.options.secured; + + this.port = Number(this.options.port) || (this.secureConnection ? 465 : 587); + this.host = this.options.host || 'localhost'; + + this.servername = this.options.servername ? this.options.servername : !net.isIP(this.host) ? this.host : false; + + this.allowInternalNetworkInterfaces = this.options.allowInternalNetworkInterfaces || false; + + if (typeof this.options.secure === 'undefined' && this.port === 465) { + // if secure option is not set but port is 465, then default to secure + this.secureConnection = true; + } + + this.name = this.options.name || this._getHostname(); + + this.logger = shared.getLogger(this.options, { + component: this.options.component || 'smtp-connection', + sid: this.id + }); + + this.customAuth = new Map(); + Object.keys(this.options.customAuth || {}).forEach(key => { + let mapKey = (key || '').toString().trim().toUpperCase(); + if (!mapKey) { + return; + } + this.customAuth.set(mapKey, this.options.customAuth[key]); + }); + + /** + * Expose version nr, just for the reference + * @type {String} + */ + this.version = packageInfo.version; + + /** + * If true, then the user is authenticated + * @type {Boolean} + */ + this.authenticated = false; + + /** + * If set to true, this instance is no longer active + * @private + */ + this.destroyed = false; + + /** + * Defines if the current connection is secure or not. If not, + * STARTTLS can be used if available + * @private + */ + this.secure = !!this.secureConnection; + + /** + * Store incomplete messages coming from the server + * @private + */ + this._remainder = ''; + + /** + * Unprocessed responses from the server + * @type {Array} + */ + this._responseQueue = []; + + this.lastServerResponse = false; + + /** + * The socket connecting to the server + * @public + */ + this._socket = false; + + /** + * Lists supported auth mechanisms + * @private + */ + this._supportedAuth = []; + + /** + * Set to true, if EHLO response includes "AUTH". + * If false then authentication is not tried + */ + this.allowsAuth = false; + + /** + * Includes current envelope (from, to) + * @private + */ + this._envelope = false; + + /** + * Lists supported extensions + * @private + */ + this._supportedExtensions = []; + + /** + * Defines the maximum allowed size for a single message + * @private + */ + this._maxAllowedSize = 0; + + /** + * Function queue to run if a data chunk comes from the server + * @private + */ + this._responseActions = []; + this._recipientQueue = []; + + /** + * Timeout variable for waiting the greeting + * @private + */ + this._greetingTimeout = false; + + /** + * Timeout variable for waiting the connection to start + * @private + */ + this._connectionTimeout = false; + + /** + * If the socket is deemed already closed + * @private + */ + this._destroyed = false; + + /** + * If the socket is already being closed + * @private + */ + this._closing = false; + + /** + * Callbacks for socket's listeners + */ + this._onSocketData = chunk => this._onData(chunk); + this._onSocketError = error => this._onError(error, 'ESOCKET', false, 'CONN'); + this._onSocketClose = () => this._onClose(); + this._onSocketEnd = () => this._onEnd(); + this._onSocketTimeout = () => this._onTimeout(); + + /** + * Connection-phase error handler (supports fallback to alternative addresses) + */ + this._onConnectionSocketError = err => this._onConnectionError(err, 'ESOCKET'); + + /** + * Connection attempt counter for fallback race condition protection + * @private + */ + this._connectionAttemptId = 0; + } + + /** + * Creates a connection to a SMTP server and sets up connection + * listener + */ + connect(connectCallback) { + if (typeof connectCallback === 'function') { + this.once('connect', () => { + this.logger.debug( + { + tnx: 'smtp' + }, + 'SMTP handshake finished' + ); + connectCallback(); + }); + + const isDestroyedMessage = this._isDestroyedMessage('connect'); + if (isDestroyedMessage) { + return connectCallback(this._formatError(isDestroyedMessage, 'ECONNECTION', false, 'CONN')); + } + } + + let opts = { + port: this.port, + host: this.host, + allowInternalNetworkInterfaces: this.allowInternalNetworkInterfaces, + timeout: this.options.dnsTimeout || DNS_TIMEOUT + }; + + if (this.options.localAddress) { + opts.localAddress = this.options.localAddress; + } + + if (this.options.connection) { + // connection is already opened + this._socket = this.options.connection; + this._setupConnectionHandlers(); + + if (this.secureConnection && !this.alreadySecured) { + setImmediate(() => + this._upgradeConnection(err => { + if (err) { + this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'ETLS', false, 'CONN'); + return; + } + this._onConnect(); + }) + ); + } else { + setImmediate(() => this._onConnect()); + } + return; + } else if (this.options.socket) { + // socket object is set up but not yet connected + this._socket = this.options.socket; + return shared.resolveHostname(opts, (err, resolved) => { + if (err) { + return setImmediate(() => this._onError(err, 'EDNS', false, 'CONN')); + } + this.logger.debug( + { + tnx: 'dns', + source: opts.host, + resolved: resolved.host, + cached: !!resolved.cached + }, + 'Resolved %s as %s [cache %s]', + opts.host, + resolved.host, + resolved.cached ? 'hit' : 'miss' + ); + Object.keys(resolved).forEach(key => { + if (key.charAt(0) !== '_' && resolved[key]) { + opts[key] = resolved[key]; + } + }); + try { + this._socket.connect(this.port, this.host, () => { + this._socket.setKeepAlive(true); + this._onConnect(); + }); + this._setupConnectionHandlers(); + } catch (E) { + return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN')); + } + }); + } else if (this.secureConnection) { + // connect using tls + if (this.options.tls) { + Object.keys(this.options.tls).forEach(key => { + opts[key] = this.options.tls[key]; + }); + } + + // ensure servername for SNI + if (this.servername && !opts.servername) { + opts.servername = this.servername; + } + + return shared.resolveHostname(opts, (err, resolved) => { + if (err) { + return setImmediate(() => this._onError(err, 'EDNS', false, 'CONN')); + } + this.logger.debug( + { + tnx: 'dns', + source: opts.host, + resolved: resolved.host, + cached: !!resolved.cached + }, + 'Resolved %s as %s [cache %s]', + opts.host, + resolved.host, + resolved.cached ? 'hit' : 'miss' + ); + Object.keys(resolved).forEach(key => { + if (key.charAt(0) !== '_' && resolved[key]) { + opts[key] = resolved[key]; + } + }); + + // Store fallback addresses for retry on connection failure + this._fallbackAddresses = (resolved._addresses || []).filter(addr => addr !== opts.host); + this._connectOpts = Object.assign({}, opts); + + this._connectToHost(opts, true); + }); + } else { + // connect using plaintext + return shared.resolveHostname(opts, (err, resolved) => { + if (err) { + return setImmediate(() => this._onError(err, 'EDNS', false, 'CONN')); + } + this.logger.debug( + { + tnx: 'dns', + source: opts.host, + resolved: resolved.host, + cached: !!resolved.cached + }, + 'Resolved %s as %s [cache %s]', + opts.host, + resolved.host, + resolved.cached ? 'hit' : 'miss' + ); + Object.keys(resolved).forEach(key => { + if (key.charAt(0) !== '_' && resolved[key]) { + opts[key] = resolved[key]; + } + }); + + // Store fallback addresses for retry on connection failure + this._fallbackAddresses = (resolved._addresses || []).filter(addr => addr !== opts.host); + this._connectOpts = Object.assign({}, opts); + + this._connectToHost(opts, false); + }); + } + } + + /** + * Attempts to connect to the specified host address + * + * @param {Object} opts Connection options + * @param {Boolean} secure Whether to use TLS + */ + _connectToHost(opts, secure) { + this._connectionAttemptId++; + const currentAttemptId = this._connectionAttemptId; + + let connectFn = secure ? tls.connect : net.connect; + try { + this._socket = connectFn(opts, () => { + // Ignore callback if this is a stale connection attempt + if (this._connectionAttemptId !== currentAttemptId) { + return; + } + this._socket.setKeepAlive(true); + this._onConnect(); + }); + this._setupConnectionHandlers(); + } catch (E) { + return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN')); + } + } + + /** + * Sets up connection timeout and error handlers + */ + _setupConnectionHandlers() { + this._connectionTimeout = setTimeout(() => { + this._onConnectionError('Connection timeout', 'ETIMEDOUT'); + }, this.options.connectionTimeout || CONNECTION_TIMEOUT); + + this._socket.on('error', this._onConnectionSocketError); + } + + /** + * Handles connection errors with fallback to alternative addresses + * + * @param {Error|String} err Error object or message + * @param {String} code Error code + */ + _onConnectionError(err, code) { + clearTimeout(this._connectionTimeout); + + // Check if we have fallback addresses to try + let canFallback = this._fallbackAddresses && this._fallbackAddresses.length && this.stage === 'init' && !this._destroyed; + + if (!canFallback) { + // No more fallback addresses, report the error + this._onError(err, code, false, 'CONN'); + return; + } + + let nextHost = this._fallbackAddresses.shift(); + + this.logger.info( + { + tnx: 'network', + failedHost: this._connectOpts.host, + nextHost, + error: err.message || err + }, + 'Connection to %s failed, trying %s', + this._connectOpts.host, + nextHost + ); + + // Clean up current socket + if (this._socket) { + try { + this._socket.removeListener('error', this._onConnectionSocketError); + this._socket.destroy(); + } catch (_E) { + // ignore + } + this._socket = null; + } + + // Update host and retry + this._connectOpts.host = nextHost; + this._connectToHost(this._connectOpts, this.secureConnection); + } + + /** + * Sends QUIT + */ + quit() { + this._sendCommand('QUIT'); + this._responseActions.push(this.close); + } + + /** + * Closes the connection to the server + */ + close() { + clearTimeout(this._connectionTimeout); + clearTimeout(this._greetingTimeout); + this._responseActions = []; + + // allow to run this function only once + if (this._closing) { + return; + } + this._closing = true; + + let closeMethod = 'end'; + + if (this.stage === 'init') { + // Close the socket immediately when connection timed out + closeMethod = 'destroy'; + } + + this.logger.debug( + { + tnx: 'smtp' + }, + 'Closing connection to the server using "%s"', + closeMethod + ); + + let socket = (this._socket && this._socket.socket) || this._socket; + + if (socket && !socket.destroyed) { + try { + // Clear socket timeout to prevent timer leaks + socket.setTimeout(0); + // Remove all listeners to allow proper garbage collection + socket.removeListener('data', this._onSocketData); + socket.removeListener('timeout', this._onSocketTimeout); + socket.removeListener('close', this._onSocketClose); + socket.removeListener('end', this._onSocketEnd); + socket.removeListener('error', this._onSocketError); + socket.removeListener('error', this._onConnectionSocketError); + // Absorb errors that may fire during socket teardown (e.g. server + // sending cleartext after TLS shutdown triggers ERR_SSL_BAD_RECORD_TYPE) + socket.on('error', TEARDOWN_NOOP); + socket[closeMethod](); + } catch (_E) { + // just ignore + } + } + + this._destroy(); + } + + /** + * Authenticate user + */ + login(authData, callback) { + const isDestroyedMessage = this._isDestroyedMessage('login'); + if (isDestroyedMessage) { + return callback(this._formatError(isDestroyedMessage, 'ECONNECTION', false, 'API')); + } + + this._auth = authData || {}; + // Select SASL authentication method + this._authMethod = (this._auth.method || '').toString().trim().toUpperCase() || false; + + if (!this._authMethod && this._auth.oauth2 && !this._auth.credentials) { + this._authMethod = 'XOAUTH2'; + } else if (!this._authMethod || (this._authMethod === 'XOAUTH2' && !this._auth.oauth2)) { + // use first supported + this._authMethod = (this._supportedAuth[0] || 'PLAIN').toUpperCase().trim(); + } + + if (this._authMethod !== 'XOAUTH2' && (!this._auth.credentials || !this._auth.credentials.user || !this._auth.credentials.pass)) { + if ((this._auth.user && this._auth.pass) || this.customAuth.has(this._authMethod)) { + this._auth.credentials = { + user: this._auth.user, + pass: this._auth.pass, + options: this._auth.options + }; + } else { + return callback(this._formatError('Missing credentials for "' + this._authMethod + '"', 'EAUTH', false, 'API')); + } + } + + if (this.customAuth.has(this._authMethod)) { + let handler = this.customAuth.get(this._authMethod); + let lastResponse; + let returned = false; + + let resolve = () => { + if (returned) { + return; + } + returned = true; + this.logger.info( + { + tnx: 'smtp', + username: this._auth.user, + action: 'authenticated', + method: this._authMethod + }, + 'User %s authenticated', + JSON.stringify(this._auth.user) + ); + this.authenticated = true; + callback(null, true); + }; + + let reject = err => { + if (returned) { + return; + } + returned = true; + callback(this._formatError(err, 'EAUTH', lastResponse, 'AUTH ' + this._authMethod)); + }; + + let handlerResponse = handler({ + auth: this._auth, + method: this._authMethod, + + extensions: [].concat(this._supportedExtensions), + authMethods: [].concat(this._supportedAuth), + maxAllowedSize: this._maxAllowedSize || false, + + sendCommand: (cmd, done) => { + let promise; + + if (!done) { + promise = new Promise((resolve, reject) => { + done = shared.callbackPromise(resolve, reject); + }); + } + + this._responseActions.push(str => { + lastResponse = str; + + let codes = str.match(/^(\d+)(?:\s(\d+\.\d+\.\d+))?\s/); + let data = { + command: cmd, + response: str + }; + if (codes) { + data.status = Number(codes[1]) || 0; + if (codes[2]) { + data.code = codes[2]; + } + data.text = str.substr(codes[0].length); + } else { + data.text = str; + data.status = 0; // just in case we need to perform numeric comparisons + } + done(null, data); + }); + setImmediate(() => this._sendCommand(cmd)); + + return promise; + }, + + resolve, + reject + }); + + if (handlerResponse && typeof handlerResponse.catch === 'function') { + // a promise was returned + handlerResponse.then(resolve).catch(reject); + } + + return; + } + + switch (this._authMethod) { + case 'XOAUTH2': + this._handleXOauth2Token(false, callback); + return; + case 'LOGIN': + this._responseActions.push(str => { + this._actionAUTH_LOGIN_USER(str, callback); + }); + this._sendCommand('AUTH LOGIN'); + return; + case 'PLAIN': + this._responseActions.push(str => { + this._actionAUTHComplete(str, callback); + }); + this._sendCommand( + 'AUTH PLAIN ' + + Buffer.from( + //this._auth.user+'\u0000'+ + '\u0000' + // skip authorization identity as it causes problems with some servers + this._auth.credentials.user + + '\u0000' + + this._auth.credentials.pass, + 'utf-8' + ).toString('base64'), + // log entry without passwords + 'AUTH PLAIN ' + + Buffer.from( + //this._auth.user+'\u0000'+ + '\u0000' + // skip authorization identity as it causes problems with some servers + this._auth.credentials.user + + '\u0000' + + '/* secret */', + 'utf-8' + ).toString('base64') + ); + return; + case 'CRAM-MD5': + this._responseActions.push(str => { + this._actionAUTH_CRAM_MD5(str, callback); + }); + this._sendCommand('AUTH CRAM-MD5'); + return; + } + + return callback(this._formatError('Unknown authentication method "' + this._authMethod + '"', 'EAUTH', false, 'API')); + } + + /** + * Sends a message + * + * @param {Object} envelope Envelope object, {from: addr, to: [addr]} + * @param {Object} message String, Buffer or a Stream + * @param {Function} callback Callback to return once sending is completed + */ + send(envelope, message, done) { + if (!message) { + return done(this._formatError('Empty message', 'EMESSAGE', false, 'API')); + } + + const isDestroyedMessage = this._isDestroyedMessage('send message'); + if (isDestroyedMessage) { + return done(this._formatError(isDestroyedMessage, 'ECONNECTION', false, 'API')); + } + + // reject larger messages than allowed + if (this._maxAllowedSize && envelope.size > this._maxAllowedSize) { + return setImmediate(() => { + done(this._formatError('Message size larger than allowed ' + this._maxAllowedSize, 'EMESSAGE', false, 'MAIL FROM')); + }); + } + + // ensure that callback is only called once + let returned = false; + let callback = function () { + if (returned) { + return; + } + returned = true; + + done(...arguments); + }; + + if (typeof message.on === 'function') { + message.on('error', err => callback(this._formatError(err, 'ESTREAM', false, 'API'))); + } + + let startTime = Date.now(); + this._setEnvelope(envelope, (err, info) => { + if (err) { + // create passthrough stream to consume to prevent OOM + let stream = new PassThrough(); + if (typeof message.pipe === 'function') { + message.pipe(stream); + } else { + stream.write(message); + stream.end(); + } + + return callback(err); + } + let envelopeTime = Date.now(); + let stream = this._createSendStream((err, str) => { + if (err) { + return callback(err); + } + + info.envelopeTime = envelopeTime - startTime; + info.messageTime = Date.now() - envelopeTime; + info.messageSize = stream.outByteCount; + info.response = str; + + return callback(null, info); + }); + if (typeof message.pipe === 'function') { + message.pipe(stream); + } else { + stream.write(message); + stream.end(); + } + }); + } + + /** + * Resets connection state + * + * @param {Function} callback Callback to return once connection is reset + */ + reset(callback) { + this._sendCommand('RSET'); + this._responseActions.push(str => { + if (str.charAt(0) !== '2') { + return callback(this._formatError('Could not reset session state. response=' + str, 'EPROTOCOL', str, 'RSET')); + } + this._envelope = false; + return callback(null, true); + }); + } + + /** + * Connection listener that is run when the connection to + * the server is opened + * + * @event + */ + _onConnect() { + clearTimeout(this._connectionTimeout); + + this.logger.info( + { + tnx: 'network', + localAddress: this._socket.localAddress, + localPort: this._socket.localPort, + remoteAddress: this._socket.remoteAddress, + remotePort: this._socket.remotePort + }, + '%s established to %s:%s', + this.secure ? 'Secure connection' : 'Connection', + this._socket.remoteAddress, + this._socket.remotePort + ); + + if (this._destroyed) { + // Connection was established after we already had canceled it + this.close(); + return; + } + + this.stage = 'connected'; + + // clear existing listeners for the socket + this._socket.removeListener('data', this._onSocketData); + this._socket.removeListener('timeout', this._onSocketTimeout); + this._socket.removeListener('close', this._onSocketClose); + this._socket.removeListener('end', this._onSocketEnd); + // Switch from connection-phase error handler to normal error handler + this._socket.removeListener('error', this._onConnectionSocketError); + + this._socket.on('error', this._onSocketError); + this._socket.on('data', this._onSocketData); + this._socket.once('close', this._onSocketClose); + this._socket.once('end', this._onSocketEnd); + + this._socket.setTimeout(this.options.socketTimeout || SOCKET_TIMEOUT); + this._socket.on('timeout', this._onSocketTimeout); + + this._greetingTimeout = setTimeout(() => { + // if still waiting for greeting, give up + if (this._socket && !this._destroyed && this._responseActions[0] === this._actionGreeting) { + this._onError('Greeting never received', 'ETIMEDOUT', false, 'CONN'); + } + }, this.options.greetingTimeout || GREETING_TIMEOUT); + + this._responseActions.push(this._actionGreeting); + + // we have a 'data' listener set up so resume socket if it was paused + this._socket.resume(); + } + + /** + * 'data' listener for data coming from the server + * + * @event + * @param {Buffer} chunk Data chunk coming from the server + */ + _onData(chunk) { + if (this._destroyed || !chunk || !chunk.length) { + return; + } + + let data = (chunk || '').toString('binary'); + let lines = (this._remainder + data).split(/\r?\n/); + let lastline; + + this._remainder = lines.pop(); + + for (let i = 0, len = lines.length; i < len; i++) { + if (this._responseQueue.length) { + lastline = this._responseQueue[this._responseQueue.length - 1]; + if (/^\d+-/.test(lastline.split('\n').pop())) { + this._responseQueue[this._responseQueue.length - 1] += '\n' + lines[i]; + continue; + } + } + this._responseQueue.push(lines[i]); + } + + if (this._responseQueue.length) { + lastline = this._responseQueue[this._responseQueue.length - 1]; + if (/^\d+-/.test(lastline.split('\n').pop())) { + return; + } + } + + this._processResponse(); + } + + /** + * 'error' listener for the socket + * + * @event + * @param {Error} err Error object + * @param {String} type Error name + */ + _onError(err, type, data, command) { + clearTimeout(this._connectionTimeout); + clearTimeout(this._greetingTimeout); + + if (this._destroyed) { + // just ignore, already closed + // this might happen when a socket is canceled because of reached timeout + // but the socket timeout error itself receives only after + return; + } + + err = this._formatError(err, type, data, command); + + const transientCodes = ['ETIMEDOUT', 'ESOCKET', 'ECONNECTION']; + if (transientCodes.includes(err.code)) { + this.logger.warn(data, err.message); + } else { + this.logger.error(data, err.message); + } + + this.emit('error', err); + this.close(); + } + + _formatError(message, type, response, command) { + let err; + + if (/Error\]$/i.test(Object.prototype.toString.call(message))) { + err = message; + } else { + err = new Error(message); + } + + if (type && type !== 'Error') { + err.code = type; + } + + if (response) { + err.response = response; + err.message += ': ' + response; + } + + let responseCode = (typeof response === 'string' && Number((response.match(/^\d+/) || [])[0])) || false; + if (responseCode) { + err.responseCode = responseCode; + } + + if (command) { + err.command = command; + } + + return err; + } + + /** + * 'close' listener for the socket + * + * @event + */ + _onClose() { + let serverResponse = false; + + if (this._remainder && this._remainder.trim()) { + if (this.options.debug || this.options.transactionLog) { + this.logger.debug( + { + tnx: 'server' + }, + this._remainder.replace(/\r?\n$/, '') + ); + } + this.lastServerResponse = serverResponse = this._remainder.trim(); + } + + this.logger.info( + { + tnx: 'network' + }, + 'Connection closed' + ); + + if (this.upgrading && !this._destroyed) { + return this._onError(new Error('Connection closed unexpectedly'), 'ETLS', serverResponse, 'CONN'); + } else if (![this._actionGreeting, this.close].includes(this._responseActions[0]) && !this._destroyed) { + return this._onError(new Error('Connection closed unexpectedly'), 'ECONNECTION', serverResponse, 'CONN'); + } else if (/^[45]\d{2}\b/.test(serverResponse)) { + return this._onError(new Error('Connection closed unexpectedly'), 'ECONNECTION', serverResponse, 'CONN'); + } + + this._destroy(); + } + + /** + * 'end' listener for the socket + * + * @event + */ + _onEnd() { + if (this._socket && !this._socket.destroyed) { + this._socket.destroy(); + } + } + + /** + * 'timeout' listener for the socket + * + * @event + */ + _onTimeout() { + return this._onError(new Error('Timeout'), 'ETIMEDOUT', false, 'CONN'); + } + + /** + * Destroys the client, emits 'end' + */ + _destroy() { + if (this._destroyed) { + return; + } + this._destroyed = true; + this.emit('end'); + } + + /** + * Upgrades the connection to TLS + * + * @param {Function} callback Callback function to run when the connection + * has been secured + */ + _upgradeConnection(callback) { + // do not remove all listeners or it breaks node v0.10 as there's + // apparently a 'finish' event set that would be cleared as well + + // we can safely keep 'error', 'end', 'close' etc. events + this._socket.removeListener('data', this._onSocketData); // incoming data is going to be gibberish from this point onwards + this._socket.removeListener('timeout', this._onSocketTimeout); // timeout will be re-set for the new socket object + + let socketPlain = this._socket; + let opts = { + socket: this._socket, + host: this.host + }; + + Object.keys(this.options.tls || {}).forEach(key => { + opts[key] = this.options.tls[key]; + }); + + // ensure servername for SNI + if (this.servername && !opts.servername) { + opts.servername = this.servername; + } + + this.upgrading = true; + // tls.connect is not an asynchronous function however it may still throw errors and requires to be wrapped with try/catch + try { + this._socket = tls.connect(opts, () => { + this.secure = true; + this.upgrading = false; + this._socket.on('data', this._onSocketData); + + // Remove all listeners from the plain socket to allow proper garbage collection + socketPlain.removeListener('close', this._onSocketClose); + socketPlain.removeListener('end', this._onSocketEnd); + socketPlain.removeListener('error', this._onSocketError); + + return callback(null, true); + }); + } catch (err) { + return callback(err); + } + + this._socket.on('error', this._onSocketError); + this._socket.once('close', this._onSocketClose); + this._socket.once('end', this._onSocketEnd); + + this._socket.setTimeout(this.options.socketTimeout || SOCKET_TIMEOUT); // 10 min. + this._socket.on('timeout', this._onSocketTimeout); + + // resume in case the socket was paused + socketPlain.resume(); + } + + /** + * Processes queued responses from the server + * + * @param {Boolean} force If true, ignores _processing flag + */ + _processResponse() { + if (!this._responseQueue.length) { + return false; + } + + let str = (this.lastServerResponse = (this._responseQueue.shift() || '').toString()); + + if (/^\d+-/.test(str.split('\n').pop())) { + // keep waiting for the final part of multiline response + return; + } + + if (this.options.debug || this.options.transactionLog) { + this.logger.debug( + { + tnx: 'server' + }, + str.replace(/\r?\n$/, '') + ); + } + + if (!str.trim()) { + // skip unexpected empty lines + setImmediate(() => this._processResponse()); + } + + let action = this._responseActions.shift(); + + if (typeof action === 'function') { + action.call(this, str); + setImmediate(() => this._processResponse()); + } else { + return this._onError(new Error('Unexpected Response'), 'EPROTOCOL', str, 'CONN'); + } + } + + /** + * Send a command to the server, append \r\n + * + * @param {String} str String to be sent to the server + * @param {String} logStr Optional string to be used for logging instead of the actual string + */ + _sendCommand(str, logStr) { + if (this._destroyed) { + // Connection already closed, can't send any more data + return; + } + + if (this._socket.destroyed) { + return this.close(); + } + + if (this.options.debug || this.options.transactionLog) { + this.logger.debug( + { + tnx: 'client' + }, + (logStr || str || '').toString().replace(/\r?\n$/, '') + ); + } + + this._socket.write(Buffer.from(str + '\r\n', 'utf-8')); + } + + /** + * Initiates a new message by submitting envelope data, starting with + * MAIL FROM: command + * + * @param {Object} envelope Envelope object in the form of + * {from:'...', to:['...']} + * or + * {from:{address:'...',name:'...'}, to:[address:'...',name:'...']} + */ + _setEnvelope(envelope, callback) { + let args = []; + let useSmtpUtf8 = false; + + this._envelope = envelope || {}; + this._envelope.from = ((this._envelope.from && this._envelope.from.address) || this._envelope.from || '').toString().trim(); + + this._envelope.to = [].concat(this._envelope.to || []).map(to => ((to && to.address) || to || '').toString().trim()); + + if (!this._envelope.to.length) { + return callback(this._formatError('No recipients defined', 'EENVELOPE', false, 'API')); + } + + if (this._envelope.from && /[\r\n<>]/.test(this._envelope.from)) { + return callback(this._formatError('Invalid sender ' + JSON.stringify(this._envelope.from), 'EENVELOPE', false, 'API')); + } + + // check if the sender address uses only ASCII characters, + // otherwise require usage of SMTPUTF8 extension + if (/[\x80-\uFFFF]/.test(this._envelope.from)) { + useSmtpUtf8 = true; + } + + for (let i = 0, len = this._envelope.to.length; i < len; i++) { + if (!this._envelope.to[i] || /[\r\n<>]/.test(this._envelope.to[i])) { + return callback(this._formatError('Invalid recipient ' + JSON.stringify(this._envelope.to[i]), 'EENVELOPE', false, 'API')); + } + + // check if the recipients addresses use only ASCII characters, + // otherwise require usage of SMTPUTF8 extension + if (/[\x80-\uFFFF]/.test(this._envelope.to[i])) { + useSmtpUtf8 = true; + } + } + + // clone the recipients array for latter manipulation + this._envelope.rcptQueue = JSON.parse(JSON.stringify(this._envelope.to || [])); + this._envelope.rejected = []; + this._envelope.rejectedErrors = []; + this._envelope.accepted = []; + + if (this._envelope.dsn) { + try { + this._envelope.dsn = this._setDsnEnvelope(this._envelope.dsn); + } catch (err) { + return callback(this._formatError('Invalid DSN ' + err.message, 'EENVELOPE', false, 'API')); + } + } + + this._responseActions.push(str => { + this._actionMAIL(str, callback); + }); + + // If the server supports SMTPUTF8 and the envelope includes an internationalized + // email address then append SMTPUTF8 keyword to the MAIL FROM command + if (useSmtpUtf8 && this._supportedExtensions.includes('SMTPUTF8')) { + args.push('SMTPUTF8'); + this._usingSmtpUtf8 = true; + } + + // If the server supports 8BITMIME and the message might contain non-ascii bytes + // then append the 8BITMIME keyword to the MAIL FROM command + if (this._envelope.use8BitMime && this._supportedExtensions.includes('8BITMIME')) { + args.push('BODY=8BITMIME'); + this._using8BitMime = true; + } + + if (this._envelope.size && this._supportedExtensions.includes('SIZE')) { + args.push('SIZE=' + this._envelope.size); + } + + // If the server supports DSN and the envelope includes an DSN prop + // then append DSN params to the MAIL FROM command + if (this._envelope.dsn && this._supportedExtensions.includes('DSN')) { + if (this._envelope.dsn.ret) { + args.push('RET=' + shared.encodeXText(this._envelope.dsn.ret)); + } + if (this._envelope.dsn.envid) { + args.push('ENVID=' + shared.encodeXText(this._envelope.dsn.envid)); + } + } + + // RFC 8689: If the envelope requests REQUIRETLS extension + // then append REQUIRETLS keyword to the MAIL FROM command + // Note: REQUIRETLS can only be used over TLS connections and requires server support + if (this._envelope.requireTLSExtensionEnabled) { + if (!this.secure) { + return callback( + this._formatError('REQUIRETLS can only be used over TLS connections (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM') + ); + } + if (!this._supportedExtensions.includes('REQUIRETLS')) { + return callback( + this._formatError('Server does not support REQUIRETLS extension (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM') + ); + } + args.push('REQUIRETLS'); + } + + this._sendCommand('MAIL FROM:<' + this._envelope.from + '>' + (args.length ? ' ' + args.join(' ') : '')); + } + + _setDsnEnvelope(params) { + let ret = (params.ret || params.return || '').toString().toUpperCase() || null; + if (ret) { + switch (ret) { + case 'HDRS': + case 'HEADERS': + ret = 'HDRS'; + break; + case 'FULL': + case 'BODY': + ret = 'FULL'; + break; + } + } + + if (ret && !['FULL', 'HDRS'].includes(ret)) { + throw new Error('ret: ' + JSON.stringify(ret)); + } + + let envid = (params.envid || params.id || '').toString() || null; + + let notify = params.notify || null; + if (notify) { + if (typeof notify === 'string') { + notify = notify.split(','); + } + notify = notify.map(n => n.trim().toUpperCase()); + let validNotify = ['NEVER', 'SUCCESS', 'FAILURE', 'DELAY']; + let invalidNotify = notify.filter(n => !validNotify.includes(n)); + if (invalidNotify.length || (notify.length > 1 && notify.includes('NEVER'))) { + throw new Error('notify: ' + JSON.stringify(notify.join(','))); + } + notify = notify.join(','); + } + + let orcpt = (params.recipient || params.orcpt || '').toString() || null; + if (orcpt && orcpt.indexOf(';') < 0) { + orcpt = 'rfc822;' + orcpt; + } + + return { + ret, + envid, + notify, + orcpt + }; + } + + _getDsnRcptToArgs() { + let args = []; + // If the server supports DSN and the envelope includes an DSN prop + // then append DSN params to the RCPT TO command + if (this._envelope.dsn && this._supportedExtensions.includes('DSN')) { + if (this._envelope.dsn.notify) { + args.push('NOTIFY=' + shared.encodeXText(this._envelope.dsn.notify)); + } + if (this._envelope.dsn.orcpt) { + args.push('ORCPT=' + shared.encodeXText(this._envelope.dsn.orcpt)); + } + } + return args.length ? ' ' + args.join(' ') : ''; + } + + _createSendStream(callback) { + let dataStream = new DataStream(); + let logStream; + + if (this.options.lmtp) { + this._envelope.accepted.forEach((recipient, i) => { + let final = i === this._envelope.accepted.length - 1; + this._responseActions.push(str => { + this._actionLMTPStream(recipient, final, str, callback); + }); + }); + } else { + this._responseActions.push(str => { + this._actionSMTPStream(str, callback); + }); + } + + dataStream.pipe(this._socket, { + end: false + }); + + if (this.options.debug) { + logStream = new PassThrough(); + logStream.on('readable', () => { + let chunk; + while ((chunk = logStream.read())) { + this.logger.debug( + { + tnx: 'message' + }, + chunk.toString('binary').replace(/\r?\n$/, '') + ); + } + }); + dataStream.pipe(logStream); + } + + dataStream.once('end', () => { + this.logger.info( + { + tnx: 'message', + inByteCount: dataStream.inByteCount, + outByteCount: dataStream.outByteCount + }, + '<%s bytes encoded mime message (source size %s bytes)>', + dataStream.outByteCount, + dataStream.inByteCount + ); + }); + + return dataStream; + } + + /** ACTIONS **/ + + /** + * Will be run after the connection is created and the server sends + * a greeting. If the incoming message starts with 220 initiate + * SMTP session by sending EHLO command + * + * @param {String} str Message from the server + */ + _actionGreeting(str) { + clearTimeout(this._greetingTimeout); + + if (str.substr(0, 3) !== '220') { + this._onError(new Error('Invalid greeting. response=' + str), 'EPROTOCOL', str, 'CONN'); + return; + } + + if (this.options.lmtp) { + this._responseActions.push(this._actionLHLO); + this._sendCommand('LHLO ' + this.name); + } else { + this._responseActions.push(this._actionEHLO); + this._sendCommand('EHLO ' + this.name); + } + } + + /** + * Handles server response for LHLO command. If it yielded in + * error, emit 'error', otherwise treat this as an EHLO response + * + * @param {String} str Message from the server + */ + _actionLHLO(str) { + if (str.charAt(0) !== '2') { + this._onError(new Error('Invalid LHLO. response=' + str), 'EPROTOCOL', str, 'LHLO'); + return; + } + + this._actionEHLO(str); + } + + /** + * Handles server response for EHLO command. If it yielded in + * error, try HELO instead, otherwise initiate TLS negotiation + * if STARTTLS is supported by the server or move into the + * authentication phase. + * + * @param {String} str Message from the server + */ + _actionEHLO(str) { + let match; + + if (str.substr(0, 3) === '421') { + this._onError(new Error('Server terminates connection. response=' + str), 'ECONNECTION', str, 'EHLO'); + return; + } + + if (str.charAt(0) !== '2') { + if (this.options.requireTLS) { + this._onError( + new Error('EHLO failed but HELO does not support required STARTTLS. response=' + str), + 'ECONNECTION', + str, + 'EHLO' + ); + return; + } + + // Try HELO instead + this._responseActions.push(this._actionHELO); + this._sendCommand('HELO ' + this.name); + return; + } + + this._ehloLines = str + .split(/\r?\n/) + .map(line => line.replace(/^\d+[ -]/, '').trim()) + .filter(line => line) + .slice(1); + + // Detect if the server supports STARTTLS + if (!this.secure && !this.options.ignoreTLS && (/[ -]STARTTLS\b/im.test(str) || this.options.requireTLS)) { + this._sendCommand('STARTTLS'); + this._responseActions.push(this._actionSTARTTLS); + return; + } + + // Detect if the server supports SMTPUTF8 + if (/[ -]SMTPUTF8\b/im.test(str)) { + this._supportedExtensions.push('SMTPUTF8'); + } + + // Detect if the server supports DSN + if (/[ -]DSN\b/im.test(str)) { + this._supportedExtensions.push('DSN'); + } + + // Detect if the server supports 8BITMIME + if (/[ -]8BITMIME\b/im.test(str)) { + this._supportedExtensions.push('8BITMIME'); + } + + // Detect if the server supports REQUIRETLS (RFC 8689) + if (/[ -]REQUIRETLS\b/im.test(str)) { + this._supportedExtensions.push('REQUIRETLS'); + } + + // Detect if the server supports PIPELINING + if (/[ -]PIPELINING\b/im.test(str)) { + this._supportedExtensions.push('PIPELINING'); + } + + // Detect if the server supports AUTH + if (/[ -]AUTH\b/i.test(str)) { + this.allowsAuth = true; + } + + // Detect if the server supports PLAIN auth + if (/[ -]AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)PLAIN/i.test(str)) { + this._supportedAuth.push('PLAIN'); + } + + // Detect if the server supports LOGIN auth + if (/[ -]AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)LOGIN/i.test(str)) { + this._supportedAuth.push('LOGIN'); + } + + // Detect if the server supports CRAM-MD5 auth + if (/[ -]AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)CRAM-MD5/i.test(str)) { + this._supportedAuth.push('CRAM-MD5'); + } + + // Detect if the server supports XOAUTH2 auth + if (/[ -]AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)XOAUTH2/i.test(str)) { + this._supportedAuth.push('XOAUTH2'); + } + + // Detect if the server supports SIZE extensions (and the max allowed size) + if ((match = str.match(/[ -]SIZE(?:[ \t]+(\d+))?/im))) { + this._supportedExtensions.push('SIZE'); + this._maxAllowedSize = Number(match[1]) || 0; + } + + this.emit('connect'); + } + + /** + * Handles server response for HELO command. If it yielded in + * error, emit 'error', otherwise move into the authentication phase. + * + * @param {String} str Message from the server + */ + _actionHELO(str) { + if (str.charAt(0) !== '2') { + this._onError(new Error('Invalid HELO. response=' + str), 'EPROTOCOL', str, 'HELO'); + return; + } + + // assume that authentication is enabled (most probably is not though) + this.allowsAuth = true; + + this.emit('connect'); + } + + /** + * Handles server response for STARTTLS command. If there's an error + * try HELO instead, otherwise initiate TLS upgrade. If the upgrade + * succeedes restart the EHLO + * + * @param {String} str Message from the server + */ + _actionSTARTTLS(str) { + if (str.charAt(0) !== '2') { + if (this.options.opportunisticTLS) { + this.logger.info( + { + tnx: 'smtp' + }, + 'Failed STARTTLS upgrade, continuing unencrypted' + ); + return this.emit('connect'); + } + this._onError(new Error('Error upgrading connection with STARTTLS'), 'ETLS', str, 'STARTTLS'); + return; + } + + this._upgradeConnection((err, secured) => { + if (err) { + this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'ETLS', false, 'STARTTLS'); + return; + } + + this.logger.info( + { + tnx: 'smtp' + }, + 'Connection upgraded with STARTTLS' + ); + + if (secured) { + // restart session + if (this.options.lmtp) { + this._responseActions.push(this._actionLHLO); + this._sendCommand('LHLO ' + this.name); + } else { + this._responseActions.push(this._actionEHLO); + this._sendCommand('EHLO ' + this.name); + } + } else { + this.emit('connect'); + } + }); + } + + /** + * Handle the response for AUTH LOGIN command. We are expecting + * '334 VXNlcm5hbWU6' (base64 for 'Username:'). Data to be sent as + * response needs to be base64 encoded username. We do not need + * exact match but settle with 334 response in general as some + * hosts invalidly use a longer message than VXNlcm5hbWU6 + * + * @param {String} str Message from the server + */ + _actionAUTH_LOGIN_USER(str, callback) { + if (!/^334[ -]/.test(str)) { + // expecting '334 VXNlcm5hbWU6' + callback(this._formatError('Invalid login sequence while waiting for "334 VXNlcm5hbWU6"', 'EAUTH', str, 'AUTH LOGIN')); + return; + } + + this._responseActions.push(str => { + this._actionAUTH_LOGIN_PASS(str, callback); + }); + + this._sendCommand(Buffer.from(this._auth.credentials.user + '', 'utf-8').toString('base64')); + } + + /** + * Handle the response for AUTH CRAM-MD5 command. We are expecting + * '334 '. Data to be sent as response needs to be + * base64 decoded challenge string, MD5 hashed using the password as + * a HMAC key, prefixed by the username and a space, and finally all + * base64 encoded again. + * + * @param {String} str Message from the server + */ + _actionAUTH_CRAM_MD5(str, callback) { + let challengeMatch = str.match(/^334\s+(.+)$/); + let challengeString = ''; + + if (!challengeMatch) { + return callback( + this._formatError('Invalid login sequence while waiting for server challenge string', 'EAUTH', str, 'AUTH CRAM-MD5') + ); + } else { + challengeString = challengeMatch[1]; + } + + // Decode from base64 + let base64decoded = Buffer.from(challengeString, 'base64').toString('ascii'), + hmacMD5 = crypto.createHmac('md5', this._auth.credentials.pass); + + hmacMD5.update(base64decoded); + + let prepended = this._auth.credentials.user + ' ' + hmacMD5.digest('hex'); + + this._responseActions.push(str => { + this._actionAUTH_CRAM_MD5_PASS(str, callback); + }); + + this._sendCommand( + Buffer.from(prepended).toString('base64'), + // hidden hash for logs + Buffer.from(this._auth.credentials.user + ' /* secret */').toString('base64') + ); + } + + /** + * Handles the response to CRAM-MD5 authentication, if there's no error, + * the user can be considered logged in. Start waiting for a message to send + * + * @param {String} str Message from the server + */ + _actionAUTH_CRAM_MD5_PASS(str, callback) { + if (!str.match(/^235\s+/)) { + return callback(this._formatError('Invalid login sequence while waiting for "235"', 'EAUTH', str, 'AUTH CRAM-MD5')); + } + + this.logger.info( + { + tnx: 'smtp', + username: this._auth.user, + action: 'authenticated', + method: this._authMethod + }, + 'User %s authenticated', + JSON.stringify(this._auth.user) + ); + this.authenticated = true; + callback(null, true); + } + + /** + * Handle the response for AUTH LOGIN command. We are expecting + * '334 UGFzc3dvcmQ6' (base64 for 'Password:'). Data to be sent as + * response needs to be base64 encoded password. + * + * @param {String} str Message from the server + */ + _actionAUTH_LOGIN_PASS(str, callback) { + if (!/^334[ -]/.test(str)) { + // expecting '334 UGFzc3dvcmQ6' + return callback(this._formatError('Invalid login sequence while waiting for "334 UGFzc3dvcmQ6"', 'EAUTH', str, 'AUTH LOGIN')); + } + + this._responseActions.push(str => { + this._actionAUTHComplete(str, callback); + }); + + this._sendCommand( + Buffer.from((this._auth.credentials.pass || '').toString(), 'utf-8').toString('base64'), + // Hidden pass for logs + Buffer.from('/* secret */', 'utf-8').toString('base64') + ); + } + + /** + * Handles the response for authentication, if there's no error, + * the user can be considered logged in. Start waiting for a message to send + * + * @param {String} str Message from the server + */ + _actionAUTHComplete(str, isRetry, callback) { + if (!callback && typeof isRetry === 'function') { + callback = isRetry; + isRetry = false; + } + + if (str.substr(0, 3) === '334') { + this._responseActions.push(str => { + if (isRetry || this._authMethod !== 'XOAUTH2') { + this._actionAUTHComplete(str, true, callback); + } else { + // fetch a new OAuth2 access token + setImmediate(() => this._handleXOauth2Token(true, callback)); + } + }); + this._sendCommand(''); + return; + } + + if (str.charAt(0) !== '2') { + this.logger.info( + { + tnx: 'smtp', + username: this._auth.user, + action: 'authfail', + method: this._authMethod + }, + 'User %s failed to authenticate', + JSON.stringify(this._auth.user) + ); + return callback(this._formatError('Invalid login', 'EAUTH', str, 'AUTH ' + this._authMethod)); + } + + this.logger.info( + { + tnx: 'smtp', + username: this._auth.user, + action: 'authenticated', + method: this._authMethod + }, + 'User %s authenticated', + JSON.stringify(this._auth.user) + ); + this.authenticated = true; + callback(null, true); + } + + /** + * Handle response for a MAIL FROM: command + * + * @param {String} str Message from the server + */ + _actionMAIL(str, callback) { + let message, curRecipient; + if (Number(str.charAt(0)) !== 2) { + if (this._usingSmtpUtf8 && /^550 /.test(str) && /[\x80-\uFFFF]/.test(this._envelope.from)) { + message = 'Internationalized mailbox name not allowed'; + } else { + message = 'Mail command failed'; + } + return callback(this._formatError(message, 'EENVELOPE', str, 'MAIL FROM')); + } + + if (!this._envelope.rcptQueue.length) { + return callback(this._formatError("Can't send mail - no recipients defined", 'EENVELOPE', false, 'API')); + } else { + this._recipientQueue = []; + + if (this._supportedExtensions.includes('PIPELINING')) { + while (this._envelope.rcptQueue.length) { + curRecipient = this._envelope.rcptQueue.shift(); + this._recipientQueue.push(curRecipient); + this._responseActions.push(str => { + this._actionRCPT(str, callback); + }); + this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs()); + } + } else { + curRecipient = this._envelope.rcptQueue.shift(); + this._recipientQueue.push(curRecipient); + this._responseActions.push(str => { + this._actionRCPT(str, callback); + }); + this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs()); + } + } + } + + /** + * Handle response for a RCPT TO: command + * + * @param {String} str Message from the server + */ + _actionRCPT(str, callback) { + let message, + err, + curRecipient = this._recipientQueue.shift(); + if (Number(str.charAt(0)) !== 2) { + // this is a soft error + if (this._usingSmtpUtf8 && /^553 /.test(str) && /[\x80-\uFFFF]/.test(curRecipient)) { + message = 'Internationalized mailbox name not allowed'; + } else { + message = 'Recipient command failed'; + } + this._envelope.rejected.push(curRecipient); + // store error for the failed recipient + err = this._formatError(message, 'EENVELOPE', str, 'RCPT TO'); + err.recipient = curRecipient; + this._envelope.rejectedErrors.push(err); + } else { + this._envelope.accepted.push(curRecipient); + } + + if (!this._envelope.rcptQueue.length && !this._recipientQueue.length) { + if (this._envelope.rejected.length < this._envelope.to.length) { + this._responseActions.push(str => { + this._actionDATA(str, callback); + }); + this._sendCommand('DATA'); + } else { + err = this._formatError("Can't send mail - all recipients were rejected", 'EENVELOPE', str, 'RCPT TO'); + err.rejected = this._envelope.rejected; + err.rejectedErrors = this._envelope.rejectedErrors; + return callback(err); + } + } else if (this._envelope.rcptQueue.length) { + curRecipient = this._envelope.rcptQueue.shift(); + this._recipientQueue.push(curRecipient); + this._responseActions.push(str => { + this._actionRCPT(str, callback); + }); + this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs()); + } + } + + /** + * Handle response for a DATA command + * + * @param {String} str Message from the server + */ + _actionDATA(str, callback) { + // response should be 354 but according to this issue https://github.com/eleith/emailjs/issues/24 + // some servers might use 250 instead, so lets check for 2 or 3 as the first digit + if (!/^[23]/.test(str)) { + return callback(this._formatError('Data command failed', 'EENVELOPE', str, 'DATA')); + } + + let response = { + accepted: this._envelope.accepted, + rejected: this._envelope.rejected + }; + + if (this._ehloLines && this._ehloLines.length) { + response.ehlo = this._ehloLines; + } + + if (this._envelope.rejectedErrors.length) { + response.rejectedErrors = this._envelope.rejectedErrors; + } + + callback(null, response); + } + + /** + * Handle response for a DATA stream when using SMTP + * We expect a single response that defines if the sending succeeded or failed + * + * @param {String} str Message from the server + */ + _actionSMTPStream(str, callback) { + if (Number(str.charAt(0)) !== 2) { + // Message failed + return callback(this._formatError('Message failed', 'EMESSAGE', str, 'DATA')); + } else { + // Message sent succesfully + return callback(null, str); + } + } + + /** + * Handle response for a DATA stream + * We expect a separate response for every recipient. All recipients can either + * succeed or fail separately + * + * @param {String} recipient The recipient this response applies to + * @param {Boolean} final Is this the final recipient? + * @param {String} str Message from the server + */ + _actionLMTPStream(recipient, final, str, callback) { + let err; + if (Number(str.charAt(0)) !== 2) { + // Message failed + err = this._formatError('Message failed for recipient ' + recipient, 'EMESSAGE', str, 'DATA'); + err.recipient = recipient; + this._envelope.rejected.push(recipient); + this._envelope.rejectedErrors.push(err); + for (let i = 0, len = this._envelope.accepted.length; i < len; i++) { + if (this._envelope.accepted[i] === recipient) { + this._envelope.accepted.splice(i, 1); + } + } + } + if (final) { + return callback(null, str); + } + } + + _handleXOauth2Token(isRetry, callback) { + this._auth.oauth2.getToken(isRetry, (err, accessToken) => { + if (err) { + this.logger.info( + { + tnx: 'smtp', + username: this._auth.user, + action: 'authfail', + method: this._authMethod + }, + 'User %s failed to authenticate', + JSON.stringify(this._auth.user) + ); + return callback(this._formatError(err, 'EAUTH', false, 'AUTH XOAUTH2')); + } + this._responseActions.push(str => { + this._actionAUTHComplete(str, isRetry, callback); + }); + this._sendCommand( + 'AUTH XOAUTH2 ' + this._auth.oauth2.buildXOAuth2Token(accessToken), + // Hidden for logs + 'AUTH XOAUTH2 ' + this._auth.oauth2.buildXOAuth2Token('/* secret */') + ); + }); + } + + /** + * + * @param {string} command + * @private + */ + _isDestroyedMessage(command) { + if (this._destroyed) { + return 'Cannot ' + command + ' - smtp connection is already destroyed.'; + } + + if (this._socket) { + if (this._socket.destroyed) { + return 'Cannot ' + command + ' - smtp connection socket is already destroyed.'; + } + + if (!this._socket.writable) { + return 'Cannot ' + command + ' - smtp connection socket is already half-closed.'; + } + } + } + + _getHostname() { + // defaul hostname is machine hostname or [IP] + let defaultHostname; + try { + defaultHostname = os.hostname() || ''; + } catch (_err) { + // fails on windows 7 + defaultHostname = 'localhost'; + } + + // ignore if not FQDN + if (!defaultHostname || defaultHostname.indexOf('.') < 0) { + defaultHostname = '[127.0.0.1]'; + } + + // IP should be enclosed in [] + if (defaultHostname.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) { + defaultHostname = '[' + defaultHostname + ']'; + } + + return defaultHostname; + } +} + +module.exports = SMTPConnection; diff --git a/node_modules/nodemailer/lib/smtp-pool/index.js b/node_modules/nodemailer/lib/smtp-pool/index.js new file mode 100644 index 0000000..2d8210e --- /dev/null +++ b/node_modules/nodemailer/lib/smtp-pool/index.js @@ -0,0 +1,653 @@ +'use strict'; + +const EventEmitter = require('events'); +const PoolResource = require('./pool-resource'); +const SMTPConnection = require('../smtp-connection'); +const wellKnown = require('../well-known'); +const shared = require('../shared'); +const errors = require('../errors'); +const packageData = require('../../package.json'); + +/** + * Creates a SMTP pool transport object for Nodemailer + * + * @constructor + * @param {Object} options SMTP Connection options + */ +class SMTPPool extends 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 && wellKnown(service) // wellknown options + ); + + this.options.maxConnections = this.options.maxConnections || 5; + this.options.maxMessages = this.options.maxMessages || 100; + + this.logger = shared.getLogger(this.options, { + component: this.options.component || 'smtp-pool' + }); + + // temporary object + let connection = new SMTPConnection(this.options); + + this.name = 'SMTP (pool)'; + this.version = packageData.version + '[client:' + connection.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 {Object} options Connection options + * @param {Function} callback Callback function to run with the socket keys + */ + getSocket(options, callback) { + // return immediatelly + return setImmediate(() => callback(null, false)); + } + + /** + * Queues an e-mail to be sent using the selected settings + * + * @param {Object} mail Mail object + * @param {Function} callback Callback function + */ + send(mail, callback) { + if (this._closed) { + 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; + let len = this._connections.length; + this._closed = true; + + // clear rate limit timer if it exists + clearTimeout(this._rateLimit.timeout); + + 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 + let invokeCallbacks = () => { + if (!this._queue.length) { + this.logger.debug( + { + tnx: 'connection' + }, + 'Pending queue entries cleared' + ); + return; + } + let entry = this._queue.shift(); + if (entry && typeof entry.callback === 'function') { + try { + entry.callback(new Error('Connection pool was closed')); + } catch (E) { + this.logger.error( + { + err: E, + tnx: 'callback', + cid: connection.id + }, + 'Callback error for #%s: %s', + connection.id, + 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 + */ + _processMessages() { + let connection; + let i, len; + + // 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 + for (i = 0, len = this._connections.length; i < len; i++) { + if (this._connections[i].available) { + connection = this._connections[i]; + break; + } + } + + 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'); + } + + let 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 + */ + _createConnection() { + let connection = new PoolResource(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 !== '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(() => { + if (connection.queueEntry) { + if (this._shouldRequeuOnConnectionClose(connection.queueEntry)) { + this._requeueEntryOnConnectionClose(connection); + } else { + this._failDeliveryOnConnectionClose(connection); + } + } + this._continueProcessing(); + }, 50); + } else { + if (!this._closed && this.idling && !this._connections.length) { + this.emit('clear'); + } + + this._continueProcessing(); + } + }); + + this._connections.push(connection); + + return connection; + } + + _shouldRequeuOnConnectionClose(queueEntry) { + if (this.options.maxRequeues === undefined || this.options.maxRequeues < 0) { + return true; + } + + return queueEntry.requeueAttempts < this.options.maxRequeues; + } + + _failDeliveryOnConnectionClose(connection) { + if (connection.queueEntry && connection.queueEntry.callback) { + try { + connection.queueEntry.callback(new Error('Reached maximum number of retries after connection was closed')); + } 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; + } + } + + _requeueEntryOnConnectionClose(connection) { + connection.queueEntry.requeueAttempts = 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; + } + + /** + * Continue to process message if the pool hasn't closed + */ + _continueProcessing() { + if (this._closed) { + this.close(); + } else { + setTimeout(() => this._processMessages(), 100); + } + } + + /** + * Remove resource from pool + * + * @param {Object} connection The PoolResource to remove + */ + _removeConnection(connection) { + let 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 {Function} callback Callback function to run once rate limiter has been cleared + */ + _checkRateLimit(callback) { + if (!this._rateLimit.limit) { + return callback(); + } + + let 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(); + } else 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 + */ + _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) { + let cb = this._rateLimit.waiting.shift(); + setImmediate(cb); + } + } + + /** + * Returns true if there are free slots in the queue + */ + isIdle() { + return this.idling; + } + + /** + * Verifies SMTP configuration + * + * @param {Function} callback Callback function + */ + verify(callback) { + let promise; + + if (!callback) { + promise = new Promise((resolve, reject) => { + callback = shared.callbackPromise(resolve, reject); + }); + } + + let auth = new PoolResource(this).auth; + + this.getSocket(this.options, (err, socketOptions) => { + if (err) { + return callback(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 = shared.assign(false, options); + Object.keys(socketOptions).forEach(key => { + options[key] = socketOptions[key]; + }); + } + + let connection = new SMTPConnection(options); + let returned = false; + + connection.once('error', err => { + if (returned) { + return; + } + returned = true; + connection.close(); + return callback(err); + }); + + connection.once('end', () => { + if (returned) { + return; + } + returned = true; + return callback(new Error('Connection closed')); + }); + + let finalize = () => { + if (returned) { + return; + } + returned = true; + connection.quit(); + return callback(null, true); + }; + + connection.connect(() => { + if (returned) { + return; + } + + if (auth && (connection.allowsAuth || options.forceAuth)) { + connection.login(auth, err => { + if (returned) { + return; + } + + if (err) { + returned = true; + connection.close(); + return callback(err); + } + + finalize(); + }); + } else if (!auth && connection.allowsAuth && options.forceAuth) { + let err = new Error('Authentication info was not provided'); + err.code = errors.ENOAUTH; + + returned = true; + connection.close(); + return callback(err); + } else { + finalize(); + } + }); + }); + + return promise; + } +} + +// expose to the world +module.exports = SMTPPool; diff --git a/node_modules/nodemailer/lib/smtp-pool/pool-resource.js b/node_modules/nodemailer/lib/smtp-pool/pool-resource.js new file mode 100644 index 0000000..00531be --- /dev/null +++ b/node_modules/nodemailer/lib/smtp-pool/pool-resource.js @@ -0,0 +1,260 @@ +'use strict'; + +const SMTPConnection = require('../smtp-connection'); +const assign = require('../shared').assign; +const XOAuth2 = require('../xoauth2'); +const errors = require('../errors'); +const EventEmitter = require('events'); + +/** + * Creates an element for the pool + * + * @constructor + * @param {Object} options SMTPPool instance + */ +class PoolResource extends 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': { + let oauth2 = new XOAuth2(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.emit('error', 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; + } + + /** + * Initiates a connection to the SMTP server + * + * @param {Function} callback Callback function to run once the connection is established or failed + */ + connect(callback) { + this.pool.getSocket(this.options, (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 || '' + ); + + options = assign(false, options); + Object.keys(socketOptions).forEach(key => { + options[key] = socketOptions[key]; + }); + } + + this.connection = new SMTPConnection(options); + + this.connection.once('error', err => { + this.emit('error', err); + if (returned) { + return; + } + returned = true; + return callback(err); + }); + + this.connection.once('end', () => { + this.close(); + if (returned) { + return; + } + returned = true; + + let timer = setTimeout(() => { + if (returned) { + return; + } + // still have not returned, this means we have an unexpected connection close + let err = new Error('Unexpected socket close'); + if (this.connection && this.connection._socket && this.connection._socket.upgrading) { + // starttls connection errors + err.code = errors.ETLS; + } + callback(err); + }, 1000); + + try { + timer.unref(); + } catch (_E) { + // Ignore. Happens on envs with non-node timer implementation + } + }); + + this.connection.connect(() => { + if (returned) { + 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.emit('error', 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 {Object} mail Mail object + * @param {Function} callback Callback function + */ + send(mail, callback) { + if (!this._connected) { + return this.connect(err => { + if (err) { + return callback(err); + } + return this.send(mail, callback); + }); + } + + let envelope = mail.message.getEnvelope(); + let messageId = mail.message.messageId(); + + let 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.connection.send(envelope, mail.message.createReadStream(), (err, info) => { + this.messages++; + + if (err) { + this.connection.close(); + this.emit('error', err); + return callback(err); + } + + info.envelope = { + from: envelope.from, + to: envelope.to + }; + info.messageId = messageId; + + setImmediate(() => { + let err; + if (this.messages >= this.options.maxMessages) { + err = new Error('Resource exhausted'); + err.code = errors.EMAXLIMIT; + this.connection.close(); + this.emit('error', err); + } else { + this.pool._checkRateLimit(() => { + this.available = true; + this.emit('available'); + }); + } + }); + + callback(null, info); + }); + } + + /** + * Closes the connection + */ + close() { + this._connected = false; + if (this.auth && this.auth.oauth2) { + this.auth.oauth2.removeAllListeners(); + } + if (this.connection) { + this.connection.close(); + } + this.emit('close'); + } +} + +module.exports = PoolResource; diff --git a/node_modules/nodemailer/lib/smtp-transport/index.js b/node_modules/nodemailer/lib/smtp-transport/index.js new file mode 100644 index 0000000..e0cbd71 --- /dev/null +++ b/node_modules/nodemailer/lib/smtp-transport/index.js @@ -0,0 +1,422 @@ +'use strict'; + +const EventEmitter = require('events'); +const SMTPConnection = require('../smtp-connection'); +const wellKnown = require('../well-known'); +const shared = require('../shared'); +const XOAuth2 = require('../xoauth2'); +const errors = require('../errors'); +const packageData = require('../../package.json'); + +/** + * Creates a SMTP transport object for Nodemailer + * + * @constructor + * @param {Object} options Connection options + */ +class SMTPTransport extends 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 && wellKnown(service) // wellknown options + ); + + this.logger = shared.getLogger(this.options, { + component: this.options.component || 'smtp-transport' + }); + + // temporary object + let connection = new SMTPConnection(this.options); + + this.name = 'SMTP'; + this.version = packageData.version + '[client:' + connection.version + ']'; + + if (this.options.auth) { + this.auth = this.getAuth({}); + } + } + + /** + * Placeholder function for creating proxy sockets. This method immediatelly returns + * without a socket + * + * @param {Object} options Connection options + * @param {Function} callback Callback function to run with the socket keys + */ + getSocket(options, callback) { + // return immediatelly + return setImmediate(() => callback(null, false)); + } + + getAuth(authOpts) { + if (!authOpts) { + return this.auth; + } + + let hasAuth = false; + let authData = {}; + + if (this.options.auth && typeof this.options.auth === 'object') { + Object.keys(this.options.auth).forEach(key => { + hasAuth = true; + authData[key] = this.options.auth[key]; + }); + } + + if (authOpts && typeof authOpts === 'object') { + Object.keys(authOpts).forEach(key => { + hasAuth = true; + authData[key] = authOpts[key]; + }); + } + + if (!hasAuth) { + return false; + } + + switch ((authData.type || '').toString().toUpperCase()) { + case 'OAUTH2': { + if (!authData.service && !authData.user) { + return false; + } + let oauth2 = new XOAuth2(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 {Object} mail Mail object + * @param {Function} callback Callback function + */ + send(mail, callback) { + this.getSocket(this.options, (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 = shared.assign(false, options); + Object.keys(socketOptions).forEach(key => { + options[key] = socketOptions[key]; + }); + } + + let connection = new SMTPConnection(options); + + connection.once('error', err => { + if (returned) { + return; + } + returned = true; + connection.close(); + return callback(err); + }); + + connection.once('end', () => { + if (returned) { + return; + } + + let timer = setTimeout(() => { + if (returned) { + return; + } + returned = true; + // still have not returned, this means we have an unexpected connection close + let err = new Error('Unexpected socket close'); + if (connection && connection._socket && connection._socket.upgrading) { + // starttls connection errors + err.code = errors.ETLS; + } + callback(err); + }, 1000); + + try { + timer.unref(); + } catch (_E) { + // Ignore. Happens on envs with non-node timer implementation + } + }); + + let sendMessage = () => { + let envelope = mail.message.getEnvelope(); + let messageId = mail.message.messageId(); + + let 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) => { + returned = true; + 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(() => { + if (returned) { + return; + } + + let auth = this.getAuth(mail.data.auth); + + if (auth && (connection.allowsAuth || options.forceAuth)) { + connection.login(auth, err => { + if (auth && auth !== this.auth && auth.oauth2) { + auth.oauth2.removeAllListeners(); + } + if (returned) { + return; + } + + if (err) { + returned = true; + connection.close(); + return callback(err); + } + + sendMessage(); + }); + } else { + sendMessage(); + } + }); + }); + } + + /** + * Verifies SMTP configuration + * + * @param {Function} callback Callback function + */ + verify(callback) { + let promise; + + if (!callback) { + promise = new Promise((resolve, reject) => { + callback = shared.callbackPromise(resolve, reject); + }); + } + + this.getSocket(this.options, (err, socketOptions) => { + if (err) { + return callback(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 = shared.assign(false, options); + Object.keys(socketOptions).forEach(key => { + options[key] = socketOptions[key]; + }); + } + + let connection = new SMTPConnection(options); + let returned = false; + + connection.once('error', err => { + if (returned) { + return; + } + returned = true; + connection.close(); + return callback(err); + }); + + connection.once('end', () => { + if (returned) { + return; + } + returned = true; + return callback(new Error('Connection closed')); + }); + + let finalize = () => { + if (returned) { + return; + } + returned = true; + connection.quit(); + return callback(null, true); + }; + + connection.connect(() => { + if (returned) { + return; + } + + let authData = this.getAuth({}); + + if (authData && (connection.allowsAuth || options.forceAuth)) { + connection.login(authData, err => { + if (returned) { + return; + } + + if (err) { + returned = true; + connection.close(); + return callback(err); + } + + finalize(); + }); + } else if (!authData && connection.allowsAuth && options.forceAuth) { + let err = new Error('Authentication info was not provided'); + err.code = errors.ENOAUTH; + + returned = true; + connection.close(); + return callback(err); + } else { + finalize(); + } + }); + }); + + return promise; + } + + /** + * Releases resources + */ + close() { + if (this.auth && this.auth.oauth2) { + this.auth.oauth2.removeAllListeners(); + } + this.emit('close'); + } +} + +// expose to the world +module.exports = SMTPTransport; diff --git a/node_modules/nodemailer/lib/stream-transport/index.js b/node_modules/nodemailer/lib/stream-transport/index.js new file mode 100644 index 0000000..1921469 --- /dev/null +++ b/node_modules/nodemailer/lib/stream-transport/index.js @@ -0,0 +1,135 @@ +'use strict'; + +const packageData = require('../../package.json'); +const shared = require('../shared'); + +/** + * 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 {Object} 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 {Object} emailMessage MailComposer object + * @param {Function} callback Callback function to run when the sending is completed + */ + send(mail, done) { + // We probably need this in the output + mail.message.keepBcc = true; + + let envelope = mail.data.envelope || mail.message.getEnvelope(); + let messageId = mail.message.messageId(); + + let 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 ? '' : '' + ); + + setImmediate(() => { + let stream; + + try { + stream = mail.message.createReadStream(); + } 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: mail.data.envelope || mail.message.getEnvelope(), + messageId, + message: stream + }); + } + + let 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: mail.data.envelope || mail.message.getEnvelope(), + messageId, + message: Buffer.concat(chunks, chunklen) + }) + ); + }); + } +} + +module.exports = StreamTransport; diff --git a/node_modules/nodemailer/lib/well-known/index.js b/node_modules/nodemailer/lib/well-known/index.js new file mode 100644 index 0000000..9fdc28f --- /dev/null +++ b/node_modules/nodemailer/lib/well-known/index.js @@ -0,0 +1,47 @@ +'use strict'; + +const services = require('./services.json'); +const normalized = {}; + +Object.keys(services).forEach(key => { + let service = services[key]; + + normalized[normalizeKey(key)] = normalizeService(service); + + [].concat(service.aliases || []).forEach(alias => { + normalized[normalizeKey(alias)] = normalizeService(service); + }); + + [].concat(service.domains || []).forEach(domain => { + normalized[normalizeKey(domain)] = normalizeService(service); + }); +}); + +function normalizeKey(key) { + return key.replace(/[^a-zA-Z0-9.-]/g, '').toLowerCase(); +} + +function normalizeService(service) { + let filter = ['domains', 'aliases']; + let response = {}; + + Object.keys(service).forEach(key => { + if (filter.indexOf(key) < 0) { + 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 {String} key [description] + * @returns {Object} SMTP config or false if not found + */ +module.exports = function (key) { + key = normalizeKey(key.split('@').pop()); + return normalized[key] || false; +}; diff --git a/node_modules/nodemailer/lib/well-known/services.json b/node_modules/nodemailer/lib/well-known/services.json new file mode 100644 index 0000000..99aff02 --- /dev/null +++ b/node_modules/nodemailer/lib/well-known/services.json @@ -0,0 +1,619 @@ +{ + "1und1": { + "description": "1&1 Mail (German hosting provider)", + "host": "smtp.1und1.de", + "port": 465, + "secure": true, + "authMethod": "LOGIN" + }, + + "126": { + "description": "126 Mail (NetEase)", + "host": "smtp.126.com", + "port": 465, + "secure": true + }, + + "163": { + "description": "163 Mail (NetEase)", + "host": "smtp.163.com", + "port": 465, + "secure": true + }, + + "Aliyun": { + "description": "Alibaba Cloud Mail", + "domains": ["aliyun.com"], + "host": "smtp.aliyun.com", + "port": 465, + "secure": true + }, + + "AliyunQiye": { + "description": "Alibaba Cloud Enterprise Mail", + "host": "smtp.qiye.aliyun.com", + "port": 465, + "secure": true + }, + + "AOL": { + "description": "AOL Mail", + "domains": ["aol.com"], + "host": "smtp.aol.com", + "port": 587 + }, + + "Aruba": { + "description": "Aruba PEC (Italian email provider)", + "domains": ["aruba.it", "pec.aruba.it"], + "aliases": ["Aruba PEC"], + "host": "smtps.aruba.it", + "port": 465, + "secure": true, + "authMethod": "LOGIN" + }, + + "Bluewin": { + "description": "Bluewin (Swiss email provider)", + "host": "smtpauths.bluewin.ch", + "domains": ["bluewin.ch"], + "port": 465 + }, + + "BOL": { + "description": "BOL Mail (Brazilian provider)", + "domains": ["bol.com.br"], + "host": "smtp.bol.com.br", + "port": 587, + "requireTLS": true + }, + + "DebugMail": { + "description": "DebugMail (email testing service)", + "host": "debugmail.io", + "port": 25 + }, + + "Disroot": { + "description": "Disroot (privacy-focused provider)", + "domains": ["disroot.org"], + "host": "disroot.org", + "port": 587, + "secure": false, + "authMethod": "LOGIN" + }, + + "DynectEmail": { + "description": "Dyn Email Delivery", + "aliases": ["Dynect"], + "host": "smtp.dynect.net", + "port": 25 + }, + + "ElasticEmail": { + "description": "Elastic Email", + "aliases": ["Elastic Email"], + "host": "smtp.elasticemail.com", + "port": 465, + "secure": true + }, + + "Ethereal": { + "description": "Ethereal Email (email testing service)", + "aliases": ["ethereal.email"], + "host": "smtp.ethereal.email", + "port": 587 + }, + + "FastMail": { + "description": "FastMail", + "domains": ["fastmail.fm"], + "host": "smtp.fastmail.com", + "port": 465, + "secure": true + }, + + "Feishu Mail": { + "description": "Feishu Mail (Lark)", + "aliases": ["Feishu", "FeishuMail"], + "domains": ["www.feishu.cn"], + "host": "smtp.feishu.cn", + "port": 465, + "secure": true + }, + + "Forward Email": { + "description": "Forward Email (email forwarding service)", + "aliases": ["FE", "ForwardEmail"], + "domains": ["forwardemail.net"], + "host": "smtp.forwardemail.net", + "port": 465, + "secure": true + }, + + "GandiMail": { + "description": "Gandi Mail", + "aliases": ["Gandi", "Gandi Mail"], + "host": "mail.gandi.net", + "port": 587 + }, + + "Gmail": { + "description": "Gmail", + "aliases": ["Google Mail"], + "domains": ["gmail.com", "googlemail.com"], + "host": "smtp.gmail.com", + "port": 465, + "secure": true + }, + + "GmailWorkspace": { + "description": "Gmail Workspace", + "aliases": ["Google Workspace Mail"], + "host": "smtp-relay.gmail.com", + "port": 465, + "secure": true + }, + + "GMX": { + "description": "GMX Mail", + "domains": ["gmx.com", "gmx.net", "gmx.de"], + "host": "mail.gmx.com", + "port": 587 + }, + + "Godaddy": { + "description": "GoDaddy Email (US)", + "host": "smtpout.secureserver.net", + "port": 25 + }, + + "GodaddyAsia": { + "description": "GoDaddy Email (Asia)", + "host": "smtp.asia.secureserver.net", + "port": 25 + }, + + "GodaddyEurope": { + "description": "GoDaddy Email (Europe)", + "host": "smtp.europe.secureserver.net", + "port": 25 + }, + + "hot.ee": { + "description": "Hot.ee (Estonian email provider)", + "host": "mail.hot.ee" + }, + + "Hotmail": { + "description": "Outlook.com / Hotmail", + "aliases": ["Outlook", "Outlook.com", "Hotmail.com"], + "domains": ["hotmail.com", "outlook.com"], + "host": "smtp-mail.outlook.com", + "port": 587 + }, + + "iCloud": { + "description": "iCloud Mail", + "aliases": ["Me", "Mac"], + "domains": ["me.com", "mac.com"], + "host": "smtp.mail.me.com", + "port": 587 + }, + + "Infomaniak": { + "description": "Infomaniak Mail (Swiss hosting provider)", + "host": "mail.infomaniak.com", + "domains": ["ik.me", "ikmail.com", "etik.com"], + "port": 587 + }, + + "KolabNow": { + "description": "KolabNow (secure email service)", + "domains": ["kolabnow.com"], + "aliases": ["Kolab"], + "host": "smtp.kolabnow.com", + "port": 465, + "secure": true, + "authMethod": "LOGIN" + }, + + "Loopia": { + "description": "Loopia (Swedish hosting provider)", + "host": "mailcluster.loopia.se", + "port": 465 + }, + + "Loops": { + "description": "Loops", + "host": "smtp.loops.so", + "port": 587 + }, + + "mail.ee": { + "description": "Mail.ee (Estonian email provider)", + "host": "smtp.mail.ee" + }, + + "Mail.ru": { + "description": "Mail.ru", + "host": "smtp.mail.ru", + "port": 465, + "secure": true + }, + + "Mailcatch.app": { + "description": "Mailcatch (email testing service)", + "host": "sandbox-smtp.mailcatch.app", + "port": 2525 + }, + + "Maildev": { + "description": "MailDev (local email testing)", + "port": 1025, + "ignoreTLS": true + }, + + "MailerSend": { + "description": "MailerSend", + "host": "smtp.mailersend.net", + "port": 587 + }, + + "Mailgun": { + "description": "Mailgun", + "host": "smtp.mailgun.org", + "port": 465, + "secure": true + }, + + "Mailjet": { + "description": "Mailjet", + "host": "in.mailjet.com", + "port": 587 + }, + + "Mailosaur": { + "description": "Mailosaur (email testing service)", + "host": "mailosaur.io", + "port": 25 + }, + + "Mailtrap": { + "description": "Mailtrap", + "host": "live.smtp.mailtrap.io", + "port": 587 + }, + + "Mandrill": { + "description": "Mandrill (by Mailchimp)", + "host": "smtp.mandrillapp.com", + "port": 587 + }, + + "Naver": { + "description": "Naver Mail (Korean email provider)", + "host": "smtp.naver.com", + "port": 587 + }, + + "OhMySMTP": { + "description": "OhMySMTP (email delivery service)", + "host": "smtp.ohmysmtp.com", + "port": 587, + "secure": false + }, + + "One": { + "description": "One.com Email", + "host": "send.one.com", + "port": 465, + "secure": true + }, + + "OpenMailBox": { + "description": "OpenMailBox", + "aliases": ["OMB", "openmailbox.org"], + "host": "smtp.openmailbox.org", + "port": 465, + "secure": true + }, + + "Outlook365": { + "description": "Microsoft 365 / Office 365", + "host": "smtp.office365.com", + "port": 587, + "secure": false + }, + + "Postmark": { + "description": "Postmark", + "aliases": ["PostmarkApp"], + "host": "smtp.postmarkapp.com", + "port": 2525 + }, + + "Proton": { + "description": "Proton Mail", + "aliases": ["ProtonMail", "Proton.me", "Protonmail.com", "Protonmail.ch"], + "domains": ["proton.me", "protonmail.com", "pm.me", "protonmail.ch"], + "host": "smtp.protonmail.ch", + "port": 587, + "requireTLS": true + }, + + "qiye.aliyun": { + "description": "Alibaba Mail Enterprise Edition", + "host": "smtp.mxhichina.com", + "port": "465", + "secure": true + }, + + "QQ": { + "description": "QQ Mail", + "domains": ["qq.com"], + "host": "smtp.qq.com", + "port": 465, + "secure": true + }, + + "QQex": { + "description": "QQ Enterprise Mail", + "aliases": ["QQ Enterprise"], + "domains": ["exmail.qq.com"], + "host": "smtp.exmail.qq.com", + "port": 465, + "secure": true + }, + + "Resend": { + "description": "Resend", + "host": "smtp.resend.com", + "port": 465, + "secure": true + }, + + "Runbox": { + "description": "Runbox (Norwegian email provider)", + "domains": ["runbox.com"], + "host": "smtp.runbox.com", + "port": 465, + "secure": true + }, + + "SendCloud": { + "description": "SendCloud (Chinese email delivery)", + "host": "smtp.sendcloud.net", + "port": 2525 + }, + + "SendGrid": { + "description": "SendGrid", + "host": "smtp.sendgrid.net", + "port": 587 + }, + + "SendinBlue": { + "description": "Brevo (formerly Sendinblue)", + "aliases": ["Brevo"], + "host": "smtp-relay.brevo.com", + "port": 587 + }, + + "SendPulse": { + "description": "SendPulse", + "host": "smtp-pulse.com", + "port": 465, + "secure": true + }, + + "SES": { + "description": "AWS SES US East (N. Virginia)", + "host": "email-smtp.us-east-1.amazonaws.com", + "port": 465, + "secure": true + }, + + "SES-AP-NORTHEAST-1": { + "description": "AWS SES Asia Pacific (Tokyo)", + "host": "email-smtp.ap-northeast-1.amazonaws.com", + "port": 465, + "secure": true + }, + + "SES-AP-NORTHEAST-2": { + "description": "AWS SES Asia Pacific (Seoul)", + "host": "email-smtp.ap-northeast-2.amazonaws.com", + "port": 465, + "secure": true + }, + + "SES-AP-NORTHEAST-3": { + "description": "AWS SES Asia Pacific (Osaka)", + "host": "email-smtp.ap-northeast-3.amazonaws.com", + "port": 465, + "secure": true + }, + + "SES-AP-SOUTH-1": { + "description": "AWS SES Asia Pacific (Mumbai)", + "host": "email-smtp.ap-south-1.amazonaws.com", + "port": 465, + "secure": true + }, + + "SES-AP-SOUTHEAST-1": { + "description": "AWS SES Asia Pacific (Singapore)", + "host": "email-smtp.ap-southeast-1.amazonaws.com", + "port": 465, + "secure": true + }, + + "SES-AP-SOUTHEAST-2": { + "description": "AWS SES Asia Pacific (Sydney)", + "host": "email-smtp.ap-southeast-2.amazonaws.com", + "port": 465, + "secure": true + }, + + "SES-CA-CENTRAL-1": { + "description": "AWS SES Canada (Central)", + "host": "email-smtp.ca-central-1.amazonaws.com", + "port": 465, + "secure": true + }, + + "SES-EU-CENTRAL-1": { + "description": "AWS SES Europe (Frankfurt)", + "host": "email-smtp.eu-central-1.amazonaws.com", + "port": 465, + "secure": true + }, + + "SES-EU-NORTH-1": { + "description": "AWS SES Europe (Stockholm)", + "host": "email-smtp.eu-north-1.amazonaws.com", + "port": 465, + "secure": true + }, + + "SES-EU-WEST-1": { + "description": "AWS SES Europe (Ireland)", + "host": "email-smtp.eu-west-1.amazonaws.com", + "port": 465, + "secure": true + }, + + "SES-EU-WEST-2": { + "description": "AWS SES Europe (London)", + "host": "email-smtp.eu-west-2.amazonaws.com", + "port": 465, + "secure": true + }, + + "SES-EU-WEST-3": { + "description": "AWS SES Europe (Paris)", + "host": "email-smtp.eu-west-3.amazonaws.com", + "port": 465, + "secure": true + }, + + "SES-SA-EAST-1": { + "description": "AWS SES South America (São Paulo)", + "host": "email-smtp.sa-east-1.amazonaws.com", + "port": 465, + "secure": true + }, + + "SES-US-EAST-1": { + "description": "AWS SES US East (N. Virginia)", + "host": "email-smtp.us-east-1.amazonaws.com", + "port": 465, + "secure": true + }, + + "SES-US-EAST-2": { + "description": "AWS SES US East (Ohio)", + "host": "email-smtp.us-east-2.amazonaws.com", + "port": 465, + "secure": true + }, + + "SES-US-GOV-EAST-1": { + "description": "AWS SES GovCloud (US-East)", + "host": "email-smtp.us-gov-east-1.amazonaws.com", + "port": 465, + "secure": true + }, + + "SES-US-GOV-WEST-1": { + "description": "AWS SES GovCloud (US-West)", + "host": "email-smtp.us-gov-west-1.amazonaws.com", + "port": 465, + "secure": true + }, + + "SES-US-WEST-1": { + "description": "AWS SES US West (N. California)", + "host": "email-smtp.us-west-1.amazonaws.com", + "port": 465, + "secure": true + }, + + "SES-US-WEST-2": { + "description": "AWS SES US West (Oregon)", + "host": "email-smtp.us-west-2.amazonaws.com", + "port": 465, + "secure": true + }, + + "Seznam": { + "description": "Seznam Email (Czech email provider)", + "aliases": ["Seznam Email"], + "domains": ["seznam.cz", "email.cz", "post.cz", "spoluzaci.cz"], + "host": "smtp.seznam.cz", + "port": 465, + "secure": true + }, + + "SMTP2GO": { + "description": "SMTP2GO", + "host": "mail.smtp2go.com", + "port": 2525 + }, + + "Sparkpost": { + "description": "SparkPost", + "aliases": ["SparkPost", "SparkPost Mail"], + "domains": ["sparkpost.com"], + "host": "smtp.sparkpostmail.com", + "port": 587, + "secure": false + }, + + "Tipimail": { + "description": "Tipimail (email delivery service)", + "host": "smtp.tipimail.com", + "port": 587 + }, + + "Tutanota": { + "description": "Tutanota (Tuta Mail)", + "domains": ["tutanota.com", "tuta.com", "tutanota.de", "tuta.io"], + "host": "smtp.tutanota.com", + "port": 465, + "secure": true + }, + + "Yahoo": { + "description": "Yahoo Mail", + "domains": ["yahoo.com"], + "host": "smtp.mail.yahoo.com", + "port": 465, + "secure": true + }, + + "Yandex": { + "description": "Yandex Mail", + "domains": ["yandex.ru"], + "host": "smtp.yandex.ru", + "port": 465, + "secure": true + }, + + "Zimbra": { + "description": "Zimbra Mail Server", + "aliases": ["Zimbra Collaboration"], + "host": "smtp.zimbra.com", + "port": 587, + "requireTLS": true + }, + + "Zoho": { + "description": "Zoho Mail", + "host": "smtp.zoho.com", + "port": 465, + "secure": true, + "authMethod": "LOGIN" + } +} diff --git a/node_modules/nodemailer/lib/xoauth2/index.js b/node_modules/nodemailer/lib/xoauth2/index.js new file mode 100644 index 0000000..8c865c7 --- /dev/null +++ b/node_modules/nodemailer/lib/xoauth2/index.js @@ -0,0 +1,442 @@ +'use strict'; + +const Stream = require('stream').Stream; +const nmfetch = require('../fetch'); +const crypto = require('crypto'); +const shared = require('../shared'); +const errors = require('../errors'); + +/** + * XOAUTH2 access_token generator for Gmail. + * Create client ID for web applications in Google API console to use it. + * See Offline Access for receiving the needed refreshToken for an user + * https://developers.google.com/accounts/docs/OAuth2WebServer#offline + * + * Usage for generating access tokens with a custom method using provisionCallback: + * provisionCallback(user, renew, callback) + * * user is the username to get the token for + * * renew is a boolean that if true indicates that existing token failed and needs to be renewed + * * callback is the callback to run with (error, accessToken [, expires]) + * * accessToken is a string + * * expires is an optional expire time in milliseconds + * If provisionCallback is used, then Nodemailer does not try to attempt generating the token by itself + * + * @constructor + * @param {Object} options Client information for token generation + * @param {String} options.user User e-mail address + * @param {String} options.clientId Client ID value + * @param {String} options.clientSecret Client secret value + * @param {String} options.refreshToken Refresh token for an user + * @param {String} options.accessUrl Endpoint for token generation, defaults to 'https://accounts.google.com/o/oauth2/token' + * @param {String} options.accessToken An existing valid accessToken + * @param {String} options.privateKey Private key for JSW + * @param {Number} options.expires Optional Access Token expire time in ms + * @param {Number} options.timeout Optional TTL for Access Token in seconds + * @param {Function} options.provisionCallback Function to run when a new access token is required + */ +class XOAuth2 extends Stream { + constructor(options, logger) { + super(); + + this.options = options || {}; + + if (options && options.serviceClient) { + if (!options.privateKey || !options.user) { + let err = new Error('Options "privateKey" and "user" are required for service account!'); + err.code = errors.EOAUTH2; + setImmediate(() => this.emit('error', err)); + return; + } + + let serviceRequestTimeout = Math.min(Math.max(Number(this.options.serviceRequestTimeout) || 0, 0), 3600); + this.options.serviceRequestTimeout = serviceRequestTimeout || 5 * 60; + } + + this.logger = shared.getLogger( + { + logger + }, + { + component: this.options.component || 'OAuth2' + } + ); + + this.provisionCallback = typeof this.options.provisionCallback === 'function' ? this.options.provisionCallback : false; + + this.options.accessUrl = this.options.accessUrl || 'https://accounts.google.com/o/oauth2/token'; + this.options.customHeaders = this.options.customHeaders || {}; + this.options.customParams = this.options.customParams || {}; + + this.accessToken = this.options.accessToken || false; + + if (this.options.expires && Number(this.options.expires)) { + this.expires = this.options.expires; + } else { + let timeout = Math.max(Number(this.options.timeout) || 0, 0); + this.expires = (timeout && Date.now() + timeout * 1000) || 0; + } + + this.renewing = false; // Track if renewal is in progress + this.renewalQueue = []; // Queue for pending requests during renewal + } + + /** + * Returns or generates (if previous has expired) a XOAuth2 token + * + * @param {Boolean} renew If false then use cached access token (if available) + * @param {Function} callback Callback function with error object and token string + */ + getToken(renew, callback) { + if (!renew && this.accessToken && (!this.expires || this.expires > Date.now())) { + this.logger.debug( + { + tnx: 'OAUTH2', + user: this.options.user, + action: 'reuse' + }, + 'Reusing existing access token for %s', + this.options.user + ); + return callback(null, this.accessToken); + } + + // check if it is possible to renew, if not, return the current token or error + if (!this.provisionCallback && !this.options.refreshToken && !this.options.serviceClient) { + if (this.accessToken) { + this.logger.debug( + { + tnx: 'OAUTH2', + user: this.options.user, + action: 'reuse' + }, + 'Reusing existing access token (no refresh capability) for %s', + this.options.user + ); + return callback(null, this.accessToken); + } + this.logger.error( + { + tnx: 'OAUTH2', + user: this.options.user, + action: 'renew' + }, + 'Cannot renew access token for %s: No refresh mechanism available', + this.options.user + ); + let err = new Error("Can't create new access token for user"); + err.code = errors.EOAUTH2; + return callback(err); + } + + // If renewal already in progress, queue this request instead of starting another + if (this.renewing) { + return this.renewalQueue.push({ renew, callback }); + } + + this.renewing = true; + + // Handles token renewal completion - processes queued requests and cleans up + const generateCallback = (err, accessToken) => { + this.renewalQueue.forEach(item => item.callback(err, accessToken)); + this.renewalQueue = []; + this.renewing = false; + + if (err) { + this.logger.error( + { + err, + tnx: 'OAUTH2', + user: this.options.user, + action: 'renew' + }, + 'Failed generating new Access Token for %s', + this.options.user + ); + } else { + this.logger.info( + { + tnx: 'OAUTH2', + user: this.options.user, + action: 'renew' + }, + 'Generated new Access Token for %s', + this.options.user + ); + } + // Complete original request + callback(err, accessToken); + }; + + if (this.provisionCallback) { + this.provisionCallback(this.options.user, !!renew, (err, accessToken, expires) => { + if (!err && accessToken) { + this.accessToken = accessToken; + this.expires = expires || 0; + } + generateCallback(err, accessToken); + }); + } else { + this.generateToken(generateCallback); + } + } + + /** + * Updates token values + * + * @param {String} accessToken New access token + * @param {Number} timeout Access token lifetime in seconds + * + * Emits 'token': { user: User email-address, accessToken: the new accessToken, timeout: TTL in seconds} + */ + updateToken(accessToken, timeout) { + this.accessToken = accessToken; + timeout = Math.max(Number(timeout) || 0, 0); + this.expires = (timeout && Date.now() + timeout * 1000) || 0; + + this.emit('token', { + user: this.options.user, + accessToken: accessToken || '', + expires: this.expires + }); + } + + /** + * Generates a new XOAuth2 token with the credentials provided at initialization + * + * @param {Function} callback Callback function with error object and token string + */ + generateToken(callback) { + let urlOptions; + let loggedUrlOptions; + if (this.options.serviceClient) { + // service account - https://developers.google.com/identity/protocols/OAuth2ServiceAccount + let iat = Math.floor(Date.now() / 1000); // unix time + let tokenData = { + iss: this.options.serviceClient, + scope: this.options.scope || 'https://mail.google.com/', + sub: this.options.user, + aud: this.options.accessUrl, + iat, + exp: iat + this.options.serviceRequestTimeout + }; + let token; + try { + token = this.jwtSignRS256(tokenData); + } catch (_err) { + let err = new Error("Can't generate token. Check your auth options"); + err.code = errors.EOAUTH2; + return callback(err); + } + + urlOptions = { + grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', + assertion: token + }; + + loggedUrlOptions = { + grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', + assertion: tokenData + }; + } else { + if (!this.options.refreshToken) { + let err = new Error("Can't create new access token for user"); + err.code = errors.EOAUTH2; + return callback(err); + } + + // web app - https://developers.google.com/identity/protocols/OAuth2WebServer + urlOptions = { + client_id: this.options.clientId || '', + client_secret: this.options.clientSecret || '', + refresh_token: this.options.refreshToken, + grant_type: 'refresh_token' + }; + + loggedUrlOptions = { + client_id: this.options.clientId || '', + client_secret: (this.options.clientSecret || '').substr(0, 6) + '...', + refresh_token: (this.options.refreshToken || '').substr(0, 6) + '...', + grant_type: 'refresh_token' + }; + } + + Object.keys(this.options.customParams).forEach(key => { + urlOptions[key] = this.options.customParams[key]; + loggedUrlOptions[key] = this.options.customParams[key]; + }); + + this.logger.debug( + { + tnx: 'OAUTH2', + user: this.options.user, + action: 'generate' + }, + 'Requesting token using: %s', + JSON.stringify(loggedUrlOptions) + ); + + this.postRequest(this.options.accessUrl, urlOptions, this.options, (error, body) => { + let data; + + if (error) { + return callback(error); + } + + try { + data = JSON.parse(body.toString()); + } catch (E) { + return callback(E); + } + + if (!data || typeof data !== 'object') { + this.logger.debug( + { + tnx: 'OAUTH2', + user: this.options.user, + action: 'post' + }, + 'Response: %s', + (body || '').toString() + ); + let err = new Error('Invalid authentication response'); + err.code = errors.EOAUTH2; + return callback(err); + } + + let logData = {}; + Object.keys(data).forEach(key => { + if (key !== 'access_token') { + logData[key] = data[key]; + } else { + logData[key] = (data[key] || '').toString().substr(0, 6) + '...'; + } + }); + + this.logger.debug( + { + tnx: 'OAUTH2', + user: this.options.user, + action: 'post' + }, + 'Response: %s', + JSON.stringify(logData) + ); + + if (data.error) { + // Error Response : https://tools.ietf.org/html/rfc6749#section-5.2 + let errorMessage = data.error; + if (data.error_description) { + errorMessage += ': ' + data.error_description; + } + if (data.error_uri) { + errorMessage += ' (' + data.error_uri + ')'; + } + let err = new Error(errorMessage); + err.code = errors.EOAUTH2; + return callback(err); + } + + if (data.access_token) { + this.updateToken(data.access_token, data.expires_in); + return callback(null, this.accessToken); + } + + let err = new Error('No access token'); + err.code = errors.EOAUTH2; + return callback(err); + }); + } + + /** + * Converts an access_token and user id into a base64 encoded XOAuth2 token + * + * @param {String} [accessToken] Access token string + * @return {String} Base64 encoded token for IMAP or SMTP login + */ + buildXOAuth2Token(accessToken) { + let authData = ['user=' + (this.options.user || ''), 'auth=Bearer ' + (accessToken || this.accessToken), '', '']; + return Buffer.from(authData.join('\x01'), 'utf-8').toString('base64'); + } + + /** + * Custom POST request handler. + * This is only needed to keep paths short in Windows – usually this module + * is a dependency of a dependency and if it tries to require something + * like the request module the paths get way too long to handle for Windows. + * As we do only a simple POST request we do not actually require complicated + * logic support (no redirects, no nothing) anyway. + * + * @param {String} url Url to POST to + * @param {String|Buffer} payload Payload to POST + * @param {Function} callback Callback function with (err, buff) + */ + postRequest(url, payload, params, callback) { + let returned = false; + + let chunks = []; + let chunklen = 0; + + let req = nmfetch(url, { + method: 'post', + headers: params.customHeaders, + body: payload, + allowErrorResponse: true + }); + + req.on('readable', () => { + let chunk; + while ((chunk = req.read()) !== null) { + chunks.push(chunk); + chunklen += chunk.length; + } + }); + + req.once('error', err => { + if (returned) { + return; + } + returned = true; + return callback(err); + }); + + req.once('end', () => { + if (returned) { + return; + } + returned = true; + return callback(null, Buffer.concat(chunks, chunklen)); + }); + } + + /** + * Encodes a buffer or a string into Base64url format + * + * @param {Buffer|String} data The data to convert + * @return {String} The encoded string + */ + toBase64URL(data) { + if (typeof data === 'string') { + data = Buffer.from(data); + } + + return data + .toString('base64') + .replace(/[=]+/g, '') // remove '='s + .replace(/\+/g, '-') // '+' → '-' + .replace(/\//g, '_'); // '/' → '_' + } + + /** + * Creates a JSON Web Token signed with RS256 (SHA256 + RSA) + * + * @param {Object} payload The payload to include in the generated token + * @return {String} The generated and signed token + */ + jwtSignRS256(payload) { + payload = ['{"alg":"RS256","typ":"JWT"}', JSON.stringify(payload)].map(val => this.toBase64URL(val)).join('.'); + let signature = crypto.createSign('RSA-SHA256').update(payload).sign(this.options.privateKey); + return payload + '.' + this.toBase64URL(signature); + } +} + +module.exports = XOAuth2; diff --git a/node_modules/nodemailer/package.json b/node_modules/nodemailer/package.json new file mode 100644 index 0000000..ca95038 --- /dev/null +++ b/node_modules/nodemailer/package.json @@ -0,0 +1,47 @@ +{ + "name": "nodemailer", + "version": "8.0.2", + "description": "Easy as cake e-mail sending from your Node.js applications", + "main": "lib/nodemailer.js", + "scripts": { + "test": "node --test --test-concurrency=1 test/**/*.test.js test/**/*-test.js", + "test:coverage": "c8 node --test --test-concurrency=1 test/**/*.test.js test/**/*-test.js", + "format": "prettier --write \"**/*.{js,json,md}\"", + "format:check": "prettier --check \"**/*.{js,json,md}\"", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "update": "rm -rf node_modules/ package-lock.json && ncu -u && npm install" + }, + "repository": { + "type": "git", + "url": "https://github.com/nodemailer/nodemailer.git" + }, + "keywords": [ + "Nodemailer" + ], + "author": "Andris Reinman", + "license": "MIT-0", + "bugs": { + "url": "https://github.com/nodemailer/nodemailer/issues" + }, + "homepage": "https://nodemailer.com/", + "devDependencies": { + "@aws-sdk/client-sesv2": "3.1004.0", + "bunyan": "1.8.15", + "c8": "11.0.0", + "eslint": "10.0.3", + "eslint-config-prettier": "10.1.8", + "globals": "17.4.0", + "libbase64": "1.3.0", + "libmime": "5.3.7", + "libqp": "2.1.1", + "nodemailer-ntlm-auth": "1.0.4", + "prettier": "3.8.1", + "proxy": "1.0.2", + "proxy-test-server": "1.0.0", + "smtp-server": "3.18.1" + }, + "engines": { + "node": ">=6.0.0" + } +} diff --git a/package-lock.json b/package-lock.json index abbf510..878e344 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,10 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "bcrypt": "^6.0.0", "better-sqlite3": "^12.6.2", "compression": "^1.8.1", + "crypto": "^1.0.1", "csurf": "^1.11.0", "dotenv": "^17.3.1", "ejs": "^5.0.1", @@ -19,6 +21,7 @@ "express-session": "^1.19.0", "helmet": "^8.1.0", "mysql2": "^3.19.1", + "nodemailer": "^8.0.2", "sqlite3": "^5.1.7" } }, @@ -203,6 +206,29 @@ ], "license": "MIT" }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/bcrypt/node_modules/node-addon-api": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.6.0.tgz", + "integrity": "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/better-sqlite3": { "version": "12.6.2", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.6.2.tgz", @@ -501,6 +527,13 @@ "node": ">=6.6.0" } }, + "node_modules/crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", + "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.", + "license": "ISC" + }, "node_modules/csrf": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz", @@ -1750,6 +1783,26 @@ "node": ">= 10.12.0" } }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/nodemailer": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.2.tgz", + "integrity": "sha512-zbj002pZAIkWQFxyAaqoxvn+zoIwRnS40hgjqTXudKOOJkiFFgBeNqjgD3/YCR12sZnrghWYBY+yP1ZucdDRpw==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", diff --git a/package.json b/package.json index d3088af..69c65c1 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,10 @@ "author": "", "license": "ISC", "dependencies": { + "bcrypt": "^6.0.0", "better-sqlite3": "^12.6.2", "compression": "^1.8.1", + "crypto": "^1.0.1", "csurf": "^1.11.0", "dotenv": "^17.3.1", "ejs": "^5.0.1", @@ -19,6 +21,7 @@ "express-session": "^1.19.0", "helmet": "^8.1.0", "mysql2": "^3.19.1", + "nodemailer": "^8.0.2", "sqlite3": "^5.1.7" }, "description": "" diff --git a/public/favicon/dok_favicon_128px.ico b/public/favicon/dok_favicon_128px.ico new file mode 100644 index 0000000..96eb9f6 Binary files /dev/null and b/public/favicon/dok_favicon_128px.ico differ diff --git a/public/favicon/dok_favicon_16px.ico b/public/favicon/dok_favicon_16px.ico new file mode 100644 index 0000000..1621d45 Binary files /dev/null and b/public/favicon/dok_favicon_16px.ico differ diff --git a/public/favicon/dok_favicon_32px.ico b/public/favicon/dok_favicon_32px.ico new file mode 100644 index 0000000..0f0bd72 Binary files /dev/null and b/public/favicon/dok_favicon_32px.ico differ diff --git a/public/favicon/dok_favicon_64px.ico b/public/favicon/dok_favicon_64px.ico new file mode 100644 index 0000000..b5f99d3 Binary files /dev/null and b/public/favicon/dok_favicon_64px.ico differ diff --git a/font/OFL.txt b/public/fonts/OFL.txt similarity index 100% rename from font/OFL.txt rename to public/fonts/OFL.txt diff --git a/font/Tangerine-Bold.ttf b/public/fonts/Tangerine-Bold.ttf similarity index 100% rename from font/Tangerine-Bold.ttf rename to public/fonts/Tangerine-Bold.ttf diff --git a/font/Tangerine-Bold.woff2 b/public/fonts/Tangerine-Bold.woff2 similarity index 100% rename from font/Tangerine-Bold.woff2 rename to public/fonts/Tangerine-Bold.woff2 diff --git a/font/Tangerine-Regular.ttf b/public/fonts/Tangerine-Regular.ttf similarity index 100% rename from font/Tangerine-Regular.ttf rename to public/fonts/Tangerine-Regular.ttf diff --git a/font/Tangerine-Regular.woff2 b/public/fonts/Tangerine-Regular.woff2 similarity index 100% rename from font/Tangerine-Regular.woff2 rename to public/fonts/Tangerine-Regular.woff2 diff --git a/images/background_login.png b/public/images/background_login.png similarity index 100% rename from images/background_login.png rename to public/images/background_login.png diff --git a/images/logo.png b/public/images/logo.png similarity index 100% rename from images/logo.png rename to public/images/logo.png diff --git a/routes/register.js b/routes/register.js new file mode 100644 index 0000000..fc0c3df --- /dev/null +++ b/routes/register.js @@ -0,0 +1,115 @@ +/*================================ + Registrierung Route +===============================*/ + +const express = require("express"); +const router = express.Router(); +const db = require("../database/database"); +const bcrypt = require("bcrypt"); +const rateLimit = require("express-rate-limit"); +const crypto = require("crypto"); +const mailer = require("../utils/mailer"); + +/*================================ + Rate Limiter für Registrierung +===============================*/ + +const registerLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 Minuten + max: 5, // max 5 Registrierungen pro IP + message: "Zu viele Registrierungen. Bitte später erneut versuchen.", +}); + +/*================================ + Register Seite anzeigen +===============================*/ + +router.get("/", async (req, res) => { + try { + const [servers] = await db.query("SELECT * FROM servers"); + + res.render("register", { + servers, + }); + } catch (error) { + console.error(error); + res.send("Server konnten nicht geladen werden"); + } +}); + +/*================================ + Registrierung speichern +===============================*/ + +router.post("/", registerLimiter, async (req, res) => { + const { username, password, email, server_id } = req.body; + const verifyToken = crypto.randomBytes(32).toString("hex"); + + try { + /* Username Filter */ + const usernameRegex = /^[a-zA-Z0-9_]{3,20}$/; + + if (!usernameRegex.test(username)) { + const [servers] = await db.query("SELECT * FROM servers"); + + return res.render("register", { + servers, + error: + "Username darf nur Buchstaben, Zahlen und _ enthalten (3-20 Zeichen).", + }); + } + + /* Passwort Länge prüfen */ + if (password.length < 6) { + const [servers] = await db.query("SELECT * FROM servers"); + + return res.render("register", { + servers, + error: "Passwort muss mindestens 6 Zeichen lang sein.", + }); + } + + /* Prüfen ob Username existiert */ + const [existingUser] = await db.query( + "SELECT id FROM accounts WHERE username = ?", + [username], + ); + + if (existingUser.length > 0) { + const [servers] = await db.query("SELECT * FROM servers"); + + return res.render("register", { + servers, + error: "Dieser Loginname existiert bereits.", + }); + } + + /* Passwort verschlüsseln */ + const hashedPassword = await bcrypt.hash(password, 10); + + /* Account speichern */ + await db.query( + "INSERT INTO accounts (username,password,email,server_id,verify_token) VALUES (?,?,?,?,?)", + [username, hashedPassword, email, server_id, verifyToken], + ); + + const verifyLink = `${process.env.APP_URL}/verify/${verifyToken}`; + await mailer.sendMail({ + from: '"Dynasty of Knights" ', + to: email, + subject: "Account Aktivierung", + html: ` +

Dynasty of Knights

+

Bitte bestätige deine Registrierung:

+ ${verifyLink} + `, + }); + + res.redirect("/"); + } catch (error) { + console.error(error); + res.send("Registrierung fehlgeschlagen"); + } +}); + +module.exports = router; diff --git a/routes/servers.js b/routes/servers.js index 4d11d57..4bdf101 100644 --- a/routes/servers.js +++ b/routes/servers.js @@ -4,6 +4,9 @@ const express = require("express"); const router = express.Router(); const db = require("../database/database"); +/*================================ +Laden der Server welche Online sind und einfach alle anderen als Offline anzeigen +===============================*/ router.get("/", async (req, res) => { try { const [servers] = await db.query("SELECT * FROM servers"); @@ -22,5 +25,5 @@ router.get("/", async (req, res) => { res.send("Datenbankfehler"); } }); -//testing + module.exports = router; diff --git a/routes/verify.js b/routes/verify.js new file mode 100644 index 0000000..45151fe --- /dev/null +++ b/routes/verify.js @@ -0,0 +1,25 @@ +const express = require("express"); +const router = express.Router(); +const db = require("../database/database"); + +router.get("/:token", async (req, res) => { + const token = req.params.token; + + const [user] = await db.query( + "SELECT id FROM accounts WHERE verify_token = ?", + [token], + ); + + if (user.length === 0) { + return res.send("Ungültiger Bestätigungslink."); + } + + await db.query( + "UPDATE accounts SET verified = TRUE, verify_token = NULL WHERE id = ?", + [user[0].id], + ); + + res.send("Account erfolgreich aktiviert."); +}); + +module.exports = router; diff --git a/utils/mailer.js b/utils/mailer.js new file mode 100644 index 0000000..bf2267e --- /dev/null +++ b/utils/mailer.js @@ -0,0 +1,13 @@ +const nodemailer = require("nodemailer"); + +const transporter = nodemailer.createTransport({ + host: "mail.dynastyofknights.com", + port: 587, + secure: false, + auth: { + user: "register@dynastyofknights.com", + pass: "111168-j-62217DwmbwPK", + }, +}); + +module.exports = transporter; diff --git a/views/index.ejs b/views/index.ejs index 1537474..da19dd9 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -3,6 +3,10 @@ Dynasty of Knights + + + + + + + + + +
+ +

Account Registrierung

+ +<% if (typeof error !== "undefined") { %> +
+<%= error %> +
+<% } %> + +
+ + + + + + + + + + + +
+ + +Zurück zum Login + + +
+ + + \ No newline at end of file