first commit
29
colors
Executable file
@ -0,0 +1,29 @@
|
||||
| Purpose | Color (Hex) | Notes |
|
||||
| ----------------------------- | ----------- | ------------------------------------- |
|
||||
| Primary (buttons, highlights) | `#4A90E2` | Bright blue — friendly & trustworthy |
|
||||
| Secondary (accent, links) | `#50E3C2` | Soft teal — fresh & modern |
|
||||
| Background (main) | `#F9F9F9` | Very light gray — clean & minimal |
|
||||
| Card/Panel Background | `#FFFFFF` | White — keeps focus on content |
|
||||
| Text Primary | `#333333` | Dark gray — easy to read |
|
||||
| Text Secondary | `#777777` | Medium gray — for less important text |
|
||||
| Error / Warning | `#E94E4E` | Red — for alerts or invalid inputs |
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="inputs" id="form">
|
||||
<input type="text" placeholder="Name" id="name">
|
||||
<input type="email" placeholder="Email" id="email">
|
||||
<input type="tel" placeholder="Phone" id="phone">
|
||||
<div class="pickers-row">
|
||||
<input class="calendar" type="hidden" id="ranged">
|
||||
<div style="display: flex; flex-direction: column; align-items: center; margin-right: 3vh;">
|
||||
<p style="font-size: 20px; margin: 0;">Select Time</p>
|
||||
<p style="font-size: 14px; margin-top: 0; margin-bottom: 20;">(08:00 - 17:00)</p>
|
||||
<div class="picker-container">
|
||||
<div class="picker" id="hour-picker"></div>
|
||||
<div class="highlight"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
41
docker-compose.yaml
Normal file
@ -0,0 +1,41 @@
|
||||
services:
|
||||
backend:
|
||||
build: ./server
|
||||
container_name: bookingSys
|
||||
ports:
|
||||
- "3000:3000"
|
||||
depends_on:
|
||||
- db
|
||||
volumes:
|
||||
- ./public:/app/public
|
||||
environment:
|
||||
- PGUSER=aleks
|
||||
- PGPASSWORD=moni3niki
|
||||
- PGDATABASE=bookingsystem
|
||||
- PGHOST=db
|
||||
- PGPORT=5432
|
||||
networks:
|
||||
- booking_system_net
|
||||
|
||||
db:
|
||||
image: postgres:15
|
||||
container_name: bookingsys_db
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_USER: aleks
|
||||
POSTGRES_PASSWORD: moni3niki
|
||||
POSTGRES_DB: bookingsystem
|
||||
ports:
|
||||
- "5433:5432"
|
||||
volumes:
|
||||
- db_data:/var/lib/postgresql/data
|
||||
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
networks:
|
||||
- booking_system_net
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
|
||||
networks:
|
||||
booking_system_net:
|
||||
external: true
|
||||
28
node_modules/.package-lock.json
generated
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "Test",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"node_modules/dotenv": {
|
||||
"version": "17.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz",
|
||||
"integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.5.tgz",
|
||||
"integrity": "sha512-nsrh2lO3j4GkLLXoeEksAMgAOqxOv6QumNRVQTJwKH4nuiww6iC2y7GyANs9kRAxCexg3+lTWM3PZ91iLlVjfg==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
586
node_modules/dotenv/CHANGELOG.md
generated
vendored
Normal file
@ -0,0 +1,586 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
## [Unreleased](https://github.com/motdotla/dotenv/compare/v17.2.1...master)
|
||||
|
||||
## [17.2.1](https://github.com/motdotla/dotenv/compare/v17.2.0...v17.2.1) (2025-07-24)
|
||||
|
||||
### Changed
|
||||
|
||||
* Fix clickable tip links by removing parentheses ([#897](https://github.com/motdotla/dotenv/pull/897))
|
||||
|
||||
## [17.2.0](https://github.com/motdotla/dotenv/compare/v17.1.0...v17.2.0) (2025-07-09)
|
||||
|
||||
### Added
|
||||
|
||||
* Optionally specify `DOTENV_CONFIG_QUIET=true` in your environment or `.env` file to quiet the runtime log ([#889](https://github.com/motdotla/dotenv/pull/889))
|
||||
* Just like dotenv any `DOTENV_CONFIG_` environment variables take precedence over any code set options like `({quiet: false})`
|
||||
|
||||
```ini
|
||||
# .env
|
||||
DOTENV_CONFIG_QUIET=true
|
||||
HELLO="World"
|
||||
```
|
||||
```js
|
||||
// index.js
|
||||
require('dotenv').config()
|
||||
console.log(`Hello ${process.env.HELLO}`)
|
||||
```
|
||||
```sh
|
||||
$ node index.js
|
||||
Hello World
|
||||
|
||||
or
|
||||
|
||||
$ DOTENV_CONFIG_QUIET=true node index.js
|
||||
```
|
||||
|
||||
## [17.1.0](https://github.com/motdotla/dotenv/compare/v17.0.1...v17.1.0) (2025-07-07)
|
||||
|
||||
### Added
|
||||
|
||||
* Add additional security and configuration tips to the runtime log ([#884](https://github.com/motdotla/dotenv/pull/884))
|
||||
* Dim the tips text from the main injection information text
|
||||
|
||||
```js
|
||||
const TIPS = [
|
||||
'🔐 encrypt with dotenvx: https://dotenvx.com',
|
||||
'🔐 prevent committing .env to code: https://dotenvx.com/precommit',
|
||||
'🔐 prevent building .env in docker: https://dotenvx.com/prebuild',
|
||||
'🛠️ run anywhere with `dotenvx run -- yourcommand`',
|
||||
'⚙️ specify custom .env file path with { path: \'/custom/path/.env\' }',
|
||||
'⚙️ enable debug logging with { debug: true }',
|
||||
'⚙️ override existing env vars with { override: true }',
|
||||
'⚙️ suppress all logs with { quiet: true }',
|
||||
'⚙️ write to custom object with { processEnv: myObject }',
|
||||
'⚙️ load multiple .env files with { path: [\'.env.local\', \'.env\'] }'
|
||||
]
|
||||
```
|
||||
|
||||
## [17.0.1](https://github.com/motdotla/dotenv/compare/v17.0.0...v17.0.1) (2025-07-01)
|
||||
|
||||
### Changed
|
||||
|
||||
* Patched injected log to count only populated/set keys to process.env ([#879](https://github.com/motdotla/dotenv/pull/879))
|
||||
|
||||
## [17.0.0](https://github.com/motdotla/dotenv/compare/v16.6.1...v17.0.0) (2025-06-27)
|
||||
|
||||
### Changed
|
||||
|
||||
- Default `quiet` to false - informational (file and keys count) runtime log message shows by default ([#875](https://github.com/motdotla/dotenv/pull/875))
|
||||
|
||||
## [16.6.1](https://github.com/motdotla/dotenv/compare/v16.6.0...v16.6.1) (2025-06-27)
|
||||
|
||||
### Changed
|
||||
|
||||
- Default `quiet` to true – hiding the runtime log message ([#874](https://github.com/motdotla/dotenv/pull/874))
|
||||
- NOTICE: 17.0.0 will be released with quiet defaulting to false. Use `config({ quiet: true })` to suppress.
|
||||
- And check out the new [dotenvx](https://github.com/dotenvx/dotenvx). As coding workflows evolve and agents increasingly handle secrets, encrypted .env files offer a much safer way to deploy both agents and code together with secure secrets. Simply switch `require('dotenv').config()` for `require('@dotenvx/dotenvx').config()`.
|
||||
|
||||
## [16.6.0](https://github.com/motdotla/dotenv/compare/v16.5.0...v16.6.0) (2025-06-26)
|
||||
|
||||
### Added
|
||||
|
||||
- Default log helpful message `[dotenv@16.6.0] injecting env (1) from .env` ([#870](https://github.com/motdotla/dotenv/pull/870))
|
||||
- Use `{ quiet: true }` to suppress
|
||||
- Aligns dotenv more closely with [dotenvx](https://github.com/dotenvx/dotenvx).
|
||||
|
||||
## [16.5.0](https://github.com/motdotla/dotenv/compare/v16.4.7...v16.5.0) (2025-04-07)
|
||||
|
||||
### Added
|
||||
|
||||
- 🎉 Added new sponsor [Graphite](https://graphite.dev/?utm_source=github&utm_medium=repo&utm_campaign=dotenv) - *the AI developer productivity platform helping teams on GitHub ship higher quality software, faster*.
|
||||
|
||||
> [!TIP]
|
||||
> **[Become a sponsor](https://github.com/sponsors/motdotla)**
|
||||
>
|
||||
> The dotenvx README is viewed thousands of times DAILY on GitHub and NPM.
|
||||
> Sponsoring dotenv is a great way to get in front of developers and give back to the developer community at the same time.
|
||||
|
||||
### Changed
|
||||
|
||||
- Remove `_log` method. Use `_debug` [#862](https://github.com/motdotla/dotenv/pull/862)
|
||||
|
||||
## [16.4.7](https://github.com/motdotla/dotenv/compare/v16.4.6...v16.4.7) (2024-12-03)
|
||||
|
||||
### Changed
|
||||
|
||||
- Ignore `.tap` folder when publishing. (oops, sorry about that everyone. - @motdotla) [#848](https://github.com/motdotla/dotenv/pull/848)
|
||||
|
||||
## [16.4.6](https://github.com/motdotla/dotenv/compare/v16.4.5...v16.4.6) (2024-12-02)
|
||||
|
||||
### Changed
|
||||
|
||||
- Clean up stale dev dependencies [#847](https://github.com/motdotla/dotenv/pull/847)
|
||||
- Various README updates clarifying usage and alternative solutions using [dotenvx](https://github.com/dotenvx/dotenvx)
|
||||
|
||||
## [16.4.5](https://github.com/motdotla/dotenv/compare/v16.4.4...v16.4.5) (2024-02-19)
|
||||
|
||||
### Changed
|
||||
|
||||
- 🐞 Fix recent regression when using `path` option. return to historical behavior: do not attempt to auto find `.env` if `path` set. (regression was introduced in `16.4.3`) [#814](https://github.com/motdotla/dotenv/pull/814)
|
||||
|
||||
## [16.4.4](https://github.com/motdotla/dotenv/compare/v16.4.3...v16.4.4) (2024-02-13)
|
||||
|
||||
### Changed
|
||||
|
||||
- 🐞 Replaced chaining operator `?.` with old school `&&` (fixing node 12 failures) [#812](https://github.com/motdotla/dotenv/pull/812)
|
||||
|
||||
## [16.4.3](https://github.com/motdotla/dotenv/compare/v16.4.2...v16.4.3) (2024-02-12)
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed processing of multiple files in `options.path` [#805](https://github.com/motdotla/dotenv/pull/805)
|
||||
|
||||
## [16.4.2](https://github.com/motdotla/dotenv/compare/v16.4.1...v16.4.2) (2024-02-10)
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed funding link in package.json to [`dotenvx.com`](https://dotenvx.com)
|
||||
|
||||
## [16.4.1](https://github.com/motdotla/dotenv/compare/v16.4.0...v16.4.1) (2024-01-24)
|
||||
|
||||
- Patch support for array as `path` option [#797](https://github.com/motdotla/dotenv/pull/797)
|
||||
|
||||
## [16.4.0](https://github.com/motdotla/dotenv/compare/v16.3.2...v16.4.0) (2024-01-23)
|
||||
|
||||
- Add `error.code` to error messages around `.env.vault` decryption handling [#795](https://github.com/motdotla/dotenv/pull/795)
|
||||
- Add ability to find `.env.vault` file when filename(s) passed as an array [#784](https://github.com/motdotla/dotenv/pull/784)
|
||||
|
||||
## [16.3.2](https://github.com/motdotla/dotenv/compare/v16.3.1...v16.3.2) (2024-01-18)
|
||||
|
||||
### Added
|
||||
|
||||
- Add debug message when no encoding set [#735](https://github.com/motdotla/dotenv/pull/735)
|
||||
|
||||
### Changed
|
||||
|
||||
- Fix output typing for `populate` [#792](https://github.com/motdotla/dotenv/pull/792)
|
||||
- Use subarray instead of slice [#793](https://github.com/motdotla/dotenv/pull/793)
|
||||
|
||||
## [16.3.1](https://github.com/motdotla/dotenv/compare/v16.3.0...v16.3.1) (2023-06-17)
|
||||
|
||||
### Added
|
||||
|
||||
- Add missing type definitions for `processEnv` and `DOTENV_KEY` options. [#756](https://github.com/motdotla/dotenv/pull/756)
|
||||
|
||||
## [16.3.0](https://github.com/motdotla/dotenv/compare/v16.2.0...v16.3.0) (2023-06-16)
|
||||
|
||||
### Added
|
||||
|
||||
- Optionally pass `DOTENV_KEY` to options rather than relying on `process.env.DOTENV_KEY`. Defaults to `process.env.DOTENV_KEY` [#754](https://github.com/motdotla/dotenv/pull/754)
|
||||
|
||||
## [16.2.0](https://github.com/motdotla/dotenv/compare/v16.1.4...v16.2.0) (2023-06-15)
|
||||
|
||||
### Added
|
||||
|
||||
- Optionally write to your own target object rather than `process.env`. Defaults to `process.env`. [#753](https://github.com/motdotla/dotenv/pull/753)
|
||||
- Add import type URL to types file [#751](https://github.com/motdotla/dotenv/pull/751)
|
||||
|
||||
## [16.1.4](https://github.com/motdotla/dotenv/compare/v16.1.3...v16.1.4) (2023-06-04)
|
||||
|
||||
### Added
|
||||
|
||||
- Added `.github/` to `.npmignore` [#747](https://github.com/motdotla/dotenv/pull/747)
|
||||
|
||||
## [16.1.3](https://github.com/motdotla/dotenv/compare/v16.1.2...v16.1.3) (2023-05-31)
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed `browser` keys for `path`, `os`, and `crypto` in package.json. These were set to false incorrectly as of 16.1. Instead, if using dotenv on the front-end make sure to include polyfills for `path`, `os`, and `crypto`. [node-polyfill-webpack-plugin](https://github.com/Richienb/node-polyfill-webpack-plugin) provides these.
|
||||
|
||||
## [16.1.2](https://github.com/motdotla/dotenv/compare/v16.1.1...v16.1.2) (2023-05-31)
|
||||
|
||||
### Changed
|
||||
|
||||
- Exposed private function `_configDotenv` as `configDotenv`. [#744](https://github.com/motdotla/dotenv/pull/744)
|
||||
|
||||
## [16.1.1](https://github.com/motdotla/dotenv/compare/v16.1.0...v16.1.1) (2023-05-30)
|
||||
|
||||
### Added
|
||||
|
||||
- Added type definition for `decrypt` function
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed `{crypto: false}` in `packageJson.browser`
|
||||
|
||||
## [16.1.0](https://github.com/motdotla/dotenv/compare/v16.0.3...v16.1.0) (2023-05-30)
|
||||
|
||||
### Added
|
||||
|
||||
- Add `populate` convenience method [#733](https://github.com/motdotla/dotenv/pull/733)
|
||||
- Accept URL as path option [#720](https://github.com/motdotla/dotenv/pull/720)
|
||||
- Add dotenv to `npm fund` command
|
||||
- Spanish language README [#698](https://github.com/motdotla/dotenv/pull/698)
|
||||
- Add `.env.vault` support. 🎉 ([#730](https://github.com/motdotla/dotenv/pull/730))
|
||||
|
||||
ℹ️ `.env.vault` extends the `.env` file format standard with a localized encrypted vault file. Package it securely with your production code deploys. It's cloud agnostic so that you can deploy your secrets anywhere – without [risky third-party integrations](https://techcrunch.com/2023/01/05/circleci-breach/). [read more](https://github.com/motdotla/dotenv#-deploying)
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed "cannot resolve 'fs'" error on tools like Replit [#693](https://github.com/motdotla/dotenv/pull/693)
|
||||
|
||||
## [16.0.3](https://github.com/motdotla/dotenv/compare/v16.0.2...v16.0.3) (2022-09-29)
|
||||
|
||||
### Changed
|
||||
|
||||
- Added library version to debug logs ([#682](https://github.com/motdotla/dotenv/pull/682))
|
||||
|
||||
## [16.0.2](https://github.com/motdotla/dotenv/compare/v16.0.1...v16.0.2) (2022-08-30)
|
||||
|
||||
### Added
|
||||
|
||||
- Export `env-options.js` and `cli-options.js` in package.json for use with downstream [dotenv-expand](https://github.com/motdotla/dotenv-expand) module
|
||||
|
||||
## [16.0.1](https://github.com/motdotla/dotenv/compare/v16.0.0...v16.0.1) (2022-05-10)
|
||||
|
||||
### Changed
|
||||
|
||||
- Minor README clarifications
|
||||
- Development ONLY: updated devDependencies as recommended for development only security risks ([#658](https://github.com/motdotla/dotenv/pull/658))
|
||||
|
||||
## [16.0.0](https://github.com/motdotla/dotenv/compare/v15.0.1...v16.0.0) (2022-02-02)
|
||||
|
||||
### Added
|
||||
|
||||
- _Breaking:_ Backtick support 🎉 ([#615](https://github.com/motdotla/dotenv/pull/615))
|
||||
|
||||
If you had values containing the backtick character, please quote those values with either single or double quotes.
|
||||
|
||||
## [15.0.1](https://github.com/motdotla/dotenv/compare/v15.0.0...v15.0.1) (2022-02-02)
|
||||
|
||||
### Changed
|
||||
|
||||
- Properly parse empty single or double quoted values 🐞 ([#614](https://github.com/motdotla/dotenv/pull/614))
|
||||
|
||||
## [15.0.0](https://github.com/motdotla/dotenv/compare/v14.3.2...v15.0.0) (2022-01-31)
|
||||
|
||||
`v15.0.0` is a major new release with some important breaking changes.
|
||||
|
||||
### Added
|
||||
|
||||
- _Breaking:_ Multiline parsing support (just works. no need for the flag.)
|
||||
|
||||
### Changed
|
||||
|
||||
- _Breaking:_ `#` marks the beginning of a comment (UNLESS the value is wrapped in quotes. Please update your `.env` files to wrap in quotes any values containing `#`. For example: `SECRET_HASH="something-with-a-#-hash"`).
|
||||
|
||||
..Understandably, (as some teams have noted) this is tedious to do across the entire team. To make it less tedious, we recommend using [dotenv cli](https://github.com/dotenv-org/cli) going forward. It's an optional plugin that will keep your `.env` files in sync between machines, environments, or team members.
|
||||
|
||||
### Removed
|
||||
|
||||
- _Breaking:_ Remove multiline option (just works out of the box now. no need for the flag.)
|
||||
|
||||
## [14.3.2](https://github.com/motdotla/dotenv/compare/v14.3.1...v14.3.2) (2022-01-25)
|
||||
|
||||
### Changed
|
||||
|
||||
- Preserve backwards compatibility on values containing `#` 🐞 ([#603](https://github.com/motdotla/dotenv/pull/603))
|
||||
|
||||
## [14.3.1](https://github.com/motdotla/dotenv/compare/v14.3.0...v14.3.1) (2022-01-25)
|
||||
|
||||
### Changed
|
||||
|
||||
- Preserve backwards compatibility on exports by re-introducing the prior in-place exports 🐞 ([#606](https://github.com/motdotla/dotenv/pull/606))
|
||||
|
||||
## [14.3.0](https://github.com/motdotla/dotenv/compare/v14.2.0...v14.3.0) (2022-01-24)
|
||||
|
||||
### Added
|
||||
|
||||
- Add `multiline` option 🎉 ([#486](https://github.com/motdotla/dotenv/pull/486))
|
||||
|
||||
## [14.2.0](https://github.com/motdotla/dotenv/compare/v14.1.1...v14.2.0) (2022-01-17)
|
||||
|
||||
### Added
|
||||
|
||||
- Add `dotenv_config_override` cli option
|
||||
- Add `DOTENV_CONFIG_OVERRIDE` command line env option
|
||||
|
||||
## [14.1.1](https://github.com/motdotla/dotenv/compare/v14.1.0...v14.1.1) (2022-01-17)
|
||||
|
||||
### Added
|
||||
|
||||
- Add React gotcha to FAQ on README
|
||||
|
||||
## [14.1.0](https://github.com/motdotla/dotenv/compare/v14.0.1...v14.1.0) (2022-01-17)
|
||||
|
||||
### Added
|
||||
|
||||
- Add `override` option 🎉 ([#595](https://github.com/motdotla/dotenv/pull/595))
|
||||
|
||||
## [14.0.1](https://github.com/motdotla/dotenv/compare/v14.0.0...v14.0.1) (2022-01-16)
|
||||
|
||||
### Added
|
||||
|
||||
- Log error on failure to load `.env` file ([#594](https://github.com/motdotla/dotenv/pull/594))
|
||||
|
||||
## [14.0.0](https://github.com/motdotla/dotenv/compare/v13.0.1...v14.0.0) (2022-01-16)
|
||||
|
||||
### Added
|
||||
|
||||
- _Breaking:_ Support inline comments for the parser 🎉 ([#568](https://github.com/motdotla/dotenv/pull/568))
|
||||
|
||||
## [13.0.1](https://github.com/motdotla/dotenv/compare/v13.0.0...v13.0.1) (2022-01-16)
|
||||
|
||||
### Changed
|
||||
|
||||
* Hide comments and newlines from debug output ([#404](https://github.com/motdotla/dotenv/pull/404))
|
||||
|
||||
## [13.0.0](https://github.com/motdotla/dotenv/compare/v12.0.4...v13.0.0) (2022-01-16)
|
||||
|
||||
### Added
|
||||
|
||||
* _Breaking:_ Add type file for `config.js` ([#539](https://github.com/motdotla/dotenv/pull/539))
|
||||
|
||||
## [12.0.4](https://github.com/motdotla/dotenv/compare/v12.0.3...v12.0.4) (2022-01-16)
|
||||
|
||||
### Changed
|
||||
|
||||
* README updates
|
||||
* Minor order adjustment to package json format
|
||||
|
||||
## [12.0.3](https://github.com/motdotla/dotenv/compare/v12.0.2...v12.0.3) (2022-01-15)
|
||||
|
||||
### Changed
|
||||
|
||||
* Simplified jsdoc for consistency across editors
|
||||
|
||||
## [12.0.2](https://github.com/motdotla/dotenv/compare/v12.0.1...v12.0.2) (2022-01-15)
|
||||
|
||||
### Changed
|
||||
|
||||
* Improve embedded jsdoc type documentation
|
||||
|
||||
## [12.0.1](https://github.com/motdotla/dotenv/compare/v12.0.0...v12.0.1) (2022-01-15)
|
||||
|
||||
### Changed
|
||||
|
||||
* README updates and clarifications
|
||||
|
||||
## [12.0.0](https://github.com/motdotla/dotenv/compare/v11.0.0...v12.0.0) (2022-01-15)
|
||||
|
||||
### Removed
|
||||
|
||||
- _Breaking:_ drop support for Flow static type checker ([#584](https://github.com/motdotla/dotenv/pull/584))
|
||||
|
||||
### Changed
|
||||
|
||||
- Move types/index.d.ts to lib/main.d.ts ([#585](https://github.com/motdotla/dotenv/pull/585))
|
||||
- Typescript cleanup ([#587](https://github.com/motdotla/dotenv/pull/587))
|
||||
- Explicit typescript inclusion in package.json ([#566](https://github.com/motdotla/dotenv/pull/566))
|
||||
|
||||
## [11.0.0](https://github.com/motdotla/dotenv/compare/v10.0.0...v11.0.0) (2022-01-11)
|
||||
|
||||
### Changed
|
||||
|
||||
- _Breaking:_ drop support for Node v10 ([#558](https://github.com/motdotla/dotenv/pull/558))
|
||||
- Patch debug option ([#550](https://github.com/motdotla/dotenv/pull/550))
|
||||
|
||||
## [10.0.0](https://github.com/motdotla/dotenv/compare/v9.0.2...v10.0.0) (2021-05-20)
|
||||
|
||||
### Added
|
||||
|
||||
- Add generic support to parse function
|
||||
- Allow for import "dotenv/config.js"
|
||||
- Add support to resolve home directory in path via ~
|
||||
|
||||
## [9.0.2](https://github.com/motdotla/dotenv/compare/v9.0.1...v9.0.2) (2021-05-10)
|
||||
|
||||
### Changed
|
||||
|
||||
- Support windows newlines with debug mode
|
||||
|
||||
## [9.0.1](https://github.com/motdotla/dotenv/compare/v9.0.0...v9.0.1) (2021-05-08)
|
||||
|
||||
### Changed
|
||||
|
||||
- Updates to README
|
||||
|
||||
## [9.0.0](https://github.com/motdotla/dotenv/compare/v8.6.0...v9.0.0) (2021-05-05)
|
||||
|
||||
### Changed
|
||||
|
||||
- _Breaking:_ drop support for Node v8
|
||||
|
||||
## [8.6.0](https://github.com/motdotla/dotenv/compare/v8.5.1...v8.6.0) (2021-05-05)
|
||||
|
||||
### Added
|
||||
|
||||
- define package.json in exports
|
||||
|
||||
## [8.5.1](https://github.com/motdotla/dotenv/compare/v8.5.0...v8.5.1) (2021-05-05)
|
||||
|
||||
### Changed
|
||||
|
||||
- updated dev dependencies via npm audit
|
||||
|
||||
## [8.5.0](https://github.com/motdotla/dotenv/compare/v8.4.0...v8.5.0) (2021-05-05)
|
||||
|
||||
### Added
|
||||
|
||||
- allow for `import "dotenv/config"`
|
||||
|
||||
## [8.4.0](https://github.com/motdotla/dotenv/compare/v8.3.0...v8.4.0) (2021-05-05)
|
||||
|
||||
### Changed
|
||||
|
||||
- point to exact types file to work with VS Code
|
||||
|
||||
## [8.3.0](https://github.com/motdotla/dotenv/compare/v8.2.0...v8.3.0) (2021-05-05)
|
||||
|
||||
### Changed
|
||||
|
||||
- _Breaking:_ drop support for Node v8 (mistake to be released as minor bump. later bumped to 9.0.0. see above.)
|
||||
|
||||
## [8.2.0](https://github.com/motdotla/dotenv/compare/v8.1.0...v8.2.0) (2019-10-16)
|
||||
|
||||
### Added
|
||||
|
||||
- TypeScript types
|
||||
|
||||
## [8.1.0](https://github.com/motdotla/dotenv/compare/v8.0.0...v8.1.0) (2019-08-18)
|
||||
|
||||
### Changed
|
||||
|
||||
- _Breaking:_ drop support for Node v6 ([#392](https://github.com/motdotla/dotenv/issues/392))
|
||||
|
||||
# [8.0.0](https://github.com/motdotla/dotenv/compare/v7.0.0...v8.0.0) (2019-05-02)
|
||||
|
||||
### Changed
|
||||
|
||||
- _Breaking:_ drop support for Node v6 ([#302](https://github.com/motdotla/dotenv/issues/392))
|
||||
|
||||
## [7.0.0] - 2019-03-12
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix removing unbalanced quotes ([#376](https://github.com/motdotla/dotenv/pull/376))
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed `load` alias for `config` for consistency throughout code and documentation.
|
||||
|
||||
## [6.2.0] - 2018-12-03
|
||||
|
||||
### Added
|
||||
|
||||
- Support preload configuration via environment variables ([#351](https://github.com/motdotla/dotenv/issues/351))
|
||||
|
||||
## [6.1.0] - 2018-10-08
|
||||
|
||||
### Added
|
||||
|
||||
- `debug` option for `config` and `parse` methods will turn on logging
|
||||
|
||||
## [6.0.0] - 2018-06-02
|
||||
|
||||
### Changed
|
||||
|
||||
- _Breaking:_ drop support for Node v4 ([#304](https://github.com/motdotla/dotenv/pull/304))
|
||||
|
||||
## [5.0.0] - 2018-01-29
|
||||
|
||||
### Added
|
||||
|
||||
- Testing against Node v8 and v9
|
||||
- Documentation on trim behavior of values
|
||||
- Documentation on how to use with `import`
|
||||
|
||||
### Changed
|
||||
|
||||
- _Breaking_: default `path` is now `path.resolve(process.cwd(), '.env')`
|
||||
- _Breaking_: does not write over keys already in `process.env` if the key has a falsy value
|
||||
- using `const` and `let` instead of `var`
|
||||
|
||||
### Removed
|
||||
|
||||
- Testing against Node v7
|
||||
|
||||
## [4.0.0] - 2016-12-23
|
||||
|
||||
### Changed
|
||||
|
||||
- Return Object with parsed content or error instead of false ([#165](https://github.com/motdotla/dotenv/pull/165)).
|
||||
|
||||
### Removed
|
||||
|
||||
- `verbose` option removed in favor of returning result.
|
||||
|
||||
## [3.0.0] - 2016-12-20
|
||||
|
||||
### Added
|
||||
|
||||
- `verbose` option will log any error messages. Off by default.
|
||||
- parses email addresses correctly
|
||||
- allow importing config method directly in ES6
|
||||
|
||||
### Changed
|
||||
|
||||
- Suppress error messages by default ([#154](https://github.com/motdotla/dotenv/pull/154))
|
||||
- Ignoring more files for NPM to make package download smaller
|
||||
|
||||
### Fixed
|
||||
|
||||
- False positive test due to case-sensitive variable ([#124](https://github.com/motdotla/dotenv/pull/124))
|
||||
|
||||
### Removed
|
||||
|
||||
- `silent` option removed in favor of `verbose`
|
||||
|
||||
## [2.0.0] - 2016-01-20
|
||||
|
||||
### Added
|
||||
|
||||
- CHANGELOG to ["make it easier for users and contributors to see precisely what notable changes have been made between each release"](http://keepachangelog.com/). Linked to from README
|
||||
- LICENSE to be more explicit about what was defined in `package.json`. Linked to from README
|
||||
- Testing nodejs v4 on travis-ci
|
||||
- added examples of how to use dotenv in different ways
|
||||
- return parsed object on success rather than boolean true
|
||||
|
||||
### Changed
|
||||
|
||||
- README has shorter description not referencing ruby gem since we don't have or want feature parity
|
||||
|
||||
### Removed
|
||||
|
||||
- Variable expansion and escaping so environment variables are encouraged to be fully orthogonal
|
||||
|
||||
## [1.2.0] - 2015-06-20
|
||||
|
||||
### Added
|
||||
|
||||
- Preload hook to require dotenv without including it in your code
|
||||
|
||||
### Changed
|
||||
|
||||
- clarified license to be "BSD-2-Clause" in `package.json`
|
||||
|
||||
### Fixed
|
||||
|
||||
- retain spaces in string vars
|
||||
|
||||
## [1.1.0] - 2015-03-31
|
||||
|
||||
### Added
|
||||
|
||||
- Silent option to silence `console.log` when `.env` missing
|
||||
|
||||
## [1.0.0] - 2015-03-13
|
||||
|
||||
### Removed
|
||||
|
||||
- support for multiple `.env` files. should always use one `.env` file for the current environment
|
||||
|
||||
[7.0.0]: https://github.com/motdotla/dotenv/compare/v6.2.0...v7.0.0
|
||||
[6.2.0]: https://github.com/motdotla/dotenv/compare/v6.1.0...v6.2.0
|
||||
[6.1.0]: https://github.com/motdotla/dotenv/compare/v6.0.0...v6.1.0
|
||||
[6.0.0]: https://github.com/motdotla/dotenv/compare/v5.0.0...v6.0.0
|
||||
[5.0.0]: https://github.com/motdotla/dotenv/compare/v4.0.0...v5.0.0
|
||||
[4.0.0]: https://github.com/motdotla/dotenv/compare/v3.0.0...v4.0.0
|
||||
[3.0.0]: https://github.com/motdotla/dotenv/compare/v2.0.0...v3.0.0
|
||||
[2.0.0]: https://github.com/motdotla/dotenv/compare/v1.2.0...v2.0.0
|
||||
[1.2.0]: https://github.com/motdotla/dotenv/compare/v1.1.0...v1.2.0
|
||||
[1.1.0]: https://github.com/motdotla/dotenv/compare/v1.0.0...v1.1.0
|
||||
[1.0.0]: https://github.com/motdotla/dotenv/compare/v0.4.0...v1.0.0
|
||||
23
node_modules/dotenv/LICENSE
generated
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
Copyright (c) 2015, Scott Motte
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* 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.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER OR CONTRIBUTORS 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.
|
||||
392
node_modules/dotenv/README-es.md
generated
vendored
Normal file
@ -0,0 +1,392 @@
|
||||
<div align="center">
|
||||
🎉 announcing <a href="https://github.com/dotenvx/dotenvx">dotenvx</a>. <em>run anywhere, multi-environment, encrypted envs</em>.
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
# dotenv [](https://www.npmjs.com/package/dotenv)
|
||||
|
||||
<img src="https://raw.githubusercontent.com/motdotla/dotenv/master/dotenv.svg" alt="dotenv" align="right" width="200" />
|
||||
|
||||
Dotenv es un módulo de dependencia cero que carga las variables de entorno desde un archivo `.env` en [`process.env`](https://nodejs.org/docs/latest/api/process.html#process_process_env). El almacenamiento de la configuración del entorno separado del código está basado en la metodología [The Twelve-Factor App](http://12factor.net/config).
|
||||
|
||||
[](https://github.com/feross/standard)
|
||||
[](LICENSE)
|
||||
|
||||
## Instalación
|
||||
|
||||
```bash
|
||||
# instalación local (recomendado)
|
||||
npm install dotenv --save
|
||||
```
|
||||
|
||||
O installación con yarn? `yarn add dotenv`
|
||||
|
||||
## Uso
|
||||
|
||||
Cree un archivo `.env` en la raíz de su proyecto:
|
||||
|
||||
```dosini
|
||||
S3_BUCKET="YOURS3BUCKET"
|
||||
SECRET_KEY="YOURSECRETKEYGOESHERE"
|
||||
```
|
||||
|
||||
Tan prónto como sea posible en su aplicación, importe y configure dotenv:
|
||||
|
||||
```javascript
|
||||
require('dotenv').config()
|
||||
console.log(process.env) // elimine esto después que haya confirmado que esta funcionando
|
||||
```
|
||||
|
||||
.. o usa ES6?
|
||||
|
||||
```javascript
|
||||
import * as dotenv from 'dotenv' // vea en https://github.com/motdotla/dotenv#como-uso-dotenv-con-import
|
||||
// REVISAR LINK DE REFERENCIA DE IMPORTACIÓN
|
||||
dotenv.config()
|
||||
import express from 'express'
|
||||
```
|
||||
|
||||
Eso es todo. `process.env` ahora tiene las claves y los valores que definiste en tu archivo `.env`:
|
||||
|
||||
```javascript
|
||||
require('dotenv').config()
|
||||
|
||||
...
|
||||
|
||||
s3.getBucketCors({Bucket: process.env.S3_BUCKET}, function(err, data) {})
|
||||
```
|
||||
|
||||
### Valores multilínea
|
||||
|
||||
Si necesita variables de varias líneas, por ejemplo, claves privadas, ahora se admiten en la versión (`>= v15.0.0`) con saltos de línea:
|
||||
|
||||
```dosini
|
||||
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
|
||||
...
|
||||
Kh9NV...
|
||||
...
|
||||
-----END RSA PRIVATE KEY-----"
|
||||
```
|
||||
|
||||
Alternativamente, puede usar comillas dobles y usar el carácter `\n`:
|
||||
|
||||
```dosini
|
||||
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nKh9NV...\n-----END RSA PRIVATE KEY-----\n"
|
||||
```
|
||||
|
||||
### Comentarios
|
||||
|
||||
Los comentarios pueden ser agregados en tu archivo o en la misma línea:
|
||||
|
||||
```dosini
|
||||
# This is a comment
|
||||
SECRET_KEY=YOURSECRETKEYGOESHERE # comment
|
||||
SECRET_HASH="something-with-a-#-hash"
|
||||
```
|
||||
|
||||
Los comentarios comienzan donde existe un `#`, entonces, si su valor contiene un `#`, enciérrelo entre comillas. Este es un cambio importante desde la versión `>= v15.0.0` en adelante.
|
||||
|
||||
### Análisis
|
||||
|
||||
El motor que analiza el contenido de su archivo que contiene variables de entorno está disponible para su uso. Este Acepta una Cadena o un Búfer y devolverá un Objeto con las claves y los valores analizados.
|
||||
|
||||
```javascript
|
||||
const dotenv = require('dotenv')
|
||||
const buf = Buffer.from('BASICO=basico')
|
||||
const config = dotenv.parse(buf) // devolverá un objeto
|
||||
console.log(typeof config, config) // objeto { BASICO : 'basico' }
|
||||
```
|
||||
|
||||
### Precarga
|
||||
|
||||
Puede usar el `--require` (`-r`) [opción de línea de comando](https://nodejs.org/api/cli.html#-r---require-module) para precargar dotenv. Al hacer esto, no necesita requerir ni cargar dotnev en el código de su aplicación.
|
||||
|
||||
```bash
|
||||
$ node -r dotenv/config tu_script.js
|
||||
```
|
||||
|
||||
Las opciones de configuración a continuación se admiten como argumentos de línea de comandos en el formato `dotenv_config_<option>=value`
|
||||
|
||||
```bash
|
||||
$ node -r dotenv/config tu_script.js dotenv_config_path=/custom/path/to/.env dotenv_config_debug=true
|
||||
```
|
||||
|
||||
Además, puede usar variables de entorno para establecer opciones de configuración. Los argumentos de línea de comandos precederán a estos.
|
||||
|
||||
```bash
|
||||
$ DOTENV_CONFIG_<OPTION>=value node -r dotenv/config tu_script.js
|
||||
```
|
||||
|
||||
```bash
|
||||
$ DOTENV_CONFIG_ENCODING=latin1 DOTENV_CONFIG_DEBUG=true node -r dotenv/config tu_script.js dotenv_config_path=/custom/path/to/.env
|
||||
```
|
||||
|
||||
### Expansión Variable
|
||||
|
||||
Necesitaras agregar el valor de otro variable en una de sus variables? Usa [dotenv-expand](https://github.com/motdotla/dotenv-expand).
|
||||
|
||||
## Ejemplos
|
||||
|
||||
Vea [ejemplos](https://github.com/dotenv-org/examples) sobre el uso de dotenv con varios frameworks, lenguajes y configuraciones.
|
||||
|
||||
* [nodejs](https://github.com/dotenv-org/examples/tree/master/dotenv-nodejs)
|
||||
* [nodejs (depurar en)](https://github.com/dotenv-org/examples/tree/master/dotenv-nodejs-debug)
|
||||
* [nodejs (anular en)](https://github.com/dotenv-org/examples/tree/master/dotenv-nodejs-override)
|
||||
* [esm](https://github.com/dotenv-org/examples/tree/master/dotenv-esm)
|
||||
* [esm (precarga)](https://github.com/dotenv-org/examples/tree/master/dotenv-esm-preload)
|
||||
* [typescript](https://github.com/dotenv-org/examples/tree/master/dotenv-typescript)
|
||||
* [typescript parse](https://github.com/dotenv-org/examples/tree/master/dotenv-typescript-parse)
|
||||
* [typescript config](https://github.com/dotenv-org/examples/tree/master/dotenv-typescript-config)
|
||||
* [webpack](https://github.com/dotenv-org/examples/tree/master/dotenv-webpack)
|
||||
* [webpack (plugin)](https://github.com/dotenv-org/examples/tree/master/dotenv-webpack2)
|
||||
* [react](https://github.com/dotenv-org/examples/tree/master/dotenv-react)
|
||||
* [react (typescript)](https://github.com/dotenv-org/examples/tree/master/dotenv-react-typescript)
|
||||
* [express](https://github.com/dotenv-org/examples/tree/master/dotenv-express)
|
||||
* [nestjs](https://github.com/dotenv-org/examples/tree/master/dotenv-nestjs)
|
||||
|
||||
## Documentación
|
||||
|
||||
Dotenv expone dos funciones:
|
||||
|
||||
* `configuración`
|
||||
* `analizar`
|
||||
|
||||
### Configuración
|
||||
|
||||
`Configuración` leerá su archivo `.env`, analizará el contenido, lo asignará a [`process.env`](https://nodejs.org/docs/latest/api/process.html#process_process_env),
|
||||
y devolverá un Objeto con una clave `parsed` que contiene el contenido cargado o una clave `error` si falla.
|
||||
|
||||
```js
|
||||
const result = dotenv.config()
|
||||
|
||||
if (result.error) {
|
||||
throw result.error
|
||||
}
|
||||
|
||||
console.log(result.parsed)
|
||||
```
|
||||
|
||||
Adicionalmente, puede pasar opciones a `configuracion`.
|
||||
|
||||
#### Opciones
|
||||
|
||||
##### Ruta
|
||||
|
||||
Por defecto: `path.resolve(process.cwd(), '.env')`
|
||||
|
||||
Especifique una ruta personalizada si el archivo que contiene las variables de entorno se encuentra localizado en otro lugar.
|
||||
|
||||
```js
|
||||
require('dotenv').config({ path: '/personalizado/ruta/a/.env' })
|
||||
```
|
||||
|
||||
##### Codificación
|
||||
|
||||
Por defecto: `utf8`
|
||||
|
||||
Especifique la codificación del archivo que contiene las variables de entorno.
|
||||
|
||||
```js
|
||||
require('dotenv').config({ encoding: 'latin1' })
|
||||
```
|
||||
|
||||
##### Depurar
|
||||
|
||||
Por defecto: `false`
|
||||
|
||||
Active el registro de ayuda para depurar por qué ciertas claves o valores no se inician como lo esperabas.
|
||||
|
||||
```js
|
||||
require('dotenv').config({ debug: process.env.DEBUG })
|
||||
```
|
||||
|
||||
##### Anular
|
||||
|
||||
Por defecto: `false`
|
||||
|
||||
Anule cualquier variable de entorno que ya se haya configurada en su maquina con los valores de su archivo .env.
|
||||
|
||||
```js
|
||||
require('dotenv').config({ override: true })
|
||||
```
|
||||
|
||||
### Analizar
|
||||
|
||||
El motor que analiza el contenido del archivo que contiene las variables de entorno está disponible para su uso. Acepta una Cadena o un Búfer y retornará un objecto con los valores analizados.
|
||||
|
||||
```js
|
||||
const dotenv = require('dotenv')
|
||||
const buf = Buffer.from('BASICO=basico')
|
||||
const config = dotenv.parse(buf) // devolverá un objeto
|
||||
console.log(typeof config, config) // objeto { BASICO : 'basico' }
|
||||
```
|
||||
|
||||
#### Opciones
|
||||
|
||||
##### Depurar
|
||||
|
||||
Por defecto: `false`
|
||||
|
||||
Active el registro de ayuda para depurar por qué ciertas claves o valores no se inician como lo esperabas.
|
||||
|
||||
```js
|
||||
const dotenv = require('dotenv')
|
||||
const buf = Buffer.from('hola mundo')
|
||||
const opt = { debug: true }
|
||||
const config = dotenv.parse(buf, opt)
|
||||
// espere por un mensaje de depuración porque el búfer no esta listo KEY=VAL
|
||||
```
|
||||
|
||||
## FAQ
|
||||
|
||||
### ¿Por qué el archivo `.env` no carga mis variables de entorno correctamente?
|
||||
|
||||
Lo más probable es que su archivo `.env` no esté en el lugar correcto. [Vea este stack overflow](https://stackoverflow.com/questions/42335016/dotenv-file-is-not-loading-environment-variables).
|
||||
|
||||
Active el modo de depuración y vuelva a intentarlo...
|
||||
|
||||
```js
|
||||
require('dotenv').config({ debug: true })
|
||||
```
|
||||
|
||||
Recibirá un error apropiado en su consola.
|
||||
|
||||
### ¿Debo confirmar mi archivo `.env`?
|
||||
|
||||
No. Recomendamos **enfáticamente** no enviar su archivo `.env` a la versión de control. Solo debe incluir los valores especificos del entorno, como la base de datos, contraseñas o claves API.
|
||||
|
||||
### ¿Debería tener multiples archivos `.env`?
|
||||
|
||||
No. Recomendamos **enfáticamente** no tener un archivo `.env` "principal" y un archivo `.env` de "entorno" como `.env.test`. Su configuración debe variar entre implementaciones y no debe compartir valores entre entornos.
|
||||
|
||||
> En una Aplicación de Doce Factores, las variables de entorno son controles diferenciados, cada uno totalmente independiente a otras variables de entorno. Nunca se agrupan como "entornos", sino que se gestionan de manera independiente para cada despliegue. Este es un modelo que se escala sin problemas a medida que la aplicación se expande de forma natural en más despliegues a lo largo de su vida.
|
||||
>
|
||||
> – [La Apliación de los Doce Factores](https://12factor.net/es/)
|
||||
|
||||
### ¿Qué reglas sigue el motor de análisis?
|
||||
|
||||
El motor de análisis actualmente admite las siguientes reglas:
|
||||
|
||||
- `BASICO=basico` se convierte en `{BASICO: 'basico'}`
|
||||
- las líneas vacías se saltan
|
||||
- las líneas que comienzan con `#` se tratan como comentarios
|
||||
- `#` marca el comienzo de un comentario (a menos que el valor esté entre comillas)
|
||||
- valores vacíos se convierten en cadenas vacías (`VACIO=` se convierte en `{VACIO: ''}`)
|
||||
- las comillas internas se mantienen (piensa en JSON) (`JSON={"foo": "bar"}` se convierte en `{JSON:"{\"foo\": \"bar\"}"`)
|
||||
- los espacios en blanco se eliminan de ambos extremos de los valores no citanos (aprende más en [`trim`](https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Global_Objects/String/Trim)) (`FOO= algo ` se convierte en `{FOO: 'algo'}`)
|
||||
- los valores entre comillas simples y dobles se escapan (`CITA_SIMPLE='citado'` se convierte en `{CITA_SIMPLE: "citado"}`)
|
||||
- los valores entre comillas simples y dobles mantienen los espacios en blanco en ambos extremos (`FOO=" algo "` se convierte en `{FOO: ' algo '}`)
|
||||
- los valores entre comillas dobles expanden nuevas líneas (`MULTILINEA="nueva\nlínea"` se convierte en
|
||||
|
||||
```
|
||||
{MULTILINEA: 'nueva
|
||||
línea'}
|
||||
```
|
||||
|
||||
- se admite la comilla simple invertida (`` SIGNO_ACENTO=`Esto tiene comillas 'simples' y "dobles" en su interior.` ``)
|
||||
|
||||
### ¿Qué sucede con las variables de entorno que ya estaban configuradas?
|
||||
|
||||
Por defecto, nunca modificaremos ninguna variable de entorno que ya haya sido establecida. En particular, si hay una variable en su archivo `.env` que colisiona con una que ya existe en su entorno, entonces esa variable se omitirá.
|
||||
|
||||
Si por el contrario, quieres anular `process.env` utiliza la opción `override`.
|
||||
|
||||
```javascript
|
||||
require('dotenv').config({ override: true })
|
||||
```
|
||||
|
||||
### ¿Por qué mis variables de entorno no aparecen para React?
|
||||
|
||||
Su código React se ejecuta en Webpack, donde el módulo `fs` o incluso el propio `process` global no son accesibles fuera-de-la-caja. El módulo `process.env` sólo puede ser inyectado a través de la configuración de Webpack.
|
||||
|
||||
Si estás usando [`react-scripts`](https://www.npmjs.com/package/react-scripts), el cual se distribuye a través de [`create-react-app`](https://create-react-app.dev/), tiene dotenv incorporado pero con una singularidad. Escriba sus variables de entorno con `REACT_APP_`. Vea [este stack overflow](https://stackoverflow.com/questions/42182577/is-it-possible-to-use-dotenv-in-a-react-project) para más detalles.
|
||||
|
||||
Si estás utilizando otros frameworks (por ejemplo, Next.js, Gatsby...), debes consultar su documentación para saber cómo injectar variables de entorno en el cliente.
|
||||
|
||||
### ¿Puedo personalizar/escribir plugins para dotenv?
|
||||
|
||||
Sí! `dotenv.config()` devuelve un objeto que representa el archivo `.env` analizado. Esto te da todo lo que necesitas para poder establecer valores en `process.env`. Por ejemplo:
|
||||
|
||||
```js
|
||||
const dotenv = require('dotenv')
|
||||
const variableExpansion = require('dotenv-expand')
|
||||
const miEnv = dotenv.config()
|
||||
variableExpansion(miEnv)
|
||||
```
|
||||
|
||||
### Cómo uso dotnev con `import`?
|
||||
|
||||
Simplemente..
|
||||
|
||||
```javascript
|
||||
// index.mjs (ESM)
|
||||
import * as dotenv from 'dotenv' // vea https://github.com/motdotla/dotenv#como-uso-dotenv-con-import
|
||||
dotenv.config()
|
||||
import express from 'express'
|
||||
```
|
||||
|
||||
Un poco de historia...
|
||||
|
||||
> Cuando se ejecuta un módulo que contiene una sentencia `import`, los módulos que importa serán cargados primero, y luego se ejecuta cada bloque del módulo en un recorrido en profundidad del gráfico de dependencias, evitando los ciclos al saltarse todo lo que ya se ha ejecutado.
|
||||
>
|
||||
> – [ES6 en Profundidad: Módulos](https://hacks.mozilla.org/2015/08/es6-in-depth-modules/)
|
||||
|
||||
¿Qué significa esto en lenguaje sencillo? Significa que se podrías pensar que lo siguiente funcionaría pero no lo hará.
|
||||
|
||||
```js
|
||||
// notificarError.mjs
|
||||
import { Cliente } from 'mejor-servicio-para-notificar-error'
|
||||
|
||||
export default new Client(process.env.CLAVE_API)
|
||||
|
||||
// index.mjs
|
||||
import dotenv from 'dotenv'
|
||||
dotenv.config()
|
||||
|
||||
import notificarError from './notificarError.mjs'
|
||||
notificarError.report(new Error('ejemplo documentado'))
|
||||
```
|
||||
|
||||
`process.env.CLAVE_API` será vacio.
|
||||
|
||||
En su lugar, el código anterior debe ser escrito como...
|
||||
|
||||
```js
|
||||
// notificarError.mjs
|
||||
import { Cliente } from 'mejor-servicio-para-notificar-errores'
|
||||
|
||||
export default new Client(process.env.CLAVE_API)
|
||||
|
||||
// index.mjs
|
||||
import * as dotenv from 'dotenv'
|
||||
dotenv.config()
|
||||
|
||||
import notificarError from './notificarError.mjs'
|
||||
notificarError.report(new Error('ejemplo documentado'))
|
||||
```
|
||||
|
||||
¿Esto tiene algo de sentido? Esto es poco poco intuitivo, pero es como funciona la importación de módulos en ES6. Aquí hay un ejemplo [ejemplo práctico de esta trampa](https://github.com/dotenv-org/examples/tree/master/dotenv-es6-import-pitfall).
|
||||
|
||||
Existen dos arternativas a este planteamiento:
|
||||
|
||||
1. Precarga dotenv: `node --require dotenv/config index.js` (_Nota: no es necesario usar `import` dotenv con este método_)
|
||||
2. Cree un archivo separado que ejecutará `config` primero como se describe en [este comentario #133](https://github.com/motdotla/dotenv/issues/133#issuecomment-255298822)
|
||||
|
||||
### ¿Qué pasa con la expansión de variable?
|
||||
|
||||
Prueba [dotenv-expand](https://github.com/motdotla/dotenv-expand)
|
||||
|
||||
## Guía de contribución
|
||||
|
||||
Vea [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||
|
||||
## REGISTRO DE CAMBIOS
|
||||
|
||||
Vea [CHANGELOG.md](CHANGELOG.md)
|
||||
|
||||
## ¿Quiénes utilizan dotenv?
|
||||
|
||||
[Estos módulos npm dependen de él.](https://www.npmjs.com/browse/depended/dotenv)
|
||||
|
||||
Los proyectos que lo amplían suelen utilizar la [palabra clave "dotenv" en npm](https://www.npmjs.com/search?q=keywords:dotenv).
|
||||
679
node_modules/dotenv/README.md
generated
vendored
Normal file
@ -0,0 +1,679 @@
|
||||
<div align="center">
|
||||
🎉 announcing <a href="https://github.com/dotenvx/dotenvx">dotenvx</a>. <em>run anywhere, multi-environment, encrypted envs</em>.
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
# dotenv [](https://www.npmjs.com/package/dotenv)
|
||||
|
||||
<img src="https://raw.githubusercontent.com/motdotla/dotenv/master/dotenv.svg" alt="dotenv" align="right" width="200" />
|
||||
|
||||
Dotenv is a zero-dependency module that loads environment variables from a `.env` file into [`process.env`](https://nodejs.org/docs/latest/api/process.html#process_process_env). Storing configuration in the environment separate from code is based on [The Twelve-Factor App](https://12factor.net/config) methodology.
|
||||
|
||||
[](https://github.com/feross/standard)
|
||||
[](LICENSE)
|
||||
[](https://codecov.io/gh/motdotla/dotenv-expand)
|
||||
|
||||
* [🌱 Install](#-install)
|
||||
* [🏗️ Usage (.env)](#%EF%B8%8F-usage)
|
||||
* [🌴 Multiple Environments 🆕](#-manage-multiple-environments)
|
||||
* [🚀 Deploying (encryption) 🆕](#-deploying)
|
||||
* [📚 Examples](#-examples)
|
||||
* [📖 Docs](#-documentation)
|
||||
* [❓ FAQ](#-faq)
|
||||
* [⏱️ Changelog](./CHANGELOG.md)
|
||||
|
||||
## 🌱 Install
|
||||
|
||||
```bash
|
||||
npm install dotenv --save
|
||||
```
|
||||
|
||||
You can also use an npm-compatible package manager like yarn, bun or pnpm:
|
||||
|
||||
```bash
|
||||
yarn add dotenv
|
||||
```
|
||||
```bash
|
||||
bun add dotenv
|
||||
```
|
||||
```bash
|
||||
pnpm add dotenv
|
||||
```
|
||||
|
||||
## 🏗️ Usage
|
||||
|
||||
<a href="https://www.youtube.com/watch?v=YtkZR0NFd1g">
|
||||
<div align="right">
|
||||
<img src="https://img.youtube.com/vi/YtkZR0NFd1g/hqdefault.jpg" alt="how to use dotenv video tutorial" align="right" width="330" />
|
||||
<img src="https://simpleicons.vercel.app/youtube/ff0000" alt="youtube/@dotenvorg" align="right" width="24" />
|
||||
</div>
|
||||
</a>
|
||||
|
||||
Create a `.env` file in the root of your project (if using a monorepo structure like `apps/backend/app.js`, put it in the root of the folder where your `app.js` process runs):
|
||||
|
||||
```dosini
|
||||
S3_BUCKET="YOURS3BUCKET"
|
||||
SECRET_KEY="YOURSECRETKEYGOESHERE"
|
||||
```
|
||||
|
||||
As early as possible in your application, import and configure dotenv:
|
||||
|
||||
```javascript
|
||||
require('dotenv').config()
|
||||
console.log(process.env) // remove this after you've confirmed it is working
|
||||
```
|
||||
|
||||
.. [or using ES6?](#how-do-i-use-dotenv-with-import)
|
||||
|
||||
```javascript
|
||||
import 'dotenv/config'
|
||||
```
|
||||
|
||||
ES6 import if you need to set config options:
|
||||
|
||||
```javascript
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
dotenv.config({ path: '/custom/path/to/.env' })
|
||||
```
|
||||
|
||||
That's it. `process.env` now has the keys and values you defined in your `.env` file:
|
||||
|
||||
```javascript
|
||||
require('dotenv').config()
|
||||
// or import 'dotenv/config' if you're using ES6
|
||||
|
||||
...
|
||||
|
||||
s3.getBucketCors({Bucket: process.env.S3_BUCKET}, function(err, data) {})
|
||||
```
|
||||
|
||||
### Multiline values
|
||||
|
||||
If you need multiline variables, for example private keys, those are now supported (`>= v15.0.0`) with line breaks:
|
||||
|
||||
```dosini
|
||||
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
|
||||
...
|
||||
Kh9NV...
|
||||
...
|
||||
-----END RSA PRIVATE KEY-----"
|
||||
```
|
||||
|
||||
Alternatively, you can double quote strings and use the `\n` character:
|
||||
|
||||
```dosini
|
||||
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nKh9NV...\n-----END RSA PRIVATE KEY-----\n"
|
||||
```
|
||||
|
||||
### Comments
|
||||
|
||||
Comments may be added to your file on their own line or inline:
|
||||
|
||||
```dosini
|
||||
# This is a comment
|
||||
SECRET_KEY=YOURSECRETKEYGOESHERE # comment
|
||||
SECRET_HASH="something-with-a-#-hash"
|
||||
```
|
||||
|
||||
Comments begin where a `#` exists, so if your value contains a `#` please wrap it in quotes. This is a breaking change from `>= v15.0.0` and on.
|
||||
|
||||
### Parsing
|
||||
|
||||
The engine which parses the contents of your file containing environment variables is available to use. It accepts a String or Buffer and will return an Object with the parsed keys and values.
|
||||
|
||||
```javascript
|
||||
const dotenv = require('dotenv')
|
||||
const buf = Buffer.from('BASIC=basic')
|
||||
const config = dotenv.parse(buf) // will return an object
|
||||
console.log(typeof config, config) // object { BASIC : 'basic' }
|
||||
```
|
||||
|
||||
### Preload
|
||||
|
||||
> Note: Consider using [`dotenvx`](https://github.com/dotenvx/dotenvx) instead of preloading. I am now doing (and recommending) so.
|
||||
>
|
||||
> It serves the same purpose (you do not need to require and load dotenv), adds better debugging, and works with ANY language, framework, or platform. – [motdotla](https://github.com/motdotla)
|
||||
|
||||
You can use the `--require` (`-r`) [command line option](https://nodejs.org/api/cli.html#-r---require-module) to preload dotenv. By doing this, you do not need to require and load dotenv in your application code.
|
||||
|
||||
```bash
|
||||
$ node -r dotenv/config your_script.js
|
||||
```
|
||||
|
||||
The configuration options below are supported as command line arguments in the format `dotenv_config_<option>=value`
|
||||
|
||||
```bash
|
||||
$ node -r dotenv/config your_script.js dotenv_config_path=/custom/path/to/.env dotenv_config_debug=true
|
||||
```
|
||||
|
||||
Additionally, you can use environment variables to set configuration options. Command line arguments will precede these.
|
||||
|
||||
```bash
|
||||
$ DOTENV_CONFIG_<OPTION>=value node -r dotenv/config your_script.js
|
||||
```
|
||||
|
||||
```bash
|
||||
$ DOTENV_CONFIG_ENCODING=latin1 DOTENV_CONFIG_DEBUG=true node -r dotenv/config your_script.js dotenv_config_path=/custom/path/to/.env
|
||||
```
|
||||
|
||||
### Variable Expansion
|
||||
|
||||
Use [dotenvx](https://github.com/dotenvx/dotenvx) to use variable expansion.
|
||||
|
||||
Reference and expand variables already on your machine for use in your .env file.
|
||||
|
||||
```ini
|
||||
# .env
|
||||
USERNAME="username"
|
||||
DATABASE_URL="postgres://${USERNAME}@localhost/my_database"
|
||||
```
|
||||
```js
|
||||
// index.js
|
||||
console.log('DATABASE_URL', process.env.DATABASE_URL)
|
||||
```
|
||||
```sh
|
||||
$ dotenvx run --debug -- node index.js
|
||||
[dotenvx@0.14.1] injecting env (2) from .env
|
||||
DATABASE_URL postgres://username@localhost/my_database
|
||||
```
|
||||
|
||||
### Command Substitution
|
||||
|
||||
Use [dotenvx](https://github.com/dotenvx/dotenvx) to use command substitution.
|
||||
|
||||
Add the output of a command to one of your variables in your .env file.
|
||||
|
||||
```ini
|
||||
# .env
|
||||
DATABASE_URL="postgres://$(whoami)@localhost/my_database"
|
||||
```
|
||||
```js
|
||||
// index.js
|
||||
console.log('DATABASE_URL', process.env.DATABASE_URL)
|
||||
```
|
||||
```sh
|
||||
$ dotenvx run --debug -- node index.js
|
||||
[dotenvx@0.14.1] injecting env (1) from .env
|
||||
DATABASE_URL postgres://yourusername@localhost/my_database
|
||||
```
|
||||
|
||||
### Syncing
|
||||
|
||||
You need to keep `.env` files in sync between machines, environments, or team members? Use [dotenvx](https://github.com/dotenvx/dotenvx) to encrypt your `.env` files and safely include them in source control. This still subscribes to the twelve-factor app rules by generating a decryption key separate from code.
|
||||
|
||||
### Multiple Environments
|
||||
|
||||
Use [dotenvx](https://github.com/dotenvx/dotenvx) to generate `.env.ci`, `.env.production` files, and more.
|
||||
|
||||
### Deploying
|
||||
|
||||
You need to deploy your secrets in a cloud-agnostic manner? Use [dotenvx](https://github.com/dotenvx/dotenvx) to generate a private decryption key that is set on your production server.
|
||||
|
||||
## 🌴 Manage Multiple Environments
|
||||
|
||||
Use [dotenvx](https://github.com/dotenvx/dotenvx)
|
||||
|
||||
Run any environment locally. Create a `.env.ENVIRONMENT` file and use `--env-file` to load it. It's straightforward, yet flexible.
|
||||
|
||||
```bash
|
||||
$ echo "HELLO=production" > .env.production
|
||||
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js
|
||||
|
||||
$ dotenvx run --env-file=.env.production -- node index.js
|
||||
Hello production
|
||||
> ^^
|
||||
```
|
||||
|
||||
or with multiple .env files
|
||||
|
||||
```bash
|
||||
$ echo "HELLO=local" > .env.local
|
||||
$ echo "HELLO=World" > .env
|
||||
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js
|
||||
|
||||
$ dotenvx run --env-file=.env.local --env-file=.env -- node index.js
|
||||
Hello local
|
||||
```
|
||||
|
||||
[more environment examples](https://dotenvx.com/docs/quickstart/environments)
|
||||
|
||||
## 🚀 Deploying
|
||||
|
||||
Use [dotenvx](https://github.com/dotenvx/dotenvx).
|
||||
|
||||
Add encryption to your `.env` files with a single command. Pass the `--encrypt` flag.
|
||||
|
||||
```
|
||||
$ dotenvx set HELLO Production --encrypt -f .env.production
|
||||
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js
|
||||
|
||||
$ DOTENV_PRIVATE_KEY_PRODUCTION="<.env.production private key>" dotenvx run -- node index.js
|
||||
[dotenvx] injecting env (2) from .env.production
|
||||
Hello Production
|
||||
```
|
||||
|
||||
[learn more](https://github.com/dotenvx/dotenvx?tab=readme-ov-file#encryption)
|
||||
|
||||
## 📚 Examples
|
||||
|
||||
See [examples](https://github.com/dotenv-org/examples) of using dotenv with various frameworks, languages, and configurations.
|
||||
|
||||
* [nodejs](https://github.com/dotenv-org/examples/tree/master/usage/dotenv-nodejs)
|
||||
* [nodejs (debug on)](https://github.com/dotenv-org/examples/tree/master/usage/dotenv-nodejs-debug)
|
||||
* [nodejs (override on)](https://github.com/dotenv-org/examples/tree/master/usage/dotenv-nodejs-override)
|
||||
* [nodejs (processEnv override)](https://github.com/dotenv-org/examples/tree/master/usage/dotenv-custom-target)
|
||||
* [esm](https://github.com/dotenv-org/examples/tree/master/usage/dotenv-esm)
|
||||
* [esm (preload)](https://github.com/dotenv-org/examples/tree/master/usage/dotenv-esm-preload)
|
||||
* [typescript](https://github.com/dotenv-org/examples/tree/master/usage/dotenv-typescript)
|
||||
* [typescript parse](https://github.com/dotenv-org/examples/tree/master/usage/dotenv-typescript-parse)
|
||||
* [typescript config](https://github.com/dotenv-org/examples/tree/master/usage/dotenv-typescript-config)
|
||||
* [webpack](https://github.com/dotenv-org/examples/tree/master/usage/dotenv-webpack)
|
||||
* [webpack (plugin)](https://github.com/dotenv-org/examples/tree/master/usage/dotenv-webpack2)
|
||||
* [react](https://github.com/dotenv-org/examples/tree/master/usage/dotenv-react)
|
||||
* [react (typescript)](https://github.com/dotenv-org/examples/tree/master/usage/dotenv-react-typescript)
|
||||
* [express](https://github.com/dotenv-org/examples/tree/master/usage/dotenv-express)
|
||||
* [nestjs](https://github.com/dotenv-org/examples/tree/master/usage/dotenv-nestjs)
|
||||
* [fastify](https://github.com/dotenv-org/examples/tree/master/usage/dotenv-fastify)
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
Dotenv exposes four functions:
|
||||
|
||||
* `config`
|
||||
* `parse`
|
||||
* `populate`
|
||||
|
||||
### Config
|
||||
|
||||
`config` will read your `.env` file, parse the contents, assign it to
|
||||
[`process.env`](https://nodejs.org/docs/latest/api/process.html#process_process_env),
|
||||
and return an Object with a `parsed` key containing the loaded content or an `error` key if it failed.
|
||||
|
||||
```js
|
||||
const result = dotenv.config()
|
||||
|
||||
if (result.error) {
|
||||
throw result.error
|
||||
}
|
||||
|
||||
console.log(result.parsed)
|
||||
```
|
||||
|
||||
You can additionally, pass options to `config`.
|
||||
|
||||
#### Options
|
||||
|
||||
##### path
|
||||
|
||||
Default: `path.resolve(process.cwd(), '.env')`
|
||||
|
||||
Specify a custom path if your file containing environment variables is located elsewhere.
|
||||
|
||||
```js
|
||||
require('dotenv').config({ path: '/custom/path/to/.env' })
|
||||
```
|
||||
|
||||
By default, `config` will look for a file called .env in the current working directory.
|
||||
|
||||
Pass in multiple files as an array, and they will be parsed in order and combined with `process.env` (or `option.processEnv`, if set). The first value set for a variable will win, unless the `options.override` flag is set, in which case the last value set will win. If a value already exists in `process.env` and the `options.override` flag is NOT set, no changes will be made to that value.
|
||||
|
||||
```js
|
||||
require('dotenv').config({ path: ['.env.local', '.env'] })
|
||||
```
|
||||
|
||||
##### quiet
|
||||
|
||||
Default: `false`
|
||||
|
||||
Suppress runtime logging message.
|
||||
|
||||
```js
|
||||
// index.js
|
||||
require('dotenv').config({ quiet: false }) // change to true to suppress
|
||||
console.log(`Hello ${process.env.HELLO}`)
|
||||
```
|
||||
|
||||
```ini
|
||||
# .env
|
||||
.env
|
||||
```
|
||||
|
||||
```sh
|
||||
$ node index.js
|
||||
[dotenv@17.0.0] injecting env (1) from .env
|
||||
Hello World
|
||||
```
|
||||
|
||||
##### encoding
|
||||
|
||||
Default: `utf8`
|
||||
|
||||
Specify the encoding of your file containing environment variables.
|
||||
|
||||
```js
|
||||
require('dotenv').config({ encoding: 'latin1' })
|
||||
```
|
||||
|
||||
##### debug
|
||||
|
||||
Default: `false`
|
||||
|
||||
Turn on logging to help debug why certain keys or values are not being set as you expect.
|
||||
|
||||
```js
|
||||
require('dotenv').config({ debug: process.env.DEBUG })
|
||||
```
|
||||
|
||||
##### override
|
||||
|
||||
Default: `false`
|
||||
|
||||
Override any environment variables that have already been set on your machine with values from your .env file(s). If multiple files have been provided in `option.path` the override will also be used as each file is combined with the next. Without `override` being set, the first value wins. With `override` set the last value wins.
|
||||
|
||||
```js
|
||||
require('dotenv').config({ override: true })
|
||||
```
|
||||
|
||||
##### processEnv
|
||||
|
||||
Default: `process.env`
|
||||
|
||||
Specify an object to write your environment variables to. Defaults to `process.env` environment variables.
|
||||
|
||||
```js
|
||||
const myObject = {}
|
||||
require('dotenv').config({ processEnv: myObject })
|
||||
|
||||
console.log(myObject) // values from .env
|
||||
console.log(process.env) // this was not changed or written to
|
||||
```
|
||||
|
||||
### Parse
|
||||
|
||||
The engine which parses the contents of your file containing environment
|
||||
variables is available to use. It accepts a String or Buffer and will return
|
||||
an Object with the parsed keys and values.
|
||||
|
||||
```js
|
||||
const dotenv = require('dotenv')
|
||||
const buf = Buffer.from('BASIC=basic')
|
||||
const config = dotenv.parse(buf) // will return an object
|
||||
console.log(typeof config, config) // object { BASIC : 'basic' }
|
||||
```
|
||||
|
||||
#### Options
|
||||
|
||||
##### debug
|
||||
|
||||
Default: `false`
|
||||
|
||||
Turn on logging to help debug why certain keys or values are not being set as you expect.
|
||||
|
||||
```js
|
||||
const dotenv = require('dotenv')
|
||||
const buf = Buffer.from('hello world')
|
||||
const opt = { debug: true }
|
||||
const config = dotenv.parse(buf, opt)
|
||||
// expect a debug message because the buffer is not in KEY=VAL form
|
||||
```
|
||||
|
||||
### Populate
|
||||
|
||||
The engine which populates the contents of your .env file to `process.env` is available for use. It accepts a target, a source, and options. This is useful for power users who want to supply their own objects.
|
||||
|
||||
For example, customizing the source:
|
||||
|
||||
```js
|
||||
const dotenv = require('dotenv')
|
||||
const parsed = { HELLO: 'world' }
|
||||
|
||||
dotenv.populate(process.env, parsed)
|
||||
|
||||
console.log(process.env.HELLO) // world
|
||||
```
|
||||
|
||||
For example, customizing the source AND target:
|
||||
|
||||
```js
|
||||
const dotenv = require('dotenv')
|
||||
const parsed = { HELLO: 'universe' }
|
||||
const target = { HELLO: 'world' } // empty object
|
||||
|
||||
dotenv.populate(target, parsed, { override: true, debug: true })
|
||||
|
||||
console.log(target) // { HELLO: 'universe' }
|
||||
```
|
||||
|
||||
#### options
|
||||
|
||||
##### Debug
|
||||
|
||||
Default: `false`
|
||||
|
||||
Turn on logging to help debug why certain keys or values are not being populated as you expect.
|
||||
|
||||
##### override
|
||||
|
||||
Default: `false`
|
||||
|
||||
Override any environment variables that have already been set.
|
||||
|
||||
## ❓ FAQ
|
||||
|
||||
### Why is the `.env` file not loading my environment variables successfully?
|
||||
|
||||
Most likely your `.env` file is not in the correct place. [See this stack overflow](https://stackoverflow.com/questions/42335016/dotenv-file-is-not-loading-environment-variables).
|
||||
|
||||
Turn on debug mode and try again..
|
||||
|
||||
```js
|
||||
require('dotenv').config({ debug: true })
|
||||
```
|
||||
|
||||
You will receive a helpful error outputted to your console.
|
||||
|
||||
### Should I commit my `.env` file?
|
||||
|
||||
No. We **strongly** recommend against committing your `.env` file to version
|
||||
control. It should only include environment-specific values such as database
|
||||
passwords or API keys. Your production database should have a different
|
||||
password than your development database.
|
||||
|
||||
### Should I have multiple `.env` files?
|
||||
|
||||
We recommend creating one `.env` file per environment. Use `.env` for local/development, `.env.production` for production and so on. This still follows the twelve factor principles as each is attributed individually to its own environment. Avoid custom set ups that work in inheritance somehow (`.env.production` inherits values form `.env` for example). It is better to duplicate values if necessary across each `.env.environment` file.
|
||||
|
||||
> In a twelve-factor app, env vars are granular controls, each fully orthogonal to other env vars. They are never grouped together as “environments”, but instead are independently managed for each deploy. This is a model that scales up smoothly as the app naturally expands into more deploys over its lifetime.
|
||||
>
|
||||
> – [The Twelve-Factor App](http://12factor.net/config)
|
||||
|
||||
### What rules does the parsing engine follow?
|
||||
|
||||
The parsing engine currently supports the following rules:
|
||||
|
||||
- `BASIC=basic` becomes `{BASIC: 'basic'}`
|
||||
- empty lines are skipped
|
||||
- lines beginning with `#` are treated as comments
|
||||
- `#` marks the beginning of a comment (unless when the value is wrapped in quotes)
|
||||
- empty values become empty strings (`EMPTY=` becomes `{EMPTY: ''}`)
|
||||
- inner quotes are maintained (think JSON) (`JSON={"foo": "bar"}` becomes `{JSON:"{\"foo\": \"bar\"}"`)
|
||||
- whitespace is removed from both ends of unquoted values (see more on [`trim`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim)) (`FOO= some value ` becomes `{FOO: 'some value'}`)
|
||||
- single and double quoted values are escaped (`SINGLE_QUOTE='quoted'` becomes `{SINGLE_QUOTE: "quoted"}`)
|
||||
- single and double quoted values maintain whitespace from both ends (`FOO=" some value "` becomes `{FOO: ' some value '}`)
|
||||
- double quoted values expand new lines (`MULTILINE="new\nline"` becomes
|
||||
|
||||
```
|
||||
{MULTILINE: 'new
|
||||
line'}
|
||||
```
|
||||
|
||||
- backticks are supported (`` BACKTICK_KEY=`This has 'single' and "double" quotes inside of it.` ``)
|
||||
|
||||
### What happens to environment variables that were already set?
|
||||
|
||||
By default, we will never modify any environment variables that have already been set. In particular, if there is a variable in your `.env` file which collides with one that already exists in your environment, then that variable will be skipped.
|
||||
|
||||
If instead, you want to override `process.env` use the `override` option.
|
||||
|
||||
```javascript
|
||||
require('dotenv').config({ override: true })
|
||||
```
|
||||
|
||||
### How come my environment variables are not showing up for React?
|
||||
|
||||
Your React code is run in Webpack, where the `fs` module or even the `process` global itself are not accessible out-of-the-box. `process.env` can only be injected through Webpack configuration.
|
||||
|
||||
If you are using [`react-scripts`](https://www.npmjs.com/package/react-scripts), which is distributed through [`create-react-app`](https://create-react-app.dev/), it has dotenv built in but with a quirk. Preface your environment variables with `REACT_APP_`. See [this stack overflow](https://stackoverflow.com/questions/42182577/is-it-possible-to-use-dotenv-in-a-react-project) for more details.
|
||||
|
||||
If you are using other frameworks (e.g. Next.js, Gatsby...), you need to consult their documentation for how to inject environment variables into the client.
|
||||
|
||||
### Can I customize/write plugins for dotenv?
|
||||
|
||||
Yes! `dotenv.config()` returns an object representing the parsed `.env` file. This gives you everything you need to continue setting values on `process.env`. For example:
|
||||
|
||||
```js
|
||||
const dotenv = require('dotenv')
|
||||
const variableExpansion = require('dotenv-expand')
|
||||
const myEnv = dotenv.config()
|
||||
variableExpansion(myEnv)
|
||||
```
|
||||
|
||||
### How do I use dotenv with `import`?
|
||||
|
||||
Simply..
|
||||
|
||||
```javascript
|
||||
// index.mjs (ESM)
|
||||
import 'dotenv/config' // see https://github.com/motdotla/dotenv#how-do-i-use-dotenv-with-import
|
||||
import express from 'express'
|
||||
```
|
||||
|
||||
A little background..
|
||||
|
||||
> When you run a module containing an `import` declaration, the modules it imports are loaded first, then each module body is executed in a depth-first traversal of the dependency graph, avoiding cycles by skipping anything already executed.
|
||||
>
|
||||
> – [ES6 In Depth: Modules](https://hacks.mozilla.org/2015/08/es6-in-depth-modules/)
|
||||
|
||||
What does this mean in plain language? It means you would think the following would work but it won't.
|
||||
|
||||
`errorReporter.mjs`:
|
||||
```js
|
||||
class Client {
|
||||
constructor (apiKey) {
|
||||
console.log('apiKey', apiKey)
|
||||
|
||||
this.apiKey = apiKey
|
||||
}
|
||||
}
|
||||
|
||||
export default new Client(process.env.API_KEY)
|
||||
```
|
||||
`index.mjs`:
|
||||
```js
|
||||
// Note: this is INCORRECT and will not work
|
||||
import * as dotenv from 'dotenv'
|
||||
dotenv.config()
|
||||
|
||||
import errorReporter from './errorReporter.mjs' // process.env.API_KEY will be blank!
|
||||
```
|
||||
|
||||
`process.env.API_KEY` will be blank.
|
||||
|
||||
Instead, `index.mjs` should be written as..
|
||||
|
||||
```js
|
||||
import 'dotenv/config'
|
||||
|
||||
import errorReporter from './errorReporter.mjs'
|
||||
```
|
||||
|
||||
Does that make sense? It's a bit unintuitive, but it is how importing of ES6 modules work. Here is a [working example of this pitfall](https://github.com/dotenv-org/examples/tree/master/usage/dotenv-es6-import-pitfall).
|
||||
|
||||
There are two alternatives to this approach:
|
||||
|
||||
1. Preload with dotenvx: `dotenvx run -- node index.js` (_Note: you do not need to `import` dotenv with this approach_)
|
||||
2. Create a separate file that will execute `config` first as outlined in [this comment on #133](https://github.com/motdotla/dotenv/issues/133#issuecomment-255298822)
|
||||
|
||||
### Why am I getting the error `Module not found: Error: Can't resolve 'crypto|os|path'`?
|
||||
|
||||
You are using dotenv on the front-end and have not included a polyfill. Webpack < 5 used to include these for you. Do the following:
|
||||
|
||||
```bash
|
||||
npm install node-polyfill-webpack-plugin
|
||||
```
|
||||
|
||||
Configure your `webpack.config.js` to something like the following.
|
||||
|
||||
```js
|
||||
require('dotenv').config()
|
||||
|
||||
const path = require('path');
|
||||
const webpack = require('webpack')
|
||||
|
||||
const NodePolyfillPlugin = require('node-polyfill-webpack-plugin')
|
||||
|
||||
module.exports = {
|
||||
mode: 'development',
|
||||
entry: './src/index.ts',
|
||||
output: {
|
||||
filename: 'bundle.js',
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
},
|
||||
plugins: [
|
||||
new NodePolyfillPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
HELLO: JSON.stringify(process.env.HELLO)
|
||||
}
|
||||
}),
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
Alternatively, just use [dotenv-webpack](https://github.com/mrsteele/dotenv-webpack) which does this and more behind the scenes for you.
|
||||
|
||||
### What about variable expansion?
|
||||
|
||||
Try [dotenv-expand](https://github.com/motdotla/dotenv-expand)
|
||||
|
||||
### What about syncing and securing .env files?
|
||||
|
||||
Use [dotenvx](https://github.com/dotenvx/dotenvx) to unlock syncing encrypted .env files over git.
|
||||
|
||||
### What if I accidentally commit my `.env` file to code?
|
||||
|
||||
Remove it, [remove git history](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository) and then install the [git pre-commit hook](https://github.com/dotenvx/dotenvx#pre-commit) to prevent this from ever happening again.
|
||||
|
||||
```
|
||||
brew install dotenvx/brew/dotenvx
|
||||
dotenvx precommit --install
|
||||
```
|
||||
|
||||
### How can I prevent committing my `.env` file to a Docker build?
|
||||
|
||||
Use the [docker prebuild hook](https://dotenvx.com/docs/features/prebuild).
|
||||
|
||||
```bash
|
||||
# Dockerfile
|
||||
...
|
||||
RUN curl -fsS https://dotenvx.sh/ | sh
|
||||
...
|
||||
RUN dotenvx prebuild
|
||||
CMD ["dotenvx", "run", "--", "node", "index.js"]
|
||||
```
|
||||
|
||||
## Contributing Guide
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||
|
||||
## CHANGELOG
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md)
|
||||
|
||||
## Who's using dotenv?
|
||||
|
||||
[These npm modules depend on it.](https://www.npmjs.com/browse/depended/dotenv)
|
||||
|
||||
Projects that expand it often use the [keyword "dotenv" on npm](https://www.npmjs.com/search?q=keywords:dotenv).
|
||||
1
node_modules/dotenv/SECURITY.md
generated
vendored
Normal file
@ -0,0 +1 @@
|
||||
Please report any security vulnerabilities to security@dotenvx.com.
|
||||
1
node_modules/dotenv/config.d.ts
generated
vendored
Normal file
@ -0,0 +1 @@
|
||||
export {};
|
||||
9
node_modules/dotenv/config.js
generated
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
(function () {
|
||||
require('./lib/main').config(
|
||||
Object.assign(
|
||||
{},
|
||||
require('./lib/env-options'),
|
||||
require('./lib/cli-options')(process.argv)
|
||||
)
|
||||
)
|
||||
})()
|
||||
17
node_modules/dotenv/lib/cli-options.js
generated
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
const re = /^dotenv_config_(encoding|path|quiet|debug|override|DOTENV_KEY)=(.+)$/
|
||||
|
||||
module.exports = function optionMatcher (args) {
|
||||
const options = args.reduce(function (acc, cur) {
|
||||
const matches = cur.match(re)
|
||||
if (matches) {
|
||||
acc[matches[1]] = matches[2]
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
if (!('quiet' in options)) {
|
||||
options.quiet = 'true'
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
28
node_modules/dotenv/lib/env-options.js
generated
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
// ../config.js accepts options via environment variables
|
||||
const options = {}
|
||||
|
||||
if (process.env.DOTENV_CONFIG_ENCODING != null) {
|
||||
options.encoding = process.env.DOTENV_CONFIG_ENCODING
|
||||
}
|
||||
|
||||
if (process.env.DOTENV_CONFIG_PATH != null) {
|
||||
options.path = process.env.DOTENV_CONFIG_PATH
|
||||
}
|
||||
|
||||
if (process.env.DOTENV_CONFIG_QUIET != null) {
|
||||
options.quiet = process.env.DOTENV_CONFIG_QUIET
|
||||
}
|
||||
|
||||
if (process.env.DOTENV_CONFIG_DEBUG != null) {
|
||||
options.debug = process.env.DOTENV_CONFIG_DEBUG
|
||||
}
|
||||
|
||||
if (process.env.DOTENV_CONFIG_OVERRIDE != null) {
|
||||
options.override = process.env.DOTENV_CONFIG_OVERRIDE
|
||||
}
|
||||
|
||||
if (process.env.DOTENV_CONFIG_DOTENV_KEY != null) {
|
||||
options.DOTENV_KEY = process.env.DOTENV_CONFIG_DOTENV_KEY
|
||||
}
|
||||
|
||||
module.exports = options
|
||||
170
node_modules/dotenv/lib/main.d.ts
generated
vendored
Normal file
@ -0,0 +1,170 @@
|
||||
// TypeScript Version: 3.0
|
||||
/// <reference types="node" />
|
||||
import type { URL } from 'url';
|
||||
|
||||
export interface DotenvParseOutput {
|
||||
[name: string]: string;
|
||||
}
|
||||
|
||||
export interface DotenvPopulateOutput {
|
||||
[name: string]: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a string or buffer in the .env file format into an object.
|
||||
*
|
||||
* See https://dotenvx.com/docs
|
||||
*
|
||||
* @param src - contents to be parsed. example: `'DB_HOST=localhost'`
|
||||
* @returns an object with keys and values based on `src`. example: `{ DB_HOST : 'localhost' }`
|
||||
*/
|
||||
export function parse<T extends DotenvParseOutput = DotenvParseOutput>(
|
||||
src: string | Buffer
|
||||
): T;
|
||||
|
||||
export interface DotenvConfigOptions {
|
||||
/**
|
||||
* Default: `path.resolve(process.cwd(), '.env')`
|
||||
*
|
||||
* Specify a custom path if your file containing environment variables is located elsewhere.
|
||||
* Can also be an array of strings, specifying multiple paths.
|
||||
*
|
||||
* example: `require('dotenv').config({ path: '/custom/path/to/.env' })`
|
||||
* example: `require('dotenv').config({ path: ['/path/to/first.env', '/path/to/second.env'] })`
|
||||
*/
|
||||
path?: string | string[] | URL;
|
||||
|
||||
/**
|
||||
* Default: `utf8`
|
||||
*
|
||||
* Specify the encoding of your file containing environment variables.
|
||||
*
|
||||
* example: `require('dotenv').config({ encoding: 'latin1' })`
|
||||
*/
|
||||
encoding?: string;
|
||||
|
||||
/**
|
||||
* Default: `false`
|
||||
*
|
||||
* Suppress all output (except errors).
|
||||
*
|
||||
* example: `require('dotenv').config({ quiet: true })`
|
||||
*/
|
||||
quiet?: boolean;
|
||||
|
||||
/**
|
||||
* Default: `false`
|
||||
*
|
||||
* Turn on logging to help debug why certain keys or values are not being set as you expect.
|
||||
*
|
||||
* example: `require('dotenv').config({ debug: process.env.DEBUG })`
|
||||
*/
|
||||
debug?: boolean;
|
||||
|
||||
/**
|
||||
* Default: `false`
|
||||
*
|
||||
* Override any environment variables that have already been set on your machine with values from your .env file.
|
||||
*
|
||||
* example: `require('dotenv').config({ override: true })`
|
||||
*/
|
||||
override?: boolean;
|
||||
|
||||
/**
|
||||
* Default: `process.env`
|
||||
*
|
||||
* Specify an object to write your secrets to. Defaults to process.env environment variables.
|
||||
*
|
||||
* example: `const processEnv = {}; require('dotenv').config({ processEnv: processEnv })`
|
||||
*/
|
||||
processEnv?: DotenvPopulateInput;
|
||||
|
||||
/**
|
||||
* Default: `undefined`
|
||||
*
|
||||
* Pass the DOTENV_KEY directly to config options. Defaults to looking for process.env.DOTENV_KEY environment variable. Note this only applies to decrypting .env.vault files. If passed as null or undefined, or not passed at all, dotenv falls back to its traditional job of parsing a .env file.
|
||||
*
|
||||
* example: `require('dotenv').config({ DOTENV_KEY: 'dotenv://:key_1234…@dotenvx.com/vault/.env.vault?environment=production' })`
|
||||
*/
|
||||
DOTENV_KEY?: string;
|
||||
}
|
||||
|
||||
export interface DotenvConfigOutput {
|
||||
error?: Error;
|
||||
parsed?: DotenvParseOutput;
|
||||
}
|
||||
|
||||
export interface DotenvPopulateOptions {
|
||||
/**
|
||||
* Default: `false`
|
||||
*
|
||||
* Turn on logging to help debug why certain keys or values are not being set as you expect.
|
||||
*
|
||||
* example: `require('dotenv').config({ debug: process.env.DEBUG })`
|
||||
*/
|
||||
debug?: boolean;
|
||||
|
||||
/**
|
||||
* Default: `false`
|
||||
*
|
||||
* Override any environment variables that have already been set on your machine with values from your .env file.
|
||||
*
|
||||
* example: `require('dotenv').config({ override: true })`
|
||||
*/
|
||||
override?: boolean;
|
||||
}
|
||||
|
||||
export interface DotenvPopulateInput {
|
||||
[name: string]: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads `.env` file contents into process.env by default. If `DOTENV_KEY` is present, it smartly attempts to load encrypted `.env.vault` file contents into process.env.
|
||||
*
|
||||
* See https://dotenvx.com/docs
|
||||
*
|
||||
* @param options - additional options. example: `{ path: './custom/path', encoding: 'latin1', quiet: false, debug: true, override: false }`
|
||||
* @returns an object with a `parsed` key if successful or `error` key if an error occurred. example: { parsed: { KEY: 'value' } }
|
||||
*
|
||||
*/
|
||||
export function config(options?: DotenvConfigOptions): DotenvConfigOutput;
|
||||
|
||||
/**
|
||||
* Loads `.env` file contents into process.env.
|
||||
*
|
||||
* See https://dotenvx.com/docs
|
||||
*
|
||||
* @param options - additional options. example: `{ path: './custom/path', encoding: 'latin1', quiet: false, debug: true, override: false }`
|
||||
* @returns an object with a `parsed` key if successful or `error` key if an error occurred. example: { parsed: { KEY: 'value' } }
|
||||
*
|
||||
*/
|
||||
export function configDotenv(options?: DotenvConfigOptions): DotenvConfigOutput;
|
||||
|
||||
/**
|
||||
* Loads `source` json contents into `target` like process.env.
|
||||
*
|
||||
* See https://dotenvx.com/docs
|
||||
*
|
||||
* @param processEnv - the target JSON object. in most cases use process.env but you can also pass your own JSON object
|
||||
* @param parsed - the source JSON object
|
||||
* @param options - additional options. example: `{ quiet: false, debug: true, override: false }`
|
||||
* @returns an object with the keys and values that were actually set
|
||||
*
|
||||
*/
|
||||
export function populate(
|
||||
processEnv: DotenvPopulateInput,
|
||||
parsed: DotenvPopulateInput,
|
||||
options?: DotenvConfigOptions
|
||||
): DotenvPopulateOutput;
|
||||
|
||||
/**
|
||||
* Decrypt ciphertext
|
||||
*
|
||||
* See https://dotenvx.com/docs
|
||||
*
|
||||
* @param encrypted - the encrypted ciphertext string
|
||||
* @param keyStr - the decryption key string
|
||||
* @returns {string}
|
||||
*
|
||||
*/
|
||||
export function decrypt(encrypted: string, keyStr: string): string;
|
||||
431
node_modules/dotenv/lib/main.js
generated
vendored
Normal file
@ -0,0 +1,431 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const os = require('os')
|
||||
const crypto = require('crypto')
|
||||
const packageJson = require('../package.json')
|
||||
|
||||
const version = packageJson.version
|
||||
|
||||
// Array of tips to display randomly
|
||||
const TIPS = [
|
||||
'🔐 encrypt with Dotenvx: https://dotenvx.com',
|
||||
'🔐 prevent committing .env to code: https://dotenvx.com/precommit',
|
||||
'🔐 prevent building .env in docker: https://dotenvx.com/prebuild',
|
||||
'📡 observe env with Radar: https://dotenvx.com/radar',
|
||||
'📡 auto-backup env with Radar: https://dotenvx.com/radar',
|
||||
'📡 version env with Radar: https://dotenvx.com/radar',
|
||||
'🛠️ run anywhere with `dotenvx run -- yourcommand`',
|
||||
'⚙️ specify custom .env file path with { path: \'/custom/path/.env\' }',
|
||||
'⚙️ enable debug logging with { debug: true }',
|
||||
'⚙️ override existing env vars with { override: true }',
|
||||
'⚙️ suppress all logs with { quiet: true }',
|
||||
'⚙️ write to custom object with { processEnv: myObject }',
|
||||
'⚙️ load multiple .env files with { path: [\'.env.local\', \'.env\'] }'
|
||||
]
|
||||
|
||||
// Get a random tip from the tips array
|
||||
function _getRandomTip () {
|
||||
return TIPS[Math.floor(Math.random() * TIPS.length)]
|
||||
}
|
||||
|
||||
function parseBoolean (value) {
|
||||
if (typeof value === 'string') {
|
||||
return !['false', '0', 'no', 'off', ''].includes(value.toLowerCase())
|
||||
}
|
||||
return Boolean(value)
|
||||
}
|
||||
|
||||
function supportsAnsi () {
|
||||
return process.stdout.isTTY // && process.env.TERM !== 'dumb'
|
||||
}
|
||||
|
||||
function dim (text) {
|
||||
return supportsAnsi() ? `\x1b[2m${text}\x1b[0m` : text
|
||||
}
|
||||
|
||||
const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg
|
||||
|
||||
// Parse src into an Object
|
||||
function parse (src) {
|
||||
const obj = {}
|
||||
|
||||
// Convert buffer to string
|
||||
let lines = src.toString()
|
||||
|
||||
// Convert line breaks to same format
|
||||
lines = lines.replace(/\r\n?/mg, '\n')
|
||||
|
||||
let match
|
||||
while ((match = LINE.exec(lines)) != null) {
|
||||
const key = match[1]
|
||||
|
||||
// Default undefined or null to empty string
|
||||
let value = (match[2] || '')
|
||||
|
||||
// Remove whitespace
|
||||
value = value.trim()
|
||||
|
||||
// Check if double quoted
|
||||
const maybeQuote = value[0]
|
||||
|
||||
// Remove surrounding quotes
|
||||
value = value.replace(/^(['"`])([\s\S]*)\1$/mg, '$2')
|
||||
|
||||
// Expand newlines if double quoted
|
||||
if (maybeQuote === '"') {
|
||||
value = value.replace(/\\n/g, '\n')
|
||||
value = value.replace(/\\r/g, '\r')
|
||||
}
|
||||
|
||||
// Add to object
|
||||
obj[key] = value
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
function _parseVault (options) {
|
||||
options = options || {}
|
||||
|
||||
const vaultPath = _vaultPath(options)
|
||||
options.path = vaultPath // parse .env.vault
|
||||
const result = DotenvModule.configDotenv(options)
|
||||
if (!result.parsed) {
|
||||
const err = new Error(`MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`)
|
||||
err.code = 'MISSING_DATA'
|
||||
throw err
|
||||
}
|
||||
|
||||
// handle scenario for comma separated keys - for use with key rotation
|
||||
// example: DOTENV_KEY="dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=prod,dotenv://:key_7890@dotenvx.com/vault/.env.vault?environment=prod"
|
||||
const keys = _dotenvKey(options).split(',')
|
||||
const length = keys.length
|
||||
|
||||
let decrypted
|
||||
for (let i = 0; i < length; i++) {
|
||||
try {
|
||||
// Get full key
|
||||
const key = keys[i].trim()
|
||||
|
||||
// Get instructions for decrypt
|
||||
const attrs = _instructions(result, key)
|
||||
|
||||
// Decrypt
|
||||
decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key)
|
||||
|
||||
break
|
||||
} catch (error) {
|
||||
// last key
|
||||
if (i + 1 >= length) {
|
||||
throw error
|
||||
}
|
||||
// try next key
|
||||
}
|
||||
}
|
||||
|
||||
// Parse decrypted .env string
|
||||
return DotenvModule.parse(decrypted)
|
||||
}
|
||||
|
||||
function _warn (message) {
|
||||
console.error(`[dotenv@${version}][WARN] ${message}`)
|
||||
}
|
||||
|
||||
function _debug (message) {
|
||||
console.log(`[dotenv@${version}][DEBUG] ${message}`)
|
||||
}
|
||||
|
||||
function _log (message) {
|
||||
console.log(`[dotenv@${version}] ${message}`)
|
||||
}
|
||||
|
||||
function _dotenvKey (options) {
|
||||
// prioritize developer directly setting options.DOTENV_KEY
|
||||
if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) {
|
||||
return options.DOTENV_KEY
|
||||
}
|
||||
|
||||
// secondary infra already contains a DOTENV_KEY environment variable
|
||||
if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) {
|
||||
return process.env.DOTENV_KEY
|
||||
}
|
||||
|
||||
// fallback to empty string
|
||||
return ''
|
||||
}
|
||||
|
||||
function _instructions (result, dotenvKey) {
|
||||
// Parse DOTENV_KEY. Format is a URI
|
||||
let uri
|
||||
try {
|
||||
uri = new URL(dotenvKey)
|
||||
} catch (error) {
|
||||
if (error.code === 'ERR_INVALID_URL') {
|
||||
const err = new Error('INVALID_DOTENV_KEY: Wrong format. Must be in valid uri format like dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=development')
|
||||
err.code = 'INVALID_DOTENV_KEY'
|
||||
throw err
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
// Get decrypt key
|
||||
const key = uri.password
|
||||
if (!key) {
|
||||
const err = new Error('INVALID_DOTENV_KEY: Missing key part')
|
||||
err.code = 'INVALID_DOTENV_KEY'
|
||||
throw err
|
||||
}
|
||||
|
||||
// Get environment
|
||||
const environment = uri.searchParams.get('environment')
|
||||
if (!environment) {
|
||||
const err = new Error('INVALID_DOTENV_KEY: Missing environment part')
|
||||
err.code = 'INVALID_DOTENV_KEY'
|
||||
throw err
|
||||
}
|
||||
|
||||
// Get ciphertext payload
|
||||
const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`
|
||||
const ciphertext = result.parsed[environmentKey] // DOTENV_VAULT_PRODUCTION
|
||||
if (!ciphertext) {
|
||||
const err = new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment ${environmentKey} in your .env.vault file.`)
|
||||
err.code = 'NOT_FOUND_DOTENV_ENVIRONMENT'
|
||||
throw err
|
||||
}
|
||||
|
||||
return { ciphertext, key }
|
||||
}
|
||||
|
||||
function _vaultPath (options) {
|
||||
let possibleVaultPath = null
|
||||
|
||||
if (options && options.path && options.path.length > 0) {
|
||||
if (Array.isArray(options.path)) {
|
||||
for (const filepath of options.path) {
|
||||
if (fs.existsSync(filepath)) {
|
||||
possibleVaultPath = filepath.endsWith('.vault') ? filepath : `${filepath}.vault`
|
||||
}
|
||||
}
|
||||
} else {
|
||||
possibleVaultPath = options.path.endsWith('.vault') ? options.path : `${options.path}.vault`
|
||||
}
|
||||
} else {
|
||||
possibleVaultPath = path.resolve(process.cwd(), '.env.vault')
|
||||
}
|
||||
|
||||
if (fs.existsSync(possibleVaultPath)) {
|
||||
return possibleVaultPath
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function _resolveHome (envPath) {
|
||||
return envPath[0] === '~' ? path.join(os.homedir(), envPath.slice(1)) : envPath
|
||||
}
|
||||
|
||||
function _configVault (options) {
|
||||
const debug = parseBoolean(process.env.DOTENV_CONFIG_DEBUG || (options && options.debug))
|
||||
const quiet = parseBoolean(process.env.DOTENV_CONFIG_QUIET || (options && options.quiet))
|
||||
|
||||
if (debug || !quiet) {
|
||||
_log('Loading env from encrypted .env.vault')
|
||||
}
|
||||
|
||||
const parsed = DotenvModule._parseVault(options)
|
||||
|
||||
let processEnv = process.env
|
||||
if (options && options.processEnv != null) {
|
||||
processEnv = options.processEnv
|
||||
}
|
||||
|
||||
DotenvModule.populate(processEnv, parsed, options)
|
||||
|
||||
return { parsed }
|
||||
}
|
||||
|
||||
function configDotenv (options) {
|
||||
const dotenvPath = path.resolve(process.cwd(), '.env')
|
||||
let encoding = 'utf8'
|
||||
let processEnv = process.env
|
||||
if (options && options.processEnv != null) {
|
||||
processEnv = options.processEnv
|
||||
}
|
||||
let debug = parseBoolean(processEnv.DOTENV_CONFIG_DEBUG || (options && options.debug))
|
||||
let quiet = parseBoolean(processEnv.DOTENV_CONFIG_QUIET || (options && options.quiet))
|
||||
|
||||
if (options && options.encoding) {
|
||||
encoding = options.encoding
|
||||
} else {
|
||||
if (debug) {
|
||||
_debug('No encoding is specified. UTF-8 is used by default')
|
||||
}
|
||||
}
|
||||
|
||||
let optionPaths = [dotenvPath] // default, look for .env
|
||||
if (options && options.path) {
|
||||
if (!Array.isArray(options.path)) {
|
||||
optionPaths = [_resolveHome(options.path)]
|
||||
} else {
|
||||
optionPaths = [] // reset default
|
||||
for (const filepath of options.path) {
|
||||
optionPaths.push(_resolveHome(filepath))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build the parsed data in a temporary object (because we need to return it). Once we have the final
|
||||
// parsed data, we will combine it with process.env (or options.processEnv if provided).
|
||||
let lastError
|
||||
const parsedAll = {}
|
||||
for (const path of optionPaths) {
|
||||
try {
|
||||
// Specifying an encoding returns a string instead of a buffer
|
||||
const parsed = DotenvModule.parse(fs.readFileSync(path, { encoding }))
|
||||
|
||||
DotenvModule.populate(parsedAll, parsed, options)
|
||||
} catch (e) {
|
||||
if (debug) {
|
||||
_debug(`Failed to load ${path} ${e.message}`)
|
||||
}
|
||||
lastError = e
|
||||
}
|
||||
}
|
||||
|
||||
const populated = DotenvModule.populate(processEnv, parsedAll, options)
|
||||
|
||||
// handle user settings DOTENV_CONFIG_ options inside .env file(s)
|
||||
debug = parseBoolean(processEnv.DOTENV_CONFIG_DEBUG || debug)
|
||||
quiet = parseBoolean(processEnv.DOTENV_CONFIG_QUIET || quiet)
|
||||
|
||||
if (debug || !quiet) {
|
||||
const keysCount = Object.keys(populated).length
|
||||
const shortPaths = []
|
||||
for (const filePath of optionPaths) {
|
||||
try {
|
||||
const relative = path.relative(process.cwd(), filePath)
|
||||
shortPaths.push(relative)
|
||||
} catch (e) {
|
||||
if (debug) {
|
||||
_debug(`Failed to load ${filePath} ${e.message}`)
|
||||
}
|
||||
lastError = e
|
||||
}
|
||||
}
|
||||
|
||||
_log(`injecting env (${keysCount}) from ${shortPaths.join(',')} ${dim(`-- tip: ${_getRandomTip()}`)}`)
|
||||
}
|
||||
|
||||
if (lastError) {
|
||||
return { parsed: parsedAll, error: lastError }
|
||||
} else {
|
||||
return { parsed: parsedAll }
|
||||
}
|
||||
}
|
||||
|
||||
// Populates process.env from .env file
|
||||
function config (options) {
|
||||
// fallback to original dotenv if DOTENV_KEY is not set
|
||||
if (_dotenvKey(options).length === 0) {
|
||||
return DotenvModule.configDotenv(options)
|
||||
}
|
||||
|
||||
const vaultPath = _vaultPath(options)
|
||||
|
||||
// dotenvKey exists but .env.vault file does not exist
|
||||
if (!vaultPath) {
|
||||
_warn(`You set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}. Did you forget to build it?`)
|
||||
|
||||
return DotenvModule.configDotenv(options)
|
||||
}
|
||||
|
||||
return DotenvModule._configVault(options)
|
||||
}
|
||||
|
||||
function decrypt (encrypted, keyStr) {
|
||||
const key = Buffer.from(keyStr.slice(-64), 'hex')
|
||||
let ciphertext = Buffer.from(encrypted, 'base64')
|
||||
|
||||
const nonce = ciphertext.subarray(0, 12)
|
||||
const authTag = ciphertext.subarray(-16)
|
||||
ciphertext = ciphertext.subarray(12, -16)
|
||||
|
||||
try {
|
||||
const aesgcm = crypto.createDecipheriv('aes-256-gcm', key, nonce)
|
||||
aesgcm.setAuthTag(authTag)
|
||||
return `${aesgcm.update(ciphertext)}${aesgcm.final()}`
|
||||
} catch (error) {
|
||||
const isRange = error instanceof RangeError
|
||||
const invalidKeyLength = error.message === 'Invalid key length'
|
||||
const decryptionFailed = error.message === 'Unsupported state or unable to authenticate data'
|
||||
|
||||
if (isRange || invalidKeyLength) {
|
||||
const err = new Error('INVALID_DOTENV_KEY: It must be 64 characters long (or more)')
|
||||
err.code = 'INVALID_DOTENV_KEY'
|
||||
throw err
|
||||
} else if (decryptionFailed) {
|
||||
const err = new Error('DECRYPTION_FAILED: Please check your DOTENV_KEY')
|
||||
err.code = 'DECRYPTION_FAILED'
|
||||
throw err
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Populate process.env with parsed values
|
||||
function populate (processEnv, parsed, options = {}) {
|
||||
const debug = Boolean(options && options.debug)
|
||||
const override = Boolean(options && options.override)
|
||||
const populated = {}
|
||||
|
||||
if (typeof parsed !== 'object') {
|
||||
const err = new Error('OBJECT_REQUIRED: Please check the processEnv argument being passed to populate')
|
||||
err.code = 'OBJECT_REQUIRED'
|
||||
throw err
|
||||
}
|
||||
|
||||
// Set process.env
|
||||
for (const key of Object.keys(parsed)) {
|
||||
if (Object.prototype.hasOwnProperty.call(processEnv, key)) {
|
||||
if (override === true) {
|
||||
processEnv[key] = parsed[key]
|
||||
populated[key] = parsed[key]
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
if (override === true) {
|
||||
_debug(`"${key}" is already defined and WAS overwritten`)
|
||||
} else {
|
||||
_debug(`"${key}" is already defined and was NOT overwritten`)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
processEnv[key] = parsed[key]
|
||||
populated[key] = parsed[key]
|
||||
}
|
||||
}
|
||||
|
||||
return populated
|
||||
}
|
||||
|
||||
const DotenvModule = {
|
||||
configDotenv,
|
||||
_configVault,
|
||||
_parseVault,
|
||||
config,
|
||||
decrypt,
|
||||
parse,
|
||||
populate
|
||||
}
|
||||
|
||||
module.exports.configDotenv = DotenvModule.configDotenv
|
||||
module.exports._configVault = DotenvModule._configVault
|
||||
module.exports._parseVault = DotenvModule._parseVault
|
||||
module.exports.config = DotenvModule.config
|
||||
module.exports.decrypt = DotenvModule.decrypt
|
||||
module.exports.parse = DotenvModule.parse
|
||||
module.exports.populate = DotenvModule.populate
|
||||
|
||||
module.exports = DotenvModule
|
||||
62
node_modules/dotenv/package.json
generated
vendored
Normal file
@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "dotenv",
|
||||
"version": "17.2.1",
|
||||
"description": "Loads environment variables from .env file",
|
||||
"main": "lib/main.js",
|
||||
"types": "lib/main.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./lib/main.d.ts",
|
||||
"require": "./lib/main.js",
|
||||
"default": "./lib/main.js"
|
||||
},
|
||||
"./config": "./config.js",
|
||||
"./config.js": "./config.js",
|
||||
"./lib/env-options": "./lib/env-options.js",
|
||||
"./lib/env-options.js": "./lib/env-options.js",
|
||||
"./lib/cli-options": "./lib/cli-options.js",
|
||||
"./lib/cli-options.js": "./lib/cli-options.js",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"scripts": {
|
||||
"dts-check": "tsc --project tests/types/tsconfig.json",
|
||||
"lint": "standard",
|
||||
"pretest": "npm run lint && npm run dts-check",
|
||||
"test": "tap run --allow-empty-coverage --disable-coverage --timeout=60000",
|
||||
"test:coverage": "tap run --show-full-coverage --timeout=60000 --coverage-report=text --coverage-report=lcov",
|
||||
"prerelease": "npm test",
|
||||
"release": "standard-version"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/motdotla/dotenv.git"
|
||||
},
|
||||
"homepage": "https://github.com/motdotla/dotenv#readme",
|
||||
"funding": "https://dotenvx.com",
|
||||
"keywords": [
|
||||
"dotenv",
|
||||
"env",
|
||||
".env",
|
||||
"environment",
|
||||
"variables",
|
||||
"config",
|
||||
"settings"
|
||||
],
|
||||
"readmeFilename": "README.md",
|
||||
"license": "BSD-2-Clause",
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.3",
|
||||
"decache": "^4.6.2",
|
||||
"sinon": "^14.0.1",
|
||||
"standard": "^17.0.0",
|
||||
"standard-version": "^9.5.0",
|
||||
"tap": "^19.2.0",
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"browser": {
|
||||
"fs": false
|
||||
}
|
||||
}
|
||||
6
node_modules/nodemailer/.gitattributes
generated
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
*.js text eol=lf
|
||||
*.txt text eol=lf
|
||||
*.html text eol=lf
|
||||
*.htm text eol=lf
|
||||
*.ics -text
|
||||
*.bin -text
|
||||
11
node_modules/nodemailer/.ncurc.js
generated
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
upgrade: true,
|
||||
reject: [
|
||||
// API changes break existing tests
|
||||
'proxy',
|
||||
|
||||
// API changes
|
||||
'eslint',
|
||||
'eslint-config-prettier'
|
||||
]
|
||||
};
|
||||
8
node_modules/nodemailer/.prettierrc.js
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
printWidth: 160,
|
||||
tabWidth: 4,
|
||||
singleQuote: true,
|
||||
endOfLine: 'lf',
|
||||
trailingComma: 'none',
|
||||
arrowParens: 'avoid'
|
||||
};
|
||||
896
node_modules/nodemailer/CHANGELOG.md
generated
vendored
Normal file
@ -0,0 +1,896 @@
|
||||
# CHANGELOG
|
||||
|
||||
## [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 <mailto:andris@kreata.ee> 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: <http://www.andrisreinman.com/nodemailer-v1-0/#migrationguide>
|
||||
|
||||
## 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
|
||||
76
node_modules/nodemailer/CODE_OF_CONDUCT.md
generated
vendored
Normal file
@ -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
|
||||
16
node_modules/nodemailer/LICENSE
generated
vendored
Normal file
@ -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.
|
||||
86
node_modules/nodemailer/README.md
generated
vendored
Normal file
@ -0,0 +1,86 @@
|
||||
# Nodemailer
|
||||
|
||||
[](https://nodemailer.com/about/)
|
||||
|
||||
Send emails from Node.js – easy as cake! 🍰✉️
|
||||
|
||||
[](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).
|
||||
22
node_modules/nodemailer/SECURITY.txt
generated
vendored
Normal file
@ -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-----
|
||||
327
node_modules/nodemailer/lib/addressparser/index.js
generated
vendored
Normal file
@ -0,0 +1,327 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Converts tokens for a single address into an address object
|
||||
*
|
||||
* @param {Array} tokens Tokens object
|
||||
* @return {Object} Address object
|
||||
*/
|
||||
function _handleAddress(tokens) {
|
||||
let isGroup = false;
|
||||
let state = 'text';
|
||||
let address;
|
||||
let addresses = [];
|
||||
let data = {
|
||||
address: [],
|
||||
comment: [],
|
||||
group: [],
|
||||
text: []
|
||||
};
|
||||
let i;
|
||||
let len;
|
||||
|
||||
// Filter out <addresses>, (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';
|
||||
break;
|
||||
case '(':
|
||||
state = 'comment';
|
||||
break;
|
||||
case ':':
|
||||
state = 'group';
|
||||
isGroup = true;
|
||||
break;
|
||||
default:
|
||||
state = 'text';
|
||||
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;
|
||||
} else {
|
||||
data[state].push(token.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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(' ');
|
||||
addresses.push({
|
||||
name: data.text || (address && address.name),
|
||||
group: data.group.length ? addressparser(data.group.join(',')) : []
|
||||
});
|
||||
} 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--) {
|
||||
if (data.text[i].match(/^[^@\s]+@[^@\s]+$/)) {
|
||||
data.address = data.text.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--) {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses structured e-mail addresses from an address field
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* 'Name <address@domain>'
|
||||
*
|
||||
* will be converted to
|
||||
*
|
||||
* [{name: 'Name', address: 'address@domain'}]
|
||||
*
|
||||
* @param {String} str Address field
|
||||
* @return {Array} An array of address objects
|
||||
*/
|
||||
function addressparser(str, options) {
|
||||
options = options || {};
|
||||
|
||||
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);
|
||||
if (address.length) {
|
||||
parsedAddresses = parsedAddresses.concat(address);
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
142
node_modules/nodemailer/lib/base64/index.js
generated
vendored
Normal file
@ -0,0 +1,142 @@
|
||||
'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')
|
||||
.trim();
|
||||
result.push(wrappedLines);
|
||||
pos += chunkLength;
|
||||
}
|
||||
|
||||
return result.join('\r\n').trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
// init Transform
|
||||
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);
|
||||
|
||||
// remove last line as it is still most probably incomplete
|
||||
let lastLF = b64.lastIndexOf('\n');
|
||||
if (lastLF < 0) {
|
||||
this._curLine = b64;
|
||||
b64 = '';
|
||||
} else if (lastLF === b64.length - 1) {
|
||||
this._curLine = '';
|
||||
} else {
|
||||
this._curLine = b64.substr(lastLF + 1);
|
||||
b64 = b64.substr(0, lastLF + 1);
|
||||
}
|
||||
}
|
||||
|
||||
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._curLine = wrap(this._curLine, this.options.lineLength);
|
||||
this.outputBytes += this._curLine.length;
|
||||
this.push(this._curLine, 'ascii');
|
||||
this._curLine = '';
|
||||
}
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
// expose to the world
|
||||
module.exports = {
|
||||
encode,
|
||||
wrap,
|
||||
Encoder
|
||||
};
|
||||
251
node_modules/nodemailer/lib/dkim/index.js
generated
vendored
Normal file
@ -0,0 +1,251 @@
|
||||
'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;
|
||||
155
node_modules/nodemailer/lib/dkim/message-parser.js
generated
vendored
Normal file
@ -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;
|
||||
154
node_modules/nodemailer/lib/dkim/relaxed-body.js
generated
vendored
Normal file
@ -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;
|
||||
117
node_modules/nodemailer/lib/dkim/sign.js
generated
vendored
Normal file
@ -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();
|
||||
}
|
||||
281
node_modules/nodemailer/lib/fetch/cookies.js
generated
vendored
Normal file
@ -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;
|
||||
274
node_modules/nodemailer/lib/fetch/index.js
generated
vendored
Normal file
@ -0,0 +1,274 @@
|
||||
'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 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.type = 'FETCH';
|
||||
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.type = 'FETCH';
|
||||
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.type = 'FETCH';
|
||||
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.type = 'FETCH';
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
});
|
||||
}
|
||||
|
||||
req.on('error', err => {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
err.type = 'FETCH';
|
||||
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.type = 'FETCH';
|
||||
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.type = 'FETCH';
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
req.abort();
|
||||
return;
|
||||
}
|
||||
|
||||
res.on('error', err => {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
err.type = 'FETCH';
|
||||
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.type = 'FETCH';
|
||||
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.type = 'FETCH';
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
req.end();
|
||||
});
|
||||
|
||||
return fetchRes;
|
||||
}
|
||||
82
node_modules/nodemailer/lib/json-transport/index.js
generated
vendored
Normal file
@ -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'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Compiles a mailcomposer message and forwards it to handler that sends it.</p>
|
||||
*
|
||||
* @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;
|
||||
577
node_modules/nodemailer/lib/mail-composer/index.js
generated
vendored
Normal file
@ -0,0 +1,577 @@
|
||||
/* 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) {
|
||||
contentTransferEncoding = '7bit';
|
||||
} 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) {
|
||||
let parsedDataUri;
|
||||
if ((element.path || element.href).match(/^data:/)) {
|
||||
parsedDataUri = parseDataURI(element.path || element.href);
|
||||
}
|
||||
|
||||
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;
|
||||
434
node_modules/nodemailer/lib/mailer/index.js
generated
vendored
Normal file
@ -0,0 +1,434 @@
|
||||
'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 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')) {
|
||||
return callback(new Error('Socks module not loaded'));
|
||||
}
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
callback(new Error('Unknown proxy configuration'));
|
||||
};
|
||||
}
|
||||
|
||||
_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(/(<img\b[^<>]{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;
|
||||
315
node_modules/nodemailer/lib/mailer/mail-message.js
generated
vendored
Normal file
@ -0,0 +1,315 @@
|
||||
'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 <protocol:url>
|
||||
return Object.keys(listData).map(key => ({
|
||||
key: 'list-' + key.toLowerCase().trim(),
|
||||
value: [].concat(listData[key] || []).map(value => ({
|
||||
prepared: true,
|
||||
foldLines: true,
|
||||
value: []
|
||||
.concat(value || [])
|
||||
.map(value => {
|
||||
if (typeof value === 'string') {
|
||||
value = {
|
||||
url: value
|
||||
};
|
||||
}
|
||||
|
||||
if (value && value.url) {
|
||||
if (key.toLowerCase().trim() === 'id') {
|
||||
// List-ID: "comment" <domain>
|
||||
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-*: <http://domain> (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 '<mailto:' + url + '>';
|
||||
}
|
||||
|
||||
return '<http://' + url + '>';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MailMessage;
|
||||
625
node_modules/nodemailer/lib/mime-funcs/index.js
generated
vendored
Normal file
@ -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\x27\x27';
|
||||
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));
|
||||
}
|
||||
};
|
||||
2104
node_modules/nodemailer/lib/mime-funcs/mime-types.js
generated
vendored
Normal file
1314
node_modules/nodemailer/lib/mime-node/index.js
generated
vendored
Normal file
33
node_modules/nodemailer/lib/mime-node/last-newline.js
generated
vendored
Normal file
@ -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;
|
||||
43
node_modules/nodemailer/lib/mime-node/le-unix.js
generated
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
'use strict';
|
||||
|
||||
const stream = require('stream');
|
||||
const Transform = stream.Transform;
|
||||
|
||||
/**
|
||||
* Ensures that only <LF> 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;
|
||||
52
node_modules/nodemailer/lib/mime-node/le-windows.js
generated
vendored
Normal file
@ -0,0 +1,52 @@
|
||||
'use strict';
|
||||
|
||||
const stream = require('stream');
|
||||
const Transform = stream.Transform;
|
||||
|
||||
/**
|
||||
* Ensures that only <CR><LF> 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;
|
||||
155
node_modules/nodemailer/lib/nodemailer.js
generated
vendored
Normal file
@ -0,0 +1,155 @@
|
||||
'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 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 = 'LegacyConfig';
|
||||
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;
|
||||
};
|
||||
460
node_modules/nodemailer/lib/punycode/index.js
generated
vendored
Normal file
@ -0,0 +1,460 @@
|
||||
/*
|
||||
|
||||
Copied from https://github.com/mathiasbynens/punycode.js/blob/ef3505c8abb5143a00d53ce59077c9f7f4b2ac47/punycode.js
|
||||
|
||||
Copyright Mathias Bynens <https://mathiasbynens.be/>
|
||||
|
||||
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 <https://mathiasbynens.be/notes/javascript-encoding>
|
||||
* @memberOf punycode.ucs2
|
||||
* @name decode
|
||||
* @param {String} string The Unicode input string (UCS-2).
|
||||
* @returns {Array} The new array of code points.
|
||||
*/
|
||||
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 <n,i> state to <m,0>,
|
||||
// but guard against overflow.
|
||||
const handledCPCountPlusOne = handledCPCount + 1;
|
||||
if (m - n > floor((maxInt - delta) / handledCPCountPlusOne)) {
|
||||
error('overflow');
|
||||
}
|
||||
|
||||
delta += (m - n) * handledCPCountPlusOne;
|
||||
n = m;
|
||||
|
||||
for (const currentValue of input) {
|
||||
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 <https://mathiasbynens.be/notes/javascript-encoding>
|
||||
* @memberOf punycode
|
||||
* @type Object
|
||||
*/
|
||||
ucs2: {
|
||||
decode: ucs2decode,
|
||||
encode: ucs2encode
|
||||
},
|
||||
decode: decode,
|
||||
encode: encode,
|
||||
toASCII: toASCII,
|
||||
toUnicode: toUnicode
|
||||
};
|
||||
|
||||
module.exports = punycode;
|
||||
219
node_modules/nodemailer/lib/qp/index.js
generated
vendored
Normal file
@ -0,0 +1,219 @@
|
||||
'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], // <TAB>
|
||||
[0x0a], // <LF>
|
||||
[0x0d], // <CR>
|
||||
[0x20, 0x3c], // <SP>!"#$%&'()*+,-./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
|
||||
};
|
||||
210
node_modules/nodemailer/lib/sendmail-transport/index.js
generated
vendored
Normal file
@ -0,0 +1,210 @@
|
||||
'use strict';
|
||||
|
||||
const spawn = require('child_process').spawn;
|
||||
const packageData = require('../../package.json');
|
||||
const shared = require('../shared');
|
||||
|
||||
/**
|
||||
* 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Compiles a mailcomposer message and forwards it to handler that sends it.</p>
|
||||
*
|
||||
* @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) {
|
||||
return done(new Error('Can not send mail. Invalid envelope addresses.'));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 {
|
||||
return callback(new Error('sendmail was not found'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SendmailTransport;
|
||||
234
node_modules/nodemailer/lib/ses-transport/index.js
generated
vendored
Normal file
@ -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: <invalid@invalid>\r\nTo: <invalid@invalid>\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;
|
||||
688
node_modules/nodemailer/lib/shared/index.js
generated
vendored
Normal file
@ -0,0 +1,688 @@
|
||||
/* 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;
|
||||
|
||||
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 || {});
|
||||
}
|
||||
|
||||
return Object.assign(
|
||||
{
|
||||
servername: value.servername,
|
||||
host:
|
||||
!value.addresses || !value.addresses.length
|
||||
? null
|
||||
: value.addresses.length === 1
|
||||
? value.addresses[0]
|
||||
: value.addresses[Math.floor(Math.random() * value.addresses.length)]
|
||||
},
|
||||
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);
|
||||
|
||||
if (!cached.expires || cached.expires >= Date.now()) {
|
||||
return callback(
|
||||
null,
|
||||
formatDNSValue(cached.value, {
|
||||
cached: true
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
resolver(4, options.host, options, (err, addresses) => {
|
||||
if (err) {
|
||||
if (cached) {
|
||||
// ignore error, use expired value
|
||||
return callback(
|
||||
null,
|
||||
formatDNSValue(cached.value, {
|
||||
cached: true,
|
||||
error: err
|
||||
})
|
||||
);
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (addresses && addresses.length) {
|
||||
let value = {
|
||||
addresses,
|
||||
servername: options.servername || options.host
|
||||
};
|
||||
|
||||
dnsCache.set(options.host, {
|
||||
value,
|
||||
expires: Date.now() + (options.dnsTtl || DNS_TTL)
|
||||
});
|
||||
|
||||
return callback(
|
||||
null,
|
||||
formatDNSValue(value, {
|
||||
cached: false
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
resolver(6, options.host, options, (err, addresses) => {
|
||||
if (err) {
|
||||
if (cached) {
|
||||
// ignore error, use expired value
|
||||
return callback(
|
||||
null,
|
||||
formatDNSValue(cached.value, {
|
||||
cached: true,
|
||||
error: err
|
||||
})
|
||||
);
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (addresses && addresses.length) {
|
||||
let value = {
|
||||
addresses,
|
||||
servername: options.servername || options.host
|
||||
};
|
||||
|
||||
dnsCache.set(options.host, {
|
||||
value,
|
||||
expires: Date.now() + (options.dnsTtl || DNS_TTL)
|
||||
});
|
||||
|
||||
return callback(
|
||||
null,
|
||||
formatDNSValue(value, {
|
||||
cached: false
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
dns.lookup(options.host, { all: true }, (err, addresses) => {
|
||||
if (err) {
|
||||
if (cached) {
|
||||
// ignore error, use expired value
|
||||
return callback(
|
||||
null,
|
||||
formatDNSValue(cached.value, {
|
||||
cached: true,
|
||||
error: err
|
||||
})
|
||||
);
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let address = addresses
|
||||
? addresses
|
||||
.filter(addr => isFamilySupported(addr.family))
|
||||
.map(addr => addr.address)
|
||||
.shift()
|
||||
: false;
|
||||
|
||||
if (addresses && addresses.length && !address) {
|
||||
// there are addresses but none can be used
|
||||
console.warn(`Failed to resolve IPv${addresses[0].family} addresses with current network`);
|
||||
}
|
||||
|
||||
if (!address && cached) {
|
||||
// nothing was found, fallback to cached value
|
||||
return callback(
|
||||
null,
|
||||
formatDNSValue(cached.value, {
|
||||
cached: true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
let value = {
|
||||
addresses: address ? [address] : [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 (err) {
|
||||
if (cached) {
|
||||
// ignore error, use expired value
|
||||
return callback(
|
||||
null,
|
||||
formatDNSValue(cached.value, {
|
||||
cached: true,
|
||||
error: err
|
||||
})
|
||||
);
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
/**
|
||||
* 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 => {
|
||||
let input = uri;
|
||||
let commaPos = input.indexOf(',');
|
||||
if (!commaPos) {
|
||||
return uri;
|
||||
}
|
||||
|
||||
let data = input.substring(commaPos + 1);
|
||||
let metaStr = input.substring('data:'.length, commaPos);
|
||||
|
||||
let encoding;
|
||||
|
||||
let metaEntries = metaStr.split(';');
|
||||
let lastMetaEntry = metaEntries.length > 1 ? metaEntries[metaEntries.length - 1] : false;
|
||||
if (lastMetaEntry && lastMetaEntry.indexOf('=') < 0) {
|
||||
encoding = lastMetaEntry.toLowerCase();
|
||||
metaEntries.pop();
|
||||
}
|
||||
|
||||
let contentType = metaEntries.shift() || 'application/octet-stream';
|
||||
let params = {};
|
||||
for (let entry of metaEntries) {
|
||||
let sep = entry.indexOf('=');
|
||||
if (sep >= 0) {
|
||||
let key = entry.substring(0, sep);
|
||||
let value = entry.substring(sep + 1);
|
||||
params[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
switch (encoding) {
|
||||
case 'base64':
|
||||
data = Buffer.from(data, 'base64');
|
||||
break;
|
||||
case 'utf8':
|
||||
data = Buffer.from(data);
|
||||
break;
|
||||
default:
|
||||
try {
|
||||
data = Buffer.from(decodeURIComponent(data));
|
||||
} catch (err) {
|
||||
data = Buffer.from(data);
|
||||
}
|
||||
data = Buffer.from(data);
|
||||
}
|
||||
|
||||
return { data, encoding, contentType, 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;
|
||||
}
|
||||
108
node_modules/nodemailer/lib/smtp-connection/data-stream.js
generated
vendored
Normal file
@ -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 <CR><LF>.<CR><LF>
|
||||
* Also makes sure that only <CR><LF> 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;
|
||||
143
node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js
generated
vendored
Normal file
@ -0,0 +1,143 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Minimal HTTP/S proxy client
|
||||
*/
|
||||
|
||||
const net = require('net');
|
||||
const tls = require('tls');
|
||||
const urllib = require('url');
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
return callback(new Error('Invalid response from proxy' + ((match && ': ' + match[1]) || '')));
|
||||
}
|
||||
|
||||
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;
|
||||
1836
node_modules/nodemailer/lib/smtp-connection/index.js
generated
vendored
Normal file
652
node_modules/nodemailer/lib/smtp-pool/index.js
generated
vendored
Normal file
@ -0,0 +1,652 @@
|
||||
'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 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.error(
|
||||
{
|
||||
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 = 'NoAuth';
|
||||
|
||||
returned = true;
|
||||
connection.close();
|
||||
return callback(err);
|
||||
} else {
|
||||
finalize();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
||||
// expose to the world
|
||||
module.exports = SMTPPool;
|
||||
253
node_modules/nodemailer/lib/smtp-pool/pool-resource.js
generated
vendored
Normal file
@ -0,0 +1,253 @@
|
||||
'use strict';
|
||||
|
||||
const SMTPConnection = require('../smtp-connection');
|
||||
const assign = require('../shared').assign;
|
||||
const XOAuth2 = require('../xoauth2');
|
||||
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 = '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;
|
||||
}
|
||||
|
||||
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 = '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;
|
||||
416
node_modules/nodemailer/lib/smtp-transport/index.js
generated
vendored
Normal file
@ -0,0 +1,416 @@
|
||||
'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 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 = '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;
|
||||
}
|
||||
|
||||
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 = 'NoAuth';
|
||||
|
||||
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;
|
||||
135
node_modules/nodemailer/lib/stream-transport/index.js
generated
vendored
Normal file
@ -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 ? '<CR><LF>' : '<LF>'
|
||||
);
|
||||
|
||||
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;
|
||||
47
node_modules/nodemailer/lib/well-known/index.js
generated
vendored
Normal file
@ -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;
|
||||
};
|
||||
558
node_modules/nodemailer/lib/well-known/services.json
generated
vendored
Normal file
@ -0,0 +1,558 @@
|
||||
{
|
||||
"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
|
||||
},
|
||||
|
||||
"Bluewin": {
|
||||
"description": "Bluewin (Swiss email provider)",
|
||||
"host": "smtpauths.bluewin.ch",
|
||||
"domains": ["bluewin.ch"],
|
||||
"port": 465
|
||||
},
|
||||
|
||||
"DebugMail": {
|
||||
"description": "DebugMail (email testing service)",
|
||||
"host": "debugmail.io",
|
||||
"port": 25
|
||||
},
|
||||
|
||||
"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
|
||||
},
|
||||
|
||||
"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
|
||||
},
|
||||
|
||||
"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
|
||||
},
|
||||
|
||||
"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
|
||||
},
|
||||
|
||||
"Zoho": {
|
||||
"description": "Zoho Mail",
|
||||
"host": "smtp.zoho.com",
|
||||
"port": 465,
|
||||
"secure": true,
|
||||
"authMethod": "LOGIN"
|
||||
}
|
||||
}
|
||||
376
node_modules/nodemailer/lib/xoauth2/index.js
generated
vendored
Normal file
@ -0,0 +1,376 @@
|
||||
'use strict';
|
||||
|
||||
const Stream = require('stream').Stream;
|
||||
const nmfetch = require('../fetch');
|
||||
const crypto = require('crypto');
|
||||
const shared = require('../shared');
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
setImmediate(() => this.emit('error', new Error('Options "privateKey" and "user" are required for service account!')));
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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())) {
|
||||
return callback(null, this.accessToken);
|
||||
}
|
||||
|
||||
let generateCallback = (...args) => {
|
||||
if (args[0]) {
|
||||
this.logger.error(
|
||||
{
|
||||
err: args[0],
|
||||
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
|
||||
);
|
||||
}
|
||||
callback(...args);
|
||||
};
|
||||
|
||||
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) {
|
||||
return callback(new Error('Can\x27t generate token. Check your auth options'));
|
||||
}
|
||||
|
||||
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) {
|
||||
return callback(new Error('Can\x27t create new access token for user'));
|
||||
}
|
||||
|
||||
// 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()
|
||||
);
|
||||
return callback(new Error('Invalid authentication response'));
|
||||
}
|
||||
|
||||
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 + ')';
|
||||
}
|
||||
return callback(new Error(errorMessage));
|
||||
}
|
||||
|
||||
if (data.access_token) {
|
||||
this.updateToken(data.access_token, data.expires_in);
|
||||
return callback(null, this.accessToken);
|
||||
}
|
||||
|
||||
return callback(new Error('No access token'));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
43
node_modules/nodemailer/package.json
generated
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "nodemailer",
|
||||
"version": "7.0.5",
|
||||
"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",
|
||||
"lint": "eslint .",
|
||||
"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.839.0",
|
||||
"bunyan": "1.8.15",
|
||||
"c8": "10.1.3",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-nodemailer": "1.2.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"libbase64": "1.3.0",
|
||||
"libmime": "5.3.7",
|
||||
"libqp": "2.1.1",
|
||||
"nodemailer-ntlm-auth": "1.0.4",
|
||||
"proxy": "1.0.2",
|
||||
"proxy-test-server": "1.0.0",
|
||||
"smtp-server": "3.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
}
|
||||
34
package-lock.json
generated
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "Test",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"dotenv": "^17.2.1",
|
||||
"nodemailer": "^7.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "17.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz",
|
||||
"integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.5.tgz",
|
||||
"integrity": "sha512-nsrh2lO3j4GkLLXoeEksAMgAOqxOv6QumNRVQTJwKH4nuiww6iC2y7GyANs9kRAxCexg3+lTWM3PZ91iLlVjfg==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
6
package.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"dotenv": "^17.2.1",
|
||||
"nodemailer": "^7.0.5"
|
||||
}
|
||||
}
|
||||
225
public/css/datepicker.material.css
Executable file
@ -0,0 +1,225 @@
|
||||
.datepicker {
|
||||
display: inline-block;
|
||||
-webkit-tap-highlight-color: rgba(0,0,0,0);
|
||||
}
|
||||
|
||||
.datepicker table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.datepicker table th,
|
||||
.datepicker table td {
|
||||
width: calc(100% / 7);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.datepicker table th {
|
||||
color: #90CAF9;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.5rem;
|
||||
line-height: 4;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.datepicker__wrapper {
|
||||
color: #333;
|
||||
border-radius: 0.125rem;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
padding: 0.5rem;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background: white;
|
||||
border: 1px solid #4A90E2;
|
||||
width: 16rem;
|
||||
}
|
||||
|
||||
.datepicker__wrapper::after {
|
||||
content: '';
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.datepicker:not(.is-inline) .datepicker__wrapper {
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.2);
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.datepicker__header {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
background: #2196F3;
|
||||
padding: 0.25rem;
|
||||
margin: -0.5rem -0.5rem 0;
|
||||
}
|
||||
|
||||
.datepicker__title {
|
||||
display: inline-block;
|
||||
padding: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.datepicker__prev,
|
||||
.datepicker__next {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
outline: none;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 50%;
|
||||
background: no-repeat center / 60%;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.datepicker__prev:hover,
|
||||
.datepicker__next:hover {
|
||||
background-color: #1E88E5;
|
||||
}
|
||||
|
||||
.datepicker__prev {
|
||||
float: left;
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><path d="M15 8.25H5.87l4.19-4.19L9 3 3 9l6 6 1.06-1.06-4.19-4.19H15v-1.5z" fill="white"/></svg>');
|
||||
}
|
||||
|
||||
.datepicker__next {
|
||||
float: right;
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><path d="M9 3L7.94 4.06l4.19 4.19H3v1.5h9.13l-4.19 4.19L9 15l6-6z" fill="white"/></svg>');
|
||||
}
|
||||
|
||||
.datepicker__prev.is-disabled,
|
||||
.datepicker__next.is-disabled {
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.datepicker__time {
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 0 -0.5rem;
|
||||
font-size: 0.75rem;
|
||||
text-align: right;
|
||||
background: #E3F2FD;
|
||||
border-bottom: 1px solid #BBDEFB;
|
||||
}
|
||||
|
||||
.datepicker__label {
|
||||
margin-right: 1rem;
|
||||
color: rgba(0,0,0,0.4);
|
||||
float: left;
|
||||
}
|
||||
|
||||
.datepicker__field {
|
||||
display: inline-block;
|
||||
margin: 0 0.125rem;
|
||||
color: #333;
|
||||
width: 2ch;
|
||||
}
|
||||
|
||||
.datepicker__field span {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.datepicker__day {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.datepicker__day div {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
background: transparent;
|
||||
position: relative;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.datepicker__day div::after {
|
||||
content: '';
|
||||
display: block;
|
||||
border-radius: 50%;
|
||||
padding-top: 100%;
|
||||
position: relative;
|
||||
background: inherit;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.datepicker__day div:hover,
|
||||
.datepicker__day.is-highlighted div {
|
||||
background: #E3F2FD;
|
||||
}
|
||||
|
||||
.datepicker__day.is-today {
|
||||
color: #2196F3;
|
||||
}
|
||||
|
||||
.datepicker__day.is-today div::after {
|
||||
box-shadow: inset 0 0 0 1px currentColor;
|
||||
}
|
||||
|
||||
.datepicker__day.is-today.is-disabled.is-selected div::after,
|
||||
.datepicker__day.is-today.is-otherMonth.is-selected div::after {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.datepicker__day.is-selected div {
|
||||
background: #2196F3;
|
||||
}
|
||||
|
||||
.datepicker__day.is-selected:hover div::after {
|
||||
background: #1E88E5;
|
||||
}
|
||||
|
||||
.datepicker__day.is-selected .datepicker__daynum {
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.datepicker__day.is-selected + .is-selected div::before,
|
||||
.datepicker__day.is-highlighted + .is-highlighted div::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: -50%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: inherit;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.datepicker__day.is-disabled,
|
||||
.datepicker__day.is-otherMonth {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
color: #BBDEFB;
|
||||
}
|
||||
|
||||
.datepicker__day.is-disabled.is-selected .datepicker__daynum,
|
||||
.datepicker__day.is-otherMonth.is-selected .datepicker__daynum {
|
||||
color: rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.datepicker__day.is-disabled.is-selected div,
|
||||
.datepicker__day.is-otherMonth.is-selected div,
|
||||
.datepicker__day.is-disabled.is-selected + .is-selected div::before,
|
||||
.datepicker__day.is-otherMonth.is-selected + .is-selected div::before {
|
||||
background: #E3F2FD;
|
||||
}
|
||||
|
||||
.datepicker__daynum {
|
||||
position: absolute;
|
||||
top: 50%; left: 0;
|
||||
width: 100%;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
margin-top: -0.5rem;
|
||||
text-align: center;
|
||||
z-index: 2;
|
||||
}
|
||||
285
public/css/style.css
Executable file
@ -0,0 +1,285 @@
|
||||
body {
|
||||
background-color: #F9F9F9;
|
||||
margin: 0;
|
||||
font-family:'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif;
|
||||
}
|
||||
.header {
|
||||
background-color: #4A90E2;
|
||||
text-align: center;
|
||||
}
|
||||
.forms-container h1 {
|
||||
margin-top: 4vh;
|
||||
color: #333333;
|
||||
}
|
||||
.forms-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 2vw;
|
||||
margin-top: 2vh;
|
||||
}
|
||||
.form {
|
||||
width: 40%;
|
||||
height: 95vh;
|
||||
background-color: white;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
border-radius: 4vh;
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
.inputs {
|
||||
width: 80%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2vh;
|
||||
margin-top: 3vh;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
input {
|
||||
height: 4vh;
|
||||
text-align: center;
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
border-color: #4A90E2;
|
||||
font-size: 2vh;
|
||||
}
|
||||
::placeholder {
|
||||
text-align: center;
|
||||
}
|
||||
input[type="date"],
|
||||
input[type="time"] {
|
||||
text-align: center;
|
||||
height: 4vh;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.datepicker__wrapper {
|
||||
width: 300px !important; /* or any desired width */
|
||||
min-width: 200px !important;
|
||||
max-width: 100%;
|
||||
margin-left: 0 !important;
|
||||
margin-right: auto !important;
|
||||
font-size: 2.5vh !important;
|
||||
margin-left: auto !important;
|
||||
margin-right: auto !important;
|
||||
margin-top: 2vh !important;
|
||||
}
|
||||
|
||||
.picker-container {
|
||||
position: relative;
|
||||
width: 60px; /* smaller width */
|
||||
flex: 0 0 70px; /* don't grow, don't shrink, fixed 60px */
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.picker {
|
||||
height: 150px; /* visible area */
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
.picker::-webkit-scrollbar {
|
||||
display: none; /* Chrome/Safari */
|
||||
}
|
||||
|
||||
.picker div {
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.4rem;
|
||||
color: #aaa; /* grayed out by default */
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.picker div.active {
|
||||
color: #222; /* normal color for selected */
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
margin-top: -25px;
|
||||
pointer-events: none;
|
||||
border-top: 2px solid #4A90E2;
|
||||
border-bottom: 2px solid #4A90E2;
|
||||
}
|
||||
|
||||
.pickers-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between; /* Left and right alignment */
|
||||
align-items: center;
|
||||
gap: 2vw;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.calendar {
|
||||
width: 20vh; /* Increased width */
|
||||
}
|
||||
|
||||
.submitBtn {
|
||||
width: 100%;
|
||||
height: 5vh;
|
||||
background-color: #4A90E2;
|
||||
color: white;
|
||||
border-style: hidden;
|
||||
font-size: large;
|
||||
cursor: pointer;
|
||||
}
|
||||
.formBottom {
|
||||
margin-top: auto;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.listBookings {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow-y: scroll;
|
||||
max-height: 66vh;
|
||||
min-height: 66vh;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.no-bookings {
|
||||
margin-top: 50%;
|
||||
font-size: 1.2rem;
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.booking-item {
|
||||
background-color: #f9f9f9;
|
||||
border: 1px solid #ddd;
|
||||
padding: 12px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 1px 1px 5px rgba(0,0,0,0.05);
|
||||
border-radius: 2vh;
|
||||
}
|
||||
.booking-item h3 {
|
||||
margin-bottom: 0; /* Reduce space below the title */
|
||||
}
|
||||
.bookingContent {
|
||||
display: flex; /* make this a flex container */
|
||||
flex-direction: row;
|
||||
gap: 0;
|
||||
justify-content: center; /* Left and right alignment */
|
||||
align-items: center;
|
||||
}
|
||||
.bookingContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between; /* Left and right alignment */
|
||||
align-items: center;
|
||||
}
|
||||
.bookingBtns {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
.closed {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#btn:disabled {
|
||||
background-color: #8294aa;
|
||||
color: white; /* Optional: make text visible */
|
||||
cursor: not-allowed; /* Optional: show disabled cursor */
|
||||
opacity: 0.6; /* Optional: slightly transparent */
|
||||
}
|
||||
|
||||
.adminHeader {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-top: 2vh;
|
||||
margin-bottom: 3vh;
|
||||
}
|
||||
|
||||
/* txt appears when hovered */
|
||||
.userinfo-target {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.userinfo-box {
|
||||
position: absolute;
|
||||
top: 125%; /* BELOW the h3 */
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
padding: 6px 10px;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.userinfo-box::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 100%; /* arrow points UP now */
|
||||
left: 50%;
|
||||
margin-left: -5px;
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent #333 transparent;
|
||||
}
|
||||
|
||||
.userinfo-target:hover .userinfo-box {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.forms-container {
|
||||
flex-direction: column;
|
||||
gap: 2vh;
|
||||
align-items: center;
|
||||
}
|
||||
.form {
|
||||
width: 90%;
|
||||
height: auto;
|
||||
margin-bottom: 2vh;
|
||||
}
|
||||
.datepicker__wrapper {
|
||||
width: 200px !important; /* Smaller width on mobile */
|
||||
min-width: 120px !important;
|
||||
font-size: 2vh !important;
|
||||
}
|
||||
.bookingContainer {
|
||||
position: relative; /* needed so button anchors to this box */
|
||||
}
|
||||
|
||||
.bookingContainer .closed {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0.5rem; /* tweak vertical placement */
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
padding-top: 8vh;
|
||||
padding-right: 2vh;
|
||||
}
|
||||
.backBtn{
|
||||
padding-top: 10vh;
|
||||
}
|
||||
}
|
||||
801
public/datepicker.js
Executable file
@ -0,0 +1,801 @@
|
||||
! function(e, t) {
|
||||
"object" == typeof exports && "undefined" != typeof module ? module.exports = t() : "function" == typeof define && define.amd ? define(t) : e.Datepicker = t()
|
||||
}(this, function() {
|
||||
"use strict";
|
||||
var e = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(e) {
|
||||
return typeof e
|
||||
} : function(e) {
|
||||
return e && "function" == typeof Symbol && e.constructor === Symbol && e !== Symbol.prototype ? "symbol" : typeof e
|
||||
},
|
||||
t = function(e, t) {
|
||||
if (!(e instanceof t)) throw new TypeError("Cannot call a class as a function")
|
||||
},
|
||||
n = function() {
|
||||
function e(e, t) {
|
||||
for (var n = 0; n < t.length; n++) {
|
||||
var i = t[n];
|
||||
i.enumerable = i.enumerable || !1, i.configurable = !0, "value" in i && (i.writable = !0), Object.defineProperty(e, i.key, i)
|
||||
}
|
||||
}
|
||||
return function(t, n, i) {
|
||||
return n && e(t.prototype, n), i && e(t, i), t
|
||||
}
|
||||
}(),
|
||||
i = Object.assign || function(e) {
|
||||
for (var t = 1; t < arguments.length; t++) {
|
||||
var n = arguments[t];
|
||||
for (var i in n) Object.prototype.hasOwnProperty.call(n, i) && (e[i] = n[i])
|
||||
}
|
||||
return e
|
||||
};
|
||||
|
||||
function a(e, t) {
|
||||
var n = (t || document).querySelectorAll(e);
|
||||
return Array.prototype.slice.call(n)
|
||||
}
|
||||
|
||||
function r(e, t) {
|
||||
var n = e.matches || e.matchesSelector || e.webkitMatchesSelector || e.msMatchesSelector;
|
||||
return n && n.call(e, t)
|
||||
}
|
||||
|
||||
function s(e, t, n) {
|
||||
for (var i = n && !n.contains(e); e && !i;) {
|
||||
if (r(e, t)) return e;
|
||||
i = n && !n.contains(e.parentNode), e = e.parentNode
|
||||
}
|
||||
return !1
|
||||
}
|
||||
|
||||
function o(e, t) {
|
||||
e.classList.add.apply(e.classList, t.split(" ").filter(Boolean))
|
||||
}
|
||||
|
||||
function l(e, t) {
|
||||
e.classList.remove.apply(e.classList, t.split(" ").filter(Boolean))
|
||||
}
|
||||
|
||||
function h(e, t, n) {
|
||||
void 0 === n && (n = ! function(e, t) {
|
||||
return t && e.classList.contains(t)
|
||||
}(e, t)), t && (n ? o(e, t) : l(e, t))
|
||||
}
|
||||
|
||||
function d(e, t) {
|
||||
return e instanceof Date && (t = e.getMonth(), e = e.getFullYear()), [31, function(e) {
|
||||
return e % 4 == 0 && e % 100 != 0 || e % 400 == 0
|
||||
}(e) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][t]
|
||||
}
|
||||
|
||||
function u(e, t, n) {
|
||||
for (var i = 0; i < t.length; i++) {
|
||||
var a = e,
|
||||
r = t[i];
|
||||
if ("year" == n ? (a = a.getFullYear(), r = r.getFullYear()) : "month" == n ? (a = a.getMonth(), r = r.getMonth()) : (a = a.getTime(), r = r.getTime()), a == r) return !0
|
||||
}
|
||||
return !1
|
||||
}
|
||||
|
||||
function c(e, t) {
|
||||
return e.getTime() - t.getTime()
|
||||
}
|
||||
|
||||
function p(e) {
|
||||
return !!e && e instanceof Date && !isNaN(e.getTime())
|
||||
}
|
||||
|
||||
function f(e) {
|
||||
return _(e, function(e) {
|
||||
return e && e.setHours(0, 0, 0, 0), e
|
||||
})
|
||||
}
|
||||
|
||||
function g(e, t) {
|
||||
var n = e = new Date(e);
|
||||
e > (t = new Date(t)) && (e = t, t = n, n = e);
|
||||
for (var i = [new Date(n)]; n < t;) n.setDate(n.getDate() + 1), i.push(new Date(n));
|
||||
return i
|
||||
}
|
||||
|
||||
function m(t) {
|
||||
if ("object" == (void 0 === t ? "undefined" : e(t)) && null !== t) {
|
||||
var n = Object.getPrototypeOf(t);
|
||||
return n === Object.prototype || null === n
|
||||
}
|
||||
return !1
|
||||
}
|
||||
|
||||
function v(t) {
|
||||
for (var n = Array.prototype.slice.call(arguments, 1), i = 0; i < n.length; i++)
|
||||
for (var a in n[i]) void 0 !== t[a] && "object" === e(n[i][a]) && null !== n[i][a] && void 0 === n[i][a].nodeName ? (n[i][a] instanceof Date && (t[a] = new Date(n[i][a].getTime())), Array.isArray(n[i][a]) ? t[a] = n[i][a].slice(0) : t[a] = v(t[a], n[i][a])) : t[a] = n[i][a];
|
||||
return t
|
||||
}
|
||||
|
||||
function _(e, t, n) {
|
||||
var i = [].concat(e).map(t, n);
|
||||
return 1 === i.length ? i[0] : i
|
||||
}
|
||||
|
||||
function y(e, t) {
|
||||
var n = new Function("obj", "var p=[],print=function(){p.push.apply(p,arguments);};with(obj){p.push('" + e.replace(/[\r\t\n]/g, " ").split("<%").join("\t").replace(/((^|%>)[^\t]*)'/g, "$1\r").replace(/\t=(.*?)%>/g, "',$1,'").split("\t").join("');").split("%>").join("p.push('").split("\r").join("\\'") + "');}return p.join('');");
|
||||
return t ? n(t) : n
|
||||
}
|
||||
var w = {
|
||||
inline: !1,
|
||||
multiple: !1,
|
||||
ranged: !1,
|
||||
time: !1,
|
||||
openOn: "first",
|
||||
min: !1,
|
||||
max: !1,
|
||||
within: !1,
|
||||
without: !1,
|
||||
yearRange: 5,
|
||||
weekStart: 0,
|
||||
defaultTime: {
|
||||
start: [0, 0],
|
||||
end: [12, 0]
|
||||
},
|
||||
separator: ",",
|
||||
serialize: function(e) {
|
||||
var t = e.toLocaleDateString();
|
||||
if (this.get("time")) {
|
||||
var n = e.toLocaleTimeString();
|
||||
return t + "@" + (n = n.replace(/(\d{1,2}:\d{2}):00/, "$1"))
|
||||
}
|
||||
return t
|
||||
},
|
||||
deserialize: function(e) {
|
||||
return new Date(e)
|
||||
},
|
||||
toValue: !1,
|
||||
fromValue: !1,
|
||||
onInit: !1,
|
||||
onChange: !1,
|
||||
onRender: !1,
|
||||
i18n: {
|
||||
months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
|
||||
weekdays: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
|
||||
time: ["Time", "Start", "End"]
|
||||
},
|
||||
classNames: {
|
||||
node: "datepicker",
|
||||
wrapper: "datepicker__wrapper",
|
||||
inline: "is-inline",
|
||||
selected: "is-selected",
|
||||
disabled: "is-disabled",
|
||||
highlighted: "is-highlighted",
|
||||
otherMonth: "is-otherMonth",
|
||||
weekend: "is-weekend",
|
||||
today: "is-today"
|
||||
},
|
||||
templates: {
|
||||
container: ['<div class="datepicker__container">', "<%= renderHeader() %>", "<%= renderTimepicker() %>", "<%= renderCalendar() %>", "</div>"].join(""),
|
||||
header: ['<header class="datepicker__header">', '<a class="datepicker__prev<%= (hasPrev) ? "" : " is-disabled" %>" data-prev>‹</a>', '<span class="datepicker__title"><%= renderMonthSelect() %></span>', '<span class="datepicker__title"><%= renderYearSelect() %></span>', '<a class="datepicker__next<%= (hasNext) ? "" : " is-disabled" %>" data-next>›</a>', "</header>"].join(""),
|
||||
timepicker: ['<div class="datepicker__time">', '<span class="datepicker__label"><%= label %></span>', '<span class="datepicker__field"><%= renderHourSelect() %></span>:', '<span class="datepicker__field"><%= renderMinuteSelect() %></span>', '<span class="datepicker__field"><%= renderPeriodSelect() %></span>', "</div>"].join(""),
|
||||
calendar: ['<table class="datepicker__cal">', "<thead>", "<tr>", "<% weekdays.forEach(function(name) { %>", "<th><%= name %></th>", "<% }); %>", "</tr>", "</thead>", "<tbody>", "<% days.forEach(function(day, i) { %>", '<%= (i % 7 == 0) ? "<tr>" : "" %>', "<%= renderDay(day) %>", '<%= (i % 7 == 6) ? "</tr>" : "" %>', "<% }); %>", "</tbody>", "</table>"].join(""),
|
||||
day: ['<% classNames.push("datepicker__day"); %>', '<td class="<%= classNames.join(" ") %>" data-day="<%= timestamp %>"><div>', '<span class="datepicker__daynum"><%= daynum %></span>', "</div></td>"].join("")
|
||||
}
|
||||
};
|
||||
var b = function() {
|
||||
function e(n, i) {
|
||||
var r = this;
|
||||
if (t(this, e), "string" == typeof n) {
|
||||
if ("#" != n.substr(0, 1)) return a(n).map(function(e) {
|
||||
return new r.constructor(e, i)
|
||||
});
|
||||
n = document.getElementById(n.substr(1))
|
||||
}
|
||||
n || (n = document.createElement("input")), "input" !== n.tagName.toLowerCase() || /input|hidden/i.test(n.type) || (n.type = "text"), this._initDOM(n), this._initOptions(i), this._initEvents(), this.setValue(n.value || n.dataset.value || ""), this._opts.onInit && this._opts.onInit(n)
|
||||
}
|
||||
return n(e, [{
|
||||
key: "_initOptions",
|
||||
value: function() {
|
||||
var e = this,
|
||||
t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {};
|
||||
this._opts = {};
|
||||
var n = function(e, t) {
|
||||
var n = t.classNames.inline;
|
||||
return this.node && (h(this.node, n, e), this.wrapper.style.position = e ? "" : "absolute", this.wrapper.style.display = e ? "" : "none"), this._isOpen = e, e
|
||||
}.bind(this),
|
||||
i = function(e, t) {
|
||||
var n = t.deserialize;
|
||||
return !!p(e = !!e && _(e, n, this)) && e
|
||||
}.bind(this),
|
||||
a = function(e, t) {
|
||||
var n = t.deserialize;
|
||||
return e.length && (e = f(_(e, n, this)), e = [].concat(e).filter(p)), !!e.length && e
|
||||
}.bind(this),
|
||||
r = function(e, t) {
|
||||
var n = t.deserialize;
|
||||
if ("string" != typeof e || /^(first|last|today)$/.test(e) || p(e = n.call(this, e)) || (e = new Date), !this._month) {
|
||||
var i = e;
|
||||
"string" != typeof i && p(i) || (i = new Date), (i = f(new Date(i.getTime()))).setDate(1), this._month = i
|
||||
}
|
||||
return e
|
||||
}.bind(this),
|
||||
s = function(e) {
|
||||
return Math.min(Math.max(e, 0), 6)
|
||||
}.bind(this),
|
||||
o = function(e, t) {
|
||||
return m(e) ? v({}, e, t.defaultTime) : {
|
||||
start: e.slice(0),
|
||||
end: e.slice(0)
|
||||
}
|
||||
}.bind(this),
|
||||
l = function(e, t) {
|
||||
var n = e.node,
|
||||
i = e.inline,
|
||||
a = e.wrapper,
|
||||
r = t.inline;
|
||||
if (this.node)
|
||||
for (var s in e) switch (s) {
|
||||
case "node":
|
||||
case "inline":
|
||||
this.node.className = n + (r ? " " + i : "");
|
||||
break;
|
||||
case "wrapper":
|
||||
this.wrapper.className = a
|
||||
}
|
||||
return e
|
||||
}.bind(this),
|
||||
d = function(e) {
|
||||
return "function" == typeof e && e.bind(this)
|
||||
}.bind(this),
|
||||
u = function(e) {
|
||||
for (var t in e) "select" !== t && (this._renderers[t] = y(e[t]));
|
||||
return e
|
||||
}.bind(this);
|
||||
this._set = {
|
||||
openOn: r,
|
||||
inline: n,
|
||||
weekstart: s,
|
||||
min: i,
|
||||
max: i,
|
||||
within: a,
|
||||
without: a,
|
||||
defaultTime: o,
|
||||
classNames: l,
|
||||
templates: u
|
||||
};
|
||||
["serialize", "deserialize", "onInit", "onChange", "onRender", "setValue", "getValue"].forEach(function(t) {
|
||||
return e._set[t] = d
|
||||
}), this._renderers = {
|
||||
select: y(['<span style="position:relative"><%= text %>', '<select data-<%= type %>="<%= value %>" data-index="<%= index %>"', 'style="position:absolute;top:0;left:0;width:100%;height:100%;margin:0;opacity:0.005;cursor:pointer;">', "<% options.forEach(function(o) { %>", '<option value="<%= o.value %>"', '<%= o.selected ? " selected" : "" %>', '<%= o.disabled ? " disabled" : "" %>', "><%= o.text %></option>", "<% }); %>", "</select>", "</span>"].join(""))
|
||||
}, this.set(v({}, this.constructor.defaults, function(e) {
|
||||
var t = function(e) {
|
||||
return e.trim()
|
||||
},
|
||||
n = {};
|
||||
if (!e || !e.dataset) return n;
|
||||
for (var i in e.dataset) {
|
||||
var a = e.dataset[i];
|
||||
/true|false/.test(a.toLowerCase()) ? a = "true" == a.toLowerCase() : "[" == a[0] && "]" == a.substr(-1) ? a = _(a.substr(1, a.length - 2).split(","), t) : /^\d*$/.test(a) && (a = parseInt(a, 10)), n[i] = a
|
||||
}
|
||||
return n
|
||||
}(this._el), t))
|
||||
}
|
||||
}, {
|
||||
key: "_initDOM",
|
||||
value: function(e) {
|
||||
this.node || (this._el = e, this.node = document.createElement("div"), this.node.style.position = "relative", this.wrapper = document.createElement("div"), this.wrapper.style.zIndex = 9999, e.parentNode && e.parentNode.insertBefore(this.node, e), this.node.appendChild(e), this.node.appendChild(this.wrapper))
|
||||
}
|
||||
}, {
|
||||
key: "_initEvents",
|
||||
value: function() {
|
||||
var e = this;
|
||||
this._isInitialized || (this._highlighted = [], this._onmousedown = this._onmousedown.bind(this), this._onmousemove = this._onmousemove.bind(this), this._onmouseup = this._onmouseup.bind(this), this._onclick = this._onclick.bind(this), "input" !== this._el.tagName.toLowerCase() ? this._el.addEventListener("click", function() {
|
||||
return e.toggle()
|
||||
}) : this._el.addEventListener("focus", function() {
|
||||
return e.open()
|
||||
}), document.addEventListener("mousedown", function(t) {
|
||||
e.node.contains(t.target) || e.hide()
|
||||
}), this.node.onselectstart = function() {
|
||||
return !1
|
||||
}, this.node.addEventListener("mousedown", this._onmousedown), this.node.addEventListener("mousemove", this._onmousemove), this.node.addEventListener("mouseup", this._onmouseup), this.node.addEventListener("click", this._onclick), this._isInitialized = !0)
|
||||
}
|
||||
}, {
|
||||
key: "_onmousedown",
|
||||
value: function(e) {
|
||||
var t = this,
|
||||
n = this._opts,
|
||||
i = n.ranged,
|
||||
r = n.multiple,
|
||||
d = n.classNames,
|
||||
u = d.selected,
|
||||
c = d.highlighted,
|
||||
p = s(e.target, "[data-day]", this.wrapper),
|
||||
f = p ? parseInt(p.dataset.day, 10) : null;
|
||||
f && (i && this._dragStart ? this._onmousemove(e) : (this._deselect = !i && this.hasDate(new Date(f)), this._highlighted = [f], this._dragStart = f, r || a("[data-day]." + u, this.wrapper).forEach(function(e) {
|
||||
l(e, u)
|
||||
}), a('[data-day="' + f + '"]', this.wrapper).forEach(function(e) {
|
||||
h(e, u, !t._deselect), o(e, c)
|
||||
})))
|
||||
}
|
||||
}, {
|
||||
key: "_onmousemove",
|
||||
value: function(e) {
|
||||
var t = this,
|
||||
n = this._opts,
|
||||
i = n.ranged,
|
||||
r = n.multiple,
|
||||
d = n.classNames,
|
||||
u = d.selected,
|
||||
c = d.highlighted;
|
||||
if ((i || r) && 1 === e.buttons) {
|
||||
var p = s(e.target, "[data-day]", this.wrapper),
|
||||
f = p ? parseInt(p.dataset.day, 10) : null;
|
||||
f && this._dragStart && (this._highlighted = g(this._dragStart, f).map(function(e) {
|
||||
return e.getTime()
|
||||
}), this._isDragging = f !== this._dragStart, a("[data-day]." + c, this.wrapper).forEach(function(e) {
|
||||
var n = new Date(parseInt(e.dataset.day, 10));
|
||||
h(e, u, !i && t.hasDate(n)), l(e, c)
|
||||
}), this._highlighted.forEach(function(e) {
|
||||
a('[data-day="' + e + '"]', t.wrapper).forEach(function(e) {
|
||||
h(e, u, !t._deselect), o(e, c)
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: "_onmouseup",
|
||||
value: function(e) {
|
||||
var t = this._opts,
|
||||
n = t.ranged,
|
||||
i = t.multiple,
|
||||
r = t.classNames.highlighted;
|
||||
if (a("[data-day]." + r, this.wrapper).forEach(function(e) {
|
||||
l(e, r)
|
||||
}), this._dragStart && s(e.target, "[data-day]", this.node)) {
|
||||
var o = this._highlighted.map(function(e) {
|
||||
return new Date(e)
|
||||
});
|
||||
n || !i ? this.setDate(o) : this.toggleDate(o, !this._deselect), this.render(), i || n && !this._isDragging || this.hide()
|
||||
}
|
||||
n && !this._isDragging || (this._highlighted = [], this._dragStart = null), this._isDragging = !1
|
||||
}
|
||||
}, {
|
||||
key: "_onclick",
|
||||
value: function(e) {
|
||||
var t = this,
|
||||
n = e.target;
|
||||
n.hasAttribute("data-prev") ? this.prev(n.dataset.prev) : n.hasAttribute("data-next") ? this.next(n.dataset.next) : n.hasAttribute("data-year") && !n.onchange ? n.onchange = function() {
|
||||
var e = n.dataset.year,
|
||||
i = t._month.getFullYear();
|
||||
t._month.setFullYear(parseInt(n.value) - (e - i)), t.render()
|
||||
} : n.hasAttribute("data-month") && !n.onchange ? n.onchange = function() {
|
||||
t._month.setMonth(n.value - n.dataset.index), t.render()
|
||||
} : n.hasAttribute("data-hour") && !n.onchange ? n.onchange = function() {
|
||||
t.setTime(n.dataset.hour, n.value), n.parentNode.firstChild.textContent = n.selectedOptions[0].textContent
|
||||
} : n.hasAttribute("data-minute") && !n.onchange ? n.onchange = function() {
|
||||
t.setTime(n.dataset.minute, null, n.value), n.parentNode.firstChild.textContent = n.selectedOptions[0].textContent
|
||||
} : n.hasAttribute("data-period") && !n.onchange && (n.onchange = function() {
|
||||
var e = n.dataset.period,
|
||||
i = "am" === n.value ? -12 : 12;
|
||||
a('[data-hour="' + e + '"] option', t.wrapper).forEach(function(e) {
|
||||
e.value = parseInt(e.value) + i
|
||||
}), t.setTime(e, (t._time ? t._time[e][0] : 0) + i), n.parentNode.firstChild.textContent = n.selectedOptions[0].textContent
|
||||
})
|
||||
}
|
||||
}, {
|
||||
key: "set",
|
||||
value: function(e, t) {
|
||||
if (e) {
|
||||
if (m(e)) {
|
||||
for (var n in this._noRender = !0, e.serialize && (this.set("serialize", e.serialize), delete e.serialize), e.deserialize && (this.set("deserialize", e.deserialize), delete e.deserialize), e) this.set(n, e[n]);
|
||||
this._noRender = !1, t = this._opts
|
||||
} else {
|
||||
var i = v({}, this.constructor.defaults, this._opts);
|
||||
e in this._set && (t = this._set[e](t, i)), m(t) && (t = v({}, i[e], t)), this._opts[e] = t
|
||||
}
|
||||
return this._isOpen && this.wrapper && this.render(), t
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: "get",
|
||||
value: function(e) {
|
||||
var t = this;
|
||||
if (arguments.length > 1) return [].concat(Array.prototype.slice.call(arguments)).reduce(function(e, n) {
|
||||
return e[n] = t.get(n), e
|
||||
}, {});
|
||||
var n = this._opts[e];
|
||||
return m(n) && (n = v({}, n)), n
|
||||
}
|
||||
}, {
|
||||
key: "open",
|
||||
value: function(e) {
|
||||
var t = [].concat(this.getDate());
|
||||
"string" == typeof(e = e || this._opts.openOn || this._month) && ("first" === (e = e.toLowerCase()) && t.length ? e = t[0] : "last" === e && t.length ? e = t[t.length - 1] : "today" !== e && (e = this._opts.deserialize(e))), p(e) || (e = new Date), this.setTime(!!this._selected.length), this.goToDate(e), this.render(), this.show()
|
||||
}
|
||||
}, {
|
||||
key: "show",
|
||||
value: function() {
|
||||
if (!this._opts.inline) {
|
||||
this.wrapper.style.display = "block";
|
||||
var e = this.node.getBoundingClientRect(),
|
||||
t = this._el.getBoundingClientRect(),
|
||||
n = t.bottom - e.top + "px",
|
||||
i = e.bottom - t.top + "px";
|
||||
this.wrapper.style.top = n, this.wrapper.style.right = "", this.wrapper.style.bottom = "", this.wrapper.style.left = 0;
|
||||
var a = this.wrapper.getBoundingClientRect(),
|
||||
r = a.right > window.innerWidth,
|
||||
s = a.bottom > window.innerHeight;
|
||||
this.wrapper.style.top = s ? "" : n, this.wrapper.style.right = r ? 0 : "", this.wrapper.style.bottom = s ? i : "", this.wrapper.style.left = r ? "" : 0;
|
||||
var o = (a = this.wrapper.getBoundingClientRect()).right >= a.width,
|
||||
l = a.bottom > a.height;
|
||||
this.wrapper.style.top = s && l ? "" : n, this.wrapper.style.right = r && o ? 0 : "", this.wrapper.style.bottom = s && l ? i : "", this.wrapper.style.left = r && o ? "" : 0, this._isOpen = !0
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: "hide",
|
||||
value: function() {
|
||||
this._opts.inline || (this.wrapper.style.display = "none", this._isOpen = !1)
|
||||
}
|
||||
}, {
|
||||
key: "toggle",
|
||||
value: function() {
|
||||
this._isOpen ? this.hide() : this.open()
|
||||
}
|
||||
}, {
|
||||
key: "next",
|
||||
value: function(e) {
|
||||
var t = new Date(this._month.getTime());
|
||||
e = Math.max(e || 1, 1), t.setMonth(t.getMonth() + e), this.goToDate(t)
|
||||
}
|
||||
}, {
|
||||
key: "prev",
|
||||
value: function(e) {
|
||||
var t = new Date(this._month.getTime());
|
||||
e = Math.max(e || 1, 1), t.setMonth(t.getMonth() - e), this.goToDate(t)
|
||||
}
|
||||
}, {
|
||||
key: "goToDate",
|
||||
value: function(e) {
|
||||
(e = f(this._opts.deserialize(e))).setDate(1), this._month = e, this._isOpen && this.render(), this._opts.onNavigate && this._opts.onNavigate(e)
|
||||
}
|
||||
}, {
|
||||
key: "hasDate",
|
||||
value: function(e) {
|
||||
return e = f(p(e) ? e : this._opts.deserialize(e)), !!this._selected && this._selected.indexOf(e.getTime()) > -1
|
||||
}
|
||||
}, {
|
||||
key: "addDate",
|
||||
value: function(e) {
|
||||
this.toggleDate(e, !0)
|
||||
}
|
||||
}, {
|
||||
key: "removeDate",
|
||||
value: function(e) {
|
||||
this.toggleDate(e, !1)
|
||||
}
|
||||
}, {
|
||||
key: "toggleDate",
|
||||
value: function(e, t) {
|
||||
var n = this,
|
||||
i = this._opts,
|
||||
a = i.ranged,
|
||||
r = i.multiple,
|
||||
s = i.deserialize,
|
||||
o = [].concat(e);
|
||||
o = (o = o.map(function(e) {
|
||||
return p(e) ? e : s(e)
|
||||
})).filter(function(e) {
|
||||
return p(e) && n.dateAllowed(e)
|
||||
}), a ? o = (o = o.concat(this.getDate()).sort(c)).length ? g(o[0], o.pop()) : [] : r || (o = o.slice(0, 1)), o.map(function(e) {
|
||||
return f(e).getTime()
|
||||
}).forEach(function(e) {
|
||||
var i = n._selected.indexOf(e),
|
||||
s = i > -1;
|
||||
s || !1 === t ? s && !0 !== t && n._selected.splice(i, 1) : a || r ? n._selected.push(e) : n._selected = [e]
|
||||
}), this._update()
|
||||
}
|
||||
}, {
|
||||
key: "_update",
|
||||
value: function() {
|
||||
var e = this._opts.onChange;
|
||||
"input" === this._el.nodeName.toLowerCase() ? this._el.value = this.getValue() : this._el.dataset.value = this.getValue(), e && e(this.getDate())
|
||||
}
|
||||
}, {
|
||||
key: "getDate",
|
||||
value: function() {
|
||||
var e = this._opts,
|
||||
t = e.ranged,
|
||||
n = e.multiple,
|
||||
i = e.time,
|
||||
a = this._time ? this._time.start : [0, 0];
|
||||
if (this._selected = (this._selected || []).sort(), n || t) {
|
||||
var r = this._selected.map(function(e) {
|
||||
return new Date(e)
|
||||
});
|
||||
if (i && r.length && (r[0].setHours(a[0], a[1]), r.length > 1)) {
|
||||
var s = this._time ? this._time.end : [0, 0];
|
||||
r[r.length - 1].setHours(s[0], s[1])
|
||||
}
|
||||
return r
|
||||
}
|
||||
if (this._selected.length) {
|
||||
var o = new Date(this._selected[0]);
|
||||
return o.setHours(a[0], a[1]), o
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: "setDate",
|
||||
value: function(e) {
|
||||
this._selected = [], this.addDate(e)
|
||||
}
|
||||
}, {
|
||||
key: "setTime",
|
||||
value: function(e, t, n) {
|
||||
var i = this._opts,
|
||||
a = i.time,
|
||||
r = i.defaultTime;
|
||||
a && (!0 !== e && this._time || (this._time = v({}, r)), e && !0 !== e && ("number" == typeof e && (n = t, t = e, e = "start"), e = "end" === e ? e : "start", t = !!t && parseInt(t, 10), n = !!n && parseInt(n, 10), t && !isNaN(t) && (this._time[e][0] = t), n && !isNaN(n) && (this._time[e][1] = n)), this._update())
|
||||
}
|
||||
}, {
|
||||
key: "getValue",
|
||||
value: function() {
|
||||
var e = this._opts,
|
||||
t = e.ranged,
|
||||
n = e.separator,
|
||||
i = e.serialize,
|
||||
a = e.toValue,
|
||||
r = [].concat(this.getDate() || []);
|
||||
t && r.length > 1 && (r = [r[0], r.pop()]);
|
||||
var s = r.map(i).join(n);
|
||||
return a && (s = a(s, r)), s
|
||||
}
|
||||
}, {
|
||||
key: "setValue",
|
||||
value: function(e) {
|
||||
var t = this._opts,
|
||||
n = t.ranged,
|
||||
i = t.time,
|
||||
a = t.separator,
|
||||
r = t.serialize,
|
||||
s = t.fromValue;
|
||||
this._selected = [];
|
||||
var o = s ? s(e) : e.split(a).filter(Boolean).map(r);
|
||||
if (this.addDate(o), i && o.length) {
|
||||
var l = o.sort(c)[0];
|
||||
if (this.setTime("start", l.getHours(), l.getMinutes()), "ranged" === i || n) {
|
||||
var h = o[o.length - 1];
|
||||
this.setTime("end", h.getHours(), h.getMinutes())
|
||||
}
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: "dateAllowed",
|
||||
value: function(e, t) {
|
||||
var n = this._opts,
|
||||
i = n.min,
|
||||
a = n.max,
|
||||
r = n.within,
|
||||
s = n.without,
|
||||
o = n.deserialize,
|
||||
l = void 0,
|
||||
h = l = !0;
|
||||
return e = f(p(e) ? e : o(e)), "month" == t ? (h = !i || e.getMonth() >= i.getMonth(), l = !a || e.getMonth() <= a.getMonth()) : "year" == t ? (h = !i || e.getFullYear() >= i.getFullYear(), l = !a || e.getFullYear() <= a.getFullYear()) : (h = !i || e >= i, l = !a || e <= a), h && l && (!s || !u(e, s, t)) && (!r || u(e, r, t))
|
||||
}
|
||||
}, {
|
||||
key: "render",
|
||||
value: function() {
|
||||
var e = this,
|
||||
t = this._opts,
|
||||
n = t.ranged,
|
||||
a = t.time,
|
||||
r = t.onRender;
|
||||
if (!this._noRender && this._renderers) {
|
||||
var s = {},
|
||||
o = function(t) {
|
||||
return s[t] || (s[t] = e.getData(t))
|
||||
};
|
||||
this.wrapper.innerHTML = this._renderers.container({
|
||||
renderHeader: function() {
|
||||
var t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : 0;
|
||||
return e._renderHeader(o(t))
|
||||
},
|
||||
renderCalendar: function() {
|
||||
var t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : 0,
|
||||
n = o(t);
|
||||
return e._renderers.calendar(i({}, n, {
|
||||
renderHeader: function() {
|
||||
return e._renderHeader(n)
|
||||
},
|
||||
renderDay: function(t) {
|
||||
return e._renderers.day(t)
|
||||
}
|
||||
}))
|
||||
},
|
||||
renderTimepicker: function() {
|
||||
var t = "";
|
||||
return a && (t = e._renderTimepicker("start"), ("ranged" === a || n) && (t += e._renderTimepicker("end"))), t
|
||||
}
|
||||
}), r && r(this.wrapper.firstChild)
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: "getData",
|
||||
value: function() {
|
||||
var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : 0,
|
||||
t = this._opts,
|
||||
n = t.i18n,
|
||||
i = t.weekStart,
|
||||
a = t.serialize,
|
||||
r = t.min,
|
||||
s = t.max,
|
||||
o = t.classNames,
|
||||
l = o.selected,
|
||||
h = o.disabled,
|
||||
u = o.otherMonth,
|
||||
c = o.weekend,
|
||||
p = o.today,
|
||||
g = new Date(this._month.getTime());
|
||||
g.setMonth(g.getMonth() + e);
|
||||
var m = g.getMonth(),
|
||||
v = g.getFullYear(),
|
||||
_ = new Date(g.getTime());
|
||||
_.setMonth(_.getMonth() + 1), _.setDate(1);
|
||||
var y = new Date(g.getTime());
|
||||
y.setMonth(y.getMonth() - 1), y.setDate(d(y));
|
||||
for (var w = [], b = g.getDay() - i; b < 0;) b += 7;
|
||||
for (var k = d(v, m) + b; k % 7;) k += 1;
|
||||
for (var D = f(new Date), x = 0; x < k; x++) {
|
||||
var M = new Date(v, m, x - b + 1),
|
||||
T = M.getMonth(),
|
||||
N = M.getDay(),
|
||||
S = this.hasDate(M),
|
||||
C = !this.dateAllowed(M),
|
||||
E = T < m,
|
||||
z = T > m,
|
||||
O = !E && !z,
|
||||
j = 0 === N || 6 === N,
|
||||
A = M.getTime() === D.getTime(),
|
||||
L = [];
|
||||
S && L.push(l), C && L.push(h), O || L.push(u), j && L.push(c), A && L.push(p), w.push({
|
||||
_date: M,
|
||||
date: a(M),
|
||||
daynum: M.getDate(),
|
||||
timestamp: M.getTime(),
|
||||
weekday: n.weekdays[N],
|
||||
isSelected: S,
|
||||
isDisabled: C,
|
||||
isPrevMonth: E,
|
||||
isNextMonth: z,
|
||||
isThisMonth: O,
|
||||
isWeekend: j,
|
||||
isToday: A,
|
||||
classNames: L
|
||||
})
|
||||
}
|
||||
return {
|
||||
_date: g,
|
||||
index: e,
|
||||
year: v,
|
||||
month: n.months[m],
|
||||
days: w,
|
||||
weekdays: n.weekdays,
|
||||
hasNext: !s || _ <= s,
|
||||
hasPrev: !r || y >= r
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: "_renderHeader",
|
||||
value: function(e) {
|
||||
var t = this,
|
||||
n = this._opts,
|
||||
a = n.yearRange,
|
||||
r = n.i18n,
|
||||
s = e._date,
|
||||
o = e.index,
|
||||
l = e.year,
|
||||
h = s.getMonth();
|
||||
return this._renderers.header(i({}, e, {
|
||||
renderMonthSelect: function() {
|
||||
for (var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : o, n = new Date(s.getTime()), i = [], a = 0; a < 12; a++) n.setMonth(a), i.push({
|
||||
text: r.months[a],
|
||||
disabled: !t.dateAllowed(n, "month"),
|
||||
selected: a === h,
|
||||
value: a
|
||||
});
|
||||
return t._renderers.select({
|
||||
index: e,
|
||||
type: "month",
|
||||
text: r.months[h],
|
||||
value: h,
|
||||
options: i
|
||||
})
|
||||
},
|
||||
renderYearSelect: function() {
|
||||
for (var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : o, n = new Date(s.getTime()), i = l - a, r = l + a, h = []; i <= r; i++) n.setFullYear(i), h.push({
|
||||
disabled: !t.dateAllowed(n, "year"),
|
||||
selected: i === l,
|
||||
value: i,
|
||||
text: i
|
||||
});
|
||||
return t._renderers.select({
|
||||
index: e,
|
||||
type: "year",
|
||||
text: l,
|
||||
value: l,
|
||||
options: h
|
||||
})
|
||||
}
|
||||
}))
|
||||
}
|
||||
}, {
|
||||
key: "_renderTimepicker",
|
||||
value: function(e) {
|
||||
var t = this,
|
||||
n = this._opts,
|
||||
i = n.ranged,
|
||||
a = n.time,
|
||||
r = n.i18n;
|
||||
if (a) {
|
||||
this._time || this.setTime(!0);
|
||||
var s = this._time[e],
|
||||
o = r.time[0];
|
||||
return ("ranged" === a || i) && (o = r.time["start" === e ? 1 : 2]), this._renderers.timepicker({
|
||||
label: o,
|
||||
renderHourSelect: function() {
|
||||
for (var n = arguments.length > 0 && void 0 !== arguments[0] && arguments[0], i = [], a = s[0], r = n ? 24 : 12, o = 0; o < r; o++) i.push({
|
||||
text: n || o ? o : "12",
|
||||
selected: a === o,
|
||||
disabled: !1,
|
||||
value: o
|
||||
});
|
||||
!n && a >= 12 ? i.forEach(function(e) {
|
||||
return e.selected = (e.value += 12) === a
|
||||
}) : n || i.push(i.shift());
|
||||
var l = i.filter(function(e) {
|
||||
return e.selected
|
||||
})[0].text;
|
||||
return t._renderers.select({
|
||||
index: 0,
|
||||
type: "hour",
|
||||
value: e,
|
||||
options: i,
|
||||
text: l
|
||||
})
|
||||
},
|
||||
renderMinuteSelect: function() {
|
||||
for (var n = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : 15, i = [], a = 0; a < 60; a += n) i.push({
|
||||
text: a < 10 ? "0" + a : a,
|
||||
selected: s[1] === a,
|
||||
disabled: !1,
|
||||
value: a
|
||||
});
|
||||
var r = i.filter(function(e) {
|
||||
return e.selected
|
||||
})[0].text;
|
||||
return t._renderers.select({
|
||||
index: null,
|
||||
type: "minute",
|
||||
value: e,
|
||||
options: i,
|
||||
text: r
|
||||
})
|
||||
},
|
||||
renderPeriodSelect: function() {
|
||||
return t._renderers.select({
|
||||
index: null,
|
||||
type: "period",
|
||||
text: s[0] >= 12 ? "PM" : "AM",
|
||||
value: e,
|
||||
options: [{
|
||||
text: "AM",
|
||||
value: "am",
|
||||
selected: s[0] < 12
|
||||
}, {
|
||||
text: "PM",
|
||||
value: "pm",
|
||||
selected: s[0] >= 12
|
||||
}]
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}]), e
|
||||
}();
|
||||
return b.defaults = w, b
|
||||
});
|
||||
BIN
public/favicon.ico
Executable file
|
After Width: | Height: | Size: 4.2 KiB |
316
public/getData.js
Executable file
@ -0,0 +1,316 @@
|
||||
// Private variable scoped to this file only
|
||||
let todaysBookings = [];
|
||||
let selectedDateBookings = [];
|
||||
|
||||
const input1 = document.getElementById("name");
|
||||
const input2 = document.getElementById("email");
|
||||
const input3 = document.getElementById("phone");
|
||||
const dpicker = document.getElementById("ranged");
|
||||
const dpicker2 = document.getElementById("currentDate");
|
||||
|
||||
export async function getData() {
|
||||
try {
|
||||
const res = await fetch('/api/book', { method: 'GET' });
|
||||
if (!res.ok) throw new Error(`HTTP error! Status: ${res.status}`);
|
||||
return await res.json();
|
||||
} catch (err) {
|
||||
console.error("Error fetching data:", err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
//formats timestamp to dd/mm/yyyy
|
||||
function formatToDMY(isoString) {
|
||||
const date = new Date(isoString);
|
||||
const day = String(date.getUTCDate()).padStart(2, '0');
|
||||
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
||||
const year = date.getUTCFullYear();
|
||||
return `${day}/${month}/${year}`;
|
||||
}
|
||||
|
||||
//formats timestamp to dd/MM/yyyy
|
||||
function formatDateToDayMonthYear(dateStr) {
|
||||
const parts = dateStr.split(/[-\/]/);
|
||||
if (parts.length < 3) return "Invalid Date";
|
||||
const year = parseInt(parts[0], 10);
|
||||
const month = parseInt(parts[1], 10) - 1;
|
||||
const day = parseInt(parts[2], 10);
|
||||
|
||||
if (isNaN(year) || isNaN(month) || isNaN(day)) return "Invalid Date";
|
||||
|
||||
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
||||
return `${day} ${months[month]} ${year}`;
|
||||
}
|
||||
|
||||
const todayStr = new Date().toISOString().split("T")[0]; // "YYYY-MM-DD"
|
||||
|
||||
function addDays(dateStr, days) {
|
||||
const date = new Date(dateStr);
|
||||
date.setDate(date.getDate() + days);
|
||||
return date.toISOString().split("T")[0];
|
||||
}
|
||||
|
||||
function formatToYMD(timestamp) {
|
||||
return new Date(timestamp).toISOString().split("T")[0];
|
||||
}
|
||||
|
||||
// Loads bookings for the given day offset and stores them in todaysBookings
|
||||
export async function loadTodaysBookings(dayOffset = 0) {
|
||||
const bookings = await getData();
|
||||
const targetDate = addDays(todayStr, dayOffset);
|
||||
|
||||
// Filter bookings for the selected day by time
|
||||
const unsortedBookings = bookings.filter(
|
||||
b => formatToYMD(b.booking_date) === targetDate
|
||||
);
|
||||
// Sort by booking_time from earliest to latest
|
||||
todaysBookings = unsortedBookings.sort((a, b) => {
|
||||
const [hA, mA, sA] = a.booking_time.split(':').map(Number);
|
||||
const [hB, mB, sB] = b.booking_time.split(':').map(Number);
|
||||
|
||||
const totalA = hA * 3600 + mA * 60 + sA;
|
||||
const totalB = hB * 3600 + mB * 60 + sB;
|
||||
|
||||
return totalA - totalB;
|
||||
});
|
||||
|
||||
return todaysBookings;
|
||||
}
|
||||
//loads date selected bookings
|
||||
|
||||
export async function loadSelectedDateBookings(date) {
|
||||
const bookings = await getData();
|
||||
|
||||
// Filter bookings for the selected day by time
|
||||
const unsortedBookings = bookings.filter(
|
||||
b => formatToDMY(b.booking_date) === date
|
||||
);
|
||||
|
||||
// Sort by booking_time from earliest to latest
|
||||
selectedDateBookings = unsortedBookings.sort((a, b) => {
|
||||
const [hA, mA, sA] = a.booking_time.split(':').map(Number);
|
||||
const [hB, mB, sB] = b.booking_time.split(':').map(Number);
|
||||
|
||||
const totalA = hA * 3600 + mA * 60 + sA;
|
||||
const totalB = hB * 3600 + mB * 60 + sB;
|
||||
|
||||
return totalA - totalB;
|
||||
});
|
||||
return selectedDateBookings;
|
||||
}
|
||||
|
||||
|
||||
// Checks if a given hour is booked in todaysBookings
|
||||
export function isHourBooked(time) {
|
||||
return selectedDateBookings.some(b => b.booking_time === time);
|
||||
}
|
||||
|
||||
// Render the bookings for the dayOffset and update the DOM
|
||||
export async function renderBookings(dayOffset = 0) {
|
||||
await loadTodaysBookings(dayOffset);
|
||||
const listContainer = document.getElementById("bookingsList");
|
||||
const today = addDays(todayStr, dayOffset);
|
||||
dpicker2.value = today;
|
||||
|
||||
dpicker2.addEventListener("change", () => {
|
||||
const date1 = new Date(dpicker2.value);
|
||||
const date2 = new Date(todayStr);
|
||||
if (!isNaN(date1) && !isNaN(date2)) {
|
||||
currentDayOffset = Math.round((date1 - date2) / (1000 * 60 * 60 * 24));
|
||||
renderBookings(currentDayOffset);
|
||||
}
|
||||
});
|
||||
|
||||
// Clear the container first
|
||||
listContainer.innerHTML = "";
|
||||
|
||||
if (todaysBookings.length === 0) {
|
||||
// Display message if no bookings
|
||||
const noBookingsDiv = document.createElement("div");
|
||||
noBookingsDiv.className = "no-bookings";
|
||||
noBookingsDiv.textContent = "No bookings found for this date.";
|
||||
listContainer.appendChild(noBookingsDiv);
|
||||
return; // Exit early since there are no bookings
|
||||
}
|
||||
|
||||
// Render bookings if there are any
|
||||
todaysBookings.forEach(booking => {
|
||||
const bookingDiv = document.createElement("div");
|
||||
bookingDiv.className = "booking-item";
|
||||
|
||||
|
||||
//update booking
|
||||
window.updateData = function(id) {
|
||||
fetch('/api/book', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ accepted: true, id })
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error('Failed to update booking');
|
||||
return response.json();
|
||||
})
|
||||
.then(() => {
|
||||
loadTodaysBookings(currentDayOffset);
|
||||
renderBookings(currentDayOffset);
|
||||
})
|
||||
.catch(err => console.error('Error updating booking:', err));
|
||||
};
|
||||
|
||||
//delete booking
|
||||
window.deleteBooking = async function(id) {
|
||||
try {
|
||||
const res = await fetch(`/api/book/${id}`, { method: 'DELETE' });
|
||||
if (!res.ok) throw new Error('Failed to delete booking');
|
||||
|
||||
await loadTodaysBookings(currentDayOffset);
|
||||
renderBookings(currentDayOffset);
|
||||
} catch (err) {
|
||||
console.error('Error deleting booking:', err);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const formattedDate = booking.booking_date ? formatDateToDayMonthYear(booking.booking_date) : "No date";
|
||||
const bookingHour = booking.booking_time ? booking.booking_time.split(':')[0] + ":" + booking.booking_time.split(':')[1] : "No hour";
|
||||
|
||||
let status = "Pending approval";
|
||||
let buttonsStyle = "padding-left: 2vh; width: 20%;";
|
||||
let buttonsStyle2 = "padding-left: 2vh; width: 20%;";
|
||||
if (booking.accepted) {
|
||||
status = "Accepted";
|
||||
buttonsStyle += " display: none;";
|
||||
} else {
|
||||
buttonsStyle2 += " display: none;";
|
||||
}
|
||||
|
||||
let buttonsStyle3 = "padding-left: 2vh; width: 20%;";
|
||||
|
||||
window.getInfo = function(button, showClosed) {
|
||||
const bookingContainer = button.closest(".bookingContainer");
|
||||
const openedDiv = bookingContainer.querySelector(".opened");
|
||||
const closedDiv = bookingContainer.querySelector(".closed");
|
||||
const btns2 = bookingContainer.querySelector(".btns2");
|
||||
const btns3 = bookingContainer.querySelector(".btns3");
|
||||
const openedInfo = bookingContainer.querySelector(".openedInfo");
|
||||
const closedInfo = bookingContainer.querySelector(".closedInfo");
|
||||
|
||||
if (showClosed) {
|
||||
openedDiv.style.display = "none";
|
||||
btns2.style.display = "none";
|
||||
openedInfo.style.display = "none";
|
||||
closedDiv.style.display = "block";
|
||||
btns3.style.display = "block";
|
||||
closedInfo.style.display = "block";
|
||||
|
||||
} else {
|
||||
openedDiv.style.display = "block";
|
||||
btns2.style.display = "block";
|
||||
closedDiv.style.display = "none";
|
||||
btns3.style.display = "none";
|
||||
openedInfo.style.display = "block";
|
||||
closedInfo.style.display = "none";
|
||||
}
|
||||
};
|
||||
|
||||
bookingDiv.innerHTML = `
|
||||
<div class="bookingContainer">
|
||||
<div style="padding-left: 2vh; width: 20%;">
|
||||
<h1 class="opened" style="font-size: 5vmin; display: block;">${bookingHour}</h1>
|
||||
<button class="closed" style="display: none; margin-left: 2vh;" onclick="deleteBooking(${booking.id})">
|
||||
<img style="width: 4vh; height: 4vh;" src="img/delete.png">
|
||||
</button>
|
||||
</div>
|
||||
<div class="openedInfo" style="display: block;">
|
||||
<div class="userinfo-container">
|
||||
<h3 class="userinfo-target">
|
||||
Booking for: <br>${booking.name}
|
||||
<div class="userinfo-box">Email: ${booking.email || "No email"}<br>Phone: ${booking.phone || "No phone"}</div>
|
||||
</h3>
|
||||
|
||||
<div class="bookingContent">
|
||||
<h5>Status: ${status}</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="closedInfo" style="display: none;">
|
||||
<div>
|
||||
<h5>
|
||||
Name: ${booking.name || "No name"}<br>
|
||||
</h5>
|
||||
<h5>
|
||||
Email: ${booking.email || "No email"}<br>
|
||||
</h5>
|
||||
<h5>
|
||||
Phone: ${booking.phone || "No phone"}<br>
|
||||
</h5>
|
||||
<h5>
|
||||
Booking Date: ${formattedDate}, ${booking.booking_time}<br>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btns" style="${buttonsStyle}">
|
||||
<div style="margin-right: 2.5vh;">
|
||||
<button class="bookingBtns" onclick="updateData(${booking.id})">
|
||||
<img style="width: 5vh; height: 5vh;" src="img/done.png">
|
||||
</button>
|
||||
</div>
|
||||
<div style="margin-right: 2.5vh;">
|
||||
<button class="bookingBtns" onclick="deleteBooking(${booking.id})">
|
||||
<img style="width: 5vh; height: 5vh;" src="img/delete.png">
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btns2" style="${buttonsStyle2}">
|
||||
<div style="margin-right: 2vh;">
|
||||
<button class="bookingBtns">
|
||||
<img style="width: 4vh; height: 4vh;" src="img/info.png" onclick="getInfo(this, true)">
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btns3" style="${buttonsStyle3} display: none;">
|
||||
<div class="backBtn" style="margin-right: 2vh;">
|
||||
<button class="bookingBtns">
|
||||
<img style="width: 4vh; height: 4vh;" src="img/back.png" onclick="getInfo(this, false)">
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
listContainer.appendChild(bookingDiv);
|
||||
});
|
||||
}
|
||||
|
||||
window.renderBookings = renderBookings;
|
||||
|
||||
// Controls to move days in the admin panel
|
||||
let currentDayOffset = 0;
|
||||
|
||||
document.getElementById("prevBtn").addEventListener("click", () => {
|
||||
currentDayOffset--;
|
||||
renderBookings(currentDayOffset);
|
||||
});
|
||||
|
||||
document.getElementById("nextBtn").addEventListener("click", () => {
|
||||
currentDayOffset++;
|
||||
renderBookings(currentDayOffset);
|
||||
});
|
||||
|
||||
// Initial render
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
renderBookings(currentDayOffset);
|
||||
});
|
||||
|
||||
//loads selected date bookings
|
||||
[input1, input2, input3, dpicker].forEach(input => {
|
||||
input.addEventListener("input", () => loadSelectedDateBookings(dpicker.value));
|
||||
input.addEventListener("change", () => loadSelectedDateBookings(dpicker.value));
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
loadSelectedDateBookings(dpicker.value);
|
||||
}, 200);
|
||||
BIN
public/img/arrow-left.png
Executable file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
public/img/arrow-right.png
Executable file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
public/img/back.png
Executable file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
public/img/delete.png
Executable file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
public/img/done.png
Executable file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
public/img/info.png
Executable file
|
After Width: | Height: | Size: 15 KiB |
117
public/index.html
Normal file
@ -0,0 +1,117 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Booking System</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link rel="stylesheet" href="css/datepicker.material.css">
|
||||
<script src="datepicker.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="forms-container">
|
||||
<div class="form">
|
||||
<h1>Book an appointment</h1>
|
||||
<div id="success" style="margin-top: 5vh;">
|
||||
<h2>Successfully booked!</h2>
|
||||
<p>We will get in touch with you soon.</p>
|
||||
<img style="height: 40vh; width: 40vh;" src="img/done.png">
|
||||
<div>
|
||||
<button id="back" class="submitBtn" onclick="reload()">Get Back</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inputs" id="form">
|
||||
<input type="text" placeholder="Name" id="name">
|
||||
<input type="email" placeholder="Email" id="email">
|
||||
<input type="tel" placeholder="Phone" id="phone">
|
||||
<div class="pickers-row">
|
||||
<input class="calendar" type="hidden" id="ranged">
|
||||
<div style="display: flex; flex-direction: column; align-items: center; margin-right: 3vh;">
|
||||
<p style="font-size: 20px; margin: 0;">Select Time</p>
|
||||
<p style="font-size: 14px; margin-top: 0; margin-bottom: 20;">(09:00 - 17:00)</p>
|
||||
<div class="picker-container">
|
||||
<div class="picker" id="hour-picker"></div>
|
||||
<div class="highlight"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="formBottom" >
|
||||
<button id="btn" class="submitBtn" onclick="postData()">Submit</button>
|
||||
<div id="selected-time"></div>
|
||||
<p style="color: rgba(128, 128, 128, 0.75)">User View*</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form">
|
||||
<h1>Bookings</h1>
|
||||
<div style="width: 80%;">
|
||||
<div class="adminHeader">
|
||||
<button id="prevBtn" class="bookingBtns">
|
||||
<img style="width: 5vh; height: 5vh;" src="img/arrow-left.png">
|
||||
</button>
|
||||
<input type="date" id="currentDate">
|
||||
<button id="nextBtn" class="bookingBtns">
|
||||
<img style="width: 5vh; height: 5vh;" src="img/arrow-right.png">
|
||||
</button>
|
||||
</div>
|
||||
<div id="bookingsList" class="listBookings">
|
||||
<script type="module" src="getData.js" defer></script>
|
||||
</div>
|
||||
<p style="color: rgba(128, 128, 128, 0.75)">Admin View*</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<script type="module" src="timepicker.js"></script>
|
||||
<script type="module" src="postData.js"></script>
|
||||
<script>
|
||||
const input1 = document.getElementById("name");
|
||||
const input2 = document.getElementById("email");
|
||||
const input3 = document.getElementById("phone");
|
||||
const dpicker = document.getElementById("ranged");
|
||||
const btn = document.getElementById("btn");
|
||||
|
||||
btn.disabled = true;
|
||||
|
||||
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
const phonePattern = /^\+?\d{7,15}$/;
|
||||
|
||||
// Validation
|
||||
function checkInputs() {
|
||||
const isNameFilled = input1.value.trim() !== "";
|
||||
const isEmailValid = emailPattern.test(input2.value.trim());
|
||||
const isPhoneValid = phonePattern.test(input3.value.trim());
|
||||
const isDatePicked = dpicker.value.trim() !== "";
|
||||
|
||||
return (isNameFilled && isEmailValid && isPhoneValid && isDatePicked);
|
||||
}
|
||||
|
||||
// Init Datepicker
|
||||
const datePickerInstance = new Datepicker(dpicker, {
|
||||
autohide: true
|
||||
});
|
||||
|
||||
// Listen to normal inputs
|
||||
[input1, input2, input3, dpicker].forEach(input => {
|
||||
input.addEventListener("input", checkInputs);
|
||||
input.addEventListener("change", checkInputs);
|
||||
});
|
||||
|
||||
// Safety: poll the datepicker value every 200ms in case it changes without firing events
|
||||
setInterval(() => {
|
||||
checkInputs();
|
||||
}, 200);
|
||||
|
||||
var success = document.getElementById("success");
|
||||
success.style.display = "none";
|
||||
var ranged = new Datepicker('#ranged', {
|
||||
inline: true,
|
||||
ranged: false,
|
||||
time: false
|
||||
});
|
||||
function reload() {
|
||||
location.reload();
|
||||
}
|
||||
</script>
|
||||
46
public/postData.js
Executable file
@ -0,0 +1,46 @@
|
||||
import {
|
||||
getData
|
||||
} from './getData.js';
|
||||
function formatDateToMMDDYYYY(dateStr) {
|
||||
const [day, month, year] = dateStr.split('/');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
export async function postData() {
|
||||
const dateValue = document.getElementById('ranged').value;
|
||||
const date = formatDateToMMDDYYYY(dateValue);
|
||||
const time = window.selectedHour ?? '09:00';
|
||||
const name = document.getElementById('name').value;
|
||||
const email = document.getElementById('email').value;
|
||||
const phone = document.getElementById('phone').value;
|
||||
|
||||
const res = await fetch('/api/book', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, email, phone, date, time })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
const responseEl = document.getElementById('response');
|
||||
if (responseEl) {
|
||||
responseEl.innerText = data.reply;
|
||||
} else {
|
||||
console.warn('No element with id="response" found');
|
||||
}
|
||||
|
||||
console.log(name, email, phone, date, time);
|
||||
|
||||
const form = document.getElementById("form");
|
||||
if(form) form.style.display = "none";
|
||||
|
||||
const btn = document.getElementById("btn");
|
||||
if(btn) btn.style.display = "none";
|
||||
|
||||
const success = document.getElementById("success");
|
||||
if(success) success.style.display = "block";
|
||||
getData();
|
||||
renderBookings();
|
||||
}
|
||||
window.postData = postData;
|
||||
|
||||
123
public/timepicker.js
Executable file
@ -0,0 +1,123 @@
|
||||
const picker = document.getElementById('hour-picker');
|
||||
window.selectedHour = null;
|
||||
const itemHeight = 50;
|
||||
let isScrolling = false;
|
||||
import {
|
||||
loadTodaysBookings,
|
||||
isHourBooked,
|
||||
loadSelectedDateBookings
|
||||
} from './getData.js';
|
||||
await loadTodaysBookings(0);
|
||||
let isHourSelectedBooked = false;
|
||||
|
||||
|
||||
let currentIndex = 0;
|
||||
|
||||
const btn = document.getElementById("btn");
|
||||
const input1 = document.getElementById("name");
|
||||
const input2 = document.getElementById("email");
|
||||
const input3 = document.getElementById("phone");
|
||||
const dpicker = document.getElementById("ranged");
|
||||
|
||||
|
||||
// Create items
|
||||
for (let h = 8; h <= 18; h++) {
|
||||
const item = document.createElement('div');
|
||||
item.textContent = `${String(h).padStart(2, '0')}:00`;
|
||||
picker.appendChild(item);
|
||||
}
|
||||
|
||||
const defaultIndex = 0;
|
||||
picker.scrollTop = defaultIndex * itemHeight;
|
||||
updateActive(defaultIndex);
|
||||
|
||||
picker.addEventListener('wheel', (e) => {
|
||||
e.preventDefault();
|
||||
if (isScrolling) return;
|
||||
|
||||
isScrolling = true;
|
||||
|
||||
let index = Math.round(picker.scrollTop / itemHeight);
|
||||
if (e.deltaY > 0) {
|
||||
index = Math.min(index + 1, picker.children.length - 1);
|
||||
} else {
|
||||
index = Math.max(index - 1, 0);
|
||||
}
|
||||
|
||||
picker.scrollTo({
|
||||
top: index * itemHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
isScrolling = false;
|
||||
}, 200);
|
||||
}, {
|
||||
passive: false
|
||||
});
|
||||
|
||||
let scrollTimeout;
|
||||
picker.addEventListener('scroll', () => {
|
||||
clearTimeout(scrollTimeout);
|
||||
scrollTimeout = setTimeout(() => {
|
||||
let index = Math.round(picker.scrollTop / itemHeight);
|
||||
index = Math.max(0, Math.min(index, picker.children.length - 1));
|
||||
picker.scrollTo({
|
||||
top: index * itemHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
currentIndex = index;
|
||||
enableBtn();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
function updateActive(index) {
|
||||
for (let i = 0; i < picker.children.length; i++) {
|
||||
picker.children[i].classList.remove('active');
|
||||
}
|
||||
picker.children[index + 1].classList.add('active');
|
||||
|
||||
window.selectedHour = picker.children[index + 1]?.textContent.trim();
|
||||
const selectedHourFormatted = selectedHour + ":00";
|
||||
isHourSelectedBooked = isHourBooked(selectedHourFormatted);
|
||||
|
||||
return isHourSelectedBooked;
|
||||
}
|
||||
|
||||
export function enableBtn() {
|
||||
let inputs = checkInputs();
|
||||
let isBooked = updateActive(currentIndex);
|
||||
|
||||
// Reset only non-active items to default color
|
||||
picker.querySelectorAll('div').forEach(item => {
|
||||
if (!item.classList.contains('active')) {
|
||||
item.style.color = '#aaa';
|
||||
}
|
||||
});
|
||||
|
||||
let activeDiv = picker.querySelector('.active');
|
||||
|
||||
if (isBooked) {
|
||||
activeDiv.style.color = '#a84747ff'; // booked color
|
||||
} else {
|
||||
activeDiv.style.color = '#222'; // available color
|
||||
}
|
||||
|
||||
if (isBooked || !inputs) {
|
||||
btn.disabled = true;
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[input1, input2, input3, dpicker].forEach(input => {
|
||||
input.addEventListener("input", enableBtn);
|
||||
input.addEventListener("change", enableBtn);
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
enableBtn();
|
||||
}, 200);
|
||||
|
||||
enableBtn()
|
||||
9
server/.env
Executable file
@ -0,0 +1,9 @@
|
||||
DB_USER=aleks
|
||||
DB_HOST=db
|
||||
DB_DATABASE=bookingsystem
|
||||
DB_PASSWORD=moni3niki
|
||||
DB_PORT=5432
|
||||
API_KEY=moni3niki
|
||||
|
||||
ETHEREAL_USER=aleks.s.petrowv@gmail.com
|
||||
ETHEREAL_PASS=ssggwoxukotvlbmo
|
||||
11
server/Dockerfile
Executable file
@ -0,0 +1,11 @@
|
||||
FROM node:18
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "start"]
|
||||
990
server/node_modules/.package-lock.json
generated
vendored
Normal file
@ -0,0 +1,990 @@
|
||||
{
|
||||
"name": "test",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"node_modules/accepts": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
||||
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-types": "^3.0.0",
|
||||
"negotiator": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
|
||||
"integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "^3.1.2",
|
||||
"content-type": "^1.0.5",
|
||||
"debug": "^4.4.0",
|
||||
"http-errors": "^2.0.0",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"on-finished": "^2.4.1",
|
||||
"qs": "^6.14.0",
|
||||
"raw-body": "^3.0.0",
|
||||
"type-is": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bound": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"get-intrinsic": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
|
||||
"integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "5.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/content-type": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
||||
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "17.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz",
|
||||
"integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/etag": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
|
||||
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.0",
|
||||
"content-disposition": "^1.0.0",
|
||||
"content-type": "^1.0.5",
|
||||
"cookie": "^0.7.1",
|
||||
"cookie-signature": "^1.2.1",
|
||||
"debug": "^4.4.0",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"etag": "^1.8.1",
|
||||
"finalhandler": "^2.1.0",
|
||||
"fresh": "^2.0.0",
|
||||
"http-errors": "^2.0.0",
|
||||
"merge-descriptors": "^2.0.0",
|
||||
"mime-types": "^3.0.0",
|
||||
"on-finished": "^2.4.1",
|
||||
"once": "^1.4.0",
|
||||
"parseurl": "^1.3.3",
|
||||
"proxy-addr": "^2.0.7",
|
||||
"qs": "^6.14.0",
|
||||
"range-parser": "^1.2.1",
|
||||
"router": "^2.2.0",
|
||||
"send": "^1.1.0",
|
||||
"serve-static": "^2.2.0",
|
||||
"statuses": "^2.0.1",
|
||||
"type-is": "^2.0.1",
|
||||
"vary": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
|
||||
"integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.0",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"on-finished": "^2.4.1",
|
||||
"parseurl": "^1.3.3",
|
||||
"statuses": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/flatpickr": {
|
||||
"version": "4.6.13",
|
||||
"resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.13.tgz",
|
||||
"integrity": "sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
||||
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "2.0.0",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"toidentifier": "1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors/node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/is-promise": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
||||
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
|
||||
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
|
||||
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.54.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
|
||||
"integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "^1.54.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
||||
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.5.tgz",
|
||||
"integrity": "sha512-nsrh2lO3j4GkLLXoeEksAMgAOqxOv6QumNRVQTJwKH4nuiww6iC2y7GyANs9kRAxCexg3+lTWM3PZ91iLlVjfg==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ee-first": "1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
|
||||
"integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/pg": {
|
||||
"version": "8.16.3",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
|
||||
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg-connection-string": "^2.9.1",
|
||||
"pg-pool": "^3.10.1",
|
||||
"pg-protocol": "^1.10.3",
|
||||
"pg-types": "2.2.0",
|
||||
"pgpass": "1.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"pg-cloudflare": "^1.2.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"pg-native": ">=3.0.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"pg-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pg-cloudflare": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz",
|
||||
"integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/pg-connection-string": {
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz",
|
||||
"integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-int8": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
||||
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-pool": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz",
|
||||
"integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"pg": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-protocol": {
|
||||
"version": "1.10.3",
|
||||
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz",
|
||||
"integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-types": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
||||
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg-int8": "1.0.1",
|
||||
"postgres-array": "~2.0.0",
|
||||
"postgres-bytea": "~1.0.0",
|
||||
"postgres-date": "~1.0.4",
|
||||
"postgres-interval": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/pgpass": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
|
||||
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"split2": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-array": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-bytea": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
|
||||
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-date": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
|
||||
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-interval": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
|
||||
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"xtend": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"forwarded": "0.2.0",
|
||||
"ipaddr.js": "1.9.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
|
||||
"integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.6.3",
|
||||
"unpipe": "1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/router": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
||||
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.0",
|
||||
"depd": "^2.0.0",
|
||||
"is-promise": "^4.0.0",
|
||||
"parseurl": "^1.3.3",
|
||||
"path-to-regexp": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
|
||||
"integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.5",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"etag": "^1.8.1",
|
||||
"fresh": "^2.0.0",
|
||||
"http-errors": "^2.0.0",
|
||||
"mime-types": "^3.0.1",
|
||||
"ms": "^2.1.3",
|
||||
"on-finished": "^2.4.1",
|
||||
"range-parser": "^1.2.1",
|
||||
"statuses": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
|
||||
"integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"parseurl": "^1.3.3",
|
||||
"send": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-list": "^1.0.0",
|
||||
"side-channel-map": "^1.0.1",
|
||||
"side-channel-weakmap": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-list": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-map": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-weakmap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-map": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/split2": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
|
||||
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"content-type": "^1.0.5",
|
||||
"media-typer": "^1.1.0",
|
||||
"mime-types": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
250
server/node_modules/accepts/HISTORY.md
generated
vendored
Normal file
@ -0,0 +1,250 @@
|
||||
2.0.0 / 2024-08-31
|
||||
==================
|
||||
|
||||
* Drop node <18 support
|
||||
* deps: mime-types@^3.0.0
|
||||
* deps: negotiator@^1.0.0
|
||||
|
||||
1.3.8 / 2022-02-02
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.1.34
|
||||
- deps: mime-db@~1.51.0
|
||||
* deps: negotiator@0.6.3
|
||||
|
||||
1.3.7 / 2019-04-29
|
||||
==================
|
||||
|
||||
* deps: negotiator@0.6.2
|
||||
- Fix sorting charset, encoding, and language with extra parameters
|
||||
|
||||
1.3.6 / 2019-04-28
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.1.24
|
||||
- deps: mime-db@~1.40.0
|
||||
|
||||
1.3.5 / 2018-02-28
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.1.18
|
||||
- deps: mime-db@~1.33.0
|
||||
|
||||
1.3.4 / 2017-08-22
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.1.16
|
||||
- deps: mime-db@~1.29.0
|
||||
|
||||
1.3.3 / 2016-05-02
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.1.11
|
||||
- deps: mime-db@~1.23.0
|
||||
* deps: negotiator@0.6.1
|
||||
- perf: improve `Accept` parsing speed
|
||||
- perf: improve `Accept-Charset` parsing speed
|
||||
- perf: improve `Accept-Encoding` parsing speed
|
||||
- perf: improve `Accept-Language` parsing speed
|
||||
|
||||
1.3.2 / 2016-03-08
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.1.10
|
||||
- Fix extension of `application/dash+xml`
|
||||
- Update primary extension for `audio/mp4`
|
||||
- deps: mime-db@~1.22.0
|
||||
|
||||
1.3.1 / 2016-01-19
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.1.9
|
||||
- deps: mime-db@~1.21.0
|
||||
|
||||
1.3.0 / 2015-09-29
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.1.7
|
||||
- deps: mime-db@~1.19.0
|
||||
* deps: negotiator@0.6.0
|
||||
- Fix including type extensions in parameters in `Accept` parsing
|
||||
- Fix parsing `Accept` parameters with quoted equals
|
||||
- Fix parsing `Accept` parameters with quoted semicolons
|
||||
- Lazy-load modules from main entry point
|
||||
- perf: delay type concatenation until needed
|
||||
- perf: enable strict mode
|
||||
- perf: hoist regular expressions
|
||||
- perf: remove closures getting spec properties
|
||||
- perf: remove a closure from media type parsing
|
||||
- perf: remove property delete from media type parsing
|
||||
|
||||
1.2.13 / 2015-09-06
|
||||
===================
|
||||
|
||||
* deps: mime-types@~2.1.6
|
||||
- deps: mime-db@~1.18.0
|
||||
|
||||
1.2.12 / 2015-07-30
|
||||
===================
|
||||
|
||||
* deps: mime-types@~2.1.4
|
||||
- deps: mime-db@~1.16.0
|
||||
|
||||
1.2.11 / 2015-07-16
|
||||
===================
|
||||
|
||||
* deps: mime-types@~2.1.3
|
||||
- deps: mime-db@~1.15.0
|
||||
|
||||
1.2.10 / 2015-07-01
|
||||
===================
|
||||
|
||||
* deps: mime-types@~2.1.2
|
||||
- deps: mime-db@~1.14.0
|
||||
|
||||
1.2.9 / 2015-06-08
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.1.1
|
||||
- perf: fix deopt during mapping
|
||||
|
||||
1.2.8 / 2015-06-07
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.1.0
|
||||
- deps: mime-db@~1.13.0
|
||||
* perf: avoid argument reassignment & argument slice
|
||||
* perf: avoid negotiator recursive construction
|
||||
* perf: enable strict mode
|
||||
* perf: remove unnecessary bitwise operator
|
||||
|
||||
1.2.7 / 2015-05-10
|
||||
==================
|
||||
|
||||
* deps: negotiator@0.5.3
|
||||
- Fix media type parameter matching to be case-insensitive
|
||||
|
||||
1.2.6 / 2015-05-07
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.0.11
|
||||
- deps: mime-db@~1.9.1
|
||||
* deps: negotiator@0.5.2
|
||||
- Fix comparing media types with quoted values
|
||||
- Fix splitting media types with quoted commas
|
||||
|
||||
1.2.5 / 2015-03-13
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.0.10
|
||||
- deps: mime-db@~1.8.0
|
||||
|
||||
1.2.4 / 2015-02-14
|
||||
==================
|
||||
|
||||
* Support Node.js 0.6
|
||||
* deps: mime-types@~2.0.9
|
||||
- deps: mime-db@~1.7.0
|
||||
* deps: negotiator@0.5.1
|
||||
- Fix preference sorting to be stable for long acceptable lists
|
||||
|
||||
1.2.3 / 2015-01-31
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.0.8
|
||||
- deps: mime-db@~1.6.0
|
||||
|
||||
1.2.2 / 2014-12-30
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.0.7
|
||||
- deps: mime-db@~1.5.0
|
||||
|
||||
1.2.1 / 2014-12-30
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.0.5
|
||||
- deps: mime-db@~1.3.1
|
||||
|
||||
1.2.0 / 2014-12-19
|
||||
==================
|
||||
|
||||
* deps: negotiator@0.5.0
|
||||
- Fix list return order when large accepted list
|
||||
- Fix missing identity encoding when q=0 exists
|
||||
- Remove dynamic building of Negotiator class
|
||||
|
||||
1.1.4 / 2014-12-10
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.0.4
|
||||
- deps: mime-db@~1.3.0
|
||||
|
||||
1.1.3 / 2014-11-09
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.0.3
|
||||
- deps: mime-db@~1.2.0
|
||||
|
||||
1.1.2 / 2014-10-14
|
||||
==================
|
||||
|
||||
* deps: negotiator@0.4.9
|
||||
- Fix error when media type has invalid parameter
|
||||
|
||||
1.1.1 / 2014-09-28
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.0.2
|
||||
- deps: mime-db@~1.1.0
|
||||
* deps: negotiator@0.4.8
|
||||
- Fix all negotiations to be case-insensitive
|
||||
- Stable sort preferences of same quality according to client order
|
||||
|
||||
1.1.0 / 2014-09-02
|
||||
==================
|
||||
|
||||
* update `mime-types`
|
||||
|
||||
1.0.7 / 2014-07-04
|
||||
==================
|
||||
|
||||
* Fix wrong type returned from `type` when match after unknown extension
|
||||
|
||||
1.0.6 / 2014-06-24
|
||||
==================
|
||||
|
||||
* deps: negotiator@0.4.7
|
||||
|
||||
1.0.5 / 2014-06-20
|
||||
==================
|
||||
|
||||
* fix crash when unknown extension given
|
||||
|
||||
1.0.4 / 2014-06-19
|
||||
==================
|
||||
|
||||
* use `mime-types`
|
||||
|
||||
1.0.3 / 2014-06-11
|
||||
==================
|
||||
|
||||
* deps: negotiator@0.4.6
|
||||
- Order by specificity when quality is the same
|
||||
|
||||
1.0.2 / 2014-05-29
|
||||
==================
|
||||
|
||||
* Fix interpretation when header not in request
|
||||
* deps: pin negotiator@0.4.5
|
||||
|
||||
1.0.1 / 2014-01-18
|
||||
==================
|
||||
|
||||
* Identity encoding isn't always acceptable
|
||||
* deps: negotiator@~0.4.0
|
||||
|
||||
1.0.0 / 2013-12-27
|
||||
==================
|
||||
|
||||
* Genesis
|
||||
23
server/node_modules/accepts/LICENSE
generated
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2014 Jonathan Ong <me@jongleberry.com>
|
||||
Copyright (c) 2015 Douglas Christopher Wilson <doug@somethingdoug.com>
|
||||
|
||||
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.
|
||||
140
server/node_modules/accepts/README.md
generated
vendored
Normal file
@ -0,0 +1,140 @@
|
||||
# accepts
|
||||
|
||||
[![NPM Version][npm-version-image]][npm-url]
|
||||
[![NPM Downloads][npm-downloads-image]][npm-url]
|
||||
[![Node.js Version][node-version-image]][node-version-url]
|
||||
[![Build Status][github-actions-ci-image]][github-actions-ci-url]
|
||||
[![Test Coverage][coveralls-image]][coveralls-url]
|
||||
|
||||
Higher level content negotiation based on [negotiator](https://www.npmjs.com/package/negotiator).
|
||||
Extracted from [koa](https://www.npmjs.com/package/koa) for general use.
|
||||
|
||||
In addition to negotiator, it allows:
|
||||
|
||||
- Allows types as an array or arguments list, ie `(['text/html', 'application/json'])`
|
||||
as well as `('text/html', 'application/json')`.
|
||||
- Allows type shorthands such as `json`.
|
||||
- Returns `false` when no types match
|
||||
- Treats non-existent headers as `*`
|
||||
|
||||
## Installation
|
||||
|
||||
This is a [Node.js](https://nodejs.org/en/) module available through the
|
||||
[npm registry](https://www.npmjs.com/). Installation is done using the
|
||||
[`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally):
|
||||
|
||||
```sh
|
||||
$ npm install accepts
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
```js
|
||||
var accepts = require('accepts')
|
||||
```
|
||||
|
||||
### accepts(req)
|
||||
|
||||
Create a new `Accepts` object for the given `req`.
|
||||
|
||||
#### .charset(charsets)
|
||||
|
||||
Return the first accepted charset. If nothing in `charsets` is accepted,
|
||||
then `false` is returned.
|
||||
|
||||
#### .charsets()
|
||||
|
||||
Return the charsets that the request accepts, in the order of the client's
|
||||
preference (most preferred first).
|
||||
|
||||
#### .encoding(encodings)
|
||||
|
||||
Return the first accepted encoding. If nothing in `encodings` is accepted,
|
||||
then `false` is returned.
|
||||
|
||||
#### .encodings()
|
||||
|
||||
Return the encodings that the request accepts, in the order of the client's
|
||||
preference (most preferred first).
|
||||
|
||||
#### .language(languages)
|
||||
|
||||
Return the first accepted language. If nothing in `languages` is accepted,
|
||||
then `false` is returned.
|
||||
|
||||
#### .languages()
|
||||
|
||||
Return the languages that the request accepts, in the order of the client's
|
||||
preference (most preferred first).
|
||||
|
||||
#### .type(types)
|
||||
|
||||
Return the first accepted type (and it is returned as the same text as what
|
||||
appears in the `types` array). If nothing in `types` is accepted, then `false`
|
||||
is returned.
|
||||
|
||||
The `types` array can contain full MIME types or file extensions. Any value
|
||||
that is not a full MIME type is passed to `require('mime-types').lookup`.
|
||||
|
||||
#### .types()
|
||||
|
||||
Return the types that the request accepts, in the order of the client's
|
||||
preference (most preferred first).
|
||||
|
||||
## Examples
|
||||
|
||||
### Simple type negotiation
|
||||
|
||||
This simple example shows how to use `accepts` to return a different typed
|
||||
respond body based on what the client wants to accept. The server lists it's
|
||||
preferences in order and will get back the best match between the client and
|
||||
server.
|
||||
|
||||
```js
|
||||
var accepts = require('accepts')
|
||||
var http = require('http')
|
||||
|
||||
function app (req, res) {
|
||||
var accept = accepts(req)
|
||||
|
||||
// the order of this list is significant; should be server preferred order
|
||||
switch (accept.type(['json', 'html'])) {
|
||||
case 'json':
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.write('{"hello":"world!"}')
|
||||
break
|
||||
case 'html':
|
||||
res.setHeader('Content-Type', 'text/html')
|
||||
res.write('<b>hello, world!</b>')
|
||||
break
|
||||
default:
|
||||
// the fallback is text/plain, so no need to specify it above
|
||||
res.setHeader('Content-Type', 'text/plain')
|
||||
res.write('hello, world!')
|
||||
break
|
||||
}
|
||||
|
||||
res.end()
|
||||
}
|
||||
|
||||
http.createServer(app).listen(3000)
|
||||
```
|
||||
|
||||
You can test this out with the cURL program:
|
||||
```sh
|
||||
curl -I -H'Accept: text/html' http://localhost:3000/
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
[coveralls-image]: https://badgen.net/coveralls/c/github/jshttp/accepts/master
|
||||
[coveralls-url]: https://coveralls.io/r/jshttp/accepts?branch=master
|
||||
[github-actions-ci-image]: https://badgen.net/github/checks/jshttp/accepts/master?label=ci
|
||||
[github-actions-ci-url]: https://github.com/jshttp/accepts/actions/workflows/ci.yml
|
||||
[node-version-image]: https://badgen.net/npm/node/accepts
|
||||
[node-version-url]: https://nodejs.org/en/download
|
||||
[npm-downloads-image]: https://badgen.net/npm/dm/accepts
|
||||
[npm-url]: https://npmjs.org/package/accepts
|
||||
[npm-version-image]: https://badgen.net/npm/v/accepts
|
||||
238
server/node_modules/accepts/index.js
generated
vendored
Normal file
@ -0,0 +1,238 @@
|
||||
/*!
|
||||
* accepts
|
||||
* Copyright(c) 2014 Jonathan Ong
|
||||
* Copyright(c) 2015 Douglas Christopher Wilson
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
* @private
|
||||
*/
|
||||
|
||||
var Negotiator = require('negotiator')
|
||||
var mime = require('mime-types')
|
||||
|
||||
/**
|
||||
* Module exports.
|
||||
* @public
|
||||
*/
|
||||
|
||||
module.exports = Accepts
|
||||
|
||||
/**
|
||||
* Create a new Accepts object for the given req.
|
||||
*
|
||||
* @param {object} req
|
||||
* @public
|
||||
*/
|
||||
|
||||
function Accepts (req) {
|
||||
if (!(this instanceof Accepts)) {
|
||||
return new Accepts(req)
|
||||
}
|
||||
|
||||
this.headers = req.headers
|
||||
this.negotiator = new Negotiator(req)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given `type(s)` is acceptable, returning
|
||||
* the best match when true, otherwise `undefined`, in which
|
||||
* case you should respond with 406 "Not Acceptable".
|
||||
*
|
||||
* The `type` value may be a single mime type string
|
||||
* such as "application/json", the extension name
|
||||
* such as "json" or an array `["json", "html", "text/plain"]`. When a list
|
||||
* or array is given the _best_ match, if any is returned.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* // Accept: text/html
|
||||
* this.types('html');
|
||||
* // => "html"
|
||||
*
|
||||
* // Accept: text/*, application/json
|
||||
* this.types('html');
|
||||
* // => "html"
|
||||
* this.types('text/html');
|
||||
* // => "text/html"
|
||||
* this.types('json', 'text');
|
||||
* // => "json"
|
||||
* this.types('application/json');
|
||||
* // => "application/json"
|
||||
*
|
||||
* // Accept: text/*, application/json
|
||||
* this.types('image/png');
|
||||
* this.types('png');
|
||||
* // => undefined
|
||||
*
|
||||
* // Accept: text/*;q=.5, application/json
|
||||
* this.types(['html', 'json']);
|
||||
* this.types('html', 'json');
|
||||
* // => "json"
|
||||
*
|
||||
* @param {String|Array} types...
|
||||
* @return {String|Array|Boolean}
|
||||
* @public
|
||||
*/
|
||||
|
||||
Accepts.prototype.type =
|
||||
Accepts.prototype.types = function (types_) {
|
||||
var types = types_
|
||||
|
||||
// support flattened arguments
|
||||
if (types && !Array.isArray(types)) {
|
||||
types = new Array(arguments.length)
|
||||
for (var i = 0; i < types.length; i++) {
|
||||
types[i] = arguments[i]
|
||||
}
|
||||
}
|
||||
|
||||
// no types, return all requested types
|
||||
if (!types || types.length === 0) {
|
||||
return this.negotiator.mediaTypes()
|
||||
}
|
||||
|
||||
// no accept header, return first given type
|
||||
if (!this.headers.accept) {
|
||||
return types[0]
|
||||
}
|
||||
|
||||
var mimes = types.map(extToMime)
|
||||
var accepts = this.negotiator.mediaTypes(mimes.filter(validMime))
|
||||
var first = accepts[0]
|
||||
|
||||
return first
|
||||
? types[mimes.indexOf(first)]
|
||||
: false
|
||||
}
|
||||
|
||||
/**
|
||||
* Return accepted encodings or best fit based on `encodings`.
|
||||
*
|
||||
* Given `Accept-Encoding: gzip, deflate`
|
||||
* an array sorted by quality is returned:
|
||||
*
|
||||
* ['gzip', 'deflate']
|
||||
*
|
||||
* @param {String|Array} encodings...
|
||||
* @return {String|Array}
|
||||
* @public
|
||||
*/
|
||||
|
||||
Accepts.prototype.encoding =
|
||||
Accepts.prototype.encodings = function (encodings_) {
|
||||
var encodings = encodings_
|
||||
|
||||
// support flattened arguments
|
||||
if (encodings && !Array.isArray(encodings)) {
|
||||
encodings = new Array(arguments.length)
|
||||
for (var i = 0; i < encodings.length; i++) {
|
||||
encodings[i] = arguments[i]
|
||||
}
|
||||
}
|
||||
|
||||
// no encodings, return all requested encodings
|
||||
if (!encodings || encodings.length === 0) {
|
||||
return this.negotiator.encodings()
|
||||
}
|
||||
|
||||
return this.negotiator.encodings(encodings)[0] || false
|
||||
}
|
||||
|
||||
/**
|
||||
* Return accepted charsets or best fit based on `charsets`.
|
||||
*
|
||||
* Given `Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5`
|
||||
* an array sorted by quality is returned:
|
||||
*
|
||||
* ['utf-8', 'utf-7', 'iso-8859-1']
|
||||
*
|
||||
* @param {String|Array} charsets...
|
||||
* @return {String|Array}
|
||||
* @public
|
||||
*/
|
||||
|
||||
Accepts.prototype.charset =
|
||||
Accepts.prototype.charsets = function (charsets_) {
|
||||
var charsets = charsets_
|
||||
|
||||
// support flattened arguments
|
||||
if (charsets && !Array.isArray(charsets)) {
|
||||
charsets = new Array(arguments.length)
|
||||
for (var i = 0; i < charsets.length; i++) {
|
||||
charsets[i] = arguments[i]
|
||||
}
|
||||
}
|
||||
|
||||
// no charsets, return all requested charsets
|
||||
if (!charsets || charsets.length === 0) {
|
||||
return this.negotiator.charsets()
|
||||
}
|
||||
|
||||
return this.negotiator.charsets(charsets)[0] || false
|
||||
}
|
||||
|
||||
/**
|
||||
* Return accepted languages or best fit based on `langs`.
|
||||
*
|
||||
* Given `Accept-Language: en;q=0.8, es, pt`
|
||||
* an array sorted by quality is returned:
|
||||
*
|
||||
* ['es', 'pt', 'en']
|
||||
*
|
||||
* @param {String|Array} langs...
|
||||
* @return {Array|String}
|
||||
* @public
|
||||
*/
|
||||
|
||||
Accepts.prototype.lang =
|
||||
Accepts.prototype.langs =
|
||||
Accepts.prototype.language =
|
||||
Accepts.prototype.languages = function (languages_) {
|
||||
var languages = languages_
|
||||
|
||||
// support flattened arguments
|
||||
if (languages && !Array.isArray(languages)) {
|
||||
languages = new Array(arguments.length)
|
||||
for (var i = 0; i < languages.length; i++) {
|
||||
languages[i] = arguments[i]
|
||||
}
|
||||
}
|
||||
|
||||
// no languages, return all requested languages
|
||||
if (!languages || languages.length === 0) {
|
||||
return this.negotiator.languages()
|
||||
}
|
||||
|
||||
return this.negotiator.languages(languages)[0] || false
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert extnames to mime.
|
||||
*
|
||||
* @param {String} type
|
||||
* @return {String}
|
||||
* @private
|
||||
*/
|
||||
|
||||
function extToMime (type) {
|
||||
return type.indexOf('/') === -1
|
||||
? mime.lookup(type)
|
||||
: type
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if mime is valid.
|
||||
*
|
||||
* @param {String} type
|
||||
* @return {Boolean}
|
||||
* @private
|
||||
*/
|
||||
|
||||
function validMime (type) {
|
||||
return typeof type === 'string'
|
||||
}
|
||||
47
server/node_modules/accepts/package.json
generated
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "accepts",
|
||||
"description": "Higher-level content negotiation",
|
||||
"version": "2.0.0",
|
||||
"contributors": [
|
||||
"Douglas Christopher Wilson <doug@somethingdoug.com>",
|
||||
"Jonathan Ong <me@jongleberry.com> (http://jongleberry.com)"
|
||||
],
|
||||
"license": "MIT",
|
||||
"repository": "jshttp/accepts",
|
||||
"dependencies": {
|
||||
"mime-types": "^3.0.0",
|
||||
"negotiator": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"deep-equal": "1.0.1",
|
||||
"eslint": "7.32.0",
|
||||
"eslint-config-standard": "14.1.1",
|
||||
"eslint-plugin-import": "2.25.4",
|
||||
"eslint-plugin-markdown": "2.2.1",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"eslint-plugin-promise": "4.3.1",
|
||||
"eslint-plugin-standard": "4.1.0",
|
||||
"mocha": "9.2.0",
|
||||
"nyc": "15.1.0"
|
||||
},
|
||||
"files": [
|
||||
"LICENSE",
|
||||
"HISTORY.md",
|
||||
"index.js"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"test": "mocha --reporter spec --check-leaks --bail test/",
|
||||
"test-ci": "nyc --reporter=lcov --reporter=text npm test",
|
||||
"test-cov": "nyc --reporter=html --reporter=text npm test"
|
||||
},
|
||||
"keywords": [
|
||||
"content",
|
||||
"negotiation",
|
||||
"accept",
|
||||
"accepts"
|
||||
]
|
||||
}
|
||||
731
server/node_modules/body-parser/HISTORY.md
generated
vendored
Normal file
@ -0,0 +1,731 @@
|
||||
2.2.0 / 2025-03-27
|
||||
=========================
|
||||
|
||||
* refactor: normalize common options for all parsers
|
||||
* deps:
|
||||
* iconv-lite@^0.6.3
|
||||
|
||||
2.1.0 / 2025-02-10
|
||||
=========================
|
||||
|
||||
* deps:
|
||||
* type-is@^2.0.0
|
||||
* debug@^4.4.0
|
||||
* Removed destroy
|
||||
* refactor: prefix built-in node module imports
|
||||
* use the node require cache instead of custom caching
|
||||
|
||||
2.0.2 / 2024-10-31
|
||||
=========================
|
||||
|
||||
* remove `unpipe` package and use native `unpipe()` method
|
||||
|
||||
2.0.1 / 2024-09-10
|
||||
=========================
|
||||
|
||||
* Restore expected behavior `extended` to `false`
|
||||
|
||||
2.0.0 / 2024-09-10
|
||||
=========================
|
||||
* Propagate changes from 1.20.3
|
||||
* add brotli support #406
|
||||
* Breaking Change: Node.js 18 is the minimum supported version
|
||||
|
||||
2.0.0-beta.2 / 2023-02-23
|
||||
=========================
|
||||
|
||||
This incorporates all changes after 1.19.1 up to 1.20.2.
|
||||
|
||||
* Remove deprecated `bodyParser()` combination middleware
|
||||
* deps: debug@3.1.0
|
||||
- Add `DEBUG_HIDE_DATE` environment variable
|
||||
- Change timer to per-namespace instead of global
|
||||
- Change non-TTY date format
|
||||
- Remove `DEBUG_FD` environment variable support
|
||||
- Support 256 namespace colors
|
||||
* deps: iconv-lite@0.5.2
|
||||
- Add encoding cp720
|
||||
- Add encoding UTF-32
|
||||
* deps: raw-body@3.0.0-beta.1
|
||||
|
||||
2.0.0-beta.1 / 2021-12-17
|
||||
=========================
|
||||
|
||||
* Drop support for Node.js 0.8
|
||||
* `req.body` is no longer always initialized to `{}`
|
||||
- it is left `undefined` unless a body is parsed
|
||||
* `urlencoded` parser now defaults `extended` to `false`
|
||||
* Use `on-finished` to determine when body read
|
||||
|
||||
1.20.3 / 2024-09-10
|
||||
===================
|
||||
|
||||
* deps: qs@6.13.0
|
||||
* add `depth` option to customize the depth level in the parser
|
||||
* IMPORTANT: The default `depth` level for parsing URL-encoded data is now `32` (previously was `Infinity`)
|
||||
|
||||
1.20.2 / 2023-02-21
|
||||
===================
|
||||
|
||||
* Fix strict json error message on Node.js 19+
|
||||
* deps: content-type@~1.0.5
|
||||
- perf: skip value escaping when unnecessary
|
||||
* deps: raw-body@2.5.2
|
||||
|
||||
1.20.1 / 2022-10-06
|
||||
===================
|
||||
|
||||
* deps: qs@6.11.0
|
||||
* perf: remove unnecessary object clone
|
||||
|
||||
1.20.0 / 2022-04-02
|
||||
===================
|
||||
|
||||
* Fix error message for json parse whitespace in `strict`
|
||||
* Fix internal error when inflated body exceeds limit
|
||||
* Prevent loss of async hooks context
|
||||
* Prevent hanging when request already read
|
||||
* deps: depd@2.0.0
|
||||
- Replace internal `eval` usage with `Function` constructor
|
||||
- Use instance methods on `process` to check for listeners
|
||||
* deps: http-errors@2.0.0
|
||||
- deps: depd@2.0.0
|
||||
- deps: statuses@2.0.1
|
||||
* deps: on-finished@2.4.1
|
||||
* deps: qs@6.10.3
|
||||
* deps: raw-body@2.5.1
|
||||
- deps: http-errors@2.0.0
|
||||
|
||||
1.19.2 / 2022-02-15
|
||||
===================
|
||||
|
||||
* deps: bytes@3.1.2
|
||||
* deps: qs@6.9.7
|
||||
* Fix handling of `__proto__` keys
|
||||
* deps: raw-body@2.4.3
|
||||
- deps: bytes@3.1.2
|
||||
|
||||
1.19.1 / 2021-12-10
|
||||
===================
|
||||
|
||||
* deps: bytes@3.1.1
|
||||
* deps: http-errors@1.8.1
|
||||
- deps: inherits@2.0.4
|
||||
- deps: toidentifier@1.0.1
|
||||
- deps: setprototypeof@1.2.0
|
||||
* deps: qs@6.9.6
|
||||
* deps: raw-body@2.4.2
|
||||
- deps: bytes@3.1.1
|
||||
- deps: http-errors@1.8.1
|
||||
* deps: safe-buffer@5.2.1
|
||||
* deps: type-is@~1.6.18
|
||||
|
||||
1.19.0 / 2019-04-25
|
||||
===================
|
||||
|
||||
* deps: bytes@3.1.0
|
||||
- Add petabyte (`pb`) support
|
||||
* deps: http-errors@1.7.2
|
||||
- Set constructor name when possible
|
||||
- deps: setprototypeof@1.1.1
|
||||
- deps: statuses@'>= 1.5.0 < 2'
|
||||
* deps: iconv-lite@0.4.24
|
||||
- Added encoding MIK
|
||||
* deps: qs@6.7.0
|
||||
- Fix parsing array brackets after index
|
||||
* deps: raw-body@2.4.0
|
||||
- deps: bytes@3.1.0
|
||||
- deps: http-errors@1.7.2
|
||||
- deps: iconv-lite@0.4.24
|
||||
* deps: type-is@~1.6.17
|
||||
- deps: mime-types@~2.1.24
|
||||
- perf: prevent internal `throw` on invalid type
|
||||
|
||||
1.18.3 / 2018-05-14
|
||||
===================
|
||||
|
||||
* Fix stack trace for strict json parse error
|
||||
* deps: depd@~1.1.2
|
||||
- perf: remove argument reassignment
|
||||
* deps: http-errors@~1.6.3
|
||||
- deps: depd@~1.1.2
|
||||
- deps: setprototypeof@1.1.0
|
||||
- deps: statuses@'>= 1.3.1 < 2'
|
||||
* deps: iconv-lite@0.4.23
|
||||
- Fix loading encoding with year appended
|
||||
- Fix deprecation warnings on Node.js 10+
|
||||
* deps: qs@6.5.2
|
||||
* deps: raw-body@2.3.3
|
||||
- deps: http-errors@1.6.3
|
||||
- deps: iconv-lite@0.4.23
|
||||
* deps: type-is@~1.6.16
|
||||
- deps: mime-types@~2.1.18
|
||||
|
||||
1.18.2 / 2017-09-22
|
||||
===================
|
||||
|
||||
* deps: debug@2.6.9
|
||||
* perf: remove argument reassignment
|
||||
|
||||
1.18.1 / 2017-09-12
|
||||
===================
|
||||
|
||||
* deps: content-type@~1.0.4
|
||||
- perf: remove argument reassignment
|
||||
- perf: skip parameter parsing when no parameters
|
||||
* deps: iconv-lite@0.4.19
|
||||
- Fix ISO-8859-1 regression
|
||||
- Update Windows-1255
|
||||
* deps: qs@6.5.1
|
||||
- Fix parsing & compacting very deep objects
|
||||
* deps: raw-body@2.3.2
|
||||
- deps: iconv-lite@0.4.19
|
||||
|
||||
1.18.0 / 2017-09-08
|
||||
===================
|
||||
|
||||
* Fix JSON strict violation error to match native parse error
|
||||
* Include the `body` property on verify errors
|
||||
* Include the `type` property on all generated errors
|
||||
* Use `http-errors` to set status code on errors
|
||||
* deps: bytes@3.0.0
|
||||
* deps: debug@2.6.8
|
||||
* deps: depd@~1.1.1
|
||||
- Remove unnecessary `Buffer` loading
|
||||
* deps: http-errors@~1.6.2
|
||||
- deps: depd@1.1.1
|
||||
* deps: iconv-lite@0.4.18
|
||||
- Add support for React Native
|
||||
- Add a warning if not loaded as utf-8
|
||||
- Fix CESU-8 decoding in Node.js 8
|
||||
- Improve speed of ISO-8859-1 encoding
|
||||
* deps: qs@6.5.0
|
||||
* deps: raw-body@2.3.1
|
||||
- Use `http-errors` for standard emitted errors
|
||||
- deps: bytes@3.0.0
|
||||
- deps: iconv-lite@0.4.18
|
||||
- perf: skip buffer decoding on overage chunk
|
||||
* perf: prevent internal `throw` when missing charset
|
||||
|
||||
1.17.2 / 2017-05-17
|
||||
===================
|
||||
|
||||
* deps: debug@2.6.7
|
||||
- Fix `DEBUG_MAX_ARRAY_LENGTH`
|
||||
- deps: ms@2.0.0
|
||||
* deps: type-is@~1.6.15
|
||||
- deps: mime-types@~2.1.15
|
||||
|
||||
1.17.1 / 2017-03-06
|
||||
===================
|
||||
|
||||
* deps: qs@6.4.0
|
||||
- Fix regression parsing keys starting with `[`
|
||||
|
||||
1.17.0 / 2017-03-01
|
||||
===================
|
||||
|
||||
* deps: http-errors@~1.6.1
|
||||
- Make `message` property enumerable for `HttpError`s
|
||||
- deps: setprototypeof@1.0.3
|
||||
* deps: qs@6.3.1
|
||||
- Fix compacting nested arrays
|
||||
|
||||
1.16.1 / 2017-02-10
|
||||
===================
|
||||
|
||||
* deps: debug@2.6.1
|
||||
- Fix deprecation messages in WebStorm and other editors
|
||||
- Undeprecate `DEBUG_FD` set to `1` or `2`
|
||||
|
||||
1.16.0 / 2017-01-17
|
||||
===================
|
||||
|
||||
* deps: debug@2.6.0
|
||||
- Allow colors in workers
|
||||
- Deprecated `DEBUG_FD` environment variable
|
||||
- Fix error when running under React Native
|
||||
- Use same color for same namespace
|
||||
- deps: ms@0.7.2
|
||||
* deps: http-errors@~1.5.1
|
||||
- deps: inherits@2.0.3
|
||||
- deps: setprototypeof@1.0.2
|
||||
- deps: statuses@'>= 1.3.1 < 2'
|
||||
* deps: iconv-lite@0.4.15
|
||||
- Added encoding MS-31J
|
||||
- Added encoding MS-932
|
||||
- Added encoding MS-936
|
||||
- Added encoding MS-949
|
||||
- Added encoding MS-950
|
||||
- Fix GBK/GB18030 handling of Euro character
|
||||
* deps: qs@6.2.1
|
||||
- Fix array parsing from skipping empty values
|
||||
* deps: raw-body@~2.2.0
|
||||
- deps: iconv-lite@0.4.15
|
||||
* deps: type-is@~1.6.14
|
||||
- deps: mime-types@~2.1.13
|
||||
|
||||
1.15.2 / 2016-06-19
|
||||
===================
|
||||
|
||||
* deps: bytes@2.4.0
|
||||
* deps: content-type@~1.0.2
|
||||
- perf: enable strict mode
|
||||
* deps: http-errors@~1.5.0
|
||||
- Use `setprototypeof` module to replace `__proto__` setting
|
||||
- deps: statuses@'>= 1.3.0 < 2'
|
||||
- perf: enable strict mode
|
||||
* deps: qs@6.2.0
|
||||
* deps: raw-body@~2.1.7
|
||||
- deps: bytes@2.4.0
|
||||
- perf: remove double-cleanup on happy path
|
||||
* deps: type-is@~1.6.13
|
||||
- deps: mime-types@~2.1.11
|
||||
|
||||
1.15.1 / 2016-05-05
|
||||
===================
|
||||
|
||||
* deps: bytes@2.3.0
|
||||
- Drop partial bytes on all parsed units
|
||||
- Fix parsing byte string that looks like hex
|
||||
* deps: raw-body@~2.1.6
|
||||
- deps: bytes@2.3.0
|
||||
* deps: type-is@~1.6.12
|
||||
- deps: mime-types@~2.1.10
|
||||
|
||||
1.15.0 / 2016-02-10
|
||||
===================
|
||||
|
||||
* deps: http-errors@~1.4.0
|
||||
- Add `HttpError` export, for `err instanceof createError.HttpError`
|
||||
- deps: inherits@2.0.1
|
||||
- deps: statuses@'>= 1.2.1 < 2'
|
||||
* deps: qs@6.1.0
|
||||
* deps: type-is@~1.6.11
|
||||
- deps: mime-types@~2.1.9
|
||||
|
||||
1.14.2 / 2015-12-16
|
||||
===================
|
||||
|
||||
* deps: bytes@2.2.0
|
||||
* deps: iconv-lite@0.4.13
|
||||
* deps: qs@5.2.0
|
||||
* deps: raw-body@~2.1.5
|
||||
- deps: bytes@2.2.0
|
||||
- deps: iconv-lite@0.4.13
|
||||
* deps: type-is@~1.6.10
|
||||
- deps: mime-types@~2.1.8
|
||||
|
||||
1.14.1 / 2015-09-27
|
||||
===================
|
||||
|
||||
* Fix issue where invalid charset results in 400 when `verify` used
|
||||
* deps: iconv-lite@0.4.12
|
||||
- Fix CESU-8 decoding in Node.js 4.x
|
||||
* deps: raw-body@~2.1.4
|
||||
- Fix masking critical errors from `iconv-lite`
|
||||
- deps: iconv-lite@0.4.12
|
||||
* deps: type-is@~1.6.9
|
||||
- deps: mime-types@~2.1.7
|
||||
|
||||
1.14.0 / 2015-09-16
|
||||
===================
|
||||
|
||||
* Fix JSON strict parse error to match syntax errors
|
||||
* Provide static `require` analysis in `urlencoded` parser
|
||||
* deps: depd@~1.1.0
|
||||
- Support web browser loading
|
||||
* deps: qs@5.1.0
|
||||
* deps: raw-body@~2.1.3
|
||||
- Fix sync callback when attaching data listener causes sync read
|
||||
* deps: type-is@~1.6.8
|
||||
- Fix type error when given invalid type to match against
|
||||
- deps: mime-types@~2.1.6
|
||||
|
||||
1.13.3 / 2015-07-31
|
||||
===================
|
||||
|
||||
* deps: type-is@~1.6.6
|
||||
- deps: mime-types@~2.1.4
|
||||
|
||||
1.13.2 / 2015-07-05
|
||||
===================
|
||||
|
||||
* deps: iconv-lite@0.4.11
|
||||
* deps: qs@4.0.0
|
||||
- Fix dropping parameters like `hasOwnProperty`
|
||||
- Fix user-visible incompatibilities from 3.1.0
|
||||
- Fix various parsing edge cases
|
||||
* deps: raw-body@~2.1.2
|
||||
- Fix error stack traces to skip `makeError`
|
||||
- deps: iconv-lite@0.4.11
|
||||
* deps: type-is@~1.6.4
|
||||
- deps: mime-types@~2.1.2
|
||||
- perf: enable strict mode
|
||||
- perf: remove argument reassignment
|
||||
|
||||
1.13.1 / 2015-06-16
|
||||
===================
|
||||
|
||||
* deps: qs@2.4.2
|
||||
- Downgraded from 3.1.0 because of user-visible incompatibilities
|
||||
|
||||
1.13.0 / 2015-06-14
|
||||
===================
|
||||
|
||||
* Add `statusCode` property on `Error`s, in addition to `status`
|
||||
* Change `type` default to `application/json` for JSON parser
|
||||
* Change `type` default to `application/x-www-form-urlencoded` for urlencoded parser
|
||||
* Provide static `require` analysis
|
||||
* Use the `http-errors` module to generate errors
|
||||
* deps: bytes@2.1.0
|
||||
- Slight optimizations
|
||||
* deps: iconv-lite@0.4.10
|
||||
- The encoding UTF-16 without BOM now defaults to UTF-16LE when detection fails
|
||||
- Leading BOM is now removed when decoding
|
||||
* deps: on-finished@~2.3.0
|
||||
- Add defined behavior for HTTP `CONNECT` requests
|
||||
- Add defined behavior for HTTP `Upgrade` requests
|
||||
- deps: ee-first@1.1.1
|
||||
* deps: qs@3.1.0
|
||||
- Fix dropping parameters like `hasOwnProperty`
|
||||
- Fix various parsing edge cases
|
||||
- Parsed object now has `null` prototype
|
||||
* deps: raw-body@~2.1.1
|
||||
- Use `unpipe` module for unpiping requests
|
||||
- deps: iconv-lite@0.4.10
|
||||
* deps: type-is@~1.6.3
|
||||
- deps: mime-types@~2.1.1
|
||||
- perf: reduce try block size
|
||||
- perf: remove bitwise operations
|
||||
* perf: enable strict mode
|
||||
* perf: remove argument reassignment
|
||||
* perf: remove delete call
|
||||
|
||||
1.12.4 / 2015-05-10
|
||||
===================
|
||||
|
||||
* deps: debug@~2.2.0
|
||||
* deps: qs@2.4.2
|
||||
- Fix allowing parameters like `constructor`
|
||||
* deps: on-finished@~2.2.1
|
||||
* deps: raw-body@~2.0.1
|
||||
- Fix a false-positive when unpiping in Node.js 0.8
|
||||
- deps: bytes@2.0.1
|
||||
* deps: type-is@~1.6.2
|
||||
- deps: mime-types@~2.0.11
|
||||
|
||||
1.12.3 / 2015-04-15
|
||||
===================
|
||||
|
||||
* Slight efficiency improvement when not debugging
|
||||
* deps: depd@~1.0.1
|
||||
* deps: iconv-lite@0.4.8
|
||||
- Add encoding alias UNICODE-1-1-UTF-7
|
||||
* deps: raw-body@1.3.4
|
||||
- Fix hanging callback if request aborts during read
|
||||
- deps: iconv-lite@0.4.8
|
||||
|
||||
1.12.2 / 2015-03-16
|
||||
===================
|
||||
|
||||
* deps: qs@2.4.1
|
||||
- Fix error when parameter `hasOwnProperty` is present
|
||||
|
||||
1.12.1 / 2015-03-15
|
||||
===================
|
||||
|
||||
* deps: debug@~2.1.3
|
||||
- Fix high intensity foreground color for bold
|
||||
- deps: ms@0.7.0
|
||||
* deps: type-is@~1.6.1
|
||||
- deps: mime-types@~2.0.10
|
||||
|
||||
1.12.0 / 2015-02-13
|
||||
===================
|
||||
|
||||
* add `debug` messages
|
||||
* accept a function for the `type` option
|
||||
* use `content-type` to parse `Content-Type` headers
|
||||
* deps: iconv-lite@0.4.7
|
||||
- Gracefully support enumerables on `Object.prototype`
|
||||
* deps: raw-body@1.3.3
|
||||
- deps: iconv-lite@0.4.7
|
||||
* deps: type-is@~1.6.0
|
||||
- fix argument reassignment
|
||||
- fix false-positives in `hasBody` `Transfer-Encoding` check
|
||||
- support wildcard for both type and subtype (`*/*`)
|
||||
- deps: mime-types@~2.0.9
|
||||
|
||||
1.11.0 / 2015-01-30
|
||||
===================
|
||||
|
||||
* make internal `extended: true` depth limit infinity
|
||||
* deps: type-is@~1.5.6
|
||||
- deps: mime-types@~2.0.8
|
||||
|
||||
1.10.2 / 2015-01-20
|
||||
===================
|
||||
|
||||
* deps: iconv-lite@0.4.6
|
||||
- Fix rare aliases of single-byte encodings
|
||||
* deps: raw-body@1.3.2
|
||||
- deps: iconv-lite@0.4.6
|
||||
|
||||
1.10.1 / 2015-01-01
|
||||
===================
|
||||
|
||||
* deps: on-finished@~2.2.0
|
||||
* deps: type-is@~1.5.5
|
||||
- deps: mime-types@~2.0.7
|
||||
|
||||
1.10.0 / 2014-12-02
|
||||
===================
|
||||
|
||||
* make internal `extended: true` array limit dynamic
|
||||
|
||||
1.9.3 / 2014-11-21
|
||||
==================
|
||||
|
||||
* deps: iconv-lite@0.4.5
|
||||
- Fix Windows-31J and X-SJIS encoding support
|
||||
* deps: qs@2.3.3
|
||||
- Fix `arrayLimit` behavior
|
||||
* deps: raw-body@1.3.1
|
||||
- deps: iconv-lite@0.4.5
|
||||
* deps: type-is@~1.5.3
|
||||
- deps: mime-types@~2.0.3
|
||||
|
||||
1.9.2 / 2014-10-27
|
||||
==================
|
||||
|
||||
* deps: qs@2.3.2
|
||||
- Fix parsing of mixed objects and values
|
||||
|
||||
1.9.1 / 2014-10-22
|
||||
==================
|
||||
|
||||
* deps: on-finished@~2.1.1
|
||||
- Fix handling of pipelined requests
|
||||
* deps: qs@2.3.0
|
||||
- Fix parsing of mixed implicit and explicit arrays
|
||||
* deps: type-is@~1.5.2
|
||||
- deps: mime-types@~2.0.2
|
||||
|
||||
1.9.0 / 2014-09-24
|
||||
==================
|
||||
|
||||
* include the charset in "unsupported charset" error message
|
||||
* include the encoding in "unsupported content encoding" error message
|
||||
* deps: depd@~1.0.0
|
||||
|
||||
1.8.4 / 2014-09-23
|
||||
==================
|
||||
|
||||
* fix content encoding to be case-insensitive
|
||||
|
||||
1.8.3 / 2014-09-19
|
||||
==================
|
||||
|
||||
* deps: qs@2.2.4
|
||||
- Fix issue with object keys starting with numbers truncated
|
||||
|
||||
1.8.2 / 2014-09-15
|
||||
==================
|
||||
|
||||
* deps: depd@0.4.5
|
||||
|
||||
1.8.1 / 2014-09-07
|
||||
==================
|
||||
|
||||
* deps: media-typer@0.3.0
|
||||
* deps: type-is@~1.5.1
|
||||
|
||||
1.8.0 / 2014-09-05
|
||||
==================
|
||||
|
||||
* make empty-body-handling consistent between chunked requests
|
||||
- empty `json` produces `{}`
|
||||
- empty `raw` produces `new Buffer(0)`
|
||||
- empty `text` produces `''`
|
||||
- empty `urlencoded` produces `{}`
|
||||
* deps: qs@2.2.3
|
||||
- Fix issue where first empty value in array is discarded
|
||||
* deps: type-is@~1.5.0
|
||||
- fix `hasbody` to be true for `content-length: 0`
|
||||
|
||||
1.7.0 / 2014-09-01
|
||||
==================
|
||||
|
||||
* add `parameterLimit` option to `urlencoded` parser
|
||||
* change `urlencoded` extended array limit to 100
|
||||
* respond with 413 when over `parameterLimit` in `urlencoded`
|
||||
|
||||
1.6.7 / 2014-08-29
|
||||
==================
|
||||
|
||||
* deps: qs@2.2.2
|
||||
- Remove unnecessary cloning
|
||||
|
||||
1.6.6 / 2014-08-27
|
||||
==================
|
||||
|
||||
* deps: qs@2.2.0
|
||||
- Array parsing fix
|
||||
- Performance improvements
|
||||
|
||||
1.6.5 / 2014-08-16
|
||||
==================
|
||||
|
||||
* deps: on-finished@2.1.0
|
||||
|
||||
1.6.4 / 2014-08-14
|
||||
==================
|
||||
|
||||
* deps: qs@1.2.2
|
||||
|
||||
1.6.3 / 2014-08-10
|
||||
==================
|
||||
|
||||
* deps: qs@1.2.1
|
||||
|
||||
1.6.2 / 2014-08-07
|
||||
==================
|
||||
|
||||
* deps: qs@1.2.0
|
||||
- Fix parsing array of objects
|
||||
|
||||
1.6.1 / 2014-08-06
|
||||
==================
|
||||
|
||||
* deps: qs@1.1.0
|
||||
- Accept urlencoded square brackets
|
||||
- Accept empty values in implicit array notation
|
||||
|
||||
1.6.0 / 2014-08-05
|
||||
==================
|
||||
|
||||
* deps: qs@1.0.2
|
||||
- Complete rewrite
|
||||
- Limits array length to 20
|
||||
- Limits object depth to 5
|
||||
- Limits parameters to 1,000
|
||||
|
||||
1.5.2 / 2014-07-27
|
||||
==================
|
||||
|
||||
* deps: depd@0.4.4
|
||||
- Work-around v8 generating empty stack traces
|
||||
|
||||
1.5.1 / 2014-07-26
|
||||
==================
|
||||
|
||||
* deps: depd@0.4.3
|
||||
- Fix exception when global `Error.stackTraceLimit` is too low
|
||||
|
||||
1.5.0 / 2014-07-20
|
||||
==================
|
||||
|
||||
* deps: depd@0.4.2
|
||||
- Add `TRACE_DEPRECATION` environment variable
|
||||
- Remove non-standard grey color from color output
|
||||
- Support `--no-deprecation` argument
|
||||
- Support `--trace-deprecation` argument
|
||||
* deps: iconv-lite@0.4.4
|
||||
- Added encoding UTF-7
|
||||
* deps: raw-body@1.3.0
|
||||
- deps: iconv-lite@0.4.4
|
||||
- Added encoding UTF-7
|
||||
- Fix `Cannot switch to old mode now` error on Node.js 0.10+
|
||||
* deps: type-is@~1.3.2
|
||||
|
||||
1.4.3 / 2014-06-19
|
||||
==================
|
||||
|
||||
* deps: type-is@1.3.1
|
||||
- fix global variable leak
|
||||
|
||||
1.4.2 / 2014-06-19
|
||||
==================
|
||||
|
||||
* deps: type-is@1.3.0
|
||||
- improve type parsing
|
||||
|
||||
1.4.1 / 2014-06-19
|
||||
==================
|
||||
|
||||
* fix urlencoded extended deprecation message
|
||||
|
||||
1.4.0 / 2014-06-19
|
||||
==================
|
||||
|
||||
* add `text` parser
|
||||
* add `raw` parser
|
||||
* check accepted charset in content-type (accepts utf-8)
|
||||
* check accepted encoding in content-encoding (accepts identity)
|
||||
* deprecate `bodyParser()` middleware; use `.json()` and `.urlencoded()` as needed
|
||||
* deprecate `urlencoded()` without provided `extended` option
|
||||
* lazy-load urlencoded parsers
|
||||
* parsers split into files for reduced mem usage
|
||||
* support gzip and deflate bodies
|
||||
- set `inflate: false` to turn off
|
||||
* deps: raw-body@1.2.2
|
||||
- Support all encodings from `iconv-lite`
|
||||
|
||||
1.3.1 / 2014-06-11
|
||||
==================
|
||||
|
||||
* deps: type-is@1.2.1
|
||||
- Switch dependency from mime to mime-types@1.0.0
|
||||
|
||||
1.3.0 / 2014-05-31
|
||||
==================
|
||||
|
||||
* add `extended` option to urlencoded parser
|
||||
|
||||
1.2.2 / 2014-05-27
|
||||
==================
|
||||
|
||||
* deps: raw-body@1.1.6
|
||||
- assert stream encoding on node.js 0.8
|
||||
- assert stream encoding on node.js < 0.10.6
|
||||
- deps: bytes@1
|
||||
|
||||
1.2.1 / 2014-05-26
|
||||
==================
|
||||
|
||||
* invoke `next(err)` after request fully read
|
||||
- prevents hung responses and socket hang ups
|
||||
|
||||
1.2.0 / 2014-05-11
|
||||
==================
|
||||
|
||||
* add `verify` option
|
||||
* deps: type-is@1.2.0
|
||||
- support suffix matching
|
||||
|
||||
1.1.2 / 2014-05-11
|
||||
==================
|
||||
|
||||
* improve json parser speed
|
||||
|
||||
1.1.1 / 2014-05-11
|
||||
==================
|
||||
|
||||
* fix repeated limit parsing with every request
|
||||
|
||||
1.1.0 / 2014-05-10
|
||||
==================
|
||||
|
||||
* add `type` option
|
||||
* deps: pin for safety and consistency
|
||||
|
||||
1.0.2 / 2014-04-14
|
||||
==================
|
||||
|
||||
* use `type-is` module
|
||||
|
||||
1.0.1 / 2014-03-20
|
||||
==================
|
||||
|
||||
* lower default limits to 100kb
|
||||
23
server/node_modules/body-parser/LICENSE
generated
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2014 Jonathan Ong <me@jongleberry.com>
|
||||
Copyright (c) 2014-2015 Douglas Christopher Wilson <doug@somethingdoug.com>
|
||||
|
||||
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.
|
||||
491
server/node_modules/body-parser/README.md
generated
vendored
Normal file
@ -0,0 +1,491 @@
|
||||
# body-parser
|
||||
|
||||
[![NPM Version][npm-version-image]][npm-url]
|
||||
[![NPM Downloads][npm-downloads-image]][npm-url]
|
||||
[![Build Status][ci-image]][ci-url]
|
||||
[![Test Coverage][coveralls-image]][coveralls-url]
|
||||
[![OpenSSF Scorecard Badge][ossf-scorecard-badge]][ossf-scorecard-visualizer]
|
||||
|
||||
Node.js body parsing middleware.
|
||||
|
||||
Parse incoming request bodies in a middleware before your handlers, available
|
||||
under the `req.body` property.
|
||||
|
||||
**Note** As `req.body`'s shape is based on user-controlled input, all
|
||||
properties and values in this object are untrusted and should be validated
|
||||
before trusting. For example, `req.body.foo.toString()` may fail in multiple
|
||||
ways, for example the `foo` property may not be there or may not be a string,
|
||||
and `toString` may not be a function and instead a string or other user input.
|
||||
|
||||
[Learn about the anatomy of an HTTP transaction in Node.js](https://nodejs.org/en/docs/guides/anatomy-of-an-http-transaction/).
|
||||
|
||||
_This does not handle multipart bodies_, due to their complex and typically
|
||||
large nature. For multipart bodies, you may be interested in the following
|
||||
modules:
|
||||
|
||||
* [busboy](https://www.npmjs.org/package/busboy#readme) and
|
||||
[connect-busboy](https://www.npmjs.org/package/connect-busboy#readme)
|
||||
* [multiparty](https://www.npmjs.org/package/multiparty#readme) and
|
||||
[connect-multiparty](https://www.npmjs.org/package/connect-multiparty#readme)
|
||||
* [formidable](https://www.npmjs.org/package/formidable#readme)
|
||||
* [multer](https://www.npmjs.org/package/multer#readme)
|
||||
|
||||
This module provides the following parsers:
|
||||
|
||||
* [JSON body parser](#bodyparserjsonoptions)
|
||||
* [Raw body parser](#bodyparserrawoptions)
|
||||
* [Text body parser](#bodyparsertextoptions)
|
||||
* [URL-encoded form body parser](#bodyparserurlencodedoptions)
|
||||
|
||||
Other body parsers you might be interested in:
|
||||
|
||||
- [body](https://www.npmjs.org/package/body#readme)
|
||||
- [co-body](https://www.npmjs.org/package/co-body#readme)
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
$ npm install body-parser
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
```js
|
||||
const bodyParser = require('body-parser')
|
||||
```
|
||||
|
||||
The `bodyParser` object exposes various factories to create middlewares. All
|
||||
middlewares will populate the `req.body` property with the parsed body when
|
||||
the `Content-Type` request header matches the `type` option.
|
||||
|
||||
The various errors returned by this module are described in the
|
||||
[errors section](#errors).
|
||||
|
||||
### bodyParser.json([options])
|
||||
|
||||
Returns middleware that only parses `json` and only looks at requests where
|
||||
the `Content-Type` header matches the `type` option. This parser accepts any
|
||||
Unicode encoding of the body and supports automatic inflation of `gzip`,
|
||||
`br` (brotli) and `deflate` encodings.
|
||||
|
||||
A new `body` object containing the parsed data is populated on the `request`
|
||||
object after the middleware (i.e. `req.body`).
|
||||
|
||||
#### Options
|
||||
|
||||
The `json` function takes an optional `options` object that may contain any of
|
||||
the following keys:
|
||||
|
||||
##### inflate
|
||||
|
||||
When set to `true`, then deflated (compressed) bodies will be inflated; when
|
||||
`false`, deflated bodies are rejected. Defaults to `true`.
|
||||
|
||||
##### limit
|
||||
|
||||
Controls the maximum request body size. If this is a number, then the value
|
||||
specifies the number of bytes; if it is a string, the value is passed to the
|
||||
[bytes](https://www.npmjs.com/package/bytes) library for parsing. Defaults
|
||||
to `'100kb'`.
|
||||
|
||||
##### reviver
|
||||
|
||||
The `reviver` option is passed directly to `JSON.parse` as the second
|
||||
argument. You can find more information on this argument
|
||||
[in the MDN documentation about JSON.parse](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#Example.3A_Using_the_reviver_parameter).
|
||||
|
||||
##### strict
|
||||
|
||||
When set to `true`, will only accept arrays and objects; when `false` will
|
||||
accept anything `JSON.parse` accepts. Defaults to `true`.
|
||||
|
||||
##### type
|
||||
|
||||
The `type` option is used to determine what media type the middleware will
|
||||
parse. This option can be a string, array of strings, or a function. If not a
|
||||
function, `type` option is passed directly to the
|
||||
[type-is](https://www.npmjs.org/package/type-is#readme) library and this can
|
||||
be an extension name (like `json`), a mime type (like `application/json`), or
|
||||
a mime type with a wildcard (like `*/*` or `*/json`). If a function, the `type`
|
||||
option is called as `fn(req)` and the request is parsed if it returns a truthy
|
||||
value. Defaults to `application/json`.
|
||||
|
||||
##### verify
|
||||
|
||||
The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)`,
|
||||
where `buf` is a `Buffer` of the raw request body and `encoding` is the
|
||||
encoding of the request. The parsing can be aborted by throwing an error.
|
||||
|
||||
### bodyParser.raw([options])
|
||||
|
||||
Returns middleware that parses all bodies as a `Buffer` and only looks at
|
||||
requests where the `Content-Type` header matches the `type` option. This
|
||||
parser supports automatic inflation of `gzip`, `br` (brotli) and `deflate`
|
||||
encodings.
|
||||
|
||||
A new `body` object containing the parsed data is populated on the `request`
|
||||
object after the middleware (i.e. `req.body`). This will be a `Buffer` object
|
||||
of the body.
|
||||
|
||||
#### Options
|
||||
|
||||
The `raw` function takes an optional `options` object that may contain any of
|
||||
the following keys:
|
||||
|
||||
##### inflate
|
||||
|
||||
When set to `true`, then deflated (compressed) bodies will be inflated; when
|
||||
`false`, deflated bodies are rejected. Defaults to `true`.
|
||||
|
||||
##### limit
|
||||
|
||||
Controls the maximum request body size. If this is a number, then the value
|
||||
specifies the number of bytes; if it is a string, the value is passed to the
|
||||
[bytes](https://www.npmjs.com/package/bytes) library for parsing. Defaults
|
||||
to `'100kb'`.
|
||||
|
||||
##### type
|
||||
|
||||
The `type` option is used to determine what media type the middleware will
|
||||
parse. This option can be a string, array of strings, or a function.
|
||||
If not a function, `type` option is passed directly to the
|
||||
[type-is](https://www.npmjs.org/package/type-is#readme) library and this
|
||||
can be an extension name (like `bin`), a mime type (like
|
||||
`application/octet-stream`), or a mime type with a wildcard (like `*/*` or
|
||||
`application/*`). If a function, the `type` option is called as `fn(req)`
|
||||
and the request is parsed if it returns a truthy value. Defaults to
|
||||
`application/octet-stream`.
|
||||
|
||||
##### verify
|
||||
|
||||
The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)`,
|
||||
where `buf` is a `Buffer` of the raw request body and `encoding` is the
|
||||
encoding of the request. The parsing can be aborted by throwing an error.
|
||||
|
||||
### bodyParser.text([options])
|
||||
|
||||
Returns middleware that parses all bodies as a string and only looks at
|
||||
requests where the `Content-Type` header matches the `type` option. This
|
||||
parser supports automatic inflation of `gzip`, `br` (brotli) and `deflate`
|
||||
encodings.
|
||||
|
||||
A new `body` string containing the parsed data is populated on the `request`
|
||||
object after the middleware (i.e. `req.body`). This will be a string of the
|
||||
body.
|
||||
|
||||
#### Options
|
||||
|
||||
The `text` function takes an optional `options` object that may contain any of
|
||||
the following keys:
|
||||
|
||||
##### defaultCharset
|
||||
|
||||
Specify the default character set for the text content if the charset is not
|
||||
specified in the `Content-Type` header of the request. Defaults to `utf-8`.
|
||||
|
||||
##### inflate
|
||||
|
||||
When set to `true`, then deflated (compressed) bodies will be inflated; when
|
||||
`false`, deflated bodies are rejected. Defaults to `true`.
|
||||
|
||||
##### limit
|
||||
|
||||
Controls the maximum request body size. If this is a number, then the value
|
||||
specifies the number of bytes; if it is a string, the value is passed to the
|
||||
[bytes](https://www.npmjs.com/package/bytes) library for parsing. Defaults
|
||||
to `'100kb'`.
|
||||
|
||||
##### type
|
||||
|
||||
The `type` option is used to determine what media type the middleware will
|
||||
parse. This option can be a string, array of strings, or a function. If not
|
||||
a function, `type` option is passed directly to the
|
||||
[type-is](https://www.npmjs.org/package/type-is#readme) library and this can
|
||||
be an extension name (like `txt`), a mime type (like `text/plain`), or a mime
|
||||
type with a wildcard (like `*/*` or `text/*`). If a function, the `type`
|
||||
option is called as `fn(req)` and the request is parsed if it returns a
|
||||
truthy value. Defaults to `text/plain`.
|
||||
|
||||
##### verify
|
||||
|
||||
The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)`,
|
||||
where `buf` is a `Buffer` of the raw request body and `encoding` is the
|
||||
encoding of the request. The parsing can be aborted by throwing an error.
|
||||
|
||||
### bodyParser.urlencoded([options])
|
||||
|
||||
Returns middleware that only parses `urlencoded` bodies and only looks at
|
||||
requests where the `Content-Type` header matches the `type` option. This
|
||||
parser accepts only UTF-8 encoding of the body and supports automatic
|
||||
inflation of `gzip`, `br` (brotli) and `deflate` encodings.
|
||||
|
||||
A new `body` object containing the parsed data is populated on the `request`
|
||||
object after the middleware (i.e. `req.body`). This object will contain
|
||||
key-value pairs, where the value can be a string or array (when `extended` is
|
||||
`false`), or any type (when `extended` is `true`).
|
||||
|
||||
#### Options
|
||||
|
||||
The `urlencoded` function takes an optional `options` object that may contain
|
||||
any of the following keys:
|
||||
|
||||
##### extended
|
||||
|
||||
The "extended" syntax allows for rich objects and arrays to be encoded into the
|
||||
URL-encoded format, allowing for a JSON-like experience with URL-encoded. For
|
||||
more information, please [see the qs
|
||||
library](https://www.npmjs.org/package/qs#readme).
|
||||
|
||||
Defaults to `false`.
|
||||
|
||||
##### inflate
|
||||
|
||||
When set to `true`, then deflated (compressed) bodies will be inflated; when
|
||||
`false`, deflated bodies are rejected. Defaults to `true`.
|
||||
|
||||
##### limit
|
||||
|
||||
Controls the maximum request body size. If this is a number, then the value
|
||||
specifies the number of bytes; if it is a string, the value is passed to the
|
||||
[bytes](https://www.npmjs.com/package/bytes) library for parsing. Defaults
|
||||
to `'100kb'`.
|
||||
|
||||
##### parameterLimit
|
||||
|
||||
The `parameterLimit` option controls the maximum number of parameters that
|
||||
are allowed in the URL-encoded data. If a request contains more parameters
|
||||
than this value, a 413 will be returned to the client. Defaults to `1000`.
|
||||
|
||||
##### type
|
||||
|
||||
The `type` option is used to determine what media type the middleware will
|
||||
parse. This option can be a string, array of strings, or a function. If not
|
||||
a function, `type` option is passed directly to the
|
||||
[type-is](https://www.npmjs.org/package/type-is#readme) library and this can
|
||||
be an extension name (like `urlencoded`), a mime type (like
|
||||
`application/x-www-form-urlencoded`), or a mime type with a wildcard (like
|
||||
`*/x-www-form-urlencoded`). If a function, the `type` option is called as
|
||||
`fn(req)` and the request is parsed if it returns a truthy value. Defaults
|
||||
to `application/x-www-form-urlencoded`.
|
||||
|
||||
##### verify
|
||||
|
||||
The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)`,
|
||||
where `buf` is a `Buffer` of the raw request body and `encoding` is the
|
||||
encoding of the request. The parsing can be aborted by throwing an error.
|
||||
|
||||
##### defaultCharset
|
||||
|
||||
The default charset to parse as, if not specified in content-type. Must be
|
||||
either `utf-8` or `iso-8859-1`. Defaults to `utf-8`.
|
||||
|
||||
##### charsetSentinel
|
||||
|
||||
Whether to let the value of the `utf8` parameter take precedence as the charset
|
||||
selector. It requires the form to contain a parameter named `utf8` with a value
|
||||
of `✓`. Defaults to `false`.
|
||||
|
||||
##### interpretNumericEntities
|
||||
|
||||
Whether to decode numeric entities such as `☺` when parsing an iso-8859-1
|
||||
form. Defaults to `false`.
|
||||
|
||||
|
||||
#### depth
|
||||
|
||||
The `depth` option is used to configure the maximum depth of the `qs` library when `extended` is `true`. This allows you to limit the amount of keys that are parsed and can be useful to prevent certain types of abuse. Defaults to `32`. It is recommended to keep this value as low as possible.
|
||||
|
||||
## Errors
|
||||
|
||||
The middlewares provided by this module create errors using the
|
||||
[`http-errors` module](https://www.npmjs.com/package/http-errors). The errors
|
||||
will typically have a `status`/`statusCode` property that contains the suggested
|
||||
HTTP response code, an `expose` property to determine if the `message` property
|
||||
should be displayed to the client, a `type` property to determine the type of
|
||||
error without matching against the `message`, and a `body` property containing
|
||||
the read body, if available.
|
||||
|
||||
The following are the common errors created, though any error can come through
|
||||
for various reasons.
|
||||
|
||||
### content encoding unsupported
|
||||
|
||||
This error will occur when the request had a `Content-Encoding` header that
|
||||
contained an encoding but the "inflation" option was set to `false`. The
|
||||
`status` property is set to `415`, the `type` property is set to
|
||||
`'encoding.unsupported'`, and the `charset` property will be set to the
|
||||
encoding that is unsupported.
|
||||
|
||||
### entity parse failed
|
||||
|
||||
This error will occur when the request contained an entity that could not be
|
||||
parsed by the middleware. The `status` property is set to `400`, the `type`
|
||||
property is set to `'entity.parse.failed'`, and the `body` property is set to
|
||||
the entity value that failed parsing.
|
||||
|
||||
### entity verify failed
|
||||
|
||||
This error will occur when the request contained an entity that could not be
|
||||
failed verification by the defined `verify` option. The `status` property is
|
||||
set to `403`, the `type` property is set to `'entity.verify.failed'`, and the
|
||||
`body` property is set to the entity value that failed verification.
|
||||
|
||||
### request aborted
|
||||
|
||||
This error will occur when the request is aborted by the client before reading
|
||||
the body has finished. The `received` property will be set to the number of
|
||||
bytes received before the request was aborted and the `expected` property is
|
||||
set to the number of expected bytes. The `status` property is set to `400`
|
||||
and `type` property is set to `'request.aborted'`.
|
||||
|
||||
### request entity too large
|
||||
|
||||
This error will occur when the request body's size is larger than the "limit"
|
||||
option. The `limit` property will be set to the byte limit and the `length`
|
||||
property will be set to the request body's length. The `status` property is
|
||||
set to `413` and the `type` property is set to `'entity.too.large'`.
|
||||
|
||||
### request size did not match content length
|
||||
|
||||
This error will occur when the request's length did not match the length from
|
||||
the `Content-Length` header. This typically occurs when the request is malformed,
|
||||
typically when the `Content-Length` header was calculated based on characters
|
||||
instead of bytes. The `status` property is set to `400` and the `type` property
|
||||
is set to `'request.size.invalid'`.
|
||||
|
||||
### stream encoding should not be set
|
||||
|
||||
This error will occur when something called the `req.setEncoding` method prior
|
||||
to this middleware. This module operates directly on bytes only and you cannot
|
||||
call `req.setEncoding` when using this module. The `status` property is set to
|
||||
`500` and the `type` property is set to `'stream.encoding.set'`.
|
||||
|
||||
### stream is not readable
|
||||
|
||||
This error will occur when the request is no longer readable when this middleware
|
||||
attempts to read it. This typically means something other than a middleware from
|
||||
this module read the request body already and the middleware was also configured to
|
||||
read the same request. The `status` property is set to `500` and the `type`
|
||||
property is set to `'stream.not.readable'`.
|
||||
|
||||
### too many parameters
|
||||
|
||||
This error will occur when the content of the request exceeds the configured
|
||||
`parameterLimit` for the `urlencoded` parser. The `status` property is set to
|
||||
`413` and the `type` property is set to `'parameters.too.many'`.
|
||||
|
||||
### unsupported charset "BOGUS"
|
||||
|
||||
This error will occur when the request had a charset parameter in the
|
||||
`Content-Type` header, but the `iconv-lite` module does not support it OR the
|
||||
parser does not support it. The charset is contained in the message as well
|
||||
as in the `charset` property. The `status` property is set to `415`, the
|
||||
`type` property is set to `'charset.unsupported'`, and the `charset` property
|
||||
is set to the charset that is unsupported.
|
||||
|
||||
### unsupported content encoding "bogus"
|
||||
|
||||
This error will occur when the request had a `Content-Encoding` header that
|
||||
contained an unsupported encoding. The encoding is contained in the message
|
||||
as well as in the `encoding` property. The `status` property is set to `415`,
|
||||
the `type` property is set to `'encoding.unsupported'`, and the `encoding`
|
||||
property is set to the encoding that is unsupported.
|
||||
|
||||
### The input exceeded the depth
|
||||
|
||||
This error occurs when using `bodyParser.urlencoded` with the `extended` property set to `true` and the input exceeds the configured `depth` option. The `status` property is set to `400`. It is recommended to review the `depth` option and evaluate if it requires a higher value. When the `depth` option is set to `32` (default value), the error will not be thrown.
|
||||
|
||||
## Examples
|
||||
|
||||
### Express/Connect top-level generic
|
||||
|
||||
This example demonstrates adding a generic JSON and URL-encoded parser as a
|
||||
top-level middleware, which will parse the bodies of all incoming requests.
|
||||
This is the simplest setup.
|
||||
|
||||
```js
|
||||
const express = require('express')
|
||||
const bodyParser = require('body-parser')
|
||||
|
||||
const app = express()
|
||||
|
||||
// parse application/x-www-form-urlencoded
|
||||
app.use(bodyParser.urlencoded())
|
||||
|
||||
// parse application/json
|
||||
app.use(bodyParser.json())
|
||||
|
||||
app.use(function (req, res) {
|
||||
res.setHeader('Content-Type', 'text/plain')
|
||||
res.write('you posted:\n')
|
||||
res.end(String(JSON.stringify(req.body, null, 2)))
|
||||
})
|
||||
```
|
||||
|
||||
### Express route-specific
|
||||
|
||||
This example demonstrates adding body parsers specifically to the routes that
|
||||
need them. In general, this is the most recommended way to use body-parser with
|
||||
Express.
|
||||
|
||||
```js
|
||||
const express = require('express')
|
||||
const bodyParser = require('body-parser')
|
||||
|
||||
const app = express()
|
||||
|
||||
// create application/json parser
|
||||
const jsonParser = bodyParser.json()
|
||||
|
||||
// create application/x-www-form-urlencoded parser
|
||||
const urlencodedParser = bodyParser.urlencoded()
|
||||
|
||||
// POST /login gets urlencoded bodies
|
||||
app.post('/login', urlencodedParser, function (req, res) {
|
||||
if (!req.body || !req.body.username) res.sendStatus(400)
|
||||
res.send('welcome, ' + req.body.username)
|
||||
})
|
||||
|
||||
// POST /api/users gets JSON bodies
|
||||
app.post('/api/users', jsonParser, function (req, res) {
|
||||
if (!req.body) res.sendStatus(400)
|
||||
// create user in req.body
|
||||
})
|
||||
```
|
||||
|
||||
### Change accepted type for parsers
|
||||
|
||||
All the parsers accept a `type` option which allows you to change the
|
||||
`Content-Type` that the middleware will parse.
|
||||
|
||||
```js
|
||||
const express = require('express')
|
||||
const bodyParser = require('body-parser')
|
||||
|
||||
const app = express()
|
||||
|
||||
// parse various different custom JSON types as JSON
|
||||
app.use(bodyParser.json({ type: 'application/*+json' }))
|
||||
|
||||
// parse some custom thing into a Buffer
|
||||
app.use(bodyParser.raw({ type: 'application/vnd.custom-type' }))
|
||||
|
||||
// parse an HTML body into a string
|
||||
app.use(bodyParser.text({ type: 'text/html' }))
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
[ci-image]: https://badgen.net/github/checks/expressjs/body-parser/master?label=ci
|
||||
[ci-url]: https://github.com/expressjs/body-parser/actions/workflows/ci.yml
|
||||
[coveralls-image]: https://badgen.net/coveralls/c/github/expressjs/body-parser/master
|
||||
[coveralls-url]: https://coveralls.io/r/expressjs/body-parser?branch=master
|
||||
[node-version-image]: https://badgen.net/npm/node/body-parser
|
||||
[node-version-url]: https://nodejs.org/en/download
|
||||
[npm-downloads-image]: https://badgen.net/npm/dm/body-parser
|
||||
[npm-url]: https://npmjs.org/package/body-parser
|
||||
[npm-version-image]: https://badgen.net/npm/v/body-parser
|
||||
[ossf-scorecard-badge]: https://api.scorecard.dev/projects/github.com/expressjs/body-parser/badge
|
||||
[ossf-scorecard-visualizer]: https://ossf.github.io/scorecard-visualizer/#/projects/github.com/expressjs/body-parser
|
||||
80
server/node_modules/body-parser/index.js
generated
vendored
Normal file
@ -0,0 +1,80 @@
|
||||
/*!
|
||||
* body-parser
|
||||
* Copyright(c) 2014-2015 Douglas Christopher Wilson
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* @typedef Parsers
|
||||
* @type {function}
|
||||
* @property {function} json
|
||||
* @property {function} raw
|
||||
* @property {function} text
|
||||
* @property {function} urlencoded
|
||||
*/
|
||||
|
||||
/**
|
||||
* Module exports.
|
||||
* @type {Parsers}
|
||||
*/
|
||||
|
||||
exports = module.exports = bodyParser
|
||||
|
||||
/**
|
||||
* JSON parser.
|
||||
* @public
|
||||
*/
|
||||
|
||||
Object.defineProperty(exports, 'json', {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: () => require('./lib/types/json')
|
||||
})
|
||||
|
||||
/**
|
||||
* Raw parser.
|
||||
* @public
|
||||
*/
|
||||
|
||||
Object.defineProperty(exports, 'raw', {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: () => require('./lib/types/raw')
|
||||
})
|
||||
|
||||
/**
|
||||
* Text parser.
|
||||
* @public
|
||||
*/
|
||||
|
||||
Object.defineProperty(exports, 'text', {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: () => require('./lib/types/text')
|
||||
})
|
||||
|
||||
/**
|
||||
* URL-encoded parser.
|
||||
* @public
|
||||
*/
|
||||
|
||||
Object.defineProperty(exports, 'urlencoded', {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: () => require('./lib/types/urlencoded')
|
||||
})
|
||||
|
||||
/**
|
||||
* Create a middleware to parse json and urlencoded bodies.
|
||||
*
|
||||
* @param {object} [options]
|
||||
* @return {function}
|
||||
* @deprecated
|
||||
* @public
|
||||
*/
|
||||
|
||||
function bodyParser () {
|
||||
throw new Error('The bodyParser() generic has been split into individual middleware to use instead.')
|
||||
}
|
||||
210
server/node_modules/body-parser/lib/read.js
generated
vendored
Normal file
@ -0,0 +1,210 @@
|
||||
/*!
|
||||
* body-parser
|
||||
* Copyright(c) 2014-2015 Douglas Christopher Wilson
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
* @private
|
||||
*/
|
||||
|
||||
var createError = require('http-errors')
|
||||
var getBody = require('raw-body')
|
||||
var iconv = require('iconv-lite')
|
||||
var onFinished = require('on-finished')
|
||||
var zlib = require('node:zlib')
|
||||
|
||||
/**
|
||||
* Module exports.
|
||||
*/
|
||||
|
||||
module.exports = read
|
||||
|
||||
/**
|
||||
* Read a request into a buffer and parse.
|
||||
*
|
||||
* @param {object} req
|
||||
* @param {object} res
|
||||
* @param {function} next
|
||||
* @param {function} parse
|
||||
* @param {function} debug
|
||||
* @param {object} options
|
||||
* @private
|
||||
*/
|
||||
|
||||
function read (req, res, next, parse, debug, options) {
|
||||
var length
|
||||
var opts = options
|
||||
var stream
|
||||
|
||||
// read options
|
||||
var encoding = opts.encoding !== null
|
||||
? opts.encoding
|
||||
: null
|
||||
var verify = opts.verify
|
||||
|
||||
try {
|
||||
// get the content stream
|
||||
stream = contentstream(req, debug, opts.inflate)
|
||||
length = stream.length
|
||||
stream.length = undefined
|
||||
} catch (err) {
|
||||
return next(err)
|
||||
}
|
||||
|
||||
// set raw-body options
|
||||
opts.length = length
|
||||
opts.encoding = verify
|
||||
? null
|
||||
: encoding
|
||||
|
||||
// assert charset is supported
|
||||
if (opts.encoding === null && encoding !== null && !iconv.encodingExists(encoding)) {
|
||||
return next(createError(415, 'unsupported charset "' + encoding.toUpperCase() + '"', {
|
||||
charset: encoding.toLowerCase(),
|
||||
type: 'charset.unsupported'
|
||||
}))
|
||||
}
|
||||
|
||||
// read body
|
||||
debug('read body')
|
||||
getBody(stream, opts, function (error, body) {
|
||||
if (error) {
|
||||
var _error
|
||||
|
||||
if (error.type === 'encoding.unsupported') {
|
||||
// echo back charset
|
||||
_error = createError(415, 'unsupported charset "' + encoding.toUpperCase() + '"', {
|
||||
charset: encoding.toLowerCase(),
|
||||
type: 'charset.unsupported'
|
||||
})
|
||||
} else {
|
||||
// set status code on error
|
||||
_error = createError(400, error)
|
||||
}
|
||||
|
||||
// unpipe from stream and destroy
|
||||
if (stream !== req) {
|
||||
req.unpipe()
|
||||
stream.destroy()
|
||||
}
|
||||
|
||||
// read off entire request
|
||||
dump(req, function onfinished () {
|
||||
next(createError(400, _error))
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// verify
|
||||
if (verify) {
|
||||
try {
|
||||
debug('verify body')
|
||||
verify(req, res, body, encoding)
|
||||
} catch (err) {
|
||||
next(createError(403, err, {
|
||||
body: body,
|
||||
type: err.type || 'entity.verify.failed'
|
||||
}))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// parse
|
||||
var str = body
|
||||
try {
|
||||
debug('parse body')
|
||||
str = typeof body !== 'string' && encoding !== null
|
||||
? iconv.decode(body, encoding)
|
||||
: body
|
||||
req.body = parse(str, encoding)
|
||||
} catch (err) {
|
||||
next(createError(400, err, {
|
||||
body: str,
|
||||
type: err.type || 'entity.parse.failed'
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content stream of the request.
|
||||
*
|
||||
* @param {object} req
|
||||
* @param {function} debug
|
||||
* @param {boolean} [inflate=true]
|
||||
* @return {object}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function contentstream (req, debug, inflate) {
|
||||
var encoding = (req.headers['content-encoding'] || 'identity').toLowerCase()
|
||||
var length = req.headers['content-length']
|
||||
|
||||
debug('content-encoding "%s"', encoding)
|
||||
|
||||
if (inflate === false && encoding !== 'identity') {
|
||||
throw createError(415, 'content encoding unsupported', {
|
||||
encoding: encoding,
|
||||
type: 'encoding.unsupported'
|
||||
})
|
||||
}
|
||||
|
||||
if (encoding === 'identity') {
|
||||
req.length = length
|
||||
return req
|
||||
}
|
||||
|
||||
var stream = createDecompressionStream(encoding, debug)
|
||||
req.pipe(stream)
|
||||
return stream
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a decompression stream for the given encoding.
|
||||
* @param {string} encoding
|
||||
* @param {function} debug
|
||||
* @return {object}
|
||||
* @api private
|
||||
*/
|
||||
function createDecompressionStream (encoding, debug) {
|
||||
switch (encoding) {
|
||||
case 'deflate':
|
||||
debug('inflate body')
|
||||
return zlib.createInflate()
|
||||
case 'gzip':
|
||||
debug('gunzip body')
|
||||
return zlib.createGunzip()
|
||||
case 'br':
|
||||
debug('brotli decompress body')
|
||||
return zlib.createBrotliDecompress()
|
||||
default:
|
||||
throw createError(415, 'unsupported content encoding "' + encoding + '"', {
|
||||
encoding: encoding,
|
||||
type: 'encoding.unsupported'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump the contents of a request.
|
||||
*
|
||||
* @param {object} req
|
||||
* @param {function} callback
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function dump (req, callback) {
|
||||
if (onFinished.isFinished(req)) {
|
||||
callback(null)
|
||||
} else {
|
||||
onFinished(req, callback)
|
||||
req.resume()
|
||||
}
|
||||
}
|
||||
206
server/node_modules/body-parser/lib/types/json.js
generated
vendored
Normal file
@ -0,0 +1,206 @@
|
||||
/*!
|
||||
* body-parser
|
||||
* Copyright(c) 2014 Jonathan Ong
|
||||
* Copyright(c) 2014-2015 Douglas Christopher Wilson
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
* @private
|
||||
*/
|
||||
|
||||
var createError = require('http-errors')
|
||||
var debug = require('debug')('body-parser:json')
|
||||
var isFinished = require('on-finished').isFinished
|
||||
var read = require('../read')
|
||||
var typeis = require('type-is')
|
||||
var { getCharset, normalizeOptions } = require('../utils')
|
||||
|
||||
/**
|
||||
* Module exports.
|
||||
*/
|
||||
|
||||
module.exports = json
|
||||
|
||||
/**
|
||||
* RegExp to match the first non-space in a string.
|
||||
*
|
||||
* Allowed whitespace is defined in RFC 7159:
|
||||
*
|
||||
* ws = *(
|
||||
* %x20 / ; Space
|
||||
* %x09 / ; Horizontal tab
|
||||
* %x0A / ; Line feed or New line
|
||||
* %x0D ) ; Carriage return
|
||||
*/
|
||||
|
||||
var FIRST_CHAR_REGEXP = /^[\x20\x09\x0a\x0d]*([^\x20\x09\x0a\x0d])/ // eslint-disable-line no-control-regex
|
||||
|
||||
var JSON_SYNTAX_CHAR = '#'
|
||||
var JSON_SYNTAX_REGEXP = /#+/g
|
||||
|
||||
/**
|
||||
* Create a middleware to parse JSON bodies.
|
||||
*
|
||||
* @param {object} [options]
|
||||
* @return {function}
|
||||
* @public
|
||||
*/
|
||||
|
||||
function json (options) {
|
||||
var { inflate, limit, verify, shouldParse } = normalizeOptions(options, 'application/json')
|
||||
|
||||
var reviver = options?.reviver
|
||||
var strict = options?.strict !== false
|
||||
|
||||
function parse (body) {
|
||||
if (body.length === 0) {
|
||||
// special-case empty json body, as it's a common client-side mistake
|
||||
// TODO: maybe make this configurable or part of "strict" option
|
||||
return {}
|
||||
}
|
||||
|
||||
if (strict) {
|
||||
var first = firstchar(body)
|
||||
|
||||
if (first !== '{' && first !== '[') {
|
||||
debug('strict violation')
|
||||
throw createStrictSyntaxError(body, first)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
debug('parse json')
|
||||
return JSON.parse(body, reviver)
|
||||
} catch (e) {
|
||||
throw normalizeJsonSyntaxError(e, {
|
||||
message: e.message,
|
||||
stack: e.stack
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return function jsonParser (req, res, next) {
|
||||
if (isFinished(req)) {
|
||||
debug('body already parsed')
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
if (!('body' in req)) {
|
||||
req.body = undefined
|
||||
}
|
||||
|
||||
// skip requests without bodies
|
||||
if (!typeis.hasBody(req)) {
|
||||
debug('skip empty body')
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
debug('content-type %j', req.headers['content-type'])
|
||||
|
||||
// determine if request should be parsed
|
||||
if (!shouldParse(req)) {
|
||||
debug('skip parsing')
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// assert charset per RFC 7159 sec 8.1
|
||||
var charset = getCharset(req) || 'utf-8'
|
||||
if (charset.slice(0, 4) !== 'utf-') {
|
||||
debug('invalid charset')
|
||||
next(createError(415, 'unsupported charset "' + charset.toUpperCase() + '"', {
|
||||
charset: charset,
|
||||
type: 'charset.unsupported'
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
// read
|
||||
read(req, res, next, parse, debug, {
|
||||
encoding: charset,
|
||||
inflate,
|
||||
limit,
|
||||
verify
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create strict violation syntax error matching native error.
|
||||
*
|
||||
* @param {string} str
|
||||
* @param {string} char
|
||||
* @return {Error}
|
||||
* @private
|
||||
*/
|
||||
|
||||
function createStrictSyntaxError (str, char) {
|
||||
var index = str.indexOf(char)
|
||||
var partial = ''
|
||||
|
||||
if (index !== -1) {
|
||||
partial = str.substring(0, index) + JSON_SYNTAX_CHAR
|
||||
|
||||
for (var i = index + 1; i < str.length; i++) {
|
||||
partial += JSON_SYNTAX_CHAR
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
JSON.parse(partial); /* istanbul ignore next */ throw new SyntaxError('strict violation')
|
||||
} catch (e) {
|
||||
return normalizeJsonSyntaxError(e, {
|
||||
message: e.message.replace(JSON_SYNTAX_REGEXP, function (placeholder) {
|
||||
return str.substring(index, index + placeholder.length)
|
||||
}),
|
||||
stack: e.stack
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first non-whitespace character in a string.
|
||||
*
|
||||
* @param {string} str
|
||||
* @return {function}
|
||||
* @private
|
||||
*/
|
||||
|
||||
function firstchar (str) {
|
||||
var match = FIRST_CHAR_REGEXP.exec(str)
|
||||
|
||||
return match
|
||||
? match[1]
|
||||
: undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a SyntaxError for JSON.parse.
|
||||
*
|
||||
* @param {SyntaxError} error
|
||||
* @param {object} obj
|
||||
* @return {SyntaxError}
|
||||
*/
|
||||
|
||||
function normalizeJsonSyntaxError (error, obj) {
|
||||
var keys = Object.getOwnPropertyNames(error)
|
||||
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var key = keys[i]
|
||||
if (key !== 'stack' && key !== 'message') {
|
||||
delete error[key]
|
||||
}
|
||||
}
|
||||
|
||||
// replace stack before message for Node.js 0.10 and below
|
||||
error.stack = obj.stack.replace(error.message, obj.message)
|
||||
error.message = obj.message
|
||||
|
||||
return error
|
||||
}
|
||||
75
server/node_modules/body-parser/lib/types/raw.js
generated
vendored
Normal file
@ -0,0 +1,75 @@
|
||||
/*!
|
||||
* body-parser
|
||||
* Copyright(c) 2014-2015 Douglas Christopher Wilson
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var debug = require('debug')('body-parser:raw')
|
||||
var isFinished = require('on-finished').isFinished
|
||||
var read = require('../read')
|
||||
var typeis = require('type-is')
|
||||
var { normalizeOptions } = require('../utils')
|
||||
|
||||
/**
|
||||
* Module exports.
|
||||
*/
|
||||
|
||||
module.exports = raw
|
||||
|
||||
/**
|
||||
* Create a middleware to parse raw bodies.
|
||||
*
|
||||
* @param {object} [options]
|
||||
* @return {function}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function raw (options) {
|
||||
var { inflate, limit, verify, shouldParse } = normalizeOptions(options, 'application/octet-stream')
|
||||
|
||||
function parse (buf) {
|
||||
return buf
|
||||
}
|
||||
|
||||
return function rawParser (req, res, next) {
|
||||
if (isFinished(req)) {
|
||||
debug('body already parsed')
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
if (!('body' in req)) {
|
||||
req.body = undefined
|
||||
}
|
||||
|
||||
// skip requests without bodies
|
||||
if (!typeis.hasBody(req)) {
|
||||
debug('skip empty body')
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
debug('content-type %j', req.headers['content-type'])
|
||||
|
||||
// determine if request should be parsed
|
||||
if (!shouldParse(req)) {
|
||||
debug('skip parsing')
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// read
|
||||
read(req, res, next, parse, debug, {
|
||||
encoding: null,
|
||||
inflate,
|
||||
limit,
|
||||
verify
|
||||
})
|
||||
}
|
||||
}
|
||||
80
server/node_modules/body-parser/lib/types/text.js
generated
vendored
Normal file
@ -0,0 +1,80 @@
|
||||
/*!
|
||||
* body-parser
|
||||
* Copyright(c) 2014-2015 Douglas Christopher Wilson
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var debug = require('debug')('body-parser:text')
|
||||
var isFinished = require('on-finished').isFinished
|
||||
var read = require('../read')
|
||||
var typeis = require('type-is')
|
||||
var { getCharset, normalizeOptions } = require('../utils')
|
||||
|
||||
/**
|
||||
* Module exports.
|
||||
*/
|
||||
|
||||
module.exports = text
|
||||
|
||||
/**
|
||||
* Create a middleware to parse text bodies.
|
||||
*
|
||||
* @param {object} [options]
|
||||
* @return {function}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function text (options) {
|
||||
var { inflate, limit, verify, shouldParse } = normalizeOptions(options, 'text/plain')
|
||||
|
||||
var defaultCharset = options?.defaultCharset || 'utf-8'
|
||||
|
||||
function parse (buf) {
|
||||
return buf
|
||||
}
|
||||
|
||||
return function textParser (req, res, next) {
|
||||
if (isFinished(req)) {
|
||||
debug('body already parsed')
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
if (!('body' in req)) {
|
||||
req.body = undefined
|
||||
}
|
||||
|
||||
// skip requests without bodies
|
||||
if (!typeis.hasBody(req)) {
|
||||
debug('skip empty body')
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
debug('content-type %j', req.headers['content-type'])
|
||||
|
||||
// determine if request should be parsed
|
||||
if (!shouldParse(req)) {
|
||||
debug('skip parsing')
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// get charset
|
||||
var charset = getCharset(req) || defaultCharset
|
||||
|
||||
// read
|
||||
read(req, res, next, parse, debug, {
|
||||
encoding: charset,
|
||||
inflate,
|
||||
limit,
|
||||
verify
|
||||
})
|
||||
}
|
||||
}
|
||||
177
server/node_modules/body-parser/lib/types/urlencoded.js
generated
vendored
Normal file
@ -0,0 +1,177 @@
|
||||
/*!
|
||||
* body-parser
|
||||
* Copyright(c) 2014 Jonathan Ong
|
||||
* Copyright(c) 2014-2015 Douglas Christopher Wilson
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
* @private
|
||||
*/
|
||||
|
||||
var createError = require('http-errors')
|
||||
var debug = require('debug')('body-parser:urlencoded')
|
||||
var isFinished = require('on-finished').isFinished
|
||||
var read = require('../read')
|
||||
var typeis = require('type-is')
|
||||
var qs = require('qs')
|
||||
var { getCharset, normalizeOptions } = require('../utils')
|
||||
|
||||
/**
|
||||
* Module exports.
|
||||
*/
|
||||
|
||||
module.exports = urlencoded
|
||||
|
||||
/**
|
||||
* Create a middleware to parse urlencoded bodies.
|
||||
*
|
||||
* @param {object} [options]
|
||||
* @return {function}
|
||||
* @public
|
||||
*/
|
||||
|
||||
function urlencoded (options) {
|
||||
var { inflate, limit, verify, shouldParse } = normalizeOptions(options, 'application/x-www-form-urlencoded')
|
||||
|
||||
var defaultCharset = options?.defaultCharset || 'utf-8'
|
||||
if (defaultCharset !== 'utf-8' && defaultCharset !== 'iso-8859-1') {
|
||||
throw new TypeError('option defaultCharset must be either utf-8 or iso-8859-1')
|
||||
}
|
||||
|
||||
// create the appropriate query parser
|
||||
var queryparse = createQueryParser(options)
|
||||
|
||||
function parse (body, encoding) {
|
||||
return body.length
|
||||
? queryparse(body, encoding)
|
||||
: {}
|
||||
}
|
||||
|
||||
return function urlencodedParser (req, res, next) {
|
||||
if (isFinished(req)) {
|
||||
debug('body already parsed')
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
if (!('body' in req)) {
|
||||
req.body = undefined
|
||||
}
|
||||
|
||||
// skip requests without bodies
|
||||
if (!typeis.hasBody(req)) {
|
||||
debug('skip empty body')
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
debug('content-type %j', req.headers['content-type'])
|
||||
|
||||
// determine if request should be parsed
|
||||
if (!shouldParse(req)) {
|
||||
debug('skip parsing')
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// assert charset
|
||||
var charset = getCharset(req) || defaultCharset
|
||||
if (charset !== 'utf-8' && charset !== 'iso-8859-1') {
|
||||
debug('invalid charset')
|
||||
next(createError(415, 'unsupported charset "' + charset.toUpperCase() + '"', {
|
||||
charset: charset,
|
||||
type: 'charset.unsupported'
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
// read
|
||||
read(req, res, next, parse, debug, {
|
||||
encoding: charset,
|
||||
inflate,
|
||||
limit,
|
||||
verify
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the extended query parser.
|
||||
*
|
||||
* @param {object} options
|
||||
*/
|
||||
|
||||
function createQueryParser (options) {
|
||||
var extended = Boolean(options?.extended)
|
||||
var parameterLimit = options?.parameterLimit !== undefined
|
||||
? options?.parameterLimit
|
||||
: 1000
|
||||
var charsetSentinel = options?.charsetSentinel
|
||||
var interpretNumericEntities = options?.interpretNumericEntities
|
||||
var depth = extended ? (options?.depth !== undefined ? options?.depth : 32) : 0
|
||||
|
||||
if (isNaN(parameterLimit) || parameterLimit < 1) {
|
||||
throw new TypeError('option parameterLimit must be a positive number')
|
||||
}
|
||||
|
||||
if (isNaN(depth) || depth < 0) {
|
||||
throw new TypeError('option depth must be a zero or a positive number')
|
||||
}
|
||||
|
||||
if (isFinite(parameterLimit)) {
|
||||
parameterLimit = parameterLimit | 0
|
||||
}
|
||||
|
||||
return function queryparse (body, encoding) {
|
||||
var paramCount = parameterCount(body, parameterLimit)
|
||||
|
||||
if (paramCount === undefined) {
|
||||
debug('too many parameters')
|
||||
throw createError(413, 'too many parameters', {
|
||||
type: 'parameters.too.many'
|
||||
})
|
||||
}
|
||||
|
||||
var arrayLimit = extended ? Math.max(100, paramCount) : 0
|
||||
|
||||
debug('parse ' + (extended ? 'extended ' : '') + 'urlencoding')
|
||||
try {
|
||||
return qs.parse(body, {
|
||||
allowPrototypes: true,
|
||||
arrayLimit: arrayLimit,
|
||||
depth: depth,
|
||||
charsetSentinel: charsetSentinel,
|
||||
interpretNumericEntities: interpretNumericEntities,
|
||||
charset: encoding,
|
||||
parameterLimit: parameterLimit,
|
||||
strictDepth: true
|
||||
})
|
||||
} catch (err) {
|
||||
if (err instanceof RangeError) {
|
||||
throw createError(400, 'The input exceeded the depth', {
|
||||
type: 'querystring.parse.rangeError'
|
||||
})
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of parameters, stopping once limit reached
|
||||
*
|
||||
* @param {string} body
|
||||
* @param {number} limit
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function parameterCount (body, limit) {
|
||||
var len = body.split('&').length
|
||||
|
||||
return len > limit ? undefined : len - 1
|
||||
}
|
||||
83
server/node_modules/body-parser/lib/utils.js
generated
vendored
Normal file
@ -0,0 +1,83 @@
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var bytes = require('bytes')
|
||||
var contentType = require('content-type')
|
||||
var typeis = require('type-is')
|
||||
|
||||
/**
|
||||
* Module exports.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
getCharset,
|
||||
normalizeOptions
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the charset of a request.
|
||||
*
|
||||
* @param {object} req
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function getCharset (req) {
|
||||
try {
|
||||
return (contentType.parse(req).parameters.charset || '').toLowerCase()
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the simple type checker.
|
||||
*
|
||||
* @param {string | string[]} type
|
||||
* @return {function}
|
||||
*/
|
||||
|
||||
function typeChecker (type) {
|
||||
return function checkType (req) {
|
||||
return Boolean(typeis(req, type))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the common options for all parsers.
|
||||
*
|
||||
* @param {object} options options to normalize
|
||||
* @param {string | string[] | function} defaultType default content type(s) or a function to determine it
|
||||
* @returns {object}
|
||||
*/
|
||||
function normalizeOptions (options, defaultType) {
|
||||
if (!defaultType) {
|
||||
// Parsers must define a default content type
|
||||
throw new TypeError('defaultType must be provided')
|
||||
}
|
||||
|
||||
var inflate = options?.inflate !== false
|
||||
var limit = typeof options?.limit !== 'number'
|
||||
? bytes.parse(options?.limit || '100kb')
|
||||
: options?.limit
|
||||
var type = options?.type || defaultType
|
||||
var verify = options?.verify || false
|
||||
|
||||
if (verify !== false && typeof verify !== 'function') {
|
||||
throw new TypeError('option verify must be function')
|
||||
}
|
||||
|
||||
// create the appropriate type checking function
|
||||
var shouldParse = typeof type !== 'function'
|
||||
? typeChecker(type)
|
||||
: type
|
||||
|
||||
return {
|
||||
inflate,
|
||||
limit,
|
||||
verify,
|
||||
shouldParse
|
||||
}
|
||||
}
|
||||
49
server/node_modules/body-parser/package.json
generated
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "body-parser",
|
||||
"description": "Node.js body parsing middleware",
|
||||
"version": "2.2.0",
|
||||
"contributors": [
|
||||
"Douglas Christopher Wilson <doug@somethingdoug.com>",
|
||||
"Jonathan Ong <me@jongleberry.com> (http://jongleberry.com)"
|
||||
],
|
||||
"license": "MIT",
|
||||
"repository": "expressjs/body-parser",
|
||||
"dependencies": {
|
||||
"bytes": "^3.1.2",
|
||||
"content-type": "^1.0.5",
|
||||
"debug": "^4.4.0",
|
||||
"http-errors": "^2.0.0",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"on-finished": "^2.4.1",
|
||||
"qs": "^6.14.0",
|
||||
"raw-body": "^3.0.0",
|
||||
"type-is": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "8.34.0",
|
||||
"eslint-config-standard": "14.1.1",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-markdown": "3.0.0",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"eslint-plugin-promise": "6.1.1",
|
||||
"eslint-plugin-standard": "4.1.0",
|
||||
"mocha": "^11.1.0",
|
||||
"nyc": "^17.1.0",
|
||||
"supertest": "^7.0.0"
|
||||
},
|
||||
"files": [
|
||||
"lib/",
|
||||
"LICENSE",
|
||||
"HISTORY.md",
|
||||
"index.js"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"test": "mocha --reporter spec --check-leaks test/",
|
||||
"test-ci": "nyc --reporter=lcovonly --reporter=text npm test",
|
||||
"test-cov": "nyc --reporter=html --reporter=text npm test"
|
||||
}
|
||||
}
|
||||
97
server/node_modules/bytes/History.md
generated
vendored
Normal file
@ -0,0 +1,97 @@
|
||||
3.1.2 / 2022-01-27
|
||||
==================
|
||||
|
||||
* Fix return value for un-parsable strings
|
||||
|
||||
3.1.1 / 2021-11-15
|
||||
==================
|
||||
|
||||
* Fix "thousandsSeparator" incorrecting formatting fractional part
|
||||
|
||||
3.1.0 / 2019-01-22
|
||||
==================
|
||||
|
||||
* Add petabyte (`pb`) support
|
||||
|
||||
3.0.0 / 2017-08-31
|
||||
==================
|
||||
|
||||
* Change "kB" to "KB" in format output
|
||||
* Remove support for Node.js 0.6
|
||||
* Remove support for ComponentJS
|
||||
|
||||
2.5.0 / 2017-03-24
|
||||
==================
|
||||
|
||||
* Add option "unit"
|
||||
|
||||
2.4.0 / 2016-06-01
|
||||
==================
|
||||
|
||||
* Add option "unitSeparator"
|
||||
|
||||
2.3.0 / 2016-02-15
|
||||
==================
|
||||
|
||||
* Drop partial bytes on all parsed units
|
||||
* Fix non-finite numbers to `.format` to return `null`
|
||||
* Fix parsing byte string that looks like hex
|
||||
* perf: hoist regular expressions
|
||||
|
||||
2.2.0 / 2015-11-13
|
||||
==================
|
||||
|
||||
* add option "decimalPlaces"
|
||||
* add option "fixedDecimals"
|
||||
|
||||
2.1.0 / 2015-05-21
|
||||
==================
|
||||
|
||||
* add `.format` export
|
||||
* add `.parse` export
|
||||
|
||||
2.0.2 / 2015-05-20
|
||||
==================
|
||||
|
||||
* remove map recreation
|
||||
* remove unnecessary object construction
|
||||
|
||||
2.0.1 / 2015-05-07
|
||||
==================
|
||||
|
||||
* fix browserify require
|
||||
* remove node.extend dependency
|
||||
|
||||
2.0.0 / 2015-04-12
|
||||
==================
|
||||
|
||||
* add option "case"
|
||||
* add option "thousandsSeparator"
|
||||
* return "null" on invalid parse input
|
||||
* support proper round-trip: bytes(bytes(num)) === num
|
||||
* units no longer case sensitive when parsing
|
||||
|
||||
1.0.0 / 2014-05-05
|
||||
==================
|
||||
|
||||
* add negative support. fixes #6
|
||||
|
||||
0.3.0 / 2014-03-19
|
||||
==================
|
||||
|
||||
* added terabyte support
|
||||
|
||||
0.2.1 / 2013-04-01
|
||||
==================
|
||||
|
||||
* add .component
|
||||
|
||||
0.2.0 / 2012-10-28
|
||||
==================
|
||||
|
||||
* bytes(200).should.eql('200b')
|
||||
|
||||
0.1.0 / 2012-07-04
|
||||
==================
|
||||
|
||||
* add bytes to string conversion [yields]
|
||||
23
server/node_modules/bytes/LICENSE
generated
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2012-2014 TJ Holowaychuk <tj@vision-media.ca>
|
||||
Copyright (c) 2015 Jed Watson <jed.watson@me.com>
|
||||
|
||||
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.
|
||||
152
server/node_modules/bytes/Readme.md
generated
vendored
Normal file
@ -0,0 +1,152 @@
|
||||
# Bytes utility
|
||||
|
||||
[![NPM Version][npm-image]][npm-url]
|
||||
[![NPM Downloads][downloads-image]][downloads-url]
|
||||
[![Build Status][ci-image]][ci-url]
|
||||
[![Test Coverage][coveralls-image]][coveralls-url]
|
||||
|
||||
Utility to parse a string bytes (ex: `1TB`) to bytes (`1099511627776`) and vice-versa.
|
||||
|
||||
## Installation
|
||||
|
||||
This is a [Node.js](https://nodejs.org/en/) module available through the
|
||||
[npm registry](https://www.npmjs.com/). Installation is done using the
|
||||
[`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally):
|
||||
|
||||
```bash
|
||||
$ npm install bytes
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
var bytes = require('bytes');
|
||||
```
|
||||
|
||||
#### bytes(number|string value, [options]): number|string|null
|
||||
|
||||
Default export function. Delegates to either `bytes.format` or `bytes.parse` based on the type of `value`.
|
||||
|
||||
**Arguments**
|
||||
|
||||
| Name | Type | Description |
|
||||
|---------|----------|--------------------|
|
||||
| value | `number`|`string` | Number value to format or string value to parse |
|
||||
| options | `Object` | Conversion options for `format` |
|
||||
|
||||
**Returns**
|
||||
|
||||
| Name | Type | Description |
|
||||
|---------|------------------|-------------------------------------------------|
|
||||
| results | `string`|`number`|`null` | Return null upon error. Numeric value in bytes, or string value otherwise. |
|
||||
|
||||
**Example**
|
||||
|
||||
```js
|
||||
bytes(1024);
|
||||
// output: '1KB'
|
||||
|
||||
bytes('1KB');
|
||||
// output: 1024
|
||||
```
|
||||
|
||||
#### bytes.format(number value, [options]): string|null
|
||||
|
||||
Format the given value in bytes into a string. If the value is negative, it is kept as such. If it is a float, it is
|
||||
rounded.
|
||||
|
||||
**Arguments**
|
||||
|
||||
| Name | Type | Description |
|
||||
|---------|----------|--------------------|
|
||||
| value | `number` | Value in bytes |
|
||||
| options | `Object` | Conversion options |
|
||||
|
||||
**Options**
|
||||
|
||||
| Property | Type | Description |
|
||||
|-------------------|--------|-----------------------------------------------------------------------------------------|
|
||||
| decimalPlaces | `number`|`null` | Maximum number of decimal places to include in output. Default value to `2`. |
|
||||
| fixedDecimals | `boolean`|`null` | Whether to always display the maximum number of decimal places. Default value to `false` |
|
||||
| thousandsSeparator | `string`|`null` | Example of values: `' '`, `','` and `'.'`... Default value to `''`. |
|
||||
| unit | `string`|`null` | The unit in which the result will be returned (B/KB/MB/GB/TB). Default value to `''` (which means auto detect). |
|
||||
| unitSeparator | `string`|`null` | Separator to use between number and unit. Default value to `''`. |
|
||||
|
||||
**Returns**
|
||||
|
||||
| Name | Type | Description |
|
||||
|---------|------------------|-------------------------------------------------|
|
||||
| results | `string`|`null` | Return null upon error. String value otherwise. |
|
||||
|
||||
**Example**
|
||||
|
||||
```js
|
||||
bytes.format(1024);
|
||||
// output: '1KB'
|
||||
|
||||
bytes.format(1000);
|
||||
// output: '1000B'
|
||||
|
||||
bytes.format(1000, {thousandsSeparator: ' '});
|
||||
// output: '1 000B'
|
||||
|
||||
bytes.format(1024 * 1.7, {decimalPlaces: 0});
|
||||
// output: '2KB'
|
||||
|
||||
bytes.format(1024, {unitSeparator: ' '});
|
||||
// output: '1 KB'
|
||||
```
|
||||
|
||||
#### bytes.parse(string|number value): number|null
|
||||
|
||||
Parse the string value into an integer in bytes. If no unit is given, or `value`
|
||||
is a number, it is assumed the value is in bytes.
|
||||
|
||||
Supported units and abbreviations are as follows and are case-insensitive:
|
||||
|
||||
* `b` for bytes
|
||||
* `kb` for kilobytes
|
||||
* `mb` for megabytes
|
||||
* `gb` for gigabytes
|
||||
* `tb` for terabytes
|
||||
* `pb` for petabytes
|
||||
|
||||
The units are in powers of two, not ten. This means 1kb = 1024b according to this parser.
|
||||
|
||||
**Arguments**
|
||||
|
||||
| Name | Type | Description |
|
||||
|---------------|--------|--------------------|
|
||||
| value | `string`|`number` | String to parse, or number in bytes. |
|
||||
|
||||
**Returns**
|
||||
|
||||
| Name | Type | Description |
|
||||
|---------|-------------|-------------------------|
|
||||
| results | `number`|`null` | Return null upon error. Value in bytes otherwise. |
|
||||
|
||||
**Example**
|
||||
|
||||
```js
|
||||
bytes.parse('1KB');
|
||||
// output: 1024
|
||||
|
||||
bytes.parse('1024');
|
||||
// output: 1024
|
||||
|
||||
bytes.parse(1024);
|
||||
// output: 1024
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
[ci-image]: https://badgen.net/github/checks/visionmedia/bytes.js/master?label=ci
|
||||
[ci-url]: https://github.com/visionmedia/bytes.js/actions?query=workflow%3Aci
|
||||
[coveralls-image]: https://badgen.net/coveralls/c/github/visionmedia/bytes.js/master
|
||||
[coveralls-url]: https://coveralls.io/r/visionmedia/bytes.js?branch=master
|
||||
[downloads-image]: https://badgen.net/npm/dm/bytes
|
||||
[downloads-url]: https://npmjs.org/package/bytes
|
||||
[npm-image]: https://badgen.net/npm/v/bytes
|
||||
[npm-url]: https://npmjs.org/package/bytes
|
||||
170
server/node_modules/bytes/index.js
generated
vendored
Normal file
@ -0,0 +1,170 @@
|
||||
/*!
|
||||
* bytes
|
||||
* Copyright(c) 2012-2014 TJ Holowaychuk
|
||||
* Copyright(c) 2015 Jed Watson
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Module exports.
|
||||
* @public
|
||||
*/
|
||||
|
||||
module.exports = bytes;
|
||||
module.exports.format = format;
|
||||
module.exports.parse = parse;
|
||||
|
||||
/**
|
||||
* Module variables.
|
||||
* @private
|
||||
*/
|
||||
|
||||
var formatThousandsRegExp = /\B(?=(\d{3})+(?!\d))/g;
|
||||
|
||||
var formatDecimalsRegExp = /(?:\.0*|(\.[^0]+)0+)$/;
|
||||
|
||||
var map = {
|
||||
b: 1,
|
||||
kb: 1 << 10,
|
||||
mb: 1 << 20,
|
||||
gb: 1 << 30,
|
||||
tb: Math.pow(1024, 4),
|
||||
pb: Math.pow(1024, 5),
|
||||
};
|
||||
|
||||
var parseRegExp = /^((-|\+)?(\d+(?:\.\d+)?)) *(kb|mb|gb|tb|pb)$/i;
|
||||
|
||||
/**
|
||||
* Convert the given value in bytes into a string or parse to string to an integer in bytes.
|
||||
*
|
||||
* @param {string|number} value
|
||||
* @param {{
|
||||
* case: [string],
|
||||
* decimalPlaces: [number]
|
||||
* fixedDecimals: [boolean]
|
||||
* thousandsSeparator: [string]
|
||||
* unitSeparator: [string]
|
||||
* }} [options] bytes options.
|
||||
*
|
||||
* @returns {string|number|null}
|
||||
*/
|
||||
|
||||
function bytes(value, options) {
|
||||
if (typeof value === 'string') {
|
||||
return parse(value);
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
return format(value, options);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the given value in bytes into a string.
|
||||
*
|
||||
* If the value is negative, it is kept as such. If it is a float,
|
||||
* it is rounded.
|
||||
*
|
||||
* @param {number} value
|
||||
* @param {object} [options]
|
||||
* @param {number} [options.decimalPlaces=2]
|
||||
* @param {number} [options.fixedDecimals=false]
|
||||
* @param {string} [options.thousandsSeparator=]
|
||||
* @param {string} [options.unit=]
|
||||
* @param {string} [options.unitSeparator=]
|
||||
*
|
||||
* @returns {string|null}
|
||||
* @public
|
||||
*/
|
||||
|
||||
function format(value, options) {
|
||||
if (!Number.isFinite(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var mag = Math.abs(value);
|
||||
var thousandsSeparator = (options && options.thousandsSeparator) || '';
|
||||
var unitSeparator = (options && options.unitSeparator) || '';
|
||||
var decimalPlaces = (options && options.decimalPlaces !== undefined) ? options.decimalPlaces : 2;
|
||||
var fixedDecimals = Boolean(options && options.fixedDecimals);
|
||||
var unit = (options && options.unit) || '';
|
||||
|
||||
if (!unit || !map[unit.toLowerCase()]) {
|
||||
if (mag >= map.pb) {
|
||||
unit = 'PB';
|
||||
} else if (mag >= map.tb) {
|
||||
unit = 'TB';
|
||||
} else if (mag >= map.gb) {
|
||||
unit = 'GB';
|
||||
} else if (mag >= map.mb) {
|
||||
unit = 'MB';
|
||||
} else if (mag >= map.kb) {
|
||||
unit = 'KB';
|
||||
} else {
|
||||
unit = 'B';
|
||||
}
|
||||
}
|
||||
|
||||
var val = value / map[unit.toLowerCase()];
|
||||
var str = val.toFixed(decimalPlaces);
|
||||
|
||||
if (!fixedDecimals) {
|
||||
str = str.replace(formatDecimalsRegExp, '$1');
|
||||
}
|
||||
|
||||
if (thousandsSeparator) {
|
||||
str = str.split('.').map(function (s, i) {
|
||||
return i === 0
|
||||
? s.replace(formatThousandsRegExp, thousandsSeparator)
|
||||
: s
|
||||
}).join('.');
|
||||
}
|
||||
|
||||
return str + unitSeparator + unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the string value into an integer in bytes.
|
||||
*
|
||||
* If no unit is given, it is assumed the value is in bytes.
|
||||
*
|
||||
* @param {number|string} val
|
||||
*
|
||||
* @returns {number|null}
|
||||
* @public
|
||||
*/
|
||||
|
||||
function parse(val) {
|
||||
if (typeof val === 'number' && !isNaN(val)) {
|
||||
return val;
|
||||
}
|
||||
|
||||
if (typeof val !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Test if the string passed is valid
|
||||
var results = parseRegExp.exec(val);
|
||||
var floatValue;
|
||||
var unit = 'b';
|
||||
|
||||
if (!results) {
|
||||
// Nothing could be extracted from the given string
|
||||
floatValue = parseInt(val, 10);
|
||||
unit = 'b'
|
||||
} else {
|
||||
// Retrieve the value and the unit
|
||||
floatValue = parseFloat(results[1]);
|
||||
unit = results[4].toLowerCase();
|
||||
}
|
||||
|
||||
if (isNaN(floatValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Math.floor(map[unit] * floatValue);
|
||||
}
|
||||
42
server/node_modules/bytes/package.json
generated
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "bytes",
|
||||
"description": "Utility to parse a string bytes to bytes and vice-versa",
|
||||
"version": "3.1.2",
|
||||
"author": "TJ Holowaychuk <tj@vision-media.ca> (http://tjholowaychuk.com)",
|
||||
"contributors": [
|
||||
"Jed Watson <jed.watson@me.com>",
|
||||
"Théo FIDRY <theo.fidry@gmail.com>"
|
||||
],
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"byte",
|
||||
"bytes",
|
||||
"utility",
|
||||
"parse",
|
||||
"parser",
|
||||
"convert",
|
||||
"converter"
|
||||
],
|
||||
"repository": "visionmedia/bytes.js",
|
||||
"devDependencies": {
|
||||
"eslint": "7.32.0",
|
||||
"eslint-plugin-markdown": "2.2.1",
|
||||
"mocha": "9.2.0",
|
||||
"nyc": "15.1.0"
|
||||
},
|
||||
"files": [
|
||||
"History.md",
|
||||
"LICENSE",
|
||||
"Readme.md",
|
||||
"index.js"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"test": "mocha --check-leaks --reporter spec",
|
||||
"test-ci": "nyc --reporter=lcov --reporter=text npm test",
|
||||
"test-cov": "nyc --reporter=html --reporter=text npm test"
|
||||
}
|
||||
}
|
||||
17
server/node_modules/call-bind-apply-helpers/.eslintrc
generated
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"root": true,
|
||||
|
||||
"extends": "@ljharb",
|
||||
|
||||
"rules": {
|
||||
"func-name-matching": 0,
|
||||
"id-length": 0,
|
||||
"new-cap": [2, {
|
||||
"capIsNewExceptions": [
|
||||
"GetIntrinsic",
|
||||
],
|
||||
}],
|
||||
"no-extra-parens": 0,
|
||||
"no-magic-numbers": 0,
|
||||
},
|
||||
}
|
||||
12
server/node_modules/call-bind-apply-helpers/.github/FUNDING.yml
generated
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [ljharb]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: npm/call-bind-apply-helpers
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||