first commit

This commit is contained in:
aleks 2025-08-17 15:38:51 +00:00
commit 4853efaf8f
1153 changed files with 152179 additions and 0 deletions

29
colors Executable file
View 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
View 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
View 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
View 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
View 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
View 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>
&nbsp;
# dotenv [![NPM version](https://img.shields.io/npm/v/dotenv.svg?style=flat-square)](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).
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard)
[![LICENSE](https://img.shields.io/github/license/motdotla/dotenv.svg)](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
View 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>
&nbsp;
# dotenv [![NPM version](https://img.shields.io/npm/v/dotenv.svg?style=flat-square)](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.
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard)
[![LICENSE](https://img.shields.io/github/license/motdotla/dotenv.svg)](LICENSE)
[![codecov](https://codecov.io/gh/motdotla/dotenv-expand/graph/badge.svg?token=pawWEyaMfg)](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
View File

@ -0,0 +1 @@
Please report any security vulnerabilities to security@dotenvx.com.

1
node_modules/dotenv/config.d.ts generated vendored Normal file
View File

@ -0,0 +1 @@
export {};

9
node_modules/dotenv/config.js generated vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,86 @@
# Nodemailer
[![Nodemailer](https://raw.githubusercontent.com/nodemailer/nodemailer/master/assets/nm_logo_200x136.png)](https://nodemailer.com/about/)
Send emails from Node.js easy as cake! 🍰✉️
[![NPM](https://nodei.co/npm/nodemailer.png?downloads=true&downloadRank=true&stars=true)](https://nodemailer.com/about/)
See [nodemailer.com](https://nodemailer.com/) for documentation and terms.
> [!TIP]
> Check out **[EmailEngine](https://emailengine.app/?utm_source=github-nodemailer&utm_campaign=nodemailer&utm_medium=readme-link)** a self-hosted email gateway that allows making **REST requests against IMAP and SMTP servers**. EmailEngine also sends webhooks whenever something changes on the registered accounts.\
> \
> Using the email accounts registered with EmailEngine, you can receive and [send emails](https://emailengine.app/sending-emails?utm_source=github-nodemailer&utm_campaign=nodemailer&utm_medium=readme-link). EmailEngine supports OAuth2, delayed sends, opens and clicks tracking, bounce detection, etc. All on top of regular email accounts without an external MTA service.
## Having an issue?
#### First review the docs
Documentation for Nodemailer can be found at [nodemailer.com](https://nodemailer.com/about/).
#### Nodemailer throws a SyntaxError for "..."
You are using an older Node.js version than v6.0. Upgrade Node.js to get support for the spread operator. Nodemailer supports all Node.js versions starting from Node.js@v6.0.0.
#### I'm having issues with Gmail
Gmail either works well, or it does not work at all. It is probably easier to switch to an alternative service instead of fixing issues with Gmail. If Gmail does not work for you, then don't use it. Read more about it [here](https://nodemailer.com/usage/using-gmail/).
#### I get ETIMEDOUT errors
Check your firewall settings. Timeout usually occurs when you try to open a connection to a firewalled port either on the server or on your machine. Some ISPs also block email ports to prevent spamming.
#### Nodemailer works on one machine but not in another
It's either a firewall issue, or your SMTP server blocks authentication attempts from some servers.
#### I get TLS errors
- If you are running the code on your machine, check your antivirus settings. Antiviruses often mess around with email ports usage. Node.js might not recognize the MITM cert your antivirus is using.
- Latest Node versions allow only TLS versions 1.2 and higher. Some servers might still use TLS 1.1 or lower. Check Node.js docs on how to get correct TLS support for your app. You can change this with [tls.minVersion](https://nodejs.org/dist/latest-v16.x/docs/api/tls.html#tls_tls_createsecurecontext_options) option
- You might have the wrong value for the `secure` option. This should be set to `true` only for port 465. For every other port, it should be `false`. Setting it to `false` does not mean that Nodemailer would not use TLS. Nodemailer would still try to upgrade the connection to use TLS if the server supports it.
- Older Node versions do not fully support the certificate chain of the newest Let's Encrypt certificates. Either set [tls.rejectUnauthorized](https://nodejs.org/dist/latest-v16.x/docs/api/tls.html#tlsconnectoptions-callback) to `false` to skip chain verification or upgrade your Node version
```js
let configOptions = {
host: "smtp.example.com",
port: 587,
tls: {
rejectUnauthorized: true,
minVersion: "TLSv1.2"
}
}
```
#### I have issues with DNS / hosts file
Node.js uses [c-ares](https://nodejs.org/en/docs/meta/topics/dependencies/#c-ares) to resolve domain names, not the DNS library provided by the system, so if you have some custom DNS routing set up, it might be ignored. Nodemailer runs [dns.resolve4()](https://nodejs.org/dist/latest-v16.x/docs/api/dns.html#dnsresolve4hostname-options-callback) and [dns.resolve6()](https://nodejs.org/dist/latest-v16.x/docs/api/dns.html#dnsresolve6hostname-options-callback) to resolve hostname into an IP address. If both calls fail, then Nodemailer will fall back to [dns.lookup()](https://nodejs.org/dist/latest-v16.x/docs/api/dns.html#dnslookuphostname-options-callback). If this does not work for you, you can hard code the IP address into the configuration like shown below. In that case, Nodemailer would not perform any DNS lookups.
```js
let configOptions = {
host: "1.2.3.4",
port: 465,
secure: true,
tls: {
// must provide server name, otherwise TLS certificate check will fail
servername: "example.com"
}
}
```
#### I have an issue with TypeScript types
Nodemailer has official support for Node.js only. For anything related to TypeScript, you need to directly contact the authors of the [type definitions](https://www.npmjs.com/package/@types/nodemailer).
#### I have a different problem
If you are having issues with Nodemailer, then the best way to find help would be [Stack Overflow](https://stackoverflow.com/search?q=nodemailer) or revisit the [docs](https://nodemailer.com/about/).
### License
Nodemailer is licensed under the **MIT No Attribution license**
---
The Nodemailer logo was designed by [Sven Kristjansen](https://www.behance.net/kristjansen).

22
node_modules/nodemailer/SECURITY.txt generated vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

1314
node_modules/nodemailer/lib/mime-node/index.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

33
node_modules/nodemailer/lib/mime-node/last-newline.js generated vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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;
}

View 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;

View 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

File diff suppressed because it is too large Load Diff

652
node_modules/nodemailer/lib/smtp-pool/index.js generated vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,6 @@
{
"dependencies": {
"dotenv": "^17.2.1",
"nodemailer": "^7.0.5"
}
}

View 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
View 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
View 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>&lsaquo;</a>', '<span class="datepicker__title"><%= renderMonthSelect() %></span>', '<span class="datepicker__title"><%= renderYearSelect() %></span>', '<a class="datepicker__next<%= (hasNext) ? "" : " is-disabled" %>" data-next>&rsaquo;</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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

316
public/getData.js Executable file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
public/img/arrow-right.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
public/img/back.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
public/img/delete.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

BIN
public/img/done.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
public/img/info.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

117
public/index.html Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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 `&#9786;` 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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(numberstring value, [options]): numberstringnull
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]): stringnull
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(stringnumber value): numbernull
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
View 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
View 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
View 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,
},
}

View 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']

Some files were not shown because too many files have changed in this diff Show More